commit 90e1244aa3a728acf90c198f353421582b8fe1f6 Author: Jeff Emmett Date: Sat Feb 21 17:42:21 2026 -0700 feat: Initial ZK-Glasses project — IR anti-surveillance eyewear Complete hardware design for glasses with flush-mounted 940nm IR LEDs that are invisible to human eyes but overwhelm camera sensors: - OpenSCAD parametric models (stealth integrated frame + snap-on clip) - SMD LEDs behind IR-pass filter strips (Wratten 87C style) - ATtiny85 PWM firmware (6 modes, brightness control, auto-off) - SVG circuit schematic (dark-themed, full driver circuit) - 10 rendered preview images (hero, front, cross-section, camera view) - KiCad project shell for future PCB layout Co-Authored-By: Claude Opus 4.6 diff --git a/firmware/zk_glasses.ino b/firmware/zk_glasses.ino new file mode 100644 index 0000000..b110ab7 --- /dev/null +++ b/firmware/zk_glasses.ino @@ -0,0 +1,372 @@ +/* + * ╔═══════════════════════════════════════════════════════════╗ + * ║ ZK-Glasses — ATtiny85 IR LED Controller Firmware ║ + * ╠═══════════════════════════════════════════════════════════╣ + * ║ Board: ATtiny85 (ATTinyCore, 8 MHz internal) ║ + * ║ Programmer: USBasp / Arduino as ISP ║ + * ║ License: MIT ║ + * ╚═══════════════════════════════════════════════════════════╝ + * + * Pin Assignment (DIP-8): + * ┌────────────────────────────┐ + * │ ATtiny85 │ + * │ (RST) PB5 ─┤1 8├─ VCC │ + * │ (ADC) PB3 ─┤2 7├─ PB2 (BTN) + * │ (OC1B)PB4 ─┤3 6├─ PB1 (LED_R) OC0B + * │ GND ─┤4 5├─ PB0 (LED_L) OC0A + * └────────────────────────────┘ + * + * PB0 - LED group LEFT (8 IR LEDs via N-MOSFET gate) + * PB1 - LED group RIGHT (8 IR LEDs via N-MOSFET gate) + * PB2 - Tactile button (internal pull-up, active LOW) + * PB3 - Status LED (green, current-limited) + * PB4 - Battery voltage sense (ADC2, voltage divider) + * + * Modes: + * 0 OFF — LEDs off, MCU in low-power idle + * 1 CONSTANT — Full brightness, steady + * 2 PULSE_30HZ — 30 Hz square wave (half power draw) + * 3 PULSE_60HZ — 60 Hz, matches common camera fps + * 4 RANDOM_FLICKER — Pseudorandom on/off, defeats adaptive filters + * 5 STEALTH — 25% duty constant (very low visibility) + * + * Controls: + * Short press (<500ms) — Cycle to next mode + * Long press (>800ms) — Cycle brightness (25→50→75→100%) + * Double press (<300ms) — Instant OFF + * + * Features: + * - Auto-off after 2 hours (configurable) + * - Low battery warning: status LED blinks when Vcc < 3.3V + * - All timing via Timer0 (no delay() in main loop) + */ + +#include +#include +#include +#include + +// ── Pin Definitions ────────────────────────────────────────── +#define PIN_LED_L PB0 // Left IR LED group (OC0A) +#define PIN_LED_R PB1 // Right IR LED group (OC0B) +#define PIN_BTN PB2 // Button input +#define PIN_STATUS PB3 // Status LED +#define PIN_VBAT PB4 // Battery ADC (ADC2) + +// ── Configuration ──────────────────────────────────────────── +#define NUM_MODES 6 +#define DEBOUNCE_MS 40 +#define SHORT_PRESS_MAX 500 // ms — below this = short press +#define LONG_PRESS_MIN 800 // ms — above this = long press +#define DOUBLE_PRESS_MAX 300 // ms — gap for double-press detection +#define AUTO_OFF_MS 7200000UL // 2 hours in ms +#define LOW_BATT_ADC 175 // ~3.3V via voltage divider (3.3/5*1024*R2/(R1+R2)) +#define BATT_CHECK_INTERVAL 10000 // Check battery every 10s + +// ── Brightness Levels (PWM duty 0–255) ─────────────────────── +const uint8_t BRIGHTNESS_LEVELS[] = { 64, 128, 192, 255 }; +#define NUM_BRIGHTNESS 4 + +// ── Mode Enumeration ───────────────────────────────────────── +enum Mode : uint8_t { + MODE_OFF = 0, + MODE_CONSTANT = 1, + MODE_PULSE_30HZ = 2, + MODE_PULSE_60HZ = 3, + MODE_RANDOM = 4, + MODE_STEALTH = 5 +}; + +// ── State ──────────────────────────────────────────────────── +volatile uint8_t g_mode = MODE_OFF; +volatile uint8_t g_brightness_idx = 3; // Start at full +volatile uint32_t g_millis = 0; +volatile bool g_btn_pressed = false; + +uint32_t g_mode_start_ms = 0; // For auto-off timer +uint32_t g_last_batt_check = 0; +bool g_low_battery = false; +uint16_t g_lfsr = 0xACE1; // LFSR seed for random mode + +// ── Millisecond Timer (Timer0 overflow at 8MHz/64/256 ≈ 488Hz) ── +// We'll use Timer1 for millis since Timer0 is used for PWM +// Actually, let's use a simple millis implementation with Timer0 +// Timer0 is set up for Fast PWM on OC0A/OC0B + +// We'll track time using Timer1 overflow +ISR(TIMER1_OVF_vect) { + // Timer1 at 8MHz/64, 8-bit overflow = every 2.048ms + // We'll count overflows for rough millisecond tracking + g_millis += 2; +} + +// ── LFSR Pseudorandom (16-bit Galois) ──────────────────────── +uint16_t lfsr_next() { + uint16_t bit = ((g_lfsr >> 0) ^ (g_lfsr >> 2) ^ + (g_lfsr >> 3) ^ (g_lfsr >> 5)) & 1u; + g_lfsr = (g_lfsr >> 1) | (bit << 15); + return g_lfsr; +} + +// ── ADC Read (blocking, 10-bit) ────────────────────────────── +uint16_t read_adc(uint8_t channel) { + ADMUX = channel; // Vcc reference, selected channel + ADCSRA = (1 << ADEN) | (1 << ADSC) | // Enable, start conversion + (1 << ADPS2) | (1 << ADPS1); // Prescaler /64 + while (ADCSRA & (1 << ADSC)); // Wait for completion + uint16_t result = ADC; + ADCSRA &= ~(1 << ADEN); // Disable ADC to save power + return result; +} + +// ── Set LED PWM Duty Cycle ─────────────────────────────────── +void set_leds(uint8_t duty) { + OCR0A = duty; // Left group + OCR0B = duty; // Right group +} + +// ── Status LED Control ─────────────────────────────────────── +void status_led(bool on) { + if (on) PORTB |= (1 << PIN_STATUS); + else PORTB &= ~(1 << PIN_STATUS); +} + +// ── Button Reading (debounced) ─────────────────────────────── +bool button_is_down() { + return !(PINB & (1 << PIN_BTN)); // Active LOW +} + +// ── Setup ──────────────────────────────────────────────────── +void setup() { + // --- GPIO --- + DDRB = (1 << PIN_LED_L) | (1 << PIN_LED_R) | (1 << PIN_STATUS); + PORTB = (1 << PIN_BTN); // Pull-up on button + + // --- Timer0: Fast PWM on OC0A (PB0) and OC0B (PB1) --- + // Mode 3 (Fast PWM, TOP=0xFF), prescaler /1 for ~31.4 kHz base + // Non-inverting output on both channels + TCCR0A = (1 << COM0A1) | (1 << COM0B1) | + (1 << WGM01) | (1 << WGM00); + TCCR0B = (1 << CS00); // No prescaler → 8MHz/256 = 31.25kHz + OCR0A = 0; + OCR0B = 0; + + // --- Timer1: Millis tracking --- + // CTC mode, prescaler /64 → overflow at 8MHz/64/256 ≈ 488 Hz + TCCR1 = (1 << CS12) | (1 << CS11) | (1 << CS10); // /64 + TIMSK |= (1 << TOIE1); // Overflow interrupt + + sei(); // Enable global interrupts + + // Startup flash + status_led(true); + _delay_ms(200); + status_led(false); +} + +// ── Process Button Input ───────────────────────────────────── +void process_button() { + static uint32_t press_start = 0; + static uint32_t last_release = 0; + static bool was_pressed = false; + static bool awaiting_double = false; + static uint8_t press_count = 0; + + bool pressed = button_is_down(); + uint32_t now = g_millis; + + if (pressed && !was_pressed) { + // --- Button just pressed --- + press_start = now; + was_pressed = true; + } + else if (!pressed && was_pressed) { + // --- Button just released --- + uint32_t duration = now - press_start; + was_pressed = false; + + if (duration > LONG_PRESS_MIN) { + // Long press → cycle brightness + g_brightness_idx = (g_brightness_idx + 1) % NUM_BRIGHTNESS; + // Flash status LED to indicate level + for (uint8_t i = 0; i <= g_brightness_idx; i++) { + status_led(true); + _delay_ms(80); + status_led(false); + _delay_ms(80); + } + } + else if (duration < SHORT_PRESS_MAX) { + // Short press — check for double press + if (awaiting_double && (now - last_release < DOUBLE_PRESS_MAX)) { + // Double press → OFF + g_mode = MODE_OFF; + set_leds(0); + awaiting_double = false; + press_count = 0; + status_led(true); + _delay_ms(50); + status_led(false); + return; + } + awaiting_double = true; + last_release = now; + press_count++; + } + } + + // Finalize single press after double-press window expires + if (awaiting_double && !pressed && + (now - last_release > DOUBLE_PRESS_MAX)) { + // Single short press → next mode + g_mode = (g_mode + 1) % NUM_MODES; + g_mode_start_ms = now; + awaiting_double = false; + press_count = 0; + + // Quick status flash for mode feedback + for (uint8_t i = 0; i <= g_mode; i++) { + status_led(true); + _delay_ms(40); + status_led(false); + _delay_ms(40); + } + } +} + +// ── Battery Check ──────────────────────────────────────────── +void check_battery() { + uint16_t adc_val = read_adc(2); // ADC2 = PB4 + g_low_battery = (adc_val < LOW_BATT_ADC); +} + +// ── Main Loop ──────────────────────────────────────────────── +void loop() { + uint32_t now = g_millis; + + // --- Button handling --- + process_button(); + + // --- Auto-off timer --- + if (g_mode != MODE_OFF && + (now - g_mode_start_ms > AUTO_OFF_MS)) { + g_mode = MODE_OFF; + set_leds(0); + } + + // --- Battery check (periodic) --- + if (now - g_last_batt_check > BATT_CHECK_INTERVAL) { + g_last_batt_check = now; + check_battery(); + } + + // --- Low battery warning --- + if (g_low_battery && g_mode != MODE_OFF) { + // Blink status LED every 2 seconds + status_led((now / 500) % 4 == 0); + } + + // --- LED Mode Execution --- + uint8_t duty = BRIGHTNESS_LEVELS[g_brightness_idx]; + + switch (g_mode) { + + case MODE_OFF: + set_leds(0); + status_led(false); + // Could enter sleep mode here for power savings + break; + + case MODE_CONSTANT: + set_leds(duty); + break; + + case MODE_PULSE_30HZ: + // 30 Hz = 33.3ms period, 16.7ms on / 16.7ms off + if ((now % 33) < 17) + set_leds(duty); + else + set_leds(0); + break; + + case MODE_PULSE_60HZ: + // 60 Hz = 16.7ms period, 8.3ms on / 8.3ms off + if ((now % 17) < 9) + set_leds(duty); + else + set_leds(0); + break; + + case MODE_RANDOM: + // Change state every 5–25ms (pseudorandom) + { + static uint32_t next_change = 0; + if (now >= next_change) { + uint16_t rnd = lfsr_next(); + // Random on/off + if (rnd & 1) + set_leds(duty); + else + set_leds(0); + // Random interval 5–25ms + next_change = now + 5 + (rnd % 21); + } + } + break; + + case MODE_STEALTH: + // 25% of selected brightness, constant + set_leds(duty / 4); + break; + } + + _delay_ms(1); // ~1 kHz loop rate +} + +/* + * ── Build & Flash Notes ────────────────────────────────────── + * + * Arduino IDE Setup: + * 1. Install ATTinyCore via Board Manager + * URL: http://drazzy.com/package_drazzy.com_index.json + * 2. Board: "ATtiny25/45/85 (No bootloader)" + * 3. Chip: ATtiny85 + * 4. Clock: 8 MHz (internal) + * 5. Programmer: USBasp (or "Arduino as ISP") + * 6. Burn Bootloader first (sets fuses) + * 7. Upload with programmer (Ctrl+Shift+U) + * + * PlatformIO (alternative): + * [env:attiny85] + * platform = atmelavr + * board = attiny85 + * framework = arduino + * board_build.f_cpu = 8000000L + * upload_protocol = usbasp + * + * Power Consumption (estimated): + * Mode | Avg Draw | Runtime (500mAh) + * ──────────────┼───────────┼───────────────── + * OFF | ~5 µA | years + * CONSTANT 100% | ~1.6 A | ~18 min + * PULSE 30Hz | ~800 mA | ~37 min + * PULSE 60Hz | ~800 mA | ~37 min + * RANDOM | ~600 mA | ~50 min + * STEALTH | ~400 mA | ~75 min + * + * (16 LEDs × 100mA = 1.6A max; MCU + driver overhead ~5mA) + * + * Wiring Quick Reference: + * + * ATtiny85 Pin 5 (PB0) ──→ 1kΩ ──→ Q1 Gate (IRLML6344) + * Q1 Drain ──→ LED Group L (8× parallel) + * Q1 Source ──→ GND + * + * ATtiny85 Pin 6 (PB1) ──→ 1kΩ ──→ Q2 Gate (IRLML6344) + * Q2 Drain ──→ LED Group R (8× parallel) + * Q2 Source ──→ GND + * + * ATtiny85 Pin 7 (PB2) ──→ Button ──→ GND (internal pull-up) + * ATtiny85 Pin 2 (PB3) ──→ 330Ω ──→ Status LED ──→ GND + * ATtiny85 Pin 3 (PB4) ──→ Voltage divider (100k/100k) ──→ VBAT + */ diff --git a/hardware/clip_on.scad b/hardware/clip_on.scad new file mode 100644 index 0000000..66491c6 --- /dev/null +++ b/hardware/clip_on.scad @@ -0,0 +1,257 @@ +// ============================================================ +// ZK-Glasses — Snap-On IR Clip Module +// Clips onto any existing glasses temple arm +// ============================================================ +// +// ┌──────────────────────────────────────────────────────┐ +// │ RENDER INSTRUCTIONS │ +// │ F5 = Quick preview (colors, transparency) │ +// │ F6 = Full render (slow, for STL export) │ +// │ F7 = Export STL │ +// │ │ +// │ Toggle variables below for different views: │ +// │ • show_beams = IR beam visualization │ +// │ • show_temple = phantom glasses arm │ +// │ • exploded = exploded assembly view │ +// │ • cross_section = cutaway internal view │ +// │ • show_electronics = ATtiny + MOSFETs + wiring │ +// └──────────────────────────────────────────────────────┘ + +include + +// ── View Toggles ───────────────────────────────────────────── +show_beams = true; // Show purple IR beam cones +show_temple = true; // Show transparent phantom glasses arm +exploded = false; // Exploded assembly view +cross_section = false; // Cutaway to show internals +show_electronics = true; // Show ATtiny, MOSFETs, wires + +// ── Glasses Temple Parameters (measure yours!) ─────────────── +temple_w = 5.0; // Width of your glasses temple arm +temple_h = 2.8; // Thickness of temple arm +temple_len = 140; // Length (for phantom display) + +// ── Clip Parameters ────────────────────────────────────────── +clip_len = 52; // Length of main clip body +clip_wall = 2.0; // Shell thickness +clip_tol = 0.4; // Tolerance around temple +grip_lip = 0.6; // Inward lip for snap retention + +// ── LED Array ──────────────────────────────────────────────── +led_count = 5; // LEDs along top of each clip +led_pitch = 9.0; // Center-to-center spacing +led_dia = 3.0; // 3mm LEDs +led_fwd_tilt = 12; // Forward tilt angle (toward cameras) +led_out_tilt = 5; // Slight outward splay + +// ── Battery Pod ────────────────────────────────────────────── +batt_l = 38; // Battery compartment length +batt_w = 22; // Battery compartment width +batt_h = 8; // Battery compartment height + +// ── Bridge Connector ───────────────────────────────────────── +bridge_w = 3.0; // Width of brow connector strip +bridge_h = 2.5; // Height of connector + +// ── Derived ────────────────────────────────────────────────── +clip_inner_w = temple_w + clip_tol * 2; +clip_inner_h = temple_h + clip_tol * 2; +clip_outer_w = clip_inner_w + clip_wall * 2; +clip_outer_h = clip_inner_h + clip_wall + 5; // extra height above for LEDs +ex = exploded ? 25 : 0; // exploded offset + +// ══════════════════════════════════════════════════════════════ +// CLIP BODY — main structural piece +// ══════════════════════════════════════════════════════════════ +module clip_body() { + difference() { + // Outer shell — rounded block + minkowski() { + cube([clip_len - 2, clip_outer_w - 2, clip_outer_h - 1], + center = true); + sphere(r = 1.0, $fn = 16); + } + + // Temple channel (open at bottom for snap-on) + translate([0, 0, -1]) + cube([clip_len + 2, clip_inner_w, clip_inner_h + 2], + center = true); + + // Bottom slot opening (narrower than channel for snap fit) + translate([0, 0, -(clip_outer_h/2)]) + cube([clip_len + 2, clip_inner_w - grip_lip * 2, clip_wall + 2], + center = true); + + // LED sockets (angled holes through top) + for (i = [0 : led_count - 1]) { + x = -(led_pitch * (led_count - 1) / 2) + i * led_pitch; + translate([x, 0, clip_outer_h / 2 - 1]) + rotate([-led_fwd_tilt, 0, 0]) + cylinder(d = led_dia + 0.4, h = 12, $fn = 24); + } + + // Wire channel (horizontal bore through body) + translate([0, 0, clip_outer_h / 2 - 3.5]) + rotate([0, 90, 0]) + cylinder(d = 2.0, h = clip_len + 4, center = true, $fn = 16); + + // Wire exit holes at each end + for (sx = [-1, 1]) + translate([sx * (clip_len/2 + 0.5), 0, clip_outer_h / 2 - 3.5]) + sphere(d = 3.0, $fn = 16); + } +} + +// ══════════════════════════════════════════════════════════════ +// BATTERY POD — sits behind the ear +// ══════════════════════════════════════════════════════════════ +module battery_pod() { + difference() { + // Outer shell + minkowski() { + cube([batt_l - 3, batt_w - 3, batt_h - 1], center = true); + sphere(r = 1.5, $fn = 16); + } + // Battery cavity + translate([0, 0, 1]) + cube([batt_l - 4, batt_w - 4, batt_h], center = true); + + // Charge port cutout (USB-C, one end) + translate([-(batt_l/2), 0, 0]) + rotate([0, 90, 0]) + rounded_rect_extrude(9.5, 3.5, 1.0, 6); + + // Wire exit (connects to clip) + translate([batt_l/2, 0, batt_h/2 - 2]) + rotate([0, 90, 0]) + cylinder(d = 2.5, h = 4, center = true, $fn = 16); + + // Slide switch cutout (top) + translate([-8, 0, batt_h/2]) + cube([10, 4.5, 4], center = true); + } +} + +// Helper for charge port cutout +module rounded_rect_extrude(w, h, r, depth) { + linear_extrude(depth) + offset(r = r) offset(delta = -r) + square([w - 2*r, h - 2*r], center = true); +} + +// ══════════════════════════════════════════════════════════════ +// BROW BRIDGE — connects left and right clips +// ══════════════════════════════════════════════════════════════ +module brow_bridge(span = 20) { + // Flexible strip that goes across the nose bridge area + color(CLR_FRAME) + difference() { + minkowski() { + cube([span, bridge_w - 1, bridge_h - 0.5], center = true); + sphere(r = 0.5, $fn = 12); + } + // Wire channel + rotate([0, 90, 0]) + cylinder(d = 1.5, h = span + 4, center = true, $fn = 12); + } +} + +// ══════════════════════════════════════════════════════════════ +// FULL ASSEMBLY — single side (mirrored for both) +// ══════════════════════════════════════════════════════════════ +module clip_assembly_side(side = "left") { + mirror_x = (side == "right") ? 1 : 0; + + mirror([mirror_x, 0, 0]) { + // ── Clip body ── + translate([0, 0, ex]) + color(CLR_FRAME) + clip_body(); + + // ── LEDs in sockets ── + for (i = [0 : led_count - 1]) { + x = -(led_pitch * (led_count - 1) / 2) + i * led_pitch; + translate([x, 0, clip_outer_h / 2 - 1 + ex * 1.5]) + rotate([-led_fwd_tilt, 0, 0]) + ir_led_3mm(show_beam = show_beams); + } + + // ── Battery pod (behind clip) ── + translate([clip_len/2 + batt_l/2 + 3, 0, -ex * 0.3]) + color(CLR_FRAME) + battery_pod(); + + // ── Battery inside pod ── + translate([clip_len/2 + batt_l/2 + 3, 0, 1 - ex * 0.1]) + lipo_battery(l = batt_l - 6, w = batt_w - 6, h = batt_h - 4); + + // ── Slide switch ── + translate([clip_len/2 + batt_l/2 + 3 - 8, 0, batt_h/2 + 0.5 - ex * 0.2]) + slide_switch(); + + // ── Phantom temple arm ── + if (show_temple) + translate([20, 0, 0]) + phantom_temple(length = temple_len, width = temple_w, height = temple_h); + + // ── Electronics (inside clip body) ── + if (show_electronics) { + // ATtiny85 + translate([10, 0, clip_outer_h / 2 - 5 + ex * 0.8]) + rotate([0, 0, 90]) + attiny85_dip8(); + + // MOSFETs + translate([-5, 2, clip_outer_h / 2 - 5 + ex * 0.6]) + mosfet_sot23(); + translate([-5, -2, clip_outer_h / 2 - 5 + ex * 0.6]) + mosfet_sot23(); + + // Button (on outer face of clip) + translate([-15, clip_outer_w/2 + 1, 0]) + rotate([90, 0, 0]) + tact_button(); + } + } +} + +// ══════════════════════════════════════════════════════════════ +// MAIN ASSEMBLY +// ══════════════════════════════════════════════════════════════ + +// Left clip +translate([-40, 0, 0]) + clip_assembly_side("left"); + +// Right clip +translate([40, 0, 0]) + clip_assembly_side("right"); + +// Brow bridge connector +translate([0, 0, clip_outer_h / 2 + ex * 0.8]) + brow_bridge(span = 60); + +// ── Optional cross section ── +if (cross_section) { + // Cut away front half to show internals + translate([0, 50, 0]) + cube([200, 100, 100], center = true); +} + +// ══════════════════════════════════════════════════════════════ +// PRINTABLE PARTS (uncomment one at a time for STL export) +// ══════════════════════════════════════════════════════════════ + +// For 3D printing, render each part flat on the build plate: + +// !clip_body(); // Print 2× +// !battery_pod(); // Print 2× +// !brow_bridge(span = 60); // Print 1× + +// Print settings: +// Material: PETG (flexibility + heat resistance) +// Layer height: 0.16mm +// Infill: 30% gyroid +// Supports: Yes (for LED socket overhangs) +// Wall count: 3 +// Brim: Yes (small footprint parts) diff --git a/hardware/common.scad b/hardware/common.scad new file mode 100644 index 0000000..aa6310a --- /dev/null +++ b/hardware/common.scad @@ -0,0 +1,355 @@ +// ============================================================ +// ZK-Glasses — Common Components Library +// Shared modules for clip-on and integrated frame variants +// ============================================================ +// +// Usage: include +// +// All dimensions in millimeters. +// Default preview resolution — bump to 128 for final render. + +$fn = 64; + +// ────────────────────────────────────────────── +// Color palette (consistent across all files) +// ────────────────────────────────────────────── +CLR_FRAME = [0.22, 0.22, 0.24]; // matte charcoal +CLR_LED_BODY = [0.12, 0.08, 0.18, 0.7]; // dark tinted epoxy +CLR_LED_BEAM = [0.55, 0.0, 1.0, 0.06]; // faint purple IR viz +CLR_SILVER = [0.78, 0.78, 0.80]; +CLR_GOLD = [0.83, 0.69, 0.22]; +CLR_PCB = [0.05, 0.35, 0.12]; +CLR_BATTERY = [0.30, 0.50, 0.72]; +CLR_RUBBER = [0.15, 0.15, 0.15]; +CLR_WIRE_R = [0.7, 0.1, 0.1]; +CLR_WIRE_B = [0.1, 0.1, 0.1]; +CLR_IR_FILTER= [0.08, 0.04, 0.10, 0.92]; // near-black IR-pass filter +CLR_FLEX_PCB = [0.15, 0.12, 0.02]; // dark gold flex PCB +CLR_SMD_LED = [0.10, 0.10, 0.12]; // tiny dark SMD body + +// ────────────────────────────────────────────── +// 2D helper — rounded rectangle +// ────────────────────────────────────────────── +module rounded_rect_2d(w, h, r) { + offset(r = r) + offset(delta = -r) + square([w, h], center = true); +} + +// ────────────────────────────────────────────── +// SMD IR LED — OSRAM SFH 4726AS style +// 940nm, PLCC-2 (3.5 × 2.8 × 1.9 mm) +// Flush-mountable, nearly invisible in frame +// ────────────────────────────────────────────── +module ir_led_smd(show_beam = false) { + // Tiny SMD body — barely visible + color(CLR_SMD_LED) { + cube([3.5, 2.8, 1.9], center = true); + // Lens window (top face, recessed) + color([0.06, 0.03, 0.08, 0.85]) + translate([0, 0, 0.9]) + cube([2.8, 2.0, 0.15], center = true); + } + // Solder pads + color(CLR_SILVER) { + translate([-1.4, 0, -0.95]) + cube([0.8, 2.0, 0.1], center = true); + translate([ 1.4, 0, -0.95]) + cube([0.8, 2.0, 0.1], center = true); + } + // IR beam — wider angle than through-hole (±60°) + if (show_beam) { + color([0.55, 0.0, 1.0, 0.04]) + translate([0, 0, 1.5]) + cylinder(d1 = 2, d2 = 80, h = 60, $fn = 32); + } +} + +// ────────────────────────────────────────────── +// Flex PCB LED Strip +// Thin flexible circuit with SMD LEDs pre-soldered +// Slides into channel inside the brow bar +// ────────────────────────────────────────────── +module flex_pcb_strip(length = 100, led_count = 6, led_pitch = 12) { + // Flex substrate (0.2mm thick, like a ribbon cable) + color(CLR_FLEX_PCB) + cube([length, 4.0, 0.2], center = true); + + // Copper traces (decorative) + color([0.72, 0.55, 0.15], 0.3) + for (dy = [-1, 1]) + translate([0, dy * 1.2, 0.12]) + cube([length - 2, 0.4, 0.04], center = true); + + // SMD LEDs on the strip + for (i = [0 : led_count - 1]) { + x = -(led_pitch * (led_count - 1) / 2) + i * led_pitch; + translate([x, 0, 1.05]) + ir_led_smd(show_beam = false); + } + + // Connector tail (one end) + color(CLR_FLEX_PCB) + translate([length/2 + 5, 0, 0]) + cube([12, 3, 0.2], center = true); + // Connector pins + color(CLR_GOLD) + translate([length/2 + 10, 0, 0]) + cube([2, 4, 0.8], center = true); +} + +// ────────────────────────────────────────────── +// IR-Pass Filter Strip +// Looks opaque black to human eyes, but +// transmits 940nm IR light freely. +// (Kodak Wratten 87C equivalent) +// ────────────────────────────────────────────── +module ir_filter_strip(length = 100, width = 4.0, thickness = 0.5) { + color(CLR_IR_FILTER) + cube([length, width, thickness], center = true); +} + +// ────────────────────────────────────────────── +// 3mm Through-Hole IR LED (TSAL6200 style) +// ────────────────────────────────────────────── +module ir_led_3mm(show_beam = false) { + // Epoxy body + dome + color(CLR_LED_BODY) { + cylinder(d = 3.0, h = 4.5); + translate([0, 0, 4.5]) + sphere(d = 3.0); + // Flange + cylinder(d = 3.6, h = 0.8); + } + // Anode / cathode leads + color(CLR_SILVER) { + translate([ 1.27/2, 0, -6]) + cylinder(d = 0.45, h = 6, $fn = 8); + translate([-1.27/2, 0, -6]) + cylinder(d = 0.45, h = 6, $fn = 8); + } + // IR beam cone (visual only) + if (show_beam) { + color(CLR_LED_BEAM) + translate([0, 0, 5.5]) + cylinder(d1 = 2.5, d2 = 45, h = 70); + } +} + +// ────────────────────────────────────────────── +// 5mm Through-Hole IR LED +// ────────────────────────────────────────────── +module ir_led_5mm(show_beam = false) { + color(CLR_LED_BODY) { + cylinder(d = 5.0, h = 6.0); + translate([0, 0, 6.0]) + sphere(d = 5.0); + cylinder(d = 5.8, h = 1.0); + } + color(CLR_SILVER) { + translate([ 1.27, 0, -8]) + cylinder(d = 0.45, h = 8, $fn = 8); + translate([-1.27, 0, -8]) + cylinder(d = 0.45, h = 8, $fn = 8); + } + if (show_beam) { + color(CLR_LED_BEAM) + translate([0, 0, 7.0]) + cylinder(d1 = 4, d2 = 55, h = 80); + } +} + +// ────────────────────────────────────────────── +// LiPo Battery Cell +// ────────────────────────────────────────────── +module lipo_battery(l = 35, w = 20, h = 5.5) { + color(CLR_BATTERY) + minkowski() { + cube([l - 2, w - 2, h - 1], center = true); + sphere(r = 0.5, $fn = 16); + } + // JST connector tab + color(CLR_GOLD) + translate([l/2 + 1, 0, 0]) + cube([3, 5, 1.2], center = true); + // Label + color("White") + translate([0, 0, h/2 + 0.01]) + linear_extrude(0.1) + text("3.7V 500mAh", size = 2.5, + halign = "center", valign = "center"); +} + +// ────────────────────────────────────────────── +// TP4056 USB-C Charge Board (25 × 14 mm) +// ────────────────────────────────────────────── +module tp4056_board() { + // PCB + color(CLR_PCB) { + cube([25.4, 14.2, 1.2], center = true); + // Copper pads (bottom hint) + translate([0, 0, -0.7]) + cube([24, 13, 0.05], center = true); + } + // USB-C receptacle + color(CLR_SILVER) + translate([-12.2, 0, 1.0]) + minkowski() { + cube([5, 7.5, 2.0], center = true); + sphere(r = 0.3, $fn = 12); + } + // TP4056 IC + color("DimGray") + translate([3, 0, 0.9]) + cube([5, 4, 1.2], center = true); + // Inductor + color([0.25, 0.25, 0.25]) + translate([9, 0, 1.0]) + cylinder(d = 4, h = 2, center = true, $fn = 16); + // Charging LED (red) + color("Red", 0.8) + translate([0, 5.5, 0.8]) + cube([1.6, 0.8, 0.6], center = true); + // Done LED (green) + color("Lime", 0.8) + translate([0, -5.5, 0.8]) + cube([1.6, 0.8, 0.6], center = true); +} + +// ────────────────────────────────────────────── +// ATtiny85 — DIP-8 Package +// ────────────────────────────────────────────── +module attiny85_dip8() { + // Body + color([0.18, 0.18, 0.20]) { + cube([9.6, 6.35, 3.3], center = true); + // Orientation notch + translate([-4.8, 0, 1.65]) + rotate([0, 90, 0]) + cylinder(d = 2.0, h = 0.5, $fn = 16); + } + // Dot marker + color("White") + translate([-3.5, -2.0, 1.66]) + cylinder(d = 0.8, h = 0.05, $fn = 12); + // Pins (4 per side, 2.54 mm pitch) + color(CLR_SILVER) + for (i = [0:3]) { + // Bottom pins + translate([-3.81 + i * 2.54, -3.8, -1.2]) + cube([0.5, 1.5, 0.25]); + // Top pins + translate([-3.81 + i * 2.54, 2.3, -1.2]) + cube([0.5, 1.5, 0.25]); + } +} + +// ────────────────────────────────────────────── +// N-MOSFET — SOT-23 Package (IRLML6344) +// ────────────────────────────────────────────── +module mosfet_sot23() { + color([0.18, 0.18, 0.20]) + cube([2.9, 1.6, 1.1], center = true); + color(CLR_SILVER) { + // Gate, Source (bottom side) + translate([-0.95, -1.3, 0]) + cube([0.4, 0.8, 0.15], center = true); + translate([ 0.95, -1.3, 0]) + cube([0.4, 0.8, 0.15], center = true); + // Drain (top side) + translate([0, 1.3, 0]) + cube([0.4, 0.8, 0.15], center = true); + } +} + +// ────────────────────────────────────────────── +// Tactile Push Button (6 × 6 mm) +// ────────────────────────────────────────────── +module tact_button() { + color([0.15, 0.15, 0.17]) { + cube([6, 6, 3.5], center = true); + } + // Button cap + color([0.3, 0.3, 0.32]) + translate([0, 0, 2.0]) + cylinder(d = 3.5, h = 1.0, $fn = 24); + // Pins + color(CLR_SILVER) + for (dx = [-3.25, 3.25]) + for (dy = [-2.25, 2.25]) + translate([dx, dy, -2.5]) + cube([0.6, 0.6, 1.5], center = true); +} + +// ────────────────────────────────────────────── +// Slide Switch (8 × 3 mm) +// ────────────────────────────────────────────── +module slide_switch() { + color([0.12, 0.12, 0.14]) + cube([8.5, 3.6, 3.5], center = true); + // Slider knob + color(CLR_SILVER) + translate([1.5, 0, 2.0]) + cube([3, 1.5, 1.2], center = true); + // Pins + color(CLR_SILVER) + for (dx = [-2.54, 0, 2.54]) + translate([dx, 0, -2.5]) + cylinder(d = 0.8, h = 2, $fn = 8); +} + +// ────────────────────────────────────────────── +// Wire Channel (hull between point list) +// ────────────────────────────────────────────── +module wire_channel(pts, d = 1.5) { + for (i = [0 : len(pts) - 2]) + hull() { + translate(pts[i]) sphere(d = d, $fn = 12); + translate(pts[i+1]) sphere(d = d, $fn = 12); + } +} + +// ────────────────────────────────────────────── +// Nose Pad (soft rubber) +// ────────────────────────────────────────────── +module nose_pad() { + color(CLR_RUBBER) + minkowski() { + scale([1, 0.6, 1]) + sphere(d = 8, $fn = 24); + cube([0.5, 0.5, 0.5], center = true); + } +} + +// ────────────────────────────────────────────── +// Barrel Hinge (5-barrel, 2.5 mm pin) +// ────────────────────────────────────────────── +module barrel_hinge(open_angle = 0) { + barrel_d = 3.0; + barrel_h = 2.8; + pin_d = 1.2; + + color(CLR_SILVER) { + // 5 interlocking barrels + for (i = [0:4]) + translate([0, 0, i * barrel_h]) + cylinder(d = barrel_d, h = barrel_h - 0.2, $fn = 20); + // Pin + translate([0, 0, -0.5]) + cylinder(d = pin_d, h = 5 * barrel_h + 1, $fn = 12); + } +} + +// ────────────────────────────────────────────── +// Quick phantom glasses temple (for context) +// ────────────────────────────────────────────── +module phantom_temple(length = 140, width = 5, height = 2.5) { + color([0.6, 0.6, 0.65], 0.3) { + // Straight section + cube([length, width, height], center = true); + // Ear hook curve + translate([length/2, 0, 0]) + rotate([0, -25, 0]) + cube([30, width, height], center = true); + } +} diff --git a/hardware/integrated_frame.scad b/hardware/integrated_frame.scad new file mode 100644 index 0000000..82136ce --- /dev/null +++ b/hardware/integrated_frame.scad @@ -0,0 +1,574 @@ +// ============================================================ +// ZK-Glasses — Stealth Integrated IR Frame +// Flush-mounted SMD LEDs behind IR-pass filter strips +// Looks like normal glasses — invisible except to cameras +// ============================================================ +// +// Design philosophy: +// Like Meta Ray-Ban cameras — tiny components hidden in +// the frame's natural geometry. A thin IR-pass filter strip +// runs along the brow bar, looking like a decorative dark +// accent line. Behind it: SMD IR LEDs on a flex PCB. +// +// ┌──────────────────────────────────────────────────────┐ +// │ F5 = Preview (fast, with colors + beams) │ +// │ F6 = Render (slow, for STL export) │ +// │ Customizer panel for real-time parameter tweaks │ +// └──────────────────────────────────────────────────────┘ + +include + +// ╔═══════════════════════════════════════════════════════════╗ +// ║ VIEW CONTROLS ║ +// ╚═══════════════════════════════════════════════════════════╝ + +/* [View] */ +show_beams = true; // IR beam visualization (camera view) +show_filter = true; // IR-pass filter strip overlay +show_flex_pcb = true; // Internal flex PCB + SMD LEDs +show_internals = true; // Battery, MCU, etc. +cross_section = false; // Cutaway showing internal channels +exploded = false; // Exploded assembly view +print_layout = false; // Flat print-ready parts +camera_view = false; // Simulate how a camera sees it (bright beams) + +// ╔═══════════════════════════════════════════════════════════╗ +// ║ FACE MEASUREMENTS ║ +// ╚═══════════════════════════════════════════════════════════╝ + +/* [Face Dimensions] */ +face_width = 142; // Total frame width +lens_width = 50; // Lens opening width +lens_height = 36; // Lens opening height +lens_corner_r = 9; // Corner radius (↑ = rounder) +bridge_gap = 18; // Nose bridge gap +nose_bridge_drop = 5; // Bridge drop below lens center + +/* [Temple Arms] */ +temple_length = 140; // Temple arm length +temple_width = 5.5; // Temple width +temple_thickness = 4.0; // Temple thickness +temple_angle = 8; // Outward splay +ear_bend_angle = 22; // Ear hook curl +ear_bend_start = 108; // Where bend begins + +// ╔═══════════════════════════════════════════════════════════╗ +// ║ FRAME STRUCTURE ║ +// ╚═══════════════════════════════════════════════════════════╝ + +/* [Frame] */ +brow_thickness = 8.0; // Brow bar height (houses LED channel) +rim_thickness = 3.5; // Lower/side rim +frame_depth = 7.0; // Front-to-back depth +frame_fillet = 1.2; // Edge rounding + +// ╔═══════════════════════════════════════════════════════════╗ +// ║ STEALTH LED CONFIGURATION ║ +// ╚═══════════════════════════════════════════════════════════╝ + +/* [IR LEDs (Stealth)] */ +led_count_brow = 4; // SMD LEDs per side along brow +led_count_bridge = 1; // Bridge LED (0 or 1) +led_count_temple = 2; // LEDs per temple arm +led_pitch = 10.0; // Brow LED center-to-center +temple_led_pitch = 8.0; // Temple LED spacing + +// Filter strip dimensions +filter_width = 3.5; // Height of the IR filter window +filter_depth = 0.6; // Filter material thickness +filter_recess = 0.3; // How deep filter sits into frame + +// Internal LED channel +channel_height = 4.5; // Internal cavity height +channel_depth = 5.0; // Internal cavity depth + +// ╔═══════════════════════════════════════════════════════════╗ +// ║ BATTERY & ELECTRONICS ║ +// ╚═══════════════════════════════════════════════════════════╝ + +/* [Battery] */ +batt_length = 30; +batt_width = 10; +batt_thickness = 4.0; + +// ── Derived ────────────────────────────────────────────────── +total_leds = (led_count_brow + led_count_temple) * 2 + led_count_bridge; +half_bridge = bridge_gap / 2; +lens_cx = half_bridge + lens_width / 2; +brow_y = lens_height / 2 + brow_thickness / 2; +filter_y = lens_height / 2 + brow_thickness * 0.35; // centered in brow +ex = exploded ? 30 : 0; + +// Total brow LED span (for filter strip length) +brow_led_span = led_pitch * (led_count_brow - 1); +filter_length_side = brow_led_span + led_pitch; // per side +filter_length_total = face_width - 12; // nearly full width + +// ══════════════════════════════════════════════════════════════ +// FRAME FRONT — 2D profile +// ══════════════════════════════════════════════════════════════ + +module frame_front_2d() { + difference() { + union() { + // Left lens surround + translate([-lens_cx, 0]) + offset(r = rim_thickness) + rounded_rect_2d(lens_width, lens_height, lens_corner_r); + // Right lens surround + translate([lens_cx, 0]) + offset(r = rim_thickness) + rounded_rect_2d(lens_width, lens_height, lens_corner_r); + // Bridge + translate([0, lens_height/2 - nose_bridge_drop]) + square([bridge_gap + rim_thickness * 1.5, rim_thickness * 2], + center = true); + // Thickened brow bar — the key structural element + translate([0, lens_height/2 + brow_thickness/2 - 0.5]) + square([face_width + 2, brow_thickness], center = true); + } + // Lens openings + translate([-lens_cx, 0]) + rounded_rect_2d(lens_width, lens_height, lens_corner_r); + translate([lens_cx, 0]) + rounded_rect_2d(lens_width, lens_height, lens_corner_r); + } +} + +// ══════════════════════════════════════════════════════════════ +// FRAME FRONT — 3D with LED channels and filter slots +// ══════════════════════════════════════════════════════════════ + +module frame_front_3d() { + color(CLR_FRAME) + difference() { + // Base frame shape + minkowski() { + linear_extrude(height = frame_depth - frame_fillet * 2, + center = true) + offset(delta = -frame_fillet) + frame_front_2d(); + sphere(r = frame_fillet, $fn = 16); + } + + // ── Filter slot (front face) ── + // A narrow horizontal groove across the brow bar front + // The IR filter strip press-fits into this slot + translate([0, filter_y, frame_depth/2 - filter_recess]) + cube([filter_length_total, filter_width, filter_depth * 2], + center = true); + + // ── Internal LED channel ── + // Runs the full width of brow bar, behind the filter slot + // Houses the flex PCB strip with SMD LEDs + translate([0, filter_y, 0]) + cube([filter_length_total - 4, channel_height, channel_depth], + center = true); + + // ── Wire channels from brow to temples ── + for (sx = [-1, 1]) + translate([sx * (face_width/2 - 2), filter_y, 0]) + rotate([0, 90, 0]) + cylinder(d = 2.2, h = 15, center = true, $fn = 16); + + // ── Temple LED slots (near hinges) ── + for (sx = [-1, 1]) + for (i = [0 : led_count_temple - 1]) { + tx = sx * (face_width/2 + 2 + i * temple_led_pitch); + translate([tx, 0, frame_depth/2 - filter_recess]) + cube([3.8, 3.2, filter_depth * 2], center = true); + // Cavity behind + translate([tx, 0, 0]) + cube([4.5, 4.0, channel_depth], center = true); + } + + // ── Bridge LED slot ── + if (led_count_bridge > 0) + translate([0, lens_height/2 - nose_bridge_drop, + frame_depth/2 - filter_recess]) { + cube([3.8, 3.2, filter_depth * 2], center = true); + translate([0, 0, -channel_depth/2]) + cube([4.5, 4.0, channel_depth], center = true); + } + + // ── Flex PCB insertion slot (side of brow bar) ── + // The strip slides in from the temple end + for (sx = [-1, 1]) + translate([sx * (filter_length_total/2 + 1), filter_y, 0]) + cube([4, 4.5, 1.0], center = true); + } +} + +// ══════════════════════════════════════════════════════════════ +// IR-PASS FILTER STRIPS — the stealth magic +// Looks like a subtle dark accent line on the frame +// Actually transmits 940nm IR while blocking visible light +// ══════════════════════════════════════════════════════════════ + +module filter_strips() { + // Main brow bar filter — continuous strip + translate([0, filter_y, frame_depth/2 - filter_recess/2]) + ir_filter_strip( + length = filter_length_total - 1, + width = filter_width - 0.2, + thickness = filter_depth + ); + + // Temple LED filters (small individual pieces) + for (sx = [-1, 1]) + for (i = [0 : led_count_temple - 1]) { + tx = sx * (face_width/2 + 2 + i * temple_led_pitch); + translate([tx, 0, frame_depth/2 - filter_recess/2]) + ir_filter_strip(length = 3.4, width = 2.8, + thickness = filter_depth); + } + + // Bridge filter + if (led_count_bridge > 0) + translate([0, lens_height/2 - nose_bridge_drop, + frame_depth/2 - filter_recess/2]) + ir_filter_strip(length = 3.4, width = 2.8, + thickness = filter_depth); +} + +// ══════════════════════════════════════════════════════════════ +// FLEX PCB + SMD LEDs — hidden inside the brow bar +// ══════════════════════════════════════════════════════════════ + +module internal_led_assembly() { + // Left brow flex strip + translate([-(half_bridge + filter_length_side/2 + 4), + filter_y, 0]) + rotate([0, 0, 0]) + flex_pcb_strip( + length = filter_length_side + 4, + led_count = led_count_brow, + led_pitch = led_pitch + ); + + // Right brow flex strip + translate([(half_bridge + filter_length_side/2 + 4), + filter_y, 0]) + rotate([0, 0, 0]) + flex_pcb_strip( + length = filter_length_side + 4, + led_count = led_count_brow, + led_pitch = led_pitch + ); + + // Bridge LED (single SMD) + if (led_count_bridge > 0) + translate([0, lens_height/2 - nose_bridge_drop, 0]) + ir_led_smd(show_beam = false); + + // Temple LEDs + for (sx = [-1, 1]) + for (i = [0 : led_count_temple - 1]) { + tx = sx * (face_width/2 + 2 + i * temple_led_pitch); + translate([tx, 0, 0]) + ir_led_smd(show_beam = false); + } +} + +// ══════════════════════════════════════════════════════════════ +// IR BEAM VISUALIZATION — what cameras see +// ══════════════════════════════════════════════════════════════ + +module ir_beams() { + beam_alpha = camera_view ? 0.12 : 0.04; + beam_color = camera_view + ? [1.0, 1.0, 1.0, 0.15] // white bloom (camera saturated) + : [0.55, 0.0, 1.0, beam_alpha]; // faint purple + + // Brow bar beams — wide wash from the filter strip + for (sx = [-1, 1]) + for (i = [0 : led_count_brow - 1]) { + x = sx * (half_bridge + 8 + i * led_pitch); + translate([x, filter_y, frame_depth/2]) + color(beam_color) + // Wide angle SMD beam (±60°) + cylinder(d1 = filter_width, d2 = 90, h = 70, $fn = 24); + } + + // Bridge beam + if (led_count_bridge > 0) + translate([0, lens_height/2 - nose_bridge_drop, frame_depth/2]) + color(beam_color) + cylinder(d1 = 3, d2 = 60, h = 50, $fn = 24); + + // Temple beams + for (sx = [-1, 1]) + for (i = [0 : led_count_temple - 1]) { + tx = sx * (face_width/2 + 2 + i * temple_led_pitch); + translate([tx, 0, frame_depth/2]) + color(beam_color) + cylinder(d1 = 3, d2 = 50, h = 45, $fn = 24); + } +} + +// ══════════════════════════════════════════════════════════════ +// NOSE BRIDGE +// ══════════════════════════════════════════════════════════════ + +module nose_assembly() { + for (sx = [-1, 1]) { + color(CLR_SILVER) + translate([sx * 5, -lens_height/2 + nose_bridge_drop, 0]) + rotate([20, 0, sx * 15]) + cylinder(d = 1.2, h = 14, $fn = 12); + translate([sx * 7, -lens_height/2 + nose_bridge_drop - 10, + frame_depth/2 + 2]) + rotate([30, sx * 10, 0]) + nose_pad(); + } +} + +// ══════════════════════════════════════════════════════════════ +// TEMPLE ARM — clean with hidden wire channel +// ══════════════════════════════════════════════════════════════ + +module temple_arm() { + color(CLR_FRAME) + difference() { + union() { + // Main section + minkowski() { + cube([ear_bend_start - 2, + temple_width - 1, temple_thickness - 0.5], + center = true); + sphere(r = 0.5, $fn = 12); + } + // Ear bend + translate([ear_bend_start/2, 0, 0]) + rotate([0, ear_bend_angle, 0]) + translate([(temple_length - ear_bend_start)/2, 0, 0]) + minkowski() { + cube([temple_length - ear_bend_start - 2, + temple_width - 1, + temple_thickness - 0.5], + center = true); + sphere(r = 0.5, $fn = 12); + } + // Battery bulge (subtle thickening at tip) + translate([ear_bend_start/2, 0, 0]) + rotate([0, ear_bend_angle, 0]) + translate([(temple_length - ear_bend_start)/2 + 5, + 0, 0]) + minkowski() { + cube([batt_length + 2, batt_width + 1, + batt_thickness], center = true); + sphere(r = 1.0, $fn = 12); + } + } + + // Wire channel (full length) + rotate([0, 90, 0]) + cylinder(d = 1.8, h = ear_bend_start + 10, + center = true, $fn = 12); + + // Battery cavity + translate([ear_bend_start/2, 0, 0]) + rotate([0, ear_bend_angle, 0]) + translate([(temple_length - ear_bend_start)/2 + 5, + 0, 0.5]) + cube([batt_length - 2, batt_width - 2, + batt_thickness - 0.5], center = true); + + // Charge port (bottom of battery area, very discreet) + translate([ear_bend_start/2, 0, 0]) + rotate([0, ear_bend_angle, 0]) + translate([(temple_length - ear_bend_start)/2 + 5, + 0, -(batt_thickness/2 + 1)]) + cube([9, 3.2, 4], center = true); + + // Button recess (tiny, inner face of temple) + translate([5, -(temple_width/2 + 0.5), 0]) + cube([4, 2, 2.5], center = true); + } +} + +// ══════════════════════════════════════════════════════════════ +// HINGE +// ══════════════════════════════════════════════════════════════ + +module hinge_block() { + color(CLR_FRAME) + difference() { + minkowski() { + cube([8, 4, frame_depth - 1], center = true); + sphere(r = 0.5, $fn = 12); + } + cylinder(d = 1.5, h = frame_depth + 2, + center = true, $fn = 16); + } + color(CLR_SILVER) + cylinder(d = 1.2, h = frame_depth + 1, + center = true, $fn = 12); +} + +// ══════════════════════════════════════════════════════════════ +// TEMPLE INTERNALS +// ══════════════════════════════════════════════════════════════ + +module temple_electronics() { + // Battery + tip_x = ear_bend_start/2; + translate([tip_x, 0, 0]) + rotate([0, ear_bend_angle, 0]) + translate([(temple_length - ear_bend_start)/2 + 5, 0, 1]) + rotate([0, 0, 90]) + lipo_battery(l = batt_length - 4, + w = batt_width - 4, + h = batt_thickness - 1.5); + + // Tiny MCU (one temple only) + translate([15, 0, 0.5]) + scale(0.5) + attiny85_dip8(); +} + +// ══════════════════════════════════════════════════════════════ +// FULL ASSEMBLY +// ══════════════════════════════════════════════════════════════ + +module full_assembly() { + + // ── Frame front ── + translate([0, 0, ex * 0.3]) + frame_front_3d(); + + // ── IR-pass filter strips ── + if (show_filter) + translate([0, 0, ex * 0.5]) + filter_strips(); + + // ── Internal flex PCB + SMD LEDs ── + if (show_flex_pcb) + translate([0, 0, ex * 0.15]) + internal_led_assembly(); + + // ── IR beams ── + if (show_beams) + translate([0, 0, ex * 0.5]) + ir_beams(); + + // ── Nose pads ── + nose_assembly(); + + // ── Hinges + Temples ── + for (side = [-1, 1]) { + hinge_x = side * (face_width / 2 + 2); + + translate([hinge_x, 0, 0]) + hinge_block(); + + translate([hinge_x + side * ear_bend_start / 2, + 0, -ex * 0.2]) + rotate([0, 0, side * temple_angle]) + translate([side * 5, 0, 0]) { + temple_arm(); + if (show_internals) + temple_electronics(); + } + } +} + +// ══════════════════════════════════════════════════════════════ +// PRINT LAYOUT +// ══════════════════════════════════════════════════════════════ + +module print_parts() { + // Frame front (flat on back) + translate([0, 0, frame_depth/2]) + rotate([90, 0, 0]) + frame_front_3d(); + // Left temple + translate([0, -55, temple_thickness/2]) + temple_arm(); + // Right temple + translate([0, -75, temple_thickness/2]) + temple_arm(); +} + +// ══════════════════════════════════════════════════════════════ +// RENDER +// ══════════════════════════════════════════════════════════════ + +if (print_layout) { + print_parts(); +} else if (cross_section) { + difference() { + full_assembly(); + // Cut front half to expose LED channel + translate([0, 0, 50 + frame_depth/2 - 1]) + cube([300, 300, 100], center = true); + } +} else { + full_assembly(); +} + +// ── Info overlay ── +color("White") + translate([0, -lens_height - 15, 0]) + linear_extrude(0.1) + text(str("ZK-Glasses Stealth — ", total_leds, + " SMD IR LEDs @ 940nm"), + size = 3.5, halign = "center", + font = "Liberation Mono"); + +color("Gray") + translate([0, -lens_height - 22, 0]) + linear_extrude(0.1) + text(str("Frame: ", face_width, + "mm | IR filter: Wratten 87C equiv"), + size = 2.8, halign = "center", + font = "Liberation Mono"); + +color([0.55, 0, 1], 0.4) + translate([0, -lens_height - 28, 0]) + linear_extrude(0.1) + text("Invisible to eyes — visible only to cameras", + size = 2.5, halign = "center", + font = "Liberation Mono:style=Italic"); + +// ══════════════════════════════════════════════════════════════ +// NOTES +// ══════════════════════════════════════════════════════════════ +// +// STEALTH DESIGN: +// The brow bar has a thin horizontal slot cut into its +// front face. An IR-pass filter strip (Kodak Wratten 87C +// or Hoya IR-85) press-fits into this slot. It looks like +// a subtle dark decorative accent line — completely normal. +// +// Behind the filter: a flex PCB strip with SMD IR LEDs +// (OSRAM SFH 4726AS or similar, 940nm, PLCC-2 package). +// Each LED is 3.5 × 2.8 × 1.9mm — invisible from outside. +// +// The 940nm wavelength is completely invisible to human +// eyes (no red glow), but cameras see bright white spots. +// +// IR-PASS FILTER OPTIONS: +// - Kodak Wratten 87C gel: cheapest, ~$5, can be cut to size +// - Hoya IR-85 glass: durable, optical quality, ~$20 +// - Lee 87C polyester: thin, flexible, press-fits easily +// - 3D print in dark PLA, thin enough to pass some IR +// +// ASSEMBLY: +// 1. 3D print frame (PETG, matte black) +// 2. Solder SMD LEDs to flex PCB strip +// 3. Slide flex strip into brow bar channel +// 4. Route wires through temple channels +// 5. Press-fit IR filter strips into front slots +// 6. Insert batteries into temple tips +// 7. Snap in charge port, button, MCU +// +// PRINT SETTINGS: +// Material: PETG matte black (or Nylon PA12) +// Layer: 0.12mm (fine detail for filter slot) +// Infill: 40% gyroid +// Walls: 4 perimeters (structural around channels) +// Supports: Tree, touching buildplate only +// Post: Light sanding, optional soft-touch paint +// +// ══════════════════════════════════════════════════════════════ diff --git a/renders/clip_on.png b/renders/clip_on.png new file mode 100644 index 0000000..865233c Binary files /dev/null and b/renders/clip_on.png differ diff --git a/renders/integrated_cross_section.png b/renders/integrated_cross_section.png new file mode 100644 index 0000000..00ccd37 Binary files /dev/null and b/renders/integrated_cross_section.png differ diff --git a/renders/integrated_frame.png b/renders/integrated_frame.png new file mode 100644 index 0000000..45eb259 Binary files /dev/null and b/renders/integrated_frame.png differ diff --git a/renders/integrated_front.png b/renders/integrated_front.png new file mode 100644 index 0000000..7eda31a Binary files /dev/null and b/renders/integrated_front.png differ diff --git a/renders/integrated_hero.png b/renders/integrated_hero.png new file mode 100644 index 0000000..bae70f7 Binary files /dev/null and b/renders/integrated_hero.png differ diff --git a/renders/stealth_camera_view.png b/renders/stealth_camera_view.png new file mode 100644 index 0000000..787e462 Binary files /dev/null and b/renders/stealth_camera_view.png differ diff --git a/renders/stealth_cross_section.png b/renders/stealth_cross_section.png new file mode 100644 index 0000000..53b937c Binary files /dev/null and b/renders/stealth_cross_section.png differ diff --git a/renders/stealth_front.png b/renders/stealth_front.png new file mode 100644 index 0000000..ad3e30a Binary files /dev/null and b/renders/stealth_front.png differ diff --git a/renders/stealth_hero.png b/renders/stealth_hero.png new file mode 100644 index 0000000..d5ae13d Binary files /dev/null and b/renders/stealth_hero.png differ diff --git a/renders/stealth_normal.png b/renders/stealth_normal.png new file mode 100644 index 0000000..c9431d4 Binary files /dev/null and b/renders/stealth_normal.png differ diff --git a/schematic/zk-glasses-schematic.svg b/schematic/zk-glasses-schematic.svg new file mode 100644 index 0000000..bae50a2 --- /dev/null +++ b/schematic/zk-glasses-schematic.svg @@ -0,0 +1,612 @@ + + + + + + + + + + + + + + + + + + + + + + ZK-GLASSES / IR Anti-Surveillance Eyewear / Driver Circuit + ATtiny85 PWM Controller | 16x 940nm IR LEDs | LiPo 3.7V 500mAh | Rev 1.0 | 2026-02-21 + Sheet 1/1 + zk-glasses.svg + + + + + + + + + + + + + BT1 + LiPo + 3.7V + 500mAh + + + - + + + + + + + + + + + + + + + + + SW2 + SPDT + Power + + + + + + + VCC 3.7V + + + + TP4056 + USB-C Charger + w/ Protection + + + USB+ + + USB- + + + + + USB + + + + + BAT+ + + + + + + + BAT- + + + + + + + + CHG + + DONE + + + + + + + + + + + C1 + 100nF + + + + + + + + + R20 + 100k + + + + + + VBAT_SENSE + + + + R21 + 100k + + + + + + + + + + + + + + + + + + + + + + + ATtiny85 + DIP-8 + 8MHz int. + + + + + PB5 + (RST) 1 + + + + PB3 + 2 + + + + PB4 + 3 + + + + GND + 4 + + + + + PB0 + 5 + + + + PB1 + 6 + + + + PB2 + 7 + + + + VCC + 8 + + + + + + + + VCC + + + + + + + + + + + + + + R22 + 10k + + + + + + R19 + 330R + + + + + + + + + + LED1 + Green + + + + + + + + + + + VBAT_SENSE + + + + + + + + + + + + + + + + + + SW1 + Tact + 6x6mm + + + + + + + + + + + + LED_L + + + + R17 + 1k + + + + + + + Q1 + IRLML6344 + N-MOSFET + SOT-23 + + G + + + D + + + S + + + + + + + + + VCC RAIL (3.7V) + + + + + + + + + + + + + + + R1 + + + + + + + + + R2 + + + + + + + + + R3 + + + + + + + + + R4 + + + + + + + + + R5 + + + + + + + + + R6 + + + + + + + + + R7 + + + + + + + + + R8 + + + + + + + + + + + D1–D8: TSAL6200 (940nm, 8× parallel) + R1–R8: 47R (each: If = (3.7-1.3)/47 = 51mA) + + + + + + + + + LED_R + + + + R18 + 1k + + + + + + + Q2 + IRLML6344 + N-MOSFET + SOT-23 + G + + D + + S + + + + + + + + + + + + + + + + + + R9 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + D9–D16: TSAL6200 (940nm, 8× parallel) + R9–R16: 47R (each: If = 51mA, total group = 408mA) + + + + + + + + + Circuit Summary: + + ATtiny85 generates PWM on PB0/PB1 → gates two IRLML6344 N-MOSFETs + + + Each MOSFET switches a bank of 8 IR LEDs (940nm) with individual 47R limiters + + + Total max draw: 16 × 51mA = 816mA + MCU overhead ≈ 820mA + + + Runtime at full power: 500mAh / 820mA ≈ 36 min + + + Pulsed modes (30/60Hz, 50% duty) double runtime to ~72 min + + + Component Count: + + 1× ATtiny85 | 2× IRLML6344 | 16× TSAL6200 | 16× 47R | 3× misc R + + + 1× 100nF cap | 1× TP4056 module | 1× LiPo 500mAh | 2× switches | 1× status LED + + + Estimated BOM cost: ~$15 USD (through-hole prototype) + + + + + + + + IR + + + diff --git a/schematic/zk-glasses.kicad_pcb b/schematic/zk-glasses.kicad_pcb new file mode 100644 index 0000000..a378270 --- /dev/null +++ b/schematic/zk-glasses.kicad_pcb @@ -0,0 +1,85 @@ +(kicad_pcb (version 20221018) (generator pcbnew) + + (general + (thickness 1.6) + ) + + (paper "A4") + (title_block + (title "zk-glasses") + (date "2026-02-21") + ) + + (layers + (0 "F.Cu" signal) + (31 "B.Cu" signal) + (32 "B.Adhes" user "B.Adhesive") + (33 "F.Adhes" user "F.Adhesive") + (34 "B.Paste" user) + (35 "F.Paste" user) + (36 "B.SilkS" user "B.Silkscreen") + (37 "F.SilkS" user "F.Silkscreen") + (38 "B.Mask" user) + (39 "F.Mask" user) + (40 "Dwgs.User" user "User.Drawings") + (41 "Cmts.User" user "User.Comments") + (42 "Eco1.User" user "User.Eco1") + (43 "Eco2.User" user "User.Eco2") + (44 "Edge.Cuts" user) + (45 "Margin" user) + (46 "B.CrtYd" user "B.Courtyard") + (47 "F.CrtYd" user "F.Courtyard") + (48 "B.Fab" user) + (49 "F.Fab" user) + (50 "User.1" user) + (51 "User.2" user) + (52 "User.3" user) + (53 "User.4" user) + (54 "User.5" user) + (55 "User.6" user) + (56 "User.7" user) + (57 "User.8" user) + (58 "User.9" user) + ) + + (setup + (pad_to_mask_clearance 0) + (pcbplotparams + (layerselection 0x00010fc_ffffffff) + (plot_on_all_layers_selection 0x0000000_00000000) + (disableapertmacros false) + (usegerberextensions false) + (usegerberattributes true) + (usegerberadvancedattributes true) + (creategerberjobfile true) + (dashed_line_dash_ratio 12.000000) + (dashed_line_gap_ratio 3.000000) + (svgprecision 4) + (plotframeref false) + (viasonmask false) + (mode 1) + (useauxorigin false) + (hpglpennumber 1) + (hpglpenspeed 20) + (hpglpendiameter 15.000000) + (dxfpolygonmode true) + (dxfimperialunits true) + (dxfusepcbnewfont true) + (psnegative false) + (psa4output false) + (plotreference true) + (plotvalue true) + (plotinvisibletext false) + (sketchpadsonfab false) + (subtractmaskfromsilk false) + (outputformat 1) + (mirror false) + (drillshape 1) + (scaleselection 1) + (outputdirectory "") + ) + ) + + (net 0 "") + +) diff --git a/schematic/zk-glasses.kicad_pro b/schematic/zk-glasses.kicad_pro new file mode 100644 index 0000000..5f1afd4 --- /dev/null +++ b/schematic/zk-glasses.kicad_pro @@ -0,0 +1,5 @@ +{ + "board": { + "filename": "zk-glasses.kicad_pcb" + } +} diff --git a/schematic/zk-glasses.kicad_sch b/schematic/zk-glasses.kicad_sch new file mode 100644 index 0000000..ba8fbaa --- /dev/null +++ b/schematic/zk-glasses.kicad_sch @@ -0,0 +1 @@ +(kicad_sch (version 20230121) (generator "KiCAD-MCP-Server")) \ No newline at end of file