brewstack/equipmentDrivers/heater/heater.js

/**
 * Kettle Heater Driver
 * @module heater
 * @author Dave Leitch
 * @requires brewdefs.js
 * @requires brewlog.js
 * @requires pwm.js
 * @requires i2c.js* @requires events
 * @desc Modulate Kettle heating element to control the power.
 * Every time the state changes an event is emitted to all listeners.
 */
 
const fs = require('fs');
const path = require('path');
 
const brewdefs = require('../../../common/brewdefs.js');
const broker = require('../../../common/broker.js');
// const i2c = require('../../nodeDrivers/i2c/i2c_mraa.js');
const i2c = require('../../nodeDrivers/i2c/i2c_raspi.js');
const pwm = require('../../equipmentDrivers/heater/pwm.js');
const pwm2 = require('../../equipmentDrivers/heater/pwm.js');

const MAX_POWER_W = 3000;
const POWER = "power";

//Heater Mark + Space
const PERIOD_INTERVAL_MS = 10 * 1000;

//shortest on/off time
const MIN_ONOFF_MS = 0.5 * 1000;
const MIN_WATTS = (MAX_POWER_W * MIN_ONOFF_MS) / PERIOD_INTERVAL_MS;
const MAX_WATTS = MAX_POWER_W - MIN_WATTS;
let currentPower;
let prevPower = 0;

let publishPower;
let publishHeater;
const energy = function(){
	const filename = path.join(brewdefs.installDir, "energy.txt");
	let currentEnergy = 0;
	function get() {
		if (!fs.existsSync(filename)) {
			set(currentEnergy);
		}
		const txt = fs.readFileSync(filename);
		currentEnergy = parseFloat(txt.toString());
		return currentEnergy;
	}

	function set(e) {
if (e==0) return;
		currentEnergy = e;
		fs.writeFileSync(filename, currentEnergy.toString());
	}

	return {
		add: e => set(get() + e),
		get,
		reset: () => set(0)
	}
}();
/** 
 @const {number} 
 @desc I2C value used to switch OFF the pump.
*/
const HEATER_OFF = 0;//i2c.LOW;

/** 
 @const {number} 
 @desc I2C value used to switch ON the pump.
*/
const HEATER_ON = 1;//i2c.HIGH;

const HEATER_DEF = {
	i2cPinOut:brewdefs.I2C_KETTLE_OUTPUT_BIT
}

//Report power every so often
const UPDATE_INTERVAL = 60 * 1000;

let _simOptions = null; 
let _updateInterval = 1;//Nominally no speed up

/** 
* Turn off the heater.
* @fires heatEvent
*/
const powerOff = () => {
	i2c.writeBit(HEATER_DEF.i2cPinOut, HEATER_OFF);
	
	//Need to stop timer on final 
	pwm.stop();	
	
	if (publishHeater != null){
		publishHeater(0);
	} else{
		console.error("heater powerOff but service not started?");
	}	
};
	
/** 
* Turn on the heater.
* @fires heatEvent
*/
const powerOn = () => {
	i2c.writeBit(HEATER_DEF.i2cPinOut, HEATER_ON);

	if (publishHeater != null){
		publishHeater(1);
	} else{
		console.error("heater powerOn but service not started?");
	}	

};

function getPower(force = false){
	const w = Math.trunc(currentPower);
		
	if ((currentPower != prevPower) || (force === true)) {
		publishPower(currentPower);
		prevPower = currentPower;
	}

	return w;
}	


/**
 * Alternative PWM approach to switching on and off
 * Ensure power is not toggled too quickly
 * @param {Number} watts 
 */
function setPower(watts){

	if (watts >= MAX_POWER_W){//3000
		//Full on
		currentPower = MAX_POWER_W;
	}else 
	if (watts <= 0) {
		//Full off
		currentPower = 0;
	}else 
	if (watts > MAX_WATTS){//2700
		//Off time is too short. Under power to avoid overshoot.
		currentPower = MAX_WATTS;
	}else
	if (watts < MIN_WATTS){//300
		//On time is too short. Under power to avoid overshoot.
		currentPower = 0;
	}else{
		currentPower = watts;
	}
	currentPower = Math.trunc(currentPower);
	
	const mark = currentPower * PERIOD_INTERVAL_MS / MAX_POWER_W;
	const space = PERIOD_INTERVAL_MS - mark;

	//Update the mark and space periods
//	pwm.restart(mark / _simOptions.speedupFactor, space / _simOptions.speedupFactor);	
	pwm.restart(mark , space);	
		
	if (currentPower != prevPower) {	
		// console.log("currentPower=",currentPower);
		publishPower(currentPower);
		prevPower = currentPower;
	}
}

let timer = null;
let ds18x20 = null;

module.exports = {
	getEnergy: energy.get,
	getPower,
	setPower,
		
	start(brewOptions) {
		return new Promise((resolve, reject) => {
			energy.get();
			_simOptions = brewOptions.sim;
			if (brewOptions.sim.simulate){
				ds18x20 = require('../../../sim/ds18x20.js');
			}
			i2c.init({number:HEATER_DEF.i2cPinOut, dir:i2c.DIR_OUTPUT, value:HEATER_OFF});
			currentPower = 0;
			prevPower = 0;
	
			publishPower = broker.create(POWER);
			publishHeater = broker.create("heater");

			//Define callbacks for PWM mark and space functions
			pwm.init(powerOn, powerOff);
	
			_updateInterval = UPDATE_INTERVAL / _simOptions.speedupFactor;
			
			const J2KWHr = j => j / (3600000);
			//Emit the current power to all listeners every 1 minute
			//timer = setInterval(getPower.bind(null, true), _updateInterval);
			timer = setInterval(() => {
				currentPower = getPower(true);
				energy.add(J2KWHr(currentPower * (_updateInterval / 1000)));
			}, _updateInterval);
	
			resolve(brewOptions);
		});
	},

	off: powerOff,
	
	MAX_W : MAX_POWER_W,
	
	stop() {
		return new Promise((resolve, reject) => {		
			pwm.stop();
			powerOff();
			broker.destroy(POWER);
			broker.destroy("heater");
			clearInterval(timer);
			timer = null;
			resolve();
		});
	},
	
	forceOn() {
		// i2c.start({sim:{simulate:false}});
		i2c.init({number:HEATER_DEF.i2cPinOut, dir:i2c.DIR_OUTPUT, value:HEATER_ON});
	},
	forceOff() {
		// i2c.start({sim:{simulate:false}});
		i2c.init({number:HEATER_DEF.i2cPinOut, dir:i2c.DIR_OUTPUT, value:HEATER_OFF});
	}
}