2022-10-04 17:47:08 +03:00
|
|
|
// RCVD.js: a small library for syncing time on BLE-enabled Casio watches
|
2022-12-22 17:25:30 +02:00
|
|
|
// Tested on: GW-B5600BC, GMW-B5000D, OCW-T200S, GM-B2100BD, GA-B001G, DW-B5600G
|
2022-10-04 17:47:08 +03:00
|
|
|
// 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
|
2022-12-23 17:02:10 +02:00
|
|
|
syncDelta = 500, //the delta value (in milliseconds) to make up for BLE transmission latency, 500 ms by default
|
2022-10-04 17:47:08 +03:00
|
|
|
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) => {
|
2022-12-23 17:02:10 +02:00
|
|
|
var slist = [...plgen(30),...plgen(31)] //full property list for primary targets, GW-B/GMW-B models
|
2022-10-25 17:29:14 +03:00
|
|
|
if(watchModel.indexOf('OCW') > 5)
|
|
|
|
|
slist = [[30,0],[30,1]]
|
2022-12-22 17:25:30 +02:00
|
|
|
else if(watchModel.indexOf('B2100') > 5 || watchModel.indexOf('B001') > 5 || watchModel.indexOf('DW-B5600') > 5 )
|
2022-10-25 17:29:14 +03:00
|
|
|
slist = [[30,0],[30,1],[31,0],[31,1]]
|
|
|
|
|
var plist = [[29,0], [29,2], [29,4]].concat(slist); //populate the props list
|
2022-10-04 17:47:08 +03:00
|
|
|
(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,
|
2022-12-23 17:02:10 +02:00
|
|
|
setSyncDelta: val => {syncDelta = +val; if(isNaN(syncDelta)) syncDelta = 500},
|
2022-10-04 17:47:08 +03:00
|
|
|
sync: dObj => new Promise((res, rej) => {
|
|
|
|
|
cyclePresyncProperties().then(_ => {
|
2022-12-23 17:02:10 +02:00
|
|
|
var d = dObj || new Date(Date.now() + syncDelta), year = d.getFullYear()
|
2022-10-04 17:47:08 +03:00
|
|
|
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)
|