rcvd.js (6333B)
1 // RCVD.js: a small library for syncing time on BLE-enabled Casio watches 2 // Tested on: GW-B5600BC, GMW-B5000D, OCW-T200S, GM-B2100BD, GA-B001G, DW-B5600G 3 // Compatible with both KaiOS 2.5.x and in-progress Web Bluetooth API spec 4 // Created by Luxferre in 2022, released into public domain 5 6 RCVD = (function(nav) { 7 var readerChar, writerChar, outputQueue, wt, 8 isKai = 'mozBluetooth' in nav && !('bluetooth' in nav), //whether or not we're interacting using non-standard KaiOS 2.5.x API 9 btx = isKai ? nav.mozBluetooth : nav.bluetooth, 10 waitForOutput = _ => new Promise((res, rej) => { 11 (wt = function() { 12 if(outputQueue.length) 13 res(outputQueue.pop()) 14 else setTimeout(wt, 1) 15 })() 16 }), 17 watchModel = '', 18 localgatt = null, //local GATT reference for disconnection 19 syncDelta = 500, //the delta value (in milliseconds) to make up for BLE transmission latency, 500 ms by default 20 TD = new TextDecoder(), 21 ids = s => `26eb00${s}-b012-49a8-b1f8-394fb2032b0f`, 22 scanids = ['00001804-0000-1000-8000-00805f9b34fb', ids('0d')], 23 connectWatch = disconnectCb => new Promise((res, rej) => { 24 if(!disconnectCb) disconnectCb = ()=>{} 25 if(isKai) { 26 var adp = btx.defaultAdapter 27 adp.startLeScan(scanids).then(dHandle => { 28 dHandle.ondevicefound = e => { 29 adp.stopLeScan(dHandle) 30 localgatt = e.device.gatt 31 localgatt.connect(false).then(_ => { 32 outputQueue = [] 33 localgatt.oncharacteristicchanged = e => { 34 outputQueue.push(new Uint8Array(e.characteristic.value)) 35 } 36 localgatt.discoverServices().then(_ => { 37 for(let srv of localgatt.services) { 38 for(var chr of srv.characteristics) { 39 if(chr.uuid === ids('2c')) { 40 readerChar = chr 41 readerChar.writeValueWithoutResponse = readerChar.writeValue 42 } 43 else if(chr.uuid === ids('2d')) { 44 writerChar = chr 45 writerChar.writeValueWithResponse = writerChar.writeValue 46 } 47 } 48 } 49 writerChar.startNotifications().then(res).catch(rej) 50 }) 51 }).catch(rej) 52 } 53 }).catch(rej) 54 } 55 else btx.requestDevice({filters:[{services:[scanids[0]]},{services:[scanids[1]]}]}) 56 .then(dev => { 57 dev.addEventListener('gattserverdisconnected', disconnectCb) 58 localgatt = dev.gatt 59 return localgatt.connect() 60 }) 61 .then(conn => conn.getPrimaryService(ids('0d'))) 62 .then(srv => srv.getCharacteristics()) 63 .then(arr => { 64 for(let chr of arr) { 65 if(chr.uuid === ids('2c')) 66 readerChar = chr 67 else if(chr.uuid === ids('2d')) 68 writerChar = chr 69 } 70 outputQueue = [] 71 writerChar.startNotifications().then(chr => { 72 chr.addEventListener('characteristicvaluechanged', e => { 73 outputQueue.push(new Uint8Array(e.target.value.buffer)) 74 }) 75 res() 76 }).catch(rej) 77 }).catch(rej) 78 }), 79 execReadCmd = (...args) => readerChar.writeValueWithoutResponse(Uint8Array.from(args).buffer).then(waitForOutput), 80 execWriteCmd = cmddata => writerChar.writeValueWithResponse(cmddata.buffer), 81 82 //some logic needed to init the time sync process 83 //these parameters need to be read from the watch and then written back 84 85 plgen = k => [...Array(6)].map((a,i)=>[k,i]), //property list generator 86 cyclePresyncProperties = _ => new Promise((res, rej) => { 87 var slist = [...plgen(30),...plgen(31)] //full property list for primary targets, GW-B/GMW-B models 88 if(watchModel.indexOf('OCW') > 5 || watchModel.indexOf('GWR-B1000') > 5) 89 slist = [[30,0],[30,1]] 90 else if(watchModel.indexOf('B2100') > 5 91 || watchModel.indexOf('B001') > 5 92 || watchModel.indexOf('DW-B5600') > 5 93 || watchModel.indexOf('GST-B') > 5 94 || watchModel.indexOf('MSG-B') > 5 95 || watchModel.indexOf('ECB') > 5 ) 96 slist = [[30,0],[30,1],[31,0],[31,1]] 97 var plist = [[29,0], [29,2], [29,4]].concat(slist); //populate the props list 98 (function cycle() { 99 var p = plist.shift() 100 if(p) //we still have properties 101 execReadCmd.apply(null, p) 102 .then(execWriteCmd) 103 .then(cycle).catch(rej) 104 else //properties over 105 res() 106 })() 107 }) 108 109 return { 110 connect: dcb => new Promise((res, rej) => { //resolve to true if full setting mode, or false if just sync 111 connectWatch(dcb) 112 .then(_ => execReadCmd(0x23).then(v => watchModel = TD.decode(v).slice(1).replace(/\x00/g,''))) 113 .then(_ => execReadCmd(0x10)) 114 .then(features => { 115 if(features[8]<2) { //we entered full setting mode, sync application info first 116 execReadCmd(0x22).then(info => //set the correct initialization after BLE reset on the watch 117 (info[11] || Math.min(...info.slice(1,11))^255) ? info : 118 execWriteCmd(Uint8Array.from([0x22, 0x34, 0x88, 0xF4, 0xE5, 0xD5, 0xAF, 0xC8, 0x29, 0xE0, 0x6D, 0x02])) 119 ).then(info => { 120 if(info instanceof Promise) 121 info.then(_=>res(true)).catch(rej) 122 else res(true) 123 }).catch(rej) 124 } 125 else res(false) 126 }).catch(rej) 127 }), 128 getModel: _ => watchModel, 129 rawRead: execReadCmd, 130 rawWrite: execWriteCmd, 131 setSyncDelta: val => {syncDelta = +val; if(isNaN(syncDelta)) syncDelta = 500}, 132 sync: dObj => new Promise((res, rej) => { 133 cyclePresyncProperties().then(_ => { 134 var d = dObj || new Date(Date.now() + syncDelta), year = d.getFullYear() 135 execWriteCmd(Uint8Array.from([ 136 9, 137 year&255, year>>>8, 138 d.getMonth()+1, d.getDate(), 139 d.getHours(), d.getMinutes(), d.getSeconds(), 140 d.getDay(), 0, 0, 1 141 ])).then(res).catch(res) //not a mistake! 142 }).catch(rej) 143 }), 144 disconnect: _ => { 145 if(localgatt) { 146 localgatt.disconnect() 147 localgatt = null 148 } 149 } 150 } 151 })(navigator)