Files

152 lines
6.2 KiB
JavaScript
Raw Permalink 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-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-12-23 21:16:49 +02:00
if(watchModel.indexOf('OCW') > 5 || watchModel.indexOf('GWR-B1000') > 5)
2022-10-25 17:29:14 +03:00
slist = [[30,0],[30,1]]
2022-12-23 21:16:49 +02:00
else if(watchModel.indexOf('B2100') > 5
|| watchModel.indexOf('B001') > 5
|| watchModel.indexOf('DW-B5600') > 5
|| watchModel.indexOf('GST-B') > 5
|| watchModel.indexOf('MSG-B') > 5
|| watchModel.indexOf('ECB') > 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)
2023-09-05 12:10:02 +03:00
.then(execWriteCmd)
2022-10-04 17:47:08 +03:00
.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)