// https://forum.arduino.cc/t/a-darkroom-enlarger-lightmeter/1464893/
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define SCREEN_ADDRESS 0x3C
#define SCREEN_RESET -1
#include "SSD1306_minimal.h" // https://github.com/kirknorthrop/SSD1306_minimal/tree/master
SSD1306_Mini oled;
#include "TinyBH1750.h" // https://github.com/aend-tech/ATTINY85_BH1750
BH1750 lightMeter;
/*
PB0 / DIP pin 5 -> SDA
PB2 / DIP pin 7 -> SCL
*/
// Pin Configurations
const byte shadowBtn = 2; // Captures brightest easel area (deep print shadow)
const byte highlightBtn = 3; // Captures dimmest easel area (bright print highlight)
const byte buzzerPin = 4; // Audio feedback buzzer
const byte paperBtn = 5; // Cycle through paper profiles
// Master Calibration Constants (Set using your Calibration Sheet)
float masterBaseLux = 12.0; // time of first appearance of image on test strip
float masterBaseTime = 26.7; // time for full black on test strip
float shadowLux = 0.0;
float highlightLux = 0.0;
float currentLux;
float calculatedTime_old;
int currentPaperIndex = 0;
// Paper Profile Lookup Table Structure
struct PaperProfile {
char *name; // was String name;
float speedFactor; // Multiplier relative to your calibration baseline (1.0)
};
const int NUM_PROFILES = 6;
char shadowLuxBuffer[6];
char highlightLuxBuffer[6];
char stopRangeBuffer[3];
char paperGradeBuffer[3];
PaperProfile paperDb[NUM_PROFILES] = {
{ "IlfordMGVRC", 1.00 }, // Your calibration baseline paper
{ "IlfordMGFBclas", 1.00 }, // establish factor
{ "IlfordMGArt300", 1.00 }, // establish factor
{ "IlfordWarmtone", 1.35 }, // Slower paper, requires \~35% more time
{ "FomatoneMG", 2.10 }, // Much slower chlorobromide paper
{ "KentmereVCSelect", 0.85 } // Faster paper, requires less time
};
void setup() {
pinMode(LED_BUILTIN, OUTPUT); // for troubleshooting using "blink()" function
pinMode(shadowBtn, INPUT_PULLUP);
pinMode(highlightBtn, INPUT_PULLUP);
pinMode(paperBtn, INPUT_PULLUP);
pinMode(buzzerPin, OUTPUT);
Wire.begin();
lightMeter.begin();
oled.init(0x3C); // Initialize display with address
oled.startScreen();
oled.clear();
oled.cursorTo(20, 3);
oled.printString("DARKROOM MASTER!");
oled.cursorTo(20, 4);
oled.printString("v2.0 AUDIO & LUT");
// Startup melody
playTone(440, 100);
playTone(880, 100);
delay(1500);
oled.clear();
}
void loop() {
currentLux = lightMeter.readLightLevel();
if (currentLux < 0.01) currentLux = 0.01;
// 1. Read Shadow Button (Bright light)
if (digitalRead(shadowBtn) == LOW) {
delay(100);
blink();
shadowLux = currentLux;
playTone(600, 100); // Sharp registration tone
}
// 2. Read Highlight Button (Dimmest light)
if (digitalRead(highlightBtn) == LOW) {
delay(100);
blink();
highlightLux = currentLux;
masterBaseLux = currentLux;
playTone(500, 100); // Secondary registration tone
}
// 3. Read Paper Selection Button
if (digitalRead(paperBtn) == LOW) {
delay(100); // Prevent multi-triggering
blink();
currentPaperIndex = (currentPaperIndex + 1) % NUM_PROFILES;
playTone(1000, 100); // High click wheel sound
}
// 4. Compute Stop Range & Contrast Grade
float stopRange = 0.0;
byte paperGradeIndex;
char *paperGrade[] = {
"N/A",
"0",
"1",
"1.5",
"2",
"2.5",
"3",
"3.5",
"4",
"4.5",
"5"
};
if (shadowLux > 0 && highlightLux > 0) {
stopRange = log(shadowLux / highlightLux) / log(2.0);
if (stopRange >= 1.40) paperGradeIndex = 1;
else if (stopRange >= 1.3 && stopRange < 1.40) paperGradeIndex = 2;
else if (stopRange >= 1.20 && stopRange < 1.30) paperGradeIndex = 3;
else if (stopRange >= 1.10 && stopRange < 1.20) paperGradeIndex = 4;
else if (stopRange >= 1.00 && stopRange < 1.10) paperGradeIndex = 5;
else if (stopRange >= 0.90 && stopRange < 1.00) paperGradeIndex = 6;
else if (stopRange >= 0.80 && stopRange < 0.90) paperGradeIndex = 7;
else if (stopRange >= 0.70 && stopRange < 0.80) paperGradeIndex = 8;
else if (stopRange >= 0.60 && stopRange < 0.70) paperGradeIndex = 9;
else if (stopRange < 0.60) paperGradeIndex = 10;
}
// 5. Compute Dynamic F-Stop Timing with Speed Factor scaling
float stopDifference = log(masterBaseLux / currentLux) / log(2.0);
float calculatedTime = masterBaseTime * pow(2.0, stopDifference) * paperDb[currentPaperIndex].speedFactor;
// 6. Render Dynamic Interface
oled.cursorTo(0, 0);
oled.printString("PPR");// Show currently active paper emulsion path
oled.printString(paperDb[currentPaperIndex].name);
horizontalLine(1);
oled.cursorTo(0, 2);
oled.printString("SHD:");
oled.printString(dtostrf(shadowLux, 5, 0, shadowLuxBuffer)); // val, chars, decimals, buffer (rounds)
oled.cursorTo(0, 3);
oled.printString("HIG:");
oled.printString(dtostrf(highlightLux, 5, 0, highlightLuxBuffer)); // val, chars, decimals, buffer (rounds)
oled.cursorTo(0, 4);
oled.printString("Range:");
oled.printString(dtostrf(stopRange, 3, 1, stopRangeBuffer)); // val, chars, decimals, buffer (rounds)
oled.printString("stops");
oled.cursorTo(0, 5);
oled.printString("Ppr Fltr: Grade");
oled.printString(dtostrf(paperGradeIndex, 2, 0, paperGradeBuffer)); // val, chars, decimals, buffer (rounds)
horizontalLine(5);
// Big text calculated time readout
// oled.set2X();
oled.cursorTo(0, 6);
oled.printString("Ppr Fltr: Grade");
if (calculatedTime_old != calculatedTime) { // data has changed
oled.cursorTo(60, 6); // position cursor
oled.printString(" "); // erase old data
oled.cursorTo(60, 6); // position cursor
oled.printString(dtostrf(calculatedTime, 6, 1, paperGradeBuffer)); // val, chars, decimals, buffer (rounds)
// oled.print("s"); // "seconds"
}
calculatedTime_old = calculatedTime; // store old data for smother time
// delay(150);
}
void playTone(int frequency, int duration) {
tone(buzzerPin, frequency, duration);
delay(duration);
noTone(buzzerPin);
}
void horizontalLine(int row) { // draw horizontal line at row (range 0 to 7)
oled.cursorTo(0, row);
for (byte i = 0; i < 21; i++) {
oled.printString("-");
}
}
void blink() {
digitalWrite(LED_BUILTIN, HIGH);
delay(50);
digitalWrite(LED_BUILTIN, LOW);
}
Loading
ssd1306
ssd1306
SHADOW
HIGHLIGHT
PAPER