├── .github
├── FUNDING.yml
└── dependabot.yml
├── .gitignore
├── GUIprototype.png
├── LICENSE
├── README.md
├── device
└── __init__.py
├── gui
├── images
│ ├── ShuttleXpress_Black.png
│ ├── delete-sweep_24.png
│ ├── icon.png
│ ├── usb_black_24.png
│ └── usb_white_24.png
├── main.py
├── mainwindow.ui
└── mainwindow_ui.py
├── plugins
└── __init__.py
└── requirements.txt
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: rdoursenaud # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
14 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "pip" # See documentation for possible values
9 | directory: "/" # Location of package manifests
10 | schedule:
11 | interval: "monthly"
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
--------------------------------------------------------------------------------
/GUIprototype.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EMATech/OpenContourShuttle/9b9601eb555870676ba8b055a71e7d10daa6f277/GUIprototype.png
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Open Contour Shuttle
2 | ====================
3 |
4 | A multiplatform userland driver, configuration editor, event manager & generator for Contour ShuttleXpress & ShuttlePRO v2.
5 |
6 | 
7 |
8 |
9 | Status
10 | ------
11 |
12 | Proof of concept!
13 |
14 | Welcoming any contributions, especially:
15 |
16 | - comments
17 | - tests: particularly on platforms other than Microsoft Windows or versions earlier than 11
18 | - implement currently unsupported hardware:
19 | - ShuttlePRO v2
20 |
21 | Features / TODO list
22 | --------------------
23 |
24 | - [ ] Device support
25 | - [x] ShuttleXpress
26 | - [ ] **(WIP)** ShuttlePRO v2
27 |
28 | - [ ] Platform support
29 | - [x] Microsoft Windows
30 | - [x] GUI
31 | - [x] Tray icon shortcut to GUI
32 | - [ ] Run as a [windows service](http://thepythoncorner.com/dev/how-to-create-a-windows-service-in-python/)?
33 | - [ ] Add configurator to control panel?
34 | - [x] Apple Mac OS X
35 | - [ ] GNU/Linux
36 | - [ ] Android?
37 | - [ ] Apple iOS/iPadOS?
38 |
39 | - [x] State management engine
40 | - [x] Find and open device
41 | - [x] USB HID via hidapi
42 | - [x] Decode raw values
43 | - [x] Observer pattern Events
44 | - [ ] **(WIP)** Central broker
45 | - [ ] Responders plugin system
46 |
47 |
48 | - [ ] **(WIP)** GUI
49 | - [x] Qt6 via PySide6
50 | - [X] Main window
51 | - [x] Icon
52 | - [x] Title
53 | - [x] System tray icon
54 | - [x] Display state graphically
55 | - [x] Status
56 | - [x] Connection
57 | - [x] Events
58 | - [x] ~~Menu?~~
59 | - [x] About window
60 | - [x] Log window
61 | - [x] Aggregate & display logs
62 | - [x] Clear logs
63 | - [ ] Write logs to file
64 | - [ ] Configuration UI
65 | - [ ] Generate configurations
66 | - [ ] Load existing configurations
67 | - [ ] Write/Store configurations
68 | - [ ] Emulation mode when no hardware is connected
69 | - [ ] Separate from the engine
70 | - [ ] Custom graphical widgets?
71 | - [ ] Wheel (Rotating image with color tick)
72 | - [ ] Dial (Rotating image)
73 | - [ ] Button (Depict depressed state)
74 |
75 | ### Events
76 |
77 | State changes.
78 |
79 | - [x] USB:
80 | - [x] connected
81 | - [x] disconnected
82 | - [x] Buttons 1 to 5:
83 | - [x] press
84 | - [x] release
85 | - [x] Wheel:
86 | - [x] centered (position 0)
87 | - [x] position change (-7 to 7)
88 | - [x] direction: up
89 | - [x] direction: down
90 | - [x] Dial:
91 | - [x] direction: up
92 | - [x] direction: down
93 | - [x] absolute position (0 to 255)
94 |
95 | ### Observers
96 |
97 | Gets notified upon events
98 |
99 | - [x] GUI
100 | - [ ] **(WIP)** Responder plugins Broker
101 |
102 | ### Broker
103 |
104 | Maps events observation to responses depending on the current configuration and available responder plugins.
105 |
106 | - [ ] Configuration system
107 | - [ ] Formats support
108 | - [ ] Custom?
109 | - [ ] Comments
110 | - [ ] [Official driver configurations](https://contourdesign.fr/support/windows-shuttle-settings/)
111 | - [ ] Parse
112 | - [ ] Generate
113 | - [ ] macros (generate multiple events)
114 |
115 | - [ ] Responder plugins
116 | - [ ] API specification
117 | - [ ] Sample implementation
118 |
119 | - [ ] runtime profiles support
120 | - [ ] configure multiple profiles
121 | - [ ] default global
122 | - [ ] named
123 | - [ ] (optional) linked to one or more specific external states:
124 | - [ ] application in focus
125 | - [ ] current desktop/monitor?
126 | - [ ] session/user?
127 | - [ ] external trigger event?
128 | - [ ] (auto?) switch profiles dynamically
129 | - [ ] using buttons
130 | - [ ] detect running application
131 | - [ ] Microsoft Windows
132 | - [ ] win32ui
133 | - [ ] Mac OS X
134 | - [ ] GNU/Linux
135 |
136 | ### Responders
137 |
138 | Plugin based system.
139 |
140 | Generates external events triggered by the broker.
141 |
142 | - [ ] **(POC)** generate keyboard strokes and/or modifiers
143 | - [x] ~~SendKeys?~~
144 | - [x] ~~pywin32 shell.SendKeys?~~
145 | - [ ] **(POC)** pynput! (Also support mouse)
146 | - [ ] Frequency (Once/Hold…)
147 |
148 | - [ ] **(POC)** generate mouse events
149 | - [ ] **(POC)** pynput! (Also support mouse)
150 | - [ ] click
151 | - [ ] wheel
152 |
153 | - [ ] generate MIDI
154 | - [ ] Arbitrary (User defined)
155 | - [ ] MCU compatible
156 | - [ ] HUI compatible
157 |
158 | - [ ] generate OSC
159 |
160 | - [ ] call APIs?
161 |
162 | - [ ] launch applications
163 | - ?
164 |
165 | Contour format settings
166 | -----------------------
167 |
168 | May be used as samples for interoperability.
169 |
170 | ### Contour
171 |
172 | - [Microsoft Windows](https://www.contourdesign.com/windows-shuttle-settings/)
173 | - [Mac OS X](https://www.contourdesign.com/mac-shuttle-settings/)
174 |
175 | ### GitHub
176 |
177 | - [ShuttleProSettings](https://github.com/Rolias/ShuttleProSettings)
178 | - Camtasia Studio 8 *Microsoft Windows*
179 | - Reaper *Apple Mac OS X*
180 | - [camtasia-shuttlepro-settings](https://github.com/digitaldrummerj/camtasia-shuttlepro-settings/blob/master/Camtasia%20Studio%209.pref)
181 | - Camtasia Studio 9 *Microsoft Windows*
182 |
183 | Similar and/or related projects
184 | ---------------------------
185 |
186 |
187 |
199 |
200 | ### [Contour-ShuttlePRO-V1-Linux-Custom-Implementation](https://github.com/c0deous/Contour-ShuttlePRO-V1-Linux-Custom-Implementation)
201 |
202 | A simple barebones driver written in Python for the Contour ShuttlePRO V1 Control Surface.
203 |
204 | - Language: Python
205 | - License: Unspecified/Proprietary (Copyright 2016 c0deous?)
206 | - OS: GNU/Linux
207 | - Dependencies: python-evdev
208 |
209 | ### [cncjs-pendant-shuttle](https://github.com/bensuffolk/cncjs-pendant-shuttle)
210 |
211 | A cncjs pendant to connect a Contour Design ShuttleXpress to the raspberry pi that is running cncjs.
212 |
213 | - Language: Javascript/NodeJS
214 | - License: MIT (Copyright (c) 2021 Ben Suffolk)
215 | - OS: GNU/Linux
216 | - Dependencies: udev, cnjs, shuttle-control-usb
217 |
218 | ### [kh750remote](https://github.com/floxch/kh750remote)
219 |
220 | Neumann KH 750 DSP remote control with Contour ShuttleXpress.
221 |
222 | - Language: Javascript/NodeJS
223 | - License: MIT (Copyright (c) 2021 floxch)
224 | - OS: Multiplatform?
225 | - Dependencies: multicast-dns, node-hid, node-notifier, node-osc
226 |
227 | ### [LightroomShuttlePro](https://github.com/abrilevskiy/LightroomShuttlePro)
228 |
229 | Lightroom plugin for support contourdesign ShuttlePro v2.
230 |
231 | - Language: C++/Lua
232 | - License: GPL-3.0 (Copyright 2018 abrilevskiy?) + Proprietary (Copyright (c) 2003 Contour Design, Inc. & Copyright
233 | 2016 Adobe Systems Incorporated)
234 | - OS: Microsoft Windows
235 | - Dependencies: ShuttleSDK.dll
236 |
237 | ### [node-red-contrib-shuttlexpress](https://github.com/legacymachine/node-red-contrib-shuttlexpress)
238 |
239 | Node-RED nodes for USB ShuttleXpress device.
240 |
241 | - Language: Javascript/NodeJS
242 | - License: MIT (Copyright (c) 2019 legacymachine aka Legacy Machine Works, LLC aka Josh Dudley)
243 | - OS: Multiplatform
244 | - Dependencies: node-hid
245 |
246 | ### [node-shuttlexpress](https://github.com/legacymachine/node-shuttlexpress)
247 |
248 | NodeJS API for USB ShuttleXpress Device.
249 |
250 | - Language: Javascript/NodeJS
251 | - License: MIT (Copyright (c) 2019 legacymachine aka Legacy Machine Works, LLC aka Josh Dudley)
252 | - OS: Multiplatform
253 | - Dependencies: node-hid
254 |
255 | ### [shuttle](https://github.com/jeamland/shuttle)
256 |
257 | A little Swift program to make a ShuttleXpress do what I want.
258 |
259 | - Language: Swift
260 | - License: BSD-2-Clause (Copyright (c) 2016, Benno Rice)
261 | - OS: Apple Mac OS X
262 | - Dependencies: Mac OS X 10.12 SDK
263 |
264 | ### [Shuttle](https://github.com/1div0/Shuttle)
265 |
266 | Controlling Ardour with Open Sound Control from Contour Design ShuttlePRO & ShuttlePRO v2.
267 |
268 | - Language: C
269 | - License: GPL-2.0 (Copyright (C) 2001-2007 Dan Dennedy)
270 | - OS: GNU/Linux
271 | - Dependencies: liblo
272 |
273 | ### [shuttle-go](https://github.com/abourget/shuttle-go)
274 |
275 | Contour Design Shuttle Pro V2 drivers for Linux, in Go, with modifiers and just more slick.
276 |
277 | - Language: Go
278 | - License: MIT (Copyright (c) 2017 Alexandre Bourget)
279 | - OS: GNU/Linux
280 | - Dependencies: udev
281 |
282 | ### [ShuttleControlUSB](https://github.com/hopejr/ShuttleControlUSB)
283 |
284 | A Library to use Contour Design ShuttleXpress and ShuttlePro v2 in Node.js projects without the driver.
285 |
286 | - Language: Javascript/NodeJS
287 | - License: MIT (Copyright (c) 2020 James Hope)
288 | - OS: Multiplatform?
289 | - Dependencies: node-hid, usb-detection, udev
290 |
291 | ### [shuttled](https://github.com/Shamanon/shuttled)
292 |
293 | ShuttlePro fork.
294 |
295 | Daemon for Contour Shuttle devices to translate button/jogwheel events to keystrokes sent to uinput.
296 |
297 | - Language: C
298 | - License: GPL-3.0 (Copyright 2013 Eric Messick, Copyleft 2015 Joshua Besneatte)
299 | - OS: GNU/Linux
300 | - Dependencies: libx11, libxtst, udev, uinput
301 |
302 | ### [shuttleevent](https://github.com/pmjdebruijn/shuttleevent)
303 |
304 | ShuttlePRO fork.
305 |
306 | User program for interpreting key, shuttle, and jog events from a Contour Design ShuttlePRO v2
307 |
308 | - Language: C
309 | - License: GPL-3.0 (Copyright 2013 Eric Messick, Copyright 2018 Albert Graef, Copyright 2019 Pascal de Bruijn)
310 | - OS: GNU/Linux
311 | - Dependencies: libx11, libxtst, udev
312 |
313 | ### [ShuttleGRBL](https://github.com/Duffmann/ShuttleGRBL)
314 |
315 | Contour's ShuttleExpress for jogging a GRBL CNC machine under cncjs and Linux using USB/hidraw.
316 |
317 | - Language: Javascript/NodeJS
318 | - License: MIT (Copyright (c) 2020 Duffmann aka Nelio Santos)
319 | - OS: GNU/Linux
320 | - Dependencies: commander, inquirer, jsonwebtoken, lodash.get, node-hid, serialport, socket.io-client, udev, vorpal
321 |
322 | ### [shuttleit](https://github.com/irontoby/shuttleit)
323 |
324 | Event handler for Contour Design ShuttleXpress.
325 |
326 | - Language: Perl
327 | - License: Apache-2.0 (Copyright 2015 Tobias Johnson)
328 | - OS: GNU/Linux
329 | - Dependencies: udev
330 |
331 | ### [shuttlemidi](https://github.com/dg1psi/shuttlemidi)
332 |
333 | Sends MIDI from the Shuttle Contour to SDR Console.
334 |
335 | - Language: Go
336 | - License: Apache-2.0 (Copyright 2021 dg1psi)
337 | - OS: Multiplatform?
338 | - Dependencies: hidapi, loopmidi
339 |
340 | ### [ShuttleNET](https://github.com/EddieMac74/ShuttleNET)
341 |
342 | A simple .NET wrapper for the Contour Shuttle SDK. Written in C#.
343 |
344 | - Language: C#
345 | - License: Unspecified/Proprietary (Copyright © Edward MacDonald 2018)
346 | - OS: Microsoft Windows?
347 | - Dependencies: ShuttleSDK.dll
348 |
349 | ### [shuttlepro](https://github.com/russells-crockpot/shuttlepro)
350 |
351 | A python app to allow usage of the Contour ShuttlePro V2 in Linux.
352 |
353 | - Language: Python
354 | - License: Unspecified/Proprietary (Copyright 2020 Brendan McGloin?)
355 | - OS: GNU/Linux
356 | - Dependencies: click, evdev, Xlib
357 |
358 | ### [ShuttlePRO](https://github.com/nanosyzygy/ShuttlePRO)
359 |
360 | User program for interpreting key, shuttle, and jog events from a Contour Design ShuttlePRO v2.
361 |
362 | - Language: C
363 | - License: GPL-3.0 (Copyright 2013 Eric Messick)
364 | - OS: GNU/Linux
365 | - Dependencies: libx11, libxtst, udev
366 |
367 | ### [ShuttlePRO](https://github.com/SERVCUBED/ShuttlePRO)
368 |
369 | ShuttlePRO fork.
370 |
371 | User program for interpreting key, shuttle, and jog events from a Contour Design ShuttlePRO v2
372 |
373 | - Language: C
374 | - License: GPL-3.0 (Copyright 2013 Eric Messick, Copyright 2018 Albert Graef, Copyright 2020 Ben Blain)
375 | - OS: GNU/Linux
376 | - Dependencies: libjack, libx11, libxtst, udev
377 |
378 | ### [ShuttlePro-M](https://github.com/w5pny/ShuttlePRO-M)
379 |
380 | ShuttlePro fork.
381 |
382 | User program for interpreting key, shuttle, and jog events from a Contour Design ShuttlePRO v2.
383 |
384 | - Language: C
385 | - License: GPL-3.0 (Copyright 2013 Eric Messick, Copyright 2020 Harry G McGavran Jr)
386 | - OS: GNU/Linux
387 | - Dependencies: udev
388 |
389 | ### [shuttleprov2_linux_driver](https://github.com/rmt/shuttleprov2_linux_driver)
390 |
391 | A Linux userspace "driver" for the ShuttlePro V2 device, and some tools to read events from a wacom tablet.
392 |
393 | - Language: D
394 | - License: LGPL-3.0 (Copyright 2013 Robert Thomson)
395 | - OS: GNU/Linux
396 | - Dependencies: None
397 |
398 | ### [shuttleroom](https://github.com/7Pass/shuttleroom)
399 |
400 | Lightroom plugin to use ShuttleXpress controller for Develop.
401 |
402 | - Language: C#
403 | - License: Unspecified/Proprietary (Copyright 2020 Ngoc Van Tran?)
404 | - OS: Microsoft Windows, Ubuntu?
405 | - Dependencies: .NET Core 3.1.100, ShuttleSDK64.dll
406 |
407 | ### [shuttle-xpress-android](https://github.com/freshollie/shuttle-xpress-android)
408 |
409 | Android Library for interfacing with a Contour Design Shuttle Xpress 🕹️.
410 |
411 | - Language: Java
412 | - License: Apache-2.0 (Copyright 2017 Oliver Bell)
413 | - OS: Android
414 | - Dependencies: API 17
415 |
416 | ### [ShuttleXpress](https://github.com/threedaymonk/shuttlexpress)
417 |
418 | Use a Contour ShuttleXpress in Linux.
419 |
420 | - Language: Scheme
421 | - License: Unspecified/Proprietary (Copyright 2012 Paul Battley?)
422 | - OS: GNU/Linux
423 | - Dependencies: Xlib, udev
424 |
425 | ### [Shuttlexpress-MPD](https://github.com/matthew-wolf-n4mtt/shuttlexpress-mpd)
426 |
427 | Shuttlexpress-MPD is a Music Player Daemon (MPD) client that uses a Contour ShuttleXpress to control the status of the
428 | playback of an MPD instance.
429 |
430 | - Language: C
431 | - License: GPL-2.0 (Copyright 2019 Matthew J. Wolf)
432 | - OS: GNU/Linux
433 | - Dependencies: mpd, udev, systemd
434 |
435 | ### [widget-shuttlexpress](https://github.com/chilipeppr/widget-shuttlexpress)
436 |
437 | The ShuttleXpress widget helps you setup your own ShuttleXpress jog dial USB device and enables audio feedback as you
438 | toggle the buttons on the device.
439 |
440 | - Language: Javascript/HTML/CSS
441 | - License: Unspecified/Proprietary (Copyright 2016 John Lauer?)
442 | - OS: ChiliPeppr
443 | - Dependencies: ChiliPeppr
444 |
445 | Development log
446 | ---------------
447 |
448 | ### Finding the right library for Microsoft Windows
449 |
450 | #### Attempt 1
451 |
452 | Using [pyUSB](https://pypi.org/project/pyusb/)
453 |
454 | Can’t access device without a kernel driver.
455 |
456 | Kernel driver has been generated using [libusb-win32](https://sourceforge.net/projects/libusb-win32/), but you need to
457 | disable driver signature enforcement before installing.
458 |
459 | The easier way is to use [Zadig](https://zadig.akeo.ie/).
460 |
461 | #### Attempt 2
462 |
463 | Using [pyWinUSB](https://pypi.org/project/pywinusb/)
464 |
465 | Can get HID without driver installation on Microsoft Windows!
466 |
467 | Let’s continue…
468 |
469 | Success!
470 |
471 | But...
472 |
473 | #### Attempt 3
474 |
475 | Using [cython-hidapi](https://pypi.org/project/hidapi/)
476 |
477 | Crossplatform (Microsoft Windows, Mac OS X, GNU/Linux)
478 |
479 | Success on Microsoft Windows without prior driver installation!
480 | Hope it’s the same on other platforms.
481 |
482 | ### Building a GUI
483 |
484 | #### PySide6 (QT6)
485 |
486 | Prototype UI done
487 |
488 | ### Exploring patterns
489 |
490 | Settled for an observer pattern with a central broker and separate plugin based responders.
491 |
492 |
493 | Legal notice
494 | ------------
495 |
496 | ### License
497 |
498 | Copyright 2021 Raphaël Doursenaud
499 |
500 | This software is released under the terms of the GNU General Public License, version 3.0 or later (GPL-3.0-or-later).
501 |
502 | See [LICENSE](LICENSE).
503 |
504 | ### Dependencies & License Acknowledgment
505 |
506 | - [Python](https://python.org) v3.10
507 | Used under the terms of the PSF License Agreement.
508 | - libusb [hidapi](https://github.com/libusb/hidapi)
509 | Copyright Alan Ott, Signal 11 Software.
510 | Used under the terms of the GNU General Public License, version 3.0 (GPL-3.0).
511 | - via Trezor [cython-hidapi](https://github.com/trezor/cython-hidapi)
512 | Copyright Pavol Rusnak, SatoshiLabs.
513 | Used under the terms of the GNU General Public License, version 3.0 (GPL-3.0).
514 | - Qt [PySide6](https://www.pyside.org)
515 | Used under the terms of the GNU Lesser General Public License v3.0 (LGPL-3.0).
516 | - UN-GCPDS [Qt-Material](https://github.com/UN-GCPDS/qt-material)
517 | Used under the BSD-2-Clause License.
518 | - [Material Design Icons](https://materialdesignicons.com)
519 | Used under the Pictogrammers Free License.
520 |
521 | ### Trademarks
522 |
523 | Contour, ShuttleXpress and ShuttlePro are trademarks of Contour Innovations LLC in the United States of America.
524 |
525 | These are not registered or active trademarks in the European Union and France where I reside.
526 |
527 | #### Other
528 |
529 | Other trademarks are property of their respective owners and used fairly for descriptive and nominative purposes only.
530 |
--------------------------------------------------------------------------------
/device/__init__.py:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: 2021 Raphaël Doursenaud
2 | #
3 | # SPDX-License-Identifier: GPL-3.0-or-later
4 | """
5 | `Open Contour Shuttle`
6 | ================================================================================
7 |
8 | A multiplatform userland driver, configuration editor, event management &
9 | generator for Contour ShuttleXpress & ShuttlePro v2.
10 |
11 | Contour, ShuttleXpress and ShuttlePro are trademarks of
12 | Contour Innovations LLC in the United States.
13 |
14 | These are not active trademarks in the European Union and France where I reside.
15 |
16 | * Author(s): Raphaël Doursenaud
17 |
18 | Implementation Notes
19 | --------------------
20 |
21 | Using a Contour ShuttleXpress device.
22 |
23 | HID report
24 | ----------
25 |
26 | HID device (vID=0x0b33, pID=0x0020,v=0x0201); Contour Design; ShuttleXpress, Path: \\?\hid#vid_0b33&pid_0020#a&2c3e917c&0&0000#{4d1e55b2-f16f-11cf-88cb-001111000030
27 |
28 | Path: \\?\hid#vid_0b33&pid_0020#a&2c3e917c&0&0000#{4d1e55b2-f16f-11cf-88cb-001111000030}
29 |
30 | Instance: HID\VID_0B33&PID_0020\A&2C3E917C&0&0000
31 |
32 | Port (ID): 29
33 |
34 | Port (str):USB\VID_0B33&PID_0020\9&10B4E550&0&3
35 |
36 | HID device documentation report
37 | ===============================
38 |
39 | Top Level Details
40 | -----------------
41 |
42 | Manufacturer String: Contour Design
43 | Product Sting: ShuttleXpress
44 | Serial Number:
45 |
46 | Vendor ID: 0x0b33
47 | Product ID: 0x0020
48 | Version number: 0x0201
49 |
50 | Device Path: \\?\hid#vid_0b33&pid_0020#a&2c3e917c&0&0000#{4d1e55b2-f16f-11cf-88cb-001111000030}
51 | Device Instance Id: HID\VID_0B33&PID_0020\A&2C3E917C&0&0000
52 | Parent Instance Id: 29
53 |
54 | Top level usage: Page=0x000c, Usage=0x01
55 | Usage identification: Consumer device, Consumer Control usage
56 | Link collections: 2 collection(shuttle)
57 |
58 | Reports
59 | -------
60 |
61 | Input Report
62 | ~~~~~~~~~~~~
63 | Length: 6 byte(shuttle)
64 | Buttons: 1 button(shuttle)
65 | Values: 2 value(shuttle)
66 |
67 | Output Report
68 | ~~~~~~~~~~~~~
69 | length: 0 byte(shuttle)
70 | Buttons: 0 button(shuttle)
71 | Values: 0 value(shuttle)
72 |
73 | Feature Report
74 | ~~~~~~~~~~~~~
75 | Length: 0 byte(shuttle)
76 | Buttons: 0 button(shuttle)
77 | Values: 0 value(shuttle)
78 |
79 | *** Input Caps ***
80 |
81 | Usage Range 1~13 (0x1~0xd), Page 0x9 (Button)
82 | bit_field: 2
83 | data_index_max: 14
84 | data_index_min: 2
85 | designator_max: 0
86 | designator_min: 0
87 | is_absolute: 1
88 | is_alias: 0
89 | is_button: True
90 | is_designator_range: 0
91 | is_range: 1
92 | is_string_range: 0
93 | is_value: False
94 | link_collection: 1
95 | link_usage: 0 (0x0)
96 | link_usage_page: 12 (0xc)
97 | report_id: 0
98 | string_max: 0
99 | string_min: 0
100 |
101 | Usage 56 (0x38), Page 0x1
102 | (Generic Desktop device, Wheel usage)
103 | bit_field: 6
104 | bit_size: 8
105 | data_index: 0
106 | designator_index: 0
107 | has_null: 0
108 | is_absolute: 0
109 | is_alias: 0
110 | is_button: False
111 | is_designator_range: 0
112 | is_range: 0
113 | is_string_range: 0
114 | is_value: True
115 | link_collection: 1
116 | link_usage: 0 (0x0)
117 | link_usage_page: 12 (0xc)
118 | logical_max: 127
119 | logical_min: -128
120 | physical_max: 0
121 | physical_min: 0
122 | report_count: 1
123 | report_id: 0
124 | string_index: 0
125 | units: 0
126 | units_exp: 0
127 |
128 | Usage 55 (0x37), Page 0x1
129 | (Generic Desktop device, Dial usage)
130 | bit_field: 46
131 | bit_size: 8
132 | data_index: 1
133 | designator_index: 0
134 | has_null: 0
135 | is_absolute: 0
136 | is_alias: 0
137 | is_button: False
138 | is_designator_range: 0
139 | is_range: 0
140 | is_string_range: 0
141 | is_value: True
142 | link_collection: 1
143 | link_usage: 0 (0x0)
144 | link_usage_page: 12 (0xc)
145 | logical_max: 255
146 | logical_min: 0
147 | physical_max: 0
148 | physical_min: 0
149 | report_count: 1
150 | report_id: 0
151 | string_index: 0
152 | units: 0
153 | units_exp: 0
154 |
155 | Raw data_from_hid
156 | -----------------
157 |
158 | Wheel
159 | ~~~~~
160 |
161 | - Encoder: 15 position with center rest and 7 positions on each side
162 | - Position: byte 1 (2nd)
163 | - Length: 1 byte
164 | - Range: 8-bit signed integer (-128 to 127)
165 |
166 | pos_center = [0, 0, xx, 0, 0, 0]
167 | pos_minus_7 = [0, 249, xx, 0, 0, 0]
168 | pos_minus_6 = [0, 250, xx, 0, 0, 0]
169 | pos_minus_5 = [0, 251, xx, 0, 0, 0]
170 | pos_minus_4 = [0, 252, xx, 0, 0, 0]
171 | pos_minus_3 = [0, 253, xx, 0, 0, 0]
172 | pos_minus_2 = [0, 254, xx, 0, 0, 0]
173 | pos_minus_1 = [0, 255, xx, 0, 0, 0]
174 | pos_plus_1 = [0, 1, xx, 0, 0, 0]
175 | pos_plus_2 = [0, 2, xx, 0, 0, 0]
176 | pos_plus_3 = [0, 3, xx, 0, 0, 0]
177 | pos_plus_4 = [0, 4, xx, 0, 0, 0]
178 | pos_plus_5 = [0, 5, xx, 0, 0, 0]
179 | pos_plus_6 = [0, 6, xx, 0, 0, 0]
180 | pos_plus_7 = [0, 7, xx, 0, 0, 0]
181 |
182 | Dial
183 | ~~~~
184 |
185 | - Encoder: 10 ticks per full rotation
186 | - Position: byte 2 (3rd)
187 | - Length: 1 byte
188 | - Range: 8-bit unsigned integer (0-255)
189 |
190 | Counts positive when rotated clockwise and negative the other way around on xx [0-255].
191 | The value wraps around.
192 |
193 | There’s a strange quirk in the hardware that makes it ignore the first tick when changing directions.
194 |
195 | Buttons
196 | ~~~~~~~
197 |
198 | - Position: byte 4-5
199 | - Length: 2 bytes
200 | - Values: bitfield (0 is released, 1 is pressed)
201 |
202 | B3
203 | B2 B4
204 | B1 B5
205 |
206 | B1_on_press = [0, 0, xx, 0, 16, 0]
207 | B2_on_press = [0, 0, xx, 0, 32, 0]
208 | B3_on_press = [0, 0, xx, 0, 64, 0]
209 | B4_on_press = [0, 0, xx, 0, 128, 0]
210 | B5_on_press = [0, 0, xx, 0, 0, 1]
211 |
212 | **Multiple button presses**
213 |
214 | Simply add the bitfield values.
215 |
216 | Unused bytes
217 | ~~~~~~~~~~~~
218 |
219 | Byte 0 and 3 appear unused.
220 |
221 | My guess is the 3rd byte is used on the Pro model.
222 | """
223 |
224 | from __future__ import annotations
225 |
226 | import logging
227 | from abc import ABC, abstractmethod
228 | from typing import Optional, Union
229 |
230 | import hid
231 |
232 | __version__ = "0.0.0-auto.0"
233 | __repo__ = ("https://github.com/EMATech/OpenContourShuttle.git")
234 |
235 | logging.basicConfig(level=logging.DEBUG)
236 |
237 | logger = logging.getLogger()
238 |
239 |
240 | class Wheel:
241 | _pos: int
242 |
243 | centered: bool
244 |
245 | @property
246 | def pos(self) -> int:
247 | if self._pos is not None:
248 | return self._pos
249 |
250 | @pos.setter
251 | def pos(self, pos: int) -> None:
252 | if pos not in range(-7, 8): # Filter only valid positions
253 | raise ValueError(f"Value {pos} was not expected. "
254 | f"Please file a bug report: "
255 | f"https://github.com/EMATech/OpenContourShuttle/issues"
256 | )
257 |
258 | # 0 is central position.
259 | self.centered = False
260 | if pos == 0:
261 | self.centered = True
262 |
263 | self._pos = pos
264 |
265 | def __init__(self) -> None:
266 | # Assumes initial state is centered
267 | self._pos = 0
268 | self.centered = True
269 |
270 |
271 | class Dial:
272 | _pos: Optional[int]
273 |
274 | @property
275 | def pos(self) -> int:
276 | if self._pos is not None:
277 | return self._pos
278 |
279 | @pos.setter
280 | def pos(self, pos: int) -> None:
281 | if not 0 <= pos <= 255: # Filter only valid values
282 | raise ValueError
283 |
284 | self._pos = pos
285 |
286 | def __init__(self) -> None:
287 | self._pos = None
288 |
289 |
290 | class Button:
291 | _num: int
292 |
293 | push: bool = False
294 |
295 | @property
296 | def num(self) -> int:
297 | return self._num
298 |
299 | @num.setter
300 | def num(self, num: int) -> None:
301 | if not 1 <= num <= 5: # Filter only valid values. TODO: change for Pro?
302 | raise ValueError
303 |
304 | def __init__(self, num: int) -> None:
305 | self._num = num
306 | self.push = False
307 |
308 |
309 | class Event(ABC):
310 | element: Union[hid.device, Wheel, Dial, Button]
311 | desc: str # Human-readable event description
312 |
313 | def __init__(self, element: Union[hid.device, Wheel, Dial, Button]) -> None:
314 | self.element = element
315 |
316 |
317 | class ConnectionEvent(Event):
318 | def __init__(self, device: hid.device):
319 | super().__init__(device)
320 | self.desc = f"{device.get_manufacturer_string()} {device.get_product_string()} connected"
321 |
322 |
323 | class DisconnectionEvent(Event):
324 | def __init__(self, device: hid.device):
325 | super().__init__(device)
326 | self.desc = f"{device.get_manufacturer_string()} {device.get_product_string()} disconnected"
327 |
328 |
329 | class RotaryEvent(Event):
330 | element: Union[Wheel, Dial]
331 |
332 | _delta: int # Delta
333 | _direction: bool # True means up
334 |
335 | @property
336 | def direction(self) -> bool:
337 | return self._direction
338 |
339 | @direction.setter
340 | def direction(self, direction: bool) -> None:
341 | self.desc = f"{type(self.element).__name__} up" if direction \
342 | else f"{type(self.element).__name__} down"
343 | self._direction = direction
344 |
345 | @property
346 | def value(self) -> int:
347 | return self._delta
348 |
349 | @value.setter
350 | def value(self, val: int) -> None:
351 | self._delta = val
352 |
353 | def __init__(self, element: Union[Wheel, Dial], value: int, direction: bool) -> None:
354 | super().__init__(element)
355 | self.value = value
356 | self.direction = direction
357 |
358 |
359 | class ButtonEvent(Event):
360 | element: Button
361 |
362 | def __init__(self, element: Button) -> None:
363 | super().__init__(element)
364 | self.desc = f"{type(element).__name__} {element.num} down" if element.push \
365 | else f"{type(element).__name__} {element.num} up"
366 |
367 |
368 | class ShuttleXpressSubject(ABC):
369 | """
370 | The Subject interface declares a set of methods for managing subscribers.
371 | """
372 | events: list[Event]
373 |
374 | @abstractmethod
375 | def attach(self, observer: ShuttleXpressObserver) -> None:
376 | """
377 | Attach an observer to the subject.
378 | """
379 | pass
380 |
381 | @abstractmethod
382 | def detach(self, observer: ShuttleXpressObserver) -> None:
383 | """
384 | Detach an observer from the subject.
385 | """
386 | pass
387 |
388 | @abstractmethod
389 | def notify(self) -> None:
390 | """
391 | Notify all observers about an event.
392 | """
393 | pass
394 |
395 |
396 | class ShuttleXpressObserver(ABC):
397 | """
398 | The Observer interface declares the update method, used by subjects.
399 | """
400 |
401 | @abstractmethod
402 | def update(self, subject: ShuttleXpressSubject) -> None:
403 | """
404 | Receive update from subject.
405 | """
406 | pass
407 |
408 |
409 | class ShuttleXpress(ShuttleXpressSubject):
410 | USB_VID: int = 0x0b33 # Contour Design, Inc.
411 | USB_PID_XPRESS: int = 0x0020 # ShuttleXpress
412 | USB_PID_PROV2: int = 0x0030 # ShuttlePRo v2 TODO: add support?
413 | HID_DATA_SIZE: int = 48
414 |
415 | # USB HID Device
416 | hid_device: hid.device
417 | connected: bool = False # Missing from hid.device class
418 |
419 | # State
420 | _wheel: Wheel
421 | _dial: Dial
422 | _prev_dial_dir: Optional[bool] = None # Needed to implement hardware quirk
423 | # TODO: Replace by list?
424 | _button1: Button
425 | _button2: Button
426 | _button3: Button
427 | _button4: Button
428 | _button5: Button
429 |
430 | # Event observation
431 | _observers: list[ShuttleXpressObserver] = []
432 |
433 | events: list[Event] = []
434 |
435 | @property
436 | def wheel(self) -> Wheel:
437 | return self._wheel
438 |
439 | @wheel.setter
440 | def wheel(self, new_pos: int) -> None:
441 | delta: Optional[int] = None
442 | logger.debug(f"{self.__class__.__name__}: Wheel position: {new_pos}")
443 | if self._wheel.pos != new_pos:
444 | delta = new_pos - self._wheel.pos
445 | self._wheel.pos = new_pos
446 | if delta:
447 | direction: bool = True if delta == 1 else False
448 | logger.info(f"{self.__class__.__name__}: Wheel position changed by {delta}")
449 | self.events.append(RotaryEvent(self.wheel, delta, direction))
450 |
451 | @property
452 | def dial(self) -> Dial:
453 | return self._dial
454 |
455 | @dial.setter
456 | def dial(self, new_pos: int) -> None:
457 | delta: Optional[int] = None
458 | logger.debug(f"{self.__class__.__name__}: Dial position: {new_pos}")
459 | if self._dial.pos is not None:
460 | if self._dial.pos != new_pos:
461 | delta = new_pos - self._dial.pos
462 | # Handle special cases when wrapping
463 | if delta == -255:
464 | delta = 1
465 | elif delta == 255:
466 | delta = -1
467 | self._dial.pos = new_pos
468 | if delta:
469 | direction: bool = True if delta == 1 else False
470 | dir_changed: bool = False if self._prev_dial_dir == direction else True
471 | self._prev_dial_dir = direction
472 | if dir_changed:
473 | delta = delta * 2 # Hardware quirk: misses one tick when changing directions
474 | logger.info(f"{self.__class__.__name__}: Dial position changed by {delta}")
475 | self.events.append(RotaryEvent(self.dial, delta, direction))
476 |
477 | @property
478 | def button1(self) -> Button:
479 | return self._button1
480 |
481 | @button1.setter
482 | def button1(self, new_state: bool) -> None:
483 | if self._button1.push != new_state:
484 | logger.info(f"{self.__class__.__name__}: Button 1 state changed!")
485 | self._button1.push = new_state
486 | self.events.append(ButtonEvent(self.button1))
487 |
488 | @property
489 | def button2(self) -> Button:
490 | return self._button2
491 |
492 | @button2.setter
493 | def button2(self, new_state: bool) -> None:
494 | if self._button2.push != new_state:
495 | logger.info(f"{self.__class__.__name__}: Button 2 state changed!")
496 | self._button2.push = new_state
497 | self.events.append(ButtonEvent(self.button2))
498 |
499 | @property
500 | def button3(self) -> Button:
501 | return self._button3
502 |
503 | @button3.setter
504 | def button3(self, new_state: bool) -> None:
505 | if self._button3.push != new_state:
506 | logger.info(f"{self.__class__.__name__}: Button 3 state changed!")
507 | self._button3.push = new_state
508 | self.events.append(ButtonEvent(self.button3))
509 |
510 | @property
511 | def button4(self) -> Button:
512 | return self._button4
513 |
514 | @button4.setter
515 | def button4(self, new_state: bool) -> None:
516 | if self._button4.push != new_state:
517 | logger.info(f"{self.__class__.__name__}: Button 4 state changed!")
518 | self._button4.push = new_state
519 | self.events.append(ButtonEvent(self.button4))
520 |
521 | @property
522 | def button5(self) -> Button:
523 | return self._button5
524 |
525 | @button5.setter
526 | def button5(self, new_state: bool) -> None:
527 | if self._button5.push != new_state:
528 | logger.info(f"{self.__class__.__name__}: Button 5 state changed!")
529 | self._button5.push = new_state
530 | self.events.append(ButtonEvent(self.button5))
531 |
532 | @classmethod
533 | def find(cls) -> list[dict]:
534 | logger.debug(f"{cls.__name__}: Listing all HID devices...")
535 | devices: list[dict] = hid.enumerate()
536 | shuttle_devices: list[dict] = []
537 | for dev in devices:
538 | logger.debug(f"{cls.__name__}: Found HID device: {dev}")
539 | if dev['vendor_id'] == ShuttleXpress.USB_VID and \
540 | dev['product_id'] == ShuttleXpress.USB_PID_XPRESS:
541 | shuttle_devices.append(dev)
542 | logger.info(f"{cls.__name__}: Found {len(shuttle_devices)} "
543 | f"Contour ShuttleXpress HID: {shuttle_devices}")
544 | return shuttle_devices
545 |
546 | def __init__(self) -> None:
547 | logger.debug(f"{self.__class__.__name__}: Instanciation.")
548 | self.hid_device = hid.device()
549 | self.connected = False
550 | self._wheel = Wheel()
551 | self._dial = Dial()
552 | self._button1 = Button(1)
553 | self._button2 = Button(2)
554 | self._button3 = Button(3)
555 | self._button4 = Button(4)
556 | self._button5 = Button(5)
557 |
558 | def __del__(self) -> None:
559 | logger.debug(f"{self.__class__.__name__}: Destruction.")
560 | if self.connected:
561 | self.events.append(DisconnectionEvent(self.hid_device))
562 | self.hid_device.close()
563 | self.connected = False
564 | self.notify()
565 |
566 | def connect(self, path: Optional[str] = None) -> None:
567 | if path:
568 | try:
569 | self.hid_device.open_path(path)
570 |
571 | logger.info(f"{self.__class__.__name__}: Connected to {self.hid_device.get_product_string()}!")
572 |
573 | except IOError as e:
574 | logger.error(f"{self.__class__.__name__}: Device not found: {e}")
575 | return
576 | else:
577 | try:
578 | self.hid_device.open(self.USB_VID, self.USB_PID_XPRESS)
579 |
580 | logger.info(f"{self.__class__.__name__}: Connected to {self.hid_device.get_product_string()}!")
581 |
582 | except IOError as e:
583 | logger.error(f"{self.__class__.__name__}: Device not found: {e}")
584 | return
585 |
586 | self.connected = True
587 | self.hid_device.set_nonblocking(True)
588 |
589 | self.events.append(ConnectionEvent(self.hid_device))
590 | self.notify()
591 |
592 | def poll(self) -> None:
593 | if not self.connected:
594 | self.connect()
595 | return
596 | else:
597 | try:
598 | data: list[int] = self.hid_device.read(self.HID_DATA_SIZE)
599 | except (OSError, ValueError) as e:
600 | logger.error(f"{self.__class__.__name__}: Could not read from device {e}")
601 | self.__del__()
602 | return
603 |
604 | if data:
605 | logger.debug(f"{self.__class__.__name__}: Data red from HID: {data!r}")
606 | self.wheel = int.from_bytes(data[0].to_bytes(1, 'big'), 'big', signed=True)
607 | self.dial = data[1]
608 | self.button1 = bool(data[3] & (1 << 4))
609 | self.button2 = bool(data[3] & (1 << 5))
610 | self.button3 = bool(data[3] & (1 << 6))
611 | self.button4 = bool(data[3] & (1 << 7))
612 | self.button5 = bool(data[4] & (1 << 0))
613 | self.notify()
614 |
615 | ##
616 | # Observers management
617 | ##
618 | def attach(self, observer: ShuttleXpressObserver) -> None:
619 | logger.debug(f"{self.__class__.__name__}: Attached an observer.")
620 | self._observers.append(observer)
621 |
622 | def detach(self, observer: ShuttleXpressObserver) -> None:
623 | self._observers.remove(observer)
624 |
625 | def notify(self) -> None:
626 | logger.debug(f"{self.__class__.__name__}: Notifying observers...")
627 | for observer in self._observers:
628 | observer.update(self)
629 | self.events.clear()
630 |
631 |
632 | class ShuttleXpressObserverSample(ShuttleXpressObserver):
633 | def update(self, subject: ShuttleXpressSubject) -> None:
634 | logger.debug(f"{self.__class__.__name__}: State changed!")
635 | for event in subject.events:
636 | logger.info(f"{self.__class__.__name__}: {event.desc}")
637 | if isinstance(event, ConnectionEvent):
638 | self._handle_connection(event.element)
639 | elif isinstance(event, DisconnectionEvent):
640 | self._handle_disconnection(event)
641 | elif isinstance(event, RotaryEvent):
642 | self._handle_rotary_event(event)
643 | elif isinstance(event, ButtonEvent):
644 | self._handle_button(event.element)
645 | else:
646 | logger.warning(f"{self.__class__.__name__}: Unsupported event type: {type(event)}")
647 |
648 | @classmethod
649 | def _handle_connection(cls, device: hid.device) -> None:
650 | logger.info(f"{cls.__name__}: Connected to {device.get_manufacturer_string()} {device.get_product_string()}")
651 |
652 | @classmethod
653 | def _handle_disconnection(cls, event: DisconnectionEvent) -> None:
654 | logger.warning(f"{cls.__name__}: Disconnected.")
655 |
656 | def _handle_rotary_event(self, event: RotaryEvent) -> None:
657 | if type(event.element) is Wheel:
658 | self._handle_wheel(event.element, event.direction, event.value)
659 | elif type(event.element) is Dial:
660 | self._handle_dial(event.element, event.direction, event.value)
661 |
662 | @classmethod
663 | def _handle_wheel(cls, wheel: Wheel, direction: bool, value: int) -> None:
664 | if direction:
665 | logger.info(f"{cls.__name__}: Wheel up by {value}")
666 | else:
667 | logger.info(f"{cls.__name__}: Wheel down by {value}!")
668 | if wheel.centered:
669 | logger.info(f"{cls.__name__}: Wheel centered!")
670 | logger.info(f"{cls.__name__}: Wheel position: {wheel.pos}")
671 |
672 | @classmethod
673 | def _handle_dial(cls, dial: Dial, direction: bool, value: int) -> None:
674 | if direction:
675 | logger.info(f"{cls.__name__}: Dial up by {value}!")
676 | else:
677 | logger.info(f"{cls.__name__}: Dial down by {value}!")
678 | logger.info(f"D{cls.__name__}: ial position: {dial.pos}")
679 |
680 | @classmethod
681 | def _handle_button(cls, button: Button) -> None:
682 | state: str = 'down' if button.push else 'up'
683 | logger.info(f"{cls.__name__}: Button {button.num} state changed to: {state}!")
684 |
685 |
686 | if __name__ == '__main__':
687 | shuttle: ShuttleXpress = ShuttleXpress()
688 |
689 | sample_observer: ShuttleXpressObserver = ShuttleXpressObserverSample()
690 | shuttle.attach(sample_observer)
691 |
692 | shuttle.connect()
693 | while True:
694 | shuttle.poll()
695 |
--------------------------------------------------------------------------------
/gui/images/ShuttleXpress_Black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EMATech/OpenContourShuttle/9b9601eb555870676ba8b055a71e7d10daa6f277/gui/images/ShuttleXpress_Black.png
--------------------------------------------------------------------------------
/gui/images/delete-sweep_24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EMATech/OpenContourShuttle/9b9601eb555870676ba8b055a71e7d10daa6f277/gui/images/delete-sweep_24.png
--------------------------------------------------------------------------------
/gui/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EMATech/OpenContourShuttle/9b9601eb555870676ba8b055a71e7d10daa6f277/gui/images/icon.png
--------------------------------------------------------------------------------
/gui/images/usb_black_24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EMATech/OpenContourShuttle/9b9601eb555870676ba8b055a71e7d10daa6f277/gui/images/usb_black_24.png
--------------------------------------------------------------------------------
/gui/images/usb_white_24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/EMATech/OpenContourShuttle/9b9601eb555870676ba8b055a71e7d10daa6f277/gui/images/usb_white_24.png
--------------------------------------------------------------------------------
/gui/main.py:
--------------------------------------------------------------------------------
1 | # This Python file uses the following encoding: utf-8
2 | #
3 | # SPDX-FileCopyrightText: 2021 Raphaël Doursenaud
4 | #
5 | # SPDX-License-Identifier: GPL-3.0-or-later
6 |
7 | """
8 | `Open Contour Shuttle GUI`
9 | ================================================================================
10 |
11 | A multiplatform configuration editor, event management & generator GUI for Contour ShuttleXpress.
12 |
13 | Contour, ShuttleXpress and ShuttlePro are trademarks of
14 | Contour Innovations LLC in the United States.
15 |
16 | These are not active trademarks in the European Union and France where I reside.
17 |
18 | * Author(s): Raphaël Doursenaud
19 |
20 | Implementation Notes
21 | --------------------
22 |
23 | """
24 |
25 | __version__ = "0.0.0-auto.0"
26 | __repo__ = "https://github.com/EMATech/OpenContourShuttle.git"
27 |
28 | import logging
29 | from platform import python_version
30 | from typing import Dict, List, Optional
31 |
32 | import PySide6
33 | from PySide6.QtCore import QObject, QThread, Signal, Slot
34 | from PySide6.QtGui import QAction, QIcon
35 | from PySide6.QtWidgets import QApplication, QMainWindow, QMenu, QSystemTrayIcon
36 | from qt_material import QtStyleTools
37 |
38 | from device import Button, ButtonEvent, ConnectionEvent, Dial, \
39 | DisconnectionEvent, Event, RotaryEvent, ShuttleXpress, \
40 | ShuttleXpressObserver, ShuttleXpressSubject, Wheel
41 | from mainwindow_ui import Ui_MainWindow
42 |
43 |
44 | ##
45 | # Logging system
46 | ##
47 |
48 |
49 | class LogSignal(QObject):
50 | signal = Signal(str, logging.LogRecord)
51 |
52 |
53 | class LogHandler(logging.Handler):
54 | def __init__(self, slotfunc, *args, **kwargs):
55 | super().__init__(*args, **kwargs)
56 | self.signaller = LogSignal()
57 | self.signaller.signal.connect(slotfunc)
58 |
59 | def emit(self, record):
60 | s = self.format(record)
61 | self.signaller.signal.emit(s, record)
62 |
63 |
64 | ##
65 | # Device communication
66 | ##
67 |
68 | class ShuttleSignals(QObject):
69 | data = Signal(object)
70 |
71 |
72 | shuttle_signals = ShuttleSignals()
73 |
74 |
75 | class GuiObserver(ShuttleXpressObserver):
76 | def update(self, subject: ShuttleXpressSubject) -> None:
77 | for event in subject.events:
78 | shuttle_signals.data.emit(event)
79 |
80 |
81 | class ShuttleWorker(QThread):
82 | active: bool = True
83 | finished: bool = False
84 |
85 | def __init__(self, parent: Optional[QObject] = None) -> None:
86 | super().__init__(parent)
87 | self.shuttle = ShuttleXpress()
88 | self.shuttle.attach(GuiObserver())
89 | self.shuttle.connect()
90 |
91 | def run(self) -> None:
92 | while self.active:
93 | self.shuttle.poll()
94 | self.finished = True
95 |
96 | def stop(self) -> None:
97 | del self.shuttle
98 | self.active = False
99 |
100 |
101 | ##
102 | # GUI
103 | ##
104 |
105 |
106 | class GUI(QMainWindow, Ui_MainWindow, QtStyleTools):
107 | ICON: QIcon
108 | TITLE: str = "Open Contour Shuttle"
109 | WHEEL_MAP: Dict[int, QObject]
110 | BUTTON_MAP: List[QObject]
111 | LOG_COLORS = {
112 | logging.DEBUG: 'lightgray',
113 | logging.INFO: 'white',
114 | logging.WARNING: 'orange',
115 | logging.ERROR: 'red',
116 | logging.CRITICAL: 'purple',
117 | }
118 |
119 | def __init__(self) -> None:
120 | super().__init__()
121 |
122 | # self.load_ui() # Broken
123 | # Use pyside-uic form.ui > gui.py to generate the UI instead
124 | self.setupUi(self)
125 |
126 | # Hide ASAP to avoid window flash
127 | self.about_widget.setHidden(True)
128 | self.plugins_widget.setHidden(True)
129 | self.config_widget.setHidden(True)
130 | self.log_widget.setHidden(True)
131 |
132 | h = LogHandler(self.append_log)
133 | fs = '%(levelname)-8s %(message)s'
134 | formatter = logging.Formatter(fs)
135 | h.setFormatter(formatter)
136 | logging.getLogger().addHandler(h)
137 |
138 | self.ICON = QIcon('images/icon.png')
139 |
140 | self.WHEEL_MAP = {
141 | -7: self.wheel_neg7,
142 | -6: self.wheel_neg6,
143 | -5: self.wheel_neg5,
144 | -4: self.wheel_neg4,
145 | -3: self.wheel_neg3,
146 | -2: self.wheel_neg2,
147 | -1: self.wheel_neg1,
148 | 0: self.wheel_cent0,
149 | 1: self.wheel_pos1,
150 | 2: self.wheel_pos2,
151 | 3: self.wheel_pos3,
152 | 4: self.wheel_pos4,
153 | 5: self.wheel_pos5,
154 | 6: self.wheel_pos6,
155 | 7: self.wheel_pos7,
156 | }
157 | self.BUTTON_MAP = [None, self.button_1, self.button_2, self.button_3, self.button_4, self.button_5]
158 |
159 | self.setWindowIcon(self.ICON)
160 | self.setWindowTitle(self.TITLE)
161 | #self.set_ms_windows_icon()
162 | extra = {
163 | # Button colors
164 | 'danger': '#dc3545',
165 | 'warning': '#ffc107',
166 | 'success': '#17a2b8',
167 |
168 | # Font
169 | 'font_family': 'Roboto',
170 | }
171 | self.apply_stylesheet(self, theme='dark_red.xml', extra=extra)
172 |
173 | # self.setWindowFlags(self.windowFlags() | Qt.Dialog | Qt.MSWindowsFixedSizeDialogHint)
174 | # self.statusbar.setSizeGripEnabled(False)
175 |
176 | self.update_status_bar("Connecting...")
177 |
178 | self.show()
179 |
180 | if QSystemTrayIcon.isSystemTrayAvailable():
181 | self.systray = QSystemTrayIcon()
182 | self.systray.setIcon(self.ICON)
183 | systray_menu = QMenu(title=self.TITLE, parent=self)
184 | quit_action = QAction("&Quit", self)
185 | systray_menu.addAction(quit_action)
186 | quit_action.triggered.connect(self.close)
187 | self.systray.setContextMenu(systray_menu)
188 | self.systray.setVisible(True)
189 | self.systray.activated.connect(self.handle_systray_activation)
190 |
191 | shuttle_signals.data.connect(self.handle_events)
192 | self.shuttle_worker = ShuttleWorker()
193 | self.shuttle_worker.start()
194 | self.shuttle_worker.finished.connect(self.shuttle_worker.quit)
195 |
196 | self.about_text.setMarkdown(f"""
197 | Open Contour Shuttle
198 | ====================
199 |
200 | A multiplatform userland driver, configuration editor, event manager & generator
201 | for Contour ShuttleXpress & ShuttlePRO v2.
202 |
203 | Version: `{__version__}`
204 |
205 | Source: `{__repo__}`
206 |
207 | Running on Python v{python_version()}
208 |
209 | Legal notice
210 | ------------
211 |
212 | ### License
213 |
214 | Copyright 2021 Raphaël Doursenaud
215 |
216 | This software is released under the terms of the GNU General Public License,
217 | version 3.0 or later (GPL-3.0-or-later).
218 |
219 | ### Dependencies & License Acknowledgment
220 |
221 | **Python**
222 |
223 | Used under the terms of the PSF License Agreement.
224 |
225 | **libusb hidapi**
226 |
227 | Copyright Alan Ott, Signal 11 Software.
228 | Used under the terms of the GNU General Public License, version 3.0 (GPL-3.0).
229 |
230 | **Trezor cython-hidapi**
231 |
232 | Copyright Pavol Rusnak, SatoshiLabs.
233 | Used under the terms of the GNU General Public License, version 3.0 (GPL-3.0).
234 |
235 | **Qt PySide6**
236 |
237 | Used under the terms of the GNU Lesser General Public License v3.0 (LGPL-3.0).
238 |
239 | **UN-GCPDS Qt-Material**
240 |
241 | Used under the BSD-2-Clause License.
242 |
243 | **Material Design Icons**
244 |
245 | Used under the Pictogrammers Free License.
246 |
247 | ### Trademarks
248 |
249 | Contour, ShuttleXpress and ShuttlePro are trademarks of
250 | Contour Innovations LLC in the United States of America.
251 |
252 | These are not registered or active trademarks in the European Union and
253 | France where I reside.
254 | """)
255 | self.about_button.clicked.connect(self.toggle_about_vis)
256 | self.plug_button.clicked.connect(self.toggle_plugins_vis)
257 | self.conf_button.clicked.connect(self.toggle_config_vis)
258 | self.log_button.clicked.connect(self.toggle_log_vis)
259 | self.log_clear_button.clicked.connect(self.clear_log)
260 |
261 | # def load_ui(self):
262 | # loader = QUiLoader()
263 | # path = os.path.join(os.path.dirname(__file__), "mainwindow.ui")
264 | # ui_file = QFile(path)
265 | # ui_file.open(QFile.ReadOnly)
266 | # print(ui_file.readAll())
267 | # loader.load(ui_file, self)
268 | # ui_file.close()
269 |
270 | def handle_systray_activation(self, reason: QSystemTrayIcon.ActivationReason) -> None:
271 | if reason is QSystemTrayIcon.ActivationReason.Trigger:
272 | self.toggle_main_window_visibility()
273 |
274 | def toggle_main_window_visibility(self) -> None:
275 | self.hide() if self.isVisible() else self.show()
276 |
277 | def update_status_bar(self, message: str) -> None:
278 | self.statusbar.showMessage(message)
279 |
280 | def toggle_about_vis(self) -> None:
281 | self.about_widget.setVisible(self.about_widget.isHidden())
282 |
283 | def toggle_config_vis(self) -> None:
284 | self.config_widget.setVisible(self.config_widget.isHidden())
285 |
286 | def toggle_plugins_vis(self) -> None:
287 | self.plugins_widget.setVisible(self.plugins_widget.isHidden())
288 |
289 | def toggle_log_vis(self) -> None:
290 | self.log_widget.setVisible(self.log_widget.isHidden())
291 |
292 | @Slot(str, logging.LogRecord)
293 | def append_log(self, message, record):
294 | color = self.LOG_COLORS.get(record.levelno, 'black')
295 | s = f'{message}
'
296 | self.log_text.appendHtml(s)
297 |
298 | def clear_log(self) -> None:
299 | self.log_text.clear()
300 |
301 | def handle_events(self, event: Event) -> None:
302 | self.update_status_bar(event.desc)
303 | if isinstance(event, ConnectionEvent):
304 | self.usb_status.setChecked(True)
305 | elif isinstance(event, DisconnectionEvent):
306 | self.usb_status.setChecked(False)
307 | # self.shuttle_worker.stop()
308 | elif isinstance(event, RotaryEvent):
309 | self.handle_rotary_event(event)
310 | elif isinstance(event, ButtonEvent):
311 | self.handle_button(event.element)
312 |
313 | def handle_rotary_event(self, rotary_event: RotaryEvent) -> None:
314 | if type(rotary_event.element) is Wheel:
315 | self.handle_wheel(rotary_event.element, rotary_event.direction, rotary_event.value)
316 | elif type(rotary_event.element) is Dial:
317 | self.handle_dial(rotary_event.element, rotary_event.direction, rotary_event.value)
318 |
319 | def handle_wheel(self, wheel: Wheel, direction: bool, change: int) -> None:
320 | self.WHEEL_MAP[wheel.pos].setChecked(True)
321 |
322 | def handle_dial(self, dial: Dial, direction: bool, change: int) -> None:
323 | self.dial.setValue(self.dial.value() + change)
324 |
325 | def handle_button(self, button: Button) -> None:
326 | self.BUTTON_MAP[button.num].setChecked(button.push)
327 |
328 | def closeEvent(self, event: PySide6.QtGui.QCloseEvent) -> None:
329 | self.shuttle_worker.stop()
330 |
331 | @staticmethod
332 | def set_ms_windows_icon() -> None:
333 | """
334 | Force Microsoft Windows to properly display the application icon in the task bar
335 |
336 | Workaround from: https://stackoverflow.com/a/27872625
337 | """
338 | try:
339 | import ctypes
340 | myappid = u'eu.ematech.contour.shuttlexpress' # arbitrary string
341 | ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
342 | except AttributeError as e:
343 | logging.warning(f"{e}")
344 | pass
345 |
346 | @staticmethod
347 | def set_mac_os_title() -> None:
348 | """
349 | Force Mac OS X to properly display the app name in the title bar using PyObjC
350 |
351 | Workaround from: https://stackoverflow.com/a/54755290
352 | """
353 | try:
354 | from Foundation import NSBundle
355 | bundle = NSBundle.mainBundle()
356 | if bundle:
357 | app_name = GUI.TITLE
358 | app_info = bundle.localizedInfoDictionary() or bundle.infoDictionary()
359 | if app_info:
360 | app_info['CFBundleName'] = app_name
361 | except ImportError as e:
362 | logging.warning(f"{e}")
363 | pass
364 |
365 |
366 | if __name__ == "__main__":
367 | import platform
368 | import sys
369 |
370 | ###
371 | # Platform idiosyncrasies
372 | ###
373 | currentplatform = platform.system()
374 | if currentplatform == 'Windows':
375 | GUI.set_ms_windows_icon()
376 | elif currentplatform == 'Darwin':
377 | GUI.set_mac_os_title()
378 | app = QApplication(sys.argv)
379 | window = GUI()
380 | sys.exit(app.exec())
381 |
--------------------------------------------------------------------------------
/gui/mainwindow.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | MainWindow
4 |
5 |
6 | Qt::ApplicationModal
7 |
8 |
9 |
10 | 0
11 | 0
12 | 1492
13 | 1852
14 |
15 |
16 |
17 |
18 | 0
19 | 0
20 |
21 |
22 |
23 |
24 | 700
25 | 700
26 |
27 |
28 |
29 |
30 | 16777215
31 | 16777215
32 |
33 |
34 |
35 |
36 | 600
37 | 600
38 |
39 |
40 |
41 |
42 | 12
43 | true
44 |
45 |
46 |
47 | Open Contour Shuttle
48 |
49 |
50 | false
51 |
52 |
53 |
54 |
55 |
56 | false
57 |
58 |
59 | QTabWidget::Rounded
60 |
61 |
62 | QMainWindow::AllowTabbedDocks|QMainWindow::AnimatedDocks
63 |
64 |
65 | false
66 |
67 |
68 |
69 | true
70 |
71 |
72 |
73 | 650
74 | 650
75 |
76 |
77 |
78 | Qt::DefaultContextMenu
79 |
80 |
81 | false
82 |
83 |
84 |
85 |
86 |
87 | -
88 |
89 |
-
90 |
91 |
92 |
93 | 0
94 | 0
95 |
96 |
97 |
98 |
99 | 24
100 | 24
101 |
102 |
103 |
104 |
105 | 9
106 | true
107 | true
108 |
109 |
110 |
111 | PointingHandCursor
112 |
113 |
114 |
115 |
116 |
117 | Log
118 |
119 |
120 | //
121 |
122 |
123 | false
124 |
125 |
126 | true
127 |
128 |
129 |
130 |
131 |
132 | -
133 |
134 |
135 |
136 | 600
137 | 600
138 |
139 |
140 |
141 |
142 | 600
143 | 600
144 |
145 |
146 |
147 | false
148 |
149 |
150 | QWidget#shuttle_widget {background: url(images/ShuttleXpress_Black.png);
151 | background-repeat:no-repeat;}
152 |
153 |
154 |
155 |
156 |
157 | 80
158 | 266
159 | 24
160 | 24
161 |
162 |
163 |
164 |
165 | 0
166 | 0
167 |
168 |
169 |
170 |
171 | 24
172 | 24
173 |
174 |
175 |
176 |
177 | 12
178 | true
179 |
180 |
181 |
182 | Button 1
183 |
184 |
185 | background: #000000ff;
186 | color: white;
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 | false
196 |
197 |
198 |
199 | 288
200 | 10
201 | 24
202 | 24
203 |
204 |
205 |
206 |
207 | 0
208 | 0
209 |
210 |
211 |
212 |
213 | 24
214 | 24
215 |
216 |
217 |
218 |
219 | 9
220 | true
221 | true
222 |
223 |
224 |
225 | ArrowCursor
226 |
227 |
228 |
229 |
230 |
231 | USB connection status
232 |
233 |
234 | USB Status
235 |
236 |
237 | false
238 |
239 |
240 |
241 |
242 |
243 |
244 | images/usb_black_24.png
245 | images/usb_white_24.png
246 |
247 |
248 |
249 |
250 | 24
251 | 24
252 |
253 |
254 |
255 | true
256 |
257 |
258 | false
259 |
260 |
261 | false
262 |
263 |
264 | true
265 |
266 |
267 |
268 |
269 |
270 | 498
271 | 266
272 | 24
273 | 24
274 |
275 |
276 |
277 |
278 | 0
279 | 0
280 |
281 |
282 |
283 |
284 | 24
285 | 24
286 |
287 |
288 |
289 |
290 | 12
291 | true
292 |
293 |
294 |
295 | Button 5
296 |
297 |
298 | Qt::RightToLeft
299 |
300 |
301 | background: #000000ff;
302 | color: white;
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 | 382
313 | 164
314 | 24
315 | 24
316 |
317 |
318 |
319 |
320 | 0
321 | 0
322 |
323 |
324 |
325 |
326 | 24
327 | 24
328 |
329 |
330 |
331 | Wheel position: right 4
332 |
333 |
334 | background: #000000ff;
335 | color: white;
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 |
344 |
345 | 289
346 | 130
347 | 24
348 | 24
349 |
350 |
351 |
352 |
353 | 0
354 | 0
355 |
356 |
357 |
358 |
359 | 24
360 | 24
361 |
362 |
363 |
364 | Wheel position: center
365 |
366 |
367 | background: #000000ff;
368 | color: white;
369 |
370 |
371 |
372 |
373 |
374 |
375 | true
376 |
377 |
378 |
379 |
380 |
381 | 314
382 | 133
383 | 24
384 | 24
385 |
386 |
387 |
388 |
389 | 0
390 | 0
391 |
392 |
393 |
394 |
395 | 24
396 | 24
397 |
398 |
399 |
400 | Wheel position: right 1
401 |
402 |
403 | color: white;
404 |
405 |
406 |
407 |
408 |
409 |
410 |
411 |
412 | 338
413 | 139
414 | 24
415 | 24
416 |
417 |
418 |
419 |
420 | 0
421 | 0
422 |
423 |
424 |
425 |
426 | 24
427 | 24
428 |
429 |
430 |
431 | Wheel position: right 2
432 |
433 |
434 | background: #000000ff;
435 | color: white;
436 |
437 |
438 |
439 |
440 |
441 |
442 |
443 |
444 |
445 | 197
446 | 178
447 | 216
448 | 216
449 |
450 |
451 |
452 |
453 | 0
454 | 0
455 |
456 |
457 |
458 |
459 | 24
460 | 24
461 |
462 |
463 |
464 | Dial
465 |
466 |
467 | false
468 |
469 |
470 | background-color: black;
471 |
472 |
473 | 0
474 |
475 |
476 | 10
477 |
478 |
479 | 1
480 |
481 |
482 | 5
483 |
484 |
485 | 5
486 |
487 |
488 | false
489 |
490 |
491 | false
492 |
493 |
494 | true
495 |
496 |
497 | 3.700000000000000
498 |
499 |
500 | false
501 |
502 |
503 |
504 |
505 |
506 | 162
507 | 204
508 | 24
509 | 24
510 |
511 |
512 |
513 |
514 | 0
515 | 0
516 |
517 |
518 |
519 |
520 | 24
521 | 24
522 |
523 |
524 |
525 | Wheel position: left 6
526 |
527 |
528 | color: white;
529 |
530 |
531 |
532 |
533 |
534 |
535 |
536 |
537 | 400
538 | 182
539 | 24
540 | 24
541 |
542 |
543 |
544 |
545 | 0
546 | 0
547 |
548 |
549 |
550 |
551 | 24
552 | 24
553 |
554 |
555 |
556 | Wheel position: right 5
557 |
558 |
559 | background: #000000ff;
560 | color: white;
561 |
562 |
563 |
564 |
565 |
566 |
567 |
568 |
569 |
570 | 156
571 | 122
572 | 24
573 | 24
574 |
575 |
576 |
577 |
578 | 0
579 | 0
580 |
581 |
582 |
583 |
584 | 24
585 | 24
586 |
587 |
588 |
589 |
590 | 12
591 | true
592 |
593 |
594 |
595 | Button 2
596 |
597 |
598 | background: #000000ff;
599 | color: white;
600 |
601 |
602 |
603 |
604 |
605 |
606 |
607 |
608 |
609 | 178
610 | 182
611 | 24
612 | 24
613 |
614 |
615 |
616 |
617 | 0
618 | 0
619 |
620 |
621 |
622 |
623 | 24
624 | 24
625 |
626 |
627 |
628 | Wheel position: left 5
629 |
630 |
631 | color: white;
632 |
633 |
634 |
635 |
636 |
637 |
638 |
639 |
640 | 416
641 | 204
642 | 24
643 | 24
644 |
645 |
646 |
647 |
648 | 0
649 | 0
650 |
651 |
652 |
653 |
654 | 24
655 | 24
656 |
657 |
658 |
659 | Wheel position: right 6
660 |
661 |
662 | background: #000000ff;
663 | color: white;
664 |
665 |
666 |
667 |
668 |
669 |
670 |
671 |
672 |
673 | 264
674 | 133
675 | 24
676 | 24
677 |
678 |
679 |
680 |
681 | 0
682 | 0
683 |
684 |
685 |
686 |
687 | 24
688 | 24
689 |
690 |
691 |
692 | Wheel position: left 1
693 |
694 |
695 | background: #000000ff;
696 | color: white;
697 |
698 |
699 |
700 |
701 |
702 |
703 |
704 |
705 |
706 | 430
707 | 122
708 | 24
709 | 24
710 |
711 |
712 |
713 |
714 | 0
715 | 0
716 |
717 |
718 |
719 |
720 | 24
721 | 24
722 |
723 |
724 |
725 |
726 | 12
727 | true
728 |
729 |
730 |
731 | Button 4
732 |
733 |
734 | Qt::RightToLeft
735 |
736 |
737 | false
738 |
739 |
740 | background: #000000ff;
741 | color: white;
742 |
743 |
744 |
745 |
746 |
747 |
748 |
749 |
750 |
751 | 427
752 | 229
753 | 24
754 | 24
755 |
756 |
757 |
758 |
759 | 0
760 | 0
761 |
762 |
763 |
764 |
765 | 24
766 | 24
767 |
768 |
769 |
770 | Wheel position: right 7
771 |
772 |
773 | background: #000000ff;
774 | color: white;
775 |
776 |
777 |
778 |
779 |
780 |
781 |
782 |
783 |
784 | 290
785 | 72
786 | 24
787 | 24
788 |
789 |
790 |
791 |
792 | 0
793 | 0
794 |
795 |
796 |
797 |
798 | 24
799 | 24
800 |
801 |
802 |
803 |
804 | 12
805 | true
806 |
807 |
808 |
809 | Button 3
810 |
811 |
812 | background: #000000ff;
813 | color: white;
814 |
815 |
816 |
817 |
818 |
819 |
820 | false
821 |
822 |
823 |
824 |
825 |
826 | 240
827 | 139
828 | 24
829 | 24
830 |
831 |
832 |
833 |
834 | 0
835 | 0
836 |
837 |
838 |
839 |
840 | 24
841 | 24
842 |
843 |
844 |
845 | Wheel position: left 2
846 |
847 |
848 | background: #000000ff;
849 | color: white;
850 |
851 |
852 |
853 |
854 |
855 |
856 |
857 |
858 |
859 | 361
860 | 149
861 | 24
862 | 24
863 |
864 |
865 |
866 |
867 | 0
868 | 0
869 |
870 |
871 |
872 |
873 | 24
874 | 24
875 |
876 |
877 |
878 | Wheel position: right 3
879 |
880 |
881 | background: #000000ff;
882 | color: white;
883 |
884 |
885 |
886 |
887 |
888 |
889 |
890 |
891 |
892 | 217
893 | 149
894 | 24
895 | 24
896 |
897 |
898 |
899 |
900 | 0
901 | 0
902 |
903 |
904 |
905 |
906 | 24
907 | 24
908 |
909 |
910 |
911 | Wheel position: left 3
912 |
913 |
914 | color: white;
915 |
916 |
917 |
918 |
919 |
920 |
921 |
922 |
923 | 196
924 | 164
925 | 24
926 | 24
927 |
928 |
929 |
930 |
931 | 0
932 | 0
933 |
934 |
935 |
936 |
937 | 24
938 | 24
939 |
940 |
941 |
942 | Wheel position: left 4
943 |
944 |
945 | background: #000000ff;
946 | color: white;
947 |
948 |
949 |
950 | -
951 |
952 |
953 |
954 |
955 |
956 | 151
957 | 229
958 | 24
959 | 24
960 |
961 |
962 |
963 |
964 | 0
965 | 0
966 |
967 |
968 |
969 |
970 | 24
971 | 24
972 |
973 |
974 |
975 | Wheel position: left 7
976 |
977 |
978 | color: white;
979 |
980 |
981 |
982 |
983 |
984 | dial
985 | button_1
986 | usb_status
987 | button_5
988 | wheel_pos4
989 | wheel_cent0
990 | wheel_pos1
991 | wheel_pos2
992 | wheel_neg6
993 | wheel_pos5
994 | button_2
995 | wheel_neg5
996 | wheel_pos6
997 | wheel_neg1
998 | button_4
999 | wheel_pos7
1000 | button_3
1001 | wheel_neg2
1002 | wheel_pos3
1003 | wheel_neg3
1004 | wheel_neg4
1005 | wheel_neg7
1006 |
1007 |
1008 | -
1009 |
1010 |
-
1011 |
1012 |
1013 |
1014 | 0
1015 | 0
1016 |
1017 |
1018 |
1019 |
1020 | 24
1021 | 24
1022 |
1023 |
1024 |
1025 |
1026 | 9
1027 | true
1028 | true
1029 |
1030 |
1031 |
1032 | WhatsThisCursor
1033 |
1034 |
1035 |
1036 |
1037 |
1038 | About
1039 |
1040 |
1041 | ?
1042 |
1043 |
1044 | false
1045 |
1046 |
1047 | true
1048 |
1049 |
1050 |
1051 |
1052 |
1053 | -
1054 |
1055 |
-
1056 |
1057 |
1058 |
1059 | 0
1060 | 0
1061 |
1062 |
1063 |
1064 |
1065 | 24
1066 | 24
1067 |
1068 |
1069 |
1070 |
1071 | 9
1072 | true
1073 | true
1074 |
1075 |
1076 |
1077 | PointingHandCursor
1078 |
1079 |
1080 |
1081 |
1082 |
1083 | Configuration
1084 |
1085 |
1086 | #
1087 |
1088 |
1089 | false
1090 |
1091 |
1092 | true
1093 |
1094 |
1095 |
1096 |
1097 |
1098 | -
1099 |
1100 |
-
1101 |
1102 |
1103 |
1104 | 0
1105 | 0
1106 |
1107 |
1108 |
1109 |
1110 | 24
1111 | 24
1112 |
1113 |
1114 |
1115 |
1116 | 9
1117 | true
1118 | true
1119 |
1120 |
1121 |
1122 | PointingHandCursor
1123 |
1124 |
1125 |
1126 |
1127 |
1128 | Configuration
1129 |
1130 |
1131 | =
1132 |
1133 |
1134 | false
1135 |
1136 |
1137 | true
1138 |
1139 |
1140 |
1141 |
1142 |
1143 |
1144 |
1145 |
1146 |
1147 |
1148 | 600
1149 | 0
1150 |
1151 |
1152 |
1153 |
1154 |
1155 | true
1156 |
1157 |
1158 |
1159 | 0
1160 | 0
1161 |
1162 |
1163 |
1164 |
1165 | 600
1166 | 600
1167 |
1168 |
1169 |
1170 | true
1171 |
1172 |
1173 | false
1174 |
1175 |
1176 | QDockWidget::DockWidgetClosable|QDockWidget::DockWidgetMovable
1177 |
1178 |
1179 | Qt::TopDockWidgetArea
1180 |
1181 |
1182 | About
1183 |
1184 |
1185 | 4
1186 |
1187 |
1188 |
1189 |
1190 | 0
1191 | 0
1192 |
1193 |
1194 |
1195 | false
1196 |
1197 |
1198 | -
1199 |
1200 |
1201 | Qt::WheelFocus
1202 |
1203 |
1204 | false
1205 |
1206 |
1207 | color: white;
1208 |
1209 |
1210 | QFrame::StyledPanel
1211 |
1212 |
1213 | QFrame::Sunken
1214 |
1215 |
1216 |
1217 |
1218 |
1219 | false
1220 |
1221 |
1222 | true
1223 |
1224 |
1225 | true
1226 |
1227 |
1228 |
1229 |
1230 |
1231 |
1232 |
1233 |
1234 | true
1235 |
1236 |
1237 |
1238 | 550
1239 | 158
1240 |
1241 |
1242 |
1243 |
1244 | 12
1245 | true
1246 |
1247 |
1248 |
1249 | true
1250 |
1251 |
1252 | false
1253 |
1254 |
1255 | QDockWidget::DockWidgetClosable|QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable
1256 |
1257 |
1258 | Qt::BottomDockWidgetArea
1259 |
1260 |
1261 | Log
1262 |
1263 |
1264 | 8
1265 |
1266 |
1267 |
1268 |
1269 | 0
1270 | 0
1271 |
1272 |
1273 |
1274 | -
1275 |
1276 |
1277 | QLayout::SetDefaultConstraint
1278 |
1279 |
-
1280 |
1281 |
1282 |
1283 | 0
1284 | 0
1285 |
1286 |
1287 |
1288 |
1289 | 0
1290 | 0
1291 |
1292 |
1293 |
1294 | false
1295 |
1296 |
1297 | QFrame::StyledPanel
1298 |
1299 |
1300 | QFrame::Sunken
1301 |
1302 |
1303 | false
1304 |
1305 |
1306 | true
1307 |
1308 |
1309 |
1310 | -
1311 |
1312 |
1313 | QLayout::SetDefaultConstraint
1314 |
1315 |
-
1316 |
1317 |
1318 | Clear
1319 |
1320 |
1321 |
1322 | images/delete-sweep_24.pngimages/delete-sweep_24.png
1323 |
1324 |
1325 |
1326 |
1327 | 24
1328 | 24
1329 |
1330 |
1331 |
1332 |
1333 |
1334 |
1335 |
1336 |
1337 |
1338 |
1339 |
1340 |
1341 |
1342 | true
1343 |
1344 |
1345 |
1346 | 250
1347 | 600
1348 |
1349 |
1350 |
1351 | false
1352 |
1353 |
1354 | QDockWidget::DockWidgetClosable|QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable
1355 |
1356 |
1357 | Qt::RightDockWidgetArea
1358 |
1359 |
1360 | Configuration
1361 |
1362 |
1363 | 2
1364 |
1365 |
1366 |
1367 | -
1368 |
1369 |
1370 |
1371 |
1372 |
1373 |
1374 |
1375 |
1376 | 250
1377 | 600
1378 |
1379 |
1380 |
1381 | Qt::LeftDockWidgetArea
1382 |
1383 |
1384 | Plugins
1385 |
1386 |
1387 | 1
1388 |
1389 |
1390 |
1391 |
1392 |
1393 | &Quit
1394 |
1395 |
1396 | about_widget
1397 | log_widget
1398 |
1399 |
1400 |
1401 |
1402 |
--------------------------------------------------------------------------------
/gui/mainwindow_ui.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | ################################################################################
4 | ## Form generated from reading UI file 'mainwindow.ui'
5 | ##
6 | ## Created by: Qt User Interface Compiler version 6.4.2
7 | ##
8 | ## WARNING! All changes made in this file will be lost when recompiling UI file!
9 | ################################################################################
10 |
11 | from PySide6.QtCore import (QCoreApplication, QMetaObject, QRect,
12 | QSize, Qt)
13 | from PySide6.QtGui import (QAction, QCursor, QFont, QIcon)
14 | from PySide6.QtWidgets import (
15 | QCheckBox, QDial, QDockWidget,
16 | QFormLayout, QFrame, QGridLayout, QHBoxLayout,
17 | QLayout, QMainWindow, QPlainTextEdit, QPushButton,
18 | QRadioButton, QSizePolicy, QStatusBar, QTabWidget,
19 | QTextEdit, QToolButton, QVBoxLayout, QWidget)
20 |
21 | class Ui_MainWindow(object):
22 | def setupUi(self, MainWindow):
23 | if not MainWindow.objectName():
24 | MainWindow.setObjectName(u"MainWindow")
25 | MainWindow.setWindowModality(Qt.ApplicationModal)
26 | MainWindow.resize(1492, 1852)
27 | sizePolicy = QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)
28 | sizePolicy.setHorizontalStretch(0)
29 | sizePolicy.setVerticalStretch(0)
30 | sizePolicy.setHeightForWidth(MainWindow.sizePolicy().hasHeightForWidth())
31 | MainWindow.setSizePolicy(sizePolicy)
32 | MainWindow.setMinimumSize(QSize(700, 700))
33 | MainWindow.setMaximumSize(QSize(16777215, 16777215))
34 | MainWindow.setBaseSize(QSize(600, 600))
35 | font = QFont()
36 | font.setPointSize(12)
37 | font.setKerning(True)
38 | MainWindow.setFont(font)
39 | MainWindow.setAutoFillBackground(False)
40 | MainWindow.setStyleSheet(u"")
41 | MainWindow.setDocumentMode(False)
42 | MainWindow.setTabShape(QTabWidget.Rounded)
43 | MainWindow.setDockOptions(QMainWindow.AllowTabbedDocks|QMainWindow.AnimatedDocks)
44 | MainWindow.setUnifiedTitleAndToolBarOnMac(False)
45 | self.action_Quit = QAction(MainWindow)
46 | self.action_Quit.setObjectName(u"action_Quit")
47 | self.main_widget = QWidget(MainWindow)
48 | self.main_widget.setObjectName(u"main_widget")
49 | self.main_widget.setEnabled(True)
50 | self.main_widget.setMinimumSize(QSize(650, 650))
51 | self.main_widget.setContextMenuPolicy(Qt.DefaultContextMenu)
52 | self.main_widget.setAutoFillBackground(False)
53 | self.main_widget.setStyleSheet(u"")
54 | self.main_layout = QGridLayout(self.main_widget)
55 | self.main_layout.setObjectName(u"main_layout")
56 | self.horizontalLayout_2 = QHBoxLayout()
57 | self.horizontalLayout_2.setObjectName(u"horizontalLayout_2")
58 | self.log_button = QPushButton(self.main_widget)
59 | self.log_button.setObjectName(u"log_button")
60 | sizePolicy1 = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
61 | sizePolicy1.setHorizontalStretch(0)
62 | sizePolicy1.setVerticalStretch(0)
63 | sizePolicy1.setHeightForWidth(self.log_button.sizePolicy().hasHeightForWidth())
64 | self.log_button.setSizePolicy(sizePolicy1)
65 | self.log_button.setMinimumSize(QSize(24, 24))
66 | font1 = QFont()
67 | font1.setPointSize(9)
68 | font1.setBold(True)
69 | font1.setKerning(True)
70 | self.log_button.setFont(font1)
71 | self.log_button.setCursor(QCursor(Qt.PointingHandCursor))
72 | self.log_button.setCheckable(False)
73 | self.log_button.setFlat(True)
74 |
75 | self.horizontalLayout_2.addWidget(self.log_button)
76 |
77 |
78 | self.main_layout.addLayout(self.horizontalLayout_2, 5, 2, 1, 1)
79 |
80 | self.shuttle_widget = QWidget(self.main_widget)
81 | self.shuttle_widget.setObjectName(u"shuttle_widget")
82 | self.shuttle_widget.setMinimumSize(QSize(600, 600))
83 | self.shuttle_widget.setMaximumSize(QSize(600, 600))
84 | self.shuttle_widget.setAutoFillBackground(False)
85 | self.shuttle_widget.setStyleSheet(u"QWidget#shuttle_widget {background: url(images/ShuttleXpress_Black.png);\n"
86 | " background-repeat:no-repeat;}\n"
87 | " ")
88 | self.button_1 = QCheckBox(self.shuttle_widget)
89 | self.button_1.setObjectName(u"button_1")
90 | self.button_1.setGeometry(QRect(80, 266, 24, 24))
91 | sizePolicy1.setHeightForWidth(self.button_1.sizePolicy().hasHeightForWidth())
92 | self.button_1.setSizePolicy(sizePolicy1)
93 | self.button_1.setMinimumSize(QSize(24, 24))
94 | self.button_1.setFont(font)
95 | self.button_1.setStyleSheet(u"background: #000000ff;\n"
96 | " color: white;\n"
97 | " ")
98 | self.usb_status = QPushButton(self.shuttle_widget)
99 | self.usb_status.setObjectName(u"usb_status")
100 | self.usb_status.setEnabled(False)
101 | self.usb_status.setGeometry(QRect(288, 10, 24, 24))
102 | sizePolicy1.setHeightForWidth(self.usb_status.sizePolicy().hasHeightForWidth())
103 | self.usb_status.setSizePolicy(sizePolicy1)
104 | self.usb_status.setMinimumSize(QSize(24, 24))
105 | self.usb_status.setFont(font1)
106 | self.usb_status.setCursor(QCursor(Qt.ArrowCursor))
107 | self.usb_status.setAutoFillBackground(False)
108 | self.usb_status.setText(u"")
109 | icon = QIcon()
110 | icon.addFile(u"images/usb_black_24.png", QSize(), QIcon.Disabled, QIcon.Off)
111 | icon.addFile(u"images/usb_white_24.png", QSize(), QIcon.Disabled, QIcon.On)
112 | self.usb_status.setIcon(icon)
113 | self.usb_status.setIconSize(QSize(24, 24))
114 | self.usb_status.setCheckable(True)
115 | self.usb_status.setChecked(False)
116 | self.usb_status.setFlat(True)
117 | self.button_5 = QCheckBox(self.shuttle_widget)
118 | self.button_5.setObjectName(u"button_5")
119 | self.button_5.setGeometry(QRect(498, 266, 24, 24))
120 | sizePolicy1.setHeightForWidth(self.button_5.sizePolicy().hasHeightForWidth())
121 | self.button_5.setSizePolicy(sizePolicy1)
122 | self.button_5.setMinimumSize(QSize(24, 24))
123 | self.button_5.setFont(font)
124 | self.button_5.setLayoutDirection(Qt.RightToLeft)
125 | self.button_5.setStyleSheet(u"background: #000000ff;\n"
126 | " color: white;\n"
127 | " ")
128 | self.wheel_pos4 = QRadioButton(self.shuttle_widget)
129 | self.wheel_pos4.setObjectName(u"wheel_pos4")
130 | self.wheel_pos4.setGeometry(QRect(382, 164, 24, 24))
131 | sizePolicy1.setHeightForWidth(self.wheel_pos4.sizePolicy().hasHeightForWidth())
132 | self.wheel_pos4.setSizePolicy(sizePolicy1)
133 | self.wheel_pos4.setMinimumSize(QSize(24, 24))
134 | self.wheel_pos4.setStyleSheet(u"background: #000000ff;\n"
135 | " color: white;\n"
136 | " ")
137 | self.wheel_cent0 = QRadioButton(self.shuttle_widget)
138 | self.wheel_cent0.setObjectName(u"wheel_cent0")
139 | self.wheel_cent0.setGeometry(QRect(289, 130, 24, 24))
140 | sizePolicy1.setHeightForWidth(self.wheel_cent0.sizePolicy().hasHeightForWidth())
141 | self.wheel_cent0.setSizePolicy(sizePolicy1)
142 | self.wheel_cent0.setMinimumSize(QSize(24, 24))
143 | self.wheel_cent0.setStyleSheet(u"background: #000000ff;\n"
144 | " color: white;\n"
145 | " ")
146 | self.wheel_cent0.setChecked(True)
147 | self.wheel_pos1 = QRadioButton(self.shuttle_widget)
148 | self.wheel_pos1.setObjectName(u"wheel_pos1")
149 | self.wheel_pos1.setGeometry(QRect(314, 133, 24, 24))
150 | sizePolicy1.setHeightForWidth(self.wheel_pos1.sizePolicy().hasHeightForWidth())
151 | self.wheel_pos1.setSizePolicy(sizePolicy1)
152 | self.wheel_pos1.setMinimumSize(QSize(24, 24))
153 | self.wheel_pos1.setStyleSheet(u"color: white;")
154 | self.wheel_pos2 = QRadioButton(self.shuttle_widget)
155 | self.wheel_pos2.setObjectName(u"wheel_pos2")
156 | self.wheel_pos2.setGeometry(QRect(338, 139, 24, 24))
157 | sizePolicy1.setHeightForWidth(self.wheel_pos2.sizePolicy().hasHeightForWidth())
158 | self.wheel_pos2.setSizePolicy(sizePolicy1)
159 | self.wheel_pos2.setMinimumSize(QSize(24, 24))
160 | self.wheel_pos2.setStyleSheet(u"background: #000000ff;\n"
161 | " color: white;\n"
162 | " ")
163 | self.dial = QDial(self.shuttle_widget)
164 | self.dial.setObjectName(u"dial")
165 | self.dial.setGeometry(QRect(197, 178, 216, 216))
166 | sizePolicy2 = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred)
167 | sizePolicy2.setHorizontalStretch(0)
168 | sizePolicy2.setVerticalStretch(0)
169 | sizePolicy2.setHeightForWidth(self.dial.sizePolicy().hasHeightForWidth())
170 | self.dial.setSizePolicy(sizePolicy2)
171 | self.dial.setMinimumSize(QSize(24, 24))
172 | self.dial.setAutoFillBackground(False)
173 | self.dial.setStyleSheet(u"background-color: black;")
174 | self.dial.setMinimum(0)
175 | self.dial.setMaximum(10)
176 | self.dial.setPageStep(1)
177 | self.dial.setValue(5)
178 | self.dial.setSliderPosition(5)
179 | self.dial.setInvertedAppearance(False)
180 | self.dial.setInvertedControls(False)
181 | self.dial.setWrapping(True)
182 | self.dial.setNotchTarget(3.700000000000000)
183 | self.dial.setNotchesVisible(False)
184 | self.wheel_neg6 = QRadioButton(self.shuttle_widget)
185 | self.wheel_neg6.setObjectName(u"wheel_neg6")
186 | self.wheel_neg6.setGeometry(QRect(162, 204, 24, 24))
187 | sizePolicy1.setHeightForWidth(self.wheel_neg6.sizePolicy().hasHeightForWidth())
188 | self.wheel_neg6.setSizePolicy(sizePolicy1)
189 | self.wheel_neg6.setMinimumSize(QSize(24, 24))
190 | self.wheel_neg6.setStyleSheet(u"color: white;")
191 | self.wheel_pos5 = QRadioButton(self.shuttle_widget)
192 | self.wheel_pos5.setObjectName(u"wheel_pos5")
193 | self.wheel_pos5.setGeometry(QRect(400, 182, 24, 24))
194 | sizePolicy1.setHeightForWidth(self.wheel_pos5.sizePolicy().hasHeightForWidth())
195 | self.wheel_pos5.setSizePolicy(sizePolicy1)
196 | self.wheel_pos5.setMinimumSize(QSize(24, 24))
197 | self.wheel_pos5.setStyleSheet(u"background: #000000ff;\n"
198 | " color: white;\n"
199 | " ")
200 | self.button_2 = QCheckBox(self.shuttle_widget)
201 | self.button_2.setObjectName(u"button_2")
202 | self.button_2.setGeometry(QRect(156, 122, 24, 24))
203 | sizePolicy1.setHeightForWidth(self.button_2.sizePolicy().hasHeightForWidth())
204 | self.button_2.setSizePolicy(sizePolicy1)
205 | self.button_2.setMinimumSize(QSize(24, 24))
206 | self.button_2.setFont(font)
207 | self.button_2.setStyleSheet(u"background: #000000ff;\n"
208 | " color: white;\n"
209 | " ")
210 | self.wheel_neg5 = QRadioButton(self.shuttle_widget)
211 | self.wheel_neg5.setObjectName(u"wheel_neg5")
212 | self.wheel_neg5.setGeometry(QRect(178, 182, 24, 24))
213 | sizePolicy1.setHeightForWidth(self.wheel_neg5.sizePolicy().hasHeightForWidth())
214 | self.wheel_neg5.setSizePolicy(sizePolicy1)
215 | self.wheel_neg5.setMinimumSize(QSize(24, 24))
216 | self.wheel_neg5.setStyleSheet(u"color: white;")
217 | self.wheel_pos6 = QRadioButton(self.shuttle_widget)
218 | self.wheel_pos6.setObjectName(u"wheel_pos6")
219 | self.wheel_pos6.setGeometry(QRect(416, 204, 24, 24))
220 | sizePolicy1.setHeightForWidth(self.wheel_pos6.sizePolicy().hasHeightForWidth())
221 | self.wheel_pos6.setSizePolicy(sizePolicy1)
222 | self.wheel_pos6.setMinimumSize(QSize(24, 24))
223 | self.wheel_pos6.setStyleSheet(u"background: #000000ff;\n"
224 | " color: white;\n"
225 | " ")
226 | self.wheel_neg1 = QRadioButton(self.shuttle_widget)
227 | self.wheel_neg1.setObjectName(u"wheel_neg1")
228 | self.wheel_neg1.setGeometry(QRect(264, 133, 24, 24))
229 | sizePolicy1.setHeightForWidth(self.wheel_neg1.sizePolicy().hasHeightForWidth())
230 | self.wheel_neg1.setSizePolicy(sizePolicy1)
231 | self.wheel_neg1.setMinimumSize(QSize(24, 24))
232 | self.wheel_neg1.setStyleSheet(u"background: #000000ff;\n"
233 | " color: white;\n"
234 | " ")
235 | self.button_4 = QCheckBox(self.shuttle_widget)
236 | self.button_4.setObjectName(u"button_4")
237 | self.button_4.setGeometry(QRect(430, 122, 24, 24))
238 | sizePolicy1.setHeightForWidth(self.button_4.sizePolicy().hasHeightForWidth())
239 | self.button_4.setSizePolicy(sizePolicy1)
240 | self.button_4.setMinimumSize(QSize(24, 24))
241 | self.button_4.setFont(font)
242 | self.button_4.setLayoutDirection(Qt.RightToLeft)
243 | self.button_4.setAutoFillBackground(False)
244 | self.button_4.setStyleSheet(u"background: #000000ff;\n"
245 | " color: white;\n"
246 | " ")
247 | self.wheel_pos7 = QRadioButton(self.shuttle_widget)
248 | self.wheel_pos7.setObjectName(u"wheel_pos7")
249 | self.wheel_pos7.setGeometry(QRect(427, 229, 24, 24))
250 | sizePolicy1.setHeightForWidth(self.wheel_pos7.sizePolicy().hasHeightForWidth())
251 | self.wheel_pos7.setSizePolicy(sizePolicy1)
252 | self.wheel_pos7.setMinimumSize(QSize(24, 24))
253 | self.wheel_pos7.setStyleSheet(u"background: #000000ff;\n"
254 | " color: white;\n"
255 | " ")
256 | self.button_3 = QCheckBox(self.shuttle_widget)
257 | self.button_3.setObjectName(u"button_3")
258 | self.button_3.setGeometry(QRect(290, 72, 24, 24))
259 | sizePolicy1.setHeightForWidth(self.button_3.sizePolicy().hasHeightForWidth())
260 | self.button_3.setSizePolicy(sizePolicy1)
261 | self.button_3.setMinimumSize(QSize(24, 24))
262 | self.button_3.setFont(font)
263 | self.button_3.setStyleSheet(u"background: #000000ff;\n"
264 | " color: white;\n"
265 | " ")
266 | self.button_3.setTristate(False)
267 | self.wheel_neg2 = QRadioButton(self.shuttle_widget)
268 | self.wheel_neg2.setObjectName(u"wheel_neg2")
269 | self.wheel_neg2.setGeometry(QRect(240, 139, 24, 24))
270 | sizePolicy1.setHeightForWidth(self.wheel_neg2.sizePolicy().hasHeightForWidth())
271 | self.wheel_neg2.setSizePolicy(sizePolicy1)
272 | self.wheel_neg2.setMinimumSize(QSize(24, 24))
273 | self.wheel_neg2.setStyleSheet(u"background: #000000ff;\n"
274 | " color: white;\n"
275 | " ")
276 | self.wheel_pos3 = QRadioButton(self.shuttle_widget)
277 | self.wheel_pos3.setObjectName(u"wheel_pos3")
278 | self.wheel_pos3.setGeometry(QRect(361, 149, 24, 24))
279 | sizePolicy1.setHeightForWidth(self.wheel_pos3.sizePolicy().hasHeightForWidth())
280 | self.wheel_pos3.setSizePolicy(sizePolicy1)
281 | self.wheel_pos3.setMinimumSize(QSize(24, 24))
282 | self.wheel_pos3.setStyleSheet(u"background: #000000ff;\n"
283 | " color: white;\n"
284 | " ")
285 | self.wheel_neg3 = QRadioButton(self.shuttle_widget)
286 | self.wheel_neg3.setObjectName(u"wheel_neg3")
287 | self.wheel_neg3.setGeometry(QRect(217, 149, 24, 24))
288 | sizePolicy1.setHeightForWidth(self.wheel_neg3.sizePolicy().hasHeightForWidth())
289 | self.wheel_neg3.setSizePolicy(sizePolicy1)
290 | self.wheel_neg3.setMinimumSize(QSize(24, 24))
291 | self.wheel_neg3.setStyleSheet(u"color: white;")
292 | self.wheel_neg4 = QRadioButton(self.shuttle_widget)
293 | self.wheel_neg4.setObjectName(u"wheel_neg4")
294 | self.wheel_neg4.setGeometry(QRect(196, 164, 24, 24))
295 | sizePolicy1.setHeightForWidth(self.wheel_neg4.sizePolicy().hasHeightForWidth())
296 | self.wheel_neg4.setSizePolicy(sizePolicy1)
297 | self.wheel_neg4.setMinimumSize(QSize(24, 24))
298 | self.wheel_neg4.setStyleSheet(u"background: #000000ff;\n"
299 | " color: white;\n"
300 | " ")
301 | self.wheel_neg7 = QRadioButton(self.shuttle_widget)
302 | self.wheel_neg7.setObjectName(u"wheel_neg7")
303 | self.wheel_neg7.setGeometry(QRect(151, 229, 24, 24))
304 | sizePolicy1.setHeightForWidth(self.wheel_neg7.sizePolicy().hasHeightForWidth())
305 | self.wheel_neg7.setSizePolicy(sizePolicy1)
306 | self.wheel_neg7.setMinimumSize(QSize(24, 24))
307 | self.wheel_neg7.setStyleSheet(u"color: white;")
308 | self.dial.raise_()
309 | self.button_1.raise_()
310 | self.usb_status.raise_()
311 | self.button_5.raise_()
312 | self.wheel_pos4.raise_()
313 | self.wheel_cent0.raise_()
314 | self.wheel_pos1.raise_()
315 | self.wheel_pos2.raise_()
316 | self.wheel_neg6.raise_()
317 | self.wheel_pos5.raise_()
318 | self.button_2.raise_()
319 | self.wheel_neg5.raise_()
320 | self.wheel_pos6.raise_()
321 | self.wheel_neg1.raise_()
322 | self.button_4.raise_()
323 | self.wheel_pos7.raise_()
324 | self.button_3.raise_()
325 | self.wheel_neg2.raise_()
326 | self.wheel_pos3.raise_()
327 | self.wheel_neg3.raise_()
328 | self.wheel_neg4.raise_()
329 | self.wheel_neg7.raise_()
330 |
331 | self.main_layout.addWidget(self.shuttle_widget, 3, 2, 1, 1)
332 |
333 | self.horizontalLayout = QHBoxLayout()
334 | self.horizontalLayout.setObjectName(u"horizontalLayout")
335 | self.about_button = QPushButton(self.main_widget)
336 | self.about_button.setObjectName(u"about_button")
337 | sizePolicy1.setHeightForWidth(self.about_button.sizePolicy().hasHeightForWidth())
338 | self.about_button.setSizePolicy(sizePolicy1)
339 | self.about_button.setMinimumSize(QSize(24, 24))
340 | self.about_button.setFont(font1)
341 | self.about_button.setCursor(QCursor(Qt.WhatsThisCursor))
342 | self.about_button.setCheckable(False)
343 | self.about_button.setFlat(True)
344 |
345 | self.horizontalLayout.addWidget(self.about_button)
346 |
347 |
348 | self.main_layout.addLayout(self.horizontalLayout, 1, 2, 1, 1)
349 |
350 | self.horizontalLayout_3 = QHBoxLayout()
351 | self.horizontalLayout_3.setObjectName(u"horizontalLayout_3")
352 | self.plug_button = QPushButton(self.main_widget)
353 | self.plug_button.setObjectName(u"plug_button")
354 | sizePolicy1.setHeightForWidth(self.plug_button.sizePolicy().hasHeightForWidth())
355 | self.plug_button.setSizePolicy(sizePolicy1)
356 | self.plug_button.setMinimumSize(QSize(24, 24))
357 | self.plug_button.setFont(font1)
358 | self.plug_button.setCursor(QCursor(Qt.PointingHandCursor))
359 | self.plug_button.setCheckable(False)
360 | self.plug_button.setFlat(True)
361 |
362 | self.horizontalLayout_3.addWidget(self.plug_button)
363 |
364 |
365 | self.main_layout.addLayout(self.horizontalLayout_3, 3, 1, 1, 1)
366 |
367 | self.verticalLayout = QVBoxLayout()
368 | self.verticalLayout.setObjectName(u"verticalLayout")
369 | self.conf_button = QPushButton(self.main_widget)
370 | self.conf_button.setObjectName(u"conf_button")
371 | sizePolicy1.setHeightForWidth(self.conf_button.sizePolicy().hasHeightForWidth())
372 | self.conf_button.setSizePolicy(sizePolicy1)
373 | self.conf_button.setMinimumSize(QSize(24, 24))
374 | self.conf_button.setFont(font1)
375 | self.conf_button.setCursor(QCursor(Qt.PointingHandCursor))
376 | self.conf_button.setCheckable(False)
377 | self.conf_button.setFlat(True)
378 |
379 | self.verticalLayout.addWidget(self.conf_button)
380 |
381 |
382 | self.main_layout.addLayout(self.verticalLayout, 3, 4, 1, 1)
383 |
384 | MainWindow.setCentralWidget(self.main_widget)
385 | self.statusbar = QStatusBar(MainWindow)
386 | self.statusbar.setObjectName(u"statusbar")
387 | self.statusbar.setMinimumSize(QSize(600, 0))
388 | MainWindow.setStatusBar(self.statusbar)
389 | self.about_widget = QDockWidget(MainWindow)
390 | self.about_widget.setObjectName(u"about_widget")
391 | self.about_widget.setEnabled(True)
392 | sizePolicy3 = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
393 | sizePolicy3.setHorizontalStretch(0)
394 | sizePolicy3.setVerticalStretch(0)
395 | sizePolicy3.setHeightForWidth(self.about_widget.sizePolicy().hasHeightForWidth())
396 | self.about_widget.setSizePolicy(sizePolicy3)
397 | self.about_widget.setMinimumSize(QSize(600, 600))
398 | self.about_widget.setVisible(True)
399 | self.about_widget.setFloating(False)
400 | self.about_widget.setFeatures(QDockWidget.DockWidgetClosable|QDockWidget.DockWidgetMovable)
401 | self.about_widget.setAllowedAreas(Qt.TopDockWidgetArea)
402 | self.about_layout = QWidget()
403 | self.about_layout.setObjectName(u"about_layout")
404 | sizePolicy3.setHeightForWidth(self.about_layout.sizePolicy().hasHeightForWidth())
405 | self.about_layout.setSizePolicy(sizePolicy3)
406 | self.about_layout.setAutoFillBackground(False)
407 | self.gridLayout_3 = QGridLayout(self.about_layout)
408 | self.gridLayout_3.setObjectName(u"gridLayout_3")
409 | self.about_text = QTextEdit(self.about_layout)
410 | self.about_text.setObjectName(u"about_text")
411 | self.about_text.setFocusPolicy(Qt.WheelFocus)
412 | self.about_text.setAcceptDrops(False)
413 | self.about_text.setStyleSheet(u"color: white;")
414 | self.about_text.setFrameShape(QFrame.StyledPanel)
415 | self.about_text.setFrameShadow(QFrame.Sunken)
416 | self.about_text.setUndoRedoEnabled(False)
417 | self.about_text.setReadOnly(True)
418 | self.about_text.setAcceptRichText(True)
419 |
420 | self.gridLayout_3.addWidget(self.about_text, 0, 0, 1, 1)
421 |
422 | self.about_widget.setWidget(self.about_layout)
423 | MainWindow.addDockWidget(Qt.TopDockWidgetArea, self.about_widget)
424 | self.log_widget = QDockWidget(MainWindow)
425 | self.log_widget.setObjectName(u"log_widget")
426 | self.log_widget.setEnabled(True)
427 | self.log_widget.setMinimumSize(QSize(550, 158))
428 | self.log_widget.setFont(font)
429 | self.log_widget.setVisible(True)
430 | self.log_widget.setFloating(False)
431 | self.log_widget.setFeatures(QDockWidget.DockWidgetClosable|QDockWidget.DockWidgetFloatable|QDockWidget.DockWidgetMovable)
432 | self.log_widget.setAllowedAreas(Qt.BottomDockWidgetArea)
433 | self.log_layout = QWidget()
434 | self.log_layout.setObjectName(u"log_layout")
435 | sizePolicy4 = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
436 | sizePolicy4.setHorizontalStretch(0)
437 | sizePolicy4.setVerticalStretch(0)
438 | sizePolicy4.setHeightForWidth(self.log_layout.sizePolicy().hasHeightForWidth())
439 | self.log_layout.setSizePolicy(sizePolicy4)
440 | self.gridLayout_2 = QGridLayout(self.log_layout)
441 | self.gridLayout_2.setObjectName(u"gridLayout_2")
442 | self.log_content_layout = QVBoxLayout()
443 | self.log_content_layout.setObjectName(u"log_content_layout")
444 | self.log_content_layout.setSizeConstraint(QLayout.SetDefaultConstraint)
445 | self.log_text = QPlainTextEdit(self.log_layout)
446 | self.log_text.setObjectName(u"log_text")
447 | sizePolicy4.setHeightForWidth(self.log_text.sizePolicy().hasHeightForWidth())
448 | self.log_text.setSizePolicy(sizePolicy4)
449 | self.log_text.setMinimumSize(QSize(0, 0))
450 | self.log_text.setAcceptDrops(False)
451 | self.log_text.setFrameShape(QFrame.StyledPanel)
452 | self.log_text.setFrameShadow(QFrame.Sunken)
453 | self.log_text.setUndoRedoEnabled(False)
454 | self.log_text.setReadOnly(True)
455 |
456 | self.log_content_layout.addWidget(self.log_text)
457 |
458 | self.buttons_layout = QHBoxLayout()
459 | self.buttons_layout.setObjectName(u"buttons_layout")
460 | self.buttons_layout.setSizeConstraint(QLayout.SetDefaultConstraint)
461 | self.log_clear_button = QToolButton(self.log_layout)
462 | self.log_clear_button.setObjectName(u"log_clear_button")
463 | icon1 = QIcon()
464 | icon1.addFile(u"images/delete-sweep_24.png", QSize(), QIcon.Normal, QIcon.Off)
465 | self.log_clear_button.setIcon(icon1)
466 | self.log_clear_button.setIconSize(QSize(24, 24))
467 |
468 | self.buttons_layout.addWidget(self.log_clear_button)
469 |
470 |
471 | self.log_content_layout.addLayout(self.buttons_layout)
472 |
473 |
474 | self.gridLayout_2.addLayout(self.log_content_layout, 0, 0, 1, 1)
475 |
476 | self.log_widget.setWidget(self.log_layout)
477 | MainWindow.addDockWidget(Qt.BottomDockWidgetArea, self.log_widget)
478 | self.config_widget = QDockWidget(MainWindow)
479 | self.config_widget.setObjectName(u"config_widget")
480 | self.config_widget.setEnabled(True)
481 | self.config_widget.setMinimumSize(QSize(250, 600))
482 | self.config_widget.setFloating(False)
483 | self.config_widget.setFeatures(QDockWidget.DockWidgetClosable|QDockWidget.DockWidgetFloatable|QDockWidget.DockWidgetMovable)
484 | self.config_widget.setAllowedAreas(Qt.RightDockWidgetArea)
485 | self.config_layout = QWidget()
486 | self.config_layout.setObjectName(u"config_layout")
487 | self.gridLayout = QGridLayout(self.config_layout)
488 | self.gridLayout.setObjectName(u"gridLayout")
489 | self.config_content_layout = QFormLayout()
490 | self.config_content_layout.setObjectName(u"config_content_layout")
491 |
492 | self.gridLayout.addLayout(self.config_content_layout, 0, 0, 1, 1)
493 |
494 | self.config_widget.setWidget(self.config_layout)
495 | MainWindow.addDockWidget(Qt.RightDockWidgetArea, self.config_widget)
496 | self.plugins_widget = QDockWidget(MainWindow)
497 | self.plugins_widget.setObjectName(u"plugins_widget")
498 | self.plugins_widget.setMinimumSize(QSize(250, 600))
499 | self.plugins_widget.setAllowedAreas(Qt.LeftDockWidgetArea)
500 | self.dockWidgetContents = QWidget()
501 | self.dockWidgetContents.setObjectName(u"dockWidgetContents")
502 | self.plugins_widget.setWidget(self.dockWidgetContents)
503 | MainWindow.addDockWidget(Qt.LeftDockWidgetArea, self.plugins_widget)
504 | self.about_widget.raise_()
505 | self.log_widget.raise_()
506 |
507 | self.retranslateUi(MainWindow)
508 |
509 | self.usb_status.setDefault(False)
510 |
511 |
512 | QMetaObject.connectSlotsByName(MainWindow)
513 | # setupUi
514 |
515 | def retranslateUi(self, MainWindow):
516 | MainWindow.setWindowTitle(
517 | QCoreApplication.translate(
518 | "MainWindow", u"Open Contour Shuttle", None
519 | )
520 | )
521 | self.action_Quit.setText(QCoreApplication.translate("MainWindow", u"&Quit", None))
522 | #if QT_CONFIG(tooltip)
523 | self.log_button.setToolTip("")
524 | #endif // QT_CONFIG(tooltip)
525 | #if QT_CONFIG(statustip)
526 | self.log_button.setStatusTip(QCoreApplication.translate("MainWindow", u"Log", None))
527 | #endif // QT_CONFIG(statustip)
528 | self.log_button.setText(QCoreApplication.translate("MainWindow", u"//", None))
529 | #if QT_CONFIG(statustip)
530 | self.button_1.setStatusTip(QCoreApplication.translate("MainWindow", u"Button 1", None))
531 | #endif // QT_CONFIG(statustip)
532 | self.button_1.setText("")
533 | #if QT_CONFIG(tooltip)
534 | self.usb_status.setToolTip("")
535 | #endif // QT_CONFIG(tooltip)
536 | #if QT_CONFIG(statustip)
537 | self.usb_status.setStatusTip(QCoreApplication.translate("MainWindow", u"USB connection status", None))
538 | #endif // QT_CONFIG(statustip)
539 | #if QT_CONFIG(whatsthis)
540 | self.usb_status.setWhatsThis(QCoreApplication.translate("MainWindow", u"USB Status", None))
541 | #endif // QT_CONFIG(whatsthis)
542 | #if QT_CONFIG(statustip)
543 | self.button_5.setStatusTip(QCoreApplication.translate("MainWindow", u"Button 5", None))
544 | #endif // QT_CONFIG(statustip)
545 | self.button_5.setText("")
546 | #if QT_CONFIG(statustip)
547 | self.wheel_pos4.setStatusTip(QCoreApplication.translate("MainWindow", u"Wheel position: right 4", None))
548 | #endif // QT_CONFIG(statustip)
549 | self.wheel_pos4.setText("")
550 | #if QT_CONFIG(statustip)
551 | self.wheel_cent0.setStatusTip(QCoreApplication.translate("MainWindow", u"Wheel position: center", None))
552 | #endif // QT_CONFIG(statustip)
553 | self.wheel_cent0.setText("")
554 | #if QT_CONFIG(statustip)
555 | self.wheel_pos1.setStatusTip(QCoreApplication.translate("MainWindow", u"Wheel position: right 1", None))
556 | #endif // QT_CONFIG(statustip)
557 | self.wheel_pos1.setText("")
558 | #if QT_CONFIG(statustip)
559 | self.wheel_pos2.setStatusTip(QCoreApplication.translate("MainWindow", u"Wheel position: right 2", None))
560 | #endif // QT_CONFIG(statustip)
561 | self.wheel_pos2.setText("")
562 | #if QT_CONFIG(statustip)
563 | self.dial.setStatusTip(QCoreApplication.translate("MainWindow", u"Dial", None))
564 | #endif // QT_CONFIG(statustip)
565 | #if QT_CONFIG(statustip)
566 | self.wheel_neg6.setStatusTip(QCoreApplication.translate("MainWindow", u"Wheel position: left 6", None))
567 | #endif // QT_CONFIG(statustip)
568 | self.wheel_neg6.setText("")
569 | #if QT_CONFIG(statustip)
570 | self.wheel_pos5.setStatusTip(QCoreApplication.translate("MainWindow", u"Wheel position: right 5", None))
571 | #endif // QT_CONFIG(statustip)
572 | self.wheel_pos5.setText("")
573 | #if QT_CONFIG(statustip)
574 | self.button_2.setStatusTip(QCoreApplication.translate("MainWindow", u"Button 2", None))
575 | #endif // QT_CONFIG(statustip)
576 | self.button_2.setText("")
577 | #if QT_CONFIG(statustip)
578 | self.wheel_neg5.setStatusTip(QCoreApplication.translate("MainWindow", u"Wheel position: left 5", None))
579 | #endif // QT_CONFIG(statustip)
580 | self.wheel_neg5.setText("")
581 | #if QT_CONFIG(statustip)
582 | self.wheel_pos6.setStatusTip(QCoreApplication.translate("MainWindow", u"Wheel position: right 6", None))
583 | #endif // QT_CONFIG(statustip)
584 | self.wheel_pos6.setText("")
585 | #if QT_CONFIG(statustip)
586 | self.wheel_neg1.setStatusTip(QCoreApplication.translate("MainWindow", u"Wheel position: left 1", None))
587 | #endif // QT_CONFIG(statustip)
588 | self.wheel_neg1.setText("")
589 | #if QT_CONFIG(statustip)
590 | self.button_4.setStatusTip(QCoreApplication.translate("MainWindow", u"Button 4", None))
591 | #endif // QT_CONFIG(statustip)
592 | self.button_4.setText("")
593 | #if QT_CONFIG(statustip)
594 | self.wheel_pos7.setStatusTip(QCoreApplication.translate("MainWindow", u"Wheel position: right 7", None))
595 | #endif // QT_CONFIG(statustip)
596 | self.wheel_pos7.setText("")
597 | #if QT_CONFIG(statustip)
598 | self.button_3.setStatusTip(QCoreApplication.translate("MainWindow", u"Button 3", None))
599 | #endif // QT_CONFIG(statustip)
600 | self.button_3.setText("")
601 | #if QT_CONFIG(statustip)
602 | self.wheel_neg2.setStatusTip(QCoreApplication.translate("MainWindow", u"Wheel position: left 2", None))
603 | #endif // QT_CONFIG(statustip)
604 | self.wheel_neg2.setText("")
605 | #if QT_CONFIG(statustip)
606 | self.wheel_pos3.setStatusTip(QCoreApplication.translate("MainWindow", u"Wheel position: right 3", None))
607 | #endif // QT_CONFIG(statustip)
608 | self.wheel_pos3.setText("")
609 | #if QT_CONFIG(statustip)
610 | self.wheel_neg3.setStatusTip(QCoreApplication.translate("MainWindow", u"Wheel position: left 3", None))
611 | #endif // QT_CONFIG(statustip)
612 | self.wheel_neg3.setText("")
613 | #if QT_CONFIG(statustip)
614 | self.wheel_neg4.setStatusTip(QCoreApplication.translate("MainWindow", u"Wheel position: left 4", None))
615 | #endif // QT_CONFIG(statustip)
616 | self.wheel_neg4.setText(QCoreApplication.translate("MainWindow", u"-", None))
617 | #if QT_CONFIG(statustip)
618 | self.wheel_neg7.setStatusTip(QCoreApplication.translate("MainWindow", u"Wheel position: left 7", None))
619 | #endif // QT_CONFIG(statustip)
620 | self.wheel_neg7.setText("")
621 | #if QT_CONFIG(tooltip)
622 | self.about_button.setToolTip("")
623 | #endif // QT_CONFIG(tooltip)
624 | #if QT_CONFIG(statustip)
625 | self.about_button.setStatusTip(QCoreApplication.translate("MainWindow", u"About", None))
626 | #endif // QT_CONFIG(statustip)
627 | self.about_button.setText(QCoreApplication.translate("MainWindow", u"?", None))
628 | #if QT_CONFIG(tooltip)
629 | self.plug_button.setToolTip("")
630 | #endif // QT_CONFIG(tooltip)
631 | #if QT_CONFIG(statustip)
632 | self.plug_button.setStatusTip(QCoreApplication.translate("MainWindow", u"Configuration", None))
633 | #endif // QT_CONFIG(statustip)
634 | self.plug_button.setText(QCoreApplication.translate("MainWindow", u"#", None))
635 | #if QT_CONFIG(tooltip)
636 | self.conf_button.setToolTip("")
637 | #endif // QT_CONFIG(tooltip)
638 | #if QT_CONFIG(statustip)
639 | self.conf_button.setStatusTip(QCoreApplication.translate("MainWindow", u"Configuration", None))
640 | #endif // QT_CONFIG(statustip)
641 | self.conf_button.setText(QCoreApplication.translate("MainWindow", u"=", None))
642 | self.about_widget.setWindowTitle(QCoreApplication.translate("MainWindow", u"About", None))
643 | self.about_text.setDocumentTitle("")
644 | self.log_widget.setWindowTitle(QCoreApplication.translate("MainWindow", u"Log", None))
645 | self.log_clear_button.setText(QCoreApplication.translate("MainWindow", u"Clear", None))
646 | self.config_widget.setWindowTitle(QCoreApplication.translate("MainWindow", u"Configuration", None))
647 | self.plugins_widget.setWindowTitle(QCoreApplication.translate("MainWindow", u"Plugins", None))
648 | # retranslateUi
649 |
650 |
--------------------------------------------------------------------------------
/plugins/__init__.py:
--------------------------------------------------------------------------------
1 | """Mediator"""
2 |
3 | from __future__ import annotations
4 |
5 | from abc import ABC
6 |
7 |
8 | class ShuttleMediator(ABC):
9 | """
10 | The Mediator interface declares a method used by components to notify the
11 | mediator about various events. The Mediator may react to these events and
12 | pass the execution to other components.
13 | """
14 |
15 | def notify(self, sender: object, event: str) -> None:
16 | pass
17 |
18 |
19 | class ShuttleBroker(ShuttleMediator):
20 | def __init__(self, component1: ShuttlePluginSample1, component2: ShuttlePluginSample2) -> None:
21 | self._component1 = component1
22 | self._component1.mediator = self
23 | self._component2 = component2
24 | self._component2.mediator = self
25 |
26 | def notify(self, sender: object, event: str) -> None:
27 | if event == "A":
28 | print("Mediator reacts on A and triggers following operations:")
29 | self._component2.do_c()
30 | elif event == "D":
31 | print("Mediator reacts on D and triggers following operations:")
32 | self._component1.do_b()
33 | self._component2.do_c()
34 |
35 |
36 | class ShuttlePlugin:
37 | """
38 | The Base Component provides the basic functionality of storing a mediator's
39 | instance inside component objects.
40 | """
41 |
42 | def __init__(self, mediator: ShuttleMediator = None) -> None:
43 | self._mediator = mediator
44 |
45 | @property
46 | def mediator(self) -> ShuttleMediator:
47 | return self._mediator
48 |
49 | @mediator.setter
50 | def mediator(self, mediator: ShuttleMediator) -> None:
51 | self._mediator = mediator
52 |
53 |
54 | """
55 | Concrete Components implement various functionality. They don't depend on other
56 | components. They also don't depend on any concrete mediator classes.
57 | """
58 |
59 |
60 | class ShuttlePluginSample1(ShuttlePlugin):
61 | def do_a(self) -> None:
62 | print("Component 1 does A.")
63 | self.mediator.notify(self, "A")
64 |
65 | def do_b(self) -> None:
66 | print("Component 1 does B.")
67 | self.mediator.notify(self, "B")
68 |
69 |
70 | class ShuttlePluginSample2(ShuttlePlugin):
71 | def do_c(self) -> None:
72 | print("Component 2 does C.")
73 | self.mediator.notify(self, "C")
74 |
75 | def do_d(self) -> None:
76 | print("Component 2 does D.")
77 | self.mediator.notify(self, "D")
78 |
79 |
80 | if __name__ == "__main__":
81 | # The client code.
82 | c1 = ShuttlePluginSample1()
83 | c2 = ShuttlePluginSample2()
84 | mediator = ShuttleBroker(c1, c2)
85 |
86 | print("Client triggers operation A.")
87 | c1.do_a()
88 |
89 | print("\n", end="")
90 |
91 | print("Client triggers operation D.")
92 | c2.do_d()
93 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | hidapi
2 | PySide6
3 | qt-material
4 | pyobjc-framework-Cocoa; sys_platform == 'darwin'
--------------------------------------------------------------------------------