After discovering cheap WiFi module ESP8266 I decided to upgrade my heater that was previously controlled by Arduino and connected to my Raspberry Pi via USB cable. Heater elements are connected to Arduino via Solid State Relay. Arduino uses PID control to achieve defined temperature and room temperature is read using digital sensor DS18B20. I had some OLED display that I got from seeedstudio and used it to display current and defined temperature on it. Arduino is connected to ESP8266. On my Raspbery Pi i made web interface and using simple python scripts i get data from Arduino and set temperature back. On front panel there is also rotary encoder used for setting temperature manually. ESP8266 is loaded with NodeMCU Lua firmware and it currently acts as simple telnet client. from RPI I send “uart.write(0,\”!6\\r\\n\”)\r\n” and ESP8266 sends to serial port !6 with carriage return and arduino send back for example 22.0,2000,21.0 (22.0Celsius current temperature, 2000 is PID output (0-4000) meaning that output is actually 50% and last value 21.0 is temperature that is defined). Sending !1,235 to arduino sets defined temperature to 23.5Celsius. I don’t have schematic but It,s pretty simple. DS18B20 data pin is connected to arduino pin 8 (use 4,7k pull up to +5v), rottary encoder is connected to pins 2 and 3 and two 10nF caps are used for debouncing (dont change pin 2 because interrupt on this pin used for trigerring read function). OLED uses 4 wires +5, GND and SDA, SCL connected to arduino SDA SCL pins. RX TX of arduino is connected to ESP8266 TX RX pins via logic level converter (arduino uses +5V and ESP8266 +3.3V) and that’s all.
Here are some making of photos and shaky video (I was recording holding my tablet in one hand and phone in another) code goes after them…
Arduino 3.3V power supply, Logic Level Converter and ESP8266 on proto board
OLED from seeedstudio
Installed to wall…
Close up of OLED and rottary encoder.
IOS web app (jquery mobile and jquery knob used to set temperature by rotating knob same as rotary encoder)
I had idea to display custom text and draw images on OLED display but atmega328 has only 2Kb of SRAM and just OLED uses 1Kb and I had to remove code because Arduino was constantly crashing. Code is still there but commented. If someone has idea how to free some more SRAM from this code… On OLED temperature and defined temperature is displayed and after 40 seconds display goes to screensaver mode to save OLED from burning out (I think I read somewhere that OLED can burn out same as TFT when same dots are always lit).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 |
#include <SerialCommand.h> #include <SPI.h> #include <Wire.h> #include <OneWire.h> #include <DallasTemperature.h> //https://github.com/milesburton/Arduino-Temperature-Control-Library #include <Adafruit_GFX.h> //https://github.com/adafruit/Adafruit-GFX-Library #include <Adafruit_SSD1306.h> //https://github.com/adafruit/Adafruit_SSD1306 #include <PID_v1.h> //https://github.com/br3ttb/Arduino-PID-Library #define encoder0PinA 2 #define encoder0PinB 3 #define ONE_WIRE_BUS 8 #define OLED_RESET 4 #define WindowSize 4000 #define screenSaverDelay 30000 #define gX0 59 //Screen saver max left so that text is not going into new line #define gY0 8 #define gH 25 #define gW 6 #define RelayPin 13 byte ssaverx= 40; byte ssavery= 20; boolean LCDPC=0; boolean stringComplete = false; unsigned long windowStartTime; byte screenSaverStartTime; byte tempStartTime; unsigned long now; double Setpoint, Input, Output; OneWire oneWire(ONE_WIRE_BUS); DallasTemperature sensors(&oneWire); PID myPID(&Input, &Output, &Setpoint,2,5,1, DIRECT); Adafruit_SSD1306 display(OLED_RESET); SerialCommand sCmd; void setup(){ //Set ENCODER pinMode(encoder0PinA, INPUT); digitalWrite(encoder0PinA, HIGH); // turn on pullup resistor pinMode(encoder0PinB, INPUT); digitalWrite(encoder0PinB, HIGH); // turn on pullup resistor attachInterrupt(0, doEncoder, CHANGE); //END Set ENCODER Serial.begin(9600); Serial.println(F("Online\r\n")); display.begin(SSD1306_SWITCHCAPVCC, 0x3C); // initialize with the I2C addr 0x3C (for the 128x64) display.display(); sensors.begin(); sensors.requestTemperatures(); Input = sensors.getTempCByIndex(0); windowStartTime = millis(); Setpoint = 10.0; screenSaverStartTime=0; tempStartTime=0; myPID.SetOutputLimits(0, WindowSize); myPID.SetMode(AUTOMATIC); sCmd.addCommand("!1", Temperatura); //sCmd.addCommand("!2", setLCDPC); //Comented because there is not enough SRAM !2,0\n\r turns on temperature info from arduino !2,1\n\r clears display and then you can draw or display custom text //sCmd.addCommand("!3", setLCDTXT); //set OLED custom text in format !3, X position, Y postion, Font size (1-4), text message (!3,0,0,1,test message\n\r) //sCmd.addCommand("!4",setLCDPIXEL); // !4,0,0\n\r turns on pixel on first pixel top left. Can be used to draw custom images from PC or... //sCmd.addCommand("!5", setLCDC); // !5\n\r Clears display sCmd.addCommand("!6", setDisplayData); pinMode(RelayPin, OUTPUT); delay(4000); display.clearDisplay(); } void doEncoder() { //Read encoder and set manualy defined temperature. screenSaverStartTime=0; //Turn off screen saver if (digitalRead(encoder0PinA) == digitalRead(encoder0PinB)) { Setpoint=Setpoint+0.05; } else { Setpoint=Setpoint-0.05; } } void loop() { sCmd.readSerial(); myPID.Compute(); now = millis(); if(now - windowStartTime>WindowSize) { windowStartTime += WindowSize; ssaverx=random(5,68); ssavery=random(0,43); screenSaverStartTime++; tempStartTime++; if (tempStartTime>4){ sensors.requestTemperatures(); Input = sensors.getTempCByIndex(0); tempStartTime=0; } if (screenSaverStartTime>10){ screenSaverStartTime=100; } } if(Output > now - windowStartTime){ digitalWrite(RelayPin,HIGH); } else { digitalWrite(RelayPin,LOW); } if (LCDPC == 0){ if (screenSaverStartTime>10){ screensaver(); } else{ powerGraph(); } } } void screensaver(){ setBrightness(display,1); display.clearDisplay(); display.setTextColor(WHITE); display.setTextSize(2); display.setCursor(ssaverx,ssavery); display.print(Input,1); display.setTextSize(1); display.print(F("o")); display.setCursor(ssaverx-2,ssavery+15); display.print(Setpoint,1); display.print(F("C-")); display.print(map(Output,0,WindowSize,0,100)); //display.print(F("%")); if (digitalRead(RelayPin)==HIGH){ display.print(F("H")); } else{ display.print(F("L")); } // display.print(freeMemory()); display.display(); }//*********************************************END screensaver void powerGraph(){ setBrightness(display,255); display.fillRect(0,0, 127, 63, BLACK); display.setTextColor(WHITE); display.drawRect(0,6, 50, 27, WHITE); display.setTextSize(1); display.setCursor(2,8); display.print(F("Temp")); display.setCursor(2,17); display.setTextSize(2); display.print(Input,1); display.drawRect(77,6, 50, 27, WHITE); display.setTextSize(1); display.setCursor(78,8); display.print(F("Def")); display.setCursor(78,17); display.setTextSize(2); display.print(Setpoint,1); display.fillRect(gX0, gY0, gW, gH, BLACK); display.drawRect(gX0, gY0, gW, gH, WHITE); display.fillRect(gX0,gY0+gH-map(Output,0,WindowSize,0,gH),gW,map(Output,0,WindowSize,0,gH),WHITE); display.setTextSize(1); display.setCursor(52,0); if (map(Output,0,WindowSize,0,100)<100) display.setCursor(55,0); if (map(Output,0,WindowSize,0,100)<10) display.setCursor(57,0); display.print(map(Output,0,WindowSize,0,100)); display.print(F("%")); if (digitalRead(RelayPin)==HIGH){ display.fillCircle(62,38,4,WHITE); } else{ display.fillCircle(62,38,4,BLACK); } // display.setCursor(0,50); // display.print(freeMemory()); display.display(); }//*********************************************END powerGraph void setBrightness(Adafruit_SSD1306 display, uint8_t brightness) { display.ssd1306_command(SSD1306_SETCONTRAST); display.ssd1306_command(brightness); }//*********************************************END setBrightness void Temperatura() { char *arg; arg = sCmd.next(); if (arg != NULL) { Setpoint = atoi(arg); Setpoint = Setpoint/10; Serial.print(F("print\"")); Serial.print(Setpoint); Serial.println(F("\"")); } }//*********************************************END Temperatura // Code commented because of insuficient SRAM :( // void setLCDPC(){ // char *arg; // arg = sCmd.next(); // if (arg != NULL) { // LCDPC = atoi(arg); // } // if (LCDPC == 1){ // display.fillRect(0, 0, 127, 63, BLACK); // display.display(); // } // else{ // screenSaverStartTime=0; // } // Serial.print(F("print\"")); // Serial.print(LCDPC,DEC); // Serial.println(F("\"")); // }//*********************************************END setLCDPC // void setLCDC(){ // display.fillRect(0,0, 127, 63, BLACK); // display.display(); // Serial.println(F("print\"OK\"")); // }//*********************************************END setLCDC // void setLCDTXT(){ // byte lcdtxtv1; // byte lcdtxtv2; // setBrightness(display,255); // display.setTextColor(WHITE); // char *arg; // arg = sCmd.next(); // if (arg != NULL) { // lcdtxtv1 = atoi(arg); // } // arg = sCmd.next(); // if (arg != NULL) { // lcdtxtv2 = atoi(arg); // } // display.setCursor(lcdtxtv1,lcdtxtv2); // arg = sCmd.next(); // if (arg != NULL) { // lcdtxtv1 = atoi(arg); // } // display.setTextSize(lcdtxtv1); // arg = sCmd.next(); // if (arg != NULL) { // } // display.print(arg); // display.display(); // Serial.println(F("print\"OK\"")); // } //*********************************************END setLCDTXT void setDisplayData(){ Serial.print(F("print\"")); Serial.print(Input,2); Serial.print(F(",")); Serial.print(Output,0); Serial.print(F(",")); Serial.print(Setpoint,1); Serial.println(F("\"")); }//*********************************************END setDisplayData // void setLCDPIXEL(){ // byte lcdtxtv1; // byte lcdtxtv2; // char *arg; // arg = sCmd.next(); // if (arg != NULL) { // lcdtxtv1 = atoi(arg); // } // arg = sCmd.next(); // if (arg != NULL) { // lcdtxtv2 = atoi(arg); // } // display.drawPixel(lcdtxtv1,lcdtxtv2,WHITE); // display.display(); // } |
ESP8266 is set to AP and Client mode so that if connection to my router is off I can log manually to Heater (AP mode) and change Client parameters if needed. So you need to execute this…
1 2 3 |
wifi.setmode(wifi.STATIONAP) wifi.sta.config("heater","your_password") wifi.ap.config({ssid="Your_Router_SSID",pwd="Routers_Password"}) |
and this two scripts are uploaded to ESP8266 (you can use Lua Loader)
init.lua
1 |
dofile("tel.lua") |
tel.lua
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
function startServer() s=net.createServer(net.TCP,180) s:listen(1001,function(c) function s_output(str) if(c~=nil) then c:send(str) end end node.output(s_output, 0) c:on("receive",function(c,l) node.input(l) end) c:on("disconnection",function(c) node.output(nil) end) --print("Connected\n\r") end) end tmr.alarm(0,1000, 1, function() if wifi.sta.getip()=="0.0.0.0" then else startServer() tmr.stop(0) end end) |
Python scripts for reading and setting temperatures…
set.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import telnetlib import sys import time zadata= sys.argv[1] HOST="192.168.0.5" tn = telnetlib.Telnet(HOST,1001) tn.write("uart.write(0,\"!1,"+zadata+"\\r\\n\")\r\n") time.sleep(0.1) tn.write("uart.write(0,\"!6\\r\\n\")\r\n") data=tn.read_until("\n")[4:-1] spdata=data.split(",") temperature=spdata[0] powerout=int(spdata[1])/4000*100 defined=spdata[2] |
read.py
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import telnetlib HOST = "192.168.0.5" tn = telnetlib.Telnet(HOST,1001) tn.write("uart.write(0,\"!6\\r\\n\")\r\n") data=tn.read_until("\n")[2:-1] spdata=data.split(",") temperature=spdata[0] powerout=int(spdata[1])/40 defined=spdata[2] print str(temperature)+","+str(powerout)+","+str(defined) |
web interface php page (jquery mobile and jquery knob…)
webint.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 |
<?php $full=1; if(isset($_GET["temperature"])) { $settemp=$_GET["temperature"]; $command = escapeshellcmd('/usr/bin/python /media/exthdd/www/heat/py/set.py '.$settemp); $output = shell_exec($command); } $command = escapeshellcmd('/usr/bin/python /media/exthdd/www/heat/py/read.py'); $output = shell_exec($command); $data = explode(",",$output); $temperature = $data[0]; $powerout = $data[1]; $defined = trim($data[2]); $defined10=$defined*10; ?> <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="initial-scale=1.0, user-scalable=no"> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="black"> <title>Heat Controller</title> <!-- iPhone --> <link href="images/apple-touch-icon-57x57.png" sizes="57x57" rel="apple-touch-icon"> <!-- iPad --> <link href="images/apple-touch-icon-72x72.png" sizes="72x72" rel="apple-touch-icon"> <!-- iPhone (Retina) --> <link href="images/apple-touch-icon-114x114.png" sizes="114x114" rel="apple-touch-icon"> <!-- iPad (Retina) --> <link href="images/apple-touch-icon-144x144.png" sizes="144x144" rel="apple-touch-icon"> <!-- iPhone --> <link href="images/apple-touch-startup-image-320x460.png" media="(device-width: 320px)" rel="apple-touch-startup-image"> <!-- iPhone (Retina) --> <link href="images/apple-touch-startup-image-640x920.png" media="(device-width: 320px) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image"> <!-- iPad (portrait) --> <link href="images/apple-touch-startup-image-768x1004.png" media="(device-width: 768px) and (orientation: portrait)" rel="apple-touch-startup-image"> <!-- iPad (landscape) --> <link href="images/apple-touch-startup-image-748x1024.png" media="(device-width: 768px) and (orientation: landscape)" rel="apple-touch-startup-image"> <!-- iPad (Retina, portrait) --> <link href="images/apple-touch-startup-image-1536x2008.png" media="(device-width: 768px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image"> <!-- iPad (Retina, landscape) --> <link href="images/apple-touch-startup-image-1496x2048.png" media="(device-width: 768px) and (orientation: landscape) and (-webkit-device-pixel-ratio: 2)" rel="apple-touch-startup-image"> <link rel="stylesheet" href="http://cdn.jtsage.com/datebox/1.4.4/jqm-datebox-1.4.4.min.css" /> <link rel="stylesheet" href="http://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.css" /> <script src="http://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script> <script src="http://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.js"></script> <script type="text/javascript" src="http://cdn.jtsage.com/external/jquery.mousewheel.min.js"></script> <script type="text/javascript" src="http://cdn.jtsage.com/datebox/1.4.4/jqm-datebox-1.4.4.core.min.js"></script> <script type="text/javascript" src="http://cdn.jtsage.com/datebox/1.4.4/jqm-datebox-1.4.4.mode.flipbox.min.js"></script> <script src="http://cdn.jque.re/plugins/forms-controls/jquery-knob/js/jquery.knob.js"></script> </head> <body> <!-- Home --> <div data-role="page" id="page1"> <div data-theme="a" data-role="header" data-id="persistent" data-position="fixed"> <h3> Heat Controller </h3> <a data-role="button" data-transition="turn" data-icon="refresh" data-iconpos="notext" class="ui-btn-right" onclick="history.go(0);">Refresh</a> <a data-role="button" data-icon="home" class="ui-btn-left" data-iconpos="notext" href="http://siklosi.duckdns.org/mobile.php" ></a> </div> <div data-role="content"> <style> .infinite { background-color: rgba(38, 38, 38, 0.5); } </style> <?php echo "Current Temperature:<b> ".$temperature."°C<br></b>"; echo "Defined temperature:<b> ".$defined."°C<br></b>"; echo "Power Output:<b> ".$powerout."%<br></b>"; echo '<form id="heat" action="webint.php" method="get">'; echo '<input type="hidden" id="temperature" class="temperature" value="'.$defined10.'" name="temperature" readonly>'; echo ' <input type="submit" value="Change temperature"> </form>';?> <script> $(function () { var v, up = 0, down = 0, i = <?php echo $defined;?>, $itemp = $("input.temp"), $ival = $("div.ival"), incr = function () { i=i+0.1; $('input.temperature').val(i.toFixed(1)*10); $ival.html(i.toFixed(1)+"°C").fadeIn(); }, decr = function () { i=i-0.1; $('input.temperature').val(i.toFixed(1)*10); $ival.html(i.toFixed(1)+"°C").fadeIn(); }; $("input.infinite").knob({ min: 0, max: 60, stopper: false, change: function () { if (v > this.cv) { if (up) { decr(); up = 0; } else { up = 1; down = 0; } } else { if (v < this.cv) { if (down) { incr(); down = 0; } else { down = 1; up = 0; } } } v = this.cv; } }); }); </script> <?php echo ' <div class="ival" style="text-align: center; margin: 0 auto; font-family: verdana; font-size: 24px; color: #FF3399" >'.$defined.'°C</font></div> <table style="text-align: center; margin: 0 auto;"> <tr> <td> <input type="text" value="30" class="infinite ui-input-text ui-body-a" data-width="200" data-cursor=true data-thickness=".5" data-fgColor="#FF3399" data-bgColor="#0099FF" data-displayInput="false" > </td></tr></table> '; ?> </div> </div> </body> </html> |
And here are all files zipped
heater_files.zip