// 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 <SSD1306Ascii.h> // https://github.com/greiman/SSD1306Ascii/blob/master/src/SSD1306Ascii.h
#include <SSD1306AsciiAvrI2c.h> // good for low-memory
SSD1306AsciiAvrI2c oled;
#include <Wire.h>? // TinyWireM does not work with BH1750.h
#include <BH1750.h>
BH1750 lightMeter;
// Pin Configurations
const int shadowBtn = 2; // Captures brightest easel area (deep print shadow)
const int highlightBtn = 3; // Captures dimmest easel area (bright print highlight)
const int buzzerPin = 4; // Audio feedback buzzer
const int 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;
float speedFactor; // Multiplier relative to your calibration baseline (1.0)
};
const int NUM_PROFILES = 6;
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.begin(&Adafruit128x64, SCREEN_ADDRESS, SCREEN_RESET);
oled.displayRemap(true); // ROTATE normal (pins BOTTOM)
// oled.displayRemap(false); // ROTATE 180 degrees (pins TOP)
oled.setFont(Adafruit5x7);
oled.clear();
oled.setCursor(20, 3);
oled.println(F("DARKROOM MASTER"));
oled.setCursor(20, 4);
oled.println(F("v2.0 AUDIO & LUT"));
// Startup melody
playTone(440, 100);
playTone(880, 100);
delay(1500);
oled.clear();
}
void loop() {
currentLux = lightMeter.readLightLevel();
currentLux = random(54000); // for debugging
if (currentLux < 0.01) currentLux = 0.01;
// 1. Read Shadow Button (Bright light)
if (digitalRead(shadowBtn) == LOW) {
blink();
delay(100);
shadowLux = currentLux;
playTone(600, 100); // Sharp registration tone
}
// 2. Read Highlight Button (Dimmest light)
if (digitalRead(highlightBtn) == LOW) {
blink();
delay(100);
highlightLux = currentLux;
masterBaseLux = currentLux;
playTone(500, 100); // Secondary registration tone
}
// 3. Read Paper Selection Button
if (digitalRead(paperBtn) == LOW) {
blink();
delay(100); // Prevent multi-triggering
currentPaperIndex = (currentPaperIndex + 1) % NUM_PROFILES;
playTone(1000, 100); // High click wheel sound
}
// 4. Compute Stop Range & Contrast Grade
float stopRange = 0.0;
int 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.set1X();
oled.setCursor(0, 0);
// Show currently active paper emulsion path
oled.print(F("PPR:"));
oled.print(paperDb[currentPaperIndex].name);
horizontalLine(1);
oled.setCursor(0, 2);
oled.print(F("SHD:"));
oled.print(shadowLux, 0);
oled.print(F(" HIG:"));
oled.println(highlightLux, 0);
oled.print(F("Range:"));
oled.print(stopRange, 1);
oled.println(F(" stops"));
oled.print(F("Ppr Fltr: Grade "));
oled.println(paperGrade[paperGradeIndex]);
horizontalLine(5);
// Big text calculated time readout
oled.set2X();
oled.setCursor(0, 6);
oled.print(F("TIME:"));
if (calculatedTime_old != calculatedTime) { // data has changed
oled.setCursor(60, 6); // position cursor
oled.print(F(" ")); // erase old data
oled.setCursor(60, 6); // position cursor
oled.println(calculatedTime, 1); // display new data
// oled.print(F("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.set1X(); // font size
oled.setCursor(0, row);
for (int i = 0; i < 21; i++) {
oled.print(F("-"));
}
}
void blink() {
digitalWrite(LED_BUILTIN, HIGH);
delay(50);
digitalWrite(LED_BUILTIN, LOW);
}Loading
ssd1306
ssd1306
SHADOW
HIGHLIGHT
PAPER