kd104a 0.1.0__tar.gz
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.
- kd104a-0.1.0/.idea/.gitignore +8 -0
- kd104a-0.1.0/.idea/inspectionProfiles/Project_Default.xml +60 -0
- kd104a-0.1.0/.idea/inspectionProfiles/profiles_settings.xml +6 -0
- kd104a-0.1.0/.idea/kd104a.iml +12 -0
- kd104a-0.1.0/.idea/misc.xml +6 -0
- kd104a-0.1.0/.idea/modules.xml +8 -0
- kd104a-0.1.0/.idea/vcs.xml +6 -0
- kd104a-0.1.0/.idea/workspace.xml +126 -0
- kd104a-0.1.0/LICENSE +21 -0
- kd104a-0.1.0/PKG-INFO +117 -0
- kd104a-0.1.0/README.md +98 -0
- kd104a-0.1.0/pyproject.toml +33 -0
- kd104a-0.1.0/src/kd104a/__init__.py +11 -0
- kd104a-0.1.0/src/kd104a/controller.py +73 -0
- kd104a-0.1.0/src/kd104a/device.py +41 -0
- kd104a-0.1.0/src/kd104a/effects.py +229 -0
- kd104a-0.1.0/src/kd104a/enums.py +28 -0
- kd104a-0.1.0/src/kd104a/packet.py +23 -0
- kd104a-0.1.0/tests/test_basic.py +14 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<component name="InspectionProjectProfileManager">
|
|
2
|
+
<profile version="1.0">
|
|
3
|
+
<option name="myName" value="Project Default" />
|
|
4
|
+
<inspection_tool class="DotEnvExtraBlankLineInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
5
|
+
<inspection_tool class="DuplicatedCode" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
|
6
|
+
<inspection_tool class="HttpUrlsUsage" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
|
7
|
+
<option name="ignoredUrls">
|
|
8
|
+
<list>
|
|
9
|
+
<option value="http://" />
|
|
10
|
+
<option value="http://0.0.0.0" />
|
|
11
|
+
<option value="http://127.0.0.1" />
|
|
12
|
+
<option value="http://activemq.apache.org/schema/" />
|
|
13
|
+
<option value="http://cxf.apache.org/schemas/" />
|
|
14
|
+
<option value="http://java.sun.com/" />
|
|
15
|
+
<option value="http://javafx.com/fxml" />
|
|
16
|
+
<option value="http://javafx.com/javafx/" />
|
|
17
|
+
<option value="http://json-schema.org/draft" />
|
|
18
|
+
<option value="http://localhost" />
|
|
19
|
+
<option value="http://maven.apache.org/POM/" />
|
|
20
|
+
<option value="http://maven.apache.org/xsd/" />
|
|
21
|
+
<option value="http://primefaces.org/ui" />
|
|
22
|
+
<option value="http://schema.cloudfoundry.org/spring/" />
|
|
23
|
+
<option value="http://schemas.xmlsoap.org/" />
|
|
24
|
+
<option value="http://tiles.apache.org/" />
|
|
25
|
+
<option value="http://www.ibm.com/webservices/xsd" />
|
|
26
|
+
<option value="http://www.jboss.com/xml/ns/" />
|
|
27
|
+
<option value="http://www.jboss.org/j2ee/schema/" />
|
|
28
|
+
<option value="http://www.springframework.org/schema/" />
|
|
29
|
+
<option value="http://www.springframework.org/security/tags" />
|
|
30
|
+
<option value="http://www.springframework.org/tags" />
|
|
31
|
+
<option value="http://www.thymeleaf.org" />
|
|
32
|
+
<option value="http://www.w3.org/" />
|
|
33
|
+
<option value="http://xmlns.jcp.org/" />
|
|
34
|
+
</list>
|
|
35
|
+
</option>
|
|
36
|
+
</inspection_tool>
|
|
37
|
+
<inspection_tool class="PyAttributeOutsideInitInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
|
38
|
+
<inspection_tool class="PyBroadExceptionInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
|
39
|
+
<inspection_tool class="PyCallingNonCallableInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
40
|
+
<inspection_tool class="PyInterpreterInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
41
|
+
<inspection_tool class="PyPep8Inspection" enabled="false" level="INFORMATION" enabled_by_default="false">
|
|
42
|
+
<option name="ignoredErrors">
|
|
43
|
+
<list>
|
|
44
|
+
<option value="E302" />
|
|
45
|
+
</list>
|
|
46
|
+
</option>
|
|
47
|
+
</inspection_tool>
|
|
48
|
+
<inspection_tool class="PyPep8NamingInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false">
|
|
49
|
+
<option name="ignoredErrors">
|
|
50
|
+
<list>
|
|
51
|
+
<option value="N802" />
|
|
52
|
+
</list>
|
|
53
|
+
</option>
|
|
54
|
+
</inspection_tool>
|
|
55
|
+
<inspection_tool class="PyProtectedMemberInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
|
56
|
+
<inspection_tool class="PyShadowingBuiltinsInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
|
57
|
+
<inspection_tool class="PyTypeCheckerInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
58
|
+
<inspection_tool class="PyUnresolvedReferencesInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
|
59
|
+
</profile>
|
|
60
|
+
</component>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<module type="PYTHON_MODULE" version="4">
|
|
3
|
+
<component name="NewModuleRootManager">
|
|
4
|
+
<content url="file://$MODULE_DIR$">
|
|
5
|
+
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
|
6
|
+
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
|
|
7
|
+
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
|
8
|
+
</content>
|
|
9
|
+
<orderEntry type="jdk" jdkName="Python 3.13 (kd104a)" jdkType="Python SDK" />
|
|
10
|
+
<orderEntry type="sourceFolder" forTests="false" />
|
|
11
|
+
</component>
|
|
12
|
+
</module>
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<project version="4">
|
|
3
|
+
<component name="AutoImportSettings">
|
|
4
|
+
<option name="autoReloadType" value="SELECTIVE" />
|
|
5
|
+
</component>
|
|
6
|
+
<component name="ChangeListManager">
|
|
7
|
+
<list default="true" id="2912c955-f467-4d38-9168-e0543a01d4c4" name="Changes" comment="alpha" />
|
|
8
|
+
<option name="SHOW_DIALOG" value="false" />
|
|
9
|
+
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
|
10
|
+
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
|
11
|
+
<option name="LAST_RESOLUTION" value="IGNORE" />
|
|
12
|
+
</component>
|
|
13
|
+
<component name="FileTemplateManagerImpl">
|
|
14
|
+
<option name="RECENT_TEMPLATES">
|
|
15
|
+
<list>
|
|
16
|
+
<option value="Python Script" />
|
|
17
|
+
</list>
|
|
18
|
+
</option>
|
|
19
|
+
</component>
|
|
20
|
+
<component name="Git.Settings">
|
|
21
|
+
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
|
22
|
+
</component>
|
|
23
|
+
<component name="GitHubPullRequestSearchHistory"><![CDATA[{
|
|
24
|
+
"lastFilter": {
|
|
25
|
+
"state": "OPEN",
|
|
26
|
+
"assignee": "yakcom"
|
|
27
|
+
}
|
|
28
|
+
}]]></component>
|
|
29
|
+
<component name="GithubPullRequestsUISettings"><![CDATA[{
|
|
30
|
+
"selectedUrlAndAccountId": {
|
|
31
|
+
"url": "https://github.com/yakcom/kd104a.git",
|
|
32
|
+
"accountId": "3bdfb013-94bd-46e6-a6b8-d56bb6fbf396"
|
|
33
|
+
}
|
|
34
|
+
}]]></component>
|
|
35
|
+
<component name="ProjectColorInfo"><![CDATA[{
|
|
36
|
+
"associatedIndex": 6
|
|
37
|
+
}]]></component>
|
|
38
|
+
<component name="ProjectId" id="3CA84dLd63yQllfjm4y0vhNCmdK" />
|
|
39
|
+
<component name="ProjectLevelVcsManager" settingsEditedManually="true">
|
|
40
|
+
<ConfirmationsSetting value="2" id="Add" />
|
|
41
|
+
</component>
|
|
42
|
+
<component name="ProjectViewState">
|
|
43
|
+
<option name="hideEmptyMiddlePackages" value="true" />
|
|
44
|
+
<option name="showLibraryContents" value="true" />
|
|
45
|
+
</component>
|
|
46
|
+
<component name="PropertiesComponent"><![CDATA[{
|
|
47
|
+
"keyToString": {
|
|
48
|
+
"Python.api.executor": "Debug",
|
|
49
|
+
"Python.controller.executor": "Debug",
|
|
50
|
+
"Python.device.executor": "Debug",
|
|
51
|
+
"Python.effects.executor": "Debug",
|
|
52
|
+
"Python.enums.executor": "Debug",
|
|
53
|
+
"Python.test_basic.executor": "Debug",
|
|
54
|
+
"RunOnceActivity.ShowReadmeOnStart": "true",
|
|
55
|
+
"git-widget-placeholder": "master",
|
|
56
|
+
"last_opened_file_path": "C:/Users/yax/Documents/Python/kd104a",
|
|
57
|
+
"node.js.detected.package.eslint": "true",
|
|
58
|
+
"node.js.detected.package.tslint": "true",
|
|
59
|
+
"node.js.selected.package.eslint": "(autodetect)",
|
|
60
|
+
"node.js.selected.package.tslint": "(autodetect)",
|
|
61
|
+
"nodejs_package_manager_path": "npm",
|
|
62
|
+
"vue.rearranger.settings.migration": "true"
|
|
63
|
+
}
|
|
64
|
+
}]]></component>
|
|
65
|
+
<component name="SharedIndexes">
|
|
66
|
+
<attachedChunks>
|
|
67
|
+
<set>
|
|
68
|
+
<option value="bundled-js-predefined-d6986cc7102b-76f8388c3a79-JavaScript-PY-243.24978.54" />
|
|
69
|
+
<option value="bundled-python-sdk-91e3b7efe1d4-466328ff949b-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-243.24978.54" />
|
|
70
|
+
</set>
|
|
71
|
+
</attachedChunks>
|
|
72
|
+
</component>
|
|
73
|
+
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
|
|
74
|
+
<component name="TaskManager">
|
|
75
|
+
<task active="true" id="Default" summary="Default task">
|
|
76
|
+
<changelist id="2912c955-f467-4d38-9168-e0543a01d4c4" name="Changes" comment="" />
|
|
77
|
+
<created>1775816824248</created>
|
|
78
|
+
<option name="number" value="Default" />
|
|
79
|
+
<option name="presentableId" value="Default" />
|
|
80
|
+
<updated>1775816824248</updated>
|
|
81
|
+
<workItem from="1775816825332" duration="18759000" />
|
|
82
|
+
</task>
|
|
83
|
+
<task id="LOCAL-00001" summary="init">
|
|
84
|
+
<option name="closed" value="true" />
|
|
85
|
+
<created>1775900791506</created>
|
|
86
|
+
<option name="number" value="00001" />
|
|
87
|
+
<option name="presentableId" value="LOCAL-00001" />
|
|
88
|
+
<option name="project" value="LOCAL" />
|
|
89
|
+
<updated>1775900791506</updated>
|
|
90
|
+
</task>
|
|
91
|
+
<task id="LOCAL-00002" summary="alpha">
|
|
92
|
+
<option name="closed" value="true" />
|
|
93
|
+
<created>1775909177743</created>
|
|
94
|
+
<option name="number" value="00002" />
|
|
95
|
+
<option name="presentableId" value="LOCAL-00002" />
|
|
96
|
+
<option name="project" value="LOCAL" />
|
|
97
|
+
<updated>1775909177743</updated>
|
|
98
|
+
</task>
|
|
99
|
+
<task id="LOCAL-00003" summary="alpha">
|
|
100
|
+
<option name="closed" value="true" />
|
|
101
|
+
<created>1775909318425</created>
|
|
102
|
+
<option name="number" value="00003" />
|
|
103
|
+
<option name="presentableId" value="LOCAL-00003" />
|
|
104
|
+
<option name="project" value="LOCAL" />
|
|
105
|
+
<updated>1775909318425</updated>
|
|
106
|
+
</task>
|
|
107
|
+
<option name="localTasksCounter" value="4" />
|
|
108
|
+
<servers />
|
|
109
|
+
</component>
|
|
110
|
+
<component name="TypeScriptGeneratedFilesManager">
|
|
111
|
+
<option name="version" value="3" />
|
|
112
|
+
</component>
|
|
113
|
+
<component name="VcsManagerConfiguration">
|
|
114
|
+
<MESSAGE value="init" />
|
|
115
|
+
<MESSAGE value="alpha" />
|
|
116
|
+
<option name="LAST_COMMIT_MESSAGE" value="alpha" />
|
|
117
|
+
</component>
|
|
118
|
+
<component name="com.intellij.coverage.CoverageDataManagerImpl">
|
|
119
|
+
<SUITE FILE_PATH="coverage/kd104a$test_basic.coverage" NAME="test_basic Coverage Results" MODIFIED="1775899333166" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/tests" />
|
|
120
|
+
<SUITE FILE_PATH="coverage/kd104a$enums.coverage" NAME="enums Coverage Results" MODIFIED="1775819700210" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/src/kd104a" />
|
|
121
|
+
<SUITE FILE_PATH="coverage/kd104a$device.coverage" NAME="device Coverage Results" MODIFIED="1775818675389" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/src/kd104a" />
|
|
122
|
+
<SUITE FILE_PATH="coverage/kd104a$api.coverage" NAME="api Coverage Results" MODIFIED="1775857158989" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/src/kd104a" />
|
|
123
|
+
<SUITE FILE_PATH="coverage/kd104a$controller.coverage" NAME="controller Coverage Results" MODIFIED="1775898860349" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/src/kd104a" />
|
|
124
|
+
<SUITE FILE_PATH="coverage/kd104a$effects.coverage" NAME="effects Coverage Results" MODIFIED="1775898630792" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/src/kd104a" />
|
|
125
|
+
</component>
|
|
126
|
+
</project>
|
kd104a-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Your Name
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
kd104a-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kd104a
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: KD104A keyboard lighting control over HID, with a small, straightforward API for building and applying effects.
|
|
5
|
+
Project-URL: Homepage, https://github.com/yakcom/kd104a
|
|
6
|
+
Project-URL: Repository, https://github.com/yakcom/kd104a
|
|
7
|
+
Author: Ilya Miller
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: dark,hid,kd104a,keyboard,lighting,rgb
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Requires-Dist: hidapi>=0.14.0
|
|
17
|
+
Requires-Dist: webcolors>=1.13
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# kd104a
|
|
21
|
+
Simple HID control for KD104A keyboard lighting.
|
|
22
|
+
|
|
23
|
+
# Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install kd104a
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
# Quick Start
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from kd104a import Device, Lighting, Direction, effects
|
|
33
|
+
|
|
34
|
+
keyboard = Device(product="Gaming Keyboard", interface=2)
|
|
35
|
+
lighting = Lighting(keyboard)
|
|
36
|
+
|
|
37
|
+
lighting.set_mode(
|
|
38
|
+
effects.wave,
|
|
39
|
+
brightness=1,
|
|
40
|
+
speed=20,
|
|
41
|
+
direction=Direction.RIGHT_LEFT,
|
|
42
|
+
color1="red",
|
|
43
|
+
color2="blue",
|
|
44
|
+
)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
# Usage
|
|
48
|
+
|
|
49
|
+
### Power
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
lighting.off()
|
|
53
|
+
lighting.on()
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Change mode
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
lighting.set_mode(effects.static, color="yellow")
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Update parameters
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
lighting.set_brightness(100)
|
|
66
|
+
lighting.set_speed(10)
|
|
67
|
+
lighting.set_direction(Direction.LEFT_RIGHT)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
# Parameters
|
|
71
|
+
|
|
72
|
+
### Brightness / Speed
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
0-100 # percent
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
# Colors
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
"red"
|
|
82
|
+
"#ff0000"
|
|
83
|
+
(255, 0, 0)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
# Direction
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
Direction.LEFT_RIGHT
|
|
90
|
+
Direction.RIGHT_LEFT
|
|
91
|
+
Direction.TOP_BOTTOM
|
|
92
|
+
Direction.BOTTOM_TOP
|
|
93
|
+
Direction.CENTER_OUT
|
|
94
|
+
Direction.OUT_CENTER
|
|
95
|
+
Direction.CLOCKWISE
|
|
96
|
+
Direction.COUNTER_CLOCKWISE
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
# Effects
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
effects.static
|
|
103
|
+
effects.neon
|
|
104
|
+
effects.breathing
|
|
105
|
+
effects.wave
|
|
106
|
+
effects.blink
|
|
107
|
+
effects.radar
|
|
108
|
+
effects.ripple
|
|
109
|
+
effects.marquee
|
|
110
|
+
effects.shine
|
|
111
|
+
effects.ripple2
|
|
112
|
+
effects.interactive
|
|
113
|
+
effects.crossing
|
|
114
|
+
effects.firework
|
|
115
|
+
effects.reactive
|
|
116
|
+
effects.equalizer
|
|
117
|
+
```
|
kd104a-0.1.0/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# kd104a
|
|
2
|
+
Simple HID control for KD104A keyboard lighting.
|
|
3
|
+
|
|
4
|
+
# Install
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pip install kd104a
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
# Quick Start
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
from kd104a import Device, Lighting, Direction, effects
|
|
14
|
+
|
|
15
|
+
keyboard = Device(product="Gaming Keyboard", interface=2)
|
|
16
|
+
lighting = Lighting(keyboard)
|
|
17
|
+
|
|
18
|
+
lighting.set_mode(
|
|
19
|
+
effects.wave,
|
|
20
|
+
brightness=1,
|
|
21
|
+
speed=20,
|
|
22
|
+
direction=Direction.RIGHT_LEFT,
|
|
23
|
+
color1="red",
|
|
24
|
+
color2="blue",
|
|
25
|
+
)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
# Usage
|
|
29
|
+
|
|
30
|
+
### Power
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
lighting.off()
|
|
34
|
+
lighting.on()
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Change mode
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
lighting.set_mode(effects.static, color="yellow")
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Update parameters
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
lighting.set_brightness(100)
|
|
47
|
+
lighting.set_speed(10)
|
|
48
|
+
lighting.set_direction(Direction.LEFT_RIGHT)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
# Parameters
|
|
52
|
+
|
|
53
|
+
### Brightness / Speed
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
0-100 # percent
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
# Colors
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
"red"
|
|
63
|
+
"#ff0000"
|
|
64
|
+
(255, 0, 0)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
# Direction
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
Direction.LEFT_RIGHT
|
|
71
|
+
Direction.RIGHT_LEFT
|
|
72
|
+
Direction.TOP_BOTTOM
|
|
73
|
+
Direction.BOTTOM_TOP
|
|
74
|
+
Direction.CENTER_OUT
|
|
75
|
+
Direction.OUT_CENTER
|
|
76
|
+
Direction.CLOCKWISE
|
|
77
|
+
Direction.COUNTER_CLOCKWISE
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
# Effects
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
effects.static
|
|
84
|
+
effects.neon
|
|
85
|
+
effects.breathing
|
|
86
|
+
effects.wave
|
|
87
|
+
effects.blink
|
|
88
|
+
effects.radar
|
|
89
|
+
effects.ripple
|
|
90
|
+
effects.marquee
|
|
91
|
+
effects.shine
|
|
92
|
+
effects.ripple2
|
|
93
|
+
effects.interactive
|
|
94
|
+
effects.crossing
|
|
95
|
+
effects.firework
|
|
96
|
+
effects.reactive
|
|
97
|
+
effects.equalizer
|
|
98
|
+
```
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "kd104a"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "KD104A keyboard lighting control over HID, with a small, straightforward API for building and applying effects."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Ilya Miller" }
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"hidapi>=0.14.0",
|
|
17
|
+
"webcolors>=1.13"
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
keywords = ["hid", "keyboard", "rgb", "lighting", "kd104a", "dark", "dark"]
|
|
21
|
+
classifiers = [
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Operating System :: OS Independent",
|
|
24
|
+
"License :: OSI Approved :: MIT License",
|
|
25
|
+
"Topic :: Software Development :: Libraries",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://github.com/yakcom/kd104a"
|
|
30
|
+
Repository = "https://github.com/yakcom/kd104a"
|
|
31
|
+
|
|
32
|
+
[tool.hatch.build.targets.wheel]
|
|
33
|
+
packages = ["kd104a"]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from . import effects
|
|
2
|
+
from .enums import Direction
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Lighting:
|
|
6
|
+
def __init__(self, device):
|
|
7
|
+
self.device = device
|
|
8
|
+
|
|
9
|
+
self._brightness = 50
|
|
10
|
+
self._prev_brightness = 50
|
|
11
|
+
self._speed = 50
|
|
12
|
+
self._direction = Direction.LEFT_RIGHT
|
|
13
|
+
|
|
14
|
+
self._mode = effects.static
|
|
15
|
+
self._kwargs = {"color": (0, 0, 0)}
|
|
16
|
+
|
|
17
|
+
def _send(self):
|
|
18
|
+
kwargs = {
|
|
19
|
+
"brightness": self._brightness,
|
|
20
|
+
**self._kwargs,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
varnames = self._mode.__code__.co_varnames
|
|
24
|
+
|
|
25
|
+
if "speed" in varnames:
|
|
26
|
+
kwargs["speed"] = self._speed
|
|
27
|
+
|
|
28
|
+
if "direction" in varnames:
|
|
29
|
+
kwargs["direction"] = self._direction
|
|
30
|
+
|
|
31
|
+
self.device.send(self._mode(**kwargs))
|
|
32
|
+
|
|
33
|
+
def on(self):
|
|
34
|
+
self._brightness = self._prev_brightness
|
|
35
|
+
self._send()
|
|
36
|
+
|
|
37
|
+
def off(self):
|
|
38
|
+
self._prev_brightness = self._brightness
|
|
39
|
+
self._brightness = 0
|
|
40
|
+
self._send()
|
|
41
|
+
|
|
42
|
+
def set_mode(
|
|
43
|
+
self,
|
|
44
|
+
mode,
|
|
45
|
+
*,
|
|
46
|
+
brightness=None,
|
|
47
|
+
speed=None,
|
|
48
|
+
direction=None,
|
|
49
|
+
**kwargs,
|
|
50
|
+
):
|
|
51
|
+
self._mode = mode
|
|
52
|
+
|
|
53
|
+
if brightness is not None:
|
|
54
|
+
self._brightness = brightness
|
|
55
|
+
if speed is not None:
|
|
56
|
+
self._speed = speed
|
|
57
|
+
if direction is not None:
|
|
58
|
+
self._direction = direction
|
|
59
|
+
|
|
60
|
+
self._kwargs = kwargs
|
|
61
|
+
self._send()
|
|
62
|
+
|
|
63
|
+
def set_brightness(self, value: int):
|
|
64
|
+
self._brightness = value
|
|
65
|
+
self._send()
|
|
66
|
+
|
|
67
|
+
def set_speed(self, value: int):
|
|
68
|
+
self._speed = value
|
|
69
|
+
self._send()
|
|
70
|
+
|
|
71
|
+
def set_direction(self, value: Direction):
|
|
72
|
+
self._direction = value
|
|
73
|
+
self._send()
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import hid
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Device:
|
|
5
|
+
def __init__(
|
|
6
|
+
self,
|
|
7
|
+
product: str | None = None,
|
|
8
|
+
manufacturer: str | None = None,
|
|
9
|
+
vid: int | None = None,
|
|
10
|
+
pid: int | None = None,
|
|
11
|
+
interface: int | None = None,
|
|
12
|
+
usage_page: int | None = None,
|
|
13
|
+
):
|
|
14
|
+
dev = next(
|
|
15
|
+
(
|
|
16
|
+
d
|
|
17
|
+
for d in hid.enumerate()
|
|
18
|
+
if (product is None or (d["product_string"] or "") == product)
|
|
19
|
+
and (manufacturer is None or (d["manufacturer_string"] or "") == manufacturer)
|
|
20
|
+
and (vid is None or d["vendor_id"] == vid)
|
|
21
|
+
and (pid is None or d["product_id"] == pid)
|
|
22
|
+
and (interface is None or d["interface_number"] == interface)
|
|
23
|
+
and (usage_page is None or d["usage_page"] == usage_page)
|
|
24
|
+
),
|
|
25
|
+
None,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
if dev is None:
|
|
29
|
+
raise RuntimeError("Device not found")
|
|
30
|
+
|
|
31
|
+
self.path = dev["path"]
|
|
32
|
+
|
|
33
|
+
def send(self, packet: bytes):
|
|
34
|
+
device = hid.device()
|
|
35
|
+
try:
|
|
36
|
+
device.open_path(self.path)
|
|
37
|
+
written = device.write(packet)
|
|
38
|
+
if written != len(packet):
|
|
39
|
+
raise RuntimeError(f"Failed to write full packet: {written}/{len(packet)} bytes")
|
|
40
|
+
finally:
|
|
41
|
+
device.close()
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
from .packet import build_packet
|
|
2
|
+
from .enums import Mode, Direction
|
|
3
|
+
import webcolors
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _color(value):
|
|
7
|
+
if isinstance(value, tuple):
|
|
8
|
+
if len(value) != 3:
|
|
9
|
+
raise ValueError("Color tuple must have 3 elements")
|
|
10
|
+
r, g, b = value
|
|
11
|
+
return int(r), int(g), int(b)
|
|
12
|
+
|
|
13
|
+
if isinstance(value, str):
|
|
14
|
+
if value.startswith("#"):
|
|
15
|
+
rgb = webcolors.hex_to_rgb(value)
|
|
16
|
+
else:
|
|
17
|
+
rgb = webcolors.name_to_rgb(value)
|
|
18
|
+
return rgb.red, rgb.green, rgb.blue
|
|
19
|
+
|
|
20
|
+
raise TypeError("Invalid color")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _level(value: int):
|
|
24
|
+
value = max(0, min(100, int(value)))
|
|
25
|
+
if value == 0:
|
|
26
|
+
return 0
|
|
27
|
+
return max(1, int(value * 4 / 100))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def static(brightness=50, color=(0, 0, 0)):
|
|
31
|
+
return build_packet(
|
|
32
|
+
mode=Mode.STATIC,
|
|
33
|
+
brightness=_level(brightness),
|
|
34
|
+
color1=_color(color),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def neon(brightness=50, speed=50, color1=(0, 0, 0), color2=(0, 0, 0), palette=False):
|
|
39
|
+
return build_packet(
|
|
40
|
+
mode=Mode.NEON,
|
|
41
|
+
brightness=_level(brightness),
|
|
42
|
+
speed=_level(speed),
|
|
43
|
+
color1=_color(color1),
|
|
44
|
+
color2=_color(color2),
|
|
45
|
+
palette=int(palette),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def breathing(brightness=50, speed=50, color1=(0, 0, 0), color2=(0, 0, 0), palette=False):
|
|
50
|
+
return build_packet(
|
|
51
|
+
mode=Mode.BREATHING,
|
|
52
|
+
brightness=_level(brightness),
|
|
53
|
+
speed=_level(speed),
|
|
54
|
+
color1=_color(color1),
|
|
55
|
+
color2=_color(color2),
|
|
56
|
+
palette=int(palette),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def wave(
|
|
61
|
+
brightness=50,
|
|
62
|
+
speed=50,
|
|
63
|
+
color1=(0, 0, 0),
|
|
64
|
+
color2=(0, 0, 0),
|
|
65
|
+
palette=False,
|
|
66
|
+
direction=Direction.LEFT_RIGHT,
|
|
67
|
+
):
|
|
68
|
+
return build_packet(
|
|
69
|
+
mode=Mode.WAVE,
|
|
70
|
+
brightness=_level(brightness),
|
|
71
|
+
speed=_level(speed),
|
|
72
|
+
color1=_color(color1),
|
|
73
|
+
color2=_color(color2),
|
|
74
|
+
direction=direction,
|
|
75
|
+
palette=int(palette),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def blink(brightness=50, speed=50, color1=(0, 0, 0), color2=(0, 0, 0), palette=False):
|
|
80
|
+
return build_packet(
|
|
81
|
+
mode=Mode.BLINK,
|
|
82
|
+
brightness=_level(brightness),
|
|
83
|
+
speed=_level(speed),
|
|
84
|
+
color1=_color(color1),
|
|
85
|
+
color2=_color(color2),
|
|
86
|
+
palette=int(palette),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def radar(
|
|
91
|
+
brightness=50,
|
|
92
|
+
speed=50,
|
|
93
|
+
color1=(0, 0, 0),
|
|
94
|
+
color2=(0, 0, 0),
|
|
95
|
+
palette=False,
|
|
96
|
+
direction=Direction.CLOCKWISE,
|
|
97
|
+
):
|
|
98
|
+
return build_packet(
|
|
99
|
+
mode=Mode.RADAR,
|
|
100
|
+
brightness=_level(brightness),
|
|
101
|
+
speed=_level(speed),
|
|
102
|
+
color1=_color(color1),
|
|
103
|
+
color2=_color(color2),
|
|
104
|
+
direction=direction,
|
|
105
|
+
palette=int(palette),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def ripple(
|
|
110
|
+
brightness=50,
|
|
111
|
+
speed=50,
|
|
112
|
+
color1=(0, 0, 0),
|
|
113
|
+
color2=(0, 0, 0),
|
|
114
|
+
palette=False,
|
|
115
|
+
direction=Direction.CENTER_OUT,
|
|
116
|
+
):
|
|
117
|
+
return build_packet(
|
|
118
|
+
mode=Mode.RIPPLE,
|
|
119
|
+
brightness=_level(brightness),
|
|
120
|
+
speed=_level(speed),
|
|
121
|
+
color1=_color(color1),
|
|
122
|
+
color2=_color(color2),
|
|
123
|
+
direction=direction,
|
|
124
|
+
palette=int(palette),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def marquee(
|
|
129
|
+
brightness=50,
|
|
130
|
+
speed=50,
|
|
131
|
+
color1=(0, 0, 0),
|
|
132
|
+
color2=(0, 0, 0),
|
|
133
|
+
palette=False,
|
|
134
|
+
direction=Direction.LEFT_RIGHT,
|
|
135
|
+
):
|
|
136
|
+
return build_packet(
|
|
137
|
+
mode=Mode.MARQUEE,
|
|
138
|
+
brightness=_level(brightness),
|
|
139
|
+
speed=_level(speed),
|
|
140
|
+
color1=_color(color1),
|
|
141
|
+
color2=_color(color2),
|
|
142
|
+
direction=direction,
|
|
143
|
+
palette=int(palette),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def shine(brightness=50, speed=50, color1=(0, 0, 0), color2=(0, 0, 0), palette=False):
|
|
148
|
+
return build_packet(
|
|
149
|
+
mode=Mode.SHINE,
|
|
150
|
+
brightness=_level(brightness),
|
|
151
|
+
speed=_level(speed),
|
|
152
|
+
color1=_color(color1),
|
|
153
|
+
color2=_color(color2),
|
|
154
|
+
palette=int(palette),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def ripple2(
|
|
159
|
+
brightness=50,
|
|
160
|
+
speed=50,
|
|
161
|
+
color1=(0, 0, 0),
|
|
162
|
+
color2=(0, 0, 0),
|
|
163
|
+
palette=False,
|
|
164
|
+
direction=Direction.CENTER_OUT,
|
|
165
|
+
):
|
|
166
|
+
return build_packet(
|
|
167
|
+
mode=Mode.RIPPLE2,
|
|
168
|
+
brightness=_level(brightness),
|
|
169
|
+
speed=_level(speed),
|
|
170
|
+
color1=_color(color1),
|
|
171
|
+
color2=_color(color2),
|
|
172
|
+
direction=direction,
|
|
173
|
+
palette=int(palette),
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def interactive(brightness=50, speed=50, color1=(0, 0, 0), color2=(0, 0, 0), palette=False):
|
|
178
|
+
return build_packet(
|
|
179
|
+
mode=Mode.INTERACTIVE,
|
|
180
|
+
brightness=_level(brightness),
|
|
181
|
+
speed=_level(speed),
|
|
182
|
+
color1=_color(color1),
|
|
183
|
+
color2=_color(color2),
|
|
184
|
+
palette=int(palette),
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def crossing(brightness=50, speed=50, color1=(0, 0, 0), color2=(0, 0, 0), palette=False):
|
|
189
|
+
return build_packet(
|
|
190
|
+
mode=Mode.CROSSING,
|
|
191
|
+
brightness=_level(brightness),
|
|
192
|
+
speed=_level(speed),
|
|
193
|
+
color1=_color(color1),
|
|
194
|
+
color2=_color(color2),
|
|
195
|
+
palette=int(palette),
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def firework(brightness=50, speed=50, color1=(0, 0, 0), color2=(0, 0, 0), palette=False):
|
|
200
|
+
return build_packet(
|
|
201
|
+
mode=Mode.FIREWORK,
|
|
202
|
+
brightness=_level(brightness),
|
|
203
|
+
speed=_level(speed),
|
|
204
|
+
color1=_color(color1),
|
|
205
|
+
color2=_color(color2),
|
|
206
|
+
palette=int(palette),
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def reactive(brightness=50, speed=50, color1=(0, 0, 0), color2=(0, 0, 0), palette=False):
|
|
211
|
+
return build_packet(
|
|
212
|
+
mode=Mode.REACTIVE,
|
|
213
|
+
brightness=_level(brightness),
|
|
214
|
+
speed=_level(speed),
|
|
215
|
+
color1=_color(color1),
|
|
216
|
+
color2=_color(color2),
|
|
217
|
+
palette=int(palette),
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def equalizer(brightness=50, speed=50, color1=(0, 0, 0), color2=(0, 0, 0), palette=False):
|
|
222
|
+
return build_packet(
|
|
223
|
+
mode=Mode.EQUALIZER,
|
|
224
|
+
brightness=_level(brightness),
|
|
225
|
+
speed=_level(speed),
|
|
226
|
+
color1=_color(color1),
|
|
227
|
+
color2=_color(color2),
|
|
228
|
+
palette=int(palette),
|
|
229
|
+
)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from enum import IntEnum
|
|
2
|
+
|
|
3
|
+
class Mode(IntEnum):
|
|
4
|
+
STATIC = 0x00
|
|
5
|
+
NEON = 0x03
|
|
6
|
+
BREATHING = 0x01
|
|
7
|
+
WAVE = 0x02
|
|
8
|
+
BLINK = 0x04
|
|
9
|
+
RADAR = 0x05
|
|
10
|
+
RIPPLE = 0x06
|
|
11
|
+
MARQUEE = 0x07
|
|
12
|
+
SHINE = 0x08
|
|
13
|
+
RIPPLE2 = 0x09
|
|
14
|
+
INTERACTIVE = 0x0A
|
|
15
|
+
CROSSING = 0x0B
|
|
16
|
+
FIREWORK = 0x0C
|
|
17
|
+
REACTIVE = 0x0D
|
|
18
|
+
EQUALIZER = 0x0E
|
|
19
|
+
|
|
20
|
+
class Direction(IntEnum):
|
|
21
|
+
RIGHT_LEFT = 0
|
|
22
|
+
LEFT_RIGHT = 1
|
|
23
|
+
BOTTOM_TOP = 2
|
|
24
|
+
TOP_BOTTOM = 3
|
|
25
|
+
CENTER_OUT = 4
|
|
26
|
+
OUT_CENTER = 5
|
|
27
|
+
CLOCKWISE = 6
|
|
28
|
+
COUNTER_CLOCKWISE = 7
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
HEADER = (0x01, 0x07, 0x00, 0x00, 0x00, 0x0E)
|
|
2
|
+
PACKET_SIZE = 64
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def build_packet(
|
|
6
|
+
mode: int,
|
|
7
|
+
brightness: int = 0,
|
|
8
|
+
speed: int = 0,
|
|
9
|
+
color1=(0, 0, 0),
|
|
10
|
+
color2=(0, 0, 0),
|
|
11
|
+
direction: int = 0,
|
|
12
|
+
palette: int = 0,
|
|
13
|
+
) -> bytes:
|
|
14
|
+
buf = [0] * PACKET_SIZE
|
|
15
|
+
buf[0:6] = HEADER
|
|
16
|
+
buf[6] = int(mode)
|
|
17
|
+
buf[7] = brightness
|
|
18
|
+
buf[8] = speed
|
|
19
|
+
buf[9:12] = color1
|
|
20
|
+
buf[12:15] = color2
|
|
21
|
+
buf[15] = direction
|
|
22
|
+
buf[16] = palette
|
|
23
|
+
return bytes(buf)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from kd104a import Device, Lighting, Direction, effects
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
keyboard = Device(product="Gaming Keyboard", interface=2)
|
|
5
|
+
lighting = Lighting(keyboard)
|
|
6
|
+
|
|
7
|
+
lighting.set_mode(
|
|
8
|
+
effects.wave,
|
|
9
|
+
brightness=1,
|
|
10
|
+
speed=20,
|
|
11
|
+
direction=Direction.RIGHT_LEFT,
|
|
12
|
+
color1="red",
|
|
13
|
+
color2="blue",
|
|
14
|
+
)
|