brewstack/equipmentDrivers/valve/valve.js

/**
 * Valve Module.
 * @module valve
 * @desc This module creates and manages the 8 valves in the system.
 * Each valve has 1 input and 2 outputs. 
 * The input (I2C) causes the valve to open or closes.
 * There is a GPIO output set when the valve is fully closed and fully open.
 * After each open or close command, the state of the valve is verified by waiting upon the valve to indicate that it is open or closed.
 */

const brewdefs = require('../../../common/brewdefs.js');
const brewlog  = require('../../../common/brewlog.js');
const broker   = require('../../../common/broker.js');

// @ts-ignore
let i2c = require('../../nodeDrivers/i2c/i2c_raspi.js');
let valveSwitchDelay = null;
let opt = null;

const ACTIVE = 1;
const INACTIVE = 0;

/** 
 @const {number} 
 @desc I2C value used to OPEN the valve.
*/
const VALVE_OPEN_REQUEST = 0;//i2c.LOW;

/** 
 @const {number} 
 @desc I2C value used to CLOSE the valve.
*/
const VALVE_CLOSE_REQUEST = 1;//i2c.HIGH;		

/** 
 @const
 @desc Definitions for all valves.
 @property {string} name - Unique valve name.
 @property {number} i2cPinOut - I2C pin number used to actuate the valve.
 @property {number} pinOpened - GPIO pin number connected to open signal from the valve.
 @property {number} pinClosed - GPIO pin number connected to close signal from the valve.
*/
const VALVE_DEFS = [{
	name: "ValveFermentIn",//2
	pinOpened: brewdefs.GPIO_VALVE1_OPENED,
	pinClosed: brewdefs.GPIO_VALVE1_CLOSED,
	i2cPinOut: brewdefs.ValveFermentIn
}, {
	name: "ValveChillWortIn",//3	
	pinOpened: brewdefs.GPIO_VALVE2_OPENED,
	pinClosed: brewdefs.GPIO_VALVE2_CLOSED,
	i2cPinOut: brewdefs.ValveChillWortIn
}, {
	name: "ValveChillCleanOut",//4
	pinOpened: brewdefs.GPIO_VALVE3_OPENED,
	pinClosed: brewdefs.GPIO_VALVE3_CLOSED,
	i2cPinOut: brewdefs.ValveChillCleanOut
}, {
	name: "ValveChillColdIn",//5
	pinOpened: brewdefs.GPIO_VALVE4_OPENED,
	pinClosed: brewdefs.GPIO_VALVE4_CLOSED,
	i2cPinOut: brewdefs.ValveChillColdIn
}, {
	name: "ValveKettleIn",//6	
	pinOpened: brewdefs.GPIO_VALVE5_OPENED,
	pinClosed: brewdefs.GPIO_VALVE5_CLOSED,
	i2cPinOut: brewdefs.ValveKettleIn
}, {
	name: "ValveMashIn",	//7
	pinOpened: brewdefs.GPIO_VALVE6_OPENED,
	pinClosed: brewdefs.GPIO_VALVE6_CLOSED,
	i2cPinOut: brewdefs.ValveMashIn
}, {
	name: "ValveFermentTempIn",//8
	pinOpened: brewdefs.GPIO_VALVE7_OPENED,
	pinClosed: brewdefs.GPIO_VALVE7_CLOSED,
	i2cPinOut: brewdefs.ValveFermentTempIn
}
];

const valves = [];
const valveNames = [];

let timeouts = [];
let _started = false;

//Call function that is passed in after a delay
//Use result as resolution
function getDelayedStatus(getStatus) {
	return new Promise((resolve, reject) => {
		timeouts.push(setTimeout(() => {
			let s = getStatus();
			resolve(s);
		}, valveSwitchDelay));
	});
}

//Set input pins during simulation only
function simSetInputs(thisValve, requested) {
	if (opt.sim.simulate !== true) {
		return;
	}

	//set inputs
	if (requested === VALVE_OPEN_REQUEST) {

		// thisValve.openedPin.dir(i2c.DIR_OUTPUT);
		// thisValve.openedPin.write(ACTIVE);
		// thisValve.openedPin.dir(i2c.DIR_INPUT);
		
		// thisValve.closedPin.dir(i2c.DIR_OUTPUT);
		// thisValve.closedPin.write(INACTIVE);
		// thisValve.closedPin.dir(i2c.DIR_INPUT);

		thisValve.status = brewdefs.VALVE_STATUS.OPENED;

	} else if (requested === VALVE_CLOSE_REQUEST) {
		// thisValve.openedPin.dir(i2c.DIR_OUTPUT);
		// thisValve.openedPin.write(INACTIVE);
		// thisValve.openedPin.dir(i2c.DIR_INPUT);

		// thisValve.closedPin.dir(i2c.DIR_OUTPUT);
		// thisValve.closedPin.write(ACTIVE);
		// thisValve.closedPin.dir(i2c.DIR_INPUT);

		thisValve.status = brewdefs.VALVE_STATUS.CLOSED;
	}
};

/**
 * @class Valve
 * @classdesc A pump can be switched on and off. It emits an event every time the state changes to/from on and off. 
 * @param {{name:string, i2cPinOut:number, pinClosed:number, pinOpened:number}} opt - Pump name, I2C output & GPIO input pin numbers.
 */
function Valve(opt) {
    this.requestPin = null;
	this.name = null;

	const thisValve = this;
	thisValve.status = brewdefs.VALVE_STATUS.CLOSED;
	thisValve.timeout = null;
	thisValve.name = opt.name;

	thisValve.requestPin = opt.i2cPinOut;
	i2c.setDir(opt.i2cPinOut, i2c.DIR_OUTPUT);

	thisValve.openOrClose = (requested) => {
		i2c.writeBit(thisValve.requestPin, requested);
		brewlog.info(`thisValve.openOrClose ${thisValve.name}=`, `${requested}`);
		simSetInputs(thisValve, requested);
	};

	/**
	 * Open the valve and verify if it has after a few seconds. 
	 */
	thisValve.open = () => {
		return thisValve.openOrClose(VALVE_OPEN_REQUEST);
	};

	/**
	 * Close the valve and verify if it has after a few seconds.
	 */
	thisValve.close = () => {
		return thisValve.openOrClose(VALVE_CLOSE_REQUEST);
	};
}//valve

module.exports = {
	names: valveNames,

	getStatus: function() {
		let result = [];
		valves.forEach(({ name, status }) => {
			result.push({ name, state: status });
		});
		return result;
	},
	/**
	 * Open a valve by name
	 * @param {string} name - Valve name
	 */
	open(name) {
		brewlog.info("OPEN", name);
	    const v = valves.find(valve => (valve.name === name));	
		if (v) {
		  v.open();
		} else {
		  brewlog.error(`Failed to open ${name}`);
		}
		return (v !== undefined);
	},

	/** 
	 * Close a valve by name
	 * @param {string} name - Valve name
	 */
	close(name) {
		brewlog.info("CLOSE", name);
	    const v = valves.find(valve => (valve.name === name));	
		if (v) {
		  v.close();
		} else {
		  brewlog.error(`Failed to close ${name}`);
		}
		return (v !== undefined);
	},

	OPENED: brewdefs.VALVE_STATUS.OPENED,
	CLOSED: brewdefs.VALVE_STATUS.CLOSED,

	/**
	* Initialize the valve driver and close it. 8 valves are created from initial values.
	*/
	start(brewOptions) {
		return new Promise((resolve, reject) => {
			opt = brewOptions;
			if (_started === true) {
				resolve(brewOptions);
				return;
			}

			valveSwitchDelay = brewOptions.valveSwitchDelay;
			const initValue = { dir: i2c.DIR_OUTPUT, value: VALVE_CLOSE_REQUEST };
			let c = [];
			VALVE_DEFS.forEach(valveDef => {
				const v = new Valve(valveDef);
				valveNames.push(valveDef.name);

				initValue.number = v.requestPin;
				i2c.init(initValue);

				simSetInputs(v, VALVE_CLOSE_REQUEST);

				c.push(v.close);
				valves.push(v);
			});

			_started = true;
			resolve(brewOptions);
		});
	},

	stop(opt) {
		return new Promise((resolve, reject) => {
			//remove all timeouts
			timeouts.forEach(timeout => {
				clearTimeout(timeout);
				timeout = null;
			});
			timeouts = [];

			const allClosed = [];
			valves.forEach(({ close }) => {
				allClosed.push(close);
			});

			Promise.all(allClosed).then(() => {
				valves.forEach(({ name }) => {
					broker.destroy(name);
				});
				_started = false;
				brewlog.info("valve.js", "stopped");

				resolve(opt);
			});
		});
	},

	selfTest() {
		return new Promise((resolve, reject) => {
			const testAll = [];
			valves.forEach(valve => {
				testAll.push(valve.selfTest());
			});
			Promise.all(testAll).then(resolve).catch(console.log);
		});
	}
}