const Storage = require('Storage'); const Sched = require('sched'); const Time_utils = require('time_utils'); // Data models // class PrimitiveTimer { constructor(origin, is_running, rate, name) { this.origin = origin || 0; // default rate +1 unit per 1000 ms, countup this.rate = rate || 0.001; this.name = name || ''; this._start_time = Date.now(); this._pause_time = is_running ? null : this._start_time; } display_name() { return this.name ? this.name : this.provisional_name(); } provisional_name() { return (this.rate >= 0 ? 'U' : 'D') + ' ' + Time_utils.formatDuration(this.origin / this.rate); } display_status() { let status = ''; // Indicate timer expired if its current value is <= 0 and it's // a countdown timer if (this.get() <= 0 && this.rate < 0) { status += '!'; } if (this.is_running()) { status += '>'; } return status; } is_running() { return !this._pause_time; } start() { if (!this.is_running()) { this._start_time += Date.now() - this._pause_time; this._pause_time = null; } } pause() { if (this.is_running()) { this._pause_time = Date.now(); } } reset() { this.set(this.origin); } get() { const now = Date.now(); const elapsed = (now - this._start_time) - (this.is_running() ? 0 : (now - this._pause_time)); return this.origin + (this.rate * elapsed); } set(new_value) { const now = Date.now(); this._start_time = (now - new_value / this.rate) + (this.origin / this.rate); if (!this.is_running()) { this._pause_time = now; } } dump() { return { cls: 'PrimitiveTimer', version: 0, origin: this.origin, rate: this.rate, name: this.name, start_time: this._start_time, pause_time: this._pause_time }; } static load(data) { if (!(data.cls == 'PrimitiveTimer' && data.version == 0)) { console.error('Incompatible data type for loading PrimitiveTimer state'); } let loaded = new this(data.origin, false, data.rate, data.name); loaded._start_time = data.start_time; loaded._pause_time = data.pause_time; return loaded; } } class TriangleTimer extends PrimitiveTimer { constructor(origin, is_running, rate, name, increment) { super(origin, is_running, rate, name); this.increment = increment || 1; this.end_alarm = false; this.outer_alarm = false; this.outer_action = 'Cont'; this.pause_checkpoint = null; this.vibrate_pattern = null; this.buzz_count = 4; } provisional_name() { const origin_as_tri = as_triangle( this.origin, this.increment ); return (this.rate >= 0 ? 'U' : 'D') + ' ' + origin_as_tri[0] + '/' + origin_as_tri[1] + ' x' + this.increment; } start() { super.start(); this.emit('status'); } pause() { super.pause(); this.emit('status'); } check_auto_pause() { const current_time = super.get(); if (this.is_running() && this.outer_action == 'Pause') { if (this.pause_checkpoint === null) { this.pause_checkpoint = current_time + this.time_to_next_outer_event() * this.rate; console.debug('timer auto-pause setup: ' + this.pause_checkpoint); } else if ( (this.rate >= 0 && current_time >= this.pause_checkpoint) || (this.rate < 0 && current_time <= this.pause_checkpoint) ) { console.debug('timer auto-pause triggered'); this.pause(); this.set(this.pause_checkpoint); this.pause_checkpoint = null; this.emit('auto-pause'); } } else { this.pause_checkpoint = null; } } reset() { this.pause_checkpoint = null; return super.reset(); } get() { return super.get(); } set(new_value) { return super.set(new_value); } time_to_next_alarm() { if (!this.is_running()) return null; if (this.outer_alarm) { return this.time_to_next_outer_event(); } if (this.end_alarm) { return this.time_to_end_event(); } return null; } time_to_next_event() { let next = null; if (this.outer_alarm || this.outer_action !== 'Cont') { next = this.time_to_next_outer_event(); } if (next === null) { next = this.time_to_end_event(); } return next } time_to_next_outer_event() { const as_tri = as_triangle(super.get(), this.increment); let inner_left = this.rate > 0 ? as_tri[0] - as_tri[1] : as_tri[1]; // Avoid getting stuck if we're paused precisely on the event time if (!inner_left) { inner_left = as_tri[0] + Math.sign(this.rate) * this.increment; } return Math.max(0, inner_left / Math.abs(this.rate)); } time_to_end_event() { if (this.rate <= 0 && this.get() > 0) { return this.get() / Math.abs(this.rate); } return null; } dump() { let data = super.dump(); data.cls = 'TriangleTimer'; data.increment = this.increment; data.end_alarm = this.end_alarm; data.outer_alarm = this.outer_alarm; data.outer_action = this.outer_action; data.pause_checkpoint = this.pause_checkpoint; data.vibrate_pattern = this.vibrate_pattern; data.buzz_count = this.buzz_count; return data; } static load(data) { if (!(data.cls == 'TriangleTimer' && data.version == 0)) { console.error('Incompatible data type for loading TriangleTimer state'); } let loaded = new this( data.origin, false, data.rate, data.name, data.increment); loaded._start_time = data.start_time; loaded._pause_time = data.pause_time; loaded.end_alarm = data.end_alarm; loaded.outer_alarm = data.outer_alarm; loaded.outer_action = data.outer_action; loaded.pause_checkpoint = data.pause_checkpoint; loaded.vibrate_pattern = data.vibrate_pattern; if (data.buzz_count !== undefined) { loaded.buzz_count = data.buzz_count; } return loaded; } } function fixed_ceil(value) { // JavaScript sucks balls return Math.ceil(Math.round(value * 1e10) / 1e10); } function as_triangle(linear_time, increment) { if (increment === undefined) increment = 1; linear_time = linear_time / increment; const outer = fixed_ceil((Math.sqrt(linear_time * 8 + 1) - 1) / 2); const inner = outer - (outer * (outer + 1) / 2 - linear_time); return [outer * increment, inner * increment]; } function as_linear(triangle_time, increment) { if (increment === undefined) increment = 1; const outer = triangle_time[0], inner = triangle_time[1]; return (outer + (outer - 1) % increment + 1) * fixed_ceil(outer / increment) / 2 - outer + inner; } function format_triangle(tri_timer) { const tri = as_triangle(tri_timer.get(), tri_timer.increment); return tri[0] + '/' + Math.ceil(tri[1]); } function format_duration(msec, have_seconds) { const time = Time_utils.decodeTime(msec); time.h += time.d * 24; let str = time.h + ":" + ("0" + time.m).substr(-2); if (have_seconds) { str += ":" + ("0" + time.s).substr(-2); } return str; } // Persistent state // const TIMERS_FILENAME = 'triangletimer.timers.json'; const SETTINGS_FILENAME = 'triangletimer.json'; const SCHEDULED_SAVE_TIMEOUT = 15000; var SAVE_TIMERS_TIMEOUT = null; var SAVE_SETTINGS_TIMEOUT = null; function load_timers() { console.log('loading timers'); let timers = Storage.readJSON(TIMERS_FILENAME, true) || []; if (timers.length) { // Deserealize timer objects timers = timers.map(t => TriangleTimer.load(t)); } else { timers = [new TriangleTimer()]; } return timers; } function save_timers() { console.log('saving timers'); const dumped_timers = TIMERS.map(t => t.dump()); if (!Storage.writeJSON(TIMERS_FILENAME, dumped_timers)) { E.showAlert('Trouble saving timers'); } } function schedule_save_timers() { if (SAVE_TIMERS_TIMEOUT === null) { console.log('scheduling timer save'); SAVE_TIMERS_TIMEOUT = setTimeout(() => { save_timers(); SAVE_TIMERS_TIMEOUT = null; }, SCHEDULED_SAVE_TIMEOUT); } else { console.log('timer save already scheduled'); } } function save_settings() { console.log('saving settings'); if (!Storage.writeJSON(SETTINGS_FILENAME, SETTINGS)) { E.showAlert('Trouble saving settings'); } } function schedule_save_settings() { if (SAVE_SETTINGS_TIMEOUT === null) { console.log('scheduling settings save'); SAVE_SETTINGS_TIMEOUT = setTimeout(() => { save_settings(); SAVE_SETTINGS_TIMEOUT = null; }, SCHEDULED_SAVE_TIMEOUT); } else { console.log('settings save already scheduled'); } } const SETTINGS = Object.assign({ 'view_mode': 0, }, Storage.readJSON(SETTINGS_FILENAME, true) || {}); var TIMERS = load_timers(); const ACTIONS = [ 'Cont', 'Pause', ]; // Persistent data convenience functions function delete_tri_timer(timers, tri_timer) { const idx = timers.indexOf(tri_timer); if (idx !== -1) { timers.splice(idx, 1); } else { console.warn('delete_tri_timer: Bug? Tried to delete a timer not in list'); } // Return another timer to switch UI to after deleting the focused // one return timers[Math.min(idx, timers.length - 1)]; } function add_tri_timer(timers, tri_timer) { // Create a copy of current timer object const new_timer = TriangleTimer.load(tri_timer.dump()); timers.unshift(new_timer); return new_timer; } function set_last_viewed_timer(tri_timer) { const idx = TIMERS.indexOf(tri_timer); if (idx == -1) { console.warn('set_last_viewed_timer: Bug? Called with a timer not found in list'); } else if (idx == 0) { console.debug('set_last_viewed_timer: Already set as last timer'); } else { // Move tri_timer to top of list TIMERS.splice(idx, 1); TIMERS.unshift(tri_timer); set_timers_dirty(); } } function set_timers_dirty() { setTimeout(update_system_alarms, 500); schedule_save_timers(); } function set_settings_dirty() { schedule_save_settings(); } // Alarm handling // function delete_system_alarms() { var alarms = Sched.getAlarms().filter(a => a.appid == 'triangletimer'); for (let alarm of alarms) { console.debug('delete sched alarm ' + alarm.id); Sched.setAlarm(alarm.id, undefined); } Sched.reload(); } function set_system_alarms() { for (let idx = 0; idx < TIMERS.length; idx++) { let timer = TIMERS[idx]; timer.check_auto_pause(); let time_to_next_alarm = timer.time_to_next_alarm(); if (time_to_next_alarm !== null) { console.debug('set sched alarm ' + idx + ' (' + time_to_next_alarm/1000 + ')'); Sched.setAlarm(idx.toString(), { appid: 'triangletimer', timer: time_to_next_alarm, msg: timer.display_name(), js: "load('triangletimer.alarm.js');", data: { idx: idx }, }); } } Sched.reload(); } function update_system_alarms() { delete_system_alarms(); set_system_alarms(); } E.on('kill', () => { save_timers(); }); E.on('kill', () => { save_settings(); }); exports = {TIMERS, SETTINGS, ACTIONS, load_timers, save_timers, schedule_save_timers, save_settings, schedule_save_settings, PrimitiveTimer, TriangleTimer, as_triangle, as_linear, format_triangle, format_duration, delete_tri_timer, add_tri_timer, set_last_viewed_timer, set_timers_dirty, set_settings_dirty, update_system_alarms};