Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/examples/api_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ async def run_api_tests(argv: Sequence[str]) -> None: # noqa: PLR0915
condition = await api.get_watch_condition()
logger.info(f"condition: {condition}")

lifelog_steps = await api.get_lifelog_steps()
logger.info(f"lifelog steps: {lifelog_steps}")

settings_local = await api.get_basic_settings()
logger.info(f"settings: {pformat(settings_local)}")

Expand Down
13 changes: 11 additions & 2 deletions src/gshock_api/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ async def connect(self, watch_filter: WatchFilter = None) -> bool:
CasioConstants.CASIO_ALL_FEATURES_CHARACTERISTIC_UUID,
self.notification_handler,
)
await self.client.start_notify(
CasioConstants.CASIO_DATA_REQUEST_SP_CHARACTERISTIC_UUID,
lambda _c, data: message_dispatcher.MessageDispatcher.on_drsp_received(data),
)
await self.client.start_notify(
CasioConstants.CASIO_CONVOY_CHARACTERISTIC_UUID,
lambda _c, data: message_dispatcher.MessageDispatcher.on_convoy_received(data),
)

return True

Expand Down Expand Up @@ -118,7 +126,8 @@ async def write(self, handle: int, data: bytes) -> None:
return

# 0x0E is CASIO_ALL_FEATURES_CHARACTERISTIC_UUID (requires response)
response_type: bool = handle == 0x0E
# 0x11 is CASIO_DATA_REQUEST_SP (lifelog needs response)
response_type: bool = handle in (0x0E, 0x11)

cmd_data: bytes = to_casio_cmd(data)

Expand Down Expand Up @@ -157,4 +166,4 @@ def init_handles_map(self) -> HandleMap:
# Replaced Any with TypeVar T
async def send_message(self, message: T) -> None:
"""Sends a message to the watch using the message dispatcher."""
await message_dispatcher.MessageDispatcher.send_to_watch(message)
await message_dispatcher.MessageDispatcher.send_to_watch(message)
5 changes: 5 additions & 0 deletions src/gshock_api/gshock_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from gshock_api.iolib.app_notification_io import AppNotificationIO
from gshock_api.iolib.button_pressed_io import WatchButton
from gshock_api.iolib.dst_watch_state_io import DtsState
from gshock_api.iolib.lifelog_io import LifelogIO
from gshock_api.utils import (
to_compact_string,
to_hex_string,
Expand Down Expand Up @@ -191,6 +192,10 @@ async def get_watch_condition(self) -> object:
result: object = await message_dispatcher.WatchConditionIO.request(self.connection)
return result

async def get_lifelog_steps(self) -> int:
"""Get lifelog steps count."""
return await LifelogIO.request(self.connection)

async def get_time_adjustment(self) -> bool:
"""Determine if auto-tame adjustment is set or not"""
# Assuming TimeAdjustmentIO.request returns a boolean
Expand Down
93 changes: 93 additions & 0 deletions src/gshock_api/iolib/lifelog_io.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
from __future__ import annotations

import struct

from gshock_api.cancelable_result import CancelableResult
from gshock_api.iolib.connection_protocol import ConnectionProtocol
from gshock_api.utils import to_compact_string, to_hex_string


class LifelogIO:
"""Handles lifelog transfer over Data Request SP + Convoy."""

_connection: ConnectionProtocol | None = None
_result: CancelableResult[int] | None = None
_buffer: bytearray = bytearray()
_expected_len: int | None = None

# ABL-100: steps appear at this offset in the 400-byte payload.
_STEPS_OFFSET = 374

@staticmethod
async def request(connection: ConnectionProtocol, timeout: float = 60.0) -> int:
"""Start lifelog transfer and return steps."""
LifelogIO._connection = connection
LifelogIO._buffer = bytearray()
LifelogIO._expected_len = None
LifelogIO._result = CancelableResult[int](timeout=timeout)

# DRSP start: command=0x00, category=0x11, length=0 (24-bit LE)
start_cmd = bytes([0x00, 0x11, 0x00, 0x00, 0x00])
await connection.write(0x11, to_compact_string(to_hex_string(start_cmd)))

return await LifelogIO._result.get_result()

@staticmethod
def on_drsp_received(data: bytes) -> None:
"""Handle Data Request SP notification/indication."""
if len(data) < 5:
return

command = data[0]
category = data[1]
if category != 0x11:
return

length = data[2] | (data[3] << 8) | (data[4] << 16)
if command == 0x00:
LifelogIO._expected_len = length

if command == 0x04:
# End transaction from watch
LifelogIO._finalize_if_ready()

@staticmethod
def on_convoy_received(data: bytes) -> None:
"""Handle Convoy notification payload."""
if not data:
return

LifelogIO._buffer.extend(data)
if LifelogIO._expected_len is not None and len(LifelogIO._buffer) >= LifelogIO._expected_len:
# DRSP end: command=0x04, category=0x11, length=0
if LifelogIO._connection is not None:
end_cmd = bytes([0x04, 0x11, 0x00, 0x00, 0x00])
# Best effort; failures should not block parsing.
try:
# fire-and-forget; connection.write is async, but we can't await here
# callers should rely on finalization below
import asyncio

asyncio.create_task(
LifelogIO._connection.write(
0x11, to_compact_string(to_hex_string(end_cmd))
)
)
except Exception:
pass
LifelogIO._finalize_if_ready()

@staticmethod
def _finalize_if_ready() -> None:
if LifelogIO._result is None:
return

if LifelogIO._expected_len is None:
return

if len(LifelogIO._buffer) < LifelogIO._expected_len:
return

if len(LifelogIO._buffer) >= LifelogIO._STEPS_OFFSET + 4:
steps = struct.unpack_from("<I", LifelogIO._buffer, LifelogIO._STEPS_OFFSET)[0]
LifelogIO._result.set_result(steps)
13 changes: 12 additions & 1 deletion src/gshock_api/message_dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from gshock_api.iolib.watch_condition_io import WatchConditionIO
from gshock_api.iolib.watch_name_io import WatchNameIO
from gshock_api.iolib.world_cities_io import WorldCitiesIO
from gshock_api.iolib.lifelog_io import LifelogIO
from gshock_api.logger import logger

CHARACTERISTICS: Final[Mapping[str, int]] = CasioConstants.CHARACTERISTICS
Expand Down Expand Up @@ -118,6 +119,16 @@ def on_received(data: bytes) -> None:
else:
MessageDispatcher.data_received_messages[key](data)

@staticmethod
def on_drsp_received(data: bytes) -> None:
"""Handles Data Request SP notifications (lifelog)."""
LifelogIO.on_drsp_received(data)

@staticmethod
def on_convoy_received(data: bytes) -> None:
"""Handles Convoy notifications (lifelog)."""
LifelogIO.on_convoy_received(data)

# Usage example (unchanged logic)
if __name__ == "__main__":
# Simulated messages
Expand All @@ -135,4 +146,4 @@ def on_received(data: bytes) -> None:
# This line is just for illustration in the if __name__ == "__main__" block
# MessageDispatcher.send_to_watch(sample_message) # noqa: ERA001
# MessageDispatcher.on_received(sample_data) # noqa: ERA001
pass
pass