#!/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 \ or model.find('ABL-100') > 5 or model.find('GD-B500') > 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 ') 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