6 Commits

29 changed files with 37437 additions and 23307 deletions

View File

@ -0,0 +1,391 @@
/*
=============================================================================
FireFly Morse Throwie
- a light controlled (LED as Sensor) morse blinker throwie with ATTiny85
=============================================================================
Project definitions, sources
-----------------------------------------------------------------------------
Version: 0.3 - ATTiny85, 1 MHz, BOD fuse disabled
gitea : https://gitea.togo-lab.io/tgohle/0001-FireFly
Date : 2026-06-14
Key changes Version 0.3:
- Removed unnecessary ADC enable/disable from loop()
- Disabled ADC and analog comparator in setup()
- Added INPUT_PULLUP for unused Arduino pins 0, 1, 2 will save power
- Added safer watchdog setup with interrupt protection to save run near brownout
- Added wdt_reset() before sleep for cleaner 8 s timing
- Added lowercase-to-uppercase handling
- Unsupported characters are now ignored without adding fake timing gaps
-----------------------------------------------------------------------------
Inspired by Karl Lunt's FireFly project:
http://www.seanet.com/~karllunt/fireflyLED.html
Morse code reference:
'A', ".-" 'B', "-..." 'C', "-.-."
'D', "-.." 'E', "." 'F', "..-."
'G', "--." 'H', "...." 'I', ".."
'J', ".---" 'K', "-.-" 'L', ".-.."
'M', "--" 'N', "-." 'O', "---"
'P', ".--." 'Q', "--.-" 'R', ".-."
'S', "..." 'T', "-" 'U', "..-"
'V', "...-" 'W', ".--" 'X', "-..-"
'Y', "-.--" 'Z', "--.."
'1', ".----" '2', "..---" '3', "...--"
'4', "....-" '5', "....." '6', "-...."
'7', "--..." '8', "---.." '9', "----."
'0', "-----"
'.', ".-.-.-" ',', "--..--" '?', "..--.."
'!', "-.-.--" ':', "---..." ';', "-.-.-."
'(', "-.--." ')', "-.--.-" '"', ".-..-."
'@', ".--.-." '&', ".-..."
-----------------------------------------------------------------------------
Legal stuff / Copyright:
License_-_CC_BY-NC_4.0
https://creativecommons.org/licenses/by-nc/4.0/
-----------------------------------------------------------------------------
*/
#include <avr/sleep.h>
#include <avr/wdt.h>
#include <avr/pgmspace.h>
#include <avr/interrupt.h>
#include <string.h>
#ifndef cbi
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#endif
#ifndef sbi
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
#endif
// ---------------------------------------------------------------------------
// Timing: unit length in ms.
// At 1 MHz, delay() is accurate when F_CPU=1000000L is set in boards.txt.
// 100 ms gives readable optical Morse; raise to 150 ms if readability is poor.
#define unitLength 100
// ---------------------------------------------------------------------------
// Sleep interval.
// Watchdog setup uses 8 s. For final use, change sleepCycles from 1 to 4
// for about 32 s between sensing/blinking activations.
const uint8_t sleepCycles = 4;
// ---------------------------------------------------------------------------
// LED pin definitions (N-side = cathode, P-side = anode for sensing/driving).
#define LED1_N_SIDE 3
#define LED1_P_SIDE 4
// ---------------------------------------------------------------------------
// ATTiny85 Arduino pins 0, 1, 2 are unused in actual design and physically open.
// Keep them in INPUT_PULLUP so they do not float and waste current.
#define UNUSED_PIN_0 0
#define UNUSED_PIN_1 1
#define UNUSED_PIN_2 2
// ---------------------------------------------------------------------------
// ATTiny85 has 512 bytes SRAM; this keeps the Morse text in flash.
// Only uppercase letters, digits, spaces, and the special chars in the switch
// below are emitted. Lowercase letters are converted to uppercase.
// Test setup: 20 x "0" because 0 = "-----" gives maximum LED ON time for test.
// shorter text will better, more text will cost more power.
const char morseText[] PROGMEM = "TOGO LAB";
// Ruler: 0....0....1...1....2
// Ruler: 0....5....0...5....0
// ---------------------------------------------------------------------------
// Darkness threshold.
// Higher value = triggers in brighter conditions.
// Best calibrated at actual dusk/dawn with the chosen LED type.
const unsigned int darknessThreshold = 17000;
// ---------------------------------------------------------------------------
// Watchdog interrupt flag — volatile because it is written in an ISR.
volatile bool f_wdt = true;
// ---------------------------------------------------------------------------
// Forward declarations.
void setup_watchdog(uint8_t ii);
void system_sleep();
void configureUnusedPins();
void releaseLedPins(int LED_N, int LED_P);
unsigned int sensDarkness(int LED_N, int LED_P);
void morse(int LED_N, int LED_P);
void dit(int LED_P);
void dah(int LED_P);
// ===========================================================================
// Setup
// ===========================================================================
void setup()
{
// Disable unused peripherals immediately to save power.
// PRTIM1, PRUSI: unused, safe to gate off.
// PRADC: ADC is not used; LED sensing is done with digitalRead().
// PRTIM0: must stay ON while awake because delay() depends on Timer0.
PRR = (1 << PRTIM1) | (1 << PRUSI) | (1 << PRADC);
// ADC off. It is not needed for digitalRead()-based LED sensing.
cbi(ADCSRA, ADEN);
// Analog comparator off. Saves a little sleep current.
ACSR |= (1 << ACD);
configureUnusedPins();
releaseLedPins(LED1_N_SIDE, LED1_P_SIDE);
setup_watchdog(9); // 8-second watchdog interval.
}
// ===========================================================================
// Main loop
// ===========================================================================
void loop()
{
if (f_wdt) {
f_wdt = false;
if (sensDarkness(LED1_N_SIDE, LED1_P_SIDE) > darknessThreshold) {
morse(LED1_N_SIDE, LED1_P_SIDE);
}
// Return LED pins to high-Z before sleeping.
// Internal pullups stay disabled for the LED pins.
releaseLedPins(LED1_N_SIDE, LED1_P_SIDE);
}
// Test setup: 1 x 8 s sleep.
// Final setup: set sleepCycles = 4 for about 32 s.
for (uint8_t i = 0; i < sleepCycles; i++) {
system_sleep();
}
}
// ===========================================================================
// Pin helpers
// ===========================================================================
void configureUnusedPins()
{
pinMode(UNUSED_PIN_0, INPUT_PULLUP);
pinMode(UNUSED_PIN_1, INPUT_PULLUP);
pinMode(UNUSED_PIN_2, INPUT_PULLUP);
}
void releaseLedPins(int LED_N, int LED_P)
{
// digitalWrite LOW before INPUT disables the internal pullup on Arduino cores.
digitalWrite(LED_N, LOW);
digitalWrite(LED_P, LOW);
pinMode(LED_N, INPUT);
pinMode(LED_P, INPUT);
}
// ===========================================================================
// Sleep helpers
// ===========================================================================
void system_sleep()
{
// Reset watchdog counter so each sleep cycle starts with a fresh interval.
wdt_reset();
set_sleep_mode(SLEEP_MODE_PWR_DOWN);
// Atomic sleep entry pattern.
// After sei(), AVR executes the next instruction before servicing interrupts,
// so sleep_cpu() is not skipped by a just-pending interrupt.
cli();
sleep_enable();
sei();
sleep_cpu();
// WDT interrupt wakes the CPU here.
sleep_disable();
}
// Watchdog setup - ii selects timeout:
// 0=16ms 1=32ms 2=64ms 3=128ms 4=250ms 5=500ms
// 6=1s 7=2s 8=4s 9=8s
void setup_watchdog(uint8_t ii)
{
if (ii > 9) ii = 9;
uint8_t bb = ii & 7;
if (ii > 7) bb |= (1 << WDP3);
uint8_t oldSREG = SREG;
cli();
wdt_reset();
MCUSR &= ~(1 << WDRF);
// Timed sequence: enable configuration change, then set interrupt-only WDT.
WDTCR |= (1 << WDCE) | (1 << WDE);
WDTCR = bb | (1 << WDIE);
SREG = oldSREG;
}
ISR(WDT_vect)
{
f_wdt = true;
}
// ===========================================================================
// Light sensor
// ===========================================================================
// Returns a "darkness level": higher = darker.
// Charges the LED junction capacitance, then times how long it takes to bleed
// back through the reverse-biased LED until the input reads LOW.
// ~30000 = pitch black, ~0 = bright light
unsigned int sensDarkness(int LED_N, int LED_P)
{
unsigned int i;
// Charge the LED junction capacitance.
pinMode(LED_N, OUTPUT);
pinMode(LED_P, OUTPUT);
digitalWrite(LED_N, HIGH);
digitalWrite(LED_P, LOW);
// Let the N-side float and measure bleed-down time.
pinMode(LED_N, INPUT);
digitalWrite(LED_N, LOW); // disable internal pullup
for (i = 0; i < 30000; i++) {
if (digitalRead(LED_N) == LOW) break;
}
// Clean up after sensing: both LED pins low, then caller releases them.
pinMode(LED_N, OUTPUT);
digitalWrite(LED_N, LOW);
pinMode(LED_P, OUTPUT);
digitalWrite(LED_P, LOW);
return i;
}
// ===========================================================================
// Morse helpers
// ===========================================================================
void dit(int LED_P)
{
digitalWrite(LED_P, HIGH);
delay(unitLength);
digitalWrite(LED_P, LOW);
delay(unitLength);
}
void dah(int LED_P)
{
digitalWrite(LED_P, HIGH);
delay(unitLength * 3);
digitalWrite(LED_P, LOW);
delay(unitLength);
}
// ===========================================================================
// Morse sender
// ===========================================================================
void morse(int LED_N, int LED_P)
{
pinMode(LED_N, OUTPUT);
pinMode(LED_P, OUTPUT);
digitalWrite(LED_N, LOW);
digitalWrite(LED_P, LOW);
const size_t len = strlen_P(morseText);
for (size_t i = 0; i < len; i++) {
char c = (char)pgm_read_byte(&morseText[i]);
// Allow lowercase source text without silently breaking timing.
if (c >= 'a' && c <= 'z') {
c = c - 'a' + 'A';
}
bool symbolSent = false;
switch (c)
{
// ----- Letters -----
case 'A': dit(LED_P); dah(LED_P); symbolSent = true; break; // .-
case 'B': dah(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break; // -...
case 'C': dah(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); symbolSent = true; break; // -.-.
case 'D': dah(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break; // -..
case 'E': dit(LED_P); symbolSent = true; break; // .
case 'F': dit(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); symbolSent = true; break; // ..-.
case 'G': dah(LED_P); dah(LED_P); dit(LED_P); symbolSent = true; break; // --.
case 'H': dit(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break; // ....
case 'I': dit(LED_P); dit(LED_P); symbolSent = true; break; // ..
case 'J': dit(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); symbolSent = true; break; // .---
case 'K': dah(LED_P); dit(LED_P); dah(LED_P); symbolSent = true; break; // -.-
case 'L': dit(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break; // .-..
case 'M': dah(LED_P); dah(LED_P); symbolSent = true; break; // --
case 'N': dah(LED_P); dit(LED_P); symbolSent = true; break; // -.
case 'O': dah(LED_P); dah(LED_P); dah(LED_P); symbolSent = true; break; // ---
case 'P': dit(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); symbolSent = true; break; // .--.
case 'Q': dah(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); symbolSent = true; break; // --.-
case 'R': dit(LED_P); dah(LED_P); dit(LED_P); symbolSent = true; break; // .-.
case 'S': dit(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break; // ...
case 'T': dah(LED_P); symbolSent = true; break; // -
case 'U': dit(LED_P); dit(LED_P); dah(LED_P); symbolSent = true; break; // ..-
case 'V': dit(LED_P); dit(LED_P); dit(LED_P); dah(LED_P); symbolSent = true; break; // ...-
case 'W': dit(LED_P); dah(LED_P); dah(LED_P); symbolSent = true; break; // .--
case 'X': dah(LED_P); dit(LED_P); dit(LED_P); dah(LED_P); symbolSent = true; break; // -..-
case 'Y': dah(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); symbolSent = true; break; // -.--
case 'Z': dah(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break; // --..
// ----- Digits -----
case '1': dit(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); symbolSent = true; break; // .----
case '2': dit(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); symbolSent = true; break; // ..---
case '3': dit(LED_P); dit(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); symbolSent = true; break; // ...--
case '4': dit(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); dah(LED_P); symbolSent = true; break; // ....-
case '5': dit(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break; // .....
case '6': dah(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break; // -....
case '7': dah(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break; // --...
case '8': dah(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break; // ---..
case '9': dah(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); symbolSent = true; break; // ----.
case '0': dah(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); symbolSent = true; break; // -----
// ----- Punctuation -----
case '.': dit(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); symbolSent = true; break; // .-.-.-
case ',': dah(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); symbolSent = true; break; // --..--
case '?': dit(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break; // ..--..
case '!': dah(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); symbolSent = true; break; // -.-.--
case ':': dah(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break; // ---...
case ';': dah(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); symbolSent = true; break; // -.-.-.
case '(': dah(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); symbolSent = true; break; // -.--.
case ')': dah(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); symbolSent = true; break; // -.--.-
case '"': dit(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); symbolSent = true; break; // .-..-.
case '@': dit(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); symbolSent = true; break; // .--.-.
case '&': dit(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); symbolSent = true; break; // .-...
// Space handling:
// Previous symbol already ended with 1 unit OFF and then received the
// normal 2 unit character gap. Add 4 units here -> total word gap 7 units.
case ' ':
delay(unitLength * 4);
break;
// Unsupported chars are ignored without adding an artificial gap.
default:
break;
}
// Inter-character gap: 3 units total.
// dit()/dah() already trail 1 unit OFF, so add the remaining 2 units.
if (symbolSent) {
delay(unitLength * 2);
}
}
digitalWrite(LED_P, LOW);
digitalWrite(LED_N, LOW);
}

View File

@ -183,8 +183,8 @@
// theoretical you can use up to 3 LED. For example you can use it for a // theoretical you can use up to 3 LED. For example you can use it for a
// landmark beacon. So if you point this LEDs to different directions you can // landmark beacon. So if you point this LEDs to different directions you can
// detect the position in dependence of your location to the bacon. // detect the position in dependence of your location to the bacon.
#define LED1_N_SIDE 4 #define LED1_N_SIDE 3
#define LED1_P_SIDE 3 #define LED1_P_SIDE 4
// =========================================================================== // ===========================================================================
// Variables // Variables

View File

@ -0,0 +1,311 @@
/*
=============================================================================
FireFly Morse Throwie
- a light controlled (LED as Sensor) morse blinker throwie with ATTiny85
=============================================================================
Project definitions, sources
-----------------------------------------------------------------------------
Version: 0.2 - ATTiny85, 1 MHz, BOD fuse disabled
gitea : https://gitea.togo-lab.io/tgohle/0001-FireFly
Date : 2026-06-13
-----------------------------------------------------------------------------
Inspired by Karl Lunt's FireFly project:
http://www.seanet.com/~karllunt/fireflyLED.html
Morse code reference:
'A', ".-" 'B', "-..." 'C', "-.-."
'D', "-.." 'E', "." 'F', "..-."
'G', "--." 'H', "...." 'I', ".."
'J', ".---" 'K', "-.-" 'L', ".-.."
'M', "--" 'N', "-." 'O', "---"
'P', ".--." 'Q', "--.-" 'R', ".-."
'S', "..." 'T', "-" 'U', "..-"
'V', "...-" 'W', ".--" 'X', "-..-"
'Y', "-.--" 'Z', "--.."
'1', ".----" '2', "..---" '3', "...--"
'4', "....-" '5', "....." '6', "-...."
'7', "--..." '8', "---.." '9', "----."
'0', "-----"
'.', ".-.-.-" ',', "--..--" '?', "..--.."
'!', "-.-.--" ':', "---..." ';', "-.-.-."
'(', "-.--." ')', "-.--.-" '"', ".-..-."
'@', ".--.-." '&', ".-..."
Legal stuff / Copyright:
License_-_CC_BY-NC_4.0
https://creativecommons.org/licenses/by-nc/4.0/
-----------------------------------------------------------------------------
*/
#include <avr/sleep.h>
#include <avr/wdt.h>
#include <avr/pgmspace.h> // FIX 3: needed for PROGMEM / pgm_read_byte
#ifndef cbi
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#endif
#ifndef sbi
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
#endif
// ---------------------------------------------------------------------------
// Timing: unit length in ms.
// At 1 MHz, delay() is accurate when F_CPU=1000000L is set in boards.txt.
// 100 ms gives readable optical Morse; raise to 150 if readability is poor.
#define unitLength 100
// ---------------------------------------------------------------------------
// LED pin definitions (N-side = cathode, P-side = anode for sensing/driving)
#define LED1_N_SIDE 3
#define LED1_P_SIDE 4
// ---------------------------------------------------------------------------
// ATTiny85 has 512 bytes SRAM; this frees all of it for the stack.
// Only uppercase letters, digits, and the special chars in the switch below.
// Test only: 20 x "0" due 0 = "-----" most energy draining
const char morseText[] PROGMEM = "0000000000000000000";
// Ruler: 0...0....1...1....2 Attention: more Text
// Ruler: 1...5....0...5....0 will cost more power!
// ---------------------------------------------------------------------------
// Darkness threshold.
// Higher value = triggers in brighter conditions.
// Best calibrated at actual dusk/dawn with the chosen LED type.
unsigned int darknessThreshold = 17000;
// ---------------------------------------------------------------------------
// Watchdog interrupt flag — volatile because it is written in an ISR
volatile boolean f_wdt = 1;
// ---------------------------------------------------------------------------
// Forward declarations
void setup_watchdog(int ii);
void system_sleep();
unsigned int sensDarkness(int LED_N, int LED_P);
void morse(int LED_N, int LED_P);
void dit(int LED_P);
void dah(int LED_P);
// ===========================================================================
// Setup
// ===========================================================================
void setup()
{
// Disable unused peripherals immediately to save some µ/mA.
// PRTIM1, PRUSI: genuinely unused, safe to gate off
// PRADC: controlled manually around sensDarkness()
// PRTIM0: must stay ON — delay() and millis() depend on Timer0
PRR = (1 << PRTIM1) | (1 << PRUSI) | (1 << PRADC);
// ADC is gated via PRR above; also clear ADEN just in case
cbi(ADCSRA, ADEN);
setup_watchdog(9); // 8-second watchdog interval
// simple repeat if more delay is need, eg 4
}
// ===========================================================================
// Main loop
// ===========================================================================
void loop()
{
if (f_wdt == 1) {
f_wdt = 0;
// Enable ADC only for the sensing window, then shut it off again.
cbi(PRR, PRADC); // un-gate ADC clock
sbi(ADCSRA, ADEN); // power ADC on
if (sensDarkness(LED1_N_SIDE, LED1_P_SIDE) > darknessThreshold) {
morse(LED1_N_SIDE, LED1_P_SIDE);
}
cbi(ADCSRA, ADEN); // ADC off
sbi(PRR, PRADC); // re-gate ADC clock
// Return LED pins to input (high-Z) before sleeping
pinMode(LED1_N_SIDE, INPUT);
pinMode(LED1_P_SIDE, INPUT);
}
// sleep 4 x 8 s = ~32 s between activations
for (uint8_t i = 0; i < 1; i++) {
system_sleep();
}
// f_wdt is set to 1 by the WDT ISR when the 8 s expire; no manual clear needed here.
}
// ===========================================================================
// Sleep helpers
// ===========================================================================
void system_sleep()
{
set_sleep_mode(SLEEP_MODE_PWR_DOWN);
sleep_enable();
sleep_mode(); // CPU halts here until WDT fires
sleep_disable();
// ADC stays off - re-enable it in loop() only when sensing
}
// Watchdog setup - ii selects timeout:
// 0=16ms 1=32ms 2=64ms 3=128ms 4=250ms 5=500ms
// 6=1s 7=2s 8=4s 9=8s
void setup_watchdog(int ii)
{
byte bb;
if (ii > 9) ii = 9;
bb = ii & 7;
if (ii > 7) bb |= (1 << 5);
bb |= (1 << WDCE);
MCUSR &= ~(1 << WDRF);
WDTCR |= (1 << WDCE) | (1 << WDE); // timed sequence - must not be split
WDTCR = bb;
WDTCR |= _BV(WDIE);
}
ISR(WDT_vect)
{
f_wdt = 1;
}
// ===========================================================================
// Light sensor
// ===========================================================================
// Returns a "darkness level": higher = darker.
// Charges the LED junction capacitor, then times how long it takes to bleed
// back through the reverse-biased diode to a logic LOW.
// ~30000 = pitch black, ~0 = bright light
// return type is unsigned int (counter can reach 30000, fits in 16 bits)
unsigned int sensDarkness(int LED_N, int LED_P)
{
unsigned int i;
// Charge the LED (forward-bias momentarily)
pinMode(LED_N, OUTPUT);
pinMode(LED_P, OUTPUT);
digitalWrite(LED_N, HIGH);
digitalWrite(LED_P, LOW);
// Let the N-end float and measure bleed-down time
pinMode(LED_N, INPUT);
digitalWrite(LED_N, LOW); // disable internal pull-up
for (i = 0; i < 30000; i++) {
if (digitalRead(LED_N) == 0) break;
}
// clean up after sensing
pinMode(LED_N, OUTPUT);
digitalWrite(LED_N, LOW);
pinMode(LED_P, OUTPUT);
digitalWrite(LED_P, LOW);
return i;
}
// ===========================================================================
// Morse helpers
// ===========================================================================
void dit(int LED_P)
{
digitalWrite(LED_P, HIGH); delay(unitLength);
digitalWrite(LED_P, LOW); delay(unitLength);
}
void dah(int LED_P)
{
digitalWrite(LED_P, HIGH); delay(unitLength * 3);
digitalWrite(LED_P, LOW); delay(unitLength);
}
// ===========================================================================
// Morse sender
// ===========================================================================
void morse(int LED_N, int LED_P)
{
pinMode(LED_N, OUTPUT);
pinMode(LED_P, OUTPUT);
digitalWrite(LED_N, LOW);
// read each character from flash with pgm_read_byte()
// use < morseText length, not <= (avoids reading past the null terminator)
uint8_t len = strlen_P(morseText);
for (uint8_t i = 0; i < len; i++)
{
char c = (char)pgm_read_byte(&morseText[i]);
switch (c)
{
// ----- Letters -----
case 'A': dit(LED_P); dah(LED_P); break; // .-
case 'B': dah(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); break; // -...
case 'C': dah(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); break; // -.-.
case 'D': dah(LED_P); dit(LED_P); dit(LED_P); break; // -..
case 'E': dit(LED_P); break; // .
case 'F': dit(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); break; // ..-.
case 'G': dah(LED_P); dah(LED_P); dit(LED_P); break; // --.
case 'H': dit(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); break; // ....
case 'I': dit(LED_P); dit(LED_P); break; // ..
case 'J': dit(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); break; // .---
case 'K': dah(LED_P); dit(LED_P); dah(LED_P); break; // -.-
case 'L': dit(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); break; // .-..
case 'M': dah(LED_P); dah(LED_P); break; // --
case 'N': dah(LED_P); dit(LED_P); break; // -.
case 'O': dah(LED_P); dah(LED_P); dah(LED_P); break; // ---
case 'P': dit(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); break; // .--.
case 'Q': dah(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); break; // --.-
case 'R': dit(LED_P); dah(LED_P); dit(LED_P); break; // .-.
case 'S': dit(LED_P); dit(LED_P); dit(LED_P); break; // ...
case 'T': dah(LED_P); break; // -
case 'U': dit(LED_P); dit(LED_P); dah(LED_P); break; // ..-
case 'V': dit(LED_P); dit(LED_P); dit(LED_P); dah(LED_P); break; // ...-
case 'W': dit(LED_P); dah(LED_P); dah(LED_P); break; // .--
case 'X': dah(LED_P); dit(LED_P); dit(LED_P); dah(LED_P); break; // -..-
case 'Y': dah(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); break; // -.--
case 'Z': dah(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); break; // --..
// ----- Digits -----
case '1': dit(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); break; // .----
case '2': dit(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); break; // ..---
case '3': dit(LED_P); dit(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); break; // ...--
case '4': dit(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); dah(LED_P); break; // ....-
case '5': dit(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); break; // .....
case '6': dah(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); break; // -....
case '7': dah(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); break; // --...
case '8': dah(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); break; // ---.
case '9': dah(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); break; // ----.
case '0': dah(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); dah(LED_P); break; // -----
// ----- Punctuation -----
case ' ': delay(unitLength * 5); break; // word gap (5 + 3 = 8 units total)
case '.': dit(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); break; // .-.-.-
case ',': dah(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); break; // --..--
case '?': dit(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); break; // ..--..
case '!': dah(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); break; // -.-.--
case ':': dah(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); break; // ---...
case ';': dah(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); break; // -.-.-.
case '(': dah(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); break; // -.--.
case ')': dah(LED_P); dit(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); break; // -.--.-
case '"': dit(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); break; // .-..-.
case '@': dit(LED_P); dah(LED_P); dah(LED_P); dit(LED_P); dah(LED_P); dit(LED_P); break; // .--.-.
case '&': dit(LED_P); dah(LED_P); dit(LED_P); dit(LED_P); dit(LED_P); break; // .-...
}
// Inter-character gap: 3 units (the dit/dah functions already trail 1 unit,
// so this adds the remaining 2 to reach the standard 3-unit gap).
// Space case already handles word gap - no extra delay needed there.
if (c != ' ') {
delay(unitLength * 2);
}
}
}

Submodule hardware/0001_FireFly-Solar/.history added at 1c8d4d7c5c

View File

@ -0,0 +1,5 @@
(version 1)
(rule Minimum_text_height_and_thickness_1
(constraint text_height (min 0.6mm))
(constraint text_thickness (min 0.1mm))
)

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"board": { "board": {
"active_layer": 0, "active_layer": 5,
"active_layer_preset": "", "active_layer_preset": "",
"auto_track_width": true, "auto_track_width": true,
"hidden_netclasses": [], "hidden_netclasses": [],
@ -67,8 +67,30 @@
"version": 5 "version": 5
}, },
"net_inspector_panel": { "net_inspector_panel": {
"col_hidden": [], "col_hidden": [
"col_order": [], false,
false,
false,
false,
false,
false,
false,
false,
false,
false
],
"col_order": [
0,
1,
2,
3,
4,
5,
6,
7,
8,
9
],
"col_widths": [], "col_widths": [],
"custom_group_rules": [], "custom_group_rules": [],
"expanded_rows": [], "expanded_rows": [],

View File

@ -1,6 +1,349 @@
{ {
"board": { "board": {
"3dviewports": [], "3dviewports": [],
"design_settings": {
"defaults": {
"apply_defaults_to_fp_barcodes": false,
"apply_defaults_to_fp_dimensions": false,
"apply_defaults_to_fp_fields": false,
"apply_defaults_to_fp_shapes": false,
"apply_defaults_to_fp_text": false,
"board_outline_line_width": 0.05,
"copper_line_width": 0.2,
"copper_text_italic": false,
"copper_text_size_h": 1.5,
"copper_text_size_v": 1.5,
"copper_text_thickness": 0.3,
"copper_text_upright": false,
"courtyard_line_width": 0.05,
"dimension_precision": 4,
"dimension_units": 3,
"dimensions": {
"arrow_length": 1270000,
"extension_offset": 500000,
"keep_text_aligned": true,
"suppress_zeroes": true,
"text_position": 0,
"units_format": 0
},
"fab_line_width": 0.1,
"fab_text_italic": false,
"fab_text_size_h": 1.0,
"fab_text_size_v": 1.0,
"fab_text_thickness": 0.15,
"fab_text_upright": false,
"other_line_width": 0.1,
"other_text_italic": false,
"other_text_size_h": 1.0,
"other_text_size_v": 1.0,
"other_text_thickness": 0.15,
"other_text_upright": false,
"pads": {
"drill": 0.7,
"height": 1.4,
"width": 1.4
},
"silk_line_width": 0.1,
"silk_text_italic": false,
"silk_text_size_h": 1.0,
"silk_text_size_v": 1.0,
"silk_text_thickness": 0.1,
"silk_text_upright": false,
"zones": {
"border_display_style": 2,
"border_hatch_pitch": 0.5,
"corner_radius": 0.0,
"corner_smoothing": 0,
"fill_mode": 0,
"hatch_gap": 1.5,
"hatch_orientation": 0.0,
"hatch_smoothing_level": 0,
"hatch_smoothing_value": 0.1,
"hatch_thickness": 1.0,
"min_clearance": 0.5,
"min_island_area": 10.0,
"min_thickness": 0.25,
"pad_connection": 1,
"remove_islands": 0,
"thermal_relief_gap": 0.5,
"thermal_relief_spoke_width": 0.5
}
},
"diff_pair_dimensions": [
{
"gap": 0.0,
"via_gap": 0.0,
"width": 0.0
}
],
"drc_exclusions": [
[
"copper_edge_clearance|131952996|49276030|d5a8d279-1277-4128-8111-479753d97832|74ba0eb0-a42b-49db-8990-e7e337244d89",
""
],
[
"copper_edge_clearance|135254996|49276021|d5a8d279-1277-4128-8111-479753d97832|6760cf2b-9d51-44e9-8541-9b08cc6bed3a",
""
],
[
"copper_edge_clearance|138556996|49276011|d5a8d279-1277-4128-8111-479753d97832|5b16be99-1a09-4cc0-a4d6-0e5fae921e96",
""
],
[
"text_height|141224000|109999000|18cc7c24-1e1e-47b4-bd81-2abaec8c3831|00000000-0000-0000-0000-000000000000",
""
],
[
"text_thickness|129032000|119126000|be078bc2-b3be-4c33-abfe-930461332b78|00000000-0000-0000-0000-000000000000",
""
],
[
"text_thickness|129032000|122936000|57275588-b94d-44a7-b263-ab2e942abbb9|00000000-0000-0000-0000-000000000000",
""
],
[
"text_thickness|129032000|126746000|ac73bb6c-6b4c-4683-bc6b-c539afa6834f|00000000-0000-0000-0000-000000000000",
""
],
[
"text_thickness|129032000|130556000|dfc1c2d5-221c-45bf-8f4f-92d8a3ddf04a|00000000-0000-0000-0000-000000000000",
""
],
[
"text_thickness|129032000|134366000|e9026731-8c2e-491a-9b73-4cda2fa80a02|00000000-0000-0000-0000-000000000000",
""
],
[
"text_thickness|129032000|138176000|cf4eeb69-e06f-463f-be45-22626ac2c2b4|00000000-0000-0000-0000-000000000000",
""
],
[
"text_thickness|129032000|141986000|c29774df-bdb9-44e2-a17d-cfa3cd693d71|00000000-0000-0000-0000-000000000000",
""
],
[
"text_thickness|129184400|150241000|88401ad0-1dea-4344-ab33-329fd06bd493|00000000-0000-0000-0000-000000000000",
""
],
[
"text_thickness|135148000|106152000|7c4ccd6a-1b52-4734-bd95-092ada163852|00000000-0000-0000-0000-000000000000",
""
],
[
"text_thickness|135382000|84709000|88b0a397-e4a2-4145-86f6-0fa60b4a4bd9|00000000-0000-0000-0000-000000000000",
""
],
[
"text_thickness|141224000|109999000|18cc7c24-1e1e-47b4-bd81-2abaec8c3831|00000000-0000-0000-0000-000000000000",
""
],
[
"text_thickness|141325600|150144800|5da7a07f-f063-43b3-a149-e672965b459b|00000000-0000-0000-0000-000000000000",
""
],
[
"text_thickness|141478000|119126000|40b0fefb-8b86-43f7-b24d-e20c4677b223|00000000-0000-0000-0000-000000000000",
""
],
[
"text_thickness|141478000|122936000|77ee43fb-9700-42ab-8702-560042b5edd3|00000000-0000-0000-0000-000000000000",
""
],
[
"text_thickness|141478000|126746000|833d2a60-9010-4cb2-8045-b8050c8ee771|00000000-0000-0000-0000-000000000000",
""
],
[
"text_thickness|141478000|130556000|5a756363-e24d-4c12-82cf-59fc84a986ae|00000000-0000-0000-0000-000000000000",
""
],
[
"text_thickness|141478000|134366000|685c5797-de13-4bd0-a3b9-5fb75b4b9dad|00000000-0000-0000-0000-000000000000",
""
],
[
"text_thickness|141478000|138176000|15931c23-e5f6-4be3-ba3e-7a720c730e9b|00000000-0000-0000-0000-000000000000",
""
],
[
"text_thickness|141478000|141986000|e240b788-8dbb-43c9-8c9f-717e069201be|00000000-0000-0000-0000-000000000000",
""
]
],
"meta": {
"version": 2
},
"rule_severities": {
"annular_width": "error",
"clearance": "error",
"connection_width": "warning",
"copper_edge_clearance": "error",
"copper_sliver": "warning",
"courtyards_overlap": "error",
"creepage": "error",
"diff_pair_gap_out_of_range": "error",
"diff_pair_uncoupled_length_too_long": "error",
"drill_out_of_range": "error",
"duplicate_footprints": "warning",
"extra_footprint": "warning",
"footprint": "error",
"footprint_filters_mismatch": "ignore",
"footprint_symbol_field_mismatch": "warning",
"footprint_symbol_mismatch": "warning",
"footprint_type_mismatch": "ignore",
"hole_clearance": "error",
"hole_to_hole": "warning",
"holes_co_located": "warning",
"invalid_outline": "error",
"isolated_copper": "warning",
"item_on_disabled_layer": "error",
"items_not_allowed": "error",
"length_out_of_range": "error",
"lib_footprint_issues": "warning",
"lib_footprint_mismatch": "warning",
"malformed_courtyard": "error",
"microvia_drill_out_of_range": "error",
"mirrored_text_on_front_layer": "warning",
"missing_courtyard": "ignore",
"missing_footprint": "warning",
"missing_tuning_profile": "warning",
"net_conflict": "warning",
"nonmirrored_text_on_back_layer": "warning",
"npth_inside_courtyard": "error",
"padstack": "warning",
"pth_inside_courtyard": "error",
"shorting_items": "error",
"silk_edge_clearance": "ignore",
"silk_over_copper": "warning",
"silk_overlap": "ignore",
"skew_out_of_range": "error",
"solder_mask_bridge": "error",
"starved_thermal": "ignore",
"text_height": "warning",
"text_on_edge_cuts": "error",
"text_thickness": "warning",
"through_hole_pad_without_hole": "error",
"too_many_vias": "error",
"track_angle": "error",
"track_dangling": "warning",
"track_not_centered_on_via": "ignore",
"track_on_post_machined_layer": "error",
"track_segment_length": "error",
"track_width": "error",
"tracks_crossing": "error",
"tuning_profile_track_geometries": "ignore",
"unconnected_items": "error",
"unresolved_variable": "error",
"via_dangling": "warning",
"zones_intersect": "error"
},
"rules": {
"max_error": 0.005,
"min_clearance": 0.0,
"min_connection": 0.0,
"min_copper_edge_clearance": 0.5,
"min_groove_width": 0.0,
"min_hole_clearance": 0.25,
"min_hole_to_hole": 0.25,
"min_microvia_diameter": 0.2,
"min_microvia_drill": 0.1,
"min_resolved_spokes": 2,
"min_silk_clearance": 0.0,
"min_text_height": 0.8,
"min_text_thickness": 0.08,
"min_through_hole_diameter": 0.3,
"min_track_width": 0.2,
"min_via_annular_width": 0.1,
"min_via_diameter": 0.5,
"solder_mask_to_copper_clearance": 0.0,
"use_height_for_length_calcs": true
},
"teardrop_options": [
{
"td_onpthpad": true,
"td_onroundshapesonly": false,
"td_onsmdpad": true,
"td_ontrackend": false,
"td_onvia": true
}
],
"teardrop_parameters": [
{
"td_allow_use_two_tracks": true,
"td_curve_segcount": 0,
"td_height_ratio": 1.0,
"td_length_ratio": 0.5,
"td_maxheight": 2.0,
"td_maxlen": 1.0,
"td_on_pad_in_zone": false,
"td_target_name": "td_round_shape",
"td_width_to_size_filter_ratio": 0.9
},
{
"td_allow_use_two_tracks": true,
"td_curve_segcount": 0,
"td_height_ratio": 1.0,
"td_length_ratio": 0.5,
"td_maxheight": 2.0,
"td_maxlen": 1.0,
"td_on_pad_in_zone": false,
"td_target_name": "td_rect_shape",
"td_width_to_size_filter_ratio": 0.9
},
{
"td_allow_use_two_tracks": true,
"td_curve_segcount": 0,
"td_height_ratio": 1.0,
"td_length_ratio": 0.5,
"td_maxheight": 2.0,
"td_maxlen": 1.0,
"td_on_pad_in_zone": false,
"td_target_name": "td_track_end",
"td_width_to_size_filter_ratio": 0.9
}
],
"track_widths": [
0.0,
0.127,
0.254,
0.508,
0.635,
1.27
],
"tuning_pattern_settings": {
"diff_pair_defaults": {
"corner_radius_percentage": 80,
"corner_style": 1,
"max_amplitude": 1.0,
"min_amplitude": 0.2,
"single_sided": false,
"spacing": 1.0
},
"diff_pair_skew_defaults": {
"corner_radius_percentage": 80,
"corner_style": 1,
"max_amplitude": 1.0,
"min_amplitude": 0.2,
"single_sided": false,
"spacing": 0.6
},
"single_track_defaults": {
"corner_radius_percentage": 80,
"corner_style": 1,
"max_amplitude": 1.0,
"min_amplitude": 0.2,
"single_sided": false,
"spacing": 0.6
}
},
"via_dimensions": [
{
"diameter": 0.0,
"drill": 0.0
}
],
"zones_allow_external_fillets": false
},
"ipc2581": { "ipc2581": {
"bom_rev": "", "bom_rev": "",
"dist": "", "dist": "",
@ -8,7 +351,7 @@
"internal_id": "", "internal_id": "",
"mfg": "", "mfg": "",
"mpn": "", "mpn": "",
"sch_revision": "0" "sch_revision": "1"
}, },
"layer_pairs": [], "layer_pairs": [],
"layer_presets": [], "layer_presets": [],
@ -416,7 +759,7 @@
"uuid": "d6486f66-f0ec-4484-bce4-0ec6b0e57e1d" "uuid": "d6486f66-f0ec-4484-bce4-0ec6b0e57e1d"
} }
], ],
"used_designators": "", "used_designators": "J1-2",
"variants": [] "variants": []
}, },
"sheets": [ "sheets": [

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@ -0,0 +1,26 @@
*
.subckt 0001_FireFly-Solar
J2 __J2
D2 __D2
D3 __D3
J3 __J3
R1 Net-_D1-K_ Net-_D4-K_ 33
D1 __D1
J1 __J1
J4 __J4
D5 __D5
U1 __U1
L1 Net-_D4-K_ Net-_D5-A_ 22u
D4 __D4
C1 Net-_D4-K_ GND 50F SuperCap
C2 Net-_D4-K_ GND 150u
C3 Net-_D5-K_ GND 100u
C4 Net-_U1-REF_ GND 0.1u
U2 __U2
D6 Net-_D6-K_ Net-_D6-A_ LED
R2 Net-_U2-XTAL1/PB3_ Net-_D6-K_ R
.ends

View File

@ -1,288 +0,0 @@
/*
FireFly tetrahedron adapter
Base STL: tetrahedron40mmC.stl
Purpose:
- Import original 40 mm foldable tetrahedron STL
- Scale it to 60 mm base geometry
- Add cable holes, vent/drain holes, and optional PCB mounting holes
Workflow:
1. Set PART = "upper"
2. Render with F6
3. Export STL
4. Set PART = "lower"
5. Render with F6
6. Export STL
Notes:
- Hole coordinates are defined on the original 40 mm STL coordinate system.
- The script scales them automatically to 60 mm.
- First export should be treated as v0.1 prototype.
*/
/* =========================
Main selection
========================= */
PART = "upper"; // "upper", "lower", or "both_preview"
/* =========================
Input file
========================= */
input_stl = "tetrahedron40mmC.stl";
/* =========================
Scaling
========================= */
// Original tetrahedron edge length
original_edge_mm = 40;
// Target mechanical base size
target_edge_mm = 60;
// XY scale from 40 mm to 60 mm
scale_xy = target_edge_mm / original_edge_mm; // 1.5
// Keep original thickness.
// Set to 1.2 or 1.3 if you want a slightly thicker part.
scale_z = 1.0;
/* =========================
General hole settings
========================= */
hole_fn = 40;
cut_height = 20;
/* =========================
Feature switches
========================= */
add_upper_cable_holes = true;
add_upper_hanger_hole = true;
add_lower_vent_holes = true;
add_lower_cable_holes = true;
add_lower_pcb_mount_holes = true;
// Keep this false for the first prototype.
// Standoffs may interfere with folding depending on print orientation.
add_lower_pcb_standoffs = false;
/* =========================
Hole sizes
========================= */
cable_hole_d = 3.2; // for small solar-panel wires
hanger_hole_d = 3.5; // for small ring / wire / eyelet
vent_hole_d = 2.0; // condensation vent / drain holes
pcb_screw_hole_d = 2.2; // M2 clearance-ish
pcb_standoff_d = 6.0;
pcb_standoff_h = 3.0;
/* =========================
Coordinate reference
=========================
The uploaded STL is approximately:
X: -34.64 to +34.64
Y: -20.00 to +60.00
Z: -0.10 to +2.00
The original model is a flat foldable tetrahedron net.
Coordinates below are before scaling.
*/
/* =========================
Upper part hole positions
========================= */
// Cable holes for three solar panels.
// Adjust after checking where your real panel wires exit.
upper_cable_points = [
[-18, 20],
[ 24, 7],
[ 24, 33]
];
// Optional hanger hole close to one upper fold/vertex area.
// If this weakens the corner too much, disable it and use an external loop instead.
upper_hanger_points = [
[0, 38]
];
/* =========================
Lower part hole positions
========================= */
// Cable transfer holes between upper solar section and lower electronics bay.
lower_cable_points = [
[-10, 20],
[ 12, 13],
[ 12, 27]
];
// Small vent/drain hole clusters near likely lower/edge areas.
// These are deliberately small.
lower_vent_centres = [
[ 0, 3],
[ 0, 37],
[-30, 20],
[ 31, 20]
];
// Approximate triangular PCB mounting pattern.
// Tune this after measuring your actual red triangle PCB.
lower_pcb_mount_points = [
[ 0, 8],
[ 25, 20],
[ 0, 32]
];
/* =========================
Basic modules
========================= */
module base_import_scaled()
{
scale([scale_xy, scale_xy, scale_z])
import(input_stl, convexity = 10);
}
module vertical_hole(p, d)
{
translate([p[0] * scale_xy, p[1] * scale_xy, 1])
cylinder(h = cut_height, d = d, center = true, $fn = hole_fn);
}
module vent_cluster(p)
{
// Small 2 x 3 vent/drain pattern.
// Pitch is in final millimetres, not original model coordinates.
pitch = 3.0;
for (ix = [-1, 0, 1])
for (iy = [0, 1])
translate([
p[0] * scale_xy + ix * pitch,
p[1] * scale_xy + iy * pitch,
1
])
cylinder(h = cut_height, d = vent_hole_d, center = true, $fn = hole_fn);
}
module pcb_standoff(p)
{
// Standoff added on top of the flat STL.
// Use only if you confirm it does not block folding.
translate([p[0] * scale_xy, p[1] * scale_xy, 2.1 * scale_z])
cylinder(h = pcb_standoff_h, d = pcb_standoff_d, center = false, $fn = hole_fn);
}
/* =========================
Upper tetrahedron
========================= */
module upper_part()
{
difference()
{
base_import_scaled();
if (add_upper_cable_holes)
{
for (p = upper_cable_points)
vertical_hole(p, cable_hole_d);
}
if (add_upper_hanger_hole)
{
for (p = upper_hanger_points)
vertical_hole(p, hanger_hole_d);
}
}
}
/* =========================
Lower tetrahedron
========================= */
module lower_part()
{
difference()
{
union()
{
base_import_scaled();
if (add_lower_pcb_standoffs)
{
for (p = lower_pcb_mount_points)
pcb_standoff(p);
}
}
if (add_lower_cable_holes)
{
for (p = lower_cable_points)
vertical_hole(p, cable_hole_d);
}
if (add_lower_vent_holes)
{
for (p = lower_vent_centres)
vent_cluster(p);
}
if (add_lower_pcb_mount_holes)
{
for (p = lower_pcb_mount_points)
vertical_hole(p, pcb_screw_hole_d);
}
}
}
/* =========================
Output
========================= */
if (PART == "upper")
{
upper_part();
}
else if (PART == "lower")
{
lower_part();
}
else if (PART == "both_preview")
{
translate([-80, 0, 0])
upper_part();
translate([80, 0, 0])
lower_part();
}
else
{
echo("Invalid PART setting. Use upper, lower, or both_preview.");
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff