tikal.low_level package
Subpackages
Submodules
tikal.low_level.brand_handler module
Part of the Low-Level API: the abstract contract every BLE toy brand implements.
A brand handler encapsulates everything brand-specific about BLE discovery and connection:
- Recognizing the brand from a BLE advertisement
- Creating the appropriate ToyData
- Establishing a connection and returning a ready-to-use Toy instance
To add a new brand, create a subpackage under tikal/low_level/brands/ with a BLEBrandHandler subclass and
register it in tikal/low_level/brands/__init__.py. The BLEConnectionBuilder instantiates every registered
handler via the standard constructor defined here, so a new handler only needs to implement the four abstract methods.
- class tikal.low_level.brand_handler.BLEBrandHandler(on_disconnect: ~typing.Callable[[str], ~typing.Any], on_power_off: ~typing.Callable[[str], ~typing.Any], logger_name: str = 'tikal', client_class: ~typing.Type[~bleak.BleakClient] = <class 'bleak.BleakClient'>)[source]
Bases:
ABCAbstract interface for handling a specific brand of BLE toy.
Each concrete implementation provides the logic for: - Recognizing the brand from BLE advertisements - Creating the appropriate
ToyData- Establishing a connection and returning a ready-to-useToyinstanceSubclasses are instantiated by the
BLEConnectionBuilderthrough the constructor below. A subclass only needs to implement the four abstract methods.- Parameters:
on_disconnect – Callback called when a toy disconnects unexpectedly. Receives the toy’s toy_id.
on_power_off – Callback called when the user powers off a toy via its physical button. Receives the toy id (address).
logger_name – Name for the logger used by this handler. Defaults to ‘tikal’.
client_class – BLE client class to use. Defaults to BleakClient. Can be overridden for testing.
- abstractmethod async create_toy(toy_data: ToyData, device: BLEDevice) Toy[source]
Connect to the toy described by
toy_datausing the providedBLEDevice.This method handles brand-specific connection steps (UUID resolution, notification setup, etc.) and returns a connected
Toyinstance.- Raises:
ValidationError – The
toy_datacontains an invalid model name.ConnectionError – The BLE connection, UUID resolution, or notification setup failed.
- abstractmethod static create_toy_data(device: BLEDevice) ToyData[source]
Create a brand-specific
ToyDataobject from a BLE device.
- abstractmethod static handles_device(device: BLEDevice) bool[source]
Return
Trueif the BLE device belongs to this brand.
tikal.low_level.connection_builder module
Part of the Low-Level API: Provides connection management for toy devices.
This module provides:
- ConnectionBuilder: The transport-agnostic entry point. Composes every per-transport connection builder
(
BLEConnectionBuilderand the fictionalMockConnectionBuilder) and presents a single, unified discovery/connection API. This is the class the higher layers (and new code) should use.
BLEConnectionBuilder: Entry point for all BLE toys. Manages BLE scanning (one‑shot and continuous) and creation of Toys.
Brand-specific logic lives behind BLEBrandHandler (brand_handler.py); concrete handlers and their
registration live under tikal/low_level/brands/. The builder gets its handlers from that registry via build_handlers.
Adding a brand that speaks a new transport (e.g., USB) only requires writing the transport’s own connection builder and
adding it to ConnectionBuilder’s internal list; every consumer keeps using ConnectionBuilder unchanged.
Example:
def handle_disconnect(transport: Transport):
print(f"Toy at {transport.toy_id} disconnected unexpectedly")
def handle_power_off(address: str):
print(f"Toy at {address} was powered off")
builder = ConnectionBuilder(
on_disconnect=handle_disconnect,
on_power_off=handle_power_off,
logger_name="tikal"
)
- class tikal.low_level.connection_builder.BLEConnectionBuilder(on_disconnect: ~typing.Callable[[str], ~typing.Any], on_power_off: ~typing.Callable[[str], ~typing.Any], logger_name: str, scanner_class: ~typing.Type[~bleak.BleakScanner] = <class 'bleak.BleakScanner'>, client_class: ~typing.Type[~bleak.BleakClient] = <class 'bleak.BleakClient'>)[source]
Bases:
object- async create_toy(to_connect: ToyData) Toy | BaseException[source]
Create a Toy instance from discovery data.
- Parameters:
to_connect – ToyData object with a valid model_name. For Lovense valid model names are in LOVENSE_TOY_NAMES.keys() of module toy_data Instances of ToyData are created with a prior call to
discover_toys()and the model_name must be set by you.- Returns:
KeyError: toy address was not found in the cache (i.e.,discover_toys()was not called first)StaleDeviceError: Subclass of ConnectionError: Device was discovered, but has since become stale. Retrieve a new snapshot i.e., viadiscover_toys()ConnectionError: BLE connection or notification setup failed, e.g., the toy may have become unavailableInvalidModelError: If model_name is not valid for this toy brand.BadModelError: If the model_name is valid, but commands still fail. See BadModelError for detailsRuntimeError: Developer error. I did not specify a handler for this subclass of ToyData. Should never happen.
- Return type:
connected Toy instance on success, or a BaseException on failure. Possible exceptions
- Example::
toys = await builder.discover_toys(5.0) # Discover toys # Set model names (e.g., from user input) toys[0].model_name = “Nora” result = await builder.create_toys(toys[0]) # Connect print(f”Connected: {isinstance(result, Toy)}”)
- async create_toys(to_connect: list[ToyData]) list[Toy | BaseException][source]
Create Toy instances from discovery data.
- Parameters:
to_connect – List of ToyData objects with valid model_names. For Lovense valid model names are in LOVENSE_TOY_NAMES.keys() of module toy_data Instances of ToyData are created by
discover_toys()orretrieve_continuous()and model_names must be set by you.- Returns:
KeyError: toy address was not found in the cache (i.e.,discover_toys()was not called first)StaleDeviceError: Subclass of ConnectionError: Device was discovered, but has since become stale. Retrieve a new snapshot i.e., viadiscover_toys()ConnectionError: BLE connection or notification setup failed, e.g., the toy may have become unavailableInvalidModelError: If model_name is not valid for this toy brand.BadModelError: If the model_name is valid, but commands still fail. See BadModelError for detailsRuntimeError: Developer error. I did not specify a handler for this subclass of ToyData. Should never happen.
The order of results matches the order of the input list.
- Return type:
List where each element is either a connected Toy instance or a BaseException for failed connections. Possible exceptions per element
- Example::
toys = await builder.discover_toys(5.0) # Discover toys # Set model names (e.g., from user input) toys[0].model_name = “Nora” toys[1].model_name = “Lush” results = await builder.create_toys(toys) # Connect # Process results connected_toys = [r for r in results if isinstance(r, Toy)] failed = [r for r in results if isinstance(r, BaseException)]
- async discover_toys(timeout: float = 10.0) list[ToyData][source]
Scan for all available BLE toys.
This method caches discovered BLE devices internally. You should call this method before calling
create_toys().- Parameters:
timeout – Scan time in seconds. Longer timeouts increase the chance of finding all nearby devices. Default is 10 seconds.
- Raises:
Exception – Any exception from BleakScanner.discover(), such as permission errors or Bluetooth adapter issues
RuntimeError – If a continuous scan is in progress. See meth: start_continuous_scan and meth: stop_continuous_scan.
- Returns:
List of
ToyDataobjects with brand‑appropriate types. This is a snapshot, not a continuous stream of updates.
Examples:
toys = await builder.discover_toys(timeout=5.0) print(f"Found {len(toys)} Lovense devices") for toy in toys: print(f"{toy.name} at {toy.toy_id}")
- handles_toy(toy_data: ToyData) bool[source]
Return
Trueif one of this builder’s brand handlers recognizestoy_data.Used by
ConnectionBuilderto route aToyDatato the builder that can connect it.
- async retrieve_continuous() list[ToyData][source]
Retrieve the current snapshot of discovered Toys. Needs a continuous scan to be running (else always return the empty list) Use
start_continuous()to start the continuous scan.If an error occurred during continuous scanning and is still relevant (=no call to
stop_continuous()was made afterward), the first call to this method raises that exception. Subsequent calls return an empty list. Note that such exceptions stop the continuous scan.- Raises:
Any exception that occurred during continuous scanning as described above. –
- Returns:
List of
ToyDataobjects with brand‑appropriate types. The list is empty if continuous discovery is not running. This is a snapshot, not a continuous stream of updates.
- async start_continuous(on_update: Callable[[list[ToyData] | Exception], Any] | None = None) None[source]
Start continuous background discovery of Toys.
Continuously scan for new toys. New toys are added to the internal cache and stale toys removed. Use meth:
retrieve_continuousto get the current snapshot. Use meth: stop_continuous to stop the background scan. The method is idempotent. Calls to this method will be ignored if a continuous scan is already running.- Parameters:
on_update – Optional callback. Called with a snapshot of all discovered Toys whenever the internal cache changes. Called with an exception if the continuous scan encounters an error. Can be seen as a push version of
retrieve_continuous(). The first call to on_update reflects the clearing of the internal cache, meaning you’ll always get an empty list first. This serves to ensure any cache you might have made is cleaned too.- Raises:
Exception – Any exception raised by the BLE scanner (e.g., permission errors).
- async stop_continuous() None[source]
Stop the continuous background discovery.
Use :meth: start_continuous to start continuous background discover. After stopping,
retrieve_continuous()will return an empty tuple. The method is idempotent. Does nothing if continuous discovery is not running.
- class tikal.low_level.connection_builder.ConnectionBuilder(on_disconnect: ~typing.Callable[[str], ~typing.Any], on_power_off: ~typing.Callable[[str], ~typing.Any], logger_name: str, bluetooth_scanner: ~typing.Type[~bleak.BleakScanner] = <class 'bleak.BleakScanner'>, bluetooth_client: ~typing.Type[~bleak.BleakClient] = <class 'bleak.BleakClient'>, mock_toys: bool = False)[source]
Bases:
objectTransport-agnostic entry point of the Low-Level API.
A
ConnectionBuildercomposes one connection builder per supported transport (BLEConnectionBuilderfor real BLE toys andMockConnectionBuilderfor the fictional MockEstimToys brand) and exposes the exact same discovery/connection API as a single builder. Discovery results from every transport are merged into one list, and connection requests are routed to the builder that owns the relevantToyData.Adding a brand on a new transport only means: write that transport’s connection builder and append it to
self._buildersbelow. Every consumer ofConnectionBuilderkeeps working unchanged.- Parameters:
on_disconnect – Callback invoked when a toy disconnects unexpectedly. Receives the toy’s toy_id. Not called for intentional disconnects.
on_power_off – Callback invoked when the user powers off a toy via the physical power button. Receives the toy id.
logger_name – Name of the logger to use. Use empty string for root logger.
bluetooth_scanner – BLE scanner class to use. Defaults to BleakScanner. Can be overridden for testing.
bluetooth_client – BLE client class to use. Defaults to BleakClient. Can be overridden for testing.
mock_toys – If True, use the MockEstimToys brand. Not part of the public API. This parameter may be removed without notice.
- async create_toy(to_connect: ToyData) Toy | BaseException[source]
Create a single Toy instance from discovery data, routing it to the transport that owns it.
See
BLEConnectionBuilder.create_toy()for arguments and the result/exception types.- Returns:
A connected
Toyon success, or aBaseExceptionon failure. ReturnsRuntimeErrorif no transport recognizes the givenToyData(developer error; should never happen).
- async create_toys(to_connect: list[ToyData]) list[Toy | BaseException][source]
Create Toy instances from discovery data, routing each to the transport that owns it.
See
BLEConnectionBuilder.create_toys()for arguments and the per-element result/exception types.- Returns:
List where each element is a connected
Toyor aBaseException. Order matches the input list.
- async discover_toys(timeout: float = 10.0) list[ToyData][source]
Scan every transport for available toys and return the combined result.
Delegates to each underlying connection builder and concatenates their results. See
BLEConnectionBuilder.discover_toys()for the per-transport behavior, arguments, and raised exceptions.- Parameters:
timeout – Scan time in seconds, passed to every transport.
- Returns:
Combined list of
ToyDataacross all transports. A snapshot, not a continuous stream.
- async retrieve_continuous() list[ToyData][source]
Retrieve the current merged snapshot of discovered Toys from every transport.
Needs a continuous scan to be running (see
start_continuous()); otherwise returns the empty list. If a transport has a pending continuous-scan exception, the first call re-raises it (seeBLEConnectionBuilder.retrieve_continuous()).- Returns:
Combined list of
ToyDataacross all transports.
- async start_continuous(on_update: Callable[[list[ToyData] | Exception], Any] | None = None) None[source]
Start continuous background discovery across every transport.
Each transport scans independently; their snapshots are merged so
on_updatealways receives a single list spanning all transports. SeeBLEConnectionBuilder.start_continuous()for the general contract.- Parameters:
on_update – Optional callback. Called with a merged snapshot of all discovered Toys whenever any transport’s view changes, or with an exception if a transport’s scan errors.
- async stop_continuous() None[source]
Stop continuous background discovery on every transport.
Idempotent. Does nothing for transports that are not currently scanning.
- exception tikal.low_level.connection_builder.StaleDeviceError[source]
Bases:
ConnectionErrorRaised when attempting to connect to a device that was at one point discovered but is no longer available.
tikal.low_level.toy module
Part of the Low Level API: the abstract Toy interface.
This module defines the brand-agnostic base class for communicating with toy devices:
- Toy: Abstract base class defining the toy communication interface
- UnexpectedToyResponse: raised when a toy returns an unexpected reply
Concrete, brand-specific implementations live in tikal/low_level/brands/ (e.g. tikal.low_level.brands.lovense.LovenseToy).
You are not meant to instantiate these classes directly.
ConnectionBuilder establishes connections to toys and returns instances of Toy
Example:
# After connecting via BLEConnectionBuilder
toy = connected_toys[0]
# Control the toy
await toy.intensity1(15) # Set primary capability to level 15
await toy.intensity2(10) # Set secondary capability to level 10
# Check battery
battery = await toy.get_battery_level()
print(f"Battery: {battery}%")
# Disconnect when done
await toy.disconnect()
- class tikal.low_level.toy.Toy(transport: Transport, model_name: str, logger_name: str)[source]
Bases:
ABCAbstract base class representing a low-level toy.
Responsible for handling the communication with physical toys. Provides functions for sending commands to the toy and handles its responses. Each toy brand implements this interface with brand-specific protocol details. You are not meant to instantiate these classes directly.
ConnectionBuilderestablishes connections to toys and returns instances ofToy- Parameters:
transport – Connected Transport instance.
model_name – Model name of the toy (e.g., “Gush”, “Nora”).
logger_name – Name of the logger to use. Use empty string for root logger.
- abstract property brand: str
Returns a human-readable identifier of the toy brand e.g., ‘Lovense’.
- abstractmethod async change_rotation_direction() bool[source]
Change rotation direction.
For toys with rotation capability (e.g., Nora, Ridge), this toggles the rotation direction. Returns True and does nothing if the toy does not support rotation.
- Returns:
True if the toy acknowledged the command or if rotation is not supported, False if the command failed.
Example:
# Reverse rotation direction await toy.rotate_change_direction()
- abstract property change_rotation_direction_available: bool
Check if the toy supports changing the rotation direction.
- Returns:
True if the rotation direction can be changed, False otherwise.
- Return type:
bool
Example:
if toy.change_rotation_direction_available: await toy.change_rotation_direction()
- property current_intensities: tuple[int, int]
Get the current intensity values for the toy’s capabilities.
- Returns:
A tuple of (primary_intensity, secondary_intensity). The secondary intensity is always 0 if the toy has only one capability.
- Return type:
tuple[int, int]
- Example::
intensity1, intensity2 = toy.current_intensities print(f”Primary intensity: {intensity1}, Secondary intensity: {intensity2}”)”)
- abstractmethod async direct_command(command: str, timeout: float = 3.0) str | None[source]
Send any command directly to the toy.
This method allows sending commands that are not implemented by the library.
- Parameters:
command – Command string in UTF-8 format
timeout – Response timeout in seconds. Defaults to 3.0.
- Returns:
Response string from the toy, or None if timeout or error occurred.
- abstractmethod async disconnect() None[source]
Disconnect from the device.
Stops all toy actions, disables notifications, and closes the BLE connection. This method should always be called before the toy object is destroyed to ensure proper cleanup. The method does not raise any exceptions.
- abstractmethod async get_battery_level() int | None[source]
Retrieve the battery level of the connected device.
- Returns:
Battery level as a percentage (0-100), or None if an error occurred, the command timed out, or the toy has no battery.
Example:
battery = await toy.get_battery_level() if battery is not None: print(f"Battery: {battery}%") else: print("Failed to read battery level")
- abstractmethod async intensity1(level: int) bool[source]
Set the primary capability of the toy to a specified level.
The primary capability varies by toy model (e.g., vibration for Gush, thrusting for Solace).
- Parameters:
level – Intensity level. The valid range is 0 - self.max_intensity. Values outside this range are clamped.
- Returns:
True if the toy acknowledged the command, False otherwise.
Example:
# Set primary capability to medium intensity success = await toy.intensity1(10) if not success: print("Command failed or timed out")
- abstractmethod async intensity2(level: int) bool[source]
Set the secondary capability of the toy to a specified level.
The secondary capability varies by toy model (e.g., Depth for Solace, air pump for Max). Not all toys have a secondary capability. Returns true and does nothing if the toy has no secondary capability.
- Parameters:
level – Intensity level. The valid range is 0 - self.max_intensity. Values outside this range are clamped.
- Returns:
True if the toy acknowledged the command or the toy does not have a secondary capability, False otherwise.
Example:
# Set secondary capability to low intensity await toy.intensity2(5)
- abstract property intensity_names: tuple[str, str | None]
Get the display names for the toy’s capabilities.
- Returns:
A tuple of (primary_name, secondary_name). The secondary name is None if the toy has only one capability.
- Return type:
tuple[str, str | None]
Example:
names = toy.intensity_names print(f"Primary: {names[0]}") # example: Vibration if names[1]: print(f"Secondary: {names[1]}") # example: Rotation
- property is_connected: bool
Check if the toy is currently connected.
- Returns:
True if connected, False otherwise.
- abstract property max_intensity: int
Get the maximum intensity value for this toy.
- Returns:
Maximum intensity value (e.g., 20 for Lovense toys).
- Return type:
int
Example:
max_val = toy.intensity_max_value await toy.intensity1(max_val) # Set to maximum
- property model_name: str
Returns the model name of the toy (e.g., “Nora”, “Lush”).
- property name: str
Returns a human-readable identifier of the toy e.g., Bluetooth name.
- abstract property recommended_min_interval: int
Get the recommended minimum interval between intensity changes, in milliseconds.
This is a per-model suggestion, primarily useful for pattern playback (the smallest sensible segment length). This is a recommendation, and you are free to ignore it.
- Returns:
Recommended minimum interval between intensity changes in milliseconds.
- Return type:
int
- abstractmethod async reconnect() bool[source]
Attempts to reconnect to the toy to after an unintentional disconnect.
Does nothing if already connected. Use only after an unintended disconnect (ConnectionError). If you disconnected via disconnect, use the ConnectionBuilder instead.
- Returns:
True if the reconnection was successful, False otherwise.
- abstractmethod async set_model_name(model_name: str) None[source]
Set the model name of the toy.
This method validates and updates the toy’s model name. The model name determines which commands are available and how they’re interpreted.
- Parameters:
model_name – New model name. Must be a valid model for this toy brand.
- Raises:
InvalidModelError – If model_name is not valid for this toy brand.
BadModelError – If the model_name is valid, but commands still fail. See BadModelError for details.
- abstractmethod async stop() bool[source]
Stop all toy actions by setting all intensities to zero.
- Returns:
True if successful, False if either intensity command failed.
Example:
await toy.stop()
- abstractmethod async strict_change_rotation_direction() bool[source]
Similar to :meth: rotate_change_direction, but this method raises an exception if the command fails. :raises UnexpectedToyResponse: The toys’ response was unexpected, e.g. “ERROR” instead of “OK”. :raises ConnectionError: Command could not be sent, or the toy did not respond within an appropriate timeout.
- Returns:
True if the toy supports rotation, False otherwise.
- abstractmethod async strict_direct_command(command: str, timeout: float = 3.0) str[source]
Similar to :meth: direct_command, but this method raises an exception if the command fails.
- Raises:
ConnectionError – Command could not be sent, or the toy did not respond within the provided timeout.
- Returns:
response string from the toy
- abstractmethod async strict_disconnect() None[source]
Similar to :meth: disconnect, but this methode does raise an exception if the disconnect fails. The exception is only for logging. The toy is still disconnected in the error case.
- Raises:
ConnectionError – Command could not be sent, or the toy did not respond within an appropriate timeout.
UnexpectedToyResponse – The toys’ response was unexpected, e.g. “ERROR” instead of “OK”.
- abstractmethod async strict_get_battery_level() int | None[source]
Similar to :meth: get_battery_level, but this method raises an exception if the command fails.
- Raises:
UnexpectedToyResponse – The toys’ response was unexpected.
ConnectionError – Command could not be sent, or the toy did not respond within an appropriate timeout.
- Returns:
Battery level as a percentage (0-100), or None if the toy has no battery.
- abstractmethod async strict_intensity1(level: int) bool[source]
Similar to :meth: intensity1, but this method raises an exception if the intensity command fails.
- Raises:
ConnectionError – Command could not be sent, or the toy did not respond within an appropriate timeout.
UnexpectedToyResponse – The toys’ response was unexpected, e.g. “ERROR” instead of “OK”.
- Returns:
Always true
- abstractmethod async strict_intensity2(level: int) bool[source]
Similar to :meth: intensity2, but this method raises an exception if the intensity command fails.
- Raises:
ConnectionError – Command could not be sent, or the toy did not respond within an appropriate timeout.
UnexpectedToyResponse – The toys’ response was unexpected, e.g. “ERROR” instead of “OK”.
- Returns:
True if the toy supports a secondary capability, False otherwise.
- abstractmethod async strict_reconnect() bool[source]
Similar to :meth: reconnect, but this methode does raise an exception if the reconnection fails.
- Raises:
ConnectionError – The reconnection failed.
RuntimeError – The reconnection was attempted after intentionally disconnecting the toy.
- Returns:
Always True.
- abstractmethod async strict_stop() bool[source]
Similar to :meth: stop, but this method raises an exception if the stop command fails.
- Raises:
UnexpectedToyResponse – The toys’ response was unexpected, e.g. “ERROR” instead of “OK”.
ConnectionError – Command could not be sent, or the toy did not respond within an appropriate timeout.
- Returns:
always True
- property toy_id: str
Returns a unique identifier of the toy e.g., Bluetooth address.
- exception tikal.low_level.toy.UnexpectedToyResponse[source]
Bases:
ConnectionErrorRaised when the toy responds with an unexpected message.
tikal.low_level.toy_data module
Part of both the Low-Level and High-Level API: brand-agnostic data structures for toy device management.
This module defines the shared data classes used throughout the toy control system:
- Exception classes for validation errors (raised if model_name is invalid)
- ToyData returned by the connection builder after discovery
- ToyCommands describing a toy model’s capabilities
Brand-specific model data (e.g., Lovense’s model -> command mapping in LOVENSE_TOY_NAMES) lives in the brand
subpackages under tikal/low_level/brands/. The BRANDS mapping of brand name -> supported model names is built
from the brand registry in tikal/low_level/brands/__init__.py.
- exception tikal.low_level.toy_data.BadModelError[source]
Bases:
ValidationErrorException raised when a model_name is valid, but its associated commands are not accepted by the toy.
- This exception can mean two things:
A valid, but wrong model_name is being set
the commands being incorrect -> the Library does not handle this model correctly. Please contact the library maintainer in this case.
Can be raised during toy initialization or when setting a toy’s model name.
- exception tikal.low_level.toy_data.InvalidModelError[source]
Bases:
ValidationErrorException raised when a model_name is invalid.
This exception is raised when attempting to set a model name that is not recognized. Can be raised during toy initialization or when setting a toy’s model name.
Example
try: toy.set_model_name("InvalidModel") except InvalidModelError as e: print(f"Invalid model: {e}")
- class tikal.low_level.toy_data.ToyCommands(intensity1_name: str, intensity1_command: str, intensity2_name: str | None = None, intensity2_command: str | None = None)[source]
Bases:
objectCommand configuration for a toy model.
Defines the available capabilities for a specific toy model. Provides names for user display and commands for protocol communication. You shouldn’t need to instantiate this class, but if you use LOVENSE_TOY_NAMES you will use instances of this class.
- intensity1_name
Display name for the primary capability shown to users (e.g., “Vibration”, “Thrust”).
- Type:
str
- intensity1_command
Command string for the primary capability sent to the toy (e.g., “Vibrate”, “Thrusting”).
- Type:
str
- intensity2_name
Display name for the secondary capability, or None if the toy has only one capability (e.g., “Rotation”, “Air”).
- Type:
str | None
- intensity2_command
Command string for the secondary capability, or None if the toy has no secondary capability (e.g., “Rotate”, “Air:Level”).
- Type:
str | None
Example
# Check capabilities commands = LOVENSE_TOY_NAMES["Nora"] print(f"{commands.intensity1_name}: {commands.intensity1_command}") if commands.intensity2_name: print(f"{commands.intensity2_name}: {commands.intensity2_command}")
- intensity1_command: str
- intensity1_name: str
- intensity2_command: str | None = None
- intensity2_name: str | None = None
- class tikal.low_level.toy_data.ToyData(_name: str, _toy_id: str, _model_name: str, _brand: str)[source]
Bases:
objectBase class for toy discovery data.
Contains the information needed to identify and connect to a toy device. You shouldn’t need to instantiate this class yourself.
ConnectionBuilder.discover_toys()creates instances of this class for you.- Properties:
name: Read-only human-readable identifier for the toy. For Bluetooth toys, this is the Bluetooth name (e.g., “LVS-B12”). toy_id: Read-only unique identifier for the toy. For Bluetooth toys, this is the Bluetooth address (e.g., “DC:F5:05:A3:6D:1E”). model_name: Model name of the toy (e.g., “Lush”). For Lovense toys, this is empty and must be set manually. brand: Read-only brand name of the toy (e.g., “Lovense”).
Example
# Created during discovery print(lovense_data.name) # "LVS-Z36D" print(lovense_data.toy_id) # "DC:F5:05:A3:6D:1E" print(lovense_data.model_name) # "" # User selects model lovense_data.model_name = "Nora"
- property brand: str
- property model_name: str
- property name: str
- property toy_id: str
- exception tikal.low_level.toy_data.ValidationError[source]
Bases:
ExceptionException raised when a model_name is invalid.
This exception is raised when attempting to set a model name that is not recognized. Can be raised during toy initialization or when setting a toy’s model name.
Example
try: toy.set_model_name("InvalidModel") except ValidationError as e: print(f"Invalid model: {e}")
tikal.low_level.transport module
Part of the low-level API: Provides the transport layer for toy communication. You are not meant to instantiate any of these classes yourself. Objects of these classes are created by the ConnectionBuilder as needed.
Decouples toy protocol logic from the underlying wire technology (BLE, USB, etc.). Concrete implementations wrap bleak or pyserial-asyncio-fast.
- class tikal.low_level.transport.BleTransport(device: ~bleak.backends.device.BLEDevice, uuid_resolver: ~typing.Callable[[~bleak.BleakClient], ~typing.Awaitable[tuple[str, str]]], on_disconnect: ~typing.Callable[[str], None] | None = None, client_class: ~typing.Type[~bleak.BleakClient] = <class 'bleak.BleakClient'>)[source]
Bases:
TransportTransportimplementation for Bluetooth Low Energy via bleak.Call
connect()after construction! It establishes the BLE connection and resolves TX / RX UUIDs, via the supplied uuid_resolver- Parameters:
device – The
BLEDeviceto connect to.uuid_resolver – Async callable that receives the connected
BleakClientand returns(tx_uuid, rx_uuid). Invoked once insideconnect()while the link is already open, so it can inspect the GATT services.on_disconnect – Optional callback is invoked with this self.toy_id when the underlying BLE connection drops unexpectedly. Not invoked for intentional disconnects via
disconnect().client_class –
BleakClientsubclass to use. Defaults toBleakClient; override for testing.
- async connect() None[source]
Do not call. The
ConnectionBuilderwill call this for you.Opens the BLE connection and resolves TX / RX UUIDs. Must be called exactly once after construction and before any other method.
- Raises:
ConnectionError – If the BLE connection fails or if UUID resolution raises.
- async disconnect() None[source]
Close the BLE connection and release all resources. After this call
is_connectedreturnsFalse.- Raises:
ConnectionError – If the operation fails. Connection is still regarded as closed if this exception is raised.
- property is_connected: bool
Truewhile the underlying link is open, elseFalse.
- async reconnect() None[source]
Attempts to reconnect to the toy. Does nothing if already connected. Use only after an unintended disconnect (
ConnectionError). If you disconnected viadisconnect(), use theConnectionBuilderinstead.- Raises:
RuntimeError – If called after an intentional disconnect.
ConnectionError – If the reconnection attempt fails.
- async send(data: bytes) None[source]
Send encoded data to the toy.
- Parameters:
data – Encoded data to send to the toy.
- Raises:
ConnectionError – If the transport is not connected or the operation fails.
- async start_notify(callback: Callable[[bytes], None]) None[source]
Start receiving inbound data and invoke callback for each one.
- Parameters:
callback – callback invoked with each inbound
bytespayload.- Raises:
ConnectionError – If the transport is not connected or the operation fails.
- class tikal.low_level.transport.MockTransport(toy_id: str, name: str, battery: int = 77)[source]
Bases:
TransportIn-memory
Transportfor the fictionalMockEstimToysbrand.Backed by no real hardware: it simulates a device that speaks a tiny line protocol (commands and responses are UTF-8 strings terminated by
;). On everysend()it computes a canned response and feeds it straight back through the notification callback, so theToylayer’s request/response flow works exactly as it would over BLE.Used purely to explore the library’s architecture and to add a second brand/transport without a physical toy.
- Parameters:
toy_id – Unique identifier for the fake device (e.g.
"Thunder_ID").name – Human-readable device name (e.g.
"Thunder1").battery – Battery percentage the device reports in response to a
Batterycommand.
- async disconnect() None[source]
Close the simulated link. After this call
is_connectedreturnsFalse.
- property is_connected: bool
Truewhile the underlying link is open, elseFalse.
- async reconnect() None[source]
Re-open the simulated link. Does nothing if already connected.
- Raises:
RuntimeError – If called after an intentional
disconnect().
- async send(data: bytes) None[source]
Accept a command and immediately feed the simulated response back through the notification callback.
- Raises:
ConnectionError – If the transport is not connected.
- async start_notify(callback: Callable[[bytes], None]) None[source]
Register the callback that receives simulated responses.
- Raises:
ConnectionError – If the transport is not connected.
- class tikal.low_level.transport.Transport(toy_id: str, name: str)[source]
Bases:
ABCAbstract transport layer for a connected toy.
Wraps the raw I/O operations so that
Toysubclasses can send commands and receive notifications without knowing whether the underlying link is BLE, USB, or anything else.A
Transportis always created in a connected state. Connection setup is the responsibility of theConnectionBuilder.- Parameters:
toy_id – Unique identifier e.g., BLE: Bluetooth address string, USB: serial port path.
name – Human-readable device name e.g., BLE: advertised name. USB: the literal string
"usb-<serial port path>".
- abstractmethod async disconnect() None[source]
Close the underlying connection and release all resources. After this call
is_connectedmust returnFalse.- Raises:
ConnectionError – If the operation fails.
- abstract property is_connected: bool
Truewhile the underlying link is open, elseFalse.
- property name: str
Returns a Human-readable device name.
- abstractmethod async reconnect() None[source]
Attempts to reconnect to the toy. Does nothing if already connected.
- abstractmethod async send(data: bytes) None[source]
Write raw bytes to the device.
- Parameters:
data – Bytes to send (the
Toylayer is responsible for framing).- Raises:
ConnectionError – If the transport is not connected or the operation fails.
- abstractmethod async start_notify(callback: Callable[[bytes], None]) None[source]
Start receiving inbound data and invoke callback for each one.
- Parameters:
callback – callback invoked with each inbound
bytespayload.- Raises:
ConnectionError – If the transport is not connected or the operation fails.
- property toy_id: str
returns a unique identifier (BLE address or USB port).
- class tikal.low_level.transport.UsbTransport(port: str, baudrate: int, reader: StreamReader, writer: StreamWriter)[source]
Bases:
TransportTransportimplementation for USB serial via pyserial-asyncio-fast.- Parameters:
port – Serial port path, e.g.
"/dev/ttyUSB0"or"COM3".baudrate – Baud rate for the serial connection.
reader –
asyncio.StreamReaderfromserial_asyncio_fast.writer –
asyncio.StreamWriterfromserial_asyncio_fast.
- async classmethod connect(port: str, baudrate: int) UsbTransport[source]
Open the serial port and return a connected
UsbTransport.
- async disconnect() None[source]
Close the serial connection and release all resources. After this call
is_connectedreturnsFalse.- Raises:
ConnectionError – If the operation fails. The connection is still regarded as closed if this exception is raised.
- property is_connected: bool
Truewhile the underlying link is open, elseFalse.
- async reconnect() None[source]
Attempts to reconnect to the toy. Does nothing if already connected. Use only after an unintended disconnect (
ConnectionError). If you disconnected viadisconnect(), use theConnectionBuilderinstead.- Raises:
RuntimeError – If called after an intentional disconnect.
ConnectionError – If the reconnection attempt fails.
- async send(data: bytes) None[source]
Write raw bytes to the device.
- Parameters:
data – Bytes to send (the
Toylayer is responsible for framing).- Raises:
ConnectionError – If the transport is not connected or the operation fails.
- async start_notify(callback: Callable[[bytes], None]) None[source]
Spawns a background task that reads lines from the serial port and invokes callback for each one.
- Parameters:
callback – callback invoked with each inbound
bytespayload.- Raises:
ConnectionError – If the transport is not connected or the operation fails.
Module contents
- class tikal.low_level.BLEBrandHandler(on_disconnect: ~typing.Callable[[str], ~typing.Any], on_power_off: ~typing.Callable[[str], ~typing.Any], logger_name: str = 'tikal', client_class: ~typing.Type[~bleak.BleakClient] = <class 'bleak.BleakClient'>)[source]
Bases:
ABCAbstract interface for handling a specific brand of BLE toy.
Each concrete implementation provides the logic for: - Recognizing the brand from BLE advertisements - Creating the appropriate
ToyData- Establishing a connection and returning a ready-to-useToyinstanceSubclasses are instantiated by the
BLEConnectionBuilderthrough the constructor below. A subclass only needs to implement the four abstract methods.- Parameters:
on_disconnect – Callback called when a toy disconnects unexpectedly. Receives the toy’s toy_id.
on_power_off – Callback called when the user powers off a toy via its physical button. Receives the toy id (address).
logger_name – Name for the logger used by this handler. Defaults to ‘tikal’.
client_class – BLE client class to use. Defaults to BleakClient. Can be overridden for testing.
- abstractmethod async create_toy(toy_data: ToyData, device: BLEDevice) Toy[source]
Connect to the toy described by
toy_datausing the providedBLEDevice.This method handles brand-specific connection steps (UUID resolution, notification setup, etc.) and returns a connected
Toyinstance.- Raises:
ValidationError – The
toy_datacontains an invalid model name.ConnectionError – The BLE connection, UUID resolution, or notification setup failed.
- abstractmethod static create_toy_data(device: BLEDevice) ToyData[source]
Create a brand-specific
ToyDataobject from a BLE device.
- class tikal.low_level.BLEConnectionBuilder(on_disconnect: ~typing.Callable[[str], ~typing.Any], on_power_off: ~typing.Callable[[str], ~typing.Any], logger_name: str, scanner_class: ~typing.Type[~bleak.BleakScanner] = <class 'bleak.BleakScanner'>, client_class: ~typing.Type[~bleak.BleakClient] = <class 'bleak.BleakClient'>)[source]
Bases:
object- async create_toy(to_connect: ToyData) Toy | BaseException[source]
Create a Toy instance from discovery data.
- Parameters:
to_connect – ToyData object with a valid model_name. For Lovense valid model names are in LOVENSE_TOY_NAMES.keys() of module toy_data Instances of ToyData are created with a prior call to
discover_toys()and the model_name must be set by you.- Returns:
KeyError: toy address was not found in the cache (i.e.,discover_toys()was not called first)StaleDeviceError: Subclass of ConnectionError: Device was discovered, but has since become stale. Retrieve a new snapshot i.e., viadiscover_toys()ConnectionError: BLE connection or notification setup failed, e.g., the toy may have become unavailableInvalidModelError: If model_name is not valid for this toy brand.BadModelError: If the model_name is valid, but commands still fail. See BadModelError for detailsRuntimeError: Developer error. I did not specify a handler for this subclass of ToyData. Should never happen.
- Return type:
connected Toy instance on success, or a BaseException on failure. Possible exceptions
- Example::
toys = await builder.discover_toys(5.0) # Discover toys # Set model names (e.g., from user input) toys[0].model_name = “Nora” result = await builder.create_toys(toys[0]) # Connect print(f”Connected: {isinstance(result, Toy)}”)
- async create_toys(to_connect: list[ToyData]) list[Toy | BaseException][source]
Create Toy instances from discovery data.
- Parameters:
to_connect – List of ToyData objects with valid model_names. For Lovense valid model names are in LOVENSE_TOY_NAMES.keys() of module toy_data Instances of ToyData are created by
discover_toys()orretrieve_continuous()and model_names must be set by you.- Returns:
KeyError: toy address was not found in the cache (i.e.,discover_toys()was not called first)StaleDeviceError: Subclass of ConnectionError: Device was discovered, but has since become stale. Retrieve a new snapshot i.e., viadiscover_toys()ConnectionError: BLE connection or notification setup failed, e.g., the toy may have become unavailableInvalidModelError: If model_name is not valid for this toy brand.BadModelError: If the model_name is valid, but commands still fail. See BadModelError for detailsRuntimeError: Developer error. I did not specify a handler for this subclass of ToyData. Should never happen.
The order of results matches the order of the input list.
- Return type:
List where each element is either a connected Toy instance or a BaseException for failed connections. Possible exceptions per element
- Example::
toys = await builder.discover_toys(5.0) # Discover toys # Set model names (e.g., from user input) toys[0].model_name = “Nora” toys[1].model_name = “Lush” results = await builder.create_toys(toys) # Connect # Process results connected_toys = [r for r in results if isinstance(r, Toy)] failed = [r for r in results if isinstance(r, BaseException)]
- async discover_toys(timeout: float = 10.0) list[ToyData][source]
Scan for all available BLE toys.
This method caches discovered BLE devices internally. You should call this method before calling
create_toys().- Parameters:
timeout – Scan time in seconds. Longer timeouts increase the chance of finding all nearby devices. Default is 10 seconds.
- Raises:
Exception – Any exception from BleakScanner.discover(), such as permission errors or Bluetooth adapter issues
RuntimeError – If a continuous scan is in progress. See meth: start_continuous_scan and meth: stop_continuous_scan.
- Returns:
List of
ToyDataobjects with brand‑appropriate types. This is a snapshot, not a continuous stream of updates.
Examples:
toys = await builder.discover_toys(timeout=5.0) print(f"Found {len(toys)} Lovense devices") for toy in toys: print(f"{toy.name} at {toy.toy_id}")
- handles_toy(toy_data: ToyData) bool[source]
Return
Trueif one of this builder’s brand handlers recognizestoy_data.Used by
ConnectionBuilderto route aToyDatato the builder that can connect it.
- async retrieve_continuous() list[ToyData][source]
Retrieve the current snapshot of discovered Toys. Needs a continuous scan to be running (else always return the empty list) Use
start_continuous()to start the continuous scan.If an error occurred during continuous scanning and is still relevant (=no call to
stop_continuous()was made afterward), the first call to this method raises that exception. Subsequent calls return an empty list. Note that such exceptions stop the continuous scan.- Raises:
Any exception that occurred during continuous scanning as described above. –
- Returns:
List of
ToyDataobjects with brand‑appropriate types. The list is empty if continuous discovery is not running. This is a snapshot, not a continuous stream of updates.
- async start_continuous(on_update: Callable[[list[ToyData] | Exception], Any] | None = None) None[source]
Start continuous background discovery of Toys.
Continuously scan for new toys. New toys are added to the internal cache and stale toys removed. Use meth:
retrieve_continuousto get the current snapshot. Use meth: stop_continuous to stop the background scan. The method is idempotent. Calls to this method will be ignored if a continuous scan is already running.- Parameters:
on_update – Optional callback. Called with a snapshot of all discovered Toys whenever the internal cache changes. Called with an exception if the continuous scan encounters an error. Can be seen as a push version of
retrieve_continuous(). The first call to on_update reflects the clearing of the internal cache, meaning you’ll always get an empty list first. This serves to ensure any cache you might have made is cleaned too.- Raises:
Exception – Any exception raised by the BLE scanner (e.g., permission errors).
- async stop_continuous() None[source]
Stop the continuous background discovery.
Use :meth: start_continuous to start continuous background discover. After stopping,
retrieve_continuous()will return an empty tuple. The method is idempotent. Does nothing if continuous discovery is not running.
- exception tikal.low_level.BadModelError[source]
Bases:
ValidationErrorException raised when a model_name is valid, but its associated commands are not accepted by the toy.
- This exception can mean two things:
A valid, but wrong model_name is being set
the commands being incorrect -> the Library does not handle this model correctly. Please contact the library maintainer in this case.
Can be raised during toy initialization or when setting a toy’s model name.
- class tikal.low_level.BleTransport(device: ~bleak.backends.device.BLEDevice, uuid_resolver: ~typing.Callable[[~bleak.BleakClient], ~typing.Awaitable[tuple[str, str]]], on_disconnect: ~typing.Callable[[str], None] | None = None, client_class: ~typing.Type[~bleak.BleakClient] = <class 'bleak.BleakClient'>)[source]
Bases:
TransportTransportimplementation for Bluetooth Low Energy via bleak.Call
connect()after construction! It establishes the BLE connection and resolves TX / RX UUIDs, via the supplied uuid_resolver- Parameters:
device – The
BLEDeviceto connect to.uuid_resolver – Async callable that receives the connected
BleakClientand returns(tx_uuid, rx_uuid). Invoked once insideconnect()while the link is already open, so it can inspect the GATT services.on_disconnect – Optional callback is invoked with this self.toy_id when the underlying BLE connection drops unexpectedly. Not invoked for intentional disconnects via
disconnect().client_class –
BleakClientsubclass to use. Defaults toBleakClient; override for testing.
- async connect() None[source]
Do not call. The
ConnectionBuilderwill call this for you.Opens the BLE connection and resolves TX / RX UUIDs. Must be called exactly once after construction and before any other method.
- Raises:
ConnectionError – If the BLE connection fails or if UUID resolution raises.
- async disconnect() None[source]
Close the BLE connection and release all resources. After this call
is_connectedreturnsFalse.- Raises:
ConnectionError – If the operation fails. Connection is still regarded as closed if this exception is raised.
- property is_connected: bool
Truewhile the underlying link is open, elseFalse.
- async reconnect() None[source]
Attempts to reconnect to the toy. Does nothing if already connected. Use only after an unintended disconnect (
ConnectionError). If you disconnected viadisconnect(), use theConnectionBuilderinstead.- Raises:
RuntimeError – If called after an intentional disconnect.
ConnectionError – If the reconnection attempt fails.
- class tikal.low_level.ConnectionBuilder(on_disconnect: ~typing.Callable[[str], ~typing.Any], on_power_off: ~typing.Callable[[str], ~typing.Any], logger_name: str, bluetooth_scanner: ~typing.Type[~bleak.BleakScanner] = <class 'bleak.BleakScanner'>, bluetooth_client: ~typing.Type[~bleak.BleakClient] = <class 'bleak.BleakClient'>, mock_toys: bool = False)[source]
Bases:
objectTransport-agnostic entry point of the Low-Level API.
A
ConnectionBuildercomposes one connection builder per supported transport (BLEConnectionBuilderfor real BLE toys andMockConnectionBuilderfor the fictional MockEstimToys brand) and exposes the exact same discovery/connection API as a single builder. Discovery results from every transport are merged into one list, and connection requests are routed to the builder that owns the relevantToyData.Adding a brand on a new transport only means: write that transport’s connection builder and append it to
self._buildersbelow. Every consumer ofConnectionBuilderkeeps working unchanged.- Parameters:
on_disconnect – Callback invoked when a toy disconnects unexpectedly. Receives the toy’s toy_id. Not called for intentional disconnects.
on_power_off – Callback invoked when the user powers off a toy via the physical power button. Receives the toy id.
logger_name – Name of the logger to use. Use empty string for root logger.
bluetooth_scanner – BLE scanner class to use. Defaults to BleakScanner. Can be overridden for testing.
bluetooth_client – BLE client class to use. Defaults to BleakClient. Can be overridden for testing.
mock_toys – If True, use the MockEstimToys brand. Not part of the public API. This parameter may be removed without notice.
- async create_toy(to_connect: ToyData) Toy | BaseException[source]
Create a single Toy instance from discovery data, routing it to the transport that owns it.
See
BLEConnectionBuilder.create_toy()for arguments and the result/exception types.- Returns:
A connected
Toyon success, or aBaseExceptionon failure. ReturnsRuntimeErrorif no transport recognizes the givenToyData(developer error; should never happen).
- async create_toys(to_connect: list[ToyData]) list[Toy | BaseException][source]
Create Toy instances from discovery data, routing each to the transport that owns it.
See
BLEConnectionBuilder.create_toys()for arguments and the per-element result/exception types.- Returns:
List where each element is a connected
Toyor aBaseException. Order matches the input list.
- async discover_toys(timeout: float = 10.0) list[ToyData][source]
Scan every transport for available toys and return the combined result.
Delegates to each underlying connection builder and concatenates their results. See
BLEConnectionBuilder.discover_toys()for the per-transport behavior, arguments, and raised exceptions.- Parameters:
timeout – Scan time in seconds, passed to every transport.
- Returns:
Combined list of
ToyDataacross all transports. A snapshot, not a continuous stream.
- async retrieve_continuous() list[ToyData][source]
Retrieve the current merged snapshot of discovered Toys from every transport.
Needs a continuous scan to be running (see
start_continuous()); otherwise returns the empty list. If a transport has a pending continuous-scan exception, the first call re-raises it (seeBLEConnectionBuilder.retrieve_continuous()).- Returns:
Combined list of
ToyDataacross all transports.
- async start_continuous(on_update: Callable[[list[ToyData] | Exception], Any] | None = None) None[source]
Start continuous background discovery across every transport.
Each transport scans independently; their snapshots are merged so
on_updatealways receives a single list spanning all transports. SeeBLEConnectionBuilder.start_continuous()for the general contract.- Parameters:
on_update – Optional callback. Called with a merged snapshot of all discovered Toys whenever any transport’s view changes, or with an exception if a transport’s scan errors.
- exception tikal.low_level.InvalidModelError[source]
Bases:
ValidationErrorException raised when a model_name is invalid.
This exception is raised when attempting to set a model name that is not recognized. Can be raised during toy initialization or when setting a toy’s model name.
Example
try: toy.set_model_name("InvalidModel") except InvalidModelError as e: print(f"Invalid model: {e}")
- tikal.low_level.Lovense
Backwards-compatible alias for
LovenseToy(the class was formerly namedLovense).
- class tikal.low_level.LovenseToy(transport: BleTransport, model_name: str, on_power_off: Callable[[str], Any], logger_name: str)[source]
Bases:
ToyLow-level representation of a Lovense BLE toy.
Implements the Lovense-specific protocol for communication with Lovense toys over Bluetooth Low Energy. Handles command formatting, response parsing, and Lovense-specific notifications (like power-off events). You are not meant to instantiate these classes directly.
BLEConnectionBuilderestablishes connections to toys and returns instances ofLovenseToy- Parameters:
transport – Transport layer handling the BLE communication.
model_name – Model name (e.g., “Nora”, “Lush”). Must be a key in LOVENSE_TOY_NAMES.
on_power_off – Callback invoked when the user powers off the toy via the physical power button. Receives the toy’s Bluetooth address as a string argument.
logger_name – Name of the logger to use. Use empty string for root logger.
Example:
# Set intensity of the primary capability to maximum await toy.intensity1(20) # Send a custom command response = await toy.direct_command("DeviceType") print(f"Device info: {response}")
- property brand: str
Returns the brand of the toy. Always “Lovense” for this class
- async change_rotation_direction() bool[source]
Change rotation direction for toys with rotation capability.
This method only affects toys that support rotation (Nora and Ridge). For other toys, it returns True immediately without sending any command.
- Returns:
True if the toy acknowledged the command or if rotation is not supported, False if the command failed.
Example:
await toy.rotate_change_direction() # Toggle rotation direction
- property change_rotation_direction_available: bool
Check if the toy supports changing the rotation direction.
- Returns:
True if the rotation direction can be changed, False otherwise.
- Return type:
bool
Example:
if toy.change_rotation_direction_available: await toy.change_rotation_direction()
- async direct_command(command: str, timeout: float = 3.0) str | None[source]
Send any command directly to the toy.
This method allows sending commands that are not implemented by the library.
- Parameters:
command – Command string in UTF-8 format. A semicolon terminator will be added if not present.
timeout – Response timeout in seconds. Defaults to 3.0.
- Returns:
Response string from the toy (with semicolon stripped), or None if timeout or error occurred.
Example:
# Query firmware version response = await toy.direct_command("DeviceType") # Response format: "C:11:0082059AD3BD" # (C = device type, 11 = firmware version, address)
- async disconnect() None[source]
Disconnect from the device.
Stops all toy actions, disables notifications, and closes the BLE connection. This is regarded as intentional disconnect and does not trigger an on_disconnect callback even if an on_disconnect callback was set (either during initialization or via set_on_disconnect). The method does not raise any exceptions.
Note
After calling this method, the toy object is unusable. To connect again, you will need to re-scan and connect using the ConnectionBuilder. Use the newly provided Lovense object by the ConnectionBuilder.
- async get_batch_number() str | None[source]
Retrieve the production batch number.
The batch number appears to be in YYMMDD format, indicating the manufacturing date.
- Returns:
Batch number string (e.g., “241015” for October 15, 2024), or None if an error occurred.
Example:
batch = await toy.get_batch_number() if batch: print(f"Manufactured: 20{batch[:2]}/{batch[2:4]}/{batch[4:6]}")
- async get_battery_level() int | None[source]
Retrieve the battery level as a percentage.
- Returns:
Battery level (0-100%), or None if the command failed or timed out.
Example:
battery = await toy.get_battery_level() if battery is not None: if battery < 20: print(f"Low battery: {battery}%") else: print(f"Battery: {battery}%")
Note
Lovense toys have a quirk where they prefix ‘s’ before the value if the toy was recently reconnected. This method strips the ‘s’ when present
- async get_device_type() str | None[source]
Retrieve device type and firmware information.
- Returns:
FirmwareVersion:Address” (e.g., “C:11:0082059AD3BD”), or None if an error occurred.
- Return type:
String in format “DeviceType
Example:
info = await toy.get_device_type() if info: parts = info.split(":") device_type = parts[0] firmware = parts[1] print(f"Device type: {device_type}, Firmware: {firmware}")
- async get_status() int | None[source]
Retrieve the status code of the toy.
- Returns:
Status code (2 = Normal operation), or None if an error occurred.
Example:
status = await toy.get_status() if status == 2: print("Toy is operating normally") else print(f"Unusual status code: {status}")
- async intensity1(level: int) bool[source]
Set the primary capability to the specified level.
The primary capability depends on the toy model (e.g., vibration for Gush, thrusting for Solace)
- Parameters:
level – Intensity level (0-20). Values outside this range are clamped.
- Returns:
True if the toy acknowledged the command, False otherwise.
Example:
await toy.intensity1(20) # Set primary capability to maximum
- async intensity2(level: int) bool[source]
Set the secondary capability to the specified level.
The secondary capability depends on the toy model (e.g., Rotation for Nora, depth control for Solace). Not all toys have a secondary capability. Returns true immediately and does not send a command if the toy has no secondary capability.
- Parameters:
level – Intensity level (0-20). Values outside the valid range are clamped.
- Returns:
True if the toy acknowledged the command or if no secondary capability exists, False otherwise
Example:
await toy.intensity2(10) # Set secondary capability to medium intensity
Note
For Max’s air pump, the level is automatically divided by 4 to convert from 0-20 scale to 0-5 scale.
- property intensity_names: tuple[str, str | None]
Get display names for Lovense toy capabilities.
- Returns:
(primary_name, secondary_name). Secondary name is None if the toy has only one capability.
- Return type:
tuple[str, str | None]
Example:
names = toy.intensity_names print(f"{names[0]}: intensity1") # "Vibration: intensity1" if names[1]: print(f"{names[1]}: intensity2") # "Rotation: intensity2"
- property max_intensity: int
Get maximum intensity value for Lovense toys.
- Returns:
Always 20 for Lovense toys.
- Return type:
int
Note
Some capabilities (like Max’s air pump) use different ranges. Those are automatically scaled for you.
- async power_off() bool[source]
Turn off power to the toy.
This sends the PowerOff command which turns off the toy. The toy can then only be turned back on via the physical power button.
- Returns:
True if successful, False otherwise.
Example:
await toy.power_off()
- property recommended_min_interval: int
Get the recommended minimum interval between intensity changes (milliseconds).
- Returns:
The Lovense per-model suggested minimum segment length.
- Return type:
int
- async reconnect() bool[source]
Attempts to reconnect to the toy to after an unintentional disconnect.
Does nothing if already connected. Use only after an unintended disconnect (ConnectionError). If you disconnected via disconnect, use the ConnectionBuilder instead.
- Returns:
True if the reconnection was successful, False otherwise.
- async set_model_name(model_name: str) None[source]
Set the model name of the toy.
- Parameters:
model_name – New model name. Must be in LOVENSE_TOY_NAMES.keys() of module ToyData. Case Insensitive. Case Insensitive.
- Raises:
InvalidModelError – If model_name is not valid for this toy brand.
BadModelError – If the model_name is valid, but commands still fail. See BadModelError for details.
Example:
# Update model name in case it was set incorrectly while building the connection via the ConnectionBuilder toy.set_model_name("Nora")
- async start_notifications() None[source]
Start listening for messages from the Lovense toy.
This method is called by the ConnectionBuilder during connection setup and should not be called manually.
- Raises:
ConnectionError – Failed to start notifications.
- async stop() bool[source]
Stop all toy actions by setting all intensities to zero.
This method is a shortcut for intensity1(0) and intensity2(0)
- Returns:
True if both commands succeeded, False if either failed.
Example:
await toy.stop()
- async strict_change_rotation_direction() bool[source]
Similar to :meth: rotate_change_direction, but this method raises an exception if the command fails.
- Raises:
UnexpectedToyResponse – The toys’ response was unexpected, e.g. “ERROR” instead of “OK”.
ConnectionError – Command could not be sent, or the toy did not respond within an appropriate timeout.
- Returns:
True if the toy supports rotation, False otherwise. Does not raise if the toy does not support rotation.
- async strict_direct_command(command: str, timeout: float = 3.0) str[source]
Similar to :meth: direct_command, but this method raises an exception if the command fails.
- Raises:
ConnectionError – Command could not be sent, or the toy did not respond within the provided timeout.
- Returns:
response string from the toy
- async strict_disconnect() None[source]
Similar to :meth: disconnect, but this methode does raise an exception if the disconnect fails. The exception is only for logging. The toy is still disconnected in the error case.
- Raises:
ConnectionError – Command could not be sent, or the toy did not respond within an appropriate timeout.
- async strict_get_batch_number() str[source]
Similar to :meth: get_batch_number, but this method raises an exception if the command fails.
- Raises:
ConnectionError – Command could not be sent, or the toy did not respond within an appropriate timeout.
UnexpectedToyResponse – The toys’ response was unexpected.
- Returns:
Batch number string (e.g., “241015” for October 15, 2024)
- async strict_get_battery_level() int[source]
Similar to :meth: get_battery_level, but this method raises an exception if the command fails.
- Raises:
UnexpectedToyResponse – The toys’ response was unexpected.
ConnectionError – Command could not be sent, or the toy did not respond within an appropriate timeout.
- Returns:
Battery level as a percentage (0-100).
- async strict_get_device_type() str[source]
Similar to :meth: get_device_type, but this method raises an exception if the command fails.
- Raises:
ConnectionError – Command could not be sent, or the toy did not respond within the provided timeout.
- Returns:
FirmwareVersion:Address” (e.g., “C:11:0082059AD3BD”)
- Return type:
String in format “DeviceType
Note
does not raise UnexpectedToyResponse as the response is not verified.
- async strict_get_status() int[source]
Similar to :meth: get_status, but this method raises an exception if the command fails.
- Raises:
ConnectionError – Command could not be sent, or the toy did not respond within the provided timeout.
UnexpectedToyResponse – The toys’ response was unexpected, in this case any string containing non-digit characters.
- Returns:
Status code (2 = Normal operation)
- async strict_intensity1(level: int) bool[source]
Similar to :meth: intensity1, but this method raises an exception if the intensity command fails.
- Raises:
ConnectionError – Command could not be sent, or the toy did not respond within an appropriate timeout.
UnexpectedToyResponse – The toys’ response was unexpected, e.g. “ERROR” instead of “OK”.
- Returns:
Always true
- async strict_intensity2(level: int) bool[source]
Similar to :meth: intensity2, but this method raises an exception if the intensity command fails.
- Raises:
ConnectionError – Command could not be sent, or the toy did not respond within an appropriate timeout.
UnexpectedToyResponse – The toys’ response was unexpected, e.g. “ERROR” instead of “OK”.
- Returns:
True if the toy supports a secondary capability, False otherwise.
- async strict_power_off() bool[source]
Similar to :meth: power_off, but this method raises an exception if the command fails.
- Raises:
UnexpectedToyResponse – The toys’ response was unexpected.
ConnectionError – Command could not be sent, or the toy did not respond within an appropriate timeout.
- Returns:
Always True.
- async strict_reconnect() bool[source]
Similar to :meth: reconnect, but this methode does raise an exception if the reconnection fails.
- Raises:
ConnectionError – The reconnection failed.
RuntimeError – The reconnection was attempted after intentionally disconnecting the toy.
- Returns:
Always True.
- async strict_stop() bool[source]
Similar to :meth: stop, but this method raises an exception if the stop command fails.
- Raises:
UnexpectedToyResponse – The toys’ response was unexpected, e.g. “ERROR” instead of “OK”.
ConnectionError – Command could not be sent, or the toy did not respond within an appropriate timeout.
- Returns:
Always True.
- class tikal.low_level.MockConnectionBuilder(on_disconnect: Callable[[str], Any], on_power_off: Callable[[str], Any], logger_name: str)[source]
Bases:
objectConnection builder for the fictional
MockEstimToysbrand.Discovers a fixed set of fake devices and connects them via in-memory transports. Implements the same surface the composite
ConnectionBuilderrequires of every per-transport builder:discover_toys,start_continuous,stop_continuous,retrieve_continuous,handles_toy, andcreate_toy.- Parameters:
on_disconnect – Callback invoked when a toy disconnects unexpectedly. Receives the toy id. Unused by the mock (the fake devices never drop on their own) but accepted for interface symmetry.
on_power_off – Callback invoked when a toy reports a power-off. Receives the toy id.
logger_name – Name of the logger to use. Use empty string for root logger.
- async create_toy(to_connect: ToyData) Toy | BaseException[source]
Connect to a fake MockEstimToys device.
- Returns:
KeyError: the toy id is not one of the known fake devices.InvalidModelError: the model name is not valid for this brand.ConnectionError: connection or notification setup failed.
- Return type:
A connected
MockEstimToyon success, or aBaseExceptionon failure
- async discover_toys(timeout: float = 10.0) list[ToyData][source]
Return the fixed set of fake MockEstimToys devices.
- Parameters:
timeout – Ignored; there is no real scan. Present for interface compatibility.
- Returns:
A fresh list of
ToyData(model name pre-filled, since the fake devices self-identify).
- handles_toy(toy_data: ToyData) bool[source]
Return
Trueif the givenToyDatabelongs to the MockEstimToys brand.
- async retrieve_continuous() list[ToyData][source]
Return the current snapshot: the fake devices while scanning, otherwise an empty list.
- async start_continuous(on_update: Callable[[list[ToyData] | Exception], Any] | None = None) None[source]
Start “continuous” discovery. The fake devices are reported immediately and then never change.
- Parameters:
on_update – Optional callback. Called once with an empty list (to clear any cache) and then with the fake devices, mirroring the contract of the real builders.
- class tikal.low_level.MockEstimToy(transport: MockTransport, model_name: str, on_power_off: Callable[[str], Any], logger_name: str)[source]
Bases:
ToyLow-level representation of a fictional MockEstimToys device.
Implements the brand’s simple channel-based protocol over
MockTransport.Thunderis a single-channel model;Lightningis a dual-channel model. You are not meant to instantiate this class directly.- Parameters:
transport – Connected
MockTransportinstance.model_name – Model name. Must be a key in
MOCK_ESTIM_TOY_NAMES(“Thunder” or “Lightning”).on_power_off – Callback invoked when the device reports a power-off. Receives the toy id.
logger_name – Name of the logger to use. Use empty string for root logger.
- property brand: str
Returns the brand of the toy. Always “MockEstimToys” for this class.
- async change_rotation_direction() bool[source]
No-op: MockEstimToys do not support rotation. Returns True.
- property change_rotation_direction_available: bool
MockEstimToys have no rotation capability.
- async direct_command(command: str, timeout: float = 3.0) str | None[source]
Send any command directly to the device. Returns the response, or None on failure.
- async disconnect() None[source]
Stop all actions and close the simulated connection. Does not raise.
- async get_battery_level() int | None[source]
Retrieve the battery level as a percentage, or None on failure.
- async intensity1(level: int) bool[source]
Set the primary channel. Returns True if the device acknowledged the command.
- async intensity2(level: int) bool[source]
Set the secondary channel. Returns True (no-op) for single-channel models.
- property intensity_names: tuple[str, str | None]
Get the display names for the device’s channels. Secondary is None for single-channel models.
- property max_intensity: int
Maximum intensity value for MockEstimToys (0 - MAX_INTENSITY).
- property recommended_min_interval: int
Recommended minimum interval between intensity changes (milliseconds).
- async reconnect() bool[source]
Attempts to reconnect after an unintentional disconnect. Does nothing if already connected.
- Returns:
True if the reconnection was successful, False otherwise.
- async set_model_name(model_name: str) None[source]
Set the model name of the toy.
- Parameters:
model_name – New model name. Must be in
MOCK_ESTIM_TOY_NAMES(case-insensitive).- Raises:
InvalidModelError – If model_name is not valid for this brand.
BadModelError – If the model_name is valid, but commands still fail.
- async start_notifications() None[source]
Start listening for messages from the device. Called by the
MockConnectionBuilderduring connection setup.- Raises:
ConnectionError – Failed to start notifications.
- async strict_change_rotation_direction() bool[source]
No-op: MockEstimToys do not support rotation. Returns False.
- async strict_direct_command(command: str, timeout: float = 3.0) str[source]
Like
direct_command(), but raises on failure.
- async strict_disconnect() None[source]
Like
disconnect(), but raises if the disconnect fails. The toy is still disconnected.
- async strict_get_battery_level() int | None[source]
Like
get_battery_level(), but raises on failure.
- async strict_intensity1(level: int) bool[source]
Like
intensity1(), but raises on failure. Always returns True.
- async strict_intensity2(level: int) bool[source]
Like
intensity2(), but raises on failure. Returns False for single-channel models.
- async strict_reconnect() bool[source]
Like
reconnect(), but raises on failure.
- class tikal.low_level.MockTransport(toy_id: str, name: str, battery: int = 77)[source]
Bases:
TransportIn-memory
Transportfor the fictionalMockEstimToysbrand.Backed by no real hardware: it simulates a device that speaks a tiny line protocol (commands and responses are UTF-8 strings terminated by
;). On everysend()it computes a canned response and feeds it straight back through the notification callback, so theToylayer’s request/response flow works exactly as it would over BLE.Used purely to explore the library’s architecture and to add a second brand/transport without a physical toy.
- Parameters:
toy_id – Unique identifier for the fake device (e.g.
"Thunder_ID").name – Human-readable device name (e.g.
"Thunder1").battery – Battery percentage the device reports in response to a
Batterycommand.
- async disconnect() None[source]
Close the simulated link. After this call
is_connectedreturnsFalse.
- property is_connected: bool
Truewhile the underlying link is open, elseFalse.
- async reconnect() None[source]
Re-open the simulated link. Does nothing if already connected.
- Raises:
RuntimeError – If called after an intentional
disconnect().
- exception tikal.low_level.StaleDeviceError[source]
Bases:
ConnectionErrorRaised when attempting to connect to a device that was at one point discovered but is no longer available.
- class tikal.low_level.Toy(transport: Transport, model_name: str, logger_name: str)[source]
Bases:
ABCAbstract base class representing a low-level toy.
Responsible for handling the communication with physical toys. Provides functions for sending commands to the toy and handles its responses. Each toy brand implements this interface with brand-specific protocol details. You are not meant to instantiate these classes directly.
ConnectionBuilderestablishes connections to toys and returns instances ofToy- Parameters:
transport – Connected Transport instance.
model_name – Model name of the toy (e.g., “Gush”, “Nora”).
logger_name – Name of the logger to use. Use empty string for root logger.
- abstract property brand: str
Returns a human-readable identifier of the toy brand e.g., ‘Lovense’.
- abstractmethod async change_rotation_direction() bool[source]
Change rotation direction.
For toys with rotation capability (e.g., Nora, Ridge), this toggles the rotation direction. Returns True and does nothing if the toy does not support rotation.
- Returns:
True if the toy acknowledged the command or if rotation is not supported, False if the command failed.
Example:
# Reverse rotation direction await toy.rotate_change_direction()
- abstract property change_rotation_direction_available: bool
Check if the toy supports changing the rotation direction.
- Returns:
True if the rotation direction can be changed, False otherwise.
- Return type:
bool
Example:
if toy.change_rotation_direction_available: await toy.change_rotation_direction()
- property current_intensities: tuple[int, int]
Get the current intensity values for the toy’s capabilities.
- Returns:
A tuple of (primary_intensity, secondary_intensity). The secondary intensity is always 0 if the toy has only one capability.
- Return type:
tuple[int, int]
- Example::
intensity1, intensity2 = toy.current_intensities print(f”Primary intensity: {intensity1}, Secondary intensity: {intensity2}”)”)
- abstractmethod async direct_command(command: str, timeout: float = 3.0) str | None[source]
Send any command directly to the toy.
This method allows sending commands that are not implemented by the library.
- Parameters:
command – Command string in UTF-8 format
timeout – Response timeout in seconds. Defaults to 3.0.
- Returns:
Response string from the toy, or None if timeout or error occurred.
- abstractmethod async disconnect() None[source]
Disconnect from the device.
Stops all toy actions, disables notifications, and closes the BLE connection. This method should always be called before the toy object is destroyed to ensure proper cleanup. The method does not raise any exceptions.
- abstractmethod async get_battery_level() int | None[source]
Retrieve the battery level of the connected device.
- Returns:
Battery level as a percentage (0-100), or None if an error occurred, the command timed out, or the toy has no battery.
Example:
battery = await toy.get_battery_level() if battery is not None: print(f"Battery: {battery}%") else: print("Failed to read battery level")
- abstractmethod async intensity1(level: int) bool[source]
Set the primary capability of the toy to a specified level.
The primary capability varies by toy model (e.g., vibration for Gush, thrusting for Solace).
- Parameters:
level – Intensity level. The valid range is 0 - self.max_intensity. Values outside this range are clamped.
- Returns:
True if the toy acknowledged the command, False otherwise.
Example:
# Set primary capability to medium intensity success = await toy.intensity1(10) if not success: print("Command failed or timed out")
- abstractmethod async intensity2(level: int) bool[source]
Set the secondary capability of the toy to a specified level.
The secondary capability varies by toy model (e.g., Depth for Solace, air pump for Max). Not all toys have a secondary capability. Returns true and does nothing if the toy has no secondary capability.
- Parameters:
level – Intensity level. The valid range is 0 - self.max_intensity. Values outside this range are clamped.
- Returns:
True if the toy acknowledged the command or the toy does not have a secondary capability, False otherwise.
Example:
# Set secondary capability to low intensity await toy.intensity2(5)
- abstract property intensity_names: tuple[str, str | None]
Get the display names for the toy’s capabilities.
- Returns:
A tuple of (primary_name, secondary_name). The secondary name is None if the toy has only one capability.
- Return type:
tuple[str, str | None]
Example:
names = toy.intensity_names print(f"Primary: {names[0]}") # example: Vibration if names[1]: print(f"Secondary: {names[1]}") # example: Rotation
- property is_connected: bool
Check if the toy is currently connected.
- Returns:
True if connected, False otherwise.
- abstract property max_intensity: int
Get the maximum intensity value for this toy.
- Returns:
Maximum intensity value (e.g., 20 for Lovense toys).
- Return type:
int
Example:
max_val = toy.intensity_max_value await toy.intensity1(max_val) # Set to maximum
- property model_name: str
Returns the model name of the toy (e.g., “Nora”, “Lush”).
- property name: str
Returns a human-readable identifier of the toy e.g., Bluetooth name.
- abstract property recommended_min_interval: int
Get the recommended minimum interval between intensity changes, in milliseconds.
This is a per-model suggestion, primarily useful for pattern playback (the smallest sensible segment length). This is a recommendation, and you are free to ignore it.
- Returns:
Recommended minimum interval between intensity changes in milliseconds.
- Return type:
int
- abstractmethod async reconnect() bool[source]
Attempts to reconnect to the toy to after an unintentional disconnect.
Does nothing if already connected. Use only after an unintended disconnect (ConnectionError). If you disconnected via disconnect, use the ConnectionBuilder instead.
- Returns:
True if the reconnection was successful, False otherwise.
- abstractmethod async set_model_name(model_name: str) None[source]
Set the model name of the toy.
This method validates and updates the toy’s model name. The model name determines which commands are available and how they’re interpreted.
- Parameters:
model_name – New model name. Must be a valid model for this toy brand.
- Raises:
InvalidModelError – If model_name is not valid for this toy brand.
BadModelError – If the model_name is valid, but commands still fail. See BadModelError for details.
- abstractmethod async stop() bool[source]
Stop all toy actions by setting all intensities to zero.
- Returns:
True if successful, False if either intensity command failed.
Example:
await toy.stop()
- abstractmethod async strict_change_rotation_direction() bool[source]
Similar to :meth: rotate_change_direction, but this method raises an exception if the command fails. :raises UnexpectedToyResponse: The toys’ response was unexpected, e.g. “ERROR” instead of “OK”. :raises ConnectionError: Command could not be sent, or the toy did not respond within an appropriate timeout.
- Returns:
True if the toy supports rotation, False otherwise.
- abstractmethod async strict_direct_command(command: str, timeout: float = 3.0) str[source]
Similar to :meth: direct_command, but this method raises an exception if the command fails.
- Raises:
ConnectionError – Command could not be sent, or the toy did not respond within the provided timeout.
- Returns:
response string from the toy
- abstractmethod async strict_disconnect() None[source]
Similar to :meth: disconnect, but this methode does raise an exception if the disconnect fails. The exception is only for logging. The toy is still disconnected in the error case.
- Raises:
ConnectionError – Command could not be sent, or the toy did not respond within an appropriate timeout.
UnexpectedToyResponse – The toys’ response was unexpected, e.g. “ERROR” instead of “OK”.
- abstractmethod async strict_get_battery_level() int | None[source]
Similar to :meth: get_battery_level, but this method raises an exception if the command fails.
- Raises:
UnexpectedToyResponse – The toys’ response was unexpected.
ConnectionError – Command could not be sent, or the toy did not respond within an appropriate timeout.
- Returns:
Battery level as a percentage (0-100), or None if the toy has no battery.
- abstractmethod async strict_intensity1(level: int) bool[source]
Similar to :meth: intensity1, but this method raises an exception if the intensity command fails.
- Raises:
ConnectionError – Command could not be sent, or the toy did not respond within an appropriate timeout.
UnexpectedToyResponse – The toys’ response was unexpected, e.g. “ERROR” instead of “OK”.
- Returns:
Always true
- abstractmethod async strict_intensity2(level: int) bool[source]
Similar to :meth: intensity2, but this method raises an exception if the intensity command fails.
- Raises:
ConnectionError – Command could not be sent, or the toy did not respond within an appropriate timeout.
UnexpectedToyResponse – The toys’ response was unexpected, e.g. “ERROR” instead of “OK”.
- Returns:
True if the toy supports a secondary capability, False otherwise.
- abstractmethod async strict_reconnect() bool[source]
Similar to :meth: reconnect, but this methode does raise an exception if the reconnection fails.
- Raises:
ConnectionError – The reconnection failed.
RuntimeError – The reconnection was attempted after intentionally disconnecting the toy.
- Returns:
Always True.
- abstractmethod async strict_stop() bool[source]
Similar to :meth: stop, but this method raises an exception if the stop command fails.
- Raises:
UnexpectedToyResponse – The toys’ response was unexpected, e.g. “ERROR” instead of “OK”.
ConnectionError – Command could not be sent, or the toy did not respond within an appropriate timeout.
- Returns:
always True
- property toy_id: str
Returns a unique identifier of the toy e.g., Bluetooth address.
- class tikal.low_level.ToyData(_name: str, _toy_id: str, _model_name: str, _brand: str)[source]
Bases:
objectBase class for toy discovery data.
Contains the information needed to identify and connect to a toy device. You shouldn’t need to instantiate this class yourself.
ConnectionBuilder.discover_toys()creates instances of this class for you.- Properties:
name: Read-only human-readable identifier for the toy. For Bluetooth toys, this is the Bluetooth name (e.g., “LVS-B12”). toy_id: Read-only unique identifier for the toy. For Bluetooth toys, this is the Bluetooth address (e.g., “DC:F5:05:A3:6D:1E”). model_name: Model name of the toy (e.g., “Lush”). For Lovense toys, this is empty and must be set manually. brand: Read-only brand name of the toy (e.g., “Lovense”).
Example
# Created during discovery print(lovense_data.name) # "LVS-Z36D" print(lovense_data.toy_id) # "DC:F5:05:A3:6D:1E" print(lovense_data.model_name) # "" # User selects model lovense_data.model_name = "Nora"
- property brand: str
- property model_name: str
- property name: str
- property toy_id: str
- class tikal.low_level.Transport(toy_id: str, name: str)[source]
Bases:
ABCAbstract transport layer for a connected toy.
Wraps the raw I/O operations so that
Toysubclasses can send commands and receive notifications without knowing whether the underlying link is BLE, USB, or anything else.A
Transportis always created in a connected state. Connection setup is the responsibility of theConnectionBuilder.- Parameters:
toy_id – Unique identifier e.g., BLE: Bluetooth address string, USB: serial port path.
name – Human-readable device name e.g., BLE: advertised name. USB: the literal string
"usb-<serial port path>".
- abstractmethod async disconnect() None[source]
Close the underlying connection and release all resources. After this call
is_connectedmust returnFalse.- Raises:
ConnectionError – If the operation fails.
- abstract property is_connected: bool
Truewhile the underlying link is open, elseFalse.
- property name: str
Returns a Human-readable device name.
- abstractmethod async reconnect() None[source]
Attempts to reconnect to the toy. Does nothing if already connected.
- abstractmethod async send(data: bytes) None[source]
Write raw bytes to the device.
- Parameters:
data – Bytes to send (the
Toylayer is responsible for framing).- Raises:
ConnectionError – If the transport is not connected or the operation fails.
- abstractmethod async start_notify(callback: Callable[[bytes], None]) None[source]
Start receiving inbound data and invoke callback for each one.
- Parameters:
callback – callback invoked with each inbound
bytespayload.- Raises:
ConnectionError – If the transport is not connected or the operation fails.
- property toy_id: str
returns a unique identifier (BLE address or USB port).
- exception tikal.low_level.UnexpectedToyResponse[source]
Bases:
ConnectionErrorRaised when the toy responds with an unexpected message.
- class tikal.low_level.UsbTransport(port: str, baudrate: int, reader: StreamReader, writer: StreamWriter)[source]
Bases:
TransportTransportimplementation for USB serial via pyserial-asyncio-fast.- Parameters:
port – Serial port path, e.g.
"/dev/ttyUSB0"or"COM3".baudrate – Baud rate for the serial connection.
reader –
asyncio.StreamReaderfromserial_asyncio_fast.writer –
asyncio.StreamWriterfromserial_asyncio_fast.
- async classmethod connect(port: str, baudrate: int) UsbTransport[source]
Open the serial port and return a connected
UsbTransport.
- async disconnect() None[source]
Close the serial connection and release all resources. After this call
is_connectedreturnsFalse.- Raises:
ConnectionError – If the operation fails. The connection is still regarded as closed if this exception is raised.
- property is_connected: bool
Truewhile the underlying link is open, elseFalse.
- async reconnect() None[source]
Attempts to reconnect to the toy. Does nothing if already connected. Use only after an unintended disconnect (
ConnectionError). If you disconnected viadisconnect(), use theConnectionBuilderinstead.- Raises:
RuntimeError – If called after an intentional disconnect.
ConnectionError – If the reconnection attempt fails.
- async send(data: bytes) None[source]
Write raw bytes to the device.
- Parameters:
data – Bytes to send (the
Toylayer is responsible for framing).- Raises:
ConnectionError – If the transport is not connected or the operation fails.
- async start_notify(callback: Callable[[bytes], None]) None[source]
Spawns a background task that reads lines from the serial port and invokes callback for each one.
- Parameters:
callback – callback invoked with each inbound
bytespayload.- Raises:
ConnectionError – If the transport is not connected or the operation fails.
- exception tikal.low_level.ValidationError[source]
Bases:
ExceptionException raised when a model_name is invalid.
This exception is raised when attempting to set a model name that is not recognized. Can be raised during toy initialization or when setting a toy’s model name.
Example
try: toy.set_model_name("InvalidModel") except ValidationError as e: print(f"Invalid model: {e}")