intval3/src/intval/index.ts

553 lines
12 KiB
TypeScript

'use strict'
const db = require('../db')
const log = require('../log')('intval')
import storage = require('node-persist')
import { pathExists, mkdir } from 'fs-extra'
import { delay } from '../delay'
let Gpio : any
try {
Gpio = require('onoff').Gpio
} catch (e) {
log.warn('Failed including Gpio, using sim')
Gpio = require('../../lib/onoffsim').Gpio
}
const PINS : any = {
fwd : {
pin : 13,
dir : 'out'
},
bwd : {
pin : 19,
dir : 'out'
},
micro : {
pin : 5,
dir : 'in',
edge : 'both'
},
release : {
pin : 6,
dir : 'in',
edge : 'both'
}
}
interface State {
}
interface Entry {
start : number
stop : number
len : number
dir : number
exposure : number
counter : number
sequence : number
}
/** class representing the intval3 features */
export default class Intval {
private STATE_DIR : string = '~/state'
private _frame : any = {
open : 250, //delay before pausing frame in open state
openBwd : 400,
closed : 100, //time that frame actually remains closed for
expected : 530 //expected length of frame, in ms
}
private _release : any = {
min : 20,
seq : 1000,
time : 0,
active : false
}
private _micro : any = {
time : 0,
primed : false, //is ready to stop frame
delay : 10// delay after stop signal before stopping motors
}
private _pin : any = {};
private _state : any = {};
private storage : any;
public sequence : any;
constructor() {
this._init()
}
/**
* Initialize the storage object and bind functions to process events.
*/
private async _init () {
let dirExists : boolean;
const storateOptions = {
dir: this.STATE_DIR,
stringify: JSON.stringify,
parse: JSON.parse,
encoding: 'utf8',
logging: false, // can also be custom logging function
continuous: true, // continously persist to disk
interval: false, // milliseconds, persist to disk on an interval
ttl: false, // ttl* [NEW], can be true for 24h default or a number in MILLISECONDS
//expiredInterval: 2 * 60 * 1000, // [NEW] every 2 minutes the process will clean-up the expired cache
//forgiveParseErrors: false // [NEW]
}
try {
dirExists = await pathExists(this.STATE_DIR);
} catch (err) {
log.error('_init', `Error locating state directory ${this.STATE_DIR}`);
}
if (!dirExists) {
try {
await mkdir(this.STATE_DIR);
} catch (err) {
log.error('_init', `Error creating state directory ${this.STATE_DIR}`);
}
}
try {
await storage.init(storateOptions);
} catch (err) {
log.error('_init', err);
}
try {
await this._restoreState();
} catch (err) {
log.warn('_init', err) ;
this.reset();
this._declarePins();
}
process.on('SIGINT', this._undeclarePins.bind(this));
process.on('uncaughtException', this._undeclarePins.bind(this));
}
/**
* Restore the state from the storage object
*/
private async _restoreState () {
let data : any;
try {
data = await storage.getItem('_state');
} catch (err) {
log.error('_restoreState', err);
}
try {
this._setState(data);
} catch (err) {
log.error('_restoreState', err);
this._setState();
}
this._declarePins();
}
/**
* Creating the state object.
*/
private _setState (data : any = undefined) {
if (typeof data !== 'undefined') {
this._state = data;
this._state.frame.cb = () => {};
log.info('_setState', 'Restored intval state from disk');
return true;
}
log.info('_setState', 'Setting state from defaults');
this._state = {
frame : {
dir : true, //forward
start : 0, //time frame started, timestamp
active : false, //should frame be running
paused : false,
exposure : 0, //length of frame exposure, in ms
delay : 0, //delay before start of frame, in ms
current : {}, //current settings
cb : () => {}
},
counter : 0,
sequence : false
}
this._storeState();
}
/**
* Store the state object.
*/
private _storeState () {
try {
storage.setItem('_state', this._state);
} catch (err) {
log.error('_storeState', err);
}
}
/**
* (internal function) Declares all Gpio pins that will be used.
*/
private _declarePins () {
let pin : any;
for (let p in PINS) {
pin = PINS[p];
if (pin.edge) this._pin[p] = new Gpio(pin.pin, pin.dir, pin.edge);
if (!pin.edge) this._pin[p] = new Gpio(pin.pin, pin.dir);
log.info('_declarePins', { pin : pin.pin, dir : pin.dir, edge : pin.edge });
}
this._pin.release.watch(this._watchRelease.bind(this));
}
/**
* (internal function) Undeclares all Gpio in event of uncaught error
* that interupts the node process.
*/
private _undeclarePins (e : Error) {
log.error('_undeclarePins', e);
if (!this._pin) {
log.warn('_undeclarePins', { reason : 'No pins'});
return process.exit();
}
log.warn('_undeclarePins', { pin : PINS.fwd.pin, val : 0, reason : 'exiting'});
this._pin.fwd.writeSync(0);
log.warn('_undeclarePins', { pin : PINS.bwd.pin, val : 0, reason : 'exiting'});
this._pin.bwd.writeSync(0);
this._pin.fwd.unexport();
this._pin.bwd.unexport();
this._pin.micro.unexport();
this._pin.release.unexport();
process.exit();
}
/**
* Start motor in forward direction by setting correct pins in h-bridge
*/
private _startFwd () {
this._pin.fwd.writeSync(1);
this._pin.bwd.writeSync(0);
}
/**
* Start motor in backward direction by setting correct pins in h-bridge
*/
private _startBwd () {
this._pin.fwd.writeSync(0);
this._pin.bwd.writeSync(1);
}
/**
* Turn off all directions
*/
private _pause () {
this._pin.fwd.writeSync(0);
this._pin.bwd.writeSync(0);
//log.info('_pause', 'frame paused')
}
/**
* Stop motor by setting both motor pins to 0 (LOW)
*/
private _stop () {
const entry : any = {};
const now : number = +new Date();
const len : number = now - this._state.frame.start;
this._pin.fwd.writeSync(0);
this._pin.bwd.writeSync(0);
log.info(`_stop`, { frame : len });
this._pin.micro.unwatch();
this._state.frame.active = false;
if (this._state.frame.cb) this._state.frame.cb(len);
entry.start = this._state.frame.start;
entry.stop = now;
entry.len = len;
entry.dir = this._state.frame.current.dir ? 1 : 0;
entry.exposure = this._state.frame.current.exposure;
entry.counter = this._state.counter;
entry.sequence = this._state.sequence ? 1 : 0;
db.insert(entry);
this._state.frame.current = {};
}
/**
* Callback for watching relese switch state changes.
* Using GPIO 06 on Raspberry Pi Zero W.
*
* 1) If closed AND frame active, start timer, set state primed to `true`.
* 1) If opened AND frame active, stop frame
*
* Microswitch + 10K ohm resistor
* * 1 === open
* * 0 === closed
*
*
* @param {object} err Error object present if problem reading pin
* @param {integer} val Current value of the pin
*
*/
private async _watchMicro (err : Error, val : number) {
const now : number = +new Date();
if (err) {
log.error('_watchMicro', err);
}
//log.info(`Microswitch val: ${val}`)
//determine when to stop
if (val === 0 && this._state.frame.active) {
if (!this._micro.primed) {
this._micro.primed = true;
this._micro.time = now;
//log.info('Microswitch primed to stop motor');
}
} else if (val === 1 && this._state.frame.active) {
if (this._micro.primed && !this._micro.paused && (now - this._state.frame.start) > this._frame.open) {
this._micro.primed = false;
this._micro.time = 0;
await delay(this._micro.delay)
this._stop();
}
}
}
/**
* Callback for watching relese switch state changes.
* Using GPIO 05 on Raspberry Pi Zero W.
*
* 1) If closed, start timer.
* 2) If opened, check timer AND
* 3) If `press` (`now - this._release.time`) greater than minimum and less than `this._release.seq`, start frame
* 4) If `press` greater than `this._release.seq`, start sequence
*
* Button + 10K ohm resistor
* * 1 === open
* * 0 === closed
*
* @param {object} err Error object present if problem reading pin
* @param {integer} val Current value of the pin
*
*/
private _watchRelease (err : Error, val : number) {
const now : number = +new Date();
let press : number = 0;
if (err) {
return log.error(err);
}
//log.info(`Release switch val: ${val}`)
if (val === 0) {
//closed
if (this._releaseClosedState(now)) {
this._release.time = now;
this._release.active = true; //maybe unncecessary
}
} else if (val === 1) {
//opened
if (this._release.active) {
press = now - this._release.time;
if (press > this._release.min && press < this._release.seq) {
this.frame();
} else if (press >= this._release.seq) {
this._sequence();
}
//log.info(`Release closed for ${press}ms`)
this._release.time = 0;
this._release.active = false;
}
}
}
private _sequence () {
if (this.sequence) {
this._state.sequence = this.sequence()
}
}
/**
*
*/
private _releaseClosedState (now : number) {
if (!this._release.active && this._release.time === 0) {
return true;
}
if (this._release.active && (now - this._release.time) > (this._release.seq * 10)) {
return true;
}
return false;
}
/**
* Reset the state and store it.
*/
public reset () {
this._setState();
this._storeState();
}
/**
* Set the default direction of the camera.
* * forward = true
* * backward = false
*
* @param {boolean} [dir=true] Direction of the camera
*/
public setDir (val : boolean = true) {
if (typeof val !== 'boolean') {
return log.warn('Direction must be represented as either true or false');
}
this._state.frame.dir = val;
this._storeState();
log.info('setDir', { direction : val ? 'forward' : 'backward' });
}
/**
* Set the exposure value for a single frame.
*
* @param {integer} val Length in milliseconds
*/
public setExposure (val : number = 0) {
this._state.frame.exposure = val;
this._storeState();
log.info('setExposure', { exposure : val });
}
/**
* Set the delay time between each frame.
*
* @param {integer} val Length in milliseconds
*/
public setDelay (val : number = 0) {
this._state.frame.delay = val;
this._storeState();
log.info('setDelay', { delay : val });
}
/**
* Set the counter to the value.
*
* @param {integer} val Frame number
*/
public setCounter (val : number = 0) {
this._state.counter = val;
this._storeState();
log.info('setCounter', { counter : val });
}
/**
* Begin a single frame with set variables or defaults
*
* @param {?boolean} [dir="null"] (optional) Direction of the frame
* @param {?integer} [exposure="null"] (optional) Exposure time, 0 = minimum
*
*/
public async frame (dir : boolean = null, exposure : number = null) {
if (this._state.frame.active) {
return false
}
if (dir === true || (dir === null && this._state.frame.dir === true) ) {
dir = true;
} else {
dir = false;
}
if (exposure === null && this._state.frame.exposure !== 0) {
exposure = this._state.frame.exposure;
} else if (exposure === null) {
exposure = 0; //default speed
}
this._state.frame.current.exposure = exposure;
this._state.frame.current.dir = dir;
this._state.frame.start = +new Date();
this._state.frame.active = true;
this._pin.micro.watch(this._watchMicro.bind(this));
log.info('frame', {dir : dir ? 'forward' : 'backward', exposure });
if (dir) {
this._startFwd();
} else {
this._startBwd();
}
if (exposure !== 0) {
this._state.frame.paused = true;
if (dir) {
await delay(this._frame.open)
this._pause()
await delay(exposure + this._frame.closed)
this._state.frame.paused = false
this._startFwd()
} else {
await delay(this._frame.openBwd)
this._pause()
await delay(exposure + this._frame.closed)
this._state.frame.paused = false;
this._startBwd()
}
}
if (dir) {
return new Promise (function (resolve : Function, reject : Function) {
this._state.frame.cb = (len : number) => {
this._state.counter++
this._storeState()
return resolve(true)
}
}.bind(this))
} else {
return new Promise (function (resolve : Function, reject : Function) {
this._state.frame.cb = (len : number) => {
this._state.counter--
this._storeState()
return resolve(true)
}
}.bind(this))
}
}
/**
* Returns the state of the
*/
public status () {
return this._state;
}
}
module.exports = Intval