Compare commits
| Author | SHA256 | Date | |
|---|---|---|---|
|
|
febdbc059e | ||
|
|
d6bc883520 | ||
|
|
b2d20671cd | ||
|
|
a72e2e7014 | ||
|
|
cf6ca1d96b | ||
|
|
9d1e49949a | ||
|
|
ea3b34e11e | ||
|
|
09dc55db0e | ||
|
|
85133db483 | ||
|
|
6fc7d75e17 | ||
|
|
be321813fa | ||
|
|
d2c0ecdf62 | ||
|
|
266a74ef79 | ||
|
|
18d27c0146 | ||
|
|
24cca1f1c6 | ||
|
|
8c4997ee2e | ||
|
|
118be418b5 | ||
|
|
2a4c2c64c9 | ||
|
|
172f007e1e | ||
|
|
4cb98f7ca0 | ||
|
|
af12d7b954 | ||
|
|
c9cfb394d7 | ||
|
|
0d2d7b6dc5 | ||
|
|
076b43e542 | ||
|
|
cc1f78106d | ||
|
|
5a5e9c01ce | ||
|
|
0fb1147eb7 | ||
|
|
2b23aa7b95 | ||
|
|
bb92cb6b2b | ||
|
|
0730837ad7 | ||
|
|
98a2631590 | ||
|
|
faada9dcdb | ||
|
|
1b7a2a8f7c | ||
|
|
9c588bd9b5 | ||
|
|
31563fe9ed | ||
|
|
81bef1e1cc | ||
|
|
0240db59ee | ||
|
|
ab4dab34aa | ||
|
|
63f13a4c29 | ||
|
|
0a416a2f5c | ||
|
|
b0887648f7 | ||
|
|
5720f1cb50 | ||
|
|
9324da5cb7 | ||
|
|
408b0eeb39 | ||
|
|
ed83b1f906 | ||
|
|
3068c3e2b8 | ||
|
|
93c6f247f7 | ||
|
|
05a9441493 | ||
|
|
aa02e92e4d | ||
|
|
c397c28acb | ||
|
|
c93cc6c522 | ||
|
|
0cdcc77606 | ||
|
|
87eec7e5c9 | ||
|
|
44a1ba01f3 | ||
|
|
d49661fa29 | ||
|
|
71fdee0cfd | ||
|
|
1ced351ea2 | ||
|
|
c048347f2e | ||
|
|
be4f76834a | ||
|
|
23edad416c | ||
|
|
f3e4233285 | ||
|
|
84e0f039cb | ||
|
|
1f8b20a7d1 | ||
|
|
f0e1ab3af8 | ||
|
|
112ab4d5d5 | ||
|
|
8768e142e3 | ||
|
|
827d2e9c3e | ||
|
|
0f3c4a621d | ||
|
|
e60475f351 | ||
|
|
0f63f7dae6 | ||
|
|
127c561a41 | ||
|
|
f9a15ab192 | ||
|
|
77f5759d60 | ||
|
|
684f3237cf | ||
|
|
23e61cc182 | ||
|
|
d2f45ff67a | ||
|
|
06e12e9103 | ||
|
|
29dddeff4f | ||
|
|
017d568aea | ||
|
|
c91b52cfd2 | ||
|
|
2bd38d8d78 | ||
|
|
7d9db68d80 | ||
|
|
4b94303d67 | ||
|
|
08628704c7 | ||
|
|
f1c1132690 | ||
|
|
d2426c473c | ||
|
|
66986b804c | ||
|
|
95daa230bb | ||
|
|
365b22d778 | ||
|
|
cf2b54464e | ||
|
|
4e60c1274a | ||
|
|
2f65e63fbe | ||
|
|
b461431197 | ||
|
|
5c92b6a734 | ||
|
|
ba348dafb3 | ||
|
|
ce2d310e8c | ||
|
|
475db28095 | ||
|
|
823a41c027 | ||
|
|
2a834f1b14 | ||
|
|
c8ffb6cf29 | ||
|
|
ecc9efd434 | ||
|
|
dd35e56029 | ||
|
|
d0e7998650 | ||
|
|
fec5e49304 | ||
|
|
3b12e14e71 | ||
|
|
86eaf2139d | ||
|
|
65fad993ad | ||
|
|
55e6e477be | ||
|
|
ba5efcc152 | ||
|
|
26253564d5 | ||
|
|
92791c77a9 | ||
|
|
465792b2ab | ||
|
|
de269fd828 | ||
|
|
8c91484f37 | ||
|
|
6904ac8b7c | ||
|
|
aea6bbcb0e | ||
|
|
a788d8bcf5 | ||
|
|
cc074a941f | ||
|
|
47574100f9 | ||
|
|
7ad74942e0 | ||
|
|
ac1cc04637 | ||
|
|
722d055e2d | ||
|
|
a9a55da8e0 | ||
|
|
f2b23ace8b | ||
|
|
1f2048e270 | ||
|
|
b16a23243e | ||
|
|
653f1268a6 | ||
|
|
56db6d0add | ||
|
|
cf2152dcfc | ||
|
|
a95bd245cf | ||
|
|
92fd315505 | ||
|
|
2225c2d173 | ||
|
|
f8a76bcd7f | ||
|
|
3efa8bb7ee | ||
|
|
5c155ef503 | ||
|
|
41d199e24a | ||
|
|
e1f2b54de3 | ||
|
|
d6c5757dfa | ||
|
|
9a489801c5 | ||
|
|
9fcdcd087b | ||
|
|
af1304022e | ||
|
|
7972676eb8 | ||
|
|
bef205aec7 | ||
|
|
49fdbbf7ae | ||
|
|
dd69a52273 | ||
|
|
c681b4d684 | ||
|
|
b166013707 | ||
|
|
3e04727022 | ||
|
|
5d13112b00 | ||
|
|
373f88086e | ||
|
|
05492306c0 | ||
|
|
423d490939 | ||
|
|
7edc0ba901 | ||
|
|
0ebb71daf1 | ||
|
|
b4480d89cf | ||
|
|
ff584ba5d1 | ||
|
|
4b15cabd4f | ||
|
|
be4a2d135a | ||
|
|
69f0fdf120 | ||
|
|
e3bebff618 | ||
|
|
f19f7b0ec4 | ||
|
|
ca4cfd9d8d | ||
|
|
96d292074b | ||
|
|
0536a018c6 | ||
|
|
81d1b84a7d | ||
|
|
61c21b245e | ||
|
|
919387f581 | ||
|
|
3b8ea70d3c | ||
|
|
477ab3b580 | ||
|
|
a1da814030 | ||
|
|
19fd5611b2 | ||
|
|
556004a557 | ||
|
|
fba6d6bba0 | ||
|
|
04252e006b | ||
|
|
436e1f0c53 | ||
|
|
21030b1d51 | ||
|
|
b583a86ade | ||
|
|
3262ec9b4a | ||
|
|
0c9afea67a | ||
|
|
b83543d018 | ||
|
|
d4a0185507 | ||
|
|
42dcf6970d | ||
|
|
0b4374141e | ||
|
|
652ddc9d88 | ||
|
|
cf6a2830c8 | ||
|
|
01d9553db4 | ||
|
|
578b526f96 | ||
|
|
e3061b46f9 | ||
|
|
2559f1e66b | ||
|
|
9ca469a075 | ||
|
|
519bce6b78 | ||
|
|
557ea96be0 | ||
|
|
9a49cc67f0 | ||
|
|
3012f0799b | ||
|
|
7a8852f64b | ||
|
|
f92e6c3cf1 | ||
|
|
72dc83daff | ||
|
|
5981d3f871 | ||
|
|
fb0c5ad3f8 | ||
|
|
04d9d588e8 | ||
|
|
e5fe925023 | ||
|
|
345a21a211 | ||
|
|
b83e8c3979 | ||
|
|
0744ac3163 | ||
|
|
3de992d251 | ||
|
|
369ef61cab | ||
|
|
e6e96c4b0d | ||
|
|
dc96033cb1 | ||
|
|
9ee6bf4380 | ||
|
|
e0f0726e68 | ||
|
|
105a56499d | ||
|
|
2bd27cd73b | ||
|
|
aed64e76a7 | ||
|
|
abfd073de8 | ||
|
|
41edd1423c | ||
|
|
471fde78c1 | ||
|
|
3e4759a0c9 | ||
|
|
b5276890fb | ||
|
|
1488bc3d6d | ||
|
|
f4e7210a40 | ||
|
|
e385bb6bf9 | ||
|
|
f56e531384 | ||
|
|
32606fe1c2 | ||
|
|
a8734846a0 | ||
|
|
ad0edf3c88 | ||
|
|
66975862f7 | ||
|
|
b9185e761b | ||
|
|
885cf463a7 | ||
|
|
689f35fea2 | ||
|
|
6bf5d1d5ed | ||
|
|
b0b8c7a445 | ||
|
|
d25c19cdaa | ||
|
|
89d06d317b | ||
|
|
c5ec32f87a | ||
|
|
ce5c348023 | ||
|
|
832eea5889 | ||
|
|
60049442f1 | ||
|
|
624557ebfd | ||
|
|
6b0379bfdc | ||
|
|
a9510a6d36 | ||
|
|
59e4156bb9 | ||
|
|
de9606519a | ||
|
|
eeb115584d | ||
|
|
ee3721dfa4 | ||
|
|
239cc231ea | ||
|
|
4bd4df7b09 | ||
|
|
d12371b84f | ||
|
|
c97b3e3ec3 | ||
|
|
2c2aad1355 | ||
|
|
9949935bcc | ||
|
|
35fc6ebf62 | ||
|
|
d2205648e6 | ||
|
|
68ed93dd24 | ||
|
|
a06b76b800 | ||
|
|
67f882b9bc | ||
|
|
17dc4981c6 | ||
|
|
0179b25d12 | ||
|
|
e3c1cbf1c0 | ||
|
|
5899bd2f77 | ||
|
|
1b0e1cf1d4 | ||
|
|
134e877b7c | ||
|
|
b75ac46781 | ||
|
|
edc94d3700 | ||
|
|
b13efa92fd | ||
|
|
6f796c98f7 | ||
|
|
3a5856c7f0 | ||
|
|
775b655aac | ||
|
|
101fd2eaa4 | ||
|
|
6f0bb01b61 | ||
|
|
b345900459 | ||
|
|
f1cfe9b6aa | ||
|
|
8941582d54 | ||
|
|
8c5de781ea | ||
|
|
baef264bd0 | ||
|
|
74df7e2645 | ||
|
|
56cd90a197 | ||
|
|
abdce05136 | ||
|
|
35565845ca | ||
|
|
a83ec2c971 | ||
|
|
4b371e142d | ||
|
|
aa2644d812 | ||
|
|
fd99250882 | ||
|
|
5344c42ceb | ||
|
|
21413268f3 | ||
|
|
aa35d87885 | ||
|
|
a53444b863 | ||
|
|
4b0031fb08 | ||
|
|
62b8534769 | ||
|
|
f3262c2d64 | ||
|
|
7986184111 | ||
|
|
9c35567389 | ||
|
|
8325cbec84 | ||
|
|
c6d310184b | ||
|
|
83892d5093 | ||
|
|
1e1cdd9e76 | ||
|
|
3e62a2a01c | ||
|
|
90d10086d7 | ||
|
|
db2d9a666b | ||
|
|
3a0899bcfe | ||
|
|
d13c60fca1 | ||
|
|
580bd6fbeb | ||
|
|
49ebf1605a | ||
|
|
f1fbb35296 | ||
|
|
b85643ca33 | ||
|
|
c27da63a3e | ||
|
|
a332ddc828 | ||
|
|
ab31ccf6d8 | ||
|
|
01b38952e5 | ||
|
|
0fdb5b245c | ||
|
|
94263a46bd | ||
|
|
56a69ab683 | ||
|
|
e73e103ac4 | ||
|
|
b8b33696ec | ||
|
|
c0b0c99f53 | ||
|
|
a2954071bd | ||
|
|
76e4a6cba0 | ||
|
|
b55fd1571e | ||
|
|
8de4e95c6a | ||
|
|
ddeaf82bfd | ||
|
|
e95f65ac78 | ||
|
|
97d37a2eb6 | ||
|
|
a23d090bc1 | ||
|
|
4956ba7352 | ||
|
|
acdd6c928b | ||
|
|
6774c26ea1 | ||
|
|
ef0cd2cb7d | ||
|
|
e921b06826 | ||
|
|
c0c29b74ab | ||
|
|
310863faec | ||
|
|
b05da86197 | ||
|
|
c44d755ce0 | ||
|
|
b7e6cf7437 | ||
|
|
e73328461e | ||
|
|
db93eace30 | ||
|
|
c5dfa47903 | ||
|
|
d96985303d | ||
|
|
27bd47dbe0 | ||
|
|
e3377a48b3 | ||
|
|
a2ed41514d | ||
|
|
78ee5a60fa | ||
|
|
bff403ea04 | ||
|
|
3061bf3d1e | ||
|
|
9b03273055 | ||
|
|
bdcab5ee05 | ||
|
|
14cc3be620 | ||
|
|
50da3e868d | ||
|
|
2c68dedea2 | ||
|
|
da12521517 | ||
|
|
28bbdb8b7c | ||
|
|
1e8e2915f9 | ||
|
|
2350745e61 | ||
|
|
1fec6c7b54 | ||
|
|
3e10407afd | ||
|
|
3d5b5b2214 | ||
|
|
c094af920e | ||
|
|
dafdae5276 | ||
|
|
df7f38bd0a | ||
|
|
4c1aeeeac8 | ||
|
|
667c5310bf | ||
|
|
126cf2f5c3 | ||
|
|
c8fa4a01a1 | ||
|
|
e764a713c4 | ||
|
|
93aef6e18b | ||
|
|
f213e9aa43 | ||
|
|
630ba30c27 | ||
|
|
b2d3061796 | ||
|
|
ecb018b32c | ||
|
|
e9f58ca004 | ||
|
|
c0dfa6c7ab | ||
|
|
78d6124f2a | ||
|
|
1a8d1c70fd | ||
|
|
e0333a9c32 | ||
|
|
9a3bc9e488 | ||
|
|
a905822515 | ||
|
|
d7c7bb3c23 | ||
|
|
97a2bee81a | ||
|
|
58bbf063ca | ||
|
|
0159dd9074 | ||
|
|
29a07a9a8b | ||
|
|
c824fb5e9b | ||
|
|
6a9746c17a | ||
|
|
af0e9e51e0 | ||
|
|
5713287c84 | ||
|
|
bd0c3dba50 | ||
|
|
e63c53a855 | ||
|
|
8be56192cb | ||
|
|
d07602b0a9 | ||
|
|
185ba5b1d3 | ||
|
|
2d48ae7a16 | ||
|
|
bec1d08757 | ||
|
|
cc59bd18ee | ||
|
|
f0b560ec06 | ||
|
|
ba3ee4290f | ||
|
|
30fcde5744 | ||
|
|
4a92a7fa22 | ||
|
|
9b188d56e9 | ||
|
|
c7bf8051b9 | ||
|
|
7591fbdace | ||
|
|
5d4d451943 | ||
|
|
828820b6e4 | ||
|
|
eaad476bf5 | ||
|
|
0b7691bdea | ||
|
|
21fbc8ffa0 | ||
|
|
24be1d0c1f | ||
|
|
2830f75f65 | ||
|
|
c4a3ff6f68 | ||
|
|
cfc92beec0 | ||
|
|
cdfc416d02 | ||
|
|
e17a6765ec | ||
|
|
7bdb3118ae | ||
|
|
126b4ba3a1 | ||
|
|
07e57b8563 | ||
|
|
a9c69e5947 | ||
|
|
1340c8e9c6 | ||
|
|
62e55dbaec | ||
|
|
1ee2a1cf62 | ||
|
|
ad45e005f5 | ||
|
|
0c7d8fac02 | ||
|
|
0b7ad79032 | ||
|
|
e2a9caa07d | ||
|
|
619d2145f9 | ||
|
|
3016d25f73 | ||
|
|
4deaedf79f | ||
|
|
3a412fcd51 | ||
|
|
d9e61e7c5b | ||
|
|
f3e4651bd5 | ||
|
|
9cbff47194 | ||
|
|
525627c972 | ||
|
|
6d8777da83 | ||
|
|
8fb428ef0d | ||
|
|
1bfe742278 | ||
|
|
76b1131d95 | ||
|
|
670e7e9743 | ||
|
|
ff911636c7 | ||
|
|
61f4c1e115 | ||
|
|
3ded1da707 | ||
|
|
c9e4b8dfbf | ||
|
|
c08826e848 | ||
|
|
bc8f4a0582 | ||
|
|
ad323d17a2 | ||
|
|
09566fdfde | ||
|
|
91ed444c90 | ||
|
|
32c046233b | ||
|
|
cf5460c5c7 | ||
|
|
c25393e3b6 | ||
|
|
8a83ac85d9 | ||
|
|
78e62997d1 | ||
|
|
c0fba4af94 | ||
|
|
9723696b2c | ||
|
|
eb5593c7be | ||
|
|
089146a137 | ||
|
|
d4c8201a88 | ||
|
|
538ec8ec73 | ||
|
|
52fa631733 | ||
|
|
284e962910 | ||
|
|
6ba7a54921 | ||
|
|
ecd059ced2 | ||
|
|
4f825e2a86 | ||
|
|
1bf1c768dd | ||
|
|
b33fa4aeaa | ||
|
|
889ce0d921 | ||
|
|
1c9841b4a6 | ||
|
|
99cf000f24 | ||
|
|
3d780a2605 | ||
|
|
1aabcf4d80 | ||
|
|
51de9779e3 | ||
|
|
2f9cf2bff1 | ||
|
|
6d3719ba71 | ||
|
|
dabda362e6 | ||
|
|
b23ecdfdf2 | ||
|
|
18bf5d65d7 | ||
|
|
37c36ffdba | ||
|
|
c7440e2b5c | ||
|
|
6949fd8a2f | ||
|
|
ef72719502 | ||
|
|
a647091a3f | ||
|
|
b5706d3ed5 | ||
|
|
0b2bee0a3d | ||
|
|
4cee326a25 | ||
|
|
bf4cecde05 | ||
|
|
84bef3365e | ||
|
|
922c18db4b | ||
|
|
22fb35d1d4 | ||
|
|
ebf7c9f18e | ||
|
|
43b0efb4d3 | ||
|
|
4430615117 | ||
|
|
e84c63c3d1 | ||
|
|
580695b486 | ||
|
|
9f1ca37977 | ||
|
|
c1964adb58 | ||
|
|
ad5525d88b | ||
|
|
98d478531b | ||
|
|
a2495afa44 | ||
|
|
3f5f94a53f | ||
|
|
97840a45d6 | ||
|
|
69cd33479b | ||
|
|
376d42cd79 | ||
|
|
b69075cbac | ||
|
|
bbca821dcd | ||
|
|
d9fe1f02b8 | ||
|
|
5fe41c7656 | ||
|
|
9cf6fabe64 | ||
|
|
cd0352f904 | ||
|
|
fa30bd2a49 | ||
|
|
e9e05c1192 | ||
|
|
b7025dde59 | ||
|
|
973a632b85 | ||
|
|
9d0da4b39f | ||
|
|
aba86fc687 | ||
|
|
4c87207129 | ||
|
|
7a167b470a | ||
|
|
1ea5390771 | ||
|
|
a218f6586d | ||
|
|
4753b83831 | ||
|
|
a2626dfdd0 | ||
|
|
8e19486cf5 | ||
|
|
e2b89da2fa | ||
|
|
f1af2bd4d4 | ||
|
|
1c94bb25a6 | ||
|
|
06c77b1c1f | ||
|
|
8bcaa192c5 | ||
|
|
8fd7f4676b | ||
|
|
93c007b2b9 | ||
|
|
eb922d918b | ||
|
|
7ba333bf6c | ||
|
|
94777c58c6 | ||
|
|
a6a5089379 | ||
|
|
eb122456ab | ||
|
|
55d34e2a87 | ||
|
|
bfffe44c4a | ||
|
|
dd49c4de00 | ||
|
|
eef760d776 | ||
|
|
fa019bcb4f | ||
|
|
432b574592 | ||
|
|
c3d20ba338 | ||
|
|
be7a3ab7a6 | ||
|
|
272d7ca1be | ||
|
|
05a4714fb1 | ||
|
|
59e5df0dd3 | ||
|
|
771758c831 | ||
|
|
ca55bfca93 | ||
|
|
71f1a6179c | ||
|
|
c13940216b | ||
|
|
f17d077f25 | ||
|
|
62ea49d1fc | ||
|
|
f653689112 | ||
|
|
df03f3f4ba | ||
|
|
34e8640e78 | ||
|
|
b6b50557a7 | ||
|
|
08d90b6e8e | ||
|
|
43a26007d6 | ||
|
|
7fdc890a85 | ||
|
|
ae3838ccf2 | ||
|
|
783b5b08e3 | ||
|
|
015caec01c | ||
|
|
526e2d9cc4 | ||
|
|
3f374f48e1 | ||
|
|
795341dd8d | ||
|
|
c523816cdf | ||
|
|
b26e09904a | ||
|
|
1526392ca5 | ||
|
|
809d897da6 | ||
|
|
eeb8ee9069 | ||
|
|
25aa57dc5e | ||
|
|
6c2449f623 | ||
|
|
d8057807a3 | ||
|
|
e532401a75 | ||
|
|
f8cc12560e | ||
|
|
c8ee9925a1 | ||
|
|
d460ea2952 | ||
|
|
e1b2c62231 | ||
|
|
bead78b372 | ||
|
|
4e14f300f9 | ||
|
|
4759521176 | ||
|
|
a309b6f3ef | ||
|
|
834cf98ef9 | ||
|
|
80ea016687 | ||
|
|
5ecaf67bcb | ||
|
|
33635886e0 | ||
|
|
bba4b7fb41 | ||
|
|
26afcb892a | ||
|
|
9633e3528d | ||
|
|
62e4338e88 | ||
|
|
d949895fec | ||
|
|
b5fa05a660 | ||
|
|
c515d5287e | ||
|
|
ae63a653c8 | ||
|
|
03b6ff3c32 | ||
|
|
935ffecbb0 | ||
|
|
c140e3aae4 | ||
|
|
7f92dc5f51 | ||
|
|
627321d4ae | ||
|
|
0c49cae055 | ||
|
|
3cafd29ee5 | ||
|
|
6c4d8cd51b | ||
|
|
d6d2bfeb73 | ||
|
|
1b1da19d3d | ||
|
|
4fb6b10a97 | ||
|
|
e9c11d6b75 | ||
|
|
2037ebaa8b | ||
|
|
45a862b11f | ||
|
|
29c6e5a0f6 | ||
|
|
aa2caf1f10 | ||
|
|
8188b91f86 | ||
|
|
eb37b43de4 | ||
|
|
eaf1affb27 | ||
|
|
ab44cc5282 | ||
|
|
19c4fd6cd1 | ||
|
|
096246542d | ||
|
|
a6be7b75aa | ||
|
|
80ffba545a | ||
|
|
dbf1f22bac | ||
|
|
7f91c60d26 | ||
|
|
7072882b0b | ||
|
|
00fc9e3926 | ||
|
|
95ec6ba037 | ||
|
|
87da6efbfb | ||
|
|
2ab1bbc02c | ||
|
|
47c53c1a14 | ||
|
|
888bb1595f | ||
|
|
2ed4f6d666 | ||
|
|
2b5fa16824 | ||
|
|
199769cac0 | ||
|
|
c9bfa2d01a | ||
|
|
fc748a744c | ||
|
|
5d8dd86c96 |
@@ -1,4 +1,11 @@
|
||||
## папки с данными создавайемыми при работе сервера
|
||||
data/
|
||||
logs/
|
||||
logs
|
||||
.understand-anything/
|
||||
|
||||
.gradle
|
||||
.gradle-home/
|
||||
build/
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
!**/src/main/**/build/
|
||||
@@ -40,4 +47,66 @@ bin/
|
||||
.vscode/
|
||||
|
||||
### Mac OS ###
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
|
||||
# временный debug token
|
||||
.debug-token
|
||||
|
||||
# Локальные артефакты и секреты Solana-модуля
|
||||
shine-solana/.git/
|
||||
shine-solana/.git-local-backup/
|
||||
shine-solana/.idea/
|
||||
shine-solana/shine/.idea/
|
||||
shine-solana/shine/.gradle/
|
||||
shine-solana/shine/.anchor/
|
||||
shine-solana/shine/.yarn/
|
||||
shine-solana/shine/.vendor/
|
||||
shine-solana/shine/node_modules/
|
||||
shine-solana/shine/target/
|
||||
shine-solana/shine/test-ledger/
|
||||
shine-solana/shine/old_vers/
|
||||
shine-solana/shine/program-keypair.json
|
||||
shine-solana/shine/keys/
|
||||
shine-solana/shine/validator.log
|
||||
shine-solana/shine/doc/КОШЕЛЬКИ_DEVNET_ТЕСТ.md
|
||||
shine-solana/shine/scripts/del/
|
||||
shine-solana/shine/scripts/**/keypairs/
|
||||
shine-solana/shine/scripts/**/runs/
|
||||
shine-solana/shine/scripts/**/*.env
|
||||
shine-solana/shine/scripts/**/TEMP_*.md
|
||||
|
||||
# Локальные артефакты и внешние материалы ESP32-подпроекта
|
||||
ESP32/esp32-config-tool/
|
||||
ESP32/**/.git/
|
||||
ESP32/**/.idea/
|
||||
ESP32-wallet/.idea/
|
||||
ESP32/**/.arduino-build/
|
||||
ESP32/**/official-demo/
|
||||
!ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/
|
||||
ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/**
|
||||
!ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/examples/
|
||||
ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/examples/**
|
||||
!ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/examples/Arduino-v3.3.5/
|
||||
ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/examples/Arduino-v3.3.5/**
|
||||
!ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/examples/Arduino-v3.3.5/libraries/
|
||||
ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/examples/Arduino-v3.3.5/libraries/**
|
||||
!ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/official-demo/examples/Arduino-v3.3.5/libraries/lv_conf.h
|
||||
ESP32/**/original-firmware/*.bin
|
||||
ESP32/**/original-firmware/*.bin.sha256
|
||||
ESP32/**/*.elf
|
||||
ESP32/**/*.map
|
||||
ESP32/**/*.merged.bin
|
||||
ESP32/**/*.uf2
|
||||
ESP32/**/*.o
|
||||
ESP32/**/*.d
|
||||
ESP32/**/*.a
|
||||
|
||||
# Полные серверные бэкапы (тяжёлые архивы, не коммитим)
|
||||
server-backup/archive/**
|
||||
!server-backup/archive/.gitkeep
|
||||
|
||||
# Локальная дев-обвязка Claude (дев-сервер shine-UI, сессии, планы) — не коммитим
|
||||
.claude/
|
||||
# Рабочие бэкапы/превью-ассеты UI — не для репозитория
|
||||
*.bak.png
|
||||
shine-UI/assets/navbar_preview.png
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
shine-server-server
|
||||
@@ -13,6 +13,8 @@
|
||||
<option value="$PROJECT_DIR$/shine-server-config" />
|
||||
<option value="$PROJECT_DIR$/shine-server-crypto" />
|
||||
<option value="$PROJECT_DIR$/shine-server-db" />
|
||||
<option value="$PROJECT_DIR$/shine-server-geo" />
|
||||
<option value="$PROJECT_DIR$/shine-server-log" />
|
||||
<option value="$PROJECT_DIR$/shine-server-net-protocol" />
|
||||
<option value="$PROJECT_DIR$/shine-server-net-server" />
|
||||
</set>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,160 @@
|
||||
# AGENTS
|
||||
|
||||
## Язык проекта
|
||||
- По умолчанию использовать русский язык во всех пользовательских текстах и технических пояснениях.
|
||||
- Пояснения к коммитам, PR и merge-запросам писать на русском языке.
|
||||
- Комментарии в коде, встроенные справки, документацию и инструкции писать по возможности на русском языке.
|
||||
|
||||
## Примечание
|
||||
- Если внешний инструмент/интеграция требует английский формат, допускается английский, но рядом желательно дать краткое пояснение на русском.
|
||||
|
||||
## Структура проекта (кратко)
|
||||
- Серверный код SHiNE находится в папке `SHiNE-server/`.
|
||||
- Код клиентского UI SHiNE находится в папке `shine-UI/`.
|
||||
- Веб-панель администратора сервера (управление Solana PDA сервера) находится в `shine-UI/`:
|
||||
- точка входа `shine-UI/server-ui.html`;
|
||||
- остальные файлы серверного UI — в `shine-UI/server-ui/`.
|
||||
- Локальный Telegram-бот агента-кодера находится в папке `SHiNE-agent-bot-coder/` и не является кодом основного серверного приложения.
|
||||
- Solana/Anchor-модуль находится в папке `shine-solana/shine/` и ведётся отдельно от основного server/UI деплоя.
|
||||
|
||||
## Сервис агента-кодера
|
||||
- В проекте есть локальный Telegram-бот-сервис агента-кодера в папке `SHiNE-agent-bot-coder/`.
|
||||
- Сервис принимает сообщения из Telegram, ведёт историю диалога, ставит задачи в очередь и вызывает Codex CLI для обработки запросов по проекту.
|
||||
- Автоматически читаемые инструкции для Codex внутри сервиса держать в `SHiNE-agent-bot-coder/AGENTS.md`.
|
||||
- Подробные служебные правила Telegram-обработчика, его очередь, история, systemd-запуск и особенности ответов описывать в `SHiNE-agent-bot-coder/AGENT.md`.
|
||||
- Если в сообщениях пользователя встречается «агент MD» или похожая формулировка про файл инструкций Codex, считать, что имеется в виду автоматически читаемый `AGENTS.md`.
|
||||
|
||||
## ESP32 UI homeserver
|
||||
- Для UI-скетча устройства `ESP32-S3-Touch-AMOLED-2.16` документ-спецификация и Arduino-скетч должны всегда оставаться синхронными.
|
||||
- Актуальный документ по экранной логике, состояниям, кнопкам, полям, статусам и ограничениям UI считать источником истины для скетча.
|
||||
- При изменении документа обязательно в том же наборе изменений приводить в соответствие скетч.
|
||||
- При изменении скетча обязательно в том же наборе изменений обновлять документ, если поменялись экраны, тексты, переходы, статусы, кнопки, поля или поведение.
|
||||
- Для нового ESP32 UI-прототипа homeserver использовать русский язык как основной и отдельно следить, чтобы текст реально отображался на устройстве, а не только логически присутствовал в коде.
|
||||
|
||||
## Solana-модуль
|
||||
- В проекте есть отдельный Solana/Anchor-модуль в папке `shine-solana/shine/`.
|
||||
- Модуль логически связан с SHiNE, но не должен автоматически подключаться к сборке или деплою основного сервера без отдельного решения.
|
||||
- В Solana-модуле действуют локальные инструкции `shine-solana/shine/AGENTS.md`; при изменениях внутри модуля сначала читать их.
|
||||
- В git добавлять исходники, lock-файлы, настройки проекта и документацию Solana-модуля, но не добавлять локальные ключи, `.git`, `.idea`, `.gradle`, `target`, `node_modules`, `test-ledger`, логи, временные run-отчёты и `.env`-конфиги.
|
||||
- Для Solana deploy/push использовать правила из локального `shine-solana/shine/AGENTS.md`; не смешивать deploy Solana-модуля с `deployServer`/`deployUI` основного проекта.
|
||||
- Для регистрации пользователей в Solana (программа `shine_users`) единая актуальная инструкция по деплою/инициализации, адресам программ, и куда их прописывать в UI/сервере находится в:
|
||||
- `Dev_Docs/Инициализация_Solana_регистрации/README.md`
|
||||
- Этот файл считать основной справкой (single source of truth) по деплою и первичной инициализации Solana-регистрации в текущем проекте.
|
||||
- Актуальная архитектурная справка по устройству Solana-программ, PDA-счетам, ролям DAO и движению средств находится в:
|
||||
- `Dev_Docs/Solana_Architecture/README.md`
|
||||
- Документ формата пользовательской PDA-записи `shine_users` находится в:
|
||||
- `shine-solana/shine/doc/formats/shine-user-pda-format-v.1.0.md`
|
||||
|
||||
## Документация блокчейна
|
||||
- Актуальная документация по форматам блокчейна находится в `Dev_Docs/Blockchain/README.md`.
|
||||
- Это точка входа (оглавление), рядом расположены детальные файлы по форматам, типам каналов и командным сообщениям.
|
||||
- При любом изменении кода, связанного с блокчейном (формат блока, типы каналов, правила чтения/записи, команды), обязательно обновлять соответствующие документы в `Dev_Docs/Blockchain/`.
|
||||
- Дополнительно обязательно вести `Dev_Docs/Blockchain/CHANGELOG.md`: дописывать изменения построчно с указанием даты/времени и хэша коммита, после которого внесено изменение.
|
||||
- Перед любым изменением формата блокчейна обязательно заранее предупреждать пользователя, что формат будет изменён.
|
||||
- Изменять формат блокчейна можно только после явного подтверждения пользователя (без подтверждения формат не менять).
|
||||
- Добавление любых данных в блокчейн выполнять только через операцию `AddBlock`.
|
||||
- Перед каждым `AddBlock` обязательно проверять/актуализировать текущее состояние вершины блокчейна (`last global number/hash`) и использовать его при формировании блока.
|
||||
|
||||
## Документация личных сообщений (DM)
|
||||
- Актуальная документация по логике личных сообщений находится в `Dev_Docs/Personal_Messages/Протокол_DM_v1.md`.
|
||||
- Точный байтовый формат DM находится в `Dev_Docs/Personal_Messages/Формат_DM_v1.md`.
|
||||
- При любом изменении кода, связанного с личными сообщениями (формат подписанного DM-блока, типы DM-сообщений, правила доставки/ACK/read-receipt, роутинг по сессиям, UI-логика чатов), обязательно обновлять оба документа:
|
||||
- `Dev_Docs/Personal_Messages/Протокол_DM_v1.md`
|
||||
- `Dev_Docs/Personal_Messages/Формат_DM_v1.md`
|
||||
- Логика личных сообщений в коде должна всегда соответствовать этим документам.
|
||||
- Документ по личным сообщениям обязан поддерживаться в актуальном состоянии.
|
||||
|
||||
## Документация API сервера
|
||||
- Актуальная документация по публичному JSON/WebSocket API сервера находится в `Dev_Docs/API/`.
|
||||
- При любом изменении серверного API/эндпоинтов/операций `op` обязательно обновлять соответствующие документы в `Dev_Docs/API/`.
|
||||
- Перед изменением самого серверного API обязательно явно предупредить пользователя, какие операции, поля запросов/ответов или коды ошибок будут изменены, и запросить отдельное подтверждение.
|
||||
- Без явного подтверждения пользователя формат серверного API не менять; допускается только приведение документации в соответствие уже существующему коду.
|
||||
- Если добавляется новая операция `op`, нужно обновить общий список операций в `Dev_Docs/API/09_Operations_Index.md` или создать его, если файла ещё нет.
|
||||
|
||||
## Документация Figma
|
||||
- Актуальная документация по переносу экранов SHiNE в Figma и обратному переносу из Figma в код находится в `Dev_Docs/Figma/`.
|
||||
- Точка входа: `Dev_Docs/Figma/README.md`.
|
||||
- Подробный рабочий регламент: `Dev_Docs/Figma/TRANSFER_UI_SCREENS.md`.
|
||||
- Для экранов регистрации, входа и других чувствительных UI-flow по умолчанию переносить экраны в Figma по одному, а не пачкой, если пользователь отдельно не подтвердил иной способ.
|
||||
|
||||
## Версионирование
|
||||
- Единый файл версий проекта: `VERSION.properties` (в корне репозитория).
|
||||
- Перед каждым новым коммитом обязательно увеличивать версии в `VERSION.properties`:
|
||||
- `client.version` — версия клиентского UI.
|
||||
- `server.version` — версия серверной части.
|
||||
- Базовое правило инкремента: `+1` по последнему числовому сегменту (patch), если не оговорено иное.
|
||||
- Обычные коммиты делать стандартным `git commit`; переменная `$GITEA_TOKEN` для коммитов не нужна и не используется.
|
||||
|
||||
## Deploy
|
||||
- Все документы и заметки по деплою хранить в папке `Deploy/`.
|
||||
- Production-хост SHiNE: `player@shineup.me` (`178.208.64.62`).
|
||||
- Второй production-хост SHiNE: `player@193.8.215.70` (`server2.shineup.me`).
|
||||
- Базовый путь на сервере для SHiNE: `/home/player` (проекты SHiNE размещать в `/home/player/SHiNE/...`).
|
||||
- По возможности все справки, комментарии и примечания в конфигах/документах писать на русском языке.
|
||||
- Для операций `git push` при необходимости использовать токен из переменной окружения `$GITEA_TOKEN`.
|
||||
- Любые изменения и любой деплой на production `shineup.me` выполнять только после отдельного явного подтверждения пользователя.
|
||||
- Если пользователь пишет просто `задеплой` без уточнения production/test, по умолчанию деплоить на `server2.shineup.me`.
|
||||
- Default server deploy: `./gradlew deployServer` или `./gradlew deployServerTest2`.
|
||||
- Default UI deploy: `./gradlew deployUI` или `./gradlew deployUITest2`.
|
||||
- Production server deploy: `./gradlew deployServerProduction`.
|
||||
- Production UI deploy: `./gradlew deployUIProduction`.
|
||||
- Для локального запуска использовать `./gradlew startLocal` (или `startLocalWithBuild`).
|
||||
- Сначала предлагать локальную проверку, а деплой на сервер выполнять по запросу пользователя.
|
||||
- Для временной бесплатной загрузки аватаров в Arweave секретный JWK нельзя хранить в git и нельзя прописывать в репозиторный `application.properties`.
|
||||
- Для продовой настройки тестового Arweave-кошелька JWK-файл нужно хранить только на сервере, например: `/home/player/SHiNE/secrets/test-free-avatar-wallet.json`.
|
||||
- Для этой временной фичи на проде должны быть заданы параметры `test.freeAvatar.walletJwkPath` и `test.freeAvatar.walletAddress` через серверный override-конфиг/секреты на хосте.
|
||||
- После изменения продовых значений `test.freeAvatar.*` нужно заново выполнить серверный деплой или перезапуск сервера, чтобы настройки были перечитаны приложением.
|
||||
- При таких изменениях в git допускается коммитить только документацию и код чтения настроек, но не сам JWK, не содержимое секрета и не реальные приватные ключи.
|
||||
|
||||
## Логи звонков (установка соединения)
|
||||
- Специальный поток диагностики установки звонков идёт через `CallDeliveryReport` (клиент → сервер).
|
||||
- На проде специальный файл для звонков:
|
||||
- `/home/player/SHiNE/shine-server/logs/call-delivery-events.log`
|
||||
- Общий серверный лог (и ротации) на проде:
|
||||
- `/home/player/SHiNE/shine-server/logs/app.log`
|
||||
- `/home/player/SHiNE/shine-server/logs/app.YYYY-MM-DD.log`
|
||||
- Для анализа причин недозвона в первую очередь фильтровать записи по ключам:
|
||||
- `CallDeliveryReport`
|
||||
- `call_connected`
|
||||
- `outgoing_failed`
|
||||
- `incoming_failed`
|
||||
- `call_busy`
|
||||
- `call_declined`
|
||||
- `unknown_error`
|
||||
- В этих записях искать поля `reason`, `failureStage`, `pcConnectionState`, `pcIceConnectionState`, `routeLabel`, `configuredTurnHosts*`, `reachableTurnHosts*`.
|
||||
|
||||
## Недопроверенные фичи (обязательно)
|
||||
- Папка для учёта недопроверенных фич: `Dev_Docs/Pending_Features/`.
|
||||
- По каждой новой доработке, которая требует ручной проверки, добавлять отдельный markdown-файл в `Dev_Docs/Pending_Features/`.
|
||||
- Рекомендуемый формат имени файла: `YYYY-MM-DD_HHMM_<short-feature-name>.md`.
|
||||
- Имена новых файлов и краткие описания фич по возможности писать на русском языке.
|
||||
- Внутри файла обязательно указывать:
|
||||
- краткое описание фичи;
|
||||
- что именно проверять;
|
||||
- ожидаемый результат;
|
||||
- статус (например: `pending`, `in_progress`, `done`).
|
||||
- После подтверждения, что фича проверена и работает корректно, соответствующий файл удалять.
|
||||
- В `Dev_Docs/Pending_Features/README.md` вести краткий регламент и поддерживать актуальность.
|
||||
|
||||
## Будущие фичи / TODO
|
||||
- Папка для задач, сознательно отложенных на будущее: `TODO/`.
|
||||
- Точка входа по планам: `TODO/README.md`.
|
||||
- Внутри планы разделены по горизонтам: `near/`, `medium/`, `far/` и тематическим подпапкам.
|
||||
- Если пользователь спрашивает, какие есть планы или что можно продолжить, сначала читать `TODO/README.md`, затем при необходимости конкретные файлы из подпапок.
|
||||
- Файлы из этой папки не считать активными задачами и не начинать реализацию без явной просьбы пользователя.
|
||||
- Старую папку `Dev_Docs/Future_Features/` считать выведенной из использования и больше не использовать для новых записей.
|
||||
- Если часть кода временно отключена или закомментирована, либо удалена как временная заглушка, в TODO-файле подробно описывать:
|
||||
- какие файлы и участки отключены;
|
||||
- что осталось в коде как заготовка;
|
||||
- какие документы нужно обновить при возврате;
|
||||
- с какого сценария продолжать разработку;
|
||||
- из какого коммита брать последнюю полную реализацию.
|
||||
|
||||
## Коммуникация по новым задачам (обязательно)
|
||||
- При получении нового задания сначала кратко пересказать задачу своими словами.
|
||||
- До начала реализации задать недостающие уточняющие вопросы (если они есть).
|
||||
- Если есть уместные идеи/улучшения — кратко предложить их; если полезных идей нет, ничего дополнительно не предлагать.
|
||||
- Добавлять краткую оценку фичи (насколько это полезно/удачно по мнению исполнителя).
|
||||
- После этого обязательно запросить подтверждение от пользователя, что задача понята верно, и только после подтверждения переходить к реализации.
|
||||
- Если вопросов нет, явно написать в формате: «Я всё понял, начинаю делать?» и ждать подтверждения.
|
||||
- Без подтверждения пользователя реализацию не начинать.
|
||||
@@ -0,0 +1,18 @@
|
||||
# Runbook для агента: тест сетевого соединения
|
||||
|
||||
## Быстрый цикл
|
||||
1. Убедись, что сервер запущен.
|
||||
2. Скажи пользователю: «Запусти двух клиентов и напиши “продолжай”».
|
||||
3. По команде «продолжай»:
|
||||
- вызови `GET /debug/ws/clients`,
|
||||
- выбери 2 активные сессии (предпочтительно разных логинов),
|
||||
- вызови `POST /debug/ws/connect`,
|
||||
- получи `runId`.
|
||||
4. Читай `GET /debug/ws/logs?limit=200&runId=<runId>` и сообщай прогресс.
|
||||
5. Если неуспех — покажи ошибки, предложи перезапуск 2 клиентов и повтори.
|
||||
|
||||
## Формат взаимодействия с пользователем
|
||||
- Старт: «Сервер готов. Запусти двух клиентов и скажи “продолжай”.»
|
||||
- После старта run: «Тест запущен, runId=..., проверяю логи.»
|
||||
- Успех: «Соединение установлено, вижу connected.»
|
||||
- Неуспех: «Соединение не поднялось, причины: ... Предлагаю перезапустить клиентов и повторить.»
|
||||
@@ -0,0 +1,11 @@
|
||||
@AGENTS.md
|
||||
@AGENT_DEBUG_RUNBOOK.md
|
||||
|
||||
## Обязательно читать при работе с UI
|
||||
@shine-UI/AGENTS.md
|
||||
|
||||
## Справка по подпроектам
|
||||
- При работе внутри `SHiNE-agent-bot-coder/` — читать `SHiNE-agent-bot-coder/AGENTS.md` и `SHiNE-agent-bot-coder/AGENT.md`.
|
||||
- При работе внутри `shine-solana/shine/` — читать `shine-solana/shine/AGENTS.md`.
|
||||
- При работе внутри `shine-UI/server-ui/` — читать `shine-UI/AGENTS.md`.
|
||||
- При работе внутри `SHiNE-server/` — читать `SHiNE-server/AGENTS.md`.
|
||||
@@ -0,0 +1,93 @@
|
||||
# Production-серверы SHiNE
|
||||
|
||||
## Короткий ответ
|
||||
|
||||
По текущим данным репозитория у SHiNE описаны **два production-контура**:
|
||||
|
||||
- `player@shineup.me`
|
||||
- домен `shineup.me`
|
||||
- IP `178.208.64.62`
|
||||
|
||||
и
|
||||
|
||||
- `player@193.8.215.70`
|
||||
- домен `server2.shineup.me`
|
||||
- IP `193.8.215.70`
|
||||
|
||||
## 1. Основной production-хост
|
||||
|
||||
- SSH: `player@shineup.me`
|
||||
- домен: `shineup.me`
|
||||
- IP: `178.208.64.62`
|
||||
- пользователь: `player`
|
||||
- базовый путь: `/home/player`
|
||||
|
||||
Основные каталоги:
|
||||
|
||||
- проект SHiNE: `/home/player/SHiNE`
|
||||
- серверный jar: `/home/player/SHiNE/shine-server/shine-server.jar`
|
||||
- UI: `/home/player/SHiNE/shine-ui`
|
||||
- данные: `/home/player/SHiNE/shine-server/data/`
|
||||
- логи: `/home/player/SHiNE/shine-server/logs/app.log`
|
||||
|
||||
Сервисы:
|
||||
|
||||
- `shine-server.service`
|
||||
- `caddy.service`
|
||||
|
||||
Caddy:
|
||||
|
||||
- активный конфиг: `/etc/caddy/Caddyfile`
|
||||
- UI root: `/home/player/SHiNE/shine-ui`
|
||||
- `/ws` проксируется на `127.0.0.1:7070`
|
||||
|
||||
Deploy:
|
||||
|
||||
- `./gradlew deployServerProduction`
|
||||
- `./gradlew deployUIProduction`
|
||||
|
||||
Правило:
|
||||
|
||||
- любые изменения на `shineup.me` делать только после отдельного подтверждения пользователя.
|
||||
|
||||
## 2. Второй production-сервер
|
||||
|
||||
- SSH: `player@193.8.215.70`
|
||||
- домен: `server2.shineup.me`
|
||||
- IP: `193.8.215.70`
|
||||
- пользователь: `player`
|
||||
- базовый путь: `/home/player`
|
||||
|
||||
Роль:
|
||||
|
||||
- второй production-контур SHiNE;
|
||||
- использовать как production-сервер, несмотря на исторические имена deploy-задач
|
||||
`deployServerTest2` / `deployUITest2`.
|
||||
|
||||
Основные каталоги:
|
||||
|
||||
- проект SHiNE: `/home/player/SHiNE`
|
||||
- серверный jar: `/home/player/SHiNE/shine-server/shine-server.jar`
|
||||
- UI: `/home/player/SHiNE/shine-ui`
|
||||
- данные: `/home/player/SHiNE/shine-server/data/`
|
||||
- логи: `/home/player/SHiNE/shine-server/logs/app.log`
|
||||
|
||||
## 3. Связанные публичные production-публикации на том же хосте
|
||||
|
||||
На этом же production-хосте есть отдельная публикация для `shine_payments`:
|
||||
|
||||
- каталог: `/home/player/sites/test-solana-tickets.shineup.me`
|
||||
- домены:
|
||||
- `https://test-solana-tickets.shineup.me`
|
||||
- `https://test-solana-tickets.shiningpeople.ru`
|
||||
|
||||
Это не второй production-хост SHiNE, а отдельный сайт на том же сервере.
|
||||
|
||||
## 4. Какие серверы не считать production
|
||||
|
||||
Не production:
|
||||
|
||||
- `t1.shineup.me`
|
||||
- `t2.shineup.me`
|
||||
- `t3.shineup.me`
|
||||
- `t4.shineup.me`
|
||||
@@ -0,0 +1,35 @@
|
||||
# Deploy
|
||||
|
||||
Подробности о том, где что задеплоено в SHiNE, нужно искать в папке `Deploy/`.
|
||||
|
||||
Эта папка служит краткой картой окружений:
|
||||
|
||||
- [TEST_SERVERS.md](/home/ai/work/SHiNE/SHiNE-server-sha256/Deploy/TEST_SERVERS.md) — тестовые стенды;
|
||||
- [PRODUCTION_SERVERS.md](/home/ai/work/SHiNE/SHiNE-server-sha256/Deploy/PRODUCTION_SERVERS.md) — production-контур и связанные публичные публикации.
|
||||
|
||||
Ниже краткая сводка.
|
||||
|
||||
## Основные публичные контуры
|
||||
|
||||
- Production SHiNE:
|
||||
- `player@shineup.me`
|
||||
- домен `shineup.me`
|
||||
- IP `178.208.64.62`
|
||||
- Второй production SHiNE:
|
||||
- `player@193.8.215.70`
|
||||
- домен `server2.shineup.me`
|
||||
- IP `193.8.215.70`
|
||||
## Отдельный quad-devnet стенд
|
||||
|
||||
На отдельном VPS `178.208.90.249` подняты 4 независимых test/devnet-инстанса:
|
||||
|
||||
- `t1.shineup.me`
|
||||
- `t2.shineup.me`
|
||||
- `t3.shineup.me`
|
||||
- `t4.shineup.me`
|
||||
|
||||
## Важно
|
||||
|
||||
- Production-контура SHiNE сейчас два: `shineup.me` и `server2.shineup.me`.
|
||||
- `t1..t4.shineup.me` — это отдельные тестовые/devnet-контуры, не production.
|
||||
- Любые изменения на `shineup.me` делать только после отдельного подтверждения пользователя.
|
||||
@@ -0,0 +1,124 @@
|
||||
# Тестовые серверы SHiNE
|
||||
|
||||
Этот файл описывает тестовые стенды, которые сейчас фигурируют в проекте.
|
||||
|
||||
## 1. Исторический `test2`, теперь второй production-сервер
|
||||
|
||||
- SSH: `player@193.8.215.70`
|
||||
- Домен: `server2.shineup.me`
|
||||
- IP: `193.8.215.70`
|
||||
- Назначение: второй production-контур SHiNE
|
||||
|
||||
Структура:
|
||||
|
||||
- каталог SHiNE: `/home/player/SHiNE`
|
||||
- сервер: `/home/player/SHiNE/shine-server/shine-server.jar`
|
||||
- UI: `/home/player/SHiNE/shine-ui`
|
||||
- данные: `/home/player/SHiNE/shine-server/data/`
|
||||
- логи: `/home/player/SHiNE/shine-server/logs/app.log`
|
||||
|
||||
Сервисы:
|
||||
|
||||
- `shine-server.service`
|
||||
- `caddy.service`
|
||||
|
||||
Deploy:
|
||||
|
||||
- `./gradlew deployServer`
|
||||
- `./gradlew deployServerTest2`
|
||||
- `./gradlew deployUI`
|
||||
- `./gradlew deployUITest2`
|
||||
|
||||
Примечания:
|
||||
|
||||
- этот хост больше не считать test-контуром;
|
||||
- исторические имена deploy-задач `deployServerTest2` / `deployUITest2` сохранены, но сам хост считать production;
|
||||
- задача `deployUITest2` по умолчанию выкладывает UI на `server2.shineup.me`, а не на `t2.shineup.me`;
|
||||
- при описании окружений перечислять его как второй production-сервер.
|
||||
|
||||
## 2. Отдельный quad-devnet стенд `t1..t4`
|
||||
|
||||
- VPS: `178.208.90.249`
|
||||
- пользователь: `player`
|
||||
- назначение: 4 независимых SHiNE-инстанса на Solana `devnet`
|
||||
|
||||
Домены и логины:
|
||||
|
||||
- `server_t1` -> `https://t1.shineup.me`
|
||||
- `server_t2` -> `https://t2.shineup.me`
|
||||
- `server_t3` -> `https://t3.shineup.me`
|
||||
- `server_t4` -> `https://t4.shineup.me`
|
||||
|
||||
Каталоги:
|
||||
|
||||
- `/home/player/t1/server`
|
||||
- `/home/player/t1/UI`
|
||||
- `/home/player/t2/server`
|
||||
- `/home/player/t2/UI`
|
||||
- `/home/player/t3/server`
|
||||
- `/home/player/t3/UI`
|
||||
- `/home/player/t4/server`
|
||||
- `/home/player/t4/UI`
|
||||
|
||||
Подробная памятка на самом VPS:
|
||||
|
||||
- `/home/player/Agents.md`
|
||||
|
||||
Порты и systemd:
|
||||
|
||||
- `t1` -> `7101` -> `shine-t1.service`
|
||||
- `t2` -> `7102` -> `shine-t2.service`
|
||||
- `t3` -> `7103` -> `shine-t3.service`
|
||||
- `t4` -> `7104` -> `shine-t4.service`
|
||||
|
||||
Что важно по конфигу каждого инстанса:
|
||||
|
||||
- отдельный `/home/player/tX/server/application.properties`
|
||||
- `server.port=710X`
|
||||
- `server.SHiNE.login=server_tX`
|
||||
- `db.path=data/shine.sqlite`
|
||||
- `solana.cluster=devnet`
|
||||
- `solana.rpcUrl=https://api.devnet.solana.com`
|
||||
- `server.ui.indexPath=/home/player/tX/UI/index.html`
|
||||
- `server.info.url=https://tX.shineup.me`
|
||||
|
||||
UI каждого инстанса:
|
||||
|
||||
- живёт в отдельной копии `shine-UI`;
|
||||
- использует свой `js/deploy-config.js`;
|
||||
- по умолчанию смотрит именно на свой `tX.shineup.me`.
|
||||
|
||||
Caddy на стенде:
|
||||
|
||||
- конфиг: `/etc/caddy/Caddyfile`
|
||||
- статика: `/home/player/tX/UI`
|
||||
- `/ws` проксируется на `127.0.0.1:710X`
|
||||
|
||||
Operational-нюанс:
|
||||
|
||||
- при одновременных рестартах возможны `HTTP 429` от `api.devnet.solana.com`;
|
||||
- поэтому сервисы `shine-t1..shine-t4` лучше перезапускать по одному, с паузой.
|
||||
|
||||
## 3. Что проверять первым делом
|
||||
|
||||
Для любого test-контура полезны такие быстрые проверки:
|
||||
|
||||
```bash
|
||||
curl -I https://server2.shineup.me
|
||||
curl -I https://t1.shineup.me
|
||||
curl -I https://t2.shineup.me
|
||||
curl -I https://t3.shineup.me
|
||||
curl -I https://t4.shineup.me
|
||||
```
|
||||
|
||||
Для quad-devnet VPS:
|
||||
|
||||
```bash
|
||||
sudo systemctl --no-pager --full status caddy shine-t1 shine-t2 shine-t3 shine-t4
|
||||
```
|
||||
|
||||
Для второго production-контура:
|
||||
|
||||
```bash
|
||||
sudo systemctl --no-pager --full status shine-server caddy
|
||||
```
|
||||
@@ -0,0 +1,28 @@
|
||||
# AGENTS for ESP32
|
||||
|
||||
## Язык UI
|
||||
|
||||
- Для ESP32-скетчей и экранного UI использовать английский язык.
|
||||
- Русский текст на экране ESP32 пока не поддерживается корректно: шрифтовой путь для кириллицы не считается рабочим.
|
||||
- Если меняется UI-скетч, все пользовательские строки на экране должны оставаться английскими, пока ограничение не снято отдельной задачей.
|
||||
|
||||
## Синхронизация со спецификацией
|
||||
|
||||
- При изменении экранов, кнопок, переходов, статусов или текстов обязательно обновлять соответствующую спецификацию в `ESP32/esp32/ESP32-S3-Touch-AMOLED-2.16/reference/`.
|
||||
|
||||
## Сборка ESP32
|
||||
|
||||
- Основной способ проверки и прошивки скетчей для `ESP32-S3-Touch-AMOLED-2.16` - `main-device/burn.sh`.
|
||||
- Не собирать эти скетчи напрямую через `arduino-cli compile` без `burn.sh`, потому что скрипт добавляет нужные локальные библиотеки и конфиги из `official-demo/examples/Arduino-v3.3.5/libraries`.
|
||||
- Если сборка падает по `lv_conf.h` или `TouchDrvCSTXXX.hpp`, сначала проверять именно `burn.sh` и его `--library` пути, а не считать, что файл пропал из репозитория.
|
||||
|
||||
## Диагностика ESP32
|
||||
|
||||
- Последнюю сохранённую ошибку или диагностическую запись читать с устройства через USB serial monitor на `115200`.
|
||||
- Базовая команда:
|
||||
`arduino-cli monitor -p /dev/ttyACM0 --config baudrate=115200`
|
||||
- После подключения отправлять одну из команд:
|
||||
`last_error`, `last_diag` или `reg_diag`
|
||||
- Для очистки сохранённой диагностики использовать:
|
||||
`clear_error` или `clear_diag`
|
||||
- При падениях в ветках регистрации и обновления PDA сначала читать именно `last_error`: запись хранится в NVS и может пережить перезагрузку устройства.
|
||||
@@ -0,0 +1,119 @@
|
||||
# ESP32-S3-Touch-AMOLED-2.16 Codex Guide
|
||||
|
||||
Этот файл переносится в другие проекты как готовая инструкция для Codex по этой плате.
|
||||
|
||||
## 1) Что это за плата
|
||||
|
||||
- Модель: `Waveshare ESP32-S3-Touch-AMOLED-2.16`
|
||||
- MCU: `ESP32-S3` (flash 16MB, PSRAM 8MB)
|
||||
- Экран: AMOLED, физически 480x480, углы скруглены (часть крайних пикселей может быть невидима)
|
||||
- Touch: CST92xx
|
||||
- IMU: QMI8658
|
||||
- Аудио:
|
||||
- DAC/вывод (динамик): ES8311
|
||||
- ADC/вход (микрофоны): ES7210
|
||||
|
||||
## 2) Что уже установлено в этой среде
|
||||
|
||||
- Ubuntu
|
||||
- `arduino-cli 1.4.0`
|
||||
- `esp32:esp32` core `3.3.5`
|
||||
- `esptool` из `~/.arduino15/packages/esp32/tools/esptool_py/5.1.0/esptool`
|
||||
- USB порт платы: обычно `/dev/ttyACM0`
|
||||
|
||||
Проверка:
|
||||
|
||||
```bash
|
||||
arduino-cli version
|
||||
arduino-cli core list
|
||||
arduino-cli board list
|
||||
ls -l /dev/ttyACM0
|
||||
```
|
||||
|
||||
## 3) Структура подпроекта (эталон)
|
||||
|
||||
- `official-demo/` — официальный repo Waveshare (примеры+библиотеки)
|
||||
- `original-firmware/` — backup/restore заводской прошивки
|
||||
- `main-device/` — прошивки и `burn.sh`
|
||||
- `reference/` — заметки и ссылки
|
||||
|
||||
## 4) Бэкап перед любыми экспериментами
|
||||
|
||||
```bash
|
||||
cd ESP32-S3-Touch-AMOLED-2.16/original-firmware
|
||||
./backup_factory.sh
|
||||
```
|
||||
|
||||
Ожидаемый результат:
|
||||
- `factory-full-16mb.bin`
|
||||
- `factory-full-16mb.bin.sha256`
|
||||
|
||||
Восстановление:
|
||||
|
||||
```bash
|
||||
./restore_factory_backup.sh
|
||||
```
|
||||
|
||||
## 5) Деплой (прошивка) — стандарт
|
||||
|
||||
Главный скрипт:
|
||||
|
||||
```bash
|
||||
cd ESP32-S3-Touch-AMOLED-2.16/main-device
|
||||
./burn.sh <mode>
|
||||
```
|
||||
|
||||
Режимы:
|
||||
- `hello` — базовый экран
|
||||
- `widgets` — экран+touch+IMU (официальный пример)
|
||||
- `audio` — тест аудио тракта
|
||||
- `simple` — кастомный интеграционный тест (экран, touch, запись/воспроизведение, VU, tilt)
|
||||
|
||||
## 6) Как писать код под эту плату (важно)
|
||||
|
||||
1. **Экран**
|
||||
- Рабочее разрешение использовать `480x480`.
|
||||
- Не рисовать критичный текст/кнопки впритык к краю; держать safe margin (`~20px+`) из-за скругленных углов.
|
||||
- Не делать полный `fillScreen` в каждом loop: только частичные обновления (`fillRect`/локальные перерисовки), иначе мерцание.
|
||||
|
||||
2. **Touch**
|
||||
- Настройка CST:
|
||||
- `setMaxCoordinates(480, 480)`
|
||||
- `setSwapXY(true)`
|
||||
- `setMirrorXY(true, false)`
|
||||
- Обрабатывать touch по IRQ + `getPoint`.
|
||||
- После смещения UI обязательно пересчитывать hitbox кнопок.
|
||||
|
||||
3. **Аудио**
|
||||
- Для динамика инициализировать `ES8311`.
|
||||
- Для микрофона обязательно инициализировать `ES7210`; без этого запись может быть пустой.
|
||||
- Для отладки записи показывать VU/peak на экране во время `RECORD`.
|
||||
- Для быстрой проверки тракта всегда держать кнопку `BEEP` (тон), чтобы отделить проблему динамика от проблемы микрофона.
|
||||
|
||||
4. **IMU**
|
||||
- QMI8658 обновлять с ограниченной частотой (например 80–150 мс для UI-строки), чтобы не шуметь перерисовками.
|
||||
|
||||
5. **Стабильность UI**
|
||||
- Статика: рисуется один раз в setup.
|
||||
- Динамика: отдельная зона, перерисовывать только по изменению данных.
|
||||
|
||||
## 7) Рекомендуемый workflow для Codex
|
||||
|
||||
1. Проверить порт и инструменты.
|
||||
2. Если новая плата/первый запуск — сделать backup flash.
|
||||
3. Собрать и залить `simple`.
|
||||
4. Пройти ручной чек:
|
||||
- экран отображает текст без обрезки,
|
||||
- touch срабатывает по кнопкам,
|
||||
- `BEEP` слышно,
|
||||
- VU двигается во время записи,
|
||||
- `PLAY` воспроизводит записанное,
|
||||
- `Tilt` меняется при повороте.
|
||||
5. Только после этого усложнять приложение.
|
||||
|
||||
## 8) Ссылки
|
||||
|
||||
- Product page: https://www.waveshare.com/product/arduino/boards-kits/esp32-s3/esp32-s3-touch-amoled-2.16.htm
|
||||
- Docs: https://docs.waveshare.com/ESP32-S3-Touch-AMOLED-2.16
|
||||
- Arduino setup: https://docs.waveshare.com/ESP32-S3-Touch-AMOLED-2.16/Development-Environment-Setup-Arduino
|
||||
- Official examples: https://github.com/waveshareteam/ESP32-S3-Touch-AMOLED-2.16
|
||||
@@ -0,0 +1,119 @@
|
||||
# ESP32-S3-Touch-AMOLED-2.16 Codex Guide
|
||||
|
||||
Этот файл переносится в другие проекты как готовая инструкция для Codex по этой плате.
|
||||
|
||||
## 1) Что это за плата
|
||||
|
||||
- Модель: `Waveshare ESP32-S3-Touch-AMOLED-2.16`
|
||||
- MCU: `ESP32-S3` (flash 16MB, PSRAM 8MB)
|
||||
- Экран: AMOLED, физически 480x480, углы скруглены (часть крайних пикселей может быть невидима)
|
||||
- Touch: CST92xx
|
||||
- IMU: QMI8658
|
||||
- Аудио:
|
||||
- DAC/вывод (динамик): ES8311
|
||||
- ADC/вход (микрофоны): ES7210
|
||||
|
||||
## 2) Что уже установлено в этой среде
|
||||
|
||||
- Ubuntu
|
||||
- `arduino-cli 1.4.0`
|
||||
- `esp32:esp32` core `3.3.5`
|
||||
- `esptool` из `~/.arduino15/packages/esp32/tools/esptool_py/5.1.0/esptool`
|
||||
- USB порт платы: обычно `/dev/ttyACM0`
|
||||
|
||||
Проверка:
|
||||
|
||||
```bash
|
||||
arduino-cli version
|
||||
arduino-cli core list
|
||||
arduino-cli board list
|
||||
ls -l /dev/ttyACM0
|
||||
```
|
||||
|
||||
## 3) Структура подпроекта (эталон)
|
||||
|
||||
- `official-demo/` — официальный repo Waveshare (примеры+библиотеки)
|
||||
- `original-firmware/` — backup/restore заводской прошивки
|
||||
- `main-device/` — прошивки и `burn.sh`
|
||||
- `reference/` — заметки и ссылки
|
||||
|
||||
## 4) Бэкап перед любыми экспериментами
|
||||
|
||||
```bash
|
||||
cd ESP32-S3-Touch-AMOLED-2.16/original-firmware
|
||||
./backup_factory.sh
|
||||
```
|
||||
|
||||
Ожидаемый результат:
|
||||
- `factory-full-16mb.bin`
|
||||
- `factory-full-16mb.bin.sha256`
|
||||
|
||||
Восстановление:
|
||||
|
||||
```bash
|
||||
./restore_factory_backup.sh
|
||||
```
|
||||
|
||||
## 5) Деплой (прошивка) — стандарт
|
||||
|
||||
Главный скрипт:
|
||||
|
||||
```bash
|
||||
cd ESP32-S3-Touch-AMOLED-2.16/main-device
|
||||
./burn.sh <mode>
|
||||
```
|
||||
|
||||
Режимы:
|
||||
- `hello` — базовый экран
|
||||
- `widgets` — экран+touch+IMU (официальный пример)
|
||||
- `audio` — тест аудио тракта
|
||||
- `simple` — кастомный интеграционный тест (экран, touch, запись/воспроизведение, VU, tilt)
|
||||
|
||||
## 6) Как писать код под эту плату (важно)
|
||||
|
||||
1. **Экран**
|
||||
- Рабочее разрешение использовать `480x480`.
|
||||
- Не рисовать критичный текст/кнопки впритык к краю; держать safe margin (`~20px+`) из-за скругленных углов.
|
||||
- Не делать полный `fillScreen` в каждом loop: только частичные обновления (`fillRect`/локальные перерисовки), иначе мерцание.
|
||||
|
||||
2. **Touch**
|
||||
- Настройка CST:
|
||||
- `setMaxCoordinates(480, 480)`
|
||||
- `setSwapXY(true)`
|
||||
- `setMirrorXY(true, false)`
|
||||
- Обрабатывать touch по IRQ + `getPoint`.
|
||||
- После смещения UI обязательно пересчитывать hitbox кнопок.
|
||||
|
||||
3. **Аудио**
|
||||
- Для динамика инициализировать `ES8311`.
|
||||
- Для микрофона обязательно инициализировать `ES7210`; без этого запись может быть пустой.
|
||||
- Для отладки записи показывать VU/peak на экране во время `RECORD`.
|
||||
- Для быстрой проверки тракта всегда держать кнопку `BEEP` (тон), чтобы отделить проблему динамика от проблемы микрофона.
|
||||
|
||||
4. **IMU**
|
||||
- QMI8658 обновлять с ограниченной частотой (например 80–150 мс для UI-строки), чтобы не шуметь перерисовками.
|
||||
|
||||
5. **Стабильность UI**
|
||||
- Статика: рисуется один раз в setup.
|
||||
- Динамика: отдельная зона, перерисовывать только по изменению данных.
|
||||
|
||||
## 7) Рекомендуемый workflow для Codex
|
||||
|
||||
1. Проверить порт и инструменты.
|
||||
2. Если новая плата/первый запуск — сделать backup flash.
|
||||
3. Собрать и залить `simple`.
|
||||
4. Пройти ручной чек:
|
||||
- экран отображает текст без обрезки,
|
||||
- touch срабатывает по кнопкам,
|
||||
- `BEEP` слышно,
|
||||
- VU двигается во время записи,
|
||||
- `PLAY` воспроизводит записанное,
|
||||
- `Tilt` меняется при повороте.
|
||||
5. Только после этого усложнять приложение.
|
||||
|
||||
## 8) Ссылки
|
||||
|
||||
- Product page: https://www.waveshare.com/product/arduino/boards-kits/esp32-s3/esp32-s3-touch-amoled-2.16.htm
|
||||
- Docs: https://docs.waveshare.com/ESP32-S3-Touch-AMOLED-2.16
|
||||
- Arduino setup: https://docs.waveshare.com/ESP32-S3-Touch-AMOLED-2.16/Development-Environment-Setup-Arduino
|
||||
- Official examples: https://github.com/waveshareteam/ESP32-S3-Touch-AMOLED-2.16
|
||||
@@ -0,0 +1,28 @@
|
||||
# ESP32-S3-Touch-AMOLED-2.16
|
||||
|
||||
Подпроект для Waveshare `ESP32-S3-Touch-AMOLED-2.16`.
|
||||
|
||||
Структура:
|
||||
|
||||
- `official-demo/` — официальный репозиторий примеров Waveshare
|
||||
- `original-firmware/` — резервная копия заводской прошивки
|
||||
- `main-device/` — скрипты быстрой проверки устройства и основной скетч `shine_homeserver_main/`
|
||||
- `reference/` — локальные заметки по документации и железу
|
||||
- `main-device/shine_homeserver_main/` — основной рабочий скетч ESP32-проекта `SHiNE`
|
||||
|
||||
Примечание по git:
|
||||
|
||||
- `official-demo/` держать как локальный внешний checkout из `https://github.com/waveshareteam/ESP32-S3-Touch-AMOLED-2.16`, в основной git его не добавлять.
|
||||
- `original-firmware/*.bin` — локальный дамп конкретной платы, в git не добавлять.
|
||||
- `.arduino-build/` и готовые `.bin/.elf/.map` — сборочные артефакты, в git не добавлять.
|
||||
|
||||
Быстрый старт:
|
||||
|
||||
1. Сделать backup текущей прошивки:
|
||||
- `cd original-firmware && ./backup_factory.sh`
|
||||
2. Залить тест экрана/тача:
|
||||
- `cd ../main-device && ./burn.sh widgets`
|
||||
3. Залить тест динамика:
|
||||
- `cd ../main-device && ./burn.sh audio`
|
||||
4. Залить основной UI:
|
||||
- `cd ../main-device && ./burn.sh shine-homeserver-main`
|
||||
@@ -0,0 +1,52 @@
|
||||
# Main Device
|
||||
|
||||
Основной скетч homeserver и старые тестовые скетчи для быстрой проверки платы.
|
||||
`burn.sh` теперь:
|
||||
- сам пытается найти USB-порт ESP32;
|
||||
- сначала делает быструю инкрементальную сборку;
|
||||
- если быстрая сборка не удалась, автоматически повторяет полную `clean`-сборку.
|
||||
|
||||
Для режимов `widgets`, `audio` и `hello` рядом должен лежать локальный checkout `official-demo/` из официального репозитория Waveshare. В основной git он не добавляется, потому что это большой внешний набор примеров, библиотек, прошивок и артефактов.
|
||||
|
||||
Режимы:
|
||||
- `widgets` — экран + touch + IMU (пример `05_LVGL_Widgets`)
|
||||
- `audio` — динамик/аудио-кодек (пример `07_ES8311`)
|
||||
- `hello` — базовый тест экрана (пример `01_HelloWorld`)
|
||||
- `simple` — простой кастомный тест: экран + touch + запись/проигрывание + наклон (IMU)
|
||||
- `argon2` — генерация masterSecret через Argon2id с SD-картой как памятью (тест скорости)
|
||||
- `homeserver-ui` — совместимый алиас, указывает на `shine_homeserver_main/`
|
||||
- `shine-homeserver-main` — основной скетч проекта `SHiNE` для ESP32, текущая рабочая версия UI
|
||||
- `shine-homeserver-ui-main` — старое имя основного скетча, оставлено как совместимый алиас
|
||||
- `legacy-homeserver-ui` — старый UI-прототип `shine_homeserver_ui/`, оставлен как тестовый и не является основным
|
||||
- `text-test` — диагностический экран рендера текста: default font, U8g2 ASCII, U8g2 кириллица, кнопки с подписями
|
||||
- `gfx-text-test` — тот же тест рендера текста, но уже внутри новой папки `test_sketches/`
|
||||
- `gfx-layout-test` — тест геометрии и нижних рядов кнопок
|
||||
- `lvgl-basic-test` — минимальный экран на `LVGL` с текстом и кнопками
|
||||
- `lvgl-interaction-test` — экран на `LVGL` с большим числом кнопок и сообщением о нажатой кнопке
|
||||
- `lvgl-touch-debug-test` — точечная диагностика touch: сырые координаты, маркер точки и большая тест-кнопка `LVGL`
|
||||
- `lvgl-official-based-test` — наш минимальный экран, но на максимально близкой к официальному `LVGL_Widgets` инициализации
|
||||
- `lvgl-subserver-touch-test` — старый гибридный тест: `LVGL`-интерфейс, но display/touch init и raw touch-read взяты из старого `shine_homeserver_ui`; подтверждено на устройстве, touch работает, зелёных линий по краям нет
|
||||
- `lvgl-russian-font-test` — тест кастомного `LVGL`-шрифта с кириллицей: русские кнопки, длинные подписи и статусы
|
||||
- `lvgl-nav-minimal-test` — старое имя основного скетча, теперь ведёт на `shine_homeserver_main/` для совместимости
|
||||
|
||||
Запуск:
|
||||
|
||||
- `./burn.sh widgets`
|
||||
- `./burn.sh audio`
|
||||
- `./burn.sh hello`
|
||||
- `./burn.sh simple`
|
||||
- `./burn.sh homeserver-ui`
|
||||
- `./burn.sh shine-homeserver-main`
|
||||
- `./burn.sh shine-homeserver-ui-main`
|
||||
- `./burn.sh legacy-homeserver-ui`
|
||||
- `./burn.sh text-test`
|
||||
- `./burn.sh gfx-text-test`
|
||||
- `./burn.sh gfx-layout-test`
|
||||
- `./burn.sh lvgl-basic-test`
|
||||
- `./burn.sh lvgl-interaction-test`
|
||||
- `./burn.sh lvgl-touch-debug-test`
|
||||
- `./burn.sh lvgl-official-based-test`
|
||||
- `./burn.sh lvgl-subserver-touch-test`
|
||||
- `./burn.sh lvgl-russian-font-test`
|
||||
- `./burn.sh lvgl-nav-minimal-test`
|
||||
- `./flash_shine_homeserver_main.sh` - автоматически находит USB-порт и заливает `shine_homeserver_main`
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
// BLAKE2b state
|
||||
struct B2State {
|
||||
uint64_t h[8], t[2], f[2];
|
||||
uint8_t buf[128];
|
||||
size_t buflen, outlen;
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
BOARD_DIR="$(cd "${ROOT_DIR}/.." && pwd)"
|
||||
DEMO_BASE="${BOARD_DIR}/official-demo/examples/Arduino-v3.3.5"
|
||||
MODE="${1:-widgets}"
|
||||
PORT="${PORT:-}"
|
||||
FQBN="${FQBN:-esp32:esp32:esp32s3:USBMode=hwcdc,CDCOnBoot=cdc,UploadSpeed=921600,CPUFreq=240,FlashMode=dio,FlashSize=16M,PartitionScheme=app3M_fat9M_16MB,PSRAM=opi}"
|
||||
BUILD_DIR="${BUILD_DIR:-${ROOT_DIR}/.arduino-build/build-${MODE}}"
|
||||
OUT_DIR="${OUT_DIR:-${ROOT_DIR}/.arduino-build/out-${MODE}}"
|
||||
|
||||
detect_port() {
|
||||
local detected
|
||||
detected="$(arduino-cli board list 2>/dev/null | awk '/\/dev\/tty(ACM|USB)/ {print $1; exit}')"
|
||||
if [[ -n "${detected}" ]]; then
|
||||
echo "${detected}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
for candidate in /dev/ttyACM* /dev/ttyUSB*; do
|
||||
if [[ -e "${candidate}" ]]; then
|
||||
echo "${candidate}"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
case "${MODE}" in
|
||||
hello) SKETCH_DIR="${DEMO_BASE}/examples/01_HelloWorld" ;;
|
||||
widgets) SKETCH_DIR="${DEMO_BASE}/examples/05_LVGL_Widgets" ;;
|
||||
audio) SKETCH_DIR="${DEMO_BASE}/examples/07_ES8311" ;;
|
||||
simple) SKETCH_DIR="${ROOT_DIR}/simple_av_test" ;;
|
||||
argon2) SKETCH_DIR="${ROOT_DIR}/argon2_sd_test" ;;
|
||||
homeserver-ui) SKETCH_DIR="${ROOT_DIR}/shine_homeserver_main" ;;
|
||||
shine-homeserver-main) SKETCH_DIR="${ROOT_DIR}/shine_homeserver_main" ;;
|
||||
shine-homeserver-ui-main) SKETCH_DIR="${ROOT_DIR}/shine_homeserver_main" ;;
|
||||
legacy-homeserver-ui) SKETCH_DIR="${ROOT_DIR}/shine_homeserver_ui" ;;
|
||||
text-test) SKETCH_DIR="${ROOT_DIR}/text_render_test" ;;
|
||||
gfx-text-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/gfx_text_render_test" ;;
|
||||
gfx-layout-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/gfx_button_layout_test" ;;
|
||||
lvgl-basic-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/lvgl_basic_test" ;;
|
||||
lvgl-interaction-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/lvgl_interaction_test" ;;
|
||||
lvgl-touch-debug-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/lvgl_touch_debug_test" ;;
|
||||
lvgl-official-based-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/lvgl_official_based_test" ;;
|
||||
lvgl-subserver-touch-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/lvgl_subserver_touch_test" ;;
|
||||
lvgl-russian-font-test) SKETCH_DIR="${ROOT_DIR}/test_sketches/lvgl_russian_font_test" ;;
|
||||
lvgl-nav-minimal-test) SKETCH_DIR="${ROOT_DIR}/shine_homeserver_main" ;;
|
||||
*)
|
||||
echo "Unknown mode: ${MODE}" >&2
|
||||
echo "Use one of: hello, widgets, audio, simple, argon2, homeserver-ui, shine-homeserver-main, shine-homeserver-ui-main, legacy-homeserver-ui, text-test, gfx-text-test, gfx-layout-test, lvgl-basic-test, lvgl-interaction-test, lvgl-touch-debug-test, lvgl-official-based-test, lvgl-subserver-touch-test, lvgl-russian-font-test" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ -z "${PORT}" ]]; then
|
||||
if ! PORT="$(detect_port)"; then
|
||||
echo "Failed to auto-detect ESP32 port. Set PORT=/dev/ttyACM0 ./burn.sh ${MODE}" >&2
|
||||
exit 3
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "== Mode: ${MODE}"
|
||||
echo "== Sketch: ${SKETCH_DIR}"
|
||||
echo "== Port: ${PORT}"
|
||||
echo "== FQBN: ${FQBN}"
|
||||
|
||||
mkdir -p "${BUILD_DIR}" "${OUT_DIR}"
|
||||
|
||||
compile_args=(
|
||||
--fqbn "${FQBN}"
|
||||
--build-path "${BUILD_DIR}"
|
||||
--output-dir "${OUT_DIR}"
|
||||
--library "${DEMO_BASE}/libraries/GFX_Library_for_Arduino"
|
||||
--library "${DEMO_BASE}/libraries/SensorLib"
|
||||
--library "${DEMO_BASE}/libraries/XPowersLib"
|
||||
--library "${DEMO_BASE}/libraries/lvgl"
|
||||
--library "${DEMO_BASE}/libraries/Mylibrary"
|
||||
"${SKETCH_DIR}"
|
||||
)
|
||||
|
||||
echo "== Compile: fast incremental build"
|
||||
if ! arduino-cli compile "${compile_args[@]}"; then
|
||||
echo "== Compile: fast build failed, retrying clean build"
|
||||
arduino-cli compile --clean "${compile_args[@]}"
|
||||
fi
|
||||
|
||||
arduino-cli upload \
|
||||
-p "${PORT}" \
|
||||
--fqbn "${FQBN}" \
|
||||
--input-dir "${OUT_DIR}" \
|
||||
"${SKETCH_DIR}"
|
||||
|
||||
echo
|
||||
echo "== Done."
|
||||
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
detect_port_from_arduino_cli() {
|
||||
local line
|
||||
while IFS= read -r line; do
|
||||
[[ -z "${line}" ]] && continue
|
||||
[[ "${line}" == Port* ]] && continue
|
||||
if [[ "${line}" == /dev/* ]]; then
|
||||
awk '{print $1}' <<<"${line}"
|
||||
return 0
|
||||
fi
|
||||
done < <(arduino-cli board list 2>/dev/null || true)
|
||||
return 1
|
||||
}
|
||||
|
||||
detect_port_from_dev() {
|
||||
local candidates=()
|
||||
local path
|
||||
for path in /dev/ttyACM* /dev/ttyUSB*; do
|
||||
[[ -e "${path}" ]] || continue
|
||||
candidates+=("${path}")
|
||||
done
|
||||
|
||||
if [[ "${#candidates[@]}" -eq 1 ]]; then
|
||||
printf '%s\n' "${candidates[0]}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
PORT="${PORT:-}"
|
||||
if [[ -z "${PORT}" ]]; then
|
||||
PORT="$(detect_port_from_arduino_cli || true)"
|
||||
fi
|
||||
if [[ -z "${PORT}" ]]; then
|
||||
PORT="$(detect_port_from_dev || true)"
|
||||
fi
|
||||
|
||||
if [[ -z "${PORT}" ]]; then
|
||||
echo "Не удалось автоматически найти USB-порт ESP32." >&2
|
||||
echo "Подключите плату и проверьте 'arduino-cli board list'." >&2
|
||||
echo "Либо укажите порт вручную: PORT=/dev/ttyACM0 ./flash_shine_homeserver_main.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "== Найден порт: ${PORT}"
|
||||
PORT="${PORT}" "${ROOT_DIR}/burn.sh" shine-homeserver-main
|
||||
@@ -0,0 +1,14 @@
|
||||
# SHiNE Homeserver UI Main
|
||||
|
||||
Это основной рабочий скетч ESP32-проекта `SHiNE`.
|
||||
|
||||
Текущая каноническая точка запуска:
|
||||
|
||||
- `./burn.sh shine-homeserver-main`
|
||||
- `./burn.sh homeserver-ui`
|
||||
|
||||
Историческое имя этого скетча:
|
||||
|
||||
- `lvgl-nav-minimal-test`
|
||||
|
||||
Прежние тестовые варианты для этой платы остаются в `main-device/test_sketches/` и должны восприниматься как старые диагностические сборки, а не как основной UI.
|
||||
@@ -0,0 +1,591 @@
|
||||
#include "shine_secret_generation.h"
|
||||
|
||||
#include <SD_MMC.h>
|
||||
#include <mbedtls/sha256.h>
|
||||
#include <mbedtls/base64.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <driver/gpio.h>
|
||||
|
||||
#define PIN_SD_CLK 2
|
||||
#define PIN_SD_CMD 1
|
||||
#define PIN_SD_D0 3
|
||||
#define SD_MEM_FILE "/argon2.bin"
|
||||
|
||||
#define A2_T 2u
|
||||
#define A2_M 65536u
|
||||
#define A2_P 1u
|
||||
#define A2_DKLEN 32u
|
||||
#define A2_SYNC 4u
|
||||
#define A2_SEG (A2_M / A2_SYNC)
|
||||
#define A2_VERSION 0x13u
|
||||
#define A2_TYPE 2u
|
||||
#define A2_BLKSZ 1024u
|
||||
#define TOTAL_FILLS (A2_T * A2_M - 2u)
|
||||
|
||||
struct B2State {
|
||||
uint64_t h[8], t[2], f[2];
|
||||
uint8_t buf[128];
|
||||
size_t buflen, outlen;
|
||||
};
|
||||
|
||||
static bool gSdReady = false;
|
||||
static bool gRunning = false;
|
||||
static bool gDone = false;
|
||||
static bool gError = false;
|
||||
static char gMessage[96] = {};
|
||||
static uint8_t gSecret[32] = {};
|
||||
static char gSecretB58[64] = {};
|
||||
static uint32_t gDoneBlocks = 0;
|
||||
static uint32_t gStartMs = 0;
|
||||
static uint32_t gCurPass = 0;
|
||||
static uint32_t gCurBlock = 2;
|
||||
static bool gInitDone = false;
|
||||
static File gSdFile;
|
||||
static uint8_t *gBufPrev = nullptr;
|
||||
static uint8_t *gBufRef = nullptr;
|
||||
static uint8_t *gBufOut = nullptr;
|
||||
static uint8_t *gBufZero = nullptr;
|
||||
static uint8_t *gBufAddr = nullptr;
|
||||
static uint32_t *gJ1Seg = nullptr;
|
||||
static uint8_t gH0[64] = {};
|
||||
static B2State gB2S;
|
||||
|
||||
#define ROTR64(x,n) (((x)>>(n))|((x)<<(64-(n))))
|
||||
static const uint64_t B2IV[8] = {
|
||||
0x6A09E667F3BCC908ULL,0xBB67AE8584CAA73BULL,
|
||||
0x3C6EF372FE94F82BULL,0xA54FF53A5F1D36F1ULL,
|
||||
0x510E527FADE682D1ULL,0x9B05688C2B3E6C1FULL,
|
||||
0x1F83D9ABFB41BD6BULL,0x5BE0CD19137E2179ULL
|
||||
};
|
||||
static const uint8_t B2SIGMA[12][16] = {
|
||||
{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15},
|
||||
{14,10,4,8,9,15,13,6,1,12,0,2,11,7,5,3},
|
||||
{11,8,12,0,5,2,15,13,10,14,3,6,7,1,9,4},
|
||||
{7,9,3,1,13,12,11,14,2,6,5,10,4,0,15,8},
|
||||
{9,0,5,7,2,4,10,15,14,1,11,12,6,8,3,13},
|
||||
{2,12,6,10,0,11,8,3,4,13,7,5,15,14,1,9},
|
||||
{12,5,1,15,14,13,4,10,0,7,6,3,9,2,8,11},
|
||||
{13,11,7,14,12,1,3,9,5,0,15,4,8,6,2,10},
|
||||
{6,15,14,9,11,3,0,8,12,2,13,7,1,4,10,5},
|
||||
{10,2,8,4,7,6,1,5,15,11,9,14,3,12,13,0},
|
||||
{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15},
|
||||
{14,10,4,8,9,15,13,6,1,12,0,2,11,7,5,3}
|
||||
};
|
||||
|
||||
static const char B58_ALPHA[] = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
||||
|
||||
static void setMessage(const char *message) {
|
||||
snprintf(gMessage, sizeof(gMessage), "%s", message ? message : "");
|
||||
}
|
||||
|
||||
static void sha256calc(const uint8_t *in, size_t len, uint8_t *out32);
|
||||
|
||||
static void finishSecretFromBytes(const uint8_t secret32[32], const char *message) {
|
||||
memcpy(gSecret, secret32, 32);
|
||||
shineSecretBase58Encode(gSecret, 32, gSecretB58, sizeof(gSecretB58));
|
||||
gDone = true;
|
||||
gRunning = false;
|
||||
gError = false;
|
||||
gInitDone = false;
|
||||
gDoneBlocks = TOTAL_FILLS;
|
||||
setMessage(message ? message : "Secret generated");
|
||||
}
|
||||
|
||||
static void b2_compress(B2State *S, const uint8_t *blk) {
|
||||
uint64_t m[16], v[16];
|
||||
for (int i = 0; i < 16; i++) m[i] = ((const uint64_t *)blk)[i];
|
||||
for (int i = 0; i < 8; i++) v[i] = S->h[i];
|
||||
v[8]=B2IV[0];v[9]=B2IV[1];v[10]=B2IV[2];v[11]=B2IV[3];
|
||||
v[12]=B2IV[4]^S->t[0];v[13]=B2IV[5]^S->t[1];
|
||||
v[14]=B2IV[6]^S->f[0];v[15]=B2IV[7]^S->f[1];
|
||||
#define BG(r,i,a,b,c,d) \
|
||||
v[a]+=v[b]+m[B2SIGMA[r][2*(i)]]; v[d]=ROTR64(v[d]^v[a],32); \
|
||||
v[c]+=v[d]; v[b]=ROTR64(v[b]^v[c],24); \
|
||||
v[a]+=v[b]+m[B2SIGMA[r][2*(i)+1]]; v[d]=ROTR64(v[d]^v[a],16); \
|
||||
v[c]+=v[d]; v[b]=ROTR64(v[b]^v[c],63)
|
||||
for (int r = 0; r < 12; r++) {
|
||||
BG(r,0,0,4,8,12);BG(r,1,1,5,9,13);BG(r,2,2,6,10,14);BG(r,3,3,7,11,15);
|
||||
BG(r,4,0,5,10,15);BG(r,5,1,6,11,12);BG(r,6,2,7,8,13);BG(r,7,3,4,9,14);
|
||||
}
|
||||
#undef BG
|
||||
for (int i = 0; i < 8; i++) S->h[i] ^= v[i] ^ v[i + 8];
|
||||
}
|
||||
|
||||
static void b2_init(B2State *S, size_t outlen) {
|
||||
memset(S, 0, sizeof(*S));
|
||||
memcpy(S->h, B2IV, 64);
|
||||
S->h[0] ^= 0x01010000ULL | outlen;
|
||||
S->outlen = outlen;
|
||||
}
|
||||
|
||||
static void b2_update(B2State *S, const uint8_t *in, size_t inlen) {
|
||||
while (inlen > 0) {
|
||||
size_t fill = 128 - S->buflen;
|
||||
size_t use = inlen < fill ? inlen : fill;
|
||||
memcpy(S->buf + S->buflen, in, use);
|
||||
S->buflen += use;
|
||||
in += use;
|
||||
inlen -= use;
|
||||
if (S->buflen == 128 && inlen > 0) {
|
||||
S->t[0] += 128;
|
||||
if (S->t[0] < 128) S->t[1]++;
|
||||
b2_compress(S, S->buf);
|
||||
S->buflen = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void b2_final(B2State *S, uint8_t *digest) {
|
||||
S->f[0] = ~0ULL;
|
||||
S->t[0] += S->buflen;
|
||||
if (S->t[0] < S->buflen) S->t[1]++;
|
||||
memset(S->buf + S->buflen, 0, 128 - S->buflen);
|
||||
b2_compress(S, S->buf);
|
||||
uint8_t tmp[64];
|
||||
for (int i = 0; i < 8; i++) {
|
||||
uint64_t w = S->h[i];
|
||||
for (int j = 0; j < 8; j++) tmp[i * 8 + j] = (uint8_t)(w >> (j * 8));
|
||||
}
|
||||
memcpy(digest, tmp, S->outlen);
|
||||
}
|
||||
|
||||
static void b2hash(const uint8_t *in, size_t inlen, uint8_t *digest, size_t outlen) {
|
||||
b2_init(&gB2S, outlen);
|
||||
b2_update(&gB2S, in, inlen);
|
||||
b2_final(&gB2S, digest);
|
||||
}
|
||||
|
||||
static void b2long(const uint8_t *in, size_t inlen, uint8_t *digest, uint32_t outlen) {
|
||||
uint8_t lenle[4] = {(uint8_t)outlen, (uint8_t)(outlen >> 8), (uint8_t)(outlen >> 16), (uint8_t)(outlen >> 24)};
|
||||
if (outlen <= 64) {
|
||||
b2_init(&gB2S, outlen);
|
||||
b2_update(&gB2S, lenle, 4);
|
||||
b2_update(&gB2S, in, inlen);
|
||||
b2_final(&gB2S, digest);
|
||||
return;
|
||||
}
|
||||
uint8_t tmp[64];
|
||||
b2_init(&gB2S, 64);
|
||||
b2_update(&gB2S, lenle, 4);
|
||||
b2_update(&gB2S, in, inlen);
|
||||
b2_final(&gB2S, tmp);
|
||||
memcpy(digest, tmp, 32);
|
||||
digest += 32;
|
||||
uint32_t rem = outlen - 32;
|
||||
while (rem > 64) {
|
||||
b2hash(tmp, 64, tmp, 64);
|
||||
memcpy(digest, tmp, 32);
|
||||
digest += 32;
|
||||
rem -= 32;
|
||||
}
|
||||
b2hash(tmp, 64, tmp, rem);
|
||||
memcpy(digest, tmp, rem);
|
||||
}
|
||||
|
||||
#define A2G(a,b,c,d) do{ \
|
||||
(a)+=(b)+2*((a)&0xFFFFFFFFULL)*((b)&0xFFFFFFFFULL); \
|
||||
(d)=ROTR64((d)^(a),32); \
|
||||
(c)+=(d)+2*((c)&0xFFFFFFFFULL)*((d)&0xFFFFFFFFULL); \
|
||||
(b)=ROTR64((b)^(c),24); \
|
||||
(a)+=(b)+2*((a)&0xFFFFFFFFULL)*((b)&0xFFFFFFFFULL); \
|
||||
(d)=ROTR64((d)^(a),16); \
|
||||
(c)+=(d)+2*((c)&0xFFFFFFFFULL)*((d)&0xFFFFFFFFULL); \
|
||||
(b)=ROTR64((b)^(c),63); \
|
||||
}while(0)
|
||||
#define A2P(v) do{ \
|
||||
A2G((v)[0],(v)[4],(v)[8],(v)[12]);A2G((v)[1],(v)[5],(v)[9],(v)[13]); \
|
||||
A2G((v)[2],(v)[6],(v)[10],(v)[14]);A2G((v)[3],(v)[7],(v)[11],(v)[15]); \
|
||||
A2G((v)[0],(v)[5],(v)[10],(v)[15]);A2G((v)[1],(v)[6],(v)[11],(v)[12]); \
|
||||
A2G((v)[2],(v)[7],(v)[8],(v)[13]);A2G((v)[3],(v)[4],(v)[9],(v)[14]); \
|
||||
}while(0)
|
||||
|
||||
static void fillBlock(uint32_t prevIdx, uint32_t refIdx, uint32_t outIdx, bool xorMode) {
|
||||
gSdFile.seek((uint64_t)prevIdx * A2_BLKSZ); gSdFile.read(gBufPrev, A2_BLKSZ);
|
||||
gSdFile.seek((uint64_t)refIdx * A2_BLKSZ); gSdFile.read(gBufRef, A2_BLKSZ);
|
||||
uint64_t *R = (uint64_t *)gBufOut, *P = (uint64_t *)gBufPrev, *F = (uint64_t *)gBufRef;
|
||||
for (int i = 0; i < 128; i++) R[i] = P[i] ^ F[i];
|
||||
uint64_t *Q = (uint64_t *)gBufRef; memcpy(Q, R, A2_BLKSZ);
|
||||
for (int j = 0; j < 8; j++) A2P(&Q[16 * j]);
|
||||
uint64_t row[16];
|
||||
for (int i = 0; i < 8; i++) {
|
||||
for (int k = 0; k < 8; k++) { row[2 * k] = Q[2 * i + 16 * k]; row[2 * k + 1] = Q[2 * i + 16 * k + 1]; }
|
||||
A2P(row);
|
||||
for (int k = 0; k < 8; k++) { Q[2 * i + 16 * k] = row[2 * k]; Q[2 * i + 16 * k + 1] = row[2 * k + 1]; }
|
||||
}
|
||||
for (int i = 0; i < 128; i++) R[i] = Q[i] ^ R[i];
|
||||
if (xorMode) {
|
||||
gSdFile.seek((uint64_t)outIdx * A2_BLKSZ); gSdFile.read(gBufPrev, A2_BLKSZ);
|
||||
uint64_t *O = (uint64_t *)gBufPrev; for (int i = 0; i < 128; i++) R[i] ^= O[i];
|
||||
}
|
||||
gSdFile.seek((uint64_t)outIdx * A2_BLKSZ); gSdFile.write(gBufOut, A2_BLKSZ);
|
||||
}
|
||||
|
||||
static void generateAddresses(uint32_t pass, uint32_t slice) {
|
||||
memset(gBufZero, 0, A2_BLKSZ);
|
||||
uint8_t inputBlk[A2_BLKSZ]; memset(inputBlk, 0, A2_BLKSZ);
|
||||
uint64_t *iv = (uint64_t *)inputBlk;
|
||||
iv[0]=pass;iv[1]=0;iv[2]=slice;iv[3]=A2_M;iv[4]=A2_T;iv[5]=A2_TYPE;
|
||||
uint32_t count = 0;
|
||||
for (uint32_t b = 0; b < A2_SEG; b += 128) {
|
||||
iv[6] = ++count;
|
||||
{
|
||||
uint64_t *R=(uint64_t*)gBufAddr,*Z=(uint64_t*)gBufZero,*I=iv;
|
||||
for(int i=0;i<128;i++)R[i]=Z[i]^I[i];
|
||||
uint64_t *Q=R,row[16];
|
||||
for(int j=0;j<8;j++)A2P(&Q[16*j]);
|
||||
for(int i=0;i<8;i++){
|
||||
for(int k=0;k<8;k++){row[2*k]=Q[2*i+16*k];row[2*k+1]=Q[2*i+16*k+1];}
|
||||
A2P(row);
|
||||
for(int k=0;k<8;k++){Q[2*i+16*k]=row[2*k];Q[2*i+16*k+1]=row[2*k+1];}
|
||||
}
|
||||
for(int i=0;i<128;i++)R[i]=Q[i]^(Z[i]^I[i]);
|
||||
}
|
||||
{
|
||||
uint64_t *R=(uint64_t*)gBufPrev,*Z=(uint64_t*)gBufZero,*A=(uint64_t*)gBufAddr;
|
||||
for(int i=0;i<128;i++)R[i]=Z[i]^A[i];
|
||||
uint64_t *Q=R,row[16];
|
||||
for(int j=0;j<8;j++)A2P(&Q[16*j]);
|
||||
for(int i=0;i<8;i++){
|
||||
for(int k=0;k<8;k++){row[2*k]=Q[2*i+16*k];row[2*k+1]=Q[2*i+16*k+1];}
|
||||
A2P(row);
|
||||
for(int k=0;k<8;k++){Q[2*i+16*k]=row[2*k];Q[2*i+16*k+1]=row[2*k+1];}
|
||||
}
|
||||
for(int i=0;i<128;i++)R[i]=Q[i]^(Z[i]^A[i]);
|
||||
memcpy(gBufAddr,gBufPrev,A2_BLKSZ);
|
||||
}
|
||||
uint64_t *addr=(uint64_t*)gBufAddr;
|
||||
uint32_t cnt=(b+128<=A2_SEG)?128:(A2_SEG-b);
|
||||
for(uint32_t j=0;j<cnt;j++)gJ1Seg[b+j]=(uint32_t)(addr[j]&0xFFFFFFFFULL);
|
||||
}
|
||||
}
|
||||
|
||||
static uint32_t indexAlpha(uint32_t pass, uint32_t slice, uint32_t posInSlice, uint32_t J1) {
|
||||
uint32_t refAreaSize;
|
||||
if (pass == 0) {
|
||||
if (slice == 0) {
|
||||
uint32_t pos = posInSlice;
|
||||
refAreaSize = (pos < 2) ? 0 : pos - 1;
|
||||
} else {
|
||||
refAreaSize = slice * A2_SEG + posInSlice - 1;
|
||||
}
|
||||
} else {
|
||||
refAreaSize = A2_M - A2_SEG + posInSlice - 1;
|
||||
}
|
||||
if (refAreaSize == 0) refAreaSize = 1;
|
||||
uint64_t relPos = (uint64_t)J1 * J1 >> 32;
|
||||
relPos = refAreaSize - 1 - ((uint64_t)refAreaSize * relPos >> 32);
|
||||
uint32_t startPos = 0;
|
||||
if (pass != 0 && slice != A2_SYNC - 1) startPos = (slice + 1) * A2_SEG;
|
||||
return (startPos + relPos) % A2_M;
|
||||
}
|
||||
|
||||
static void sha256calc(const uint8_t *in, size_t len, uint8_t *out32) {
|
||||
mbedtls_sha256_context ctx;
|
||||
mbedtls_sha256_init(&ctx);
|
||||
mbedtls_sha256_starts(&ctx, 0);
|
||||
mbedtls_sha256_update(&ctx, in, len);
|
||||
mbedtls_sha256_finish(&ctx, out32);
|
||||
mbedtls_sha256_free(&ctx);
|
||||
}
|
||||
|
||||
static void trimInPlace(char *text) {
|
||||
if (!text) return;
|
||||
size_t len = strlen(text);
|
||||
size_t start = 0;
|
||||
while (start < len && (text[start] == ' ' || text[start] == '\t' || text[start] == '\n' || text[start] == '\r')) start++;
|
||||
size_t end = len;
|
||||
while (end > start && (text[end - 1] == ' ' || text[end - 1] == '\t' || text[end - 1] == '\n' || text[end - 1] == '\r')) end--;
|
||||
if (start > 0 && end > start) memmove(text, text + start, end - start);
|
||||
if (end <= start) { text[0] = '\0'; return; }
|
||||
text[end - start] = '\0';
|
||||
}
|
||||
|
||||
static void lowercaseAsciiInPlace(char *text) {
|
||||
if (!text) return;
|
||||
for (int i = 0; text[i]; i++) if (text[i] >= 'A' && text[i] <= 'Z') text[i] += 32;
|
||||
}
|
||||
|
||||
static void normalizeLoginInPlace(char *text) {
|
||||
trimInPlace(text);
|
||||
lowercaseAsciiInPlace(text);
|
||||
}
|
||||
|
||||
static void bytesToBase64Std(const uint8_t *data, size_t len, char *out, size_t outSz) {
|
||||
size_t b64len = 0;
|
||||
if (outSz == 0) return;
|
||||
if (mbedtls_base64_encode((uint8_t*)out, outSz, &b64len, data, len) != 0) {
|
||||
out[0] = '\0';
|
||||
return;
|
||||
}
|
||||
if (b64len >= outSz) b64len = outSz - 1;
|
||||
out[b64len] = '\0';
|
||||
}
|
||||
|
||||
static void deriveLegacyMasterSecret(const char *password, uint8_t *out32) {
|
||||
uint8_t baseHash[32];
|
||||
sha256calc((const uint8_t*)password, strlen(password), baseHash);
|
||||
char baseB64[64];
|
||||
bytesToBase64Std(baseHash, 32, baseB64, sizeof(baseB64));
|
||||
char material[96];
|
||||
snprintf(material, sizeof(material), "%smaster.secret", baseB64);
|
||||
sha256calc((const uint8_t*)material, strlen(material), out32);
|
||||
}
|
||||
|
||||
void shineSecretBase58Encode(const uint8_t *data, size_t len, char *out, size_t outSz) {
|
||||
int zeros = 0;
|
||||
while (zeros < (int)len && data[zeros] == 0) zeros++;
|
||||
uint8_t tmp[128] = {};
|
||||
int tmpLen = 0;
|
||||
for (int i = zeros; i < (int)len; i++) {
|
||||
int carry = data[i];
|
||||
for (int j = 0; j < tmpLen; j++) { carry += 256 * tmp[j]; tmp[j] = carry % 58; carry /= 58; }
|
||||
while (carry) { tmp[tmpLen++] = carry % 58; carry /= 58; }
|
||||
}
|
||||
int n = 0;
|
||||
for (int i = 0; i < zeros && n < (int)outSz - 1; i++) out[n++] = '1';
|
||||
for (int i = tmpLen - 1; i >= 0 && n < (int)outSz - 1; i--) out[n++] = B58_ALPHA[(int)tmp[i]];
|
||||
out[n] = '\0';
|
||||
}
|
||||
|
||||
bool shineSecretBase58Decode(const char *input, uint8_t *out, size_t *outLen, size_t maxOutLen, String &error) {
|
||||
error = "";
|
||||
if (!input || !*input) {
|
||||
error = "secret too short";
|
||||
return false;
|
||||
}
|
||||
size_t inLen = strlen(input);
|
||||
uint8_t tmp[256] = {};
|
||||
size_t tmpLen = 0;
|
||||
size_t zeros = 0;
|
||||
while (zeros < inLen && input[zeros] == '1') zeros++;
|
||||
|
||||
for (size_t i = zeros; i < inLen; ++i) {
|
||||
const char *pos = strchr(B58_ALPHA, input[i]);
|
||||
if (!pos) {
|
||||
error = "invalid base58";
|
||||
return false;
|
||||
}
|
||||
int carry = (int)(pos - B58_ALPHA);
|
||||
for (size_t j = 0; j < tmpLen; ++j) {
|
||||
carry += 58 * tmp[j];
|
||||
tmp[j] = carry & 0xFF;
|
||||
carry >>= 8;
|
||||
}
|
||||
while (carry > 0) {
|
||||
tmp[tmpLen++] = carry & 0xFF;
|
||||
carry >>= 8;
|
||||
if (tmpLen >= sizeof(tmp)) {
|
||||
error = "secret too long";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
size_t decodedLen = zeros + tmpLen;
|
||||
if (decodedLen > maxOutLen) {
|
||||
error = "secret too long";
|
||||
return false;
|
||||
}
|
||||
memset(out, 0, maxOutLen);
|
||||
size_t offset = 0;
|
||||
for (size_t i = 0; i < zeros; ++i) out[offset++] = 0;
|
||||
for (size_t i = 0; i < tmpLen; ++i) out[offset + i] = tmp[tmpLen - 1 - i];
|
||||
*outLen = decodedLen;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void argon2Init(const char *login, const char *password) {
|
||||
char saltSrc[256];
|
||||
snprintf(saltSrc, sizeof(saltSrc), "shine-auth-v2|login=%s|suffix=master.secret", login);
|
||||
uint8_t saltHash[32]; sha256calc((uint8_t*)saltSrc, strlen(saltSrc), saltHash);
|
||||
uint8_t salt[16]; memcpy(salt, saltHash, 16);
|
||||
|
||||
char passBytes[128];
|
||||
snprintf(passBytes, sizeof(passBytes), "%s\n%s", login, password);
|
||||
uint32_t passLen = strlen(passBytes), saltLen = 16;
|
||||
|
||||
b2_init(&gB2S, 64);
|
||||
uint8_t le4[4];
|
||||
#define B2LE32(val) do{uint32_t _v=(val);le4[0]=_v;le4[1]=_v>>8;le4[2]=_v>>16;le4[3]=_v>>24;b2_update(&gB2S,le4,4);}while(0)
|
||||
B2LE32(A2_P);B2LE32(A2_DKLEN);B2LE32(A2_M);B2LE32(A2_T);
|
||||
B2LE32(A2_VERSION);B2LE32(A2_TYPE);
|
||||
B2LE32(passLen);b2_update(&gB2S,(const uint8_t*)passBytes,passLen);
|
||||
B2LE32(saltLen);b2_update(&gB2S,salt,saltLen);
|
||||
B2LE32(0);B2LE32(0);
|
||||
#undef B2LE32
|
||||
b2_final(&gB2S, gH0);
|
||||
|
||||
uint8_t input[72]; memcpy(input, gH0, 64);
|
||||
for (uint32_t i = 0; i < 2; i++) {
|
||||
input[64]=i;input[65]=0;input[66]=0;input[67]=0;
|
||||
input[68]=0;input[69]=0;input[70]=0;input[71]=0;
|
||||
b2long(input, 72, gBufOut, A2_BLKSZ);
|
||||
gSdFile.seek((uint64_t)i * A2_BLKSZ); gSdFile.write(gBufOut, A2_BLKSZ);
|
||||
}
|
||||
gCurPass = 0;
|
||||
gCurBlock = 2;
|
||||
gDoneBlocks = 0;
|
||||
gStartMs = millis();
|
||||
gInitDone = true;
|
||||
generateAddresses(0, 0);
|
||||
}
|
||||
|
||||
bool shineSecretInitSd(String &error) {
|
||||
error = "";
|
||||
if (gSdReady) return true;
|
||||
gpio_reset_pin(GPIO_NUM_2);
|
||||
pinMode(PIN_SD_CMD, INPUT_PULLUP);
|
||||
pinMode(PIN_SD_D0, INPUT_PULLUP);
|
||||
delay(20);
|
||||
SD_MMC.setPins(PIN_SD_CLK, PIN_SD_CMD, PIN_SD_D0);
|
||||
gSdReady = SD_MMC.begin("/sdcard", true);
|
||||
if (!gSdReady) {
|
||||
error = "SD card error";
|
||||
return false;
|
||||
}
|
||||
if (!gBufPrev) {
|
||||
gBufPrev=(uint8_t*)ps_malloc(A2_BLKSZ);
|
||||
gBufRef=(uint8_t*)ps_malloc(A2_BLKSZ);
|
||||
gBufOut=(uint8_t*)ps_malloc(A2_BLKSZ);
|
||||
gBufZero=(uint8_t*)ps_calloc(1, A2_BLKSZ);
|
||||
gBufAddr=(uint8_t*)ps_malloc(A2_BLKSZ);
|
||||
gJ1Seg=(uint32_t*)ps_malloc(A2_SEG * sizeof(uint32_t));
|
||||
}
|
||||
if (!gBufPrev || !gBufRef || !gBufOut || !gBufZero || !gBufAddr || !gJ1Seg) {
|
||||
error = "PSRAM alloc failed";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool shineSecretStart(const char *normalizedLogin, const char *password, String &error) {
|
||||
error = "";
|
||||
if (!normalizedLogin || !*normalizedLogin) {
|
||||
error = "login not set";
|
||||
return false;
|
||||
}
|
||||
|
||||
char loginBuf[64];
|
||||
snprintf(loginBuf, sizeof(loginBuf), "%s", normalizedLogin);
|
||||
normalizeLoginInPlace(loginBuf);
|
||||
if (!loginBuf[0]) {
|
||||
error = "login not set";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!shineSecretInitSd(error)) return false;
|
||||
|
||||
if (gSdFile) gSdFile.close();
|
||||
SD_MMC.remove(SD_MEM_FILE);
|
||||
gSdFile = SD_MMC.open(SD_MEM_FILE, "w+");
|
||||
if (!gSdFile) {
|
||||
error = "SD open failed";
|
||||
return false;
|
||||
}
|
||||
|
||||
memset(gSecret, 0, sizeof(gSecret));
|
||||
gSecretB58[0] = '\0';
|
||||
gDone = false;
|
||||
gError = false;
|
||||
gRunning = true;
|
||||
gInitDone = false;
|
||||
setMessage("Generating secret...");
|
||||
|
||||
if (!password || !password[0]) {
|
||||
deriveLegacyMasterSecret(password ? password : "", gSecret);
|
||||
shineSecretBase58Encode(gSecret, 32, gSecretB58, sizeof(gSecretB58));
|
||||
gDone = true;
|
||||
gRunning = false;
|
||||
setMessage("Secret generated");
|
||||
gSdFile.close();
|
||||
SD_MMC.remove(SD_MEM_FILE);
|
||||
return true;
|
||||
}
|
||||
|
||||
argon2Init(loginBuf, password);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool shineSecretStep(uint32_t blocksPerTick) {
|
||||
if (!gRunning || !gInitDone) return gDone;
|
||||
bool done = false;
|
||||
for (uint32_t i = 0; i < blocksPerTick && !done; i++) {
|
||||
if (gCurPass >= A2_T) {
|
||||
done = true;
|
||||
break;
|
||||
}
|
||||
uint32_t blk = gCurBlock;
|
||||
uint32_t slice = blk / A2_SEG;
|
||||
uint32_t posInSlice = blk - slice * A2_SEG;
|
||||
bool xorMode = (gCurPass > 0);
|
||||
uint32_t J1;
|
||||
bool useArgon2i = (gCurPass == 0 && slice < 2);
|
||||
if (useArgon2i) {
|
||||
J1 = gJ1Seg[posInSlice];
|
||||
} else {
|
||||
uint32_t prevIdx = (blk == 0) ? A2_M - 1 : blk - 1;
|
||||
gSdFile.seek((uint64_t)prevIdx * A2_BLKSZ);
|
||||
gSdFile.read(gBufRef, 8);
|
||||
J1 = ((uint32_t*)gBufRef)[0];
|
||||
}
|
||||
uint32_t prevIdx = (blk == 0) ? A2_M - 1 : blk - 1;
|
||||
uint32_t refIdx = indexAlpha(gCurPass, slice, posInSlice, J1);
|
||||
fillBlock(prevIdx, refIdx, blk, xorMode);
|
||||
gDoneBlocks++;
|
||||
gCurBlock++;
|
||||
if (gCurBlock >= A2_M) {
|
||||
gCurBlock = 0;
|
||||
gCurPass++;
|
||||
} else {
|
||||
uint32_t newSlice = gCurBlock / A2_SEG;
|
||||
if (newSlice != slice && gCurPass == 0 && newSlice < 2) generateAddresses(gCurPass, newSlice);
|
||||
}
|
||||
done = (gCurPass >= A2_T);
|
||||
}
|
||||
|
||||
if (done) {
|
||||
gSdFile.seek((uint64_t)(A2_M - 1) * A2_BLKSZ);
|
||||
gSdFile.read(gBufOut, A2_BLKSZ);
|
||||
b2long(gBufOut, A2_BLKSZ, gSecret, A2_DKLEN);
|
||||
shineSecretBase58Encode(gSecret, 32, gSecretB58, sizeof(gSecretB58));
|
||||
gDone = true;
|
||||
gRunning = false;
|
||||
setMessage("Secret generated");
|
||||
gSdFile.close();
|
||||
SD_MMC.remove(SD_MEM_FILE);
|
||||
}
|
||||
return gDone;
|
||||
}
|
||||
|
||||
void shineSecretAbort() {
|
||||
gRunning = false;
|
||||
gDone = false;
|
||||
gError = false;
|
||||
gInitDone = false;
|
||||
gDoneBlocks = 0;
|
||||
if (gSdFile) gSdFile.close();
|
||||
if (gSdReady) SD_MMC.remove(SD_MEM_FILE);
|
||||
setMessage("Generation cancelled");
|
||||
}
|
||||
|
||||
ShineSecretGenerationStatus shineSecretStatus() {
|
||||
ShineSecretGenerationStatus status = {};
|
||||
status.running = gRunning;
|
||||
status.done = gDone;
|
||||
status.error = gError;
|
||||
status.doneBlocks = gDoneBlocks;
|
||||
status.totalBlocks = TOTAL_FILLS;
|
||||
status.elapsedSec = gStartMs == 0 ? 0 : (millis() - gStartMs) / 1000;
|
||||
snprintf(status.message, sizeof(status.message), "%s", gMessage);
|
||||
snprintf(status.secretBase58, sizeof(status.secretBase58), "%s", gSecretB58);
|
||||
return status;
|
||||
}
|
||||
|
||||
const uint8_t *shineSecretBytes() {
|
||||
return gSecret;
|
||||
}
|
||||
|
||||
const char *shineSecretBase58() {
|
||||
return gSecretB58;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
struct ShineSecretGenerationStatus {
|
||||
bool running;
|
||||
bool done;
|
||||
bool error;
|
||||
uint32_t doneBlocks;
|
||||
uint32_t totalBlocks;
|
||||
uint32_t elapsedSec;
|
||||
char message[96];
|
||||
char secretBase58[64];
|
||||
};
|
||||
|
||||
bool shineSecretInitSd(String &error);
|
||||
bool shineSecretStart(const char *normalizedLogin, const char *password, String &error);
|
||||
bool shineSecretStep(uint32_t blocksPerTick = 4);
|
||||
void shineSecretAbort();
|
||||
ShineSecretGenerationStatus shineSecretStatus();
|
||||
const uint8_t *shineSecretBytes();
|
||||
const char *shineSecretBase58();
|
||||
void shineSecretBase58Encode(const uint8_t *data, size_t len, char *out, size_t outSz);
|
||||
bool shineSecretBase58Decode(const char *input, uint8_t *out, size_t *outLen, size_t maxOutLen, String &error);
|
||||
@@ -0,0 +1,6 @@
|
||||
# SHiNE Homeserver UI Legacy
|
||||
|
||||
Это старый тестовый вариант UI для ESP32-платы `Waveshare ESP32-S3-Touch-AMOLED-2.16`.
|
||||
|
||||
Не использовать как основной скетч проекта.
|
||||
Основной рабочий скетч сейчас лежит в `../shine_homeserver_main/`.
|
||||
@@ -0,0 +1 @@
|
||||
#include "../../official-demo/examples/Arduino-v3.3.5/libraries/lvgl/src/extra/libs/qrcode/qrcodegen.c"
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* ESPRESSIF MIT License
|
||||
*
|
||||
* Copyright (c) 2018 <ESPRESSIF SYSTEMS (SHANGHAI) PTE LTD>
|
||||
*
|
||||
* Permission is hereby granted for use on all ESPRESSIF SYSTEMS products, in which case,
|
||||
* it is free of charge, to any person obtaining a copy of this software and associated
|
||||
* documentation files (the "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
* and/or sell copies of the Software, and to permit persons to whom the Software is furnished
|
||||
* to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef _AUDIO_HAL_H_
|
||||
#define _AUDIO_HAL_H_
|
||||
|
||||
#define AUDIO_HAL_VOL_DEFAULT 60
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Select media hal codec mode
|
||||
*/
|
||||
typedef enum {
|
||||
AUDIO_HAL_CODEC_MODE_ENCODE = 1, /*!< select adc */
|
||||
AUDIO_HAL_CODEC_MODE_DECODE, /*!< select dac */
|
||||
AUDIO_HAL_CODEC_MODE_BOTH, /*!< select both adc and dac */
|
||||
AUDIO_HAL_CODEC_MODE_LINE_IN, /*!< set adc channel */
|
||||
} audio_hal_codec_mode_t;
|
||||
|
||||
/**
|
||||
* @brief Select adc channel for input mic signal
|
||||
*/
|
||||
typedef enum {
|
||||
AUDIO_HAL_ADC_INPUT_LINE1 = 0x00, /*!< mic input to adc channel 1 */
|
||||
AUDIO_HAL_ADC_INPUT_LINE2, /*!< mic input to adc channel 2 */
|
||||
AUDIO_HAL_ADC_INPUT_ALL, /*!< mic input to both channels of adc */
|
||||
AUDIO_HAL_ADC_INPUT_DIFFERENCE, /*!< mic input to adc difference channel */
|
||||
} audio_hal_adc_input_t;
|
||||
|
||||
/**
|
||||
* @brief Select channel for dac output
|
||||
*/
|
||||
typedef enum {
|
||||
AUDIO_HAL_DAC_OUTPUT_LINE1 = 0x00, /*!< dac output signal to channel 1 */
|
||||
AUDIO_HAL_DAC_OUTPUT_LINE2, /*!< dac output signal to channel 2 */
|
||||
AUDIO_HAL_DAC_OUTPUT_ALL, /*!< dac output signal to both channels */
|
||||
} audio_hal_dac_output_t;
|
||||
|
||||
/**
|
||||
* @brief Select operating mode i.e. start or stop for audio codec chip
|
||||
*/
|
||||
typedef enum {
|
||||
AUDIO_HAL_CTRL_STOP = 0x00, /*!< set stop mode */
|
||||
AUDIO_HAL_CTRL_START = 0x01, /*!< set start mode */
|
||||
} audio_hal_ctrl_t;
|
||||
|
||||
/**
|
||||
* @brief Select I2S interface operating mode i.e. master or slave for audio codec chip
|
||||
*/
|
||||
typedef enum {
|
||||
AUDIO_HAL_MODE_SLAVE = 0x00, /*!< set slave mode */
|
||||
AUDIO_HAL_MODE_MASTER = 0x01, /*!< set master mode */
|
||||
} audio_hal_iface_mode_t;
|
||||
|
||||
/**
|
||||
* @brief Select I2S interface samples per second
|
||||
*/
|
||||
typedef enum {
|
||||
AUDIO_HAL_08K_SAMPLES, /*!< set to 8k samples per second */
|
||||
AUDIO_HAL_11K_SAMPLES, /*!< set to 11.025k samples per second */
|
||||
AUDIO_HAL_16K_SAMPLES, /*!< set to 16k samples in per second */
|
||||
AUDIO_HAL_22K_SAMPLES, /*!< set to 22.050k samples per second */
|
||||
AUDIO_HAL_24K_SAMPLES, /*!< set to 24k samples in per second */
|
||||
AUDIO_HAL_32K_SAMPLES, /*!< set to 32k samples in per second */
|
||||
AUDIO_HAL_44K_SAMPLES, /*!< set to 44.1k samples per second */
|
||||
AUDIO_HAL_48K_SAMPLES, /*!< set to 48k samples per second */
|
||||
} audio_hal_iface_samples_t;
|
||||
|
||||
/**
|
||||
* @brief Select I2S interface number of bits per sample
|
||||
*/
|
||||
typedef enum {
|
||||
AUDIO_HAL_BIT_LENGTH_16BITS = 1, /*!< set 16 bits per sample */
|
||||
AUDIO_HAL_BIT_LENGTH_24BITS, /*!< set 24 bits per sample */
|
||||
AUDIO_HAL_BIT_LENGTH_32BITS, /*!< set 32 bits per sample */
|
||||
} audio_hal_iface_bits_t;
|
||||
|
||||
/**
|
||||
* @brief Select I2S interface format for audio codec chip
|
||||
*/
|
||||
typedef enum {
|
||||
AUDIO_HAL_I2S_NORMAL = 0, /*!< set normal I2S format */
|
||||
AUDIO_HAL_I2S_LEFT, /*!< set all left format */
|
||||
AUDIO_HAL_I2S_RIGHT, /*!< set all right format */
|
||||
AUDIO_HAL_I2S_DSP, /*!< set dsp/pcm format */
|
||||
} audio_hal_iface_format_t;
|
||||
|
||||
/**
|
||||
* @brief I2s interface configuration for audio codec chip
|
||||
*/
|
||||
typedef struct {
|
||||
audio_hal_iface_mode_t mode; /*!< audio codec chip mode */
|
||||
audio_hal_iface_format_t fmt; /*!< I2S interface format */
|
||||
audio_hal_iface_samples_t samples; /*!< I2S interface samples per second */
|
||||
audio_hal_iface_bits_t bits; /*!< i2s interface number of bits per sample */
|
||||
} audio_hal_codec_i2s_iface_t;
|
||||
|
||||
/**
|
||||
* @brief Configure media hal for initialization of audio codec chip
|
||||
*/
|
||||
typedef struct {
|
||||
audio_hal_adc_input_t adc_input; /*!< set adc channel */
|
||||
audio_hal_dac_output_t dac_output; /*!< set dac channel */
|
||||
audio_hal_codec_mode_t codec_mode; /*!< select codec mode: adc, dac or both */
|
||||
audio_hal_codec_i2s_iface_t i2s_iface; /*!< set I2S interface configuration */
|
||||
} audio_hal_codec_config_t;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif //__AUDIO_HAL_H__
|
||||
@@ -0,0 +1,549 @@
|
||||
/*
|
||||
* ESPRESSIF MIT License
|
||||
*
|
||||
* Copyright (c) 2021 <ESPRESSIF SYSTEMS (SHANGHAI) CO., LTD>
|
||||
*
|
||||
* Permission is hereby granted for use on all ESPRESSIF SYSTEMS products, in which case,
|
||||
* it is free of charge, to any person obtaining a copy of this software and associated
|
||||
* documentation files (the "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
* and/or sell copies of the Software, and to permit persons to whom the Software is furnished
|
||||
* to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifdef ESP32
|
||||
|
||||
#include <Wire.h>
|
||||
#include <string.h>
|
||||
#include "esp_log.h"
|
||||
#include "es7210.h"
|
||||
|
||||
|
||||
#define I2S_DSP_MODE_A 0
|
||||
#define MCLK_DIV_FRE 256
|
||||
|
||||
|
||||
#define ES7210_MCLK_SOURCE FROM_CLOCK_DOUBLE_PIN /* In master mode, 0 : MCLK from pad 1 : MCLK from clock doubler */
|
||||
#define FROM_PAD_PIN 0
|
||||
#define FROM_CLOCK_DOUBLE_PIN 1
|
||||
|
||||
|
||||
static TwoWire *es7210wire;
|
||||
static es7210_gain_value_t gain;
|
||||
|
||||
/*
|
||||
* Clock coefficient structer
|
||||
*/
|
||||
struct _coeff_div_es7210 {
|
||||
uint32_t mclk; /* mclk frequency */
|
||||
uint32_t lrck; /* lrck */
|
||||
uint8_t ss_ds;
|
||||
uint8_t adc_div; /* adcclk divider */
|
||||
uint8_t dll; /* dll_bypass */
|
||||
uint8_t doubler; /* doubler enable */
|
||||
uint8_t osr; /* adc osr */
|
||||
uint8_t mclk_src; /* select mclk source */
|
||||
uint32_t lrck_h; /* The high 4 bits of lrck */
|
||||
uint32_t lrck_l; /* The low 8 bits of lrck */
|
||||
};
|
||||
|
||||
static const char *TAG = "ES7210";
|
||||
|
||||
static es7210_input_mics_t mic_select = (es7210_input_mics_t)(ES7210_INPUT_MIC1 | ES7210_INPUT_MIC2 | ES7210_INPUT_MIC3 | ES7210_INPUT_MIC4);
|
||||
|
||||
/* Codec hifi mclk clock divider coefficients
|
||||
* MEMBER REG
|
||||
* mclk: 0x03
|
||||
* lrck: standard
|
||||
* ss_ds: --
|
||||
* adc_div: 0x02
|
||||
* dll: 0x06
|
||||
* doubler: 0x02
|
||||
* osr: 0x07
|
||||
* mclk_src: 0x03
|
||||
* lrckh: 0x04
|
||||
* lrckl: 0x05
|
||||
*/
|
||||
static const struct _coeff_div_es7210 coeff_div[] = {
|
||||
//mclk lrck ss_ds adc_div dll doubler osr mclk_src lrckh lrckl
|
||||
/* 8k */
|
||||
{12288000, 8000 , 0x00, 0x03, 0x01, 0x00, 0x20, 0x00, 0x06, 0x00},
|
||||
{16384000, 8000 , 0x00, 0x04, 0x01, 0x00, 0x20, 0x00, 0x08, 0x00},
|
||||
{19200000, 8000 , 0x00, 0x1e, 0x00, 0x01, 0x28, 0x00, 0x09, 0x60},
|
||||
{4096000, 8000 , 0x00, 0x01, 0x01, 0x00, 0x20, 0x00, 0x02, 0x00},
|
||||
|
||||
/* 11.025k */
|
||||
{11289600, 11025, 0x00, 0x02, 0x01, 0x00, 0x20, 0x00, 0x01, 0x00},
|
||||
|
||||
/* 12k */
|
||||
{12288000, 12000, 0x00, 0x02, 0x01, 0x00, 0x20, 0x00, 0x04, 0x00},
|
||||
{19200000, 12000, 0x00, 0x14, 0x00, 0x01, 0x28, 0x00, 0x06, 0x40},
|
||||
|
||||
/* 16k */
|
||||
{4096000, 16000, 0x00, 0x01, 0x01, 0x01, 0x20, 0x00, 0x01, 0x00},
|
||||
{19200000, 16000, 0x00, 0x0a, 0x00, 0x00, 0x1e, 0x00, 0x04, 0x80},
|
||||
{16384000, 16000, 0x00, 0x02, 0x01, 0x00, 0x20, 0x00, 0x04, 0x00},
|
||||
{12288000, 16000, 0x00, 0x03, 0x01, 0x01, 0x20, 0x00, 0x03, 0x00},
|
||||
|
||||
/* 22.05k */
|
||||
{11289600, 22050, 0x00, 0x01, 0x01, 0x00, 0x20, 0x00, 0x02, 0x00},
|
||||
|
||||
/* 24k */
|
||||
{12288000, 24000, 0x00, 0x01, 0x01, 0x00, 0x20, 0x00, 0x02, 0x00},
|
||||
{19200000, 24000, 0x00, 0x0a, 0x00, 0x01, 0x28, 0x00, 0x03, 0x20},
|
||||
|
||||
/* 32k */
|
||||
{12288000, 32000, 0x00, 0x03, 0x00, 0x00, 0x20, 0x00, 0x01, 0x80},
|
||||
{16384000, 32000, 0x00, 0x01, 0x01, 0x00, 0x20, 0x00, 0x02, 0x00},
|
||||
{19200000, 32000, 0x00, 0x05, 0x00, 0x00, 0x1e, 0x00, 0x02, 0x58},
|
||||
|
||||
/* 44.1k */
|
||||
{11289600, 44100, 0x00, 0x01, 0x01, 0x01, 0x20, 0x00, 0x01, 0x00},
|
||||
|
||||
/* 48k */
|
||||
{12288000, 48000, 0x00, 0x01, 0x01, 0x01, 0x20, 0x00, 0x01, 0x00},
|
||||
{19200000, 48000, 0x00, 0x05, 0x00, 0x01, 0x28, 0x00, 0x01, 0x90},
|
||||
|
||||
/* 64k */
|
||||
{16384000, 64000, 0x01, 0x01, 0x01, 0x00, 0x20, 0x00, 0x01, 0x00},
|
||||
{19200000, 64000, 0x00, 0x05, 0x00, 0x01, 0x1e, 0x00, 0x01, 0x2c},
|
||||
|
||||
/* 88.2k */
|
||||
{11289600, 88200, 0x01, 0x01, 0x01, 0x01, 0x20, 0x00, 0x00, 0x80},
|
||||
|
||||
/* 96k */
|
||||
{12288000, 96000, 0x01, 0x01, 0x01, 0x01, 0x20, 0x00, 0x00, 0x80},
|
||||
{19200000, 96000, 0x01, 0x05, 0x00, 0x01, 0x28, 0x00, 0x00, 0xc8},
|
||||
};
|
||||
|
||||
static esp_err_t es7210_write_reg(uint8_t reg_addr, uint8_t data)
|
||||
{
|
||||
|
||||
es7210wire->beginTransmission(ES7210_ADDR);
|
||||
es7210wire->write(reg_addr);
|
||||
es7210wire->write(data);
|
||||
return es7210wire->endTransmission();
|
||||
|
||||
}
|
||||
|
||||
static esp_err_t es7210_update_reg_bit(uint8_t reg_addr, uint8_t update_bits, uint8_t data)
|
||||
{
|
||||
uint8_t regv;
|
||||
regv = es7210_read_reg(reg_addr);
|
||||
regv = (regv & (~update_bits)) | (update_bits & data);
|
||||
return es7210_write_reg(reg_addr, regv);
|
||||
}
|
||||
|
||||
static int get_coeff(uint32_t mclk, uint32_t lrck)
|
||||
{
|
||||
for (int i = 0; i < (sizeof(coeff_div) / sizeof(coeff_div[0])); i++) {
|
||||
if (coeff_div[i].lrck == lrck && coeff_div[i].mclk == mclk)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int8_t get_es7210_mclk_src(void)
|
||||
{
|
||||
return ES7210_MCLK_SOURCE;
|
||||
}
|
||||
|
||||
int es7210_read_reg(uint8_t reg_addr)
|
||||
{
|
||||
uint8_t data;
|
||||
es7210wire->beginTransmission(ES7210_ADDR);
|
||||
es7210wire->write(reg_addr);
|
||||
es7210wire->endTransmission(false);
|
||||
es7210wire->requestFrom(ES7210_ADDR, (size_t)1);
|
||||
data = es7210wire->read();
|
||||
return (int)data;
|
||||
}
|
||||
|
||||
esp_err_t es7210_config_sample(audio_hal_iface_samples_t sample)
|
||||
{
|
||||
uint8_t regv;
|
||||
int coeff;
|
||||
int sample_fre = 0;
|
||||
int mclk_fre = 0;
|
||||
esp_err_t ret = ESP_OK;
|
||||
switch (sample) {
|
||||
case AUDIO_HAL_08K_SAMPLES:
|
||||
sample_fre = 8000;
|
||||
break;
|
||||
case AUDIO_HAL_11K_SAMPLES:
|
||||
sample_fre = 11025;
|
||||
break;
|
||||
case AUDIO_HAL_16K_SAMPLES:
|
||||
sample_fre = 16000;
|
||||
break;
|
||||
case AUDIO_HAL_22K_SAMPLES:
|
||||
sample_fre = 22050;
|
||||
break;
|
||||
case AUDIO_HAL_24K_SAMPLES:
|
||||
sample_fre = 24000;
|
||||
break;
|
||||
case AUDIO_HAL_32K_SAMPLES:
|
||||
sample_fre = 32000;
|
||||
break;
|
||||
case AUDIO_HAL_44K_SAMPLES:
|
||||
sample_fre = 44100;
|
||||
break;
|
||||
case AUDIO_HAL_48K_SAMPLES:
|
||||
sample_fre = 48000;
|
||||
break;
|
||||
default:
|
||||
ESP_LOGE(TAG, "Unable to configure sample rate %dHz", sample_fre);
|
||||
break;
|
||||
}
|
||||
mclk_fre = sample_fre * MCLK_DIV_FRE;
|
||||
coeff = get_coeff(mclk_fre, sample_fre);
|
||||
if (coeff < 0) {
|
||||
ESP_LOGE(TAG, "Unable to configure sample rate %dHz with %dHz MCLK", sample_fre, mclk_fre);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
/* Set clock parammeters */
|
||||
if (coeff >= 0) {
|
||||
/* Set adc_div & doubler & dll */
|
||||
regv = es7210_read_reg(ES7210_MAINCLK_REG02) & 0x00;
|
||||
regv |= coeff_div[coeff].adc_div;
|
||||
regv |= coeff_div[coeff].doubler << 6;
|
||||
regv |= coeff_div[coeff].dll << 7;
|
||||
ret |= es7210_write_reg(ES7210_MAINCLK_REG02, regv);
|
||||
/* Set osr */
|
||||
regv = coeff_div[coeff].osr;
|
||||
ret |= es7210_write_reg(ES7210_OSR_REG07, regv);
|
||||
/* Set lrck */
|
||||
regv = coeff_div[coeff].lrck_h;
|
||||
ret |= es7210_write_reg(ES7210_LRCK_DIVH_REG04, regv);
|
||||
regv = coeff_div[coeff].lrck_l;
|
||||
ret |= es7210_write_reg(ES7210_LRCK_DIVL_REG05, regv);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
esp_err_t es7210_mic_select(es7210_input_mics_t mic)
|
||||
{
|
||||
esp_err_t ret = ESP_OK;
|
||||
mic_select = mic;
|
||||
if (mic_select & (ES7210_INPUT_MIC1 | ES7210_INPUT_MIC2 | ES7210_INPUT_MIC3 | ES7210_INPUT_MIC4)) {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
ret |= es7210_update_reg_bit(ES7210_MIC1_GAIN_REG43 + i, 0x10, 0x00);
|
||||
}
|
||||
ret |= es7210_write_reg(ES7210_MIC12_POWER_REG4B, 0xff);
|
||||
ret |= es7210_write_reg(ES7210_MIC34_POWER_REG4C, 0xff);
|
||||
if (mic_select & ES7210_INPUT_MIC1) {
|
||||
ESP_LOGI(TAG, "Enable ES7210_INPUT_MIC1");
|
||||
ret |= es7210_update_reg_bit(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00);
|
||||
ret |= es7210_write_reg(ES7210_MIC12_POWER_REG4B, 0x00);
|
||||
ret |= es7210_update_reg_bit(ES7210_MIC1_GAIN_REG43, 0x10, 0x10);
|
||||
}
|
||||
if (mic_select & ES7210_INPUT_MIC2) {
|
||||
ESP_LOGI(TAG, "Enable ES7210_INPUT_MIC2");
|
||||
ret |= es7210_update_reg_bit(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00);
|
||||
ret |= es7210_write_reg(ES7210_MIC12_POWER_REG4B, 0x00);
|
||||
ret |= es7210_update_reg_bit(ES7210_MIC2_GAIN_REG44, 0x10, 0x10);
|
||||
}
|
||||
if (mic_select & ES7210_INPUT_MIC3) {
|
||||
ESP_LOGI(TAG, "Enable ES7210_INPUT_MIC3");
|
||||
ret |= es7210_update_reg_bit(ES7210_CLOCK_OFF_REG01, 0x15, 0x00);
|
||||
ret |= es7210_write_reg(ES7210_MIC34_POWER_REG4C, 0x00);
|
||||
ret |= es7210_update_reg_bit(ES7210_MIC3_GAIN_REG45, 0x10, 0x10);
|
||||
}
|
||||
if (mic_select & ES7210_INPUT_MIC4) {
|
||||
ESP_LOGI(TAG, "Enable ES7210_INPUT_MIC4");
|
||||
ret |= es7210_update_reg_bit(ES7210_CLOCK_OFF_REG01, 0x15, 0x00);
|
||||
ret |= es7210_write_reg(ES7210_MIC34_POWER_REG4C, 0x00);
|
||||
ret |= es7210_update_reg_bit(ES7210_MIC4_GAIN_REG46, 0x10, 0x10);
|
||||
}
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Microphone selection error");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
esp_err_t es7210_adc_init(TwoWire *tw, audio_hal_codec_config_t *codec_cfg)
|
||||
{
|
||||
esp_err_t ret = ESP_OK;
|
||||
|
||||
es7210wire = tw;
|
||||
|
||||
ret |= es7210_write_reg(ES7210_RESET_REG00, 0xff);
|
||||
ret |= es7210_write_reg(ES7210_RESET_REG00, 0x41);
|
||||
ret |= es7210_write_reg(ES7210_CLOCK_OFF_REG01, 0x1f);
|
||||
ret |= es7210_write_reg(ES7210_TIME_CONTROL0_REG09, 0x30); /* Set chip state cycle */
|
||||
ret |= es7210_write_reg(ES7210_TIME_CONTROL1_REG0A, 0x30); /* Set power on state cycle */
|
||||
// ret |= es7210_write_reg(ES7210_ADC12_HPF2_REG23, 0x2a); /* Quick setup */
|
||||
// ret |= es7210_write_reg(ES7210_ADC12_HPF1_REG22, 0x0a);
|
||||
// ret |= es7210_write_reg(ES7210_ADC34_HPF2_REG20, 0x0a);
|
||||
// ret |= es7210_write_reg(ES7210_ADC34_HPF1_REG21, 0x2a);
|
||||
/* Set master/slave audio interface */
|
||||
audio_hal_codec_i2s_iface_t *i2s_cfg = & (codec_cfg->i2s_iface);
|
||||
switch (i2s_cfg->mode) {
|
||||
case AUDIO_HAL_MODE_MASTER: /* MASTER MODE */
|
||||
ESP_LOGI(TAG, "ES7210 in Master mode");
|
||||
// ret |= es7210_update_reg_bit(ES7210_MODE_CONFIG_REG08, 0x01, 0x01);
|
||||
ret |= es7210_write_reg(ES7210_MODE_CONFIG_REG08, 0x20);
|
||||
/* Select clock source for internal mclk */
|
||||
switch (get_es7210_mclk_src()) {
|
||||
case FROM_PAD_PIN:
|
||||
ret |= es7210_update_reg_bit(ES7210_MASTER_CLK_REG03, 0x80, 0x00);
|
||||
break;
|
||||
case FROM_CLOCK_DOUBLE_PIN:
|
||||
ret |= es7210_update_reg_bit(ES7210_MASTER_CLK_REG03, 0x80, 0x80);
|
||||
break;
|
||||
default:
|
||||
ret |= es7210_update_reg_bit(ES7210_MASTER_CLK_REG03, 0x80, 0x00);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case AUDIO_HAL_MODE_SLAVE: /* SLAVE MODE */
|
||||
ESP_LOGI(TAG, "ES7210 in Slave mode");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
ret |= es7210_write_reg(ES7210_ANALOG_REG40, 0xC3); /* Select power off analog, vdda = 3.3V, close vx20ff, VMID select 5KΩ start */
|
||||
ret |= es7210_write_reg(ES7210_MIC12_BIAS_REG41, 0x70); /* Select 2.87v */
|
||||
ret |= es7210_write_reg(ES7210_MIC34_BIAS_REG42, 0x70); /* Select 2.87v */
|
||||
ret |= es7210_write_reg(ES7210_OSR_REG07, 0x20);
|
||||
ret |= es7210_write_reg(ES7210_MAINCLK_REG02, 0xc1); /* Set the frequency division coefficient and use dll except clock doubler, and need to set 0xc1 to clear the state */
|
||||
ret |= es7210_config_sample(i2s_cfg->samples);
|
||||
ret |= es7210_mic_select(mic_select);
|
||||
ret |= es7210_adc_set_gain_all(GAIN_0DB);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t es7210_adc_deinit()
|
||||
{
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t es7210_config_fmt(audio_hal_iface_format_t fmt)
|
||||
{
|
||||
esp_err_t ret = ESP_OK;
|
||||
uint8_t adc_iface = 0;
|
||||
adc_iface = es7210_read_reg(ES7210_SDP_INTERFACE1_REG11);
|
||||
adc_iface &= 0xfc;
|
||||
switch (fmt) {
|
||||
case AUDIO_HAL_I2S_NORMAL:
|
||||
ESP_LOGD(TAG, "ES7210 in I2S Format");
|
||||
adc_iface |= 0x00;
|
||||
break;
|
||||
case AUDIO_HAL_I2S_LEFT:
|
||||
case AUDIO_HAL_I2S_RIGHT:
|
||||
ESP_LOGD(TAG, "ES7210 in LJ Format");
|
||||
adc_iface |= 0x01;
|
||||
break;
|
||||
case AUDIO_HAL_I2S_DSP:
|
||||
if (I2S_DSP_MODE_A) {
|
||||
ESP_LOGD(TAG, "ES7210 in DSP-A Format");
|
||||
adc_iface |= 0x03;
|
||||
} else {
|
||||
ESP_LOGD(TAG, "ES7210 in DSP-B Format");
|
||||
adc_iface |= 0x13;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
adc_iface &= 0xfc;
|
||||
break;
|
||||
}
|
||||
ret |= es7210_write_reg(ES7210_SDP_INTERFACE1_REG11, adc_iface);
|
||||
/* Force ADC1/2 output to SDOUT1 and ADC3/4 output to SDOUT2 */
|
||||
ret |= es7210_write_reg(ES7210_SDP_INTERFACE2_REG12, 0x00);
|
||||
return ret;
|
||||
}
|
||||
|
||||
esp_err_t es7210_set_bits(audio_hal_iface_bits_t bits)
|
||||
{
|
||||
esp_err_t ret = ESP_OK;
|
||||
uint8_t adc_iface = 0;
|
||||
adc_iface = es7210_read_reg(ES7210_SDP_INTERFACE1_REG11);
|
||||
adc_iface &= 0x1f;
|
||||
switch (bits) {
|
||||
case AUDIO_HAL_BIT_LENGTH_16BITS:
|
||||
adc_iface |= 0x60;
|
||||
break;
|
||||
case AUDIO_HAL_BIT_LENGTH_24BITS:
|
||||
adc_iface |= 0x00;
|
||||
break;
|
||||
case AUDIO_HAL_BIT_LENGTH_32BITS:
|
||||
adc_iface |= 0x80;
|
||||
break;
|
||||
default:
|
||||
adc_iface |= 0x60;
|
||||
break;
|
||||
}
|
||||
ret |= es7210_write_reg(ES7210_SDP_INTERFACE1_REG11, adc_iface);
|
||||
return ret;
|
||||
}
|
||||
|
||||
esp_err_t es7210_adc_config_i2s(audio_hal_codec_mode_t mode, audio_hal_codec_i2s_iface_t *iface)
|
||||
{
|
||||
esp_err_t ret = ESP_OK;
|
||||
ret |= es7210_set_bits(iface->bits);
|
||||
ret |= es7210_config_fmt(iface->fmt);
|
||||
ret |= es7210_config_sample(iface->samples);
|
||||
return ret;
|
||||
}
|
||||
|
||||
esp_err_t es7210_start(uint8_t clock_reg_value)
|
||||
{
|
||||
esp_err_t ret = ESP_OK;
|
||||
ret |= es7210_write_reg(ES7210_CLOCK_OFF_REG01, clock_reg_value);
|
||||
ret |= es7210_write_reg(ES7210_POWER_DOWN_REG06, 0x00);
|
||||
// ret |= es7210_write_reg(ES7210_ANALOG_REG40, 0x40);
|
||||
ret |= es7210_write_reg(ES7210_MIC1_POWER_REG47, 0x00);
|
||||
ret |= es7210_write_reg(ES7210_MIC2_POWER_REG48, 0x00);
|
||||
ret |= es7210_write_reg(ES7210_MIC3_POWER_REG49, 0x00);
|
||||
ret |= es7210_write_reg(ES7210_MIC4_POWER_REG4A, 0x00);
|
||||
ret |= es7210_mic_select(mic_select);
|
||||
return ret;
|
||||
}
|
||||
|
||||
esp_err_t es7210_stop(void)
|
||||
{
|
||||
esp_err_t ret = ESP_OK;
|
||||
ret |= es7210_write_reg(ES7210_MIC1_POWER_REG47, 0xff);
|
||||
ret |= es7210_write_reg(ES7210_MIC2_POWER_REG48, 0xff);
|
||||
ret |= es7210_write_reg(ES7210_MIC3_POWER_REG49, 0xff);
|
||||
ret |= es7210_write_reg(ES7210_MIC4_POWER_REG4A, 0xff);
|
||||
ret |= es7210_write_reg(ES7210_MIC12_POWER_REG4B,0xff);
|
||||
ret |= es7210_write_reg(ES7210_MIC34_POWER_REG4C, 0xff);
|
||||
// ret |= es7210_write_reg(ES7210_ANALOG_REG40, 0xc0);
|
||||
ret |= es7210_write_reg(ES7210_CLOCK_OFF_REG01, 0x7f);
|
||||
ret |= es7210_write_reg(ES7210_POWER_DOWN_REG06, 0x07);
|
||||
return ret;
|
||||
}
|
||||
|
||||
esp_err_t es7210_adc_ctrl_state(audio_hal_codec_mode_t mode, audio_hal_ctrl_t ctrl_state)
|
||||
{
|
||||
static uint8_t regv;
|
||||
esp_err_t ret = ESP_OK;
|
||||
// ESP_LOGW(TAG, "ES7210 only supports ADC mode");
|
||||
ret = es7210_read_reg(ES7210_CLOCK_OFF_REG01);
|
||||
if ((ret != 0x7f) && (ret != 0xff)) {
|
||||
regv = es7210_read_reg(ES7210_CLOCK_OFF_REG01);
|
||||
}
|
||||
if (ctrl_state == AUDIO_HAL_CTRL_START) {
|
||||
ESP_LOGI(TAG, "The ES7210_CLOCK_OFF_REG01 value before stop is %x",regv);
|
||||
ret |= es7210_start(regv);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "The codec is about to stop");
|
||||
regv = es7210_read_reg(ES7210_CLOCK_OFF_REG01);
|
||||
ret |= es7210_stop();
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t es7210_adc_set_gain(es7210_input_mics_t mic_mask, es7210_gain_value_t gain)
|
||||
{
|
||||
esp_err_t ret_val = ESP_OK;
|
||||
|
||||
if (gain < GAIN_0DB) {
|
||||
gain = GAIN_0DB;
|
||||
}
|
||||
|
||||
if (gain > GAIN_37_5DB) {
|
||||
gain = GAIN_37_5DB;
|
||||
}
|
||||
|
||||
if (mic_mask & ES7210_INPUT_MIC1) {
|
||||
ret_val |= es7210_update_reg_bit(ES7210_MIC1_GAIN_REG43, 0x0f, gain);
|
||||
}
|
||||
if (mic_mask & ES7210_INPUT_MIC2) {
|
||||
ret_val |= es7210_update_reg_bit(ES7210_MIC2_GAIN_REG44, 0x0f, gain);
|
||||
}
|
||||
if (mic_mask & ES7210_INPUT_MIC3) {
|
||||
ret_val |= es7210_update_reg_bit(ES7210_MIC3_GAIN_REG45, 0x0f, gain);
|
||||
}
|
||||
if (mic_mask & ES7210_INPUT_MIC4) {
|
||||
ret_val |= es7210_update_reg_bit(ES7210_MIC4_GAIN_REG46, 0x0f, gain);
|
||||
}
|
||||
|
||||
return ret_val;
|
||||
}
|
||||
|
||||
esp_err_t es7210_adc_set_gain_all(es7210_gain_value_t gain)
|
||||
{
|
||||
esp_err_t ret = ESP_OK;
|
||||
uint32_t max_gain_vaule = 14;
|
||||
if (gain < 0) {
|
||||
gain = (es7210_gain_value_t) 0;
|
||||
} else if (gain > max_gain_vaule) {
|
||||
gain = (es7210_gain_value_t) max_gain_vaule;
|
||||
}
|
||||
ESP_LOGD(TAG, "SET: gain:%d", gain);
|
||||
if (mic_select & ES7210_INPUT_MIC1) {
|
||||
ret |= es7210_update_reg_bit(ES7210_MIC1_GAIN_REG43, 0x0f, gain);
|
||||
}
|
||||
if (mic_select & ES7210_INPUT_MIC2) {
|
||||
ret |= es7210_update_reg_bit(ES7210_MIC2_GAIN_REG44, 0x0f, gain);
|
||||
}
|
||||
if (mic_select & ES7210_INPUT_MIC3) {
|
||||
ret |= es7210_update_reg_bit(ES7210_MIC3_GAIN_REG45, 0x0f, gain);
|
||||
}
|
||||
if (mic_select & ES7210_INPUT_MIC4) {
|
||||
ret |= es7210_update_reg_bit(ES7210_MIC4_GAIN_REG46, 0x0f, gain);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
esp_err_t es7210_adc_get_gain(es7210_input_mics_t mic_mask, es7210_gain_value_t *gain)
|
||||
{
|
||||
int regv = 0;
|
||||
uint8_t gain_value;
|
||||
if (mic_mask & ES7210_INPUT_MIC1) {
|
||||
regv = es7210_read_reg(ES7210_MIC1_GAIN_REG43);
|
||||
} else if (mic_mask & ES7210_INPUT_MIC2) {
|
||||
regv = es7210_read_reg(ES7210_MIC2_GAIN_REG44);
|
||||
} else if (mic_mask & ES7210_INPUT_MIC3) {
|
||||
regv = es7210_read_reg(ES7210_MIC3_GAIN_REG45);
|
||||
} else if (mic_mask & ES7210_INPUT_MIC4) {
|
||||
regv = es7210_read_reg(ES7210_MIC4_GAIN_REG46);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "No MIC selected");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
if (regv == ESP_FAIL) {
|
||||
return regv;
|
||||
}
|
||||
gain_value = (regv & 0x0f); /* Retain the last four bits for gain */
|
||||
*gain = (es7210_gain_value_t) gain_value;
|
||||
ESP_LOGI(TAG, "GET: gain_value:%d", gain_value);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t es7210_adc_set_volume(int volume)
|
||||
{
|
||||
esp_err_t ret = ESP_OK;
|
||||
ESP_LOGD(TAG, "ADC can adjust gain");
|
||||
return ret;
|
||||
}
|
||||
|
||||
esp_err_t es7210_set_mute(bool enable)
|
||||
{
|
||||
ESP_LOGD(TAG, "ES7210 SetMute :%d", enable);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void es7210_read_all(void)
|
||||
{
|
||||
for (int i = 0; i <= 0x4E; i++) {
|
||||
uint8_t reg = es7210_read_reg(i);
|
||||
ets_printf("REG:%02x, %02x\n", reg, i);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,260 @@
|
||||
/*
|
||||
* ESPRESSIF MIT License
|
||||
*
|
||||
* Copyright (c) 2021 <ESPRESSIF SYSTEMS (SHANGHAI) CO., LTD>
|
||||
*
|
||||
* Permission is hereby granted for use on all ESPRESSIF SYSTEMS products, in which case,
|
||||
* it is free of charge, to any person obtaining a copy of this software and associated
|
||||
* documentation files (the "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
* and/or sell copies of the Software, and to permit persons to whom the Software is furnished
|
||||
* to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or
|
||||
* substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef _ES7210_H
|
||||
#define _ES7210_H
|
||||
|
||||
#include "audio_hal.h"
|
||||
#include <Wire.h>
|
||||
|
||||
typedef enum {
|
||||
ES7210_AD1_AD0_00 = 0x40,
|
||||
ES7210_AD1_AD0_01 = 0x41,
|
||||
ES7210_AD1_AD0_10 = 0x42,
|
||||
ES7210_AD1_AD0_11 = 0x43,
|
||||
} es7210_address_t;
|
||||
|
||||
/* ES7210 address*/
|
||||
#define ES7210_ADDR ES7210_AD1_AD0_00
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define ES7210_RESET_REG00 0x00 /* Reset control */
|
||||
#define ES7210_CLOCK_OFF_REG01 0x01 /* Used to turn off the ADC clock */
|
||||
#define ES7210_MAINCLK_REG02 0x02 /* Set ADC clock frequency division */
|
||||
#define ES7210_MASTER_CLK_REG03 0x03 /* MCLK source $ SCLK division */
|
||||
#define ES7210_LRCK_DIVH_REG04 0x04 /* lrck_divh */
|
||||
#define ES7210_LRCK_DIVL_REG05 0x05 /* lrck_divl */
|
||||
#define ES7210_POWER_DOWN_REG06 0x06 /* power down */
|
||||
#define ES7210_OSR_REG07 0x07
|
||||
#define ES7210_MODE_CONFIG_REG08 0x08 /* Set master/slave & channels */
|
||||
#define ES7210_TIME_CONTROL0_REG09 0x09 /* Set Chip intial state period*/
|
||||
#define ES7210_TIME_CONTROL1_REG0A 0x0A /* Set Power up state period */
|
||||
#define ES7210_SDP_INTERFACE1_REG11 0x11 /* Set sample & fmt */
|
||||
#define ES7210_SDP_INTERFACE2_REG12 0x12 /* Pins state */
|
||||
#define ES7210_ADC_AUTOMUTE_REG13 0x13 /* Set mute */
|
||||
#define ES7210_ADC34_MUTERANGE_REG14 0x14 /* Set mute range */
|
||||
#define ES7210_ADC34_HPF2_REG20 0x20 /* HPF */
|
||||
#define ES7210_ADC34_HPF1_REG21 0x21
|
||||
#define ES7210_ADC12_HPF1_REG22 0x22
|
||||
#define ES7210_ADC12_HPF2_REG23 0x23
|
||||
#define ES7210_ANALOG_REG40 0x40 /* ANALOG Power */
|
||||
#define ES7210_MIC12_BIAS_REG41 0x41
|
||||
#define ES7210_MIC34_BIAS_REG42 0x42
|
||||
#define ES7210_MIC1_GAIN_REG43 0x43
|
||||
#define ES7210_MIC2_GAIN_REG44 0x44
|
||||
#define ES7210_MIC3_GAIN_REG45 0x45
|
||||
#define ES7210_MIC4_GAIN_REG46 0x46
|
||||
#define ES7210_MIC1_POWER_REG47 0x47
|
||||
#define ES7210_MIC2_POWER_REG48 0x48
|
||||
#define ES7210_MIC3_POWER_REG49 0x49
|
||||
#define ES7210_MIC4_POWER_REG4A 0x4A
|
||||
#define ES7210_MIC12_POWER_REG4B 0x4B /* MICBias & ADC & PGA Power */
|
||||
#define ES7210_MIC34_POWER_REG4C 0x4C
|
||||
|
||||
|
||||
|
||||
typedef enum {
|
||||
ES7210_INPUT_MIC1 = 0x01,
|
||||
ES7210_INPUT_MIC2 = 0x02,
|
||||
ES7210_INPUT_MIC3 = 0x04,
|
||||
ES7210_INPUT_MIC4 = 0x08
|
||||
} es7210_input_mics_t;
|
||||
|
||||
typedef enum gain_value{
|
||||
GAIN_0DB = 0,
|
||||
GAIN_3DB,
|
||||
GAIN_6DB,
|
||||
GAIN_9DB,
|
||||
GAIN_12DB,
|
||||
GAIN_15DB,
|
||||
GAIN_18DB,
|
||||
GAIN_21DB,
|
||||
GAIN_24DB,
|
||||
GAIN_27DB,
|
||||
GAIN_30DB,
|
||||
GAIN_33DB,
|
||||
GAIN_34_5DB,
|
||||
GAIN_36DB,
|
||||
GAIN_37_5DB,
|
||||
} es7210_gain_value_t;
|
||||
|
||||
/*
|
||||
* @brief Initialize ES7210 ADC chip
|
||||
*
|
||||
* @param[in] codec_cfg: configuration of ES7210
|
||||
*
|
||||
* @return
|
||||
* - ESP_OK
|
||||
* - ESP_FAIL
|
||||
*/
|
||||
esp_err_t es7210_adc_init(TwoWire *tw, audio_hal_codec_config_t *codec_cfg);
|
||||
|
||||
/**
|
||||
* @brief Deinitialize ES7210 ADC chip
|
||||
*
|
||||
* @return
|
||||
* - ESP_OK
|
||||
* - ESP_FAIL
|
||||
*/
|
||||
esp_err_t es7210_adc_deinit();
|
||||
|
||||
/**
|
||||
* @brief Configure ES7210 ADC mode and I2S interface
|
||||
*
|
||||
* @param[in] mode: codec mode
|
||||
* @param[in] iface: I2S config
|
||||
*
|
||||
* @return
|
||||
* - ESP_FAIL Parameter error
|
||||
* - ESP_OK Success
|
||||
*/
|
||||
esp_err_t es7210_adc_config_i2s(audio_hal_codec_mode_t mode, audio_hal_codec_i2s_iface_t *iface);
|
||||
|
||||
/**
|
||||
* @brief Control ES7210 ADC chip
|
||||
*
|
||||
* @param[in] mode: codec mode
|
||||
* @param[in] ctrl_state: start or stop progress
|
||||
*
|
||||
* @return
|
||||
* - ESP_FAIL Parameter error
|
||||
* - ESP_OK Success
|
||||
*/
|
||||
esp_err_t es7210_adc_ctrl_state(audio_hal_codec_mode_t mode, audio_hal_ctrl_t ctrl_state);
|
||||
|
||||
/**
|
||||
* @brief Set gain of given mask
|
||||
*
|
||||
* @param[in] mic_mask Mask of MIC channel
|
||||
*
|
||||
* @param[in] gain: gain
|
||||
*
|
||||
* gain : value
|
||||
* GAIN_0DB : 1
|
||||
* GAIN_3DB : 2
|
||||
* GAIN_6DB : 3
|
||||
* ·
|
||||
* ·
|
||||
* ·
|
||||
* GAIN_30DB : 10
|
||||
* GAIN_33DB : 11
|
||||
* GAIN_34_5DB : 12
|
||||
* GAIN_36DB : 13
|
||||
* GAIN_37_5DB : 14
|
||||
*
|
||||
* @return
|
||||
* - ESP_OK
|
||||
* - ESP_FAIL
|
||||
*/
|
||||
esp_err_t es7210_adc_set_gain(es7210_input_mics_t mic_mask, es7210_gain_value_t gain);
|
||||
|
||||
/**
|
||||
* @brief Set gain (Note: the enabled microphone sets the same gain)
|
||||
*
|
||||
* @param[in] gain: gain
|
||||
*
|
||||
* gain : value
|
||||
* GAIN_0DB : 1
|
||||
* GAIN_3DB : 2
|
||||
* GAIN_6DB : 3
|
||||
* ·
|
||||
* ·
|
||||
* ·
|
||||
* GAIN_30DB : 10
|
||||
* GAIN_33DB : 11
|
||||
* GAIN_34_5DB : 12
|
||||
* GAIN_36DB : 13
|
||||
* GAIN_37_5DB : 14
|
||||
*
|
||||
* @return
|
||||
* - ESP_OK
|
||||
* - ESP_FAIL
|
||||
*/
|
||||
esp_err_t es7210_adc_set_gain_all(es7210_gain_value_t gain);
|
||||
|
||||
/**
|
||||
* @brief Get MIC gain
|
||||
*
|
||||
* @param mic_mask Selected MIC
|
||||
* @param gain Pointer to `es7210_gain_value_t`
|
||||
* @return
|
||||
* - ESP_OK
|
||||
* - ESP_FAIL
|
||||
*/
|
||||
esp_err_t es7210_adc_get_gain(es7210_input_mics_t mic_mask, es7210_gain_value_t *gain);
|
||||
|
||||
/**
|
||||
* @brief Set volume
|
||||
*
|
||||
* @param[in] volume: volume
|
||||
*
|
||||
* @return
|
||||
* - ESP_OK
|
||||
*/
|
||||
esp_err_t es7210_adc_set_volume(int volume);
|
||||
|
||||
/**
|
||||
* @brief Set ES7210 ADC mute status
|
||||
*
|
||||
* @return
|
||||
* - ESP_FAIL
|
||||
* - ESP_OK
|
||||
*/
|
||||
esp_err_t es7210_set_mute(bool enable);
|
||||
|
||||
/**
|
||||
* @brief Select ES7210 mic
|
||||
*
|
||||
* @param[in] mic: mics
|
||||
*
|
||||
* @return
|
||||
* - ESP_FAIL
|
||||
* - ESP_OK
|
||||
*/
|
||||
esp_err_t es7210_mic_select(es7210_input_mics_t mic);
|
||||
|
||||
/**
|
||||
* @brief Read regs of ES7210
|
||||
*
|
||||
* @param[in] reg_addr: reg_addr
|
||||
*
|
||||
* @return
|
||||
* - ESP_FAIL
|
||||
* - ESP_OK
|
||||
*/
|
||||
int es7210_read_reg(uint8_t reg_addr);
|
||||
|
||||
/**
|
||||
* @brief Read all regs of ES7210
|
||||
*/
|
||||
void es7210_read_all(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* _ES7210_H_ */
|
||||
@@ -0,0 +1,443 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2015-2022 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include "es8311.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_err.h"
|
||||
#include "esp_check.h"
|
||||
#include "es8311_reg.h"
|
||||
#include "esp32-hal-i2c.h"
|
||||
|
||||
|
||||
typedef struct {
|
||||
unsigned int port;
|
||||
uint16_t dev_addr;
|
||||
} es8311_dev_t;
|
||||
|
||||
/*
|
||||
* Clock coefficient structure
|
||||
*/
|
||||
struct _coeff_div {
|
||||
uint32_t mclk; /* mclk frequency */
|
||||
uint32_t rate; /* sample rate */
|
||||
uint8_t pre_div; /* the pre divider with range from 1 to 8 */
|
||||
uint8_t pre_multi; /* the pre multiplier with 0: 1x, 1: 2x, 2: 4x, 3: 8x selection */
|
||||
uint8_t adc_div; /* adcclk divider */
|
||||
uint8_t dac_div; /* dacclk divider */
|
||||
uint8_t fs_mode; /* double speed or single speed, =0, ss, =1, ds */
|
||||
uint8_t lrck_h; /* adclrck divider and daclrck divider */
|
||||
uint8_t lrck_l;
|
||||
uint8_t bclk_div; /* sclk divider */
|
||||
uint8_t adc_osr; /* adc osr */
|
||||
uint8_t dac_osr; /* dac osr */
|
||||
};
|
||||
|
||||
/* codec hifi mclk clock divider coefficients */
|
||||
static const struct _coeff_div coeff_div[] = {
|
||||
/*!<mclk rate pre_div mult adc_div dac_div fs_mode lrch lrcl bckdiv osr */
|
||||
/* 8k */
|
||||
{12288000, 8000, 0x06, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{18432000, 8000, 0x03, 0x01, 0x03, 0x03, 0x00, 0x05, 0xff, 0x18, 0x10, 0x10},
|
||||
{16384000, 8000, 0x08, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{8192000, 8000, 0x04, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{6144000, 8000, 0x03, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{4096000, 8000, 0x02, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{3072000, 8000, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{2048000, 8000, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{1536000, 8000, 0x03, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{1024000, 8000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
|
||||
/* 11.025k */
|
||||
{11289600, 11025, 0x04, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{5644800, 11025, 0x02, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{2822400, 11025, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{1411200, 11025, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
|
||||
/* 12k */
|
||||
{12288000, 12000, 0x04, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{6144000, 12000, 0x02, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{3072000, 12000, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{1536000, 12000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
|
||||
/* 16k */
|
||||
{12288000, 16000, 0x03, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{18432000, 16000, 0x03, 0x01, 0x03, 0x03, 0x00, 0x02, 0xff, 0x0c, 0x10, 0x10},
|
||||
{16384000, 16000, 0x04, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{8192000, 16000, 0x02, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{6144000, 16000, 0x03, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{4096000, 16000, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{3072000, 16000, 0x03, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{2048000, 16000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{1536000, 16000, 0x03, 0x03, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{1024000, 16000, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
|
||||
/* 22.05k */
|
||||
{11289600, 22050, 0x02, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{5644800, 22050, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{2822400, 22050, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{1411200, 22050, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{705600, 22050, 0x01, 0x03, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
|
||||
/* 24k */
|
||||
{12288000, 24000, 0x02, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{18432000, 24000, 0x03, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{6144000, 24000, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{3072000, 24000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{1536000, 24000, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
|
||||
/* 32k */
|
||||
{12288000, 32000, 0x03, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{18432000, 32000, 0x03, 0x02, 0x03, 0x03, 0x00, 0x02, 0xff, 0x0c, 0x10, 0x10},
|
||||
{16384000, 32000, 0x02, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{8192000, 32000, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{6144000, 32000, 0x03, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{4096000, 32000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{3072000, 32000, 0x03, 0x03, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{2048000, 32000, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{1536000, 32000, 0x03, 0x03, 0x01, 0x01, 0x01, 0x00, 0x7f, 0x02, 0x10, 0x10},
|
||||
{1024000, 32000, 0x01, 0x03, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
|
||||
/* 44.1k */
|
||||
{11289600, 44100, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{5644800, 44100, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{2822400, 44100, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{1411200, 44100, 0x01, 0x03, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
|
||||
/* 48k */
|
||||
{12288000, 48000, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{18432000, 48000, 0x03, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{6144000, 48000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{3072000, 48000, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{1536000, 48000, 0x01, 0x03, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
|
||||
/* 64k */
|
||||
{12288000, 64000, 0x03, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{18432000, 64000, 0x03, 0x02, 0x03, 0x03, 0x01, 0x01, 0x7f, 0x06, 0x10, 0x10},
|
||||
{16384000, 64000, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{8192000, 64000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{6144000, 64000, 0x01, 0x02, 0x03, 0x03, 0x01, 0x01, 0x7f, 0x06, 0x10, 0x10},
|
||||
{4096000, 64000, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{3072000, 64000, 0x01, 0x03, 0x03, 0x03, 0x01, 0x01, 0x7f, 0x06, 0x10, 0x10},
|
||||
{2048000, 64000, 0x01, 0x03, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{1536000, 64000, 0x01, 0x03, 0x01, 0x01, 0x01, 0x00, 0xbf, 0x03, 0x18, 0x18},
|
||||
{1024000, 64000, 0x01, 0x03, 0x01, 0x01, 0x01, 0x00, 0x7f, 0x02, 0x10, 0x10},
|
||||
|
||||
/* 88.2k */
|
||||
{11289600, 88200, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{5644800, 88200, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{2822400, 88200, 0x01, 0x03, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{1411200, 88200, 0x01, 0x03, 0x01, 0x01, 0x01, 0x00, 0x7f, 0x02, 0x10, 0x10},
|
||||
|
||||
/* 96k */
|
||||
{12288000, 96000, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{18432000, 96000, 0x03, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{6144000, 96000, 0x01, 0x02, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{3072000, 96000, 0x01, 0x03, 0x01, 0x01, 0x00, 0x00, 0xff, 0x04, 0x10, 0x10},
|
||||
{1536000, 96000, 0x01, 0x03, 0x01, 0x01, 0x01, 0x00, 0x7f, 0x02, 0x10, 0x10},
|
||||
};
|
||||
|
||||
static const char *TAG = "ES8311";
|
||||
|
||||
static inline esp_err_t es8311_write_reg(es8311_handle_t dev, uint8_t reg_addr, uint8_t data)
|
||||
{
|
||||
es8311_dev_t *es = (es8311_dev_t *) dev;
|
||||
const uint8_t write_buf[2] = {reg_addr, data};
|
||||
return i2cWrite(es->port, es->dev_addr, write_buf, sizeof(write_buf), 1000);
|
||||
}
|
||||
|
||||
static inline esp_err_t es8311_read_reg(es8311_handle_t dev, uint8_t reg_addr, uint8_t *reg_value)
|
||||
{
|
||||
es8311_dev_t *es = (es8311_dev_t *) dev;
|
||||
size_t readCount = 0;
|
||||
return i2cWriteReadNonStop(es->port, es->dev_addr, ®_addr, 1, reg_value, 1, 1000, &readCount);
|
||||
}
|
||||
|
||||
/*
|
||||
* look for the coefficient in coeff_div[] table
|
||||
*/
|
||||
static int get_coeff(uint32_t mclk, uint32_t rate)
|
||||
{
|
||||
for (int i = 0; i < (sizeof(coeff_div) / sizeof(coeff_div[0])); i++) {
|
||||
if (coeff_div[i].rate == rate && coeff_div[i].mclk == mclk) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
esp_err_t es8311_sample_frequency_config(es8311_handle_t dev, int mclk_frequency, int sample_frequency)
|
||||
{
|
||||
uint8_t regv;
|
||||
|
||||
/* Get clock coefficients from coefficient table */
|
||||
int coeff = get_coeff(mclk_frequency, sample_frequency);
|
||||
|
||||
if (coeff < 0) {
|
||||
ESP_LOGE(TAG, "Unable to configure sample rate %dHz with %dHz MCLK", sample_frequency, mclk_frequency);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
const struct _coeff_div *const selected_coeff = &coeff_div[coeff];
|
||||
|
||||
/* register 0x02 */
|
||||
ESP_RETURN_ON_ERROR(es8311_read_reg(dev, ES8311_CLK_MANAGER_REG02, ®v), TAG, "I2C read/write error");
|
||||
regv &= 0x07;
|
||||
regv |= (selected_coeff->pre_div - 1) << 5;
|
||||
regv |= selected_coeff->pre_multi << 3;
|
||||
ESP_RETURN_ON_ERROR(es8311_write_reg(dev, ES8311_CLK_MANAGER_REG02, regv), TAG, "I2C read/write error");
|
||||
|
||||
/* register 0x03 */
|
||||
const uint8_t reg03 = (selected_coeff->fs_mode << 6) | selected_coeff->adc_osr;
|
||||
ESP_RETURN_ON_ERROR(es8311_write_reg(dev, ES8311_CLK_MANAGER_REG03, reg03), TAG, "I2C read/write error");
|
||||
|
||||
/* register 0x04 */
|
||||
ESP_RETURN_ON_ERROR(es8311_write_reg(dev, ES8311_CLK_MANAGER_REG04, selected_coeff->dac_osr), TAG, "I2C read/write error");
|
||||
|
||||
/* register 0x05 */
|
||||
const uint8_t reg05 = ((selected_coeff->adc_div - 1) << 4) | (selected_coeff->dac_div - 1);
|
||||
ESP_RETURN_ON_ERROR(es8311_write_reg(dev, ES8311_CLK_MANAGER_REG05, reg05), TAG, "I2C read/write error");
|
||||
|
||||
/* register 0x06 */
|
||||
ESP_RETURN_ON_ERROR(es8311_read_reg(dev, ES8311_CLK_MANAGER_REG06, ®v), TAG, "I2C read/write error");
|
||||
regv &= 0xE0;
|
||||
|
||||
if (selected_coeff->bclk_div < 19) {
|
||||
regv |= (selected_coeff->bclk_div - 1) << 0;
|
||||
} else {
|
||||
regv |= (selected_coeff->bclk_div) << 0;
|
||||
}
|
||||
|
||||
ESP_RETURN_ON_ERROR(es8311_write_reg(dev, ES8311_CLK_MANAGER_REG06, regv), TAG, "I2C read/write error");
|
||||
|
||||
/* register 0x07 */
|
||||
ESP_RETURN_ON_ERROR(es8311_read_reg(dev, ES8311_CLK_MANAGER_REG07, ®v), TAG, "I2C read/write error");
|
||||
regv &= 0xC0;
|
||||
regv |= selected_coeff->lrck_h << 0;
|
||||
ESP_RETURN_ON_ERROR(es8311_write_reg(dev, ES8311_CLK_MANAGER_REG07, regv), TAG, "I2C read/write error");
|
||||
|
||||
/* register 0x08 */
|
||||
ESP_RETURN_ON_ERROR(es8311_write_reg(dev, ES8311_CLK_MANAGER_REG08, selected_coeff->lrck_l), TAG, "I2C read/write error");
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t es8311_clock_config(es8311_handle_t dev, const es8311_clock_config_t *const clk_cfg, es8311_resolution_t res)
|
||||
{
|
||||
uint8_t reg06;
|
||||
uint8_t reg01 = 0x3F; // Enable all clocks
|
||||
int mclk_hz;
|
||||
|
||||
/* Select clock source for internal MCLK and determine its frequency */
|
||||
if (clk_cfg->mclk_from_mclk_pin) {
|
||||
mclk_hz = clk_cfg->mclk_frequency;
|
||||
} else {
|
||||
mclk_hz = clk_cfg->sample_frequency * (int)res * 2;
|
||||
reg01 |= BIT(7); // Select BCLK (a.k.a. SCK) pin
|
||||
}
|
||||
|
||||
if (clk_cfg->mclk_inverted) {
|
||||
reg01 |= BIT(6); // Invert MCLK pin
|
||||
}
|
||||
ESP_RETURN_ON_ERROR(es8311_write_reg(dev, ES8311_CLK_MANAGER_REG01, reg01), TAG, "I2C read/write error");
|
||||
|
||||
ESP_RETURN_ON_ERROR(es8311_read_reg(dev, ES8311_CLK_MANAGER_REG06, ®06), TAG, "I2C read/write error");
|
||||
if (clk_cfg->sclk_inverted) {
|
||||
reg06 |= BIT(5);
|
||||
} else {
|
||||
reg06 &= ~BIT(5);
|
||||
}
|
||||
ESP_RETURN_ON_ERROR(es8311_write_reg(dev, ES8311_CLK_MANAGER_REG06, reg06), TAG, "I2C read/write error");
|
||||
|
||||
/* Configure clock dividers */
|
||||
return es8311_sample_frequency_config(dev, mclk_hz, clk_cfg->sample_frequency);
|
||||
}
|
||||
|
||||
static esp_err_t es8311_resolution_config(const es8311_resolution_t res, uint8_t *reg)
|
||||
{
|
||||
switch (res) {
|
||||
case ES8311_RESOLUTION_16:
|
||||
*reg |= (3 << 2);
|
||||
break;
|
||||
case ES8311_RESOLUTION_18:
|
||||
*reg |= (2 << 2);
|
||||
break;
|
||||
case ES8311_RESOLUTION_20:
|
||||
*reg |= (1 << 2);
|
||||
break;
|
||||
case ES8311_RESOLUTION_24:
|
||||
*reg |= (0 << 2);
|
||||
break;
|
||||
case ES8311_RESOLUTION_32:
|
||||
*reg |= (4 << 2);
|
||||
break;
|
||||
default:
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t es8311_fmt_config(es8311_handle_t dev, const es8311_resolution_t res_in, const es8311_resolution_t res_out)
|
||||
{
|
||||
uint8_t reg09 = 0; // SDP In
|
||||
uint8_t reg0a = 0; // SDP Out
|
||||
|
||||
ESP_LOGI(TAG, "ES8311 in Slave mode and I2S format");
|
||||
uint8_t reg00;
|
||||
ESP_RETURN_ON_ERROR(es8311_read_reg(dev, ES8311_RESET_REG00, ®00), TAG, "I2C read/write error");
|
||||
reg00 &= 0xBF;
|
||||
ESP_RETURN_ON_ERROR(es8311_write_reg(dev, ES8311_RESET_REG00, reg00), TAG, "I2C read/write error"); // Slave serial port - default
|
||||
|
||||
/* Setup SDP In and Out resolution */
|
||||
es8311_resolution_config(res_in, ®09);
|
||||
es8311_resolution_config(res_out, ®0a);
|
||||
|
||||
ESP_RETURN_ON_ERROR(es8311_write_reg(dev, ES8311_SDPIN_REG09, reg09), TAG, "I2C read/write error");
|
||||
ESP_RETURN_ON_ERROR(es8311_write_reg(dev, ES8311_SDPOUT_REG0A, reg0a), TAG, "I2C read/write error");
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t es8311_microphone_config(es8311_handle_t dev, bool digital_mic)
|
||||
{
|
||||
uint8_t reg14 = 0x1A; // enable analog MIC and max PGA gain
|
||||
|
||||
/* PDM digital microphone enable or disable */
|
||||
if (digital_mic) {
|
||||
reg14 |= BIT(6);
|
||||
}
|
||||
es8311_write_reg(dev, ES8311_ADC_REG17, 0xC8); // Set ADC gain @todo move this to ADC config section
|
||||
|
||||
return es8311_write_reg(dev, ES8311_SYSTEM_REG14, reg14);
|
||||
}
|
||||
|
||||
esp_err_t es8311_init(es8311_handle_t dev, const es8311_clock_config_t *const clk_cfg, const es8311_resolution_t res_in, const es8311_resolution_t res_out)
|
||||
{
|
||||
ESP_RETURN_ON_FALSE(
|
||||
(clk_cfg->sample_frequency >= 8000) && (clk_cfg->sample_frequency <= 96000),
|
||||
ESP_ERR_INVALID_ARG, TAG, "ES8311 init needs frequency in interval [8000; 96000] Hz"
|
||||
);
|
||||
if (!clk_cfg->mclk_from_mclk_pin) {
|
||||
ESP_RETURN_ON_FALSE(res_out == res_in, ESP_ERR_INVALID_ARG, TAG, "Resolution IN/OUT must be equal if MCLK is taken from SCK pin");
|
||||
}
|
||||
|
||||
|
||||
/* Reset ES8311 to its default */
|
||||
ESP_RETURN_ON_ERROR(es8311_write_reg(dev, ES8311_RESET_REG00, 0x1F), TAG, "I2C read/write error");
|
||||
ESP_RETURN_ON_ERROR(es8311_write_reg(dev, ES8311_RESET_REG00, 0x00), TAG, "I2C read/write error");
|
||||
ESP_RETURN_ON_ERROR(es8311_write_reg(dev, ES8311_RESET_REG00, 0x80), TAG, "I2C read/write error"); // Power-on command
|
||||
|
||||
/* Setup clock: source, polarity and clock dividers */
|
||||
ESP_RETURN_ON_ERROR(es8311_clock_config(dev, clk_cfg, res_out), TAG, "");
|
||||
|
||||
/* Setup audio format (fmt): master/slave, resolution, I2S */
|
||||
ESP_RETURN_ON_ERROR(es8311_fmt_config(dev, res_in, res_out), TAG, "");
|
||||
|
||||
ESP_RETURN_ON_ERROR(es8311_write_reg(dev, ES8311_SYSTEM_REG0D, 0x01), TAG, "I2C read/write error"); // Power up analog circuitry - NOT default
|
||||
ESP_RETURN_ON_ERROR(es8311_write_reg(dev, ES8311_SYSTEM_REG0E, 0x02), TAG, "I2C read/write error"); // Enable analog PGA, enable ADC modulator - NOT default
|
||||
ESP_RETURN_ON_ERROR(es8311_write_reg(dev, ES8311_SYSTEM_REG12, 0x00), TAG, "I2C read/write error"); // power-up DAC - NOT default
|
||||
ESP_RETURN_ON_ERROR(es8311_write_reg(dev, ES8311_SYSTEM_REG13, 0x10), TAG, "I2C read/write error"); // Enable output to HP drive - NOT default
|
||||
ESP_RETURN_ON_ERROR(es8311_write_reg(dev, ES8311_ADC_REG1C, 0x6A), TAG, "I2C read/write error"); // ADC Equalizer bypass, cancel DC offset in digital domain
|
||||
ESP_RETURN_ON_ERROR(es8311_write_reg(dev, ES8311_DAC_REG37, 0x08), TAG, "I2C read/write error"); // Bypass DAC equalizer - NOT default
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void es8311_delete(es8311_handle_t dev)
|
||||
{
|
||||
free(dev);
|
||||
}
|
||||
|
||||
esp_err_t es8311_voice_volume_set(es8311_handle_t dev, int volume, int *volume_set)
|
||||
{
|
||||
if (volume < 0) {
|
||||
volume = 0;
|
||||
} else if (volume > 100) {
|
||||
volume = 100;
|
||||
}
|
||||
|
||||
int reg32;
|
||||
if (volume == 0) {
|
||||
reg32 = 0;
|
||||
} else {
|
||||
reg32 = ((volume) * 256 / 100) - 1;
|
||||
}
|
||||
|
||||
// provide user with real volume set
|
||||
if (volume_set != NULL) {
|
||||
*volume_set = volume;
|
||||
}
|
||||
return es8311_write_reg(dev, ES8311_DAC_REG32, reg32);
|
||||
}
|
||||
|
||||
esp_err_t es8311_voice_volume_get(es8311_handle_t dev, int *volume)
|
||||
{
|
||||
uint8_t reg32;
|
||||
ESP_RETURN_ON_ERROR(es8311_read_reg(dev, ES8311_DAC_REG32, ®32), TAG, "I2C read/write error");
|
||||
|
||||
if (reg32 == 0) {
|
||||
*volume = 0;
|
||||
} else {
|
||||
*volume = ((reg32 * 100) / 256) + 1;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t es8311_voice_mute(es8311_handle_t dev, bool mute)
|
||||
{
|
||||
uint8_t reg31;
|
||||
ESP_RETURN_ON_ERROR(es8311_read_reg(dev, ES8311_DAC_REG31, ®31), TAG, "I2C read/write error");
|
||||
|
||||
if (mute) {
|
||||
reg31 |= BIT(6) | BIT(5);
|
||||
} else {
|
||||
reg31 &= ~(BIT(6) | BIT(5));
|
||||
}
|
||||
|
||||
return es8311_write_reg(dev, ES8311_DAC_REG31, reg31);
|
||||
}
|
||||
|
||||
esp_err_t es8311_microphone_gain_set(es8311_handle_t dev, es8311_mic_gain_t gain_db)
|
||||
{
|
||||
return es8311_write_reg(dev, ES8311_ADC_REG16, gain_db); // ADC gain scale up
|
||||
}
|
||||
|
||||
esp_err_t es8311_voice_fade(es8311_handle_t dev, const es8311_fade_t fade)
|
||||
{
|
||||
uint8_t reg37;
|
||||
ESP_RETURN_ON_ERROR(es8311_read_reg(dev, ES8311_DAC_REG37, ®37), TAG, "I2C read/write error");
|
||||
reg37 &= 0x0F;
|
||||
reg37 |= (fade << 4);
|
||||
return es8311_write_reg(dev, ES8311_DAC_REG37, reg37);
|
||||
}
|
||||
|
||||
esp_err_t es8311_microphone_fade(es8311_handle_t dev, const es8311_fade_t fade)
|
||||
{
|
||||
uint8_t reg15;
|
||||
ESP_RETURN_ON_ERROR(es8311_read_reg(dev, ES8311_ADC_REG15, ®15), TAG, "I2C read/write error");
|
||||
reg15 &= 0x0F;
|
||||
reg15 |= (fade << 4);
|
||||
return es8311_write_reg(dev, ES8311_ADC_REG15, reg15);
|
||||
}
|
||||
|
||||
void es8311_register_dump(es8311_handle_t dev)
|
||||
{
|
||||
for (int reg = 0; reg < 0x4A; reg++) {
|
||||
uint8_t value;
|
||||
ESP_ERROR_CHECK(es8311_read_reg(dev, reg, &value));
|
||||
printf("REG:%02x: %02x", reg, value);
|
||||
}
|
||||
}
|
||||
|
||||
es8311_handle_t es8311_create(const unsigned int port, const uint16_t dev_addr)
|
||||
{
|
||||
es8311_dev_t *sensor = (es8311_dev_t *) calloc(1, sizeof(es8311_dev_t));
|
||||
sensor->port = port;
|
||||
sensor->dev_addr = dev_addr;
|
||||
return (es8311_handle_t) sensor;
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2015-2022 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file
|
||||
* @brief ES8311 driver
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "esp_types.h"
|
||||
#include "esp_err.h"
|
||||
|
||||
/* ES8311 address: CE pin low - 0x18, CE pin high - 0x19 */
|
||||
#define ES8311_ADDRRES_0 0x18u // Leaving this here for backward compatibility
|
||||
#define ES8311_ADDRESS_0 0x18u
|
||||
#define ES8311_ADDRESS_1 0x19u
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef void *es8311_handle_t;
|
||||
|
||||
typedef enum {
|
||||
ES8311_MIC_GAIN_MIN = -1,
|
||||
ES8311_MIC_GAIN_0DB,
|
||||
ES8311_MIC_GAIN_6DB,
|
||||
ES8311_MIC_GAIN_12DB,
|
||||
ES8311_MIC_GAIN_18DB,
|
||||
ES8311_MIC_GAIN_24DB,
|
||||
ES8311_MIC_GAIN_30DB,
|
||||
ES8311_MIC_GAIN_36DB,
|
||||
ES8311_MIC_GAIN_42DB,
|
||||
ES8311_MIC_GAIN_MAX
|
||||
} es8311_mic_gain_t;
|
||||
|
||||
typedef enum {
|
||||
ES8311_FADE_OFF = 0,
|
||||
ES8311_FADE_4LRCK, // 4LRCK means ramp 0.25dB/4LRCK
|
||||
ES8311_FADE_8LRCK,
|
||||
ES8311_FADE_16LRCK,
|
||||
ES8311_FADE_32LRCK,
|
||||
ES8311_FADE_64LRCK,
|
||||
ES8311_FADE_128LRCK,
|
||||
ES8311_FADE_256LRCK,
|
||||
ES8311_FADE_512LRCK,
|
||||
ES8311_FADE_1024LRCK,
|
||||
ES8311_FADE_2048LRCK,
|
||||
ES8311_FADE_4096LRCK,
|
||||
ES8311_FADE_8192LRCK,
|
||||
ES8311_FADE_16384LRCK,
|
||||
ES8311_FADE_32768LRCK,
|
||||
ES8311_FADE_65536LRCK
|
||||
} es8311_fade_t;
|
||||
|
||||
typedef enum es8311_resolution_t {
|
||||
ES8311_RESOLUTION_16 = 16,
|
||||
ES8311_RESOLUTION_18 = 18,
|
||||
ES8311_RESOLUTION_20 = 20,
|
||||
ES8311_RESOLUTION_24 = 24,
|
||||
ES8311_RESOLUTION_32 = 32
|
||||
} es8311_resolution_t;
|
||||
|
||||
typedef struct es8311_clock_config_t {
|
||||
bool mclk_inverted;
|
||||
bool sclk_inverted;
|
||||
bool mclk_from_mclk_pin; // true: from MCLK pin (pin no. 2), false: from SCLK pin (pin no. 6)
|
||||
int mclk_frequency; // This parameter is ignored if MCLK is taken from SCLK pin
|
||||
int sample_frequency; // in Hz
|
||||
} es8311_clock_config_t;
|
||||
|
||||
/**
|
||||
* @brief Initialize ES8311
|
||||
*
|
||||
* There are two ways of providing Master Clock (MCLK) signal to ES8311 in Slave Mode:
|
||||
* 1. From MCLK pin:
|
||||
* For flexible scenarios. A clock signal from I2S master is routed to MCLK pin.
|
||||
* Its frequency must be defined in clk_cfg->mclk_frequency parameter.
|
||||
* 2. From SCLK pin:
|
||||
* For simpler scenarios. ES8311 takes its clock from SCK pin. MCLK pin does not have to be connected.
|
||||
* In this case, res_in must equal res_out; clk_cfg->mclk_frequency parameter is ignored
|
||||
* and MCLK is calculated as MCLK = clk_cfg->sample_frequency * res_out * 2.
|
||||
* Not all sampling frequencies are supported in this mode.
|
||||
*
|
||||
* @param dev ES8311 handle
|
||||
* @param[in] clk_cfg Clock configuration
|
||||
* @param[in] res_in Input serial port resolution
|
||||
* @param[in] res_out Output serial port resolution
|
||||
* @return
|
||||
* - ESP_OK success
|
||||
* - ESP_ERR_INVALID_ARG Sample frequency or resolution invalid
|
||||
* - Else fail
|
||||
*/
|
||||
esp_err_t es8311_init(es8311_handle_t dev, const es8311_clock_config_t *const clk_cfg, const es8311_resolution_t res_in,
|
||||
const es8311_resolution_t res_out);
|
||||
|
||||
/**
|
||||
* @brief Set output volume
|
||||
*
|
||||
* Volume paramter out of <0, 100> interval will be truncated.
|
||||
*
|
||||
* @param dev ES8311 handle
|
||||
* @param[in] volume Set volume (0 ~ 100)
|
||||
* @param[out] volume_set Volume that was set. Same as volume, unless volume is outside of <0, 100> interval.
|
||||
* This parameter can be set to NULL, if user does not need this information.
|
||||
*
|
||||
* @return
|
||||
* - ESP_OK success
|
||||
* - Else fail
|
||||
*/
|
||||
esp_err_t es8311_voice_volume_set(es8311_handle_t dev, int volume, int *volume_set);
|
||||
|
||||
/**
|
||||
* @brief Get output volume
|
||||
*
|
||||
* @param dev ES8311 handle
|
||||
* @param[out] volume get volume (0 ~ 100)
|
||||
*
|
||||
* @return
|
||||
* - ESP_OK success
|
||||
* - Else fail
|
||||
*/
|
||||
esp_err_t es8311_voice_volume_get(es8311_handle_t dev, int *volume);
|
||||
|
||||
/**
|
||||
* @brief Print out ES8311 register content
|
||||
*
|
||||
* @param dev ES8311 handle
|
||||
*/
|
||||
void es8311_register_dump(es8311_handle_t dev);
|
||||
|
||||
/**
|
||||
* @brief Mute ES8311 output
|
||||
*
|
||||
* @param dev ES8311 handle
|
||||
* @param[in] enable true: mute, false: don't mute
|
||||
* @return
|
||||
* - ESP_OK success
|
||||
* - Else fail
|
||||
*/
|
||||
esp_err_t es8311_voice_mute(es8311_handle_t dev, bool enable);
|
||||
|
||||
/**
|
||||
* @brief Set Microphone gain
|
||||
*
|
||||
* @param dev ES8311 handle
|
||||
* @param[in] gain_db Microphone gain
|
||||
* @return
|
||||
* - ESP_OK success
|
||||
* - Else fail
|
||||
*/
|
||||
esp_err_t es8311_microphone_gain_set(es8311_handle_t dev, es8311_mic_gain_t gain_db);
|
||||
|
||||
/**
|
||||
* @brief Configure microphone
|
||||
*
|
||||
* @param dev ES8311 handle
|
||||
* @param[in] digital_mic Set to true for digital microphone
|
||||
* @return
|
||||
* - ESP_OK success
|
||||
* - Else fail
|
||||
*/
|
||||
esp_err_t es8311_microphone_config(es8311_handle_t dev, bool digital_mic);
|
||||
|
||||
/**
|
||||
* @brief Configure sampling frequency
|
||||
*
|
||||
* @note This function is called by es8311_init().
|
||||
* Call this function explicitly only if you want to change sample frequency during runtime.
|
||||
* @param dev ES8311 handle
|
||||
* @param[in] mclk_frequency MCLK frequency in [Hz] (MCLK or SCLK pin, depending on bit register01[7])
|
||||
* @param[in] sample_frequency Required sample frequency in [Hz], e.g. 44100, 22050...
|
||||
* @return
|
||||
* - ESP_OK success
|
||||
* - ESP_ERR_INVALID_ARG cannot set clock dividers for given MCLK and sampling frequency
|
||||
* - Else I2C read/write error
|
||||
*/
|
||||
esp_err_t es8311_sample_frequency_config(es8311_handle_t dev, int mclk_frequency, int sample_frequency);
|
||||
|
||||
/**
|
||||
* @brief Configure fade in/out for ADC: voice
|
||||
*
|
||||
* @param dev ES8311 handle
|
||||
* @param[in] fade Fade ramp rate
|
||||
* @return
|
||||
* - ESP_OK success
|
||||
* - Else I2C read/write error
|
||||
*/
|
||||
esp_err_t es8311_voice_fade(es8311_handle_t dev, const es8311_fade_t fade);
|
||||
|
||||
/**
|
||||
* @brief Configure fade in/out for DAC: microphone
|
||||
*
|
||||
* @param dev ES8311 handle
|
||||
* @param[in] fade Fade ramp rate
|
||||
* @return
|
||||
* - ESP_OK success
|
||||
* - Else I2C read/write error
|
||||
*/
|
||||
esp_err_t es8311_microphone_fade(es8311_handle_t dev, const es8311_fade_t fade);
|
||||
|
||||
/**
|
||||
* @brief Create ES8311 object and return its handle
|
||||
*
|
||||
* @param[in] port I2C port number
|
||||
* @param[in] dev_addr I2C device address of ES8311
|
||||
*
|
||||
* @return
|
||||
* - NULL Fail
|
||||
* - Others Success
|
||||
*/
|
||||
es8311_handle_t es8311_create(const unsigned int port, const uint16_t dev_addr);
|
||||
|
||||
/**
|
||||
* @brief Delete ES8311 object
|
||||
*
|
||||
* @param dev ES8311 handle
|
||||
*/
|
||||
void es8311_delete(es8311_handle_t dev);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2015-2021 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
/*
|
||||
* ES8311_REGISTER NAME_REG_REGISTER ADDRESS
|
||||
*/
|
||||
#define ES8311_RESET_REG00 0x00 /*reset digital,csm,clock manager etc.*/
|
||||
|
||||
/*
|
||||
* Clock Scheme Register definition
|
||||
*/
|
||||
#define ES8311_CLK_MANAGER_REG01 0x01 /* select clk src for mclk, enable clock for codec */
|
||||
#define ES8311_CLK_MANAGER_REG02 0x02 /* clk divider and clk multiplier */
|
||||
#define ES8311_CLK_MANAGER_REG03 0x03 /* adc fsmode and osr */
|
||||
#define ES8311_CLK_MANAGER_REG04 0x04 /* dac osr */
|
||||
#define ES8311_CLK_MANAGER_REG05 0x05 /* clk divier for adc and dac */
|
||||
#define ES8311_CLK_MANAGER_REG06 0x06 /* bclk inverter and divider */
|
||||
#define ES8311_CLK_MANAGER_REG07 0x07 /* tri-state, lrck divider */
|
||||
#define ES8311_CLK_MANAGER_REG08 0x08 /* lrck divider */
|
||||
/*
|
||||
* SDP
|
||||
*/
|
||||
#define ES8311_SDPIN_REG09 0x09 /* dac serial digital port */
|
||||
#define ES8311_SDPOUT_REG0A 0x0A /* adc serial digital port */
|
||||
/*
|
||||
* SYSTEM
|
||||
*/
|
||||
#define ES8311_SYSTEM_REG0B 0x0B /* system */
|
||||
#define ES8311_SYSTEM_REG0C 0x0C /* system */
|
||||
#define ES8311_SYSTEM_REG0D 0x0D /* system, power up/down */
|
||||
#define ES8311_SYSTEM_REG0E 0x0E /* system, power up/down */
|
||||
#define ES8311_SYSTEM_REG0F 0x0F /* system, low power */
|
||||
#define ES8311_SYSTEM_REG10 0x10 /* system */
|
||||
#define ES8311_SYSTEM_REG11 0x11 /* system */
|
||||
#define ES8311_SYSTEM_REG12 0x12 /* system, Enable DAC */
|
||||
#define ES8311_SYSTEM_REG13 0x13 /* system */
|
||||
#define ES8311_SYSTEM_REG14 0x14 /* system, select DMIC, select analog pga gain */
|
||||
/*
|
||||
* ADC
|
||||
*/
|
||||
#define ES8311_ADC_REG15 0x15 /* ADC, adc ramp rate, dmic sense */
|
||||
#define ES8311_ADC_REG16 0x16 /* ADC */
|
||||
#define ES8311_ADC_REG17 0x17 /* ADC, volume */
|
||||
#define ES8311_ADC_REG18 0x18 /* ADC, alc enable and winsize */
|
||||
#define ES8311_ADC_REG19 0x19 /* ADC, alc maxlevel */
|
||||
#define ES8311_ADC_REG1A 0x1A /* ADC, alc automute */
|
||||
#define ES8311_ADC_REG1B 0x1B /* ADC, alc automute, adc hpf s1 */
|
||||
#define ES8311_ADC_REG1C 0x1C /* ADC, equalizer, hpf s2 */
|
||||
/*
|
||||
* DAC
|
||||
*/
|
||||
#define ES8311_DAC_REG31 0x31 /* DAC, mute */
|
||||
#define ES8311_DAC_REG32 0x32 /* DAC, volume */
|
||||
#define ES8311_DAC_REG33 0x33 /* DAC, offset */
|
||||
#define ES8311_DAC_REG34 0x34 /* DAC, drc enable, drc winsize */
|
||||
#define ES8311_DAC_REG35 0x35 /* DAC, drc maxlevel, minilevel */
|
||||
#define ES8311_DAC_REG37 0x37 /* DAC, ramprate */
|
||||
/*
|
||||
*GPIO
|
||||
*/
|
||||
#define ES8311_GPIO_REG44 0x44 /* GPIO, dac2adc for test */
|
||||
#define ES8311_GP_REG45 0x45 /* GP CONTROL */
|
||||
/*
|
||||
* CHIP
|
||||
*/
|
||||
#define ES8311_CHD1_REGFD 0xFD /* CHIP ID1 */
|
||||
#define ES8311_CHD2_REGFE 0xFE /* CHIP ID2 */
|
||||
#define ES8311_CHVER_REGFF 0xFF /* VERSION */
|
||||
#define ES8311_CHD1_REGFD 0xFD /* CHIP ID1 */
|
||||
|
||||
#define ES8311_MAX_REGISTER 0xFF
|
||||
@@ -0,0 +1,272 @@
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
#include <math.h>
|
||||
#include "ESP_I2S.h"
|
||||
#include "esp_check.h"
|
||||
#include "Arduino_GFX_Library.h"
|
||||
#include "pin_config.h"
|
||||
#include "TouchDrvCSTXXX.hpp"
|
||||
#include "SensorQMI8658.hpp"
|
||||
#include "es8311.h"
|
||||
#include "es7210.h"
|
||||
|
||||
static const int DISP_W = 480;
|
||||
static const int DISP_H = 480;
|
||||
static const int SAFE = 20;
|
||||
static const uint32_t SAMPLE_RATE = 16000;
|
||||
static const size_t REC_SECONDS = 2;
|
||||
static const size_t AUDIO_BYTES = SAMPLE_RATE * REC_SECONDS * 2 * 2;
|
||||
static const int I2C_NUM = 0;
|
||||
|
||||
I2SClass i2s;
|
||||
TouchDrvCST92xx touch;
|
||||
SensorQMI8658 qmi;
|
||||
IMUdata acc;
|
||||
|
||||
Arduino_DataBus *bus = new Arduino_ESP32QSPI(LCD_CS, LCD_SCLK, LCD_SDIO0, LCD_SDIO1, LCD_SDIO2, LCD_SDIO3);
|
||||
Arduino_CO5300 *gfx = new Arduino_CO5300(bus, LCD_RESET, 0, DISP_W, DISP_H, 0, 0, 0, 0);
|
||||
|
||||
uint8_t *audioBuf = nullptr;
|
||||
size_t audioLen = 0;
|
||||
bool touchIrq = false;
|
||||
String statusLine = "Ready";
|
||||
String tiltLine = "Tilt: FLAT";
|
||||
String audioLine = "Audio: empty";
|
||||
uint32_t lastTiltMs = 0;
|
||||
bool micReady = false;
|
||||
|
||||
void IRAM_ATTR onTouchInt() { touchIrq = true; }
|
||||
|
||||
esp_err_t codecInit() {
|
||||
es8311_handle_t esHandle = es8311_create(I2C_NUM, ES8311_ADDRRES_0);
|
||||
ESP_RETURN_ON_FALSE(esHandle, ESP_FAIL, "AV", "es8311 create failed");
|
||||
const es8311_clock_config_t clk = {
|
||||
.mclk_inverted = false,
|
||||
.sclk_inverted = false,
|
||||
.mclk_from_mclk_pin = true,
|
||||
.mclk_frequency = SAMPLE_RATE * 256,
|
||||
.sample_frequency = SAMPLE_RATE,
|
||||
};
|
||||
ESP_ERROR_CHECK(es8311_init(esHandle, &clk, ES8311_RESOLUTION_16, ES8311_RESOLUTION_16));
|
||||
ESP_RETURN_ON_ERROR(es8311_sample_frequency_config(esHandle, clk.mclk_frequency, clk.sample_frequency), "AV", "clock cfg failed");
|
||||
ESP_RETURN_ON_ERROR(es8311_microphone_config(esHandle, false), "AV", "mic cfg failed");
|
||||
ESP_RETURN_ON_ERROR(es8311_voice_volume_set(esHandle, 90, NULL), "AV", "volume failed");
|
||||
ESP_RETURN_ON_ERROR(es8311_microphone_gain_set(esHandle, (es8311_mic_gain_t)6), "AV", "mic gain failed");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t micInit() {
|
||||
audio_hal_codec_config_t cfg = {
|
||||
.adc_input = AUDIO_HAL_ADC_INPUT_ALL,
|
||||
.codec_mode = AUDIO_HAL_CODEC_MODE_ENCODE,
|
||||
.i2s_iface = {
|
||||
.mode = AUDIO_HAL_MODE_SLAVE,
|
||||
.fmt = AUDIO_HAL_I2S_NORMAL,
|
||||
.samples = AUDIO_HAL_16K_SAMPLES,
|
||||
.bits = AUDIO_HAL_BIT_LENGTH_16BITS,
|
||||
},
|
||||
};
|
||||
esp_err_t ret = ESP_OK;
|
||||
ret |= es7210_adc_init(&Wire, &cfg);
|
||||
ret |= es7210_adc_config_i2s(cfg.codec_mode, &cfg.i2s_iface);
|
||||
ret |= es7210_adc_set_gain(
|
||||
(es7210_input_mics_t)(ES7210_INPUT_MIC1 | ES7210_INPUT_MIC2),
|
||||
(es7210_gain_value_t)GAIN_24DB);
|
||||
ret |= es7210_adc_set_gain(
|
||||
(es7210_input_mics_t)(ES7210_INPUT_MIC3 | ES7210_INPUT_MIC4),
|
||||
(es7210_gain_value_t)GAIN_24DB);
|
||||
ret |= es7210_adc_ctrl_state(cfg.codec_mode, AUDIO_HAL_CTRL_START);
|
||||
return ret;
|
||||
}
|
||||
|
||||
void drawStaticUi() {
|
||||
gfx->fillScreen(RGB565_BLACK);
|
||||
gfx->setTextSize(2);
|
||||
gfx->setTextColor(RGB565_WHITE);
|
||||
gfx->setCursor(SAFE + 16, SAFE + 10);
|
||||
gfx->println("Simple AV + Tilt Test");
|
||||
|
||||
gfx->drawRect(24, 76, 132, 64, RGB565_GREEN);
|
||||
gfx->setCursor(44, 100);
|
||||
gfx->println("RECORD");
|
||||
|
||||
gfx->drawRect(174, 76, 132, 64, RGB565_YELLOW);
|
||||
gfx->setCursor(212, 100);
|
||||
gfx->println("PLAY");
|
||||
|
||||
gfx->drawRect(324, 76, 132, 64, RGB565_CYAN);
|
||||
gfx->setCursor(360, 100);
|
||||
gfx->println("BEEP");
|
||||
}
|
||||
|
||||
void drawDynamic() {
|
||||
gfx->fillRect(20, 166, 440, 140, RGB565_BLACK);
|
||||
gfx->setTextSize(2);
|
||||
gfx->setTextColor(RGB565_WHITE);
|
||||
gfx->setCursor(28, 178);
|
||||
gfx->println(statusLine);
|
||||
gfx->setTextColor(RGB565_CYAN);
|
||||
gfx->setCursor(28, 212);
|
||||
gfx->println(tiltLine);
|
||||
gfx->setTextColor(RGB565_ORANGE);
|
||||
gfx->setCursor(28, 246);
|
||||
gfx->println(audioLine);
|
||||
gfx->drawRect(28, 278, 300, 20, RGB565_DARKGREY);
|
||||
}
|
||||
|
||||
void updateTilt() {
|
||||
if (!qmi.getDataReady()) return;
|
||||
if (!qmi.getAccelerometer(acc.x, acc.y, acc.z)) return;
|
||||
String next = "Tilt: FLAT";
|
||||
if (acc.x > 0.8f) next = "Tilt: RIGHT";
|
||||
else if (acc.x < -0.8f) next = "Tilt: LEFT";
|
||||
else if (acc.y > 0.8f) next = "Tilt: UP";
|
||||
else if (acc.y < -0.8f) next = "Tilt: DOWN";
|
||||
if (next != tiltLine) {
|
||||
tiltLine = next;
|
||||
drawDynamic();
|
||||
}
|
||||
}
|
||||
|
||||
void playBeep() {
|
||||
static int16_t tone[16000];
|
||||
for (int i = 0; i < 8000; ++i) {
|
||||
float s = sinf(2.0f * 3.1415926f * 440.0f * ((float)i / SAMPLE_RATE));
|
||||
int16_t v = (int16_t)(s * 6000.0f);
|
||||
tone[i * 2] = v;
|
||||
tone[i * 2 + 1] = v;
|
||||
}
|
||||
statusLine = "Playing test beep...";
|
||||
drawDynamic();
|
||||
i2s.write((uint8_t *)tone, 8000 * 2 * sizeof(int16_t));
|
||||
statusLine = "Beep done";
|
||||
drawDynamic();
|
||||
}
|
||||
|
||||
void recordAudio() {
|
||||
if (!audioBuf || !micReady) {
|
||||
statusLine = "No audio buffer";
|
||||
drawDynamic();
|
||||
return;
|
||||
}
|
||||
statusLine = "Recording 2 sec...";
|
||||
drawDynamic();
|
||||
size_t total = 0;
|
||||
uint16_t peakAll = 0;
|
||||
while (total < AUDIO_BYTES) {
|
||||
size_t chunk = min((size_t)4096, AUDIO_BYTES - total);
|
||||
size_t got = i2s.readBytes((char *)(audioBuf + total), chunk);
|
||||
if (!got) break;
|
||||
total += got;
|
||||
|
||||
int16_t *s = (int16_t *)(audioBuf + total - got);
|
||||
size_t n = got / sizeof(int16_t);
|
||||
uint16_t peak = 0;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
uint16_t a = (uint16_t)abs((int)s[i]);
|
||||
if (a > peak) peak = a;
|
||||
}
|
||||
if (peak > peakAll) peakAll = peak;
|
||||
|
||||
int bar = map((int)peak, 0, 20000, 0, 296);
|
||||
if (bar < 0) bar = 0;
|
||||
if (bar > 296) bar = 296;
|
||||
gfx->fillRect(30, 280, 296, 16, RGB565_BLACK);
|
||||
uint16_t c = (peak > 12000) ? RGB565_RED : ((peak > 6000) ? RGB565_YELLOW : RGB565_GREEN);
|
||||
gfx->fillRect(30, 280, bar, 16, c);
|
||||
}
|
||||
audioLen = total;
|
||||
audioLine = (total > 0) ? "Audio: recorded" : "Audio: empty";
|
||||
if (total > 0) {
|
||||
statusLine = String("Record done, peak=") + String((int)peakAll);
|
||||
} else {
|
||||
statusLine = "Record failed";
|
||||
}
|
||||
drawDynamic();
|
||||
}
|
||||
|
||||
void playAudio() {
|
||||
if (!audioLen) {
|
||||
statusLine = "No recording. Try BEEP.";
|
||||
drawDynamic();
|
||||
return;
|
||||
}
|
||||
statusLine = "Playing recording...";
|
||||
drawDynamic();
|
||||
size_t wrote = i2s.write(audioBuf, audioLen);
|
||||
statusLine = (wrote > 0) ? "Play done" : "Play failed";
|
||||
drawDynamic();
|
||||
}
|
||||
|
||||
void handleTouch() {
|
||||
if (!touchIrq) return;
|
||||
touchIrq = false;
|
||||
int16_t x[1];
|
||||
int16_t y[1];
|
||||
uint8_t touched = touch.getPoint(x, y, 1);
|
||||
if (!touched) return;
|
||||
|
||||
int px = x[0];
|
||||
int py = y[0];
|
||||
if (py < 76 || py > 140) return;
|
||||
if (px >= 24 && px <= 156) recordAudio();
|
||||
else if (px >= 174 && px <= 306) playAudio();
|
||||
else if (px >= 324 && px <= 456) playBeep();
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
Wire.begin(IIC_SDA, IIC_SCL);
|
||||
|
||||
pinMode(PA, OUTPUT);
|
||||
digitalWrite(PA, HIGH);
|
||||
|
||||
if (!gfx->begin()) Serial.println("gfx begin failed");
|
||||
bus->writeC8D8(0x36, 0xA0);
|
||||
gfx->setBrightness(200);
|
||||
|
||||
pinMode(TP_RST, OUTPUT);
|
||||
digitalWrite(TP_RST, LOW);
|
||||
delay(30);
|
||||
digitalWrite(TP_RST, HIGH);
|
||||
delay(50);
|
||||
touch.setPins(TP_RST, TP_INT);
|
||||
touch.begin(Wire, 0x5A, IIC_SDA, IIC_SCL);
|
||||
touch.setMaxCoordinates(480, 480);
|
||||
touch.setSwapXY(true);
|
||||
touch.setMirrorXY(true, false);
|
||||
attachInterrupt(TP_INT, onTouchInt, FALLING);
|
||||
|
||||
if (qmi.begin(Wire, QMI8658_L_SLAVE_ADDRESS, IIC_SDA, IIC_SCL)) {
|
||||
qmi.configAccelerometer(SensorQMI8658::ACC_RANGE_4G, SensorQMI8658::ACC_ODR_1000Hz, SensorQMI8658::LPF_MODE_0);
|
||||
qmi.enableAccelerometer();
|
||||
} else {
|
||||
tiltLine = "Tilt: IMU not found";
|
||||
}
|
||||
|
||||
i2s.setPins(9, 45, 8, 10, 42);
|
||||
if (!i2s.begin(I2S_MODE_STD, SAMPLE_RATE, I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO, I2S_STD_SLOT_BOTH)) {
|
||||
statusLine = "I2S init failed";
|
||||
} else if (codecInit() != ESP_OK) {
|
||||
statusLine = "ES8311 init failed";
|
||||
} else if (micInit() != ESP_OK) {
|
||||
statusLine = "ES7210 init failed";
|
||||
} else {
|
||||
micReady = true;
|
||||
}
|
||||
|
||||
audioBuf = (uint8_t *)ps_malloc(AUDIO_BYTES);
|
||||
if (!audioBuf) statusLine = "No PSRAM buffer";
|
||||
|
||||
drawStaticUi();
|
||||
drawDynamic();
|
||||
}
|
||||
|
||||
void loop() {
|
||||
handleTouch();
|
||||
if (millis() - lastTiltMs > 120) {
|
||||
lastTiltMs = millis();
|
||||
updateTilt();
|
||||
}
|
||||
delay(5);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
# Test Sketches
|
||||
|
||||
Набор старых отдельных диагностических скетчей для `Waveshare ESP32-S3-Touch-AMOLED-2.16`.
|
||||
|
||||
Скетчи в этой папке нужны для быстрой проверки конкретных гипотез и не являются основным UI проекта.
|
||||
Основной скетч сейчас лежит в `main-device/shine_homeserver_main/`.
|
||||
|
||||
## Список
|
||||
|
||||
- `gfx_text_render_test/` - проверка рендера текста через `Arduino_GFX` и сравнение с `U8g2`
|
||||
- `gfx_button_layout_test/` - проверка геометрии кнопок, особенно нижних рядов и широких кнопок
|
||||
- `lvgl_basic_test/` - минимальный тест `LVGL` с заголовком, текстом и кнопками
|
||||
- `lvgl_interaction_test/` - расширенный тест `LVGL` с 9 кнопками, touch-вводом и статусом нажатия
|
||||
- `lvgl_touch_debug_test/` - диагностика touch: сырые координаты, точка касания и одна большая кнопка `LVGL`
|
||||
- `lvgl_official_based_test/` - минимальный наш экран поверх максимально близкой к официальному `05_LVGL_Widgets` инициализации
|
||||
- `lvgl_subserver_touch_test/` - старый гибридный тест: `LVGL`-экран с инициализацией дисплея и чтением touch из старого `shine_homeserver_ui`; подтверждён на реальном устройстве
|
||||
- `lvgl_russian_font_test/` - тест кастомного кириллического `LVGL`-шрифта с русскими кнопками, длинными строками и рабочим touch
|
||||
- `lvgl_nav_minimal_test/` - старое тестовое имя, этот скетч перенесён в `shine_homeserver_main/` и теперь является основным
|
||||
|
||||
## Запуск
|
||||
|
||||
Использовать через `burn.sh`:
|
||||
|
||||
- `./burn.sh gfx-text-test`
|
||||
- `./burn.sh gfx-layout-test`
|
||||
- `./burn.sh lvgl-basic-test`
|
||||
- `./burn.sh lvgl-interaction-test`
|
||||
- `./burn.sh lvgl-touch-debug-test`
|
||||
- `./burn.sh lvgl-official-based-test`
|
||||
- `./burn.sh lvgl-subserver-touch-test`
|
||||
- `./burn.sh lvgl-russian-font-test`
|
||||
@@ -0,0 +1,93 @@
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
#include <Arduino_GFX_Library.h>
|
||||
|
||||
#define PIN_LCD_CS 12
|
||||
#define PIN_LCD_SCLK 38
|
||||
#define PIN_LCD_D0 4
|
||||
#define PIN_LCD_D1 5
|
||||
#define PIN_LCD_D2 6
|
||||
#define PIN_LCD_D3 7
|
||||
#define PIN_LCD_RST 2
|
||||
#define PIN_I2C_SDA 15
|
||||
#define PIN_I2C_SCL 14
|
||||
|
||||
#define DISP_W 480
|
||||
#define DISP_H 480
|
||||
|
||||
#define C_BG 0x0841u
|
||||
#define C_PANEL 0x1082u
|
||||
#define C_CARD 0x18C3u
|
||||
#define C_BORDER 0x39C7u
|
||||
#define C_TEXT 0xFFFFu
|
||||
#define C_OK 0x3666u
|
||||
#define C_WARN 0xECA0u
|
||||
#define C_BUTTON 0x2145u
|
||||
#define C_BUTTON2 0x3186u
|
||||
|
||||
Arduino_DataBus *gBus = new Arduino_ESP32QSPI(
|
||||
PIN_LCD_CS, PIN_LCD_SCLK, PIN_LCD_D0, PIN_LCD_D1, PIN_LCD_D2, PIN_LCD_D3);
|
||||
Arduino_CO5300 *gfx = new Arduino_CO5300(
|
||||
gBus, PIN_LCD_RST, 0, DISP_W, DISP_H, 0, 0, 0, 0);
|
||||
|
||||
static void drawPanel(int x, int y, int w, int h, uint16_t fill, uint16_t border, int radius = 10) {
|
||||
gfx->fillRoundRect(x, y, w, h, radius, fill);
|
||||
gfx->drawRoundRect(x, y, w, h, radius, border);
|
||||
}
|
||||
|
||||
static void drawText(int x, int y, const char *text, uint16_t color, uint8_t size = 2) {
|
||||
gfx->setFont();
|
||||
gfx->setTextSize(size);
|
||||
gfx->setTextColor(color);
|
||||
gfx->setCursor(x, y);
|
||||
gfx->print(text);
|
||||
}
|
||||
|
||||
static void drawButton(int x, int y, int w, int h, uint16_t fill, const char *label, uint8_t size = 2) {
|
||||
drawPanel(x, y, w, h, fill, C_BORDER, 12);
|
||||
gfx->setFont();
|
||||
gfx->setTextSize(size);
|
||||
gfx->setTextColor(C_TEXT);
|
||||
int textW = strlen(label) * 6 * size;
|
||||
int textX = x + (w - textW) / 2;
|
||||
if (textX < x + 8) {
|
||||
textX = x + 8;
|
||||
}
|
||||
int textY = y + (h + 8 * size) / 2 - 2;
|
||||
gfx->setCursor(textX, textY);
|
||||
gfx->print(label);
|
||||
}
|
||||
|
||||
static void drawScreen() {
|
||||
gfx->fillScreen(C_BG);
|
||||
drawPanel(12, 12, 456, 456, C_PANEL, C_BORDER, 16);
|
||||
drawText(24, 42, "BUTTON LAYOUT TEST", C_TEXT, 2);
|
||||
drawText(24, 70, "Check bottom and wide buttons", C_WARN, 1);
|
||||
|
||||
drawButton(20, 110, 136, 52, C_BUTTON, "STATUS");
|
||||
drawButton(172, 110, 136, 52, C_BUTTON, "CONNECT");
|
||||
drawButton(324, 110, 136, 52, C_BUTTON, "ACCOUNT");
|
||||
|
||||
drawButton(20, 180, 136, 52, C_BUTTON, "WALLET");
|
||||
drawButton(172, 180, 136, 52, C_BUTTON, "REQUESTS");
|
||||
drawButton(324, 180, 136, 52, C_BUTTON, "SETTINGS");
|
||||
|
||||
drawButton(20, 270, 212, 48, C_BUTTON2, "BACK");
|
||||
drawButton(248, 270, 212, 48, C_OK, "REFRESH");
|
||||
|
||||
drawButton(20, 338, 440, 40, C_BUTTON2, "FULL WIDTH BUTTON", 2);
|
||||
drawButton(20, 390, 440, 56, C_OK, "REGISTER DEVICE", 2);
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
Wire.begin(PIN_I2C_SDA, PIN_I2C_SCL);
|
||||
gfx->begin();
|
||||
gBus->writeC8D8(0x36, 0xA0);
|
||||
gfx->setBrightness(220);
|
||||
drawScreen();
|
||||
}
|
||||
|
||||
void loop() {
|
||||
delay(1000);
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
#include <Arduino_GFX_Library.h>
|
||||
#include <U8g2lib.h>
|
||||
|
||||
#define PIN_LCD_CS 12
|
||||
#define PIN_LCD_SCLK 38
|
||||
#define PIN_LCD_D0 4
|
||||
#define PIN_LCD_D1 5
|
||||
#define PIN_LCD_D2 6
|
||||
#define PIN_LCD_D3 7
|
||||
#define PIN_LCD_RST 2
|
||||
#define PIN_I2C_SDA 15
|
||||
#define PIN_I2C_SCL 14
|
||||
|
||||
#define DISP_W 480
|
||||
#define DISP_H 480
|
||||
|
||||
#define C_BG 0x0841u
|
||||
#define C_PANEL 0x1082u
|
||||
#define C_CARD 0x18C3u
|
||||
#define C_BORDER 0x39C7u
|
||||
#define C_TEXT 0xFFFFu
|
||||
#define C_MUTE 0xBDF7u
|
||||
#define C_OK 0x3666u
|
||||
#define C_WARN 0xECA0u
|
||||
#define C_BUTTON 0x2145u
|
||||
#define C_BUTTON2 0x3186u
|
||||
|
||||
#define FONT_BODY u8g2_font_9x15_t_cyrillic
|
||||
#define FONT_SMALL u8g2_font_6x13_t_cyrillic
|
||||
|
||||
Arduino_DataBus *gBus = new Arduino_ESP32QSPI(
|
||||
PIN_LCD_CS, PIN_LCD_SCLK, PIN_LCD_D0, PIN_LCD_D1, PIN_LCD_D2, PIN_LCD_D3);
|
||||
Arduino_CO5300 *gfx = new Arduino_CO5300(
|
||||
gBus, PIN_LCD_RST, 0, DISP_W, DISP_H, 0, 0, 0, 0);
|
||||
|
||||
static void drawPanel(int x, int y, int w, int h, uint16_t fill, uint16_t border, int radius = 10) {
|
||||
gfx->fillRoundRect(x, y, w, h, radius, fill);
|
||||
gfx->drawRoundRect(x, y, w, h, radius, border);
|
||||
}
|
||||
|
||||
static void drawDefaultText(int x, int y, const char *text, uint16_t color, uint8_t size = 1) {
|
||||
gfx->setFont();
|
||||
gfx->setTextSize(size);
|
||||
gfx->setTextColor(color);
|
||||
gfx->setCursor(x, y);
|
||||
gfx->print(text);
|
||||
}
|
||||
|
||||
static void drawU8Text(int x, int y, const String &text, uint16_t color, const uint8_t *font) {
|
||||
gfx->setTextSize(1);
|
||||
gfx->setFont(font);
|
||||
gfx->setTextColor(color);
|
||||
gfx->setCursor(x, y);
|
||||
gfx->print(text);
|
||||
}
|
||||
|
||||
static void drawButton(int x, int y, int w, int h, uint16_t fill, const String &label, const uint8_t *font, bool useDefaultFont = false) {
|
||||
drawPanel(x, y, w, h, fill, C_BORDER, 12);
|
||||
if (useDefaultFont) {
|
||||
drawDefaultText(x + 14, y + 30, label.c_str(), C_TEXT, 2);
|
||||
return;
|
||||
}
|
||||
drawU8Text(x + 14, y + 32, label, C_TEXT, font);
|
||||
}
|
||||
|
||||
static void drawScreen() {
|
||||
gfx->fillScreen(C_BG);
|
||||
|
||||
drawPanel(12, 12, 456, 456, C_PANEL, C_BORDER, 16);
|
||||
drawDefaultText(28, 42, "TEXT TEST 123", C_TEXT, 2);
|
||||
drawU8Text(28, 72, "Default ASCII above", C_MUTE, (const uint8_t *)FONT_SMALL);
|
||||
|
||||
drawPanel(24, 90, 432, 72, C_CARD, C_BORDER, 12);
|
||||
drawDefaultText(38, 118, "A: Default font ABC123", C_TEXT, 2);
|
||||
drawDefaultText(38, 146, "Visible? then base path OK", C_WARN, 1);
|
||||
|
||||
drawPanel(24, 174, 432, 84, C_CARD, C_BORDER, 12);
|
||||
drawU8Text(38, 204, "B: U8g2 ASCII abc123", C_TEXT, (const uint8_t *)FONT_BODY);
|
||||
drawU8Text(38, 230, "C: Русский текст 123", C_OK, (const uint8_t *)FONT_BODY);
|
||||
|
||||
drawPanel(24, 270, 432, 84, C_CARD, C_BORDER, 12);
|
||||
drawU8Text(38, 298, "D: Мелкий шрифт кнопок", C_TEXT, (const uint8_t *)FONT_SMALL);
|
||||
drawU8Text(38, 320, "Если это видно, FONT_SMALL жив", C_MUTE, (const uint8_t *)FONT_SMALL);
|
||||
|
||||
drawButton(24, 368, 128, 72, C_BUTTON, "BTN 1", nullptr, true);
|
||||
drawButton(176, 368, 128, 72, C_OK, "abc123", (const uint8_t *)FONT_BODY);
|
||||
drawButton(328, 368, 128, 72, C_BUTTON2, "Русский", (const uint8_t *)FONT_BODY);
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
Wire.begin(PIN_I2C_SDA, PIN_I2C_SCL);
|
||||
gfx->begin();
|
||||
gBus->writeC8D8(0x36, 0xA0);
|
||||
gfx->setBrightness(220);
|
||||
gfx->setUTF8Print(true);
|
||||
drawScreen();
|
||||
}
|
||||
|
||||
void loop() {
|
||||
delay(1000);
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
#include <lvgl.h>
|
||||
#include <Arduino_GFX_Library.h>
|
||||
#include "pin_config.h"
|
||||
|
||||
#define EXAMPLE_LVGL_TICK_PERIOD_MS 2
|
||||
|
||||
static lv_disp_draw_buf_t gDrawBuf;
|
||||
static lv_color_t *gBuf1 = nullptr;
|
||||
static lv_color_t *gBuf2 = nullptr;
|
||||
|
||||
Arduino_DataBus *gBus = new Arduino_ESP32QSPI(
|
||||
LCD_CS, LCD_SCLK, LCD_SDIO0, LCD_SDIO1, LCD_SDIO2, LCD_SDIO3);
|
||||
Arduino_CO5300 *gfx = new Arduino_CO5300(
|
||||
gBus, LCD_RESET, 0, LCD_WIDTH, LCD_HEIGHT, 0, 0, 0, 0);
|
||||
|
||||
static void lvglRounderCb(lv_disp_drv_t *dispDrv, lv_area_t *area) {
|
||||
LV_UNUSED(dispDrv);
|
||||
if (area->x1 % 2 != 0) area->x1--;
|
||||
if (area->y1 % 2 != 0) area->y1--;
|
||||
if (area->x2 % 2 == 0) area->x2++;
|
||||
if (area->y2 % 2 == 0) area->y2++;
|
||||
}
|
||||
|
||||
static void lvglFlushCb(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *colorP) {
|
||||
uint32_t w = area->x2 - area->x1 + 1;
|
||||
uint32_t h = area->y2 - area->y1 + 1;
|
||||
#if (LV_COLOR_16_SWAP != 0)
|
||||
gfx->draw16bitBeRGBBitmap(area->x1, area->y1, (uint16_t *)&colorP->full, w, h);
|
||||
#else
|
||||
gfx->draw16bitRGBBitmap(area->x1, area->y1, (uint16_t *)&colorP->full, w, h);
|
||||
#endif
|
||||
lv_disp_flush_ready(disp);
|
||||
}
|
||||
|
||||
static void lvglTick(void *arg) {
|
||||
LV_UNUSED(arg);
|
||||
lv_tick_inc(EXAMPLE_LVGL_TICK_PERIOD_MS);
|
||||
}
|
||||
|
||||
static void createUi() {
|
||||
lv_obj_set_style_bg_color(lv_scr_act(), lv_color_hex(0x0B1320), 0);
|
||||
lv_obj_set_style_bg_opa(lv_scr_act(), LV_OPA_COVER, 0);
|
||||
|
||||
lv_obj_t *title = lv_label_create(lv_scr_act());
|
||||
lv_label_set_text(title, "LVGL BASIC TEST");
|
||||
lv_obj_set_style_text_color(title, lv_color_hex(0xFFFFFF), 0);
|
||||
lv_obj_set_style_text_font(title, &lv_font_montserrat_22, 0);
|
||||
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 18);
|
||||
|
||||
lv_obj_t *subtitle = lv_label_create(lv_scr_act());
|
||||
lv_label_set_text(subtitle, "If this text is visible, LVGL path works.");
|
||||
lv_obj_set_width(subtitle, 420);
|
||||
lv_label_set_long_mode(subtitle, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_style_text_color(subtitle, lv_color_hex(0xD5DCE5), 0);
|
||||
lv_obj_set_style_text_font(subtitle, &lv_font_montserrat_16, 0);
|
||||
lv_obj_align(subtitle, LV_ALIGN_TOP_MID, 0, 58);
|
||||
|
||||
const char *labels[3] = {"Button One", "Second Button", "Bottom Action"};
|
||||
const lv_color_t colors[3] = {
|
||||
lv_color_hex(0x2B4C7E),
|
||||
lv_color_hex(0x2E8B57),
|
||||
lv_color_hex(0xB45F06),
|
||||
};
|
||||
const lv_coord_t ys[3] = {150, 236, 340};
|
||||
const lv_coord_t hs[3] = {58, 58, 64};
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
lv_obj_t *btn = lv_btn_create(lv_scr_act());
|
||||
lv_obj_set_size(btn, 360, hs[i]);
|
||||
lv_obj_align(btn, LV_ALIGN_TOP_MID, 0, ys[i]);
|
||||
lv_obj_set_style_radius(btn, 14, 0);
|
||||
lv_obj_set_style_bg_color(btn, colors[i], 0);
|
||||
lv_obj_set_style_border_width(btn, 2, 0);
|
||||
lv_obj_set_style_border_color(btn, lv_color_hex(0x7F96B0), 0);
|
||||
|
||||
lv_obj_t *label = lv_label_create(btn);
|
||||
lv_label_set_text(label, labels[i]);
|
||||
lv_obj_set_style_text_font(label, &lv_font_montserrat_20, 0);
|
||||
lv_obj_set_style_text_color(label, lv_color_hex(0xFFFFFF), 0);
|
||||
lv_obj_center(label);
|
||||
}
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
Wire.begin(IIC_SDA, IIC_SCL);
|
||||
|
||||
gfx->begin();
|
||||
gBus->writeC8D8(0x36, 0xA0);
|
||||
gfx->setBrightness(220);
|
||||
|
||||
lv_init();
|
||||
|
||||
uint32_t screenWidth = gfx->width();
|
||||
uint32_t screenHeight = gfx->height();
|
||||
gBuf1 = (lv_color_t *)heap_caps_malloc(screenWidth * screenHeight / 4 * sizeof(lv_color_t), MALLOC_CAP_DMA);
|
||||
gBuf2 = (lv_color_t *)heap_caps_malloc(screenWidth * screenHeight / 4 * sizeof(lv_color_t), MALLOC_CAP_DMA);
|
||||
lv_disp_draw_buf_init(&gDrawBuf, gBuf1, gBuf2, screenWidth * screenHeight / 4);
|
||||
|
||||
static lv_disp_drv_t dispDrv;
|
||||
lv_disp_drv_init(&dispDrv);
|
||||
dispDrv.hor_res = screenWidth;
|
||||
dispDrv.ver_res = screenHeight;
|
||||
dispDrv.flush_cb = lvglFlushCb;
|
||||
dispDrv.rounder_cb = lvglRounderCb;
|
||||
dispDrv.draw_buf = &gDrawBuf;
|
||||
lv_disp_drv_register(&dispDrv);
|
||||
|
||||
const esp_timer_create_args_t lvglTickTimerArgs = {
|
||||
.callback = &lvglTick,
|
||||
.name = "lvgl_tick"};
|
||||
esp_timer_handle_t lvglTickTimer = nullptr;
|
||||
esp_timer_create(&lvglTickTimerArgs, &lvglTickTimer);
|
||||
esp_timer_start_periodic(lvglTickTimer, EXAMPLE_LVGL_TICK_PERIOD_MS * 1000);
|
||||
|
||||
createUi();
|
||||
Serial.println("LVGL basic test ready");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
lv_timer_handler();
|
||||
delay(5);
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
#include <lvgl.h>
|
||||
#include <Arduino_GFX_Library.h>
|
||||
#include "pin_config.h"
|
||||
#include "TouchDrvCSTXXX.hpp"
|
||||
|
||||
#define EXAMPLE_LVGL_TICK_PERIOD_MS 2
|
||||
|
||||
static lv_disp_draw_buf_t gDrawBuf;
|
||||
static lv_color_t *gBuf1 = nullptr;
|
||||
static lv_color_t *gBuf2 = nullptr;
|
||||
static lv_obj_t *gStatusLabel = nullptr;
|
||||
static uint32_t gPressCount = 0;
|
||||
|
||||
static TouchDrvCST92xx gTouch;
|
||||
static int16_t gTouchX[5];
|
||||
static int16_t gTouchY[5];
|
||||
|
||||
Arduino_DataBus *gBus = new Arduino_ESP32QSPI(
|
||||
LCD_CS, LCD_SCLK, LCD_SDIO0, LCD_SDIO1, LCD_SDIO2, LCD_SDIO3);
|
||||
Arduino_CO5300 *gfx = new Arduino_CO5300(
|
||||
gBus, LCD_RESET, 0, LCD_WIDTH, LCD_HEIGHT, 0, 0, 0, 0);
|
||||
|
||||
static void lvglFlushCb(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *colorP) {
|
||||
uint32_t w = area->x2 - area->x1 + 1;
|
||||
uint32_t h = area->y2 - area->y1 + 1;
|
||||
#if (LV_COLOR_16_SWAP != 0)
|
||||
gfx->draw16bitBeRGBBitmap(area->x1, area->y1, (uint16_t *)&colorP->full, w, h);
|
||||
#else
|
||||
gfx->draw16bitRGBBitmap(area->x1, area->y1, (uint16_t *)&colorP->full, w, h);
|
||||
#endif
|
||||
lv_disp_flush_ready(disp);
|
||||
}
|
||||
|
||||
static void lvglTick(void *arg) {
|
||||
LV_UNUSED(arg);
|
||||
lv_tick_inc(EXAMPLE_LVGL_TICK_PERIOD_MS);
|
||||
}
|
||||
|
||||
static void lvglTouchRead(lv_indev_drv_t *indevDrv, lv_indev_data_t *data) {
|
||||
LV_UNUSED(indevDrv);
|
||||
uint8_t touched = gTouch.getPoint(gTouchX, gTouchY, gTouch.getSupportTouchPoint());
|
||||
if (touched > 0) {
|
||||
data->state = LV_INDEV_STATE_PR;
|
||||
data->point.x = gTouchX[0];
|
||||
data->point.y = gTouchY[0];
|
||||
return;
|
||||
}
|
||||
|
||||
data->state = LV_INDEV_STATE_REL;
|
||||
}
|
||||
|
||||
static void buttonEventCb(lv_event_t *event) {
|
||||
if (lv_event_get_code(event) != LV_EVENT_CLICKED) {
|
||||
return;
|
||||
}
|
||||
|
||||
lv_obj_t *btn = lv_event_get_target(event);
|
||||
const char *name = (const char *)lv_event_get_user_data(event);
|
||||
gPressCount++;
|
||||
|
||||
lv_obj_set_style_outline_width(btn, 4, 0);
|
||||
lv_obj_set_style_outline_color(btn, lv_color_hex(0xFFF3B0), 0);
|
||||
lv_obj_set_style_outline_pad(btn, 2, 0);
|
||||
|
||||
String status = "Pressed: ";
|
||||
status += name;
|
||||
status += " (#";
|
||||
status += String(gPressCount);
|
||||
status += ")";
|
||||
lv_label_set_text(gStatusLabel, status.c_str());
|
||||
|
||||
Serial.println(status);
|
||||
}
|
||||
|
||||
static void createButton(lv_obj_t *parent, const char *labelText, lv_coord_t col, lv_coord_t row, uint32_t colorHex) {
|
||||
static const lv_coord_t kStartX = 22;
|
||||
static const lv_coord_t kStartY = 122;
|
||||
static const lv_coord_t kButtonW = 134;
|
||||
static const lv_coord_t kButtonH = 64;
|
||||
static const lv_coord_t kGapX = 18;
|
||||
static const lv_coord_t kGapY = 16;
|
||||
|
||||
lv_obj_t *btn = lv_btn_create(parent);
|
||||
lv_obj_set_size(btn, kButtonW, kButtonH);
|
||||
lv_obj_set_pos(btn, kStartX + col * (kButtonW + kGapX), kStartY + row * (kButtonH + kGapY));
|
||||
lv_obj_set_style_radius(btn, 16, 0);
|
||||
lv_obj_set_style_bg_color(btn, lv_color_hex(colorHex), 0);
|
||||
lv_obj_set_style_border_width(btn, 2, 0);
|
||||
lv_obj_set_style_border_color(btn, lv_color_hex(0x7F96B0), 0);
|
||||
lv_obj_set_style_shadow_width(btn, 0, 0);
|
||||
lv_obj_add_event_cb(btn, buttonEventCb, LV_EVENT_CLICKED, (void *)labelText);
|
||||
|
||||
lv_obj_t *label = lv_label_create(btn);
|
||||
lv_label_set_text(label, labelText);
|
||||
lv_obj_set_style_text_font(label, &lv_font_montserrat_18, 0);
|
||||
lv_obj_set_style_text_color(label, lv_color_hex(0xFFFFFF), 0);
|
||||
lv_obj_center(label);
|
||||
}
|
||||
|
||||
static void createUi() {
|
||||
lv_obj_t *screen = lv_scr_act();
|
||||
lv_obj_set_style_bg_color(screen, lv_color_hex(0x0B1320), 0);
|
||||
lv_obj_set_style_bg_opa(screen, LV_OPA_COVER, 0);
|
||||
|
||||
lv_obj_t *title = lv_label_create(screen);
|
||||
lv_label_set_text(title, "LVGL INTERACTION TEST");
|
||||
lv_obj_set_style_text_color(title, lv_color_hex(0xFFFFFF), 0);
|
||||
lv_obj_set_style_text_font(title, &lv_font_montserrat_22, 0);
|
||||
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 14);
|
||||
|
||||
lv_obj_t *subtitle = lv_label_create(screen);
|
||||
lv_label_set_text(subtitle, "Tap buttons. Bottom label must change on every press.");
|
||||
lv_obj_set_width(subtitle, 420);
|
||||
lv_label_set_long_mode(subtitle, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_style_text_color(subtitle, lv_color_hex(0xD5DCE5), 0);
|
||||
lv_obj_set_style_text_font(subtitle, &lv_font_montserrat_14, 0);
|
||||
lv_obj_align(subtitle, LV_ALIGN_TOP_MID, 0, 48);
|
||||
|
||||
createButton(screen, "Status", 0, 0, 0x355C7D);
|
||||
createButton(screen, "Connect", 1, 0, 0x2A9D8F);
|
||||
createButton(screen, "Wallet", 2, 0, 0x457B9D);
|
||||
createButton(screen, "Requests", 0, 1, 0xE76F51);
|
||||
createButton(screen, "Settings", 1, 1, 0x8D5A97);
|
||||
createButton(screen, "Register", 2, 1, 0x6A994E);
|
||||
createButton(screen, "Approve", 0, 2, 0xBC6C25);
|
||||
createButton(screen, "Reject", 1, 2, 0xB56576);
|
||||
createButton(screen, "Back", 2, 2, 0x6C757D);
|
||||
|
||||
lv_obj_t *statusPanel = lv_obj_create(screen);
|
||||
lv_obj_set_size(statusPanel, 436, 56);
|
||||
lv_obj_align(statusPanel, LV_ALIGN_BOTTOM_MID, 0, -16);
|
||||
lv_obj_set_style_radius(statusPanel, 14, 0);
|
||||
lv_obj_set_style_bg_color(statusPanel, lv_color_hex(0x162033), 0);
|
||||
lv_obj_set_style_border_width(statusPanel, 2, 0);
|
||||
lv_obj_set_style_border_color(statusPanel, lv_color_hex(0x415A77), 0);
|
||||
lv_obj_set_style_pad_all(statusPanel, 10, 0);
|
||||
|
||||
gStatusLabel = lv_label_create(statusPanel);
|
||||
lv_label_set_text(gStatusLabel, "Pressed: none");
|
||||
lv_obj_set_style_text_font(gStatusLabel, &lv_font_montserrat_18, 0);
|
||||
lv_obj_set_style_text_color(gStatusLabel, lv_color_hex(0xFFFFFF), 0);
|
||||
lv_obj_center(gStatusLabel);
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
Wire.begin(IIC_SDA, IIC_SCL);
|
||||
|
||||
pinMode(TP_RST, OUTPUT);
|
||||
pinMode(TP_INT, INPUT);
|
||||
digitalWrite(TP_RST, LOW);
|
||||
delay(30);
|
||||
digitalWrite(TP_RST, HIGH);
|
||||
delay(50);
|
||||
delay(1000);
|
||||
|
||||
gTouch.setPins(TP_RST, TP_INT);
|
||||
bool touchOk = gTouch.begin(Wire, 0x5A, IIC_SDA, IIC_SCL);
|
||||
if (!touchOk) {
|
||||
Serial.println("Touch init failed");
|
||||
} else {
|
||||
Serial.print("Touch model: ");
|
||||
Serial.println(gTouch.getModelName());
|
||||
gTouch.sleep();
|
||||
gTouch.reset();
|
||||
gTouch.setMaxCoordinates(480, 480);
|
||||
gTouch.setSwapXY(true);
|
||||
gTouch.setMirrorXY(true, false);
|
||||
}
|
||||
|
||||
gfx->begin();
|
||||
gBus->writeC8D8(0x36, 0xA0);
|
||||
gfx->setBrightness(220);
|
||||
gfx->fillScreen(0x0000);
|
||||
|
||||
lv_init();
|
||||
|
||||
uint32_t screenWidth = gfx->width();
|
||||
uint32_t screenHeight = gfx->height();
|
||||
gBuf1 = (lv_color_t *)heap_caps_malloc(screenWidth * screenHeight / 4 * sizeof(lv_color_t), MALLOC_CAP_DMA);
|
||||
gBuf2 = (lv_color_t *)heap_caps_malloc(screenWidth * screenHeight / 4 * sizeof(lv_color_t), MALLOC_CAP_DMA);
|
||||
lv_disp_draw_buf_init(&gDrawBuf, gBuf1, gBuf2, screenWidth * screenHeight / 4);
|
||||
|
||||
static lv_disp_drv_t dispDrv;
|
||||
lv_disp_drv_init(&dispDrv);
|
||||
dispDrv.hor_res = screenWidth;
|
||||
dispDrv.ver_res = screenHeight;
|
||||
dispDrv.flush_cb = lvglFlushCb;
|
||||
dispDrv.draw_buf = &gDrawBuf;
|
||||
lv_disp_drv_register(&dispDrv);
|
||||
|
||||
static lv_indev_drv_t indevDrv;
|
||||
lv_indev_drv_init(&indevDrv);
|
||||
indevDrv.type = LV_INDEV_TYPE_POINTER;
|
||||
indevDrv.read_cb = lvglTouchRead;
|
||||
lv_indev_drv_register(&indevDrv);
|
||||
|
||||
const esp_timer_create_args_t lvglTickTimerArgs = {
|
||||
.callback = &lvglTick,
|
||||
.name = "lvgl_tick"};
|
||||
esp_timer_handle_t lvglTickTimer = nullptr;
|
||||
esp_timer_create(&lvglTickTimerArgs, &lvglTickTimer);
|
||||
esp_timer_start_periodic(lvglTickTimer, EXAMPLE_LVGL_TICK_PERIOD_MS * 1000);
|
||||
|
||||
createUi();
|
||||
Serial.println("LVGL interaction test ready");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
lv_timer_handler();
|
||||
delay(5);
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
#include <lvgl.h>
|
||||
#include "Arduino_GFX_Library.h"
|
||||
#include "pin_config.h"
|
||||
#include "TouchDrvCSTXXX.hpp"
|
||||
#include "lv_conf.h"
|
||||
#include "HWCDC.h"
|
||||
#include <Wire.h>
|
||||
|
||||
HWCDC USBSerial;
|
||||
|
||||
#define EXAMPLE_LVGL_TICK_PERIOD_MS 2
|
||||
|
||||
static lv_disp_draw_buf_t draw_buf;
|
||||
static lv_color_t *buf1 = nullptr;
|
||||
static lv_color_t *buf2 = nullptr;
|
||||
static lv_obj_t *status_label = nullptr;
|
||||
static uint32_t click_count = 0;
|
||||
|
||||
Arduino_DataBus *bus = new Arduino_ESP32QSPI(
|
||||
LCD_CS, LCD_SCLK, LCD_SDIO0, LCD_SDIO1, LCD_SDIO2, LCD_SDIO3);
|
||||
|
||||
Arduino_CO5300 *gfx = new Arduino_CO5300(
|
||||
bus, LCD_RESET, 0, LCD_WIDTH, LCD_HEIGHT, 0, 0, 0, 0);
|
||||
|
||||
TouchDrvCST92xx touch;
|
||||
int16_t touch_x[5], touch_y[5];
|
||||
bool isPressed = false;
|
||||
uint32_t screenWidth = 0;
|
||||
uint32_t screenHeight = 0;
|
||||
|
||||
#if LV_USE_LOG != 0
|
||||
void my_print(const char *buf) {
|
||||
USBSerial.printf(buf);
|
||||
USBSerial.flush();
|
||||
}
|
||||
#endif
|
||||
|
||||
static void example_lvgl_rounder_cb(struct _lv_disp_drv_t *disp_drv, lv_area_t *area) {
|
||||
LV_UNUSED(disp_drv);
|
||||
if (area->x1 % 2 != 0) area->x1--;
|
||||
if (area->y1 % 2 != 0) area->y1--;
|
||||
if (area->x2 % 2 == 0) area->x2++;
|
||||
if (area->y2 % 2 == 0) area->y2++;
|
||||
}
|
||||
|
||||
static void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p) {
|
||||
uint32_t w = (area->x2 - area->x1 + 1);
|
||||
uint32_t h = (area->y2 - area->y1 + 1);
|
||||
|
||||
#if (LV_COLOR_16_SWAP != 0)
|
||||
gfx->draw16bitBeRGBBitmap(area->x1, area->y1, (uint16_t *)&color_p->full, w, h);
|
||||
#else
|
||||
gfx->draw16bitRGBBitmap(area->x1, area->y1, (uint16_t *)&color_p->full, w, h);
|
||||
#endif
|
||||
|
||||
lv_disp_flush_ready(disp);
|
||||
}
|
||||
|
||||
static void example_increase_lvgl_tick(void *arg) {
|
||||
LV_UNUSED(arg);
|
||||
lv_tick_inc(EXAMPLE_LVGL_TICK_PERIOD_MS);
|
||||
}
|
||||
|
||||
static void my_touchpad_read(lv_indev_drv_t *indev_driver, lv_indev_data_t *data) {
|
||||
LV_UNUSED(indev_driver);
|
||||
if (isPressed) {
|
||||
uint8_t touched = touch.getPoint(touch_x, touch_y, touch.getSupportTouchPoint());
|
||||
if (touched) {
|
||||
isPressed = false;
|
||||
data->state = LV_INDEV_STATE_PR;
|
||||
data->point.x = touch_x[0];
|
||||
data->point.y = touch_y[0];
|
||||
USBSerial.print("Touch x=");
|
||||
USBSerial.print(touch_x[0]);
|
||||
USBSerial.print(" y=");
|
||||
USBSerial.println(touch_y[0]);
|
||||
} else {
|
||||
data->state = LV_INDEV_STATE_REL;
|
||||
}
|
||||
} else {
|
||||
data->state = LV_INDEV_STATE_REL;
|
||||
}
|
||||
}
|
||||
|
||||
static void button_event_cb(lv_event_t *event) {
|
||||
if (lv_event_get_code(event) != LV_EVENT_CLICKED) {
|
||||
return;
|
||||
}
|
||||
|
||||
const char *name = (const char *)lv_event_get_user_data(event);
|
||||
click_count++;
|
||||
String text = "Pressed: ";
|
||||
text += name;
|
||||
text += " (#";
|
||||
text += String(click_count);
|
||||
text += ")";
|
||||
lv_label_set_text(status_label, text.c_str());
|
||||
USBSerial.println(text);
|
||||
}
|
||||
|
||||
static lv_obj_t *make_button(lv_obj_t *parent, const char *label_text, lv_coord_t x, lv_coord_t y, lv_coord_t w, lv_coord_t h, lv_color_t bg) {
|
||||
lv_obj_t *btn = lv_btn_create(parent);
|
||||
lv_obj_set_size(btn, w, h);
|
||||
lv_obj_set_pos(btn, x, y);
|
||||
lv_obj_set_style_radius(btn, 16, 0);
|
||||
lv_obj_set_style_bg_color(btn, bg, 0);
|
||||
lv_obj_set_style_border_width(btn, 2, 0);
|
||||
lv_obj_set_style_border_color(btn, lv_color_hex(0x8AA4BF), 0);
|
||||
lv_obj_set_style_shadow_width(btn, 0, 0);
|
||||
lv_obj_add_event_cb(btn, button_event_cb, LV_EVENT_CLICKED, (void *)label_text);
|
||||
|
||||
lv_obj_t *label = lv_label_create(btn);
|
||||
lv_label_set_text(label, label_text);
|
||||
lv_obj_set_style_text_font(label, &lv_font_montserrat_18, 0);
|
||||
lv_obj_set_style_text_color(label, lv_color_hex(0xFFFFFF), 0);
|
||||
lv_obj_center(label);
|
||||
return btn;
|
||||
}
|
||||
|
||||
static void create_ui() {
|
||||
lv_obj_t *screen = lv_scr_act();
|
||||
lv_obj_set_style_bg_color(screen, lv_color_hex(0x0B1320), 0);
|
||||
lv_obj_set_style_bg_opa(screen, LV_OPA_COVER, 0);
|
||||
|
||||
lv_obj_t *title = lv_label_create(screen);
|
||||
lv_label_set_text(title, "LVGL OFFICIAL BASED TEST");
|
||||
lv_obj_set_style_text_font(title, &lv_font_montserrat_20, 0);
|
||||
lv_obj_set_style_text_color(title, lv_color_hex(0xFFFFFF), 0);
|
||||
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 14);
|
||||
|
||||
lv_obj_t *hint = lv_label_create(screen);
|
||||
lv_label_set_text(hint, "Based on official LVGL_Widgets init. Tap buttons and watch the status.");
|
||||
lv_obj_set_width(hint, 430);
|
||||
lv_label_set_long_mode(hint, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_style_text_font(hint, &lv_font_montserrat_14, 0);
|
||||
lv_obj_set_style_text_color(hint, lv_color_hex(0xD4DDE7), 0);
|
||||
lv_obj_align(hint, LV_ALIGN_TOP_MID, 0, 44);
|
||||
|
||||
make_button(screen, "Status", 22, 120, 136, 60, lv_color_hex(0x355C7D));
|
||||
make_button(screen, "Connect", 174, 120, 136, 60, lv_color_hex(0x2A9D8F));
|
||||
make_button(screen, "Wallet", 326, 120, 132, 60, lv_color_hex(0x457B9D));
|
||||
|
||||
make_button(screen, "Requests", 22, 196, 136, 60, lv_color_hex(0xE76F51));
|
||||
make_button(screen, "Settings",174, 196, 136, 60, lv_color_hex(0x8D5A97));
|
||||
make_button(screen, "Register",326, 196, 132, 60, lv_color_hex(0x6A994E));
|
||||
|
||||
make_button(screen, "Approve", 22, 272, 136, 60, lv_color_hex(0xBC6C25));
|
||||
make_button(screen, "Reject", 174, 272, 136, 60, lv_color_hex(0xB56576));
|
||||
make_button(screen, "Back", 326, 272, 132, 60, lv_color_hex(0x6C757D));
|
||||
|
||||
lv_obj_t *bottom_btn = make_button(screen, "Bottom Action", 22, 350, 436, 64, lv_color_hex(0x1D6F42));
|
||||
lv_obj_set_style_text_font(lv_obj_get_child(bottom_btn, 0), &lv_font_montserrat_20, 0);
|
||||
|
||||
lv_obj_t *status_panel = lv_obj_create(screen);
|
||||
lv_obj_set_size(status_panel, 436, 42);
|
||||
lv_obj_set_pos(status_panel, 22, 426);
|
||||
lv_obj_set_style_radius(status_panel, 12, 0);
|
||||
lv_obj_set_style_bg_color(status_panel, lv_color_hex(0x17263A), 0);
|
||||
lv_obj_set_style_border_width(status_panel, 2, 0);
|
||||
lv_obj_set_style_border_color(status_panel, lv_color_hex(0x496582), 0);
|
||||
lv_obj_set_style_pad_all(status_panel, 6, 0);
|
||||
|
||||
status_label = lv_label_create(status_panel);
|
||||
lv_label_set_text(status_label, "Pressed: none");
|
||||
lv_obj_set_style_text_font(status_label, &lv_font_montserrat_16, 0);
|
||||
lv_obj_set_style_text_color(status_label, lv_color_hex(0xFFFFFF), 0);
|
||||
lv_obj_center(status_label);
|
||||
}
|
||||
|
||||
void setup() {
|
||||
USBSerial.begin(115200);
|
||||
|
||||
Wire.begin(IIC_SDA, IIC_SCL);
|
||||
|
||||
digitalWrite(TP_RST, LOW);
|
||||
delay(30);
|
||||
digitalWrite(TP_RST, HIGH);
|
||||
delay(50);
|
||||
delay(1000);
|
||||
|
||||
Wire.begin(IIC_SDA, IIC_SCL);
|
||||
|
||||
touch.setPins(TP_RST, TP_INT);
|
||||
bool result = touch.begin(Wire, 0x5A, IIC_SDA, IIC_SCL);
|
||||
if (result == false) {
|
||||
USBSerial.println("touch is not online...");
|
||||
while (1) delay(1000);
|
||||
}
|
||||
USBSerial.print("Touch model: ");
|
||||
USBSerial.println(touch.getModelName());
|
||||
touch.sleep();
|
||||
touch.reset();
|
||||
touch.setMaxCoordinates(480, 480);
|
||||
touch.setSwapXY(true);
|
||||
touch.setMirrorXY(true, false);
|
||||
attachInterrupt(
|
||||
TP_INT, []() {
|
||||
isPressed = true;
|
||||
},
|
||||
FALLING);
|
||||
|
||||
gfx->begin();
|
||||
gfx->setBrightness(200);
|
||||
bus->writeC8D8(0x36, 0xA0);
|
||||
|
||||
screenWidth = gfx->width();
|
||||
screenHeight = gfx->height();
|
||||
|
||||
lv_init();
|
||||
|
||||
buf1 = (lv_color_t *)heap_caps_malloc(screenWidth * screenHeight / 4 * sizeof(lv_color_t), MALLOC_CAP_DMA);
|
||||
buf2 = (lv_color_t *)heap_caps_malloc(screenWidth * screenHeight / 4 * sizeof(lv_color_t), MALLOC_CAP_DMA);
|
||||
|
||||
#if LV_USE_LOG != 0
|
||||
lv_log_register_print_cb(my_print);
|
||||
#endif
|
||||
|
||||
lv_disp_draw_buf_init(&draw_buf, buf1, buf2, screenWidth * screenHeight / 4);
|
||||
|
||||
static lv_disp_drv_t disp_drv;
|
||||
lv_disp_drv_init(&disp_drv);
|
||||
disp_drv.hor_res = screenWidth;
|
||||
disp_drv.ver_res = screenHeight;
|
||||
disp_drv.flush_cb = my_disp_flush;
|
||||
disp_drv.rounder_cb = example_lvgl_rounder_cb;
|
||||
disp_drv.draw_buf = &draw_buf;
|
||||
disp_drv.sw_rotate = 1;
|
||||
lv_disp_drv_register(&disp_drv);
|
||||
|
||||
static lv_indev_drv_t indev_drv;
|
||||
lv_indev_drv_init(&indev_drv);
|
||||
indev_drv.type = LV_INDEV_TYPE_POINTER;
|
||||
indev_drv.read_cb = my_touchpad_read;
|
||||
lv_indev_drv_register(&indev_drv);
|
||||
|
||||
const esp_timer_create_args_t lvgl_tick_timer_args = {
|
||||
.callback = &example_increase_lvgl_tick,
|
||||
.name = "lvgl_tick"
|
||||
};
|
||||
|
||||
esp_timer_handle_t lvgl_tick_timer = NULL;
|
||||
esp_timer_create(&lvgl_tick_timer_args, &lvgl_tick_timer);
|
||||
esp_timer_start_periodic(lvgl_tick_timer, EXAMPLE_LVGL_TICK_PERIOD_MS * 1000);
|
||||
|
||||
create_ui();
|
||||
USBSerial.println("LVGL official based test ready");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
lv_timer_handler();
|
||||
delay(5);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
#pragma once
|
||||
#include <lvgl.h>
|
||||
LV_FONT_DECLARE(lv_font_ru_18);
|
||||
LV_FONT_DECLARE(lv_font_ru_24);
|
||||
@@ -0,0 +1,264 @@
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
#include <lvgl.h>
|
||||
#include <Arduino_GFX_Library.h>
|
||||
#include <TouchDrvCSTXXX.hpp>
|
||||
#include "lv_font_ru.h"
|
||||
|
||||
#define PIN_LCD_CS 12
|
||||
#define PIN_LCD_SCLK 38
|
||||
#define PIN_LCD_D0 4
|
||||
#define PIN_LCD_D1 5
|
||||
#define PIN_LCD_D2 6
|
||||
#define PIN_LCD_D3 7
|
||||
#define PIN_LCD_RST 2
|
||||
#define PIN_I2C_SDA 15
|
||||
#define PIN_I2C_SCL 14
|
||||
#define PIN_TP_INT 11
|
||||
|
||||
#define DISP_W 480
|
||||
#define DISP_H 480
|
||||
#define LVGL_TICK_MS 2
|
||||
#define TEST_VERSION "v1-ru-font"
|
||||
|
||||
static lv_disp_draw_buf_t gDrawBuf;
|
||||
static lv_color_t *gBuf1 = nullptr;
|
||||
static lv_color_t *gBuf2 = nullptr;
|
||||
static lv_obj_t *gStatusLabel = nullptr;
|
||||
static lv_obj_t *gTouchLabel = nullptr;
|
||||
static uint32_t gClickCount = 0;
|
||||
static bool gTouchActive = false;
|
||||
static int16_t gLastX = -1;
|
||||
static int16_t gLastY = -1;
|
||||
|
||||
Arduino_DataBus *gBus = new Arduino_ESP32QSPI(
|
||||
PIN_LCD_CS, PIN_LCD_SCLK, PIN_LCD_D0, PIN_LCD_D1, PIN_LCD_D2, PIN_LCD_D3);
|
||||
Arduino_CO5300 *gfx = new Arduino_CO5300(
|
||||
gBus, PIN_LCD_RST, 0, DISP_W, DISP_H, 0, 0, 0, 0);
|
||||
TouchDrvCST92xx gTouch;
|
||||
|
||||
static void lvglFlushCb(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *colorP) {
|
||||
uint32_t w = area->x2 - area->x1 + 1;
|
||||
uint32_t h = area->y2 - area->y1 + 1;
|
||||
#if (LV_COLOR_16_SWAP != 0)
|
||||
gfx->draw16bitBeRGBBitmap(area->x1, area->y1, (uint16_t *)&colorP->full, w, h);
|
||||
#else
|
||||
gfx->draw16bitRGBBitmap(area->x1, area->y1, (uint16_t *)&colorP->full, w, h);
|
||||
#endif
|
||||
lv_disp_flush_ready(disp);
|
||||
}
|
||||
|
||||
static void lvglTick(void *arg) {
|
||||
LV_UNUSED(arg);
|
||||
lv_tick_inc(LVGL_TICK_MS);
|
||||
}
|
||||
|
||||
static void updateTouchLabel() {
|
||||
if (!gTouchLabel) {
|
||||
return;
|
||||
}
|
||||
|
||||
char text[96];
|
||||
if (gTouchActive) {
|
||||
snprintf(text, sizeof(text), "Касание: x=%d y=%d", gLastX, gLastY);
|
||||
} else {
|
||||
snprintf(text, sizeof(text), "Касание: нет x=%d y=%d", gLastX, gLastY);
|
||||
}
|
||||
lv_label_set_text(gTouchLabel, text);
|
||||
}
|
||||
|
||||
static void lvglTouchRead(lv_indev_drv_t *indevDrv, lv_indev_data_t *data) {
|
||||
LV_UNUSED(indevDrv);
|
||||
int16_t x = 0;
|
||||
int16_t y = 0;
|
||||
bool touching = gTouch.getPoint(&x, &y, 1) > 0;
|
||||
|
||||
if (touching) {
|
||||
gTouchActive = true;
|
||||
gLastX = x;
|
||||
gLastY = y;
|
||||
data->state = LV_INDEV_STATE_PR;
|
||||
data->point.x = x;
|
||||
data->point.y = y;
|
||||
} else {
|
||||
gTouchActive = false;
|
||||
data->state = LV_INDEV_STATE_REL;
|
||||
data->point.x = gLastX >= 0 ? gLastX : 0;
|
||||
data->point.y = gLastY >= 0 ? gLastY : 0;
|
||||
}
|
||||
|
||||
updateTouchLabel();
|
||||
}
|
||||
|
||||
static void buttonEventCb(lv_event_t *event) {
|
||||
if (lv_event_get_code(event) != LV_EVENT_CLICKED) {
|
||||
return;
|
||||
}
|
||||
|
||||
const char *name = (const char *)lv_event_get_user_data(event);
|
||||
gClickCount++;
|
||||
|
||||
char text[128];
|
||||
snprintf(text, sizeof(text), "Нажато: %s (%lu)", name, (unsigned long)gClickCount);
|
||||
lv_label_set_text(gStatusLabel, text);
|
||||
Serial.println(text);
|
||||
}
|
||||
|
||||
static lv_obj_t *makeButton(lv_obj_t *parent,
|
||||
const char *labelText,
|
||||
lv_coord_t x,
|
||||
lv_coord_t y,
|
||||
lv_coord_t w,
|
||||
lv_coord_t h,
|
||||
uint32_t bgColor) {
|
||||
lv_obj_t *btn = lv_btn_create(parent);
|
||||
lv_obj_set_size(btn, w, h);
|
||||
lv_obj_set_pos(btn, x, y);
|
||||
lv_obj_set_style_radius(btn, 16, 0);
|
||||
lv_obj_set_style_bg_color(btn, lv_color_hex(bgColor), 0);
|
||||
lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_border_width(btn, 2, 0);
|
||||
lv_obj_set_style_border_color(btn, lv_color_hex(0x6F8BA4), 0);
|
||||
lv_obj_set_style_shadow_width(btn, 0, 0);
|
||||
lv_obj_set_style_pad_all(btn, 4, 0);
|
||||
lv_obj_add_event_cb(btn, buttonEventCb, LV_EVENT_CLICKED, (void *)labelText);
|
||||
|
||||
lv_obj_t *label = lv_label_create(btn);
|
||||
lv_label_set_text(label, labelText);
|
||||
lv_obj_set_width(label, w - 12);
|
||||
lv_label_set_long_mode(label, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, 0);
|
||||
lv_obj_set_style_text_color(label, lv_color_hex(0xFFFFFF), 0);
|
||||
lv_obj_set_style_text_font(label, &lv_font_ru_18, 0);
|
||||
lv_obj_center(label);
|
||||
return btn;
|
||||
}
|
||||
|
||||
static void createUi() {
|
||||
lv_obj_t *screen = lv_scr_act();
|
||||
lv_obj_set_style_bg_color(screen, lv_color_hex(0x08131E), 0);
|
||||
lv_obj_set_style_bg_opa(screen, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_pad_all(screen, 0, 0);
|
||||
lv_obj_set_scrollbar_mode(screen, LV_SCROLLBAR_MODE_OFF);
|
||||
|
||||
lv_obj_t *title = lv_label_create(screen);
|
||||
lv_label_set_text(title, "Русский тест LVGL");
|
||||
lv_obj_set_style_text_font(title, &lv_font_ru_24, 0);
|
||||
lv_obj_set_style_text_color(title, lv_color_hex(0xFFFFFF), 0);
|
||||
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 10);
|
||||
|
||||
lv_obj_t *version = lv_label_create(screen);
|
||||
lv_label_set_text(version, "Версия: " TEST_VERSION);
|
||||
lv_obj_set_style_text_font(version, &lv_font_ru_18, 0);
|
||||
lv_obj_set_style_text_color(version, lv_color_hex(0xD8E0EA), 0);
|
||||
lv_obj_align(version, LV_ALIGN_TOP_MID, 0, 42);
|
||||
|
||||
lv_obj_t *hint = lv_label_create(screen);
|
||||
lv_label_set_text(hint, "Проверка кириллицы, длинных слов, кнопок и нажатий.");
|
||||
lv_obj_set_width(hint, 436);
|
||||
lv_label_set_long_mode(hint, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_style_text_font(hint, &lv_font_ru_18, 0);
|
||||
lv_obj_set_style_text_color(hint, lv_color_hex(0xD8E0EA), 0);
|
||||
lv_obj_align(hint, LV_ALIGN_TOP_MID, 0, 72);
|
||||
|
||||
makeButton(screen, "Статус", 22, 126, 136, 62, 0x355C7D);
|
||||
makeButton(screen, "Подключение", 172, 126, 136, 62, 0x2A9D8F);
|
||||
makeButton(screen, "Кошелёк", 322, 126, 136, 62, 0x457B9D);
|
||||
|
||||
makeButton(screen, "Запросы", 22, 204, 136, 62, 0xE76F51);
|
||||
makeButton(screen, "Настройки", 172, 204, 136, 62, 0x8D5A97);
|
||||
makeButton(screen, "Регистрация", 322, 204, 136, 62, 0x6A994E);
|
||||
|
||||
makeButton(screen, "Разрешить", 22, 282, 136, 62, 0xBC6C25);
|
||||
makeButton(screen, "Отклонить", 172, 282, 136, 62, 0xB56576);
|
||||
makeButton(screen, "Назад", 322, 282, 136, 62, 0x6C757D);
|
||||
|
||||
makeButton(screen, "Проверка переноса русского текста", 22, 360, 436, 64, 0x1D6F42);
|
||||
|
||||
lv_obj_t *statusPanel = lv_obj_create(screen);
|
||||
lv_obj_set_size(statusPanel, 436, 24);
|
||||
lv_obj_set_pos(statusPanel, 22, 428);
|
||||
lv_obj_set_style_radius(statusPanel, 10, 0);
|
||||
lv_obj_set_style_bg_color(statusPanel, lv_color_hex(0x162435), 0);
|
||||
lv_obj_set_style_bg_opa(statusPanel, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_border_width(statusPanel, 1, 0);
|
||||
lv_obj_set_style_border_color(statusPanel, lv_color_hex(0x4D6986), 0);
|
||||
lv_obj_set_style_pad_all(statusPanel, 2, 0);
|
||||
|
||||
gStatusLabel = lv_label_create(statusPanel);
|
||||
lv_label_set_text(gStatusLabel, "Нажато: ничего");
|
||||
lv_obj_set_style_text_font(gStatusLabel, &lv_font_ru_18, 0);
|
||||
lv_obj_set_style_text_color(gStatusLabel, lv_color_hex(0xFFFFFF), 0);
|
||||
lv_obj_center(gStatusLabel);
|
||||
|
||||
lv_obj_t *touchPanel = lv_obj_create(screen);
|
||||
lv_obj_set_size(touchPanel, 436, 24);
|
||||
lv_obj_set_pos(touchPanel, 22, 454);
|
||||
lv_obj_set_style_radius(touchPanel, 10, 0);
|
||||
lv_obj_set_style_bg_color(touchPanel, lv_color_hex(0x162435), 0);
|
||||
lv_obj_set_style_bg_opa(touchPanel, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_border_width(touchPanel, 1, 0);
|
||||
lv_obj_set_style_border_color(touchPanel, lv_color_hex(0x4D6986), 0);
|
||||
lv_obj_set_style_pad_all(touchPanel, 2, 0);
|
||||
|
||||
gTouchLabel = lv_label_create(touchPanel);
|
||||
lv_label_set_text(gTouchLabel, "Касание: нет x=-1 y=-1");
|
||||
lv_obj_set_style_text_font(gTouchLabel, &lv_font_ru_18, 0);
|
||||
lv_obj_set_style_text_color(gTouchLabel, lv_color_hex(0xD8E0EA), 0);
|
||||
lv_obj_center(gTouchLabel);
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
Wire.begin(PIN_I2C_SDA, PIN_I2C_SCL);
|
||||
|
||||
gfx->begin();
|
||||
gBus->writeC8D8(0x36, 0xA0);
|
||||
gfx->setBrightness(220);
|
||||
gfx->fillScreen(0x0000);
|
||||
|
||||
gTouch.setPins(PIN_TP_INT, -1);
|
||||
gTouch.begin(Wire, CST92XX_SLAVE_ADDRESS, PIN_I2C_SDA, PIN_I2C_SCL);
|
||||
gTouch.setMaxCoordinates(DISP_W, DISP_H);
|
||||
gTouch.setSwapXY(true);
|
||||
gTouch.setMirrorXY(true, false);
|
||||
|
||||
lv_init();
|
||||
|
||||
uint32_t screenWidth = gfx->width();
|
||||
uint32_t screenHeight = gfx->height();
|
||||
uint32_t bufPixels = screenWidth * 40;
|
||||
gBuf1 = (lv_color_t *)heap_caps_malloc(bufPixels * sizeof(lv_color_t), MALLOC_CAP_DMA);
|
||||
gBuf2 = (lv_color_t *)heap_caps_malloc(bufPixels * sizeof(lv_color_t), MALLOC_CAP_DMA);
|
||||
|
||||
lv_disp_draw_buf_init(&gDrawBuf, gBuf1, gBuf2, bufPixels);
|
||||
|
||||
static lv_disp_drv_t dispDrv;
|
||||
lv_disp_drv_init(&dispDrv);
|
||||
dispDrv.hor_res = screenWidth;
|
||||
dispDrv.ver_res = screenHeight;
|
||||
dispDrv.flush_cb = lvglFlushCb;
|
||||
dispDrv.draw_buf = &gDrawBuf;
|
||||
lv_disp_drv_register(&dispDrv);
|
||||
|
||||
static lv_indev_drv_t indevDrv;
|
||||
lv_indev_drv_init(&indevDrv);
|
||||
indevDrv.type = LV_INDEV_TYPE_POINTER;
|
||||
indevDrv.read_cb = lvglTouchRead;
|
||||
lv_indev_drv_register(&indevDrv);
|
||||
|
||||
const esp_timer_create_args_t tickArgs = {
|
||||
.callback = &lvglTick,
|
||||
.name = "lvgl_tick"};
|
||||
esp_timer_handle_t tickTimer = nullptr;
|
||||
esp_timer_create(&tickArgs, &tickTimer);
|
||||
esp_timer_start_periodic(tickTimer, LVGL_TICK_MS * 1000);
|
||||
|
||||
createUi();
|
||||
Serial.println("Русский LVGL font test ready: " TEST_VERSION);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
lv_timer_handler();
|
||||
delay(5);
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
#include <lvgl.h>
|
||||
#include <Arduino_GFX_Library.h>
|
||||
#include <TouchDrvCSTXXX.hpp>
|
||||
|
||||
// Подтверждено на устройстве: LVGL-рендер работает вместе с touch-путём из shine_homeserver_ui.
|
||||
|
||||
#define PIN_LCD_CS 12
|
||||
#define PIN_LCD_SCLK 38
|
||||
#define PIN_LCD_D0 4
|
||||
#define PIN_LCD_D1 5
|
||||
#define PIN_LCD_D2 6
|
||||
#define PIN_LCD_D3 7
|
||||
#define PIN_LCD_RST 2
|
||||
#define PIN_I2C_SDA 15
|
||||
#define PIN_I2C_SCL 14
|
||||
#define PIN_TP_INT 11
|
||||
|
||||
#define DISP_W 480
|
||||
#define DISP_H 480
|
||||
#define LVGL_TICK_MS 2
|
||||
#define TEST_VERSION "v1-subtouch"
|
||||
|
||||
static lv_disp_draw_buf_t gDrawBuf;
|
||||
static lv_color_t *gBuf1 = nullptr;
|
||||
static lv_color_t *gBuf2 = nullptr;
|
||||
static lv_obj_t *gStatusLabel = nullptr;
|
||||
static lv_obj_t *gTouchLabel = nullptr;
|
||||
static lv_obj_t *gVersionLabel = nullptr;
|
||||
static uint32_t gClickCount = 0;
|
||||
static bool gTouchActive = false;
|
||||
static int16_t gLastX = -1;
|
||||
static int16_t gLastY = -1;
|
||||
|
||||
Arduino_DataBus *gBus = new Arduino_ESP32QSPI(
|
||||
PIN_LCD_CS, PIN_LCD_SCLK, PIN_LCD_D0, PIN_LCD_D1, PIN_LCD_D2, PIN_LCD_D3);
|
||||
Arduino_CO5300 *gfx = new Arduino_CO5300(
|
||||
gBus, PIN_LCD_RST, 0, DISP_W, DISP_H, 0, 0, 0, 0);
|
||||
TouchDrvCST92xx gTouch;
|
||||
|
||||
static void lvglFlushCb(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *colorP) {
|
||||
uint32_t w = area->x2 - area->x1 + 1;
|
||||
uint32_t h = area->y2 - area->y1 + 1;
|
||||
#if (LV_COLOR_16_SWAP != 0)
|
||||
gfx->draw16bitBeRGBBitmap(area->x1, area->y1, (uint16_t *)&colorP->full, w, h);
|
||||
#else
|
||||
gfx->draw16bitRGBBitmap(area->x1, area->y1, (uint16_t *)&colorP->full, w, h);
|
||||
#endif
|
||||
lv_disp_flush_ready(disp);
|
||||
}
|
||||
|
||||
static void lvglTick(void *arg) {
|
||||
LV_UNUSED(arg);
|
||||
lv_tick_inc(LVGL_TICK_MS);
|
||||
}
|
||||
|
||||
static void updateTouchLabel() {
|
||||
if (!gTouchLabel) {
|
||||
return;
|
||||
}
|
||||
|
||||
char text[96];
|
||||
if (gTouchActive) {
|
||||
snprintf(text, sizeof(text), "Touch: pressed x=%d y=%d", gLastX, gLastY);
|
||||
} else {
|
||||
snprintf(text, sizeof(text), "Touch: released x=%d y=%d", gLastX, gLastY);
|
||||
}
|
||||
lv_label_set_text(gTouchLabel, text);
|
||||
}
|
||||
|
||||
static void lvglTouchRead(lv_indev_drv_t *indevDrv, lv_indev_data_t *data) {
|
||||
LV_UNUSED(indevDrv);
|
||||
int16_t x = 0;
|
||||
int16_t y = 0;
|
||||
bool touching = gTouch.getPoint(&x, &y, 1) > 0;
|
||||
|
||||
if (touching) {
|
||||
gTouchActive = true;
|
||||
gLastX = x;
|
||||
gLastY = y;
|
||||
data->state = LV_INDEV_STATE_PR;
|
||||
data->point.x = x;
|
||||
data->point.y = y;
|
||||
} else {
|
||||
gTouchActive = false;
|
||||
data->state = LV_INDEV_STATE_REL;
|
||||
data->point.x = gLastX >= 0 ? gLastX : 0;
|
||||
data->point.y = gLastY >= 0 ? gLastY : 0;
|
||||
}
|
||||
|
||||
updateTouchLabel();
|
||||
}
|
||||
|
||||
static void buttonEventCb(lv_event_t *event) {
|
||||
if (lv_event_get_code(event) != LV_EVENT_CLICKED) {
|
||||
return;
|
||||
}
|
||||
|
||||
const char *name = (const char *)lv_event_get_user_data(event);
|
||||
gClickCount++;
|
||||
|
||||
char text[96];
|
||||
snprintf(text, sizeof(text), "Pressed: %s (#%lu)", name, (unsigned long)gClickCount);
|
||||
lv_label_set_text(gStatusLabel, text);
|
||||
Serial.println(text);
|
||||
}
|
||||
|
||||
static lv_obj_t *makeButton(lv_obj_t *parent,
|
||||
const char *labelText,
|
||||
lv_coord_t x,
|
||||
lv_coord_t y,
|
||||
lv_coord_t w,
|
||||
lv_coord_t h,
|
||||
uint32_t bgColor) {
|
||||
lv_obj_t *btn = lv_btn_create(parent);
|
||||
lv_obj_set_size(btn, w, h);
|
||||
lv_obj_set_pos(btn, x, y);
|
||||
lv_obj_set_style_radius(btn, 16, 0);
|
||||
lv_obj_set_style_bg_color(btn, lv_color_hex(bgColor), 0);
|
||||
lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_border_width(btn, 2, 0);
|
||||
lv_obj_set_style_border_color(btn, lv_color_hex(0x6F8BA4), 0);
|
||||
lv_obj_set_style_shadow_width(btn, 0, 0);
|
||||
lv_obj_add_event_cb(btn, buttonEventCb, LV_EVENT_CLICKED, (void *)labelText);
|
||||
|
||||
lv_obj_t *label = lv_label_create(btn);
|
||||
lv_label_set_text(label, labelText);
|
||||
lv_obj_set_style_text_color(label, lv_color_hex(0xFFFFFF), 0);
|
||||
lv_obj_set_style_text_font(label, &lv_font_montserrat_18, 0);
|
||||
lv_obj_center(label);
|
||||
return btn;
|
||||
}
|
||||
|
||||
static void createUi() {
|
||||
lv_obj_t *screen = lv_scr_act();
|
||||
lv_obj_set_style_bg_color(screen, lv_color_hex(0x07111C), 0);
|
||||
lv_obj_set_style_bg_opa(screen, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_pad_all(screen, 0, 0);
|
||||
lv_obj_set_scrollbar_mode(screen, LV_SCROLLBAR_MODE_OFF);
|
||||
|
||||
gVersionLabel = lv_label_create(screen);
|
||||
lv_label_set_text(gVersionLabel, "LVGL + subserver touch " TEST_VERSION);
|
||||
lv_obj_set_style_text_font(gVersionLabel, &lv_font_montserrat_18, 0);
|
||||
lv_obj_set_style_text_color(gVersionLabel, lv_color_hex(0xFFFFFF), 0);
|
||||
lv_obj_align(gVersionLabel, LV_ALIGN_TOP_MID, 0, 12);
|
||||
|
||||
lv_obj_t *subtitle = lv_label_create(screen);
|
||||
lv_label_set_text(subtitle, "Touch path comes from shine_homeserver_ui. Tap buttons and watch status.");
|
||||
lv_obj_set_width(subtitle, 436);
|
||||
lv_label_set_long_mode(subtitle, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_style_text_font(subtitle, &lv_font_montserrat_14, 0);
|
||||
lv_obj_set_style_text_color(subtitle, lv_color_hex(0xD6DEE7), 0);
|
||||
lv_obj_align(subtitle, LV_ALIGN_TOP_MID, 0, 42);
|
||||
|
||||
makeButton(screen, "Status", 22, 110, 136, 62, 0x355C7D);
|
||||
makeButton(screen, "Connect", 172, 110, 136, 62, 0x2A9D8F);
|
||||
makeButton(screen, "Wallet", 322, 110, 136, 62, 0x457B9D);
|
||||
|
||||
makeButton(screen, "Requests", 22, 188, 136, 62, 0xE76F51);
|
||||
makeButton(screen, "Settings", 172, 188, 136, 62, 0x8D5A97);
|
||||
makeButton(screen, "Register", 322, 188, 136, 62, 0x6A994E);
|
||||
|
||||
makeButton(screen, "Approve", 22, 266, 136, 62, 0xBC6C25);
|
||||
makeButton(screen, "Reject", 172, 266, 136, 62, 0xB56576);
|
||||
makeButton(screen, "Back", 322, 266, 136, 62, 0x6C757D);
|
||||
|
||||
makeButton(screen, "Bottom Action", 22, 346, 436, 68, 0x1D6F42);
|
||||
|
||||
lv_obj_t *statusPanel = lv_obj_create(screen);
|
||||
lv_obj_set_size(statusPanel, 436, 26);
|
||||
lv_obj_set_pos(statusPanel, 22, 420);
|
||||
lv_obj_set_style_radius(statusPanel, 10, 0);
|
||||
lv_obj_set_style_bg_color(statusPanel, lv_color_hex(0x162435), 0);
|
||||
lv_obj_set_style_bg_opa(statusPanel, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_border_width(statusPanel, 1, 0);
|
||||
lv_obj_set_style_border_color(statusPanel, lv_color_hex(0x4D6986), 0);
|
||||
lv_obj_set_style_pad_all(statusPanel, 2, 0);
|
||||
|
||||
gStatusLabel = lv_label_create(statusPanel);
|
||||
lv_label_set_text(gStatusLabel, "Pressed: none");
|
||||
lv_obj_set_style_text_font(gStatusLabel, &lv_font_montserrat_14, 0);
|
||||
lv_obj_set_style_text_color(gStatusLabel, lv_color_hex(0xFFFFFF), 0);
|
||||
lv_obj_center(gStatusLabel);
|
||||
|
||||
lv_obj_t *touchPanel = lv_obj_create(screen);
|
||||
lv_obj_set_size(touchPanel, 436, 26);
|
||||
lv_obj_set_pos(touchPanel, 22, 450);
|
||||
lv_obj_set_style_radius(touchPanel, 10, 0);
|
||||
lv_obj_set_style_bg_color(touchPanel, lv_color_hex(0x162435), 0);
|
||||
lv_obj_set_style_bg_opa(touchPanel, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_border_width(touchPanel, 1, 0);
|
||||
lv_obj_set_style_border_color(touchPanel, lv_color_hex(0x4D6986), 0);
|
||||
lv_obj_set_style_pad_all(touchPanel, 2, 0);
|
||||
|
||||
gTouchLabel = lv_label_create(touchPanel);
|
||||
lv_label_set_text(gTouchLabel, "Touch: released x=-1 y=-1");
|
||||
lv_obj_set_style_text_font(gTouchLabel, &lv_font_montserrat_14, 0);
|
||||
lv_obj_set_style_text_color(gTouchLabel, lv_color_hex(0xD6DEE7), 0);
|
||||
lv_obj_center(gTouchLabel);
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
Wire.begin(PIN_I2C_SDA, PIN_I2C_SCL);
|
||||
|
||||
gfx->begin();
|
||||
gBus->writeC8D8(0x36, 0xA0);
|
||||
gfx->setBrightness(220);
|
||||
gfx->fillScreen(0x0000);
|
||||
|
||||
gTouch.setPins(PIN_TP_INT, -1);
|
||||
gTouch.begin(Wire, CST92XX_SLAVE_ADDRESS, PIN_I2C_SDA, PIN_I2C_SCL);
|
||||
gTouch.setMaxCoordinates(DISP_W, DISP_H);
|
||||
gTouch.setSwapXY(true);
|
||||
gTouch.setMirrorXY(true, false);
|
||||
|
||||
lv_init();
|
||||
|
||||
uint32_t screenWidth = gfx->width();
|
||||
uint32_t screenHeight = gfx->height();
|
||||
uint32_t bufPixels = screenWidth * 40;
|
||||
gBuf1 = (lv_color_t *)heap_caps_malloc(bufPixels * sizeof(lv_color_t), MALLOC_CAP_DMA);
|
||||
gBuf2 = (lv_color_t *)heap_caps_malloc(bufPixels * sizeof(lv_color_t), MALLOC_CAP_DMA);
|
||||
|
||||
lv_disp_draw_buf_init(&gDrawBuf, gBuf1, gBuf2, bufPixels);
|
||||
|
||||
static lv_disp_drv_t dispDrv;
|
||||
lv_disp_drv_init(&dispDrv);
|
||||
dispDrv.hor_res = screenWidth;
|
||||
dispDrv.ver_res = screenHeight;
|
||||
dispDrv.flush_cb = lvglFlushCb;
|
||||
dispDrv.draw_buf = &gDrawBuf;
|
||||
lv_disp_drv_register(&dispDrv);
|
||||
|
||||
static lv_indev_drv_t indevDrv;
|
||||
lv_indev_drv_init(&indevDrv);
|
||||
indevDrv.type = LV_INDEV_TYPE_POINTER;
|
||||
indevDrv.read_cb = lvglTouchRead;
|
||||
lv_indev_drv_register(&indevDrv);
|
||||
|
||||
const esp_timer_create_args_t tickArgs = {
|
||||
.callback = &lvglTick,
|
||||
.name = "lvgl_tick"};
|
||||
esp_timer_handle_t tickTimer = nullptr;
|
||||
esp_timer_create(&tickArgs, &tickTimer);
|
||||
esp_timer_start_periodic(tickTimer, LVGL_TICK_MS * 1000);
|
||||
|
||||
createUi();
|
||||
Serial.println("LVGL + subserver touch test ready: " TEST_VERSION);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
lv_timer_handler();
|
||||
delay(5);
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
#include <lvgl.h>
|
||||
#include <Arduino_GFX_Library.h>
|
||||
#include "pin_config.h"
|
||||
#include "TouchDrvCSTXXX.hpp"
|
||||
|
||||
#define EXAMPLE_LVGL_TICK_PERIOD_MS 2
|
||||
|
||||
static lv_disp_draw_buf_t gDrawBuf;
|
||||
static lv_color_t *gBuf1 = nullptr;
|
||||
static lv_color_t *gBuf2 = nullptr;
|
||||
static lv_obj_t *gRawLabel = nullptr;
|
||||
static lv_obj_t *gEventLabel = nullptr;
|
||||
static lv_obj_t *gMarker = nullptr;
|
||||
|
||||
static TouchDrvCST92xx gTouch;
|
||||
static int16_t gTouchX[5];
|
||||
static int16_t gTouchY[5];
|
||||
static bool gTouchPressed = false;
|
||||
static uint32_t gRawTouchCount = 0;
|
||||
static uint32_t gLvglButtonCount = 0;
|
||||
|
||||
Arduino_DataBus *gBus = new Arduino_ESP32QSPI(
|
||||
LCD_CS, LCD_SCLK, LCD_SDIO0, LCD_SDIO1, LCD_SDIO2, LCD_SDIO3);
|
||||
Arduino_CO5300 *gfx = new Arduino_CO5300(
|
||||
gBus, LCD_RESET, 0, LCD_WIDTH, LCD_HEIGHT, 0, 0, 0, 0);
|
||||
|
||||
static void lvglFlushCb(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *colorP) {
|
||||
uint32_t w = area->x2 - area->x1 + 1;
|
||||
uint32_t h = area->y2 - area->y1 + 1;
|
||||
#if (LV_COLOR_16_SWAP != 0)
|
||||
gfx->draw16bitBeRGBBitmap(area->x1, area->y1, (uint16_t *)&colorP->full, w, h);
|
||||
#else
|
||||
gfx->draw16bitRGBBitmap(area->x1, area->y1, (uint16_t *)&colorP->full, w, h);
|
||||
#endif
|
||||
lv_disp_flush_ready(disp);
|
||||
}
|
||||
|
||||
static void lvglTick(void *arg) {
|
||||
LV_UNUSED(arg);
|
||||
lv_tick_inc(EXAMPLE_LVGL_TICK_PERIOD_MS);
|
||||
}
|
||||
|
||||
static void lvglTouchRead(lv_indev_drv_t *indevDrv, lv_indev_data_t *data) {
|
||||
LV_UNUSED(indevDrv);
|
||||
uint8_t touched = gTouch.getPoint(gTouchX, gTouchY, gTouch.getSupportTouchPoint());
|
||||
if (touched > 0) {
|
||||
data->state = LV_INDEV_STATE_PR;
|
||||
data->point.x = gTouchX[0];
|
||||
data->point.y = gTouchY[0];
|
||||
return;
|
||||
}
|
||||
data->state = LV_INDEV_STATE_REL;
|
||||
}
|
||||
|
||||
static void bigButtonEventCb(lv_event_t *event) {
|
||||
if (lv_event_get_code(event) != LV_EVENT_CLICKED) {
|
||||
return;
|
||||
}
|
||||
gLvglButtonCount++;
|
||||
String status = "LVGL button clicked: ";
|
||||
status += String(gLvglButtonCount);
|
||||
lv_label_set_text(gEventLabel, status.c_str());
|
||||
Serial.println(status);
|
||||
}
|
||||
|
||||
static void updateRawTouchState() {
|
||||
uint8_t touched = gTouch.getPoint(gTouchX, gTouchY, gTouch.getSupportTouchPoint());
|
||||
if (touched > 0) {
|
||||
gTouchPressed = true;
|
||||
gRawTouchCount++;
|
||||
|
||||
String line1 = "RAW pressed";
|
||||
String line2 = "x=" + String(gTouchX[0]) + " y=" + String(gTouchY[0]) + " n=" + String(gRawTouchCount);
|
||||
String text = line1 + "\n" + line2;
|
||||
lv_label_set_text(gRawLabel, text.c_str());
|
||||
|
||||
int markerX = gTouchX[0] - 8;
|
||||
int markerY = gTouchY[0] - 8;
|
||||
if (markerX < 0) markerX = 0;
|
||||
if (markerY < 0) markerY = 0;
|
||||
lv_obj_set_pos(gMarker, markerX, markerY);
|
||||
lv_obj_clear_flag(gMarker, LV_OBJ_FLAG_HIDDEN);
|
||||
return;
|
||||
}
|
||||
|
||||
if (gTouchPressed) {
|
||||
gTouchPressed = false;
|
||||
lv_label_set_text(gRawLabel, "RAW released\nTouch ended");
|
||||
lv_obj_add_flag(gMarker, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
}
|
||||
|
||||
static void createUi() {
|
||||
lv_obj_t *screen = lv_scr_act();
|
||||
lv_obj_set_style_bg_color(screen, lv_color_hex(0x000000), 0);
|
||||
lv_obj_set_style_bg_opa(screen, LV_OPA_COVER, 0);
|
||||
|
||||
lv_obj_t *title = lv_label_create(screen);
|
||||
lv_label_set_text(title, "LVGL TOUCH DEBUG");
|
||||
lv_obj_set_style_text_font(title, &lv_font_montserrat_22, 0);
|
||||
lv_obj_set_style_text_color(title, lv_color_hex(0xFFFFFF), 0);
|
||||
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 12);
|
||||
|
||||
gRawLabel = lv_label_create(screen);
|
||||
lv_label_set_text(gRawLabel, "RAW released\nTouch not seen yet");
|
||||
lv_obj_set_style_text_font(gRawLabel, &lv_font_montserrat_18, 0);
|
||||
lv_obj_set_style_text_color(gRawLabel, lv_color_hex(0x9EE493), 0);
|
||||
lv_obj_align(gRawLabel, LV_ALIGN_TOP_LEFT, 16, 56);
|
||||
|
||||
gEventLabel = lv_label_create(screen);
|
||||
lv_label_set_text(gEventLabel, "LVGL button clicked: 0");
|
||||
lv_obj_set_style_text_font(gEventLabel, &lv_font_montserrat_18, 0);
|
||||
lv_obj_set_style_text_color(gEventLabel, lv_color_hex(0xFFD166), 0);
|
||||
lv_obj_align(gEventLabel, LV_ALIGN_TOP_LEFT, 16, 118);
|
||||
|
||||
lv_obj_t *btn = lv_btn_create(screen);
|
||||
lv_obj_set_size(btn, 360, 110);
|
||||
lv_obj_align(btn, LV_ALIGN_CENTER, 0, 36);
|
||||
lv_obj_set_style_radius(btn, 18, 0);
|
||||
lv_obj_set_style_bg_color(btn, lv_color_hex(0x2A9D8F), 0);
|
||||
lv_obj_set_style_border_width(btn, 3, 0);
|
||||
lv_obj_set_style_border_color(btn, lv_color_hex(0xBDE0FE), 0);
|
||||
lv_obj_add_event_cb(btn, bigButtonEventCb, LV_EVENT_CLICKED, nullptr);
|
||||
|
||||
lv_obj_t *btnLabel = lv_label_create(btn);
|
||||
lv_label_set_text(btnLabel, "Tap Here");
|
||||
lv_obj_set_style_text_font(btnLabel, &lv_font_montserrat_24, 0);
|
||||
lv_obj_set_style_text_color(btnLabel, lv_color_hex(0xFFFFFF), 0);
|
||||
lv_obj_center(btnLabel);
|
||||
|
||||
lv_obj_t *hint = lv_label_create(screen);
|
||||
lv_label_set_text(hint, "Need both: RAW coords change and LVGL button click.");
|
||||
lv_obj_set_width(hint, 430);
|
||||
lv_label_set_long_mode(hint, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_style_text_font(hint, &lv_font_montserrat_16, 0);
|
||||
lv_obj_set_style_text_color(hint, lv_color_hex(0xD9D9D9), 0);
|
||||
lv_obj_align(hint, LV_ALIGN_BOTTOM_MID, 0, -34);
|
||||
|
||||
gMarker = lv_obj_create(screen);
|
||||
lv_obj_set_size(gMarker, 16, 16);
|
||||
lv_obj_set_style_radius(gMarker, LV_RADIUS_CIRCLE, 0);
|
||||
lv_obj_set_style_bg_color(gMarker, lv_color_hex(0xFF4D6D), 0);
|
||||
lv_obj_set_style_border_width(gMarker, 2, 0);
|
||||
lv_obj_set_style_border_color(gMarker, lv_color_hex(0xFFFFFF), 0);
|
||||
lv_obj_add_flag(gMarker, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
Wire.begin(IIC_SDA, IIC_SCL);
|
||||
|
||||
pinMode(TP_RST, OUTPUT);
|
||||
pinMode(TP_INT, INPUT);
|
||||
digitalWrite(TP_RST, LOW);
|
||||
delay(30);
|
||||
digitalWrite(TP_RST, HIGH);
|
||||
delay(50);
|
||||
delay(1000);
|
||||
|
||||
gTouch.setPins(TP_RST, TP_INT);
|
||||
bool touchOk = gTouch.begin(Wire, 0x5A, IIC_SDA, IIC_SCL);
|
||||
if (!touchOk) {
|
||||
Serial.println("Touch init failed");
|
||||
} else {
|
||||
Serial.print("Touch model: ");
|
||||
Serial.println(gTouch.getModelName());
|
||||
gTouch.sleep();
|
||||
gTouch.reset();
|
||||
gTouch.setMaxCoordinates(480, 480);
|
||||
gTouch.setSwapXY(true);
|
||||
gTouch.setMirrorXY(true, false);
|
||||
}
|
||||
|
||||
gfx->begin();
|
||||
gBus->writeC8D8(0x36, 0xA0);
|
||||
gfx->setBrightness(220);
|
||||
gfx->fillScreen(0x0000);
|
||||
|
||||
lv_init();
|
||||
|
||||
uint32_t screenWidth = gfx->width();
|
||||
uint32_t screenHeight = gfx->height();
|
||||
gBuf1 = (lv_color_t *)heap_caps_malloc(screenWidth * screenHeight / 4 * sizeof(lv_color_t), MALLOC_CAP_DMA);
|
||||
gBuf2 = (lv_color_t *)heap_caps_malloc(screenWidth * screenHeight / 4 * sizeof(lv_color_t), MALLOC_CAP_DMA);
|
||||
lv_disp_draw_buf_init(&gDrawBuf, gBuf1, gBuf2, screenWidth * screenHeight / 4);
|
||||
|
||||
static lv_disp_drv_t dispDrv;
|
||||
lv_disp_drv_init(&dispDrv);
|
||||
dispDrv.hor_res = screenWidth;
|
||||
dispDrv.ver_res = screenHeight;
|
||||
dispDrv.flush_cb = lvglFlushCb;
|
||||
dispDrv.draw_buf = &gDrawBuf;
|
||||
lv_disp_drv_register(&dispDrv);
|
||||
|
||||
static lv_indev_drv_t indevDrv;
|
||||
lv_indev_drv_init(&indevDrv);
|
||||
indevDrv.type = LV_INDEV_TYPE_POINTER;
|
||||
indevDrv.read_cb = lvglTouchRead;
|
||||
lv_indev_drv_register(&indevDrv);
|
||||
|
||||
const esp_timer_create_args_t lvglTickTimerArgs = {
|
||||
.callback = &lvglTick,
|
||||
.name = "lvgl_tick"};
|
||||
esp_timer_handle_t lvglTickTimer = nullptr;
|
||||
esp_timer_create(&lvglTickTimerArgs, &lvglTickTimer);
|
||||
esp_timer_start_periodic(lvglTickTimer, EXAMPLE_LVGL_TICK_PERIOD_MS * 1000);
|
||||
|
||||
createUi();
|
||||
Serial.println("LVGL touch debug test ready");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
updateRawTouchState();
|
||||
lv_timer_handler();
|
||||
delay(5);
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
#include <Arduino_GFX_Library.h>
|
||||
#include <U8g2lib.h>
|
||||
|
||||
#define PIN_LCD_CS 12
|
||||
#define PIN_LCD_SCLK 38
|
||||
#define PIN_LCD_D0 4
|
||||
#define PIN_LCD_D1 5
|
||||
#define PIN_LCD_D2 6
|
||||
#define PIN_LCD_D3 7
|
||||
#define PIN_LCD_RST 2
|
||||
#define PIN_I2C_SDA 15
|
||||
#define PIN_I2C_SCL 14
|
||||
|
||||
#define DISP_W 480
|
||||
#define DISP_H 480
|
||||
|
||||
#define C_BG 0x0841u
|
||||
#define C_PANEL 0x1082u
|
||||
#define C_CARD 0x18C3u
|
||||
#define C_BORDER 0x39C7u
|
||||
#define C_TEXT 0xFFFFu
|
||||
#define C_MUTE 0xBDF7u
|
||||
#define C_OK 0x3666u
|
||||
#define C_WARN 0xECA0u
|
||||
#define C_BAD 0xD145u
|
||||
#define C_BUTTON 0x2145u
|
||||
#define C_BUTTON2 0x3186u
|
||||
|
||||
#define FONT_HEAD u8g2_font_10x20_t_cyrillic
|
||||
#define FONT_BODY u8g2_font_9x15_t_cyrillic
|
||||
#define FONT_SMALL u8g2_font_6x13_t_cyrillic
|
||||
|
||||
Arduino_DataBus *gBus = new Arduino_ESP32QSPI(
|
||||
PIN_LCD_CS, PIN_LCD_SCLK, PIN_LCD_D0, PIN_LCD_D1, PIN_LCD_D2, PIN_LCD_D3);
|
||||
Arduino_CO5300 *gfx = new Arduino_CO5300(
|
||||
gBus, PIN_LCD_RST, 0, DISP_W, DISP_H, 0, 0, 0, 0);
|
||||
|
||||
static void drawPanel(int x, int y, int w, int h, uint16_t fill, uint16_t border, int radius = 10) {
|
||||
gfx->fillRoundRect(x, y, w, h, radius, fill);
|
||||
gfx->drawRoundRect(x, y, w, h, radius, border);
|
||||
}
|
||||
|
||||
static void drawDefaultText(int x, int y, const char *text, uint16_t color, uint8_t size = 1) {
|
||||
gfx->setFont();
|
||||
gfx->setTextSize(size);
|
||||
gfx->setTextColor(color);
|
||||
gfx->setCursor(x, y);
|
||||
gfx->print(text);
|
||||
}
|
||||
|
||||
static void drawU8Text(int x, int y, const String &text, uint16_t color, const uint8_t *font) {
|
||||
gfx->setTextSize(1);
|
||||
gfx->setFont(font);
|
||||
gfx->setTextColor(color);
|
||||
gfx->setCursor(x, y);
|
||||
gfx->print(text);
|
||||
}
|
||||
|
||||
static void drawButton(int x, int y, int w, int h, uint16_t fill, const String &label, const uint8_t *font, bool useDefaultFont = false) {
|
||||
drawPanel(x, y, w, h, fill, C_BORDER, 12);
|
||||
if (useDefaultFont) {
|
||||
drawDefaultText(x + 14, y + 30, label.c_str(), C_TEXT, 2);
|
||||
return;
|
||||
}
|
||||
drawU8Text(x + 14, y + 32, label, C_TEXT, font);
|
||||
}
|
||||
|
||||
static void drawScreen() {
|
||||
gfx->fillScreen(C_BG);
|
||||
|
||||
drawPanel(12, 12, 456, 456, C_PANEL, C_BORDER, 16);
|
||||
drawDefaultText(28, 42, "TEXT TEST 123", C_TEXT, 2);
|
||||
drawU8Text(28, 72, "Default ASCII above", C_MUTE, (const uint8_t *)FONT_SMALL);
|
||||
|
||||
drawPanel(24, 90, 432, 72, C_CARD, C_BORDER, 12);
|
||||
drawDefaultText(38, 118, "A: Default font ABC123", C_TEXT, 2);
|
||||
drawDefaultText(38, 146, "Visible? then base path OK", C_WARN, 1);
|
||||
|
||||
drawPanel(24, 174, 432, 84, C_CARD, C_BORDER, 12);
|
||||
drawU8Text(38, 204, "B: U8g2 ASCII abc123", C_TEXT, (const uint8_t *)FONT_BODY);
|
||||
drawU8Text(38, 230, "C: Русский текст 123", C_OK, (const uint8_t *)FONT_BODY);
|
||||
|
||||
drawPanel(24, 270, 432, 84, C_CARD, C_BORDER, 12);
|
||||
drawU8Text(38, 298, "D: Мелкий шрифт кнопок", C_TEXT, (const uint8_t *)FONT_SMALL);
|
||||
drawU8Text(38, 320, "Если это видно, FONT_SMALL жив", C_MUTE, (const uint8_t *)FONT_SMALL);
|
||||
|
||||
drawButton(24, 368, 128, 72, C_BUTTON, "BTN 1", nullptr, true);
|
||||
drawButton(176, 368, 128, 72, C_OK, "abc123", (const uint8_t *)FONT_BODY);
|
||||
drawButton(328, 368, 128, 72, C_BUTTON2, "Русский", (const uint8_t *)FONT_BODY);
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
Wire.begin(PIN_I2C_SDA, PIN_I2C_SCL);
|
||||
|
||||
gfx->begin();
|
||||
gBus->writeC8D8(0x36, 0xA0);
|
||||
gfx->setBrightness(220);
|
||||
gfx->setUTF8Print(true);
|
||||
|
||||
drawScreen();
|
||||
|
||||
Serial.println();
|
||||
Serial.println("=== text_render_test ===");
|
||||
Serial.println("A: default Arduino_GFX font");
|
||||
Serial.println("B: U8g2 ASCII");
|
||||
Serial.println("C: U8g2 Cyrillic");
|
||||
Serial.println("Buttons: default / ASCII / Cyrillic");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
delay(1000);
|
||||
}
|
||||
@@ -0,0 +1,784 @@
|
||||
/**
|
||||
* @file lv_conf.h
|
||||
* Configuration file for v8.4.0
|
||||
*/
|
||||
|
||||
/*
|
||||
* Copy this file as `lv_conf.h`
|
||||
* 1. simply next to the `lvgl` folder
|
||||
* 2. or any other places and
|
||||
* - define `LV_CONF_INCLUDE_SIMPLE`
|
||||
* - add the path as include path
|
||||
*/
|
||||
|
||||
/* clang-format off */
|
||||
#if 1 /*Set it to "1" to enable content*/
|
||||
|
||||
#ifndef LV_CONF_H
|
||||
#define LV_CONF_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/*====================
|
||||
COLOR SETTINGS
|
||||
*====================*/
|
||||
|
||||
/*Color depth: 1 (1 byte per pixel), 8 (RGB332), 16 (RGB565), 32 (ARGB8888)*/
|
||||
#define LV_COLOR_DEPTH 16
|
||||
|
||||
/*Swap the 2 bytes of RGB565 color. Useful if the display has an 8-bit interface (e.g. SPI)*/
|
||||
#define LV_COLOR_16_SWAP 0
|
||||
|
||||
/*Enable features to draw on transparent background.
|
||||
*It's required if opa, and transform_* style properties are used.
|
||||
*Can be also used if the UI is above another layer, e.g. an OSD menu or video player.*/
|
||||
#define LV_COLOR_SCREEN_TRANSP 0
|
||||
|
||||
/* Adjust color mix functions rounding. GPUs might calculate color mix (blending) differently.
|
||||
* 0: round down, 64: round up from x.75, 128: round up from half, 192: round up from x.25, 254: round up */
|
||||
#define LV_COLOR_MIX_ROUND_OFS 0
|
||||
|
||||
/*Images pixels with this color will not be drawn if they are chroma keyed)*/
|
||||
#define LV_COLOR_CHROMA_KEY lv_color_hex(0x00ff00) /*pure green*/
|
||||
|
||||
/*=========================
|
||||
MEMORY SETTINGS
|
||||
*=========================*/
|
||||
|
||||
/*1: use custom malloc/free, 0: use the built-in `lv_mem_alloc()` and `lv_mem_free()`*/
|
||||
#define LV_MEM_CUSTOM 0
|
||||
#if LV_MEM_CUSTOM == 0
|
||||
/*Size of the memory available for `lv_mem_alloc()` in bytes (>= 2kB)*/
|
||||
#define LV_MEM_SIZE (48U * 1024U) /*[bytes]*/
|
||||
|
||||
/*Set an address for the memory pool instead of allocating it as a normal array. Can be in external SRAM too.*/
|
||||
#define LV_MEM_ADR 0 /*0: unused*/
|
||||
/*Instead of an address give a memory allocator that will be called to get a memory pool for LVGL. E.g. my_malloc*/
|
||||
#if LV_MEM_ADR == 0
|
||||
#undef LV_MEM_POOL_INCLUDE
|
||||
#undef LV_MEM_POOL_ALLOC
|
||||
#endif
|
||||
|
||||
#else /*LV_MEM_CUSTOM*/
|
||||
#define LV_MEM_CUSTOM_INCLUDE <stdlib.h> /*Header for the dynamic memory function*/
|
||||
#define LV_MEM_CUSTOM_ALLOC malloc
|
||||
#define LV_MEM_CUSTOM_FREE free
|
||||
#define LV_MEM_CUSTOM_REALLOC realloc
|
||||
#endif /*LV_MEM_CUSTOM*/
|
||||
|
||||
/*Number of the intermediate memory buffer used during rendering and other internal processing mechanisms.
|
||||
*You will see an error log message if there wasn't enough buffers. */
|
||||
#define LV_MEM_BUF_MAX_NUM 16
|
||||
|
||||
/*Use the standard `memcpy` and `memset` instead of LVGL's own functions. (Might or might not be faster).*/
|
||||
#define LV_MEMCPY_MEMSET_STD 0
|
||||
|
||||
/*====================
|
||||
HAL SETTINGS
|
||||
*====================*/
|
||||
|
||||
/*Default display refresh period. LVG will redraw changed areas with this period time*/
|
||||
#define LV_DISP_DEF_REFR_PERIOD 10 /*[ms]*/
|
||||
|
||||
/*Input device read period in milliseconds*/
|
||||
#define LV_INDEV_DEF_READ_PERIOD 10 /*[ms]*/
|
||||
|
||||
/*Use a custom tick source that tells the elapsed time in milliseconds.
|
||||
*It removes the need to manually update the tick with `lv_tick_inc()`)*/
|
||||
#define LV_TICK_CUSTOM 0
|
||||
#if LV_TICK_CUSTOM
|
||||
#define LV_TICK_CUSTOM_INCLUDE "Arduino.h" /*Header for the system time function*/
|
||||
#define LV_TICK_CUSTOM_SYS_TIME_EXPR (millis()) /*Expression evaluating to current system time in ms*/
|
||||
/*If using lvgl as ESP32 component*/
|
||||
// #define LV_TICK_CUSTOM_INCLUDE "esp_timer.h"
|
||||
// #define LV_TICK_CUSTOM_SYS_TIME_EXPR ((esp_timer_get_time() / 1000LL))
|
||||
#endif /*LV_TICK_CUSTOM*/
|
||||
|
||||
/*Default Dot Per Inch. Used to initialize default sizes such as widgets sized, style paddings.
|
||||
*(Not so important, you can adjust it to modify default sizes and spaces)*/
|
||||
#define LV_DPI_DEF 130 /*[px/inch]*/
|
||||
|
||||
/*=======================
|
||||
* FEATURE CONFIGURATION
|
||||
*=======================*/
|
||||
|
||||
/*-------------
|
||||
* Drawing
|
||||
*-----------*/
|
||||
|
||||
/*Enable complex draw engine.
|
||||
*Required to draw shadow, gradient, rounded corners, circles, arc, skew lines, image transformations or any masks*/
|
||||
#define LV_DRAW_COMPLEX 1
|
||||
#if LV_DRAW_COMPLEX != 0
|
||||
|
||||
/*Allow buffering some shadow calculation.
|
||||
*LV_SHADOW_CACHE_SIZE is the max. shadow size to buffer, where shadow size is `shadow_width + radius`
|
||||
*Caching has LV_SHADOW_CACHE_SIZE^2 RAM cost*/
|
||||
#define LV_SHADOW_CACHE_SIZE 0
|
||||
|
||||
/* Set number of maximally cached circle data.
|
||||
* The circumference of 1/4 circle are saved for anti-aliasing
|
||||
* radius * 4 bytes are used per circle (the most often used radiuses are saved)
|
||||
* 0: to disable caching */
|
||||
#define LV_CIRCLE_CACHE_SIZE 4
|
||||
#endif /*LV_DRAW_COMPLEX*/
|
||||
|
||||
/**
|
||||
* "Simple layers" are used when a widget has `style_opa < 255` to buffer the widget into a layer
|
||||
* and blend it as an image with the given opacity.
|
||||
* Note that `bg_opa`, `text_opa` etc don't require buffering into layer)
|
||||
* The widget can be buffered in smaller chunks to avoid using large buffers.
|
||||
*
|
||||
* - LV_LAYER_SIMPLE_BUF_SIZE: [bytes] the optimal target buffer size. LVGL will try to allocate it
|
||||
* - LV_LAYER_SIMPLE_FALLBACK_BUF_SIZE: [bytes] used if `LV_LAYER_SIMPLE_BUF_SIZE` couldn't be allocated.
|
||||
*
|
||||
* Both buffer sizes are in bytes.
|
||||
* "Transformed layers" (where transform_angle/zoom properties are used) use larger buffers
|
||||
* and can't be drawn in chunks. So these settings affects only widgets with opacity.
|
||||
*/
|
||||
#define LV_LAYER_SIMPLE_BUF_SIZE (24 * 1024)
|
||||
#define LV_LAYER_SIMPLE_FALLBACK_BUF_SIZE (3 * 1024)
|
||||
|
||||
/*Default image cache size. Image caching keeps the images opened.
|
||||
*If only the built-in image formats are used there is no real advantage of caching. (I.e. if no new image decoder is added)
|
||||
*With complex image decoders (e.g. PNG or JPG) caching can save the continuous open/decode of images.
|
||||
*However the opened images might consume additional RAM.
|
||||
*0: to disable caching*/
|
||||
#define LV_IMG_CACHE_DEF_SIZE 0
|
||||
|
||||
/*Number of stops allowed per gradient. Increase this to allow more stops.
|
||||
*This adds (sizeof(lv_color_t) + 1) bytes per additional stop*/
|
||||
#define LV_GRADIENT_MAX_STOPS 2
|
||||
|
||||
/*Default gradient buffer size.
|
||||
*When LVGL calculates the gradient "maps" it can save them into a cache to avoid calculating them again.
|
||||
*LV_GRAD_CACHE_DEF_SIZE sets the size of this cache in bytes.
|
||||
*If the cache is too small the map will be allocated only while it's required for the drawing.
|
||||
*0 mean no caching.*/
|
||||
#define LV_GRAD_CACHE_DEF_SIZE 0
|
||||
|
||||
/*Allow dithering the gradients (to achieve visual smooth color gradients on limited color depth display)
|
||||
*LV_DITHER_GRADIENT implies allocating one or two more lines of the object's rendering surface
|
||||
*The increase in memory consumption is (32 bits * object width) plus 24 bits * object width if using error diffusion */
|
||||
#define LV_DITHER_GRADIENT 0
|
||||
#if LV_DITHER_GRADIENT
|
||||
/*Add support for error diffusion dithering.
|
||||
*Error diffusion dithering gets a much better visual result, but implies more CPU consumption and memory when drawing.
|
||||
*The increase in memory consumption is (24 bits * object's width)*/
|
||||
#define LV_DITHER_ERROR_DIFFUSION 0
|
||||
#endif
|
||||
|
||||
/*Maximum buffer size to allocate for rotation.
|
||||
*Only used if software rotation is enabled in the display driver.*/
|
||||
#define LV_DISP_ROT_MAX_BUF (10*1024)
|
||||
|
||||
/*-------------
|
||||
* GPU
|
||||
*-----------*/
|
||||
|
||||
/*Use Arm's 2D acceleration library Arm-2D */
|
||||
#define LV_USE_GPU_ARM2D 0
|
||||
|
||||
/*Use STM32's DMA2D (aka Chrom Art) GPU*/
|
||||
#define LV_USE_GPU_STM32_DMA2D 0
|
||||
#if LV_USE_GPU_STM32_DMA2D
|
||||
/*Must be defined to include path of CMSIS header of target processor
|
||||
e.g. "stm32f7xx.h" or "stm32f4xx.h"*/
|
||||
#define LV_GPU_DMA2D_CMSIS_INCLUDE
|
||||
#endif
|
||||
|
||||
/*Enable RA6M3 G2D GPU*/
|
||||
#define LV_USE_GPU_RA6M3_G2D 0
|
||||
#if LV_USE_GPU_RA6M3_G2D
|
||||
/*include path of target processor
|
||||
e.g. "hal_data.h"*/
|
||||
#define LV_GPU_RA6M3_G2D_INCLUDE "hal_data.h"
|
||||
#endif
|
||||
|
||||
/*Use SWM341's DMA2D GPU*/
|
||||
#define LV_USE_GPU_SWM341_DMA2D 0
|
||||
#if LV_USE_GPU_SWM341_DMA2D
|
||||
#define LV_GPU_SWM341_DMA2D_INCLUDE "SWM341.h"
|
||||
#endif
|
||||
|
||||
/*Use NXP's PXP GPU iMX RTxxx platforms*/
|
||||
#define LV_USE_GPU_NXP_PXP 0
|
||||
#if LV_USE_GPU_NXP_PXP
|
||||
/*1: Add default bare metal and FreeRTOS interrupt handling routines for PXP (lv_gpu_nxp_pxp_osa.c)
|
||||
* and call lv_gpu_nxp_pxp_init() automatically during lv_init(). Note that symbol SDK_OS_FREE_RTOS
|
||||
* has to be defined in order to use FreeRTOS OSA, otherwise bare-metal implementation is selected.
|
||||
*0: lv_gpu_nxp_pxp_init() has to be called manually before lv_init()
|
||||
*/
|
||||
#define LV_USE_GPU_NXP_PXP_AUTO_INIT 0
|
||||
#endif
|
||||
|
||||
/*Use NXP's VG-Lite GPU iMX RTxxx platforms*/
|
||||
#define LV_USE_GPU_NXP_VG_LITE 0
|
||||
|
||||
/*Use SDL renderer API*/
|
||||
#define LV_USE_GPU_SDL 0
|
||||
#if LV_USE_GPU_SDL
|
||||
#define LV_GPU_SDL_INCLUDE_PATH <SDL2/SDL.h>
|
||||
/*Texture cache size, 8MB by default*/
|
||||
#define LV_GPU_SDL_LRU_SIZE (1024 * 1024 * 8)
|
||||
/*Custom blend mode for mask drawing, disable if you need to link with older SDL2 lib*/
|
||||
#define LV_GPU_SDL_CUSTOM_BLEND_MODE (SDL_VERSION_ATLEAST(2, 0, 6))
|
||||
#endif
|
||||
|
||||
/*-------------
|
||||
* Logging
|
||||
*-----------*/
|
||||
|
||||
/*Enable the log module*/
|
||||
#define LV_USE_LOG 0
|
||||
#if LV_USE_LOG
|
||||
|
||||
/*How important log should be added:
|
||||
*LV_LOG_LEVEL_TRACE A lot of logs to give detailed information
|
||||
*LV_LOG_LEVEL_INFO Log important events
|
||||
*LV_LOG_LEVEL_WARN Log if something unwanted happened but didn't cause a problem
|
||||
*LV_LOG_LEVEL_ERROR Only critical issue, when the system may fail
|
||||
*LV_LOG_LEVEL_USER Only logs added by the user
|
||||
*LV_LOG_LEVEL_NONE Do not log anything*/
|
||||
#define LV_LOG_LEVEL LV_LOG_LEVEL_WARN
|
||||
|
||||
/*1: Print the log with 'printf';
|
||||
*0: User need to register a callback with `lv_log_register_print_cb()`*/
|
||||
#define LV_LOG_PRINTF 0
|
||||
|
||||
/*Enable/disable LV_LOG_TRACE in modules that produces a huge number of logs*/
|
||||
#define LV_LOG_TRACE_MEM 1
|
||||
#define LV_LOG_TRACE_TIMER 1
|
||||
#define LV_LOG_TRACE_INDEV 1
|
||||
#define LV_LOG_TRACE_DISP_REFR 1
|
||||
#define LV_LOG_TRACE_EVENT 1
|
||||
#define LV_LOG_TRACE_OBJ_CREATE 1
|
||||
#define LV_LOG_TRACE_LAYOUT 1
|
||||
#define LV_LOG_TRACE_ANIM 1
|
||||
|
||||
#endif /*LV_USE_LOG*/
|
||||
|
||||
/*-------------
|
||||
* Asserts
|
||||
*-----------*/
|
||||
|
||||
/*Enable asserts if an operation is failed or an invalid data is found.
|
||||
*If LV_USE_LOG is enabled an error message will be printed on failure*/
|
||||
#define LV_USE_ASSERT_NULL 1 /*Check if the parameter is NULL. (Very fast, recommended)*/
|
||||
#define LV_USE_ASSERT_MALLOC 1 /*Checks is the memory is successfully allocated or no. (Very fast, recommended)*/
|
||||
#define LV_USE_ASSERT_STYLE 0 /*Check if the styles are properly initialized. (Very fast, recommended)*/
|
||||
#define LV_USE_ASSERT_MEM_INTEGRITY 0 /*Check the integrity of `lv_mem` after critical operations. (Slow)*/
|
||||
#define LV_USE_ASSERT_OBJ 0 /*Check the object's type and existence (e.g. not deleted). (Slow)*/
|
||||
|
||||
/*Add a custom handler when assert happens e.g. to restart the MCU*/
|
||||
#define LV_ASSERT_HANDLER_INCLUDE <stdint.h>
|
||||
#define LV_ASSERT_HANDLER while(1); /*Halt by default*/
|
||||
|
||||
/*-------------
|
||||
* Others
|
||||
*-----------*/
|
||||
|
||||
/*1: Show CPU usage and FPS count*/
|
||||
#define LV_USE_PERF_MONITOR 0
|
||||
#if LV_USE_PERF_MONITOR
|
||||
#define LV_USE_PERF_MONITOR_POS LV_ALIGN_BOTTOM_RIGHT
|
||||
#endif
|
||||
|
||||
/*1: Show the used memory and the memory fragmentation
|
||||
* Requires LV_MEM_CUSTOM = 0*/
|
||||
#define LV_USE_MEM_MONITOR 0
|
||||
#if LV_USE_MEM_MONITOR
|
||||
#define LV_USE_MEM_MONITOR_POS LV_ALIGN_BOTTOM_LEFT
|
||||
#endif
|
||||
|
||||
/*1: Draw random colored rectangles over the redrawn areas*/
|
||||
#define LV_USE_REFR_DEBUG 0
|
||||
|
||||
/*Change the built in (v)snprintf functions*/
|
||||
#define LV_SPRINTF_CUSTOM 0
|
||||
#if LV_SPRINTF_CUSTOM
|
||||
#define LV_SPRINTF_INCLUDE <stdio.h>
|
||||
#define lv_snprintf snprintf
|
||||
#define lv_vsnprintf vsnprintf
|
||||
#else /*LV_SPRINTF_CUSTOM*/
|
||||
#define LV_SPRINTF_USE_FLOAT 0
|
||||
#endif /*LV_SPRINTF_CUSTOM*/
|
||||
|
||||
#define LV_USE_USER_DATA 1
|
||||
|
||||
/*Garbage Collector settings
|
||||
*Used if lvgl is bound to higher level language and the memory is managed by that language*/
|
||||
#define LV_ENABLE_GC 0
|
||||
#if LV_ENABLE_GC != 0
|
||||
#define LV_GC_INCLUDE "gc.h" /*Include Garbage Collector related things*/
|
||||
#endif /*LV_ENABLE_GC*/
|
||||
|
||||
/*=====================
|
||||
* COMPILER SETTINGS
|
||||
*====================*/
|
||||
|
||||
/*For big endian systems set to 1*/
|
||||
#define LV_BIG_ENDIAN_SYSTEM 0
|
||||
|
||||
/*Define a custom attribute to `lv_tick_inc` function*/
|
||||
#define LV_ATTRIBUTE_TICK_INC
|
||||
|
||||
/*Define a custom attribute to `lv_timer_handler` function*/
|
||||
#define LV_ATTRIBUTE_TIMER_HANDLER
|
||||
|
||||
/*Define a custom attribute to `lv_disp_flush_ready` function*/
|
||||
#define LV_ATTRIBUTE_FLUSH_READY
|
||||
|
||||
/*Required alignment size for buffers*/
|
||||
#define LV_ATTRIBUTE_MEM_ALIGN_SIZE 1
|
||||
|
||||
/*Will be added where memories needs to be aligned (with -Os data might not be aligned to boundary by default).
|
||||
* E.g. __attribute__((aligned(4)))*/
|
||||
#define LV_ATTRIBUTE_MEM_ALIGN
|
||||
|
||||
/*Attribute to mark large constant arrays for example font's bitmaps*/
|
||||
#define LV_ATTRIBUTE_LARGE_CONST
|
||||
|
||||
/*Compiler prefix for a big array declaration in RAM*/
|
||||
#define LV_ATTRIBUTE_LARGE_RAM_ARRAY
|
||||
|
||||
/*Place performance critical functions into a faster memory (e.g RAM)*/
|
||||
#define LV_ATTRIBUTE_FAST_MEM
|
||||
|
||||
/*Prefix variables that are used in GPU accelerated operations, often these need to be placed in RAM sections that are DMA accessible*/
|
||||
#define LV_ATTRIBUTE_DMA
|
||||
|
||||
/*Export integer constant to binding. This macro is used with constants in the form of LV_<CONST> that
|
||||
*should also appear on LVGL binding API such as Micropython.*/
|
||||
#define LV_EXPORT_CONST_INT(int_value) struct _silence_gcc_warning /*The default value just prevents GCC warning*/
|
||||
|
||||
/*Extend the default -32k..32k coordinate range to -4M..4M by using int32_t for coordinates instead of int16_t*/
|
||||
#define LV_USE_LARGE_COORD 0
|
||||
|
||||
/*==================
|
||||
* FONT USAGE
|
||||
*===================*/
|
||||
|
||||
/*Montserrat fonts with ASCII range and some symbols using bpp = 4
|
||||
*https://fonts.google.com/specimen/Montserrat*/
|
||||
#define LV_FONT_MONTSERRAT_8 1
|
||||
#define LV_FONT_MONTSERRAT_10 1
|
||||
#define LV_FONT_MONTSERRAT_12 1
|
||||
#define LV_FONT_MONTSERRAT_14 1
|
||||
#define LV_FONT_MONTSERRAT_16 1
|
||||
#define LV_FONT_MONTSERRAT_18 1
|
||||
#define LV_FONT_MONTSERRAT_20 1
|
||||
#define LV_FONT_MONTSERRAT_22 1
|
||||
#define LV_FONT_MONTSERRAT_24 1
|
||||
#define LV_FONT_MONTSERRAT_26 1
|
||||
#define LV_FONT_MONTSERRAT_28 1
|
||||
#define LV_FONT_MONTSERRAT_30 1
|
||||
#define LV_FONT_MONTSERRAT_32 1
|
||||
#define LV_FONT_MONTSERRAT_34 1
|
||||
#define LV_FONT_MONTSERRAT_36 1
|
||||
#define LV_FONT_MONTSERRAT_38 1
|
||||
#define LV_FONT_MONTSERRAT_40 1
|
||||
#define LV_FONT_MONTSERRAT_42 1
|
||||
#define LV_FONT_MONTSERRAT_44 1
|
||||
#define LV_FONT_MONTSERRAT_46 1
|
||||
#define LV_FONT_MONTSERRAT_48 1
|
||||
|
||||
/*Demonstrate special features*/
|
||||
#define LV_FONT_MONTSERRAT_12_SUBPX 0
|
||||
#define LV_FONT_MONTSERRAT_28_COMPRESSED 0 /*bpp = 3*/
|
||||
#define LV_FONT_DEJAVU_16_PERSIAN_HEBREW 0 /*Hebrew, Arabic, Persian letters and all their forms*/
|
||||
#define LV_FONT_SIMSUN_16_CJK 0 /*1000 most common CJK radicals*/
|
||||
|
||||
/*Pixel perfect monospace fonts*/
|
||||
#define LV_FONT_UNSCII_8 0
|
||||
#define LV_FONT_UNSCII_16 0
|
||||
|
||||
/*Optionally declare custom fonts here.
|
||||
*You can use these fonts as default font too and they will be available globally.
|
||||
*E.g. #define LV_FONT_CUSTOM_DECLARE LV_FONT_DECLARE(my_font_1) LV_FONT_DECLARE(my_font_2)*/
|
||||
#define LV_FONT_CUSTOM_DECLARE
|
||||
|
||||
/*Always set a default font*/
|
||||
#define LV_FONT_DEFAULT &lv_font_montserrat_14
|
||||
|
||||
/*Enable handling large font and/or fonts with a lot of characters.
|
||||
*The limit depends on the font size, font face and bpp.
|
||||
*Compiler error will be triggered if a font needs it.*/
|
||||
#define LV_FONT_FMT_TXT_LARGE 0
|
||||
|
||||
/*Enables/disables support for compressed fonts.*/
|
||||
#define LV_USE_FONT_COMPRESSED 0
|
||||
|
||||
/*Enable subpixel rendering*/
|
||||
#define LV_USE_FONT_SUBPX 0
|
||||
#if LV_USE_FONT_SUBPX
|
||||
/*Set the pixel order of the display. Physical order of RGB channels. Doesn't matter with "normal" fonts.*/
|
||||
#define LV_FONT_SUBPX_BGR 0 /*0: RGB; 1:BGR order*/
|
||||
#endif
|
||||
|
||||
/*Enable drawing placeholders when glyph dsc is not found*/
|
||||
#define LV_USE_FONT_PLACEHOLDER 1
|
||||
|
||||
/*=================
|
||||
* TEXT SETTINGS
|
||||
*=================*/
|
||||
|
||||
/**
|
||||
* Select a character encoding for strings.
|
||||
* Your IDE or editor should have the same character encoding
|
||||
* - LV_TXT_ENC_UTF8
|
||||
* - LV_TXT_ENC_ASCII
|
||||
*/
|
||||
#define LV_TXT_ENC LV_TXT_ENC_UTF8
|
||||
|
||||
/*Can break (wrap) texts on these chars*/
|
||||
#define LV_TXT_BREAK_CHARS " ,.;:-_"
|
||||
|
||||
/*If a word is at least this long, will break wherever "prettiest"
|
||||
*To disable, set to a value <= 0*/
|
||||
#define LV_TXT_LINE_BREAK_LONG_LEN 0
|
||||
|
||||
/*Minimum number of characters in a long word to put on a line before a break.
|
||||
*Depends on LV_TXT_LINE_BREAK_LONG_LEN.*/
|
||||
#define LV_TXT_LINE_BREAK_LONG_PRE_MIN_LEN 3
|
||||
|
||||
/*Minimum number of characters in a long word to put on a line after a break.
|
||||
*Depends on LV_TXT_LINE_BREAK_LONG_LEN.*/
|
||||
#define LV_TXT_LINE_BREAK_LONG_POST_MIN_LEN 3
|
||||
|
||||
/*The control character to use for signalling text recoloring.*/
|
||||
#define LV_TXT_COLOR_CMD "#"
|
||||
|
||||
/*Support bidirectional texts. Allows mixing Left-to-Right and Right-to-Left texts.
|
||||
*The direction will be processed according to the Unicode Bidirectional Algorithm:
|
||||
*https://www.w3.org/International/articles/inline-bidi-markup/uba-basics*/
|
||||
#define LV_USE_BIDI 0
|
||||
#if LV_USE_BIDI
|
||||
/*Set the default direction. Supported values:
|
||||
*`LV_BASE_DIR_LTR` Left-to-Right
|
||||
*`LV_BASE_DIR_RTL` Right-to-Left
|
||||
*`LV_BASE_DIR_AUTO` detect texts base direction*/
|
||||
#define LV_BIDI_BASE_DIR_DEF LV_BASE_DIR_AUTO
|
||||
#endif
|
||||
|
||||
/*Enable Arabic/Persian processing
|
||||
*In these languages characters should be replaced with an other form based on their position in the text*/
|
||||
#define LV_USE_ARABIC_PERSIAN_CHARS 0
|
||||
|
||||
/*==================
|
||||
* WIDGET USAGE
|
||||
*================*/
|
||||
|
||||
/*Documentation of the widgets: https://docs.lvgl.io/latest/en/html/widgets/index.html*/
|
||||
|
||||
#define LV_USE_ARC 1
|
||||
|
||||
#define LV_USE_BAR 1
|
||||
|
||||
#define LV_USE_BTN 1
|
||||
|
||||
#define LV_USE_BTNMATRIX 1
|
||||
|
||||
#define LV_USE_CANVAS 1
|
||||
|
||||
#define LV_USE_CHECKBOX 1
|
||||
|
||||
#define LV_USE_DROPDOWN 1 /*Requires: lv_label*/
|
||||
|
||||
#define LV_USE_IMG 1 /*Requires: lv_label*/
|
||||
|
||||
#define LV_USE_LABEL 1
|
||||
#if LV_USE_LABEL
|
||||
#define LV_LABEL_TEXT_SELECTION 1 /*Enable selecting text of the label*/
|
||||
#define LV_LABEL_LONG_TXT_HINT 1 /*Store some extra info in labels to speed up drawing of very long texts*/
|
||||
#endif
|
||||
|
||||
#define LV_USE_LINE 1
|
||||
|
||||
#define LV_USE_ROLLER 1 /*Requires: lv_label*/
|
||||
#if LV_USE_ROLLER
|
||||
#define LV_ROLLER_INF_PAGES 7 /*Number of extra "pages" when the roller is infinite*/
|
||||
#endif
|
||||
|
||||
#define LV_USE_SLIDER 1 /*Requires: lv_bar*/
|
||||
|
||||
#define LV_USE_SWITCH 1
|
||||
|
||||
#define LV_USE_TEXTAREA 1 /*Requires: lv_label*/
|
||||
#if LV_USE_TEXTAREA != 0
|
||||
#define LV_TEXTAREA_DEF_PWD_SHOW_TIME 1500 /*ms*/
|
||||
#endif
|
||||
|
||||
#define LV_USE_TABLE 1
|
||||
|
||||
/*==================
|
||||
* EXTRA COMPONENTS
|
||||
*==================*/
|
||||
|
||||
/*-----------
|
||||
* Widgets
|
||||
*----------*/
|
||||
#define LV_USE_ANIMIMG 1
|
||||
|
||||
#define LV_USE_CALENDAR 1
|
||||
#if LV_USE_CALENDAR
|
||||
#define LV_CALENDAR_WEEK_STARTS_MONDAY 0
|
||||
#if LV_CALENDAR_WEEK_STARTS_MONDAY
|
||||
#define LV_CALENDAR_DEFAULT_DAY_NAMES {"Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"}
|
||||
#else
|
||||
#define LV_CALENDAR_DEFAULT_DAY_NAMES {"Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"}
|
||||
#endif
|
||||
|
||||
#define LV_CALENDAR_DEFAULT_MONTH_NAMES {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
|
||||
#define LV_USE_CALENDAR_HEADER_ARROW 1
|
||||
#define LV_USE_CALENDAR_HEADER_DROPDOWN 1
|
||||
#endif /*LV_USE_CALENDAR*/
|
||||
|
||||
#define LV_USE_CHART 1
|
||||
|
||||
#define LV_USE_COLORWHEEL 1
|
||||
|
||||
#define LV_USE_IMGBTN 1
|
||||
|
||||
#define LV_USE_KEYBOARD 1
|
||||
|
||||
#define LV_USE_LED 1
|
||||
|
||||
#define LV_USE_LIST 1
|
||||
|
||||
#define LV_USE_MENU 1
|
||||
|
||||
#define LV_USE_METER 1
|
||||
|
||||
#define LV_USE_MSGBOX 1
|
||||
|
||||
#define LV_USE_SPAN 1
|
||||
#if LV_USE_SPAN
|
||||
/*A line text can contain maximum num of span descriptor */
|
||||
#define LV_SPAN_SNIPPET_STACK_SIZE 64
|
||||
#endif
|
||||
|
||||
#define LV_USE_SPINBOX 1
|
||||
|
||||
#define LV_USE_SPINNER 1
|
||||
|
||||
#define LV_USE_TABVIEW 1
|
||||
|
||||
#define LV_USE_TILEVIEW 1
|
||||
|
||||
#define LV_USE_WIN 1
|
||||
|
||||
/*-----------
|
||||
* Themes
|
||||
*----------*/
|
||||
|
||||
/*A simple, impressive and very complete theme*/
|
||||
#define LV_USE_THEME_DEFAULT 1
|
||||
#if LV_USE_THEME_DEFAULT
|
||||
|
||||
/*0: Light mode; 1: Dark mode*/
|
||||
#define LV_THEME_DEFAULT_DARK 0
|
||||
|
||||
/*1: Enable grow on press*/
|
||||
#define LV_THEME_DEFAULT_GROW 1
|
||||
|
||||
/*Default transition time in [ms]*/
|
||||
#define LV_THEME_DEFAULT_TRANSITION_TIME 80
|
||||
#endif /*LV_USE_THEME_DEFAULT*/
|
||||
|
||||
/*A very simple theme that is a good starting point for a custom theme*/
|
||||
#define LV_USE_THEME_BASIC 1
|
||||
|
||||
/*A theme designed for monochrome displays*/
|
||||
#define LV_USE_THEME_MONO 1
|
||||
|
||||
/*-----------
|
||||
* Layouts
|
||||
*----------*/
|
||||
|
||||
/*A layout similar to Flexbox in CSS.*/
|
||||
#define LV_USE_FLEX 1
|
||||
|
||||
/*A layout similar to Grid in CSS.*/
|
||||
#define LV_USE_GRID 1
|
||||
|
||||
/*---------------------
|
||||
* 3rd party libraries
|
||||
*--------------------*/
|
||||
|
||||
/*File system interfaces for common APIs */
|
||||
|
||||
/*API for fopen, fread, etc*/
|
||||
#define LV_USE_FS_STDIO 0
|
||||
#if LV_USE_FS_STDIO
|
||||
#define LV_FS_STDIO_LETTER '\0' /*Set an upper cased letter on which the drive will accessible (e.g. 'A')*/
|
||||
#define LV_FS_STDIO_PATH "" /*Set the working directory. File/directory paths will be appended to it.*/
|
||||
#define LV_FS_STDIO_CACHE_SIZE 0 /*>0 to cache this number of bytes in lv_fs_read()*/
|
||||
#endif
|
||||
|
||||
/*API for open, read, etc*/
|
||||
#define LV_USE_FS_POSIX 0
|
||||
#if LV_USE_FS_POSIX
|
||||
#define LV_FS_POSIX_LETTER '\0' /*Set an upper cased letter on which the drive will accessible (e.g. 'A')*/
|
||||
#define LV_FS_POSIX_PATH "" /*Set the working directory. File/directory paths will be appended to it.*/
|
||||
#define LV_FS_POSIX_CACHE_SIZE 0 /*>0 to cache this number of bytes in lv_fs_read()*/
|
||||
#endif
|
||||
|
||||
/*API for CreateFile, ReadFile, etc*/
|
||||
#define LV_USE_FS_WIN32 0
|
||||
#if LV_USE_FS_WIN32
|
||||
#define LV_FS_WIN32_LETTER '\0' /*Set an upper cased letter on which the drive will accessible (e.g. 'A')*/
|
||||
#define LV_FS_WIN32_PATH "" /*Set the working directory. File/directory paths will be appended to it.*/
|
||||
#define LV_FS_WIN32_CACHE_SIZE 0 /*>0 to cache this number of bytes in lv_fs_read()*/
|
||||
#endif
|
||||
|
||||
/*API for FATFS (needs to be added separately). Uses f_open, f_read, etc*/
|
||||
#define LV_USE_FS_FATFS 0
|
||||
#if LV_USE_FS_FATFS
|
||||
#define LV_FS_FATFS_LETTER '\0' /*Set an upper cased letter on which the drive will accessible (e.g. 'A')*/
|
||||
#define LV_FS_FATFS_CACHE_SIZE 0 /*>0 to cache this number of bytes in lv_fs_read()*/
|
||||
#endif
|
||||
|
||||
/*API for LittleFS (library needs to be added separately). Uses lfs_file_open, lfs_file_read, etc*/
|
||||
#define LV_USE_FS_LITTLEFS 0
|
||||
#if LV_USE_FS_LITTLEFS
|
||||
#define LV_FS_LITTLEFS_LETTER '\0' /*Set an upper cased letter on which the drive will accessible (e.g. 'A')*/
|
||||
#define LV_FS_LITTLEFS_CACHE_SIZE 0 /*>0 to cache this number of bytes in lv_fs_read()*/
|
||||
#endif
|
||||
|
||||
/*PNG decoder library*/
|
||||
#define LV_USE_PNG 0
|
||||
|
||||
/*BMP decoder library*/
|
||||
#define LV_USE_BMP 0
|
||||
|
||||
/* JPG + split JPG decoder library.
|
||||
* Split JPG is a custom format optimized for embedded systems. */
|
||||
#define LV_USE_SJPG 0
|
||||
|
||||
/*GIF decoder library*/
|
||||
#define LV_USE_GIF 0
|
||||
|
||||
/*QR code library*/
|
||||
#define LV_USE_QRCODE 1
|
||||
|
||||
/*FreeType library*/
|
||||
#define LV_USE_FREETYPE 0
|
||||
#if LV_USE_FREETYPE
|
||||
/*Memory used by FreeType to cache characters [bytes] (-1: no caching)*/
|
||||
#define LV_FREETYPE_CACHE_SIZE (16 * 1024)
|
||||
#if LV_FREETYPE_CACHE_SIZE >= 0
|
||||
/* 1: bitmap cache use the sbit cache, 0:bitmap cache use the image cache. */
|
||||
/* sbit cache:it is much more memory efficient for small bitmaps(font size < 256) */
|
||||
/* if font size >= 256, must be configured as image cache */
|
||||
#define LV_FREETYPE_SBIT_CACHE 0
|
||||
/* Maximum number of opened FT_Face/FT_Size objects managed by this cache instance. */
|
||||
/* (0:use system defaults) */
|
||||
#define LV_FREETYPE_CACHE_FT_FACES 0
|
||||
#define LV_FREETYPE_CACHE_FT_SIZES 0
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/*Tiny TTF library*/
|
||||
#define LV_USE_TINY_TTF 0
|
||||
#if LV_USE_TINY_TTF
|
||||
/*Load TTF data from files*/
|
||||
#define LV_TINY_TTF_FILE_SUPPORT 0
|
||||
#endif
|
||||
|
||||
/*Rlottie library*/
|
||||
#define LV_USE_RLOTTIE 0
|
||||
|
||||
/*FFmpeg library for image decoding and playing videos
|
||||
*Supports all major image formats so do not enable other image decoder with it*/
|
||||
#define LV_USE_FFMPEG 0
|
||||
#if LV_USE_FFMPEG
|
||||
/*Dump input information to stderr*/
|
||||
#define LV_FFMPEG_DUMP_FORMAT 0
|
||||
#endif
|
||||
|
||||
/*-----------
|
||||
* Others
|
||||
*----------*/
|
||||
|
||||
/*1: Enable API to take snapshot for object*/
|
||||
#define LV_USE_SNAPSHOT 0
|
||||
|
||||
/*1: Enable Monkey test*/
|
||||
#define LV_USE_MONKEY 0
|
||||
|
||||
/*1: Enable grid navigation*/
|
||||
#define LV_USE_GRIDNAV 0
|
||||
|
||||
/*1: Enable lv_obj fragment*/
|
||||
#define LV_USE_FRAGMENT 0
|
||||
|
||||
/*1: Support using images as font in label or span widgets */
|
||||
#define LV_USE_IMGFONT 0
|
||||
|
||||
/*1: Enable a published subscriber based messaging system */
|
||||
#define LV_USE_MSG 0
|
||||
|
||||
/*1: Enable Pinyin input method*/
|
||||
/*Requires: lv_keyboard*/
|
||||
#define LV_USE_IME_PINYIN 0
|
||||
#if LV_USE_IME_PINYIN
|
||||
/*1: Use default thesaurus*/
|
||||
/*If you do not use the default thesaurus, be sure to use `lv_ime_pinyin` after setting the thesauruss*/
|
||||
#define LV_IME_PINYIN_USE_DEFAULT_DICT 1
|
||||
/*Set the maximum number of candidate panels that can be displayed*/
|
||||
/*This needs to be adjusted according to the size of the screen*/
|
||||
#define LV_IME_PINYIN_CAND_TEXT_NUM 6
|
||||
|
||||
/*Use 9 key input(k9)*/
|
||||
#define LV_IME_PINYIN_USE_K9_MODE 1
|
||||
#if LV_IME_PINYIN_USE_K9_MODE == 1
|
||||
#define LV_IME_PINYIN_K9_CAND_TEXT_NUM 3
|
||||
#endif // LV_IME_PINYIN_USE_K9_MODE
|
||||
#endif
|
||||
|
||||
/*==================
|
||||
* EXAMPLES
|
||||
*==================*/
|
||||
|
||||
/*Enable the examples to be built with the library*/
|
||||
#define LV_BUILD_EXAMPLES 1
|
||||
|
||||
/*===================
|
||||
* DEMO USAGE
|
||||
====================*/
|
||||
|
||||
/*Show some widget. It might be required to increase `LV_MEM_SIZE` */
|
||||
#define LV_USE_DEMO_WIDGETS 1
|
||||
#if LV_USE_DEMO_WIDGETS
|
||||
#define LV_DEMO_WIDGETS_SLIDESHOW 1
|
||||
#endif
|
||||
|
||||
/*Demonstrate the usage of encoder and keyboard*/
|
||||
#define LV_USE_DEMO_KEYPAD_AND_ENCODER 0
|
||||
|
||||
/*Benchmark your system*/
|
||||
#define LV_USE_DEMO_BENCHMARK 1
|
||||
#if LV_USE_DEMO_BENCHMARK
|
||||
/*Use RGB565A8 images with 16 bit color depth instead of ARGB8565*/
|
||||
#define LV_DEMO_BENCHMARK_RGB565A8 0
|
||||
#endif
|
||||
|
||||
/*Stress test for LVGL*/
|
||||
#define LV_USE_DEMO_STRESS 1
|
||||
|
||||
/*Music player demo*/
|
||||
#define LV_USE_DEMO_MUSIC 1
|
||||
#if LV_USE_DEMO_MUSIC
|
||||
#define LV_DEMO_MUSIC_SQUARE 0
|
||||
#define LV_DEMO_MUSIC_LANDSCAPE 0
|
||||
#define LV_DEMO_MUSIC_ROUND 0
|
||||
#define LV_DEMO_MUSIC_LARGE 0
|
||||
#define LV_DEMO_MUSIC_AUTO_PLAY 0
|
||||
#endif
|
||||
|
||||
/*--END OF LV_CONF_H--*/
|
||||
|
||||
#endif /*LV_CONF_H*/
|
||||
|
||||
#endif /*End of "Content enable"*/
|
||||
@@ -0,0 +1,11 @@
|
||||
# Factory Firmware Backup
|
||||
|
||||
Здесь хранится полный дамп флеша текущего состояния платы.
|
||||
|
||||
Файлы:
|
||||
- `factory-full-16mb.bin` — полный дамп флеша `0x000000..0xFFFFFF`
|
||||
- `factory-full-16mb.bin.sha256` — контрольная сумма
|
||||
|
||||
Скрипты:
|
||||
- `backup_factory.sh` — снять резервную копию
|
||||
- `restore_factory_backup.sh` — восстановить резервную копию на плату
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PORT="${PORT:-/dev/ttyACM0}"
|
||||
ESPTOOL="${ESPTOOL:-$HOME/.arduino15/packages/esp32/tools/esptool_py/5.1.0/esptool}"
|
||||
BAUD="${BAUD:-921600}"
|
||||
OUT_BIN="${ROOT_DIR}/factory-full-16mb.bin"
|
||||
|
||||
echo "== Port: ${PORT}"
|
||||
echo "== Output: ${OUT_BIN}"
|
||||
echo "== Esptool: ${ESPTOOL}"
|
||||
echo "== Baud: ${BAUD}"
|
||||
|
||||
"${ESPTOOL}" --port "${PORT}" --baud "${BAUD}" read-flash 0 0x1000000 "${OUT_BIN}"
|
||||
sha256sum "${OUT_BIN}" | tee "${OUT_BIN}.sha256"
|
||||
|
||||
echo
|
||||
echo "== Backup done."
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PORT="${PORT:-/dev/ttyACM0}"
|
||||
ESPTOOL="${ESPTOOL:-$HOME/.arduino15/packages/esp32/tools/esptool_py/5.1.0/esptool}"
|
||||
IN_BIN="${IN_BIN:-${ROOT_DIR}/factory-full-16mb.bin}"
|
||||
|
||||
if [[ ! -f "${IN_BIN}" ]]; then
|
||||
echo "Backup file not found: ${IN_BIN}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "== Port: ${PORT}"
|
||||
echo "== Input: ${IN_BIN}"
|
||||
echo "== Esptool: ${ESPTOOL}"
|
||||
|
||||
"${ESPTOOL}" --port "${PORT}" --baud 921600 write-flash 0 "${IN_BIN}"
|
||||
|
||||
echo
|
||||
echo "== Restore done."
|
||||
@@ -0,0 +1,17 @@
|
||||
# Reference Notes
|
||||
|
||||
Модель: Waveshare `ESP32-S3-Touch-AMOLED-2.16` (SKU 33969/33970)
|
||||
|
||||
Полезные страницы:
|
||||
- Product: https://www.waveshare.com/product/arduino/boards-kits/esp32-s3/esp32-s3-touch-amoled-2.16.htm
|
||||
- Docs home: https://docs.waveshare.com/ESP32-S3-Touch-AMOLED-2.16
|
||||
- Arduino setup: https://docs.waveshare.com/ESP32-S3-Touch-AMOLED-2.16/Development-Environment-Setup-Arduino
|
||||
- Official examples: https://github.com/waveshareteam/ESP32-S3-Touch-AMOLED-2.16
|
||||
|
||||
Ключевое по железу (по документации):
|
||||
- ESP32-S3R8, 16MB flash, 8MB PSRAM
|
||||
- AMOLED 2.16" 480x480
|
||||
- Capacitive touch (CST9220)
|
||||
- IMU QMI8658
|
||||
- Audio codec ES8311 (playback) и ES7210 (microphones)
|
||||
- TF slot (SD), сейчас без карты
|
||||
@@ -0,0 +1,353 @@
|
||||
# SHiNE ESP32 Homeserver UI Nav Minimal Spec
|
||||
|
||||
Легаси-спецификация старого навигационного теста для `Waveshare ESP32-S3-Touch-AMOLED-2.16`.
|
||||
Актуальный основной скетч теперь находится в `../main-device/shine_homeserver_main/`; этот документ оставлен только как историческая справка по старому тестовому UI.
|
||||
|
||||
## Цель
|
||||
|
||||
Этот прототип проверяет базовую механику экранов, крупных кнопок, свайпов, первичную настройку Wi-Fi и настройку серверных адресов через общий экран редактирования текста.
|
||||
|
||||
На этом этапе отсутствуют:
|
||||
- логин/пароль учётной записи SHiNE;
|
||||
- PIN;
|
||||
- кошелёк;
|
||||
- QR;
|
||||
- регистрация;
|
||||
- PDA update/create транзакции;
|
||||
- входящие запросы.
|
||||
|
||||
## Экраны
|
||||
|
||||
Прототип содержит 10 экранов:
|
||||
- `HOME`
|
||||
- `SETTINGS_MENU`
|
||||
- `WIFI_SCREEN`
|
||||
- `SERVER_SCREEN`
|
||||
- `ACCOUNT_SCREEN`
|
||||
- `ACCOUNT_HOMESERVER_SCREEN`
|
||||
- `ACCOUNT_SECRET_SCREEN`
|
||||
- `SECRET_SHOW_SCREEN`
|
||||
- `SECRET_GENERATE_*`
|
||||
- `TEXT_EDIT_SCREEN`
|
||||
- `REGISTER_ACCOUNT_PLACEHOLDER`
|
||||
|
||||
## HOME
|
||||
|
||||
Показывает:
|
||||
- сверху слева значение homeserver или `homeserver not set`;
|
||||
- ниже значение логина или `login not set`;
|
||||
- справа от строки логина индикатор статуса Solana-аккаунта:
|
||||
- зелёный — все ключи совпадают;
|
||||
- красный — есть mismatch;
|
||||
- белый контур — пользователь не найден в Solana PDA;
|
||||
- рядом с индикатором краткий текст ошибки, если статус не зелёный;
|
||||
- третьей строкой `secret not set`, если секрет ещё не помечен как установленный;
|
||||
- сверху справа один ряд индикаторов:
|
||||
- процент батареи;
|
||||
- иконка батареи;
|
||||
- индикатор Wi-Fi уровня сигнала;
|
||||
- по центру крупный текст `STATUS`;
|
||||
- одна строка Wi-Fi вида `Wi-Fi (<ssid>) connected/disconnected`;
|
||||
- кнопка баланса вида `Balance: <value SOL>` или `Balance: failed to load`, по нажатию выполняет повторный запрос;
|
||||
- строка `SHiNE: <server> connected/account not configured/unavailable`;
|
||||
- при отсутствии пользователя в Solana PDA слева снизу появляется кнопка `REGISTER ACCOUNT`;
|
||||
- снизу кнопку `SETTINGS`, уменьшенную примерно до половины ширины экрана и сдвинутую к правому краю.
|
||||
- внизу на тёмной полосе подпись `SHiNE homeserver (v.0.18)`.
|
||||
|
||||
Строка Wi-Fi на `HOME`:
|
||||
- `Wi-Fi (not configured) not configured`
|
||||
- `Wi-Fi (<saved_ssid>) disconnected`
|
||||
- `Wi-Fi (<current_ssid>) connected`
|
||||
|
||||
Переходы:
|
||||
- кнопка `REGISTER ACCOUNT` -> `REGISTER_ACCOUNT_PLACEHOLDER`, только если пользователь не найден;
|
||||
- кнопка `SETTINGS` -> `SETTINGS_MENU`;
|
||||
- свайп влево -> `SETTINGS_MENU`.
|
||||
|
||||
Примечание:
|
||||
- поведение `REGISTER ACCOUNT -> REGISTER_ACCOUNT_PLACEHOLDER` относится к старой тестовой версии и не является актуальным для основного скетча.
|
||||
|
||||
Фоновая логика:
|
||||
- пока открыт `HOME`, экран сам обновляется примерно раз в секунду;
|
||||
- при наличии `login + secret + homeserver` и Wi-Fi устройство читает Solana `user_pda` напрямую через RPC;
|
||||
- сравниваются `root key`, `blockchain key`, `client key` и `homeserver` session-запись типа `100`;
|
||||
- для строки `SHiNE:` устройство держит отдельную WebSocket-сессию с сервером SHiNE:
|
||||
- авторизация через `AuthChallenge/CreateAuthSession` или `SessionChallenge/SessionLogin`;
|
||||
- session key = публичный `homeserver key`;
|
||||
- подтверждение создания сессии подписывается `client key`;
|
||||
- heartbeat выполняется `Ping` раз в минуту.
|
||||
|
||||
## SETTINGS_MENU
|
||||
|
||||
Показывает вертикальное меню из 3 пунктов:
|
||||
- `Wi-Fi`
|
||||
- `Server`
|
||||
- `Account`
|
||||
|
||||
Одновременно видны только 2 пункта.
|
||||
|
||||
Начальное состояние:
|
||||
- `Wi-Fi`
|
||||
- `Server`
|
||||
|
||||
После свайпа вверх:
|
||||
- `Server`
|
||||
- `Account`
|
||||
|
||||
После свайпа вниз:
|
||||
- `Wi-Fi`
|
||||
- `Server`
|
||||
|
||||
Обе видимые карточки меню имеют одинаковый цвет.
|
||||
|
||||
Переходы:
|
||||
- нажатие `Wi-Fi` -> `WIFI_SCREEN`;
|
||||
- нажатие `Server` -> `SERVER_SCREEN`;
|
||||
- нажатие `Account` -> `ACCOUNT_SCREEN`;
|
||||
- свайп вправо -> `HOME`.
|
||||
|
||||
## WIFI_SCREEN
|
||||
|
||||
Экран содержит 2 внутренних режима.
|
||||
|
||||
### 1. Overview
|
||||
|
||||
Показывает:
|
||||
- текущий Wi-Fi статус;
|
||||
- сохранённый SSID и число известных сетей;
|
||||
- статусное сообщение;
|
||||
- кнопку `SELECT NETWORK`;
|
||||
- кнопку `CLEAR SAVED WI-FI`;
|
||||
- кнопку `BACK`.
|
||||
|
||||
### 2. Scan Results
|
||||
|
||||
После `SELECT NETWORK` выполняется скан доступных Wi-Fi сетей.
|
||||
|
||||
Показывает:
|
||||
- заголовок `SELECT NETWORK`;
|
||||
- количество или результат сканирования;
|
||||
- список найденных SSID как крупные кнопки;
|
||||
- кнопку `SCAN AGAIN`;
|
||||
- кнопку `BACK`.
|
||||
|
||||
Нажатие на SSID открывает `TEXT_EDIT_SCREEN` для ввода пароля.
|
||||
|
||||
Поведение:
|
||||
- если для выбранного SSID пароль уже был сохранён раньше, он сразу подставляется в поле ввода;
|
||||
- если пароль меняют для другого SSID, старые сохранённые пароли других сетей не теряются;
|
||||
- после успешного подключения выбранная сеть становится текущей `wifi_ssid/wifi_pass`;
|
||||
- одновременно хранится до `8` известных сетей `SSID -> password`;
|
||||
- `CLEAR SAVED WI-FI` очищает все сохранённые сети и текущую сеть.
|
||||
|
||||
Переходы:
|
||||
- свайп вправо из любого режима `WIFI_SCREEN` -> `SETTINGS_MENU`
|
||||
- кнопка `BACK` -> `SETTINGS_MENU`
|
||||
|
||||
## SERVER_SCREEN
|
||||
|
||||
Показывает:
|
||||
- статусное сообщение;
|
||||
- текущий `Solana RPC` адрес;
|
||||
- кнопку `SOLANA RPC`;
|
||||
- текущий `SHiNE server login` или уже резолвленный адрес;
|
||||
- кнопку `SHiNE SERVER LOGIN`, если обычный `user PDA` ещё не зарегистрирован.
|
||||
|
||||
Значения по умолчанию:
|
||||
- Solana RPC: `https://api.mainnet-beta.solana.com`
|
||||
- SHiNE server login: `shineupme`
|
||||
|
||||
Нажатие на любую из двух кнопок открывает `TEXT_EDIT_SCREEN`.
|
||||
|
||||
Переходы:
|
||||
- свайп вправо -> `SETTINGS_MENU`
|
||||
|
||||
## ACCOUNT_SCREEN
|
||||
|
||||
Показывает:
|
||||
- заголовок `ACCOUNT`;
|
||||
- статусное сообщение;
|
||||
- кнопку `Login (<value|not set>)`;
|
||||
- кнопку `Homeserver (<value|not set>)`;
|
||||
- кнопку `Secret (<*****|not set>)`.
|
||||
|
||||
Переходы:
|
||||
- свайп вправо -> `SETTINGS_MENU`
|
||||
- `Login` -> `TEXT_EDIT_SCREEN`
|
||||
- `Homeserver` -> `ACCOUNT_HOMESERVER_SCREEN`
|
||||
- `Secret` -> `ACCOUNT_SECRET_SCREEN`
|
||||
|
||||
## ACCOUNT_HOMESERVER_SCREEN
|
||||
|
||||
Показывает:
|
||||
- текущий `homeserver`;
|
||||
- рекомендацию оставить `homeserver1`, если устройство одно;
|
||||
- кнопку `USE HOMESERVER1`;
|
||||
- кнопку `EDIT MANUALLY`;
|
||||
- кнопку `BACK`.
|
||||
|
||||
Переходы:
|
||||
- `USE HOMESERVER1` -> сохраняет `homeserver1` и возвращает в `ACCOUNT_SCREEN`
|
||||
- `EDIT MANUALLY` -> `TEXT_EDIT_SCREEN`
|
||||
- свайп вправо -> `ACCOUNT_SCREEN`
|
||||
|
||||
## ACCOUNT_SECRET_SCREEN
|
||||
|
||||
Показывает:
|
||||
- кнопку `SHOW SECRET` или серую `SECRET NOT SET`, если секрета ещё нет;
|
||||
- кнопку `ENTER SECRET MANUALLY (NOT RECOMMENDED)`;
|
||||
- кнопку `GENERATE SECRET`.
|
||||
|
||||
Переходы:
|
||||
- свайп вправо -> `ACCOUNT_SCREEN`
|
||||
- `SHOW SECRET` -> экран просмотра секрета
|
||||
- `ENTER SECRET MANUALLY (NOT RECOMMENDED)` -> `TEXT_EDIT_SCREEN`
|
||||
- `GENERATE SECRET` -> экран подтверждения генерации
|
||||
|
||||
## SECRET_SHOW_SCREEN
|
||||
|
||||
Показывает:
|
||||
- заголовок `SECRET`;
|
||||
- вертикально прокручиваемый список ключей;
|
||||
- `Secret (base58)`;
|
||||
- `Root key (base58)`;
|
||||
- `Root key priv (base58)`;
|
||||
- `Blockchain key (base58)`;
|
||||
- `Blockchain key priv (base58)`;
|
||||
- `Client key (base58)`;
|
||||
- `Client key priv (base58)`;
|
||||
- `Homeserver key (base58)`;
|
||||
- `Homeserver key priv (base58)`;
|
||||
- для каждого поля показывается формула derivation;
|
||||
- значения ключей показываются полными строками увеличенным шрифтом;
|
||||
- кнопку `BACK`.
|
||||
|
||||
## TEXT_EDIT_SCREEN
|
||||
|
||||
Общий экран редактирования строковых значений.
|
||||
|
||||
Используется для:
|
||||
- пароля Wi-Fi;
|
||||
- Solana RPC;
|
||||
- SHiNE server login.
|
||||
|
||||
Показывает:
|
||||
- заголовок;
|
||||
- поле ввода, уже заполненное старым значением;
|
||||
- новые символы всегда добавляются только в конец текста;
|
||||
- визуальный курсор не показывается;
|
||||
- пароль Wi-Fi показывается открыто, без маскировки точками;
|
||||
- кнопки `SAVE`, `CANCEL`, `DEL`, `CLR`;
|
||||
- большую экранную клавиатуру.
|
||||
|
||||
## REGISTER_ACCOUNT_PLACEHOLDER
|
||||
|
||||
Временный экран-заглушка регистрации.
|
||||
|
||||
Показывает:
|
||||
- заголовок `REGISTER ACCOUNT`;
|
||||
- сообщение, что регистрационный flow пока не реализован;
|
||||
- кнопку `BACK`.
|
||||
|
||||
## Клавиатура
|
||||
|
||||
Клавиатура единая для всех текстовых вводов.
|
||||
|
||||
Особенности:
|
||||
- занимает нижнюю часть экрана;
|
||||
- разбита на 2 страницы;
|
||||
- переключение страниц выполняется свайпом влево/вправо внутри `TEXT_EDIT_SCREEN`;
|
||||
- страница 1 содержит в основном буквы и базовые URL-символы;
|
||||
- страница 2 содержит цифры и дополнительные URL/символьные кнопки;
|
||||
- на символьных страницах должны быть доступны как минимум `!` и отдельная кнопка `SPACE`;
|
||||
- переключение `ABC/123` и `SHIFT` не должно очищать уже введённый текст;
|
||||
- переключение страниц клавиатуры свайпом тоже не должно очищать уже введённый текст, включая цифры, символы и уже набранные заглавные буквы;
|
||||
- есть специальные действия `DEL` и `CLR`.
|
||||
|
||||
## Хранение Wi-Fi
|
||||
|
||||
Используется `Preferences` (NVS памяти ESP32):
|
||||
- `wifi_ssid`
|
||||
- `wifi_pass`
|
||||
- `wifi_known_good`
|
||||
- до `8` сохранённых пар `SSID -> password`
|
||||
|
||||
При старте устройства, если сохранён SSID, выполняется попытка подключения к текущей сохранённой сети.
|
||||
|
||||
Если пользователь уже вводил пароль для сети раньше:
|
||||
- при повторном выборе этого SSID старый пароль сразу подставляется в editor screen;
|
||||
- сохранение пароля для другой сети не удаляет уже сохранённые пароли остальных сетей.
|
||||
|
||||
Если сеть раньше уже была успешно подключена и помечена как валидная:
|
||||
- после потери связи устройство автоматически пытается переподключиться;
|
||||
- первые повторные попытки идут раз в `10` секунд;
|
||||
- если связи нет долго, интервал увеличивается до `30` секунд.
|
||||
- пока открыт `HOME`, экран сам обновляется примерно раз в секунду и подтягивает актуальные:
|
||||
- статус Wi-Fi;
|
||||
- SSID;
|
||||
- уровень сигнала;
|
||||
- процент батареи.
|
||||
|
||||
## Хранение серверов
|
||||
|
||||
Используется `Preferences` (NVS памяти ESP32):
|
||||
- `solana_rpc`
|
||||
- `shine_server_login`
|
||||
|
||||
## Хранение аккаунта
|
||||
|
||||
Используется `Preferences` (NVS памяти ESP32):
|
||||
- `login`
|
||||
- `homeserver`
|
||||
- `secret_set`
|
||||
|
||||
## Детали клавиатуры
|
||||
|
||||
- клавиатура занимает примерно `2/3`-`3/4` высоты экрана;
|
||||
- сверху остаются только заголовок и поле ввода;
|
||||
- буквы занимают 3 ряда;
|
||||
- половина букв находится на левой странице, половина на правой;
|
||||
- на правой странице кнопки тоже стоят в ровных колонках, без сдвига рядов вправо;
|
||||
- отдельный режим `symbols` тоже разделён на 2 страницы;
|
||||
- в режиме `symbols` на правой странице есть отдельная кнопка `SPACE` и отдельная кнопка `!`;
|
||||
- 3 основных ряда клавиш и нижний служебный ряд увеличены по высоте;
|
||||
- клавиши дополнительно увеличены по высоте по сравнению с предыдущим промежуточным вариантом;
|
||||
- четвёртый ряд содержит:
|
||||
- переключение `ABC/123`
|
||||
- `SHIFT`
|
||||
- `DEL`
|
||||
- `SAVE`
|
||||
- `CANCEL`
|
||||
- ниже рамки клавиатурного блока остаётся отдельная тёмная полоса с версией `SHiNE homeserver (v.0.18)`.
|
||||
|
||||
## Жесты
|
||||
|
||||
Поддерживаются направления:
|
||||
- `SWIPE_LEFT`
|
||||
- `SWIPE_RIGHT`
|
||||
- `SWIPE_UP`
|
||||
- `SWIPE_DOWN`
|
||||
|
||||
Маршруты:
|
||||
- `HOME`: свайп влево -> `SETTINGS_MENU`
|
||||
- `SETTINGS_MENU`: свайп вправо -> `HOME`
|
||||
- `SETTINGS_MENU`: свайп вверх/вниз -> прокрутка списка
|
||||
- `WIFI_SCREEN`: свайп вправо -> `SETTINGS_MENU`
|
||||
- `SERVER_SCREEN`: свайп вправо -> `SETTINGS_MENU`
|
||||
- `ACCOUNT_SCREEN`: свайп вправо -> `SETTINGS_MENU`
|
||||
- `ACCOUNT_HOMESERVER_SCREEN`: свайп вправо -> `ACCOUNT_SCREEN`
|
||||
- `ACCOUNT_SECRET_SCREEN`: свайп вправо -> `ACCOUNT_SCREEN`
|
||||
- `TEXT_EDIT_SCREEN`: свайп влево/вправо -> переключение страниц клавиатуры
|
||||
- переключение страниц клавиатуры срабатывает только если свайп начался в зоне самой клавиатуры, а не по всему экрану редактора
|
||||
|
||||
## Особенность обработки жестов
|
||||
|
||||
- если палец ушёл из зоны обычного тапа и жест определяется как свайп, случайное попадание на кнопку по пути не должно превращать свайп в нажатие;
|
||||
- приоритет у свайпа, а не у `click`.
|
||||
- одно непрерывное касание может вызвать не более одного действия кнопки;
|
||||
- скольжение пальцем по экранной клавиатуре не должно вызывать цепочку из нескольких нажатий;
|
||||
- периодический refresh на `HOME` не должен срабатывать во время активного касания.
|
||||
|
||||
## Язык
|
||||
|
||||
Пока интерфейс отображается в ASCII/English, чтобы использовать подтверждённый рабочий шрифтовой путь на устройстве.
|
||||
@@ -0,0 +1,681 @@
|
||||
# SHiNE ESP32 Homeserver UI Spec
|
||||
|
||||
## Назначение
|
||||
|
||||
Этот документ описывает актуальный UI-прототип homeserver `SHiNE` для платы `Waveshare ESP32-S3-Touch-AMOLED-2.16`.
|
||||
Актуальный основной Arduino-скетч лежит в `../main-device/shine_homeserver_main/`.
|
||||
|
||||
Документ является источником истины для Arduino-скетча:
|
||||
|
||||
- если меняется этот документ, должен меняться и скетч;
|
||||
- если меняется скетч, должен обновляться и этот документ;
|
||||
- экраны, кнопки, поля, статусы, переходы и тексты не должны расходиться.
|
||||
|
||||
## Текущий объём реализации
|
||||
|
||||
Текущая реализация является интерактивным прототипом экрана устройства, пригодным для ручной проверки на железе.
|
||||
|
||||
Что уже входит в прототип:
|
||||
|
||||
- локальный UI на тач-экране;
|
||||
- хранение настроек и секретов во внутренней памяти `ESP32` через `NVS`;
|
||||
- экранный UI устройства работает на английском языке, потому что кириллица на этом шрифтовом пути пока не поддерживается стабильно;
|
||||
- экран пополнения с реальным `solana:` URI и рисованием QR-кода через `LVGL`;
|
||||
- реальное подключение к `Wi-Fi` по сохранённым `SSID/паролю`;
|
||||
- реальная проверка доступности `API`, `RPC` и `WS`-адресов;
|
||||
- реальное чтение баланса кошелька из `Solana RPC`;
|
||||
- проверка обязательных условий перед регистрацией;
|
||||
- живая on-chain регистрация серверного `user_pda` в `shine_users` по нажатию кнопки на главном экране;
|
||||
- живой экран заявок на подключение новых устройств через доверенную homeserver-сессию;
|
||||
- PIN-блокировка (в текущей временной сборке вход по PIN отключён, устройство открывает `HOME` сразу после старта);
|
||||
- базовые настройки, статус и главный экран;
|
||||
- сохранение `PDA` и `tx signature` после успешной регистрации.
|
||||
- создание и возобновление серверной сессии `SHiNE` через WebSocket с `sessionType = 100` и `clientPlatform = "ESP32"`.
|
||||
|
||||
Что пока считается именно прототипом, а не финальной интеграцией:
|
||||
|
||||
- пока реализован только сценарий заявок на подключение устройств через доверенную сессию;
|
||||
- другие типы входящих запросов на подпись и произвольные approval-flow на этом устройстве ещё не подключены.
|
||||
|
||||
## Основная идея устройства
|
||||
|
||||
Устройство работает как отдельный homeserver:
|
||||
|
||||
- хранит секрет на самом устройстве;
|
||||
- позволяет ввести логин, секрет и имя homeserver;
|
||||
- показывает адрес кошелька устройства;
|
||||
- позволяет пополнить баланс перед регистрацией;
|
||||
- после выполнения условий даёт зарегистрировать устройство как homeserver;
|
||||
- после авторизации в SHiNE может подтверждать заявки на подключение новых устройств пользователя.
|
||||
|
||||
`SD`-карта не нужна для постоянного хранения секрета в этом прототипе.
|
||||
Основное сохранение идёт во внутреннюю flash-память через `NVS`.
|
||||
|
||||
## Данные, которые хранятся на устройстве
|
||||
|
||||
Прототип хранит:
|
||||
|
||||
- `PIN`;
|
||||
- `Wi-Fi SSID`;
|
||||
- `Wi-Fi password`;
|
||||
- `login`;
|
||||
- `session/homeserver name`;
|
||||
- `master secret`;
|
||||
- `wallet address`;
|
||||
- `user pda address`;
|
||||
- `registration signature`;
|
||||
- `balance`;
|
||||
- `server login` для первичной привязки;
|
||||
- `resolved server api url` / `rpc url` / `ws url` после чтения PDA сервера;
|
||||
- флаги:
|
||||
`wifiReady`, `serversReady`, `secretReady`, `registered`, `online`.
|
||||
|
||||
Для первой регистрации обычного `user PDA` устройство берёт `createdAtMs` из NTP прямо перед отправкой транзакции в Solana. При последующих обновлениях `user PDA` устройство так же берёт актуальный `updatedAtMs` из NTP перед отправкой update-транзакции. Дальше в `user PDA` сохраняется `accessServers`, где по умолчанию лежит `shineupme`.
|
||||
|
||||
## Правило серверной сессии SHiNE
|
||||
|
||||
При подключении к серверу `SHiNE` устройство должно авторизовываться как homeserver-сеанс:
|
||||
|
||||
- `sessionType = 100`
|
||||
- `clientPlatform = "ESP32"`
|
||||
- `clientInfo = "ESP32 homeserver:<homeserverName>"`
|
||||
|
||||
Это относится и к `CreateAuthSession`, и к `SessionLogin`.
|
||||
|
||||
## Правила готовности к регистрации
|
||||
|
||||
Кнопка регистрации доступна только если одновременно выполнены условия:
|
||||
|
||||
1. настроен и подтверждён `Wi-Fi`;
|
||||
2. задан и подтверждён `SHiNE server login`;
|
||||
3. задан логин;
|
||||
4. сгенерирован или введён секрет;
|
||||
5. баланс кошелька не меньше `0.20 SOL`;
|
||||
6. устройство ещё не зарегистрировано.
|
||||
|
||||
Если хотя бы одно условие не выполнено, главный экран показывает, чего именно не хватает.
|
||||
|
||||
## Экранная модель
|
||||
|
||||
В прототипе используются следующие экраны:
|
||||
|
||||
1. `LOCK`
|
||||
2. `HOME`
|
||||
3. `STATUS`
|
||||
4. `CONNECTION`
|
||||
5. `WIFI_EDIT`
|
||||
6. `SERVERS`
|
||||
7. `ACCOUNT`
|
||||
8. `WALLET`
|
||||
9. `WALLET_QR`
|
||||
10. `WALLET_SIGN_REQUEST`
|
||||
11. `REQUESTS`
|
||||
12. `REQUEST_DETAIL`
|
||||
13. `SETTINGS`
|
||||
14. `PIN_EDIT`
|
||||
15. `TEXT_INPUT`
|
||||
16. `CONFIRM`
|
||||
17. `REGISTER_ACCOUNT_CONFIRM`
|
||||
18. `REGISTER_ACCOUNT_RESULT`
|
||||
|
||||
## Общие правила интерфейса
|
||||
|
||||
- Верхняя строка всегда показывает краткий статус устройства:
|
||||
`PIN`, `Wi-Fi`, `сервер`, `регистрация`.
|
||||
- В правом верхнем углу рядом с батареей:
|
||||
- сначала процент заряда;
|
||||
- затем, если устройство реально заряжается, маленькая иконка молнии;
|
||||
- затем контур батареи;
|
||||
- затем индикатор `Wi-Fi`.
|
||||
- Основной язык прототипа: английский.
|
||||
- Для вывода текста в текущей временной сборке используется стандартный шрифт `Arduino_GFX`.
|
||||
- Русские строки в UI устройства пока не использовать.
|
||||
- Кнопки крупные, с тач-ориентированным размером.
|
||||
- Опасные действия подтверждаются отдельным диалогом.
|
||||
- После изменения данных конфигурация сразу сохраняется в `NVS`.
|
||||
|
||||
## Экран LOCK
|
||||
|
||||
Назначение:
|
||||
|
||||
- экран блокировки предусмотрен в UI, но в текущей временной сборке после запуска пропускается;
|
||||
- вход по PIN временно отключён для отладки остальных экранов.
|
||||
|
||||
Отображается:
|
||||
|
||||
- заголовок `SHiNE Device`;
|
||||
- статус `Устройство заблокировано`;
|
||||
- поле ввода PIN в виде маски;
|
||||
- кнопки цифровой клавиатуры;
|
||||
- кнопки `Стереть` и `Открыть`.
|
||||
|
||||
Поведение:
|
||||
|
||||
- если PIN введён верно, открывается `HOME`;
|
||||
- если PIN неверный, показывается ошибка `Неверный PIN`;
|
||||
- в текущей временной сборке этот экран не показывается автоматически при старте.
|
||||
|
||||
## Экран HOME
|
||||
|
||||
Это основной экран устройства.
|
||||
|
||||
Показывает:
|
||||
|
||||
- крупный статус регистрации;
|
||||
- имя логина;
|
||||
- имя homeserver;
|
||||
- строку `Wi-Fi: <SSID> connected|disconnected`;
|
||||
- строку `SHiNE: <server address> connected|unavailable`;
|
||||
- короткий статус баланса.
|
||||
|
||||
Особенности верхнего блока:
|
||||
|
||||
- зелёный/контурный статусный кружок аккаунта расположен слева от строки логина;
|
||||
- блок `STATUS` поднят выше относительно предыдущей версии;
|
||||
- если состояние хорошее, слово `connected` в строках `Wi-Fi` и `SHiNE` показывается зелёным.
|
||||
|
||||
В зоне кошелька:
|
||||
|
||||
- вместо старых двух кнопок `баланс` и `QR` показывается одна широкая кнопка;
|
||||
- текст кнопки: `Wallet: <selected wallet name>`;
|
||||
- доступные имена:
|
||||
- `ClientKey`
|
||||
- `RootKey`
|
||||
- либо сохранённое имя `custom`-кошелька;
|
||||
- после старта устройства баланс активного выбранного кошелька пытается загрузиться автоматически, если уже есть секрет и `Wi-Fi`;
|
||||
- нажатие на эту кнопку открывает экран `WALLET`.
|
||||
|
||||
Нижние кнопки:
|
||||
|
||||
- `Статус`
|
||||
- `Подключение`
|
||||
- `Аккаунт`
|
||||
- `Wallet`
|
||||
- `Запросы`
|
||||
- `Настройки`
|
||||
|
||||
Дополнительная большая кнопка:
|
||||
|
||||
- `REGISTER ACCOUNT`
|
||||
- либо жёлтая `ADD HOMESERVER`
|
||||
- либо жёлтая `FIX HOMESERVER PASSWORD`
|
||||
|
||||
Если регистрация уже сделана:
|
||||
|
||||
- если пользователь создан, но в `PDA` ещё нет сессии текущего homeserver, показывается жёлтая кнопка `ADD HOMESERVER`;
|
||||
- если в `PDA` есть homeserver с тем же именем, но с другим ключом, показывается жёлтая кнопка `FIX HOMESERVER PASSWORD`;
|
||||
- если и пользователь, и homeserver-сессия уже корректны, вместо призыва к регистрации показывается статус `Homeserver активен`.
|
||||
- две нижние кнопки внизу экрана не прилегают вплотную друг к другу, между ними есть небольшой зазор.
|
||||
|
||||
## Экран REGISTER_ACCOUNT_CONFIRM
|
||||
|
||||
Показывает предварительную проверку перед отправкой транзакции регистрации.
|
||||
|
||||
Отображается:
|
||||
|
||||
- повторный `login`;
|
||||
- сообщение о том, что логин свободен или уже занят;
|
||||
- баланс кошелька с проверкой порога `0.020 SOL`;
|
||||
- имя homeserver;
|
||||
- пометка, если используется стандартное значение `homeserver1`;
|
||||
- отдельное предупреждение, если `Wi-Fi` не подключён.
|
||||
|
||||
Кнопки:
|
||||
|
||||
- `ЗАРЕГИСТРИРОВАТЬ В СИЯНИИ`
|
||||
- `BACK`
|
||||
|
||||
Поведение:
|
||||
|
||||
- если `Wi-Fi` не подключён, на экране прямо показывается уведомление об этом и кнопка регистрации недоступна;
|
||||
- если `login` уже занят в `shine_users`, регистрация недоступна;
|
||||
- если баланс меньше `0.020 SOL`, на экране показывается соответствующая ошибка;
|
||||
- если все проверки пройдены, кнопка регистрации активна и запускает on-chain регистрацию.
|
||||
|
||||
## Экран REGISTER_ACCOUNT_RESULT
|
||||
|
||||
Показывает результат отправки регистрации.
|
||||
|
||||
Отображается:
|
||||
|
||||
- текст успеха или ошибки;
|
||||
- подробное сообщение;
|
||||
- при успехе краткий `user_pda`;
|
||||
- при успехе краткий `tx signature`.
|
||||
|
||||
Кнопки:
|
||||
|
||||
- `BACK HOME`
|
||||
- `ACCOUNT`
|
||||
|
||||
Поведение:
|
||||
|
||||
- после успешной регистрации данные `user_pda` и `tx signature` сохраняются в `NVS`;
|
||||
- при ошибке на экране показывается причина отказа;
|
||||
- если ошибку вернул `sendTransaction`, экран старается показать не только общий текст, но и детали `RPC`/preflight/simulate-логов.
|
||||
|
||||
## Экран HOMESERVER_PDA_CONFIRM
|
||||
|
||||
Показывает, что именно не так с homeserver-секцией уже существующей пользовательской `PDA`.
|
||||
|
||||
Отображается:
|
||||
|
||||
- причина (`homeserver` отсутствует в `PDA` или ключ не совпадает с локальным секретом);
|
||||
- `login`;
|
||||
- имя `homeserver`;
|
||||
- короткое пояснение, что именно будет сделано.
|
||||
|
||||
Кнопки:
|
||||
|
||||
- `ADD HOMESERVER` или `FIX HOMESERVER PASSWORD`
|
||||
- `BACK`
|
||||
|
||||
Поведение:
|
||||
|
||||
- если `Wi-Fi` не подключён, действие недоступно;
|
||||
- экран не создаёт нового пользователя, а запускает `update_user_pda`;
|
||||
- при `ADD HOMESERVER` в блок `sessions` добавляется запись `session_type=100`;
|
||||
- при `FIX HOMESERVER PASSWORD` обновляется публичный ключ уже существующей записи `homeserver`.
|
||||
|
||||
## Экран HOMESERVER_PDA_RESULT
|
||||
|
||||
Показывает результат обновления `sessions` в пользовательской `PDA`.
|
||||
|
||||
Отображается:
|
||||
|
||||
- успех или ошибка;
|
||||
- короткое сообщение;
|
||||
- при успехе краткий `tx signature`.
|
||||
|
||||
Кнопки:
|
||||
|
||||
- `BACK HOME`
|
||||
- `ACCOUNT`
|
||||
|
||||
Поведение:
|
||||
|
||||
- при ошибке текст ошибки сохраняется в ту же USB/NVS-диагностику, что и регистрация;
|
||||
- после успешного обновления выполняется повторная проверка `PDA`, и основной экран должен перейти в состояние `ok`.
|
||||
|
||||
## Экран STATUS
|
||||
|
||||
Показывает сводку:
|
||||
|
||||
- логин;
|
||||
- homeserver;
|
||||
- есть ли секрет;
|
||||
- зарегистрировано ли устройство;
|
||||
- подключён ли Wi-Fi;
|
||||
- доступны ли серверы;
|
||||
- хватает ли баланса;
|
||||
- находится ли устройство онлайн;
|
||||
- краткий отпечаток `PDA` или `tx`.
|
||||
|
||||
Кнопки:
|
||||
|
||||
- `Назад`
|
||||
- `Обновить статус`
|
||||
|
||||
`Обновить статус` в прототипе не делает сеть, а просто перерисовывает текущие вычисленные состояния.
|
||||
|
||||
## Экран CONNECTION
|
||||
|
||||
Показывает:
|
||||
|
||||
- `Wi-Fi`: готов / не готов;
|
||||
- `Серверы`: готовы / не готовы;
|
||||
- `Онлайн`: да / нет.
|
||||
|
||||
Кнопки:
|
||||
|
||||
- `Wi-Fi`
|
||||
- `Серверы`
|
||||
- `Подключить`
|
||||
- `Отключить`
|
||||
- `Назад`
|
||||
|
||||
Поведение:
|
||||
|
||||
- `Подключить` переводит устройство в `online=true`, если `Wi-Fi` реально подключён и серверы реально проверены;
|
||||
- `Отключить` переводит устройство в `online=false`.
|
||||
|
||||
## Экран WIFI_EDIT
|
||||
|
||||
Поля:
|
||||
|
||||
- `SSID`
|
||||
- `Пароль`
|
||||
|
||||
Кнопки:
|
||||
|
||||
- `Изменить SSID`
|
||||
- `Изменить пароль`
|
||||
- `Проверить`
|
||||
- `Сбросить`
|
||||
- `Назад`
|
||||
|
||||
Поведение:
|
||||
|
||||
- `Проверить` делает реальную попытку подключения к `Wi-Fi`;
|
||||
- `Сбросить` очищает обе строки и ставит `wifiReady=false`.
|
||||
|
||||
## Экран SERVERS
|
||||
|
||||
Поля:
|
||||
|
||||
- `API URL`
|
||||
- `RPC URL`
|
||||
- `WS URL`
|
||||
|
||||
Кнопки:
|
||||
|
||||
- `Изменить API`
|
||||
- `Изменить RPC`
|
||||
- `Изменить WS`
|
||||
- `Проверить`
|
||||
- `Тестовые`
|
||||
- `Назад`
|
||||
|
||||
Поведение:
|
||||
|
||||
- `Тестовые` подставляет дефолтные тестовые значения;
|
||||
- `Проверить` делает реальные сетевые проверки:
|
||||
- `API URL` должен отвечать по `HTTP/HTTPS`;
|
||||
- `RPC URL` должен отвечать на `Solana JSON-RPC`;
|
||||
- `WS URL` должен принимать `TCP/TLS`-соединение.
|
||||
|
||||
## Экран ACCOUNT
|
||||
|
||||
Показывает:
|
||||
|
||||
- логин;
|
||||
- имя homeserver;
|
||||
- статус секрета;
|
||||
- короткий отпечаток секрета;
|
||||
- статус регистрации;
|
||||
- короткий отпечаток `PDA` или `tx`.
|
||||
|
||||
Кнопки:
|
||||
|
||||
- `Изменить логин`
|
||||
- `Секрет`
|
||||
- `Имя homeserver`
|
||||
- `Сгенерировать`
|
||||
- `Очистить`
|
||||
- `Назад`
|
||||
|
||||
Поведение:
|
||||
|
||||
- `Сгенерировать` создаёт новый `master secret` и пересчитывает из него `device`-кошелёк;
|
||||
- `Очистить` удаляет секрет, адрес кошелька, `PDA`, `tx`, регистрацию и онлайн-статус;
|
||||
- логин приводится к нижнему регистру и trim.
|
||||
- после успешной регистрации на экране сохраняются и отображаются краткие отпечатки `PDA` и `TX`.
|
||||
|
||||
## Экран WALLET
|
||||
|
||||
Показывает:
|
||||
|
||||
- кнопку `Wallet: <selected wallet name>`;
|
||||
- строку текущего статуса/баланса активного кошелька;
|
||||
- сокращённый публичный ключ активного кошелька.
|
||||
|
||||
Кнопки:
|
||||
|
||||
- `Wallet: <selected wallet name>`
|
||||
- `SHOW BALANCE`
|
||||
- `SHOW WALLET QR`
|
||||
|
||||
Поведение:
|
||||
|
||||
- верхняя кнопка открывает экран выбора кошелька `WALLET_SELECT`;
|
||||
- `Показать баланс кошелька` читает реальный баланс именно активного выбранного кошелька из `Solana RPC`;
|
||||
- `Показать QR-код кошелька` открывает экран `WALLET_QR`;
|
||||
- этот экран не меняет логику Solana-регистрации пользователя: on-chain регистрация и проверки `PDA` по-прежнему завязаны на `client key`.
|
||||
|
||||
## Экран WALLET_SELECT
|
||||
|
||||
Показывает:
|
||||
|
||||
- строку `Current: <selected wallet name>`;
|
||||
- три кнопки выбора:
|
||||
- `ClientKey`
|
||||
- `RootKey`
|
||||
- `Custom` или `Custom: <имя>`;
|
||||
- у текущего выбора видна галочка.
|
||||
|
||||
Поведение:
|
||||
|
||||
- `ClientKey` активирует кошелёк, выведенный из suffix `client.key`;
|
||||
- `RootKey` активирует кошелёк, выведенный из suffix `root.key`;
|
||||
- `Custom` использует derivation:
|
||||
`sha256(base64(secret32) + "|wallet." + customName)`;
|
||||
- если имя `custom`-кошелька ещё не задано, нажатие `Custom` открывает экран текстового ввода;
|
||||
- если `custom` уже выбран, повторное нажатие на него открывает экран редактирования имени;
|
||||
- после выбора кошелька устройство возвращается на экран `WALLET`.
|
||||
|
||||
## Экран WALLET_QR
|
||||
|
||||
Экран показывает:
|
||||
|
||||
- крупный реальный `QR` для строки `solana:<wallet_address>`;
|
||||
- снизу крупный текст самого адреса кошелька.
|
||||
|
||||
Поведение:
|
||||
|
||||
- отдельная текстовая подсказка возврата не показывается;
|
||||
- возврат на главный экран выполняется обычным тапом по экрану.
|
||||
|
||||
Показывает:
|
||||
|
||||
- QR-код для строки вида:
|
||||
`solana:<wallet>`;
|
||||
- мелкую подпись с полным адресом кошелька под QR.
|
||||
|
||||
Поведение:
|
||||
|
||||
- QR должен быть сканируемым, а не декоративным;
|
||||
- адрес кошелька берётся из текущего активного выбранного кошелька;
|
||||
- нажатие в любую точку экрана возвращает пользователя на `WALLET`.
|
||||
|
||||
## Экран WALLET_SIGN_REQUEST
|
||||
|
||||
Экран показывается, когда браузерное wallet-расширение присылает на ESP32 запрос `sign_transaction`.
|
||||
|
||||
Показывает:
|
||||
|
||||
- заголовок `SIGN REQUEST`;
|
||||
- вопрос о подписи транзакции текущим активным кошельком;
|
||||
- сокращённый публичный ключ кошелька;
|
||||
- комментарий, присланный полем `comment`;
|
||||
- две кнопки:
|
||||
- `REJECT`
|
||||
- `APPROVE`
|
||||
|
||||
Поведение:
|
||||
|
||||
- если пользователь нажимает `APPROVE`, ESP32 подписывает транзакцию текущим активным кошельком и отправляет ответ в расширение;
|
||||
- если пользователь нажимает `REJECT`, ESP32 отправляет отказ `rejected_by_user`;
|
||||
- пока запрос активен, свайпом этот экран не закрывается;
|
||||
- после ответа расширению устройство возвращается на предыдущий экран.
|
||||
|
||||
## Экран REQUESTS
|
||||
|
||||
Экран доступен только если homeserver уже авторизован в SHiNE.
|
||||
|
||||
Показывает:
|
||||
|
||||
- слева сверху кнопку `REFRESH`;
|
||||
- заголовок `REQUESTS:` немного правее стандартного левого положения;
|
||||
- справа сверху только большую цифру числа активных заявок;
|
||||
- ниже прокручиваемый список активных pairing-заявок;
|
||||
- на экране одновременно видны примерно две плитки.
|
||||
|
||||
Каждая плитка показывает:
|
||||
|
||||
- код подключения из `10` цифр в виде `5` пар: `XX XX XX XX XX`;
|
||||
- строку `Session: <platform/name>`;
|
||||
- строку `Kind: Client session` или `Kind: Wallet session`.
|
||||
|
||||
Поведение:
|
||||
|
||||
- список берётся из живой операции `ListTrustedDeviceLoginRequests`;
|
||||
- если заявок нет, экран показывает `No active requests`;
|
||||
- отдельная строка вида `Active requests: N` на этом экране не показывается;
|
||||
- вертикальный скролл позволяет просматривать все активные заявки;
|
||||
- нажатие по плитке открывает `REQUEST_DETAIL`;
|
||||
- свайп вправо возвращает в `SETTINGS`.
|
||||
|
||||
## Экран REQUEST_DETAIL
|
||||
|
||||
Показывает детали выбранной pairing-заявки:
|
||||
|
||||
- вопрос `Connect session ...?`;
|
||||
- код подключения `XX XX XX XX XX`;
|
||||
- строку `Session: <platform/name>`;
|
||||
- строку `Kind: Client session` или `Kind: Wallet session`;
|
||||
- пояснение:
|
||||
- для client session: `Only client key will be transferred. No additional keys will be sent.`
|
||||
- для wallet session: `No keys will be transferred.`
|
||||
|
||||
Кнопки:
|
||||
|
||||
- `YES`
|
||||
- `NO`
|
||||
|
||||
Поведение:
|
||||
|
||||
- `YES` подтверждает заявку:
|
||||
- для client session устройство передаёт только `client key`;
|
||||
- для wallet session устройство выпускает отдельную `wallet-session` без передачи ключей;
|
||||
- `NO` отклоняет заявку;
|
||||
- после любого решения устройство возвращается в список `REQUESTS` и обновляет его;
|
||||
- свайп вправо возвращает в `REQUESTS`.
|
||||
|
||||
## Экран SETTINGS
|
||||
|
||||
Показывает вертикальное меню крупных пунктов.
|
||||
|
||||
Если homeserver ещё не авторизован в SHiNE:
|
||||
|
||||
- `1. Wi-Fi`
|
||||
- `2. Server`
|
||||
- `3. Account`
|
||||
|
||||
Если homeserver уже авторизован:
|
||||
|
||||
- `1. Device requests`
|
||||
- `2. Wi-Fi`
|
||||
- `3. Server`
|
||||
- `4. Account`
|
||||
|
||||
Поведение:
|
||||
|
||||
- одновременно видны только две карточки;
|
||||
- список листается свайпом вверх/вниз;
|
||||
- свайп вправо возвращает на `HOME`;
|
||||
- пункт `Device requests` должен быть первым и появляется только для авторизованного homeserver.
|
||||
|
||||
## Экран PIN_EDIT
|
||||
|
||||
Используется для ввода нового PIN.
|
||||
|
||||
Правила:
|
||||
|
||||
- допустимы только цифры;
|
||||
- длина PIN: 4..8 символов;
|
||||
- после сохранения новый PIN немедленно пишется в `NVS`.
|
||||
|
||||
## Экран TEXT_INPUT
|
||||
|
||||
Это общий экран редактирования текстовых полей.
|
||||
|
||||
Используется для:
|
||||
|
||||
- `SSID`
|
||||
- `Пароль Wi-Fi`
|
||||
- `Логин`
|
||||
- `Имя homeserver`
|
||||
- `API URL`
|
||||
- `RPC URL`
|
||||
- `WS URL`
|
||||
|
||||
Состав экрана:
|
||||
|
||||
- заголовок поля;
|
||||
- текущее значение;
|
||||
- программная клавиатура;
|
||||
- кнопки `Стереть`, `OK`, `Отмена`.
|
||||
|
||||
## Экран CONFIRM
|
||||
|
||||
Это модальный экран подтверждения.
|
||||
|
||||
Используется для:
|
||||
|
||||
- регистрации;
|
||||
- очистки секрета;
|
||||
- полного сброса.
|
||||
|
||||
Кнопки:
|
||||
|
||||
- `Подтвердить`
|
||||
- `Отмена`
|
||||
|
||||
## Сценарий первой настройки
|
||||
|
||||
Ожидаемый путь пользователя:
|
||||
|
||||
1. открыть устройство сразу после старта; экран PIN временно не требуется;
|
||||
2. открыть `Подключение -> Wi-Fi`;
|
||||
3. ввести `SSID` и пароль, нажать `Проверить`;
|
||||
4. открыть `Подключение -> Серверы`;
|
||||
5. проверить или задать `SHiNE server login` (по умолчанию `shineupme`);
|
||||
6. открыть `Аккаунт`;
|
||||
7. ввести логин;
|
||||
8. задать имя homeserver;
|
||||
9. сгенерировать секрет;
|
||||
10. открыть `Кошелёк`;
|
||||
11. при необходимости пополнить баланс;
|
||||
12. вернуться на `HOME`;
|
||||
13. нажать `REGISTER ACCOUNT`;
|
||||
14. на экране проверки ещё раз увидеть `login`, статус свободного `PDA`, баланс, `homeserver1`, серверный login и при необходимости сообщение о неподключённом `Wi-Fi`;
|
||||
15. нажать `ЗАРЕГИСТРИРОВАТЬ В СИЯНИИ`;
|
||||
16. после завершения увидеть либо экран успеха с `user_pda` и `tx signature`, либо подробную ошибку;
|
||||
17. после успешной регистрации увидеть статус `Homeserver активен`.
|
||||
|
||||
Примечание:
|
||||
|
||||
- устройство реально отправляет `create_user_pda` в `shine_users`, а после подтверждения сохраняет `PDA` и `tx signature`;
|
||||
- при первой регистрации для обычного `user PDA` не заполняется `serverAddress`, а `accessServers` получает `shineupme` или другой выбранный `SHiNE server login`.
|
||||
|
||||
## Сценарий входящего запроса
|
||||
|
||||
1. открыть `Запросы`;
|
||||
2. выбрать один из запросов;
|
||||
3. прочитать детали;
|
||||
4. нажать `Разрешить` или `Отклонить`;
|
||||
5. убедиться, что статус запроса изменился.
|
||||
|
||||
## Временный режим текста
|
||||
|
||||
В текущей диагностической версии:
|
||||
|
||||
- строковые литералы экранного UI должны оставаться английскими ASCII-совместимыми;
|
||||
- возвращение кириллицы допустимо только после отдельной доработки шрифтов и реальной проверки на устройстве;
|
||||
- рендер выполняется стандартным шрифтом `Arduino_GFX`;
|
||||
- это обходной режим, пока `U8g2`-шрифты на устройстве не начнут рисоваться стабильно.
|
||||
|
||||
## Критерии ручной проверки
|
||||
|
||||
Минимально нужно проверить:
|
||||
|
||||
1. устройство загружается и сразу открывает `HOME`; экран блокировки временно отключён;
|
||||
2. весь экранный текст отображается читаемо на английском без замены символов;
|
||||
3. ввод по экранной клавиатуре работает;
|
||||
4. после перезагрузки сохранённые поля остаются в памяти;
|
||||
5. секрет и адрес кошелька сохраняются на устройстве;
|
||||
6. экран `QR и URI` рисует читаемый QR-код;
|
||||
7. регистрация блокируется, пока условия не выполнены;
|
||||
8. после выполнения условий регистрация становится доступной;
|
||||
9. список запросов и экран подтверждения работают;
|
||||
10. полный сброс действительно очищает сохранённое состояние.
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
# Задание для Айдара: навести порядок в инструкциях агентов SHiNE
|
||||
|
||||
## Кратко
|
||||
Нужно согласовать и оформить единый порядок инструкций для Codex/Telegram-агентов в проекте SHiNE, чтобы агенты стабильно понимали структуру проекта, границы ответственности и правила работы с сервером, UI, Solana-модулем, Telegram-ботом и игроками.
|
||||
|
||||
## Зачем это нужно
|
||||
Сейчас проект состоит из нескольких связанных, но разных частей:
|
||||
|
||||
- основной сервер `SHiNE-server/`;
|
||||
- UI `shine-UI/`;
|
||||
- Solana/Anchor-модуль `shine-solana/shine/`;
|
||||
- Telegram-агент-кодер `SHiNE-agent-bot-coder/`;
|
||||
- TURN-сервер;
|
||||
- документация `Dev_Docs/`;
|
||||
- отдельные рабочие папки игроков `Players/`.
|
||||
|
||||
Без явных инструкций агент может путать эти зоны: например, смешать деплой Solana с деплоем сервера, изменить код от имени игрока, не обновить документацию API/DM/блокчейна или неправильно трактовать файл инструкций.
|
||||
|
||||
## Что предлагается сделать
|
||||
1. Утвердить корневой `AGENTS.md` как главный набор правил проекта.
|
||||
2. Проверить и при необходимости уточнить локальный `AGENTS.md` внутри `shine-solana/shine/`.
|
||||
3. Оставить отдельные служебные инструкции Telegram-агента в `SHiNE-agent-bot-coder/AGENT.md`.
|
||||
4. Оставить автоматически читаемые инструкции Telegram-агента в `SHiNE-agent-bot-coder/AGENTS.md`.
|
||||
5. Явно закрепить режим игроков:
|
||||
- игроки могут задавать вопросы, просить анализ, идеи и ТЗ;
|
||||
- игроки не меняют код проекта напрямую;
|
||||
- материалы игроков сохраняются только в `Players/<username>/`.
|
||||
6. Зафиксировать правило: если пользователь говорит «агент MD» или похожую формулировку, считать, что речь про автоматически читаемый `AGENTS.md`.
|
||||
7. Добавить простой процесс согласования изменений инструкций:
|
||||
- Дима или другой участник готовит предложение;
|
||||
- Айдар получает уведомление/заявку;
|
||||
- Айдар отвечает: одобрить, отклонить или попросить доработать;
|
||||
- только после одобрения агент вносит изменения в проектные инструкции.
|
||||
|
||||
## Предлагаемая логика уведомления Айдару
|
||||
Минимальный вариант без сложной разработки:
|
||||
|
||||
1. Агент готовит текст заявки.
|
||||
2. Текст отправляется Айдару в Telegram или в общий рабочий чат.
|
||||
3. В заявке явно указаны варианты ответа:
|
||||
- `одобрить`;
|
||||
- `отклонить`;
|
||||
- `доработать: ...`.
|
||||
4. После ответа Айдара агент либо выполняет согласованные правки, либо фиксирует, что задача отклонена/нужна доработка.
|
||||
|
||||
Более удобный вариант на будущее:
|
||||
|
||||
- добавить в Telegram-бота команду или сценарий согласования задач, например:
|
||||
- `/approve <id>`;
|
||||
- `/reject <id> причина`;
|
||||
- `/revise <id> комментарий`.
|
||||
|
||||
Но для начала достаточно простого текстового согласования через Telegram.
|
||||
|
||||
## Что нужно от Айдара
|
||||
Подтвердить, что такой порядок подходит:
|
||||
|
||||
1. Корневой `AGENTS.md` остается главным правилом проекта.
|
||||
2. Для Solana, Telegram-агента и игроков сохраняются отдельные локальные правила.
|
||||
3. Игроки не меняют код напрямую, а готовят материалы и предложения.
|
||||
4. Изменения инструкций выполняются только после явного одобрения Айдара.
|
||||
5. Уведомления Айдару на первом этапе можно делать простым текстом в Telegram, без отдельной сложной системы заявок.
|
||||
|
||||
## Ожидаемый результат
|
||||
После одобрения:
|
||||
|
||||
- агенты будут стабильнее понимать границы проекта;
|
||||
- снизится риск случайных изменений не в той части системы;
|
||||
- появится понятный порядок согласования задач от игроков;
|
||||
- Айдар будет явно контролировать изменения в инструкциях и правилах работы агентов.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 28 KiB |
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# SHiNE
|
||||
|
||||
## План запуска DAO
|
||||
|
||||
План запуска DAO зафиксирован в [DAO_запуск/README.md](DAO_запуск/README.md).
|
||||
|
||||
Это рабочий список задач по `этап1` и `этап2`. Дальше ведём его как основной чек-лист запуска DAO и отмечаем в нём выполненные пункты по мере готовности.
|
||||
@@ -0,0 +1,19 @@
|
||||
TELEGRAM_BOT_TOKEN=replace_me
|
||||
OPENAI_API_KEY=replace_me
|
||||
ALLOWED_TELEGRAM_USERNAME=AidarKC
|
||||
ALLOWED_TELEGRAM_PLAYERS=malvviiina:Милана,zodiaktechnika32:Сергей,oidasyda:Иван,blackbyrd1:Ворон,dimasol1:Дима
|
||||
ALLOWED_TELEGRAM_CHANNEL_USERNAME=shine_writing
|
||||
BOT_USERNAME=aidar_su_bot
|
||||
OPENAI_TRANSCRIBE_MODEL=gpt-4o-mini-transcribe
|
||||
TELEGRAM_FILE_DOWNLOAD_TIMEOUT_SECONDS=300
|
||||
OPENAI_TRANSCRIBE_TIMEOUT_SECONDS=900
|
||||
OPENAI_TTS_MODEL=gpt-4o-mini-tts
|
||||
OPENAI_TTS_VOICE=alloy
|
||||
OPENAI_TTS_RESPONSE_FORMAT=opus
|
||||
OPENAI_TTS_TIMEOUT_SECONDS=180
|
||||
OPENAI_TTS_CHUNK_CHARS=3500
|
||||
CODEX_BIN=/home/ai/.cache/JetBrains/IntelliJIdea2026.1/aia/codex/bin/codex-x86_64-unknown-linux-musl
|
||||
CODEX_WORKDIR=/home/ai/work/SHiNE/SHiNE-server-sha256
|
||||
CODEX_TIMEOUT_SECONDS=900
|
||||
MAX_RETRIES=3
|
||||
DATA_DIR=./data
|
||||
@@ -0,0 +1,5 @@
|
||||
.env
|
||||
data/
|
||||
logs/
|
||||
run/
|
||||
__pycache__/
|
||||
@@ -0,0 +1,81 @@
|
||||
# AGENT.md для SHiNE-agent-bot-coder
|
||||
|
||||
Ты запущен как обработчик входящего Telegram-сообщения от пользователя.
|
||||
|
||||
## Контекст
|
||||
- `SHiNE-agent-bot-coder` — локальный Telegram-бот-сервис агента-кодера для работы с этим проектом.
|
||||
- Сервис принимает входящие сообщения от пользователя Telegram, сохраняет историю, ставит задачи в очередь и последовательно запускает Codex CLI в рабочем проекте.
|
||||
- Текстовые сообщения обрабатываются напрямую, voice/audio сначала распознаются через OpenAI transcription, затем передаются как текстовая задача.
|
||||
- История диалога хранится в JSONL-файле, путь передаётся в промпте.
|
||||
- Сообщение может быть текстом или результатом распознавания голосового.
|
||||
- Ответ пойдёт пользователю в Telegram как обычное текстовое сообщение.
|
||||
- Единственная рабочая реализация сервиса — Python-скрипт `py_bot_service.py`; старая Java-реализация удалена как нерабочая и не должна восстанавливаться без отдельного решения Айдара.
|
||||
- В репозитории также есть отдельный Solana/Anchor-модуль `shine-solana/shine/`; он логически связан с SHiNE, но не должен автоматически подключаться к основному серверному deploy без отдельной команды.
|
||||
- Перед изменениями внутри `shine-solana/shine/` читать локальные инструкции `shine-solana/shine/AGENTS.md`; в git не добавлять локальные ключи, `.git`, `.idea`, `.gradle`, `target`, `node_modules`, `test-ledger`, логи, временные run-отчёты и `.env`-конфиги.
|
||||
|
||||
## Авторитет команд и история
|
||||
- Основной пользователь и источник команд — Айдар: `@AidarKC` / `@aidarkc`.
|
||||
- Дополнительно разрешены игроки из whitelist (`ALLOWED_TELEGRAM_PLAYERS`), каждый со своей отдельной историей и рабочей папкой `Players/<username>/`.
|
||||
- Игроки работают в режиме вопросов/анализа/подготовки материалов: в промпте явно задано правило не менять код проекта и писать материалы только в своей папке.
|
||||
- Для неизвестных пользователей в личном чате сервис отвечает вежливым отказом.
|
||||
- В Telegram-канале/группе `@shine_writing` сервис выполняет сообщения только от Айдара, а ответы отправляет в тот же чат.
|
||||
- Если Telegram сообщает о миграции обычной группы в supergroup, сервис должен запомнить новый `chat_id` и отправлять ответы уже туда.
|
||||
- На события подключения/отключения пользователей (join/leave) сервис не отвечает и ничего не отправляет.
|
||||
|
||||
## Очередь и состояние
|
||||
- Входящие задачи записываются в файловую очередь и обрабатываются строго по одной, чтобы не смешивать изменения в проекте.
|
||||
- Сервис ведёт состояние активной задачи и текущего файла истории, а после рестарта продолжает незавершённую обработку с учётом сохранённого состояния.
|
||||
- Истории диалогов хранятся в JSONL по каждому разрешённому username отдельно: `data/history/<username>/`.
|
||||
- Архив истории после `/new`: `data/history/<username>/archive/`.
|
||||
- После `/new` для этого же пользователя должен сбрасываться и контекст продолжения Codex-сессии; следующий запрос запускается как новая сессия, не через resume.
|
||||
- Для просмотра истории игрока открывать файлы в его папке истории по username.
|
||||
- Дедупликация входящих Telegram update нужна, чтобы одно сообщение не попало в обработку повторно.
|
||||
- Если Codex молчит во время активной задачи 2 минуты подряд, сервис отправляет аварийный статус с общим временем работы задачи; при дальнейшем молчании повторяет статус каждые 2 минуты.
|
||||
- После успешной обработки задачи из личного чата Айдара сервис должен отправить публичный итоговый отчёт в группу `@shine_writing`: первым сообщением исходный запрос, вторым сообщением-ответом итоговый ответ Codex. Промежуточные статусы в группу не дублировать.
|
||||
- Для приватных voice/audio-запросов в публичном отчёте первым сообщением отправлять исходный Telegram voice/audio-файл с подписью, где указан распознанный текст. В пользовательском тексте отчёта не показывать Telegram `file_id`.
|
||||
- Озвучивание финальных ответов настраивается персонально для каждого Telegram-пользователя командами `/voice_on`, `/voice_off`; для новых пользователей оно включено по умолчанию.
|
||||
- Адаптация текста перед озвучкой настраивается персонально командами `/voice_rewrite_on`, `/voice_rewrite_off`. Если она включена, сервис перед TTS вызывает дешёвую текстовую модель OpenAI и делает голосовую версию без длинных хэшей, путей, команд и технического шума, сохраняя смысл и порядок исходного ответа.
|
||||
- Режим личных ответов настраивается персонально командами `/single_message_on`, `/single_message_off`: либо одно редактируемое сообщение по этапам, либо отдельные сообщения как раньше.
|
||||
- Команда `/settings` должна сразу показывать текущее состояние всех персональных настроек пользователя и список команд для их изменения.
|
||||
- Если озвучивание включено, после полного текстового финального ответа сервис дополнительно отправляет voice-файл с синтезированной речью через OpenAI TTS даже для текстовых запросов. Voice отправляется в исходный чат, а также в известный личный чат пользователя и в общий чат `@shine_writing`, если они отличаются и доступны. Промежуточные статусы не озвучивать.
|
||||
- Команда `/status` должна показывать состояние очереди и персональные настройки: voice-ответы, адаптацию текста перед озвучкой и режим одного сообщения в личке.
|
||||
|
||||
## Правила голосовой версии ответа
|
||||
- Текстовый финальный ответ должен оставаться полноценным: в нём можно указывать команды, пути, хэши коммитов, номера версий, результаты проверок и другие технические детали.
|
||||
- Голосовую версию финального ответа нужно делать короче и проще для восприятия на слух. Основной механизм — персонально включаемая адаптация текста через дополнительный OpenAI-вызов перед TTS.
|
||||
- В голосовой версии не зачитывать длинные хэши коммитов, токены, file_id, длинные команды, полные пути и другие строки, которые человек всё равно не сможет надёжно запомнить на слух.
|
||||
- Для commit/push в голосовой версии достаточно сказать краткий итог: что коммит сделан, что именно изменено, проверки прошли без ошибок, push выполнен, рабочее дерево чистое.
|
||||
- Если пользователю нужны точные команды, хэши или подробности, они должны оставаться в текстовом ответе.
|
||||
|
||||
## Планы и отложенные фичи
|
||||
- Планы проекта по отложенным фичам хранятся в `TODO/`.
|
||||
- Внутри есть три горизонта:
|
||||
- `near/` - ближайшие планы, обычно сегодня/завтра;
|
||||
- `medium/` - среднесрочные планы, обычно недели или 1-2 месяца;
|
||||
- `far/` - дальнее будущее без понятного срока.
|
||||
- Если пользователь спрашивает, какие есть планы или что можно продолжить, нужно смотреть эти три папки и отвечать кратким списком по горизонтам.
|
||||
- Файлы из `TODO/` не начинать реализовывать без явной команды пользователя.
|
||||
- После реализации фичи, требующей ручной проверки, нужно добавить отдельный файл в `Dev_Docs/Pending_Features/`.
|
||||
|
||||
## Центр задач и предложений
|
||||
- Сервис хранит простые задачи и предложения в `data/task_center/items.json`.
|
||||
- Айдар может смотреть список через `/tasks` или естественные фразы вроде «покажи мои задачи», «покажи задачи Миланы».
|
||||
- Айдар может ставить задачи игрокам фразой вида «поставь задачу Милане: ...».
|
||||
- Игроки могут отправлять предложения Айдару фразой вида `предложение: ...`, `идея: ...` или `заявка: ...`.
|
||||
- Статусы меняются фразами с ID: `одобрить TC-0001`, `отклонить TC-0001`, `доработать TC-0001`, `закрыть TC-0001`.
|
||||
- После финального ответа в личном чате сервис добавляет короткое напоминание, если у пользователя есть активные задачи или предложения.
|
||||
|
||||
## Локальный запуск и systemd
|
||||
- Основной запуск сервиса выполняется Python-скриптом `py_bot_service.py` из папки `SHiNE-agent-bot-coder/`.
|
||||
- Локальные секреты и параметры должны храниться в `.env`, этот файл не коммитится.
|
||||
- Для проверки Codex без Telegram можно использовать self-test режим сервиса.
|
||||
- Для постоянного локального запуска используется user-level systemd service `shine-agent-bot-coder`; скрипты установки лежат в `SHiNE-agent-bot-coder/scripts/systemd/`.
|
||||
- Если меняется логика сервиса, после изменений нужно проверить запуск локально и при необходимости перезапустить user systemd service.
|
||||
- Команда Telegram `/restart` (`/restart_service`) доступна только Айдару и выполняет отложенный рестарт после текущей задачи, до взятия следующей. Аварийный жёсткий рестарт доступен только Айдару командами `/restart_hard`, `/restart_now`, `/restart_force`.
|
||||
|
||||
## Правила ответа
|
||||
- Пиши содержательно и коротко.
|
||||
- Не упоминай внутренние служебные детали, файловую систему и технические логи.
|
||||
- Если запрос требует действий с кодом/проектом, выполняй их в рабочей директории.
|
||||
- Если для ответа данных недостаточно, задай ровно один уточняющий вопрос.
|
||||
- Если была ошибка предыдущего запуска, в промпте будет пометка retry — учти это и продолжи с учётом текущего состояния проекта.
|
||||
@@ -0,0 +1,25 @@
|
||||
# AGENTS
|
||||
|
||||
## Назначение
|
||||
- Это автоматически читаемые инструкции Codex для папки `SHiNE-agent-bot-coder/`.
|
||||
- `SHiNE-agent-bot-coder` — локальный Telegram-бот-сервис агента-кодера для работы с проектом SHiNE.
|
||||
- Если пользователь говорит «агент MD», «агент с MD» или похожим образом про файл инструкций Codex, считать, что имеется в виду `AGENTS.md`.
|
||||
|
||||
## Связанные инструкции
|
||||
- Подробные служебные правила Telegram-обработчика лежат в `AGENT.md`.
|
||||
- `AGENT.md` используется самим сервисом как файл инструкций, который передаётся в промпт обработчика входящих Telegram-сообщений.
|
||||
- При изменении логики сервиса сначала читать `AGENT.md`, затем код `py_bot_service.py`.
|
||||
|
||||
## Планы и задачи
|
||||
- Отложенные задачи проекта лежат в `../TODO/`.
|
||||
- Точка входа по планам: `../TODO/README.md`.
|
||||
- Горизонты планов:
|
||||
- `near/` - ближайшие планы;
|
||||
- `medium/` - среднесрочные планы;
|
||||
- `far/` - дальнее будущее.
|
||||
- Если пользователь спрашивает, какие есть планы или что можно продолжить, кратко перечислять задачи по этим горизонтам.
|
||||
- Не начинать реализацию задач из `TODO` без явной команды пользователя.
|
||||
|
||||
## Проверка после изменений
|
||||
- Если меняется логика Telegram-бота, проверить локальный запуск или self-test, когда это уместно.
|
||||
- Если меняется только документация или инструкции, достаточно проверить, что ссылки на документы актуальны.
|
||||
@@ -0,0 +1,2 @@
|
||||
@AGENTS.md
|
||||
@AGENT.md
|
||||
@@ -0,0 +1,26 @@
|
||||
# Промпты для режима игроков (на согласование)
|
||||
|
||||
## 1) Базовый служебный промпт (добавка к задаче игрока)
|
||||
|
||||
```text
|
||||
Режим игрока (обязательно):
|
||||
- Пользователь: <Имя> (@<username>).
|
||||
- Рабочая папка игрока: <project>/Players/<username>
|
||||
- Код проекта не изменять.
|
||||
- Можно отвечать на вопросы по проекту, предлагать идеи и готовить ТЗ.
|
||||
- Если нужны правки кода, описывать предложение текстом и сохранять материалы только в папке игрока.
|
||||
```
|
||||
|
||||
## 2) Приветственное сообщение игроку (один раз)
|
||||
|
||||
```text
|
||||
Привет, <Имя>.
|
||||
Можно задавать вопросы по проекту, просить анализ, идеи и подготовку готового ТЗ.
|
||||
Команда /new начинает новую сессию и архивирует текущую историю.
|
||||
```
|
||||
|
||||
## 3) Отказ неизвестному пользователю
|
||||
|
||||
```text
|
||||
Извините, доступ к этому агенту пока не выдан. Обратитесь к Айдару.
|
||||
```
|
||||
@@ -0,0 +1,100 @@
|
||||
# SHiNE-agent-bot-coder
|
||||
|
||||
Локальный Telegram-бот-сервис для пользователя `ai`:
|
||||
- принимает сообщения от `@AidarKC`;
|
||||
- поддерживает whitelist игроков (`ALLOWED_TELEGRAM_PLAYERS`) с отдельными историями;
|
||||
- ведёт историю диалога в `JSONL`;
|
||||
- ставит задачи в файловую очередь;
|
||||
- обрабатывает задачи строго последовательно;
|
||||
- поддерживает текстовые и голосовые сообщения (voice/audio через OpenAI transcription);
|
||||
- вызывает Codex CLI и отправляет ответ в Telegram;
|
||||
- в личном чате умеет работать в двух персонально переключаемых режимах: через одно редактируемое статусное сообщение или через отдельные сообщения по этапам;
|
||||
- умеет персонально для каждого пользователя озвучивать финальный ответ через OpenAI TTS;
|
||||
- при рестарте восстанавливает незавершённые задачи;
|
||||
- отправляет аварийный статус только если Codex молчит 2 минуты подряд во время активной задачи;
|
||||
- принимает сообщения из канала/группы `@shine_writing`, выполняет команды только от `@AidarKC`;
|
||||
- учитывает миграцию обычной Telegram-группы в supergroup и перенаправляет ответы на новый `chat_id`.
|
||||
|
||||
Рабочая реализация сервиса — только `py_bot_service.py`. Старая Java-реализация удалена, потому что не заработала и больше не используется.
|
||||
|
||||
## Структура
|
||||
- `.env` — локальные секреты и параметры запуска (не коммитится);
|
||||
- `data/py_queue.jsonl` — очередь Python-сервиса;
|
||||
- `data/py_state.json` — текущее состояние Python-сервиса;
|
||||
- `data/py_processed_updates.log` — дедуп входящих update;
|
||||
- `data/history/<username>/*.jsonl` — активные истории по пользователям;
|
||||
- `data/history/<username>/archive/*.jsonl` — архивы после `/new`.
|
||||
|
||||
## Локальный запуск
|
||||
1. Скопировать пример:
|
||||
- `cp .env.example .env`
|
||||
2. Заполнить секреты в `.env`.
|
||||
- `TELEGRAM_BOT_TOKEN` — токен рабочего Telegram-бота.
|
||||
- `ALLOWED_TELEGRAM_USERNAME` — пользователь, чьи сообщения выполняются как команды.
|
||||
- `ALLOWED_TELEGRAM_PLAYERS` — whitelist игроков в формате `username:Имя,username2:Имя2`.
|
||||
- `ALLOWED_TELEGRAM_CHANNEL_USERNAME` — канал, из которого принимаются `channel_post`; обычные group/supergroup-сообщения обрабатываются как `message`.
|
||||
- `TELEGRAM_API_BASE_URL` — базовый URL Bot API; по умолчанию `https://api.telegram.org`. Для очень больших voice/audio можно поднять локальный `telegram-bot-api` и направить бота туда.
|
||||
- `TELEGRAM_FILE_DOWNLOAD_TIMEOUT_SECONDS` — тайм-аут скачивания voice/audio из Telegram, по умолчанию 300 секунд.
|
||||
- `OPENAI_TRANSCRIBE_TIMEOUT_SECONDS` — тайм-аут распознавания voice/audio в OpenAI, по умолчанию 900 секунд.
|
||||
- `OPENAI_TRANSCRIBE_MAX_UPLOAD_BYTES` — безопасный лимит размера одного куска для OpenAI transcription, по умолчанию `24 MiB`.
|
||||
- `OPENAI_TRANSCRIBE_MAX_CHUNK_SECONDS` — максимальная длина одного куска при длинном аудио, по умолчанию `900` секунд.
|
||||
- `OPENAI_TRANSCRIBE_OVERLAP_SECONDS` — перекрытие соседних кусков для более ровной склейки текста, по умолчанию `2` секунды.
|
||||
- `OPENAI_TRANSCRIBE_REENCODE_BITRATE_KBPS` — битрейт локального пережатия длинного аудио через `ffmpeg`, по умолчанию `24`.
|
||||
- `OPENAI_TRANSCRIBE_FFMPEG_TIMEOUT_SECONDS` — тайм-аут локальной обработки длинного аудио через `ffmpeg`/`ffprobe`, по умолчанию `1800`.
|
||||
- `FFMPEG_BIN` и `FFPROBE_BIN` — пути к локальным бинарям `ffmpeg`/`ffprobe`, если они не лежат в `PATH`.
|
||||
- `OPENAI_TTS_MODEL` — модель синтеза речи, по умолчанию `gpt-4o-mini-tts`.
|
||||
- `OPENAI_TTS_VOICE` — голос синтеза речи, по умолчанию `alloy`.
|
||||
- `OPENAI_TTS_RESPONSE_FORMAT` — аудиоформат для Telegram voice, по умолчанию `opus`.
|
||||
- `OPENAI_TTS_TIMEOUT_SECONDS` — тайм-аут генерации одного фрагмента речи, по умолчанию 180 секунд.
|
||||
- `OPENAI_TTS_CHUNK_CHARS` — максимальный размер одного фрагмента озвучки, по умолчанию 3500 символов.
|
||||
3. Запуск:
|
||||
- `python3 SHiNE-agent-bot-coder/py_bot_service.py`
|
||||
|
||||
## Быстрый self-test Codex (без Telegram)
|
||||
```bash
|
||||
python3 SHiNE-agent-bot-coder/py_bot_service.py --selftest-codex "Ответь одной строкой: Codex работает"
|
||||
```
|
||||
|
||||
## Длинные voice/audio
|
||||
- Если аудио короткое, бот отправляет его в OpenAI как раньше.
|
||||
- Если аудио большое или длинное, бот локально пережимает его через `ffmpeg`, при необходимости режет на куски и распознаёт последовательно.
|
||||
- Если Telegram заранее сообщает большой размер файла, бот больше не отказывается сразу: сначала явно пишет, что пробует скачать файл, затем отдельно сообщает, удалось ли скачивание, и только после успешной загрузки переходит к подготовке аудио и OpenAI.
|
||||
- Для очень больших файлов упираемся не только в OpenAI, но и в лимит обычного облачного Telegram Bot API на скачивание файла ботом. Для таких случаев нужно использовать локальный `telegram-bot-api` сервер и указать его через `TELEGRAM_API_BASE_URL`.
|
||||
|
||||
## Статусы в личке
|
||||
- Для `private`-чата бот поддерживает персональную настройку режима ответа.
|
||||
- По умолчанию он старается не засорять переписку промежуточными сообщениями: создаёт одно статусное сообщение и редактирует его по этапам.
|
||||
- Если включить `/single_message_off`, бот возвращается к старому режиму и отправляет отдельные сообщения по этапам и финальный ответ отдельно.
|
||||
- Если финальный текст в режиме одного сообщения не помещается целиком, бот оставляет первую часть в отредактированном статусном сообщении и отправляет максимум ещё одно дополнительное текстовое сообщение с хвостом ответа.
|
||||
- Голосовой ответ, если он включён, всегда приходит отдельным новым сообщением.
|
||||
|
||||
## Запуск как systemd-сервис
|
||||
Файлы для установки:
|
||||
- `scripts/systemd/shine-agent-bot-coder.service`
|
||||
- `scripts/systemd/install-local-systemd.sh`
|
||||
|
||||
Установка:
|
||||
- `bash SHiNE-agent-bot-coder/scripts/systemd/install-local-systemd.sh`
|
||||
|
||||
Проверка:
|
||||
- `systemctl --user status shine-agent-bot-coder --no-pager`
|
||||
- `journalctl --user -u shine-agent-bot-coder -f`
|
||||
|
||||
Перезапуск после изменений:
|
||||
- `systemctl --user restart shine-agent-bot-coder`
|
||||
|
||||
## Telegram-команды
|
||||
- `/status` — активная задача и размер очереди.
|
||||
- `/settings` — текущие пользовательские настройки и команды для их изменения.
|
||||
- `/queue` — список задач в очереди.
|
||||
- `/stop` — остановить текущую задачу.
|
||||
- `/cancel <id|all>` — удалить задачу по id/префиксу или очистить очередь.
|
||||
- `/new` — архивировать текущую историю, сбросить продолжение Codex-сессии для этого пользователя и начать новый диалог.
|
||||
- `/voice_on` — включить озвучивание финальных ответов для текущего пользователя.
|
||||
- `/voice_off` — выключить озвучивание финальных ответов для текущего пользователя.
|
||||
- `/voice_rewrite_on` — включить адаптацию текста перед озвучкой.
|
||||
- `/voice_rewrite_off` — выключить адаптацию текста перед озвучкой.
|
||||
- `/single_message_on` — вести ответ в личке через одно редактируемое сообщение.
|
||||
- `/single_message_off` — слать отдельные сообщения по этапам и отдельный финальный ответ.
|
||||
- `/restart` или `/restart_service` — отложенный рестарт после текущей задачи, до взятия следующей (только для Айдара).
|
||||
- `/restart_hard` — жёсткий рестарт прямо сейчас (только для Айдара).
|
||||
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="/home/ai/work/SHiNE/SHiNE-server-sha256"
|
||||
SERVICE_DIR="${ROOT_DIR}/SHiNE-agent-bot-coder"
|
||||
UNIT_SRC="${SERVICE_DIR}/scripts/systemd/shine-agent-bot-coder.service"
|
||||
UNIT_DST="${HOME}/.config/systemd/user/shine-agent-bot-coder.service"
|
||||
|
||||
echo "[1/6] Проверка python3..."
|
||||
command -v python3 >/dev/null 2>&1 || { echo "python3 не найден"; exit 1; }
|
||||
|
||||
echo "[2/6] Подготовка папки логов..."
|
||||
mkdir -p "${SERVICE_DIR}/logs"
|
||||
|
||||
echo "[3/6] Копирование user systemd unit..."
|
||||
mkdir -p "$(dirname "${UNIT_DST}")"
|
||||
cp "${UNIT_SRC}" "${UNIT_DST}"
|
||||
|
||||
echo "[4/6] daemon-reload..."
|
||||
systemctl --user daemon-reload
|
||||
|
||||
echo "[5/6] enable + start..."
|
||||
systemctl --user enable --now shine-agent-bot-coder
|
||||
|
||||
echo "[6/6] Статус:"
|
||||
systemctl --user status shine-agent-bot-coder --no-pager
|
||||
|
||||
echo "Готово. Логи: journalctl --user -u shine-agent-bot-coder -f"
|
||||
@@ -0,0 +1,19 @@
|
||||
[Unit]
|
||||
Description=SHiNE Agent Bot Coder (Telegram + Codex queue worker)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/home/ai/work/SHiNE/SHiNE-server-sha256/SHiNE-agent-bot-coder
|
||||
EnvironmentFile=/home/ai/work/SHiNE/SHiNE-server-sha256/SHiNE-agent-bot-coder/.env
|
||||
ExecStart=/usr/bin/python3 /home/ai/work/SHiNE/SHiNE-server-sha256/SHiNE-agent-bot-coder/py_bot_service.py
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
TimeoutStopSec=20
|
||||
SuccessExitStatus=143 0
|
||||
StandardOutput=append:/home/ai/work/SHiNE/SHiNE-server-sha256/SHiNE-agent-bot-coder/logs/service.log
|
||||
StandardError=append:/home/ai/work/SHiNE/SHiNE-server-sha256/SHiNE-agent-bot-coder/logs/service.log
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -0,0 +1,44 @@
|
||||
.gradle
|
||||
build/
|
||||
node_modules/
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
.kotlin
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea/modules.xml
|
||||
.idea/jarRepositories.xml
|
||||
.idea/compiler.xml
|
||||
.idea/libraries/
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
out/
|
||||
!**/src/main/**/out/
|
||||
!**/src/test/**/out/
|
||||
|
||||
### Eclipse ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
bin/
|
||||
!**/src/main/**/bin/
|
||||
!**/src/test/**/bin/
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
### Mac OS ###
|
||||
.DS_Store
|
||||
@@ -0,0 +1,10 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Ignored default folder with query files
|
||||
/queries/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
@@ -0,0 +1 @@
|
||||
ESP-wallet
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="myGradleHome" value="" />
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,45 @@
|
||||
# SHiNE Browser Plugin Wallet
|
||||
|
||||
Chrome-compatible Manifest V3 plugin for SHiNE wallet-session login.
|
||||
|
||||
## Что уже умеет
|
||||
|
||||
- создать `wallet-session` через `StartTrustedDeviceLogin`;
|
||||
- показать код подключения;
|
||||
- дождаться подтверждения на доверенном устройстве;
|
||||
- принять `session-only` payload без передачи `clientKey/rootKey/blockchainKey`;
|
||||
- сохранить `sessionPriv/sessionKey/sessionId` в локальном хранилище plugin;
|
||||
- восстанавливать session через `SessionChallenge -> SessionLogin`;
|
||||
- держать wallet-state в `background service worker`, а side panel использовать как UI.
|
||||
- принимать не адрес сервера, а логин серверного аккаунта SHiNE и находить точный `https://...` / `wss://...` адрес через его PDA.
|
||||
- внедрять legacy `window.solana` / `window.phantom.solana` provider для сайтов.
|
||||
- регистрировать кошелёк как `Wallet Standard` wallet для dapp, которые ищут стандартные кошельки.
|
||||
|
||||
## Как загрузить локально
|
||||
|
||||
1. Открой `chrome://extensions/`
|
||||
2. Включи `Developer mode`
|
||||
3. Нажми `Load unpacked`
|
||||
4. Выбери папку `SHiNE-browser-plugin-wallet/`
|
||||
5. Нажми на иконку расширения в toolbar: откроется side panel SHiNE Wallet
|
||||
|
||||
## Ограничения текущего этапа
|
||||
|
||||
- plugin пока не держит постоянный фоновый WS-канал без активного действия пользователя, но хранит wallet-state в `background`;
|
||||
- на этом этапе реализован только `session-only login`;
|
||||
- запросы на подпись будут следующим этапом.
|
||||
- pairing-пароль, если он используется, должен генерироваться в формате `sha256$<hex>` от строки `shine-pairing|loginLower|password`.
|
||||
- сторона side panel в Chromium выбирается самим браузером/пользователем; extension не закрепляет панель принудительно слева.
|
||||
- для совместимости с некоторыми dapp расширение одновременно держит и legacy provider, и Wallet Standard регистрацию.
|
||||
|
||||
## Сборка crypto bundle
|
||||
|
||||
Для обычной загрузки plugin это не нужно: bundled crypto-файл уже лежит в репозитории.
|
||||
|
||||
Если понадобится пересобрать локальный crypto bundle:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npx esbuild js/lib/vendor/noble-ed25519-entry.js --bundle --format=esm --platform=browser --outfile=js/lib/vendor/noble-ed25519-bundle.js
|
||||
npx esbuild js/lib/vendor/solana-publickey-entry.js --bundle --format=esm --platform=browser --outfile=js/lib/vendor/solana-publickey-bundle.js
|
||||
```
|
||||
@@ -0,0 +1,958 @@
|
||||
import { createRequesterPairingMaterial, decryptPairingPayloadFromEnvelope, deriveEspPairingPasswordHash } from './js/lib/device-pairing.js';
|
||||
import { loadPluginSettings, loadSessionMaterial, savePluginSettings, saveSessionMaterial, clearSessionMaterial } from './js/lib/session-store.js';
|
||||
import { ShineApiClient } from './js/lib/shine-api.js';
|
||||
import {
|
||||
DEFAULT_SHINE_SERVER_LOGIN,
|
||||
buildHttpBase,
|
||||
readWalletProfileByLogin,
|
||||
resolveShineServerByUserLogin,
|
||||
} from './js/lib/shine-server-resolver.js';
|
||||
|
||||
const state = {
|
||||
api: null,
|
||||
settings: {
|
||||
serverLogin: DEFAULT_SHINE_SERVER_LOGIN,
|
||||
serverHttp: buildHttpBase('shineup.me'),
|
||||
serverUrl: 'wss://shineup.me/ws',
|
||||
login: '',
|
||||
},
|
||||
requesterMaterial: null,
|
||||
pairingId: '',
|
||||
expiresAtMs: 0,
|
||||
shortCode: '',
|
||||
trustedSessionOnline: false,
|
||||
pollTimer: 0,
|
||||
activeSession: null,
|
||||
connectionOnline: false,
|
||||
walletProfile: null,
|
||||
signing: {
|
||||
selectedDeviceName: '',
|
||||
devicesResolvedAtMs: 0,
|
||||
},
|
||||
currentWallet: null,
|
||||
pendingApprovals: [],
|
||||
siteApprovalChain: Promise.resolve(),
|
||||
sessionAttachInProgress: false,
|
||||
statusText: '',
|
||||
statusKind: 'info',
|
||||
};
|
||||
|
||||
const WALLET_RPC_REQUEST_TYPE = 9100;
|
||||
const WALLET_RPC_RESPONSE_TYPE = 9101;
|
||||
|
||||
async function configureSidePanelBehavior() {
|
||||
if (!chrome.sidePanel?.setPanelBehavior) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });
|
||||
} catch (error) {
|
||||
console.warn('Failed to configure SHiNE side panel behavior:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeOrigin(value = '') {
|
||||
const raw = String(value || '').trim();
|
||||
if (!raw) return '';
|
||||
try {
|
||||
return new URL(raw).origin;
|
||||
} catch {
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
|
||||
function makeCodeError(message, code) {
|
||||
const error = new Error(String(message || 'Wallet error'));
|
||||
error.code = String(code || '').trim().toUpperCase();
|
||||
return error;
|
||||
}
|
||||
|
||||
function setStatus(message = '', kind = 'info') {
|
||||
state.statusText = String(message || '');
|
||||
state.statusKind = kind === 'error' ? 'error' : 'info';
|
||||
}
|
||||
|
||||
function makePendingApprovalSnapshot(payload = {}) {
|
||||
const pendingId = String(payload?.id || '').trim();
|
||||
const pendingApprovals = Array.isArray(state.pendingApprovals) ? state.pendingApprovals : [];
|
||||
const queueIndex = pendingApprovals.findIndex((item) => String(item?.id || '').trim() === pendingId);
|
||||
const queueLength = pendingApprovals.length;
|
||||
return {
|
||||
id: pendingId,
|
||||
kind: String(payload?.kind || 'sign_transaction').trim() || 'sign_transaction',
|
||||
origin: String(payload?.origin || '').trim(),
|
||||
publicKeyBase58: String(payload?.publicKeyBase58 || '').trim(),
|
||||
comment: String(payload?.comment || '').trim(),
|
||||
createdAtMs: Number(payload?.createdAtMs || Date.now()),
|
||||
status: String(payload?.status || 'queued').trim() || 'queued',
|
||||
queuePosition: queueIndex >= 0 ? queueIndex + 1 : 1,
|
||||
queueLength: queueLength || 1,
|
||||
transactionSummary: payload?.transactionSummary && typeof payload.transactionSummary === 'object'
|
||||
? { ...payload.transactionSummary }
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
function getCurrentPendingApproval() {
|
||||
return Array.isArray(state.pendingApprovals) && state.pendingApprovals.length
|
||||
? state.pendingApprovals[0]
|
||||
: null;
|
||||
}
|
||||
|
||||
function removePendingApproval(pendingId, { rejectError = null } = {}) {
|
||||
const pendingApprovals = Array.isArray(state.pendingApprovals) ? state.pendingApprovals : [];
|
||||
const index = pendingApprovals.findIndex((item) => String(item?.id || '').trim() === String(pendingId || '').trim());
|
||||
if (index < 0) return;
|
||||
const [pending] = pendingApprovals.splice(index, 1);
|
||||
if (pending.timeoutId) {
|
||||
clearTimeout(pending.timeoutId);
|
||||
}
|
||||
if (rejectError && pending.abortController) {
|
||||
try {
|
||||
pending.abortController.abort(rejectError);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
async function openSidePanelForSender(sender) {
|
||||
if (!chrome.sidePanel?.open || !sender?.tab?.id) return;
|
||||
try {
|
||||
await chrome.sidePanel.open({ tabId: sender.tab.id });
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function stopPoll() {
|
||||
if (state.pollTimer) {
|
||||
clearTimeout(state.pollTimer);
|
||||
state.pollTimer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function clearPairingState() {
|
||||
stopPoll();
|
||||
state.requesterMaterial = null;
|
||||
state.pairingId = '';
|
||||
state.expiresAtMs = 0;
|
||||
state.shortCode = '';
|
||||
state.trustedSessionOnline = false;
|
||||
}
|
||||
|
||||
function ensureApi(serverUrl = state.settings.serverUrl) {
|
||||
const normalized = String(serverUrl || '').trim() || 'wss://shineup.me/ws';
|
||||
if (!state.api || state.api.serverUrl !== normalized) {
|
||||
state.api?.close();
|
||||
state.api = new ShineApiClient(normalized);
|
||||
}
|
||||
return state.api;
|
||||
}
|
||||
|
||||
async function loadStateFromStorage() {
|
||||
const settings = await loadPluginSettings();
|
||||
state.settings = {
|
||||
serverLogin: String(settings?.serverLogin || state.settings.serverLogin || DEFAULT_SHINE_SERVER_LOGIN).trim(),
|
||||
serverHttp: String(settings?.serverHttp || state.settings.serverHttp || buildHttpBase('shineup.me')).trim() || buildHttpBase('shineup.me'),
|
||||
serverUrl: String(settings?.serverUrl || state.settings.serverUrl || 'wss://shineup.me/ws').trim() || 'wss://shineup.me/ws',
|
||||
login: String(settings?.login || '').trim(),
|
||||
connectedOrigins: Array.isArray(settings?.connectedOrigins) ? settings.connectedOrigins.map((item) => normalizeOrigin(item)).filter(Boolean) : [],
|
||||
};
|
||||
const storedSession = await loadSessionMaterial();
|
||||
if (storedSession || !state.sessionAttachInProgress) {
|
||||
state.activeSession = storedSession;
|
||||
}
|
||||
state.walletProfile = state.activeSession?.walletProfile || null;
|
||||
state.signing = {
|
||||
selectedDeviceName: String(state.activeSession?.selectedDeviceName || ''),
|
||||
devicesResolvedAtMs: Number(state.activeSession?.devicesResolvedAtMs || 0),
|
||||
};
|
||||
state.currentWallet = state.activeSession?.currentWallet || null;
|
||||
}
|
||||
|
||||
async function persistSettings(nextSettings = {}) {
|
||||
state.settings = {
|
||||
...state.settings,
|
||||
...nextSettings,
|
||||
};
|
||||
if (!Array.isArray(state.settings.connectedOrigins)) {
|
||||
state.settings.connectedOrigins = [];
|
||||
}
|
||||
await savePluginSettings(state.settings);
|
||||
return state.settings;
|
||||
}
|
||||
|
||||
function isOriginApproved(origin) {
|
||||
const normalized = normalizeOrigin(origin);
|
||||
return !!normalized && Array.isArray(state.settings.connectedOrigins) && state.settings.connectedOrigins.includes(normalized);
|
||||
}
|
||||
|
||||
async function setOriginApproved(origin, approved) {
|
||||
const normalized = normalizeOrigin(origin);
|
||||
const current = new Set(Array.isArray(state.settings.connectedOrigins) ? state.settings.connectedOrigins : []);
|
||||
if (approved) {
|
||||
if (normalized) current.add(normalized);
|
||||
} else if (normalized) {
|
||||
current.delete(normalized);
|
||||
}
|
||||
await persistSettings({ connectedOrigins: [...current] });
|
||||
}
|
||||
|
||||
async function resolveServerForLogin(login) {
|
||||
const cleanLogin = String(login || state.settings.login || '').trim();
|
||||
if (!cleanLogin) {
|
||||
state.settings = {
|
||||
...state.settings,
|
||||
login: '',
|
||||
serverLogin: '',
|
||||
};
|
||||
await savePluginSettings(state.settings);
|
||||
return { ok: true, resolved: false };
|
||||
}
|
||||
|
||||
const resolved = await resolveShineServerByUserLogin(cleanLogin);
|
||||
state.settings = {
|
||||
...state.settings,
|
||||
login: cleanLogin,
|
||||
serverLogin: resolved.serverLogin,
|
||||
serverHttp: resolved.serverHttp,
|
||||
serverUrl: resolved.serverUrl,
|
||||
};
|
||||
await savePluginSettings(state.settings);
|
||||
return { ok: true, resolved: true, ...resolved };
|
||||
}
|
||||
|
||||
async function saveActiveSessionRecord() {
|
||||
if (!state.activeSession) return;
|
||||
const nextRecord = {
|
||||
...state.activeSession,
|
||||
walletProfile: state.walletProfile,
|
||||
selectedDeviceName: state.signing.selectedDeviceName,
|
||||
devicesResolvedAtMs: state.signing.devicesResolvedAtMs,
|
||||
currentWallet: state.currentWallet,
|
||||
};
|
||||
state.activeSession = nextRecord;
|
||||
await saveSessionMaterial(nextRecord);
|
||||
}
|
||||
|
||||
function shortKey(value = '', size = 10) {
|
||||
const raw = String(value || '').trim();
|
||||
return raw ? raw.slice(0, size) : '';
|
||||
}
|
||||
|
||||
function extractErrorCode(message = '') {
|
||||
const match = String(message || '').match(/\(([A-Z0-9_]+)\)\s*$/i);
|
||||
return match ? String(match[1]).toUpperCase() : '';
|
||||
}
|
||||
|
||||
function toWalletErrorMessage(error, fallback = 'Не удалось выполнить операцию кошелька.') {
|
||||
const raw = String(error?.message || '').trim();
|
||||
const code = String(error?.code || extractErrorCode(raw) || '').toUpperCase();
|
||||
if (code === 'PAIRING_NOT_AVAILABLE') {
|
||||
return 'Для этого логина ещё не включено подключение по коду. На доверенном устройстве откройте «Подключить по коду» и нажмите «Включить подключение по коду» или задайте дополнительный пароль.';
|
||||
}
|
||||
if (code === 'PAIRING_NO_TRUSTED_SESSION_ONLINE') {
|
||||
return 'Сейчас нет ни одной онлайн доверенной сессии этого пользователя. Откройте SHiNE на другом уже подключённом устройстве и держите его в сети.';
|
||||
}
|
||||
if (code === 'PAIRING_PASSWORD_INVALID') {
|
||||
return 'Дополнительный пароль подключения не подходит.';
|
||||
}
|
||||
return raw || fallback;
|
||||
}
|
||||
|
||||
function homeserverSessionNameFromClientInfo(value = '') {
|
||||
const raw = String(value || '').trim();
|
||||
const match = raw.match(/^ESP32 homeserver:(.+)$/i);
|
||||
return match ? String(match[1] || '').trim() : '';
|
||||
}
|
||||
|
||||
function mergeHomeserverStatuses(publishedHomeservers = [], serverSessions = []) {
|
||||
const published = Array.isArray(publishedHomeservers) ? publishedHomeservers : [];
|
||||
const homeserverSessions = Array.isArray(serverSessions)
|
||||
? serverSessions.filter((item) => Number(item?.sessionType || 0) === 100)
|
||||
: [];
|
||||
const onlineHomeservers = homeserverSessions.filter((item) => !!item?.onlineOnThisServer);
|
||||
const byName = new Map();
|
||||
onlineHomeservers.forEach((item) => {
|
||||
const sessionName = homeserverSessionNameFromClientInfo(item?.clientInfoFromClient);
|
||||
if (sessionName) {
|
||||
byName.set(sessionName, item);
|
||||
}
|
||||
});
|
||||
|
||||
return published.map((item) => {
|
||||
const matched = byName.get(String(item?.sessionName || '').trim()) || null;
|
||||
let onlineState = matched ? 'online' : 'offline';
|
||||
let activeSessionId = matched?.sessionId ? String(matched.sessionId) : '';
|
||||
if (!matched && published.length === 1 && onlineHomeservers.length === 1) {
|
||||
onlineState = 'online';
|
||||
activeSessionId = String(onlineHomeservers[0]?.sessionId || '');
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
activeSessionId,
|
||||
onlineState,
|
||||
onlineLabel: onlineState === 'online' ? 'online' : onlineState === 'offline' ? 'offline' : 'unknown',
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function hydrateWalletProfile(login) {
|
||||
const cleanLogin = String(login || state.activeSession?.login || state.settings.login || '').trim();
|
||||
if (!cleanLogin) throw new Error('Нет логина для чтения PDA кошелька.');
|
||||
const profile = await readWalletProfileByLogin(cleanLogin);
|
||||
const selectedDeviceName = state.signing.selectedDeviceName
|
||||
|| String(profile?.homeserverSessions?.[0]?.sessionName || '');
|
||||
|
||||
state.walletProfile = {
|
||||
...profile,
|
||||
homeserverSessions: Array.isArray(profile.homeserverSessions) ? profile.homeserverSessions.map((item) => ({
|
||||
...item,
|
||||
onlineState: 'unknown',
|
||||
onlineLabel: 'unknown',
|
||||
activeSessionId: '',
|
||||
})) : [],
|
||||
};
|
||||
state.signing = {
|
||||
...state.signing,
|
||||
selectedDeviceName,
|
||||
};
|
||||
await saveActiveSessionRecord();
|
||||
return state.walletProfile;
|
||||
}
|
||||
|
||||
async function resumeActiveSession({ keepConnected = false } = {}) {
|
||||
const sessionRecord = await loadSessionMaterial();
|
||||
state.activeSession = sessionRecord;
|
||||
if (!sessionRecord) {
|
||||
state.connectionOnline = false;
|
||||
setStatus('Wallet-session ещё не подключена.', 'info');
|
||||
return { ok: true, connected: false };
|
||||
}
|
||||
|
||||
try {
|
||||
await persistSettings({
|
||||
serverLogin: String(sessionRecord?.serverLogin || state.settings.serverLogin || DEFAULT_SHINE_SERVER_LOGIN).trim(),
|
||||
serverHttp: String(sessionRecord?.serverHttp || state.settings.serverHttp || buildHttpBase('shineup.me')).trim(),
|
||||
serverUrl: String(sessionRecord?.serverUrl || state.settings.serverUrl || 'wss://shineup.me/ws').trim(),
|
||||
login: String(sessionRecord?.login || state.settings.login || '').trim(),
|
||||
});
|
||||
const resumed = await ensureApi().resumeSession(sessionRecord);
|
||||
state.connectionOnline = !!keepConnected;
|
||||
if (!keepConnected) {
|
||||
ensureApi().close();
|
||||
state.api = null;
|
||||
setStatus(`Wallet-session сохранена для @${resumed.login}. Подключение будет открываться только по действию.`, 'info');
|
||||
} else {
|
||||
setStatus(`Wallet-session активна для @${resumed.login}.`, 'info');
|
||||
}
|
||||
return { ok: true, connected: true, login: resumed.login, sessionId: resumed.sessionId };
|
||||
} catch (error) {
|
||||
state.connectionOnline = false;
|
||||
setStatus(error.message || 'Не удалось восстановить wallet-session.', 'error');
|
||||
return { ok: false, connected: false, error: state.statusText };
|
||||
}
|
||||
}
|
||||
|
||||
async function attachApprovedSession(payload) {
|
||||
if (String(payload?.type || '') !== 'shine-esp-session-attach') {
|
||||
throw new Error('Доверенное устройство вернуло неподдерживаемый payload. Для plugin нужен session-only approve.');
|
||||
}
|
||||
|
||||
const login = String(payload?.login || state.settings.login || '').trim();
|
||||
const approvedSession = payload?.session || {};
|
||||
const sessionRecord = {
|
||||
login,
|
||||
sessionId: String(approvedSession?.sessionId || '').trim(),
|
||||
sessionKey: state.requesterMaterial?.sessionKey || '',
|
||||
sessionPrivPkcs8: state.requesterMaterial?.sessionPrivPkcs8 || '',
|
||||
sessionType: Number(approvedSession?.sessionType || 50) || 50,
|
||||
serverLogin: state.settings.serverLogin,
|
||||
serverHttp: state.settings.serverHttp,
|
||||
serverUrl: state.settings.serverUrl,
|
||||
};
|
||||
if (!sessionRecord.login || !sessionRecord.sessionId || !sessionRecord.sessionKey || !sessionRecord.sessionPrivPkcs8) {
|
||||
throw new Error('Получен неполный session-only payload');
|
||||
}
|
||||
|
||||
state.sessionAttachInProgress = true;
|
||||
try {
|
||||
state.activeSession = sessionRecord;
|
||||
state.walletProfile = null;
|
||||
state.currentWallet = null;
|
||||
state.signing = {
|
||||
...state.signing,
|
||||
selectedDeviceName: '',
|
||||
devicesResolvedAtMs: 0,
|
||||
};
|
||||
await saveActiveSessionRecord();
|
||||
await hydrateWalletProfile(login);
|
||||
await saveActiveSessionRecord();
|
||||
await persistSettings({
|
||||
login: sessionRecord.login,
|
||||
serverLogin: sessionRecord.serverLogin,
|
||||
serverHttp: sessionRecord.serverHttp,
|
||||
serverUrl: sessionRecord.serverUrl,
|
||||
});
|
||||
state.connectionOnline = false;
|
||||
state.currentWallet = null;
|
||||
} finally {
|
||||
state.sessionAttachInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function pollPairingStatus() {
|
||||
if (!state.pairingId || !state.requesterMaterial) return;
|
||||
try {
|
||||
const payload = await ensureApi().getTrustedDeviceLoginStatus(state.pairingId);
|
||||
const stateValue = String(payload?.state || '');
|
||||
if (stateValue === 'created') {
|
||||
state.pollTimer = setTimeout(() => {
|
||||
void pollPairingStatus();
|
||||
}, 2200);
|
||||
return;
|
||||
}
|
||||
if (stateValue === 'approved') {
|
||||
const decoded = await decryptPairingPayloadFromEnvelope(payload?.encryptedPayload, state.requesterMaterial);
|
||||
await attachApprovedSession(decoded);
|
||||
clearPairingState();
|
||||
setStatus('Wallet-session создана и сохранена. Кошелёк остаётся офлайн до запроса подписи.', 'info');
|
||||
return;
|
||||
}
|
||||
if (stateValue === 'rejected') {
|
||||
clearPairingState();
|
||||
setStatus('Заявка отклонена на доверенном устройстве.', 'error');
|
||||
return;
|
||||
}
|
||||
if (stateValue === 'expired' || stateValue === 'canceled') {
|
||||
clearPairingState();
|
||||
setStatus('Ожидание подключения завершено.', 'error');
|
||||
return;
|
||||
}
|
||||
state.pollTimer = setTimeout(() => {
|
||||
void pollPairingStatus();
|
||||
}, 2200);
|
||||
} catch (error) {
|
||||
clearPairingState();
|
||||
setStatus(error.message || 'Не удалось проверить pairing-статус.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function startPairing({ login, usePassword, password }) {
|
||||
const cleanLogin = String(login || '').trim();
|
||||
if (!cleanLogin) {
|
||||
throw new Error('Введите логин.');
|
||||
}
|
||||
|
||||
await persistSettings({ login: cleanLogin });
|
||||
await resolveServerForLogin(cleanLogin);
|
||||
clearPairingState();
|
||||
setStatus('Проверяем пользователя и создаём wallet-session заявку...', 'info');
|
||||
|
||||
const api = ensureApi();
|
||||
const user = await api.getUser(cleanLogin);
|
||||
if (user?.exists !== true) {
|
||||
throw new Error('Пользователь не найден.');
|
||||
}
|
||||
|
||||
state.requesterMaterial = await createRequesterPairingMaterial();
|
||||
const passwordHash = usePassword
|
||||
? await deriveEspPairingPasswordHash(cleanLogin, String(password || ''))
|
||||
: '';
|
||||
const payload = await api.startTrustedDeviceLogin({
|
||||
login: cleanLogin,
|
||||
passwordHash,
|
||||
requesterSessionKey: state.requesterMaterial.sessionKey,
|
||||
payloadType: 1,
|
||||
});
|
||||
|
||||
state.pairingId = String(payload?.pairingId || '').trim();
|
||||
state.expiresAtMs = Number(payload?.expiresAtMs || 0);
|
||||
state.shortCode = String(payload?.shortCode || '');
|
||||
state.trustedSessionOnline = !!payload?.trustedSessionOnline;
|
||||
if (!state.pairingId) {
|
||||
throw new Error('Сервер не вернул pairingId.');
|
||||
}
|
||||
|
||||
state.pollTimer = setTimeout(() => {
|
||||
void pollPairingStatus();
|
||||
}, 1800);
|
||||
|
||||
setStatus('Wallet-session заявка создана. Ожидаем подтверждение на доверенном устройстве.', 'info');
|
||||
return {
|
||||
pairingId: state.pairingId,
|
||||
shortCode: String(payload?.shortCode || ''),
|
||||
expiresAtMs: state.expiresAtMs,
|
||||
trustedSessionOnline: !!payload?.trustedSessionOnline,
|
||||
};
|
||||
}
|
||||
|
||||
async function cancelPairing() {
|
||||
if (!state.pairingId || !state.requesterMaterial?.sessionKey) {
|
||||
clearPairingState();
|
||||
return { ok: true };
|
||||
}
|
||||
await ensureApi().cancelTrustedDeviceLogin(state.pairingId, state.requesterMaterial.sessionKey);
|
||||
clearPairingState();
|
||||
setStatus('Ожидание подключения отменено.', 'info');
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
async function disconnectSession() {
|
||||
ensureApi().close();
|
||||
state.api = null;
|
||||
await clearSessionMaterial();
|
||||
state.activeSession = null;
|
||||
state.connectionOnline = false;
|
||||
state.walletProfile = null;
|
||||
state.signing = {
|
||||
selectedDeviceName: '',
|
||||
devicesResolvedAtMs: 0,
|
||||
};
|
||||
state.currentWallet = null;
|
||||
setStatus('Сохранённая wallet-session удалена из plugin.', 'info');
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
async function refreshWalletDevices() {
|
||||
if (!state.activeSession?.login) {
|
||||
throw new Error('Сначала подключите wallet-session.');
|
||||
}
|
||||
await hydrateWalletProfile(state.activeSession.login);
|
||||
const resumed = await resumeActiveSession({ keepConnected: true });
|
||||
if (!resumed.ok) {
|
||||
throw new Error(resumed.error || 'Не удалось открыть wallet-session.');
|
||||
}
|
||||
try {
|
||||
const sessions = await ensureApi().listSessions();
|
||||
state.walletProfile = {
|
||||
...state.walletProfile,
|
||||
homeserverSessions: mergeHomeserverStatuses(state.walletProfile?.homeserverSessions, sessions),
|
||||
};
|
||||
state.signing.devicesResolvedAtMs = Date.now();
|
||||
if (!state.signing.selectedDeviceName && state.walletProfile.homeserverSessions[0]?.sessionName) {
|
||||
state.signing.selectedDeviceName = state.walletProfile.homeserverSessions[0].sessionName;
|
||||
}
|
||||
await saveActiveSessionRecord();
|
||||
setStatus('Список доверенных homeserver-устройств обновлён.', 'info');
|
||||
return {
|
||||
ok: true,
|
||||
devices: state.walletProfile.homeserverSessions,
|
||||
};
|
||||
} finally {
|
||||
ensureApi().close();
|
||||
state.api = null;
|
||||
state.connectionOnline = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function updateSigningSelection({ selectedDeviceName } = {}) {
|
||||
state.signing = {
|
||||
...state.signing,
|
||||
selectedDeviceName: String(selectedDeviceName || state.signing.selectedDeviceName || ''),
|
||||
};
|
||||
await saveActiveSessionRecord();
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
async function resolveSelectedHomeserverSession() {
|
||||
if (!state.activeSession?.login) {
|
||||
throw new Error('Сначала подключите wallet-session.');
|
||||
}
|
||||
if (!state.signing.selectedDeviceName) {
|
||||
throw new Error('Не выбрано устройство homeserver.');
|
||||
}
|
||||
let selectedDevice = (state.walletProfile?.homeserverSessions || []).find((item) => item.sessionName === state.signing.selectedDeviceName);
|
||||
if (!selectedDevice) {
|
||||
throw new Error('Выбранное устройство не найдено в PDA аккаунта.');
|
||||
}
|
||||
if (!selectedDevice.activeSessionId) {
|
||||
await refreshWalletDevices();
|
||||
selectedDevice = (state.walletProfile?.homeserverSessions || []).find((item) => item.sessionName === state.signing.selectedDeviceName);
|
||||
}
|
||||
if (!selectedDevice?.activeSessionId) {
|
||||
throw new Error('Выбранный homeserver сейчас не найден онлайн на сервере SHiNE.');
|
||||
}
|
||||
return selectedDevice;
|
||||
}
|
||||
|
||||
async function callWalletRpc(requestData, timeoutMs = 8000, abortSignal = null) {
|
||||
const selectedDevice = await resolveSelectedHomeserverSession();
|
||||
const resumed = await resumeActiveSession({ keepConnected: true });
|
||||
if (!resumed.ok) {
|
||||
throw new Error(resumed.error || 'Не удалось открыть wallet-session.');
|
||||
}
|
||||
|
||||
const requestId = String(requestData?.requestId || `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`);
|
||||
const callId = `wallet-rpc-${requestId}`;
|
||||
const payload = {
|
||||
...requestData,
|
||||
requestId,
|
||||
timeMs: Number(requestData?.timeMs || Date.now()),
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await new Promise((resolve, reject) => {
|
||||
let settled = false;
|
||||
let timeoutId = 0;
|
||||
let off = () => {};
|
||||
let removeAbortListener = () => {};
|
||||
const cleanup = () => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
off();
|
||||
removeAbortListener();
|
||||
};
|
||||
timeoutId = setTimeout(() => {
|
||||
cleanup();
|
||||
reject(new Error('Таймаут ответа от ESP32.'));
|
||||
}, timeoutMs);
|
||||
off = ensureApi().onEvent('IncomingCallSignal', (evt) => {
|
||||
const eventPayload = evt?.payload || {};
|
||||
if (String(eventPayload?.callId || '') !== callId) return;
|
||||
if (Number(eventPayload?.type || 0) !== WALLET_RPC_RESPONSE_TYPE) return;
|
||||
cleanup();
|
||||
try {
|
||||
resolve(JSON.parse(String(eventPayload?.data || '{}')));
|
||||
} catch {
|
||||
reject(new Error('ESP32 вернул некорректный JSON.'));
|
||||
}
|
||||
});
|
||||
if (abortSignal) {
|
||||
const onAbort = () => {
|
||||
cleanup();
|
||||
reject(abortSignal.reason instanceof Error ? abortSignal.reason : new Error('Ожидание подписи отменено.'));
|
||||
};
|
||||
if (abortSignal.aborted) {
|
||||
onAbort();
|
||||
return;
|
||||
}
|
||||
abortSignal.addEventListener('abort', onAbort, { once: true });
|
||||
removeAbortListener = () => {
|
||||
abortSignal.removeEventListener('abort', onAbort);
|
||||
};
|
||||
}
|
||||
ensureApi().callSignalToSession({
|
||||
toLogin: state.activeSession.login,
|
||||
targetSessionId: selectedDevice.activeSessionId,
|
||||
callId,
|
||||
type: WALLET_RPC_REQUEST_TYPE,
|
||||
data: JSON.stringify(payload),
|
||||
}).catch((error) => {
|
||||
cleanup();
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
return { response, selectedDevice, requestId };
|
||||
} finally {
|
||||
ensureApi().close();
|
||||
state.api = null;
|
||||
state.connectionOnline = false;
|
||||
}
|
||||
}
|
||||
|
||||
function verifyWalletAgainstPda(wallet) {
|
||||
const type = String(wallet?.type || '').trim();
|
||||
const pub = String(wallet?.publicKeyBase58 || '').trim();
|
||||
const rootKey = String(state.walletProfile?.publicKeys?.rootKeyBase58 || '').trim();
|
||||
const clientKey = String(
|
||||
state.walletProfile?.publicKeys?.clientKeyBase58 || '',
|
||||
).trim();
|
||||
if (type === 'client.key') {
|
||||
return {
|
||||
verified: !!clientKey && clientKey === pub,
|
||||
verificationText: clientKey === pub ? 'Совпадает с clientKey из PDA.' : 'Не совпадает с clientKey из PDA.',
|
||||
};
|
||||
}
|
||||
if (type === 'root.key') {
|
||||
return {
|
||||
verified: !!rootKey && rootKey === pub,
|
||||
verificationText: rootKey === pub ? 'Совпадает с rootKey из PDA.' : 'Не совпадает с rootKey из PDA.',
|
||||
};
|
||||
}
|
||||
return {
|
||||
verified: null,
|
||||
verificationText: 'Для custom-кошелька проверка через PDA пока не выполняется.',
|
||||
};
|
||||
}
|
||||
|
||||
async function requestCurrentWallet() {
|
||||
const { response, selectedDevice, requestId } = await callWalletRpc({
|
||||
v: 1,
|
||||
operation: 'get_wallet_public_key',
|
||||
requestId: `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
|
||||
});
|
||||
if (!response?.ok) {
|
||||
throw new Error(`ESP32 отклонил запрос: ${String(response?.error || 'unknown_error')}`);
|
||||
}
|
||||
state.currentWallet = {
|
||||
type: String(response?.wallet?.type || '').trim(),
|
||||
publicKeyBase58: String(response?.wallet?.publicKeyBase58 || '').trim(),
|
||||
homeserverName: selectedDevice.sessionName,
|
||||
requestId: String(response?.requestId || requestId),
|
||||
timeMs: Number(response?.timeMs || 0),
|
||||
...verifyWalletAgainstPda(response?.wallet || {}),
|
||||
};
|
||||
await saveActiveSessionRecord();
|
||||
setStatus(`Кошелёк получен с ${selectedDevice.sessionName}.`, 'info');
|
||||
return { ok: true, wallet: state.currentWallet };
|
||||
}
|
||||
|
||||
async function cancelPendingSiteApproval() {
|
||||
const pending = getCurrentPendingApproval();
|
||||
if (!pending) {
|
||||
setStatus('Сейчас нет активного ожидания подписи.', 'info');
|
||||
return { ok: true };
|
||||
}
|
||||
removePendingApproval(pending.id, {
|
||||
rejectError: makeCodeError('User canceled request in extension.', 'USER_REJECTED'),
|
||||
});
|
||||
setStatus('Ожидание подписи отменено в расширении.', 'info');
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
async function markPendingSiteApprovalResolved(pendingId) {
|
||||
removePendingApproval(pendingId);
|
||||
}
|
||||
|
||||
function enqueueSiteApproval(work) {
|
||||
const run = state.siteApprovalChain.then(work, work);
|
||||
state.siteApprovalChain = run.catch(() => {});
|
||||
return run;
|
||||
}
|
||||
|
||||
async function activatePendingApproval(pending, sender = null) {
|
||||
const abortController = new AbortController();
|
||||
pending.status = 'active';
|
||||
pending.abortController = abortController;
|
||||
pending.timeoutId = setTimeout(() => {
|
||||
removePendingApproval(pending.id, {
|
||||
rejectError: makeCodeError('Signing request timed out in extension.', 'USER_REJECTED'),
|
||||
});
|
||||
setStatus('Ожидание подписи истекло в расширении.', 'error');
|
||||
}, 120000);
|
||||
setStatus(`Сайт ${pending.origin} запросил подпись. Подтвердите или отмените на доверенном устройстве.`, 'info');
|
||||
await openSidePanelForSender(sender);
|
||||
return pending;
|
||||
}
|
||||
|
||||
function beginSiteTransactionFlow(payload = {}) {
|
||||
const pending = makePendingApprovalSnapshot({
|
||||
...payload,
|
||||
kind: 'sign_transaction',
|
||||
id: `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
|
||||
createdAtMs: Date.now(),
|
||||
status: 'queued',
|
||||
});
|
||||
state.pendingApprovals.push({
|
||||
...pending,
|
||||
timeoutId: 0,
|
||||
abortController: null,
|
||||
});
|
||||
return pending;
|
||||
}
|
||||
|
||||
async function siteConnect({ origin, onlyIfTrusted = false } = {}) {
|
||||
const normalizedOrigin = normalizeOrigin(origin);
|
||||
if (!normalizedOrigin) {
|
||||
throw makeCodeError('Site origin is missing.', 'BAD_ORIGIN');
|
||||
}
|
||||
if (onlyIfTrusted && !isOriginApproved(normalizedOrigin)) {
|
||||
throw makeCodeError('Site is not trusted yet.', 'NOT_TRUSTED');
|
||||
}
|
||||
const result = await requestCurrentWallet();
|
||||
const publicKeyBase58 = String(result?.wallet?.publicKeyBase58 || '').trim();
|
||||
if (!publicKeyBase58) {
|
||||
throw makeCodeError('Wallet public key is not available.', 'WALLET_UNAVAILABLE');
|
||||
}
|
||||
if (!isOriginApproved(normalizedOrigin)) {
|
||||
await setOriginApproved(normalizedOrigin, true);
|
||||
}
|
||||
setStatus(`Site ${normalizedOrigin} connected to ${shortKey(publicKeyBase58, 8)}.`, 'info');
|
||||
return {
|
||||
ok: true,
|
||||
publicKeyBase58,
|
||||
walletType: String(result?.wallet?.type || '').trim(),
|
||||
};
|
||||
}
|
||||
|
||||
async function siteDisconnect({ origin } = {}) {
|
||||
const normalizedOrigin = normalizeOrigin(origin);
|
||||
setStatus(normalizedOrigin ? `Site ${normalizedOrigin} disconnected.` : 'Site disconnected.', 'info');
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
async function siteSignTransaction({ origin, publicKeyBase58, transactionBase64, comment, transactionSummary } = {}, sender = null) {
|
||||
const normalizedOrigin = normalizeOrigin(origin);
|
||||
if (!normalizedOrigin) {
|
||||
throw makeCodeError('Site origin is missing.', 'BAD_ORIGIN');
|
||||
}
|
||||
if (!isOriginApproved(normalizedOrigin)) {
|
||||
throw makeCodeError('Site is not trusted yet.', 'NOT_TRUSTED');
|
||||
}
|
||||
const cleanPub = String(publicKeyBase58 || '').trim();
|
||||
const cleanTx = String(transactionBase64 || '').trim();
|
||||
if (!cleanPub || !cleanTx) {
|
||||
throw makeCodeError('Transaction payload is incomplete.', 'BAD_REQUEST');
|
||||
}
|
||||
const pending = beginSiteTransactionFlow({
|
||||
origin: normalizedOrigin,
|
||||
publicKeyBase58: cleanPub,
|
||||
comment: String(comment || '').trim(),
|
||||
transactionSummary: transactionSummary || null,
|
||||
});
|
||||
return enqueueSiteApproval(async () => {
|
||||
await activatePendingApproval(getCurrentPendingApproval() || pending, sender);
|
||||
const activePending = getCurrentPendingApproval() || pending;
|
||||
const requestId = `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
||||
const signComment = String(comment || '').trim() || `Site ${normalizedOrigin} requested transaction signature`;
|
||||
try {
|
||||
const { response } = await callWalletRpc({
|
||||
v: 1,
|
||||
operation: 'sign_transaction',
|
||||
requestId,
|
||||
publicKeyBase58: cleanPub,
|
||||
transactionBase64: cleanTx,
|
||||
comment: signComment,
|
||||
}, 120000, activePending.abortController?.signal || null);
|
||||
if (!response?.ok) {
|
||||
const errorCode = String(response?.error || 'unknown_error').trim().toUpperCase();
|
||||
if (errorCode === 'REJECTED_BY_USER') {
|
||||
throw makeCodeError('User rejected transaction signature on ESP32.', 'USER_REJECTED');
|
||||
}
|
||||
throw makeCodeError(`ESP32 rejected transaction signature: ${String(response?.error || 'unknown_error')}`, errorCode || 'RPC_REJECTED');
|
||||
}
|
||||
setStatus(`Подпись для ${normalizedOrigin} завершена.`, 'info');
|
||||
return {
|
||||
ok: true,
|
||||
publicKeyBase58: String(response?.publicKeyBase58 || cleanPub).trim(),
|
||||
signedTransactionBase64: String(response?.signedTransactionBase64 || '').trim(),
|
||||
signatureBase58: String(response?.signatureBase58 || '').trim(),
|
||||
};
|
||||
} finally {
|
||||
await markPendingSiteApprovalResolved(activePending.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function snapshot() {
|
||||
return {
|
||||
settings: { ...state.settings },
|
||||
pairing: {
|
||||
active: !!state.pairingId,
|
||||
pairingId: state.pairingId,
|
||||
expiresAtMs: state.expiresAtMs,
|
||||
shortCode: state.shortCode,
|
||||
trustedSessionOnline: state.trustedSessionOnline,
|
||||
},
|
||||
session: state.activeSession ? { ...state.activeSession } : null,
|
||||
connectionOnline: !!state.activeSession,
|
||||
walletProfile: state.walletProfile ? { ...state.walletProfile } : null,
|
||||
currentWallet: state.currentWallet ? { ...state.currentWallet } : null,
|
||||
pendingApproval: getCurrentPendingApproval() ? makePendingApprovalSnapshot(getCurrentPendingApproval()) : null,
|
||||
signing: { ...state.signing },
|
||||
status: {
|
||||
text: state.statusText,
|
||||
kind: state.statusKind,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||
(async () => {
|
||||
const type = String(message?.type || '');
|
||||
if (type === 'wallet:getState') {
|
||||
await loadStateFromStorage();
|
||||
sendResponse({ ok: true, state: snapshot() });
|
||||
return;
|
||||
}
|
||||
if (type === 'wallet:saveSettings') {
|
||||
await persistSettings(message?.payload || {});
|
||||
sendResponse({ ok: true, state: snapshot() });
|
||||
return;
|
||||
}
|
||||
if (type === 'wallet:resolveServerInfo') {
|
||||
const result = await resolveServerForLogin(String(message?.payload?.login || '').trim());
|
||||
sendResponse({ ok: true, result, state: snapshot() });
|
||||
return;
|
||||
}
|
||||
if (type === 'wallet:startPairing') {
|
||||
const result = await startPairing(message?.payload || {});
|
||||
sendResponse({ ok: true, result, state: snapshot() });
|
||||
return;
|
||||
}
|
||||
if (type === 'wallet:cancelPairing') {
|
||||
const result = await cancelPairing();
|
||||
sendResponse({ ok: true, result, state: snapshot() });
|
||||
return;
|
||||
}
|
||||
if (type === 'wallet:resumeSession') {
|
||||
const result = await resumeActiveSession();
|
||||
sendResponse({ ok: true, result, state: snapshot() });
|
||||
return;
|
||||
}
|
||||
if (type === 'wallet:refreshWalletDevices') {
|
||||
const result = await refreshWalletDevices();
|
||||
sendResponse({ ok: true, result, state: snapshot() });
|
||||
return;
|
||||
}
|
||||
if (type === 'wallet:updateSigningSelection') {
|
||||
const result = await updateSigningSelection(message?.payload || {});
|
||||
sendResponse({ ok: true, result, state: snapshot() });
|
||||
return;
|
||||
}
|
||||
if (type === 'wallet:requestCurrentWallet') {
|
||||
const result = await requestCurrentWallet();
|
||||
sendResponse({ ok: true, result, state: snapshot() });
|
||||
return;
|
||||
}
|
||||
if (type === 'wallet:disconnectSession') {
|
||||
const result = await disconnectSession();
|
||||
sendResponse({ ok: true, result, state: snapshot() });
|
||||
return;
|
||||
}
|
||||
if (type === 'wallet:cancelPendingSiteApproval') {
|
||||
const result = await cancelPendingSiteApproval();
|
||||
sendResponse({ ok: true, result, state: snapshot() });
|
||||
return;
|
||||
}
|
||||
if (type === 'wallet:siteConnect') {
|
||||
const result = await siteConnect(message?.payload || {});
|
||||
sendResponse({ ok: true, result, state: snapshot() });
|
||||
return;
|
||||
}
|
||||
if (type === 'wallet:siteDisconnect') {
|
||||
const result = await siteDisconnect(message?.payload || {});
|
||||
sendResponse({ ok: true, result, state: snapshot() });
|
||||
return;
|
||||
}
|
||||
if (type === 'wallet:siteSignTransaction') {
|
||||
const result = await siteSignTransaction(message?.payload || {}, _sender);
|
||||
sendResponse({ ok: true, result, state: snapshot() });
|
||||
return;
|
||||
}
|
||||
sendResponse({ ok: false, error: 'UNKNOWN_MESSAGE' });
|
||||
})().catch((error) => {
|
||||
const message = toWalletErrorMessage(error, 'Unknown error');
|
||||
setStatus(message, 'error');
|
||||
sendResponse({ ok: false, error: message, code: String(error?.code || ''), state: snapshot() });
|
||||
});
|
||||
return true;
|
||||
});
|
||||
|
||||
chrome.runtime.onInstalled.addListener(() => {
|
||||
void configureSidePanelBehavior();
|
||||
});
|
||||
|
||||
chrome.runtime.onStartup.addListener(() => {
|
||||
void configureSidePanelBehavior();
|
||||
});
|
||||
|
||||
void configureSidePanelBehavior();
|
||||
|
||||
void loadStateFromStorage().then(async () => {
|
||||
if (state.activeSession?.login) {
|
||||
await hydrateWalletProfile(state.activeSession.login).catch(() => {});
|
||||
setStatus(`Wallet-session сохранена для @${state.activeSession.login}. Подключение будет открываться только по действию.`, 'info');
|
||||
}
|
||||
}).catch((error) => {
|
||||
setStatus(error?.message || 'Не удалось инициализировать wallet plugin.', 'error');
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
}
|
||||
|
||||
group = 'org.example'
|
||||
version = '1.0-SNAPSHOT'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation platform('org.junit:junit-bom:6.0.0')
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter'
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
const PAGE_REQUEST = 'shine-wallet-page-request';
|
||||
const PAGE_RESPONSE = 'shine-wallet-page-response';
|
||||
const PAGE_MESSAGE_TARGET_ORIGIN = '*';
|
||||
|
||||
function injectProviderBridge() {
|
||||
const root = document.head || document.documentElement;
|
||||
if (!root) return;
|
||||
const script = document.createElement('script');
|
||||
script.type = 'module';
|
||||
script.src = chrome.runtime.getURL('provider-bridge.js');
|
||||
script.dataset.shineWalletProvider = '1';
|
||||
root.appendChild(script);
|
||||
script.remove();
|
||||
}
|
||||
|
||||
function respondToPage(id, ok, result, error, code) {
|
||||
window.postMessage({
|
||||
target: PAGE_RESPONSE,
|
||||
id: String(id || ''),
|
||||
ok: !!ok,
|
||||
result: result || null,
|
||||
error: error ? String(error) : '',
|
||||
code: code ? String(code) : '',
|
||||
}, PAGE_MESSAGE_TARGET_ORIGIN);
|
||||
}
|
||||
|
||||
function sendRuntimeMessage(type, payload = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
chrome.runtime.sendMessage({ type, payload }, (response) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
const raw = String(chrome.runtime.lastError.message || 'Runtime message failed');
|
||||
if (/Extension context invalidated/i.test(raw)) {
|
||||
const error = new Error('Расширение было перезагружено или отключено. Обновите страницу и откройте кошелёк заново.');
|
||||
error.code = 'EXTENSION_CONTEXT_INVALIDATED';
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
reject(new Error(raw));
|
||||
return;
|
||||
}
|
||||
if (!response?.ok) {
|
||||
const error = new Error(String(response?.error || 'Wallet operation failed'));
|
||||
error.code = String(response?.code || '');
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.source !== window) return;
|
||||
const data = event.data || {};
|
||||
if (data?.target !== PAGE_REQUEST) return;
|
||||
|
||||
const id = String(data?.id || '');
|
||||
const method = String(data?.method || '');
|
||||
const params = data?.params || {};
|
||||
const origin = window.location.origin;
|
||||
|
||||
(async () => {
|
||||
if (method === 'connect') {
|
||||
const response = await sendRuntimeMessage('wallet:siteConnect', {
|
||||
origin,
|
||||
onlyIfTrusted: !!params?.onlyIfTrusted,
|
||||
});
|
||||
respondToPage(id, true, response.result || null);
|
||||
return;
|
||||
}
|
||||
if (method === 'disconnect') {
|
||||
const response = await sendRuntimeMessage('wallet:siteDisconnect', { origin });
|
||||
respondToPage(id, true, response.result || null);
|
||||
return;
|
||||
}
|
||||
if (method === 'signTransaction') {
|
||||
const response = await sendRuntimeMessage('wallet:siteSignTransaction', {
|
||||
origin,
|
||||
publicKeyBase58: String(params?.publicKeyBase58 || '').trim(),
|
||||
transactionBase64: String(params?.transactionBase64 || '').trim(),
|
||||
comment: String(params?.comment || '').trim(),
|
||||
transactionSummary: params?.transactionSummary || null,
|
||||
});
|
||||
respondToPage(id, true, response.result || null);
|
||||
return;
|
||||
}
|
||||
respondToPage(id, false, null, 'Unsupported provider method', 'UNSUPPORTED_METHOD');
|
||||
})().catch((error) => {
|
||||
respondToPage(id, false, null, error?.message || 'Wallet bridge error', error?.code || '');
|
||||
});
|
||||
});
|
||||
|
||||
injectProviderBridge();
|
||||
@@ -0,0 +1,6 @@
|
||||
#Mon Apr 13 16:45:38 MSK 2026
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
@@ -0,0 +1,234 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=${0##*/}
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
@@ -0,0 +1,89 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||