diff --git a/README.md b/README.md index 38ce09f75..a6c449cc7 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,18 @@ try and keep filenames short to avoid overflowing the buffer. }, ``` +### Screenshots + +In the app `metadata.json` file you can add a list of screenshots with a line like: `"screenshots" : [ { url:"screenshot.png" } ],` + +To get a screenshot you can: + +* Type `g.dump()` in the left-hand side of the Web IDE when connected to a Bangle.js 2 - you can then +right-click and save the image shown in the terminal (this only works on Bangle.js 2 - Bangle.js 1 is +unable to read data back from the LCD controller). +* Run your code in the emulator and use the screenshot button in the bottom right of the window. + + ## Testing ### Online diff --git a/apps/bledetect/ChangeLog b/apps/bledetect/ChangeLog index e52015f04..e9b98e08c 100644 --- a/apps/bledetect/ChangeLog +++ b/apps/bledetect/ChangeLog @@ -1,3 +1,4 @@ 0.01: New App! 0.02: Fixed issue with wrong device informations 0.03: Ensure manufacturer:undefined doesn't overflow screen +0.04: Set Bangle.js 2 compatible, show widgets diff --git a/apps/bledetect/bledetect.js b/apps/bledetect/bledetect.js index ca8699f9a..f3fc70e92 100644 --- a/apps/bledetect/bledetect.js +++ b/apps/bledetect/bledetect.js @@ -5,6 +5,7 @@ let menu = { function showMainMenu() { menu["< Back"] = () => load(); + Bangle.drawWidgets(); return E.showMenu(menu); } @@ -55,5 +56,6 @@ function waitMessage() { E.showMessage("scanning"); } +Bangle.loadWidgets(); scan(); waitMessage(); diff --git a/apps/bledetect/metadata.json b/apps/bledetect/metadata.json index f5e0ffb19..0c30fe8f6 100644 --- a/apps/bledetect/metadata.json +++ b/apps/bledetect/metadata.json @@ -2,11 +2,11 @@ "id": "bledetect", "name": "BLE Detector", "shortName": "BLE Detector", - "version": "0.03", + "version": "0.04", "description": "Detect BLE devices and show some informations.", "icon": "bledetect.png", "tags": "app,bluetooth,tool", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS", "BANGLEJS2"], "readme": "README.md", "storage": [ {"name":"bledetect.app.js","url":"bledetect.js"}, diff --git a/apps/crowclk/ChangeLog b/apps/crowclk/ChangeLog index b7e18abe3..4f48bdd14 100644 --- a/apps/crowclk/ChangeLog +++ b/apps/crowclk/ChangeLog @@ -1,2 +1,3 @@ 0.01: New App! 0.02: Removed "wake LCD on face-up"-feature: A watch-face should not set things like "wake LCD on face-up". +0.03: Fix the clock for dark mode. diff --git a/apps/crowclk/crow_clock.js b/apps/crowclk/crow_clock.js index d06369fa8..eee1653cb 100644 --- a/apps/crowclk/crow_clock.js +++ b/apps/crowclk/crow_clock.js @@ -76,7 +76,7 @@ function draw_clock(){ // g.drawLine(clock_center.x - radius, clock_center.y, clock_center.x + radius, clock_center.y); // g.drawLine(clock_center.x, clock_center.y - radius, clock_center.x, clock_center.y + radius); - g.setColor(g.theme.fg); + g.setColor(g.theme.dark ? g.theme.bg : g.theme.fg); let ticks = [0, 90, 180, 270]; ticks.forEach((item)=>{ let agl = item+180; @@ -92,13 +92,13 @@ function draw_clock(){ let minute_agl = minute_angle(date); g.drawImage(hour_hand, hour_pos_x(hour_agl), hour_pos_y(hour_agl), {rotate:hour_agl*p180}); // g.drawImage(minute_hand, minute_pos_x(minute_agl), minute_pos_y(minute_agl), {rotate:minute_agl*p180}); // - g.setColor(g.theme.fg); + g.setColor(g.theme.dark ? g.theme.bg : g.theme.fg); g.fillCircle(clock_center.x, clock_center.y, 6); - g.setColor(g.theme.bg); + g.setColor(g.theme.dark ? g.theme.fg : g.theme.bg); g.fillCircle(clock_center.x, clock_center.y, 3); // draw minute ticks. Takes long time to draw! - g.setColor(g.theme.fg); + g.setColor(g.theme.dark ? g.theme.bg : g.theme.fg); for (var i=0; i<60; i++){ let agl = i*6+180; g.drawImage(tick1.asImage(), rotate_around_x(big_wheel_x(i*6), agl, tick1), rotate_around_y(big_wheel_y(i*6), agl, tick1), {rotate:agl*p180}); diff --git a/apps/crowclk/metadata.json b/apps/crowclk/metadata.json index 752e30fb0..6985cf11a 100644 --- a/apps/crowclk/metadata.json +++ b/apps/crowclk/metadata.json @@ -1,7 +1,7 @@ { "id": "crowclk", "name": "Crow Clock", - "version": "0.02", + "version": "0.03", "description": "A simple clock based on Bold Clock that has MST3K's Crow T. Robot for a face", "icon": "crow_clock.png", "screenshots": [{"url":"screenshot_crow.png"}], diff --git a/apps/cycling/ChangeLog b/apps/cycling/ChangeLog new file mode 100644 index 000000000..ec66c5568 --- /dev/null +++ b/apps/cycling/ChangeLog @@ -0,0 +1 @@ +0.01: Initial version diff --git a/apps/cycling/README.md b/apps/cycling/README.md new file mode 100644 index 000000000..7ba8ee224 --- /dev/null +++ b/apps/cycling/README.md @@ -0,0 +1,34 @@ +# Cycling +> Displays data from a BLE Cycling Speed and Cadence sensor. + +*This is a fork of the CSCSensor app using the layout library and separate module for CSC functionality. It also drops persistence of total distance on the Bangle, as this information is also persisted on the sensor itself. Further, it allows configuration of display units (metric/imperial) independent of chosen locale. Finally, multiple sensors can be used and wheel circumference can be configured for each sensor individually.* + +The following data are displayed: +- curent speed +- moving time +- average speed +- maximum speed +- trip distance +- total distance + +Other than in the original version of the app, total distance is not stored on the Bangle, but instead is calculated from the CWR (cumulative wheel revolutions) reported by the sensor. This metric is, according to the BLE spec, an absolute value that persists throughout the lifetime of the sensor and never rolls over. + +**Cadence / Crank features are currently not implemented** + +## Usage +Open the app and connect to a CSC sensor. + +Upon first connection, close the app afain and enter the settings app to configure the wheel circumference. The total circumference is (cm + mm) - it is split up into two values for ease of configuration. Check the status screen inside the Cycling app while connected to see the address of the currently connected sensor (if you need to differentiate between multiple sensors). + +Inside the Cycling app, use button / tap screen to: +- cycle through screens (if connected) +- reconnect (if connection aborted) + +## TODO +* Sensor battery status +* Implement crank events / show cadence +* Bangle.js 1 compatibility +* Allow setting CWR on the sensor (this is a feature intended by the BLE CSC spec, in case the sensor is replaced or transferred to a different bike) + +## Development +There is a "mock" version of the `blecsc` module, which can be used to test features in the emulator. Check `blecsc-emu.js` for usage. diff --git a/apps/cycling/blecsc-emu.js b/apps/cycling/blecsc-emu.js new file mode 100644 index 000000000..ca5058545 --- /dev/null +++ b/apps/cycling/blecsc-emu.js @@ -0,0 +1,111 @@ +// UUID of the Bluetooth CSC Service +const SERVICE_UUID = "1816"; +// UUID of the CSC measurement characteristic +const MEASUREMENT_UUID = "2a5b"; + +// Wheel revolution present bit mask +const FLAGS_WREV_BM = 0x01; +// Crank revolution present bit mask +const FLAGS_CREV_BM = 0x02; + +/** + * Fake BLECSC implementation for the emulator, where it's hard to test + * with actual hardware. Generates "random" wheel events (no crank). + * + * To upload as a module, paste the entire file in the console using this + * command: require("Storage").write("blecsc-emu",``); + */ +class BLECSCEmulator { + constructor() { + this.timeout = undefined; + this.interval = 500; + this.ccr = 0; + this.lwt = 0; + this.handlers = { + // value + // disconnect + // wheelEvent + // crankEvent + }; + } + + getDeviceAddress() { + return 'fa:ke:00:de:vi:ce'; + } + + /** + * Callback for the GATT characteristicvaluechanged event. + * Consumers must not call this method! + */ + onValue(event) { + // Not interested in non-CSC characteristics + if (event.target.uuid != "0x" + MEASUREMENT_UUID) return; + + // Notify the generic 'value' handler + if (this.handlers.value) this.handlers.value(event); + + const flags = event.target.value.getUint8(0, true); + // Notify the 'wheelEvent' handler + if ((flags & FLAGS_WREV_BM) && this.handlers.wheelEvent) this.handlers.wheelEvent({ + cwr: event.target.value.getUint32(1, true), // cumulative wheel revolutions + lwet: event.target.value.getUint16(5, true), // last wheel event time + }); + + // Notify the 'crankEvent' handler + if ((flags & FLAGS_CREV_BM) && this.handlers.crankEvent) this.handlers.crankEvent({ + ccr: event.target.value.getUint16(7, true), // cumulative crank revolutions + lcet: event.target.value.getUint16(9, true), // last crank event time + }); + } + + /** + * Register an event handler. + * + * @param {string} event value|disconnect + * @param {function} handler handler function that receives the event as its first argument + */ + on(event, handler) { + this.handlers[event] = handler; + } + + fakeEvent() { + this.interval = Math.max(50, Math.min(1000, this.interval + Math.random()*40-20)); + this.lwt = (this.lwt + this.interval) % 0x10000; + this.ccr++; + + var buffer = new ArrayBuffer(8); + var view = new DataView(buffer); + view.setUint8(0, 0x01); // Wheel revolution data present bit + view.setUint32(1, this.ccr, true); // Cumulative crank revolutions + view.setUint16(5, this.lwt, true); // Last wheel event time + + this.onValue({ + target: { + uuid: "0x2a5b", + value: view, + }, + }); + + this.timeout = setTimeout(this.fakeEvent.bind(this), this.interval); + } + + /** + * Find and connect to a device which exposes the CSC service. + * + * @return {Promise} + */ + connect() { + this.timeout = setTimeout(this.fakeEvent.bind(this), this.interval); + return Promise.resolve(true); + } + + /** + * Disconnect the device. + */ + disconnect() { + if (!this.timeout) return; + clearTimeout(this.timeout); + } +} + +exports = BLECSCEmulator; diff --git a/apps/cycling/blecsc.js b/apps/cycling/blecsc.js new file mode 100644 index 000000000..7a47108e5 --- /dev/null +++ b/apps/cycling/blecsc.js @@ -0,0 +1,150 @@ +const SERVICE_UUID = "1816"; +// UUID of the CSC measurement characteristic +const MEASUREMENT_UUID = "2a5b"; + +// Wheel revolution present bit mask +const FLAGS_WREV_BM = 0x01; +// Crank revolution present bit mask +const FLAGS_CREV_BM = 0x02; + +/** + * This class communicates with a Bluetooth CSC peripherial using the Espruino NRF library. + * + * ## Usage: + * 1. Register event handlers using the \`on(eventName, handlerFunction)\` method + * You can subscribe to the \`wheelEvent\` and \`crankEvent\` events or you can + * have raw characteristic values passed through using the \`value\` event. + * 2. Search and connect to a BLE CSC peripherial by calling the \`connect()\` method + * 3. To tear down the connection, call the \`disconnect()\` method + * + * ## Events + * - \`wheelEvent\` - the peripharial sends a notification containing wheel event data + * - \`crankEvent\` - the peripharial sends a notification containing crank event data + * - \`value\` - the peripharial sends any CSC characteristic notification (including wheel & crank event) + * - \`disconnect\` - the peripherial ends the connection or the connection is lost + * + * Each event can only have one handler. Any call to \`on()\` will + * replace a previously registered handler for the same event. + */ +class BLECSC { + constructor() { + this.device = undefined; + this.ccInterval = undefined; + this.gatt = undefined; + this.handlers = { + // wheelEvent + // crankEvent + // value + // disconnect + }; + } + + getDeviceAddress() { + if (!this.device || !this.device.id) + return '00:00:00:00:00:00'; + return this.device.id.split(" ")[0]; + } + + checkConnection() { + if (!this.device) + console.log("no device"); + // else + // console.log("rssi: " + this.device.rssi); + } + + /** + * Callback for the GATT characteristicvaluechanged event. + * Consumers must not call this method! + */ + onValue(event) { + // Not interested in non-CSC characteristics + if (event.target.uuid != "0x" + MEASUREMENT_UUID) return; + + // Notify the generic 'value' handler + if (this.handlers.value) this.handlers.value(event); + + const flags = event.target.value.getUint8(0, true); + // Notify the 'wheelEvent' handler + if ((flags & FLAGS_WREV_BM) && this.handlers.wheelEvent) this.handlers.wheelEvent({ + cwr: event.target.value.getUint32(1, true), // cumulative wheel revolutions + lwet: event.target.value.getUint16(5, true), // last wheel event time + }); + + // Notify the 'crankEvent' handler + if ((flags & FLAGS_CREV_BM) && this.handlers.crankEvent) this.handlers.crankEvent({ + ccr: event.target.value.getUint16(7, true), // cumulative crank revolutions + lcet: event.target.value.getUint16(9, true), // last crank event time + }); + } + + /** + * Callback for the NRF disconnect event. + * Consumers must not call this method! + */ + onDisconnect(event) { + console.log("disconnected"); + if (this.ccInterval) + clearInterval(this.ccInterval); + + if (!this.handlers.disconnect) return; + this.handlers.disconnect(event); + } + + /** + * Register an event handler. + * + * @param {string} event wheelEvent|crankEvent|value|disconnect + * @param {function} handler function that will receive the event as its first argument + */ + on(event, handler) { + this.handlers[event] = handler; + } + + /** + * Find and connect to a device which exposes the CSC service. + * + * @return {Promise} + */ + connect() { + // Register handler for the disconnect event to be passed throug + NRF.on('disconnect', this.onDisconnect.bind(this)); + + // Find a device, then get the CSC Service and subscribe to + // notifications on the CSC Measurement characteristic. + // NRF.setLowPowerConnection(true); + return NRF.requestDevice({ + timeout: 5000, + filters: [{ services: [SERVICE_UUID] }], + }).then(device => { + this.device = device; + this.device.on('gattserverdisconnected', this.onDisconnect.bind(this)); + this.ccInterval = setInterval(this.checkConnection.bind(this), 2000); + return device.gatt.connect(); + }).then(gatt => { + this.gatt = gatt; + return gatt.getPrimaryService(SERVICE_UUID); + }).then(service => { + return service.getCharacteristic(MEASUREMENT_UUID); + }).then(characteristic => { + characteristic.on('characteristicvaluechanged', this.onValue.bind(this)); + return characteristic.startNotifications(); + }); + } + + /** + * Disconnect the device. + */ + disconnect() { + if (this.ccInterval) + clearInterval(this.ccInterval); + + if (!this.gatt) return; + try { + this.gatt.disconnect(); + } catch { + // + } + } +} + +exports = BLECSC; diff --git a/apps/cycling/cycling.app.js b/apps/cycling/cycling.app.js new file mode 100644 index 000000000..268284a29 --- /dev/null +++ b/apps/cycling/cycling.app.js @@ -0,0 +1,453 @@ +const Layout = require('Layout'); +const storage = require('Storage'); + +const SETTINGS_FILE = 'cycling.json'; +const SETTINGS_DEFAULT = { + sensors: {}, + metric: true, +}; + +const RECONNECT_TIMEOUT = 4000; +const MAX_CONN_ATTEMPTS = 2; + +class CSCSensor { + constructor(blecsc, display) { + // Dependency injection + this.blecsc = blecsc; + this.display = display; + + // Load settings + this.settings = storage.readJSON(SETTINGS_FILE, true) || SETTINGS_DEFAULT; + this.wheelCirc = undefined; + + // CSC runtime variables + this.movingTime = 0; // unit: s + this.lastBangleTime = Date.now(); // unit: ms + this.lwet = 0; // last wheel event time (unit: s/1024) + this.cwr = -1; // cumulative wheel revolutions + this.cwrTrip = 0; // wheel revolutions since trip start + this.speed = 0; // unit: m/s + this.maxSpeed = 0; // unit: m/s + this.speedFailed = 0; + + // Other runtime variables + this.connected = false; + this.failedAttempts = 0; + this.failed = false; + + // Layout configuration + this.layout = 0; + this.display.useMetricUnits(true); + this.deviceAddress = undefined; + this.display.useMetricUnits((this.settings.metric)); + } + + onDisconnect(event) { + console.log("disconnected ", event); + + this.connected = false; + this.wheelCirc = undefined; + + this.setLayout(0); + this.display.setDeviceAddress("unknown"); + + if (this.failedAttempts >= MAX_CONN_ATTEMPTS) { + this.failed = true; + this.display.setStatus("Connection failed after " + MAX_CONN_ATTEMPTS + " attempts."); + } else { + this.display.setStatus("Disconnected"); + setTimeout(this.connect.bind(this), RECONNECT_TIMEOUT); + } + } + + loadCircumference() { + if (!this.deviceAddress) return; + + // Add sensor to settings if not present + if (!this.settings.sensors[this.deviceAddress]) { + this.settings.sensors[this.deviceAddress] = { + cm: 223, + mm: 0, + }; + storage.writeJSON(SETTINGS_FILE, this.settings); + } + + const high = this.settings.sensors[this.deviceAddress].cm || 223; + const low = this.settings.sensors[this.deviceAddress].mm || 0; + this.wheelCirc = (10*high + low) / 1000; + } + + connect() { + this.connected = false; + this.setLayout(0); + this.display.setStatus("Connecting"); + console.log("Trying to connect to BLE CSC"); + + // Hook up events + this.blecsc.on('wheelEvent', this.onWheelEvent.bind(this)); + this.blecsc.on('disconnect', this.onDisconnect.bind(this)); + + // Scan for BLE device and connect + this.blecsc.connect() + .then(function() { + this.failedAttempts = 0; + this.failed = false; + this.connected = true; + this.deviceAddress = this.blecsc.getDeviceAddress(); + console.log("Connected to " + this.deviceAddress); + + this.display.setDeviceAddress(this.deviceAddress); + this.display.setStatus("Connected"); + + this.loadCircumference(); + + // Switch to speed screen in 2s + setTimeout(function() { + this.setLayout(1); + this.updateScreen(); + }.bind(this), 2000); + }.bind(this)) + .catch(function(e) { + this.failedAttempts++; + this.onDisconnect(e); + }.bind(this)); + } + + disconnect() { + this.blecsc.disconnect(); + this.reset(); + this.setLayout(0); + this.display.setStatus("Disconnected"); + } + + setLayout(num) { + this.layout = num; + if (this.layout == 0) { + this.display.updateLayout("status"); + } else if (this.layout == 1) { + this.display.updateLayout("speed"); + } else if (this.layout == 2) { + this.display.updateLayout("distance"); + } + } + + reset() { + this.connected = false; + this.failed = false; + this.failedAttempts = 0; + this.wheelCirc = undefined; + } + + interact(d) { + // Only interested in tap / center button + if (d) return; + + // Reconnect in failed state + if (this.failed) { + this.reset(); + this.connect(); + } else if (this.connected) { + this.setLayout((this.layout + 1) % 3); + } + } + + updateScreen() { + var tripDist = this.cwrTrip * this.wheelCirc; + var avgSpeed = this.movingTime > 3 ? tripDist / this.movingTime : 0; + + this.display.setTotalDistance(this.cwr * this.wheelCirc); + this.display.setTripDistance(tripDist); + this.display.setSpeed(this.speed); + this.display.setAvg(avgSpeed); + this.display.setMax(this.maxSpeed); + this.display.setTime(Math.floor(this.movingTime)); + } + + onWheelEvent(event) { + // Calculate number of revolutions since last wheel event + var dRevs = (this.cwr > 0 ? event.cwr - this.cwr : 0); + this.cwr = event.cwr; + + // Increment the trip revolutions counter + this.cwrTrip += dRevs; + + // Calculate time delta since last wheel event + var dT = (event.lwet - this.lwet)/1024; + var now = Date.now(); + var dBT = (now-this.lastBangleTime)/1000; + this.lastBangleTime = now; + if (dT<0) dT+=64; // wheel event time wraps every 64s + if (Math.abs(dT-dBT)>3) dT = dBT; // not sure about the reason for this + this.lwet = event.lwet; + + // Recalculate current speed + if (dRevs>0 && dT>0) { + this.speed = dRevs * this.wheelCirc / dT; + this.speedFailed = 0; + this.movingTime += dT; + } else { + this.speedFailed++; + if (this.speedFailed>3) { + this.speed = 0; + } + } + + // Update max speed + if (this.speed>this.maxSpeed + && (this.movingTime>3 || this.speed<20) + && this.speed<50 + ) this.maxSpeed = this.speed; + + this.updateScreen(); + } +} + +class CSCDisplay { + constructor() { + this.metric = true; + this.fontLabel = "6x8"; + this.fontSmall = "15%"; + this.fontMed = "18%"; + this.fontLarge = "32%"; + this.currentLayout = "status"; + this.layouts = {}; + this.layouts.speed = new Layout({ + type: "v", + c: [ + { + type: "h", + id: "speed_g", + fillx: 1, + filly: 1, + pad: 4, + bgCol: "#fff", + c: [ + {type: undefined, width: 32, halign: -1}, + {type: "txt", id: "speed", label: "00.0", font: this.fontLarge, bgCol: "#fff", col: "#000", width: 122}, + {type: "txt", id: "speed_u", label: " km/h", font: this.fontLabel, col: "#000", width: 22, r: 90}, + ] + }, + { + type: "h", + id: "time_g", + fillx: 1, + pad: 4, + bgCol: "#000", + height: 36, + c: [ + {type: undefined, width: 32, halign: -1}, + {type: "txt", id: "time", label: "00:00", font: this.fontMed, bgCol: "#000", col: "#fff", width: 122}, + {type: "txt", id: "time_u", label: "mins", font: this.fontLabel, bgCol: "#000", col: "#fff", width: 22, r: 90}, + ] + }, + { + type: "h", + id: "stats_g", + fillx: 1, + bgCol: "#fff", + height: 36, + c: [ + { + type: "v", + pad: 4, + bgCol: "#fff", + c: [ + {type: "txt", id: "max_l", label: "MAX", font: this.fontLabel, col: "#000"}, + {type: "txt", id: "max", label: "00.0", font: this.fontSmall, bgCol: "#fff", col: "#000", width: 69}, + ], + }, + { + type: "v", + pad: 4, + bgCol: "#fff", + c: [ + {type: "txt", id: "avg_l", label: "AVG", font: this.fontLabel, col: "#000"}, + {type: "txt", id: "avg", label: "00.0", font: this.fontSmall, bgCol: "#fff", col: "#000", width: 69}, + ], + }, + {type: "txt", id: "stats_u", label: " km/h", font: this.fontLabel, bgCol: "#fff", col: "#000", width: 22, r: 90}, + ] + }, + ], + }); + this.layouts.distance = new Layout({ + type: "v", + bgCol: "#fff", + c: [ + { + type: "h", + id: "tripd_g", + fillx: 1, + pad: 4, + bgCol: "#fff", + height: 32, + c: [ + {type: "txt", id: "tripd_l", label: "TRP", font: this.fontLabel, bgCol: "#fff", col: "#000", width: 36}, + {type: "txt", id: "tripd", label: "0", font: this.fontMed, bgCol: "#fff", col: "#000", width: 118}, + {type: "txt", id: "tripd_u", label: "km", font: this.fontLabel, bgCol: "#fff", col: "#000", width: 22, r: 90}, + ] + }, + { + type: "h", + id: "totald_g", + fillx: 1, + pad: 4, + bgCol: "#fff", + height: 32, + c: [ + {type: "txt", id: "totald_l", label: "TTL", font: this.fontLabel, bgCol: "#fff", col: "#000", width: 36}, + {type: "txt", id: "totald", label: "0", font: this.fontMed, bgCol: "#fff", col: "#000", width: 118}, + {type: "txt", id: "totald_u", label: "km", font: this.fontLabel, bgCol: "#fff", col: "#000", width: 22, r: 90}, + ] + }, + ], + }); + this.layouts.status = new Layout({ + type: "v", + c: [ + { + type: "h", + id: "status_g", + fillx: 1, + bgCol: "#fff", + height: 100, + c: [ + {type: "txt", id: "status", label: "Bangle Cycling", font: this.fontSmall, bgCol: "#fff", col: "#000", width: 176, wrap: 1}, + ] + }, + { + type: "h", + id: "addr_g", + fillx: 1, + pad: 4, + bgCol: "#fff", + height: 32, + c: [ + { type: "txt", id: "addr_l", label: "ADDR", font: this.fontLabel, bgCol: "#fff", col: "#000", width: 36 }, + { type: "txt", id: "addr", label: "unknown", font: this.fontLabel, bgCol: "#fff", col: "#000", width: 140 }, + ] + }, + ], + }); + } + + updateLayout(layout) { + this.currentLayout = layout; + + g.clear(); + this.layouts[layout].update(); + this.layouts[layout].render(); + Bangle.drawWidgets(); + } + + renderIfLayoutActive(layout, node) { + if (layout != this.currentLayout) return; + this.layouts[layout].render(node); + } + + useMetricUnits(metric) { + this.metric = metric; + + // console.log("using " + (metric ? "metric" : "imperial") + " units"); + + var speedUnit = metric ? "km/h" : "mph"; + this.layouts.speed.speed_u.label = speedUnit; + this.layouts.speed.stats_u.label = speedUnit; + + var distanceUnit = metric ? "km" : "mi"; + this.layouts.distance.tripd_u.label = distanceUnit; + this.layouts.distance.totald_u.label = distanceUnit; + + this.updateLayout(this.currentLayout); + } + + convertDistance(meters) { + if (this.metric) return meters / 1000; + return meters / 1609.344; + } + + convertSpeed(mps) { + if (this.metric) return mps * 3.6; + return mps * 2.23694; + } + + setSpeed(speed) { + this.layouts.speed.speed.label = this.convertSpeed(speed).toFixed(1); + this.renderIfLayoutActive("speed", this.layouts.speed.speed_g); + } + + setAvg(speed) { + this.layouts.speed.avg.label = this.convertSpeed(speed).toFixed(1); + this.renderIfLayoutActive("speed", this.layouts.speed.stats_g); + } + + setMax(speed) { + this.layouts.speed.max.label = this.convertSpeed(speed).toFixed(1); + this.renderIfLayoutActive("speed", this.layouts.speed.stats_g); + } + + setTime(seconds) { + var time = ''; + var hours = Math.floor(seconds/3600); + if (hours) { + time += hours + ":"; + this.layouts.speed.time_u.label = " hrs"; + } else { + this.layouts.speed.time_u.label = "mins"; + } + + time += String(Math.floor((seconds%3600)/60)).padStart(2, '0') + ":"; + time += String(seconds % 60).padStart(2, '0'); + + this.layouts.speed.time.label = time; + this.renderIfLayoutActive("speed", this.layouts.speed.time_g); + } + + setTripDistance(distance) { + this.layouts.distance.tripd.label = this.convertDistance(distance).toFixed(1); + this.renderIfLayoutActive("distance", this.layouts.distance.tripd_g); + } + + setTotalDistance(distance) { + distance = this.convertDistance(distance); + if (distance >= 1000) { + this.layouts.distance.totald.label = String(Math.round(distance)); + } else { + this.layouts.distance.totald.label = distance.toFixed(1); + } + this.renderIfLayoutActive("distance", this.layouts.distance.totald_g); + } + + setDeviceAddress(address) { + this.layouts.status.addr.label = address; + this.renderIfLayoutActive("status", this.layouts.status.addr_g); + } + + setStatus(status) { + this.layouts.status.status.label = status; + this.renderIfLayoutActive("status", this.layouts.status.status_g); + } +} + +var BLECSC; +if (process.env.BOARD === "EMSCRIPTEN" || process.env.BOARD === "EMSCRIPTEN2") { + // Emulator + BLECSC = require("blecsc-emu"); +} else { + // Actual hardware + BLECSC = require("blecsc"); +} +var blecsc = new BLECSC(); +var display = new CSCDisplay(); +var sensor = new CSCSensor(blecsc, display); + +E.on('kill',()=>{ + sensor.disconnect(); +}); + +Bangle.setUI("updown", d => { + sensor.interact(d); +}); + +Bangle.loadWidgets(); +sensor.connect(); diff --git a/apps/cycling/cycling.icon.js b/apps/cycling/cycling.icon.js new file mode 100644 index 000000000..12c597956 --- /dev/null +++ b/apps/cycling/cycling.icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH/OAAIuuGFYuEGFQv/ADOlwV8wK/qwN8AAelGAguiFogACWsulFw6SERcwAFSISLnSMuAFZWCGENWllWLRSZC0vOAAovWmUslkyvbqJwIuHGC4uBAARiDdAwueL4YACMQLmfX5IAFqwwoMIowpMQ4wpGIcywDiYAA2IAAgwGq2kFwIvGC5YtPDJIuCF4gXPFxQHLF44XQFxAKOF4oXRBg4LOFwYvEEag7OBgReQNZzLNF5IXPBJlXq4vVC5Qv8R9TXQFwbvYJBgLlNbYXRBoYOEA44XfCAgAFCxgXYDI4VPC7IA/AH4A/AH4AWA")) diff --git a/apps/cycling/icons8-cycling-48.png b/apps/cycling/icons8-cycling-48.png new file mode 100644 index 000000000..0bc83859f Binary files /dev/null and b/apps/cycling/icons8-cycling-48.png differ diff --git a/apps/cycling/metadata.json b/apps/cycling/metadata.json new file mode 100644 index 000000000..cb4260bb2 --- /dev/null +++ b/apps/cycling/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "cycling", + "name": "Bangle Cycling", + "shortName": "Cycling", + "version": "0.01", + "description": "Display live values from a BLE CSC sensor", + "icon": "icons8-cycling-48.png", + "tags": "outdoors,exercise,ble,bluetooth", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"cycling.app.js","url":"cycling.app.js"}, + {"name":"cycling.settings.js","url":"settings.js"}, + {"name":"blecsc","url":"blecsc.js"}, + {"name":"cycling.img","url":"cycling.icon.js","evaluate": true} + ] +} diff --git a/apps/cycling/settings.js b/apps/cycling/settings.js new file mode 100644 index 000000000..76303379d --- /dev/null +++ b/apps/cycling/settings.js @@ -0,0 +1,57 @@ +// This file should contain exactly one function, which shows the app's settings +/** + * @param {function} back Use back() to return to settings menu + */ +(function(back) { + const storage = require('Storage') + const SETTINGS_FILE = 'cycling.json' + + // Set default values and merge with stored values + let settings = Object.assign({ + metric: true, + sensors: {}, + }, (storage.readJSON(SETTINGS_FILE, true) || {})); + + const menu = { + '': { 'title': 'Cycling' }, + '< Back': back, + 'Units': { + value: settings.metric, + format: v => v ? 'metric' : 'imperial', + onchange: (metric) => { + settings.metric = metric; + storage.writeJSON(SETTINGS_FILE, settings); + }, + }, + } + + const sensorMenus = {}; + for (var addr of Object.keys(settings.sensors)) { + // Define sub menu + sensorMenus[addr] = { + '': { title: addr }, + '< Back': () => E.showMenu(menu), + 'cm': { + value: settings.sensors[addr].cm, + min: 80, max: 240, step: 1, + onchange: (v) => { + settings.sensors[addr].cm = v; + storage.writeJSON(SETTINGS_FILE, settings); + }, + }, + '+ mm': { + value: settings.sensors[addr].mm, + min: 0, max: 9, step: 1, + onchange: (v) => { + settings.sensors[addr].mm = v; + storage.writeJSON(SETTINGS_FILE, settings); + }, + }, + }; + + // Add entry to main menu + menu[addr] = () => E.showMenu(sensorMenus[addr]); + } + + E.showMenu(menu); +}) diff --git a/apps/dtlaunch/ChangeLog b/apps/dtlaunch/ChangeLog index 811784b39..da07af798 100644 --- a/apps/dtlaunch/ChangeLog +++ b/apps/dtlaunch/ChangeLog @@ -8,3 +8,4 @@ 0.08: Optimize line wrapping for Bangle 2 0.09: fix the trasparent widget bar if there are no widgets for Bangle 2 0.10: added "one click exit" setting for Bangle 2 +0.11: Fix bangle.js 1 white icons not displaying diff --git a/apps/dtlaunch/app-b1.js b/apps/dtlaunch/app-b1.js index ec0569127..ed9cc778e 100644 --- a/apps/dtlaunch/app-b1.js +++ b/apps/dtlaunch/app-b1.js @@ -48,6 +48,7 @@ function draw_icon(p,n,selected) { var x = (n%3)*80; var y = n>2?130:40; (selected?g.setColor(0.3,0.3,0.3):g.setColor(0,0,0)).fillRect(x,y,x+79,y+89); + g.setColor(g.theme.fg); g.drawImage(s.read(apps[p*6+n].icon),x+10,y+10,{scale:1.25}); g.setColor(-1).setFontAlign(0,-1,0).setFont("6x8",1); var txt = apps[p*6+n].name.split(" "); diff --git a/apps/dtlaunch/metadata.json b/apps/dtlaunch/metadata.json index 7a4094e54..b3f94442f 100644 --- a/apps/dtlaunch/metadata.json +++ b/apps/dtlaunch/metadata.json @@ -1,7 +1,7 @@ { "id": "dtlaunch", "name": "Desktop Launcher", - "version": "0.10", + "version": "0.11", "description": "Desktop style App Launcher with six (four for Bangle 2) apps per page - fast access if you have lots of apps installed.", "screenshots": [{"url":"shot1.png"},{"url":"shot2.png"},{"url":"shot3.png"}], "icon": "icon.png", diff --git a/apps/game1024/ChangeLog b/apps/game1024/ChangeLog new file mode 100644 index 000000000..ffb1f94bc --- /dev/null +++ b/apps/game1024/ChangeLog @@ -0,0 +1,5 @@ +0.01: Initial version +0.02: Temporary intermediate version +0.03: Basic colors +0.04: Bug fix score reset after Game Over, new icon +0.05: Chevron marker on the randomly added square \ No newline at end of file diff --git a/apps/game1024/README.md b/apps/game1024/README.md new file mode 100644 index 000000000..500453145 --- /dev/null +++ b/apps/game1024/README.md @@ -0,0 +1,36 @@ + +# Play the game of 1024 + +Move the tiles by swiping to the lefthand, righthand or up- and downward side of the watch. + +When two tiles with the same number are squashed together they will add up as exponentials: + +**1 + 1 = 2** or **A + A = D** which is a representation of **2^1 + 2^1 = 2^1 = 4** + +**2 + 2 = 3** or **B + B = C** which is a representation of **2^2 + 2^2 = 2^3 = 8** + +**3 + 3 = 4** or **C + C = D** which is a representation of **2^3 + 2^3 = 2^4 = 16** + +After each move a new tile will be added on a random empty square. The value can be 1 or 2, and will be marked with a chevron. + +So you can continue till you reach **1024** which equals **2^(10)**. So when you reach tile **10** you have won. + +The score is maintained by adding the outcome of the sum of all pairs of squashed tiles (4+16+4+8 etc.) + +Use the side **BTN** to exit the game, score and tile positions will be saved. + +## Buttons on the screen + + - Button **U**: Undo the last move. There are currently a maximum of 4 undo levels. The level is indicated with a small number in the lower righthand corner of the Undo button + - Button **\***: Change the text on the tile to number, capitals or Roman numbers + - Button **R**: Reset the game. The Higscore will be remembered. You will be prompted first. + +### Credits + +Game 1024 is based on Saming's 2048 and Misho M. Petkovic 1024game.org and conceptually similar to Threes by Asher Vollmer. + +In Dark theme with numbers: +![Screenshot from the Banglejs 2 watch with the game in dark theme](./game1024_sc_dump_dark.png) + +In Light theme with characters: +![Screenshot from the Banglejs 2 watch with the game in light theme](./game1024_sc_dump_light.png) \ No newline at end of file diff --git a/apps/game1024/app-icon.js b/apps/game1024/app-icon.js new file mode 100644 index 000000000..8e8b56d9f --- /dev/null +++ b/apps/game1024/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwkBkQAWkAyVgQXx5gAMCQOqAAeiC/4X/AAXdC6HP7gECn///oXH///+QXEn4XC4f/mf/AwQXEmczmQXD74QD7/8AQZHLFIPfC4QzC4ZICC5XPngXD/4CB5oXNIYQXG+YXSCYQXKkQXWU4oXbL5mjC5M/R5evC5PfniwBa5Gvd4gXE5/z7s/DQIXGl6PJ5v//5eCC46/F4YXCAgMzAoYXFkYXFABTvMC/4X0ACkCC/4XJu4AMCQOIAAeCC+0///zC6dz/8z/83C6V/CgN/+4XSn4DCF6ZcGC6Hyv53V+Z3WCgR3OkQAWA=")) \ No newline at end of file diff --git a/apps/game1024/app.js b/apps/game1024/app.js new file mode 100644 index 000000000..9f6081376 --- /dev/null +++ b/apps/game1024/app.js @@ -0,0 +1,691 @@ +const debugMode = 'off'; // valid values are: off, test, production, development +const middle = {x:Math.floor(g.getWidth()/2)-20, y: Math.floor(g.getHeight()/2)}; +const rows = 4, cols = 4; +const borderWidth = 6; +const sqWidth = (Math.floor(Bangle.appRect.w - 48) / rows) - borderWidth; +const cellColors = [{bg:'#00FFFF', fg: '#000000'}, + {bg:'#FF00FF', fg: '#000000'}, {bg:'#808000', fg: '#FFFFFF'}, {bg:'#0000FF', fg: '#FFFFFF'}, {bg:'#008000', fg: '#FFFFFF'}, + {bg:'#800000', fg: '#FFFFFF'}, {bg:'#00FF00', fg: '#000000'}, {bg:'#000080', fg: '#FFFFFF'}, {bg:'#FFFF00', fg: '#000000'}, + {bg:'#800080', fg: '#FFFFFF'}, {bg:'#FF0000', fg: '#FFFFFF'}]; +const cellFonts = ["12x20", "12x20", "Vector:14"]; +const cellChars = [ + [0,1,2,3,4,5,6,7,8,9,10], + ['0','A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'], + ['0','I', 'II', 'III', 'IV', 'V', 'VI', 'VII','VIII', 'IX', 'X'] +]; +// const numInitialCells = 2; +const maxUndoLevels = 4; +const noExceptions = true; +let charIndex = 0; // plain numbers on the grid +const themeBg = g.theme.bg; + + +const scores = { + currentScore: 0, + highScore: 0, + lastScores: [0], + add: function(val) { + this.currentScore = this.currentScore + Math.pow(2, val); + debug(() => console.log("new score=",this.currentScore)); + }, + addToUndo: function () { + this.lastScores.push(this.currentScore); + if (this.lastScores.length > maxUndoLevels) this.lastScores.shift(); + }, + undo: function () { + this.currentScore = this.lastScores.pop(); + debug(() => console.log("undo score =", this.currentScore, "rest:", this.lastScores)); + }, + reset: function () { + this.currentScore = 0; + this.lastScores = [0]; + }, + draw: function () { + g.setColor(btnAtribs.fg); + let ulCorner = {x: Bangle.appRect.x + 6, y: Bangle.appRect.y2 -22 }; + let lrCorner = {x: Bangle.appRect.x2, y: Bangle.appRect.y2 - 1}; + g.fillRect(ulCorner.x, ulCorner.y, lrCorner.x, lrCorner.y) + .setFont12x20(1) + .setFontAlign(0,0,0); + let scrX = Math.floor((ulCorner.x + lrCorner.x)/3); + let scrY = Math.floor((ulCorner.y + lrCorner.y)/2) + 1; + g.setColor('#000000') + .drawString(this.currentScore, scrX+1, scrY+1) + .setColor(btnAtribs.bg) + .drawString(this.currentScore, scrX, scrY); + scrX = Math.floor(4*(ulCorner.x + lrCorner.x)/5); + g.setFont("6x8:1x2") + .drawString(this.highScore, btnAtribs.x + Math.floor(btnAtribs.w/2), scrY); + }, + hsContents: function () { + return {"highScore": this.highScore, "lastScore": this.currentScore}; + }, + check: function () { + this.highScore = (this.currentScore > this.highScore) ? this.currentScore : this.highScore; + debug(() => console.log('highScore =', this.highScore)); + } +}; + +// snapshot interval is the number of moves after wich a snapshot is wriiten to file +const snInterval = 1; + +const snReadOnInit = true; +// a snapshot contains a json file dump of the last positions of the tiles on the board, including the scores +const snapshot = { + interval: snInterval, + snFileName: 'game1024.json', + counter: 0, + updCounter: function() { + this.counter = ++this.counter > this.interval ? 0 : this.counter; + }, + dump: {gridsize: rows * cols, expVals: [], score: 0, highScore: 0, charIndex: charIndex}, + write: function() { + require("Storage").writeJSON(this.snFileName, this.dump); + }, + read: function () { + let sn = require("Storage").readJSON(this.snFileName, noExceptions); + if ((typeof sn == "undefined") || (sn.gridsize !== rows * cols)) { + require("Storage").writeJSON(this.snFileName, this.dump); + return false; + } else { + if ((typeof sn !== "undefined") && (sn.gridsize == rows * cols)){ + this.dump = sn; + return true; + } + } + }, + setDump: function () { + this.dump.expVals = []; + allSquares.forEach(sq => { + this.dump.expVals.push(sq.expVal); + }); + this.dump.score = scores.currentScore; + this.dump.highScore = scores.highScore; + this.dump.charIndex = charIndex; + }, + make: function () { + this.updCounter(); + if (this.counter == this.interval) { + this.setDump(); + this.write(); + debug(() => console.log("snapped the state of the game:", this.dump)); + } + }, + recover: function () { + if (this.read()) { + this.dump.expVals.forEach((val, idx) => { + allSquares[idx].setExpVal(val); + }); + scores.currentScore = this.dump.score ? this.dump.score : 0; + scores.highScore = this.dump.highScore ? this.dump.highScore : 0 ; + charIndex = this.dump.charIndex ? this.dump.charIndex : 0 ; + } + }, + reset: function () { + this.dump.gridsize = rows * cols; + this.dump.expVals = []; + for (let i = 0; i< this.dump.gridsize; i++) { + this.dump.expVals[i] = 0; + } + this.dump.score = 0; + this.dump.highScore = scores.highScore; + this.dump.charIndex = charIndex; + this.write(); + debug(() => console.log("reset D U M P E D!", this.dump)); + } +}; +const btnAtribs = {x: 134, w: 42, h: 42, fg:'#C0C0C0', bg:'#800000'}; +const buttons = { + all: [], + draw: function () { + this.all.forEach(btn => { + btn.draw(); + }); + }, + add: function(btn) { + this.all.push(btn); + } +}; +/** + * to the right = -1 + all tiles move to the left, begin with the outer righthand side tiles + moving 0 to max 3 places to the right + + find first tile beginning with bottom row, righthand side + */ + +const mover = { + direction: { + up: {name: 'up', step: 1, innerBegin: 0, innerEnd: rows-1, outerBegin: 0, outerEnd: cols-1, iter: rows -1, + sqIndex: function (m,n) {return m*(cols) + n;}, sqNextIndex: function (m,n) {return m < rows -1 ? (m+1)*(cols) + n : -1;} + }, + down: {name: 'down', step:-1, innerBegin: rows-1, innerEnd: 0, outerBegin: cols-1, outerEnd: 0, iter: rows -1, + sqIndex: function (m,n) {return m*(cols) + n;}, sqNextIndex: function (m,n) {return m > 0 ? (m-1)*(cols) + n : -1;} + }, + left: {name: 'left', step: 1, innerBegin: 0, innerEnd: cols-1, outerBegin: 0, outerEnd: rows-1, iter: cols -1, + sqIndex: function (m,n) {return n*(rows) + m;}, sqNextIndex: function (m,n) {return m < cols -1 ? n*(rows) + m +1 : -1;} + }, + right: {name: 'right', step:-1, innerBegin: cols-1, innerEnd: 0, outerBegin: rows-1, outerEnd: 0, iter: cols -1, + sqIndex: function (m,n) {return n*(rows) + m;}, sqNextIndex: function (m,n) {return m > 0 ? n*(rows) + m -1: -1;} + } + }, + anyLeft: function() { + let canContinue = false; + [this.direction.up,this.direction.left].forEach (dir => { + const step = dir.step; + // outer loop for all colums/rows + for (let n = dir.outerBegin; step*n <= step*dir.outerEnd; n=n+step) { + // lets move squares one position in a row or column, counting backwards starting from the and where the squares will end up + for (let m = dir.innerBegin; step*m <= step*dir.innerEnd; m=m+step) { + const idx = dir.sqIndex(m,n); + const nextIdx = dir.sqNextIndex(m,n); + if (allSquares[idx].expVal == 0) { + canContinue = true; // there is an empty cell found + break; + } + if (nextIdx >= 0) { + if (allSquares[idx].expVal == allSquares[nextIdx].expVal) { + canContinue = true; // equal adjacent cells > 0 found + break; + } + if (allSquares[nextIdx].expVal == 0) { + canContinue = true; // there is an empty cell found + break; + } + } + if (canContinue) break; + } + if (canContinue) break; + } + }); + return canContinue; + }, + nonEmptyCells: function (dir) { + debug(() => console.log("Move: ", dir.name)); + const step = dir.step; + // outer loop for all colums/rows + for (let n = dir.outerBegin; step*n <= step*dir.outerEnd; n=n+step) { + // let rowStr = '| '; + + // Move a number of iteration with the squares to move them all to one side + for (let iter = 0; iter < dir.iter; iter++) { + + // lets move squares one position in a row or column, counting backwards starting from the and where the squares will end up + for (let m = dir.innerBegin; step*m <= step*dir.innerEnd; m=m+step) { + // get the array of squares index for current cell + const idx = dir.sqIndex(m,n); + const nextIdx = dir.sqNextIndex(m,n); + + if (allSquares[idx].expVal == 0 && nextIdx >= 0) { + allSquares[idx].setExpVal(allSquares[nextIdx].expVal); + allSquares[nextIdx].setExpVal(0); + } + } + } + } + }, + // add up the conjacent squares with identical values en set next square to empty in the process + mergeEqlCells: function(dir) { + const step = dir.step; + // outer loop for all colums/rows + for (let n = dir.outerBegin; step*n <= step*dir.outerEnd; n=n+step) { + // lets move squares one position in a row or column, counting backwards starting from the and where the squares will end up + for (let m = dir.innerBegin; step*m <= step*dir.innerEnd; m=m+step) { + const idx = dir.sqIndex(m,n); + const nextIdx = dir.sqNextIndex(m,n); + + if ((allSquares[idx].expVal > 0) && nextIdx >= 0) { + if (allSquares[idx].expVal == allSquares[nextIdx].expVal) { + let expVal = allSquares[idx].expVal; + allSquares[idx].setExpVal(++expVal); + allSquares[idx].addToScore(); + allSquares[nextIdx].setExpVal(0); + } + } + } + } + } +}; +// Minimum number of pixels to interpret it as drag gesture +const dragThreshold = 10; + +// Maximum number of pixels to interpret a click from a drag event series +const clickThreshold = 3; + +let allSquares = []; +// let buttons = []; + +class Button { + constructor(name, x0, y0, width, height, text, bg, fg, cb, enabled) { + this.x0 = x0; + this.y0 = y0; + this.x1 = x0 + width; + this.y1 = y0 + height; + this.name = name; + this.cb = cb; + this.text = text; + this.bg = bg; + this.fg = fg; + this.font = "6x8:3"; + this.enabled = enabled; + } + disable() { + this.enabled = false; + } + enable() { + this.enabled = true; + } + draw() { + g.setColor(this.bg) + .fillRect(this.x0, this.y0, this.x1, this.y1) + .setFont(this.font) + .setFontAlign(0,0,0); + let strX = Math.floor((this.x0+this.x1)/2); + let strY = Math.floor((this.y0+this.y1)/2); + g.setColor("#000000") + .drawString(this.text, strX+2, strY+2) + .setColor(this.fg) + .drawString(this.text, strX, strY); + // buttons.push(this); + } + onClick() {if (typeof this.cb === 'function' && this.enabled) { + this.cb(this); + } + } +} + +class Cell { + constructor(x0, y0, width, idx, cb) { + this.x0 = x0; + this.y0 = y0; + this.x1 = x0 + width; + this.y1 = y0 + width; + this.expVal = 0; + this.previousExpVals=[]; + this.idx = idx; + this.cb = cb; + this.isRndm = false; + this.ax = x0; + this.ay = Math.floor(0.2*width+y0); + this.bx = Math.floor(0.3*width+x0); + this.by = Math.floor(0.5*width+y0); + this.cx = x0; + this.cy = Math.floor(0.8*width+y0); + } + getColor(i) { + return cellColors[i >= cellColors.length ? cellColors.length -1 : i]; + } + drawBg() { + debug(()=>console.log("Drawbg!!")); + if (this.isRndm == true) { + debug(()=>console.log('Random: (ax)', this.ax)); + g.setColor(this.getColor(this.expVal).bg) + .fillRect(this.x0, this.y0, this.x1, this.y1) + .setColor(themeBg) + .fillPoly([this.cx,this.cy,this.bx,this.by,this.ax,this.ay]); + } else { + g.setColor(this.getColor(this.expVal).bg) + .fillRect(this.x0, this.y0, this.x1, this.y1); + } + } + drawNumber() { + if (this.expVal !== 0) { + g.setFont(cellFonts[charIndex]) + .setFontAlign(0,0,0); + let char = cellChars[charIndex][this.expVal]; + let strX = Math.floor((this.x0 + this.x1)/2); + let strY = Math.floor((this.y0 + this.y1)/2); + g.setColor(this.getColor(this.expVal).fg) + .drawString(char, strX, strY); + } + } + setExpVal(val) { + this.expVal = val; + } + getIdx() {return this.idx;} + pushToUndo() { + // remember this new step + this.previousExpVals.push(this.expVal); + // keep the undo list not longer than max undo levels + if (this.previousExpVals.length > maxUndoLevels) this.previousExpVals.shift(); + } + popFromUndo() { + // take one step back + if (this.previousExpVals.length > 0) { + this.expVal = this.previousExpVals.pop(); + } + } + removeUndo() { + this.previousExpVals=[0]; + } + addToScore() {if (typeof this.cb === 'function') { + this.cb(this.expVal); + } + } + setRndmFalse() { + this.isRndm = false; + } + setRndmTrue() { + this.isRndm = true; + } + drawRndmIndicator(){ + if (this.isRndm == true) { + debug(()=>console.log('Random: (ax)', this.ax)); + g.setColor(this.getColor(0).bg) + .fillPoly(this.ax,this.ay,this.bx,this.by,this.cx,this.cy); + } + } +} + +function undoGame() { + g.clear(); + if (scores.lastScores.length > 0) { + allSquares.forEach(sq => { + sq.popFromUndo(); + sq.drawBg(); + sq.drawNumber(); + }); + scores.undo(); + scores.draw(); + buttons.draw(); + updUndoLvlIndex(); + snapshot.make(); + } + Bangle.loadWidgets(); + Bangle.drawWidgets(); +} +function addToUndo() { + allSquares.forEach(sq => { + sq.pushToUndo(); + }); + scores.addToUndo(); +} +function addToScore (val) { + scores.add(val); + if (val == 10) messageYouWin(); +} +function createGrid () { + let cn =0; + for (let r = 0; r < rows; r++) { + for (let c = 0; c < cols; c++) { + let x0 = borderWidth + c*(borderWidth + sqWidth) - (rows/2)*(2*borderWidth + sqWidth) + middle.x + Math.floor(sqWidth/3); + let y0 = borderWidth + r*(borderWidth + sqWidth) - (cols/2)*(2*borderWidth + sqWidth) + middle.y + Math.floor(sqWidth/3); + let cell = new Cell(x0, y0, sqWidth, c + r*cols, addToScore); + allSquares.push(cell); + } + } +} +function messageGameOver () { + const c = (g.theme.dark) ? {"fg": "#FFFFFF", "bg": "#808080"} : {"fg": "#FF0000", "bg": "#000000"}; + g.setColor(c.bg) + .setFont12x20(2).setFontAlign(0,0,0) + .drawString("G A M E", middle.x+13, middle.y-24) + .drawString("O V E R !", middle.x+13, middle.y+24); + g.setColor(c.fg) + .drawString("G A M E", middle.x+12, middle.y-25) + .drawString("O V E R !", middle.x+12, middle.y+25); +} +function messageYouWin () { + g.setColor("#1a0d00") + .setFont12x20(2) + .setFontAlign(0,0,0) + .drawString("YOU HAVE", middle.x+18, middle.y-24) + .drawString("W O N ! !", middle.x+18, middle.y+24); + g.setColor("#FF0808") + .drawString("YOU HAVE", middle.x+17, middle.y-25) + .drawString("W O N ! !", middle.x+17, middle.y+25); + Bangle.buzz(200, 1); +} +function makeRandomNumber () { + return Math.ceil(2*Math.random()); +} +function addRandomNumber() { + let emptySquaresIdxs = []; + allSquares.forEach(sq => { + if (sq.expVal == 0) emptySquaresIdxs.push(sq.getIdx()); + }); + if (emptySquaresIdxs.length > 0) { + let randomIdx = Math.floor( emptySquaresIdxs.length * Math.random() ); + allSquares[emptySquaresIdxs[randomIdx]].setExpVal(makeRandomNumber()); + allSquares[emptySquaresIdxs[randomIdx]].setRndmTrue(); + } +} +function drawGrid() { + allSquares.forEach(sq => { + sq.drawBg(); + // sq.drawRndmIndicator(); + sq.drawNumber(); + }); +} +function initGame() { + g.clear(); + // scores.read(); + createGrid(); + if (snReadOnInit) { + snapshot.recover(); + debug(() => console.log("R E C O V E R E D !", snapshot.dump)); + let sum = allSquares.reduce(function (tv, sq) {return (sq.expVal + tv) ;}, 0); + if (!sum) { + addRandomNumber(); + } + } else { + addRandomNumber(); + // addToUndo(); + } + addRandomNumber(); + drawGrid(); + scores.draw(); + buttons.draw(); + // Clock mode allows short-press on button to exit + Bangle.setUI("clock"); + // Load widgets + Bangle.loadWidgets(); + Bangle.drawWidgets(); +} +function drawPopUp(message,cb) { + g.setColor('#FFFFFF'); + let rDims = Bangle.appRect; + g.fillPoly([rDims.x+10, rDims.y+20, + rDims.x+20, rDims.y+10, + rDims.x2-30, rDims.y+10, + rDims.x2-20, rDims.y+20, + rDims.x2-20, rDims.y2-40, + rDims.x2-30, rDims.y2-30, + rDims.x+20, rDims.y2-30, + rDims.x+10, rDims.y2-40 + ]); + buttons.all.forEach(btn => {btn.disable();}); + const btnYes = new Button('yes', rDims.x+16, rDims.y2-80, 54, btnAtribs.h, 'YES', btnAtribs.fg, btnAtribs.bg, cb, true); + const btnNo = new Button('no', rDims.x2-80, rDims.y2-80, 54, btnAtribs.h, 'NO', btnAtribs.fg, btnAtribs.bg, cb, true); + btnYes.draw(); + btnNo.draw(); + g.setColor('#000000'); + g.setFont12x20(1); + g.setFontAlign(-1,-1,0); + g.drawString(message, rDims.x+20, rDims.y+20); + buttons.add(btnYes); + buttons.add(btnNo); +} +function handlePopUpClicks(btn) { + const name = btn.name; + buttons.all.pop(); // remove the no button + buttons.all.pop(); // remove the yes button + buttons.all.forEach(b => {b.enable();}); // enable the remaining buttons again + debug(() => console.log("Button name =", name)); + switch (name) { + case 'yes': + resetGame(); + break; + default: + g.clear(); + drawGrid(); + scores.draw(); + buttons.draw(); + updUndoLvlIndex(); + Bangle.loadWidgets(); + Bangle.drawWidgets(); + } +} +function resetGame() { + g.clear(); + scores.reset(); + allSquares.forEach(sq => {sq.setExpVal(0);sq.removeUndo();sq.setRndmFalse();}); + addRandomNumber(); + addRandomNumber(); + drawGrid(); + scores.draw(); + buttons.draw(); + Bangle.loadWidgets(); + Bangle.drawWidgets(); +} + +/** + * Function that can be used in test or development environment, or production. + * Depends on global constant debugMode + * @param {function} func function to call like console.log() + */ + const debug = (func) => { + switch (debugMode) { + case "development": + if (typeof func === 'function') { + func(); + } + break; + case "off": + default: break; + } +}; + +// Handle a "click" event (only needed for menu button) +function handleclick(e) { + buttons.all.forEach(btn => { + if ((e.x >= btn.x0) && (e.x <= btn.x1) && (e.y >= btn.y0) && (e.y <= btn.y1)) { + btn.onClick(); + debug(() => console.log(btn.name)); + } + }); +} + +// Handle a drag event (moving the stones around) +function handledrag(e) { + /*debug(Math.abs(e.dx) > Math.abs(e.dy) ? + (e.dx > 0 ? e => console.log('To the right') : e => console.log('To the left') ) : + (e.dy > 0 ? e => console.log('Move down') : e => console.log('Move up') )); + */ + // [move.right, move.left, move.up, move.down] + runGame((Math.abs(e.dx) > Math.abs(e.dy) ? + (e.dx > 0 ? mover.direction.right : mover.direction.left ) : + (e.dy > 0 ? mover.direction.down : mover.direction.up ))); +} +// Evaluate "drag" events from the UI and call handlers for drags or clicks +// The UI sends a drag as a series of events indicating partial movements +// of the finger. +// This class combines such parts to a long drag from start to end +// If the drag is short, it is interpreted as click, +// otherwise as drag. +// The approprate method is called with the data of the drag. +class Dragger { + + constructor(clickHandler, dragHandler, clickThreshold, dragThreshold) { + this.clickHandler = clickHandler; + this.dragHandler = dragHandler; + this.clickThreshold = (clickThreshold === undefined ? 3 : clickThreshold); + this.dragThreshold = (dragThreshold === undefined ? 10 : dragThreshold); + this.dx = 0; + this.dy = 0; + this.enabled = true; + } + + // Enable or disable the Dragger + setEnabled(b) { + this.enabled = b; + } + + // Handle a raw drag event from the UI + handleRawDrag(e) { + if (!this.enabled) + return; + this.dx += e.dx; // Always accumulate + this.dy += e.dy; + if (e.b === 0) { // Drag event ended: Evaluate full drag + if (Math.abs(this.dx) < this.clickThreshold && Math.abs(this.dy) < this.clickThreshold) + this.clickHandler({ + x: e.x - this.dx, + y: e.y - this.dy + }); // take x and y from the drag start + else if (Math.abs(this.dx) > this.dragThreshold || Math.abs(this.dy) > this.dragThreshold) + this.dragHandler({ + x: e.x - this.dx, + y: e.y - this.dy, + dx: this.dx, + dy: this.dy + }); + this.dx = 0; // Clear the drag accumulator + this.dy = 0; + } + } + + // Attach the drag evaluator to the UI + attach() { + Bangle.on("drag", e => this.handleRawDrag(e)); + } +} + +// Dragger is needed for interaction during the game +var dragger = new Dragger(handleclick, handledrag, clickThreshold, dragThreshold); + +// Disable dragger as board is not yet initialized +dragger.setEnabled(false); + +// Nevertheless attach it so that it is ready once the game starts +dragger.attach(); + +function runGame(dir){ + addToUndo(); + updUndoLvlIndex(); + mover.nonEmptyCells(dir); + mover.mergeEqlCells(dir); + mover.nonEmptyCells(dir); + allSquares.forEach(sq => {sq.setRndmFalse();}); + addRandomNumber(); + drawGrid(); + scores.check(); + scores.draw(); + // scores.write(); + snapshot.make(); + dragger.setEnabled(true); + if (!(mover.anyLeft())) { + debug(() => console.log("G A M E O V E R !!")); + snapshot.reset(); + messageGameOver(); + } +} + +function updUndoLvlIndex() { + let x = 170; + let y = 60; + g.setColor(btnAtribs.fg) + .fillRect(x-6,y-6, 176, 67); + if (scores.lastScores.length > 0) { + g.setColor("#000000") + .setFont("4x6:2") + .drawString(scores.lastScores.length, x, y); + } +} +function incrCharIndex() { + charIndex++; + if (charIndex >= cellChars.length) charIndex = 0; + drawGrid(); +} +buttons.add(new Button('undo', btnAtribs.x, 25, btnAtribs.w, btnAtribs.h, 'U', btnAtribs.fg, btnAtribs.bg, undoGame, true)); +buttons.add(new Button('chars', btnAtribs.x, 71, btnAtribs.w, 31, '*', btnAtribs.fg, btnAtribs.bg, function(){incrCharIndex();}, true)); +buttons.add(new Button('restart', btnAtribs.x, 106, btnAtribs.w, btnAtribs.h, 'R', btnAtribs.fg, btnAtribs.bg, function(){drawPopUp('Do you want\nto restart?',handlePopUpClicks);}, true)); + +initGame(); + +dragger.setEnabled(true); + +E.on('kill',function() { + this.write(); + debug(() => console.log("1024 game got killed!")); +}); \ No newline at end of file diff --git a/apps/game1024/game1024.app.info b/apps/game1024/game1024.app.info new file mode 100644 index 000000000..b1c9d84ce --- /dev/null +++ b/apps/game1024/game1024.app.info @@ -0,0 +1,6 @@ +require("Storage").write("timer.info",{ + "id":"game1024", + "name":"1024 Game", + "src":"game1024.app.js", + "icon":"game1024.img" +}); \ No newline at end of file diff --git a/apps/game1024/game1024.json b/apps/game1024/game1024.json new file mode 100644 index 000000000..3749649ee --- /dev/null +++ b/apps/game1024/game1024.json @@ -0,0 +1 @@ +{"gridsize": 16, "expVals": [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], "score": 0, "highScore": 0, "charIndex": 1} \ No newline at end of file diff --git a/apps/game1024/game1024.png b/apps/game1024/game1024.png new file mode 100644 index 000000000..c0f7eaf21 Binary files /dev/null and b/apps/game1024/game1024.png differ diff --git a/apps/game1024/game1024_sc_dump_dark.png b/apps/game1024/game1024_sc_dump_dark.png new file mode 100644 index 000000000..87577ecfa Binary files /dev/null and b/apps/game1024/game1024_sc_dump_dark.png differ diff --git a/apps/game1024/game1024_sc_dump_light.png b/apps/game1024/game1024_sc_dump_light.png new file mode 100644 index 000000000..06ada65ac Binary files /dev/null and b/apps/game1024/game1024_sc_dump_light.png differ diff --git a/apps/game1024/metadata.json b/apps/game1024/metadata.json new file mode 100644 index 000000000..557d77b89 --- /dev/null +++ b/apps/game1024/metadata.json @@ -0,0 +1,17 @@ +{ "id": "game1024", + "name": "1024 Game", + "shortName" : "1024 Game", + "version": "0.05", + "icon": "game1024.png", + "screenshots": [ {"url":"screenshot.png" } ], + "readme":"README.md", + "description": "Swipe the squares up, down, to the left or right, join the numbers and get to the 10 (2^1024), J or X tile!", + "type": "app", + "tags": "game,puzzle", + "allow_emulator": true, + "supports" : ["BANGLEJS2"], + "storage": [ + {"name":"game1024.app.js","url":"app.js"}, + {"name":"game1024.img","url":"app-icon.js","evaluate":true} + ] + } diff --git a/apps/game1024/screenshot.png b/apps/game1024/screenshot.png new file mode 100644 index 000000000..8be52f8cb Binary files /dev/null and b/apps/game1024/screenshot.png differ diff --git a/apps/gbdebug/app-icon.js b/apps/gbdebug/app-icon.js index a701ef3a9..0cecad73b 100644 --- a/apps/gbdebug/app-icon.js +++ b/apps/gbdebug/app-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("mEw4cBzsE/4AClMywH680rlOW9N9kmSpICnyBBBgQRMkBUDgIRKoBoGGRYAFHBGARpARHT5MJKxQAFLgzELCIlIBQkSCIsEPRKBHCIYbGoIRFiQRJhJgFCISeEBwMQOQykCCIqlBpMEBIgRHOQYRIYQbPDhAbBNwgRJVwOCTIgRFMAJKDgQRGOQprBCIMSGogHBJwwbBkC2FCJNbUgMNwHYBYPJCIhODju0yFNCIUGCJGCoE2NwO24EAmw1FHgWCpMGgQOBBIMwCJGSpMmyAjDCI6eBCIWAhu2I4IRCUIYREk+Ah3brEB2CzFAAIRCl3b23btsNCJckjoRC1h2CyAREtoNC9oDC2isCCIgHBjdt5MtCJj2CowjD2uyCIOSCI83lu123tAQIRI4EB28/++39/0mwRCoARCgbfByU51/3rev+mWCIQwCPok0EYIRB/gRDpJ+EcYQRJkARQdgq/Bl5HE7IRDZAltwAREyXbCIbIFgEfCIXsBwQCDQAYRNLgvfCIXtCI44Dm3JCIUlYoYCGkrjBk9bxMkyy9CChICFA=")) +require("heatshrink").decompress(atob("mEw4cBzsE/4AClMywH680rlOW9N9kmSpICnyBBBgQRMkBUDgIRKoBoGGRYAFHBGARpARHT5MJKxQAFLgzELCIlIBQkSCIsEPRKBHCIYbGoIRFiQRJhJgFCISeEBwMQOQykCCIqlBpMEBIgRHOQYRIYQbPDhAbBNwgRJVwOCTIgRFMAJKDgQRGOQprBCIMSGogHBJwwbBkC2FCJNbUgMNwHYBYPJCIhODju0yFNCIUGCJGCoE2NwO24EAmw1FHgWCpMGgQOBBIMwCJGSpMmyAjDCI6eBCIWAhu2I4IRCUIYREk+Ah3brEB2CzFAAIRCl3b23btsNCJckjoRC1h2CyAREtoNC9oDC2isCCIgHBjdt5MtCJj2CowjD2uyCIOSCI83lu123tAQIRI4EB28/++39/0mwRCoARCgbfByU51/3rev+mWCIQwCPok0EYIRB/gRDpJ+EcYQRJkARQdgq/Bl5HE7IRDZAltwAREyXbCIbIFgEfCIXsBwQCDQAYRNLgvfCIXtCI44Dm3JCIUlYoYCGkrjBk9bxMkyy9CChICFA=")) \ No newline at end of file diff --git a/apps/hidjoystick/ChangeLog b/apps/hidjoystick/ChangeLog index 5560f00bc..625daf4bb 100644 --- a/apps/hidjoystick/ChangeLog +++ b/apps/hidjoystick/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Make Bangle.js 2 compatible diff --git a/apps/hidjoystick/app.js b/apps/hidjoystick/app.js index 134814cee..69f56463d 100644 --- a/apps/hidjoystick/app.js +++ b/apps/hidjoystick/app.js @@ -1,7 +1,86 @@ -var storage = require('Storage'); +const storage = require('Storage'); +const Layout = require("Layout"); const settings = storage.readJSON('setting.json',1) || { HID: false }; +const BANGLEJS2 = process.env.HWVERSION == 2; +const sidebarWidth=18; +const buttonWidth = (Bangle.appRect.w-sidebarWidth)/2; +const buttonHeight = (Bangle.appRect.h-16)/2*0.85; // subtract text row and add a safety margin var sendInProgress = false; // Only send one message at a time, do not flood +var touchBtn2 = 0; +var touchBtn3 = 0; +var touchBtn4 = 0; +var touchBtn5 = 0; + +function renderBtnArrows(l) { + const d = g.getWidth() - l.width; + + function c(a) { + return { + width: 8, + height: a.length, + bpp: 1, + buffer: (new Uint8Array(a)).buffer + }; + } + + g.drawImage(c([0,8,12,14,255,14,12,8]),d,g.getHeight()/2); + if (!BANGLEJS2) { + g.drawImage(c([16,56,124,254,16,16,16,16]),d,40); + g.drawImage(c([16,16,16,16,254,124,56,16]),d,194); + } +} + +const layoutChilden = []; +if (BANGLEJS2) { // add virtual buttons in display + layoutChilden.push({type:"h", c:[ + {type:"btn", width:buttonWidth, height:buttonHeight, label:"BTN2", id:"touchBtn2" }, + {type:"btn", width:buttonWidth, height:buttonHeight, label:"BTN3", id:"touchBtn3" }, + ]}); +} +layoutChilden.push({type:"h", c:[ + {type:"txt", font:"6x8:2", label:"Joystick" }, +]}); +if (BANGLEJS2) { // add virtual buttons in display + layoutChilden.push({type:"h", c:[ + {type:"btn", width:buttonWidth, height:buttonHeight, label:"BTN4", id:"touchBtn4" }, + {type:"btn", width:buttonWidth, height:buttonHeight, label:"BTN5", id:"touchBtn5" }, + ]}); +} + +const layout = new Layout( + {type:"h", c:[ + {type:"v", width:Bangle.appRect.w-sidebarWidth, c: layoutChilden}, + {type:"custom", width:18, height: Bangle.appRect.h, render:renderBtnArrows } + ]} +); + +function isInBox(box, x, y) { + return x >= box.x && x < box.x+box.w && y >= box.y && y < box.y+box.h; +} + +if (BANGLEJS2) { + Bangle.on('drag', function(event) { + if (event.b == 0) { // release + touchBtn2 = touchBtn3 = touchBtn4 = touchBtn5 = 0; + } else if (isInBox(layout.touchBtn2, event.x, event.y)) { + touchBtn2 = 1; + touchBtn3 = touchBtn4 = touchBtn5 = 0; + } else if (isInBox(layout.touchBtn3, event.x, event.y)) { + touchBtn3 = 1; + touchBtn2 = touchBtn4 = touchBtn5 = 0; + } else if (isInBox(layout.touchBtn4, event.x, event.y)) { + touchBtn4 = 1; + touchBtn2 = touchBtn3 = touchBtn5 = 0; + } else if (isInBox(layout.touchBtn5, event.x, event.y)) { + touchBtn5 = 1; + touchBtn2 = touchBtn3 = touchBtn4 = 0; + } else { + // outside any buttons, release all + touchBtn2 = touchBtn3 = touchBtn4 = touchBtn5 = 0; + } + }); +} const sendHid = function (x, y, btn1, btn2, btn3, btn4, btn5, cb) { try { @@ -20,31 +99,17 @@ const sendHid = function (x, y, btn1, btn2, btn3, btn4, btn5, cb) { function drawApp() { g.clear(); - g.setFont("6x8",2); - g.setFontAlign(0,0); - g.drawString("Joystick", 120, 120); - const d = g.getWidth() - 18; - - function c(a) { - return { - width: 8, - height: a.length, - bpp: 1, - buffer: (new Uint8Array(a)).buffer - }; - } - - g.drawImage(c([16,56,124,254,16,16,16,16]),d,40); - g.drawImage(c([16,16,16,16,254,124,56,16]),d,194); - g.drawImage(c([0,8,12,14,255,14,12,8]),d,116); + Bangle.loadWidgets(); + Bangle.drawWidgets(); + layout.render(); } function update() { - const btn1 = BTN1.read(); - const btn2 = BTN2.read(); - const btn3 = BTN3.read(); - const btn4 = BTN4.read(); - const btn5 = BTN5.read(); + const btn1 = BTN1 ? BTN1.read() : 0; + const btn2 = !BANGLEJS2 ? BTN2.read() : touchBtn2; + const btn3 = !BANGLEJS2 ? BTN3.read() : touchBtn3; + const btn4 = !BANGLEJS2 ? BTN4.read() : touchBtn4; + const btn5 = !BANGLEJS2 ? BTN5.read() : touchBtn5; const acc = Bangle.getAccel(); var x = acc.x*-127; var y = acc.y*-127; diff --git a/apps/hidjoystick/metadata.json b/apps/hidjoystick/metadata.json index e2b78a97b..c13ae2efa 100644 --- a/apps/hidjoystick/metadata.json +++ b/apps/hidjoystick/metadata.json @@ -2,11 +2,11 @@ "id": "hidjoystick", "name": "Bluetooth Joystick", "shortName": "Joystick", - "version": "0.01", - "description": "Emulates a 2 axis/5 button Joystick using the accelerometer as stick input and buttons 1-3, touch left as button 4 and touch right as button 5.", + "version": "0.02", + "description": "Emulates a 2 axis/5 button Joystick using the accelerometer as stick input and buttons 1-3, touch left as button 4 and touch right as button 5. On Bangle.js 2 buttons 2-5 are emulated with the touchscreen.", "icon": "app.png", "tags": "bluetooth", - "supports": ["BANGLEJS"], + "supports": ["BANGLEJS", "BANGLEJS2"], "storage": [ {"name":"hidjoystick.app.js","url":"app.js"}, {"name":"hidjoystick.img","url":"app-icon.js","evaluate":true} diff --git a/apps/hrm/ChangeLog b/apps/hrm/ChangeLog index e559adfb6..f05a9dc13 100644 --- a/apps/hrm/ChangeLog +++ b/apps/hrm/ChangeLog @@ -6,3 +6,4 @@ 0.06: Add widgets 0.07: Update scaling for new firmware 0.08: Don't force backlight on/watch unlocked on Bangle 2 +0.09: Grey out BPM until confidence is over 50% diff --git a/apps/hrm/heartrate.js b/apps/hrm/heartrate.js index 305f0e1bc..703b60c01 100644 --- a/apps/hrm/heartrate.js +++ b/apps/hrm/heartrate.js @@ -35,9 +35,9 @@ function onHRM(h) { g.clearRect(0,24,g.getWidth(),80); g.setFont("6x8").drawString("Confidence "+hrmInfo.confidence+"%", px, 75); var str = hrmInfo.bpm; - g.setFontVector(40).drawString(str,px,45); + g.setFontVector(40).setColor(hrmInfo.confidence > 50 ? g.theme.fg : "#888").drawString(str,px,45); px += g.stringWidth(str)/2; - g.setFont("6x8"); + g.setFont("6x8").setColor(g.theme.fg); g.drawString("BPM",px+15,45); } Bangle.on('HRM', onHRM); @@ -101,4 +101,3 @@ function readHRM() { lastHrmPt = [hrmOffset, y]; } } - diff --git a/apps/hrm/metadata.json b/apps/hrm/metadata.json index 3e94c163c..10821d094 100644 --- a/apps/hrm/metadata.json +++ b/apps/hrm/metadata.json @@ -1,7 +1,7 @@ { "id": "hrm", "name": "Heart Rate Monitor", - "version": "0.08", + "version": "0.09", "description": "Measure your heart rate and see live sensor data", "icon": "heartrate.png", "tags": "health", diff --git a/apps/lightswitch/ChangeLog b/apps/lightswitch/ChangeLog index 7a7ecd027..4210ccf03 100644 --- a/apps/lightswitch/ChangeLog +++ b/apps/lightswitch/ChangeLog @@ -1,2 +1,3 @@ 0.01: New App! 0.02: Add the option to enable touching the widget only on clock and settings. +0.03: Settings page now uses built-in min/max/wrap (fix #1607) diff --git a/apps/lightswitch/metadata.json b/apps/lightswitch/metadata.json index 902b1536b..9ac388eda 100644 --- a/apps/lightswitch/metadata.json +++ b/apps/lightswitch/metadata.json @@ -2,7 +2,7 @@ "id": "lightswitch", "name": "Light Switch Widget", "shortName": "Light Switch", - "version": "0.02", + "version": "0.03", "description": "A fast way to switch LCD backlight on/off, change the brightness and show the lock status. All in one widget.", "icon": "images/app.png", "screenshots": [ diff --git a/apps/lightswitch/settings.js b/apps/lightswitch/settings.js index aac159148..bebb16d15 100644 --- a/apps/lightswitch/settings.js +++ b/apps/lightswitch/settings.js @@ -44,9 +44,11 @@ // return entry for string value return { value: entry.value.indexOf(settings[key]), + min : 0, + max : entry.value.length-1, + wrap : true, format: v => entry.title ? entry.title[v] : entry.value[v], onchange: function(v) { - this.value = v = v >= entry.value.length ? 0 : v < 0 ? entry.value.length - 1 : v; writeSetting(key, entry.value[v], entry.drawWidgets); if (entry.exec) entry.exec(entry.value[v]); } @@ -57,8 +59,10 @@ value: settings[key] * entry.factor, step: entry.step, format: v => v > 0 ? v + entry.unit : "off", + min : entry.min, + max : entry.max, + wrap : true, onchange: function(v) { - this.value = v = v > entry.max ? entry.min : v < entry.min ? entry.max : v; writeSetting(key, v / entry.factor, entry.drawWidgets); }, }; @@ -133,16 +137,16 @@ title: "Light Switch" }, "< Back": () => back(), - "-- Widget --------": 0, + "-- Widget": 0, "Bulb col": getEntry("colors"), "Image": getEntry("image"), - "-- Control -------": 0, + "-- Control": 0, "Touch": getEntry("touchOn"), "Drag Delay": getEntry("dragDelay"), "Min Value": getEntry("minValue"), - "-- Unlock --------": 0, + "-- Unlock": 0, "TapSide": getEntry("unlockSide"), - "-- Flash ---------": 0, + "-- Flash": 0, "TapSide ": getEntry("tapSide"), "Tap": getEntry("tapOn"), "Timeout": getEntry("tOut"), diff --git a/apps/mmind/ChangeLog b/apps/mmind/ChangeLog index 939ac3b5d..040e62671 100644 --- a/apps/mmind/ChangeLog +++ b/apps/mmind/ChangeLog @@ -1 +1,2 @@ 0.01: First release +0.02: Make sure to reset turns diff --git a/apps/mmind/metadata.json b/apps/mmind/metadata.json index c2ed474b6..ea970ee23 100644 --- a/apps/mmind/metadata.json +++ b/apps/mmind/metadata.json @@ -3,7 +3,7 @@ "name": "Classic Mind Game", "shortName":"Master Mind", "icon": "mmind.png", - "version":"0.01", + "version":"0.02", "description": "This is the classic game for masterminds", "screenshots": [{"url":"screenshot_mmind.png"}], "type": "app", diff --git a/apps/mmind/mmind.app.js b/apps/mmind/mmind.app.js index e7def025d..10d315285 100644 --- a/apps/mmind/mmind.app.js +++ b/apps/mmind/mmind.app.js @@ -172,6 +172,7 @@ Bangle.on('touch', function(zone,e) { break; case 4: //new game + turn = 0; play = [-1,-1,-1,-1]; game = []; endgame=false; @@ -189,10 +190,3 @@ Bangle.on('touch', function(zone,e) { game = []; get_secret(); draw(); -//Bangle.loadWidgets(); -//Bangle.drawWidgets(); - - - - - diff --git a/apps/sleepphasealarm/app.js b/apps/sleepphasealarm/app.js index 39f9b59db..e963f2c40 100644 --- a/apps/sleepphasealarm/app.js +++ b/apps/sleepphasealarm/app.js @@ -29,11 +29,11 @@ function calc_ess(val) { if (nonmot) { slsnds+=1; if (slsnds >= sleepthresh) { - return true; // awake + return true; // sleep } } else { slsnds=0; - return false; // sleep + return false; // awake } } } diff --git a/apps/smclock/ChangeLog b/apps/smclock/ChangeLog index 0300d5ceb..2a3874d34 100644 --- a/apps/smclock/ChangeLog +++ b/apps/smclock/ChangeLog @@ -1,4 +1,6 @@ 0.01: Initial version 0.02: Add battery level -0.03: Fix battery display when full +0.03: Fix battery display when full (incorporating code by Ronin0000) 0.04: Add support for settings +0.05: Add ability to change background (3bit or 4bit) +0.06: Replace battery text with image diff --git a/apps/smclock/README.md b/apps/smclock/README.md index 635292d0c..2fc239ab2 100644 --- a/apps/smclock/README.md +++ b/apps/smclock/README.md @@ -4,13 +4,16 @@ Just a simple watch face for the Banglejs2. It shows battery level in the upper left corner, date information in the upper right, and time information in the bottom. -![](screenshot.png) +![](screenshot0.png) +![](screenshot1.png) ## Settings -**Analog Clock:** +**Analog Clock:** *Not yet implemented.* -**Human Readable Date:** When the setting is on, the date is shown in a more human-friendly format (e.g. "Oct 2"), otherwise the date is shown in a standard format (e.g. "02/10"). Default is off. +**Background:** When the setting is set as "3bit", a background with more accurate colors is chosen for the watchface. Otherwise, it uses a background following the 16-bit Mac Color Palette. + +**Date Format:** When the setting is set as "Long", the date is shown in a more human-friendly format (e.g. "Oct 2"), otherwise the date is shown in a standard format (e.g. "02/10"). Default is off. **Show Week Info:** When the setting is on, the weekday and week number are shown in the upper right box. When the setting is off, the full year is shown instead. Default is off. @@ -20,4 +23,4 @@ It shows battery level in the upper left corner, date information in the upper r Monogram Watch Face can be selected as the default clock or it can be run manually from the launcher. Its settings can be accessed and changed via the relevant menu. -Tapping on the "Alerts" area will replace the current time display with the time of the most immediate alert. +*Tapping on the "Alerts" area will replace the current time display with the time of the most immediate alert.* - *Feature not implemented yet.* diff --git a/apps/smclock/app.js b/apps/smclock/app.js index 350c0dd07..41bc2b5e4 100644 --- a/apps/smclock/app.js +++ b/apps/smclock/app.js @@ -1,23 +1,23 @@ const SETTINGSFILE = "smclock.json"; -const background = { - width: 176, - height: 176, - bpp: 3, - transparent: 1, - buffer: require("heatshrink").decompress( - atob( - "/4A/AH4ACUb8H9MkyVJAThB/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/INP/AH4A/AAX8Yz4Afn5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/INI=" - ) - ), +const image3bit = { + width : 176, height : 176, bpp : 3, + transparent : 1, + buffer : require("heatshrink").decompress(atob("/4A/AH4AC23btoCct/pkmSpICcIP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5Bp/4A/AH4AC/kAAH0/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5B/IP5BpA=")) +}; +const image4bit = { + width : 176, height : 176, bpp : 4, + transparent : 1, + buffer : require("heatshrink").decompress(atob("/4A/AH4Au1QAp1/2swApK/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K/5X/K+//AH4A/AF8AAH4AUK/5X/K/4A/K/5X/K/4A/K/5X/AH5X/K/5X/AH5X/K/5X/AH5X/K/4A/K/5X/K/4A/K/5X/K/4A/K/5X/AH5X/K/5X/AH5X/K/5X/AH5X/K/4A/K/5X/K/4A/K/5X/K/4A/K/5X/AH5X/K/5X/AH5X/K/5X/AH5X/K/4A/K/5X/K/4A/K/5X/K/4A/K/5X/AH5X/K/5X/AH5X/K/5X/AH5X/K/4A/K/5X/K/4A/K/5X/K/4A/K/5X/AH5X/K/5X/AH5X/K/5X/AH5X/K/4A/K/5X/K/4A/K/5X/K/AA==")) }; const monthName = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]; const weekday = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; // dynamic variables var batLevel = -1; -var batColor = [0, 0, 0]; +var batColor = ""; // settings variables +var backgroundImage; var dateFormat; var drawInterval; var pollInterval; @@ -31,6 +31,7 @@ function loadSettings() { function def(value, def) {return value !== undefined ? value : def;} var settings = require("Storage").readJSON(SETTINGSFILE, true) || {}; + backgroundImage = def(settings.backgroundImage, "3bit"); dateFormat = def(settings.dateFormat, "Short"); drawInterval = def(settings.drawInterval, 10); pollInterval = def(settings.pollInterval, 60); @@ -67,23 +68,29 @@ function getBatteryColor(level) { level = batLevel; } if (level > 80) { - color = [0, 0, 1]; + color = "#00f"; } else if (level > 60) { - color = [0, 1, 1]; + color = "#0ff"; } else if (level > 40) { - color = [0, 1, 0]; + color = "#0f0"; } else if (level > 20) { - color = [1, 1, 0]; + color = "#f40"; } else { - color = [1, 0, 0]; + color = "f00"; } return color; } function draw() { + var background; + if (backgroundImage == "3bit") { + background = image3bit; + } else { + background = image4bit; + } g.drawImage(background); - const color = getBatteryColor(batLevel); + batColor = getBatteryColor(batLevel); var bat = ""; const d = new Date(); const day = d.getDate(); @@ -95,32 +102,38 @@ function draw() { const m = d.getMinutes(); const time = d02(h) + ":" + d02(m); - if (E.getBattery() < 100) { - bat = d02(E.getBattery()) + "%"; - } else { - bat = E.getBattery() + "%"; - } - g.reset(); // draw battery info - g.setColor(1, 1, 1); + var x = 12; + var y = 16; + if (Bangle.isCharging()) { + g.setColor("#ff0").drawImage(atob("DhgBHOBzgc4HOP////////////////////3/4HgB4AeAHgB4AeAHgB4AeAHg"),x,y); + } else { + g.clearRect(x,y,x+14,y+24); + g.setColor("#000").fillRect(x+2,y+2,x+12,y+22).clearRect(x+4,y+4,x+10,y+20).fillRect(x+5,y+1,x+9,y+2); + g.setColor(batColor).fillRect(x+4,y+20-(batLevel*16/100),x+10,y+20); + } + if (Bangle.isCharging()) { + g.setColor("#ff0"); + } else { + g.setColor(batColor); + } if (useVectorFont == true) { g.setFont("Vector", 16); - g.drawString("Bat:", 12, 22, false); } else { - g.setFont("4x6", 2); - g.drawString("Bat:", 10, 22, false); + g.setFont("4x6", 3); } - g.setColor(color[0], color[1], color[2]); if (batLevel < 100) { - g.drawString(bat, 52, 22, false); + bat = d02(batLevel) + "%"; + g.drawString(bat, 50, 22, false); } else { - g.drawString(bat, 46, 22, false); + bat = "100%"; + g.drawString(bat, 40, 22, false); } // draw date info - g.setColor(0, 0, 0); + g.setColor("#000"); if (useVectorFont == true) { g.setFont("Vector", 20); } else { @@ -136,7 +149,7 @@ function draw() { // draw week info if (showWeekInfo == true) { - date2 = weekday[d.getDay()] + " " + d02(week) + date2 = weekday[d.getDay()] + " " + d02(week); if (useVectorFont == true) { g.setFont("Vector", 18); } else { @@ -155,7 +168,7 @@ function draw() { } // draw time - g.setColor(1, 1, 1); + g.setColor("#fff"); if (useVectorFont == true) { g.setFont("Vector", 60); g.drawString(time, 10, 108, false); diff --git a/apps/smclock/metadata.json b/apps/smclock/metadata.json index cc995d587..55668adcc 100644 --- a/apps/smclock/metadata.json +++ b/apps/smclock/metadata.json @@ -3,13 +3,13 @@ "name": "Monogram Watch Face", "shortName": "MonoClock", "icon": "app.png", - "screenshots": [{ "url": "screenshot.png" }], + "screenshots": [{ "url": "screenshot0.png" }, {"url": "screenshot1.png" }], "version": "0.04", "description": "A simple watchface based on my stylised monogram.", "type": "clock", "tags": "clock", "readme": "README.md", - "supports": ["BANGLEJS", "BANGLEJS2"], + "supports": ["BANGLEJS2"], "allow_emulator": true, "storage": [ { "name": "smclock.app.js", "url": "app.js" }, diff --git a/apps/smclock/screenshot.png b/apps/smclock/screenshot.png deleted file mode 100644 index c0e0bd0ee..000000000 Binary files a/apps/smclock/screenshot.png and /dev/null differ diff --git a/apps/smclock/screenshot0.png b/apps/smclock/screenshot0.png new file mode 100644 index 000000000..07eff8ddf Binary files /dev/null and b/apps/smclock/screenshot0.png differ diff --git a/apps/smclock/screenshot1.png b/apps/smclock/screenshot1.png new file mode 100644 index 000000000..da25b2579 Binary files /dev/null and b/apps/smclock/screenshot1.png differ diff --git a/apps/smclock/settings.js b/apps/smclock/settings.js index a6c7d1b98..ee4a35a26 100644 --- a/apps/smclock/settings.js +++ b/apps/smclock/settings.js @@ -52,6 +52,7 @@ writeSettings(); }, }, + "Background": stringInSettings("backgroundImage", ["3bit", "4bit"]), Date: stringInSettings("dateFormat", ["Long", "Short"]), "Draw Interval": { value: settings.drawInterval, diff --git a/apps/terminalclock/ChangeLog b/apps/terminalclock/ChangeLog index 6515ab627..14159bc19 100644 --- a/apps/terminalclock/ChangeLog +++ b/apps/terminalclock/ChangeLog @@ -1,2 +1,3 @@ 0.01: New App! -0.02: Rename "Activity" in "Motion" and display the true values for it +0.02: Rename "Activity" in "Motion" and display the true values for it +0.03: Add Banglejs 1 compatibility diff --git a/apps/terminalclock/app.js b/apps/terminalclock/app.js index ab83a696f..d219b84d8 100644 --- a/apps/terminalclock/app.js +++ b/apps/terminalclock/app.js @@ -1,16 +1,28 @@ var locale = require("locale"); var fontColor = g.theme.dark ? "#0f0" : "#000"; -var paddingY = 2; -var font6x8At4Size = 32; -var font6x8At2Size = 18; var heartRate = 0; +// handling the differents versions of the Banglejs smartwatch +if (process.env.HWVERSION == 1){ + var paddingY = 3; + var font6x8At4Size = 48; + var font6x8At2Size = 27; + var font6x8FirstTextSize = 6; + var font6x8DefaultTextSize = 3; +} +else{ + var paddingY = 2; + var font6x8At4Size = 32; + var font6x8At2Size = 18; + var font6x8FirstTextSize = 4; + var font6x8DefaultTextSize = 2; +} function setFontSize(pos){ if(pos == 1) - g.setFont("6x8", 4); + g.setFont("6x8", font6x8FirstTextSize); else - g.setFont("6x8", 2); + g.setFont("6x8", font6x8DefaultTextSize); } function clearField(pos){ diff --git a/apps/terminalclock/metadata.json b/apps/terminalclock/metadata.json index de0244318..de369bf10 100644 --- a/apps/terminalclock/metadata.json +++ b/apps/terminalclock/metadata.json @@ -3,11 +3,12 @@ "name": "Terminal Clock", "shortName":"Terminal Clock", "description": "A terminal cli like clock displaying multiple sensor data", - "version":"0.02", + "version":"0.03", "icon": "app.png", "type": "clock", "tags": "clock", - "supports": ["BANGLEJS2"], + "supports": ["BANGLEJS", "BANGLEJS2"], + "allow_emulator": true, "readme": "README.md", "storage": [ {"name": "terminalclock.app.js","url": "app.js"}, diff --git a/apps/touchtimer/ChangeLog b/apps/touchtimer/ChangeLog index 01904c6ea..0969a3da4 100644 --- a/apps/touchtimer/ChangeLog +++ b/apps/touchtimer/ChangeLog @@ -1,2 +1,4 @@ 0.01: Initial creation of the touch timer app -0.02: Add settings menu \ No newline at end of file +0.02: Add settings menu +0.03: Add ability to repeat last timer +0.04: Add 5 second count down buzzer diff --git a/apps/touchtimer/app.js b/apps/touchtimer/app.js index ffa1af80a..c2f2fb5e9 100644 --- a/apps/touchtimer/app.js +++ b/apps/touchtimer/app.js @@ -126,6 +126,14 @@ var main = () => { timerIntervalId = setInterval(() => { timerCountDown.draw(); + // Buzz lightly when there are less then 5 seconds left + if (settings.countDownBuzz) { + var remainingSeconds = timerCountDown.getAdjustedTime().seconds; + if (remainingSeconds <= 5 && remainingSeconds > 0) { + Bangle.buzz(); + } + } + if (timerCountDown.isFinished()) { buttonStartPause.value = "FINISHED!"; buttonStartPause.draw(); @@ -141,6 +149,13 @@ var main = () => { if (buzzCount >= settings.buzzCount) { clearInterval(buzzIntervalId); buzzIntervalId = undefined; + + buttonStartPause.value = "REPEAT"; + buttonStartPause.draw(); + buttonStartPause.value = "START"; + timerCountDown = undefined; + timerEdit.draw(); + return; } else { Bangle.buzz(settings.buzzDuration * 1000, 1); diff --git a/apps/touchtimer/metadata.json b/apps/touchtimer/metadata.json index 645a0ce18..0f2b9f491 100644 --- a/apps/touchtimer/metadata.json +++ b/apps/touchtimer/metadata.json @@ -2,7 +2,7 @@ "id": "touchtimer", "name": "Touch Timer", "shortName": "Touch Timer", - "version": "0.02", + "version": "0.04", "description": "Quickly and easily create a timer with touch-only input. The time can be easily set with a number pad.", "icon": "app.png", "tags": "tools", diff --git a/apps/touchtimer/settings.js b/apps/touchtimer/settings.js index 885670f57..79424f250 100644 --- a/apps/touchtimer/settings.js +++ b/apps/touchtimer/settings.js @@ -31,6 +31,14 @@ writeSettings(settings); }, }, + "CountDown Buzz": { + value: !!settings.countDownBuzz, + format: value => value?"On":"Off", + onchange: (value) => { + settings.countDownBuzz = value; + writeSettings(settings); + }, + }, "Pause Between": { value: settings.pauseBetween, min: 1,