I like stuff that’s standard, modular, dumb, fault tolerant and degrades gracefully. The mount, the camera, the heating, the guiding — these all have different priorities, different needs and complexities. This is why I chose to use the 12V 7.2Ah standard UPS batteries, many of them, in separate circuits. This setup, obvious from the way the charging is solved, is for occasional one night sessions, not continuous deployment.
HEQ5
One circuit is for the mount. I put one or more batteries in parallel, or more like max(): the batteries are isolated from each other through diodes.
DSLR
My Canon 1100D (modified) does work from a USB phone charger, but it has its own 12V battery. It works well, non stop all night, from a single battery, without depleting it. A fellow amateur astronomer has already asked me to make him a similar wonder-box :D See details.
The DSLR was the last item that a voltage other than the one provided by the standard 12V batteries. Its own batteries meant special care, special costs, and thankfully that is over, once and for all.
Autoguider laptop
It’s a veteran Asus netbook that’s been serving me since 2011. It’s slow as hell, even with a brand new SSD. The constant win10 update nagging and all the crapware win10 comes bundled with is not helping the laptops age. Its battery is still working though. However, for redundancy the laptop „charges” from a 12V battery, through a DC-DC boost to its standard input of 19V. One battery at a time. It’s a power hog, thus it has its own circuit. When one battery is done, at around 11V, I just switch to the next one, without any of the rest of the equipment being affected, the laptops own battery providing the needed power in between.
Dew heaters
Since the heaters are dumb rezistors, the circuit itself could not get any more simple, all that’s needed are a few watts. Careful calibration is not a must, the heaters work from any battery and most voltages, even from the batteries „depleted” by the laptop (the DC-DC circuit cuts off at about 11V). The single real danger is a short circuit, which is not particularly damaging (it melts a cable at most), but would interrupt the imaging session for more than half an hour, should the circuit be shared by the heaters and the mount. Sometimes the dew heater is improvised…
Charging
At the time of writing this article, I have 8 such batteries but only two chargers, and I don’t want to buy more. (On the long run it might be a good idea to switch to much larger capacity batteries, but some other time.)
In order to be able to be relaxed about the batteries all being charged without me commuting the chargers, and to exercise, I built this little thingy. It’s controlled by a raspberry pi, which through 2×4 relays, commutes the chargers between the 2×4 batteries. I wanted it to be automated and modular, ie always easy to take apart and use just the charger with one battery. The program’s written in javascript for a node server, it has the standard 10 hours charging timeout, but it also monitors the LED on the charger – which goes dark when the charging is done -, and in case the charging is done before the ten hours, it switches to the next battery without waiting. As an extra, the node script reports to my website, so I can check remotely the status of the charging platform. I enjoyed this home project :)
/* 2x4 battery charger */ /* uj akkutolto */ const DEBUGGING = false; const querystring = require('query-string'); const { exec } = require('child_process'); const https = require('follow-redirects').https; // wiring pi pin value for desired relay status: it is inverted const STATUS_OFF = 1; const STATUS_ON = 0; const CHARGING_LED_IS_OFF_TIMEOUT = "30s"; const TIME_LEFT_INITVALUE = -1000000; var app = { chargers: [], reportingPeriod: "10s", reportedMSAgo: -1, inputPinPeriod_ms: 1000, createInputPin: function (wiringPiPin){ exec("gpio mode "+wiringPiPin+" in"); var inpi = { wiringPiPin: wiringPiPin, status: STATUS_OFF, sameValCnt: 0, }; var that = this; setInterval(function (){ exec("gpio read "+wiringPiPin, function (err, stdout, stderr){ var old = inpi.status; if (stdout.toString){ stdout = stdout.toString("utf8").trim(); } // console.log("ezt olvasta", stdout, typeof stdout); if (stdout+"" == STATUS_ON+""){ //console.log("status is now on"); inpi.status = STATUS_ON; } if (stdout+"" == STATUS_OFF+""){ inpi.status = STATUS_OFF; //console.log("status is now off"); } if (old == inpi.status){ inpi.sameValCnt++; }else{ inpi.sameValCnt = 0; } }); }, this.inputPinPeriod_ms); return inpi; }, charger_getInputPinLightHasBeenOffForALongTime: function (c){ if (!c.inputPin){ return false; } var chargingIndicatorLEDIsDarkMeansNotCharging = (c.inputPin.status == 1); var sameValue_ms = c.inputPin.sameValCnt * this.inputPinPeriod_ms; var longTimeSameValue = ( sameValue_ms >= getMilliSeconds(CHARGING_LED_IS_OFF_TIMEOUT) ); var chargerConnected = (c.chBitState == 1); if (chargingIndicatorLEDIsDarkMeansNotCharging){ if (longTimeSameValue){ if (chargerConnected){ return true; } } } return false; }, charger_resetInputPinCounter: function (c){ if (!c.inputPin){ return false; } c.inputPin.sameValCnt = 0; return true; }, init: function (){ var that = this; if (!this._inited){ this._inited = true; if (DEBUGGING){ this.chargers.push({ period: "180s", emptyTime: "1s", chsAreOnWirPins: [ 4, 1, 5, 6 ], actCh: 0, chBitState: 0, inputPin: this.createInputPin(8), history: [], }); this.chargers.push({ period: "180s", emptyTime: "1s", chsAreOnWirPins: [ 27, 28, 29, 26 ], actCh: 0, chBitState: 0, inputPin: this.createInputPin(9), history: [], }); }else{ // LIVE this.chargers.push({ period: "10h", emptyTime: "1m", chsAreOnWirPins: [ 4, 1, 5, 6 ], actCh: 0, chBitState: 0, inputPin: this.createInputPin(8), history: [], }); this.chargers.push({ period: "10h", emptyTime: "1m", chsAreOnWirPins: [ 27, 28, 29, 26 ], actCh: 0, chBitState: 0, inputPin: this.createInputPin(9), history: [], }); } this.initChargers(); } return this; }, _inited: false, initChargers: function (){ // set gpio as output, // set gpio ports to low, ie turn everyting off this.chargers.forEach(function (charger){ charger.debugging = DEBUGGING; charger.tLeftOnActCh_ms = TIME_LEFT_INITVALUE; charger.chsAreOnWirPins.forEach(function (ch){ exec("gpio mode "+ch+" out && gpio write "+ch+" "+STATUS_OFF); }); }); var that = this; var k = 0; setInterval(function (){ that.chargers.forEach(function (charger, index){ if (charger.inputPin){ console.log("charger["+index+"].inputStatus", charger.inputPin); } if (k % 10 == 0){ that.webLog("the charger["+index+"] is " + (charger.inputPin.status ? "idle" : "charging")+" on channel "+charger.actCh); } }); if (k > 100000){ k = 0; } k++; }, 500); this.webLog("started, chargers.length="+this.chargers.length); this.reportStatusToWeb(); return this; }, processChargers: function (){ var that = this; this.chargers.forEach(function (charger, index){ if (typeof charger.channelsBeenHigh_ms == "undefined"){ charger.channelsBeenHigh_ms = 0; } if (charger.chsAreOnWirPins.length == 0){ return ; } charger.emptyTimeLeft_ms = Math.max( 0, charger.emptyTimeLeft_ms-that._intervalPeriod_ms ); var pre_reason = "time"; if (charger.tLeftOnActCh_ms == TIME_LEFT_INITVALUE){ pre_reason = 'init'; } charger.tLeftOnActCh_ms -= that._intervalPeriod_ms; if (charger.tLeftOnActCh_ms < 0){ charger.tLeftOnActCh_ms = 0; } if (charger.chBitState == 1){ charger.channelsBeenHigh_ms += that._intervalPeriod_ms; }else{ charger.channelsBeenHigh_ms = 0; } // need to advance channel? if ( (that.charger_getInputPinLightHasBeenOffForALongTime(charger)) || (charger.tLeftOnActCh_ms == 0) ){ charger.actChAdvaReason = charger.tLeftOnActCh_ms == 0 ? pre_reason : "inputpin"; charger.history.push({ h_ms: charger.channelsBeenHigh_ms, reason: charger.actChAdvaReason, }); if (charger.history.length > 4){ charger.history.shift(); } that.webLog("advancing charger["+index+"], reason = "+charger.actChAdvaReason); charger.tLeftOnActCh_ms = getMilliSeconds(charger.period); charger.channelsBeenHigh_ms = 0; // turn all off charger.chsAreOnWirPins.forEach(function (ch){ exec("gpio write "+ch+" "+STATUS_OFF); }); charger.chBitState = 0; // find next pin charger.actCh++; if (charger.actCh >= charger.chsAreOnWirPins.length){ charger.actCh = 0; } var pin = charger.chsAreOnWirPins[charger.actCh]; //turn pin on after emptyTime charger.emptyTimeLeft_ms = getMilliSeconds(charger.emptyTime); that.charger_resetInputPinCounter(charger); that.reportStatusToWeb(); setTimeout(function (){ charger.emptyTimeLeft_ms = 0; charger.chBitState = 1; that.charger_resetInputPinCounter(charger); exec("gpio write "+pin+" "+STATUS_ON); that.reportStatusToWeb(); that.webLog("charger["+index+"] turned active ("+charger.actChAdvaReason+") on channel "+charger.actCh); }, getMilliSeconds(charger.emptyTime)/3); } }); var old = (this.reportedMSAgo == -1); old = (old) || (this.reportedMSAgo >= getMilliSeconds(this.reportingPeriod) ); if (old){ this.reportStatusToWeb(); }else{ this.reportedMSAgo += this._intervalPeriod_ms; } return this; }, webPost: function (path, data){ var options = { host: 'csillagtura.ro', port: 443, path: path, method: 'POST' }; var req = http.request(options, function(res) { res.setEncoding('utf8'); res.on('data', function (chunk) { }); }); req.on('error', function(e) { console.log('problem with request: ' + e.message); }); // write data to request body req.write(data); req.end(); }, webGet: function (path){ const options = { protocol: "https:", hostname: 'csillagtura.ro', port: 443, path: path, method: 'GET', }; const req = https.request(options, (res) => { //console.log('statusCode:', res.statusCode); //console.log('headers:', res.headers); res.on('data', (d) => { //process.stdout.write(d); }); }); req.on('error', (e) => { console.error(e); }); req.end(); }, webLog: function (msg){ this.webGet('OBFUSCATED?msg='+encodeURIComponent(JSON.stringify(msg))); return this; }, reportStatusToWeb: function (){ this.reportedMSAgo = 0; var that = this; this.chargers.forEach(function (charger, index){ var state = { chargerIndex: index, charger: charger, reportingPeriod: that.reportingPeriod }; that.webGet( 'OBFUSCATED'+index+'='+encodeURIComponent(JSON.stringify(state)) ); //that.webPost( // 'OBFUSCATED', // 'payload'+index+'='+JSON.stringify(state) //); }); return this; }, _intervalId: -1, _intervalPeriod_ms: 1000, _intervalBlankCycles: 3, run: function (){ if (this._intervalId > -1){ clearInterval(this._intervalId); } var that = this; var blanx = this._intervalBlankCycles+1; this._intervalId = setInterval(function (){ blanx = Math.max(blanx-1, 0); if (blanx == 0){ that.processChargers(); } }, this._intervalPeriod_ms); return this; }, } function getMilliSeconds(n){ if ((n+"").indexOf("s") > -1){ n = parseFloat(n.replace("s", ""))*1000; } if ((n+"").indexOf("m") > -1){ n = parseFloat(n.replace("m", ""))*1000*60; } if ((n+"").indexOf("h") > -1){ n = parseFloat(n.replace("h", ""))*1000*60*60; } return n; } app.init().run();





