I like horizontal extensions :)

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

an improvised dew heater

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 :)

the „left” module

the „left” module

the RPi in the middle

the RPi in the middle

the „right” module

the „right” module

the chargers and the light/dark detection modules

the chargers and the light/dark detection modules

the „right” quartet

the „right” quartet

 

The web interface I can check to see what the batteries are doing

 

/* 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();

 

Facebooktwitterredditpinterestlinkedinmail