// RCVD.js: a small library for syncing time on BLE-enabled Casio watches // Tested on: GW-B5600BC, GMW-B5000D, OCW-T200S, GM-B2100BD, GA-B001G, DW-B5600G // Compatible with both KaiOS 2.5.x and in-progress Web Bluetooth API spec // Created by Luxferre in 2022, released into public domain RCVD = (function(nav) { var readerChar, writerChar, outputQueue, wt, isKai = 'mozBluetooth' in nav && !('bluetooth' in nav), //whether or not we're interacting using non-standard KaiOS 2.5.x API btx = isKai ? nav.mozBluetooth : nav.bluetooth, waitForOutput = _ => new Promise((res, rej) => { (wt = function() { if(outputQueue.length) res(outputQueue.pop()) else setTimeout(wt, 1) })() }), watchModel = '', localgatt = null, //local GATT reference for disconnection syncDelta = 500, //the delta value (in milliseconds) to make up for BLE transmission latency, 500 ms by default TD = new TextDecoder(), ids = s => `26eb00${s}-b012-49a8-b1f8-394fb2032b0f`, scanids = ['00001804-0000-1000-8000-00805f9b34fb', ids('0d')], connectWatch = disconnectCb => new Promise((res, rej) => { if(!disconnectCb) disconnectCb = ()=>{} if(isKai) { var adp = btx.defaultAdapter adp.startLeScan(scanids).then(dHandle => { dHandle.ondevicefound = e => { adp.stopLeScan(dHandle) localgatt = e.device.gatt localgatt.connect(false).then(_ => { outputQueue = [] localgatt.oncharacteristicchanged = e => { outputQueue.push(new Uint8Array(e.characteristic.value)) } localgatt.discoverServices().then(_ => { for(let srv of localgatt.services) { for(var chr of srv.characteristics) { if(chr.uuid === ids('2c')) { readerChar = chr readerChar.writeValueWithoutResponse = readerChar.writeValue } else if(chr.uuid === ids('2d')) { writerChar = chr writerChar.writeValueWithResponse = writerChar.writeValue } } } writerChar.startNotifications().then(res).catch(rej) }) }).catch(rej) } }).catch(rej) } else btx.requestDevice({filters:[{services:[scanids[0]]},{services:[scanids[1]]}]}) .then(dev => { dev.addEventListener('gattserverdisconnected', disconnectCb) localgatt = dev.gatt return localgatt.connect() }) .then(conn => conn.getPrimaryService(ids('0d'))) .then(srv => srv.getCharacteristics()) .then(arr => { for(let chr of arr) { if(chr.uuid === ids('2c')) readerChar = chr else if(chr.uuid === ids('2d')) writerChar = chr } outputQueue = [] writerChar.startNotifications().then(chr => { chr.addEventListener('characteristicvaluechanged', e => { outputQueue.push(new Uint8Array(e.target.value.buffer)) }) res() }).catch(rej) }).catch(rej) }), execReadCmd = (...args) => readerChar.writeValueWithoutResponse(Uint8Array.from(args).buffer).then(waitForOutput), execWriteCmd = cmddata => writerChar.writeValueWithResponse(cmddata.buffer), //some logic needed to init the time sync process //these parameters need to be read from the watch and then written back plgen = k => [...Array(6)].map((a,i)=>[k,i]), //property list generator cyclePresyncProperties = _ => new Promise((res, rej) => { var slist = [...plgen(30),...plgen(31)] //full property list for primary targets, GW-B/GMW-B models if(watchModel.indexOf('OCW') > 5) slist = [[30,0],[30,1]] else if(watchModel.indexOf('B2100') > 5 || watchModel.indexOf('B001') > 5 || watchModel.indexOf('DW-B5600') > 5 ) slist = [[30,0],[30,1],[31,0],[31,1]] var plist = [[29,0], [29,2], [29,4]].concat(slist); //populate the props list (function cycle() { var p = plist.shift() if(p) //we still have properties execReadCmd.apply(null, p) .then(execWriteCmd) .then(cycle).catch(rej) else //properties over res() })() }) return { connect: dcb => new Promise((res, rej) => { //resolve to true if full setting mode, or false if just sync connectWatch(dcb) .then(_ => execReadCmd(0x23).then(v => watchModel = TD.decode(v).slice(1).replace(/\x00/g,''))) .then(_ => execReadCmd(0x10)) .then(features => { if(features[8]<2) { //we entered full setting mode, sync application info first execReadCmd(0x22).then(info => //set the correct initialization after BLE reset on the watch (info[11] || Math.min(...info.slice(1,11))^255) ? info : execWriteCmd(Uint8Array.from([0x22, 0x34, 0x88, 0xF4, 0xE5, 0xD5, 0xAF, 0xC8, 0x29, 0xE0, 0x6D, 0x02])) ).then(info => { if(info instanceof Promise) info.then(_=>res(true)).catch(rej) else res(true) }).catch(rej) } else res(false) }).catch(rej) }), getModel: _ => watchModel, rawRead: execReadCmd, rawWrite: execWriteCmd, setSyncDelta: val => {syncDelta = +val; if(isNaN(syncDelta)) syncDelta = 500}, sync: dObj => new Promise((res, rej) => { cyclePresyncProperties().then(_ => { var d = dObj || new Date(Date.now() + syncDelta), year = d.getFullYear() execWriteCmd(Uint8Array.from([ 9, year&255, year>>>8, d.getMonth()+1, d.getDate(), d.getHours(), d.getMinutes(), d.getSeconds(), d.getDay(), 0, 0, 1 ])).then(res).catch(res) //not a mistake! }).catch(rej) }), disconnect: _ => { if(localgatt) { localgatt.disconnect() localgatt = null } } } })(navigator)