rcvd

A JS library and a KaiOS 2.5.x application for BLE-enabled Casio watch time synchronization
git clone git://git.luxferre.top/rcvd.git
Log | Files | Refs | README | LICENSE

rcvd.js (6333B)


      1 // RCVD.js: a small library for syncing time on BLE-enabled Casio watches
      2 // Tested on: GW-B5600BC, GMW-B5000D, OCW-T200S, GM-B2100BD, GA-B001G, DW-B5600G
      3 // Compatible with both KaiOS 2.5.x and in-progress Web Bluetooth API spec
      4 // Created by Luxferre in 2022, released into public domain
      5 
      6 RCVD = (function(nav) {
      7   var readerChar, writerChar, outputQueue, wt,
      8     isKai = 'mozBluetooth' in nav && !('bluetooth' in nav), //whether or not we're interacting using non-standard KaiOS 2.5.x API
      9     btx = isKai ? nav.mozBluetooth : nav.bluetooth,
     10     waitForOutput = _ => new Promise((res, rej) => {
     11       (wt = function() {
     12         if(outputQueue.length)
     13           res(outputQueue.pop())
     14         else setTimeout(wt, 1)
     15       })()
     16     }),
     17     watchModel = '',
     18     localgatt = null, //local GATT reference for disconnection
     19     syncDelta = 500, //the delta value (in milliseconds) to make up for BLE transmission latency, 500 ms by default
     20     TD = new TextDecoder(),
     21     ids = s => `26eb00${s}-b012-49a8-b1f8-394fb2032b0f`,
     22     scanids = ['00001804-0000-1000-8000-00805f9b34fb', ids('0d')],
     23     connectWatch = disconnectCb => new Promise((res, rej) => {
     24       if(!disconnectCb) disconnectCb = ()=>{}
     25       if(isKai) {
     26         var adp = btx.defaultAdapter
     27         adp.startLeScan(scanids).then(dHandle => {
     28           dHandle.ondevicefound = e => {
     29             adp.stopLeScan(dHandle)
     30             localgatt = e.device.gatt
     31             localgatt.connect(false).then(_ => {
     32               outputQueue = []
     33               localgatt.oncharacteristicchanged = e => {
     34                 outputQueue.push(new Uint8Array(e.characteristic.value))
     35               }
     36               localgatt.discoverServices().then(_ => {
     37                 for(let srv of localgatt.services) {
     38                   for(var chr of srv.characteristics) {
     39                     if(chr.uuid === ids('2c')) {
     40                       readerChar = chr
     41                       readerChar.writeValueWithoutResponse = readerChar.writeValue
     42                     }
     43                     else if(chr.uuid === ids('2d')) {
     44                       writerChar = chr
     45                       writerChar.writeValueWithResponse = writerChar.writeValue
     46                     }
     47                   }
     48                 }
     49                 writerChar.startNotifications().then(res).catch(rej)
     50               })
     51             }).catch(rej)
     52           }
     53         }).catch(rej)
     54       }
     55       else btx.requestDevice({filters:[{services:[scanids[0]]},{services:[scanids[1]]}]})
     56         .then(dev => {
     57           dev.addEventListener('gattserverdisconnected', disconnectCb)
     58           localgatt = dev.gatt
     59           return localgatt.connect()
     60         })
     61         .then(conn => conn.getPrimaryService(ids('0d')))
     62         .then(srv => srv.getCharacteristics())
     63         .then(arr => {
     64           for(let chr of arr) {
     65             if(chr.uuid === ids('2c'))
     66               readerChar = chr
     67             else if(chr.uuid === ids('2d'))
     68               writerChar = chr
     69           }
     70           outputQueue = []
     71           writerChar.startNotifications().then(chr => {
     72             chr.addEventListener('characteristicvaluechanged', e => {
     73               outputQueue.push(new Uint8Array(e.target.value.buffer))
     74             })
     75             res()
     76           }).catch(rej)
     77         }).catch(rej)
     78     }),
     79     execReadCmd = (...args) => readerChar.writeValueWithoutResponse(Uint8Array.from(args).buffer).then(waitForOutput),
     80     execWriteCmd = cmddata => writerChar.writeValueWithResponse(cmddata.buffer),
     81 
     82     //some logic needed to init the time sync process
     83     //these parameters need to be read from the watch and then written back
     84 
     85     plgen = k => [...Array(6)].map((a,i)=>[k,i]), //property list generator
     86     cyclePresyncProperties = _ => new Promise((res, rej) => {
     87       var slist = [...plgen(30),...plgen(31)] //full property list for primary targets, GW-B/GMW-B models
     88       if(watchModel.indexOf('OCW') > 5 || watchModel.indexOf('GWR-B1000') > 5)
     89         slist = [[30,0],[30,1]]
     90       else if(watchModel.indexOf('B2100') > 5
     91         || watchModel.indexOf('B001') > 5 
     92         || watchModel.indexOf('DW-B5600') > 5 
     93         || watchModel.indexOf('GST-B') > 5 
     94         || watchModel.indexOf('MSG-B') > 5 
     95         || watchModel.indexOf('ECB') > 5 )
     96         slist = [[30,0],[30,1],[31,0],[31,1]]
     97       var plist = [[29,0], [29,2], [29,4]].concat(slist); //populate the props list
     98       (function cycle() {
     99         var p = plist.shift()
    100         if(p) //we still have properties
    101           execReadCmd.apply(null, p)
    102             .then(execWriteCmd)
    103             .then(cycle).catch(rej)
    104         else //properties over
    105           res()
    106       })()
    107     })
    108 
    109   return {
    110     connect: dcb => new Promise((res, rej) => { //resolve to true if full setting mode, or false if just sync
    111       connectWatch(dcb)
    112         .then(_ => execReadCmd(0x23).then(v => watchModel = TD.decode(v).slice(1).replace(/\x00/g,''))) 
    113         .then(_ => execReadCmd(0x10))
    114         .then(features => {
    115           if(features[8]<2) { //we entered full setting mode, sync application info first
    116             execReadCmd(0x22).then(info => //set the correct initialization after BLE reset on the watch
    117               (info[11] || Math.min(...info.slice(1,11))^255) ? info :
    118                 execWriteCmd(Uint8Array.from([0x22, 0x34, 0x88, 0xF4, 0xE5, 0xD5, 0xAF, 0xC8, 0x29, 0xE0, 0x6D, 0x02]))
    119             ).then(info => {
    120               if(info instanceof Promise)
    121                 info.then(_=>res(true)).catch(rej)
    122               else res(true)
    123             }).catch(rej)
    124           }
    125           else res(false)
    126         }).catch(rej)
    127     }),
    128     getModel: _ => watchModel,
    129     rawRead: execReadCmd,
    130     rawWrite: execWriteCmd,
    131     setSyncDelta: val => {syncDelta = +val; if(isNaN(syncDelta)) syncDelta = 500},
    132     sync: dObj => new Promise((res, rej) => {
    133       cyclePresyncProperties().then(_ => {
    134         var d = dObj || new Date(Date.now() + syncDelta), year = d.getFullYear()
    135         execWriteCmd(Uint8Array.from([
    136           9,
    137           year&255, year>>>8,
    138           d.getMonth()+1, d.getDate(),
    139           d.getHours(), d.getMinutes(), d.getSeconds(), 
    140           d.getDay(), 0, 0, 1
    141         ])).then(res).catch(res) //not a mistake!
    142       }).catch(rej)
    143     }),
    144     disconnect: _ => {
    145       if(localgatt) {
    146         localgatt.disconnect()
    147         localgatt = null
    148       }
    149     }
    150   }
    151 })(navigator)