initial upload
This commit is contained in:
@@ -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.
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
bleak>=0.21.1
|
||||||
|
asyncio>=3.4.3
|
||||||
|
ntplib>=0.4.0
|
||||||
Reference in New Issue
Block a user