From 9c5723d7702a0df07c2e44f0720cc493b736e128 Mon Sep 17 00:00:00 2001 From: Luxferre Date: Tue, 4 Oct 2022 17:47:08 +0300 Subject: [PATCH] LOL --- README.md | 50 +++++++++++++++++ app.js | 47 ++++++++++++++++ img/icon112.png | Bin 0 -> 10555 bytes img/icon56.png | Bin 0 -> 3534 bytes index.html | 66 +++++++++++++++++++++++ manifest.webapp | 23 ++++++++ rcvd.js | 140 ++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 326 insertions(+) create mode 100644 README.md create mode 100644 app.js create mode 100644 img/icon112.png create mode 100644 img/icon56.png create mode 100644 index.html create mode 100644 manifest.webapp create mode 100644 rcvd.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..29a4d43 --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# RCVD: sync BLE-enabled Casio watches from KaiOS and modern Web + +RCVD is the missing KaiOS application and, first and foremost, a [JS library](rcvd.js) that allows you to interact with a set of most recent Casio watches via Bluetooth 4.0LE communication protocol. + +## Why? + +- Official Casio apps like G-Shock Connected, Oceanus Connected etc. are too bulky, proprietary and opaque, require too many permissions to work and only available for two major mobile platforms. +- The only existing FOSS alternative for the time being is no less bulky, written for Android only, has a lot of obscure and suboptimal code and also doesn't solve Android's problem with location permission required to be granted to use BLE scanning. +- As such, up until this point, there was no way to sync these watches from a Bluetooth-enabled PC in a fully cross-platform way. +- Up until this point, KaiOS also didn't have any simple and straightforward examples of working with Bluetooth GATT characteristics on real-world consumer-grade devices. +- BLE Web API interfaces totally differ across KaiOS and modern browsers, so the lowest-level abstraction layer is still necessary. + +## Supported host platforms + +- As a certified app: KaiOS 2.5.x (install directly from this repo via WebIDE); +- As a JS library: Chrome 85+, Opera 43+, Edge 79+ and other modern Chromium-based browsers (including Android ones) except the ones with support excplicitly disabled, such as Brave. + +## Supported models + +- GW-B5600 +- GMW-B5000 +- OCW-T200 + +The list is incomplete and your watch may be supported too. Please [let me know](https://matrix.to/#/@luxferre:anonymousland.org) if this is the case! + +## How to use the app + +1. Ensure your phone time is synced from the network. +2. Open the app and press SYNC (central D-pad key). +3. Enter the fast or full sync mode on your watch (depends on your model, please refer to the manual). +4. Wait until the time is set. If the app cannot connect, retry steps 2-3 until the app says your model is synced. + +## How to use the library + +The [library](rcvd.js) can be used "as is" or as a building block for more advanced tools. For its purposes, it exposes several methods: + +- `RCVD.connect() -> Promise` - connect to a watch, perform the necessary handshake operations and resolve the Promise on success; +- `RCVD.disconnect()` - disconnect from the currently connected watch's GATT server; +- `RCVD.sync(Date obj?) -> Promise` - perform time synchronization with the **optional** Date object (if it's not passed, actual current local time will be set), running all the required DST/world time setting cycles before setting the actual time; +- `RCVD.getModel() -> String` - return the watch model name obdained during the connection process; +- `RCVD.rawRead(uint8 param1, uint8 param2...) -> Promise(Uint8Array)` - accept variable number of integer parameters and execute the read command on the watch, resolving to the result in a Uint8Array value; +- `RCVD.rawWrite(Uint8Array command) -> Promise` - accept a write command shaped as byte data in Uint8Array and resolve on successful write operation. + +The last two methods are not used in the app directly in their exposed format, but allow to use the library to easily create more advanced applications to interact with Casio watches based on already known protocol details, without having to worry about low-level connectivity bits. + +## Credits + +Created by Luxferre in 2022. Both the app and the library are released into public domain. + +Made in Ukraine. diff --git a/app.js b/app.js new file mode 100644 index 0000000..d01ad6a --- /dev/null +++ b/app.js @@ -0,0 +1,47 @@ +addEventListener('DOMContentLoaded', _ => { + + var fmt2 = n => ('0'+n).slice(-2), + tElem = document.getElementById('timeline'), + dElem = document.getElementById('dateline'), + sElem = document.getElementById('status'), + upd = function() { + var now = new Date() + tElem.textContent = [now.getHours(), now.getMinutes(), now.getSeconds()].map(fmt2).join(':') + dElem.textContent = [now.getFullYear()].concat([now.getMonth()+1, now.getDate()].map(fmt2)).join('-') + setTimeout(upd, 1000) + }, runSync = function() { + sElem.textContent = 'Connecting...' + RCVD.disconnect() + RCVD.connect().then(mode => { + var model = RCVD.getModel() + console.log('Main setting mode:', mode) + sElem.textContent = 'Syncing ' + model + RCVD.sync().then(_ => { + sElem.textContent = model + ' synced' + RCVD.disconnect() + }).catch(e => {console.error(e)}) + }).catch(e => { + sElem.textContent = 'Failed, please retry!' + console.error(e) + }) + } + + addEventListener('keydown', e => { + if(e.key === 'MicrophoneToggle') + e.preventDefault() + else if(e.key === 'Enter') { + e.preventDefault() + e.stopPropagation() + runSync() + } + }) + + addEventListener('click', e => { + if(e.target.id === 'syncbtn') { + e.preventDefault() + runSync() + } + }) + + upd() +}) diff --git a/img/icon112.png b/img/icon112.png new file mode 100644 index 0000000000000000000000000000000000000000..4cd8467763b617be52fdad77f49ca0d62439bb02 GIT binary patch literal 10555 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 JPDHLkV1ip@=T86t literal 0 HcmV?d00001 diff --git a/img/icon56.png b/img/icon56.png new file mode 100644 index 0000000000000000000000000000000000000000..f0f536bb15b1a0c2d572627948cce33ae0d0e449 GIT binary patch literal 3534 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 literal 0 HcmV?d00001 diff --git a/index.html b/index.html new file mode 100644 index 0000000..903b428 --- /dev/null +++ b/index.html @@ -0,0 +1,66 @@ + + + + + + RCVD + + + +
+

RCVD

+

by Luxferre

+
+
+
+
+
Press both sync buttons
+
+
+ SYNC +
+ + + + diff --git a/manifest.webapp b/manifest.webapp new file mode 100644 index 0000000..7acec9a --- /dev/null +++ b/manifest.webapp @@ -0,0 +1,23 @@ +{ + "version": "0.0.1", + "name": "RCVD", + "description": "Casio BLE-enabled watches time synchronization app", + "type": "certified", + "origin": "app://rcvd.luxferre.top", + "launch_path": "/index.html", + "orientation": ["portrait"], + "installs_allowed_from": [ + "*" + ], + "icons": { + "56": "/img/icon56.png", + "112": "/img/icon112.png" + }, + "developer": { + "name": "Luxferre" + }, + "permissions": { + "bluetooth": "Perform BLE operations" + } +} + diff --git a/rcvd.js b/rcvd.js new file mode 100644 index 0000000..c84f286 --- /dev/null +++ b/rcvd.js @@ -0,0 +1,140 @@ +// RCVD.js: a small library for syncing time on BLE-enabled Casio watches +// Tested on: GW-B5600BC, OCW-T200S +// 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) => { + var plist = [[29,0], [29,2], [29,4]].concat(watchModel.indexOf('OCW')>5 ? [[30,0],[30,1]] : [...plgen(30),...plgen(31)]); //populate the props list + (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)