initial upload

This commit is contained in:
Luxferre
2024-04-13 17:51:48 +03:00
commit 4cca67ec1f
3 changed files with 259 additions and 0 deletions
+101
View File
@@ -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.
+155
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
bleak>=0.21.1
asyncio>=3.4.3
ntplib>=0.4.0