Introduction
Astronomy is my primary hobby and taking photographs of night-sky objects is my particular interest. A downside to this hobby is that it is very weather dependent. If it’s cloudy nothing can be seen. Weather reports are important to monitor but they just serve the general area. The sky conditions at my specific location are better monitored with an AllSky Camera.
An AllSky Camera is simply a camera with a fisheye lens that’s pointed up into the sky. A program takes pictures of the sky all night long so checking the sky conditions can be done by looking at the latest sky image. Is it too cloudy to take images? Are clouds starting to move in? Just check the AllSky Camera!
The Issue with Dew
Unfortunately, the dome of the AllSky Camera is prone to have dew forming on it when the humidity gets high. Once that happens, the Allsky images are totally unusable. To combat dew, many AllSky Cameras have a dew heater built into them. The heater in my camera is very simple: Apply 12 VDC and the heater is on. Remove the voltage and the heater is off. This applies about 10 W of power to the heater, and it does get hot enough to keep dew from forming on the dome. Sometimes it gets too hot.
If the humidity is moderate the 10 W of power is way more than needed to keep the dome clear. An unwanted side effect of too much heat is that cameras don’t like it. The hotter a camera gets the noisier, or grainer, its picture becomes. This is especially apparent with long exposures and AllSky Cameras can take up to 60-second exposures under a dark sky! The better solution is to vary the amount of power applied to the heater so that only enough is applied to keep dew from forming, and no more.
The Heater Controller
Designing and building a heater controller that varies the heater power as needed was the answer. Fortunately Adafruit provides most the parts needed for such a project. In addition to a microcontroller, a humidity sensor was needed. I chose to use a commercial wireless sensor that could be mounted outside the observatory. A web page interface to monitor the status of the controller would be convenient.
The Feather M0 Wifi microcontroller was a perfect fit. It has the I/O to read data in, can produce a pulse-width-modulation (PWM) signal out and has a WiFi module that can host a web server. The Acurite 592TXR “Tower Sensor” reads humidity and temperature and transmits the data on the 433 MHz ISM band. To receive that data an inexpensive RXB12 433 MHz receiver module was selected. Finally, a power MOSFET is used to control the current from a 12 VDC power supply to the heater. All these devices are mounted on a half-size proto board. Here’s a diagram of how it’s all connected.
Parts available from Adafruit
Parts available from various online sources
(1) Acurite 592TXR Temperature and Humidity Sensor
(1) RBX12 433MHz Superheterodyne Receiver Module
Putting it all Together
The assembled controller was installed into a 3D printed enclosure so that it would be easy to handle. The 12 VDC power supply plugs into the right jack and the heater cable plugs into the left jack. A USB power supply provides power for the Feather and RXB12 receiver. The temperature/humidity sensor and its 3D printed enclosure was installed on the north-facing wall of the observatory.
When interrogated, the web server simply outputs two lines of text: a header and the current temperature in C and F, dew point, humidity and the percentage of heater power being applied. The server connects to the home network so getting current data is easily done with a browser.
Monitoring the weather conditions and the controller’s response over time was also desirable. A python program was created that interrogates the web server every five minutes and produces a daily CSV formatted file that’s easily read into a spreadsheet. Here’s the data recorded during a recent foggy night.
The blue line shows the humidity level as the fog moved in and later dissipated. The purple line shows the percentage of power that was applied to the heater. It was experimentally determined that for my location, starting heater power at 70% humidity and ramping it up to 100% power at 95% humidity was effective at keeping dew off the Allsky Camera dome. In this case the controller applied 100% heater power for only two hours of the entire fifteen-hour event! That kept camera heating to a minimum and ensured that its images remained crisp and clean.
Feather M0 WiFi code: AllSkyHtr433.ino
/* AllSkyHtr433.ino 3/03/2024 Control Allsky heater with Wx data via 433 MHz * 433 MHz Wx reception code based from : Ray Wang (Rayshobby LLC) * Uses Adafruit Feather M0 WiFi, RXB6/RXB12 433 MHz receiver, MOSFET heater driver * Reads Wx data from an Acurite 592TXR Temperature/Humidity "Tower" sensor * Default PWM output of Feather M0 WiFi is at 733 Hz * Creates a web server to allow retreval of data in csv format */ #include <SPI.h> #include <WiFi101.h> #include "secrets.h" // contains WiFi network connection info // 433 reception defines #define RING_BUFFER_SIZE 256 // large enough to fit data between two successive syncs #define SYNC_LENGTH 2200 #define SYNC_HIGH 600 #define SYNC_LOW 600 #define BIT1_HIGH 400 #define BIT1_LOW 220 #define BIT0_HIGH 220 #define BIT0_LOW 400 // heater function defines #define SENSOR_ID 43 // ID of Acurite Wx sensor to monitor #define DATAPIN 5 // Connection to output of 433 MHz receiver #define PWM_PIN 10 // PWM output to heater circuit #define HTR_ON_THRES 70 // turn heater on >= humidity level #define HTR_MAX_THRES 95 // heater at 100% power >= humidity level // 433 reception global variables unsigned long timings[RING_BUFFER_SIZE]; unsigned int syncIndex1 = 0; // index of the first sync signal unsigned int syncIndex2 = 0; // index of the second sync signal bool received = false; // WiFi connection global variables char ssid[] = SECRET_SSID; // WiFi network SSID char pass[] = SECRET_PASS; // WiFi network password //IPAddress ip(HOST_IP); // static IP address for WiFi board IPAddress ip(192,168,1,215); // static IP address for WiFi board int wifiStatus = WL_IDLE_STATUS; bool wifiPresent = false; WiFiServer server(80); // Wx global variables int tempC = 0; int tempF = 0; int humidity = 0; int dewPoint = 0; // variables from selected sensor int tC = 0; int tF = 0; int hum = 0; int dP = 0; // temporary variables from 433 receiver int pwmPercent = 0; // pwm value to drive heater power with float pwmScale = 100 / (HTR_MAX_THRES - HTR_ON_THRES); // adjust for threshold changes //***** 433 MHz reception routines ***** // detect if a sync signal is present bool isSync(unsigned int idx) { // check if we've received 4 squarewaves of matching timing int i; for(i=0;i<8;i+=2) { unsigned long t1 = timings[(idx+RING_BUFFER_SIZE-i) % RING_BUFFER_SIZE]; unsigned long t0 = timings[(idx+RING_BUFFER_SIZE-i-1) % RING_BUFFER_SIZE]; if(t0<(SYNC_HIGH-100) || t0>(SYNC_HIGH+100) || t1<(SYNC_LOW-100) || t1>(SYNC_LOW+100)) { return false; } } // check if there is a long sync period prior to the 4 squarewaves unsigned long t = timings[(idx+RING_BUFFER_SIZE-i)%RING_BUFFER_SIZE]; if(t<(SYNC_LENGTH-400) || t>(SYNC_LENGTH+400) || digitalRead(DATAPIN) != HIGH) { return false; } return true; } /* Interrupt 1 handler */ void handler() { static unsigned long duration = 0; static unsigned long lastTime = 0; static unsigned int ringIndex = 0; static unsigned int syncCount = 0; // ignore if we haven't processed the previous received signal if (received == true) { return; } // calculating timing since last change long time = micros(); duration = time - lastTime; lastTime = time; // store data in ring buffer ringIndex = (ringIndex + 1) % RING_BUFFER_SIZE; timings[ringIndex] = duration; // detect sync signal if (isSync(ringIndex)) { syncCount ++; // first time sync is seen, record buffer index if (syncCount == 1) { syncIndex1 = (ringIndex+1) % RING_BUFFER_SIZE; } else if (syncCount == 2) { // second time sync is seen, start bit conversion syncCount = 0; syncIndex2 = (ringIndex+1) % RING_BUFFER_SIZE; unsigned int changeCount = (syncIndex2 < syncIndex1) ? (syncIndex2+RING_BUFFER_SIZE - syncIndex1) : (syncIndex2 - syncIndex1); // changeCount must be 122 -- 60 bits x 2 + 2 for sync if (changeCount != 122) { received = false; syncIndex1 = 0; syncIndex2 = 0; } else { received = true; } } } } //*************************************** void setup() { // Configure the Feather ATWINC1500 connections WiFi.setPins(8,7,4,2); Serial.begin(9600); delay(1000); Serial.println("AllSkyHtr433 Started."); pinMode(DATAPIN, INPUT); // put the 433 MHz data pin in input mode attachInterrupt(digitalPinToInterrupt(DATAPIN), handler, CHANGE); pinMode(PWM_PIN, OUTPUT); // put the PWM pin in output mode pinMode(13, OUTPUT); // put the red LED pin in output mode digitalWrite(13, LOW); // turn the red LED off // check for presence of onboard WiFi system: if (WiFi.status() == WL_NO_SHIELD) { wifiPresent = false; Serial.println(">> Onboard WiFi system not detected <<"); } else { // WiFi system is present to try to connect to it wifiPresent = true; WiFi.config(ip); // use this static IP address instead of DNS while (wifiStatus != WL_CONNECTED) { // this should abort if tried too many times Serial.print("Attempting to connect to "); Serial.println(ssid); wifiStatus = WiFi.begin(ssid, pass); delay(5000); // wait 5 seconds to allow for connection: } server.begin(); printWiFiStatus(); // print out wifi status after connection } } int t2b(unsigned int t0, unsigned int t1) { if (t0>(BIT1_HIGH-100) && t0<(BIT1_HIGH+100) && t1>(BIT1_LOW-100) && t1<(BIT1_LOW+100)) { return 1; } else if (t0>(BIT0_HIGH-100) && t0<(BIT0_HIGH+100) && t1>(BIT0_LOW-100) && t1<(BIT0_LOW+100)){ return 0; } return -1; // undefined } void loop() { WiFiClient client = server.available(); // listen for a web client to connect if (client) { // service the web client connection Serial.println("Web client connected"); // an http request ends with a blank line bool currentLineIsBlank = true; while (client.connected()) { if (client.available()) { char c = client.read(); Serial.write(c); if (c == '\n' && currentLineIsBlank) { // send a standard http response header client.println("HTTP/1.1 200 OK"); client.println("Content-Type: text/html"); client.println("Connection: close"); // the connection will be closed after completion of the response //client.println("Refresh: 5"); // refresh the page automatically every 5 sec client.println(); client.println("<!DOCTYPE HTML>"); client.println("<html>"); // output the web page content in csv format client.print("TempC,TempF,DewPoint,Humidity,HtrPower"); client.println("<br />"); client.print(tempC); client.print(","); client.print(tempF); client.print(","); client.print(dewPoint); client.print(","); client.print(humidity); client.print(","); client.print(pwmPercent); client.println("<br />"); client.println("</html>"); break; } } } delay(1); // give the web browser time to receive the data client.stop(); // close the connection Serial.println("client disconnected"); } // end of web client processing if (received == true) { // process the received 433 data // disable interrupt to avoid new data corrupting the buffer detachInterrupt(digitalPinToInterrupt(DATAPIN)); unsigned int startIndex, stopIndex; // extract sensorID value unsigned int sensorID = 0; bool fail = false; bool decodeFail = false; startIndex = (syncIndex1 + (1*8+1)*2) % RING_BUFFER_SIZE; stopIndex = (syncIndex1 + (1*8+8)*2) % RING_BUFFER_SIZE; for(int i=startIndex; i!=stopIndex; i=(i+2)%RING_BUFFER_SIZE) { int bit = t2b(timings[i], timings[(i+1)%RING_BUFFER_SIZE]); sensorID = (sensorID<<1) + bit; if (bit < 0) fail = true; // fail for this value } if (fail) { Serial.print("sensorID decoding error: "); Serial.println(sensorID); // print the erroneous value decodeFail = true; // fail all values for this loop } else { Serial.print("\nSensorID: "); Serial.println(sensorID); } // extract humidity value hum = 0; fail = false; startIndex = (syncIndex1 + (3*8+1)*2) % RING_BUFFER_SIZE; stopIndex = (syncIndex1 + (3*8+8)*2) % RING_BUFFER_SIZE; for(int i=startIndex; i!=stopIndex; i=(i+2)%RING_BUFFER_SIZE) { int bit = t2b(timings[i], timings[(i+1)%RING_BUFFER_SIZE]); hum = (hum<<1) + bit; if (bit < 0) fail = true; // fail for this value } if (fail) { Serial.print("hum decoding error: "); Serial.println(hum); // print the erroneous value decodeFail = true; // fail all values for this loop } else { if (hum < 0) hum = 0; // ensure minimum limit if (hum > 100) hum = 100; // ensure maximum limit Serial.print(hum); Serial.print(","); } // extract temperature from two bytes //unsigned long temp = 0; int temp = 0; fail = false; // most significant 4 bits startIndex = (syncIndex1 + (4*8+4)*2) % RING_BUFFER_SIZE; stopIndex = (syncIndex1 + (4*8+8)*2) % RING_BUFFER_SIZE; for(int i=startIndex; i!=stopIndex; i=(i+2)%RING_BUFFER_SIZE) { int bit = t2b(timings[i], timings[(i+1)%RING_BUFFER_SIZE]); temp = (temp<<1) + bit; if (bit < 0) fail = true; // fail for this value } // least significant 7 bits startIndex = (syncIndex1 + (5*8+1)*2) % RING_BUFFER_SIZE; stopIndex = (syncIndex1 + (5*8+8)*2) % RING_BUFFER_SIZE; for(int i=startIndex; i!=stopIndex; i=(i+2)%RING_BUFFER_SIZE) { int bit = t2b(timings[i], timings[(i+1)%RING_BUFFER_SIZE]); temp = (temp<<1) + bit; if (bit < 0) fail = true; // fail for this value } if (fail) { Serial.print("temp decoding error: "); Serial.println(temp); // print the erroneous value decodeFail = true; // fail all values for this loop } else { tC = int((temp-1024)/10+1.9+0.5); // round to the nearest integer tF = int(tC*9/5+32+0.5); // round to the nearest integer if (!decodeFail) dP = int((tC-(100-hum)/5)*9/5+32+0.5); // simplistic approximation calculation Serial.print(tC); Serial.print(","); Serial.print(tF); Serial.print(","); Serial.println(dP); } // delay for 1 second to avoid repetitions delay(1000); received = false; syncIndex1 = 0; syncIndex2 = 0; // Set Wx values for web page and output PWM signal to Heater if (sensorID == SENSOR_ID && !decodeFail) { // only use selected sensor tempC = tC; tempF = tF; dewPoint = dP; humidity = hum; // save values from selected sensor pwmPercent = int((humidity - HTR_ON_THRES) * pwmScale); // using measured humidity //pwmPercent = int(random(0,100)); // using random humidity for testing if (pwmPercent < 0) pwmPercent = 0; // ensure lower limit if (pwmPercent > HTR_MAX_THRES) pwmPercent = 100; // ensure upper limit Serial.print("Set PWM to "); Serial.print(pwmPercent); Serial.print(" on pin "); Serial.println(PWM_PIN); analogWrite(PWM_PIN, int(pwmPercent*2.55)); // scale to 0-255 range needed by analogWrite } blink13(25); // blink the red LED when 433 MHz data is received // re-enable interrupt attachInterrupt(digitalPinToInterrupt(DATAPIN), handler, CHANGE); } } void printWiFiStatus() { // print the SSID of the network the board is attached to: Serial.print("Connected to SSID: "); Serial.println(WiFi.SSID()); // print the onboard WiFi system IP address: IPAddress ip = WiFi.localIP(); Serial.print("Board's IP Address: "); Serial.println(ip); // print the received signal strength: long rssi = WiFi.RSSI(); Serial.print("signal strength (RSSI):"); Serial.print(rssi); Serial.println(" dBm"); } void blink13(int mS){ digitalWrite(13, HIGH); // turn LED on delay(mS); // wait time in mSec digitalWrite(13, LOW); // turn LED off }
Secrets.h contents:
#define SECRET_SSID "MyNetwork" #define SECRET_PASS "MyPassword" #define HOST_IP "192,168,1,215"
Python CSV recording code: GetAllskyWxData.py
# GetAllskyWxData.py 2024-02-27 import time, os, urllib.request from time import strftime htrURL = "http://192.168.1.215" # feather web server IP dataPath = "D:/Astronomy/AllSky/Wx/" # location to store data files while True: nowDate = strftime("%m/%d/%Y",time.localtime()) nowTime = strftime("%H:%M:%S",time.localtime()) with urllib.request.urlopen(htrURL) as f: # open web page lines = f.readlines() # read all lines of page header = str(lines[2]) # extract header line header = header[header.find('T'):header.find('<')] # parse it header = "Date,Time," + header # add Date, Time data = str(lines[3]) # extract data line data = data[data.find("'")+1:data.find('<')] # parse it # set data csv filename based on date dataFile = strftime("%Y%m%d",time.localtime()) + ".csv" if not os.path.exists(dataPath+dataFile): # if data file doesn't exist f = open(dataPath+dataFile, 'a') # create it with header f.write(header+'\n') f.write(nowDate+","+nowTime+","+data+'\n') f.close() else: f = open(dataPath+dataFile, 'a') # the data file does exist f.write(nowDate+","+nowTime+","+data+'\n') # so just add new data f.close() print(nowDate+","+nowTime+","+data) time.sleep((5*60)-1) # pause for 5 minutes