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:19:42 +03:00
var plist = [[ 29 , 0 ], [ 29 , 2 ], [ 29 , 4 ]]. concat (( watchModel . indexOf ( 'OCW' ) > 5 || watchModel . indexOf ( 'B2100' ) > 5 ) ? [[ 30 , 0 ],[ 30 , 1 ],[ 31 , 0 ],[ 31 , 1 ]] : [... plgen ( 30 ),... plgen ( 31 )]); //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 )