diff --git a/apps.json b/apps.json index 622216ecf..688518bb0 100644 --- a/apps.json +++ b/apps.json @@ -2251,5 +2251,18 @@ {"name":"digiclock.app.js","url":"digiclock.js"}, {"name":"digiclock.img","url":"digiclock-icon.js","evaluate":true} ] -} +}, + { "id": "dsdrelay", + "name": "DSD BLE Relay controller", + "shortName":"DSDRelay", + "icon": "icons8-relay-48.png", + "version":"0.01", + "description": "Control BLE relay board from the watch", + "tags": "ble,bluetooth", + "readme": "README.md", + "storage": [ + {"name":"dsdrelay.app.js","url":"dsdrelay.app.js"}, + {"name":"dsdrelay.img","url":"dsdrelay-icon.js","evaluate":true} + ] + } ] diff --git a/apps/dsdrelay/ChangeLog b/apps/dsdrelay/ChangeLog new file mode 100644 index 000000000..1a3bc1757 --- /dev/null +++ b/apps/dsdrelay/ChangeLog @@ -0,0 +1 @@ +0.01: New app! diff --git a/apps/dsdrelay/README.md b/apps/dsdrelay/README.md new file mode 100644 index 000000000..395aba636 --- /dev/null +++ b/apps/dsdrelay/README.md @@ -0,0 +1,14 @@ +# DSDRelay + +Small app to control DSD Tech BLE relay boards from the watch. I have seen them being sold as 1-, 2- and 4-relay boards. The app shows controls for +4 relays, regardless of the actual configuration of the board connected. + +![](dsdrelay-pic.jpg) + +## Controls +- buttons 1 and 3 cycle the selection of the currently active channel +- swipe right turns the selected channel's relay *on* +- swipe left turns the selected channel's relay *off* + +I only own a 1-relay board, so only the "Ch 1" functionality was tested; the other channels were implemented per the manufacturer's documentation. +In particular, the method for determining the relay states on app startup for channels 2-4 was mostly an educated guess. diff --git a/apps/dsdrelay/dsdrelay-icon.js b/apps/dsdrelay/dsdrelay-icon.js new file mode 100644 index 000000000..ac98e6eea --- /dev/null +++ b/apps/dsdrelay/dsdrelay-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AH4A/AAcK1QAO0AXFCx4ABFyowGC/4X/C/4X/C48AC6IWEGCIuFAAUN6AED7vdAwgEDC/4X/C/4X86AGGC85fpAH4A/AH4ASA")) diff --git a/apps/dsdrelay/dsdrelay-pic.jpg b/apps/dsdrelay/dsdrelay-pic.jpg new file mode 100644 index 000000000..5bffebe18 Binary files /dev/null and b/apps/dsdrelay/dsdrelay-pic.jpg differ diff --git a/apps/dsdrelay/dsdrelay.app.js b/apps/dsdrelay/dsdrelay.app.js new file mode 100644 index 000000000..18e7293aa --- /dev/null +++ b/apps/dsdrelay/dsdrelay.app.js @@ -0,0 +1,116 @@ +var device; +var gatt; +var service; +var characteristic; + +// on/off commands +// Channel 1 ON: A00101A2 +// Channel 1 OFF: A00100A1 +// Channel 2 ON: A00201A3 +// Channel 2 OFF: A00200A2 +// Channel 3 ON: A00301A4 +// Channel 3 OFF: A00300A3 +// Channel 4 ON: A00401A5 +// Channel 4 OFF: A00400A4 + +var cmds = [{ on: new Uint8Array([0xa0, 0x01, 0x01, 0xa2]), + off: new Uint8Array([0xa0, 0x01, 0x00, 0xa1]) }, + { on: new Uint8Array([0xa0, 0x02, 0x01, 0xa3]), + off: new Uint8Array([0xa0, 0x02, 0x00, 0xa2]) }, + { on: new Uint8Array([0xa0, 0x03, 0x01, 0xa4]), + off: new Uint8Array([0xa0, 0x03, 0x00, 0xa3]) }, + { on: new Uint8Array([0xa0, 0x04, 0x01, 0xa5]), + off: new Uint8Array([0xa0, 0x04, 0x00, 0xa4]) }]; + +const button_w = 100; +const button_h = 36; +const button_r = button_h/2-4; +const button_sp = 46; + +var n_channels = 4; +var channel = 0; +var channel_state = []; + +function drawButton(x, y, state) { + if (state) g.setColor(0.2, 0.2, 0.95); + else g.setColor(0.5, 0.5, 0.5); + g.fillCircle(x+button_h/2, y+button_h/2, button_h/2). + fillRect(x+button_h/2, y, x+button_w-button_h/2, y+button_h). + fillCircle(x+button_w-button_h/2, y+button_h/2, button_h/2); + g.setColor(0.85, 0.85, 0.85); + if (state) + g.fillCircle(x+button_w-button_h/2, y+button_h/2, button_r); + else + g.fillCircle(x+button_h/2, y+button_h/2, button_r); + g.flip(); +} + +function setup_screen() { + g.clearRect(0, 60, g.getWidth()-1, g.getHeight()-1); + for (var i=0; i<4; ++i) { + g.setFontVector(22).setFontAlign(-1, 0, 0).setColor(0xffff).drawString("Ch"+String(i+1), 16, 60+i*button_sp+button_h/2); + drawButton((g.getWidth()-button_w)/2, 60+i*button_sp, channel_state[i]); + } + moveChannelFrame(channel, channel); +} + +function parseDevice(d) { + device = d; + g.clearRect(0, 60, 239, 239).setFontAlign(0, 0, 0).setColor(0, 1, 0).drawString("Found device", 120, 120).flip(); + device.gatt.connect().then(function(ga) { + gatt = ga; + g.clearRect(0, 60, 239, 239).setFontAlign(0, 0, 0).setColor(0, 1, 0).drawString("Connected", 120, 120).flip(); + return gatt.getPrimaryService("FFE0"); +}).then(function(s) { + service = s; + return service.getCharacteristic("FFE1"); +}).then(function(c) { + characteristic = c; + console.log(c); + return; +}).then(function() { + console.log("Done!"); + g.clearRect(0, 60, 239, 239).setColor(1, 1, 1).flip(); + setup_app(); +}).catch(function(e) { + g.clearRect(0, 60, 239, 239).setColor(1, 0, 0).setFontAlign(0, 0, 0).drawString("ERROR"+e, 120, 120).flip(); + console.log(e); +})} + +function connection_setup() { + NRF.setScan(); + NRF.setScan(parseDevice, { filters: [{services:["FFE0"]}], timeout: 2000}); + g.clearRect(0, 60, 239, 239).setFontVector(18).setFontAlign(0, 0, 0).setColor(0, 1, 0); + g.drawString("Scanning for relay...", 120, 120); +} + +function moveChannelFrame(oldc, newc) { + g.setColor(0).drawRect(8, 60+oldc*button_sp-4, g.getWidth()-8, 60+oldc*button_sp+button_h+4); + g.setColor(0.9, 0.9, 0.9).drawRect(8, 60+newc*button_sp-4, g.getWidth()-8, 60+newc*button_sp+button_h+4); +} + +function setup_app() { + characteristic.readValue().then(function(r) { + for (var i=0; i