Files
rcvd/rcvd.js
T

146 lines
5.7 KiB
JavaScript
Raw Normal View History

2022-10-04 17:47:08 +03:00
// RCVD.js: a small library for syncing time on BLE-enabled Casio watches
2022-10-25 17:19:42 +03:00
// Tested on: GW-B5600BC, GMW-B5000D, OCW-T200S, GM-B2100BD
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
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-10-25 17:29:14 +03:00
var slist = [...plgen(30),...plgen(31)]
if(watchModel.indexOf('OCW') > 5)
slist = [[30,0],[30,1]]
else if(watchModel.indexOf('B2100') > 5)
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,
sync: dObj => new Promise((res, rej) => {
cyclePresyncProperties().then(_ => {
//set to the next second to make up for latency
var d = dObj || new Date(Date.now() + 1000), 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)