XRAD'S Live Steam Model Gas Valve Controller
Posted: Wed Nov 15, 2023 9:43 pm
This is the third build in the series of Live Steam Model Boiler Controllers (in order top to bottom pic below).
1) live steam condenser pump (basic mapped code to operate a 5 volt mechanical pump)
2) Live steam automatic boiler fill valve controller
viewtopic.php?p=925948#p925948
3) Live Steam Automatic gas valve controller (today's post)
So for this latest build, I need a way to control the gas burner valve. I decided on a servo with 90 degree throw to a valve with 90 off. good match. Why do I need this? because, if the boiler has hit the blow off PSI (usually about 50-60psi), then any additional heating would be wasted AND as the heating gas expands, it cool significantly, lowering the output pressure. So slow and low gas flow when not needed is best. Once you know the blow off pressure (by reading the PSI output), you can now set the 'pressure' with first selection (highlighted by white DOT and RED LED). this is the trigger point. Above this pressure, the BLUE LED comes on and the gas valve is turned down moderately (to the 'servoEnd' position), but not to OFF, obviously. When the pressure is reduced by steam usage below threshold, then the servo arm moves back to initial setting 'servoBegin' setting, and burner turns up to full blast again. I 'could' also trigger the burner to full ON if the boiler automatic filler (unit 2) is triggered, in order to heat the newest 'colder' water up to steam ASAP (but not really necessary as it passes through a pre-heater). ...but that is for another day.... Or.....I could put all the electronics/code into one build....also for another day. The current modular system is just that, modular. If I do not want to use a component, I can leave it out. and it's easy to place around the hull for balance. Everything runs off a 7.4 50c lipo. more than enough juice for hours....
Automatic Gas Valve Controller code is here. If you are new to arduino/C++ coding, you may enjoy reading through this for tips and tricks. Especially how to run a single rotary encoder for multiple tasks. Also, using some basic float variables converted to integers, and how to take an analog sensor environmental reading and convert it to an adjustable output back into the environment. Enjoy!
1) live steam condenser pump (basic mapped code to operate a 5 volt mechanical pump)
2) Live steam automatic boiler fill valve controller
viewtopic.php?p=925948#p925948
3) Live Steam Automatic gas valve controller (today's post)
So for this latest build, I need a way to control the gas burner valve. I decided on a servo with 90 degree throw to a valve with 90 off. good match. Why do I need this? because, if the boiler has hit the blow off PSI (usually about 50-60psi), then any additional heating would be wasted AND as the heating gas expands, it cool significantly, lowering the output pressure. So slow and low gas flow when not needed is best. Once you know the blow off pressure (by reading the PSI output), you can now set the 'pressure' with first selection (highlighted by white DOT and RED LED). this is the trigger point. Above this pressure, the BLUE LED comes on and the gas valve is turned down moderately (to the 'servoEnd' position), but not to OFF, obviously. When the pressure is reduced by steam usage below threshold, then the servo arm moves back to initial setting 'servoBegin' setting, and burner turns up to full blast again. I 'could' also trigger the burner to full ON if the boiler automatic filler (unit 2) is triggered, in order to heat the newest 'colder' water up to steam ASAP (but not really necessary as it passes through a pre-heater). ...but that is for another day.... Or.....I could put all the electronics/code into one build....also for another day. The current modular system is just that, modular. If I do not want to use a component, I can leave it out. and it's easy to place around the hull for balance. Everything runs off a 7.4 50c lipo. more than enough juice for hours....
Automatic Gas Valve Controller code is here. If you are new to arduino/C++ coding, you may enjoy reading through this for tips and tricks. Especially how to run a single rotary encoder for multiple tasks. Also, using some basic float variables converted to integers, and how to take an analog sensor environmental reading and convert it to an adjustable output back into the environment. Enjoy!
Code: Select all
/*XRAD'S Live Steam Model Boiler Gas Valve Controller 11/11/23
So I wanted to be able to turn down the burner flame when
the boiler blow off valve releases. This will decrease the
gas waste. To do this, I needed a pressure sensor to measure
boiler pressure, and a servo to control the gas valve. Sounds
easy enough, but it was definitely a 'fun' challenge to
write the program.
The input/output flow is basically:
read pressure sensor -> A to D value -> convert value -> set
blow off pressure -> set initial servo arm throw -> compare
real psi to blowoff setting ->adjust final servo arm throw to
turn burner down.
This 'requires' (to save build space) using one rotary encoder
for three functions, and writing/reading/clearing 3 values to
EEPROM. there is also a selection button for choosing between
the 3 switch/case functions.
There is usually a better way to write code, and I am sure
mine can be comresssed a bit with a class for the rotary encoder,
and arrays for the hardware/functions, but it is easy to follow as
is and can be helpful to beginners. Note: PWMServo and Encoder.h
are part of the Teensy bootloader; and easy to use 'interrupts'
come with Encoder.h library.
As always, feel free to use, modify, update...have fun!
Hardware:
using ADAFRUIT SSD1306 128x64 monochrome I2C display (I modified
the base files so the splash screen does not show..these are
great displays and always work with GFX Adacode), Teensy 3.2
(NLA, but easy to use), basic rotary encoder with built in
resistors (on the data and clock wires only, not on switch),
one stand alone led, one momentary button, one 0-100 standard
1/8th NPT pressure transducer from Amazon (not he best, but 14$),
ABS case, and hobby servo 5v.....
things to do: non blocking or mapped servo code for
better throw speed control...but not really needed...
*/
#define ENCODER_USE_INTERRUPTS //must be called before Encoder.h
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Encoder.h>
#include <EEPROM.h>
#include <PWMServo.h>
//rotary encoder variables
Encoder encoder(3, 2); //reverse pins here to reverse rotary knob direction
#define SW 4
unsigned long lastButtonPress = 0;
//variable for rotary encoder LED
#define ledPin 9//ON BLUE when rotary encoder button pressed and EEPROM written,
//also turns ON BLUE when servo activated by pressure
//'selection' push button variables
#define selectionButton 7
#define selectionButtonLed 16 //RED LED turns on when selection button pressed
int selectionButtonState = 1;
int selection = 4;
int count, old_count;
int caseSelection = 3;
int buttonCount = 3;
//EEPROM variables
unsigned int addressPressure = 0; //where to store blow off value in EEPROM
unsigned int addressGasBegin = 10; //where to store servo begin value in EEPROM
unsigned int addressGasEnd = 20; //where to store servo end value in EEPROM
int positionPressure = 0; //the trigger pressure for turning off the gas via servo arm
int positionGasBegin = 0; //where to set beginning of servo movement
int positionGasEnd = 0; //where to set end of servo arm movement
bool allowStorageFlag = false; //can only clear/save to memory if this is true
//servo variables
PWMServo myservo; // create servo object to control a servo
int posServoBegin = 0; // variable to store the servo position
int posServoEnd = 0;
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
// Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)
#define OLED_RESET 4 // Reset pin # (or -1 if sharing Arduino reset pin)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
//variable for pressure sensor 0-100psi
#define sensorPin A0 // input sensor pin
//variables for calculating 0-100 psiVal
const float alpha = 0.95; // Low Pass Filter alpha (0 - 1 )
const float aRef = 3.3; // 3.3 for teensy 3.2....5.1 for others?
float filteredVal = 0; // starting point
float sensorVal;
float voltage;
float psiVal; //this is rounded in the loop!
int psiOldVal;
//display update timer for psi
unsigned long startMillis;
unsigned long currentMillis;
#define updateTime 1000
void setup() {
Serial.begin(9600);
delay(2000);
myservo.attach(SERVO_PIN_B); // attaches the servo on pin 10 teensy 3.2 to the servo object
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3D)) { // Address 0x3D for 128x64
Serial.println(F("SSD1306 allocation failed"));
}
pinMode(SW, INPUT_PULLUP); //rotary encoder button
pinMode(ledPin, OUTPUT); // rotary encoder led
digitalWrite(ledPin, HIGH);
pinMode(selectionButton, INPUT); // push button to change selection
digitalWrite(7, HIGH);
pinMode(selectionButtonLed, OUTPUT); //selection button led
digitalWrite(16, HIGH);
old_count = count = encoder.read(); //encoder variables
EEPROMREADSETUP(); //do this only once to update display variables from EEPROM memory
drawIntro();
digitalWrite(ledPin, LOW);
digitalWrite(selectionButtonLed, LOW);
startMillis = millis();
updateOLEDPressure();
updateOLEDGas();
updatePsi();
servoControlBegin();
myservo.write(positionGasBegin);//write the first sevo position
}
void loop() {
selectionButtonState = digitalRead(selectionButton);
if (selectionButtonState == 0) {
buttonCount = buttonCount + 1;
delay(250);
caseSelection = buttonCount;
if (buttonCount >= 4) {
buttonCount = 0;
caseSelection = 0;
}
//Serial.print("buttonCount: ");
//Serial.println(buttonCount);
//Serial.print("caseSelection: ");
//Serial.println(caseSelection);
}
//case 0 allows real time testing of pressure/servo
//case 1 allows real time postioning of servo
//case 2 is the end position in degrees for servo
//default allows normal loop funtion
switch (caseSelection) {
case 0: //boiler pressure sensor functions
selection = 0;
allowStorageFlag = true;
digitalWrite(16, HIGH);
drawCirclePressure();
break;
case 1: //gas valve servo functions
selection = 1;
allowStorageFlag = true;
digitalWrite(16, HIGH);
clearCirclePressure();
drawCircleGasBegin(); //highlights the target value
break;
case 2: //gas valve servo functions
selection = 2;
allowStorageFlag = true;
digitalWrite(16, HIGH);
clearCircleGasBegin();
drawCircleGasEnd(); //highlights the target value
break;
default:
selection = 3;
allowStorageFlag = false;
digitalWrite(16, LOW);
clearCircleGasEnd();
break;
}
count = encoder.read();
if (selection == 0) {
if (old_count != count)
countPressure();
}
if (selection == 1) {
if (old_count != count)
countGasBegin();
}
if (selection == 2) {
if (old_count != count)
countGasEnd();
}
//choosing 'selection == x' alows for specific associated clear/write EEPROM funtions
if (allowStorageFlag == true) {
int btnState = digitalRead(SW);
if (btnState == LOW) {
if (millis() - lastButtonPress > 50) {
//Serial.println("Button pressed!")
digitalWrite(ledPin, HIGH);
clearEEPROM(); //erase ONLY old data specific to 'selection'
writeEEPROM(); //write ONLY new data specific to 'selection'
delay(500);
digitalWrite(ledPin, LOW);
}
lastButtonPress = millis(); // Remember last button press event
}
}
// get a PSI display integer from and analog input
sensorVal = (float)analogRead(sensorPin); // Read pressure sensor val (A0) WAS= (float)analogRead(sensorPin);
filteredVal = (alpha * filteredVal) + ((1.0 - alpha) * sensorVal); // Low Pass Filter
voltage = (filteredVal / 1024.0) * aRef; // calculate voltage
psiVal = (voltage - 0.4784) / 0.0401; // x=(y-b)/m
int roundedUp = ((int)psiVal);
if (roundedUp != psiOldVal) {
//Serial.print("psiVal: ");
//Serial.println(roundedUp);
//Serial.print("psiOldVal: ");
//Serial.println(psiOldVal);
currentMillis = millis();
if (currentMillis - startMillis >= updateTime) {
updatePsi();
psiOldVal = roundedUp;
startMillis = currentMillis;
}
}
if (selection == 3) {
if (psiVal > positionPressure) {
digitalWrite(ledPin, HIGH);
servoControlEnd();
}
if (psiVal < positionPressure) {
digitalWrite(ledPin, LOW);
servoControlBegin();
}
}
}
//count the rotary ecoder ticks and update position variables
void countPressure() {
if (count) {
if (count > old_count)
positionPressure++;
else
positionPressure--;
if (positionPressure >= 90) {
positionPressure = 90;
} else {
if (positionPressure <= 0) {
positionPressure = 0;
}
}
display.fillRect(97, 0, 20, 10, SSD1306_BLACK);
display.display();
display.setTextSize(1);
display.setCursor(97, 0);
display.println(positionPressure);
display.display();
//Serial.print("PressureSet = ");
//Serial.println(positionPressure);
}
old_count = count;
}
void countGasBegin() {
if (count) {
if (count > old_count)
positionGasBegin++;
else positionGasBegin--;
if (positionGasBegin >= 90) {
positionGasBegin = 90;
} else {
if (positionGasBegin <= 0) {
positionGasBegin = 0;
}
}
display.fillRect(97, 14, 20, 10, SSD1306_BLACK);
display.display();
display.setTextSize(1);
display.setCursor(97, 14);
display.println(positionGasBegin);
display.display();
//Serial.print("GasSet = ");
//Serial.println(positionGasBegin);
myservo.write(positionGasBegin); //used for real time positiong of servo
}
old_count = count;
}
void countGasEnd() {
if (count) {
if (count > old_count)
positionGasEnd++;
else positionGasEnd--;
if (positionGasEnd >= 90) {
positionGasEnd = 90;
} else {
if (positionGasEnd <= 0) {
positionGasEnd = 0;
}
}
display.fillRect(97, 30, 20, 10, SSD1306_BLACK);
display.display();
display.setTextSize(1);
display.setCursor(97, 30);
display.println(positionGasEnd);
display.display();
myservo.write(positionGasEnd);
//Serial.print("GasSet = ");
//Serial.println(positionGasBegin);
}
old_count = count;
}
void updateOLEDPressure() {
display.setTextSize(1);
display.setTextColor(WHITE);
display.setCursor(0, 0);
display.print("GasOffPressure: "); //the set value of blow off pressure
display.setCursor(97, 0);
display.print(positionPressure);
}
void updateOLEDGas() {
display.setTextSize(1);
display.setTextColor(WHITE);
display.setCursor(0, 15);
display.print("ServoArmBegin: "); //how far is servo throw
display.setCursor(97, 15);
display.print(positionGasBegin);
display.setCursor(0, 30);
display.print("ServoArmEnd: "); //how far is servo throw
display.setCursor(97, 30);
display.print(positionGasEnd);
display.display();
}
void updatePsi() {
display.fillRect(80, 45, 40, 15, SSD1306_BLACK);
display.display();
display.setTextSize(2);
display.setCursor(10, 45);
display.print("psi: ");
display.setCursor(80, 45);
display.print(psiVal, 0);
display.display();
}
void drawIntro() {
display.clearDisplay();
display.drawRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, SSD1306_WHITE);
display.drawRect(1, 1, SCREEN_WIDTH - 2, SCREEN_HEIGHT - 2, SSD1306_WHITE); //draw double thick rectangle
display.setCursor(10, 10);
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
display.print(" XRAD'S");
display.setCursor(10, 35);
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.print("MODEL STEAM BOILER");
display.setCursor(5, 50);
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.print("GAS VALVE CONTROLLER");
display.display();
delay(3000);
display.clearDisplay();
}
void drawCirclePressure() {
display.fillCircle(125, 3, 2, SSD1306_WHITE);
display.display();
}
void clearCirclePressure() {
display.fillCircle(125, 3, 2, SSD1306_BLACK);
display.display();
}
void drawCircleGasBegin() {
display.fillCircle(125, 18, 2, SSD1306_WHITE);
display.display();
}
void clearCircleGasBegin() {
display.fillCircle(125, 18, 2, SSD1306_BLACK);
display.display();
}
void drawCircleGasEnd() {
display.fillCircle(125, 33, 2, SSD1306_WHITE);
display.display();
}
void clearCircleGasEnd() {
display.fillCircle(125, 33, 2, SSD1306_BLACK);
display.display();
}
void EEPROMREADSETUP() { //read once to udate variables from memory
positionPressure = EEPROM.read(addressPressure); // read a byte from the current address of the EEPROM
//Serial.print("EEPROMvalueBlowOff: ");
//Serial.println(positionPressure);
positionGasBegin = EEPROM.read(addressGasBegin); // read a byte from the current address of the EEPROM
//Serial.print("EEPROMvalueSetServoArmDegreesBegin: ");
//Serial.println(positionGasBegin);
positionGasEnd = EEPROM.read(addressGasEnd); // read a byte from the current address of the EEPROM
//Serial.print("EEPROMvalueSetServoArmDegreesEnd: ");
//Serial.println(positionGasEnd);
}
void EEPROMREAD() { //specifically read certain addresses based on 'selection'
if (selection == 0) {
positionPressure = EEPROM.read(addressPressure); // read a byte from the current address of the EEPROM
//Serial.print("EEPROMvalueBlowOff: ");
//Serial.println(positionPressure);
}
if (selection == 1) {
positionGasBegin = EEPROM.read(addressGasBegin); // read a byte from the current address of the EEPROM
//Serial.print("EEPROMvalueSetServoArmDegrees: ");
//Serial.println(positionGasBegin);
}
if (selection == 2) {
positionGasEnd = EEPROM.read(addressGasEnd); // read a byte from the current address of the EEPROM
//Serial.print("EEPROMvalueSetServoArmDegreesEnd: ");
//Serial.println(positionGasEnd);
}
}
void clearEEPROM() {
if (selection == 0) {
EEPROM.read(addressPressure); //read address
EEPROM.write(addressPressure, 0); //write 0 to address 0
//Serial.println("EEPROM Pressure erased");
}
if (selection == 1) {
EEPROM.read(addressGasBegin); //read address
EEPROM.write(addressGasBegin, 0); //write 0 to address 0
//Serial.println("EEPROM Gas erased");
}
if (selection == 2) {
EEPROM.read(addressGasEnd); //read address
EEPROM.write(addressGasEnd, 0); //write 0 to address 0
//Serial.println("EEPROM Gas erased");
}
}
void writeEEPROM() {
if (selection == 0) {
EEPROM.write(addressPressure, positionPressure); //write specific data to specific address
//Serial.println("Button pressed!");
//Serial.println("Writing to EEPROM...");
//Serial.print("valueBlowOff: ");
//Serial.println(positionPressure);
}
if (selection == 1) {
EEPROM.write(addressGasBegin, positionGasBegin);
//Serial.println("Button pressed!");
//Serial.println("Writing to EEPROM...");
//Serial.print("valueSetServoArmDegreesBeginning: ");
//Serial.println(positionGasBegin);
}
if (selection == 2) {
EEPROM.write(addressGasEnd, positionGasEnd);
//Serial.println("Button pressed!");
//Serial.println("Writing to EEPROM...");
//Serial.print("valueSetServoArmDegreesEnd: ");
//Serial.println(positionGasEnd);
}
}
//servo stuff
void servoControlBegin() {
posServoBegin = positionGasBegin;
myservo.write(posServoBegin);
}
void servoControlEnd() {
posServoEnd = positionGasEnd;
myservo.write(posServoEnd);
}
//end