rcvd-py

Python 3 port of RCVD project
git clone git://git.luxferre.top/rcvd-py.git
Log | Files | Refs | README

commit 4cca67ec1f6bf89a0a7cc615c2d8d243a09a902b
Author: Luxferre <lux@ferre>
Date:   Sat, 13 Apr 2024 17:51:48 +0300

initial upload

Diffstat:
AREADME | 101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Arcvd.py | 155+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Arequirements.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