triki-library 0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
triki/__init__.py ADDED
@@ -0,0 +1,21 @@
1
+ from .controller import TRIKIController
2
+ from .scanner import TRIKIScanner
3
+ from .types import TRIKIDevice
4
+ from .exceptions import (
5
+ TRIKIError,
6
+ TRIKIConnectionError,
7
+ TRIKIDeviceNotFoundError,
8
+ TRIKINotConnectedError,
9
+ )
10
+
11
+ __version__ = "0.1"
12
+
13
+ __all__ = [
14
+ "TRIKIController",
15
+ "TRIKIScanner",
16
+ "TRIKIDevice",
17
+ "TRIKIError",
18
+ "TRIKIConnectionError",
19
+ "TRIKIDeviceNotFoundError",
20
+ "TRIKINotConnectedError",
21
+ ]
triki/controller.py ADDED
@@ -0,0 +1,197 @@
1
+ import bleak.exc
2
+ from bleak import BleakClient
3
+
4
+ from .exceptions import TRIKIDeviceNotFoundError, TRIKINotConnectedError, TRIKIConnectionError
5
+
6
+ UART_RX_UUID = "6e400002-b5a3-f393-e0a9-e50e24dcca9e"
7
+ UART_TX_UUID = "6e400003-b5a3-f393-e0a9-e50e24dcca9e"
8
+ BATTERY_LEVEL_UUID = "00002a19-0000-1000-8000-00805f9b34fb"
9
+ LED_CHARACTERISTIC_UUID = "6e400004-b5a3-f393-e0a9-e50e24dcca9e"
10
+ START_STREAM_COMMAND = bytes.fromhex("20 10 00 d0 07 34 00 03")
11
+
12
+
13
+ class TRIKIController:
14
+ """
15
+ Base class for the TRIKI controller
16
+
17
+ Args:
18
+ address:
19
+ Device Bluetooth address
20
+ """
21
+ __device_client: BleakClient = None
22
+ __bt_address: str = "AA:BB:CC:DD:EE:FF"
23
+ __battery_level: int = 0
24
+ __sensor_spin_x: int = 0
25
+ __sensor_spin_y: int = 0
26
+ __sensor_turn_z: int = 0
27
+ __sensor_tilt_x: int = 0
28
+ __sensor_tilt_y: int = 0
29
+ __sensor_flip_z: int = 0
30
+ __button_pressed: bool = False
31
+
32
+ def __init__(self, address: str) -> None:
33
+ self.__bt_address = address
34
+
35
+ def __little_endian(self, a: int, b: int) -> int:
36
+ """Parse data by using a little endian algorithm"""
37
+ x = a + (b * 256)
38
+ if x > 32767:
39
+ x -= 65536
40
+ return x
41
+
42
+ def __parse_received_data(self, sender, data: bytearray) -> None:
43
+ """Parse data when received"""
44
+ if len(data) >= 16 and data[0] == 0x22:
45
+ data_sliced = data.hex(" ").split(" ")
46
+ data_decimal = []
47
+ for i in range(len(data_sliced)):
48
+ data_decimal.append(int(data_sliced[i], 16))
49
+
50
+ self.__sensor_spin_x = self.__little_endian(data_decimal[2], data_decimal[3])
51
+ self.__sensor_spin_y = self.__little_endian(data_decimal[4], data_decimal[5])
52
+ self.__sensor_turn_z = self.__little_endian(data_decimal[6], data_decimal[7])
53
+ self.__sensor_tilt_x = self.__little_endian(data_decimal[8], data_decimal[9])
54
+ self.__sensor_tilt_y = self.__little_endian(data_decimal[10], data_decimal[11])
55
+ self.__sensor_flip_z = self.__little_endian(data_decimal[12], data_decimal[13])
56
+
57
+ self.__button_pressed = data_decimal[15] == 1
58
+
59
+ async def connect(self, raise_exception_on_fail: bool = False) -> None:
60
+ """Connect to the TRIKI controller"""
61
+ self.__device_client = BleakClient(self.__bt_address, timeout=10)
62
+ try:
63
+ await self.__device_client.connect()
64
+ except bleak.exc.BleakDeviceNotFoundError as error:
65
+ if raise_exception_on_fail:
66
+ raise TRIKIDeviceNotFoundError(
67
+ f"TRIKI device with address {self.__bt_address} was not found"
68
+ ) from error
69
+ except bleak.exc.BleakError as error:
70
+ if raise_exception_on_fail:
71
+ raise TRIKIConnectionError(
72
+ f"Connection with TRIKI device (at {self.__bt_address}) failed"
73
+ ) from error
74
+
75
+ if self.__device_client.is_connected:
76
+ try:
77
+ battery_data = await self.__device_client.read_gatt_char(BATTERY_LEVEL_UUID)
78
+ self.__battery_level = int(battery_data[0])
79
+
80
+ await self.__device_client.start_notify(UART_TX_UUID, self.__parse_received_data)
81
+
82
+ await self.__device_client.write_gatt_char(
83
+ UART_RX_UUID, START_STREAM_COMMAND, response=True
84
+ )
85
+ except bleak.exc.BleakError as error:
86
+ raise TRIKIConnectionError(
87
+ f"Connected to the TRIKI device (at {self.__bt_address}), but initialization failed"
88
+ ) from error
89
+ else:
90
+ raise TRIKIConnectionError(
91
+ f"Connection with TRIKI device (at {self.__bt_address}) failed"
92
+ )
93
+
94
+ async def disconnect(self) -> None:
95
+ """Disconnect from the TRIKI device"""
96
+ if self.__device_client is not None and self.__device_client.is_connected:
97
+ await self.toggle_led(False)
98
+ await self.__device_client.disconnect()
99
+
100
+ @property
101
+ def is_connected(self) -> bool:
102
+ """Check if the TRIKI controller is connected"""
103
+ return self.__device_client is not None and self.__device_client.is_connected
104
+
105
+ @property
106
+ def battery_level(self) -> int:
107
+ """Get the TRIKI controller battery level"""
108
+ if not self.is_connected:
109
+ raise TRIKINotConnectedError(
110
+ "Cannot read battery_level, because the TRIKI controller is not connected"
111
+ )
112
+ return self.__battery_level
113
+
114
+ @property
115
+ def spin_x(self) -> int:
116
+ """Get the TRIKI controller X Spin value"""
117
+ if not self.is_connected:
118
+ raise TRIKINotConnectedError(
119
+ "Cannot read spin_x, because the TRIKI controller is not connected"
120
+ )
121
+ return self.__sensor_spin_x
122
+
123
+ @property
124
+ def spin_y(self) -> int:
125
+ """Get the TRIKI controller Y Spin value"""
126
+ if not self.is_connected:
127
+ raise TRIKINotConnectedError(
128
+ "Cannot read spin_y, because the TRIKI controller is not connected"
129
+ )
130
+ return self.__sensor_spin_y
131
+
132
+ @property
133
+ def turn_z(self) -> int:
134
+ """Get the TRIKI controller Z Turn value"""
135
+ if not self.is_connected:
136
+ raise TRIKINotConnectedError(
137
+ "Cannot read turn_z, because the TRIKI controller is not connected"
138
+ )
139
+ return self.__sensor_turn_z
140
+
141
+ @property
142
+ def tilt_x(self) -> int:
143
+ """Get the TRIKI controller X Tilt value"""
144
+ if not self.is_connected:
145
+ raise TRIKINotConnectedError(
146
+ "Cannot read tilt_x, because the TRIKI controller is not connected"
147
+ )
148
+ return self.__sensor_tilt_x
149
+
150
+ @property
151
+ def tilt_y(self) -> int:
152
+ """Get the TRIKI controller Y Tilt value"""
153
+ if not self.is_connected:
154
+ raise TRIKINotConnectedError(
155
+ "Cannot read tilt_y, because the TRIKI controller is not connected"
156
+ )
157
+ return self.__sensor_tilt_y
158
+
159
+ @property
160
+ def flip_z(self) -> int:
161
+ """Get the TRIKI controller Z Flip value"""
162
+ if not self.is_connected:
163
+ raise TRIKINotConnectedError(
164
+ "Cannot read flip_z, because the TRIKI controller is not connected"
165
+ )
166
+ return self.__sensor_flip_z
167
+
168
+ @property
169
+ def button_pressed(self) -> bool:
170
+ """Check if the TRIKI controller button is pressed"""
171
+ if not self.is_connected:
172
+ raise TRIKINotConnectedError(
173
+ "Cannot read button_pressed, because the TRIKI controller is not connected"
174
+ )
175
+ return self.__button_pressed
176
+
177
+ async def toggle_led(self, is_on: bool) -> None:
178
+ """
179
+ Toggle the TRIKI controller built-in LED light
180
+
181
+ Args:
182
+ is_on:
183
+ Set the LED status
184
+ """
185
+ if not self.is_connected:
186
+ raise TRIKINotConnectedError(
187
+ "Cannot toggle LED, because the TRIKI controller is not connected"
188
+ )
189
+
190
+ if is_on:
191
+ bytes_to_send = bytes([0x01])
192
+ else:
193
+ bytes_to_send = bytes([0x00])
194
+
195
+ await self.__device_client.write_gatt_char(
196
+ LED_CHARACTERISTIC_UUID, bytes_to_send, response=True
197
+ )
triki/exceptions.py ADDED
@@ -0,0 +1,14 @@
1
+ class TRIKIError(Exception):
2
+ """Base exception for triki errors."""
3
+
4
+
5
+ class TRIKIConnectionError(TRIKIError):
6
+ """Raised when connection to a TRIKI device fails."""
7
+
8
+
9
+ class TRIKIDeviceNotFoundError(TRIKIError):
10
+ """Raised when a TRIKI device cannot be found."""
11
+
12
+
13
+ class TRIKINotConnectedError(TRIKIError):
14
+ """Raised when called operation requires a connected TRIKI device."""
triki/scanner.py ADDED
@@ -0,0 +1,27 @@
1
+ from bleak import BleakScanner, BLEDevice
2
+ from .types import TRIKIDevice
3
+
4
+
5
+ class TRIKIScanner:
6
+ """Base class for the TRIKI controller scanner"""
7
+ __scanner = None
8
+ __bleak_devices: list[BLEDevice] = []
9
+ __scanned_devices: list[TRIKIDevice] = []
10
+
11
+ def __init__(self) -> None:
12
+ self.__scanner = BleakScanner()
13
+
14
+ async def scan(self) -> None:
15
+ """Scan for TRIKI controllers"""
16
+ self.__bleak_devices = await self.__scanner.discover()
17
+ self.__scanned_devices.clear()
18
+
19
+ for device in self.__bleak_devices:
20
+ if device.name is not None and "Triki" in device.name:
21
+ triki_device = TRIKIDevice("Triki " + device.address[-6:], device.address)
22
+ self.__scanned_devices.append(triki_device)
23
+
24
+ @property
25
+ def scanned_devices(self) -> list[TRIKIDevice]:
26
+ """Scanned TRIKI controllers"""
27
+ return self.__scanned_devices
triki/types.py ADDED
@@ -0,0 +1,7 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass(frozen=True)
5
+ class TRIKIDevice:
6
+ name: str
7
+ address: str
@@ -0,0 +1,126 @@
1
+ Metadata-Version: 2.4
2
+ Name: triki-library
3
+ Version: 0.1
4
+ Summary: A Python library for integrating the TRIKI motion controller (made by Caps Apps) into your projects.
5
+ Project-URL: Homepage, https://github.com/woofter-wolf/triki-library-python
6
+ Project-URL: Repository, https://github.com/woofter-wolf/triki-library-python
7
+ Project-URL: Issues, https://github.com/woofter-wolf/triki-library-python
8
+ Author: Woofter_Wolf
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: ble,bluetooth,controller,triki
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: Microsoft :: Windows
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development :: Libraries
19
+ Classifier: Topic :: System :: Hardware
20
+ Requires-Python: >=3.11
21
+ Requires-Dist: bleak>=3.0.2
22
+ Description-Content-Type: text/markdown
23
+
24
+ # TRIKI Library
25
+ A Python library for the integrating TRIKI motion controller (made by Caps Apps) into your projects.
26
+
27
+ > This library is still in development. Some things may change sligthly.
28
+
29
+ ---
30
+
31
+ ## What is TRIKI?
32
+ TRIKI is a Bluetooth Low Energy (BLE) motion controller that has the shape and size of a bottle cap. It is produced by Caps Apps (also known as HOPX) and sold by 'Żabka', a convenience store franchise.
33
+
34
+ It was designed to be used in mini-games which are on the store's loyalty app.
35
+
36
+ ## Why I made this library
37
+ I wanted to make this library to give everyone a way to integrate this niche controller into their own games, experiments and other Python projects.
38
+
39
+ ## Features
40
+ - Scan for TRIKI controllers
41
+ - Connect to TRIKI controller
42
+ - Get battery level
43
+ - Get IMU data from the controller
44
+ - Get state of the built-in button
45
+ - Control built-in LED
46
+
47
+ ## Platforms
48
+ | Platform | Does it work? |
49
+ |---------------|---------------------|
50
+ | Windows 10/11 | ✅ Tested |
51
+ | macOS | ❔ Untested |
52
+ | Linux | ⚠️ Partially tested |
53
+ > Linux support needs further testing. Some Arch-based distributions may have issues with receiving data.
54
+ ---
55
+ ## Installation
56
+ ```bash
57
+ pip install triki-library
58
+ ```
59
+
60
+ ## Usage
61
+ ```python
62
+ # Import the library
63
+ import asyncio
64
+ from triki import TRIKIScanner, TRIKIController
65
+ ```
66
+
67
+ ```python
68
+ # Scan for TRIKI controllers
69
+ scanner = TRIKIScanner()
70
+ await scanner.scan()
71
+ devices = scanner.scanned_devices # Returned as list[TRIKIDevice]
72
+
73
+ devices[0].name # Get device name
74
+ devices[0].address # Get device BT address
75
+ ```
76
+ ```python
77
+ # Connect to the controller
78
+ controller = TRIKIController("AA:BB:CC:DD:EE:FF")
79
+ await controller.connect()
80
+ ```
81
+ ```python
82
+ controller.is_connected # Check if device is connected
83
+ controller.battery_level # Get battery level
84
+ ```
85
+ ```python
86
+ controller.spin_x # Get X Spin
87
+ controller.spin_y # Get Y Spin
88
+ controller.turn_z # Get Z Turn
89
+ controller.tilt_x # Get X Tilt
90
+ controller.tilt_y # Get Y Tilt
91
+ controller.flip_z # Get Z Flip
92
+ ```
93
+ ```python
94
+ controller.button_pressed # Check if built-in button is pressed
95
+ ```
96
+ ```python
97
+ #turn on LED
98
+ await controller.toggle_led(True)
99
+
100
+ #turn off LED
101
+ await controller.toggle_led(False)
102
+ ```
103
+ ```python
104
+ # Loop
105
+ try:
106
+ while controller.is_connected:
107
+ # Do stuff
108
+ finally:
109
+ await controller.disconnect()
110
+ ```
111
+ ```python
112
+ # IMPORTANT: disconnect from the controller when you're done with it!
113
+ await controller.disconnect()
114
+ ```
115
+ >Examples can be found on the 'examples' directory.
116
+ ---
117
+
118
+ ## Credits
119
+ Library made by [Woofter_Wolf](https://woofterwolf.space)
120
+
121
+ Special thanks to [Wojciech "Koksny" Górny](https://koksny.com) for doing the hard work of documenting this device (look here for communication documentation): [Link](https://github.com/koksny/TRIKI-Control)
122
+
123
+ ## Copyright
124
+ "TRIKI" and "Żabka" are trademarks of Żabka Polska sp. z o.o. "HOPX" is a trademark of Caps Apps sp. z o.o.
125
+
126
+ This library is an independent, open-source project. It is **NOT** affiliated, endorsed or sponsored by these companies. This library ships with no software, images or any assets belonging to these companies.
@@ -0,0 +1,9 @@
1
+ triki/__init__.py,sha256=AHjiD0BA2QFSnSfpgUbRpiB2bBc0I6Th0adQHKLAk4Q,460
2
+ triki/controller.py,sha256=7XH_JM6WffCzyuQuwLoCffbQwIIU2iNFpRwCAutEo1k,7515
3
+ triki/exceptions.py,sha256=wwUwVGIXCudFYMTk1ItNpIiXgGg9WTjlaeCvr7w0ShQ,404
4
+ triki/scanner.py,sha256=UZovNbsSws_bdu6-0Dxfsih_NtbJe2GrufwqYhxtwLw,944
5
+ triki/types.py,sha256=3dcQ_ULegVH5B173x8iqKIg7AlB5n_fxKefoAR83hzU,117
6
+ triki_library-0.1.dist-info/METADATA,sha256=E_h4T_if4z7WKrVGWk3DoJp7iIBpDo3G6xhFpwoTU1c,4106
7
+ triki_library-0.1.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
8
+ triki_library-0.1.dist-info/licenses/LICENSE,sha256=p3f-kD18WHRPEDAoq1ZetdFSIO4XszzcuJ9A6cQRQCo,1083
9
+ triki_library-0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright © 2026 Woofter_Wolf
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.