Compare commits
627 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
edc563e0e4 | ||
|
|
969ca59948 | ||
|
|
53abd22176 | ||
|
|
f449b37bda | ||
|
|
c97f4082dd | ||
|
|
5e6761c61b | ||
|
|
b2c87b4f80 | ||
|
|
58fcb074ff | ||
|
|
dbff65a4d1 | ||
|
|
a7ac589692 | ||
|
|
3d4570cd1f | ||
|
|
b6bc35ed3e | ||
|
|
5d4d0c07ad | ||
|
|
15cb97c56e | ||
|
|
f902b32755 | ||
|
|
fcfc7cd6fc | ||
|
|
c3718d22d6 | ||
|
|
2829acd6f5 | ||
|
|
c35ae8ffc9 | ||
|
|
f7159e9338 | ||
|
|
4074fb7750 | ||
|
|
444169d84b | ||
|
|
5d5f97a776 | ||
|
|
a8c5d3bc24 | ||
|
|
dd2a18d60e | ||
|
|
a3f9860181 | ||
|
|
1fed000855 | ||
|
|
61dfefde94 | ||
|
|
454fbcbc6e | ||
|
|
8112e90ded | ||
|
|
a4c0ad3d73 | ||
|
|
30f2afacb7 | ||
|
|
2a27c1d14e | ||
|
|
cccf4ec428 | ||
|
|
38c76393b1 | ||
|
|
ce9b10dc2d | ||
|
|
13e89dc219 | ||
|
|
16af3a9acc | ||
|
|
c690b2c9e9 | ||
|
|
43ab629a26 | ||
|
|
b83a14e16f | ||
|
|
a966bbff6f | ||
|
|
141a5c3dee | ||
|
|
994bb07bc4 | ||
|
|
24f658d8fe | ||
|
|
49d77a2f90 | ||
|
|
876cf35954 | ||
|
|
46c0448854 | ||
|
|
d78c7ed9be | ||
|
|
51d0ccf654 | ||
|
|
9c8252b59f | ||
|
|
488a78cc52 | ||
|
|
0e6d088372 | ||
|
|
d4d1a4f8e2 | ||
|
|
34801bfd1e | ||
|
|
948b4c4d51 | ||
|
|
85efe92d30 | ||
|
|
b01636e05d | ||
|
|
c0a7838429 | ||
|
|
eda4112501 | ||
|
|
dbb9025e26 | ||
|
|
beee0bc8c2 | ||
|
|
1bd6fd512b | ||
|
|
ab8421b2f2 | ||
|
|
1be56dd072 | ||
|
|
c023f81f1a | ||
|
|
47f64d40de | ||
|
|
d0b8233c9a | ||
|
|
39534d11dd | ||
|
|
ca2f5f927c | ||
|
|
2ab040591c | ||
|
|
8b26759731 | ||
|
|
0e43287386 | ||
|
|
7221ceb7b4 | ||
|
|
66416ff4cc | ||
|
|
0d89d1e6c8 | ||
|
|
81765ee971 | ||
|
|
b72273b2a9 | ||
|
|
e697a2d6cf | ||
|
|
94d07eed53 | ||
|
|
64ce3872b4 | ||
|
|
508444526a | ||
|
|
faa6de3b75 | ||
|
|
77d2ae5249 | ||
|
|
e2f94a9e0e | ||
|
|
ba55e4d9bd | ||
|
|
8e9ff939f3 | ||
|
|
bd3198f6f0 | ||
|
|
3319feeb73 | ||
|
|
47bcd4478c | ||
|
|
065b8628d2 | ||
|
|
87e2f1cad7 | ||
|
|
373b07f271 | ||
|
|
58f598fc88 | ||
|
|
8f44c294d7 | ||
|
|
f65079ad6d | ||
|
|
edce56cd20 | ||
|
|
cd437c3809 | ||
|
|
007cbef086 | ||
|
|
a03cc9ce64 | ||
|
|
3275aa4c12 | ||
|
|
a0f880fbba | ||
|
|
833f087fdf | ||
|
|
9a2a28e0aa | ||
|
|
bf5fc42f07 | ||
|
|
fb495fd2fc | ||
|
|
90ab7af069 | ||
|
|
0914e4c48d | ||
|
|
3436055c7f | ||
|
|
20aa797e96 | ||
|
|
5ebc2634ae | ||
|
|
94464a40b8 | ||
|
|
1194abf125 | ||
|
|
ceba698678 | ||
|
|
b1e9480f5a | ||
|
|
eaa38c858c | ||
|
|
4504045bc6 | ||
|
|
a9ea69b9a1 | ||
|
|
9a8d089c73 | ||
|
|
0d56cfbded | ||
|
|
9896875180 | ||
|
|
a1c04ce39f | ||
|
|
da541e000a | ||
|
|
e490162e4f | ||
|
|
a0b7d39f83 | ||
|
|
c464e049bf | ||
|
|
daad9b5d5f | ||
|
|
2682722cb1 | ||
|
|
8d5718178f | ||
|
|
1d438f098f | ||
|
|
1d305db29e | ||
|
|
c2a549c114 | ||
|
|
f43e298ac7 | ||
|
|
c25b769578 | ||
|
|
0cf1f1983d | ||
|
|
39ec568ab1 | ||
|
|
5b94ebae34 | ||
|
|
a857b5fc43 | ||
|
|
eb8791ec47 | ||
|
|
ff6da43007 | ||
|
|
d42fdf8a4a | ||
|
|
adab25ad44 | ||
|
|
90b057938b | ||
|
|
fc5c72e164 | ||
|
|
bd5c03812f | ||
|
|
6b0b7ea823 | ||
|
|
dd24ed5fe0 | ||
|
|
f64942b51d | ||
|
|
375db9996f | ||
|
|
20dfc063ff | ||
|
|
70c341e268 | ||
|
|
dc59c79bd3 | ||
|
|
422cd26b92 | ||
|
|
cec892ce88 | ||
|
|
d5374b9f51 | ||
|
|
51e2d965cf | ||
|
|
a6c8f2212a | ||
|
|
b3aac40246 | ||
|
|
fb138b77ff | ||
|
|
273284da39 | ||
|
|
7102229937 | ||
|
|
be80ba3b3b | ||
|
|
4410cb6dc6 | ||
|
|
df8df2bffa | ||
|
|
ae4c921300 | ||
|
|
3d1aa6698d | ||
|
|
613b115eb4 | ||
|
|
e19b690bd5 | ||
|
|
888be8e8a8 | ||
|
|
5785896007 | ||
|
|
8d522de7e7 | ||
|
|
81ae4c8d4d | ||
|
|
8cd3b8fdef | ||
|
|
c1aa341a7a | ||
|
|
f9c192c47a | ||
|
|
cdc6da844e | ||
|
|
fd35f23af7 | ||
|
|
c04851f64e | ||
|
|
3a63c3ad43 | ||
|
|
431f1c97cf | ||
|
|
1b68a3d5a9 | ||
|
|
c797808a40 | ||
|
|
d810ccba84 | ||
|
|
2fea5d9af2 | ||
|
|
1d9a3bf23a | ||
|
|
3d8c764eb0 | ||
|
|
eb317f0d46 | ||
|
|
27de1aea11 | ||
|
|
92f7e761ee | ||
|
|
b2f1786eff | ||
|
|
f8369e7c63 | ||
|
|
f095a74f2a | ||
|
|
4e3fccb009 | ||
|
|
a2995ed3c5 | ||
|
|
5edff9235b | ||
|
|
31e134c1e0 | ||
|
|
47e22b788f | ||
|
|
798628fdf8 | ||
|
|
ca9825d262 | ||
|
|
1a36ec65ee | ||
|
|
d07a20e6f8 | ||
|
|
783b317281 | ||
|
|
4bc525f250 | ||
|
|
11cfc7ea09 | ||
|
|
a806f2672d | ||
|
|
2fee88e54b | ||
|
|
7e1a2a6e4f | ||
|
|
d5b311760e | ||
|
|
dffe6f8b91 | ||
|
|
b4bca4670b | ||
|
|
150c43f41f | ||
|
|
9fc11df229 | ||
|
|
56a7a960c3 | ||
|
|
cd44cc6396 | ||
|
|
4af714f1dd | ||
|
|
836719587c | ||
|
|
2d6fab6e14 | ||
|
|
0ae58a3346 | ||
|
|
f32e958984 | ||
|
|
0703d927ca | ||
|
|
62b5deb77d | ||
|
|
82e718b3d1 | ||
|
|
41409c9b94 | ||
|
|
57c759773a | ||
|
|
9d6402a4f7 | ||
|
|
a29609df21 | ||
|
|
04ea8293a8 | ||
|
|
bde54aba3b | ||
|
|
687e1be3e5 | ||
|
|
a44d9b7ca8 | ||
|
|
febf571d84 | ||
|
|
818ece9f10 | ||
|
|
ec10c9c70c | ||
|
|
8852d50971 | ||
|
|
908f6763be | ||
|
|
03e20842fb | ||
|
|
084913ac91 | ||
|
|
d9801e2d57 | ||
|
|
29fe913fd8 | ||
|
|
5b68f9623e | ||
|
|
2a6662472d | ||
|
|
5cf6777340 | ||
|
|
17863c85b6 | ||
|
|
237d0f9d9a | ||
|
|
b98e27e464 | ||
|
|
d01d9edb78 | ||
|
|
0a86683392 | ||
|
|
b2b4febd31 | ||
|
|
ad67e6eaee | ||
|
|
ed5ce48f65 | ||
|
|
2e351bfc78 | ||
|
|
f324b97e8b | ||
|
|
46544e1cbe | ||
|
|
ffab6c0e12 | ||
|
|
10e736386b | ||
|
|
a1ecebf5bf | ||
|
|
996597dcb1 | ||
|
|
e09ad373b3 | ||
|
|
ddf7d34982 | ||
|
|
db9a4e4402 | ||
|
|
39a75a6792 | ||
|
|
d78484ff38 | ||
|
|
8900ac9a1f | ||
|
|
7df2e8af97 | ||
|
|
2ed723dfc2 | ||
|
|
087177274d | ||
|
|
59459ebbd6 | ||
|
|
2d981cd737 | ||
|
|
4c4e6fd6f9 | ||
|
|
c52f2fc069 | ||
|
|
eb3e2566e9 | ||
|
|
c186573775 | ||
|
|
3d9d8d6efc | ||
|
|
1ed82fc7d5 | ||
|
|
162228aff3 | ||
|
|
dd051f4c6e | ||
|
|
e59645b347 | ||
|
|
cbdc0bd99b | ||
|
|
b4fc36701a | ||
|
|
f2f00e3017 | ||
|
|
c706fbc6e7 | ||
|
|
88fb8458c1 | ||
|
|
982f298fc4 | ||
|
|
4a9a05718e | ||
|
|
b99a851af3 | ||
|
|
f452d6f5b7 | ||
|
|
b98b94de91 | ||
|
|
b18aee9dba | ||
|
|
bfe7bcf1b4 | ||
|
|
55bb732bb0 | ||
|
|
465fea8a16 | ||
|
|
149758ccab | ||
|
|
16095544cb | ||
|
|
37265f25d8 | ||
|
|
c61eb3f039 | ||
|
|
39862034e1 | ||
|
|
f938b29040 | ||
|
|
984b92815c | ||
|
|
82c4b0007c | ||
|
|
f4a3f699e8 | ||
|
|
9c4055a046 | ||
|
|
088493dc4f | ||
|
|
479ab1fb7b | ||
|
|
749cde6e72 | ||
|
|
7317fad754 | ||
|
|
83a29328c7 | ||
|
|
788fb145af | ||
|
|
b1b1a9b5de | ||
|
|
941a36a385 | ||
|
|
3ef3c23a29 | ||
|
|
39e630cece | ||
|
|
b2a62c2dd7 | ||
|
|
26dd3a2ca8 | ||
|
|
c328d6f6c6 | ||
|
|
44c79d71d7 | ||
|
|
ecd90d605a | ||
|
|
b6bd4b9da0 | ||
|
|
6ae11f862c | ||
|
|
c224141355 | ||
|
|
28c35633d3 | ||
|
|
6e36e33b22 | ||
|
|
093cca9d21 | ||
|
|
410c19dd78 | ||
|
|
4a7be89be6 | ||
|
|
51305e0a21 | ||
|
|
fda3398440 | ||
|
|
340ff0107c | ||
|
|
8374a58dc1 | ||
|
|
3690863a27 | ||
|
|
078d003cfe | ||
|
|
b6ab795878 | ||
|
|
7e81dede2f | ||
|
|
3482257b15 | ||
|
|
9f3ab8d3dd | ||
|
|
8f2ee555ec | ||
|
|
db53de2233 | ||
|
|
3596faed2f | ||
|
|
9e13d134d3 | ||
|
|
7f229005a6 | ||
|
|
e3a887f36c | ||
|
|
bca8e64574 | ||
|
|
3873d7099c | ||
|
|
48b176f49a | ||
|
|
8b8b387f2a | ||
|
|
f459d9b953 | ||
|
|
b276fbab90 | ||
|
|
e73f076324 | ||
|
|
131f8f7f45 | ||
|
|
c3b73964d5 | ||
|
|
790fc7c04b | ||
|
|
cd33314d0b | ||
|
|
4ba50267f1 | ||
|
|
d46e603366 | ||
|
|
f8878d73c3 | ||
|
|
9c0dac044c | ||
|
|
2f8e330b73 | ||
|
|
e6e55aa827 | ||
|
|
07bff8cb37 | ||
|
|
d857144653 | ||
|
|
17fdd840bb | ||
|
|
da6711c93c | ||
|
|
36b307eb49 | ||
|
|
2d8ec2fd78 | ||
|
|
11ba96103c | ||
|
|
843d2a224d | ||
|
|
0faf01c194 | ||
|
|
e868cf6270 | ||
|
|
255b3d0da7 | ||
|
|
5fad8d0680 | ||
|
|
d3dcdcb92e | ||
|
|
44ae5edc56 | ||
|
|
f0542923d7 | ||
|
|
29d0c07164 | ||
|
|
152abb086a | ||
|
|
d5c36ddc89 | ||
|
|
226a4ea2f3 | ||
|
|
ac0eafe6ab | ||
|
|
87bc9a5431 | ||
|
|
ef62892f60 | ||
|
|
7ae24488d9 | ||
|
|
6482670fdf | ||
|
|
317cf6d00f | ||
|
|
a5eeac6e5a | ||
|
|
e2cf9255ac | ||
|
|
25941cdbdd | ||
|
|
1764633bc5 | ||
|
|
f6eb90bd2d | ||
|
|
5f46c820e6 | ||
|
|
c20b279362 | ||
|
|
24ceb8801e | ||
|
|
8d5ca66db7 | ||
|
|
f0d250ff15 | ||
|
|
074cafbab1 | ||
|
|
79cd52833c | ||
|
|
6b75286885 | ||
|
|
b884ae039c | ||
|
|
3ecfdd027e | ||
|
|
55ca9927b0 | ||
|
|
b67377c41e | ||
|
|
15f24eb26b | ||
|
|
0283bb0e91 | ||
|
|
d39c0995b6 | ||
|
|
db4c088ce9 | ||
|
|
a4898e24b7 | ||
|
|
086db0d52f | ||
|
|
7b3523fb24 | ||
|
|
74ca91d611 | ||
|
|
e94d4b28b0 | ||
|
|
a85aa2da52 | ||
|
|
dded8504e6 | ||
|
|
a2bad425f6 | ||
|
|
a4ec8ec05d | ||
|
|
1b4bf7918b | ||
|
|
1f48e44cc6 | ||
|
|
83613f02a0 | ||
|
|
9d8743e765 | ||
|
|
84b9b11ef5 | ||
|
|
2327a0012e | ||
|
|
4cdd796a3d | ||
|
|
e819265e7e | ||
|
|
070a96c506 | ||
|
|
e1be54823c | ||
|
|
d7dd9393ff | ||
|
|
64c36a1562 | ||
|
|
13a54565ba | ||
|
|
12eb17cc41 | ||
|
|
9715dfa126 | ||
|
|
a6e3689e97 | ||
|
|
2e5cf81231 | ||
|
|
4eeca12d6a | ||
|
|
ad577d22d0 | ||
|
|
5e68094e87 | ||
|
|
10c26fe642 | ||
|
|
49ecd252ed | ||
|
|
2ee2a5dc46 | ||
|
|
430fdf4fcf | ||
|
|
abca81ea38 | ||
|
|
a0a76f738b | ||
|
|
7d440c2430 | ||
|
|
94274fd092 | ||
|
|
266edd80f3 | ||
|
|
d1b1be32ca | ||
|
|
fbdd923d43 | ||
|
|
2c30414f88 | ||
|
|
d8e469a33a | ||
|
|
a6b89b3ea3 | ||
|
|
0f80901bce | ||
|
|
8fc45e5590 | ||
|
|
c829304e31 | ||
|
|
d3f94ff2fb | ||
|
|
85d9a9dc27 | ||
|
|
04db6d3838 | ||
|
|
e5a28ac64e | ||
|
|
2397c6c0ea | ||
|
|
c3e6c3d1da | ||
|
|
22da75cb72 | ||
|
|
22a8ca37b6 | ||
|
|
692afba3d9 | ||
|
|
9c621d5816 | ||
|
|
feedbcf581 | ||
|
|
a57f3be58d | ||
|
|
897dffc426 | ||
|
|
6f0b61e6d8 | ||
|
|
674ee9ac9e | ||
|
|
5eab013cf6 | ||
|
|
079ef8d800 | ||
|
|
1a9be279c6 | ||
|
|
f5d2eba0c4 | ||
|
|
e7d19c9084 | ||
|
|
638bf38b25 | ||
|
|
c4dc49ac5e | ||
|
|
926600e29d | ||
|
|
5e58f1e273 | ||
|
|
c79ad65d37 | ||
|
|
95a97ad776 | ||
|
|
6c28cedf23 | ||
|
|
a3599b53bb | ||
|
|
438c501fae | ||
|
|
25cbd55841 | ||
|
|
5ef53f251d | ||
|
|
92819b6c9e | ||
|
|
d22674f362 | ||
|
|
0fc83e9255 | ||
|
|
f171c5d59e | ||
|
|
78b8299130 | ||
|
|
2c7fe0c92d | ||
|
|
6ff30f2a0e | ||
|
|
7707c5e558 | ||
|
|
9b7a920e0d | ||
|
|
2e51380be5 | ||
|
|
445f508ea5 | ||
|
|
b82c89c582 | ||
|
|
85a47a8049 | ||
|
|
f8e9798038 | ||
|
|
581d6747ad | ||
|
|
00a1aa7628 | ||
|
|
79a9048db5 | ||
|
|
2bfba2e399 | ||
|
|
484419e526 | ||
|
|
8c18830c97 | ||
|
|
653d370a85 | ||
|
|
316e91cfcf | ||
|
|
f31632c8c8 | ||
|
|
bb52c13f9a | ||
|
|
738d43fd83 | ||
|
|
1c6c8588d9 | ||
|
|
7a97cb2e02 | ||
|
|
5e0d2992c9 | ||
|
|
210859a5ef | ||
|
|
25f6d229dd | ||
|
|
60405abf58 | ||
|
|
8070b11c27 | ||
|
|
7d76ce77c9 | ||
|
|
3d48cbc111 | ||
|
|
36f34da227 | ||
|
|
d06e07542e | ||
|
|
a0c8646923 | ||
|
|
6c924de209 | ||
|
|
500503c069 | ||
|
|
5dd8cd66dd | ||
|
|
41e84e55f1 | ||
|
|
7f8888d7dd | ||
|
|
216928f904 | ||
|
|
345bab3a99 | ||
|
|
064a7a113c | ||
|
|
115d5c7db1 | ||
|
|
8a86ecc38d | ||
|
|
430a2d71e3 | ||
|
|
187c0d2a52 | ||
|
|
8fbe77afb2 | ||
|
|
e43e92f2b0 | ||
|
|
cc6c0ee7df | ||
|
|
3cd0665166 | ||
|
|
a42e696695 | ||
|
|
83886fbcf1 | ||
|
|
f0c5046ccf | ||
|
|
9db2a65b31 | ||
|
|
59c5619b44 | ||
|
|
b39016df7c | ||
|
|
c22fe6320d | ||
|
|
b99ccdf612 | ||
|
|
2d338ffbfb | ||
|
|
739cb9306a | ||
|
|
954fc29613 | ||
|
|
630f71b349 | ||
|
|
2b3f274aae | ||
|
|
e834e46ef6 | ||
|
|
27f10cd10e | ||
|
|
f4b92ee690 | ||
|
|
1c4d3d5f13 | ||
|
|
7bb7a092a4 | ||
|
|
0ed623caf9 | ||
|
|
19cd40cfdd | ||
|
|
7617a1ef8e | ||
|
|
a370afe94b | ||
|
|
ff5a1ac7cb | ||
|
|
893c7e3ab2 | ||
|
|
17698bfeae | ||
|
|
8b90e99658 | ||
|
|
fa72b89b3a | ||
|
|
fd3c88cb67 | ||
|
|
8b44ca913f | ||
|
|
7fe79a703e | ||
|
|
80c36a02a9 | ||
|
|
4bdfe9b9b8 | ||
|
|
767e8a0db4 | ||
|
|
d8a2ffa835 | ||
|
|
d38bec39b6 | ||
|
|
33a9ca060d | ||
|
|
30fedf418c | ||
|
|
5bdacb377c | ||
|
|
ee16892481 | ||
|
|
e2909f6165 | ||
|
|
205d25e341 | ||
|
|
f49ced2d92 | ||
|
|
b86dcce381 | ||
|
|
c4be58f074 | ||
|
|
b3f0bfa4fb | ||
|
|
37daf3a192 | ||
|
|
7ada4af31b | ||
|
|
67db483b0b | ||
|
|
357e118ace | ||
|
|
f2cdbe26b6 | ||
|
|
3569489ce7 | ||
|
|
6c3fa224e0 | ||
|
|
9066003240 | ||
|
|
f0007d0a13 | ||
|
|
ff6db1d00a | ||
|
|
27403a08d2 | ||
|
|
6d70d2aada | ||
|
|
0e551383ba | ||
|
|
b5b86ab8b8 | ||
|
|
61d0159e8a | ||
|
|
c41859816b | ||
|
|
5a99e55309 | ||
|
|
1b6e6692c0 | ||
|
|
116ba72f37 | ||
|
|
3fe9caf0e6 | ||
|
|
a403c3b596 | ||
|
|
2a44b0bb5b | ||
|
|
130a23dd4f | ||
|
|
db0d6e8bbc | ||
|
|
1cc532d600 | ||
|
|
8f5fa0cf9a | ||
|
|
89c6904d69 | ||
|
|
14663ca204 | ||
|
|
64d2b35ace | ||
|
|
ab5e09db93 | ||
|
|
932a015e49 | ||
|
|
d6b18cd426 | ||
|
|
84540fb087 | ||
|
|
5b8c983ab3 | ||
|
|
13a11e78f0 | ||
|
|
fb8cf26c51 | ||
|
|
41db725e15 | ||
|
|
f25d2ba183 | ||
|
|
66531635bc | ||
|
|
4c5cf85091 | ||
|
|
6db4deafee | ||
|
|
2c312c1a5b | ||
|
|
bcc2bf123e | ||
|
|
50fedab19e | ||
|
|
d0ccd3e9c2 | ||
|
|
4b2d8a8d5a | ||
|
|
c12364a7c7 | ||
|
|
560400e786 | ||
|
|
f7a4d89467 |
14
.gitignore
vendored
14
.gitignore
vendored
@@ -1,4 +1,16 @@
|
||||
*.so
|
||||
*.pyc
|
||||
*.pyo
|
||||
__pycache__/
|
||||
*.log
|
||||
*.mo
|
||||
|
||||
/lib/Solaar.egg-info/
|
||||
/build/
|
||||
/sdist/
|
||||
/dist/
|
||||
/deb_dist/
|
||||
/MANIFEST
|
||||
|
||||
/docs/captures/
|
||||
/share/logitech_icons/
|
||||
/share/locale/
|
||||
|
||||
41
COPYING
41
COPYING
@@ -1,12 +1,12 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
|
||||
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
@@ -15,7 +15,7 @@ software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Library General Public License instead.) You can apply it to
|
||||
the GNU Lesser General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
@@ -55,8 +55,8 @@ patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
@@ -110,7 +110,7 @@ above, provided that you also meet all of these conditions:
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
@@ -168,7 +168,7 @@ access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
@@ -225,7 +225,7 @@ impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
@@ -255,7 +255,7 @@ make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
@@ -277,9 +277,9 @@ YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
@@ -303,17 +303,16 @@ the "copyright" line and a pointer to where the full notice is found.
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
@@ -336,5 +335,5 @@ necessary. Here is a sample; alter the names:
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Library General
|
||||
library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License.
|
||||
|
||||
2
COPYRIGHT
Normal file
2
COPYRIGHT
Normal file
@@ -0,0 +1,2 @@
|
||||
Copyright 2012, 2013
|
||||
Daniel Pavel <daniel.pavel@gmail.com>
|
||||
62
ChangeLog
Normal file
62
ChangeLog
Normal file
@@ -0,0 +1,62 @@
|
||||
0.9.2:
|
||||
* Added support for hand detection on the K800.
|
||||
* Added support for V550 and V450 Nano.
|
||||
* Fixed side-scrolling wit the M705 Marathon.
|
||||
* Fixed identification of the T650 Touchpad.
|
||||
* Added internationalization support and romanian translation.
|
||||
* Polish translation courtesy of Adrian Piotrowicz.
|
||||
|
||||
0.9.1:
|
||||
* When devices report a battery alert, only show the alert once.
|
||||
* Make sure devices in the window tree are sorted by registration index.
|
||||
* Added an autostart .desktop file.
|
||||
* Replaced single-instance code with GtkApplication.
|
||||
* Fixed indentification of the M505 mouse.
|
||||
* Fixed an occasional windowing layout bug with the C52F Nano Receiver.
|
||||
|
||||
0.9.0:
|
||||
* New single-window UI.
|
||||
* Performance MX leds show the current battery charge.
|
||||
* Support the VX Nano mouse.
|
||||
* Faster and more accurate detection of devices.
|
||||
* If upower is accessible through DBus, handle suspend/resume.
|
||||
* Replaced Solaar icons with SVGs.
|
||||
* Running solaar-cli in parallel with solaar is now less likely to cause issues.
|
||||
* Bugfixes to saving and applying device settings.
|
||||
* Properly handle ^C when running in console.
|
||||
|
||||
0.8.9:
|
||||
|
||||
* Improved support for gnome-shell/Unity.
|
||||
* Persist devices settings between runs.
|
||||
* Fixed reading of MK700 keyboard battery status.
|
||||
* Use battery icons from the current theme instead of custom ones.
|
||||
* Debian/Ubuntu packages now depend on an icon theme, to make sure
|
||||
no missing icons appear in the application window.
|
||||
* Fixed missing icons under Kubuntu.
|
||||
* Many more bug-fixes and reliability improvements.
|
||||
|
||||
0.8.8:
|
||||
|
||||
* Partial support for some Nano receivers.
|
||||
* Improved support for some devices: M510, K800, Performance MX.
|
||||
* Improved battery support for some HID++ 1.0 devices.
|
||||
* Properly handle device loss on computer sleep/wake.
|
||||
* Better handling of receiver adding and removal at runtime.
|
||||
* Removed a few more unhelpful notifications.
|
||||
* Incipient support for multiple connected receivers.
|
||||
* More Python 3 fixes.
|
||||
|
||||
0.8.7:
|
||||
|
||||
* Don't show the "device disconnected" notification, it can be annoying and
|
||||
not very useful.
|
||||
* More robust detection of systray icon visibility.
|
||||
|
||||
0.8.6:
|
||||
|
||||
* Ensure the Gtk application is single-instance.
|
||||
* Fix identifying available dpi values.
|
||||
* Fixed locating application icons when installed in a custom prefix.
|
||||
* Fixed some icon names for the oxygen theme.
|
||||
* Python 3 fixes.
|
||||
3
MANIFEST.in
Normal file
3
MANIFEST.in
Normal file
@@ -0,0 +1,3 @@
|
||||
include COPYRIGHT COPYING README.md ChangeLog
|
||||
recursive-include rules.d *
|
||||
recursive-include share/locale *
|
||||
60
README
60
README
@@ -1,60 +0,0 @@
|
||||
Solaar
|
||||
------
|
||||
|
||||
|
||||
This application connects to a Logitech Unifying Receiver
|
||||
(http://www.logitech.com/en-us/66/6079) and listens for events from devices
|
||||
attached to it.
|
||||
|
||||
Currently the K750 solar keyboard is also queried for its solar charge status.
|
||||
Support for other devices could be added in the future, but the K750 keyboard is
|
||||
the only device I have and can test on.
|
||||
|
||||
|
||||
Requirements
|
||||
------------
|
||||
|
||||
- Python (2.7 or 3.2). Either version should work well.
|
||||
- Gtk 3; Gtk 2 should partially work with some problems.
|
||||
- Python GI (GObject Introspection), for Gtk bindings.
|
||||
- pyudev for enumerating udev devices.
|
||||
- Optional libnotify GI bindings, for desktop notifications.
|
||||
|
||||
The necessary packages for Debian/Ubuntu are `python-pyudev`/`python3-pyudev`,
|
||||
`python-gi`/`python3-gi`, `gir1.2-gtk-3.0`, and optionally `gir1.2-notify-0.7`.
|
||||
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
Normally USB devices are not accessible for r/w by regular users, so you will
|
||||
need to install a udev rule to allow access to the Logitech Unifying Receiver.
|
||||
|
||||
In rules.d/ you'll find a udev rule file, to be copied in /etc/udev/rules.d/ (as
|
||||
root).
|
||||
|
||||
In its current form it makes the UR device available for r/w by all users
|
||||
belonging to the 'plugdev' system group (standard Debian/Ubuntu group for
|
||||
pluggable devices). It may need changes, specific to your particular system's
|
||||
configuration.
|
||||
|
||||
If in doubt, replacing GROUP="plugdev" with GROUP="<your username>" should just
|
||||
work.
|
||||
|
||||
After you copy the file to /etc/udev/rules.d (and possibly modify it for your
|
||||
system), run 'udevadm control --reload-rules' as root for it to apply. Then
|
||||
physically remove the Unifying Receiver, wait 30 seconds and re-insert it.
|
||||
|
||||
|
||||
Thanks
|
||||
------
|
||||
|
||||
This project began as a third-hand clone of Noah K. Tilton's logitech-solar-k750
|
||||
project on GitHub (no longer available). It was developed further thanks to the
|
||||
diggings in Logitech's HID protocol done, among others, by Julien Danjou
|
||||
(http://julien.danjou.info/blog/2012/logitech-k750-linux-support) and
|
||||
Lars-Dominik Braun (http://6xq.net/git/lars/lshidpp.git).
|
||||
|
||||
|
||||
Cheers,
|
||||
-pwr
|
||||
88
README.md
Normal file
88
README.md
Normal file
@@ -0,0 +1,88 @@
|
||||
**Solaar** is a Linux device manager for Logitech's [Unifying Receiver][unifying]
|
||||
peripherals. It is able to pair/unpair devices to the receiver, and for most
|
||||
devices read battery status.
|
||||
|
||||
It comes in two flavors, command-line and GUI. Both are able to list the
|
||||
devices paired to a Unifying Receiver, show detailed info for each device, and
|
||||
also pair/unpair supported devices with the receiver.
|
||||
|
||||
[unifying]: http://logitech.com/en-us/66/6079
|
||||
|
||||
## Supported Devices
|
||||
|
||||
**Solaar** will detect all devices paired with your Unifying Receiver, and at
|
||||
the very least display some basic information about them.
|
||||
|
||||
For some devices, extra settings (usually not available through the standard
|
||||
Linux system configuration) are supported. For a full list of supported devices
|
||||
and their features, see [docs/devices.md](docs/devices.md).
|
||||
|
||||
|
||||
## Pre-built packages
|
||||
|
||||
Pre-built packages are available for a few Linux distros.
|
||||
|
||||
* Debian 7 (Wheezy) or higher: packages in this [repository](docs/debian.md)
|
||||
* Ubuntu/Kubuntu 12.04+: [ppa:daniel.pavel/solaar][ppa]
|
||||
|
||||
The `solaar` package uses a standard system tray implementation; to ensure
|
||||
integration with *gnome-shell* or *Unity*, install `solaar-gnome3`.
|
||||
|
||||
* a [Gentoo overlay][gentoo], courtesy of Carlos Silva
|
||||
* an [OpenSUSE rpm][opensuse], courtesy of Mathias Homann
|
||||
* an [Arch package][arch], courtesy of Arnaud Taffanel
|
||||
|
||||
[ppa]: http://launchpad.net/~daniel.pavel/+archive/solaar
|
||||
[gentoo]: http://code.r3pek.org/gentoo-overlay/src
|
||||
[opensuse]: http://software.opensuse.org/package/Solaar
|
||||
[arch]: http://aur.archlinux.org/packages/solaar
|
||||
|
||||
|
||||
## Manual installation
|
||||
|
||||
See [docs/installation.md](docs/installation.md) for the step-by-step
|
||||
procedure for manual installation.
|
||||
|
||||
|
||||
## Known Issues
|
||||
|
||||
- KDE/Kubuntu: if some icons appear broken in the application, make sure you've
|
||||
properly configured the Gtk theme and icon theme in KDE's control panel.
|
||||
|
||||
- Some devices using the [Nano Receiver][nano] (which is very similar to the
|
||||
Unifying Receiver) are supported, but not all. For details, see
|
||||
[docs/devices.md](docs/devices.md).
|
||||
|
||||
- Running the command-line application (`bin/solaar-cli`) while the GUI
|
||||
application is also running *may* occasionally cause either of them to become
|
||||
confused about the state of the devices. I haven't encountered this often
|
||||
enough to be able to be able to diagnose it properly yet.
|
||||
|
||||
[nano]: http://logitech.com/mice-pointers/articles/5926
|
||||
|
||||
|
||||
## License
|
||||
|
||||
This software is distributed under the terms of the
|
||||
[GNU Public License, v2](COPYING).
|
||||
|
||||
|
||||
## Thanks
|
||||
|
||||
This project began as a third-hand clone of [Noah K. Tilton](https://github.com/noah)'s
|
||||
logitech-solar-k750 project on GitHub (no longer available). It was developed
|
||||
further thanks to the diggings in Logitech's HID++ protocol done by many other
|
||||
people:
|
||||
|
||||
- [Julien Danjou](http://julien.danjou.info/blog/2012/logitech-k750-linux-support),
|
||||
who also provided some internal
|
||||
[Logitech documentation](http://julien.danjou.info/blog/2012/logitech-unifying-upower)
|
||||
- [Lars-Dominik Braun](http://6xq.net/git/lars/lshidpp.git)
|
||||
- [Alexander Hofbauer](http://derhofbauer.at/blog/blog/2012/08/28/logitech-performance-mx)
|
||||
- [Clach04](http://bitbucket.org/clach04/logitech-unifying-receiver-tools)
|
||||
- [Peter Wu](https://lekensteyn.nl/logitech-unifying.html)
|
||||
- [Nestor Lopez Casado](http://drive.google.com/folderview?id=0BxbRzx7vEV7eWmgwazJ3NUFfQ28)
|
||||
provided some more Logitech specifications for the HID++ protocol
|
||||
|
||||
Also thanks to Douglas Wagner, Julien Gascard and Peter Wu for helping with
|
||||
application testing and supporting new devices.
|
||||
@@ -1,82 +0,0 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from logging import getLogger as _Logger
|
||||
_l = _Logger('pairing')
|
||||
|
||||
from logitech.unifying_receiver import base as _base
|
||||
|
||||
state = None
|
||||
|
||||
class State(object):
|
||||
TICK = 400
|
||||
PAIR_TIMEOUT = 60 * 1000 / TICK
|
||||
|
||||
def __init__(self, listener):
|
||||
self.listener = listener
|
||||
self.reset()
|
||||
|
||||
def device(self, number):
|
||||
return self.listener.devices.get(number)
|
||||
|
||||
def reset(self):
|
||||
self.success = None
|
||||
self.detected_device = None
|
||||
self._countdown = self.PAIR_TIMEOUT
|
||||
|
||||
def countdown(self, assistant):
|
||||
if self._countdown < 0 or not self.listener:
|
||||
return False
|
||||
|
||||
if self._countdown == self.PAIR_TIMEOUT:
|
||||
self.start_scan()
|
||||
self._countdown -= 1
|
||||
return True
|
||||
|
||||
self._countdown -= 1
|
||||
if self._countdown > 0 and self.success is None:
|
||||
return True
|
||||
|
||||
self.stop_scan()
|
||||
assistant.scan_complete(assistant, self.detected_device)
|
||||
return False
|
||||
|
||||
def start_scan(self):
|
||||
self.reset()
|
||||
self.listener.events_filter = self.filter_events
|
||||
reply = _base.request(self.listener.handle, 0xFF, b'\x80\xB2', b'\x01')
|
||||
_l.debug("start scan reply %s", repr(reply))
|
||||
|
||||
def stop_scan(self):
|
||||
if self._countdown >= 0:
|
||||
self._countdown = -1
|
||||
reply = _base.request(self.listener.handle, 0xFF, b'\x80\xB2', b'\x02')
|
||||
_l.debug("stop scan reply %s", repr(reply))
|
||||
self.listener.events_filter = None
|
||||
|
||||
def filter_events(self, event):
|
||||
if event.devnumber == 0xFF:
|
||||
if event.code == 0x10:
|
||||
if event.data == b'\x4A\x01\x00\x00\x00':
|
||||
_l.debug("receiver listening for device wakeup")
|
||||
return True
|
||||
if event.data == b'\x4A\x00\x01\x00\x00':
|
||||
_l.debug("receiver gave up")
|
||||
self.success = False
|
||||
return True
|
||||
return False
|
||||
|
||||
if event.devnumber in self.listener.receiver.devices:
|
||||
return False
|
||||
|
||||
_l.debug("event for new device? %s", event)
|
||||
if event.code == 0x10 and event.data[0:2] == b'\x41\x04':
|
||||
self.detected_device = self.listener.make_device(event)
|
||||
return True
|
||||
|
||||
return True
|
||||
|
||||
def unpair(self, device):
|
||||
_l.debug("unpair %s", device)
|
||||
self.listener.unpair_device(device)
|
||||
340
app/receiver.py
340
app/receiver.py
@@ -1,340 +0,0 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from logging import getLogger as _Logger
|
||||
from struct import pack as _pack
|
||||
from time import sleep as _sleep
|
||||
|
||||
from logitech.unifying_receiver import base as _base
|
||||
from logitech.unifying_receiver import api as _api
|
||||
from logitech.unifying_receiver.listener import EventsListener as _EventsListener
|
||||
from logitech.unifying_receiver.common import FallbackDict as _FallbackDict
|
||||
from logitech import devices as _devices
|
||||
from logitech.devices.constants import (STATUS, STATUS_NAME, PROPS)
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
class _FeaturesArray(object):
|
||||
__slots__ = ('device', 'features', 'supported')
|
||||
|
||||
def __init__(self, device):
|
||||
self.device = device
|
||||
self.features = None
|
||||
self.supported = True
|
||||
|
||||
def _check(self):
|
||||
if self.supported:
|
||||
if self.features is not None:
|
||||
return True
|
||||
|
||||
if self.device.status >= STATUS.CONNECTED:
|
||||
handle = self.device.handle
|
||||
try:
|
||||
index = _api.get_feature_index(handle, self.device.number, _api.FEATURE.FEATURE_SET)
|
||||
except _api._FeatureNotSupported:
|
||||
self.supported = False
|
||||
else:
|
||||
count = None if index is None else _base.request(handle, self.device.number, _pack('!BB', index, 0x00))
|
||||
if count is None:
|
||||
self.supported = False
|
||||
else:
|
||||
count = ord(count[:1])
|
||||
self.features = [None] * (1 + count)
|
||||
self.features[0] = _api.FEATURE.ROOT
|
||||
self.features[index] = _api.FEATURE.FEATURE_SET
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
__bool__ = __nonzero__ = _check
|
||||
|
||||
def __getitem__(self, index):
|
||||
if not self._check():
|
||||
return None
|
||||
|
||||
if index < 0 or index >= len(self.features):
|
||||
raise IndexError
|
||||
if self.features[index] is None:
|
||||
fs_index = self.features.index(_api.FEATURE.FEATURE_SET)
|
||||
feature = _base.request(self.device.handle, self.device.number, _pack('!BB', fs_index, 0x10), _pack('!B', index))
|
||||
if feature is not None:
|
||||
self.features[index] = feature[:2]
|
||||
|
||||
return self.features[index]
|
||||
|
||||
def __contains__(self, value):
|
||||
if self._check():
|
||||
if value in self.features:
|
||||
return True
|
||||
|
||||
for index in range(0, len(self.features)):
|
||||
f = self.features[index] or self.__getitem__(index)
|
||||
assert f is not None
|
||||
if f == value:
|
||||
return True
|
||||
if f > value:
|
||||
break
|
||||
|
||||
return False
|
||||
|
||||
def index(self, value):
|
||||
if self._check():
|
||||
if self.features is not None and value in self.features:
|
||||
return self.features.index(value)
|
||||
raise ValueError("%s not in list" % repr(value))
|
||||
|
||||
def __iter__(self):
|
||||
if self._check():
|
||||
yield _api.FEATURE.ROOT
|
||||
index = 1
|
||||
last_index = len(self.features)
|
||||
while index < last_index:
|
||||
yield self.__getitem__(index)
|
||||
index += 1
|
||||
|
||||
def __len__(self):
|
||||
return len(self.features) if self._check() else 0
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
class DeviceInfo(_api.PairedDevice):
|
||||
"""A device attached to the receiver.
|
||||
"""
|
||||
def __init__(self, listener, number, status=STATUS.UNKNOWN):
|
||||
super(DeviceInfo, self).__init__(listener.handle, number)
|
||||
self._features = _FeaturesArray(self)
|
||||
|
||||
self.LOG = _Logger("Device[%d]" % number)
|
||||
self._listener = listener
|
||||
|
||||
self._status = status
|
||||
self.props = {}
|
||||
|
||||
# read them now, otherwise it it temporarily hang the UI
|
||||
# if status >= STATUS.CONNECTED:
|
||||
# n, k, s, f = self.name, self.kind, self.serial, self.firmware
|
||||
|
||||
@property
|
||||
def receiver(self):
|
||||
return self._listener.receiver
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
return self._status
|
||||
|
||||
@status.setter
|
||||
def status(self, new_status):
|
||||
if new_status != self._status and not (new_status == STATUS.CONNECTED and self._status > new_status):
|
||||
self.LOG.debug("status %d => %d", self._status, new_status)
|
||||
urgent = new_status < STATUS.CONNECTED or self._status < STATUS.CONNECTED
|
||||
self._status = new_status
|
||||
self._listener.status_changed(self, urgent)
|
||||
|
||||
if new_status < STATUS.CONNECTED:
|
||||
self.props.clear()
|
||||
|
||||
@property
|
||||
def status_text(self):
|
||||
if self._status < STATUS.CONNECTED:
|
||||
return STATUS_NAME[self._status]
|
||||
|
||||
t = []
|
||||
if self.props.get(PROPS.BATTERY_LEVEL):
|
||||
t.append('Battery: %d%%' % self.props[PROPS.BATTERY_LEVEL])
|
||||
if self.props.get(PROPS.BATTERY_STATUS):
|
||||
t.append(self.props[PROPS.BATTERY_STATUS])
|
||||
if self.props.get(PROPS.LIGHT_LEVEL):
|
||||
t.append('Light: %d lux' % self.props[PROPS.LIGHT_LEVEL])
|
||||
return ', '.join(t) if t else STATUS_NAME[STATUS.CONNECTED]
|
||||
|
||||
def process_event(self, code, data):
|
||||
if code == 0x10 and data[:1] == b'\x8F':
|
||||
self.status = STATUS.UNAVAILABLE
|
||||
return True
|
||||
|
||||
if code == 0x11:
|
||||
status = _devices.process_event(self, data)
|
||||
if status:
|
||||
if type(status) == int:
|
||||
self.status = status
|
||||
return True
|
||||
|
||||
if type(status) == tuple:
|
||||
p = dict(self.props)
|
||||
self.props.update(status[1])
|
||||
if self.status == status[0]:
|
||||
if p != self.props:
|
||||
self._listener.status_changed(self)
|
||||
else:
|
||||
self.status = status[0]
|
||||
return True
|
||||
|
||||
self.LOG.warn("don't know how to handle processed event status %s", status)
|
||||
|
||||
return False
|
||||
|
||||
def __str__(self):
|
||||
return '<DeviceInfo(%d,%s,%d)>' % (self.number, self._name or '?', self._status)
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
_RECEIVER_STATUS_NAME = _FallbackDict(
|
||||
lambda x:
|
||||
'1 device found' if x == STATUS.CONNECTED + 1 else
|
||||
'%d devices found' if x > STATUS.CONNECTED else
|
||||
'?',
|
||||
{
|
||||
STATUS.UNKNOWN: 'Initializing...',
|
||||
STATUS.UNAVAILABLE: 'Receiver not found.',
|
||||
STATUS.BOOTING: 'Scanning...',
|
||||
STATUS.CONNECTED: 'No devices found.',
|
||||
}
|
||||
)
|
||||
|
||||
class ReceiverListener(_EventsListener):
|
||||
"""Keeps the status of a Unifying Receiver.
|
||||
"""
|
||||
|
||||
def __init__(self, receiver, status_changed_callback=None):
|
||||
super(ReceiverListener, self).__init__(receiver.handle, self._events_handler)
|
||||
self.receiver = receiver
|
||||
|
||||
self.LOG = _Logger("ReceiverListener(%s)" % receiver.path)
|
||||
|
||||
self.events_filter = None
|
||||
self.events_handler = None
|
||||
|
||||
self.status_changed_callback = status_changed_callback
|
||||
|
||||
receiver.kind = receiver.name
|
||||
receiver.devices = {}
|
||||
receiver.status = STATUS.BOOTING
|
||||
receiver.status_text = _RECEIVER_STATUS_NAME[STATUS.BOOTING]
|
||||
|
||||
if _base.request(receiver.handle, 0xFF, b'\x80\x00', b'\x00\x01'):
|
||||
self.LOG.info("initialized")
|
||||
else:
|
||||
self.LOG.warn("initialization failed")
|
||||
|
||||
if _base.request(receiver.handle, 0xFF, b'\x80\x02', b'\x02'):
|
||||
self.LOG.info("triggered device events")
|
||||
else:
|
||||
self.LOG.warn("failed to trigger device events")
|
||||
|
||||
def change_status(self, new_status):
|
||||
if new_status != self.receiver.status:
|
||||
self.LOG.debug("status %d => %d", self.receiver.status, new_status)
|
||||
self.receiver.status = new_status
|
||||
self.receiver.status_text = _RECEIVER_STATUS_NAME[new_status]
|
||||
self.status_changed(None, True)
|
||||
|
||||
def status_changed(self, device=None, urgent=False):
|
||||
if self.status_changed_callback:
|
||||
self.status_changed_callback(self.receiver, device, urgent)
|
||||
|
||||
def _device_status_from(self, event):
|
||||
state_code = ord(event.data[2:3]) & 0xC0
|
||||
state = STATUS.UNAVAILABLE if state_code == 0x40 else \
|
||||
STATUS.CONNECTED if state_code == 0x80 else \
|
||||
STATUS.CONNECTED if state_code == 0x00 else \
|
||||
None
|
||||
if state is None:
|
||||
self.LOG.warn("don't know how to handle state code 0x%02X: %s", state_code, event)
|
||||
return state
|
||||
|
||||
def _events_handler(self, event):
|
||||
if self.events_filter and self.events_filter(event):
|
||||
return
|
||||
|
||||
if event.code == 0x10 and event.data[0:2] == b'\x41\x04':
|
||||
|
||||
if event.devnumber in self.receiver.devices:
|
||||
status = self._device_status_from(event)
|
||||
if status is not None:
|
||||
self.receiver.devices[event.devnumber].status = status
|
||||
else:
|
||||
dev = self.make_device(event)
|
||||
if dev is None:
|
||||
self.LOG.warn("failed to make new device from %s", event)
|
||||
else:
|
||||
self.receiver.devices[event.devnumber] = dev
|
||||
self.change_status(STATUS.CONNECTED + len(self.receiver.devices))
|
||||
return
|
||||
|
||||
if event.devnumber == 0xFF:
|
||||
if event.code == 0xFF and event.data is None:
|
||||
# receiver disconnected
|
||||
self.LOG.warn("disconnected")
|
||||
self.receiver.devices = {}
|
||||
self.change_status(STATUS.UNAVAILABLE)
|
||||
return
|
||||
elif event.devnumber in self.receiver.devices:
|
||||
dev = self.receiver.devices[event.devnumber]
|
||||
if dev.process_event(event.code, event.data):
|
||||
return
|
||||
|
||||
if self.events_handler and self.events_handler(event):
|
||||
return
|
||||
|
||||
self.LOG.warn("don't know how to handle event %s", event)
|
||||
|
||||
def make_device(self, event):
|
||||
if event.devnumber < 1 or event.devnumber > self.receiver.max_devices:
|
||||
self.LOG.warn("got event for invalid device number %d: %s", event.devnumber, event)
|
||||
return None
|
||||
|
||||
status = self._device_status_from(event)
|
||||
if status is not None:
|
||||
dev = DeviceInfo(self, event.devnumber, status)
|
||||
self.LOG.info("new device %s", dev)
|
||||
self.status_changed(dev, True)
|
||||
return dev
|
||||
|
||||
self.LOG.error("failed to identify status of device %d from %s", event.devnumber, event)
|
||||
|
||||
def unpair_device(self, device):
|
||||
try:
|
||||
del self.receiver[device.number]
|
||||
except IndexError:
|
||||
self.LOG.error("failed to unpair device %s", device)
|
||||
return False
|
||||
|
||||
del self.receiver.devices[device.number]
|
||||
self.LOG.info("unpaired device %s", device)
|
||||
self.change_status(STATUS.CONNECTED + len(self.receiver.devices))
|
||||
device.status = STATUS.UNPAIRED
|
||||
return True
|
||||
|
||||
def __str__(self):
|
||||
return '<ReceiverListener(%s,%d)>' % (self.receiver.path, self.receiver.status)
|
||||
|
||||
@classmethod
|
||||
def open(self, status_changed_callback=None):
|
||||
receiver = _api.Receiver.open()
|
||||
if receiver:
|
||||
rl = ReceiverListener(receiver, status_changed_callback)
|
||||
rl.start()
|
||||
while not rl._active:
|
||||
_sleep(0.1)
|
||||
return rl
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
class _DUMMY_RECEIVER(object):
|
||||
__slots__ = ['name', 'max_devices', 'status', 'status_text', 'devices']
|
||||
name = kind = _api.Receiver.name
|
||||
max_devices = _api.Receiver.max_devices
|
||||
status = STATUS.UNAVAILABLE
|
||||
status_text = _RECEIVER_STATUS_NAME[STATUS.UNAVAILABLE]
|
||||
devices = {}
|
||||
__bool__ = __nonzero__ = lambda self: False
|
||||
DUMMY = _DUMMY_RECEIVER()
|
||||
127
app/solaar.py
127
app/solaar.py
@@ -1,127 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
NAME = 'Solaar'
|
||||
VERSION = '0.7.2'
|
||||
__author__ = "Daniel Pavel <daniel.pavel@gmail.com>"
|
||||
__version__ = VERSION
|
||||
__license__ = "GPL"
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def _parse_arguments():
|
||||
import argparse
|
||||
arg_parser = argparse.ArgumentParser(prog=NAME.lower())
|
||||
arg_parser.add_argument('-v', '--verbose',
|
||||
action='count', default=0,
|
||||
help='increase the logger verbosity (may be repeated)')
|
||||
arg_parser.add_argument('-S', '--no-systray',
|
||||
action='store_false',
|
||||
dest='systray',
|
||||
help='don\'t embed the application window into the systray')
|
||||
arg_parser.add_argument('-N', '--no-notifications',
|
||||
action='store_false',
|
||||
dest='notifications',
|
||||
help='disable desktop notifications (shown only when in systray)')
|
||||
arg_parser.add_argument('-V', '--version',
|
||||
action='version',
|
||||
version='%(prog)s ' + __version__)
|
||||
args = arg_parser.parse_args()
|
||||
|
||||
import logging
|
||||
log_level = logging.WARNING - 10 * args.verbose
|
||||
log_format='%(asctime)s %(levelname)8s [%(threadName)s] %(name)s: %(message)s'
|
||||
logging.basicConfig(level=max(log_level, logging.DEBUG), format=log_format)
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def _check_requirements():
|
||||
try:
|
||||
import pyudev
|
||||
except ImportError:
|
||||
return 'python-pyudev'
|
||||
|
||||
try:
|
||||
import gi.repository
|
||||
except ImportError:
|
||||
return 'python-gi'
|
||||
|
||||
try:
|
||||
from gi.repository import Gtk
|
||||
except ImportError:
|
||||
return 'gir1.2-gtk-3.0'
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = _parse_arguments()
|
||||
|
||||
req_fail = _check_requirements()
|
||||
if req_fail:
|
||||
raise ImportError('missing required package: %s' % req_fail)
|
||||
|
||||
import ui
|
||||
|
||||
# check if the notifications are available and enabled
|
||||
args.notifications &= args.systray
|
||||
if ui.notify.available and ui.notify.init(NAME):
|
||||
ui.action.toggle_notifications.set_active(args.notifications)
|
||||
else:
|
||||
ui.action.toggle_notifications = None
|
||||
|
||||
from receiver import DUMMY
|
||||
window = ui.main_window.create(NAME, DUMMY.name, DUMMY.max_devices, args.systray)
|
||||
if args.systray:
|
||||
menu_actions = (ui.action.toggle_notifications,
|
||||
ui.action.about)
|
||||
icon = ui.status_icon.create(window, menu_actions)
|
||||
else:
|
||||
icon = None
|
||||
window.present()
|
||||
|
||||
import pairing
|
||||
from gi.repository import Gtk, GObject
|
||||
|
||||
listener = None
|
||||
notify_missing = True
|
||||
|
||||
def status_changed(receiver, device=None, urgent=False):
|
||||
ui.update(receiver, icon, window, device)
|
||||
if ui.notify.available and urgent:
|
||||
GObject.idle_add(ui.notify.show, device or receiver)
|
||||
|
||||
global listener
|
||||
if not listener:
|
||||
GObject.timeout_add(5000, check_for_listener)
|
||||
listener = None
|
||||
|
||||
from receiver import ReceiverListener
|
||||
def check_for_listener(retry=True):
|
||||
global listener, notify_missing
|
||||
|
||||
if listener is None:
|
||||
try:
|
||||
listener = ReceiverListener.open(status_changed)
|
||||
except OSError:
|
||||
ui.show_permissions_warning(window)
|
||||
|
||||
if listener is None:
|
||||
pairing.state = None
|
||||
if notify_missing:
|
||||
status_changed(DUMMY, None, True)
|
||||
notify_missing = False
|
||||
return retry
|
||||
|
||||
# print ("opened receiver", listener, listener.receiver)
|
||||
notify_missing = True
|
||||
pairing.state = pairing.State(listener)
|
||||
status_changed(listener.receiver, None, True)
|
||||
|
||||
GObject.timeout_add(100, check_for_listener, False)
|
||||
Gtk.main()
|
||||
|
||||
if listener is not None:
|
||||
listener.stop()
|
||||
|
||||
ui.notify.uninit()
|
||||
@@ -1,70 +0,0 @@
|
||||
# pass
|
||||
|
||||
from . import (notify, status_icon, main_window, pair_window, action)
|
||||
|
||||
from gi.repository import (GObject, Gtk)
|
||||
GObject.threads_init()
|
||||
|
||||
|
||||
from solaar import NAME as _NAME
|
||||
_APP_ICONS = (_NAME + '-fail', _NAME + '-init', _NAME)
|
||||
def appicon(receiver_status):
|
||||
return (_APP_ICONS[0] if receiver_status < 0 else
|
||||
_APP_ICONS[1] if receiver_status < 1 else
|
||||
_APP_ICONS[2])
|
||||
|
||||
|
||||
_ICON_THEME = Gtk.IconTheme.get_default()
|
||||
|
||||
def get_icon(name, fallback):
|
||||
return name if name and _ICON_THEME.has_icon(name) else fallback
|
||||
|
||||
def icon_file(name):
|
||||
if name and _ICON_THEME.has_icon(name):
|
||||
return _ICON_THEME.lookup_icon(name, 0, 0).get_filename()
|
||||
return None
|
||||
|
||||
|
||||
def show_permissions_warning(window):
|
||||
text = ('Found a possible Unifying Receiver device,\n'
|
||||
'but did not have permission to open it.')
|
||||
|
||||
m = Gtk.MessageDialog(window, Gtk.DialogFlags.MODAL, Gtk.MessageType.ERROR, Gtk.ButtonsType.CLOSE, text)
|
||||
m.set_title('Permissions error')
|
||||
m.run()
|
||||
m.destroy()
|
||||
|
||||
|
||||
def find_children(container, *child_names):
|
||||
assert container is not None
|
||||
|
||||
def _iterate_children(widget, names, result, count):
|
||||
wname = widget.get_name()
|
||||
if wname in names:
|
||||
index = names.index(wname)
|
||||
names[index] = None
|
||||
result[index] = widget
|
||||
count -= 1
|
||||
|
||||
if count > 0 and isinstance(widget, Gtk.Container):
|
||||
for w in widget:
|
||||
count = _iterate_children(w, names, result, count)
|
||||
if count == 0:
|
||||
break
|
||||
|
||||
return count
|
||||
|
||||
names = list(child_names)
|
||||
count = len(names)
|
||||
result = [None] * count
|
||||
_iterate_children(container, names, result, count)
|
||||
return tuple(result) if count > 1 else result[0]
|
||||
|
||||
|
||||
def update(receiver, icon, window, reason):
|
||||
assert receiver is not None
|
||||
assert reason is not None
|
||||
if window:
|
||||
GObject.idle_add(main_window.update, window, receiver, reason)
|
||||
if icon:
|
||||
GObject.idle_add(status_icon.update, icon, receiver)
|
||||
@@ -1,91 +0,0 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
# from sys import version as PYTTHON_VERSION
|
||||
from gi.repository import Gtk
|
||||
|
||||
import ui.notify
|
||||
import ui.pair_window
|
||||
from solaar import NAME as _NAME
|
||||
from solaar import VERSION as _VERSION
|
||||
|
||||
|
||||
def _action(name, label, function, *args):
|
||||
action = Gtk.Action(name, label, label, None)
|
||||
action.set_icon_name(name)
|
||||
if function:
|
||||
action.connect('activate', function, *args)
|
||||
return action
|
||||
|
||||
|
||||
def _toggle_action(name, label, function, *args):
|
||||
action = Gtk.ToggleAction(name, label, label, None)
|
||||
action.set_icon_name(name)
|
||||
action.connect('activate', function, *args)
|
||||
return action
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def _toggle_notifications(action):
|
||||
if action.get_active():
|
||||
ui.notify.init(_NAME)
|
||||
else:
|
||||
ui.notify.uninit()
|
||||
action.set_sensitive(ui.notify.available)
|
||||
toggle_notifications = _toggle_action('notifications', 'Notifications', _toggle_notifications)
|
||||
|
||||
|
||||
def _show_about_window(action):
|
||||
about = Gtk.AboutDialog()
|
||||
about.set_icon_name(_NAME)
|
||||
about.set_program_name(_NAME)
|
||||
about.set_logo_icon_name(_NAME)
|
||||
about.set_version(_VERSION)
|
||||
about.set_license_type(Gtk.License.GPL_2_0)
|
||||
about.set_authors(('Daniel Pavel http://github.com/pwr', ))
|
||||
about.set_website('http://github.com/pwr/Solaar/wiki')
|
||||
about.set_website_label('Solaar Wiki')
|
||||
# about.set_comments('Using Python %s\n' % PYTTHON_VERSION.split(' ')[0])
|
||||
about.run()
|
||||
about.destroy()
|
||||
about = _action('help-about', 'About ' + _NAME, _show_about_window)
|
||||
|
||||
quit = _action('exit', 'Quit', Gtk.main_quit)
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
import pairing
|
||||
|
||||
def _pair_device(action, frame):
|
||||
window = frame.get_toplevel()
|
||||
|
||||
pair_dialog = ui.pair_window.create( action, pairing.state)
|
||||
pair_dialog.set_transient_for(window)
|
||||
pair_dialog.set_modal(True)
|
||||
|
||||
window.present()
|
||||
pair_dialog.present()
|
||||
|
||||
def pair(frame):
|
||||
return _action('add', 'Pair new device', _pair_device, frame)
|
||||
|
||||
|
||||
def _unpair_device(action, frame):
|
||||
window = frame.get_toplevel()
|
||||
window.present()
|
||||
device = frame._device
|
||||
qdialog = Gtk.MessageDialog(window, 0,
|
||||
Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO,
|
||||
"Unpair device\n%s ?" % device.name)
|
||||
choice = qdialog.run()
|
||||
qdialog.destroy()
|
||||
if choice == Gtk.ResponseType.YES:
|
||||
pairing.state.unpair(device)
|
||||
|
||||
def unpair(frame):
|
||||
return _action('remove', 'Unpair', _unpair_device, frame)
|
||||
@@ -1,304 +0,0 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from gi.repository import (Gtk, Gdk)
|
||||
|
||||
import ui
|
||||
from logitech.devices.constants import (STATUS, PROPS)
|
||||
|
||||
|
||||
_SMALL_DEVICE_ICON_SIZE = Gtk.IconSize.BUTTON
|
||||
_DEVICE_ICON_SIZE = Gtk.IconSize.DIALOG
|
||||
_STATUS_ICON_SIZE = Gtk.IconSize.LARGE_TOOLBAR
|
||||
_PLACEHOLDER = '~'
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def _info_text(dev):
|
||||
fw_text = '\n'.join(['%-12s\t<tt>%s%s%s</tt>' %
|
||||
(f.kind, f.name, ' ' if f.name else '', f.version) for f in dev.firmware])
|
||||
return ('<small>'
|
||||
'Serial \t\t<tt>%s</tt>\n'
|
||||
'HID protocol\t<tt>%1.1f</tt>\n'
|
||||
'%s'
|
||||
'</small>' % (dev.serial, dev.protocol, fw_text))
|
||||
|
||||
def _toggle_info(action, label_widget, box_widget, frame):
|
||||
if action.get_active():
|
||||
box_widget.set_visible(True)
|
||||
if not label_widget.get_text():
|
||||
label_widget.set_markup(_info_text(frame._device))
|
||||
else:
|
||||
box_widget.set_visible(False)
|
||||
|
||||
|
||||
def _make_receiver_box(name):
|
||||
frame = Gtk.Frame()
|
||||
frame._device = None
|
||||
frame.set_name(name)
|
||||
|
||||
icon_name = ui.get_icon(name, 'preferences-desktop-peripherals')
|
||||
icon = Gtk.Image.new_from_icon_name(icon_name, _SMALL_DEVICE_ICON_SIZE)
|
||||
|
||||
label = Gtk.Label('Scanning...')
|
||||
label.set_name('label')
|
||||
label.set_alignment(0, 0.5)
|
||||
|
||||
toolbar = Gtk.Toolbar()
|
||||
toolbar.set_name('toolbar')
|
||||
toolbar.set_style(Gtk.ToolbarStyle.ICONS)
|
||||
toolbar.set_icon_size(Gtk.IconSize.MENU)
|
||||
toolbar.set_show_arrow(False)
|
||||
|
||||
hbox = Gtk.HBox(homogeneous=False, spacing=8)
|
||||
hbox.pack_start(icon, False, False, 0)
|
||||
hbox.pack_start(label, True, True, 0)
|
||||
hbox.pack_end(toolbar, False, False, 0)
|
||||
|
||||
info_label = Gtk.Label()
|
||||
info_label.set_name('info-label')
|
||||
info_label.set_alignment(0, 0.5)
|
||||
info_label.set_padding(8, 2)
|
||||
info_label.set_selectable(True)
|
||||
|
||||
info_box = Gtk.Frame()
|
||||
info_box.add(info_label)
|
||||
info_box.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
|
||||
|
||||
toggle_info_action = ui.action._toggle_action('info', 'Receiver info', _toggle_info, info_label, info_box, frame)
|
||||
toolbar.insert(toggle_info_action.create_tool_item(), 0)
|
||||
toolbar.insert(ui.action.pair(frame).create_tool_item(), -1)
|
||||
|
||||
vbox = Gtk.VBox(homogeneous=False, spacing=2)
|
||||
vbox.set_border_width(4)
|
||||
vbox.pack_start(hbox, True, True, 0)
|
||||
vbox.pack_start(info_box, True, True, 0)
|
||||
|
||||
frame.add(vbox)
|
||||
frame.show_all()
|
||||
info_box.set_visible(False)
|
||||
return frame
|
||||
|
||||
|
||||
def _make_device_box(index):
|
||||
frame = Gtk.Frame()
|
||||
frame._device = None
|
||||
frame.set_name(_PLACEHOLDER)
|
||||
|
||||
icon_name = 'preferences-desktop-peripherals'
|
||||
icon = Gtk.Image.new_from_icon_name(icon_name, _DEVICE_ICON_SIZE)
|
||||
icon.set_name('icon')
|
||||
icon.set_alignment(0.5, 0)
|
||||
|
||||
label = Gtk.Label('Initializing...')
|
||||
label.set_name('label')
|
||||
label.set_alignment(0, 0.5)
|
||||
label.set_padding(4, 4)
|
||||
|
||||
battery_icon = Gtk.Image.new_from_icon_name('battery_unknown', _STATUS_ICON_SIZE)
|
||||
|
||||
battery_label = Gtk.Label()
|
||||
battery_label.set_width_chars(6)
|
||||
battery_label.set_alignment(0, 0.5)
|
||||
|
||||
light_icon = Gtk.Image.new_from_icon_name('light_unknown', _STATUS_ICON_SIZE)
|
||||
|
||||
light_label = Gtk.Label()
|
||||
light_label.set_alignment(0, 0.5)
|
||||
light_label.set_width_chars(8)
|
||||
|
||||
toolbar = Gtk.Toolbar()
|
||||
toolbar.set_name('toolbar')
|
||||
toolbar.set_style(Gtk.ToolbarStyle.ICONS)
|
||||
toolbar.set_icon_size(Gtk.IconSize.MENU)
|
||||
toolbar.set_show_arrow(False)
|
||||
|
||||
status_box = Gtk.HBox(homogeneous=False, spacing=0)
|
||||
status_box.set_name('status')
|
||||
status_box.pack_start(battery_icon, False, True, 0)
|
||||
status_box.pack_start(battery_label, False, True, 0)
|
||||
status_box.pack_start(light_icon, False, True, 0)
|
||||
status_box.pack_start(light_label, False, True, 0)
|
||||
status_box.pack_end(toolbar, False, False, 0)
|
||||
|
||||
info_label = Gtk.Label()
|
||||
info_label.set_name('info-label')
|
||||
info_label.set_alignment(0, 0.5)
|
||||
info_label.set_padding(8, 2)
|
||||
info_label.set_selectable(True)
|
||||
|
||||
info_box = Gtk.Frame()
|
||||
info_box.add(info_label)
|
||||
|
||||
toggle_info_action = ui.action._toggle_action('info', 'Device info', _toggle_info, info_label, info_box, frame)
|
||||
toolbar.insert(toggle_info_action.create_tool_item(), 0)
|
||||
toolbar.insert(ui.action.unpair(frame).create_tool_item(), -1)
|
||||
|
||||
vbox = Gtk.VBox(homogeneous=False, spacing=4)
|
||||
vbox.pack_start(label, True, True, 0)
|
||||
vbox.pack_start(status_box, True, True, 0)
|
||||
vbox.pack_start(info_box, True, True, 0)
|
||||
|
||||
box = Gtk.HBox(homogeneous=False, spacing=4)
|
||||
box.set_border_width(4)
|
||||
box.pack_start(icon, False, False, 0)
|
||||
box.pack_start(vbox, True, True, 0)
|
||||
box.show_all()
|
||||
|
||||
frame.add(box)
|
||||
info_box.set_visible(False)
|
||||
return frame
|
||||
|
||||
|
||||
def toggle(window, trigger):
|
||||
if window.get_visible():
|
||||
position = window.get_position()
|
||||
window.hide()
|
||||
window.move(*position)
|
||||
else:
|
||||
if trigger and type(trigger) == Gtk.StatusIcon:
|
||||
x, y = window.get_position()
|
||||
if x == 0 and y == 0:
|
||||
x, y, _ = Gtk.StatusIcon.position_menu(Gtk.Menu(), trigger)
|
||||
window.move(x, y)
|
||||
window.present()
|
||||
return True
|
||||
|
||||
|
||||
def create(title, name, max_devices, systray=False):
|
||||
window = Gtk.Window()
|
||||
window.set_title(title)
|
||||
window.set_icon_name(ui.appicon(0))
|
||||
window.set_role('status-window')
|
||||
|
||||
vbox = Gtk.VBox(homogeneous=False, spacing=4)
|
||||
vbox.set_border_width(4)
|
||||
|
||||
rbox = _make_receiver_box(name)
|
||||
vbox.add(rbox)
|
||||
for i in range(1, 1 + max_devices):
|
||||
dbox = _make_device_box(i)
|
||||
vbox.add(dbox)
|
||||
vbox.set_visible(True)
|
||||
|
||||
window.add(vbox)
|
||||
|
||||
geometry = Gdk.Geometry()
|
||||
geometry.min_width = 320
|
||||
geometry.min_height = 32
|
||||
window.set_geometry_hints(vbox, geometry, Gdk.WindowHints.MIN_SIZE)
|
||||
window.set_resizable(False)
|
||||
|
||||
window.toggle_visible = lambda i: toggle(window, i)
|
||||
|
||||
if systray:
|
||||
window.set_keep_above(True)
|
||||
window.connect('delete-event', toggle)
|
||||
else:
|
||||
window.connect('delete-event', Gtk.main_quit)
|
||||
|
||||
return window
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def _update_receiver_box(frame, receiver):
|
||||
label, toolbar, info_label = ui.find_children(frame, 'label', 'toolbar', 'info-label')
|
||||
|
||||
label.set_text(receiver.status_text or '')
|
||||
if receiver.status < STATUS.CONNECTED:
|
||||
toolbar.set_sensitive(False)
|
||||
toolbar.get_children()[0].set_active(False)
|
||||
info_label.set_text('')
|
||||
frame._device = None
|
||||
else:
|
||||
toolbar.set_sensitive(True)
|
||||
frame._device = receiver
|
||||
|
||||
|
||||
def _update_device_box(frame, dev):
|
||||
frame._device = dev
|
||||
|
||||
icon, label, info_label = ui.find_children(frame, 'icon', 'label', 'info-label')
|
||||
|
||||
if frame.get_name() != dev.name:
|
||||
frame.set_name(dev.name)
|
||||
icon_name = ui.get_icon(dev.name, dev.kind)
|
||||
icon.set_from_icon_name(icon_name, _DEVICE_ICON_SIZE)
|
||||
label.set_markup('<b>' + dev.name + '</b>')
|
||||
|
||||
status = ui.find_children(frame, 'status')
|
||||
status_icons = status.get_children()
|
||||
toolbar = status_icons[-1]
|
||||
if dev.status < STATUS.CONNECTED:
|
||||
icon.set_sensitive(False)
|
||||
label.set_sensitive(False)
|
||||
status.set_sensitive(False)
|
||||
for c in status_icons[1:-1]:
|
||||
c.set_visible(False)
|
||||
toolbar.get_children()[0].set_active(False)
|
||||
else:
|
||||
icon.set_sensitive(True)
|
||||
label.set_sensitive(True)
|
||||
status.set_sensitive(True)
|
||||
|
||||
battery_icon, battery_label = status_icons[0:2]
|
||||
battery_level = dev.props.get(PROPS.BATTERY_LEVEL)
|
||||
if battery_level is None:
|
||||
battery_icon.set_from_icon_name('battery_unknown', _STATUS_ICON_SIZE)
|
||||
battery_icon.set_sensitive(False)
|
||||
battery_label.set_visible(False)
|
||||
else:
|
||||
icon_name = 'battery_%03d' % (20 * ((battery_level + 10) // 20))
|
||||
battery_icon.set_from_icon_name(icon_name, _STATUS_ICON_SIZE)
|
||||
battery_icon.set_sensitive(True)
|
||||
battery_label.set_text('%d%%' % battery_level)
|
||||
battery_label.set_visible(True)
|
||||
|
||||
battery_status = dev.props.get(PROPS.BATTERY_STATUS)
|
||||
battery_icon.set_tooltip_text(battery_status or '')
|
||||
|
||||
light_icon, light_label = status_icons[2:4]
|
||||
light_level = dev.props.get(PROPS.LIGHT_LEVEL)
|
||||
if light_level is None:
|
||||
light_icon.set_visible(False)
|
||||
light_label.set_visible(False)
|
||||
else:
|
||||
icon_name = 'light_%03d' % (20 * ((light_level + 50) // 100))
|
||||
light_icon.set_from_icon_name(icon_name, _STATUS_ICON_SIZE)
|
||||
light_icon.set_visible(True)
|
||||
light_label.set_text('%d lux' % light_level)
|
||||
light_label.set_visible(True)
|
||||
|
||||
for b in toolbar.get_children()[:-1]:
|
||||
b.set_sensitive(True)
|
||||
|
||||
frame.set_visible(True)
|
||||
|
||||
|
||||
def update(window, receiver, device=None):
|
||||
# print ("update", receiver, receiver.status, device)
|
||||
window.set_icon_name(ui.appicon(receiver.status))
|
||||
|
||||
vbox = window.get_child()
|
||||
frames = list(vbox.get_children())
|
||||
|
||||
if device is None:
|
||||
_update_receiver_box(frames[0], receiver)
|
||||
if receiver.status < STATUS.CONNECTED:
|
||||
for frame in frames[1:]:
|
||||
frame.set_visible(False)
|
||||
frame.set_name(_PLACEHOLDER)
|
||||
frame._device = None
|
||||
else:
|
||||
frame = frames[device.number]
|
||||
if device.status == STATUS.UNPAIRED:
|
||||
frame.set_visible(False)
|
||||
frame.set_name(_PLACEHOLDER)
|
||||
frame._device = None
|
||||
else:
|
||||
_update_device_box(frame, device)
|
||||
@@ -1,75 +0,0 @@
|
||||
#
|
||||
# Optional desktop notifications.
|
||||
#
|
||||
|
||||
import logging
|
||||
|
||||
|
||||
try:
|
||||
from gi.repository import Notify
|
||||
|
||||
import ui
|
||||
from logitech.devices.constants import STATUS
|
||||
|
||||
# necessary because the notifications daemon does not know about our XDG_DATA_DIRS
|
||||
_icons = {}
|
||||
|
||||
def _icon(title):
|
||||
if title not in _icons:
|
||||
_icons[title] = ui.icon_file(title)
|
||||
|
||||
return _icons.get(title)
|
||||
|
||||
# assumed to be working since the import succeeded
|
||||
available = True
|
||||
|
||||
_notifications = {}
|
||||
|
||||
|
||||
def init(app_title):
|
||||
"""Init the notifications system."""
|
||||
global available
|
||||
if available:
|
||||
if not Notify.is_initted():
|
||||
logging.info("starting desktop notifications")
|
||||
try:
|
||||
return Notify.init(app_title)
|
||||
except:
|
||||
logging.exception("initializing desktop notifications")
|
||||
available = False
|
||||
return available and Notify.is_initted()
|
||||
|
||||
|
||||
def uninit():
|
||||
if available and Notify.is_initted():
|
||||
logging.info("stopping desktop notifications")
|
||||
_notifications.clear()
|
||||
Notify.uninit()
|
||||
|
||||
|
||||
def show(dev):
|
||||
"""Show a notification with title and text."""
|
||||
if available and Notify.is_initted():
|
||||
summary = dev.name
|
||||
|
||||
# if a notification with same name is already visible, reuse it to avoid spamming
|
||||
n = _notifications.get(summary)
|
||||
if n is None:
|
||||
n = _notifications[summary] = Notify.Notification()
|
||||
|
||||
n.update(summary, dev.status_text, _icon(summary) or dev.kind)
|
||||
urgency = Notify.Urgency.LOW if dev.status > STATUS.CONNECTED else Notify.Urgency.NORMAL
|
||||
n.set_urgency(urgency)
|
||||
|
||||
try:
|
||||
# logging.debug("showing %s", n)
|
||||
n.show()
|
||||
except Exception:
|
||||
logging.exception("showing %s", n)
|
||||
|
||||
except ImportError:
|
||||
logging.warn("desktop notifications disabled")
|
||||
available = False
|
||||
init = lambda app_title: False
|
||||
uninit = lambda: None
|
||||
show = lambda dev: None
|
||||
@@ -1,125 +0,0 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
# import logging
|
||||
from gi.repository import (Gtk, GObject)
|
||||
|
||||
import ui
|
||||
|
||||
|
||||
def _create_page(assistant, text, kind):
|
||||
p = Gtk.VBox(False, 12)
|
||||
p.set_border_width(8)
|
||||
|
||||
if text:
|
||||
label = Gtk.Label(text)
|
||||
label.set_alignment(0, 0)
|
||||
p.pack_start(label, False, True, 0)
|
||||
|
||||
assistant.append_page(p)
|
||||
assistant.set_page_type(p, kind)
|
||||
|
||||
p.show_all()
|
||||
return p
|
||||
|
||||
|
||||
def _device_confirmed(entry, _2, trigger, assistant, page):
|
||||
assistant.commit()
|
||||
assistant.set_page_complete(page, True)
|
||||
return True
|
||||
|
||||
|
||||
def _finish(assistant):
|
||||
# logging.debug("finish %s", assistant)
|
||||
assistant.destroy()
|
||||
|
||||
def _cancel(assistant, state):
|
||||
# logging.debug("cancel %s", assistant)
|
||||
state.stop_scan()
|
||||
_finish(assistant)
|
||||
|
||||
def _prepare(assistant, page, state):
|
||||
index = assistant.get_current_page()
|
||||
# logging.debug("prepare %s %d %s", assistant, index, page)
|
||||
|
||||
if index == 0:
|
||||
state.reset()
|
||||
GObject.timeout_add(state.TICK, state.countdown, assistant)
|
||||
spinner = page.get_children()[-1]
|
||||
spinner.start()
|
||||
return
|
||||
|
||||
assistant.remove_page(0)
|
||||
state.stop_scan()
|
||||
|
||||
|
||||
def _scan_complete_ui(assistant, device):
|
||||
if device is None:
|
||||
page = _create_page(assistant,
|
||||
'No new device detected.\n'
|
||||
'\n'
|
||||
'Make sure your device is within range of the receiver,\nand it has a decent battery charge.\n',
|
||||
Gtk.AssistantPageType.CONFIRM)
|
||||
else:
|
||||
page = _create_page(assistant,
|
||||
None,
|
||||
Gtk.AssistantPageType.CONFIRM)
|
||||
|
||||
hbox = Gtk.HBox(False, 16)
|
||||
device_icon = Gtk.Image()
|
||||
device_icon.set_from_icon_name(ui.get_icon(device.name, device.kind), Gtk.IconSize.DIALOG)
|
||||
hbox.pack_start(device_icon, False, False, 0)
|
||||
device_label = Gtk.Label(device.kind + '\n' + device.name)
|
||||
hbox.pack_start(device_label, False, False, 0)
|
||||
halign = Gtk.Alignment.new(0.5, 0.5, 0, 1)
|
||||
halign.add(hbox)
|
||||
page.pack_start(halign, False, True, 0)
|
||||
|
||||
hbox = Gtk.HBox(False, 16)
|
||||
hbox.pack_start(Gtk.Entry(), False, False, 0)
|
||||
hbox.pack_start(Gtk.ToggleButton('Test'), False, False, 0)
|
||||
halign = Gtk.Alignment.new(0.5, 0.5, 0, 1)
|
||||
halign.add(hbox)
|
||||
page.pack_start(halign, False, False, 0)
|
||||
|
||||
entry_info = Gtk.Label('Use the controls above to confirm\n'
|
||||
'this is the device you want to pair.')
|
||||
entry_info.set_sensitive(False)
|
||||
page.pack_start(entry_info, False, False, 0)
|
||||
|
||||
page.show_all()
|
||||
assistant.set_page_complete(page, True)
|
||||
|
||||
assistant.next_page()
|
||||
|
||||
def _scan_complete(assistant, device):
|
||||
GObject.idle_add(_scan_complete_ui, assistant, device)
|
||||
|
||||
|
||||
def create(action, state):
|
||||
assistant = Gtk.Assistant()
|
||||
assistant.set_title(action.get_label())
|
||||
assistant.set_icon_name(action.get_icon_name())
|
||||
|
||||
assistant.set_size_request(440, 240)
|
||||
assistant.set_resizable(False)
|
||||
assistant.set_role('pair-device')
|
||||
|
||||
page_intro = _create_page(assistant,
|
||||
'Turn on the device you want to pair.\n'
|
||||
'\n'
|
||||
'If the device is already turned on,\nturn if off and on again.',
|
||||
Gtk.AssistantPageType.INTRO)
|
||||
spinner = Gtk.Spinner()
|
||||
spinner.set_visible(True)
|
||||
page_intro.pack_end(spinner, True, True, 16)
|
||||
|
||||
assistant.scan_complete = _scan_complete
|
||||
|
||||
assistant.connect('prepare', _prepare, state)
|
||||
assistant.connect('cancel', _cancel, state)
|
||||
assistant.connect('close', _finish)
|
||||
assistant.connect('apply', _finish)
|
||||
|
||||
return assistant
|
||||
@@ -1,55 +0,0 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from gi.repository import Gtk
|
||||
import ui
|
||||
|
||||
|
||||
def create(window, menu_actions=None):
|
||||
icon = Gtk.StatusIcon()
|
||||
icon.set_title(window.get_title())
|
||||
icon.set_name(window.get_title())
|
||||
icon.set_from_icon_name(ui.appicon(0))
|
||||
|
||||
icon.connect('activate', window.toggle_visible)
|
||||
|
||||
menu = Gtk.Menu()
|
||||
for action in menu_actions or ():
|
||||
if action:
|
||||
menu.append(action.create_menu_item())
|
||||
|
||||
menu.append(ui.action.quit.create_menu_item())
|
||||
menu.show_all()
|
||||
|
||||
icon.connect('popup_menu',
|
||||
lambda icon, button, time, menu:
|
||||
menu.popup(None, None, icon.position_menu, icon, button, time),
|
||||
menu)
|
||||
|
||||
return icon
|
||||
|
||||
|
||||
def update(icon, receiver):
|
||||
icon.set_from_icon_name(ui.appicon(receiver.status))
|
||||
|
||||
if receiver.devices:
|
||||
lines = []
|
||||
if receiver.status < 1:
|
||||
lines += (receiver.status_text, '')
|
||||
|
||||
devlist = [receiver.devices[d] for d in range(1, 1 + receiver.max_devices) if d in receiver.devices]
|
||||
for dev in devlist:
|
||||
name = '<b>' + dev.name + '</b>'
|
||||
if dev.status < 1:
|
||||
lines.append(name + ' (' + dev.status_text + ')')
|
||||
else:
|
||||
lines.append(name)
|
||||
if dev.status > 1:
|
||||
lines.append(' ' + dev.status_text)
|
||||
lines.append('')
|
||||
|
||||
text = '\n'.join(lines).rstrip('\n')
|
||||
icon.set_tooltip_markup(text)
|
||||
else:
|
||||
icon.set_tooltip_text(receiver.status_text)
|
||||
@@ -1,9 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
Z=`readlink -f "$0"`
|
||||
LIB=`readlink -f $(dirname "$Z")/../lib`
|
||||
#export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$LIB/native/`arch`
|
||||
export PYTHONPATH=$LIB
|
||||
|
||||
PYTHON=`which python python2 python3 | head -n 1`
|
||||
exec $PYTHON -OOu -m hidapi.hidconsole "$@"
|
||||
9
bin/scan
9
bin/scan
@@ -1,9 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
Z=`readlink -f "$0"`
|
||||
LIB=`readlink -f $(dirname "$Z")/../lib`
|
||||
#export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$LIB/native/`arch`
|
||||
export PYTHONPATH=$LIB
|
||||
|
||||
PYTHON=`which python python2 python3 | head -n 1`
|
||||
exec $PYTHON -OOu -m logitech.scanner "$@"
|
||||
51
bin/solaar
51
bin/solaar
@@ -1,13 +1,44 @@
|
||||
#!/bin/sh
|
||||
#!/usr/bin/env python
|
||||
# -*- python-mode -*-
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
Z=`readlink -f "$0"`
|
||||
APP=`readlink -f $(dirname "$Z")/../app`
|
||||
LIB=`readlink -f $(dirname "$Z")/../lib`
|
||||
SHARE=`readlink -f $(dirname "$Z")/../share`
|
||||
## Copyright (C) 2012-2013 Daniel Pavel
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published by
|
||||
## the Free Software Foundation; either version 2 of the License, or
|
||||
## (at your option) any later version.
|
||||
##
|
||||
## This program is distributed in the hope that it will be useful,
|
||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
## GNU General Public License for more details.
|
||||
##
|
||||
## You should have received a copy of the GNU General Public License along
|
||||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
#export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$LIB/native/`arch`
|
||||
export PYTHONPATH=$APP:$LIB
|
||||
export XDG_DATA_DIRS=$SHARE:$XDG_DATA_DIRS
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
PYTHON=`which python python2 python3 | head -n 1`
|
||||
exec $PYTHON -OOu -m solaar "$@"
|
||||
|
||||
def init_paths():
|
||||
"""Make the app work in the source tree."""
|
||||
import sys
|
||||
import os.path as _path
|
||||
|
||||
prefix = _path.normpath(_path.join(_path.realpath(sys.path[0]), '..'))
|
||||
src_lib = _path.join(prefix, 'lib')
|
||||
share_lib = _path.join(prefix, 'share', 'solaar', 'lib')
|
||||
for location in src_lib, share_lib:
|
||||
init_py = _path.join(location, 'solaar', '__init__.py')
|
||||
# print ("sys.path[0]: checking", init_py)
|
||||
if _path.exists(init_py):
|
||||
# print ("sys.path[0]: found", location, "replacing", sys.path[0])
|
||||
sys.path[0] = location
|
||||
break
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
init_paths()
|
||||
import solaar.gtk
|
||||
solaar.gtk.main()
|
||||
|
||||
42
bin/solaar-cli
Executable file
42
bin/solaar-cli
Executable file
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- python-mode -*-
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
## Copyright (C) 2012-2013 Daniel Pavel
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published by
|
||||
## the Free Software Foundation; either version 2 of the License, or
|
||||
## (at your option) any later version.
|
||||
##
|
||||
## This program is distributed in the hope that it will be useful,
|
||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
## GNU General Public License for more details.
|
||||
##
|
||||
## You should have received a copy of the GNU General Public License along
|
||||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
|
||||
def init_paths():
|
||||
"""Make the app work in the source tree."""
|
||||
import sys
|
||||
import os.path as _path
|
||||
|
||||
prefix = _path.normpath(_path.join(_path.realpath(sys.path[0]), '..'))
|
||||
src_lib = _path.join(prefix, 'lib')
|
||||
share_lib = _path.join(prefix, 'share', 'solaar', 'lib')
|
||||
for location in src_lib, share_lib:
|
||||
init_py = _path.join(location, 'solaar', '__init__.py')
|
||||
if _path.exists(init_py):
|
||||
sys.path[0] = location
|
||||
break
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
init_paths()
|
||||
import solaar.cli
|
||||
solaar.cli.main()
|
||||
BIN
docs/20121210110342697.pdf
Normal file
BIN
docs/20121210110342697.pdf
Normal file
Binary file not shown.
7
docs/debian.md
Normal file
7
docs/debian.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Debian repository
|
||||
|
||||
To use this repository with your Debian machine, create a file `solaar.list` in
|
||||
`/etc/apt/sources.list.d/`, with the following contents:
|
||||
|
||||
deb http://pwr.github.io/Solaar/packages/ ./
|
||||
deb-src http://pwr.github.io/Solaar/packages/ ./
|
||||
139
docs/devices.md
Normal file
139
docs/devices.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# Supported devices
|
||||
|
||||
**Solaar** will detect all devices paired with your receiver, and at the very
|
||||
least display some basic information about them.
|
||||
|
||||
At this moment, all [Unifying Receiver][unifying] are supported (devices with
|
||||
USB ID `046d:c52b` or `046d:c532`), but only some newer [Nano Receiver][nano]s
|
||||
(devices with USB ID `046d:c52f`). You can check your connected Logitech devices
|
||||
by running `lsusb -d 046d:` in a console.
|
||||
|
||||
For some devices, extra settings (usually not available through the standard
|
||||
Linux system configuration) are supported:
|
||||
|
||||
* The [K750 Solar Keyboard][K750] is also queried for its solar charge status.
|
||||
Pressing the `Light-Check` button on the keyboard will pop-up the application
|
||||
window and display the current lighting value (Lux) as reported by the
|
||||
keyboard, similar to Logitech's *Solar.app* for Windows.
|
||||
|
||||
* The state of the `FN` key can be toggled on some keyboards ([K360][K360],
|
||||
[MK700][K700], [K750][K750] and [K800][K800]). It changes the way the function
|
||||
keys (`F1`..`F12`) work, i.e. whether holding `FN` while pressing the function
|
||||
keys will generate the standard `Fx` keycodes or the special function (yellow
|
||||
icons) keycodes.
|
||||
|
||||
* The DPI can be changed on the [Performance MX Mouse][P_MX].
|
||||
|
||||
* Smooth scrolling (higher sensitivity on vertical scrolling with the wheel) can
|
||||
be toggled on the [M705 Marathon Mouse][M705] and [M510 Wireless Mouse][M510].
|
||||
|
||||
|
||||
# Supported features
|
||||
|
||||
These tables list all known Logitech [Unifying][unifying] devices, and to what
|
||||
degree their features are supported by Solaar. If your device is not listed here
|
||||
at all, it is very unlikely Solaar would be able to support it.
|
||||
|
||||
The information in these tables is incomplete, based on what devices myself and
|
||||
other users have been able to test Solaar with. If your device works with
|
||||
Solaar, but its supported features are not specified here, I would love to hear
|
||||
about it.
|
||||
|
||||
|
||||
Devices marked with an asterisk (*) use a Nano receiver that knows the Unifying
|
||||
protocol, and should be fully supported by Solaar.
|
||||
|
||||
The HID++ column specifies the device's HID++ version.
|
||||
|
||||
The Battery column specifies if Solaar is able to read the device's battery
|
||||
level.
|
||||
|
||||
For mice, the DPI column specifies if the mouse's sensitivity is fixed (`-`),
|
||||
can only be read (`R`), or can be read and changed by Solaar (`R/W`).
|
||||
|
||||
The reprog(rammable) keys feature is currently not fully supported by Solaar.
|
||||
You are able to read this feature using solaar-cli, but it is not possible to
|
||||
assign different keys.
|
||||
|
||||
|
||||
Keyboards:
|
||||
|
||||
| Device | HID++ | Battery | Other supported features |
|
||||
|------------------|-------|---------|-----------------------------------------|
|
||||
| K230 | 2.0 | yes | |
|
||||
| K270 | | | |
|
||||
| K340 | | | |
|
||||
| K350 | | | |
|
||||
| K360 | 2.0 | yes | FN swap, reprog keys |
|
||||
| K400 Touch | 2.0 | yes | |
|
||||
| K750 Solar | 2.0 | yes | FN swap, Lux reading, light button |
|
||||
| K800 Illuminated | 1.0 | yes | FN swap, reprog keys |
|
||||
| MK700 | 1.0 | yes | FN swap, reprog keys |
|
||||
|
||||
|
||||
Mice:
|
||||
|
||||
| Device | HID++ | Battery | DPI | Other supported features |
|
||||
|------------------|-------|---------|-------|---------------------------------|
|
||||
| V450 Nano | 1.0 | yes | - | smooth scrolling |
|
||||
| V550 Nano | 1.0 | yes | - | smooth scrolling |
|
||||
| VX Nano | 1.0 | yes | - | smooth scrolling |
|
||||
| M175 * | | yes | | |
|
||||
| M185 * | | yes | | |
|
||||
| M187 * | 2.0 | yes | | |
|
||||
| M215 * | 1.0 | yes | | |
|
||||
| M235 * | | yes | | |
|
||||
| M305 * | 1.0 | yes | | |
|
||||
| M310 * | | yes | | |
|
||||
| M315 * | | yes | | |
|
||||
| M317 | | | | |
|
||||
| M325 | | | | |
|
||||
| M345 | 2.0 | yes | - | |
|
||||
| M505 | 1.0 | yes | | |
|
||||
| M510 | 1.0 | yes | | smooth scrolling |
|
||||
| M515 Couch | 2.0 | yes | - | |
|
||||
| M525 | 2.0 | yes | - | |
|
||||
| M600 Touch | 2.0 | yes | | |
|
||||
| M705 Marathon | 1.0 | yes | - | smooth scrolling |
|
||||
| T400 Zone Touch | | | | |
|
||||
| T620 Touch | 2.0 | | | |
|
||||
| Performance MX | 1.0 | yes | R/W | |
|
||||
| Anywhere MX | 1.0 | yes | - | |
|
||||
| Cube | 2.0 | yes | | |
|
||||
|
||||
|
||||
Trackballs:
|
||||
|
||||
| Device | HID++ | Battery | DPI | Other supported features |
|
||||
|------------------|-------|---------|-------|---------------------------------|
|
||||
| M570 Trackball | | | | |
|
||||
|
||||
|
||||
Touchpads:
|
||||
|
||||
| Device | HID++ | Battery | DPI | Other supported features |
|
||||
|------------------|-------|---------|-------|---------------------------------|
|
||||
| Wireless Touch | 2.0 | | | |
|
||||
| T650 Touchpad | 2.0 | | | |
|
||||
|
||||
|
||||
Mouse-Keyboard combos:
|
||||
|
||||
| Device | HID++ | Battery | Other supported features |
|
||||
|------------------|-------|---------|-----------------------------------------|
|
||||
| MK330 | | | |
|
||||
| MK520 | | | |
|
||||
| MK550 | | | |
|
||||
| MK710 | 1.0 | yes | FN swap, reprog keys |
|
||||
|
||||
|
||||
[unifying]: http://logitech.com/en-us/66/6079
|
||||
[nano]: http://logitech.com/mice-pointers/articles/5926
|
||||
[K360]: http://logitech.com/product/keyboard-k360
|
||||
[K700]: http://logitech.com/product/wireless-desktop-mk710
|
||||
[K750]: http://logitech.com/product/k750-keyboard
|
||||
[K800]: http://logitech.com/product/wireless-illuminated-keyboard-k800
|
||||
[M510]: http://logitech.com/product/wireless-mouse-m510
|
||||
[M705]: http://logitech.com/product/marathon-mouse-m705
|
||||
[P_MX]: http://logitech.com/product/performance-mouse-mx
|
||||
[A_MX]: http://logitech.com/product/anywhere-mouse-mx
|
||||
93
docs/devices/k360.txt
Normal file
93
docs/devices/k360.txt
Normal file
@@ -0,0 +1,93 @@
|
||||
Receiver
|
||||
LZ22175-DJ
|
||||
LZ30965-DJ (another receiver)
|
||||
M/N:C-U0007
|
||||
(ltunify)
|
||||
Serial number: 53B19204
|
||||
Serial number: 82C3964B (another receiver)
|
||||
Firmware version: 012.001.00019
|
||||
Bootloader version: BL.002.014
|
||||
(solaar-cli)
|
||||
-: Unifying Receiver
|
||||
Device path : /dev/hidraw2
|
||||
Serial : 53B19204
|
||||
Serial : 82C3964B (another receiver)
|
||||
Firmware : 12.01.B0019
|
||||
Bootloader : 02.14
|
||||
Has 1 paired device(s) out of a maximum of 6
|
||||
Enabled notifications: 0x000900 = wireless, software present.
|
||||
|
||||
Keyboard
|
||||
K360
|
||||
P/N: 820-003472
|
||||
S/N: 1223CE0521E8
|
||||
S/N: 1311CE0097D8 (another keyboard)
|
||||
M/N: Y-R0017
|
||||
(ltunify)
|
||||
HID++ version: 2.0
|
||||
Device index 1
|
||||
Keyboard
|
||||
Name: K360
|
||||
Wireless Product ID: 4004
|
||||
Serial number: 60BA944E
|
||||
Device was unavailable, version information not available.
|
||||
Total number of HID++ 2.0 features: 12
|
||||
0: [0000] IRoot
|
||||
1: [0001] IFeatureSet
|
||||
2: [0003] IFirmwareInfo
|
||||
3: [0005] GetDeviceNameType
|
||||
4: [1000] batteryLevelStatus
|
||||
5: [1820] H unknown
|
||||
6: [1B00] SpecialKeysMSEButtons
|
||||
7: [1D4B] WirelessDeviceStatus
|
||||
8: [1DF0] H unknown
|
||||
9: [1DF3] H unknown
|
||||
10: [40A0] FnInversion
|
||||
11: [4100] Encryption
|
||||
12: [4520] KeyboardLayout
|
||||
(O = obsolete feature; H = SW hidden feature)
|
||||
(solaar-cli)
|
||||
1: Wireless Keyboard K360
|
||||
Codename : K360
|
||||
Kind : keyboard
|
||||
Protocol : HID++ 2.0
|
||||
Polling rate : 20 ms
|
||||
Wireless PID : 4004
|
||||
Serial number: 60BA944E
|
||||
Serial number: 0D2694C9 (another keyboard)
|
||||
Firmware : RQK 36.00.B0007
|
||||
The power switch is located on the top case
|
||||
Supports 13 HID++ 2.0 features:
|
||||
0: ROOT {0000}
|
||||
1: FEATURE SET {0001}
|
||||
2: DEVICE FW VERSION {0003}
|
||||
3: DEVICE NAME {0005}
|
||||
4: BATTERY STATUS {1000}
|
||||
5: unknown:1820 {1820} hidden
|
||||
6: REPROG CONTROLS {1B00}
|
||||
7: WIRELESS DEVICE STATUS {1D4B}
|
||||
8: unknown:1DF0 {1DF0} hidden
|
||||
9: unknown:1DF3 {1DF3} hidden
|
||||
10: FN INVERSION {40A0}
|
||||
11: ENCRYPTION {4100}
|
||||
12: KEYBOARD LAYOUT {4520}
|
||||
Has 18 reprogrammable keys:
|
||||
0: MY HOME => HomePage FN sensitive, is FN, reprogrammable
|
||||
1: Mail => Mail FN sensitive, is FN, reprogrammable
|
||||
2: SEARCH => Search FN sensitive, is FN, reprogrammable
|
||||
3: MEDIA PLAYER => Music FN sensitive, is FN, reprogrammable
|
||||
4: Application Switcher => Application Switcher FN sensitive, is FN, reprogrammable
|
||||
5: SHOW DESKTOP => ShowDesktop FN sensitive, is FN, reprogrammable
|
||||
6: MINIMIZE AS WIN M => WindowsMinimize FN sensitive, is FN, reprogrammable
|
||||
7: MAXIMIZE AS WIN SHIFT M => WindowsRestore FN sensitive, is FN, reprogrammable
|
||||
8: MY COMPUTER AS WIN E => My Computer FN sensitive, is FN, reprogrammable
|
||||
9: Lock PC => WindowsLock FN sensitive, is FN, reprogrammable
|
||||
10: SLEEP => Sleep FN sensitive, is FN, reprogrammable
|
||||
11: Calculator => Calculator FN sensitive, is FN, reprogrammable
|
||||
12: Previous => Previous nonstandard
|
||||
13: Play/Pause => Play/Pause nonstandard
|
||||
14: Next => Next nonstandard
|
||||
15: Mute => Mute nonstandard
|
||||
16: Volume Down => Volume Down nonstandard
|
||||
17: Volume Up => Volume Up nonstandard
|
||||
Battery is 90% charged, discharging
|
||||
52
docs/devices/k800.txt
Normal file
52
docs/devices/k800.txt
Normal file
@@ -0,0 +1,52 @@
|
||||
# 0x00 - Enabled Notifications. rw (see HID++ 1.0 spec)
|
||||
<< ( 0.055) [10 02 8100 000000] '\x10\x02\x81\x00\x00\x00\x00'
|
||||
>> ( 0.084) [10 02 8100 000000] '\x10\x02\x81\x00\x00\x00\x00'
|
||||
|
||||
# 0x01 - Keyboard hand detection. rw, last param is 00 when hand detection is
|
||||
# enabled, 30 when disabled. (when enabled, keyboard will light up if not
|
||||
# already when hovering over the front)
|
||||
<< ( 1.085) [10 02 8101 000000] '\x10\x02\x81\x01\x00\x00\x00'
|
||||
>> ( 1.114) [10 02 8101 000000] '\x10\x02\x81\x01\x00\x00\x00'
|
||||
|
||||
# 0x07 - Battery status (3 = one bar; 1 = red/critical; 5=two bars; 7=three
|
||||
# bars/full. Second returned param is 25 when keyboard is charging )
|
||||
<< ( 7.327) [10 02 8107 000000] '\x10\x02\x81\x07\x00\x00\x00'
|
||||
>> ( 7.368) [10 02 8107 030000] '\x10\x02\x81\x07\x03\x00\x00'
|
||||
|
||||
# 0x09 - F key function. rw (read: status, set/get: 00 01 00 means swap
|
||||
# functions, 00 00 00 means do not swap functions)
|
||||
<< ( 9.411) [10 02 8109 000000] '\x10\x02\x81\t\x00\x00\x00'
|
||||
>> ( 9.440) [10 02 8109 000000] '\x10\x02\x81\t\x00\x00\x00'
|
||||
|
||||
# 0x17 - Illumination info r/w. Last param: 02 to disable backlight, 01 to
|
||||
# enable backlight
|
||||
<< ( 24.965) [10 02 8117 000000] '\x10\x02\x81\x17\x00\x00\x00'
|
||||
>> ( 24.988) [10 02 8117 3C0001] '\x10\x02\x81\x17<\x00\x01'
|
||||
|
||||
# 0x51 - ?
|
||||
<< ( 99.294) [10 02 8151 000000] '\x10\x02\x81Q\x00\x00\x00'
|
||||
>> ( 99.543) [10 02 8151 000000] '\x10\x02\x81Q\x00\x00\x00'
|
||||
|
||||
# 0x54 - ?
|
||||
<< ( 103.046) [10 02 8154 000000] '\x10\x02\x81T\x00\x00\x00'
|
||||
>> ( 103.295) [10 02 8154 FF0000] '\x10\x02\x81T\xff\x00\x00'
|
||||
|
||||
# 0xD0 - ?
|
||||
<< ( 253.860) [10 02 81D0 000000] '\x10\x02\x81\xd0\x00\x00\x00'
|
||||
>> ( 253.883) [10 02 81D0 000000] '\x10\x02\x81\xd0\x00\x00\x00'
|
||||
|
||||
# 0xF1 - Version info (params 0n 00 00 where n is 1..4)
|
||||
<< ( 289.991) [10 02 81F1 000000] '\x10\x02\x81\xf1\x00\x00\x00'
|
||||
>> ( 290.032) [10 02 8F81 F10300] '\x10\x02\x8f\x81\xf1\x03\x00'
|
||||
|
||||
# 0xF3 - ?
|
||||
<< ( 292.075) [10 02 81F3 000000] '\x10\x02\x81\xf3\x00\x00\x00'
|
||||
>> ( 292.116) [10 02 81F3 000000] '\x10\x02\x81\xf3\x00\x00\x00'
|
||||
|
||||
# 0x0F - This changes, the last commented line was observed in an earlier run
|
||||
<< ( 17.728) [10 02 830F 000000] '\x10\x02\x83\x0f\x00\x00\x00'
|
||||
>> ( 17.976) [11 02 830F FFFB00000240025C000000000FF90080] '\x11\x02\x83\x0f\xff\xfb\x00\x00\x02@\x02\\\x00\x00\x00\x00\x0f\xf9\x00\x80'
|
||||
#>> ( 17.999) [11 02 830F FFFC007F0243025D000000000FF60080] '\x11\x02\x83\x0f\xff\xfc\x00\x7f\x02C\x02]\x00\x00\x00\x00\x0f\xf6\x00\x80'
|
||||
|
||||
# See also https://git.lekensteyn.nl/ltunify/tree/registers.txt for a verbose
|
||||
# meaning of registers and params.
|
||||
56
docs/devices/m345.txt
Normal file
56
docs/devices/m345.txt
Normal file
@@ -0,0 +1,56 @@
|
||||
Receiver
|
||||
LZ141AX-DJ
|
||||
M/N: C-U0008
|
||||
(ltunify)
|
||||
Serial number: 574197D3
|
||||
Firmware version: 024.000.00018
|
||||
Bootloader version: BL.000.006
|
||||
|
||||
Mouse
|
||||
HID++ version: 2.0
|
||||
Device index 1
|
||||
Mouse
|
||||
Name: M345
|
||||
Wireless Product ID: 4017
|
||||
Serial number: 920DC223
|
||||
Device was unavailable, version information not available.
|
||||
Total number of HID++ 2.0 features: 12
|
||||
0: [0000] IRoot
|
||||
1: [0001] IFeatureSet
|
||||
2: [0003] IFirmwareInfo
|
||||
3: [0005] GetDeviceNameType
|
||||
4: [1000] batteryLevelStatus
|
||||
5: [1D4B] WirelessDeviceStatus
|
||||
6: [1DF3] H unknown
|
||||
7: [1B00] SpecialKeysMSEButtons
|
||||
8: [1DF0] H unknown
|
||||
9: [1F03] H unknown
|
||||
10: [2100] VerticalScrolling
|
||||
11: [2120] HiResScrolling
|
||||
12: [2200] MousePointer
|
||||
(O = obsolete feature; H = SW hidden feature)
|
||||
(solaar-cli)
|
||||
1: Wireless Mouse M345
|
||||
Codename : M345
|
||||
Kind : mouse
|
||||
Wireless PID : 4017
|
||||
Protocol : HID++ 2.0
|
||||
Polling rate : 8 ms
|
||||
Serial number: 920DC223
|
||||
Firmware: RQM 27.02.B0028
|
||||
The power switch is located on the base.
|
||||
Supports 13 HID++ 2.0 features:
|
||||
0: ROOT {0000}
|
||||
1: FEATURE SET {0001}
|
||||
2: DEVICE FW VERSION {0003}
|
||||
3: DEVICE NAME {0005}
|
||||
4: BATTERY STATUS {1000}
|
||||
5: WIRELESS DEVICE STATUS {1D4B}
|
||||
6: unknown:1DF3 {1DF3} hidden
|
||||
7: REPROG CONTROLS {1B00}
|
||||
8: unknown:1DF0 {1DF0} hidden
|
||||
9: unknown:1F03 {1F03} hidden
|
||||
10: VERTICAL SCROLLING {2100}
|
||||
11: HI RES SCROLLING {2120}
|
||||
12: MOUSE POINTER {2200}
|
||||
Battery: 90%, discharging,
|
||||
35
docs/devices/m510.txt
Normal file
35
docs/devices/m510.txt
Normal file
@@ -0,0 +1,35 @@
|
||||
# notification flags
|
||||
<< ( 0.001) [10 01 8100 000000] '\x10\x01\x81\x00\x00\x00\x00'
|
||||
>> ( 0.062) [10 01 8100 000000] '\x10\x01\x81\x00\x00\x00\x00'
|
||||
|
||||
# smooth scroll
|
||||
<< ( 1.063) [10 01 8101 000000] '\x10\x01\x81\x01\x00\x00\x00'
|
||||
>> ( 1.078) [10 01 8101 820000] '\x10\x01\x81\x01\x82\x00\x00'
|
||||
|
||||
# ?
|
||||
<< ( 2.079) [10 01 8102 000000] '\x10\x01\x81\x02\x00\x00\x00'
|
||||
>> ( 2.094) [10 01 8102 000080] '\x10\x01\x81\x02\x00\x00\x80'
|
||||
|
||||
# battery status
|
||||
<< ( 7.263) [10 01 8107 000000] '\x10\x01\x81\x07\x00\x00\x00'
|
||||
>> ( 7.278) [10 01 8107 050000] '\x10\x01\x81\x07\x05\x00\x00'
|
||||
|
||||
# ?
|
||||
<< ( 41.121) [10 01 8128 000000] '\x10\x01\x81(\x00\x00\x00'
|
||||
>> ( 41.136) [10 01 8128 000200] '\x10\x01\x81(\x00\x02\x00'
|
||||
|
||||
# ?
|
||||
<< ( 215.788) [10 01 81D0 000000] '\x10\x01\x81\xd0\x00\x00\x00'
|
||||
>> ( 215.802) [10 01 81D0 000000] '\x10\x01\x81\xd0\x00\x00\x00'
|
||||
|
||||
# read-only, 01-04 firmware info
|
||||
<< ( 250.779) [10 01 81F1 000000] '\x10\x01\x81\xf1\x00\x00\x00'
|
||||
>> ( 250.794) [10 01 8F81 F10300] '\x10\x01\x8f\x81\xf1\x03\x00'
|
||||
|
||||
# ?
|
||||
<< ( 252.809) [10 01 81F3 000000] '\x10\x01\x81\xf3\x00\x00\x00'
|
||||
>> ( 252.824) [10 01 81F3 000000] '\x10\x01\x81\xf3\x00\x00\x00'
|
||||
|
||||
# ?
|
||||
<< ( 253.825) [10 01 81F4 000000] '\x10\x01\x81\xf4\x00\x00\x00'
|
||||
>> ( 253.838) [10 01 81F4 800000] '\x10\x01\x81\xf4\x80\x00\x00'
|
||||
34
docs/devices/m515.txt
Normal file
34
docs/devices/m515.txt
Normal file
@@ -0,0 +1,34 @@
|
||||
1: Couch Mouse M515
|
||||
Codename : M515
|
||||
Kind : mouse
|
||||
Wireless PID : 4007
|
||||
Protocol : HID++ 2.0
|
||||
Polling rate : 8 ms
|
||||
Serial number: BED587E9
|
||||
Firmware: RQM 24.00.B0023
|
||||
Bootloader: DFU 00.02.B0010
|
||||
The power switch is located on the base.
|
||||
Supports 16 HID++ 2.0 features:
|
||||
0: ROOT {0000}
|
||||
1: FEATURE SET {0001}
|
||||
2: DEVICE FW VERSION {0003}
|
||||
3: DEVICE NAME {0005}
|
||||
4: DFUCONTROL {00C0}
|
||||
5: BATTERY STATUS {1000}
|
||||
6: unknown:1A30 {1A30} hidden
|
||||
7: REPROG CONTROLS {1B00}
|
||||
8: WIRELESS DEVICE STATUS {1D4B}
|
||||
9: unknown:1DF3 {1DF3} hidden
|
||||
10: VERTICAL SCROLLING {2100}
|
||||
11: HI RES SCROLLING {2120}
|
||||
12: MOUSE POINTER {2200}
|
||||
13: unknown:1F02 {1F02} hidden
|
||||
14: unknown:1F03 {1F03} hidden
|
||||
15: unknown:1E80 {1E80} hidden
|
||||
Has 5 reprogrammable keys:
|
||||
0: LEFT CLICK => LeftClick mse, reprogrammable
|
||||
1: RIGHT CLICK => RightClick mse, reprogrammable
|
||||
2: MIDDLE BUTTON => MiddleMouseButton mse, reprogrammable
|
||||
3: BACK AS BUTTON 4 => BackEx mse, reprogrammable
|
||||
4: FORWARD AS BUTTON 5 => BrowserForwardEx mse, reprogrammable
|
||||
Battery: 65%, discharging,
|
||||
2
docs/devices/m525.txt
Normal file
2
docs/devices/m525.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
No non-error messages received for GET_REG and GET_REG_LONG. Perhaps because
|
||||
this is a HID++ 2.0 device?
|
||||
42
docs/devices/m705.txt
Normal file
42
docs/devices/m705.txt
Normal file
@@ -0,0 +1,42 @@
|
||||
registers:
|
||||
|
||||
# writing 0x10 in this register will generate an event
|
||||
# 10 02 0Dxx yyzz00
|
||||
# where 0D happens to be the battery register number
|
||||
# xx is the battery charge
|
||||
# yy, zz ?
|
||||
<< ( 0.001) [10 02 8100 000000] '\x10\x02\x81\x00\x00\x00\x00'
|
||||
>> ( 1.132) [10 02 8100 100000] '\x10\x02\x81\x00\x10\x00\x00'
|
||||
|
||||
# smooth scroll - possible values
|
||||
# - 00 (off)
|
||||
# - 02 ?, apparently off as well, default value at power-on
|
||||
# - 0x40 (on)
|
||||
<< ( 2.005) [10 02 8101 000000] '\x10\x02\x81\x01\x00\x00\x00'
|
||||
>> ( 2.052) [10 02 8101 020000] '\x10\x02\x81\x01\x02\x00\x00'
|
||||
|
||||
# battery status: percentage full, ?, ?
|
||||
<< ( 14.835) [10 02 810D 000000] '\x10\x02\x81\r\x00\x00\x00'
|
||||
>> ( 14.847) [10 02 810D 644734] '\x10\x02\x81\rdG4'
|
||||
|
||||
# accepts mask 0xF1
|
||||
# setting 0x10 turns off the movement events (but buttons still work)
|
||||
<< ( 221.495) [10 02 81D0 000000] '\x10\x02\x81\xd0\x00\x00\x00'
|
||||
>> ( 221.509) [10 02 81D0 000000] '\x10\x02\x81\xd0\x00\x00\x00'
|
||||
|
||||
# appears to be read-only?
|
||||
<< ( 223.527) [10 02 81D2 000000] '\x10\x02\x81\xd2\x00\x00\x00'
|
||||
>> ( 223.540) [10 02 81D2 000003] '\x10\x02\x81\xd2\x00\x00\x03'
|
||||
|
||||
# appears to be read-only?
|
||||
<< ( 225.557) [10 02 81D4 000000] '\x10\x02\x81\xd4\x00\x00\x00'
|
||||
>> ( 225.571) [10 02 81D4 000004] '\x10\x02\x81\xd4\x00\x00\x04'
|
||||
|
||||
# read-only, 01-04 firmware info
|
||||
<< ( 259.270) [10 02 81F1 000000] '\x10\x02\x81\xf1\x00\x00\x00'
|
||||
>> ( 259.283) [10 02 8F81 F10300] '\x10\x02\x8f\x81\xf1\x03\x00'
|
||||
|
||||
# writing 01 here will trigger an avalance of events, most likely
|
||||
# raw input from the mouse; disable by writing 00
|
||||
<< ( 261.300) [10 02 81F3 000000] '\x10\x02\x81\xf3\x00\x00\x00'
|
||||
>> ( 261.315) [10 02 81F3 000000] '\x10\x02\x81\xf3\x00\x00\x00'
|
||||
26
docs/devices/mk700.txt
Normal file
26
docs/devices/mk700.txt
Normal file
@@ -0,0 +1,26 @@
|
||||
# Enabled Notifications
|
||||
# 10 - battery status
|
||||
# 02 + 01 - remap FN keys (multimedia + power buttons)
|
||||
>> ( 1.412) [10 02 8100 130000] '\x10\x02\x81\x00\x13\x00\x00'
|
||||
<< ( 0.011) [10 02 8101 000000] '\x10\x02\x81\x01\x00\x00\x00'
|
||||
>> ( 0.276) [10 02 8101 000000] '\x10\x02\x81\x01\x00\x00\x00'
|
||||
|
||||
# Battery status
|
||||
<< ( 6.033) [10 02 8107 000000] '\x10\x02\x81\x07\x00\x00\x00'
|
||||
>> ( 6.344) [10 02 8107 070000] '\x10\x02\x81\x07\x07\x00\x00'
|
||||
|
||||
# FN status
|
||||
<< ( 8.055) [10 02 8109 000000] '\x10\x02\x81\t\x00\x00\x00'
|
||||
>> ( 8.144) [10 02 8109 000000] '\x10\x02\x81\t\x00\x00\x00'
|
||||
|
||||
# ?
|
||||
<< ( 208.316) [10 02 81D0 000000] '\x10\x02\x81\xd0\x00\x00\x00'
|
||||
>> ( 208.353) [10 02 81D0 000000] '\x10\x02\x81\xd0\x00\x00\x00'
|
||||
|
||||
# version info
|
||||
<< ( 237.436) [10 02 81F1 000000] '\x10\x02\x81\xf1\x00\x00\x00'
|
||||
>> ( 237.744) [10 02 8F81 F10300] '\x10\x02\x8f\x81\xf1\x03\x00'
|
||||
|
||||
# ?
|
||||
<< ( 239.459) [10 02 81F3 000000] '\x10\x02\x81\xf3\x00\x00\x00'
|
||||
>> ( 239.766) [10 02 81F3 000000] '\x10\x02\x81\xf3\x00\x00\x00'
|
||||
40
docs/devices/performance-mx.txt
Normal file
40
docs/devices/performance-mx.txt
Normal file
@@ -0,0 +1,40 @@
|
||||
# Notifications (r1_bit0 = battery status?)
|
||||
<< ( 0.113) [10 01 8100 000000] '\x10\x01\x81\x00\x00\x00\x00'
|
||||
>> ( 0.157) [10 01 8100 100000] '\x10\x01\x81\x00\x10\x00\x00'
|
||||
|
||||
# ?
|
||||
<< ( 1.050) [10 01 8101 000000] '\x10\x01\x81\x01\x00\x00\x00'
|
||||
>> ( 1.097) [10 01 8101 020000] '\x10\x01\x81\x01\x02\x00\x00'
|
||||
|
||||
# battery (07 means full)
|
||||
<< ( 7.335) [10 01 8107 000000] '\x10\x01\x81\x07\x00\x00\x00'
|
||||
>> ( 7.382) [10 01 8107 070000] '\x10\x01\x81\x07\x07\x00\x00'
|
||||
|
||||
# Set LEDS - ab cd 00, where a/b/c/d values are 1=off, 2=on, 3=flash
|
||||
# a = lower led
|
||||
# b = red led
|
||||
# c = upper led
|
||||
# d = middle led
|
||||
# below: all leds are off
|
||||
<< ( 86.592) [10 01 8151 000000] '\x10\x01\x81Q\x00\x00\x00'
|
||||
>> ( 86.639) [10 01 8151 111100] '\x10\x01\x81Q\x11\x11\x00'
|
||||
|
||||
# DPI (values in range 0x81..0x8F; logical value: 100..1500)
|
||||
<< ( 108.430) [10 01 8163 000000] '\x10\x01\x81c\x00\x00\x00'
|
||||
>> ( 108.477) [10 01 8163 890000] '\x10\x01\x81c\x89\x00\x00'
|
||||
|
||||
# ?
|
||||
<< ( 240.505) [10 01 81D0 000000] '\x10\x01\x81\xd0\x00\x00\x00'
|
||||
>> ( 240.550) [10 01 81D0 000000] '\x10\x01\x81\xd0\x00\x00\x00'
|
||||
|
||||
# ?
|
||||
<< ( 245.690) [10 01 81D4 000000] '\x10\x01\x81\xd4\x00\x00\x00'
|
||||
>> ( 245.737) [10 01 81D4 000012] '\x10\x01\x81\xd4\x00\x00\x12'
|
||||
|
||||
# Firmware/bootloader version
|
||||
<< ( 281.016) [10 01 81F1 000000] '\x10\x01\x81\xf1\x00\x00\x00'
|
||||
>> ( 282.177) [10 01 8F81 F10300] '\x10\x01\x8f\x81\xf1\x03\x00'
|
||||
|
||||
# ?
|
||||
<< ( 284.106) [10 01 81F3 000000] '\x10\x01\x81\xf3\x00\x00\x00'
|
||||
>> ( 284.153) [10 01 81F3 000000] '\x10\x01\x81\xf3\x00\x00\x00'
|
||||
30
docs/i18n.md
Normal file
30
docs/i18n.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Translating Solaar
|
||||
|
||||
First, make sure you have installed the `gettext` package.
|
||||
|
||||
Here are the steps to add/update a translation (you should run all scripts from
|
||||
the source root):
|
||||
|
||||
1. Get an up-to-date copy of the source files. Preferrably, make a clone on
|
||||
GitHub and clone it locally on your machine; this way you can later make a
|
||||
pull request to the main project.
|
||||
|
||||
2. Run `./tools/po-update.sh <language>`; it will create/update the file
|
||||
`./po/<language>.po`.
|
||||
|
||||
3. Edit `./po/<language>.po` with your favourite editor (just make sure it saves
|
||||
the file with the UTF-8 encoding). For each string in english (msgid), edit
|
||||
the translation (msgstr); if you leave msgstr empty, the string will remain
|
||||
untranslated.
|
||||
|
||||
Alternatively, you can use the excellent `poedit`.
|
||||
|
||||
4. Run `./tools/po-compile.sh`. It will bring up-to-date all the compiled
|
||||
language files, necessary at runtime.
|
||||
|
||||
5. Start Solaar (`./bin/solaar`). By default it will pick up the system languge
|
||||
from your environment; to start it in another language, run
|
||||
`LANGUAGE=<language> ./bin/solaar`.
|
||||
|
||||
You can edit the translation iteratively, just repeat from step 3.
|
||||
If the upstream changes, do a `git pull` and then repeat from step 2.
|
||||
34
docs/icons_names.txt
Normal file
34
docs/icons_names.txt
Normal file
@@ -0,0 +1,34 @@
|
||||
# battery icon names across various icon themes
|
||||
|
||||
B = 'battery'
|
||||
CG = 'charging'
|
||||
GB = 'gpm-battery'
|
||||
|
||||
theme (unknown) 0 0-CG 20 20-CG 40 40-CG 60 60-CG 80 80-CG 100 100-CG 100-full
|
||||
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
gnome B-missing B-empty - B-caution B-caution-CG B-low B-low-CG - - B-good B-good-CG B-full B-full-CG B-full-charged
|
||||
|
||||
Humanity GB-missing GB-000 GB-000-CG GB-020 GB-020-CG GB-040 GB-040-CG GB-060 GB-060-CG GB-080 GB-080-CG GB-100 GB-100-CG GB-charged
|
||||
(gnome) - B_empty - B-caution - B-low - B_two_thirds - B_third_fourth - B_full B_plugged B_charged
|
||||
|
||||
elementary B-missing B-000 B-000-CG B-020 B-020-CG B-040 B-040-CG B-060 B-060-CG B-080 B-080-CG B-100 B-100-CG B-charged
|
||||
(gnome) - B-empty - B-caution B-caution-CG B-low B-low-CG - - B-good B-good-CG B-full B-full-CG B-full-charged
|
||||
- B_empty - - - - - B_two_thirds - B_third_fourth - B_full B_plugged B_charged
|
||||
|
||||
faenza - GB-000 GB-000-CG GB-020 GB-020-CG GB-040 GB-040-CG GB-060 GB-060-CG GB-080 GB-080-CG GB-100 GB-100-CG GB-charged
|
||||
(gnome) - B_empty - B_caution - B_low - B_two_thirds - B_third_fourth - B_full B_plugged B_charged
|
||||
|
||||
ubuntu-mono GB-missing GB-000 GB-000-CG GB-020 GB-020-CG GB-040 GB-040-CG GB-060 GB-060-CG GB-080 GB-080-CG GB-100 GB-100-CG GB-charged
|
||||
(Humanity) B-000 B-000-CG B-020 B-020-CG B-040 B-040-CG B-060 B-060-CG B-080 B-080-CG B-100 B-100-CG B-charged
|
||||
B_empty - B-caution - B-low - - - - - B_full - B_charged
|
||||
|
||||
oxygen B-missing B-low B-CG-low B-caution B-CG-caution B-040 B-CG-040 B-060 B-CG-060 B-080 B-CG-080 B-100 B-CG -
|
||||
|
||||
moblin - - - B-low - B-caution - - - B-good - B-full B-charging -
|
||||
|
||||
nuvola B-missing B-low - - - - - - - - - - B-charging -
|
||||
|
||||
|
||||
|
||||
# weather icons (for lux)
|
||||
53
docs/installation.md
Normal file
53
docs/installation.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Manual installation
|
||||
|
||||
### Requirements
|
||||
|
||||
You should have a reasonably new kernel (3.2+), with the `logitech-djreceiver`
|
||||
driver enabled and loaded; also, the `udev` package must be installed and the
|
||||
daemon running. If you have a modern Linux distribution (2011+), you're most
|
||||
likely good to go.
|
||||
|
||||
The command-line application (`bin/solaar-cli`) requires Python 2.7.3 or 3.2+
|
||||
(either version should work), and the `python-pyudev`/`python3-pyudev` package.
|
||||
|
||||
The GUI application (`bin/solaar`) also requires Gtk3, and its GObject
|
||||
Introspection bindings. The Debian/Ubuntu package names are
|
||||
`python-gi`/`python3-gi` and `gir1.2-gtk-3.0`; if you're using another
|
||||
distribution the required packages are most likely named something similar.
|
||||
If the desktop notifications bindings are also installed (`gir1.2-notify-0.7`),
|
||||
you will also get desktop notifications when devices come online/go offline.
|
||||
|
||||
For gnome-shell/Unity support, you also need to have `gir1.2-appindicator3-0.1`
|
||||
installed.
|
||||
|
||||
|
||||
### Installation
|
||||
|
||||
Normally USB devices are not accessible for r/w by regular users, so you will
|
||||
need to do a one-time udev rule installation to allow access to the Logitech
|
||||
Unifying Receiver.
|
||||
|
||||
You can run the `rules.d/install.sh` script from Solaar to do this installation
|
||||
automatically (make sure to run it as your regular desktop user, it will switch
|
||||
to root when necessary), or you can do all the required steps by hand, as the
|
||||
root user:
|
||||
|
||||
1. Copy `rules.d/99-logitech-unifying-receiver.rules` from Solaar to
|
||||
`/etc/udev/rules.d/`. The `udev` daemon will automatically pick up this file
|
||||
using inotify.
|
||||
|
||||
By default, the rule allows all members of the `plugdev` group to have
|
||||
read/write access to the Unifying Receiver device. (standard Debian/Ubuntu
|
||||
group for pluggable devices). It may need changes, specific to your
|
||||
particular system's configuration. If in doubt, replacing `GROUP="plugdev"`
|
||||
with `GROUP="<your username>"` should just work.
|
||||
|
||||
2. Physically remove the Unifying Receiver and re-insert it.
|
||||
|
||||
This is necessary because if the receiver is already plugged-in, it already
|
||||
has a `/dev/hidrawX` device node, but with the old (`root:root`) permissions.
|
||||
Plugging it again will re-create the device node with the right permissions.
|
||||
|
||||
3. Make sure your desktop users are part of the `plugdev` group, by running
|
||||
`gpasswd <desktop username> plugdev`. If these users were not assigned to the
|
||||
group before, they must re-login for the changes to take effect.
|
||||
BIN
docs/logitech/2200_mousepointer.pdf
Normal file
BIN
docs/logitech/2200_mousepointer.pdf
Normal file
Binary file not shown.
BIN
docs/logitech/4301_k750_solarkeyboard_lightandbattery.pdf
Normal file
BIN
docs/logitech/4301_k750_solarkeyboard_lightandbattery.pdf
Normal file
Binary file not shown.
BIN
docs/logitech/6100_touchpadraw.pdf
Normal file
BIN
docs/logitech/6100_touchpadraw.pdf
Normal file
Binary file not shown.
BIN
docs/logitech/6110_touchmouseraw.pdf
Normal file
BIN
docs/logitech/6110_touchmouseraw.pdf
Normal file
Binary file not shown.
26
docs/logitech/battery-level.txt
Normal file
26
docs/logitech/battery-level.txt
Normal file
@@ -0,0 +1,26 @@
|
||||
The battery/charging level and status is reported only if the related
|
||||
reporting flag in register 0x00 is enabled by the host. The
|
||||
"Battery/Charging Level" byte indicates the battery level if the
|
||||
"Charging State" indicates 0x00 ("Not Charging"). If "Charging State"
|
||||
indicates 0x21 to 0x23 ("Charging"), the "Battery/Charging Level" byte
|
||||
indicates the level of charging.
|
||||
|
||||
10 ix 07 r0 r1 r2 00
|
||||
r0 -> Battery/Charging Level
|
||||
0x00 = Reserved/Unknown
|
||||
0x01 = Critical
|
||||
0x02 = Critical (legacy value, don't use)
|
||||
0x03 = Low
|
||||
0x04 = Low (legacy value, don't use)
|
||||
0x05 = Good
|
||||
0x06 = Good (legacy value, don't use)
|
||||
0x07 = Full
|
||||
0x08..0xFF = Reserved
|
||||
r1 -> Charging state
|
||||
0x00 = Not charging
|
||||
0x01..0x1F = Reserved (not charging)
|
||||
0x20 = Unknown charging state
|
||||
0x21 = Charging
|
||||
0x22 = Charging complete
|
||||
0x23 = Charging error
|
||||
0x24..0xFF = Reserved
|
||||
278
docs/logitech/hid10.txt
Normal file
278
docs/logitech/hid10.txt
Normal file
@@ -0,0 +1,278 @@
|
||||
*Read short register command*
|
||||
|
||||
10 ix 81 02 00 00 00
|
||||
|
||||
ix
|
||||
|
||||
Index 0x0n: Device #n
|
||||
|
||||
0xFF: Transceiver
|
||||
|
||||
*Response to Read command (success)*
|
||||
|
||||
10 ix 81 02 00 r1 r2
|
||||
|
||||
ix
|
||||
|
||||
Index (same as command)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
r1
|
||||
|
||||
Number of Connected Devices
|
||||
|
||||
bit 0..7: Number of connected devices (receivers only)
|
||||
|
||||
r2
|
||||
|
||||
Number of Remaining Pairing Slots
|
||||
|
||||
bit 0..7: Number of remaining pairing slots
|
||||
|
||||
|
||||
*Read long register command*
|
||||
|
||||
10 ix 83 B5 nn 00 00
|
||||
|
||||
ix
|
||||
|
||||
Index 0xFF: Transceiver
|
||||
|
||||
nn
|
||||
|
||||
0x20 Device 1
|
||||
|
||||
0x21 Device 2
|
||||
|
||||
0x22 Device 3
|
||||
|
||||
0x23 Device 4
|
||||
|
||||
0x24 Device 5
|
||||
|
||||
0x25 Device 6
|
||||
|
||||
0x26..0x2F Reserved for future extensions
|
||||
|
||||
*Response to Read command (success)*
|
||||
|
||||
11 ix 83 B5 nn r1 r2 r3 r4 r5 r6 r7 r8 r9 ra rb rc rd 00 00
|
||||
|
||||
ix
|
||||
|
||||
Index (same as command)
|
||||
|
||||
nn
|
||||
|
||||
(same format as above)
|
||||
|
||||
r1
|
||||
|
||||
Destination ID
|
||||
|
||||
r2
|
||||
|
||||
Reserved
|
||||
|
||||
r3
|
||||
|
||||
Wireless PID MSB
|
||||
|
||||
r4
|
||||
|
||||
Wireless PID LSB
|
||||
|
||||
r5
|
||||
|
||||
Reserved
|
||||
|
||||
r6
|
||||
|
||||
Reserved
|
||||
|
||||
r7
|
||||
|
||||
Device type
|
||||
|
||||
0 undefined
|
||||
|
||||
1 keyboard
|
||||
|
||||
2 mouse
|
||||
|
||||
3 numpad
|
||||
|
||||
4 presenter
|
||||
|
||||
5 reserved
|
||||
|
||||
6 reserved
|
||||
|
||||
7 remote control
|
||||
|
||||
8 trackball
|
||||
|
||||
9 touchpad
|
||||
|
||||
a tablet
|
||||
|
||||
b gamepad
|
||||
|
||||
c joystick
|
||||
|
||||
r8
|
||||
|
||||
Reserved
|
||||
|
||||
r9
|
||||
|
||||
Reserved
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Alternatively, if enabled, you can also receive a notification when a new
|
||||
device is paired:
|
||||
|
||||
This message is sent by a receiver to the host SW to report a freshly
|
||||
connected device. Enable the HID++ connection reporting by setting the
|
||||
corresponding bit in register 0x00 via HID++ Set Register command.
|
||||
|
||||
*Notification*
|
||||
|
||||
10 ix 41 r0 r1 r2 r3
|
||||
|
||||
ix
|
||||
|
||||
Index
|
||||
|
||||
r0
|
||||
|
||||
bits [0..2] Protocol type
|
||||
|
||||
0x03 = eQUAD
|
||||
|
||||
0x04 = eQuad step 4 DJ
|
||||
|
||||
bits [3..7] Reserved
|
||||
|
||||
r1
|
||||
|
||||
Device Info
|
||||
|
||||
bit0..3 = Device Type
|
||||
|
||||
0x00 = Unknown
|
||||
|
||||
0x01 = Keyboard
|
||||
|
||||
0x02 = Mouse
|
||||
|
||||
0x03 = Numpad
|
||||
|
||||
0x04 = Presenter
|
||||
|
||||
|
||||
r2
|
||||
|
||||
Wireless PID LSB
|
||||
|
||||
r3
|
||||
|
||||
Wireless PID MSB
|
||||
|
||||
To enable the notifications:
|
||||
Enable HID++ Notifications:
|
||||
|
||||
This register defines a number of flags that allow the SW to turn on or off
|
||||
individual spontaneous HID++ reports. Not setting a flag means default
|
||||
reporting. See the table below for more details on each flag.
|
||||
|
||||
For all bits: *0 = disabled* (default value at power-up), 1 = enabled.
|
||||
|
||||
|
||||
|
||||
*Read short register command*
|
||||
|
||||
10 ix 81 00 00 00 00
|
||||
|
||||
ix
|
||||
|
||||
Index 0x0n: Device #n
|
||||
|
||||
0xFF: Transceiver
|
||||
|
||||
*Response to Read command (success)*
|
||||
|
||||
10 ix 81 00 r0 r1 r2
|
||||
|
||||
ix
|
||||
|
||||
Index (same as command)
|
||||
|
||||
r0
|
||||
|
||||
HID++ Reporting Flags (Devices)
|
||||
|
||||
bit 0..3. reserved
|
||||
|
||||
bit 4: Battery Status
|
||||
|
||||
bit 5..7 reserved
|
||||
|
||||
r1
|
||||
|
||||
HID++ Reporting Flags (Receiver)
|
||||
|
||||
bit 0: Wireless notifications
|
||||
|
||||
bit 1..7 reserved
|
||||
|
||||
r2
|
||||
|
||||
|
||||
|
||||
|
||||
*Write short register command*
|
||||
|
||||
10 ix 80 00 p0 p1 p2
|
||||
|
||||
ix
|
||||
|
||||
Index 0x0n: Device #n
|
||||
|
||||
0xFF: Transceiver
|
||||
|
||||
p0
|
||||
|
||||
HID++ Reporting Flags (Devices)
|
||||
|
||||
(same format as above)
|
||||
|
||||
p1
|
||||
|
||||
HID++ Reporting Flags (Receiver)
|
||||
|
||||
(same format as above)
|
||||
|
||||
p2
|
||||
|
||||
|
||||
*Response to Write command (success)*
|
||||
|
||||
10 ix 80 00 zz zz zz
|
||||
|
||||
ix
|
||||
|
||||
Index (same as command)
|
||||
|
||||
zz
|
||||
|
||||
(don't care, recommended to return 0)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
BIN
docs/logitech/performance_mx_led_registers.pdf
Normal file
BIN
docs/logitech/performance_mx_led_registers.pdf
Normal file
Binary file not shown.
12
docs/usb-ids.txt
Normal file
12
docs/usb-ids.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
Unifying receiver:
|
||||
046d:c52b interface: 2 driver: logitech-djreceiver
|
||||
046d:c532 interface: 2 driver: logitech-djreceiver
|
||||
|
||||
|
||||
Nano receiver, Advanced/Unifying ready:
|
||||
046d:c52f interface: 1 driver: hid-generic
|
||||
|
||||
|
||||
Nano receiver:
|
||||
046d:c51a interface: 1 driver: hid-generic
|
||||
046d:c526 interface: 1 driver: hid-generic
|
||||
364
docs/usb.ids.txt
Normal file
364
docs/usb.ids.txt
Normal file
@@ -0,0 +1,364 @@
|
||||
#
|
||||
# List of USB ID's
|
||||
#
|
||||
# Maintained by Stephen J. Gowdy <linux.usb.ids@gmail.com>
|
||||
# If you have any new entries, please submit them via
|
||||
# http://www.linux-usb.org/usb-ids.html
|
||||
# or send entries as patches (diff -u old new) in the
|
||||
# body of your email (a bot will attempt to deal with it).
|
||||
# The latest version can be obtained from
|
||||
# http://www.linux-usb.org/usb.ids
|
||||
#
|
||||
# Version: 2013.05.24
|
||||
# Date: 2013-05-24 20:34:03
|
||||
#
|
||||
|
||||
# Vendors, devices and interfaces. Please keep sorted.
|
||||
|
||||
# Syntax:
|
||||
# vendor vendor_name
|
||||
# device device_name <-- single tab
|
||||
# interface interface_name <-- two tabs
|
||||
|
||||
046d Logitech, Inc.
|
||||
0082 Acer Aspire 5672 Webcam
|
||||
0200 WingMan Extreme Joystick
|
||||
0203 M2452 Keyboard
|
||||
0301 M4848 Mouse
|
||||
0401 HP PageScan
|
||||
0402 NEC PageScan
|
||||
040f Logitech/Storm PageScan
|
||||
0430 Mic (Cordless)
|
||||
0801 QuickCam Home
|
||||
0802 Webcam C200
|
||||
0804 Webcam C250
|
||||
0805 Webcam C300
|
||||
0807 Webcam B500
|
||||
0808 Webcam C600
|
||||
0809 Webcam Pro 9000
|
||||
080a Portable Webcam C905
|
||||
080f Webcam C120
|
||||
0810 QuickCam Pro
|
||||
0819 Webcam C210
|
||||
081b Webcam C310
|
||||
081d HD Webcam C510
|
||||
0820 QuickCam VC
|
||||
0821 HD Webcam C910
|
||||
0825 Webcam C270
|
||||
0828 HD Webcam B990
|
||||
082d HD Pro Webcam C920
|
||||
0830 QuickClip
|
||||
0840 QuickCam Express
|
||||
0850 QuickCam Web
|
||||
0870 QuickCam Express
|
||||
0890 QuickCam Traveler
|
||||
0892 OrbiCam
|
||||
0894 CrystalCam
|
||||
0895 QuickCam for Dell Notebooks
|
||||
0896 OrbiCam
|
||||
0897 QuickCam for Dell Notebooks
|
||||
0899 QuickCam for Dell Notebooks
|
||||
089d QuickCam E2500 series
|
||||
08a0 QuickCam IM
|
||||
08a1 QuickCam IM with sound
|
||||
08a2 Labtec Webcam Pro
|
||||
08a3 QuickCam QuickCam Chat
|
||||
08a6 QuickCam IM
|
||||
08a7 QuickCam Image
|
||||
08a9 Notebook Deluxe
|
||||
08aa Labtec Notebooks
|
||||
08ac QuickCam Cool
|
||||
08ad QuickCam Communicate STX
|
||||
08ae QuickCam for Notebooks
|
||||
08af QuickCam Easy/Cool
|
||||
08b0 QuickCam 3000 Pro [pwc]
|
||||
08b1 QuickCam Notebook Pro
|
||||
08b2 QuickCam Pro 4000
|
||||
08b3 QuickCam Zoom
|
||||
08b4 QuickCam Zoom
|
||||
08b5 QuickCam Sphere
|
||||
08b9 QuickCam IM
|
||||
08bd Microphone (Pro 4000)
|
||||
08c0 QuickCam Pro 3000
|
||||
08c1 QuickCam Fusion
|
||||
08c2 QuickCam PTZ
|
||||
08c3 Camera (Notebooks Pro)
|
||||
08c5 QuickCam Pro 5000
|
||||
08c6 QuickCam for DELL Notebooks
|
||||
08c7 QuickCam OEM Cisco VT Camera II
|
||||
08c9 QuickCam Ultra Vision
|
||||
08ca Mic (Fusion)
|
||||
08cb Mic (Notebooks Pro)
|
||||
08cc Mic (PTZ)
|
||||
08ce QuickCam Pro 5000
|
||||
08cf QuickCam UpdateMe
|
||||
08d0 QuickCam Express
|
||||
08d7 QuickCam Communicate STX
|
||||
08d8 QuickCam for Notebook Deluxe
|
||||
08d9 QuickCam IM/Connect
|
||||
08da QuickCam Messanger
|
||||
08dd QuickCam for Notebooks
|
||||
08e0 QuickCam Express
|
||||
08e1 Labtec Webcam
|
||||
08f0 QuickCam Messenger
|
||||
08f1 QuickCam Express
|
||||
08f2 Microphone (Messenger)
|
||||
08f3 QuickCam Express
|
||||
08f4 Labtec Webcam
|
||||
08f5 QuickCam Messenger Communicate
|
||||
08f6 QuickCam Messenger Plus
|
||||
0900 ClickSmart 310
|
||||
0901 ClickSmart 510
|
||||
0903 ClickSmart 820
|
||||
0905 ClickSmart 820
|
||||
0910 QuickCam Cordless
|
||||
0920 QuickCam Express
|
||||
0921 Labtec Webcam
|
||||
0922 QuickCam Live
|
||||
0928 QuickCam Express
|
||||
0929 Labtec Webcam Pro
|
||||
092a QuickCam for Notebooks
|
||||
092b Labtec Webcam Plus
|
||||
092c QuickCam Chat
|
||||
092d QuickCam Express / Go
|
||||
092e QuickCam Chat
|
||||
092f QuickCam Express Plus
|
||||
0950 Pocket Camera
|
||||
0960 ClickSmart 420
|
||||
0970 Pocket750
|
||||
0990 QuickCam Pro 9000
|
||||
0991 QuickCam Pro for Notebooks
|
||||
0992 QuickCam Communicate Deluxe
|
||||
0994 QuickCam Orbit/Sphere AF
|
||||
09a1 QuickCam Communicate MP/S5500
|
||||
09a2 QuickCam Communicate Deluxe/S7500
|
||||
09a4 QuickCam E 3500
|
||||
09a5 Quickcam 3000 For Business
|
||||
09a6 QuickCam Vision Pro
|
||||
09b0 Acer OrbiCam
|
||||
09b2 Fujitsu Webcam
|
||||
09c0 QuickCam for Dell Notebooks Mic
|
||||
09c1 QuickCam Deluxe for Notebooks
|
||||
0a01 USB Headset
|
||||
0a02 Premium Stereo USB Headset 350
|
||||
0a03 Logitech USB Microphone
|
||||
0a04 V20 portable speakers (USB powered)
|
||||
0a07 Z-10 Speakers
|
||||
0a0b ClearChat Pro USB
|
||||
0a0c Clear Chat Comfort USB Headset
|
||||
0a13 Z-5 Speakers
|
||||
0a17 G330 Headset
|
||||
0a1f G930
|
||||
0b02 C-UV35 [Bluetooth Mini-Receiver] (HID proxy mode)
|
||||
8801 Video Camera
|
||||
b305 BT Mini-Receiver
|
||||
bfe4 Premium Optical Wheel Mouse
|
||||
c000 N43 [Pilot Mouse]
|
||||
c001 N48/M-BB48 [FirstMouse Plus]
|
||||
c002 M-BA47 [MouseMan Plus]
|
||||
c003 MouseMan
|
||||
c004 WingMan Gaming Mouse
|
||||
c005 WingMan Gaming Wheel Mouse
|
||||
c00b MouseMan Wheel
|
||||
c00c Optical Wheel Mouse
|
||||
c00d MouseMan Wheel+
|
||||
c00e M-BJ58/M-BJ69 Optical Wheel Mouse
|
||||
c00f MouseMan Traveler/Mobile
|
||||
c011 Optical MouseMan
|
||||
c012 Mouseman Dual Optical
|
||||
c014 Corded Workstation Mouse
|
||||
c015 Corded Workstation Mouse
|
||||
c016 Optical Wheel Mouse
|
||||
c018 Optical Wheel Mouse
|
||||
c019 Optical Tilt Wheel Mouse
|
||||
c01a M-BQ85 Optical Wheel Mouse
|
||||
c01b MX310 Optical Mouse
|
||||
c01c Optical Mouse
|
||||
c01d MX510 Optical Mouse
|
||||
c01e MX518 Optical Mouse
|
||||
c024 MX300 Optical Mouse
|
||||
c025 MX500 Optical Mouse
|
||||
c030 iFeel Mouse
|
||||
c031 iFeel Mouse+
|
||||
c032 MouseMan iFeel
|
||||
c033 iFeel MouseMan+
|
||||
c034 MouseMan Optical
|
||||
c035 Mouse
|
||||
c036 Mouse
|
||||
c037 Mouse
|
||||
c038 Mouse
|
||||
c03d M-BT96a Pilot Optical Mouse
|
||||
c03e Premium Optical Wheel Mouse (M-BT58)
|
||||
c03f M-BT85 [UltraX Optical Mouse]
|
||||
c040 Corded Tilt-Wheel Mouse
|
||||
c041 G5 Laser Mouse
|
||||
c042 G3 Laser Mouse
|
||||
c043 MX320/MX400 Laser Mouse
|
||||
c044 LX3 Optical Mouse
|
||||
c045 Optical Mouse
|
||||
c046 RX1000 Laser Mouse
|
||||
c047 Laser Mouse M-UAL120
|
||||
c048 G9 Laser Mouse
|
||||
c049 G5 Laser Mouse
|
||||
c050 RX 250 Optical Mouse
|
||||
c051 G3 (MX518) Optical Mouse
|
||||
c053 Laser Mouse
|
||||
c054 Bluetooth mini-receiver
|
||||
c058 M115 Mouse
|
||||
c05a M90/M100 Optical Mouse
|
||||
c05b M-U0004 810-001317 [B110 Optical USB Mouse]
|
||||
c05d Optical Mouse
|
||||
c05f M115 Optical Mouse
|
||||
c061 RX1500 Laser Mouse
|
||||
c062 M-UAS144 [LS1 Laser Mouse]
|
||||
c063 DELL Laser Mouse
|
||||
c068 G500 Laser Mouse
|
||||
c069 M500 Laser Mouse
|
||||
c06a USB Optical Mouse
|
||||
c06b G700 Wireless Gaming Mouse
|
||||
c06c Optical Mouse
|
||||
c101 UltraX Media Remote
|
||||
c110 Harmony 785/885 Remote
|
||||
c111 Harmony 525 Remote
|
||||
c112 Harmony 890 Remote
|
||||
c11f Harmony 900/1100 Remote
|
||||
c121 Harmony One Remote
|
||||
c122 Harmony 700 Remote
|
||||
c124 Harmony 300 Remote
|
||||
c125 Harmony 200 Remote
|
||||
c201 WingMan Extreme Joystick with Throttle
|
||||
c202 WingMan Formula
|
||||
c207 WingMan Extreme Digital 3D
|
||||
c208 WingMan Gamepad Extreme
|
||||
c209 WingMan Gamepad
|
||||
c20a WingMan RumblePad
|
||||
c20b WingMan Action Pad
|
||||
c20c WingMan Precision
|
||||
c20d WingMan Attack 2
|
||||
c20e WingMan Formula GP
|
||||
c211 iTouch Cordless Reciever
|
||||
c212 WingMan Extreme Digital 3D
|
||||
c213 J-UH16 (Freedom 2.4 Cordless Joystick)
|
||||
c214 ATK3 (Attack III Joystick)
|
||||
c215 Extreme 3D Pro
|
||||
c216 Dual Action Gamepad
|
||||
c218 Logitech RumblePad 2 USB
|
||||
c219 Cordless RumblePad 2
|
||||
c21a Precision Gamepad
|
||||
c21c G13 Advanced Gameboard
|
||||
c21d F310 Gamepad [XInput Mode]
|
||||
c21e F510 Gamepad [XInput Mode]
|
||||
c21f F710 Wireless Gamepad [XInput Mode]
|
||||
c221 G11/G15 Keyboard / Keyboard
|
||||
c222 G15 Keyboard / LCD
|
||||
c223 G11/G15 Keyboard / USB Hub
|
||||
c225 G11/G15 Keyboard / G keys
|
||||
c226 G15 Refresh Keyboard
|
||||
c227 G15 Refresh Keyboard
|
||||
c22a Gaming Keyboard G110
|
||||
c22b Gaming Keyboard G110 G-keys
|
||||
c22d G510 Gaming Keyboard
|
||||
c22e G510 Gaming Keyboard onboard audio
|
||||
c245 G400 Optical Mouse
|
||||
c246 Gaming Mouse G300
|
||||
c281 WingMan Force
|
||||
c283 WingMan Force 3D
|
||||
c285 WingMan Strike Force 3D
|
||||
c286 Force 3D Pro
|
||||
c287 Flight System G940
|
||||
c291 WingMan Formula Force
|
||||
c293 WingMan Formula Force GP
|
||||
c294 Driving Force
|
||||
c295 Momo Force Steering Wheel
|
||||
c298 Driving Force Pro
|
||||
c299 G25 Racing Wheel
|
||||
c29b G27 Racing Wheel
|
||||
c29c Speed Force Wireless Wheel for Wii
|
||||
c2a0 Wingman Force Feedback Mouse
|
||||
c2a1 WingMan Force Feedback Mouse
|
||||
c301 iTouch Keyboard
|
||||
c302 iTouch Pro Keyboard
|
||||
c303 iTouch Keyboard
|
||||
c305 Internet Keyboard
|
||||
c307 Internet Keyboard
|
||||
c308 Internet Navigator Keyboard
|
||||
c309 Internet Keyboard
|
||||
c30a iTouch Composite
|
||||
c30b NetPlay Keyboard
|
||||
c30c Internet Keys (X)
|
||||
c30d Internet Keys
|
||||
c30e UltraX Keyboard (Y-BL49)
|
||||
c30f Logicool HID-Compliant Keyboard (106 key)
|
||||
c311 Y-UF49 [Internet Pro Keyboard]
|
||||
c312 DeLuxe 250 Keyboard
|
||||
c313 Internet 350 Keyboard
|
||||
c315 Classic Keyboard 200
|
||||
c316 HID-Compliant Keyboard
|
||||
c317 Wave Corded Keyboard
|
||||
c318 Illuminated Keyboard
|
||||
c31a Comfort Wave 450
|
||||
c31b Compact Keyboard K300
|
||||
c31c Keyboard K120 for Business
|
||||
c31d Media Keyboard K200
|
||||
c401 TrackMan Marble Wheel
|
||||
c402 Marble Mouse (2-button)
|
||||
c403 Turbo TrackMan Marble FX
|
||||
c404 TrackMan Wheel
|
||||
c408 Marble Mouse (4-button)
|
||||
c501 Cordless Mouse Receiver
|
||||
c502 Cordless Mouse & iTouch Keys
|
||||
c503 Cordless Mouse+Keyboard Receiver
|
||||
c504 Cordless Mouse+Keyboard Receiver
|
||||
c505 Cordless Mouse+Keyboard Receiver
|
||||
c506 MX700 Cordless Mouse Receiver
|
||||
c508 Cordless Trackball
|
||||
c509 Cordless Keyboard & Mouse
|
||||
c50a Cordless Mouse
|
||||
c50b Cordless Desktop Optical
|
||||
c50c Cordless Desktop S510
|
||||
c50d Cordless Mouse
|
||||
c50e Cordless Mouse Receiver
|
||||
c510 Cordless Mouse
|
||||
c512 LX-700 Cordless Desktop Receiver
|
||||
c513 MX3000 Cordless Desktop Receiver
|
||||
c514 Cordless Mouse
|
||||
c515 Cordless 2.4 GHz Presenter Presentation remote control
|
||||
c517 LX710 Cordless Desktop Laser
|
||||
c518 MX610 Laser Cordless Mouse
|
||||
c51a MX Revolution/G7 Cordless Mouse
|
||||
c51b V220 Cordless Optical Mouse for Notebooks
|
||||
c521 Cordless Mouse Receiver
|
||||
c525 MX Revolution Cordless Mouse
|
||||
c526 Nano Receiver
|
||||
c529 Logitech Keyboard + Mice
|
||||
c52b Unifying Receiver
|
||||
c52f Unifying Receiver
|
||||
c532 Unifying Receiver
|
||||
c623 3Dconnexion Space Traveller 3D Mouse
|
||||
c625 3Dconnexion Space Pilot 3D Mouse
|
||||
c626 3Dconnexion Space Navigator 3D Mouse
|
||||
c627 3Dconnexion Space Explorer 3D Mouse
|
||||
c702 Cordless Presenter
|
||||
c703 Elite Keyboard Y-RP20 + Mouse MX900 (Bluetooth)
|
||||
c704 diNovo Wireless Desktop
|
||||
c705 MX900 Bluetooth Wireless Hub (C-UJ16A)
|
||||
c707 Bluetooth wireless hub
|
||||
c708 Bluetooth wireless hub
|
||||
c709 BT Mini-Receiver (HCI mode)
|
||||
c70a MX5000 Cordless Desktop
|
||||
c70b BT Mini-Receiver (HID proxy mode)
|
||||
c70c BT Mini-Receiver (HID proxy mode)
|
||||
c70d Bluetooth wireless hub
|
||||
c70e MX1000 Bluetooth Laser Mouse
|
||||
c70f Bluetooth wireless hub
|
||||
c712 Bluetooth wireless hub
|
||||
c714 diNovo Edge Keyboard
|
||||
c715 Bluetooth wireless hub
|
||||
c71a Bluetooth wireless hub
|
||||
c71d Bluetooth wireless hub
|
||||
c71f diNovo Mini Wireless Keyboard
|
||||
c720 Bluetooth wireless hub
|
||||
ca03 MOMO Racing
|
||||
ca04 Formula Vibration Feedback Wheel
|
||||
cab1 Cordless Keyboard for Wii HID Receiver
|
||||
d001 QuickCam Pro
|
||||
12
jekyll/_config.yml
Normal file
12
jekyll/_config.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
title: Solaar
|
||||
tagline: Linux devices manager for the Logitech Unifying Receiver.
|
||||
owner: pwr
|
||||
owner_url: https://github.com/pwr
|
||||
repository: https://github.com/pwr/Solaar
|
||||
version: 0.9.1
|
||||
tar_download: https://github.com/pwr/Solaar/archive/0.9.1.tar.gz
|
||||
ga_id: UA-36908718-1
|
||||
|
||||
pygments: true
|
||||
safe: true
|
||||
lsi: false
|
||||
14
jekyll/_includes/solaar.svg
Normal file
14
jekyll/_includes/solaar.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" style="width: 48px; height:48px; margin-bottom: -10px;">
|
||||
<defs>
|
||||
<linearGradient id="gradient_blue">
|
||||
<stop style="stop-color:#009099;stop-opacity:1" offset="0" />
|
||||
<stop style="stop-color:#00a899;stop-opacity:0.9" offset="1" />
|
||||
</linearGradient>
|
||||
<linearGradient x1="5" y1="50" x2="95" y2="50" id="gradient_rect" xlink:href="#gradient_blue" gradientUnits="userSpaceOnUse" />
|
||||
<linearGradient x1="37" y1="50" x2="63" y2="50" id="gradient_dot" xlink:href="#gradient_blue" gradientUnits="userSpaceOnUse" />
|
||||
</defs>
|
||||
<g transform="scale(0.48)">
|
||||
<path d="M 21.5,5.5 C 12.636,5.5 5.5,12.636 5.5,21.5 L 5.5,78.5 C 5.5,87.364 12.636,94.5 21.5,94.5 L 78.5,94.5 C 87.364,94.5 94.5,87.364 94.5,78.5 L 94.5,21.5 C 94.5,12.636 87.364,5.5 78.5,5.5 L 21.5,5.5 z M 37.6875,16.6875 46.71875,32.3125 C 47.784179,32.115965 48.877705,32 50,32 51.122295,32 52.215821,32.115965 53.28125,32.3125 L 62.3125,16.6875 72.6875,22.6875 63.65625,38.3125 C 65.078123,39.972287 66.191785,41.898777 66.9375,44 L 85,44 85,56 66.9375,56 C 66.191785,58.101223 65.078123,60.027713 63.65625,61.6875 L 72.6875,77.3125 62.3125,83.3125 53.28125,67.6875 C 52.215821,67.884035 51.122295,68 50,68 48.877705,68 47.784179,67.884035 46.71875,67.6875 L 37.6875,83.3125 27.3125,77.3125 36.34375,61.6875 C 34.921877,60.027713 33.808215,58.101223 33.0625,56 L 15,56 15,44 33.0625,44 C 33.808215,41.898777 34.921877,39.972287 36.34375,38.3125 L 27.3125,22.6875 37.6875,16.6875 z" style="fill:url(#gradient_rect);fill-opacity:1;fill-rule:nonzero;stroke:#16161d;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1" />
|
||||
<path d="M 62,50 A 12,12 0 1 1 38,50 12,12 0 1 1 62,50 z" style="fill:url(#gradient_dot);fill-opacity:1;fill-rule:nonzero;stroke:#16161d;stroke-opacity:1" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
56
jekyll/_layouts/default.html
Normal file
56
jekyll/_layouts/default.html
Normal file
@@ -0,0 +1,56 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset='utf-8' />
|
||||
<meta name="description" content="{{ site.tagline }}" />
|
||||
|
||||
<link rel="stylesheet" type="text/css" media="screen" href="style/stylesheet.css">
|
||||
<link rel="icon" type="image/png" href="images/favicon.png" />
|
||||
|
||||
<title>{{ page.title }}</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="header_wrap" class="outer">
|
||||
<header class="inner">
|
||||
<a id="forkme_banner" href="{{ site.repository }}">View on GitHub</a>
|
||||
|
||||
<h1 id="project_title">{% include solaar.svg %} {{ site.title }}</h1>
|
||||
<h2 id="project_tagline">{{ site.tagline }}</h2>
|
||||
|
||||
<section id="downloads">
|
||||
<a class="tar_download_link" href="{{ site.tar_download }}">Solaar {{ site.version }}</a>
|
||||
<p style="color: #fff">
|
||||
Latest version:<br/>
|
||||
<span style="float: right; font-weight: bolder;">{{ site.version }}</span>
|
||||
</p>
|
||||
</section>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div id="main_content_wrap" class="outer">
|
||||
<section id="main_content" class="inner">
|
||||
{{ content }}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div id="footer_wrap" class="outer">
|
||||
<footer class="inner">
|
||||
<p class="copyright"><a href="{{ site.repository }}">{{ site.title }}</a> maintained by <a href="{{ site.owner_url }}">{{ site.owner }}</a></p>
|
||||
<p><a href="https://github.com/jsncostello/slate">Slate</a> theme by <a href="https://github.com/jsncostello">Jason Costello</a></p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
|
||||
document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));
|
||||
</script>
|
||||
<script type="text/javascript">
|
||||
try {
|
||||
var pageTracker = _gat._getTracker("{{ site.ga_id }}");
|
||||
pageTracker._trackPageview();
|
||||
} catch(err) {}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
47
jekyll/_layouts/page.html
Normal file
47
jekyll/_layouts/page.html
Normal file
@@ -0,0 +1,47 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset='utf-8' />
|
||||
<meta name="description" content="{{ site.tagline }}" />
|
||||
|
||||
<link rel="stylesheet" type="text/css" media="screen" href="style/stylesheet.css">
|
||||
<link rel="icon" type="image/png" href="images/favicon.png" />
|
||||
|
||||
<title>{{ page.title }}</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="header_wrap" class="outer">
|
||||
<header class="inner">
|
||||
<h1 id="project_title">
|
||||
<a href="index.html">{% include solaar.svg %} {{ site.title }}</a>
|
||||
</h1>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div id="main_content_wrap" class="outer">
|
||||
<section id="main_content" class="inner">
|
||||
{{ content }}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div id="footer_wrap" class="outer">
|
||||
<footer class="inner">
|
||||
<p class="copyright"><a href="{{ site.repository }}">{{ site.title }}</a> maintained by <a href="{{ site.owner_url }}">{{ site.owner }}</a></p>
|
||||
<p><a href="https://github.com/jsncostello/slate">Slate</a> theme by <a href="https://github.com/jsncostello">Jason Costello</a></p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
|
||||
document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));
|
||||
</script>
|
||||
<script type="text/javascript">
|
||||
try {
|
||||
var pageTracker = _gat._getTracker("{{ site.ga_id }}");
|
||||
pageTracker._trackPageview();
|
||||
} catch(err) {}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
jekyll/images/bg_hr.png
Normal file
BIN
jekyll/images/bg_hr.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 943 B |
BIN
jekyll/images/blacktocat.png
Normal file
BIN
jekyll/images/blacktocat.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
jekyll/images/icon_download.png
Normal file
BIN
jekyll/images/icon_download.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
BIN
jekyll/images/sprite_download.png
Normal file
BIN
jekyll/images/sprite_download.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
70
jekyll/style/pygment_trac.css
Normal file
70
jekyll/style/pygment_trac.css
Normal file
@@ -0,0 +1,70 @@
|
||||
.highlight .hll { background-color: #ffffcc }
|
||||
.highlight { background: #f0f3f3; }
|
||||
.highlight .c { color: #0099FF; font-style: italic } /* Comment */
|
||||
.highlight .err { color: #AA0000; background-color: #FFAAAA } /* Error */
|
||||
.highlight .k { color: #006699; font-weight: bold } /* Keyword */
|
||||
.highlight .o { color: #555555 } /* Operator */
|
||||
.highlight .cm { color: #0099FF; font-style: italic } /* Comment.Multiline */
|
||||
.highlight .cp { color: #009999 } /* Comment.Preproc */
|
||||
.highlight .c1 { color: #0099FF; font-style: italic } /* Comment.Single */
|
||||
.highlight .cs { color: #0099FF; font-weight: bold; font-style: italic } /* Comment.Special */
|
||||
.highlight .gd { background-color: #FFCCCC; border: 1px solid #CC0000 } /* Generic.Deleted */
|
||||
.highlight .ge { font-style: italic } /* Generic.Emph */
|
||||
.highlight .gr { color: #FF0000 } /* Generic.Error */
|
||||
.highlight .gh { color: #003300; font-weight: bold } /* Generic.Heading */
|
||||
.highlight .gi { background-color: #CCFFCC; border: 1px solid #00CC00 } /* Generic.Inserted */
|
||||
.highlight .go { color: #AAAAAA } /* Generic.Output */
|
||||
.highlight .gp { color: #000099; font-weight: bold } /* Generic.Prompt */
|
||||
.highlight .gs { font-weight: bold } /* Generic.Strong */
|
||||
.highlight .gu { color: #003300; font-weight: bold } /* Generic.Subheading */
|
||||
.highlight .gt { color: #99CC66 } /* Generic.Traceback */
|
||||
.highlight .kc { color: #006699; font-weight: bold } /* Keyword.Constant */
|
||||
.highlight .kd { color: #006699; font-weight: bold } /* Keyword.Declaration */
|
||||
.highlight .kn { color: #006699; font-weight: bold } /* Keyword.Namespace */
|
||||
.highlight .kp { color: #006699 } /* Keyword.Pseudo */
|
||||
.highlight .kr { color: #006699; font-weight: bold } /* Keyword.Reserved */
|
||||
.highlight .kt { color: #007788; font-weight: bold } /* Keyword.Type */
|
||||
.highlight .m { color: #FF6600 } /* Literal.Number */
|
||||
.highlight .s { color: #CC3300 } /* Literal.String */
|
||||
.highlight .na { color: #330099 } /* Name.Attribute */
|
||||
.highlight .nb { color: #336666 } /* Name.Builtin */
|
||||
.highlight .nc { color: #00AA88; font-weight: bold } /* Name.Class */
|
||||
.highlight .no { color: #336600 } /* Name.Constant */
|
||||
.highlight .nd { color: #9999FF } /* Name.Decorator */
|
||||
.highlight .ni { color: #999999; font-weight: bold } /* Name.Entity */
|
||||
.highlight .ne { color: #CC0000; font-weight: bold } /* Name.Exception */
|
||||
.highlight .nf { color: #CC00FF } /* Name.Function */
|
||||
.highlight .nl { color: #9999FF } /* Name.Label */
|
||||
.highlight .nn { color: #00CCFF; font-weight: bold } /* Name.Namespace */
|
||||
.highlight .nt { color: #330099; font-weight: bold } /* Name.Tag */
|
||||
.highlight .nv { color: #003333 } /* Name.Variable */
|
||||
.highlight .ow { color: #000000; font-weight: bold } /* Operator.Word */
|
||||
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
|
||||
.highlight .mf { color: #FF6600 } /* Literal.Number.Float */
|
||||
.highlight .mh { color: #FF6600 } /* Literal.Number.Hex */
|
||||
.highlight .mi { color: #FF6600 } /* Literal.Number.Integer */
|
||||
.highlight .mo { color: #FF6600 } /* Literal.Number.Oct */
|
||||
.highlight .sb { color: #CC3300 } /* Literal.String.Backtick */
|
||||
.highlight .sc { color: #CC3300 } /* Literal.String.Char */
|
||||
.highlight .sd { color: #CC3300; font-style: italic } /* Literal.String.Doc */
|
||||
.highlight .s2 { color: #CC3300 } /* Literal.String.Double */
|
||||
.highlight .se { color: #CC3300; font-weight: bold } /* Literal.String.Escape */
|
||||
.highlight .sh { color: #CC3300 } /* Literal.String.Heredoc */
|
||||
.highlight .si { color: #AA0000 } /* Literal.String.Interpol */
|
||||
.highlight .sx { color: #CC3300 } /* Literal.String.Other */
|
||||
.highlight .sr { color: #33AAAA } /* Literal.String.Regex */
|
||||
.highlight .s1 { color: #CC3300 } /* Literal.String.Single */
|
||||
.highlight .ss { color: #FFCC33 } /* Literal.String.Symbol */
|
||||
.highlight .bp { color: #336666 } /* Name.Builtin.Pseudo */
|
||||
.highlight .vc { color: #003333 } /* Name.Variable.Class */
|
||||
.highlight .vg { color: #003333 } /* Name.Variable.Global */
|
||||
.highlight .vi { color: #003333 } /* Name.Variable.Instance */
|
||||
.highlight .il { color: #FF6600 } /* Literal.Number.Integer.Long */
|
||||
|
||||
.type-csharp .highlight .k { color: #0000FF }
|
||||
.type-csharp .highlight .kt { color: #0000FF }
|
||||
.type-csharp .highlight .nf { color: #000000; font-weight: normal }
|
||||
.type-csharp .highlight .nc { color: #2B91AF }
|
||||
.type-csharp .highlight .nn { color: #000000 }
|
||||
.type-csharp .highlight .s { color: #A31515 }
|
||||
.type-csharp .highlight .sc { color: #A31515 }
|
||||
442
jekyll/style/stylesheet.css
Normal file
442
jekyll/style/stylesheet.css
Normal file
@@ -0,0 +1,442 @@
|
||||
/*******************************************************************************
|
||||
Slate Theme for Github Pages
|
||||
by Jason Costello, @jsncostello
|
||||
*******************************************************************************/
|
||||
|
||||
@import url(pygment_trac.css);
|
||||
|
||||
/*******************************************************************************
|
||||
MeyerWeb Reset
|
||||
*******************************************************************************/
|
||||
|
||||
html, body, div, span, applet, object, iframe,
|
||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||
a, abbr, acronym, address, big, cite, code,
|
||||
del, dfn, em, img, ins, kbd, q, s, samp,
|
||||
small, strike, strong, sub, sup, tt, var,
|
||||
b, u, i, center,
|
||||
dl, dt, dd, ol, ul, li,
|
||||
fieldset, form, label, legend,
|
||||
table, caption, tbody, tfoot, thead, tr, th, td,
|
||||
article, aside, canvas, details, embed,
|
||||
figure, figcaption, footer, header, hgroup,
|
||||
menu, nav, output, ruby, section, summary,
|
||||
time, mark, audio, video {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/* HTML5 display-role reset for older browsers */
|
||||
article, aside, details, figcaption, figure,
|
||||
footer, header, hgroup, menu, nav, section {
|
||||
display: block;
|
||||
}
|
||||
|
||||
ol, ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
blockquote, q {
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
a:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
Theme Styles
|
||||
*******************************************************************************/
|
||||
|
||||
body {
|
||||
box-sizing: border-box;
|
||||
color:#373737;
|
||||
background: #212121;
|
||||
font-size: 18px;
|
||||
font-family: 'Myriad Pro', Calibri, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.4;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin: 20px 0;
|
||||
font-weight: 700;
|
||||
color:#222222;
|
||||
font-family: 'Lucida Grande', 'Calibri', Helvetica, Arial, sans-serif;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
h2 {
|
||||
padding-bottom: 0px;
|
||||
font-size: 32px;
|
||||
background: url('../images/bg_hr.png') repeat-x bottom;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 21px;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 10px 0 15px 0;
|
||||
}
|
||||
|
||||
footer p {
|
||||
color: #f2f2f2;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #007edf;
|
||||
text-shadow: none;
|
||||
|
||||
transition: color 0.5s ease;
|
||||
transition: text-shadow 0.5s ease;
|
||||
-webkit-transition: color 0.5s ease;
|
||||
-webkit-transition: text-shadow 0.5s ease;
|
||||
-moz-transition: color 0.5s ease;
|
||||
-moz-transition: text-shadow 0.5s ease;
|
||||
-o-transition: color 0.5s ease;
|
||||
-o-transition: text-shadow 0.5s ease;
|
||||
-ms-transition: color 0.5s ease;
|
||||
-ms-transition: text-shadow 0.5s ease;
|
||||
}
|
||||
|
||||
#main_content a:hover {
|
||||
color: #0069ba;
|
||||
text-shadow: #0090ff 0px 0px 2px;
|
||||
}
|
||||
|
||||
footer a:hover {
|
||||
color: #43adff;
|
||||
text-shadow: #0090ff 0px 0px 2px;
|
||||
}
|
||||
|
||||
em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
img {
|
||||
position: relative;
|
||||
margin: 0 auto;
|
||||
max-width: 739px;
|
||||
padding: 5px;
|
||||
margin: 10px 0 10px 0;
|
||||
border: 1px solid #ebebeb;
|
||||
|
||||
box-shadow: 0 0 5px #ebebeb;
|
||||
-webkit-box-shadow: 0 0 5px #ebebeb;
|
||||
-moz-box-shadow: 0 0 5px #ebebeb;
|
||||
-o-box-shadow: 0 0 5px #ebebeb;
|
||||
-ms-box-shadow: 0 0 5px #ebebeb;
|
||||
}
|
||||
|
||||
img.logo {
|
||||
max-width: 48px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
bottom: -10px;
|
||||
|
||||
box-shadow: none;
|
||||
-webkit-box-shadow: 0;
|
||||
-moz-box-shadow: 0;
|
||||
-o-box-shadow: 0;
|
||||
-ms-box-shadow: 0;
|
||||
}
|
||||
|
||||
pre, code {
|
||||
width: 100%;
|
||||
color: #222;
|
||||
background-color: #fff;
|
||||
|
||||
font-family: Monaco, "Bitstream Vera Sans Mono", "Lucida Console", Terminal, monospace;
|
||||
font-size: 14px;
|
||||
|
||||
border-radius: 2px;
|
||||
-moz-border-radius: 2px;
|
||||
-webkit-border-radius: 2px;
|
||||
}
|
||||
|
||||
pre {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,.1);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 3px;
|
||||
margin: 0 3px;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,.1);
|
||||
}
|
||||
|
||||
pre code {
|
||||
display: block;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
color: #666;
|
||||
margin-bottom: 20px;
|
||||
padding: 0 0 0 20px;
|
||||
border-left: 3px solid #bbb;
|
||||
}
|
||||
|
||||
ul, ol, dl {
|
||||
margin-bottom: 15px
|
||||
}
|
||||
|
||||
ul li {
|
||||
list-style: inside;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
ol li {
|
||||
list-style: decimal inside;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
dl dt {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
dl dd {
|
||||
padding-left: 20px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
dl p {
|
||||
padding-left: 20px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
hr {
|
||||
height: 1px;
|
||||
margin-bottom: 5px;
|
||||
border: none;
|
||||
background: url('../images/bg_hr.png') repeat-x center;
|
||||
}
|
||||
|
||||
table {
|
||||
border: 1px solid #373737;
|
||||
margin-bottom: 20px;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
th {
|
||||
font-family: 'Lucida Grande', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
padding: 10px;
|
||||
background: #373737;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 10px;
|
||||
border: 1px solid #373737;
|
||||
}
|
||||
|
||||
form {
|
||||
background: #f2f2f2;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
Full-Width Styles
|
||||
*******************************************************************************/
|
||||
|
||||
.outer {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inner {
|
||||
position: relative;
|
||||
max-width: 940px;
|
||||
padding: 20px 10px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.copyright {
|
||||
float: right;
|
||||
}
|
||||
|
||||
#forkme_banner {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top:0;
|
||||
right: 10px;
|
||||
z-index: 10;
|
||||
padding: 10px 50px 10px 10px;
|
||||
color: #fff;
|
||||
background: url('../images/blacktocat.png') #0090ff no-repeat 95% 50%;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,.5);
|
||||
border-bottom-left-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
}
|
||||
|
||||
#header_wrap {
|
||||
background: #212121;
|
||||
}
|
||||
|
||||
#header_wrap .inner {
|
||||
padding 50px 10px 30px 10px;
|
||||
}
|
||||
|
||||
#project_title {
|
||||
margin: 0;
|
||||
color: #fff;
|
||||
font-size: 42px;
|
||||
font-weight: 700;
|
||||
text-shadow: #111 0px 0px 10px;
|
||||
}
|
||||
|
||||
#project_tagline {
|
||||
color: #fff;
|
||||
font-size: 24px;
|
||||
font-weight: 300;
|
||||
background: none;
|
||||
text-shadow: #111 0px 0px 10px;
|
||||
}
|
||||
|
||||
#downloads {
|
||||
position: absolute;
|
||||
width: 240px;
|
||||
z-index: 10;
|
||||
bottom: 0px;
|
||||
right: 0;
|
||||
height: 80px;
|
||||
background: url('../images/icon_download.png') no-repeat 15% 70%;
|
||||
}
|
||||
|
||||
.zip_download_link {
|
||||
display: block;
|
||||
float: right;
|
||||
width: 90px;
|
||||
height:70px;
|
||||
text-indent: -5000px;
|
||||
overflow: hidden;
|
||||
background: url(../images/sprite_download.png) no-repeat bottom left;
|
||||
}
|
||||
|
||||
.tar_download_link {
|
||||
display: block;
|
||||
float: right;
|
||||
width: 90px;
|
||||
height:70px;
|
||||
text-indent: -5000px;
|
||||
overflow: hidden;
|
||||
background: url(../images/sprite_download.png) no-repeat bottom right;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.zip_download_link:hover {
|
||||
background: url(../images/sprite_download.png) no-repeat top left;
|
||||
}
|
||||
|
||||
.tar_download_link:hover {
|
||||
background: url(../images/sprite_download.png) no-repeat top right;
|
||||
}
|
||||
|
||||
#main_content_wrap {
|
||||
background: #f2f2f2;
|
||||
border-top: 1px solid #111;
|
||||
border-bottom: 1px solid #111;
|
||||
}
|
||||
|
||||
#main_content {
|
||||
padding-top: 40px;
|
||||
}
|
||||
|
||||
#footer_wrap {
|
||||
background: #212121;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
Small Device Styles
|
||||
*******************************************************************************/
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
body {
|
||||
font-size:14px;
|
||||
}
|
||||
|
||||
#downloads {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.inner {
|
||||
min-width: 320px;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
#project_title {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 21px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
code, pre {
|
||||
min-width: 320px;
|
||||
max-width: 480px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,10 +1,37 @@
|
||||
# -*- python-mode -*-
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
## Copyright (C) 2012-2013 Daniel Pavel
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published by
|
||||
## the Free Software Foundation; either version 2 of the License, or
|
||||
## (at your option) any later version.
|
||||
##
|
||||
## This program is distributed in the hope that it will be useful,
|
||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
## GNU General Public License for more details.
|
||||
##
|
||||
## You should have received a copy of the GNU General Public License along
|
||||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
"""Generic Human Interface Device API."""
|
||||
|
||||
__author__ = "Daniel Pavel"
|
||||
__license__ = "GPL"
|
||||
__version__ = "0.4"
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
try:
|
||||
from hidapi.udev import *
|
||||
except ImportError:
|
||||
from hidapi.native import *
|
||||
__version__ = '0.9'
|
||||
|
||||
from hidapi.udev import (
|
||||
enumerate,
|
||||
open,
|
||||
close,
|
||||
open_path,
|
||||
monitor_glib,
|
||||
read,
|
||||
write,
|
||||
get_manufacturer,
|
||||
get_product,
|
||||
get_serial,
|
||||
)
|
||||
|
||||
@@ -1,109 +1,258 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- python-mode -*-
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
## Copyright (C) 2012-2013 Daniel Pavel
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published by
|
||||
## the Free Software Foundation; either version 2 of the License, or
|
||||
## (at your option) any later version.
|
||||
##
|
||||
## This program is distributed in the hope that it will be useful,
|
||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
## GNU General Public License for more details.
|
||||
##
|
||||
## You should have received a copy of the GNU General Public License along
|
||||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
import os
|
||||
import sys
|
||||
from select import select as _select
|
||||
import time
|
||||
from binascii import hexlify, unhexlify
|
||||
_hex = lambda d: hexlify(d).decode('ascii').upper()
|
||||
import hidapi as _hid
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
interactive = os.isatty(0)
|
||||
start_time = 0
|
||||
try:
|
||||
read_packet = raw_input
|
||||
except:
|
||||
except NameError:
|
||||
# Python 3 equivalent of raw_input
|
||||
read_packet = input
|
||||
|
||||
interactive = os.isatty(0)
|
||||
prompt = '?? Input: ' if interactive else ''
|
||||
start_time = time.time()
|
||||
|
||||
strhex = lambda d: hexlify(d).decode('ascii').upper()
|
||||
try:
|
||||
unicode
|
||||
# this is certanly Python 2
|
||||
is_string = lambda d: isinstance(d, unicode)
|
||||
# no easy way to distinguish between b'' and '' :(
|
||||
# or (isinstance(d, str) \
|
||||
# and not any((chr(k) in d for k in range(0x00, 0x1F))) \
|
||||
# and not any((chr(k) in d for k in range(0x80, 0xFF))) \
|
||||
# )
|
||||
except:
|
||||
# this is certanly Python 3
|
||||
# In Py3, unicode and str are equal (the unicode object does not exist)
|
||||
is_string = lambda d: isinstance(d, str)
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from threading import Lock
|
||||
print_lock = Lock()
|
||||
del Lock
|
||||
|
||||
def _print(marker, data, scroll=False):
|
||||
t = time.time() - start_time
|
||||
|
||||
if interactive and scroll:
|
||||
sys.stdout.write('\033[s')
|
||||
sys.stdout.write('\033[S') # scroll up
|
||||
sys.stdout.write('\033[A\033[L\033[G') # insert new line above the current one, position on first column
|
||||
|
||||
hexs = _hex(data)
|
||||
s = '%s (% 8.3f) [%s %s %s %s] %s' % (marker, t, hexs[0:2], hexs[2:4], hexs[4:8], hexs[8:], repr(data))
|
||||
sys.stdout.write(s)
|
||||
|
||||
if interactive and scroll:
|
||||
sys.stdout.write('\033[u')
|
||||
if is_string(data):
|
||||
s = marker + ' ' + data
|
||||
else:
|
||||
sys.stdout.write('\n')
|
||||
hexs = strhex(data)
|
||||
s = '%s (% 8.3f) [%s %s %s %s] %s' % (marker, t, hexs[0:2], hexs[2:4], hexs[4:8], hexs[8:], repr(data))
|
||||
|
||||
with print_lock:
|
||||
# allow only one thread at a time to write to the console, otherwise
|
||||
# the output gets garbled, especially with ANSI codes.
|
||||
|
||||
if interactive and scroll:
|
||||
# scroll the entire screen above the current line up by 1 line
|
||||
sys.stdout.write('\033[s' # save cursor position
|
||||
'\033[S' # scroll up
|
||||
'\033[A' # cursor up
|
||||
'\033[L' # insert 1 line
|
||||
'\033[G') # move cursor to column 1
|
||||
sys.stdout.write(s)
|
||||
if interactive and scroll:
|
||||
# restore cursor position
|
||||
sys.stdout.write('\033[u')
|
||||
else:
|
||||
sys.stdout.write('\n')
|
||||
|
||||
# flush stdout manually...
|
||||
# because trying to open stdin/out unbuffered programatically
|
||||
# works much too differently in Python 2/3
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def _continuous_read(handle, timeout=1000):
|
||||
def _error(text, scroll=False):
|
||||
_print('!!', text, scroll)
|
||||
|
||||
|
||||
def _continuous_read(handle, timeout=2000):
|
||||
while True:
|
||||
reply = hidapi.read(handle, 128, timeout)
|
||||
if reply is None:
|
||||
print ("!! Read failed, aborting")
|
||||
try:
|
||||
reply = _hid.read(handle, 128, timeout)
|
||||
except OSError as e:
|
||||
_error("Read failed, aborting: " + str(e), True)
|
||||
break
|
||||
elif reply:
|
||||
assert reply is not None
|
||||
if reply:
|
||||
_print('>>', reply, True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
def _validate_input(line, hidpp=False):
|
||||
try:
|
||||
data = unhexlify(line.encode('ascii'))
|
||||
except Exception as e:
|
||||
_error("Invalid input: " + str(e))
|
||||
return None
|
||||
|
||||
if hidpp:
|
||||
if len(data) < 4:
|
||||
_error("Invalid HID++ request: need at least 4 bytes")
|
||||
return None
|
||||
if data[:1] not in b'\x10\x11':
|
||||
_error("Invalid HID++ request: first byte must be 0x10 or 0x11")
|
||||
return None
|
||||
if data[1:2] not in b'\xFF\x01\x02\x03\x04\x05\x06':
|
||||
_error("Invalid HID++ request: second byte must be 0xFF or one of 0x01..0x06")
|
||||
return None
|
||||
if data[:1] == b'\x10':
|
||||
if len(data) > 7:
|
||||
_error("Invalid HID++ request: maximum length of a 0x10 request is 7 bytes")
|
||||
return None
|
||||
while len(data) < 7:
|
||||
data = (data + b'\x00' * 7)[:7]
|
||||
elif data[:1] == b'\x11':
|
||||
if len(data) > 20:
|
||||
_error("Invalid HID++ request: maximum length of a 0x11 request is 20 bytes")
|
||||
return None
|
||||
while len(data) < 20:
|
||||
data = (data + b'\x00' * 20)[:20]
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def _open(args):
|
||||
device = args.device
|
||||
if args.hidpp and not device:
|
||||
for d in _hid.enumerate(vendor_id=0x046d):
|
||||
if d.driver == 'logitech-djreceiver':
|
||||
device = d.path
|
||||
break
|
||||
if not device:
|
||||
sys.exit("!! No HID++ receiver found.")
|
||||
if not device:
|
||||
sys.exit("!! Device path required.")
|
||||
|
||||
print (".. Opening device", device)
|
||||
handle = _hid.open_path(device)
|
||||
if not handle:
|
||||
sys.exit("!! Failed to open %s, aborting." % device)
|
||||
|
||||
print (".. Opened handle %r, vendor %r product %r serial %r." % (
|
||||
handle,
|
||||
_hid.get_manufacturer(handle),
|
||||
_hid.get_product(handle),
|
||||
_hid.get_serial(handle)))
|
||||
if args.hidpp:
|
||||
if _hid.get_manufacturer(handle) != b'Logitech':
|
||||
sys.exit("!! Only Logitech devices support the HID++ protocol.")
|
||||
print (".. HID++ validation enabled.")
|
||||
else:
|
||||
if (_hid.get_manufacturer(handle) == b'Logitech' and
|
||||
b'Receiver' in _hid.get_product(handle)):
|
||||
args.hidpp = True
|
||||
print (".. Logitech receiver detected, HID++ validation enabled.")
|
||||
|
||||
return handle
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def _parse_arguments():
|
||||
import argparse
|
||||
arg_parser = argparse.ArgumentParser()
|
||||
arg_parser.add_argument('--history', help='history file')
|
||||
arg_parser.add_argument('device', default=None, help='linux device to connect to')
|
||||
args = arg_parser.parse_args()
|
||||
arg_parser.add_argument('--history', help="history file (default ~/.hidconsole-history)")
|
||||
arg_parser.add_argument('--hidpp', action='store_true', help="ensure input data is a valid HID++ request")
|
||||
arg_parser.add_argument('device', nargs='?', help="linux device to connect to (/dev/hidrawX); "
|
||||
"may be omitted if --hidpp is given, in which case it looks for the first Logitech receiver")
|
||||
return arg_parser.parse_args()
|
||||
|
||||
import hidapi
|
||||
print (".. Opening device %s" % args.device)
|
||||
handle = hidapi.open_path(args.device.encode('utf-8'))
|
||||
if handle:
|
||||
print (".. Opened handle %X, vendor %s product %s serial %s" % (handle,
|
||||
repr(hidapi.get_manufacturer(handle)),
|
||||
repr(hidapi.get_product(handle)),
|
||||
repr(hidapi.get_serial(handle))))
|
||||
if interactive:
|
||||
print (".. Press ^C/^D to exit, or type hex bytes to write to the device.")
|
||||
|
||||
import readline
|
||||
if args.history is None:
|
||||
import os.path
|
||||
args.history = os.path.join(os.path.expanduser("~"), ".hidconsole-history")
|
||||
try:
|
||||
readline.read_history_file(args.history)
|
||||
except:
|
||||
# file may not exist yet
|
||||
pass
|
||||
def main():
|
||||
args = _parse_arguments()
|
||||
handle = _open(args)
|
||||
|
||||
start_time = time.time()
|
||||
if interactive:
|
||||
print (".. Press ^C/^D to exit, or type hex bytes to write to the device.")
|
||||
|
||||
import readline
|
||||
if args.history is None:
|
||||
import os.path
|
||||
args.history = os.path.join(os.path.expanduser('~'), '.hidconsole-history')
|
||||
try:
|
||||
from threading import Thread
|
||||
t = Thread(target=_continuous_read, args=(handle,))
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
prompt = '?? Input: ' if interactive else ''
|
||||
|
||||
while t.is_alive():
|
||||
line = read_packet(prompt).strip().replace(' ', '')
|
||||
if line:
|
||||
try:
|
||||
data = unhexlify(line.encode('ascii'))
|
||||
except Exception as e:
|
||||
print ("!! Invalid input.")
|
||||
else:
|
||||
_print('<<', data)
|
||||
hidapi.write(handle, data)
|
||||
# wait for some kind of reply
|
||||
if not interactive:
|
||||
rlist, wlist, xlist = _select([handle], [], [], 1)
|
||||
time.sleep(0.1)
|
||||
except EOFError:
|
||||
readline.read_history_file(args.history)
|
||||
except:
|
||||
# file may not exist yet
|
||||
pass
|
||||
except Exception as e:
|
||||
print ('%s: %s' % (type(e).__name__, e))
|
||||
|
||||
print (".. Closing handle %X" % handle)
|
||||
hidapi.close(handle)
|
||||
try:
|
||||
from threading import Thread
|
||||
t = Thread(target=_continuous_read, args=(handle,))
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
if interactive:
|
||||
# move the cursor at the bottom of the screen
|
||||
sys.stdout.write('\033[300B') # move cusor at most 300 lines down, don't scroll
|
||||
|
||||
while t.is_alive():
|
||||
line = read_packet(prompt)
|
||||
line = line.strip().replace(' ', '')
|
||||
# print ("line", line)
|
||||
if not line:
|
||||
continue
|
||||
|
||||
data = _validate_input(line, args.hidpp)
|
||||
if data is None:
|
||||
continue
|
||||
|
||||
_print('<<', data)
|
||||
_hid.write(handle, data)
|
||||
# wait for some kind of reply
|
||||
if args.hidpp and not interactive:
|
||||
rlist, wlist, xlist = _select([handle], [], [], 1)
|
||||
if data[1:2] == b'\xFF':
|
||||
# the receiver will reply very fast, in a few milliseconds
|
||||
time.sleep(0.010)
|
||||
else:
|
||||
# the devices might reply quite slow
|
||||
time.sleep(0.700)
|
||||
except EOFError:
|
||||
if interactive:
|
||||
print ("")
|
||||
else:
|
||||
time.sleep(1)
|
||||
|
||||
finally:
|
||||
print (".. Closing handle %r" % handle)
|
||||
_hid.close(handle)
|
||||
if interactive:
|
||||
readline.write_history_file(args.history)
|
||||
else:
|
||||
print ("!! Failed to open %s, aborting" % args.device)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
@@ -1,380 +0,0 @@
|
||||
"""Generic Human Interface Device API.
|
||||
|
||||
It is little more than a thin ctypes layer over a native hidapi implementation.
|
||||
The docstrings are mostly copied from the hidapi API header, with changes where
|
||||
necessary.
|
||||
|
||||
The native HID API implemenation is available at
|
||||
https://github.com/signal11/hidapi.
|
||||
|
||||
The native implementation comes in two flavors, hidraw (``libhidapi-hidraw.so``)
|
||||
and libusb (``libhidapi-libusb.so``). For this API to work, at least one of them
|
||||
must be in ``LD_LIBRARY_PATH``; otherwise an ImportError will be raised.
|
||||
|
||||
Using the native hidraw implementation is recommended.
|
||||
Currently the native libusb implementation (temporarily) detaches the device's
|
||||
USB driver from the kernel, and it may cause the device to become unresponsive.
|
||||
"""
|
||||
|
||||
__version__ = '0.3-hidapi-0.7.0'
|
||||
|
||||
|
||||
import ctypes as _C
|
||||
from struct import pack as _pack
|
||||
|
||||
|
||||
#
|
||||
# look for a native implementation in the same directory as this file
|
||||
#
|
||||
|
||||
# The CDLL native library object.
|
||||
_native = None
|
||||
|
||||
for native_implementation in ('hidraw', 'libusb'):
|
||||
try:
|
||||
_native = _C.cdll.LoadLibrary('libhidapi-' + native_implementation + '.so')
|
||||
break
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
if _native is None:
|
||||
raise ImportError('hidapi: failed to load any HID API native implementation')
|
||||
|
||||
|
||||
#
|
||||
# Structures used by this API.
|
||||
#
|
||||
|
||||
|
||||
# used by the native implementation when enumerating, no need to expose it
|
||||
class _NativeDeviceInfo(_C.Structure):
|
||||
pass
|
||||
_NativeDeviceInfo._fields_ = [
|
||||
('path', _C.c_char_p),
|
||||
('vendor_id', _C.c_ushort),
|
||||
('product_id', _C.c_ushort),
|
||||
('serial', _C.c_wchar_p),
|
||||
('release', _C.c_ushort),
|
||||
('manufacturer', _C.c_wchar_p),
|
||||
('product', _C.c_wchar_p),
|
||||
('usage_page', _C.c_ushort),
|
||||
('usage', _C.c_ushort),
|
||||
('interface', _C.c_int),
|
||||
('next_device', _C.POINTER(_NativeDeviceInfo))
|
||||
]
|
||||
|
||||
|
||||
# the tuple object we'll expose when enumerating devices
|
||||
from collections import namedtuple
|
||||
DeviceInfo = namedtuple('DeviceInfo', [
|
||||
'path',
|
||||
'vendor_id',
|
||||
'product_id',
|
||||
'serial',
|
||||
'release',
|
||||
'manufacturer',
|
||||
'product',
|
||||
'interface',
|
||||
'driver',
|
||||
])
|
||||
del namedtuple
|
||||
|
||||
|
||||
# create a DeviceInfo tuple from a hid_device object
|
||||
def _makeDeviceInfo(native_device_info):
|
||||
return DeviceInfo(
|
||||
path=native_device_info.path.decode('ascii'),
|
||||
vendor_id=hex(native_device_info.vendor_id)[2:].zfill(4),
|
||||
product_id=hex(native_device_info.product_id)[2:].zfill(4),
|
||||
serial=native_device_info.serial if native_device_info.serial else None,
|
||||
release=hex(native_device_info.release)[2:],
|
||||
manufacturer=native_device_info.manufacturer,
|
||||
product=native_device_info.product,
|
||||
interface=native_device_info.interface,
|
||||
driver=None)
|
||||
|
||||
|
||||
#
|
||||
# set-up arguments and return types for each hidapi function
|
||||
#
|
||||
|
||||
_native.hid_init.argtypes = None
|
||||
_native.hid_init.restype = _C.c_int
|
||||
|
||||
_native.hid_exit.argtypes = None
|
||||
_native.hid_exit.restype = _C.c_int
|
||||
|
||||
_native.hid_enumerate.argtypes = [_C.c_ushort, _C.c_ushort]
|
||||
_native.hid_enumerate.restype = _C.POINTER(_NativeDeviceInfo)
|
||||
|
||||
_native.hid_free_enumeration.argtypes = [_C.POINTER(_NativeDeviceInfo)]
|
||||
_native.hid_free_enumeration.restype = None
|
||||
|
||||
_native.hid_open.argtypes = [_C.c_ushort, _C.c_ushort, _C.c_wchar_p]
|
||||
_native.hid_open.restype = _C.c_void_p
|
||||
|
||||
_native.hid_open_path.argtypes = [_C.c_char_p]
|
||||
_native.hid_open_path.restype = _C.c_void_p
|
||||
|
||||
_native.hid_close.argtypes = [_C.c_void_p]
|
||||
_native.hid_close.restype = None
|
||||
|
||||
_native.hid_write.argtypes = [_C.c_void_p, _C.c_char_p, _C.c_size_t]
|
||||
_native.hid_write.restype = _C.c_int
|
||||
|
||||
_native.hid_read.argtypes = [_C.c_void_p, _C.c_char_p, _C.c_size_t]
|
||||
_native.hid_read.restype = _C.c_int
|
||||
|
||||
_native.hid_read_timeout.argtypes = [_C.c_void_p, _C.c_char_p, _C.c_size_t, _C.c_int]
|
||||
_native.hid_read_timeout.restype = _C.c_int
|
||||
|
||||
_native.hid_set_nonblocking.argtypes = [_C.c_void_p, _C.c_int]
|
||||
_native.hid_set_nonblocking.restype = _C.c_int
|
||||
|
||||
_native.hid_send_feature_report.argtypes = [_C.c_void_p, _C.c_char_p, _C.c_size_t]
|
||||
_native.hid_send_feature_report.restype = _C.c_int
|
||||
|
||||
_native.hid_get_feature_report.argtypes = [_C.c_void_p, _C.c_char_p, _C.c_size_t]
|
||||
_native.hid_get_feature_report.restype = _C.c_int
|
||||
|
||||
_native.hid_get_manufacturer_string.argtypes = [_C.c_void_p, _C.c_wchar_p, _C.c_size_t]
|
||||
_native.hid_get_manufacturer_string.restype = _C.c_int
|
||||
|
||||
_native.hid_get_product_string.argtypes = [_C.c_void_p, _C.c_wchar_p, _C.c_size_t]
|
||||
_native.hid_get_product_string.restype = _C.c_int
|
||||
|
||||
_native.hid_get_serial_number_string.argtypes = [_C.c_void_p, _C.c_wchar_p, _C.c_size_t]
|
||||
_native.hid_get_serial_number_string.restype = _C.c_int
|
||||
|
||||
_native.hid_get_indexed_string.argtypes = [_C.c_void_p, _C.c_int, _C.c_wchar_p, _C.c_size_t]
|
||||
_native.hid_get_indexed_string.restype = _C.c_int
|
||||
|
||||
_native.hid_error.argtypes = [_C.c_void_p]
|
||||
_native.hid_error.restype = _C.c_wchar_p
|
||||
|
||||
|
||||
#
|
||||
# exposed API
|
||||
# docstrings mostly copied from hidapi.h
|
||||
#
|
||||
|
||||
|
||||
def init():
|
||||
"""Initialize the HIDAPI library.
|
||||
|
||||
This function initializes the HIDAPI library. Calling it is not strictly
|
||||
necessary, as it will be called automatically by enumerate() and any of the
|
||||
open_*() functions if it is needed. This function should be called at the
|
||||
beginning of execution however, if there is a chance of HIDAPI handles
|
||||
being opened by different threads simultaneously.
|
||||
|
||||
:returns: ``True`` if successful.
|
||||
"""
|
||||
return _native.hid_init() == 0
|
||||
|
||||
|
||||
def exit():
|
||||
"""Finalize the HIDAPI library.
|
||||
|
||||
This function frees all of the static data associated with HIDAPI. It should
|
||||
be called at the end of execution to avoid memory leaks.
|
||||
|
||||
:returns: ``True`` if successful.
|
||||
"""
|
||||
return _native.hid_exit() == 0
|
||||
|
||||
|
||||
def enumerate(vendor_id=None, product_id=None, interface_number=None):
|
||||
"""Enumerate the HID Devices.
|
||||
|
||||
List all the HID devices attached to the system, optionally filtering by
|
||||
vendor_id, product_id, and/or interface_number.
|
||||
|
||||
:returns: an iterable of matching ``DeviceInfo`` tuples.
|
||||
"""
|
||||
|
||||
devices = _native.hid_enumerate(vendor_id, product_id)
|
||||
d = devices
|
||||
while d:
|
||||
if interface_number is None or interface_number == d.contents.interface:
|
||||
yield _makeDeviceInfo(d.contents)
|
||||
d = d.contents.next_device
|
||||
|
||||
if devices:
|
||||
_native.hid_free_enumeration(devices)
|
||||
|
||||
|
||||
def open(vendor_id, product_id, serial=None):
|
||||
"""Open a HID device by its Vendor ID, Product ID and optional serial number.
|
||||
|
||||
If no serial is provided, the first device with the specified IDs is opened.
|
||||
|
||||
:returns: an opaque device handle, or ``None``.
|
||||
"""
|
||||
return _native.hid_open(vendor_id, product_id, serial) or None
|
||||
|
||||
|
||||
def open_path(device_path):
|
||||
"""Open a HID device by its path name.
|
||||
|
||||
:param device_path: the path of a ``DeviceInfo`` tuple returned by
|
||||
enumerate().
|
||||
|
||||
:returns: an opaque device handle, or ``None``.
|
||||
"""
|
||||
if type(device_path) == str:
|
||||
device_path = device_path.encode('ascii')
|
||||
return _native.hid_open_path(device_path) or None
|
||||
|
||||
|
||||
def close(device_handle):
|
||||
"""Close a HID device.
|
||||
|
||||
:param device_handle: a device handle returned by open() or open_path().
|
||||
"""
|
||||
_native.hid_close(device_handle)
|
||||
|
||||
|
||||
def write(device_handle, data):
|
||||
"""Write an Output report to a HID device.
|
||||
|
||||
:param device_handle: a device handle returned by open() or open_path().
|
||||
:param data: the data bytes to send including the report number as the
|
||||
first byte.
|
||||
|
||||
The first byte of data[] must contain the Report ID. For
|
||||
devices which only support a single report, this must be set
|
||||
to 0x0. The remaining bytes contain the report data. Since
|
||||
the Report ID is mandatory, calls to hid_write() will always
|
||||
contain one more byte than the report contains. For example,
|
||||
if a hid report is 16 bytes long, 17 bytes must be passed to
|
||||
hid_write(), the Report ID (or 0x0, for devices with a
|
||||
single report), followed by the report data (16 bytes). In
|
||||
this example, the length passed in would be 17.
|
||||
|
||||
write() will send the data on the first OUT endpoint, if
|
||||
one exists. If it does not, it will send the data through
|
||||
the Control Endpoint (Endpoint 0).
|
||||
|
||||
:returns: ``True`` if the write was successful.
|
||||
"""
|
||||
bytes_written = _native.hid_write(device_handle, _C.c_char_p(data), len(data))
|
||||
return bytes_written > -1
|
||||
|
||||
|
||||
def read(device_handle, bytes_count, timeout_ms=-1):
|
||||
"""Read an Input report from a HID device.
|
||||
|
||||
:param device_handle: a device handle returned by open() or open_path().
|
||||
:param bytes_count: maximum number of bytes to read.
|
||||
:param timeout_ms: can be -1 (default) to wait for data indefinitely, 0 to
|
||||
read whatever is in the device's input buffer, or a positive integer to
|
||||
wait that many milliseconds.
|
||||
|
||||
Input reports are returned to the host through the INTERRUPT IN endpoint.
|
||||
The first byte will contain the Report number if the device uses numbered
|
||||
reports.
|
||||
|
||||
:returns: the data packet read, an empty bytes string if a timeout was
|
||||
reached, or None if there was an error while reading.
|
||||
"""
|
||||
out_buffer = _C.create_string_buffer(b'\x00' * (bytes_count + 1))
|
||||
bytes_read = _native.hid_read_timeout(device_handle, out_buffer, bytes_count, timeout_ms)
|
||||
if bytes_read == -1:
|
||||
return None
|
||||
if bytes_read == 0:
|
||||
return b''
|
||||
return out_buffer[:bytes_read]
|
||||
|
||||
|
||||
def send_feature_report(device_handle, data, report_number=None):
|
||||
"""Send a Feature report to the device.
|
||||
|
||||
:param device_handle: a device handle returned by open() or open_path().
|
||||
:param data: the data bytes to send including the report number as the
|
||||
first byte.
|
||||
:param report_number: if set, it is sent as the first byte with the data.
|
||||
|
||||
Feature reports are sent over the Control endpoint as a
|
||||
Set_Report transfer. The first byte of data[] must
|
||||
contain the Report ID. For devices which only support a
|
||||
single report, this must be set to 0x0. The remaining bytes
|
||||
contain the report data. Since the Report ID is mandatory,
|
||||
calls to send_feature_report() will always contain one
|
||||
more byte than the report contains. For example, if a hid
|
||||
report is 16 bytes long, 17 bytes must be passed to
|
||||
send_feature_report(): the Report ID (or 0x0, for
|
||||
devices which do not use numbered reports), followed by the
|
||||
report data (16 bytes).
|
||||
|
||||
:returns: ``True`` if the report was successfully written to the device.
|
||||
"""
|
||||
if report_number is not None:
|
||||
data = _pack('!B', report_number) + data
|
||||
bytes_written = _native.hid_send_feature_report(device_handle, _C.c_char_p(data), len(data))
|
||||
return bytes_written > -1
|
||||
|
||||
|
||||
def get_feature_report(device_handle, bytes_count, report_number=None):
|
||||
"""Get a feature report from a HID device.
|
||||
|
||||
:param device_handle: a device handle returned by open() or open_path().
|
||||
:param bytes_count: how many bytes to read.
|
||||
:param report_number: if set, it is sent as the report number.
|
||||
|
||||
:returns: the feature report data.
|
||||
"""
|
||||
out_buffer = _C.create_string_buffer('\x00' * (bytes_count + 2))
|
||||
if report_number is not None:
|
||||
out_buffer[0] = _pack('!B', report_number)
|
||||
bytes_read = _native.hid_get_feature_report(device_handle, out_buffer, bytes_count)
|
||||
if bytes_read > -1:
|
||||
return out_buffer[:bytes_read]
|
||||
|
||||
|
||||
def _read_wchar(func, device_handle, index=None):
|
||||
_BUFFER_SIZE = 64
|
||||
buf = _C.create_unicode_buffer('\x00' * _BUFFER_SIZE)
|
||||
if index is None:
|
||||
ok = func(device_handle, buf, _BUFFER_SIZE)
|
||||
else:
|
||||
ok = func(device_handle, index, buf, _BUFFER_SIZE)
|
||||
if ok == 0:
|
||||
return buf.value
|
||||
|
||||
|
||||
def get_manufacturer(device_handle):
|
||||
"""Get the Manufacturer String from a HID device.
|
||||
|
||||
:param device_handle: a device handle returned by open() or open_path().
|
||||
"""
|
||||
return _read_wchar(_native.hid_get_manufacturer_string, device_handle)
|
||||
|
||||
|
||||
def get_product(device_handle):
|
||||
"""Get the Product String from a HID device.
|
||||
|
||||
:param device_handle: a device handle returned by open() or open_path().
|
||||
"""
|
||||
return _read_wchar(_native.hid_get_product_string, device_handle)
|
||||
|
||||
|
||||
def get_serial(device_handle):
|
||||
"""Get the serial number from a HID device.
|
||||
|
||||
:param device_handle: a device handle returned by open() or open_path().
|
||||
"""
|
||||
serial = _read_wchar(_native.hid_get_serial_number_string, device_handle)
|
||||
if serial is not None:
|
||||
return ''.join(hex(ord(c)) for c in serial)
|
||||
|
||||
|
||||
def get_indexed_string(device_handle, index):
|
||||
"""Get a string from a HID device, based on its string index.
|
||||
|
||||
Note: currently not working in the ``hidraw`` native implementation.
|
||||
|
||||
:param device_handle: a device handle returned by open() or open_path().
|
||||
:param index: the index of the string to get.
|
||||
"""
|
||||
return _read_wchar(_native.hid_get_indexed_string, device_handle, index)
|
||||
@@ -1,3 +1,22 @@
|
||||
# -*- python-mode -*-
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
## Copyright (C) 2012-2013 Daniel Pavel
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published by
|
||||
## the Free Software Foundation; either version 2 of the License, or
|
||||
## (at your option) any later version.
|
||||
##
|
||||
## This program is distributed in the hope that it will be useful,
|
||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
## GNU General Public License for more details.
|
||||
##
|
||||
## You should have received a copy of the GNU General Public License along
|
||||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
"""Generic Human Interface Device API.
|
||||
|
||||
It is currently a partial pure-Python implementation of the native HID API
|
||||
@@ -7,10 +26,12 @@ The docstrings are mostly copied from the hidapi API header, with changes where
|
||||
necessary.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
import os as _os
|
||||
import errno as _errno
|
||||
from select import select as _select
|
||||
from pyudev import Context as _Context
|
||||
from pyudev import Device as _Device
|
||||
from pyudev import Context as _Context, Monitor as _Monitor, Device as _Device
|
||||
|
||||
|
||||
native_implementation = 'udev'
|
||||
@@ -31,6 +52,7 @@ DeviceInfo = namedtuple('DeviceInfo', [
|
||||
])
|
||||
del namedtuple
|
||||
|
||||
|
||||
#
|
||||
# exposed API
|
||||
# docstrings mostly copied from hidapi.h
|
||||
@@ -54,7 +76,118 @@ def exit():
|
||||
return True
|
||||
|
||||
|
||||
def enumerate(vendor_id=None, product_id=None, interface_number=None):
|
||||
def _match(action, device, vendor_id=None, product_id=None, interface_number=None, hid_driver=None):
|
||||
usb_device = device.find_parent('usb', 'usb_device')
|
||||
# print ("* parent", action, device, "usb:", usb_device)
|
||||
if not usb_device:
|
||||
return
|
||||
|
||||
vid = usb_device.get('ID_VENDOR_ID')
|
||||
pid = usb_device.get('ID_MODEL_ID')
|
||||
if not ((vendor_id is None or vendor_id == int(vid, 16)) and
|
||||
(product_id is None or product_id == int(pid, 16))):
|
||||
return
|
||||
|
||||
if action == 'add':
|
||||
hid_device = device.find_parent('hid')
|
||||
if not hid_device:
|
||||
return
|
||||
hid_driver_name = hid_device.get('DRIVER')
|
||||
# print ("** found hid", action, device, "hid:", hid_device, hid_driver_name)
|
||||
if hid_driver:
|
||||
if isinstance(hid_driver, tuple):
|
||||
if hid_driver_name not in hid_driver:
|
||||
return
|
||||
elif hid_driver_name != hid_driver:
|
||||
return
|
||||
|
||||
intf_device = device.find_parent('usb', 'usb_interface')
|
||||
# print ("*** usb interface", action, device, "usb_interface:", intf_device)
|
||||
if interface_number is None:
|
||||
usb_interface = None if intf_device is None else intf_device.attributes.asint('bInterfaceNumber')
|
||||
else:
|
||||
usb_interface = None if intf_device is None else intf_device.attributes.asint('bInterfaceNumber')
|
||||
if usb_interface is None or interface_number != usb_interface:
|
||||
return
|
||||
|
||||
attrs = usb_device.attributes
|
||||
d_info = DeviceInfo(path=device.device_node,
|
||||
vendor_id=vid[-4:],
|
||||
product_id=pid[-4:],
|
||||
serial=hid_device.get('HID_UNIQ'),
|
||||
release=attrs.get('bcdDevice'),
|
||||
manufacturer=attrs.get('manufacturer'),
|
||||
product=attrs.get('product'),
|
||||
interface=usb_interface,
|
||||
driver=hid_driver_name)
|
||||
return d_info
|
||||
|
||||
elif action == 'remove':
|
||||
# print (dict(device), dict(usb_device))
|
||||
|
||||
d_info = DeviceInfo(path=device.device_node,
|
||||
vendor_id=vid[-4:],
|
||||
product_id=pid[-4:],
|
||||
serial=None,
|
||||
release=None,
|
||||
manufacturer=None,
|
||||
product=None,
|
||||
interface=None,
|
||||
driver=None)
|
||||
return d_info
|
||||
|
||||
|
||||
def monitor_glib(callback, *device_filters):
|
||||
from gi.repository import GLib
|
||||
|
||||
c = _Context()
|
||||
|
||||
# already existing devices
|
||||
# for device in c.list_devices(subsystem='hidraw'):
|
||||
# # print (device, dict(device), dict(device.attributes))
|
||||
# for filter in device_filters:
|
||||
# d_info = _match('add', device, *filter)
|
||||
# if d_info:
|
||||
# GLib.idle_add(callback, 'add', d_info)
|
||||
# break
|
||||
|
||||
m = _Monitor.from_netlink(c)
|
||||
m.filter_by(subsystem='hidraw')
|
||||
|
||||
def _process_udev_event(monitor, condition, cb, filters):
|
||||
if condition == GLib.IO_IN:
|
||||
event = monitor.receive_device()
|
||||
if event:
|
||||
action, device = event
|
||||
# print ("***", action, device)
|
||||
if action == 'add':
|
||||
for filter in filters:
|
||||
d_info = _match(action, device, *filter)
|
||||
if d_info:
|
||||
GLib.idle_add(cb, action, d_info)
|
||||
break
|
||||
elif action == 'remove':
|
||||
# the GLib notification does _not_ match!
|
||||
pass
|
||||
return True
|
||||
|
||||
try:
|
||||
# io_add_watch_full may not be available...
|
||||
GLib.io_add_watch_full(m, GLib.PRIORITY_LOW, GLib.IO_IN, _process_udev_event, callback, device_filters)
|
||||
# print ("did io_add_watch_full")
|
||||
except AttributeError:
|
||||
try:
|
||||
# and the priority parameter appeared later in the API
|
||||
GLib.io_add_watch(m, GLib.PRIORITY_LOW, GLib.IO_IN, _process_udev_event, callback, device_filters)
|
||||
# print ("did io_add_watch with priority")
|
||||
except:
|
||||
GLib.io_add_watch(m, GLib.IO_IN, _process_udev_event, callback, device_filters)
|
||||
# print ("did io_add_watch")
|
||||
|
||||
m.start()
|
||||
|
||||
|
||||
def enumerate(vendor_id=None, product_id=None, interface_number=None, hid_driver=None):
|
||||
"""Enumerate the HID Devices.
|
||||
|
||||
List all the HID devices attached to the system, optionally filtering by
|
||||
@@ -63,45 +196,9 @@ def enumerate(vendor_id=None, product_id=None, interface_number=None):
|
||||
:returns: a list of matching ``DeviceInfo`` tuples.
|
||||
"""
|
||||
for dev in _Context().list_devices(subsystem='hidraw'):
|
||||
hid_dev = dev.find_parent('hid')
|
||||
if not hid_dev:
|
||||
continue
|
||||
|
||||
assert 'HID_ID' in hid_dev
|
||||
bus, vid, pid = hid_dev['HID_ID'].split(':')
|
||||
if vendor_id is not None and vendor_id != int(vid, 16):
|
||||
continue
|
||||
if product_id is not None and product_id != int(pid, 16):
|
||||
continue
|
||||
|
||||
if bus == '0003': # USB
|
||||
intf_dev = dev.find_parent('usb', 'usb_interface')
|
||||
if not intf_dev:
|
||||
continue
|
||||
|
||||
interface = intf_dev.attributes.asint('bInterfaceNumber')
|
||||
if interface_number is not None and interface_number != interface:
|
||||
continue
|
||||
|
||||
serial = hid_dev['HID_UNIQ'] if 'HID_UNIQ' in hid_dev else None
|
||||
|
||||
usb_dev = dev.find_parent('usb', 'usb_device')
|
||||
assert usb_dev
|
||||
attrs = usb_dev.attributes
|
||||
d_info = DeviceInfo(path=dev.device_node,
|
||||
vendor_id=vid[-4:],
|
||||
product_id=pid[-4:],
|
||||
serial=serial,
|
||||
release=attrs['bcdDevice'],
|
||||
manufacturer=attrs['manufacturer'],
|
||||
product=attrs['product'],
|
||||
interface=interface,
|
||||
driver=hid_dev['DRIVER'])
|
||||
yield d_info
|
||||
|
||||
elif bus == '0005': # BLUETOOTH
|
||||
# TODO
|
||||
pass
|
||||
dev_info = _match('add', dev, vendor_id, product_id, interface_number, hid_driver)
|
||||
if dev_info:
|
||||
yield dev_info
|
||||
|
||||
|
||||
def open(vendor_id, product_id, serial=None):
|
||||
@@ -124,6 +221,8 @@ def open_path(device_path):
|
||||
|
||||
:returns: an opaque device handle, or ``None``.
|
||||
"""
|
||||
assert device_path
|
||||
assert device_path.startswith('/dev/hidraw')
|
||||
return _os.open(device_path, _os.O_RDWR | _os.O_SYNC)
|
||||
|
||||
|
||||
@@ -132,6 +231,7 @@ def close(device_handle):
|
||||
|
||||
:param device_handle: a device handle returned by open() or open_path().
|
||||
"""
|
||||
assert device_handle
|
||||
_os.close(device_handle)
|
||||
|
||||
|
||||
@@ -155,14 +255,13 @@ def write(device_handle, data):
|
||||
write() will send the data on the first OUT endpoint, if
|
||||
one exists. If it does not, it will send the data through
|
||||
the Control Endpoint (Endpoint 0).
|
||||
|
||||
:returns: ``True`` if the write was successful.
|
||||
"""
|
||||
try:
|
||||
bytes_written = _os.write(device_handle, data)
|
||||
return bytes_written == len(data)
|
||||
except:
|
||||
pass
|
||||
assert device_handle
|
||||
assert data
|
||||
assert isinstance(data, bytes), (repr(data), type(data))
|
||||
bytes_written = _os.write(device_handle, data)
|
||||
if bytes_written != len(data):
|
||||
raise IOError(_errno.EIO, 'written %d bytes out of expected %d' % (bytes_written, len(data)))
|
||||
|
||||
|
||||
def read(device_handle, bytes_count, timeout_ms=-1):
|
||||
@@ -181,15 +280,22 @@ def read(device_handle, bytes_count, timeout_ms=-1):
|
||||
:returns: the data packet read, an empty bytes string if a timeout was
|
||||
reached, or None if there was an error while reading.
|
||||
"""
|
||||
try:
|
||||
timeout = None if timeout_ms < 0 else timeout_ms / 1000.0
|
||||
rlist, wlist, xlist = _select([device_handle], [], [], timeout)
|
||||
if rlist:
|
||||
assert rlist == [device_handle]
|
||||
return _os.read(device_handle, bytes_count)
|
||||
assert device_handle
|
||||
timeout = None if timeout_ms < 0 else timeout_ms / 1000.0
|
||||
rlist, wlist, xlist = _select([device_handle], [], [device_handle], timeout)
|
||||
|
||||
if xlist:
|
||||
assert xlist == [device_handle]
|
||||
raise IOError(_errno.EIO, 'exception on file descriptor %d' % device_handle)
|
||||
|
||||
if rlist:
|
||||
assert rlist == [device_handle]
|
||||
data = _os.read(device_handle, bytes_count)
|
||||
assert data is not None
|
||||
assert isinstance(data, bytes), (repr(data), type(data))
|
||||
return data
|
||||
else:
|
||||
return b''
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
_DEVICE_STRINGS = {
|
||||
@@ -236,13 +342,14 @@ def get_indexed_string(device_handle, index):
|
||||
if index not in _DEVICE_STRINGS:
|
||||
return None
|
||||
|
||||
assert device_handle
|
||||
stat = _os.fstat(device_handle)
|
||||
dev = _Device.from_device_number(_Context(), 'char', stat.st_rdev)
|
||||
if dev:
|
||||
hid_dev = dev.find_parent('hid')
|
||||
if hid_dev:
|
||||
assert 'HID_ID' in hid_dev
|
||||
bus, _, _ = hid_dev['HID_ID'].split(':')
|
||||
bus, _ignore, _ignore = hid_dev['HID_ID'].split(':')
|
||||
|
||||
if bus == '0003': # USB
|
||||
usb_dev = dev.find_parent('usb', 'usb_device')
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
#
|
||||
|
||||
__author__ = "Daniel Pavel"
|
||||
__license__ = "GPL"
|
||||
__version__ = "0.5"
|
||||
@@ -1,100 +0,0 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
import logging
|
||||
|
||||
from .constants import (STATUS, PROPS)
|
||||
from ..unifying_receiver.constants import (FEATURE, BATTERY_STATUS)
|
||||
from ..unifying_receiver import api as _api
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
_DEVICE_MODULES = {}
|
||||
|
||||
def _module(device_name):
|
||||
if device_name not in _DEVICE_MODULES:
|
||||
shortname = device_name.split(' ')[-1].lower()
|
||||
try:
|
||||
m = __import__(shortname, globals(), level=1)
|
||||
_DEVICE_MODULES[device_name] = m
|
||||
except:
|
||||
# logging.exception(shortname)
|
||||
_DEVICE_MODULES[device_name] = None
|
||||
|
||||
return _DEVICE_MODULES[device_name]
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def default_request_status(devinfo):
|
||||
if FEATURE.BATTERY in devinfo.features:
|
||||
reply = _api.get_device_battery_level(devinfo.handle, devinfo.number, features=devinfo.features)
|
||||
if reply:
|
||||
discharge, dischargeNext, status = reply
|
||||
return STATUS.CONNECTED, {PROPS.BATTERY_LEVEL: discharge, PROPS.BATTERY_STATUS: status}
|
||||
|
||||
reply = _api.ping(devinfo.handle, devinfo.number)
|
||||
return STATUS.CONNECTED if reply else STATUS.UNAVAILABLE
|
||||
|
||||
|
||||
def default_process_event(devinfo, data):
|
||||
feature_index = ord(data[0:1])
|
||||
if feature_index >= len(devinfo.features):
|
||||
logging.warn("mistery event %s for %s", repr(data), devinfo)
|
||||
return None
|
||||
|
||||
feature = devinfo.features[feature_index]
|
||||
feature_function = ord(data[1:2]) & 0xF0
|
||||
|
||||
if feature == FEATURE.BATTERY:
|
||||
if feature_function == 0:
|
||||
discharge = ord(data[2:3])
|
||||
status = BATTERY_STATUS[ord(data[3:4])]
|
||||
return STATUS.CONNECTED, {PROPS.BATTERY_LEVEL: discharge, PROPS.BATTERY_STATUS: status}
|
||||
# ?
|
||||
elif feature == FEATURE.REPROGRAMMABLE_KEYS:
|
||||
if feature_function == 0:
|
||||
logging.debug('reprogrammable key: %s', repr(data))
|
||||
# TODO
|
||||
pass
|
||||
# ?
|
||||
elif feature == FEATURE.WIRELESS:
|
||||
if feature_function == 0:
|
||||
logging.debug("wireless status: %s", repr(data))
|
||||
if data[2:5] == b'\x01\x01\x01':
|
||||
return STATUS.CONNECTED
|
||||
# TODO
|
||||
pass
|
||||
# ?
|
||||
|
||||
|
||||
def request_status(devinfo):
|
||||
"""Trigger a status request for a device.
|
||||
|
||||
:param devinfo: the device info tuple.
|
||||
:param listener: the EventsListener that will be used to send the request,
|
||||
and which will receive the status events from the device.
|
||||
"""
|
||||
m = _module(devinfo.name)
|
||||
if m and 'request_status' in m.__dict__:
|
||||
return m.request_status(devinfo)
|
||||
return default_request_status(devinfo)
|
||||
|
||||
|
||||
def process_event(devinfo, data):
|
||||
"""Process an event received for a device.
|
||||
|
||||
:param devinfo: the device info tuple.
|
||||
:param data: the event data (event packet sans the first two bytes: reply code and device number)
|
||||
"""
|
||||
default_result = default_process_event(devinfo, data)
|
||||
if default_result is not None:
|
||||
return default_result
|
||||
|
||||
m = _module(devinfo.name)
|
||||
if m and 'process_event' in m.__dict__:
|
||||
return m.process_event(devinfo, data)
|
||||
@@ -1,47 +0,0 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
STATUS = type('STATUS', (),
|
||||
dict(
|
||||
UNKNOWN=-9999,
|
||||
UNPAIRED=-1000,
|
||||
UNAVAILABLE=-1,
|
||||
BOOTING=0,
|
||||
CONNECTED=1,
|
||||
))
|
||||
|
||||
STATUS_NAME = {
|
||||
STATUS.UNKNOWN: '...',
|
||||
STATUS.UNPAIRED: 'unpaired',
|
||||
STATUS.UNAVAILABLE: 'inactive',
|
||||
STATUS.BOOTING: 'initializing',
|
||||
STATUS.CONNECTED: 'connected',
|
||||
}
|
||||
|
||||
|
||||
# device properties that may be reported
|
||||
PROPS = type('PROPS', (),
|
||||
dict(
|
||||
BATTERY_LEVEL='battery_level',
|
||||
BATTERY_STATUS='battery_status',
|
||||
LIGHT_LEVEL='light_level',
|
||||
))
|
||||
|
||||
# when the receiver reports a device that is not connected
|
||||
# (and thus cannot be queried), guess the name and type
|
||||
# based on this table
|
||||
NAMES = {
|
||||
'M315': ('Wireless Mouse M315', 'mouse'),
|
||||
'M325': ('Wireless Mouse M325', 'mouse'),
|
||||
'M510': ('Wireless Mouse M510', 'mouse'),
|
||||
'M515': ('Couch Mouse M515', 'mouse'),
|
||||
'M525': ('Wireless Mouse M525', 'mouse'),
|
||||
'M570': ('Wireless Trackball M570', 'trackball'),
|
||||
'K270': ('Wireless Keyboard K270', 'keyboard'),
|
||||
'K350': ('Wireless Keyboard K350', 'keyboard'),
|
||||
'K750': ('Wireless Solar Keyboard K750', 'keyboard'),
|
||||
'K800': ('Wireless Illuminated Keyboard K800', 'keyboard'),
|
||||
'T650': ('Wireless Rechargeable Touchpad T650', 'touchpad'),
|
||||
'Performance MX': ('Performance Mouse MX', 'mouse'),
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
#
|
||||
# Functions specific to the K750 solar keyboard.
|
||||
#
|
||||
|
||||
import logging
|
||||
from struct import unpack as _unpack
|
||||
|
||||
from .constants import (STATUS, PROPS)
|
||||
from ..unifying_receiver.constants import FEATURE
|
||||
from ..unifying_receiver import api as _api
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
_CHARGE_LEVELS = (10, 25, 256)
|
||||
def _charge_status(data, hasLux=False):
|
||||
charge, lux = _unpack('!BH', data[2:5])
|
||||
|
||||
for i in range(0, len(_CHARGE_LEVELS)):
|
||||
if charge < _CHARGE_LEVELS[i]:
|
||||
charge_index = i
|
||||
break
|
||||
|
||||
return 0x10 << charge_index, {
|
||||
PROPS.BATTERY_LEVEL: charge,
|
||||
PROPS.LIGHT_LEVEL: lux if hasLux else None,
|
||||
}
|
||||
|
||||
|
||||
def request_status(devinfo):
|
||||
reply = _api.request(devinfo.handle, devinfo.number,
|
||||
feature=FEATURE.SOLAR_CHARGE, function=b'\x03', params=b'\x78\x01',
|
||||
features=devinfo.features)
|
||||
if reply is None:
|
||||
return STATUS.UNAVAILABLE
|
||||
|
||||
|
||||
def process_event(devinfo, data):
|
||||
if data[:2] == b'\x09\x00' and data[7:11] == b'GOOD':
|
||||
# usually sent after the keyboard is turned on or just connected
|
||||
return _charge_status(data)
|
||||
|
||||
if data[:2] == b'\x09\x10' and data[7:11] == b'GOOD':
|
||||
# regular solar charge events
|
||||
return _charge_status(data, True)
|
||||
|
||||
if data[:2] == b'\x09\x20' and data[7:11] == b'GOOD':
|
||||
logging.debug("Solar key pressed")
|
||||
return request_status(devinfo) or _charge_status(data)
|
||||
@@ -1,73 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
|
||||
def print_receiver(receiver):
|
||||
print (str(receiver))
|
||||
|
||||
print (" Serial : %s" % receiver.serial)
|
||||
for f in receiver.firmware:
|
||||
print (" %-10s: %s" % (f.kind, f.version))
|
||||
|
||||
|
||||
def scan_devices(receiver):
|
||||
for dev in receiver:
|
||||
print ("--------")
|
||||
print (str(dev))
|
||||
print ("Name : %s" % dev.name)
|
||||
print ("Kind : %s" % dev.kind)
|
||||
print ("Serial number: %s" % dev.serial)
|
||||
if not dev.protocol:
|
||||
print ("HID protocol : UNKNOWN")
|
||||
continue
|
||||
|
||||
print ("HID protocol : HID %01.1f" % dev.protocol)
|
||||
if dev.protocol < 2.0:
|
||||
print ("Features query not supported by this device")
|
||||
continue
|
||||
|
||||
firmware = dev.firmware
|
||||
for fw in firmware:
|
||||
print (" %-10s: %s %s" % (fw.kind, fw.name, fw.version))
|
||||
|
||||
all_features = api.get_device_features(dev.handle, dev.number)
|
||||
for index in range(0, len(all_features)):
|
||||
feature = all_features[index]
|
||||
if feature:
|
||||
print (" ~ Feature %-20s (%s) at index %02X" % (FEATURE_NAME[feature], api._hex(feature), index))
|
||||
|
||||
if FEATURE.BATTERY in all_features:
|
||||
discharge, dischargeNext, status = api.get_device_battery_level(dev.handle, dev.number, features=all_features)
|
||||
print (" Battery %d charged (next level %d%), status %s" % (discharge, dischargeNext, status))
|
||||
|
||||
if FEATURE.REPROGRAMMABLE_KEYS in all_features:
|
||||
keys = api.get_device_keys(dev.handle, dev.number, features=all_features)
|
||||
if keys is not None and keys:
|
||||
print (" %d reprogrammable keys found" % len(keys))
|
||||
for k in keys:
|
||||
flags = ','.join(KEY_FLAG_NAME[f] for f in KEY_FLAG_NAME if k.flags & f)
|
||||
print (" %2d: %-12s => %-12s :%s" % (k.index, KEY_NAME[k.id], KEY_NAME[k.task], flags))
|
||||
|
||||
print ("--------")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import argparse
|
||||
arg_parser = argparse.ArgumentParser(prog='scan')
|
||||
arg_parser.add_argument('-v', '--verbose', action='store_true', default=False,
|
||||
help='log the HID data traffic')
|
||||
args = arg_parser.parse_args()
|
||||
|
||||
import logging
|
||||
logging.basicConfig(level=logging.DEBUG if args.verbose else logging.WARNING)
|
||||
|
||||
from .unifying_receiver import api
|
||||
from .unifying_receiver.constants import *
|
||||
|
||||
receiver = api.Receiver.open()
|
||||
if receiver is None:
|
||||
print ("!! Logitech Unifying Receiver not found.")
|
||||
else:
|
||||
print ("!! Found Logitech Unifying Receiver: %s" % receiver)
|
||||
print_receiver(receiver)
|
||||
scan_devices(receiver)
|
||||
receiver.close()
|
||||
@@ -1,35 +0,0 @@
|
||||
"""Low-level interface for devices connected through a Logitech Universal
|
||||
Receiver (UR).
|
||||
|
||||
Uses the HID api exposed through hidapi.py, a Python thin layer over a native
|
||||
implementation.
|
||||
|
||||
Incomplete. Based on a bit of documentation, trial-and-error, and guesswork.
|
||||
|
||||
Strongly recommended to use these functions from a single thread; calling
|
||||
multiple functions from different threads has a high chance of mixing the
|
||||
replies and causing apparent failures.
|
||||
|
||||
Basic order of operations is:
|
||||
- open() to obtain a UR handle
|
||||
- request() to make a feature call to one of the devices attached to the UR
|
||||
- close() to close the UR handle
|
||||
|
||||
References:
|
||||
http://julien.danjou.info/blog/2012/logitech-k750-linux-support
|
||||
http://6xq.net/git/lars/lshidpp.git/plain/doc/
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
if logging.root.level > logging.DEBUG:
|
||||
log = logging.getLogger('LUR')
|
||||
log.addHandler(logging.NullHandler())
|
||||
log.propagate = 0
|
||||
|
||||
del logging
|
||||
|
||||
|
||||
from .constants import *
|
||||
from .exceptions import *
|
||||
from .api import *
|
||||
@@ -1,501 +0,0 @@
|
||||
#
|
||||
# Logitech Unifying Receiver API.
|
||||
#
|
||||
|
||||
from struct import pack as _pack
|
||||
from struct import unpack as _unpack
|
||||
import errno as _errno
|
||||
|
||||
|
||||
from . import base as _base
|
||||
from .common import (FirmwareInfo as _FirmwareInfo,
|
||||
ReprogrammableKeyInfo as _ReprogrammableKeyInfo)
|
||||
from .constants import (FEATURE, FEATURE_NAME, FEATURE_FLAGS,
|
||||
FIRMWARE_KIND, DEVICE_KIND,
|
||||
BATTERY_STATUS, KEY_NAME,
|
||||
MAX_ATTACHED_DEVICES)
|
||||
from .exceptions import FeatureNotSupported as _FeatureNotSupported
|
||||
|
||||
|
||||
_hex = _base._hex
|
||||
|
||||
from logging import getLogger
|
||||
_log = getLogger('LUR').getChild('api')
|
||||
del getLogger
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
class PairedDevice(object):
|
||||
def __init__(self, handle, number):
|
||||
self.handle = handle
|
||||
self.number = number
|
||||
|
||||
self._protocol = None
|
||||
self._features = None
|
||||
self._codename = None
|
||||
self._name = None
|
||||
self._kind = None
|
||||
self._serial = None
|
||||
self._firmware = None
|
||||
|
||||
@property
|
||||
def protocol(self):
|
||||
if self._protocol is None:
|
||||
self._protocol = _base.ping(self.handle, self.number)
|
||||
return 0 if self._protocol is None else self._protocol
|
||||
|
||||
@property
|
||||
def features(self):
|
||||
if self._features is None:
|
||||
if self.protocol >= 2.0:
|
||||
self._features = [FEATURE.ROOT]
|
||||
return self._features
|
||||
|
||||
@property
|
||||
def codename(self):
|
||||
if self._codename is None:
|
||||
codename = _base.request(self.handle, 0xFF, b'\x83\xB5', 0x40 + self.number - 1)
|
||||
if codename:
|
||||
self._codename = codename[2:].rstrip(b'\x00').decode('ascii')
|
||||
return self._codename or '?'
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
if self._name is None:
|
||||
if self.protocol < 2.0:
|
||||
from ..devices.constants import NAMES as _DEVICE_NAMES
|
||||
if self.codename in _DEVICE_NAMES:
|
||||
self._name, self._kind = _DEVICE_NAMES[self._codename]
|
||||
else:
|
||||
self._name = get_device_name(self.handle, self.number, self.features)
|
||||
return self._name or self.codename
|
||||
|
||||
@property
|
||||
def kind(self):
|
||||
if self._kind is None:
|
||||
if self.protocol < 2.0:
|
||||
from ..devices.constants import NAMES as _DEVICE_NAMES
|
||||
if self.codename in _DEVICE_NAMES:
|
||||
self._name, self._kind = _DEVICE_NAMES[self._codename]
|
||||
else:
|
||||
self._kind = get_device_kind(self.handle, self.number, self.features)
|
||||
return self._kind or '?'
|
||||
|
||||
@property
|
||||
def firmware(self):
|
||||
if self._firmware is None and self.protocol >= 2.0:
|
||||
self._firmware = get_device_firmware(self.handle, self.number, self.features)
|
||||
return self._firmware or ()
|
||||
|
||||
@property
|
||||
def serial(self):
|
||||
if self._serial is None:
|
||||
prefix = _base.request(self.handle, 0xFF, b'\x83\xB5', 0x20 + self.number - 1)
|
||||
serial = _base.request(self.handle, 0xFF, b'\x83\xB5', 0x30 + self.number - 1)
|
||||
if prefix and serial:
|
||||
self._serial = _base._hex(prefix[3:5]) + '-' + _base._hex(serial[1:5])
|
||||
return self._serial or '?'
|
||||
|
||||
def ping(self):
|
||||
return _base.ping(self.handle, self.number) is not None
|
||||
|
||||
def __str__(self):
|
||||
return '<PairedDevice(%X,%d,%s)>' % (self.handle, self.number, self._name or '?')
|
||||
|
||||
def __hash__(self):
|
||||
return self.number
|
||||
|
||||
|
||||
class Receiver(object):
|
||||
name = 'Unifying Receiver'
|
||||
max_devices = MAX_ATTACHED_DEVICES
|
||||
|
||||
def __init__(self, handle, path=None):
|
||||
self.handle = handle
|
||||
self.path = path
|
||||
|
||||
self._serial = None
|
||||
self._firmware = None
|
||||
|
||||
def close(self):
|
||||
handle, self.handle = self.handle, 0
|
||||
return (handle and _base.close(handle))
|
||||
|
||||
@property
|
||||
def serial(self):
|
||||
if self._serial is None and self.handle:
|
||||
serial = _base.request(self.handle, 0xFF, b'\x83\xB5', b'\x03')
|
||||
if serial:
|
||||
self._serial = _hex(serial[1:5])
|
||||
return self._serial
|
||||
|
||||
@property
|
||||
def firmware(self):
|
||||
if self._firmware is None and self.handle:
|
||||
firmware = []
|
||||
|
||||
reply = _base.request(self.handle, 0xFF, b'\x83\xB5', b'\x02')
|
||||
if reply and reply[0:1] == b'\x02':
|
||||
fw_version = _hex(reply[1:5])
|
||||
fw_version = '%s.%s.B%s' % (fw_version[0:2], fw_version[2:4], fw_version[4:8])
|
||||
firmware.append(_FirmwareInfo(0, FIRMWARE_KIND[0], '', fw_version, None))
|
||||
|
||||
reply = _base.request(self.handle, 0xFF, b'\x81\xF1', b'\x04')
|
||||
if reply and reply[0:1] == b'\x04':
|
||||
bl_version = _hex(reply[1:3])
|
||||
bl_version = '%s.%s' % (bl_version[0:2], bl_version[2:4])
|
||||
firmware.append(_FirmwareInfo(1, FIRMWARE_KIND[1], '', bl_version, None))
|
||||
|
||||
self._firmware = tuple(firmware)
|
||||
|
||||
return self._firmware
|
||||
|
||||
def __iter__(self):
|
||||
if self.handle == 0:
|
||||
return
|
||||
|
||||
for number in range(1, 1 + MAX_ATTACHED_DEVICES):
|
||||
dev = get_device(self.handle, number)
|
||||
if dev is not None:
|
||||
yield dev
|
||||
|
||||
def __getitem__(self, key):
|
||||
if type(key) != int:
|
||||
raise TypeError('key must be an integer')
|
||||
if self.handle == 0 or key < 0 or key > MAX_ATTACHED_DEVICES:
|
||||
raise IndexError(key)
|
||||
return get_device(self.handle, key) if key > 0 else None
|
||||
|
||||
def __delitem__(self, key):
|
||||
if type(key) != int:
|
||||
raise TypeError('key must be an integer')
|
||||
if self.handle == 0 or key < 0 or key > MAX_ATTACHED_DEVICES:
|
||||
raise IndexError(key)
|
||||
if key > 0:
|
||||
_log.debug("unpairing device %d", key)
|
||||
reply = _base.request(self.handle, 0xFF, b'\x80\xB2', _pack('!BB', 0x03, key))
|
||||
if reply is None or reply[1:2] == b'\x8F':
|
||||
raise IndexError(key)
|
||||
|
||||
def __len__(self):
|
||||
if self.handle == 0:
|
||||
return 0
|
||||
# not really sure about this one...
|
||||
count = _base.request(self.handle, 0xFF, b'\x81\x00')
|
||||
return 0 if count is None else ord(count[1:2])
|
||||
|
||||
def __contains__(self, dev):
|
||||
if self.handle == 0:
|
||||
return False
|
||||
if type(dev) == int:
|
||||
return dev > 0 and dev <= MAX_ATTACHED_DEVICES and _base.ping(self.handle, dev) is not None
|
||||
return dev.ping()
|
||||
|
||||
def __str__(self):
|
||||
return '<Receiver(%X,%s)>' % (self.handle, self.path)
|
||||
|
||||
def __hash__(self):
|
||||
return self.handle
|
||||
|
||||
__bool__ = __nonzero__ = lambda self: self.handle != 0
|
||||
|
||||
@classmethod
|
||||
def open(self):
|
||||
"""Opens the first Logitech Unifying Receiver found attached to the machine.
|
||||
|
||||
:returns: An open file handle for the found receiver, or ``None``.
|
||||
"""
|
||||
exception = None
|
||||
|
||||
for rawdevice in _base.list_receiver_devices():
|
||||
exception = None
|
||||
try:
|
||||
handle = _base.try_open(rawdevice.path)
|
||||
if handle:
|
||||
return Receiver(handle, rawdevice.path)
|
||||
except OSError as e:
|
||||
_log.exception("open %s", rawdevice.path)
|
||||
if e.errno == _errno.EACCES:
|
||||
exception = e
|
||||
|
||||
if exception:
|
||||
# only keep the last exception
|
||||
raise exception
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def request(handle, devnumber, feature, function=b'\x00', params=b'', features=None):
|
||||
"""Makes a feature call to the device, and returns the reply data.
|
||||
|
||||
Basically a write() followed by (possibly multiple) reads, until a reply
|
||||
matching the called feature is received. In theory the UR will always reply
|
||||
to feature call; otherwise this function will wait indefinitely.
|
||||
|
||||
Incoming data packets not matching the feature and function will be
|
||||
delivered to the unhandled hook (if any), and ignored.
|
||||
|
||||
:param function: the function to call on that feature, may be an byte value
|
||||
or a bytes string of length 1.
|
||||
:param params: optional bytes string to send as function parameters to the
|
||||
feature; may also be an integer if the function only takes a single byte as
|
||||
parameter.
|
||||
|
||||
The optional ``features`` parameter is a cached result of the
|
||||
get_device_features function for this device, necessary to find the feature
|
||||
index. If the ``features_arrary`` is not provided, one will be obtained by
|
||||
manually calling get_device_features before making the request call proper.
|
||||
|
||||
:raises FeatureNotSupported: if the device does not support the feature.
|
||||
"""
|
||||
feature_index = None
|
||||
if feature == FEATURE.ROOT:
|
||||
feature_index = b'\x00'
|
||||
else:
|
||||
feature_index = _get_feature_index(handle, devnumber, feature, features)
|
||||
if feature_index is None:
|
||||
# i/o read error
|
||||
return None
|
||||
|
||||
feature_index = _pack('!B', feature_index)
|
||||
|
||||
if type(function) == int:
|
||||
function = _pack('!B', function)
|
||||
if type(params) == int:
|
||||
params = _pack('!B', params)
|
||||
|
||||
return _base.request(handle, devnumber, feature_index + function, params)
|
||||
|
||||
|
||||
def get_device(handle, devnumber, features=None):
|
||||
"""Gets the complete info for a device (type, features).
|
||||
|
||||
:returns: a PairedDevice or ``None``.
|
||||
"""
|
||||
if _base.ping(handle, devnumber):
|
||||
devinfo = PairedDevice(handle, devnumber)
|
||||
# _log.debug("found device %s", devinfo)
|
||||
return devinfo
|
||||
|
||||
|
||||
def get_feature_index(handle, devnumber, feature):
|
||||
"""Reads the index of a device's feature.
|
||||
|
||||
:returns: An int, or ``None`` if the feature is not available.
|
||||
"""
|
||||
# _log.debug("device %d get feature index <%s:%s>", devnumber, _hex(feature), FEATURE_NAME[feature])
|
||||
if len(feature) != 2:
|
||||
raise ValueError("invalid feature <%s>: it must be a two-byte string" % feature)
|
||||
|
||||
# FEATURE.ROOT should always be available for any attached devices
|
||||
reply = _base.request(handle, devnumber, FEATURE.ROOT, feature)
|
||||
if reply:
|
||||
feature_index = ord(reply[0:1])
|
||||
if feature_index:
|
||||
# feature_flags = ord(reply[1:2]) & 0xE0
|
||||
# if feature_flags:
|
||||
# _log.debug("device %d feature <%s:%s> has index %d: %s",
|
||||
# devnumber, _hex(feature), FEATURE_NAME[feature], feature_index,
|
||||
# ','.join([FEATURE_FLAGS[k] for k in FEATURE_FLAGS if feature_flags & k]))
|
||||
# else:
|
||||
# _log.debug("device %d feature <%s:%s> has index %d", devnumber, _hex(feature), FEATURE_NAME[feature], feature_index)
|
||||
|
||||
# only consider active and supported features?
|
||||
# if feature_flags:
|
||||
# raise E.FeatureNotSupported(devnumber, feature)
|
||||
|
||||
return feature_index
|
||||
|
||||
_log.warn("device %d feature <%s:%s> not supported by the device", devnumber, _hex(feature), FEATURE_NAME[feature])
|
||||
raise _FeatureNotSupported(devnumber, feature)
|
||||
|
||||
|
||||
def _get_feature_index(handle, devnumber, feature, features=None):
|
||||
if features is None:
|
||||
return get_feature_index(handle, devnumber, feature)
|
||||
|
||||
if feature in features:
|
||||
return features.index(feature)
|
||||
|
||||
index = get_feature_index(handle, devnumber, feature)
|
||||
if index is not None:
|
||||
if len(features) <= index:
|
||||
features += [None] * (index + 1 - len(features))
|
||||
features[index] = feature
|
||||
# _log.debug("%s: found feature %s at %d", features, _base._hex(feature), index)
|
||||
return index
|
||||
|
||||
|
||||
def get_device_features(handle, devnumber):
|
||||
"""Returns an array of feature ids.
|
||||
|
||||
Their position in the array is the index to be used when requesting that
|
||||
feature on the device.
|
||||
"""
|
||||
# _log.debug("device %d get device features", devnumber)
|
||||
|
||||
# get the index of the FEATURE_SET
|
||||
# FEATURE.ROOT should always be available for all devices
|
||||
fs_index = _base.request(handle, devnumber, FEATURE.ROOT, FEATURE.FEATURE_SET)
|
||||
if fs_index is None:
|
||||
_log.warn("device %d FEATURE_SET not available", devnumber)
|
||||
return None
|
||||
fs_index = fs_index[:1]
|
||||
|
||||
# For debugging purposes, query all the available features on the device,
|
||||
# even if unknown.
|
||||
|
||||
# get the number of active features the device has
|
||||
features_count = _base.request(handle, devnumber, fs_index + b'\x00')
|
||||
if not features_count:
|
||||
# this can happen if the device disappeard since the fs_index request
|
||||
# otherwise we should get at least a count of 1 (the FEATURE_SET we've just used above)
|
||||
_log.debug("device %d no features available?!", devnumber)
|
||||
return None
|
||||
|
||||
features_count = ord(features_count[:1])
|
||||
# _log.debug("device %d found %d features", devnumber, features_count)
|
||||
|
||||
features = [None] * 0x20
|
||||
for index in range(1, 1 + features_count):
|
||||
# for each index, get the feature residing at that index
|
||||
feature = _base.request(handle, devnumber, fs_index + b'\x10', _pack('!B', index))
|
||||
if feature:
|
||||
# feature_flags = ord(feature[2:3]) & 0xE0
|
||||
feature = feature[0:2].upper()
|
||||
features[index] = feature
|
||||
|
||||
# if feature_flags:
|
||||
# _log.debug("device %d feature <%s:%s> at index %d: %s",
|
||||
# devnumber, _hex(feature), FEATURE_NAME[feature], index,
|
||||
# ','.join([FEATURE_FLAGS[k] for k in FEATURE_FLAGS if feature_flags & k]))
|
||||
# else:
|
||||
# _log.debug("device %d feature <%s:%s> at index %d", devnumber, _hex(feature), FEATURE_NAME[feature], index)
|
||||
|
||||
features[0] = FEATURE.ROOT
|
||||
while features[-1] is None:
|
||||
del features[-1]
|
||||
return tuple(features)
|
||||
|
||||
|
||||
def get_device_firmware(handle, devnumber, features=None):
|
||||
"""Reads a device's firmware info.
|
||||
|
||||
:returns: a list of FirmwareInfo tuples, ordered by firmware layer.
|
||||
"""
|
||||
fw_fi = _get_feature_index(handle, devnumber, FEATURE.FIRMWARE, features)
|
||||
if fw_fi is None:
|
||||
return None
|
||||
|
||||
fw_count = _base.request(handle, devnumber, _pack('!BB', fw_fi, 0x00))
|
||||
if fw_count:
|
||||
fw_count = ord(fw_count[:1])
|
||||
|
||||
fw = []
|
||||
for index in range(0, fw_count):
|
||||
fw_info = _base.request(handle, devnumber, _pack('!BB', fw_fi, 0x10), params=index)
|
||||
if fw_info:
|
||||
level = ord(fw_info[:1]) & 0x0F
|
||||
if level == 0 or level == 1:
|
||||
kind = FIRMWARE_KIND[level]
|
||||
name, = _unpack('!3s', fw_info[1:4])
|
||||
name = name.decode('ascii')
|
||||
version = _hex(fw_info[4:6])
|
||||
version = '%s.%s' % (version[0:2], version[2:4])
|
||||
build, = _unpack('!H', fw_info[6:8])
|
||||
if build:
|
||||
version += ' b%d' % build
|
||||
extras = fw_info[9:].rstrip(b'\x00') or None
|
||||
fw_info = _FirmwareInfo(level, kind, name, version, extras)
|
||||
elif level == 2:
|
||||
fw_info = _FirmwareInfo(2, FIRMWARE_KIND[2], '', ord(fw_info[1:2]), None)
|
||||
else:
|
||||
fw_info = _FirmwareInfo(level, FIRMWARE_KIND[-1], '', '', None)
|
||||
|
||||
fw.append(fw_info)
|
||||
# _log.debug("device %d firmware %s", devnumber, fw_info)
|
||||
return tuple(fw)
|
||||
|
||||
|
||||
def get_device_kind(handle, devnumber, features=None):
|
||||
"""Reads a device's type.
|
||||
|
||||
:see DEVICE_KIND:
|
||||
:returns: a string describing the device type, or ``None`` if the device is
|
||||
not available or does not support the ``NAME`` feature.
|
||||
"""
|
||||
name_fi = _get_feature_index(handle, devnumber, FEATURE.NAME, features)
|
||||
if name_fi is None:
|
||||
return None
|
||||
|
||||
d_kind = _base.request(handle, devnumber, _pack('!BB', name_fi, 0x20))
|
||||
if d_kind:
|
||||
d_kind = ord(d_kind[:1])
|
||||
# _log.debug("device %d type %d = %s", devnumber, d_kind, DEVICE_KIND[d_kind])
|
||||
return DEVICE_KIND[d_kind]
|
||||
|
||||
|
||||
def get_device_name(handle, devnumber, features=None):
|
||||
"""Reads a device's name.
|
||||
|
||||
:returns: a string with the device name, or ``None`` if the device is not
|
||||
available or does not support the ``NAME`` feature.
|
||||
"""
|
||||
name_fi = _get_feature_index(handle, devnumber, FEATURE.NAME, features)
|
||||
if name_fi is None:
|
||||
return None
|
||||
|
||||
name_length = _base.request(handle, devnumber, _pack('!BB', name_fi, 0x00))
|
||||
if name_length:
|
||||
name_length = ord(name_length[:1])
|
||||
|
||||
d_name = b''
|
||||
while len(d_name) < name_length:
|
||||
name_fragment = _base.request(handle, devnumber, _pack('!BB', name_fi, 0x10), len(d_name))
|
||||
if name_fragment:
|
||||
name_fragment = name_fragment[:name_length - len(d_name)]
|
||||
d_name += name_fragment
|
||||
else:
|
||||
break
|
||||
|
||||
d_name = d_name.decode('ascii')
|
||||
# _log.debug("device %d name %s", devnumber, d_name)
|
||||
return d_name
|
||||
|
||||
|
||||
def get_device_battery_level(handle, devnumber, features=None):
|
||||
"""Reads a device's battery level.
|
||||
|
||||
:raises FeatureNotSupported: if the device does not support this feature.
|
||||
"""
|
||||
bat_fi = _get_feature_index(handle, devnumber, FEATURE.BATTERY, features)
|
||||
if bat_fi is not None:
|
||||
battery = _base.request(handle, devnumber, _pack('!BB', bat_fi, 0))
|
||||
if battery:
|
||||
discharge, dischargeNext, status = _unpack('!BBB', battery[:3])
|
||||
_log.debug("device %d battery %d%% charged, next level %d%% charge, status %d = %s",
|
||||
devnumber, discharge, dischargeNext, status, BATTERY_STATUS[status])
|
||||
return (discharge, dischargeNext, BATTERY_STATUS[status])
|
||||
|
||||
|
||||
def get_device_keys(handle, devnumber, features=None):
|
||||
rk_fi = _get_feature_index(handle, devnumber, FEATURE.REPROGRAMMABLE_KEYS, features)
|
||||
if rk_fi is None:
|
||||
return None
|
||||
|
||||
count = _base.request(handle, devnumber, _pack('!BB', rk_fi, 0))
|
||||
if count:
|
||||
keys = []
|
||||
|
||||
count = ord(count[:1])
|
||||
for index in range(0, count):
|
||||
keydata = _base.request(handle, devnumber, _pack('!BB', rk_fi, 0x10), index)
|
||||
if keydata:
|
||||
key, key_task, flags = _unpack('!HHB', keydata[:5])
|
||||
rki = _ReprogrammableKeyInfo(index, key, KEY_NAME[key], key_task, KEY_NAME[key_task], flags)
|
||||
keys.append(rki)
|
||||
|
||||
return keys
|
||||
@@ -1,348 +0,0 @@
|
||||
#
|
||||
# Base low-level functions used by the API proper.
|
||||
# Unlikely to be used directly unless you're expanding the API.
|
||||
#
|
||||
|
||||
from struct import pack as _pack
|
||||
from struct import unpack as _unpack
|
||||
from binascii import hexlify as _hexlify
|
||||
_hex = lambda d: _hexlify(d).decode('ascii').upper()
|
||||
|
||||
from .constants import ERROR_NAME
|
||||
from .exceptions import (NoReceiver as _NoReceiver,
|
||||
FeatureCallError as _FeatureCallError)
|
||||
|
||||
from logging import getLogger
|
||||
_log = getLogger('LUR').getChild('base')
|
||||
del getLogger
|
||||
|
||||
import hidapi as _hid
|
||||
|
||||
|
||||
#
|
||||
# These values are defined by the Logitech documentation.
|
||||
# Overstepping these boundaries will only produce log warnings.
|
||||
#
|
||||
|
||||
"""Minimim lenght of a feature call packet."""
|
||||
_MIN_CALL_SIZE = 7
|
||||
|
||||
|
||||
"""Maximum lenght of a feature call packet."""
|
||||
_MAX_CALL_SIZE = 20
|
||||
|
||||
|
||||
"""Minimum size of a feature reply packet."""
|
||||
_MIN_REPLY_SIZE = _MIN_CALL_SIZE
|
||||
|
||||
|
||||
"""Maximum size of a feature reply packet."""
|
||||
_MAX_REPLY_SIZE = _MAX_CALL_SIZE
|
||||
|
||||
|
||||
"""Default timeout on read (in ms)."""
|
||||
DEFAULT_TIMEOUT = 1500
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def _logdebug_hook(reply_code, devnumber, data):
|
||||
"""Default unhandled hook, logs the reply as DEBUG."""
|
||||
_log.warn("UNHANDLED [%02X %02X %s %s] (%s)", reply_code, devnumber, _hex(data[:2]), _hex(data[2:]), repr(data))
|
||||
|
||||
|
||||
"""The function that will be called on unhandled incoming events.
|
||||
|
||||
The hook must be a function with the signature: ``_(int, int, str)``, where
|
||||
the parameters are: (reply_code, devnumber, data).
|
||||
|
||||
This hook will only be called by the request() function, when it receives
|
||||
replies that do not match the requested feature call. As such, it is not
|
||||
suitable for intercepting broadcast events from the device (e.g. special
|
||||
keys being pressed, battery charge events, etc), at least not in a timely
|
||||
manner. However, these events *may* be delivered here if they happen while
|
||||
doing a feature call to the device.
|
||||
|
||||
The default implementation logs the unhandled reply as DEBUG.
|
||||
"""
|
||||
unhandled_hook = _logdebug_hook
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def list_receiver_devices():
|
||||
"""List all the Linux devices exposed by the UR attached to the machine."""
|
||||
# (Vendor ID, Product ID) = ('Logitech', 'Unifying Receiver')
|
||||
# interface 2 if the actual receiver interface
|
||||
for d in _hid.enumerate(0x046d, 0xc52b, 2):
|
||||
if d.driver is None or d.driver == 'logitech-djreceiver':
|
||||
yield d
|
||||
|
||||
|
||||
_COUNT_DEVICES_REQUEST = b'\x10\xFF\x81\x00\x00\x00\x00'
|
||||
|
||||
def try_open(path):
|
||||
"""Checks if the given Linux device path points to the right UR device.
|
||||
|
||||
:param path: the Linux device path.
|
||||
|
||||
The UR physical device may expose multiple linux devices with the same
|
||||
interface, so we have to check for the right one. At this moment the only
|
||||
way to distinguish betheen them is to do a test ping on an invalid
|
||||
(attached) device number (i.e., 0), expecting a 'ping failed' reply.
|
||||
|
||||
:returns: an open receiver handle if this is the right Linux device, or
|
||||
``None``.
|
||||
"""
|
||||
receiver_handle = _hid.open_path(path)
|
||||
if receiver_handle is None:
|
||||
# could be a file permissions issue (did you add the udev rules?)
|
||||
# in any case, unreachable
|
||||
_log.debug("[%s] open failed", path)
|
||||
return None
|
||||
|
||||
_hid.write(receiver_handle, _COUNT_DEVICES_REQUEST)
|
||||
|
||||
# if this is the right hidraw device, we'll receive a 'bad device' from the UR
|
||||
# otherwise, the read should produce nothing
|
||||
reply = _hid.read(receiver_handle, _MAX_REPLY_SIZE, DEFAULT_TIMEOUT / 2)
|
||||
if reply:
|
||||
if reply[:5] == _COUNT_DEVICES_REQUEST[:5]:
|
||||
# 'device 0 unreachable' is the expected reply from a valid receiver handle
|
||||
_log.info("[%s] success: handle %X", path, receiver_handle)
|
||||
return receiver_handle
|
||||
_log.debug("[%s] %X ignored reply %s", path, receiver_handle, _hex(reply))
|
||||
else:
|
||||
_log.debug("[%s] %X no reply", path, receiver_handle)
|
||||
|
||||
close(receiver_handle)
|
||||
|
||||
|
||||
def open():
|
||||
"""Opens the first Logitech Unifying Receiver found attached to the machine.
|
||||
|
||||
:returns: An open file handle for the found receiver, or ``None``.
|
||||
"""
|
||||
for rawdevice in list_receiver_devices():
|
||||
_log.info("checking %s", rawdevice)
|
||||
handle = try_open(rawdevice.path)
|
||||
if handle:
|
||||
return handle
|
||||
|
||||
|
||||
def close(handle):
|
||||
"""Closes a HID device handle."""
|
||||
if handle:
|
||||
try:
|
||||
_hid.close(handle)
|
||||
# _log.info("closed receiver handle %X", handle)
|
||||
return True
|
||||
except:
|
||||
_log.exception("closing receiver handle %X", handle)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def write(handle, devnumber, data):
|
||||
"""Writes some data to a certain device.
|
||||
|
||||
:param handle: an open UR handle.
|
||||
:param devnumber: attached device number.
|
||||
:param data: data to send, up to 5 bytes.
|
||||
|
||||
The first two (required) bytes of data must be the feature index for the
|
||||
device, and a function code for that feature.
|
||||
|
||||
:raises NoReceiver: if the receiver is no longer available, i.e. has
|
||||
been physically removed from the machine, or the kernel driver has been
|
||||
unloaded. The handle will be closed automatically.
|
||||
"""
|
||||
assert _MIN_CALL_SIZE == 7
|
||||
assert _MAX_CALL_SIZE == 20
|
||||
# the data is padded to either 5 or 18 bytes
|
||||
wdata = _pack('!BB18s' if len(data) > 5 else '!BB5s', 0x10, devnumber, data)
|
||||
_log.debug("<= w[10 %02X %s %s]", devnumber, _hex(wdata[2:4]), _hex(wdata[4:]))
|
||||
if not _hid.write(handle, wdata):
|
||||
_log.warn("write failed, assuming receiver %X no longer available", handle)
|
||||
close(handle)
|
||||
raise _NoReceiver
|
||||
|
||||
|
||||
def read(handle, timeout=DEFAULT_TIMEOUT):
|
||||
"""Read some data from the receiver. Usually called after a write (feature
|
||||
call), to get the reply.
|
||||
|
||||
:param handle: an open UR handle.
|
||||
:param timeout: read timeout on the UR handle.
|
||||
|
||||
If any data was read in the given timeout, returns a tuple of
|
||||
(reply_code, devnumber, message data). The reply code is generally ``0x11``
|
||||
for a successful feature call, or ``0x10`` to indicate some error, e.g. the
|
||||
device is no longer available.
|
||||
|
||||
:raises NoReceiver: if the receiver is no longer available, i.e. has
|
||||
been physically removed from the machine, or the kernel driver has been
|
||||
unloaded. The handle will be closed automatically.
|
||||
"""
|
||||
data = _hid.read(handle, _MAX_REPLY_SIZE, timeout)
|
||||
if data is None:
|
||||
_log.warn("read failed, assuming receiver %X no longer available", handle)
|
||||
close(handle)
|
||||
raise _NoReceiver
|
||||
|
||||
if data:
|
||||
if len(data) < _MIN_REPLY_SIZE:
|
||||
_log.warn("=> r[%s] read packet too short: %d bytes", _hex(data), len(data))
|
||||
data += b'\x00' * (_MIN_REPLY_SIZE - len(data))
|
||||
if len(data) > _MAX_REPLY_SIZE:
|
||||
_log.warn("=> r[%s] read packet too long: %d bytes", _hex(data), len(data))
|
||||
code = ord(data[:1])
|
||||
devnumber = ord(data[1:2])
|
||||
_log.debug("=> r[%02X %02X %s %s]", code, devnumber, _hex(data[2:4]), _hex(data[4:]))
|
||||
return code, devnumber, data[2:]
|
||||
|
||||
# _l.log(_LOG_LEVEL, "(-) => r[]")
|
||||
|
||||
|
||||
_MAX_READ_TIMES = 3
|
||||
request_context = None
|
||||
from collections import namedtuple
|
||||
_DEFAULT_REQUEST_CONTEXT_CLASS = namedtuple('_DEFAULT_REQUEST_CONTEXT_CLASS', ['write', 'read', 'unhandled_hook'])
|
||||
_DEFAULT_REQUEST_CONTEXT = _DEFAULT_REQUEST_CONTEXT_CLASS(write=write, read=read, unhandled_hook=unhandled_hook)
|
||||
del namedtuple
|
||||
|
||||
def request(handle, devnumber, feature_index_function, params=b'', features=None):
|
||||
"""Makes a feature call to a device and waits for a matching reply.
|
||||
|
||||
This function will skip all incoming messages and events not related to the
|
||||
device we're requesting for, or the feature specified in the initial
|
||||
request; it will also wait for a matching reply indefinitely.
|
||||
|
||||
:param handle: an open UR handle.
|
||||
:param devnumber: attached device number.
|
||||
:param feature_index_function: a two-byte string of (feature_index, feature_function).
|
||||
:param params: parameters for the feature call, 3 to 16 bytes.
|
||||
:param features: optional features array for the device, only used to fill
|
||||
the FeatureCallError exception if one occurs.
|
||||
:returns: the reply data packet, or ``None`` if the device is no longer
|
||||
available.
|
||||
:raisees FeatureCallError: if the feature call replied with an error.
|
||||
"""
|
||||
if type(params) == int:
|
||||
params = _pack('!B', params)
|
||||
|
||||
# _log.debug("device %d request {%s} params [%s]", devnumber, _hex(feature_index_function), _hex(params))
|
||||
if len(feature_index_function) != 2:
|
||||
raise ValueError('invalid feature_index_function {%s}: it must be a two-byte string' % _hex(feature_index_function))
|
||||
|
||||
if request_context is None or handle != request_context.handle:
|
||||
context = _DEFAULT_REQUEST_CONTEXT
|
||||
_unhandled = unhandled_hook
|
||||
else:
|
||||
context = request_context
|
||||
_unhandled = getattr(context, 'unhandled_hook')
|
||||
|
||||
context.write(handle, devnumber, feature_index_function + params)
|
||||
|
||||
read_times = _MAX_READ_TIMES
|
||||
while read_times > 0:
|
||||
divisor = (1 + _MAX_READ_TIMES - read_times)
|
||||
reply = context.read(handle, int(DEFAULT_TIMEOUT * (divisor + 1) / 2 / divisor))
|
||||
read_times -= 1
|
||||
|
||||
if not reply:
|
||||
# keep waiting...
|
||||
continue
|
||||
|
||||
reply_code, reply_devnumber, reply_data = reply
|
||||
|
||||
if reply_devnumber != devnumber:
|
||||
# this message not for the device we're interested in
|
||||
# _l.log(_LOG_LEVEL, "device %d request got reply for unexpected device %d: [%s]", devnumber, reply_devnumber, _hex(reply_data))
|
||||
# worst case scenario, this is a reply for a concurrent request
|
||||
# on this receiver
|
||||
if _unhandled:
|
||||
_unhandled(reply_code, reply_devnumber, reply_data)
|
||||
continue
|
||||
|
||||
if reply_code == 0x10 and reply_data[:1] == b'\x8F' and reply_data[1:3] == feature_index_function:
|
||||
# device not present
|
||||
_log.debug("device %d request failed on {%s} call: [%s]", devnumber, _hex(feature_index_function), _hex(reply_data))
|
||||
return None
|
||||
|
||||
if reply_code == 0x10 and reply_data[:1] == b'\x8F':
|
||||
# device not present
|
||||
_log.debug("device %d request failed: [%s]", devnumber, _hex(reply_data))
|
||||
return None
|
||||
|
||||
if reply_code == 0x11 and reply_data[0] == b'\xFF' and reply_data[1:3] == feature_index_function:
|
||||
# the feature call returned with an error
|
||||
error_code = ord(reply_data[3])
|
||||
_log.warn("device %d request feature call error %d = %s: %s", devnumber, error_code, ERROR_NAME[error_code], _hex(reply_data))
|
||||
feature_index = ord(feature_index_function[:1])
|
||||
feature_function = feature_index_function[1:2]
|
||||
feature = None if features is None else features[feature_index] if feature_index < len(features) else None
|
||||
raise _FeatureCallError(devnumber, feature, feature_index, feature_function, error_code, reply_data)
|
||||
|
||||
if reply_code == 0x11 and reply_data[:2] == feature_index_function:
|
||||
# a matching reply
|
||||
# _log.debug("device %d matched reply with feature-index-function [%s]", devnumber, _hex(reply_data[2:]))
|
||||
return reply_data[2:]
|
||||
|
||||
if reply_code == 0x10 and devnumber == 0xFF and reply_data[:2] == feature_index_function:
|
||||
# direct calls to the receiver (device 0xFF) may also return successfully with reply code 0x10
|
||||
# _log.debug("device %d matched reply with feature-index-function [%s]", devnumber, _hex(reply_data[2:]))
|
||||
return reply_data[2:]
|
||||
|
||||
# _log.debug("device %d unmatched reply {%s} (expected {%s})", devnumber, _hex(reply_data[:2]), _hex(feature_index_function))
|
||||
if _unhandled:
|
||||
_unhandled(reply_code, reply_devnumber, reply_data)
|
||||
|
||||
|
||||
def ping(handle, devnumber):
|
||||
"""Check if a device is connected to the UR.
|
||||
|
||||
:returns: The HID protocol supported by the device, as a floating point number, if the device is active.
|
||||
"""
|
||||
if request_context is None or handle != request_context.handle:
|
||||
context = _DEFAULT_REQUEST_CONTEXT
|
||||
_unhandled = unhandled_hook
|
||||
else:
|
||||
context = request_context
|
||||
_unhandled = getattr(context, 'unhandled_hook')
|
||||
|
||||
context.write(handle, devnumber, b'\x00\x10\x00\x00\xAA')
|
||||
read_times = _MAX_READ_TIMES
|
||||
while read_times > 0:
|
||||
divisor = (1 + _MAX_READ_TIMES - read_times)
|
||||
reply = context.read(handle, int(DEFAULT_TIMEOUT * (divisor + 1) / 2 / divisor))
|
||||
read_times -= 1
|
||||
|
||||
if not reply:
|
||||
# keep waiting...
|
||||
continue
|
||||
|
||||
reply_code, reply_devnumber, reply_data = reply
|
||||
|
||||
if reply_devnumber != devnumber:
|
||||
# this message not for the device we're interested in
|
||||
# _l.log(_LOG_LEVEL, "device %d request got reply for unexpected device %d: [%s]", devnumber, reply_devnumber, _hex(reply_data))
|
||||
# worst case scenario, this is a reply for a concurrent request
|
||||
# on this receiver
|
||||
if _unhandled:
|
||||
_unhandled(reply_code, reply_devnumber, reply_data)
|
||||
continue
|
||||
|
||||
if reply_code == 0x11 and reply_data[:2] == b'\x00\x10' and reply_data[4:5] == b'\xAA':
|
||||
major, minor = _unpack('!BB', reply_data[2:4])
|
||||
return major + minor / 10.0
|
||||
|
||||
if reply_code == 0x10 and reply_data == b'\x8F\x00\x10\x01\x00':
|
||||
return 1.0
|
||||
|
||||
if reply_code == 0x10 and reply_data[:3] == b'\x8F\x00\x10':
|
||||
return None
|
||||
|
||||
_log.warn("don't know how to interpret ping reply %s", reply)
|
||||
@@ -1,48 +0,0 @@
|
||||
#
|
||||
# Some common functions and types.
|
||||
#
|
||||
|
||||
from collections import namedtuple
|
||||
from binascii import hexlify as _hexlify
|
||||
_hex = lambda d: _hexlify(d).decode('ascii').upper()
|
||||
|
||||
|
||||
class FallbackDict(dict):
|
||||
def __init__(self, fallback_function=lambda x: None, *args, **kwargs):
|
||||
super(FallbackDict, self).__init__(*args, **kwargs)
|
||||
self.fallback = fallback_function
|
||||
|
||||
def __getitem__(self, key):
|
||||
try:
|
||||
return super(FallbackDict, self).__getitem__(key)
|
||||
except KeyError:
|
||||
return self.fallback(key)
|
||||
|
||||
|
||||
def list2dict(values_list):
|
||||
return dict(zip(range(0, len(values_list)), values_list))
|
||||
|
||||
|
||||
"""Firmware information."""
|
||||
FirmwareInfo = namedtuple('FirmwareInfo', [
|
||||
'level',
|
||||
'kind',
|
||||
'name',
|
||||
'version',
|
||||
'extras'])
|
||||
|
||||
"""Reprogrammable keys informations."""
|
||||
ReprogrammableKeyInfo = namedtuple('ReprogrammableKeyInfo', [
|
||||
'index',
|
||||
'id',
|
||||
'name',
|
||||
'task',
|
||||
'task_name',
|
||||
'flags'])
|
||||
|
||||
|
||||
class Packet(namedtuple('Packet', ['code', 'devnumber', 'data'])):
|
||||
def __str__(self):
|
||||
return 'Packet(%02X,%02X,%s)' % (self.code, self.devnumber, 'None' if self.data is None else _hex(self.data))
|
||||
|
||||
del namedtuple
|
||||
@@ -1,109 +0,0 @@
|
||||
#
|
||||
# Constants used by the rest of the API.
|
||||
#
|
||||
|
||||
from struct import pack as _pack
|
||||
from binascii import hexlify as _hexlify
|
||||
_hex = lambda d: _hexlify(d).decode('ascii').upper()
|
||||
|
||||
from .common import (FallbackDict, list2dict)
|
||||
|
||||
|
||||
"""Possible features available on a Logitech device.
|
||||
|
||||
A particular device might not support all these features, and may support other
|
||||
unknown features as well.
|
||||
"""
|
||||
FEATURE = type('FEATURE', (),
|
||||
dict(
|
||||
ROOT=b'\x00\x00',
|
||||
FEATURE_SET=b'\x00\x01',
|
||||
FIRMWARE=b'\x00\x03',
|
||||
NAME=b'\x00\x05',
|
||||
BATTERY=b'\x10\x00',
|
||||
REPROGRAMMABLE_KEYS=b'\x1B\x00',
|
||||
WIRELESS=b'\x1D\x4B',
|
||||
SOLAR_CHARGE=b'\x43\x01',
|
||||
))
|
||||
|
||||
def _feature_name(key):
|
||||
if key is None:
|
||||
return None
|
||||
if type(key) == int:
|
||||
return FEATURE_NAME[_pack('!H', key)]
|
||||
return 'UNKNOWN_' + _hex(key)
|
||||
|
||||
|
||||
"""Feature names indexed by feature id."""
|
||||
FEATURE_NAME = FallbackDict(_feature_name)
|
||||
FEATURE_NAME[FEATURE.ROOT] = 'ROOT'
|
||||
FEATURE_NAME[FEATURE.FEATURE_SET] = 'FEATURE_SET'
|
||||
FEATURE_NAME[FEATURE.FIRMWARE] = 'FIRMWARE'
|
||||
FEATURE_NAME[FEATURE.NAME] = 'NAME'
|
||||
FEATURE_NAME[FEATURE.BATTERY] = 'BATTERY'
|
||||
FEATURE_NAME[FEATURE.REPROGRAMMABLE_KEYS] = 'REPROGRAMMABLE_KEYS'
|
||||
FEATURE_NAME[FEATURE.WIRELESS] = 'WIRELESS'
|
||||
FEATURE_NAME[FEATURE.SOLAR_CHARGE] = 'SOLAR_CHARGE'
|
||||
|
||||
|
||||
FEATURE_FLAGS = { 0x20: 'internal', 0x40: 'hidden', 0x80: 'obsolete' }
|
||||
|
||||
|
||||
_DEVICE_KINDS = ('keyboard', 'remote control', 'numpad', 'mouse',
|
||||
'touchpad', 'trackball', 'presenter', 'receiver')
|
||||
|
||||
"""Possible types of devices connected to an UR."""
|
||||
DEVICE_KIND = FallbackDict(lambda x: 'unknown', list2dict(_DEVICE_KINDS))
|
||||
|
||||
|
||||
_FIRMWARE_KINDS = ('Firmware', 'Bootloader', 'Hardware', 'Other')
|
||||
|
||||
"""Names of different firmware levels possible, indexed by level."""
|
||||
FIRMWARE_KIND = FallbackDict(lambda x: 'Unknown', list2dict(_FIRMWARE_KINDS))
|
||||
|
||||
|
||||
_BATTERY_STATUSES = ('Discharging (in use)', 'Recharging', 'Almost full',
|
||||
'Full', 'Slow recharge', 'Invalid battery', 'Thermal error',
|
||||
'Charging error')
|
||||
|
||||
"""Names for possible battery status values."""
|
||||
BATTERY_STATUS = FallbackDict(lambda x: 'unknown', list2dict(_BATTERY_STATUSES))
|
||||
|
||||
_KEY_NAMES = ( 'unknown_0000', 'Volume up', 'Volume down', 'Mute', 'Play/Pause',
|
||||
'Next', 'Previous', 'Stop', 'Application switcher',
|
||||
'unknown_0009', 'Calculator', 'unknown_000B', 'unknown_000C',
|
||||
'unknown_000D', 'Mail')
|
||||
|
||||
"""Standard names for reprogrammable keys."""
|
||||
KEY_NAME = FallbackDict(lambda x: 'unknown_%04X' % x, list2dict(_KEY_NAMES))
|
||||
|
||||
"""Possible flags on a reprogrammable key."""
|
||||
KEY_FLAG = type('KEY_FLAG', (), dict(
|
||||
REPROGRAMMABLE=0x10,
|
||||
FN_SENSITIVE=0x08,
|
||||
NONSTANDARD=0x04,
|
||||
IS_FN=0x02,
|
||||
MSE=0x01,
|
||||
))
|
||||
|
||||
KEY_FLAG_NAME = FallbackDict(lambda x: 'unknown')
|
||||
KEY_FLAG_NAME[KEY_FLAG.REPROGRAMMABLE] = 'reprogrammable'
|
||||
KEY_FLAG_NAME[KEY_FLAG.FN_SENSITIVE] = 'fn-sensitive'
|
||||
KEY_FLAG_NAME[KEY_FLAG.NONSTANDARD] = 'nonstandard'
|
||||
KEY_FLAG_NAME[KEY_FLAG.IS_FN] = 'is-fn'
|
||||
KEY_FLAG_NAME[KEY_FLAG.MSE] = 'mse'
|
||||
|
||||
_ERROR_NAMES = ('Ok', 'Unknown', 'Invalid argument', 'Out of range',
|
||||
'Hardware error', 'Logitech internal', 'Invalid feature index',
|
||||
'Invalid function', 'Busy', 'Unsupported')
|
||||
|
||||
"""Names for error codes."""
|
||||
ERROR_NAME = FallbackDict(lambda x: 'Unknown error', list2dict(_ERROR_NAMES))
|
||||
|
||||
|
||||
"""Maximum number of devices that can be attached to a single receiver."""
|
||||
MAX_ATTACHED_DEVICES = 6
|
||||
|
||||
|
||||
del FallbackDict
|
||||
del list2dict
|
||||
@@ -1,36 +0,0 @@
|
||||
#
|
||||
# Exceptions that may be raised by this API.
|
||||
#
|
||||
|
||||
from .constants import (FEATURE_NAME, ERROR_NAME)
|
||||
|
||||
|
||||
class NoReceiver(Exception):
|
||||
"""May be raised when trying to talk through a previously connected
|
||||
receiver that is no longer available. Should only happen if the receiver is
|
||||
physically disconnected from the machine, or its kernel driver module is
|
||||
unloaded."""
|
||||
pass
|
||||
|
||||
|
||||
class FeatureNotSupported(Exception):
|
||||
"""Raised when trying to request a feature not supported by the device."""
|
||||
def __init__(self, devnumber, feature):
|
||||
super(FeatureNotSupported, self).__init__(devnumber, feature, FEATURE_NAME[feature])
|
||||
self.devnumber = devnumber
|
||||
self.feature = feature
|
||||
self.feature_name = FEATURE_NAME[feature]
|
||||
|
||||
|
||||
class FeatureCallError(Exception):
|
||||
"""Raised if the device replied to a feature call with an error."""
|
||||
def __init__(self, devnumber, feature, feature_index, feature_function, error_code, data=None):
|
||||
super(FeatureCallError, self).__init__(devnumber, feature, feature_index, feature_function, error_code, ERROR_NAME[error_code])
|
||||
self.devnumber = devnumber
|
||||
self.feature = feature
|
||||
self.feature_name = None if feature is None else FEATURE_NAME[feature]
|
||||
self.feature_index = feature_index
|
||||
self.feature_function = feature_function
|
||||
self.error_code = error_code
|
||||
self.error_string = ERROR_NAME[error_code]
|
||||
self.data = data
|
||||
@@ -1,141 +0,0 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from threading import Thread as _Thread
|
||||
# from time import sleep as _sleep
|
||||
|
||||
from . import base as _base
|
||||
from .exceptions import NoReceiver as _NoReceiver
|
||||
from .common import Packet as _Packet
|
||||
|
||||
# for both Python 2 and 3
|
||||
try:
|
||||
from Queue import Queue as _Queue
|
||||
except ImportError:
|
||||
from queue import Queue as _Queue
|
||||
|
||||
|
||||
from logging import getLogger
|
||||
_log = getLogger('LUR').getChild('listener')
|
||||
del getLogger
|
||||
|
||||
|
||||
_READ_EVENT_TIMEOUT = int(_base.DEFAULT_TIMEOUT / 2) # ms
|
||||
|
||||
def _event_dispatch(listener, callback):
|
||||
while listener._active: # or not listener._events.empty():
|
||||
try:
|
||||
event = listener._events.get(True, _READ_EVENT_TIMEOUT * 10)
|
||||
except:
|
||||
continue
|
||||
# _log.debug("delivering event %s", event)
|
||||
try:
|
||||
callback(event)
|
||||
except:
|
||||
_log.exception("callback for %s", event)
|
||||
|
||||
|
||||
class EventsListener(_Thread):
|
||||
"""Listener thread for events from the Unifying Receiver.
|
||||
|
||||
Incoming packets will be passed to the callback function in sequence, by a
|
||||
separate thread.
|
||||
"""
|
||||
def __init__(self, receiver_handle, events_callback):
|
||||
super(EventsListener, self).__init__(group='Unifying Receiver', name=self.__class__.__name__)
|
||||
|
||||
self.daemon = True
|
||||
self._active = False
|
||||
|
||||
self._handle = receiver_handle
|
||||
|
||||
self._tasks = _Queue(1)
|
||||
self._backup_unhandled_hook = _base.unhandled_hook
|
||||
_base.unhandled_hook = self.unhandled_hook
|
||||
|
||||
self._events = _Queue(32)
|
||||
self._dispatcher = _Thread(group='Unifying Receiver',
|
||||
name=self.__class__.__name__ + '-dispatch',
|
||||
target=_event_dispatch, args=(self, events_callback))
|
||||
self._dispatcher.daemon = True
|
||||
|
||||
def run(self):
|
||||
self._active = True
|
||||
_log.debug("started")
|
||||
_base.request_context = self
|
||||
_base.unhandled_hook = self._backup_unhandled_hook
|
||||
del self._backup_unhandled_hook
|
||||
|
||||
self._dispatcher.start()
|
||||
|
||||
while self._active:
|
||||
try:
|
||||
# _log.debug("read next event")
|
||||
event = _base.read(self._handle, _READ_EVENT_TIMEOUT)
|
||||
except _NoReceiver:
|
||||
self._handle = 0
|
||||
_log.warn("receiver disconnected")
|
||||
self._events.put(_Packet(0xFF, 0xFF, None))
|
||||
self._active = False
|
||||
else:
|
||||
if event is not None:
|
||||
matched = False
|
||||
task = None if self._tasks.empty() else self._tasks.queue[0]
|
||||
if task and task[-1] is None:
|
||||
task_dev, task_data = task[:2]
|
||||
if event[1] == task_dev:
|
||||
# _log.debug("matching %s to (%d, %s)", event, task_dev, repr(task_data))
|
||||
matched = event[2][:2] == task_data[:2] or (event[2][:1] in b'\x8F\xFF' and event[2][1:3] == task_data[:2])
|
||||
|
||||
if matched:
|
||||
# _log.debug("request reply %s", event)
|
||||
task[-1] = event
|
||||
self._tasks.task_done()
|
||||
else:
|
||||
event = _Packet(*event)
|
||||
_log.info("queueing event %s", event)
|
||||
self._events.put(event)
|
||||
|
||||
_base.request_context = None
|
||||
handle, self._handle = self._handle, 0
|
||||
_base.close(handle)
|
||||
_log.debug("stopped")
|
||||
|
||||
def stop(self):
|
||||
"""Tells the listener to stop as soon as possible."""
|
||||
if self._active:
|
||||
_log.debug("stopping")
|
||||
self._active = False
|
||||
# wait for the receiver handle to be closed
|
||||
self.join()
|
||||
|
||||
@property
|
||||
def handle(self):
|
||||
return self._handle
|
||||
|
||||
def write(self, handle, devnumber, data):
|
||||
assert handle == self._handle
|
||||
# _log.debug("write %02X %s", devnumber, _base._hex(data))
|
||||
task = [devnumber, data, None]
|
||||
self._tasks.put(task)
|
||||
_base.write(self._handle, devnumber, data)
|
||||
# _log.debug("task queued %s", task)
|
||||
|
||||
def read(self, handle, timeout=_base.DEFAULT_TIMEOUT):
|
||||
assert handle == self._handle
|
||||
# _log.debug("read %d", timeout)
|
||||
assert not self._tasks.empty()
|
||||
self._tasks.join()
|
||||
task = self._tasks.get(False)
|
||||
# _log.debug("task ready %s", task)
|
||||
return task[-1]
|
||||
|
||||
def unhandled_hook(self, reply_code, devnumber, data):
|
||||
event = _Packet(reply_code, devnumber, data)
|
||||
_log.info("queueing unhandled event %s", event)
|
||||
self._events.put(event)
|
||||
|
||||
def __bool__(self):
|
||||
return bool(self._active and self._handle)
|
||||
__nonzero__ = __bool__
|
||||
@@ -1,3 +0,0 @@
|
||||
#
|
||||
# Tests for the logitech.unifying_receiver package.
|
||||
#
|
||||
@@ -1,17 +0,0 @@
|
||||
#
|
||||
# test loading the hidapi library
|
||||
#
|
||||
|
||||
import logging
|
||||
import unittest
|
||||
|
||||
|
||||
class Test_Import_HIDAPI(unittest.TestCase):
|
||||
def test_00_import_hidapi(self):
|
||||
import hidapi
|
||||
self.assertIsNotNone(hidapi)
|
||||
logging.info("hidapi loaded native implementation %s", hidapi.native_implementation)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -1,33 +0,0 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
import unittest
|
||||
import struct
|
||||
|
||||
from ..constants import *
|
||||
|
||||
|
||||
class Test_UR_Constants(unittest.TestCase):
|
||||
|
||||
def test_10_feature_names(self):
|
||||
for code in range(0x0000, 0x10000):
|
||||
feature = struct.pack('!H', code)
|
||||
name = FEATURE_NAME[feature]
|
||||
self.assertIsNotNone(name)
|
||||
self.assertEqual(FEATURE_NAME[code], name)
|
||||
if name.startswith('UNKNOWN_'):
|
||||
self.assertEqual(code, struct.unpack('!H', feature)[0])
|
||||
else:
|
||||
self.assertTrue(hasattr(FEATURE, name))
|
||||
self.assertEqual(feature, getattr(FEATURE, name))
|
||||
|
||||
def test_20_error_names(self):
|
||||
for code in range(0, len(ERROR_NAME)):
|
||||
name = ERROR_NAME[code]
|
||||
self.assertIsNotNone(name)
|
||||
# self.assertEqual(code, ERROR_NAME.index(name))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -1,187 +0,0 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
import unittest
|
||||
|
||||
from .. import base
|
||||
from ..exceptions import *
|
||||
from ..constants import *
|
||||
|
||||
|
||||
class Test_UR_Base(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.ur_available = False
|
||||
cls.handle = None
|
||||
cls.device = None
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
if cls.handle:
|
||||
base.close(cls.handle)
|
||||
cls.ur_available = False
|
||||
cls.handle = None
|
||||
cls.device = None
|
||||
|
||||
def test_10_list_receiver_devices(self):
|
||||
rawdevices = base.list_receiver_devices()
|
||||
self.assertIsNotNone(rawdevices, "list_receiver_devices returned None")
|
||||
# self.assertIsInstance(rawdevices, Iterable, "list_receiver_devices should have returned an iterable")
|
||||
Test_UR_Base.ur_available = len(list(rawdevices)) > 0
|
||||
|
||||
def test_20_try_open(self):
|
||||
if not self.ur_available:
|
||||
self.fail("No receiver found")
|
||||
|
||||
for rawdevice in base.list_receiver_devices():
|
||||
handle = base.try_open(rawdevice.path)
|
||||
if handle is None:
|
||||
continue
|
||||
|
||||
self.assertIsInstance(handle, int, "try_open should have returned an int")
|
||||
|
||||
if Test_UR_Base.handle is None:
|
||||
Test_UR_Base.handle = handle
|
||||
else:
|
||||
base.close(handle)
|
||||
base.close(Test_UR_Base.handle)
|
||||
Test_UR_Base.handle = None
|
||||
self.fail("try_open found multiple valid receiver handles")
|
||||
|
||||
self.assertIsNotNone(self.handle, "no valid receiver handles found")
|
||||
|
||||
def test_25_ping_device_zero(self):
|
||||
if self.handle is None:
|
||||
self.fail("No receiver found")
|
||||
|
||||
w = base.write(self.handle, 0, b'\x00\x10\x00\x00\xAA')
|
||||
self.assertIsNone(w, "write should have returned None")
|
||||
reply = base.read(self.handle, base.DEFAULT_TIMEOUT * 3)
|
||||
self.assertIsNotNone(reply, "None reply for ping")
|
||||
self.assertIsInstance(reply, tuple, "read should have returned a tuple")
|
||||
|
||||
reply_code, reply_device, reply_data = reply
|
||||
self.assertEqual(reply_device, 0, "got ping reply for valid device")
|
||||
self.assertGreater(len(reply_data), 4, "ping reply has wrong length: %s" % base._hex(reply_data))
|
||||
if reply_code == 0x10:
|
||||
# ping fail
|
||||
self.assertEqual(reply_data[:3], b'\x8F\x00\x10', "0x10 reply with unknown reply data: %s" % base._hex(reply_data))
|
||||
elif reply_code == 0x11:
|
||||
self.fail("Got valid ping from device 0")
|
||||
else:
|
||||
self.fail("ping got bad reply code: " + reply)
|
||||
|
||||
def test_30_ping_all_devices(self):
|
||||
if self.handle is None:
|
||||
self.fail("No receiver found")
|
||||
|
||||
devices = []
|
||||
|
||||
for device in range(1, 1 + MAX_ATTACHED_DEVICES):
|
||||
w = base.write(self.handle, device, b'\x00\x10\x00\x00\xAA')
|
||||
self.assertIsNone(w, "write should have returned None")
|
||||
reply = base.read(self.handle, base.DEFAULT_TIMEOUT * 3)
|
||||
self.assertIsNotNone(reply, "None reply for ping")
|
||||
self.assertIsInstance(reply, tuple, "read should have returned a tuple")
|
||||
|
||||
reply_code, reply_device, reply_data = reply
|
||||
self.assertEqual(reply_device, device, "ping reply for wrong device")
|
||||
self.assertGreater(len(reply_data), 4, "ping reply has wrong length: %s" % base._hex(reply_data))
|
||||
if reply_code == 0x10:
|
||||
# ping fail
|
||||
self.assertEqual(reply_data[:3], b'\x8F\x00\x10', "0x10 reply with unknown reply data: %s" % base._hex(reply_data))
|
||||
elif reply_code == 0x11:
|
||||
# ping ok
|
||||
self.assertEqual(reply_data[:2], b'\x00\x10', "0x11 reply with unknown reply data: %s" % base._hex(reply_data))
|
||||
self.assertEqual(reply_data[4:5], b'\xAA')
|
||||
devices.append(device)
|
||||
else:
|
||||
self.fail("ping got bad reply code: " + reply)
|
||||
|
||||
if devices:
|
||||
Test_UR_Base.device = devices[0]
|
||||
|
||||
def test_50_request_bad_device(self):
|
||||
if self.handle is None:
|
||||
self.fail("No receiver found")
|
||||
|
||||
device = 1 if self.device is None else self.device + 1
|
||||
reply = base.request(self.handle, device, FEATURE.ROOT, FEATURE.FEATURE_SET)
|
||||
self.assertIsNone(reply, "request returned valid reply")
|
||||
|
||||
def test_52_request_root_no_feature(self):
|
||||
if self.handle is None:
|
||||
self.fail("No receiver found")
|
||||
if self.device is None:
|
||||
self.fail("No devices attached")
|
||||
|
||||
reply = base.request(self.handle, self.device, FEATURE.ROOT)
|
||||
self.assertIsNotNone(reply, "request returned None reply")
|
||||
self.assertEqual(reply[:2], b'\x00\x00', "request returned for wrong feature id")
|
||||
|
||||
def test_55_request_root_feature_set(self):
|
||||
if self.handle is None:
|
||||
self.fail("No receiver found")
|
||||
if self.device is None:
|
||||
self.fail("No devices attached")
|
||||
|
||||
reply = base.request(self.handle, self.device, FEATURE.ROOT, FEATURE.FEATURE_SET)
|
||||
self.assertIsNotNone(reply, "request returned None reply")
|
||||
index = reply[:1]
|
||||
self.assertGreater(index, b'\x00', "FEATURE_SET not available on device " + str(self.device))
|
||||
|
||||
def test_57_request_ignore_undhandled(self):
|
||||
if self.handle is None:
|
||||
self.fail("No receiver found")
|
||||
if self.device is None:
|
||||
self.fail("No devices attached")
|
||||
|
||||
fs_index = base.request(self.handle, self.device, FEATURE.ROOT, FEATURE.FEATURE_SET)
|
||||
self.assertIsNotNone(fs_index)
|
||||
fs_index = fs_index[:1]
|
||||
self.assertGreater(fs_index, b'\x00')
|
||||
|
||||
global received_unhandled
|
||||
received_unhandled = None
|
||||
|
||||
def _unhandled(code, device, data):
|
||||
self.assertIsNotNone(code)
|
||||
self.assertIsInstance(code, int)
|
||||
self.assertIsNotNone(device)
|
||||
self.assertIsInstance(device, int)
|
||||
self.assertIsNotNone(data)
|
||||
self.assertIsInstance(data, str)
|
||||
global received_unhandled
|
||||
received_unhandled = (code, device, data)
|
||||
|
||||
base.unhandled_hook = _unhandled
|
||||
base.write(self.handle, self.device, FEATURE.ROOT + FEATURE.FEATURE_SET)
|
||||
reply = base.request(self.handle, self.device, fs_index + b'\x00')
|
||||
self.assertIsNotNone(reply, "request returned None reply")
|
||||
self.assertNotEquals(reply[:1], b'\x00')
|
||||
self.assertIsNotNone(received_unhandled, "extra message not received by unhandled hook")
|
||||
|
||||
received_unhandled = None
|
||||
base.unhandled_hook = None
|
||||
base.write(self.handle, self.device, FEATURE.ROOT + FEATURE.FEATURE_SET)
|
||||
reply = base.request(self.handle, self.device, fs_index + b'\x00')
|
||||
self.assertIsNotNone(reply, "request returned None reply")
|
||||
self.assertNotEquals(reply[:1], b'\x00')
|
||||
self.assertIsNone(received_unhandled)
|
||||
|
||||
del received_unhandled
|
||||
|
||||
# def test_90_receiver_missing(self):
|
||||
# if self.handle is None:
|
||||
# self.fail("No receiver found")
|
||||
#
|
||||
# logging.warn("remove the receiver in 5 seconds or this test will fail")
|
||||
# import time
|
||||
# time.sleep(5)
|
||||
# with self.assertRaises(NoReceiver):
|
||||
# self.test_30_ping_all_devices()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -1,134 +0,0 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
import unittest
|
||||
import warnings
|
||||
|
||||
from .. import api
|
||||
from ..constants import *
|
||||
from ..common import *
|
||||
|
||||
|
||||
class Test_UR_API(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.receiver = None
|
||||
cls.device = None
|
||||
cls.features = None
|
||||
cls.device_info = None
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
if cls.receiver:
|
||||
cls.receiver.close()
|
||||
cls.device = None
|
||||
cls.features = None
|
||||
cls.device_info = None
|
||||
|
||||
def _check(self, check_device=True, check_features=False):
|
||||
if self.receiver is None:
|
||||
self.fail("No receiver found")
|
||||
if check_device and self.device is None:
|
||||
self.fail("Found no devices attached.")
|
||||
if check_device and check_features and self.features is None:
|
||||
self.fail("no feature set available")
|
||||
|
||||
def test_00_open_receiver(self):
|
||||
Test_UR_API.receiver = api.Receiver.open()
|
||||
self._check(check_device=False)
|
||||
|
||||
def test_05_ping_device_zero(self):
|
||||
self._check(check_device=False)
|
||||
|
||||
ok = api.ping(self.receiver.handle, 0)
|
||||
self.assertIsNotNone(ok, "invalid ping reply")
|
||||
self.assertFalse(ok, "device zero replied")
|
||||
|
||||
def test_10_ping_all_devices(self):
|
||||
self._check(check_device=False)
|
||||
|
||||
devices = []
|
||||
|
||||
for devnumber in range(1, 1 + MAX_ATTACHED_DEVICES):
|
||||
ok = api.ping(self.receiver.handle, devnumber)
|
||||
self.assertIsNotNone(ok, "invalid ping reply")
|
||||
if ok:
|
||||
devices.append(self.receiver[devnumber])
|
||||
|
||||
if devices:
|
||||
Test_UR_API.device = devices[0].number
|
||||
|
||||
def test_30_get_feature_index(self):
|
||||
self._check()
|
||||
|
||||
fs_index = api.get_feature_index(self.receiver.handle, self.device, FEATURE.FEATURE_SET)
|
||||
self.assertIsNotNone(fs_index, "feature FEATURE_SET not available")
|
||||
self.assertGreater(fs_index, 0, "invalid FEATURE_SET index: " + str(fs_index))
|
||||
|
||||
def test_31_bad_feature(self):
|
||||
self._check()
|
||||
|
||||
reply = api.request(self.receiver.handle, self.device, FEATURE.ROOT, params=b'\xFF\xFF')
|
||||
self.assertIsNotNone(reply, "invalid reply")
|
||||
self.assertEqual(reply[:5], b'\x00' * 5, "invalid reply")
|
||||
|
||||
def test_40_get_device_features(self):
|
||||
self._check()
|
||||
|
||||
features = api.get_device_features(self.receiver.handle, self.device)
|
||||
self.assertIsNotNone(features, "failed to read features array")
|
||||
self.assertIn(FEATURE.FEATURE_SET, features, "feature FEATURE_SET not available")
|
||||
# cache this to simplify next tests
|
||||
Test_UR_API.features = features
|
||||
|
||||
def test_50_get_device_firmware(self):
|
||||
self._check(check_features=True)
|
||||
|
||||
d_firmware = api.get_device_firmware(self.receiver.handle, self.device, self.features)
|
||||
self.assertIsNotNone(d_firmware, "failed to get device firmware")
|
||||
self.assertGreater(len(d_firmware), 0, "device reported no firmware")
|
||||
for fw in d_firmware:
|
||||
self.assertIsInstance(fw, FirmwareInfo)
|
||||
|
||||
def test_52_get_device_kind(self):
|
||||
self._check(check_features=True)
|
||||
|
||||
d_kind = api.get_device_kind(self.receiver.handle, self.device, self.features)
|
||||
self.assertIsNotNone(d_kind, "failed to get device kind")
|
||||
self.assertGreater(len(d_kind), 0, "empty device kind")
|
||||
|
||||
def test_55_get_device_name(self):
|
||||
self._check(check_features=True)
|
||||
|
||||
d_name = api.get_device_name(self.receiver.handle, self.device, self.features)
|
||||
self.assertIsNotNone(d_name, "failed to read device name")
|
||||
self.assertGreater(len(d_name), 0, "empty device name")
|
||||
|
||||
def test_59_get_device_info(self):
|
||||
self._check(check_features=True)
|
||||
|
||||
device_info = api.get_device(self.receiver.handle, self.device, features=self.features)
|
||||
self.assertIsNotNone(device_info, "failed to read full device info")
|
||||
self.assertIsInstance(device_info, api.PairedDevice)
|
||||
Test_UR_API.device_info = device_info
|
||||
|
||||
def test_60_get_battery_level(self):
|
||||
self._check(check_features=True)
|
||||
|
||||
if FEATURE.BATTERY in self.features:
|
||||
battery = api.get_device_battery_level(self.receiver.handle, self.device, self.features)
|
||||
self.assertIsNotNone(battery, "failed to read battery level")
|
||||
self.assertIsInstance(battery, tuple, "result not a tuple")
|
||||
else:
|
||||
warnings.warn("BATTERY feature not supported by device %d" % self.device)
|
||||
|
||||
def test_70_list_devices(self):
|
||||
self._check(check_device=False)
|
||||
|
||||
for dev in self.receiver:
|
||||
self.assertIsNotNone(dev)
|
||||
self.assertIsInstance(dev, api.PairedDevice)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
56
lib/logitech_receiver/__init__.py
Normal file
56
lib/logitech_receiver/__init__.py
Normal file
@@ -0,0 +1,56 @@
|
||||
# -*- python-mode -*-
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
## Copyright (C) 2012-2013 Daniel Pavel
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published by
|
||||
## the Free Software Foundation; either version 2 of the License, or
|
||||
## (at your option) any later version.
|
||||
##
|
||||
## This program is distributed in the hope that it will be useful,
|
||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
## GNU General Public License for more details.
|
||||
##
|
||||
## You should have received a copy of the GNU General Public License along
|
||||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
"""Low-level interface for devices connected through a Logitech Universal
|
||||
Receiver (UR).
|
||||
|
||||
Uses the HID api exposed through hidapi.py, a Python thin layer over a native
|
||||
implementation.
|
||||
|
||||
Incomplete. Based on a bit of documentation, trial-and-error, and guesswork.
|
||||
|
||||
References:
|
||||
http://julien.danjou.info/blog/2012/logitech-k750-linux-support
|
||||
http://6xq.net/git/lars/lshidpp.git/plain/doc/
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
import logging
|
||||
|
||||
_DEBUG = logging.DEBUG
|
||||
_log = logging.getLogger(__name__)
|
||||
_log.setLevel(logging.root.level)
|
||||
# if logging.root.level > logging.DEBUG:
|
||||
# _log.addHandler(logging.NullHandler())
|
||||
# _log.propagate = 0
|
||||
|
||||
del logging
|
||||
|
||||
|
||||
__version__ = '0.9'
|
||||
|
||||
|
||||
from .common import strhex
|
||||
from .base import NoReceiver, NoSuchDevice, DeviceUnreachable
|
||||
from .receiver import Receiver, PairedDevice
|
||||
from .hidpp20 import FeatureNotSupported, FeatureCallError
|
||||
|
||||
from . import listener
|
||||
from . import status
|
||||
482
lib/logitech_receiver/base.py
Normal file
482
lib/logitech_receiver/base.py
Normal file
@@ -0,0 +1,482 @@
|
||||
# -*- python-mode -*-
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
## Copyright (C) 2012-2013 Daniel Pavel
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published by
|
||||
## the Free Software Foundation; either version 2 of the License, or
|
||||
## (at your option) any later version.
|
||||
##
|
||||
## This program is distributed in the hope that it will be useful,
|
||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
## GNU General Public License for more details.
|
||||
##
|
||||
## You should have received a copy of the GNU General Public License along
|
||||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
# Base low-level functions used by the API proper.
|
||||
# Unlikely to be used directly unless you're expanding the API.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
from time import time as _timestamp
|
||||
from random import getrandbits as _random_bits
|
||||
|
||||
from logging import getLogger, DEBUG as _DEBUG
|
||||
_log = getLogger(__name__)
|
||||
del getLogger
|
||||
|
||||
|
||||
from .common import strhex as _strhex, KwException as _KwException, pack as _pack
|
||||
from . import hidpp10 as _hidpp10
|
||||
from . import hidpp20 as _hidpp20
|
||||
import hidapi as _hid
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
_SHORT_MESSAGE_SIZE = 7
|
||||
_LONG_MESSAGE_SIZE = 20
|
||||
_MEDIUM_MESSAGE_SIZE = 15
|
||||
_MAX_READ_SIZE = 32
|
||||
|
||||
"""Default timeout on read (in seconds)."""
|
||||
DEFAULT_TIMEOUT = 4
|
||||
# the receiver itself should reply very fast, within 500ms
|
||||
_RECEIVER_REQUEST_TIMEOUT = 0.9
|
||||
# devices may reply a lot slower, as the call has to go wireless to them and come back
|
||||
_DEVICE_REQUEST_TIMEOUT = DEFAULT_TIMEOUT
|
||||
# when pinging, be extra patient
|
||||
_PING_TIMEOUT = DEFAULT_TIMEOUT * 2
|
||||
|
||||
#
|
||||
# Exceptions that may be raised by this API.
|
||||
#
|
||||
|
||||
class NoReceiver(_KwException):
|
||||
"""Raised when trying to talk through a previously open handle, when the
|
||||
receiver is no longer available. Should only happen if the receiver is
|
||||
physically disconnected from the machine, or its kernel driver module is
|
||||
unloaded."""
|
||||
pass
|
||||
|
||||
|
||||
class NoSuchDevice(_KwException):
|
||||
"""Raised when trying to reach a device number not paired to the receiver."""
|
||||
pass
|
||||
|
||||
|
||||
class DeviceUnreachable(_KwException):
|
||||
"""Raised when a request is made to an unreachable (turned off) device."""
|
||||
pass
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from .base_usb import ALL as _RECEIVER_USB_IDS
|
||||
|
||||
def receivers():
|
||||
"""List all the Linux devices exposed by the UR attached to the machine."""
|
||||
for receiver_usb_id in _RECEIVER_USB_IDS:
|
||||
for d in _hid.enumerate(*receiver_usb_id):
|
||||
yield d
|
||||
|
||||
|
||||
def notify_on_receivers_glib(callback):
|
||||
"""Watch for matching devices and notifies the callback on the GLib thread."""
|
||||
_hid.monitor_glib(callback, *_RECEIVER_USB_IDS)
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def open_path(path):
|
||||
"""Checks if the given Linux device path points to the right UR device.
|
||||
|
||||
:param path: the Linux device path.
|
||||
|
||||
The UR physical device may expose multiple linux devices with the same
|
||||
interface, so we have to check for the right one. At this moment the only
|
||||
way to distinguish betheen them is to do a test ping on an invalid
|
||||
(attached) device number (i.e., 0), expecting a 'ping failed' reply.
|
||||
|
||||
:returns: an open receiver handle if this is the right Linux device, or
|
||||
``None``.
|
||||
"""
|
||||
return _hid.open_path(path)
|
||||
|
||||
|
||||
def open():
|
||||
"""Opens the first Logitech Unifying Receiver found attached to the machine.
|
||||
|
||||
:returns: An open file handle for the found receiver, or ``None``.
|
||||
"""
|
||||
for rawdevice in receivers():
|
||||
handle = open_path(rawdevice.path)
|
||||
if handle:
|
||||
return handle
|
||||
|
||||
|
||||
def close(handle):
|
||||
"""Closes a HID device handle."""
|
||||
if handle:
|
||||
try:
|
||||
if isinstance(handle, int):
|
||||
_hid.close(handle)
|
||||
else:
|
||||
handle.close()
|
||||
# _log.info("closed receiver handle %r", handle)
|
||||
return True
|
||||
except:
|
||||
# _log.exception("closing receiver handle %r", handle)
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def write(handle, devnumber, data):
|
||||
"""Writes some data to the receiver, addressed to a certain device.
|
||||
|
||||
:param handle: an open UR handle.
|
||||
:param devnumber: attached device number.
|
||||
:param data: data to send, up to 5 bytes.
|
||||
|
||||
The first two (required) bytes of data must be the SubId and address.
|
||||
|
||||
:raises NoReceiver: if the receiver is no longer available, i.e. has
|
||||
been physically removed from the machine, or the kernel driver has been
|
||||
unloaded. The handle will be closed automatically.
|
||||
"""
|
||||
# the data is padded to either 5 or 18 bytes
|
||||
assert data is not None
|
||||
assert isinstance(data, bytes), (repr(data), type(data))
|
||||
|
||||
if len(data) > _SHORT_MESSAGE_SIZE - 2 or data[:1] == b'\x82':
|
||||
wdata = _pack('!BB18s', 0x11, devnumber, data)
|
||||
else:
|
||||
wdata = _pack('!BB5s', 0x10, devnumber, data)
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("(%s) <= w[%02X %02X %s %s]", handle, ord(wdata[:1]), devnumber, _strhex(wdata[2:4]), _strhex(wdata[4:]))
|
||||
|
||||
try:
|
||||
_hid.write(int(handle), wdata)
|
||||
except Exception as reason:
|
||||
_log.error("write failed, assuming handle %r no longer available", handle)
|
||||
close(handle)
|
||||
raise NoReceiver(reason=reason)
|
||||
|
||||
|
||||
def read(handle, timeout=DEFAULT_TIMEOUT):
|
||||
"""Read some data from the receiver. Usually called after a write (feature
|
||||
call), to get the reply.
|
||||
|
||||
:param: handle open handle to the receiver
|
||||
:param: timeout how long to wait for a reply, in seconds
|
||||
|
||||
:returns: a tuple of (devnumber, message data), or `None`
|
||||
|
||||
:raises NoReceiver: if the receiver is no longer available, i.e. has
|
||||
been physically removed from the machine, or the kernel driver has been
|
||||
unloaded. The handle will be closed automatically.
|
||||
"""
|
||||
reply = _read(handle, timeout)
|
||||
if reply:
|
||||
return reply[1:]
|
||||
|
||||
|
||||
def _read(handle, timeout):
|
||||
"""Read an incoming packet from the receiver.
|
||||
|
||||
:returns: a tuple of (report_id, devnumber, data), or `None`.
|
||||
|
||||
:raises NoReceiver: if the receiver is no longer available, i.e. has
|
||||
been physically removed from the machine, or the kernel driver has been
|
||||
unloaded. The handle will be closed automatically.
|
||||
"""
|
||||
try:
|
||||
# convert timeout to milliseconds, the hidapi expects it
|
||||
timeout = int(timeout * 1000)
|
||||
data = _hid.read(int(handle), _MAX_READ_SIZE, timeout)
|
||||
except Exception as reason:
|
||||
_log.error("read failed, assuming handle %r no longer available", handle)
|
||||
close(handle)
|
||||
raise NoReceiver(reason=reason)
|
||||
|
||||
if data:
|
||||
assert isinstance(data, bytes), (repr(data), type(data))
|
||||
report_id = ord(data[:1])
|
||||
assert ((report_id & 0xF0 == 0) or
|
||||
(report_id == 0x10 and len(data) == _SHORT_MESSAGE_SIZE) or
|
||||
(report_id == 0x11 and len(data) == _LONG_MESSAGE_SIZE) or
|
||||
(report_id == 0x20 and len(data) == _MEDIUM_MESSAGE_SIZE)), \
|
||||
"unexpected message size: report_id %02X message %s" % (report_id, _strhex(data))
|
||||
if report_id & 0xF0 == 0x00:
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("(%s) => r[%02X %s] ignoring unknown report", handle, report_id, _strhex(data[1:]))
|
||||
return
|
||||
devnumber = ord(data[1:2])
|
||||
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("(%s) => r[%02X %02X %s %s]", handle, report_id, devnumber, _strhex(data[2:4]), _strhex(data[4:]))
|
||||
|
||||
return report_id, devnumber, data[2:]
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def _skip_incoming(handle, ihandle, notifications_hook):
|
||||
"""Read anything already in the input buffer.
|
||||
|
||||
Used by request() and ping() before their write.
|
||||
"""
|
||||
|
||||
while True:
|
||||
try:
|
||||
# read whatever is already in the buffer, if any
|
||||
data = _hid.read(ihandle, _MAX_READ_SIZE, 0)
|
||||
except Exception as reason:
|
||||
_log.error("read failed, assuming receiver %s no longer available", handle)
|
||||
close(handle)
|
||||
raise NoReceiver(reason=reason)
|
||||
|
||||
if data:
|
||||
assert isinstance(data, bytes), (repr(data), type(data))
|
||||
report_id = ord(data[:1])
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
assert ((report_id & 0xF0 == 0) or
|
||||
(report_id == 0x10 and len(data) == _SHORT_MESSAGE_SIZE) or
|
||||
(report_id == 0x11 and len(data) == _LONG_MESSAGE_SIZE) or
|
||||
(report_id == 0x20 and len(data) == _MEDIUM_MESSAGE_SIZE)), \
|
||||
"unexpected message size: report_id %02X message %s" % (report_id, _strhex(data))
|
||||
if notifications_hook and report_id & 0xF0:
|
||||
n = make_notification(ord(data[1:2]), data[2:])
|
||||
if n:
|
||||
notifications_hook(n)
|
||||
else:
|
||||
# nothing in the input buffer, we're done
|
||||
return
|
||||
|
||||
|
||||
def make_notification(devnumber, data):
|
||||
"""Guess if this is a notification (and not just a request reply), and
|
||||
return a Notification tuple if it is."""
|
||||
sub_id = ord(data[:1])
|
||||
if sub_id & 0x80 == 0x80:
|
||||
# this is either a HID++1.0 register r/w, or an error reply
|
||||
return
|
||||
|
||||
address = ord(data[1:2])
|
||||
if (
|
||||
# standard HID++ 1.0 notification, SubId may be 0x40 - 0x7F
|
||||
(sub_id >= 0x40)
|
||||
or
|
||||
# custom HID++1.0 battery events, where SubId is 0x07/0x0D
|
||||
(sub_id in (0x07, 0x0D) and len(data) == 5 and data[4:5] == b'\x00')
|
||||
or
|
||||
# custom HID++1.0 illumination event, where SubId is 0x17
|
||||
(sub_id == 0x17 and len(data) == 5)
|
||||
or
|
||||
# HID++ 2.0 feature notifications have the SoftwareID 0
|
||||
(address & 0x0F == 0x00)
|
||||
):
|
||||
return _HIDPP_Notification(devnumber, sub_id, address, data[2:])
|
||||
|
||||
from collections import namedtuple
|
||||
_HIDPP_Notification = namedtuple('_HIDPP_Notification', ('devnumber', 'sub_id', 'address', 'data'))
|
||||
_HIDPP_Notification.__str__ = lambda self: 'Notification(%d,%02X,%02X,%s)' % (self.devnumber, self.sub_id, self.address, _strhex(self.data))
|
||||
_HIDPP_Notification.__unicode__ = _HIDPP_Notification.__str__
|
||||
del namedtuple
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def request(handle, devnumber, request_id, *params):
|
||||
"""Makes a feature call to a device and waits for a matching reply.
|
||||
|
||||
This function will wait for a matching reply indefinitely.
|
||||
|
||||
:param handle: an open UR handle.
|
||||
:param devnumber: attached device number.
|
||||
:param request_id: a 16-bit integer.
|
||||
:param params: parameters for the feature call, 3 to 16 bytes.
|
||||
:returns: the reply data, or ``None`` if some error occured.
|
||||
"""
|
||||
|
||||
# import inspect as _inspect
|
||||
# print ('\n '.join(str(s) for s in _inspect.stack()))
|
||||
|
||||
assert isinstance(request_id, int)
|
||||
if devnumber != 0xFF and request_id < 0x8000:
|
||||
# For HID++ 2.0 feature requests, randomize the SoftwareId to make it
|
||||
# easier to recognize the reply for this request. also, always set the
|
||||
# most significant bit (8) in SoftwareId, to make notifications easier
|
||||
# to distinguish from request replies.
|
||||
# This only applies to peripheral requests, ofc.
|
||||
request_id = (request_id & 0xFFF0) | 0x08 | _random_bits(3)
|
||||
|
||||
timeout = _RECEIVER_REQUEST_TIMEOUT if devnumber == 0xFF else _DEVICE_REQUEST_TIMEOUT
|
||||
# be extra patient on long register read
|
||||
if request_id & 0xFF00 == 0x8300:
|
||||
timeout *= 2
|
||||
|
||||
if params:
|
||||
params = b''.join(_pack('B', p) if isinstance(p, int) else p for p in params)
|
||||
else:
|
||||
params = b''
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("(%s) device %d request_id {%04X} params [%s]", handle, devnumber, request_id, _strhex(params))
|
||||
request_data = _pack('!H', request_id) + params
|
||||
|
||||
ihandle = int(handle)
|
||||
notifications_hook = getattr(handle, 'notifications_hook', None)
|
||||
_skip_incoming(handle, ihandle, notifications_hook)
|
||||
write(ihandle, devnumber, request_data)
|
||||
|
||||
# we consider timeout from this point
|
||||
request_started = _timestamp()
|
||||
delta = 0
|
||||
|
||||
while delta < timeout:
|
||||
reply = _read(handle, timeout)
|
||||
|
||||
if reply:
|
||||
report_id, reply_devnumber, reply_data = reply
|
||||
if reply_devnumber == devnumber:
|
||||
if report_id == 0x10 and reply_data[:1] == b'\x8F' and reply_data[1:3] == request_data[:2]:
|
||||
error = ord(reply_data[3:4])
|
||||
|
||||
# if error == _hidpp10.ERROR.resource_error: # device unreachable
|
||||
# _log.warn("(%s) device %d error on request {%04X}: unknown device", handle, devnumber, request_id)
|
||||
# raise DeviceUnreachable(number=devnumber, request=request_id)
|
||||
|
||||
# if error == _hidpp10.ERROR.unknown_device: # unknown device
|
||||
# _log.error("(%s) device %d error on request {%04X}: unknown device", handle, devnumber, request_id)
|
||||
# raise NoSuchDevice(number=devnumber, request=request_id)
|
||||
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("(%s) device 0x%02X error on request {%04X}: %d = %s",
|
||||
handle, devnumber, request_id, error, _hidpp10.ERROR[error])
|
||||
return
|
||||
|
||||
if reply_data[:1] == b'\xFF' and reply_data[1:3] == request_data[:2]:
|
||||
# a HID++ 2.0 feature call returned with an error
|
||||
error = ord(reply_data[3:4])
|
||||
_log.error("(%s) device %d error on feature request {%04X}: %d = %s",
|
||||
handle, devnumber, request_id, error, _hidpp20.ERROR[error])
|
||||
raise _hidpp20.FeatureCallError(number=devnumber, request=request_id, error=error, params=params)
|
||||
|
||||
if reply_data[:2] == request_data[:2]:
|
||||
if request_id & 0xFE00 == 0x8200:
|
||||
# long registry r/w should return a long reply
|
||||
assert report_id == 0x11
|
||||
elif request_id & 0xFE00 == 0x8000:
|
||||
# short registry r/w should return a short reply
|
||||
assert report_id == 0x10
|
||||
|
||||
if devnumber == 0xFF:
|
||||
if request_id == 0x83B5 or request_id == 0x81F1:
|
||||
# these replies have to match the first parameter as well
|
||||
if reply_data[2:3] == params[:1]:
|
||||
return reply_data[2:]
|
||||
else:
|
||||
# hm, not mathing my request, and certainly not a notification
|
||||
continue
|
||||
else:
|
||||
return reply_data[2:]
|
||||
else:
|
||||
return reply_data[2:]
|
||||
else:
|
||||
# a reply was received, but did not match our request in any way
|
||||
# reset the timeout starting point
|
||||
request_started = _timestamp()
|
||||
|
||||
if notifications_hook:
|
||||
n = make_notification(reply_devnumber, reply_data)
|
||||
if n:
|
||||
notifications_hook(n)
|
||||
# elif _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("(%s) ignoring reply %02X [%s]", handle, reply_devnumber, _strhex(reply_data))
|
||||
# elif _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("(%s) ignoring reply %02X [%s]", handle, reply_devnumber, _strhex(reply_data))
|
||||
|
||||
delta = _timestamp() - request_started
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("(%s) still waiting for reply, delta %f", handle, delta)
|
||||
|
||||
_log.warn("timeout (%0.2f/%0.2f) on device %d request {%04X} params [%s]",
|
||||
delta, timeout, devnumber, request_id, _strhex(params))
|
||||
# raise DeviceUnreachable(number=devnumber, request=request_id)
|
||||
|
||||
|
||||
def ping(handle, devnumber):
|
||||
"""Check if a device is connected to the receiver.
|
||||
|
||||
:returns: The HID protocol supported by the device, as a floating point number, if the device is active.
|
||||
"""
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("(%s) pinging device %d", handle, devnumber)
|
||||
|
||||
# import inspect as _inspect
|
||||
# print ('\n '.join(str(s) for s in _inspect.stack()))
|
||||
|
||||
assert devnumber != 0xFF
|
||||
assert devnumber > 0x00
|
||||
assert devnumber < 0x0F
|
||||
|
||||
# randomize the SoftwareId and mark byte to be able to identify the ping
|
||||
# reply, and set most significant (0x8) bit in SoftwareId so that the reply
|
||||
# is always distinguishable from notifications
|
||||
request_id = 0x0018 | _random_bits(3)
|
||||
request_data = _pack('!HBBB', request_id, 0, 0, _random_bits(8))
|
||||
|
||||
ihandle = int(handle)
|
||||
notifications_hook = getattr(handle, 'notifications_hook', None)
|
||||
_skip_incoming(handle, ihandle, notifications_hook)
|
||||
write(ihandle, devnumber, request_data)
|
||||
|
||||
# we consider timeout from this point
|
||||
request_started = _timestamp()
|
||||
delta = 0
|
||||
|
||||
while delta < _PING_TIMEOUT:
|
||||
reply = _read(handle, _PING_TIMEOUT)
|
||||
|
||||
if reply:
|
||||
report_id, reply_devnumber, reply_data = reply
|
||||
if reply_devnumber == devnumber:
|
||||
if reply_data[:2] == request_data[:2] and reply_data[4:5] == request_data[-1:]:
|
||||
# HID++ 2.0+ device, currently connected
|
||||
return ord(reply_data[2:3]) + ord(reply_data[3:4]) / 10.0
|
||||
|
||||
if report_id == 0x10 and reply_data[:1] == b'\x8F' and reply_data[1:3] == request_data[:2]:
|
||||
assert reply_data[-1:] == b'\x00'
|
||||
error = ord(reply_data[3:4])
|
||||
|
||||
if error == _hidpp10.ERROR.invalid_SubID__command: # a valid reply from a HID++ 1.0 device
|
||||
return 1.0
|
||||
|
||||
if error == _hidpp10.ERROR.resource_error: # device unreachable
|
||||
return
|
||||
|
||||
if error == _hidpp10.ERROR.unknown_device: # no paired device with that number
|
||||
_log.error("(%s) device %d error on ping request: unknown device", handle, devnumber)
|
||||
raise NoSuchDevice(number=devnumber, request=request_id)
|
||||
|
||||
if notifications_hook:
|
||||
n = make_notification(reply_devnumber, reply_data)
|
||||
if n:
|
||||
notifications_hook(n)
|
||||
# elif _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("(%s) ignoring reply %02X [%s]", handle, reply_devnumber, _strhex(reply_data))
|
||||
|
||||
delta = _timestamp() - request_started
|
||||
|
||||
_log.warn("(%s) timeout (%0.2f/%0.2f) on device %d ping", handle, delta, _PING_TIMEOUT, devnumber)
|
||||
# raise DeviceUnreachable(number=devnumber, request=request_id)
|
||||
63
lib/logitech_receiver/base_usb.py
Normal file
63
lib/logitech_receiver/base_usb.py
Normal file
@@ -0,0 +1,63 @@
|
||||
# -*- python-mode -*-
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
## Copyright (C) 2012-2013 Daniel Pavel
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published by
|
||||
## the Free Software Foundation; either version 2 of the License, or
|
||||
## (at your option) any later version.
|
||||
##
|
||||
## This program is distributed in the hope that it will be useful,
|
||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
## GNU General Public License for more details.
|
||||
##
|
||||
## You should have received a copy of the GNU General Public License along
|
||||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
# USB ids of Logitech wireless receivers.
|
||||
# Only receivers supporting the HID++ protocol can go in here.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
|
||||
_UNIFYING_DRIVER = 'logitech-djreceiver'
|
||||
_GENERIC_DRIVER = ('hid-generic', 'generic-usb')
|
||||
|
||||
|
||||
# each tuple contains (vendor_id, product_id, usb interface number, hid driver)
|
||||
|
||||
# standard Unifying receivers (marked with the orange Unifying logo)
|
||||
UNIFYING_RECEIVER = (0x046d, 0xc52b, 2, _UNIFYING_DRIVER)
|
||||
UNIFYING_RECEIVER_2 = (0x046d, 0xc532, 2, _UNIFYING_DRIVER)
|
||||
|
||||
|
||||
|
||||
# Nano receviers that support the Unifying protocol
|
||||
NANO_RECEIVER_ADVANCED = (0x046d, 0xc52f, 1, _GENERIC_DRIVER)
|
||||
|
||||
# Nano receivers that don't support the Unifying protocol
|
||||
NANO_RECEIVER_C517 = (0x046d, 0xc517, 1, _GENERIC_DRIVER)
|
||||
NANO_RECEIVER_C518 = (0x046d, 0xc518, 1, _GENERIC_DRIVER)
|
||||
NANO_RECEIVER_C51A = (0x046d, 0xc51a, 1, _GENERIC_DRIVER)
|
||||
NANO_RECEIVER_C51B = (0x046d, 0xc51b, 1, _GENERIC_DRIVER)
|
||||
NANO_RECEIVER_C521 = (0x046d, 0xc521, 1, _GENERIC_DRIVER)
|
||||
NANO_RECEIVER_C525 = (0x046d, 0xc525, 1, _GENERIC_DRIVER)
|
||||
NANO_RECEIVER_C526 = (0x046d, 0xc526, 1, _GENERIC_DRIVER)
|
||||
|
||||
|
||||
|
||||
ALL = (
|
||||
UNIFYING_RECEIVER,
|
||||
UNIFYING_RECEIVER_2,
|
||||
NANO_RECEIVER_ADVANCED,
|
||||
NANO_RECEIVER_C517,
|
||||
NANO_RECEIVER_C518,
|
||||
NANO_RECEIVER_C51A,
|
||||
NANO_RECEIVER_C51B,
|
||||
NANO_RECEIVER_C521,
|
||||
NANO_RECEIVER_C525,
|
||||
NANO_RECEIVER_C526,
|
||||
)
|
||||
277
lib/logitech_receiver/common.py
Normal file
277
lib/logitech_receiver/common.py
Normal file
@@ -0,0 +1,277 @@
|
||||
# -*- python-mode -*-
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
## Copyright (C) 2012-2013 Daniel Pavel
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published by
|
||||
## the Free Software Foundation; either version 2 of the License, or
|
||||
## (at your option) any later version.
|
||||
##
|
||||
## This program is distributed in the hope that it will be useful,
|
||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
## GNU General Public License for more details.
|
||||
##
|
||||
## You should have received a copy of the GNU General Public License along
|
||||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
# Some common functions and types.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
from binascii import hexlify as _hexlify
|
||||
from struct import pack, unpack
|
||||
try:
|
||||
unicode
|
||||
# if Python2, unicode_literals will mess our first (un)pack() argument
|
||||
_pack_str = pack
|
||||
_unpack_str = unpack
|
||||
pack = lambda x, *args: _pack_str(str(x), *args)
|
||||
unpack = lambda x, *args: _unpack_str(str(x), *args)
|
||||
|
||||
is_string = lambda d: isinstance(d, unicode) or isinstance(d, str)
|
||||
# no easy way to distinguish between b'' and '' :(
|
||||
# or (isinstance(d, str) \
|
||||
# and not any((chr(k) in d for k in range(0x00, 0x1F))) \
|
||||
# and not any((chr(k) in d for k in range(0x80, 0xFF))) \
|
||||
# )
|
||||
except:
|
||||
# this is certanly Python 3
|
||||
# In Py3, unicode and str are equal (the unicode object does not exist)
|
||||
is_string = lambda d: isinstance(d, str)
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
class NamedInt(int):
|
||||
"""An reqular Python integer with an attached name.
|
||||
|
||||
Caution: comparison with strings will also match this NamedInt's name
|
||||
(case-insensitive)."""
|
||||
|
||||
def __new__(cls, value, name):
|
||||
assert is_string(name)
|
||||
obj = int.__new__(cls, value)
|
||||
obj.name = str(name)
|
||||
return obj
|
||||
|
||||
def bytes(self, count=2):
|
||||
return int2bytes(self, count)
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, NamedInt):
|
||||
return int(self) == int(other) and self.name == other.name
|
||||
if isinstance(other, int):
|
||||
return int(self) == int(other)
|
||||
if is_string(other):
|
||||
return self.name.lower() == other.lower()
|
||||
# this should catch comparisons with bytes in Py3
|
||||
if other is not None:
|
||||
raise TypeError('Unsupported type ' + str(type(other)))
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __hash__(self):
|
||||
return int(self)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
__unicode__ = __str__
|
||||
|
||||
def __repr__(self):
|
||||
return 'NamedInt(%d, %r)' % (int(self), self.name)
|
||||
|
||||
|
||||
class NamedInts(object):
|
||||
"""An ordered set of NamedInt values.
|
||||
|
||||
Indexing can be made by int or string, and will return the corresponding
|
||||
NamedInt if it exists in this set, or `None`.
|
||||
|
||||
Extracting slices will return all present NamedInts in the given interval
|
||||
(extended slices are not supported).
|
||||
|
||||
Assigning a string to an indexed int will create a new NamedInt in this set;
|
||||
if the value already exists in the set (int or string), ValueError will be
|
||||
raised.
|
||||
"""
|
||||
__slots__ = ('__dict__', '_values', '_indexed', '_fallback')
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
def _readable_name(n):
|
||||
if not is_string(n):
|
||||
raise TypeError("expected (unicode) string, got " + str(type(n)))
|
||||
return n.replace('__', '/').replace('_', ' ')
|
||||
|
||||
# print (repr(kwargs))
|
||||
values = {k: NamedInt(v, _readable_name(k)) for (k, v) in kwargs.items()}
|
||||
self.__dict__ = values
|
||||
self._values = sorted(list(values.values()))
|
||||
self._indexed = {int(v): v for v in self._values}
|
||||
# assert len(values) == len(self._indexed), "(%d) %r\n=> (%d) %r" % (len(values), values, len(self._indexed), self._indexed)
|
||||
self._fallback = None
|
||||
|
||||
@classmethod
|
||||
def range(cls, from_value, to_value, name_generator=lambda x: str(x), step=1):
|
||||
values = {name_generator(x): x for x in range(from_value, to_value + 1, step)}
|
||||
return NamedInts(**values)
|
||||
|
||||
def flag_names(self, value):
|
||||
unknown_bits = value
|
||||
for k in self._indexed:
|
||||
assert bin(k).count('1') == 1
|
||||
if k & value == k:
|
||||
unknown_bits &= ~k
|
||||
yield str(self._indexed[k])
|
||||
|
||||
if unknown_bits:
|
||||
yield 'unknown:%06X' % unknown_bits
|
||||
|
||||
def __getitem__(self, index):
|
||||
if isinstance(index, int):
|
||||
if index in self._indexed:
|
||||
return self._indexed[int(index)]
|
||||
if self._fallback and isinstance(index, int):
|
||||
value = NamedInt(index, self._fallback(index))
|
||||
self._indexed[index] = value
|
||||
self._values = sorted(self._values + [value])
|
||||
return value
|
||||
|
||||
elif is_string(index):
|
||||
if index in self.__dict__:
|
||||
return self.__dict__[index]
|
||||
|
||||
elif isinstance(index, slice):
|
||||
if index.start is None and index.stop is None:
|
||||
return self._values[:]
|
||||
|
||||
v_start = int(self._values[0]) if index.start is None else int(index.start)
|
||||
v_stop = (self._values[-1] + 1) if index.stop is None else int(index.stop)
|
||||
|
||||
if v_start > v_stop or v_start > self._values[-1] or v_stop <= self._values[0]:
|
||||
return []
|
||||
|
||||
if v_start <= self._values[0] and v_stop > self._values[-1]:
|
||||
return self._values[:]
|
||||
|
||||
start_index = 0
|
||||
stop_index = len(self._values)
|
||||
for i, value in enumerate(self._values):
|
||||
if value < v_start:
|
||||
start_index = i + 1
|
||||
elif index.stop is None:
|
||||
break
|
||||
if value >= v_stop:
|
||||
stop_index = i
|
||||
break
|
||||
|
||||
return self._values[start_index:stop_index]
|
||||
|
||||
def __setitem__(self, index, name):
|
||||
assert isinstance(index, int), type(index)
|
||||
if isinstance(name, NamedInt):
|
||||
assert int(index) == int(name), repr(index) + ' ' + repr(name)
|
||||
value = name
|
||||
elif is_string(name):
|
||||
value = NamedInt(index, name)
|
||||
else:
|
||||
raise TypeError('name must be a string')
|
||||
|
||||
if str(value) in self.__dict__:
|
||||
raise ValueError('%s (%d) already known' % (value, int(value)))
|
||||
if int(value) in self._indexed:
|
||||
raise ValueError('%d (%s) already known' % (int(value), value))
|
||||
|
||||
self._values = sorted(self._values + [value])
|
||||
self.__dict__[str(value)] = value
|
||||
self._indexed[int(value)] = value
|
||||
|
||||
def __contains__(self, value):
|
||||
if isinstance(value, int):
|
||||
return value in self._indexed
|
||||
elif is_string(value):
|
||||
return value in self.__dict__
|
||||
|
||||
def __iter__(self):
|
||||
for v in self._values:
|
||||
yield v
|
||||
|
||||
def __len__(self):
|
||||
return len(self._values)
|
||||
|
||||
def __repr__(self):
|
||||
return 'NamedInts(%s)' % ', '.join(repr(v) for v in self._values)
|
||||
|
||||
|
||||
def strhex(x):
|
||||
assert x is not None
|
||||
"""Produce a hex-string representation of a sequence of bytes."""
|
||||
return _hexlify(x).decode('ascii').upper()
|
||||
|
||||
|
||||
def bytes2int(x):
|
||||
"""Convert a bytes string to an int.
|
||||
The bytes are assumed to be in most-significant-first order.
|
||||
"""
|
||||
assert isinstance(x, bytes)
|
||||
assert len(x) < 9
|
||||
qx = (b'\x00' * 8) + x
|
||||
result, = unpack('!Q', qx[-8:])
|
||||
# assert x == int2bytes(result, len(x))
|
||||
return result
|
||||
|
||||
|
||||
def int2bytes(x, count=None):
|
||||
"""Convert an int to a bytes representation.
|
||||
The bytes are ordered in most-significant-first order.
|
||||
If 'count' is not given, the necessary number of bytes is computed.
|
||||
"""
|
||||
assert isinstance(x, int)
|
||||
result = pack('!Q', x)
|
||||
assert isinstance(result, bytes)
|
||||
# assert x == bytes2int(result)
|
||||
|
||||
if count is None:
|
||||
return result.lstrip(b'\x00')
|
||||
|
||||
assert isinstance(count, int)
|
||||
assert count > 0
|
||||
assert x.bit_length() <= count * 8
|
||||
return result[-count:]
|
||||
|
||||
|
||||
class KwException(Exception):
|
||||
"""An exception that remembers all arguments passed to the constructor.
|
||||
They can be later accessed by simple member access.
|
||||
"""
|
||||
def __init__(self, **kwargs):
|
||||
super(KwException, self).__init__(kwargs)
|
||||
|
||||
def __getattr__(self, k):
|
||||
try:
|
||||
return super(KwException, self).__getattr__(k)
|
||||
except AttributeError:
|
||||
return self.args[0][k]
|
||||
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
"""Firmware information."""
|
||||
FirmwareInfo = namedtuple('FirmwareInfo', [
|
||||
'kind',
|
||||
'name',
|
||||
'version',
|
||||
'extras'])
|
||||
|
||||
"""Reprogrammable keys informations."""
|
||||
ReprogrammableKeyInfo = namedtuple('ReprogrammableKeyInfo', [
|
||||
'index',
|
||||
'key',
|
||||
'task',
|
||||
'flags'])
|
||||
|
||||
del namedtuple
|
||||
273
lib/logitech_receiver/descriptors.py
Normal file
273
lib/logitech_receiver/descriptors.py
Normal file
@@ -0,0 +1,273 @@
|
||||
# -*- python-mode -*-
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
## Copyright (C) 2012-2013 Daniel Pavel
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published by
|
||||
## the Free Software Foundation; either version 2 of the License, or
|
||||
## (at your option) any later version.
|
||||
##
|
||||
## This program is distributed in the hope that it will be useful,
|
||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
## GNU General Public License for more details.
|
||||
##
|
||||
## You should have received a copy of the GNU General Public License along
|
||||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
|
||||
from . import hidpp10 as _hidpp10
|
||||
from .common import NamedInts as _NamedInts
|
||||
from .settings_templates import RegisterSettings as _RS, FeatureSettings as _FS
|
||||
|
||||
_R = _hidpp10.REGISTERS
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from collections import namedtuple
|
||||
_DeviceDescriptor = namedtuple('_DeviceDescriptor',
|
||||
('name', 'kind', 'wpid', 'codename', 'protocol', 'registers', 'settings'))
|
||||
del namedtuple
|
||||
|
||||
DEVICES = {}
|
||||
|
||||
def _D(name, codename=None, kind=None, wpid=None, protocol=None, registers=None, settings=None):
|
||||
assert name
|
||||
|
||||
if kind is None:
|
||||
kind = (_hidpp10.DEVICE_KIND.mouse if 'Mouse' in name
|
||||
else _hidpp10.DEVICE_KIND.keyboard if 'Keyboard' in name
|
||||
else _hidpp10.DEVICE_KIND.touchpad if 'Touchpad' in name
|
||||
else _hidpp10.DEVICE_KIND.trackball if 'Trackball' in name
|
||||
else None)
|
||||
assert kind is not None, 'descriptor for %s does not have kind set' % name
|
||||
|
||||
# heuristic: the codename is the last word in the device name
|
||||
if codename is None and ' ' in name:
|
||||
codename = name.split(' ')[-1]
|
||||
assert codename is not None, 'descriptor for %s does not have codename set' % name
|
||||
|
||||
if protocol is not None:
|
||||
# ? 2.0 devices should not have any registers
|
||||
if protocol < 2.0:
|
||||
assert settings is None or all(s._rw.kind == 1 for s in settings)
|
||||
else:
|
||||
assert registers is None
|
||||
assert settings is None or all(s._rw.kind == 2 for s in settings)
|
||||
|
||||
if wpid:
|
||||
for w in wpid if isinstance(wpid, tuple) else (wpid, ):
|
||||
if protocol > 1.0:
|
||||
assert w[0:1] == '4', name + ' has protocol ' + protocol + ', wpid ' + w
|
||||
else:
|
||||
if w[0:1] == '1':
|
||||
assert kind == _hidpp10.DEVICE_KIND.mouse, name + ' has protocol ' + protocol + ', wpid ' + w
|
||||
elif w[0:1] == '2':
|
||||
assert kind == _hidpp10.DEVICE_KIND.keyboard, name + ' has protocol ' + protocol + ', wpid ' + w
|
||||
|
||||
device_descriptor = _DeviceDescriptor(name=name, kind=kind,
|
||||
wpid=wpid, codename=codename, protocol=protocol,
|
||||
registers=registers, settings=settings)
|
||||
|
||||
assert codename not in DEVICES, 'duplicate codename in device descriptors: %s' % (DEVICES[codename], )
|
||||
DEVICES[codename] = device_descriptor
|
||||
|
||||
if wpid:
|
||||
if not isinstance(wpid, tuple):
|
||||
wpid = (wpid, )
|
||||
|
||||
for w in wpid:
|
||||
assert w not in DEVICES, 'duplicate wpid in device descriptors: %s' % (DEVICES[w], )
|
||||
DEVICES[w] = device_descriptor
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
_PERFORMANCE_MX_DPIS = _NamedInts.range(0x81, 0x8F, lambda x: str((x - 0x80) * 100))
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
# Some HID++1.0 registers and HID++2.0 features can be discovered at run-time,
|
||||
# so they are not specified here.
|
||||
#
|
||||
# For known registers, however, please do specify them here -- avoids
|
||||
# unnecessary communication with the device and makes it easier to make certain
|
||||
# decisions when querying the device's state.
|
||||
#
|
||||
# Specify a negative value to blacklist a certain register for a device.
|
||||
#
|
||||
# Usually, state registers (battery, leds, some features, etc) are only used by
|
||||
# HID++ 1.0 devices, while HID++ 2.0 devices use features for the same
|
||||
# functionalities. This is a rule that's been discovered by trial-and-error,
|
||||
# so it may change in the future.
|
||||
|
||||
# Well-known registers (in hex):
|
||||
# * 00 - notification flags (all devices)
|
||||
# 01 - mice: smooth scrolling
|
||||
# 07 - battery status
|
||||
# 09 - keyboards: FN swap (if it has the FN key)
|
||||
# 0D - battery charge
|
||||
# a device may have either the 07 or 0D register available;
|
||||
# no known device uses both
|
||||
# 51 - leds
|
||||
# 63 - mice: DPI
|
||||
# * F1 - firmware info
|
||||
# Some registers appear to be universally supported, no matter the HID++ version
|
||||
# (marked with *). The rest may or may not be supported, and their values may or
|
||||
# may not mean the same thing across different devices.
|
||||
|
||||
# The 'codename' and 'kind' fields are usually guessed from the device name,
|
||||
# but in some cases (like the Logitech Cube) that heuristic fails and they have
|
||||
# to be specified.
|
||||
#
|
||||
# The 'protocol' and 'wpid' fields are optional (they can be discovered at
|
||||
# runtime), but specifying them here speeds up device discovery and reduces the
|
||||
# USB traffic Solaar has to do to fully identify peripherals.
|
||||
# Same goes for HID++ 2.0 feature settings (like _feature_fn_swap).
|
||||
#
|
||||
# The 'registers' field indicates read-only registers, specifying a state. These
|
||||
# are valid (AFAIK) only to HID++ 1.0 devices.
|
||||
# The 'settings' field indicates a read/write register; based on them Solaar
|
||||
# generates, at runtime, the settings controls in the device panel. HID++ 1.0
|
||||
# devices may only have register-based settings; HID++ 2.0 devices may only have
|
||||
# feature-based settings.
|
||||
|
||||
# Keyboards
|
||||
|
||||
_D('Wireless Keyboard K230', protocol=2.0, wpid='400D')
|
||||
_D('Wireless Keyboard K270')
|
||||
_D('Wireless Keyboard MK330')
|
||||
_D('Wireless Keyboard K340')
|
||||
_D('Wireless Keyboard K350', wpid='200A')
|
||||
_D('Wireless Keyboard K360', protocol=2.0, wpid='4004',
|
||||
settings=[
|
||||
_FS.fn_swap()
|
||||
],
|
||||
)
|
||||
_D('Wireless Touch Keyboard K400', protocol=2.0, wpid=('400E', '4024'),
|
||||
settings=[
|
||||
_FS.fn_swap()
|
||||
],
|
||||
)
|
||||
_D('Wireless Keyboard MK520')
|
||||
_D('Wireless Keyboard MK550')
|
||||
_D('Wireless Keyboard MK700', protocol=1.0, wpid='2008',
|
||||
registers=(_R.battery_status, ),
|
||||
settings=[
|
||||
_RS.fn_swap(),
|
||||
],
|
||||
)
|
||||
_D('Wireless Solar Keyboard K750', protocol=2.0, wpid='4002',
|
||||
settings=[
|
||||
_FS.fn_swap()
|
||||
],
|
||||
)
|
||||
_D('Wireless Illuminated Keyboard K800', protocol=1.0, wpid='2010',
|
||||
registers=(_R.battery_status, _R.three_leds, ),
|
||||
settings=[
|
||||
_RS.fn_swap(),
|
||||
_RS.hand_detection(),
|
||||
],
|
||||
)
|
||||
|
||||
# Mice
|
||||
|
||||
_D('Wireless Mouse M175')
|
||||
_D('Wireless Mouse M185')
|
||||
_D('Wireless Mouse M187', protocol=2.0, wpid='4019')
|
||||
_D('Wireless Mouse M215', protocol=1.0, wpid='1020')
|
||||
_D('Wireless Mouse M235')
|
||||
_D('Wireless Mouse M305', protocol=1.0, wpid='101F',
|
||||
registers=(_R.battery_status, ),
|
||||
settings=[
|
||||
_RS.side_scroll(),
|
||||
],
|
||||
)
|
||||
_D('Wireless Mouse M310')
|
||||
_D('Wireless Mouse M315')
|
||||
_D('Wireless Mouse M317')
|
||||
_D('Wireless Mouse M325')
|
||||
_D('Wireless Mouse M345', protocol=2.0, wpid='4017')
|
||||
_D('Wireless Mouse M505', codename='M505/B605', protocol=1.0, wpid='101D',
|
||||
registers=(_R.battery_charge, ),
|
||||
settings=[
|
||||
_RS.smooth_scroll(),
|
||||
_RS.side_scroll(),
|
||||
],
|
||||
)
|
||||
_D('Wireless Mouse M510', protocol=1.0, wpid='1025',
|
||||
registers=(_R.battery_status, ),
|
||||
settings=[
|
||||
_RS.smooth_scroll(),
|
||||
_RS.side_scroll(),
|
||||
],
|
||||
)
|
||||
_D('Couch Mouse M515', protocol=2.0, wpid='4007')
|
||||
_D('Wireless Mouse M525', protocol=2.0, wpid='4013')
|
||||
_D('Touch Mouse M600', protocol=2.0, wpid='401A')
|
||||
_D('Marathon Mouse M705', protocol=1.0, wpid='101B',
|
||||
registers=(_R.battery_charge, ),
|
||||
settings=[
|
||||
_RS.smooth_scroll(),
|
||||
_RS.side_scroll(),
|
||||
],
|
||||
)
|
||||
_D('Zone Touch Mouse T400')
|
||||
_D('Touch Mouse T620', protocol=2.0)
|
||||
_D('Logitech Cube', kind=_hidpp10.DEVICE_KIND.mouse, protocol=2.0)
|
||||
_D('Anywhere Mouse MX', codename='Anywhere MX', protocol=1.0, wpid='1017',
|
||||
registers=(_R.battery_charge, ),
|
||||
settings=[
|
||||
_RS.smooth_scroll(),
|
||||
_RS.side_scroll(),
|
||||
],
|
||||
)
|
||||
_D('Performance Mouse MX', codename='Performance MX', protocol=1.0, wpid='101A',
|
||||
registers=(_R.battery_status, _R.three_leds, ),
|
||||
settings=[
|
||||
_RS.dpi(choices=_PERFORMANCE_MX_DPIS),
|
||||
_RS.smooth_scroll(),
|
||||
_RS.side_scroll(),
|
||||
],
|
||||
)
|
||||
|
||||
# Trackballs
|
||||
|
||||
_D('Wireless Trackball M570')
|
||||
|
||||
# Touchpads
|
||||
|
||||
_D('Wireless Rechargeable Touchpad T650', protocol=2.0, wpid='4101')
|
||||
_D('Wireless Touchpad', codename='Wireless Touch', protocol=2.0, wpid='4011')
|
||||
|
||||
#
|
||||
# Classic Nano peripherals (that don't support the Unifying protocol).
|
||||
# A wpid is necessary to properly identify them.
|
||||
#
|
||||
|
||||
_D('VX Nano Cordless Laser Mouse', codename='VX Nano', protocol=1.0, wpid='100F',
|
||||
registers=(_R.battery_charge, ),
|
||||
settings=[
|
||||
_RS.smooth_scroll(),
|
||||
_RS.side_scroll(),
|
||||
],
|
||||
)
|
||||
_D('V450 Nano Cordless Laser Mouse', codename='V450 Nano', protocol=1.0, wpid='1011',
|
||||
registers=(_R.battery_charge, ),
|
||||
)
|
||||
_D('V550 Nano Cordless Laser Mouse', codename='V550 Nano', protocol=1.0, wpid='1013',
|
||||
registers=(_R.battery_charge, ),
|
||||
settings=[
|
||||
_RS.smooth_scroll(),
|
||||
_RS.side_scroll(),
|
||||
],
|
||||
)
|
||||
329
lib/logitech_receiver/hidpp10.py
Normal file
329
lib/logitech_receiver/hidpp10.py
Normal file
@@ -0,0 +1,329 @@
|
||||
# -*- python-mode -*-
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
## Copyright (C) 2012-2013 Daniel Pavel
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published by
|
||||
## the Free Software Foundation; either version 2 of the License, or
|
||||
## (at your option) any later version.
|
||||
##
|
||||
## This program is distributed in the hope that it will be useful,
|
||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
## GNU General Public License for more details.
|
||||
##
|
||||
## You should have received a copy of the GNU General Public License along
|
||||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
from logging import getLogger # , DEBUG as _DEBUG
|
||||
_log = getLogger(__name__)
|
||||
del getLogger
|
||||
|
||||
|
||||
from .common import (strhex as _strhex,
|
||||
bytes2int as _bytes2int,
|
||||
int2bytes as _int2bytes,
|
||||
NamedInts as _NamedInts,
|
||||
FirmwareInfo as _FirmwareInfo)
|
||||
from .hidpp20 import FIRMWARE_KIND, BATTERY_STATUS
|
||||
|
||||
#
|
||||
# Constants - most of them as defined by the official Logitech HID++ 1.0
|
||||
# documentation, some of them guessed.
|
||||
#
|
||||
|
||||
DEVICE_KIND = _NamedInts(
|
||||
keyboard=0x01,
|
||||
mouse=0x02,
|
||||
numpad=0x03,
|
||||
presenter=0x04,
|
||||
trackball=0x08,
|
||||
touchpad=0x09)
|
||||
|
||||
POWER_SWITCH_LOCATION = _NamedInts(
|
||||
base=0x01,
|
||||
top_case=0x02,
|
||||
edge_of_top_right_corner=0x03,
|
||||
top_left_corner=0x05,
|
||||
bottom_left_corner=0x06,
|
||||
top_right_corner=0x07,
|
||||
bottom_right_corner=0x08,
|
||||
top_edge=0x09,
|
||||
right_edge=0x0A,
|
||||
left_edge=0x0B,
|
||||
bottom_edge=0x0C)
|
||||
|
||||
# Some flags are used both by devices and receivers. The Logitech documentation
|
||||
# mentions that the first and last (third) byte are used for devices while the
|
||||
# second is used for the receiver. In practise, the second byte is also used for
|
||||
# some device-specific notifications (keyboard illumination level). Do not
|
||||
# simply set all notification bits if the software does not support it. For
|
||||
# example, enabling keyboard_sleep_raw makes the Sleep key a no-operation unless
|
||||
# the software is updated to handle that event.
|
||||
# Observations:
|
||||
# - wireless and software present were seen on receivers, reserved_r1b4 as well
|
||||
# - the rest work only on devices as far as we can tell right now
|
||||
# In the future would be useful to have separate enums for receiver and device notification flags,
|
||||
# but right now we don't know enough.
|
||||
NOTIFICATION_FLAG = _NamedInts(
|
||||
battery_status= 0x100000, # send battery charge notifications (0x07 or 0x0D)
|
||||
keyboard_sleep_raw= 0x020000, # system control keys such as Sleep
|
||||
keyboard_multimedia_raw=0x010000, # consumer controls such as Mute and Calculator
|
||||
# reserved_r1b4= 0x001000, # unknown, seen on a unifying receiver
|
||||
software_present= 0x000800, # .. no idea
|
||||
keyboard_illumination= 0x000200, # illumination brightness level changes (by pressing keys)
|
||||
wireless= 0x000100, # notify when the device wireless goes on/off-line
|
||||
)
|
||||
|
||||
ERROR = _NamedInts(
|
||||
invalid_SubID__command=0x01,
|
||||
invalid_address=0x02,
|
||||
invalid_value=0x03,
|
||||
connection_request_failed=0x04,
|
||||
too_many_devices=0x05,
|
||||
already_exists=0x06,
|
||||
busy=0x07,
|
||||
unknown_device=0x08,
|
||||
resource_error=0x09,
|
||||
request_unavailable=0x0A,
|
||||
unsupported_parameter_value=0x0B,
|
||||
wrong_pin_code=0x0C)
|
||||
|
||||
PAIRING_ERRORS = _NamedInts(
|
||||
device_timeout=0x01,
|
||||
device_not_supported=0x02,
|
||||
too_many_devices=0x03,
|
||||
sequence_timeout=0x06)
|
||||
|
||||
BATTERY_APPOX = _NamedInts(
|
||||
empty = 0,
|
||||
critical = 5,
|
||||
low = 20,
|
||||
good = 50,
|
||||
full = 90)
|
||||
|
||||
"""Known registers.
|
||||
Devices usually have a (small) sub-set of these. Some registers are only
|
||||
applicable to certain device kinds (e.g. smooth_scroll only applies to mice."""
|
||||
REGISTERS = _NamedInts(
|
||||
# only apply to receivers
|
||||
receiver_connection=0x02,
|
||||
receiver_pairing=0xB2,
|
||||
devices_activity=0x2B3,
|
||||
receiver_info=0x2B5,
|
||||
|
||||
# only apply to devices
|
||||
mouse_button_flags=0x01,
|
||||
keyboard_hand_detection=0x01,
|
||||
battery_status=0x07,
|
||||
keyboard_fn_swap=0x09,
|
||||
battery_charge=0x0D,
|
||||
keyboard_illumination=0x17,
|
||||
three_leds=0x51,
|
||||
mouse_dpi=0x63,
|
||||
|
||||
# apply to both
|
||||
notifications=0x00,
|
||||
firmware=0xF1,
|
||||
)
|
||||
|
||||
#
|
||||
# functions
|
||||
#
|
||||
|
||||
def read_register(device, register_number, *params):
|
||||
assert device, 'tried to read register %02X from invalid device %s' % (register_number, device)
|
||||
# support long registers by adding a 2 in front of the register number
|
||||
request_id = 0x8100 | (int(register_number) & 0x2FF)
|
||||
return device.request(request_id, *params)
|
||||
|
||||
|
||||
def write_register(device, register_number, *value):
|
||||
assert device, 'tried to write register %02X to invalid device %s' % (register_number, device)
|
||||
# support long registers by adding a 2 in front of the register number
|
||||
request_id = 0x8000 | (int(register_number) & 0x2FF)
|
||||
return device.request(request_id, *value)
|
||||
|
||||
|
||||
def get_battery(device):
|
||||
assert device
|
||||
assert device.kind is not None
|
||||
if not device.online:
|
||||
return
|
||||
|
||||
"""Reads a device's battery level, if provided by the HID++ 1.0 protocol."""
|
||||
if device.protocol and device.protocol >= 2.0:
|
||||
# let's just assume HID++ 2.0 devices do not provide the battery info in a register
|
||||
return
|
||||
|
||||
for r in (REGISTERS.battery_status, REGISTERS.battery_charge):
|
||||
if r in device.registers:
|
||||
reply = read_register(device, r)
|
||||
if reply:
|
||||
return parse_battery_status(r, reply)
|
||||
return
|
||||
|
||||
# the descriptor does not tell us which register this device has, try them both
|
||||
reply = read_register(device, REGISTERS.battery_charge)
|
||||
if reply:
|
||||
# remember this for the next time
|
||||
device.registers.append(REGISTERS.battery_charge)
|
||||
return parse_battery_status(REGISTERS.battery_charge, reply)
|
||||
|
||||
reply = read_register(device, REGISTERS.battery_status)
|
||||
if reply:
|
||||
# remember this for the next time
|
||||
device.registers.append(REGISTERS.battery_status)
|
||||
return parse_battery_status(REGISTERS.battery_status, reply)
|
||||
|
||||
|
||||
def parse_battery_status(register, reply):
|
||||
if register == REGISTERS.battery_charge:
|
||||
charge = ord(reply[:1])
|
||||
status_byte = ord(reply[2:3]) & 0xF0
|
||||
status_text = (BATTERY_STATUS.discharging if status_byte == 0x30
|
||||
else BATTERY_STATUS.recharging if status_byte == 0x50
|
||||
else BATTERY_STATUS.full if status_byte == 0x90
|
||||
else None)
|
||||
return charge, status_text
|
||||
|
||||
if register == REGISTERS.battery_status:
|
||||
status_byte = ord(reply[:1])
|
||||
charge = (BATTERY_APPOX.full if status_byte == 7 # full
|
||||
else BATTERY_APPOX.good if status_byte == 5 # good
|
||||
else BATTERY_APPOX.low if status_byte == 3 # low
|
||||
else BATTERY_APPOX.critical if status_byte == 1 # critical
|
||||
# pure 'charging' notifications may come without a status
|
||||
else BATTERY_APPOX.empty)
|
||||
|
||||
charging_byte = ord(reply[1:2])
|
||||
if charging_byte == 0x00:
|
||||
status_text = BATTERY_STATUS.discharging
|
||||
elif charging_byte & 0x21 == 0x21:
|
||||
status_text = BATTERY_STATUS.recharging
|
||||
elif charging_byte & 0x22 == 0x22:
|
||||
status_text = BATTERY_STATUS.full
|
||||
else:
|
||||
_log.warn("could not parse 0x07 battery status: %02X (level %02X)", charging_byte, status_byte)
|
||||
status_text = None
|
||||
|
||||
if charging_byte & 0x03 and status_byte == 0:
|
||||
# some 'charging' notifications may come with no battery level information
|
||||
charge = None
|
||||
|
||||
return charge, status_text
|
||||
|
||||
|
||||
def get_firmware(device):
|
||||
assert device
|
||||
|
||||
firmware = [None, None, None]
|
||||
|
||||
reply = read_register(device, REGISTERS.firmware, 0x01)
|
||||
if not reply:
|
||||
# won't be able to read any of it now...
|
||||
return
|
||||
|
||||
fw_version = _strhex(reply[1:3])
|
||||
fw_version = '%s.%s' % (fw_version[0:2], fw_version[2:4])
|
||||
reply = read_register(device, REGISTERS.firmware, 0x02)
|
||||
if reply:
|
||||
fw_version += '.B' + _strhex(reply[1:3])
|
||||
fw = _FirmwareInfo(FIRMWARE_KIND.Firmware, '', fw_version, None)
|
||||
firmware[0] = fw
|
||||
|
||||
reply = read_register(device, REGISTERS.firmware, 0x04)
|
||||
if reply:
|
||||
bl_version = _strhex(reply[1:3])
|
||||
bl_version = '%s.%s' % (bl_version[0:2], bl_version[2:4])
|
||||
bl = _FirmwareInfo(FIRMWARE_KIND.Bootloader, '', bl_version, None)
|
||||
firmware[1] = bl
|
||||
|
||||
reply = read_register(device, REGISTERS.firmware, 0x03)
|
||||
if reply:
|
||||
o_version = _strhex(reply[1:3])
|
||||
o_version = '%s.%s' % (o_version[0:2], o_version[2:4])
|
||||
o = _FirmwareInfo(FIRMWARE_KIND.Other, '', o_version, None)
|
||||
firmware[2] = o
|
||||
|
||||
if any(firmware):
|
||||
return tuple(f for f in firmware if f)
|
||||
|
||||
|
||||
def set_3leds(device, battery_level=None, charging=None, warning=None):
|
||||
assert device
|
||||
assert device.kind is not None
|
||||
if not device.online:
|
||||
return
|
||||
|
||||
if REGISTERS.three_leds not in device.registers:
|
||||
return
|
||||
|
||||
if battery_level is not None:
|
||||
if battery_level < BATTERY_APPOX.critical:
|
||||
# 1 orange, and force blink
|
||||
v1, v2 = 0x22, 0x00
|
||||
warning = True
|
||||
elif battery_level < BATTERY_APPOX.low:
|
||||
# 1 orange
|
||||
v1, v2 = 0x22, 0x00
|
||||
elif battery_level < BATTERY_APPOX.good:
|
||||
# 1 green
|
||||
v1, v2 = 0x20, 0x00
|
||||
elif battery_level < BATTERY_APPOX.full:
|
||||
# 2 greens
|
||||
v1, v2 = 0x20, 0x02
|
||||
else:
|
||||
# all 3 green
|
||||
v1, v2 = 0x20, 0x22
|
||||
if warning:
|
||||
# set the blinking flag for the leds already set
|
||||
v1 |= (v1 >> 1)
|
||||
v2 |= (v2 >> 1)
|
||||
elif charging:
|
||||
# blink all green
|
||||
v1, v2 = 0x30,0x33
|
||||
elif warning:
|
||||
# 1 red
|
||||
v1, v2 = 0x02, 0x00
|
||||
else:
|
||||
# turn off all leds
|
||||
v1, v2 = 0x11, 0x11
|
||||
|
||||
write_register(device, REGISTERS.three_leds, v1, v2)
|
||||
|
||||
|
||||
def get_notification_flags(device):
|
||||
assert device
|
||||
|
||||
# Avoid a call if the device is not online,
|
||||
# or the device does not support registers.
|
||||
if device.kind is not None:
|
||||
# peripherals with protocol >= 2.0 don't support registers
|
||||
if device.protocol and device.protocol >= 2.0:
|
||||
return
|
||||
|
||||
flags = read_register(device, REGISTERS.notifications)
|
||||
if flags is not None:
|
||||
assert len(flags) == 3
|
||||
return _bytes2int(flags)
|
||||
|
||||
|
||||
def set_notification_flags(device, *flag_bits):
|
||||
assert device
|
||||
|
||||
# Avoid a call if the device is not online,
|
||||
# or the device does not support registers.
|
||||
if device.kind is not None:
|
||||
# peripherals with protocol >= 2.0 don't support registers
|
||||
if device.protocol and device.protocol >= 2.0:
|
||||
return
|
||||
|
||||
flag_bits = sum(int(b) for b in flag_bits)
|
||||
assert flag_bits & 0x00FFFFFF == flag_bits
|
||||
result = write_register(device, REGISTERS.notifications, _int2bytes(flag_bits, 3))
|
||||
return result is not None
|
||||
429
lib/logitech_receiver/hidpp20.py
Normal file
429
lib/logitech_receiver/hidpp20.py
Normal file
@@ -0,0 +1,429 @@
|
||||
# -*- python-mode -*-
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
## Copyright (C) 2012-2013 Daniel Pavel
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published by
|
||||
## the Free Software Foundation; either version 2 of the License, or
|
||||
## (at your option) any later version.
|
||||
##
|
||||
## This program is distributed in the hope that it will be useful,
|
||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
## GNU General Public License for more details.
|
||||
##
|
||||
## You should have received a copy of the GNU General Public License along
|
||||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
# Logitech Unifying Receiver API.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
from logging import getLogger, DEBUG as _DEBUG
|
||||
_log = getLogger(__name__)
|
||||
del getLogger
|
||||
|
||||
|
||||
from .common import (FirmwareInfo as _FirmwareInfo,
|
||||
ReprogrammableKeyInfo as _ReprogrammableKeyInfo,
|
||||
KwException as _KwException,
|
||||
NamedInts as _NamedInts,
|
||||
pack as _pack,
|
||||
unpack as _unpack)
|
||||
from . import special_keys
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
"""Possible features available on a Logitech device.
|
||||
|
||||
A particular device might not support all these features, and may support other
|
||||
unknown features as well.
|
||||
"""
|
||||
FEATURE = _NamedInts(
|
||||
ROOT=0x0000,
|
||||
FEATURE_SET=0x0001,
|
||||
FEATURE_INFO=0x0002,
|
||||
DEVICE_FW_VERSION=0x0003,
|
||||
DEVICE_NAME=0x0005,
|
||||
DEVICE_GROUPS=0x0006,
|
||||
DFUCONTROL=0x00C0,
|
||||
BATTERY_STATUS=0x1000,
|
||||
BACKLIGHT=0x1981,
|
||||
REPROG_CONTROLS=0x1B00,
|
||||
REPROG_CONTROLS_V2=0x1B01,
|
||||
REPROG_CONTROLS_V3=0x1B03,
|
||||
WIRELESS_DEVICE_STATUS=0x1D4B,
|
||||
LEFT_RIGHT_SWAP=0x2001,
|
||||
VERTICAL_SCROLLING=0x2100,
|
||||
HI_RES_SCROLLING=0x2120,
|
||||
MOUSE_POINTER=0x2200,
|
||||
FN_INVERSION=0x40A0,
|
||||
NEW_FN_INVERSION=0x40A2,
|
||||
ENCRYPTION=0x4100,
|
||||
SOLAR_DASHBOARD=0x4301,
|
||||
KEYBOARD_LAYOUT=0x4520,
|
||||
TOUCHPAD_FW_ITEMS=0x6010,
|
||||
TOUCHPAD_SW_ITEMS=0x6011,
|
||||
TOUCHPAD_WIN8_FW_ITEMS=0x6012,
|
||||
TOUCHPAD_RAW_XY=0x6100,
|
||||
TOUCHMOUSE_RAW_POINTS=0x6110,
|
||||
)
|
||||
FEATURE._fallback = lambda x: 'unknown:%04X' % x
|
||||
|
||||
FEATURE_FLAG = _NamedInts(
|
||||
internal=0x20,
|
||||
hidden=0x40,
|
||||
obsolete=0x80)
|
||||
|
||||
DEVICE_KIND = _NamedInts(
|
||||
keyboard=0x00,
|
||||
remote_control=0x01,
|
||||
numpad=0x02,
|
||||
mouse=0x03,
|
||||
touchpad=0x04,
|
||||
trackball=0x05,
|
||||
presenter=0x06,
|
||||
receiver=0x07)
|
||||
|
||||
FIRMWARE_KIND = _NamedInts(
|
||||
Firmware=0x00,
|
||||
Bootloader=0x01,
|
||||
Hardware=0x02,
|
||||
Other=0x03)
|
||||
|
||||
BATTERY_OK = lambda status: status not in (BATTERY_STATUS.invalid_battery, BATTERY_STATUS.thermal_error)
|
||||
|
||||
BATTERY_STATUS = _NamedInts(
|
||||
discharging=0x00,
|
||||
recharging=0x01,
|
||||
almost_full=0x02,
|
||||
full=0x03,
|
||||
slow_recharge=0x04,
|
||||
invalid_battery=0x05,
|
||||
thermal_error=0x06)
|
||||
|
||||
ERROR = _NamedInts(
|
||||
unknown=0x01,
|
||||
invalid_argument=0x02,
|
||||
out_of_range=0x03,
|
||||
hardware_error=0x04,
|
||||
logitech_internal=0x05,
|
||||
invalid_feature_index=0x06,
|
||||
invalid_function=0x07,
|
||||
busy=0x08,
|
||||
unsupported=0x09)
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
class FeatureNotSupported(_KwException):
|
||||
"""Raised when trying to request a feature not supported by the device."""
|
||||
pass
|
||||
|
||||
class FeatureCallError(_KwException):
|
||||
"""Raised if the device replied to a feature call with an error."""
|
||||
pass
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
class FeaturesArray(object):
|
||||
"""A sequence of features supported by a HID++ 2.0 device."""
|
||||
__slots__ = ('supported', 'device', 'features')
|
||||
assert FEATURE.ROOT == 0x0000
|
||||
|
||||
def __init__(self, device):
|
||||
assert device is not None
|
||||
self.device = device
|
||||
self.supported = True
|
||||
self.features = None
|
||||
|
||||
def __del__(self):
|
||||
self.supported = False
|
||||
self.device = None
|
||||
self.features = None
|
||||
|
||||
def _check(self):
|
||||
# print (self.device, "check", self.supported, self.features, self.device.protocol)
|
||||
if self.supported:
|
||||
assert self.device
|
||||
if self.features is not None:
|
||||
return True
|
||||
|
||||
if not self.device.online:
|
||||
# device is not connected right now, will have to try later
|
||||
return False
|
||||
|
||||
# I _think_ this is universally true
|
||||
if self.device.protocol and self.device.protocol < 2.0:
|
||||
self.supported = False
|
||||
self.device.features = None
|
||||
self.device = None
|
||||
return False
|
||||
|
||||
reply = self.device.request(0x0000, _pack('!H', FEATURE.FEATURE_SET))
|
||||
if reply is None:
|
||||
self.supported = False
|
||||
else:
|
||||
fs_index = ord(reply[0:1])
|
||||
if fs_index:
|
||||
count = self.device.request(fs_index << 8)
|
||||
if count is None:
|
||||
_log.warn("FEATURE_SET found, but failed to read features count")
|
||||
# most likely the device is unavailable
|
||||
return False
|
||||
else:
|
||||
count = ord(count[:1])
|
||||
assert count >= fs_index
|
||||
self.features = [None] * (1 + count)
|
||||
self.features[0] = FEATURE.ROOT
|
||||
self.features[fs_index] = FEATURE.FEATURE_SET
|
||||
return True
|
||||
else:
|
||||
self.supported = False
|
||||
|
||||
return False
|
||||
|
||||
__bool__ = __nonzero__ = _check
|
||||
|
||||
def __getitem__(self, index):
|
||||
if self._check():
|
||||
if isinstance(index, int):
|
||||
if index < 0 or index >= len(self.features):
|
||||
raise IndexError(index)
|
||||
|
||||
if self.features[index] is None:
|
||||
feature = self.device.feature_request(FEATURE.FEATURE_SET, 0x10, index)
|
||||
if feature:
|
||||
feature, = _unpack('!H', feature[:2])
|
||||
self.features[index] = FEATURE[feature]
|
||||
|
||||
return self.features[index]
|
||||
|
||||
elif isinstance(index, slice):
|
||||
indices = index.indices(len(self.features))
|
||||
return [self.__getitem__(i) for i in range(*indices)]
|
||||
|
||||
def __contains__(self, value):
|
||||
if self._check():
|
||||
ivalue = int(value)
|
||||
|
||||
may_have = False
|
||||
for f in self.features:
|
||||
if f is None:
|
||||
may_have = True
|
||||
elif ivalue == int(f):
|
||||
return True
|
||||
elif ivalue < int(f):
|
||||
break
|
||||
|
||||
if may_have:
|
||||
reply = self.device.request(0x0000, _pack('!H', ivalue))
|
||||
if reply:
|
||||
index = ord(reply[0:1])
|
||||
if index:
|
||||
self.features[index] = FEATURE[ivalue]
|
||||
return True
|
||||
|
||||
def index(self, value):
|
||||
if self._check():
|
||||
may_have = False
|
||||
ivalue = int(value)
|
||||
for index, f in enumerate(self.features):
|
||||
if f is None:
|
||||
may_have = True
|
||||
elif ivalue == int(f):
|
||||
return index
|
||||
elif ivalue < int(f):
|
||||
raise ValueError("%r not in list" % value)
|
||||
|
||||
if may_have:
|
||||
reply = self.device.request(0x0000, _pack('!H', ivalue))
|
||||
if reply:
|
||||
index = ord(reply[0:1])
|
||||
self.features[index] = FEATURE[ivalue]
|
||||
return index
|
||||
|
||||
raise ValueError("%r not in list" % value)
|
||||
|
||||
def __iter__(self):
|
||||
if self._check():
|
||||
yield FEATURE.ROOT
|
||||
index = 1
|
||||
last_index = len(self.features)
|
||||
while index < last_index:
|
||||
yield self.__getitem__(index)
|
||||
index += 1
|
||||
|
||||
def __len__(self):
|
||||
return len(self.features) if self._check() else 0
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
class KeysArray(object):
|
||||
"""A sequence of key mappings supported by a HID++ 2.0 device."""
|
||||
__slots__ = ('device', 'keys')
|
||||
|
||||
def __init__(self, device, count):
|
||||
assert device is not None
|
||||
self.device = device
|
||||
self.keys = [None] * count
|
||||
|
||||
def __getitem__(self, index):
|
||||
if isinstance(index, int):
|
||||
if index < 0 or index >= len(self.keys):
|
||||
raise IndexError(index)
|
||||
|
||||
if self.keys[index] is None:
|
||||
keydata = feature_request(self.device, FEATURE.REPROG_CONTROLS, 0x10, index)
|
||||
if keydata:
|
||||
key, key_task, flags = _unpack('!HHB', keydata[:5])
|
||||
ctrl_id_text = special_keys.CONTROL[key]
|
||||
ctrl_task_text = special_keys.TASK[key_task]
|
||||
self.keys[index] = _ReprogrammableKeyInfo(index, ctrl_id_text, ctrl_task_text, flags)
|
||||
|
||||
return self.keys[index]
|
||||
|
||||
elif isinstance(index, slice):
|
||||
indices = index.indices(len(self.keys))
|
||||
return [self.__getitem__(i) for i in range(*indices)]
|
||||
|
||||
def index(self, value):
|
||||
for index, k in enumerate(self.keys):
|
||||
if k is not None and int(value) == int(k.key):
|
||||
return index
|
||||
|
||||
for index, k in enumerate(self.keys):
|
||||
if k is None:
|
||||
k = self.__getitem__(index)
|
||||
if k is not None:
|
||||
return index
|
||||
|
||||
def __iter__(self):
|
||||
for k in range(0, len(self.keys)):
|
||||
yield self.__getitem__(k)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.keys)
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def feature_request(device, feature, function=0x00, *params):
|
||||
if device.online and device.features:
|
||||
if feature in device.features:
|
||||
feature_index = device.features.index(int(feature))
|
||||
return device.request((feature_index << 8) + (function & 0xFF), *params)
|
||||
|
||||
|
||||
def get_firmware(device):
|
||||
"""Reads a device's firmware info.
|
||||
|
||||
:returns: a list of FirmwareInfo tuples, ordered by firmware layer.
|
||||
"""
|
||||
count = feature_request(device, FEATURE.DEVICE_FW_VERSION)
|
||||
if count:
|
||||
count = ord(count[:1])
|
||||
|
||||
fw = []
|
||||
for index in range(0, count):
|
||||
fw_info = feature_request(device, FEATURE.DEVICE_FW_VERSION, 0x10, index)
|
||||
if fw_info:
|
||||
level = ord(fw_info[:1]) & 0x0F
|
||||
if level == 0 or level == 1:
|
||||
name, version_major, version_minor, build = _unpack('!3sBBH', fw_info[1:8])
|
||||
version = '%02X.%02X' % (version_major, version_minor)
|
||||
if build:
|
||||
version += '.B%04X' % build
|
||||
extras = fw_info[9:].rstrip(b'\x00') or None
|
||||
fw_info = _FirmwareInfo(FIRMWARE_KIND[level], name.decode('ascii'), version, extras)
|
||||
elif level == FIRMWARE_KIND.Hardware:
|
||||
fw_info = _FirmwareInfo(FIRMWARE_KIND.Hardware, '', str(ord(fw_info[1:2])), None)
|
||||
else:
|
||||
fw_info = _FirmwareInfo(FIRMWARE_KIND.Other, '', '', None)
|
||||
|
||||
fw.append(fw_info)
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("device %d firmware %s", devnumber, fw_info)
|
||||
return tuple(fw)
|
||||
|
||||
|
||||
def get_kind(device):
|
||||
"""Reads a device's type.
|
||||
|
||||
:see DEVICE_KIND:
|
||||
:returns: a string describing the device type, or ``None`` if the device is
|
||||
not available or does not support the ``DEVICE_NAME`` feature.
|
||||
"""
|
||||
kind = feature_request(device, FEATURE.DEVICE_NAME, 0x20)
|
||||
if kind:
|
||||
kind = ord(kind[:1])
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("device %d type %d = %s", devnumber, kind, DEVICE_KIND[kind])
|
||||
return DEVICE_KIND[kind]
|
||||
|
||||
|
||||
def get_name(device):
|
||||
"""Reads a device's name.
|
||||
|
||||
:returns: a string with the device name, or ``None`` if the device is not
|
||||
available or does not support the ``DEVICE_NAME`` feature.
|
||||
"""
|
||||
name_length = feature_request(device, FEATURE.DEVICE_NAME)
|
||||
if name_length:
|
||||
name_length = ord(name_length[:1])
|
||||
|
||||
name = b''
|
||||
while len(name) < name_length:
|
||||
fragment = feature_request(device, FEATURE.DEVICE_NAME, 0x10, len(name))
|
||||
if fragment:
|
||||
name += fragment[:name_length - len(name)]
|
||||
else:
|
||||
_log.error("failed to read whole name of %s (expected %d chars)", device, name_length)
|
||||
return None
|
||||
|
||||
return name.decode('ascii')
|
||||
|
||||
|
||||
def get_battery(device):
|
||||
"""Reads a device's battery level.
|
||||
|
||||
:raises FeatureNotSupported: if the device does not support this feature.
|
||||
"""
|
||||
battery = feature_request(device, FEATURE.BATTERY_STATUS)
|
||||
if battery:
|
||||
discharge, dischargeNext, status = _unpack('!BBB', battery[:3])
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("device %d battery %d%% charged, next level %d%% charge, status %d = %s",
|
||||
device.number, discharge, dischargeNext, status, BATTERY_STATUS[status])
|
||||
return discharge, BATTERY_STATUS[status]
|
||||
|
||||
|
||||
def get_keys(device):
|
||||
count = feature_request(device, FEATURE.REPROG_CONTROLS)
|
||||
if count:
|
||||
return KeysArray(device, ord(count[:1]))
|
||||
|
||||
|
||||
def get_mouse_pointer_info(device):
|
||||
pointer_info = feature_request(device, FEATURE.MOUSE_POINTER)
|
||||
if pointer_info:
|
||||
dpi, flags = _unpack('!HB', pointer_info[:3])
|
||||
acceleration = ('none', 'low', 'med', 'high')[flags & 0x3]
|
||||
suggest_os_ballistics = (flags & 0x04) != 0
|
||||
suggest_vertical_orientation = (flags & 0x08) != 0
|
||||
return {
|
||||
'dpi': dpi,
|
||||
'acceleration': acceleration,
|
||||
'suggest_os_ballistics': suggest_os_ballistics,
|
||||
'suggest_vertical_orientation': suggest_vertical_orientation
|
||||
}
|
||||
50
lib/logitech_receiver/i18n.py
Normal file
50
lib/logitech_receiver/i18n.py
Normal file
@@ -0,0 +1,50 @@
|
||||
# -*- python-mode -*-
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
## Copyright (C) 2012-2013 Daniel Pavel
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published by
|
||||
## the Free Software Foundation; either version 2 of the License, or
|
||||
## (at your option) any later version.
|
||||
##
|
||||
## This program is distributed in the hope that it will be useful,
|
||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
## GNU General Public License for more details.
|
||||
##
|
||||
## You should have received a copy of the GNU General Public License along
|
||||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
# Translation support for the Logitech receivers library
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
import gettext as _gettext
|
||||
|
||||
|
||||
try:
|
||||
unicode
|
||||
_ = lambda x: _gettext.gettext(x).decode('UTF-8')
|
||||
except:
|
||||
_ = _gettext.gettext
|
||||
|
||||
|
||||
# A few common strings, not always accessible as such in the code.
|
||||
|
||||
_DUMMY = (
|
||||
# approximative battery levels
|
||||
_("empty"), _("critical"), _("low"), _("good"), _("full"),
|
||||
|
||||
# battery charging statuses
|
||||
_("discharging"), _("recharging"), _("almost full"), _("full"),
|
||||
_("slow recharge"), _("invalid battery"), _("thermal error"),
|
||||
|
||||
# pairing errors
|
||||
_("device timeout"), _("device not supported"), _("too many devices"), _("sequence timeout"),
|
||||
|
||||
# firmware kinds
|
||||
_("Firmware"), _("Bootloader"), _("Hardware"), _("Other"),
|
||||
|
||||
)
|
||||
229
lib/logitech_receiver/listener.py
Normal file
229
lib/logitech_receiver/listener.py
Normal file
@@ -0,0 +1,229 @@
|
||||
# -*- python-mode -*-
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
## Copyright (C) 2012-2013 Daniel Pavel
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published by
|
||||
## the Free Software Foundation; either version 2 of the License, or
|
||||
## (at your option) any later version.
|
||||
##
|
||||
## This program is distributed in the hope that it will be useful,
|
||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
## GNU General Public License for more details.
|
||||
##
|
||||
## You should have received a copy of the GNU General Public License along
|
||||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
import threading as _threading
|
||||
# from time import time as _timestamp
|
||||
|
||||
# for both Python 2 and 3
|
||||
try:
|
||||
from Queue import Queue as _Queue
|
||||
except ImportError:
|
||||
from queue import Queue as _Queue
|
||||
|
||||
from logging import getLogger, DEBUG as _DEBUG, INFO as _INFO
|
||||
_log = getLogger(__name__)
|
||||
del getLogger
|
||||
|
||||
|
||||
from . import base as _base
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
class _ThreadedHandle(object):
|
||||
"""A thread-local wrapper with different open handles for each thread.
|
||||
|
||||
Closing a ThreadedHandle will close all handles.
|
||||
"""
|
||||
|
||||
__slots__ = ('path', '_local', '_handles', '_listener')
|
||||
|
||||
def __init__(self, listener, path, handle):
|
||||
assert listener is not None
|
||||
assert path is not None
|
||||
assert handle is not None
|
||||
assert isinstance(handle, int)
|
||||
|
||||
self._listener = listener
|
||||
self.path = path
|
||||
self._local = _threading.local()
|
||||
# take over the current handle for the thread doing the replacement
|
||||
self._local.handle = handle
|
||||
self._handles = [handle]
|
||||
|
||||
def _open(self):
|
||||
handle = _base.open_path(self.path)
|
||||
if handle is None:
|
||||
_log.error("%r failed to open new handle", self)
|
||||
else:
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("%r opened new handle %d", self, handle)
|
||||
self._local.handle = handle
|
||||
self._handles.append(handle)
|
||||
return handle
|
||||
|
||||
def close(self):
|
||||
if self._local:
|
||||
self._local = None
|
||||
handles, self._handles = self._handles, []
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("%r closing %s", self, handles)
|
||||
for h in handles:
|
||||
_base.close(h)
|
||||
|
||||
@property
|
||||
def notifications_hook(self):
|
||||
if self._listener:
|
||||
assert isinstance(self._listener, _threading.Thread)
|
||||
if _threading.current_thread() == self._listener:
|
||||
return self._listener._notifications_hook
|
||||
|
||||
def __del__(self):
|
||||
self._listener = None
|
||||
self.close()
|
||||
|
||||
def __index__(self):
|
||||
if self._local:
|
||||
try:
|
||||
return self._local.handle
|
||||
except:
|
||||
return self._open()
|
||||
__int__ = __index__
|
||||
|
||||
def __str__(self):
|
||||
if self._local:
|
||||
return str(int(self))
|
||||
__unicode__ = __str__
|
||||
|
||||
def __repr__(self):
|
||||
return '<_ThreadedHandle(%s)>' % self.path
|
||||
|
||||
def __bool__(self):
|
||||
return bool(self._local)
|
||||
__nonzero__ = __bool__
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
# How long to wait during a read for the next packet, in seconds
|
||||
# Ideally this should be rather long (10s ?), but the read is blocking
|
||||
# and this means that when the thread is signalled to stop, it would take
|
||||
# a while for it to acknowledge it.
|
||||
# Forcibly closing the file handle on another thread does _not_ interrupt the
|
||||
# read on Linux systems.
|
||||
_EVENT_READ_TIMEOUT = 0.4 # in seconds
|
||||
|
||||
# After this many reads that did not produce a packet, call the tick() method.
|
||||
# This only happens if tick_period is enabled (>0) for the Listener instance.
|
||||
# _IDLE_READS = 1 + int(5 // _EVENT_READ_TIMEOUT) # wait at least 5 seconds between ticks
|
||||
|
||||
|
||||
class EventsListener(_threading.Thread):
|
||||
"""Listener thread for notifications from the Unifying Receiver.
|
||||
|
||||
Incoming packets will be passed to the callback function in sequence.
|
||||
"""
|
||||
def __init__(self, receiver, notifications_callback):
|
||||
super(EventsListener, self).__init__(name=self.__class__.__name__ + ':' + receiver.path.split('/')[2])
|
||||
|
||||
self.daemon = True
|
||||
self._active = False
|
||||
|
||||
self.receiver = receiver
|
||||
self._queued_notifications = _Queue(16)
|
||||
self._notifications_callback = notifications_callback
|
||||
|
||||
# self.tick_period = 0
|
||||
|
||||
def run(self):
|
||||
self._active = True
|
||||
|
||||
# replace the handle with a threaded one
|
||||
self.receiver.handle = _ThreadedHandle(self, self.receiver.path, self.receiver.handle)
|
||||
# get the right low-level handle for this thead
|
||||
ihandle = int(self.receiver.handle)
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("started with %s (%d)", self.receiver, ihandle)
|
||||
|
||||
self.has_started()
|
||||
|
||||
# last_tick = 0
|
||||
# the first idle read -- delay it a bit, and make sure to stagger
|
||||
# idle reads for multiple receivers
|
||||
# idle_reads = _IDLE_READS + (ihandle % 5) * 2
|
||||
|
||||
while self._active:
|
||||
if self._queued_notifications.empty():
|
||||
try:
|
||||
# _log.debug("read next notification")
|
||||
n = _base.read(ihandle, _EVENT_READ_TIMEOUT)
|
||||
except _base.NoReceiver:
|
||||
_log.warning("receiver disconnected")
|
||||
self.receiver.close()
|
||||
break
|
||||
|
||||
if n:
|
||||
n = _base.make_notification(*n)
|
||||
else:
|
||||
# deliver any queued notifications
|
||||
n = self._queued_notifications.get()
|
||||
|
||||
if n:
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("%s: processing %s", self.receiver, n)
|
||||
try:
|
||||
self._notifications_callback(n)
|
||||
except:
|
||||
_log.exception("processing %s", n)
|
||||
|
||||
# elif self.tick_period:
|
||||
# idle_reads -= 1
|
||||
# if idle_reads <= 0:
|
||||
# idle_reads = _IDLE_READS
|
||||
# now = _timestamp()
|
||||
# if now - last_tick >= self.tick_period:
|
||||
# last_tick = now
|
||||
# self.tick(now)
|
||||
|
||||
del self._queued_notifications
|
||||
self.has_stopped()
|
||||
|
||||
def stop(self):
|
||||
"""Tells the listener to stop as soon as possible."""
|
||||
self._active = False
|
||||
|
||||
def has_started(self):
|
||||
"""Called right after the thread has started, and before it starts
|
||||
reading notification packets."""
|
||||
pass
|
||||
|
||||
def has_stopped(self):
|
||||
"""Called right before the thread stops."""
|
||||
pass
|
||||
|
||||
# def tick(self, timestamp):
|
||||
# """Called about every tick_period seconds."""
|
||||
# pass
|
||||
|
||||
def _notifications_hook(self, n):
|
||||
# Only consider unhandled notifications that were sent from this thread,
|
||||
# i.e. triggered by a callback handling a previous notification.
|
||||
assert _threading.current_thread() == self
|
||||
if self._active: # and _threading.current_thread() == self:
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("queueing unhandled %s", n)
|
||||
self._queued_notifications.put(n)
|
||||
|
||||
def __bool__(self):
|
||||
return bool(self._active and self.receiver)
|
||||
__nonzero__ = __bool__
|
||||
277
lib/logitech_receiver/notifications.py
Normal file
277
lib/logitech_receiver/notifications.py
Normal file
@@ -0,0 +1,277 @@
|
||||
# -*- python-mode -*-
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
## Copyright (C) 2012-2013 Daniel Pavel
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published by
|
||||
## the Free Software Foundation; either version 2 of the License, or
|
||||
## (at your option) any later version.
|
||||
##
|
||||
## This program is distributed in the hope that it will be useful,
|
||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
## GNU General Public License for more details.
|
||||
##
|
||||
## You should have received a copy of the GNU General Public License along
|
||||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
# Handles incoming events from the receiver/devices, updating the related
|
||||
# status object as appropiate.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
from logging import getLogger, DEBUG as _DEBUG, INFO as _INFO
|
||||
_log = getLogger(__name__)
|
||||
del getLogger
|
||||
|
||||
|
||||
from .i18n import _
|
||||
from .common import strhex as _strhex, unpack as _unpack
|
||||
from . import hidpp10 as _hidpp10
|
||||
from . import hidpp20 as _hidpp20
|
||||
from .status import KEYS as _K, ALERT as _ALERT
|
||||
|
||||
_R = _hidpp10.REGISTERS
|
||||
_F = _hidpp20.FEATURE
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def process(device, notification):
|
||||
assert device
|
||||
assert notification
|
||||
|
||||
assert hasattr(device, 'status')
|
||||
status = device.status
|
||||
assert status is not None
|
||||
|
||||
if device.kind is None:
|
||||
return _process_receiver_notification(device, status, notification)
|
||||
|
||||
return _process_device_notification(device, status, notification)
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def _process_receiver_notification(receiver, status, n):
|
||||
# supposedly only 0x4x notifications arrive for the receiver
|
||||
assert n.sub_id & 0x40 == 0x40
|
||||
|
||||
# pairing lock notification
|
||||
if n.sub_id == 0x4A:
|
||||
status.lock_open = bool(n.address & 0x01)
|
||||
reason = _("pairing lock is ") + (_("open") if status.lock_open else _("closed"))
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("%s: %s", receiver, reason)
|
||||
|
||||
status[_K.ERROR] = None
|
||||
if status.lock_open:
|
||||
status.new_device = None
|
||||
|
||||
pair_error = ord(n.data[:1])
|
||||
if pair_error:
|
||||
status[_K.ERROR] = error_string = _hidpp10.PAIRING_ERRORS[pair_error]
|
||||
status.new_device = None
|
||||
_log.warn("pairing error %d: %s", pair_error, error_string)
|
||||
|
||||
status.changed(reason=reason)
|
||||
return True
|
||||
|
||||
_log.warn("%s: unhandled notification %s", receiver, n)
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def _process_device_notification(device, status, n):
|
||||
# incoming packets with SubId >= 0x80 are supposedly replies from
|
||||
# HID++ 1.0 requests, should never get here
|
||||
assert n.sub_id & 0x80 == 0
|
||||
|
||||
# 0x40 to 0x7F appear to be HID++ 1.0 notifications
|
||||
if n.sub_id >= 0x40:
|
||||
return _process_hidpp10_notification(device, status, n)
|
||||
|
||||
# At this point, we need to know the device's protocol, otherwise it's
|
||||
# possible to not know how to handle it.
|
||||
assert device.protocol is not None
|
||||
|
||||
# some custom battery events for HID++ 1.0 devices
|
||||
if device.protocol < 2.0:
|
||||
return _process_hidpp10_custom_notification(device, status, n)
|
||||
|
||||
# assuming 0x00 to 0x3F are feature (HID++ 2.0) notifications
|
||||
assert device.features
|
||||
try:
|
||||
feature = device.features[n.sub_id]
|
||||
except IndexError:
|
||||
_log.warn("%s: notification from invalid feature index %02X: %s", device, n.sub_id, n)
|
||||
return False
|
||||
|
||||
return _process_feature_notification(device, status, n, feature)
|
||||
|
||||
|
||||
def _process_hidpp10_custom_notification(device, status, n):
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("%s (%s) custom notification %s", device, device.protocol, n)
|
||||
|
||||
if n.sub_id in (_R.battery_status, _R.battery_charge):
|
||||
# message layout: 10 ix <register> <xx> <yy> <zz> <00>
|
||||
assert n.data[-1:] == b'\x00'
|
||||
data = chr(n.address).encode() + n.data
|
||||
charge, status_text = _hidpp10.parse_battery_status(n.sub_id, data)
|
||||
status.set_battery_info(charge, status_text)
|
||||
return True
|
||||
|
||||
if n.sub_id == _R.illumination:
|
||||
# message layout: 10 ix 17("address") <??> <?> <??> <light level 1=off..5=max>
|
||||
# TODO anything we can do with this?
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("illumination event: %s", n)
|
||||
return True
|
||||
|
||||
_log.warn("%s: unrecognized %s", device, n)
|
||||
|
||||
|
||||
def _process_hidpp10_notification(device, status, n):
|
||||
# unpair notification
|
||||
if n.sub_id == 0x40:
|
||||
if n.address == 0x02:
|
||||
# device un-paired
|
||||
status.clear()
|
||||
device.wpid = None
|
||||
device.status = None
|
||||
if device.number in device.receiver:
|
||||
del device.receiver[device.number]
|
||||
status.changed(active=False, alert=_ALERT.ALL, reason='unpaired')
|
||||
else:
|
||||
_log.warn("%s: disconnection with unknown type %02X: %s", device, n.address, n)
|
||||
return True
|
||||
|
||||
# wireless link notification
|
||||
if n.sub_id == 0x41:
|
||||
protocol_name = ('unifying (eQuad DJ)' if n.address == 0x04
|
||||
else 'eQuad' if n.address == 0x03
|
||||
else None)
|
||||
if protocol_name:
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
wpid = _strhex(n.data[2:3] + n.data[1:2])
|
||||
assert wpid == device.wpid, "%s wpid mismatch, got %s" % (device, wpid)
|
||||
|
||||
flags = ord(n.data[:1]) & 0xF0
|
||||
link_encrypyed = bool(flags & 0x20)
|
||||
link_established = not (flags & 0x40)
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
sw_present = bool(flags & 0x10)
|
||||
has_payload = bool(flags & 0x80)
|
||||
_log.debug("%s: %s connection notification: software=%s, encrypted=%s, link=%s, payload=%s",
|
||||
device, protocol_name, sw_present, link_encrypyed, link_established, has_payload)
|
||||
status[_K.LINK_ENCRYPTED] = link_encrypyed
|
||||
status.changed(active=link_established)
|
||||
else:
|
||||
_log.warn("%s: connection notification with unknown protocol %02X: %s", device.number, n.address, n)
|
||||
|
||||
return True
|
||||
|
||||
if n.sub_id == 0x49:
|
||||
# raw input event? just ignore it
|
||||
# if n.address == 0x01, no idea what it is, but they keep on coming
|
||||
# if n.address == 0x03, appears to be an actual input event,
|
||||
# because they only come when input happents
|
||||
return True
|
||||
|
||||
# power notification
|
||||
if n.sub_id == 0x4B:
|
||||
if n.address == 0x01:
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("%s: device powered on", device)
|
||||
reason = str(status) or _("powered on")
|
||||
status.changed(active=True, alert=_ALERT.NOTIFICATION, reason=reason)
|
||||
else:
|
||||
_log.warn("%s: unknown %s", device, n)
|
||||
return True
|
||||
|
||||
_log.warn("%s: unrecognized %s", device, n)
|
||||
|
||||
|
||||
def _process_feature_notification(device, status, n, feature):
|
||||
if feature == _F.BATTERY_STATUS:
|
||||
if n.address == 0x00:
|
||||
discharge = ord(n.data[:1])
|
||||
battery_status = ord(n.data[1:2])
|
||||
status.set_battery_info(discharge, _hidpp20.BATTERY_STATUS[battery_status])
|
||||
else:
|
||||
_log.warn("%s: unknown BATTERY %s", device, n)
|
||||
return True
|
||||
|
||||
# TODO: what are REPROG_CONTROLS_V{2,3}?
|
||||
if feature == _F.REPROG_CONTROLS:
|
||||
if n.address == 0x00:
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("%s: reprogrammable key: %s", device, n)
|
||||
else:
|
||||
_log.warn("%s: unknown REPROGRAMMABLE KEYS %s", device, n)
|
||||
return True
|
||||
|
||||
if feature == _F.WIRELESS_DEVICE_STATUS:
|
||||
if n.address == 0x00:
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("wireless status: %s", n)
|
||||
if n.data[0:3] == b'\x01\x01\x01':
|
||||
status.changed(active=True, alert=_ALERT.NOTIFICATION, reason='powered on')
|
||||
else:
|
||||
_log.warn("%s: unknown WIRELESS %s", device, n)
|
||||
else:
|
||||
_log.warn("%s: unknown WIRELESS %s", device, n)
|
||||
return True
|
||||
|
||||
if feature == _F.SOLAR_DASHBOARD:
|
||||
if n.data[5:9] == b'GOOD':
|
||||
charge, lux, adc = _unpack('!BHH', n.data[:5])
|
||||
# guesstimate the battery voltage, emphasis on 'guess'
|
||||
# status_text = '%1.2fV' % (adc * 2.67793237653 / 0x0672)
|
||||
status_text = _hidpp20.BATTERY_STATUS.discharging
|
||||
|
||||
if n.address == 0x00:
|
||||
status[_K.LIGHT_LEVEL] = None
|
||||
status.set_battery_info(charge, status_text)
|
||||
elif n.address == 0x10:
|
||||
status[_K.LIGHT_LEVEL] = lux
|
||||
if lux > 200:
|
||||
status_text = _hidpp20.BATTERY_STATUS.recharging
|
||||
status.set_battery_info(charge, status_text)
|
||||
elif n.address == 0x20:
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("%s: Light Check button pressed", device)
|
||||
status.changed(alert=_ALERT.SHOW_WINDOW)
|
||||
# first cancel any reporting
|
||||
# device.feature_request(_F.SOLAR_DASHBOARD)
|
||||
# trigger a new report chain
|
||||
reports_count = 15
|
||||
reports_period = 2 # seconds
|
||||
device.feature_request(_F.SOLAR_DASHBOARD, 0x00, reports_count, reports_period)
|
||||
else:
|
||||
_log.warn("%s: unknown SOLAR CHAGE %s", device, n)
|
||||
else:
|
||||
_log.warn("%s: SOLAR CHARGE not GOOD? %s", device, n)
|
||||
return True
|
||||
|
||||
if feature == _F.TOUCHMOUSE_RAW_POINTS:
|
||||
if n.address == 0x00:
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("%s: TOUCH MOUSE points %s", device, n)
|
||||
elif n.address == 0x10:
|
||||
touch = ord(n.data[:1])
|
||||
button_down = bool(touch & 0x02)
|
||||
mouse_lifted = bool(touch & 0x01)
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("%s: TOUCH MOUSE status: button_down=%s mouse_lifted=%s", device, button_down, mouse_lifted)
|
||||
else:
|
||||
_log.warn("%s: unknown TOUCH MOUSE %s", device, n)
|
||||
return True
|
||||
|
||||
_log.warn("%s: unrecognized %s for feature %s (index %02X)", device, n, feature, n.sub_id)
|
||||
531
lib/logitech_receiver/receiver.py
Normal file
531
lib/logitech_receiver/receiver.py
Normal file
@@ -0,0 +1,531 @@
|
||||
# -*- python-mode -*-
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
## Copyright (C) 2012-2013 Daniel Pavel
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published by
|
||||
## the Free Software Foundation; either version 2 of the License, or
|
||||
## (at your option) any later version.
|
||||
##
|
||||
## This program is distributed in the hope that it will be useful,
|
||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
## GNU General Public License for more details.
|
||||
##
|
||||
## You should have received a copy of the GNU General Public License along
|
||||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
import errno as _errno
|
||||
|
||||
from logging import getLogger, INFO as _INFO
|
||||
_log = getLogger(__name__)
|
||||
del getLogger
|
||||
|
||||
|
||||
from .i18n import _
|
||||
from . import base as _base
|
||||
from . import hidpp10 as _hidpp10
|
||||
from . import hidpp20 as _hidpp20
|
||||
from .common import strhex as _strhex
|
||||
from .descriptors import DEVICES as _DESCRIPTORS
|
||||
from .settings_templates import check_feature_settings as _check_feature_settings
|
||||
|
||||
_R = _hidpp10.REGISTERS
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
class PairedDevice(object):
|
||||
def __init__(self, receiver, number, link_notification=None):
|
||||
assert receiver
|
||||
self.receiver = receiver
|
||||
|
||||
assert number > 0 and number <= receiver.max_devices
|
||||
# Device number, 1..6 for unifying devices, 1 otherwise.
|
||||
self.number = number
|
||||
# 'device active' flag; requires manual management.
|
||||
self.online = None
|
||||
|
||||
# the Wireless PID is unique per device model
|
||||
self.wpid = None
|
||||
self.descriptor = None
|
||||
|
||||
# mose, keyboard, etc (see _hidpp10.DEVICE_KIND)
|
||||
self._kind = None
|
||||
# Unifying peripherals report a codename.
|
||||
self._codename = None
|
||||
# the full name of the model
|
||||
self._name = None
|
||||
# HID++ protocol version, 1.0 or 2.0
|
||||
self._protocol = None
|
||||
# serial number (an 8-char hex string)
|
||||
self._serial = None
|
||||
|
||||
self._firmware = None
|
||||
self._keys = None
|
||||
self._registers = None
|
||||
self._settings = None
|
||||
|
||||
# Misc stuff that's irrelevant to any functionality, but may be
|
||||
# displayed in the UI and caching it here helps.
|
||||
self._polling_rate = None
|
||||
self._power_switch = None
|
||||
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("new PairedDevice(%s, %s, %s)", receiver, number, link_notification)
|
||||
|
||||
if link_notification is not None:
|
||||
self.online = bool(ord(link_notification.data[0:1]) & 0x40)
|
||||
self.wpid = _strhex(link_notification.data[2:3] + link_notification.data[1:2])
|
||||
# assert link_notification.address == (0x04 if unifying else 0x03)
|
||||
kind = ord(link_notification.data[0:1]) & 0x0F
|
||||
self._kind = _hidpp10.DEVICE_KIND[kind]
|
||||
else:
|
||||
# force a reading of the wpid
|
||||
pair_info = receiver.read_register(_R.receiver_info, 0x20 + number - 1)
|
||||
if pair_info:
|
||||
# may be either a Unifying receiver, or an Unifying-ready receiver
|
||||
self.wpid = _strhex(pair_info[3:5])
|
||||
kind = ord(pair_info[7:8]) & 0x0F
|
||||
self._kind = _hidpp10.DEVICE_KIND[kind]
|
||||
self._polling_rate = ord(pair_info[2:3])
|
||||
|
||||
else:
|
||||
# unifying protocol not supported, must be a Nano receiver
|
||||
device_info = self.receiver.read_register(_R.receiver_info, 0x04)
|
||||
if device_info is None:
|
||||
_log.error("failed to read Nano wpid for device %d of %s", number, receiver)
|
||||
raise _base.NoSuchDevice(number=number, receiver=receiver, error="read Nano wpid")
|
||||
|
||||
self.wpid = _strhex(device_info[3:5])
|
||||
self._polling_rate = 0
|
||||
self._power_switch = '(' + _("unknown") + ')'
|
||||
|
||||
# the wpid is necessary to properly identify wireless link on/off notifications
|
||||
# also it gets set to None on this object when the device is unpaired
|
||||
assert self.wpid is not None, "failed to read wpid: device %d of %s" % (number, receiver)
|
||||
|
||||
self.descriptor = _DESCRIPTORS.get(self.wpid)
|
||||
if self.descriptor is None:
|
||||
# Last chance to correctly identify the device; many Nano receivers
|
||||
# do not support this call.
|
||||
codename = self.receiver.read_register(_R.receiver_info, 0x40 + self.number - 1)
|
||||
if codename:
|
||||
codename_length = ord(codename[1:2])
|
||||
codename = codename[2:2 + codename_length]
|
||||
self._codename = codename.decode('ascii')
|
||||
self.descriptor = _DESCRIPTORS.get(self._codename)
|
||||
|
||||
if self.descriptor:
|
||||
self._name = self.descriptor.name
|
||||
self._protocol = self.descriptor.protocol
|
||||
if self._codename is None:
|
||||
self._codename = self.descriptor.codename
|
||||
if self._kind is None:
|
||||
self._kind = self.descriptor.kind
|
||||
|
||||
if self._protocol is not None:
|
||||
self.features = None if self._protocol < 2.0 else _hidpp20.FeaturesArray(self)
|
||||
else:
|
||||
# may be a 2.0 device; if not, it will fix itself later
|
||||
self.features = _hidpp20.FeaturesArray(self)
|
||||
|
||||
@property
|
||||
def protocol(self):
|
||||
if self._protocol is None and self.online is not False:
|
||||
self._protocol = _base.ping(self.receiver.handle, self.number)
|
||||
# if the ping failed, the peripheral is (almost) certainly offline
|
||||
self.online = self._protocol is not None
|
||||
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("device %d protocol %s", self.number, self._protocol)
|
||||
return self._protocol or 0
|
||||
|
||||
@property
|
||||
def codename(self):
|
||||
if self._codename is None:
|
||||
codename = self.receiver.read_register(_R.receiver_info, 0x40 + self.number - 1)
|
||||
if codename:
|
||||
codename_length = ord(codename[1:2])
|
||||
codename = codename[2:2 + codename_length]
|
||||
self._codename = codename.decode('ascii')
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("device %d codename %s", self.number, self._codename)
|
||||
else:
|
||||
self._codename = '? (%s)' % self.wpid
|
||||
return self._codename
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
if self._name is None:
|
||||
if self.online and self.protocol >= 2.0:
|
||||
self._name = _hidpp20.get_name(self)
|
||||
return self._name or self.codename or ('Unknown device %s' % self.wpid)
|
||||
|
||||
@property
|
||||
def kind(self):
|
||||
if self._kind is None:
|
||||
pair_info = self.receiver.read_register(_R.receiver_info, 0x20 + self.number - 1)
|
||||
if pair_info:
|
||||
kind = ord(pair_info[7:8]) & 0x0F
|
||||
self._kind = _hidpp10.DEVICE_KIND[kind]
|
||||
self._polling_rate = ord(pair_info[2:3])
|
||||
elif self.online and self.protocol >= 2.0:
|
||||
self._kind = _hidpp20.get_kind(self)
|
||||
return self._kind or '?'
|
||||
|
||||
@property
|
||||
def firmware(self):
|
||||
if self._firmware is None and self.online:
|
||||
if self.protocol >= 2.0:
|
||||
self._firmware = _hidpp20.get_firmware(self)
|
||||
else:
|
||||
self._firmware = _hidpp10.get_firmware(self)
|
||||
return self._firmware or ()
|
||||
|
||||
@property
|
||||
def serial(self):
|
||||
if self._serial is None:
|
||||
serial = self.receiver.read_register(_R.receiver_info, 0x30 + self.number - 1)
|
||||
if serial:
|
||||
ps = ord(serial[9:10]) & 0x0F
|
||||
self._power_switch = _hidpp10.POWER_SWITCH_LOCATION[ps]
|
||||
else:
|
||||
# some Nano receivers?
|
||||
serial = self.receiver.read_register(0x2D5)
|
||||
|
||||
if serial:
|
||||
self._serial = _strhex(serial[1:5])
|
||||
else:
|
||||
# fallback...
|
||||
self._serial = self.receiver.serial
|
||||
return self._serial or '?'
|
||||
|
||||
@property
|
||||
def power_switch_location(self):
|
||||
if self._power_switch is None:
|
||||
ps = self.receiver.read_register(_R.receiver_info, 0x30 + self.number - 1)
|
||||
if ps is not None:
|
||||
ps = ord(ps[9:10]) & 0x0F
|
||||
self._power_switch = _hidpp10.POWER_SWITCH_LOCATION[ps]
|
||||
else:
|
||||
self._power_switch = '(unknown)'
|
||||
return self._power_switch
|
||||
|
||||
@property
|
||||
def polling_rate(self):
|
||||
if self._polling_rate is None:
|
||||
pair_info = self.receiver.read_register(_R.receiver_info, 0x20 + self.number - 1)
|
||||
if pair_info:
|
||||
self._polling_rate = ord(pair_info[2:3])
|
||||
else:
|
||||
self._polling_rate = 0
|
||||
return self._polling_rate
|
||||
|
||||
@property
|
||||
def keys(self):
|
||||
if self._keys is None:
|
||||
if self.online and self.protocol >= 2.0:
|
||||
self._keys = _hidpp20.get_keys(self) or ()
|
||||
return self._keys
|
||||
|
||||
@property
|
||||
def registers(self):
|
||||
if self._registers is None:
|
||||
if self.descriptor and self.descriptor.registers:
|
||||
self._registers = list(self.descriptor.registers)
|
||||
else:
|
||||
self._registers = []
|
||||
return self._registers
|
||||
|
||||
@property
|
||||
def settings(self):
|
||||
if self._settings is None:
|
||||
if self.descriptor and self.descriptor.settings:
|
||||
self._settings = [s(self) for s in self.descriptor.settings]
|
||||
else:
|
||||
self._settings = []
|
||||
|
||||
_check_feature_settings(self, self._settings)
|
||||
return self._settings
|
||||
|
||||
def enable_notifications(self, enable=True):
|
||||
"""Enable or disable device (dis)connection notifications on this
|
||||
receiver."""
|
||||
if not bool(self.receiver) or self.protocol >= 2.0:
|
||||
return False
|
||||
|
||||
if enable:
|
||||
set_flag_bits = ( _hidpp10.NOTIFICATION_FLAG.battery_status
|
||||
| _hidpp10.NOTIFICATION_FLAG.keyboard_illumination
|
||||
| _hidpp10.NOTIFICATION_FLAG.wireless
|
||||
| _hidpp10.NOTIFICATION_FLAG.software_present )
|
||||
else:
|
||||
set_flag_bits = 0
|
||||
ok = _hidpp10.set_notification_flags(self, set_flag_bits)
|
||||
if ok is None:
|
||||
_log.warn("%s: failed to %s device notifications", self, 'enable' if enable else 'disable')
|
||||
|
||||
flag_bits = _hidpp10.get_notification_flags(self)
|
||||
flag_names = None if flag_bits is None else tuple(_hidpp10.NOTIFICATION_FLAG.flag_names(flag_bits))
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("%s: device notifications %s %s", self, 'enabled' if enable else 'disabled', flag_names)
|
||||
return flag_bits if ok else None
|
||||
|
||||
def request(self, request_id, *params):
|
||||
return _base.request(self.receiver.handle, self.number, request_id, *params)
|
||||
|
||||
read_register = _hidpp10.read_register
|
||||
write_register = _hidpp10.write_register
|
||||
|
||||
def feature_request(self, feature, function=0x00, *params):
|
||||
if self.protocol >= 2.0:
|
||||
return _hidpp20.feature_request(self, feature, function, *params)
|
||||
|
||||
def ping(self):
|
||||
"""Checks if the device is online, returns True of False"""
|
||||
protocol = _base.ping(self.receiver.handle, self.number)
|
||||
self.online = protocol is not None
|
||||
if protocol is not None:
|
||||
self._protocol = protocol
|
||||
return self.online
|
||||
|
||||
def __index__(self):
|
||||
return self.number
|
||||
__int__ = __index__
|
||||
|
||||
def __eq__(self, other):
|
||||
return other is not None and self.kind == other.kind and self.wpid == other.wpid
|
||||
|
||||
def __ne__(self, other):
|
||||
return other is None or self.kind != other.kind or self.wpid != other.wpid
|
||||
|
||||
def __hash__(self):
|
||||
return self.wpid.__hash__()
|
||||
|
||||
__bool__ = __nonzero__ = lambda self: self.wpid is not None and self.number in self.receiver
|
||||
|
||||
def __str__(self):
|
||||
return '<PairedDevice(%d,%s,%s)>' % (self.number, self.wpid, self.codename or '?')
|
||||
__unicode__ = __repr__ = __str__
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
class Receiver(object):
|
||||
"""A Unifying Receiver instance.
|
||||
|
||||
The paired devices are available through the sequence interface.
|
||||
"""
|
||||
number = 0xFF
|
||||
kind = None
|
||||
|
||||
def __init__(self, handle, device_info):
|
||||
assert handle
|
||||
self.handle = handle
|
||||
assert device_info
|
||||
self.path = device_info.path
|
||||
# USB product id, used for some Nano receivers
|
||||
self.product_id = device_info.product_id
|
||||
|
||||
# read the serial immediately, so we can find out max_devices
|
||||
# this will tell us if it's a Unifying or Nano receiver
|
||||
serial_reply = self.read_register(_R.receiver_info, 0x03)
|
||||
assert serial_reply
|
||||
self.serial = _strhex(serial_reply[1:5])
|
||||
self.max_devices = ord(serial_reply[6:7])
|
||||
|
||||
if self.max_devices == 6:
|
||||
self.name = 'Unifying Receiver'
|
||||
elif self.max_devices < 6:
|
||||
self.name = 'Nano Receiver'
|
||||
else:
|
||||
raise Exception("unknown receiver type", self.max_devices)
|
||||
self._str = '<%s(%s,%s%s)>' % (self.name.replace(' ', ''), self.path, '' if isinstance(self.handle, int) else 'T', self.handle)
|
||||
|
||||
# TODO _properly_ figure out which receivers do and which don't support unpairing
|
||||
self.may_unpair = self.write_register(_R.receiver_pairing) is None
|
||||
|
||||
self._firmware = None
|
||||
self._devices = {}
|
||||
|
||||
def close(self):
|
||||
handle, self.handle = self.handle, None
|
||||
self._devices.clear()
|
||||
return (handle and _base.close(handle))
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
|
||||
@property
|
||||
def firmware(self):
|
||||
if self._firmware is None and self.handle:
|
||||
self._firmware = _hidpp10.get_firmware(self)
|
||||
return self._firmware
|
||||
|
||||
def enable_notifications(self, enable=True):
|
||||
"""Enable or disable device (dis)connection notifications on this
|
||||
receiver."""
|
||||
if not self.handle:
|
||||
return False
|
||||
|
||||
if enable:
|
||||
set_flag_bits = ( _hidpp10.NOTIFICATION_FLAG.battery_status
|
||||
| _hidpp10.NOTIFICATION_FLAG.wireless
|
||||
| _hidpp10.NOTIFICATION_FLAG.software_present )
|
||||
else:
|
||||
set_flag_bits = 0
|
||||
ok = _hidpp10.set_notification_flags(self, set_flag_bits)
|
||||
if ok is None:
|
||||
_log.warn("%s: failed to %s receiver notifications", self, 'enable' if enable else 'disable')
|
||||
return None
|
||||
|
||||
flag_bits = _hidpp10.get_notification_flags(self)
|
||||
flag_names = None if flag_bits is None else tuple(_hidpp10.NOTIFICATION_FLAG.flag_names(flag_bits))
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("%s: receiver notifications %s => %s", self, 'enabled' if enable else 'disabled', flag_names)
|
||||
return flag_bits
|
||||
|
||||
def notify_devices(self):
|
||||
"""Scan all devices."""
|
||||
if self.handle:
|
||||
if not self.write_register(_R.receiver_connection, 0x02):
|
||||
_log.warn("%s: failed to trigger device link notifications", self)
|
||||
|
||||
def register_new_device(self, number, notification=None):
|
||||
if self._devices.get(number) is not None:
|
||||
raise IndexError("%s: device number %d already registered" % (self, number))
|
||||
|
||||
assert notification is None or notification.devnumber == number
|
||||
assert notification is None or notification.sub_id == 0x41
|
||||
|
||||
try:
|
||||
dev = PairedDevice(self, number, notification)
|
||||
assert dev.wpid
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("%s: found new device %d (%s)", self, number, dev.wpid)
|
||||
self._devices[number] = dev
|
||||
return dev
|
||||
except _base.NoSuchDevice:
|
||||
_log.exception("register_new_device")
|
||||
|
||||
_log.warning("%s: looked for device %d, not found", self, number)
|
||||
self._devices[number] = None
|
||||
|
||||
def set_lock(self, lock_closed=True, device=0, timeout=0):
|
||||
if self.handle:
|
||||
action = 0x02 if lock_closed else 0x01
|
||||
reply = self.write_register(_R.receiver_pairing, action, device, timeout)
|
||||
if reply:
|
||||
return True
|
||||
_log.warn("%s: failed to %s the receiver lock", self, 'close' if lock_closed else 'open')
|
||||
|
||||
def count(self):
|
||||
count = self.read_register(_R.receiver_connection)
|
||||
return 0 if count is None else ord(count[1:2])
|
||||
|
||||
# def has_devices(self):
|
||||
# return len(self) > 0 or self.count() > 0
|
||||
|
||||
def request(self, request_id, *params):
|
||||
if bool(self):
|
||||
return _base.request(self.handle, 0xFF, request_id, *params)
|
||||
|
||||
read_register = _hidpp10.read_register
|
||||
write_register = _hidpp10.write_register
|
||||
|
||||
def __iter__(self):
|
||||
for number in range(1, 1 + self.max_devices):
|
||||
if number in self._devices:
|
||||
dev = self._devices[number]
|
||||
else:
|
||||
dev = self.__getitem__(number)
|
||||
if dev is not None:
|
||||
yield dev
|
||||
|
||||
def __getitem__(self, key):
|
||||
if not bool(self):
|
||||
return None
|
||||
|
||||
dev = self._devices.get(key)
|
||||
if dev is not None:
|
||||
return dev
|
||||
|
||||
if not isinstance(key, int):
|
||||
raise TypeError('key must be an integer')
|
||||
if key < 1 or key > self.max_devices:
|
||||
raise IndexError(key)
|
||||
|
||||
return self.register_new_device(key)
|
||||
|
||||
def __delitem__(self, key):
|
||||
key = int(key)
|
||||
|
||||
if self._devices.get(key) is None:
|
||||
raise IndexError(key)
|
||||
|
||||
dev = self._devices[key]
|
||||
if not dev:
|
||||
if key in self._devices:
|
||||
del self._devices[key]
|
||||
return
|
||||
|
||||
action = 0x03
|
||||
reply = self.write_register(_R.receiver_pairing, action, key)
|
||||
if reply:
|
||||
# invalidate the device
|
||||
dev.online = False
|
||||
dev.wpid = None
|
||||
if key in self._devices:
|
||||
del self._devices[key]
|
||||
_log.warn("%s unpaired device %s", self, dev)
|
||||
else:
|
||||
_log.error("%s failed to unpair device %s", self, dev)
|
||||
raise IndexError(key)
|
||||
|
||||
def __len__(self):
|
||||
return len([d for d in self._devices.values() if d is not None])
|
||||
|
||||
def __contains__(self, dev):
|
||||
if isinstance(dev, int):
|
||||
return self._devices.get(dev) is not None
|
||||
|
||||
return self.__contains__(dev.number)
|
||||
|
||||
def __eq__(self, other):
|
||||
return other is not None and self.kind == other.kind and self.path == other.path
|
||||
|
||||
def __ne__(self, other):
|
||||
return other is None or self.kind != other.kind or self.path != other.path
|
||||
|
||||
def __hash__(self):
|
||||
return self.path.__hash__()
|
||||
|
||||
def __str__(self):
|
||||
return self._str
|
||||
__unicode__ = __repr__ = __str__
|
||||
|
||||
__bool__ = __nonzero__ = lambda self: self.handle is not None
|
||||
|
||||
@classmethod
|
||||
def open(self, device_info):
|
||||
"""Opens a Logitech Receiver found attached to the machine, by Linux device path.
|
||||
|
||||
:returns: An open file handle for the found receiver, or ``None``.
|
||||
"""
|
||||
try:
|
||||
handle = _base.open_path(device_info.path)
|
||||
if handle:
|
||||
return Receiver(handle, device_info)
|
||||
except OSError as e:
|
||||
_log.exception("open %s", device_info)
|
||||
if e.errno == _errno.EACCES:
|
||||
raise
|
||||
except:
|
||||
_log.exception("open %s", device_info)
|
||||
351
lib/logitech_receiver/settings.py
Normal file
351
lib/logitech_receiver/settings.py
Normal file
@@ -0,0 +1,351 @@
|
||||
# -*- python-mode -*-
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
## Copyright (C) 2012-2013 Daniel Pavel
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published by
|
||||
## the Free Software Foundation; either version 2 of the License, or
|
||||
## (at your option) any later version.
|
||||
##
|
||||
## This program is distributed in the hope that it will be useful,
|
||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
## GNU General Public License for more details.
|
||||
##
|
||||
## You should have received a copy of the GNU General Public License along
|
||||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
from logging import getLogger, DEBUG as _DEBUG
|
||||
_log = getLogger(__name__)
|
||||
del getLogger
|
||||
|
||||
from copy import copy as _copy
|
||||
|
||||
|
||||
from .common import (
|
||||
NamedInt as _NamedInt,
|
||||
NamedInts as _NamedInts,
|
||||
bytes2int as _bytes2int,
|
||||
)
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
KIND = _NamedInts(toggle=0x01, choice=0x02, range=0x12)
|
||||
|
||||
class Setting(object):
|
||||
"""A setting descriptor.
|
||||
Needs to be instantiated for each specific device."""
|
||||
__slots__ = ('name', 'label', 'description', 'kind', 'persister', 'device_kind',
|
||||
'_rw', '_validator', '_device', '_value')
|
||||
|
||||
def __init__(self, name, rw, validator, kind=None, label=None, description=None, device_kind=None):
|
||||
assert name
|
||||
self.name = name
|
||||
self.label = label or name
|
||||
self.description = description
|
||||
self.device_kind = device_kind
|
||||
|
||||
self._rw = rw
|
||||
self._validator = validator
|
||||
|
||||
assert kind is None or kind & validator.kind != 0
|
||||
self.kind = kind or validator.kind
|
||||
self.persister = None
|
||||
|
||||
def __call__(self, device):
|
||||
assert not hasattr(self, '_value')
|
||||
assert self.device_kind is None or self.device_kind == device.kind
|
||||
p = device.protocol
|
||||
if p == 1.0:
|
||||
# HID++ 1.0 devices do not support features
|
||||
assert self._rw.kind == RegisterRW.kind
|
||||
elif p >= 2.0:
|
||||
# HID++ 2.0 devices do not support registers
|
||||
assert self._rw.kind == FeatureRW.kind
|
||||
|
||||
o = _copy(self)
|
||||
o._value = None
|
||||
o._device = device
|
||||
return o
|
||||
|
||||
@property
|
||||
def choices(self):
|
||||
assert hasattr(self, '_value')
|
||||
assert hasattr(self, '_device')
|
||||
|
||||
return self._validator.choices if self._validator.kind & KIND.choice else None
|
||||
|
||||
def read(self, cached=True):
|
||||
assert hasattr(self, '_value')
|
||||
assert hasattr(self, '_device')
|
||||
|
||||
if self._value is None and self.persister:
|
||||
# We haven't read a value from the device yet,
|
||||
# maybe we have something in the configuration.
|
||||
self._value = self.persister.get(self.name)
|
||||
|
||||
if cached and self._value is not None:
|
||||
if self.persister and self.name not in self.persister:
|
||||
# If this is a new device (or a new setting for an old device),
|
||||
# make sure to save its current value for the next time.
|
||||
self.persister[self.name] = self._value
|
||||
return self._value
|
||||
|
||||
if self._device.online:
|
||||
reply = self._rw.read(self._device)
|
||||
if reply:
|
||||
self._value = self._validator.validate_read(reply)
|
||||
if self.persister and self.name not in self.persister:
|
||||
# Don't update the persister if it already has a value,
|
||||
# otherwise the first read might overwrite the value we wanted.
|
||||
self.persister[self.name] = self._value
|
||||
return self._value
|
||||
|
||||
def write(self, value):
|
||||
assert hasattr(self, '_value')
|
||||
assert hasattr(self, '_device')
|
||||
assert value is not None
|
||||
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("%s: write %r to %s", self.name, value, self._device)
|
||||
|
||||
if self._device.online:
|
||||
# Remember the value we're trying to set, even if the write fails.
|
||||
# This way even if the device is offline or some other error occurs,
|
||||
# the last value we've tried to write is remembered in the configuration.
|
||||
self._value = value
|
||||
if self.persister:
|
||||
self.persister[self.name] = value
|
||||
|
||||
current_value = None
|
||||
if self._validator.needs_current_value:
|
||||
# the validator needs the current value, possibly to merge flag values
|
||||
current_value = self._rw.read(self._device)
|
||||
|
||||
data_bytes = self._validator.prepare_write(value, current_value)
|
||||
if data_bytes is not None:
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("%s: prepare write(%s) => %r", self.name, value, data_bytes)
|
||||
|
||||
reply = self._rw.write(self._device, data_bytes)
|
||||
if not reply:
|
||||
# tell whomever is calling that the write failed
|
||||
return None
|
||||
|
||||
return value
|
||||
|
||||
def apply(self):
|
||||
assert hasattr(self, '_value')
|
||||
assert hasattr(self, '_device')
|
||||
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("%s: apply %s (%s)", self.name, self._value, self._device)
|
||||
|
||||
value = self.read()
|
||||
if value is not None:
|
||||
self.write(value)
|
||||
|
||||
def __str__(self):
|
||||
if hasattr(self, '_value'):
|
||||
assert hasattr(self, '_device')
|
||||
return '<Setting([%s:%s] %s:%s=%s)>' % (self._rw.kind, self._validator.kind, self._device.codename, self.name, self._value)
|
||||
return '<Setting([%s:%s] %s)>' % (self._rw.kind, self._validator.kind, self.name)
|
||||
__unicode__ = __repr__ = __str__
|
||||
|
||||
#
|
||||
# read/write low-level operators
|
||||
#
|
||||
|
||||
class RegisterRW(object):
|
||||
__slots__ = ('register', )
|
||||
|
||||
kind = _NamedInt(0x01, 'register')
|
||||
|
||||
def __init__(self, register):
|
||||
assert isinstance(register, int)
|
||||
self.register = register
|
||||
|
||||
def read(self, device):
|
||||
return device.read_register(self.register)
|
||||
|
||||
def write(self, device, data_bytes):
|
||||
return device.write_register(self.register, data_bytes)
|
||||
|
||||
|
||||
class FeatureRW(object):
|
||||
__slots__ = ('feature', 'read_fnid', 'write_fnid')
|
||||
|
||||
kind = _NamedInt(0x02, 'feature')
|
||||
default_read_fnid = 0x00
|
||||
default_write_fnid = 0x10
|
||||
|
||||
def __init__(self, feature, read_fnid=default_read_fnid, write_fnid=default_write_fnid):
|
||||
assert isinstance(feature, _NamedInt)
|
||||
self.feature = feature
|
||||
self.read_fnid = read_fnid
|
||||
self.write_fnid = write_fnid
|
||||
|
||||
def read(self, device):
|
||||
assert self.feature is not None
|
||||
return device.feature_request(self.feature, self.read_fnid)
|
||||
|
||||
def write(self, device, data_bytes):
|
||||
assert self.feature is not None
|
||||
return device.feature_request(self.feature, self.write_fnid, data_bytes)
|
||||
|
||||
#
|
||||
# value validators
|
||||
# handle the conversion from read bytes, to setting value, and back
|
||||
#
|
||||
|
||||
class BooleanValidator(object):
|
||||
__slots__ = ('true_value', 'false_value', 'mask', 'needs_current_value')
|
||||
|
||||
kind = KIND.toggle
|
||||
default_true = 0x01
|
||||
default_false = 0x00
|
||||
# mask specifies all the affected bits in the value
|
||||
default_mask = 0xFF
|
||||
|
||||
def __init__(self, true_value=default_true, false_value=default_false, mask=default_mask):
|
||||
if isinstance(true_value, int):
|
||||
assert isinstance(false_value, int)
|
||||
if mask is None:
|
||||
mask = self.default_mask
|
||||
else:
|
||||
assert isinstance(mask, int)
|
||||
assert true_value & false_value == 0
|
||||
assert true_value & mask == true_value
|
||||
assert false_value & mask == false_value
|
||||
self.needs_current_value = (mask != self.default_mask)
|
||||
elif isinstance(true_value, bytes):
|
||||
if false_value is None or false_value == self.default_false:
|
||||
false_value = b'\x00' * len(true_value)
|
||||
else:
|
||||
assert isinstance(false_value, bytes)
|
||||
if mask is None or mask == self.default_mask:
|
||||
mask = b'\xFF' * len(true_value)
|
||||
else:
|
||||
assert isinstance(mask, bytes)
|
||||
assert len(mask) == len(true_value) == len(false_value)
|
||||
tv = _bytes2int(true_value)
|
||||
fv = _bytes2int(false_value)
|
||||
mv = _bytes2int(mask)
|
||||
assert tv & fv == 0
|
||||
assert tv & mv == tv
|
||||
assert fv & mv == fv
|
||||
self.needs_current_value = any(m != b'\xFF' for m in mask)
|
||||
else:
|
||||
raise Exception("invalid mask '%r', type %s" % (mask, type(mask)))
|
||||
|
||||
self.true_value = true_value
|
||||
self.false_value = false_value
|
||||
self.mask = mask
|
||||
|
||||
def validate_read(self, reply_bytes):
|
||||
if isinstance(self.mask, int):
|
||||
reply_value = ord(reply_bytes[:1]) & self.mask
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("BooleanValidator: validate read %r => %02X", reply_bytes, reply_value)
|
||||
if reply_value == self.true_value:
|
||||
return True
|
||||
if reply_value == self.false_value:
|
||||
return False
|
||||
_log.warn("BooleanValidator: reply %02X mismatched %02X/%02X/%02X",
|
||||
reply_value, self.true_value, self.false_value, self.mask)
|
||||
return False
|
||||
|
||||
count = len(self.mask)
|
||||
mask = _bytes2int(self.mask)
|
||||
reply_value = _bytes2int(reply_bytes[:count]) & mask
|
||||
|
||||
true_value = _bytes2int(self.true_value)
|
||||
if reply_value == true_value:
|
||||
return True
|
||||
|
||||
false_value = _bytes2int(self.false_value)
|
||||
if reply_value == false_value:
|
||||
return False
|
||||
|
||||
_log.warn("BooleanValidator: reply %r mismatched %r/%r/%r",
|
||||
reply_bytes, self.true_value, self.false_value, self.mask)
|
||||
return False
|
||||
|
||||
def prepare_write(self, new_value, current_value=None):
|
||||
if new_value is None:
|
||||
new_value = False
|
||||
else:
|
||||
assert isinstance(new_value, bool)
|
||||
|
||||
to_write = self.true_value if new_value else self.false_value
|
||||
|
||||
if isinstance(self.mask, int):
|
||||
if current_value is not None and self.needs_current_value:
|
||||
to_write |= ord(current_value[:1]) & (0xFF ^ self.mask)
|
||||
if current_value is not None and to_write == ord(current_value[:1]):
|
||||
return None
|
||||
else:
|
||||
to_write = list(to_write)
|
||||
count = len(self.mask)
|
||||
for i in range(0, count):
|
||||
b = ord(to_write[i])
|
||||
m = ord(self.mask[i : i + 1])
|
||||
assert b & m == b
|
||||
# b &= m
|
||||
if current_value is not None and self.needs_current_value:
|
||||
b |= ord(current_value[i : i + 1]) & (0xFF ^ m)
|
||||
to_write[i] = chr(b)
|
||||
to_write = b''.join(to_write)
|
||||
|
||||
if current_value is not None and to_write == current_value[:len(to_write)]:
|
||||
return None
|
||||
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("BooleanValidator: prepare_write(%s, %s) => %r", new_value, current_value, to_write)
|
||||
|
||||
return to_write
|
||||
|
||||
|
||||
class ChoicesValidator(object):
|
||||
__slots__ = ('choices', 'flag', '_bytes_count', 'needs_current_value')
|
||||
|
||||
kind = KIND.choice
|
||||
|
||||
def __init__(self, choices):
|
||||
assert choices is not None
|
||||
assert isinstance(choices, _NamedInts)
|
||||
assert len(choices) > 2
|
||||
self.choices = choices
|
||||
self.needs_current_value = False
|
||||
|
||||
max_bits = max(x.bit_length() for x in choices)
|
||||
self._bytes_count = (max_bits // 8) + (1 if max_bits % 8 else 0)
|
||||
assert self._bytes_count < 8
|
||||
|
||||
def validate_read(self, reply_bytes):
|
||||
reply_value = _bytes2int(reply_bytes[:self._bytes_count])
|
||||
valid_value = self.choices[reply_value]
|
||||
assert valid_value is not None, "%s: failed to validate read value %02X" % (self.__class__.__name__, reply_value)
|
||||
return valid_value
|
||||
|
||||
def prepare_write(self, new_value, current_value=None):
|
||||
if new_value is None:
|
||||
choice = self.choices[:][0]
|
||||
else:
|
||||
if isinstance(new_value, int):
|
||||
choice = self.choices[new_value]
|
||||
elif new_value in self.choices:
|
||||
choice = self.choices[new_value]
|
||||
else:
|
||||
raise ValueError(new_value)
|
||||
|
||||
if choice is None:
|
||||
raise ValueError("invalid choice %r" % new_value)
|
||||
assert isinstance(choice, _NamedInt)
|
||||
return choice.bytes(self._bytes_count)
|
||||
175
lib/logitech_receiver/settings_templates.py
Normal file
175
lib/logitech_receiver/settings_templates.py
Normal file
@@ -0,0 +1,175 @@
|
||||
# -*- python-mode -*-
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
## Copyright (C) 2012-2013 Daniel Pavel
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published by
|
||||
## the Free Software Foundation; either version 2 of the License, or
|
||||
## (at your option) any later version.
|
||||
##
|
||||
## This program is distributed in the hope that it will be useful,
|
||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
## GNU General Public License for more details.
|
||||
##
|
||||
## You should have received a copy of the GNU General Public License along
|
||||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
|
||||
from .i18n import _
|
||||
from . import hidpp10 as _hidpp10
|
||||
from . import hidpp20 as _hidpp20
|
||||
from .settings import (
|
||||
KIND as _KIND,
|
||||
Setting as _Setting,
|
||||
RegisterRW as _RegisterRW,
|
||||
FeatureRW as _FeatureRW,
|
||||
BooleanValidator as _BooleanV,
|
||||
ChoicesValidator as _ChoicesV,
|
||||
)
|
||||
|
||||
_DK = _hidpp10.DEVICE_KIND
|
||||
_R = _hidpp10.REGISTERS
|
||||
_F = _hidpp20.FEATURE
|
||||
|
||||
#
|
||||
# pre-defined basic setting descriptors
|
||||
#
|
||||
|
||||
def register_toggle(name, register,
|
||||
true_value=_BooleanV.default_true,
|
||||
false_value=_BooleanV.default_false,
|
||||
mask=_BooleanV.default_mask,
|
||||
label=None, description=None, device_kind=None):
|
||||
validator = _BooleanV(true_value=true_value, false_value=false_value, mask=mask)
|
||||
rw = _RegisterRW(register)
|
||||
return _Setting(name, rw, validator, label=label, description=description, device_kind=device_kind)
|
||||
|
||||
|
||||
def register_choices(name, register, choices,
|
||||
kind=_KIND.choice,
|
||||
label=None, description=None, device_kind=None):
|
||||
assert choices
|
||||
validator = _ChoicesV(choices)
|
||||
rw = _RegisterRW(register)
|
||||
return _Setting(name, rw, validator, kind=kind, label=label, description=description, device_kind=device_kind)
|
||||
|
||||
|
||||
def feature_toggle(name, feature,
|
||||
read_function_id=_FeatureRW.default_read_fnid,
|
||||
write_function_id=_FeatureRW.default_write_fnid,
|
||||
true_value=_BooleanV.default_true,
|
||||
false_value=_BooleanV.default_false,
|
||||
mask=_BooleanV.default_mask,
|
||||
label=None, description=None, device_kind=None):
|
||||
validator = _BooleanV(true_value=true_value, false_value=false_value, mask=mask)
|
||||
rw = _FeatureRW(feature, read_function_id, write_function_id)
|
||||
return _Setting(name, rw, validator, label=label, description=description, device_kind=device_kind)
|
||||
|
||||
#
|
||||
# common strings for settings
|
||||
#
|
||||
|
||||
_SMOOTH_SCROLL = ('smooth-scroll', _("Smooth Scrolling"),
|
||||
_("High-sensitivity mode for vertical scroll with the wheel."))
|
||||
_SIDE_SCROLL = ('side-scroll', _("Side Scrolling"),
|
||||
_("When disabled, pushing the wheel sideways sends custom button events\n"
|
||||
"instead of the standard side-scrolling events."))
|
||||
_DPI = ('dpi', _("Sensitivity (DPI)"), None)
|
||||
_FN_SWAP = ('fn-swap', _("Swap Fx function"),
|
||||
_("When set, the F1..F12 keys will activate their special function,\n"
|
||||
"and you must hold the FN key to activate their standard function.")
|
||||
+ '\n\n' +
|
||||
_("When unset, the F1..F12 keys will activate their standard function,\n"
|
||||
"and you must hold the FN key to activate their special function."))
|
||||
_HAND_DETECTION = ('hand-detection', _("Hand Detection"),
|
||||
_("Turn on illumination when the hands hover over the keyboard."))
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def _register_hand_detection(register=_R.keyboard_hand_detection,
|
||||
true_value=b'\x00\x00\x00', false_value=b'\x00\x00\x30', mask=b'\x00\x00\xFF'):
|
||||
return register_toggle(_HAND_DETECTION[0], register, true_value=true_value, false_value=false_value,
|
||||
label=_HAND_DETECTION[1], description=_HAND_DETECTION[2],
|
||||
device_kind=_DK.keyboard)
|
||||
|
||||
def _register_fn_swap(register=_R.keyboard_fn_swap, true_value=b'\x00\x01', mask=b'\x00\x01'):
|
||||
return register_toggle(_FN_SWAP[0], register, true_value=true_value, mask=mask,
|
||||
label=_FN_SWAP[1], description=_FN_SWAP[2],
|
||||
device_kind=_DK.keyboard)
|
||||
|
||||
def _register_smooth_scroll(register=_R.mouse_button_flags, true_value=0x40, mask=0x40):
|
||||
return register_toggle(_SMOOTH_SCROLL[0], register, true_value=true_value, mask=mask,
|
||||
label=_SMOOTH_SCROLL[1], description=_SMOOTH_SCROLL[2],
|
||||
device_kind=_DK.mouse)
|
||||
|
||||
def _register_side_scroll(register=_R.mouse_button_flags, true_value=0x02, mask=0x02):
|
||||
return register_toggle(_SIDE_SCROLL[0], register, true_value=true_value, mask=mask,
|
||||
label=_SIDE_SCROLL[1], description=_SIDE_SCROLL[2],
|
||||
device_kind=_DK.mouse)
|
||||
|
||||
def _register_dpi(register=_R.mouse_dpi, choices=None):
|
||||
return register_choices(_DPI[0], register, choices,
|
||||
label=_DPI[1], description=_DPI[2],
|
||||
device_kind=_DK.mouse)
|
||||
|
||||
|
||||
def _feature_fn_swap():
|
||||
return feature_toggle(_FN_SWAP[0], _F.FN_INVERSION,
|
||||
label=_FN_SWAP[1], description=_FN_SWAP[2],
|
||||
device_kind=_DK.keyboard)
|
||||
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from collections import namedtuple
|
||||
_SETTINGS_LIST = namedtuple('_SETTINGS_LIST', [
|
||||
'fn_swap',
|
||||
'smooth_scroll',
|
||||
'side_scroll',
|
||||
'dpi',
|
||||
'hand_detection',
|
||||
'typing_illumination',
|
||||
])
|
||||
del namedtuple
|
||||
|
||||
RegisterSettings = _SETTINGS_LIST(
|
||||
fn_swap=_register_fn_swap,
|
||||
smooth_scroll=_register_smooth_scroll,
|
||||
side_scroll=_register_side_scroll,
|
||||
dpi=_register_dpi,
|
||||
hand_detection=_register_hand_detection,
|
||||
typing_illumination=None,
|
||||
)
|
||||
FeatureSettings = _SETTINGS_LIST(
|
||||
fn_swap=_feature_fn_swap,
|
||||
smooth_scroll=None,
|
||||
side_scroll=None,
|
||||
dpi=None,
|
||||
hand_detection=None,
|
||||
typing_illumination=None,
|
||||
)
|
||||
|
||||
del _SETTINGS_LIST
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def check_feature_settings(device, already_known):
|
||||
"""Try to auto-detect device settings by the HID++ 2.0 features they have."""
|
||||
if device.features is None:
|
||||
return
|
||||
if device.protocol and device.protocol < 2.0:
|
||||
return
|
||||
if not any(s.name == _FN_SWAP[0] for s in already_known) and _F.FN_INVERSION in device.features:
|
||||
fn_swap = FeatureSettings.fn_swap()
|
||||
already_known.append(fn_swap(device))
|
||||
334
lib/logitech_receiver/special_keys.py
Normal file
334
lib/logitech_receiver/special_keys.py
Normal file
@@ -0,0 +1,334 @@
|
||||
# -*- python-mode -*-
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
## Copyright (C) 2012-2013 Daniel Pavel
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published by
|
||||
## the Free Software Foundation; either version 2 of the License, or
|
||||
## (at your option) any later version.
|
||||
##
|
||||
## This program is distributed in the hope that it will be useful,
|
||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
## GNU General Public License for more details.
|
||||
##
|
||||
## You should have received a copy of the GNU General Public License along
|
||||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
# Reprogrammable keys information
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
|
||||
from .common import NamedInts as _NamedInts
|
||||
|
||||
# <controls.xml awk -F\" '/<Control /{sub(/^LD_FINFO_(CTRLID_)?/, "", $2);printf("\t%s=0x%04X,\n", $2, $4)}' | sort -t= -k2
|
||||
CONTROL = _NamedInts(
|
||||
Volume_Up=0x0001,
|
||||
Volume_Down=0x0002,
|
||||
Mute=0x0003,
|
||||
Play__Pause=0x0004,
|
||||
Next=0x0005,
|
||||
Previous=0x0006,
|
||||
Stop=0x0007,
|
||||
Application_Switcher=0x0008,
|
||||
BURN=0x0009,
|
||||
Calculator=0x000A,
|
||||
CALENDAR=0x000B,
|
||||
CLOSE=0x000C,
|
||||
EJECT=0x000D,
|
||||
Mail=0x000E,
|
||||
HELP_AS_HID=0x000F,
|
||||
HELP_AS_F1=0x0010,
|
||||
LAUNCH_WORD_PROC=0x0011,
|
||||
LAUNCH_SPREADSHEET=0x0012,
|
||||
LAUNCH_PRESENTATION=0x0013,
|
||||
UNDO_AS_CTRL_Z=0x0014,
|
||||
UNDO_AS_HID=0x0015,
|
||||
REDO_AS_CTRL_Y=0x0016,
|
||||
REDO_AS_HID=0x0017,
|
||||
PRINT_AS_CTRL_P=0x0018,
|
||||
PRINT_AS_HID=0x0019,
|
||||
SAVE_AS_CTRL_S=0x001A,
|
||||
SAVE_AS_HID=0x001B,
|
||||
PRESET_A=0x001C,
|
||||
PRESET_B=0x001D,
|
||||
PRESET_C=0x001E,
|
||||
PRESET_D=0x001F,
|
||||
FAVORITES=0x0020,
|
||||
GADGETS=0x0021,
|
||||
MY_HOME=0x0022,
|
||||
GADGETS_AS_WIN_G=0x0023,
|
||||
MAXIMIZE_AS_HID=0x0024,
|
||||
MAXIMIZE_AS_WIN_SHIFT_M=0x0025,
|
||||
MINIMIZE_AS_HID=0x0026,
|
||||
MINIMIZE_AS_WIN_M=0x0027,
|
||||
MEDIA_PLAYER=0x0028,
|
||||
MEDIA_CENTER_LOGI=0x0029,
|
||||
MEDIA_CENTER_MSFT=0x002A, # Should not be used as it is not reprogrammable under Windows
|
||||
CUSTOM_MENU=0x002B,
|
||||
MESSENGER=0x002C,
|
||||
MY_DOCUMENTS=0x002D,
|
||||
MY_MUSIC=0x002E,
|
||||
WEBCAM=0x002F,
|
||||
MY_PICTURES=0x0030,
|
||||
MY_VIDEOS=0x0031,
|
||||
MY_COMPUTER_AS_HID=0x0032,
|
||||
MY_COMPUTER_AS_WIN_E=0x0033,
|
||||
LAUNC_PICTURE_VIEWER=0x0035,
|
||||
ONE_TOUCH_SEARCH=0x0036,
|
||||
PRESET_1=0x0037,
|
||||
PRESET_2=0x0038,
|
||||
PRESET_3=0x0039,
|
||||
PRESET_4=0x003A,
|
||||
RECORD=0x003B,
|
||||
INTERNET_REFRESH=0x003C,
|
||||
ROTATE_RIGHT=0x003D,
|
||||
SEARCH=0x003E,
|
||||
SHUFFLE=0x003F,
|
||||
SLEEP=0x0040,
|
||||
INTERNET_STOP=0x0041,
|
||||
SYNCHRONIZE=0x0042,
|
||||
ZOOM=0x0043,
|
||||
ZOOM_IN_AS_HID=0x0044,
|
||||
ZOOM_IN_AS_CTRL_WHEEL=0x0045,
|
||||
ZOOM_IN_AS_CLTR_PLUS=0x0046,
|
||||
ZOOM_OUT_AS_HID=0x0047,
|
||||
ZOOM_OUT_AS_CTRL_WHEEL=0x0048,
|
||||
ZOOM_OUT_AS_CLTR_MINUS=0x0049,
|
||||
ZOOM_RESET=0x004A,
|
||||
ZOOM_FULL_SCREEN=0x004B,
|
||||
PRINT_SCREEN=0x004C,
|
||||
PAUSE_BREAK=0x004D,
|
||||
SCROLL_LOCK=0x004E,
|
||||
CONTEXTUAL_MENU=0x004F,
|
||||
LEFT_CLICK=0x0050,
|
||||
RIGHT_CLICK=0x0051,
|
||||
MIDDLE_BUTTON=0x0052,
|
||||
BACK_AS_BUTTON_4=0x0053,
|
||||
BACK_AS_HID=0x0054,
|
||||
BACK_AS_ALT_WIN_ARROW=0x0055,
|
||||
FORWARD_AS_BUTTON_5=0x0056,
|
||||
FORWARD_AS_HID=0x0057,
|
||||
FORWARD_AS_ALT_WIN_ARROW=0x0058,
|
||||
BUTTON_6=0x0059,
|
||||
LEFT_SCROLL_AS_BUTTON_7=0x005A,
|
||||
LEFT_SCROLL_AS_AC_PAN=0x005B,
|
||||
RIGHT_SCROLL_AS_BUTTON_8=0x005C,
|
||||
RIGHT_SCROLL_AS_AC_PAN=0x005D,
|
||||
BUTTON_9=0x005E,
|
||||
BUTTON_10=0x005F,
|
||||
BUTTON_11=0x0060,
|
||||
BUTTON_12=0x0061,
|
||||
BUTTON_13=0x0062,
|
||||
BUTTON_14=0x0063,
|
||||
BUTTON_15=0x0064,
|
||||
BUTTON_16=0x0065,
|
||||
BUTTON_17=0x0066,
|
||||
BUTTON_18=0x0067,
|
||||
BUTTON_19=0x0068,
|
||||
BUTTON_20=0x0069,
|
||||
BUTTON_21=0x006A,
|
||||
BUTTON_22=0x006B,
|
||||
BUTTON_23=0x006C,
|
||||
BUTTON_24=0x006D,
|
||||
SHOW_DESKTOP=0x006E,
|
||||
Lock_PC=0x006F,
|
||||
FN_F1=0x0070,
|
||||
FN_F2=0x0071,
|
||||
FN_F3=0x0072,
|
||||
FN_F4=0x0073,
|
||||
FN_F5=0x0074,
|
||||
FN_F6=0x0075,
|
||||
FN_F7=0x0076,
|
||||
FN_F8=0x0077,
|
||||
FN_F9=0x0078,
|
||||
FN_F10=0x0079,
|
||||
FN_F11=0x007A,
|
||||
FN_F12=0x007B,
|
||||
FN_F13=0x007C,
|
||||
FN_F14=0x007D,
|
||||
FN_F15=0x007E,
|
||||
FN_F16=0x007F,
|
||||
FN_F17=0x0080,
|
||||
FN_F18=0x0081,
|
||||
FN_F19=0x0082,
|
||||
IOS_HOME=0x0083,
|
||||
ANDROID_HOME=0x0084,
|
||||
ANDROID_MENU=0x0085,
|
||||
ANDROID_SEARCH=0x0086,
|
||||
ANDROID_BACK=0x0087,
|
||||
HOME_COMBO=0x0088,
|
||||
LOCK_COMBO=0x0089,
|
||||
IOS_VIRTUAL_KEYBOARD=0x008A,
|
||||
IOS_LANGUAGE_SWICH=0x008B,
|
||||
MAC_EXPOSE=0x008C,
|
||||
MAC_DASHBOARD=0x008D,
|
||||
WIN7_SNAP_LEFT=0x008E,
|
||||
WIN7_SNAP_RIGHT=0x008F,
|
||||
WIN7_MINIMIZE_AS_WIN_ARROW=0x0090,
|
||||
WIN7_MAXIMIZE_AS_WIN_ARROW=0x0091,
|
||||
WIN7_STRETCH_UP=0x0092,
|
||||
WIN7_MONITOR_SWITCH_AS_WIN_SHIFT_LEFTARROW=0x0093,
|
||||
WIN7_MONITOR_SWITCH_AS_WIN_SHIFT_RIGHTARROW=0x0094,
|
||||
WIN7_SHOW_PRESENTATION_MODE=0x0095,
|
||||
WIN7_SHOW_MOBILITY_CENTER=0x0096,
|
||||
ANALOG_HSCROLL=0x0097,
|
||||
METRO_APPSWITCH=0x009F,
|
||||
METRO_APPBAR=0x00A0,
|
||||
METRO_CHARMS=0x00A1,
|
||||
CALC_VKEYBOARD=0x00A2,
|
||||
METRO_SEARCH=0x00A3,
|
||||
COMBO_SLEEP=0x00A4,
|
||||
METRO_SHARE=0x00A5,
|
||||
METRO_SETTINGS=0x00A6,
|
||||
METRO_DEVICES=0x00A7,
|
||||
METRO_START_SCREEN=0x00A9,
|
||||
ZOOMIN=0x00AA,
|
||||
ZOOMOUT=0x00AB,
|
||||
BACK_HSCROLL=0x00AC,
|
||||
SHOW_DESKTOP_HPP=0x00AE,
|
||||
)
|
||||
CONTROL._fallback = lambda x: 'unknown:%04X' % x
|
||||
|
||||
# <tasks.xml awk -F\" '/<Task /{gsub(/ /, "_", $6); printf("\t%s=0x%04X,\n", $6, $4)}'
|
||||
TASK = _NamedInts(
|
||||
Volume_Up=0x0001,
|
||||
Volume_Down=0x0002,
|
||||
Mute=0x0003,
|
||||
# Multimedia tasks:
|
||||
Play__Pause=0x0004,
|
||||
Next=0x0005,
|
||||
Previous=0x0006,
|
||||
Stop=0x0007,
|
||||
|
||||
Application_Switcher=0x0008,
|
||||
BurnMediaPlayer=0x0009,
|
||||
Calculator=0x000A,
|
||||
Calendar=0x000B,
|
||||
Close_Application=0x000C,
|
||||
Eject=0x000D,
|
||||
Email=0x000E,
|
||||
Help=0x000F,
|
||||
OffDocument=0x0010,
|
||||
OffSpreadsheet=0x0011,
|
||||
OffPowerpnt=0x0012,
|
||||
Undo=0x0013,
|
||||
Redo=0x0014,
|
||||
Print=0x0015,
|
||||
Save=0x0016,
|
||||
SmartKeySet=0x0017,
|
||||
Favorites=0x0018,
|
||||
GadgetsSet=0x0019,
|
||||
HomePage=0x001A,
|
||||
WindowsRestore=0x001B,
|
||||
WindowsMinimize=0x001C,
|
||||
Music=0x001D, # also known as MediaPlayer
|
||||
|
||||
# Both 0x001E and 0x001F are known as MediaCenterSet
|
||||
Media_Center_Logitech=0x001E,
|
||||
Media_Center_Microsoft=0x001F,
|
||||
|
||||
UserMenu=0x0020,
|
||||
Messenger=0x0021,
|
||||
PersonalFolders=0x0022,
|
||||
MyMusic=0x0023,
|
||||
Webcam=0x0024,
|
||||
PicturesFolder=0x0025,
|
||||
MyVideos=0x0026,
|
||||
My_Computer=0x0027,
|
||||
PictureAppSet=0x0028,
|
||||
Search=0x0029, # also known as AdvSmartSearch
|
||||
RecordMediaPlayer=0x002A,
|
||||
BrowserRefresh=0x002B,
|
||||
RotateRight=0x002C,
|
||||
SearchForFiles=0x002D,
|
||||
MM_SHUFFLE=0x002E,
|
||||
Sleep=0x002F, # also known as StandBySet
|
||||
BrowserStop=0x0030,
|
||||
OneTouchSync=0x0031,
|
||||
ZoomSet=0x0032,
|
||||
ZoomBtnInSet2=0x0033,
|
||||
ZoomBtnInSet=0x0034,
|
||||
ZoomBtnOutSet2=0x0035,
|
||||
ZoomBtnOutSet=0x0036,
|
||||
ZoomBtnResetSet=0x0037,
|
||||
LeftClick=0x0038,
|
||||
RightClick=0x0039,
|
||||
MiddleMouseButton=0x003A,
|
||||
Back=0x003B,
|
||||
BackEx=0x003C,
|
||||
BrowserForward=0x003D,
|
||||
BrowserForwardEx=0x003E,
|
||||
HorzScrollLeftSet=0x003F,
|
||||
HorzScrollRightSet=0x0040,
|
||||
QuickSwitch=0x0041,
|
||||
BatteryStatus=0x0042,
|
||||
ShowDesktop=0x0043,
|
||||
WindowsLock=0x0044,
|
||||
FileLauncher=0x0045,
|
||||
FolderLauncher=0x0046,
|
||||
GotoWebAddress=0x0047,
|
||||
GenericMouseButton=0x0048,
|
||||
KeystrokeAssignment=0x0049,
|
||||
LaunchProgram=0x004A,
|
||||
MinMaxWindow=0x004B,
|
||||
VOLUMEMUTE_NoOSD=0x004C,
|
||||
New=0x004D,
|
||||
Copy=0x004E,
|
||||
CruiseDown=0x004F,
|
||||
CruiseUp=0x0050,
|
||||
Cut=0x0051,
|
||||
Do_Nothing=0x0052,
|
||||
PageDown=0x0053,
|
||||
PageUp=0x0054,
|
||||
Paste=0x0055,
|
||||
SearchPicture=0x0056,
|
||||
Reply=0x0057,
|
||||
PhotoGallerySet=0x0058,
|
||||
MM_REWIND=0x0059,
|
||||
MM_FASTFORWARD=0x005A,
|
||||
Send=0x005B,
|
||||
ControlPanel=0x005C,
|
||||
UniversalScroll=0x005D,
|
||||
AutoScroll=0x005E,
|
||||
GenericButton=0x005F,
|
||||
MM_NEXT=0x0060,
|
||||
MM_PREVIOUS=0x0061,
|
||||
Do_Nothing_One=0x0062, # also known as Do_Nothing
|
||||
SnapLeft=0x0063,
|
||||
SnapRight=0x0064,
|
||||
WinMinRestore=0x0065,
|
||||
WinMaxRestore=0x0066,
|
||||
WinStretch=0x0067,
|
||||
SwitchMonitorLeft=0x0068,
|
||||
SwitchMonitorRight=0x0069,
|
||||
ShowPresentation=0x006A,
|
||||
ShowMobilityCenter=0x006B,
|
||||
HorzScrollNoRepeatSet=0x006C,
|
||||
TouchBackForwardHorzScroll=0x0077,
|
||||
MetroAppSwitch=0x0078,
|
||||
MetroAppBar=0x0079,
|
||||
MetroCharms=0x007A,
|
||||
Calculator_VKEY=0x007B, # also known as Calculator
|
||||
MetroSearch=0x007C,
|
||||
MetroStartScreen=0x0080,
|
||||
MetroShare=0x007D,
|
||||
MetroSettings=0x007E,
|
||||
MetroDevices=0x007F,
|
||||
MetroBackLeftHorz=0x0082,
|
||||
MetroForwRightHorz=0x0083,
|
||||
Win8_Back=0x0084, # also known as MetroCharms
|
||||
Win8_Forward=0x0085, # also known as AppSwitchBar
|
||||
)
|
||||
TASK._fallback = lambda x: 'unknown:%04X' % x
|
||||
|
||||
KEY_FLAG = _NamedInts(
|
||||
reprogrammable=0x10,
|
||||
FN_sensitive=0x08,
|
||||
nonstandard=0x04,
|
||||
is_FN=0x02,
|
||||
mse=0x01
|
||||
)
|
||||
325
lib/logitech_receiver/status.py
Normal file
325
lib/logitech_receiver/status.py
Normal file
@@ -0,0 +1,325 @@
|
||||
# -*- python-mode -*-
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
## Copyright (C) 2012-2013 Daniel Pavel
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published by
|
||||
## the Free Software Foundation; either version 2 of the License, or
|
||||
## (at your option) any later version.
|
||||
##
|
||||
## This program is distributed in the hope that it will be useful,
|
||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
## GNU General Public License for more details.
|
||||
##
|
||||
## You should have received a copy of the GNU General Public License along
|
||||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
from time import time as _timestamp
|
||||
|
||||
from logging import getLogger, DEBUG as _DEBUG
|
||||
_log = getLogger(__name__)
|
||||
del getLogger
|
||||
|
||||
|
||||
from .i18n import _
|
||||
from .common import NamedInts as _NamedInts, NamedInt as _NamedInt
|
||||
from . import hidpp10 as _hidpp10
|
||||
from . import hidpp20 as _hidpp20
|
||||
|
||||
_R = _hidpp10.REGISTERS
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
ALERT = _NamedInts(NONE=0x00, NOTIFICATION=0x01, SHOW_WINDOW=0x02, ATTENTION=0x04, ALL=0xFF)
|
||||
|
||||
KEYS = _NamedInts(
|
||||
BATTERY_LEVEL=1,
|
||||
BATTERY_CHARGING=2,
|
||||
BATTERY_STATUS=3,
|
||||
LIGHT_LEVEL=4,
|
||||
LINK_ENCRYPTED=5,
|
||||
NOTIFICATION_FLAGS=6,
|
||||
ERROR=7,
|
||||
)
|
||||
|
||||
# If the battery charge is under this percentage, trigger an attention event
|
||||
# (blink systray icon/notification/whatever).
|
||||
_BATTERY_ATTENTION_LEVEL = 5
|
||||
|
||||
# If no updates have been receiver from the device for a while, ping the device
|
||||
# and update it status accordinly.
|
||||
# _STATUS_TIMEOUT = 5 * 60 # seconds
|
||||
_LONG_SLEEP = 15 * 60 # seconds
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def attach_to(device, changed_callback):
|
||||
assert device
|
||||
assert changed_callback
|
||||
|
||||
if device.kind is None:
|
||||
device.status = ReceiverStatus(device, changed_callback)
|
||||
else:
|
||||
device.status = DeviceStatus(device, changed_callback)
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
class ReceiverStatus(dict):
|
||||
"""The 'runtime' status of a receiver, mostly about the pairing process --
|
||||
is the pairing lock open or closed, any pairing errors, etc.
|
||||
"""
|
||||
def __init__(self, receiver, changed_callback):
|
||||
assert receiver
|
||||
self._receiver = receiver
|
||||
|
||||
assert changed_callback
|
||||
self._changed_callback = changed_callback
|
||||
|
||||
# self.updated = 0
|
||||
|
||||
self.lock_open = False
|
||||
self.new_device = None
|
||||
|
||||
self[KEYS.ERROR] = None
|
||||
|
||||
def __str__(self):
|
||||
count = len(self._receiver)
|
||||
return (_("No paired devices.") if count == 0 else
|
||||
_("1 paired device.") if count == 1 else
|
||||
(str(count) + _(" paired devices.")))
|
||||
__unicode__ = __str__
|
||||
|
||||
def changed(self, alert=ALERT.NOTIFICATION, reason=None):
|
||||
# self.updated = _timestamp()
|
||||
self._changed_callback(self._receiver, alert=alert, reason=reason)
|
||||
|
||||
# def poll(self, timestamp):
|
||||
# r = self._receiver
|
||||
# assert r
|
||||
#
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("polling status of %s", r)
|
||||
#
|
||||
# # make sure to read some stuff that may be read later by the UI
|
||||
# r.serial, r.firmware, None
|
||||
#
|
||||
# # get an update of the notification flags
|
||||
# # self[KEYS.NOTIFICATION_FLAGS] = _hidpp10.get_notification_flags(r)
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
class DeviceStatus(dict):
|
||||
"""Holds the 'runtime' status of a peripheral -- things like
|
||||
active/inactive, battery charge, lux, etc. It updates them mostly by
|
||||
processing incoming notification events from the device itself.
|
||||
"""
|
||||
def __init__(self, device, changed_callback):
|
||||
assert device
|
||||
self._device = device
|
||||
|
||||
assert changed_callback
|
||||
self._changed_callback = changed_callback
|
||||
|
||||
# is the device active?
|
||||
self._active = None
|
||||
|
||||
# timestamp of when this status object was last updated
|
||||
self.updated = 0
|
||||
|
||||
def __str__(self):
|
||||
def _item(name, format):
|
||||
value = self.get(name)
|
||||
if value is not None:
|
||||
return format % value
|
||||
|
||||
def _items():
|
||||
# TODO properly string approximative battery levels
|
||||
battery_level = self.get(KEYS.BATTERY_LEVEL)
|
||||
if battery_level is not None:
|
||||
if isinstance(battery_level, _NamedInt):
|
||||
yield _("Battery") + ': ' + _(str(battery_level))
|
||||
else:
|
||||
yield _("Battery") + ': ' + ('%d%%' % battery_level)
|
||||
|
||||
battery_status = _item(KEYS.BATTERY_STATUS, ' (%s)')
|
||||
if battery_status:
|
||||
yield battery_status
|
||||
|
||||
light_level = _item(KEYS.LIGHT_LEVEL, _("Lighting") + ': %d ' + _("lux"))
|
||||
if light_level:
|
||||
if battery_level:
|
||||
yield ', '
|
||||
yield light_level
|
||||
|
||||
return ''.join(i for i in _items())
|
||||
|
||||
__unicode__ = __str__
|
||||
|
||||
def __bool__(self):
|
||||
return bool(self._active)
|
||||
__nonzero__ = __bool__
|
||||
|
||||
def set_battery_info(self, level, status, timestamp=None):
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("%s: battery %s, %s", self._device, level, status)
|
||||
|
||||
if level is None:
|
||||
# Some notifications may come with no battery level info, just
|
||||
# charging state info, so assume the level is unchanged.
|
||||
level = self.get(KEYS.BATTERY_LEVEL)
|
||||
else:
|
||||
assert isinstance(level, int)
|
||||
|
||||
# TODO: this is also executed when pressing Fn+F7 on K800.
|
||||
old_level, self[KEYS.BATTERY_LEVEL] = self.get(KEYS.BATTERY_LEVEL), level
|
||||
old_status, self[KEYS.BATTERY_STATUS] = self.get(KEYS.BATTERY_STATUS), status
|
||||
|
||||
charging = status in (_hidpp20.BATTERY_STATUS.recharging, _hidpp20.BATTERY_STATUS.slow_recharge)
|
||||
old_charging, self[KEYS.BATTERY_CHARGING] = self.get(KEYS.BATTERY_CHARGING), charging
|
||||
|
||||
changed = old_level != level or old_status != status or old_charging != charging
|
||||
alert, reason = ALERT.NONE, None
|
||||
|
||||
if _hidpp20.BATTERY_OK(status) and level > _BATTERY_ATTENTION_LEVEL:
|
||||
self[KEYS.ERROR] = None
|
||||
else:
|
||||
_log.warn("%s: battery %d%%, ALERT %s", self._device, level, status)
|
||||
if self.get(KEYS.ERROR) != status:
|
||||
self[KEYS.ERROR] = status
|
||||
# only show the notification once
|
||||
alert = ALERT.NOTIFICATION | ALERT.ATTENTION
|
||||
if isinstance(level, _NamedInt):
|
||||
reason = 'battery: %s (%s)' % (level, status)
|
||||
else:
|
||||
reason = 'battery: %d%% (%s)' % (level, status)
|
||||
|
||||
if changed or reason:
|
||||
# update the leds on the device, if any
|
||||
_hidpp10.set_3leds(self._device, level, charging=charging, warning=bool(alert))
|
||||
self.changed(active=True, alert=alert, reason=reason, timestamp=timestamp)
|
||||
|
||||
def read_battery(self, timestamp=None):
|
||||
if self._active:
|
||||
d = self._device
|
||||
assert d
|
||||
|
||||
if d.protocol < 2.0:
|
||||
battery = _hidpp10.get_battery(d)
|
||||
else:
|
||||
battery = _hidpp20.get_battery(d)
|
||||
|
||||
# Really unnecessary, if the device has SOLAR_DASHBOARD it should be
|
||||
# broadcasting it's battery status anyway, it will just take a little while.
|
||||
# However, when the device has just been detected, it will not show
|
||||
# any battery status for a while (broadcasts happen every 90 seconds).
|
||||
if battery is None and _hidpp20.FEATURE.SOLAR_DASHBOARD in d.features:
|
||||
d.feature_request(_hidpp20.FEATURE.SOLAR_DASHBOARD, 0x00, 1, 1)
|
||||
return
|
||||
|
||||
if battery is not None:
|
||||
level, status = battery
|
||||
self.set_battery_info(level, status)
|
||||
elif KEYS.BATTERY_STATUS in self:
|
||||
self[KEYS.BATTERY_STATUS] = None
|
||||
self[KEYS.BATTERY_CHARGING] = None
|
||||
self.changed()
|
||||
|
||||
def changed(self, active=None, alert=ALERT.NONE, reason=None, timestamp=None):
|
||||
assert self._changed_callback
|
||||
d = self._device
|
||||
# assert d # may be invalid when processing the 'unpaired' notification
|
||||
timestamp = timestamp or _timestamp()
|
||||
|
||||
if active is not None:
|
||||
d.online = active
|
||||
was_active, self._active = self._active, active
|
||||
if active:
|
||||
if not was_active:
|
||||
# Make sure to set notification flags on the device, they
|
||||
# get cleared when the device is turned off (but not when the device
|
||||
# goes idle, and we can't tell the difference right now).
|
||||
if d.protocol < 2.0:
|
||||
self[KEYS.NOTIFICATION_FLAGS] = d.enable_notifications()
|
||||
|
||||
# If we've been inactive for a long time, forget anything
|
||||
# about the battery.
|
||||
if self.updated > 0 and timestamp - self.updated > _LONG_SLEEP:
|
||||
self[KEYS.BATTERY_LEVEL] = None
|
||||
self[KEYS.BATTERY_STATUS] = None
|
||||
self[KEYS.BATTERY_CHARGING] = None
|
||||
|
||||
# Devices lose configuration when they are turned off,
|
||||
# make sure they're up-to-date.
|
||||
# _log.debug("%s settings %s", d, d.settings)
|
||||
for s in d.settings:
|
||||
s.apply()
|
||||
|
||||
if self.get(KEYS.BATTERY_LEVEL) is None:
|
||||
self.read_battery(timestamp)
|
||||
else:
|
||||
if was_active:
|
||||
battery = self.get(KEYS.BATTERY_LEVEL)
|
||||
self.clear()
|
||||
# If we had a known battery level before, assume it's not going
|
||||
# to change much while the device is offline.
|
||||
if battery is not None:
|
||||
self[KEYS.BATTERY_LEVEL] = battery
|
||||
|
||||
if self.updated == 0 and active == True:
|
||||
# if the device is active on the very first status notification,
|
||||
# (meaning just when the program started or a new receiver was just
|
||||
# detected), pop-up a notification about it
|
||||
alert |= ALERT.NOTIFICATION
|
||||
self.updated = timestamp
|
||||
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("device %d changed: active=%s %s", d.number, self._active, dict(self))
|
||||
self._changed_callback(d, alert, reason)
|
||||
|
||||
# def poll(self, timestamp):
|
||||
# d = self._device
|
||||
# if not d:
|
||||
# _log.error("polling status of invalid device")
|
||||
# return
|
||||
#
|
||||
# if self._active:
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("polling status of %s", d)
|
||||
#
|
||||
# # read these from the device, the UI may need them later
|
||||
# d.protocol, d.serial, d.firmware, d.kind, d.name, d.settings, None
|
||||
#
|
||||
# # make sure we know all the features of the device
|
||||
# # if d.features:
|
||||
# # d.features[:]
|
||||
#
|
||||
# # devices may go out-of-range while still active, or the computer
|
||||
# # may go to sleep and wake up without the devices available
|
||||
# if timestamp - self.updated > _STATUS_TIMEOUT:
|
||||
# if d.ping():
|
||||
# timestamp = self.updated = _timestamp()
|
||||
# else:
|
||||
# self.changed(active=False, reason='out of range')
|
||||
#
|
||||
# # if still active, make sure we know the battery level
|
||||
# if KEYS.BATTERY_LEVEL not in self:
|
||||
# self.read_battery(timestamp)
|
||||
#
|
||||
# elif timestamp - self.updated > _STATUS_TIMEOUT:
|
||||
# if d.ping():
|
||||
# self.changed(active=True)
|
||||
# else:
|
||||
# self.updated = _timestamp()
|
||||
23
lib/solaar/__init__.py
Normal file
23
lib/solaar/__init__.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# -*- python-mode -*-
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
## Copyright (C) 2012-2013 Daniel Pavel
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published by
|
||||
## the Free Software Foundation; either version 2 of the License, or
|
||||
## (at your option) any later version.
|
||||
##
|
||||
## This program is distributed in the hope that it will be useful,
|
||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
## GNU General Public License for more details.
|
||||
##
|
||||
## You should have received a copy of the GNU General Public License along
|
||||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
__version__ = '0.9.2'
|
||||
NAME = 'Solaar'
|
||||
434
lib/solaar/cli.py
Normal file
434
lib/solaar/cli.py
Normal file
@@ -0,0 +1,434 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- python-mode -*-
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
## Copyright (C) 2012-2013 Daniel Pavel
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published by
|
||||
## the Free Software Foundation; either version 2 of the License, or
|
||||
## (at your option) any later version.
|
||||
##
|
||||
## This program is distributed in the hope that it will be useful,
|
||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
## GNU General Public License for more details.
|
||||
##
|
||||
## You should have received a copy of the GNU General Public License along
|
||||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
import sys
|
||||
import logging
|
||||
|
||||
|
||||
NAME = 'solaar-cli'
|
||||
from solaar import __version__
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def _fail(text):
|
||||
if sys.exc_info()[0]:
|
||||
logging.exception(text)
|
||||
sys.exit("%s: error: %s" % (NAME, text))
|
||||
|
||||
|
||||
def _require(module, os_package):
|
||||
try:
|
||||
__import__(module)
|
||||
except ImportError:
|
||||
_fail("missing required package '%s'" % os_package)
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def _receiver(dev_path=None):
|
||||
from logitech_receiver import Receiver
|
||||
from logitech_receiver.base import receivers
|
||||
for dev_info in receivers():
|
||||
if dev_path is not None and dev_path != dev_info.path:
|
||||
continue
|
||||
try:
|
||||
r = Receiver.open(dev_info)
|
||||
if r:
|
||||
return r
|
||||
except Exception as e:
|
||||
_fail(str(e))
|
||||
return r
|
||||
_fail("Logitech receiver not found")
|
||||
|
||||
|
||||
def _find_device(receiver, name, may_be_receiver=False):
|
||||
if len(name) == 1:
|
||||
try:
|
||||
number = int(name)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
if number < 1 or number > receiver.max_devices:
|
||||
_fail("%s (%s) supports device numbers 1 to %d" % (receiver.name, receiver.path, receiver.max_devices))
|
||||
dev = receiver[number]
|
||||
if dev is None:
|
||||
_fail("no paired device with number %s" % number)
|
||||
return dev
|
||||
|
||||
if len(name) < 3:
|
||||
_fail("need at least 3 characters to match a device")
|
||||
|
||||
name = name.lower()
|
||||
if may_be_receiver and ('receiver'.startswith(name) or name == receiver.serial.lower()):
|
||||
return receiver
|
||||
|
||||
for dev in receiver:
|
||||
if (name == dev.serial.lower() or
|
||||
name == dev.codename.lower() or
|
||||
name == str(dev.kind).lower() or
|
||||
name in dev.name.lower()):
|
||||
return dev
|
||||
|
||||
_fail("no device found matching '%s'" % name)
|
||||
|
||||
|
||||
def _print_receiver(receiver, verbose=False):
|
||||
paired_count = receiver.count()
|
||||
if not verbose:
|
||||
print ("Unifying Receiver [%s:%s] with %d devices" % (receiver.path, receiver.serial, paired_count))
|
||||
return
|
||||
|
||||
print ("Unifying Receiver")
|
||||
print (" Device path :", receiver.path)
|
||||
print (" USB id : 046d:%s" % receiver.product_id)
|
||||
print (" Serial :", receiver.serial)
|
||||
for f in receiver.firmware:
|
||||
print (" %-11s: %s" % (f.kind, f.version))
|
||||
|
||||
print (" Has", paired_count, "paired device(s) out of a maximum of", receiver.max_devices, ".")
|
||||
|
||||
from logitech_receiver import hidpp10
|
||||
notification_flags = hidpp10.get_notification_flags(receiver)
|
||||
if notification_flags is not None:
|
||||
if notification_flags:
|
||||
notification_names = hidpp10.NOTIFICATION_FLAG.flag_names(notification_flags)
|
||||
print (" Notifications: 0x%06X = %s" % (notification_flags, ', '.join(notification_names)))
|
||||
else:
|
||||
print (" Notifications: (none)")
|
||||
|
||||
activity = receiver.read_register(hidpp10.REGISTERS.devices_activity)
|
||||
if activity:
|
||||
activity = [(d, ord(activity[d - 1:d])) for d in range(1, receiver.max_devices)]
|
||||
activity_text = ', '.join(('%d=%d' % (d, a)) for d, a in activity if a > 0)
|
||||
print (" Device activity counters:", activity_text or '(empty)')
|
||||
|
||||
|
||||
def _print_device(dev, verbose=False):
|
||||
assert dev
|
||||
state = '' if dev.ping() else 'offline'
|
||||
|
||||
if not verbose:
|
||||
print ("%d: %s [%s:%s]" % (dev.number, dev.name, dev.codename, dev.serial), state)
|
||||
return
|
||||
|
||||
print ("%d: %s" % (dev.number, dev.name))
|
||||
print (" Codename :", dev.codename)
|
||||
print (" Kind :", dev.kind)
|
||||
print (" Wireless PID :", dev.wpid)
|
||||
if dev.protocol:
|
||||
print (" Protocol : HID++ %1.1f" % dev.protocol)
|
||||
else:
|
||||
print (" Protocol : unknown (device is offline)")
|
||||
print (" Polling rate :", dev.polling_rate, "ms")
|
||||
print (" Serial number:", dev.serial)
|
||||
for fw in dev.firmware:
|
||||
print (" %11s:" % fw.kind, (fw.name + ' ' + fw.version).strip())
|
||||
|
||||
if dev.power_switch_location:
|
||||
print (" The power switch is located on the %s." % dev.power_switch_location)
|
||||
|
||||
from logitech_receiver import hidpp10, hidpp20, special_keys
|
||||
|
||||
if dev.online:
|
||||
notification_flags = hidpp10.get_notification_flags(dev)
|
||||
if notification_flags is not None:
|
||||
if notification_flags:
|
||||
notification_names = hidpp10.NOTIFICATION_FLAG.flag_names(notification_flags)
|
||||
print (" Notifications: 0x%06X = %s." % (notification_flags, ', '.join(notification_names)))
|
||||
else:
|
||||
print (" Notifications: (none).")
|
||||
|
||||
if dev.online:
|
||||
if dev.features:
|
||||
print (" Supports %d HID++ 2.0 features:" % len(dev.features))
|
||||
for index, feature in enumerate(dev.features):
|
||||
feature = dev.features[index]
|
||||
flags = dev.request(0x0000, feature.bytes(2))
|
||||
flags = 0 if flags is None else ord(flags[1:2])
|
||||
flags = hidpp20.FEATURE_FLAG.flag_names(flags)
|
||||
print (" %2d: %-22s {%04X} %s" % (index, feature, feature, ', '.join(flags)))
|
||||
|
||||
if dev.online:
|
||||
if dev.keys:
|
||||
print (" Has %d reprogrammable keys:" % len(dev.keys))
|
||||
for k in dev.keys:
|
||||
flags = special_keys.KEY_FLAG.flag_names(k.flags)
|
||||
print (" %2d: %-26s => %-27s %s" % (k.index, k.key, k.task, ', '.join(flags)))
|
||||
|
||||
if dev.online:
|
||||
battery = hidpp20.get_battery(dev)
|
||||
if battery is None:
|
||||
battery = hidpp10.get_battery(dev)
|
||||
if battery is not None:
|
||||
from logitech_receiver.common import NamedInt as _NamedInt
|
||||
level, status = battery
|
||||
if isinstance(level, _NamedInt):
|
||||
text = str(level)
|
||||
else:
|
||||
text = '%d%%' % level
|
||||
print (" Battery: %s, %s," % (text, status))
|
||||
else:
|
||||
print (" Battery status unavailable.")
|
||||
else:
|
||||
print (" Battery status is unknown (device is offline).")
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def show_devices(receiver, args):
|
||||
if args.device == 'all':
|
||||
_print_receiver(receiver, args.verbose)
|
||||
for dev in receiver:
|
||||
if args.verbose:
|
||||
print ("")
|
||||
_print_device(dev, args.verbose)
|
||||
else:
|
||||
dev = _find_device(receiver, args.device, True)
|
||||
if dev is receiver:
|
||||
_print_receiver(receiver, args.verbose)
|
||||
else:
|
||||
_print_device(dev, args.verbose)
|
||||
|
||||
|
||||
def pair_device(receiver, args):
|
||||
# get all current devices
|
||||
known_devices = [dev.number for dev in receiver]
|
||||
|
||||
from logitech_receiver import base, hidpp10, status, notifications
|
||||
receiver.status = status.ReceiverStatus(receiver, lambda *args, **kwargs: None)
|
||||
|
||||
# check if it's necessary to set the notification flags
|
||||
old_notification_flags = hidpp10.get_notification_flags(receiver) or 0
|
||||
if not (old_notification_flags & hidpp10.NOTIFICATION_FLAG.wireless):
|
||||
hidpp10.set_notification_flags(receiver, old_notification_flags | hidpp10.NOTIFICATION_FLAG.wireless)
|
||||
|
||||
class HandleWithNotificationHook(int):
|
||||
def notifications_hook(self, n):
|
||||
assert n
|
||||
if n.devnumber == 0xFF:
|
||||
notifications.process(receiver, n)
|
||||
elif n.sub_id == 0x41 and n.address == 0x04:
|
||||
if n.devnumber not in known_devices:
|
||||
receiver.status.new_device = receiver[n.devnumber]
|
||||
|
||||
timeout = 20 # seconds
|
||||
receiver.handle = HandleWithNotificationHook(receiver.handle)
|
||||
receiver.set_lock(False, timeout=timeout)
|
||||
print ("Pairing: turn your new device on (timing out in", timeout, "seconds).")
|
||||
|
||||
# the lock-open notification may come slightly later, wait for it a bit
|
||||
from time import time as timestamp
|
||||
pairing_start = timestamp()
|
||||
patience = 5 # seconds
|
||||
|
||||
while receiver.status.lock_open or timestamp() - pairing_start < patience:
|
||||
n = base.read(receiver.handle)
|
||||
if n:
|
||||
n = base.make_notification(*n)
|
||||
if n:
|
||||
receiver.handle.notifications_hook(n)
|
||||
|
||||
if not (old_notification_flags & hidpp10.NOTIFICATION_FLAG.wireless):
|
||||
# only clear the flags if they weren't set before, otherwise a
|
||||
# concurrently running Solaar app might stop working properly
|
||||
hidpp10.set_notification_flags(receiver, old_notification_flags)
|
||||
|
||||
if receiver.status.new_device:
|
||||
dev = receiver.status.new_device
|
||||
print ("Paired device %d: %s [%s:%s:%s]" % (dev.number, dev.name, dev.wpid, dev.codename, dev.serial))
|
||||
else:
|
||||
error = receiver.status[status.KEYS.ERROR] or 'no device detected?'
|
||||
_fail(error)
|
||||
|
||||
|
||||
def unpair_device(receiver, args):
|
||||
dev = _find_device(receiver, args.device)
|
||||
|
||||
# query these now, it's last chance to get them
|
||||
number, name, codename, serial = dev.number, dev.name, dev.codename, dev.serial
|
||||
try:
|
||||
del receiver[number]
|
||||
print ("Unpaired %d: %s [%s:%s]" % (number, name, codename, serial))
|
||||
except Exception as e:
|
||||
_fail("failed to unpair device %s: %s" % (dev.name, e))
|
||||
|
||||
|
||||
def config_device(receiver, args):
|
||||
dev = _find_device(receiver, args.device)
|
||||
# if dev is receiver:
|
||||
# _fail("no settings for the receiver")
|
||||
|
||||
if not dev.settings:
|
||||
_fail("no settings for %s" % dev.name)
|
||||
|
||||
if not args.setting:
|
||||
print ("[%s:%s]" % (dev.serial, dev.kind))
|
||||
print ("#", dev.name)
|
||||
for s in dev.settings:
|
||||
print ("")
|
||||
print ("# %s" % s.label)
|
||||
if s.choices:
|
||||
print ("# possible values: one of [", ', '.join(str(v) for v in s.choices), "], or higher/lower/highest/max/lowest/min")
|
||||
else:
|
||||
print ("# possible values: on/true/t/yes/y/1 or off/false/f/no/n/0")
|
||||
value = s.read()
|
||||
if value is None:
|
||||
print ("# %s = ? (failed to read from device)" % s.name)
|
||||
else:
|
||||
print (s.name, "=", value)
|
||||
return
|
||||
|
||||
setting = None
|
||||
for s in dev.settings:
|
||||
if args.setting.lower() == s.name.lower():
|
||||
setting = s
|
||||
break
|
||||
if setting is None:
|
||||
_fail("no setting '%s' for %s" % (args.setting, dev.name))
|
||||
|
||||
if args.value is None:
|
||||
result = setting.read()
|
||||
if result is None:
|
||||
_fail("failed to read '%s'" % setting.name)
|
||||
print ("%s = %s" % (setting.name, setting.read()))
|
||||
return
|
||||
|
||||
from logitech_receiver import settings as _settings
|
||||
|
||||
if setting.kind == _settings.KIND.toggle:
|
||||
value = args.value
|
||||
try:
|
||||
value = bool(int(value))
|
||||
except:
|
||||
if value.lower() in ('1', 'true', 'yes', 'on', 't', 'y'):
|
||||
value = True
|
||||
elif value.lower() in ('0', 'false', 'no', 'off', 'f', 'n'):
|
||||
value = False
|
||||
else:
|
||||
_fail("don't know how to interpret '%s' as boolean" % value)
|
||||
|
||||
elif setting.choices:
|
||||
value = args.value.lower()
|
||||
|
||||
if value in ('higher', 'lower'):
|
||||
old_value = setting.read()
|
||||
if old_value is None:
|
||||
_fail("could not read current value of '%s'" % setting.name)
|
||||
|
||||
if value == 'lower':
|
||||
lower_values = setting.choices[:old_value]
|
||||
value = lower_values[-1] if lower_values else setting.choices[:][0]
|
||||
elif value == 'higher':
|
||||
higher_values = setting.choices[old_value + 1:]
|
||||
value = higher_values[0] if higher_values else setting.choices[:][-1]
|
||||
elif value in ('highest', 'max'):
|
||||
value = setting.choices[:][-1]
|
||||
elif value in ('lowest', 'min'):
|
||||
value = setting.choices[:][0]
|
||||
elif value not in setting.choices:
|
||||
_fail("possible values for '%s' are: [%s]" % (setting.name, ', '.join(str(v) for v in setting.choices)))
|
||||
value = setting.choices[value]
|
||||
|
||||
else:
|
||||
raise NotImplemented
|
||||
|
||||
result = setting.write(value)
|
||||
if result is None:
|
||||
_fail("failed to set '%s' = '%s' [%r]" % (setting.name, value, value))
|
||||
print ("%s = %s" % (setting.name, result))
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def _parse_arguments():
|
||||
from argparse import ArgumentParser
|
||||
arg_parser = ArgumentParser(prog=NAME.lower())
|
||||
arg_parser.add_argument('-d', '--debug', action='count', default=0,
|
||||
help='print logging messages, for debugging purposes (may be repeated for extra verbosity)')
|
||||
arg_parser.add_argument('-V', '--version', action='version', version='%(prog)s ' + __version__)
|
||||
arg_parser.add_argument('-D', '--hidraw', action='store', dest='hidraw_path', metavar='PATH',
|
||||
help='unifying receiver to use; the first detected receiver if unspecified. Example: /dev/hidraw2')
|
||||
|
||||
subparsers = arg_parser.add_subparsers(title='commands')
|
||||
|
||||
sp = subparsers.add_parser('show', help='show information about paired devices')
|
||||
sp.add_argument('device', nargs='?', default='all',
|
||||
help='device to show information about; may be a device number (1..6), a device serial, '
|
||||
'at least 3 characters of a device\'s name, "receiver", or "all" (the default)')
|
||||
sp.add_argument('-v', '--verbose', action='store_true',
|
||||
help='print all available information about the inspected device(s)')
|
||||
sp.set_defaults(cmd=show_devices)
|
||||
|
||||
sp = subparsers.add_parser('config', help='read/write device-specific settings',
|
||||
epilog='Please note that configuration only works on active devices.')
|
||||
sp.add_argument('device',
|
||||
help='device to configure; may be a device number (1..6), a device serial, '
|
||||
'or at least 3 characters of a device\'s name')
|
||||
sp.add_argument('setting', nargs='?',
|
||||
help='device-specific setting; leave empty to list available settings')
|
||||
sp.add_argument('value', nargs='?',
|
||||
help='new value for the setting')
|
||||
sp.set_defaults(cmd=config_device)
|
||||
|
||||
sp = subparsers.add_parser('pair', help='pair a new device',
|
||||
epilog='The Logitech Unifying Receiver supports up to 6 paired devices at the same time.')
|
||||
sp.set_defaults(cmd=pair_device)
|
||||
|
||||
sp = subparsers.add_parser('unpair', help='unpair a device')
|
||||
sp.add_argument('device',
|
||||
help='device to unpair; may be a device number (1..6), a device serial, '
|
||||
'or at least 3 characters of a device\'s name.')
|
||||
sp.set_defaults(cmd=unpair_device)
|
||||
|
||||
args = arg_parser.parse_args()
|
||||
|
||||
# Python 3 has an undocumented 'feature' that breaks parsing empty args
|
||||
# http://bugs.python.org/issue16308
|
||||
if not 'cmd' in args:
|
||||
arg_parser.print_usage(sys.stderr)
|
||||
sys.stderr.write('%s: error: too few arguments\n' % NAME.lower())
|
||||
sys.exit(2)
|
||||
|
||||
if args.debug > 0:
|
||||
log_level = logging.WARNING - 10 * args.debug
|
||||
log_format='%(asctime)s,%(msecs)03d %(levelname)8s %(name)s: %(message)s'
|
||||
logging.basicConfig(level=max(log_level, logging.DEBUG), format=log_format, datefmt='%H:%M:%S')
|
||||
else:
|
||||
logging.root.addHandler(logging.NullHandler())
|
||||
logging.root.setLevel(logging.ERROR)
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def main():
|
||||
_require('pyudev', 'python-pyudev')
|
||||
args = _parse_arguments()
|
||||
receiver = _receiver(args.hidraw_path)
|
||||
args.cmd(receiver, args)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
133
lib/solaar/configuration.py
Normal file
133
lib/solaar/configuration.py
Normal file
@@ -0,0 +1,133 @@
|
||||
# -*- python-mode -*-
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
## Copyright (C) 2012-2013 Daniel Pavel
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published by
|
||||
## the Free Software Foundation; either version 2 of the License, or
|
||||
## (at your option) any later version.
|
||||
##
|
||||
## This program is distributed in the hope that it will be useful,
|
||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
## GNU General Public License for more details.
|
||||
##
|
||||
## You should have received a copy of the GNU General Public License along
|
||||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
import os as _os
|
||||
import os.path as _path
|
||||
from json import load as _json_load, dump as _json_save
|
||||
|
||||
from logging import getLogger, DEBUG as _DEBUG, INFO as _INFO
|
||||
_log = getLogger(__name__)
|
||||
del getLogger
|
||||
|
||||
_XDG_CONFIG_HOME = _os.environ.get('XDG_CONFIG_HOME') or _path.expanduser(_path.join('~', '.config'))
|
||||
_file_path = _path.join(_XDG_CONFIG_HOME, 'solaar', 'config.json')
|
||||
|
||||
|
||||
from solaar import __version__
|
||||
_KEY_VERSION = '_version'
|
||||
_KEY_NAME = '_name'
|
||||
_configuration = {}
|
||||
|
||||
|
||||
|
||||
def _load():
|
||||
if _path.isfile(_file_path):
|
||||
loaded_configuration = {}
|
||||
try:
|
||||
with open(_file_path, 'r') as config_file:
|
||||
loaded_configuration = _json_load(config_file)
|
||||
except:
|
||||
_log.error("failed to load from %s", _file_path)
|
||||
|
||||
# loaded_configuration.update(_configuration)
|
||||
_configuration.clear()
|
||||
_configuration.update(loaded_configuration)
|
||||
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("load => %s", _configuration)
|
||||
|
||||
_cleanup(_configuration)
|
||||
_configuration[_KEY_VERSION] = __version__
|
||||
return _configuration
|
||||
|
||||
|
||||
def save():
|
||||
# don't save if the configuration hasn't been loaded
|
||||
if _KEY_VERSION not in _configuration:
|
||||
return
|
||||
|
||||
dirname = _os.path.dirname(_file_path)
|
||||
if not _path.isdir(dirname):
|
||||
try:
|
||||
_os.makedirs(dirname)
|
||||
except:
|
||||
_log.error("failed to create %s", dirname)
|
||||
return False
|
||||
|
||||
_cleanup(_configuration)
|
||||
|
||||
try:
|
||||
with open(_file_path, 'w') as config_file:
|
||||
_json_save(_configuration, config_file, skipkeys=True, indent=2, sort_keys=True)
|
||||
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("saved %s to %s", _configuration, _file_path)
|
||||
return True
|
||||
except:
|
||||
_log.error("failed to save to %s", _file_path)
|
||||
|
||||
|
||||
def _cleanup(d):
|
||||
# remove None values from the dict
|
||||
for key in list(d.keys()):
|
||||
value = d.get(key)
|
||||
if value is None:
|
||||
del d[key]
|
||||
elif isinstance(value, dict):
|
||||
_cleanup(value)
|
||||
|
||||
|
||||
def _device_key(device):
|
||||
return '%s:%s' % (device.wpid, device.serial)
|
||||
|
||||
|
||||
class _DeviceEntry(dict):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(_DeviceEntry, self).__init__(*args, **kwargs)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
super(_DeviceEntry, self).__setitem__(key, value)
|
||||
save()
|
||||
|
||||
|
||||
def _device_entry(device):
|
||||
if not _configuration:
|
||||
_load()
|
||||
|
||||
device_key = _device_key(device)
|
||||
c = _configuration.get(device_key) or {}
|
||||
|
||||
if not isinstance(c, _DeviceEntry):
|
||||
c[_KEY_NAME] = device.name
|
||||
c = _DeviceEntry(c)
|
||||
_configuration[device_key] = c
|
||||
|
||||
return c
|
||||
|
||||
|
||||
def attach_to(device):
|
||||
"""Apply the last saved configuration to a device."""
|
||||
if not _configuration:
|
||||
_load()
|
||||
|
||||
persister = _device_entry(device)
|
||||
for s in device.settings:
|
||||
if s.persister is None:
|
||||
s.persister = persister
|
||||
assert s.persister == persister
|
||||
90
lib/solaar/gtk.py
Normal file
90
lib/solaar/gtk.py
Normal file
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- python-mode -*-
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
## Copyright (C) 2012-2013 Daniel Pavel
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published by
|
||||
## the Free Software Foundation; either version 2 of the License, or
|
||||
## (at your option) any later version.
|
||||
##
|
||||
## This program is distributed in the hope that it will be useful,
|
||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
## GNU General Public License for more details.
|
||||
##
|
||||
## You should have received a copy of the GNU General Public License along
|
||||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
|
||||
from solaar import __version__, NAME
|
||||
import solaar.i18n as _i18n
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def _require(module, os_package):
|
||||
try:
|
||||
__import__(module)
|
||||
except ImportError:
|
||||
import sys
|
||||
sys.exit("%s: missing required package '%s'" % (NAME, os_package))
|
||||
|
||||
|
||||
def _parse_arguments():
|
||||
import argparse
|
||||
arg_parser = argparse.ArgumentParser(prog=NAME.lower())
|
||||
arg_parser.add_argument('-d', '--debug', action='count', default=0,
|
||||
help="print logging messages, for debugging purposes (may be repeated for extra verbosity)")
|
||||
arg_parser.add_argument('-V', '--version', action='version', version='%(prog)s ' + __version__)
|
||||
args = arg_parser.parse_args()
|
||||
|
||||
import logging
|
||||
if args.debug > 0:
|
||||
log_level = logging.WARNING - 10 * args.debug
|
||||
log_format='%(asctime)s,%(msecs)03d %(levelname)8s [%(threadName)s] %(name)s: %(message)s'
|
||||
logging.basicConfig(level=max(log_level, logging.DEBUG), format=log_format, datefmt='%H:%M:%S')
|
||||
else:
|
||||
logging.root.addHandler(logging.NullHandler())
|
||||
logging.root.setLevel(logging.ERROR)
|
||||
|
||||
if logging.root.isEnabledFor(logging.INFO):
|
||||
logging.info("language %s (%s), translations path %s", _i18n.language, _i18n.encoding, _i18n.path)
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def main():
|
||||
_require('pyudev', 'python-pyudev')
|
||||
_require('gi.repository', 'python-gi')
|
||||
_require('gi.repository.Gtk', 'gir1.2-gtk-3.0')
|
||||
_parse_arguments()
|
||||
|
||||
# handle ^C in console
|
||||
import signal
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||
|
||||
try:
|
||||
import solaar.ui as ui
|
||||
ui.init()
|
||||
|
||||
import solaar.listener as listener
|
||||
listener.setup_scanner(ui.status_changed, ui.error_dialog)
|
||||
listener.start_all()
|
||||
|
||||
# main UI event loop
|
||||
ui.run_loop()
|
||||
|
||||
listener.stop_all()
|
||||
except Exception as e:
|
||||
import sys
|
||||
sys.exit('%s: error: %s' % (NAME.lower(), e))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
64
lib/solaar/i18n.py
Normal file
64
lib/solaar/i18n.py
Normal file
@@ -0,0 +1,64 @@
|
||||
# -*- python-mode -*-
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
## Copyright (C) 2012-2013 Daniel Pavel
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published by
|
||||
## the Free Software Foundation; either version 2 of the License, or
|
||||
## (at your option) any later version.
|
||||
##
|
||||
## This program is distributed in the hope that it will be useful,
|
||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
## GNU General Public License for more details.
|
||||
##
|
||||
## You should have received a copy of the GNU General Public License along
|
||||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
from solaar import NAME as _NAME
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def _find_locale_path(lc_domain):
|
||||
import os.path as _path
|
||||
|
||||
import sys as _sys
|
||||
prefix_share = _path.normpath(_path.join(_path.realpath(_sys.path[0]), '..'))
|
||||
src_share = _path.normpath(_path.join(_path.realpath(_sys.path[0]), '..', 'share'))
|
||||
del _sys
|
||||
|
||||
from glob import glob as _glob
|
||||
|
||||
for location in prefix_share, src_share:
|
||||
mo_files = _glob(_path.join(location, 'locale', '*', 'LC_MESSAGES', lc_domain + '.mo'))
|
||||
if mo_files:
|
||||
return _path.join(location, 'locale')
|
||||
|
||||
# del _path
|
||||
|
||||
|
||||
import locale
|
||||
locale.setlocale(locale.LC_ALL, '')
|
||||
language, encoding = locale.getlocale()
|
||||
del locale
|
||||
|
||||
_LOCALE_DOMAIN = _NAME.lower()
|
||||
path = _find_locale_path(_LOCALE_DOMAIN)
|
||||
|
||||
import gettext as _gettext
|
||||
|
||||
_gettext.bindtextdomain(_LOCALE_DOMAIN, path)
|
||||
_gettext.textdomain(_LOCALE_DOMAIN)
|
||||
_gettext.install(_LOCALE_DOMAIN)
|
||||
|
||||
try:
|
||||
unicode
|
||||
_ = lambda x: _gettext.gettext(x).decode('UTF-8')
|
||||
except:
|
||||
_ = _gettext.gettext
|
||||
311
lib/solaar/listener.py
Normal file
311
lib/solaar/listener.py
Normal file
@@ -0,0 +1,311 @@
|
||||
# -*- python-mode -*-
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
## Copyright (C) 2012-2013 Daniel Pavel
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published by
|
||||
## the Free Software Foundation; either version 2 of the License, or
|
||||
## (at your option) any later version.
|
||||
##
|
||||
## This program is distributed in the hope that it will be useful,
|
||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
## GNU General Public License for more details.
|
||||
##
|
||||
## You should have received a copy of the GNU General Public License along
|
||||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
from logging import getLogger, INFO as _INFO
|
||||
_log = getLogger(__name__)
|
||||
del getLogger
|
||||
|
||||
|
||||
from solaar.i18n import _
|
||||
from . import configuration
|
||||
from logitech_receiver import (
|
||||
Receiver,
|
||||
listener as _listener,
|
||||
status as _status,
|
||||
notifications as _notifications
|
||||
)
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from collections import namedtuple
|
||||
_GHOST_DEVICE = namedtuple('_GHOST_DEVICE', ('receiver', 'number', 'name', 'kind', 'status', 'online'))
|
||||
_GHOST_DEVICE.__bool__ = lambda self: False
|
||||
_GHOST_DEVICE.__nonzero__ = _GHOST_DEVICE.__bool__
|
||||
del namedtuple
|
||||
|
||||
def _ghost(device):
|
||||
return _GHOST_DEVICE(
|
||||
receiver=device.receiver,
|
||||
number=device.number,
|
||||
name=device.name,
|
||||
kind=device.kind,
|
||||
status=None,
|
||||
online=False)
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
# how often to poll devices that haven't updated their statuses on their own
|
||||
# (through notifications)
|
||||
# _POLL_TICK = 5 * 60 # seconds
|
||||
|
||||
|
||||
class ReceiverListener(_listener.EventsListener):
|
||||
"""Keeps the status of a Receiver.
|
||||
"""
|
||||
def __init__(self, receiver, status_changed_callback):
|
||||
super(ReceiverListener, self).__init__(receiver, self._notifications_handler)
|
||||
# no reason to enable polling yet
|
||||
# self.tick_period = _POLL_TICK
|
||||
# self._last_tick = 0
|
||||
|
||||
assert status_changed_callback
|
||||
self.status_changed_callback = status_changed_callback
|
||||
_status.attach_to(receiver, self._status_changed)
|
||||
|
||||
def has_started(self):
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("%s: notifications listener has started (%s)", self.receiver, self.receiver.handle)
|
||||
notification_flags = self.receiver.enable_notifications()
|
||||
self.receiver.status[_status.KEYS.NOTIFICATION_FLAGS] = notification_flags
|
||||
self.receiver.notify_devices()
|
||||
self._status_changed(self.receiver) #, _status.ALERT.NOTIFICATION)
|
||||
|
||||
def has_stopped(self):
|
||||
r, self.receiver = self.receiver, None
|
||||
assert r is not None
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("%s: notifications listener has stopped", r)
|
||||
|
||||
# because udev is not notifying us about device removal,
|
||||
# make sure to clean up in _all_listeners
|
||||
_all_listeners.pop(r.path, None)
|
||||
|
||||
r.status = _("The receiver was unplugged.")
|
||||
if r:
|
||||
try:
|
||||
r.close()
|
||||
except:
|
||||
_log.exception("closing receiver %s" % r.path)
|
||||
self.status_changed_callback(r) #, _status.ALERT.NOTIFICATION)
|
||||
|
||||
# def tick(self, timestamp):
|
||||
# if not self.tick_period:
|
||||
# raise Exception("tick() should not be called without a tick_period: %s", self)
|
||||
#
|
||||
# # not necessary anymore, we're now using udev monitor to watch for receiver status
|
||||
# # if self._last_tick > 0 and timestamp - self._last_tick > _POLL_TICK * 2:
|
||||
# # # if we missed a couple of polls, most likely the computer went into
|
||||
# # # sleep, and we have to reinitialize the receiver again
|
||||
# # _log.warn("%s: possible sleep detected, closing this listener", self.receiver)
|
||||
# # self.stop()
|
||||
# # return
|
||||
#
|
||||
# self._last_tick = timestamp
|
||||
#
|
||||
# try:
|
||||
# # read these in case they haven't been read already
|
||||
# # self.receiver.serial, self.receiver.firmware
|
||||
# if self.receiver.status.lock_open:
|
||||
# # don't mess with stuff while pairing
|
||||
# return
|
||||
#
|
||||
# self.receiver.status.poll(timestamp)
|
||||
#
|
||||
# # Iterating directly through the reciver would unnecessarily probe
|
||||
# # all possible devices, even unpaired ones.
|
||||
# # Checking for each device number in turn makes sure only already
|
||||
# # known devices are polled.
|
||||
# # This is okay because we should have already known about them all
|
||||
# # long before the first poll() happents, through notifications.
|
||||
# for number in range(1, 6):
|
||||
# if number in self.receiver:
|
||||
# dev = self.receiver[number]
|
||||
# if dev and dev.status is not None:
|
||||
# dev.status.poll(timestamp)
|
||||
# except Exception as e:
|
||||
# _log.exception("polling", e)
|
||||
|
||||
def _status_changed(self, device, alert=_status.ALERT.NONE, reason=None):
|
||||
assert device is not None
|
||||
if _log.isEnabledFor(_INFO):
|
||||
if device.kind is None:
|
||||
_log.info("status_changed %s: %s, %s (%X) %s", device,
|
||||
'present' if bool(device) else 'removed',
|
||||
device.status, alert, reason or '')
|
||||
else:
|
||||
_log.info("status_changed %s: %s %s, %s (%X) %s", device,
|
||||
'paired' if bool(device) else 'unpaired',
|
||||
'online' if device.online else 'offline',
|
||||
device.status, alert, reason or '')
|
||||
|
||||
if device.kind is None:
|
||||
assert device == self.receiver
|
||||
# the status of the receiver changed
|
||||
self.status_changed_callback(device, alert, reason)
|
||||
return
|
||||
|
||||
assert device.receiver == self.receiver
|
||||
if not device:
|
||||
# Device was unpaired, and isn't valid anymore.
|
||||
# We replace it with a ghost so that the UI has something to work
|
||||
# with while cleaning up.
|
||||
_log.warn("device %s was unpaired, ghosting", device)
|
||||
device = _ghost(device)
|
||||
|
||||
self.status_changed_callback(device, alert, reason)
|
||||
|
||||
if not device:
|
||||
# the device was just unpaired, need to update the
|
||||
# status of the receiver as well
|
||||
self.status_changed_callback(self.receiver)
|
||||
|
||||
def _notifications_handler(self, n):
|
||||
assert self.receiver
|
||||
# if _log.isEnabledFor(_DEBUG):
|
||||
# _log.debug("%s: handling %s", self.receiver, n)
|
||||
if n.devnumber == 0xFF:
|
||||
# a receiver notification
|
||||
_notifications.process(self.receiver, n)
|
||||
return
|
||||
|
||||
# a device notification
|
||||
assert n.devnumber > 0 and n.devnumber <= self.receiver.max_devices
|
||||
already_known = n.devnumber in self.receiver
|
||||
if not already_known and n.sub_id == 0x41:
|
||||
dev = self.receiver.register_new_device(n.devnumber, n)
|
||||
else:
|
||||
dev = self.receiver[n.devnumber]
|
||||
|
||||
if not dev:
|
||||
_log.warn("%s: received %s for invalid device %d: %r", self.receiver, n, n.devnumber, dev)
|
||||
return
|
||||
|
||||
if not already_known:
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("%s triggered new device %s (%s)", n, dev, dev.kind)
|
||||
# If there are saved configs, bring the device's settings up-to-date.
|
||||
# They will be applied when the device is marked as online.
|
||||
configuration.attach_to(dev)
|
||||
_status.attach_to(dev, self._status_changed)
|
||||
# the receiver changed status as well
|
||||
self._status_changed(self.receiver)
|
||||
|
||||
assert dev
|
||||
assert dev.status is not None
|
||||
_notifications.process(dev, n)
|
||||
if self.receiver.status.lock_open and not already_known:
|
||||
# this should be the first notification after a device was paired
|
||||
assert n.sub_id == 0x41 and n.address == 0x04
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("%s: pairing detected new device", self.receiver)
|
||||
self.receiver.status.new_device = dev
|
||||
elif dev:
|
||||
if dev.online is None:
|
||||
dev.ping()
|
||||
|
||||
def __str__(self):
|
||||
return '<ReceiverListener(%s,%s)>' % (self.receiver.path, self.receiver.handle)
|
||||
__unicode__ = __str__
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
# all known receiver listeners
|
||||
# listeners that stop on their own may remain here
|
||||
_all_listeners = {}
|
||||
|
||||
|
||||
def _start(device_info):
|
||||
assert _status_callback
|
||||
receiver = Receiver.open(device_info)
|
||||
if receiver:
|
||||
rl = ReceiverListener(receiver, _status_callback)
|
||||
rl.start()
|
||||
_all_listeners[device_info.path] = rl
|
||||
return rl
|
||||
|
||||
_log.warn("failed to open %s", device_info)
|
||||
|
||||
|
||||
def start_all():
|
||||
# just in case this it called twice in a row...
|
||||
stop_all()
|
||||
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("starting receiver listening threads")
|
||||
for device_info in _base.receivers():
|
||||
_process_receiver_event('add', device_info)
|
||||
|
||||
|
||||
def stop_all():
|
||||
listeners = list(_all_listeners.values())
|
||||
_all_listeners.clear()
|
||||
|
||||
if listeners:
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("stopping receiver listening threads %s", listeners)
|
||||
|
||||
for l in listeners:
|
||||
l.stop()
|
||||
|
||||
configuration.save()
|
||||
|
||||
if listeners:
|
||||
for l in listeners:
|
||||
l.join()
|
||||
|
||||
|
||||
# stop/start all receiver threads on suspend/resume events, if possible
|
||||
from . import upower
|
||||
upower.watch(start_all, stop_all)
|
||||
|
||||
|
||||
from logitech_receiver import base as _base
|
||||
_status_callback = None
|
||||
_error_callback = None
|
||||
|
||||
def setup_scanner(status_changed_callback, error_callback):
|
||||
global _status_callback, _error_callback
|
||||
assert _status_callback is None, 'scanner was already set-up'
|
||||
|
||||
_status_callback = status_changed_callback
|
||||
_error_callback = error_callback
|
||||
|
||||
_base.notify_on_receivers_glib(_process_receiver_event)
|
||||
|
||||
|
||||
# receiver add/remove events will start/stop listener threads
|
||||
def _process_receiver_event(action, device_info):
|
||||
assert action is not None
|
||||
assert device_info is not None
|
||||
assert _error_callback
|
||||
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("receiver event %s %s", action, device_info)
|
||||
|
||||
# whatever the action, stop any previous receivers at this path
|
||||
l = _all_listeners.pop(device_info.path, None)
|
||||
if l is not None:
|
||||
assert isinstance(l, ReceiverListener)
|
||||
l.stop()
|
||||
|
||||
if action == 'add':
|
||||
# a new receiver device was detected
|
||||
try:
|
||||
_start(device_info)
|
||||
except OSError:
|
||||
# permission error, ignore this path for now
|
||||
_error_callback('permissions', device_info.path)
|
||||
183
lib/solaar/ui/__init__.py
Normal file
183
lib/solaar/ui/__init__.py
Normal file
@@ -0,0 +1,183 @@
|
||||
# -*- python-mode -*-
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
## Copyright (C) 2012-2013 Daniel Pavel
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published by
|
||||
## the Free Software Foundation; either version 2 of the License, or
|
||||
## (at your option) any later version.
|
||||
##
|
||||
## This program is distributed in the hope that it will be useful,
|
||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
## GNU General Public License for more details.
|
||||
##
|
||||
## You should have received a copy of the GNU General Public License along
|
||||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
|
||||
from logging import getLogger, DEBUG as _DEBUG, INFO as _INFO
|
||||
_log = getLogger(__name__)
|
||||
del getLogger
|
||||
|
||||
from gi.repository import GLib, Gtk
|
||||
|
||||
|
||||
from solaar.i18n import _
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
assert Gtk.get_major_version() > 2, 'Solaar requires Gtk 3 python bindings'
|
||||
|
||||
GLib.threads_init()
|
||||
|
||||
def _init_application():
|
||||
APP_ID = 'io.github.pwr.solaar'
|
||||
app = Gtk.Application.new(APP_ID, 0)
|
||||
# not sure this is necessary...
|
||||
# app.set_property('register-session', True)
|
||||
registered = app.register(None)
|
||||
dbus_path = app.get_dbus_object_path() if hasattr(app, 'get_dbus_object_path') else APP_ID
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info("application %s, registered %s", dbus_path, registered)
|
||||
# assert registered, "failed to register unique application %s" % app
|
||||
|
||||
# if there is already a running instance, bail out
|
||||
if app.get_is_remote():
|
||||
# pop up the window in the other instance
|
||||
app.activate()
|
||||
raise Exception("already running")
|
||||
|
||||
return app
|
||||
|
||||
application = _init_application()
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
def _error_dialog(reason, object):
|
||||
_log.error("error: %s %s", reason, object)
|
||||
|
||||
if reason == 'permissions':
|
||||
title = _("Permissions error")
|
||||
text = _("Found a Logitech Receiver (%s), but did not have permission to open it.") % object + \
|
||||
'\n\n' + \
|
||||
_("If you've just installed Solaar, try removing the receiver and plugging it back in.")
|
||||
elif reason == 'unpair':
|
||||
title = _("Unpairing failed")
|
||||
text = _("Failed to unpair %s from %s.") % (object.name, object.receiver.name) + \
|
||||
'\n\n' + \
|
||||
_("The receiver returned an error, with no further details.")
|
||||
else:
|
||||
raise Exception("ui.error_dialog: don't know how to handle (%s, %s)", reason, object)
|
||||
|
||||
assert title
|
||||
assert text
|
||||
|
||||
m = Gtk.MessageDialog(None, Gtk.DialogFlags.MODAL, Gtk.MessageType.ERROR, Gtk.ButtonsType.CLOSE, text)
|
||||
m.set_title(title)
|
||||
m.run()
|
||||
m.destroy()
|
||||
|
||||
|
||||
def error_dialog(reason, object):
|
||||
assert reason is not None
|
||||
GLib.idle_add(_error_dialog, reason, object)
|
||||
|
||||
#
|
||||
# A separate thread is used to read/write from the device
|
||||
# so as not to block the main (GUI) thread.
|
||||
#
|
||||
|
||||
try:
|
||||
from Queue import Queue
|
||||
except ImportError:
|
||||
from queue import Queue
|
||||
_task_queue = Queue(16)
|
||||
del Queue
|
||||
|
||||
|
||||
from threading import Thread, current_thread as _current_thread
|
||||
|
||||
def _process_async_queue():
|
||||
t = _current_thread()
|
||||
t.alive = True
|
||||
while t.alive:
|
||||
function, args, kwargs = _task_queue.get()
|
||||
if function:
|
||||
function(*args, **kwargs)
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("stopped")
|
||||
|
||||
_queue_processor = Thread(name='AsyncUI', target=_process_async_queue)
|
||||
_queue_processor.daemon = True
|
||||
_queue_processor.alive = False
|
||||
_queue_processor.start()
|
||||
|
||||
del Thread
|
||||
|
||||
def async(function, *args, **kwargs):
|
||||
task = (function, args, kwargs)
|
||||
_task_queue.put(task)
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from . import notify, tray, window
|
||||
|
||||
def init():
|
||||
notify.init()
|
||||
tray.init(lambda _ignore: window.destroy())
|
||||
window.init()
|
||||
|
||||
def run_loop():
|
||||
def _activate(app):
|
||||
assert app == application
|
||||
if app.get_windows():
|
||||
window.popup()
|
||||
else:
|
||||
app.add_window(window._window)
|
||||
|
||||
def _shutdown(app):
|
||||
# stop the async UI processor
|
||||
_queue_processor.alive = False
|
||||
async(None)
|
||||
|
||||
tray.destroy()
|
||||
notify.uninit()
|
||||
|
||||
application.connect('activate', _activate)
|
||||
application.connect('shutdown', _shutdown)
|
||||
application.run(None)
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from logitech_receiver.status import ALERT
|
||||
def _status_changed(device, alert, reason):
|
||||
assert device is not None
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug("status changed: %s (%s) %s", device, alert, reason)
|
||||
|
||||
tray.update(device)
|
||||
if alert & ALERT.ATTENTION:
|
||||
tray.attention(reason)
|
||||
|
||||
need_popup = alert & (ALERT.SHOW_WINDOW | ALERT.ATTENTION)
|
||||
window.update(device, need_popup)
|
||||
|
||||
if alert & ALERT.NOTIFICATION:
|
||||
notify.show(device, reason)
|
||||
|
||||
|
||||
def status_changed(device, alert=ALERT.NONE, reason=None):
|
||||
GLib.idle_add(_status_changed, device, alert, reason)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user