Commit b031997f authored by Tim Soderstrom's avatar Tim Soderstrom

Migration from GH

parent 7f8a96fc
Pipeline #41019127 failed with stages
in 1 minute and 28 seconds
This diff is collapsed.
https://electronics.stackexchange.com/questions/99567/arduino-voltage-ladder-and-analog-input-pins
https://www.adafruit.com/product/399
https://www.youtube.com/watch?v=NZCOM9tfudc
http://www.instructables.com/id/Control-DC-and-stepper-motors-with-L298N-Dual-Moto/
https://www.youtube.com/watch?v=suQTE3wx_Jk
http://sgwetplate.com/2016/10/diy-rotary-film-processor/
# DevDawg
DevDawg
=======
Arduino Based Project to Build a DIY Film Rotary Processor
\ No newline at end of file
Open source Arduino-based microcontroller for DIY film development projects.
Overview
--------
This started as a means to simply control the temperature of a water bath
for chemicals and turned into what is currently mostly just a code proof
of concept for what a more full featured controller would look like.
The current code is mostly written for controlling the temperature of a
water bath via PID control, keeping track of development steps, and
controlling the speed and direction of a motor that could be used to make
a rotary. I had also thought about having it also control the state of
a circulation pump.
It is currently based off an Arduino Uno. I did that because such a design is
very easy to convert to a purpose built board while still being DIY friendly
(using mostly through hole components). Right now I'm building it as a prototype
using shields and pre-built boards for things.
Used Libraries
--------------
* RGB LCD Shield (https://github.com/adafruit/Adafruit-RGB-LCD-Shield-Library)
* DB18S20 Temperature Control (https://github.com/milesburton/Arduino-Temperature-Control-Library)
* Arduino PID Library (https://github.com/br3ttb/Arduino-PID-Library/)
Pin Map
-------
Uno:
* I2C: A4, A5
* DB18S20 Temperature Probe: D2
* Buzzer: D3
* Heater Relay: D4
* Motor: D5 (PWM), D6, D7
* Recirc Pump Relay: D8
* SPI: D10-D13 (Maybe, for SD Card functinos)
Needed Parts
------------
Base + Temperature Control:
* Arduino Uno
* Adafruit RGB LCD Shield (https://learn.adafruit.com/rgb-lcd-shield)
* DS18S20 Waterproof Temperature Sensor (e.g. https://www.adafruit.com/product/381)
* 120V Relay (PowerSwitch Tail II is a nice safe choice)
* Submersible Heater
* Container for Water and Chemical Bottles, Film Tank (e.g. Ice Chest)
Rotary Processing:
* DC Motor
* H-Bridge Controller
* A buncha 3D printed and metal and other parts I haven't figured out yet
Current State
-------------
Right now I'm looking for a heating element that can bring water up to at least
40C (104) but ideally up to 50C for being able to pre-heat water for mixing
chemicals as well. I've found a few options, some which seems sketch but basically
to keeps costs low, probably an AC submersible heating element that I can control
with a relay. The PID library can handle both relays and PWM options but for a
decent amount of water, a simple relay may be enough (still need to test).
Otherwise currently the basic UI is implemented allowing one to select various
modes and development processes and a proof of concept of handing tank rotation
has been tested using a motor and an H-bridge driver. There are also routines
to pre-heat the water bath and run through a full development cycle.
A dev cycle is a sequence of development steps to go through such as Development,
Blix, Wash, etc. The controller will track the remaining time while also keeping
the water bath at the desired temperature and managing tank rotation.
The motor rotation stuff works - it will rotate at a provided speed and optionally
change direction at specific intervals. This is implemented using an H-bridge and
a 12V DC motor. Building the rig to actually turn the tank take some design work
and has yet to be done. The code is pretty easy - the physical design work,
not so much.
License
-------
Code is licensed Under the GPLv3. Please see LICENSE for more information.
TODO
----
Near-Term:
* Find a heater element, test/tune PID
* Optional controlling of recirc pump
* Add a buzzer (for warnings and when dev steps finish)
Future Work:
* Purpose built all in one controller board
* Better heating solution (PWM)
* Fancy controller enclosure
* Fancy chemcial bath, rotary solution
* Somehow a means to automatically cool the water for BW?
TODO
----
* Add pause when changing direction (stresses mechanics and electronics otherwise)
/* DevDawg
*
* Open Source Arduino-Based Rotary Film Development Controller.
* by Tim Soderstrom
*
* Licensed under the GPLv3 (see LICENSE for more information)
*/
/**********
* Defines
**********/
#define VERSION "v0.04"
#define BANNER_DELAY_MS 1000
#define RED 0x1
#define YELLOW 0x3
#define GREEN 0x2
#define TEAL 0x6
#define BLUE 0x4
#define VIOLET 0x5
#define WHITE 0x7
#define LCD_COLUMNS 16
#define LCD_ROWS 2
#define SECONDS_MS 1000
#define FORWARD 0
#define REVERSE 1
#define ON 1
#define OFF 0
// Menu Options
#define MAIN_MENU 0
#define DEVELOP_FILM 1
#define PREHEAT 2
#define RUN_MOTOR 3
#define MAX_OPTION 3
// Tunable Defines
// Maximum number of dev steps a particular Dev Process can have.
#define MAX_DEV_STEPS 8
// How often to check the temperature probe
#define TEMP_UPDATE_INTERVAL 10
// Default Preheat Temperature (C)
#define DEFAULT_PREHEAT_TEMP 39
// Manual Temperature Adjustment Steps
#define TEMP_ADJUSTMENT_PRECISION 0.5
// Specified Motor RPM
#define MOTOR_RPM 45
// Slowet Motor Will Turn
#define MOTOR_MIN_RPM 24
// PID Parameters
//#define KP 2
//#define KI 5
//#define KD 1
#define KP 4
#define KI 5
#define KD 2
/***********
* Includes
***********/
// Maybe for the future for storing and easily transferring development processes
// Might be simpler to use I2C and an EEPROM but then there needs to be a mechanicsm
// to add/remove development options
// #include <SPI.h>
// #include <SD.h>
#include <PID_v1.h>
#include <Wire.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <Adafruit_RGBLCDShield.h>
#include "structs.h"
/*******
* Pins
*******/
const byte temperatureProbes = 2;
const byte buzzer = 3;
const byte heater = 4;
const byte motorSpeed = 5;
const byte motorX = 6;
const byte motorY = 7;
const byte recircPump = 8;
/************
* Variables
************/
byte pickedProcess = 0;
byte button = 0;
byte mode = 0;
byte option = 0;
double pidOutput;
double currentTemperature = 0;
double targetTemperature = DEFAULT_PREHEAT_TEMP;
unsigned long pidWindowStartTime;
unsigned int pidWindowSizeMS = 10000;
bool heaterState = OFF;
unsigned long previousMotorMillis = 0;
bool motorDirection = FORWARD;
/**********
* Objects
**********/
Adafruit_RGBLCDShield lcd = Adafruit_RGBLCDShield();
// Setup a oneWire instance to communicate with any OneWire devices (not just Maxim/Dallas temperature ICs)
OneWire oneWire(temperatureProbes);
DallasTemperature sensors(&oneWire);
DeviceAddress thermometer;
// PID Object
PID pid(&currentTemperature, &pidOutput, &targetTemperature, KP, KI, KD, DIRECT);
/******************************
* Development Process Structs
******************************/
// Dev Processes (These can eventually be in the EEPROM)
const byte processCount = 3;
// Dev Processes
const DevProcess process[] =
{
{
"Test",
255,
10,
24,
5,
{
{"Develop", 30, RED},
{"Blix", 35, VIOLET},
{"Wash 1", 36, BLUE},
{"Wash 2", 37, TEAL},
{"Stabilizer", 40, WHITE}
}
},
{
"C41",
255,
10,
38,
5,
{
{"Develop", 195, RED},
{"Blix", 195, VIOLET},
{"Wash 1", 360, BLUE},
{"Wash 2", 120, TEAL},
{"Stabilizer", 60, WHITE}
}
},
{
"Dlta100 DDX 1+4",
255,
10,
20,
3,
{
{"Develop", 720, BLUE},
{"Stop", 60, RED},
{"Fix", 300, WHITE}
}
}
};
DevProcess currentProcess;
/*******
* Code
********/
void setup()
{
pinMode(buzzer, OUTPUT);
pinMode(heater, OUTPUT);
pinMode(motorSpeed, OUTPUT);
pinMode(motorX, OUTPUT);
pinMode(motorY, OUTPUT);
pinMode(recircPump, OUTPUT);
digitalWrite(buzzer, LOW);
digitalWrite(heater, LOW);
analogWrite(motorSpeed, 0);
digitalWrite(motorX, LOW);
digitalWrite(motorY, LOW);
digitalWrite(recircPump, LOW);
Serial.begin(9600);
lcd.begin(LCD_COLUMNS, LCD_ROWS);
/* Print Title and Version */
lcd.setBacklight(YELLOW);
lcd.setCursor(0,0);
lcd.print(F("DevDawg"));
lcd.setCursor(0,1);
lcd.print(VERSION);
delay(BANNER_DELAY_MS);
/* Setup PID */
pidWindowStartTime = millis();
pid.SetOutputLimits(0, pidWindowSizeMS);
pid.SetMode(AUTOMATIC);
currentTemperature = collectTemperatures();
}
void loop()
{
/* Menu Functions */
switch(mode)
{
case PREHEAT:
{
preheat();
mode = 0;
break;
}
case DEVELOP_FILM:
{
developFilm();
mode = 0;
break;
}
case RUN_MOTOR:
{
runMotor();
mode = 0;
break;
}
// Main Menu
default:
{
switch(option)
{
case DEVELOP_FILM:
{
button = wait("Select Mode", "Develop Film");
break;
}
case PREHEAT:
{
button = wait("Select Mode", "Preheat");
break;
}
case RUN_MOTOR:
{
button = wait("Select Mode", "Turn Rotary");
break;
}
default:
{
button = wait("Main Menu","");
break;
}
}
delay(250);
switch(button)
{
case BUTTON_UP:
{
++option;
if(option > 2)
option = 0;
Serial.println(F("Button Up"));
Serial.print(F("Option: "));
Serial.println(option);
break;
}
case BUTTON_DOWN:
{
--option;
if(option > 2)
option = MAX_OPTION;
Serial.println(F("Button Down"));
Serial.print(F("Option: "));
Serial.println(option);
break;
}
case BUTTON_SELECT:
{
Serial.println(F("Button Select"));
mode = option;
option = 0;
break;
}
}
}
}
// Reset back to main menu
button = 0;
}
// Pick a development process using the LCD/Buttons
byte pickProcess()
{
// Selected process (by array index)
byte processIndex = 0;
byte button = 0;
while(true)
{
while(!button)
button = wait("Dev for", process[processIndex].name);
if (button & BUTTON_UP)
{
++processIndex;
if(processIndex > processCount - 1)
processIndex = 0;
}
if (button & BUTTON_DOWN)
--processIndex;
// Greater than because we are using a byte so we need to check
// for overflow rather than negative numbers.
if(processIndex > processCount - 1)
processIndex = processCount - 1;
if (button & BUTTON_SELECT)
return processIndex;
button = 0;
delay(250);
Serial.print("Index: ");
Serial.println(processIndex);
}
}
// Run a development step
void processStep(DevStep step, byte motorSpeed, byte motorDirectionInterval, int targetTemperature)
{
unsigned long currentMillis = millis();
unsigned long previousMillis = 0;
long timeRemainingMS = long(step.duration) * SECONDS_MS;
int currentTemperature = collectTemperatures();
byte tempUpdateCycle = 0;
byte motorUpdateCycle = 0;
byte motorDirection = FORWARD;
Serial.println(step.duration);
Serial.println(motorSpeed);
Serial.println(targetTemperature);
Serial.println(currentTemperature);
Serial.println(timeRemainingMS);
lcd.setBacklight(step.color);
lcd.print(step.name);
while(timeRemainingMS >= 0)
{
currentMillis = millis();
controlTemp();
// Update once a second
if(currentMillis - previousMillis >= SECONDS_MS)
{
Serial.print("Time remaining: ");
Serial.println(timeRemainingMS);
// Every 10 seconds, check temperature
if(tempUpdateCycle > TEMP_UPDATE_INTERVAL - 1)
{
currentTemperature = collectTemperatures();
tempUpdateCycle = 0;
Serial.print("Temperature: ");
Serial.println(currentTemperature);
}
else
++tempUpdateCycle;
controlMotor(motorSpeed, motorDirectionInterval);
updateDisplay(int(timeRemainingMS / SECONDS_MS), currentTemperature);
timeRemainingMS -= SECONDS_MS;
previousMillis = millis();
}
}
}
// Run a full development process
void developFilm()
{
pickedProcess = pickProcess();
currentProcess = process[pickedProcess];
wait(currentProcess.name, "Press Any Key");
controlMotor(0, 0);
// After picking a process, pre-heat then start dev
targetTemperature = currentProcess.targetTemperature;
preheat();
for(byte i = 0 ; i < currentProcess.steps; ++i)
{
processStep(currentProcess.devStep[i],
currentProcess.motorSpeed,
currentProcess.motorDirectionInterval,
currentProcess.targetTemperature);
Serial.println("Motor: Stopping");
controlMotor(0, 0);
Serial.println("done");
wait(currentProcess.devStep[i].name, "Complete");
}
lcd.setBacklight(GREEN);
wait("Dev Cycle", "Complete");
}
void preheat()
{
unsigned long previousMillis = 0;
byte button = 0;
char buffer[17];
char currentTempChar[6];
char targetTempChar[6];
while(true)
{
controlTemp();
if(millis() - previousMillis >= SECONDS_MS * TEMP_UPDATE_INTERVAL || button)
{
currentTemperature = collectTemperatures();
Serial.print("Current Temperature: ");
Serial.println(currentTemperature);
Serial.print("Target Temperature: ");
Serial.println(targetTemperature);
Serial.print("PID Output: ");
Serial.println(pidOutput);
/* Removing for now since for mixing chemicals we want it to still adjust
* temperatures (since heating the chemicals would bring the water
* temperature down over time).
if(currentTemperature == targetTemperature)
{
wait("Target", "Reached");
return;
}
else
{
*/
dtostrf(currentTemperature, 5, 2, currentTempChar);
dtostrf(targetTemperature, 5, 2, targetTempChar);
sprintf(buffer, "%sC / %sC", currentTempChar, targetTempChar);
//sprintf(buffer, "%02dC / %02dC", currentTemperature, targetTemperature);
drawDisplay("Preheating", buffer);
previousMillis = millis();
}
button = lcd.readButtons();
if(button & BUTTON_LEFT)
return;
if(button & BUTTON_UP)
targetTemperature += TEMP_ADJUSTMENT_PRECISION;
if(button & BUTTON_DOWN)
targetTemperature -= TEMP_ADJUSTMENT_PRECISION;
}
}
void controlTemp()
{
pid.Compute();
if (millis() - pidWindowStartTime > pidWindowSizeMS)
pidWindowStartTime += pidWindowSizeMS;
if (pidOutput > millis() - pidWindowStartTime)
{
if(heaterState != ON)
{
heaterState = ON;
Serial.println("Turned Heater On");
}
digitalWrite(heater, HIGH);
}
else
{
if(heaterState != OFF)
{
heaterState = OFF;
Serial.println("Turned Heater Off");
}
digitalWrite(heater, LOW);
}
}
void controlMotor(byte speed, byte directionInterval)
{
analogWrite(motorSpeed, speed);
if(directionInterval > 0)
{
if(millis() - previousMotorMillis >= SECONDS_MS * directionInterval)
{
if(motorDirection == FORWARD)
{
Serial.println("Motor: Reverse");
digitalWrite(motorX, LOW);
digitalWrite(motorY, HIGH);
motorDirection = REVERSE;
}
else
{
Serial.println("Motor: Forward");
digitalWrite(motorX, HIGH);
digitalWrite(motorY, LOW);
motorDirection = FORWARD;
}
previousMotorMillis = millis();
}
}
/* If directionInterval is zero, never change speed */
else
{
digitalWrite(motorX, HIGH);
digitalWrite(motorY, LOW);
motorDirection = FORWARD;
}
}
void updateDisplay(int totalSeconds, int temperature)
{
char buffer[32];
sprintf(buffer, "%02d:%02d Temp: %02dC", int(totalSeconds / 60), int(totalSeconds % 60), temperature);
lcd.setCursor(0,1);
lcd.print(buffer);
}
byte wait(char line1[16], char line2[16])
{
byte button=0;
drawDisplay(line1, line2);
while(!button)
button = lcd.readButtons();
lcd.clear();
delay(250);
return button;
}
void drawDisplay(char line1[16], char line2[16])
{
lcd.clear();
lcd.print(line1);
lcd.setCursor(0,1);
lcd.print(line2);
}
double collectTemperatures()
{
sensors.requestTemperatures();
if(sensors.getAddress(thermometer, 0))
{
return double(sensors.getTempC(thermometer));
}
return -255;
}
void runMotor()
{
byte rpm = MOTOR_RPM;
byte directionInterval = 5;
bool runMotor = true;
char buffer[32];
byte button=0;
sprintf(buffer, "RPM %02d : I %02d", rpm, directionInterval);
drawDisplay("Turning Rotary", buffer);
while(true)
{
if(runMotor)
controlMotor(toSpeed(rpm), directionInterval);
else
controlMotor(0, 0);