Bangle.loadWidgets(); Bangle.drawWidgets(); const storage = require('Storage'); let settings; function updateSettings() { //storage.erase('setting.json'); // - not needed, just causes extra writes if settings were the same if (Object.keys(settings.qmOptions).length === 0) delete settings.qmOptions; storage.write('setting.json', settings); if (!('qmOptions' in settings)) settings.qmOptions = {}; // easier if this always exists in this file } function updateOptions() { updateSettings(); Bangle.setOptions(settings.options) if (settings.quiet) { Bangle.setOptions(settings.qmOptions) } } function gToInternal(g) { // converts g to Espruino internal unit return g * 8192; } function internalToG(u) { // converts Espruino internal unit to g return u / 8192 } function resetSettings() { settings = { ble: true, // Bluetooth enabled by default blerepl: true, // Is REPL on Bluetooth - can Espruino IDE be used? log: false, // Do log messages appear on screen? quiet: 0, // quiet mode: 0: off, 1: priority only, 2: total silence timeout: 10, // Default LCD timeout in seconds vibrate: true, // Vibration enabled by default. App must support beep: "vib", // Beep enabled by default. App must support timezone: 0, // Set the timezone for the device HID: false, // BLE HID mode, off by default clock: null, // a string for the default clock's name "12hour" : false, // 12 or 24 hour clock? brightness: 1, // LCD brightness from 0 to 1 // welcomed : undefined/true (whether welcome app should show) options: { wakeOnBTN1: true, wakeOnBTN2: true, wakeOnBTN3: true, wakeOnFaceUp: false, wakeOnTouch: false, wakeOnTwist: true, twistThreshold: 819.2, twistMaxY: -800, twistTimeout: 1000 }, // Quiet Mode options: // we only set these if we want to override the default value // qmOptions: {}, // qmBrightness: undefined, // qmTimeout: undefined, }; updateSettings(); } settings = storage.readJSON('setting.json', 1); if (!settings) resetSettings(); if (!('qmOptions' in settings)) settings.qmOptions = {}; // easier if this always exists in here const boolFormat = v => v ? "On" : "Off"; function showMainMenu() { var beepV = [false, true, "vib"]; var beepN = ["Off", "Piezo", "Vibrate"]; const mainmenu = { '': { 'title': 'Settings' }, '< Back': ()=>load(), 'Make Connectable': ()=>makeConnectable(), 'App/Widget Settings': ()=>showAppSettingsMenu(), 'BLE': ()=>showBLEMenu(), 'Debug Info': { value: settings.log, format: v => v ? "Show" : "Hide", onchange: () => { settings.log = !settings.log; updateSettings(); } }, 'Beep': { value: 0 | beepV.indexOf(settings.beep), min: 0, max: 2, format: v => beepN[v], onchange: v => { settings.beep = beepV[v]; if (v==1) { analogWrite(D18,0.5,{freq:2000});setTimeout(()=>D18.reset(),200); } // piezo else if (v==2) { analogWrite(D13,0.1,{freq:2000});setTimeout(()=>D13.reset(),200); } // vibrate updateSettings(); } }, 'Vibration': { value: settings.vibrate, format: boolFormat, onchange: () => { settings.vibrate = !settings.vibrate; updateSettings(); if (settings.vibrate) { VIBRATE.write(1); setTimeout(() => VIBRATE.write(0), 10); } } }, "Quiet Mode": ()=>showQuietModeMenu(), 'Locale': ()=>showLocaleMenu(), 'Select Clock': ()=>showClockMenu(), 'Set Time': ()=>showSetTimeMenu(), 'LCD': ()=>showLCDMenu(), 'Theme': ()=>showThemeMenu(), 'Reset Settings': ()=>showResetMenu(), 'Turn Off': ()=>{ if (Bangle.softOff) Bangle.softOff(); else Bangle.off() }, }; return E.showMenu(mainmenu); } function showBLEMenu() { var hidV = [false, "kbmedia", "kb", "joy"]; var hidN = ["Off", "Kbrd & Media", "Kbrd","Joystick"]; E.showMenu({ '< Back': ()=>showMainMenu(), 'BLE': { value: settings.ble, format: boolFormat, onchange: () => { settings.ble = !settings.ble; updateSettings(); } }, 'Programmable': { value: settings.blerepl, format: boolFormat, onchange: () => { settings.blerepl = !settings.blerepl; updateSettings(); } }, 'HID': { value: 0 | hidV.indexOf(settings.HID), min: 0, max: 3, format: v => hidN[v], onchange: v => { settings.HID = hidV[v]; updateSettings(); } }, 'Passkey BETA': { value: settings.passkey?settings.passkey:"none", onchange: () => setTimeout(showPasskeyMenu) // graphical_menu redraws after the call }, 'Whitelist': { value: settings.whitelist?(settings.whitelist.length+" devs"):"off", onchange: () => setTimeout(showWhitelistMenu) // graphical_menu redraws after the call } }); } function showThemeMenu() { function cl(x) { return g.setColor(x).getColor(); } function upd(th) { g.theme = th; settings.theme = th; updateSettings(); delete g.reset; g._reset = g.reset; g.reset = function(n) { return g._reset().setColor(th.fg).setBgColor(th.bg); }; g.clear = function(n) { if (n) g.reset(); return g.clearRect(0,0,g.getWidth(),g.getHeight()); }; g.clear(1); Bangle.drawWidgets(); m.draw(); } var m = E.showMenu({ '':{title:'Theme'}, '< Back': ()=>showMainMenu(), 'Dark BW': ()=>{ upd({ fg:cl("#fff"), bg:cl("#000"), fg2:cl("#0ff"), bg2:cl("#000"), fgH:cl("#fff"), bgH:cl("#00f"), dark:true }); }, 'Light BW': ()=>{ upd({ fg:cl("#000"), bg:cl("#fff"), fg2:cl("#00f"), bg2:cl("#0ff"), fgH:cl("#000"), bgH:cl("#0ff"), dark:false }); }, 'Customize': ()=>showCustomThemeMenu(), }); function showCustomThemeMenu() { function cv(x) { return g.setColor(x).getColor(); } function setT(t, v) { let th = g.theme; th[t] = v; if (t==="bg") { th['dark'] = (v===cv("#000")); } upd(th); } const rgb = { black: "#000", white: "#fff", red: "#f00", green: "#0f0", blue: "#00f", cyan: "#0ff", magenta: "#f0f", yellow: "#ff0", }; let colors = [], names = []; for(const c in rgb) { names.push(c); colors.push(cv(rgb[c])); } function cn(v) { const i = colors.indexOf(v); return i!== -1 ? names[i] : v; // another color: just show value } let menu = { '':{title:'Custom Theme'}, "< Back": () => showThemeMenu() }; const labels = { fg: 'Foreground', bg: 'Background', fg2: 'Foreground 2', bg2: 'Background 2', fgH: 'Highlight FG', bgH: 'Highlight BG', }; ["fg", "bg", "fg2", "bg2", "fgH", "bgH"].forEach(t => { menu[labels[t]] = { value: colors.indexOf(g.theme[t]), format: () => cn(g.theme[t]), onchange: function(v) { // wrap around if (v>=colors.length) {v = 0;} if (v<0) {v = colors.length-1;} this.value = v; const c = colors[v]; // if we select the same fg and bg: set the other to the old color // e.g. bg=black;fg=white, user selects fg=black -> bg changes to white automatically // so users don't end up with a black-on-black menu if (t === 'fg' && g.theme.bg === c) setT('bg', g.theme.fg); if (t === 'bg' && g.theme.fg === c) setT('fg', g.theme.bg); setT(t, c); }, }; }); menu["< Back"] = () => showThemeMenu(); m = E.showMenu(menu); } } function showPasskeyMenu() { var menu = { "< Back" : ()=>showBLEMenu(), "Disable" : () => { settings.passkey = undefined; updateSettings(); showBLEMenu(); } }; if (!settings.passkey || settings.passkey.length!=6) settings.passkey = "123456"; for (var i=0;i<6;i++) (function(i){ menu[`Digit ${i+1}`] = { value : 0|settings.passkey[i], min: 0, max: 9, onchange: v => { var p = settings.passkey.split(""); p[i] = v; settings.passkey = p.join(""); updateSettings(); } }; })(i); E.showMenu(menu); } function showWhitelistMenu() { var menu = { "< Back" : ()=>showBLEMenu(), "Disable" : () => { settings.whitelist = undefined; updateSettings(); showBLEMenu(); } }; if (settings.whitelist) settings.whitelist.forEach(function(d){ menu[d.substr(0,17)] = function() { E.showPrompt('Remove\n'+d).then((v) => { if (v) { settings.whitelist.splice(settings.whitelist.indexOf(d),1); updateSettings(); } setTimeout(showWhitelistMenu, 50); }); } }); menu['Add Device']=function() { E.showAlert("Connect device\nto add to\nwhitelist","Whitelist").then(function() { NRF.removeAllListeners('connect'); showWhitelistMenu(); }); NRF.removeAllListeners('connect'); NRF.on('connect', function(addr) { if (!settings.whitelist) settings.whitelist=[]; settings.whitelist.push(addr); updateSettings(); NRF.removeAllListeners('connect'); showWhitelistMenu(); }); }; E.showMenu(menu); } function showLCDMenu() { const lcdMenu = { '': { 'title': 'LCD' }, '< Back': ()=>showMainMenu(), 'LCD Brightness': { value: settings.brightness, min: 0.1, max: 1, step: 0.1, onchange: v => { settings.brightness = v || 1; updateSettings(); if (!(settings.quiet && "qmBrightness" in settings)) { Bangle.setLCDBrightness(settings.brightness); } } }, 'LCD Timeout': { value: settings.timeout, min: 0, max: 60, step: 5, onchange: v => { settings.timeout = 0 | v; updateSettings(); if (!(settings.quiet && "qmTimeout" in settings)) { Bangle.setLCDTimeout(settings.timeout); } } }, 'Wake on BTN1': { value: settings.options.wakeOnBTN1, format: boolFormat, onchange: () => { settings.options.wakeOnBTN1 = !settings.options.wakeOnBTN1; updateOptions(); } }, 'Wake on BTN2': { value: settings.options.wakeOnBTN2, format: boolFormat, onchange: () => { settings.options.wakeOnBTN2 = !settings.options.wakeOnBTN2; updateOptions(); } }, 'Wake on BTN3': { value: settings.options.wakeOnBTN3, format: boolFormat, onchange: () => { settings.options.wakeOnBTN3 = !settings.options.wakeOnBTN3; updateOptions(); } }, 'Wake on FaceUp': { value: settings.options.wakeOnFaceUp, format: boolFormat, onchange: () => { settings.options.wakeOnFaceUp = !settings.options.wakeOnFaceUp; updateOptions(); } }, 'Wake on Touch': { value: settings.options.wakeOnTouch, format: boolFormat, onchange: () => { settings.options.wakeOnTouch = !settings.options.wakeOnTouch; updateOptions(); } }, 'Wake on Twist': { value: settings.options.wakeOnTwist, format: boolFormat, onchange: () => { settings.options.wakeOnTwist = !settings.options.wakeOnTwist; updateOptions(); } }, 'Twist Threshold': { value: internalToG(settings.options.twistThreshold), min: -0.5, max: 0.5, step: 0.01, onchange: v => { settings.options.twistThreshold = gToInternal(v || 0.1); updateOptions(); } }, 'Twist Max Y': { value: settings.options.twistMaxY, min: -1500, max: 1500, step: 100, onchange: v => { settings.options.twistMaxY = v || -800; updateOptions(); } }, 'Twist Timeout': { value: settings.options.twistTimeout, min: 0, max: 2000, step: 100, onchange: v => { settings.options.twistTimeout = v || 1000; updateOptions(); } } } return E.showMenu(lcdMenu) } function showQuietModeMenu() { // we always keep settings.quiet and settings.qmOptions // other qm values are deleted when not set const modes = ["Off", "Alarms", "Silent"]; const qmDisabledFormat = v => v ? "Off" : "-"; const qmMenu = { "": {"title": "Quiet Mode"}, "< Back": () => showMainMenu(), "Quiet Mode": { value: settings.quiet|0, format: v => modes[v%3], onchange: v => { settings.quiet = v%3; updateSettings(); updateOptions(); if ("qmsched" in WIDGETS) {WIDGETS["qmsched"].draw();} }, }, "LCD Brightness": { value: settings.qmBrightness || 0, min: 0, // 0 = use default max: 1, step: 0.1, format: v => (v>0.05) ? v : "-", onchange: v => { if (v>0.05) { // prevent v=0.000000000000001 bugs settings.qmBrightness = v; } else { delete settings.qmBrightness; } updateSettings(); if (settings.qmBrightness) { // show result, even if not quiet right now Bangle.setLCDBrightness(v); } else { Bangle.setLCDBrightness(settings.brightness); } }, }, "LCD Timeout": { value: settings.qmTimeout || 0, min: 0, // 0 = use default (no constant on for quiet mode) max: 60, step: 5, format: v => v>1 ? v : "-", onchange: v => { if (v>1) { settings.qmTimeout = v; } else { delete settings.qmTimeout; } updateSettings(); if (settings.quiet && v>1) { Bangle.setLCDTimeout(v); } else { Bangle.setLCDTimeout(settings.timeout); } }, }, // we disable wakeOn* events by overwriting them as false in qmOptions // not disabled = not present in qmOptions at all "Wake on FaceUp": { value: "wakeOnFaceUp" in settings.qmOptions, format: qmDisabledFormat, onchange: () => { if ("wakeOnFaceUp" in settings.qmOptions) { delete settings.qmOptions.wakeOnFaceUp; } else { settings.qmOptions.wakeOnFaceUp = false; } updateOptions(); }, }, "Wake on Touch": { value: "wakeOnTouch" in settings.qmOptions, format: qmDisabledFormat, onchange: () => { if ("wakeOnTouch" in settings.qmOptions) { delete settings.qmOptions.wakeOnTouch; } else { settings.qmOptions.wakeOnTouch = false; } updateOptions(); }, }, "Wake on Twist": { value: "wakeOnTwist" in settings.qmOptions, format: qmDisabledFormat, onchange: () => { if ("wakeOnTwist" in settings.qmOptions) { delete settings.qmOptions.wakeOnTwist; } else { settings.qmOptions.wakeOnTwist = false; } updateOptions(); }, }, }; return E.showMenu(qmMenu); } function showLocaleMenu() { const localemenu = { '': { 'title': 'Locale' }, '< Back': ()=>showMainMenu(), 'Time Zone': { value: settings.timezone, min: -11, max: 13, step: 0.5, onchange: v => { settings.timezone = v || 0; updateSettings(); } }, 'Clock Style': { value: !!settings["12hour"], format: v => v ? "12hr" : "24hr", onchange: v => { settings["12hour"] = v; updateSettings(); } } }; return E.showMenu(localemenu); } function showResetMenu() { const resetmenu = { '': { 'title': 'Reset' }, '< Back': ()=>showMainMenu(), 'Reset Settings': () => { E.showPrompt('Reset Settings?').then((v) => { if (v) { E.showMessage('Resetting'); resetSettings(); } setTimeout(showMainMenu, 50); }); } }; return E.showMenu(resetmenu); } function makeConnectable() { try { NRF.wake(); } catch (e) { } Bluetooth.setConsole(1); var name = "Bangle.js " + NRF.getAddress().substr(-5).replace(":", ""); E.showPrompt(name + "\nStay Connectable?", { title: "Connectable" }).then(r => { if (settings.ble != r) { settings.ble = r; updateSettings(); } if (!r) try { NRF.sleep(); } catch (e) { } showMainMenu(); }); } function showClockMenu() { var clockApps = require("Storage").list(/\.info$/) .map(app => {var a=storage.readJSON(app, 1);return (a&&a.type == "clock")?a:undefined}) .filter(app => app) // filter out any undefined apps .sort((a, b) => a.sortorder - b.sortorder); const clockMenu = { '': { 'title': 'Select Clock', }, '< Back': ()=>showMainMenu(), }; clockApps.forEach((app, index) => { var label = app.name; if ((!settings.clock && index === 0) || (settings.clock === app.src)) { label = "* " + label; } clockMenu[label] = () => { if (settings.clock !== app.src) { settings.clock = app.src; updateSettings(); showMainMenu(); } }; }); if (clockApps.length === 0) { clockMenu["No Clocks Found"] = () => { }; } return E.showMenu(clockMenu); } function showSetTimeMenu() { d = new Date(); const timemenu = { '': { 'title': 'Set Time' }, '< Back': function () { setTime(d.getTime() / 1000); showMainMenu(); }, 'Hour': { value: d.getHours(), onchange: function (v) { this.value = (v+24)%24; d.setHours(this.value); } }, 'Minute': { value: d.getMinutes(), onchange: function (v) { this.value = (v+60)%60; d.setMinutes(this.value); } }, 'Second': { value: d.getSeconds(), onchange: function (v) { this.value = (v+60)%60; d.setSeconds(this.value); } }, 'Date': { value: d.getDate(), onchange: function (v) { this.value = ((v+30)%31)+1; d.setDate(this.value); } }, 'Month': { value: d.getMonth() + 1, onchange: function (v) { this.value = ((v+11)%12)+1; d.setMonth(this.value - 1); } }, 'Year': { value: d.getFullYear(), min: 2019, max: 2100, onchange: function (v) { d.setFullYear(v); } } }; return E.showMenu(timemenu); } function showAppSettingsMenu() { let appmenu = { '': { 'title': 'App Settings' }, '< Back': ()=>showMainMenu(), } const apps = storage.list(/\.settings\.js$/) .map(s => s.substr(0, s.length-12)) .map(id => { const a=storage.readJSON(id+'.info',1) || {name: id}; return {id:id,name:a.name,sortorder:a.sortorder}; }) .sort((a, b) => { const n = (0|a.sortorder)-(0|b.sortorder); if (n) return n; // do sortorder first if (a.nameb.name) return 1; return 0; }) if (apps.length === 0) { appmenu['No app has settings'] = () => { }; } apps.forEach(function (app) { appmenu[app.name] = () => { showAppSettings(app) }; }) E.showMenu(appmenu) } function showAppSettings(app) { const showError = msg => { E.showMessage(`${app.name}:\n${msg}!\n\nBTN1 to go back`); setWatch(showAppSettingsMenu, BTN1, { repeat: false }); } let appSettings = storage.read(app.id+'.settings.js'); try { appSettings = eval(appSettings); } catch (e) { console.log(`${app.name} settings error:`, e) return showError('Error in settings'); } if (typeof appSettings !== "function") { return showError('Invalid settings'); } try { // pass showAppSettingsMenu as "back" argument appSettings(()=>showAppSettingsMenu()); } catch (e) { console.log(`${app.name} settings error:`, e) return showError('Error in settings'); } } showMainMenu();