/* jshint esversion:6, strict:true, browser:true*/
/* global console, alert */
'use strict';
var pwa = {};
pwa.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 : {},
peripheral : {},
service : {},
characteristics : {},
descriptors : {},
connected : false,
active : false
};
pwa.wifi = {
current : 'null',
available : [],
ip : null
};
async function delay (ms) {
return new Promise((resolve, reject) => {
return setTimeout(resolve, ms);
})
}
pwa.wble.scan = async function () {
let device;
UI.spinner.show('Scanning for INTVAL3...');
UI.overlay.show();
document.getElementById('tap').style.display = 'none';
pwa.wble.devices = [];
try {
device = await navigator.bluetooth.requestDevice({
filters: [{
name : pwa.wble.DEVICE_ID
}],
optionalServices: [ pwa.wble.SERVICE_ID ]
});
} catch (err) {
console.error(err);
pwa.wble.onError(err.message);
}
if (device) {
pwa.wble.onDiscover(device);
}
await delay(5000);
UI.spinner.hide();
UI.overlay.hide();
if (!pwa.wble.connected) {
pwa.alert('No devices found.')
settingsPage();
}
};
pwa.wble.onDiscover = function (device) {
if (device && device.name && device.name.indexOf('intval3') !== -1) {
console.log('BLE - Discovered INTVAL3');
//console.dir(device);
pwa.wble.devices.push(device);
if (!pwa.wble.connected) {
pwa.wble.connect(device);
}
} else {
//console.log(`BLE - Discovered Other ${device.id}`);
}
};
pwa.wble.connect = async function (device) {
console.log(`BLE - Connecting to ${device.id}`);
let peripheral;
let service;
try {
peripheral = await device.gatt.connect();
} catch (err) {
pwa.wble.onError(err);
}
try {
service = await peripheral.getPrimaryService(pwa.wble.SERVICE_ID);
} catch (err) {
console.error(err);
pwa.wble.onError(err.message);
return false;
}
pwa.wble.onConnect(peripheral, device, service);
};
pwa.wble.onConnect = function (peripheral, device, service) {
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.dir(peripheral);
//console.dir(device);
pwa.wble.peripheral = peripheral;
pwa.wble.device = device;
pwa.wble.service = service;
pwa.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();
getWifi();
};
pwa.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 (!pwa.wble.connected) {
console.warn('Not connected to any device');
return false;
}
device = pwa.wble.device;
console.log(`BLE - Disconnecting from ${device.id}`);
//ble.disconnect(device.id, pwa.wble.onDisconnect, pwa.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();
};
pwa.wble.onDisconnect = function (res) {
console.log(`BLE - Disconnected from ${res}`);
pwa.wble.connected = false;
pwa.wble.device = {};
};
pwa.wble.onError = function (err) {
if (err.errorMessage && err.errorMessage === 'Peripheral Disconnected') {
console.log('Device disconnected');
pwa.wble.disconnect()
} else {
pwa.alert(JSON.stringify(err));
}
};
pwa.wble.read = async function (characteristicId) {
const decoder = new TextDecoder('utf-8');
let characteristic;
let value;
let json;
if (typeof pwa.wble.characteristics[characteristicId] === 'undefined') {
try {
characteristic = await pwa.wble.service.getCharacteristic(characteristicId);
pwa.wble.characteristics[characteristicId] = characteristic;
} catch (err) {
console.error(err);
pwa.wble.onError(err.message);
return false;
}
} else {
characteristic = pwa.wble.characteristics[characteristicId]
}
try {
value = await characteristic.readValue();
} catch (err) {
console.error(err);
pwa.wble.onError(err.message);
}
json = decoder.decode(value);
return JSON.parse(json);
}
pwa.wble.write = async function ( characteristicId, json) {
const encoder = new TextEncoder('utf-8');
let characteristic;
let value;
if (typeof pwa.wble.characteristics[characteristicId] === 'undefined') {
try {
characteristic = await pwa.wble.service.getCharacteristic(characteristicId);
pwa.wble.charactersitics[characteristicId] = characteristic;
} catch (err) {
console.error(err);
pwa.wble.onError(err.message);
return false;
}
} else {
characteristic = pwa.wble.characteristics[characteristicId];
}
value = JSON.stringify(json);
encoder.encode(value);
try {
await characteristic.writeValue(encoder.encode(value))
} catch (err) {
console.error(err);
pwa.wble.onError(err.message);
return false;
}
return true;
}
pwa.init = function () {
const bleInputs = document.querySelectorAll('.ble');
const bolIso = document.querySelector('.iso');
const bolF = document.querySelector('.fstop');
const tap = document.getElementById('tap');
//document.querySelector('body').classList.add('mobile');
window.frame = pwa.frame;
window.getState = pwa.getState;
window.setDir = pwa.setDir;
window.setExposure = pwa.setExposure;
window.setDelay = pwa.setDelay;
window.setCounter = pwa.setCounter;
window.sequence = pwa.sequence;
window.reset = pwa.reset;
window.restart = pwa.restart;
window.update = pwa.update;
window.getWifi = pwa.getWifi;
window.setWifi = pwa.setWifi;
window.editWifi = pwa.editWifi;
//show ble-specific fields in settings
for (let i of bleInputs) {
i.classList.add('active');
}
UI.spinner.init();
UI.overlay.show();
tap.onclick = pwa.wble.scan;
tap.style.display = 'block';
//pwa.cameraValues();
};
pwa.pairInteraction = function () {
pwa.wble.scan();
}
pwa.getState = async function () {
let state;
if (!pwa.wble.connected) {
return false
}
try {
state = await pwa.wble.read(pwa.wble.CHAR_ID);
} catch (err) {
console.error(err);
pwa.wble.onError(err.message);
return false;
}
setState(state);
};
pwa.frame = async function () {
const opts = {
type : 'frame'
};
if (!pwa.wble.connected) {
return pwa.alert('Not connected to an INTVAL3 device.');
}
if (pwa.wble.active) {
return false;
}
document.getElementById('frame').classList.add('focus');
pwa.wble.active = true;
try {
await pwa.wble.write(pwa.wble.CHAR_ID, opts);
} catch (err) {
console.error(err);
pwa.wble.onError(err.message);
return false;``
}
pwa.frameSuccess();
};
pwa.frameSuccess = function () {
if (STATE.exposure < 5000) {
console.log('Frame finished, getting state.');
pwa.wble.active = false;
document.getElementById('frame').classList.remove('focus');
pwa.getState();
} else {
setTimeout(() => {
console.log('Frame finished, getting state.');
pwa.wble.active = false;
document.getElementById('frame').classList.remove('focus');
pwa.getState();
}, STATE.exposure + 500)
}
}
pwa.setDir = async function () {
const opts = {
type : 'dir',
dir : !document.getElementById('dir').checked
};
try {
await pwa.wble.write(pwa.wble.CHAR_ID, opts);
} catch (err) {
console.error(err);
pwa.wble.onError(err.message);
return false;
}
pwa.dirSuccess();
};
pwa.dirSuccess = function () {
console.log('Set direction');
pwa.getState();
setTimeout(() => {
setDirLabel(STATE.dir);
}, 50);
};
pwa.setExposure = async function () {
const opts = {
type : 'exposure'
};
let exposure = document.getElementById('exposure').value;
let scaledExposure;
if (exposure === '' || exposure === null) {
exposure = 0;
}
scaledExposure = scaleTime(exposure, STATE.scale);
opts.exposure = scaledExposure;
try {
await pwa.wble.write(pwa.wble.CHAR_ID, opts);
} catch (err) {
console.error(err);
pwa.wble.onError(err.message);
return false;
}
pwa.exposureSuccess();
};
pwa.exposureSuccess = function () {
console.log('Set exposure');
pwa.getState();
};
pwa.setDelay = async function () {
const delay = document.getElementById('delay').value;
const scaledDelay = scaleTime(delay, STATE.delayScale);
const opts = {
type : 'delay',
delay : scaledDelay
};
try {
await pwa.wble.write(pwa.wble.CHAR_ID, opts);
} catch (err) {
console.error(err);
pwa.wble.onError(err.message);
return false;
}
pwa.delaySuccess();
}
pwa.delaySuccess = function () {
console.log('Set delay');
pwa.getState();
};
pwa.setCounter = async function () {
const opts = {
type : 'counter',
counter : null
};
const counter = document.getElementById('counter').value;
const change = prompt(`Change counter value?`, counter);
if (change === null || !isNumeric(change)) return false;
if (change === counter) return true;
opts.counter = change;
try {
await pwa.wble.write(pwa.wble.CHAR_ID, opts);
} catch (err) {
console.error(err);
pwa.wble.onError(err.message);
return false;
}
pwa.counterSuccess();
};
pwa.counterSuccess = function () {
console.log('Set counter');
pwa.getState();
};
pwa.sequence = async function () {
const opts = {
type : 'sequence'
};
const elem = document.getElementById('seq');
if (!pwa.wble.connected) {
return pwa.alert('Not connected to an INTVAL3 device.');
}
try {
await pwa.wble.write(pwa.wble.CHAR_ID, opts);
} catch (err) {
console.error(err);
pwa.wble.onError(err.message);
return false;
}
pwa.sequenceSuccess();
if (!elem.classList.contains('focus')) {
elem.classList.add('focus');
}
pwa.wble.active = true;
};
pwa.sequenceSuccess = function () {
console.log('Sequence state changed');
pwa.getState();
setTimeout(() => {
if (STATE.sequence) {
pwa.wble.active = true;
seqState(true);
} else {
pwa.wble.active = false;
seqState(false);
}
}, 20);
};
//retreive object with list of available Wifi APs,
//and state of current connection, if available
pwa.getWifi = async function () {
let wifiRes;
UI.spinner.show('Refreshing WIFI...');
UI.overlay.show();
try {
wifiRes = await pwa.wble.read(pwa.wble.WIFI_ID);
} catch (err) {
console.error(err);
pwa.wble.onError(err.message);
return false
}
pwa.getWifiSuccess(wifiRes);
};
pwa.getWifiSuccess = function (res) {
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');
UI.spinner.hide();
UI.overlay.hide();
elem.innerHTML = '';
console.dir(res);
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');
}
}
pwa.wifi.current = res.current;
pwa.wifi.available = res.available;
pwa.wifi.ip = res.ip;
};
pwa.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 !== pwa.wifi.current && available.classList.contains('active')) {
available.classList.remove('active');
}
};
pwa.setWifi = async 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 pwa.alert('Cannot set wireless credentials with a blank SSID');
}
if (pwd === '' || pwd === null || pwd === undefined) {
return pwa.alert('Cannot set wireless credentials with a blank passphrase');
}
if (pwd.length < 8 || pwd.length > 63) {
return pwa.alert('Passphrase must be 8..63 characters');
}
try {
await pwa.wble.write(pwa.wble.WIFI_ID, opts);
} catch (err) {
console.error(err);
pwa.wble.onError(err.message);
return false;
}
pwa.setWifiSuccess();
};
pwa.setWifiSuccess = function () {
UI.spinner.hide();
UI.overlay.hide();
console.log('Set new wifi credentials');
setTimeout(pwa.getWifi, 100);
};
/*
pwa.exif = {}
pwa.getCamera = function () {
const opts = {
quality: 30,
sourceType: Camera.PictureSourceType.CAMERA,
destinationType: Camera.DestinationType.FILE_URI
};
navigator.camera.getPicture(pwa.cameraSuccess, pwa.cameraError, opts);
};
pwa.cameraSuccess = function (result) {
const thisResult = JSON.parse(result);
const metadata = JSON.parse(thisResult.json_metadata);
pwa.cameraExposure(metadata.Exif);
};
pwa.cameraError = function (err) {
console.error(err);
pwa.alert(JSON.stringify(err));
};
pwa.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;
pwa.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
}
}
};
pwa.refreshExposure = function () {
if (typeof pwa.exif.ExposureTime !== 'undefined') {
pwa.cameraExposure(pwa.exif);
}
};
pwa.EV = function (fstop, shutter) {
const sec = shutter / 1000; //shutter in ms => seconds
const square = Math.pow(fstop, 2);
return Math.log(square / sec);
};*/
pwa.reset = async function () {
const opts = {
type : 'reset'
};
const cont = confirm(`Reset INTVAL3 to default settings and clear counter?`);
if (cont) {
try {
await pwa.wble.write(pwa.wble.CHAR_ID, opts);
} catch (err) {
console.error(err);
pwa.wble.onError(err.message);
return false;
}
pwa.resetSuccess();
}
};
pwa.resetSuccess = function () {
console.log('Reset to default settings');
setTimeout(() => {
pwa.getState();
}, 100)
};
pwa.update = async function () {
const opts = {
type : 'update'
};
const cont = confirm(`Check for updates? You will be disconnected from the INTVAL3 during this process.`);
if (cont) {
UI.spinner.show('Updating INTVAL3...');
UI.overlay.show();
try {
await pwa.wble.write(pwa.wble.CHAR_ID, opts);
} catch (err) {
console.error(err);
pwa.wble.onError(err.message);
return false;
}
pwa.updateSuccess();
}
};
pwa.updateSuccess = function () {
console.log('Finished updating firmware, restarting...');
};
pwa.restart = async function () {
const opts = {
type : 'restart'
};
const cont = confirm(`Restart the INTVAL3? You will be disconnected from it during this process.`);
if (cont) {
UI.spinner.show('Restarting INTVAL3...');
UI.overlay.show();
try {
await pwa.wble.write(pwa.wble.CHAR_ID, opts);
} catch (err) {
console.error(err);
pwa.wble.onError(err.message);
return false;
}
pwa.restartSuccess();
}
};
pwa.restartSuccess = function () {
console.log('Restarting... ');
}
pwa.alert = function (msg) {
if (typeof navigator !== 'undefined' && typeof navigator.notification !== 'undefined') {
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)
}