mirror of https://github.com/nirenjan/libx52.git
Compare commits
686 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
57676cf540 | |
|
|
b8f059a881 | |
|
|
03d7336234 | |
|
|
a710ab5591 | |
|
|
130a1f67de | |
|
|
4422ee89c0 | |
|
|
9aaec8b2f0 | |
|
|
3f4990de4d | |
|
|
fb913a06a2 | |
|
|
c373ca9647 | |
|
|
9cddfe9cef | |
|
|
5fecdd3929 | |
|
|
772017661d | |
|
|
fdf884cb1c | |
|
|
e0ab5bbab7 | |
|
|
3c02fe5ec2 | |
|
|
dbf891f951 | |
|
|
79676250fc | |
|
|
a2ce2e2218 | |
|
|
c487508b7b | |
|
|
84e151389a | |
|
|
75f0125f54 | |
|
|
f52e328a8b | |
|
|
806a88c93d | |
|
|
c0c8787331 | |
|
|
de465fbf6a | |
|
|
583d4fd646 | |
|
|
836206d93f | |
|
|
9c1eaaa4b2 | |
|
|
9c47e78fc5 | |
|
|
7bdaea442e | |
|
|
8b139a05c4 | |
|
|
c49689c1ee | |
|
|
0fdcb725af | |
|
|
03d58c62e8 | |
|
|
991a307191 | |
|
|
6ec133488b | |
|
|
667e8e2a7b | |
|
|
357ea96676 | |
|
|
6a36fc7764 | |
|
|
283b476c5e | |
|
|
75e6f253c9 | |
|
|
230951a232 | |
|
|
c99f775b70 | |
|
|
8914184613 | |
|
|
fdafda1d34 | |
|
|
9501813c36 | |
|
|
aa555f5e66 | |
|
|
842e7e53ed | |
|
|
dfbe3e6d21 | |
|
|
732bc21b65 | |
|
|
3c1abd57d5 | |
|
|
b0b457d14e | |
|
|
ae077dbed8 | |
|
|
d29be6213f | |
|
|
273ed22f8e | |
|
|
cdb00739ca | |
|
|
08a6b0a736 | |
|
|
45d561e0d8 | |
|
|
b626a9367f | |
|
|
a1098bc134 | |
|
|
569902be76 | |
|
|
0cb137bbe0 | |
|
|
899ea57bf7 | |
|
|
e1e020a4f5 | |
|
|
7cbf091dc7 | |
|
|
9d180531b9 | |
|
|
74229b391d | |
|
|
5f8177f16b | |
|
|
c5ec15231f | |
|
|
33bbafe970 | |
|
|
e9a806a6a2 | |
|
|
ad30bfff7b | |
|
|
cccb561020 | |
|
|
6743c60dfd | |
|
|
b4ec8d4629 | |
|
|
3fb0d72124 | |
|
|
74fe559f4a | |
|
|
b6e61fc54e | |
|
|
e479e338a2 | |
|
|
69ae9626c7 | |
|
|
e9f4e1b3a8 | |
|
|
f43ba6b902 | |
|
|
378cbbd931 | |
|
|
47da6e22d1 | |
|
|
e98b8b4bc3 | |
|
|
7b7065f8f0 | |
|
|
2fa9f52ddb | |
|
|
a17312dcbc | |
|
|
2be7792024 | |
|
|
f51b777ca0 | |
|
|
b3dff7182b | |
|
|
0356a2d610 | |
|
|
c1e3c85738 | |
|
|
accd2a1f4e | |
|
|
e8208e97cb | |
|
|
421e2964b3 | |
|
|
2378ba7dc4 | |
|
|
762a3468b2 | |
|
|
ef4cbee127 | |
|
|
c63b924705 | |
|
|
1b00bf4a69 | |
|
|
a40546bda3 | |
|
|
108293abdf | |
|
|
9361c7af5c | |
|
|
004eca2418 | |
|
|
1902ca0d27 | |
|
|
6330d28c4d | |
|
|
5c37c4a9db | |
|
|
863e43e4ad | |
|
|
49c57f4a6a | |
|
|
b0b9123a2e | |
|
|
50a911160f | |
|
|
7a56af032b | |
|
|
c46cec3138 | |
|
|
21050e40a8 | |
|
|
9e2e8cb8ff | |
|
|
5f4dfe4c01 | |
|
|
0870518598 | |
|
|
d7b4a694fa | |
|
|
326ac992ac | |
|
|
d3973a0abf | |
|
|
ebca9566d7 | |
|
|
03c0376e7c | |
|
|
d4412aba3e | |
|
|
e08c46f6c4 | |
|
|
708ace20be | |
|
|
4b411d6767 | |
|
|
8b2c0e4a2f | |
|
|
b810c457f9 | |
|
|
ccabb5c953 | |
|
|
b2f292bf58 | |
|
|
7f59984357 | |
|
|
c2c2e91089 | |
|
|
d60ab7e1e4 | |
|
|
2b7c643537 | |
|
|
d96b86a817 | |
|
|
fea095dc50 | |
|
|
1dcadb9428 | |
|
|
ecfb865c58 | |
|
|
b1c7a16eac | |
|
|
b38a75462d | |
|
|
619d516ccc | |
|
|
88159d4fc5 | |
|
|
c3a4fea139 | |
|
|
1d1e5781c2 | |
|
|
1a02ad22d9 | |
|
|
b822d3aed8 | |
|
|
dfa78ff2a9 | |
|
|
d3d32cf278 | |
|
|
b0f9006594 | |
|
|
35f5da6b50 | |
|
|
41812e3c1e | |
|
|
95a10b5ac9 | |
|
|
d0f0232dae | |
|
|
ba9348b888 | |
|
|
0ac3a3d0c4 | |
|
|
552e638b27 | |
|
|
44fba0c6cd | |
|
|
a119fe2c60 | |
|
|
8c8f261c80 | |
|
|
6a8dff0a17 | |
|
|
8f9ab9cefd | |
|
|
3168061c99 | |
|
|
24ce9bc97b | |
|
|
251ccfde0d | |
|
|
611bc9d965 | |
|
|
69426c62d8 | |
|
|
054df1ab7a | |
|
|
527d4d5a1e | |
|
|
c87c7caa68 | |
|
|
0f7cd3fdb3 | |
|
|
33e940606c | |
|
|
f0ad185421 | |
|
|
98822190ed | |
|
|
cf6811d923 | |
|
|
5ab4784b21 | |
|
|
385f1ca574 | |
|
|
cf89e3d610 | |
|
|
e9d4e81a4d | |
|
|
3896fa0d3e | |
|
|
f6c6db6c61 | |
|
|
2119e00647 | |
|
|
f4a8e7c4d5 | |
|
|
417641dee0 | |
|
|
86f599fc6e | |
|
|
09740e0fe9 | |
|
|
7448334824 | |
|
|
f2b0110380 | |
|
|
87bf5881dd | |
|
|
e82f9032eb | |
|
|
2fe7b8af43 | |
|
|
20abe8974c | |
|
|
116b9e2c0c | |
|
|
7b8c71dd35 | |
|
|
eb98804607 | |
|
|
d8a5a2c3b8 | |
|
|
5a78492140 | |
|
|
abc74d6e37 | |
|
|
3225d37e6e | |
|
|
931f945133 | |
|
|
18c0c72c74 | |
|
|
293ba0a99d | |
|
|
52429e8dc3 | |
|
|
6c3efa44f5 | |
|
|
6d78ab1940 | |
|
|
3c006d0929 | |
|
|
6c17e73284 | |
|
|
581d2c0bbd | |
|
|
8d38c4d16b | |
|
|
41be44cc94 | |
|
|
2cc3cc5bfe | |
|
|
8fd16d544a | |
|
|
1c0d98c474 | |
|
|
ad178d3f6d | |
|
|
d9eed14e0d | |
|
|
fec67b5994 | |
|
|
247e98c5dc | |
|
|
9a39e971f1 | |
|
|
e7d91fd3a4 | |
|
|
f51985dd20 | |
|
|
b45dc59ae0 | |
|
|
aef1b6fade | |
|
|
794b09e766 | |
|
|
ba936df6f8 | |
|
|
5f21ccd2e9 | |
|
|
3374bb98dc | |
|
|
00ed62b72e | |
|
|
a5a25c307c | |
|
|
fe54730447 | |
|
|
4d2736e03c | |
|
|
4f427b2ebe | |
|
|
e8abbd0374 | |
|
|
d8fc859e44 | |
|
|
4365e86f2a | |
|
|
16e53b897f | |
|
|
8deb6a1513 | |
|
|
c56f715155 | |
|
|
025a06351a | |
|
|
53957d0813 | |
|
|
2a8ca8424e | |
|
|
276b512478 | |
|
|
7f29f5f5fe | |
|
|
42850bc4cd | |
|
|
b9e5f34aa4 | |
|
|
016851478a | |
|
|
8874a282aa | |
|
|
3a81acf828 | |
|
|
fa1d54f9da | |
|
|
7d757dd40f | |
|
|
627c1fb004 | |
|
|
f2884c57b7 | |
|
|
5fcac86999 | |
|
|
a3cc0adb84 | |
|
|
f34f84a3ee | |
|
|
0f83cd5a95 | |
|
|
2290900da6 | |
|
|
91f378c4fc | |
|
|
e0f6813028 | |
|
|
0899df60c2 | |
|
|
76b1b99717 | |
|
|
38dfc7d7b0 | |
|
|
1174f7f1c4 | |
|
|
a39945f461 | |
|
|
4c9ef85223 | |
|
|
52d6920352 | |
|
|
5be91b6e50 | |
|
|
ff10525028 | |
|
|
874c46705a | |
|
|
3d15da385f | |
|
|
2b664513a7 | |
|
|
a09a8bee84 | |
|
|
e7af5df69b | |
|
|
86960e7e20 | |
|
|
0cf6f247be | |
|
|
7f5b5a2eaf | |
|
|
65f4ec9659 | |
|
|
0fae24b5d0 | |
|
|
f5331cdef3 | |
|
|
e968656672 | |
|
|
446fec3b9f | |
|
|
f6cfc59cb6 | |
|
|
dc72e43f1e | |
|
|
d9c1c80163 | |
|
|
699f663df3 | |
|
|
3efdce5abe | |
|
|
38917ed6e5 | |
|
|
78e4f3334f | |
|
|
e358aa9688 | |
|
|
2cdf22b8c1 | |
|
|
dbd683a98b | |
|
|
377aea90b5 | |
|
|
9047485204 | |
|
|
23ff7c7202 | |
|
|
51913094cb | |
|
|
abdf47d721 | |
|
|
ac68ee07e5 | |
|
|
23fa0daf4f | |
|
|
a94b079cf5 | |
|
|
711e4385c1 | |
|
|
3b2378a54b | |
|
|
7f30863e5d | |
|
|
34adeaec45 | |
|
|
e3bccd3ac3 | |
|
|
0eeab91a8d | |
|
|
8db1be2ba8 | |
|
|
7a4d63adc1 | |
|
|
06b8d15dda | |
|
|
50906b0a92 | |
|
|
009fba0151 | |
|
|
386174d2a4 | |
|
|
d003e7f7c4 | |
|
|
018852a012 | |
|
|
77606ae906 | |
|
|
602071612d | |
|
|
f422202ec8 | |
|
|
bd2dbbb9cc | |
|
|
52232b1a14 | |
|
|
e0d15961e0 | |
|
|
e4d1b6aff2 | |
|
|
9941234bbe | |
|
|
9970a8edc4 | |
|
|
2cecd1890a | |
|
|
bd682cd5c7 | |
|
|
88955418b8 | |
|
|
0fa638fa16 | |
|
|
499cad666f | |
|
|
ae13480717 | |
|
|
6175dcabe6 | |
|
|
c45a84bd38 | |
|
|
3bca8da541 | |
|
|
50f77119ff | |
|
|
ac8bb6cdd9 | |
|
|
c9cb89f833 | |
|
|
b81d89aad3 | |
|
|
98cc439f05 | |
|
|
e54f6037d4 | |
|
|
1b598c2d78 | |
|
|
13f54588a6 | |
|
|
9fa1a428a4 | |
|
|
3a68148472 | |
|
|
82c778c7de | |
|
|
27eb123062 | |
|
|
0d407d77fe | |
|
|
ab946b4a1a | |
|
|
5a283672c4 | |
|
|
2ce9ff2280 | |
|
|
81002444d7 | |
|
|
8545e28d09 | |
|
|
e32f836485 | |
|
|
fc9bebbe5a | |
|
|
738879f79f | |
|
|
fa298455aa | |
|
|
19859b79c5 | |
|
|
cbe7f00a5a | |
|
|
fc8e7b6b95 | |
|
|
f82c31a6eb | |
|
|
4b1d524d39 | |
|
|
50dc946c31 | |
|
|
fd79166a89 | |
|
|
dcd878b7cc | |
|
|
a28c622941 | |
|
|
36dfdd0ad3 | |
|
|
16b2cf7348 | |
|
|
3e2b960c0e | |
|
|
b294a1a950 | |
|
|
2c522b9a66 | |
|
|
7116af8f66 | |
|
|
d8c6c8d574 | |
|
|
d41762df11 | |
|
|
bea668b87e | |
|
|
121c86a190 | |
|
|
e3758a2f29 | |
|
|
b9ef8a82d6 | |
|
|
abb366d89c | |
|
|
59c4643474 | |
|
|
1df4f29d4e | |
|
|
7f554d7ac6 | |
|
|
482c5980ab | |
|
|
b0150c46b8 | |
|
|
4388eceec0 | |
|
|
d3c55da89d | |
|
|
3409a7bad6 | |
|
|
1fa4cb4eb4 | |
|
|
dcb5b60cdf | |
|
|
d77342ced9 | |
|
|
1119fe3373 | |
|
|
4f39078998 | |
|
|
53f9c33ffa | |
|
|
55c1fadba6 | |
|
|
9fb2d246c6 | |
|
|
9486d1dbe4 | |
|
|
ed654f501a | |
|
|
104fcb46f9 | |
|
|
798714dd1c | |
|
|
453f9517d9 | |
|
|
869d564aa3 | |
|
|
1d5e1073ce | |
|
|
22d4218189 | |
|
|
5108e34ce8 | |
|
|
afb442d9c4 | |
|
|
f6136fcef0 | |
|
|
c8ad37b3f7 | |
|
|
81cb7367f8 | |
|
|
b766fb75fa | |
|
|
597c73ab35 | |
|
|
46bd78bdd9 | |
|
|
4982071764 | |
|
|
62894dea43 | |
|
|
a0b7769dab | |
|
|
2c40785c2b | |
|
|
4bd3ae69fe | |
|
|
16cb1e4698 | |
|
|
9ab3cce73e | |
|
|
4f18aa3dc8 | |
|
|
cf6c458fae | |
|
|
63a2f465d2 | |
|
|
9d3acfd35a | |
|
|
aebd5e14f9 | |
|
|
329274e6c9 | |
|
|
bcc90ac24e | |
|
|
1cbad472df | |
|
|
f963991161 | |
|
|
945ddc63a3 | |
|
|
729bbcaf90 | |
|
|
4311c020a0 | |
|
|
c40847b833 | |
|
|
3981b873e0 | |
|
|
87ad48a37f | |
|
|
7b423f4ea0 | |
|
|
efd984ef63 | |
|
|
108b7e2522 | |
|
|
681a8e8aa1 | |
|
|
57cda79320 | |
|
|
8388f3308e | |
|
|
c4696f6055 | |
|
|
34b023b1fa | |
|
|
e9167b4c20 | |
|
|
3afe999fe8 | |
|
|
9f37cde784 | |
|
|
bf9b1bdfbd | |
|
|
f0ed2f39e3 | |
|
|
c4acd0ce49 | |
|
|
3eaee7b8f4 | |
|
|
e1915bc734 | |
|
|
21a5da3c70 | |
|
|
f754533a67 | |
|
|
16a7801e59 | |
|
|
02165a8712 | |
|
|
8b49b91267 | |
|
|
dbacc27164 | |
|
|
1e2dd5699f | |
|
|
3b8b98e74c | |
|
|
65c889827a | |
|
|
09eb7d31e8 | |
|
|
37162510ac | |
|
|
fffb0bb69e | |
|
|
49d162fa07 | |
|
|
1efcaf8970 | |
|
|
071162a907 | |
|
|
c96ba7fec4 | |
|
|
92b0eb584f | |
|
|
2cb3474861 | |
|
|
4d93df1d58 | |
|
|
c9ffb415c8 | |
|
|
d53e56c491 | |
|
|
23a980e250 | |
|
|
5715b19326 | |
|
|
e70a1b74e9 | |
|
|
2d46b395a1 | |
|
|
0cf977b751 | |
|
|
cdd5e773e2 | |
|
|
88d57958f9 | |
|
|
85d2fc3522 | |
|
|
af49ce6500 | |
|
|
f5145de36b | |
|
|
4f22983739 | |
|
|
5b7afa6ae1 | |
|
|
780d9b4da4 | |
|
|
95bc71859b | |
|
|
01e815fc3b | |
|
|
99bfb7d36a | |
|
|
1188bea444 | |
|
|
758d1d05d2 | |
|
|
bac20b410d | |
|
|
89c233e244 | |
|
|
88f02bc5da | |
|
|
543aec85b1 | |
|
|
011bb737af | |
|
|
8e77d6f09b | |
|
|
b6ebdef7ef | |
|
|
cdc6a594e4 | |
|
|
dbfe26f709 | |
|
|
a7d5b7e34d | |
|
|
cb96d297ab | |
|
|
c2c852cee1 | |
|
|
79bd8466c1 | |
|
|
7ae5cad0cc | |
|
|
a43cbc83a5 | |
|
|
57f7758dd1 | |
|
|
e7d14d7b53 | |
|
|
a16b1822aa | |
|
|
127ab10995 | |
|
|
2db24e8759 | |
|
|
e5ce827d7e | |
|
|
b0a07fe364 | |
|
|
a711f0a882 | |
|
|
7889124217 | |
|
|
f9be0b3172 | |
|
|
374fd94fcd | |
|
|
c86e3f027a | |
|
|
973348e537 | |
|
|
491e5dffeb | |
|
|
aff5576106 | |
|
|
d9ae8d4b79 | |
|
|
93f1091b95 | |
|
|
6dc5d51461 | |
|
|
dfdf6468bc | |
|
|
fd6afde59c | |
|
|
132b72f562 | |
|
|
d89cce807b | |
|
|
482943e7a3 | |
|
|
0b6bc8f074 | |
|
|
cc8d6e9344 | |
|
|
97743d4ebd | |
|
|
db8629a6a9 | |
|
|
84a7e0fe30 | |
|
|
94a262f13a | |
|
|
2ea6dcd748 | |
|
|
254bf6baaa | |
|
|
1b6736c0f8 | |
|
|
ec9443dcdd | |
|
|
3949550b65 | |
|
|
1c822f9d6b | |
|
|
45f009ac90 | |
|
|
be1f7e0d5a | |
|
|
16b4ad693b | |
|
|
665dba187d | |
|
|
74b828a790 | |
|
|
0204103ccd | |
|
|
34dc1b8a32 | |
|
|
cb050f2c30 | |
|
|
79b1f930b8 | |
|
|
d45b1f7bfd | |
|
|
116f7b3a57 | |
|
|
06fa56bb9f | |
|
|
848d70fcf2 | |
|
|
e49261c8d6 | |
|
|
9dc92eb52e | |
|
|
42f416af1d | |
|
|
aa259bf343 | |
|
|
946916f456 | |
|
|
152a3e7932 | |
|
|
f9639a9a00 | |
|
|
243b0330af | |
|
|
a5b69124a4 | |
|
|
dc80a0f2f1 | |
|
|
a7caba19df | |
|
|
40b2e9bdac | |
|
|
40c14fed24 | |
|
|
1d51429f10 | |
|
|
9e581bf051 | |
|
|
b1139806f5 | |
|
|
7dcd3049ec | |
|
|
e31f1e442b | |
|
|
0913212ecc | |
|
|
02c24cc964 | |
|
|
46cba64e6b | |
|
|
86642e5b16 | |
|
|
3845c81229 | |
|
|
6f3f8d7c46 | |
|
|
d4afbd6de2 | |
|
|
e5ea621899 | |
|
|
f3270def9d | |
|
|
fb222dda89 | |
|
|
82fa0cea28 | |
|
|
21d6b503a7 | |
|
|
d54e02be5a | |
|
|
acdcebc52e | |
|
|
52abd335ab | |
|
|
6b89a9d7f9 | |
|
|
533a472b10 | |
|
|
780447122c | |
|
|
b5f4e72148 | |
|
|
2e96378f80 | |
|
|
b9a7e5de1f | |
|
|
3eb837df1b | |
|
|
994c39ce3c | |
|
|
8f7c262ea6 | |
|
|
44d46b70e0 | |
|
|
e0d7cb8341 | |
|
|
2c2dbb3c42 | |
|
|
d9732d498d | |
|
|
b776101cb6 | |
|
|
0a8db132dc | |
|
|
f6bf25d66f | |
|
|
991218a8b0 | |
|
|
0a45bd5ddf | |
|
|
55963ba824 | |
|
|
a2496d5d28 | |
|
|
c79373676f | |
|
|
e463d9b890 | |
|
|
f0a0a7dcaf | |
|
|
7bc0ba522c | |
|
|
74eeb27ad4 | |
|
|
dc352c58da | |
|
|
45c66a4f1a | |
|
|
ea14d1132d | |
|
|
dc7300db26 | |
|
|
59652c0aff | |
|
|
f0e6836195 | |
|
|
262d125cd4 | |
|
|
9070d88588 | |
|
|
5c69289cff | |
|
|
e053e1ac1c | |
|
|
793cd519a2 | |
|
|
0f7b5e5668 | |
|
|
1efd88d770 | |
|
|
a7806c43b9 | |
|
|
8892b3ef7e | |
|
|
56faafca1e | |
|
|
4532c0d868 | |
|
|
c9dd29199d | |
|
|
6d8d5a4fd4 | |
|
|
1b9c52ea07 | |
|
|
ae97d58bd5 | |
|
|
e5229b6aa2 | |
|
|
aaab4c6b1d | |
|
|
95e933e27c | |
|
|
15b8abf3c6 | |
|
|
30622eefa8 | |
|
|
bbe86d554f | |
|
|
b39c13fe1b | |
|
|
ea2927859b | |
|
|
710ef00109 | |
|
|
224b08a8e2 | |
|
|
bc9001705d | |
|
|
abaa36f35e | |
|
|
6528459645 | |
|
|
db19e4dcae | |
|
|
059ec6af1a | |
|
|
b0309b1b40 | |
|
|
320e3c85dc | |
|
|
ce9f58c15a | |
|
|
3849024816 | |
|
|
f0b2e6fecc | |
|
|
b18391b3a6 | |
|
|
67e6e68fb3 | |
|
|
4dcba4bfe1 | |
|
|
367a367ff9 | |
|
|
e752be9805 | |
|
|
b45c9fd9a2 | |
|
|
7614e2f961 | |
|
|
450cdbabee | |
|
|
090fbe6a3b | |
|
|
c87e785a18 | |
|
|
326075406a | |
|
|
7ddde96cc6 | |
|
|
0ad71bd24f | |
|
|
b7141a3e8b | |
|
|
f2ee9707cb | |
|
|
531c92ecab | |
|
|
b7fe3e484c | |
|
|
f8a7257b54 | |
|
|
cd4fca0d2e | |
|
|
ebf566d9be | |
|
|
21f5440349 | |
|
|
9dcc3507ed | |
|
|
5d0684a154 | |
|
|
f4a81aba83 | |
|
|
8813be2de2 | |
|
|
87fe93f6e9 | |
|
|
dbac37b8ac | |
|
|
b72eba6d4c | |
|
|
c1f3b6abdf | |
|
|
edfff0a5f6 | |
|
|
77cf6c490f | |
|
|
032dda1dd4 | |
|
|
5e4a6dc826 | |
|
|
d688334eb9 | |
|
|
995c5b3c63 | |
|
|
2ccfd79bd6 | |
|
|
548f2e9357 | |
|
|
4a1ca0badb | |
|
|
0a541e18ba | |
|
|
b5d33226c5 |
|
|
@ -0,0 +1,5 @@
|
|||
/version-info ident
|
||||
*/meson.build ident
|
||||
/.github/ export-ignore
|
||||
.gitignore export-ignore
|
||||
.gitattributes export-ignore
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Run '...'
|
||||
2. Scroll down to '....'
|
||||
3. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots/Logs**
|
||||
* If applicable, add screenshots to help explain your problem.
|
||||
* Attach the detailed X52 daemon logs.
|
||||
|
||||
**Environment (please complete the following information):**
|
||||
- Output of `x52bugreport`
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
name: Generic issue
|
||||
about: Generic issue that isn't a bug report or feature request
|
||||
title: ''
|
||||
labels: question
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
version: 2
|
||||
updates:
|
||||
# Maintain dependencies for Github Actions
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 5
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
#!/bin/bash
|
||||
# Run the build and tests
|
||||
set -e
|
||||
|
||||
BUILDDIR="${1:-build}"
|
||||
|
||||
rm -rf "$BUILDDIR"
|
||||
|
||||
# Handle the meson dist failure in CI
|
||||
if [[ "$GITHUB_ACTIONS" == "true" ]]
|
||||
then
|
||||
# If in a container, then use the system directory
|
||||
git config --system --add safe.directory '*' || \
|
||||
git config --global --add safe.directory '*'
|
||||
fi
|
||||
|
||||
meson setup -Dprefix=/usr -Dsysconfdir=/etc -Dlocalstatedir=/var -Dnls=enabled "$BUILDDIR"
|
||||
cd "$BUILDDIR"
|
||||
meson compile
|
||||
meson test
|
||||
|
||||
if [[ $(printf "%s\n" "0.62.0" "$(meson --version)" | sort -V | head -1) == "0.62.0" ]]
|
||||
then
|
||||
meson dist --allow-dirty # Required to fix CI build
|
||||
else
|
||||
meson dist
|
||||
fi
|
||||
|
||||
# Print bugreport output
|
||||
./bugreport/x52bugreport
|
||||
|
||||
# Make sure that there are no changes to the source code
|
||||
# This may happen if the source have changed with differences to the
|
||||
# translation files and templates. Enabling this will allow us to catch
|
||||
# missing/modified translations.
|
||||
git diff --exit-code
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
#!/bin/bash
|
||||
# Generate Doxygen documentation
|
||||
set -e
|
||||
|
||||
meson setup -Dprefix=/usr -Dsysconfdir=/etc -Dlocalstatedir=/var -Dnls=enabled build
|
||||
cd build
|
||||
ninja docs
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Generate the list of distros as a build matrix"""
|
||||
|
||||
import pathlib
|
||||
import json
|
||||
import sys
|
||||
|
||||
class BuildMatrix:
|
||||
"""Generate a build matrix for Github actions"""
|
||||
|
||||
COMPILER = 'gcc'
|
||||
OS = 'ubuntu-latest'
|
||||
EXPERIMENTAL = False
|
||||
|
||||
def __init__(self, image_prefix):
|
||||
self.matrix = []
|
||||
self.image_prefix = image_prefix
|
||||
self.get_distros()
|
||||
self.add_extra_builds()
|
||||
self.generate_output()
|
||||
|
||||
def build_matrix_obj(self, distro, experimental, os=None, compiler=None, image=None):
|
||||
"""Build the matrix object for the given distro"""
|
||||
matrix_obj = {
|
||||
'distro': distro,
|
||||
'experimental': experimental,
|
||||
'os': os or self.OS,
|
||||
'compiler': compiler or self.COMPILER,
|
||||
}
|
||||
|
||||
if image is None:
|
||||
image = f"{self.image_prefix}/ci-build-{distro}:latest"
|
||||
|
||||
matrix_obj['image'] = image
|
||||
|
||||
return matrix_obj
|
||||
|
||||
def get_distros(self):
|
||||
"""Get the list of distros from the Dockerfiles"""
|
||||
for dockerfile in pathlib.Path('.').glob('docker/Dockerfile.*'):
|
||||
distro = dockerfile.suffix[1:]
|
||||
|
||||
with open(dockerfile, encoding='utf-8') as dfd:
|
||||
experimental = 'experimental="true"' in dfd.read()
|
||||
|
||||
self.matrix.append(self.build_matrix_obj(distro, experimental))
|
||||
|
||||
def add_extra_builds(self):
|
||||
"""Add manual canary builds that don't have a corresponding dockerfile"""
|
||||
canary_build = self.build_matrix_obj('ubuntu22', False, compiler='clang')
|
||||
self.matrix.append(canary_build)
|
||||
|
||||
macos_build = self.build_matrix_obj('macos', True, os='macos-latest',
|
||||
compiler='clang', image='')
|
||||
self.matrix.append(macos_build)
|
||||
|
||||
def generate_output(self):
|
||||
"""Generate the output for github actions"""
|
||||
matrix_data = json.dumps(self.matrix)
|
||||
print(f"matrix={matrix_data}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
BuildMatrix(sys.argv[1])
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Generate a changelog for the latest release and dump it to stdout"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
def get_git_root():
|
||||
script_path = Path(__file__).resolve()
|
||||
|
||||
# This is always going to reside at <git-root>/.github/scripts/*.py
|
||||
scripts_dir = Path(script_path.parent)
|
||||
gh_dir = Path(scripts_dir.parent)
|
||||
|
||||
return Path(gh_dir.parent)
|
||||
|
||||
def main():
|
||||
git_root = get_git_root()
|
||||
|
||||
changelog_file = git_root / 'ChangeLog.md'
|
||||
|
||||
latest = False
|
||||
with open(changelog_file) as cfd:
|
||||
for line in cfd:
|
||||
if line.startswith('## '):
|
||||
if 'Unreleased' in line:
|
||||
continue
|
||||
|
||||
if latest:
|
||||
break
|
||||
|
||||
latest = True
|
||||
continue
|
||||
|
||||
if latest:
|
||||
print(line, end='')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
#!/bin/bash
|
||||
# Get the list of changed Dockerfiles
|
||||
# Usage: get-changed-dockerfiles.sh <before-SHA> <head-SHA>
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
mapfile -t ALL_DOCKERFILES < <(git ls-files 'docker/Dockerfile.*')
|
||||
if [[ -n "${BUILD_DOCKER_MANUAL_ENV:-}" ]]
|
||||
then
|
||||
|
||||
if [[ "${BUILD_DOCKER_MANUAL_ENV}" == "ALL" ]]
|
||||
then
|
||||
DOCKERFILES=("${ALL_DOCKERFILES[@]}")
|
||||
else
|
||||
BUILD_LIST=($BUILD_DOCKER_MANUAL_ENV)
|
||||
DOCKERFILES=()
|
||||
for item in "${BUILD_LIST[@]}"
|
||||
do
|
||||
if [[ -f "docker/Dockerfile.${item}" ]]
|
||||
then
|
||||
DOCKERFILES+=("$item")
|
||||
fi
|
||||
done
|
||||
fi
|
||||
else
|
||||
CHANGED_FILES=$(git diff --name-only --diff-filter=ACMR HEAD^..HEAD)
|
||||
|
||||
mapfile -t DOCKERFILES < <(echo "$CHANGED_FILES" | grep 'docker/Dockerfile')
|
||||
mapfile -t SCRIPT_CHANGES < <(echo "$CHANGED_FILES" | grep 'docker/' | grep '\.sh$')
|
||||
|
||||
for file in "${SCRIPT_CHANGES[@]}"
|
||||
do
|
||||
for dockerfile in "${ALL_DOCKERFILES[@]}"
|
||||
do
|
||||
if grep -q "$(basename "$file")" "$dockerfile"
|
||||
then
|
||||
DOCKERFILES+=("$dockerfile")
|
||||
fi
|
||||
done
|
||||
done
|
||||
fi
|
||||
|
||||
echo -n "matrix="
|
||||
echo "${DOCKERFILES[@]}" | \
|
||||
tr ' ' '\n' | sort -u | sed 's,docker/Dockerfile\.,,' | \
|
||||
jq -Rsc 'split("\n") | map(select(length > 0)) | unique'
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
#!/bin/bash -x
|
||||
# Install dependencies to build and test on Ubuntu runners
|
||||
brew install \
|
||||
pkg-config \
|
||||
python3 \
|
||||
gettext \
|
||||
libusb \
|
||||
hidapi \
|
||||
inih \
|
||||
doxygen \
|
||||
cmocka \
|
||||
meson \
|
||||
ninja \
|
||||
inih
|
||||
|
||||
exit 0
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
#!/bin/bash
|
||||
# Install dependencies to build and test on Ubuntu runners
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
pkg-config \
|
||||
python3 \
|
||||
gettext \
|
||||
autopoint \
|
||||
libusb-1.0-0-dev \
|
||||
libhidapi-dev \
|
||||
libevdev-dev \
|
||||
libinih-dev \
|
||||
doxygen \
|
||||
libcmocka-dev \
|
||||
faketime \
|
||||
meson \
|
||||
ninja-build
|
||||
|
||||
exit 0
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
#!/bin/bash
|
||||
# Install dependencies to build kernel modules on Ubuntu runners
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y linux-headers-$(uname -r)
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 180
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 14
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- pinned
|
||||
- security
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: stale
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity in the past 6 months. It will be closed within 2 weeks
|
||||
if no further activity occurs. Thank you for your contributions.
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: false
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
name: Build/Test
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '*'
|
||||
- '!gh-pages'
|
||||
paths-ignore:
|
||||
- 'kernel_module/**'
|
||||
- 'docker/**'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
list-distros:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- id: set-matrix
|
||||
run: .github/scripts/generate_build_matrix.py ghcr.io/${{ github.repository }} >> $GITHUB_OUTPUT
|
||||
|
||||
build:
|
||||
needs: list-distros
|
||||
if: "!(contains(github.event.head_commit.message, '[ci skip]') || contains(github.event.head_commit.message, '[skip ci]'))"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include: ${{ fromJson(needs.list-distros.outputs.matrix) }}
|
||||
|
||||
name: ${{ format('{0}/{1}{2}', matrix.distro, matrix.compiler, matrix.experimental == true && ' (experimental)' || '') }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
continue-on-error: ${{ matrix.experimental }}
|
||||
container:
|
||||
image: ${{ matrix.image }}
|
||||
credentials:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install dependencies (MacOS)
|
||||
run: ./.github/scripts/install-dependencies-macos.sh
|
||||
if: ${{ startsWith(matrix.os, 'macos-') }}
|
||||
|
||||
- name: Build and Test
|
||||
env:
|
||||
CC: ${{ matrix.compiler }}
|
||||
run: ./.github/scripts/build-and-test.sh
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
name: "CodeQL"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [master]
|
||||
schedule:
|
||||
- cron: '30 7 * * 1,3,5'
|
||||
|
||||
jobs:
|
||||
analyse:
|
||||
name: Analyse
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Install dependencies
|
||||
run: ./.github/scripts/install-dependencies-ubuntu.sh
|
||||
|
||||
# If this run was triggered by a pull request event, then checkout
|
||||
# the head of the pull request instead of the merge commit.
|
||||
- run: git checkout HEAD^2
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
# Override language selection by uncommenting this and choosing your languages
|
||||
# with:
|
||||
# languages: go, javascript, csharp, python, cpp, java
|
||||
|
||||
- name: Build
|
||||
run: ./.github/scripts/build-and-test.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v4
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
name: Code Coverage
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '*'
|
||||
- '!gh-pages'
|
||||
pull_request:
|
||||
branches:
|
||||
- 'master'
|
||||
|
||||
jobs:
|
||||
coverage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
./.github/scripts/install-dependencies-ubuntu.sh
|
||||
sudo apt-get install -y gcovr
|
||||
|
||||
- name: Configure and Build
|
||||
run: |
|
||||
meson setup build -Db_coverage=true --buildtype=debug
|
||||
meson compile -C build
|
||||
|
||||
- name: Run Tests
|
||||
run: meson test -C build
|
||||
|
||||
- name: Generate Coverage Report
|
||||
run: |
|
||||
# This generates the XML report for the upload step
|
||||
ninja -C build coverage-xml
|
||||
|
||||
- name: Upload Report to Codecov
|
||||
uses: codecov/codecov-action@v6
|
||||
with:
|
||||
files: buildd/meson-logs/coverage.xml
|
||||
fail_ci_if_error: true
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
name: Build Docker CI Images
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
distros:
|
||||
description: "List distros to build (space separated)"
|
||||
type: string
|
||||
default: "ALL"
|
||||
push:
|
||||
paths:
|
||||
- "docker/Dockerfile.*"
|
||||
- "docker/*.sh"
|
||||
|
||||
jobs:
|
||||
detect-changes:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
BUILD_DOCKER_MANUAL_ENV: ${{ inputs.distros || '' }}
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0 # Needed for detecting changes
|
||||
|
||||
- name: Set build matrix
|
||||
id: set-matrix
|
||||
run: .github/scripts/get-changed-dockerfiles.sh >> $GITHUB_OUTPUT
|
||||
|
||||
build-and-push:
|
||||
needs: detect-changes
|
||||
if: ${{ needs.detect-changes.outputs.matrix != '[]' }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false # Don't cancel other distros if this one fails
|
||||
matrix:
|
||||
distro: ${{ fromJson(needs.detect-changes.outputs.matrix) }}
|
||||
permissions:
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: docker
|
||||
file: ./docker/Dockerfile.${{ matrix.distro }}
|
||||
push: true
|
||||
tags: ghcr.io/${{ github.repository }}/ci-build-${{ matrix.distro }}:latest
|
||||
|
||||
- name: Cleanup old builds
|
||||
uses: actions/delete-package-versions@v5
|
||||
with:
|
||||
package-name: libx52/ci-build-${{ matrix.distro }}
|
||||
package-type: container
|
||||
delete-only-untagged-versions: true
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
name: Doxygen
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write # Required to push to the Pages server
|
||||
id-token: write # Required to verify the deployment is legitimate
|
||||
|
||||
jobs:
|
||||
doxygen:
|
||||
if: "!(contains(github.event.head_commit.message, '[doxy skip]') || contains(github.event.head_commit.message, '[skip doxy]'))"
|
||||
runs-on: 'ubuntu-latest'
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install dependencies
|
||||
run: ./.github/scripts/install-dependencies-ubuntu.sh
|
||||
|
||||
- name: Generate Doxygen documentation
|
||||
run: ./.github/scripts/build-doxygen.sh
|
||||
|
||||
- name: Dump generated files
|
||||
run: find ./build -type f -print
|
||||
|
||||
- name: Upload built pages
|
||||
uses: actions/upload-pages-artifact@v5
|
||||
with:
|
||||
path: './build/docs/html'
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v5
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
name: Kernel Module
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ '*' ]
|
||||
paths:
|
||||
- 'kernel_module/**'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: "!(contains(github.event.head_commit.message, '[ci skip]') || contains(github.event.head_commit.message, '[skip ci]'))"
|
||||
name: ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: ['ubuntu-22.04', 'ubuntu-24.04']
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install kernel dependencies
|
||||
run: ./.github/scripts/install-kernel-dependencies.sh
|
||||
|
||||
- name: Build kernel module
|
||||
run: |
|
||||
cd kernel_module
|
||||
make
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
name: Create Release
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Upload Release Asset
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install dependencies
|
||||
run: ./.github/scripts/install-dependencies-ubuntu.sh
|
||||
|
||||
- name: Build project
|
||||
run: ./.github/scripts/build-and-test.sh
|
||||
|
||||
- name: Prepare release assets
|
||||
run: |
|
||||
# Find the Meson generated tarball
|
||||
# meson dist usuall creates a tar.xz, but be prepared to handle
|
||||
# additional compression formats
|
||||
DIST_FILE=$(find build/meson-dist -name 'libx52-*.tar.*' -a ! -name '*.tar.*sum')
|
||||
|
||||
# Extract the version from the filename
|
||||
VERSION=$(echo "$DIST_FILE" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
|
||||
EXTENSION="${DIST_FILE#*${VERSION}}"
|
||||
|
||||
ASSET_NAME="libx52_${VERSION}.orig${EXTENSION}"
|
||||
ASSET_PATH="build/meson-dist/${ASSET_NAME}"
|
||||
|
||||
# Rename the file
|
||||
mv -v "$DIST_FILE" "$ASSET_PATH"
|
||||
|
||||
cd build/meson-dist
|
||||
rm *.sha256sum
|
||||
sha256sum "$ASSET_NAME" > "${ASSET_NAME}.sha256sum"
|
||||
cd ../..
|
||||
|
||||
- name: Generate changelog
|
||||
run: ./.github/scripts/generate_changelog.py > ${{ github.workspace }}/CHANGELOG.txt
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v3
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
name: Release ${{ github.ref_name }}
|
||||
body_path: ${{ github.workspace }}/CHANGELOG.txt
|
||||
draft: false
|
||||
prerelease: false
|
||||
files: |
|
||||
build/meson-dist/*
|
||||
|
|
@ -1,10 +1,24 @@
|
|||
# Compiled object files
|
||||
*.ko
|
||||
*.o
|
||||
*.mod.*
|
||||
*.mod*
|
||||
|
||||
# Compiled executables
|
||||
# Generated objects (source, executables, tarballs, etc.)
|
||||
a.out
|
||||
x52cli*
|
||||
x52test*
|
||||
x52evtest*
|
||||
libx52/test_*
|
||||
libx52test*
|
||||
libx52util/char_map.c
|
||||
udev/*.rules
|
||||
# Built artifacts / local installs named x52d*; daemon sources use short names.
|
||||
# Keep tracked daemon files that still use the x52d prefix (headers, configs).
|
||||
x52d*
|
||||
!daemon/x52d*.*
|
||||
!libx52/x52dcomm.h
|
||||
test-*
|
||||
libx52-*.tar.gz
|
||||
|
||||
# Module files
|
||||
modules.order
|
||||
|
|
@ -14,3 +28,57 @@ Module.symvers
|
|||
|
||||
# Vim swap files
|
||||
.*.swp
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
# Autotools objects
|
||||
.deps
|
||||
.dirstamp
|
||||
.libs
|
||||
ar-lib
|
||||
autom4te.cache
|
||||
m4
|
||||
compile
|
||||
config.*
|
||||
configure
|
||||
depcomp
|
||||
install-sh
|
||||
libtool
|
||||
ltmain.sh
|
||||
missing
|
||||
Makefile
|
||||
Makefile.in
|
||||
*.la
|
||||
*.lo
|
||||
*.m4
|
||||
stamp-h1
|
||||
tap-driver.sh
|
||||
test-driver
|
||||
tests/test-suite.log
|
||||
tests/**/*.log
|
||||
tests/**/*.trs
|
||||
*.pc
|
||||
|
||||
# Autotools Gettext objects
|
||||
po/Makevars.template
|
||||
po/*.in
|
||||
po/*.sin
|
||||
po/*.sed
|
||||
po/*.header
|
||||
po/Rules-quot
|
||||
ABOUT-NLS
|
||||
po/*.gmo
|
||||
po/*.mo
|
||||
po/POTFILES
|
||||
po/stamp-po
|
||||
|
||||
# Doxygen files
|
||||
Doxyfile
|
||||
docs/.stamp
|
||||
docs/html
|
||||
docs/man
|
||||
|
||||
# Build directory
|
||||
/build/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
Contribution Instructions
|
||||
=========================
|
||||
|
||||
This project welcomes contributions. Contributions are managed through GitHub
|
||||
pull requests.
|
||||
|
||||
# Issue guidelines
|
||||
|
||||
If you find an issue with any code in this project, feel free to open an issue.
|
||||
Templates exist for bug reports and feature requests.
|
||||
|
||||
# Pull request guidelines
|
||||
|
||||
* Fork this repository.
|
||||
* Create a branch off master, and make your commits in that branch.
|
||||
* Test your changes locally before raising a pull request.
|
||||
|
||||
# Commit guidelines
|
||||
|
||||
* Isolate each commit to a single component/folder as far as possible
|
||||
* Use a standard message template. I follow the template from [this
|
||||
link](https://codeinthehole.com/tips/a-useful-template-for-commit-messages/)
|
||||
* Ensure that you have any necessary tests included to test your changes.
|
||||
|
||||
## Additional commit requirements
|
||||
|
||||
* Commits should have a `Signed-off-by` line. You can use `git commit -s` to
|
||||
automatically append this to your message.
|
||||
* Any generated objects should be included in `.gitignore`.
|
||||
* A commit should be self-contained, i.e., if I check out any commit in a clean
|
||||
workspace, I should be able to run `./.github/scripts/build-and-test.sh` and
|
||||
not encounter any failures.
|
||||
* Any additional dependencies should be called out in `INSTALL.md`
|
||||
|
||||
# Contribution License
|
||||
|
||||
The project is licensed under GPLv2, with a linking exception. Your
|
||||
contributions will also be licensed the same way. However, you maintain the
|
||||
copyright to your contributions.
|
||||
|
||||
If this is your first contribution to this project, you may update the `AUTHORS`
|
||||
file with your full name. Please keep the change to this file in a separate
|
||||
commit.
|
||||
|
||||
|
|
@ -0,0 +1,240 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based upon [Keep a Changelog].
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
|
||||
- Migrated CI builds to run in multiple distro containers.
|
||||
- Improved virtual mouse motion to use a smoother approach, as well as allow an isometric speed calculation. This change deprecates the old `Mouse.Speed` configuration option and replaces it with a Sensitivity percentage option.
|
||||
|
||||
### Fixed
|
||||
- Addressed meson build bugs found in v0.3.3
|
||||
|
||||
## [0.3.3] - 2026-03-12
|
||||
|
||||
**Note:** While this release does introduce Meson support and deprecate the
|
||||
Autotools framework, there are several bugs in the Meson build files, causing a
|
||||
number of missing builds, notably the systemd service file, the documentation
|
||||
and man pages. In addition, the translation files were not handled at all, and
|
||||
translation services were disabled by default, compared to the Autotools
|
||||
framework where it was enabled by default. These bugs will be addressed in the
|
||||
next release.
|
||||
|
||||
### Added
|
||||
- Updated build infrastructure to use Meson instead of Autotools.
|
||||
- Added a [privacy policy](PRIVACY.md) to comply with GDPR/CCPA regulations.
|
||||
- Added a [security policy](SECURITY.md) to help securely report vulnerabilities.
|
||||
- Added Dependabot configuration to keep Action files up to date.
|
||||
- Added a changelog script to automatically generate the latest release changelog.
|
||||
|
||||
### Changed
|
||||
- **BREAKING**: Removed vendored inih package and switched build framework to use inih from the system package manager.
|
||||
- `x52bugreport` tool now strips out potentially identifying information.
|
||||
- Removed the use of a 3rd party action to deploy generated Doxygen pages to the gh-pages branch. This now uses the modern gh-pages deployment action.
|
||||
- Updated release action to use softprops/action-gh-release@v2, since the original actions are no longer maintained.
|
||||
|
||||
### Deprecated
|
||||
- Autotools build framework is now deprecated, and will be removed in the next release.
|
||||
|
||||
### Fixed
|
||||
- Github Actions updated to use current set of runners
|
||||
- Fixed handling malformed UTF-8 input in libx52util
|
||||
- Fixed boundary check issue in libx52util that incorrectly returned `-E2BIG` if the output buffer was the exact size to capture the translated string and the null terminator.
|
||||
- Fixed potential UB in libx52-string-test
|
||||
- Fixed NULL pointer dereference in `libx52_exit`
|
||||
- Fixed errors identified by the GCC `-fanalyzer` flag
|
||||
|
||||
### Security
|
||||
- Updated action files to include permission blocks
|
||||
|
||||
## [0.3.2] - 2024-06-09
|
||||
### Added
|
||||
- Updated bug report utility to add details about build host details and
|
||||
compiler information.
|
||||
|
||||
### Fixed
|
||||
- Updated syntax check for calloc calls. See
|
||||
[#52](https://github.com/nirenjan/libx52/issues/52)
|
||||
- Fixed a tooling bug where running make check on a system without cmocka
|
||||
library installed would fail during daemon testing.
|
||||
- Cleaned up daemon protocol documentation
|
||||
|
||||
### Changed
|
||||
- Moved socket code around to make it easier to reuse the communication logic
|
||||
out in both client(s) and server.
|
||||
|
||||
## 0.3.1 - 2024-06-08
|
||||
|
||||
**Important:** Tag 0.3.1 has a bad Version file and should not be used. This has
|
||||
been superseded by 0.3.2 with corrected metadata. The changes from the previous
|
||||
release are the same.
|
||||
|
||||
## [0.3.0] - 2022-12-25
|
||||
### Added
|
||||
- Bug report utility to make it easier to gather system and build information
|
||||
when reporting issues.
|
||||
- Communication infrastructure to communicate with the daemon. This includes the
|
||||
`x52ctl` utility which can be used either interactively or non-interactively
|
||||
from a separate program.
|
||||
- Links to prebuilt packages in Ubuntu PPA and Arch Linux AUR
|
||||
- Ability to change mouse scroll direction. See
|
||||
[#45](https://github.com/nirenjan/libx52/issues/45)
|
||||
|
||||
### Changed
|
||||
- Renamed project from `x52pro-linux` to `libx52`
|
||||
|
||||
### Fixed
|
||||
- Removed dependency on `rsync` during `make install`
|
||||
- Reduced default logging level of daemon to error only. See
|
||||
[#38](https://github.com/nirenjan/libx52/issues/38)
|
||||
- Fixed daemon crash when disconnecting/reconnecting the joystick. See
|
||||
[#43](https://github.com/nirenjan/libx52/issues/43)
|
||||
|
||||
## [0.2.3] - 2021-09-20
|
||||
### Added
|
||||
- CI for macOS 11 (Big Sur)
|
||||
- Virtual mouse driver (on Linux only)
|
||||
|
||||
### Fixed
|
||||
- Device erratic behavior when running daemon. See
|
||||
[#33](https://github.com/nirenjan/libx52/issues/33).
|
||||
- `make install` on OpenSUSE Tumbleweed. See
|
||||
[#35](https://github.com/nirenjan/libx52/issues/35).
|
||||
|
||||
## [0.2.2] - 2021-09-03
|
||||
### Added
|
||||
- IO library to read and parse events from a supported joystick.
|
||||
- Event test utility which displays the events similar to evtest.
|
||||
- Daemon to control and update the X52 joystick.
|
||||
- Import pinelog library for daemon logging.
|
||||
- Import [inih](https://github.com/benhoyt/inih) library sources for daemon
|
||||
configuration parsing.
|
||||
|
||||
### Changed
|
||||
- Linux kernel driver to correctly handle the X52/X52 Pro. This is not required
|
||||
for users running kernels with at least the following versions:
|
||||
- 5.9+
|
||||
- 5.8.10+
|
||||
- 5.4.66+
|
||||
- 4.19.146+
|
||||
- Make udev rules customizable at build time, so that the right input group can
|
||||
be used in the actual rules file. This allows systems such as openSUSE which
|
||||
use `input` as the group for input devices to behave the same as Ubuntu and
|
||||
other similar systems.
|
||||
- Code layout changed to improve parallel builds.
|
||||
- x52cli tests modified to use cmocka tests.
|
||||
|
||||
## [0.2.1] - 2020-06-28
|
||||
### Added
|
||||
- Connect/Disconnect methods in libx52. These allow for dynamically connecting
|
||||
or disconnecting from a supported joystick without having to reinitialize the
|
||||
library.
|
||||
- Internationalization for the following:
|
||||
* libx52
|
||||
* x52test
|
||||
- Doxygen generation of HTML documentation for libx52 methods.
|
||||
- Tests for libx52 that run on all supported platforms.
|
||||
|
||||
### Changed
|
||||
- libx52_init no longer fails when a supported joystick is not connected.
|
||||
- Tests now use [TAP].
|
||||
- Python build scripts now use Python 3.
|
||||
|
||||
### Fixed
|
||||
- Error reporting in x52cli and x52test commands.
|
||||
- Handling of very large time_t values in `libx52_set_clock`
|
||||
- Secondary and tertiary clock setting when primary clock is set to local time
|
||||
and local timezone is observing daylight savings time (summer time). See
|
||||
[#20](https://github.com/nirenjan/libx52/issues/20).
|
||||
|
||||
## [0.2.0] - 2020-04-14
|
||||
### Changed
|
||||
- `libx52_init` now returns a `libx52_error_code`, and returns the
|
||||
`libx52_device` pointer in an output parameter.
|
||||
- All libx52 APIs now return a `libx52_error_code` indicating the error.
|
||||
- libx52 now checks the version of libusb and calls the appropriate method
|
||||
to set logging level.
|
||||
- x52test has an option to not sleep between consecutive calls to the libx52
|
||||
APIs.
|
||||
|
||||
### Fixed
|
||||
- `libx52_write_time` handling of large timezone offsets.
|
||||
|
||||
## [0.1.2] - 2017-08-17
|
||||
### Added
|
||||
- Autotools based unit tests - tests run on Linux only
|
||||
- libusb mock library for use by test programs
|
||||
- License file and usage clarification
|
||||
- Automatic builds on Ubuntu Trusty (14.04) with both GCC and clang on Travis
|
||||
- Enhanced documentation for libx52
|
||||
- Support for X52 (non-Pro) version
|
||||
- New raw time and date APIs for libx52
|
||||
- Support raw time and date commands in x52cli
|
||||
- Unicode translation points for halfwidth CJK and Katakana symbols
|
||||
|
||||
### Changed
|
||||
- Update Python character map generator to comply with PEP-8 guidelines
|
||||
|
||||
### Fixed
|
||||
- Compilation on OSX
|
||||
|
||||
## [0.1.1] - 2016-05-06
|
||||
### Added
|
||||
- Manpage for x52cli
|
||||
- Manpages for libx52 in RONN format
|
||||
- Unicode translation points for Latin, Greek and mathematical symbols
|
||||
- Travis-CI based automatic compilation
|
||||
|
||||
### Changed
|
||||
- libx52 clock API will return -EAGAIN if no update is needed.
|
||||
- x52test accepts a list of tests to run, defaulting to ALL
|
||||
- UTF-8 parser rewritten in Python
|
||||
|
||||
## [0.1.0] - 2015-12-09
|
||||
### Added
|
||||
- Support for semantic LED names instead of numbers in libx52
|
||||
- Simpler API to control clocks
|
||||
- Add documentation for X52 design and USB interface
|
||||
- Add CLI application to interface with libx52
|
||||
- Add test application to test all aspects of libx52
|
||||
- Add API to convert UTF-8 string to X52 character map
|
||||
|
||||
### Changed
|
||||
- Migrate project to autotools
|
||||
|
||||
### Deprecated
|
||||
- Mark kernel driver as proof-of-concept and unsuitable for production
|
||||
|
||||
## [0.0.2] - 2014-10-18
|
||||
### Added
|
||||
- Proposed design documentation for MFD pages and input mapping
|
||||
|
||||
### Fixed
|
||||
- Kernel module compilation on Linux v3.5 and later
|
||||
|
||||
## [0.0.1] - 2012-10-25
|
||||
### Added
|
||||
- Kernel module for Saitek X52 Pro Joystick
|
||||
- First release of userspace application
|
||||
|
||||
|
||||
[Keep a Changelog]: http://keepachangelog.com/en/1.0.0/
|
||||
[Semantic Versioning]: http://semver.org/spec/v2.0.0.html
|
||||
[TAP]: https://testanything.org
|
||||
[Unreleased]: https://github.com/nirenjan/libx52/compare/v0.3.3...HEAD
|
||||
[0.3.3]: https://github.com/nirenjan/libx52/compare/v0.3.2...v0.3.3
|
||||
[0.3.2]: https://github.com/nirenjan/libx52/compare/v0.3.0...v0.3.2
|
||||
[0.3.0]: https://github.com/nirenjan/libx52/compare/v0.2.3...v0.3.0
|
||||
[0.2.3]: https://github.com/nirenjan/libx52/compare/v0.2.2...v0.2.3
|
||||
[0.2.2]: https://github.com/nirenjan/libx52/compare/v0.2.1...v0.2.2
|
||||
[0.2.1]: https://github.com/nirenjan/libx52/compare/v0.2.0...v0.2.1
|
||||
[0.2.0]: https://github.com/nirenjan/libx52/compare/v0.1.2...v0.2.0
|
||||
[0.1.2]: https://github.com/nirenjan/libx52/compare/v0.1.1...v0.1.2
|
||||
[0.1.1]: https://github.com/nirenjan/libx52/compare/v0.1.0...v0.1.1
|
||||
[0.1.0]: https://github.com/nirenjan/libx52/compare/v0.0.2...v0.1.0
|
||||
[0.0.2]: https://github.com/nirenjan/libx52/compare/v0.0.1...v0.0.2
|
||||
[0.0.1]: https://github.com/nirenjan/libx52/releases/tag/v0.0.1
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,227 @@
|
|||
<doxygenlayout version="1.0">
|
||||
<!-- Generated by doxygen 1.8.17 -->
|
||||
<!-- Navigation index tabs for HTML output -->
|
||||
<navindex>
|
||||
<tab type="mainpage" visible="yes" title=""/>
|
||||
<tab type="pages" visible="yes" title="" intro=""/>
|
||||
<tab type="modules" visible="yes" title="" intro=""/>
|
||||
<tab type="namespaces" visible="yes" title="">
|
||||
<tab type="namespacelist" visible="yes" title="" intro=""/>
|
||||
<tab type="namespacemembers" visible="yes" title="" intro=""/>
|
||||
</tab>
|
||||
<tab type="interfaces" visible="yes" title="">
|
||||
<tab type="interfacelist" visible="yes" title="" intro=""/>
|
||||
<tab type="interfaceindex" visible="$ALPHABETICAL_INDEX" title=""/>
|
||||
<tab type="interfacehierarchy" visible="yes" title="" intro=""/>
|
||||
</tab>
|
||||
<tab type="classes" visible="yes" title="">
|
||||
<tab type="classlist" visible="yes" title="" intro=""/>
|
||||
<tab type="classindex" visible="$ALPHABETICAL_INDEX" title=""/>
|
||||
<tab type="hierarchy" visible="yes" title="" intro=""/>
|
||||
<tab type="classmembers" visible="yes" title="" intro=""/>
|
||||
</tab>
|
||||
<tab type="structs" visible="yes" title="">
|
||||
<tab type="structlist" visible="yes" title="" intro=""/>
|
||||
<tab type="structindex" visible="$ALPHABETICAL_INDEX" title=""/>
|
||||
</tab>
|
||||
<tab type="exceptions" visible="yes" title="">
|
||||
<tab type="exceptionlist" visible="yes" title="" intro=""/>
|
||||
<tab type="exceptionindex" visible="$ALPHABETICAL_INDEX" title=""/>
|
||||
<tab type="exceptionhierarchy" visible="yes" title="" intro=""/>
|
||||
</tab>
|
||||
<tab type="files" visible="yes" title="">
|
||||
<tab type="filelist" visible="yes" title="" intro=""/>
|
||||
<tab type="globals" visible="yes" title="" intro=""/>
|
||||
</tab>
|
||||
<tab type="examples" visible="yes" title="" intro=""/>
|
||||
<tab type="user" url="https://github.com/nirenjan/libx52" title="View on GitHub" />
|
||||
</navindex>
|
||||
|
||||
<!-- Layout definition for a class page -->
|
||||
<class>
|
||||
<briefdescription visible="yes"/>
|
||||
<includes visible="$SHOW_INCLUDE_FILES"/>
|
||||
<inheritancegraph visible="$CLASS_GRAPH"/>
|
||||
<collaborationgraph visible="$COLLABORATION_GRAPH"/>
|
||||
<memberdecl>
|
||||
<nestedclasses visible="yes" title=""/>
|
||||
<publictypes title=""/>
|
||||
<services title=""/>
|
||||
<interfaces title=""/>
|
||||
<publicslots title=""/>
|
||||
<signals title=""/>
|
||||
<publicmethods title=""/>
|
||||
<publicstaticmethods title=""/>
|
||||
<publicattributes title=""/>
|
||||
<publicstaticattributes title=""/>
|
||||
<protectedtypes title=""/>
|
||||
<protectedslots title=""/>
|
||||
<protectedmethods title=""/>
|
||||
<protectedstaticmethods title=""/>
|
||||
<protectedattributes title=""/>
|
||||
<protectedstaticattributes title=""/>
|
||||
<packagetypes title=""/>
|
||||
<packagemethods title=""/>
|
||||
<packagestaticmethods title=""/>
|
||||
<packageattributes title=""/>
|
||||
<packagestaticattributes title=""/>
|
||||
<properties title=""/>
|
||||
<events title=""/>
|
||||
<privatetypes title=""/>
|
||||
<privateslots title=""/>
|
||||
<privatemethods title=""/>
|
||||
<privatestaticmethods title=""/>
|
||||
<privateattributes title=""/>
|
||||
<privatestaticattributes title=""/>
|
||||
<friends title=""/>
|
||||
<related title="" subtitle=""/>
|
||||
<membergroups visible="yes"/>
|
||||
</memberdecl>
|
||||
<detaileddescription title=""/>
|
||||
<memberdef>
|
||||
<inlineclasses title=""/>
|
||||
<typedefs title=""/>
|
||||
<enums title=""/>
|
||||
<services title=""/>
|
||||
<interfaces title=""/>
|
||||
<constructors title=""/>
|
||||
<functions title=""/>
|
||||
<related title=""/>
|
||||
<variables title=""/>
|
||||
<properties title=""/>
|
||||
<events title=""/>
|
||||
</memberdef>
|
||||
<allmemberslink visible="yes"/>
|
||||
<usedfiles visible="$SHOW_USED_FILES"/>
|
||||
<authorsection visible="yes"/>
|
||||
</class>
|
||||
|
||||
<!-- Layout definition for a namespace page -->
|
||||
<namespace>
|
||||
<briefdescription visible="yes"/>
|
||||
<memberdecl>
|
||||
<nestednamespaces visible="yes" title=""/>
|
||||
<constantgroups visible="yes" title=""/>
|
||||
<interfaces visible="yes" title=""/>
|
||||
<classes visible="yes" title=""/>
|
||||
<structs visible="yes" title=""/>
|
||||
<exceptions visible="yes" title=""/>
|
||||
<typedefs title=""/>
|
||||
<sequences title=""/>
|
||||
<dictionaries title=""/>
|
||||
<enums title=""/>
|
||||
<functions title=""/>
|
||||
<variables title=""/>
|
||||
<membergroups visible="yes"/>
|
||||
</memberdecl>
|
||||
<detaileddescription title=""/>
|
||||
<memberdef>
|
||||
<inlineclasses title=""/>
|
||||
<typedefs title=""/>
|
||||
<sequences title=""/>
|
||||
<dictionaries title=""/>
|
||||
<enums title=""/>
|
||||
<functions title=""/>
|
||||
<variables title=""/>
|
||||
</memberdef>
|
||||
<authorsection visible="yes"/>
|
||||
</namespace>
|
||||
|
||||
<!-- Layout definition for a file page -->
|
||||
<file>
|
||||
<briefdescription visible="yes"/>
|
||||
<includes visible="$SHOW_INCLUDE_FILES"/>
|
||||
<includegraph visible="$INCLUDE_GRAPH"/>
|
||||
<includedbygraph visible="$INCLUDED_BY_GRAPH"/>
|
||||
<sourcelink visible="yes"/>
|
||||
<memberdecl>
|
||||
<interfaces visible="yes" title=""/>
|
||||
<classes visible="yes" title=""/>
|
||||
<structs visible="yes" title=""/>
|
||||
<exceptions visible="yes" title=""/>
|
||||
<namespaces visible="yes" title=""/>
|
||||
<constantgroups visible="yes" title=""/>
|
||||
<defines title=""/>
|
||||
<typedefs title=""/>
|
||||
<sequences title=""/>
|
||||
<dictionaries title=""/>
|
||||
<enums title=""/>
|
||||
<functions title=""/>
|
||||
<variables title=""/>
|
||||
<membergroups visible="yes"/>
|
||||
</memberdecl>
|
||||
<detaileddescription title=""/>
|
||||
<memberdef>
|
||||
<inlineclasses title=""/>
|
||||
<defines title=""/>
|
||||
<typedefs title=""/>
|
||||
<sequences title=""/>
|
||||
<dictionaries title=""/>
|
||||
<enums title=""/>
|
||||
<functions title=""/>
|
||||
<variables title=""/>
|
||||
</memberdef>
|
||||
<authorsection/>
|
||||
</file>
|
||||
|
||||
<!-- Layout definition for a group page -->
|
||||
<group>
|
||||
<briefdescription visible="yes"/>
|
||||
<groupgraph visible="$GROUP_GRAPHS"/>
|
||||
<memberdecl>
|
||||
<nestedgroups visible="yes" title=""/>
|
||||
<dirs visible="yes" title=""/>
|
||||
<files visible="yes" title=""/>
|
||||
<namespaces visible="yes" title=""/>
|
||||
<classes visible="yes" title=""/>
|
||||
<defines title=""/>
|
||||
<typedefs title=""/>
|
||||
<sequences title=""/>
|
||||
<dictionaries title=""/>
|
||||
<enums title=""/>
|
||||
<enumvalues title=""/>
|
||||
<functions title=""/>
|
||||
<variables title=""/>
|
||||
<signals title=""/>
|
||||
<publicslots title=""/>
|
||||
<protectedslots title=""/>
|
||||
<privateslots title=""/>
|
||||
<events title=""/>
|
||||
<properties title=""/>
|
||||
<friends title=""/>
|
||||
<membergroups visible="yes"/>
|
||||
</memberdecl>
|
||||
<detaileddescription title=""/>
|
||||
<memberdef>
|
||||
<pagedocs/>
|
||||
<inlineclasses title=""/>
|
||||
<defines title=""/>
|
||||
<typedefs title=""/>
|
||||
<sequences title=""/>
|
||||
<dictionaries title=""/>
|
||||
<enums title=""/>
|
||||
<enumvalues title=""/>
|
||||
<functions title=""/>
|
||||
<variables title=""/>
|
||||
<signals title=""/>
|
||||
<publicslots title=""/>
|
||||
<protectedslots title=""/>
|
||||
<privateslots title=""/>
|
||||
<events title=""/>
|
||||
<properties title=""/>
|
||||
<friends title=""/>
|
||||
</memberdef>
|
||||
<authorsection visible="yes"/>
|
||||
</group>
|
||||
|
||||
<!-- Layout definition for a directory page -->
|
||||
<directory>
|
||||
<briefdescription visible="yes"/>
|
||||
<directorygraph visible="yes"/>
|
||||
<memberdecl>
|
||||
<dirs visible="yes"/>
|
||||
<files visible="yes"/>
|
||||
</memberdecl>
|
||||
<detaileddescription title=""/>
|
||||
</directory>
|
||||
</doxygenlayout>
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
Installation instructions for libx52
|
||||
==========================================
|
||||
|
||||
Build has been tested on the following operating systems (x86-64 only):
|
||||
|
||||
* Ubuntu 22.04 LTS
|
||||
* Ubuntu 24.04 LTS
|
||||
* Fedora latest (Fedora 42, as of this commit)
|
||||
* Archlinux
|
||||
* Alpine Linux (Experimental)
|
||||
* Ubuntu 26.04 LTS (Experimental as of this commit)
|
||||
* macOS (latest tag on Github, ARM)
|
||||
|
||||
# Prerequisites
|
||||
|
||||
## Required Packages
|
||||
|
||||
* meson
|
||||
* ninja
|
||||
* gettext
|
||||
* hidapi + headers
|
||||
* inih
|
||||
* libusb-1.0 + headers
|
||||
* pkg-config
|
||||
* python3 (3.6 or greater)
|
||||
* git (not required for builds, but necessary to clone the repository)
|
||||
|
||||
### Installation instructions
|
||||
|
||||
| Platform | Install instructions |
|
||||
| -------- | -------------------- |
|
||||
| Ubuntu | `sudo apt-get install meson gettext libhidapi-dev libevdev-dev libusb-1.0-0-dev libinih-dev pkg-config python3 git` |
|
||||
| MacOS + Homebrew | `brew install meson gettext hidapi libusb pkg-config python3 git` |
|
||||
| Arch Linux | `pacman -S base-devel meson libusb hidapi libevdev libinih python git` |
|
||||
| Fedora | `sudo dnf install meson gettext-devel findutils hidapi-devel libusb-devel libevdev-devel inih-devel pkg-config python3 git` |
|
||||
|
||||
## Optional Packages
|
||||
|
||||
* doxygen - to generate HTML documentation and man pages
|
||||
* libcmocka (1.1 or greater) + headers - to run unit tests
|
||||
* libevdev + headers (on Linux) - to add virtual keyboard/mouse support
|
||||
|
||||
# Installation Instructions
|
||||
|
||||
1. Clone the repository
|
||||
```
|
||||
git clone https://github.com/nirenjan/libx52.git
|
||||
```
|
||||
|
||||
2. Configure the build (from the repository root)
|
||||
```
|
||||
cd libx52
|
||||
meson setup build -Dprefix=/usr
|
||||
```
|
||||
|
||||
3. Compile and install:
|
||||
```
|
||||
meson compile -C build && meson install -C build
|
||||
```
|
||||
|
||||
You may want to remove or edit the `-Dprefix=/usr` option, most users prefer
|
||||
non-distro binaries in `/usr/local` (default without `-Dprefix`) or `/opt`.
|
||||
|
||||
## Configuration options
|
||||
|
||||
### udev
|
||||
|
||||
The configuration system should automatically detect the udev rules directory,
|
||||
but you can override it by using the following argument to `meson setup`:
|
||||
|
||||
```
|
||||
-Dudev-rules-dir=/path/to/udev/rules.d
|
||||
```
|
||||
|
||||
### Input group
|
||||
|
||||
The udev rules that are installed provide read/write access to members of the
|
||||
input devices group. This defaults to `plugdev`, but can be modified using
|
||||
the following argument to `meson setup`:
|
||||
|
||||
```
|
||||
-Dinput-group=group
|
||||
```
|
||||
|
||||
### Systemd support
|
||||
|
||||
The X52 daemon can run either as a foreground process, or it can daemonize
|
||||
itself to run in the background. Typical deployments with systemd will have it
|
||||
run in the foreground, and disable timestamps in the logs, since those are
|
||||
inserted automatically by journald.
|
||||
|
||||
Systemd support is enabled by default, which disables timestamps in the program
|
||||
logs, but you can re-enable timestamps by passing `-Dsystemd-logs=disabled`
|
||||
argument to `meson setup`
|
||||
|
||||
It is also possible to configure the directory in which the service file is
|
||||
installed with the following option. This is ignored if systemd is not found.
|
||||
|
||||
```
|
||||
-Dsystemd-unit-dir=/path/to/systemd/system
|
||||
```
|
||||
|
|
@ -337,3 +337,23 @@ 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 Lesser General
|
||||
Public License instead of this License.
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
|
||||
X52Pro-Linux is licensed under the terms of the GNU General Public License with
|
||||
the following clarification and special exception.
|
||||
|
||||
Linking this module statically or dynamically with other modules is making a
|
||||
combined work based on this module. Thus, the terms and conditions of the GNU
|
||||
General Public License cover the whole combination.
|
||||
|
||||
As a special exception, the copyright holders of this module give you
|
||||
permission to link this module with independent modules to produce an
|
||||
executable, regardless of the license terms of these independent modules, and
|
||||
to copy and distribute the resulting executable under terms of your choice,
|
||||
provided that you also meet, for each linked independent module, the terms and
|
||||
conditions of the license of that module. An independent module is a module
|
||||
which is not derived from or based on this module. If you modify this module,
|
||||
you may extend this exception to your version of the module, but you are not
|
||||
obliged to do so. If you do not wish to do so, delete this exception statement
|
||||
from your version.
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
# Privacy
|
||||
|
||||
This document describes how the libx52 project handles data, for compliance with
|
||||
privacy laws such as the GDPR and CCPA.
|
||||
|
||||
## Summary
|
||||
|
||||
- **No automatic collection**: The software does not transmit any data to the
|
||||
project or third parties. No telemetry, analytics, or crash reporting is
|
||||
implemented.
|
||||
- **Local operation**: All processing is on your machine. Configuration and logs
|
||||
stay local unless you choose to share them (e.g. when opening a bug report).
|
||||
|
||||
## Data that may be displayed or stored locally
|
||||
|
||||
| Data | Where | Purpose |
|
||||
|------|--------|---------|
|
||||
| **Device serial number** | `evtest` only | Shown when you run evtest against a connected device (for local identification). **Not** included in **x52bugreport** output. |
|
||||
| **Device type info** | `x52bugreport`, `evtest` | Vendor ID, product ID, device version, manufacturer and product name (e.g. "Saitek" / "X52 Pro"). No serial number or hostname in bugreport. |
|
||||
| **System information** | `x52bugreport` only | Kernel name/release, machine architecture, kernel version string. **Hostname is not included.** |
|
||||
| **Build environment** | `x52bugreport` only | Compiler, build date, kernel/arch/OS version at build time. No hostname or other machine identifier. |
|
||||
| **Paths** | Daemon logs (if enabled) | Log file path, config path, socket path. Default paths use system directories (e.g. `/var`, `/run`, `/etc`), not your home directory. |
|
||||
| **Configuration** | Config files (e.g. under `/etc/x52d/`) | MFD/LED and daemon settings. Stored only on your system. |
|
||||
|
||||
None of this data is sent anywhere by the software. The only way it leaves your
|
||||
system is if you voluntarily paste it (e.g. into a GitHub issue).
|
||||
|
||||
## Bug reports
|
||||
|
||||
**x52bugreport** output is designed to avoid personal and device identifiers:
|
||||
|
||||
- **Device serial number** and **system hostname** are **not** included in the output.
|
||||
- Included: package/build version, compiler, build date, kernel and machine type, library versions, device vendor/product ID and device name (manufacturer/product strings only).
|
||||
|
||||
You may still redact any line before posting if you prefer. For most bugs, the information above is sufficient.
|
||||
|
||||
## Your rights (GDPR / CCPA style)
|
||||
|
||||
- **No account or sign-up** is required to use the software, so we do not hold
|
||||
an account-based profile on you.
|
||||
- **No selling of data**: We do not collect or sell personal data.
|
||||
- **Transparency**: This document describes what the software can display or
|
||||
store locally.
|
||||
- **Control**: You decide whether to run `x52bugreport` and what to include when
|
||||
opening an issue.
|
||||
|
||||
## Third-party services
|
||||
|
||||
- **Source and issues**: If you clone the repo or open an issue on GitHub,
|
||||
GitHub’s privacy policy applies to that interaction ([GitHub Privacy
|
||||
Statement](https://docs.github.com/en/site-policy/privacy-policies/github-privacy-statement)).
|
||||
- **Packages**: Installation via Ubuntu PPA or Arch AUR is subject to
|
||||
Canonical’s or the AUR’s respective terms and privacy practices.
|
||||
|
||||
## Changes
|
||||
|
||||
We may update this document to reflect changes in the software or in legal
|
||||
requirements. The current version is in the project repository.
|
||||
51
README.md
51
README.md
|
|
@ -1,6 +1,12 @@
|
|||
Saitek X52Pro joystick driver for Linux
|
||||
=======================================
|
||||
|
||||

|
||||

|
||||

|
||||
[](https://sonarcloud.io/summary/new_code?id=nirenjan_libx52)
|
||||
[](https://codecov.io/gh/nirenjan/libx52)
|
||||
|
||||
This project adds a new driver for the Saitek/MadCatz X52 Pro flight
|
||||
control system. The X52 pro is a HOTAS (hand on throttle and stick)
|
||||
with 7 axes, 39 buttons, 1 hat and 1 thumbstick and a multi-function
|
||||
|
|
@ -8,16 +14,39 @@ display which is programmable.
|
|||
|
||||
Currently, only Windows drivers are available from Saitek PLC, which
|
||||
led me to develop a new Linux driver which can program the MFD and
|
||||
the individual LEDs on the joystick. Although the standard usbhid
|
||||
driver is capable of reading the joystick, it is not sufficient to
|
||||
really utilize all the capabilities of this system.
|
||||
the individual LEDs on the joystick. The standard usbhid driver is
|
||||
capable of reading the joystick, but it cannot control the MFD or LEDs.
|
||||
|
||||
This project is currently a work-in-progress. However a high level
|
||||
outline of the current objectives are listed below:
|
||||
Most of the extra functionality can be handled from userspace. See
|
||||
the individual folders for README information.
|
||||
|
||||
* Write a kernel module and export sysfs interfaces to act as a
|
||||
driver.
|
||||
* Write a userspace program that can configure the kernel module
|
||||
and create custom button mappings to keyboard or mouse events.
|
||||
* Add interrupt handling and export a /dev/input/jsX interface.
|
||||
* Allow userspace programs to register callbacks on MFD button events.
|
||||
For data handling and privacy (e.g. GDPR/CCPA), see [PRIVACY.md](PRIVACY.md).
|
||||
|
||||
**Note:** This repository currently only provides commandline interfaces to
|
||||
control the MFD and LEDs. If you are not comfortable working in the commandline,
|
||||
then the [gx52](https://gitlab.com/leinardi/gx52) project might be a better fit
|
||||
for your needs as it provides a graphical interface to control the MFD and LEDs.
|
||||
|
||||
# Installing released versions
|
||||
|
||||
Beginning from version v0.2.3, prebuilt packages are available on Ubuntu PPA and
|
||||
the Arch User Repository.
|
||||
|
||||
## Ubuntu
|
||||
|
||||
This project has been released as a PPA on Ubuntu. To install the package, run
|
||||
the following commands in the terminal.
|
||||
|
||||
```
|
||||
sudo apt-add-repository ppa:nirenjan/libx52
|
||||
sudo apt update
|
||||
sudo apt install libx52-1
|
||||
```
|
||||
|
||||
## Arch Linux
|
||||
|
||||
This is available on the [AUR](https://aur.archlinux.org/packages/libx52)
|
||||
|
||||
# Building and installing from source
|
||||
|
||||
See [INSTALL.md](INSTALL.md)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
# Security Policy
|
||||
|
||||
## Reporting Security Vulnerabiltiies
|
||||
|
||||
You may report a security vulnerability by [creating a new security advisory](https://github.com/nirenjan/libx52/security/advisories/new).
|
||||
|
||||
## Supported Versions
|
||||
|
||||
All security fixes will be made to the `master` branch. Older versions are not supported.
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* libx52 bugreport utility
|
||||
*
|
||||
* Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "build-config.h"
|
||||
#include <stdio.h>
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
#include <sys/utsname.h>
|
||||
|
||||
#include "libusb.h"
|
||||
#include "hidapi.h"
|
||||
#include <libx52/libx52io.h>
|
||||
#include "version-info.h"
|
||||
|
||||
static void print_sysinfo(void)
|
||||
{
|
||||
struct utsname uts;
|
||||
|
||||
puts("");
|
||||
puts("System info:");
|
||||
puts("============");
|
||||
if (uname(&uts) < 0) {
|
||||
printf("Unable to get system info: %s\n", strerror(errno));
|
||||
} else {
|
||||
printf("%s %s %s (%s)\n", uts.sysname, uts.release, uts.machine, uts.version);
|
||||
}
|
||||
}
|
||||
|
||||
static void print_devinfo(void)
|
||||
{
|
||||
libx52io_context *ctx;
|
||||
int rc;
|
||||
|
||||
puts("");
|
||||
puts("Device info:");
|
||||
puts("============");
|
||||
|
||||
rc = libx52io_init(&ctx);
|
||||
if (rc != LIBX52IO_SUCCESS) {
|
||||
puts(libx52io_strerror(rc));
|
||||
return;
|
||||
}
|
||||
|
||||
rc = libx52io_open(ctx);
|
||||
if (rc != LIBX52IO_SUCCESS) {
|
||||
puts(libx52io_strerror(rc));
|
||||
goto devinfo_cleanup;
|
||||
}
|
||||
|
||||
printf("Device ID: vendor 0x%04x product 0x%04x version 0x%04x\n",
|
||||
libx52io_get_vendor_id(ctx),
|
||||
libx52io_get_product_id(ctx),
|
||||
libx52io_get_device_version(ctx));
|
||||
printf("Device name: '%s' '%s'\n",
|
||||
libx52io_get_manufacturer_string(ctx),
|
||||
libx52io_get_product_string(ctx));
|
||||
|
||||
libx52io_close(ctx);
|
||||
devinfo_cleanup:
|
||||
libx52io_exit(ctx);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
const struct libusb_version *libusb;
|
||||
|
||||
puts("libx52 bugreport");
|
||||
puts("================");
|
||||
printf("Package version: %s\n", VERSION);
|
||||
printf("Build version: %s\n", BUILD_VERSION);
|
||||
printf("Build host kernel: %s\n", BUILD_KERNEL);
|
||||
printf("Build host architecture: %s\n", BUILD_ARCH);
|
||||
printf("Build host version: %s\n", BUILD_OS_VERSION);
|
||||
printf("Build target: %s\n", BUILD_TARGET);
|
||||
printf("Compiler: %s\n", BUILD_COMPILER);
|
||||
printf("Build date: %s\n", BUILD_DATE);
|
||||
printf("version-info %s\n", BUILD_VERSION_INFO_IDENT);
|
||||
|
||||
puts("");
|
||||
puts("Built against:");
|
||||
puts("==============");
|
||||
|
||||
printf("libusb API version: 0x%08x\n", LIBUSB_API_VERSION);
|
||||
#if defined HID_API_VERSION_STR
|
||||
printf("hidapi version: %s\n", HID_API_VERSION_STR);
|
||||
#endif
|
||||
|
||||
libusb = libusb_get_version();
|
||||
puts("");
|
||||
puts("System versions:");
|
||||
puts("================");
|
||||
printf("libusb: %d.%d.%d.%d%s (%s)\n",
|
||||
libusb->major, libusb->minor, libusb->micro, libusb->nano,
|
||||
libusb->rc, libusb->describe);
|
||||
#if defined HID_API_VERSION_STR
|
||||
printf("hidapi: %s\n", hid_version_str());
|
||||
#endif
|
||||
|
||||
print_sysinfo();
|
||||
print_devinfo();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
@page x52bugreport System information utility for bug reports
|
||||
|
||||
\htmlonly
|
||||
<b>x52bugreport</b> - System information utility for bug reports
|
||||
\endhtmlonly
|
||||
|
||||
# SYNOPSIS
|
||||
|
||||
<tt>\b x52bugreport</tt>
|
||||
|
||||
# DESCRIPTION
|
||||
|
||||
\b x52bugreport is a utility that collects and reports information on the
|
||||
current system and build environment. The reported information can be provided
|
||||
when raising a bug report on https://github.com/nirenjan/libx52/issues.
|
||||
|
||||
The output does not include device serial number or system hostname. You may
|
||||
redact any line before posting if you prefer. See the project PRIVACY.md for
|
||||
details.
|
||||
|
||||
# USAGE
|
||||
|
||||
\b x52bugreport
|
||||
|
||||
*/
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
#######################################################################
|
||||
# Version information
|
||||
#######################################################################
|
||||
compiler_version = run_command(compiler.cmd_array(), '--version',
|
||||
capture: true,
|
||||
check: true).stdout().split('\n')[0]
|
||||
|
||||
build_date = run_command('date', '+%Y-%m-%dT%H:%M:%S%z',
|
||||
capture: true,
|
||||
check: true).stdout().strip()
|
||||
|
||||
build_kernel = run_command('uname', '-sr',
|
||||
capture: true,
|
||||
check: true).stdout().strip()
|
||||
|
||||
build_arch = run_command('uname', '-mp',
|
||||
capture: true,
|
||||
check: true).stdout().strip()
|
||||
|
||||
build_os_version = run_command('uname', '-v',
|
||||
capture: true,
|
||||
check: true).stdout().strip()
|
||||
|
||||
built_for = '@0@ @1@ @2@-endian'.format(
|
||||
host_machine.system(),
|
||||
host_machine.cpu(),
|
||||
host_machine.endian(),
|
||||
)
|
||||
|
||||
git = find_program('git', required: false)
|
||||
if git.found()
|
||||
vcs_describe = run_command(git, 'describe', '--dirty',
|
||||
capture: true,
|
||||
check: false).stdout().strip()
|
||||
if vcs_describe == ''
|
||||
vcs_describe = meson.project_version()
|
||||
endif
|
||||
else
|
||||
vcs_describe = meson.project_version()
|
||||
endif
|
||||
|
||||
version_data = configuration_data()
|
||||
version_data.set_quoted('BUILD_VERSION', vcs_describe)
|
||||
version_data.set_quoted('BUILD_DATE', build_date)
|
||||
version_data.set_quoted('BUILD_KERNEL', build_kernel)
|
||||
version_data.set_quoted('BUILD_ARCH', build_arch)
|
||||
version_data.set_quoted('BUILD_OS_VERSION', build_os_version)
|
||||
version_data.set_quoted('BUILD_COMPILER', compiler_version)
|
||||
version_data.set_quoted('BUILD_TARGET', built_for)
|
||||
version_data.set_quoted('BUILD_VERSION_INFO_IDENT', '$Id$')
|
||||
|
||||
version_info_h = configure_file(
|
||||
input: 'version-info.h.meson',
|
||||
output: 'version-info.h',
|
||||
configuration: version_data
|
||||
)
|
||||
|
||||
# x52bugreport
|
||||
exe_bugreport = executable('x52bugreport', 'bugreport.c',
|
||||
install: true,
|
||||
include_directories: [includes],
|
||||
dependencies: [dep_libusb, dep_hidapi],
|
||||
link_with: [lib_libx52io])
|
||||
|
||||
# Test only to get code coverage
|
||||
test('x52bugreport', exe_bugreport, protocol:'exitcode')
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
#mesondefine BUILD_VERSION
|
||||
#mesondefine BUILD_DATE
|
||||
#mesondefine BUILD_KERNEL
|
||||
#mesondefine BUILD_ARCH
|
||||
#mesondefine BUILD_OS_VERSION
|
||||
#mesondefine BUILD_COMPILER
|
||||
#mesondefine BUILD_TARGET
|
||||
#mesondefine BUILD_VERSION_INFO_IDENT
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# x52cli
|
||||
executable('x52cli', 'x52_cli.c',
|
||||
install: true,
|
||||
include_directories: [includes],
|
||||
link_with: lib_libx52)
|
||||
|
||||
test_cli = executable('test-cli', 'x52_cli.c', 'test_x52_cli.c',
|
||||
build_by_default: false,
|
||||
c_args: ['-DX52_CLI_TESTING'],
|
||||
include_directories: [includes],
|
||||
dependencies: [dep_cmocka],
|
||||
)
|
||||
|
||||
test('test-cli', test_cli, protocol: 'tap')
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - CLI test harness
|
||||
*
|
||||
* Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <setjmp.h>
|
||||
#include <cmocka.h>
|
||||
|
||||
#include <libx52/libx52.h>
|
||||
|
||||
extern int run_main(int argc, char **argv);
|
||||
|
||||
/* Wrapper functions for libx52 */
|
||||
int libx52_init(libx52_device **dev)
|
||||
{
|
||||
function_called();
|
||||
*dev = NULL;
|
||||
return mock();
|
||||
}
|
||||
|
||||
int libx52_connect(libx52_device *dev)
|
||||
{
|
||||
(void)dev;
|
||||
function_called();
|
||||
return mock();
|
||||
}
|
||||
|
||||
int libx52_update(libx52_device *dev)
|
||||
{
|
||||
(void)dev;
|
||||
return LIBX52_SUCCESS;
|
||||
}
|
||||
|
||||
void libx52_exit(libx52_device *dev)
|
||||
{
|
||||
(void)dev;
|
||||
return;
|
||||
}
|
||||
|
||||
const char *libx52_strerror(libx52_error_code rc)
|
||||
{
|
||||
(void)rc;
|
||||
function_called();
|
||||
return "";
|
||||
}
|
||||
|
||||
int libx52_set_text(libx52_device *x52, uint8_t line, const char *text, uint8_t length)
|
||||
{
|
||||
function_called();
|
||||
assert_ptr_equal(x52, NULL);
|
||||
check_expected(line);
|
||||
check_expected(text);
|
||||
check_expected(length);
|
||||
|
||||
return mock();
|
||||
}
|
||||
|
||||
int libx52_set_led_state(libx52_device *x52, libx52_led_id id, libx52_led_state state)
|
||||
{
|
||||
function_called();
|
||||
assert_ptr_equal(x52, NULL);
|
||||
check_expected(id);
|
||||
check_expected(state);
|
||||
|
||||
return mock();
|
||||
}
|
||||
|
||||
int libx52_set_clock(libx52_device *x52, time_t time, int local)
|
||||
{
|
||||
function_called();
|
||||
assert_ptr_equal(x52, NULL);
|
||||
check_expected(time);
|
||||
check_expected(local);
|
||||
|
||||
return mock();
|
||||
}
|
||||
|
||||
int libx52_set_clock_timezone(libx52_device *x52, libx52_clock_id clock, int offset)
|
||||
{
|
||||
function_called();
|
||||
assert_ptr_equal(x52, NULL);
|
||||
check_expected(clock);
|
||||
check_expected(offset);
|
||||
|
||||
return mock();
|
||||
}
|
||||
|
||||
int libx52_set_clock_format(libx52_device *x52, libx52_clock_id clock, libx52_clock_format format)
|
||||
{
|
||||
function_called();
|
||||
assert_ptr_equal(x52, NULL);
|
||||
check_expected(clock);
|
||||
check_expected(format);
|
||||
|
||||
return mock();
|
||||
}
|
||||
|
||||
int libx52_set_time(libx52_device *x52, uint8_t hour, uint8_t minute)
|
||||
{
|
||||
function_called();
|
||||
assert_ptr_equal(x52, NULL);
|
||||
check_expected(hour);
|
||||
check_expected(minute);
|
||||
|
||||
return mock();
|
||||
}
|
||||
|
||||
int libx52_set_date(libx52_device *x52, uint8_t dd, uint8_t mm, uint8_t yy)
|
||||
{
|
||||
function_called();
|
||||
assert_ptr_equal(x52, NULL);
|
||||
check_expected(dd);
|
||||
check_expected(mm);
|
||||
check_expected(yy);
|
||||
|
||||
return mock();
|
||||
}
|
||||
|
||||
int libx52_set_date_format(libx52_device *x52, libx52_date_format format)
|
||||
{
|
||||
function_called();
|
||||
assert_ptr_equal(x52, NULL);
|
||||
check_expected(format);
|
||||
|
||||
return mock();
|
||||
}
|
||||
|
||||
int libx52_set_brightness(libx52_device *x52, uint8_t mfd, uint16_t brightness)
|
||||
{
|
||||
function_called();
|
||||
assert_ptr_equal(x52, NULL);
|
||||
check_expected(mfd);
|
||||
check_expected(brightness);
|
||||
|
||||
return mock();
|
||||
}
|
||||
|
||||
int libx52_set_shift(libx52_device *x52, uint8_t state)
|
||||
{
|
||||
function_called();
|
||||
assert_ptr_equal(x52, NULL);
|
||||
check_expected(state);
|
||||
|
||||
return mock();
|
||||
}
|
||||
|
||||
int libx52_set_blink(libx52_device *x52, uint8_t state)
|
||||
{
|
||||
function_called();
|
||||
assert_ptr_equal(x52, NULL);
|
||||
check_expected(state);
|
||||
|
||||
return mock();
|
||||
}
|
||||
|
||||
int libx52_vendor_command(libx52_device *x52, uint16_t index, uint16_t value)
|
||||
{
|
||||
function_called();
|
||||
assert_ptr_equal(x52, NULL);
|
||||
check_expected(index);
|
||||
check_expected(value);
|
||||
|
||||
return mock();
|
||||
}
|
||||
|
||||
#include "test_x52_cli_tests.c"
|
||||
#define TEST_LIST
|
||||
|
||||
const struct CMUnitTest tests[] = {
|
||||
#include "test_x52_cli_tests.c"
|
||||
};
|
||||
|
||||
int main(void)
|
||||
{
|
||||
cmocka_set_message_output(CM_OUTPUT_TAP);
|
||||
cmocka_run_group_tests(tests, NULL, NULL);
|
||||
return 0;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,547 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver
|
||||
*
|
||||
* Copyright (C) 2015-2021 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
@page x52cli Command Line Interface to libx52
|
||||
|
||||
\htmlonly
|
||||
<b>x52cli</b> - Command line interface to libx52
|
||||
\endhtmlonly
|
||||
|
||||
# SYNOPSIS
|
||||
<tt>\b x52cli \a commands [\a command-options]</tt>
|
||||
|
||||
# DESCRIPTION
|
||||
|
||||
\b x52cli is a command line interface to the X52 library that allows you to set
|
||||
the LEDs and different parameters on the multifunction display (MFD).
|
||||
|
||||
Running \b x52cli without any arguments will display a brief help message.
|
||||
|
||||
# COMMANDS
|
||||
|
||||
Commands are not case sensitive.
|
||||
|
||||
- <tt>\b bri { \b mfd | \b led } < \a brightness >
|
||||
</tt>\n \manonly \fR \endmanonly
|
||||
Set the brightness of the \b MFD or <b>LED</b>s. \a brightness can be any
|
||||
numeric value between 0 and 128. Higher values are accepted, but may not have
|
||||
the desired effect.
|
||||
|
||||
- <tt>\b mfd < \a line > < \a text > </tt>\n \manonly \fR \endmanonly
|
||||
Set the text on the MFD \a line. \a line can be \c 0, \c 1 or \c 2, and refers
|
||||
to the first, second or third line of the multifunction display respectively.
|
||||
\a text cannot have embedded NUL characters (0x00) and must correspond with
|
||||
the character map fo the MFD. \a text should be quoted in order to preserve
|
||||
embedded whitespace. To pass raw hex values, use \b printf(1) as shown in the
|
||||
\ref x52cli_examples section. Note that \a text is limited to a length of 16
|
||||
characters. While you can pass in longer strings, they will be silenty
|
||||
truncated.
|
||||
|
||||
- <tt>\b led < \a led-id > < \a state > </tt>\n \manonly \fR \endmanonly
|
||||
Set the LED \a led-id to \a state. See \ref x52cli_leds for a list of
|
||||
supported values.
|
||||
|
||||
- <tt>\b blink { \b on | \b off } </tt>\n \manonly \fR \endmanonly
|
||||
Turn the \b blink state \b on or \b off.
|
||||
|
||||
- <tt>\b shift { \b on | \b off } </tt>\n \manonly \fR \endmanonly
|
||||
Turn the \b shift indicator in the MFD \b on or \b off.
|
||||
|
||||
- <tt>\b clock { \b local | \b gmt } { \b 12hr | \b 24hr }
|
||||
{ \b ddmmyy | \b mmddyy | \b yymmdd }</tt>\n \manonly \fR \endmanonly
|
||||
Set the clock 1 display to the current \b local or \b GMT time and date.
|
||||
Clock can be configured to display in either \b 12hr or \b 24hr mode. Date
|
||||
can be displayed in one of the following formats: \b DD-MM-YY, \b MM-DD-YY,
|
||||
or \b YY-MM-DD.
|
||||
|
||||
- <tt>\b offset { \b 2 | \b 3 } < \a offset-val >
|
||||
{ \b 12hr | \b 24hr }</tt>\n \manonly \fR \endmanonly
|
||||
Set the offset for clock \b 2 or \b 3 and configure them to display in either
|
||||
\b 12hr or \b 24hr mode. \a offset-val is in minutes east of \b UTC and can
|
||||
range from \a -1440 to \a +1440.
|
||||
|
||||
- <tt>\b time < \a hour > < \a minute >
|
||||
{ \b 12hr | \b 24hr }</tt>\n \manonly \fR \endmanonly
|
||||
Set the time for clock 1 to <em>hour:minute</em> and configure it to display
|
||||
in \b 12hr or \b 24hr mode.
|
||||
|
||||
- <tt>\b date <\a dd > <\a mm > <\a yy>
|
||||
{ \b ddmmyy | \b mmddyy | \b yymmdd }</tt>\n \manonly \fR \endmanonly
|
||||
Set the date on the MFD to the values represented by \a dd, \a mm and \a yy in
|
||||
the requested format.
|
||||
|
||||
- <tt>\b raw < \a wIndex > < \a wValue ></tt>\n \manonly \fR \endmanonly
|
||||
Send a raw vendor control request to the connected joystick.\n
|
||||
\warning You should only use the raw command if you know what you are doing.
|
||||
Sending an invalid control sequence can potentially damage or destroy your
|
||||
device.
|
||||
|
||||
@section x52cli_leds LEDs
|
||||
This is the list of LED IDs and corresponding states supported by the X52 Pro.
|
||||
Note that the \b on state is only allowed for the \b fire and \b throttle LEDs,
|
||||
and they do not support the \b red, \b amber and \b green states. The remaining
|
||||
LEDs do not support the \b on state, but support all the other states.
|
||||
|
||||
\note The \b led command is only supported on the X52 Pro, not on the X52.
|
||||
|
||||
## LED IDs
|
||||
|
||||
- \b fire
|
||||
- \b a
|
||||
- \b b
|
||||
- \b d
|
||||
- \b e
|
||||
- \b t1
|
||||
- \b t2
|
||||
- \b t3
|
||||
- \b pov
|
||||
- \b clutch
|
||||
- \b throttle
|
||||
|
||||
## States
|
||||
|
||||
- \b off
|
||||
- \b on
|
||||
- \b red
|
||||
- \b amber
|
||||
- \b green
|
||||
|
||||
# LIMITATIONS
|
||||
|
||||
\b x52cli does not maintain any state between invocations. As a result the
|
||||
\b clock command will reset the relative offsets for clocks 2 and 3 back to 0
|
||||
and configure them to be in 12 hour mode. To work around this, use the \b date
|
||||
and \b time commands instead to manually configure the date and time.
|
||||
|
||||
\note The device does not have an internal clock; as a result, the MFD
|
||||
display will not advance automatically. You must call the \b clock or \b date
|
||||
and \b time commands periodically to update the time on the device. However, if
|
||||
you are running \b x52d and the clock manager is enabled, then \b x52d will
|
||||
manage and automatically update the clock on the X52 MFD display.
|
||||
|
||||
# PERMISSIONS
|
||||
|
||||
You must have write permissions to the USB device in order to use the \b libx52
|
||||
library, and by extension, \b x52cli.
|
||||
|
||||
The simplest method to obtain such permissions is to run \b x52cli as root,
|
||||
possibly through \b sudo(8)
|
||||
|
||||
@section x52cli_examples EXAMPLES
|
||||
|
||||
- Turn off the T1 LED.
|
||||
> <tt>\b x52cli led t1 off</tt>
|
||||
|
||||
- Turn the B LED to Amber.
|
||||
> <tt>\b x52cli led B amber</tt>
|
||||
|
||||
- Set line 1 of the MFD to display "Hello World"
|
||||
> <tt>\b x52cli mfd 0 "Hello World"</tt>
|
||||
|
||||
- Set line 2 of the MFD to display "¿Cómo Estás?"
|
||||
> <tt>\b x52cli mfd 1 "$(printf '\\x9FC\\xE2mo Est\\xE0s?')"</tt>
|
||||
|
||||
*/
|
||||
|
||||
#define _GNU_SOURCE
|
||||
#include "build-config.h"
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <time.h>
|
||||
|
||||
#include <libx52/libx52.h>
|
||||
|
||||
struct string_map {
|
||||
const char *key;
|
||||
union {
|
||||
libx52_clock_id clock_id;
|
||||
libx52_clock_format clock_format;
|
||||
libx52_date_format date_format;
|
||||
libx52_led_id led_id;
|
||||
libx52_led_state led_state;
|
||||
int int_val;
|
||||
intptr_t ptr_val;
|
||||
} value;
|
||||
};
|
||||
|
||||
// Max 4 arguments for now
|
||||
#define MAX_ARGS 4
|
||||
|
||||
typedef int (*handler_cb)(libx52_device *x52, void *args[]);
|
||||
struct command_handler {
|
||||
handler_cb handler;
|
||||
int num_args;
|
||||
const struct string_map *maps[MAX_ARGS];
|
||||
const char *help;
|
||||
};
|
||||
|
||||
#define SAVE_ARGUMENT(type, name, arg) type name = (type)(uintptr_t)(arg)
|
||||
#define PARSE_ARGUMENT(type, arg) ((type)(uintptr_t)(arg))
|
||||
|
||||
#define MAP(name) name##_map
|
||||
#define DEFINE_MAP(name) static const struct string_map MAP(name)[]
|
||||
#define MAP_CLOCK_ID(id) {.key = #id, .value.clock_id = LIBX52_CLOCK_ ## id}
|
||||
#define MAP_CLOCK_FORMAT(fmt) {.key = #fmt, .value.clock_format = LIBX52_CLOCK_FORMAT_ ## fmt}
|
||||
#define MAP_DATE_FORMAT(fmt) {.key = #fmt, .value.date_format = LIBX52_DATE_FORMAT_ ## fmt}
|
||||
#define MAP_LED_ID(id) {.key = #id, .value.led_id = LIBX52_LED_ ## id}
|
||||
#define MAP_LED_STATE(state) {.key = #state, .value.led_state = LIBX52_LED_STATE_ ## state}
|
||||
#define MAP_INT(str, val) {.key = str, .value.int_val = val}
|
||||
#define MAP_TERMINATOR MAP_INT(NULL, -1)
|
||||
|
||||
/**
|
||||
* Parse a string and match it with a corresponding value
|
||||
* Maps are arrays which contain a terminating entry with a NULL key.
|
||||
*
|
||||
* Return 0 if no match
|
||||
*/
|
||||
static int map_lookup(const struct string_map *map, const char *str, struct string_map *result)
|
||||
{
|
||||
const struct string_map *map_index = map;
|
||||
|
||||
/* Search through the map for a matching key */
|
||||
while (map_index->key) {
|
||||
if (!strcasecmp(str, map_index->key)) {
|
||||
result->value = map_index->value;
|
||||
return 1;
|
||||
}
|
||||
|
||||
map_index++;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Map for LED state */
|
||||
DEFINE_MAP(led_state) = {
|
||||
MAP_LED_STATE(OFF),
|
||||
MAP_LED_STATE(ON),
|
||||
MAP_LED_STATE(RED),
|
||||
MAP_LED_STATE(AMBER),
|
||||
MAP_LED_STATE(GREEN),
|
||||
MAP_TERMINATOR
|
||||
};
|
||||
|
||||
/* Map for LED identifier */
|
||||
DEFINE_MAP(led_id) = {
|
||||
MAP_LED_ID(FIRE),
|
||||
MAP_LED_ID(A),
|
||||
MAP_LED_ID(B),
|
||||
MAP_LED_ID(D),
|
||||
MAP_LED_ID(E),
|
||||
MAP_LED_ID(T1),
|
||||
MAP_LED_ID(T2),
|
||||
MAP_LED_ID(T3),
|
||||
MAP_LED_ID(POV),
|
||||
MAP_LED_ID(CLUTCH),
|
||||
MAP_LED_ID(THROTTLE),
|
||||
MAP_TERMINATOR
|
||||
};
|
||||
|
||||
/* Map for date format */
|
||||
DEFINE_MAP(date_format) = {
|
||||
MAP_DATE_FORMAT(DDMMYY),
|
||||
MAP_DATE_FORMAT(MMDDYY),
|
||||
MAP_DATE_FORMAT(YYMMDD),
|
||||
MAP_TERMINATOR
|
||||
};
|
||||
|
||||
/* Map for brightness setting */
|
||||
DEFINE_MAP(brightness_targets) = {
|
||||
MAP_INT( "mfd", 1 ),
|
||||
MAP_INT( "led", 0 ),
|
||||
MAP_TERMINATOR
|
||||
};
|
||||
|
||||
/* Map for blink/shift on/off */
|
||||
DEFINE_MAP(on_off) = {
|
||||
MAP_INT( "off", 0 ),
|
||||
MAP_INT( "on", 1 ),
|
||||
MAP_TERMINATOR
|
||||
};
|
||||
|
||||
/* Map for clock 0 timezone */
|
||||
DEFINE_MAP(clock0_timezone) = {
|
||||
MAP_INT( "gmt", 0 ),
|
||||
MAP_INT( "local", 1 ),
|
||||
MAP_TERMINATOR
|
||||
};
|
||||
|
||||
/* Map for identifying the clock for the timezone */
|
||||
DEFINE_MAP(clocks) = {
|
||||
MAP_CLOCK_ID(1),
|
||||
MAP_CLOCK_ID(2),
|
||||
MAP_CLOCK_ID(3),
|
||||
MAP_TERMINATOR
|
||||
};
|
||||
|
||||
/* Map for identifying the time format */
|
||||
DEFINE_MAP(time_format) = {
|
||||
MAP_CLOCK_FORMAT(12HR),
|
||||
MAP_CLOCK_FORMAT(24HR),
|
||||
MAP_TERMINATOR
|
||||
};
|
||||
|
||||
static int update_led(libx52_device *x52, void *args[])
|
||||
{
|
||||
return libx52_set_led_state(x52,
|
||||
PARSE_ARGUMENT(libx52_led_id, args[0]),
|
||||
PARSE_ARGUMENT(libx52_led_state, args[1]));
|
||||
}
|
||||
|
||||
static int update_bri(libx52_device *x52, void *args[])
|
||||
{
|
||||
unsigned long int brightness = strtoul(args[1], NULL, 0);
|
||||
|
||||
return libx52_set_brightness(x52,
|
||||
PARSE_ARGUMENT(uint8_t, args[0]), (uint16_t)brightness);
|
||||
}
|
||||
|
||||
static int update_mfd(libx52_device *x52, void *args[])
|
||||
{
|
||||
uint8_t line = (uint8_t)strtoul(args[0], NULL, 0);
|
||||
uint8_t length = strlen(args[1]);
|
||||
|
||||
return libx52_set_text(x52, line, args[1], length);
|
||||
}
|
||||
|
||||
static int update_blink(libx52_device *x52, void *args[])
|
||||
{
|
||||
return libx52_set_blink(x52, PARSE_ARGUMENT(int, args[0]));
|
||||
}
|
||||
|
||||
static int update_shift(libx52_device *x52, void *args[])
|
||||
{
|
||||
return libx52_set_shift(x52, PARSE_ARGUMENT(int, args[0]));
|
||||
}
|
||||
|
||||
static int update_clock(libx52_device *x52, void *args[])
|
||||
{
|
||||
int rc;
|
||||
rc = libx52_set_clock(x52, time(NULL),
|
||||
PARSE_ARGUMENT(int, args[0]));
|
||||
|
||||
if (!rc) {
|
||||
rc = libx52_set_clock_format(x52, LIBX52_CLOCK_1,
|
||||
PARSE_ARGUMENT(libx52_clock_format, args[1]));
|
||||
}
|
||||
|
||||
if (!rc) {
|
||||
rc = libx52_set_date_format(x52,
|
||||
PARSE_ARGUMENT(libx52_date_format, args[2]));
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int update_offset(libx52_device *x52, void *args[])
|
||||
{
|
||||
int offset = (int)strtol(args[1], NULL, 0);
|
||||
int rc;
|
||||
SAVE_ARGUMENT(libx52_clock_id, clock, args[0]);
|
||||
|
||||
rc = libx52_set_clock_timezone(x52, clock, offset);
|
||||
|
||||
if (!rc) {
|
||||
rc = libx52_set_clock_format(x52, clock,
|
||||
PARSE_ARGUMENT(libx52_clock_format, args[2]));
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int update_time(libx52_device *x52, void *args[])
|
||||
{
|
||||
int hh = (int)strtol(args[0], NULL, 0);
|
||||
int mm = (int)strtol(args[1], NULL, 0);
|
||||
int rc;
|
||||
|
||||
/* Set the time value */
|
||||
rc = libx52_set_time(x52, hh, mm);
|
||||
if (!rc) {
|
||||
rc = libx52_set_clock_format(x52, LIBX52_CLOCK_1,
|
||||
PARSE_ARGUMENT(libx52_clock_format, args[2]));
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int update_date(libx52_device *x52, void *args[])
|
||||
{
|
||||
int dd = (int)strtol(args[0], NULL, 0);
|
||||
int mm = (int)strtol(args[1], NULL, 0);
|
||||
int yy = (int)strtol(args[2], NULL, 0);
|
||||
int rc;
|
||||
|
||||
/* Set the date value */
|
||||
rc = libx52_set_date(x52, dd, mm, yy);
|
||||
if (!rc) {
|
||||
rc = libx52_set_date_format(x52,
|
||||
PARSE_ARGUMENT(libx52_date_format, args[3]));
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int update_raw(libx52_device *x52, void *args[])
|
||||
{
|
||||
uint16_t wIndex = (uint16_t)strtoul(args[0], NULL, 0);
|
||||
uint16_t wValue = (uint16_t)strtoul(args[1], NULL, 0);
|
||||
|
||||
return libx52_vendor_command(x52, wIndex, wValue);
|
||||
}
|
||||
|
||||
/* Commands for CLI */
|
||||
#define COMMANDS \
|
||||
X(led, LED_STATE, "<led-id> <state>", 2, \
|
||||
MAP(led_id), MAP(led_state)) \
|
||||
X(bri, BRIGHTNESS, "{mfd | led} <brightness level>", 2, \
|
||||
MAP(brightness_targets), NULL) \
|
||||
X(mfd, MFD_TEXT, "<line> <text in quotes>", 2, \
|
||||
NULL, NULL) \
|
||||
X(blink, BLINK, "{ on | off }", 1, \
|
||||
MAP(on_off)) \
|
||||
X(shift, SHIFT, "{ on | off }", 1, \
|
||||
MAP(on_off)) \
|
||||
X(clock, CLOCK, \
|
||||
"{local | gmt} {12hr | 24hr} {ddmmyy | mmddyy | yymmdd}", \
|
||||
3, MAP(clock0_timezone), MAP(time_format), MAP(date_format)) \
|
||||
X(offset, OFFSET, \
|
||||
"{2 | 3} <offset from clock 1 in minutes> {12hr | 24hr}", \
|
||||
3, MAP(clocks), NULL, MAP(time_format)) \
|
||||
X(time, TIME, \
|
||||
"<hour> <minute> {12hr | 24hr}", 3, \
|
||||
NULL, NULL, MAP(time_format)) \
|
||||
X(date, DATE, \
|
||||
"<dd> <mm> <yy> {ddmmyy | mmddyy | yymmdd}", 4, \
|
||||
NULL, NULL, NULL, MAP(date_format)) \
|
||||
X(raw, RAW, "<wIndex> <wValue>", 2, NULL, NULL)
|
||||
|
||||
/* Enums for command identification */
|
||||
#define X(cmd, en, help, args, ...) X52_CTL_CMD_ ## en,
|
||||
enum {
|
||||
COMMANDS
|
||||
|
||||
X52_CTL_CMD_MAX
|
||||
};
|
||||
#undef X
|
||||
|
||||
/* Map for commands */
|
||||
#define X(cmd, en, help, args, ...) MAP_INT( #cmd, X52_CTL_CMD_ ## en),
|
||||
DEFINE_MAP(command) = {
|
||||
COMMANDS
|
||||
|
||||
MAP_TERMINATOR
|
||||
};
|
||||
#undef X
|
||||
|
||||
/* Command handlers */
|
||||
#define X(cmd, en, help, args, ...) \
|
||||
[X52_CTL_CMD_ ## en] = { \
|
||||
update_ ## cmd, \
|
||||
args, \
|
||||
{ __VA_ARGS__ }, \
|
||||
#cmd " " help \
|
||||
},
|
||||
const struct command_handler handlers[X52_CTL_CMD_MAX] = {
|
||||
COMMANDS
|
||||
};
|
||||
#undef X
|
||||
|
||||
static void do_help(const struct command_handler *cmd)
|
||||
{
|
||||
int i;
|
||||
if (cmd) {
|
||||
fprintf(stderr, "Command usage: %s\n", cmd->help);
|
||||
} else {
|
||||
fprintf(stderr, "\nCommands:\n");
|
||||
for (i = 0; i < X52_CTL_CMD_MAX; i++) {
|
||||
fprintf(stderr, "\t%s\n", handlers[i].help);
|
||||
}
|
||||
|
||||
fprintf(stderr, "\nWARNING: raw command may damage your device\n\n");
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef X52_CLI_TESTING
|
||||
int run_main(int argc, char **argv)
|
||||
#else
|
||||
int main(int argc, char **argv)
|
||||
#endif
|
||||
{
|
||||
libx52_device *x52;
|
||||
struct string_map result;
|
||||
const struct command_handler *cmd;
|
||||
int i;
|
||||
void *args[MAX_ARGS];
|
||||
int rc;
|
||||
|
||||
if (argc < 2) {
|
||||
fprintf(stderr, "Usage: %s <command> [arguments]\n", argv[0]);
|
||||
do_help(NULL);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!map_lookup(command_map, argv[1], &result)) {
|
||||
fprintf(stderr, "Unsupported command %s\n", argv[1]);
|
||||
do_help(NULL);
|
||||
return 1;
|
||||
}
|
||||
|
||||
cmd = &handlers[result.value.int_val];
|
||||
if (cmd->num_args > argc - 2) {
|
||||
fprintf(stderr, "Insufficient arguments for command %s\n", argv[1]);
|
||||
do_help(cmd);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Clear the arguments array */
|
||||
memset(args, 0, sizeof(args));
|
||||
|
||||
for (i = 0; i < cmd->num_args; i++) {
|
||||
if (cmd->maps[i]) {
|
||||
if (!map_lookup(cmd->maps[i], argv[2+i], &result)) {
|
||||
fprintf(stderr, "Invalid argument %s\n", argv[2+i]);
|
||||
return 1;
|
||||
}
|
||||
args[i] = (void *)result.value.ptr_val;
|
||||
} else {
|
||||
args[i] = argv[2+i];
|
||||
}
|
||||
}
|
||||
|
||||
/* Initialize libx52 */
|
||||
rc = libx52_init(&x52);
|
||||
|
||||
if (rc != LIBX52_SUCCESS) {
|
||||
fprintf(stderr, "Error initializing X52 library: %s\n", libx52_strerror(rc));
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Make sure we are connected to the joystick */
|
||||
rc = libx52_connect(x52);
|
||||
if (rc != LIBX52_SUCCESS) {
|
||||
fprintf(stderr, "Error connecting to joystick: %s\n", libx52_strerror(rc));
|
||||
return 1;
|
||||
}
|
||||
|
||||
rc = (*(cmd->handler))(x52, args);
|
||||
if (rc != LIBX52_SUCCESS) {
|
||||
fprintf(stderr, "Error: %s\n", libx52_strerror(rc));
|
||||
rc = 1;
|
||||
}
|
||||
|
||||
libx52_update(x52);
|
||||
|
||||
libx52_exit(x52);
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - Client handling
|
||||
*
|
||||
* Copyright (C) 2022 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include <errno.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "pinelog.h"
|
||||
#include <daemon/client.h>
|
||||
#include <daemon/x52dcomm-internal.h>
|
||||
|
||||
void x52d_client_init(int client_fd[X52D_MAX_CLIENTS])
|
||||
{
|
||||
for (int i = 0; i < X52D_MAX_CLIENTS; i++) {
|
||||
client_fd[i] = INVALID_CLIENT;
|
||||
}
|
||||
}
|
||||
|
||||
bool x52d_client_register(int client_fd[X52D_MAX_CLIENTS], int sock_fd)
|
||||
{
|
||||
int fd;
|
||||
int i;
|
||||
|
||||
#if defined(__GNUC__) && !defined(__clang__) && __GNUC__ >= 13
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wanalyzer-fd-leak"
|
||||
#endif
|
||||
|
||||
fd = accept(sock_fd, NULL, NULL);
|
||||
if (fd < 0) {
|
||||
PINELOG_ERROR(_("Error accepting client connection on socket fd %d: %s"),
|
||||
sock_fd, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (x52d_set_socket_nonblocking(fd) < 0) {
|
||||
PINELOG_ERROR(_("Error marking client fd %d as nonblocking: %s"),
|
||||
fd, strerror(errno));
|
||||
goto error;
|
||||
}
|
||||
|
||||
#if defined(__GNUC__) && !defined(__clang__) && __GNUC__ >= 13
|
||||
#pragma GCC diagnostic pop
|
||||
#endif
|
||||
|
||||
|
||||
for (i = 0; i < X52D_MAX_CLIENTS; i++) {
|
||||
if (client_fd[i] == INVALID_CLIENT) {
|
||||
PINELOG_TRACE("Accepted client %d on socket %d, slot %d", fd, sock_fd, i);
|
||||
client_fd[i] = fd;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* At this point, we've looped through the entirity of client_fd, but
|
||||
* have not registered an empty slot. We need to close the socket and
|
||||
* tell the caller that we haven't been able to register the client.
|
||||
*/
|
||||
PINELOG_TRACE("Maximum connections reached, closing socket %d", fd);
|
||||
error:
|
||||
close(fd);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool x52d_client_deregister(int client_fd[X52D_MAX_CLIENTS], int fd)
|
||||
{
|
||||
bool deregistered = false;
|
||||
|
||||
for (int i = 0; i < X52D_MAX_CLIENTS; i++) {
|
||||
if (client_fd[i] == fd) {
|
||||
client_fd[i] = INVALID_CLIENT;
|
||||
deregistered = true;
|
||||
close(fd);
|
||||
|
||||
PINELOG_TRACE("Disconnected client %d from socket", fd);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return deregistered;
|
||||
}
|
||||
|
||||
bool x52d_client_error(int client_fd[X52D_MAX_CLIENTS], int fd)
|
||||
{
|
||||
int error;
|
||||
socklen_t errlen = sizeof(error);
|
||||
|
||||
getsockopt(fd, SOL_SOCKET, SO_ERROR, (void *)&error, &errlen);
|
||||
PINELOG_ERROR(_("Error when polling socket: FD %d, error %d, len %lu"),
|
||||
fd, error, (unsigned long int)errlen);
|
||||
return x52d_client_deregister(client_fd, fd);
|
||||
}
|
||||
|
||||
int x52d_client_poll(int client_fd[X52D_MAX_CLIENTS], struct pollfd pfd[MAX_CONN], int listen_fd)
|
||||
{
|
||||
int pfd_count;
|
||||
int rc;
|
||||
|
||||
memset(pfd, 0, sizeof(*pfd) * MAX_CONN);
|
||||
|
||||
pfd_count = 1;
|
||||
pfd[0].fd = listen_fd;
|
||||
pfd[0].events = POLLIN | POLLERR;
|
||||
for (int i = 0; i < X52D_MAX_CLIENTS; i++) {
|
||||
if (client_fd[i] != INVALID_CLIENT) {
|
||||
pfd[pfd_count].fd = client_fd[i];
|
||||
pfd[pfd_count].events = POLLIN | POLLERR | POLLHUP;
|
||||
pfd_count++;
|
||||
}
|
||||
}
|
||||
|
||||
PINELOG_TRACE("Polling %d file descriptors", pfd_count);
|
||||
|
||||
do {
|
||||
rc = poll(pfd, pfd_count, -1);
|
||||
} while (rc < 0 && errno == EINTR);
|
||||
|
||||
if (rc < 0) {
|
||||
PINELOG_ERROR(_("Error %d when polling %d descriptors: %s"),
|
||||
errno, pfd_count, strerror(errno));
|
||||
} else if (rc == 0) {
|
||||
PINELOG_INFO(_("Timed out when polling"));
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
void x52d_client_handle(int client_fd[X52D_MAX_CLIENTS], struct pollfd *pfd, int listen_fd, x52d_poll_handler handler)
|
||||
{
|
||||
for (int i = 0; i < MAX_CONN; i++) {
|
||||
if (pfd[i].revents & POLLHUP) {
|
||||
/* Remote hungup */
|
||||
x52d_client_deregister(client_fd, pfd[i].fd);
|
||||
} else if (pfd[i].revents & POLLERR) {
|
||||
/* Error reading from the socket */
|
||||
x52d_client_error(client_fd, pfd[i].fd);
|
||||
} else if (pfd[i].revents & POLLIN) {
|
||||
if (pfd[i].fd == listen_fd) {
|
||||
x52d_client_register(client_fd, listen_fd);
|
||||
} else {
|
||||
if (handler != NULL) {
|
||||
handler(pfd[i].fd);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - Client handling
|
||||
*
|
||||
* Copyright (C) 2022 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#ifndef X52D_CLIENT_H
|
||||
#define X52D_CLIENT_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <poll.h>
|
||||
|
||||
#include <daemon/constants.h>
|
||||
|
||||
#define MAX_CONN (X52D_MAX_CLIENTS + 1)
|
||||
|
||||
#define INVALID_CLIENT -1
|
||||
|
||||
typedef void (*x52d_poll_handler)(int);
|
||||
|
||||
void x52d_client_init(int client_fd[X52D_MAX_CLIENTS]);
|
||||
bool x52d_client_register(int client_fd[X52D_MAX_CLIENTS], int sock_fd);
|
||||
bool x52d_client_deregister(int client_fd[X52D_MAX_CLIENTS], int fd);
|
||||
bool x52d_client_error(int client_fd[X52D_MAX_CLIENTS], int fd);
|
||||
int x52d_client_poll(int client_fd[X52D_MAX_CLIENTS], struct pollfd pfd[MAX_CONN], int listen_fd);
|
||||
void x52d_client_handle(int client_fd[X52D_MAX_CLIENTS], struct pollfd *pfd, int listen_fd, x52d_poll_handler handler);
|
||||
|
||||
#endif //!defined X52D_CLIENT_H
|
||||
|
|
@ -0,0 +1,215 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - Clock manager
|
||||
*
|
||||
* Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "build-config.h"
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <pthread.h>
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
|
||||
#define PINELOG_MODULE X52D_MOD_CLOCK
|
||||
#include "pinelog.h"
|
||||
#include <daemon/config.h>
|
||||
#include <daemon/clock.h>
|
||||
#include <daemon/constants.h>
|
||||
#include <daemon/device.h>
|
||||
|
||||
static bool clock_enabled = false;
|
||||
static int clock_primary_is_local = false;
|
||||
|
||||
void x52d_cfg_set_Clock_Enabled(bool enabled)
|
||||
{
|
||||
PINELOG_DEBUG(_("Setting clock enable to %s"),
|
||||
enabled ? _("on") : _("off"));
|
||||
clock_enabled = enabled;
|
||||
}
|
||||
|
||||
void x52d_cfg_set_Clock_PrimaryIsLocal(bool param)
|
||||
{
|
||||
PINELOG_DEBUG(_("Setting %s clock timezone to %s"),
|
||||
libx52_clock_id_to_str(LIBX52_CLOCK_1),
|
||||
param ? _("local") : _("UTC"));
|
||||
clock_primary_is_local = !!param;
|
||||
}
|
||||
|
||||
static int get_tz_offset(const char *tz)
|
||||
{
|
||||
char *orig_tz = NULL;
|
||||
char *orig_tz_copy = NULL;
|
||||
time_t t;
|
||||
struct tm tmp;
|
||||
struct tm *timeval;
|
||||
char *new_tz = NULL;
|
||||
size_t new_tz_len;
|
||||
int offset = 0;
|
||||
|
||||
new_tz_len = strlen(tz) + 2;
|
||||
new_tz = malloc(new_tz_len);
|
||||
if (new_tz == NULL) {
|
||||
PINELOG_WARN(_("Unable to allocate memory for timezone. Falling back to UTC"));
|
||||
goto cleanup;
|
||||
}
|
||||
snprintf(new_tz, new_tz_len, ":%s", tz);
|
||||
|
||||
orig_tz = getenv("TZ");
|
||||
if (orig_tz != NULL) {
|
||||
/* TZ was set in the environment */
|
||||
orig_tz_copy = strdup(orig_tz);
|
||||
if (orig_tz_copy == NULL) {
|
||||
PINELOG_WARN(_("Unable to backup timezone environment. Falling back to UTC"));
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
|
||||
setenv("TZ", new_tz, true);
|
||||
t = time(NULL);
|
||||
tzset();
|
||||
timeval = localtime_r(&t, &tmp);
|
||||
if (timeval != NULL) {
|
||||
#if HAVE_STRUCT_TM_TM_GMTOFF
|
||||
/* If valid, then timeval.tm_gmtoff contains the offset in seconds east
|
||||
* of GMT. Divide by 60 to get the offset in minutes east of GMT.
|
||||
*/
|
||||
offset = (int)(timeval->tm_gmtoff / 60);
|
||||
#else
|
||||
/* The compiler does not provide tm_gmtoff. Fallback to using the
|
||||
* timezone variable, which is in seconds west of GMT. Divide by -60 to
|
||||
* get the offset in minutes east of GMT.
|
||||
*
|
||||
* ============
|
||||
* XXX NOTE XXX
|
||||
* ============
|
||||
* timezone is always the default (non-summer) timezone offset from GMT.
|
||||
* Therefore, this may not be accurate during the summer time months
|
||||
* for the region in question.
|
||||
*/
|
||||
offset = (int)(timezone / -60);
|
||||
#endif
|
||||
}
|
||||
|
||||
cleanup:
|
||||
if (orig_tz == NULL) {
|
||||
unsetenv("TZ");
|
||||
} else {
|
||||
// If the copy is NULL, then we didn't change TZ, so don't bother
|
||||
if (orig_tz_copy != NULL) {
|
||||
setenv("TZ", orig_tz_copy, true);
|
||||
free(orig_tz_copy);
|
||||
}
|
||||
}
|
||||
|
||||
if (new_tz != NULL) {
|
||||
free(new_tz);
|
||||
}
|
||||
|
||||
tzset();
|
||||
PINELOG_TRACE("Offset for timezone '%s' is %d", tz, offset);
|
||||
return offset;
|
||||
}
|
||||
|
||||
static void set_clock_offset(libx52_clock_id id, const char *param)
|
||||
{
|
||||
if (clock_enabled) {
|
||||
PINELOG_DEBUG(_("Setting %s clock timezone to %s"),
|
||||
libx52_clock_id_to_str(id), param);
|
||||
x52d_dev_set_clock_timezone(id, get_tz_offset(param));
|
||||
}
|
||||
}
|
||||
|
||||
void x52d_cfg_set_Clock_Secondary(char* param)
|
||||
{
|
||||
set_clock_offset(LIBX52_CLOCK_2, param);
|
||||
}
|
||||
|
||||
void x52d_cfg_set_Clock_Tertiary(char* param)
|
||||
{
|
||||
set_clock_offset(LIBX52_CLOCK_3, param);
|
||||
}
|
||||
|
||||
static void set_clock_format(libx52_clock_id id, libx52_clock_format fmt)
|
||||
{
|
||||
if (clock_enabled) {
|
||||
PINELOG_DEBUG(_("Setting %s clock format to %s"),
|
||||
libx52_clock_id_to_str(id), libx52_clock_format_to_str(fmt));
|
||||
x52d_dev_set_clock_format(id, fmt);
|
||||
}
|
||||
}
|
||||
|
||||
void x52d_cfg_set_Clock_FormatPrimary(libx52_clock_format fmt)
|
||||
{
|
||||
set_clock_format(LIBX52_CLOCK_1, fmt);
|
||||
}
|
||||
|
||||
void x52d_cfg_set_Clock_FormatSecondary(libx52_clock_format fmt)
|
||||
{
|
||||
set_clock_format(LIBX52_CLOCK_2, fmt);
|
||||
}
|
||||
|
||||
void x52d_cfg_set_Clock_FormatTertiary(libx52_clock_format fmt)
|
||||
{
|
||||
set_clock_format(LIBX52_CLOCK_3, fmt);
|
||||
}
|
||||
|
||||
void x52d_cfg_set_Clock_DateFormat(libx52_date_format fmt)
|
||||
{
|
||||
if (clock_enabled) {
|
||||
PINELOG_DEBUG(_("Setting date format to %s"), libx52_date_format_to_str(fmt));
|
||||
x52d_dev_set_date_format(fmt);
|
||||
}
|
||||
}
|
||||
|
||||
static pthread_t clock_thr;
|
||||
|
||||
static void * x52_clock_thr(void *param)
|
||||
{
|
||||
int rc;
|
||||
(void)param;
|
||||
|
||||
PINELOG_INFO(_("Starting X52 clock manager thread"));
|
||||
for (;;) {
|
||||
time_t cur_time;
|
||||
|
||||
sleep(1);
|
||||
if (!clock_enabled) {
|
||||
/* Clock thread is disabled, check again next time */
|
||||
continue;
|
||||
}
|
||||
|
||||
if (time(&cur_time) < 0) {
|
||||
PINELOG_WARN(_("Error %d retrieving current time: %s"),
|
||||
errno, strerror(errno));
|
||||
continue;
|
||||
}
|
||||
rc = x52d_dev_set_clock(cur_time, clock_primary_is_local);
|
||||
if (rc == LIBX52_SUCCESS) {
|
||||
// Device manager will update the clock, this is only for debugging
|
||||
PINELOG_TRACE("Setting X52 clock to %ld", cur_time);
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void x52d_clock_init(void)
|
||||
{
|
||||
int rc;
|
||||
|
||||
PINELOG_TRACE("Initializing clock manager");
|
||||
rc = pthread_create(&clock_thr, NULL, x52_clock_thr, NULL);
|
||||
if (rc != 0) {
|
||||
PINELOG_FATAL(_("Error %d initializing clock thread: %s"),
|
||||
rc, strerror(rc));
|
||||
}
|
||||
}
|
||||
|
||||
void x52d_clock_exit(void)
|
||||
{
|
||||
PINELOG_INFO(_("Shutting down X52 clock manager thread"));
|
||||
pthread_cancel(clock_thr);
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - Clock manager
|
||||
*
|
||||
* Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#ifndef X52D_CLOCK_H
|
||||
#define X52D_CLOCK_H
|
||||
|
||||
void x52d_clock_init(void);
|
||||
void x52d_clock_exit(void);
|
||||
|
||||
#endif // !defined X52D_CLOCK_H
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - Client communication library
|
||||
*
|
||||
* Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "build-config.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <libx52/x52dcomm.h>
|
||||
#include <daemon/x52dcomm-internal.h>
|
||||
|
||||
static int _setup_socket(struct sockaddr_un *remote, int len)
|
||||
{
|
||||
int sock;
|
||||
int saved_errno;
|
||||
|
||||
/* Create a socket */
|
||||
sock = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (sock == -1) {
|
||||
/* Failure creating the socket, abort early */
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Connect to the socket */
|
||||
if (connect(sock, (struct sockaddr *)remote, (socklen_t)len) == -1) {
|
||||
/* Failure connecting to the socket. Cleanup */
|
||||
saved_errno = errno;
|
||||
/* close may modify errno, so we save it prior to the call */
|
||||
close(sock);
|
||||
sock = -1;
|
||||
errno = saved_errno;
|
||||
}
|
||||
|
||||
return sock;
|
||||
}
|
||||
|
||||
int x52d_dial_command(const char *sock_path)
|
||||
{
|
||||
int len;
|
||||
struct sockaddr_un remote;
|
||||
|
||||
len = x52d_setup_command_sock(sock_path, &remote);
|
||||
if (len < 0) {
|
||||
/* Error when setting up sockaddr */
|
||||
return -1;
|
||||
}
|
||||
|
||||
return _setup_socket(&remote, len);
|
||||
}
|
||||
|
||||
int x52d_dial_notify(const char *sock_path)
|
||||
{
|
||||
int len;
|
||||
struct sockaddr_un remote;
|
||||
|
||||
len = x52d_setup_notify_sock(sock_path, &remote);
|
||||
if (len < 0) {
|
||||
/* Error when setting up sockaddr */
|
||||
return -1;
|
||||
}
|
||||
|
||||
return _setup_socket(&remote, len);
|
||||
}
|
||||
|
||||
int x52d_format_command(int argc, const char **argv, char *buffer, size_t buflen)
|
||||
{
|
||||
int msglen;
|
||||
int i;
|
||||
|
||||
if (argc == 0 || argv == NULL || buffer == NULL || buflen < X52D_BUFSZ) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
memset(buffer, 0, buflen);
|
||||
msglen = 0;
|
||||
for (i = 0; i < argc; i++) {
|
||||
int arglen = strlen(argv[i]) + 1;
|
||||
if ((size_t)(msglen + arglen) >= buflen) {
|
||||
errno = E2BIG;
|
||||
return -1;
|
||||
}
|
||||
|
||||
memcpy(&buffer[msglen], argv[i], arglen);
|
||||
msglen += arglen;
|
||||
}
|
||||
|
||||
return msglen;
|
||||
}
|
||||
|
||||
int x52d_send_command(int sock_fd, char *buffer, size_t bufin, size_t bufout)
|
||||
{
|
||||
int rc;
|
||||
|
||||
for (;;) {
|
||||
/*
|
||||
* Unix sockets should have sufficient capacity to send the full
|
||||
* datagram in a single message. Assume that is the case.
|
||||
*/
|
||||
rc = send(sock_fd, buffer, bufin, 0);
|
||||
if (rc < 0) {
|
||||
// Error
|
||||
if (errno == EINTR) {
|
||||
// System call interrupted due to signal. Try again
|
||||
continue;
|
||||
} else {
|
||||
// Failed. Return early
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
/* Wait till we get a response */
|
||||
for (;;) {
|
||||
rc = recv(sock_fd, buffer, bufout, 0);
|
||||
if (rc < 0) {
|
||||
// Error
|
||||
if (errno == EINTR) {
|
||||
// System call interrupted due to signal. Try again
|
||||
continue;
|
||||
} else {
|
||||
// Failed. Return early
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
int x52d_recv_notification(int sock_fd, x52d_notify_callback_fn callback)
|
||||
{
|
||||
int rc;
|
||||
char buffer[X52D_BUFSZ];
|
||||
int argc;
|
||||
char *argv[X52D_BUFSZ];
|
||||
|
||||
if (callback == NULL) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Wait till we get a response */
|
||||
for (;;) {
|
||||
rc = recv(sock_fd, buffer, sizeof(buffer), 0);
|
||||
if (rc < 0) {
|
||||
// Error
|
||||
if (errno == EINTR) {
|
||||
// System call interrupted due to signal. Try again
|
||||
continue;
|
||||
} else {
|
||||
// Failed. Return early
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
/* Split into individual arguments */
|
||||
x52d_split_args(&argc, argv, buffer, rc);
|
||||
|
||||
return callback(argc, argv);
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - Client communication library
|
||||
*
|
||||
* Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "build-config.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <daemon/x52dcomm-internal.h>
|
||||
#include <daemon/constants.h>
|
||||
|
||||
const char * x52d_command_sock_path(const char *sock_path)
|
||||
{
|
||||
if (sock_path == NULL) {
|
||||
sock_path = X52D_SOCK_COMMAND;
|
||||
}
|
||||
|
||||
return sock_path;
|
||||
}
|
||||
|
||||
const char * x52d_notify_sock_path(const char *sock_path)
|
||||
{
|
||||
if (sock_path == NULL) {
|
||||
sock_path = X52D_SOCK_NOTIFY;
|
||||
}
|
||||
|
||||
return sock_path;
|
||||
}
|
||||
|
||||
static int _setup_sockaddr(struct sockaddr_un *remote, const char *sock_path)
|
||||
{
|
||||
int len;
|
||||
if (remote == NULL) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
len = strlen(sock_path);
|
||||
if ((size_t)len >= sizeof(remote->sun_path)) {
|
||||
/* Socket path will not fit inside sun_path */
|
||||
errno = E2BIG;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Setup the sockaddr structure */
|
||||
memset(remote, 0, sizeof(*remote));
|
||||
remote->sun_family = AF_UNIX;
|
||||
/* We've already verified that sock_path will fit, so we don't need strncpy */
|
||||
strcpy(remote->sun_path, sock_path);
|
||||
len += sizeof(*remote) - sizeof(remote->sun_path);
|
||||
|
||||
return len;
|
||||
}
|
||||
|
||||
int x52d_setup_command_sock(const char *sock_path, struct sockaddr_un *remote)
|
||||
{
|
||||
return _setup_sockaddr(remote, x52d_command_sock_path(sock_path));
|
||||
}
|
||||
|
||||
int x52d_setup_notify_sock(const char *sock_path, struct sockaddr_un *remote)
|
||||
{
|
||||
return _setup_sockaddr(remote, x52d_notify_sock_path(sock_path));
|
||||
}
|
||||
|
||||
int x52d_set_socket_nonblocking(int sock_fd)
|
||||
{
|
||||
int flags;
|
||||
|
||||
/* Mark the socket as non-blocking */
|
||||
flags = fcntl(sock_fd, F_GETFL);
|
||||
if (flags < 0) {
|
||||
goto sock_failure;
|
||||
}
|
||||
if (fcntl(sock_fd, F_SETFL, flags | O_NONBLOCK) < 0) {
|
||||
goto sock_failure;
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
sock_failure:
|
||||
close(sock_fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int x52d_listen_socket(struct sockaddr_un *local, int len, int sock_fd)
|
||||
{
|
||||
/* Cleanup any existing socket */
|
||||
unlink(local->sun_path);
|
||||
if (bind(sock_fd, (struct sockaddr *)local, (socklen_t)len) < 0) {
|
||||
/* Failure binding socket */
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (listen(sock_fd, X52D_MAX_CLIENTS) < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void x52d_split_args(int *argc, char **argv, char *buffer, int buflen)
|
||||
{
|
||||
int i = 0;
|
||||
|
||||
while (i < buflen) {
|
||||
if (buffer[i]) {
|
||||
argv[*argc] = buffer + i;
|
||||
(*argc)++;
|
||||
for (; i < buflen && buffer[i]; i++);
|
||||
// At this point, buffer[i] = '\0'
|
||||
// Skip to the next character.
|
||||
i++;
|
||||
} else {
|
||||
// We should never reach here, unless we have two NULs in a row
|
||||
argv[*argc] = buffer + i;
|
||||
(*argc)++;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,424 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - Command processor
|
||||
*
|
||||
* Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "build-config.h"
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
#include <stdarg.h>
|
||||
#include <errno.h>
|
||||
#include <pthread.h>
|
||||
#include <poll.h>
|
||||
#include <signal.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
#define PINELOG_MODULE X52D_MOD_COMMAND
|
||||
#include "pinelog.h"
|
||||
#include <daemon/constants.h>
|
||||
#include <daemon/command.h>
|
||||
#include <daemon/config.h>
|
||||
#include <daemon/client.h>
|
||||
#include <daemon/x52dcomm-internal.h>
|
||||
|
||||
static int client_fd[X52D_MAX_CLIENTS];
|
||||
|
||||
static pthread_t command_thr;
|
||||
static int command_sock_fd;
|
||||
static const char *command_sock;
|
||||
|
||||
#if defined __has_attribute
|
||||
# if __has_attribute(format)
|
||||
__attribute((format(printf, 4, 5)))
|
||||
# endif
|
||||
#endif
|
||||
static void response_formatted(char *buffer, int *buflen, const char *type,
|
||||
const char *fmt, ...)
|
||||
{
|
||||
va_list ap;
|
||||
char response[X52D_BUFSZ];
|
||||
int resplen;
|
||||
int typelen;
|
||||
|
||||
typelen = strlen(type) + 1;
|
||||
strcpy(response, type);
|
||||
resplen = typelen;
|
||||
|
||||
if (*fmt) {
|
||||
va_start(ap, fmt);
|
||||
resplen += vsnprintf(response + typelen, sizeof(response) - typelen, fmt, ap) + 1;
|
||||
va_end(ap);
|
||||
}
|
||||
|
||||
memcpy(buffer, response, resplen);
|
||||
*buflen = resplen;
|
||||
}
|
||||
|
||||
static void response_strings(char *buffer, int *buflen, const char *type, int count, ...)
|
||||
{
|
||||
va_list ap;
|
||||
char response[X52D_BUFSZ];
|
||||
int resplen;
|
||||
int arglen;
|
||||
int i;
|
||||
char *arg;
|
||||
|
||||
arglen = strlen(type) + 1;
|
||||
strcpy(response, type);
|
||||
resplen = arglen;
|
||||
|
||||
va_start(ap, count);
|
||||
for (i = 0; i < count; i++) {
|
||||
arg = va_arg(ap, char *);
|
||||
arglen = strlen(arg) + 1;
|
||||
if ((size_t)(arglen + resplen) >= sizeof(response)) {
|
||||
PINELOG_ERROR("Too many arguments for response_strings %s", type);
|
||||
break;
|
||||
}
|
||||
|
||||
strcpy(response + resplen, arg);
|
||||
resplen += arglen;
|
||||
}
|
||||
va_end(ap);
|
||||
|
||||
memcpy(buffer, response, resplen);
|
||||
*buflen = resplen;
|
||||
}
|
||||
|
||||
#define NUMARGS(...) (sizeof((const char *[]){__VA_ARGS__}) / sizeof(const char *))
|
||||
#define ERR(...) response_strings(buffer, buflen, "ERR", NUMARGS(__VA_ARGS__), ##__VA_ARGS__)
|
||||
#define ERR_fmt(fmt, ...) response_formatted(buffer, buflen, "ERR", fmt, ##__VA_ARGS__)
|
||||
|
||||
#define OK(...) response_strings(buffer, buflen, "OK", NUMARGS(__VA_ARGS__), ##__VA_ARGS__)
|
||||
#define OK_fmt(fmt, ...) response_formatted(buffer, buflen, "OK", fmt, ##__VA_ARGS__)
|
||||
|
||||
#define DATA(...) response_strings(buffer, buflen, "DATA", NUMARGS(__VA_ARGS__), ##__VA_ARGS__)
|
||||
|
||||
#define MATCH(idx, cmd) if (strcasecmp(argv[idx], cmd) == 0)
|
||||
|
||||
static bool check_file(const char *file_path, int mode)
|
||||
{
|
||||
if (*file_path == '\0') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mode && access(file_path, mode)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void cmd_config(char *buffer, int *buflen, int argc, char **argv)
|
||||
{
|
||||
if (argc < 2) {
|
||||
ERR("Insufficient arguments for 'config' command");
|
||||
return;
|
||||
}
|
||||
|
||||
MATCH(1, "load") {
|
||||
if (argc == 3) {
|
||||
if (!check_file(argv[2], R_OK)) {
|
||||
ERR_fmt("Invalid file '%s' for 'config load' command", argv[2]);
|
||||
return;
|
||||
}
|
||||
|
||||
x52d_config_load(argv[2]);
|
||||
x52d_config_apply();
|
||||
OK("config", "load", argv[2]);
|
||||
} else {
|
||||
// Invalid number of args
|
||||
ERR_fmt("Unexpected arguments for 'config load' command; got %d, expected 3", argc);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
MATCH(1, "reload") {
|
||||
if (argc == 2) {
|
||||
raise(SIGHUP);
|
||||
OK("config", "reload");
|
||||
} else {
|
||||
ERR_fmt("Unexpected arguments for 'config reload' command; got %d, expected 2", argc);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
MATCH(1, "dump") {
|
||||
if (argc == 3) {
|
||||
if (!check_file(argv[2], 0)) {
|
||||
ERR_fmt("Invalid file '%s' for 'config dump' command", argv[2]);
|
||||
return;
|
||||
}
|
||||
|
||||
x52d_config_save(argv[2]);
|
||||
OK("config", "dump", argv[2]);
|
||||
} else {
|
||||
ERR_fmt("Unexpected arguments for 'config dump' command; got %d, expected 3", argc);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
MATCH(1, "save") {
|
||||
if (argc == 2) {
|
||||
raise(SIGUSR1);
|
||||
OK("config", "save");
|
||||
} else {
|
||||
ERR_fmt("Unexpected arguments for 'config save' command; got %d, expected 2", argc);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
MATCH(1, "set") {
|
||||
if (argc == 5) {
|
||||
int rc = x52d_config_set(argv[2], argv[3], argv[4]);
|
||||
if (rc != 0) {
|
||||
ERR_fmt("Error %d setting '%s.%s'='%s': %s", rc,
|
||||
argv[2], argv[3], argv[4], strerror(rc));
|
||||
} else {
|
||||
x52d_config_apply_immediate(argv[2], argv[3]);
|
||||
OK("config", "set", argv[2], argv[3], argv[4]);
|
||||
}
|
||||
} else {
|
||||
ERR_fmt("Unexpected arguments for 'config set' command; got %d, expected 5", argc);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
MATCH(1, "get") {
|
||||
if (argc == 4) {
|
||||
const char *rv = x52d_config_get(argv[2], argv[3]);
|
||||
if (rv == NULL) {
|
||||
ERR_fmt("Error getting '%s.%s'", argv[2], argv[3]);
|
||||
} else {
|
||||
DATA(argv[2], argv[3], rv);
|
||||
}
|
||||
} else {
|
||||
ERR_fmt("Unexpected arguments for 'config get' command; got %d, expected 4", argc);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
ERR_fmt("Unknown subcommand '%s' for 'config' command", argv[1]);
|
||||
}
|
||||
|
||||
#define DATA_LMAP(level, resp) do {\
|
||||
int input_level_ ## __LINE__ = level; \
|
||||
const char *lmap_level_ ## __LINE__ = lookup_level_by_id(input_level_ ## __LINE__); \
|
||||
char lmap_unknown_level ## __LINE__[32] = {0}; \
|
||||
if (lmap_level_ ## __LINE__ == NULL) { \
|
||||
snprintf(lmap_unknown_level ## __LINE__, sizeof(lmap_unknown_level ## __LINE__), \
|
||||
"unknown (%d)", input_level_ ## __LINE__); \
|
||||
lmap_level_ ## __LINE__ = lmap_unknown_level ## __LINE__; \
|
||||
} \
|
||||
DATA(resp, lmap_level_ ## __LINE__); \
|
||||
} while(0)
|
||||
|
||||
|
||||
static void cmd_logging(char *buffer, int *buflen, int argc, char **argv)
|
||||
{
|
||||
if (argc < 2) {
|
||||
ERR("Insufficient arguments for 'logging' command");
|
||||
return;
|
||||
}
|
||||
|
||||
// logging show [module]
|
||||
MATCH(1, "show") {
|
||||
if (argc == 2) {
|
||||
DATA_LMAP(pinelog_get_level(), "global");
|
||||
} else if (argc == 3) {
|
||||
int module = lookup_module_by_name(argv[2]);
|
||||
if (module == INT_MAX) {
|
||||
ERR_fmt("Invalid module '%s'", argv[2]);
|
||||
} else {
|
||||
DATA_LMAP(pinelog_get_module_level(module), argv[2]);
|
||||
}
|
||||
} else {
|
||||
ERR_fmt("Unexpected arguments for 'logging show' command; got %d, expected 2 or 3", argc);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// logging set [module] <level>
|
||||
MATCH(1, "set") {
|
||||
if (argc == 3) {
|
||||
int level = lookup_level_by_name(argv[2]);
|
||||
if (level == INT_MAX) {
|
||||
ERR_fmt("Unknown level '%s' for 'logging set' command", argv[2]);
|
||||
} else if (level == PINELOG_LVL_NOTSET) {
|
||||
ERR("'default' level is not valid without a module");
|
||||
} else {
|
||||
pinelog_set_level(level);
|
||||
OK("logging", "set", argv[2]);
|
||||
}
|
||||
} else if (argc == 4) {
|
||||
int level = lookup_level_by_name(argv[3]);
|
||||
int module = lookup_module_by_name(argv[2]);
|
||||
|
||||
if (module == INT_MAX) {
|
||||
ERR_fmt("Invalid module '%s'", argv[2]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (level == INT_MAX) {
|
||||
ERR_fmt("Unknown level '%s' for 'logging set' command", argv[3]);
|
||||
} else {
|
||||
pinelog_set_module_level(module, level);
|
||||
OK("logging", "set", argv[2], argv[3]);
|
||||
}
|
||||
} else {
|
||||
ERR_fmt("Unexpected arguments for 'logging set' command; got %d, expected 3 or 4", argc);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
ERR_fmt("Unknown subcommand '%s' for 'logging' command", argv[1]);
|
||||
}
|
||||
|
||||
static void command_parser(char *buffer, int *buflen)
|
||||
{
|
||||
int argc = 0;
|
||||
char *argv[X52D_BUFSZ] = { 0 };
|
||||
|
||||
x52d_split_args(&argc, argv, buffer, *buflen);
|
||||
|
||||
MATCH(0, "config") {
|
||||
cmd_config(buffer, buflen, argc, argv);
|
||||
} else MATCH(0, "logging") {
|
||||
cmd_logging(buffer, buflen, argc, argv);
|
||||
} else {
|
||||
ERR_fmt("Unknown command '%s'", argv[0]);
|
||||
}
|
||||
}
|
||||
|
||||
static void client_handler(int fd)
|
||||
{
|
||||
char buffer[X52D_BUFSZ] = { 0 };
|
||||
int sent;
|
||||
int rc;
|
||||
|
||||
rc = recv(fd, buffer, sizeof(buffer), 0);
|
||||
if (rc < 0) {
|
||||
PINELOG_ERROR(_("Error reading from client %d: %s"),
|
||||
fd, strerror(errno));
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse and handle command.
|
||||
command_parser(buffer, &rc);
|
||||
|
||||
PINELOG_TRACE("Sending %d bytes in response '%s'", rc, buffer);
|
||||
sent = send(fd, buffer, rc, 0);
|
||||
if (sent != rc) {
|
||||
PINELOG_ERROR(_("Short write to client %d; expected %d bytes, wrote %d bytes"),
|
||||
fd, rc, sent);
|
||||
}
|
||||
}
|
||||
|
||||
int x52d_command_loop(int sock_fd)
|
||||
{
|
||||
struct pollfd pfd[MAX_CONN];
|
||||
int rc;
|
||||
|
||||
rc = x52d_client_poll(client_fd, pfd, sock_fd);
|
||||
if (rc <= 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
x52d_client_handle(client_fd, pfd, sock_fd, client_handler);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void * x52d_command_thread(void *param)
|
||||
{
|
||||
(void)param;
|
||||
for (;;) {
|
||||
if (x52d_command_loop(command_sock_fd) < 0) {
|
||||
PINELOG_FATAL(_("Error %d during command loop: %s"),
|
||||
errno, strerror(errno));
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int x52d_command_init(const char *sock_path)
|
||||
{
|
||||
int sock_fd;
|
||||
int len;
|
||||
struct sockaddr_un local;
|
||||
|
||||
x52d_client_init(client_fd);
|
||||
|
||||
command_sock = sock_path;
|
||||
command_sock_fd = -1;
|
||||
|
||||
len = x52d_setup_command_sock(command_sock, &local);
|
||||
if (len < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
sock_fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (sock_fd < 0) {
|
||||
/* Failure creating the socket. Abort early */
|
||||
PINELOG_ERROR(_("Error creating command socket: %s"), strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
command_sock_fd = sock_fd;
|
||||
|
||||
/* Mark the socket as non-blocking */
|
||||
if (x52d_set_socket_nonblocking(sock_fd) < 0) {
|
||||
PINELOG_ERROR(_("Error marking command socket as nonblocking: %s"),
|
||||
strerror(errno));
|
||||
goto sock_failure;
|
||||
}
|
||||
|
||||
if (x52d_listen_socket(&local, len, sock_fd) < 0) {
|
||||
PINELOG_ERROR(_("Error listening on command socket: %s"), strerror(errno));
|
||||
goto listen_failure;
|
||||
}
|
||||
|
||||
PINELOG_INFO(_("Starting command processing thread"));
|
||||
pthread_create(&command_thr, NULL, x52d_command_thread, NULL);
|
||||
|
||||
return 0;
|
||||
|
||||
listen_failure:
|
||||
unlink(local.sun_path);
|
||||
sock_failure:
|
||||
if (command_sock_fd >= 0) {
|
||||
close(command_sock_fd);
|
||||
command_sock_fd = -1;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
void x52d_command_exit(void)
|
||||
{
|
||||
PINELOG_INFO(_("Shutting down command processing thread"));
|
||||
pthread_cancel(command_thr);
|
||||
|
||||
// Close the socket and remove the socket file
|
||||
if (command_sock_fd >= 0) {
|
||||
command_sock = x52d_command_sock_path(command_sock);
|
||||
PINELOG_TRACE("Closing command socket %s", command_sock);
|
||||
|
||||
close(command_sock_fd);
|
||||
command_sock_fd = -1;
|
||||
|
||||
unlink(command_sock);
|
||||
command_sock = NULL;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - Command processor
|
||||
*
|
||||
* Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#ifndef X52D_COMMAND_H
|
||||
#define X52D_COMMAND_H
|
||||
|
||||
int x52d_command_init(const char *sock_path);
|
||||
void x52d_command_exit(void);
|
||||
int x52d_command_loop(int sock_fd);
|
||||
|
||||
#endif // !defined X52D_COMMAND_H
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - Configuration parser
|
||||
*
|
||||
* Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "build-config.h"
|
||||
#include <errno.h>
|
||||
|
||||
#define PINELOG_MODULE X52D_MOD_CONFIG
|
||||
#include "pinelog.h"
|
||||
#include <daemon/config.h>
|
||||
#include <daemon/constants.h>
|
||||
|
||||
static struct x52d_config x52d_config;
|
||||
|
||||
void x52d_config_load(const char *cfg_file)
|
||||
{
|
||||
int rc;
|
||||
|
||||
if (cfg_file == NULL) {
|
||||
cfg_file = X52D_SYS_CFG_FILE;
|
||||
}
|
||||
|
||||
rc = x52d_config_set_defaults(&x52d_config);
|
||||
if (rc != 0) {
|
||||
PINELOG_FATAL(_("Error %d setting configuration defaults: %s"),
|
||||
rc, strerror(rc));
|
||||
}
|
||||
|
||||
rc = x52d_config_load_file(&x52d_config, cfg_file);
|
||||
if (rc != 0) {
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
// Apply overrides
|
||||
rc = x52d_config_apply_overrides(&x52d_config);
|
||||
x52d_config_clear_overrides();
|
||||
if (rc != 0) {
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
void x52d_config_save(const char *cfg_file)
|
||||
{
|
||||
int rc;
|
||||
|
||||
if (cfg_file == NULL) {
|
||||
cfg_file = X52D_SYS_CFG_FILE;
|
||||
}
|
||||
|
||||
rc = x52d_config_save_file(&x52d_config, cfg_file);
|
||||
if (rc != 0) {
|
||||
PINELOG_ERROR(_("Error %d saving configuration file: %s"),
|
||||
rc, strerror(rc));
|
||||
}
|
||||
}
|
||||
|
||||
int x52d_config_set(const char *section, const char *key, const char *value)
|
||||
{
|
||||
if (section == NULL || key == NULL || value == NULL) {
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
PINELOG_TRACE("Processing config set '%s.%s'='%s'", section, key, value);
|
||||
|
||||
return x52d_config_process_kv(&x52d_config, section, key, value);
|
||||
}
|
||||
|
||||
const char *x52d_config_get(const char *section, const char *key)
|
||||
{
|
||||
const char *value;
|
||||
if (section == NULL || key == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
value = x52d_config_get_param(&x52d_config, section, key);
|
||||
PINELOG_TRACE("Processed config get '%s.%s'='%s'", section, key, value);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/* Callback stubs
|
||||
* TODO: Remove the ones below when their implementation is complete
|
||||
*/
|
||||
void x52d_cfg_set_Profiles_Directory(char* param) { (void)param; }
|
||||
void x52d_cfg_set_Profiles_ClutchEnabled(bool param) { (void)param; }
|
||||
void x52d_cfg_set_Profiles_ClutchLatched(bool param) { (void)param; }
|
||||
|
||||
void x52d_config_apply_immediate(const char *section, const char *key)
|
||||
{
|
||||
#define CFG(c_sec, c_key, name, parser, def) \
|
||||
if (!strcasecmp(section, #c_sec) && !strcasecmp(key, #c_key)) { \
|
||||
PINELOG_TRACE("Invoking " #c_sec "." #c_key " callback"); \
|
||||
x52d_cfg_set_ ## c_sec ## _ ## c_key(x52d_config . name); \
|
||||
} else
|
||||
|
||||
#include <daemon/config.def>
|
||||
// Dummy to capture the trailing else
|
||||
// Wrap it in braces in case tracing has been disabled
|
||||
{ PINELOG_TRACE("Ignoring apply_immediate(%s.%s)", section, key); }
|
||||
}
|
||||
|
||||
void x52d_config_apply(void)
|
||||
{
|
||||
#define CFG(section, key, name, parser, def) \
|
||||
PINELOG_TRACE("Calling configuration callback for " #section "." #key); \
|
||||
x52d_cfg_set_ ## section ## _ ## key(x52d_config . name);
|
||||
#include <daemon/config.def>
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
/**********************************************************************
|
||||
* X52 Daemon Configuration
|
||||
*********************************************************************/
|
||||
|
||||
// The settings below are the defaults. Note that the section and key
|
||||
// strings are case insensitive, but the values are not necessarily so,
|
||||
// especially for those referring to paths or timezone names.
|
||||
|
||||
/* CFG(section, key, name, parser/dumper type, default) */
|
||||
/**********************************************************************
|
||||
* Clock Settings
|
||||
*********************************************************************/
|
||||
|
||||
// Enabled controls whether the clock is enabled or not. Set this to no to
|
||||
// disable the clock update. Keep in mind that if the clock was originally
|
||||
// enabled on the X52, then disabling it here won't make the clock disappear
|
||||
// on the MFD. You will need to unplug and reattach the X52 to make the
|
||||
// clock disappear
|
||||
CFG(Clock, Enabled, clock_enabled, bool, true)
|
||||
|
||||
// PrimaryIsLocal controls whether the primary clock displays local time or UTC.
|
||||
// Set this to yes to display local time, no for UTC.
|
||||
CFG(Clock, PrimaryIsLocal, primary_clock_local, bool, true)
|
||||
|
||||
// Secondary controls the timezone of the secondary clock. Use the standard
|
||||
// timezone name as defined by the Olson time database.
|
||||
CFG(Clock, Secondary, clock_2_tz, string, UTC)
|
||||
|
||||
// Tertiary controls the timezone of the tertiary clock. Use the standard
|
||||
// timezone name as defined by the Olson time database.
|
||||
CFG(Clock, Tertiary, clock_3_tz, string, UTC)
|
||||
|
||||
// Clock format for the primary clock
|
||||
CFG(Clock, FormatPrimary, clock_format[LIBX52_CLOCK_1], clock_format, 12hr)
|
||||
|
||||
// Clock format for the secondary clock
|
||||
CFG(Clock, FormatSecondary, clock_format[LIBX52_CLOCK_2], clock_format, 12hr)
|
||||
|
||||
// Clock format for the tertiary clock
|
||||
CFG(Clock, FormatTertiary, clock_format[LIBX52_CLOCK_3], clock_format, 12hr)
|
||||
|
||||
// Date format for the date display
|
||||
CFG(Clock, DateFormat, date_format, date_format, ddmmyy)
|
||||
|
||||
/**********************************************************************
|
||||
* LED Settings - only applicable to X52Pro
|
||||
*********************************************************************/
|
||||
// The LED settings map a color code or state to the corresponding LED.
|
||||
CFG(LED, Fire, leds[LIBX52_LED_FIRE], led, on)
|
||||
CFG(LED, Throttle, leds[LIBX52_LED_THROTTLE], led, on)
|
||||
CFG(LED, A, leds[LIBX52_LED_A], led, green)
|
||||
CFG(LED, B, leds[LIBX52_LED_B], led, green)
|
||||
CFG(LED, D, leds[LIBX52_LED_D], led, green)
|
||||
CFG(LED, E, leds[LIBX52_LED_E], led, green)
|
||||
CFG(LED, T1, leds[LIBX52_LED_T1], led, green)
|
||||
CFG(LED, T2, leds[LIBX52_LED_T2], led, green)
|
||||
CFG(LED, T3, leds[LIBX52_LED_T3], led, green)
|
||||
CFG(LED, POV, leds[LIBX52_LED_POV], led, green)
|
||||
CFG(LED, Clutch, leds[LIBX52_LED_CLUTCH], led, green)
|
||||
|
||||
/**********************************************************************
|
||||
* Brightness Settings
|
||||
*********************************************************************/
|
||||
// The brightness settings map the brightness value to the LEDs/MFD.
|
||||
CFG(Brightness, MFD, brightness[0], int, 128)
|
||||
CFG(Brightness, LED, brightness[1], int, 128)
|
||||
|
||||
/**********************************************************************
|
||||
* Mouse Settings
|
||||
*********************************************************************/
|
||||
// Enabled controls whether the virtual mouse is enabled or not.
|
||||
CFG(Mouse, Enabled, mouse_enabled, bool, true)
|
||||
|
||||
// DEPRECATED: Speed is a value that is proportional to the speed of updates to
|
||||
// the virtual mouse
|
||||
CFG(Mouse, Speed, mouse_speed, int, 0)
|
||||
|
||||
// Sensitivity is a percentage that is used to scale the speed of the virtual
|
||||
// mouse. This replaces the old speed value.
|
||||
CFG(Mouse, Sensitivity, mouse_sensitivity, int, 0)
|
||||
|
||||
// ReverseScroll controls the scrolling direction
|
||||
CFG(Mouse, ReverseScroll, mouse_reverse_scroll, bool, false)
|
||||
|
||||
// IsometricMode controls whether to use linear or isometric speed calculations
|
||||
CFG(Mouse, IsometricMode, mouse_isometric_mode, bool, false)
|
||||
|
||||
// CurveFactor controls the speed curve
|
||||
CFG(Mouse, CurveFactor, mouse_curve_factor, int, 3)
|
||||
|
||||
// Deadzone controls the deadzone range for the thumbstick
|
||||
CFG(Mouse, Deadzone, mouse_deadzone_factor, int, 0)
|
||||
|
||||
/**********************************************************************
|
||||
* Profiles - only valid on Linux
|
||||
*********************************************************************/
|
||||
// Directory is the location of the folder containing the individual profiles.
|
||||
CFG(Profiles, Directory, profiles_dir, string, /etc/x52d/profiles.d)
|
||||
|
||||
// ClutchEnabled determines if the clutch button is treated specially
|
||||
CFG(Profiles, ClutchEnabled, clutch_enabled, bool, false)
|
||||
|
||||
// ClutchLatched controls if the clutch button (if enabled) is a latched button
|
||||
// (press once to enter clutch mode, press again to exit clutch mode), or must
|
||||
// be held down to remain in clutch mode.
|
||||
CFG(Profiles, ClutchLatched, clutch_latched, bool, false)
|
||||
|
||||
// KeyboardLayout is the default keyboard layout used when mapping
|
||||
// profile keys to keyboard events.
|
||||
CFG(Profiles, KeyboardLayout, profile_keyboard_layout, string, us)
|
||||
|
||||
#undef CFG
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - Configuration parser header
|
||||
*
|
||||
* Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#ifndef X52D_CONFIG_H
|
||||
#define X52D_CONFIG_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <limits.h>
|
||||
#include <libx52/libx52.h>
|
||||
|
||||
/**
|
||||
* @brief Configuration structure
|
||||
*
|
||||
* Keep this in sync with the sample configuration
|
||||
*/
|
||||
struct x52d_config {
|
||||
bool clock_enabled;
|
||||
bool primary_clock_local;
|
||||
|
||||
// Since we don't have a _MAX identifier for libx52_clock_id, use
|
||||
// the maximum clock ID + 1 as the length
|
||||
libx52_clock_format clock_format[LIBX52_CLOCK_3 + 1];
|
||||
libx52_date_format date_format;
|
||||
|
||||
char clock_2_tz[NAME_MAX];
|
||||
char clock_3_tz[NAME_MAX];
|
||||
|
||||
// Since we don't have a _MAX identifier for libx52_led_id, hardcode
|
||||
// the length in the following declaration.
|
||||
libx52_led_state leds[21];
|
||||
|
||||
int brightness[2];
|
||||
|
||||
bool mouse_enabled;
|
||||
int mouse_speed;
|
||||
int mouse_sensitivity;
|
||||
bool mouse_reverse_scroll;
|
||||
bool mouse_isometric_mode;
|
||||
int mouse_curve_factor;
|
||||
int mouse_deadzone_factor;
|
||||
|
||||
bool clutch_enabled;
|
||||
bool clutch_latched;
|
||||
|
||||
char profiles_dir[NAME_MAX];
|
||||
|
||||
char profile_keyboard_layout[NAME_MAX];
|
||||
};
|
||||
|
||||
/* Callback functions for configuration */
|
||||
// These functions are defined in the individual modules
|
||||
void x52d_cfg_set_Clock_Enabled(bool param);
|
||||
void x52d_cfg_set_Clock_PrimaryIsLocal(bool param);
|
||||
void x52d_cfg_set_Clock_Secondary(char* param);
|
||||
void x52d_cfg_set_Clock_Tertiary(char* param);
|
||||
void x52d_cfg_set_Clock_FormatPrimary(libx52_clock_format param);
|
||||
void x52d_cfg_set_Clock_FormatSecondary(libx52_clock_format param);
|
||||
void x52d_cfg_set_Clock_FormatTertiary(libx52_clock_format param);
|
||||
void x52d_cfg_set_Clock_DateFormat(libx52_date_format param);
|
||||
void x52d_cfg_set_LED_Fire(libx52_led_state param);
|
||||
void x52d_cfg_set_LED_Throttle(libx52_led_state param);
|
||||
void x52d_cfg_set_LED_A(libx52_led_state param);
|
||||
void x52d_cfg_set_LED_B(libx52_led_state param);
|
||||
void x52d_cfg_set_LED_D(libx52_led_state param);
|
||||
void x52d_cfg_set_LED_E(libx52_led_state param);
|
||||
void x52d_cfg_set_LED_T1(libx52_led_state param);
|
||||
void x52d_cfg_set_LED_T2(libx52_led_state param);
|
||||
void x52d_cfg_set_LED_T3(libx52_led_state param);
|
||||
void x52d_cfg_set_LED_POV(libx52_led_state param);
|
||||
void x52d_cfg_set_LED_Clutch(libx52_led_state param);
|
||||
void x52d_cfg_set_Brightness_MFD(uint16_t param);
|
||||
void x52d_cfg_set_Brightness_LED(uint16_t param);
|
||||
void x52d_cfg_set_Mouse_Enabled(bool param);
|
||||
void x52d_cfg_set_Mouse_Speed(int param);
|
||||
void x52d_cfg_set_Mouse_Sensitivity(int param);
|
||||
void x52d_cfg_set_Mouse_ReverseScroll(bool param);
|
||||
void x52d_cfg_set_Mouse_IsometricMode(bool param);
|
||||
void x52d_cfg_set_Mouse_CurveFactor(int param);
|
||||
void x52d_cfg_set_Mouse_Deadzone(int param);
|
||||
void x52d_cfg_set_Profiles_Directory(char* param);
|
||||
void x52d_cfg_set_Profiles_ClutchEnabled(bool param);
|
||||
void x52d_cfg_set_Profiles_ClutchLatched(bool param);
|
||||
void x52d_cfg_set_Profiles_KeyboardLayout(char *param);
|
||||
|
||||
int x52d_config_process_kv(void *user, const char *section, const char *key, const char *value);
|
||||
const char *x52d_config_get_param(struct x52d_config *cfg, const char *section, const char *key);
|
||||
|
||||
int x52d_config_set_defaults(struct x52d_config *cfg);
|
||||
|
||||
int x52d_config_load_file(struct x52d_config *cfg, const char *cfg_file);
|
||||
|
||||
int x52d_config_save_override(const char *override_str);
|
||||
|
||||
int x52d_config_apply_overrides(struct x52d_config *cfg);
|
||||
|
||||
void x52d_config_clear_overrides(void);
|
||||
|
||||
void x52d_config_load(const char *cfg_file);
|
||||
void x52d_config_apply_immediate(const char *section, const char *key);
|
||||
void x52d_config_apply(void);
|
||||
|
||||
int x52d_config_save_file(struct x52d_config *cfg, const char *cfg_file);
|
||||
void x52d_config_save(const char *cfg_file);
|
||||
|
||||
int x52d_config_set(const char *section, const char *key, const char *value);
|
||||
const char *x52d_config_get(const char *section, const char *key);
|
||||
|
||||
#endif // !defined X52D_CONFIG_H
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - Configuration dumper
|
||||
*
|
||||
* Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "build-config.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stddef.h>
|
||||
#include <stdlib.h>
|
||||
#include <errno.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#define PINELOG_MODULE X52D_MOD_CONFIG
|
||||
#include "pinelog.h"
|
||||
#include <libx52/libx52.h>
|
||||
#include <daemon/config.h>
|
||||
#include <daemon/constants.h>
|
||||
|
||||
// Create a pointer "name" of type "type", which stores the value of the
|
||||
// corresponding element within the config struct.
|
||||
#define CONFIG_PTR(type, name) type name = (type)((uintptr_t)cfg + offset)
|
||||
|
||||
// Check if the parameters are all valid
|
||||
#define CHECK_PARAMS() do { if (cfg == NULL || section == NULL || key == NULL) { return NULL; } } while(0)
|
||||
|
||||
static const char * bool_dumper(const char *section, const char *key, const struct x52d_config *cfg, size_t offset)
|
||||
{
|
||||
CONFIG_PTR(bool *, config);
|
||||
CHECK_PARAMS();
|
||||
|
||||
PINELOG_TRACE("Printing bool value %s.%s from offset %lu value = %d",
|
||||
section, key, offset, *config);
|
||||
return *config ? "true" : "false";
|
||||
}
|
||||
|
||||
static const char * string_dumper(const char *section, const char *key, struct x52d_config *cfg, size_t offset)
|
||||
{
|
||||
CONFIG_PTR(char *, config);
|
||||
CHECK_PARAMS();
|
||||
|
||||
PINELOG_TRACE("Printing string value %s.%s from offset %lu value = %s",
|
||||
section, key, offset, config);
|
||||
return config;
|
||||
}
|
||||
|
||||
static const char * int_dumper(const char *section, const char *key, struct x52d_config *cfg, size_t offset)
|
||||
{
|
||||
static char dump[256];
|
||||
CONFIG_PTR(int *, config);
|
||||
CHECK_PARAMS();
|
||||
|
||||
PINELOG_TRACE("Printing int value %s.%s from offset %lu value = %d",
|
||||
section, key, offset, *config);
|
||||
snprintf(dump, sizeof(dump), "%d", *config);
|
||||
|
||||
return dump;
|
||||
}
|
||||
|
||||
static const char * led_dumper(const char *section, const char *key, struct x52d_config *cfg, size_t offset)
|
||||
{
|
||||
CONFIG_PTR(libx52_led_state *, config);
|
||||
CHECK_PARAMS();
|
||||
|
||||
PINELOG_TRACE("Printing led value %s.%s from offset %lu value = %d",
|
||||
section, key, offset, *config);
|
||||
return libx52_led_state_to_str(*config);
|
||||
}
|
||||
|
||||
static const char * clock_format_dumper(const char *section, const char *key, struct x52d_config *cfg, size_t offset)
|
||||
{
|
||||
CONFIG_PTR(libx52_clock_format *, config);
|
||||
CHECK_PARAMS();
|
||||
|
||||
PINELOG_TRACE("Printing clock format value %s.%s from offset %lu value = %d",
|
||||
section, key, offset, *config);
|
||||
return libx52_clock_format_to_str(*config);
|
||||
}
|
||||
|
||||
static const char * date_format_dumper(const char *section, const char *key, struct x52d_config *cfg, size_t offset)
|
||||
{
|
||||
CONFIG_PTR(libx52_date_format *, config);
|
||||
CHECK_PARAMS();
|
||||
|
||||
PINELOG_TRACE("Printing date format value %s.%s from offset %lu value = %d",
|
||||
section, key, offset, *config);
|
||||
return libx52_date_format_to_str(*config);
|
||||
}
|
||||
|
||||
#undef CHECK_PARAMS
|
||||
#undef CONFIG_PTR
|
||||
|
||||
int x52d_config_save_file(struct x52d_config *cfg, const char *cfg_file)
|
||||
{
|
||||
FILE *cfg_fp;
|
||||
char *current_section = NULL;
|
||||
const char *value;
|
||||
|
||||
if (cfg == NULL || cfg_file == NULL) {
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
cfg_fp = fopen(cfg_file, "w");
|
||||
if (cfg_fp == NULL) {
|
||||
PINELOG_ERROR(_("Unable to save config file %s - code %d: %s"),
|
||||
cfg_file, errno, strerror(errno));
|
||||
return 1;
|
||||
}
|
||||
|
||||
PINELOG_TRACE("Saving configuration to file %s", cfg_file);
|
||||
#define CFG(section, key, name, type, def) do { \
|
||||
if (current_section == NULL || strcasecmp(current_section, #section)) { \
|
||||
if (current_section != NULL) { \
|
||||
free(current_section); \
|
||||
} \
|
||||
current_section = strdup(#section); \
|
||||
PINELOG_TRACE("Printing section header %s", #section); \
|
||||
fprintf(cfg_fp, "[%s]\n", #section); \
|
||||
} \
|
||||
PINELOG_TRACE("Dumping " #section "." #key " to file %s", cfg_file); \
|
||||
value = type ## _dumper(#section, #key, cfg, offsetof(struct x52d_config, name)); \
|
||||
if (value == NULL) { \
|
||||
PINELOG_ERROR(_("Failed to dump %s.%s to config file %s"), \
|
||||
#section, #key, cfg_file); \
|
||||
goto exit_dump; \
|
||||
} else { \
|
||||
fprintf(cfg_fp, "%s = %s\n", #key, value); \
|
||||
} \
|
||||
} while (0);
|
||||
#include <daemon/config.def>
|
||||
|
||||
exit_dump:
|
||||
free(current_section);
|
||||
fclose(cfg_fp);
|
||||
return (value == NULL);
|
||||
}
|
||||
|
||||
const char *x52d_config_get_param(struct x52d_config *cfg, const char *section, const char *key)
|
||||
{
|
||||
#define CFG(section_c, key_c, name, type, def) do { \
|
||||
if (strcasecmp(section, #section_c) == 0 && strcasecmp(key, #key_c) == 0) { \
|
||||
return type ## _dumper(section, key, cfg, offsetof(struct x52d_config, name)); \
|
||||
} \
|
||||
} while (0);
|
||||
#include <daemon/config.def>
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
|
@ -0,0 +1,365 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - Configuration parser
|
||||
*
|
||||
* Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "build-config.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stddef.h>
|
||||
#include <stdlib.h>
|
||||
#include <errno.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#define PINELOG_MODULE X52D_MOD_CONFIG
|
||||
#include "ini.h"
|
||||
#include "pinelog.h"
|
||||
#include <daemon/config.h>
|
||||
#include <daemon/constants.h>
|
||||
|
||||
/* Parser function typedef */
|
||||
typedef int (*parser_fn)(struct x52d_config *, size_t, const char *);
|
||||
|
||||
// Check if the parameters are all valid
|
||||
#define CHECK_PARAMS() do { if (cfg == NULL || value == NULL) { return EINVAL; } } while(0)
|
||||
|
||||
// Create a pointer "name" of type "type", which stores the pointer to the
|
||||
// corresponding element within the config struct.
|
||||
#define CONFIG_PTR(type, name) type name = (type)((uintptr_t)cfg + offset)
|
||||
|
||||
static int bool_parser(struct x52d_config *cfg, size_t offset, const char *value)
|
||||
{
|
||||
CONFIG_PTR(bool *, config);
|
||||
CHECK_PARAMS();
|
||||
|
||||
if (!strcasecmp(value, "yes") || !strcasecmp(value, "true")) {
|
||||
*config = true;
|
||||
} else if (!strcasecmp(value, "no") || !strcasecmp(value, "false")) {
|
||||
*config = false;
|
||||
} else {
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int string_parser(struct x52d_config *cfg, size_t offset, const char *value)
|
||||
{
|
||||
CONFIG_PTR(char *, config);
|
||||
CHECK_PARAMS();
|
||||
|
||||
/* String parameters are all NAME_MAX len */
|
||||
strncpy(config, value, NAME_MAX-1);
|
||||
config[NAME_MAX-1] = '\0';
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int int_parser(struct x52d_config *cfg, size_t offset, const char *value)
|
||||
{
|
||||
CONFIG_PTR(int *, config);
|
||||
char *endptr;
|
||||
int retval;
|
||||
|
||||
CHECK_PARAMS();
|
||||
|
||||
errno = 0;
|
||||
retval = strtol(value, &endptr, 0);
|
||||
if (errno != 0) {
|
||||
return errno;
|
||||
}
|
||||
if (*endptr != '\0') {
|
||||
// Invalid characters in string
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
*config = retval;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int led_parser(struct x52d_config *cfg, size_t offset, const char *value)
|
||||
{
|
||||
CONFIG_PTR(libx52_led_state *, config);
|
||||
CHECK_PARAMS();
|
||||
|
||||
#define MATCH_STATE(val) if (!strcasecmp(value, #val)) { *config = LIBX52_LED_STATE_ ## val ; }
|
||||
MATCH_STATE(OFF)
|
||||
else MATCH_STATE(ON)
|
||||
else MATCH_STATE(RED)
|
||||
else MATCH_STATE(AMBER)
|
||||
else MATCH_STATE(GREEN)
|
||||
else return EINVAL;
|
||||
#undef MATCH_STATE
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int clock_format_parser(struct x52d_config *cfg, size_t offset, const char *value)
|
||||
{
|
||||
CONFIG_PTR(libx52_clock_format *, config);
|
||||
CHECK_PARAMS();
|
||||
|
||||
if (!strcasecmp(value, "12hr") || !strcasecmp(value, "12")) {
|
||||
*config = LIBX52_CLOCK_FORMAT_12HR;
|
||||
} else if (!strcasecmp(value, "24hr") || !strcasecmp(value, "24")) {
|
||||
*config = LIBX52_CLOCK_FORMAT_24HR;
|
||||
} else {
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int date_format_parser(struct x52d_config *cfg, size_t offset, const char *value)
|
||||
{
|
||||
CONFIG_PTR(libx52_date_format *, config);
|
||||
CHECK_PARAMS();
|
||||
|
||||
if (!strcasecmp(value, "ddmmyy") || !strcasecmp(value, "dd-mm-yy")) {
|
||||
*config = LIBX52_DATE_FORMAT_DDMMYY;
|
||||
} else if (!strcasecmp(value, "mmddyy") || !strcasecmp(value, "mm-dd-yy")) {
|
||||
*config = LIBX52_DATE_FORMAT_MMDDYY;
|
||||
} else if (!strcasecmp(value, "yymmdd") || !strcasecmp(value, "yy-mm-dd")) {
|
||||
*config = LIBX52_DATE_FORMAT_YYMMDD;
|
||||
} else {
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
#undef CHECK_PARAMS
|
||||
#undef CONFIG_PTR
|
||||
|
||||
/* Map for config->param */
|
||||
#define CFG(section, key, name, type, def) {#section, #key, type ## _parser, offsetof(struct x52d_config, name)},
|
||||
static const struct config_map {
|
||||
const char *section;
|
||||
const char *key;
|
||||
parser_fn parser;
|
||||
size_t offset;
|
||||
} config_map[] = {
|
||||
#include <daemon/config.def>
|
||||
|
||||
// Terminating entry
|
||||
{NULL, NULL, NULL, 0}
|
||||
};
|
||||
|
||||
int x52d_config_process_kv(void *user, const char *section, const char *key, const char *value)
|
||||
{
|
||||
int i;
|
||||
int rc = 0;
|
||||
bool found = false;
|
||||
struct x52d_config *cfg = (struct x52d_config*)user;
|
||||
|
||||
for (i = 0; config_map[i].key != NULL; i++) {
|
||||
rc = 0;
|
||||
if (!strcasecmp(config_map[i].key, key) &&
|
||||
!strcasecmp(config_map[i].section, section)) {
|
||||
found = true;
|
||||
PINELOG_TRACE("Setting '%s.%s'='%s'",
|
||||
config_map[i].section, config_map[i].key, value);
|
||||
rc = config_map[i].parser(cfg, config_map[i].offset, value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
// Print error message, but continue
|
||||
PINELOG_INFO(_("Ignoring unknown key '%s.%s'"), section, key);
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set configuration defaults
|
||||
*
|
||||
* @param[in] cfg Pointer to config struct
|
||||
*
|
||||
* @returns 0 on success, non-zero error code on failure
|
||||
*/
|
||||
int x52d_config_set_defaults(struct x52d_config *cfg) {
|
||||
int rc;
|
||||
|
||||
if (cfg == NULL) {
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
PINELOG_TRACE("Setting configuration defaults");
|
||||
#define CFG(section, key, name, parser, def) \
|
||||
rc = x52d_config_process_kv(cfg, #section, #key, #def); \
|
||||
if (rc != 0) { \
|
||||
return rc; \
|
||||
}
|
||||
#include <daemon/config.def>
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int x52d_config_load_file(struct x52d_config *cfg, const char *cfg_file)
|
||||
{
|
||||
int rc;
|
||||
if (cfg == NULL || cfg_file == NULL) {
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
PINELOG_TRACE("Loading configuration from file %s", cfg_file);
|
||||
rc = ini_parse(cfg_file, x52d_config_process_kv, cfg);
|
||||
if (rc < 0) {
|
||||
PINELOG_ERROR(_("Failed processing configuration file %s - code %d"),
|
||||
cfg_file, rc);
|
||||
return EIO;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct x52d_config_override {
|
||||
char *section;
|
||||
char *key;
|
||||
char *value;
|
||||
struct x52d_config_override *next;
|
||||
};
|
||||
|
||||
static struct x52d_config_override *override_head;
|
||||
static struct x52d_config_override *override_tail;
|
||||
|
||||
int x52d_config_save_override(const char *override_str)
|
||||
{
|
||||
// Parse override string of the form section.key=value
|
||||
struct x52d_config_override *override;
|
||||
char *string = NULL;
|
||||
char *free_ptr = NULL;
|
||||
char *ptr;
|
||||
int rc;
|
||||
|
||||
PINELOG_TRACE("Allocating memory (%lu bytes) for override structure", sizeof(*override));
|
||||
override = calloc(1, sizeof(*override));
|
||||
if (override == NULL) {
|
||||
PINELOG_ERROR(_("Failed to allocate memory for override structure"));
|
||||
rc = ENOMEM;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
errno = 0;
|
||||
PINELOG_TRACE("Duplicating override string");
|
||||
string = strdup(override_str);
|
||||
if (string == NULL) {
|
||||
PINELOG_ERROR(_("Failed to allocate memory for override string"));
|
||||
rc = errno;
|
||||
goto cleanup;
|
||||
}
|
||||
free_ptr = string;
|
||||
|
||||
override->section = string;
|
||||
// Ensure that the string is of the form ([^.]+\.[^=]+=.*)
|
||||
ptr = strchr(string, '.');
|
||||
if (ptr == NULL || ptr == string) {
|
||||
// No section found
|
||||
PINELOG_ERROR(_("No section found in override string '%s'"), string);
|
||||
rc = EINVAL;
|
||||
goto cleanup;
|
||||
}
|
||||
// Reset the . to NUL
|
||||
*ptr = '\0';
|
||||
ptr++;
|
||||
PINELOG_TRACE("Splitting override string to '%s' and '%s'", string, ptr);
|
||||
string = ptr;
|
||||
|
||||
override->key = string;
|
||||
ptr = strchr(string, '=');
|
||||
if (ptr == NULL || ptr == string) {
|
||||
// No key found
|
||||
PINELOG_ERROR(_("No key found in override string '%s'"), string);
|
||||
rc = EINVAL;
|
||||
goto cleanup;
|
||||
}
|
||||
// Reset the = to NUL
|
||||
*ptr = '\0';
|
||||
ptr++;
|
||||
PINELOG_TRACE("Splitting override string to '%s' and '%s'", string, ptr);
|
||||
|
||||
if (*ptr == '\0') {
|
||||
// No value found
|
||||
PINELOG_ERROR(_("No value found in override string '%s'"), string);
|
||||
rc = EINVAL;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
override->value = ptr;
|
||||
|
||||
// Add the override to the linked list
|
||||
if (override_tail != NULL) {
|
||||
PINELOG_TRACE("Linking override to list tail");
|
||||
override_tail->next = override;
|
||||
}
|
||||
PINELOG_TRACE("Setting list tail to override");
|
||||
override_tail = override;
|
||||
|
||||
if (override_head == NULL) {
|
||||
PINELOG_TRACE("Setting list head to override");
|
||||
override_head = override;
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
cleanup:
|
||||
if (free_ptr != NULL) {
|
||||
free(free_ptr);
|
||||
}
|
||||
if (override != NULL) {
|
||||
free(override);
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
int x52d_config_apply_overrides(struct x52d_config *cfg)
|
||||
{
|
||||
int rc;
|
||||
struct x52d_config_override *tmp = override_head;
|
||||
|
||||
if (cfg == NULL) {
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
while (tmp != NULL) {
|
||||
PINELOG_TRACE("Processing override '%s.%s=%s'",
|
||||
tmp->section,
|
||||
tmp->key,
|
||||
tmp->value);
|
||||
rc = x52d_config_process_kv(cfg,
|
||||
tmp->section,
|
||||
tmp->key,
|
||||
tmp->value);
|
||||
if (rc != 0) {
|
||||
PINELOG_ERROR(_("Error processing override '%s.%s=%s'"),
|
||||
tmp->section,
|
||||
tmp->key,
|
||||
tmp->value);
|
||||
return rc;
|
||||
}
|
||||
tmp = tmp->next;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void x52d_config_clear_overrides(void)
|
||||
{
|
||||
struct x52d_config_override *tmp;
|
||||
while (override_head != NULL) {
|
||||
tmp = override_head;
|
||||
override_head = override_head->next;
|
||||
PINELOG_TRACE("Freeing override '%s.%s=%s'",
|
||||
tmp->section,
|
||||
tmp->key,
|
||||
tmp->value);
|
||||
free(tmp);
|
||||
}
|
||||
|
||||
override_tail = NULL;
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
{
|
||||
"_comment": "The configuration registry is a historic record of\nall configuration identifiers. Do NOT edit this file manually, or else, the\ncommunication protocol may break.",
|
||||
"sections": {
|
||||
"CLOCK": 1,
|
||||
"LED": 2,
|
||||
"BRIGHTNESS": 3,
|
||||
"MOUSE": 4,
|
||||
"PROFILES": 5
|
||||
},
|
||||
"options": {
|
||||
"CLOCK": {
|
||||
"ENABLED": 1,
|
||||
"PRIMARYISLOCAL": 2,
|
||||
"SECONDARY": 3,
|
||||
"TERTIARY": 4,
|
||||
"FORMATPRIMARY": 5,
|
||||
"FORMATSECONDARY": 6,
|
||||
"FORMATTERTIARY": 7,
|
||||
"DATEFORMAT": 8
|
||||
},
|
||||
"LED": {
|
||||
"FIRE": 1,
|
||||
"THROTTLE": 2,
|
||||
"A": 3,
|
||||
"B": 4,
|
||||
"D": 5,
|
||||
"E": 6,
|
||||
"T1": 7,
|
||||
"T2": 8,
|
||||
"T3": 9,
|
||||
"POV": 10,
|
||||
"CLUTCH": 11
|
||||
},
|
||||
"BRIGHTNESS": {
|
||||
"MFD": 1,
|
||||
"LED": 2
|
||||
},
|
||||
"MOUSE": {
|
||||
"ENABLED": 1,
|
||||
"SENSITIVITY": 2,
|
||||
"SPEED": 3,
|
||||
"REVERSESCROLL": 4,
|
||||
"ISOMETRICMODE": 5,
|
||||
"CURVEFACTOR": 6,
|
||||
"DEADZONE": 7
|
||||
},
|
||||
"PROFILES": {
|
||||
"DIRECTORY": 1,
|
||||
"CLUTCHENABLED": 2,
|
||||
"CLUTCHLATCHED": 3,
|
||||
"KEYBOARDLAYOUT": 4
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - Application constants
|
||||
*
|
||||
* Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#ifndef X52D_CONST_H
|
||||
#define X52D_CONST_H
|
||||
|
||||
#define X52D_APP_NAME "x52d"
|
||||
|
||||
#define X52D_LOG_FILE LOGDIR "/" X52D_APP_NAME ".log"
|
||||
|
||||
#define X52D_SYS_CFG_FILE SYSCONFDIR "/" X52D_APP_NAME "/" X52D_APP_NAME ".conf"
|
||||
|
||||
#define X52D_PID_FILE RUNDIR "/" X52D_APP_NAME ".pid"
|
||||
|
||||
#define X52D_SOCK_COMMAND RUNDIR "/" X52D_APP_NAME ".cmd"
|
||||
#define X52D_SOCK_NOTIFY RUNDIR "/" X52D_APP_NAME ".notify"
|
||||
|
||||
#include "gettext.h"
|
||||
#define N_(x) gettext_noop(x)
|
||||
#define _(x) gettext(x)
|
||||
|
||||
#define X52D_MAX_CLIENTS 63
|
||||
|
||||
#include "module-map.h" // For module IDs
|
||||
|
||||
#endif // !defined X52D_CONST_H
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - CRC-32 (zlib / Python zlib.crc32 compatible)
|
||||
*
|
||||
* Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include <daemon/crc32.h>
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* Table matches zlib's crc_table (reflect, poly 0xEDB88320). */
|
||||
static const uint32_t x52_crc32_table[256] = {
|
||||
0x00000000u, 0x77073096u, 0xee0e612cu, 0x990951bau, 0x076dc419u, 0x706af48fu, 0xe963a535u,
|
||||
0x9e6495a3u, 0x0edb8832u, 0x79dcb8a4u, 0xe0d5e91eu, 0x97d2d988u, 0x09b64c2bu, 0x7eb17cbdu,
|
||||
0xe7b82d07u, 0x90bf1d91u, 0x1db71064u, 0x6ab020f2u, 0xf3b97148u, 0x84be41deu, 0x1adad47du,
|
||||
0x6ddde4ebu, 0xf4d4b551u, 0x83d385c7u, 0x136c9856u, 0x646ba8c0u, 0xfd62f97au, 0x8a65c9ecu,
|
||||
0x14015c4fu, 0x63066cd9u, 0xfa0f3d63u, 0x8d080df5u, 0x3b6e20c8u, 0x4c69105eu, 0xd56041e4u,
|
||||
0xa2677172u, 0x3c03e4d1u, 0x4b04d447u, 0xd20d85fdu, 0xa50ab56bu, 0x35b5a8fau, 0x42b2986cu,
|
||||
0xdbbbc9d6u, 0xacbcf940u, 0x32d86ce3u, 0x45df5c75u, 0xdcd60dcfu, 0xabd13d59u, 0x26d930acu,
|
||||
0x51de003au, 0xc8d75180u, 0xbfd06116u, 0x21b4f4b5u, 0x56b3c423u, 0xcfba9599u, 0xb8bda50fu,
|
||||
0x2802b89eu, 0x5f058808u, 0xc60cd9b2u, 0xb10be924u, 0x2f6f7c87u, 0x58684c11u, 0xc1611dabu,
|
||||
0xb6662d3du, 0x76dc4190u, 0x01db7106u, 0x98d220bcu, 0xefd5102au, 0x71b18589u, 0x06b6b51fu,
|
||||
0x9fbfe4a5u, 0xe8b8d433u, 0x7807c9a2u, 0x0f00f934u, 0x9609a88eu, 0xe10e9818u, 0x7f6a0dbbu,
|
||||
0x086d3d2du, 0x91646c97u, 0xe6635c01u, 0x6b6b51f4u, 0x1c6c6162u, 0x856530d8u, 0xf262004eu,
|
||||
0x6c0695edu, 0x1b01a57bu, 0x8208f4c1u, 0xf50fc457u, 0x65b0d9c6u, 0x12b7e950u, 0x8bbeb8eau,
|
||||
0xfcb9887cu, 0x62dd1ddfu, 0x15da2d49u, 0x8cd37cf3u, 0xfbd44c65u, 0x4db26158u, 0x3ab551ceu,
|
||||
0xa3bc0074u, 0xd4bb30e2u, 0x4adfa541u, 0x3dd895d7u, 0xa4d1c46du, 0xd3d6f4fbu, 0x4369e96au,
|
||||
0x346ed9fcu, 0xad678846u, 0xda60b8d0u, 0x44042d73u, 0x33031de5u, 0xaa0a4c5fu, 0xdd0d7cc9u,
|
||||
0x5005713cu, 0x270241aau, 0xbe0b1010u, 0xc90c2086u, 0x5768b525u, 0x206f85b3u, 0xb966d409u,
|
||||
0xce61e49fu, 0x5edef90eu, 0x29d9c998u, 0xb0d09822u, 0xc7d7a8b4u, 0x59b33d17u, 0x2eb40d81u,
|
||||
0xb7bd5c3bu, 0xc0ba6cadu, 0xedb88320u, 0x9abfb3b6u, 0x03b6e20cu, 0x74b1d29au, 0xead54739u,
|
||||
0x9dd277afu, 0x04db2615u, 0x73dc1683u, 0xe3630b12u, 0x94643b84u, 0x0d6d6a3eu, 0x7a6a5aa8u,
|
||||
0xe40ecf0bu, 0x9309ff9du, 0x0a00ae27u, 0x7d079eb1u, 0xf00f9344u, 0x8708a3d2u, 0x1e01f268u,
|
||||
0x6906c2feu, 0xf762575du, 0x806567cbu, 0x196c3671u, 0x6e6b06e7u, 0xfed41b76u, 0x89d32be0u,
|
||||
0x10da7a5au, 0x67dd4accu, 0xf9b9df6fu, 0x8ebeeff9u, 0x17b7be43u, 0x60b08ed5u, 0xd6d6a3e8u,
|
||||
0xa1d1937eu, 0x38d8c2c4u, 0x4fdff252u, 0xd1bb67f1u, 0xa6bc5767u, 0x3fb506ddu, 0x48b2364bu,
|
||||
0xd80d2bdau, 0xaf0a1b4cu, 0x36034af6u, 0x41047a60u, 0xdf60efc3u, 0xa867df55u, 0x316e8eefu,
|
||||
0x4669be79u, 0xcb61b38cu, 0xbc66831au, 0x256fd2a0u, 0x5268e236u, 0xcc0c7795u, 0xbb0b4703u,
|
||||
0x220216b9u, 0x5505262fu, 0xc5ba3bbeu, 0xb2bd0b28u, 0x2bb45a92u, 0x5cb36a04u, 0xc2d7ffa7u,
|
||||
0xb5d0cf31u, 0x2cd99e8bu, 0x5bdeae1du, 0x9b64c2b0u, 0xec63f226u, 0x756aa39cu, 0x026d930au,
|
||||
0x9c0906a9u, 0xeb0e363fu, 0x72076785u, 0x05005713u, 0x95bf4a82u, 0xe2b87a14u, 0x7bb12baeu,
|
||||
0x0cb61b38u, 0x92d28e9bu, 0xe5d5be0du, 0x7cdcefb7u, 0x0bdbdf21u, 0x86d3d2d4u, 0xf1d4e242u,
|
||||
0x68ddb3f8u, 0x1fda836eu, 0x81be16cdu, 0xf6b9265bu, 0x6fb077e1u, 0x18b74777u, 0x88085ae6u,
|
||||
0xff0f6a70u, 0x66063bcau, 0x11010b5cu, 0x8f659effu, 0xf862ae69u, 0x616bffd3u, 0x166ccf45u,
|
||||
0xa00ae278u, 0xd70dd2eeu, 0x4e048354u, 0x3903b3c2u, 0xa7672661u, 0xd06016f7u, 0x4969474du,
|
||||
0x3e6e77dbu, 0xaed16a4au, 0xd9d65adcu, 0x40df0b66u, 0x37d83bf0u, 0xa9bcae53u, 0xdebb9ec5u,
|
||||
0x47b2cf7fu, 0x30b5ffe9u, 0xbdbdf21cu, 0xcabac28au, 0x53b39330u, 0x24b4a3a6u, 0xbad03605u,
|
||||
0xcdd70693u, 0x54de5729u, 0x23d967bfu, 0xb3667a2eu, 0xc4614ab8u, 0x5d681b02u, 0x2a6f2b94u,
|
||||
0xb40bbe37u, 0xc30c8ea1u, 0x5a05df1bu, 0x2d02ef8du,
|
||||
};
|
||||
|
||||
uint32_t x52_crc32_init(void)
|
||||
{
|
||||
return X52_CRC32_INIT;
|
||||
}
|
||||
|
||||
uint32_t x52_crc32_update(uint32_t crc, const void *data, size_t len)
|
||||
{
|
||||
const unsigned char *p = data;
|
||||
|
||||
if (len == 0) {
|
||||
return crc;
|
||||
}
|
||||
|
||||
crc = ~crc;
|
||||
|
||||
while (len != 0) {
|
||||
crc = x52_crc32_table[(crc ^ *p++) & 0xffu] ^ (crc >> 8);
|
||||
len--;
|
||||
}
|
||||
|
||||
return ~crc;
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - CRC-32 (zlib / Python zlib.crc32 compatible)
|
||||
*
|
||||
* Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file crc32.h
|
||||
* @brief IEEE/ZIP CRC-32 matching @c zlib.crc32 / Python @c zlib.crc32 (polynomial 0xEDB88320).
|
||||
*
|
||||
* Incremental use: @code
|
||||
* uint32_t crc = x52_crc32_init();
|
||||
* crc = x52_crc32_update(crc, chunk0, len0);
|
||||
* crc = x52_crc32_update(crc, chunk1, len1);
|
||||
* // crc is final value (unsigned 32-bit, same encoding as Python after & 0xFFFFFFFF)
|
||||
* @endcode
|
||||
*/
|
||||
#ifndef X52D_CRC32_H
|
||||
#define X52D_CRC32_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Initial accumulator value (same as first argument to zlib @c crc32 for a new stream). */
|
||||
#define X52_CRC32_INIT 0u
|
||||
|
||||
/** Start a new CRC computation. */
|
||||
uint32_t x52_crc32_init(void);
|
||||
|
||||
/**
|
||||
* Feed zero or more bytes into a running CRC.
|
||||
*
|
||||
* @param crc Value from @ref x52_crc32_init or a prior @ref x52_crc32_update
|
||||
* @param data Input bytes; must be non-NULL if @p len is non-zero
|
||||
* @param len Number of bytes
|
||||
* @return Updated CRC-32 (same as Python <tt>zlib.crc32(data, crc) & 0xFFFFFFFF</tt>
|
||||
* when @p data is the new chunk only)
|
||||
*/
|
||||
uint32_t x52_crc32_update(uint32_t crc, const void *data, size_t len);
|
||||
|
||||
/** Alias for @ref x52_crc32_update (zlib-style name). */
|
||||
static inline uint32_t x52_crc32(uint32_t crc, const void *data, size_t len)
|
||||
{
|
||||
return x52_crc32_update(crc, data, len);
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* X52D_CRC32_H */
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - CRC-32 unit tests
|
||||
*
|
||||
* Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "build-config.h"
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdarg.h>
|
||||
#include <setjmp.h>
|
||||
#include <cmocka.h>
|
||||
|
||||
#include <daemon/crc32.h>
|
||||
|
||||
/* Golden values: Python zlib.crc32(data) & 0xFFFFFFFF */
|
||||
|
||||
static void test_init_zero(void **state)
|
||||
{
|
||||
(void)state;
|
||||
assert_int_equal((int)x52_crc32_init(), 0);
|
||||
assert_int_equal((int)X52_CRC32_INIT, 0);
|
||||
}
|
||||
|
||||
static void test_empty(void **state)
|
||||
{
|
||||
(void)state;
|
||||
uint32_t crc = x52_crc32_init();
|
||||
crc = x52_crc32_update(crc, NULL, 0);
|
||||
assert_true(crc == 0u);
|
||||
crc = x52_crc32_update(crc, "", 0);
|
||||
assert_true(crc == 0u);
|
||||
}
|
||||
|
||||
static void test_len0_preserves(void **state)
|
||||
{
|
||||
(void)state;
|
||||
const char *s = "abc";
|
||||
uint32_t crc = x52_crc32_update(0, s, 3);
|
||||
uint32_t again = x52_crc32_update(crc, s, 0);
|
||||
assert_true(again == crc);
|
||||
}
|
||||
|
||||
static void test_string_123456789(void **state)
|
||||
{
|
||||
(void)state;
|
||||
static const char s[] = "123456789";
|
||||
uint32_t crc = x52_crc32_update(0, s, sizeof(s) - 1u);
|
||||
assert_true(crc == 0xcbf43926u);
|
||||
}
|
||||
|
||||
static void test_bytes_0_to_255(void **state)
|
||||
{
|
||||
(void)state;
|
||||
unsigned char buf[256];
|
||||
for (unsigned i = 0; i < 256; i++) {
|
||||
buf[i] = (unsigned char)i;
|
||||
}
|
||||
uint32_t crc = x52_crc32_update(0, buf, sizeof(buf));
|
||||
assert_true(crc == 0x29058c73u);
|
||||
}
|
||||
|
||||
static void test_incremental_matches_one_shot(void **state)
|
||||
{
|
||||
(void)state;
|
||||
static const char s[] = "123456789";
|
||||
uint32_t a = x52_crc32_update(0, s, sizeof(s) - 1u);
|
||||
|
||||
uint32_t b = x52_crc32_init();
|
||||
b = x52_crc32_update(b, s, 3);
|
||||
b = x52_crc32_update(b, s + 3, 3);
|
||||
b = x52_crc32_update(b, s + 6, 3);
|
||||
|
||||
assert_true(b == a);
|
||||
}
|
||||
|
||||
static void test_chaining_second_segment(void **state)
|
||||
{
|
||||
(void)state;
|
||||
static const char h[] = "hello";
|
||||
static const char w[] = "world";
|
||||
static const char hw[] = "helloworld";
|
||||
|
||||
uint32_t c = x52_crc32_update(0, h, sizeof(h) - 1u);
|
||||
c = x52_crc32_update(c, w, sizeof(w) - 1u);
|
||||
|
||||
uint32_t whole = x52_crc32_update(0, hw, sizeof(hw) - 1u);
|
||||
assert_true(c == whole);
|
||||
assert_true(c == 0xf9eb20adu);
|
||||
}
|
||||
|
||||
static void test_one_byte_at_a_time(void **state)
|
||||
{
|
||||
(void)state;
|
||||
static const char s[] = "123456789";
|
||||
uint32_t expect = x52_crc32_update(0, s, sizeof(s) - 1u);
|
||||
uint32_t crc = x52_crc32_init();
|
||||
for (size_t i = 0; i < sizeof(s) - 1u; i++) {
|
||||
crc = x52_crc32_update(crc, s + i, 1);
|
||||
}
|
||||
assert_true(crc == expect);
|
||||
}
|
||||
|
||||
static void test_x52_crc32_alias(void **state)
|
||||
{
|
||||
(void)state;
|
||||
static const char s[] = "123456789";
|
||||
uint32_t u = x52_crc32_update(0, s, sizeof(s) - 1u);
|
||||
uint32_t v = x52_crc32(0, s, sizeof(s) - 1u);
|
||||
assert_true(u == v);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_init_zero),
|
||||
cmocka_unit_test(test_empty),
|
||||
cmocka_unit_test(test_len0_preserves),
|
||||
cmocka_unit_test(test_string_123456789),
|
||||
cmocka_unit_test(test_bytes_0_to_255),
|
||||
cmocka_unit_test(test_incremental_matches_one_shot),
|
||||
cmocka_unit_test(test_chaining_second_segment),
|
||||
cmocka_unit_test(test_one_byte_at_a_time),
|
||||
cmocka_unit_test(test_x52_crc32_alias),
|
||||
};
|
||||
|
||||
cmocka_set_message_output(CM_OUTPUT_TAP);
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
@page x52d X52 driver daemon
|
||||
|
||||
\b x52d is a daemon program that manages the X52 device in a similar fashion to
|
||||
the Windows X52 driver. It currently manages the following:
|
||||
|
||||
- LED state
|
||||
- LED brightness
|
||||
- MFD brightness
|
||||
- Clock display on MFD
|
||||
|
||||
# Command line arguments
|
||||
|
||||
- \c -f - Run daemon in foreground (default: no)
|
||||
- \c -v - Increase logging verbosity (default: log warnings)
|
||||
- \c -q - Reduce logging verbosity to minimum (default: no)
|
||||
- \c -l - Path to log file
|
||||
- \c -c - Path to configuration file
|
||||
- \c -p - Path to PID file
|
||||
- \c -o - Configuration override - only applied during startup
|
||||
- \c -s - Path to command socket (see \ref x52d_protocol)
|
||||
- \c -b - Path to notify socket
|
||||
|
||||
# Configuration file
|
||||
|
||||
\b x52d can be controlled by means of a configuration file. The default location
|
||||
of the configuration file is in `$(SYSCONFDIR)/x52d/x52d.conf`. The configuration
|
||||
file is an INI style file, and the default configuration is as listed below:
|
||||
|
||||
\include x52d.conf
|
||||
|
||||
## Configuration overrides
|
||||
|
||||
Configuration overrides are a means of testing a configuration parameter for a
|
||||
single instance of \b x52d, or to override the default configuration. The syntax
|
||||
for an override is \c section.key=value, where \c section, \c key and \c value
|
||||
correspond to the configuration \b section, \b key and \b value respectively.
|
||||
|
||||
For example, to override the secondary clock timezone to US Eastern Time, use
|
||||
the following syntax. Note that while the section and key are case-insensitive,
|
||||
the value may be case-sensitive, depending on which parameter is being
|
||||
overridden.
|
||||
|
||||
@code{.unparsed}
|
||||
-o clock.secondary=America/New_York
|
||||
@endcode
|
||||
*/
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - Daemon controller
|
||||
*
|
||||
* Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
@page x52ctl Command Line controller to X52 daemon
|
||||
|
||||
\htmlonly
|
||||
<b>x52ctl</b> - Command line controller to X52 daemon
|
||||
\endhtmlonly
|
||||
|
||||
# SYNOPSIS
|
||||
<tt>\b x52ctl [\a -i] [\a -s socket-path] [command] </tt>
|
||||
|
||||
# DESCRIPTION
|
||||
|
||||
x52ctl is a program that can be used to communicate with the X52 daemon. It can
|
||||
be used either as a one-shot program that can be run from another program or
|
||||
script, or it can be run interactively.
|
||||
|
||||
Commands are sent to the running daemon, and responses are written to standard
|
||||
output.
|
||||
|
||||
If not running interactively, then you must specify a command, or the program
|
||||
will exit with a failure exit code. If running interactively, the program will
|
||||
request input and send that to the daemon, until the user either enters the
|
||||
string "quit", or terminates input by using Ctrl+D.
|
||||
|
||||
# OPTIONS
|
||||
|
||||
- <tt>\b -i</tt>
|
||||
Run in interactive mode. Any additional non-option arguments are ignored.
|
||||
|
||||
- <tt>\b -s < \a socket-path ></tt>
|
||||
Use the socket at the given path. If this is not specified, then it uses a
|
||||
default socket.
|
||||
*/
|
||||
|
||||
#include "build-config.h"
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include <daemon/constants.h>
|
||||
#include <libx52/x52dcomm.h>
|
||||
|
||||
#define APP_NAME "x52ctl"
|
||||
#if HAVE_FUNC_ATTRIBUTE_NORETURN
|
||||
__attribute__((noreturn))
|
||||
#endif
|
||||
static void usage(int exit_code)
|
||||
{
|
||||
fprintf(stderr, _("Usage: %s [-i] [-s socket-path] [command]\n"), APP_NAME);
|
||||
exit(exit_code);
|
||||
}
|
||||
|
||||
static int send_command(int sock_fd, int argc, char **argv)
|
||||
{
|
||||
int rc;
|
||||
char buffer[1024];
|
||||
int buflen;
|
||||
|
||||
buflen = x52d_format_command(argc, (const char **)argv, buffer, sizeof(buffer));
|
||||
if (buflen < 0) {
|
||||
if (errno == E2BIG) {
|
||||
fprintf(stderr, _("Argument length too long\n"));
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
rc = x52d_send_command(sock_fd, buffer, buflen, sizeof(buffer));
|
||||
if (rc >= 0) {
|
||||
if (write(STDOUT_FILENO, buffer, rc) < 0) {
|
||||
perror("write");
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
perror("x52d_send_command");
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void interactive_mode(int sock_fd)
|
||||
{
|
||||
bool keep_running = true;
|
||||
char buffer[1024];
|
||||
|
||||
fputs("> ", stdout);
|
||||
while (keep_running && fgets(buffer, sizeof(buffer), stdin) != NULL) {
|
||||
int sargc;
|
||||
char *sargv[512] = { 0 };
|
||||
int pos;
|
||||
|
||||
if (strcasecmp(buffer, "quit\n") == 0) {
|
||||
keep_running = false;
|
||||
} else {
|
||||
/* Break the buffer into argc/argv */
|
||||
sargc = 0;
|
||||
pos = 0;
|
||||
while (buffer[pos]) {
|
||||
if (isspace(buffer[pos])) {
|
||||
buffer[pos] = '\0';
|
||||
pos++;
|
||||
} else {
|
||||
sargv[sargc] = &buffer[pos];
|
||||
sargc++;
|
||||
for (; buffer[pos] && !isspace(buffer[pos]); pos++);
|
||||
}
|
||||
}
|
||||
|
||||
if (send_command(sock_fd, sargc, sargv)) {
|
||||
keep_running = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (keep_running) {
|
||||
fputs("\n> ", stdout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
bool interactive = false;
|
||||
const char *socket_path = NULL;
|
||||
int opt;
|
||||
int sock_fd;
|
||||
int rc = EXIT_SUCCESS;
|
||||
|
||||
|
||||
/*
|
||||
* Parse command line arguments
|
||||
*
|
||||
* -i Interactive
|
||||
* -s Socket path
|
||||
*/
|
||||
while ((opt = getopt(argc, argv, "is:h")) != -1) {
|
||||
switch (opt) {
|
||||
case 'i':
|
||||
interactive = true;
|
||||
break;
|
||||
|
||||
case 's':
|
||||
socket_path = optarg;
|
||||
break;
|
||||
|
||||
case 'h':
|
||||
usage(EXIT_SUCCESS);
|
||||
break;
|
||||
|
||||
default:
|
||||
usage(EXIT_FAILURE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!interactive && optind >= argc) {
|
||||
usage(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
/* Connect to the socket */
|
||||
sock_fd = x52d_dial_command(socket_path);
|
||||
if (sock_fd < 0) {
|
||||
perror("x52d_dial_command");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
if (interactive) {
|
||||
if (optind < argc) {
|
||||
fprintf(stderr,
|
||||
_("Running in interactive mode, ignoring extra arguments\n"));
|
||||
}
|
||||
|
||||
interactive_mode(sock_fd);
|
||||
} else {
|
||||
if (send_command(sock_fd, argc - optind, &argv[optind])) {
|
||||
rc = EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
close(sock_fd);
|
||||
return rc;
|
||||
}
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - Device manager
|
||||
*
|
||||
* Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "build-config.h"
|
||||
#include <unistd.h>
|
||||
#include <pthread.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#define PINELOG_MODULE X52D_MOD_DEVICE
|
||||
#include <daemon/constants.h>
|
||||
#include <daemon/config.h>
|
||||
#include <daemon/device.h>
|
||||
#include <daemon/notify.h>
|
||||
#include <libx52/libx52.h>
|
||||
#include "pinelog.h"
|
||||
|
||||
static libx52_device *x52_dev;
|
||||
|
||||
static pthread_mutex_t device_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static pthread_t device_thr;
|
||||
static volatile bool device_update_needed;
|
||||
|
||||
static void *x52_dev_thr(void *param)
|
||||
{
|
||||
int rc;
|
||||
(void)param;
|
||||
|
||||
#define DEV_ACQ_DELAY 5 // seconds
|
||||
#define DEV_UPD_DELAY 50000 // microseconds
|
||||
|
||||
PINELOG_INFO(_("Starting X52 device manager thread"));
|
||||
for (;;) {
|
||||
if (!libx52_is_connected(x52_dev)) {
|
||||
PINELOG_TRACE("Attempting to connect to X52 device");
|
||||
rc = libx52_connect(x52_dev);
|
||||
if (rc != LIBX52_SUCCESS) {
|
||||
if (rc != LIBX52_ERROR_NO_DEVICE) {
|
||||
PINELOG_ERROR(_("Error %d connecting to device: %s"),
|
||||
rc, libx52_strerror(rc));
|
||||
} else {
|
||||
PINELOG_TRACE("No compatible X52 device found");
|
||||
}
|
||||
PINELOG_TRACE("Sleeping for %d seconds before trying to acquire device again", DEV_ACQ_DELAY);
|
||||
sleep(DEV_ACQ_DELAY);
|
||||
} else {
|
||||
/* Successfully connected */
|
||||
PINELOG_INFO(_("Device connected, writing configuration"));
|
||||
X52D_NOTIFY("CONNECTED");
|
||||
x52d_config_apply();
|
||||
}
|
||||
} else {
|
||||
if (!device_update_needed) {
|
||||
usleep(DEV_UPD_DELAY);
|
||||
continue;
|
||||
}
|
||||
|
||||
(void)x52d_dev_update();
|
||||
}
|
||||
}
|
||||
|
||||
#undef DEV_ACQ_DELAY
|
||||
#undef DEV_UPD_DELAY
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void x52d_dev_init(void)
|
||||
{
|
||||
int rc;
|
||||
PINELOG_INFO(_("Initializing libx52"));
|
||||
rc = libx52_init(&x52_dev);
|
||||
|
||||
if (rc != LIBX52_SUCCESS) {
|
||||
PINELOG_FATAL(_("Failure %d initializing libx52: %s"),
|
||||
rc, libx52_strerror(rc));
|
||||
}
|
||||
|
||||
// Create and initialize the thread
|
||||
pthread_create(&device_thr, NULL, x52_dev_thr, NULL);
|
||||
}
|
||||
|
||||
void x52d_dev_exit(void)
|
||||
{
|
||||
// Shutdown any threads
|
||||
PINELOG_INFO(_("Shutting down X52 device manager thread"));
|
||||
pthread_cancel(device_thr);
|
||||
|
||||
libx52_exit(x52_dev);
|
||||
}
|
||||
|
||||
#define WRAP_LIBX52(func) \
|
||||
int rc; \
|
||||
pthread_mutex_lock(&device_mutex); \
|
||||
rc = func; \
|
||||
pthread_mutex_unlock(&device_mutex); \
|
||||
if (rc != LIBX52_SUCCESS) { \
|
||||
if (rc != LIBX52_ERROR_TRY_AGAIN) { \
|
||||
PINELOG_ERROR(_("Error %d when updating X52 parameter: %s"), \
|
||||
rc, libx52_strerror(rc)); \
|
||||
} \
|
||||
} else { \
|
||||
device_update_needed = true; \
|
||||
} \
|
||||
return rc
|
||||
|
||||
int x52d_dev_set_text(uint8_t line, const char *text, uint8_t length)
|
||||
{
|
||||
WRAP_LIBX52(libx52_set_text(x52_dev, line, text, length));
|
||||
}
|
||||
int x52d_dev_set_led_state(libx52_led_id led, libx52_led_state state)
|
||||
{
|
||||
if (libx52_check_feature(x52_dev, LIBX52_FEATURE_LED) != LIBX52_ERROR_NOT_SUPPORTED) {
|
||||
WRAP_LIBX52(libx52_set_led_state(x52_dev, led, state));
|
||||
}
|
||||
|
||||
// If the target device does not support setting individual LEDs,
|
||||
// then ignore the set and let the caller think it succeeded.
|
||||
PINELOG_TRACE("Ignoring set LED state call as the device does not support it");
|
||||
return LIBX52_SUCCESS;
|
||||
}
|
||||
int x52d_dev_set_clock(time_t time, int local)
|
||||
{
|
||||
WRAP_LIBX52(libx52_set_clock(x52_dev, time, local));
|
||||
}
|
||||
int x52d_dev_set_clock_timezone(libx52_clock_id clock, int offset)
|
||||
{
|
||||
WRAP_LIBX52(libx52_set_clock_timezone(x52_dev, clock, offset));
|
||||
}
|
||||
int x52d_dev_set_clock_format(libx52_clock_id clock, libx52_clock_format format)
|
||||
{
|
||||
WRAP_LIBX52(libx52_set_clock_format(x52_dev, clock, format));
|
||||
}
|
||||
int x52d_dev_set_time(uint8_t hour, uint8_t minute)
|
||||
{
|
||||
WRAP_LIBX52(libx52_set_time(x52_dev, hour, minute));
|
||||
}
|
||||
int x52d_dev_set_date(uint8_t dd, uint8_t mm, uint8_t yy)
|
||||
{
|
||||
WRAP_LIBX52(libx52_set_date(x52_dev, dd, mm, yy));
|
||||
}
|
||||
int x52d_dev_set_date_format(libx52_date_format format)
|
||||
{
|
||||
WRAP_LIBX52(libx52_set_date_format(x52_dev, format));
|
||||
}
|
||||
int x52d_dev_set_brightness(uint8_t mfd, uint16_t brightness)
|
||||
{
|
||||
WRAP_LIBX52(libx52_set_brightness(x52_dev, mfd, brightness));
|
||||
}
|
||||
int x52d_dev_set_shift(uint8_t state)
|
||||
{
|
||||
WRAP_LIBX52(libx52_set_shift(x52_dev, state));
|
||||
}
|
||||
int x52d_dev_set_blink(uint8_t state)
|
||||
{
|
||||
WRAP_LIBX52(libx52_set_blink(x52_dev, state));
|
||||
}
|
||||
|
||||
int x52d_dev_update(void)
|
||||
{
|
||||
int rc;
|
||||
|
||||
pthread_mutex_lock(&device_mutex);
|
||||
rc = libx52_update(x52_dev);
|
||||
pthread_mutex_unlock(&device_mutex);
|
||||
|
||||
if (rc != LIBX52_SUCCESS) {
|
||||
if (rc == LIBX52_ERROR_NO_DEVICE) {
|
||||
// Detach from the existing device, the next thread run will
|
||||
// pick it up.
|
||||
PINELOG_TRACE("Disconnecting detached device");
|
||||
libx52_disconnect(x52_dev);
|
||||
X52D_NOTIFY("DISCONNECTED");
|
||||
} else {
|
||||
PINELOG_ERROR(_("Error %d when updating X52 device: %s"),
|
||||
rc, libx52_strerror(rc));
|
||||
}
|
||||
} else {
|
||||
device_update_needed = false;
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - Device manager header
|
||||
*
|
||||
* Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#ifndef X52D_DEVICE_H
|
||||
#define X52D_DEVICE_H
|
||||
|
||||
#include <libx52/libx52.h>
|
||||
|
||||
void x52d_dev_init(void);
|
||||
void x52d_dev_exit(void);
|
||||
|
||||
/* Wrapper methods for libx52 calls */
|
||||
int x52d_dev_set_text(uint8_t line, const char *text, uint8_t length);
|
||||
int x52d_dev_set_led_state(libx52_led_id led, libx52_led_state state);
|
||||
int x52d_dev_set_clock(time_t time, int local);
|
||||
int x52d_dev_set_clock_timezone(libx52_clock_id clock, int offset);
|
||||
int x52d_dev_set_clock_format(libx52_clock_id clock, libx52_clock_format format);
|
||||
int x52d_dev_set_time(uint8_t hour, uint8_t minute);
|
||||
int x52d_dev_set_date(uint8_t dd, uint8_t mm, uint8_t yy);
|
||||
int x52d_dev_set_date_format(libx52_date_format format);
|
||||
int x52d_dev_set_brightness(uint8_t mfd, uint16_t brightness);
|
||||
int x52d_dev_set_shift(uint8_t state);
|
||||
int x52d_dev_set_blink(uint8_t state);
|
||||
int x52d_dev_update(void);
|
||||
|
||||
#endif // !defined X52D_DEVICE_H
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - I/O driver
|
||||
*
|
||||
* Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "build-config.h"
|
||||
#include <stdbool.h>
|
||||
#include <unistd.h>
|
||||
#include <pthread.h>
|
||||
|
||||
#include <daemon/constants.h>
|
||||
#include <daemon/config.h>
|
||||
#include <daemon/io.h>
|
||||
#include <daemon/mouse.h>
|
||||
#include <libx52/libx52io.h>
|
||||
|
||||
#define PINELOG_MODULE X52D_MOD_IO
|
||||
#include "pinelog.h"
|
||||
|
||||
static libx52io_context *io_ctx;
|
||||
|
||||
static pthread_t io_thr;
|
||||
|
||||
static void process_report(libx52io_report *report, libx52io_report *prev)
|
||||
{
|
||||
// TODO: Process changes
|
||||
x52d_mouse_report_event(report);
|
||||
memcpy(prev, report, sizeof(*prev));
|
||||
}
|
||||
|
||||
static void *x52_io_thr(void *param)
|
||||
{
|
||||
int rc;
|
||||
libx52io_report report;
|
||||
libx52io_report prev_report;
|
||||
(void)param;
|
||||
|
||||
#define IO_READ_TIMEOUT 50 /* milliseconds */
|
||||
#define IO_ACQ_TIMEOUT 5 /* seconds */
|
||||
PINELOG_INFO(_("Starting X52 I/O thread"));
|
||||
|
||||
// Reset the previous report, so that process_report can handle changes.
|
||||
memset(&prev_report, 0, sizeof(prev_report));
|
||||
|
||||
for (;;) {
|
||||
rc = libx52io_read_timeout(io_ctx, &report, IO_READ_TIMEOUT);
|
||||
switch (rc) {
|
||||
case LIBX52IO_SUCCESS:
|
||||
// Found a report
|
||||
process_report(&report, &prev_report);
|
||||
break;
|
||||
|
||||
case LIBX52IO_ERROR_TIMEOUT:
|
||||
// No report received within the timeout
|
||||
break;
|
||||
|
||||
case LIBX52IO_ERROR_NO_DEVICE:
|
||||
PINELOG_TRACE("Device disconnected, trying to connect");
|
||||
rc = libx52io_open(io_ctx);
|
||||
if (rc != LIBX52IO_SUCCESS) {
|
||||
if (rc != LIBX52IO_ERROR_NO_DEVICE) {
|
||||
PINELOG_ERROR(_("Error %d opening X52 I/O device: %s"),
|
||||
rc, libx52io_strerror(rc));
|
||||
} else {
|
||||
PINELOG_TRACE("No compatible X52 I/O device found. Sleeping %d seconds before trying again.",
|
||||
IO_ACQ_TIMEOUT);
|
||||
}
|
||||
sleep(IO_ACQ_TIMEOUT);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
PINELOG_ERROR(_("Error %d reading from X52 I/O device: %s"),
|
||||
rc, libx52io_strerror(rc));
|
||||
|
||||
/*
|
||||
* Possibly disconnected, better to force disconnect now, and try
|
||||
* to reconnect later
|
||||
*/
|
||||
libx52io_close(io_ctx);
|
||||
|
||||
/* Report a NULL report to reset the mouse to default state */
|
||||
x52d_mouse_report_event(NULL);
|
||||
break;
|
||||
}
|
||||
}
|
||||
#undef IO_READ_TIMEOUT
|
||||
#undef IO_ACQ_TIMEOUT
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void x52d_io_init(void)
|
||||
{
|
||||
int rc;
|
||||
|
||||
PINELOG_TRACE("Initializing I/O driver");
|
||||
rc = libx52io_init(&io_ctx);
|
||||
if (rc != LIBX52IO_SUCCESS) {
|
||||
PINELOG_FATAL(_("Error %d initializing X52 I/O library: %s"),
|
||||
rc, libx52io_strerror(rc));
|
||||
}
|
||||
|
||||
rc = pthread_create(&io_thr, NULL, x52_io_thr, NULL);
|
||||
if (rc != 0) {
|
||||
PINELOG_FATAL(_("Error %d initializing I/O driver thread: %s"),
|
||||
rc, strerror(rc));
|
||||
}
|
||||
}
|
||||
|
||||
void x52d_io_exit(void)
|
||||
{
|
||||
PINELOG_INFO(_("Shutting down X52 I/O driver thread"));
|
||||
pthread_cancel(io_thr);
|
||||
|
||||
libx52io_exit(io_ctx);
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - I/O driver
|
||||
*
|
||||
* Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#ifndef X52D_IO_H
|
||||
#define X52D_IO_H
|
||||
|
||||
void x52d_io_init(void);
|
||||
void x52d_io_exit(void);
|
||||
|
||||
#endif // !defined X52D_IO_H
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - keyboard layout from config
|
||||
*
|
||||
* Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "build-config.h"
|
||||
|
||||
#define PINELOG_MODULE X52D_MOD_KEYBOARD_LAYOUT
|
||||
#include "pinelog.h"
|
||||
#include <daemon/constants.h>
|
||||
#include <daemon/keyboard_layout.h>
|
||||
#include <daemon/layout_format.h>
|
||||
|
||||
#include <errno.h>
|
||||
#include <limits.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static x52_layout *active_layout;
|
||||
|
||||
/**
|
||||
* Normal install: @c $DATADIR/x52d/<basename>.x52l
|
||||
*
|
||||
* If @c X52D_LAYOUT_DIR is set (non-empty), load @c $X52D_LAYOUT_DIR/<basename>.x52l instead so
|
||||
* uninstalled tests can point at the Meson build directory.
|
||||
*/
|
||||
static int load_layout_for_basename(const char *basename, x52_layout **out)
|
||||
{
|
||||
const char *layout_dir = getenv("X52D_LAYOUT_DIR");
|
||||
|
||||
if (layout_dir != NULL && layout_dir[0] != '\0') {
|
||||
char path[PATH_MAX];
|
||||
int n = snprintf(path, sizeof path, "%s/%s.x52l", layout_dir, basename);
|
||||
if (n < 0) {
|
||||
return EIO;
|
||||
}
|
||||
if ((size_t)n >= sizeof path) {
|
||||
return ENAMETOOLONG;
|
||||
}
|
||||
return x52_layout_load_path(path, out);
|
||||
}
|
||||
|
||||
return x52_layout_load_datadir(DATADIR, basename, out);
|
||||
}
|
||||
|
||||
const x52_layout *x52d_keyboard_layout_get(void)
|
||||
{
|
||||
return active_layout;
|
||||
}
|
||||
|
||||
void x52d_keyboard_layout_fini(void)
|
||||
{
|
||||
x52_layout_free(active_layout);
|
||||
active_layout = NULL;
|
||||
}
|
||||
|
||||
void x52d_keyboard_layout_reload(char *profile_keyboard_layout_value)
|
||||
{
|
||||
char basename[256];
|
||||
bool rejected = false;
|
||||
|
||||
x52_layout_normalize_keyboard_basename(profile_keyboard_layout_value, basename, sizeof basename, &rejected);
|
||||
if (rejected) {
|
||||
PINELOG_WARN(_("Invalid Profiles.KeyboardLayout value; using default layout basename 'us'"));
|
||||
}
|
||||
|
||||
x52_layout *new_layout = NULL;
|
||||
int err = load_layout_for_basename(basename, &new_layout);
|
||||
|
||||
if (err != 0 && strcmp(basename, "us") != 0) {
|
||||
PINELOG_WARN(
|
||||
_("Keyboard layout '%s' could not be loaded (%s); loading default 'us'"),
|
||||
basename, strerror(err > 0 ? err : EIO));
|
||||
err = load_layout_for_basename("us", &new_layout);
|
||||
}
|
||||
|
||||
if (err != 0) {
|
||||
PINELOG_FATAL(_("Could not load keyboard layout from %s/x52d (%s)"), DATADIR,
|
||||
strerror(err > 0 ? err : EIO));
|
||||
}
|
||||
|
||||
x52_layout *old_layout = active_layout;
|
||||
active_layout = new_layout;
|
||||
x52_layout_free(old_layout);
|
||||
|
||||
const char *desc = x52_layout_description(active_layout);
|
||||
PINELOG_INFO(_("Keyboard layout ready: %s (%s)"), x52_layout_name(active_layout),
|
||||
desc[0] != '\0' ? desc : _("no description"));
|
||||
}
|
||||
|
||||
void x52d_cfg_set_Profiles_KeyboardLayout(char *param)
|
||||
{
|
||||
x52d_keyboard_layout_reload(param);
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - keyboard layout from config
|
||||
*
|
||||
* Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#ifndef X52D_KEYBOARD_LAYOUT_H
|
||||
#define X52D_KEYBOARD_LAYOUT_H
|
||||
|
||||
#include <daemon/layout_format.h>
|
||||
|
||||
/**
|
||||
* @brief Load or reload layout from @c profile_keyboard_layout (@c Profiles.KeyboardLayout).
|
||||
*
|
||||
* Resolves @c $datadir/x52d/<basename>.x52l with basename hardening; falls back to @c us once if the
|
||||
* requested file is missing. Exits the process if no layout can be loaded.
|
||||
*/
|
||||
void x52d_keyboard_layout_reload(char *profile_keyboard_layout_value);
|
||||
|
||||
/** Active layout after @ref x52d_keyboard_layout_reload, or @c NULL before the first apply. */
|
||||
const x52_layout *x52d_keyboard_layout_get(void);
|
||||
|
||||
void x52d_keyboard_layout_fini(void);
|
||||
|
||||
#endif /* X52D_KEYBOARD_LAYOUT_H */
|
||||
|
|
@ -0,0 +1,197 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - x52layout v1 binary format
|
||||
*
|
||||
* Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file layout_format.h
|
||||
* @brief On-disk keyboard layout (@c .x52l) v1: constants, documentation, load and lookup API.
|
||||
*
|
||||
* The file is a dense index: entry @c entries[c] maps Unicode scalar @c c when
|
||||
* @c 0 <= c < codepoint_limit. For @c c >= codepoint_limit there is no mapping.
|
||||
*
|
||||
* **Header (128 bytes, all multi-byte integers big-endian / network order):**
|
||||
* - **0..3:** magic @c 'X' @c '5' @c '2' @c 'L'
|
||||
* - **4..5:** @c version (must be @ref X52_LAYOUT_FORMAT_VERSION)
|
||||
* - **6..7:** @c flags (v1: only @ref X52_LAYOUT_FLAG_NAME_TRUNCATED and/or
|
||||
* @ref X52_LAYOUT_FLAG_DESCRIPTION_TRUNCATED; other bits are reserved and must be zero)
|
||||
* - **8..11:** @c codepoint_limit — exclusive end of range; number of two-byte rows in @c entries
|
||||
* - **12..15:** @c checksum — CRC-32 (ZIP/IEEE, same as Python @c zlib.crc32) over the full
|
||||
* file with bytes 12..15 taken as zero when computing the digest
|
||||
* - **16..47:** @c layout_name (required: at least one character before @c NUL; remainder zero)
|
||||
* - **48..111:** @c description (optional, NUL-terminated, remainder zero)
|
||||
* - **112..127:** reserved (ignored on read in v1)
|
||||
*
|
||||
* **128+:** @c entries[] — pairs @c (modifiers, usage_key) for HID page 0x07; @c (0, 0) is empty.
|
||||
*
|
||||
* **File size:** exactly @c 128 + 2 * @c codepoint_limit bytes.
|
||||
*/
|
||||
#ifndef X52D_LAYOUT_FORMAT_H
|
||||
#define X52D_LAYOUT_FORMAT_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Four-byte magic at offset 0 (not NUL-terminated in the file). */
|
||||
#define X52_LAYOUT_MAGIC_0 'X'
|
||||
#define X52_LAYOUT_MAGIC_1 '5'
|
||||
#define X52_LAYOUT_MAGIC_2 '2'
|
||||
#define X52_LAYOUT_MAGIC_3 'L'
|
||||
|
||||
/** Total header size and byte offset of the @c entries table. */
|
||||
#define X52_LAYOUT_HEADER_BYTES 128u
|
||||
|
||||
#define X52_LAYOUT_FORMAT_VERSION 1u
|
||||
|
||||
/** Inclusive maximum for @c codepoint_limit - 1 (Unicode scalar space + sentinel ceiling). */
|
||||
#define X52_LAYOUT_CODEPOINT_LIMIT_MAX 0x110000u
|
||||
|
||||
/** @c layout_name field size in the header (offset 16). */
|
||||
#define X52_LAYOUT_NAME_FIELD_BYTES 32u
|
||||
|
||||
/** @c description field size in the header (offset 48). */
|
||||
#define X52_LAYOUT_DESCRIPTION_FIELD_BYTES 64u
|
||||
|
||||
/**
|
||||
* v1 header @c flags (big-endian on disk). @ref x52_layout_flags returns the host-endian value.
|
||||
*/
|
||||
#define X52_LAYOUT_FLAG_NAME_TRUNCATED 1u
|
||||
#define X52_LAYOUT_FLAG_DESCRIPTION_TRUNCATED 2u
|
||||
|
||||
/** Bitmask of flags defined for v1; other bits must be zero. */
|
||||
#define X52_LAYOUT_FLAGS_KNOWN (X52_LAYOUT_FLAG_NAME_TRUNCATED | X52_LAYOUT_FLAG_DESCRIPTION_TRUNCATED)
|
||||
|
||||
/** Loaded layout snapshot (opaque): full file copy, validated at load time. */
|
||||
typedef struct x52_layout x52_layout;
|
||||
|
||||
/**
|
||||
* @brief Read a layout file into a malloc'd snapshot and validate it (no @c mmap).
|
||||
*
|
||||
* @param path Path to the @c .x52l file; must not be @c NULL (otherwise @c EINVAL)
|
||||
* @param out On success, receives a new @ref x52_layout; must not be @c NULL; caller must @ref x52_layout_free
|
||||
*
|
||||
* @returns 0 on success, or a positive @c errno value (@c EINVAL, @c ENOMEM, @c EIO, @c ENOENT, …)
|
||||
*/
|
||||
int x52_layout_load_path(const char *path, x52_layout **out);
|
||||
|
||||
/**
|
||||
* @brief Turn @c Profiles.KeyboardLayout INI value into a safe layout basename.
|
||||
*
|
||||
* Empty or @c NULL yields @c "us" with @p rejected_out @c false. Values containing @c '/' ,
|
||||
* @c '\\' , @c ".." , disallowed characters, or oversize strings are rejected: @p out becomes
|
||||
* @c "us" and @p rejected_out is @c true (caller should log once). Requires @p out_sz @c >= 3.
|
||||
*
|
||||
* Allowed characters: ASCII alphanumeric, @c '_' , @c '-'.
|
||||
*/
|
||||
void x52_layout_normalize_keyboard_basename(const char *cfg_value, char *out, size_t out_sz, bool *rejected_out);
|
||||
|
||||
/**
|
||||
* @brief Build @c <datadir>/x52d/<basename>.x52l into @p path.
|
||||
*
|
||||
* @returns 0 on success, or @c ENAMETOOLONG if the path does not fit
|
||||
*/
|
||||
int x52_layout_join_file_path(char *path, size_t path_sz, const char *datadir, const char *basename);
|
||||
|
||||
/**
|
||||
* @brief Load @c join(datadir, "x52d", basename + ".x52l") after the same validation as @ref x52_layout_load_path.
|
||||
*
|
||||
* @returns 0 on success, or a positive @c errno (e.g. @c ENOENT, @c EINVAL, @c ENAMETOOLONG)
|
||||
*/
|
||||
int x52_layout_load_datadir(const char *datadir, const char *basename, x52_layout **out);
|
||||
|
||||
/**
|
||||
* @brief Copy @p data into an owned buffer and validate it.
|
||||
*
|
||||
* Same validation rules as @ref x52_layout_load_path (magic, version, flags, size, CRC-32, entries,
|
||||
* non-empty @c layout_name, etc.).
|
||||
*
|
||||
* @param data Layout file bytes; may be @c NULL only if @p len is zero (otherwise @c EINVAL)
|
||||
* @param len Number of bytes in @p data
|
||||
* @param out On success, receives a new @ref x52_layout; must not be @c NULL; caller must @ref x52_layout_free
|
||||
*
|
||||
* @returns 0 on success, or a positive @c errno value (@c EINVAL, @c ENOMEM, …)
|
||||
*/
|
||||
int x52_layout_load_memory(const void *data, size_t len, x52_layout **out);
|
||||
|
||||
/**
|
||||
* @brief Release a layout loaded by @ref x52_layout_load_path or @ref x52_layout_load_memory.
|
||||
*
|
||||
* @param layout Layout to free; @c NULL is a no-op
|
||||
*/
|
||||
void x52_layout_free(x52_layout *layout);
|
||||
|
||||
/**
|
||||
* @brief Exclusive end of the Unicode scalar range covered by @c entries (same as on-disk @c codepoint_limit).
|
||||
*
|
||||
* Lookups for @c code_point >= this value are not in the table.
|
||||
*
|
||||
* @param layout Loaded layout, or @c NULL
|
||||
*
|
||||
* @returns The limit value, or @c 0 if @p layout is @c NULL
|
||||
*/
|
||||
uint32_t x52_layout_codepoint_limit(const x52_layout *layout);
|
||||
|
||||
/**
|
||||
* @brief Host-endian copy of the on-disk @c flags field at header offset 6.
|
||||
*
|
||||
* Only bits in @ref X52_LAYOUT_FLAGS_KNOWN may be set in valid files; the loader rejects unknown bits.
|
||||
*
|
||||
* @param layout Loaded layout, or @c NULL
|
||||
*
|
||||
* @returns Flag word, or @c 0 if @p layout is @c NULL
|
||||
*/
|
||||
uint16_t x52_layout_flags(const x52_layout *layout);
|
||||
|
||||
/**
|
||||
* @brief Layout name from the header @c layout_name field.
|
||||
*
|
||||
* If @ref X52_LAYOUT_FLAG_NAME_TRUNCATED is set, the returned string is the on-disk name plus
|
||||
* @c "<truncated>". The pointer remains valid until @ref x52_layout_free; do not modify the string.
|
||||
*
|
||||
* @param layout Loaded layout, or @c NULL
|
||||
*
|
||||
* @returns Read-only NUL-terminated UTF-8 string; empty string if @p layout is @c NULL
|
||||
*/
|
||||
const char *x52_layout_name(const x52_layout *layout);
|
||||
|
||||
/**
|
||||
* @brief Optional description from the header @c description field.
|
||||
*
|
||||
* If @ref X52_LAYOUT_FLAG_DESCRIPTION_TRUNCATED is set, the returned string is the on-disk text plus
|
||||
* @c "<truncated>". The pointer remains valid until @ref x52_layout_free; do not modify the string.
|
||||
*
|
||||
* @param layout Loaded layout, or @c NULL
|
||||
*
|
||||
* @returns Read-only NUL-terminated UTF-8 string, or empty string if @p layout is @c NULL or the field is empty
|
||||
*/
|
||||
const char *x52_layout_description(const x52_layout *layout);
|
||||
|
||||
/**
|
||||
* @brief O(1) lookup of the HID chord for Unicode scalar @p code_point.
|
||||
*
|
||||
* Returns @c false when @p code_point is out of range, when the slot is empty @c (modifiers, usage_key) = (0, 0),
|
||||
* or when any argument is invalid. On @c false, @p modifiers_out and @p usage_key_out are left unchanged.
|
||||
*
|
||||
* @param layout Loaded layout; if @c NULL, returns @c false
|
||||
* @param code_point Unicode scalar value
|
||||
* @param[out] modifiers_out HID keyboard modifier byte (@ref vkm_key_modifiers bits); if @c NULL, returns @c false
|
||||
* @param[out] usage_key_out HID usage (page 0x07); if @c NULL, returns @c false
|
||||
*
|
||||
* @returns @c true if a non-empty mapping exists and was written to @p modifiers_out and @p usage_key_out
|
||||
*/
|
||||
bool x52_layout_lookup(const x52_layout *layout, uint32_t code_point, uint8_t *modifiers_out,
|
||||
uint8_t *usage_key_out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* X52D_LAYOUT_FORMAT_H */
|
||||
|
|
@ -0,0 +1,392 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - x52layout v1 loader
|
||||
*
|
||||
* Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "build-config.h"
|
||||
|
||||
#include <daemon/crc32.h>
|
||||
#include <daemon/layout_format.h>
|
||||
#include <daemon/layout_usage_allowlist.h>
|
||||
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <limits.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
struct x52_layout {
|
||||
uint8_t *data;
|
||||
size_t size;
|
||||
uint32_t codepoint_limit;
|
||||
uint16_t flags;
|
||||
char *name;
|
||||
char *description;
|
||||
};
|
||||
|
||||
static size_t meta_field_len(const uint8_t *field, size_t nbytes)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
for (i = 0; i < nbytes; i++) {
|
||||
if (field[i] == '\0') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
static char *copy_meta_string(const uint8_t *field, size_t nbytes, bool add_trunc_suffix)
|
||||
{
|
||||
static const char suf[] = "<truncated>";
|
||||
const size_t suf_len = sizeof(suf) - 1u;
|
||||
size_t base_len = meta_field_len(field, nbytes);
|
||||
size_t total = base_len + (add_trunc_suffix ? suf_len : 0);
|
||||
char *s = (char *)malloc(total + 1u);
|
||||
|
||||
if (s == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
if (base_len != 0) {
|
||||
memcpy(s, field, base_len);
|
||||
}
|
||||
if (add_trunc_suffix) {
|
||||
memcpy(s + base_len, suf, suf_len);
|
||||
}
|
||||
s[total] = '\0';
|
||||
return s;
|
||||
}
|
||||
|
||||
static uint16_t read_be16(const uint8_t *p)
|
||||
{
|
||||
return (uint16_t)((uint16_t)p[0] << 8 | (uint16_t)p[1]);
|
||||
}
|
||||
|
||||
static uint32_t read_be32(const uint8_t *p)
|
||||
{
|
||||
return (uint32_t)p[0] << 24 | (uint32_t)p[1] << 16 | (uint32_t)p[2] << 8 | (uint32_t)p[3];
|
||||
}
|
||||
|
||||
static uint32_t x52_layout_file_crc(const uint8_t *buf, size_t len)
|
||||
{
|
||||
uint32_t crc = x52_crc32_init();
|
||||
crc = x52_crc32_update(crc, buf, 12u);
|
||||
static const uint8_t zero_chk[4] = {0, 0, 0, 0};
|
||||
crc = x52_crc32_update(crc, zero_chk, 4u);
|
||||
if (len > 16u) {
|
||||
crc = x52_crc32_update(crc, buf + 16u, len - 16u);
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
static int validate_entries(const uint8_t *buf, uint32_t codepoint_limit)
|
||||
{
|
||||
const uint8_t *base = buf + X52_LAYOUT_HEADER_BYTES;
|
||||
|
||||
for (uint32_t i = 0; i < codepoint_limit; i++) {
|
||||
uint8_t usage = base[2u * (size_t)i + 1u];
|
||||
if (usage == 0) {
|
||||
continue;
|
||||
}
|
||||
if (!x52_layout_usage_key_allowed(usage)) {
|
||||
return EINVAL;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int layout_validate_and_adopt(uint8_t *buf, size_t len, x52_layout **out)
|
||||
{
|
||||
if (out == NULL) {
|
||||
free(buf);
|
||||
return EINVAL;
|
||||
}
|
||||
*out = NULL;
|
||||
|
||||
if (len < X52_LAYOUT_HEADER_BYTES) {
|
||||
free(buf);
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
if (memcmp(buf, "X52L", 4) != 0) {
|
||||
free(buf);
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
uint16_t version = read_be16(buf + 4);
|
||||
uint16_t flags = read_be16(buf + 6);
|
||||
uint32_t codepoint_limit = read_be32(buf + 8);
|
||||
uint32_t stored_crc = read_be32(buf + 12);
|
||||
|
||||
if (version != X52_LAYOUT_FORMAT_VERSION) {
|
||||
free(buf);
|
||||
return EINVAL;
|
||||
}
|
||||
if ((flags & (uint16_t)~X52_LAYOUT_FLAGS_KNOWN) != 0) {
|
||||
free(buf);
|
||||
return EINVAL;
|
||||
}
|
||||
if (codepoint_limit > X52_LAYOUT_CODEPOINT_LIMIT_MAX) {
|
||||
free(buf);
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
size_t body = (size_t)codepoint_limit * 2u;
|
||||
size_t expected = X52_LAYOUT_HEADER_BYTES + body;
|
||||
if (expected < X52_LAYOUT_HEADER_BYTES || len != expected) {
|
||||
free(buf);
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
if (meta_field_len(buf + 16, X52_LAYOUT_NAME_FIELD_BYTES) == 0) {
|
||||
free(buf);
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
uint32_t calc = x52_layout_file_crc(buf, len);
|
||||
if (calc != stored_crc) {
|
||||
free(buf);
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
int en = validate_entries(buf, codepoint_limit);
|
||||
if (en != 0) {
|
||||
free(buf);
|
||||
return en;
|
||||
}
|
||||
|
||||
x52_layout *L = (x52_layout *)malloc(sizeof *L);
|
||||
if (L == NULL) {
|
||||
free(buf);
|
||||
return ENOMEM;
|
||||
}
|
||||
L->data = buf;
|
||||
L->size = len;
|
||||
L->codepoint_limit = codepoint_limit;
|
||||
L->flags = flags;
|
||||
L->name = copy_meta_string(buf + 16, X52_LAYOUT_NAME_FIELD_BYTES,
|
||||
(flags & X52_LAYOUT_FLAG_NAME_TRUNCATED) != 0);
|
||||
if (L->name == NULL) {
|
||||
free(L);
|
||||
free(buf);
|
||||
return ENOMEM;
|
||||
}
|
||||
L->description = copy_meta_string(buf + 48, X52_LAYOUT_DESCRIPTION_FIELD_BYTES,
|
||||
(flags & X52_LAYOUT_FLAG_DESCRIPTION_TRUNCATED) != 0);
|
||||
if (L->description == NULL) {
|
||||
free(L->name);
|
||||
free(L);
|
||||
free(buf);
|
||||
return ENOMEM;
|
||||
}
|
||||
*out = L;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void x52_layout_normalize_keyboard_basename(const char *cfg_value, char *out, size_t out_sz, bool *rejected_out)
|
||||
{
|
||||
*rejected_out = false;
|
||||
if (out_sz < 3) {
|
||||
if (out_sz > 0) {
|
||||
out[0] = '\0';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (cfg_value == NULL || cfg_value[0] == '\0') {
|
||||
out[0] = 'u';
|
||||
out[1] = 's';
|
||||
out[2] = '\0';
|
||||
return;
|
||||
}
|
||||
|
||||
if (strchr(cfg_value, '/') != NULL || strchr(cfg_value, '\\') != NULL ||
|
||||
strstr(cfg_value, "..") != NULL) {
|
||||
goto failed_normalization;
|
||||
}
|
||||
|
||||
size_t len = strlen(cfg_value);
|
||||
if (len >= out_sz) {
|
||||
goto failed_normalization;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
unsigned char c = (unsigned char)cfg_value[i];
|
||||
if (!isalnum((int)c) && c != '_' && c != '-') {
|
||||
goto failed_normalization;
|
||||
}
|
||||
}
|
||||
|
||||
memcpy(out, cfg_value, len + 1u);
|
||||
return;
|
||||
|
||||
failed_normalization:
|
||||
*rejected_out = true;
|
||||
out[0] = 'u';
|
||||
out[1] = 's';
|
||||
out[2] = '\0';
|
||||
}
|
||||
|
||||
int x52_layout_join_file_path(char *path, size_t path_sz, const char *datadir, const char *basename)
|
||||
{
|
||||
if (path == NULL || datadir == NULL || basename == NULL || path_sz == 0) {
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
int n = snprintf(path, path_sz, "%s/x52d/%s.x52l", datadir, basename);
|
||||
if (n < 0) {
|
||||
return EIO;
|
||||
}
|
||||
if ((size_t)n >= path_sz) {
|
||||
return ENAMETOOLONG;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int x52_layout_load_datadir(const char *datadir, const char *basename, x52_layout **out)
|
||||
{
|
||||
char path[PATH_MAX];
|
||||
|
||||
int rc = x52_layout_join_file_path(path, sizeof path, datadir, basename);
|
||||
if (rc != 0) {
|
||||
return rc;
|
||||
}
|
||||
return x52_layout_load_path(path, out);
|
||||
}
|
||||
|
||||
int x52_layout_load_path(const char *path, x52_layout **out)
|
||||
{
|
||||
if (path == NULL) {
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
FILE *fp = fopen(path, "rb");
|
||||
if (fp == NULL) {
|
||||
return errno != 0 ? errno : EIO;
|
||||
}
|
||||
|
||||
if (fseek(fp, 0, SEEK_END) != 0) {
|
||||
int e = errno != 0 ? errno : EIO;
|
||||
fclose(fp);
|
||||
return e;
|
||||
}
|
||||
|
||||
long pos = ftell(fp);
|
||||
if (pos < 0) {
|
||||
int e = errno != 0 ? errno : EIO;
|
||||
fclose(fp);
|
||||
return e;
|
||||
}
|
||||
if (fseek(fp, 0, SEEK_SET) != 0) {
|
||||
int e = errno != 0 ? errno : EIO;
|
||||
fclose(fp);
|
||||
return e;
|
||||
}
|
||||
|
||||
size_t len = (size_t)pos;
|
||||
if ((long)len != pos || pos < (long)X52_LAYOUT_HEADER_BYTES) {
|
||||
fclose(fp);
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
uint8_t *buf = (uint8_t *)malloc(len);
|
||||
if (buf == NULL) {
|
||||
fclose(fp);
|
||||
return ENOMEM;
|
||||
}
|
||||
|
||||
size_t n = fread(buf, 1, len, fp);
|
||||
fclose(fp);
|
||||
if (n != len) {
|
||||
free(buf);
|
||||
return EIO;
|
||||
}
|
||||
|
||||
return layout_validate_and_adopt(buf, len, out);
|
||||
}
|
||||
|
||||
int x52_layout_load_memory(const void *data, size_t len, x52_layout **out)
|
||||
{
|
||||
if (data == NULL && len != 0) {
|
||||
return EINVAL;
|
||||
}
|
||||
|
||||
uint8_t *buf = (uint8_t *)malloc(len);
|
||||
if (buf == NULL) {
|
||||
return ENOMEM;
|
||||
}
|
||||
if (len != 0) {
|
||||
memcpy(buf, data, len);
|
||||
}
|
||||
return layout_validate_and_adopt(buf, len, out);
|
||||
}
|
||||
|
||||
void x52_layout_free(x52_layout *layout)
|
||||
{
|
||||
if (layout == NULL) {
|
||||
return;
|
||||
}
|
||||
free(layout->name);
|
||||
free(layout->description);
|
||||
free(layout->data);
|
||||
free(layout);
|
||||
}
|
||||
|
||||
uint32_t x52_layout_codepoint_limit(const x52_layout *layout)
|
||||
{
|
||||
if (layout == NULL) {
|
||||
return 0;
|
||||
}
|
||||
return layout->codepoint_limit;
|
||||
}
|
||||
|
||||
uint16_t x52_layout_flags(const x52_layout *layout)
|
||||
{
|
||||
if (layout == NULL) {
|
||||
return 0;
|
||||
}
|
||||
return layout->flags;
|
||||
}
|
||||
|
||||
const char *x52_layout_name(const x52_layout *layout)
|
||||
{
|
||||
if (layout == NULL || layout->name == NULL) {
|
||||
return "";
|
||||
}
|
||||
return layout->name;
|
||||
}
|
||||
|
||||
const char *x52_layout_description(const x52_layout *layout)
|
||||
{
|
||||
if (layout == NULL || layout->description == NULL) {
|
||||
return "";
|
||||
}
|
||||
return layout->description;
|
||||
}
|
||||
|
||||
bool x52_layout_lookup(const x52_layout *layout, uint32_t code_point, uint8_t *modifiers_out,
|
||||
uint8_t *usage_key_out)
|
||||
{
|
||||
if (layout == NULL || modifiers_out == NULL || usage_key_out == NULL) {
|
||||
return false;
|
||||
}
|
||||
if (code_point >= layout->codepoint_limit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t off = X52_LAYOUT_HEADER_BYTES + 2u * (size_t)code_point;
|
||||
uint8_t mod = layout->data[off];
|
||||
uint8_t usage = layout->data[off + 1u];
|
||||
if (usage == 0) {
|
||||
return false;
|
||||
}
|
||||
*modifiers_out = mod;
|
||||
*usage_key_out = usage;
|
||||
return true;
|
||||
}
|
||||
|
|
@ -0,0 +1,463 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - x52layout loader tests
|
||||
*
|
||||
* Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "build-config.h"
|
||||
#include <errno.h>
|
||||
#include <limits.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdarg.h>
|
||||
#include <setjmp.h>
|
||||
#include <cmocka.h>
|
||||
|
||||
#include <daemon/crc32.h>
|
||||
#include <daemon/layout_format.h>
|
||||
#include <vkm/vkm.h>
|
||||
|
||||
static void write_be16(uint8_t *p, uint16_t v)
|
||||
{
|
||||
p[0] = (uint8_t)(v >> 8);
|
||||
p[1] = (uint8_t)v;
|
||||
}
|
||||
|
||||
static void write_be32(uint8_t *p, uint32_t v)
|
||||
{
|
||||
p[0] = (uint8_t)(v >> 24);
|
||||
p[1] = (uint8_t)(v >> 16);
|
||||
p[2] = (uint8_t)(v >> 8);
|
||||
p[3] = (uint8_t)v;
|
||||
}
|
||||
|
||||
static uint32_t read_be32(const uint8_t *p)
|
||||
{
|
||||
return (uint32_t)p[0] << 24 | (uint32_t)p[1] << 16 | (uint32_t)p[2] << 8 | (uint32_t)p[3];
|
||||
}
|
||||
|
||||
/** Patch checksum (offsets 12..15) after building the rest; @p len is full file size. */
|
||||
static void finalize_crc(uint8_t *buf, size_t len)
|
||||
{
|
||||
uint32_t crc = x52_crc32_init();
|
||||
crc = x52_crc32_update(crc, buf, 12u);
|
||||
static const uint8_t z[4] = {0, 0, 0, 0};
|
||||
crc = x52_crc32_update(crc, z, 4u);
|
||||
if (len > 16u) {
|
||||
crc = x52_crc32_update(crc, buf + 16u, len - 16u);
|
||||
}
|
||||
write_be32(buf + 12, crc);
|
||||
}
|
||||
|
||||
/** Python @c zlib.crc32 over @c minimal v1 layout (@c limit=1, name @c "x", empty entry). */
|
||||
#define X52_LAYOUT_TEST_MINIMAL_ZLIB_CRC32 0xc951bfaau
|
||||
|
||||
static void test_load_minimal_lookup(void **state)
|
||||
{
|
||||
(void)state;
|
||||
const uint32_t limit = 98u; /* enough for 'a' at 97 */
|
||||
size_t len = X52_LAYOUT_HEADER_BYTES + 2u * (size_t)limit;
|
||||
uint8_t *buf = (uint8_t *)calloc(1, len);
|
||||
assert_non_null(buf);
|
||||
|
||||
memcpy(buf, "X52L", 4);
|
||||
write_be16(buf + 4, X52_LAYOUT_FORMAT_VERSION);
|
||||
write_be16(buf + 6, 0);
|
||||
write_be32(buf + 8, limit);
|
||||
memcpy(buf + 16, "minimal", 8);
|
||||
/* chord for 'a': VKM_KEY_A */
|
||||
size_t off = X52_LAYOUT_HEADER_BYTES + 2u * 97u;
|
||||
buf[off] = 0x02; /* LSHIFT */
|
||||
buf[off + 1] = (uint8_t)VKM_KEY_A;
|
||||
finalize_crc(buf, len);
|
||||
|
||||
x52_layout *L = NULL;
|
||||
assert_int_equal(x52_layout_load_memory(buf, len, &L), 0);
|
||||
assert_non_null(L);
|
||||
assert_int_equal((int)x52_layout_codepoint_limit(L), (int)limit);
|
||||
|
||||
uint8_t m = 0xff;
|
||||
uint8_t u = 0xff;
|
||||
assert_true(x52_layout_lookup(L, 97u, &m, &u));
|
||||
assert_int_equal((int)m, 0x02);
|
||||
assert_int_equal((int)u, (int)VKM_KEY_A);
|
||||
|
||||
m = 0;
|
||||
u = 0;
|
||||
assert_false(x52_layout_lookup(L, 0u, &m, &u));
|
||||
assert_false(x52_layout_lookup(L, limit, &m, &u));
|
||||
|
||||
assert_int_equal((int)x52_layout_flags(L), 0);
|
||||
assert_string_equal(x52_layout_name(L), "minimal");
|
||||
assert_string_equal(x52_layout_description(L), "");
|
||||
|
||||
x52_layout_free(L);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_reject_bad_checksum(void **state)
|
||||
{
|
||||
(void)state;
|
||||
const uint32_t limit = 1u;
|
||||
size_t len = X52_LAYOUT_HEADER_BYTES + 2u;
|
||||
uint8_t *buf = (uint8_t *)calloc(1, len);
|
||||
assert_non_null(buf);
|
||||
memcpy(buf, "X52L", 4);
|
||||
write_be16(buf + 4, X52_LAYOUT_FORMAT_VERSION);
|
||||
write_be16(buf + 6, 0);
|
||||
write_be32(buf + 8, limit);
|
||||
memcpy(buf + 16, "x", 2);
|
||||
write_be32(buf + 12, 0xdeadbeefu);
|
||||
|
||||
x52_layout *L = NULL;
|
||||
assert_int_equal(x52_layout_load_memory(buf, len, &L), EINVAL);
|
||||
assert_null(L);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_layout_crc_matches_python_zlib_minimal(void **state)
|
||||
{
|
||||
(void)state;
|
||||
const uint32_t limit = 1u;
|
||||
size_t len = X52_LAYOUT_HEADER_BYTES + 2u;
|
||||
uint8_t *buf = (uint8_t *)calloc(1, len);
|
||||
assert_non_null(buf);
|
||||
memcpy(buf, "X52L", 4);
|
||||
write_be16(buf + 4, X52_LAYOUT_FORMAT_VERSION);
|
||||
write_be16(buf + 6, 0);
|
||||
write_be32(buf + 8, limit);
|
||||
memcpy(buf + 16, "x", 2);
|
||||
finalize_crc(buf, len);
|
||||
assert_int_equal((int)read_be32(buf + 12), (int)X52_LAYOUT_TEST_MINIMAL_ZLIB_CRC32);
|
||||
|
||||
x52_layout *L = NULL;
|
||||
assert_int_equal(x52_layout_load_memory(buf, len, &L), 0);
|
||||
assert_non_null(L);
|
||||
assert_string_equal(x52_layout_name(L), "x");
|
||||
x52_layout_free(L);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_reject_tampered_checksum_byte(void **state)
|
||||
{
|
||||
(void)state;
|
||||
const uint32_t limit = 1u;
|
||||
size_t len = X52_LAYOUT_HEADER_BYTES + 2u;
|
||||
uint8_t *buf = (uint8_t *)calloc(1, len);
|
||||
assert_non_null(buf);
|
||||
memcpy(buf, "X52L", 4);
|
||||
write_be16(buf + 4, X52_LAYOUT_FORMAT_VERSION);
|
||||
write_be16(buf + 6, 0);
|
||||
write_be32(buf + 8, limit);
|
||||
memcpy(buf + 16, "x", 2);
|
||||
finalize_crc(buf, len);
|
||||
buf[12] ^= 0x01u;
|
||||
|
||||
x52_layout *L = NULL;
|
||||
assert_int_equal(x52_layout_load_memory(buf, len, &L), EINVAL);
|
||||
assert_null(L);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_reject_codepoint_limit_not_big_endian(void **state)
|
||||
{
|
||||
(void)state;
|
||||
/* Little-endian uint32_t 1 in the codepoint_limit field: read_be32 → 0x01000000. */
|
||||
const uint32_t limit = 1u;
|
||||
size_t len = X52_LAYOUT_HEADER_BYTES + 2u * (size_t)limit;
|
||||
uint8_t *buf = (uint8_t *)calloc(1, len);
|
||||
assert_non_null(buf);
|
||||
memcpy(buf, "X52L", 4);
|
||||
write_be16(buf + 4, X52_LAYOUT_FORMAT_VERSION);
|
||||
write_be16(buf + 6, 0);
|
||||
buf[8] = 0x01u;
|
||||
buf[9] = 0x00u;
|
||||
buf[10] = 0x00u;
|
||||
buf[11] = 0x00u;
|
||||
memcpy(buf + 16, "x", 2);
|
||||
finalize_crc(buf, len);
|
||||
|
||||
x52_layout *L = NULL;
|
||||
assert_int_equal(x52_layout_load_memory(buf, len, &L), EINVAL);
|
||||
assert_null(L);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_reject_version_word_not_big_endian_one(void **state)
|
||||
{
|
||||
(void)state;
|
||||
const uint32_t limit = 1u;
|
||||
size_t len = X52_LAYOUT_HEADER_BYTES + 2u;
|
||||
uint8_t *buf = (uint8_t *)calloc(1, len);
|
||||
assert_non_null(buf);
|
||||
memcpy(buf, "X52L", 4);
|
||||
/* Native little-endian 0x0001 would appear as 01 00 — not BE version 1 (00 01). */
|
||||
buf[4] = 0x01u;
|
||||
buf[5] = 0x00u;
|
||||
write_be16(buf + 6, 0);
|
||||
write_be32(buf + 8, limit);
|
||||
memcpy(buf + 16, "x", 2);
|
||||
finalize_crc(buf, len);
|
||||
|
||||
x52_layout *L = NULL;
|
||||
assert_int_equal(x52_layout_load_memory(buf, len, &L), EINVAL);
|
||||
assert_null(L);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_reject_size_mismatch(void **state)
|
||||
{
|
||||
(void)state;
|
||||
const uint32_t limit = 4u;
|
||||
size_t len = X52_LAYOUT_HEADER_BYTES + 2u * (size_t)limit;
|
||||
uint8_t *buf = (uint8_t *)calloc(1, len);
|
||||
assert_non_null(buf);
|
||||
memcpy(buf, "X52L", 4);
|
||||
write_be16(buf + 4, X52_LAYOUT_FORMAT_VERSION);
|
||||
write_be16(buf + 6, 0);
|
||||
write_be32(buf + 8, limit);
|
||||
memcpy(buf + 16, "x", 2);
|
||||
finalize_crc(buf, len);
|
||||
|
||||
x52_layout *L = NULL;
|
||||
assert_int_equal(x52_layout_load_memory(buf, len - 1u, &L), EINVAL);
|
||||
assert_null(L);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_reject_disallowed_usage(void **state)
|
||||
{
|
||||
(void)state;
|
||||
const uint32_t limit = 1u;
|
||||
size_t len = X52_LAYOUT_HEADER_BYTES + 2u;
|
||||
uint8_t *buf = (uint8_t *)calloc(1, len);
|
||||
assert_non_null(buf);
|
||||
memcpy(buf, "X52L", 4);
|
||||
write_be16(buf + 4, X52_LAYOUT_FORMAT_VERSION);
|
||||
write_be16(buf + 6, 0);
|
||||
write_be32(buf + 8, limit);
|
||||
memcpy(buf + 16, "x", 2);
|
||||
buf[X52_LAYOUT_HEADER_BYTES + 1u] = 0x3A; /* VKM_KEY_F1 — not in allowlist */
|
||||
finalize_crc(buf, len);
|
||||
|
||||
x52_layout *L = NULL;
|
||||
assert_int_equal(x52_layout_load_memory(buf, len, &L), EINVAL);
|
||||
assert_null(L);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_metadata_plain(void **state)
|
||||
{
|
||||
(void)state;
|
||||
const uint32_t limit = 1u;
|
||||
size_t len = X52_LAYOUT_HEADER_BYTES + 2u;
|
||||
uint8_t *buf = (uint8_t *)calloc(1, len);
|
||||
assert_non_null(buf);
|
||||
memcpy(buf, "X52L", 4);
|
||||
write_be16(buf + 4, X52_LAYOUT_FORMAT_VERSION);
|
||||
write_be16(buf + 6, 0);
|
||||
write_be32(buf + 8, limit);
|
||||
memcpy(buf + 16, "us", 3);
|
||||
memcpy(buf + 48, "US QWERTY", 10);
|
||||
finalize_crc(buf, len);
|
||||
|
||||
x52_layout *L = NULL;
|
||||
assert_int_equal(x52_layout_load_memory(buf, len, &L), 0);
|
||||
assert_non_null(L);
|
||||
assert_string_equal(x52_layout_name(L), "us");
|
||||
assert_string_equal(x52_layout_description(L), "US QWERTY");
|
||||
x52_layout_free(L);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_metadata_truncated_suffix(void **state)
|
||||
{
|
||||
(void)state;
|
||||
const uint32_t limit = 1u;
|
||||
size_t len = X52_LAYOUT_HEADER_BYTES + 2u;
|
||||
uint8_t *buf = (uint8_t *)calloc(1, len);
|
||||
assert_non_null(buf);
|
||||
memcpy(buf, "X52L", 4);
|
||||
write_be16(buf + 4, X52_LAYOUT_FORMAT_VERSION);
|
||||
write_be16(buf + 6,
|
||||
(uint16_t)(X52_LAYOUT_FLAG_NAME_TRUNCATED | X52_LAYOUT_FLAG_DESCRIPTION_TRUNCATED));
|
||||
write_be32(buf + 8, limit);
|
||||
memcpy(buf + 16, "longish", 8);
|
||||
memcpy(buf + 48, "desc", 5);
|
||||
finalize_crc(buf, len);
|
||||
|
||||
x52_layout *L = NULL;
|
||||
assert_int_equal(x52_layout_load_memory(buf, len, &L), 0);
|
||||
assert_non_null(L);
|
||||
assert_int_equal((int)x52_layout_flags(L),
|
||||
(int)(X52_LAYOUT_FLAG_NAME_TRUNCATED | X52_LAYOUT_FLAG_DESCRIPTION_TRUNCATED));
|
||||
assert_string_equal(x52_layout_name(L), "longish<truncated>");
|
||||
assert_string_equal(x52_layout_description(L), "desc<truncated>");
|
||||
x52_layout_free(L);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_metadata_name_truncated_flag_only(void **state)
|
||||
{
|
||||
(void)state;
|
||||
const uint32_t limit = 1u;
|
||||
size_t len = X52_LAYOUT_HEADER_BYTES + 2u;
|
||||
uint8_t *buf = (uint8_t *)calloc(1, len);
|
||||
assert_non_null(buf);
|
||||
memcpy(buf, "X52L", 4);
|
||||
write_be16(buf + 4, X52_LAYOUT_FORMAT_VERSION);
|
||||
write_be16(buf + 6, X52_LAYOUT_FLAG_NAME_TRUNCATED);
|
||||
write_be32(buf + 8, limit);
|
||||
memcpy(buf + 16, "nm", 3);
|
||||
memcpy(buf + 48, "plain", 6);
|
||||
finalize_crc(buf, len);
|
||||
|
||||
x52_layout *L = NULL;
|
||||
assert_int_equal(x52_layout_load_memory(buf, len, &L), 0);
|
||||
assert_non_null(L);
|
||||
assert_int_equal((int)x52_layout_flags(L), (int)X52_LAYOUT_FLAG_NAME_TRUNCATED);
|
||||
assert_string_equal(x52_layout_name(L), "nm<truncated>");
|
||||
assert_string_equal(x52_layout_description(L), "plain");
|
||||
x52_layout_free(L);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_reject_unknown_flags(void **state)
|
||||
{
|
||||
(void)state;
|
||||
const uint32_t limit = 1u;
|
||||
size_t len = X52_LAYOUT_HEADER_BYTES + 2u;
|
||||
uint8_t *buf = (uint8_t *)calloc(1, len);
|
||||
assert_non_null(buf);
|
||||
memcpy(buf, "X52L", 4);
|
||||
write_be16(buf + 4, X52_LAYOUT_FORMAT_VERSION);
|
||||
write_be16(buf + 6, 0x8000);
|
||||
write_be32(buf + 8, limit);
|
||||
memcpy(buf + 16, "x", 2);
|
||||
finalize_crc(buf, len);
|
||||
|
||||
x52_layout *L = NULL;
|
||||
assert_int_equal(x52_layout_load_memory(buf, len, &L), EINVAL);
|
||||
assert_null(L);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_reject_empty_name(void **state)
|
||||
{
|
||||
(void)state;
|
||||
const uint32_t limit = 1u;
|
||||
size_t len = X52_LAYOUT_HEADER_BYTES + 2u;
|
||||
uint8_t *buf = (uint8_t *)calloc(1, len);
|
||||
assert_non_null(buf);
|
||||
memcpy(buf, "X52L", 4);
|
||||
write_be16(buf + 4, X52_LAYOUT_FORMAT_VERSION);
|
||||
write_be16(buf + 6, 0);
|
||||
write_be32(buf + 8, limit);
|
||||
finalize_crc(buf, len);
|
||||
|
||||
x52_layout *L = NULL;
|
||||
assert_int_equal(x52_layout_load_memory(buf, len, &L), EINVAL);
|
||||
assert_null(L);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_basename_normalize_and_join(void **state)
|
||||
{
|
||||
(void)state;
|
||||
char out[256];
|
||||
bool rej;
|
||||
|
||||
x52_layout_normalize_keyboard_basename(NULL, out, sizeof out, &rej);
|
||||
assert_false(rej);
|
||||
assert_string_equal(out, "us");
|
||||
|
||||
x52_layout_normalize_keyboard_basename("", out, sizeof out, &rej);
|
||||
assert_false(rej);
|
||||
assert_string_equal(out, "us");
|
||||
|
||||
x52_layout_normalize_keyboard_basename("de", out, sizeof out, &rej);
|
||||
assert_false(rej);
|
||||
assert_string_equal(out, "de");
|
||||
|
||||
x52_layout_normalize_keyboard_basename("ab_cd-9", out, sizeof out, &rej);
|
||||
assert_false(rej);
|
||||
assert_string_equal(out, "ab_cd-9");
|
||||
|
||||
x52_layout_normalize_keyboard_basename("../x", out, sizeof out, &rej);
|
||||
assert_true(rej);
|
||||
assert_string_equal(out, "us");
|
||||
|
||||
x52_layout_normalize_keyboard_basename("a/b", out, sizeof out, &rej);
|
||||
assert_true(rej);
|
||||
assert_string_equal(out, "us");
|
||||
|
||||
x52_layout_normalize_keyboard_basename("bad name", out, sizeof out, &rej);
|
||||
assert_true(rej);
|
||||
assert_string_equal(out, "us");
|
||||
|
||||
x52_layout_normalize_keyboard_basename("a\\b", out, sizeof out, &rej);
|
||||
assert_true(rej);
|
||||
assert_string_equal(out, "us");
|
||||
|
||||
x52_layout_normalize_keyboard_basename("x..y", out, sizeof out, &rej);
|
||||
assert_true(rej);
|
||||
assert_string_equal(out, "us");
|
||||
|
||||
memset(out, 0, sizeof out);
|
||||
x52_layout_normalize_keyboard_basename("almost..", out, sizeof out, &rej);
|
||||
assert_true(rej);
|
||||
assert_string_equal(out, "us");
|
||||
|
||||
char path[PATH_MAX];
|
||||
assert_int_equal(x52_layout_join_file_path(path, sizeof path, "/usr/share", "us"), 0);
|
||||
assert_string_equal(path, "/usr/share/x52d/us.x52l");
|
||||
assert_int_equal(x52_layout_join_file_path(path, 20, "/usr/share", "us"), ENAMETOOLONG);
|
||||
}
|
||||
|
||||
static void test_reject_version(void **state)
|
||||
{
|
||||
(void)state;
|
||||
const uint32_t limit = 1u;
|
||||
size_t len = X52_LAYOUT_HEADER_BYTES + 2u;
|
||||
uint8_t *buf = (uint8_t *)calloc(1, len);
|
||||
assert_non_null(buf);
|
||||
memcpy(buf, "X52L", 4);
|
||||
write_be16(buf + 4, 99);
|
||||
write_be16(buf + 6, 0);
|
||||
write_be32(buf + 8, limit);
|
||||
memcpy(buf + 16, "v", 2);
|
||||
finalize_crc(buf, len);
|
||||
|
||||
x52_layout *L = NULL;
|
||||
assert_int_equal(x52_layout_load_memory(buf, len, &L), EINVAL);
|
||||
assert_null(L);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_load_minimal_lookup),
|
||||
cmocka_unit_test(test_metadata_plain),
|
||||
cmocka_unit_test(test_metadata_truncated_suffix),
|
||||
cmocka_unit_test(test_metadata_name_truncated_flag_only),
|
||||
cmocka_unit_test(test_reject_unknown_flags),
|
||||
cmocka_unit_test(test_reject_empty_name),
|
||||
cmocka_unit_test(test_reject_bad_checksum),
|
||||
cmocka_unit_test(test_layout_crc_matches_python_zlib_minimal),
|
||||
cmocka_unit_test(test_reject_tampered_checksum_byte),
|
||||
cmocka_unit_test(test_reject_codepoint_limit_not_big_endian),
|
||||
cmocka_unit_test(test_reject_version_word_not_big_endian_one),
|
||||
cmocka_unit_test(test_reject_size_mismatch),
|
||||
cmocka_unit_test(test_reject_disallowed_usage),
|
||||
cmocka_unit_test(test_reject_version),
|
||||
cmocka_unit_test(test_basename_normalize_and_join),
|
||||
};
|
||||
|
||||
cmocka_set_message_output(CM_OUTPUT_TAP);
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - Keyboard layout HID usage allowlist
|
||||
*
|
||||
* Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include <daemon/layout_usage_allowlist.h>
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <vkm/vkm.h>
|
||||
|
||||
/* layout binary and compiler must stay aligned with vkm_key numeric values. */
|
||||
_Static_assert((unsigned)VKM_KEY_A == 0x04u, "vkm_key main block start");
|
||||
_Static_assert((unsigned)VKM_KEY_CAPS_LOCK == 0x39u, "vkm_key main block end");
|
||||
_Static_assert((unsigned)VKM_KEY_INTL_BACKSLASH == 0x64u, "vkm_key ISO backslash");
|
||||
|
||||
bool x52_layout_usage_key_allowed(uint8_t usage)
|
||||
{
|
||||
if (usage == 0) {
|
||||
return false;
|
||||
}
|
||||
if (usage >= 0x04 && usage <= 0x39) {
|
||||
return true;
|
||||
}
|
||||
if (usage == 0x64) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - Keyboard layout HID usage allowlist
|
||||
*
|
||||
* Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file layout_usage_allowlist.h
|
||||
* @brief HID keyboard/page (0x07) usages permitted as layout chord \c usage_key bytes.
|
||||
*
|
||||
* The set is the USB HID "main block" (usages @c 0x04-@c 0x39, i.e. @c VKM_KEY_A
|
||||
* through @c VKM_KEY_CAPS_LOCK) plus @c VKM_KEY_INTL_BACKSLASH (@c 0x64). It excludes
|
||||
* @c VKM_KEY_NONE, modifiers, function row, navigation cluster, keypad, and all other
|
||||
* @ref vkm_key values; same rule as @c tools/x52compile_layout.py.
|
||||
*/
|
||||
#ifndef X52D_LAYOUT_USAGE_ALLOWLIST_H
|
||||
#define X52D_LAYOUT_USAGE_ALLOWLIST_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Return whether @p usage may appear as the non-modifier byte in a layout entry.
|
||||
*
|
||||
* @param usage HID usage ID (page 0x07), same encoding as @ref vkm_key.
|
||||
*
|
||||
* @returns true if @p usage is in the main-block allowlist; false for @c VKM_KEY_NONE
|
||||
* and for any disallowed usage.
|
||||
*/
|
||||
bool x52_layout_usage_key_allowed(uint8_t usage);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* X52D_LAYOUT_USAGE_ALLOWLIST_H */
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - layout HID usage allowlist tests
|
||||
*
|
||||
* Copyright (C) 2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "build-config.h"
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <setjmp.h>
|
||||
#include <cmocka.h>
|
||||
|
||||
#include <daemon/layout_usage_allowlist.h>
|
||||
#include <vkm/vkm.h>
|
||||
|
||||
static void test_allows_main_block(void **state)
|
||||
{
|
||||
(void)state;
|
||||
|
||||
assert_true(x52_layout_usage_key_allowed(VKM_KEY_A));
|
||||
assert_true(x52_layout_usage_key_allowed(VKM_KEY_Z));
|
||||
assert_true(x52_layout_usage_key_allowed(VKM_KEY_1));
|
||||
assert_true(x52_layout_usage_key_allowed(VKM_KEY_0));
|
||||
assert_true(x52_layout_usage_key_allowed(VKM_KEY_SPACE));
|
||||
assert_true(x52_layout_usage_key_allowed(VKM_KEY_CAPS_LOCK));
|
||||
assert_true(x52_layout_usage_key_allowed(VKM_KEY_INTL_BACKSLASH));
|
||||
assert_true(x52_layout_usage_key_allowed(VKM_KEY_NONUS_HASH));
|
||||
}
|
||||
|
||||
static void test_rejects_disallowed(void **state)
|
||||
{
|
||||
(void)state;
|
||||
|
||||
assert_false(x52_layout_usage_key_allowed(0));
|
||||
assert_false(x52_layout_usage_key_allowed(VKM_KEY_F1));
|
||||
assert_false(x52_layout_usage_key_allowed(VKM_KEY_LEFT_CTRL));
|
||||
assert_false(x52_layout_usage_key_allowed(VKM_KEY_KEYPAD_1));
|
||||
assert_false(x52_layout_usage_key_allowed(0x03));
|
||||
assert_false(x52_layout_usage_key_allowed(0x3A));
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_allows_main_block),
|
||||
cmocka_unit_test(test_rejects_disallowed),
|
||||
};
|
||||
|
||||
cmocka_set_message_output(CM_OUTPUT_TAP);
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - Clock manager
|
||||
*
|
||||
* Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "build-config.h"
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <pthread.h>
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
|
||||
#define PINELOG_MODULE X52D_MOD_LED
|
||||
#include "pinelog.h"
|
||||
#include <daemon/config.h>
|
||||
#include <daemon/constants.h>
|
||||
#include <daemon/device.h>
|
||||
|
||||
#define SET_LED_STATE(led, state) \
|
||||
PINELOG_TRACE("Setting LED %s state to %s (%d)", \
|
||||
libx52_led_id_to_str(LIBX52_LED_ ## led), \
|
||||
libx52_led_state_to_str(state), state); \
|
||||
x52d_dev_set_led_state(LIBX52_LED_ ## led, state);
|
||||
|
||||
void x52d_cfg_set_LED_Fire(libx52_led_state state)
|
||||
{
|
||||
SET_LED_STATE(FIRE, state);
|
||||
}
|
||||
|
||||
void x52d_cfg_set_LED_Throttle(libx52_led_state state)
|
||||
{
|
||||
SET_LED_STATE(THROTTLE, state);
|
||||
}
|
||||
|
||||
void x52d_cfg_set_LED_A(libx52_led_state state)
|
||||
{
|
||||
SET_LED_STATE(A, state);
|
||||
}
|
||||
|
||||
void x52d_cfg_set_LED_B(libx52_led_state state)
|
||||
{
|
||||
SET_LED_STATE(B, state);
|
||||
}
|
||||
|
||||
void x52d_cfg_set_LED_D(libx52_led_state state)
|
||||
{
|
||||
SET_LED_STATE(D, state);
|
||||
}
|
||||
|
||||
void x52d_cfg_set_LED_E(libx52_led_state state)
|
||||
{
|
||||
SET_LED_STATE(E, state);
|
||||
}
|
||||
|
||||
void x52d_cfg_set_LED_T1(libx52_led_state state)
|
||||
{
|
||||
SET_LED_STATE(T1, state);
|
||||
}
|
||||
|
||||
void x52d_cfg_set_LED_T2(libx52_led_state state)
|
||||
{
|
||||
SET_LED_STATE(T2, state);
|
||||
}
|
||||
|
||||
void x52d_cfg_set_LED_T3(libx52_led_state state)
|
||||
{
|
||||
SET_LED_STATE(T3, state);
|
||||
}
|
||||
|
||||
void x52d_cfg_set_LED_POV(libx52_led_state state)
|
||||
{
|
||||
SET_LED_STATE(POV, state);
|
||||
}
|
||||
|
||||
void x52d_cfg_set_LED_Clutch(libx52_led_state state)
|
||||
{
|
||||
SET_LED_STATE(CLUTCH, state);
|
||||
}
|
||||
|
||||
#define SET_BRIGHTNESS(mfd, brightness) \
|
||||
PINELOG_TRACE("Setting %s brightness to %u", mfd ? "MFD" : "LED", brightness); \
|
||||
x52d_dev_set_brightness(mfd, brightness);
|
||||
|
||||
void x52d_cfg_set_Brightness_MFD(uint16_t brightness)
|
||||
{
|
||||
SET_BRIGHTNESS(1, brightness);
|
||||
}
|
||||
|
||||
void x52d_cfg_set_Brightness_LED(uint16_t brightness)
|
||||
{
|
||||
SET_BRIGHTNESS(0, brightness);
|
||||
}
|
||||
|
|
@ -0,0 +1,384 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - Service daemon
|
||||
*
|
||||
* Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "build-config.h"
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <signal.h>
|
||||
#include <inttypes.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include <daemon/clock.h>
|
||||
#include <daemon/constants.h>
|
||||
#include <daemon/config.h>
|
||||
#include <daemon/device.h>
|
||||
#include <daemon/io.h>
|
||||
#include <daemon/mouse.h>
|
||||
#include <daemon/command.h>
|
||||
#include <daemon/notify.h>
|
||||
#include <daemon/x52dcomm-internal.h>
|
||||
#include <daemon/keyboard_layout.h>
|
||||
#include <libx52/x52dcomm.h>
|
||||
#include "pinelog.h"
|
||||
|
||||
static volatile int flag_quit;
|
||||
|
||||
static void termination_handler(int signum)
|
||||
{
|
||||
flag_quit = signum;
|
||||
}
|
||||
|
||||
static volatile bool flag_reload;
|
||||
static void reload_handler(int signum)
|
||||
{
|
||||
(void)signum;
|
||||
flag_reload = true;
|
||||
}
|
||||
|
||||
static volatile bool flag_save_cfg;
|
||||
static void save_config_handler(int signum)
|
||||
{
|
||||
(void)signum;
|
||||
flag_save_cfg = true;
|
||||
}
|
||||
|
||||
static void set_log_file(bool foreground, const char *log_file)
|
||||
{
|
||||
int rc = 0;
|
||||
if (log_file != NULL) {
|
||||
rc = pinelog_set_output_file(log_file);
|
||||
} else {
|
||||
if (foreground) {
|
||||
rc = pinelog_set_output_stream(stdout);
|
||||
} else {
|
||||
rc = pinelog_set_output_file(X52D_LOG_FILE);
|
||||
}
|
||||
}
|
||||
|
||||
if (rc != 0) {
|
||||
fprintf(stderr, _("Error %d setting log file: %s\n"), rc, strerror(rc));
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
static void listen_signal(int signum, void (*handler)(int))
|
||||
{
|
||||
struct sigaction action;
|
||||
int rc;
|
||||
|
||||
action.sa_handler = handler;
|
||||
sigemptyset(&action.sa_mask);
|
||||
action.sa_flags = SA_RESTART;
|
||||
|
||||
rc = sigaction(signum, &action, NULL);
|
||||
if (rc < 0) {
|
||||
PINELOG_FATAL(_("Error %d installing handler for signal %d: %s"),
|
||||
errno, signum, strerror(errno));
|
||||
}
|
||||
}
|
||||
|
||||
#if HAVE_FUNC_ATTRIBUTE_NORETURN
|
||||
__attribute__((noreturn))
|
||||
#endif
|
||||
static void usage(int exit_code)
|
||||
{
|
||||
fprintf(stderr,
|
||||
_("Usage: %s [-f] [-v] [-q]\n"
|
||||
"\t[-l log-file] [-o override]\n"
|
||||
"\t[-c config-file] [-p pid-file]\n"
|
||||
"\t[-s command-socket-path]\n"
|
||||
"\t[-b notify-socket-path]\n"),
|
||||
X52D_APP_NAME);
|
||||
exit(exit_code);
|
||||
}
|
||||
|
||||
static void start_daemon(bool foreground, const char *pid_file)
|
||||
{
|
||||
pid_t pid;
|
||||
FILE *pid_fd;
|
||||
|
||||
if (pid_file == NULL) {
|
||||
pid_file = X52D_PID_FILE;
|
||||
}
|
||||
|
||||
if (!foreground) {
|
||||
/* Check if there is an existing daemon process running */
|
||||
pid_fd = fopen(pid_file, "r");
|
||||
if (pid_fd != NULL) {
|
||||
int rc;
|
||||
intmax_t tmp_pid;
|
||||
|
||||
/* File exists, read the PID and check if it exists */
|
||||
rc = fscanf(pid_fd, "%" SCNdMAX, &tmp_pid);
|
||||
fclose(pid_fd);
|
||||
|
||||
if (rc != 1) {
|
||||
perror("fscanf");
|
||||
} else {
|
||||
pid = (pid_t)tmp_pid;
|
||||
rc = kill(pid, 0);
|
||||
if (rc == 0 || (rc < 0 && errno == EPERM)) {
|
||||
PINELOG_FATAL(_("Daemon is already running as PID %u"), pid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Fork off the parent process */
|
||||
pid = fork();
|
||||
if (pid < 0) {
|
||||
/* Error occurred during first fork */
|
||||
perror("fork");
|
||||
exit(EXIT_FAILURE);
|
||||
} else if (pid > 0) {
|
||||
/* Terminate the parent process */
|
||||
exit(EXIT_SUCCESS);
|
||||
}
|
||||
|
||||
/* Make child process a session leader */
|
||||
if (setsid() < 0) {
|
||||
perror("setsid");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
/* Initialize signal handlers. This step is the same whether in foreground
|
||||
* or background mode
|
||||
*/
|
||||
listen_signal(SIGINT, termination_handler);
|
||||
listen_signal(SIGTERM, termination_handler);
|
||||
listen_signal(SIGQUIT, termination_handler);
|
||||
listen_signal(SIGHUP, reload_handler);
|
||||
listen_signal(SIGUSR1, save_config_handler);
|
||||
|
||||
if (!foreground) {
|
||||
/* Fork off for the second time */
|
||||
pid = fork();
|
||||
if (pid < 0) {
|
||||
/* Error occurred during second fork */
|
||||
perror("fork");
|
||||
exit(EXIT_FAILURE);
|
||||
} else if (pid > 0) {
|
||||
/* Terminate the parent */
|
||||
exit(EXIT_SUCCESS);
|
||||
}
|
||||
|
||||
/* Write the PID to the pid_file */
|
||||
pid_fd = fopen(pid_file, "w");
|
||||
if (pid_fd == NULL) {
|
||||
/* Unable to open PID file */
|
||||
perror("fopen");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
if (fprintf(pid_fd, "%u\n", getpid()) < 0) {
|
||||
perror("fprintf");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
if (fclose(pid_fd) != 0) {
|
||||
perror("fclose");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
/* Set new file permissions */
|
||||
umask(0);
|
||||
|
||||
/* Change the working directory */
|
||||
if (chdir("/")) {
|
||||
/* Error changing the directory */
|
||||
perror("chdir");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
/* Close all open file descriptors */
|
||||
for (int x = sysconf(_SC_OPEN_MAX); x >= 0; x--) {
|
||||
close(x);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
int verbosity = 0;
|
||||
bool quiet = false;
|
||||
bool foreground = false;
|
||||
char *log_file = NULL;
|
||||
char *conf_file = NULL;
|
||||
const char *pid_file = NULL;
|
||||
const char *command_sock = NULL;
|
||||
const char *notify_sock = NULL;
|
||||
int opt;
|
||||
int rc;
|
||||
sigset_t sigblockset;
|
||||
|
||||
/* Initialize gettext */
|
||||
#if ENABLE_NLS
|
||||
setlocale(LC_ALL, "");
|
||||
bindtextdomain(PACKAGE, LOCALEDIR);
|
||||
textdomain(PACKAGE);
|
||||
#endif
|
||||
|
||||
/* Set system defaults */
|
||||
pinelog_init(X52D_MOD_MAX);
|
||||
pinelog_set_level(PINELOG_LVL_WARNING);
|
||||
|
||||
/*
|
||||
* Parse command line arguments
|
||||
*
|
||||
* -f run in foreground
|
||||
* -c path to config file
|
||||
* -o option overrides
|
||||
* -v verbose logging
|
||||
* -q silent behavior
|
||||
* -l path to log file
|
||||
* -p path to PID file (only used if running in background)
|
||||
* -s path to command socket
|
||||
* -b path to notify socket
|
||||
*/
|
||||
while ((opt = getopt(argc, argv, "fvql:o:c:p:s:b:h")) != -1) {
|
||||
switch (opt) {
|
||||
case 'f':
|
||||
foreground = true;
|
||||
break;
|
||||
|
||||
case 'v':
|
||||
if (!quiet) {
|
||||
if (verbosity <= PINELOG_LVL_TRACE) {
|
||||
verbosity++;
|
||||
pinelog_set_level(pinelog_get_level() + 1);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'q':
|
||||
quiet = true;
|
||||
pinelog_set_level(PINELOG_LVL_ERROR);
|
||||
break;
|
||||
|
||||
case 'l':
|
||||
log_file = optarg;
|
||||
break;
|
||||
|
||||
case 'o':
|
||||
if (x52d_config_save_override(optarg)) {
|
||||
fprintf(stderr,
|
||||
_("Unable to parse configuration override '%s'\n"),
|
||||
optarg);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'c':
|
||||
conf_file = optarg;
|
||||
break;
|
||||
|
||||
case 'p':
|
||||
pid_file = optarg;
|
||||
break;
|
||||
|
||||
case 's':
|
||||
command_sock = optarg;
|
||||
break;
|
||||
|
||||
case 'b':
|
||||
notify_sock = optarg;
|
||||
break;
|
||||
|
||||
case 'h':
|
||||
usage(EXIT_SUCCESS);
|
||||
break;
|
||||
|
||||
default:
|
||||
usage(EXIT_FAILURE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
PINELOG_DEBUG(_("Foreground = %s"), foreground ? _("true") : _("false"));
|
||||
PINELOG_DEBUG(_("Quiet = %s"), quiet ? _("true") : _("false"));
|
||||
PINELOG_DEBUG(_("Verbosity = %d"), verbosity);
|
||||
PINELOG_DEBUG(_("Log file = %s"), log_file);
|
||||
PINELOG_DEBUG(_("Config file = %s"), conf_file);
|
||||
PINELOG_DEBUG(_("PID file = %s"), pid_file);
|
||||
PINELOG_DEBUG(_("Command socket = %s"), command_sock);
|
||||
PINELOG_DEBUG(_("Notify socket = %s"), notify_sock);
|
||||
|
||||
start_daemon(foreground, pid_file);
|
||||
|
||||
set_log_file(foreground, log_file);
|
||||
x52d_config_load(conf_file);
|
||||
|
||||
// Disable pthread signals
|
||||
sigfillset(&sigblockset);
|
||||
rc = pthread_sigmask(SIG_BLOCK, &sigblockset, NULL);
|
||||
if (rc != 0) {
|
||||
PINELOG_FATAL(_("Error %d blocking signals on child threads: %s"),
|
||||
errno, strerror(errno));
|
||||
}
|
||||
|
||||
// Start device threads
|
||||
x52d_dev_init();
|
||||
x52d_clock_init();
|
||||
if (x52d_command_init(command_sock) < 0) {
|
||||
goto cleanup;
|
||||
}
|
||||
x52d_notify_init(notify_sock);
|
||||
x52d_io_init();
|
||||
x52d_mouse_handler_init();
|
||||
|
||||
// Re-enable signals
|
||||
rc = pthread_sigmask(SIG_UNBLOCK, &sigblockset, NULL);
|
||||
if (rc != 0) {
|
||||
PINELOG_FATAL(_("Error %d unblocking signals on child threads: %s"),
|
||||
errno, strerror(errno));
|
||||
}
|
||||
|
||||
// Apply configuration
|
||||
x52d_config_apply();
|
||||
|
||||
flag_quit = 0;
|
||||
while(!flag_quit) {
|
||||
pause();
|
||||
|
||||
/* Check if we need to reload configuration */
|
||||
if (flag_reload) {
|
||||
PINELOG_INFO(_("Reloading X52 configuration"));
|
||||
x52d_config_load(conf_file);
|
||||
x52d_config_apply();
|
||||
flag_reload = false;
|
||||
}
|
||||
|
||||
if (flag_save_cfg) {
|
||||
PINELOG_INFO(_("Saving X52 configuration to disk"));
|
||||
x52d_config_save(conf_file);
|
||||
flag_save_cfg = false;
|
||||
}
|
||||
}
|
||||
|
||||
PINELOG_INFO(_("Received termination signal %s"), strsignal(flag_quit));
|
||||
|
||||
cleanup:
|
||||
x52d_keyboard_layout_fini();
|
||||
// Stop device threads
|
||||
x52d_clock_exit();
|
||||
x52d_dev_exit();
|
||||
x52d_command_exit();
|
||||
x52d_notify_exit();
|
||||
x52d_mouse_handler_exit();
|
||||
x52d_io_exit();
|
||||
|
||||
// Remove the PID file
|
||||
PINELOG_TRACE("Removing PID file %s", pid_file);
|
||||
unlink(pid_file);
|
||||
|
||||
PINELOG_INFO(_("Shutting down X52 daemon"));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
# x52d (dep_config_h: Meson build-config.h; private API is daemon/config.h)
|
||||
config_defs = custom_target('config-defs',
|
||||
depend_files: ['x52d_map_config.py', 'x52d.conf'],
|
||||
input: [
|
||||
'x52d.conf',
|
||||
'config_registry.json'
|
||||
],
|
||||
output: [
|
||||
'config-defs.h',
|
||||
'config-defs.c',
|
||||
'config_defs.py'
|
||||
],
|
||||
command: [
|
||||
python, meson.current_source_dir() / 'x52d_map_config.py',
|
||||
'@INPUT0@', '@INPUT1@',
|
||||
'@OUTPUT0@', '@OUTPUT1@', '@OUTPUT2@'
|
||||
])
|
||||
|
||||
module_defs = custom_target('module-defs',
|
||||
depend_files: ['x52d_gen_module.py', 'module_defs.py'],
|
||||
output: ['module-map.h', 'module-map.c'],
|
||||
command: [python, meson.current_source_dir() / 'x52d_gen_module.py',
|
||||
'@OUTPUT0@', '@OUTPUT1@'])
|
||||
|
||||
# Header only: ordering on module-map.h without compiling module-map.c per target.
|
||||
dep_module_map_gen = declare_dependency(sources: module_defs[0])
|
||||
|
||||
# Full module_defs: name-id-map.c includes module-map.h (generated with the .c).
|
||||
slib_comm_defs = static_library('x52dcommdefs',
|
||||
config_defs[1],
|
||||
module_defs,
|
||||
'name-id-map.c',
|
||||
)
|
||||
|
||||
libx52dcomm_version = '1.0.0'
|
||||
|
||||
libx52dcomm_sources = [
|
||||
'comm_client.c',
|
||||
'comm_internal.c'
|
||||
]
|
||||
|
||||
root_includes = include_directories('..')
|
||||
|
||||
lib_libx52dcomm = library('x52dcomm', libx52dcomm_sources,
|
||||
dependencies: [dep_intl, dep_config_h, dep_module_map_gen],
|
||||
version: libx52dcomm_version,
|
||||
c_args: sym_hidden_cargs,
|
||||
install: true,
|
||||
include_directories: [includes, root_includes])
|
||||
|
||||
pkgconfig.generate(lib_libx52dcomm,
|
||||
name: 'x52dcomm',
|
||||
description: 'Client library for communicating with the x52d X52 daemon.',
|
||||
version: libx52dcomm_version,
|
||||
)
|
||||
|
||||
x52d_sources = [
|
||||
'main.c',
|
||||
'config_parser.c',
|
||||
'config_dump.c',
|
||||
'config.c',
|
||||
'device.c',
|
||||
'client.c',
|
||||
'clock.c',
|
||||
'mouse.c',
|
||||
'notify.c',
|
||||
'led.c',
|
||||
'command.c',
|
||||
'io.c',
|
||||
'mouse_handler.c',
|
||||
'layout_usage_allowlist.c',
|
||||
'layout_load.c',
|
||||
'keyboard_layout.c',
|
||||
'crc32.c',
|
||||
]
|
||||
|
||||
dep_threads = dependency('threads')
|
||||
|
||||
# Comm sources are compiled into x52d (same as Autotools); libx52dcomm is only for x52ctl.
|
||||
x52d_linkwith = [lib_libx52, lib_vkm, lib_libx52io, slib_comm_defs]
|
||||
x52d_deps = [dep_pinelog, dep_inih, dep_threads, dep_math, dep_intl, dep_config_h,
|
||||
dep_module_map_gen]
|
||||
x52d_cflags = []
|
||||
|
||||
exe_x52d = executable('x52d', x52d_sources + libx52dcomm_sources,
|
||||
install: true,
|
||||
include_directories: [includes, root_includes],
|
||||
c_args: sym_hidden_cargs + x52d_cflags,
|
||||
dependencies: x52d_deps,
|
||||
link_with: x52d_linkwith)
|
||||
|
||||
exe_x52ctl = executable('x52ctl', 'daemon_control.c',
|
||||
install: true,
|
||||
dependencies: [dep_intl, dep_config_h, dep_module_map_gen],
|
||||
include_directories: [includes, root_includes],
|
||||
link_with: lib_libx52dcomm)
|
||||
|
||||
install_data('x52d.conf',
|
||||
install_dir: join_paths(get_option('sysconfdir'), 'x52d'))
|
||||
|
||||
us_x52l = custom_target(
|
||||
'us-x52l',
|
||||
input: files('../data/layouts/us.layout'),
|
||||
output: 'us.x52l',
|
||||
command: [
|
||||
python,
|
||||
join_paths(meson.project_source_root(), 'tools', 'x52compile_layout.py'),
|
||||
'@INPUT@',
|
||||
'@OUTPUT@',
|
||||
],
|
||||
install: true,
|
||||
install_dir: join_paths(get_option('datadir'), 'x52d'))
|
||||
|
||||
test('daemon-communication', files('test_daemon_comm.py')[0],
|
||||
depends: [exe_x52d, exe_x52ctl], protocol: 'tap')
|
||||
|
||||
x52d_mouse_test_sources = ['mouse_test.c', 'mouse.c']
|
||||
x52d_mouse_test = executable('x52d-mouse-test', x52d_mouse_test_sources,
|
||||
include_directories: [includes, root_includes],
|
||||
dependencies: [dep_pinelog, dep_cmocka, dep_intl, dep_math, dep_config_h,
|
||||
dep_module_map_gen])
|
||||
|
||||
test('x52d-mouse-test', x52d_mouse_test, protocol: 'tap')
|
||||
|
||||
layout_usage_allowlist_test = executable('layout-usage-allowlist-test',
|
||||
'layout_usage_allowlist_test.c',
|
||||
'layout_usage_allowlist.c',
|
||||
build_by_default: false,
|
||||
include_directories: [includes, root_includes],
|
||||
dependencies: [dep_cmocka, dep_config_h])
|
||||
|
||||
test('layout-usage-allowlist', layout_usage_allowlist_test, protocol: 'tap')
|
||||
|
||||
crc32_test = executable('crc32-test', 'crc32_test.c', 'crc32.c',
|
||||
build_by_default: false,
|
||||
include_directories: [includes, root_includes],
|
||||
dependencies: [dep_cmocka, dep_config_h])
|
||||
|
||||
test('crc32', crc32_test, protocol: 'tap')
|
||||
|
||||
layout_load_test = executable('layout-load-test',
|
||||
'layout_load_test.c',
|
||||
'layout_load.c',
|
||||
'layout_usage_allowlist.c',
|
||||
'crc32.c',
|
||||
build_by_default: false,
|
||||
include_directories: [includes, root_includes],
|
||||
dependencies: [dep_cmocka, dep_config_h])
|
||||
|
||||
test('layout-load', layout_load_test, protocol: 'tap')
|
||||
|
||||
pymod_daemon = import('python')
|
||||
python_layout_test = pymod_daemon.find_installation('python3')
|
||||
test('layout-usage-allowlist-sync', python_layout_test,
|
||||
args: [join_paths(meson.project_source_root(), 'tools', 'test_layout_allowlist_sync.py')],
|
||||
protocol: 'tap')
|
||||
|
||||
test('layout-compile-py', python_layout_test,
|
||||
args: [join_paths(meson.project_source_root(), 'tools', 'test_x52compile_layout.py')],
|
||||
protocol: 'tap')
|
||||
|
||||
# Install service file
|
||||
if dep_systemd.found()
|
||||
systemd_system_unit_dir = get_option('systemd-unit-dir')
|
||||
if systemd_system_unit_dir == ''
|
||||
systemd_system_unit_dir = dep_systemd.get_variable(
|
||||
pkgconfig: 'systemd_system_unit_dir',
|
||||
default_value: '/lib/systemd/system')
|
||||
endif
|
||||
sed = find_program('sed')
|
||||
bindir_path = get_option('prefix') / get_option('bindir')
|
||||
sed_script = 's|%bindir%|' + bindir_path + '|g'
|
||||
|
||||
systemd_service_file = configure_file(
|
||||
input: 'x52d.service.in',
|
||||
output: 'x52d.service',
|
||||
command: [sed, sed_script, '@INPUT@'],
|
||||
capture: true,
|
||||
install: true,
|
||||
install_dir: systemd_system_unit_dir
|
||||
)
|
||||
endif
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
"""Module name to identifier mapping"""
|
||||
|
||||
from enum import Enum
|
||||
|
||||
class Module(Enum):
|
||||
"""Module name to identifier"""
|
||||
CONFIG = 0
|
||||
CLOCK = 1
|
||||
DEVICE = 2
|
||||
IO = 3
|
||||
LED = 4
|
||||
MOUSE = 5
|
||||
COMMAND = 6
|
||||
CLIENT = 7
|
||||
NOTIFY = 8
|
||||
KEYBOARD_LAYOUT = 9
|
||||
|
||||
class LogLevel(Enum):
|
||||
"""Map log level names to pinelog levels"""
|
||||
# This is hard coded to the pinelog levels
|
||||
NOTSET = -2
|
||||
NONE = -1
|
||||
FATAL = 0
|
||||
ERROR = 1
|
||||
WARNING = 2
|
||||
INFO = 3
|
||||
DEBUG = 4
|
||||
TRACE = 5
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - Mouse driver
|
||||
*
|
||||
* Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "build-config.h"
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
#include <math.h>
|
||||
|
||||
#define PINELOG_MODULE X52D_MOD_MOUSE
|
||||
#include "pinelog.h"
|
||||
#include <daemon/config.h>
|
||||
#include <daemon/constants.h>
|
||||
#include <daemon/mouse.h>
|
||||
|
||||
// Mouse speed is the delay in microseconds between subsequent mouse reports
|
||||
#define DEFAULT_MOUSE_DELAY 70000
|
||||
#define MOUSE_DELAY_DELTA 5000
|
||||
#define MOUSE_DELAY_MIN 10000
|
||||
#define MOUSE_MULT_FACTOR 4
|
||||
#define MAX_MOUSE_MULT 5
|
||||
#define MIN_SENSITIVITY 10
|
||||
#define MAX_SENSITIVITY 500
|
||||
|
||||
volatile int mouse_scroll_dir = 1;
|
||||
volatile bool mouse_isometric_mode = false;
|
||||
volatile int mouse_curve_factor = 3;
|
||||
volatile int mouse_deadzone_factor = 0;
|
||||
volatile int mouse_sensitivity = 0;
|
||||
|
||||
static int clamp_int(const char *description, int value, int min, int max)
|
||||
{
|
||||
if (value < min) {
|
||||
PINELOG_DEBUG(_("Clamping %s value %d to range [%d..%d]"),
|
||||
description, value, min, max);
|
||||
return min;
|
||||
}
|
||||
|
||||
if (value > max) {
|
||||
PINELOG_DEBUG(_("Clamping %s value %d to range [%d..%d]"),
|
||||
description, value, min, max);
|
||||
return max;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
void x52d_cfg_set_Mouse_Enabled(bool enabled)
|
||||
{
|
||||
PINELOG_DEBUG(_("Setting mouse enable to %s"),
|
||||
enabled ? _("on") : _("off"));
|
||||
x52d_mouse_thread_control(enabled);
|
||||
}
|
||||
|
||||
void x52d_cfg_set_Mouse_Speed(int speed)
|
||||
{
|
||||
// DEPRECATED, calculate the sensitivity instead
|
||||
int new_delay;
|
||||
int new_mult;
|
||||
|
||||
int max_base_speed = (DEFAULT_MOUSE_DELAY - MOUSE_DELAY_MIN) / MOUSE_DELAY_DELTA;
|
||||
int max_speed = max_base_speed + MAX_MOUSE_MULT * MOUSE_MULT_FACTOR;
|
||||
|
||||
double sensitivity;
|
||||
|
||||
if (mouse_sensitivity == 0) {
|
||||
PINELOG_WARN(_("Config option 'mouse.speed' is DEPRECATED. Please use 'mouse.sensitivity' instead"));
|
||||
}
|
||||
|
||||
speed = clamp_int("mouse speed", speed, 0, max_speed);
|
||||
if (speed <= max_base_speed) {
|
||||
new_delay = DEFAULT_MOUSE_DELAY - speed * MOUSE_DELAY_DELTA;
|
||||
new_mult = MOUSE_MULT_FACTOR;
|
||||
} else {
|
||||
// speed between max_base_speed & max_speed
|
||||
new_delay = MOUSE_DELAY_MIN;
|
||||
new_mult = MOUSE_MULT_FACTOR + (speed - max_base_speed);
|
||||
}
|
||||
|
||||
sensitivity = round(1e6 / new_delay * new_mult / (double)MOUSE_MULT_FACTOR);
|
||||
|
||||
PINELOG_INFO(_("Migrating legacy mouse speed '%d' to sensitivity '%d' (percentage)"),
|
||||
speed, (int)sensitivity);
|
||||
mouse_sensitivity = clamp_int(_("speed -> sensitivity"), (int)sensitivity,
|
||||
MIN_SENSITIVITY, MAX_SENSITIVITY);
|
||||
}
|
||||
|
||||
void x52d_cfg_set_Mouse_ReverseScroll(bool enabled)
|
||||
{
|
||||
PINELOG_DEBUG(_("Setting mouse reverse scroll to %s"),
|
||||
enabled ? _("on") : _("off"));
|
||||
|
||||
if (enabled) {
|
||||
mouse_scroll_dir = -1;
|
||||
} else {
|
||||
mouse_scroll_dir = 1;
|
||||
}
|
||||
}
|
||||
|
||||
void x52d_cfg_set_Mouse_IsometricMode(bool enabled)
|
||||
{
|
||||
PINELOG_DEBUG(_("Setting mouse isometric mode to %s"),
|
||||
enabled ? _("on") : _("off"));
|
||||
mouse_isometric_mode = enabled;
|
||||
}
|
||||
|
||||
void x52d_cfg_set_Mouse_Sensitivity(int factor)
|
||||
{
|
||||
mouse_sensitivity = clamp_int(_("sensitivity"), factor,
|
||||
MIN_SENSITIVITY, MAX_SENSITIVITY);
|
||||
|
||||
PINELOG_DEBUG(_("Setting mouse sensitivity to %d%%"), mouse_sensitivity);
|
||||
}
|
||||
|
||||
void x52d_cfg_set_Mouse_CurveFactor(int factor)
|
||||
{
|
||||
// Factor ranges from 1-5, clamp it in this range
|
||||
// Shift by 1 so it uses the correct index
|
||||
mouse_curve_factor = clamp_int(_("curve factor"), factor, 1, 5) - 1;
|
||||
PINELOG_DEBUG(_("Setting mouse curve factor to %d"), mouse_curve_factor);
|
||||
}
|
||||
|
||||
void x52d_cfg_set_Mouse_Deadzone(int factor)
|
||||
{
|
||||
// Factor ranges from 0-12, clamp it in this range
|
||||
mouse_deadzone_factor = clamp_int(_("deadzone factor"), factor, 0, 11);
|
||||
PINELOG_DEBUG(_("Setting mouse deadzone to %d"), mouse_deadzone_factor);
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - Mouse driver
|
||||
*
|
||||
* Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#ifndef X52D_MOUSE_H
|
||||
#define X52D_MOUSE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <libx52/libx52io.h>
|
||||
|
||||
extern volatile bool mouse_isometric_mode;
|
||||
extern volatile int mouse_scroll_dir;
|
||||
extern volatile int mouse_curve_factor;
|
||||
extern volatile int mouse_deadzone_factor;
|
||||
extern volatile int mouse_sensitivity;
|
||||
|
||||
void x52d_mouse_thread_control(bool enabled);
|
||||
void x52d_mouse_handler_init(void);
|
||||
void x52d_mouse_handler_exit(void);
|
||||
void x52d_mouse_report_event(libx52io_report *report);
|
||||
|
||||
#endif // !defined X52D_MOUSE_H
|
||||
|
|
@ -0,0 +1,318 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - Mouse driver
|
||||
*
|
||||
* Copyright (C) 2021-2026 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "build-config.h"
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
#include <pthread.h>
|
||||
#include <unistd.h>
|
||||
#include <math.h>
|
||||
|
||||
#include <libx52/libx52io.h>
|
||||
#include <vkm/vkm.h>
|
||||
|
||||
#define PINELOG_MODULE X52D_MOD_MOUSE
|
||||
#include "pinelog.h"
|
||||
#include <daemon/config.h>
|
||||
#include <daemon/constants.h>
|
||||
#include <daemon/mouse.h>
|
||||
|
||||
static pthread_t mouse_thr;
|
||||
static bool mouse_thr_enabled = false;
|
||||
|
||||
static vkm_context *mouse_context;
|
||||
|
||||
static volatile libx52io_report old_report;
|
||||
static volatile libx52io_report new_report;
|
||||
|
||||
static int report_button_change(vkm_mouse_button button, int index)
|
||||
{
|
||||
vkm_result rc = VKM_ERROR_NO_CHANGE;
|
||||
bool old_button = old_report.button[index];
|
||||
bool new_button = new_report.button[index];
|
||||
vkm_button_state state;
|
||||
|
||||
if (old_button != new_button) {
|
||||
state = new_button ? VKM_BUTTON_PRESSED : VKM_BUTTON_RELEASED;
|
||||
rc = vkm_mouse_click(mouse_context, button, state);
|
||||
if (rc != VKM_SUCCESS && rc != VKM_ERROR_NO_CHANGE) {
|
||||
PINELOG_ERROR(_("Error %d writing mouse button event (button %d, state %d)"),
|
||||
rc, button, (int)new_button);
|
||||
}
|
||||
}
|
||||
|
||||
return (rc == VKM_SUCCESS);
|
||||
}
|
||||
|
||||
static int report_wheel(void)
|
||||
{
|
||||
vkm_result rc = VKM_ERROR_NO_CHANGE;
|
||||
int wheel = 0;
|
||||
bool scroll_up = new_report.button[LIBX52IO_BTN_MOUSE_SCROLL_UP];
|
||||
bool scroll_dn = new_report.button[LIBX52IO_BTN_MOUSE_SCROLL_DN];
|
||||
bool old_scroll_up = old_report.button[LIBX52IO_BTN_MOUSE_SCROLL_UP];
|
||||
bool old_scroll_dn = old_report.button[LIBX52IO_BTN_MOUSE_SCROLL_DN];
|
||||
vkm_mouse_scroll_direction dir;
|
||||
|
||||
/*
|
||||
* Handle multiple scroll button presses in sequence. This happens if a
|
||||
* hardware axis is very noisy and the firmware sends a sequence of reports
|
||||
* with button down, even though this is technically a momentary button.
|
||||
*/
|
||||
scroll_up = scroll_up && !old_scroll_up;
|
||||
scroll_dn = scroll_dn && !old_scroll_dn;
|
||||
|
||||
if (scroll_up) {
|
||||
// Scroll up event
|
||||
wheel = 1 * mouse_scroll_dir;
|
||||
} else if (scroll_dn) {
|
||||
// Scroll down event
|
||||
wheel = -1 * mouse_scroll_dir;
|
||||
}
|
||||
|
||||
if (wheel != 0) {
|
||||
dir = (wheel == 1) ? VKM_MOUSE_SCROLL_UP : VKM_MOUSE_SCROLL_DOWN;
|
||||
rc = vkm_mouse_scroll(mouse_context, dir);
|
||||
if (rc != VKM_SUCCESS) {
|
||||
PINELOG_ERROR(_("Error writing mouse wheel event %d"), dir);
|
||||
}
|
||||
}
|
||||
|
||||
return (rc == VKM_SUCCESS);
|
||||
}
|
||||
|
||||
static inline int fsgn(double f)
|
||||
{
|
||||
return (f >= 0 ? 1 : -1);
|
||||
}
|
||||
|
||||
static const double MOUSE_CURVE_FACTORS[5] = {
|
||||
1.0, 1.2, 1.5, 1.8, 2.0
|
||||
};
|
||||
|
||||
static const double MOUSE_DEADZONES[12] = {
|
||||
0.0, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.3, 1.4, 1.5
|
||||
};
|
||||
|
||||
static int report_axis(void)
|
||||
{
|
||||
#define MAX_TICK_SPEED 250.0
|
||||
|
||||
static double accum_x = 0.0;
|
||||
static double accum_y = 0.0;
|
||||
|
||||
/* Center raw HID values (0,15) => (-8, 7) */
|
||||
int dx = new_report.axis[LIBX52IO_AXIS_THUMBX] - 8;
|
||||
int dy = new_report.axis[LIBX52IO_AXIS_THUMBY] - 8;
|
||||
|
||||
/* Calculate radial magnitude */
|
||||
double mag = sqrt((double)(dx * dx + dy * dy));
|
||||
double cfg_deadzone = MOUSE_DEADZONES[mouse_deadzone_factor];
|
||||
|
||||
/* Radial deadzone check */
|
||||
if (mag <= cfg_deadzone) {
|
||||
accum_x = 0.0;
|
||||
accum_y = 0.0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Calculate gain */
|
||||
double gain = (double)mouse_sensitivity / 100.0;
|
||||
double exponent = MOUSE_CURVE_FACTORS[mouse_curve_factor];
|
||||
|
||||
/* Normalize magnitude */
|
||||
double adj_mag = mag - cfg_deadzone;
|
||||
double out_x = 0.0;
|
||||
double out_y = 0.0;
|
||||
|
||||
if (mouse_isometric_mode) {
|
||||
/* Isometric mode: speed is a function of total distance */
|
||||
double speed = gain * pow(adj_mag, exponent);
|
||||
|
||||
/* Clamp total speed before breaking into components */
|
||||
if (speed > MAX_TICK_SPEED) {
|
||||
speed = MAX_TICK_SPEED;
|
||||
}
|
||||
|
||||
/* Unit vector * speed */
|
||||
out_x = (dx / mag) * speed;
|
||||
out_y = (dy / mag) * speed;
|
||||
} else {
|
||||
/* Linear mode: speed is independently calculated for X & Y axes */
|
||||
double ratio = adj_mag / mag;
|
||||
double cur_x = dx * ratio;
|
||||
double cur_y = dy * ratio;
|
||||
|
||||
out_x = fsgn(cur_x) * gain * pow(fabs(cur_x), exponent);
|
||||
out_y = fsgn(cur_y) * gain * pow(fabs(cur_y), exponent);
|
||||
|
||||
/* Clamp individual axis speeds */
|
||||
if (fabs(out_x) > MAX_TICK_SPEED) {
|
||||
out_x = fsgn(out_x) * MAX_TICK_SPEED;
|
||||
}
|
||||
|
||||
if (fabs(out_y) > MAX_TICK_SPEED) {
|
||||
out_y = fsgn(out_y) * MAX_TICK_SPEED;
|
||||
}
|
||||
}
|
||||
|
||||
/* Accumulate movement and independent resets */
|
||||
accum_x += out_x;
|
||||
accum_y += out_y;
|
||||
|
||||
if (dx == 0) {
|
||||
accum_x = 0.0;
|
||||
}
|
||||
if (dy == 0) {
|
||||
accum_y = 0.0;
|
||||
}
|
||||
|
||||
/* Extract integer values for VKM injection */
|
||||
int move_x = (int)accum_x;
|
||||
int move_y = (int)accum_y;
|
||||
|
||||
accum_x -= move_x;
|
||||
accum_y -= move_y;
|
||||
|
||||
vkm_result rc;
|
||||
rc = vkm_mouse_move(mouse_context, move_x, move_y);
|
||||
if (rc != VKM_SUCCESS && rc != VKM_ERROR_NO_CHANGE) {
|
||||
PINELOG_ERROR(_("Error %d writing mouse axis event (dx %d, dy %d)"),
|
||||
rc, move_x, move_y);
|
||||
}
|
||||
|
||||
return (rc == VKM_SUCCESS);
|
||||
}
|
||||
|
||||
static void report_sync(void)
|
||||
{
|
||||
vkm_result rc;
|
||||
rc = vkm_sync(mouse_context);
|
||||
if (rc != VKM_SUCCESS) {
|
||||
PINELOG_ERROR(_("Error writing mouse sync event"));
|
||||
} else {
|
||||
memcpy((void *)&old_report, (void *)&new_report, sizeof(old_report));
|
||||
}
|
||||
}
|
||||
|
||||
static void reset_reports(void)
|
||||
{
|
||||
memset((void *)&old_report, 0, sizeof(old_report));
|
||||
/* Set the default thumbstick values to the mid-point */
|
||||
old_report.axis[LIBX52IO_AXIS_THUMBX] = 8;
|
||||
old_report.axis[LIBX52IO_AXIS_THUMBY] = 8;
|
||||
memcpy((void *)&new_report, (void *)&old_report, sizeof(new_report));
|
||||
}
|
||||
|
||||
static void * x52_mouse_thr(void *param)
|
||||
{
|
||||
(void)param;
|
||||
|
||||
PINELOG_INFO(_("Starting X52 virtual mouse driver thread"));
|
||||
for (;;) {
|
||||
if (report_axis()) {
|
||||
report_sync();
|
||||
}
|
||||
|
||||
usleep(10000);
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void x52d_mouse_thr_init(void)
|
||||
{
|
||||
int rc;
|
||||
|
||||
PINELOG_TRACE("Initializing virtual mouse driver");
|
||||
rc = pthread_create(&mouse_thr, NULL, x52_mouse_thr, NULL);
|
||||
if (rc != 0) {
|
||||
PINELOG_FATAL(_("Error %d initializing mouse thread: %s"),
|
||||
rc, strerror(rc));
|
||||
}
|
||||
}
|
||||
|
||||
static void x52d_mouse_thr_exit(void)
|
||||
{
|
||||
PINELOG_INFO(_("Shutting down X52 virtual mouse driver thread"));
|
||||
pthread_cancel(mouse_thr);
|
||||
}
|
||||
|
||||
void x52d_mouse_thread_control(bool enabled)
|
||||
{
|
||||
if (!vkm_is_ready(mouse_context)) {
|
||||
PINELOG_INFO(_("Virtual mouse not created. Ignoring thread state change"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (enabled) {
|
||||
if (mouse_thr_enabled) {
|
||||
PINELOG_TRACE("Ignoring re-enable mouse thread");
|
||||
return;
|
||||
} else {
|
||||
reset_reports();
|
||||
x52d_mouse_thr_init();
|
||||
}
|
||||
} else {
|
||||
if (!mouse_thr_enabled) {
|
||||
PINELOG_TRACE("Ignoring re-disable mouse thread");
|
||||
return;
|
||||
} else {
|
||||
x52d_mouse_thr_exit();
|
||||
}
|
||||
}
|
||||
mouse_thr_enabled = enabled;
|
||||
}
|
||||
|
||||
void x52d_mouse_report_event(libx52io_report *report)
|
||||
{
|
||||
bool state_changed;
|
||||
if (report) {
|
||||
memcpy((void *)&new_report, report, sizeof(new_report));
|
||||
|
||||
if (!vkm_is_ready(mouse_context) || !mouse_thr_enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
state_changed = false;
|
||||
state_changed = (0 == report_button_change(VKM_MOUSE_BTN_LEFT, LIBX52IO_BTN_MOUSE_PRIMARY)) || state_changed;
|
||||
state_changed = (0 == report_button_change(VKM_MOUSE_BTN_RIGHT, LIBX52IO_BTN_MOUSE_SECONDARY)) || state_changed;
|
||||
state_changed = (0 == report_wheel()) || state_changed;
|
||||
|
||||
if (state_changed) {
|
||||
report_sync();
|
||||
}
|
||||
} else {
|
||||
reset_reports();
|
||||
}
|
||||
}
|
||||
|
||||
void x52d_mouse_handler_init(void)
|
||||
{
|
||||
vkm_result rc;
|
||||
|
||||
rc = vkm_init(&mouse_context);
|
||||
if (rc != VKM_SUCCESS) {
|
||||
PINELOG_ERROR(_("Error %d creating X52 virtual mouse"), rc);
|
||||
return;
|
||||
}
|
||||
|
||||
vkm_set_option(mouse_context, VKM_OPT_DEVICE_NAME, "X52 virtual mouse");
|
||||
|
||||
rc = vkm_start(mouse_context);
|
||||
if (rc != VKM_SUCCESS) {
|
||||
PINELOG_ERROR(_("Error %d creating X52 virtual mouse"), rc);
|
||||
}
|
||||
}
|
||||
|
||||
void x52d_mouse_handler_exit(void)
|
||||
{
|
||||
x52d_mouse_thread_control(false);
|
||||
vkm_exit(mouse_context);
|
||||
mouse_context = NULL;
|
||||
}
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - Mouse driver test harness
|
||||
*
|
||||
* Copyright (C) 2022 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include "build-config.h"
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <setjmp.h>
|
||||
#include <cmocka.h>
|
||||
|
||||
#define PINELOG_MODULE X52D_MOD_MOUSE
|
||||
#include "pinelog.h"
|
||||
#include <daemon/config.h>
|
||||
#include <daemon/constants.h>
|
||||
#include <daemon/mouse.h>
|
||||
|
||||
/* Stub for handler */
|
||||
void x52d_mouse_thread_control(bool enabled)
|
||||
{
|
||||
function_called();
|
||||
check_expected(enabled);
|
||||
}
|
||||
|
||||
static void test_mouse_thread_enabled(void **state)
|
||||
{
|
||||
(void)state;
|
||||
expect_function_calls(x52d_mouse_thread_control, 1);
|
||||
expect_value(x52d_mouse_thread_control, enabled, true);
|
||||
|
||||
x52d_cfg_set_Mouse_Enabled(true);
|
||||
}
|
||||
|
||||
static void test_mouse_thread_disabled(void **state)
|
||||
{
|
||||
(void)state;
|
||||
expect_function_calls(x52d_mouse_thread_control, 1);
|
||||
expect_value(x52d_mouse_thread_control, enabled, false);
|
||||
|
||||
x52d_cfg_set_Mouse_Enabled(false);
|
||||
}
|
||||
|
||||
/* The following tests are dependent on the values in mouse.c */
|
||||
static void test_mouse_speed_negative(void **state)
|
||||
{
|
||||
(void)state;
|
||||
x52d_cfg_set_Mouse_Speed(-1);
|
||||
assert_int_equal(mouse_sensitivity, 14);
|
||||
}
|
||||
|
||||
static void test_mouse_speed_0(void **state)
|
||||
{
|
||||
(void)state;
|
||||
x52d_cfg_set_Mouse_Speed(0);
|
||||
assert_int_equal(mouse_sensitivity, 14);
|
||||
}
|
||||
|
||||
static void test_mouse_speed_mid_base(void **state)
|
||||
{
|
||||
(void)state;
|
||||
x52d_cfg_set_Mouse_Speed(6);
|
||||
assert_int_equal(mouse_sensitivity, 25);
|
||||
}
|
||||
|
||||
static void test_mouse_speed_max_base(void **state)
|
||||
{
|
||||
(void)state;
|
||||
x52d_cfg_set_Mouse_Speed(12);
|
||||
assert_int_equal(mouse_sensitivity, 100);
|
||||
}
|
||||
|
||||
static void test_mouse_speed_min_hyper(void **state)
|
||||
{
|
||||
(void)state;
|
||||
x52d_cfg_set_Mouse_Speed(13);
|
||||
assert_int_equal(mouse_sensitivity, 125);
|
||||
}
|
||||
|
||||
static void test_mouse_speed_mid_hyper(void **state)
|
||||
{
|
||||
(void)state;
|
||||
x52d_cfg_set_Mouse_Speed(22);
|
||||
assert_int_equal(mouse_sensitivity, 350);
|
||||
}
|
||||
|
||||
static void test_mouse_speed_max_hyper(void **state)
|
||||
{
|
||||
(void)state;
|
||||
x52d_cfg_set_Mouse_Speed(32);
|
||||
assert_int_equal(mouse_sensitivity, 500);
|
||||
}
|
||||
|
||||
static void test_mouse_speed_above_max(void **state)
|
||||
{
|
||||
(void)state;
|
||||
x52d_cfg_set_Mouse_Speed(33);
|
||||
assert_int_equal(mouse_sensitivity, 500);
|
||||
}
|
||||
|
||||
static void test_mouse_reverse_scroll_enabled(void **state)
|
||||
{
|
||||
(void)state;
|
||||
x52d_cfg_set_Mouse_ReverseScroll(true);
|
||||
assert_int_equal(mouse_scroll_dir, -1);
|
||||
}
|
||||
|
||||
static void test_mouse_reverse_scroll_disabled(void **state)
|
||||
{
|
||||
(void)state;
|
||||
x52d_cfg_set_Mouse_ReverseScroll(false);
|
||||
assert_int_equal(mouse_scroll_dir, 1);
|
||||
}
|
||||
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_mouse_thread_enabled),
|
||||
cmocka_unit_test(test_mouse_thread_disabled),
|
||||
cmocka_unit_test(test_mouse_speed_negative),
|
||||
cmocka_unit_test(test_mouse_speed_0),
|
||||
cmocka_unit_test(test_mouse_speed_mid_base),
|
||||
cmocka_unit_test(test_mouse_speed_max_base),
|
||||
cmocka_unit_test(test_mouse_speed_min_hyper),
|
||||
cmocka_unit_test(test_mouse_speed_mid_hyper),
|
||||
cmocka_unit_test(test_mouse_speed_max_hyper),
|
||||
cmocka_unit_test(test_mouse_speed_above_max),
|
||||
cmocka_unit_test(test_mouse_reverse_scroll_enabled),
|
||||
cmocka_unit_test(test_mouse_reverse_scroll_disabled),
|
||||
};
|
||||
|
||||
int main(void)
|
||||
{
|
||||
cmocka_set_message_output(CM_OUTPUT_TAP);
|
||||
cmocka_run_group_tests(tests, NULL, NULL);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Name ID map - needed to map module/loglevel names to numeric v alues
|
||||
*
|
||||
* Copyright (C) 2026 Nirenjan Krishnan <nirenjan@nirenjan.org>
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "name-id-map.h"
|
||||
#include "module-map.h"
|
||||
|
||||
static int map_get_id(const struct name_id_map *map, const char *string)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i = 0; map[i].name != NULL; i++) {
|
||||
if (strcasecmp(map[i].name, string) == 0) {
|
||||
return map[i].id;
|
||||
}
|
||||
}
|
||||
|
||||
// We've broken out of the loop, return the current ID
|
||||
return map[i].id;
|
||||
}
|
||||
|
||||
static const char *map_get_name(const struct name_id_map *map, int id)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i = 0; map[i].name != NULL; i++) {
|
||||
if (map[i].id == id) {
|
||||
return map[i].name;
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int lookup_module_by_name(const char *name)
|
||||
{
|
||||
return map_get_id(module_map, name);
|
||||
}
|
||||
|
||||
const char * lookup_module_by_id(int id)
|
||||
{
|
||||
return map_get_name(module_map, id);
|
||||
}
|
||||
|
||||
int lookup_level_by_name(const char *name)
|
||||
{
|
||||
return map_get_id(loglevel_map, name);
|
||||
}
|
||||
|
||||
const char * lookup_level_by_id(int id)
|
||||
{
|
||||
return map_get_name(loglevel_map, id);
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Name ID map - needed to map module/loglevel names to numeric v alues
|
||||
*
|
||||
* Copyright (C) 2026 Nirenjan Krishnan <nirenjan@nirenjan.org>
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#ifndef NAME_ID_MAP_H
|
||||
#define NAME_ID_MAP_H
|
||||
|
||||
struct name_id_map {
|
||||
char *name;
|
||||
int id;
|
||||
};
|
||||
|
||||
extern const struct name_id_map module_map[];
|
||||
extern const struct name_id_map loglevel_map[];
|
||||
|
||||
#endif // !defined NAME_ID_MAP_H
|
||||
|
|
@ -0,0 +1,202 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - Notification manager
|
||||
*
|
||||
* Copyright (C) 2022 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
#include <pthread.h>
|
||||
|
||||
#define PINELOG_MODULE X52D_MOD_NOTIFY
|
||||
#include "pinelog.h"
|
||||
#include <daemon/constants.h>
|
||||
#include <daemon/notify.h>
|
||||
#include <daemon/client.h>
|
||||
#include <libx52/x52dcomm.h>
|
||||
#include <daemon/x52dcomm-internal.h>
|
||||
|
||||
static pthread_t notify_thr;
|
||||
static pthread_t notify_listen;
|
||||
static pthread_mutex_t notify_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static int notify_pipe[2];
|
||||
static int notify_sock;
|
||||
|
||||
static int client_fd[X52D_MAX_CLIENTS];
|
||||
|
||||
/* Bind and listen to the notify socket */
|
||||
static int listen_notify(const char *notify_sock_path)
|
||||
{
|
||||
int sock_fd;
|
||||
int len;
|
||||
struct sockaddr_un local;
|
||||
|
||||
len = x52d_setup_notify_sock(notify_sock_path, &local);
|
||||
if (len < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
sock_fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (sock_fd < 0) {
|
||||
/* Failure creating the socket. Abort early */
|
||||
PINELOG_ERROR(_("Error creating notification socket: %s"), strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (x52d_set_socket_nonblocking(sock_fd) < 0) {
|
||||
PINELOG_ERROR(_("Error marking notification socket as nonblocking: %s"), strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (x52d_listen_socket(&local, len, sock_fd) < 0) {
|
||||
PINELOG_ERROR(_("Error listening on notification socket: %s"), strerror(errno));
|
||||
goto listen_failure;
|
||||
}
|
||||
|
||||
return sock_fd;
|
||||
|
||||
listen_failure:
|
||||
unlink(local.sun_path);
|
||||
close(sock_fd);
|
||||
PINELOG_FATAL(_("Error setting up notification socket"));
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void * x52_notify_thr(void * param)
|
||||
{
|
||||
char buffer[X52D_BUFSZ];
|
||||
uint16_t bufsiz;
|
||||
int rc;
|
||||
(void)param;
|
||||
|
||||
for (;;) {
|
||||
do {
|
||||
rc = read(notify_pipe[0], &bufsiz, sizeof(bufsiz));
|
||||
} while (rc < 0 && errno == EINTR);
|
||||
if (rc < 0) {
|
||||
PINELOG_ERROR(_("Error %d reading from pipe: %s"),
|
||||
errno, strerror(errno));
|
||||
// Error condition, try again
|
||||
continue;
|
||||
}
|
||||
|
||||
do {
|
||||
rc = read(notify_pipe[0], buffer, bufsiz);
|
||||
} while (rc < 0 && errno == EINTR);
|
||||
if (rc < 0) {
|
||||
PINELOG_ERROR(_("Error %d reading from pipe: %s"),
|
||||
errno, strerror(errno));
|
||||
// Error condition, try again
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int i = 0; i < X52D_MAX_CLIENTS; i++) {
|
||||
// Broadcast to every connected client
|
||||
if (client_fd[i] != INVALID_CLIENT) {
|
||||
do {
|
||||
rc = write(client_fd[i], buffer, bufsiz);
|
||||
} while (rc < 0 && errno == EINTR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void x52d_notify_send(int argc, const char **argv)
|
||||
{
|
||||
char buffer[X52D_BUFSZ + sizeof(uint16_t)];
|
||||
uint16_t bufsiz;
|
||||
uint16_t written;
|
||||
int rc;
|
||||
|
||||
bufsiz = (uint16_t)x52d_format_command(argc, argv, buffer + sizeof(uint16_t), X52D_BUFSZ);
|
||||
memcpy(buffer, &bufsiz, sizeof(bufsiz));
|
||||
|
||||
pthread_mutex_lock(¬ify_mutex);
|
||||
written = 0;
|
||||
while (written < bufsiz) {
|
||||
rc = write(notify_pipe[1], buffer + written, bufsiz - written);
|
||||
if (rc < 0) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
PINELOG_ERROR(_("Error %d writing notification pipe: %s"),
|
||||
errno, strerror(errno));
|
||||
} else {
|
||||
written += rc;
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(¬ify_mutex);
|
||||
}
|
||||
|
||||
static void client_handler(int fd)
|
||||
{
|
||||
char buffer[X52D_BUFSZ] = { 0 };
|
||||
int rc;
|
||||
|
||||
rc = recv(fd, buffer, sizeof(buffer), 0);
|
||||
PINELOG_TRACE("Received and discarded %d bytes from notification client %d", rc, fd);
|
||||
}
|
||||
|
||||
static void * x52_notify_loop(void * param)
|
||||
{
|
||||
struct pollfd pfd[MAX_CONN];
|
||||
int rc;
|
||||
(void)param;
|
||||
|
||||
for (;;) {
|
||||
rc = x52d_client_poll(client_fd, pfd, notify_sock);
|
||||
if (rc <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
x52d_client_handle(client_fd, pfd, notify_sock, client_handler);
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void x52d_notify_init(const char *notify_sock_path)
|
||||
{
|
||||
int rc;
|
||||
|
||||
PINELOG_TRACE("Initializing notification manager");
|
||||
x52d_client_init(client_fd);
|
||||
|
||||
PINELOG_TRACE("Creating notifications pipe");
|
||||
rc = pipe(notify_pipe);
|
||||
if (rc != 0) {
|
||||
PINELOG_FATAL(_("Error %d creating notification pipe: %s"),
|
||||
errno, strerror(errno));
|
||||
}
|
||||
|
||||
PINELOG_TRACE("Opening notification listener socket");
|
||||
notify_sock = listen_notify(notify_sock_path);
|
||||
|
||||
rc = pthread_create(¬ify_thr, NULL, x52_notify_thr, NULL);
|
||||
if (rc != 0) {
|
||||
PINELOG_FATAL(_("Error %d initializing notify thread: %s"),
|
||||
rc, strerror(rc));
|
||||
}
|
||||
|
||||
rc = pthread_create(¬ify_listen, NULL, x52_notify_loop, NULL);
|
||||
if (rc != 0) {
|
||||
PINELOG_FATAL(_("Error %d initializing notify listener: %s"),
|
||||
rc, strerror(rc));
|
||||
}
|
||||
}
|
||||
|
||||
void x52d_notify_exit(void)
|
||||
{
|
||||
close(notify_pipe[0]);
|
||||
close(notify_pipe[1]);
|
||||
close(notify_sock);
|
||||
|
||||
pthread_cancel(notify_thr);
|
||||
pthread_cancel(notify_listen);
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - Notification manager
|
||||
*
|
||||
* Copyright (C) 2022 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#ifndef X52D_NOTIFY_H
|
||||
#define X52D_NOTIFY_H
|
||||
|
||||
void x52d_notify_init(const char *notify_sock_path);
|
||||
void x52d_notify_exit(void);
|
||||
void x52d_notify_send(int argc, const char **argv);
|
||||
|
||||
#define X52D_NOTIFY(...) do { \
|
||||
const char *argv ## __LINE__ [] = {__VA_ARGS__}; \
|
||||
x52d_notify_send(sizeof(argv ## __LINE__ )/sizeof(argv ## __LINE__ [0]), argv ## __LINE__ ); \
|
||||
} while(0)
|
||||
|
||||
#endif // !defined X52D_NOTIFY_H
|
||||
|
||||
|
|
@ -0,0 +1,308 @@
|
|||
/**
|
||||
@page x52d_protocol X52 daemon socket communication protocol
|
||||
|
||||
The X52 daemon creates a Unix domain stream socket, by default at
|
||||
`$(LOCALSTATEDIR)/run/x52d.cmd` and listens for connection requests from
|
||||
clients at this location. This can be overridden by passing the -s flag when
|
||||
starting the daemon.
|
||||
|
||||
# Protocol Overview
|
||||
|
||||
\b x52d requires that clients send it commands as a series of NUL terminated
|
||||
strings, without any interleaving space. The command should be sent in a
|
||||
single `send` call, and the client may expect a response in a single `recv`
|
||||
call.
|
||||
|
||||
The `send` call must send exactly the number of bytes in the command text.
|
||||
Extra bytes will be treated as additional arguments, which would cause the
|
||||
command to fail. It is recommended that the `recv` call uses a 1024 byte buffer
|
||||
to read the data. Responses will never exceed this length.
|
||||
|
||||
# Responses
|
||||
|
||||
The daemon sends the response as a series of NUL terminated strings, without
|
||||
any interleaving space. The first string is always one of the following:
|
||||
|
||||
- \c OK
|
||||
- \c ERR
|
||||
- \c DATA
|
||||
|
||||
This determines whether the request was successful or not, and subsequent
|
||||
strings describe the action, error or requested data.
|
||||
|
||||
# Examples
|
||||
|
||||
## Reloading configuration
|
||||
|
||||
- \b send <tt>config\0reload\0</tt>
|
||||
- \b recv <tt>OK\0config\0reload\0</tt>
|
||||
|
||||
## Reading mouse speed
|
||||
|
||||
- \b send <tt>config\0get\0mouse\0speed\0</tt>
|
||||
- \b recv <tt>DATA\0mouse\0speed\010\0</tt>
|
||||
|
||||
## Sending an invalid command
|
||||
|
||||
- \b send <tt>config reload</tt>
|
||||
- \b recv <tt>ERR\0Unknown command 'config reload'\0</tt>
|
||||
|
||||
# Commands
|
||||
|
||||
\b x52d commands are arranged in a hierarchical fashion as follows:
|
||||
|
||||
```
|
||||
<command-group> [<sub-command-group> [<sub-command-group> [...]]] <command> [<arguments>]
|
||||
```
|
||||
|
||||
The list of supported commands are shown below:
|
||||
|
||||
- @subpage proto_config
|
||||
- @subpage proto_logging
|
||||
|
||||
*/
|
||||
|
||||
/**
|
||||
@page proto_config Configuration management
|
||||
|
||||
The \c config commands deal with \b x52d configuration subsystem, and have the
|
||||
following subcommands.
|
||||
|
||||
@tableofcontents
|
||||
|
||||
# Load configuration
|
||||
|
||||
The `config load` subgroup allows you to load a configuration from a given file
|
||||
(discarding anything that was already in memory), or reload the configuration
|
||||
from the command-line specified configuration file (or default configuration
|
||||
file if no option was given on the command line.)
|
||||
|
||||
## Load from file
|
||||
|
||||
\b Arguments
|
||||
|
||||
- `config`
|
||||
- `load`
|
||||
- \a path-to-file
|
||||
|
||||
\b Returns
|
||||
|
||||
- `OK`
|
||||
- `config`
|
||||
- `load`
|
||||
- \a path-to-file
|
||||
|
||||
\b Error
|
||||
|
||||
- `ERR`
|
||||
- <tt>Invalid file '/none' for 'config load' command</tt>
|
||||
|
||||
## Reload system configuration
|
||||
|
||||
\b Arguments
|
||||
|
||||
- `config`
|
||||
- `reload`
|
||||
|
||||
\b Returns
|
||||
|
||||
- `OK`
|
||||
- `config`
|
||||
- `reload`
|
||||
|
||||
# Save configuration
|
||||
|
||||
The `config save` subgroup requests the \b x52d daemon to save the current state
|
||||
of the configuration to disk. This is either the system configuration file, or
|
||||
may be a user specified configuration file. Note that this will be created with
|
||||
the permissions of the running daemon, which may be running as root.
|
||||
|
||||
## Dump configuration to file
|
||||
|
||||
\b Arguments
|
||||
|
||||
- `config`
|
||||
- `dump`
|
||||
- \a path-to-file
|
||||
|
||||
\b Returns
|
||||
|
||||
- `OK`
|
||||
- `config`
|
||||
- `dump`
|
||||
- \a path-to-file
|
||||
|
||||
\b Error
|
||||
|
||||
- `ERR`
|
||||
- <tt>Invalid file '/none' for 'config dump' command</tt>
|
||||
|
||||
## Save system configuration
|
||||
|
||||
\b Arguments
|
||||
|
||||
- `config`
|
||||
- `save`
|
||||
|
||||
\b Returns
|
||||
|
||||
- `OK`
|
||||
- `config`
|
||||
- `save`
|
||||
|
||||
# Retrieve configuration parameter
|
||||
|
||||
The `config get` command requests a specific configuration value, given the
|
||||
section and the key. Refer to \ref x52d for the section and key names, as these
|
||||
are derived from the base configuration.
|
||||
|
||||
\b Arguments
|
||||
|
||||
- `config`
|
||||
- `get`
|
||||
- \a section
|
||||
- \a key
|
||||
|
||||
\b Returns
|
||||
|
||||
- `DATA`
|
||||
- \a section
|
||||
- \a key
|
||||
- \a value
|
||||
|
||||
\b Example
|
||||
|
||||
```
|
||||
DATA\0mouse\0enabled\0true\0
|
||||
```
|
||||
|
||||
<b>Error example</b>
|
||||
|
||||
```
|
||||
ERR\0Error getting 'foo.bar'\0
|
||||
```
|
||||
|
||||
# Set configuration parameter
|
||||
|
||||
The `config set` command requests the \b x52d daemon to set the given (section,
|
||||
key) parameter to the given value. The daemon will treat it the same way as if
|
||||
it was being read from the configuration file, i.e., it will follow identical
|
||||
parsing logic, including ignoring unknown keys and not reporting errors for them.
|
||||
|
||||
A side effect of this is that the client could request a set for any arbitrary
|
||||
section and key pair, and if that pair was not recognized, it would be ignored,
|
||||
but the daemon would still send an `OK` response.
|
||||
|
||||
This will set the value within the configuration memory structures, and will
|
||||
immediately invoke the relevant callback to update the rest of the threads or
|
||||
device state.
|
||||
|
||||
\b Arguments
|
||||
|
||||
- `config`
|
||||
- `set`
|
||||
- \a section
|
||||
- \a key
|
||||
- \a value
|
||||
|
||||
\b Returns
|
||||
|
||||
- `OK`
|
||||
- `config`
|
||||
- `set`
|
||||
- \a section
|
||||
- \a key
|
||||
- \a value
|
||||
|
||||
<b>Error example</b>
|
||||
|
||||
```
|
||||
ERR\0Error 22 setting 'led.fire'='none': Invalid argument\0
|
||||
```
|
||||
|
||||
*/
|
||||
|
||||
/**
|
||||
@page proto_logging Logging management
|
||||
|
||||
The \c logging commands allow the user to fine tune the logging configuration
|
||||
of \c x52d as well as adjust the log levels for either all the modules, or for
|
||||
each of the modules individually.
|
||||
|
||||
While the `-v` and `-q` command line options allow you to either increase the
|
||||
logging verbosity or suppress it entirely, they are required to be specified at
|
||||
program startup. On the other hand, having the `logging` commands allows the
|
||||
user to fine tune the logging while the daemon is running.
|
||||
|
||||
@tableofcontents
|
||||
|
||||
# Modules
|
||||
|
||||
\c x52d is split into several modules as far as logging is concerned. The list
|
||||
of modules is below:
|
||||
|
||||
- \c Config
|
||||
- \c Cllient
|
||||
- \c Clock
|
||||
- \c Command
|
||||
- \c Device
|
||||
- \c IO
|
||||
- \c LED
|
||||
- \c Mouse
|
||||
- \c Notify
|
||||
|
||||
# Logging levels
|
||||
|
||||
The following is a list of supported logging levels. Each level logs the ones
|
||||
above it as well as the current level
|
||||
|
||||
- \c none - Disable logging entirely
|
||||
- \c fatal - Log fatal messages
|
||||
- \c error - Log error messages
|
||||
- \c warning - Log warning messages
|
||||
- \c info - Log informational messages
|
||||
- \c debug - Log debug messages
|
||||
- \c trace - Log trace messages - useful for tracing program flow.
|
||||
- \c default - Not a level, but used when configuring module log levels, makes
|
||||
the module log level fallback to the global log level.
|
||||
|
||||
# Show logging configuration
|
||||
|
||||
The `logging show` command takes in an optional module name, as listed in the
|
||||
Modules section above. It returns the module name, if specified, and the log
|
||||
level for that module. If the module is configured to fallback to the global
|
||||
level, then it will return the global level.
|
||||
|
||||
\b Arguments
|
||||
|
||||
- `logging`
|
||||
- `show`
|
||||
- \a module-name (Optional)
|
||||
|
||||
\b Returns
|
||||
|
||||
- `DATA`
|
||||
- <tt>\a module-name</tt> (if specified)
|
||||
- \a log-level
|
||||
|
||||
# Set logging configuration
|
||||
|
||||
The `logging set` command takes in the optional module name and the log level
|
||||
and sets the log level for that module, if specified, or the global level
|
||||
otherwise.
|
||||
|
||||
\b Arguments
|
||||
|
||||
- `logging`
|
||||
- `set`
|
||||
- \a module-name (Optional)
|
||||
- \a log-level
|
||||
|
||||
\b Returns
|
||||
|
||||
- `OK`
|
||||
- `logging`
|
||||
- `set`
|
||||
- <tt>\a module-name</tt> (if specified)
|
||||
- \a log-level
|
||||
*/
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Test communication with x52d and verify that the behavior is as expected"""
|
||||
# pylint: disable=consider-using-f-string
|
||||
|
||||
import glob
|
||||
import os
|
||||
import os.path
|
||||
import platform
|
||||
import shlex
|
||||
import signal
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
import sys
|
||||
|
||||
class TestCase:
|
||||
"""TestCase class handles an individual test case"""
|
||||
def __init__(self, data):
|
||||
"""Create a new test case"""
|
||||
self.desc = None
|
||||
self.in_cmd = None
|
||||
self.exp_resp = None
|
||||
self.parse(data)
|
||||
|
||||
def parse(self, data):
|
||||
"""Parses a string of the following form:
|
||||
<description>
|
||||
space separated input line, possibly quoted
|
||||
space separated expected response, possibly quoted
|
||||
"""
|
||||
self.desc, in_cmd, exp_resp = data.splitlines()
|
||||
self.in_cmd = shlex.split(in_cmd)
|
||||
self.exp_resp = '\0'.join(shlex.split(exp_resp)).encode() + b'\0'
|
||||
|
||||
def execute(self, index, suite):
|
||||
"""Execute the test case and return the result in TAP format"""
|
||||
def dump_failed(name, value):
|
||||
"""Dump the failed test case"""
|
||||
print("# {}".format(name))
|
||||
for argv in value.decode().split('\0'):
|
||||
print("#\t {}".format(argv))
|
||||
print()
|
||||
|
||||
def print_result(passed):
|
||||
"""Print the test case result and description"""
|
||||
out = "ok {} - {}".format(index+1, self.desc)
|
||||
if not passed:
|
||||
out = "not " + out
|
||||
print(out)
|
||||
|
||||
cmd = [suite.find_control_program(),
|
||||
'-s', suite.command, '--',
|
||||
*self.in_cmd]
|
||||
|
||||
testcase = subprocess.run(cmd, stdout=subprocess.PIPE, check=False)
|
||||
if testcase.returncode != 0:
|
||||
print_result(False)
|
||||
print("# x52ctl returned code: {}".format(testcase.returncode))
|
||||
dump_failed("Expected", self.exp_resp)
|
||||
dump_failed("Got", testcase.stdout)
|
||||
elif testcase.stdout != self.exp_resp:
|
||||
print_result(False)
|
||||
dump_failed("Expected", self.exp_resp)
|
||||
dump_failed("Got", testcase.stdout)
|
||||
else:
|
||||
print_result(True)
|
||||
|
||||
|
||||
class Test:
|
||||
"""Test class runs a series of unit tests"""
|
||||
|
||||
def __init__(self):
|
||||
"""Create a new instance of the Test class"""
|
||||
self.program = self.find_daemon_program()
|
||||
self.tmpdir = tempfile.TemporaryDirectory() # pylint: disable=consider-using-with
|
||||
self.command = os.path.join(self.tmpdir.name, "x52d.cmd")
|
||||
self.notify = os.path.join(self.tmpdir.name, "x52d.notify")
|
||||
self.daemon = None
|
||||
self.testcases = []
|
||||
|
||||
def __enter__(self):
|
||||
"""Context manager entry"""
|
||||
self.launch_daemon()
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc):
|
||||
"""Context manager exit"""
|
||||
self.terminate_daemon()
|
||||
self.tmpdir.cleanup()
|
||||
|
||||
@staticmethod
|
||||
def find_daemon_program():
|
||||
"""Find the daemon program. This script should be run from the
|
||||
root of the build directory"""
|
||||
daemon_candidates = glob.glob('**/x52d', recursive=True)
|
||||
if not daemon_candidates:
|
||||
print("Bail out! Unable to find X52 daemon.")
|
||||
sys.exit(1)
|
||||
|
||||
return os.path.realpath(daemon_candidates[0])
|
||||
|
||||
@staticmethod
|
||||
def find_control_program():
|
||||
"""Find the control program. This script should be run from the
|
||||
root of the build directory"""
|
||||
ctl_candidates = glob.glob('**/x52ctl', recursive=True)
|
||||
if not ctl_candidates:
|
||||
print("Bail out! Unable to find x52ctl.")
|
||||
sys.exit(1)
|
||||
|
||||
return os.path.realpath(ctl_candidates[0])
|
||||
|
||||
def launch_daemon(self):
|
||||
"""Launch an instance of the running daemon"""
|
||||
if self.daemon is not None:
|
||||
# We've already started the daemon, check if it is still running
|
||||
if self.daemon.poll() is None:
|
||||
return
|
||||
|
||||
self.daemon = None
|
||||
|
||||
daemon_cmdline = [
|
||||
self.program,
|
||||
"-f", # Run in foreground
|
||||
"-q", # Quiet logging
|
||||
"-c", os.path.join(self.tmpdir.name, "x52d.cfg"), # Default config file
|
||||
"-l", os.path.join(self.tmpdir.name, "x52d.log"), # Output logs to log file
|
||||
"-p", os.path.join(self.tmpdir.name, "x52d.pid"), # PID file
|
||||
"-s", self.command, # Command socket path
|
||||
"-b", self.notify, # Notification socket path
|
||||
]
|
||||
|
||||
# Create empty config file
|
||||
with open(daemon_cmdline[4], 'w', encoding='utf-8'):
|
||||
pass
|
||||
|
||||
env = os.environ.copy()
|
||||
# Uninstalled build: us.x52l lives next to the x52d binary (see daemon/meson.build).
|
||||
env['X52D_LAYOUT_DIR'] = os.path.dirname(self.program)
|
||||
|
||||
self.daemon = subprocess.Popen(daemon_cmdline, env=env) # pylint: disable=consider-using-with
|
||||
|
||||
print("# Sleeping 2 seconds for daemon to start")
|
||||
time.sleep(2)
|
||||
|
||||
def terminate_daemon(self):
|
||||
"""Terminate a running daemon"""
|
||||
if self.daemon is None:
|
||||
return
|
||||
|
||||
# Send a SIGTERM to the daemon
|
||||
os.kill(self.daemon.pid, signal.SIGTERM)
|
||||
try:
|
||||
self.daemon.wait(timeout=15)
|
||||
except subprocess.TimeoutExpired:
|
||||
# Forcibly kill the running process
|
||||
self.daemon.kill()
|
||||
finally:
|
||||
self.daemon = None
|
||||
|
||||
def append(self, testcase):
|
||||
"""Add one testcase to the test case list"""
|
||||
self.testcases.append(testcase)
|
||||
|
||||
def extend(self, testcases):
|
||||
"""Add one or more testcases to the test case list"""
|
||||
self.testcases.extend(testcases)
|
||||
|
||||
@staticmethod
|
||||
def dump_failed(name, value):
|
||||
"""Dump the failed test case"""
|
||||
print("# {}".format(name))
|
||||
for argv in value.decode().split('\0'):
|
||||
print("#\t {}".format(argv))
|
||||
print()
|
||||
|
||||
def run_tests(self):
|
||||
"""Run test cases"""
|
||||
print("1..{}".format(len(self.testcases)))
|
||||
for index, testcase in enumerate(self.testcases):
|
||||
testcase.execute(index, self)
|
||||
|
||||
def find_and_parse_testcase_files(self):
|
||||
"""Find and parse *.tc files"""
|
||||
basedir = os.path.dirname(os.path.realpath(__file__))
|
||||
pattern = os.path.join(basedir, 'tests', '**', '*.tc')
|
||||
tc_files = sorted(glob.glob(pattern, recursive=True))
|
||||
|
||||
for tc_file in tc_files:
|
||||
with open(tc_file, encoding='utf-8') as tc_fd:
|
||||
# Test cases are separated by blank lines
|
||||
testcases = tc_fd.read().split('\n\n')
|
||||
self.extend(TestCase(tc_data) for tc_data in testcases)
|
||||
|
||||
def main():
|
||||
"""Main routine adds test cases to the Test class and runs them"""
|
||||
# Only run the tests on Linux platform
|
||||
if platform.system() != 'Linux':
|
||||
print('1..0 # Skipping tests on', platform.system())
|
||||
return
|
||||
|
||||
with Test() as test:
|
||||
test.find_and_parse_testcase_files()
|
||||
test.run_tests()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
Check for invalid command
|
||||
foo
|
||||
ERR "Unknown command 'foo'"
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
Configuration with insufficient arguments
|
||||
config
|
||||
ERR "Insufficient arguments for 'config' command"
|
||||
|
||||
Load with invalid file argument
|
||||
config load ''
|
||||
ERR "Invalid file '' for 'config load' command"
|
||||
|
||||
Load with nonexistent file argument
|
||||
config load /nonexistent
|
||||
ERR "Invalid file '/nonexistent' for 'config load' command"
|
||||
|
||||
Load with empty file argument
|
||||
config load /dev/null
|
||||
OK config load /dev/null
|
||||
|
||||
Load with extra arguments
|
||||
config load /dev/null ''
|
||||
ERR "Unexpected arguments for 'config load' command; got 4, expected 3"
|
||||
|
||||
Load with missing argument
|
||||
config load
|
||||
ERR "Unexpected arguments for 'config load' command; got 2, expected 3"
|
||||
|
||||
Reload configuration
|
||||
config reload
|
||||
OK config reload
|
||||
|
||||
Reload configuration with extra arguments
|
||||
config reload ''
|
||||
ERR "Unexpected arguments for 'config reload' command; got 3, expected 2"
|
||||
|
||||
Dump configuration with insufficient arguments
|
||||
config dump
|
||||
ERR "Unexpected arguments for 'config dump' command; got 2, expected 3"
|
||||
|
||||
Dump configuration with invalid file
|
||||
config dump ''
|
||||
ERR "Invalid file '' for 'config dump' command"
|
||||
|
||||
Dump configuration with extra arguments
|
||||
config dump /dev/null ''
|
||||
ERR "Unexpected arguments for 'config dump' command; got 4, expected 3"
|
||||
|
||||
Dump configuration to /dev/null
|
||||
config dump /dev/null
|
||||
OK config dump /dev/null
|
||||
|
||||
Save configuration
|
||||
config save
|
||||
OK config save
|
||||
|
||||
Save configuration with extra arguments
|
||||
config save ''
|
||||
ERR "Unexpected arguments for 'config save' command; got 3, expected 2"
|
||||
|
||||
Config command with empty subcommand
|
||||
config ''
|
||||
ERR "Unknown subcommand '' for 'config' command"
|
||||
|
||||
Config command with unknown subcommand
|
||||
config foo
|
||||
ERR "Unknown subcommand 'foo' for 'config' command"
|
||||
|
||||
Get configuration with fewer arguments
|
||||
config get
|
||||
ERR "Unexpected arguments for 'config get' command; got 2, expected 4"
|
||||
|
||||
Get configuration with extra arguments
|
||||
config get foo bar baz
|
||||
ERR "Unexpected arguments for 'config get' command; got 5, expected 4"
|
||||
|
||||
Set configuration with fewer arguments
|
||||
config set
|
||||
ERR "Unexpected arguments for 'config set' command; got 2, expected 5"
|
||||
|
||||
Set configuration with extra arguments
|
||||
config set foo bar baz quux
|
||||
ERR "Unexpected arguments for 'config set' command; got 6, expected 5"
|
||||
|
||||
Get configuration of unknown parameter
|
||||
config get foo bar
|
||||
ERR "Error getting 'foo.bar'"
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
Set clock enabled to false
|
||||
config set clock enabled false
|
||||
OK config set clock enabled false
|
||||
|
||||
Verify clock enabled was set to false
|
||||
config get clock enabled
|
||||
DATA clock enabled false
|
||||
|
||||
Set clock enabled to true
|
||||
config set clock enabled true
|
||||
OK config set clock enabled true
|
||||
|
||||
Verify clock enabled was set to true
|
||||
config get clock enabled
|
||||
DATA clock enabled true
|
||||
|
||||
Set clock PrimaryIsLocal to no
|
||||
config set clock PrimaryIsLocal no
|
||||
OK config set clock PrimaryIsLocal no
|
||||
|
||||
Verify clock PrimaryIsLocal was set to false (no)
|
||||
config get clock PrimaryIsLocal
|
||||
DATA clock PrimaryIsLocal false
|
||||
|
||||
Set clock PrimaryIsLocal to yes
|
||||
config set clock PrimaryIsLocal yes
|
||||
OK config set clock PrimaryIsLocal yes
|
||||
|
||||
Verify clock PrimaryIsLocal was set to true (yes)
|
||||
config get clock PrimaryIsLocal
|
||||
DATA clock PrimaryIsLocal true
|
||||
|
||||
Set clock PrimaryIsLocal to invalid value
|
||||
config set clock PrimaryIsLocal foo
|
||||
ERR "Error 22 setting 'clock.PrimaryIsLocal'='foo': Invalid argument"
|
||||
|
||||
Verify clock PrimaryIsLocal was not changed
|
||||
config get clock PrimaryIsLocal
|
||||
DATA clock PrimaryIsLocal true
|
||||
|
||||
Set clock secondary to America/Los_Angeles
|
||||
config set clock secondary America/Los_Angeles
|
||||
OK config set clock secondary America/Los_Angeles
|
||||
|
||||
Verify clock secondary was set to America/Los_Angeles
|
||||
config get clock secondary
|
||||
DATA clock secondary America/Los_Angeles
|
||||
|
||||
Set clock secondary to UTC
|
||||
config set clock secondary UTC
|
||||
OK config set clock secondary UTC
|
||||
|
||||
Verify clock secondary was set to UTC
|
||||
config get clock secondary
|
||||
DATA clock secondary UTC
|
||||
|
||||
Set clock tertiary to America/Los_Angeles
|
||||
config set clock tertiary America/Los_Angeles
|
||||
OK config set clock tertiary America/Los_Angeles
|
||||
|
||||
Verify clock tertiary was set to America/Los_Angeles
|
||||
config get clock tertiary
|
||||
DATA clock tertiary America/Los_Angeles
|
||||
|
||||
Set clock tertiary to UTC
|
||||
config set clock tertiary UTC
|
||||
OK config set clock tertiary UTC
|
||||
|
||||
Verify clock tertiary was set to UTC
|
||||
config get clock tertiary
|
||||
DATA clock tertiary UTC
|
||||
|
||||
Set clock formatprimary to 24hr
|
||||
config set clock formatprimary 24hr
|
||||
OK config set clock formatprimary 24hr
|
||||
|
||||
Verify clock formatprimary was set to 24hr
|
||||
config get clock formatprimary
|
||||
DATA clock formatprimary '24 hour'
|
||||
|
||||
Set clock formatprimary to 12hr
|
||||
config set clock formatprimary 12hr
|
||||
OK config set clock formatprimary 12hr
|
||||
|
||||
Verify clock formatprimary was set to 12hr
|
||||
config get clock formatprimary
|
||||
DATA clock formatprimary '12 hour'
|
||||
|
||||
Set clock formatsecondary to 24hr
|
||||
config set clock formatsecondary 24hr
|
||||
OK config set clock formatsecondary 24hr
|
||||
|
||||
Verify clock formatsecondary was set to 24hr
|
||||
config get clock formatsecondary
|
||||
DATA clock formatsecondary '24 hour'
|
||||
|
||||
Set clock formatsecondary to 12hr
|
||||
config set clock formatsecondary 12hr
|
||||
OK config set clock formatsecondary 12hr
|
||||
|
||||
Verify clock formatsecondary was set to 12hr
|
||||
config get clock formatsecondary
|
||||
DATA clock formatsecondary '12 hour'
|
||||
|
||||
Set clock formattertiary to 24
|
||||
config set clock formattertiary 24
|
||||
OK config set clock formattertiary 24
|
||||
|
||||
Verify clock formattertiary was set to 24
|
||||
config get clock formattertiary
|
||||
DATA clock formattertiary '24 hour'
|
||||
|
||||
Set clock formattertiary to 12
|
||||
config set clock formattertiary 12
|
||||
OK config set clock formattertiary 12
|
||||
|
||||
Verify clock formattertiary was set to 12
|
||||
config get clock formattertiary
|
||||
DATA clock formattertiary '12 hour'
|
||||
|
||||
Set clock formattertiary to invalid value
|
||||
config set clock formattertiary '12 hour'
|
||||
ERR "Error 22 setting 'clock.formattertiary'='12 hour': Invalid argument"
|
||||
|
||||
Verify clock formattertiary was not changed
|
||||
config get clock formattertiary
|
||||
DATA clock formattertiary '12 hour'
|
||||
|
||||
Set clock dateformat to yymmdd
|
||||
config set clock dateformat yymmdd
|
||||
OK config set clock dateformat yymmdd
|
||||
|
||||
Verify clock dateformat was set to yymmdd
|
||||
config get clock dateformat
|
||||
DATA clock dateformat YY-MM-DD
|
||||
|
||||
Set clock dateformat to mmddyy
|
||||
config set clock dateformat mmddyy
|
||||
OK config set clock dateformat mmddyy
|
||||
|
||||
Verify clock dateformat was set to mmddyy
|
||||
config get clock dateformat
|
||||
DATA clock dateformat MM-DD-YY
|
||||
|
||||
Set clock dateformat to ddmmyy
|
||||
config set clock dateformat ddmmyy
|
||||
OK config set clock dateformat ddmmyy
|
||||
|
||||
Verify clock dateformat was set to ddmmyy
|
||||
config get clock dateformat
|
||||
DATA clock dateformat DD-MM-YY
|
||||
|
||||
Set clock dateformat to yy-mm-dd
|
||||
config set clock dateformat yy-mm-dd
|
||||
OK config set clock dateformat yy-mm-dd
|
||||
|
||||
Verify clock dateformat was set to yy-mm-dd
|
||||
config get clock dateformat
|
||||
DATA clock dateformat YY-MM-DD
|
||||
|
||||
Set clock dateformat to mm-dd-yy
|
||||
config set clock dateformat mm-dd-yy
|
||||
OK config set clock dateformat mm-dd-yy
|
||||
|
||||
Verify clock dateformat was set to mm-dd-yy
|
||||
config get clock dateformat
|
||||
DATA clock dateformat MM-DD-YY
|
||||
|
||||
Set clock dateformat to dd-mm-yy
|
||||
config set clock dateformat dd-mm-yy
|
||||
OK config set clock dateformat dd-mm-yy
|
||||
|
||||
Verify clock dateformat was set to dd-mm-yy
|
||||
config get clock dateformat
|
||||
DATA clock dateformat DD-MM-YY
|
||||
|
||||
Set clock dateformat to invalid value
|
||||
config set clock dateformat foo-bar-baz
|
||||
ERR "Error 22 setting 'clock.dateformat'='foo-bar-baz': Invalid argument"
|
||||
|
||||
Verify clock dateformat was not changed
|
||||
config get clock dateformat
|
||||
DATA clock dateformat DD-MM-YY
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
Set LED Fire to off
|
||||
config set led fire off
|
||||
OK config set led fire off
|
||||
|
||||
Verify LED Fire was set to off
|
||||
config get led fire
|
||||
DATA led fire off
|
||||
|
||||
Set LED Fire to on
|
||||
config set led fire on
|
||||
OK config set led fire on
|
||||
|
||||
Verify LED Fire was set to on
|
||||
config get led fire
|
||||
DATA led fire on
|
||||
|
||||
Set LED Fire to red TODO: This passes since the LED parser doesn't know the LED name
|
||||
config set led fire red
|
||||
OK config set led fire red
|
||||
|
||||
Verify LED Fire was set to red
|
||||
config get led fire
|
||||
DATA led fire red
|
||||
|
||||
Set LED Fire to amber TODO: This passes since the LED parser doesn't know the LED name
|
||||
config set led fire amber
|
||||
OK config set led fire amber
|
||||
|
||||
Verify LED Fire was set to amber
|
||||
config get led fire
|
||||
DATA led fire amber
|
||||
|
||||
Set LED Fire to green TODO: This passes since the LED parser doesn't know the LED name
|
||||
config set led fire green
|
||||
OK config set led fire green
|
||||
|
||||
Verify LED Fire was set to green
|
||||
config get led fire
|
||||
DATA led fire green
|
||||
|
||||
Set LED Fire to invalid value
|
||||
config set led fire foo
|
||||
ERR "Error 22 setting 'led.fire'='foo': Invalid argument"
|
||||
|
||||
Verify LED Fire was not changed
|
||||
config get led fire
|
||||
DATA led fire green
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
Set mouse speed to 32
|
||||
config set mouse speed 32
|
||||
OK config set mouse speed 32
|
||||
|
||||
Verify mouse speed is set to 32
|
||||
config get mouse speed
|
||||
DATA mouse speed 32
|
||||
|
||||
Set mouse speed to invalid value
|
||||
config set mouse speed off
|
||||
ERR "Error 22 setting 'mouse.speed'='off': Invalid argument"
|
||||
|
||||
Verify mouse speed is unchanged
|
||||
config get mouse speed
|
||||
DATA mouse speed 32
|
||||
|
||||
Set mouse speed to 33 (Exceeds max speed)
|
||||
config set mouse speed 33
|
||||
OK config set mouse speed 33
|
||||
|
||||
Set mouse speed to 20 (In multiplier range)
|
||||
config set mouse speed 20
|
||||
OK config set mouse speed 20
|
||||
|
||||
Set mouse speed to negative value
|
||||
config set mouse speed -1
|
||||
OK config set mouse speed -1
|
||||
|
||||
Reset mouse speed to minimum
|
||||
config set mouse speed 0
|
||||
OK config set mouse speed 0
|
||||
|
||||
Set mouse reverse scroll to enabled
|
||||
config set mouse reversescroll true
|
||||
OK config set mouse reversescroll true
|
||||
|
||||
Check if reverse scrolling is enabled
|
||||
config get mouse reversescroll
|
||||
DATA mouse reversescroll true
|
||||
|
||||
Set mouse reverse scroll to disabled
|
||||
config set mouse reversescroll false
|
||||
OK config set mouse reversescroll false
|
||||
|
||||
Check if reverse scrolling is disabled
|
||||
config get mouse reversescroll
|
||||
DATA mouse reversescroll false
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
Logging with insufficient arguments
|
||||
logging
|
||||
ERR "Insufficient arguments for 'logging' command"
|
||||
|
||||
Get logging level with extra arguments
|
||||
logging show foo bar
|
||||
ERR "Unexpected arguments for 'logging show' command; got 4, expected 2 or 3"
|
||||
|
||||
Set logging level with insufficient arguments
|
||||
logging set
|
||||
ERR "Unexpected arguments for 'logging set' command; got 2, expected 3 or 4"
|
||||
|
||||
Invalid logging subcommand
|
||||
logging foo
|
||||
ERR "Unknown subcommand 'foo' for 'logging' command"
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
Set global logging level to error
|
||||
logging set error
|
||||
OK logging set error
|
||||
|
||||
Get global logging level (should be error)
|
||||
logging show
|
||||
DATA global error
|
||||
|
||||
Set global logging level to default - should return error
|
||||
logging set default
|
||||
ERR "'default' level is not valid without a module"
|
||||
|
||||
Set global logging level to unknown value - should return error
|
||||
logging set foo
|
||||
ERR "Unknown level 'foo' for 'logging set' command"
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
Set config module logging level to error
|
||||
logging set config error
|
||||
OK logging set config error
|
||||
|
||||
Get config module logging level
|
||||
logging show config
|
||||
DATA config error
|
||||
|
||||
Get configuration for invalid module
|
||||
logging show foo
|
||||
ERR "Invalid module 'foo'"
|
||||
|
||||
Set module logging level for invalid module
|
||||
logging set foo error
|
||||
ERR "Invalid module 'foo'"
|
||||
|
||||
Set invalid logging level for module
|
||||
logging set config foo
|
||||
ERR "Unknown level 'foo' for 'logging set' command"
|
||||
|
||||
Set module logging level to default
|
||||
logging set config default
|
||||
OK logging set config default
|
||||
|
||||
Get module logging level (should be error - same as global)
|
||||
logging show config
|
||||
DATA config error
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
#######################################################################
|
||||
# X52 Daemon Configuration
|
||||
######################################################################
|
||||
|
||||
# The settings below are the defaults. Note that the section and key
|
||||
# strings are case insensitive, but the values are not necessarily so,
|
||||
# especially for those referring to paths or timezone names.
|
||||
|
||||
######################################################################
|
||||
# Clock Settings
|
||||
######################################################################
|
||||
[Clock]
|
||||
|
||||
# Enabled controls whether the clock is enabled or not. Set this to no to
|
||||
# disable the clock update. Keep in mind that if the clock was originally
|
||||
# enabled on the X52, then disabling it here won't make the clock disappear on
|
||||
# the MFD. You will need to unplug and reattach the X52 to make the clock
|
||||
# disappear
|
||||
Enabled=yes
|
||||
|
||||
# PrimaryIsLocal controls whether the primary clock displays local time or UTC.
|
||||
# Set this to yes to display local time, no for UTC.
|
||||
PrimaryIsLocal=yes
|
||||
|
||||
# Secondary controls the timezone of the secondary clock. Use the standard
|
||||
# timezone name as defined by the Olson time database.
|
||||
Secondary=UTC
|
||||
|
||||
# Tertiary controls the timezone of the tertiary clock. Use the standard
|
||||
# timezone name as defined by the Olson time database.
|
||||
Tertiary=UTC
|
||||
|
||||
# PrimaryFormat controls the clock format of the primary clock. This is
|
||||
# either 12hr or 24hr, and can be abbreviated to 12 or 24
|
||||
FormatPrimary=12hr
|
||||
|
||||
# SecondaryFormat controls the clock format of the secondary clock. This is
|
||||
# either 12hr or 24hr, and can be abbreviated to 12 or 24
|
||||
FormatSecondary=12hr
|
||||
|
||||
# TertiaryFormat controls the clock format of the tertiary clock. This is
|
||||
# either 12hr or 24hr, and can be abbreviated to 12 or 24
|
||||
FormatTertiary=12hr
|
||||
|
||||
# DateFormat controls the format of the date display. This can be one of
|
||||
# ddmmyy, mmddyy or yymmdd. Alternate representations of these are
|
||||
# dd-mm-yy, mm-dd-yy or yy-mm-dd respectively.
|
||||
DateFormat=ddmmyy
|
||||
|
||||
######################################################################
|
||||
# LED Settings - only applicable to X52Pro
|
||||
######################################################################
|
||||
[LED]
|
||||
|
||||
# The LED settings map a color code or state to the corresponding LED.
|
||||
Fire=on
|
||||
Throttle=on
|
||||
A=green
|
||||
B=green
|
||||
D=green
|
||||
E=green
|
||||
T1=green
|
||||
T2=green
|
||||
T3=green
|
||||
POV=green
|
||||
Clutch=green
|
||||
|
||||
######################################################################
|
||||
# Brightness Settings
|
||||
######################################################################
|
||||
[Brightness]
|
||||
|
||||
# The brightness settings map the brightness value to the LEDs/MFD.
|
||||
MFD=128
|
||||
LED=128
|
||||
|
||||
######################################################################
|
||||
# Mouse - only valid on Linux
|
||||
######################################################################
|
||||
[Mouse]
|
||||
|
||||
# Enabled controls whether the virtual mouse is enabled or not.
|
||||
Enabled=yes
|
||||
|
||||
# Sensitivity is the sensitivity percentage of the virtual mouse. This
|
||||
# replaces the old Speed option, and is a percentage value by which to
|
||||
# scale the input. The sensitivity can vary from 10% to 500%.
|
||||
Sensitivity=100
|
||||
|
||||
# DEPRECATED: Speed is proportional to the speed of updates to the virtual mouse
|
||||
# This used a calculation with delays and multiplication factors to simulate
|
||||
# the mouse moves, but it felt choppy at lower speeds.
|
||||
Speed=0
|
||||
|
||||
# ReverseScroll reverses the direction of the virtual scroll wheel
|
||||
ReverseScroll=no
|
||||
|
||||
# Isometric mode controls if the mouse movement is computed based on
|
||||
# both X and Y movements. If enabled, the behavior is similar to the
|
||||
# mouse nubs found on some laptops. Otherwise, the X and Y movements
|
||||
# are independent of each other.
|
||||
IsometricMode=no
|
||||
|
||||
# Curve factor controls the speed curve in an exponential manner, so
|
||||
# that the user can get finer control at the lower end of motion, while
|
||||
# increasing speeds at the upper end. Values range from 1-5, with the
|
||||
# following descriptions. Values are clamped in this range.
|
||||
# 1: Linear motion - no curve
|
||||
# 2: Soft curve: slight dampening in the lower ranges
|
||||
# 3: Standard: Feels like a Thinkpad
|
||||
# 4: Precision: heavy dampening in lower ranges, high speed elsewhere
|
||||
# 5: Aggressive: "sniper" mode in the lower rnages, "flick" elsewhere
|
||||
CurveFactor=3
|
||||
|
||||
# Deadzone is a configurable value from 0-11, with 0 being no deadzone
|
||||
# and the deadzone size increasing with increasing values. This is useful
|
||||
# when there is a loose thumbstick and you want to restrict the motion
|
||||
# when there's no user input. A deadzone of 0 is perfectly fine for a
|
||||
# new joystick, but keep in mind that the higher values will require
|
||||
# you to push more to get any motion out of the virtual mouse.
|
||||
Deadzone=0
|
||||
|
||||
######################################################################
|
||||
# Profiles - only valid on Linux
|
||||
######################################################################
|
||||
[Profiles]
|
||||
# TODO: Profiles are used to map the buttons and axis to keyboard events, and
|
||||
# can be used to write macros. This is a placeholder only for now, and is not
|
||||
# supported yet.
|
||||
|
||||
# Directory is the location of the folder containing the individual profiles.
|
||||
Directory=/etc/x52d/profiles.d
|
||||
|
||||
# ClutchEnabled determines if the clutch button is treated specially
|
||||
ClutchEnabled=no
|
||||
|
||||
# ClutchLatched controls if the clutch button (if enabled) is a latched button
|
||||
# (press once to enter clutch mode, press again to exit clutch mode), or must
|
||||
# be held down to remain in clutch mode.
|
||||
ClutchLatched=no
|
||||
|
||||
# KeyboardLayout is a basename only (alphanumeric, underscore, hyphen), not a path.
|
||||
# Resolves to $datadir/x52d/<basename>.x52l; default us uses the installed us.x52l pack.
|
||||
KeyboardLayout=us
|
||||
|
||||
##################
|
||||
#X52 Input Servic#
|
||||
#Version 0.3.3 #
|
||||
#OS: Linux #
|
||||
##################
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
[Unit]
|
||||
Description=X52 driver daemon
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=%bindir%/x52d -f -v
|
||||
ExecReload=kill -HUP $MAINPID
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Generate the module name to map for use by the daemon"""
|
||||
|
||||
import os.path
|
||||
import sys
|
||||
|
||||
import module_defs
|
||||
|
||||
def main():
|
||||
|
||||
if len(sys.argv) != 3:
|
||||
print("Usage: {sys.argv[0]} <output-header> <output-source>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
with open(sys.argv[1], 'w', encoding='utf-8') as out_fd:
|
||||
# Generate the header
|
||||
print("// Autogenerated module/loglevel header - DO NOT EDIT\n",
|
||||
file=out_fd)
|
||||
|
||||
include_guard = os.path.basename(sys.argv[1]).replace('-', '_').replace('.', '_').upper()
|
||||
print(f"#ifndef {include_guard}", file=out_fd)
|
||||
print(f"#define {include_guard}\n", file=out_fd)
|
||||
|
||||
for mod in module_defs.Module:
|
||||
print(f"#define X52D_MOD_{mod.name} {mod.value}", file=out_fd)
|
||||
|
||||
print(f"#define X52D_MOD_GLOBAL 0xFF", file=out_fd)
|
||||
print(f"#define X52D_MOD_MAX {len(module_defs.Module)}\n", file=out_fd)
|
||||
|
||||
print(f"int lookup_module_by_name(const char *name);", file=out_fd)
|
||||
print(f"const char * lookup_module_by_id(int id);", file=out_fd)
|
||||
print(f"int lookup_level_by_name(const char *name);", file=out_fd)
|
||||
print(f"const char * lookup_level_by_id(int id);", file=out_fd)
|
||||
|
||||
print(f"\n#endif // !defined {include_guard}", file=out_fd)
|
||||
|
||||
with open(sys.argv[2], 'w', encoding='utf-8') as out_fd:
|
||||
print("// Autogenerated module/loglevel tables - DO NOT EDIT\n",
|
||||
file=out_fd)
|
||||
|
||||
print('#include <stddef.h>', file=out_fd)
|
||||
print('#include <limits.h>\n', file=out_fd)
|
||||
|
||||
print(f'#include "{os.path.basename(sys.argv[1])}"', file=out_fd)
|
||||
print('#include "name-id-map.h"\n', file=out_fd)
|
||||
|
||||
print('const struct name_id_map module_map[] = {', file=out_fd)
|
||||
for mod in module_defs.Module:
|
||||
print(f' {{ "{mod.name.lower()}", {mod.value} }},', file=out_fd)
|
||||
|
||||
print(' { NULL, INT_MAX }', file=out_fd)
|
||||
print('};\n', file=out_fd)
|
||||
|
||||
print('const struct name_id_map loglevel_map[] = {', file=out_fd)
|
||||
for level in module_defs.LogLevel:
|
||||
if level == module_defs.LogLevel.NOTSET:
|
||||
level_name = 'default'
|
||||
else:
|
||||
level_name = level.name.lower()
|
||||
|
||||
print(f' {{ "{level_name}", {level.value} }},', file=out_fd)
|
||||
|
||||
print(' { NULL, INT_MAX }', file=out_fd)
|
||||
|
||||
print('};\n', file=out_fd)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -0,0 +1,197 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Read the default configuration file, and create ID enums for sections
|
||||
and options.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import configparser
|
||||
import json
|
||||
import os.path
|
||||
|
||||
from collections import defaultdict
|
||||
from itertools import count
|
||||
from pprint import pprint
|
||||
|
||||
class ConfigToEnum:
|
||||
"""ConfigToEnum scans a configuration file, and dumps the secions and
|
||||
options within a section as Python Enums"""
|
||||
|
||||
REGISTRY_COMMENT = """The configuration registry is a historic record of
|
||||
all configuration identifiers. Do NOT edit this file manually, or else, the
|
||||
communication protocol may break."""
|
||||
|
||||
def __init__(self, cfg_file, config_ids):
|
||||
"""Initialize the object"""
|
||||
self.config = configparser.ConfigParser(default_section=None, interpolation=None)
|
||||
self.config.optionxform = str
|
||||
self.config.read(cfg_file)
|
||||
|
||||
self.registry_file = config_ids
|
||||
|
||||
try:
|
||||
with open(self.registry_file, encoding='utf-8') as regfd:
|
||||
self.config_ids = json.load(regfd)
|
||||
except Exception:
|
||||
# On any error, ignore it and start with a clean slate
|
||||
self.config_ids = {}
|
||||
|
||||
self.sections = {}
|
||||
self.options = {}
|
||||
|
||||
def parse(self):
|
||||
"""Parse the config object and assign IDs"""
|
||||
self._parse_sections()
|
||||
for section in self.config.sections():
|
||||
self._parse_options(section)
|
||||
|
||||
def _parse_sections(self):
|
||||
"""Assign IDs to each section"""
|
||||
sections = {}
|
||||
unassigned = []
|
||||
for section in self.config.sections():
|
||||
section = section.upper()
|
||||
section_id = self.config_ids.get('sections', {}).get(section)
|
||||
if section_id is None:
|
||||
unassigned.append(section)
|
||||
else:
|
||||
sections[section] = section_id
|
||||
|
||||
if not sections:
|
||||
counter = count(1)
|
||||
else:
|
||||
counter = count(max(sections.values()) + 1)
|
||||
|
||||
sections.update({k:v for k, v in zip(unassigned, counter)})
|
||||
|
||||
orig_sections = self.config_ids.get('sections', {})
|
||||
sections.update({k:v for k, v in orig_sections.items() if k not in sections})
|
||||
|
||||
self.sections = sections
|
||||
|
||||
def _parse_options(self, section):
|
||||
options = {}
|
||||
unassigned = []
|
||||
for option in self.config.options(section):
|
||||
option = option.upper()
|
||||
section = section.upper()
|
||||
option_id = self.config_ids.get('options', {}).get(section, {}).get(option)
|
||||
if option_id is None:
|
||||
unassigned.append(option)
|
||||
else:
|
||||
options[option] = option_id
|
||||
|
||||
if not options:
|
||||
counter = count(1)
|
||||
else:
|
||||
counter = count(max(options.values()) + 1)
|
||||
|
||||
options.update({k:v for k, v in zip(unassigned, counter)})
|
||||
orig_options = self.config_ids.get('options', {}).get(section, {})
|
||||
|
||||
# Make sure that we have all the entries already
|
||||
options.update({k:v for k, v in orig_options.items() if k not in options})
|
||||
|
||||
self.options[section] = options
|
||||
|
||||
def save_registry(self):
|
||||
"""Save the generated registry"""
|
||||
registry = {
|
||||
"_comment": self.REGISTRY_COMMENT,
|
||||
"sections": self.sections,
|
||||
"options": self.options,
|
||||
}
|
||||
|
||||
with open(self.registry_file, 'w', encoding='utf-8') as regfd:
|
||||
json.dump(registry, regfd, indent=4)
|
||||
|
||||
def generate_c_definitions(self, output_header, output_source):
|
||||
"""Generate the C definitions"""
|
||||
with open(output_header, 'w', encoding='utf-8') as out_fd:
|
||||
include_guard = os.path.basename(output_header).replace('-', '_').replace('.', '_').upper()
|
||||
|
||||
print("// Autogenerated config identifiers - DO NOT EDIT\n", file=out_fd)
|
||||
print(f"#ifndef {include_guard}", file=out_fd)
|
||||
print(f"#define {include_guard}", file=out_fd)
|
||||
print(file=out_fd)
|
||||
|
||||
max_sec_val = max(self.sections.values()) + 1
|
||||
max_opt_val_global = 0
|
||||
for section, value in self.sections.items():
|
||||
print(f"#define CFG_SECTION_{section} {value}", file=out_fd)
|
||||
max_opt_val = max(self.options[section].values()) + 1
|
||||
max_opt_val_global = max(max_opt_val, max_opt_val_global)
|
||||
|
||||
for option, value in self.options[section].items():
|
||||
print(f"#define CFG_OPTION_{section}_{option} {value}", file=out_fd)
|
||||
|
||||
print(f"#define CFG_OPTION_{section}_MAX_OPTIONS {max_opt_val}\n", file=out_fd)
|
||||
|
||||
print(f"#define CFG_SECTION_MAX {max_sec_val}\n", file=out_fd)
|
||||
print(f"#define CFG_SECTION_MAX_OPT_VAL {max_opt_val_global}\n", file=out_fd)
|
||||
|
||||
print("extern const char * section_names[CFG_SECTION_MAX];", file=out_fd)
|
||||
print("extern const char * option_names[CFG_SECTION_MAX][CFG_SECTION_MAX_OPT_VAL];", file=out_fd)
|
||||
|
||||
print(f"#endif // !defined {include_guard}", file=out_fd)
|
||||
|
||||
with open(output_source, 'w', encoding='utf-8') as out_fd:
|
||||
print("// Autogenerated config string table - DO NOT EDIT\n", file=out_fd)
|
||||
print(f'#include "{os.path.basename(output_header)}"', file=out_fd)
|
||||
|
||||
print("const char * section_names[CFG_SECTION_MAX] = {", file=out_fd)
|
||||
for section, value in self.sections.items():
|
||||
print(f' [{value}] = "{section.lower()}",', file=out_fd)
|
||||
print("};\n", file=out_fd)
|
||||
|
||||
print("const char * options_names[CFG_SECTION_MAX][CFG_SECTION_MAX_OPT_VAL] = {", file=out_fd)
|
||||
for section, value in self.sections.items():
|
||||
print(f' [{value}] =', '{', file=out_fd)
|
||||
for option, value in self.options[section].items():
|
||||
print(f' [{value}] = "{option.lower()}",', file=out_fd)
|
||||
print(' },', file=out_fd)
|
||||
print("};\n", file=out_fd)
|
||||
|
||||
|
||||
def generate_py_definitions(self, output_file):
|
||||
"""Generate the Python definitions"""
|
||||
try:
|
||||
out_fd = open(output_file, 'w', encoding='utf-8')
|
||||
|
||||
print("'''Autogenerated config identifiers from x52d.conf'''", file=out_fd)
|
||||
print("# DO NOT EDIT\n", file=out_fd)
|
||||
print("from enum import Enum", file=out_fd)
|
||||
|
||||
print("\nclass Section(Enum):", file=out_fd)
|
||||
print(" '''Section identifiers'''", file=out_fd)
|
||||
for section, value in self.sections.items():
|
||||
print(f" {section} = {value}", file=out_fd)
|
||||
|
||||
for section in self.sections.keys():
|
||||
print(f"\nclass {section}(Enum):", file=out_fd)
|
||||
print(f" '''Section {section} identifiers'''", file=out_fd)
|
||||
for option, value in self.options[section].items():
|
||||
print(f" {option} = {value}", file=out_fd)
|
||||
|
||||
finally:
|
||||
out_fd.close()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Generate C enum and python enum for config")
|
||||
parser.add_argument('input_file')
|
||||
parser.add_argument('registry')
|
||||
parser.add_argument('output_c_header')
|
||||
parser.add_argument('output_c_strings')
|
||||
parser.add_argument('output_py_defs')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
c2e = ConfigToEnum(args.input_file, args.registry)
|
||||
c2e.parse()
|
||||
c2e.save_registry()
|
||||
|
||||
c2e.generate_c_definitions(args.output_c_header, args.output_c_strings)
|
||||
c2e.generate_py_definitions(args.output_py_defs)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Saitek X52 Pro MFD & LED driver - communication library interal functions
|
||||
*
|
||||
* Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org)
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only WITH Classpath-exception-2.0
|
||||
*/
|
||||
|
||||
#ifndef X52DCOMM_INTERNAL_H
|
||||
#define X52DCOMM_INTERNAL_H
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
|
||||
#define X52D_BUFSZ 1024
|
||||
|
||||
const char *x52d_command_sock_path(const char *sock_path);
|
||||
int x52d_setup_command_sock(const char *sock_path, struct sockaddr_un *remote);
|
||||
const char *x52d_notify_sock_path(const char *sock_path);
|
||||
int x52d_setup_notify_sock(const char *sock_path, struct sockaddr_un *remote);
|
||||
int x52d_set_socket_nonblocking(int sock_fd);
|
||||
int x52d_listen_socket(struct sockaddr_un *local, int len, int sock_fd);
|
||||
void x52d_split_args(int *argc, char **argv, char *buffer, int buflen);
|
||||
|
||||
#endif // !defined X52DCOMM_INTERNAL_H
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue