From 6c3b263ac179c50ce41184b6d2012181dd6b9609 Mon Sep 17 00:00:00 2001 From: mmcwilliams Date: Fri, 15 Feb 2019 15:06:12 -0500 Subject: [PATCH] Started work on navitator.bluetooth version of the mobile app as a PWA. More on this to come. --- app/www/static/js/intval.pwa.js | 804 ++++++++++++++++++++++++++++++++ 1 file changed, 804 insertions(+) create mode 100644 app/www/static/js/intval.pwa.js diff --git a/app/www/static/js/intval.pwa.js b/app/www/static/js/intval.pwa.js new file mode 100644 index 0000000..4ff1866 --- /dev/null +++ b/app/www/static/js/intval.pwa.js @@ -0,0 +1,804 @@ +/* jshint esversion:6, strict:true, browser:true*/ +/* global console, alert */ + + +'use strict'; +var mobile = {}; + +mobile.wble = { + BLENO_DEVICE_NAME : 'intval3', + DEVICE_ID : 'intval3', + SERVICE_ID : '149582bd-d49d-4b5c-acd1-1ae503d09e7a', + CHAR_ID : '47bf69fb-f62f-4ef8-9be8-eb727a54fae4', //general data + WIFI_ID : '3fe7d9cf-7bd2-4ff0-97c5-ebe87288c2cc', //wifi only + devices : [], + device : {}, + connected : false, + active : false +}; + +mobile.wifi = { + current : 'null', + available : [], + ip : null +}; + +async function delay (ms) { + return new Promise((resolve, reject) => { + return setTimeout(resolve, ms); + }) +} + +mobile.wble.scan = async function () { + let device; + UI.spinner.show('Scanning for INTVAL3...'); + UI.overlay.show(); + try { + device = await navigator.bluetooth.requestDevice({ + filters: [{ services: [ mobile.wble.SERVICE_ID ] }], + //optionalServices: optionalServices + }); + mobile.wble.onDiscover(device); + } catch (err) { + mobile.wble.onError(err); + } + //ble.scan([], 5, mobile.wble.onDiscover, mobile.wble.onError); + mobile.wble.devices = []; + + await delay(5000); + + UI.spinner.hide(); + UI.overlay.hide(); + + if (!mobile.wble.connected) { + mobile.alert('No devices found.') + settingsPage(); + } +}; + +mobile.wble.onDiscover = function (device) { + if (device && device.name && device.name.indexOf('intval3') !== -1) { + console.log('BLE - Discovered INTVAL3'); + console.dir(device); + mobile.wble.devices.push(device); + if (!mobile.wble.connected) { + mobile.wble.connect(device); + } + } else { + //console.log(`BLE - Discovered Other ${device.id}`); + } +}; + +mobile.wble.connect = async function (device) { + console.log(`BLE - Connecting to ${device.id}`); + try { + await device.gatt.connect() + } catch (err) { + mobile.wble.onError(err); + } + ble.connect(device.id, (peripheral) => { + mobile.wble.onConnect(peripheral, device); + }, mobile.wble.onError); +}; + +mobile.wble.onConnect = function (peripheral, device) { + const elem = document.getElementById('bluetooth'); + const option = document.createElement('option'); + const disconnect = document.getElementById('disconnect'); + const scan = document.getElementById('scan'); + + UI.spinner.hide(); + UI.overlay.hide(); + console.log(`BLE - Connected to ${device.id}`); + console.log(peripheral); + console.dir(device); + + mobile.wble.device = device; + mobile.wble.connected = true; + + elem.innerHTML = ''; + option.text = device.name; + option.value = device.id; + elem.add(option); + + disconnect.classList.add('active'); + scan.classList.remove('active'); + + getState(); + mobile.getWifi(); +}; + +mobile.wble.disconnect = function () { + const elem = document.getElementById('bluetooth'); + const option = document.createElement('option'); + const disconnect = document.getElementById('disconnect'); + const scan = document.getElementById('scan'); + let device; + if (!mobile.wble.connected) { + console.warn('Not connected to any device'); + return false; + } + device = mobile.wble.device; + console.log(`BLE - Disconnecting from ${device.id}`); + ble.disconnect(device.id, mobile.wble.onDisconnect, mobile.wble.onDisconnect); + + elem.innerHTML = ''; + option.text = 'N/A'; + elem.add(option); + + disconnect.classList.remove('active'); + scan.classList.add('active'); + UI.spinner.hide(); + UI.overlay.hide(); +}; + +mobile.wble.onDisconnect = function (res) { + console.log(`BLE - Disconnected from ${res}`); + mobile.wble.connected = false; + mobile.wble.device = {}; +}; + +mobile.wble.onError = function (err) { + if (err.errorMessage && err.errorMessage === 'Peripheral Disconnected') { + console.log('Device disconnected'); + mobile.wble.disconnect() + } else { + mobile.alert(JSON.stringify(err)); + } + /* + Object + errorDescription: "The specified device has disconnected from us." + errorMessage: "Peripheral Disconnected" + id: "E8EF4B8B-0B5E-4E96-B337-E878DB1E3C4B" + name: "intval3_b827ebc7461d" + */ +}; + +mobile.init = function () { + const bleInputs = document.querySelectorAll('.ble'); + const bolIso = document.querySelector('.iso'); + const bolF = document.querySelector('.fstop'); + + document.querySelector('body').classList.add('mobile'); + + window.frame = mobile.frame; + window.getState = mobile.getState; + window.setDir = mobile.setDir; + window.setExposure = mobile.setExposure; + window.setDelay = mobile.setDelay; + window.setCounter = mobile.setCounter; + window.sequence = mobile.sequence; + window.reset = mobile.reset; + window.restart = mobile.restart; + window.update = mobile.update; + + //show ble-specific fields in settings + for (let i of bleInputs) { + i.classList.add('active'); + } + UI.spinner.init() + mobile.wble.scan(); + mobile.cameraValues(); + +}; + +mobile.getState = function () { + if (!mobile.wble.connected) { + //returning here will prevent error alert + } + ble.read(mobile.wble.device.id, + mobile.wble.SERVICE_ID, + mobile.wble.CHAR_ID, + mobile.stateSuccess, + mobile.wble.onError); +}; +mobile.stateSuccess = function (data) { + let str = bytesToString(data); + let res = JSON.parse(str); + setState(res); +}; + +mobile.frame = function () { + const opts = { + type : 'frame' + }; + if (!mobile.wble.connected) { + return mobile.alert('Not connected to an INTVAL3 device.'); + } + if (mobile.wble.active) { + return false; + } + ble.write(mobile.wble.device.id, + mobile.wble.SERVICE_ID, + mobile.wble.CHAR_ID, + stringToBytes(JSON.stringify(opts)), //check length? + mobile.frameSuccess, + mobile.wble.onError); + document.getElementById('frame').classList.add('focus'); + mobile.wble.active = true; +}; + + +mobile.frameSuccess = function () { + if (STATE.exposure < 5000) { + console.log('Frame finished, getting state.'); + mobile.wble.active = false; + document.getElementById('frame').classList.remove('focus'); + mobile.getState(); + } else { + setTimeout(() => { + console.log('Frame finished, getting state.'); + mobile.wble.active = false; + document.getElementById('frame').classList.remove('focus'); + mobile.getState(); + }, STATE.exposure + 500) + } +} +mobile.setDir = function () { + const opts = { + type : 'dir', + dir : !document.getElementById('dir').checked + }; + + ble.write(mobile.wble.device.id, + mobile.wble.SERVICE_ID, + mobile.wble.CHAR_ID, + stringToBytes(JSON.stringify(opts)), //check length? + mobile.dirSuccess, + mobile.wble.onError); +}; +mobile.dirSuccess = function () { + console.log('Set direction'); + mobile.getState(); + setTimeout(() => { + setDirLabel(STATE.dir); + }, 50); +}; +mobile.setExposure = function () { + let exposure = document.getElementById('exposure').value; + let scaledExposure; + let opts = { + type : 'exposure' + }; + if (exposure === '' || exposure === null) { + exposure = 0; + } + scaledExposure = scaleTime(exposure, STATE.scale); + opts.exposure = scaledExposure; + ble.write(mobile.wble.device.id, + mobile.wble.SERVICE_ID, + mobile.wble.CHAR_ID, + stringToBytes(JSON.stringify(opts)), //check length? + mobile.exposureSuccess, + mobile.wble.onError); +}; +mobile.exposureSuccess = function () { + console.log('Set exposure'); + mobile.getState(); +}; + +mobile.setDelay = function () { + const delay = document.getElementById('delay').value; + const scaledDelay = scaleTime(delay, STATE.delayScale); + let opts = { + type : 'delay', + delay : scaledDelay + }; + ble.write(mobile.wble.device.id, + mobile.wble.SERVICE_ID, + mobile.wble.CHAR_ID, + stringToBytes(JSON.stringify(opts)), //check length? + mobile.delaySuccess, + mobile.wble.onError); +} + +mobile.delaySuccess = function () { + console.log('Set delay'); + mobile.getState(); +}; + +mobile.setCounter = function () { + let opts = { + type : 'counter', + counter : null + }; + const counter = document.getElementById('counter').value; + function counterPrompt (results) { + let change = results.input1 + if (results.buttonIndex === 1) { + if (change === null || !isNumeric(change)) return false; + opts.counter = change; + ble.write(mobile.wble.device.id, + mobile.wble.SERVICE_ID, + mobile.wble.CHAR_ID, + stringToBytes(JSON.stringify(opts)), //check length? + mobile.counterSuccess, + mobile.wble.onError); + } + } + navigator.notification.prompt( + `Change counter value?`, + counterPrompt, + 'INTVAL3', + ['Okay', 'Cancel'], + counter); +}; + +mobile.counterSuccess = function () { + console.log('Set counter'); + mobile.getState(); +}; + +mobile.sequence = function () { + const opts = { + type : 'sequence' + }; + const elem = document.getElementById('seq'); + if (!mobile.wble.connected) { + return mobile.alert('Not connected to an INTVAL3 device.'); + } + ble.write(mobile.wble.device.id, + mobile.wble.SERVICE_ID, + mobile.wble.CHAR_ID, + stringToBytes(JSON.stringify(opts)), //check length? + mobile.sequenceSuccess, + mobile.wble.onError); + + if (!elem.classList.contains('focus')) { + elem.classList.add('focus'); + } + + mobile.wble.active = true; +}; + +mobile.sequenceSuccess = function () { + console.log('Sequence state changed'); + mobile.getState(); + setTimeout(() => { + if (STATE.sequence) { + mobile.wble.active = true; + seqState(true); + } else { + mobile.wble.active = false; + seqState(false); + } + }, 20); +}; + + +//retreive object with list of available Wifi APs, +//and state of current connection, if available +mobile.getWifi = function () { + UI.spinner.show('Refreshing WIFI...'); + UI.overlay.show(); + + ble.read(mobile.wble.device.id, + mobile.wble.SERVICE_ID, + mobile.wble.WIFI_ID, + mobile.getWifiSuccess, + mobile.wble.onError); +}; + +mobile.getWifiSuccess = function (data) { + const elem = document.getElementById('available'); + const wifi = document.getElementById('wifi'); + const password = document.getElementById('password'); + const ip = document.getElementById('ip'); + let option = document.createElement('option'); + let str = bytesToString(data); + let res = JSON.parse(str); + + UI.spinner.hide(); + UI.overlay.hide(); + elem.innerHTML = '' + if (!res.available || res.available.length === 0) { + if (elem.classList.contains('active')) { + elem.classList.remove('active'); + } + option.text = 'N/A' + elem.add(option); + elem.value = ''; + } else { + for (let ap of res.available) { + option = document.createElement('option'); + option.text = ap; + option.value = ap; + elem.add(option); + } + if (res.current && res.available.indexOf(res.current) !== -1) { + elem.value = res.current + if (!elem.classList.contains('active')) { + elem.classList.add('active'); + } + if (wifi.classList.contains('active')) { + wifi.classList.remove('active'); + } + if (password.classList.contains('active')) { + password.classList.remove('active'); + } + } else { + if (!wifi.classList.contains('active')) { + wifi.classList.add('active'); + } + if (!password.classList.contains('active')) { + password.classList.add('active'); + } + } + } + if (typeof res.ip !== 'undefined' && res.ip != null ) { + ip.innerHTML = `Local IP: ${res.ip}` + if (!ip.classList.contains('active')) { + ip.classList.add('active'); + } + } else { + ip.innerHTML = 'Local IP: null' + if (ip.classList.contains('active')) { + ip.classList.remove('active'); + } + } + mobile.wifi.current = res.current; + mobile.wifi.available = res.available; + mobile.wifi.ip = res.ip; +}; + +mobile.editWifi = function () { + const available = document.getElementById('available'); + const wifi = document.getElementById('wifi'); + const password = document.getElementById('password'); + if (!wifi.classList.contains('active')) { + wifi.classList.add('active'); + } + if (!password.classList.contains('active')) { + password.classList.add('active'); + } + password.focus(); + if (available.value !== mobile.wifi.current && available.classList.contains('active')) { + available.classList.remove('active'); + } +}; + +mobile.setWifi = function () { + const ssid = document.getElementById('available').value; + const pwd = document.getElementById('password').value; + const opts = { + ssid : ssid, + pwd : pwd + }; + UI.spinner.show('Setting WIFI...'); + UI.overlay.show(); + + if (ssid === '' || ssid === null || ssid === undefined) { + return mobile.alert('Cannot set wireless credentials with a blank SSID'); + } + if (pwd === '' || pwd === null || pwd === undefined) { + return mobile.alert('Cannot set wireless credentials with a blank passphrase'); + } + if (pwd.length < 8 || pwd.length > 63) { + return mobile.alert('Passphrase must be 8..63 characters'); + } + ble.write(mobile.wble.device.id, + mobile.wble.SERVICE_ID, + mobile.wble.WIFI_ID, + stringToBytes(JSON.stringify(opts)), + mobile.setWifiSuccess, + mobile.wble.onError); +}; + +mobile.setWifiSuccess = function () { + UI.spinner.hide(); + UI.overlay.hide(); + console.log('Set new wifi credentials'); + setTimeout(mobile.getWifi, 100); +}; +mobile.exif = {} + +mobile.getCamera = function () { + const opts = { + quality: 30, + sourceType: Camera.PictureSourceType.CAMERA, + destinationType: Camera.DestinationType.FILE_URI + }; + navigator.camera.getPicture(mobile.cameraSuccess, mobile.cameraError, opts); +}; +mobile.cameraSuccess = function (result) { + const thisResult = JSON.parse(result); + const metadata = JSON.parse(thisResult.json_metadata); + + mobile.cameraExposure(metadata.Exif); +}; +mobile.cameraError = function (err) { + console.error(err); + mobile.alert(JSON.stringify(err)); +}; + +mobile.cameraExposure = function (exif) { + const cam_exp = document.getElementById('cam_exp'); + const cam_f = document.getElementById('cam_f'); + const cam_iso = document.getElementById('cam_iso'); + const bol_exp = document.getElementById('bol_exp'); + const bol_f = document.getElementById('bol_f'); + const bol_iso = document.getElementById('bol_iso'); + const bol_f_diff = document.getElementById('bol_f_diff'); + const bol_iso_diff = document.getElementById('bol_iso_diff'); + const bol_exp_diff = document.getElementById('bol_exp_diff'); + + const fstop = BOLEX.fstop || 5.6; + const iso = BOLEX.iso || 100; + const prism = BOLEX.prism || 0.8; + + const cFstop = exif.ApertureValue || exif.FNumber; + const cExposure = exif.ExposureTime * 1000; + const cIso = exif.ISOSpeedRatings[0]; + + //convert fstop to "fnumber", an absolute scale where stops are scaled to 1.0 + const f = fnumber(cFstop); + const target = fnumber(fstop); //bolex + + let exposure = cExposure; + let isoStops = 0; + let fStops = 0; + let expDiff; + + let scale_elem; + let exposure_elem; + + let proceed; + let e1; + let e2; + + mobile.exif = exif; + + //Determine if fstop of phone camera "f" + if (target !== f) { + fStops = f - target; + exposure = exposure / Math.pow(2, fStops); + } + + if (cIso != iso) { + isoStops = (Math.log(cIso) / Math.log(2)) - (Math.log(iso) / Math.log(2)); + } + + //Double or halve exposure based on the differences in ISO stops + exposure = exposure * Math.pow(2, isoStops); + + //Compensate for Bolex prism + exposure = exposure * Math.pow(2, prism); + + exposure = Math.round(exposure) //round to nearest millisecond + + bol_f.value = fstop; + bol_iso.value = iso; + bol_exp.value = exposure; + + //Total difference in exposure from phone camera to Bolex + expDiff = (Math.log(exposure) / Math.log(2)) - (Math.log(cExposure) / Math.log(2)); + + bol_exp_diff.innerHTML = floatDisplay(expDiff); + bol_iso_diff.innerHTML = floatDisplay(isoStops); + bol_f_diff.innerHTML = floatDisplay(-fStops); + + cam_exp.value = cExposure; + cam_f.value = cFstop; + cam_iso.value = cIso; + + function exposureConfirm (index) { + if (index === 1) { + e1 = new Event('change'); + e2 = new Event('change'); + + scale_elem = document.getElementById('scale'); + exposure_elem = document.getElementById('exposure'); + + scale_elem.value = 'ms'; + scale_elem.dispatchEvent(e1); + + exposure_elem.value = exposure; + exposure_elem.dispatchEvent(e2); + } + } + + if (exposure > 500) { + navigator.notification.confirm( + `Set camera exposure to ${exposure}ms to match photo?`, + exposureConfirm, + 'INTVAL3', + ['Okay', 'Cancel'] + ); + } + + /* +{ + "Exif": { + "DateTimeOriginal": "2018:02:02 16:59:13", + "ExposureBiasValue": 0, + "SensingMethod": 2, + "BrightnessValue": -0.9969016228800144, + "LensMake": "Apple", + "FNumber": 1.8, + "FocalLength": 3.99, + "ShutterSpeedValue": 2.049355412374274, + "SceneType": 1, + "ApertureValue": 1.6959938131099002, + "SubjectArea": [ + 2015, + 1511, + 2217, + 1330 + ], + "ColorSpace": 65535, + "LensSpecification": [ + 3.99, + 3.99, + 1.8, + 1.8 + ], + "PixelYDimension": 3024, + "WhiteBalance": 0, + "DateTimeDigitized": "2018:02:02 16:59:13", + "ExposureMode": 0, + "ISOSpeedRatings": [ + 100 + ], + "PixelXDimension": 4032, + "LensModel": "iPhone 8 back camera 3.99mm f/1.8", + "ExposureTime": 0.25, + "Flash": 24, + "SubsecTimeDigitized": "209", + "SubsecTimeOriginal": "209", + "ExposureProgram": 2, + "FocalLenIn35mmFilm": 28, + "MeteringMode": 5 + } +} + */ +}; + +mobile.refreshExposure = function () { + if (typeof mobile.exif.ExposureTime !== 'undefined') { + mobile.cameraExposure(mobile.exif); + } +}; + +mobile.EV = function (fstop, shutter) { + const sec = shutter / 1000; //shutter in ms => seconds + const square = Math.pow(fstop, 2); + return Math.log(square / sec); +}; + +mobile.reset = function () { + let opts = { + type : 'reset' + }; + function resetConfirm (index) { + if (index === 1) { + ble.write(mobile.wble.device.id, + mobile.wble.SERVICE_ID, + mobile.wble.CHAR_ID, + stringToBytes(JSON.stringify(opts)), + mobile.resetSuccess, + mobile.wble.onError); + } + } + navigator.notification.confirm( + `Reset INTVAL3 to default settings and clear counter?`, + resetConfirm, + 'INTVAL3', + ['Okay', 'Cancel'] + ); +}; + +mobile.resetSuccess = function () { + console.log('Reset to default settings'); + setTimeout(() => { + mobile.getState(); + }, 100) +}; + +mobile.update = function () { + let opts = { + type : 'update' + }; + function updateConfirm (index) { + if (index === 1) { + UI.spinner.show('Updating INTVAL3...'); + UI.overlay.show(); + ble.write(mobile.wble.device.id, + mobile.wble.SERVICE_ID, + mobile.wble.CHAR_ID, + stringToBytes(JSON.stringify(opts)), + mobile.updateSuccess, + mobile.wble.onError); + } + } + navigator.notification.confirm( + `Check for updates? You will be disconnected from the INTVAL3 during this process.`, + updateConfirm, + 'INTVAL3', + ['Okay', 'Cancel'] + ); +}; + +mobile.updateSuccess = function () { + console.log('Finished updating firmware, restarting...'); +}; + +mobile.restart = function () { + let opts = { + type : 'restart' + }; + function restartConfirm (index) { + if (index === 1) { + UI.spinner.show('Restarting INTVAL3...'); + UI.overlay.show(); + ble.write(mobile.wble.device.id, + mobile.wble.SERVICE_ID, + mobile.wble.CHAR_ID, + stringToBytes(JSON.stringify(opts)), + mobile.restartSuccess, + mobile.wble.onError); + } + } + navigator.notification.confirm( + `Restart the INTVAL3? You will be disconnected from it during this process.`, + restartConfirm, + 'INTVAL3', + ['Okay', 'Cancel'] + ); +}; +mobile.restartSuccess = function () { + console.log('Restarting... '); +} + +mobile.alert = function (msg) { + if (navigator && navigator.notification) { + navigator.notification.alert( + msg, + () => {}, + 'INTVAL3', + 'Okay' + ); + } else { + alert(msg); + } +}; + +/** + * Mobile helper functions + */ + +function bytesToString (buffer) { + return String.fromCharCode.apply(null, new Uint8Array(buffer)); +} + +function stringToBytes(string) { + var array = new Uint8Array(string.length); + for (var i = 0, l = string.length; i < l; i++) { + array[i] = string.charCodeAt(i); + } + return array.buffer; +} + +function fnumber (fstop) { + return Math.log(fstop) / Math.log(Math.sqrt(2)); +} + +function floatDisplay (value) { + let str = value + ''; + const period = str.indexOf('.'); + if (period === -1) { + str = str + '.0'; + } else { + str = roundTenth(value) + ''; + } + if (value < 0) { + str = `${(str + '')}`; + } else if (value > 0) { + str = `+${(str + '')}`; + } + return str; +} + +function roundTenth (value) { + return Math.round((value * 10) / 10) +} +