diff --git a/apps/tevtimer/alarm.js b/apps/tevtimer/alarm.js index a4229aeb1..c635f2640 100644 --- a/apps/tevtimer/alarm.js +++ b/apps/tevtimer/alarm.js @@ -1,3 +1,6 @@ +// Derived from `sched.js` from the `sched` app, with modifications +// for features unique to the `tevtimer` app. + // Chances are boot0.js got run already and scheduled *another* // 'load(sched.js)' - so let's remove it first! if (Bangle.SCHED) { @@ -8,6 +11,8 @@ if (Bangle.SCHED) { const tt = require('tevtimer'); function showAlarm(alarm) { + // Alert the user of the alarm and handle the response + const settings = require("sched").getSettings(); const timer = tt.TIMERS[tt.find_timer_by_id(alarm.id)]; if (timer === undefined) { @@ -52,8 +57,8 @@ function showAlarm(alarm) { // Alarm options for chained timer are OK (dismiss) and Halt (dismiss // and pause the triggering timer). let promptButtons = isChainedTimer - ? { "Halt": 'halt', "OK": 'ok' } - : { "Snooze": 'snooze', "OK": 'ok' }; + ? { 'Halt': 'halt', 'OK': 'ok' } + : { 'Snooze': 'snooze', 'OK': 'ok' }; E.showPrompt(message, { title: 'tev timer', buttons: promptButtons, @@ -108,6 +113,8 @@ function showAlarm(alarm) { }); function buzz() { + // Handle buzzing and screen unlocking + if (settings.unlockAtBuzz) { Bangle.setLocked(false); } @@ -127,6 +134,9 @@ function showAlarm(alarm) { } function setNextRepeatDate(alarm) { + // Handle repeating alarms + // This is not used in tevtimer + let date = new Date(alarm.date); let rp = alarm.rp; if (rp===true) { // fallback in case rp is set wrong diff --git a/apps/tevtimer/app.js b/apps/tevtimer/app.js index a63027815..d1d2ffa82 100644 --- a/apps/tevtimer/app.js +++ b/apps/tevtimer/app.js @@ -14,10 +14,13 @@ const MOVE_TO_TOP_TIMEOUT = 5000; // Min number of pixels of movement to recognize a touchscreen drag/swipe const DRAG_THRESHOLD = 50; +// Physical left/right button size in UI const ARROW_BTN_SIZE = 15; +// IDs of main screen labels const ROW_IDS = ['row1', 'row2', 'row3']; +// Fonts to use for each screen label and display format const FONT = { 'row1': { 'start hh:mm:ss': '12x20', @@ -30,7 +33,7 @@ const FONT = { 'name': '12x20', - 'mode': '12x20', + 'format-menu': '12x20', }, 'row2': { @@ -44,7 +47,7 @@ const FONT = { 'name': 'Vector:24x42', - 'mode': 'Vector:26x42', + 'format-menu': 'Vector:26x42', }, 'row3': { @@ -58,10 +61,12 @@ const FONT = { 'name': 'Vector:24x56', - 'mode': 'Vector:26x56', + 'format-menu': 'Vector:26x56', } }; +// List of format IDs available in the format menu +// (in the order they are displayed in the menu) const FORMAT_MENU = [ 'start hh:mm:ss', 'start hh:mm', @@ -72,6 +77,8 @@ const FORMAT_MENU = [ 'name', ]; +// Mapping of format IDs to their human-friendly names displayed in the +// format menu const FORMAT_DISPLAY = { 'start hh:mm:ss': 'Start HMS', 'start hh:mm': 'Start HM', @@ -83,20 +90,29 @@ const FORMAT_DISPLAY = { }; -function row_font(row_name, mode_name) { - let font = FONT[row_name][mode_name]; +function row_font(row_name, format) { + // Convenience function to retrieve the font ID for the given display + // field and format mode + + let font = FONT[row_name][format]; if (font === undefined) { - console.error('Unknown font for row_font("' + row_name + '", "' + mode_name + '")'); + console.error('Unknown font for row_font("' + row_name + '", "' + format + '")'); return '12x20'; } return font; } -// Determine time in milliseconds until next display update for a timer -// that should be updated every `interval` milliseconds. function next_time_update(interval, curr_time, direction) { - if (interval <= 0) { + // Determine time in milliseconds until next display update for a timer + // that should be updated every `interval` milliseconds. + // + // `curr_time` is the current time in milliseconds, and `direction` + // is either 1 (forward) or -1 (backward). The function returns the + // time in milliseconds until the next update, or Infinity if there + // is no update needed (e.g. if interval is zero or negative). + +if (interval <= 0) { // Don't update if interval is zero or negative return Infinity; } @@ -119,6 +135,10 @@ function next_time_update(interval, curr_time, direction) { class TimerView { + // Primary UI for displaying and operating a timer. The + // PrimitiveTimer object is passed to the constructor as a + // parameter. + constructor(timer) { this.timer = timer; @@ -128,6 +148,8 @@ class TimerView { } start() { + // Initialize, display, and activate the UI + this._initLayout(); this.layout.update(); this.layout.clear(); @@ -175,6 +197,8 @@ class TimerView { } stop() { + // Shut down the UI and clean up listeners and handlers + if (this.listeners.timer_render_timeout !== null) { clearTimeout(this.listeners.timer_render_timeout); this.listeners.timer_render_timeout = null; @@ -232,6 +256,10 @@ class TimerView { } render(item) { + // Draw the timer display and update the status and buttons. The + // `item` parameter specifies which part of the display to update. + // If `item` is not specified, the entire display is updated. + console.debug('render called: ' + item); if (!item) { @@ -254,14 +282,14 @@ class TimerView { elem.label = tt.format_duration(this.timer.to_msec(), true); if (running) { update_interval = Math.min( - update_interval, + update_interval, next_time_update(1000, this.timer.to_msec(), this.timer.rate) ); } } else if (mode == 'time hh:mm:ss') { elem.label = locale.time(new Date()).trim(); update_interval = Math.min( - update_interval, + update_interval, next_time_update(1000, Date.now(), 1) ); @@ -325,6 +353,8 @@ class TimerView { } start_stop_timer() { + // Start or pause the timer + if (this.timer.is_running()) { this.timer.pause(); } else { @@ -338,6 +368,10 @@ class TimerView { class TimerFormatView { + // UI for selecting the display format of a timer. The + // PrimitiveTimer object is passed to the constructor as a + // parameter. + constructor(timer) { this.timer = timer; @@ -357,6 +391,8 @@ class TimerFormatView { } start() { + // Initialize, display, and activate the UI + this._initLayout(); this.layout.update(); this.layout.clear(); @@ -430,6 +466,8 @@ class TimerFormatView { } stop() { + // Shut down the UI and clean up listeners and handlers + Bangle.removeListener('drag', this.listeners.drag); Bangle.removeListener('touch', this.listeners.touch); clearWatch(this.listeners.button); @@ -465,7 +503,7 @@ class TimerFormatView { type: 'txt', id: 'row1', label: FORMAT_DISPLAY[FORMAT_MENU[this.format_idx.row1]], - font: row_font('row1', 'mode'), + font: row_font('row1', 'format-menu'), fillx: 1, }, { @@ -491,7 +529,7 @@ class TimerFormatView { type: 'txt', id: 'row2', label: FORMAT_DISPLAY[FORMAT_MENU[this.format_idx.row2]], - font: row_font('row2', 'mode'), + font: row_font('row2', 'format-menu'), fillx: 1, }, { @@ -517,7 +555,7 @@ class TimerFormatView { type: 'txt', id: 'row3', label: FORMAT_DISPLAY[FORMAT_MENU[this.format_idx.row3]], - font: row_font('row3', 'mode'), + font: row_font('row3', 'format-menu'), fillx: 1, }, { @@ -548,10 +586,15 @@ class TimerFormatView { } render() { + // Draw the format selection UI. + this.layout.render(); } update_row(row_id) { + // Render the display format for the given row ID. The row ID + // should be one of 'row1', 'row2', or 'row3'. + const elem = this.layout[row_id]; elem.label = FORMAT_DISPLAY[FORMAT_MENU[this.format_idx[row_id]]]; this.layout.clear(elem); @@ -559,6 +602,9 @@ class TimerFormatView { } incr_format_idx(row_id) { + // Increment the selected format for the given row ID. The row ID + // should be one of 'row1', 'row2', or 'row3'. + this.format_idx[row_id] += 1; if (this.format_idx[row_id] >= FORMAT_MENU.length) { this.format_idx[row_id] = 0; @@ -567,6 +613,9 @@ class TimerFormatView { } decr_format_idx(row_id) { + // Decrement the selected format for the given row ID. The row ID + // should be one of 'row1', 'row2', or 'row3'. + this.format_idx[row_id] -= 1; if (this.format_idx[row_id] < 0) { this.format_idx[row_id] = FORMAT_MENU.length - 1; @@ -574,8 +623,8 @@ class TimerFormatView { this.update_row(row_id); } - // Save new format settings and return to TimerView ok() { + // Save new format settings and return to TimerView for (var row_id of ROW_IDS) { tt.SETTINGS.format[row_id] = FORMAT_MENU[this.format_idx[row_id]]; } @@ -583,31 +632,43 @@ class TimerFormatView { switch_UI(new TimerView(this.timer)); } - // Return to TimerViewMenu without saving changes cancel() { + // Return to TimerViewMenu without saving changes switch_UI(new TimerViewMenu(this.timer)); } } class TimerViewMenu { + // UI for displaying the timer menu. The PrimitiveTimer object is + // passed to the constructor as a parameter. + constructor(timer) { this.timer = timer; } start() { + // Display and activate the top menu of the timer view menu. + this.top_menu(); } stop() { + // Shut down the UI and clean up listeners and handlers + E.showMenu(); } back() { + // Return to the timer view + // (i.e. the timer that was previously displayed) + switch_UI(new TimerView(this.timer)); } top_menu() { + // Display the top-level menu for the timer + const top_menu = { '': { title: this.timer.display_name(), @@ -663,6 +724,9 @@ class TimerViewMenu { } edit_menu() { + // Display the edit menu for the timer. This can be called in + // place of `start` to jump directly to the edit menu. + let keyboard = null; try { keyboard = require("textinput"); } catch (e) {} @@ -724,6 +788,9 @@ class TimerViewMenu { } edit_start() { + // Display the edit > start menu for the timer + // (i.e. the timer's starting value) + let origin_hms = { h: Math.floor(this.timer.origin / 3600), m: Math.floor(this.timer.origin / 60) % 60, @@ -764,24 +831,37 @@ class TimerViewMenu { class TimerMenu { + // UI for displaying the list of timers. The list of + // PrimitiveTimer objects is passed to the constructor as a + // parameter. The currently focused timer is passed as the + // second parameter. + constructor(timers, focused_timer) { this.timers = timers; this.focused_timer = focused_timer; } start() { + // Display the top timer menu + this.top_menu(); } stop() { + // Shut down the UI and clean up listeners and handlers + E.showMenu(); } back() { + // Return to the timer's menu + switch_UI(new TimerViewMenu(this.focused_timer)); } top_menu() { + // Display the top-level menu for the timer list + let menu = { '': { title: "Timers", @@ -798,6 +878,10 @@ class TimerMenu { function switch_UI(new_UI) { + // Switch from one UI to another. The new UI instance is passed as a + // parameter. The old UI is stopped and cleaned up, and the new + // UI is started. + if (CURRENT_UI) { CURRENT_UI.stop(); } diff --git a/apps/tevtimer/lib.js b/apps/tevtimer/lib.js index f4fbe8cf9..62a97f71c 100644 --- a/apps/tevtimer/lib.js +++ b/apps/tevtimer/lib.js @@ -3,7 +3,7 @@ const Sched = require('sched'); const Time_utils = require('time_utils'); -// Convenience // +// Convenience functions // function mod(n, m) { // Modulus function that works like Python's % operator @@ -20,7 +20,20 @@ function ceil(value) { // Data models // class PrimitiveTimer { + // A simple timer object that can be used as a countdown or countup + // timer. It can be paused and resumed, and it can be reset to its + // original value. It can also be saved to and loaded from a + // persistent storage. + constructor(origin, is_running, rate, name, id) { + // origin: initial value of the timer + // is_running: true if the timer should begin running immediately, + // false if it should be paused + // rate: rate of the timer, in units per second. Positive for + // countup, negative for countdown + // name: name of the timer (can be empty) + // id: ID of the timer + this.origin = origin || 0; // default rate +1 unit per 1000 ms, countup this.rate = rate || 0.001; @@ -36,18 +49,26 @@ class PrimitiveTimer { } display_name() { + // Return a string to display as the timer name + // If the name is empty, return a generated name return this.name ? this.name : this.provisional_name(); } provisional_name() { + // Return a generated name for the timer based on the timer's + // origin and current value + return ( - Time_utils.formatDuration(this.origin / Math.abs(this.rate)) + Time_utils.formatDuration(this.to_msec(this.origin)) + ' / ' - + Time_utils.formatDuration(Math.abs(this.get() / Math.abs(this.rate))) + + Time_utils.formatDuration(this.to_msec()) ); } display_status() { + // Return a string representing the timer's status + // (e.g. running, paused, expired) + let status = ''; // Indicate timer expired if its current value is <= 0 and it's @@ -64,10 +85,14 @@ class PrimitiveTimer { } is_running() { + // Return true if the timer is running, false if it is paused + return !this._pause_time; } start() { + // Start the timer if it is paused + if (!this.is_running()) { this._start_time += Date.now() - this._pause_time; this._pause_time = null; @@ -75,6 +100,8 @@ class PrimitiveTimer { } pause() { + // Pause the timer if it is running + if (this.is_running()) { this._pause_time = Date.now(); } @@ -85,6 +112,8 @@ class PrimitiveTimer { } get() { + // Return the current value of the timer, in rate units + const now = Date.now(); const elapsed = (now - this._start_time) @@ -93,6 +122,8 @@ class PrimitiveTimer { } set(new_value) { + // Set the timer to a new value, in rate units + const now = Date.now(); this._start_time = (now - new_value / this.rate) + (this.origin / this.rate); @@ -101,9 +132,9 @@ class PrimitiveTimer { } } - // Convert given timer value to milliseconds using this.rate - // Uses the current value of the timer if no value is provided to_msec(value) { + // Convert given timer value to milliseconds using this.rate + // Uses the current value of the timer if no value is provided if (value === undefined) { value = this.get(); } @@ -111,6 +142,8 @@ class PrimitiveTimer { } dump() { + // Serialize the timer object to a JSON-compatible object + return { cls: 'PrimitiveTimer', version: 0, @@ -127,6 +160,9 @@ class PrimitiveTimer { } static load(data) { + // Deserialize a JSON-compatible object to a PrimitiveTimer + // object + if (!(data.cls == 'PrimitiveTimer' && data.version == 0)) { console.error('Incompatible data type for loading PrimitiveTimer state'); } @@ -143,6 +179,9 @@ class PrimitiveTimer { function format_duration(msec, have_seconds) { + // Format a duration in milliseconds as a string in HH:MM format + // (have_seconds is false) or HH:MM:SS format (have_seconds is true) + if (msec < 0) { return '-' + format_duration(-msec, have_seconds); } @@ -189,10 +228,13 @@ function find_timer_by_id(id) { } function load_timers() { + // Load timers from persistent storage + // If no timers are found, create and return a default timer + console.log('loading timers'); let timers = Storage.readJSON(TIMERS_FILENAME, true) || []; if (timers.length) { - // Deserealize timer objects + // Deserialize timer objects timers = timers.map(t => PrimitiveTimer.load(t)); } else { timers = [new PrimitiveTimer(600, false, -0.001, '', 1)]; @@ -202,6 +244,8 @@ function load_timers() { } function save_timers() { + // Save TIMERS to persistent storage + console.log('saving timers'); const dumped_timers = TIMERS.map(t => t.dump()); if (!Storage.writeJSON(TIMERS_FILENAME, dumped_timers)) { @@ -210,6 +254,11 @@ function save_timers() { } function schedule_save_timers() { + // Schedule a save of the timers to persistent storage + // after a timeout. This is used to reduce the number of + // writes to the flash storage when several changes are + // made in a short time. + if (SAVE_TIMERS_TIMEOUT === null) { console.log('scheduling timer save'); SAVE_TIMERS_TIMEOUT = setTimeout(() => { @@ -222,6 +271,8 @@ function schedule_save_timers() { } function save_settings() { + // Save SETTINGS to persistent storage + console.log('saving settings'); if (!Storage.writeJSON(SETTINGS_FILENAME, SETTINGS)) { E.showAlert('Trouble saving settings'); @@ -229,6 +280,11 @@ function save_settings() { } function schedule_save_settings() { + // Schedule a save of the settings to persistent storage + // after a timeout. This is used to reduce the number of + // writes to the flash storage when several changes are + // made in a short time. + if (SAVE_SETTINGS_TIMEOUT === null) { console.log('scheduling settings save'); SAVE_SETTINGS_TIMEOUT = setTimeout(() => { @@ -255,6 +311,10 @@ var TIMERS = load_timers(); // Persistent data convenience functions function delete_timer(timers, timer) { + // Find `timer` in array `timers` and remove it. + // Return the next timer in the list, or the last one if `timer` + // was the last one in the list. + const idx = timers.indexOf(timer); if (idx !== -1) { timers.splice(idx, 1); @@ -267,6 +327,11 @@ function delete_timer(timers, timer) { } function add_timer(timers, timer) { + // Create a independent timer object duplicating `timer`, assign it a + // new unique ID, and add it to the top of the array `timers`. + // Return the new timer object. + // This is used to create a new timer from an existing one. + // Create a copy of current timer object const new_timer = PrimitiveTimer.load(timer.dump()); // Assign a new ID to the timer @@ -277,6 +342,9 @@ function add_timer(timers, timer) { } function set_last_viewed_timer(timer) { + // Move `timer` to the top of the list of timers, so it will be + // displayed first when the timer list is shown. + const idx = TIMERS.indexOf(timer); if (idx == -1) { console.warn('set_last_viewed_timer: Bug? Called with a timer not found in list'); @@ -291,11 +359,17 @@ function set_last_viewed_timer(timer) { } function set_timers_dirty() { + // Mark the timers as modified and schedule a write to + // persistent storage. + setTimeout(update_system_alarms, 500); schedule_save_timers(); } function set_settings_dirty() { + // Mark the settings as modified and schedule a write to + // persistent storage. + schedule_save_settings(); } @@ -303,6 +377,9 @@ function set_settings_dirty() { // Alarm handling // function delete_system_alarms() { + // Delete system alarms associated with the tevtimer app (except those + // that are snoozed, so that they will trigger later) + var alarms = Sched.getAlarms().filter(a => a.appid == 'tevtimer'); for (let alarm of alarms) { if (alarm.ot === undefined) { @@ -317,6 +394,9 @@ function delete_system_alarms() { } function set_system_alarms() { + // Set system alarms (via `sched` app) for running countdown timers + // that will expire in the future. + for (let idx = 0; idx < TIMERS.length; idx++) { let timer = TIMERS[idx]; let time_to_next_alarm = timer.to_msec(); @@ -335,11 +415,15 @@ function set_system_alarms() { } function update_system_alarms() { + // Refresh system alarms (`sched` app) to reflect changes to timers + delete_system_alarms(); set_system_alarms(); } +// Make sure we save timers and settings when switching to another app +// or rebooting E.on('kill', () => { save_timers(); }); E.on('kill', () => { save_settings(); });