diff --git a/appinfo.js b/appinfo.js index 3964d18a2..5efcfde50 100644 --- a/appinfo.js +++ b/appinfo.js @@ -45,7 +45,7 @@ var AppInfo = { storageFile.cmd = `\x10require('Storage').write(${toJS(storageFile.name)},${js});`; }); resolve(fileContents); - }); + }).catch(err => reject(err)); }); }, }; diff --git a/apps.json b/apps.json index 4137fbc5e..7f1862014 100644 --- a/apps.json +++ b/apps.json @@ -82,6 +82,28 @@ {"name":"*astroid","url":"asteroids-icon.js","evaluate":true} ] }, + { "id": "clickms", + "name": "Click Master", + "icon": "click-master.png", + "description": "Get several friends to start the game, then compete to see who can press BTN1 the most!", + "tags": "game", + "storage": [ + {"name":"+clickms","url":"click-master.json"}, + {"name":"-clickms","url":"click-master.js"}, + {"name":"*clickms","url":"click-master-icon.js","evaluate":true} + ] + }, + { "id": "horsey", + "name": "Horse Race!", + "icon": "horse-race.png", + "description": "Get several friends to start the game, then compete to see who can press BTN1 the most!", + "tags": "game", + "storage": [ + {"name":"+horsey","url":"horse-race.json"}, + {"name":"-horsey","url":"horse-race.js"}, + {"name":"*horsey","url":"horse-race-icon.js","evaluate":true} + ] + }, { "id": "compass", "name": "Compass", "icon": "compass.png", @@ -213,6 +235,17 @@ {"name":"*hidkbd","url":"hid-keyboard-icon.js","evaluate":true} ] }, + { "id": "hidbkbd", + "name": "Binary Bluetooth Keyboard", + "icon": "hid-binary-keyboard.png", + "description": "Enable HID in settings, pair with your phone/PC, then type messages using the onscreen keyboard by tapping repeatedly on the key you want", + "tags": "bluetooth", + "storage": [ + {"name":"+hidbkbd","url":"hid-binary-keyboard.json"}, + {"name":"-hidbkbd","url":"hid-binary-keyboard.js"}, + {"name":"*hidbkbd","url":"hid-binary-keyboard-icon.js","evaluate":true} + ] + }, { "id": "animals", "name": "Animals Game", "icon": "animals.png", @@ -350,6 +383,30 @@ {"name":"*pparrot","url":"party-parrot-icon.js","evaluate":true} ] }, + { "id": "hrings", + "name": "Hypno Rings", + "icon": "hypno-rings.png", + "description": "Experiment with trippy rings, press buttons for change", + "tags": "rings,hypnosis,psychadelic", + "type":"app", + "storage": [ + {"name":"+hrings","url":"hypno-rings.json"}, + {"name":"-hrings","url":"hypno-rings.js"}, + {"name":"*hrings","url":"hypno-rings-icon.js","evaluate":true} + ] + }, + { "id": "morse", + "name": "Morse Code", + "icon": "morse-code.png", + "description": "Learn morse code by hearing/seeing/feeling the code. Tap to toggle buzz!", + "tags": "morse,sound,visual,input", + "type":"app", + "storage": [ + {"name":"+morse","url":"morse-code.json"}, + {"name":"-morse","url":"morse-code.js"}, + {"name":"*morse","url":"morse-code-icon.js","evaluate":true} + ] + }, { "id": "blescan", "name": "BLE Scanner", @@ -408,5 +465,42 @@ {"name":"-miclock","url":"clock-mixed.js"}, {"name":"*miclock","url":"clock-mixed-icon.js","evaluate":true} ] + }, + { "id": "bclock", + "name": "Binary Clock", + "icon": "clock-binary.png", + "description": "A simple binary clock watch face", + "tags": "clock", + "type":"clock", + "storage": [ + {"name":"+bclock","url":"clock-binary.json"}, + {"name":"-bclock","url":"clock-binary.js"}, + {"name":"*bclock","url":"clock-binary-icon.js","evaluate":true} + ] + }, + { "id": "clotris", + "name": "Clock-Tris", + "icon": "clock-tris.png", + "description": "A fully functional clone of a classic game of falling blocks", + "tags": "game", + "storage": [ + {"name":"+clotris","url":"clock-tris.json"}, + {"name":"-clotris","url":"clock-tris.js"}, + {"name":"*clotris","url":"clock-tris-icon.js","evaluate":true}, + {"name":".trishig","url":"clock-tris-high"} + ] + }, + { + "id": "gpsinfo", + "name": "GPS Info", + "icon": "gps-info.png", + "description": "An application that displays information about altitude, lat/lon, satellites and time", + "tags": "gps", + "type": "app", + "storage": [ + {"name": "+gpsinfo","url": "gps-info.json"}, + {"name": "-gpsinfo","url": "gps-info.js"}, + {"name": "*gpsinfo","url": "gps-info-icon.js","evaluate": true} + ] } -] +] \ No newline at end of file diff --git a/apps/bootloader.js b/apps/bootloader.js index f2895a14c..14b9ef440 100644 --- a/apps/bootloader.js +++ b/apps/bootloader.js @@ -73,7 +73,7 @@ if (startapp) { setWatch(function() { // run if (!apps[selected].src) return; clearWatch(); - g.clear(); + g.clear(1); g.setFont("6x8",2); g.setFontAlign(0,0); g.drawString("Loading...",120,120); @@ -84,6 +84,10 @@ if (startapp) { // re-add the menu button if we're going to the clock if (apps[selected].type=="clock") { setWatch(displayMenu, BTN2, {repeat:false,edge:"falling"}); + WIDGETPOS={tl:32,tr:g.getWidth()-32,bl:32,br:g.getWidth()-32}; + WIDGETS={}; + require("Storage").list().filter(a=>a[0]=='=').forEach(widget=>eval(require("Storage").read(widget))); + setTimeout(drawWidgets,100); } else { delete WIDGETS; delete WIDGETPOS; @@ -95,15 +99,15 @@ if (startapp) { var WIDGETPOS={tl:32,tr:g.getWidth()-32,bl:32,br:g.getWidth()-32}; var WIDGETS={}; function drawWidgets() { - Object.keys(WIDGETS).forEach(k=>WIDGETS[k].draw()); + for (var w of WIDGETS) w.draw(); } - var clockApp = require("Storage").list().filter(a=>a[0]=='+').map(app=>{ + var clockApps = require("Storage").list().filter(a=>a[0]=='+').map(app=>{ try { return require("Storage").readJSON(app); } catch (e) {} - }).find(app=>app.type=="clock"); - if (clockApp) eval(require("Storage").read(clockApp.src)); + }).filter(app=>app.type=="clock").sort((a, b) => a.sortorder - b.sortorder); + if (clockApps && clockApps.length > 0) eval(require("Storage").read(clockApps[0].src)); else E.showMessage("No Clock Found"); - delete clockApp; + delete clockApps; require("Storage").list().filter(a=>a[0]=='=').forEach(widget=>eval(require("Storage").read(widget))); setTimeout(drawWidgets,100); } diff --git a/apps/click-master-icon.js b/apps/click-master-icon.js new file mode 100644 index 000000000..e59c4990f --- /dev/null +++ b/apps/click-master-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AHmIAAIWThAXCwAuVGCYuCkQwTCYOE7tIGCQSBkf/mgXUof/nsoF6kzJAQXSlvd7vSC4/d6B3L7vUC40NBQKnJwQMB7sgFwwXJGAVNBoI+ECoQJGGApgCBoZECC5YwBMAQ+C6AXFJBJWFCgoXQ6AXIJBJZGC7xIOC8Jg/C9ZIMC8RIMFhIXMU5HTnoLBJBQXIn/zRykNC5ruJn5HBRqnT9q8hXZalKCxoyICx4yFRJIA/AH4AQA==")) diff --git a/apps/click-master.js b/apps/click-master.js new file mode 100644 index 000000000..b9f76364b --- /dev/null +++ b/apps/click-master.js @@ -0,0 +1,50 @@ +var ME = "-= ME =-"; +var mycounter = 0; +var players = {}; +setWatch(x=>{ + mycounter++; + updateAdvertising(); +},BTN1,{repeat:true}); + +function updateAdvertising() { +try { + NRF.setAdvertising({},{ + manufacturer: 0x0590, + manufacturerData: new Uint8Array([mycounter>>8,mycounter&255]) + }); +} catch(e){} +} + +function drawPlayers() { + g.clear(1); + g.setFont("6x8",2); + var l = [{name:ME,cnt:mycounter}]; + for (p of players) l.push(p); + l.sort((a,b)=>a.cnt-b.cnt); + var y=0; + l.forEach(player=>{ + if (player.name==ME) g.setColor(1,0,0); + else g.setColor(1,1,1); + g.drawString(player.name,10,y); + g.drawString(player.cnt,180,y); + y+=16; + }); +} + +function doScan() { + NRF.findDevices(devs=>{ + devs.forEach(dev => { + players[dev.id] = { + name : dev.id.substr(12,5), + cnt : (dev.manufacturerData[0]<<8)|dev.manufacturerData[1] + }; + }); + drawPlayers(); + doScan(); + },{timeout : 1000, filters : [{ manufacturerData:{0x0590:{}} }] }); +} + +drawPlayers(); +try { NRF.wake(); } catch (e) {} +doScan(); +updateAdvertising(); diff --git a/apps/click-master.json b/apps/click-master.json new file mode 100644 index 000000000..6a2874259 --- /dev/null +++ b/apps/click-master.json @@ -0,0 +1,5 @@ +{ + "name":"Click Master", + "icon": "*clickms", + "src":"-clickms" +} diff --git a/apps/click-master.png b/apps/click-master.png new file mode 100644 index 000000000..bff973976 Binary files /dev/null and b/apps/click-master.png differ diff --git a/apps/clock-analog.js b/apps/clock-analog.js index dfd1a7ddb..427a03db0 100644 --- a/apps/clock-analog.js +++ b/apps/clock-analog.js @@ -1,67 +1,99 @@ -g.clear(); - -var minuteDate = new Date(); -var secondDate = new Date(); - -function seconds(angle, r) { - var a = angle*Math.PI/180; - var x = 120+Math.sin(a)*r; - var y = 120-Math.cos(a)*r; - g.fillRect(x-1,y-1,x+1,y+1); -} -function hand(angle, r1,r2) { - var a = angle*Math.PI/180; - var r3 = 3; - var p = Math.PI/2; - g.fillPoly([ - 120+Math.sin(a)*r1, - 120-Math.cos(a)*r1, - 120+Math.sin(a+p)*r3, - 120-Math.cos(a+p)*r3, - 120+Math.sin(a)*r2, - 120-Math.cos(a)*r2, - 120+Math.sin(a-p)*r3, - 120-Math.cos(a-p)*r3]); -} - -function drawAll() { +(function(){ g.clear(); - g.setColor(0,0,0.6); - for (var i=0;i<60;i++) - seconds(360*i/60, 90); - secondDate = minuteDate = new Date(); - onSecond(); - onMinute(); -} - -function onSecond() { - g.setColor(0,0,0.6); - seconds(360*secondDate.getSeconds()/60, 90); - g.setColor(1,0,0); - secondDate = new Date(); - seconds(360*secondDate.getSeconds()/60, 90); - g.setColor(1,1,1); - -} - -function onMinute() { - g.setColor(0,0,0); - hand(360*minuteDate.getHours()/12, -10, 50); - hand(360*minuteDate.getMinutes()/60, -10, 82); - oldMinute = new Date(); - g.setColor(1,1,1); - hand(360*minuteDate.getHours()/12, -10, 50); - hand(360*minuteDate.getMinutes()/60, -10, 82); -} - -setInterval(onSecond,1000); -setInterval(onMinute,60*1000); -drawAll(); - -Bangle.on('lcdPower',function(on) { - if (on) { - g.clear(); - drawAll(); - drawWidgets(); + const p = Math.PI/2; + const PRad = Math.PI/180; + + let intervalRefMin = null; + let intervalRefSec = null; + + let minuteDate = new Date(); + let secondDate = new Date(); + + function seconds(angle, r) { + const a = angle*PRad; + const x = 120+Math.sin(a)*r; + const y = 120-Math.cos(a)*r; + g.fillRect(x-1,y-1,x+1,y+1); } -}); + function hand(angle, r1,r2) { + const a = angle*PRad; + const r3 = 3; + g.fillPoly([ + 120+Math.sin(a)*r1, + 120-Math.cos(a)*r1, + 120+Math.sin(a+p)*r3, + 120-Math.cos(a+p)*r3, + 120+Math.sin(a)*r2, + 120-Math.cos(a)*r2, + 120+Math.sin(a-p)*r3, + 120-Math.cos(a-p)*r3]); + } + + function drawAll() { + g.clear(); + g.setColor(0,0,0.6); + for (let i=0;i<60;i++) + seconds(360*i/60, 90); + secondDate = minuteDate = new Date(); + onSecond(); + onMinute(); + } + + function onSecond() { + g.setColor(0,0,0.6); + seconds(360*secondDate.getSeconds()/60, 90); + g.setColor(1,0,0); + secondDate = new Date(); + seconds(360*secondDate.getSeconds()/60, 90); + g.setColor(1,1,1); + + } + + function onMinute() { + g.setColor(0,0,0); + hand(360*minuteDate.getHours()/12, -10, 50); + hand(360*minuteDate.getMinutes()/60, -10, 82); + minuteDate = new Date(); + g.setColor(1,1,1); + hand(360*minuteDate.getHours()/12, -10, 50); + hand(360*minuteDate.getMinutes()/60, -10, 82); + if(minuteDate.getHours() >= 0 && minuteDate.getMinutes() === 0) { + Bangle.buzz(); + } + } + + function clearTimers() { + if(intervalRefMin) {clearInterval(intervalRefMin);} + if(intervalRefSec) {clearInterval(intervalRefSec);} + } + + function startTimers() { + minuteDate = new Date(); + secondDate = new Date(); + intervalRefSec = setInterval(onSecond,1000); + intervalRefMin = setInterval(onMinute,60*1000); + drawAll(); + } + + startTimers(); + + Bangle.on('lcdPower',function(on) { + if (on) { + g.clear(); + startTimers(); + drawWidgets(); + }else { + clearTimers(); + } + }); + + Bangle.on('gesture',function(gesture){ + if (gesture && !Bangle.isLCDOn()) { + clearTimers(); + Bangle.setLCDTimeout(30); + Bangle.setLCDPower(true); + } + }); + + })(); + \ No newline at end of file diff --git a/apps/clock-binary-icon.js b/apps/clock-binary-icon.js new file mode 100644 index 000000000..1c167ff57 --- /dev/null +++ b/apps/clock-binary-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH8AAAAAAMGAAAAAAYDAAAAAAwBgAAAABgAwAAAABAAQAAAABAAQAAAABAAQAAAABAAQAAAABAAQAAAABgAwAAAAAwBgAAAAAYDAAAAAAMGAAAAAAH8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH8AAAAAAP+AAAAAAf/AAAAAA//gAAAAB//wAAAAB//wAAAAB//wAAAAB//wAAAAB//wAAAAB//wAAAAB//wAAAAA//gAAAAAf/AAAAAAP+AAAAAAH8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")) \ No newline at end of file diff --git a/apps/clock-binary.js b/apps/clock-binary.js new file mode 100644 index 000000000..1ece14b81 --- /dev/null +++ b/apps/clock-binary.js @@ -0,0 +1,106 @@ +(() => { + const canvasWidth = 290; + const numberOfColumns = 6; + const drawFullGrid = false; + + const colpos = canvasWidth / numberOfColumns - 10; + const binSize = (canvasWidth / numberOfColumns) / 3; + + const findBinary = target => { + return [ + [0, 0, 0, 0], // 0 + [1, 0, 0, 0], // 1 + [0, 1, 0, 0], // 2 + [1, 1, 0, 0], // 3 + [0, 0, 1, 0], // 4 + [1, 0, 1, 0], // 5 + [0, 1, 1, 0], // 6 + [1, 1, 1, 0], // 7 + [0, 0, 0, 1], // 8 + [1, 0, 0, 1], // 9 + ][target]; + }; + + const getCurrentTime = () => { + const flattenArray = (array = []) => [].concat.apply([], array); + const format = number => { + const numberStr = number.toString(); + return numberStr.length === 1 ? ["0", numberStr] : numberStr.split(""); + }; + const now = new Date(); + return flattenArray([now.getHours(), now.getMinutes(), now.getSeconds()].map(format)); + }; + + let prevFrame = []; + const drawColumn = (position = 0, column = [0, 0, 0, 0]) => { + const maxDotsPerColumn = [2, 4, 3, 4, 3, 4]; + + const columnPos = position * colpos; + let pos = colpos / 2 + 45; + const frame = column.reverse(); + const drawDot = fn => g[fn]((columnPos + colpos / 2), pos, binSize); + + for (let i = 0; i < frame.length; i += 1) { + if (i + maxDotsPerColumn[position] >= 4 || drawFullGrid) { + if (prevFrame && prevFrame[position] && prevFrame[position][i]) { + if (frame[i] !== prevFrame[position][i]) { + // subsequent draw + g.clearRect((columnPos + colpos / 2) - 15, pos - 15, (columnPos + colpos / 2) + 20, pos + 20); + if (frame[i]) { + drawDot('fillCircle'); + } else { + drawDot('drawCircle'); + } + } + } else { + // First draw + if (frame[i]) { + drawDot('fillCircle'); + } else { + drawDot('drawCircle'); + } + } + } + pos += colpos; + } + }; + + const drawClock = () => { + const data = getCurrentTime().map(findBinary); + for (let i = 0; i < data.length; i += 1) { + drawColumn(i, data[i]); + } + prevFrame = data; + }; + + // Themes + const drawTheme = (idx) => () => { + idx += 1; + const themes = [ + [[0, 0, 0], [1, 1, 1]], + [[1, 1, 1], [0, 0, 0]], + [[0, 0, 0], [1, 0, 0]], + [[0, 0, 0], [0, 1, 0]], + [[0, 0, 0], [0, 0, 1]], + ]; + if (idx >= themes.length) idx = 0; + const color = themes[idx]; + g.setBgColor.apply(g, color[0]); + g.setColor.apply(g, color[1]); + g.clear(); + }; + + const nextTheme = drawTheme(0); + setWatch(() => { + prevFrame = []; + Bangle.beep(); + nextTheme(); + }, BTN1, { repeat: true }); + + Bangle.on('lcdPower', on => { + if (on) drawClock(); + }); + + g.clear(); + setInterval(() => { drawClock(); }, 1000); +})(); \ No newline at end of file diff --git a/apps/clock-binary.json b/apps/clock-binary.json new file mode 100644 index 000000000..c00dd9d76 --- /dev/null +++ b/apps/clock-binary.json @@ -0,0 +1,7 @@ +{ + "name":"Binary Clock", + "type":"clock", + "icon":"*bclock", + "src":"-bclock" + } + \ No newline at end of file diff --git a/apps/clock-binary.png b/apps/clock-binary.png new file mode 100644 index 000000000..21717834b Binary files /dev/null and b/apps/clock-binary.png differ diff --git a/apps/clock-tris-high b/apps/clock-tris-high new file mode 100644 index 000000000..c22708346 --- /dev/null +++ b/apps/clock-tris-high @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/apps/clock-tris-icon.js b/apps/clock-tris-icon.js new file mode 100644 index 000000000..d24eac516 --- /dev/null +++ b/apps/clock-tris-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwiBC/AH4A/AHOQzYBNJ/5f/L/5P/L/5f/J/5f/L/5P/L/4A/AH6/haP5f/L/5f/L/5f/L/5f/L/5f/L/4A/AFP/ABy/lL/5f/L/5f/L/5f/L/5f/L/5f/L/4A/VtK/jL/5f/L/5f/L/5f/L/5f/L/5f/L/4A/X/7RxEa5f/L/5f/L/5f/L/5f/L/5f/L/5ffAH4A/AHYA=")) \ No newline at end of file diff --git a/apps/clock-tris.js b/apps/clock-tris.js new file mode 100644 index 000000000..12fee4027 --- /dev/null +++ b/apps/clock-tris.js @@ -0,0 +1,309 @@ +Bangle.setLCDMode("doublebuffered"); + +const storage = require("Storage"); + +var BTN_L = BTN1; +var BTN_R = BTN3; +var BTN_ROT = BTN2; +var BTN_DOWN = BTN5; +var BTN_PAUSE = BTN4; + +const W = g.getWidth(); +const H = g.getHeight(); +const CX = W / 2; +const CY = H / 2; + +const HEIGHT_BUFFER = 4; + +const LINES = 20; +const COLUMNS = 11; +const CELL_SIZE = Math.floor((H - HEIGHT_BUFFER) / (LINES + 1)); + +const BOARD_X = Math.floor((W - CELL_SIZE * COLUMNS) / 2) + 2; +const BOARD_Y = Math.floor((H - CELL_SIZE * (LINES + 1)) / 2); +const BOARD_W = COLUMNS * CELL_SIZE; +const BOARD_H = LINES * CELL_SIZE; + +const TEXT_X = BOARD_X + BOARD_W + 10; + +const BLOCKS = [ + [ + [2, 7], + [2, 6, 2], + [0, 7, 2], + [2, 3, 2] + ], + [ + [1, 3, 2], + [6, 3] + ], + [ + [2, 3, 1], + [3, 6] + ], + [ + [2, 2, 6], + [0, 7, 1], + [3, 2, 2], + [4, 7] + ], + [ + [2, 2, 3], + [1, 7], + [6, 2, 2], + [0, 7, 4] + ], + [ + [2, 2, 2, 2], + [0, 15] + ], + [[3, 3]] +]; + +const COLOR_WHITE = 0b1111111111111111; +const COLOR_BLACK = 0b0000000000000000; + +const BLOCK_COLORS = [ + //0brrrrrggggggbbbbb + 0b0111100000001111, + 0b0000011111100000, + 0b1111100000000011, + 0b0111100111100000, + 0b0000000000011111, + 0b0000001111111111, + 0b1111111111100000 +]; + +const EMPTY_LINE = 0b00000000000000; +const BOUNDARY = 0b10000000000010; +const FULL_LINE = 0b01111111111100; + +let gameOver = false; +let paused = false; +let currentBlock = 0; +let nextBlock = 0; +let x, y; +let points; +let level; +let lines; +let board; +let rotation = 0; +let ticker = null; +let needDraw = true; +let highScore = parseInt(storage.read(".trishig") || 0, 10); + +function getBlock(a, c, d) { + const block = BLOCKS[a % 7]; + return block[(a + c) % block.length]; +} + +function drawBlock(block, screenX, screenY, x, y) { + for (let row in block) { + let mask = block[row]; + for (let col = 0; mask; mask >>= 1, col++) { + if (mask % 2) { + const dx = screenX + (x + col) * CELL_SIZE; + const dy = screenY + (y + row) * CELL_SIZE; + g.fillRect(dx, dy, dx + CELL_SIZE - 3, dy + CELL_SIZE - 3); + } + } + } +} + +function drawBoard() { + g.setColor(COLOR_WHITE); + g.drawRect(BOARD_X - 3, BOARD_Y - 3, BOARD_X + BOARD_W, BOARD_Y + BOARD_H); + drawBlock(board, BOARD_X, BOARD_Y, -2, 0); + + g.setColor(BLOCK_COLORS[currentBlock]); + drawBlock(getBlock(currentBlock, rotation), BOARD_X, BOARD_Y, x - 2, y); +} + +function drawNextBlock() { + g.setFontAlign(0, -1, 0); + g.setColor(COLOR_WHITE); + g.drawString("NEXT BLOCK", BOARD_X / 2, 10); + g.setColor(BLOCK_COLORS[nextBlock]); + drawBlock(getBlock(nextBlock, 0), BOARD_X / 2 - 2 * CELL_SIZE, 25, 0, 0); +} + +function drawTextLine(text, line) { + g.drawString(text, TEXT_X, 10 + line * 15); +} + +function drawGameState() { + g.setFontAlign(-1, -1, 0); + g.setColor(COLOR_WHITE); + let ln = 0; + drawTextLine("CLOCK-TRIS", ln++); + ln++; + drawTextLine("LVL " + level, ln++); + drawTextLine("LNS " + lines, ln++); + drawTextLine("PTS " + points, ln++); + drawTextLine("TOP " + highScore, ln++); +} + +function drawBanner(text) { + g.setFontAlign(0, 0, 0); + g.setColor(COLOR_BLACK); + g.fillRect(CX - 46, CY - 11, CX + 46, CY + 9); + g.setColor(COLOR_WHITE); + g.drawRect(CX - 45, CY - 10, CX + 45, CY + 8); + g.drawString(text, CX, CY); +} + +function drawPaused() { + drawBanner("PAUSED"); +} + +function drawGameOver() { + drawBanner("GAME OVER"); +} + +function draw() { + g.clear(); + g.setFont("6x8"); + drawBoard(); + drawNextBlock(); + drawGameState(); + if (paused) { + drawPaused(); + } + if (gameOver) { + drawGameOver(); + } + g.flip(); +} + +function getNextBlock() { + currentBlock = nextBlock; + nextBlock = (Math.random() * BLOCKS.length) | 0; + x = 6; + y = 0; + rotation = 0; +} + +function landBlock(a) { + const block = getBlock(currentBlock, rotation); + for (let row in block) { + board[y + (row | 0)] |= block[row] << x; + } + + let clearedLines = 0; + let keepLine = LINES; + for (let line = LINES - 1; line >= 0; line--) { + if (board[line] === FULL_LINE) { + clearedLines++; + } else { + board[--keepLine] = board[line]; + } + } + + lines += clearedLines; + if (lines > level * 10) { + level++; + setSpeed(); + } + + while (--keepLine > 0) { + board[keepLine] = EMPTY_LINE; + } + if (clearedLines) { + points += 100 * (1 << (clearedLines - 1)); + needDraw = true; + } + + getNextBlock(); + if (!checkMove(0, 0, 0)) { + gameOver = true; + needDraw = true; + highScore = Math.max(points, highScore); + storage.write(".trishig", highScore.toString()); + } +} + +function checkMove(dx, dy, rot) { + if (gameOver) { + startGame(); + return; + } + if (paused) { + return; + } + const block = getBlock(currentBlock, rotation + rot); + for (const row in block) { + const movedBlockRow = block[row] << (x + dx); + if ( + row + y === LINES - 1 || + movedBlockRow & board[y + dy + row] || + movedBlockRow & BOUNDARY + ) { + if (dy) { + landBlock(); + } + return false; + } + } + rotation += rot; + x += dx; + y += dy; + needDraw = true; + return true; +} + +function drawLoop() { + if (needDraw) { + needDraw = false; + draw(); + } + setTimeout(drawLoop, 10); +} + +function gameTick() { + if (!gameOver) { + checkMove(0, 1, 0); + } +} + +function setSpeed() { + if (ticker) { + clearInterval(ticker); + } + ticker = setInterval(gameTick, 1000 - level * 100); +} + +function togglePause() { + if (!gameOver) { + paused = !paused; + needDraw = true; + } +} + +function startGame() { + board = []; + for (let i = 0; i < LINES; i++) { + board[i] = EMPTY_LINE; + } + + gameOver = false; + points = 0; + lines = 0; + level = 0; + getNextBlock(); + setSpeed(); + needDraw = true; +} + +function bindButton(btn, dx, dy, r) { + setWatch(checkMove.bind(null, dx, dy, r), btn, { repeat: true }); +} + +bindButton(BTN_L, -1, 0, 0); +bindButton(BTN_R, 1, 0, 0); +bindButton(BTN_ROT, 0, 0, 1); +bindButton(BTN_DOWN, 0, 1, 0); + +setWatch(togglePause, BTN_PAUSE, { repeat: true }); + +startGame(); +drawLoop(); diff --git a/apps/clock-tris.json b/apps/clock-tris.json new file mode 100644 index 000000000..ddbb7d10c --- /dev/null +++ b/apps/clock-tris.json @@ -0,0 +1,5 @@ +{ + "name":"Clock-Tris", + "icon":"*clotris", + "src":"-clotris" +} \ No newline at end of file diff --git a/apps/clock-tris.png b/apps/clock-tris.png new file mode 100644 index 000000000..841182df4 Binary files /dev/null and b/apps/clock-tris.png differ diff --git a/apps/gps-info-icon.js b/apps/gps-info-icon.js new file mode 100644 index 000000000..b3f2dd3d4 --- /dev/null +++ b/apps/gps-info-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwghC/AH4AKg9wC6t3u4uVC6wWBI6t3uJeVuMQCqcBLisAi4XLxAABFxAXKgc4DBAuBRhQXEDAq7MmYXEwBHEXZYXFGAOqAAKDMmczC4mIC62CC50PC4JIBkQABiIvRmURAAUSjQXSFwMoxGKC6CRFwUSVYgXLPIgXXwMYegoXLJAYXCGBnzGA0hPQIwMgYwGC6gwCC4ZIMC4gYBC604C4ZISmcRVgapQAAMhC6GIJIwXCMBcIxGDDBAuLC4IwGAARGMAAQWGmAXPJQoWMC4pwCCpoXJAB4XXAH4A/ABQA=")) diff --git a/apps/gps-info.js b/apps/gps-info.js new file mode 100644 index 000000000..d452cd191 --- /dev/null +++ b/apps/gps-info.js @@ -0,0 +1,55 @@ +var img = require("heatshrink").decompress(atob("mEwghC/AH4AKg9wC6t3u4uVC6wWBI6t3uJeVuMQCqcBLisAi4XLxAABFxAXKgc4DBAuBRhQXEDAq7MmYXEwBHEXZYXFGAOqAAKDMmczC4mIC62CC50PC4JIBkQABiIvRmURAAUSjQXSFwMoxGKC6CRFwUSVYgXLPIgXXwMYegoXLJAYXCGBnzGA0hPQIwMgYwGC6gwCC4ZIMC4gYBC604C4ZISmcRVgapQAAMhC6GIJIwXCMBcIxGDDBAuLC4IwGAARGMAAQWGmAXPJQoWMC4pwCCpoXJAB4XXAH4A/ABQA=")) + +Bangle.setGPSPower(1); +Bangle.setLCDMode("doublebuffered"); + +var lastFix = { + fix: 0, + alt: 0, + lat: 0, + lon: 0, + time: 0, + satellites: 0 +}; + +function formatTime(now) { + var fd = now.toUTCString().split(" "); + var time = fd[4].substr(0, 5); + var date = [fd[0], fd[1], fd[2]].join(" "); + var year = now.getFullYear(); + return time + " - " + date; +} + +function onGPS(fix) { + lastFix = fix; + g.clear(); + g.setFontAlign(-1, -1); + g.drawImage(img, 30, -6); + g.setFont("6x8"); + g.setFontVector(22); + g.drawString("GPS Info", 80, 6); + if (fix.fix) { + var alt = fix.alt; + var lat = fix.lat; + var lon = fix.lon; + var time = formatTime(fix.time); + var satellites = fix.satellites; + + var s = 12; + g.setFontVector(s+4); + g.drawString("Altitude: "+alt+" m",0,60); + g.setFontVector(s); + g.drawString("Lat: "+lat, 0, 60+20+s/2); + g.drawString("Lon: "+lon,0,60+40+s/2); + g.drawString("Time: "+time, 0, 60+60+s/2); + g.drawString("Satellites: "+satellites,0,60+80+s/2); + + } else { + g.setFontAlign(0, 1); + g.setFont("6x8", 2); + g.drawString("Waiting for GPS", 120, 80); + } + g.flip(); +} + +Bangle.on('GPS', onGPS); diff --git a/apps/gps-info.json b/apps/gps-info.json new file mode 100644 index 000000000..b86b11d58 --- /dev/null +++ b/apps/gps-info.json @@ -0,0 +1,6 @@ +{ + "name": "GPS Info", + "type": "app", + "icon": "*gpsinfo", + "src": "-gpsinfo" +} diff --git a/apps/gps-info.png b/apps/gps-info.png new file mode 100644 index 000000000..970e85139 Binary files /dev/null and b/apps/gps-info.png differ diff --git a/apps/horse-race-icon.js b/apps/horse-race-icon.js new file mode 100644 index 000000000..23a974ef3 --- /dev/null +++ b/apps/horse-race-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/AH4AVvNPp95F1tPqujF9IuCvPO5wspAAXHF84uFp4uBL84xEvPN5q+rqtiCBl6F7tiAAY9LvQDBF86cDvQvCGLYvEGAr7EF4IwDF7c4GQwuERwIACqpecrlcF4lWLw4ACF7s3F58rGDIvEA4VcFwtWL4ovSCwwvHFoiOHGCQXHXYdcBwQuIDIwsMI5QwEGIKNERgh6JFpIuKAAOAGIYvCqpYJAwUyFxaNIGAovEXBheOF5pfCrl6RYjoTVAYvMRwYOKF76NDwAveGBaNEF8AwLFzgvHeRovoqtWFxtVDQbwSF44KBqouLDYzAYqz8OPg5gSD6K9FGCIvJVoIdLdoxgUDop9NF7gcEPZwvZJgwvnbiwTHF54bLMCEsAAIvVTRBEOF7zBSF7StPGDB1HPpyMVDAgZFbxztWAH4A/AGw")) diff --git a/apps/horse-race.js b/apps/horse-race.js new file mode 100644 index 000000000..170ca22f2 --- /dev/null +++ b/apps/horse-race.js @@ -0,0 +1,85 @@ +Bangle.setLCDMode("doublebuffered"); +var img = { + width : 48, height : 48, bpp : 8, + transparent : 254, + buffer : require("heatshrink").decompress(atob("/wA/AH4A/ACl5p9PvIutp9V0YvpFwV553OFlIAC44vnFwtPFwJfnGIl55vNX1dVsQQMvQvdsQADHpd6AYIvnTgd6F4QxbF4gwFfYgvBGAYvbnAyGFwiOBAAVVLzlcrgvEqxeHAAQvdm4vPlYwZF4gHCrguFqxfFF6QWGF44tERw4wSC467DrgOCFxAZGFhhHKGAgxBRoiMEPRItJFxQABwAxDF4VVLBIGCmQuLRpAwFF4i4MLxwvNL4VcvSLEdCaoDF5iODBxQvfRoeAF7wwLRogvgGBYucF47yNF9FVqwuNqoaDeCQvHBQNVFxYbGYDFWfhx8HMCQfRXoowRF5KtBDpbtGMCgdFPpovcDgh7OF7JMGF87cWCY4vPDZZgQlgABF6qaIIhwveYKQvaVp4wYOo59ORioYEDIreOdqwA/AH4A2A")) +}; +// ideally we'd just load the image file but it looks like NodeConf +// Bangle.js firmware had bug which meant that transparency in image +// strings wasn't used +//var img = require("Storage").read("*horsey"); +var mycounter = 0; +var players = {}; +setWatch(x=>{ + mycounter++; + updateAdvertising(); +},BTN1,{repeat:true}); + +function updateAdvertising() { +try { + NRF.setAdvertising({},{ + manufacturer: 0x0590, + manufacturerData: new Uint8Array([mycounter>>8,mycounter&255]), + interval: 60 + }); +} catch(e){} +} + +function drawPlayers() { + g.setColor(0,0.3,0); + g.fillRect(0,0,240,160); + g.setColor(1,1,1); + g.setFont("6x8"); + g.setFontAlign(0,0); + var max = mycounter; + var playerCount=0; + for (var player of players) { + max = Math.max(player.cnt, mycounter); + playerCount++; + } + var offset = 0; + if (max > 200) + offset = max-200; + + var d = 63 - (offset&63); + g.fillRect(0,10,240,12); + for (var x=d;x<240;x+=64) + g.fillRect(x,12,x+2,12+20); + var y = 20; + var p = mycounter-offset; + g.drawString("You",p-16,y+20); + g.drawImage(img, p,y); + + var spacing = (120-20)/(playerCount+1); + for (var player of players) { + y+=45; + var p = player.cnt-offset; + g.drawString(player.name,p-16,y+20); + g.drawImage(img, p,y); + } + + g.fillRect(0,150,240,152); + for (var x=d;x<240;x+=64) + g.fillRect(x,152,x+2,160); + g.flip(); +} + +function doScan() { + NRF.findDevices(devs=>{ + devs.forEach(dev => { + players[dev.id] = { + name : dev.id.substr(12,5), + cnt : (dev.manufacturerData[0]<<8)|dev.manufacturerData[1] + }; + }); + drawPlayers(); + doScan(); + },{timeout : 250, filters : [{ manufacturerData:{0x0590:{}} }] }); +} + +drawPlayers(); +try { NRF.wake(); } catch (e) {} +doScan(); +setInterval(drawPlayers, 100); + +updateAdvertising(); diff --git a/apps/horse-race.json b/apps/horse-race.json new file mode 100644 index 000000000..cf276e2ac --- /dev/null +++ b/apps/horse-race.json @@ -0,0 +1,5 @@ +{ + "name":"Horse Race", + "icon": "*horsey", + "src":"-horsey" +} diff --git a/apps/horse-race.png b/apps/horse-race.png new file mode 100644 index 000000000..2cef05005 Binary files /dev/null and b/apps/horse-race.png differ diff --git a/apps/hypno-rings-icon.js b/apps/hypno-rings-icon.js new file mode 100644 index 000000000..8d1f71a6a --- /dev/null +++ b/apps/hypno-rings-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwghC/AB8M5gAE4AWVDB4WCCAgHHCxIEBu4ACAoIYMCwUHCwYABuALDFxPACwQJDDAQMCFxAWHDAwuIBwQGBxAACDAYmCFxMAhAWDAAOADAIwIC4JuCFYYzDQAQXGAwIuCCwoYEEgLLGIwNwIoIHBgMRiMQDAWABoIRBIw8IK4IWCDAYJBJA6NCEoIWCBYYYCJAQSCLwpGCIYZLDJARgFC4RGDFwgwCJAYXFOweAgIuEGAUQC4Z4DAYJeEfY0AMAgXJIwxIDC/4X/U7DXW5gXDJA4GBC4fMC4t3hBgCGAkBLwWAu4XEgHMJ4JgDGAgdCLwQSCC4Z4BJAYYBAAQWBIwR2BC4h4EEoIYDJYRGBOwpgFBwIYCAAYHCLwxIFDAwFCIw4XDGARJBAAhnBFwQXGD4V3DAIrCGYYJCIwwwFDAQADA4IuIGAYYHCwQMCC44wBEQIYCAAaABFxIwDBgQWDEQYuJDAjLGCxgQEAAoWNDBAWPAH8AA")) diff --git a/apps/hypno-rings.js b/apps/hypno-rings.js new file mode 100644 index 000000000..356273d54 --- /dev/null +++ b/apps/hypno-rings.js @@ -0,0 +1,44 @@ +class Ring { + constructor() { + this.alive = true; + this.radius = 0; + this.color = [ + Math.random() > 0.5 ? 1 : 0, Math.random() > 0.5 ? 1 : 0, Math.random() > 0.5 ? 1 : 0 + ]; + } +} + +const LIMIT = 10; +const RADIUS_LIMIT = 240; +const pool = []; +let RANDOM = 0; +let BUFFER = 0; +let BUFFER_SPREAD = 20; + +const animate = () => { + if (pool.length < LIMIT && BUFFER === 0) { + const available = pool.filter(ring => !ring.alive); + const newRing = available.length ? available[0] : new Ring(); + pool.push(newRing); + BUFFER = BUFFER_SPREAD; + } + g.clear(); + if (BUFFER > 0) BUFFER--; + for (const ring of pool) { + if (ring.radius > RADIUS_LIMIT) { + ring.radius = 0; + ring.alive = false; + } else { + if (RANDOM) g.setColor(ring.color[0], ring.color[1], ring.color[2]); + else g.setColor(1, 1, 1); + g.drawCircle(120, 120, ring.radius++); + } + } + setTimeout(animate, 1000/60); +}; + +setWatch(() => (BUFFER_SPREAD += 5), BTN1, {repeat: true}); +setWatch(() => (RANDOM = !RANDOM), BTN2, {repeat: true}); +setWatch(() => (BUFFER_SPREAD -= 5), BTN3, {repeat: true}); + +animate(); \ No newline at end of file diff --git a/apps/hypno-rings.json b/apps/hypno-rings.json new file mode 100644 index 000000000..a21a84561 --- /dev/null +++ b/apps/hypno-rings.json @@ -0,0 +1,5 @@ +{ + "name":"Hypno Rings","type":"app", + "icon":"*hrings", + "src":"-hrings" +} \ No newline at end of file diff --git a/apps/hypno-rings.png b/apps/hypno-rings.png new file mode 100644 index 000000000..9d4323dd7 Binary files /dev/null and b/apps/hypno-rings.png differ diff --git a/apps/morse-code-icon.js b/apps/morse-code-icon.js new file mode 100644 index 000000000..b93a94dfa --- /dev/null +++ b/apps/morse-code-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkCIf4A/AH4A/AH4A/AA8RAAgHBgIHEiAXIp3uAAdQgEeA4ngC/4XoqlEAAaHBigHEoAQBgtVAAK4Thw2CC/4X/C8cBiIABC6YA/AH4A/AH4A/AHw")) diff --git a/apps/morse-code.js b/apps/morse-code.js new file mode 100644 index 000000000..72e58d6eb --- /dev/null +++ b/apps/morse-code.js @@ -0,0 +1,147 @@ +/** + * Teach a user morse code +*/ +/** + * Constants +*/ +const FONT_NAME = 'Vector12'; +const FONT_SIZE = 80; +const SCREEN_PIXELS = 240; +const UNIT = 100; +const MORSE_MAP = { + A: '.-', + B: '-...', + C: '-.-.', + D: '-..', + E: '.', + F: '..-.', + G: '--.', + H: '....', + I: '..', + J: '.---', + K: '-.-', + L: '.-..', + M: '--', + N: '-.', + O: '---', + P: '.--.', + Q: '--.-', + R: '.-.', + S: '...', + T: '-', + U: '..-', + V: '...-', + W: '.--', + X: '-..-', + Y: '-.--', + Z: '--..', + '1': '.----', + '2': '..---', + '3': '...--', + '4': '....-', + '5': '.....', + '6': '-....', + '7': '--...', + '8': '---..', + '9': '----.', + '0': '-----', +}; + +/** + * Set the local state +*/ +let INDEX = 0; +let BEEPING = false; +let BUZZING = true; +let UNIT_INDEX = 0; +let UNITS = MORSE_MAP[Object.keys(MORSE_MAP)[INDEX]].split(''); +/** + * Utility functions for writing text, changing state +*/ +const writeText = (txt) => { + g.clear(); + const width = g.stringWidth(txt); + g.drawString(txt, (SCREEN_PIXELS / 2) - (width / 2), SCREEN_PIXELS / 2); +}; +const writeLetter = () => { + writeText(Object.keys(MORSE_MAP)[INDEX]); +}; +const writeCode = () => { + writeText(MORSE_MAP[Object.keys(MORSE_MAP)[INDEX]]); +}; +const setUnits = () => { + UNITS = MORSE_MAP[Object.keys(MORSE_MAP)[INDEX]].split(''); +}; +/** + * Bootstrapping +*/ +g.clear(); +g.setFont(FONT_NAME, FONT_SIZE); +g.setColor(0, 1, 0); +g.setFontAlign(-1, 0, 0); +/** + * The length of a dot is one unit + * The length of a dash is three units + * The length of a space is one unit + * The space between letters is three units + * The space between words is seven units +*/ +const beepItOut = () => { + // If we are starting the beeps, use a timeout for pause of three units + const wait = UNIT_INDEX === 0 ? UNIT * 3 : 0; + setTimeout(() => { + Promise.all([ + Bangle.beep(UNITS[UNIT_INDEX] === '.' ? UNIT : 3 * UNIT), + // Could make buzz optional or switchable potentially + BUZZING ? Bangle.buzz(UNITS[UNIT_INDEX] === '.' ? UNIT : 3 * UNIT) : null + ]) + .then(() => { + if (UNITS[UNIT_INDEX + 1]) { + setTimeout(() => { + UNIT_INDEX++; + beepItOut(); + }, UNIT); + } else { + setTimeout(() => { + BEEPING = false; + UNIT_INDEX = 0; + writeLetter(); + }, 3 * UNIT); + } + }); + }, wait); +}; +const startBeep = () => { + if (BEEPING) return; + else { + BEEPING = true; + writeCode(); + beepItOut(); + } +}; + +const step = (positive) => () => { + if (BEEPING) return; + if (positive) { + INDEX = INDEX + 1; + if (INDEX > Object.keys(MORSE_MAP).length - 1) INDEX = 0; + } else { + INDEX = INDEX - 1; + if (INDEX < 0) INDEX = Object.keys(MORSE_MAP).length - 1; + } + setUnits(); + writeLetter(); +}; + +const toggleBuzzing = () => (BUZZING = !BUZZING); + +writeLetter(); + +// Press the middle button to hear the morse code translation +setWatch(startBeep, BTN2, { repeat: true }); +// Allow user to switch between letters +setWatch(step(true), BTN1, { repeat: true }); +setWatch(step(false), BTN3, { repeat: true }); +// Toggle buzzing/beeping with the touchscreen +setWatch(toggleBuzzing, BTN4, { repeat: true }); +setWatch(toggleBuzzing, BTN5, { repeat: true }); \ No newline at end of file diff --git a/apps/morse-code.json b/apps/morse-code.json new file mode 100644 index 000000000..bbd142c18 --- /dev/null +++ b/apps/morse-code.json @@ -0,0 +1,5 @@ +{ + "name":"Morse Code","type":"app", + "icon":"*morse", + "src":"-morse" +} \ No newline at end of file diff --git a/apps/morse-code.png b/apps/morse-code.png new file mode 100644 index 000000000..41e1b405f Binary files /dev/null and b/apps/morse-code.png differ diff --git a/apps/settings-init.js b/apps/settings-init.js index 661ae26ee..1390464fc 100644 --- a/apps/settings-init.js +++ b/apps/settings-init.js @@ -13,14 +13,14 @@ Bangle.HID = E.toUint8Array(atob("BQEJBqEBhQIFBxngKecVACUBdQGVCIEClQF1CIEBlQV1AQ } else delete Bangle.HID; } - setTimeout(function() { - NRF.setServices({}, adv); - if (s.ble) NRF.wake(); - else NRF.sleep(); - },10); - - if (!s.vibrate) Bangle.buzz=()=>Promise.resolve(); - if (!s.beep) Bangle.beep=()=>Promise.resolve(); + NRF.setServices({}, adv); + // we just reset, so BLE should be on + try { // disable advertising if BLE should be off + if (!s.ble) NRF.sleep(); + else NRF.wake(); + } catch(e) {} + if (!s.vibrate) Bangle.buzz=Promise.resolve; + if (!s.beep) Bangle.beep=Promise.resolve; Bangle.setLCDTimeout(s.timeout); if (!s.timeout) Bangle.setLCDPower(1); E.setTimeZone(s.timezone); diff --git a/comms.js b/comms.js index 96857ce32..9d103c246 100644 --- a/comms.js +++ b/comms.js @@ -63,5 +63,28 @@ setTime : () => { resolve(); }); }); +}, +disconnectDevice: () => { + var connection = Puck.getConnection(); + + if (!connection) return; + + connection.close(); +}, +watchConnectionChange : cb => { + var connected = Puck.isConnected(); + + //TODO Switch to an event listener when Puck will support it + var interval = setInterval(() => { + if (connected === Puck.isConnected()) return; + + connected = Puck.isConnected(); + cb(connected); + }, 1000); + + //stop watching + return () => { + clearInterval(interval); + }; } }; diff --git a/index.html b/index.html index 4d6e0691d..58e27a0ee 100644 --- a/index.html +++ b/index.html @@ -18,7 +18,7 @@ background: #ddd; } #toastcontainer { - position:absolute; + position:fixed; bottom:8px;left:0px;right:0px; z-index: 100; } @@ -36,7 +36,7 @@