From 39ce60213210c454f0f3a200c625e86d875fd496 Mon Sep 17 00:00:00 2001 From: Peter Prince Date: Mon, 24 May 2021 14:59:01 +0100 Subject: [PATCH] Add 1.5.0 changes --- about.js | 8 +- constants.js | 79 +++-- expansion/expansion.html | 357 +++++++++++++++----- expansion/split.html | 181 ++++++++++ expansion/uiCommon.js | 237 ++++++++++++++ expansion/uiExpansion.js | 421 ++++++++++++++++++------ expansion/uiSplit.js | 329 +++++++++++++++++++ expansion/wavHeader.js | 227 +++++++------ index.html | 15 +- lifeDisplay.js | 304 ++++++++++++----- main.js | 308 ++++++++++++++--- nightMode.js | 11 +- package.json | 7 +- packetReader.js | 163 +++++++-- saveLoad.js | 161 ++++++--- schedule/dateInput.js | 48 ++- schedule/schedule.html | 2 +- schedule/schedule.js | 8 +- schedule/scheduleEditor.js | 20 +- schedule/timeInput.js | 112 +++---- schedule/uiSchedule.js | 52 ++- schedule/uiScheduleEditor.js | 62 ++-- scheduleBar.js | 83 +++-- settings/advanced.html | 139 +------- settings/durationInput.js | 60 ++-- settings/filtering.html | 171 ++++++++++ settings/uiAdvanced.js | 49 +++ settings/uiFiltering.js | 505 ++++++++++++++++++++++------ settings/uiSettings.js | 94 +++--- timeHandler.js | 78 ++--- ui.css | 7 + ui.js | 28 +- uiIndex.js | 617 +++++++++++++++++++++++++---------- uiNight.css | 7 + versionChecker.js | 30 +- 35 files changed, 3680 insertions(+), 1300 deletions(-) create mode 100644 expansion/split.html create mode 100644 expansion/uiCommon.js create mode 100644 expansion/uiSplit.js create mode 100644 settings/filtering.html create mode 100644 settings/uiAdvanced.js diff --git a/about.js b/about.js index db623d8..5519038 100644 --- a/about.js +++ b/about.js @@ -11,10 +11,10 @@ const electron = require('electron'); const audiomoth = require('audiomoth-hid'); -var versionDisplay = document.getElementById('version-display'); -var electronVersionDisplay = document.getElementById('electron-version-display'); -var audiomothHidVersionDisplay = document.getElementById('audiomoth-hid-version-display'); -var websiteLink = document.getElementById('website-link'); +const versionDisplay = document.getElementById('version-display'); +const electronVersionDisplay = document.getElementById('electron-version-display'); +const audiomothHidVersionDisplay = document.getElementById('audiomoth-hid-version-display'); +const websiteLink = document.getElementById('website-link'); versionDisplay.textContent = 'Version ' + electron.remote.app.getVersion(); electronVersionDisplay.textContent = 'Running on Electron version ' + electron.remote.process.versions.electron; diff --git a/constants.js b/constants.js index 19b62d5..461451a 100644 --- a/constants.js +++ b/constants.js @@ -13,8 +13,10 @@ exports.configurations = [{ oversampleRate: 1, sampleRate: 384000, sampleRateDivider: 48, - startCurrent: 11.0, - recordCurrent: 10.0 + recordCurrent: 9.22, + energySaverRecordCurrent: 5.92, + listenCurrent: 8.59, + energySaverListenCurrent: 5.41 }, { trueSampleRate: 16, clockDivider: 4, @@ -22,8 +24,10 @@ exports.configurations = [{ oversampleRate: 1, sampleRate: 384000, sampleRateDivider: 24, - startCurrent: 11.2, - recordCurrent: 10.9 + recordCurrent: 9.83, + energySaverRecordCurrent: 6.63, + listenCurrent: 8.72, + energySaverListenCurrent: 5.54 }, { trueSampleRate: 32, clockDivider: 4, @@ -31,8 +35,10 @@ exports.configurations = [{ oversampleRate: 1, sampleRate: 384000, sampleRateDivider: 12, - startCurrent: 11.5, - recordCurrent: 12.3 + recordCurrent: 11.3, + energySaverRecordCurrent: 8.04, + listenCurrent: 8.95, + energySaverListenCurrent: 5.78 }, { trueSampleRate: 48, clockDivider: 4, @@ -40,8 +46,10 @@ exports.configurations = [{ oversampleRate: 1, sampleRate: 384000, sampleRateDivider: 8, - startCurrent: 11.8, - recordCurrent: 14.0 + recordCurrent: 12.3, + energySaverRecordCurrent: 8.93, + listenCurrent: 9.14, + energySaverListenCurrent: 5.98 }, { trueSampleRate: 96, clockDivider: 4, @@ -49,8 +57,10 @@ exports.configurations = [{ oversampleRate: 1, sampleRate: 384000, sampleRateDivider: 4, - startCurrent: 12.7, - recordCurrent: 17.4 + recordCurrent: 15.8, + energySaverRecordCurrent: 15.8, + listenCurrent: 10.0, + energySaverListenCurrent: 10.0 }, { trueSampleRate: 192, clockDivider: 4, @@ -58,8 +68,10 @@ exports.configurations = [{ oversampleRate: 1, sampleRate: 384000, sampleRateDivider: 2, - startCurrent: 14.5, - recordCurrent: 25.6 + recordCurrent: 24.1, + energySaverRecordCurrent: 24.1, + listenCurrent: 11.5, + energySaverListenCurrent: 11.5 }, { trueSampleRate: 250, clockDivider: 4, @@ -67,8 +79,10 @@ exports.configurations = [{ oversampleRate: 1, sampleRate: 250000, sampleRateDivider: 1, - startCurrent: 15.8, - recordCurrent: 29.5 + recordCurrent: 26.4, + energySaverRecordCurrent: 26.4, + listenCurrent: 10.6, + energySaverListenCurrent: 10.6 }, { trueSampleRate: 384, clockDivider: 4, @@ -76,11 +90,14 @@ exports.configurations = [{ oversampleRate: 1, sampleRate: 384000, sampleRateDivider: 1, - startCurrent: 18.2, - recordCurrent: 41.6 + recordCurrent: 38.5, + energySaverRecordCurrent: 38.5, + listenCurrent: 12.7, + energySaverListenCurrent: 12.7 }]; /* Configuration settings to be used when a device is on firmware < 1.4.4 */ +/* Only sent to devices. Not used in energy calculations */ exports.oldConfigurations = [{ trueSampleRate: 8, @@ -88,27 +105,21 @@ exports.oldConfigurations = [{ acquisitionCycles: 16, oversampleRate: 1, sampleRate: 128000, - sampleRateDivider: 16, - startCurrent: 11.0, - recordCurrent: 10.0 + sampleRateDivider: 16 }, { trueSampleRate: 16, clockDivider: 4, acquisitionCycles: 16, oversampleRate: 1, sampleRate: 128000, - sampleRateDivider: 8, - startCurrent: 11.2, - recordCurrent: 10.9 + sampleRateDivider: 8 }, { trueSampleRate: 32, clockDivider: 4, acquisitionCycles: 16, oversampleRate: 1, sampleRate: 128000, - sampleRateDivider: 4, - startCurrent: 11.5, - recordCurrent: 12.3 + sampleRateDivider: 4 }]; /* Packet lengths for each version */ @@ -131,6 +142,22 @@ exports.packetLengthVersions = [{ }, { firmwareVersion: '1.5.0', packetLength: 59 +}, { + firmwareVersion: '1.6.0', + packetLength: 62 }]; -exports.supportedFirmwareDescs = ['AudioMoth-Firmware-Basic', 'AudioMoth-Firmware-Basic-RC1', 'AudioMoth-Firmware-Basic-RC2', 'AudioMoth-Firmware-Basic-RC3']; +/* Remove trailing digit and check if description is in list of supported firmware descriptions */ + +exports.isSupportedFirmwareDescription = (desc) => { + + const supportedFirmwareDescs = ['AudioMoth-Firmware-Basic', 'AudioMoth-Firmware-Basic-RC']; + + return supportedFirmwareDescs.includes(desc.replace(/\d+$/, '')); + +}; + +/* Version number for the latest firmware */ + +exports.latestFirmwareVersionArray = [1, 6, 0]; +exports.latestFirmwareVersionString = '1.6.0'; diff --git a/expansion/expansion.html b/expansion/expansion.html index 7d6bb67..422dc4e 100644 --- a/expansion/expansion.html +++ b/expansion/expansion.html @@ -4,117 +4,294 @@ - Expand AudioMoth Recordings + Expand AudioMoth T.WAV Recordings
-
-
- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - -
15 s30 s1 min5 min10 min1 hrNone
Maximum file length: - - - - - - - - - - - - - -
+ +
+ +
+ + +
+ + +
+ +
+
+ +
+
+ Enable length limit: +
+
+ +
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Maximum file length: + + + + + + + + + + + + + + + +
+
+
+ +
+
+ Generate silent files: +
+
+ +
+
+
+ + +
+ +
+
+ +
+
+ Enable length limit: +
+
+ +
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Maximum file length: + + + + + + + + + + + + + + + +
+
+
+
+ +
+
+ Align files to second transition: +
+
+ +
+
+ +
+
+ +
+
-
-
- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - -
FileFolder
Selection type: - - - -
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+
+ +
+
+
+
+ +
+
+
-
-
- +
+
+ +
+
+ +
+
+ +
+
+
+ Writing WAV files to source folder. +
+
-
-
- No T.WAV files selected. + +
+
+ +
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
FileFolder
Selection type: + + + +
+
+
+
+ +
+
+ +
+
+
+ No AudioMoth T.WAV files selected. +
-
-
-
+
@@ -123,6 +300,10 @@ + + \ No newline at end of file diff --git a/expansion/split.html b/expansion/split.html new file mode 100644 index 0000000..bc875cb --- /dev/null +++ b/expansion/split.html @@ -0,0 +1,181 @@ + + + + + + + Split AudioMoth Recordings + + + + +
+ +
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Maximum file length: + + + + + + + + + + + + + + + +
+
+
+
+ +
+
+ + + +
+ +
+
+ +
+
+ +
+
+
+ +
+
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ Writing WAV files to source folder. +
+
+
+ +
+
+ +
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
FileFolder
Selection type: + + + +
+
+
+
+ +
+
+ +
+
+
+ No AudioMoth WAV files selected. +
+
+
+
+
+ +
+
+ +
+
+
+ + + + + + + + \ No newline at end of file diff --git a/expansion/uiCommon.js b/expansion/uiCommon.js new file mode 100644 index 0000000..04d35eb --- /dev/null +++ b/expansion/uiCommon.js @@ -0,0 +1,237 @@ +/**************************************************************************** + * uiCommons.js + * openacousticdevices.info + * February 2021 + *****************************************************************************/ + +/* Functions which control elements common to the expansion and split windows */ + +const electron = require('electron'); +const dialog = electron.remote.dialog; + +const path = require('path'); +const fs = require('fs'); + +const nightMode = require('../nightMode.js'); + +const currentWindow = electron.remote.getCurrentWindow(); + +const fileButton = document.getElementById('file-button'); + +const prefixInput = document.getElementById('prefix-input'); +const prefixCheckbox = document.getElementById('prefix-checkbox'); +const prefixLabel = document.getElementById('prefix-label'); + +const outputLabel = document.getElementById('output-label'); + +function getSelectedRadioValue (radioName) { + + return parseInt(document.querySelector('input[name="' + radioName + '"]:checked').value); + +} + +exports.getSelectedRadioValue = getSelectedRadioValue; + +/* Pause execution */ + +exports.sleep = (milliseconds) => { + + const date = Date.now(); + let currentDate = null; + + do { + + currentDate = Date.now(); + + } while (currentDate - date < milliseconds); + +}; + +electron.ipcRenderer.on('night-mode', (e, nm) => { + + if (nm !== undefined) { + + nightMode.setNightMode(nm); + + } else { + + nightMode.toggle(); + + } + +}); + +/* Update text on selection button to reflect selection mode (file or folder selection) */ + +exports.updateButtonText = () => { + + const selectionType = getSelectedRadioValue('selection-radio'); + + if (selectionType === 0) { + + fileButton.innerText = 'Select Files'; + + } else { + + fileButton.innerText = 'Select Folder'; + + } + +}; + +/* Open dialog and set files to be expanded */ + +exports.selectRecordings = (fileRegex) => { + + let folderContents, i, filePath, fileName, recordings; + + const selectionTypes = ['openFile', 'openDirectory']; + const selectionType = getSelectedRadioValue('selection-radio'); + const properties = [selectionTypes[selectionType]]; + + /* If files are being selected, allow users to selectt more than one item. Only a single folder can be selected */ + + if (selectionType === 0) { + + properties.push('multiSelections'); + + } + + /* If files are being selected, limit selection to .wav files */ + + const filters = (selectionType === 0) ? [{name: 'wav', extensions: ['wav']}] : []; + + const selection = dialog.showOpenDialogSync(currentWindow, { + title: 'Select recording file or folder containing recordings', + nameFieldLabel: 'Recordings', + properties: properties, + filters: filters + }); + + if (selection) { + + recordings = []; + + if (selectionType === 0) { + + for (i = 0; i < selection.length; i++) { + + filePath = selection[i]; + fileName = path.basename(filePath); + + /* Check if wav files match a given regex and thus can be expanded/split */ + + if (filePath.charAt(0) !== '.' && fileRegex.test(fileName.toUpperCase())) { + + recordings.push(filePath); + + } + + } + + } else { + + folderContents = fs.readdirSync(selection[0]); + + for (i = 0; i < folderContents.length; i++) { + + filePath = folderContents[i]; + + if (filePath.charAt(0) !== '.' && fileRegex.test(filePath.toUpperCase())) { + + recordings.push(path.join(selection[0], filePath)); + + } + + } + + } + + return recordings; + + } + +}; + +/* Remove all characters which aren't A-Z, a-z, 0-9, and _ */ + +prefixInput.addEventListener('keydown', (e) => { + + if (prefixInput.disabled) { + + e.preventDefault(); + return; + + } + + var reg = /[^A-Za-z_0-9]{1}/g; + + if (reg.test(e.key)) { + + e.preventDefault(); + + } + +}); + +prefixInput.addEventListener('paste', (e) => { + + e.stopPropagation(); + e.preventDefault(); + + if (prefixInput.disabled) { + + return; + + } + + /* Read text from clipboard */ + + const clipboardData = e.clipboardData || window.clipboardData; + const pastedData = clipboardData.getData('Text'); + + /* Perform paste, but remove all unsupported characters */ + + prefixInput.value += pastedData.replace(/[^A-Za-z_0-9]{1}/g, ''); + + /* Limit max number of characters */ + + prefixInput.value = prefixInput.value.substring(0, prefixInput.maxLength); + +}); + +/* Add listener to handle enabling/disabling prefix UI */ + +prefixCheckbox.addEventListener('change', () => { + + if (prefixCheckbox.checked) { + + prefixLabel.style.color = ''; + prefixInput.style.color = ''; + prefixInput.disabled = false; + + } else { + + prefixLabel.style.color = 'lightgray'; + prefixInput.style.color = 'lightgray'; + prefixInput.disabled = true; + + } + +}); + +/* Update label to notify user if a custom output directtory is being used */ + +exports.updateOutputLabel = (outputDir) => { + + if (outputDir === '') { + + outputLabel.textContent = 'Writing WAV files to source folder.'; + + } else { + + outputLabel.textContent = 'Writing WAV files to custom folder.'; + + } + +}; diff --git a/expansion/uiExpansion.js b/expansion/uiExpansion.js index e1d246f..5d09d87 100644 --- a/expansion/uiExpansion.js +++ b/expansion/uiExpansion.js @@ -11,37 +11,54 @@ const electron = require('electron'); const dialog = electron.remote.dialog; +/* Get functions which control elements common to the expansion and split windows */ +const ui = require('./uiCommon.js'); + const path = require('path'); const fs = require('fs'); -const nightMode = require('../nightMode.js'); -const expander = require('./expander.js'); +const audiomothUtils = require('audiomoth-utils'); -const MAX_LENGTHS = [15, 30, 60, 300, 600, 3600, null]; +const MAX_LENGTHS = [5, 10, 15, 30, 60, 300, 600, 3600]; +const MAX_LENGTH_STRINGS = ['5 seconds', '10 seconds', '15 seconds', '30 seconds', '1 minute', '5 minutes', '10 minutes', '1 hour']; -var currentWindow = electron.remote.getCurrentWindow(); +const FILE_REGEX = /^\d\d\d\d\d\d\d\d_\d\d\d\d\d\dT.WAV$/; -var selectionRadios = document.getElementsByName('selection-radio'); -var fileLabel = document.getElementById('file-label'); -var fileButton = document.getElementById('file-button'); -var expandButton = document.getElementById('expand-button'); +const durationTabButton = document.getElementById('duration-tab-link'); +const eventTabButton = document.getElementById('event-tab-link'); -var files = []; -var expanding = false; +const silentFilesCheckbox = document.getElementById('silent-files-checkbox'); +const alignmentCheckbox = document.getElementById('alignment-checkbox'); -electron.ipcRenderer.on('night-mode', function (e, nm) { +const durationMaxLengthCheckbox = document.getElementById('duration-max-length-checkbox'); +const eventMaxLengthCheckbox = document.getElementById('event-max-length-checkbox'); - if (nm !== undefined) { +const selectionRadios = document.getElementsByName('selection-radio'); - nightMode.setNightMode(nm); +const eventMaxLengthRadios = document.getElementsByName('event-max-length-radio'); +const durationMaxLengthRadios = document.getElementsByName('duration-max-length-radio'); - } else { +const overviewPanel = document.getElementById('overview-panel'); - nightMode.toggle(); +const prefixCheckbox = document.getElementById('prefix-checkbox'); +const prefixInput = document.getElementById('prefix-input'); - } +const outputCheckbox = document.getElementById('output-checkbox'); +const outputButton = document.getElementById('output-button'); +const outputLabel = document.getElementById('output-label'); -}); +var outputDir = ''; + +const fileLabel = document.getElementById('file-label'); +const fileButton = document.getElementById('file-button'); +const expandButton = document.getElementById('expand-button'); + +var files = []; +var expanding = false; + +var expansionType = 'DURATION'; + +/* Disable UI elements in main window while progress bar is open and expansion is in progress */ function disableUI () { @@ -50,6 +67,28 @@ function disableUI () { selectionRadios[0].disabled = true; selectionRadios[1].disabled = true; + durationTabButton.disabled = true; + eventTabButton.disabled = true; + + durationMaxLengthCheckbox.disabled = true; + eventMaxLengthCheckbox.disabled = true; + + for (let i = 0; i < eventMaxLengthRadios.length; i++) { + + eventMaxLengthRadios[i].disabled = true; + durationMaxLengthRadios[i].disabled = true; + + } + + silentFilesCheckbox.disabled = true; + alignmentCheckbox.disabled = true; + + outputCheckbox.disabled = true; + outputButton.disabled = true; + + prefixCheckbox.disabled = true; + prefixInput.disabled = true; + } function enableUI () { @@ -58,26 +97,69 @@ function enableUI () { expandButton.disabled = false; selectionRadios[0].disabled = false; selectionRadios[1].disabled = false; + + durationTabButton.disabled = false; + eventTabButton.disabled = false; + + durationMaxLengthCheckbox.disabled = false; + eventMaxLengthCheckbox.disabled = false; + + for (let i = 0; i < eventMaxLengthRadios.length; i++) { + + if (durationMaxLengthCheckbox.checked) { + + durationMaxLengthRadios[i].disabled = false; + + } + + if (eventMaxLengthCheckbox.checked) { + + eventMaxLengthRadios[i].disabled = false; + + } + + } + + silentFilesCheckbox.disabled = !durationMaxLengthCheckbox.checked; + alignmentCheckbox.disabled = false; + + outputCheckbox.disabled = false; + outputButton.disabled = false; + + prefixCheckbox.disabled = false; + + if (prefixCheckbox.checked) { + + prefixInput.disabled = false; + + } + expanding = false; } -function sleep (milliseconds) { +/* Enable/disable a given set of max length radio buttons based on a checkbox */ - var date = Date.now(); - var currentDate = null; +function updateFileMaxLengthUI (elementClass, checkbox) { - do { + const maxLengthElements = document.getElementsByClassName(elementClass); - currentDate = Date.now(); + const maxLengthEnabled = checkbox.checked; - } while (currentDate - date < milliseconds); + for (let i = 0; i < maxLengthElements.length; i++) { -} + maxLengthElements[i].disabled = !maxLengthEnabled; + maxLengthElements[i].style.color = maxLengthEnabled ? '' : 'lightgrey'; + + } + +}; + +/* Expand selected files */ function expandFiles () { - var i, successCount, errorCount, errors, errorFiles, cancelled, response, filePath, fileContent, j, maxLength; + let successCount, errorCount, cancelled, response, filePath, fileContent, maxLength, outputPath, prefix, errorFileLocation; if (!files) { @@ -85,14 +167,21 @@ function expandFiles () { } + const generateSilentFiles = expansionType === 'DURATION' ? silentFilesCheckbox.checked : false; + const alignToSecondTransitions = expansionType === 'EVENT' ? alignmentCheckbox.checked : false; + + const maxLengthRadioName = expansionType === 'DURATION' ? 'duration-max-length-radio' : 'event-max-length-radio'; + successCount = 0; errorCount = 0; - errors = []; - errorFiles = []; + const errors = []; + const errorFiles = []; + + for (let i = 0; i < files.length; i++) { - for (i = 0; i < files.length; i++) { + /* If progress bar is closed, the expansion task is considered cancelled. This will contact the main thread and ask if that has happened */ - cancelled = electron.ipcRenderer.sendSync('poll-cancelled'); + cancelled = electron.ipcRenderer.sendSync('poll-expansion-cancelled'); if (cancelled) { @@ -102,14 +191,46 @@ function expandFiles () { } - console.log('Expanding: ' + files[i]); - electron.ipcRenderer.send('set-bar-progress', i, 0, path.basename(files[i])); + /* Let the main thread know what value to set the progress bar to */ + + electron.ipcRenderer.send('set-expansion-bar-progress', i, 0, path.basename(files[i])); + + /* If max length is enabled for the current expansion mode (there is separate UI for each and only the relevant one is shown) */ + + if ((durationMaxLengthCheckbox.checked && expansionType === 'DURATION') || (eventMaxLengthCheckbox.checked && expansionType === 'EVENT')) { + + maxLength = MAX_LENGTHS[ui.getSelectedRadioValue(maxLengthRadioName)]; + + } else { + + maxLength = null; + + } + + console.log('Expanding:', files[i]); + console.log('Expansion type:', expansionType); + console.log('Maximum file length:', maxLength); + + if (expansionType === 'DURATION') { + + console.log('Generate silent files:', generateSilentFiles); + + } else { + + console.log('Align files to second transitions:', alignToSecondTransitions); + + } + + console.log('-'); - maxLength = MAX_LENGTHS[getSelectedRadioValue('max-length-radio')]; + /* Check if the optional prefix/output directory setttings are being used. If left as null, expander will put expanded file(s) in the same directory as the input with no prefix */ - response = expander.expand(files[i], maxLength, function (progress) { + outputPath = outputCheckbox.checked ? outputDir : null; + prefix = (prefixCheckbox.checked && prefixInput.value !== '') ? prefixInput.value : null; - electron.ipcRenderer.send('set-bar-progress', i, progress, path.basename(files[i])); + response = audiomothUtils.expand(files[i], outputPath, prefix, expansionType, maxLength, generateSilentFiles, alignToSecondTransitions, (progress) => { + + electron.ipcRenderer.send('set-expansion-bar-progress', i, progress, path.basename(files[i])); }); @@ -119,25 +240,31 @@ function expandFiles () { } else { + /* Keep track of the errors to write to the log at the end */ + errorCount++; errors.push(response.error); errorFiles.push(files[i]); - electron.ipcRenderer.send('set-bar-error', path.basename(files[i])); + electron.ipcRenderer.send('set-expansion-bar-error', path.basename(files[i])); - sleep(3000); + ui.sleep(3000); } } + /* Build error file */ + if (errorCount > 0) { - filePath = path.join(path.dirname(errorFiles[0]), 'ERRORS.TXT'); + errorFileLocation = outputCheckbox.checked ? outputDir : path.dirname(errorFiles[0]); + + filePath = path.join(errorFileLocation, 'ERRORS.TXT'); fileContent = ''; - for (j = 0; j < errorCount; j++) { + for (let j = 0; j < errorCount; j++) { fileContent += path.basename(errorFiles[j]) + ' - ' + errors[j] + '\n'; @@ -150,7 +277,7 @@ function expandFiles () { } catch (err) { console.error(err); - electron.ipcRenderer.send('set-bar-completed', successCount, errorCount, true); + electron.ipcRenderer.send('set-expansion-bar-completed', successCount, errorCount, true); return; } @@ -159,23 +286,29 @@ function expandFiles () { } - electron.ipcRenderer.send('set-bar-completed', successCount, errorCount, false); + /* Notify main thread that expansion is complete so progress bar is closed */ + + electron.ipcRenderer.send('set-expansion-bar-completed', successCount, errorCount, false); } -electron.ipcRenderer.on('summary-closed', enableUI); +/* When the progress bar is complete and the summary window at the end has been displayed for a fixed amount of ttime, it will close and this re-enables the UI */ + +electron.ipcRenderer.on('expansion-summary-closed', enableUI); + +/* Update label to reflect new file/folder selection */ -function updateFirmwareDirectoryDisplay (directoryArray) { +function updateInputDirectoryDisplay (directoryArray) { - if (directoryArray.length === 0) { + if (directoryArray.length === 0 || !directoryArray) { - fileLabel.innerHTML = 'No T.WAV files found.'; + fileLabel.innerHTML = 'No AudioMoth T.WAV files selected.'; expandButton.disabled = true; } else { fileLabel.innerHTML = 'Found '; - fileLabel.innerHTML += directoryArray.length + ' T.WAV file'; + fileLabel.innerHTML += directoryArray.length + ' AudioMoth T.WAV file'; fileLabel.innerHTML += (directoryArray.length === 1 ? '' : 's'); fileLabel.innerHTML += '.'; expandButton.disabled = false; @@ -184,117 +317,211 @@ function updateFirmwareDirectoryDisplay (directoryArray) { } -function getSelectedRadioValue (radioName) { +/* Reset UI back to default state, clearing the selected files */ + +function resetUI () { + + files = []; + + fileLabel.innerHTML = 'No AudioMoth T.WAV files selected.'; + + expandButton.disabled = true; - return parseInt(document.querySelector('input[name="' + radioName + '"]:checked').value); + ui.updateButtonText(); + updateOverviewPanel(); } -function updateButtonText () { +/* Update text of overview panel to explain the result of using the current settings */ + +function updateOverviewPanel () { + + let fileLength; + + if (expansionType === 'DURATION') { + + if (!durationMaxLengthCheckbox.checked) { + + // 1 + overviewPanel.innerHTML = 'Expand each AudioMoth T.WAV file into a single WAV file.
Expansion restores silent periods between trigger events.'; + + } else { + + fileLength = MAX_LENGTH_STRINGS[ui.getSelectedRadioValue('duration-max-length-radio')]; - var selectionType = getSelectedRadioValue('selection-radio'); + if (!silentFilesCheckbox.checked) { - if (selectionType === 0) { + // 2 + overviewPanel.innerHTML = 'Expand each AudioMoth T.WAV file into multiple WAV files.
Maximum file length is ' + fileLength + ' and silent files will not be generated.'; - fileButton.innerText = 'Select Files'; + } else { + + // 3 + overviewPanel.innerHTML = 'Expand each AudioMoth T.WAV file into multiple WAV files.
Maximum file length is ' + fileLength + '.'; + + } + + } } else { - fileButton.innerText = 'Select Folder'; + if (!eventMaxLengthCheckbox.checked) { + + if (!alignmentCheckbox.checked) { + + // 1 + overviewPanel.innerHTML = 'Split each AudioMoth T.WAV file into separate events.
Each event generates a separate WAV file aligned to the millisecond. '; + + } else { + + // 2 + overviewPanel.innerHTML = 'Split each AudioMoth T.WAV file into separate events.
Each WAV file is aligned to the second and may contain multiple events.'; + + } + + } else { + + fileLength = MAX_LENGTH_STRINGS[ui.getSelectedRadioValue('event-max-length-radio')]; + + if (!alignmentCheckbox.checked) { + + // 3 + overviewPanel.innerHTML = 'Split each AudioMoth T.WAV file into separate events. Each event generates a separate WAV file, with maximum length of ' + fileLength + ', aligned to the millisecond.'; + + } else { + + // 4 + overviewPanel.innerHTML = 'Split each AudioMoth T.WAV file into separate events. Each WAV file is aligned to the second, has a maximum length of ' + fileLength + ', and may contain multiple events.'; + + } + + } } } -function selectRecordings () { +/* Add listeners which update the overview panel whenever the max length radio buttons are used */ - var selectionTypes, properties, filters, selectionType, selection, folderContents, i, fileName, recordings; +function addMaxLengthRadioButtonListeners () { - selectionTypes = ['openFile', 'openDirectory']; + for (let i = 0; i < eventMaxLengthRadios.length; i++) { - selectionType = getSelectedRadioValue('selection-radio'); + eventMaxLengthRadios[i].addEventListener('click', updateOverviewPanel); - properties = [selectionTypes[selectionType]]; + } - if (selectionType === 0) { + for (let j = 0; j < durationMaxLengthRadios.length; j++) { - properties.push('multiSelections'); + durationMaxLengthRadios[j].addEventListener('click', updateOverviewPanel); } - filters = (selectionType === 0) ? [{name: 'wav', extensions: ['wav']}] : []; +} - selection = dialog.showOpenDialogSync(currentWindow, { - title: 'Select recording file or folder containing recordings', - nameFieldLabel: 'Recordings', - properties: properties, - filters: filters - }); +/* Swittch between the two expansion modes (Duration and Event-based) */ + +durationTabButton.addEventListener('click', () => { + + expansionType = 'DURATION'; + updateOverviewPanel(); - if (selection) { +}); - recordings = []; +eventTabButton.addEventListener('click', () => { - if (selectionType === 0) { + expansionType = 'EVENT'; + updateOverviewPanel(); - for (i = 0; i < selection.length; i++) { +}); - fileName = selection[i]; +/* Enable/disable max length radio buttons */ - if (fileName.charAt(0) !== '.' && fileName.toLowerCase().includes('t.wav')) { +durationMaxLengthCheckbox.addEventListener('click', () => { - recordings.push(fileName); + updateFileMaxLengthUI('duration-max-length-ui', durationMaxLengthCheckbox); + updateOverviewPanel(); - } +}); - } +eventMaxLengthCheckbox.addEventListener('click', () => { - } else { + updateFileMaxLengthUI('event-max-length-ui', eventMaxLengthCheckbox); + updateOverviewPanel(); - folderContents = fs.readdirSync(selection[0]); +}); - for (i = 0; i < folderContents.length; i++) { +/* Add listeners which update overview panel */ - fileName = folderContents[i]; +alignmentCheckbox.addEventListener('click', updateOverviewPanel); +silentFilesCheckbox.addEventListener('click', updateOverviewPanel); - if (fileName.charAt(0) !== '.' && fileName.toLowerCase().includes('t.wav')) { +addMaxLengthRadioButtonListeners(); - recordings.push(path.join(selection[0], fileName)); +/* Initialise the overview panel */ - } +updateOverviewPanel(); - } +/* Initialise max length radio butttons for each block of UI elements */ - } +updateFileMaxLengthUI('duration-max-length-ui', durationMaxLengthCheckbox); +updateFileMaxLengthUI('event-max-length-ui', eventMaxLengthCheckbox); + +/* Add listener which handles enabling/disabling custom output directory UI */ - files = recordings; +outputCheckbox.addEventListener('change', () => { - updateFirmwareDirectoryDisplay(files); + if (outputCheckbox.checked) { - updateButtonText(); + outputLabel.style.color = ''; + outputButton.disabled = false; + + } else { + + outputLabel.style.color = 'lightgray'; + outputButton.disabled = true; + outputDir = ''; + ui.updateOutputLabel(outputDir); } -} +}); -function resetUI () { +/* Select a custom output directory. If Cancel is pressed, assume no custom direcotry is wantted */ - files = []; +outputButton.addEventListener('click', () => { - fileLabel.innerHTML = 'No T.WAV files selected.'; + const destinationName = dialog.showOpenDialogSync({ + title: 'Select Destination', + nameFieldLabel: 'Destination', + multiSelections: false, + properties: ['openDirectory'] + }); - expandButton.disabled = true; + outputDir = (destinationName !== undefined) ? destinationName[0] : ''; - updateButtonText(); + ui.updateOutputLabel(outputDir); -} +}); + +/* Whenever tthe file/folder radio button changes, reset the UI */ selectionRadios[0].addEventListener('change', resetUI); selectionRadios[1].addEventListener('change', resetUI); -fileButton.addEventListener('click', selectRecordings); +/* Select/process file(s) buttons */ + +fileButton.addEventListener('click', () => { + + files = ui.selectRecordings(FILE_REGEX); + + updateInputDirectoryDisplay(files); + + ui.updateButtonText(); + +}); -expandButton.addEventListener('click', function () { +expandButton.addEventListener('click', () => { if (expanding) { @@ -305,7 +532,7 @@ expandButton.addEventListener('click', function () { expanding = true; disableUI(); - electron.ipcRenderer.send('start-bar', files.length); + electron.ipcRenderer.send('start-expansion-bar', files.length); setTimeout(expandFiles, 2000); }); diff --git a/expansion/uiSplit.js b/expansion/uiSplit.js new file mode 100644 index 0000000..ba54b13 --- /dev/null +++ b/expansion/uiSplit.js @@ -0,0 +1,329 @@ +/**************************************************************************** + * uiSplit.js + * openacousticdevices.info + * February 2021 + *****************************************************************************/ + +'use strict'; + +/* global document */ + +const electron = require('electron'); +const dialog = electron.remote.dialog; + +/* Get functions which control elements common to the expansion and split windows */ +const ui = require('./uiCommon.js'); + +const path = require('path'); +const fs = require('fs'); + +const audiomothUtils = require('audiomoth-utils'); + +var currentWindow = electron.remote.getCurrentWindow(); + +const MAX_LENGTHS = [5, 10, 15, 30, 60, 300, 600, 3600]; + +const FILE_REGEX = /^\d\d\d\d\d\d\d\d_\d\d\d\d\d\d.WAV$/; + +const maxLengthRadios = document.getElementsByName('max-length-radio'); + +const selectionRadios = document.getElementsByName('selection-radio'); + +const prefixCheckbox = document.getElementById('prefix-checkbox'); +const prefixInput = document.getElementById('prefix-input'); + +const outputCheckbox = document.getElementById('output-checkbox'); +const outputButton = document.getElementById('output-button'); +const outputLabel = document.getElementById('output-label'); + +var outputDir = ''; + +const fileLabel = document.getElementById('file-label'); +const fileButton = document.getElementById('file-button'); +const splitButton = document.getElementById('split-button'); + +var files = []; +var splitting = false; + +/* Disable UI elements in main window while progress bar is open and split is in progress */ + +function disableUI () { + + fileButton.disabled = true; + splitButton.disabled = true; + selectionRadios[0].disabled = true; + selectionRadios[1].disabled = true; + + for (let i = 0; i < maxLengthRadios.length; i++) { + + maxLengthRadios[i].disabled = true; + + } + + outputCheckbox.disabled = true; + outputButton.disabled = true; + + prefixCheckbox.disabled = true; + prefixInput.disabled = true; + +} + +function enableUI () { + + fileButton.disabled = false; + splitButton.disabled = false; + selectionRadios[0].disabled = false; + selectionRadios[1].disabled = false; + + for (let i = 0; i < maxLengthRadios.length; i++) { + + maxLengthRadios[i].disabled = false; + + } + + outputCheckbox.disabled = false; + outputButton.disabled = false; + + prefixCheckbox.disabled = false; + + if (prefixCheckbox.checked) { + + prefixInput.disabled = false; + + } + + splitting = false; + +} + +/* Split selected files */ + +function splitFiles () { + + let successCount, errorCount, cancelled, response, filePath, fileContent, maxLength, outputPath, prefix, errorFileLocation; + + if (!files) { + + return; + + } + + successCount = 0; + errorCount = 0; + const errors = []; + const errorFiles = []; + + for (let i = 0; i < files.length; i++) { + + /* If progress bar is closed, the split task is considered cancelled. This will contact the main thread and ask if that has happened */ + + cancelled = electron.ipcRenderer.sendSync('poll-split-cancelled'); + + if (cancelled) { + + console.log('Split cancelled.'); + enableUI(); + return; + + } + + /* Let the main thread know what value to set the progress bar to */ + + electron.ipcRenderer.send('set-split-bar-progress', i, 0, path.basename(files[i])); + + maxLength = MAX_LENGTHS[ui.getSelectedRadioValue('max-length-radio')]; + + console.log('Splitting:', files[i]); + console.log('Maximum file length:', maxLength); + + console.log('-'); + + /* Check if the optional prefix/output directory setttings are being used. If left as null, splitter will put file(s) in the same directory as the input with no prefix */ + + outputPath = outputCheckbox.checked ? outputDir : null; + prefix = (prefixCheckbox.checked && prefixInput.value !== '') ? prefixInput.value : null; + + response = audiomothUtils.split(files[i], outputPath, prefix, maxLength, (progress) => { + + electron.ipcRenderer.send('set-split-bar-progress', i, progress, path.basename(files[i])); + + }); + + if (response.success) { + + successCount++; + + } else { + + /* Keep track of the errors to write to the log at the end */ + + errorCount++; + errors.push(response.error); + errorFiles.push(files[i]); + + electron.ipcRenderer.send('set-split-bar-error', path.basename(files[i])); + + ui.sleep(3000); + + } + + } + + /* Build error file */ + + if (errorCount > 0) { + + errorFileLocation = outputCheckbox.checked ? outputDir : path.dirname(errorFiles[0]); + + filePath = path.join(errorFileLocation, 'ERRORS.TXT'); + + fileContent = ''; + + for (let j = 0; j < errorCount; j++) { + + fileContent += path.basename(errorFiles[j]) + ' - ' + errors[j] + '\n'; + + } + + try { + + fs.writeFileSync(filePath, fileContent); + + } catch (err) { + + console.error(err); + electron.ipcRenderer.send('set-split-bar-completed', successCount, errorCount, true); + return; + + } + + console.log('Error summary written to ' + filePath); + + } + + /* Notify main thread that split is complete so progress bar is closed */ + + electron.ipcRenderer.send('set-split-bar-completed', successCount, errorCount, false); + +} + +/* When the progress bar is complete and the summary window at the end has been displayed for a fixed amount of ttime, it will close and this re-enables the UI */ + +electron.ipcRenderer.on('split-summary-closed', enableUI); + +/* Update label to reflect new file/folder selection */ + +function updateInputDirectoryDisplay (directoryArray) { + + if (directoryArray.length === 0 || !directoryArray) { + + fileLabel.innerHTML = 'No AudioMoth WAV files selected.'; + splitButton.disabled = true; + + } else { + + fileLabel.innerHTML = 'Found '; + fileLabel.innerHTML += directoryArray.length + ' AudioMoth WAV file'; + fileLabel.innerHTML += (directoryArray.length === 1 ? '' : 's'); + fileLabel.innerHTML += '.'; + splitButton.disabled = false; + + } + +} + +/* Reset UI back to default state, clearing the selected files */ + +function resetUI () { + + files = []; + + fileLabel.innerHTML = 'No AudioMoth WAV files selected.'; + + splitButton.disabled = true; + + ui.updateButtonText(); + +} + +/* Add listener which handles enabling/disabling custom output directory UI */ + +outputCheckbox.addEventListener('change', () => { + + if (outputCheckbox.checked) { + + outputLabel.style.color = ''; + outputButton.disabled = false; + + } else { + + outputLabel.style.color = 'lightgray'; + outputButton.disabled = true; + outputDir = ''; + ui.updateOutputLabel(outputDir); + + } + +}); + +/* Select a custom output directory. If Cancel is pressed, assume no custom direcotry is wantted */ + +outputButton.addEventListener('click', () => { + + const destinationName = dialog.showOpenDialogSync({ + title: 'Select Destination', + nameFieldLabel: 'Destination', + multiSelections: false, + properties: ['openDirectory'] + }); + + outputDir = (destinationName !== undefined) ? destinationName[0] : ''; + + ui.updateOutputLabel(outputDir); + +}); + +/* Whenever tthe file/folder radio button changes, reset the UI */ + +selectionRadios[0].addEventListener('change', resetUI); +selectionRadios[1].addEventListener('change', resetUI); + +/* Select/process file(s) buttons */ + +fileButton.addEventListener('click', () => { + + files = ui.selectRecordings(FILE_REGEX); + + updateInputDirectoryDisplay(files); + + ui.updateButtonText(); + +}); + +splitButton.addEventListener('click', () => { + + if (splitting) { + + return; + + } + + if ((!prefixCheckbox.checked || prefixInput.value === '') && (!outputCheckbox.checked || outputDir === '')) { + + dialog.showMessageBox(currentWindow, { + type: 'error', + title: 'Cannot split with current settings', + message: 'Without a prefix or custom destination, splitting will overwrite the original file. Set one of these values to continue.' + }); + + return; + + } + + splitting = true; + disableUI(); + + electron.ipcRenderer.send('start-split-bar', files.length); + setTimeout(splitFiles, 2000); + +}); diff --git a/expansion/wavHeader.js b/expansion/wavHeader.js index fb06ca6..c3f08a4 100644 --- a/expansion/wavHeader.js +++ b/expansion/wavHeader.js @@ -11,14 +11,20 @@ const UINT16_LENGTH = 2; const UINT32_LENGTH = 4; const RIFF_ID_LENGTH = 4; -const LENGTH_OF_ARTIST = 32; -const LENGTH_OF_COMMENT = 384; -const LENGTH_OF_WAV_HEADER = 488; -/* WAV header component read functions */ +/* WAV format constants */ + +const PCM_FORMAT = 1; +const NUMBER_OF_CHANNELS = 1; +const NUMBER_OF_BITS_IN_SAMPLE = 16; +const NUMBER_OF_BYTES_IN_SAMPLE = 2; + +/* WAV header base component read functions */ function readString (state, length) { + if (state.buffer.length - state.index < length) throw new Error('WAVE header exceeded buffer length.'); + var result = state.buffer.toString('utf8', state.index, state.index + length).replace(/\0/g, ''); state.index += length; return result; @@ -27,6 +33,8 @@ function readString (state, length) { function readUInt32LE (state) { + if (state.buffer.length - state.index < UINT32_LENGTH) throw new Error('WAVE header exceeded buffer length.'); + var result = state.buffer.readUInt32LE(state.index); state.index += UINT32_LENGTH; return result; @@ -35,17 +43,36 @@ function readUInt32LE (state) { function readUInt16LE (state) { + if (state.buffer.length - state.index < UINT16_LENGTH) throw new Error('WAVE header exceeded buffer length.'); + var result = state.buffer.readUInt16LE(state.index); state.index += UINT16_LENGTH; return result; } -function readChunk (state) { +/* WAV header high-level component read functions */ + +function readID (state, id) { + + const result = readString(state, id.length); + + if (result !== id) throw new Error('Could not find ' + id + ' ID.'); + + return result; + +} + +function readChunk (state, id) { + + const result = {}; - var result = {}; result.id = readString(state, RIFF_ID_LENGTH); + + if (result.id !== id) throw new Error('Could not find ' + id.replace(' ', '') + ' chunk ID.'); + result.size = readUInt32LE(state); + return result; } @@ -54,7 +81,7 @@ function readChunk (state) { function writeString (state, string, length, zeroTerminated) { - var maximumWriteLength = zeroTerminated ? Math.min(string.length, length - 1) : Math.min(string.length, length); + const maximumWriteLength = zeroTerminated ? Math.min(string.length, length - 1) : Math.min(string.length, length); state.buffer.fill(0, state.index, state.index + length); state.buffer.write(string, state.index, maximumWriteLength, 'utf8'); state.index += length; @@ -84,148 +111,151 @@ function writeChunk (state, chunk) { /* WAV header read and write functions */ -function readHeader (buffer) { +function readHeader (buffer, fileSize) { - var state, header; + const header = {}; - header = {}; + const state = {buffer: buffer, index: 0}; - state = {buffer: buffer, index: 0}; + try { - header.riff = readChunk(state); + /* Read RIFF chunk */ - header.format = readString(state, RIFF_ID_LENGTH); + header.riff = readChunk(state, 'RIFF'); - header.fmt = readChunk(state); + if (header.riff.size + RIFF_ID_LENGTH + UINT32_LENGTH !== fileSize) { - header.wavFormat = {}; - header.wavFormat.format = readUInt16LE(state); - header.wavFormat.numberOfChannels = readUInt16LE(state); - header.wavFormat.samplesPerSecond = readUInt32LE(state); - header.wavFormat.bytesPerSecond = readUInt32LE(state); - header.wavFormat.bytesPerCapture = readUInt16LE(state); - header.wavFormat.bitsPerSample = readUInt16LE(state); + return { + success: false, + error: 'RIFF chunk size does not match file size.' + }; - header.list = readChunk(state); + } - header.info = readString(state, RIFF_ID_LENGTH); + /* Read WAVE ID */ - header.icmt = readChunk(state); - header.icmt.comment = readString(state, LENGTH_OF_COMMENT); + header.format = readID(state, 'WAVE'); - header.iart = readChunk(state); - header.iart.artist = readString(state, LENGTH_OF_ARTIST); + /* Read FMT chunk */ - header.data = readChunk(state); + header.fmt = readChunk(state, 'fmt '); - return header; + header.wavFormat = {}; + header.wavFormat.format = readUInt16LE(state); + header.wavFormat.numberOfChannels = readUInt16LE(state); + header.wavFormat.samplesPerSecond = readUInt32LE(state); + header.wavFormat.bytesPerSecond = readUInt32LE(state); + header.wavFormat.bytesPerCapture = readUInt16LE(state); + header.wavFormat.bitsPerSample = readUInt16LE(state); -} + if (header.wavFormat.format !== PCM_FORMAT || header.wavFormat.numberOfChannels !== NUMBER_OF_CHANNELS || header.wavFormat.bytesPerSecond !== NUMBER_OF_BYTES_IN_SAMPLE * header.wavFormat.samplesPerSecond || header.wavFormat.bytesPerCapture !== NUMBER_OF_BYTES_IN_SAMPLE || header.wavFormat.bitsPerSample !== NUMBER_OF_BITS_IN_SAMPLE) { -function writeHeader (buffer, header) { + return { + success: false, + error: 'Unexpected WAVE format.' + }; - var state = {buffer: buffer, index: 0}; + } - writeChunk(state, header.riff); + /* Read LIST chunk */ - writeString(state, header.format, RIFF_ID_LENGTH, false); + header.list = readChunk(state, 'LIST'); - writeChunk(state, header.fmt); + /* Read INFO ID */ - writeUInt16LE(state, header.wavFormat.format); - writeUInt16LE(state, header.wavFormat.numberOfChannels); - writeUInt32LE(state, header.wavFormat.samplesPerSecond); - writeUInt32LE(state, header.wavFormat.bytesPerSecond); - writeUInt16LE(state, header.wavFormat.bytesPerCapture); - writeUInt16LE(state, header.wavFormat.bitsPerSample); + header.info = readID(state, 'INFO'); - writeChunk(state, header.list); + /* Read ICMT chunk */ - writeString(state, header.info, RIFF_ID_LENGTH, false); + header.icmt = readChunk(state, 'ICMT'); - writeChunk(state, header.icmt); - writeString(state, header.icmt.comment, LENGTH_OF_COMMENT, true); + header.icmt.comment = readString(state, header.icmt.size); - writeChunk(state, header.iart); - writeString(state, header.iart.artist, LENGTH_OF_ARTIST, true); + /* Read IART chunk */ - writeChunk(state, header.data); + header.iart = readChunk(state, 'IART'); - return buffer; + header.iart.artist = readString(state, header.iart.size); -} + /* Check LIST chunk size */ -/* Function to check header */ + if (header.list.size !== 3 * RIFF_ID_LENGTH + 2 * UINT32_LENGTH + header.iart.size + header.icmt.size) { -function checkHeader (header, fileSize) { + return { + success: false, + error: 'LIST chunk size does not match total size of INFO, ICMT and IART chunks.' + }; - if (header.riff.id !== 'RIFF') { + } - return { - success: false, - error: 'Could not find RIFF chunk in the input file.' - }; + /* Read DATA chunk */ - } + header.data = readChunk(state, 'data'); - if (header.riff.size + RIFF_ID_LENGTH + UINT32_LENGTH !== fileSize) { + /* Set the header size and check DATA chunk size */ - return { - success: false, - error: 'RIFF chunk file size is incorrect.' - }; + header.size = state.index; - } + if (header.data.size + header.size !== fileSize) { + + return { + success: false, + error: 'DATA chunk size does not match file size.' + }; + + } - if (header.format !== 'WAVE') { + /* Success */ return { - success: false, - error: 'Could not find WAVE format indicator in the input file.' + header: header, + success: true, + error: null }; - } + } catch (e) { - if (header.fmt.id !== 'fmt ') { + /* Header has exceed file buffer length */ return { success: false, - error: 'Could not find fmt segment in the input file.' + error: e.message }; } - if (header.icmt.id !== 'ICMT') { +} - return { - success: false, - error: 'Could not find comment segment in the input file.' - }; +function writeHeader (buffer, header) { - } + const state = {buffer: buffer, index: 0}; - if (header.data.id !== 'data') { + writeChunk(state, header.riff); - return { - success: false, - error: 'Could not find data segment in the input file.' - }; + writeString(state, header.format, RIFF_ID_LENGTH, false); - } + writeChunk(state, header.fmt); - if (header.data.size + LENGTH_OF_WAV_HEADER !== fileSize) { + writeUInt16LE(state, header.wavFormat.format); + writeUInt16LE(state, header.wavFormat.numberOfChannels); + writeUInt32LE(state, header.wavFormat.samplesPerSecond); + writeUInt32LE(state, header.wavFormat.bytesPerSecond); + writeUInt16LE(state, header.wavFormat.bytesPerCapture); + writeUInt16LE(state, header.wavFormat.bitsPerSample); - return { - success: false, - error: 'DATA chunk file size is incorrect.' - }; + writeChunk(state, header.list); - } + writeString(state, header.info, RIFF_ID_LENGTH, false); + + writeChunk(state, header.icmt); + writeString(state, header.icmt.comment, header.icmt.size, true); + + writeChunk(state, header.iart); + writeString(state, header.iart.artist, header.iart.size, true); + + writeChunk(state, header.data); - return { - success: true, - error: null - }; + return buffer; } @@ -233,15 +263,21 @@ function checkHeader (header, fileSize) { function updateDataSize (header, size) { - header.riff.size = LENGTH_OF_WAV_HEADER + size - UINT32_LENGTH - RIFF_ID_LENGTH; + header.riff.size = header.size + size - UINT32_LENGTH - RIFF_ID_LENGTH; header.data.size = size; } +function updateComment (header, comment) { + + header.icmt.comment = comment; + +} + function overwriteComment (header, comment) { - var length = Math.min(comment.length, LENGTH_OF_COMMENT - 1); + var length = Math.min(comment.length, header.icmt.size - 1); header.icmt.comment = comment.substr(0, length) + header.icmt.comment.substr(length); @@ -249,9 +285,8 @@ function overwriteComment (header, comment) { /* Exports */ -exports.LENGTH_OF_WAV_HEADER = LENGTH_OF_WAV_HEADER; exports.writeHeader = writeHeader; exports.readHeader = readHeader; -exports.checkHeader = checkHeader; exports.updateDataSize = updateDataSize; +exports.updateComment = updateComment; exports.overwriteComment = overwriteComment; diff --git a/index.html b/index.html index 82eb0c7..5d84a9d 100644 --- a/index.html +++ b/index.html @@ -7,7 +7,7 @@ - +
@@ -65,7 +65,7 @@
@@ -89,6 +93,10 @@
+
+
+
+
@@ -124,6 +132,7 @@ window.jQuery = window.$ = require('jquery'); $("#schedule-content").load('./schedule/schedule.html'); $("#settings-content").load('./settings/settings.html'); + $("#filtering-content").load('./settings/filtering.html'); $("#advanced-content").load('./settings/advanced.html'); diff --git a/lifeDisplay.js b/lifeDisplay.js index 0c33c18..806e4f5 100644 --- a/lifeDisplay.js +++ b/lifeDisplay.js @@ -8,25 +8,17 @@ /* global document */ -const MAX_WAV_LENGTH = 4294966806; +const MAX_WAV_SIZE = 4294966806; /* UI components */ -var lifeDisplayPanel = document.getElementById('life-display-panel'); - -/* Energy consumed while device is awaiting an active period */ - -const SLEEP_ENERGY = 0.125; - -/* Time spent opening files and waiting for microphone to warm up before recording starts */ - -const START_UP_TIME = 2; +const lifeDisplayPanel = document.getElementById('life-display-panel'); /* Whether or not the life display box should display the warning or original information */ var displaySizeWarning = true; -exports.getPanel = function () { +exports.getPanel = () => { return lifeDisplayPanel; @@ -36,50 +28,84 @@ exports.getPanel = function () { function getDailyCounts (timePeriods, recSecs, sleepSecs) { - var i, periodSecs, totalCompleteRecCount, completeRecCount, totalRecLength, timeRemaining, truncatedRecCount, truncatedRecTime; + let totalPrepTime = 0; + + const recordingTimes = []; + + let totalSleepTime = 0; + + let containsTruncatedRecording = false; + + for (let i = 0; i < timePeriods.length; i += 1) { + + let periodSecs = (timePeriods[i].endMins - timePeriods[i].startMins) * 60; + + /* First recording in a period has prep time scheduled for just before period */ + + periodSecs += 1; - /* Total number of recordings of the intended length */ - totalCompleteRecCount = 0; - /* Total number of recordings which could not be the intended length, so have been truncated */ - truncatedRecCount = 0; - /* Total length of all truncated files in seconds */ - truncatedRecTime = 0; + while (periodSecs > 1) { - for (i = 0; i < timePeriods.length; i += 1) { + /* If there's enough time to open a new file and record */ - /* Calculate how many full recording periods fit in the allotted time */ + if (periodSecs >= 1 + recSecs + sleepSecs) { - periodSecs = (timePeriods[i].endMins - timePeriods[i].startMins) * 60; - completeRecCount = Math.floor(periodSecs / (recSecs + sleepSecs)); + totalPrepTime += 1; + periodSecs -= 1; - /* Check if a truncated recording will fit in the rest of the period */ - totalRecLength = completeRecCount * (recSecs + sleepSecs); - timeRemaining = periodSecs - totalRecLength; + if (sleepSecs === 0) { - if (timeRemaining > 0) { + /* Prep time cuts into recording time */ - if (timeRemaining >= recSecs) { + recordingTimes.push(recSecs - 1); + periodSecs -= recSecs - 1; - completeRecCount += 1; + } else { + + recordingTimes.push(recSecs); + periodSecs -= recSecs; + + /* Prep time cuts into sleep time */ + + totalSleepTime += sleepSecs - 1; + periodSecs -= sleepSecs - 1; + + } } else { - truncatedRecTime += timeRemaining; - truncatedRecCount += 1; + containsTruncatedRecording = true; + + totalPrepTime += 1; + periodSecs -= 1; + + const truncatedRecordingLength = Math.min(recSecs, periodSecs); + recordingTimes.push(truncatedRecordingLength); + periodSecs -= truncatedRecordingLength; + + totalSleepTime += Math.min(periodSecs, sleepSecs); + periodSecs = 0; } } - totalCompleteRecCount += completeRecCount; + /* If there's an extra second left (not enough time to open a file and record anything), then just sleep */ + + if (periodSecs > 0) { + + totalSleepTime += periodSecs; + + } } return { - completeRecCount: totalCompleteRecCount, - truncatedRecCount: truncatedRecCount, - truncatedRecTime: truncatedRecTime - }; + prepTime: totalPrepTime, + recordingTimes: recordingTimes, + sleepTime: totalSleepTime, + containsTruncatedRecording: containsTruncatedRecording + } } @@ -109,14 +135,15 @@ function formatFileSize (fileSize) { } -/* Update storage and energy usage values in life display box */ +function getFileSize (sampleRate, sampleRateDivider, secs) { -exports.updateLifeDisplay = function (schedule, configuration, recLength, sleepLength, amplitudeThresholdingEnabled, dutyEnabled) { + return sampleRate / sampleRateDivider * 2 * secs; - var text, countResponse, completeRecCount, totalRecCount, recSize, truncatedRecordingSize, totalSize, energyUsed, energyPrecision, totalRecLength, truncatedRecCount, truncatedRecTime, upToFile, upToTotal, i, period, maxLength, length, upToSize, maxFileSize, recordingSize, prevPeriod, prevLength; +} - upToFile = amplitudeThresholdingEnabled ? 'up to ' : ''; - upToTotal = amplitudeThresholdingEnabled ? 'up to ' : ''; +/* Update storage and energy usage values in life display box */ + +exports.updateLifeDisplay = (schedule, configuration, recLength, sleepLength, amplitudeThresholdingEnabled, dutyEnabled, energySaverChecked) => { /* If no recording periods exist, do not perform energy calculations */ @@ -128,89 +155,146 @@ exports.updateLifeDisplay = function (schedule, configuration, recLength, sleepL } + const energySaverEnabled = energySaverChecked && (configuration.trueSampleRate <= 48); + + const fileOpenTime = 0.5; + const fileOpenEnergy = energySaverEnabled ? 35.0 : 40.0; + const waitTime = 0.5; + const waitEnergy = energySaverEnabled ? 7.0 : 10.0; + + const sleepCurrent = 0.16; + const recordingCurrent = energySaverEnabled ? configuration.energySaverRecordCurrent : configuration.recordCurrent; + const listenCurrent = energySaverEnabled ? configuration.energySaverListenCurrent : configuration.listenCurrent; + + let preparationInstances = 0; + + /* It's possible for file sizes to vary but overall storage consumption be known, so whether or not 'up to' is used in two locations in the information text varies */ + + let upToFile = amplitudeThresholdingEnabled; + const upToTotal = amplitudeThresholdingEnabled; + + let totalSleepTime = 0; + let totalSize = 0; + let maxLength = 0; + + let recordingTimes = []; + /* Calculate the amount of time spent recording each day */ if (dutyEnabled) { - countResponse = getDailyCounts(schedule, recLength, sleepLength); - completeRecCount = countResponse.completeRecCount; - truncatedRecCount = countResponse.truncatedRecCount; - truncatedRecTime = countResponse.truncatedRecTime; - totalRecLength = (completeRecCount * recLength) + truncatedRecTime; + const countResponse = getDailyCounts(schedule, recLength, sleepLength); + + preparationInstances = countResponse.prepTime; + + recordingTimes = countResponse.recordingTimes; + totalSleepTime = countResponse.sleepTime; /* Calculate the size of a days worth of recordings */ - recSize = configuration.sampleRate / configuration.sampleRateDivider * 2 * recLength; - truncatedRecordingSize = (truncatedRecTime * configuration.sampleRate / configuration.sampleRateDivider * 2); + for (let i = 0; i < recordingTimes.length; i++) { - totalSize = (recSize * completeRecCount) + truncatedRecordingSize; + totalSize += configuration.sampleRate / configuration.sampleRateDivider * 2 * recordingTimes[i]; - } else { + } - completeRecCount = schedule.length; - truncatedRecCount = 0; - truncatedRecTime = 0; + /* If any files are truncated, file size can vary */ - totalRecLength = 0; + if (countResponse.containsTruncatedRecording) { + + upToFile = true; + + } + + } else { maxLength = 0; - for (i = 0; i < schedule.length; i++) { + for (let i = 0; i < schedule.length; i++) { - period = schedule[i]; - length = period.endMins - period.startMins; + const period = schedule[i]; + const length = period.endMins - period.startMins; + + preparationInstances += 1; + + const recordingLength = (length * 60) - 1; + recordingTimes.push(recordingLength); /* If the periods differ in size, include 'up to' when describing the file size. If amplitude thresholding is enabled, it already will include this */ if (i > 0 && !amplitudeThresholdingEnabled) { - prevPeriod = schedule[i - 1]; - prevLength = prevPeriod.endMins - prevPeriod.startMins; + const prevPeriod = schedule[i - 1]; + const prevLength = prevPeriod.endMins - prevPeriod.startMins; if (length !== prevLength) { - upToFile = 'up to '; + upToFile = true; } } - totalRecLength += length; + totalSize += getFileSize(configuration.sampleRate, configuration.sampleRateDivider, recordingLength); - maxLength = (length > maxLength) ? length : maxLength; + maxLength = (recordingLength > maxLength) ? recordingLength : maxLength; } - totalRecLength *= 60; + } - totalSize = configuration.sampleRate / configuration.sampleRateDivider * 2 * totalRecLength; + const longestRecording = Math.max(...recordingTimes); - } + let upToSize = getFileSize(configuration.sampleRate, configuration.sampleRateDivider, longestRecording); + + let recordingsWillBeSplit = false; + + const maxWavLength = Math.floor(MAX_WAV_SIZE / (configuration.sampleRate / configuration.sampleRateDivider * 2)); + + for (let i = 0; i < recordingTimes.length; i++) { + + const recLength = recordingTimes[i]; - totalRecCount = completeRecCount + truncatedRecCount; + const recSize = getFileSize(configuration.sampleRate, configuration.sampleRateDivider, recLength); - if (completeRecCount > 1) { + if (recSize > MAX_WAV_SIZE) { - if (dutyEnabled) { + upToFile = true; + upToSize = MAX_WAV_SIZE; - upToSize = recSize; + recordingsWillBeSplit = true; - } else { + /* Cap recording at max length */ - maxFileSize = configuration.sampleRate / configuration.sampleRateDivider * 2 * maxLength * 60; - upToSize = maxFileSize; + recordingTimes[i] = maxWavLength; + + const timeRemaining = recLength - maxWavLength; + + /* If there's not enough remaining time to open a file and also record, don't bother */ + + if (timeRemaining > 1) { + + preparationInstances += 1; + + /* Order doesn't matter for storage/energy consumption, so just append recording period to the end */ + + recordingTimes.push(timeRemaining - 1); + + } } } - recordingSize = (completeRecCount > 1) ? upToSize : totalSize; + const totalRecCount = recordingTimes.length; + + const totalRecLength = recordingTimes.reduce((a, b) => a + b, 0); /* Generate life display message */ - text = ''; + let text = ''; - if (recordingSize > MAX_WAV_LENGTH && displaySizeWarning) { + if (recordingsWillBeSplit && displaySizeWarning) { text += 'Recordings will exceed the 4.3 GB max size of a WAV file so will be split.
'; @@ -222,35 +306,85 @@ exports.updateLifeDisplay = function (schedule, configuration, recLength, sleepL text += totalRecCount > 1 ? 's, ' : ' '; - if (completeRecCount > 1) { + if (totalRecCount > 1) { - text += 'each ' + upToFile + formatFileSize(upToSize) + ', '; + text += 'each '; + text += upToFile ? 'up to ' : ''; + text += formatFileSize(upToSize) + ', '; } - text += 'totalling ' + upToTotal + formatFileSize(totalSize) + '.
'; + text += 'totalling '; + text += upToTotal ? 'up to ' : ''; + text += formatFileSize(totalSize) + '.
'; } - /* Calculate amount of energy used both recording a sleeping over the course of a day */ + /* Add all the time outside the schedule as sleep time */ + + totalSleepTime += Math.max(0, 86400 - preparationInstances - totalRecLength - totalSleepTime); - energyUsed = Math.min(86400 - totalRecCount * START_UP_TIME, totalRecLength) * configuration.recordCurrent / 3600; + /* Preparation is split between opening the file and waiting. Each takes half a second */ - energyUsed += totalRecCount * START_UP_TIME * configuration.startCurrent / 3600; + const fileOpenEnergyUsage = preparationInstances * fileOpenTime * fileOpenEnergy / 3600; + const waitEnergyUsage = preparationInstances * waitTime * waitEnergy / 3600; - energyUsed += Math.max(0, 86400 - totalRecCount * START_UP_TIME - totalRecLength) * SLEEP_ENERGY / 3600; + const recordingEnergyUsage = totalRecLength * recordingCurrent / 3600; + const listeningEnergyUsage = totalRecLength * listenCurrent / 3600; - energyPrecision = energyUsed > 100 ? 10 : energyUsed > 50 ? 5 : energyUsed > 20 ? 2 : 1; + const sleepEnergyUsage = totalSleepTime * sleepCurrent / 3600; - energyUsed = Math.round(energyUsed / energyPrecision) * energyPrecision; + if (amplitudeThresholdingEnabled) { - text += 'Daily energy consumption will be approximately ' + energyUsed + ' mAh.'; + let minEnergyUsed = 0; + + minEnergyUsed += fileOpenEnergyUsage; + minEnergyUsed += waitEnergyUsage; + + minEnergyUsed += listeningEnergyUsage; + minEnergyUsed += sleepEnergyUsage; + + const minEnergyPrecision = minEnergyUsed > 100 ? 10 : minEnergyUsed > 50 ? 5 : minEnergyUsed > 20 ? 2 : 1; + + minEnergyUsed = Math.round(minEnergyUsed / minEnergyPrecision) * minEnergyPrecision; + + let maxEnergyUsed = 0; + + maxEnergyUsed += fileOpenEnergyUsage; + maxEnergyUsed += waitEnergyUsage; + + maxEnergyUsed += recordingEnergyUsage; + maxEnergyUsed += sleepEnergyUsage; + + const maxEnergyPrecision = maxEnergyUsed > 100 ? 10 : maxEnergyUsed > 50 ? 5 : maxEnergyUsed > 20 ? 2 : 1; + + maxEnergyUsed = Math.round(maxEnergyUsed / maxEnergyPrecision) * maxEnergyPrecision; + + text += 'Daily energy consumption will be between ' + minEnergyUsed + ' and ' + maxEnergyUsed + ' mAh.'; + + } else { + + let energyUsed = 0; + + energyUsed += fileOpenEnergyUsage; + energyUsed += waitEnergyUsage; + + energyUsed += recordingEnergyUsage; + energyUsed += sleepEnergyUsage; + + const energyPrecision = energyUsed > 100 ? 10 : energyUsed > 50 ? 5 : energyUsed > 20 ? 2 : 1; + + energyUsed = Math.round(energyUsed / energyPrecision) * energyPrecision; + + text += 'Daily energy consumption will be approximately ' + energyUsed + ' mAh.'; + + } lifeDisplayPanel.innerHTML = text; }; -exports.toggleSizeWarning = function (updateFunction) { +exports.toggleSizeWarning = (updateFunction) => { displaySizeWarning = !displaySizeWarning; updateFunction(); diff --git a/main.js b/main.js index 2c0dca1..e2e7c48 100644 --- a/main.js +++ b/main.js @@ -27,9 +27,9 @@ require('electron-debug')({ devToolsMode: 'undocked' }); -var mainWindow, aboutWindow, expansionWindow; +var mainWindow, aboutWindow, expansionWindow, splitWindow; -var expandProgressBar; +var expandProgressBar, splitProgressBar; function shrinkWindowHeight (windowHeight) { @@ -47,28 +47,77 @@ function shrinkWindowHeight (windowHeight) { } -function openExpansionWindow () { - - var iconLocation; +function openSplitWindow () { - if (expansionWindow) { + if (splitWindow) { return; } - iconLocation = '/build/icon.ico'; + const iconLocation = (process.platform === 'linux') ? '/build/icon.png' : '/build/icon.ico'; + + splitWindow = new BrowserWindow({ + width: 565, + height: shrinkWindowHeight(403), + title: 'Split AudioMoth Recordings', + useContentSize: true, + resizable: false, + fullscreenable: false, + icon: path.join(__dirname, iconLocation), + parent: mainWindow, + webPreferences: { + nodeIntegration: true + } + }); + + splitWindow.setMenu(null); + splitWindow.loadURL(path.join('file://', __dirname, 'expansion/split.html')); + + splitWindow.webContents.on('dom-ready', function () { + + mainWindow.webContents.send('poll-night-mode'); + + }); + + ipcMain.on('night-mode-poll-reply', (e, nightMode) => { + + if (splitWindow) { + + splitWindow.webContents.send('night-mode', nightMode); - if (process.platform === 'linux') { + } + + }); + + splitWindow.on('close', function (e) { + + if (splitProgressBar) { + + e.preventDefault(); + + } + + splitWindow = null; + + }); - iconLocation = '/build/icon.png'; +} + +function openExpansionWindow () { + + if (expansionWindow) { + + return; } + const iconLocation = (process.platform === 'linux') ? '/build/icon.png' : '/build/icon.ico'; + expansionWindow = new BrowserWindow({ width: 565, - height: shrinkWindowHeight(263), - title: 'Expand AudioMoth Recordings', + height: shrinkWindowHeight(575), + title: 'Expand AudioMoth T.WAV Recordings', useContentSize: true, resizable: false, fullscreenable: false, @@ -114,19 +163,13 @@ function openExpansionWindow () { function openAboutWindow () { - var iconLocation = '/build/icon.ico'; - if (aboutWindow) { return; } - if (process.platform === 'linux') { - - iconLocation = '/build/icon.png'; - - } + const iconLocation = (process.platform === 'linux') ? '/build/icon.png' : '/build/icon.ico'; aboutWindow = new BrowserWindow({ width: 400, @@ -166,16 +209,8 @@ function toggleNightMode () { app.on('ready', function () { - var menu, menuTemplate, iconLocation, windowHeight; - - iconLocation = '/build/icon.ico'; - windowHeight = shrinkWindowHeight(665); - - if (process.platform === 'linux') { - - iconLocation = '/build/icon.png'; - - } + const iconLocation = (process.platform === 'linux') ? '/build/icon.png' : '/build/icon.ico'; + const windowHeight = shrinkWindowHeight(665); mainWindow = new BrowserWindow({ title: 'AudioMoth Configuration App', @@ -202,7 +237,7 @@ app.on('ready', function () { }); - menuTemplate = [{ + const menuTemplate = [{ label: 'File', submenu: [{ label: 'Open Configuration', @@ -256,7 +291,47 @@ app.on('ready', function () { }, { type: 'separator' }, { - label: 'Expand AudioMoth Recordings', + type: 'radio', + id: 'scale1', + label: '16-Bit Amplitude Threshold Scale', + checked: false, + click: function () { + + mainWindow.webContents.send('amplitude-threshold-scale', 1); + + } + }, { + type: 'radio', + id: 'scale2', + label: 'Decibel Amplitude Threshold Scale', + checked: false, + click: function () { + + mainWindow.webContents.send('amplitude-threshold-scale', 2); + + } + }, { + type: 'radio', + id: 'scale0', + label: 'Percentage Amplitude Threshold Scale', + checked: true, + click: function () { + + mainWindow.webContents.send('amplitude-threshold-scale', 0); + + } + }, { + type: 'separator' + }, { + label: 'Split AudioMoth WAV Recordings', + accelerator: 'CommandOrControl+P', + click: function () { + + openSplitWindow(); + + } + }, { + label: 'Expand AudioMoth T.WAV Recordings', accelerator: 'CommandOrControl+E', click: function () { @@ -321,7 +396,7 @@ app.on('ready', function () { }] }]; - menu = Menu.buildFromTemplate(menuTemplate); + const menu = Menu.buildFromTemplate(menuTemplate); Menu.setApplicationMenu(menu); @@ -337,7 +412,9 @@ app.on('window-all-closed', function () { app.disableHardwareAcceleration(); -ipcMain.on('start-bar', (event, fileCount) => { +/* Expansion progress bar functions */ + +ipcMain.on('start-expansion-bar', (event, fileCount) => { if (expandProgressBar) { @@ -345,7 +422,7 @@ ipcMain.on('start-bar', (event, fileCount) => { } - var detail = 'Starting to expand file'; + let detail = 'Starting to expand file'; detail += (fileCount > 1) ? 's' : ''; detail += '.'; @@ -379,12 +456,10 @@ ipcMain.on('start-bar', (event, fileCount) => { }); -ipcMain.on('set-bar-progress', (event, fileNum, progress, name) => { +ipcMain.on('set-expansion-bar-progress', (event, fileNum, progress, name) => { - var index, fileCount; - - index = fileNum + 1; - fileCount = expandProgressBar.getOptions().maxValue / 100; + const index = fileNum + 1; + const fileCount = expandProgressBar.getOptions().maxValue / 100; if (expandProgressBar) { @@ -395,7 +470,7 @@ ipcMain.on('set-bar-progress', (event, fileNum, progress, name) => { }); -ipcMain.on('set-bar-error', (event, name) => { +ipcMain.on('set-expansion-bar-error', (event, name) => { if (expandProgressBar) { @@ -405,12 +480,12 @@ ipcMain.on('set-bar-error', (event, name) => { }); -ipcMain.on('set-bar-completed', (event, successCount, errorCount, errorWritingLog) => { - - var messageText; +ipcMain.on('set-expansion-bar-completed', (event, successCount, errorCount, errorWritingLog) => { if (expandProgressBar) { + let messageText; + expandProgressBar.setCompleted(); if (errorCount > 0) { @@ -446,7 +521,7 @@ ipcMain.on('set-bar-completed', (event, successCount, errorCount, errorWritingLo if (expansionWindow) { - expansionWindow.send('summary-closed'); + expansionWindow.send('expansion-summary-closed'); } @@ -456,7 +531,7 @@ ipcMain.on('set-bar-completed', (event, successCount, errorCount, errorWritingLo }); -ipcMain.on('poll-cancelled', (event) => { +ipcMain.on('poll-expansion-cancelled', (event) => { if (expandProgressBar) { @@ -469,3 +544,148 @@ ipcMain.on('poll-cancelled', (event) => { } }); + +/* Splitting progress bar functions */ + +ipcMain.on('start-split-bar', (event, fileCount) => { + + if (splitProgressBar) { + + return; + + } + + let detail = 'Starting to split file'; + detail += (fileCount > 1) ? 's' : ''; + detail += '.'; + + splitProgressBar = new ProgressBar({ + title: 'AudioMoth Configuration App', + text: 'Splitting files...', + detail: detail, + closeOnComplete: false, + indeterminate: false, + browserWindow: { + parent: splitWindow, + webPreferences: { + nodeIntegration: true + }, + closable: true, + modal: false + }, + maxValue: fileCount * 100 + }); + + splitProgressBar.on('aborted', () => { + + if (splitProgressBar) { + + splitProgressBar.close(); + splitProgressBar = null; + + } + + }); + +}); + +ipcMain.on('set-split-bar-progress', (event, fileNum, progress, name) => { + + const index = fileNum + 1; + const fileCount = splitProgressBar.getOptions().maxValue / 100; + + if (splitProgressBar) { + + splitProgressBar.value = (fileNum * 100) + progress; + splitProgressBar.detail = 'Splitting ' + name + ' (' + index + ' of ' + fileCount + ').'; + + } + +}); + +ipcMain.on('set-split-bar-error', (event, name) => { + + if (splitProgressBar) { + + splitProgressBar.detail = 'Error when splitting ' + name + '.'; + + } + +}); + +ipcMain.on('set-split-bar-completed', (event, successCount, errorCount, errorWritingLog) => { + + if (splitProgressBar) { + + let messageText; + + splitProgressBar.setCompleted(); + + if (errorCount > 0) { + + messageText = 'Errors occurred in ' + errorCount + ' file'; + messageText += (errorCount === 1 ? '' : 's'); + messageText += '.
'; + + if (errorWritingLog) { + + messageText += 'Failed to write ERRORS.TXT to destination.'; + + } else { + + messageText += 'See ERRORS.TXT for details.'; + + } + + } else { + + messageText = 'Successfully split ' + successCount + ' file'; + messageText += (successCount === 1 ? '' : 's'); + messageText += '.'; + + } + + splitProgressBar.detail = messageText; + + setTimeout(function () { + + splitProgressBar.close(); + splitProgressBar = null; + + if (splitWindow) { + + splitWindow.send('split-summary-closed'); + + } + + }, 5000); + + } + +}); + +ipcMain.on('poll-split-cancelled', (event) => { + + if (splitProgressBar) { + + event.returnValue = false; + + } else { + + event.returnValue = true; + + } + +}); + +/* Update which amplitude threshold scale option is checked in menu */ + +ipcMain.on('set-amplitude-threshold-scale', (event, index) => { + + const menu = Menu.getApplicationMenu(); + + const scaleMenuItems = [menu.getMenuItemById('scale0'), menu.getMenuItemById('scale1'), menu.getMenuItemById('scale2')]; + + scaleMenuItems[index].checked = true; + +}); diff --git a/nightMode.js b/nightMode.js index bbb76e8..5b6ddd4 100644 --- a/nightMode.js +++ b/nightMode.js @@ -10,7 +10,7 @@ const electron = require('electron'); var nightMode = false; -exports.isEnabled = function () { +exports.isEnabled = () => { return nightMode; @@ -18,13 +18,10 @@ exports.isEnabled = function () { function setNightMode (nm) { - var oldLink, newLink; - nightMode = nm; - oldLink = document.getElementById('uiCSS'); - - newLink = document.createElement('link'); + const oldLink = document.getElementById('uiCSS'); + const newLink = document.createElement('link'); newLink.setAttribute('id', 'uiCSS'); newLink.setAttribute('rel', 'stylesheet'); @@ -46,7 +43,7 @@ function setNightMode (nm) { exports.setNightMode = setNightMode; -exports.toggle = function () { +exports.toggle = () => { setNightMode(!nightMode); diff --git a/package.json b/package.json index 2743bee..75824f2 100755 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { "name": "AudioMoth-Config", - "version": "1.4.1", + "version": "1.5.0", "description": "The configuration app for the AudioMoth acoustic monitoring device.", "main": "main.js", "author": "openacousticdevices.info", "license": "ISC", "repository": { - "type" : "git", - "url" : "https://github.com/OpenAcousticDevices/AudioMoth-Configuration-App.git" + "type": "git", + "url": "https://github.com/OpenAcousticDevices/AudioMoth-Configuration-App.git" }, "scripts": { "postinstall": "install-app-deps", @@ -66,6 +66,7 @@ }, "dependencies": { "audiomoth-hid": "^2.1.0", + "audiomoth-utils": "^1.0.0", "bootstrap": "4.3.1", "bootstrap-slider": "^10.6.2", "electron-debug": "3.0.1", diff --git a/packetReader.js b/packetReader.js index 49398f7..5677101 100644 --- a/packetReader.js +++ b/packetReader.js @@ -22,7 +22,7 @@ function twoBytesToNumber (buffer, offset) { function digitWithLeadingZero (value) { - var formattedString = '0' + value; + const formattedString = '0' + value; return formattedString.substring(formattedString.length - 2); @@ -40,6 +40,24 @@ function formatDate (date) { } +function formatPercentage (mantissa, exponent) { + + let response = ''; + + if (exponent < 0) { + + response += '0.0000'.substring(0, 1 - exponent); + + } + + response += mantissa; + + for (let i = 0; i < exponent; i += 1) response += '0'; + + return response; + +} + /* #define MAX_START_STOP_PERIODS 5 @@ -74,35 +92,42 @@ typedef struct { uint16_t amplitudeThreshold; uint8_t requireAcousticConfiguration; AM_batteryLevelDisplayType_t batteryLevelDisplayType; + uint8_t minimumAmplitudeThresholdDuration; + uint8_t enableAmplitudeThresholdDecibelScale : 1; + uint8_t amplitudeThresholdDecibels : 7; + uint8_t enableAmplitudeThresholdPercentageScale : 1; + uint8_t amplitudeThresholdPercentageMantissa : 4; + int8_t amplitudeThresholdPercentageExponent : 3; } configSettings_t; */ -exports.read = function (packet) { +exports.read = (packet) => { - var i, j, time, gain, clockDivider, acquisitionCycles, oversampleRate, sampleRate, sampleRateDivider, sleepDuration, recordDuration, enableLED, activeStartStopPeriods, startStopPeriods, timezoneHours, enableLowVoltageCutoff, disableBatteryLevelDisplay, timezoneMinutes, disableSleepRecordCycle, earliestRecordingTime, latestRecordingTime, lowerFilterFreq, higherFilterFreq, amplitudeThreshold, startMinutes, stopMinutes, requireAcousticConfig, displayVoltageRange; + let startMinutes, stopMinutes; + // , amplitudeThresholdPercentageExponent, amplitudeThresholdPercentageMantissa, amplitudeThresholdPercentage, enableAmplitudeThresholdPercentageScale, amplitudeThresholdDecibel, amplitudeThresholdScale; /* Read and decode configuration packet */ - time = audiomoth.convertFourBytesFromBufferToDate(packet, 0); + const time = audiomoth.convertFourBytesFromBufferToDate(packet, 0); - gain = packet[4]; - clockDivider = packet[5]; - acquisitionCycles = packet[6]; - oversampleRate = packet[7]; + const gain = packet[4]; + const clockDivider = packet[5]; + const acquisitionCycles = packet[6]; + const oversampleRate = packet[7]; - sampleRate = fourBytesToNumber(packet, 8); - sampleRateDivider = packet[12]; + const sampleRate = fourBytesToNumber(packet, 8); + const sampleRateDivider = packet[12]; - sleepDuration = twoBytesToNumber(packet, 13); - recordDuration = twoBytesToNumber(packet, 15); + const sleepDuration = twoBytesToNumber(packet, 13); + const recordDuration = twoBytesToNumber(packet, 15); - enableLED = packet[17]; + const enableLED = packet[17]; - activeStartStopPeriods = packet[18]; - startStopPeriods = []; + const activeStartStopPeriods = packet[18]; + const startStopPeriods = []; - for (i = 0; i < activeStartStopPeriods; i += 1) { + for (let i = 0; i < activeStartStopPeriods; i += 1) { startMinutes = twoBytesToNumber(packet, 19 + 4 * i); stopMinutes = twoBytesToNumber(packet, 21 + 4 * i); @@ -111,28 +136,81 @@ exports.read = function (packet) { } - timezoneHours = packet[39]; + const timezoneHours = packet[39]; + + const enableLowVoltageCutoff = packet[40]; + const disableBatteryLevelDisplay = packet[41]; + + const timezoneMinutes = packet[42]; + + const disableSleepRecordCycle = packet[43]; + + const earliestRecordingTime = audiomoth.convertFourBytesFromBufferToDate(packet, 44); + const latestRecordingTime = audiomoth.convertFourBytesFromBufferToDate(packet, 48); + + const lowerFilterFreq = twoBytesToNumber(packet, 52); + const higherFilterFreq = twoBytesToNumber(packet, 54); + + const amplitudeThreshold = twoBytesToNumber(packet, 56); + + const packedByte0 = packet[58]; + + const requireAcousticConfig = packedByte0 & 1; + + const displayVoltageRange = (packedByte0 >> 1) & 1; + + const minimumAmplitudeThresholdDuration = (packedByte0 >> 2) & 0b111111; + + /* Amplitude threshold decibel scale */ + + const packedByte1 = packet[59]; + + /* Read bottom 7 bits */ + + const enableAmplitudeThresholdDecibelScale = packedByte1 & 1; - enableLowVoltageCutoff = packet[40]; - disableBatteryLevelDisplay = packet[41]; + const amplitudeThresholdDecibel = -1 * ((packedByte1 >> 1) & 0b1111111); - timezoneMinutes = packet[42]; + /* Amplitude threshold percentage scale */ - disableSleepRecordCycle = packet[43]; + const packedByte2 = packet[60]; - earliestRecordingTime = audiomoth.convertFourBytesFromBufferToDate(packet, 44); - latestRecordingTime = audiomoth.convertFourBytesFromBufferToDate(packet, 48); + const enableAmplitudeThresholdPercentageScale = packedByte2 & 1; - lowerFilterFreq = twoBytesToNumber(packet, 52); - higherFilterFreq = twoBytesToNumber(packet, 54); + /* Read the percentage value as a 4-bit mantissa and a 3-bit exponent */ - amplitudeThreshold = twoBytesToNumber(packet, 56); + const amplitudeThresholdPercentageMantissa = (packedByte2 >> 1) & 0b1111; - var packedByte = packet[58]; + /* Read final 3 bits and read as 3-bit two's complement */ - displayVoltageRange = (packedByte >> 1) & 1; + let amplitudeThresholdPercentageExponent = (packedByte2 >> 5) & 0b111; + amplitudeThresholdPercentageExponent = amplitudeThresholdPercentageExponent < 0b100 ? amplitudeThresholdPercentageExponent : amplitudeThresholdPercentageExponent - 0b1000; - requireAcousticConfig = packedByte & 1; + const amplitudeThresholdPercentage = formatPercentage(amplitudeThresholdPercentageMantissa, amplitudeThresholdPercentageExponent) + '%'; + + /* Which amplitude threshold scale should be displayed */ + + let amplitudeThresholdScale; + + if (enableAmplitudeThresholdPercentageScale === 1 && enableAmplitudeThresholdDecibelScale === 0) { + + amplitudeThresholdScale = 'Percentage'; + + } else if (enableAmplitudeThresholdDecibelScale === 1 && enableAmplitudeThresholdPercentageScale === 0) { + + amplitudeThresholdScale = 'Decibel'; + + } else { + + amplitudeThresholdScale = '16-Bit'; + + } + + const packedByte3 = packet[61]; + + const energySaverModeEnabled = packedByte3 & 1; + + const disable48DCFilter = (packedByte3 >> 1) & 1; /* Display configuration */ @@ -158,7 +236,7 @@ exports.read = function (packet) { console.log('Active recording periods:', activeStartStopPeriods); - for (j = 0; j < activeStartStopPeriods; j++) { + for (let j = 0; j < activeStartStopPeriods.length; j++) { console.log('Start: ' + formatTime(startStopPeriods[j].startMinutes) + ' - Stop: ' + formatTime(startStopPeriods[j].stopMinutes)); @@ -170,10 +248,33 @@ exports.read = function (packet) { console.log('Lower filter value:', lowerFilterFreq); console.log('Higher filter value:', higherFilterFreq); - console.log('Amplitude threshold:', amplitudeThreshold); + console.log('Amplitude threshold percentage flag:', (enableAmplitudeThresholdPercentageScale === 1)); + console.log('Amplitude threshold decibel flag:', (enableAmplitudeThresholdDecibelScale === 1)); + + console.log('Amplitude threshold (16-bit):', amplitudeThreshold); + + if (amplitudeThresholdScale === 'Percentage') { + + console.log('Percentage exponent:', amplitudeThresholdPercentageExponent); + console.log('Percentage mantissa:', amplitudeThresholdPercentageMantissa); + console.log('Amplitude threshold (percentage):', amplitudeThresholdPercentage); + + } else if (amplitudeThresholdScale === 'Decibel') { + + console.log('Amplitude threshold (decibels):', amplitudeThresholdDecibel); + + } + + console.log('Displayed amplitude threshold scale:', amplitudeThresholdScale); + + console.log('Minimum amplitude threshold duration:', minimumAmplitudeThresholdDuration); console.log('Acoustic configuration required:', requireAcousticConfig === 1); console.log('Use NiMH/LiPo voltage range for battery level indication:', displayVoltageRange === 1); + console.log('Energy saver mode enabled:', energySaverModeEnabled === 1); + + console.log('48 Hz DC blocking filter disabled:', disable48DCFilter === 1); + }; diff --git a/saveLoad.js b/saveLoad.js index 660889c..00916b7 100644 --- a/saveLoad.js +++ b/saveLoad.js @@ -22,23 +22,27 @@ const defaultSettings = { recordDuration: 55, sleepDuration: 5, passFiltersEnabled: false, - filterType: 1, + filterTypeIndex: 1, lowerFilter: 0, higherFilter: 24000, amplitudeThresholdingEnabled: false, amplitudeThreshold: 0, requireAcousticConfig: false, - displayVoltageRange: false + displayVoltageRange: false, + minimumAmplitudeThresholdDuration: 0, + amplitudeThresholdingScaleIndex: 0, + energySaverModeEnabled: false, + disable48DCFilter: false }; exports.defaultSettings = defaultSettings; /* Save configuration settings in UI to .config file */ -function saveConfiguration (timePeriods, ledEnabled, lowVoltageCutoffEnabled, batteryLevelCheckEnabled, sampleRateIndex, gain, recordDuration, sleepDuration, localTime, firstRecordingDate, lastRecordingDate, dutyEnabled, passFiltersEnabled, filterTypeIndex, lowerFilter, higherFilter, amplitudeThresholdingEnabled, amplitudeThreshold, requireAcousticConfig, displayVoltageRange, callback) { +function saveConfiguration (timePeriods, ledEnabled, lowVoltageCutoffEnabled, batteryLevelCheckEnabled, sampleRateIndex, gain, recordDuration, sleepDuration, localTime, firstRecordingDate, lastRecordingDate, dutyEnabled, passFiltersEnabled, filterTypeIndex, lowerFilter, higherFilter, amplitudeThresholdingEnabled, amplitudeThreshold, requireAcousticConfig, displayVoltageRange, minimumAmplitudeThresholdDuration, amplitudeThresholdingScaleIndex, energySaverModeEnabled, disable48DCFilter, callback) { - var configuration, fileName, sampleRate, filterType; + const sampleRate = constants.configurations[sampleRateIndex].trueSampleRate * 1000; - sampleRate = constants.configurations[sampleRateIndex].trueSampleRate * 1000; + let filterType; switch (filterTypeIndex) { @@ -54,7 +58,25 @@ function saveConfiguration (timePeriods, ledEnabled, lowVoltageCutoffEnabled, ba } - configuration = '{\n'; + let amplitudeThresholdingScale; + + switch (amplitudeThresholdingScaleIndex) { + + case 0: + amplitudeThresholdingScale = 'percentage'; + break; + case 1: + amplitudeThresholdingScale = '16bit'; + break; + case 2: + amplitudeThresholdingScale = 'decibel'; + break; + + } + + const versionString = electron.remote.app.getVersion(); + + let configuration = '{\n'; configuration += '"timePeriods": ' + JSON.stringify(timePeriods) + ',\n'; configuration += '"ledEnabled": ' + ledEnabled + ',\n'; configuration += '"lowVoltageCutoffEnabled": ' + lowVoltageCutoffEnabled + ',\n'; @@ -76,10 +98,15 @@ function saveConfiguration (timePeriods, ledEnabled, lowVoltageCutoffEnabled, ba configuration += '"amplitudeThresholdingEnabled": ' + amplitudeThresholdingEnabled + ',\n'; configuration += '"amplitudeThreshold": ' + amplitudeThreshold + ',\n'; configuration += '"requireAcousticConfig": ' + requireAcousticConfig + ',\n'; - configuration += '"displayVoltageRange": ' + displayVoltageRange + '\n'; + configuration += '"displayVoltageRange": ' + displayVoltageRange + ',\n'; + configuration += '"minimumAmplitudeThresholdDuration": ' + minimumAmplitudeThresholdDuration + ',\n'; + configuration += '"amplitudeThresholdingScale": \"' + amplitudeThresholdingScale + '\",\n'; + configuration += '"version": \"' + versionString + '\",\n'; + configuration += '"energySaverModeEnabled": ' + energySaverModeEnabled + ',\n'; + configuration += '"disable48DCFilter": ' + disable48DCFilter + '\n'; configuration += '}'; - fileName = dialog.showSaveDialogSync({ + const fileName = dialog.showSaveDialogSync({ title: 'Save configuration', nameFieldLabel: 'Configuration name', defaultPath: 'AudioMoth.config', @@ -103,18 +130,16 @@ exports.saveConfiguration = saveConfiguration; function getSampleRateIndex (jsonSampleRateIndex, jsonSampleRate) { - var i, closestIndex, distance, minDistance; - if (typeof jsonSampleRateIndex === 'undefined') { jsonSampleRate /= 1000; - minDistance = -1; - closestIndex = 0; + let minDistance = -1; + let closestIndex = 0; - for (i = 0; i < constants.configurations.length; i++) { + for (let i = 0; i < constants.configurations.length; i++) { - distance = Math.abs(constants.configurations[i].trueSampleRate - jsonSampleRate); + const distance = Math.abs(constants.configurations[i].trueSampleRate - jsonSampleRate); if (minDistance === -1 || distance < minDistance) { @@ -139,8 +164,6 @@ function getSampleRateIndex (jsonSampleRateIndex, jsonSampleRate) { function useLoadedConfiguration (err, data, callback) { - var jsonObj, validator, schema, timePeriods, ledEnabled, lowVoltageCutoffEnabled, batteryLevelCheckEnabled, sampleRateIndex, gain, dutyEnabled, recordDuration, sleepDuration, localTime, firstRecordingDate, lastRecordingDate, passFiltersEnabled, filterType, lowerFilter, higherFilter, amplitudeThresholdingEnabled, amplitudeThreshold, requireAcousticConfig, displayVoltageRange; - if (err) { dialog.showMessageBox(BrowserWindow.getFocusedWindow(), { @@ -157,9 +180,9 @@ function useLoadedConfiguration (err, data, callback) { try { - jsonObj = JSON.parse(data); - validator = new Validator(); - schema = { + const jsonObj = JSON.parse(data); + const validator = new Validator(); + const schema = { id: '/configuration', type: 'object', properties: { @@ -238,13 +261,28 @@ function useLoadedConfiguration (err, data, callback) { type: 'boolean' }, amplitudeThreshold: { - type: 'integer' + type: 'number' }, requireAcousticConfig: { type: 'boolean' }, displayVoltageRange: { type: 'boolean' + }, + minimumAmplitudeThresholdDuration: { + type: 'integer' + }, + amplitudeThresholdingScale: { + type: 'string' + }, + version: { + type: 'string' + }, + energySaverModeEnabled: { + type: 'boolean' + }, + disable48DCFilter: { + type: 'boolean' } }, required: ['timePeriods', 'ledEnabled', 'sleepDuration'] @@ -263,19 +301,19 @@ function useLoadedConfiguration (err, data, callback) { console.log(jsonObj); - timePeriods = []; + const timePeriods = jsonObj.timePeriods; - timePeriods = jsonObj.timePeriods; + const ledEnabled = jsonObj.ledEnabled; + const lowVoltageCutoffEnabled = (typeof jsonObj.lowVoltageCutoffEnabled === 'undefined') ? jsonObj.batteryCheckEnabled : jsonObj.lowVoltageCutoffEnabled; + const batteryLevelCheckEnabled = jsonObj.batteryLevelCheckEnabled; - ledEnabled = jsonObj.ledEnabled; - lowVoltageCutoffEnabled = (typeof jsonObj.lowVoltageCutoffEnabled === 'undefined') ? jsonObj.batteryCheckEnabled : jsonObj.lowVoltageCutoffEnabled; - batteryLevelCheckEnabled = jsonObj.batteryLevelCheckEnabled; + const sampleRateIndex = getSampleRateIndex(jsonObj.sampleRateIndex, jsonObj.sampleRate); - sampleRateIndex = getSampleRateIndex(jsonObj.sampleRateIndex, jsonObj.sampleRate); + const gain = (typeof jsonObj.gain === 'undefined') ? jsonObj.gainIndex : jsonObj.gain; - gain = (typeof jsonObj.gain === 'undefined') ? jsonObj.gainIndex : jsonObj.gain; + const dutyEnabled = (typeof jsonObj.dutyEnabled === 'undefined') ? true : jsonObj.dutyEnabled; - dutyEnabled = (typeof jsonObj.dutyEnabled === 'undefined') ? true : jsonObj.dutyEnabled; + let sleepDuration, recordDuration; if (dutyEnabled) { @@ -289,47 +327,78 @@ function useLoadedConfiguration (err, data, callback) { } - localTime = jsonObj.localTime; - firstRecordingDate = (typeof jsonObj.firstRecordingDate === 'undefined') ? '' : jsonObj.firstRecordingDate; - lastRecordingDate = (typeof jsonObj.lastRecordingDate === 'undefined') ? '' : jsonObj.lastRecordingDate; + const localTime = jsonObj.localTime; + const firstRecordingDate = (typeof jsonObj.firstRecordingDate === 'undefined') ? '' : jsonObj.firstRecordingDate; + const lastRecordingDate = (typeof jsonObj.lastRecordingDate === 'undefined') ? '' : jsonObj.lastRecordingDate; - passFiltersEnabled = (typeof jsonObj.passFiltersEnabled === 'undefined') ? false : jsonObj.passFiltersEnabled; + const passFiltersEnabled = (typeof jsonObj.passFiltersEnabled === 'undefined') ? false : jsonObj.passFiltersEnabled; + + let filterTypeIndex; if (passFiltersEnabled) { switch (jsonObj.filterType) { case 'low': - filterType = 0; + filterTypeIndex = 0; break; case 'band': - filterType = 1; + filterTypeIndex = 1; break; case 'high': - filterType = 2; + filterTypeIndex = 2; break; } } else { - filterType = 0; + filterTypeIndex = 0; } - lowerFilter = (typeof jsonObj.lowerFilter === 'undefined') ? -1 : jsonObj.lowerFilter; - higherFilter = (typeof jsonObj.higherFilter === 'undefined') ? -1 : jsonObj.higherFilter; + const lowerFilter = (typeof jsonObj.lowerFilter === 'undefined') ? -1 : jsonObj.lowerFilter; + const higherFilter = (typeof jsonObj.higherFilter === 'undefined') ? -1 : jsonObj.higherFilter; + + const amplitudeThresholdingEnabled = (typeof jsonObj.amplitudeThresholdingEnabled === 'undefined') ? false : jsonObj.amplitudeThresholdingEnabled; + const amplitudeThreshold = amplitudeThresholdingEnabled ? jsonObj.amplitudeThreshold : 0; + + const requireAcousticConfig = (typeof jsonObj.requireAcousticConfig === 'undefined') ? false : jsonObj.requireAcousticConfig; + + const displayVoltageRange = (typeof jsonObj.displayVoltageRange === 'undefined') ? false : jsonObj.displayVoltageRange; + + const minimumAmplitudeThresholdDuration = (typeof jsonObj.minimumAmplitudeThresholdDuration === 'undefined') ? 0 : jsonObj.minimumAmplitudeThresholdDuration; + + let amplitudeThresholdingScaleIndex; + + /* Previous versions of the app used 0 - 32768 as the amplitude threshold scale. If the scale index isn't in the save file, assume it's from an older app version and match threshold and scale to old range */ + + switch (jsonObj.amplitudeThresholdingScale) { + + case 'percentage': + amplitudeThresholdingScaleIndex = 0; + break; + case '16bit': + amplitudeThresholdingScaleIndex = 1; + break; + case 'decibel': + amplitudeThresholdingScaleIndex = 2; + break; + default: + amplitudeThresholdingScaleIndex = 1; + break; + + } - amplitudeThresholdingEnabled = (typeof jsonObj.amplitudeThresholdingEnabled === 'undefined') ? false : jsonObj.amplitudeThresholdingEnabled; - amplitudeThreshold = amplitudeThresholdingEnabled ? jsonObj.amplitudeThreshold : 0; + const energySaverModeEnabled = (jsonObj.energySaverModeEnabled === 'undefined') ? false : jsonObj.energySaverModeEnabled; - requireAcousticConfig = (typeof jsonObj.requireAcousticConfig === 'undefined') ? false : jsonObj.requireAcousticConfig; + const disable48DCFilter = (jsonObj.disable48DCFilter === 'undefined') ? false : jsonObj.disable48DCFilter; - displayVoltageRange = (typeof jsonObj.displayVoltageRange === 'undefined') ? false : jsonObj.displayVoltageRange; + const version = (typeof jsonObj.version === 'undefined') ? '< 1.5.0' : jsonObj.version; - callback(timePeriods, ledEnabled, lowVoltageCutoffEnabled, batteryLevelCheckEnabled, sampleRateIndex, gain, dutyEnabled, recordDuration, sleepDuration, localTime, firstRecordingDate, lastRecordingDate, passFiltersEnabled, filterType, lowerFilter, higherFilter, amplitudeThresholdingEnabled, amplitudeThreshold, requireAcousticConfig, displayVoltageRange); + callback(timePeriods, ledEnabled, lowVoltageCutoffEnabled, batteryLevelCheckEnabled, sampleRateIndex, gain, dutyEnabled, recordDuration, sleepDuration, localTime, firstRecordingDate, lastRecordingDate, passFiltersEnabled, filterTypeIndex, lowerFilter, higherFilter, amplitudeThresholdingEnabled, amplitudeThreshold, requireAcousticConfig, displayVoltageRange, minimumAmplitudeThresholdDuration, amplitudeThresholdingScaleIndex, energySaverModeEnabled, disable48DCFilter); - console.log('Config loaded'); + console.log('Loaded config created by Configuration App version ' + version); } catch (usageErr) { @@ -349,9 +418,9 @@ function useLoadedConfiguration (err, data, callback) { /* Display open dialog to allow users to load a .config file */ -exports.loadConfiguration = function (callback) { +exports.loadConfiguration = (callback) => { - var fileName = dialog.showOpenDialogSync({ + const fileName = dialog.showOpenDialogSync({ title: 'Open configuration', nameFieldLabel: 'Configuration name', defaultPath: 'AudioMoth.config', diff --git a/schedule/dateInput.js b/schedule/dateInput.js index 6bbc6be..8111c02 100644 --- a/schedule/dateInput.js +++ b/schedule/dateInput.js @@ -2,28 +2,26 @@ /* global document */ -var dateInputs = document.getElementsByClassName('custom-date-input'); +const dateInputs = document.getElementsByClassName('custom-date-input'); function dateToUTCString (date) { - var year, month, day; - - year = ('000' + date.getUTCFullYear()).slice(-4); - month = ('0' + (date.getUTCMonth() + 1)).slice(-2); - day = ('0' + date.getUTCDate()).slice(-2); + const year = ('000' + date.getUTCFullYear()).slice(-4); + const month = ('0' + (date.getUTCMonth() + 1)).slice(-2); + const day = ('0' + date.getUTCDate()).slice(-2); return year + '-' + month + '-' + day; } -exports.updateLocalTimeStatus = function (input, isLocalTime) { +exports.updateLocalTimeStatus = (input, isLocalTime) => { - var today, todayYear, todayMonth, todayDay, dayDiff, currentDate, lastValidDate; + let todayYear, todayMonth, todayDay, currentDate; - today = new Date(); + const today = new Date(); - dayDiff = today.getDate() - today.getUTCDate(); + const dayDiff = today.getDate() - today.getUTCDate(); - lastValidDate = new Date(input.getAttribute('lastValidDate')); + const lastValidDate = new Date(input.getAttribute('lastValidDate')); if (isLocalTime) { @@ -57,8 +55,6 @@ exports.updateLocalTimeStatus = function (input, isLocalTime) { function isValidDate (input, d) { - var maxDate, minDate; - if (!(d instanceof Date)) { return false; @@ -71,7 +67,7 @@ function isValidDate (input, d) { } - maxDate = new Date(input.getAttribute('max')); + const maxDate = new Date(input.getAttribute('max')); if (d > maxDate.getTime()) { @@ -79,7 +75,7 @@ function isValidDate (input, d) { } - minDate = new Date(input.getAttribute('min')); + const minDate = new Date(input.getAttribute('min')); if (d.getTime() < minDate.getTime()) { @@ -99,11 +95,9 @@ function setToLastValidDate (input, lastValidDateString) { function handleFocusOut (e) { - var input, inputDate; + const input = e.srcElement; - input = e.srcElement; - - inputDate = new Date(input.value); + const inputDate = new Date(input.value); if (isValidDate(input, inputDate)) { @@ -132,24 +126,22 @@ function handleFocusOut (e) { } -var year, month, day, today; - -today = new Date(); +const today = new Date(); -year = ('000' + today.getUTCFullYear()).slice(-4); -month = ('0' + (today.getUTCMonth() + 1)).slice(-2); -day = ('0' + today.getUTCDate()).slice(-2); -today = year + '-' + month + '-' + day; +const year = ('000' + today.getUTCFullYear()).slice(-4); +const month = ('0' + (today.getUTCMonth() + 1)).slice(-2); +const day = ('0' + today.getUTCDate()).slice(-2); +const todayString = year + '-' + month + '-' + day; for (let i = 0; i < dateInputs.length; i++) { dateInputs[i].addEventListener('focusout', handleFocusOut); - setToLastValidDate(dateInputs[i], today); + setToLastValidDate(dateInputs[i], todayString); dateInputs[i].setAttribute('lastValidDate', dateInputs[i].value); - dateInputs[i].setAttribute('min', today); + dateInputs[i].setAttribute('min', todayString); dateInputs[i].setAttribute('max', '2029-12-31'); } diff --git a/schedule/schedule.html b/schedule/schedule.html index 26ccfa0..02c5e2c 100644 --- a/schedule/schedule.html +++ b/schedule/schedule.html @@ -3,7 +3,7 @@
- +
diff --git a/schedule/schedule.js b/schedule/schedule.js index 877c900..f94e03c 100644 --- a/schedule/schedule.js +++ b/schedule/schedule.js @@ -9,13 +9,13 @@ exports.MAX_PERIODS = MAX_PERIODS; var timePeriods = []; -exports.getTimePeriodCount = function () { +exports.getTimePeriodCount = () => { return timePeriods.length; }; -exports.clear = function () { +exports.clear = () => { timePeriods = []; @@ -23,13 +23,13 @@ exports.clear = function () { /* Recording period data structure getter and setter */ -exports.getTimePeriods = function () { +exports.getTimePeriods = () => { return timePeriods; }; -exports.setTimePeriods = function (tp) { +exports.setTimePeriods = (tp) => { timePeriods = tp; diff --git a/schedule/scheduleEditor.js b/schedule/scheduleEditor.js index 26f998e..73ee71d 100644 --- a/schedule/scheduleEditor.js +++ b/schedule/scheduleEditor.js @@ -13,7 +13,7 @@ const scheduleBar = require('../scheduleBar.js'); const timeHandler = require('../timeHandler.js'); const schedule = require('../schedule/schedule.js'); -var timeList = document.getElementById('time-list'); +const timeList = document.getElementById('time-list'); exports.setSelectedPeriod = scheduleBar.setSelectedPeriod; exports.clearSelectedPeriod = scheduleBar.clearSelectedPeriod; @@ -22,11 +22,9 @@ exports.clearSelectedPeriod = scheduleBar.clearSelectedPeriod; function removeTime (timePeriod, tps) { - var i, startMins; + const startMins = timePeriod.startMins; - startMins = timePeriod.startMins; - - for (i = 0; i < tps.length; i++) { + for (let i = 0; i < tps.length; i++) { if (tps[i].startMins === startMins) { @@ -77,11 +75,11 @@ exports.overlaps = overlaps; function addTime (startMins, endMins) { - var timePeriods, i, newStart, newEnd; + let timePeriods, newStart, newEnd; timePeriods = schedule.getTimePeriods(); - for (i = 0; i < timePeriods.length; i++) { + for (let i = 0; i < timePeriods.length; i++) { /* If an overlap occurs, attempt to merge the overlapping time periods */ @@ -119,9 +117,9 @@ function addTime (startMins, endMins) { function formatAndAddTime (startTimestamp, endTimestamp) { - var timePeriod, utcPeriod, added; + let utcPeriod, added; - timePeriod = { + const timePeriod = { startMins: startTimestamp, endMins: endTimestamp }; @@ -185,10 +183,8 @@ exports.setTimePeriods = schedule.setTimePeriods; scheduleBar.prepareScheduleCanvas(true, function (selectedIndex) { - var event = new Event('change'); - timeList.options.selectedIndex = selectedIndex; timeList.focus(); - timeList.dispatchEvent(event); + timeList.dispatchEvent(new Event('change')); }); diff --git a/schedule/timeInput.js b/schedule/timeInput.js index 5a19574..a451d30 100644 --- a/schedule/timeInput.js +++ b/schedule/timeInput.js @@ -5,14 +5,14 @@ const DEFAULT_WIDTH = '50px'; const DEFAULT_HEIGHT = '25px'; -exports.getValue = function (div) { +exports.getValue = (div) => { - var inputValues = div.getAttribute('inputValues').split(','); + const inputValues = div.getAttribute('inputValues').split(','); return inputValues[0] + ':' + inputValues[1]; }; -exports.setValue = function (div, hour, minute) { +exports.setValue = (div, hour, minute) => { if (hour < 0 || hour > 24) { @@ -26,7 +26,7 @@ exports.setValue = function (div, hour, minute) { } - var textInput = getTextInput(div); + const textInput = getTextInput(div); setAttributeValue(textInput, 'inputValues', hour + ',' + minute); div.setAttribute('numbersEntered', '0'); div.setAttribute('selectedIndex', '-1'); @@ -42,16 +42,14 @@ function isEnabled (node) { } -exports.setEnabled = function (div, setting) { - - var hourSpan, colonSpan, minuteSpan, textInput; +exports.setEnabled = (div, setting) => { div.setAttribute('enabled', setting ? 'true' : 'false'); - hourSpan = getMinuteSpan(div); - colonSpan = getColonSpan(div); - minuteSpan = getHourSpan(div); - textInput = getTextInput(div); + const hourSpan = getMinuteSpan(div); + const colonSpan = getColonSpan(div); + const minuteSpan = getHourSpan(div); + const textInput = getTextInput(div); if (setting) { @@ -155,7 +153,7 @@ function getMinuteSpanFromChild (node) { function getInputValuesFromChild (node) { - var inputValues = getAttributeValue(node, 'inputValues').split(','); + const inputValues = getAttributeValue(node, 'inputValues').split(','); return [parseInt(inputValues[0]), parseInt(inputValues[1])]; @@ -175,14 +173,12 @@ function getNumbersEntered (node) { function highlightInput (node) { - var index, hourSpan, minuteSpan, deselectedColor; - - index = getSelectedIndex(node); + const index = getSelectedIndex(node); - hourSpan = getHourSpanFromChild(node); - minuteSpan = getMinuteSpanFromChild(node); + const hourSpan = getHourSpanFromChild(node); + const minuteSpan = getMinuteSpanFromChild(node); - deselectedColor = getTextInputFromChild(node).style.background; + const deselectedColor = getTextInputFromChild(node).style.background; if (index === 0) { @@ -209,7 +205,7 @@ function highlightInput (node) { } -exports.updateHighlights = function (div) { +exports.updateHighlights = (div) => { highlightInput(div.getElementsByClassName('time-holder')[0]); @@ -217,7 +213,7 @@ exports.updateHighlights = function (div) { function setHour (node, value) { - var inputValues = getInputValuesFromChild(node); + const inputValues = getInputValuesFromChild(node); setAttributeValue(node, 'inputValues', value + ',' + inputValues[1].toString()); @@ -225,7 +221,7 @@ function setHour (node, value) { function setMinute (node, value) { - var inputValues = getInputValuesFromChild(node); + const inputValues = getInputValuesFromChild(node); setAttributeValue(node, 'inputValues', inputValues[0].toString() + ',' + value); @@ -249,7 +245,7 @@ function resetInput (node, index) { function checkForBlanks (node) { - var inputValues = getInputValuesFromChild(node); + const inputValues = getInputValuesFromChild(node); if (inputValues[0] === -1) { @@ -267,10 +263,8 @@ function checkForBlanks (node) { function nextSelection (node) { - var currentSelectedIndex, newSelectedIndex; - - currentSelectedIndex = getSelectedIndex(node); - newSelectedIndex = (currentSelectedIndex + 1 >= 2) ? 1 : currentSelectedIndex + 1; + const currentSelectedIndex = getSelectedIndex(node); + const newSelectedIndex = (currentSelectedIndex + 1 >= 2) ? 1 : currentSelectedIndex + 1; setAttributeValue(node, 'selectedIndex', newSelectedIndex); highlightInput(node); @@ -280,10 +274,8 @@ function nextSelection (node) { function previousSelection (node) { - var currentSelectedIndex, newSelectedIndex; - - currentSelectedIndex = getSelectedIndex(node); - newSelectedIndex = (currentSelectedIndex - 1 < 0) ? 0 : currentSelectedIndex - 1; + const currentSelectedIndex = getSelectedIndex(node); + const newSelectedIndex = (currentSelectedIndex - 1 < 0) ? 0 : currentSelectedIndex - 1; setAttributeValue(node, 'selectedIndex', newSelectedIndex); highlightInput(node); @@ -293,10 +285,10 @@ function previousSelection (node) { function getMaxValue (node) { - var maxValue, selectedIndex, inputValues; + let maxValue; - selectedIndex = getSelectedIndex(node); - inputValues = getInputValuesFromChild(node); + const selectedIndex = getSelectedIndex(node); + const inputValues = getInputValuesFromChild(node); if (selectedIndex === 0) { @@ -326,10 +318,10 @@ function getMaxValue (node) { function updateHourValue (node, inputKey) { - var newValue, maxValue, selectedIndex, inputValues; + let newValue, inputValues; - maxValue = getMaxValue(node); - selectedIndex = getSelectedIndex(node); + const maxValue = getMaxValue(node); + const selectedIndex = getSelectedIndex(node); if (getNumbersEntered(node) === 1) { @@ -383,10 +375,10 @@ function updateHourValue (node, inputKey) { function updateMinuteValue (node, inputKey) { - var newValue, maxValue, selectedIndex, inputValues; + let newValue, inputValues; - maxValue = getMaxValue(node); - selectedIndex = getSelectedIndex(node); + const maxValue = getMaxValue(node); + const selectedIndex = getSelectedIndex(node); if (getNumbersEntered(node) === 1) { @@ -440,11 +432,11 @@ function updateMinuteValue (node, inputKey) { function incrementValue (node) { - var newValue, maxValue, inputValues, selectedIndex; + let newValue; - maxValue = getMaxValue(node); - inputValues = getInputValuesFromChild(node); - selectedIndex = getSelectedIndex(node); + const maxValue = getMaxValue(node); + const inputValues = getInputValuesFromChild(node); + const selectedIndex = getSelectedIndex(node); if (inputValues[selectedIndex] === -1) { @@ -475,11 +467,11 @@ function incrementValue (node) { function decrementValue (node) { - var newValue, maxValue, inputValues, selectedIndex; + let newValue; - maxValue = getMaxValue(node); - inputValues = getInputValuesFromChild(node); - selectedIndex = getSelectedIndex(node); + const maxValue = getMaxValue(node); + const inputValues = getInputValuesFromChild(node); + const selectedIndex = getSelectedIndex(node); if (inputValues[selectedIndex] === -1) { @@ -510,7 +502,7 @@ function decrementValue (node) { function handleKeyDown (e) { - var selectedIndex, patt, numbersEntered; + let selectedIndex, numbersEntered; selectedIndex = getAttributeValue(e.target, 'selectedIndex'); @@ -613,7 +605,7 @@ function handleKeyDown (e) { } - patt = new RegExp('^[0-9]$'); + const patt = new RegExp('^[0-9]$'); if (patt.test(e.key)) { @@ -646,11 +638,11 @@ class TimeInput extends HTMLElement { connectedCallback () { - var parent, width, height, divNode, attributes, i, inputNode, blockerNode, holderNode, hourSpanNode, colonSpanNode, minuteSpanNode; + let width, height; - parent = this.parentNode; + const parent = this.parentNode; - attributes = this.attributes; + const attributes = this.attributes; width = this.style.width; width = (width === '') ? DEFAULT_WIDTH : width; @@ -659,10 +651,10 @@ class TimeInput extends HTMLElement { parent.removeChild(this); - divNode = document.createElement('div'); + const divNode = document.createElement('div'); divNode.style = 'position: relative;'; - for (i = 0; i < attributes.length; i++) { + for (let i = 0; i < attributes.length; i++) { if (attributes[i].name !== 'style') { @@ -672,29 +664,29 @@ class TimeInput extends HTMLElement { } - inputNode = document.createElement('input'); + const inputNode = document.createElement('input'); inputNode.className = 'time-text-input'; inputNode.type = 'text'; inputNode.style = 'width: ' + width + '; height: ' + height + '; color: white; caret-color: transparent;'; divNode.appendChild(inputNode); - blockerNode = document.createElement('div'); + const blockerNode = document.createElement('div'); blockerNode.style = 'position: absolute; top: 0px; margin-left: 0px; margin-top: 0px; width: 100%; height: 100%;'; - holderNode = document.createElement('div'); + const holderNode = document.createElement('div'); holderNode.className = 'time-holder'; holderNode.style = 'position: absolute; top: 0px; margin-left: 9px; margin-top: 3px;'; - hourSpanNode = document.createElement('span'); + const hourSpanNode = document.createElement('span'); hourSpanNode.className = 'hour-span'; hourSpanNode.innerText = '00'; - colonSpanNode = document.createElement('span'); + const colonSpanNode = document.createElement('span'); colonSpanNode.className = 'colon-span'; colonSpanNode.innerText = ':'; - minuteSpanNode = document.createElement('span'); + const minuteSpanNode = document.createElement('span'); minuteSpanNode.className = 'minute-span'; minuteSpanNode.innerText = '00'; diff --git a/schedule/uiSchedule.js b/schedule/uiSchedule.js index 617356c..c9fe3cd 100644 --- a/schedule/uiSchedule.js +++ b/schedule/uiSchedule.js @@ -15,19 +15,17 @@ const timeHandler = require('../timeHandler.js'); const dateInput = require('./dateInput.js'); -var firstRecordingDateCheckbox = document.getElementById('first-date-checkbox'); -var firstRecordingDateLabel = document.getElementById('first-date-label'); -var firstRecordingDateInput = document.getElementById('first-date-input'); +const firstRecordingDateCheckbox = document.getElementById('first-date-checkbox'); +const firstRecordingDateLabel = document.getElementById('first-date-label'); +const firstRecordingDateInput = document.getElementById('first-date-input'); -var lastRecordingDateCheckbox = document.getElementById('last-date-checkbox'); -var lastRecordingDateLabel = document.getElementById('last-date-label'); -var lastRecordingDateInput = document.getElementById('last-date-input'); +const lastRecordingDateCheckbox = document.getElementById('last-date-checkbox'); +const lastRecordingDateLabel = document.getElementById('last-date-label'); +const lastRecordingDateInput = document.getElementById('last-date-input'); -exports.updateTimezoneStatus = function (isLocalTime) { +exports.updateTimezoneStatus = (isLocalTime) => { - var timezoneText; - - timezoneText = 'UTC'; + let timezoneText = 'UTC'; if (isLocalTime) { @@ -125,23 +123,21 @@ function updateLastRecordingDateUI () { } -exports.getFirstRecordingDate = function () { +exports.getFirstRecordingDate = () => { return firstRecordingDateCheckbox.checked ? firstRecordingDateInput.value : ''; }; -exports.getLastRecordingDate = function () { +exports.getLastRecordingDate = () => { return lastRecordingDateCheckbox.checked ? lastRecordingDateInput.value : ''; }; -exports.setFirstRecordingDate = function (firstRecordingDate) { - - var today; +exports.setFirstRecordingDate = (firstRecordingDate) => { - today = new Date(); + const today = new Date(); firstRecordingDateCheckbox.checked = (firstRecordingDate !== ''); updateFirstRecordingDateUI(); @@ -150,11 +146,9 @@ exports.setFirstRecordingDate = function (firstRecordingDate) { }; -exports.setLastRecordingDate = function (lastRecordingDate) { +exports.setLastRecordingDate = (lastRecordingDate) => { - var today; - - today = new Date(); + const today = new Date(); lastRecordingDateCheckbox.checked = (lastRecordingDate !== ''); updateLastRecordingDateUI(); @@ -215,19 +209,17 @@ exports.updateTimeList = uiScheduleEditor.updateTimeList; /* Prepare UI */ -exports.prepareUI = function (changeFunction) { - - var year, month, day, today; +exports.prepareUI = (changeFunction) => { - today = new Date(); + const today = new Date(); - year = ('000' + today.getUTCFullYear()).slice(-4); - month = ('0' + (today.getUTCMonth() + 1)).slice(-2); - day = ('0' + today.getUTCDate()).slice(-2); - today = year + '-' + month + '-' + day; + const year = ('000' + today.getUTCFullYear()).slice(-4); + const month = ('0' + (today.getUTCMonth() + 1)).slice(-2); + const day = ('0' + today.getUTCDate()).slice(-2); + const todayString = year + '-' + month + '-' + day; - firstRecordingDateInput.value = today; - lastRecordingDateInput.value = today; + firstRecordingDateInput.value = todayString; + lastRecordingDateInput.value = todayString; updateFirstRecordingDateUI(); updateLastRecordingDateUI(); diff --git a/schedule/uiScheduleEditor.js b/schedule/uiScheduleEditor.js index 6e3e360..d3170c2 100644 --- a/schedule/uiScheduleEditor.js +++ b/schedule/uiScheduleEditor.js @@ -14,13 +14,13 @@ const timeInput = require('./timeInput.js'); /* UI components */ -var timeList = document.getElementById('time-list'); -var addTimeButton = document.getElementById('add-time-button'); -var removeTimeButton = document.getElementById('remove-time-button'); -var clearTimeButton = document.getElementById('clear-time-button'); +const timeList = document.getElementById('time-list'); +const addTimeButton = document.getElementById('add-time-button'); +const removeTimeButton = document.getElementById('remove-time-button'); +const clearTimeButton = document.getElementById('clear-time-button'); -var startTimeInput = document.getElementById('start-time-input'); -var endTimeInput = document.getElementById('end-time-input'); +const startTimeInput = document.getElementById('start-time-input'); +const endTimeInput = document.getElementById('end-time-input'); /* Function which uses changed schedule to update life approximation */ @@ -30,13 +30,11 @@ var updateLifeDisplayOnChange; function addTimeOnClick () { - var startTimeSplit, endTimeSplit, startTimestamp, endTimestamp; + const startTimeSplit = timeInput.getValue(startTimeInput).split(':'); + const endTimeSplit = timeInput.getValue(endTimeInput).split(':'); - startTimeSplit = timeInput.getValue(startTimeInput).split(':'); - endTimeSplit = timeInput.getValue(endTimeInput).split(':'); - - startTimestamp = (parseInt(startTimeSplit[0], 10) * 60) + parseInt(startTimeSplit[1], 10); - endTimestamp = (parseInt(endTimeSplit[0], 10) * 60) + parseInt(endTimeSplit[1], 10); + const startTimestamp = (parseInt(startTimeSplit[0], 10) * 60) + parseInt(startTimeSplit[1], 10); + let endTimestamp = (parseInt(endTimeSplit[0], 10) * 60) + parseInt(endTimeSplit[1], 10); endTimestamp = (endTimestamp > 0) ? endTimestamp : 1440; @@ -50,11 +48,9 @@ function addTimeOnClick () { function getTimePeriodFromList () { - var values, timePeriod; - - values = timeList.value.split(','); + const values = timeList.value.split(','); - timePeriod = { + const timePeriod = { startMins: parseInt(values[0], 10), endMins: parseInt(values[1], 10) }; @@ -68,15 +64,13 @@ function getTimePeriodFromList () { function removeTimeOnClick () { - var timePeriods, timePeriod, ltps; + let timePeriods = schedule.getTimePeriods(); - timePeriods = schedule.getTimePeriods(); - - timePeriod = getTimePeriodFromList(); + const timePeriod = getTimePeriodFromList(); if (ui.isLocalTime()) { - ltps = timeHandler.convertTimePeriodsToLocal(timePeriods); + let ltps = timeHandler.convertTimePeriodsToLocal(timePeriods); ltps = scheduleEditor.removeTime(timePeriod, ltps); timePeriods = timeHandler.convertLocalTimePeriodsToUTC(ltps); @@ -100,9 +94,9 @@ function removeTimeOnClick () { function updateTimeList () { - var timePeriods, tp, i, startMins, endMins, timezoneText, timezoneOffset, option; + let tp; - timePeriods = schedule.getTimePeriods(); + const timePeriods = schedule.getTimePeriods(); if (ui.isLocalTime()) { @@ -124,15 +118,15 @@ function updateTimeList () { }); - for (i = 0; i < tp.length; i += 1) { + for (let i = 0; i < tp.length; i += 1) { - startMins = tp[i].startMins; - endMins = tp[i].endMins; + const startMins = tp[i].startMins; + const endMins = tp[i].endMins; - timezoneText = '(UTC'; + let timezoneText = '(UTC'; if (ui.isLocalTime()) { - timezoneOffset = timeHandler.calculateTimezoneOffsetHours(); + const timezoneOffset = timeHandler.calculateTimezoneOffsetHours(); if (timezoneOffset >= 0) { @@ -146,7 +140,7 @@ function updateTimeList () { timezoneText += ')'; - option = document.createElement('option'); + const option = document.createElement('option'); option.text = timeHandler.minsToTimeString(startMins) + ' - ' + timeHandler.minsToTimeString(endMins) + ' ' + timezoneText; option.value = [startMins, endMins]; timeList.add(option); @@ -175,15 +169,13 @@ function clearTimesOnClick () { } -exports.disableRemoveTimeButton = function () { +exports.disableRemoveTimeButton = () => { removeTimeButton.disabled = true; }; -exports.prepareUI = function (changeFunction) { - - var valueSplit, selectedTimePeriod; +exports.prepareUI = (changeFunction) => { updateLifeDisplayOnChange = changeFunction; @@ -197,8 +189,8 @@ exports.prepareUI = function (changeFunction) { if (timeList.value !== null && timeList.value !== '') { - valueSplit = timeList.value.split(','); - selectedTimePeriod = {startMins: parseInt(valueSplit[0]), endMins: parseInt(valueSplit[1])}; + const valueSplit = timeList.value.split(','); + const selectedTimePeriod = {startMins: parseInt(valueSplit[0]), endMins: parseInt(valueSplit[1])}; scheduleEditor.setSelectedPeriod(selectedTimePeriod); } else { diff --git a/scheduleBar.js b/scheduleBar.js index 04b21e8..1a24d1a 100644 --- a/scheduleBar.js +++ b/scheduleBar.js @@ -12,12 +12,12 @@ const ui = require('./ui.js'); const timeHandler = require('./timeHandler.js'); const schedule = require('./schedule/schedule.js'); -var timeCanvas = document.getElementById('time-canvas'); -var timeContext = timeCanvas.getContext('2d'); -var labelCanvas = document.getElementById('label-canvas'); -var labelContext = labelCanvas.getContext('2d'); +const timeCanvas = document.getElementById('time-canvas'); +const timeContext = timeCanvas.getContext('2d'); +const labelCanvas = document.getElementById('label-canvas'); +const labelContext = labelCanvas.getContext('2d'); -var canvasHolder = document.getElementById('canvas-holder'); +const canvasHolder = document.getElementById('canvas-holder'); var clickableCanvas; var clickCallback; @@ -27,7 +27,7 @@ var selectedPeriod = null; function rescale (canvas) { - var scaleFactor = 1; + let scaleFactor = 1; if (Object.prototype.hasOwnProperty.call(window, 'devicePixelRatio')) { @@ -53,11 +53,9 @@ function rescale (canvas) { function drawPeriod (startMins, endMins, timeCanvas) { - var recX, recLen; - /* width / 1440 minutes */ - recX = startMins * timeCanvas.width / 1440; - recLen = (endMins - startMins) * timeCanvas.width / 1440; + const recX = startMins * timeCanvas.width / 1440; + const recLen = (endMins - startMins) * timeCanvas.width / 1440; timeContext.fillRect(recX, 0, recLen, timeCanvas.height); @@ -65,23 +63,21 @@ function drawPeriod (startMins, endMins, timeCanvas) { function updateCanvas () { - var timePeriods, i, startMins, endMins, currentTimeDate, currentMins, currentX, localMidnight, localMidnightPx, startingAtMidnight, endingAtMidnight; - - timePeriods = schedule.getTimePeriods(); + let timePeriods = schedule.getTimePeriods(); timePeriods = ui.isLocalTime() ? timeHandler.convertTimePeriodsToLocal(timePeriods) : timePeriods; - currentTimeDate = new Date(); + const currentTimeDate = new Date(); timeContext.clearRect(0, 0, timeCanvas.width, timeCanvas.height); - localMidnight = timeHandler.convertTimeToLocal(1440); - startingAtMidnight = false; - endingAtMidnight = false; + const localMidnight = timeHandler.convertTimeToLocal(1440); + let startingAtMidnight = false; + let endingAtMidnight = false; - for (i = 0; i < timePeriods.length; i++) { + for (let i = 0; i < timePeriods.length; i++) { - startMins = timePeriods[i].startMins; - endMins = timePeriods[i].endMins; + const startMins = timePeriods[i].startMins; + const endMins = timePeriods[i].endMins; if (ui.isLocalTime()) { @@ -130,7 +126,7 @@ function updateCanvas () { if (startingAtMidnight && endingAtMidnight) { - localMidnightPx = localMidnight / 1440 * timeCanvas.width; + const localMidnightPx = localMidnight / 1440 * timeCanvas.width; timeContext.fillStyle = '#CC0000'; timeContext.fillRect(localMidnightPx, 0, 0.002 * timeCanvas.width, timeCanvas.height); @@ -153,6 +149,8 @@ function updateCanvas () { timeContext.fillRect(0.5 * timeCanvas.width, 0, 0.002 * timeCanvas.width, timeCanvas.height); timeContext.fillRect(0.75 * timeCanvas.width, 0, 0.002 * timeCanvas.width, timeCanvas.height); + let currentMins; + if (ui.isLocalTime()) { currentMins = (currentTimeDate.getHours() * 60) + currentTimeDate.getMinutes(); @@ -163,7 +161,7 @@ function updateCanvas () { } - currentX = currentMins * timeCanvas.width / 1440; + const currentX = currentMins * timeCanvas.width / 1440; timeContext.fillStyle = '#00AF00'; timeContext.fillRect((currentX - 1), 0, 0.004 * timeCanvas.width, timeCanvas.height); @@ -185,15 +183,13 @@ function updateCanvasTimer () { function updateSelectedPeriod (event) { - var rect, clickMins, timePeriods, i, startMins, endMins, selectedIndex; - - timePeriods = schedule.getTimePeriods(); + let timePeriods = schedule.getTimePeriods(); /* If there's only one possible time period and it covers the entire length of the schedule, don't bother with the full check */ if (timePeriods.length === 1 && timePeriods[0].startMins === 0 && timePeriods[0].endMins === 1440) { - selectedIndex = 0; + const selectedIndex = 0; if (clickCallback) { @@ -205,8 +201,8 @@ function updateSelectedPeriod (event) { } - rect = clickableCanvas.getBoundingClientRect(); - clickMins = (event.clientX - rect.left) / clickableCanvas.width * 1440; + const rect = clickableCanvas.getBoundingClientRect(); + const clickMins = (event.clientX - rect.left) / clickableCanvas.width * 1440; if (ui.isLocalTime()) { @@ -214,12 +210,12 @@ function updateSelectedPeriod (event) { } - selectedIndex = -1; + let selectedIndex = -1; - for (i = 0; i < timePeriods.length; i++) { + for (let i = 0; i < timePeriods.length; i++) { - startMins = timePeriods[i].startMins; - endMins = timePeriods[i].endMins; + const startMins = timePeriods[i].startMins; + const endMins = timePeriods[i].endMins; if (startMins > endMins) { @@ -251,7 +247,7 @@ function updateSelectedPeriod (event) { /* Set clicked period to specific index */ -exports.setSelectedPeriod = function (period) { +exports.setSelectedPeriod = (period) => { selectedPeriod = period; updateCanvas(); @@ -271,7 +267,7 @@ exports.clearSelectedPeriod = clearSelectedPeriod; function drawTimeLabels () { - var fontSize = 0.32 * timeCanvas.height; + const fontSize = 0.32 * timeCanvas.height; labelContext.clearRect(0, 0, labelCanvas.width, labelCanvas.height); @@ -288,18 +284,16 @@ function drawTimeLabels () { } labelContext.fillText('00:00', 0, fontSize); - labelContext.fillText('06:00', 0.225 * timeCanvas.width, fontSize); - labelContext.fillText('12:00', 0.475 * timeCanvas.width, fontSize); - labelContext.fillText('18:00', 0.725 * timeCanvas.width, fontSize); - labelContext.fillText('24:00', 0.945 * timeCanvas.width, fontSize); + labelContext.fillText('06:00', 0.25 * timeCanvas.width, fontSize); + labelContext.fillText('12:00', 0.5 * timeCanvas.width, fontSize); + labelContext.fillText('18:00', 0.751 * timeCanvas.width, fontSize); + labelContext.fillText('24:00', timeCanvas.width, fontSize); } exports.drawTimeLabels = drawTimeLabels; -exports.prepareScheduleCanvas = function (clickable, callback) { - - var offset; +exports.prepareScheduleCanvas = (clickable, callback) => { clickCallback = null; @@ -316,7 +310,8 @@ exports.prepareScheduleCanvas = function (clickable, callback) { clickableCanvas.style.position = 'absolute'; clickableCanvas.style.left = '50%'; - offset = -1 * clickableCanvas.width / 2; + + const offset = -1 * clickableCanvas.width / 2; clickableCanvas.style.marginLeft = offset + 'px'; clickableCanvas.style.top = timeCanvas.offsetTop + 'px'; @@ -339,9 +334,9 @@ exports.prepareScheduleCanvas = function (clickable, callback) { }; -exports.setSchedule = function (timePeriods) { +exports.setSchedule = (timePeriods) => { - var tps = timePeriods; + let tps = timePeriods; tps = timeHandler.sortPeriods(tps); diff --git a/settings/advanced.html b/settings/advanced.html index bea6cde..e371fa6 100644 --- a/settings/advanced.html +++ b/settings/advanced.html @@ -1,142 +1,37 @@ -
-
-
- Enable filtering: -
-
- -
-
- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - -
LowBandHigh
Filter type: - - - - - -
-
-
-
- - - - - -
-
- 0kHz -
-
- -
-
- 24kHz -
-
- - - - - -
-
- -
-
- -
-
-
-
- Enable amplitude threshold recording: -
-
- -
-
-
-
- 0 -
-
- +
+
+ Always require acoustic chime on switching to CUSTOM:
-
- 32768 +
+
-
-
- +
+
+ Use NiMH/LiPo voltage range for battery level indication: +
+
+
-
-
- Always require acoustic chime on switching to CUSTOM: + Disable 48Hz DC blocking filter:
-
- +
+
- Use NiMH/LiPo voltage range for battery level indication: + Enable energy saver mode:
-
- +
+
diff --git a/settings/durationInput.js b/settings/durationInput.js index 0b656dd..3179530 100644 --- a/settings/durationInput.js +++ b/settings/durationInput.js @@ -1,3 +1,9 @@ +/**************************************************************************** + * durationInput.js + * openacousticdevices.info + * November 2019 + *****************************************************************************/ + 'use strict'; /* global document, HTMLElement, customElements, Event */ @@ -40,13 +46,13 @@ function setAttributeValue (node, attributeName, value) { } -exports.getValue = function (div) { +exports.getValue = (div) => { return parseInt(div.getAttribute('inputValue')); }; -exports.setValue = function (div, value) { +exports.setValue = (div, value) => { div.setAttribute('inputValue', value.toString()); div.setAttribute('numbersEntered', value.toString().length - 1); @@ -54,14 +60,12 @@ exports.setValue = function (div, value) { }; -exports.setEnabled = function (div, setting) { - - var span, textInput; +exports.setEnabled = (div, setting) => { div.setAttribute('enabled', setting ? 'true' : 'false'); - span = getSpan(div); - textInput = getTextInput(div); + const span = getSpan(div); + const textInput = getTextInput(div); if (setting) { @@ -99,7 +103,7 @@ exports.setEnabled = function (div, setting) { function highlightInput (node) { - var span = getSpanFromChild(node); + const span = getSpanFromChild(node); if (getAttributeValue(node, 'selected') === 'true') { @@ -115,7 +119,7 @@ function highlightInput (node) { } -exports.updateHighlights = function (div) { +exports.updateHighlights = (div) => { highlightInput(div.getElementsByClassName('duration-holder')[0]); @@ -123,10 +127,10 @@ exports.updateHighlights = function (div) { function updateValue (inputKey, node) { - var currentNumbersEntered, currentInputValue, newValue, maxValue; + let newValue, maxValue; - currentNumbersEntered = parseInt(getAttributeValue(node, 'numbersEntered')); - currentInputValue = getAttributeValue(node, 'inputValue'); + const currentNumbersEntered = parseInt(getAttributeValue(node, 'numbersEntered')); + const currentInputValue = getAttributeValue(node, 'inputValue'); maxValue = getAttributeValue(node, 'maxValue'); maxValue = (maxValue === null) ? DEFAULT_MAX_VALUE : maxValue; @@ -173,7 +177,7 @@ function updateValue (inputKey, node) { function incrementValue (node) { - var newValue, maxValue, minValue; + let newValue, maxValue, minValue; maxValue = getAttributeValue(node, 'maxValue'); maxValue = (maxValue === null) ? DEFAULT_MAX_VALUE : parseInt(maxValue); @@ -193,7 +197,7 @@ function incrementValue (node) { function decrementValue (node) { - var newValue, minValue; + let newValue, minValue; minValue = getAttributeValue(node, 'minValue'); minValue = (minValue === null) ? DEFAULT_MIN_VALUE : parseInt(minValue); @@ -210,8 +214,6 @@ function decrementValue (node) { function handleKeyDown (e) { - var patt, currentNumbersEntered, maxCharLength, minValue; - if (e.key === 'Tab') { return; @@ -238,7 +240,7 @@ function handleKeyDown (e) { if (e.key === 'Backspace' || e.key === 'Delete') { - minValue = getAttributeValue(e.target, 'minValue'); + let minValue = getAttributeValue(e.target, 'minValue'); minValue = (minValue === null) ? DEFAULT_MIN_VALUE : parseInt(minValue); setAttributeValue(e.target, 'inputValue', minValue.toString()); @@ -251,13 +253,13 @@ function handleKeyDown (e) { } - patt = new RegExp('^[0-9]$'); + const patt = new RegExp('^[0-9]$'); if (patt.test(e.key)) { - currentNumbersEntered = parseInt(getAttributeValue(e.target, 'numbersEntered')); + const currentNumbersEntered = parseInt(getAttributeValue(e.target, 'numbersEntered')); - maxCharLength = getAttributeValue(e.target, 'maxcharlength'); + let maxCharLength = getAttributeValue(e.target, 'maxcharlength'); maxCharLength = (maxCharLength === null) ? DEFAULT_MAX_CHAR_LENGTH : maxCharLength; if (currentNumbersEntered <= maxCharLength) { @@ -283,11 +285,11 @@ class DurationInput extends HTMLElement { connectedCallback () { - var parent, attributes, width, height, i, divNode, blockerNode, inputNode, holderNode, spanNode, minValue; + let width, height, minValue; - parent = this.parentNode; + const parent = this.parentNode; - attributes = this.attributes; + const attributes = this.attributes; width = this.style.width; width = (width === '') ? DEFAULT_WIDTH : width; @@ -296,10 +298,10 @@ class DurationInput extends HTMLElement { parent.removeChild(this); - divNode = document.createElement('div'); + const divNode = document.createElement('div'); divNode.style = 'position: relative;'; - for (i = 0; i < attributes.length; i++) { + for (let i = 0; i < attributes.length; i++) { if (attributes[i].name !== 'style') { @@ -312,21 +314,21 @@ class DurationInput extends HTMLElement { minValue = divNode.getAttribute('minValue'); minValue = (minValue === null) ? DEFAULT_MIN_VALUE : parseInt(minValue); - inputNode = document.createElement('input'); + const inputNode = document.createElement('input'); inputNode.className = 'duration-text-input'; inputNode.type = 'text'; inputNode.style = 'width: ' + width + '; height: ' + height + '; color: white; caret-color: transparent;'; divNode.appendChild(inputNode); - blockerNode = document.createElement('div'); + const blockerNode = document.createElement('div'); blockerNode.style = 'position: absolute; top: 0px; margin-left: 0px; margin-top: 0px; width: 100%; height: 100%;'; - holderNode = document.createElement('div'); + const holderNode = document.createElement('div'); holderNode.className = 'duration-holder'; holderNode.style = 'position: absolute; top: 0px; margin-left: 5%; margin-top: 2px; width: 90%;'; - spanNode = document.createElement('span'); + const spanNode = document.createElement('span'); spanNode.className = 'duration-span'; spanNode.innerText = minValue.toString(); spanNode.style = 'float: right;'; diff --git a/settings/filtering.html b/settings/filtering.html new file mode 100644 index 0000000..9a0ee58 --- /dev/null +++ b/settings/filtering.html @@ -0,0 +1,171 @@ +
+
+
+ Enable filtering: +
+
+ +
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
LowBandHigh
Filter type: + + + + + +
+
+
+
+ + + + + +
+
+ 0kHz +
+
+ +
+
+ 24kHz +
+
+ + + + + +
+
+ +
+
+ +
+ +
+
+
+ Enable amplitude threshold: +
+
+ +
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
012510153060
Minimum trigger duration (s): + + + + + + + + + + + + + + + +
+
+
+
+ +
+
+ 0.001% +
+
+ +
+
+ 100% +
+
+ +
+
+ +
+
+ +
diff --git a/settings/uiAdvanced.js b/settings/uiAdvanced.js new file mode 100644 index 0000000..9ec2b66 --- /dev/null +++ b/settings/uiAdvanced.js @@ -0,0 +1,49 @@ +/**************************************************************************** + * uiAdvanced.js + * openacousticdevices.info + * January 2021 + *****************************************************************************/ + +const acousticConfigCheckBox = document.getElementById('acoustic-config-checkbox'); +const voltageRangeCheckBox = document.getElementById('voltage-range-checkbox'); +const energySaverModeCheckbox = document.getElementById('energy-saver-mode-checkbox'); +const disable48DCFilterCheckbox = document.getElementById('disable-48-dc-filter-checkbox'); + +exports.isAcousticConfigRequired = () => { + + return acousticConfigCheckBox.checked; + +}; + +exports.displayVoltageRange = () => { + + return voltageRangeCheckBox.checked; + +}; + +exports.isEnergySaverModeEnabled = () => { + + return energySaverModeCheckbox.checked; + +}; + +exports.is48DCFilterDisabled = () => { + + return disable48DCFilterCheckbox.checked; + +}; + +exports.fillUI = (settings) => { + + acousticConfigCheckBox.checked = settings.requireAcousticConfig; + voltageRangeCheckBox.checked = settings.displayVoltageRange; + energySaverModeCheckbox.checked = settings.energySaverModeEnabled; + disable48DCFilterCheckbox.checked = settings.disable48DCFilter; + +}; + +exports.prepareUI = (changeFunction) => { + + energySaverModeCheckbox.addEventListener('change', changeFunction); + +}; diff --git a/settings/uiFiltering.js b/settings/uiFiltering.js index 3bca23f..93cc6dc 100644 --- a/settings/uiFiltering.js +++ b/settings/uiFiltering.js @@ -4,42 +4,52 @@ * November 2019 *****************************************************************************/ -var constants = require('../constants.js'); +const electron = require('electron'); +const dialog = electron.remote.dialog; +const BrowserWindow = electron.remote.BrowserWindow; + +const constants = require('../constants.js'); const Slider = require('bootstrap-slider'); -const SLIDER_STEPS = [100, 100, 100, 100, 200, 500, 500, 1000]; +const FILTER_SLIDER_STEPS = [100, 100, 100, 100, 200, 500, 500, 1000]; + +const filterTypeLabel = document.getElementById('filter-type-label'); +const filterRadioButtons = document.getElementsByName('filter-radio'); +const filterRadioLabels = document.getElementsByName('filter-radio-label'); + +const highPassRow = document.getElementById('high-pass-row'); +const lowPassRow = document.getElementById('low-pass-row'); +const bandPassRow = document.getElementById('band-pass-row'); -var amplitudeThresholdValues; +const bandPassMaxLabel = document.getElementById('band-pass-filter-max-label'); +const bandPassMinLabel = document.getElementById('band-pass-filter-min-label'); +const lowPassMaxLabel = document.getElementById('low-pass-filter-max-label'); +const lowPassMinLabel = document.getElementById('low-pass-filter-min-label'); +const highPassMaxLabel = document.getElementById('high-pass-filter-max-label'); +const highPassMinLabel = document.getElementById('high-pass-min-label'); -var filterTypeLabel = document.getElementById('filter-type-label'); -var filterRadioButtons = document.getElementsByName('filter-radio'); -var filterRadioLabels = document.getElementsByName('filter-radio-label'); +const filterCheckbox = document.getElementById('filter-checkbox'); +const highPassFilterSlider = new Slider('#high-pass-filter-slider', {}); +const lowPassFilterSlider = new Slider('#low-pass-filter-slider', {}); +const bandPassFilterSlider = new Slider('#band-pass-filter-slider', {}); -var highPassRow = document.getElementById('high-pass-row'); -var lowPassRow = document.getElementById('low-pass-row'); -var bandPassRow = document.getElementById('band-pass-row'); +const filterLabel = document.getElementById('filter-label'); -var bandPassMaxLabel = document.getElementById('band-pass-filter-max-label'); -var bandPassMinLabel = document.getElementById('band-pass-filter-min-label'); -var lowPassMaxLabel = document.getElementById('low-pass-filter-max-label'); -var lowPassMinLabel = document.getElementById('low-pass-filter-min-label'); -var highPassMaxLabel = document.getElementById('high-pass-filter-max-label'); -var highPassMinLabel = document.getElementById('high-pass-min-label'); +const amplitudeThresholdingMaxLabel = document.getElementById('amplitude-thresholding-max-label'); +const amplitudeThresholdingMinLabel = document.getElementById('amplitude-thresholding-min-label'); -var filterCheckbox = document.getElementById('filter-checkbox'); -var highPassFilterSlider = new Slider('#high-pass-filter-slider', {}); -var lowPassFilterSlider = new Slider('#low-pass-filter-slider', {}); -var bandPassFilterSlider = new Slider('#band-pass-filter-slider', {}); +const amplitudeThresholdingCheckbox = document.getElementById('amplitude-thresholding-checkbox'); +const amplitudeThresholdingSlider = new Slider('#amplitude-thresholding-slider', {}); +const amplitudeThresholdingLabel = document.getElementById('amplitude-thresholding-label'); +const amplitudeThresholdingTimeTable = document.getElementById('trigger-time-table'); +const amplitudeThresholdingRadioButtons = document.getElementsByName('trigger-time-radio'); -var filterLabel = document.getElementById('filter-label'); +const VALID_AMPLITUDE_VALUES = [0, 1, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 36, 40, 44, 48, 52, 56, 60, 64, 72, 80, 88, 96, 104, 112, 120, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 832, 896, 960, 1024, 1152, 1280, 1408, 1536, 1664, 1792, 1920, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4608, 5120, 5632, 6144, 6656, 7168, 7680, 8192, 9216, 10240, 11264, 12288, 13312, 14336, 15360, 16384, 18432, 20480, 22528, 24576, 26624, 28672, 30720, 32768]; -var amplitudeThresholdingMaxLabel = document.getElementById('amplitude-thresholding-max-label'); -var amplitudeThresholdingMinLabel = document.getElementById('amplitude-thresholding-min-label'); +/* Whether or not to display a warning if minimum amplitude threshold is greater than recording length */ -var amplitudeThresholdingCheckbox = document.getElementById('amplitude-thresholding-checkbox'); -var amplitudeThresholdingSlider = new Slider('#amplitude-thresholding-slider', {}); -var amplitudeThresholdingLabel = document.getElementById('amplitude-thresholding-label'); +var displayDurationWarning = true; const FILTER_LOW = 0; const FILTER_BAND = 1; @@ -48,15 +58,46 @@ exports.FILTER_LOW = FILTER_LOW; exports.FILTER_BAND = FILTER_BAND; exports.FILTER_HIGH = FILTER_HIGH; +/* 0: 0-100%, 1: 16-Bit, 2: Decibels */ + +var amplitudeThresholdingScaleIndex = 0; +const AMPLITUDE_THRESHOLD_SCALE_PERCENTAGE = 0; +const AMPLITUDE_THRESHOLD_SCALE_16BIT = 1; +const AMPLITUDE_THRESHOLD_SCALE_DECIBEL = 2; + var previousSelectionType = 1; var updateLifeDisplayOnChange; +/* All possible slider values */ + +const THRESHOLD_PERCENTAGE_SLIDER_VALUES = []; +const THRESHOLD_16BIT_SLIDER_VALUES = []; +const THRESHOLD_DECIBEL_SLIDER_VALUES = []; + +/* Fill possible slider value lists */ + +const sliderMin = amplitudeThresholdingSlider.getAttribute('min'); +const sliderMax = amplitudeThresholdingSlider.getAttribute('max'); +const sliderStep = amplitudeThresholdingSlider.getAttribute('step'); + +for (let sIndex = sliderMin; sIndex <= sliderMax; sIndex += sliderStep) { + + const rawSlider = (sIndex / sliderMax); + + const amplitudeThresholdValues = convertAmplitudeThreshold(rawSlider); + + THRESHOLD_PERCENTAGE_SLIDER_VALUES.push(parseFloat(amplitudeThresholdValues.percentage)); + THRESHOLD_16BIT_SLIDER_VALUES.push(amplitudeThresholdValues.amplitude); + THRESHOLD_DECIBEL_SLIDER_VALUES.push(amplitudeThresholdValues.decibels); + +} + /* Add last() to Array */ if (!Array.prototype.last) { - Array.prototype.last = function () { + Array.prototype.last = () => { return this[this.length - 1]; @@ -74,7 +115,7 @@ function getSelectedRadioValue (radioName) { function updateFilterSliders () { - var newSelectionType = getSelectedRadioValue('filter-radio'); + const newSelectionType = getSelectedRadioValue('filter-radio'); if (previousSelectionType === FILTER_LOW) { @@ -118,11 +159,13 @@ function updateFilterSliders () { } +/* Update the text on the label which describes the range of frequencies covered by the filter */ + function updateFilterLabel () { - var filterIndex, currentBandPassLower, currentBandPassHigher, currentHighPass, currentLowPass; + let currentBandPassLower, currentBandPassHigher, currentHighPass, currentLowPass; - filterIndex = getSelectedRadioValue('filter-radio'); + const filterIndex = getSelectedRadioValue('filter-radio'); switch (filterIndex) { @@ -144,70 +187,160 @@ function updateFilterLabel () { } -function calculateAmplitudeThresholdValues () { +/* Work out where on the slider a given amplitude threshold value is */ - var i, j, step; +function lookupAmplitudeThresholdingSliderValue (amplitudeThreshold, scaleIndex) { - step = 2; + let searchList; - amplitudeThresholdValues = [0, 2, 4, 6, 8, 10, 12, 14, 16]; + switch (scaleIndex) { - for (i = 0; i < 11; i += 1) { + case AMPLITUDE_THRESHOLD_SCALE_PERCENTAGE: + searchList = THRESHOLD_PERCENTAGE_SLIDER_VALUES; + break; - for (j = 0; j < 8; j += 1) { + case AMPLITUDE_THRESHOLD_SCALE_16BIT: + searchList = THRESHOLD_16BIT_SLIDER_VALUES; + break; - amplitudeThresholdValues.push(amplitudeThresholdValues.last() + step); + case AMPLITUDE_THRESHOLD_SCALE_DECIBEL: + searchList = THRESHOLD_DECIBEL_SLIDER_VALUES; + break; - } + } + + for (let i = 0; i < searchList.length; i++) { + + if (searchList[i] === amplitudeThreshold) { - step *= 2; + return i; + + } } } -function lookupAmplitudeThresholdingSliderValue (amplitudeThreshold) { +/* Convert exponent and mantissa into a string */ + +function formatPercentage (mantissa, exponent) { - if (!amplitudeThresholdValues) calculateAmplitudeThresholdValues(); + let response = ''; - var index = amplitudeThresholdValues.findIndex(x => x === amplitudeThreshold); + if (exponent < 0) { + + response += '0.0000'.substring(0, 1 - exponent); + + } - return index ? Math.round(index / (amplitudeThresholdValues.length - 1) * 32768) : 0; + response += mantissa; + + for (let i = 0; i < exponent; i += 1) response += '0'; + + return response; } -function lookupAmplitudeThreshold (sliderValue) { +/* Calculate the amplitude threshold in the currently enabled scale */ + +function convertAmplitudeThreshold (rawSlider) { + + let exponent, mantissa, validAmplitude; + + const rawLog = (100 * rawSlider - 100); + + /* Decibels */ + + const decibelValue = 2 * Math.round(rawLog / 2); + + /* Percentage */ + + exponent = 2 + Math.floor(rawLog / 20.0); + mantissa = Math.round(Math.pow(10, rawLog / 20.0 - exponent + 2)); + + if (mantissa === 10) { + + mantissa = 1; + exponent += 1; + + } + + const percentageString = formatPercentage(mantissa, exponent); + + /* 16-bit */ + + const rawAmplitude = Math.round(32768 * Math.pow(10, rawLog / 20)); - if (!amplitudeThresholdValues) calculateAmplitudeThresholdValues(); + for (let i = 0; i < VALID_AMPLITUDE_VALUES.length; i++) { - return amplitudeThresholdValues[Math.round((amplitudeThresholdValues.length - 1) * sliderValue / 32768)]; + if (rawAmplitude <= VALID_AMPLITUDE_VALUES[i]) { + + validAmplitude = VALID_AMPLITUDE_VALUES[i]; + break; + + } + + } + + return { + decibels: decibelValue, + percentageExponent: exponent, + percentageMantissa: mantissa, + percentage: percentageString, + amplitude: validAmplitude + }; } +/* Update the amplitude threshold limit labels to match the chosen scale */ + function updateAmplitudeThresholdingLabel () { - var threshold, sliderValue; + const amplitudeThreshold = convertAmplitudeThreshold(amplitudeThresholdingSlider.getValue() / amplitudeThresholdingSlider.getAttribute('max')); - sliderValue = amplitudeThresholdingSlider.getValue(); + amplitudeThresholdingLabel.textContent = 'Amplitude threshold of '; - threshold = lookupAmplitudeThreshold(sliderValue); + switch (amplitudeThresholdingScaleIndex) { + + case AMPLITUDE_THRESHOLD_SCALE_PERCENTAGE: + + amplitudeThresholdingLabel.textContent += amplitudeThreshold.percentage + '%'; + break; - amplitudeThresholdingLabel.textContent = 'Audio segments with amplitude less than ' + threshold + ' will not be written to the SD card.'; + case AMPLITUDE_THRESHOLD_SCALE_16BIT: + + amplitudeThresholdingLabel.textContent += amplitudeThreshold.amplitude; + break; + + case AMPLITUDE_THRESHOLD_SCALE_DECIBEL: + + amplitudeThresholdingLabel.textContent += amplitudeThreshold.decibels + ' dB'; + break; + + } + + amplitudeThresholdingLabel.textContent += ' will be used when generating T.WAV files.'; } +/* Set the high-pass filter values to given value */ + function setHighPassSliderValue (value) { highPassFilterSlider.setValue(value); } +/* Set the low-pass filter values to given value */ + function setLowPassSliderValue (value) { lowPassFilterSlider.setValue(value); } +/* Set the band-pass filter values to 2 given values */ + function setBandPass (lowerSliderValue, higherSliderValue) { lowerSliderValue = (lowerSliderValue === -1) ? 0 : lowerSliderValue; @@ -217,9 +350,9 @@ function setBandPass (lowerSliderValue, higherSliderValue) { } -exports.setFilters = function (enabled, lowerSliderValue, higherSliderValue, filterType) { +/* Exported functions for setting values */ - var i; +exports.setFilters = (enabled, lowerSliderValue, higherSliderValue, filterType) => { filterCheckbox.checked = enabled; @@ -241,7 +374,7 @@ exports.setFilters = function (enabled, lowerSliderValue, higherSliderValue, fil } - for (i = 0; i < filterRadioButtons.length; i++) { + for (let i = 0; i < filterRadioButtons.length; i++) { filterRadioButtons[i].checked = (i === filterType); @@ -253,29 +386,48 @@ exports.setFilters = function (enabled, lowerSliderValue, higherSliderValue, fil }; -exports.setAmplitudeThreshold = function (enabled, amplitudeThreshold) { +exports.setAmplitudeThresholdScaleIndex = (scaleIndex) => { + + electron.ipcRenderer.send('set-amplitude-threshold-scale', scaleIndex); + + amplitudeThresholdingScaleIndex = scaleIndex; + updateAmplitudeThresholdingScale(); + +}; + +exports.setAmplitudeThreshold = (enabled, amplitudeThreshold) => { amplitudeThresholdingCheckbox.checked = enabled; - amplitudeThresholdingSlider.setValue(lookupAmplitudeThresholdingSliderValue(amplitudeThreshold)); + amplitudeThresholdingSlider.setValue(lookupAmplitudeThresholdingSliderValue(amplitudeThreshold, amplitudeThresholdingScaleIndex)); }; -exports.filteringIsEnabled = function () { +function setMinimumAmplitudeThresholdDuration (index) { + + amplitudeThresholdingRadioButtons[index].checked = true; + +} + +exports.setMinimumAmplitudeThresholdDuration = setMinimumAmplitudeThresholdDuration; + +/* External functions for obtaining values */ + +exports.filteringIsEnabled = () => { return filterCheckbox.checked; }; -exports.getFilterType = function () { +exports.getFilterType = () => { return getSelectedRadioValue('filter-radio'); }; -exports.getLowerSliderValue = function () { +exports.getLowerSliderValue = () => { - var filterIndex = getSelectedRadioValue('filter-radio'); + const filterIndex = getSelectedRadioValue('filter-radio'); switch (filterIndex) { @@ -290,9 +442,9 @@ exports.getLowerSliderValue = function () { }; -exports.getHigherSliderValue = function () { +exports.getHigherSliderValue = () => { - var filterIndex = getSelectedRadioValue('filter-radio'); + const filterIndex = getSelectedRadioValue('filter-radio'); switch (filterIndex) { @@ -307,20 +459,80 @@ exports.getHigherSliderValue = function () { }; -exports.getAmplitudeThreshold = function () { +function get16BitAmplitudeThreshold () { + + return convertAmplitudeThreshold(amplitudeThresholdingSlider.getValue() / amplitudeThresholdingSlider.getAttribute('max')).amplitude; + +} + +exports.get16BitAmplitudeThreshold = get16BitAmplitudeThreshold; + +function getPercentageAmplitudeThreshold () { + + return convertAmplitudeThreshold(amplitudeThresholdingSlider.getValue() / amplitudeThresholdingSlider.getAttribute('max')).percentage; + +} + +exports.getPercentageAmplitudeThreshold = getPercentageAmplitudeThreshold; + +function getDecibelAmplitudeThreshold () { + + return convertAmplitudeThreshold(amplitudeThresholdingSlider.getValue() / amplitudeThresholdingSlider.getAttribute('max')).decibels; - var sliderValue = amplitudeThresholdingSlider.getValue(); +} + +exports.getDecibelAmplitudeThreshold = getDecibelAmplitudeThreshold; + +function getPercentageAmplitudeThresholdExponentMantissa () { + + const results = convertAmplitudeThreshold(amplitudeThresholdingSlider.getValue() / amplitudeThresholdingSlider.getAttribute('max')); + + return { + exponent: results.percentageExponent, + mantissa: results.percentageMantissa + }; + +} + +exports.getPercentageAmplitudeThresholdExponentMantissa = getPercentageAmplitudeThresholdExponentMantissa; + +exports.getAmplitudeThreshold = () => { + + switch (amplitudeThresholdingScaleIndex) { + + case AMPLITUDE_THRESHOLD_SCALE_PERCENTAGE: + return getPercentageAmplitudeThreshold(); - return lookupAmplitudeThreshold(sliderValue); + case AMPLITUDE_THRESHOLD_SCALE_16BIT: + return get16BitAmplitudeThreshold(); + + case AMPLITUDE_THRESHOLD_SCALE_DECIBEL: + return getDecibelAmplitudeThreshold(); + + } }; -exports.amplitudeThresholdingIsEnabled = function () { +exports.amplitudeThresholdingIsEnabled = () => { return amplitudeThresholdingCheckbox.checked; }; +exports.getAmplitudeThresholdScaleIndex = () => { + + return amplitudeThresholdingScaleIndex; + +}; + +function getMinimumAmplitudeThresholdDuration () { + + return parseInt(getSelectedRadioValue('trigger-time-radio')); + +} + +exports.getMinimumAmplitudeThresholdDuration = getMinimumAmplitudeThresholdDuration; + /* Enable/disable amplitude threshold UI based on checkbox */ function updateAmplitudeThresholdingUI () { @@ -334,6 +546,14 @@ function updateAmplitudeThresholdingUI () { amplitudeThresholdingLabel.style.color = ''; updateAmplitudeThresholdingLabel(); + amplitudeThresholdingTimeTable.style.color = ''; + + for (let i = 0; i < amplitudeThresholdingRadioButtons.length; i++) { + + amplitudeThresholdingRadioButtons[i].disabled = false; + + } + } else { amplitudeThresholdingSlider.disable(); @@ -341,7 +561,15 @@ function updateAmplitudeThresholdingUI () { amplitudeThresholdingMinLabel.style.color = 'grey'; amplitudeThresholdingLabel.style.color = 'grey'; - amplitudeThresholdingLabel.textContent = 'All audio segments will be written to the SD card.'; + amplitudeThresholdingLabel.textContent = 'All audio will be written to a .WAV file.'; + + amplitudeThresholdingTimeTable.style.color = 'grey'; + + for (let i = 0; i < amplitudeThresholdingRadioButtons.length; i++) { + + amplitudeThresholdingRadioButtons[i].disabled = true; + + } } @@ -351,11 +579,11 @@ function updateAmplitudeThresholdingUI () { exports.updateAmplitudeThresholdingUI = updateAmplitudeThresholdingUI; -function updateFilterUI () { +/* Check if the filtering UI should be enabled and update accordingly */ - var filterIndex, i; +function updateFilterUI () { - filterIndex = getSelectedRadioValue('filter-radio'); + const filterIndex = getSelectedRadioValue('filter-radio'); switch (filterIndex) { @@ -381,7 +609,7 @@ function updateFilterUI () { filterTypeLabel.style.color = ''; - for (i = 0; i < filterRadioButtons.length; i++) { + for (let i = 0; i < filterRadioButtons.length; i++) { filterRadioButtons[i].style.color = ''; filterRadioButtons[i].disabled = false; @@ -405,7 +633,7 @@ function updateFilterUI () { filterTypeLabel.style.color = 'grey'; - for (i = 0; i < filterRadioButtons.length; i++) { + for (let i = 0; i < filterRadioButtons.length; i++) { filterRadioButtons[i].style.color = 'grey'; filterRadioButtons[i].disabled = true; @@ -442,16 +670,14 @@ function roundToSliderStep (value, step) { /* Update UI according to new sample rate selection */ -exports.sampleRateChange = function () { - - var sampleRateIndex, sampleRate, maxFreq, currentBandPassHigher, currentBandPassLower, newBandPassHigher, newBandPassLower, labelText, currentLowPass, currentHighPass, newLowPass, newHighPass; +exports.sampleRateChange = () => { - sampleRateIndex = getSelectedRadioValue('sample-rate-radio'); + const sampleRateIndex = getSelectedRadioValue('sample-rate-radio'); - sampleRate = constants.configurations[sampleRateIndex].trueSampleRate * 1000; - maxFreq = sampleRate / 2; + const sampleRate = constants.configurations[sampleRateIndex].trueSampleRate * 1000; + const maxFreq = sampleRate / 2; - labelText = (maxFreq / 1000) + 'kHz'; + const labelText = (maxFreq / 1000) + 'kHz'; lowPassMaxLabel.textContent = labelText; highPassMaxLabel.textContent = labelText; @@ -461,44 +687,108 @@ exports.sampleRateChange = function () { lowPassFilterSlider.setAttribute('max', maxFreq); bandPassFilterSlider.setAttribute('max', maxFreq); - highPassFilterSlider.setAttribute('step', SLIDER_STEPS[sampleRateIndex]); - lowPassFilterSlider.setAttribute('step', SLIDER_STEPS[sampleRateIndex]); - bandPassFilterSlider.setAttribute('step', SLIDER_STEPS[sampleRateIndex]); + highPassFilterSlider.setAttribute('step', FILTER_SLIDER_STEPS[sampleRateIndex]); + lowPassFilterSlider.setAttribute('step', FILTER_SLIDER_STEPS[sampleRateIndex]); + bandPassFilterSlider.setAttribute('step', FILTER_SLIDER_STEPS[sampleRateIndex]); /* Validate current band-pass filter values */ - currentBandPassHigher = Math.max(...bandPassFilterSlider.getValue()); - currentBandPassLower = Math.min(...bandPassFilterSlider.getValue()); + const currentBandPassHigher = Math.max(...bandPassFilterSlider.getValue()); + const currentBandPassLower = Math.min(...bandPassFilterSlider.getValue()); - newBandPassLower = currentBandPassLower > maxFreq ? 0 : currentBandPassLower; + const newBandPassLower = currentBandPassLower > maxFreq ? 0 : currentBandPassLower; - newBandPassHigher = currentBandPassHigher > maxFreq ? maxFreq : currentBandPassHigher; + const newBandPassHigher = currentBandPassHigher > maxFreq ? maxFreq : currentBandPassHigher; - setBandPass(roundToSliderStep(Math.max(newBandPassHigher, newBandPassLower), SLIDER_STEPS[sampleRateIndex]), roundToSliderStep(Math.min(newBandPassHigher, newBandPassLower), SLIDER_STEPS[sampleRateIndex])); + setBandPass(roundToSliderStep(Math.max(newBandPassHigher, newBandPassLower), FILTER_SLIDER_STEPS[sampleRateIndex]), roundToSliderStep(Math.min(newBandPassHigher, newBandPassLower), FILTER_SLIDER_STEPS[sampleRateIndex])); /* Validate current low-pass filter value */ - currentLowPass = lowPassFilterSlider.getValue(); - newLowPass = currentLowPass > maxFreq ? maxFreq : currentLowPass; - setLowPassSliderValue(roundToSliderStep(newLowPass, SLIDER_STEPS[sampleRateIndex])); + const currentLowPass = lowPassFilterSlider.getValue(); + const newLowPass = currentLowPass > maxFreq ? maxFreq : currentLowPass; + setLowPassSliderValue(roundToSliderStep(newLowPass, FILTER_SLIDER_STEPS[sampleRateIndex])); /* Validate current high-pass filter value */ - currentHighPass = highPassFilterSlider.getValue(); - newHighPass = currentHighPass > maxFreq ? maxFreq : currentHighPass; - setHighPassSliderValue(roundToSliderStep(newHighPass, SLIDER_STEPS[sampleRateIndex])); + const currentHighPass = highPassFilterSlider.getValue(); + const newHighPass = currentHighPass > maxFreq ? maxFreq : currentHighPass; + setHighPassSliderValue(roundToSliderStep(newHighPass, FILTER_SLIDER_STEPS[sampleRateIndex])); updateFilterLabel(); }; +/* Update the labels either side of the amplitude threshold scale */ + +function updateAmplitudeThresholdingScale () { + + updateAmplitudeThresholdingLabel(); + + switch (amplitudeThresholdingScaleIndex) { + + default: + case AMPLITUDE_THRESHOLD_SCALE_PERCENTAGE: + amplitudeThresholdingMinLabel.innerHTML = '0.001%'; + amplitudeThresholdingMaxLabel.innerHTML = '100%'; + break; + + case AMPLITUDE_THRESHOLD_SCALE_16BIT: + amplitudeThresholdingMinLabel.innerHTML = '0'; + amplitudeThresholdingMaxLabel.innerHTML = '32768'; + break; + + case AMPLITUDE_THRESHOLD_SCALE_DECIBEL: + amplitudeThresholdingMinLabel.innerHTML = '-100 dB'; + amplitudeThresholdingMaxLabel.innerHTML = '0 dB'; + break; + + } + +} + +/* Receive message from the menu about which amplitude threshold scale to use */ + +electron.ipcRenderer.on('amplitude-threshold-scale', (e, indexSelected) => { + + amplitudeThresholdingScaleIndex = indexSelected; + updateAmplitudeThresholdingScale(); + +}); + +/* Check whether recording time is less than minimum amplitude threshold duration and display warning if needed */ + +exports.checkMinimumTriggerTime = (recordingLength) => { + + if (!amplitudeThresholdingCheckbox.checked || !displayDurationWarning) { + + return; + + } + + const minimumLengths = [0, 1, 2, 5, 10, 15, 30, 60]; + + if (minimumLengths[getMinimumAmplitudeThresholdDuration()] > recordingLength) { + + dialog.showMessageBox(BrowserWindow.getFocusedWindow(), { + type: 'warning', + checkboxLabel: 'Don\'t warn me again', + title: 'Warning', + message: 'Note that the minimum threshold duration is currently longer than the scheduled recording duration.' + }).then(response => { + + displayDurationWarning = !response.checkboxChecked; + + }); + + } + +}; + /* Add listeners to all radio buttons which update the filter sliders */ function addRadioButtonListeners () { - var i; - - for (i = 0; i < filterRadioButtons.length; i++) { + for (let i = 0; i < filterRadioButtons.length; i++) { filterRadioButtons[i].addEventListener('change', function () { @@ -514,24 +804,39 @@ function addRadioButtonListeners () { /* Prepare UI */ -exports.prepareUI = function (changeFunction) { +exports.prepareUI = (changeFunction, checkRecordingDurationFunction) => { updateLifeDisplayOnChange = changeFunction; amplitudeThresholdingCheckbox.addEventListener('change', updateAmplitudeThresholdingUI); - filterCheckbox.addEventListener('change', function () { - updateFilterUI(); + addRadioButtonListeners(); + + filterCheckbox.addEventListener('change', () => { + updateFilterLabel(); + updateFilterUI(); }); - addRadioButtonListeners(); + for (let i = 0; i < amplitudeThresholdingRadioButtons.length; i++) { - bandPassFilterSlider.on('change', updateFilterLabel); + amplitudeThresholdingRadioButtons[i].addEventListener('click', checkRecordingDurationFunction); - lowPassFilterSlider.on('change', updateFilterLabel); + } + + amplitudeThresholdingCheckbox.addEventListener('click', () => { + if (amplitudeThresholdingCheckbox.checked) { + + checkRecordingDurationFunction(); + + } + + }); + + bandPassFilterSlider.on('change', updateFilterLabel); + lowPassFilterSlider.on('change', updateFilterLabel); highPassFilterSlider.on('change', updateFilterLabel); amplitudeThresholdingSlider.on('change', updateAmplitudeThresholdingLabel); diff --git a/settings/uiSettings.js b/settings/uiSettings.js index e140666..ee3d28b 100644 --- a/settings/uiSettings.js +++ b/settings/uiSettings.js @@ -8,24 +8,21 @@ const electron = require('electron'); const dialog = electron.remote.dialog; const uiFiltering = require('./uiFiltering.js'); +const uiAdvanced = require('./uiAdvanced.js'); const durationInput = require('./durationInput.js'); /* UI components */ -var sampleRadioButtons = document.getElementsByName('sample-rate-radio'); -var gainRadioButtons = document.getElementsByName('gain-radio'); +const sampleRadioButtons = document.getElementsByName('sample-rate-radio'); +const gainRadioButtons = document.getElementsByName('gain-radio'); -var dutyCheckBox = document.getElementById('duty-checkbox'); +const dutyCheckBox = document.getElementById('duty-checkbox'); -var recordingDurationInput = document.getElementById('recording-duration-input'); -var sleepDurationInput = document.getElementById('sleep-duration-input'); +const recordingDurationInput = document.getElementById('recording-duration-input'); +const sleepDurationInput = document.getElementById('sleep-duration-input'); -var recordingDurationLabel = document.getElementById('recording-duration-label'); -var sleepDurationLabel = document.getElementById('sleep-duration-label'); - -var acousticConfigCheckBox = document.getElementById('acoustic-config-checkbox'); - -var voltageRangeCheckBox = document.getElementById('voltage-range-checkbox'); +const recordingDurationLabel = document.getElementById('recording-duration-label'); +const sleepDurationLabel = document.getElementById('sleep-duration-label'); /* Whether or not the warning on sleep duration being set less than 5 has been displayed this app load */ var sleepWarningDisplayed = false; @@ -34,9 +31,7 @@ var sleepWarningDisplayed = false; function addRadioButtonListeners (changeFunction) { - var i; - - for (i = 0; i < sampleRadioButtons.length; i++) { + for (let i = 0; i < sampleRadioButtons.length; i++) { sampleRadioButtons[i].addEventListener('change', function () { @@ -73,26 +68,39 @@ function updateDutyCycleUI () { } +/* Run check on recording duration and amplitude threshold minimum trigger time */ + +function checkRecordingDuration () { + + if (dutyCheckBox.checked) { + + const duration = durationInput.getValue(recordingDurationInput); + uiFiltering.checkMinimumTriggerTime(duration); + + } + +} + /* Prepare UI */ -exports.prepareUI = function (changeFunction) { +exports.prepareUI = (changeFunction) => { recordingDurationInput.addEventListener('change', changeFunction); - sleepDurationInput.addEventListener('change', changeFunction); + recordingDurationInput.addEventListener('focusout', checkRecordingDuration); - sleepDurationInput.addEventListener('focusout', function () { + sleepDurationInput.addEventListener('change', changeFunction); - var buttonIndex; + sleepDurationInput.addEventListener('focusout', () => { if (!sleepWarningDisplayed && durationInput.getValue(sleepDurationInput) < 5) { sleepWarningDisplayed = true; - buttonIndex = dialog.showMessageBoxSync({ + const buttonIndex = dialog.showMessageBoxSync({ type: 'warning', buttons: ['Yes', 'No'], - title: 'Are you sure?', + title: 'Minimum sleep duration', message: 'In some circumstances, your AudioMoth may not be able to open and a close each file in less than 5 seconds. Are you sure you wish to continue?' }); @@ -106,7 +114,7 @@ exports.prepareUI = function (changeFunction) { }); - dutyCheckBox.addEventListener('change', function () { + dutyCheckBox.addEventListener('change', () => { updateDutyCycleUI(); changeFunction(); @@ -117,7 +125,8 @@ exports.prepareUI = function (changeFunction) { updateDutyCycleUI(); - uiFiltering.prepareUI(changeFunction); + uiFiltering.prepareUI(changeFunction, checkRecordingDuration); + uiAdvanced.prepareUI(changeFunction); durationInput.setValue(sleepDurationInput, 5); durationInput.setValue(recordingDurationInput, 55); @@ -130,29 +139,38 @@ function getSelectedRadioValue (radioName) { } -exports.getSettings = function () { +exports.getSettings = () => { - var settings = { + const settings = { sampleRateIndex: parseInt(getSelectedRadioValue('sample-rate-radio')), gain: parseInt(getSelectedRadioValue('gain-radio')), dutyEnabled: dutyCheckBox.checked, recordDuration: durationInput.getValue(recordingDurationInput), sleepDuration: durationInput.getValue(sleepDurationInput), passFiltersEnabled: uiFiltering.filteringIsEnabled(), - filterType: uiFiltering.getFilterType(), + filterTypeIndex: uiFiltering.getFilterType(), lowerFilter: uiFiltering.getLowerSliderValue(), higherFilter: uiFiltering.getHigherSliderValue(), amplitudeThresholdingEnabled: uiFiltering.amplitudeThresholdingIsEnabled(), - amplitudeThreshold: parseInt(uiFiltering.getAmplitudeThreshold()), - requireAcousticConfig: acousticConfigCheckBox.checked, - displayVoltageRange: voltageRangeCheckBox.checked + amplitudeThreshold: parseFloat(uiFiltering.getAmplitudeThreshold()), + requireAcousticConfig: uiAdvanced.isAcousticConfigRequired(), + displayVoltageRange: uiAdvanced.displayVoltageRange(), + minimumAmplitudeThresholdDuration: uiFiltering.getMinimumAmplitudeThresholdDuration(), + amplitudeThresholdingScaleIndex: uiFiltering.getAmplitudeThresholdScaleIndex(), + energySaverModeEnabled: uiAdvanced.isEnergySaverModeEnabled(), + disable48DCFilter: uiAdvanced.is48DCFilterDisabled() }; return settings; }; -exports.fillUI = function (settings) { +exports.get16BitAmplitudeThreshold = uiFiltering.get16BitAmplitudeThreshold; +exports.getPercentageAmplitudeThreshold = uiFiltering.getPercentageAmplitudeThreshold; +exports.getDecibelAmplitudeThreshold = uiFiltering.getDecibelAmplitudeThreshold; +exports.getPercentageAmplitudeThresholdExponentMantissa = uiFiltering.getPercentageAmplitudeThresholdExponentMantissa; + +exports.fillUI = (settings) => { sampleRadioButtons[settings.sampleRateIndex].checked = true; gainRadioButtons[settings.gain].checked = true; @@ -160,29 +178,19 @@ exports.fillUI = function (settings) { dutyCheckBox.checked = settings.dutyEnabled; updateDutyCycleUI(); - if (settings.dutyEnabled) { - - recordingDurationInput.value = settings.recordDuration; - sleepDurationInput.value = settings.sleepDuration; - - } else { - - recordingDurationInput.value = 86400; - sleepDurationInput.value = 0; - - } - uiFiltering.sampleRateChange(); uiFiltering.setFilters(settings.passFiltersEnabled, settings.lowerFilter, settings.higherFilter, settings.filterType); uiFiltering.updateFilterUI(); + uiFiltering.setAmplitudeThresholdScaleIndex(settings.amplitudeThresholdingScaleIndex); uiFiltering.setAmplitudeThreshold(settings.amplitudeThresholdingEnabled, settings.amplitudeThreshold); uiFiltering.updateAmplitudeThresholdingUI(); durationInput.setValue(sleepDurationInput, settings.sleepDuration); durationInput.setValue(recordingDurationInput, settings.recordDuration); - acousticConfigCheckBox.checked = settings.requireAcousticConfig; - voltageRangeCheckBox.checked = settings.displayVoltageRange; + uiAdvanced.fillUI(settings); + + uiFiltering.setMinimumAmplitudeThresholdDuration(settings.minimumAmplitudeThresholdDuration); }; diff --git a/timeHandler.js b/timeHandler.js index e9a65e3..3f9f2d2 100644 --- a/timeHandler.js +++ b/timeHandler.js @@ -6,15 +6,13 @@ function getConnections (periods) { - var i, j, connections; + const connections = []; - connections = []; - - for (i = 0; i < periods.length; i++) { + for (let i = 0; i < periods.length; i++) { connections[i] = -1; - for (j = 0; j < periods.length; j++) { + for (let j = 0; j < periods.length; j++) { if ((periods[i].startMins === 0 && periods[j].endMins === 1440) || (periods[i].endMins === 1440 && periods[j].startMins === 0)) { @@ -34,7 +32,7 @@ exports.getConnections = getConnections; function sortPeriods (periods) { - var sortedPeriods = periods.sort(function (a, b) { + const sortedPeriods = periods.sort(function (a, b) { return a.startMins - b.startMins; @@ -50,7 +48,7 @@ exports.sortPeriods = sortPeriods; function calculateTimezoneOffsetMins () { - var currentDate = new Date(); + const currentDate = new Date(); return (-1 * currentDate.getTimezoneOffset()); } @@ -67,11 +65,9 @@ exports.calculateTimezoneOffsetHours = calculateTimezoneOffsetHours; function convertTimeToLocal (time) { - var timezoneOffset; - /* Offset is given as UTC - local time in minutes */ - timezoneOffset = calculateTimezoneOffsetMins(); + const timezoneOffset = calculateTimezoneOffsetMins(); time = (time + timezoneOffset) % 1440; @@ -91,10 +87,8 @@ exports.convertTimeToLocal = convertTimeToLocal; function convertTimePeriodToLocal (timePeriod) { - var startMins, endMins; - - startMins = convertTimeToLocal(timePeriod.startMins); - endMins = convertTimeToLocal(timePeriod.endMins); + const startMins = convertTimeToLocal(timePeriod.startMins); + const endMins = convertTimeToLocal(timePeriod.endMins); return { startMins: startMins, @@ -107,11 +101,9 @@ function convertTimePeriodToLocal (timePeriod) { function checkTimePeriodsForSplits (localTimePeriods) { - var i, localTimePeriod; + for (let i = 0; i < localTimePeriods.length; i++) { - for (i = 0; i < localTimePeriods.length; i++) { - - localTimePeriod = localTimePeriods[i]; + const localTimePeriod = localTimePeriods[i]; if (localTimePeriod.startMins > localTimePeriod.endMins) { @@ -140,11 +132,9 @@ exports.checkTimePeriodsForSplits = checkTimePeriodsForSplits; function checkTimePeriodsForOverlaps (localTimePeriods) { - var i, j; - - for (i = 0; i < localTimePeriods.length; i++) { + for (let i = 0; i < localTimePeriods.length; i++) { - for (j = 0; j < localTimePeriods.length; j++) { + for (let j = 0; j < localTimePeriods.length; j++) { if (localTimePeriods[i].endMins === localTimePeriods[j].startMins) { @@ -169,14 +159,12 @@ exports.checkTimePeriodsForOverlaps = checkTimePeriodsForOverlaps; function convertTimePeriodsToLocal (tps) { - var localTimePeriods, i, timePeriod, localTimePeriod; + const localTimePeriods = []; - localTimePeriods = []; + for (let i = 0; i < tps.length; i++) { - for (i = 0; i < tps.length; i++) { - - timePeriod = tps[i]; - localTimePeriod = convertTimePeriodToLocal(timePeriod); + const timePeriod = tps[i]; + const localTimePeriod = convertTimePeriodToLocal(timePeriod); localTimePeriods.push({ startMins: localTimePeriod.startMins, @@ -195,11 +183,9 @@ exports.convertTimePeriodsToLocal = convertTimePeriodsToLocal; function convertTimetoUTC (time) { - var timezoneOffset; - /* Offset is given as UTC - local time in minutes */ - timezoneOffset = calculateTimezoneOffsetMins(); + const timezoneOffset = calculateTimezoneOffsetMins(); time = (time - timezoneOffset) % 1440; @@ -221,10 +207,8 @@ exports.convertTimetoUTC = convertTimetoUTC; function convertTimePeriodToUTC (timePeriod) { - var startMins, endMins; - - startMins = convertTimetoUTC(timePeriod.startMins); - endMins = convertTimetoUTC(timePeriod.endMins); + const startMins = convertTimetoUTC(timePeriod.startMins); + const endMins = convertTimetoUTC(timePeriod.endMins); /* If the start and end times are the same, the time period covers the entire day */ @@ -245,17 +229,15 @@ exports.convertTimePeriodToUTC = convertTimePeriodToUTC; /* Convert a list of local time periods to UTC */ -exports.convertLocalTimePeriodsToUTC = function (localTimePeriods) { +exports.convertLocalTimePeriodsToUTC = (localTimePeriods) => { - var utcTimePeriods, i, localTimePeriod, utcTimePeriod; + let utcTimePeriods = []; - utcTimePeriods = []; + for (let i = 0; i < localTimePeriods.length; i++) { - for (i = 0; i < localTimePeriods.length; i++) { + const localTimePeriod = localTimePeriods[i]; - localTimePeriod = localTimePeriods[i]; - - utcTimePeriod = convertTimePeriodToUTC(localTimePeriod); + const utcTimePeriod = convertTimePeriodToUTC(localTimePeriod); utcTimePeriods.push({ startMins: utcTimePeriod.startMins, @@ -278,18 +260,16 @@ exports.convertLocalTimePeriodsToUTC = function (localTimePeriods) { function getTimezoneText (localTime) { - var timezoneText, timezoneOffset, timezoneOffsetHours, timezoneOffsetMins; - - timezoneText = 'UTC'; + let timezoneText = 'UTC'; if (localTime) { /* Offset is given as UTC - local time */ - timezoneOffset = calculateTimezoneOffsetHours(); + const timezoneOffset = calculateTimezoneOffsetHours(); - timezoneOffsetHours = Math.floor(timezoneOffset); - timezoneOffsetMins = Math.abs(timezoneOffset - timezoneOffsetHours) * 60; + const timezoneOffsetHours = Math.floor(timezoneOffset); + const timezoneOffsetMins = Math.abs(timezoneOffset - timezoneOffsetHours) * 60; if (timezoneOffset !== 0) { @@ -329,7 +309,7 @@ function pad (n) { function minsToTimeString (mins) { - var timeHours = Math.floor(mins / 60); + const timeHours = Math.floor(mins / 60); return pad(timeHours) + ':' + pad((mins - (timeHours * 60))); diff --git a/ui.css b/ui.css index 769f0dd..cdbd293 100644 --- a/ui.css +++ b/ui.css @@ -4,6 +4,13 @@ input[type=number]::-webkit-outer-spin-button { margin: 0; } +*, *::after, *::before { + -webkit-user-select: none; + -webkit-user-drag: none; + -webkit-app-region: no-drag; + cursor: default; +} + .table>tbody>tr>td, .table>tbody>tr>th { border-top: none; diff --git a/ui.js b/ui.js index ad482cb..891a49d 100644 --- a/ui.js +++ b/ui.js @@ -20,11 +20,11 @@ const nightMode = require('./nightMode.js'); /* UI components */ -var applicationMenu = menu.getApplicationMenu(); +const applicationMenu = menu.getApplicationMenu(); -var timezoneLabel = document.getElementById('timezone-label'); +const timezoneLabel = document.getElementById('timezone-label'); -var timeDisplay = document.getElementById('time-display'); +const timeDisplay = document.getElementById('time-display'); var localTime = false; @@ -49,9 +49,7 @@ function setLocalTime (lTime) { function showTime () { - var timezoneOffset, strftimeUTC; - - timezoneOffset = 0; + let timezoneOffset = 0; if (isLocalTime()) { @@ -59,7 +57,7 @@ function showTime () { } - strftimeUTC = strftime.timezone(timezoneOffset); + const strftimeUTC = strftime.timezone(timezoneOffset); if (timeDisplay) { @@ -87,13 +85,13 @@ function update () { exports.update = update; -exports.updateDate = function (date) { +exports.updateDate = (date) => { deviceDate = date; }; -exports.disableTimeDisplay = function (blankValue) { +exports.disableTimeDisplay = (blankValue) => { if (timeDisplay) { @@ -111,9 +109,9 @@ exports.disableTimeDisplay = function (blankValue) { }; -exports.enableTimeDisplay = function () { +exports.enableTimeDisplay = () => { - var textColor; + let textColor; if (timeDisplay) { @@ -138,9 +136,7 @@ exports.enableTimeDisplay = function () { function setTimezoneStatus (local) { - var timezoneText; - - timezoneText = 'UTC'; + let timezoneText = 'UTC'; setLocalTime(local); @@ -194,9 +190,7 @@ exports.isNightMode = nightMode.isEnabled; function checkUtcToggleability () { - var timezoneOffset; - - timezoneOffset = timeHandler.calculateTimezoneOffsetMins(); + const timezoneOffset = timeHandler.calculateTimezoneOffsetMins(); if (timezoneOffset === 0) { diff --git a/uiIndex.js b/uiIndex.js index b9fd4f5..2b13024 100644 --- a/uiIndex.js +++ b/uiIndex.js @@ -28,34 +28,37 @@ const constants = require('./constants.js'); const uiSchedule = require('./schedule/uiSchedule.js'); const uiSettings = require('./settings/uiSettings.js'); -const uiFiltering = require('./settings/uiFiltering.js'); const versionChecker = require('./versionChecker.js'); -const UINT16_MAX = 0xFFFF; const UINT32_MAX = 0xFFFFFFFF; +const UINT16_MAX = 0xFFFF; const SECONDS_IN_DAY = 86400; +const AMPLITUDE_THRESHOLD_SCALE_PERCENTAGE = 0; +const AMPLITUDE_THRESHOLD_SCALE_16BIT = 1; +const AMPLITUDE_THRESHOLD_SCALE_DECIBEL = 2; + /* UI components */ -var applicationMenu = Menu.getApplicationMenu(); +const applicationMenu = Menu.getApplicationMenu(); -var idDisplay = document.getElementById('id-display'); -var idLabel = document.getElementById('id-label'); +const idDisplay = document.getElementById('id-display'); +const idLabel = document.getElementById('id-label'); -var firmwareVersionDisplay = document.getElementById('firmware-version-display'); -var firmwareVersionLabel = document.getElementById('firmware-version-label'); -var firmwareDescriptionDisplay = document.getElementById('firmware-description-display'); -var firmwareDescriptionLabel = document.getElementById('firmware-description-label'); +const firmwareVersionDisplay = document.getElementById('firmware-version-display'); +const firmwareVersionLabel = document.getElementById('firmware-version-label'); +const firmwareDescriptionDisplay = document.getElementById('firmware-description-display'); +const firmwareDescriptionLabel = document.getElementById('firmware-description-label'); -var batteryDisplay = document.getElementById('battery-display'); -var batteryLabel = document.getElementById('battery-label'); +const batteryDisplay = document.getElementById('battery-display'); +const batteryLabel = document.getElementById('battery-label'); -var ledCheckbox = document.getElementById('led-checkbox'); -var lowVoltageCutoffCheckbox = document.getElementById('low-voltage-cutoff-checkbox'); -var batteryLevelCheckbox = document.getElementById('battery-level-checkbox'); +const ledCheckbox = document.getElementById('led-checkbox'); +const lowVoltageCutoffCheckbox = document.getElementById('low-voltage-cutoff-checkbox'); +const batteryLevelCheckbox = document.getElementById('battery-level-checkbox'); -var configureButton = document.getElementById('configure-button'); +const configureButton = document.getElementById('configure-button'); /* Store version number for packet size checks and description for compatibility check */ @@ -82,12 +85,10 @@ var communicating = false; function isOlderSemanticVersion (aVersion, bVersion) { - var aVersionNum, bVersionNum; - for (let i = 0; i < aVersion.length; i++) { - aVersionNum = aVersion[i]; - bVersionNum = bVersion[i]; + const aVersionNum = aVersion[i]; + const bVersionNum = bVersion[i]; if (aVersionNum > bVersionNum) { @@ -105,80 +106,175 @@ function isOlderSemanticVersion (aVersion, bVersion) { } -/* Request, receive and handle AudioMoth information packet */ +/* Request, receive and handle packet containing battery level and complete request chain by using values */ -function getAudioMothPacket () { +function requestBatteryState () { - var firmwareVersionArr; + audiomoth.getBatteryState(function (err, battery) { - if (communicating) { + if (err) { - return; + console.error(err); + disableDisplay(); - } + } else if (battery === null) { - audiomoth.getPacket(function (err, packet) { + disableDisplay(); - if (err || packet === null) { + } else { - date = null; - id = null; - batteryState = null; - firmwareVersion = '0.0.0'; - firmwareDescription = '-'; + batteryState = battery; - } else { + usePacketValues(); + + } - date = audiomoth.convertFourBytesFromBufferToDate(packet, 1); + }); - id = audiomoth.convertEightBytesFromBufferToID(packet, 1 + 4); +} - /* If a new device is connected, allow warnings */ +/* Request, receive and handle packet containing the current firmware version and check the version/description to see if a warning message should be shown */ - if (id !== previousId) { +function requestFirmwareVersion () { - previousId = id; + audiomoth.getFirmwareVersion(function (err, versionArr) { - versionWarningShown = false; + if (err) { - } + console.error(err); + disableDisplay(); - batteryState = audiomoth.convertOneByteFromBufferToBatteryState(packet, 1 + 4 + 8); + } else if (versionArr === null) { - firmwareVersionArr = audiomoth.convertThreeBytesFromBufferToFirmwareVersion(packet, 1 + 4 + 8 + 1); - firmwareVersion = firmwareVersionArr[0] + '.' + firmwareVersionArr[1] + '.' + firmwareVersionArr[2]; + disableDisplay(); - firmwareDescription = audiomoth.convertBytesFromBufferToFirmwareDescription(packet, 1 + 4 + 8 + 1 + 3); + } else { - if (!versionWarningShown && !constants.supportedFirmwareDescs.includes(firmwareDescription)) { + firmwareVersion = versionArr[0] + '.' + versionArr[1] + '.' + versionArr[2]; - versionWarningShown = true; + if (!versionWarningShown) { - dialog.showMessageBox(BrowserWindow.getFocusedWindow(), { - type: 'warning', - title: 'Unsupported firmware', - message: 'The firmware installed on your AudioMoth may not be supported by this version of the AudioMoth Configuration App.' - }); + if (!constants.isSupportedFirmwareDescription(firmwareDescription)) { - } else { + versionWarningShown = true; + + dialog.showMessageBoxSync(BrowserWindow.getFocusedWindow(), { + type: 'warning', + title: 'Unsupported firmware', + message: 'The firmware installed on your AudioMoth may not be supported by this version of the AudioMoth Configuration App.' + }); - if (!versionWarningShown && isOlderSemanticVersion(firmwareVersionArr, [1, 5, 0])) { + } else if (isOlderSemanticVersion(versionArr, constants.latestFirmwareVersionArray)) { versionWarningShown = true; - dialog.showMessageBox(BrowserWindow.getFocusedWindow(), { + dialog.showMessageBoxSync(BrowserWindow.getFocusedWindow(), { type: 'warning', title: 'Firmware update recommended', - message: 'Update to at least version 1.5.0 of AudioMoth-Firmware-Basic to use all the features of this version of the AudioMoth Configuration App.' + message: 'Update to at least version ' + constants.latestFirmwareVersionString + ' of AudioMoth-Firmware-Basic to use all the features of this version of the AudioMoth Configuration App.' }); } } + requestBatteryState(); + + } + + }); + +} + +/* Request, receive and handle the packet containing the description of the current firmware */ + +function requestFirmwareDescription () { + + audiomoth.getFirmwareDescription(function (err, description) { + + if (err || description === null || description === '') { + + if (err) { + + console.error(err); + + } + + disableDisplay(); + + } else { + + firmwareDescription = description; + + requestFirmwareVersion(); + + } + + }); + +} + +/* Request, receive and handle the packet containing the ID of the current device */ + +function requestID () { + + audiomoth.getID(function (err, deviceId) { + + if (err || deviceId === null) { + + if (err) { + + console.error(err); + + } + + disableDisplay(); + + } else { + + id = deviceId; + + /* If a new device is connected, allow warnings */ + + if (id !== previousId) { + + previousId = id; + + versionWarningShown = false; + + } + + requestFirmwareDescription(); + } - usePacketValues(); + }); + +} + +/* Request, receive and handle AudioMoth information packet */ + +function getAudioMothPacket () { + + if (communicating) { + + return; + + } + + audiomoth.getTime(function (err, currentDate) { + + if (err || currentDate === null) { + + date = null; + + } else { + + date = currentDate; + + } + + requestID(); setTimeout(getAudioMothPacket, 200); @@ -222,9 +318,7 @@ function usePacketValues () { function writeLittleEndianBytes (buffer, start, byteCount, value) { - var i; - - for (i = 0; i < byteCount; i++) { + for (let i = 0; i < byteCount; i++) { buffer[start + i] = (value >> (i * 8)) & 255; @@ -238,15 +332,13 @@ function sendPacket (packet) { audiomoth.setPacket(packet, function (err, data) { - var k, j, matches, packetLength, showError, possibleFirmwareVersion; - - showError = function () { + const showError = () => { dialog.showMessageBox(BrowserWindow.getFocusedWindow(), { type: 'error', title: 'Configuration failed', message: 'The connected AudioMoth did not respond correctly and the configuration may not have been applied. Please try again.' - }); + }); configureButton.style.color = ''; @@ -258,16 +350,16 @@ function sendPacket (packet) { } else { - matches = true; + let matches = true; /* Check if the firmware version of the device being configured has a known packet length */ /* If not, the length of the packet sent/received is used */ - packetLength = Math.min(packet.length, data.length - 1); + let packetLength = Math.min(packet.length, data.length - 1); - for (k = 0; k < constants.packetLengthVersions.length; k++) { + for (let k = 0; k < constants.packetLengthVersions.length; k++) { - possibleFirmwareVersion = constants.packetLengthVersions[k].firmwareVersion; + const possibleFirmwareVersion = constants.packetLengthVersions[k].firmwareVersion; if (isOlderSemanticVersion(firmwareVersion.split('.'), possibleFirmwareVersion.split('.'))) { @@ -282,7 +374,7 @@ function sendPacket (packet) { /* Verify the packet sent was read correctly by the device by comparing it to the returned packet */ - for (j = 0; j < packetLength; j++) { + for (let j = 0; j < packetLength; j++) { if (packet[j] !== data[j + 1]) { @@ -309,18 +401,16 @@ function sendPacket (packet) { function configureDevice () { - var i, index, delay, sendTime, maxPacketLength, packet, configurations, sampleRateConfiguration, timePeriods, firstRecordingDateTimestamp, lastRecordingDateTimestamp, lowerFilter, higherFilter, firstRecordingDate, lastRecordingDate, settings, amplitudeThreshold, today, dayDiff, firstRecordingDateText, lastRecordingDateText, earliestRecordingTime, latestRecordingTime, now, sendTimeDiff, USB_LAG, MINIMUM_DELAY, MILLISECONDS_IN_SECOND; - communicating = true; ui.disableTimeDisplay(false); configureButton.disabled = true; - USB_LAG = 20; + const USB_LAG = 20; - MINIMUM_DELAY = 100; + const MINIMUM_DELAY = 100; - MILLISECONDS_IN_SECOND = 1000; + const MILLISECONDS_IN_SECOND = 1000; setTimeout(function () { @@ -328,29 +418,27 @@ function configureDevice () { getAudioMothPacket(); - configureButton.disabled = false; - }, 1500); console.log('Configuring device'); - settings = uiSettings.getSettings(); + const settings = uiSettings.getSettings(); /* Build configuration packet */ - index = 0; + let index = 0; /* Packet length is only increased with updates, so take the size of the latest firmware version packet */ - maxPacketLength = constants.packetLengthVersions.slice(-1)[0].packetLength; + const maxPacketLength = constants.packetLengthVersions.slice(-1)[0].packetLength; - packet = new Uint8Array(maxPacketLength); + const packet = new Uint8Array(maxPacketLength); /* Increment to next second transition */ - sendTime = new Date(); + const sendTime = new Date(); - delay = MILLISECONDS_IN_SECOND - sendTime.getMilliseconds() - USB_LAG; + let delay = MILLISECONDS_IN_SECOND - sendTime.getMilliseconds() - USB_LAG; if (delay < MINIMUM_DELAY) delay += MILLISECONDS_IN_SECOND; @@ -363,9 +451,9 @@ function configureDevice () { packet[index++] = settings.gain; - configurations = (isOlderSemanticVersion(firmwareVersion.split('.'), ['1', '4', '4']) && settings.sampleRateIndex < 3) ? constants.oldConfigurations : constants.configurations; + const configurations = (isOlderSemanticVersion(firmwareVersion.split('.'), ['1', '4', '4']) && settings.sampleRateIndex < 3) ? constants.oldConfigurations : constants.configurations; - sampleRateConfiguration = configurations[settings.sampleRateIndex]; + const sampleRateConfiguration = configurations[settings.sampleRateIndex]; packet[index++] = sampleRateConfiguration.clockDivider; @@ -386,7 +474,7 @@ function configureDevice () { packet[index++] = ledCheckbox.checked ? 1 : 0; - timePeriods = JSON.parse(JSON.stringify(scheduleBar.getTimePeriods())); + let timePeriods = JSON.parse(JSON.stringify(scheduleBar.getTimePeriods())); timePeriods = timePeriods.sort(function (a, b) { @@ -396,7 +484,7 @@ function configureDevice () { packet[index++] = timePeriods.length; - for (i = 0; i < timePeriods.length; i++) { + for (let i = 0; i < timePeriods.length; i++) { writeLittleEndianBytes(packet, index, 2, timePeriods[i].startMins); index += 2; @@ -406,7 +494,7 @@ function configureDevice () { } - for (i = 0; i < (scheduleBar.MAX_PERIODS + 1) - timePeriods.length; i++) { + for (let i = 0; i < (scheduleBar.MAX_PERIODS + 1) - timePeriods.length; i++) { writeLittleEndianBytes(packet, index, 2, 0); index += 2; @@ -432,13 +520,15 @@ function configureDevice () { /* Start/stop dates */ - today = new Date(); - dayDiff = today.getDate() - today.getUTCDate(); + const today = new Date(); + const dayDiff = today.getDate() - today.getUTCDate(); + + const timezoneOffset = -60 * today.getTimezoneOffset(); - var timezoneOffset = -60 * today.getTimezoneOffset(); + const firstRecordingDateText = uiSchedule.getFirstRecordingDate(); + const firstRecordingDate = new Date(uiSchedule.getFirstRecordingDate()); - firstRecordingDateText = uiSchedule.getFirstRecordingDate(); - firstRecordingDate = new Date(uiSchedule.getFirstRecordingDate()); + let earliestRecordingTime; if (firstRecordingDateText === '') { @@ -456,14 +546,14 @@ function configureDevice () { } - firstRecordingDateTimestamp = (firstRecordingDate === -1) ? 0 : new Date(firstRecordingDate).valueOf() / 1000; - - earliestRecordingTime = firstRecordingDateTimestamp; + earliestRecordingTime = (firstRecordingDate === -1) ? 0 : new Date(firstRecordingDate).valueOf() / 1000; } - lastRecordingDateText = uiSchedule.getLastRecordingDate(); - lastRecordingDate = new Date(uiSchedule.getLastRecordingDate()); + const lastRecordingDateText = uiSchedule.getLastRecordingDate(); + const lastRecordingDate = new Date(uiSchedule.getLastRecordingDate()); + + let latestRecordingTime; if (lastRecordingDateText === '') { @@ -479,7 +569,7 @@ function configureDevice () { } - lastRecordingDateTimestamp = (lastRecordingDate === -1) ? 0 : new Date(lastRecordingDate).valueOf() / 1000; + const lastRecordingDateTimestamp = (lastRecordingDate === -1) ? 0 : new Date(lastRecordingDate).valueOf() / 1000; /* Make latestRecordingTime timestamp inclusive by setting it to the end of the chosen day */ latestRecordingTime = lastRecordingDateTimestamp + SECONDS_IN_DAY; @@ -497,23 +587,28 @@ function configureDevice () { writeLittleEndianBytes(packet, index, 4, latestRecordingTime); index += 4; + let lowerFilter, higherFilter; + /* Filter settings */ if (settings.passFiltersEnabled) { - switch (settings.filterType) { + switch (settings.filterTypeIndex) { - case uiFiltering.FILTER_LOW: + case 0: + /* Low-pass */ lowerFilter = UINT16_MAX; higherFilter = settings.higherFilter / 100; break; - case uiFiltering.FILTER_HIGH: + case 1: + /* Band-pass */ lowerFilter = settings.lowerFilter / 100; - higherFilter = UINT16_MAX; + higherFilter = settings.higherFilter / 100; break; - case uiFiltering.FILTER_BAND: + case 2: + /* High-pass */ lowerFilter = settings.lowerFilter / 100; - higherFilter = settings.higherFilter / 100; + higherFilter = UINT16_MAX; break; } @@ -530,24 +625,147 @@ function configureDevice () { writeLittleEndianBytes(packet, index, 2, higherFilter); index += 2; - /* CMV settings */ + /* Amplitude threshold */ + + let amplitudeThreshold; + + const amplitudeThresholdingScaleIndex = settings.amplitudeThresholdingScaleIndex; + + if (settings.amplitudeThresholdingEnabled) { - amplitudeThreshold = settings.amplitudeThreshold; + let percentageAmplitudeThreshold; - writeLittleEndianBytes(packet, index, 2, settings.amplitudeThresholdingEnabled ? amplitudeThreshold : 0); + /* Amplitude threshold value is based on the value displayed to the user, rather than the raw position on the slider */ + /* E.g. 10% selected, threshold = 10% of the max amplitude */ + + switch (amplitudeThresholdingScaleIndex) { + + case AMPLITUDE_THRESHOLD_SCALE_16BIT: + amplitudeThreshold = uiSettings.get16BitAmplitudeThreshold(); + break; + + case AMPLITUDE_THRESHOLD_SCALE_PERCENTAGE: + percentageAmplitudeThreshold = uiSettings.getPercentageAmplitudeThresholdExponentMantissa(); + amplitudeThreshold = Math.round(32768 * percentageAmplitudeThreshold.mantissa * Math.pow(10, percentageAmplitudeThreshold.exponent) / 100); + break; + + case AMPLITUDE_THRESHOLD_SCALE_DECIBEL: + amplitudeThreshold = Math.round(32768 * Math.pow(10, uiSettings.getDecibelAmplitudeThreshold() / 20)); + break; + + } + + } else { + + amplitudeThreshold = 0; + + } + + writeLittleEndianBytes(packet, index, 2, amplitudeThreshold); index += 2; /* Pack values into a single byte */ /* Whether or not deployment ID is required */ - var requireAcousticConfig = settings.requireAcousticConfig ? 1 : 0; + const requireAcousticConfig = settings.requireAcousticConfig ? 1 : 0; /* Whether to use NiMH/LiPo voltage range for battery level indication */ - var displayVoltageRange = settings.displayVoltageRange ? 1 : 0; + const displayVoltageRange = settings.displayVoltageRange ? 1 : 0; - var packetValue = (displayVoltageRange << 1) + requireAcousticConfig; + /* Minimum amplitude threshold duration, voltage range and whether acoustic configuration is required before deployment */ + + let minimumAmplitudeThresholdDuration; + + if (settings.amplitudeThresholdingEnabled) { + + const minimumAmplitudeThresholdDurations = [0, 1, 2, 5, 10, 15, 30, 60]; + minimumAmplitudeThresholdDuration = minimumAmplitudeThresholdDurations[settings.minimumAmplitudeThresholdDuration]; + + } else { + + minimumAmplitudeThresholdDuration = 0; + + } + + let packedValue0 = requireAcousticConfig; + packedValue0 |= (displayVoltageRange << 1); + packedValue0 |= (minimumAmplitudeThresholdDuration << 2); + + packet[index++] = packedValue0; + + let enableAmplitudeThresholdDecibelScale, enableAmplitudeThresholdPercentageScale; + + if (settings.amplitudeThresholdingEnabled) { + + switch (amplitudeThresholdingScaleIndex) { + + case AMPLITUDE_THRESHOLD_SCALE_16BIT: + enableAmplitudeThresholdDecibelScale = 1; + enableAmplitudeThresholdPercentageScale = 1; + break; + + case AMPLITUDE_THRESHOLD_SCALE_PERCENTAGE: + enableAmplitudeThresholdDecibelScale = 0; + enableAmplitudeThresholdPercentageScale = 1; + break; + + case AMPLITUDE_THRESHOLD_SCALE_DECIBEL: + enableAmplitudeThresholdDecibelScale = 1; + enableAmplitudeThresholdPercentageScale = 0; + break; + + } + + /* Decibel-scale amplitude threshold */ + + const amplitudeThresholdDecibels = (amplitudeThresholdingScaleIndex === AMPLITUDE_THRESHOLD_SCALE_DECIBEL) ? Math.abs(uiSettings.getDecibelAmplitudeThreshold()) : 0; + + let packedValue1 = enableAmplitudeThresholdDecibelScale & 1; + packedValue1 |= (amplitudeThresholdDecibels << 1); + + packet[index++] = packedValue1; + + /* Percentage-scale amplitude threshold */ + + let amplitudeThresholdPercentageExponent, amplitudeThresholdPercentageMantissa; + + if (amplitudeThresholdingScaleIndex === AMPLITUDE_THRESHOLD_SCALE_PERCENTAGE) { + + const percentageAmplitudeThreshold = uiSettings.getPercentageAmplitudeThresholdExponentMantissa(); + + amplitudeThresholdPercentageExponent = percentageAmplitudeThreshold.exponent; + amplitudeThresholdPercentageMantissa = percentageAmplitudeThreshold.mantissa; + + } else { + + amplitudeThresholdPercentageExponent = 0; + amplitudeThresholdPercentageMantissa = 0; + + } + + let packedValue2 = enableAmplitudeThresholdPercentageScale & 1; + packedValue2 |= (amplitudeThresholdPercentageMantissa << 1); + packedValue2 |= (amplitudeThresholdPercentageExponent << 5); + + packet[index++] = packedValue2; + + } else { + + packet[index++] = 0; + packet[index++] = 0; + + } + + /* Whether to use NiMH/LiPo voltage range for battery level indication */ + const energySaverModeEnabled = settings.energySaverModeEnabled ? 1 : 0; - packet[index++] = packetValue; + /* Whether to turn off the 48Hz DC blocking filter which is on by default */ + const disable48DCFilter = settings.disable48DCFilter ? 1 : 0; + + let packedByte3 = energySaverModeEnabled & 1; + packedByte3 |= (disable48DCFilter << 1); + + packet[index++] = packedByte3; console.log('Packet length: ', index); @@ -558,8 +776,8 @@ function configureDevice () { packetReader.read(packet); - now = new Date(); - sendTimeDiff = sendTime.getTime() - now.getTime(); + const now = new Date(); + const sendTimeDiff = sendTime.getTime() - now.getTime(); if (sendTimeDiff <= 0) { @@ -601,22 +819,20 @@ function initialiseDisplay () { function disableDisplay () { + ui.updateDate(date); + updateIdDisplay(id); + updateFirmwareDisplay(firmwareVersion, firmwareDescription); + updateBatteryDisplay(batteryState); + ui.disableTimeDisplay(false); idLabel.style.color = 'lightgrey'; - idDisplay.style.color = 'lightgrey'; - firmwareVersionLabel.style.color = 'lightgrey'; - firmwareVersionDisplay.style.color = 'lightgrey'; - firmwareDescriptionLabel.style.color = 'lightgrey'; - firmwareDescriptionDisplay.style.color = 'lightgrey'; - batteryLabel.style.color = 'lightgrey'; - batteryDisplay.style.color = 'lightgrey'; configureButton.disabled = true; @@ -627,17 +843,7 @@ function disableDisplay () { function enableDisplay () { - var textColor; - - if (ui.isNightMode()) { - - textColor = 'white'; - - } else { - - textColor = 'black'; - - } + const textColor = ui.isNightMode() ? 'white' : 'black'; idLabel.style.color = textColor; @@ -665,11 +871,19 @@ function enableDisplay () { /* Insert retrieved values into device information display */ -function updateIdDisplay (id) { +function updateIdDisplay (deviceId) { + + if (deviceId !== idDisplay.textContent) { + + if (deviceId === null) { + + idDisplay.textContent = '-'; + + } else { - if (id !== idDisplay.textContent) { + idDisplay.textContent = deviceId; - idDisplay.textContent = id; + } } @@ -679,9 +893,9 @@ function updateFirmwareDisplay (version, description) { if (version !== firmwareVersionDisplay.value) { - if (version === '0.0.0' && constants.supportedFirmwareDescs.includes(firmwareDescription)) { + if (version === '0.0.0') { - firmwareVersionDisplay.textContent = '< 1.2.0'; + firmwareVersionDisplay.textContent = '-'; } else { @@ -693,21 +907,37 @@ function updateFirmwareDisplay (version, description) { if (description !== firmwareDescriptionDisplay.textContent) { - firmwareDescriptionDisplay.textContent = description; + if (description === '') { + + firmwareDescriptionDisplay.textContent = '-'; + + } else { + + firmwareDescriptionDisplay.textContent = description; + + } } }; -function updateBatteryDisplay (batteryState) { +function updateBatteryDisplay (battery) { - batteryDisplay.textContent = batteryState; + if (battery === null) { + + batteryDisplay.textContent = '-'; + + } else { + + batteryDisplay.textContent = battery; + + } }; function copyDeviceID () { - var id = idDisplay.textContent; + const id = idDisplay.textContent; if (id !== '0000000000000000') { @@ -742,18 +972,16 @@ function toggleNightMode () { function updateLifeDisplayOnChange () { - var sortedPeriods, settings; - - sortedPeriods = JSON.parse(JSON.stringify(scheduleBar.getTimePeriods())); + let sortedPeriods = JSON.parse(JSON.stringify(scheduleBar.getTimePeriods())); sortedPeriods = sortedPeriods.sort(function (a, b) { return a.startMins - b.startMins; }); - settings = uiSettings.getSettings(); + const settings = uiSettings.getSettings(); - lifeDisplay.updateLifeDisplay(sortedPeriods, constants.configurations[settings.sampleRateIndex], settings.recordDuration, settings.sleepDuration, settings.amplitudeThresholdingEnabled, settings.dutyEnabled); + lifeDisplay.updateLifeDisplay(sortedPeriods, constants.configurations[settings.sampleRateIndex], settings.recordDuration, settings.sleepDuration, settings.amplitudeThresholdingEnabled, settings.dutyEnabled, settings.energySaverModeEnabled); } @@ -767,39 +995,46 @@ lifeDisplay.getPanel().addEventListener('click', function () { electron.ipcRenderer.on('save', function () { - var timePeriods, localTime, ledEnabled, lowVoltageCutoffEnabled, batteryLevelCheckEnabled, sampleRateIndex, gain, recordDuration, sleepDuration, dutyEnabled, passFiltersEnabled, filterType, lowerFilter, higherFilter, amplitudeThresholdingEnabled, amplitudeThreshold, firstRecordingDate, lastRecordingDate, requireAcousticConfig, displayVoltageRange, settings; + const timePeriods = scheduleBar.getTimePeriods(); + + const localTime = ui.isLocalTime(); - timePeriods = scheduleBar.getTimePeriods(); + const ledEnabled = ledCheckbox.checked; + const lowVoltageCutoffEnabled = lowVoltageCutoffCheckbox.checked; + const batteryLevelCheckEnabled = batteryLevelCheckbox.checked; - localTime = ui.isLocalTime(); + const settings = uiSettings.getSettings(); - ledEnabled = ledCheckbox.checked; - lowVoltageCutoffEnabled = lowVoltageCutoffCheckbox.checked; - batteryLevelCheckEnabled = batteryLevelCheckbox.checked; + const sampleRateIndex = settings.sampleRateIndex; + const gain = settings.gain; + const recordDuration = settings.recordDuration; + const sleepDuration = settings.sleepDuration; + const dutyEnabled = settings.dutyEnabled; - settings = uiSettings.getSettings(); + const passFiltersEnabled = settings.passFiltersEnabled; + const filterTypeIndex = settings.filterTypeIndex; + const lowerFilter = settings.lowerFilter; + const higherFilter = settings.higherFilter; - sampleRateIndex = settings.sampleRateIndex; - gain = settings.gain; - recordDuration = settings.recordDuration; - sleepDuration = settings.sleepDuration; - dutyEnabled = settings.dutyEnabled; + const amplitudeThresholdingEnabled = settings.amplitudeThresholdingEnabled; + const amplitudeThreshold = settings.amplitudeThreshold; - passFiltersEnabled = settings.passFiltersEnabled; - filterType = settings.filterType; - lowerFilter = settings.lowerFilter; - higherFilter = settings.higherFilter; - amplitudeThresholdingEnabled = settings.amplitudeThresholdingEnabled; - amplitudeThreshold = settings.amplitudeThreshold; + const firstRecordingDate = uiSchedule.getFirstRecordingDate(); + const lastRecordingDate = uiSchedule.getLastRecordingDate(); - firstRecordingDate = uiSchedule.getFirstRecordingDate(); - lastRecordingDate = uiSchedule.getLastRecordingDate(); + const requireAcousticConfig = settings.requireAcousticConfig; - requireAcousticConfig = settings.requireAcousticConfig; + const displayVoltageRange = settings.displayVoltageRange; - displayVoltageRange = settings.displayVoltageRange; + const minimumAmplitudeThresholdDuration = settings.minimumAmplitudeThresholdDuration; - saveLoad.saveConfiguration(timePeriods, ledEnabled, lowVoltageCutoffEnabled, batteryLevelCheckEnabled, sampleRateIndex, gain, recordDuration, sleepDuration, localTime, firstRecordingDate, lastRecordingDate, dutyEnabled, passFiltersEnabled, filterType, lowerFilter, higherFilter, amplitudeThresholdingEnabled, amplitudeThreshold, requireAcousticConfig, displayVoltageRange, function (err) { + const amplitudeThresholdingScaleIndex = settings.amplitudeThresholdingScaleIndex; + + const energySaverModeEnabled = settings.energySaverModeEnabled; + + const disable48DCFilter = settings.disable48DCFilter; + + saveLoad.saveConfiguration(timePeriods, ledEnabled, lowVoltageCutoffEnabled, batteryLevelCheckEnabled, sampleRateIndex, gain, recordDuration, sleepDuration, localTime, firstRecordingDate, lastRecordingDate, dutyEnabled, passFiltersEnabled, filterTypeIndex, lowerFilter, higherFilter, amplitudeThresholdingEnabled, amplitudeThreshold, requireAcousticConfig, displayVoltageRange, minimumAmplitudeThresholdDuration, amplitudeThresholdingScaleIndex, energySaverModeEnabled, disable48DCFilter, function (err) { if (err) { @@ -817,11 +1052,9 @@ electron.ipcRenderer.on('save', function () { electron.ipcRenderer.on('load', function () { - saveLoad.loadConfiguration(function (timePeriods, ledEnabled, lowVoltageCutoffEnabled, batteryLevelCheckEnabled, sampleRateIndex, gain, dutyEnabled, recordDuration, sleepDuration, localTime, start, end, passFiltersEnabled, filterType, lowerFilter, higherFilter, amplitudeThresholdingEnabled, amplitudeThreshold, requireAcousticConfig, displayVoltageRange) { - - var sortedPeriods, settings; + saveLoad.loadConfiguration(function (timePeriods, ledEnabled, lowVoltageCutoffEnabled, batteryLevelCheckEnabled, sampleRateIndex, gain, dutyEnabled, recordDuration, sleepDuration, localTime, start, end, passFiltersEnabled, filterType, lowerFilter, higherFilter, amplitudeThresholdingEnabled, amplitudeThreshold, requireAcousticConfig, displayVoltageRange, minimumAmplitudeThresholdDuration, amplitudeThresholdingScaleIndex, energySaverModeEnabled, disable48DCFilter) { - sortedPeriods = timePeriods; + let sortedPeriods = timePeriods; sortedPeriods = sortedPeriods.sort(function (a, b) { return a.startMins - b.startMins; @@ -836,7 +1069,7 @@ electron.ipcRenderer.on('load', function () { uiSchedule.setFirstRecordingDate(start); uiSchedule.setLastRecordingDate(end); - settings = { + const settings = { sampleRateIndex: sampleRateIndex, gain: gain, dutyEnabled: dutyEnabled, @@ -849,7 +1082,11 @@ electron.ipcRenderer.on('load', function () { amplitudeThresholdingEnabled: amplitudeThresholdingEnabled, amplitudeThreshold: amplitudeThreshold, requireAcousticConfig: requireAcousticConfig, - displayVoltageRange: displayVoltageRange + displayVoltageRange: displayVoltageRange, + minimumAmplitudeThresholdDuration: minimumAmplitudeThresholdDuration, + amplitudeThresholdingScaleIndex: amplitudeThresholdingScaleIndex, + energySaverModeEnabled: energySaverModeEnabled, + disable48DCFilter: disable48DCFilter }; uiSettings.fillUI(settings); @@ -860,7 +1097,7 @@ electron.ipcRenderer.on('load', function () { ui.update(); - lifeDisplay.updateLifeDisplay(sortedPeriods, constants.configurations[sampleRateIndex], recordDuration, sleepDuration, amplitudeThresholdingEnabled, dutyEnabled); + updateLifeDisplayOnChange(); }); @@ -870,8 +1107,6 @@ electron.ipcRenderer.on('update-check', function () { versionChecker.checkLatestRelease(function (response) { - var buttonIndex; - if (response.error) { console.error(response.error); @@ -899,10 +1134,10 @@ electron.ipcRenderer.on('update-check', function () { } - buttonIndex = dialog.showMessageBoxSync({ + const buttonIndex = dialog.showMessageBoxSync({ type: 'warning', buttons: ['Yes', 'No'], - title: 'Are you sure?', + title: 'Download newer version', message: 'A newer version of this app is available (' + response.latestVersion + '), would you like to download it?' }); @@ -931,7 +1166,31 @@ electron.ipcRenderer.on('night-mode', toggleNightMode); electron.ipcRenderer.on('local-time', toggleTimezoneStatus); -configureButton.addEventListener('click', configureDevice); +configureButton.addEventListener('click', () => { + + const timePeriods = scheduleBar.getTimePeriods(); + + if (timePeriods.length === 0) { + + const buttonIndex = dialog.showMessageBoxSync({ + type: 'warning', + buttons: ['Yes', 'No'], + title: 'No recording periods', + message: 'No recording periods have been scheduled. This means the AudioMoth will not record when in CUSTOM mode. Are you sure you wish to apply this configuration?' + }); + + if (buttonIndex === 1) { + + console.log('Configuration cancelled'); + return; + + } + + } + + configureDevice(); + +}); ui.checkUtcToggleability(); diff --git a/uiNight.css b/uiNight.css index 2e54191..46922b1 100644 --- a/uiNight.css +++ b/uiNight.css @@ -4,6 +4,13 @@ input[type=number]::-webkit-outer-spin-button { margin: 0; } +*, *::after, *::before { + -webkit-user-select: none; + -webkit-user-drag: none; + -webkit-app-region: no-drag; + cursor: default; +} + .table>tbody>tr>td, .table>tbody>tr>th { border-top: none; diff --git a/versionChecker.js b/versionChecker.js index 016d6d8..fa0be0c 100644 --- a/versionChecker.js +++ b/versionChecker.js @@ -10,18 +10,16 @@ const electron = require('electron'); -var pjson = require('./package.json'); +const pjson = require('./package.json'); /* Compare two semantic versions and return true if older */ function isOlderSemanticVersion (aVersion, bVersion) { - var aVersionNum, bVersionNum; - for (let i = 0; i < aVersion.length; i++) { - aVersionNum = aVersion[i]; - bVersionNum = bVersion[i]; + const aVersionNum = aVersion[i]; + const bVersionNum = bVersion[i]; if (aVersionNum > bVersionNum) { @@ -41,9 +39,7 @@ function isOlderSemanticVersion (aVersion, bVersion) { /* Check current app version in package.json against latest version in repository's releases */ -exports.checkLatestRelease = function (callback) { - - var version, repoGitURL, repoURL, xmlHttp, responseJson, latestVersion, updateNeeded; +exports.checkLatestRelease = (callback) => { /* Check for internet connection */ @@ -54,30 +50,30 @@ exports.checkLatestRelease = function (callback) { } - version = electron.remote.app.getVersion(); + const version = electron.remote.app.getVersion(); /* Transform repository URL into release API URL */ - repoGitURL = pjson.repository.url; - repoURL = repoGitURL.replace('.git', '/releases'); + const repoGitURL = pjson.repository.url; + let repoURL = repoGitURL.replace('.git', '/releases'); repoURL = repoURL.replace('github.com', 'api.github.com/repos'); - xmlHttp = new XMLHttpRequest(); + const xmlHttp = new XMLHttpRequest(); xmlHttp.open('GET', repoURL, true); - xmlHttp.onload = function () { + xmlHttp.onload = () => { if (xmlHttp.status === 200) { - responseJson = JSON.parse(xmlHttp.responseText); + const responseJson = JSON.parse(xmlHttp.responseText); - latestVersion = responseJson[0].tag_name; + const latestVersion = responseJson[0].tag_name; console.log('Comparing latest release (' + latestVersion + ') with currently installed version (' + version + ')'); /* Compare current version in package.json to latest version pulled from Github */ - updateNeeded = isOlderSemanticVersion(version, latestVersion); + const updateNeeded = isOlderSemanticVersion(version, latestVersion); callback({updateNeeded: updateNeeded, latestVersion: updateNeeded ? latestVersion : version}); @@ -85,7 +81,7 @@ exports.checkLatestRelease = function (callback) { }; - xmlHttp.onerror = function () { + xmlHttp.onerror = () => { console.error('Failed to pull release information.'); callback({updateNeeded: false, error: 'HTTP connection error, failed to request app version information.'});