commit 4cca67ec1f6bf89a0a7cc615c2d8d243a09a902b
Author: Luxferre <lux@ferre>
Date: Sat, 13 Apr 2024 17:51:48 +0300
initial upload
Diffstat:
A | README | | | 101 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | rcvd.py | | | 155 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | requirements.txt | | | 3 | +++ |
3 files changed, 259 insertions(+), 0 deletions(-)
diff --git a/README b/README
@@ -0,0 +1,101 @@
+RCVD.py: sync BLE-enabled Casio watches from Python 3
+-----------------------------------------------------
+The RCVD project is now fully ported to Python 3. This is an application that
+allows you to synchronize time and date on your Bluetooth LE enabled Casio
+wristwatches (see supported models in the FAQ section). The original concept
+was created for KaiOS 2.5.x and Web Bluetooth API enabled browsers in 2022.
+This version is a standalone CLI application but optionally can be used as
+a library.
+
+== Dependencies ==
+
+RCVD.py has been tested on Python 3.10 and depends on these libraries:
+bleak (0.21.1+), asyncio (3.4.3+) and ntplib (0.4.0+). Just install all of
+them by running pip install -r requirements.txt from the project directory.
+
+Of course, you need to run RCVD on a PC with a Bluetooth 4.0+ adapter or on
+another Bluetooth-enabled device that supports Bluetooth LE.
+
+== Usage ==
+
+RCVD can be run like this:
+
+python rcvd.py [-h] [-t timeout] [-d delta] [-s ntp_server] [-n ntp_version]
+
+All parameters are optional here:
+
+* -h: display help screen
+* -t: BLE scanning timeout in seconds (default 4)
+* -d: manual delta correction in milliseconds (default 500, see below)
+* -s: specify NTP server to fetch time from (if no server is specified, then
+ local system time is used)
+* -n: specify NTP protocol version to use, default is 4, good for most cases
+
+After running the command, you must press the corresponding time sync button
+on your watch (usually the lower right) to put it into the BLE sync mode. If
+possible, press this button at the same time as running the command, but a
+short delay (within the timeout specified with -t parameter) is allowed. Once
+the watch is connected and the handshake is performed, RCVD will fetch the
+current time from the specified source (local system time by default, or the
+NTP server of your choice) and set it on the watch. Look for the OK indicator
+on the watch itself to confirm that the setting process has been successful.
+Refer to the manual for your model on how to do that.
+
+Depending on your specific watch and host Bluetooth hardware, you may need to
+set a different delta correction value to account for transmission losses.
+You can do this by altering the -d parameter when running the command. This
+parameter defines how many milliseconds are added to the time value to be
+immediately set on the watch. It is 500 ms by default, but tends to be more on
+the digital Casio models, so you need to find out yourself which delta gives
+the most accurate time setting as a result.
+
+== FAQ ==
+
+- How is this better than the official (and unofficial) Casio syncing apps?
+
+As of now, RCVD focuses on doing one thing well and works on any CLI-enabled
+platform, as opposed to the official Casio apps only working on mobile OSes.
+It doesn't aim to replace them. Just as its JS-based predecessor, RCVD.py is
+a lightweight alternative for those who just want to sync time on the watches
+and nothing else.
+
+- Which Casio models are supported as of now?
+
+The following models were directly tested with both (JS and Python) versions:
+GW-B5600, GMW-B5000, OCW-T200, GM-B2100, DW-B5600, GA-B001.
+
+The following models are theoretically supported too but not directly tested:
+MRG-B5000, GWR-B1000, GA-B2100, GST-B400, GST-B500, ECB-S100, MSG-B100,
+ECB-2000, ECB-2200, G-B001.
+
+If RCVD works with one of the above models (or with some other ones), please
+let me know at https://matrix.to/#/@luxferre:hackliberty.org and I'll mark it
+as tested.
+
+- Will both JS and Python versions be developed in parallel?
+
+Most likely, no. If there is an issue that really needs to be fixed in the
+KaiOS/JS version of RCVD, the fix will be backported there, but all new
+feature development will only be focused upon in the Python version, i.e. this
+one. This version, by the way, already is ahead of the JS version because it
+already integrates NTP support.
+
+For any new Casio models though, if the basic time sync support is trivial to
+add into the Python version, it will most likely be added to the JS version as
+well.
+
+- Are there going to be advanced features for the models supporting them, like
+ alarm setting, reminders, DST rules, timezone DB, waypoints etc?
+
+Probably. Not in the short term though. And it will most likely be a separate
+script using rcvd.py as a library.
+
+- Are there going to be any features utilizing "phone finder" and autosync?
+
+A research is planned in both of these directions, but, again, its results
+will be implemented in separate applications, not in this one.
+
+== Credits ==
+
+Original research, JS library and KaiOS application by Luxferre, 2022.
+Ported to Python in 2024. Released into public domain with no warranties.
diff --git a/rcvd.py b/rcvd.py
@@ -0,0 +1,155 @@
+#!/usr/bin/env python3
+# rcvd.py: a port of RCVD.js library to Python 3
+# RCVD is a small library for syncing time on BLE-enabled Casio watches
+# This script also offers a standalone way of running the sync
+# Tested on: GW-B5600BC, GMW-B5000D, OCW-T200S, GM-B2100BD, GA-B001G, DW-B5600G
+# Created by Luxferre in 2022, ported to Python in 2024
+# Released into public domain
+
+import asyncio, time, ntplib
+from bleak import BleakScanner, BleakClient
+from bleak.exc import BleakDBusError
+
+# time fetching part
+# must return a dictionary with the following fields:
+# year, month, date, hour, minute, second, day (0-6)
+def fetchtime(params = {}):
+ res = {}
+ delta = 0.0 # in milliseconds
+ if 'delta' in params:
+ delta = float(params['delta']) / 1000
+ if 'server' in params and params['server'] is not None: # NTP server set
+ ntpver = 3
+ if 'version' in params:
+ ntpver = params['version']
+ c = ntplib.NTPClient()
+ resp = c.request(params['server'], version=ntpver)
+ unixtm = resp.tx_time
+ else: # use current system time by default
+ unixtm = time.time()
+ tm = time.localtime(unixtm + delta) # account for delta
+ res['year'] = tm.tm_year
+ res['month'] = tm.tm_mon
+ res['date'] = tm.tm_mday
+ res['hour'] = tm.tm_hour
+ res['minute'] = tm.tm_min
+ res['second'] = tm.tm_sec
+ res['day'] = tm.tm_wday
+ return res
+
+# device interaction part
+
+# generate full Casio-specific UUID from the short one
+def fullid(s):
+ return '26eb00' + s + '-b012-49a8-b1f8-394fb2032b0f'
+
+# populate constants
+CASIO_SYNC_SRV = fullid('0d') # for service discovery
+READER_CHAR = fullid('2c') # for parameter reading
+WRITER_CHAR = fullid('2d') # for parameter writing
+
+# the queue for GATT notification data
+notiqueue = asyncio.Queue()
+
+# GATT notification handler
+def readfiller(sender, data):
+ global notiqueue
+ notiqueue.put_nowait(data)
+
+# execute a read command:
+# first write the value without response to READER_CHAR
+# then read the result from the WRITER_CHAR (!) notifier
+async def exec_read(client, *cmdargs):
+ global notiqueue
+ await client.write_gatt_char(READER_CHAR, bytes(cmdargs), response=False)
+ res = await notiqueue.get()
+ return res
+
+# execute a write command:
+# write to WRITER_CHAR with an immediate response
+async def exec_write(client, cmd: bytearray):
+ res = await client.write_gatt_char(WRITER_CHAR, cmd, response=True)
+ return res
+
+# handshake sequence, returns the watch model
+async def handshake(client):
+ rawmodel = await exec_read(client, 0x23) # read the raw model
+ model = rawmodel[1:].decode('utf-8').rstrip('\x00') # human-readable model
+ features = await exec_read(client, 0x10) # read feature setting
+ if features[8] < 2: # we entered full setting mode, sync app info first
+ info = await exec_read(client, 0x22) # detect if BLE has been reset
+ # it has been reset if it consists of 11 FF bytes and then 00
+ if info[:12] == b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00':
+ restorecmd = b'\x22\x34\x88\xf4\xe5\xd5\xaf\xc8\x29\xe0\x6d\x02'
+ await exec_write(client, restorecmd)
+ return model
+
+# presync property cycling required to init the time sync process
+# these parameters need to be read from the watch and then written back
+# the set of the parameters is model-specific
+# this function returns the final precycle list based on the mode
+def precycle_prop_list(model: str):
+ # full property list for primary targets, GW-B/GMW-B models
+ props = [[30,0], [30,1], [30,2], [30,3], [30,4], [30,5],
+ [31,0], [31,1], [31,2], [31,3], [31,4], [31,5]]
+ if model.find('OCW') > 5 or model.find('GWR-B1000') > 5:
+ props = [[30,0], [30,1]]
+ elif (model.find('B2100') > 5 or model.find('B001') > 5 \
+ or model.find('DW-B5600') > 5 or model.find('GST-B') > 5 \
+ or model.find('MSG-B') > 5 or model.find('ECB') > 5):
+ props = [[30,0], [30,1], [31,0], [31,1]]
+ return [[29,0], [29,2], [29,4]] + props # concatenate the prop lists
+
+# sync the watch from here
+async def dosync(client, timeparams):
+ global notiqueue
+ print('Starting handshake...')
+ await client.start_notify(WRITER_CHAR, readfiller) # set up notifications
+ model = await handshake(client) # run the handshake and detect watch model
+ print('Connected to %s, preparing to sync...' % model)
+ precycle_props = precycle_prop_list(model)
+ for prop in precycle_props: # just read and write again
+ data = await exec_read(client, prop[0], prop[1])
+ await exec_write(client, data)
+ print('Fetching the time...')
+ timeparts = fetchtime(timeparams)
+ print('Syncing the time...')
+ timelist = [9, # sync time command
+ timeparts['year'] & 255, timeparts['year'] >> 8,
+ timeparts['month'], timeparts['date'],
+ timeparts['hour'], timeparts['minute'], timeparts['second'],
+ timeparts['day'], 0, 0, 1]
+ try: # some models disconnect prematurely as soon as sync OK
+ await exec_write(client, bytes(timelist))
+ except (EOFError, BleakDBusError):
+ pass
+ print('Time synced')
+
+
+# the process starts from here
+# accepts fetchtime parameters and BLE scan timeout
+async def syncstart(timeparams):
+ # list of IDs to scan
+ devices = await BleakScanner.discover(timeout=timeparams['timeout'],
+ return_adv=True,
+ service_uuids=['00001804-0000-1000-8000-00805f9b34fb', CASIO_SYNC_SRV])
+ for d in devices: # enumerate found watches
+ async with BleakClient(str(d)) as client:
+ await dosync(client, timeparams) # init the client by BLE MAC
+
+if __name__ == '__main__': # application entry point
+ from argparse import ArgumentParser
+ parser = ArgumentParser(description='RCVD: an opensource time synchronizer for BLE-enabled Casio watches', epilog='(c) Luxferre 2024 --- No rights reserved <https://unlicense.org>')
+ parser.add_argument('-t', '--timeout', type=int, default=4, help='BLE scanning timeout (in seconds, default 4)')
+ parser.add_argument('-d', '--delta', type=int, default=500, help='Manual delta correction (in ms, must be determined individually, 500 ms by default)')
+ parser.add_argument('-s', '--ntp-server', type=str, default=None, help='NTP server to sync from (if not specified then will sync from the local system time)')
+ parser.add_argument('-n', '--ntp-version', type=int, default=4, help='NTP protocol version to use (default 4)')
+ args = parser.parse_args()
+ params = { # populate parameters from the command line
+ 'server': args.ntp_server, 'version': args.ntp_version,
+ 'delta': args.delta, 'timeout': args.timeout
+ }
+ if params['version'] < 1 or params['version'] > 4:
+ params['version'] = 4
+ print('RCVD by Luxferre\nPress sync button on the watch ASAP to connect')
+ asyncio.run(syncstart(params)) # start the process
diff --git a/requirements.txt b/requirements.txt
@@ -0,0 +1,3 @@
+bleak>=0.21.1
+asyncio>=3.4.3
+ntplib>=0.4.0