mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-08-21 06:59:02 +00:00
Compare commits
976 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 572776bb9e | |||
| 073c4ed9f2 | |||
| 42233e62df | |||
| 9390317a75 | |||
| 89b51095bf | |||
| 9438c9a574 | |||
| 7feef9d38b | |||
| 582bb02a75 | |||
| bfa7f3ca6b | |||
| 21facd9cc3 | |||
| d30234aaa6 | |||
| d45a17236e | |||
| 281e52d143 | |||
| 1f78646054 | |||
| 31c844b48e | |||
| 87069f11e5 | |||
| e6afd7744d | |||
| 76763b9bab | |||
| 02b75f567a | |||
| a2c4fefe34 | |||
| 121352b149 | |||
| 0ae7768f20 | |||
| 1a0555f9eb | |||
| 6752bf0279 | |||
| 6dc7e3dfc5 | |||
| c992780974 | |||
| 750d592288 | |||
| c77252cca2 | |||
| df3981876d | |||
| 05146638fc | |||
| 765e3be55b | |||
| 9211975868 | |||
| 96a60e82bc | |||
| 5d9428e568 | |||
| 2fdd531fe7 | |||
| f13756d4c7 | |||
| ae6724b359 | |||
| e11c28cf30 | |||
| b93fbad207 | |||
| 2bedb5f09f | |||
| 19eb6b44d5 | |||
| 048359fb0e | |||
| ae75540d35 | |||
| 9ef58b7763 | |||
| 032168234a | |||
| b53b5c70d3 | |||
| ee53016c6e | |||
| 2bf1116c6a | |||
| ced8012783 | |||
| a8203a6372 | |||
| e4d8bcb88d | |||
| db3ae29bd0 | |||
| 2a78c543a1 | |||
| 53b59a8a22 | |||
| 100bea644c | |||
| fe71592b68 | |||
| 4a96feb829 | |||
| e666eb1797 | |||
| 963c6e5238 | |||
| fac71677d4 | |||
| 075b23b291 | |||
| 3628576d60 | |||
| a3b71d0e3d | |||
| 0ea8453e8a | |||
| 1ffedace7b | |||
| e43f7a7980 | |||
| f6a2d21624 | |||
| f572b00391 | |||
| d74fd527e2 | |||
| 819f35ac93 | |||
| 60da661999 | |||
| 43077c79cf | |||
| 06651dc97c | |||
| 67975d76a0 | |||
| 282e4da1cb | |||
| e7359563c6 | |||
| 03ca87b13e | |||
| b2da21b88a | |||
| bb1b703dc1 | |||
| 990f63e5de | |||
| 6fe8ab4f99 | |||
| d6ed12191d | |||
| 8b159f447a | |||
| e9c32504d1 | |||
| 004799e1a3 | |||
| 8e90c65a62 | |||
| bfe67a13b7 | |||
| f091db3270 | |||
| a602abf1d8 | |||
| 689224fd8d | |||
| 23cf1c2737 | |||
| c48cbb7e55 | |||
| 3665b3c607 | |||
| 804b7c87b7 | |||
| a30875b582 | |||
| 56303eb385 | |||
| d29172f41e | |||
| 6d9d5ad1af | |||
| 2acefee4d2 | |||
| cb9fee8f08 | |||
| b473930f94 | |||
| 6e5eadd25c | |||
| 76017c91a9 | |||
| f1ebb1a6e3 | |||
| 746bf1c15f | |||
| 0db65ccb6b | |||
| fe2fbb847f | |||
| ba36680a7b | |||
| 90ff1e62ce | |||
| 2e1e02baac | |||
| 2dca408e45 | |||
| fa56cdd015 | |||
| 264fa9a28e | |||
| d5c8525afc | |||
| d9bbd1b9ff | |||
| b47cee435e | |||
| c078bac0e7 | |||
| 6177b08632 | |||
| 53a1bacd46 | |||
| 4c012a9493 | |||
| abb9b6c8ad | |||
| 6e2507d609 | |||
| 536981998b | |||
| 1a87f2d701 | |||
| 81e24d582d | |||
| 28a5fc5915 | |||
| ba4290cf0d | |||
| 692644d214 | |||
| 4aeaf94d2d | |||
| 8391ca8052 | |||
| 807c76346b | |||
| 308573918c | |||
| 74442f197d | |||
| b53c8e1f80 | |||
| c5f73cf8f4 | |||
| c1bf2620c7 | |||
| 8383363e74 | |||
| 6e34343bde | |||
| 01e4ff65a2 | |||
| 9e40e38f07 | |||
| 181624daf1 | |||
| f0614c882b | |||
| 14f42d1fe8 | |||
| 999f9e2ded | |||
| 61dc8a0225 | |||
| 0b2d5f75cc | |||
| 3c489e1a81 | |||
| 910fd82051 | |||
| 866e12fbc5 | |||
| ccd71d9593 | |||
| e9b9ba4602 | |||
| 986e37a3c9 | |||
| 2ac36d2d0b | |||
| 3b79549129 | |||
| 160a778104 | |||
| e496f8ecc2 | |||
| 9d8132978f | |||
| 73c29f666f | |||
| d895c9fa91 | |||
| b5e66387c3 | |||
| 1fbfdbccf8 | |||
| 2648f18b12 | |||
| 908c0e2cd5 | |||
| 4f3530db5e | |||
| 73137891c4 | |||
| c28087ac44 | |||
| 9e9dd3a521 | |||
| 3bf689f339 | |||
| 86c326ef12 | |||
| 5ae97d47f1 | |||
| 762512b7dc | |||
| 9c848a6215 | |||
| 9c86e15ddb | |||
| 57ccdb0044 | |||
| 1172175cc8 | |||
| 9ed8ad5903 | |||
| e4da8b979e | |||
| 4b83fd8a40 | |||
| cd80da58d8 | |||
| a1f7010e08 | |||
| 3499d2fa75 | |||
| 948e039574 | |||
| 27dc37713b | |||
| ed834e3553 | |||
| ce08d93ae2 | |||
| 13fd1b1f3b | |||
| d04c781ada | |||
| 24605777a6 | |||
| b5ac792853 | |||
| 44c319dcc6 | |||
| a442971b81 | |||
| 8b5adb6bc1 | |||
| c3add0ce84 | |||
| 8725b01995 | |||
| 936e8279f9 | |||
| 6193023c8f | |||
| 1800939833 | |||
| 2f48fc2ef9 | |||
| 918ec0574f | |||
| 1139b22141 | |||
| 7598c25fc8 | |||
| 0f6a9277a8 | |||
| 6a8650d484 | |||
| 46d9e02684 | |||
| 367479d163 | |||
| bbbf5a37c8 | |||
| b2ad1fa979 | |||
| 998be5ba09 | |||
| 9e8d279298 | |||
| fd03bb800c | |||
| f80c28cde7 | |||
| b7e5bedf4b | |||
| 994e426d1e | |||
| c5bf0d94f9 | |||
| 8ad9ada015 | |||
| e854261124 | |||
| 0370b4eb55 | |||
| 5eef5b4e31 | |||
| 61d9c051de | |||
| 5ea1bff1e6 | |||
| bc035cff6a | |||
| f2c008602d | |||
| b8073bca0f | |||
| c30a61441b | |||
| f0f279eec5 | |||
| 464cc8449e | |||
| ee2e57d702 | |||
| a2a5fb166e | |||
| f59f5dfaa3 | |||
| 13f12227bd | |||
| c9db21063b | |||
| 125184f300 | |||
| 8068517808 | |||
| a4f6bbda5e | |||
| 8b2598fdac | |||
| c36fe78bd5 | |||
| bd8385464e | |||
| 8c996b5bc6 | |||
| d1901939e1 | |||
| 41ec11fb66 | |||
| df47490d85 | |||
| a2f6d14b15 | |||
| 6a6c6d6502 | |||
| 2d98c5d311 | |||
| 03b13f8ab0 | |||
| 6e74ad942f | |||
| 0f85f247b6 | |||
| ac1d350aaf | |||
| 5f8e1ef307 | |||
| aa5d4ad078 | |||
| 44d9572f5c | |||
| 3314001205 | |||
| ce1084abc7 | |||
| ea8563cb19 | |||
| b3c1c83677 | |||
| fcbc3b4d75 | |||
| 39e1a0ffe8 | |||
| 99bbd8805c | |||
| e7af85ffba | |||
| 75f3277102 | |||
| 216f8b64bf | |||
| bc301c5fdb | |||
| ae9d6de093 | |||
| 7a4c3184a1 | |||
| 58307aee9c | |||
| 409242507c | |||
| 0a8194fa10 | |||
| eaac40df3a | |||
| 58b488b840 | |||
| 1676702a09 | |||
| 3943848add | |||
| 831dbeaab5 | |||
| 49e719ded8 | |||
| c2d2409bff | |||
| 485f57d86b | |||
| 0e6f303eec | |||
| ad94ae05c1 | |||
| 89dc8c0463 | |||
| 77640c5cc0 | |||
| cdc8cfeb7d | |||
| 4740df2e2c | |||
| ec0f45a488 | |||
| 9d7e06c255 | |||
| 4b93e78dd9 | |||
| 5ced8c90d3 | |||
| 9d22711368 | |||
| 23e6295dd2 | |||
| bbd75ab7ef | |||
| 7508fee62c | |||
| fe88760766 | |||
| 75ece38725 | |||
| b53032a8a5 | |||
| e7f8952ece | |||
| 82a6ae0791 | |||
| d303098e35 | |||
| 1a69ce2d97 | |||
| eedebe1c52 | |||
| 9e61f80809 | |||
| ff591c68a0 | |||
| 8761b9d7e0 | |||
| 1b5125b0a8 | |||
| 9ba885361d | |||
| afa34d83b2 | |||
| d93c914dd3 | |||
| 715a2dd448 | |||
| 58bf60695d | |||
| fbe7954f2e | |||
| d122a8fc3b | |||
| 3998dc124a | |||
| 3d2da9c0db | |||
| 3189d3af4c | |||
| 1b3a99cc5e | |||
| 935c43e88f | |||
| 2893c031ab | |||
| 94cdb16ca9 | |||
| 84d4066986 | |||
| 2153c2dd10 | |||
| a7393c3492 | |||
| 794fece8cc | |||
| 1bf95e85a0 | |||
| a85357a8af | |||
| 7bf3e02f6c | |||
| 459e8d0b24 | |||
| 93585d846a | |||
| 4eb5c4603e | |||
| f42ad5ffc6 | |||
| d0bd8e50be | |||
| 337e3f4449 | |||
| 53bbf74f4d | |||
| 19a66fac20 | |||
| 36e1b9a36e | |||
| 1ca9288bbe | |||
| 4ca2d1e352 | |||
| 0b11730835 | |||
| 6069145b68 | |||
| 2ccda5aebd | |||
| 02810bf527 | |||
| b58ee761a1 | |||
| 5b9d898202 | |||
| 5f8e44e7b7 | |||
| ec80859d93 | |||
| 5e9d92d65c | |||
| c3bb9c0a16 | |||
| bd475e6f9c | |||
| 0f1708d8fd | |||
| efa0e270ad | |||
| ff882024d4 | |||
| 60d95447fb | |||
| a474bf8fe3 | |||
| 1bf0d2b3e6 | |||
| ea02d98a59 | |||
| 9624d7b429 | |||
| 4f440055cb | |||
| 7354ba8fd5 | |||
| b7b4a10485 | |||
| cca0e18f5a | |||
| 8846926b68 | |||
| 7ceefb1b81 | |||
| 861f684d00 | |||
| 17b146b960 | |||
| 02251eeeb4 | |||
| c486572d3d | |||
| a6ed6e3099 | |||
| 175e5e2e8c | |||
| 7bc27038be | |||
| 609bf3219f | |||
| bdbd616a84 | |||
| 1cbe5cb293 | |||
| df6003c08f | |||
| 50fcbf0f5a | |||
| 5a1676a0b9 | |||
| 309f2fbd2c | |||
| 650aacfaa7 | |||
| 8655027a5b | |||
| de937f02d4 | |||
| 2c0446e83d | |||
| 9e6699d8cc | |||
| ef92c5bc0a | |||
| 10f6600f52 | |||
| aeb607e73b | |||
| 8479d2edf0 | |||
| d2e9a8dc06 | |||
| 4571cb0b61 | |||
| 351f7b13b3 | |||
| 5953cb57b0 | |||
| b60b873428 | |||
| a3249cae26 | |||
| f7f784c0f4 | |||
| f4f538f69b | |||
| bbd1a5a2ef | |||
| bc71238046 | |||
| a9df9dde69 | |||
| 7a87e3f3af | |||
| 68f960e4e7 | |||
| 6784fe72dd | |||
| 312df0fcec | |||
| 2913a41aec | |||
| adabbfbef1 | |||
| 2b7962fa7c | |||
| 645f02e2f3 | |||
| b178462e47 | |||
| 1639348b94 | |||
| f67fe262bb | |||
| 0681117af6 | |||
| b5c244a09b | |||
| dceb7255b0 | |||
| 62307b93d7 | |||
| 61ce8b11a6 | |||
| 211f40ddf3 | |||
| 01dd0c79ed | |||
| be7b402c95 | |||
| a11929d2ca | |||
| 0bacb31d17 | |||
| cc7e4aec8a | |||
| 15c338e5b3 | |||
| 80bfcd5eb1 | |||
| b96f14762f | |||
| 0feacfa102 | |||
| a1496c1b79 | |||
| 1b73a138b6 | |||
| efcedd9005 | |||
| 86d4b17257 | |||
| f6a437f7fb | |||
| 3db8a5ac5b | |||
| 266a626acf | |||
| 411e2fb93a | |||
| 6ed83c08e3 | |||
| 9929376acf | |||
| 5135b6404d | |||
| 790669c242 | |||
| 4bb8b1882c | |||
| 56702ca7c2 | |||
| 9be4b9596d | |||
| 37c6b35fb7 | |||
| 5cd7a09797 | |||
| a1856b9bb0 | |||
| 29a36c8658 | |||
| 264b1b38ba | |||
| 65e71508ac | |||
| e5546159c5 | |||
| d7c961ea4f | |||
| ad987f2b5e | |||
| defb934c3f | |||
| 5a850984b9 | |||
| 1185b67b28 | |||
| abb42e8df4 | |||
| 7fdb1af69d | |||
| 78713cc2af | |||
| 951890eaff | |||
| fb00bfac3f | |||
| a6a1483083 | |||
| ef03f020ce | |||
| 070eb4003e | |||
| fca373d936 | |||
| 537cf2e79b | |||
| af424aa5f8 | |||
| 70e0f2913f | |||
| b28283271b | |||
| 4926cf8df5 | |||
| b3312a704a | |||
| 31e8019642 | |||
| 9525738118 | |||
| 62310ad9a9 | |||
| 4819a2f4e2 | |||
| 2435ee780f | |||
| 5d602ece5c | |||
| 900086c1c9 | |||
| 07be74701f | |||
| f46e20c977 | |||
| 4bc752b7a8 | |||
| c28b608146 | |||
| bd4948602a | |||
| 4774b3097f | |||
| de81156223 | |||
| e7e1bedcad | |||
| d8930db8af | |||
| 014db9e233 | |||
| 933920edfd | |||
| cf25d85d51 | |||
| 9af1308e29 | |||
| f4496d8f45 | |||
| 966d1c938b | |||
| f537c7896e | |||
| 4a1479957c | |||
| dc9a16c54b | |||
| a476647d70 | |||
| 95a48e5693 | |||
| 09404ca198 | |||
| 564f76b47d | |||
| c6424f1623 | |||
| a4325ac20d | |||
| d8a6422d37 | |||
| cc963c3562 | |||
| 63f99742fc | |||
| a4636d7872 | |||
| bd2ee03e44 | |||
| deeb81c183 | |||
| 12dbf957b3 | |||
| e1cc0b1ad0 | |||
| f079d57fc6 | |||
| 09b9ed642f | |||
| e74a9f3b41 | |||
| ff7a9d10cb | |||
| 5cfd7f1dc2 | |||
| 853ca2ef53 | |||
| cb0a4854f5 | |||
| 1188157a3e | |||
| 2ae639412d | |||
| 37386c29cb | |||
| e2ad0a1b1a | |||
| 96cdb2e9e0 | |||
| 002ef720ee | |||
| 2214533894 | |||
| 06b7a0419e | |||
| d24ff7b3bc | |||
| a24b3ad3de | |||
| 91c6e3fc30 | |||
| 411f6c0a34 | |||
| 0a3aa706ef | |||
| 5ca74db2f9 | |||
| f24523c2f1 | |||
| e7c5a862da | |||
| b27c738b69 | |||
| 8c565bbf15 | |||
| 37aa9c046f | |||
| 195a6224a0 | |||
| ea238f4988 | |||
| 3514a9a608 | |||
| e2bca9aebb | |||
| 6ae164e59b | |||
| e6ad5efbb7 | |||
| 7bd370b5bc | |||
| 828eb67a52 | |||
| c4df2e66be | |||
| 47e95c0c47 | |||
| 3b5890d0aa | |||
| bee2dc17af | |||
| d6b0173e7a | |||
| 8eafe27955 | |||
| 2af95456a2 | |||
| 8ce48dbcf6 | |||
| 92072b4225 | |||
| 4e823dd3a3 | |||
| 53d9cecdaf | |||
| 680f1f49ff | |||
| 6574272ca8 | |||
| f5c4aa2be2 | |||
| 2498861003 | |||
| 25923e82a4 | |||
| 61cb57e48d | |||
| 4985d836ea | |||
| e1a79a0ff1 | |||
| d72d935607 | |||
| 4ed06b9eff | |||
| fa4623db7c | |||
| a44e8b9921 | |||
| 6b4f1a552e | |||
| 3d32682a67 | |||
| 29e495a17f | |||
| 0c599f7d3a | |||
| 006e4c9fce | |||
| 63be4de418 | |||
| 497ca3f934 | |||
| 60de625c37 | |||
| a09c0e4c2d | |||
| a60f4ce9a8 | |||
| fcb681f4ae | |||
| 8530fa8194 | |||
| 89e8956df1 | |||
| 82ba35c162 | |||
| d750afe38f | |||
| 9b665a43c5 | |||
| 80c769b97d | |||
| 44dd8fd083 | |||
| 4e128e8930 | |||
| c2279e297c | |||
| cdd524b9f3 | |||
| 99809c7919 | |||
| 2390d79111 | |||
| 3b41d73ddd | |||
| 3d46c28911 | |||
| ca7840557b | |||
| ceaa115025 | |||
| 8dbcefbc75 | |||
| b28b96fb4a | |||
| e15a8ed2d8 | |||
| 19b77bf03b | |||
| 7b1541d2e8 | |||
| 830fea63ed | |||
| 3d59de55f8 | |||
| 5c1f5a7d2e | |||
| 4e985420d4 | |||
| 3e584a9652 | |||
| 39acaf846a | |||
| 76948cffa8 | |||
| 4ea8abb53b | |||
| 32085f317b | |||
| faa4367295 | |||
| 3f8a4957af | |||
| b5ea81cf96 | |||
| e14fa5eab0 | |||
| 960ecf616c | |||
| 996ccb0e40 | |||
| 407fee23b9 | |||
| 199570d4a5 | |||
| 5cd9c377d2 | |||
| e08273b8b8 | |||
| 6b0f90c321 | |||
| 0dea488e98 | |||
| e60c4dc34b | |||
| 0c75713820 | |||
| 6a28987a36 | |||
| 7331979107 | |||
| f22808f8ca | |||
| e1c519d71e | |||
| 909f37f89b | |||
| 63b0093321 | |||
| fa06df1400 | |||
| f83027d451 | |||
| 7ebf3d3cdd | |||
| 5daa0dfbb1 | |||
| 7b669d7001 | |||
| ba2407d691 | |||
| 5743d3a3b9 | |||
| 7f1c0f6f7e | |||
| 650d299c99 | |||
| 3a5c2da66c | |||
| d29826e6f2 | |||
| 34fefd1cad | |||
| a5c7eb2fbc | |||
| 6298388fe1 | |||
| 19491ce3f7 | |||
| 3db2053c21 | |||
| ccd7bd6fc2 | |||
| fff78ce00f | |||
| 8bd3d6249d | |||
| f65e743c4c | |||
| 843880d22e | |||
| ec3cc7a859 | |||
| b1733cbd2a | |||
| 4ec2d64cb7 | |||
| d370dae170 | |||
| 72f0a1be54 | |||
| a2e5e03f07 | |||
| 47eda54d6a | |||
| db81b9b1e1 | |||
| b9de1918b4 | |||
| b87d26f271 | |||
| d326ec831c | |||
| faac5b45c6 | |||
| 5e930a7717 | |||
| aff5e22843 | |||
| 344a8dc0a0 | |||
| 7576432e44 | |||
| 3ddc753c88 | |||
| d3a7414e51 | |||
| 1406c81e03 | |||
| 0f28fefc82 | |||
| 496b124baf | |||
| 5a5445e980 | |||
| eecdd3acda | |||
| c66df8c807 | |||
| f2a7617b1b | |||
| af448cf444 | |||
| 9c39061590 | |||
| 28319652c1 | |||
| 7e8a139732 | |||
| e3e179e380 | |||
| 4f768140c1 | |||
| e5728b3501 | |||
| f66f438487 | |||
| 2fd3bf0136 | |||
| c3264f0d15 | |||
| 8d7fdeb633 | |||
| 69011754c9 | |||
| e50d8dc148 | |||
| 23c24355ab | |||
| 0dcf2af50d | |||
| cf07428a5d | |||
| 35b381ae77 | |||
| 0fc748a234 | |||
| 638ea78db9 | |||
| e81747ef7b | |||
| 89e24cb358 | |||
| 00667cecf9 | |||
| 53fcdedb88 | |||
| 5bdf9363e9 | |||
| 6b44a77ef6 | |||
| edc6dc819c | |||
| ffd72eb232 | |||
| feee1f7ab3 | |||
| 2d79cd4b24 | |||
| 637bcfdbc7 | |||
| 22c9dd31bf | |||
| 9e63d80667 | |||
| 67b5b57533 | |||
| c2314f4838 | |||
| 1cb682473a | |||
| 63dd85cbfc | |||
| 6dbc8460b4 | |||
| ec8bf04baa | |||
| a1189faad1 | |||
| ba9cc55b5a | |||
| c2e527bce2 | |||
| 98f3bf151f | |||
| 802a4e243b | |||
| 215ad4fd4c | |||
| 7150d95a98 | |||
| 80636c53da | |||
| 874d3c251c | |||
| 6c541af0fd | |||
| 4d1269051b | |||
| be1b2716d6 | |||
| 34d46a9d5d | |||
| cac3a7cf1e | |||
| 0e86e63e97 | |||
| c0ff5715de | |||
| 24ce1b9893 | |||
| edd8e7c3b9 | |||
| fa0d229418 | |||
| 28ed16d50b | |||
| 01bcf18d03 | |||
| f18764f109 | |||
| 02811b9967 | |||
| 693d7864e4 | |||
| 6aff53f184 | |||
| 4d3f9a8333 | |||
| e4639fc20a | |||
| 033c4e9a5b | |||
| 7aca7ce74d | |||
| 3af282c10a | |||
| d059248802 | |||
| 6bb2a3559d | |||
| 1a829707f4 | |||
| 22430fb8d4 | |||
| acc20844cd | |||
| e694e73a75 | |||
| b4da86d491 | |||
| a27881ecd6 | |||
| 9ce2f3fdea | |||
| 2a4c49c38c | |||
| 415d8cfa54 | |||
| a8cdde7bea | |||
| 8b395239d2 | |||
| 730e7ad72b | |||
| 1e4ea753ba | |||
| a86993af91 | |||
| a832bd433d | |||
| f9fdeb3419 | |||
| 3c2fc74f82 | |||
| 69927e3c2d | |||
| 1edd3716a1 | |||
| bc1ad5598c | |||
| 8cae998146 | |||
| 9a22a760ad | |||
| 909b00bdae | |||
| 58e731c56e | |||
| 04e77e0ee8 | |||
| 55b93a7044 | |||
| f22046eccc | |||
| dfadd38e32 | |||
| 3cb927c840 | |||
| 8ba5715db0 | |||
| c7e8506ad0 | |||
| 8cb0a91bb5 | |||
| 1bcbefeb96 | |||
| 31031edbd1 | |||
| 7116f92d07 | |||
| df8e79f9e6 | |||
| e80e4e3bfd | |||
| da6c6bb03c | |||
| 75718c79b9 | |||
| 9b263def37 | |||
| c7d88da1a6 | |||
| 7b6f8b271c | |||
| 1b18b3ac45 | |||
| 68fc2a143f | |||
| 1deebaff09 | |||
| d6118c9fc4 | |||
| 44a62aef21 | |||
| 0742fc169d | |||
| 62859f901f | |||
| c6316f5aa6 | |||
| 5f2043d229 | |||
| 074658c4bb | |||
| d641ae5b47 | |||
| bec70b4ee5 | |||
| c4373d1560 | |||
| 1dae2c0d25 | |||
| 658d0c9b3a | |||
| 2f343adf21 | |||
| f7a5f2c437 | |||
| 5a336d5e71 | |||
| 91675e26cb | |||
| 078da8ca16 | |||
| 77260bc68a | |||
| ca143f9788 | |||
| 3fc23354ce | |||
| d48e25167f | |||
| cfe5a6bfe4 | |||
| d77102426a | |||
| 11f4edc892 | |||
| 804fa0d3c4 | |||
| d5f20c45b9 | |||
| 2cba96ff36 | |||
| 7139abb947 | |||
| bf4ed416bd | |||
| 06643df6a5 | |||
| 81752c8d78 | |||
| 0cb8c9200a | |||
| 8e64221384 | |||
| fe1837ec23 | |||
| ee53b28e60 | |||
| 18c986cd76 | |||
| a1c3cbcea9 | |||
| 1135a42932 | |||
| a74d0e5167 | |||
| c5579e25fd | |||
| e8b8f0ca5c | |||
| 98e7116230 | |||
| c916eaf43f | |||
| e3169a3495 | |||
| 4b37f1bb2f | |||
| cb93f2636e | |||
| 596c304b08 | |||
| 63f7be02b6 | |||
| de99afa19b | |||
| 5c4e27ea29 | |||
| 54e4230191 | |||
| f069383c2a | |||
| d3a356d415 | |||
| f0e4fdcbcc | |||
| d5f6f42fae | |||
| 7a89858cc6 | |||
| 15c9a76536 | |||
| d7359a8cd5 | |||
| 50e5bc60ee | |||
| 9385be4f1e | |||
| 9cddab9a5e | |||
| 6e01a1d0dc | |||
| f73cd39d2d | |||
| 0303c658ea | |||
| 94ddb63b1d | |||
| b1aa607a7c | |||
| 509f86c1f7 | |||
| b45ca43e27 | |||
| 47482d1e58 | |||
| 59dfe5b50b | |||
| b1f5023fe1 | |||
| a54b94fe53 | |||
| 45bdabcef9 | |||
| 6cdada1118 | |||
| cffc72c516 | |||
| e53514aa12 | |||
| 775de5ce6f | |||
| 4ff92be23b | |||
| 948d8d947e | |||
| 55c300ac4b | |||
| 29b7a5ee56 | |||
| 2ef9ebd7bd | |||
| 850a79c9bb | |||
| 812f0f8211 | |||
| d00c21adb7 | |||
| 925c1548a8 | |||
| cd1f1bd388 | |||
| 474fe00146 | |||
| 44b2e5ea61 | |||
| 48ed7b1b1a | |||
| 6201be82ea | |||
| fe1d296b48 | |||
| 697876d86a | |||
| b72c765911 | |||
| 2a14888b80 | |||
| 99f1bded4d | |||
| d8e9fad205 | |||
| 0e8dc1567c | |||
| 4ff71dcfca | |||
| ed0f0eb15e | |||
| 193a80edea | |||
| df0c85db8f | |||
| c5738c46d5 | |||
| 0acf7ec2c2 | |||
| dc560ba01f | |||
| d74fd92451 | |||
| bcefa774cd | |||
| 8382eaf45f | |||
| 9f700a30c0 | |||
| d815e6c952 | |||
| e60ec953ab | |||
| 72d1e21fc6 | |||
| e4d42817ee | |||
| 5729bde214 | |||
| ec7a77896a | |||
| 99c26c3b7b | |||
| aa20bff2b9 | |||
| f5e949047d | |||
| afb75af5b1 | |||
| 7f99b1181e | |||
| cec0fe3288 | |||
| ba3114c0b6 | |||
| cc2bb772e5 | |||
| fbf9138410 | |||
| 0559aa2037 | |||
| 0d7072ec5d | |||
| c1f55c0dec | |||
| 464b7dd98d | |||
| 4d429437b5 | |||
| bc39292e72 | |||
| 27f70cb9fe | |||
| e5f80adeb7 | |||
| 5a6f6e8db0 | |||
| 21547e6c55 | |||
| 12cfbf70f0 | |||
| c3971cae32 | |||
| f7c2dde79f | |||
| cd2d810c31 | |||
| ba6202e651 | |||
| 40d64030c8 | |||
| 049a20b878 | |||
| 0e5ae7a6a4 | |||
| c867f98117 | |||
| 54eebedc1f | |||
| 35db368527 | |||
| be691fd278 | |||
| 3db3e78b44 | |||
| a48ecccd7d | |||
| 570775552c | |||
| ecbb232d9c | |||
| 0e59e61461 | |||
| 7da322f926 | |||
| 04392752d1 | |||
| db5fcaba44 | |||
| a8f0c8eac9 | |||
| 6fadd70c1d | |||
| 8d8d457da2 | |||
| 893df7272f | |||
| bb90fdc5fc | |||
| 50859171f4 | |||
| fc1b676ce6 | |||
| 776ac652a8 | |||
| c9c438824d | |||
| 95eb1e6d59 | |||
| 1f32367495 | |||
| 0ed5e6542e | |||
| 7240d45a4f | |||
| 17250304e0 | |||
| 835bc49999 | |||
| a6eb430830 | |||
| 8dd68a0811 | |||
| 9540776afd | |||
| f5d4fce945 | |||
| 8840a6e894 | |||
| 7ef98f7741 | |||
| b41be47c9d | |||
| 1e2bc703b8 | |||
| 9115eadd17 | |||
| 05429d3d15 | |||
| 30c1f9f594 | |||
| f994f8d697 | |||
| e045628dab | |||
| 17cdb34fea | |||
| 9df0f6e9df | |||
| 54e4b535cc | |||
| d6555a7af0 | |||
| 30004a1c3c | |||
| 8d60da0a5a | |||
| 17f30b6f26 | |||
| 3d13ee828e | |||
| 9398b12093 | |||
| 61b82645f7 | |||
| c285084dcd | |||
| e034c6e66e | |||
| 7b4662fe4a | |||
| 8fcedb017c | |||
| 7b3ab64930 | |||
| b856b047cc |
@@ -0,0 +1,14 @@
|
||||
---
|
||||
BasedOnStyle: Chromium
|
||||
IndentWidth: '4'
|
||||
SortIncludes: 'false'
|
||||
AllowAllArgumentsOnNextLine: 'true'
|
||||
ColumnLimit: 0
|
||||
Cpp11BracedListStyle: 'true'
|
||||
AllowShortLoopsOnASingleLine: 'true'
|
||||
AllowShortIfStatementsOnASingleLine: 'true'
|
||||
SpacesInLineCommentPrefix:
|
||||
Minimum: 1
|
||||
Maximum: 1
|
||||
|
||||
...
|
||||
+1
-1
@@ -9,4 +9,4 @@ community_bridge: # Replace with a single Community Bridge project-name e.g., cl
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: https://salt.bountysource.com/checkout/amount?team=portapack-mayhem
|
||||
custom: # https://salt.bountysource.com/checkout/amount?team=portapack-mayhem
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
name: Bug report
|
||||
description: File a bug reports regarding the firmware.
|
||||
labels:
|
||||
- bug
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for taking the time to fill out an issue, this template is meant for any issues related to the Mayhem firmware.
|
||||
Please try the latest nightly release before submitting this. You can find the latest nightly version here: https://github.com/portapack-mayhem/mayhem-firmware/releases
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Describe the bug.
|
||||
description: "A clear and concise description of what the bug is."
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: repro
|
||||
attributes:
|
||||
label: Reproduction
|
||||
description: "How can this bug be reproduced?"
|
||||
placeholder: |
|
||||
1. Switch on...
|
||||
2. Press button '....'
|
||||
3. Wait for the end of the universe
|
||||
4. It burns
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: "A clear and concise description of what you expected to happen"
|
||||
placeholder: |
|
||||
1. Generates file on...
|
||||
2. I get a cheeseburger...
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: target
|
||||
attributes:
|
||||
label: Environment/versions
|
||||
description: Specify extra details about versions and environments affected
|
||||
- type: textarea
|
||||
id: anything-else
|
||||
attributes:
|
||||
label: Anything else?
|
||||
description: Let us know if you have anything else to share.
|
||||
@@ -0,0 +1,25 @@
|
||||
name: Feature Request
|
||||
description: For feature requests regarding the firmware.
|
||||
labels:
|
||||
- enhancement
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for taking the time to fill out an issue, this template is meant for any feature suggestions.
|
||||
Please try the latest nightly release before submitting this, make sure it has not been implemented already. You can find the latest nightly version here: https://github.com/portapack-mayhem/mayhem-firmware/releases
|
||||
- type: textarea
|
||||
id: proposal
|
||||
attributes:
|
||||
label: "Description of the feature you're suggesting."
|
||||
description: |
|
||||
Please describe your feature request in as many details as possible.
|
||||
- Describe what it should do.
|
||||
- Note whether it is to extend existing functionality or introduce new functionality.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: anything-else
|
||||
attributes:
|
||||
label: Anything else?
|
||||
description: Let us know if you have anything else to share.
|
||||
@@ -0,0 +1,45 @@
|
||||
name: Firmware/boot issue
|
||||
description: If you are having firmware upgrade or booting problems.
|
||||
labels:
|
||||
- firmware
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: >2-
|
||||
Thank you for taking the time to fill out an issue, this template is meant for any issues related to the Mayhem firmware.
|
||||
Before creating this issue, **do the following**:
|
||||
* Read the Wiki on booting:
|
||||
https://github.com/portapack-mayhem/mayhem-firmware/wiki/Won't-boot
|
||||
|
||||
* Read:
|
||||
https://github.com/portapack-mayhem/mayhem-firmware/wiki/Update-firmware
|
||||
|
||||
* Watch carefully: https://www.youtube.com/watch?v=_zx4ZvurgOs
|
||||
|
||||
* (if you are not in Windows) also check:
|
||||
https://www.youtube.com/watch?v=kjFB58Y1TAo
|
||||
|
||||
* Check hardware versions:
|
||||
https://github.com/portapack-mayhem/mayhem-firmware/wiki/PortaPack-Versions
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: If after following the above troubleshooting the problem still remains
|
||||
placeholder: Describe what was is the problem
|
||||
- type: input
|
||||
id: hardware
|
||||
attributes:
|
||||
label: Your hardware
|
||||
description: Check the link with the versions above
|
||||
placeholder: Which device you have?
|
||||
- type: checkboxes
|
||||
id: firmware_update_before
|
||||
attributes:
|
||||
label: Tests
|
||||
description: >-
|
||||
Things might be confusing the first time, please check the documentation
|
||||
above
|
||||
options:
|
||||
- label: I managed to update/boot previously
|
||||
required: false
|
||||
@@ -1,35 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve the software
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
----
|
||||
|
||||
(Please try the latest nightly release before submitting this. You can find the latest nightly verison here: https://github.com/eried/portapack-mayhem/releases)
|
||||
|
||||
----
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Tap on '....'
|
||||
3.
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Affected versions**
|
||||
Please write any difference related with the Expected behavior, on the following versions:
|
||||
* Latest Stable release:
|
||||
* Latest Nightly release:
|
||||
* Previous working release:
|
||||
|
||||
**Additional**
|
||||
If the bug is difficult to explain, additionally to the text please include images and videos.
|
||||
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Need more help?
|
||||
url: https://discord.com/invite/tuwVMv3
|
||||
about: For any question that does not fit as an issue, check our discord.
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen. Remember that adding stuff is always possible, but time is a limited resource for everyone. Check the wiki for more information how to compile the firmware and try to explore modifying the code yourself.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional**
|
||||
If the suggestion is difficult to explain, additionally to the text please include images and videos.
|
||||
@@ -1,44 +0,0 @@
|
||||
---
|
||||
name: Problem upgrading the firmware or booting
|
||||
about: If you are having firmware upgrade or booting problems
|
||||
title: ''
|
||||
labels: 'firmware'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
----
|
||||
Before creating this issue, **do the following**:
|
||||
* Read the Wiki on booting: https://github.com/eried/portapack-mayhem/wiki/Won't-boot
|
||||
* Read: https://github.com/eried/portapack-havoc/wiki/Update-firmware
|
||||
* Watch carefully: https://www.youtube.com/watch?v=_zx4ZvurgOs
|
||||
* (if you are not in Windows) also check: https://www.youtube.com/watch?v=kjFB58Y1TAo
|
||||
|
||||
----
|
||||
|
||||
**Describe the issue**
|
||||
A clear and concise description of what the issue you are facing is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Tap on '....'
|
||||
|
||||
|
||||
**My Hardware**
|
||||
Please specify what PortaPack hardware version you are using.
|
||||
You can find the list of versions here: https://github.com/eried/portapack-mayhem/wiki/PortaPack-Versions
|
||||
|
||||
**Affected versions**
|
||||
Please tell us what version you are running.
|
||||
Also please try the latest nightly release before submitting this.
|
||||
You can find the latest nightly version here https://github.com/eried/portapack-mayhem/releases
|
||||
|
||||
**Were you able to update the firmware before?**
|
||||
Things might be confusing the first time, please check the video available on the link above.
|
||||
|
||||
**Can you try the upgrade with a different PC/Portapack/HackRF?**
|
||||
If is possible, swap hardware and try again. Also, try different USB cables, even if the one you are using works fine for other purposes.
|
||||
|
||||
**Additional**
|
||||
If the issue is difficult to explain, additionally to the text please include images and videos.
|
||||
@@ -1,28 +1,55 @@
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
raw_git = os.popen('git log next --since="24 hours" --pretty=format:"- %h - {USERNAME}*+%al-%an*: %s"').read()
|
||||
import requests
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
# Set up your personal access token and the repository details
|
||||
token = os.environ.get('GH_TOKEN')
|
||||
repo_owner = "portapack-mayhem"
|
||||
repo_name = "mayhem-firmware"
|
||||
|
||||
|
||||
def compute_username(line):
|
||||
stripped = re.search(r'(?<=\*)(.*?)(?=\*)', line).group(0)
|
||||
def print_stable_changelog(previous_sha):
|
||||
url = f"compare/{previous_sha}...next"
|
||||
commits = handle_get_request(url)
|
||||
for commit in commits["commits"]:
|
||||
# Print the commit details
|
||||
print(format_output(commit))
|
||||
|
||||
pattern = re.compile("[$@+&?].*[$@+&?]")
|
||||
if pattern.match(stripped):
|
||||
stripped = re.sub("[$@+&?].*[$@+&?]", "", stripped)
|
||||
stripped = re.match(r'.+?(?=-)', stripped).group(0)
|
||||
|
||||
def print_nightly_changelog():
|
||||
# Calculate the date and time 24 hours ago
|
||||
since_time = (datetime.now(timezone.utc) - timedelta(hours=24)).isoformat() # Check that this is UTC
|
||||
url = "commits"
|
||||
commits = handle_get_request(url, since_time)
|
||||
for commit in commits:
|
||||
# Print the commit details
|
||||
print(format_output(commit))
|
||||
|
||||
|
||||
def handle_get_request(path, offset=None):
|
||||
headers = {} if token == None else {"Authorization": f"Bearer {token}"}
|
||||
params = {"since": offset}
|
||||
url_base = f"https://api.github.com/repos/{repo_owner}/{repo_name}/"
|
||||
response = requests.get(url_base + path, headers=headers, params=params)
|
||||
|
||||
# Check if the request was successful (status code 200)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
stripped = re.sub(r'^.*?-', "", stripped)
|
||||
return "@" + stripped
|
||||
print(f"Request failed with status code: {response.status_code}")
|
||||
return None
|
||||
|
||||
|
||||
def compile_line(line):
|
||||
username = compute_username(line)
|
||||
line = re.sub(r'[*].*[*]', "", line)
|
||||
line = line.replace("{USERNAME}", username)
|
||||
return line
|
||||
def format_output(commit):
|
||||
message_lines = commit["commit"]["message"].split("\n")
|
||||
author = commit["author"]["login"] if commit["author"] and "login" in commit["author"] else commit["commit"]["author"]["name"]
|
||||
return '- ' + commit["sha"][:8] + ' - @' + author + ': ' + message_lines[0]
|
||||
|
||||
|
||||
for row in raw_git.splitlines():
|
||||
print(compile_line(row))
|
||||
if len(sys.argv) < 2:
|
||||
print_nightly_changelog()
|
||||
else:
|
||||
past_version = sys.argv[1]
|
||||
print_stable_changelog(past_version)
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
name: Check formatting
|
||||
|
||||
on: [pull_request]
|
||||
|
||||
jobs:
|
||||
check_date:
|
||||
runs-on: ubuntu-latest
|
||||
name: Check formatting
|
||||
strategy:
|
||||
matrix:
|
||||
path:
|
||||
- "firmware/common"
|
||||
- "firmware/application"
|
||||
- "firmware/baseband"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: print latest_commit
|
||||
run: echo ${{ github.sha }}
|
||||
- name: clang-format Check
|
||||
uses: jidicula/clang-format-action@v4.11.0
|
||||
with:
|
||||
check-path: ${{ matrix.path }}
|
||||
fallback-style: Chromium
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
- name: check latest commit is less than a day
|
||||
id: should_run
|
||||
continue-on-error: true
|
||||
run: test -z $(git rev-list --after="24 hours" ${{ github.sha }}) && echo "::set-output name=should_run::false"
|
||||
run: test -z $(git rev-list --after="24 hours" ${{ github.sha }}) && echo "should_run=false" >> $GITHUB_OUTPUT
|
||||
build:
|
||||
needs: check_date
|
||||
if: ${{ needs.check_date.outputs.should_run != 'false' }}
|
||||
@@ -28,15 +28,20 @@ jobs:
|
||||
steps:
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
|
||||
run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||
- name: Get version date
|
||||
id: version_date
|
||||
run: echo "::set-output name=date::n_$(date +'%y%m%d')"
|
||||
run: echo "date=n_$(date +'%y%m%d')" >> $GITHUB_OUTPUT
|
||||
- name: Checkout
|
||||
uses: actions/checkout@master
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: next
|
||||
#ref: next
|
||||
# The branch, tag or SHA to checkout. When checking out the repository that
|
||||
# triggered a workflow, this defaults to the reference or SHA for that event.
|
||||
# Otherwise, uses the default branch.
|
||||
# https://github.com/actions/checkout
|
||||
# So scheduled runs will use the default branch (next) but its now possible to trigger a workflow from another branch
|
||||
submodules: true
|
||||
- name: Git Sumbodule Update
|
||||
run: |
|
||||
@@ -47,19 +52,30 @@ jobs:
|
||||
run: mkdir ${{ github.workspace }}/build
|
||||
- name: Run the Docker image
|
||||
run: docker run -e VERSION_STRING=${{ steps.version_date.outputs.date }} -i -v ${{ github.workspace }}:/havoc portapack-dev
|
||||
- name: Create Small SD Card ZIP - No World Map
|
||||
run: |
|
||||
mkdir -p sdcard/FIRMWARE && cp build/firmware/portapack-h1_h2-mayhem.bin sdcard/FIRMWARE/portapack-mayhem_${{ steps.version_date.outputs.date }}.bin && mkdir -p sdcard/APPS && cp build/firmware/application/*.ppma sdcard/APPS && cp build/firmware/standalone/*/*.ppmp sdcard/APPS && cd sdcard && zip -r ../sdcard-no-map.zip . && cd ..
|
||||
- name: Download world map
|
||||
run: |
|
||||
wget https://github.com/portapack-mayhem/mayhem-firmware/releases/download/world_map/world_map.zip
|
||||
- name: Unzip world map
|
||||
run: |
|
||||
unzip world_map.zip -d sdcard/ADSB
|
||||
- name: Create Firmware ZIP
|
||||
run: |
|
||||
zip -j firmware.zip build/firmware/portapack-h1_h2-mayhem.bin && cd flashing && zip -r ../firmware.zip *
|
||||
- name: Create SD Card ZIP
|
||||
run: |
|
||||
zip -r sdcard.zip sdcard
|
||||
mkdir -p sdcard/FIRMWARE && cp build/firmware/portapack-h1_h2-mayhem.bin sdcard/FIRMWARE/portapack-mayhem_${{ steps.version_date.outputs.date }}.bin && mkdir -p sdcard/APPS && cp build/firmware/application/*.ppma sdcard/APPS && cp build/firmware/standalone/*/*.ppmp sdcard/APPS && cd sdcard && zip -r ../sdcard.zip . && cd ..
|
||||
- name: Create changelog
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
CHANGELOG=$(python3 .github/workflows/changelog.py)
|
||||
CHANGELOG="${CHANGELOG//'%'/'%25'}"
|
||||
CHANGELOG="${CHANGELOG//$'\n'/'%0A'}"
|
||||
CHANGELOG="${CHANGELOG//$'\r'/'%0D'}"
|
||||
echo "::set-output name=content::$CHANGELOG"
|
||||
{
|
||||
echo 'content<<EOF'
|
||||
python3 .github/workflows/changelog.py
|
||||
echo EOF
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
id: changelog
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
@@ -77,6 +93,16 @@ jobs:
|
||||
${{ steps.changelog.outputs.content }}
|
||||
draft: false
|
||||
prerelease: true
|
||||
- name: Upload Firmware TAR Asset
|
||||
id: upload-firmware-tar-asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: build/firmware/portapack-mayhem_OCI.ppfw.tar
|
||||
asset_name: mayhem_nightly_${{ steps.version_date.outputs.date }}_OCI.ppfw.tar
|
||||
asset_content_type: application/x-tar
|
||||
- name: Upload Firmware Asset
|
||||
id: upload-firmware-asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
@@ -97,3 +123,13 @@ jobs:
|
||||
asset_path: ./sdcard.zip
|
||||
asset_name: mayhem_nightly_${{ steps.version_date.outputs.date }}_COPY_TO_SDCARD.zip
|
||||
asset_content_type: application/zip
|
||||
- name: Upload SD Card Assets - No Map
|
||||
id: upload-sd-card-asset-no-map
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./sdcard-no-map.zip
|
||||
asset_name: mayhem_nightly_${{ steps.version_date.outputs.date }}_COPY_TO_SDCARD-no-world-map.zip
|
||||
asset_content_type: application/zip
|
||||
|
||||
@@ -9,41 +9,57 @@ jobs:
|
||||
steps:
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
|
||||
run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||
- name: Checkout
|
||||
uses: actions/checkout@master
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: next
|
||||
#ref: next
|
||||
# The branch, tag or SHA to checkout. When checking out the repository that
|
||||
# triggered a workflow, this defaults to the reference or SHA for that event.
|
||||
# Otherwise, uses the default branch.
|
||||
# https://github.com/actions/checkout
|
||||
# So scheduled runs will use the default branch (next) but its now possible to trigger a workflow from another branch
|
||||
submodules: true
|
||||
- name: Git Sumbodule Update
|
||||
run: |
|
||||
git submodule update --init --recursive
|
||||
- name: Get version
|
||||
id: version
|
||||
run: echo "::set-output name=version::$(cat .github/workflows/version.txt)"
|
||||
run: echo "version=$(cat .github/workflows/version.txt)" >> $GITHUB_OUTPUT
|
||||
- name: Get past version
|
||||
id: past_version
|
||||
run: echo "::set-output name=past_version::$(cat .github/workflows/past_version.txt)"
|
||||
run: echo "past_version=$(cat .github/workflows/past_version.txt)" >> $GITHUB_OUTPUT
|
||||
- name: Build the Docker image
|
||||
run: docker build -t portapack-dev -f dockerfile-nogit . --tag my-image-name:$(date +%s)
|
||||
- name: Make build folder
|
||||
run: mkdir ${{ github.workspace }}/build
|
||||
- name: Run the Docker image
|
||||
run: docker run -e VERSION_STRING=${{ steps.version.outputs.version }} -i -v ${{ github.workspace }}:/havoc portapack-dev
|
||||
- name: Create Small SD Card ZIP - No World Map
|
||||
run: |
|
||||
mkdir -p sdcard/FIRMWARE && cp build/firmware/portapack-h1_h2-mayhem.bin sdcard/FIRMWARE/portapack-mayhem_${{ steps.version.outputs.version }}.bin && mkdir -p sdcard/APPS && cp build/firmware/application/*.ppma sdcard/APPS && cp build/firmware/standalone/*/*.ppmp sdcard/APPS && cd sdcard && zip -r ../sdcard-no-map.zip . && cd ..
|
||||
- name: Download world map
|
||||
run: |
|
||||
wget https://github.com/portapack-mayhem/mayhem-firmware/releases/download/world_map/world_map.zip
|
||||
- name: Unzip world map
|
||||
run: |
|
||||
unzip world_map.zip -d sdcard/ADSB
|
||||
- name: Create Firmware ZIP
|
||||
run: |
|
||||
zip -j firmware.zip build/firmware/portapack-h1_h2-mayhem.bin && cd flashing && zip -r ../firmware.zip *
|
||||
- name: Create SD Card ZIP
|
||||
run: |
|
||||
zip -r sdcard.zip sdcard
|
||||
mkdir -p sdcard/FIRMWARE && cp build/firmware/portapack-h1_h2-mayhem.bin sdcard/FIRMWARE/portapack-mayhem_${{ steps.version.outputs.version }}.bin && mkdir -p sdcard/APPS && cp build/firmware/application/*.ppma sdcard/APPS && cp build/firmware/standalone/*/*.ppmp sdcard/APPS && cd sdcard && zip -r ../sdcard.zip . && cd ..
|
||||
- name: Create changelog
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
CHANGELOG=$(python3 .github/workflows/stable_changelog.py ${{ steps.past_version.outputs.past_version }})
|
||||
CHANGELOG="${CHANGELOG//'%'/'%25'}"
|
||||
CHANGELOG="${CHANGELOG//$'\n'/'%0A'}"
|
||||
CHANGELOG="${CHANGELOG//$'\r'/'%0D'}"
|
||||
echo "::set-output name=content::$CHANGELOG"
|
||||
{
|
||||
echo 'content<<EOF'
|
||||
python3 .github/workflows/changelog.py ${{ steps.past_version.outputs.past_version }}
|
||||
echo EOF
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
id: changelog
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
@@ -55,21 +71,32 @@ jobs:
|
||||
release_name: Mayhem firmware ${{ steps.version.outputs.version }}
|
||||
body: |
|
||||
**Stable release - ${{ steps.version.outputs.version }}**
|
||||
This is a fork of the [Havoc](https://github.com/furrtek/portapack-havoc/) firmware, which itself was a fork of the [PortaPack](https://github.com/sharebrained/portapack-hackrf) firmware, an add-on for the [HackRF](http://greatscottgadgets.com/hackrf/). Please check the [readme](https://github.com/eried/portapack-mayhem/blob/master/README.md) for details.
|
||||
This is a fork of the [Havoc](https://github.com/furrtek/portapack-havoc/) firmware, which itself was a fork of the [PortaPack](https://github.com/sharebrained/portapack-hackrf) firmware, an add-on for the [HackRF](http://greatscottgadgets.com/hackrf/). Please check the [readme](https://github.com/portapack-mayhem/mayhem-firmware/blob/master/README.md) for details.
|
||||
## Release notes
|
||||
### Revision (${{ steps.version.outputs.version }}):
|
||||
${{ steps.changelog.outputs.content }}
|
||||
|
||||
**Full Changelog**: https://github.com/eried/portapack-mayhem/compare/${{ steps.past_version.outputs.past_version }}...${{ steps.version.outputs.version }}
|
||||
**Full Changelog**: https://github.com/portapack-mayhem/mayhem-firmware/compare/${{ steps.past_version.outputs.past_version }}...${{ steps.version.outputs.version }}
|
||||
|
||||
## Installation
|
||||
Check the [wiki](https://github.com/eried/portapack-havoc/wiki/Update-firmware) for details how to upgrade.
|
||||
Check the [wiki](https://github.com/portapack-mayhem/mayhem-firmware/wiki/Update-firmware) for details how to upgrade.
|
||||
__Warning:__ Since release 1.8.0, some applications has been moved to the SD card as we ran out of flash space.
|
||||
|
||||
### MicroSD card files
|
||||
|
||||
For certain functionality, like the world map, GPS simulator, and others you need to uncompress (using [7-zip](https://www.7-zip.org/download.html)) the files from `mayhem_vX.Y.Z_COPY_TO_SDCARD.zip` to a FAT32 formatted MicroSD card.
|
||||
For certain functionality, like external apps, the world map, GPS simulator, and others you need to uncompress (using [7-zip](https://www.7-zip.org/download.html)) the files from `mayhem_vX.Y.Z_COPY_TO_SDCARD.zip` to a FAT32 formatted MicroSD card.
|
||||
draft: true
|
||||
prerelease: false
|
||||
- name: Upload Firmware TAR Asset
|
||||
id: upload-firmware-tar-asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: build/firmware/portapack-mayhem_OCI.ppfw.tar
|
||||
asset_name: mayhem_${{ steps.version.outputs.version }}_OCI.ppfw.tar
|
||||
asset_content_type: application/x-tar
|
||||
- name: Upload Firmware Asset
|
||||
id: upload-firmware-asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
@@ -90,4 +117,14 @@ jobs:
|
||||
asset_path: ./sdcard.zip
|
||||
asset_name: mayhem_${{ steps.version.outputs.version }}_COPY_TO_SDCARD.zip
|
||||
asset_content_type: application/zip
|
||||
- name: Upload SD Card Assets - No Map
|
||||
id: upload-sd-card-asset-no-map
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./sdcard-no-map.zip
|
||||
asset_name: mayhem_${{ steps.version.outputs.version }}_COPY_TO_SDCARD-no-world-map.zip
|
||||
asset_content_type: application/zip
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
v1.5.4
|
||||
v2.0.1
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
past_version = sys.argv[1]
|
||||
|
||||
raw_git = os.popen('git log ' + past_version + '..next --pretty=format:"- %h - {USERNAME}*+%al-%an*: %s"').read()
|
||||
|
||||
|
||||
def compute_username(line):
|
||||
stripped = re.search(r'(?<=\*)(.*?)(?=\*)', line).group(0)
|
||||
|
||||
pattern = re.compile("[$@+&?].*[$@+&?]")
|
||||
if pattern.match(stripped):
|
||||
stripped = re.sub("[$@+&?].*[$@+&?]", "", stripped)
|
||||
stripped = re.match(r'.+?(?=-)', stripped).group(0)
|
||||
else:
|
||||
stripped = re.sub(r'^.*?-', "", stripped)
|
||||
return "@" + stripped
|
||||
|
||||
|
||||
def compile_line(line):
|
||||
username = compute_username(line)
|
||||
line = re.sub(r'[*].*[*]', "", line)
|
||||
line = line.replace("{USERNAME}", username)
|
||||
return line
|
||||
|
||||
|
||||
for row in raw_git.splitlines():
|
||||
print(compile_line(row))
|
||||
@@ -1 +1 @@
|
||||
v1.6.0
|
||||
v2.0.2
|
||||
|
||||
+16
-3
@@ -13,6 +13,7 @@
|
||||
/sdcard/FREQMAN/BHT*
|
||||
/sdcard/FREQMAN/R.TXT
|
||||
/sdcard/FREQMAN/XXX.TXT
|
||||
/armbin
|
||||
|
||||
# Compiled Object files
|
||||
*.slo
|
||||
@@ -48,23 +49,35 @@
|
||||
*.map
|
||||
*.lst
|
||||
.dep/
|
||||
build/
|
||||
/build*
|
||||
CMakeFiles/
|
||||
cmake-build-debug/
|
||||
|
||||
# Debugging
|
||||
.gdbinit*
|
||||
|
||||
# Editor files
|
||||
# Editor/ IDE files
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
.vscode
|
||||
.idea
|
||||
|
||||
# VSCodium extensions
|
||||
.history
|
||||
|
||||
# Host OS leftovers
|
||||
.DS_Store
|
||||
/firmware/CMakeCache.txt
|
||||
|
||||
# Python env
|
||||
# Python env/ venv and cache
|
||||
env/
|
||||
venv/
|
||||
**/__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Other
|
||||
*.bak
|
||||
|
||||
# generated bitmap arr file
|
||||
# TODO: generate bitmap during build, since we use python during build anyway, lemme know if this is a bad idea @zxkmm
|
||||
/firmware/tools/bitmap.hpp
|
||||
|
||||
-55
@@ -1,55 +0,0 @@
|
||||
language: cpp
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- os: linux
|
||||
compiler: gcc
|
||||
cache: apt
|
||||
dist: xenial
|
||||
|
||||
env:
|
||||
global:
|
||||
- PROJECT_NAME=PortaPack-HAVOC
|
||||
- SHORT_COMMIT_HASH=`git rev-parse --short HEAD`
|
||||
- VERSION_STRING=nightly-$SHORT_COMMIT_HASH
|
||||
- BUILD_DATE="`date +%Y-%m-%d`"
|
||||
- BUILD_NAME="$PROJECT_NAME-$BUILD_DATE-$SHORT_COMMIT_HASH"
|
||||
- ARTEFACT_BASE=$TRAVIS_BUILD_DIR/artefacts/
|
||||
- ARTEFACT_PATH=$ARTEFACT_BASE/$BUILD_NAME
|
||||
|
||||
before_install:
|
||||
- sudo add-apt-repository ppa:team-gcc-arm-embedded/ppa -y
|
||||
- sudo apt-get update -q
|
||||
- sudo apt-get install gcc-arm-embedded -y
|
||||
|
||||
script:
|
||||
# TODO: Introduce top-level Makefile, this is lame.
|
||||
- sed -e "s/\#set(VERSION.*/set(VERSION \"$VERSION_STRING\")/" -i".bak" CMakeLists.txt
|
||||
- mkdir build/
|
||||
- pushd build/
|
||||
- cmake ..
|
||||
- make firmware
|
||||
- popd
|
||||
|
||||
after_success:
|
||||
- mkdir -p $ARTEFACT_PATH
|
||||
# Copy firmware to firmware-bin directory
|
||||
- cd $TRAVIS_BUILD_DIR/build
|
||||
- cp firmware/portapack-h1-havoc.bin $ARTEFACT_PATH/
|
||||
- cp hackrf/firmware/hackrf_usb/hackrf_usb.dfu $ARTEFACT_PATH/
|
||||
- cd $TRAVIS_BUILD_DIR
|
||||
- cp LICENSE $ARTEFACT_PATH/
|
||||
# Build the archive
|
||||
- cd $ARTEFACT_BASE
|
||||
- tar -cJvf $ARTEFACT_BASE/$BUILD_NAME.tar.xz $BUILD_NAME
|
||||
- md5sum --binary $BUILD_NAME.tar.xz >MD5SUMS
|
||||
- sha256sum --binary $BUILD_NAME.tar.xz >SHA256SUMS
|
||||
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- coreutils
|
||||
- tar
|
||||
- sed
|
||||
- cmake
|
||||
- dfu-util
|
||||
Vendored
+148
@@ -0,0 +1,148 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "(gdb) JTAG probe",
|
||||
"type": "cppdbg",
|
||||
"request": "launch",
|
||||
"miDebuggerPath": "arm-none-eabi-gdb",
|
||||
"targetArchitecture": "arm",
|
||||
"program": "${workspaceRoot}/build/firmware/baseband/baseband_adsbrx.elf",
|
||||
"cwd": "${workspaceRoot}",
|
||||
"setupCommands": [
|
||||
// use logging for troubleshooting
|
||||
//{"text": "set logging file ${workspaceRoot}/build/arm-none-eabi-gdb.log"},
|
||||
//{"text": "set logging on"},
|
||||
{
|
||||
"text": "file '${workspaceRoot}/build/firmware/baseband/baseband_adsbrx.elf'"
|
||||
},
|
||||
{
|
||||
"text": "target extended-remote /dev/ttyACM0"
|
||||
},
|
||||
{
|
||||
"text": "monitor swdp_scan"
|
||||
},
|
||||
{
|
||||
"text": "attach 1"
|
||||
},
|
||||
],
|
||||
"launchCompleteCommand": "None",
|
||||
"externalConsole": false,
|
||||
},
|
||||
{
|
||||
"name": "(gdb) OpenOCD m4 baseband",
|
||||
"type": "cppdbg",
|
||||
"request": "launch",
|
||||
"program": "${workspaceRoot}/build/firmware/baseband/baseband_sd_over_usb.elf",
|
||||
"args": [],
|
||||
"cwd": "${workspaceRoot}",
|
||||
// "environment": [
|
||||
// {
|
||||
// "name": "PATH",
|
||||
// "value": "${env:PATH}"
|
||||
// }
|
||||
// ],
|
||||
"externalConsole": false,
|
||||
"MIMode": "gdb",
|
||||
"miDebuggerPath": "arm-none-eabi-gdb",
|
||||
"targetArchitecture": "arm",
|
||||
"debugServerPath": "openocd",
|
||||
"debugServerArgs": "-f interface/ftdi/um232h.cfg -f target/lpc4350.cfg -c \"gdb_breakpoint_override hard\"",
|
||||
"serverStarted": "Listening on port [0-9]+ for gdb connections",
|
||||
"filterStderr": true,
|
||||
"filterStdout": false,
|
||||
"launchCompleteCommand": "None",
|
||||
"postRemoteConnectCommands": [
|
||||
{
|
||||
"description": "Target Remote Device on Port 3333",
|
||||
"text": "target extended-remote :3333",
|
||||
"ignoreFailures": false
|
||||
},
|
||||
{
|
||||
"description": "Respect Hardware Limitations",
|
||||
"text": "set remote hardware-watchpoint-limit 2",
|
||||
"ignoreFailures": false
|
||||
},
|
||||
{
|
||||
"description": "Respect Hardware Limitations",
|
||||
"text": "set remote hardware-breakpoint-limit 4",
|
||||
"ignoreFailures": false
|
||||
},
|
||||
{
|
||||
"description": "Shutdown GDB Server on GDB Detach",
|
||||
"text": "monitor [target current] configure -event gdb-detach { shutdown }",
|
||||
"ignoreFailures": false
|
||||
},
|
||||
],
|
||||
"stopAtConnect": false,
|
||||
"logging": {
|
||||
"exceptions": true,
|
||||
"engineLogging": false,
|
||||
"moduleLoad": true,
|
||||
"programOutput": true,
|
||||
"trace": false,
|
||||
"traceResponse": false
|
||||
},
|
||||
"useExtendedRemote": true
|
||||
},
|
||||
{
|
||||
"name": "(gdb) OpenOCD m0 application",
|
||||
"type": "cppdbg",
|
||||
"request": "launch",
|
||||
"program": "${workspaceRoot}/build/firmware/application/application.elf",
|
||||
"args": [],
|
||||
"cwd": "${workspaceRoot}",
|
||||
// "environment": [
|
||||
// {
|
||||
// "name": "PATH",
|
||||
// "value": "${env:PATH}"
|
||||
// }
|
||||
// ],
|
||||
"externalConsole": false,
|
||||
"MIMode": "gdb",
|
||||
"miDebuggerPath": "arm-none-eabi-gdb",
|
||||
"targetArchitecture": "arm",
|
||||
"debugServerPath": "openocd",
|
||||
"debugServerArgs": "-f interface/ftdi/um232h.cfg -f target/lpc4350.cfg -c \"gdb_breakpoint_override hard\"",
|
||||
"serverStarted": "Listening on port [0-9]+ for gdb connections",
|
||||
"filterStderr": true,
|
||||
"filterStdout": false,
|
||||
"launchCompleteCommand": "None",
|
||||
"postRemoteConnectCommands": [
|
||||
{
|
||||
"description": "Target Remote Device on Port 3334",
|
||||
"text": "target extended-remote :3334",
|
||||
"ignoreFailures": false
|
||||
},
|
||||
{
|
||||
"description": "Respect Hardware Limitations",
|
||||
"text": "set remote hardware-watchpoint-limit 1",
|
||||
"ignoreFailures": false
|
||||
},
|
||||
{
|
||||
"description": "Respect Hardware Limitations",
|
||||
"text": "set remote hardware-breakpoint-limit 2",
|
||||
"ignoreFailures": false
|
||||
},
|
||||
{
|
||||
"description": "Shutdown GDB Server on GDB Detach",
|
||||
"text": "monitor [target current] configure -event gdb-detach { shutdown }",
|
||||
"ignoreFailures": false
|
||||
},
|
||||
],
|
||||
"stopAtConnect": false,
|
||||
"logging": {
|
||||
"exceptions": true,
|
||||
"engineLogging": false,
|
||||
"moduleLoad": true,
|
||||
"programOutput": true,
|
||||
"trace": false,
|
||||
"traceResponse": false
|
||||
},
|
||||
"useExtendedRemote": true
|
||||
},
|
||||
]
|
||||
}
|
||||
+26
-3
@@ -18,15 +18,17 @@
|
||||
# Boston, MA 02110-1301, USA.
|
||||
#
|
||||
|
||||
cmake_minimum_required(VERSION 2.8.9)
|
||||
cmake_minimum_required(VERSION 3.5)
|
||||
cmake_policy(SET CMP0005 NEW)
|
||||
|
||||
set(CMAKE_TOOLCHAIN_FILE ${CMAKE_CURRENT_LIST_DIR}/firmware/toolchain-arm-cortex-m.cmake)
|
||||
|
||||
project(portapack-h1)
|
||||
|
||||
set(EXPECTED_GCC_VERSION "9.2.1")
|
||||
|
||||
set(VERSION "$ENV{VERSION_STRING}")
|
||||
if ("$ENV{VERSION_STRING}" STREQUAL "")
|
||||
if ("${VERSION}" STREQUAL "")
|
||||
execute_process(
|
||||
COMMAND git log -n 1 --format=%h
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}
|
||||
@@ -40,12 +42,24 @@ if ("$ENV{VERSION_STRING}" STREQUAL "")
|
||||
else (GIT_VERSION_FOUND)
|
||||
set(VERSION "${GIT_VERSION}")
|
||||
endif (GIT_VERSION_FOUND)
|
||||
|
||||
set(VERSION_NOHASH "dev")
|
||||
else()
|
||||
set(VERSION_NOHASH "${VERSION}")
|
||||
endif()
|
||||
|
||||
execute_process(
|
||||
COMMAND bash "-c" "echo ${VERSION} | md5sum | head -c 8"
|
||||
OUTPUT_VARIABLE VERSION_MD5
|
||||
)
|
||||
set(VERSION_MD5 "0x${VERSION_MD5}")
|
||||
|
||||
message("Building version: ${VERSION} MD5: ${VERSION_MD5}")
|
||||
|
||||
set(LICENSE_PATH ${CMAKE_CURRENT_LIST_DIR}/LICENSE)
|
||||
set(HARDWARE_PATH ${CMAKE_CURRENT_LIST_DIR}/hardware)
|
||||
|
||||
add_subdirectory(hackrf/firmware/hackrf_usb)
|
||||
add_subdirectory(hackrf-portapack)
|
||||
|
||||
set(HACKRF_FIRMWARE_DFU_FILENAME hackrf_usb.dfu)
|
||||
set(HACKRF_FIRMWARE_BIN_FILENAME hackrf_usb_ram.bin)
|
||||
@@ -58,4 +72,13 @@ set(HACKRF_CPLD_XSVF_PATH ${HACKRF_PATH}/firmware/cpld/sgpio_if/${HACKRF_CPLD_XS
|
||||
set(HACKRF_FIRMWARE_DFU_IMAGE ${hackrf_usb_BINARY_DIR}/${HACKRF_FIRMWARE_DFU_FILENAME})
|
||||
set(HACKRF_FIRMWARE_BIN_IMAGE ${hackrf_usb_BINARY_DIR}/${HACKRF_FIRMWARE_BIN_FILENAME})
|
||||
|
||||
# this seems causing some issues (ref. in discord discussions), so temporarily disabled, until figure out the pros and cons and bugs of this tool.
|
||||
#find_program(CCACHE "ccache")
|
||||
#if(CCACHE)
|
||||
# set(CMAKE_C_COMPILER_LAUNCHER ${CCACHE})
|
||||
# set(CMAKE_CXX_COMPILER_LAUNCHER ${CCACHE})
|
||||
# set(ENV{CCACHE_SLOPPINESS} pch_defines,time_macros)
|
||||
#endif(CCACHE)
|
||||
|
||||
enable_testing()
|
||||
add_subdirectory(firmware)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/>
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
@@ -290,8 +290,8 @@ to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
{description}
|
||||
Copyright (C) {year} {fullname}
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -329,11 +329,11 @@ necessary. Here is a sample; alter the names:
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
{signature of Ty Coon}, 1 April 1989
|
||||
<signature of Ty Coon>, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License.
|
||||
Public License instead of this License.
|
||||
@@ -1,7 +0,0 @@
|
||||
# License
|
||||
Portapack Mayhem is distributed under [GPL v3.0](LICENSE), however some sub projects might have different (GPL v3.0 compatible) licenses.
|
||||
|
||||
## Other Licenses (compatible with GPL v3.0)
|
||||
- Most of Portapack Mayhem is distributed under GPL-2.0-or-later. [license](LICENSE.GPL-2.0-or-later)
|
||||
- ChibiOS is distributed under GPL v3.0. [license](/firmware/chibios/license.txt)
|
||||
- Portapack-ChibiOS is distributed under Apache-2.0 [license](http://www.apache.org/licenses/LICENSE-2.0)
|
||||
@@ -1,62 +1,63 @@
|
||||
> [!WARNING]
|
||||
> __IF YOU'VE PAID FOR MAYHEM OR ANY PREPACKAGED VERSIONS, YOU'RE BEING SCAMMED.__
|
||||
>
|
||||
> The only legitimate link to our repositories is the [portapack-mayhem](https://github.com/portapack-mayhem/mayhem-firmware) organization on GitHub.
|
||||
|
||||
# PortaPack Mayhem
|
||||
|
||||
[](https://travis-ci.com/eried/portapack-mayhem) [](https://github.com/eried/portapack-mayhem/actions/workflows/create_nightly_release.yml) [](https://codescene.io/projects/8381) [](https://github.com/eried/portapack-mayhem/releases) [](https://github.com/eried/portapack-mayhem/releases/latest) [](https://hub.docker.com/r/eried/portapack) [](https://discord.gg/tuwVMv3) [](https://www.bountysource.com/teams/portapack-mayhem/issues)
|
||||
[](https://github.com/portapack-mayhem/mayhem-firmware/actions/workflows/create_nightly_release.yml) [](https://codescene.io/projects/8381) [](https://github.com/portapack-mayhem/mayhem-firmware/releases) [](https://github.com/portapack-mayhem/mayhem-firmware/releases/latest) [](https://hub.docker.com/r/eried/portapack) [](https://discord.gg/tuwVMv3)
|
||||
|
||||
This is a fork of the [Havoc](https://github.com/furrtek/portapack-havoc/) firmware, which itself was a fork of the [PortaPack](https://github.com/sharebrained/portapack-hackrf) firmware, an add-on for the [HackRF](http://greatscottgadgets.com/hackrf/). A fork is a derivate, in this case one that has extra features and fixes when compared to the older versions.
|
||||
|
||||
[<img src="https://raw.githubusercontent.com/wiki/eried/portapack-mayhem/img/hw_overview_h2_front.png" height="400">](https://github.com/eried/portapack-mayhem/wiki/Hardware-overview) [<img src="https://raw.githubusercontent.com/wiki/eried/portapack-mayhem/img/hw_overview_h2_inside.png" height="400">](https://github.com/eried/portapack-mayhem/wiki/Hardware-overview#portapack-internals)
|
||||
[<img src="https://raw.githubusercontent.com/wiki/portapack-mayhem/mayhem-firmware/img/hw_overview_h2_front.png" height="400">](https://github.com/portapack-mayhem/mayhem-firmware/wiki/Hardware-overview) [<img src="https://raw.githubusercontent.com/wiki/portapack-mayhem/mayhem-firmware/img/hw_overview_h2_inside.png" height="400">](https://github.com/portapack-mayhem/mayhem-firmware/wiki/Hardware-overview#portapack-internals)
|
||||
|
||||
*[PortaPack H2+HackRF+battery](https://s.click.aliexpress.com/e/_DmU7GQX) (clone) with a custom [3d printed case](https://github.com/eried/portapack-mayhem/wiki/H2-Enclosure)*
|
||||
*[PortaPack H2+HackRF+battery](https://grabify.link/7T28JP) (clone) with a custom [3d printed case](https://github.com/portapack-mayhem/mayhem-firmware/wiki/H2-Enclosure)*
|
||||
|
||||
# Quick overview
|
||||
# What is this?
|
||||
|
||||
If you are new to *HackRF+PortaPack+Mayhem*, there is an awesome introductory video by [Tech Minds](https://www.youtube.com/channel/UC9a8Z6Sp6eb2s3O79pX5Zvg) available:
|
||||
If you are new to *HackRF+PortaPack+Mayhem*, check these:
|
||||
|
||||
[](https://www.youtube.com/watch?v=kjFB58Y1TAo)
|
||||
[<img alt="It’s TOO Easy to Accidentally Do Illegal Stuff with This" src="https://img.youtube.com/vi/OPckpjBSAOw/maxresdefault.jpg" width="700">](https://grabify.link/X4D5TF)
|
||||
|
||||
[<img alt="What is the HackRF One Portapack H2+?" src="https://img.youtube.com/vi/alrFbY5vxt4/maxresdefault.jpg" width="230">](https://grabify.link/9UZGEW) [<img alt="Beginner's Guide To The HackRF & Portapak With Mayhem" src="https://img.youtube.com/vi/H-bqdWfbhpg/maxresdefault.jpg" width="230">](https://grabify.link/5MU2VH) [<img alt="HackRF 101 : Everything You Need to Know to Get Started!" src="https://img.youtube.com/vi/xGR_PMD9LeU/maxresdefault.jpg" width="230">](https://grabify.link/C0J6ZR)
|
||||
|
||||
# Frequently Asked Questions
|
||||
|
||||
This repository expands upon the previous work by many people and aims to constantly add new features, bugfixes and generate documentation to make further development easier. [Collaboration](https://github.com/eried/portapack-mayhem/wiki/How-to-collaborate) is always welcomed and appreciated.
|
||||
This repository expands upon the previous work by many people and aims to constantly add new features, bugfixes and generate documentation to make further development easier. [Collaboration](https://github.com/portapack-mayhem/mayhem-firmware/wiki/How-to-collaborate) is always welcomed and appreciated.
|
||||
|
||||
## What to buy?
|
||||
|
||||
:heavy_check_mark: A recommended one is this [PortaPack H2](https://s.click.aliexpress.com/e/_DmU7GQX) pack, that includes everything you need. Sadly, the people making the H2 never made the updated schematics available (against the terms of the license).
|
||||
:heavy_check_mark:  The fabulous [PortaPack H4M Mayhem](https://grabify.link/VPMPSL), featuring numerous improvements and accessories. Sold by our friends at OpenSourceSDRLab. Join their giveaways on discord (check the badge on top).
|
||||
|
||||
:heavy_check_mark: Another popular option is the clone of the [PortaPack H1](https://s.click.aliexpress.com/e/_Dkbqs2X).
|
||||
:heavy_check_mark: A recommended one is this [PortaPack H2](https://grabify.link/7T28JP), that includes everything you need with the plastic case "inspired" on [this](https://github.com/portapack-mayhem/mayhem-firmware/wiki/H2-Enclosure).
|
||||
|
||||
:heavy_check_mark: Some individuals lean towards the [H2 with a metal enclosure](https://grabify.link/HTDXG5), but its advantages remain debated. Share your insights on our [wiki](https://github.com/portapack-mayhem/mayhem-firmware/wiki/Hardware-overview).
|
||||
|
||||
:warning: Be cautious , *ask* the seller about compatibility with the latest releases. Look out for the description of the item, if they provide the firmware files for an older version or they have custom setup instructions, this means it might be **NOT compatible**, for example:
|
||||
|
||||

|
||||
|
||||
:warning: If it looks **too different**, this might mean that they are using their own recipe, check the [different models](https://github.com/eried/portapack-mayhem/wiki/PortaPack-Versions) in our wiki. For example all the H3 and clones of that version use their own version of the firmware. They do not contribute the changes back and eventually you will be left with a device that nobody maintains:
|
||||
:warning: If it looks **too different**, this might mean that they are using their own recipe, check the [different models](https://github.com/portapack-mayhem/mayhem-firmware/wiki/PortaPack-Versions) in our wiki. For example all the H3 and clones of that version use their own version of the firmware. They do not contribute the changes back and eventually you will be left with a device that nobody maintains:
|
||||
|
||||

|
||||
|
||||
## Where is the latest version?
|
||||
|
||||
The current **stable release** is on the [](https://github.com/eried/portapack-mayhem/releases/latest) page. Follow the instructions you can find in the release description. The **latest (nightly) release** can be found [here](https://github.com/eried/portapack-mayhem/releases/).
|
||||
The current **stable release** is on the [](https://github.com/portapack-mayhem/mayhem-firmware/releases/latest) page. Follow the instructions you can find in the release description. The **latest (nightly) release** can be found [here](https://github.com/portapack-mayhem/mayhem-firmware/releases/).
|
||||
|
||||
## How can I collaborate
|
||||
You can write [documentation](https://github.com/eried/portapack-mayhem/wiki), fix bugs and [answer issues](https://github.com/eried/portapack-mayhem/issues) or add new functionality. Please check the following [guide](https://github.com/eried/portapack-mayhem/wiki/How-to-collaborate) with details.
|
||||
You can write [documentation](https://github.com/portapack-mayhem/mayhem-firmware/wiki), fix bugs and [answer issues](https://github.com/portapack-mayhem/mayhem-firmware/issues) or add new functionality. Please check the following [guide](https://github.com/portapack-mayhem/mayhem-firmware/wiki/How-to-collaborate) with details.
|
||||
|
||||
Consider that the hardware and firmware has been created and maintain by a [lot](https://github.com/mossmann/hackrf/graphs/contributors) of [people](https://github.com/eried/portapack-mayhem/graphs/contributors), so always try colaborating your time and effort first. For coding related questions, if something does not fit as an issue, please join our Discord by clicking the chat badge on [top](#portapack-mayhem).
|
||||
Consider that the hardware and firmware has been created and maintain by a [lot](https://github.com/mossmann/hackrf/graphs/contributors) of [people](https://github.com/portapack-mayhem/mayhem-firmware/graphs/contributors), so always try collaborating your time and effort first. For coding related questions, if something does not fit as an issue, please join our Discord by clicking the chat badge on [top](#portapack-mayhem).
|
||||
|
||||
[](https://github.com/eried/portapack-mayhem/graphs/contributors)
|
||||
[](https://github.com/portapack-mayhem/mayhem-firmware/graphs/contributors)
|
||||
|
||||
To support the people behind the hardware, please buy a genuine [HackRF](https://greatscottgadgets.com/hackrf/) and [PortaPack](https://store.sharebrained.com/products/portapack-for-hackrf-one-kit).
|
||||
|
||||
As a last option, if you want to send money directly to me for getting more boards, antennas and such:
|
||||
|
||||
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=CBPQA4HRRPJQ6&source=url)
|
||||
|
||||
## What if I really want something specific?
|
||||
If what you need can be relevant in general, you can [request a feature](https://github.com/eried/portapack-mayhem/issues/new?labels=enhancement&template=feature_request.md).
|
||||
|
||||
You can create a bounty and invite people to your own bounty. This will incentivize coders to work on a new feature, solving a bug or even writting documentation. Start a bounty by [creating](https://github.com/eried/portapack-mayhem/issues/new/choose) or [choosing](https://github.com/eried/portapack-mayhem/issues/) an existing issue. Then, go to [Bountysource](https://www.bountysource.com/) and post a bounty using the link to that specific [issue](https://www.bountysource.com/teams/portapack-mayhem/issues).
|
||||
|
||||
Promote your bounty over our Discord by clicking the chat badge on [top](#portapack-mayhem).
|
||||
If what you need can be relevant in general, you can [request a feature](https://github.com/portapack-mayhem/mayhem-firmware/issues/new?labels=enhancement&template=feature_request.md). Alternatively, go to our Discord by clicking the chat badge on [top](#portapack-mayhem) and discuss there.
|
||||
|
||||
## What if I need help?
|
||||
First, check the [documentation](https://github.com/eried/portapack-mayhem/wiki). If you find a bug or you think the problem is related to the current repository, please open an [issue](https://github.com/eried/portapack-mayhem/issues/new/choose).
|
||||
First, check the [documentation](https://github.com/portapack-mayhem/mayhem-firmware/wiki). If you find a bug or you think the problem is related to the current repository, please open an [issue](https://github.com/portapack-mayhem/mayhem-firmware/issues/new/choose).
|
||||
|
||||
You can reach the [official community](https://www.facebook.com/groups/177623356165819) in Facebook, and our Discord by clicking the chat badge on [top](#portapack-mayhem).
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ COPY ./ /havocsrc
|
||||
|
||||
#Fetch dependencies from APT
|
||||
RUN apt-get update && \
|
||||
apt-get install -y tar wget dfu-util cmake python bzip2 curl python3 python3-yaml && \
|
||||
apt-get install -y tar wget dfu-util cmake python bzip2 lz4 curl python3 python3-yaml && \
|
||||
apt-get -qy autoremove
|
||||
|
||||
#Install current pip from PyPa
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ COPY ./ /havocsrc
|
||||
# Fetch dependencies from APK
|
||||
RUN echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories
|
||||
RUN apk update -U
|
||||
RUN apk add --no-cache git tar wget cmake curl bzip2 make
|
||||
RUN apk add --no-cache git tar wget cmake curl bzip2 lz4 make
|
||||
RUN apk add --no-cache dfu-util ccache icu-data-full
|
||||
RUN apk add --no-cache python3 py3-pip py3-yaml
|
||||
RUN apk add --no-cache py3-pyyaml-env-tag
|
||||
|
||||
+16
-15
@@ -10,32 +10,33 @@ VOLUME /havoc
|
||||
WORKDIR /havoc/firmware
|
||||
|
||||
# Fetch dependencies from APT
|
||||
RUN apt-get update && \
|
||||
apt-get install -y git tar wget dfu-util cmake python3 ccache bzip2 curl && \
|
||||
apt-get -qy autoremove
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y git tar wget dfu-util cmake python3 ccache bzip2 liblz4-tool curl ninja-build \
|
||||
&& apt-get -qy autoremove \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
#Install current pip from PyPa
|
||||
RUN curl https://bootstrap.pypa.io/pip/3.4/get-pip.py -o get-pip.py && \
|
||||
python3 get-pip.py
|
||||
|
||||
#Fetch additional dependencies from Python 3.x pip
|
||||
RUN pip install pyyaml
|
||||
RUN ln -s /usr/bin/python3 /usr/bin/python && \
|
||||
ln -s /usr/bin/pip3 /usr/bin/pip
|
||||
RUN pip install pyyaml \
|
||||
&& ln -s /usr/bin/python3 /usr/bin/python \
|
||||
&& ln -s /usr/bin/pip3 /usr/bin/pip
|
||||
|
||||
ENV LANG C.UTF-8
|
||||
ENV LC_ALL C.UTF-8
|
||||
|
||||
# Grab the GNU ARM toolchain from arm.com
|
||||
# Then extract contents to /opt/build/armbin/
|
||||
RUN mkdir /opt/build && cd /opt/build && \
|
||||
wget -O gcc-arm-none-eabi $ARMBINURL && \
|
||||
mkdir armbin && \
|
||||
tar --strip=1 -xjvf gcc-arm-none-eabi -C armbin
|
||||
RUN mkdir /opt/build \
|
||||
&& cd /opt/build \
|
||||
&& wget -O gcc-arm-none-eabi $ARMBINURL \
|
||||
&& mkdir armbin \
|
||||
&& tar --strip=1 -xjvf gcc-arm-none-eabi -C armbin
|
||||
|
||||
# Configure CCACHE
|
||||
RUN mkdir ~/bin && cd ~/bin && \
|
||||
for tool in gcc g++ cpp c++;do ln -s $(which ccache) arm-none-eabi-$tool;done
|
||||
ADD firmware/tools/docker-entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
||||
|
||||
CMD cd .. && cd build && \
|
||||
cmake .. && make firmware
|
||||
# replace make with ninja temporarily while building your image if you prefer to use that by default
|
||||
CMD ["make"]
|
||||
|
||||
@@ -10,7 +10,7 @@ WORKDIR /havoc/firmware
|
||||
# Fetch dependencies from APK
|
||||
RUN echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories
|
||||
RUN apk update -U
|
||||
RUN apk add --no-cache git tar wget cmake curl bzip2 make
|
||||
RUN apk add --no-cache git tar wget cmake curl bzip2 lz4 make
|
||||
RUN apk add --no-cache dfu-util ccache icu-data-full
|
||||
RUN apk add --no-cache python3 py3-pip py3-yaml
|
||||
RUN apk add --no-cache py3-pyyaml-env-tag
|
||||
@@ -19,9 +19,5 @@ RUN apk add --no-cache g++ gcc clang clang-static clang-dev llvm-dev llvm-static
|
||||
ENV LANG C.UTF-8
|
||||
ENV LC_ALL C.UTF-8
|
||||
|
||||
# Configure CCACHE
|
||||
RUN mkdir ~/bin && cd ~/bin && \
|
||||
for tool in gcc g++ cpp c++;do ln -s $(which ccache) arm-none-eabi-$tool;done
|
||||
|
||||
CMD cd .. && cd build && \
|
||||
cmake .. && make firmware
|
||||
cmake .. && make ppfw
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
FROM ubuntu:xenial
|
||||
|
||||
# Set location to download ARM toolkit from.
|
||||
# This will need to be changed over time or replaced with a static link to the latest release.
|
||||
ENV ARMBINURL=https://developer.arm.com/-/media/Files/downloads/gnu-rm/9-2019q4/gcc-arm-none-eabi-9-2019-q4-major-aarch64-linux.tar.bz2?revision=4583ce78-e7e7-459a-ad9f-bff8e94839f1&rev=4583ce78e7e7459aad9fbff8e94839f1&hash=C9B942D74CEA05FF9DE28380F267BB7D3F3B17A6 \
|
||||
PATH=$HOME/bin:$PATH:/opt/build/armbin/bin
|
||||
|
||||
#Create volume /havoc/bin for compiled firmware binaries
|
||||
VOLUME /havoc
|
||||
WORKDIR /havoc/firmware
|
||||
|
||||
# Fetch dependencies from APT
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y git tar wget dfu-util cmake python3 ccache bzip2 liblz4-tool curl ninja-build \
|
||||
&& apt-get -qy autoremove \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
#Install current pip from PyPa
|
||||
RUN curl https://bootstrap.pypa.io/pip/3.4/get-pip.py -o get-pip.py && \
|
||||
python3 get-pip.py
|
||||
|
||||
#Fetch additional dependencies from Python 3.x pip
|
||||
RUN pip install pyyaml \
|
||||
&& ln -s /usr/bin/python3 /usr/bin/python \
|
||||
&& ln -s /usr/bin/pip3 /usr/bin/pip
|
||||
|
||||
ENV LANG C.UTF-8
|
||||
ENV LC_ALL C.UTF-8
|
||||
|
||||
# Grab the GNU ARM toolchain from arm.com
|
||||
# Then extract contents to /opt/build/armbin/
|
||||
RUN mkdir /opt/build \
|
||||
&& cd /opt/build \
|
||||
&& wget -O gcc-arm-none-eabi $ARMBINURL \
|
||||
&& mkdir armbin \
|
||||
&& tar --strip=1 -xjvf gcc-arm-none-eabi -C armbin
|
||||
|
||||
ADD firmware/tools/docker-entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
||||
|
||||
# replace make with ninja temporarily while building your image if you prefer to use that by default
|
||||
CMD ["make"]
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
<head>
|
||||
<meta http-equiv="refresh" content="0; URL=https://github.com/eried/portapack-mayhem/" />
|
||||
<meta http-equiv="refresh" content="0; URL=https://github.com/portapack-mayhem/mayhem-firmware/" />
|
||||
</head>
|
||||
<body>
|
||||
<p>If you are not redirected, <a href="https://github.com/eried/portapack-mayhem/">click here</a>.</p>
|
||||
<p>If you are not redirected, <a href="https://github.com/portapack-mayhem/mayhem-firmware/">click here</a>.</p>
|
||||
</body>
|
||||
|
||||
+66
-4
@@ -18,6 +18,8 @@
|
||||
# Boston, MA 02110-1301, USA.
|
||||
#
|
||||
|
||||
cmake_minimum_required(VERSION 3.5)
|
||||
|
||||
project(firmware)
|
||||
|
||||
set(BASEBAND ${PROJECT_SOURCE_DIR}/baseband)
|
||||
@@ -26,14 +28,30 @@ set(CHIBIOS ${PROJECT_SOURCE_DIR}/chibios)
|
||||
set(CHIBIOS_PORTAPACK ${PROJECT_SOURCE_DIR}/chibios-portapack)
|
||||
|
||||
set(EXTRACT_CPLD_DATA ${PROJECT_SOURCE_DIR}/tools/extract_cpld_data.py)
|
||||
set(EXPORT_EXTERNAL_APP_IMAGES ${PROJECT_SOURCE_DIR}/tools/export_external_apps.py)
|
||||
set(MAKE_SPI_IMAGE ${PROJECT_SOURCE_DIR}/tools/make_spi_image.py)
|
||||
set(MAKE_IMAGE_CHUNK ${PROJECT_SOURCE_DIR}/tools/make_image_chunk.py)
|
||||
set(LZ4 lz4)
|
||||
|
||||
set(FIRMWARE_NAME portapack-h1_h2-mayhem)
|
||||
set(FIRMWARE_FILENAME ${FIRMWARE_NAME}.bin)
|
||||
set(PPFW_FILENAME "portapack-mayhem_OCI.ppfw.tar")
|
||||
|
||||
# In our current build container cmake need a little help to get the version :)
|
||||
if(NOT DEFINED ${CMAKE_CXX_COMPILER_VERSION})
|
||||
execute_process(COMMAND bash "-c" "arm-none-eabi-g++ -v 2>&1 | grep 'gcc version' | awk '{print $3}'" OUTPUT_VARIABLE CMAKE_CXX_COMPILER_VERSION)
|
||||
string(STRIP ${CMAKE_CXX_COMPILER_VERSION} CMAKE_CXX_COMPILER_VERSION)
|
||||
endif()
|
||||
|
||||
set(GCC_VERSION_MISMATCH 0)
|
||||
if(NOT ${CMAKE_CXX_COMPILER_VERSION} VERSION_EQUAL ${EXPECTED_GCC_VERSION})
|
||||
set(GCC_VERSION_MISMATCH 1)
|
||||
endif()
|
||||
|
||||
add_subdirectory(application)
|
||||
add_subdirectory(baseband)
|
||||
add_subdirectory(standalone)
|
||||
add_subdirectory(test)
|
||||
|
||||
# NOTE: Dependencies break if the .bin files aren't included in DEPENDS. WTF, CMake?
|
||||
add_custom_command(
|
||||
@@ -45,16 +63,49 @@ add_custom_command(
|
||||
)
|
||||
|
||||
add_custom_target(
|
||||
firmware ALL
|
||||
firmware
|
||||
DEPENDS ${FIRMWARE_FILENAME} ${HACKRF_FIRMWARE_DFU_FILENAME}
|
||||
)
|
||||
|
||||
if(${GCC_VERSION_MISMATCH})
|
||||
set(COMPILER_MISMATCH_MESSAGE "WARNING: Compiler version mismatch, please use the official compiler version ${EXPECTED_GCC_VERSION} when sharing builds! Current compiler version: ${CMAKE_CXX_COMPILER_VERSION}")
|
||||
message(${COMPILER_MISMATCH_MESSAGE})
|
||||
add_custom_command(
|
||||
TARGET firmware POST_BUILD
|
||||
COMMAND echo ${COMPILER_MISMATCH_MESSAGE}
|
||||
VERBATIM)
|
||||
endif()
|
||||
|
||||
add_custom_target(
|
||||
program
|
||||
COMMAND dfu-util --device 1fc9:000c --download ${HACKRF_FIRMWARE_DFU_IMAGE}
|
||||
COMMAND ${PROJECT_SOURCE_DIR}/tools/enter_mode.sh hackrf
|
||||
COMMAND dfu-util --device 1fc9:000c --download ${HACKRF_FIRMWARE_DFU_IMAGE} || (exit 0) # We need to add it for dfu-utils v.011 , (in v.09 it is not necessary)
|
||||
COMMAND sleep 3s
|
||||
COMMAND hackrf_spiflash -w ${FIRMWARE_FILENAME}
|
||||
DEPENDS ${FIRMWARE_FILENAME}
|
||||
COMMAND hackrf_spiflash -i -R -w ${FIRMWARE_FILENAME}
|
||||
DEPENDS firmware ${FIRMWARE_FILENAME}
|
||||
)
|
||||
|
||||
add_custom_target(
|
||||
program-external-apps
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
|
||||
COMMAND ${PROJECT_SOURCE_DIR}/tools/copy_external_apps.sh
|
||||
DEPENDS program standalone_apps
|
||||
)
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT ${PPFW_FILENAME}
|
||||
|
||||
COMMAND rm -rf firmware_tar
|
||||
COMMAND mkdir -p firmware_tar/FIRMWARE
|
||||
# Using VERSION_NOHASH to avoid dev builds piling up in the FIRMWARE folder of the sd card of testers in #test-drive
|
||||
COMMAND cp ${FIRMWARE_FILENAME} firmware_tar/FIRMWARE/portapack-mayhem_${VERSION_NOHASH}.bin
|
||||
COMMAND mkdir -p firmware_tar/APPS
|
||||
COMMAND cp application/*.ppma firmware_tar/APPS
|
||||
COMMAND cp standalone/*/*.ppmp firmware_tar/APPS
|
||||
COMMAND cd firmware_tar && tar -cvaf ../${PPFW_FILENAME} *
|
||||
DEPENDS firmware ${FIRMWARE_FILENAME} standalone_apps
|
||||
# Dont use VERBATIM here as it prevents usage of globbing (*)
|
||||
# There shouldnt be any funny business in the filenames above :)
|
||||
)
|
||||
|
||||
# TODO: Bad hack to fix location of LICENSE file for tar.
|
||||
@@ -76,7 +127,18 @@ add_custom_command(
|
||||
DEPENDS ${FIRMWARE_NAME}-${VERSION}.tar.bz2 ${FIRMWARE_NAME}-${VERSION}.zip
|
||||
)
|
||||
|
||||
add_custom_target(
|
||||
ppfw ALL
|
||||
DEPENDS ${PPFW_FILENAME}
|
||||
)
|
||||
|
||||
add_custom_target(
|
||||
oci
|
||||
DEPENDS ${PPFW_FILENAME}
|
||||
)
|
||||
|
||||
add_custom_target(
|
||||
release
|
||||
DEPENDS MD5SUMS SHA256SUMS
|
||||
)
|
||||
|
||||
|
||||
@@ -27,16 +27,24 @@
|
||||
|
||||
enable_language(C CXX ASM)
|
||||
|
||||
include(CheckCXXCompilerFlag)
|
||||
|
||||
project(application)
|
||||
|
||||
# Compiler options here.
|
||||
set(USE_OPT "-Os -g --specs=nano.specs")
|
||||
set(USE_OPT "-Os -g --specs=nano.specs -specs=nosys.specs")
|
||||
|
||||
# C specific options here (added to USE_OPT).
|
||||
set(USE_COPT "-std=gnu99")
|
||||
|
||||
# C++ specific options here (added to USE_OPT).
|
||||
set(USE_CPPOPT "-std=c++17 -fno-rtti -fno-exceptions -Weffc++ -Wuninitialized")
|
||||
check_cxx_compiler_flag("-std=c++20" cpp20_supported)
|
||||
if(cpp20_supported)
|
||||
set(USE_CPPOPT "-std=c++20")
|
||||
else()
|
||||
set(USE_CPPOPT "-std=c++17")
|
||||
endif()
|
||||
set(USE_CPPOPT "${USE_CPPOPT} -fno-rtti -fno-exceptions -Weffc++ -Wuninitialized -Wno-volatile")
|
||||
|
||||
# Enable this if you want the linker to remove unused code and data
|
||||
set(USE_LINK_GC yes)
|
||||
@@ -44,7 +52,7 @@ set(USE_LINK_GC yes)
|
||||
# Linker extra options here.
|
||||
set(USE_LDOPT)
|
||||
|
||||
# Enable this if you want link time optimizations (LTO)
|
||||
# Enable this if you want link time optimizations (LTO) - this flag affects chibios only
|
||||
set(USE_LTO no)
|
||||
|
||||
# If enabled, this option allows to compile the application in THUMB mode.
|
||||
@@ -89,6 +97,7 @@ include(${CHIBIOS_PORTAPACK}/os/ports/GCC/ARMCMx/LPC43xx_M0/port.cmake)
|
||||
include(${CHIBIOS}/os/kernel/kernel.cmake)
|
||||
include(${CHIBIOS_PORTAPACK}/os/various/fatfs_bindings/fatfs.cmake)
|
||||
include(${CHIBIOS}/test/test.cmake)
|
||||
include(external/external.cmake)
|
||||
|
||||
# Define linker script file here
|
||||
set(LDSCRIPT ${PORTLD}/LPC43xx_M0.ld)
|
||||
@@ -104,12 +113,23 @@ set(CSRC
|
||||
${BOARDSRC}
|
||||
${FATFSSRC}
|
||||
firmware_info.c
|
||||
usb_serial_cdc.c
|
||||
usb_serial_descriptor.c
|
||||
usb_serial_endpoints.c
|
||||
usb_serial_device_to_host.c
|
||||
${HACKRF_PATH}/firmware/common/usb.c
|
||||
${HACKRF_PATH}/firmware/common/usb_queue.c
|
||||
${HACKRF_PATH}/firmware/hackrf_usb/usb_device.c
|
||||
${HACKRF_PATH}/firmware/common/usb_request.c
|
||||
${HACKRF_PATH}/firmware/common/usb_standard_request.c
|
||||
${CHIBIOS}/os/various/chprintf.c
|
||||
)
|
||||
|
||||
# C++ sources that can be compiled in ARM or THUMB mode depending on the global
|
||||
# setting.
|
||||
set(CPPSRC
|
||||
main.cpp
|
||||
shell.cpp
|
||||
${COMMON}/acars_packet.cpp
|
||||
${COMMON}/adsb.cpp
|
||||
${COMMON}/adsb_frame.cpp
|
||||
@@ -153,8 +173,14 @@ set(CPPSRC
|
||||
${COMMON}/ui_painter.cpp
|
||||
${COMMON}/ui_text.cpp
|
||||
${COMMON}/ui_widget.cpp
|
||||
${COMMON}/ui_language.cpp
|
||||
${COMMON}/utility.cpp
|
||||
${COMMON}/wm8731.cpp
|
||||
${COMMON}/ads1110.cpp
|
||||
${COMMON}/max17055.cpp
|
||||
${COMMON}/battery.cpp
|
||||
${COMMON}/performance_counter.cpp
|
||||
${COMMON}/bmpfile.cpp
|
||||
app_settings.cpp
|
||||
audio.cpp
|
||||
baseband_api.cpp
|
||||
@@ -166,15 +192,29 @@ set(CPPSRC
|
||||
#emu_cc1101.cpp
|
||||
rfm69.cpp
|
||||
event_m0.cpp
|
||||
file_reader.cpp
|
||||
file.cpp
|
||||
file_path.cpp
|
||||
freqman_db.cpp
|
||||
freqman.cpp
|
||||
io_convert.cpp
|
||||
io_file.cpp
|
||||
io_wave.cpp
|
||||
iq_trim.cpp
|
||||
irq_controls.cpp
|
||||
irq_lcd_frame.cpp
|
||||
irq_rtc.cpp
|
||||
log_file.cpp
|
||||
metadata_file.cpp
|
||||
flipper_subfile.cpp
|
||||
portapack.cpp
|
||||
usb_serial_shell.cpp
|
||||
usb_serial_shell_filesystem.cpp
|
||||
usb_serial_event.cpp
|
||||
usb_serial_thread.cpp
|
||||
usb_serial.cpp
|
||||
usb_serial_host_to_device.cpp
|
||||
usb_serial_asyncmsg.cpp
|
||||
qrcodegen.cpp
|
||||
radio.cpp
|
||||
receiver_model.cpp
|
||||
@@ -187,6 +227,7 @@ set(CPPSRC
|
||||
spectrum_color_lut.cpp
|
||||
string_format.cpp
|
||||
temperature_logger.cpp
|
||||
theme.cpp
|
||||
touch.cpp
|
||||
tone_key.cpp
|
||||
transmitter_model.cpp
|
||||
@@ -209,80 +250,100 @@ set(CPPSRC
|
||||
ui/ui_alphanum.cpp
|
||||
ui/ui_audio.cpp
|
||||
ui/ui_channel.cpp
|
||||
ui/ui_font_fixed_5x8.cpp
|
||||
ui/ui_font_fixed_8x16.cpp
|
||||
ui/ui_geomap.cpp
|
||||
ui/ui_qrcode.cpp
|
||||
ui/ui_qrcode.cpp
|
||||
ui/ui_menu.cpp
|
||||
ui/ui_btngrid.cpp
|
||||
ui/ui_receiver.cpp
|
||||
ui/ui_rssi.cpp
|
||||
ui/ui_freqlist.cpp
|
||||
ui/ui_tv.cpp
|
||||
ui/ui_spectrum.cpp
|
||||
ui/ui_tabview.cpp
|
||||
ui/ui_textentry.cpp
|
||||
ui/ui_tone_key.cpp
|
||||
ui/ui_transmitter.cpp
|
||||
ui/ui_bmpview.cpp
|
||||
apps/acars_app.cpp
|
||||
apps/ais_app.cpp
|
||||
apps/analog_audio_app.cpp
|
||||
# apps/analog_tv_app.cpp
|
||||
apps/ble_comm_app.cpp
|
||||
apps/ble_rx_app.cpp
|
||||
apps/ble_tx_app.cpp
|
||||
apps/capture_app.cpp
|
||||
apps/ert_app.cpp
|
||||
# apps/gps_sim_app.cpp
|
||||
# apps/lge_app.cpp
|
||||
apps/pocsag_app.cpp
|
||||
# apps/replay_app.cpp
|
||||
apps/soundboard_app.cpp
|
||||
# apps/tpms_app.cpp
|
||||
apps/ui_about_simple.cpp
|
||||
apps/ui_adsb_rx.cpp
|
||||
apps/ui_adsb_tx.cpp
|
||||
apps/ui_afsk_rx.cpp
|
||||
# apps/ui_adsb_tx.cpp #moved to ext
|
||||
apps/ui_aprs_rx.cpp
|
||||
apps/ui_btle_rx.cpp
|
||||
apps/ui_nrf_rx.cpp
|
||||
apps/ui_aprs_tx.cpp
|
||||
apps/ui_battinfo.cpp
|
||||
apps/ui_bht_tx.cpp
|
||||
apps/ui_coasterp.cpp
|
||||
apps/ui_bmp_file_viewer.cpp
|
||||
apps/ui_btle_rx.cpp
|
||||
# apps/ui_coasterp.cpp
|
||||
apps/ui_debug.cpp
|
||||
apps/ui_debug_battery.cpp
|
||||
apps/ui_dfu_menu.cpp
|
||||
apps/ui_encoders.cpp
|
||||
apps/ui_fileman.cpp
|
||||
apps/ui_flash_utility.cpp
|
||||
apps/ui_freqman.cpp
|
||||
apps/ui_jammer.cpp
|
||||
apps/ui_keyfob.cpp
|
||||
apps/ui_lcr.cpp
|
||||
apps/lge_app.cpp
|
||||
apps/ui_fsk_rx.cpp
|
||||
apps/ui_iq_trim.cpp
|
||||
# apps/ui_jammer.cpp
|
||||
# apps/ui_keyfob.cpp
|
||||
# apps/ui_lcr.cpp
|
||||
apps/ui_level.cpp
|
||||
apps/ui_looking_glass_app.cpp
|
||||
apps/ui_mictx.cpp
|
||||
apps/ui_modemsetup.cpp
|
||||
apps/ui_morse.cpp
|
||||
# apps/ui_morse.cpp
|
||||
# apps/ui_nrf_rx.cpp
|
||||
# apps/ui_nuoptix.cpp
|
||||
apps/ui_playlist.cpp
|
||||
apps/ui_pocsag_tx.cpp
|
||||
apps/ui_rds.cpp
|
||||
apps/ui_recon_settings.cpp
|
||||
apps/ui_recon.cpp
|
||||
apps/ui_remote.cpp
|
||||
apps/ui_scanner.cpp
|
||||
apps/ui_search.cpp
|
||||
apps/ui_sd_over_usb.cpp
|
||||
apps/ui_sd_wipe.cpp
|
||||
apps/ui_search.cpp
|
||||
apps/ui_settings.cpp
|
||||
apps/ui_siggen.cpp
|
||||
apps/ui_sonde.cpp
|
||||
apps/ui_sstvtx.cpp
|
||||
# apps/ui_spectrum_painter_image.cpp
|
||||
# apps/ui_spectrum_painter_text.cpp
|
||||
# apps/ui_spectrum_painter.cpp
|
||||
apps/ui_ss_viewer.cpp
|
||||
# apps/ui_sstvtx.cpp #moved to ext
|
||||
apps/ui_standalone_view.cpp
|
||||
apps/ui_subghzd.cpp
|
||||
# apps/ui_test.cpp
|
||||
apps/ui_text_editor.cpp
|
||||
apps/ui_tone_search.cpp
|
||||
apps/ui_touch_calibration.cpp
|
||||
apps/ui_touchtunes.cpp
|
||||
apps/ui_view_wav.cpp
|
||||
apps/ui_weatherstation.cpp
|
||||
apps/ui_whipcalc.cpp
|
||||
apps/acars_app.cpp
|
||||
apps/ais_app.cpp
|
||||
apps/analog_audio_app.cpp
|
||||
apps/analog_tv_app.cpp
|
||||
apps/capture_app.cpp
|
||||
apps/ert_app.cpp
|
||||
apps/lge_app.cpp
|
||||
apps/pocsag_app.cpp
|
||||
apps/replay_app.cpp
|
||||
apps/ui_playlist.cpp
|
||||
apps/gps_sim_app.cpp
|
||||
apps/soundboard_app.cpp
|
||||
apps/ui_recon.cpp
|
||||
apps/ui_recon_settings.cpp
|
||||
apps/ui_level.cpp
|
||||
apps/tpms_app.cpp
|
||||
apps/tpms_app.cpp
|
||||
protocols/aprs.cpp
|
||||
protocols/ax25.cpp
|
||||
protocols/bht.cpp
|
||||
protocols/dcs.cpp
|
||||
protocols/encoders.cpp
|
||||
protocols/lcr.cpp
|
||||
# protocols/lcr.cpp
|
||||
protocols/modems.cpp
|
||||
protocols/rds.cpp
|
||||
# ui_handwrite.cpp
|
||||
@@ -291,11 +352,18 @@ set(CPPSRC
|
||||
# ui_replay_view.cpp
|
||||
# ui_script.cpp
|
||||
ui_sd_card_debug.cpp
|
||||
config_mode.cpp
|
||||
${CPLD_20150901_DATA_CPP}
|
||||
${CPLD_20170522_DATA_CPP}
|
||||
${HACKRF_CPLD_DATA_CPP}
|
||||
ui_external_items_menu_loader.cpp
|
||||
view_factory_base.cpp
|
||||
)
|
||||
|
||||
set_source_files_properties(${CPPSRC} PROPERTIES COMPILE_FLAGS -flto) # Add lto flag to the non-external sources only
|
||||
|
||||
list (APPEND CPPSRC ${EXTCPPSRC}) # Append external sources after setting lto flag to internal ones
|
||||
|
||||
# C sources to be compiled in ARM mode regardless of the global setting.
|
||||
# NOTE: Mixing ARM and THUMB mode enables the -mthumb-interwork compiler
|
||||
# option that results in lower performance and larger code size.
|
||||
@@ -317,12 +385,18 @@ set(TCSRC)
|
||||
set(TCPPSRC)
|
||||
|
||||
# List ASM source files here
|
||||
set(ASMSRC ${PORTASM})
|
||||
set(ASMSRC
|
||||
${PORTASM}
|
||||
lz4.S
|
||||
)
|
||||
|
||||
set(INCDIR ${CMAKE_CURRENT_BINARY_DIR} ${COMMON} ${PORTINC} ${KERNINC} ${TESTINC}
|
||||
${HALINC} ${PLATFORMINC} ${BOARDINC}
|
||||
${FATFSINC}
|
||||
${CHIBIOS}/os/various
|
||||
${HACKRF_PATH}/firmware/libopencm3/include
|
||||
${HACKRF_PATH}/firmware/common
|
||||
${HACKRF_PATH}/firmware
|
||||
ui
|
||||
hw
|
||||
apps
|
||||
@@ -366,7 +440,7 @@ set(CPPWARN "-Wall -Wextra -Wno-psabi")
|
||||
# List all default C defines here, like -D_DEBUG=1
|
||||
# TODO: Switch -DCRT0_INIT_DATA depending on load from RAM or SPIFI?
|
||||
# NOTE: _RANDOM_TCC to kill a GCC 4.9.3 error with std::max argument types
|
||||
set(DDEFS "-DLPC43XX -DLPC43XX_M0 -D__NEWLIB__ -DHACKRF_ONE -DTOOLCHAIN_GCC -DTOOLCHAIN_GCC_ARM -D_RANDOM_TCC=0 -D'VERSION_STRING=\"${VERSION}\"'")
|
||||
set(DDEFS "-DLPC43XX -DLPC43XX_M0 -D__NEWLIB__ -DHACKRF_ONE -DTOOLCHAIN_GCC -DTOOLCHAIN_GCC_ARM -D_RANDOM_TCC=0 -D'VERSION_STRING=\"${VERSION}\"' -DVERSION_MD5=${VERSION_MD5} -D'GCC_VERSION_MISMATCH=${GCC_VERSION_MISMATCH}'")
|
||||
|
||||
# List all default ASM defines here, like -D_DEBUG=1
|
||||
set(DADEFS)
|
||||
@@ -435,12 +509,14 @@ set_target_properties(${PROJECT_NAME}.elf PROPERTIES LINK_DEPENDS ${LDSCRIPT})
|
||||
add_definitions(${DEFS})
|
||||
include_directories(. ${INCDIR})
|
||||
link_directories(${LLIBDIR})
|
||||
target_link_libraries(${PROJECT_NAME}.elf ${LIBS})
|
||||
target_link_libraries(${PROJECT_NAME}.elf ${LIBS} "-L${CMAKE_CURRENT_LIST_DIR}/external")
|
||||
target_link_libraries(${PROJECT_NAME}.elf -Wl,-Map=${PROJECT_NAME}.map)
|
||||
target_link_libraries(${PROJECT_NAME}.elf -Wl,--print-memory-usage)
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT ${PROJECT_NAME}.bin
|
||||
COMMAND ${CMAKE_OBJCOPY} -O binary ${PROJECT_NAME}.elf ${PROJECT_NAME}.bin
|
||||
COMMAND ${CMAKE_OBJCOPY} -v -O binary ${PROJECT_NAME}.elf ${PROJECT_NAME}.bin --remove-section=.external_app_*
|
||||
COMMAND ${EXPORT_EXTERNAL_APP_IMAGES} ${PROJECT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_OBJCOPY} ${EXTAPPLIST}
|
||||
DEPENDS ${PROJECT_NAME}.elf
|
||||
)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2016 Furrtek
|
||||
* Copyright (C) 2022 Arjan Onwezen
|
||||
* Copyright (C) 2023 Kyle Reed
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
@@ -23,93 +24,278 @@
|
||||
|
||||
#include "app_settings.hpp"
|
||||
|
||||
#include "convert.hpp"
|
||||
#include "file.hpp"
|
||||
#include "file_reader.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include <cstring>
|
||||
#include "utility.hpp"
|
||||
#include "file_path.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <string_view>
|
||||
|
||||
namespace std {
|
||||
namespace fs = std::filesystem;
|
||||
using namespace portapack;
|
||||
|
||||
int app_settings::load(std::string application, AppSettings* settings) {
|
||||
namespace {
|
||||
fs::path get_settings_path(const std::string& app_name) {
|
||||
return settings_dir / app_name + u".ini";
|
||||
}
|
||||
} // namespace
|
||||
|
||||
if (portapack::persistent_memory::load_app_settings()) {
|
||||
file_path = folder+"/"+application+".ini";
|
||||
|
||||
auto error = settings_file.open(file_path);
|
||||
if (!error.is_valid()) {
|
||||
auto error = settings_file.read(file_content, std::min((int)settings_file.size(), MAX_FILE_CONTENT_SIZE));
|
||||
|
||||
settings->baseband_bandwidth=std::app_settings::read_long_long(file_content, "baseband_bandwidth=");
|
||||
settings->channel_bandwidth=std::app_settings::read_long_long(file_content, "channel_bandwidth=");
|
||||
settings->lna=std::app_settings::read_long_long(file_content, "lna=");
|
||||
settings->modulation=std::app_settings::read_long_long(file_content, "modulation=");
|
||||
settings->rx_amp=std::app_settings::read_long_long(file_content, "rx_amp=");
|
||||
settings->rx_frequency=std::app_settings::read_long_long(file_content, "rx_frequency=");
|
||||
settings->sampling_rate=std::app_settings::read_long_long(file_content, "sampling_rate=");
|
||||
settings->vga=std::app_settings::read_long_long(file_content, "vga=");
|
||||
settings->tx_amp=std::app_settings::read_long_long(file_content, "tx_amp=");
|
||||
settings->tx_frequency=std::app_settings::read_long_long(file_content, "tx_frequency=");
|
||||
settings->tx_gain=std::app_settings::read_long_long(file_content, "tx_gain=");
|
||||
settings->step=std::app_settings::read_long_long(file_content, "step=");
|
||||
settings->modulation=std::app_settings::read_long_long(file_content, "modulation=");
|
||||
settings->am_config_index=std::app_settings::read_long_long(file_content, "am_config_index=");
|
||||
settings->nbfm_config_index=std::app_settings::read_long_long(file_content, "nbfm_config_index=");
|
||||
settings->wfm_config_index=std::app_settings::read_long_long(file_content, "wfm_config_index=");
|
||||
settings->squelch=std::app_settings::read_long_long(file_content, "squelch=");
|
||||
|
||||
rc = SETTINGS_OK;
|
||||
}
|
||||
else rc = SETTINGS_UNABLE_TO_LOAD;
|
||||
}
|
||||
else rc = SETTINGS_DISABLED;
|
||||
return(rc);
|
||||
void BoundSetting::parse(std::string_view value) {
|
||||
switch (type_) {
|
||||
case SettingType::I64:
|
||||
parse_int(value, as<int64_t>());
|
||||
break;
|
||||
case SettingType::I32:
|
||||
parse_int(value, as<int32_t>());
|
||||
break;
|
||||
case SettingType::U32:
|
||||
parse_int(value, as<uint32_t>());
|
||||
break;
|
||||
case SettingType::U8:
|
||||
parse_int(value, as<uint8_t>());
|
||||
break;
|
||||
case SettingType::String:
|
||||
as<std::string>() = trim(value);
|
||||
break;
|
||||
case SettingType::Bool: {
|
||||
int parsed = 0;
|
||||
parse_int(value, parsed);
|
||||
as<bool>() = (parsed != 0);
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
int app_settings::save(std::string application, AppSettings* settings) {
|
||||
void BoundSetting::write(File& file) const {
|
||||
// NB: Write directly without allocations. This happens on every
|
||||
// app exit when enabled so should be fast to keep the UX responsive.
|
||||
StringFormatBuffer buffer;
|
||||
size_t length = 0;
|
||||
|
||||
if (portapack::persistent_memory::save_app_settings()) {
|
||||
file_path = folder+"/"+application+".ini";
|
||||
make_new_directory(folder);
|
||||
file.write(name_.data(), name_.length());
|
||||
file.write("=", 1);
|
||||
|
||||
auto error = settings_file.create(file_path);
|
||||
if (!error.is_valid()) {
|
||||
// Save common setting
|
||||
settings_file.write_line("baseband_bandwidth="+to_string_dec_uint(portapack::receiver_model.baseband_bandwidth()));
|
||||
settings_file.write_line("channel_bandwidth="+to_string_dec_uint(portapack::transmitter_model.channel_bandwidth()));
|
||||
settings_file.write_line("lna="+to_string_dec_uint(portapack::receiver_model.lna()));
|
||||
settings_file.write_line("rx_amp="+to_string_dec_uint(portapack::receiver_model.rf_amp()));
|
||||
settings_file.write_line("sampling_rate="+to_string_dec_uint(portapack::receiver_model.sampling_rate()));
|
||||
settings_file.write_line("tx_amp="+to_string_dec_uint(portapack::transmitter_model.rf_amp()));
|
||||
settings_file.write_line("tx_gain="+to_string_dec_uint(portapack::transmitter_model.tx_gain()));
|
||||
settings_file.write_line("vga="+to_string_dec_uint(portapack::receiver_model.vga()));
|
||||
// Save other settings from struct
|
||||
settings_file.write_line("rx_frequency="+to_string_dec_uint(settings->rx_frequency));
|
||||
settings_file.write_line("tx_frequency="+to_string_dec_uint(settings->tx_frequency));
|
||||
settings_file.write_line("step="+to_string_dec_uint(settings->step));
|
||||
settings_file.write_line("modulation="+to_string_dec_uint(settings->modulation));
|
||||
settings_file.write_line("am_config_index="+to_string_dec_uint(settings->am_config_index));
|
||||
settings_file.write_line("nbfm_config_index="+to_string_dec_uint(settings->nbfm_config_index));
|
||||
settings_file.write_line("wfm_config_index="+to_string_dec_uint(settings->wfm_config_index));
|
||||
settings_file.write_line("squelch="+to_string_dec_uint(settings->squelch));
|
||||
switch (type_) {
|
||||
case SettingType::I64:
|
||||
file.write(to_string_dec_int(as<int64_t>(), buffer, length), length);
|
||||
break;
|
||||
case SettingType::I32:
|
||||
file.write(to_string_dec_int(as<int32_t>(), buffer, length), length);
|
||||
break;
|
||||
case SettingType::U32:
|
||||
file.write(to_string_dec_uint(as<uint32_t>(), buffer, length), length);
|
||||
break;
|
||||
case SettingType::U8:
|
||||
file.write(to_string_dec_uint(as<uint8_t>(), buffer, length), length);
|
||||
break;
|
||||
case SettingType::String: {
|
||||
const auto& str = as<std::string>();
|
||||
file.write(str.data(), str.length());
|
||||
break;
|
||||
}
|
||||
case SettingType::Bool:
|
||||
file.write(as<bool>() ? "1" : "0", 1);
|
||||
break;
|
||||
}
|
||||
|
||||
rc = SETTINGS_OK;
|
||||
}
|
||||
else rc = SETTINGS_UNABLE_TO_SAVE;
|
||||
}
|
||||
else rc = SETTINGS_DISABLED;
|
||||
return(rc);
|
||||
file.write("\r\n", 2);
|
||||
}
|
||||
|
||||
|
||||
long long int app_settings::read_long_long(char* file_content, const char* setting_text) {
|
||||
auto position = strstr(file_content, (char *)setting_text);
|
||||
if (position) {
|
||||
position += strlen((char *)setting_text);
|
||||
setting_value = strtoll(position, nullptr, 10);
|
||||
}
|
||||
return(setting_value);
|
||||
SettingsStore::SettingsStore(std::string_view store_name, SettingBindings bindings)
|
||||
: store_name_{store_name}, bindings_{bindings} {
|
||||
reload();
|
||||
}
|
||||
|
||||
SettingsStore::~SettingsStore() {
|
||||
save();
|
||||
}
|
||||
|
||||
} /* namespace std */
|
||||
void SettingsStore::reload() {
|
||||
load_settings(store_name_, bindings_);
|
||||
}
|
||||
|
||||
void SettingsStore::save() const {
|
||||
save_settings(store_name_, bindings_);
|
||||
}
|
||||
|
||||
bool load_settings(std::string_view store_name, SettingBindings& bindings) {
|
||||
File f;
|
||||
auto path = get_settings_path(std::string{store_name});
|
||||
|
||||
auto error = f.open(path);
|
||||
if (error)
|
||||
return false;
|
||||
|
||||
auto reader = FileLineReader(f);
|
||||
for (const auto& line : reader) {
|
||||
auto cols = split_string(line, '=');
|
||||
|
||||
if (cols.size() != 2)
|
||||
continue;
|
||||
|
||||
// Find a binding with the name.
|
||||
auto it = std::find_if(
|
||||
bindings.begin(), bindings.end(),
|
||||
[name = cols[0]](auto& bound_setting) {
|
||||
return name == bound_setting.name();
|
||||
});
|
||||
|
||||
// If found, parse the value.
|
||||
if (it != bindings.end())
|
||||
it->parse(cols[1]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool save_settings(std::string_view store_name, const SettingBindings& bindings) {
|
||||
File f;
|
||||
auto path = get_settings_path(std::string{store_name});
|
||||
|
||||
ensure_directory(settings_dir);
|
||||
auto error = f.create(path);
|
||||
if (error)
|
||||
return false;
|
||||
|
||||
for (const auto& bound_setting : bindings)
|
||||
bound_setting.write(f);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
namespace app_settings {
|
||||
|
||||
void copy_to_radio_model(const AppSettings& settings) {
|
||||
// NB: Don't actually adjust the radio here or it may hang.
|
||||
// Specifically 'modulation' which requires a running baseband.
|
||||
|
||||
if (flags_enabled(settings.mode, Mode::TX)) {
|
||||
persistent_memory::set_target_frequency(settings.tx_frequency);
|
||||
transmitter_model.configure_from_app_settings(settings);
|
||||
}
|
||||
|
||||
if (flags_enabled(settings.mode, Mode::RX)) {
|
||||
persistent_memory::set_target_frequency(settings.rx_frequency);
|
||||
receiver_model.configure_from_app_settings(settings);
|
||||
receiver_model.set_configuration_without_update(
|
||||
static_cast<ReceiverModel::Mode>(settings.modulation),
|
||||
settings.step,
|
||||
settings.am_config_index,
|
||||
settings.nbfm_config_index,
|
||||
settings.wfm_config_index,
|
||||
settings.squelch);
|
||||
}
|
||||
|
||||
receiver_model.set_frequency_step(settings.step);
|
||||
receiver_model.set_normalized_headphone_volume(settings.volume);
|
||||
}
|
||||
|
||||
void copy_from_radio_model(AppSettings& settings) {
|
||||
if (flags_enabled(settings.mode, Mode::TX)) {
|
||||
settings.tx_frequency = transmitter_model.target_frequency();
|
||||
settings.baseband_bandwidth = transmitter_model.baseband_bandwidth();
|
||||
settings.sampling_rate = transmitter_model.sampling_rate();
|
||||
settings.tx_amp = transmitter_model.rf_amp();
|
||||
settings.tx_gain = transmitter_model.tx_gain();
|
||||
settings.channel_bandwidth = transmitter_model.channel_bandwidth();
|
||||
}
|
||||
|
||||
if (flags_enabled(settings.mode, Mode::RX)) {
|
||||
settings.rx_frequency = receiver_model.target_frequency();
|
||||
settings.baseband_bandwidth = receiver_model.baseband_bandwidth();
|
||||
settings.sampling_rate = receiver_model.sampling_rate();
|
||||
settings.lna = receiver_model.lna();
|
||||
settings.vga = receiver_model.vga();
|
||||
settings.rx_amp = receiver_model.rf_amp();
|
||||
settings.squelch = receiver_model.squelch_level();
|
||||
|
||||
settings.modulation = static_cast<uint8_t>(receiver_model.modulation());
|
||||
settings.am_config_index = receiver_model.am_configuration();
|
||||
settings.nbfm_config_index = receiver_model.nbfm_configuration();
|
||||
settings.wfm_config_index = receiver_model.wfm_configuration();
|
||||
}
|
||||
|
||||
settings.step = receiver_model.frequency_step();
|
||||
settings.volume = receiver_model.normalized_headphone_volume();
|
||||
}
|
||||
|
||||
/* SettingsManager *************************************************/
|
||||
SettingsManager::SettingsManager(std::string_view app_name, Mode mode, Options options)
|
||||
: SettingsManager{app_name, mode, options, {}} {}
|
||||
|
||||
SettingsManager::SettingsManager(
|
||||
std::string_view app_name,
|
||||
Mode mode,
|
||||
SettingBindings additional_settings)
|
||||
: SettingsManager{app_name, mode, Options::None, std::move(additional_settings)} {}
|
||||
|
||||
SettingsManager::SettingsManager(
|
||||
std::string_view app_name,
|
||||
Mode mode,
|
||||
Options options,
|
||||
SettingBindings additional_settings)
|
||||
: app_name_{app_name},
|
||||
settings_{},
|
||||
bindings_{},
|
||||
loaded_{false} {
|
||||
settings_.mode = mode;
|
||||
settings_.options = options;
|
||||
|
||||
// Pre-alloc enough for app settings and additional settings.
|
||||
additional_settings.reserve(COMMON_APP_SETTINGS_COUNT + additional_settings.size());
|
||||
bindings_ = std::move(additional_settings);
|
||||
|
||||
// Settings should always be loaded because apps now rely
|
||||
// on being able to store UI settings, config, etc.
|
||||
|
||||
// Transmitter model settings.
|
||||
if (flags_enabled(mode, Mode::TX)) {
|
||||
bindings_.emplace_back("tx_frequency"sv, &settings_.tx_frequency);
|
||||
bindings_.emplace_back("tx_amp"sv, &settings_.tx_amp);
|
||||
bindings_.emplace_back("tx_gain"sv, &settings_.tx_gain);
|
||||
bindings_.emplace_back("channel_bandwidth"sv, &settings_.channel_bandwidth);
|
||||
}
|
||||
|
||||
// Receiver model settings.
|
||||
if (flags_enabled(mode, Mode::RX)) {
|
||||
bindings_.emplace_back("rx_frequency"sv, &settings_.rx_frequency);
|
||||
bindings_.emplace_back("lna"sv, &settings_.lna);
|
||||
bindings_.emplace_back("vga"sv, &settings_.vga);
|
||||
bindings_.emplace_back("rx_amp"sv, &settings_.rx_amp);
|
||||
bindings_.emplace_back("modulation"sv, &settings_.modulation);
|
||||
bindings_.emplace_back("am_config_index"sv, &settings_.am_config_index);
|
||||
bindings_.emplace_back("nbfm_config_index"sv, &settings_.nbfm_config_index);
|
||||
bindings_.emplace_back("wfm_config_index"sv, &settings_.wfm_config_index);
|
||||
bindings_.emplace_back("squelch"sv, &settings_.squelch);
|
||||
}
|
||||
|
||||
// Common model settings.
|
||||
bindings_.emplace_back("baseband_bandwidth"sv, &settings_.baseband_bandwidth);
|
||||
bindings_.emplace_back("sampling_rate"sv, &settings_.sampling_rate);
|
||||
bindings_.emplace_back("step"sv, &settings_.step);
|
||||
bindings_.emplace_back("volume"sv, &settings_.volume);
|
||||
|
||||
// RadioState should have initialized default radio parameters before this function;
|
||||
// copy them to settings_ before checking settings .ini file (in case the file doesn't exist
|
||||
// or doesn't include all parameters). Settings in the file can overwrite all, or a subset of parameters.
|
||||
copy_from_radio_model(settings_);
|
||||
|
||||
loaded_ = load_settings(app_name_, bindings_);
|
||||
|
||||
// Only copy to the radio if load was successful.
|
||||
if (loaded_)
|
||||
copy_to_radio_model(settings_);
|
||||
}
|
||||
|
||||
SettingsManager::~SettingsManager() {
|
||||
copy_from_radio_model(settings_);
|
||||
|
||||
save_settings(app_name_, bindings_);
|
||||
}
|
||||
|
||||
} // namespace app_settings
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2016 Furrtek
|
||||
* Copyright (C) 2022 Arjan Onwezen
|
||||
* Copyright (C) 2023 Kyle Reed
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
@@ -24,69 +25,165 @@
|
||||
#ifndef __APP_SETTINGS_H__
|
||||
#define __APP_SETTINGS_H__
|
||||
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
|
||||
#include "file.hpp"
|
||||
#include "max283x.hpp"
|
||||
#include "string_format.hpp"
|
||||
|
||||
// Bring in the string_view literal.
|
||||
using std::literals::operator""sv;
|
||||
|
||||
namespace std {
|
||||
class app_settings {
|
||||
#define COMMON_APP_SETTINGS_COUNT 19
|
||||
|
||||
/* Represents a named setting bound to a variable instance. */
|
||||
/* Using void* instead of std::variant, because variant is a pain to dispatch over. */
|
||||
class BoundSetting {
|
||||
/* The type of bound setting. */
|
||||
enum class SettingType : uint8_t {
|
||||
I64,
|
||||
I32,
|
||||
U32,
|
||||
U8,
|
||||
String,
|
||||
Bool,
|
||||
};
|
||||
|
||||
public:
|
||||
BoundSetting(std::string_view name, int64_t* target)
|
||||
: name_{name}, target_{target}, type_{SettingType::I64} {}
|
||||
|
||||
public:
|
||||
BoundSetting(std::string_view name, int32_t* target)
|
||||
: name_{name}, target_{target}, type_{SettingType::I32} {}
|
||||
|
||||
#define SETTINGS_OK 0 // settings found
|
||||
#define SETTINGS_UNABLE_TO_LOAD -1 // settings (file) not found
|
||||
#define SETTINGS_UNABLE_TO_SAVE -2 // unable to save settings
|
||||
#define SETTINGS_DISABLED -3 // load/save settings disabled in settings
|
||||
BoundSetting(std::string_view name, uint32_t* target)
|
||||
: name_{name}, target_{target}, type_{SettingType::U32} {}
|
||||
|
||||
BoundSetting(std::string_view name, uint8_t* target)
|
||||
: name_{name}, target_{target}, type_{SettingType::U8} {}
|
||||
|
||||
BoundSetting(std::string_view name, std::string* target)
|
||||
: name_{name}, target_{target}, type_{SettingType::String} {}
|
||||
|
||||
struct AppSettings {
|
||||
uint32_t baseband_bandwidth;
|
||||
uint32_t channel_bandwidth;
|
||||
uint8_t lna;
|
||||
uint8_t modulation;
|
||||
uint8_t rx_amp;
|
||||
uint32_t rx_frequency;
|
||||
uint32_t sampling_rate;
|
||||
uint8_t tx_amp;
|
||||
uint32_t tx_frequency;
|
||||
uint8_t tx_gain;
|
||||
uint8_t vga;
|
||||
uint32_t step;
|
||||
uint8_t am_config_index;
|
||||
uint8_t nbfm_config_index;
|
||||
uint8_t wfm_config_index;
|
||||
uint8_t squelch;
|
||||
};
|
||||
BoundSetting(std::string_view name, bool* target)
|
||||
: name_{name}, target_{target}, type_{SettingType::Bool} {}
|
||||
|
||||
int load(std::string application, AppSettings* settings);
|
||||
int save(std::string application, AppSettings* settings);
|
||||
std::string_view name() const { return name_; }
|
||||
void parse(std::string_view value);
|
||||
void write(File& file) const;
|
||||
|
||||
private:
|
||||
template <typename T>
|
||||
constexpr auto& as() const {
|
||||
return *reinterpret_cast<T*>(target_);
|
||||
}
|
||||
|
||||
private:
|
||||
std::string_view name_;
|
||||
void* target_;
|
||||
SettingType type_;
|
||||
};
|
||||
|
||||
#define MAX_FILE_CONTENT_SIZE 1000
|
||||
using SettingBindings = std::vector<BoundSetting>;
|
||||
|
||||
char file_content[MAX_FILE_CONTENT_SIZE] = {};
|
||||
std::string file_path = "";
|
||||
std::string folder = "SETTINGS";
|
||||
int rc = SETTINGS_OK;
|
||||
File settings_file { };
|
||||
long long int setting_value {} ;
|
||||
/* RAII wrapper for Settings that loads/saves to the SD card. */
|
||||
class SettingsStore {
|
||||
public:
|
||||
SettingsStore(std::string_view store_name, SettingBindings bindings);
|
||||
~SettingsStore();
|
||||
|
||||
long long int read_long_long(char* file_content, const char* setting_text);
|
||||
void reload();
|
||||
void save() const;
|
||||
|
||||
private:
|
||||
std::string_view store_name_;
|
||||
SettingBindings bindings_;
|
||||
};
|
||||
|
||||
}; // class app_settings
|
||||
} // namespace std
|
||||
bool load_settings(std::string_view store_name, SettingBindings& bindings);
|
||||
bool save_settings(std::string_view store_name, const SettingBindings& bindings);
|
||||
|
||||
namespace app_settings {
|
||||
|
||||
enum class Mode : uint8_t {
|
||||
NO_RF = 0x00,
|
||||
RX = 0x01,
|
||||
TX = 0x02,
|
||||
RX_TX = 0x03, // Both TX/RX
|
||||
};
|
||||
|
||||
#endif/*__APP_SETTINGS_H__*/
|
||||
enum class Options {
|
||||
None = 0x0000,
|
||||
};
|
||||
|
||||
/* NB: See RX/TX model headers for default values. */
|
||||
struct AppSettings {
|
||||
Mode mode = Mode::RX;
|
||||
Options options = Options::None;
|
||||
uint32_t baseband_bandwidth = max283x::filter::bandwidth_minimum;
|
||||
uint32_t sampling_rate = 3072000; // Good for 48k audio.
|
||||
uint8_t lna = 32;
|
||||
uint8_t vga = 32;
|
||||
uint8_t rx_amp = 0;
|
||||
uint8_t tx_amp = 0;
|
||||
uint8_t tx_gain = 35;
|
||||
uint32_t channel_bandwidth = 1;
|
||||
uint32_t rx_frequency;
|
||||
uint32_t tx_frequency;
|
||||
uint32_t step = 25000;
|
||||
uint8_t modulation = 1; // NFM
|
||||
uint8_t am_config_index = 0;
|
||||
uint8_t nbfm_config_index = 0;
|
||||
uint8_t wfm_config_index = 0;
|
||||
uint8_t squelch = 80;
|
||||
uint8_t volume;
|
||||
// NOTE: update COMMON_APP_SETTINGS_COUNT when adding to this
|
||||
};
|
||||
|
||||
/* Copies common values to the receiver/transmitter models. */
|
||||
void copy_to_radio_model(const AppSettings& settings);
|
||||
|
||||
/* Copies common values from the receiver/transmitter models. */
|
||||
void copy_from_radio_model(AppSettings& settings);
|
||||
|
||||
/* RAII wrapper for automatically loading and saving settings for an app.
|
||||
* "Additional" settings are always loaded, but radio settings are conditionally
|
||||
* saved/loaded if the global app settings options are enabled.
|
||||
* NB: This should be added to a class before any LNA/VGA controls so that
|
||||
* the receiver/transmitter models are set before the control ctors run. */
|
||||
class SettingsManager {
|
||||
public:
|
||||
SettingsManager(std::string_view app_name, Mode mode, Options options = Options::None);
|
||||
SettingsManager(std::string_view app_name, Mode mode, SettingBindings additional_settings);
|
||||
SettingsManager(std::string_view app_name, Mode mode, Options options, SettingBindings additional_settings);
|
||||
~SettingsManager();
|
||||
|
||||
SettingsManager(const SettingsManager&) = delete;
|
||||
SettingsManager& operator=(const SettingsManager&) = delete;
|
||||
SettingsManager(SettingsManager&&) = delete;
|
||||
SettingsManager& operator=(SettingsManager&&) = delete;
|
||||
|
||||
/* True if settings were successfully loaded from file. */
|
||||
bool loaded() const { return loaded_; }
|
||||
|
||||
Mode mode() const { return settings_.mode; }
|
||||
|
||||
AppSettings& raw() { return settings_; }
|
||||
|
||||
private:
|
||||
std::string_view app_name_;
|
||||
AppSettings settings_;
|
||||
SettingBindings bindings_;
|
||||
bool loaded_;
|
||||
};
|
||||
|
||||
} // namespace app_settings
|
||||
|
||||
ENABLE_FLAGS_OPERATORS(app_settings::Mode);
|
||||
ENABLE_FLAGS_OPERATORS(app_settings::Options);
|
||||
|
||||
#endif /*__APP_SETTINGS_H__*/
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
#include "baseband_api.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include "file_path.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
using namespace acars;
|
||||
@@ -32,118 +33,86 @@ using namespace acars;
|
||||
#include "utility.hpp"
|
||||
|
||||
void ACARSLogger::log_raw_data(const acars::Packet& packet, const uint32_t frequency) {
|
||||
(void)frequency;
|
||||
std::string entry { }; //= "Raw: F:" + to_string_dec_uint(frequency) + "Hz ";
|
||||
entry.reserve(256);
|
||||
|
||||
// Raw hex dump of all the bytes
|
||||
//for (size_t c = 0; c < packet.length(); c += 32)
|
||||
// entry += to_string_hex(packet.read(c, 32), 8) + " ";
|
||||
|
||||
for (size_t c = 0; c < 256; c += 32)
|
||||
entry += to_string_bin(packet.read(c, 32), 32);
|
||||
|
||||
log_file.write_entry(packet.received_at(), entry);
|
||||
(void)frequency;
|
||||
std::string entry{}; //= "Raw: F:" + to_string_dec_uint(frequency) + "Hz ";
|
||||
entry.reserve(256);
|
||||
|
||||
// Raw hex dump of all the bytes
|
||||
// for (size_t c = 0; c < packet.length(); c += 32)
|
||||
// entry += to_string_hex(packet.read(c, 32), 8) + " ";
|
||||
|
||||
for (size_t c = 0; c < 256; c += 32)
|
||||
entry += to_string_bin(packet.read(c, 32), 32);
|
||||
|
||||
log_file.write_entry(packet.received_at(), entry);
|
||||
}
|
||||
|
||||
/*void ACARSLogger::log_decoded(
|
||||
const acars::Packet& packet,
|
||||
const std::string text) {
|
||||
|
||||
log_file.write_entry(packet.timestamp(), text);
|
||||
const acars::Packet& packet,
|
||||
const std::string text) {
|
||||
|
||||
log_file.write_entry(packet.timestamp(), text);
|
||||
}*/
|
||||
|
||||
namespace ui {
|
||||
|
||||
void ACARSAppView::update_freq(rf::Frequency f) {
|
||||
set_target_frequency(f);
|
||||
portapack::persistent_memory::set_tuned_frequency(f); // Maybe not ?
|
||||
}
|
||||
ACARSAppView::ACARSAppView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_acars);
|
||||
|
||||
ACARSAppView::ACARSAppView(NavigationView& nav) {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_acars);
|
||||
add_children({&rssi,
|
||||
&channel,
|
||||
&field_rf_amp,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&field_frequency,
|
||||
&check_log,
|
||||
&console});
|
||||
|
||||
add_children({
|
||||
&rssi,
|
||||
&channel,
|
||||
&field_rf_amp,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&field_frequency,
|
||||
&check_log,
|
||||
&console
|
||||
});
|
||||
|
||||
receiver_model.set_sampling_rate(2457600);
|
||||
receiver_model.set_baseband_bandwidth(1750000);
|
||||
receiver_model.enable();
|
||||
|
||||
field_frequency.set_value(receiver_model.tuning_frequency());
|
||||
update_freq(receiver_model.tuning_frequency());
|
||||
field_frequency.set_step(receiver_model.frequency_step());
|
||||
field_frequency.on_change = [this](rf::Frequency f) {
|
||||
update_freq(f);
|
||||
};
|
||||
field_frequency.on_edit = [this, &nav]() {
|
||||
// TODO: Provide separate modal method/scheme?
|
||||
auto new_view = nav.push<FrequencyKeypadView>(receiver_model.tuning_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
update_freq(f);
|
||||
field_frequency.set_value(f);
|
||||
};
|
||||
};
|
||||
|
||||
check_log.set_value(logging);
|
||||
check_log.on_select = [this](Checkbox&, bool v) {
|
||||
logging = v;
|
||||
};
|
||||
|
||||
logger = std::make_unique<ACARSLogger>();
|
||||
if (logger)
|
||||
logger->append("acars.txt");
|
||||
receiver_model.enable();
|
||||
|
||||
check_log.set_value(logging);
|
||||
check_log.on_select = [this](Checkbox&, bool v) {
|
||||
logging = v;
|
||||
};
|
||||
|
||||
logger = std::make_unique<ACARSLogger>();
|
||||
if (logger)
|
||||
logger->append(logs_dir / u"ACARS.TXT");
|
||||
}
|
||||
|
||||
ACARSAppView::~ACARSAppView() {
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
void ACARSAppView::focus() {
|
||||
field_frequency.focus();
|
||||
field_frequency.focus();
|
||||
}
|
||||
|
||||
void ACARSAppView::on_packet(const acars::Packet& packet) {
|
||||
std::string console_info;
|
||||
|
||||
/*if (!packet.is_valid()) {
|
||||
console_info = to_string_datetime(packet.received_at(), HMS);
|
||||
console_info += " INVALID";
|
||||
|
||||
console.writeln(console_info);
|
||||
} else {
|
||||
console_info = to_string_datetime(packet.received_at(), HMS);
|
||||
console_info += ":" + to_string_bin(packet.read(0, 32), 32);
|
||||
//console_info += " REG:" + packet.registration_number();
|
||||
|
||||
console.writeln(console_info);
|
||||
}*/
|
||||
|
||||
packet_counter++;
|
||||
if (packet_counter % 10 == 0)
|
||||
console.writeln("Block #" + to_string_dec_uint(packet_counter));
|
||||
|
||||
// Log raw data whatever it contains
|
||||
if (logger && logging)
|
||||
logger->log_raw_data(packet, target_frequency());
|
||||
}
|
||||
std::string console_info;
|
||||
|
||||
void ACARSAppView::set_target_frequency(const uint32_t new_value) {
|
||||
target_frequency_ = new_value;
|
||||
receiver_model.set_tuning_frequency(new_value);
|
||||
}
|
||||
/*if (!packet.is_valid()) {
|
||||
console_info = to_string_datetime(packet.received_at(), HMS);
|
||||
console_info += " INVALID";
|
||||
|
||||
uint32_t ACARSAppView::target_frequency() const {
|
||||
return target_frequency_;
|
||||
console.writeln(console_info);
|
||||
} else {
|
||||
console_info = to_string_datetime(packet.received_at(), HMS);
|
||||
console_info += ":" + to_string_bin(packet.read(0, 32), 32);
|
||||
//console_info += " REG:" + packet.registration_number();
|
||||
|
||||
console.writeln(console_info);
|
||||
}*/
|
||||
|
||||
packet_counter++;
|
||||
if (packet_counter % 10 == 0)
|
||||
console.writeln("Block #" + to_string_dec_uint(packet_counter));
|
||||
|
||||
// Log raw data whatever it contains
|
||||
if (logger && logging)
|
||||
logger->log_raw_data(packet, receiver_model.target_frequency());
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -23,94 +23,89 @@
|
||||
#ifndef __ACARS_APP_H__
|
||||
#define __ACARS_APP_H__
|
||||
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "ui_rssi.hpp"
|
||||
|
||||
#include "log_file.hpp"
|
||||
|
||||
#include "acars_packet.hpp"
|
||||
|
||||
class ACARSLogger {
|
||||
public:
|
||||
Optional<File::Error> append(const std::string& filename) {
|
||||
return log_file.append(filename);
|
||||
}
|
||||
|
||||
void log_raw_data(const acars::Packet& packet, const uint32_t frequency);
|
||||
//void log_decoded(const acars::Packet& packet, const std::string text);
|
||||
public:
|
||||
Optional<File::Error> append(const std::filesystem::path& filename) {
|
||||
return log_file.append(filename);
|
||||
}
|
||||
|
||||
private:
|
||||
LogFile log_file { };
|
||||
void log_raw_data(const acars::Packet& packet, const uint32_t frequency);
|
||||
// void log_decoded(const acars::Packet& packet, const std::string text);
|
||||
|
||||
private:
|
||||
LogFile log_file{};
|
||||
};
|
||||
|
||||
namespace ui {
|
||||
|
||||
class ACARSAppView : public View {
|
||||
public:
|
||||
ACARSAppView(NavigationView& nav);
|
||||
~ACARSAppView();
|
||||
public:
|
||||
ACARSAppView(NavigationView& nav);
|
||||
~ACARSAppView();
|
||||
|
||||
void focus() override;
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "ACARS (WIP)"; };
|
||||
std::string title() const override { return "ACARS (WIP)"; };
|
||||
|
||||
private:
|
||||
bool logging { false };
|
||||
uint32_t packet_counter { 0 };
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
RxRadioState radio_state_{
|
||||
131550000 /* frequency */,
|
||||
1750000 /* bandwidth */,
|
||||
2457600 /* sampling rate */
|
||||
};
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_acars", app_settings::Mode::RX};
|
||||
|
||||
RFAmpField field_rf_amp {
|
||||
{ 13 * 8, 0 * 16 }
|
||||
};
|
||||
LNAGainField field_lna {
|
||||
{ 15 * 8, 0 * 16 }
|
||||
};
|
||||
VGAGainField field_vga {
|
||||
{ 18 * 8, 0 * 16 }
|
||||
};
|
||||
RSSI rssi {
|
||||
{ 21 * 8, 0, 6 * 8, 4 },
|
||||
};
|
||||
Channel channel {
|
||||
{ 21 * 8, 5, 6 * 8, 4 },
|
||||
};
|
||||
|
||||
FrequencyField field_frequency {
|
||||
{ 0 * 8, 0 * 8 },
|
||||
};
|
||||
Checkbox check_log {
|
||||
{ 22 * 8, 21 },
|
||||
3,
|
||||
"LOG",
|
||||
true
|
||||
};
|
||||
bool logging{false};
|
||||
uint32_t packet_counter{0};
|
||||
|
||||
Console console {
|
||||
{ 0, 3 * 16, 240, 256 }
|
||||
};
|
||||
RFAmpField field_rf_amp{
|
||||
{13 * 8, 0 * 16}};
|
||||
LNAGainField field_lna{
|
||||
{15 * 8, 0 * 16}};
|
||||
VGAGainField field_vga{
|
||||
{18 * 8, 0 * 16}};
|
||||
RSSI rssi{
|
||||
{21 * 8, 0, 6 * 8, 4}};
|
||||
Channel channel{
|
||||
{21 * 8, 5, 6 * 8, 4}};
|
||||
|
||||
std::unique_ptr<ACARSLogger> logger { };
|
||||
RxFrequencyField field_frequency{
|
||||
{0 * 8, 0 * 8},
|
||||
nav_};
|
||||
Checkbox check_log{
|
||||
{22 * 8, 21},
|
||||
3,
|
||||
"LOG",
|
||||
true};
|
||||
|
||||
uint32_t target_frequency_ { };
|
||||
|
||||
void update_freq(rf::Frequency f);
|
||||
Console console{
|
||||
{0, 3 * 16, 240, 256}};
|
||||
|
||||
void on_packet(const acars::Packet& packet);
|
||||
std::unique_ptr<ACARSLogger> logger{};
|
||||
|
||||
uint32_t target_frequency() const;
|
||||
void set_target_frequency(const uint32_t new_value);
|
||||
|
||||
MessageHandlerRegistration message_handler_packet {
|
||||
Message::ID::ACARSPacket,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const ACARSPacketMessage*>(p);
|
||||
const acars::Packet packet { message->packet };
|
||||
this->on_packet(packet);
|
||||
}
|
||||
};
|
||||
void on_packet(const acars::Packet& packet);
|
||||
|
||||
MessageHandlerRegistration message_handler_packet{
|
||||
Message::ID::ACARSPacket,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const ACARSPacketMessage*>(p);
|
||||
const acars::Packet packet{message->packet};
|
||||
this->on_packet(packet);
|
||||
}};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
#endif/*__ACARS_APP_H__*/
|
||||
#endif /*__ACARS_APP_H__*/
|
||||
|
||||
@@ -20,9 +20,11 @@
|
||||
*/
|
||||
|
||||
#include "ais_app.hpp"
|
||||
#include "audio.hpp"
|
||||
|
||||
#include "string_format.hpp"
|
||||
#include "database.hpp"
|
||||
#include "file_path.hpp"
|
||||
|
||||
#include "baseband_api.hpp"
|
||||
|
||||
@@ -31,444 +33,434 @@ using namespace portapack;
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace pmem = portapack::persistent_memory;
|
||||
|
||||
namespace ais {
|
||||
namespace format {
|
||||
|
||||
static std::string latlon_abs_normalized(const int32_t normalized, const char suffixes[2]) {
|
||||
const auto suffix = suffixes[(normalized < 0) ? 0 : 1];
|
||||
const uint32_t normalized_abs = std::abs(normalized);
|
||||
const uint32_t t = (normalized_abs * 5) / 3;
|
||||
const uint32_t degrees = t / (100 * 10000);
|
||||
const uint32_t fraction = t % (100 * 10000);
|
||||
return to_string_dec_uint(degrees) + "." + to_string_dec_uint(fraction, 6, '0') + suffix;
|
||||
const auto suffix = suffixes[(normalized < 0) ? 0 : 1];
|
||||
const uint32_t normalized_abs = std::abs(normalized);
|
||||
const uint32_t t = (normalized_abs * 5) / 3;
|
||||
const uint32_t degrees = t / (100 * 10000);
|
||||
const uint32_t fraction = t % (100 * 10000);
|
||||
return to_string_dec_uint(degrees) + "." + to_string_dec_uint(fraction, 6, '0') + suffix;
|
||||
}
|
||||
|
||||
static std::string latlon(const Latitude latitude, const Longitude longitude) {
|
||||
if( latitude.is_valid() && longitude.is_valid() ) {
|
||||
return latlon_abs_normalized(latitude.normalized(), "SN") + " " + latlon_abs_normalized(longitude.normalized(), "WE");
|
||||
} else if( latitude.is_not_available() && longitude.is_not_available() ) {
|
||||
return "not available";
|
||||
} else {
|
||||
return "invalid";
|
||||
}
|
||||
if (latitude.is_valid() && longitude.is_valid()) {
|
||||
return latlon_abs_normalized(latitude.normalized(), "SN") + " " + latlon_abs_normalized(longitude.normalized(), "WE");
|
||||
} else if (latitude.is_not_available() && longitude.is_not_available()) {
|
||||
return "not available";
|
||||
} else {
|
||||
return "invalid";
|
||||
}
|
||||
}
|
||||
|
||||
static float latlon_float(const int32_t normalized) {
|
||||
return ((((float) normalized) * 5) / 3) / (100 * 10000);
|
||||
return ((((float)normalized) * 5) / 3) / (100 * 10000);
|
||||
}
|
||||
|
||||
static std::string mmsi(
|
||||
const ais::MMSI& mmsi
|
||||
) {
|
||||
return to_string_dec_uint(mmsi, 9, '0'); // MMSI is always is always 9 characters pre-padded with zeros
|
||||
const ais::MMSI& mmsi) {
|
||||
return to_string_dec_uint(mmsi, 9, '0'); // MMSI is always is always 9 characters pre-padded with zeros
|
||||
}
|
||||
|
||||
|
||||
static std::string mid(
|
||||
const ais::MMSI& mmsi
|
||||
) {
|
||||
std::database db;
|
||||
std::string mid_code = "";
|
||||
std::database::MidDBRecord mid_record = {};
|
||||
int return_code = 0;
|
||||
const ais::MMSI& mmsi) {
|
||||
database db;
|
||||
std::string mid_code = "";
|
||||
database::MidDBRecord mid_record = {};
|
||||
int return_code = 0;
|
||||
|
||||
// Try getting the country name from mids.db using MID code for given MMSI
|
||||
mid_code = to_string_dec_uint(mmsi, 9, ' ').substr(0, 3);
|
||||
return_code = db.retrieve_mid_record(&mid_record, mid_code);
|
||||
switch(return_code) {
|
||||
case DATABASE_RECORD_FOUND: return mid_record.country;
|
||||
case DATABASE_NOT_FOUND: return "No mids.db file";
|
||||
default: return "";
|
||||
}
|
||||
// Try getting the country name from mids.db using MID code for given MMSI
|
||||
mid_code = to_string_dec_uint(mmsi, 9, ' ').substr(0, 3);
|
||||
return_code = db.retrieve_mid_record(&mid_record, mid_code);
|
||||
switch (return_code) {
|
||||
case DATABASE_RECORD_FOUND:
|
||||
return mid_record.country;
|
||||
case DATABASE_NOT_FOUND:
|
||||
return "No mids.db file";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
static std::string text(const std::string &text) {
|
||||
size_t end = text.find_last_not_of("@");
|
||||
static std::string text(const std::string& text) {
|
||||
size_t end = text.find_last_not_of("@");
|
||||
return (end == std::string::npos) ? "" : text.substr(0, end + 1);
|
||||
|
||||
}
|
||||
|
||||
static std::string navigational_status(const unsigned int value) {
|
||||
switch(value) {
|
||||
case 0: return "under way w/engine";
|
||||
case 1: return "at anchor";
|
||||
case 2: return "not under command";
|
||||
case 3: return "restricted maneuv";
|
||||
case 4: return "constrained draught";
|
||||
case 5: return "moored";
|
||||
case 6: return "aground";
|
||||
case 7: return "fishing";
|
||||
case 8: return "sailing";
|
||||
case 9: case 10: case 13: return "reserved";
|
||||
case 11: return "towing astern";
|
||||
case 12: return "towing ahead/along";
|
||||
case 14: return "SART/MOB/EPIRB";
|
||||
case 15: return "undefined";
|
||||
default: return "unknown";
|
||||
}
|
||||
switch (value) {
|
||||
case 0:
|
||||
return "under way w/engine";
|
||||
case 1:
|
||||
return "at anchor";
|
||||
case 2:
|
||||
return "not under command";
|
||||
case 3:
|
||||
return "restricted maneuv";
|
||||
case 4:
|
||||
return "constrained draught";
|
||||
case 5:
|
||||
return "moored";
|
||||
case 6:
|
||||
return "aground";
|
||||
case 7:
|
||||
return "fishing";
|
||||
case 8:
|
||||
return "sailing";
|
||||
case 9:
|
||||
case 10:
|
||||
case 13:
|
||||
return "reserved";
|
||||
case 11:
|
||||
return "towing astern";
|
||||
case 12:
|
||||
return "towing ahead/along";
|
||||
case 14:
|
||||
return "SART/MOB/EPIRB";
|
||||
case 15:
|
||||
return "undefined";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
static std::string rate_of_turn(const RateOfTurn value) {
|
||||
switch(value) {
|
||||
case -128: return "not available";
|
||||
case -127: return "left >5 deg/30sec";
|
||||
case 0: return "0 deg/min";
|
||||
case 127: return "right >5 deg/30sec";
|
||||
default:
|
||||
{
|
||||
std::string result = (value < 0) ? "left " : "right ";
|
||||
const float value_deg_sqrt = value / 4.733f;
|
||||
const int32_t value_deg = value_deg_sqrt * value_deg_sqrt;
|
||||
result += to_string_dec_uint(value_deg);
|
||||
result += " deg/min";
|
||||
return result;
|
||||
}
|
||||
}
|
||||
switch (value) {
|
||||
case -128:
|
||||
return "not available";
|
||||
case -127:
|
||||
return "left >5 deg/30sec";
|
||||
case 0:
|
||||
return "0 deg/min";
|
||||
case 127:
|
||||
return "right >5 deg/30sec";
|
||||
default: {
|
||||
std::string result = (value < 0) ? "left " : "right ";
|
||||
const float value_deg_sqrt = value / 4.733f;
|
||||
const int32_t value_deg = value_deg_sqrt * value_deg_sqrt;
|
||||
result += to_string_dec_uint(value_deg);
|
||||
result += " deg/min";
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static std::string speed_over_ground(const SpeedOverGround value) {
|
||||
if( value == 1023 ) {
|
||||
return "not available";
|
||||
} else if( value == 1022 ) {
|
||||
return ">= 102.2 knots";
|
||||
} else {
|
||||
return to_string_dec_uint(value / 10) + "." + to_string_dec_uint(value % 10, 1) + " knots";
|
||||
}
|
||||
if (value == 1023) {
|
||||
return "not available";
|
||||
} else if (value == 1022) {
|
||||
return ">= 102.2 knots";
|
||||
} else {
|
||||
return to_string_dec_uint(value / 10) + "." + to_string_dec_uint(value % 10, 1) + " knots";
|
||||
}
|
||||
}
|
||||
|
||||
static std::string course_over_ground(const CourseOverGround value) {
|
||||
if( value > 3600 ) {
|
||||
return "invalid";
|
||||
} else if( value == 3600 ) {
|
||||
return "not available";
|
||||
} else {
|
||||
return to_string_dec_uint(value / 10) + "." + to_string_dec_uint(value % 10, 1) + " deg";
|
||||
}
|
||||
if (value > 3600) {
|
||||
return "invalid";
|
||||
} else if (value == 3600) {
|
||||
return "not available";
|
||||
} else {
|
||||
return to_string_dec_uint(value / 10) + "." + to_string_dec_uint(value % 10, 1) + " deg";
|
||||
}
|
||||
}
|
||||
|
||||
static std::string true_heading(const TrueHeading value) {
|
||||
if( value == 511 ) {
|
||||
return "not available";
|
||||
} else if( value > 359 ) {
|
||||
return "invalid";
|
||||
} else {
|
||||
return to_string_dec_uint(value) + " deg";
|
||||
}
|
||||
if (value == 511) {
|
||||
return "not available";
|
||||
} else if (value > 359) {
|
||||
return "invalid";
|
||||
} else {
|
||||
return to_string_dec_uint(value) + " deg";
|
||||
}
|
||||
}
|
||||
|
||||
} /* namespace format */
|
||||
} /* namespace ais */
|
||||
|
||||
void AISLogger::on_packet(const ais::Packet& packet) {
|
||||
// TODO: Unstuff here, not in baseband!
|
||||
std::string entry;
|
||||
entry.reserve((packet.length() + 3) / 4);
|
||||
// TODO: Unstuff here, not in baseband!
|
||||
std::string entry;
|
||||
entry.reserve((packet.length() + 3) / 4);
|
||||
|
||||
for(size_t i=0; i<packet.length(); i+=4) {
|
||||
const auto nibble = packet.read(i, 4);
|
||||
entry += (nibble >= 10) ? ('W' + nibble) : ('0' + nibble);
|
||||
}
|
||||
for (size_t i = 0; i < packet.length(); i += 4) {
|
||||
const auto nibble = packet.read(i, 4);
|
||||
entry += (nibble >= 10) ? ('W' + nibble) : ('0' + nibble);
|
||||
}
|
||||
|
||||
log_file.write_entry(packet.received_at(), entry);
|
||||
log_file.write_entry(packet.received_at(), entry);
|
||||
|
||||
if (pmem::beep_on_packets()) {
|
||||
baseband::request_audio_beep(1000, 24000, 60);
|
||||
}
|
||||
}
|
||||
|
||||
void AISRecentEntry::update(const ais::Packet& packet) {
|
||||
received_count++;
|
||||
received_count++;
|
||||
|
||||
switch(packet.message_id()) {
|
||||
case 1:
|
||||
case 2:
|
||||
case 3:
|
||||
navigational_status = packet.read(38, 4);
|
||||
last_position.rate_of_turn = packet.read(42, 8);
|
||||
last_position.speed_over_ground = packet.read(50, 10);
|
||||
last_position.timestamp = packet.received_at();
|
||||
last_position.latitude = packet.latitude(89);
|
||||
last_position.longitude = packet.longitude(61);
|
||||
last_position.course_over_ground = packet.read(116, 12);
|
||||
last_position.true_heading = packet.read(128, 9);
|
||||
break;
|
||||
switch (packet.message_id()) {
|
||||
case 1:
|
||||
case 2:
|
||||
case 3:
|
||||
navigational_status = packet.read(38, 4);
|
||||
last_position.rate_of_turn = packet.read(42, 8);
|
||||
last_position.speed_over_ground = packet.read(50, 10);
|
||||
last_position.timestamp = packet.received_at();
|
||||
last_position.latitude = packet.latitude(89);
|
||||
last_position.longitude = packet.longitude(61);
|
||||
last_position.course_over_ground = packet.read(116, 12);
|
||||
last_position.true_heading = packet.read(128, 9);
|
||||
break;
|
||||
|
||||
case 4:
|
||||
last_position.timestamp = packet.received_at();
|
||||
last_position.latitude = packet.latitude(107);
|
||||
last_position.longitude = packet.longitude(79);
|
||||
break;
|
||||
case 4:
|
||||
last_position.timestamp = packet.received_at();
|
||||
last_position.latitude = packet.latitude(107);
|
||||
last_position.longitude = packet.longitude(79);
|
||||
break;
|
||||
|
||||
case 5:
|
||||
call_sign = packet.text(70, 7);
|
||||
name = packet.text(112, 20);
|
||||
destination = packet.text(302, 20);
|
||||
break;
|
||||
case 5:
|
||||
call_sign = packet.text(70, 7);
|
||||
name = packet.text(112, 20);
|
||||
destination = packet.text(302, 20);
|
||||
break;
|
||||
|
||||
case 18:
|
||||
last_position.timestamp = packet.received_at();
|
||||
last_position.speed_over_ground = packet.read(46, 10);
|
||||
last_position.latitude = packet.latitude(85);
|
||||
last_position.longitude = packet.longitude(57);
|
||||
last_position.course_over_ground = packet.read(112, 12);
|
||||
last_position.true_heading = packet.read(124, 9);
|
||||
break;
|
||||
case 18:
|
||||
last_position.timestamp = packet.received_at();
|
||||
last_position.speed_over_ground = packet.read(46, 10);
|
||||
last_position.latitude = packet.latitude(85);
|
||||
last_position.longitude = packet.longitude(57);
|
||||
last_position.course_over_ground = packet.read(112, 12);
|
||||
last_position.true_heading = packet.read(124, 9);
|
||||
break;
|
||||
|
||||
case 21:
|
||||
name = packet.text(43, 20);
|
||||
last_position.timestamp = packet.received_at();
|
||||
last_position.latitude = packet.latitude(192);
|
||||
last_position.longitude = packet.longitude(164);
|
||||
break;
|
||||
case 21:
|
||||
name = packet.text(43, 20);
|
||||
last_position.timestamp = packet.received_at();
|
||||
last_position.latitude = packet.latitude(192);
|
||||
last_position.longitude = packet.longitude(164);
|
||||
break;
|
||||
|
||||
case 24:
|
||||
switch (packet.read(38, 2))
|
||||
{
|
||||
case 0:
|
||||
name = packet.text(40, 20);
|
||||
break;
|
||||
case 24:
|
||||
switch (packet.read(38, 2)) {
|
||||
case 0:
|
||||
name = packet.text(40, 20);
|
||||
break;
|
||||
|
||||
case 1:
|
||||
call_sign = packet.text(90, 7);
|
||||
break;
|
||||
case 1:
|
||||
call_sign = packet.text(90, 7);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
namespace ui {
|
||||
|
||||
template<>
|
||||
template <>
|
||||
void RecentEntriesTable<AISRecentEntries>::draw(
|
||||
const Entry& entry,
|
||||
const Rect& target_rect,
|
||||
Painter& painter,
|
||||
const Style& style
|
||||
) {
|
||||
std::string line = ais::format::mmsi(entry.mmsi) + " ";
|
||||
if( !entry.name.empty() ) {
|
||||
line += ais::format::text(entry.name);
|
||||
} else {
|
||||
line += ais::format::text(entry.call_sign);
|
||||
}
|
||||
const Entry& entry,
|
||||
const Rect& target_rect,
|
||||
Painter& painter,
|
||||
const Style& style) {
|
||||
std::string line = ais::format::mmsi(entry.mmsi) + " ";
|
||||
if (!entry.name.empty()) {
|
||||
line += ais::format::text(entry.name);
|
||||
} else {
|
||||
line += ais::format::text(entry.call_sign);
|
||||
}
|
||||
|
||||
line.resize(target_rect.width() / 8, ' ');
|
||||
painter.draw_string(target_rect.location(), style, line);
|
||||
line.resize(target_rect.width() / 8, ' ');
|
||||
painter.draw_string(target_rect.location(), style, line);
|
||||
}
|
||||
|
||||
AISRecentEntryDetailView::AISRecentEntryDetailView(NavigationView& nav) {
|
||||
add_children({
|
||||
&button_done,
|
||||
&button_see_map,
|
||||
});
|
||||
add_children({
|
||||
&button_done,
|
||||
&button_see_map,
|
||||
});
|
||||
|
||||
button_done.on_select = [this](const ui::Button&) {
|
||||
if( this->on_close ) {
|
||||
this->on_close();
|
||||
}
|
||||
};
|
||||
button_done.on_select = [this](const ui::Button&) {
|
||||
if (on_close) {
|
||||
on_close();
|
||||
}
|
||||
};
|
||||
|
||||
button_see_map.on_select = [this, &nav](Button&) {
|
||||
geomap_view = nav.push<GeoMapView>(
|
||||
ais::format::text(entry_.name),
|
||||
0,
|
||||
GeoPos::alt_unit::METERS,
|
||||
ais::format::latlon_float(entry_.last_position.latitude.normalized()),
|
||||
ais::format::latlon_float(entry_.last_position.longitude.normalized()),
|
||||
entry_.last_position.true_heading,
|
||||
[this]() {
|
||||
send_updates = false;
|
||||
});
|
||||
send_updates = true;
|
||||
|
||||
|
||||
};
|
||||
button_see_map.on_select = [this, &nav](Button&) {
|
||||
geomap_view = nav.push<GeoMapView>(
|
||||
ais::format::text(entry_.name),
|
||||
0,
|
||||
GeoPos::alt_unit::METERS,
|
||||
GeoPos::spd_unit::NONE,
|
||||
ais::format::latlon_float(entry_.last_position.latitude.normalized()),
|
||||
ais::format::latlon_float(entry_.last_position.longitude.normalized()),
|
||||
entry_.last_position.true_heading,
|
||||
[this]() {
|
||||
send_updates = false;
|
||||
});
|
||||
send_updates = true;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
AISRecentEntryDetailView::AISRecentEntryDetailView(const AISRecentEntryDetailView&Entry) : View()
|
||||
{
|
||||
AISRecentEntryDetailView::AISRecentEntryDetailView(const AISRecentEntryDetailView& Entry)
|
||||
: View() {
|
||||
(void)Entry;
|
||||
}
|
||||
|
||||
AISRecentEntryDetailView & AISRecentEntryDetailView::operator=(const AISRecentEntryDetailView&Entry)
|
||||
{
|
||||
AISRecentEntryDetailView& AISRecentEntryDetailView::operator=(const AISRecentEntryDetailView& Entry) {
|
||||
(void)Entry;
|
||||
return *this;
|
||||
}
|
||||
|
||||
void AISRecentEntryDetailView::update_position() {
|
||||
if (send_updates)
|
||||
geomap_view->update_position(ais::format::latlon_float(entry_.last_position.latitude.normalized()), ais::format::latlon_float(entry_.last_position.longitude.normalized()), (float)entry_.last_position.true_heading, 0);
|
||||
if (send_updates)
|
||||
geomap_view->update_position(ais::format::latlon_float(entry_.last_position.latitude.normalized()), ais::format::latlon_float(entry_.last_position.longitude.normalized()), (float)entry_.last_position.true_heading, 0, entry_.last_position.speed_over_ground);
|
||||
}
|
||||
|
||||
void AISRecentEntryDetailView::focus() {
|
||||
button_done.focus();
|
||||
button_done.focus();
|
||||
}
|
||||
|
||||
Rect AISRecentEntryDetailView::draw_field(
|
||||
Painter& painter,
|
||||
const Rect& draw_rect,
|
||||
const Style& style,
|
||||
const std::string& label,
|
||||
const std::string& value
|
||||
) {
|
||||
const int label_length_max = 4;
|
||||
Painter& painter,
|
||||
const Rect& draw_rect,
|
||||
const Style& style,
|
||||
const std::string& label,
|
||||
const std::string& value) {
|
||||
const int label_length_max = 4;
|
||||
|
||||
painter.draw_string(Point { draw_rect.left(), draw_rect.top() }, style, label);
|
||||
painter.draw_string(Point { draw_rect.left() + (label_length_max + 1) * 8, draw_rect.top() }, style, value);
|
||||
painter.draw_string(Point{draw_rect.left(), draw_rect.top()}, style, label);
|
||||
painter.draw_string(Point{draw_rect.left() + (label_length_max + 1) * 8, draw_rect.top()}, style, value);
|
||||
|
||||
return { draw_rect.left(), draw_rect.top() + draw_rect.height(), draw_rect.width(), draw_rect.height() };
|
||||
return {draw_rect.left(), draw_rect.top() + draw_rect.height(), draw_rect.width(), draw_rect.height()};
|
||||
}
|
||||
|
||||
void AISRecentEntryDetailView::paint(Painter& painter) {
|
||||
View::paint(painter);
|
||||
View::paint(painter);
|
||||
|
||||
const auto s = style();
|
||||
const auto rect = screen_rect();
|
||||
const auto s = style();
|
||||
const auto rect = screen_rect();
|
||||
|
||||
auto field_rect = Rect { rect.left(), rect.top() + 16, rect.width(), 16 };
|
||||
auto field_rect = Rect{rect.left(), rect.top() + 16, rect.width(), 16};
|
||||
|
||||
field_rect = draw_field(painter, field_rect, s, "MMSI", ais::format::mmsi(entry_.mmsi));
|
||||
field_rect = draw_field(painter, field_rect, s, "Ctry", ais::format::mid(entry_.mmsi));
|
||||
field_rect = draw_field(painter, field_rect, s, "Name", ais::format::text(entry_.name));
|
||||
field_rect = draw_field(painter, field_rect, s, "Call", ais::format::text(entry_.call_sign));
|
||||
field_rect = draw_field(painter, field_rect, s, "Dest", ais::format::text(entry_.destination));
|
||||
field_rect = draw_field(painter, field_rect, s, "Last", to_string_datetime(entry_.last_position.timestamp));
|
||||
field_rect = draw_field(painter, field_rect, s, "Pos ", ais::format::latlon(entry_.last_position.latitude, entry_.last_position.longitude));
|
||||
field_rect = draw_field(painter, field_rect, s, "Stat", ais::format::navigational_status(entry_.navigational_status));
|
||||
field_rect = draw_field(painter, field_rect, s, "RoT ", ais::format::rate_of_turn(entry_.last_position.rate_of_turn));
|
||||
field_rect = draw_field(painter, field_rect, s, "SoG ", ais::format::speed_over_ground(entry_.last_position.speed_over_ground));
|
||||
field_rect = draw_field(painter, field_rect, s, "CoG ", ais::format::course_over_ground(entry_.last_position.course_over_ground));
|
||||
field_rect = draw_field(painter, field_rect, s, "Head", ais::format::true_heading(entry_.last_position.true_heading));
|
||||
field_rect = draw_field(painter, field_rect, s, "Rx #", to_string_dec_uint(entry_.received_count));
|
||||
field_rect = draw_field(painter, field_rect, s, "MMSI", ais::format::mmsi(entry_.mmsi));
|
||||
field_rect = draw_field(painter, field_rect, s, "Ctry", ais::format::mid(entry_.mmsi));
|
||||
field_rect = draw_field(painter, field_rect, s, "Name", ais::format::text(entry_.name));
|
||||
field_rect = draw_field(painter, field_rect, s, "Call", ais::format::text(entry_.call_sign));
|
||||
field_rect = draw_field(painter, field_rect, s, "Dest", ais::format::text(entry_.destination));
|
||||
field_rect = draw_field(painter, field_rect, s, "Last", to_string_datetime(entry_.last_position.timestamp));
|
||||
field_rect = draw_field(painter, field_rect, s, "Pos ", ais::format::latlon(entry_.last_position.latitude, entry_.last_position.longitude));
|
||||
field_rect = draw_field(painter, field_rect, s, "Stat", ais::format::navigational_status(entry_.navigational_status));
|
||||
field_rect = draw_field(painter, field_rect, s, "RoT ", ais::format::rate_of_turn(entry_.last_position.rate_of_turn));
|
||||
field_rect = draw_field(painter, field_rect, s, "SoG ", ais::format::speed_over_ground(entry_.last_position.speed_over_ground));
|
||||
field_rect = draw_field(painter, field_rect, s, "CoG ", ais::format::course_over_ground(entry_.last_position.course_over_ground));
|
||||
field_rect = draw_field(painter, field_rect, s, "Head", ais::format::true_heading(entry_.last_position.true_heading));
|
||||
field_rect = draw_field(painter, field_rect, s, "Rx #", to_string_dec_uint(entry_.received_count));
|
||||
}
|
||||
|
||||
void AISRecentEntryDetailView::set_entry(const AISRecentEntry& entry) {
|
||||
entry_ = entry;
|
||||
set_dirty();
|
||||
entry_ = entry;
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
AISAppView::AISAppView(NavigationView& nav) : nav_ { nav } {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_ais);
|
||||
AISAppView::AISAppView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_ais);
|
||||
|
||||
add_children({
|
||||
&label_channel,
|
||||
&options_channel,
|
||||
&field_rf_amp,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&rssi,
|
||||
&channel,
|
||||
&recent_entries_view,
|
||||
&recent_entry_detail_view,
|
||||
});
|
||||
add_children({
|
||||
&label_channel,
|
||||
&options_channel,
|
||||
&field_rf_amp,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&rssi,
|
||||
&field_volume,
|
||||
&channel,
|
||||
&recent_entries_view,
|
||||
&recent_entry_detail_view,
|
||||
});
|
||||
|
||||
recent_entry_detail_view.hidden(true);
|
||||
|
||||
// load app settings
|
||||
auto rc = settings.load("rx_ais", &app_settings);
|
||||
if(rc == SETTINGS_OK) {
|
||||
field_lna.set_value(app_settings.lna);
|
||||
field_vga.set_value(app_settings.vga);
|
||||
field_rf_amp.set_value(app_settings.rx_amp);
|
||||
target_frequency_ = app_settings.rx_frequency;
|
||||
}
|
||||
else target_frequency_ = initial_target_frequency;
|
||||
receiver_model.enable();
|
||||
|
||||
recent_entry_detail_view.hidden(true);
|
||||
options_channel.on_change = [this](size_t, OptionsField::value_t v) {
|
||||
receiver_model.set_target_frequency(v);
|
||||
};
|
||||
options_channel.set_by_value(receiver_model.target_frequency());
|
||||
|
||||
receiver_model.set_tuning_frequency(tuning_frequency());
|
||||
receiver_model.set_sampling_rate(sampling_rate);
|
||||
receiver_model.set_baseband_bandwidth(baseband_bandwidth);
|
||||
receiver_model.enable(); // Before using radio::enable(), but not updating Ant.DC-Bias.
|
||||
recent_entries_view.on_select = [this](const AISRecentEntry& entry) {
|
||||
on_show_detail(entry);
|
||||
};
|
||||
recent_entry_detail_view.on_close = [this]() {
|
||||
on_show_list();
|
||||
};
|
||||
|
||||
options_channel.on_change = [this](size_t, OptionsField::value_t v) {
|
||||
this->on_frequency_changed(v);
|
||||
};
|
||||
options_channel.set_by_value(target_frequency());
|
||||
logger = std::make_unique<AISLogger>();
|
||||
if (logger) {
|
||||
logger->append(logs_dir / u"AIS.TXT");
|
||||
}
|
||||
|
||||
recent_entries_view.on_select = [this](const AISRecentEntry& entry) {
|
||||
this->on_show_detail(entry);
|
||||
};
|
||||
recent_entry_detail_view.on_close = [this]() {
|
||||
this->on_show_list();
|
||||
};
|
||||
|
||||
logger = std::make_unique<AISLogger>();
|
||||
if( logger ) {
|
||||
logger->append(u"ais.txt");
|
||||
}
|
||||
if (pmem::beep_on_packets()) {
|
||||
audio::set_rate(audio::Rate::Hz_24000);
|
||||
audio::output::start();
|
||||
}
|
||||
}
|
||||
|
||||
AISAppView::~AISAppView() {
|
||||
|
||||
// save app settings
|
||||
app_settings.rx_frequency = target_frequency_;
|
||||
settings.save("rx_ais", &app_settings);
|
||||
|
||||
receiver_model.disable(); // to switch off all, including DC bias.
|
||||
|
||||
baseband::shutdown();
|
||||
audio::output::stop();
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
void AISAppView::focus() {
|
||||
options_channel.focus();
|
||||
options_channel.focus();
|
||||
}
|
||||
|
||||
void AISAppView::set_parent_rect(const Rect new_parent_rect) {
|
||||
View::set_parent_rect(new_parent_rect);
|
||||
const Rect content_rect { 0, header_height, new_parent_rect.width(), new_parent_rect.height() - header_height };
|
||||
recent_entries_view.set_parent_rect(content_rect);
|
||||
recent_entry_detail_view.set_parent_rect(content_rect);
|
||||
View::set_parent_rect(new_parent_rect);
|
||||
const Rect content_rect{0, header_height, new_parent_rect.width(), new_parent_rect.height() - header_height};
|
||||
recent_entries_view.set_parent_rect(content_rect);
|
||||
recent_entry_detail_view.set_parent_rect(content_rect);
|
||||
}
|
||||
|
||||
void AISAppView::on_packet(const ais::Packet& packet) {
|
||||
if( logger ) {
|
||||
logger->on_packet(packet);
|
||||
}
|
||||
if (logger) {
|
||||
logger->on_packet(packet);
|
||||
}
|
||||
|
||||
auto& entry = ::on_packet(recent, packet.source_id());
|
||||
entry.update(packet);
|
||||
recent_entries_view.set_dirty();
|
||||
auto& entry = ::on_packet(recent, packet.source_id());
|
||||
entry.update(packet);
|
||||
recent_entries_view.set_dirty();
|
||||
|
||||
// TODO: Crude hack, should be a more formal listener arrangement...
|
||||
if( entry.key() == recent_entry_detail_view.entry().key() ) {
|
||||
recent_entry_detail_view.set_entry(entry);
|
||||
recent_entry_detail_view.update_position();
|
||||
}
|
||||
// TODO: Crude hack, should be a more formal listener arrangement...
|
||||
if (entry.key() == recent_entry_detail_view.entry().key()) {
|
||||
recent_entry_detail_view.set_entry(entry);
|
||||
recent_entry_detail_view.update_position();
|
||||
}
|
||||
}
|
||||
|
||||
void AISAppView::on_show_list() {
|
||||
recent_entries_view.hidden(false);
|
||||
recent_entry_detail_view.hidden(true);
|
||||
recent_entries_view.focus();
|
||||
recent_entries_view.hidden(false);
|
||||
recent_entry_detail_view.hidden(true);
|
||||
recent_entries_view.focus();
|
||||
}
|
||||
|
||||
void AISAppView::on_show_detail(const AISRecentEntry& entry) {
|
||||
recent_entries_view.hidden(true);
|
||||
recent_entry_detail_view.hidden(false);
|
||||
recent_entry_detail_view.set_entry(entry);
|
||||
recent_entry_detail_view.focus();
|
||||
}
|
||||
|
||||
void AISAppView::on_frequency_changed(const uint32_t new_target_frequency) {
|
||||
set_target_frequency(new_target_frequency);
|
||||
}
|
||||
|
||||
void AISAppView::set_target_frequency(const uint32_t new_value) {
|
||||
target_frequency_ = new_value;
|
||||
radio::set_tuning_frequency(tuning_frequency());
|
||||
}
|
||||
|
||||
uint32_t AISAppView::target_frequency() const {
|
||||
return target_frequency_;
|
||||
}
|
||||
|
||||
uint32_t AISAppView::tuning_frequency() const {
|
||||
return target_frequency() - (sampling_rate / 4);
|
||||
recent_entries_view.hidden(true);
|
||||
recent_entry_detail_view.hidden(false);
|
||||
recent_entry_detail_view.set_entry(entry);
|
||||
recent_entry_detail_view.focus();
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
|
||||
#include "log_file.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "ais_packet.hpp"
|
||||
|
||||
#include "lpc43xx_cpp.hpp"
|
||||
@@ -50,64 +51,62 @@ using namespace lpc43xx;
|
||||
#include "recent_entries.hpp"
|
||||
|
||||
struct AISPosition {
|
||||
rtc::RTC timestamp { };
|
||||
ais::Latitude latitude { };
|
||||
ais::Longitude longitude { };
|
||||
ais::RateOfTurn rate_of_turn { -128 };
|
||||
ais::SpeedOverGround speed_over_ground { 1023 };
|
||||
ais::CourseOverGround course_over_ground { 3600 };
|
||||
ais::TrueHeading true_heading { 511 };
|
||||
rtc::RTC timestamp{};
|
||||
ais::Latitude latitude{};
|
||||
ais::Longitude longitude{};
|
||||
ais::RateOfTurn rate_of_turn{-128};
|
||||
ais::SpeedOverGround speed_over_ground{1023};
|
||||
ais::CourseOverGround course_over_ground{3600};
|
||||
ais::TrueHeading true_heading{511};
|
||||
};
|
||||
|
||||
struct AISRecentEntry {
|
||||
using Key = ais::MMSI;
|
||||
using Key = ais::MMSI;
|
||||
|
||||
static constexpr Key invalid_key = 0xffffffff;
|
||||
static constexpr Key invalid_key = 0xffffffff;
|
||||
|
||||
ais::MMSI mmsi;
|
||||
std::string name;
|
||||
std::string call_sign;
|
||||
std::string destination;
|
||||
AISPosition last_position;
|
||||
size_t received_count;
|
||||
int8_t navigational_status;
|
||||
ais::MMSI mmsi;
|
||||
std::string name;
|
||||
std::string call_sign;
|
||||
std::string destination;
|
||||
AISPosition last_position;
|
||||
size_t received_count;
|
||||
int8_t navigational_status;
|
||||
|
||||
AISRecentEntry(
|
||||
) : AISRecentEntry { 0 }
|
||||
{
|
||||
}
|
||||
AISRecentEntry()
|
||||
: AISRecentEntry{0} {
|
||||
}
|
||||
|
||||
AISRecentEntry(
|
||||
const ais::MMSI& mmsi
|
||||
) : mmsi { mmsi },
|
||||
name { },
|
||||
call_sign { },
|
||||
destination { },
|
||||
last_position { },
|
||||
received_count { 0 },
|
||||
navigational_status { -1 }
|
||||
{
|
||||
}
|
||||
AISRecentEntry(
|
||||
const ais::MMSI& mmsi)
|
||||
: mmsi{mmsi},
|
||||
name{},
|
||||
call_sign{},
|
||||
destination{},
|
||||
last_position{},
|
||||
received_count{0},
|
||||
navigational_status{-1} {
|
||||
}
|
||||
|
||||
Key key() const {
|
||||
return mmsi;
|
||||
}
|
||||
Key key() const {
|
||||
return mmsi;
|
||||
}
|
||||
|
||||
void update(const ais::Packet& packet);
|
||||
void update(const ais::Packet& packet);
|
||||
};
|
||||
|
||||
using AISRecentEntries = RecentEntries<AISRecentEntry>;
|
||||
|
||||
class AISLogger {
|
||||
public:
|
||||
Optional<File::Error> append(const std::filesystem::path& filename) {
|
||||
return log_file.append(filename);
|
||||
}
|
||||
|
||||
void on_packet(const ais::Packet& packet);
|
||||
public:
|
||||
Optional<File::Error> append(const std::filesystem::path& filename) {
|
||||
return log_file.append(filename);
|
||||
}
|
||||
|
||||
private:
|
||||
LogFile log_file { };
|
||||
void on_packet(const ais::Packet& packet);
|
||||
|
||||
private:
|
||||
LogFile log_file{};
|
||||
};
|
||||
|
||||
namespace ui {
|
||||
@@ -115,142 +114,123 @@ namespace ui {
|
||||
using AISRecentEntriesView = RecentEntriesView<AISRecentEntries>;
|
||||
|
||||
class AISRecentEntryDetailView : public View {
|
||||
public:
|
||||
std::function<void(void)> on_close { };
|
||||
public:
|
||||
std::function<void(void)> on_close{};
|
||||
|
||||
AISRecentEntryDetailView(NavigationView& nav);
|
||||
AISRecentEntryDetailView(NavigationView& nav);
|
||||
|
||||
void set_entry(const AISRecentEntry& new_entry);
|
||||
const AISRecentEntry& entry() const { return entry_; };
|
||||
void set_entry(const AISRecentEntry& new_entry);
|
||||
const AISRecentEntry& entry() const { return entry_; };
|
||||
|
||||
void update_position();
|
||||
void focus() override;
|
||||
void paint(Painter&) override;
|
||||
void update_position();
|
||||
void focus() override;
|
||||
void paint(Painter&) override;
|
||||
|
||||
AISRecentEntryDetailView(const AISRecentEntryDetailView&Entry);
|
||||
AISRecentEntryDetailView &operator=(const AISRecentEntryDetailView&Entry);
|
||||
AISRecentEntryDetailView(const AISRecentEntryDetailView& Entry);
|
||||
AISRecentEntryDetailView& operator=(const AISRecentEntryDetailView& Entry);
|
||||
|
||||
private:
|
||||
AISRecentEntry entry_ { };
|
||||
private:
|
||||
AISRecentEntry entry_{};
|
||||
|
||||
Button button_done {
|
||||
{ 125, 224, 96, 24 },
|
||||
"Done"
|
||||
};
|
||||
Button button_see_map {
|
||||
{ 19, 224, 96, 24 },
|
||||
"See on map"
|
||||
};
|
||||
GeoMapView* geomap_view { nullptr };
|
||||
bool send_updates { false };
|
||||
Button button_done{
|
||||
{125, 224, 96, 24},
|
||||
"Done"};
|
||||
Button button_see_map{
|
||||
{19, 224, 96, 24},
|
||||
"See on map"};
|
||||
GeoMapView* geomap_view{nullptr};
|
||||
bool send_updates{false};
|
||||
|
||||
Rect draw_field(
|
||||
Painter& painter,
|
||||
const Rect& draw_rect,
|
||||
const Style& style,
|
||||
const std::string& label,
|
||||
const std::string& value
|
||||
);
|
||||
Rect draw_field(
|
||||
Painter& painter,
|
||||
const Rect& draw_rect,
|
||||
const Style& style,
|
||||
const std::string& label,
|
||||
const std::string& value);
|
||||
};
|
||||
|
||||
class AISAppView : public View {
|
||||
public:
|
||||
AISAppView(NavigationView& nav);
|
||||
~AISAppView();
|
||||
public:
|
||||
AISAppView(NavigationView& nav);
|
||||
~AISAppView();
|
||||
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
void paint(Painter&) override{};
|
||||
|
||||
// Prevent painting of region covered entirely by a child.
|
||||
// TODO: Add flag to View that specifies view does not need to be cleared before painting.
|
||||
void paint(Painter&) override { };
|
||||
void focus() override;
|
||||
|
||||
void focus() override;
|
||||
std::string title() const override { return "AIS RX"; };
|
||||
|
||||
std::string title() const override { return "AIS RX"; };
|
||||
private:
|
||||
RxRadioState radio_state_{
|
||||
162025000 /* frequency*/,
|
||||
1750000 /* bandwidth */,
|
||||
2457600 /* sampling rate */
|
||||
};
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_ais", app_settings::Mode::RX};
|
||||
|
||||
private:
|
||||
static constexpr uint32_t initial_target_frequency = 162025000;
|
||||
static constexpr uint32_t sampling_rate = 2457600;
|
||||
static constexpr uint32_t baseband_bandwidth = 1750000;
|
||||
NavigationView& nav_;
|
||||
|
||||
AISRecentEntries recent{};
|
||||
std::unique_ptr<AISLogger> logger{};
|
||||
|
||||
// app save settings
|
||||
std::app_settings settings { };
|
||||
std::app_settings::AppSettings app_settings { };
|
||||
const RecentEntriesColumns columns{{
|
||||
{"MMSI", 9},
|
||||
{"Name/Call", 20},
|
||||
}};
|
||||
AISRecentEntriesView recent_entries_view{columns, recent};
|
||||
AISRecentEntryDetailView recent_entry_detail_view{nav_};
|
||||
|
||||
NavigationView& nav_;
|
||||
static constexpr auto header_height = 1 * 16;
|
||||
|
||||
AISRecentEntries recent { };
|
||||
std::unique_ptr<AISLogger> logger { };
|
||||
Text label_channel{
|
||||
{0 * 8, 0 * 16, 2 * 8, 1 * 16},
|
||||
"Ch"};
|
||||
|
||||
const RecentEntriesColumns columns { {
|
||||
{ "MMSI", 9 },
|
||||
{ "Name/Call", 20 },
|
||||
} };
|
||||
AISRecentEntriesView recent_entries_view { columns, recent };
|
||||
AISRecentEntryDetailView recent_entry_detail_view { nav_ };
|
||||
OptionsField options_channel{
|
||||
{3 * 8, 0 * 16},
|
||||
3,
|
||||
{
|
||||
{"87B", 161975000},
|
||||
{"88B", 162025000},
|
||||
}};
|
||||
|
||||
static constexpr auto header_height = 1 * 16;
|
||||
RFAmpField field_rf_amp{
|
||||
{13 * 8, 0 * 16}};
|
||||
|
||||
Text label_channel {
|
||||
{ 0 * 8, 0 * 16, 2 * 8, 1 * 16 },
|
||||
"Ch"
|
||||
};
|
||||
LNAGainField field_lna{
|
||||
{15 * 8, 0 * 16}};
|
||||
|
||||
OptionsField options_channel {
|
||||
{ 3 * 8, 0 * 16 },
|
||||
3,
|
||||
{
|
||||
{ "87B", 161975000 },
|
||||
{ "88B", 162025000 },
|
||||
}
|
||||
};
|
||||
VGAGainField field_vga{
|
||||
{18 * 8, 0 * 16}};
|
||||
|
||||
RFAmpField field_rf_amp {
|
||||
{ 13 * 8, 0 * 16 }
|
||||
};
|
||||
RSSI rssi{
|
||||
{21 * 8, 0, 6 * 8, 4},
|
||||
};
|
||||
|
||||
LNAGainField field_lna {
|
||||
{ 15 * 8, 0 * 16 }
|
||||
};
|
||||
AudioVolumeField field_volume{
|
||||
{28 * 8, 0 * 16}};
|
||||
|
||||
VGAGainField field_vga {
|
||||
{ 18 * 8, 0 * 16 }
|
||||
};
|
||||
Channel channel{
|
||||
{21 * 8, 5, 6 * 8, 4},
|
||||
};
|
||||
|
||||
RSSI rssi {
|
||||
{ 21 * 8, 0, 6 * 8, 4 },
|
||||
};
|
||||
MessageHandlerRegistration message_handler_packet{
|
||||
Message::ID::AISPacket,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const AISPacketMessage*>(p);
|
||||
const ais::Packet packet{message->packet};
|
||||
if (packet.is_valid()) {
|
||||
this->on_packet(packet);
|
||||
}
|
||||
}};
|
||||
|
||||
Channel channel {
|
||||
{ 21 * 8, 5, 6 * 8, 4 },
|
||||
};
|
||||
|
||||
MessageHandlerRegistration message_handler_packet {
|
||||
Message::ID::AISPacket,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const AISPacketMessage*>(p);
|
||||
const ais::Packet packet { message->packet };
|
||||
if( packet.is_valid() ) {
|
||||
this->on_packet(packet);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
uint32_t target_frequency_ = initial_target_frequency;
|
||||
|
||||
void on_packet(const ais::Packet& packet);
|
||||
void on_show_list();
|
||||
void on_show_detail(const AISRecentEntry& entry);
|
||||
|
||||
void on_frequency_changed(const uint32_t new_target_frequency);
|
||||
|
||||
uint32_t target_frequency() const;
|
||||
void set_target_frequency(const uint32_t new_value);
|
||||
|
||||
uint32_t tuning_frequency() const;
|
||||
void on_packet(const ais::Packet& packet);
|
||||
void on_show_list();
|
||||
void on_show_detail(const AISRecentEntry& entry);
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
#endif/*__AIS_APP_H__*/
|
||||
#endif /*__AIS_APP_H__*/
|
||||
|
||||
@@ -22,428 +22,420 @@
|
||||
|
||||
#include "analog_audio_app.hpp"
|
||||
|
||||
#include "audio.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
|
||||
#include "file.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "ui_freqman.hpp"
|
||||
#include "utility.hpp"
|
||||
#include "radio.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
using namespace tonekey;
|
||||
|
||||
#include "audio.hpp"
|
||||
#include "file.hpp"
|
||||
|
||||
#include "utility.hpp"
|
||||
|
||||
#include "string_format.hpp"
|
||||
|
||||
namespace ui {
|
||||
|
||||
/* AMOptionsView *********************************************************/
|
||||
|
||||
AMOptionsView::AMOptionsView(
|
||||
const Rect parent_rect, const Style* const style
|
||||
) : View { parent_rect }
|
||||
{
|
||||
set_style(style);
|
||||
Rect parent_rect,
|
||||
const Style* style)
|
||||
: View{parent_rect} {
|
||||
set_style(style);
|
||||
|
||||
add_children({
|
||||
&label_config,
|
||||
&options_config,
|
||||
});
|
||||
add_children({
|
||||
&label_config,
|
||||
&options_config,
|
||||
});
|
||||
|
||||
options_config.set_selected_index(receiver_model.am_configuration());
|
||||
options_config.on_change = [this](size_t n, OptionsField::value_t) {
|
||||
receiver_model.set_am_configuration(n);
|
||||
};
|
||||
freqman_set_bandwidth_option(AM_MODULATION, options_config); // adding the common message from freqman.cpp to the options_config
|
||||
options_config.set_by_value(receiver_model.am_configuration());
|
||||
options_config.on_change = [this](size_t, OptionsField::value_t n) {
|
||||
receiver_model.set_am_configuration(n);
|
||||
};
|
||||
}
|
||||
|
||||
/* NBFMOptionsView *******************************************************/
|
||||
|
||||
NBFMOptionsView::NBFMOptionsView(
|
||||
const Rect parent_rect, const Style* const style
|
||||
) : View { parent_rect }
|
||||
{
|
||||
set_style(style);
|
||||
Rect parent_rect,
|
||||
const Style* style)
|
||||
: View{parent_rect} {
|
||||
set_style(style);
|
||||
|
||||
add_children({
|
||||
&label_config,
|
||||
&options_config,
|
||||
&text_squelch,
|
||||
&field_squelch
|
||||
});
|
||||
add_children({&label_config,
|
||||
&options_config,
|
||||
&text_squelch,
|
||||
&field_squelch});
|
||||
|
||||
options_config.set_selected_index(receiver_model.nbfm_configuration());
|
||||
options_config.on_change = [this](size_t n, OptionsField::value_t) {
|
||||
receiver_model.set_nbfm_configuration(n);
|
||||
};
|
||||
|
||||
field_squelch.set_value(receiver_model.squelch_level());
|
||||
field_squelch.on_change = [this](int32_t v) {
|
||||
receiver_model.set_squelch_level(v);
|
||||
};
|
||||
freqman_set_bandwidth_option(NFM_MODULATION, options_config); // adding the common message from freqman.cpp to the options_config
|
||||
options_config.set_by_value(receiver_model.nbfm_configuration());
|
||||
options_config.on_change = [this](size_t, OptionsField::value_t n) {
|
||||
receiver_model.set_nbfm_configuration(n);
|
||||
};
|
||||
|
||||
field_squelch.set_value(receiver_model.squelch_level());
|
||||
field_squelch.on_change = [this](int32_t v) {
|
||||
receiver_model.set_squelch_level(v);
|
||||
};
|
||||
}
|
||||
|
||||
/* WFMOptionsView *******************************************************/
|
||||
|
||||
WFMOptionsView::WFMOptionsView(
|
||||
Rect parent_rect,
|
||||
const Style* style)
|
||||
: View{parent_rect} {
|
||||
set_style(style);
|
||||
|
||||
add_children({
|
||||
&label_config,
|
||||
&options_config,
|
||||
});
|
||||
|
||||
freqman_set_bandwidth_option(WFM_MODULATION, options_config); // adding the common message from freqman.cpp to the options_config
|
||||
options_config.set_by_value(receiver_model.wfm_configuration());
|
||||
options_config.on_change = [this](size_t, OptionsField::value_t n) {
|
||||
receiver_model.set_wfm_configuration(n);
|
||||
};
|
||||
}
|
||||
|
||||
/* SPECOptionsView *******************************************************/
|
||||
|
||||
SPECOptionsView::SPECOptionsView(
|
||||
AnalogAudioView* view, const Rect parent_rect, const Style* const style
|
||||
) : View { parent_rect }
|
||||
{
|
||||
set_style(style);
|
||||
AnalogAudioView* view,
|
||||
Rect parent_rect,
|
||||
const Style* style)
|
||||
: View{parent_rect} {
|
||||
set_style(style);
|
||||
|
||||
add_children({
|
||||
&label_config,
|
||||
&options_config,
|
||||
&text_speed,
|
||||
&field_speed
|
||||
});
|
||||
add_children({
|
||||
&label_config,
|
||||
&options_config,
|
||||
&text_speed,
|
||||
&field_speed,
|
||||
&text_rx_cal,
|
||||
&field_rx_iq_phase_cal,
|
||||
});
|
||||
|
||||
options_config.set_selected_index(view->get_spec_bw_index());
|
||||
options_config.on_change = [this, view](size_t n, OptionsField::value_t bw) {
|
||||
view->set_spec_bw(n, bw);
|
||||
};
|
||||
options_config.set_selected_index(view->get_spec_bw_index());
|
||||
options_config.on_change = [this, view](size_t n, OptionsField::value_t bw) {
|
||||
view->set_spec_bw(n, bw);
|
||||
};
|
||||
|
||||
field_speed.set_value(view->get_spec_trigger());
|
||||
field_speed.on_change = [this, view](int32_t v) {
|
||||
view->set_spec_trigger(v);
|
||||
};
|
||||
field_speed.set_value(view->get_spec_trigger());
|
||||
field_speed.on_change = [this, view](int32_t v) {
|
||||
view->set_spec_trigger(v);
|
||||
};
|
||||
|
||||
field_rx_iq_phase_cal.set_range(0, hackrf_r9 ? 63 : 31); // max2839 has 6 bits [0..63], max2837 has 5 bits [0..31]
|
||||
field_rx_iq_phase_cal.set_value(view->get_spec_iq_phase_calibration_value()); // using accessor function of AnalogAudioView to read iq_phase_calibration_value from rx_audio.ini
|
||||
field_rx_iq_phase_cal.on_change = [this, view](int32_t v) {
|
||||
view->set_spec_iq_phase_calibration_value(v); // using accessor function of AnalogAudioView to write inside SPEC submenu, register value to max283x and save it to rx_audio.ini
|
||||
};
|
||||
|
||||
view->set_spec_iq_phase_calibration_value(view->get_spec_iq_phase_calibration_value()); // initialize iq_phase_calibration in radio
|
||||
}
|
||||
|
||||
/* AnalogAudioView *******************************************************/
|
||||
|
||||
AnalogAudioView::AnalogAudioView(
|
||||
NavigationView& nav
|
||||
) : nav_ (nav)
|
||||
{
|
||||
add_children({
|
||||
&rssi,
|
||||
&channel,
|
||||
&audio,
|
||||
&field_frequency,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&options_modulation,
|
||||
&field_volume,
|
||||
&text_ctcss,
|
||||
&record_view,
|
||||
&waterfall
|
||||
});
|
||||
NavigationView& nav)
|
||||
: nav_(nav) {
|
||||
// A baseband image _must_ be running before add waterfall view.
|
||||
baseband::run_image(portapack::spi_flash::image_tag_wideband_spectrum);
|
||||
|
||||
// Set on_change before initialising the field
|
||||
field_frequency.on_change = [this](rf::Frequency f) {
|
||||
this->on_tuning_frequency_changed(f);
|
||||
};
|
||||
add_children({&rssi,
|
||||
&channel,
|
||||
&audio,
|
||||
&field_frequency,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&options_modulation,
|
||||
&field_volume,
|
||||
&text_ctcss,
|
||||
&record_view,
|
||||
&waterfall});
|
||||
|
||||
// load app settings
|
||||
auto rc = settings.load("rx_audio", &app_settings);
|
||||
if(rc == SETTINGS_OK) {
|
||||
field_lna.set_value(app_settings.lna);
|
||||
field_vga.set_value(app_settings.vga);
|
||||
receiver_model.set_rf_amp(app_settings.rx_amp);
|
||||
// field_frequency.set_value(app_settings.rx_frequency);
|
||||
receiver_model.set_configuration_without_init(static_cast<ReceiverModel::Mode>(app_settings.modulation), app_settings.step, app_settings.am_config_index, app_settings.nbfm_config_index, app_settings.wfm_config_index, app_settings.squelch);
|
||||
}
|
||||
field_frequency.set_value(receiver_model.tuning_frequency());
|
||||
|
||||
//Filename Datetime and Frequency
|
||||
record_view.set_filename_date_frequency(true);
|
||||
// Filename Datetime and Frequency
|
||||
record_view.set_filename_date_frequency(true);
|
||||
|
||||
field_frequency.set_step(receiver_model.frequency_step());
|
||||
field_frequency.on_edit = [this, &nav]() {
|
||||
// TODO: Provide separate modal method/scheme?
|
||||
auto new_view = nav.push<FrequencyKeypadView>(receiver_model.tuning_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
this->on_tuning_frequency_changed(f);
|
||||
this->field_frequency.set_value(f);
|
||||
};
|
||||
};
|
||||
field_frequency.on_show_options = [this]() {
|
||||
this->on_show_options_frequency();
|
||||
};
|
||||
|
||||
field_frequency.on_show_options = [this]() {
|
||||
this->on_show_options_frequency();
|
||||
};
|
||||
field_lna.on_show_options = [this]() {
|
||||
this->on_show_options_rf_gain();
|
||||
};
|
||||
|
||||
field_lna.on_show_options = [this]() {
|
||||
this->on_show_options_rf_gain();
|
||||
};
|
||||
field_vga.on_show_options = [this]() {
|
||||
this->on_show_options_rf_gain();
|
||||
};
|
||||
|
||||
field_vga.on_show_options = [this]() {
|
||||
this->on_show_options_rf_gain();
|
||||
};
|
||||
auto modulation = receiver_model.modulation();
|
||||
// This app doesn't handle "Capture" mode.
|
||||
if (modulation > ReceiverModel::Mode::SpectrumAnalysis)
|
||||
modulation = ReceiverModel::Mode::SpectrumAnalysis;
|
||||
|
||||
const auto modulation = receiver_model.modulation();
|
||||
options_modulation.set_by_value(toUType(modulation));
|
||||
options_modulation.set_by_value(toUType(modulation));
|
||||
options_modulation.on_change = [this](size_t, OptionsField::value_t v) {
|
||||
this->on_modulation_changed(static_cast<ReceiverModel::Mode>(v));
|
||||
};
|
||||
options_modulation.on_show_options = [this]() {
|
||||
this->on_show_options_modulation();
|
||||
};
|
||||
|
||||
options_modulation.on_change = [this](size_t, OptionsField::value_t v) {
|
||||
this->on_modulation_changed(static_cast<ReceiverModel::Mode>(v));
|
||||
};
|
||||
options_modulation.on_show_options = [this]() {
|
||||
this->on_show_options_modulation();
|
||||
};
|
||||
record_view.on_error = [&nav](std::string message) {
|
||||
nav.display_modal("Error", message);
|
||||
};
|
||||
|
||||
field_volume.set_value((receiver_model.headphone_volume() - audio::headphone::volume_range().max).decibel() + 99);
|
||||
field_volume.on_change = [this](int32_t v) {
|
||||
this->on_headphone_volume_changed(v);
|
||||
};
|
||||
waterfall.on_select = [this](int32_t offset) {
|
||||
field_frequency.set_value(receiver_model.target_frequency() + offset);
|
||||
};
|
||||
|
||||
record_view.on_error = [&nav](std::string message) {
|
||||
nav.display_modal("Error", message);
|
||||
};
|
||||
|
||||
waterfall.on_select = [this](int32_t offset) {
|
||||
field_frequency.set_value(receiver_model.tuning_frequency() + offset);
|
||||
};
|
||||
audio::output::start();
|
||||
|
||||
audio::output::start();
|
||||
// This call starts the correct baseband image to run
|
||||
// and sets the radio up as necessary for the given modulation.
|
||||
on_modulation_changed(modulation);
|
||||
}
|
||||
|
||||
update_modulation(static_cast<ReceiverModel::Mode>(modulation));
|
||||
on_modulation_changed(static_cast<ReceiverModel::Mode>(modulation));
|
||||
AnalogAudioView::AnalogAudioView(
|
||||
NavigationView& nav,
|
||||
ReceiverModel::settings_t override)
|
||||
: AnalogAudioView(nav) {
|
||||
// Settings to override when launched from another app (versus from AppSettings .ini file)
|
||||
// TODO: Which other settings make sense to override?
|
||||
field_frequency.set_value(override.frequency_app_override);
|
||||
on_frequency_step_changed(override.frequency_step);
|
||||
options_modulation.set_by_value(toUType(override.mode));
|
||||
}
|
||||
|
||||
size_t AnalogAudioView::get_spec_bw_index() {
|
||||
return spec_bw_index;
|
||||
return spec_bw_index;
|
||||
}
|
||||
|
||||
void AnalogAudioView::set_spec_bw(size_t index, uint32_t bw) {
|
||||
spec_bw_index = index;
|
||||
spec_bw = bw;
|
||||
spec_bw_index = index;
|
||||
spec_bw = bw;
|
||||
|
||||
baseband::set_spectrum(bw, spec_trigger);
|
||||
receiver_model.set_sampling_rate(bw);
|
||||
receiver_model.set_baseband_bandwidth(bw/2);
|
||||
baseband::set_spectrum(bw, spec_trigger);
|
||||
receiver_model.set_sampling_rate(bw);
|
||||
receiver_model.set_baseband_bandwidth(bw / 2);
|
||||
}
|
||||
|
||||
uint8_t AnalogAudioView::get_spec_iq_phase_calibration_value() { // define accessor functions inside AnalogAudioView to read & write real iq_phase_calibration_value
|
||||
return iq_phase_calibration_value;
|
||||
}
|
||||
|
||||
void AnalogAudioView::set_spec_iq_phase_calibration_value(uint8_t cal_value) { // define accessor functions
|
||||
iq_phase_calibration_value = cal_value;
|
||||
radio::set_rx_max283x_iq_phase_calibration(iq_phase_calibration_value);
|
||||
}
|
||||
|
||||
uint16_t AnalogAudioView::get_spec_trigger() {
|
||||
return spec_trigger;
|
||||
return spec_trigger;
|
||||
}
|
||||
|
||||
void AnalogAudioView::set_spec_trigger(uint16_t trigger) {
|
||||
spec_trigger = trigger;
|
||||
spec_trigger = trigger;
|
||||
|
||||
baseband::set_spectrum(spec_bw, spec_trigger);
|
||||
baseband::set_spectrum(spec_bw, spec_trigger);
|
||||
}
|
||||
|
||||
AnalogAudioView::~AnalogAudioView() {
|
||||
|
||||
// save app settings
|
||||
app_settings.rx_frequency = field_frequency.value();
|
||||
app_settings.lna = receiver_model.lna();
|
||||
app_settings.vga = receiver_model.vga();
|
||||
app_settings.rx_amp = receiver_model.rf_amp();
|
||||
app_settings.step = receiver_model.frequency_step();
|
||||
app_settings.modulation = (uint8_t)receiver_model.modulation();
|
||||
app_settings.am_config_index = receiver_model.am_configuration();
|
||||
app_settings.nbfm_config_index = receiver_model.nbfm_configuration();
|
||||
app_settings.wfm_config_index = receiver_model.wfm_configuration();
|
||||
app_settings.squelch = receiver_model.squelch_level();
|
||||
settings.save("rx_audio", &app_settings);
|
||||
|
||||
// TODO: Manipulating audio codec here, and in ui_receiver.cpp. Good to do
|
||||
// both?
|
||||
audio::output::stop();
|
||||
|
||||
receiver_model.set_sampling_rate(3072000); // Just a hack to avoid hanging other apps if the last modulation was SPEC
|
||||
receiver_model.disable();
|
||||
|
||||
baseband::shutdown();
|
||||
audio::output::stop();
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
void AnalogAudioView::on_hide() {
|
||||
// TODO: Terrible kludge because widget system doesn't notify Waterfall that
|
||||
// it's being shown or hidden.
|
||||
waterfall.on_hide();
|
||||
View::on_hide();
|
||||
}
|
||||
void AnalogAudioView::set_parent_rect(Rect new_parent_rect) {
|
||||
View::set_parent_rect(new_parent_rect);
|
||||
|
||||
void AnalogAudioView::set_parent_rect(const Rect new_parent_rect) {
|
||||
View::set_parent_rect(new_parent_rect);
|
||||
|
||||
const ui::Rect waterfall_rect { 0, header_height, new_parent_rect.width(), new_parent_rect.height() - header_height };
|
||||
waterfall.set_parent_rect(waterfall_rect);
|
||||
ui::Rect waterfall_rect{0, header_height, new_parent_rect.width(), new_parent_rect.height() - header_height};
|
||||
waterfall.set_parent_rect(waterfall_rect);
|
||||
}
|
||||
|
||||
void AnalogAudioView::focus() {
|
||||
field_frequency.focus();
|
||||
}
|
||||
|
||||
void AnalogAudioView::on_tuning_frequency_changed(rf::Frequency f) {
|
||||
receiver_model.set_tuning_frequency(f);
|
||||
field_frequency.focus();
|
||||
}
|
||||
|
||||
void AnalogAudioView::on_baseband_bandwidth_changed(uint32_t bandwidth_hz) {
|
||||
receiver_model.set_baseband_bandwidth(bandwidth_hz);
|
||||
receiver_model.set_baseband_bandwidth(bandwidth_hz);
|
||||
}
|
||||
|
||||
void AnalogAudioView::on_modulation_changed(const ReceiverModel::Mode modulation) {
|
||||
// TODO: Terrible kludge because widget system doesn't notify Waterfall that
|
||||
// it's being shown or hidden.
|
||||
waterfall.on_hide();
|
||||
update_modulation(modulation);
|
||||
on_show_options_modulation();
|
||||
waterfall.on_show();
|
||||
void AnalogAudioView::on_modulation_changed(ReceiverModel::Mode modulation) {
|
||||
// This app doesn't know what to do with "Capture" mode.
|
||||
if (modulation > ReceiverModel::Mode::SpectrumAnalysis)
|
||||
modulation = ReceiverModel::Mode::SpectrumAnalysis;
|
||||
|
||||
baseband::spectrum_streaming_stop();
|
||||
update_modulation(modulation);
|
||||
on_show_options_modulation();
|
||||
baseband::spectrum_streaming_start();
|
||||
}
|
||||
|
||||
void AnalogAudioView::remove_options_widget() {
|
||||
if( options_widget ) {
|
||||
remove_child(options_widget.get());
|
||||
options_widget.reset();
|
||||
}
|
||||
|
||||
field_lna.set_style(nullptr);
|
||||
options_modulation.set_style(nullptr);
|
||||
field_frequency.set_style(nullptr);
|
||||
if (options_widget) {
|
||||
remove_child(options_widget.get());
|
||||
options_widget.reset();
|
||||
}
|
||||
|
||||
field_lna.set_style(nullptr);
|
||||
options_modulation.set_style(nullptr);
|
||||
field_frequency.set_style(nullptr);
|
||||
}
|
||||
|
||||
void AnalogAudioView::set_options_widget(std::unique_ptr<Widget> new_widget) {
|
||||
remove_options_widget();
|
||||
remove_options_widget();
|
||||
|
||||
if( new_widget ) {
|
||||
options_widget = std::move(new_widget);
|
||||
} else {
|
||||
// TODO: Lame hack to hide options view due to my bad paint/damage algorithm.
|
||||
options_widget = std::make_unique<Rectangle>(options_view_rect, style_options_group.background);
|
||||
}
|
||||
add_child(options_widget.get());
|
||||
if (new_widget) {
|
||||
options_widget = std::move(new_widget);
|
||||
} else {
|
||||
// TODO: Lame hack to hide options view due to my bad paint/damage algorithm.
|
||||
options_widget = std::make_unique<Rectangle>(options_view_rect, Theme::getInstance()->option_active->background);
|
||||
}
|
||||
add_child(options_widget.get());
|
||||
}
|
||||
|
||||
void AnalogAudioView::on_show_options_frequency() {
|
||||
auto widget = std::make_unique<FrequencyOptionsView>(options_view_rect, &style_options_group);
|
||||
auto widget = std::make_unique<FrequencyOptionsView>(options_view_rect, Theme::getInstance()->option_active);
|
||||
|
||||
widget->set_step(receiver_model.frequency_step());
|
||||
widget->on_change_step = [this](rf::Frequency f) {
|
||||
this->on_frequency_step_changed(f);
|
||||
};
|
||||
widget->set_reference_ppm_correction(persistent_memory::correction_ppb() / 1000);
|
||||
widget->on_change_reference_ppm_correction = [this](int32_t v) {
|
||||
this->on_reference_ppm_correction_changed(v);
|
||||
};
|
||||
widget->set_step(receiver_model.frequency_step());
|
||||
widget->on_change_step = [this](rf::Frequency f) {
|
||||
this->on_frequency_step_changed(f);
|
||||
};
|
||||
widget->set_reference_ppm_correction(persistent_memory::correction_ppb() / 1000);
|
||||
widget->on_change_reference_ppm_correction = [this](int32_t v) {
|
||||
this->on_reference_ppm_correction_changed(v);
|
||||
};
|
||||
|
||||
set_options_widget(std::move(widget));
|
||||
field_frequency.set_style(&style_options_group);
|
||||
set_options_widget(std::move(widget));
|
||||
field_frequency.set_style(Theme::getInstance()->option_active);
|
||||
}
|
||||
|
||||
void AnalogAudioView::on_show_options_rf_gain() {
|
||||
auto widget = std::make_unique<RadioGainOptionsView>(options_view_rect, &style_options_group);
|
||||
auto widget = std::make_unique<RadioGainOptionsView>(options_view_rect, Theme::getInstance()->option_active);
|
||||
|
||||
set_options_widget(std::move(widget));
|
||||
field_lna.set_style(&style_options_group);
|
||||
set_options_widget(std::move(widget));
|
||||
field_lna.set_style(Theme::getInstance()->option_active);
|
||||
}
|
||||
|
||||
void AnalogAudioView::on_show_options_modulation() {
|
||||
std::unique_ptr<Widget> widget;
|
||||
std::unique_ptr<Widget> widget;
|
||||
|
||||
const auto modulation = static_cast<ReceiverModel::Mode>(receiver_model.modulation());
|
||||
switch(modulation) {
|
||||
case ReceiverModel::Mode::AMAudio:
|
||||
widget = std::make_unique<AMOptionsView>(options_view_rect, &style_options_group);
|
||||
waterfall.show_audio_spectrum_view(false);
|
||||
text_ctcss.hidden(true);
|
||||
break;
|
||||
const auto modulation = receiver_model.modulation();
|
||||
switch (modulation) {
|
||||
case ReceiverModel::Mode::AMAudio:
|
||||
widget = std::make_unique<AMOptionsView>(options_view_rect, Theme::getInstance()->option_active);
|
||||
waterfall.show_audio_spectrum_view(false);
|
||||
text_ctcss.hidden(true);
|
||||
break;
|
||||
|
||||
case ReceiverModel::Mode::NarrowbandFMAudio:
|
||||
widget = std::make_unique<NBFMOptionsView>(nbfm_view_rect, &style_options_group);
|
||||
waterfall.show_audio_spectrum_view(false);
|
||||
text_ctcss.hidden(false);
|
||||
break;
|
||||
|
||||
case ReceiverModel::Mode::WidebandFMAudio:
|
||||
waterfall.show_audio_spectrum_view(true);
|
||||
text_ctcss.hidden(true);
|
||||
break;
|
||||
|
||||
case ReceiverModel::Mode::SpectrumAnalysis:
|
||||
widget = std::make_unique<SPECOptionsView>(this, nbfm_view_rect, &style_options_group);
|
||||
waterfall.show_audio_spectrum_view(false);
|
||||
text_ctcss.hidden(true);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
case ReceiverModel::Mode::NarrowbandFMAudio:
|
||||
widget = std::make_unique<NBFMOptionsView>(nbfm_view_rect, Theme::getInstance()->option_active);
|
||||
waterfall.show_audio_spectrum_view(false);
|
||||
text_ctcss.hidden(false);
|
||||
break;
|
||||
|
||||
set_options_widget(std::move(widget));
|
||||
options_modulation.set_style(&style_options_group);
|
||||
case ReceiverModel::Mode::WidebandFMAudio:
|
||||
widget = std::make_unique<WFMOptionsView>(options_view_rect, Theme::getInstance()->option_active);
|
||||
waterfall.show_audio_spectrum_view(true);
|
||||
text_ctcss.hidden(true);
|
||||
break;
|
||||
|
||||
case ReceiverModel::Mode::SpectrumAnalysis:
|
||||
widget = std::make_unique<SPECOptionsView>(this, nbfm_view_rect, Theme::getInstance()->option_active);
|
||||
waterfall.show_audio_spectrum_view(false);
|
||||
text_ctcss.hidden(true);
|
||||
break;
|
||||
|
||||
default:
|
||||
chDbgPanic("Unhandled Mode");
|
||||
break;
|
||||
}
|
||||
|
||||
set_options_widget(std::move(widget));
|
||||
options_modulation.set_style(Theme::getInstance()->option_active);
|
||||
}
|
||||
|
||||
void AnalogAudioView::on_frequency_step_changed(rf::Frequency f) {
|
||||
receiver_model.set_frequency_step(f);
|
||||
field_frequency.set_step(f);
|
||||
receiver_model.set_frequency_step(f);
|
||||
field_frequency.set_step(f);
|
||||
}
|
||||
|
||||
void AnalogAudioView::on_reference_ppm_correction_changed(int32_t v) {
|
||||
persistent_memory::set_correction_ppb(v * 1000);
|
||||
persistent_memory::set_correction_ppb(v * 1000);
|
||||
}
|
||||
|
||||
void AnalogAudioView::on_headphone_volume_changed(int32_t v) {
|
||||
const auto new_volume = volume_t::decibel(v - 99) + audio::headphone::volume_range().max;
|
||||
receiver_model.set_headphone_volume(new_volume);
|
||||
void AnalogAudioView::update_modulation(ReceiverModel::Mode modulation) {
|
||||
audio::output::mute();
|
||||
record_view.stop();
|
||||
|
||||
baseband::shutdown();
|
||||
|
||||
portapack::spi_flash::image_tag_t image_tag;
|
||||
switch (modulation) {
|
||||
case ReceiverModel::Mode::AMAudio:
|
||||
image_tag = portapack::spi_flash::image_tag_am_audio;
|
||||
break;
|
||||
case ReceiverModel::Mode::NarrowbandFMAudio:
|
||||
image_tag = portapack::spi_flash::image_tag_nfm_audio;
|
||||
break;
|
||||
case ReceiverModel::Mode::WidebandFMAudio:
|
||||
image_tag = portapack::spi_flash::image_tag_wfm_audio;
|
||||
break;
|
||||
case ReceiverModel::Mode::SpectrumAnalysis:
|
||||
image_tag = portapack::spi_flash::image_tag_wideband_spectrum;
|
||||
break;
|
||||
default:
|
||||
chDbgPanic("Unhandled Mode");
|
||||
break;
|
||||
}
|
||||
|
||||
baseband::run_image(image_tag);
|
||||
|
||||
if (modulation == ReceiverModel::Mode::SpectrumAnalysis) {
|
||||
baseband::set_spectrum(spec_bw, spec_trigger);
|
||||
}
|
||||
|
||||
const auto is_wideband_spectrum_mode = (modulation == ReceiverModel::Mode::SpectrumAnalysis);
|
||||
receiver_model.set_modulation(modulation);
|
||||
|
||||
receiver_model.set_sampling_rate(is_wideband_spectrum_mode ? spec_bw : 3072000);
|
||||
receiver_model.set_baseband_bandwidth(is_wideband_spectrum_mode ? spec_bw / 2 : 1750000);
|
||||
|
||||
receiver_model.enable();
|
||||
|
||||
// TODO: This doesn't belong here! There's a better way.
|
||||
size_t sampling_rate = 0;
|
||||
switch (modulation) {
|
||||
case ReceiverModel::Mode::AMAudio:
|
||||
sampling_rate = 12000;
|
||||
break;
|
||||
case ReceiverModel::Mode::NarrowbandFMAudio:
|
||||
sampling_rate = 24000;
|
||||
break;
|
||||
case ReceiverModel::Mode::WidebandFMAudio:
|
||||
sampling_rate = 48000;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
record_view.set_sampling_rate(sampling_rate);
|
||||
|
||||
if (!is_wideband_spectrum_mode) {
|
||||
audio::output::unmute();
|
||||
}
|
||||
}
|
||||
|
||||
void AnalogAudioView::update_modulation(const ReceiverModel::Mode modulation) {
|
||||
audio::output::mute();
|
||||
record_view.stop();
|
||||
|
||||
baseband::shutdown();
|
||||
|
||||
portapack::spi_flash::image_tag_t image_tag;
|
||||
switch(modulation) {
|
||||
case ReceiverModel::Mode::AMAudio: image_tag = portapack::spi_flash::image_tag_am_audio; break;
|
||||
case ReceiverModel::Mode::NarrowbandFMAudio: image_tag = portapack::spi_flash::image_tag_nfm_audio; break;
|
||||
case ReceiverModel::Mode::WidebandFMAudio: image_tag = portapack::spi_flash::image_tag_wfm_audio; break;
|
||||
case ReceiverModel::Mode::SpectrumAnalysis: image_tag = portapack::spi_flash::image_tag_wideband_spectrum; break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
baseband::run_image(image_tag);
|
||||
|
||||
if (modulation == ReceiverModel::Mode::SpectrumAnalysis) {
|
||||
baseband::set_spectrum(spec_bw, spec_trigger);
|
||||
}
|
||||
|
||||
const auto is_wideband_spectrum_mode = (modulation == ReceiverModel::Mode::SpectrumAnalysis);
|
||||
receiver_model.set_modulation(modulation);
|
||||
|
||||
receiver_model.set_sampling_rate(is_wideband_spectrum_mode ? spec_bw : 3072000);
|
||||
receiver_model.set_baseband_bandwidth(is_wideband_spectrum_mode ? spec_bw/2 : 1750000);
|
||||
|
||||
receiver_model.enable();
|
||||
|
||||
// TODO: This doesn't belong here! There's a better way.
|
||||
size_t sampling_rate = 0;
|
||||
switch(modulation) {
|
||||
case ReceiverModel::Mode::AMAudio: sampling_rate = 12000; break;
|
||||
case ReceiverModel::Mode::NarrowbandFMAudio: sampling_rate = 24000; break;
|
||||
case ReceiverModel::Mode::WidebandFMAudio: sampling_rate = 48000; break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
record_view.set_sampling_rate(sampling_rate);
|
||||
|
||||
if( !is_wideband_spectrum_mode ) {
|
||||
audio::output::unmute();
|
||||
}
|
||||
void AnalogAudioView::handle_coded_squelch(uint32_t value) {
|
||||
text_ctcss.set(tone_key_string_by_value(value, text_ctcss.parent_rect().width() / 8));
|
||||
}
|
||||
|
||||
|
||||
void AnalogAudioView::handle_coded_squelch(const uint32_t value) {
|
||||
float diff, min_diff = value;
|
||||
size_t min_idx { 0 };
|
||||
size_t c;
|
||||
|
||||
// Find nearest match
|
||||
for (c = 0; c < tone_keys.size(); c++) {
|
||||
diff = abs(((float)value / 100.0) - tone_keys[c].second);
|
||||
if (diff < min_diff) {
|
||||
min_idx = c;
|
||||
min_diff = diff;
|
||||
}
|
||||
}
|
||||
|
||||
// Arbitrary confidence threshold
|
||||
if (min_diff < 40)
|
||||
text_ctcss.set("CTCSS " + tone_keys[min_idx].first);
|
||||
else
|
||||
text_ctcss.set("???");
|
||||
void AnalogAudioView::on_freqchg(int64_t freq) {
|
||||
field_frequency.set_value(freq);
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -26,245 +26,244 @@
|
||||
#include "receiver_model.hpp"
|
||||
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "ui_spectrum.hpp"
|
||||
#include "ui_record_view.hpp"
|
||||
#include "ui_font_fixed_8x16.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "tone_key.hpp"
|
||||
|
||||
|
||||
namespace ui {
|
||||
|
||||
constexpr Style style_options_group {
|
||||
.font = font::fixed_8x16,
|
||||
.background = Color::blue(),
|
||||
.foreground = Color::white(),
|
||||
};
|
||||
|
||||
class AMOptionsView : public View {
|
||||
public:
|
||||
AMOptionsView(const Rect parent_rect, const Style* const style);
|
||||
public:
|
||||
AMOptionsView(Rect parent_rect, const Style* style);
|
||||
|
||||
private:
|
||||
Text label_config {
|
||||
{ 0 * 8, 0 * 16, 2 * 8, 1 * 16 },
|
||||
"BW",
|
||||
};
|
||||
private:
|
||||
Text label_config{
|
||||
{0 * 8, 0 * 16, 2 * 8, 1 * 16},
|
||||
"BW",
|
||||
};
|
||||
|
||||
OptionsField options_config {
|
||||
{ 3 * 8, 0 * 16 },
|
||||
5,
|
||||
{
|
||||
{ "DSB 9k ", 0 },
|
||||
{ "DSB 6k ", 0 },
|
||||
{ "USB+3k ", 0 },
|
||||
{ "LSB-3k ", 0 },
|
||||
{ "CW ", 0 },
|
||||
}
|
||||
};
|
||||
OptionsField options_config{
|
||||
{3 * 8, 0 * 16},
|
||||
6, // Max option length
|
||||
{
|
||||
// Using common messages from freqman_ui.cpp
|
||||
}};
|
||||
};
|
||||
|
||||
class NBFMOptionsView : public View {
|
||||
public:
|
||||
NBFMOptionsView(const Rect parent_rect, const Style* const style);
|
||||
public:
|
||||
NBFMOptionsView(Rect parent_rect, const Style* style);
|
||||
|
||||
private:
|
||||
Text label_config {
|
||||
{ 0 * 8, 0 * 16, 2 * 8, 1 * 16 },
|
||||
"BW",
|
||||
};
|
||||
OptionsField options_config {
|
||||
{ 3 * 8, 0 * 16 },
|
||||
4,
|
||||
{
|
||||
{ " 8k5", 0 },
|
||||
{ "11k ", 0 },
|
||||
{ "16k ", 0 },
|
||||
}
|
||||
};
|
||||
|
||||
Text text_squelch {
|
||||
{ 9 * 8, 0 * 16, 8 * 8, 1 * 16 },
|
||||
"SQ /99"
|
||||
};
|
||||
NumberField field_squelch {
|
||||
{ 12 * 8, 0 * 16 },
|
||||
2,
|
||||
{ 0, 99 },
|
||||
1,
|
||||
' ',
|
||||
};
|
||||
private:
|
||||
Text label_config{
|
||||
{0 * 8, 0 * 16, 2 * 8, 1 * 16},
|
||||
"BW",
|
||||
};
|
||||
OptionsField options_config{
|
||||
{3 * 8, 0 * 16},
|
||||
3, // Max option length
|
||||
{
|
||||
// Using common messages from freqman_ui.cpp
|
||||
}};
|
||||
|
||||
Text text_squelch{
|
||||
{7 * 8, 0 * 16, 8 * 8, 1 * 16},
|
||||
"SQ /99"};
|
||||
NumberField field_squelch{
|
||||
{10 * 8, 0 * 16},
|
||||
2,
|
||||
{0, 99},
|
||||
1,
|
||||
' ',
|
||||
};
|
||||
};
|
||||
|
||||
class WFMOptionsView : public View {
|
||||
public:
|
||||
WFMOptionsView(Rect parent_rect, const Style* style);
|
||||
|
||||
private:
|
||||
Text label_config{
|
||||
{0 * 8, 0 * 16, 2 * 8, 1 * 16},
|
||||
"BW",
|
||||
};
|
||||
OptionsField options_config{
|
||||
{3 * 8, 0 * 16},
|
||||
4, // Max option length
|
||||
{
|
||||
// Using common messages from freqman_ui.cpp
|
||||
}};
|
||||
};
|
||||
|
||||
class AnalogAudioView;
|
||||
|
||||
class SPECOptionsView : public View {
|
||||
public:
|
||||
SPECOptionsView(AnalogAudioView* view, const Rect parent_rect, const Style* const style);
|
||||
public:
|
||||
SPECOptionsView(AnalogAudioView* view, Rect parent_rect, const Style* style);
|
||||
|
||||
private:
|
||||
Text label_config {
|
||||
{ 0 * 8, 0 * 16, 2 * 8, 1 * 16 },
|
||||
"BW",
|
||||
};
|
||||
OptionsField options_config {
|
||||
{ 3 * 8, 0 * 16 },
|
||||
4,
|
||||
{
|
||||
{ "20m ", 20000000 },
|
||||
{ "10m ", 10000000 },
|
||||
{ " 5m ", 5000000 },
|
||||
{ " 2m ", 2000000 },
|
||||
{ " 1m ", 1000000 },
|
||||
{ "500k", 500000 },
|
||||
}
|
||||
};
|
||||
|
||||
Text text_speed {
|
||||
{ 9 * 8, 0 * 16, 8 * 8, 1 * 16 },
|
||||
"SP /63"
|
||||
};
|
||||
NumberField field_speed {
|
||||
{ 12 * 8, 0 * 16 },
|
||||
2,
|
||||
{ 0, 63 },
|
||||
1,
|
||||
' ',
|
||||
};
|
||||
private:
|
||||
Text label_config{
|
||||
{0 * 8, 0 * 16, 2 * 8, 1 * 16},
|
||||
"BW",
|
||||
};
|
||||
OptionsField options_config{
|
||||
{3 * 8, 0 * 16},
|
||||
4,
|
||||
{
|
||||
{"20m ", 20000000},
|
||||
{"10m ", 10000000},
|
||||
{" 5m ", 5000000},
|
||||
{" 2m ", 2000000},
|
||||
{" 1m ", 1000000},
|
||||
{"500k", 500000},
|
||||
{"100k", 100000},
|
||||
}};
|
||||
|
||||
Text text_speed{
|
||||
{9 * 8, 0 * 16, 8 * 8, 1 * 16},
|
||||
"SP /63"};
|
||||
NumberField field_speed{
|
||||
{12 * 8, 0 * 16},
|
||||
2,
|
||||
{0, 63},
|
||||
1,
|
||||
' ',
|
||||
};
|
||||
Text text_rx_cal{
|
||||
{19 * 8, 0 * 16, 11 * 8, 1 * 16}, // 18 (x col.) x char_size, 12 (length) x 8 blanking space to delete previous chars.
|
||||
"Rx_IQ_CAL "};
|
||||
NumberField field_rx_iq_phase_cal{
|
||||
{28 * 8, 0 * 16},
|
||||
2,
|
||||
{0, 63}, // 5 or 6 bits IQ CAL phase adjustment (range updated later)
|
||||
1,
|
||||
' ',
|
||||
};
|
||||
};
|
||||
|
||||
class AnalogAudioView : public View {
|
||||
public:
|
||||
AnalogAudioView(NavigationView& nav);
|
||||
~AnalogAudioView();
|
||||
public:
|
||||
AnalogAudioView(NavigationView& nav);
|
||||
AnalogAudioView(NavigationView& nav, ReceiverModel::settings_t override);
|
||||
~AnalogAudioView();
|
||||
|
||||
void on_hide() override;
|
||||
void set_parent_rect(Rect new_parent_rect) override;
|
||||
void focus() override;
|
||||
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
std::string title() const override { return "Audio RX"; };
|
||||
|
||||
void focus() override;
|
||||
size_t get_spec_bw_index();
|
||||
void set_spec_bw(size_t index, uint32_t bw);
|
||||
|
||||
std::string title() const override { return "Audio RX"; };
|
||||
uint16_t get_spec_trigger();
|
||||
void set_spec_trigger(uint16_t trigger);
|
||||
|
||||
size_t get_spec_bw_index();
|
||||
void set_spec_bw(size_t index, uint32_t bw);
|
||||
uint8_t get_spec_iq_phase_calibration_value();
|
||||
void set_spec_iq_phase_calibration_value(uint8_t cal_value);
|
||||
|
||||
uint16_t get_spec_trigger();
|
||||
void set_spec_trigger(uint16_t trigger);
|
||||
private:
|
||||
static constexpr ui::Dim header_height = 3 * 16;
|
||||
|
||||
private:
|
||||
static constexpr ui::Dim header_height = 3 * 16;
|
||||
NavigationView& nav_;
|
||||
RxRadioState radio_state_{};
|
||||
uint8_t iq_phase_calibration_value{15}; // initial default RX IQ phase calibration value , used for both max2837 & max2839
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_audio",
|
||||
app_settings::Mode::RX,
|
||||
{
|
||||
{"iq_phase_calibration"sv, &iq_phase_calibration_value}, // we are saving and restoring that CAL from Settings.
|
||||
}};
|
||||
|
||||
// app save settings
|
||||
std::app_settings settings { };
|
||||
std::app_settings::AppSettings app_settings { };
|
||||
const Rect options_view_rect{0 * 8, 1 * 16, 30 * 8, 1 * 16};
|
||||
const Rect nbfm_view_rect{0 * 8, 1 * 16, 18 * 8, 1 * 16};
|
||||
|
||||
const Rect options_view_rect { 0 * 8, 1 * 16, 30 * 8, 1 * 16 };
|
||||
const Rect nbfm_view_rect { 0 * 8, 1 * 16, 18 * 8, 1 * 16 };
|
||||
size_t spec_bw_index = 0;
|
||||
uint32_t spec_bw = 20000000;
|
||||
uint16_t spec_trigger = 63;
|
||||
|
||||
size_t spec_bw_index = 0;
|
||||
uint32_t spec_bw = 20000000;
|
||||
uint16_t spec_trigger = 63;
|
||||
RSSI rssi{
|
||||
{21 * 8, 0, 6 * 8, 4}};
|
||||
|
||||
NavigationView& nav_;
|
||||
//bool exit_on_squelch { false };
|
||||
|
||||
RSSI rssi {
|
||||
{ 21 * 8, 0, 6 * 8, 4 },
|
||||
};
|
||||
Channel channel{
|
||||
{21 * 8, 5, 6 * 8, 4}};
|
||||
|
||||
Channel channel {
|
||||
{ 21 * 8, 5, 6 * 8, 4 },
|
||||
};
|
||||
Audio audio{
|
||||
{21 * 8, 10, 6 * 8, 4}};
|
||||
|
||||
Audio audio {
|
||||
{ 21 * 8, 10, 6 * 8, 4 },
|
||||
};
|
||||
RxFrequencyField field_frequency{
|
||||
{5 * 8, 0 * 16},
|
||||
nav_};
|
||||
|
||||
FrequencyField field_frequency {
|
||||
{ 5 * 8, 0 * 16 },
|
||||
};
|
||||
LNAGainField field_lna{
|
||||
{15 * 8, 0 * 16}};
|
||||
|
||||
LNAGainField field_lna {
|
||||
{ 15 * 8, 0 * 16 }
|
||||
};
|
||||
VGAGainField field_vga{
|
||||
{18 * 8, 0 * 16}};
|
||||
|
||||
VGAGainField field_vga {
|
||||
{ 18 * 8, 0 * 16 }
|
||||
};
|
||||
OptionsField options_modulation{
|
||||
{0 * 8, 0 * 16},
|
||||
4,
|
||||
{
|
||||
{" AM ", toUType(ReceiverModel::Mode::AMAudio)},
|
||||
{"NFM ", toUType(ReceiverModel::Mode::NarrowbandFMAudio)},
|
||||
{"WFM ", toUType(ReceiverModel::Mode::WidebandFMAudio)},
|
||||
{"SPEC", toUType(ReceiverModel::Mode::SpectrumAnalysis)},
|
||||
}};
|
||||
|
||||
OptionsField options_modulation {
|
||||
{ 0 * 8, 0 * 16 },
|
||||
4,
|
||||
{
|
||||
{ " AM ", toUType(ReceiverModel::Mode::AMAudio) },
|
||||
{ "NFM ", toUType(ReceiverModel::Mode::NarrowbandFMAudio) },
|
||||
{ "WFM ", toUType(ReceiverModel::Mode::WidebandFMAudio) },
|
||||
{ "SPEC", toUType(ReceiverModel::Mode::SpectrumAnalysis) },
|
||||
}
|
||||
};
|
||||
AudioVolumeField field_volume{
|
||||
{28 * 8, 0 * 16}};
|
||||
|
||||
NumberField field_volume {
|
||||
{ 28 * 8, 0 * 16 },
|
||||
2,
|
||||
{ 0, 99 },
|
||||
1,
|
||||
' ',
|
||||
};
|
||||
|
||||
Text text_ctcss {
|
||||
{ 19 * 8, 1 * 16, 11 * 8, 1 * 16 },
|
||||
""
|
||||
};
|
||||
Text text_ctcss{
|
||||
{16 * 8, 1 * 16, 14 * 8, 1 * 16},
|
||||
""};
|
||||
|
||||
std::unique_ptr<Widget> options_widget { };
|
||||
std::unique_ptr<Widget> options_widget{};
|
||||
|
||||
RecordView record_view {
|
||||
{ 0 * 8, 2 * 16, 30 * 8, 1 * 16 },
|
||||
u"AUD",
|
||||
RecordView::FileType::WAV,
|
||||
4096,
|
||||
4
|
||||
};
|
||||
RecordView record_view{
|
||||
{0 * 8, 2 * 16, 30 * 8, 1 * 16},
|
||||
u"AUD",
|
||||
u"AUDIO",
|
||||
RecordView::FileType::WAV,
|
||||
4096,
|
||||
4};
|
||||
|
||||
spectrum::WaterfallWidget waterfall { true };
|
||||
spectrum::WaterfallView waterfall{true};
|
||||
|
||||
void on_tuning_frequency_changed(rf::Frequency f);
|
||||
void on_baseband_bandwidth_changed(uint32_t bandwidth_hz);
|
||||
void on_modulation_changed(const ReceiverModel::Mode modulation);
|
||||
void on_show_options_frequency();
|
||||
void on_show_options_rf_gain();
|
||||
void on_show_options_modulation();
|
||||
void on_frequency_step_changed(rf::Frequency f);
|
||||
void on_reference_ppm_correction_changed(int32_t v);
|
||||
void on_headphone_volume_changed(int32_t v);
|
||||
void on_edit_frequency();
|
||||
void on_baseband_bandwidth_changed(uint32_t bandwidth_hz);
|
||||
void on_modulation_changed(ReceiverModel::Mode modulation);
|
||||
void on_show_options_frequency();
|
||||
void on_show_options_rf_gain();
|
||||
void on_show_options_modulation();
|
||||
void on_frequency_step_changed(rf::Frequency f);
|
||||
void on_reference_ppm_correction_changed(int32_t v);
|
||||
|
||||
void remove_options_widget();
|
||||
void set_options_widget(std::unique_ptr<Widget> new_widget);
|
||||
void remove_options_widget();
|
||||
void set_options_widget(std::unique_ptr<Widget> new_widget);
|
||||
|
||||
void update_modulation(const ReceiverModel::Mode modulation);
|
||||
|
||||
//void squelched();
|
||||
void handle_coded_squelch(const uint32_t value);
|
||||
|
||||
/*MessageHandlerRegistration message_handler_squelch_signal {
|
||||
Message::ID::RequestSignal,
|
||||
[this](const Message* const p) {
|
||||
(void)p;
|
||||
this->squelched();
|
||||
}
|
||||
};*/
|
||||
|
||||
MessageHandlerRegistration message_handler_coded_squelch {
|
||||
Message::ID::CodedSquelch,
|
||||
[this](const Message* const p) {
|
||||
const auto message = *reinterpret_cast<const CodedSquelchMessage*>(p);
|
||||
this->handle_coded_squelch(message.value);
|
||||
}
|
||||
};
|
||||
void update_modulation(ReceiverModel::Mode modulation);
|
||||
|
||||
void handle_coded_squelch(uint32_t value);
|
||||
|
||||
void on_freqchg(int64_t freq);
|
||||
|
||||
MessageHandlerRegistration message_handler_coded_squelch{
|
||||
Message::ID::CodedSquelch,
|
||||
[this](const Message* p) {
|
||||
const auto message = *reinterpret_cast<const CodedSquelchMessage*>(p);
|
||||
this->handle_coded_squelch(message.value);
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_freqchg{
|
||||
Message::ID::FreqChangeCommand,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const FreqChangeCommandMessage*>(p);
|
||||
this->on_freqchg(message->freq);
|
||||
}};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
#endif/*__ANALOG_AUDIO_APP_H__*/
|
||||
#endif /*__ANALOG_AUDIO_APP_H__*/
|
||||
|
||||
@@ -1,254 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2018 Furrtek
|
||||
* Copyright (C) 2020 Shao
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "analog_tv_app.hpp"
|
||||
|
||||
#include "baseband_api.hpp"
|
||||
|
||||
#include "portapack.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
using namespace portapack;
|
||||
using namespace tonekey;
|
||||
|
||||
#include "audio.hpp"
|
||||
#include "file.hpp"
|
||||
|
||||
#include "utility.hpp"
|
||||
|
||||
#include "string_format.hpp"
|
||||
|
||||
namespace ui {
|
||||
|
||||
/* AnalogTvView *******************************************************/
|
||||
|
||||
AnalogTvView::AnalogTvView(
|
||||
NavigationView& nav
|
||||
) : nav_ (nav)
|
||||
{
|
||||
add_children({
|
||||
&rssi,
|
||||
&channel,
|
||||
&audio,
|
||||
&field_frequency,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&options_modulation,
|
||||
&field_volume,
|
||||
&tv
|
||||
});
|
||||
|
||||
// Set on_change before initialising the field
|
||||
field_frequency.on_change = [this](rf::Frequency f) {
|
||||
this->on_tuning_frequency_changed(f);
|
||||
};
|
||||
|
||||
// load app settings
|
||||
auto rc = settings.load("rx_tv", &app_settings);
|
||||
if(rc == SETTINGS_OK) {
|
||||
field_lna.set_value(app_settings.lna);
|
||||
field_vga.set_value(app_settings.vga);
|
||||
receiver_model.set_rf_amp(app_settings.rx_amp);
|
||||
field_frequency.set_value(app_settings.rx_frequency);
|
||||
}
|
||||
else field_frequency.set_value(receiver_model.tuning_frequency());
|
||||
|
||||
|
||||
field_frequency.set_step(receiver_model.frequency_step());
|
||||
field_frequency.on_edit = [this, &nav]() {
|
||||
// TODO: Provide separate modal method/scheme?
|
||||
auto new_view = nav.push<FrequencyKeypadView>(receiver_model.tuning_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
this->on_tuning_frequency_changed(f);
|
||||
this->field_frequency.set_value(f);
|
||||
};
|
||||
};
|
||||
|
||||
field_frequency.on_show_options = [this]() {
|
||||
this->on_show_options_frequency();
|
||||
};
|
||||
|
||||
field_lna.on_show_options = [this]() {
|
||||
this->on_show_options_rf_gain();
|
||||
};
|
||||
|
||||
field_vga.on_show_options = [this]() {
|
||||
this->on_show_options_rf_gain();
|
||||
};
|
||||
|
||||
const auto modulation = receiver_model.modulation();
|
||||
options_modulation.set_by_value(toUType(ReceiverModel::Mode::WidebandFMAudio));
|
||||
options_modulation.on_change = [this](size_t, OptionsField::value_t v) {
|
||||
this->on_modulation_changed(static_cast<ReceiverModel::Mode>(v));
|
||||
};
|
||||
options_modulation.on_show_options = [this]() {
|
||||
this->on_show_options_modulation();
|
||||
};
|
||||
|
||||
field_volume.set_value(0);
|
||||
field_volume.on_change = [this](int32_t v) {
|
||||
this->on_headphone_volume_changed(v);
|
||||
};
|
||||
|
||||
tv.on_select = [this](int32_t offset) {
|
||||
field_frequency.set_value(receiver_model.tuning_frequency() + offset);
|
||||
};
|
||||
|
||||
update_modulation(static_cast<ReceiverModel::Mode>(modulation));
|
||||
on_modulation_changed(ReceiverModel::Mode::WidebandFMAudio);
|
||||
}
|
||||
|
||||
AnalogTvView::~AnalogTvView() {
|
||||
|
||||
// save app settings
|
||||
app_settings.rx_frequency = field_frequency.value();
|
||||
settings.save("rx_tv", &app_settings);
|
||||
|
||||
// TODO: Manipulating audio codec here, and in ui_receiver.cpp. Good to do
|
||||
// both?
|
||||
audio::output::stop();
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
void AnalogTvView::on_hide() {
|
||||
// TODO: Terrible kludge because widget system doesn't notify Waterfall that
|
||||
// it's being shown or hidden.
|
||||
tv.on_hide();
|
||||
View::on_hide();
|
||||
}
|
||||
|
||||
void AnalogTvView::set_parent_rect(const Rect new_parent_rect) {
|
||||
View::set_parent_rect(new_parent_rect);
|
||||
|
||||
const ui::Rect tv_rect { 0, header_height, new_parent_rect.width(), new_parent_rect.height() - header_height };
|
||||
tv.set_parent_rect(tv_rect);
|
||||
}
|
||||
|
||||
void AnalogTvView::focus() {
|
||||
field_frequency.focus();
|
||||
}
|
||||
|
||||
void AnalogTvView::on_tuning_frequency_changed(rf::Frequency f) {
|
||||
receiver_model.set_tuning_frequency(f);
|
||||
}
|
||||
|
||||
void AnalogTvView::on_baseband_bandwidth_changed(uint32_t bandwidth_hz) {
|
||||
receiver_model.set_baseband_bandwidth(bandwidth_hz);
|
||||
}
|
||||
|
||||
void AnalogTvView::on_modulation_changed(const ReceiverModel::Mode modulation) {
|
||||
// TODO: Terrible kludge because widget system doesn't notify Waterfall that
|
||||
// it's being shown or hidden.
|
||||
tv.on_hide();
|
||||
update_modulation(modulation);
|
||||
on_show_options_modulation();
|
||||
tv.on_show();
|
||||
}
|
||||
|
||||
void AnalogTvView::remove_options_widget() {
|
||||
if( options_widget ) {
|
||||
remove_child(options_widget.get());
|
||||
options_widget.reset();
|
||||
}
|
||||
|
||||
field_lna.set_style(nullptr);
|
||||
options_modulation.set_style(nullptr);
|
||||
field_frequency.set_style(nullptr);
|
||||
}
|
||||
|
||||
void AnalogTvView::set_options_widget(std::unique_ptr<Widget> new_widget) {
|
||||
remove_options_widget();
|
||||
|
||||
if( new_widget ) {
|
||||
options_widget = std::move(new_widget);
|
||||
} else {
|
||||
// TODO: Lame hack to hide options view due to my bad paint/damage algorithm.
|
||||
options_widget = std::make_unique<Rectangle>(options_view_rect, style_options_group_new.background);
|
||||
}
|
||||
add_child(options_widget.get());
|
||||
}
|
||||
|
||||
void AnalogTvView::on_show_options_frequency() {
|
||||
auto widget = std::make_unique<FrequencyOptionsView>(options_view_rect, &style_options_group_new);
|
||||
|
||||
widget->set_step(receiver_model.frequency_step());
|
||||
widget->on_change_step = [this](rf::Frequency f) {
|
||||
this->on_frequency_step_changed(f);
|
||||
};
|
||||
widget->set_reference_ppm_correction(persistent_memory::correction_ppb() / 1000);
|
||||
widget->on_change_reference_ppm_correction = [this](int32_t v) {
|
||||
this->on_reference_ppm_correction_changed(v);
|
||||
};
|
||||
|
||||
set_options_widget(std::move(widget));
|
||||
field_frequency.set_style(&style_options_group_new);
|
||||
}
|
||||
|
||||
void AnalogTvView::on_show_options_rf_gain() {
|
||||
auto widget = std::make_unique<RadioGainOptionsView>(options_view_rect, &style_options_group_new);
|
||||
|
||||
set_options_widget(std::move(widget));
|
||||
field_lna.set_style(&style_options_group_new);
|
||||
}
|
||||
|
||||
void AnalogTvView::on_show_options_modulation() {
|
||||
std::unique_ptr<Widget> widget;
|
||||
|
||||
static_cast<ReceiverModel::Mode>(receiver_model.modulation());
|
||||
tv.show_audio_spectrum_view(true);
|
||||
|
||||
set_options_widget(std::move(widget));
|
||||
options_modulation.set_style(&style_options_group_new);
|
||||
}
|
||||
|
||||
void AnalogTvView::on_frequency_step_changed(rf::Frequency f) {
|
||||
receiver_model.set_frequency_step(f);
|
||||
field_frequency.set_step(f);
|
||||
}
|
||||
|
||||
void AnalogTvView::on_reference_ppm_correction_changed(int32_t v) {
|
||||
persistent_memory::set_correction_ppb(v * 1000);
|
||||
}
|
||||
|
||||
void AnalogTvView::on_headphone_volume_changed(int32_t v) {
|
||||
(void)v; //avoid warning
|
||||
//tv::TVView::set_headphone_volume(this,v);
|
||||
}
|
||||
|
||||
void AnalogTvView::update_modulation(const ReceiverModel::Mode modulation) {
|
||||
audio::output::mute();
|
||||
|
||||
baseband::shutdown();
|
||||
|
||||
portapack::spi_flash::image_tag_t image_tag;
|
||||
image_tag = portapack::spi_flash::image_tag_am_tv;
|
||||
|
||||
baseband::run_image(image_tag);
|
||||
|
||||
receiver_model.set_modulation(modulation);
|
||||
receiver_model.set_sampling_rate(2000000);
|
||||
receiver_model.set_baseband_bandwidth(2000000);
|
||||
receiver_model.enable();
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
@@ -1,137 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2018 Furrtek
|
||||
* Copyright (C) 2020 Shao
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __ANALOG_TV_APP_H__
|
||||
#define __ANALOG_TV_APP_H__
|
||||
|
||||
#include "receiver_model.hpp"
|
||||
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_tv.hpp"
|
||||
#include "ui_record_view.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "ui_font_fixed_8x16.hpp"
|
||||
|
||||
#include "tone_key.hpp"
|
||||
|
||||
namespace ui {
|
||||
|
||||
constexpr Style style_options_group_new {
|
||||
.font = font::fixed_8x16,
|
||||
.background = Color::blue(),
|
||||
.foreground = Color::white(),
|
||||
};
|
||||
|
||||
class AnalogTvView : public View {
|
||||
public:
|
||||
AnalogTvView(NavigationView& nav);
|
||||
~AnalogTvView();
|
||||
|
||||
void on_hide() override;
|
||||
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "AnalogTV RX"; };
|
||||
|
||||
private:
|
||||
static constexpr ui::Dim header_height = 3 * 16;
|
||||
|
||||
// app save settings
|
||||
std::app_settings settings { };
|
||||
std::app_settings::AppSettings app_settings { };
|
||||
|
||||
const Rect options_view_rect { 0 * 8, 1 * 16, 30 * 8, 1 * 16 };
|
||||
const Rect nbfm_view_rect { 0 * 8, 1 * 16, 18 * 8, 1 * 16 };
|
||||
|
||||
NavigationView& nav_;
|
||||
|
||||
RSSI rssi {
|
||||
{ 21 * 8, 0, 6 * 8, 4 },
|
||||
};
|
||||
|
||||
Channel channel {
|
||||
{ 21 * 8, 5, 6 * 8, 4 },
|
||||
};
|
||||
|
||||
Audio audio {
|
||||
{ 21 * 8, 10, 6 * 8, 4 },
|
||||
};
|
||||
|
||||
FrequencyField field_frequency {
|
||||
{ 5 * 8, 0 * 16 },
|
||||
};
|
||||
|
||||
LNAGainField field_lna {
|
||||
{ 15 * 8, 0 * 16 }
|
||||
};
|
||||
|
||||
VGAGainField field_vga {
|
||||
{ 18 * 8, 0 * 16 }
|
||||
};
|
||||
|
||||
OptionsField options_modulation {
|
||||
{ 0 * 8, 0 * 16 },
|
||||
4,
|
||||
{
|
||||
{ "TV ", toUType(ReceiverModel::Mode::WidebandFMAudio) },
|
||||
{ "TV ", toUType(ReceiverModel::Mode::WidebandFMAudio) },
|
||||
{ "TV ", toUType(ReceiverModel::Mode::WidebandFMAudio) },
|
||||
}
|
||||
};
|
||||
|
||||
NumberField field_volume {
|
||||
{ 27 * 8, 0 * 16 },
|
||||
3,
|
||||
{ 0, 255 },
|
||||
1,
|
||||
' ',
|
||||
};
|
||||
|
||||
std::unique_ptr<Widget> options_widget { };
|
||||
|
||||
tv::TVWidget tv { };
|
||||
|
||||
void on_tuning_frequency_changed(rf::Frequency f);
|
||||
void on_baseband_bandwidth_changed(uint32_t bandwidth_hz);
|
||||
void on_modulation_changed(const ReceiverModel::Mode modulation);
|
||||
void on_show_options_frequency();
|
||||
void on_show_options_rf_gain();
|
||||
void on_show_options_modulation();
|
||||
void on_frequency_step_changed(rf::Frequency f);
|
||||
void on_reference_ppm_correction_changed(int32_t v);
|
||||
void on_headphone_volume_changed(int32_t v);
|
||||
void on_edit_frequency();
|
||||
|
||||
void remove_options_widget();
|
||||
void set_options_widget(std::unique_ptr<Widget> new_widget);
|
||||
|
||||
void update_modulation(const ReceiverModel::Mode modulation);
|
||||
|
||||
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
#endif/*__ANALOG_TV_APP_H__*/
|
||||
@@ -0,0 +1,332 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2017 Furrtek
|
||||
* Copyright (C) 2023 TJ Baginski
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "ble_comm_app.hpp"
|
||||
|
||||
#include "ui_modemsetup.hpp"
|
||||
|
||||
#include "modems.hpp"
|
||||
#include "audio.hpp"
|
||||
#include "rtc_time.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include "ui_text.hpp"
|
||||
#include "file_path.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
using namespace modems;
|
||||
|
||||
void BLECommLogger::log_raw_data(const std::string& data) {
|
||||
log_file.write_entry(data);
|
||||
}
|
||||
|
||||
namespace ui {
|
||||
static std::uint64_t get_freq_by_channel_number(uint8_t channel_number) {
|
||||
uint64_t freq_hz;
|
||||
|
||||
switch (channel_number) {
|
||||
case 37:
|
||||
freq_hz = 2'402'000'000ull;
|
||||
break;
|
||||
case 38:
|
||||
freq_hz = 2'426'000'000ull;
|
||||
break;
|
||||
case 39:
|
||||
freq_hz = 2'480'000'000ull;
|
||||
break;
|
||||
case 0 ... 10:
|
||||
freq_hz = 2'404'000'000ull + channel_number * 2'000'000ull;
|
||||
break;
|
||||
case 11 ... 36:
|
||||
freq_hz = 2'428'000'000ull + (channel_number - 11) * 2'000'000ull;
|
||||
break;
|
||||
default:
|
||||
freq_hz = UINT64_MAX;
|
||||
}
|
||||
|
||||
return freq_hz;
|
||||
}
|
||||
|
||||
void BLECommView::focus() {
|
||||
options_channel.focus();
|
||||
}
|
||||
|
||||
BLECommView::BLECommView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
add_children({&rssi,
|
||||
&channel,
|
||||
&field_rf_amp,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&options_channel,
|
||||
&field_frequency,
|
||||
&label_send_adv,
|
||||
&button_send_adv,
|
||||
&check_log,
|
||||
&label_packets_sent,
|
||||
&text_packets_sent,
|
||||
&console});
|
||||
|
||||
field_frequency.set_step(0);
|
||||
|
||||
button_send_adv.on_select = [this](ImageButton&) {
|
||||
this->toggle();
|
||||
};
|
||||
|
||||
check_log.set_value(logging);
|
||||
|
||||
check_log.on_select = [this](Checkbox&, bool v) {
|
||||
str_log = "";
|
||||
logging = v;
|
||||
|
||||
if (logger && logging)
|
||||
logger->append(logs_dir.string() + "/BLELOG_" + to_string_timestamp(rtc_time::now()) + ".TXT");
|
||||
};
|
||||
|
||||
options_channel.on_change = [this](size_t, int32_t i) {
|
||||
// If we selected Auto don't do anything and Auto will handle changing.
|
||||
if (i == 40) {
|
||||
auto_channel = true;
|
||||
return;
|
||||
} else {
|
||||
auto_channel = false;
|
||||
}
|
||||
|
||||
field_frequency.set_value(get_freq_by_channel_number(i));
|
||||
channel_number_rx = i;
|
||||
channel_number_tx = i;
|
||||
};
|
||||
|
||||
options_channel.set_selected_index(3, true);
|
||||
|
||||
logger = std::make_unique<BLECommLogger>();
|
||||
|
||||
// Generate new random Mac Address upon each new startup.
|
||||
generateRandomMacAddress(randomMac);
|
||||
|
||||
// Setup Initial Advertise Packet.
|
||||
advertisePacket = build_adv_packet();
|
||||
}
|
||||
|
||||
void BLECommView::set_parent_rect(const Rect new_parent_rect) {
|
||||
View::set_parent_rect(new_parent_rect);
|
||||
const Rect content_rect{0, header_height, new_parent_rect.width(), new_parent_rect.height() - header_height};
|
||||
console.set_parent_rect(content_rect);
|
||||
}
|
||||
|
||||
BLECommView::~BLECommView() {
|
||||
receiver_model.disable();
|
||||
transmitter_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
bool BLECommView::in_tx_mode() const {
|
||||
return (bool)is_running_tx;
|
||||
}
|
||||
|
||||
void BLECommView::toggle() {
|
||||
if (in_tx_mode()) {
|
||||
sendAdvertisement(false);
|
||||
} else {
|
||||
sendAdvertisement(true);
|
||||
}
|
||||
}
|
||||
|
||||
BLETxPacket BLECommView::build_adv_packet() {
|
||||
BLETxPacket bleTxPacket;
|
||||
memset(&bleTxPacket, 0, sizeof(BLETxPacket));
|
||||
|
||||
std::string dataString = "11094861636b524620506f7274617061636b";
|
||||
|
||||
strncpy(bleTxPacket.macAddress, randomMac, 12);
|
||||
strncpy(bleTxPacket.advertisementData, dataString.c_str(), dataString.length());
|
||||
|
||||
// Duty cycle of 40% per 100ms advertisment periods.
|
||||
strncpy(bleTxPacket.packetCount, "80", 3);
|
||||
bleTxPacket.packet_count = 80;
|
||||
|
||||
bleTxPacket.packetType = PKT_TYPE_DISCOVERY;
|
||||
|
||||
return bleTxPacket;
|
||||
}
|
||||
|
||||
void BLECommView::sendAdvertisement(bool enable) {
|
||||
if (enable) {
|
||||
startTx(advertisePacket);
|
||||
is_adv = true;
|
||||
} else {
|
||||
is_adv = false;
|
||||
stopTx();
|
||||
}
|
||||
}
|
||||
|
||||
void BLECommView::startTx(BLETxPacket packetToSend) {
|
||||
int randomChannel = channel_number_tx;
|
||||
|
||||
if (auto_channel) {
|
||||
int min = 37;
|
||||
int max = 39;
|
||||
|
||||
randomChannel = min + std::rand() % (max - min + 1);
|
||||
|
||||
field_frequency.set_value(get_freq_by_channel_number(randomChannel));
|
||||
}
|
||||
|
||||
if (!in_tx_mode()) {
|
||||
switch_rx_tx(false);
|
||||
|
||||
currentPacket = packetToSend;
|
||||
packet_counter = currentPacket.packet_count;
|
||||
|
||||
button_send_adv.set_bitmap(&bitmap_stop);
|
||||
baseband::set_btletx(randomChannel, currentPacket.macAddress, currentPacket.advertisementData, currentPacket.packetType);
|
||||
transmitter_model.set_tx_gain(47);
|
||||
transmitter_model.enable();
|
||||
|
||||
is_running_tx = true;
|
||||
} else {
|
||||
baseband::set_btletx(randomChannel, currentPacket.macAddress, currentPacket.advertisementData, currentPacket.packetType);
|
||||
}
|
||||
|
||||
if ((packet_counter % 10) == 0) {
|
||||
text_packets_sent.set(to_string_dec_uint(packet_counter));
|
||||
}
|
||||
|
||||
is_sending = true;
|
||||
|
||||
packet_counter--;
|
||||
}
|
||||
|
||||
void BLECommView::stopTx() {
|
||||
button_send_adv.set_bitmap(&bitmap_play);
|
||||
text_packets_sent.set(to_string_dec_uint(packet_counter));
|
||||
|
||||
switch_rx_tx(true);
|
||||
|
||||
baseband::set_btlerx(channel_number_rx);
|
||||
receiver_model.enable();
|
||||
|
||||
is_running_tx = false;
|
||||
}
|
||||
|
||||
void BLECommView::switch_rx_tx(bool inRxMode) {
|
||||
if (inRxMode) {
|
||||
// Start Rx
|
||||
transmitter_model.disable();
|
||||
baseband::shutdown();
|
||||
baseband::run_image(portapack::spi_flash::image_tag_btle_rx);
|
||||
} else {
|
||||
// Start Tx
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
baseband::run_image(portapack::spi_flash::image_tag_btle_tx);
|
||||
}
|
||||
}
|
||||
|
||||
void BLECommView::on_data(BlePacketData* packet) {
|
||||
parse_received_packet(packet, (ADV_PDU_TYPE)packet->type);
|
||||
}
|
||||
|
||||
// called each 1/60th of second, so 6 = 100ms
|
||||
void BLECommView::on_timer() {
|
||||
// Send advertise burst only once every 100ms
|
||||
if (++timer_counter == timer_period) {
|
||||
timer_counter = 0;
|
||||
|
||||
if (!is_adv) {
|
||||
sendAdvertisement(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BLECommView::on_tx_progress(const bool done) {
|
||||
if (done) {
|
||||
if (in_tx_mode()) {
|
||||
is_sending = false;
|
||||
|
||||
if (packet_counter == 0) {
|
||||
if (is_adv) {
|
||||
sendAdvertisement(false);
|
||||
} else {
|
||||
stopTx();
|
||||
}
|
||||
} else {
|
||||
startTx(currentPacket);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BLECommView::parse_received_packet(const BlePacketData* packet, ADV_PDU_TYPE pdu_type) {
|
||||
std::string data_string;
|
||||
|
||||
int i;
|
||||
|
||||
for (i = 0; i < packet->dataLen; i++) {
|
||||
data_string += to_string_hex(packet->data[i], 2);
|
||||
}
|
||||
|
||||
receivedPacket.dbValue = packet->max_dB;
|
||||
receivedPacket.timestamp = to_string_timestamp(rtc_time::now());
|
||||
receivedPacket.dataString = data_string;
|
||||
|
||||
receivedPacket.packetData.type = packet->type;
|
||||
receivedPacket.packetData.size = packet->size;
|
||||
receivedPacket.packetData.dataLen = packet->dataLen;
|
||||
|
||||
receivedPacket.packetData.macAddress[0] = packet->macAddress[0];
|
||||
receivedPacket.packetData.macAddress[1] = packet->macAddress[1];
|
||||
receivedPacket.packetData.macAddress[2] = packet->macAddress[2];
|
||||
receivedPacket.packetData.macAddress[3] = packet->macAddress[3];
|
||||
receivedPacket.packetData.macAddress[4] = packet->macAddress[4];
|
||||
receivedPacket.packetData.macAddress[5] = packet->macAddress[5];
|
||||
|
||||
receivedPacket.numHits++;
|
||||
|
||||
for (int i = 0; i < packet->dataLen; i++) {
|
||||
receivedPacket.packetData.data[i] = packet->data[i];
|
||||
}
|
||||
|
||||
if (pdu_type == SCAN_REQ || pdu_type == CONNECT_REQ) {
|
||||
ADV_PDU_PAYLOAD_TYPE_1_3* directed_mac_data = (ADV_PDU_PAYLOAD_TYPE_1_3*)packet->data;
|
||||
|
||||
std::reverse(directed_mac_data->A1, directed_mac_data->A1 + 6);
|
||||
console.clear(true);
|
||||
std::string str_console = "";
|
||||
std::string pduTypeStr = "";
|
||||
|
||||
if (pdu_type == SCAN_REQ) {
|
||||
pduTypeStr += "SCAN_REQ";
|
||||
} else if (pdu_type == CONNECT_REQ) {
|
||||
pduTypeStr += "CONNECT_REQ";
|
||||
}
|
||||
|
||||
str_console += "PACKET TYPE:" + pduTypeStr + "\n";
|
||||
str_console += "MY MAC:" + to_string_formatted_mac_address(randomMac) + "\n";
|
||||
str_console += "SCAN MAC:" + to_string_mac_address(directed_mac_data->A1, 6, false) + "\n";
|
||||
console.write(str_console);
|
||||
}
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2017 Furrtek
|
||||
* Copyright (C) 2023 TJ Baginski
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __BLE_COMM_APP_H__
|
||||
#define __BLE_COMM_APP_H__
|
||||
|
||||
#include "ble_rx_app.hpp"
|
||||
#include "ble_tx_app.hpp"
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_transmitter.hpp"
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "ui_record_view.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "log_file.hpp"
|
||||
#include "utility.hpp"
|
||||
|
||||
#include "recent_entries.hpp"
|
||||
|
||||
class BLECommLogger {
|
||||
public:
|
||||
Optional<File::Error> append(const std::filesystem::path& filename) {
|
||||
return log_file.append(filename);
|
||||
}
|
||||
|
||||
void log_raw_data(const std::string& data);
|
||||
|
||||
private:
|
||||
LogFile log_file{};
|
||||
};
|
||||
|
||||
namespace ui {
|
||||
class BLECommView : public View {
|
||||
public:
|
||||
BLECommView(NavigationView& nav);
|
||||
~BLECommView();
|
||||
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
void paint(Painter&) override{};
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "BLE Comm"; };
|
||||
|
||||
private:
|
||||
BLETxPacket build_adv_packet();
|
||||
void switch_rx_tx(bool inRxMode);
|
||||
void startTx(BLETxPacket packetToSend);
|
||||
void stopTx();
|
||||
void toggle();
|
||||
bool in_tx_mode() const;
|
||||
void on_timer();
|
||||
void on_data(BlePacketData* packetData);
|
||||
void on_tx_progress(const bool done);
|
||||
void parse_received_packet(const BlePacketData* packet, ADV_PDU_TYPE pdu_type);
|
||||
void sendAdvertisement(bool enable);
|
||||
|
||||
NavigationView& nav_;
|
||||
|
||||
RxRadioState radio_state_rx_{
|
||||
2'402'000'000 /* frequency */,
|
||||
4'000'000 /* bandwidth */,
|
||||
4'000'000 /* sampling rate */,
|
||||
ReceiverModel::Mode::WidebandFMAudio};
|
||||
|
||||
TxRadioState radio_state_tx_{
|
||||
2'402'000'000 /* frequency */,
|
||||
4'000'000 /* bandwidth */,
|
||||
4'000'000 /* sampling rate */
|
||||
};
|
||||
|
||||
app_settings::SettingsManager settings_{
|
||||
"BLE Comm Tx", app_settings::Mode::RX_TX};
|
||||
|
||||
uint8_t console_color{0};
|
||||
uint32_t prev_value{0};
|
||||
|
||||
uint8_t channel_number_tx = 37;
|
||||
uint8_t channel_number_rx = 37;
|
||||
bool auto_channel = false;
|
||||
|
||||
char randomMac[13] = "010203040506";
|
||||
|
||||
bool is_running_tx = false;
|
||||
bool is_sending = false;
|
||||
bool is_adv = false;
|
||||
|
||||
int16_t timer_period{6}; // Delay each packet by 16ms.
|
||||
int16_t timer_counter = 0;
|
||||
int16_t timer_rx_counter = 0;
|
||||
int16_t timer_rx_period{12}; // Poll Rx for at least 200ms. (TBD)
|
||||
|
||||
uint32_t packet_counter{0};
|
||||
BLETxPacket advertisePacket{};
|
||||
BLETxPacket currentPacket{};
|
||||
BleRecentEntry receivedPacket{};
|
||||
|
||||
static constexpr auto header_height = 5 * 16;
|
||||
|
||||
OptionsField options_channel{
|
||||
{0 * 8, 0 * 8},
|
||||
5,
|
||||
{{"Ch.37 ", 37},
|
||||
{"Ch.38", 38},
|
||||
{"Ch.39", 39},
|
||||
{"Auto", 40}}};
|
||||
|
||||
RxFrequencyField field_frequency{
|
||||
{6 * 8, 0 * 16},
|
||||
nav_};
|
||||
|
||||
RFAmpField field_rf_amp{
|
||||
{16 * 8, 0 * 16}};
|
||||
|
||||
LNAGainField field_lna{
|
||||
{18 * 8, 0 * 16}};
|
||||
|
||||
VGAGainField field_vga{
|
||||
{21 * 8, 0 * 16}};
|
||||
|
||||
RSSI rssi{
|
||||
{24 * 8, 0, 6 * 8, 4}};
|
||||
|
||||
Channel channel{
|
||||
{24 * 8, 5, 6 * 8, 4}};
|
||||
|
||||
Labels label_send_adv{
|
||||
{{0 * 8, 2 * 8}, "Send Advertisement:", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
ImageButton button_send_adv{
|
||||
{21 * 8, 1 * 16, 10 * 8, 2 * 16},
|
||||
&bitmap_play,
|
||||
Theme::getInstance()->fg_green->foreground,
|
||||
Theme::getInstance()->fg_green->background};
|
||||
|
||||
Checkbox check_log{
|
||||
{24 * 8, 2 * 8},
|
||||
3,
|
||||
"Log",
|
||||
true};
|
||||
|
||||
Labels label_packets_sent{
|
||||
{{0 * 8, 4 * 8}, "Packets Left:", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
Text text_packets_sent{
|
||||
{13 * 8, 2 * 16, 12 * 8, 16},
|
||||
"-"};
|
||||
|
||||
Console console{
|
||||
{0, 4 * 16, 240, 240}};
|
||||
|
||||
std::string str_log{""};
|
||||
bool logging{false};
|
||||
|
||||
std::unique_ptr<BLECommLogger> logger{};
|
||||
|
||||
MessageHandlerRegistration message_handler_packet{
|
||||
Message::ID::BlePacket,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const BLEPacketMessage*>(p);
|
||||
this->on_data(message->packet);
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_tx_progress{
|
||||
Message::ID::TXProgress,
|
||||
[this](const Message* const p) {
|
||||
const auto message = *reinterpret_cast<const TXProgressMessage*>(p);
|
||||
this->on_tx_progress(message.done);
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_frame_sync{
|
||||
Message::ID::DisplayFrameSync,
|
||||
[this](const Message* const) {
|
||||
this->on_timer();
|
||||
}};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
#endif /*__UI_AFSK_RX_H__*/
|
||||
@@ -0,0 +1,962 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2017 Furrtek
|
||||
* Copyright (C) 2023 TJ Baginski
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "ble_rx_app.hpp"
|
||||
#include "ble_rx_app.hpp"
|
||||
#include "ui_modemsetup.hpp"
|
||||
|
||||
#include "modems.hpp"
|
||||
#include "audio.hpp"
|
||||
#include "io_file.hpp"
|
||||
#include "rtc_time.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include "ui_fileman.hpp"
|
||||
#include "ui_textentry.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
using namespace modems;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
void BLELogger::log_raw_data(const std::string& data) {
|
||||
log_file.write_entry(data);
|
||||
}
|
||||
|
||||
std::string pad_string_with_spaces(int snakes) {
|
||||
std::string paddedStr(snakes, ' ');
|
||||
return paddedStr;
|
||||
}
|
||||
|
||||
uint64_t copy_mac_address_to_uint64(const uint8_t* macAddress) {
|
||||
uint64_t result = 0;
|
||||
|
||||
// Copy each byte of the MAC address to the corresponding byte in the uint64_t.
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
result |= static_cast<uint64_t>(macAddress[i]) << ((5 - i) * 8);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void reverse_byte_array(uint8_t* arr, int length) {
|
||||
int start = 0;
|
||||
int end = length - 1;
|
||||
|
||||
while (start < end) {
|
||||
// Swap elements at start and end
|
||||
uint8_t temp = arr[start];
|
||||
arr[start] = arr[end];
|
||||
arr[end] = temp;
|
||||
|
||||
// Move the indices towards the center
|
||||
start++;
|
||||
end--;
|
||||
}
|
||||
}
|
||||
|
||||
namespace ui {
|
||||
|
||||
std::string pdu_type_to_string(ADV_PDU_TYPE type) {
|
||||
std::string pduTypeStr = "";
|
||||
|
||||
switch (type) {
|
||||
case ADV_IND:
|
||||
pduTypeStr += "ADV_IND";
|
||||
break;
|
||||
case ADV_DIRECT_IND:
|
||||
pduTypeStr += "ADV_DIRECT_IND";
|
||||
break;
|
||||
case ADV_NONCONN_IND:
|
||||
pduTypeStr += "ADV_NONCONN_IND";
|
||||
break;
|
||||
case SCAN_REQ:
|
||||
pduTypeStr += "SCAN_REQ";
|
||||
break;
|
||||
case SCAN_RSP:
|
||||
pduTypeStr += "SCAN_RSP";
|
||||
break;
|
||||
case CONNECT_REQ:
|
||||
pduTypeStr += "CONNECT_REQ";
|
||||
break;
|
||||
case ADV_SCAN_IND:
|
||||
pduTypeStr += "ADV_SCAN_IND";
|
||||
break;
|
||||
case RESERVED0:
|
||||
case RESERVED1:
|
||||
case RESERVED2:
|
||||
case RESERVED3:
|
||||
case RESERVED4:
|
||||
case RESERVED5:
|
||||
case RESERVED6:
|
||||
case RESERVED7:
|
||||
case RESERVED8:
|
||||
pduTypeStr += "RESERVED";
|
||||
break;
|
||||
default:
|
||||
pduTypeStr += "UNKNOWN";
|
||||
break;
|
||||
}
|
||||
|
||||
return pduTypeStr;
|
||||
}
|
||||
|
||||
template <>
|
||||
void RecentEntriesTable<BleRecentEntries>::draw(
|
||||
const Entry& entry,
|
||||
const Rect& target_rect,
|
||||
Painter& painter,
|
||||
const Style& style) {
|
||||
std::string line{};
|
||||
line.reserve(30);
|
||||
|
||||
if (!entry.nameString.empty() && entry.include_name) {
|
||||
line = entry.nameString;
|
||||
|
||||
if (line.length() < 17) {
|
||||
line += pad_string_with_spaces(17 - line.length());
|
||||
} else {
|
||||
line = truncate(line, 17);
|
||||
}
|
||||
} else {
|
||||
line = to_string_mac_address(entry.packetData.macAddress, 6, false);
|
||||
}
|
||||
|
||||
// Pushing single digit values down right justified.
|
||||
std::string hitsStr = to_string_dec_int(entry.numHits);
|
||||
int hitsDigits = hitsStr.length();
|
||||
uint8_t hits_spacing = 8 - hitsDigits;
|
||||
|
||||
// Pushing single digit values down right justified.
|
||||
std::string dbStr = to_string_dec_int(entry.dbValue);
|
||||
int dbDigits = dbStr.length();
|
||||
uint8_t db_spacing = 5 - dbDigits;
|
||||
|
||||
line += pad_string_with_spaces(hits_spacing) + hitsStr;
|
||||
|
||||
line += pad_string_with_spaces(db_spacing) + dbStr;
|
||||
|
||||
line.resize(target_rect.width() / 8, ' ');
|
||||
painter.draw_string(target_rect.location(), style, line);
|
||||
}
|
||||
|
||||
BleRecentEntryDetailView::BleRecentEntryDetailView(NavigationView& nav, const BleRecentEntry& entry)
|
||||
: nav_{nav},
|
||||
entry_{entry} {
|
||||
add_children({&button_done,
|
||||
&button_send,
|
||||
&button_save,
|
||||
&label_mac_address,
|
||||
&text_mac_address,
|
||||
&label_pdu_type,
|
||||
&text_pdu_type,
|
||||
&labels});
|
||||
|
||||
text_mac_address.set(to_string_mac_address(entry.packetData.macAddress, 6, false));
|
||||
text_pdu_type.set(pdu_type_to_string(entry.pduType));
|
||||
|
||||
button_done.on_select = [&nav](const ui::Button&) {
|
||||
nav.pop();
|
||||
};
|
||||
|
||||
button_send.on_select = [this, &nav](const ui::Button&) {
|
||||
auto packetToSend = build_packet();
|
||||
nav.set_on_pop([packetToSend, &nav]() {
|
||||
nav.replace<BLETxView>(packetToSend);
|
||||
});
|
||||
nav.pop();
|
||||
};
|
||||
|
||||
button_save.on_select = [this, &nav](const ui::Button&) {
|
||||
auto packetToSave = build_packet();
|
||||
|
||||
packetFileBuffer = "";
|
||||
text_prompt(
|
||||
nav,
|
||||
packetFileBuffer,
|
||||
64,
|
||||
[this, packetToSave](std::string& buffer) {
|
||||
on_save_file(buffer, packetToSave);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
void BleRecentEntryDetailView::on_save_file(const std::string value, BLETxPacket packetToSave) {
|
||||
ensure_directory(packet_save_path);
|
||||
auto folder = packet_save_path.parent_path();
|
||||
auto ext = packet_save_path.extension();
|
||||
auto new_path = folder / value + ext;
|
||||
|
||||
saveFile(new_path, packetToSave);
|
||||
}
|
||||
|
||||
bool BleRecentEntryDetailView::saveFile(const std::filesystem::path& path, BLETxPacket packetToSave) {
|
||||
File f;
|
||||
auto error = f.create(path);
|
||||
if (error)
|
||||
return false;
|
||||
|
||||
std::string macAddressStr = packetToSave.macAddress;
|
||||
std::string advertisementDataStr = packetToSave.advertisementData;
|
||||
std::string packetCountStr = packetToSave.packetCount;
|
||||
|
||||
std::string packetString = macAddressStr + ' ' + advertisementDataStr + ' ' + packetCountStr;
|
||||
|
||||
f.write(packetString.c_str(), packetString.length());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void BleRecentEntryDetailView::update_data() {
|
||||
}
|
||||
|
||||
void BleRecentEntryDetailView::focus() {
|
||||
button_done.focus();
|
||||
}
|
||||
|
||||
Rect BleRecentEntryDetailView::draw_field(
|
||||
Painter& painter,
|
||||
const Rect& draw_rect,
|
||||
const Style& style,
|
||||
const std::string& label,
|
||||
const std::string& value) {
|
||||
const int label_length_max = 4;
|
||||
|
||||
painter.draw_string(Point{draw_rect.left(), draw_rect.top()}, style, label);
|
||||
painter.draw_string(Point{draw_rect.left() + (label_length_max + 1) * 8, draw_rect.top()}, style, value);
|
||||
|
||||
return {draw_rect.left(), draw_rect.top() + draw_rect.height(), draw_rect.width(), draw_rect.height()};
|
||||
}
|
||||
|
||||
void BleRecentEntryDetailView::paint(Painter& painter) {
|
||||
View::paint(painter);
|
||||
|
||||
const auto s = style();
|
||||
const auto rect = screen_rect();
|
||||
|
||||
auto field_rect = Rect{rect.left(), rect.top() + 64, rect.width(), 16};
|
||||
|
||||
uint8_t type[total_data_lines];
|
||||
uint8_t length[total_data_lines];
|
||||
uint8_t data[total_data_lines][40];
|
||||
|
||||
int currentByte = 0;
|
||||
int currentPacket = 0;
|
||||
int i = 0;
|
||||
int j = 0;
|
||||
int k = 0;
|
||||
|
||||
switch (entry_.pduType) {
|
||||
case ADV_IND:
|
||||
case ADV_NONCONN_IND:
|
||||
case SCAN_RSP:
|
||||
case ADV_SCAN_IND: {
|
||||
ADV_PDU_PAYLOAD_TYPE_0_2_4_6* advertiseData = (ADV_PDU_PAYLOAD_TYPE_0_2_4_6*)entry_.packetData.data;
|
||||
|
||||
for (currentByte = 0; (currentByte < entry_.packetData.dataLen) && (currentPacket < total_data_lines);) {
|
||||
length[currentPacket] = advertiseData->Data[currentByte++];
|
||||
type[currentPacket] = advertiseData->Data[currentByte++];
|
||||
|
||||
// Subtract 1 because type is part of the length.
|
||||
for (i = 0; i < length[currentPacket] - 1; i++) {
|
||||
data[currentPacket][i] = advertiseData->Data[currentByte++];
|
||||
}
|
||||
|
||||
currentPacket++;
|
||||
}
|
||||
|
||||
for (i = 0; i < currentPacket; i++) {
|
||||
uint8_t number_data_lines = ceil((float)(length[i] - 1) / 10.0);
|
||||
uint8_t current_line = 0;
|
||||
std::array<std::string, total_data_lines> data_strings{};
|
||||
|
||||
for (j = 0; (j < (number_data_lines * 10)) && (j < length[i] - 1); j++) {
|
||||
if ((j / 10) != current_line) {
|
||||
current_line++;
|
||||
}
|
||||
|
||||
data_strings[current_line] += to_string_hex(data[i][j], 2);
|
||||
}
|
||||
|
||||
// Read the type back to the total length.
|
||||
field_rect = draw_field(painter, field_rect, s, to_string_hex(length[i]), to_string_hex(type[i]) + pad_string_with_spaces(3) + data_strings[0]);
|
||||
|
||||
if (number_data_lines > 1) {
|
||||
for (k = 1; k < number_data_lines; k++) {
|
||||
if (!data_strings[k].empty()) {
|
||||
field_rect = draw_field(painter, field_rect, s, "", pad_string_with_spaces(5) + data_strings[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} break;
|
||||
|
||||
case ADV_DIRECT_IND:
|
||||
case SCAN_REQ: {
|
||||
ADV_PDU_PAYLOAD_TYPE_1_3* directed_mac_data = (ADV_PDU_PAYLOAD_TYPE_1_3*)entry_.packetData.data;
|
||||
uint8_t type = 0xFF;
|
||||
field_rect = draw_field(painter, field_rect, s, to_string_hex(entry_.packetData.dataLen), to_string_hex(type) + pad_string_with_spaces(3) + to_string_mac_address(directed_mac_data->A1, 6, false));
|
||||
} break;
|
||||
|
||||
case CONNECT_REQ:
|
||||
default: {
|
||||
uint8_t type = 0xFF;
|
||||
|
||||
// TODO: Display Connect Request Information. For right now just printing full hex data.
|
||||
// This struct will eventually be used to break apart containing data of Connect Request.
|
||||
// ADV_PDU_PAYLOAD_TYPE_5 * connect_req = (ADV_PDU_PAYLOAD_TYPE_5 *)entry_.packetData.data;
|
||||
|
||||
for (currentByte = 0; (currentByte < entry_.packetData.dataLen); currentByte++) {
|
||||
data[0][currentByte] = entry_.packetData.data[currentByte];
|
||||
}
|
||||
|
||||
uint8_t number_data_lines = ceil((float)entry_.packetData.dataLen / 10.0);
|
||||
uint8_t current_line = 0;
|
||||
std::array<std::string, total_data_lines> data_strings{};
|
||||
|
||||
for (j = 0; (j < (number_data_lines * 10)) && (j < entry_.packetData.dataLen); j++) {
|
||||
if ((j / 10) != current_line) {
|
||||
current_line++;
|
||||
}
|
||||
|
||||
data_strings[current_line] += to_string_hex(data[0][j], 2);
|
||||
}
|
||||
|
||||
field_rect = draw_field(painter, field_rect, s, to_string_hex(entry_.packetData.dataLen), to_string_hex(type) + pad_string_with_spaces(3) + data_strings[0]);
|
||||
|
||||
if (number_data_lines > 1) {
|
||||
for (k = 1; k < number_data_lines; k++) {
|
||||
if (!data_strings[k].empty()) {
|
||||
field_rect = draw_field(painter, field_rect, s, "", pad_string_with_spaces(5) + data_strings[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
void BleRecentEntryDetailView::set_entry(const BleRecentEntry& entry) {
|
||||
entry_ = entry;
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
BLETxPacket BleRecentEntryDetailView::build_packet() {
|
||||
BLETxPacket bleTxPacket;
|
||||
memset(&bleTxPacket, 0, sizeof(BLETxPacket));
|
||||
|
||||
std::string macAddressStr = to_string_mac_address(entry_.packetData.macAddress, 6, true);
|
||||
|
||||
strncpy(bleTxPacket.macAddress, macAddressStr.c_str(), 12);
|
||||
strncpy(bleTxPacket.advertisementData, entry_.dataString.c_str(), entry_.packetData.dataLen * 2);
|
||||
strncpy(bleTxPacket.packetCount, "50", 3);
|
||||
bleTxPacket.packet_count = 50;
|
||||
|
||||
return bleTxPacket;
|
||||
}
|
||||
|
||||
static std::uint64_t get_freq_by_channel_number(uint8_t channel_number) {
|
||||
uint64_t freq_hz;
|
||||
|
||||
switch (channel_number) {
|
||||
case 37:
|
||||
freq_hz = 2'402'000'000ull;
|
||||
break;
|
||||
case 38:
|
||||
freq_hz = 2'426'000'000ull;
|
||||
break;
|
||||
case 39:
|
||||
freq_hz = 2'480'000'000ull;
|
||||
break;
|
||||
case 0 ... 10:
|
||||
freq_hz = 2'404'000'000ull + channel_number * 2'000'000ull;
|
||||
break;
|
||||
case 11 ... 36:
|
||||
freq_hz = 2'428'000'000ull + (channel_number - 11) * 2'000'000ull;
|
||||
break;
|
||||
default:
|
||||
freq_hz = UINT64_MAX;
|
||||
}
|
||||
|
||||
return freq_hz;
|
||||
}
|
||||
|
||||
void BLERxView::focus() {
|
||||
options_channel.focus();
|
||||
}
|
||||
|
||||
void BLERxView::file_error() {
|
||||
nav_.display_modal("Error", "File read error.");
|
||||
}
|
||||
|
||||
BLERxView::BLERxView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_btle_rx);
|
||||
|
||||
add_children({&rssi,
|
||||
&channel,
|
||||
&field_rf_amp,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&options_channel,
|
||||
&field_frequency,
|
||||
&check_log,
|
||||
&button_find,
|
||||
&check_name,
|
||||
&label_sort,
|
||||
&options_sort,
|
||||
&label_found,
|
||||
&text_found_count,
|
||||
&check_serial_log,
|
||||
&button_filter,
|
||||
&options_filter,
|
||||
&button_save_list,
|
||||
&button_clear_list,
|
||||
&button_switch,
|
||||
&recent_entries_view});
|
||||
|
||||
recent_entries_view.on_select = [this](const BleRecentEntry& entry) {
|
||||
nav_.push<BleRecentEntryDetailView>(entry);
|
||||
};
|
||||
|
||||
check_serial_log.on_select = [this](Checkbox&, bool v) {
|
||||
serial_logging = v;
|
||||
if (v) {
|
||||
usb_serial_thread = std::make_unique<UsbSerialThread>();
|
||||
} else {
|
||||
usb_serial_thread.reset();
|
||||
}
|
||||
};
|
||||
check_serial_log.set_value(serial_logging);
|
||||
|
||||
ensure_directory(find_packet_path);
|
||||
ensure_directory(log_packets_path);
|
||||
ensure_directory(packet_save_path);
|
||||
|
||||
filterBuffer = filter;
|
||||
|
||||
button_filter.on_select = [this](Button&) {
|
||||
text_prompt(
|
||||
nav_,
|
||||
filterBuffer,
|
||||
64,
|
||||
[this](std::string& buffer) {
|
||||
on_filter_change(buffer);
|
||||
});
|
||||
};
|
||||
|
||||
logger = std::make_unique<BLELogger>();
|
||||
|
||||
check_log.on_select = [this](Checkbox&, bool v) {
|
||||
str_log = "";
|
||||
logging = v;
|
||||
|
||||
if (logger && logging)
|
||||
logger->append(blerx_dir.string() + "/Logs/BLELOG_" + to_string_timestamp(rtc_time::now()) + ".TXT");
|
||||
};
|
||||
check_log.set_value(logging);
|
||||
|
||||
button_save_list.on_select = [this, &nav](const ui::Button&) {
|
||||
listFileBuffer = "";
|
||||
text_prompt(
|
||||
nav,
|
||||
listFileBuffer,
|
||||
64,
|
||||
[this](std::string& buffer) {
|
||||
on_save_file(buffer);
|
||||
});
|
||||
};
|
||||
|
||||
button_clear_list.on_select = [this](Button&) {
|
||||
recent.clear();
|
||||
};
|
||||
|
||||
button_switch.on_select = [&nav](Button&) {
|
||||
nav.replace<BLETxView>();
|
||||
};
|
||||
|
||||
field_frequency.set_step(0);
|
||||
|
||||
check_name.set_value(name_enable);
|
||||
|
||||
check_name.on_select = [this](Checkbox&, bool v) {
|
||||
name_enable = v;
|
||||
// update the include_name instance variable value of each entry in recent entries
|
||||
setAllMembersToValue(recent, &BleRecentEntry::include_name, v);
|
||||
recent_entries_view.set_dirty();
|
||||
};
|
||||
|
||||
options_channel.on_change = [this](size_t index, int32_t v) {
|
||||
channel_index = (uint8_t)index;
|
||||
|
||||
// If we selected Auto don't do anything and Auto will handle changing.
|
||||
if (v == 40) {
|
||||
auto_channel = true;
|
||||
return;
|
||||
} else {
|
||||
auto_channel = false;
|
||||
}
|
||||
|
||||
field_frequency.set_value(get_freq_by_channel_number(v));
|
||||
channel_number = v;
|
||||
|
||||
baseband::set_btlerx(channel_number);
|
||||
};
|
||||
|
||||
options_sort.on_change = [this](size_t index, int32_t v) {
|
||||
sort_index = (uint8_t)index;
|
||||
handle_entries_sort(v);
|
||||
};
|
||||
|
||||
options_filter.on_change = [this](size_t index, int32_t v) {
|
||||
filter_index = (uint8_t)index;
|
||||
handle_filter_options(v);
|
||||
};
|
||||
|
||||
options_channel.set_selected_index(channel_index, true);
|
||||
options_sort.set_selected_index(sort_index, true);
|
||||
options_filter.set_selected_index(filter_index, true);
|
||||
|
||||
button_find.on_select = [this](Button&) {
|
||||
auto open_view = nav_.push<FileLoadView>(".TXT");
|
||||
open_view->on_changed = [this](std::filesystem::path new_file_path) {
|
||||
on_file_changed(new_file_path);
|
||||
|
||||
// nav_.set_on_pop([this]() { button_play.focus(); });
|
||||
};
|
||||
};
|
||||
|
||||
// Auto-configure modem for LCR RX (will be removed later)
|
||||
baseband::set_btlerx(channel_number);
|
||||
|
||||
receiver_model.enable();
|
||||
}
|
||||
|
||||
std::string BLERxView::build_line_str(BleRecentEntry entry) {
|
||||
std::string macAddressStr = to_string_mac_address(entry.packetData.macAddress, 6, false) + ",";
|
||||
std::string timestameStr = entry.timestamp + ",";
|
||||
std::string nameStr = entry.nameString + ",";
|
||||
std::string pduStr = pdu_type_to_string(entry.pduType) + ",";
|
||||
std::string dataStr = "0x" + entry.dataString + ",";
|
||||
std::string hitsStr = to_string_dec_int(entry.numHits) + ",";
|
||||
std::string dbStr = to_string_dec_int(entry.dbValue) + ",";
|
||||
std::string channelStr = to_string_dec_int(entry.channelNumber) + ",";
|
||||
|
||||
std::string lineStr = timestameStr + macAddressStr + nameStr + pduStr + dataStr + hitsStr + dbStr + channelStr;
|
||||
lineStr += pad_string_with_spaces(maxLineLength - lineStr.length());
|
||||
|
||||
return lineStr;
|
||||
}
|
||||
|
||||
void BLERxView::on_save_file(const std::string value) {
|
||||
auto folder = packet_save_path.parent_path();
|
||||
auto ext = packet_save_path.extension();
|
||||
auto new_path = folder / value + ext;
|
||||
|
||||
saveFile(new_path);
|
||||
}
|
||||
|
||||
bool BLERxView::saveFile(const std::filesystem::path& path) {
|
||||
// Check to see if file was previously saved.
|
||||
bool file_existed = file_exists(path);
|
||||
|
||||
// Attempt to open, if it can't be opened. Create new.
|
||||
auto src = std::make_unique<File>();
|
||||
auto error = src->open(path, false, true);
|
||||
|
||||
if (error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const auto& entry : recent) {
|
||||
tempList.emplace_back(entry);
|
||||
}
|
||||
|
||||
if (!file_existed) {
|
||||
src->write_line(headerStr.c_str());
|
||||
|
||||
auto it = tempList.begin();
|
||||
|
||||
while (it != tempList.end()) {
|
||||
BleRecentEntry entry = (BleRecentEntry)*it;
|
||||
src->write_line(build_line_str(entry).c_str());
|
||||
it++;
|
||||
}
|
||||
} else {
|
||||
// Check file for macAddressStr before adding.
|
||||
char currentLine[maxLineLength];
|
||||
uint64_t startPos = headerStr.length();
|
||||
uint64_t bytesRead = 0;
|
||||
uint64_t bytePos = 0;
|
||||
|
||||
File::Size currentSize = src->size();
|
||||
|
||||
auto dst = std::make_unique<File>();
|
||||
const std::filesystem::path tempFilePath = path + "~";
|
||||
auto error = dst->open(tempFilePath, false, true);
|
||||
|
||||
if (error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
dst->write_line(headerStr.c_str());
|
||||
|
||||
src->seek(startPos);
|
||||
|
||||
// Look for ones found and rewrite.
|
||||
do {
|
||||
memset(currentLine, 0, maxLineLength);
|
||||
|
||||
bytesRead = readUntil(*src, currentLine, currentSize, '\n');
|
||||
|
||||
if (!bytesRead) {
|
||||
break;
|
||||
}
|
||||
|
||||
bytePos += bytesRead;
|
||||
|
||||
std::string lineStr = "";
|
||||
std::string macAddressStr = "";
|
||||
BleRecentEntry foundEntry;
|
||||
|
||||
char* token;
|
||||
token = strtok(currentLine, ",");
|
||||
|
||||
while (token != NULL) {
|
||||
auto it = tempList.begin();
|
||||
|
||||
while (it != tempList.end()) {
|
||||
BleRecentEntry& entry = reinterpret_cast<BleRecentEntry&>(*it);
|
||||
|
||||
macAddressStr = to_string_mac_address(entry.packetData.macAddress, 6, false);
|
||||
|
||||
if (strstr(token, macAddressStr.c_str()) != NULL) {
|
||||
entry.entryFound = true;
|
||||
foundEntry = entry;
|
||||
break;
|
||||
}
|
||||
|
||||
it++;
|
||||
}
|
||||
|
||||
if (foundEntry.entryFound) {
|
||||
break;
|
||||
}
|
||||
|
||||
token = strtok(NULL, ",");
|
||||
}
|
||||
|
||||
if (foundEntry.entryFound) {
|
||||
dst->write_line(build_line_str(foundEntry).c_str());
|
||||
}
|
||||
|
||||
} while (bytePos <= currentSize);
|
||||
|
||||
// Write the ones not found.
|
||||
auto it = tempList.begin();
|
||||
|
||||
while (it != tempList.end()) {
|
||||
BleRecentEntry entry = (BleRecentEntry)*it;
|
||||
|
||||
if (!entry.entryFound) {
|
||||
dst->write_line(build_line_str(entry).c_str());
|
||||
}
|
||||
|
||||
it++;
|
||||
}
|
||||
|
||||
// Close files before renaming/deleting.
|
||||
src.reset();
|
||||
dst.reset();
|
||||
|
||||
// Delete original and overwrite with temp file.
|
||||
delete_file(path);
|
||||
rename_file(tempFilePath, path);
|
||||
}
|
||||
|
||||
tempList.clear();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void BLERxView::on_data(BlePacketData* packet) {
|
||||
if (!logging) {
|
||||
str_log = "";
|
||||
}
|
||||
|
||||
str_console += pdu_type_to_string((ADV_PDU_TYPE)packet->type);
|
||||
str_console += " Len:";
|
||||
str_console += to_string_dec_uint(packet->size);
|
||||
str_console += " Mac:";
|
||||
str_console += to_string_mac_address(packet->macAddress, 6, false);
|
||||
str_console += " Data:";
|
||||
|
||||
int i;
|
||||
|
||||
for (i = 0; i < packet->dataLen; i++) {
|
||||
str_console += to_string_hex(packet->data[i], 2);
|
||||
}
|
||||
|
||||
uint64_t macAddressEncoded = copy_mac_address_to_uint64(packet->macAddress);
|
||||
|
||||
// Start of Packet stuffing.
|
||||
// Masking off the top 2 bytes to avoid invalid keys.
|
||||
auto& entry = ::on_packet(recent, macAddressEncoded & 0xFFFFFFFFFFFF);
|
||||
updateEntry(packet, entry, (ADV_PDU_TYPE)packet->type);
|
||||
|
||||
// Add entries if they meet the criteria.
|
||||
// auto value = filter;
|
||||
// resetFilteredEntries(recent, [&value](const BleRecentEntry& entry) {
|
||||
// return (entry.dataString.find(value) == std::string::npos) && (entry.nameString.find(value) == std::string::npos);
|
||||
// });
|
||||
handle_filter_options(options_filter.selected_index());
|
||||
|
||||
handle_entries_sort(options_sort.selected_index());
|
||||
|
||||
// Log at End of Packet.
|
||||
if (logger && logging) {
|
||||
logger->log_raw_data(str_console + "\r\n");
|
||||
}
|
||||
|
||||
if (serial_logging) {
|
||||
usb_serial_thread->serial_str = str_console + "\r\n";
|
||||
usb_serial_thread->str_ready = true;
|
||||
}
|
||||
str_console = "";
|
||||
|
||||
if (!searchList.empty()) {
|
||||
auto it = searchList.begin();
|
||||
|
||||
while (it != searchList.end()) {
|
||||
std::string searchStr = (std::string)*it;
|
||||
|
||||
if (entry.dataString.find(searchStr) != std::string::npos) {
|
||||
searchList.erase(it);
|
||||
found_count++;
|
||||
break;
|
||||
}
|
||||
|
||||
it++;
|
||||
}
|
||||
|
||||
text_found_count.set(to_string_dec_uint(found_count) + "/" + to_string_dec_uint(total_count));
|
||||
}
|
||||
}
|
||||
|
||||
void BLERxView::on_filter_change(std::string value) {
|
||||
// New filter? Reset list from recent entries.
|
||||
if (filter != value) {
|
||||
// resetFilteredEntries(recent, [&value](const BleRecentEntry& entry) {
|
||||
// // return (entry.dataString.find(value) == std::string::npos) && (entry.nameString.find(value) == std::string::npos);
|
||||
// return (entry.dataString.find(value) == std::string::npos) && (entry.nameString.find(value) == std::string::npos) && (to_string_mac_address(entry.packetData.macAddress, 6, false).find(value) == std::string::npos);
|
||||
// });
|
||||
filter = value;
|
||||
handle_filter_options(options_filter.selected_index());
|
||||
}
|
||||
}
|
||||
|
||||
void BLERxView::on_file_changed(const std::filesystem::path& new_file_path) {
|
||||
file_path = new_file_path;
|
||||
found_count = 0;
|
||||
total_count = 0;
|
||||
searchList.clear();
|
||||
|
||||
{ // Get the size of the data file.
|
||||
File data_file;
|
||||
auto error = data_file.open(file_path, true, false);
|
||||
if (error) {
|
||||
file_error();
|
||||
file_path = "";
|
||||
return;
|
||||
}
|
||||
|
||||
uint64_t bytesRead = 0;
|
||||
uint64_t bytePos = 0;
|
||||
char currentLine[maxLineLength];
|
||||
|
||||
do {
|
||||
memset(currentLine, 0, maxLineLength);
|
||||
|
||||
bytesRead = readUntil(data_file, currentLine, maxLineLength, '\n');
|
||||
|
||||
// Remove return if found.
|
||||
if (currentLine[strlen(currentLine)] == '\r') {
|
||||
currentLine[strlen(currentLine)] = '\0';
|
||||
}
|
||||
|
||||
if (!bytesRead) {
|
||||
break;
|
||||
}
|
||||
|
||||
searchList.push_back(currentLine);
|
||||
total_count++;
|
||||
|
||||
bytePos += bytesRead;
|
||||
|
||||
} while (bytePos <= data_file.size());
|
||||
}
|
||||
}
|
||||
|
||||
// called each 1/60th of second, so 6 = 100ms
|
||||
void BLERxView::on_timer() {
|
||||
if (++timer_count == timer_period) {
|
||||
timer_count = 0;
|
||||
|
||||
if (auto_channel) {
|
||||
int min = 37;
|
||||
int max = 39;
|
||||
|
||||
int randomChannel = min + std::rand() % (max - min + 1);
|
||||
|
||||
field_frequency.set_value(get_freq_by_channel_number(randomChannel));
|
||||
baseband::set_btlerx(randomChannel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BLERxView::handle_entries_sort(uint8_t index) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
sortEntriesBy(
|
||||
recent, [](const BleRecentEntry& entry) { return entry.macAddress; }, true);
|
||||
break;
|
||||
case 1:
|
||||
sortEntriesBy(
|
||||
recent, [](const BleRecentEntry& entry) { return entry.numHits; }, false);
|
||||
break;
|
||||
case 2:
|
||||
sortEntriesBy(
|
||||
recent, [](const BleRecentEntry& entry) { return entry.dbValue; }, false);
|
||||
break;
|
||||
case 3:
|
||||
sortEntriesBy(
|
||||
recent, [](const BleRecentEntry& entry) { return entry.timestamp; }, false);
|
||||
break;
|
||||
case 4:
|
||||
sortEntriesBy(
|
||||
recent, [](const BleRecentEntry& entry) { return entry.nameString; }, true);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
recent_entries_view.set_dirty();
|
||||
}
|
||||
|
||||
void BLERxView::handle_filter_options(uint8_t index) {
|
||||
auto value = filter;
|
||||
switch (index) {
|
||||
case 0: // filter by Data
|
||||
resetFilteredEntries(recent, [&value](const BleRecentEntry& entry) {
|
||||
return (entry.dataString.find(value) == std::string::npos) && (entry.nameString.find(value) == std::string::npos);
|
||||
});
|
||||
break;
|
||||
case 1: // filter by MAC address (All caps: e.g. AA:BB:CC:DD:EE:FF)
|
||||
resetFilteredEntries(recent, [&value](const BleRecentEntry& entry) {
|
||||
return (to_string_mac_address(entry.packetData.macAddress, 6, false).find(value) == std::string::npos);
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void BLERxView::set_parent_rect(const Rect new_parent_rect) {
|
||||
View::set_parent_rect(new_parent_rect);
|
||||
const Rect content_rect{0, header_height, new_parent_rect.width(), new_parent_rect.height() - header_height - switch_button_height};
|
||||
recent_entries_view.set_parent_rect(content_rect);
|
||||
}
|
||||
|
||||
BLERxView::~BLERxView() {
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
void BLERxView::updateEntry(const BlePacketData* packet, BleRecentEntry& entry, ADV_PDU_TYPE pdu_type) {
|
||||
std::string data_string;
|
||||
|
||||
int i;
|
||||
|
||||
for (i = 0; i < packet->dataLen; i++) {
|
||||
data_string += to_string_hex(packet->data[i], 2);
|
||||
}
|
||||
|
||||
entry.dbValue = packet->max_dB;
|
||||
entry.timestamp = to_string_timestamp(rtc_time::now());
|
||||
entry.dataString = data_string;
|
||||
|
||||
entry.packetData.type = packet->type;
|
||||
entry.packetData.size = packet->size;
|
||||
entry.packetData.dataLen = packet->dataLen;
|
||||
|
||||
// Mac Address of sender.
|
||||
entry.packetData.macAddress[0] = packet->macAddress[0];
|
||||
entry.packetData.macAddress[1] = packet->macAddress[1];
|
||||
entry.packetData.macAddress[2] = packet->macAddress[2];
|
||||
entry.packetData.macAddress[3] = packet->macAddress[3];
|
||||
entry.packetData.macAddress[4] = packet->macAddress[4];
|
||||
entry.packetData.macAddress[5] = packet->macAddress[5];
|
||||
|
||||
entry.numHits++;
|
||||
entry.pduType = pdu_type;
|
||||
entry.channelNumber = channel_number;
|
||||
|
||||
// Parse Data Section into buffer to be interpretted later.
|
||||
for (int i = 0; i < packet->dataLen; i++) {
|
||||
entry.packetData.data[i] = packet->data[i];
|
||||
}
|
||||
|
||||
entry.include_name = check_name.value();
|
||||
|
||||
// Only parse name for advertisment packets and empty name entries
|
||||
if ((pdu_type == ADV_IND || pdu_type == ADV_NONCONN_IND || pdu_type == SCAN_RSP || pdu_type == ADV_SCAN_IND) && entry.nameString.empty()) {
|
||||
ADV_PDU_PAYLOAD_TYPE_0_2_4_6* advertiseData = (ADV_PDU_PAYLOAD_TYPE_0_2_4_6*)entry.packetData.data;
|
||||
|
||||
uint8_t currentByte = 0;
|
||||
uint8_t length = 0;
|
||||
uint8_t type = 0;
|
||||
|
||||
std::string decoded_data;
|
||||
for (currentByte = 0; (currentByte < entry.packetData.dataLen);) {
|
||||
length = advertiseData->Data[currentByte++];
|
||||
type = advertiseData->Data[currentByte++];
|
||||
|
||||
// Subtract 1 because type is part of the length.
|
||||
for (int i = 0; i < length - 1; i++) {
|
||||
// parse the name of bluetooth device: 0x08->Shortened Local Name; 0x09->Complete Local Name
|
||||
if (type == 0x08 || type == 0x09) {
|
||||
decoded_data += (char)advertiseData->Data[currentByte];
|
||||
}
|
||||
currentByte++;
|
||||
}
|
||||
if (!decoded_data.empty()) {
|
||||
entry.nameString = std::move(decoded_data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (pdu_type == ADV_DIRECT_IND || pdu_type == SCAN_REQ) {
|
||||
ADV_PDU_PAYLOAD_TYPE_1_3* directed_mac_data = (ADV_PDU_PAYLOAD_TYPE_1_3*)entry.packetData.data;
|
||||
reverse_byte_array(directed_mac_data->A1, 6);
|
||||
}
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
@@ -0,0 +1,387 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2017 Furrtek
|
||||
* Copyright (C) 2023 TJ Baginski
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __BLE_RX_APP_H__
|
||||
#define __BLE_RX_APP_H__
|
||||
|
||||
#include "ble_tx_app.hpp"
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "ui_record_view.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "log_file.hpp"
|
||||
#include "utility.hpp"
|
||||
#include "usb_serial_thread.hpp"
|
||||
#include "file_path.hpp"
|
||||
|
||||
#include "recent_entries.hpp"
|
||||
|
||||
class BLELogger {
|
||||
public:
|
||||
Optional<File::Error> append(const std::filesystem::path& filename) {
|
||||
return log_file.append(filename);
|
||||
}
|
||||
|
||||
void log_raw_data(const std::string& data);
|
||||
|
||||
private:
|
||||
LogFile log_file{};
|
||||
};
|
||||
|
||||
namespace ui {
|
||||
typedef enum {
|
||||
ADV_IND = 0,
|
||||
ADV_DIRECT_IND = 1,
|
||||
ADV_NONCONN_IND = 2,
|
||||
SCAN_REQ = 3,
|
||||
SCAN_RSP = 4,
|
||||
CONNECT_REQ = 5,
|
||||
ADV_SCAN_IND = 6,
|
||||
RESERVED0 = 7,
|
||||
RESERVED1 = 8,
|
||||
RESERVED2 = 9,
|
||||
RESERVED3 = 10,
|
||||
RESERVED4 = 11,
|
||||
RESERVED5 = 12,
|
||||
RESERVED6 = 13,
|
||||
RESERVED7 = 14,
|
||||
RESERVED8 = 15
|
||||
} ADV_PDU_TYPE;
|
||||
|
||||
struct BleRecentEntry {
|
||||
using Key = uint64_t;
|
||||
|
||||
static constexpr Key invalid_key = 0xffffffff;
|
||||
|
||||
uint64_t macAddress;
|
||||
int dbValue;
|
||||
BlePacketData packetData;
|
||||
std::string timestamp;
|
||||
std::string dataString;
|
||||
std::string nameString;
|
||||
bool include_name;
|
||||
uint16_t numHits;
|
||||
ADV_PDU_TYPE pduType;
|
||||
uint8_t channelNumber;
|
||||
bool entryFound;
|
||||
|
||||
BleRecentEntry()
|
||||
: BleRecentEntry{0} {
|
||||
}
|
||||
|
||||
BleRecentEntry(
|
||||
const uint64_t macAddress)
|
||||
: macAddress{macAddress},
|
||||
dbValue{},
|
||||
packetData{},
|
||||
timestamp{},
|
||||
dataString{},
|
||||
nameString{},
|
||||
include_name{},
|
||||
numHits{},
|
||||
pduType{},
|
||||
channelNumber{},
|
||||
entryFound{} {
|
||||
}
|
||||
|
||||
Key key() const {
|
||||
return macAddress;
|
||||
}
|
||||
};
|
||||
|
||||
using BleRecentEntries = RecentEntries<BleRecentEntry>;
|
||||
using BleRecentEntriesView = RecentEntriesView<BleRecentEntries>;
|
||||
|
||||
class BleRecentEntryDetailView : public View {
|
||||
public:
|
||||
BleRecentEntryDetailView(NavigationView& nav, const BleRecentEntry& entry);
|
||||
|
||||
void set_entry(const BleRecentEntry& new_entry);
|
||||
const BleRecentEntry& entry() const { return entry_; };
|
||||
|
||||
void update_data();
|
||||
void focus() override;
|
||||
void paint(Painter&) override;
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
BleRecentEntry entry_{};
|
||||
BLETxPacket build_packet();
|
||||
void on_save_file(const std::string value, BLETxPacket packetToSave);
|
||||
bool saveFile(const std::filesystem::path& path, BLETxPacket packetToSave);
|
||||
std::string packetFileBuffer{};
|
||||
std::filesystem::path packet_save_path{blerx_dir / u"Lists/????.csv"};
|
||||
|
||||
static constexpr uint8_t total_data_lines{5};
|
||||
|
||||
Labels label_mac_address{
|
||||
{{0 * 8, 0 * 16}, "Mac Address:", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
Text text_mac_address{
|
||||
{12 * 8, 0 * 16, 17 * 8, 16},
|
||||
"-"};
|
||||
|
||||
Labels label_pdu_type{
|
||||
{{0 * 8, 1 * 16}, "PDU Type:", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
Text text_pdu_type{
|
||||
{9 * 8, 1 * 16, 17 * 8, 16},
|
||||
"-"};
|
||||
|
||||
Labels labels{
|
||||
{{0 * 8, 3 * 16}, "Len", Theme::getInstance()->fg_light->foreground},
|
||||
{{5 * 8, 3 * 16}, "Type", Theme::getInstance()->fg_light->foreground},
|
||||
{{10 * 8, 3 * 16}, "Value", Theme::getInstance()->fg_light->foreground},
|
||||
};
|
||||
|
||||
Button button_send{
|
||||
{19, 224, 96, 24},
|
||||
"Send"};
|
||||
|
||||
Button button_done{
|
||||
{125, 224, 96, 24},
|
||||
"Done"};
|
||||
|
||||
Button button_save{
|
||||
{72, 264, 96, 24},
|
||||
"Save"};
|
||||
|
||||
bool send_updates{false};
|
||||
|
||||
Rect draw_field(
|
||||
Painter& painter,
|
||||
const Rect& draw_rect,
|
||||
const Style& style,
|
||||
const std::string& label,
|
||||
const std::string& value);
|
||||
};
|
||||
|
||||
class BLERxView : public View {
|
||||
public:
|
||||
BLERxView(NavigationView& nav);
|
||||
~BLERxView();
|
||||
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
void paint(Painter&) override{};
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "BLE RX"; };
|
||||
|
||||
private:
|
||||
std::string build_line_str(BleRecentEntry entry);
|
||||
void on_save_file(const std::string value);
|
||||
bool saveFile(const std::filesystem::path& path);
|
||||
std::unique_ptr<UsbSerialThread> usb_serial_thread{};
|
||||
void on_data(BlePacketData* packetData);
|
||||
void on_filter_change(std::string value);
|
||||
void on_file_changed(const std::filesystem::path& new_file_path);
|
||||
void file_error();
|
||||
void on_timer();
|
||||
void handle_entries_sort(uint8_t index);
|
||||
void handle_filter_options(uint8_t index);
|
||||
void updateEntry(const BlePacketData* packet, BleRecentEntry& entry, ADV_PDU_TYPE pdu_type);
|
||||
|
||||
NavigationView& nav_;
|
||||
RxRadioState radio_state_{
|
||||
2402000000 /* frequency */,
|
||||
4000000 /* bandwidth */,
|
||||
4000000 /* sampling rate */,
|
||||
ReceiverModel::Mode::WidebandFMAudio};
|
||||
|
||||
uint8_t channel_index{0};
|
||||
uint8_t sort_index{0};
|
||||
uint8_t filter_index{0};
|
||||
std::string filter{};
|
||||
bool logging{false};
|
||||
bool serial_logging{false};
|
||||
|
||||
bool name_enable{true};
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_ble",
|
||||
app_settings::Mode::RX,
|
||||
{
|
||||
{"channel_index"sv, &channel_index},
|
||||
{"sort_index"sv, &sort_index},
|
||||
{"filter"sv, &filter},
|
||||
{"log"sv, &logging},
|
||||
{"filter_index"sv, &filter_index},
|
||||
// disabled to always start without USB serial activated until we can make it non blocking if not connected
|
||||
// {"serial_log"sv, &serial_logging},
|
||||
{"name"sv, &name_enable},
|
||||
}};
|
||||
|
||||
std::string str_console = "";
|
||||
uint8_t console_color{0};
|
||||
uint32_t prev_value{0};
|
||||
uint8_t channel_number = 37;
|
||||
bool auto_channel = false;
|
||||
|
||||
int16_t timer_count{0};
|
||||
int16_t timer_period{6}; // 100ms
|
||||
|
||||
std::string filterBuffer{};
|
||||
std::string listFileBuffer{};
|
||||
std::string headerStr = "Timestamp, MAC Address, Name, Packet Type, Data, Hits, dB, Channel";
|
||||
uint16_t maxLineLength = 140;
|
||||
|
||||
std::filesystem::path file_path{};
|
||||
uint64_t found_count = 0;
|
||||
uint64_t total_count = 0;
|
||||
std::vector<std::string> searchList{};
|
||||
|
||||
std::filesystem::path find_packet_path{blerx_dir / u"Find/????.TXT"};
|
||||
std::filesystem::path log_packets_path{blerx_dir / u"Logs/????.TXT"};
|
||||
std::filesystem::path packet_save_path{blerx_dir / u"Lists/????.csv"};
|
||||
|
||||
static constexpr auto header_height = 9 * 8;
|
||||
static constexpr auto switch_button_height = 3 * 16;
|
||||
|
||||
OptionsField options_channel{
|
||||
{0 * 8, 0 * 8},
|
||||
5,
|
||||
{{"Ch.37", 37},
|
||||
{"Ch.38", 38},
|
||||
{"Ch.39", 39},
|
||||
{"Auto", 40}}};
|
||||
|
||||
RxFrequencyField field_frequency{
|
||||
{6 * 8, 0 * 16},
|
||||
nav_};
|
||||
|
||||
RFAmpField field_rf_amp{
|
||||
{16 * 8, 0 * 16}};
|
||||
|
||||
LNAGainField field_lna{
|
||||
{18 * 8, 0 * 16}};
|
||||
|
||||
VGAGainField field_vga{
|
||||
{21 * 8, 0 * 16}};
|
||||
|
||||
RSSI rssi{
|
||||
{24 * 8, 0, 6 * 8, 4}};
|
||||
|
||||
Channel channel{
|
||||
{24 * 8, 5, 6 * 8, 4}};
|
||||
|
||||
Labels label_sort{
|
||||
{{0 * 8, 2 * 8}, "Sort:", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
OptionsField options_sort{
|
||||
{5 * 8, 2 * 8},
|
||||
4,
|
||||
{{"MAC", 0},
|
||||
{"Hits", 1},
|
||||
{"dB", 2},
|
||||
{"Time", 3},
|
||||
{"Name", 4}}};
|
||||
|
||||
Button button_filter{
|
||||
{11 * 8, 2 * 8, 7 * 8, 16},
|
||||
"Filter:"};
|
||||
|
||||
OptionsField options_filter{
|
||||
{18 * 8 + 2, 2 * 8},
|
||||
4,
|
||||
{{"Data", 0},
|
||||
{"MAC", 1}}};
|
||||
|
||||
Checkbox check_log{
|
||||
{10 * 8, 4 * 8 + 2},
|
||||
3,
|
||||
"Log",
|
||||
true};
|
||||
|
||||
Checkbox check_name{
|
||||
{0 * 8, 4 * 8 + 2},
|
||||
3,
|
||||
"Name",
|
||||
true};
|
||||
|
||||
Button button_find{
|
||||
{0 * 8, 7 * 8 - 2, 4 * 8, 16},
|
||||
"Find"};
|
||||
|
||||
Labels label_found{
|
||||
{{5 * 8, 7 * 8 - 2}, "Found:", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
Text text_found_count{
|
||||
{11 * 8, 7 * 8 - 2, 20 * 8, 16},
|
||||
"0/0"};
|
||||
|
||||
Checkbox check_serial_log{
|
||||
{18 * 8 + 2, 4 * 8 + 2},
|
||||
7,
|
||||
"USB Log",
|
||||
true};
|
||||
|
||||
// Console console{
|
||||
// {0, 10 * 8, 240, 240}};
|
||||
|
||||
Button button_clear_list{
|
||||
{2 * 8, 320 - (16 + 32), 7 * 8, 32},
|
||||
"Clear"};
|
||||
|
||||
Button button_save_list{
|
||||
{11 * 8, 320 - (16 + 32), 11 * 8, 32},
|
||||
"Export CSV"};
|
||||
|
||||
Button button_switch{
|
||||
{240 - 6 * 8, 320 - (16 + 32), 4 * 8, 32},
|
||||
"Tx"};
|
||||
|
||||
std::string str_log{""};
|
||||
std::unique_ptr<BLELogger> logger{};
|
||||
|
||||
BleRecentEntries recent{};
|
||||
BleRecentEntries tempList{};
|
||||
|
||||
const RecentEntriesColumns columns{{
|
||||
{"Mac Address", 17},
|
||||
{"Hits", 7},
|
||||
{"dB", 4},
|
||||
}};
|
||||
|
||||
BleRecentEntriesView recent_entries_view{columns, recent};
|
||||
|
||||
MessageHandlerRegistration message_handler_packet{
|
||||
Message::ID::BlePacket,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const BLEPacketMessage*>(p);
|
||||
this->on_data(message->packet);
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_frame_sync{
|
||||
Message::ID::DisplayFrameSync,
|
||||
[this](const Message* const) {
|
||||
this->on_timer();
|
||||
}};
|
||||
}; /* BLERxView */
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
#endif /*__UI_AFSK_RX_H__*/
|
||||
@@ -0,0 +1,562 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2017 Furrtek
|
||||
* Copyright (C) 2023 TJ Baginski
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "ble_tx_app.hpp"
|
||||
#include "ble_rx_app.hpp"
|
||||
|
||||
#include "ui_fileman.hpp"
|
||||
#include "ui_modemsetup.hpp"
|
||||
|
||||
#include "audio.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "io_file.hpp"
|
||||
#include "modems.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include "rtc_time.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "file_path.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
using namespace modems;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
void BLELoggerTx::log_raw_data(const std::string& data) {
|
||||
log_file.write_entry(data);
|
||||
}
|
||||
|
||||
bool hasValidHexPairs(const std::string& str, int totalPairs) {
|
||||
int validPairs = 0;
|
||||
|
||||
for (int i = 0; i < totalPairs * 2; i += 2) {
|
||||
char c1 = str[i];
|
||||
char c2 = str[i + 1];
|
||||
|
||||
if (!(std::isxdigit(c1) && std::isxdigit(c2))) {
|
||||
return false; // Return false if any pair is invalid.
|
||||
}
|
||||
|
||||
validPairs++;
|
||||
}
|
||||
|
||||
return (validPairs == totalPairs);
|
||||
}
|
||||
|
||||
std::vector<std::string> splitIntoStrings(const char* input) {
|
||||
std::vector<std::string> result;
|
||||
int length = std::strlen(input);
|
||||
int start = 0;
|
||||
|
||||
while (start < length) {
|
||||
int remaining = length - start;
|
||||
int chunkSize = (remaining > 29) ? 29 : remaining;
|
||||
result.push_back(std::string(input + start, chunkSize));
|
||||
start += chunkSize;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32_t stringToUint32(const std::string& str) {
|
||||
size_t pos = 0;
|
||||
uint32_t result = 0;
|
||||
|
||||
while (pos < str.size() && std::isdigit(str[pos])) {
|
||||
int digit = str[pos] - '0';
|
||||
|
||||
// Check for overflow before adding the next digit
|
||||
if (result > (UINT32_MAX - digit) / 10) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
result = result * 10 + digit;
|
||||
pos++;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::uint64_t get_freq_by_channel_number(uint8_t channel_number) {
|
||||
uint64_t freq_hz;
|
||||
|
||||
switch (channel_number) {
|
||||
case 37:
|
||||
freq_hz = 2'402'000'000ull;
|
||||
break;
|
||||
case 38:
|
||||
freq_hz = 2'426'000'000ull;
|
||||
break;
|
||||
case 39:
|
||||
freq_hz = 2'480'000'000ull;
|
||||
break;
|
||||
case 0 ... 10:
|
||||
freq_hz = 2'404'000'000ull + channel_number * 2'000'000ull;
|
||||
break;
|
||||
case 11 ... 36:
|
||||
freq_hz = 2'428'000'000ull + (channel_number - 11) * 2'000'000ull;
|
||||
break;
|
||||
default:
|
||||
freq_hz = UINT64_MAX;
|
||||
}
|
||||
|
||||
return freq_hz;
|
||||
}
|
||||
|
||||
namespace ui {
|
||||
|
||||
void BLETxView::focus() {
|
||||
button_open.focus();
|
||||
}
|
||||
|
||||
bool BLETxView::is_active() const {
|
||||
return (bool)is_running;
|
||||
}
|
||||
|
||||
void BLETxView::file_error() {
|
||||
nav_.display_modal("Error", "File read error.");
|
||||
}
|
||||
|
||||
bool BLETxView::saveFile(const std::filesystem::path& path) {
|
||||
File f;
|
||||
auto error = f.create(path);
|
||||
if (error)
|
||||
return false;
|
||||
|
||||
for (uint32_t i = 0; i < num_packets; i++) {
|
||||
std::string macAddressStr = packets[i].macAddress;
|
||||
std::string advertisementDataStr = packets[i].advertisementData;
|
||||
std::string packetCountStr = packets[i].packetCount;
|
||||
|
||||
std::string packetString = macAddressStr + ' ' + advertisementDataStr + ' ' + packetCountStr;
|
||||
|
||||
// Are we on the last line?
|
||||
if (i != num_packets - 1) {
|
||||
packetString += '\n';
|
||||
}
|
||||
|
||||
f.write(packetString.c_str(), packetString.length());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void BLETxView::toggle() {
|
||||
if (is_active()) {
|
||||
stop();
|
||||
} else {
|
||||
start();
|
||||
}
|
||||
}
|
||||
|
||||
void BLETxView::start() {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_btle_tx);
|
||||
transmitter_model.enable();
|
||||
|
||||
// Generate new random Mac Address.
|
||||
generateRandomMacAddress(randomMac);
|
||||
|
||||
// If this is our first run, check file.
|
||||
if (!is_active()) {
|
||||
File data_file;
|
||||
|
||||
auto error = data_file.open(file_path);
|
||||
if (error && !file_override) {
|
||||
file_error();
|
||||
check_loop.set_value(false);
|
||||
return;
|
||||
}
|
||||
|
||||
button_play.set_bitmap(&bitmap_stop);
|
||||
is_running = true;
|
||||
}
|
||||
|
||||
char advertisementData[63] = {0};
|
||||
strcpy(advertisementData, packets[current_packet].advertisementData);
|
||||
|
||||
// TODO: Make this a checkbox.
|
||||
if (!markedBytes.empty()) {
|
||||
for (size_t i = 0; i < strlen(advertisementData); i++) {
|
||||
bool found = false;
|
||||
|
||||
auto it = std::find(markedBytes.begin(), markedBytes.end(), i);
|
||||
|
||||
if (it != markedBytes.end()) {
|
||||
found = true;
|
||||
}
|
||||
|
||||
if (found) {
|
||||
uint8_t hexDigit;
|
||||
switch (marked_data_sequence.selected_index_value()) {
|
||||
case 0:
|
||||
hexDigit = marked_counter++;
|
||||
break;
|
||||
case 1:
|
||||
hexDigit = marked_counter--;
|
||||
break;
|
||||
case 2: {
|
||||
uint8_t min = 0x00;
|
||||
uint8_t max = 0x0F;
|
||||
|
||||
hexDigit = min + std::rand() % (max - min + 1);
|
||||
} break;
|
||||
default:
|
||||
hexDigit = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
advertisementData[i] = uint_to_char(hexDigit, 16);
|
||||
|
||||
// Bounding to Hex.
|
||||
if (marked_counter == 16) {
|
||||
marked_counter = 0;
|
||||
} else if (marked_counter == 255) {
|
||||
marked_counter = 15;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Setup next packet configuration.
|
||||
progressbar.set_max(packets[current_packet].packet_count);
|
||||
baseband::set_btletx(channel_number, random_mac ? randomMac : packets[current_packet].macAddress, advertisementData, pduType);
|
||||
}
|
||||
|
||||
void BLETxView::stop() {
|
||||
transmitter_model.disable();
|
||||
baseband::shutdown();
|
||||
|
||||
progressbar.set_value(0);
|
||||
button_play.set_bitmap(&bitmap_play);
|
||||
check_loop.set_value(false);
|
||||
|
||||
update_current_packet(packets[0], 0);
|
||||
|
||||
is_running = false;
|
||||
}
|
||||
|
||||
void BLETxView::reset() {
|
||||
transmitter_model.disable();
|
||||
baseband::shutdown();
|
||||
|
||||
start();
|
||||
}
|
||||
|
||||
// called each 1/60th of second, so 6 = 100ms
|
||||
void BLETxView::on_timer() {
|
||||
if (++timer_count == timer_period) {
|
||||
timer_count = 0;
|
||||
|
||||
if (is_active()) {
|
||||
// Reached end of current packet repeats.
|
||||
if (packet_counter == 0) {
|
||||
// Done sending all packets.
|
||||
if (current_packet == (num_packets - 1)) {
|
||||
current_packet = 0;
|
||||
|
||||
// If looping, restart from beginning.
|
||||
if (check_loop.value()) {
|
||||
update_current_packet(packets[current_packet], current_packet);
|
||||
reset();
|
||||
} else {
|
||||
stop();
|
||||
}
|
||||
} else {
|
||||
current_packet++;
|
||||
update_current_packet(packets[current_packet], current_packet);
|
||||
reset();
|
||||
}
|
||||
} else {
|
||||
reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (++auto_channel_counter == auto_channel_period) {
|
||||
auto_channel_counter = 0;
|
||||
|
||||
if (auto_channel) {
|
||||
int min = 37;
|
||||
int max = 39;
|
||||
|
||||
channel_number = min + std::rand() % (max - min + 1);
|
||||
|
||||
field_frequency.set_value(get_freq_by_channel_number(channel_number));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BLETxView::on_tx_progress(const bool done) {
|
||||
if (done) {
|
||||
if (is_active()) {
|
||||
if ((packet_counter % 10) == 0) {
|
||||
text_packets_sent.set(to_string_dec_uint(packet_counter));
|
||||
}
|
||||
|
||||
packet_counter--;
|
||||
progressbar.set_value(packets[current_packet].packet_count - packet_counter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BLETxView::BLETxView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
add_children({&button_open,
|
||||
&text_filename,
|
||||
&progressbar,
|
||||
&check_rand_mac,
|
||||
&field_frequency,
|
||||
&tx_view, // now it handles previous rfgain, rfamp.
|
||||
&check_loop,
|
||||
&button_play,
|
||||
&label_speed,
|
||||
&options_speed,
|
||||
&options_channel,
|
||||
&options_adv_type,
|
||||
&label_marked_data,
|
||||
&marked_data_sequence,
|
||||
&label_packet_index,
|
||||
&text_packet_index,
|
||||
&label_packets_sent,
|
||||
&text_packets_sent,
|
||||
&label_mac_address,
|
||||
&text_mac_address,
|
||||
&label_data_packet,
|
||||
&dataEditView,
|
||||
&button_clear_marked,
|
||||
&button_save_packet,
|
||||
&button_switch});
|
||||
|
||||
field_frequency.set_step(0);
|
||||
|
||||
ensure_directory(packet_save_path);
|
||||
|
||||
button_play.on_select = [this](ImageButton&) {
|
||||
this->toggle();
|
||||
};
|
||||
|
||||
options_channel.on_change = [this](size_t, int32_t i) {
|
||||
// If we selected Auto don't do anything and Auto will handle changing.
|
||||
if (i == 40) {
|
||||
auto_channel = true;
|
||||
return;
|
||||
} else {
|
||||
auto_channel = false;
|
||||
}
|
||||
|
||||
field_frequency.set_value(get_freq_by_channel_number(i));
|
||||
channel_number = i;
|
||||
};
|
||||
|
||||
options_speed.on_change = [this](size_t, int32_t i) {
|
||||
timer_period = i;
|
||||
timer_count = 0;
|
||||
};
|
||||
|
||||
options_adv_type.on_change = [this](size_t, int32_t i) {
|
||||
pduType = (PKT_TYPE)i;
|
||||
};
|
||||
|
||||
options_speed.set_selected_index(0);
|
||||
options_channel.set_selected_index(0);
|
||||
options_adv_type.set_selected_index(0);
|
||||
|
||||
check_rand_mac.set_value(false);
|
||||
check_rand_mac.on_select = [this](Checkbox&, bool v) {
|
||||
random_mac = v;
|
||||
};
|
||||
|
||||
button_open.on_select = [this](Button&) {
|
||||
auto open_view = nav_.push<FileLoadView>(".TXT");
|
||||
open_view->on_changed = [this](std::filesystem::path new_file_path) {
|
||||
on_file_changed(new_file_path);
|
||||
|
||||
nav_.set_on_pop([this]() { button_play.focus(); });
|
||||
};
|
||||
};
|
||||
|
||||
button_save_packet.on_select = [this, &nav](Button&) {
|
||||
packetFileBuffer = "";
|
||||
text_prompt(
|
||||
nav,
|
||||
packetFileBuffer,
|
||||
64,
|
||||
[this](std::string& buffer) {
|
||||
on_save_file(buffer);
|
||||
});
|
||||
};
|
||||
|
||||
button_switch.on_select = [&nav](Button&) {
|
||||
nav.replace<BLERxView>();
|
||||
};
|
||||
|
||||
dataEditView.on_select = [this] {
|
||||
// Save last selected cursor.
|
||||
cursor_pos.line = dataEditView.line();
|
||||
cursor_pos.col = dataEditView.col();
|
||||
|
||||
// Reject setting newline at index 29.
|
||||
if (cursor_pos.col != 29) {
|
||||
uint16_t dataBytePos = (cursor_pos.line * 29) + cursor_pos.col;
|
||||
|
||||
auto it = std::find(markedBytes.begin(), markedBytes.end(), dataBytePos);
|
||||
|
||||
if (it != markedBytes.end()) {
|
||||
markedBytes.erase(it);
|
||||
} else {
|
||||
markedBytes.push_back(dataBytePos);
|
||||
}
|
||||
|
||||
dataEditView.cursor_mark_selected();
|
||||
}
|
||||
};
|
||||
|
||||
button_clear_marked.on_select = [this](Button&) {
|
||||
marked_counter = 0;
|
||||
markedBytes.clear();
|
||||
dataEditView.cursor_clear_marked();
|
||||
};
|
||||
}
|
||||
|
||||
BLETxView::BLETxView(
|
||||
NavigationView& nav,
|
||||
BLETxPacket packet)
|
||||
: BLETxView(nav) {
|
||||
packets[0] = packet;
|
||||
|
||||
update_current_packet(packets[0], 0);
|
||||
|
||||
num_packets = 1;
|
||||
file_override = true;
|
||||
}
|
||||
|
||||
void BLETxView::on_file_changed(const fs::path& new_file_path) {
|
||||
file_path = new_file_path;
|
||||
num_packets = 0;
|
||||
|
||||
{ // Get the size of the data file.
|
||||
File data_file;
|
||||
auto error = data_file.open(file_path);
|
||||
if (error) {
|
||||
file_error();
|
||||
file_path = "";
|
||||
return;
|
||||
}
|
||||
|
||||
do {
|
||||
readUntil(data_file, packets[num_packets].macAddress, mac_address_size_str, ' ');
|
||||
readUntil(data_file, packets[num_packets].advertisementData, max_packet_size_str, ' ');
|
||||
readUntil(data_file, packets[num_packets].packetCount, max_packet_repeat_str, '\n');
|
||||
|
||||
uint64_t macAddressSize = strlen(packets[num_packets].macAddress);
|
||||
uint64_t advertisementDataSize = strlen(packets[num_packets].advertisementData);
|
||||
uint64_t packetCountSize = strlen(packets[num_packets].packetCount);
|
||||
|
||||
packets[num_packets].packet_count = stringToUint32(packets[num_packets].packetCount);
|
||||
|
||||
// Verify Data.
|
||||
if ((macAddressSize == mac_address_size_str) && (advertisementDataSize < max_packet_size_str) && (packetCountSize < max_packet_repeat_str) &&
|
||||
hasValidHexPairs(packets[num_packets].macAddress, macAddressSize / 2) && hasValidHexPairs(packets[num_packets].advertisementData, advertisementDataSize / 2) && (packets[num_packets].packet_count >= 1) && (packets[num_packets].packet_count < max_packet_repeat_count)) {
|
||||
text_filename.set(truncate(file_path.filename().string(), 12));
|
||||
|
||||
} else {
|
||||
// Did not find any packets.
|
||||
if (num_packets == 0) {
|
||||
file_path = "";
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
num_packets++;
|
||||
|
||||
} while (num_packets < max_num_packets);
|
||||
|
||||
update_current_packet(packets[0], 0);
|
||||
}
|
||||
}
|
||||
|
||||
void BLETxView::on_save_file(const std::string value) {
|
||||
auto folder = packet_save_path.parent_path();
|
||||
auto ext = packet_save_path.extension();
|
||||
auto new_path = folder / value + ext;
|
||||
|
||||
saveFile(new_path);
|
||||
}
|
||||
|
||||
void BLETxView::on_data(uint32_t value, bool is_data) {
|
||||
std::string str_console = "";
|
||||
|
||||
if (is_data) {
|
||||
str_console += (char)(value);
|
||||
}
|
||||
}
|
||||
|
||||
void BLETxView::update_current_packet(BLETxPacket packet, uint32_t currentIndex) {
|
||||
std::string formattedMacAddress = to_string_formatted_mac_address(packet.macAddress);
|
||||
|
||||
std::vector<std::string> strings = splitIntoStrings(packet.advertisementData);
|
||||
|
||||
text_packet_index.set(to_string_dec_uint(current_packet));
|
||||
|
||||
text_packets_sent.set(to_string_dec_uint(packet.packet_count));
|
||||
|
||||
text_mac_address.set(formattedMacAddress);
|
||||
|
||||
packet_counter = packet.packet_count;
|
||||
current_packet = currentIndex;
|
||||
|
||||
dataFile.create(dataTempFilePath);
|
||||
|
||||
for (const std::string& str : strings) {
|
||||
dataFile.write(str.c_str(), str.size());
|
||||
dataFile.write("\n", 1);
|
||||
;
|
||||
}
|
||||
|
||||
dataFile.~File();
|
||||
|
||||
auto result = FileWrapper::open(dataTempFilePath);
|
||||
|
||||
if (!result)
|
||||
return;
|
||||
|
||||
dataFileWrapper = *std::move(result);
|
||||
|
||||
dataEditView.set_font_zoom(true);
|
||||
dataEditView.set_file(*dataFileWrapper);
|
||||
dataEditView.redraw(true, true);
|
||||
}
|
||||
|
||||
void BLETxView::set_parent_rect(const Rect new_parent_rect) {
|
||||
View::set_parent_rect(new_parent_rect);
|
||||
const Rect content_rect{0, header_height, new_parent_rect.width(), new_parent_rect.height() - header_height - switch_button_height};
|
||||
dataEditView.set_parent_rect(content_rect);
|
||||
}
|
||||
|
||||
BLETxView::~BLETxView() {
|
||||
delete_file(dataTempFilePath);
|
||||
transmitter_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
@@ -0,0 +1,338 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2017 Furrtek
|
||||
* Copyright (C) 2023 TJ Baginski
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __BLE_TX_APP_H__
|
||||
#define __BLE_TX_APP_H__
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_transmitter.hpp"
|
||||
#include "ui_text_editor.hpp"
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "ui_record_view.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "replay_thread.hpp"
|
||||
#include "log_file.hpp"
|
||||
#include "utility.hpp"
|
||||
#include "file_path.hpp"
|
||||
|
||||
#include "recent_entries.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <memory>
|
||||
|
||||
class BLELoggerTx {
|
||||
public:
|
||||
Optional<File::Error> append(const std::filesystem::path& filename) {
|
||||
return log_file.append(filename);
|
||||
}
|
||||
|
||||
void log_raw_data(const std::string& data);
|
||||
|
||||
private:
|
||||
LogFile log_file{};
|
||||
};
|
||||
|
||||
namespace ui {
|
||||
|
||||
enum PKT_TYPE {
|
||||
PKT_TYPE_INVALID_TYPE,
|
||||
PKT_TYPE_RAW,
|
||||
PKT_TYPE_DISCOVERY,
|
||||
PKT_TYPE_IBEACON,
|
||||
PKT_TYPE_ADV_IND,
|
||||
PKT_TYPE_ADV_DIRECT_IND,
|
||||
PKT_TYPE_ADV_NONCONN_IND,
|
||||
PKT_TYPE_ADV_SCAN_IND,
|
||||
PKT_TYPE_SCAN_REQ,
|
||||
PKT_TYPE_SCAN_RSP,
|
||||
PKT_TYPE_CONNECT_REQ,
|
||||
PKT_TYPE_LL_DATA,
|
||||
PKT_TYPE_LL_CONNECTION_UPDATE_REQ,
|
||||
PKT_TYPE_LL_CHANNEL_MAP_REQ,
|
||||
PKT_TYPE_LL_TERMINATE_IND,
|
||||
PKT_TYPE_LL_ENC_REQ,
|
||||
PKT_TYPE_LL_ENC_RSP,
|
||||
PKT_TYPE_LL_START_ENC_REQ,
|
||||
PKT_TYPE_LL_START_ENC_RSP,
|
||||
PKT_TYPE_LL_UNKNOWN_RSP,
|
||||
PKT_TYPE_LL_FEATURE_REQ,
|
||||
PKT_TYPE_LL_FEATURE_RSP,
|
||||
PKT_TYPE_LL_PAUSE_ENC_REQ,
|
||||
PKT_TYPE_LL_PAUSE_ENC_RSP,
|
||||
PKT_TYPE_LL_VERSION_IND,
|
||||
PKT_TYPE_LL_REJECT_IND,
|
||||
PKT_TYPE_NUM_PKT_TYPE
|
||||
};
|
||||
|
||||
struct BLETxPacket {
|
||||
char macAddress[13];
|
||||
char advertisementData[63];
|
||||
char packetCount[11];
|
||||
uint32_t packet_count;
|
||||
PKT_TYPE packetType;
|
||||
};
|
||||
|
||||
class BLETxView : public View {
|
||||
public:
|
||||
BLETxView(NavigationView& nav);
|
||||
BLETxView(NavigationView& nav, BLETxPacket packet);
|
||||
~BLETxView();
|
||||
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
void paint(Painter&) override{};
|
||||
|
||||
void focus() override;
|
||||
|
||||
bool is_active() const;
|
||||
void toggle();
|
||||
void start();
|
||||
void stop();
|
||||
void reset();
|
||||
void handle_replay_thread_done(const uint32_t return_code);
|
||||
void file_error();
|
||||
bool saveFile(const std::filesystem::path& path);
|
||||
|
||||
std::string title() const override { return "BLE TX"; };
|
||||
|
||||
private:
|
||||
void on_timer();
|
||||
void on_data(uint32_t value, bool is_data);
|
||||
void on_file_changed(const std::filesystem::path& new_file_path);
|
||||
void on_save_file(const std::string value);
|
||||
void on_tx_progress(const bool done);
|
||||
void on_random_data_change(std::string value);
|
||||
void update_current_packet(BLETxPacket packet, uint32_t currentIndex);
|
||||
|
||||
NavigationView& nav_;
|
||||
TxRadioState radio_state_{
|
||||
2'402'000'000 /* frequency */,
|
||||
4'000'000 /* bandwidth */,
|
||||
4'000'000 /* sampling rate */
|
||||
};
|
||||
app_settings::SettingsManager settings_{
|
||||
"tx_ble", app_settings::Mode::TX};
|
||||
|
||||
uint8_t console_color{0};
|
||||
uint32_t prev_value{0};
|
||||
|
||||
std::filesystem::path file_path{};
|
||||
std::filesystem::path packet_save_path{bletx_dir / u"BLETX_????.TXT"};
|
||||
uint8_t channel_number = 37;
|
||||
bool auto_channel = false;
|
||||
|
||||
char randomMac[13] = "010203040506";
|
||||
|
||||
bool is_running = false;
|
||||
|
||||
int16_t timer_count{0};
|
||||
int16_t timer_period{1};
|
||||
int16_t auto_channel_counter = 0;
|
||||
int16_t auto_channel_period{6};
|
||||
|
||||
bool repeatLoop = false;
|
||||
uint32_t packet_counter{0};
|
||||
uint32_t num_packets{0};
|
||||
uint32_t current_packet{0};
|
||||
bool random_mac = false;
|
||||
bool file_override = false;
|
||||
|
||||
typedef struct {
|
||||
uint16_t line;
|
||||
uint16_t col;
|
||||
} CursorPos;
|
||||
|
||||
std::unique_ptr<FileWrapper> dataFileWrapper{};
|
||||
File dataFile{};
|
||||
std::filesystem::path dataTempFilePath{bletx_dir / u"dataFileTemp.TXT"};
|
||||
std::vector<uint16_t> markedBytes{};
|
||||
CursorPos cursor_pos{};
|
||||
uint8_t marked_counter = 0;
|
||||
|
||||
static constexpr uint8_t mac_address_size_str{12};
|
||||
static constexpr uint8_t max_packet_size_str{62};
|
||||
static constexpr uint8_t max_packet_repeat_str{10};
|
||||
static constexpr uint32_t max_packet_repeat_count{UINT32_MAX};
|
||||
static constexpr uint32_t max_num_packets{32};
|
||||
|
||||
BLETxPacket packets[max_num_packets];
|
||||
|
||||
PKT_TYPE pduType = {PKT_TYPE_DISCOVERY};
|
||||
|
||||
static constexpr auto header_height = 10 * 16;
|
||||
static constexpr auto switch_button_height = 6 * 16;
|
||||
|
||||
Button button_open{
|
||||
{0 * 8, 0 * 16, 10 * 8, 2 * 16},
|
||||
"Open file"};
|
||||
|
||||
Text text_filename{
|
||||
{11 * 8, 0 * 16, 12 * 8, 16},
|
||||
"-"};
|
||||
|
||||
ProgressBar progressbar{
|
||||
{11 * 8, 1 * 16, 9 * 8, 16}};
|
||||
|
||||
Checkbox check_rand_mac{
|
||||
{21 * 8, 1 * 16},
|
||||
6,
|
||||
"?? Mac",
|
||||
true};
|
||||
|
||||
TxFrequencyField field_frequency{
|
||||
{0 * 8, 2 * 16},
|
||||
nav_};
|
||||
|
||||
TransmitterView2 tx_view{
|
||||
{11 * 8, 2 * 16},
|
||||
/*short_ui*/ true};
|
||||
|
||||
Checkbox check_loop{
|
||||
{21 * 8, 2 * 16},
|
||||
4,
|
||||
"Loop",
|
||||
true};
|
||||
|
||||
ImageButton button_play{
|
||||
{28 * 8, 2 * 16, 2 * 8, 1 * 16},
|
||||
&bitmap_play,
|
||||
Theme::getInstance()->fg_green->foreground,
|
||||
Theme::getInstance()->fg_green->background};
|
||||
|
||||
Labels label_speed{
|
||||
{{0 * 8, 6 * 8}, "Speed:", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
OptionsField options_speed{
|
||||
{7 * 8, 6 * 8},
|
||||
3,
|
||||
{{"1 ", 1}, // 16ms
|
||||
{"2 ", 2}, // 32ms
|
||||
{"3 ", 3}, // 48ms
|
||||
{"4 ", 6}, // 100ms
|
||||
{"5 ", 12}}}; // 200ms
|
||||
|
||||
OptionsField options_channel{
|
||||
{11 * 8, 6 * 8},
|
||||
5,
|
||||
{{"Ch.37 ", 37},
|
||||
{"Ch.38", 38},
|
||||
{"Ch.39", 39},
|
||||
{"Auto", 40}}};
|
||||
|
||||
OptionsField options_adv_type{
|
||||
{17 * 8, 6 * 8},
|
||||
14,
|
||||
{{"DISCOVERY ", PKT_TYPE_DISCOVERY},
|
||||
{"ADV_IND", PKT_TYPE_ADV_IND},
|
||||
{"ADV_DIRECT", PKT_TYPE_ADV_DIRECT_IND},
|
||||
{"ADV_NONCONN", PKT_TYPE_ADV_NONCONN_IND},
|
||||
{"ADV_SCAN_IND", PKT_TYPE_ADV_SCAN_IND},
|
||||
{"SCAN_REQ", PKT_TYPE_SCAN_REQ},
|
||||
{"SCAN_RSP", PKT_TYPE_SCAN_RSP},
|
||||
{"CONNECT_REQ", PKT_TYPE_CONNECT_REQ}}};
|
||||
|
||||
Labels label_marked_data{
|
||||
{{0 * 8, 4 * 16}, "Marked Data:", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
OptionsField marked_data_sequence{
|
||||
{12 * 8, 8 * 8},
|
||||
8,
|
||||
{{"Ascend", 0},
|
||||
{"Descend", 1},
|
||||
{"Random", 2}}};
|
||||
|
||||
Labels label_packet_index{
|
||||
{{0 * 8, 12 * 8}, "Packet Index:", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
Text text_packet_index{
|
||||
{13 * 8, 6 * 16, 12 * 8, 16},
|
||||
"-"};
|
||||
|
||||
Labels label_packets_sent{
|
||||
{{0 * 8, 14 * 8}, "Repeat Count:", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
Text text_packets_sent{
|
||||
{13 * 8, 7 * 16, 12 * 8, 16},
|
||||
"-"};
|
||||
|
||||
Labels label_mac_address{
|
||||
{{0 * 8, 16 * 8}, "Mac Address:", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
Text text_mac_address{
|
||||
{12 * 8, 8 * 16, 20 * 8, 16},
|
||||
"-"};
|
||||
|
||||
Labels label_data_packet{
|
||||
{{0 * 8, 9 * 16}, "Packet Data:", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
Console console{
|
||||
{0, 9 * 18, 240, 240}};
|
||||
|
||||
TextViewer dataEditView{
|
||||
{0, 9 * 18, 240, 240}};
|
||||
|
||||
Button button_clear_marked{
|
||||
{1 * 8, 14 * 16, 13 * 8, 3 * 8},
|
||||
"Clear Marked"};
|
||||
|
||||
Button button_save_packet{
|
||||
{1 * 8, 16 * 16, 13 * 8, 2 * 16},
|
||||
"Save Packet"};
|
||||
|
||||
Button button_switch{
|
||||
{16 * 8, 16 * 16, 13 * 8, 2 * 16},
|
||||
"Switch to Rx"};
|
||||
|
||||
std::string str_log{""};
|
||||
bool logging{true};
|
||||
bool logging_done{false};
|
||||
std::string packetFileBuffer{};
|
||||
|
||||
std::unique_ptr<BLELoggerTx> logger{};
|
||||
|
||||
MessageHandlerRegistration message_handler_packet{
|
||||
Message::ID::AFSKData,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const AFSKDataMessage*>(p);
|
||||
this->on_data(message->value, message->is_data);
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_tx_progress{
|
||||
Message::ID::TXProgress,
|
||||
[this](const Message* const p) {
|
||||
const auto message = *reinterpret_cast<const TXProgressMessage*>(p);
|
||||
this->on_tx_progress(message.done);
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_frame_sync{
|
||||
Message::ID::DisplayFrameSync,
|
||||
[this](const Message* const) {
|
||||
this->on_timer();
|
||||
}};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
#endif /*__UI_AFSK_RX_H__*/
|
||||
@@ -21,151 +21,115 @@
|
||||
*/
|
||||
|
||||
#include "capture_app.hpp"
|
||||
|
||||
#include "baseband_api.hpp"
|
||||
|
||||
#include "portapack.hpp"
|
||||
#include "ui_freqman.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
|
||||
namespace ui {
|
||||
|
||||
CaptureAppView::CaptureAppView(NavigationView& nav) {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_capture);
|
||||
CaptureAppView::CaptureAppView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_capture);
|
||||
|
||||
add_children({
|
||||
&labels,
|
||||
&rssi,
|
||||
&channel,
|
||||
&field_frequency,
|
||||
&field_frequency_step,
|
||||
&field_rf_amp,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&option_bandwidth,
|
||||
&record_view,
|
||||
&waterfall,
|
||||
});
|
||||
add_children({
|
||||
&labels,
|
||||
&rssi,
|
||||
&channel,
|
||||
&field_frequency,
|
||||
&field_frequency_step,
|
||||
&field_rf_amp,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&option_bandwidth,
|
||||
&option_format,
|
||||
&check_trim,
|
||||
&record_view,
|
||||
&waterfall,
|
||||
});
|
||||
|
||||
// Hack for initialization
|
||||
// TODO: This should be included in a more global section so apps dont need to do it
|
||||
receiver_model.set_sampling_rate(3072000);
|
||||
receiver_model.set_baseband_bandwidth(1750000);
|
||||
//-------------------
|
||||
field_frequency_step.set_by_value(receiver_model.frequency_step());
|
||||
field_frequency_step.on_change = [this](size_t, OptionsField::value_t v) {
|
||||
receiver_model.set_frequency_step(v);
|
||||
this->field_frequency.set_step(v);
|
||||
};
|
||||
|
||||
field_frequency.set_value(receiver_model.tuning_frequency());
|
||||
field_frequency.set_step(receiver_model.frequency_step());
|
||||
field_frequency.on_change = [this](rf::Frequency f) {
|
||||
this->on_tuning_frequency_changed(f);
|
||||
};
|
||||
field_frequency.on_edit = [this, &nav]() {
|
||||
// TODO: Provide separate modal method/scheme?
|
||||
auto new_view = nav.push<FrequencyKeypadView>(receiver_model.tuning_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
this->on_tuning_frequency_changed(f);
|
||||
this->field_frequency.set_value(f);
|
||||
};
|
||||
};
|
||||
|
||||
field_frequency_step.set_by_value(receiver_model.frequency_step());
|
||||
field_frequency_step.on_change = [this](size_t, OptionsField::value_t v) {
|
||||
receiver_model.set_frequency_step(v);
|
||||
this->field_frequency.set_step(v);
|
||||
};
|
||||
|
||||
option_bandwidth.on_change = [this](size_t, uint32_t base_rate) {
|
||||
sampling_rate = 8 * base_rate; // Decimation by 8 done on baseband side
|
||||
/* base_rate is used for FFT calculation and display LCD, and also in recording writing SD Card rate. */
|
||||
/* ex. sampling_rate values, 4Mhz, when recording 500 kHz (BW) and fs 8 Mhz , when selected 1 Mhz BW ...*/
|
||||
/* ex. recording 500kHz BW to .C16 file, base_rate clock 500kHz x2(I,Q) x 2 bytes (int signed) =2MB/sec rate SD Card */
|
||||
option_format.set_selected_index(0); // Default to C16
|
||||
option_format.on_change = [this](size_t, uint32_t file_type) {
|
||||
record_view.set_file_type((RecordView::FileType)file_type);
|
||||
};
|
||||
|
||||
waterfall.on_hide();
|
||||
record_view.set_sampling_rate(sampling_rate);
|
||||
receiver_model.set_sampling_rate(sampling_rate);
|
||||
/* Set up proper anti aliasing BPF bandwith in MAX2837 before ADC sampling according to the new added BW Options . */
|
||||
|
||||
switch(sampling_rate) { // we use the var fs (sampling_rate) , to set up BPF aprox < fs_max/2 by Nyquist theorem.
|
||||
|
||||
case 0 ... 2000000: // BW Captured range (0 <= 250kHz max ) fs = 8 x 250 kHz
|
||||
anti_alias_baseband_bandwidth_filter = 1750000; // Minimum BPF MAX2837 for all those lower BW options.
|
||||
break;
|
||||
check_trim.on_select = [this](Checkbox&, bool v) {
|
||||
record_view.set_auto_trim(v);
|
||||
};
|
||||
|
||||
case 4000000 ... 6000000: // BW capture range (500k ... 750kHz max ) fs_max = 8 x 750kHz = 6Mhz
|
||||
// BW 500k ... 750kHz , ex. 500kHz (fs = 8*BW = 4Mhz) , BW 600kHz (fs = 4,8Mhz) , BW 750 kHz (fs = 6Mhz)
|
||||
anti_alias_baseband_bandwidth_filter = 2500000; // in some IC MAX2837 appear 2250000 , but both works similar.
|
||||
break;
|
||||
freqman_set_bandwidth_option(SPEC_MODULATION, option_bandwidth);
|
||||
option_bandwidth.on_change = [this](size_t, uint32_t bandwidth) {
|
||||
/* Nyquist would imply a sample rate of 2x bandwidth, but because the ADC
|
||||
* provides 2 values (I,Q), the sample_rate is equal to bandwidth here. */
|
||||
auto sample_rate = bandwidth;
|
||||
|
||||
case 8800000: // BW capture 1,1Mhz fs = 8 x 1,1Mhz = 8,8Mhz . (1Mhz showed slightly higher noise background).
|
||||
anti_alias_baseband_bandwidth_filter = 3500000;
|
||||
break;
|
||||
/* base_rate (bandwidth) is used for FFT calculation and display LCD, and also in recording writing SD Card rate. */
|
||||
/* ex. sampling_rate values, 4Mhz, when recording 500 kHz (BW) and fs 8 Mhz, when selected 1 Mhz BW ... */
|
||||
/* ex. recording 500kHz BW to .C16 file, base_rate clock 500kHz x2(I,Q) x 2 bytes (int signed) =2MB/sec rate SD Card. */
|
||||
|
||||
case 14000000: // BW capture 1,75Mhz , fs = 8 x 1,75Mhz = 14Mhz
|
||||
// good BPF ,good matching, but LCD making flicker , refresh rate should be < 20 Hz , but reasonable picture
|
||||
anti_alias_baseband_bandwidth_filter = 5000000;
|
||||
break;
|
||||
waterfall.stop();
|
||||
|
||||
case 16000000: // BW capture 2Mhz , fs = 8 x 2Mhz = 16Mhz
|
||||
// good BPF ,good matching, but LCD making flicker , refresh rate should be < 20 Hz , but reasonable picture
|
||||
anti_alias_baseband_bandwidth_filter = 6000000;
|
||||
break;
|
||||
|
||||
case 20000000: // BW capture 2,5Mhz , fs= 8 x 2,5 Mhz = 20Mhz
|
||||
// good BPF ,good matching, but LCD making flicker , refresh rate should be < 20 Hz , but reasonable picture
|
||||
anti_alias_baseband_bandwidth_filter = 7000000;
|
||||
break;
|
||||
|
||||
default: // BW capture 2,75Mhz, fs = 8 x 2,75Mhz= 22Mhz max ADC sampling) and others.
|
||||
// We tested also 9Mhz FPB stightly too much noise floor , better 8Mhz
|
||||
anti_alias_baseband_bandwidth_filter = 8000000;
|
||||
}
|
||||
receiver_model.set_baseband_bandwidth(anti_alias_baseband_bandwidth_filter);
|
||||
|
||||
|
||||
waterfall.on_show();
|
||||
};
|
||||
|
||||
option_bandwidth.set_selected_index(7); // 500k, Preselected starting default option 500kHz
|
||||
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::Capture);
|
||||
receiver_model.enable();
|
||||
// record_view determines the correct oversampling to apply and returns the actual sample rate.
|
||||
// NB: record_view is what actually updates proc_capture baseband settings.
|
||||
auto actual_sample_rate = record_view.set_sampling_rate(sample_rate);
|
||||
|
||||
record_view.on_error = [&nav](std::string message) {
|
||||
nav.display_modal("Error", message);
|
||||
};
|
||||
// Update the radio model with the actual sampling rate.
|
||||
receiver_model.set_sampling_rate(actual_sample_rate);
|
||||
|
||||
// Get suitable anti-aliasing BPF bandwidth for MAX2837 given the actual sample rate.
|
||||
auto anti_alias_filter_bandwidth = filter_bandwidth_for_sampling_rate(actual_sample_rate);
|
||||
receiver_model.set_baseband_bandwidth(anti_alias_filter_bandwidth);
|
||||
|
||||
// Automatically switch default capture format to C8 when bandwidth setting is increased to >=1.5MHz
|
||||
if ((bandwidth >= 1500000) && (previous_bandwidth < 1500000)) {
|
||||
option_format.set_selected_index(1);
|
||||
}
|
||||
previous_bandwidth = bandwidth;
|
||||
|
||||
waterfall.start();
|
||||
};
|
||||
|
||||
receiver_model.enable();
|
||||
option_bandwidth.set_by_value(500000);
|
||||
|
||||
record_view.on_error = [&nav](std::string message) {
|
||||
nav.display_modal("Error", message);
|
||||
};
|
||||
}
|
||||
|
||||
CaptureAppView::CaptureAppView(
|
||||
NavigationView& nav,
|
||||
ReceiverModel::settings_t override)
|
||||
: CaptureAppView(nav) {
|
||||
// Settings to override when launched from another app (versus from AppSettings .ini file)
|
||||
field_frequency.set_value(override.frequency_app_override);
|
||||
}
|
||||
|
||||
CaptureAppView::~CaptureAppView() {
|
||||
|
||||
// Hack for preventing halting other apps
|
||||
// TODO: This should be also part of something global
|
||||
receiver_model.set_sampling_rate(3072000);
|
||||
receiver_model.set_baseband_bandwidth(1750000);
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::WidebandFMAudio);
|
||||
// ----------------------------
|
||||
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
void CaptureAppView::on_hide() {
|
||||
// TODO: Terrible kludge because widget system doesn't notify Waterfall that
|
||||
// it's being shown or hidden.
|
||||
waterfall.on_hide();
|
||||
View::on_hide();
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
void CaptureAppView::set_parent_rect(const Rect new_parent_rect) {
|
||||
View::set_parent_rect(new_parent_rect);
|
||||
View::set_parent_rect(new_parent_rect);
|
||||
|
||||
const ui::Rect waterfall_rect { 0, header_height, new_parent_rect.width(), new_parent_rect.height() - header_height };
|
||||
waterfall.set_parent_rect(waterfall_rect);
|
||||
ui::Rect waterfall_rect{0, header_height, new_parent_rect.width(), new_parent_rect.height() - header_height};
|
||||
waterfall.set_parent_rect(waterfall_rect);
|
||||
}
|
||||
|
||||
void CaptureAppView::focus() {
|
||||
record_view.focus();
|
||||
record_view.focus();
|
||||
}
|
||||
|
||||
void CaptureAppView::on_tuning_frequency_changed(rf::Frequency f) {
|
||||
receiver_model.set_tuning_frequency(f);
|
||||
void CaptureAppView::on_freqchg(int64_t freq) {
|
||||
field_frequency.set_value(freq);
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -26,94 +26,99 @@
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "ui_record_view.hpp"
|
||||
#include "ui_spectrum.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "file_path.hpp"
|
||||
|
||||
namespace ui {
|
||||
|
||||
class CaptureAppView : public View {
|
||||
public:
|
||||
CaptureAppView(NavigationView& nav);
|
||||
~CaptureAppView();
|
||||
public:
|
||||
CaptureAppView(NavigationView& nav);
|
||||
CaptureAppView(NavigationView& nav, ReceiverModel::settings_t override);
|
||||
~CaptureAppView();
|
||||
|
||||
void on_hide() override;
|
||||
void focus() override;
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
std::string title() const override { return "Capture"; };
|
||||
|
||||
void focus() override;
|
||||
private:
|
||||
static constexpr ui::Dim header_height = 3 * 16;
|
||||
uint32_t previous_bandwidth{500000};
|
||||
|
||||
std::string title() const override { return "Capture"; };
|
||||
NavigationView& nav_;
|
||||
RxRadioState radio_state_{ReceiverModel::Mode::Capture};
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_capture", app_settings::Mode::RX};
|
||||
|
||||
private:
|
||||
static constexpr ui::Dim header_height = 3 * 16;
|
||||
Labels labels{
|
||||
{{0 * 8, 1 * 16}, "Rate:", Theme::getInstance()->fg_light->foreground},
|
||||
{{11 * 8, 1 * 16}, "Format:", Theme::getInstance()->fg_light->foreground},
|
||||
};
|
||||
|
||||
uint32_t sampling_rate = 0;
|
||||
uint32_t anti_alias_baseband_bandwidth_filter = 2500000; // we rename the previous var , and change type static constexpr to normal var.
|
||||
RSSI rssi{
|
||||
{24 * 8, 0, 6 * 8, 4}};
|
||||
|
||||
void on_tuning_frequency_changed(rf::Frequency f);
|
||||
Channel channel{
|
||||
{24 * 8, 5, 6 * 8, 4}};
|
||||
|
||||
Labels labels {
|
||||
{ { 0 * 8, 1 * 16 }, "Rate:", Color::light_grey() },
|
||||
};
|
||||
|
||||
RSSI rssi {
|
||||
{ 24 * 8, 0, 6 * 8, 4 },
|
||||
};
|
||||
RxFrequencyField field_frequency{
|
||||
{0 * 8, 0 * 16},
|
||||
nav_};
|
||||
|
||||
Channel channel {
|
||||
{ 24 * 8, 5, 6 * 8, 4 },
|
||||
};
|
||||
FrequencyStepView field_frequency_step{
|
||||
{10 * 8, 0 * 16}};
|
||||
|
||||
FrequencyField field_frequency {
|
||||
{ 0 * 8, 0 * 16 },
|
||||
};
|
||||
|
||||
FrequencyStepView field_frequency_step {
|
||||
{ 10 * 8, 0 * 16 },
|
||||
};
|
||||
RFAmpField field_rf_amp{
|
||||
{16 * 8, 0 * 16}};
|
||||
|
||||
RFAmpField field_rf_amp {
|
||||
{ 16 * 8, 0 * 16 }
|
||||
};
|
||||
LNAGainField field_lna{
|
||||
{18 * 8, 0 * 16}};
|
||||
|
||||
LNAGainField field_lna {
|
||||
{ 18 * 8, 0 * 16 }
|
||||
};
|
||||
VGAGainField field_vga{
|
||||
{21 * 8, 0 * 16}};
|
||||
|
||||
VGAGainField field_vga {
|
||||
{ 21 * 8, 0 * 16 }
|
||||
};
|
||||
OptionsField option_bandwidth{
|
||||
{5 * 8, 1 * 16},
|
||||
5,
|
||||
{}};
|
||||
|
||||
OptionsField option_bandwidth {
|
||||
{ 5 * 8, 1 * 16 },
|
||||
5,
|
||||
{
|
||||
{ " 8k5", 8500 },
|
||||
{ " 11k ", 11000 },
|
||||
{ " 16k ", 16000 },
|
||||
{ " 25k ", 25000 },
|
||||
{ " 50k ", 50000 },
|
||||
{ "100k ", 100000 },
|
||||
{ "250k ", 250000 },
|
||||
{ "500k ", 500000 }, // Previous Limit bandwith Option with perfect micro SD write .C16 format operaton.
|
||||
{ "600k ", 600000 }, // That extended option is still possible to record with FW version Mayhem v1.41 (< 2,5MB/sec)
|
||||
{ "750k ", 750000 }, // From that BW onwards, the LCD is ok, but the recorded file is auto decimated,(not real file size)
|
||||
{ "1100k", 1100000 },
|
||||
{ "1750k", 1750000 },
|
||||
{ "2000k", 2000000 },
|
||||
{ "2500k", 2500000 },
|
||||
{ "2750k", 2750000 } // That is our max Capture option , to keep using later / 8 decimation (22Mhz sampling ADC)
|
||||
}
|
||||
};
|
||||
|
||||
RecordView record_view {
|
||||
{ 0 * 8, 2 * 16, 30 * 8, 1 * 16 },
|
||||
u"BBD_????", RecordView::FileType::RawS16, 16384, 3
|
||||
};
|
||||
OptionsField option_format{
|
||||
{18 * 8, 1 * 16},
|
||||
3,
|
||||
{{"C16", RecordView::FileType::RawS16},
|
||||
{"C8", RecordView::FileType::RawS8}}};
|
||||
|
||||
spectrum::WaterfallWidget waterfall { };
|
||||
Checkbox check_trim{
|
||||
{23 * 8, 1 * 16},
|
||||
4,
|
||||
"Trim",
|
||||
/*small*/ true};
|
||||
|
||||
RecordView record_view{
|
||||
{0 * 8, 2 * 16, 30 * 8, 1 * 16},
|
||||
u"BBD_????.*",
|
||||
captures_dir,
|
||||
RecordView::FileType::RawS16,
|
||||
16384,
|
||||
3};
|
||||
|
||||
spectrum::WaterfallView waterfall{};
|
||||
|
||||
MessageHandlerRegistration message_handler_freqchg{
|
||||
Message::ID::FreqChangeCommand,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const FreqChangeCommandMessage*>(p);
|
||||
this->on_freqchg(message->freq);
|
||||
}};
|
||||
|
||||
void on_freqchg(int64_t freq);
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
#endif/*__CAPTURE_APP_HPP__*/
|
||||
#endif /*__CAPTURE_APP_HPP__*/
|
||||
|
||||
@@ -52,54 +52,53 @@
|
||||
|
||||
// Default version number, if not overriden by the Makefile
|
||||
#ifndef MODES_DUMP1090_VERSION
|
||||
# define MODES_DUMP1090_VERSION "unknown"
|
||||
#define MODES_DUMP1090_VERSION "unknown"
|
||||
#endif
|
||||
|
||||
#ifndef MODES_DUMP1090_VARIANT
|
||||
# define MODES_DUMP1090_VARIANT "dump1090-unknown"
|
||||
#define MODES_DUMP1090_VARIANT "dump1090-unknown"
|
||||
#endif
|
||||
|
||||
#define MODES_DEFAULT_FREQ 1090000000
|
||||
#define MODES_DEFAULT_WIDTH 1000
|
||||
#define MODES_DEFAULT_HEIGHT 700
|
||||
#define MODES_RTL_BUFFERS 15 // Number of RTL buffers
|
||||
#define MODES_RTL_BUF_SIZE (16 * 16384) // 256k
|
||||
#define MODES_MAG_BUF_SAMPLES (MODES_RTL_BUF_SIZE / 2) // Each sample is 2 bytes
|
||||
#define MODES_MAG_BUFFERS 12 // Number of magnitude buffers (should be smaller than RTL_BUFFERS for flowcontrol to work)
|
||||
#define MODES_AUTO_GAIN -100 // Use automatic gain
|
||||
#define MODES_MAX_GAIN 999999 // Use max available gain
|
||||
#define MODES_MSG_SQUELCH_DB 4.0 // Minimum SNR, in dB
|
||||
#define MODES_MSG_ENCODER_ERRS 3 // Maximum number of encoding errors
|
||||
|
||||
#define MODES_DEFAULT_FREQ 1090000000
|
||||
#define MODES_DEFAULT_WIDTH 1000
|
||||
#define MODES_DEFAULT_HEIGHT 700
|
||||
#define MODES_RTL_BUFFERS 15 // Number of RTL buffers
|
||||
#define MODES_RTL_BUF_SIZE (16*16384) // 256k
|
||||
#define MODES_MAG_BUF_SAMPLES (MODES_RTL_BUF_SIZE / 2) // Each sample is 2 bytes
|
||||
#define MODES_MAG_BUFFERS 12 // Number of magnitude buffers (should be smaller than RTL_BUFFERS for flowcontrol to work)
|
||||
#define MODES_AUTO_GAIN -100 // Use automatic gain
|
||||
#define MODES_MAX_GAIN 999999 // Use max available gain
|
||||
#define MODES_MSG_SQUELCH_DB 4.0 // Minimum SNR, in dB
|
||||
#define MODES_MSG_ENCODER_ERRS 3 // Maximum number of encoding errors
|
||||
#define MODEAC_MSG_SAMPLES (25 * 2) // include up to the SPI bit
|
||||
#define MODEAC_MSG_BYTES 2
|
||||
#define MODEAC_MSG_SQUELCH_LEVEL 0x07FF // Average signal strength limit
|
||||
|
||||
#define MODEAC_MSG_SAMPLES (25 * 2) // include up to the SPI bit
|
||||
#define MODEAC_MSG_BYTES 2
|
||||
#define MODEAC_MSG_SQUELCH_LEVEL 0x07FF // Average signal strength limit
|
||||
#define MODES_PREAMBLE_US 8 // microseconds = bits
|
||||
#define MODES_PREAMBLE_SAMPLES (MODES_PREAMBLE_US * 2)
|
||||
#define MODES_PREAMBLE_SIZE (MODES_PREAMBLE_SAMPLES * sizeof(uint16_t))
|
||||
#define MODES_LONG_MSG_BYTES 14
|
||||
#define MODES_SHORT_MSG_BYTES 7
|
||||
#define MODES_LONG_MSG_BITS (MODES_LONG_MSG_BYTES * 8)
|
||||
#define MODES_SHORT_MSG_BITS (MODES_SHORT_MSG_BYTES * 8)
|
||||
#define MODES_LONG_MSG_SAMPLES (MODES_LONG_MSG_BITS * 2)
|
||||
#define MODES_SHORT_MSG_SAMPLES (MODES_SHORT_MSG_BITS * 2)
|
||||
#define MODES_LONG_MSG_SIZE (MODES_LONG_MSG_SAMPLES * sizeof(uint16_t))
|
||||
#define MODES_SHORT_MSG_SIZE (MODES_SHORT_MSG_SAMPLES * sizeof(uint16_t))
|
||||
|
||||
#define MODES_PREAMBLE_US 8 // microseconds = bits
|
||||
#define MODES_PREAMBLE_SAMPLES (MODES_PREAMBLE_US * 2)
|
||||
#define MODES_PREAMBLE_SIZE (MODES_PREAMBLE_SAMPLES * sizeof(uint16_t))
|
||||
#define MODES_LONG_MSG_BYTES 14
|
||||
#define MODES_SHORT_MSG_BYTES 7
|
||||
#define MODES_LONG_MSG_BITS (MODES_LONG_MSG_BYTES * 8)
|
||||
#define MODES_SHORT_MSG_BITS (MODES_SHORT_MSG_BYTES * 8)
|
||||
#define MODES_LONG_MSG_SAMPLES (MODES_LONG_MSG_BITS * 2)
|
||||
#define MODES_SHORT_MSG_SAMPLES (MODES_SHORT_MSG_BITS * 2)
|
||||
#define MODES_LONG_MSG_SIZE (MODES_LONG_MSG_SAMPLES * sizeof(uint16_t))
|
||||
#define MODES_SHORT_MSG_SIZE (MODES_SHORT_MSG_SAMPLES * sizeof(uint16_t))
|
||||
|
||||
#define MODES_OS_PREAMBLE_SAMPLES (20)
|
||||
#define MODES_OS_PREAMBLE_SIZE (MODES_OS_PREAMBLE_SAMPLES * sizeof(uint16_t))
|
||||
#define MODES_OS_LONG_MSG_SAMPLES (268)
|
||||
#define MODES_OS_PREAMBLE_SAMPLES (20)
|
||||
#define MODES_OS_PREAMBLE_SIZE (MODES_OS_PREAMBLE_SAMPLES * sizeof(uint16_t))
|
||||
#define MODES_OS_LONG_MSG_SAMPLES (268)
|
||||
#define MODES_OS_SHORT_MSG_SAMPLES (135)
|
||||
#define MODES_OS_LONG_MSG_SIZE (MODES_LONG_MSG_SAMPLES * sizeof(uint16_t))
|
||||
#define MODES_OS_SHORT_MSG_SIZE (MODES_SHORT_MSG_SAMPLES * sizeof(uint16_t))
|
||||
#define MODES_OS_LONG_MSG_SIZE (MODES_LONG_MSG_SAMPLES * sizeof(uint16_t))
|
||||
#define MODES_OS_SHORT_MSG_SIZE (MODES_SHORT_MSG_SAMPLES * sizeof(uint16_t))
|
||||
|
||||
#define MODES_OUT_BUF_SIZE (1500)
|
||||
#define MODES_OUT_FLUSH_SIZE (MODES_OUT_BUF_SIZE - 256)
|
||||
#define MODES_OUT_FLUSH_INTERVAL (60000)
|
||||
#define MODES_OUT_BUF_SIZE (1500)
|
||||
#define MODES_OUT_FLUSH_SIZE (MODES_OUT_BUF_SIZE - 256)
|
||||
#define MODES_OUT_FLUSH_INTERVAL (60000)
|
||||
|
||||
#define MODES_USER_LATLON_VALID (1<<0)
|
||||
#define MODES_USER_LATLON_VALID (1 << 0)
|
||||
|
||||
#define INVALID_ALTITUDE (-9999)
|
||||
|
||||
@@ -119,19 +118,19 @@ typedef enum {
|
||||
* (Earlier values are higher priority)
|
||||
*/
|
||||
typedef enum {
|
||||
ADDR_ADSB_ICAO, /* Mode S or ADS-B, ICAO address, transponder sourced */
|
||||
ADDR_ADSB_ICAO_NT, /* ADS-B, ICAO address, non-transponder */
|
||||
ADDR_ADSR_ICAO, /* ADS-R, ICAO address */
|
||||
ADDR_TISB_ICAO, /* TIS-B, ICAO address */
|
||||
ADDR_ADSB_ICAO, /* Mode S or ADS-B, ICAO address, transponder sourced */
|
||||
ADDR_ADSB_ICAO_NT, /* ADS-B, ICAO address, non-transponder */
|
||||
ADDR_ADSR_ICAO, /* ADS-R, ICAO address */
|
||||
ADDR_TISB_ICAO, /* TIS-B, ICAO address */
|
||||
|
||||
ADDR_ADSB_OTHER, /* ADS-B, other address format */
|
||||
ADDR_ADSR_OTHER, /* ADS-R, other address format */
|
||||
ADDR_TISB_TRACKFILE, /* TIS-B, Mode A code + track file number */
|
||||
ADDR_TISB_OTHER, /* TIS-B, other address format */
|
||||
ADDR_ADSB_OTHER, /* ADS-B, other address format */
|
||||
ADDR_ADSR_OTHER, /* ADS-R, other address format */
|
||||
ADDR_TISB_TRACKFILE, /* TIS-B, Mode A code + track file number */
|
||||
ADDR_TISB_OTHER, /* TIS-B, other address format */
|
||||
|
||||
ADDR_MODE_A, /* Mode A */
|
||||
ADDR_MODE_A, /* Mode A */
|
||||
|
||||
ADDR_UNKNOWN /* unknown address format */
|
||||
ADDR_UNKNOWN /* unknown address format */
|
||||
} addrtype_t;
|
||||
|
||||
typedef enum {
|
||||
@@ -158,20 +157,25 @@ typedef enum {
|
||||
} airground_t;
|
||||
|
||||
typedef enum {
|
||||
SIL_INVALID, SIL_UNKNOWN, SIL_PER_SAMPLE, SIL_PER_HOUR
|
||||
SIL_INVALID,
|
||||
SIL_UNKNOWN,
|
||||
SIL_PER_SAMPLE,
|
||||
SIL_PER_HOUR
|
||||
} sil_type_t;
|
||||
|
||||
typedef enum {
|
||||
CPR_SURFACE, CPR_AIRBORNE, CPR_COARSE
|
||||
CPR_SURFACE,
|
||||
CPR_AIRBORNE,
|
||||
CPR_COARSE
|
||||
} cpr_type_t;
|
||||
|
||||
typedef enum {
|
||||
HEADING_INVALID, // Not set
|
||||
HEADING_GROUND_TRACK, // Direction of track over ground, degrees clockwise from true north
|
||||
HEADING_TRUE, // Heading, degrees clockwise from true north
|
||||
HEADING_MAGNETIC, // Heading, degrees clockwise from magnetic north
|
||||
HEADING_MAGNETIC_OR_TRUE, // HEADING_MAGNETIC or HEADING_TRUE depending on the HRD bit in opstatus
|
||||
HEADING_TRACK_OR_HEADING // GROUND_TRACK / MAGNETIC / TRUE depending on the TAH bit in opstatus
|
||||
HEADING_INVALID, // Not set
|
||||
HEADING_GROUND_TRACK, // Direction of track over ground, degrees clockwise from true north
|
||||
HEADING_TRUE, // Heading, degrees clockwise from true north
|
||||
HEADING_MAGNETIC, // Heading, degrees clockwise from magnetic north
|
||||
HEADING_MAGNETIC_OR_TRUE, // HEADING_MAGNETIC or HEADING_TRUE depending on the HRD bit in opstatus
|
||||
HEADING_TRACK_OR_HEADING // GROUND_TRACK / MAGNETIC / TRUE depending on the TAH bit in opstatus
|
||||
} heading_type_t;
|
||||
|
||||
typedef enum {
|
||||
@@ -209,61 +213,68 @@ typedef enum {
|
||||
} emergency_t;
|
||||
|
||||
typedef enum {
|
||||
NAV_ALT_INVALID, NAV_ALT_UNKNOWN, NAV_ALT_AIRCRAFT, NAV_ALT_MCP, NAV_ALT_FMS
|
||||
NAV_ALT_INVALID,
|
||||
NAV_ALT_UNKNOWN,
|
||||
NAV_ALT_AIRCRAFT,
|
||||
NAV_ALT_MCP,
|
||||
NAV_ALT_FMS
|
||||
} nav_altitude_source_t;
|
||||
|
||||
#define MODES_NON_ICAO_ADDRESS (1<<24) // Set on addresses to indicate they are not ICAO addresses
|
||||
#define MODES_NON_ICAO_ADDRESS (1 << 24) // Set on addresses to indicate they are not ICAO addresses
|
||||
|
||||
#define MODES_INTERACTIVE_REFRESH_TIME 250 // Milliseconds
|
||||
#define MODES_INTERACTIVE_DISPLAY_TTL 60000 // Delete from display after 60 seconds
|
||||
#define MODES_INTERACTIVE_REFRESH_TIME 250 // Milliseconds
|
||||
#define MODES_INTERACTIVE_DISPLAY_TTL 60000 // Delete from display after 60 seconds
|
||||
|
||||
#define MODES_NET_HEARTBEAT_INTERVAL 60000 // milliseconds
|
||||
#define MODES_NET_HEARTBEAT_INTERVAL 60000 // milliseconds
|
||||
|
||||
#define MODES_CLIENT_BUF_SIZE 1024
|
||||
#define MODES_NET_SNDBUF_SIZE (1024*64)
|
||||
#define MODES_NET_SNDBUF_MAX (7)
|
||||
#define MODES_CLIENT_BUF_SIZE 1024
|
||||
#define MODES_NET_SNDBUF_SIZE (1024 * 64)
|
||||
#define MODES_NET_SNDBUF_MAX (7)
|
||||
|
||||
#define HISTORY_SIZE 120
|
||||
#define HISTORY_INTERVAL 30000
|
||||
|
||||
#define MODES_NOTUSED(V) ((void) V)
|
||||
#define MODES_NOTUSED(V) ((void)V)
|
||||
|
||||
#define MAX_AMPLITUDE 65535.0
|
||||
#define MAX_POWER (MAX_AMPLITUDE * MAX_AMPLITUDE)
|
||||
|
||||
#define FAUP_DEFAULT_RATE_MULTIPLIER 1.0 // FA Upload rate multiplier
|
||||
|
||||
#define FAUP_DEFAULT_RATE_MULTIPLIER 1.0 // FA Upload rate multiplier
|
||||
|
||||
//======================== structure declarations =========================
|
||||
|
||||
typedef enum {
|
||||
SDR_NONE, SDR_IFILE, SDR_RTLSDR, SDR_BLADERF, SDR_HACKRF, SDR_LIMESDR
|
||||
SDR_NONE,
|
||||
SDR_IFILE,
|
||||
SDR_RTLSDR,
|
||||
SDR_BLADERF,
|
||||
SDR_HACKRF,
|
||||
SDR_LIMESDR
|
||||
} sdr_type_t;
|
||||
|
||||
|
||||
// The struct we use to store information about a decoded message.
|
||||
struct modesMessage {
|
||||
// Generic fields
|
||||
unsigned char msg[MODES_LONG_MSG_BYTES]; // Binary message.
|
||||
unsigned char verbatim[MODES_LONG_MSG_BYTES]; // Binary message, as originally received before correction
|
||||
int msgbits; // Number of bits in message
|
||||
int msgtype; // Downlink format #
|
||||
uint32_t crc; // Message CRC
|
||||
int correctedbits; // No. of bits corrected
|
||||
uint32_t addr; // Address Announced
|
||||
addrtype_t addrtype; // address format / source
|
||||
uint64_t timestampMsg; // Timestamp of the message (12MHz clock)
|
||||
uint64_t sysTimestampMsg; // Timestamp of the message (system time)
|
||||
int remote; // If set this message is from a remote station
|
||||
double signalLevel; // RSSI, in the range [0..1], as a fraction of full-scale power
|
||||
int score; // Scoring from scoreModesMessage, if used
|
||||
int reliable; // is this a "reliable" message (uncorrected DF11/DF17/DF18)?
|
||||
unsigned char msg[MODES_LONG_MSG_BYTES]; // Binary message.
|
||||
unsigned char verbatim[MODES_LONG_MSG_BYTES]; // Binary message, as originally received before correction
|
||||
int msgbits; // Number of bits in message
|
||||
int msgtype; // Downlink format #
|
||||
uint32_t crc; // Message CRC
|
||||
int correctedbits; // No. of bits corrected
|
||||
uint32_t addr; // Address Announced
|
||||
addrtype_t addrtype; // address format / source
|
||||
uint64_t timestampMsg; // Timestamp of the message (12MHz clock)
|
||||
uint64_t sysTimestampMsg; // Timestamp of the message (system time)
|
||||
int remote; // If set this message is from a remote station
|
||||
double signalLevel; // RSSI, in the range [0..1], as a fraction of full-scale power
|
||||
int score; // Scoring from scoreModesMessage, if used
|
||||
int reliable; // is this a "reliable" message (uncorrected DF11/DF17/DF18)?
|
||||
|
||||
datasource_t source; // Characterizes the overall message source
|
||||
datasource_t source; // Characterizes the overall message source
|
||||
|
||||
// Raw data, just extracted directly from the message
|
||||
// The names reflect the field names in Annex 4
|
||||
unsigned IID; // extracted from CRC of DF11s
|
||||
unsigned IID; // extracted from CRC of DF11s
|
||||
unsigned AA;
|
||||
unsigned AC;
|
||||
unsigned CA;
|
||||
@@ -312,25 +323,25 @@ struct modesMessage {
|
||||
unsigned alert : 1;
|
||||
unsigned emergency_valid : 1;
|
||||
|
||||
unsigned metype; // DF17/18 ME type
|
||||
unsigned mesub; // DF17/18 ME subtype
|
||||
unsigned metype; // DF17/18 ME type
|
||||
unsigned mesub; // DF17/18 ME subtype
|
||||
|
||||
commb_format_t commb_format; // Inferred format of a comm-b message
|
||||
commb_format_t commb_format; // Inferred format of a comm-b message
|
||||
|
||||
// valid if altitude_baro_valid:
|
||||
int altitude_baro; // Altitude in either feet or meters
|
||||
altitude_unit_t altitude_baro_unit; // the unit used for altitude
|
||||
int altitude_baro; // Altitude in either feet or meters
|
||||
altitude_unit_t altitude_baro_unit; // the unit used for altitude
|
||||
|
||||
// valid if altitude_geom_valid:
|
||||
int altitude_geom; // Altitude in either feet or meters
|
||||
altitude_unit_t altitude_geom_unit; // the unit used for altitude
|
||||
int altitude_geom; // Altitude in either feet or meters
|
||||
altitude_unit_t altitude_geom_unit; // the unit used for altitude
|
||||
|
||||
// following fields are valid if the corresponding _valid field is set:
|
||||
int geom_delta; // Difference between geometric and baro alt
|
||||
float heading; // ground track or heading, degrees (0-359). Reported directly or computed from from EW and NS velocity
|
||||
heading_type_t heading_type;// how to interpret 'track_or_heading'
|
||||
float track_rate; // Rate of change of track, degrees/second
|
||||
float roll; // Roll, degrees, negative is left roll
|
||||
int geom_delta; // Difference between geometric and baro alt
|
||||
float heading; // ground track or heading, degrees (0-359). Reported directly or computed from from EW and NS velocity
|
||||
heading_type_t heading_type; // how to interpret 'track_or_heading'
|
||||
float track_rate; // Rate of change of track, degrees/second
|
||||
float roll; // Roll, degrees, negative is left roll
|
||||
struct {
|
||||
// Groundspeed, kts, reported directly or computed from from EW and NS velocity
|
||||
// For surface movement, this has different interpretations for v0 and v2; both
|
||||
@@ -339,23 +350,23 @@ struct modesMessage {
|
||||
float v2;
|
||||
float selected;
|
||||
} gs;
|
||||
unsigned ias; // Indicated airspeed, kts
|
||||
unsigned tas; // True airspeed, kts
|
||||
double mach; // Mach number
|
||||
int baro_rate; // Rate of change of barometric altitude, feet/minute
|
||||
int geom_rate; // Rate of change of geometric (GNSS / INS) altitude, feet/minute
|
||||
unsigned squawk; // 13 bits identity (Squawk), encoded as 4 hex digits
|
||||
char callsign[9]; // 8 chars flight number, NUL-terminated
|
||||
unsigned category; // A0 - D7 encoded as a single hex byte
|
||||
emergency_t emergency; // emergency/priority status
|
||||
unsigned ias; // Indicated airspeed, kts
|
||||
unsigned tas; // True airspeed, kts
|
||||
double mach; // Mach number
|
||||
int baro_rate; // Rate of change of barometric altitude, feet/minute
|
||||
int geom_rate; // Rate of change of geometric (GNSS / INS) altitude, feet/minute
|
||||
unsigned squawk; // 13 bits identity (Squawk), encoded as 4 hex digits
|
||||
char callsign[9]; // 8 chars flight number, NUL-terminated
|
||||
unsigned category; // A0 - D7 encoded as a single hex byte
|
||||
emergency_t emergency; // emergency/priority status
|
||||
|
||||
// valid if cpr_valid
|
||||
cpr_type_t cpr_type; // The encoding type used (surface, airborne, coarse TIS-B)
|
||||
unsigned cpr_lat; // Non decoded latitude.
|
||||
unsigned cpr_lon; // Non decoded longitude.
|
||||
unsigned cpr_nucp; // NUCp/NIC value implied by message type
|
||||
cpr_type_t cpr_type; // The encoding type used (surface, airborne, coarse TIS-B)
|
||||
unsigned cpr_lat; // Non decoded latitude.
|
||||
unsigned cpr_lon; // Non decoded longitude.
|
||||
unsigned cpr_nucp; // NUCp/NIC value implied by message type
|
||||
|
||||
airground_t airground; // air/ground state
|
||||
airground_t airground; // air/ground state
|
||||
|
||||
// valid if cpr_decoded:
|
||||
double decoded_lat;
|
||||
@@ -374,20 +385,20 @@ struct modesMessage {
|
||||
unsigned gva_valid : 1;
|
||||
unsigned sda_valid : 1;
|
||||
|
||||
unsigned nic_a : 1; // if nic_a_valid
|
||||
unsigned nic_b : 1; // if nic_b_valid
|
||||
unsigned nic_c : 1; // if nic_c_valid
|
||||
unsigned nic_baro : 1; // if nic_baro_valid
|
||||
unsigned nic_a : 1; // if nic_a_valid
|
||||
unsigned nic_b : 1; // if nic_b_valid
|
||||
unsigned nic_c : 1; // if nic_c_valid
|
||||
unsigned nic_baro : 1; // if nic_baro_valid
|
||||
|
||||
unsigned nac_p : 4; // if nac_p_valid
|
||||
unsigned nac_v : 3; // if nac_v_valid
|
||||
unsigned nac_p : 4; // if nac_p_valid
|
||||
unsigned nac_v : 3; // if nac_v_valid
|
||||
|
||||
unsigned sil : 2; // if sil_type != SIL_INVALID
|
||||
unsigned sil : 2; // if sil_type != SIL_INVALID
|
||||
sil_type_t sil_type;
|
||||
|
||||
unsigned gva : 2; // if gva_valid
|
||||
unsigned gva : 2; // if gva_valid
|
||||
|
||||
unsigned sda : 2; // if sda_valid
|
||||
unsigned sda : 2; // if sda_valid
|
||||
} accuracy;
|
||||
|
||||
// Operational Status
|
||||
@@ -428,11 +439,11 @@ struct modesMessage {
|
||||
unsigned qnh_valid : 1;
|
||||
unsigned modes_valid : 1;
|
||||
|
||||
float heading; // heading, degrees (0-359) (could be magnetic or true heading; magnetic recommended)
|
||||
float heading; // heading, degrees (0-359) (could be magnetic or true heading; magnetic recommended)
|
||||
heading_type_t heading_type;
|
||||
unsigned fms_altitude; // FMS selected altitude
|
||||
unsigned mcp_altitude; // MCP/FCU selected altitude
|
||||
float qnh; // altimeter setting (QFE or QNH/QNE), millibars
|
||||
float qnh; // altimeter setting (QFE or QNH/QNE), millibars
|
||||
|
||||
nav_altitude_source_t altitude_source;
|
||||
|
||||
@@ -440,5 +451,4 @@ struct modesMessage {
|
||||
} nav;
|
||||
};
|
||||
|
||||
|
||||
#endif // __DUMP1090_H
|
||||
#endif // __DUMP1090_H
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2023 Mark Thompson
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
@@ -22,7 +23,7 @@
|
||||
#include "ert_app.hpp"
|
||||
|
||||
#include "baseband_api.hpp"
|
||||
|
||||
#include "audio.hpp"
|
||||
#include "portapack.hpp"
|
||||
using namespace portapack;
|
||||
|
||||
@@ -30,144 +31,155 @@ using namespace portapack;
|
||||
|
||||
#include "crc.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "file_path.hpp"
|
||||
|
||||
namespace pmem = portapack::persistent_memory;
|
||||
|
||||
namespace ert {
|
||||
|
||||
namespace format {
|
||||
|
||||
std::string type(Packet::Type value) {
|
||||
switch(value) {
|
||||
default:
|
||||
case Packet::Type::Unknown: return "???";
|
||||
case Packet::Type::IDM: return "IDM";
|
||||
case Packet::Type::SCM: return "SCM";
|
||||
}
|
||||
switch (value) {
|
||||
default:
|
||||
case Packet::Type::Unknown:
|
||||
return "???";
|
||||
case Packet::Type::IDM:
|
||||
return "IDM";
|
||||
case Packet::Type::SCM:
|
||||
return "SCM";
|
||||
case Packet::Type::SCMPLUS:
|
||||
return "SCM+";
|
||||
}
|
||||
}
|
||||
|
||||
std::string id(ID value) {
|
||||
return to_string_dec_uint(value, 10);
|
||||
return to_string_dec_uint(value, 10);
|
||||
}
|
||||
|
||||
std::string consumption(Consumption value) {
|
||||
return to_string_dec_uint(value, 10);
|
||||
return to_string_dec_uint(value, 8);
|
||||
}
|
||||
|
||||
std::string commodity_type(CommodityType value) {
|
||||
return to_string_dec_uint(value, 2);
|
||||
return to_string_dec_uint(value, 2);
|
||||
}
|
||||
|
||||
std::string tamper_flags(TamperFlags value) {
|
||||
return to_string_hex(value & 0xFFFF, 4); // Note: ignoring bits 32-47 of tamper flags in IDM type due to screen width
|
||||
}
|
||||
|
||||
std::string tamper_flags_scm(TamperFlags value) {
|
||||
return " " + to_string_hex(value & 0x0F, 1) + "/" + to_string_hex(value >> 4, 1); // Physical/Encoder flags
|
||||
}
|
||||
|
||||
} /* namespace format */
|
||||
|
||||
} /* namespace ert */
|
||||
|
||||
void ERTLogger::on_packet(const ert::Packet& packet) {
|
||||
const auto formatted = packet.symbols_formatted();
|
||||
log_file.write_entry(packet.received_at(), formatted.data + "/" + formatted.errors);
|
||||
void ERTLogger::on_packet(const ert::Packet& packet, const uint32_t target_frequency) {
|
||||
const auto formatted = packet.symbols_formatted();
|
||||
const auto target_frequency_str = to_string_dec_uint(target_frequency, 10);
|
||||
|
||||
std::string entry = target_frequency_str + " " + ert::format::type(packet.type()) + " " + formatted.data + "/" + formatted.errors + " ID:" + to_string_dec_uint(packet.id(), 1);
|
||||
|
||||
log_file.write_entry(packet.received_at(), entry);
|
||||
}
|
||||
|
||||
const ERTRecentEntry::Key ERTRecentEntry::invalid_key { };
|
||||
const ERTRecentEntry::Key ERTRecentEntry::invalid_key{};
|
||||
|
||||
void ERTRecentEntry::update(const ert::Packet& packet) {
|
||||
received_count++;
|
||||
received_count++;
|
||||
|
||||
last_consumption = packet.consumption();
|
||||
last_consumption = packet.consumption();
|
||||
last_tamper_flags = packet.tamper_flags();
|
||||
packet_type = packet.type();
|
||||
}
|
||||
|
||||
namespace ui {
|
||||
|
||||
template<>
|
||||
template <>
|
||||
void RecentEntriesTable<ERTRecentEntries>::draw(
|
||||
const Entry& entry,
|
||||
const Rect& target_rect,
|
||||
Painter& painter,
|
||||
const Style& style
|
||||
) {
|
||||
std::string line = ert::format::id(entry.id) + " " + ert::format::commodity_type(entry.commodity_type) + " " + ert::format::consumption(entry.last_consumption);
|
||||
const Entry& entry,
|
||||
const Rect& target_rect,
|
||||
Painter& painter,
|
||||
const Style& style) {
|
||||
std::string line = ert::format::id(entry.id) + " " + ert::format::commodity_type(entry.commodity_type) + " " + ert::format::consumption(entry.last_consumption) + " ";
|
||||
|
||||
if( entry.received_count > 999 ) {
|
||||
line += " +++";
|
||||
} else {
|
||||
line += " " + to_string_dec_uint(entry.received_count, 3);
|
||||
}
|
||||
line += (entry.packet_type == ert::Packet::Type::SCM) ? ert::format::tamper_flags_scm(entry.last_tamper_flags) : ert::format::tamper_flags(entry.last_tamper_flags);
|
||||
line += (entry.received_count > 99) ? " ++" : to_string_dec_uint(entry.received_count, 3);
|
||||
|
||||
line.resize(target_rect.width() / 8, ' ');
|
||||
painter.draw_string(target_rect.location(), style, line);
|
||||
line.resize(target_rect.width() / 8, ' ');
|
||||
painter.draw_string(target_rect.location(), style, line);
|
||||
}
|
||||
|
||||
ERTAppView::ERTAppView(NavigationView&) {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_ert);
|
||||
ERTAppView::ERTAppView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_ert);
|
||||
|
||||
add_children({
|
||||
&field_rf_amp,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&rssi,
|
||||
&recent_entries_view,
|
||||
});
|
||||
add_children({
|
||||
&field_frequency,
|
||||
&field_rf_amp,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&rssi,
|
||||
&field_volume,
|
||||
&recent_entries_view,
|
||||
});
|
||||
|
||||
// load app settings
|
||||
auto rc = settings.load("rx_ert", &app_settings);
|
||||
if(rc == SETTINGS_OK) {
|
||||
field_lna.set_value(app_settings.lna);
|
||||
field_vga.set_value(app_settings.vga);
|
||||
field_rf_amp.set_value(app_settings.rx_amp);
|
||||
}
|
||||
field_frequency.set_step(1000000);
|
||||
|
||||
receiver_model.set_tuning_frequency(initial_target_frequency);
|
||||
receiver_model.set_sampling_rate(sampling_rate);
|
||||
receiver_model.set_baseband_bandwidth(baseband_bandwidth);
|
||||
receiver_model.enable(); // Before using radio::enable(), but not updating Ant.DC-Bias.
|
||||
receiver_model.enable();
|
||||
|
||||
/* radio::enable({
|
||||
initial_target_frequency,
|
||||
sampling_rate,
|
||||
baseband_bandwidth,
|
||||
rf::Direction::Receive,
|
||||
receiver_model.rf_amp(),
|
||||
static_cast<int8_t>(receiver_model.lna()),
|
||||
static_cast<int8_t>(receiver_model.vga()),
|
||||
}); */
|
||||
logger = std::make_unique<ERTLogger>();
|
||||
if (logger) {
|
||||
logger->append(logs_dir / u"ERT.TXT");
|
||||
}
|
||||
|
||||
logger = std::make_unique<ERTLogger>();
|
||||
if( logger ) {
|
||||
logger->append(u"ert.txt");
|
||||
}
|
||||
if (pmem::beep_on_packets()) {
|
||||
audio::set_rate(audio::Rate::Hz_24000);
|
||||
audio::output::start();
|
||||
}
|
||||
}
|
||||
|
||||
ERTAppView::~ERTAppView() {
|
||||
|
||||
// save app settings
|
||||
settings.save("rx_ert", &app_settings);
|
||||
|
||||
receiver_model.disable(); // to switch off all, including DC bias and change flag enabled_
|
||||
|
||||
baseband::shutdown();
|
||||
audio::output::stop();
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
void ERTAppView::focus() {
|
||||
field_vga.focus();
|
||||
field_vga.focus();
|
||||
}
|
||||
|
||||
void ERTAppView::set_parent_rect(const Rect new_parent_rect) {
|
||||
View::set_parent_rect(new_parent_rect);
|
||||
recent_entries_view.set_parent_rect({ 0, header_height, new_parent_rect.width(), new_parent_rect.height() - header_height });
|
||||
View::set_parent_rect(new_parent_rect);
|
||||
recent_entries_view.set_parent_rect({0, header_height, new_parent_rect.width(), new_parent_rect.height() - header_height});
|
||||
}
|
||||
|
||||
void ERTAppView::on_packet(const ert::Packet& packet) {
|
||||
if( logger ) {
|
||||
logger->on_packet(packet);
|
||||
}
|
||||
if (logger) {
|
||||
logger->on_packet(packet, receiver_model.target_frequency());
|
||||
}
|
||||
|
||||
if( packet.crc_ok() ) {
|
||||
auto& entry = ::on_packet(recent, ERTRecentEntry::Key { packet.id(), packet.commodity_type() });
|
||||
entry.update(packet);
|
||||
recent_entries_view.set_dirty();
|
||||
}
|
||||
if (packet.crc_ok()) {
|
||||
auto& entry = ::on_packet(recent, ERTRecentEntry::Key{packet.id(), packet.commodity_type()});
|
||||
entry.update(packet);
|
||||
recent_entries_view.set_dirty();
|
||||
}
|
||||
|
||||
if (pmem::beep_on_packets()) {
|
||||
baseband::request_audio_beep(1000, 24000, 60);
|
||||
}
|
||||
}
|
||||
|
||||
void ERTAppView::on_show_list() {
|
||||
recent_entries_view.hidden(false);
|
||||
recent_entries_view.focus();
|
||||
recent_entries_view.hidden(false);
|
||||
recent_entries_view.focus();
|
||||
}
|
||||
|
||||
void ERTAppView::on_freqchg(int64_t freq) {
|
||||
field_frequency.set_value(freq);
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2023 Mark Thompson
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
@@ -24,11 +25,13 @@
|
||||
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "ui_rssi.hpp"
|
||||
#include "ui_channel.hpp"
|
||||
|
||||
#include "event_m0.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "log_file.hpp"
|
||||
|
||||
#include "ert_packet.hpp"
|
||||
@@ -39,67 +42,66 @@
|
||||
#include <string>
|
||||
|
||||
struct ERTKey {
|
||||
ert::ID id;
|
||||
ert::CommodityType commodity_type;
|
||||
ert::ID id;
|
||||
ert::CommodityType commodity_type;
|
||||
|
||||
constexpr ERTKey(
|
||||
ert::ID id = ert::invalid_id,
|
||||
ert::CommodityType commodity_type = ert::invalid_commodity_type
|
||||
) : id { id },
|
||||
commodity_type { commodity_type }
|
||||
{
|
||||
}
|
||||
constexpr ERTKey(
|
||||
ert::ID id = ert::invalid_id,
|
||||
ert::CommodityType commodity_type = ert::invalid_commodity_type)
|
||||
: id{id},
|
||||
commodity_type{commodity_type} {
|
||||
}
|
||||
|
||||
ERTKey( const ERTKey& other ) = default;
|
||||
ERTKey(const ERTKey& other) = default;
|
||||
|
||||
ERTKey& operator=(const ERTKey& other) {
|
||||
id = other.id;
|
||||
commodity_type = other.commodity_type;
|
||||
return *this;
|
||||
}
|
||||
ERTKey& operator=(const ERTKey& other) {
|
||||
id = other.id;
|
||||
commodity_type = other.commodity_type;
|
||||
return *this;
|
||||
}
|
||||
|
||||
bool operator==(const ERTKey& other) const {
|
||||
return (id == other.id) && (commodity_type == other.commodity_type);
|
||||
}
|
||||
bool operator==(const ERTKey& other) const {
|
||||
return (id == other.id) && (commodity_type == other.commodity_type);
|
||||
}
|
||||
};
|
||||
|
||||
struct ERTRecentEntry {
|
||||
using Key = ERTKey;
|
||||
using Key = ERTKey;
|
||||
|
||||
// TODO: Is this the right choice of invalid key value?
|
||||
static const Key invalid_key;
|
||||
// TODO: Is this the right choice of invalid key value?
|
||||
static const Key invalid_key;
|
||||
|
||||
ert::ID id { ert::invalid_id };
|
||||
ert::CommodityType commodity_type { ert::invalid_commodity_type };
|
||||
ert::ID id{ert::invalid_id};
|
||||
ert::CommodityType commodity_type{ert::invalid_commodity_type};
|
||||
ert::Consumption last_consumption{};
|
||||
ert::TamperFlags last_tamper_flags{};
|
||||
ert::Packet::Type packet_type{};
|
||||
|
||||
size_t received_count { 0 };
|
||||
size_t received_count{0};
|
||||
|
||||
ert::Consumption last_consumption { };
|
||||
ERTRecentEntry(
|
||||
const Key& key)
|
||||
: id{key.id},
|
||||
commodity_type{key.commodity_type} {
|
||||
}
|
||||
|
||||
ERTRecentEntry(
|
||||
const Key& key
|
||||
) : id { key.id },
|
||||
commodity_type { key.commodity_type }
|
||||
{
|
||||
}
|
||||
Key key() const {
|
||||
return {id, commodity_type};
|
||||
}
|
||||
|
||||
Key key() const {
|
||||
return { id, commodity_type };
|
||||
}
|
||||
|
||||
void update(const ert::Packet& packet);
|
||||
void update(const ert::Packet& packet);
|
||||
};
|
||||
|
||||
class ERTLogger {
|
||||
public:
|
||||
Optional<File::Error> append(const std::filesystem::path& filename) {
|
||||
return log_file.append(filename);
|
||||
}
|
||||
|
||||
void on_packet(const ert::Packet& packet);
|
||||
public:
|
||||
Optional<File::Error> append(const std::filesystem::path& filename) {
|
||||
return log_file.append(filename);
|
||||
}
|
||||
|
||||
private:
|
||||
LogFile log_file { };
|
||||
void on_packet(const ert::Packet& packet, const uint32_t target_frequency);
|
||||
|
||||
private:
|
||||
LogFile log_file{};
|
||||
};
|
||||
|
||||
using ERTRecentEntries = RecentEntries<ERTRecentEntry>;
|
||||
@@ -109,72 +111,83 @@ namespace ui {
|
||||
using ERTRecentEntriesView = RecentEntriesView<ERTRecentEntries>;
|
||||
|
||||
class ERTAppView : public View {
|
||||
public:
|
||||
static constexpr uint32_t initial_target_frequency = 911600000;
|
||||
static constexpr uint32_t sampling_rate = 4194304;
|
||||
static constexpr uint32_t baseband_bandwidth = 2500000;
|
||||
public:
|
||||
ERTAppView(NavigationView& nav);
|
||||
~ERTAppView();
|
||||
|
||||
ERTAppView(NavigationView& nav);
|
||||
~ERTAppView();
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
// Prevent painting of region covered entirely by a child.
|
||||
// TODO: Add flag to View that specifies view does not need to be cleared before painting.
|
||||
void paint(Painter&) override{};
|
||||
|
||||
// Prevent painting of region covered entirely by a child.
|
||||
// TODO: Add flag to View that specifies view does not need to be cleared before painting.
|
||||
void paint(Painter&) override { };
|
||||
void focus() override;
|
||||
|
||||
void focus() override;
|
||||
std::string title() const override { return "ERT RX"; };
|
||||
|
||||
std::string title() const override { return "ERT RX"; };
|
||||
private:
|
||||
ERTRecentEntries recent{};
|
||||
std::unique_ptr<ERTLogger> logger{};
|
||||
|
||||
private:
|
||||
ERTRecentEntries recent { };
|
||||
std::unique_ptr<ERTLogger> logger { };
|
||||
NavigationView& nav_;
|
||||
RxRadioState radio_state_{
|
||||
911600000 /* frequency */,
|
||||
2500000 /* bandwidth */,
|
||||
4194304 /* sampling rate */};
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_ert", app_settings::Mode::RX};
|
||||
|
||||
// app save settings
|
||||
std::app_settings settings { };
|
||||
std::app_settings::AppSettings app_settings { };
|
||||
const RecentEntriesColumns columns{{
|
||||
{"ID", 10},
|
||||
{"Ty", 2},
|
||||
{"Consumpt", 8},
|
||||
{"Tamp", 4},
|
||||
{"Ct", 2},
|
||||
}};
|
||||
ERTRecentEntriesView recent_entries_view{columns, recent};
|
||||
|
||||
static constexpr auto header_height = 1 * 16;
|
||||
|
||||
const RecentEntriesColumns columns { {
|
||||
{ "ID", 10 },
|
||||
{ "Tp", 2 },
|
||||
{ "Consumpt", 10 },
|
||||
{ "Cnt", 3 },
|
||||
} };
|
||||
ERTRecentEntriesView recent_entries_view { columns, recent };
|
||||
RxFrequencyField field_frequency{
|
||||
{0 * 8, 0 * 16},
|
||||
nav_};
|
||||
|
||||
static constexpr auto header_height = 1 * 16;
|
||||
RFAmpField field_rf_amp{
|
||||
{13 * 8, 0 * 16}};
|
||||
|
||||
RFAmpField field_rf_amp {
|
||||
{ 13 * 8, 0 * 16 }
|
||||
};
|
||||
LNAGainField field_lna{
|
||||
{15 * 8, 0 * 16}};
|
||||
|
||||
LNAGainField field_lna {
|
||||
{ 15 * 8, 0 * 16 }
|
||||
};
|
||||
VGAGainField field_vga{
|
||||
{18 * 8, 0 * 16}};
|
||||
|
||||
VGAGainField field_vga {
|
||||
{ 18 * 8, 0 * 16 }
|
||||
};
|
||||
RSSI rssi{
|
||||
{21 * 8, 0, 6 * 8, 4},
|
||||
};
|
||||
|
||||
RSSI rssi {
|
||||
{ 21 * 8, 0, 6 * 8, 4 },
|
||||
};
|
||||
AudioVolumeField field_volume{
|
||||
{28 * 8, 0 * 16}};
|
||||
|
||||
MessageHandlerRegistration message_handler_packet {
|
||||
Message::ID::ERTPacket,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const ERTPacketMessage*>(p);
|
||||
const ert::Packet packet { message->type, message->packet };
|
||||
this->on_packet(packet);
|
||||
}
|
||||
};
|
||||
MessageHandlerRegistration message_handler_packet{
|
||||
Message::ID::ERTPacket,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const ERTPacketMessage*>(p);
|
||||
const ert::Packet packet{message->type, message->packet};
|
||||
this->on_packet(packet);
|
||||
}};
|
||||
|
||||
void on_packet(const ert::Packet& packet);
|
||||
void on_show_list();
|
||||
MessageHandlerRegistration message_handler_freqchg{
|
||||
Message::ID::FreqChangeCommand,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const FreqChangeCommandMessage*>(p);
|
||||
this->on_freqchg(message->freq);
|
||||
}};
|
||||
|
||||
void on_freqchg(int64_t freq);
|
||||
void on_packet(const ert::Packet& packet);
|
||||
void on_show_list();
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
#endif/*__ERT_APP_H__*/
|
||||
#endif /*__ERT_APP_H__*/
|
||||
|
||||
@@ -1,278 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2016 Furrtek
|
||||
* Copyright (C) 2020 Shao
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "gps_sim_app.hpp"
|
||||
#include "string_format.hpp"
|
||||
|
||||
#include "ui_fileman.hpp"
|
||||
#include "io_file.hpp"
|
||||
|
||||
#include "baseband_api.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "cpld_update.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
|
||||
namespace ui {
|
||||
|
||||
void GpsSimAppView::set_ready() {
|
||||
ready_signal = true;
|
||||
}
|
||||
|
||||
void GpsSimAppView::on_file_changed(std::filesystem::path new_file_path) {
|
||||
File data_file, info_file;
|
||||
char file_data[257];
|
||||
|
||||
// Get file size
|
||||
auto data_open_error = data_file.open("/" + new_file_path.string());
|
||||
if (data_open_error.is_valid()) {
|
||||
file_error();
|
||||
return;
|
||||
}
|
||||
|
||||
file_path = new_file_path;
|
||||
|
||||
// Get original record frequency if available
|
||||
std::filesystem::path info_file_path = file_path;
|
||||
info_file_path.replace_extension(u".TXT");
|
||||
|
||||
sample_rate = 500000;
|
||||
|
||||
auto info_open_error = info_file.open("/" + info_file_path.string());
|
||||
if (!info_open_error.is_valid()) {
|
||||
memset(file_data, 0, 257);
|
||||
auto read_size = info_file.read(file_data, 256);
|
||||
if (!read_size.is_error()) {
|
||||
auto pos1 = strstr(file_data, "center_frequency=");
|
||||
if (pos1) {
|
||||
pos1 += 17;
|
||||
field_frequency.set_value(strtoll(pos1, nullptr, 10));
|
||||
}
|
||||
|
||||
auto pos2 = strstr(file_data, "sample_rate=");
|
||||
if (pos2) {
|
||||
pos2 += 12;
|
||||
sample_rate = strtoll(pos2, nullptr, 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
text_sample_rate.set(unit_auto_scale(sample_rate, 3, 1) + "Hz");
|
||||
|
||||
auto file_size = data_file.size();
|
||||
auto duration = (file_size * 1000) / (1 * 2 * sample_rate);
|
||||
|
||||
progressbar.set_max(file_size / 1024);
|
||||
text_filename.set(file_path.filename().string().substr(0, 12));
|
||||
text_duration.set(to_string_time_ms(duration));
|
||||
|
||||
button_play.focus();
|
||||
}
|
||||
|
||||
void GpsSimAppView::on_tx_progress(const uint32_t progress) {
|
||||
progressbar.set_value(progress);
|
||||
}
|
||||
|
||||
void GpsSimAppView::focus() {
|
||||
button_open.focus();
|
||||
}
|
||||
|
||||
void GpsSimAppView::file_error() {
|
||||
nav_.display_modal("Error", "File read error.");
|
||||
}
|
||||
|
||||
bool GpsSimAppView::is_active() const {
|
||||
return (bool)replay_thread;
|
||||
}
|
||||
|
||||
void GpsSimAppView::toggle() {
|
||||
if( is_active() ) {
|
||||
stop(false);
|
||||
} else {
|
||||
start();
|
||||
}
|
||||
}
|
||||
|
||||
void GpsSimAppView::start() {
|
||||
stop(false);
|
||||
|
||||
std::unique_ptr<stream::Reader> reader;
|
||||
|
||||
auto p = std::make_unique<FileReader>();
|
||||
auto open_error = p->open(file_path);
|
||||
if( open_error.is_valid() ) {
|
||||
file_error();
|
||||
} else {
|
||||
reader = std::move(p);
|
||||
}
|
||||
|
||||
if( reader ) {
|
||||
button_play.set_bitmap(&bitmap_stop);
|
||||
baseband::set_sample_rate(sample_rate );
|
||||
|
||||
replay_thread = std::make_unique<ReplayThread>(
|
||||
std::move(reader),
|
||||
read_size, buffer_count,
|
||||
&ready_signal,
|
||||
[](uint32_t return_code) {
|
||||
ReplayThreadDoneMessage message { return_code };
|
||||
EventDispatcher::send_message(message);
|
||||
}
|
||||
);
|
||||
}
|
||||
field_rfgain.on_change = [this](int32_t v) {
|
||||
tx_gain = v;
|
||||
};
|
||||
field_rfgain.set_value(tx_gain);
|
||||
receiver_model.set_tx_gain(tx_gain);
|
||||
|
||||
|
||||
field_rfamp.on_change = [this](int32_t v) {
|
||||
rf_amp = (bool)v;
|
||||
};
|
||||
field_rfamp.set_value(rf_amp ? 14 : 0);
|
||||
|
||||
radio::enable({
|
||||
receiver_model.tuning_frequency(),
|
||||
sample_rate ,
|
||||
baseband_bandwidth,
|
||||
rf::Direction::Transmit,
|
||||
rf_amp, // previous code line : "receiver_model.rf_amp()," was passing the same rf_amp of all Receiver Apps
|
||||
static_cast<int8_t>(receiver_model.lna()),
|
||||
static_cast<int8_t>(receiver_model.vga())
|
||||
});
|
||||
}
|
||||
|
||||
void GpsSimAppView::stop(const bool do_loop) {
|
||||
if( is_active() )
|
||||
replay_thread.reset();
|
||||
|
||||
if (do_loop && check_loop.value()) {
|
||||
start();
|
||||
} else {
|
||||
radio::disable();
|
||||
button_play.set_bitmap(&bitmap_play);
|
||||
}
|
||||
|
||||
ready_signal = false;
|
||||
}
|
||||
|
||||
void GpsSimAppView::handle_replay_thread_done(const uint32_t return_code) {
|
||||
if (return_code == ReplayThread::END_OF_FILE) {
|
||||
stop(true);
|
||||
} else if (return_code == ReplayThread::READ_ERROR) {
|
||||
stop(false);
|
||||
file_error();
|
||||
}
|
||||
|
||||
progressbar.set_value(0);
|
||||
}
|
||||
|
||||
GpsSimAppView::GpsSimAppView(
|
||||
NavigationView& nav
|
||||
) : nav_ (nav)
|
||||
{
|
||||
tx_gain = 35;field_rfgain.set_value(tx_gain); // Initial default value (-12 dB's max 47dBs ).
|
||||
field_rfamp.set_value(rf_amp ? 14 : 0); // Initial default value True. (TX RF amp on , +14dB's)
|
||||
|
||||
baseband::run_image(portapack::spi_flash::image_tag_gps);
|
||||
|
||||
add_children({
|
||||
&labels,
|
||||
&button_open,
|
||||
&text_filename,
|
||||
&text_sample_rate,
|
||||
&text_duration,
|
||||
&progressbar,
|
||||
&field_frequency,
|
||||
&field_rfgain,
|
||||
&field_rfamp, // let's not use common persistent rf_amp , local rfamp is enough
|
||||
&check_loop,
|
||||
&button_play,
|
||||
&waterfall,
|
||||
});
|
||||
|
||||
field_frequency.set_value(target_frequency());
|
||||
field_frequency.set_step(receiver_model.frequency_step());
|
||||
field_frequency.on_change = [this](rf::Frequency f) {
|
||||
this->on_target_frequency_changed(f);
|
||||
};
|
||||
field_frequency.on_edit = [this, &nav]() {
|
||||
// TODO: Provide separate modal method/scheme?
|
||||
auto new_view = nav.push<FrequencyKeypadView>(this->target_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
this->on_target_frequency_changed(f);
|
||||
this->field_frequency.set_value(f);
|
||||
};
|
||||
};
|
||||
|
||||
field_frequency.set_step(5000);
|
||||
|
||||
button_play.on_select = [this](ImageButton&) {
|
||||
this->toggle();
|
||||
};
|
||||
|
||||
button_open.on_select = [this, &nav](Button&) {
|
||||
auto open_view = nav.push<FileLoadView>(".C8");
|
||||
open_view->on_changed = [this](std::filesystem::path new_file_path) {
|
||||
on_file_changed(new_file_path);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
GpsSimAppView::~GpsSimAppView() {
|
||||
radio::disable();
|
||||
hackrf::cpld::load_sram_no_verify(); // to leave all RX ok, without ghost signal problem at the exit .
|
||||
baseband::shutdown(); // better this function at the end, not load_sram() that sometimes produces hang up.
|
||||
}
|
||||
|
||||
void GpsSimAppView::on_hide() {
|
||||
// TODO: Terrible kludge because widget system doesn't notify Waterfall that
|
||||
// it's being shown or hidden.
|
||||
if( is_active() )
|
||||
stop(false);
|
||||
waterfall.on_hide();
|
||||
View::on_hide();
|
||||
}
|
||||
|
||||
void GpsSimAppView::set_parent_rect(const Rect new_parent_rect) {
|
||||
View::set_parent_rect(new_parent_rect);
|
||||
|
||||
const ui::Rect waterfall_rect { 0, header_height, new_parent_rect.width(), new_parent_rect.height() - header_height };
|
||||
waterfall.set_parent_rect(waterfall_rect);
|
||||
}
|
||||
|
||||
void GpsSimAppView::on_target_frequency_changed(rf::Frequency f) {
|
||||
set_target_frequency(f);
|
||||
}
|
||||
|
||||
void GpsSimAppView::set_target_frequency(const rf::Frequency new_value) {
|
||||
persistent_memory::set_tuned_frequency(new_value);;
|
||||
}
|
||||
|
||||
rf::Frequency GpsSimAppView::target_frequency() const {
|
||||
return persistent_memory::tuned_frequency();
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
@@ -1,167 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2016 Furrtek
|
||||
* Copyright (C) 2020 Shao
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __GPS_SIM_APP_HPP__
|
||||
#define __GPS_SIM_APP_HPP__
|
||||
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "replay_thread.hpp"
|
||||
#include "ui_spectrum.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <memory>
|
||||
|
||||
namespace ui {
|
||||
|
||||
class GpsSimAppView : public View {
|
||||
public:
|
||||
GpsSimAppView(NavigationView& nav);
|
||||
~GpsSimAppView();
|
||||
|
||||
void on_hide() override;
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "GPS Sim TX"; };
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
|
||||
static constexpr ui::Dim header_height = 3 * 16;
|
||||
|
||||
uint32_t sample_rate = 0;
|
||||
int32_t tx_gain { 47 };
|
||||
bool rf_amp { true }; // aux private var to store temporal, same as Replay App rf_amp user selection.
|
||||
static constexpr uint32_t baseband_bandwidth = 3000000; //filter bandwidth
|
||||
const size_t read_size { 16384 };
|
||||
const size_t buffer_count { 3 };
|
||||
|
||||
void on_file_changed(std::filesystem::path new_file_path);
|
||||
void on_target_frequency_changed(rf::Frequency f);
|
||||
void on_tx_progress(const uint32_t progress);
|
||||
|
||||
void set_target_frequency(const rf::Frequency new_value);
|
||||
rf::Frequency target_frequency() const;
|
||||
|
||||
void toggle();
|
||||
void start();
|
||||
void stop(const bool do_loop);
|
||||
bool is_active() const;
|
||||
void set_ready();
|
||||
void handle_replay_thread_done(const uint32_t return_code);
|
||||
void file_error();
|
||||
|
||||
std::filesystem::path file_path { };
|
||||
std::unique_ptr<ReplayThread> replay_thread { };
|
||||
bool ready_signal { false };
|
||||
|
||||
Labels labels {
|
||||
{ { 10 * 8, 2 * 16 }, "GAIN A:", Color::light_grey() }
|
||||
};
|
||||
|
||||
Button button_open {
|
||||
{ 0 * 8, 0 * 16, 10 * 8, 2 * 16 },
|
||||
"Open file"
|
||||
};
|
||||
|
||||
Text text_filename {
|
||||
{ 11 * 8, 0 * 16, 12 * 8, 16 },
|
||||
"-"
|
||||
};
|
||||
Text text_sample_rate {
|
||||
{ 24 * 8, 0 * 16, 6 * 8, 16 },
|
||||
"-"
|
||||
};
|
||||
|
||||
Text text_duration {
|
||||
{ 11 * 8, 1 * 16, 6 * 8, 16 },
|
||||
"-"
|
||||
};
|
||||
ProgressBar progressbar {
|
||||
{ 18 * 8, 1 * 16, 12 * 8, 16 }
|
||||
};
|
||||
|
||||
FrequencyField field_frequency {
|
||||
{ 0 * 8, 2 * 16 },
|
||||
};
|
||||
NumberField field_rfgain {
|
||||
{ 14 * 8, 2 * 16 },
|
||||
2,
|
||||
{ 0, 47 },
|
||||
1,
|
||||
' '
|
||||
};
|
||||
NumberField field_rfamp { // previously we were using "RFAmpField field_rf_amp" but that is general Receiver amp setting.
|
||||
{ 19 * 8, 2 * 16 },
|
||||
2,
|
||||
{ 0, 14 }, // this time we will display GUI , 0 or 14 dBs same as Mic and Replay App
|
||||
14,
|
||||
' '
|
||||
};
|
||||
Checkbox check_loop {
|
||||
{ 21 * 8, 2 * 16 },
|
||||
4,
|
||||
"Loop",
|
||||
true
|
||||
};
|
||||
ImageButton button_play {
|
||||
{ 28 * 8, 2 * 16, 2 * 8, 1 * 16 },
|
||||
&bitmap_play,
|
||||
Color::green(),
|
||||
Color::black()
|
||||
};
|
||||
|
||||
spectrum::WaterfallWidget waterfall { };
|
||||
|
||||
MessageHandlerRegistration message_handler_replay_thread_error {
|
||||
Message::ID::ReplayThreadDone,
|
||||
[this](const Message* const p) {
|
||||
const auto message = *reinterpret_cast<const ReplayThreadDoneMessage*>(p);
|
||||
this->handle_replay_thread_done(message.return_code);
|
||||
}
|
||||
};
|
||||
|
||||
MessageHandlerRegistration message_handler_fifo_signal {
|
||||
Message::ID::RequestSignal,
|
||||
[this](const Message* const p) {
|
||||
const auto message = static_cast<const RequestSignalMessage*>(p);
|
||||
if (message->signal == RequestSignalMessage::Signal::FillRequest) {
|
||||
this->set_ready();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
MessageHandlerRegistration message_handler_tx_progress {
|
||||
Message::ID::TXProgress,
|
||||
[this](const Message* const p) {
|
||||
const auto message = *reinterpret_cast<const TXProgressMessage*>(p);
|
||||
this->on_tx_progress(message.progress);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
#endif/*__GPS_SIM_APP_HPP__*/
|
||||
@@ -1,373 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2019 Furrtek
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
// The UI for this app is in French because it concerns leisure centers
|
||||
// only established in France. "LGE" stands for a trademark I'd rather
|
||||
// not spell out completely here.
|
||||
|
||||
#include "lge_app.hpp"
|
||||
|
||||
#include "baseband_api.hpp"
|
||||
#include "cpld_update.hpp"
|
||||
#include "ui_textentry.hpp"
|
||||
|
||||
#include "string_format.hpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <stdio.h>
|
||||
|
||||
using namespace portapack;
|
||||
|
||||
namespace ui {
|
||||
|
||||
void LGEView::focus() {
|
||||
options_frame.focus();
|
||||
}
|
||||
|
||||
LGEView::~LGEView() {
|
||||
// save app settings
|
||||
app_settings.tx_frequency = transmitter_model.tuning_frequency();
|
||||
settings.save("tx_lge", &app_settings);
|
||||
|
||||
transmitter_model.disable();
|
||||
hackrf::cpld::load_sram_no_verify(); // to leave all RX ok, without ghost signal problem at the exit .
|
||||
baseband::shutdown();// better this function at the end, not load_sram() that sometimes produces hang up.
|
||||
}
|
||||
|
||||
void LGEView::generate_lge_frame(const uint8_t command, const uint16_t address_a, const uint16_t address_b, std::vector<uint8_t>& data) {
|
||||
std::array<uint8_t, 5> header = {
|
||||
command,
|
||||
(uint8_t)(address_a & 255),
|
||||
(uint8_t)(address_a >> 8),
|
||||
(uint8_t)(address_b & 255),
|
||||
(uint8_t)(address_b >> 8),
|
||||
};
|
||||
|
||||
data.insert(data.begin(), header.begin(), header.end());
|
||||
|
||||
frame_size = rfm69.gen_frame(data);
|
||||
|
||||
for (auto b : data)
|
||||
console.write(to_string_hex(b, 2) + " ");
|
||||
}
|
||||
|
||||
void LGEView::generate_frame_touche() {
|
||||
// 0001.89s
|
||||
// 0D 96 02 12 0E 00 46 28 01 45 27 01 44 23 66 30
|
||||
std::vector<uint8_t> data { 0x46, 0x28, 0x01, 0x45, 0x27, 0x01, 0x44, 0x23 };
|
||||
|
||||
console.write("\n\x1B\x07Touche:\x1B\x10");
|
||||
generate_lge_frame(0x96, (field_player.value() << 8) | field_room.value(), 0x0001, data);
|
||||
}
|
||||
|
||||
void LGEView::generate_frame_nickname() {
|
||||
// 0040.48s:
|
||||
// 30 02 1A 00 19 00 FF 00 02 19 42 52 45 42 49 53 20 00 00 00 00 00 00 00 00 00
|
||||
// 04 01 B0 04 7F 1F 11 33 40 1F 22 01 07 00 00 01 07 00 00 63 05 00 00 99 A2
|
||||
|
||||
std::vector<uint8_t> data { };
|
||||
std::array<uint8_t, 3> data_header = { 0xFF, 0x00, 0x02 };
|
||||
std::array<uint8_t, 22> data_footer = {
|
||||
0x01, 0xB0, 0x04, 0x7F,
|
||||
0x1F, 0x11, 0x33, 0x40,
|
||||
0x1F, 0x22, 0x01, 0x07,
|
||||
0x00, 0x00, 0x01, 0x07,
|
||||
0x00, 0x00, 0x63, 0x05,
|
||||
0x00, 0x00
|
||||
};
|
||||
uint32_t c;
|
||||
|
||||
//data_header[2] = field_room.value(); // ?
|
||||
//data_footer[0] = field_room.value(); // ?
|
||||
|
||||
data.insert(data.begin(), data_header.begin(), data_header.end());
|
||||
|
||||
data.push_back(field_player.value());
|
||||
|
||||
c = 0;
|
||||
for (auto &ch : nickname) {
|
||||
data.push_back(ch);
|
||||
c++;
|
||||
}
|
||||
// Space at the end, is this required ?
|
||||
data.push_back(0x20);
|
||||
// Pad with zeroes
|
||||
while (++c < 16)
|
||||
data.push_back(0x00);
|
||||
|
||||
data.push_back(field_team.value());
|
||||
|
||||
data.insert(data.end(), data_footer.begin(), data_footer.end());
|
||||
|
||||
console.write("\n\x1B\x0ESet nickname:\x1B\x10");
|
||||
|
||||
generate_lge_frame(0x02, 0x001A, field_player.value(), data);
|
||||
}
|
||||
|
||||
void LGEView::generate_frame_team() {
|
||||
// 0041.83s:
|
||||
// 3D 03 FF FF FF FF 02 03 01 52 4F 55 47 45 00 00 00 00 00 00 00 00 00 00 00 00
|
||||
// 02 56 45 52 54 45 00 00 00 00 00 00 00 00 00 00 00 01 03 42 4C 45 55 45 00 00
|
||||
// 00 00 00 00 00 00 00 00 00 02 43 29
|
||||
|
||||
std::vector<uint8_t> data { };
|
||||
std::array<uint8_t, 2> data_header = { 0x02, 0x01 };
|
||||
uint32_t c;
|
||||
|
||||
data.insert(data.begin(), data_header.begin(), data_header.end());
|
||||
|
||||
data.push_back(field_team.value());
|
||||
|
||||
c = 0;
|
||||
for (auto &ch : nickname) {
|
||||
data.push_back(ch);
|
||||
c++;
|
||||
}
|
||||
// Pad with zeroes
|
||||
while (c++ < 16)
|
||||
data.push_back(0x00);
|
||||
|
||||
data.push_back(field_team.value() - 1); // Color ?
|
||||
|
||||
console.write("\n\x1B\x0ASet team:\x1B\x10");
|
||||
|
||||
generate_lge_frame(0x03, data);
|
||||
}
|
||||
|
||||
void LGEView::generate_frame_broadcast_nickname() {
|
||||
// 0043.86s:
|
||||
// 3D 04 FF FF FF FF 02 03 19 42 52 45 42 49 53 20 00 00 00 00 00 00 00 00 00 04
|
||||
// 07 50 4F 4E 45 59 20 00 00 00 00 00 00 00 00 00 00 05 1B 41 42 42 59 20 00 00
|
||||
// 00 00 00 00 00 00 00 00 00 04 0A 02
|
||||
|
||||
std::vector<uint8_t> data { };
|
||||
std::array<uint8_t, 2> data_header = { 0x02, 0x01 };
|
||||
uint32_t c;
|
||||
|
||||
data.insert(data.begin(), data_header.begin(), data_header.end());
|
||||
|
||||
data.push_back(field_player.value());
|
||||
|
||||
c = 0;
|
||||
for (auto &ch : nickname) {
|
||||
data.push_back(ch);
|
||||
c++;
|
||||
}
|
||||
// Space at the end, is this required ?
|
||||
data.push_back(0x20);
|
||||
// Pad with zeroes
|
||||
while (++c < 16)
|
||||
data.push_back(0x00);
|
||||
|
||||
data.push_back(field_team.value());
|
||||
|
||||
console.write("\n\x1B\x09" "Broadcast nickname:\x1B\x10");
|
||||
|
||||
generate_lge_frame(0x04, data);
|
||||
}
|
||||
|
||||
void LGEView::generate_frame_start() {
|
||||
// 0166.13s:
|
||||
// 0A 05 FF FF FF FF 02 EC FF FF FF A3 35
|
||||
std::vector<uint8_t> data { 0x02, 0xEC, 0xFF, 0xFF, 0xFF };
|
||||
|
||||
//data[0] = field_room.value(); // ?
|
||||
|
||||
console.write("\n\x1B\x0DStart:\x1B\x10");
|
||||
generate_lge_frame(0x05, data);
|
||||
}
|
||||
|
||||
void LGEView::generate_frame_gameover() {
|
||||
std::vector<uint8_t> data { (uint8_t)field_room.value() };
|
||||
|
||||
console.write("\n\x1B\x0CGameover:\x1B\x10");
|
||||
generate_lge_frame(0x0D, data);
|
||||
}
|
||||
|
||||
void LGEView::generate_frame_collier() {
|
||||
uint8_t flags = 0;
|
||||
|
||||
// Custom
|
||||
// 0C 00 13 37 13 37 id flags channel playerid zapduty zaptime checksum CRC CRC
|
||||
// channel: field_channel
|
||||
// playerid: field_player
|
||||
// zapduty: field_power
|
||||
// zaptime: field_duration
|
||||
|
||||
if (checkbox_heartbeat.value())
|
||||
flags |= 1;
|
||||
if (checkbox_rxtick.value())
|
||||
flags |= 2;
|
||||
|
||||
uint8_t checksum = 0;
|
||||
uint8_t id = (uint8_t)field_id.value();
|
||||
|
||||
std::vector<uint8_t> data {
|
||||
id,
|
||||
flags,
|
||||
(uint8_t)field_room.value(),
|
||||
(uint8_t)field_player.value(),
|
||||
(uint8_t)field_power.value(),
|
||||
(uint8_t)(field_duration.value() * 10)
|
||||
};
|
||||
|
||||
for (auto &v : data)
|
||||
checksum += v;
|
||||
|
||||
data.push_back(checksum - id);
|
||||
|
||||
console.write("\n\x1B\x06" "Config:\x1B\x10");
|
||||
generate_lge_frame(0x00, 0x3713, 0x3713, data);
|
||||
}
|
||||
|
||||
void LGEView::start_tx() {
|
||||
if (tx_mode == ALL) {
|
||||
transmitter_model.set_tuning_frequency(channels[channel_index]);
|
||||
tx_view.on_show(); // Refresh tuning frequency display
|
||||
tx_view.set_dirty();
|
||||
}
|
||||
transmitter_model.set_sampling_rate(2280000);
|
||||
transmitter_model.set_baseband_bandwidth(1750000);
|
||||
transmitter_model.enable();
|
||||
|
||||
chThdSleep(100);
|
||||
|
||||
baseband::set_fsk_data(frame_size * 8, 2280000 / 9600, 4000, 256);
|
||||
}
|
||||
|
||||
void LGEView::stop_tx() {
|
||||
tx_mode = IDLE;
|
||||
transmitter_model.disable();
|
||||
tx_view.set_transmitting(false);
|
||||
}
|
||||
|
||||
void LGEView::on_tx_progress(const uint32_t progress, const bool done) {
|
||||
(void)progress;
|
||||
|
||||
if (!done) return;
|
||||
|
||||
transmitter_model.disable();
|
||||
|
||||
/*if (repeats < 2) {
|
||||
chThdSleep(100);
|
||||
repeats++;
|
||||
start_tx();
|
||||
} else {*/
|
||||
if (tx_mode == ALL) {
|
||||
if (channel_index < 2) {
|
||||
channel_index++;
|
||||
repeats = 0;
|
||||
start_tx();
|
||||
} else {
|
||||
stop_tx();
|
||||
}
|
||||
} else {
|
||||
stop_tx();
|
||||
}
|
||||
//}
|
||||
}
|
||||
|
||||
LGEView::LGEView(NavigationView& nav) {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_fsktx);
|
||||
|
||||
add_children({
|
||||
&labels,
|
||||
&options_frame,
|
||||
&field_room,
|
||||
&button_text,
|
||||
&field_team,
|
||||
&field_player,
|
||||
&field_id,
|
||||
&field_power,
|
||||
&field_duration,
|
||||
&checkbox_heartbeat,
|
||||
&checkbox_rxtick,
|
||||
&checkbox_channels,
|
||||
&console,
|
||||
&tx_view
|
||||
});
|
||||
|
||||
// load app settings
|
||||
auto rc = settings.load("tx_lge", &app_settings);
|
||||
if(rc == SETTINGS_OK) {
|
||||
transmitter_model.set_rf_amp(app_settings.tx_amp);
|
||||
transmitter_model.set_channel_bandwidth(app_settings.channel_bandwidth);
|
||||
transmitter_model.set_tuning_frequency(app_settings.tx_frequency);
|
||||
transmitter_model.set_tx_gain(app_settings.tx_gain);
|
||||
}
|
||||
|
||||
field_room.set_value(1);
|
||||
field_team.set_value(1);
|
||||
field_player.set_value(1);
|
||||
field_id.set_value(1);
|
||||
field_power.set_value(1);
|
||||
field_duration.set_value(2);
|
||||
|
||||
button_text.on_select = [this, &nav](Button&) {
|
||||
text_prompt(
|
||||
nav,
|
||||
nickname,
|
||||
15,
|
||||
[this](std::string& buffer) {
|
||||
button_text.set_text(buffer);
|
||||
});
|
||||
};
|
||||
|
||||
tx_view.on_edit_frequency = [this, &nav]() {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(receiver_model.tuning_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
receiver_model.set_tuning_frequency(f);
|
||||
};
|
||||
};
|
||||
|
||||
tx_view.on_start = [this]() {
|
||||
if (tx_mode == IDLE) {
|
||||
auto i = options_frame.selected_index_value();
|
||||
if (i == 0)
|
||||
generate_frame_touche();
|
||||
else if (i == 1)
|
||||
generate_frame_nickname();
|
||||
else if (i == 2)
|
||||
generate_frame_team();
|
||||
else if (i == 3)
|
||||
generate_frame_broadcast_nickname();
|
||||
else if (i == 4)
|
||||
generate_frame_start();
|
||||
else if (i == 5)
|
||||
generate_frame_gameover();
|
||||
else if (i == 6)
|
||||
generate_frame_collier();
|
||||
|
||||
repeats = 0;
|
||||
channel_index = 0;
|
||||
tx_mode = checkbox_channels.value() ? ALL : SINGLE;
|
||||
tx_view.set_transmitting(true);
|
||||
start_tx();
|
||||
}
|
||||
};
|
||||
|
||||
tx_view.on_stop = [this]() {
|
||||
stop_tx();
|
||||
};
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
@@ -1,201 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2019 Furrtek
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_transmitter.hpp"
|
||||
#include "ui_font_fixed_8x16.hpp"
|
||||
#include "rfm69.hpp"
|
||||
|
||||
#include "message.hpp"
|
||||
#include "transmitter_model.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "app_settings.hpp"
|
||||
|
||||
namespace ui {
|
||||
|
||||
class LGEView : public View {
|
||||
public:
|
||||
LGEView(NavigationView& nav);
|
||||
~LGEView();
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "LGE tool TX"; };
|
||||
|
||||
private:
|
||||
enum tx_modes {
|
||||
IDLE = 0,
|
||||
SINGLE,
|
||||
ALL
|
||||
};
|
||||
|
||||
// app save settings
|
||||
std::app_settings settings { };
|
||||
std::app_settings::AppSettings app_settings { };
|
||||
|
||||
tx_modes tx_mode = IDLE;
|
||||
|
||||
RFM69 rfm69 { 5, 0x2DD4, true, true };
|
||||
|
||||
uint32_t frame_size { 0 };
|
||||
uint32_t repeats { 0 };
|
||||
uint32_t channel_index { 0 };
|
||||
std::string nickname { "ABCDEF" };
|
||||
|
||||
rf::Frequency channels[3] = { 868067000, 868183000, 868295000 };
|
||||
|
||||
void start_tx();
|
||||
void stop_tx();
|
||||
|
||||
void generate_lge_frame(const uint8_t command, std::vector<uint8_t>& data) {
|
||||
generate_lge_frame(command, 0xFFFF, 0xFFFF, data);
|
||||
}
|
||||
void generate_lge_frame(const uint8_t command, const uint16_t address_a, const uint16_t address_b, std::vector<uint8_t>& data);
|
||||
void generate_frame_touche();
|
||||
void generate_frame_nickname();
|
||||
void generate_frame_team();
|
||||
void generate_frame_broadcast_nickname();
|
||||
void generate_frame_start();
|
||||
void generate_frame_gameover();
|
||||
void generate_frame_collier();
|
||||
|
||||
void on_tx_progress(const uint32_t progress, const bool done);
|
||||
|
||||
Labels labels {
|
||||
//{ { 7 * 8, 1 * 8 }, "NO FUN ALLOWED !", Color::red() },
|
||||
{ { 1 * 8, 1 * 8 }, "Frame:", Color::light_grey() },
|
||||
{ { 2 * 8, 3 * 8 }, "Room:", Color::light_grey() },
|
||||
{ { 14 * 8, 3 * 8 }, "Text:", Color::light_grey() },
|
||||
{ { 2 * 8, 5 * 8 }, "Team:", Color::light_grey() },
|
||||
{ { 0 * 8, 7 * 8 }, "Player:", Color::light_grey() },
|
||||
{ { 0 * 8, 10 * 8 }, "Vest:", Color::light_grey() },
|
||||
{ { 4 * 8, 12 * 8 }, "ID:", Color::light_grey() },
|
||||
{ { 3 * 8, 14 * 8 }, "Pow: /10", Color::light_grey() },
|
||||
{ { 2 * 8, 16 * 8 }, "Time: x100ms", Color::light_grey() }
|
||||
};
|
||||
|
||||
OptionsField options_frame {
|
||||
{ 7 * 8, 1 * 8 },
|
||||
13,
|
||||
{
|
||||
{ "Key", 0 },
|
||||
{ "Set nickname", 1 },
|
||||
{ "Set team", 2 },
|
||||
{ "Brdcst nick", 3 },
|
||||
{ "Start", 4 },
|
||||
{ "Game over", 5 },
|
||||
{ "Set vest", 6 }
|
||||
}
|
||||
};
|
||||
|
||||
Checkbox checkbox_channels {
|
||||
{ 20 * 8, 1 * 8 },
|
||||
7,
|
||||
"All ch.",
|
||||
true
|
||||
};
|
||||
|
||||
NumberField field_room {
|
||||
{ 7 * 8, 3 * 8 },
|
||||
1,
|
||||
{ 1, 2 },
|
||||
1,
|
||||
'0'
|
||||
};
|
||||
|
||||
Button button_text {
|
||||
{ 14 * 8, 5 * 8, 16 * 8, 3 * 8 },
|
||||
"ABCDEF"
|
||||
};
|
||||
|
||||
NumberField field_team {
|
||||
{ 7 * 8, 5 * 8 },
|
||||
1,
|
||||
{ 1, 6 },
|
||||
1,
|
||||
'0'
|
||||
};
|
||||
|
||||
NumberField field_player {
|
||||
{ 7 * 8, 7 * 8 },
|
||||
2,
|
||||
{ 1, 50 },
|
||||
1,
|
||||
'0'
|
||||
};
|
||||
|
||||
Checkbox checkbox_heartbeat {
|
||||
{ 17 * 8, 12 * 8 },
|
||||
9,
|
||||
"Heartbeat",
|
||||
true
|
||||
};
|
||||
Checkbox checkbox_rxtick {
|
||||
{ 17 * 8, 15 * 8 },
|
||||
7,
|
||||
"RX tick",
|
||||
true
|
||||
};
|
||||
NumberField field_id {
|
||||
{ 7 * 8, 12 * 8 },
|
||||
1,
|
||||
{ 1, 2 },
|
||||
1,
|
||||
'0'
|
||||
};
|
||||
NumberField field_power {
|
||||
{ 7 * 8, 14 * 8 },
|
||||
2,
|
||||
{ 1, 10 },
|
||||
1,
|
||||
'0'
|
||||
};
|
||||
NumberField field_duration {
|
||||
{ 7 * 8, 16 * 8 },
|
||||
2,
|
||||
{ 1, 25 },
|
||||
1,
|
||||
'0'
|
||||
};
|
||||
|
||||
Console console {
|
||||
{ 0, 18 * 8, 30 * 8, 7 * 16 }
|
||||
};
|
||||
|
||||
TransmitterView tx_view {
|
||||
16 * 16,
|
||||
10000,
|
||||
12
|
||||
};
|
||||
|
||||
MessageHandlerRegistration message_handler_tx_progress {
|
||||
Message::ID::TXProgress,
|
||||
[this](const Message* const p) {
|
||||
const auto message = *reinterpret_cast<const TXProgressMessage*>(p);
|
||||
this->on_tx_progress(message.progress, message.done);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
@@ -22,227 +22,343 @@
|
||||
|
||||
#include "pocsag_app.hpp"
|
||||
|
||||
#include "audio.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "utility.hpp"
|
||||
#include "file_path.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
using namespace pocsag;
|
||||
|
||||
#include "string_format.hpp"
|
||||
#include "utility.hpp"
|
||||
#include "audio.hpp"
|
||||
namespace pmem = portapack::persistent_memory;
|
||||
|
||||
void POCSAGLogger::log_raw_data(const pocsag::POCSAGPacket& packet, const uint32_t frequency) {
|
||||
std::string entry = "Raw: F:" + to_string_dec_uint(frequency) + "Hz " +
|
||||
to_string_dec_uint(packet.bitrate()) + " Codewords:";
|
||||
|
||||
// Raw hex dump of all the codewords
|
||||
for (size_t c = 0; c < 16; c++)
|
||||
entry += to_string_hex(packet[c], 8) + " ";
|
||||
|
||||
log_file.write_entry(packet.timestamp(), entry);
|
||||
std::string entry = "Raw: F:" + to_string_dec_uint(frequency) + "Hz " +
|
||||
to_string_dec_uint(packet.bitrate()) + " Codewords:";
|
||||
|
||||
// Raw hex dump of all the codewords
|
||||
for (size_t c = 0; c < 16; c++)
|
||||
entry += to_string_hex(packet[c], 8) + " ";
|
||||
|
||||
log_file.write_entry(packet.timestamp(), entry);
|
||||
}
|
||||
|
||||
void POCSAGLogger::log_decoded(
|
||||
const pocsag::POCSAGPacket& packet,
|
||||
const std::string text) {
|
||||
|
||||
log_file.write_entry(packet.timestamp(), text);
|
||||
void POCSAGLogger::log_decoded(Timestamp timestamp, const std::string& text) {
|
||||
log_file.write_entry(timestamp, text);
|
||||
}
|
||||
|
||||
namespace ui {
|
||||
|
||||
void POCSAGAppView::update_freq(rf::Frequency f) {
|
||||
set_target_frequency(f);
|
||||
portapack::persistent_memory::set_tuned_frequency(f); // Maybe not ?
|
||||
POCSAGSettingsView::POCSAGSettingsView(
|
||||
NavigationView& nav,
|
||||
POCSAGSettings& settings)
|
||||
: settings_{settings} {
|
||||
add_children(
|
||||
{&labels,
|
||||
&check_log,
|
||||
&check_log_raw,
|
||||
&check_small_font,
|
||||
&check_hide_bad,
|
||||
&check_hide_addr_only,
|
||||
&opt_filter_mode,
|
||||
&field_filter_address,
|
||||
&button_save});
|
||||
|
||||
check_log.set_value(settings_.enable_logging);
|
||||
check_log_raw.set_value(settings_.enable_raw_log);
|
||||
check_small_font.set_value(settings_.enable_small_font);
|
||||
check_hide_bad.set_value(settings_.hide_bad_data);
|
||||
check_hide_addr_only.set_value(settings_.hide_addr_only);
|
||||
opt_filter_mode.set_by_value(settings_.filter_mode);
|
||||
field_filter_address.set_value(settings_.filter_address);
|
||||
|
||||
button_save.on_select = [this, &nav](Button&) {
|
||||
settings_.enable_logging = check_log.value();
|
||||
settings_.enable_raw_log = check_log_raw.value();
|
||||
settings_.enable_small_font = check_small_font.value();
|
||||
settings_.hide_bad_data = check_hide_bad.value();
|
||||
settings_.hide_addr_only = check_hide_addr_only.value();
|
||||
settings_.filter_mode = opt_filter_mode.selected_index_value();
|
||||
settings_.filter_address = field_filter_address.to_integer();
|
||||
|
||||
nav.pop();
|
||||
};
|
||||
}
|
||||
|
||||
POCSAGAppView::POCSAGAppView(NavigationView& nav) {
|
||||
uint32_t ignore_address;
|
||||
|
||||
baseband::run_image(portapack::spi_flash::image_tag_pocsag);
|
||||
POCSAGAppView::POCSAGAppView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_pocsag2);
|
||||
|
||||
add_children({
|
||||
&rssi,
|
||||
&channel,
|
||||
&audio,
|
||||
&field_rf_amp,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&field_frequency,
|
||||
&check_log,
|
||||
&field_volume,
|
||||
&check_ignore,
|
||||
&sym_ignore,
|
||||
&console
|
||||
});
|
||||
|
||||
// Set on_change before initialising the field
|
||||
field_frequency.on_change = [this](rf::Frequency f) {
|
||||
update_freq(f);
|
||||
};
|
||||
add_children(
|
||||
{&rssi,
|
||||
&audio,
|
||||
&field_rf_amp,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&field_frequency,
|
||||
&field_squelch,
|
||||
&field_volume,
|
||||
&image_status,
|
||||
&text_packet_count,
|
||||
&widget_baud,
|
||||
&widget_bits,
|
||||
&widget_frames,
|
||||
&button_filter_last,
|
||||
&button_config,
|
||||
&console});
|
||||
|
||||
// load app settings
|
||||
auto rc = settings.load("rx_pocsag", &app_settings);
|
||||
if(rc == SETTINGS_OK) {
|
||||
field_lna.set_value(app_settings.lna);
|
||||
field_vga.set_value(app_settings.vga);
|
||||
field_rf_amp.set_value(app_settings.rx_amp);
|
||||
field_frequency.set_value(app_settings.rx_frequency);
|
||||
}
|
||||
else field_frequency.set_value(receiver_model.tuning_frequency());
|
||||
// No app settings, use fallbacks from pmem.
|
||||
if (!app_settings_.loaded()) {
|
||||
settings_.filter_address = pmem::pocsag_ignore_address();
|
||||
settings_.filter_mode = (settings_.filter_address == 0)
|
||||
? FILTER_NONE
|
||||
: FILTER_DROP;
|
||||
}
|
||||
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::NarrowbandFMAudio);
|
||||
logger.append(logs_dir / u"POCSAG.TXT");
|
||||
|
||||
receiver_model.set_sampling_rate(3072000);
|
||||
receiver_model.set_baseband_bandwidth(1750000);
|
||||
receiver_model.enable();
|
||||
|
||||
field_frequency.set_step(receiver_model.frequency_step());
|
||||
field_frequency.on_edit = [this, &nav]() {
|
||||
// TODO: Provide separate modal method/scheme?
|
||||
auto new_view = nav.push<FrequencyKeypadView>(receiver_model.tuning_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
update_freq(f);
|
||||
field_frequency.set_value(f);
|
||||
};
|
||||
};
|
||||
|
||||
check_log.set_value(logging);
|
||||
check_log.on_select = [this](Checkbox&, bool v) {
|
||||
logging = v;
|
||||
};
|
||||
|
||||
field_volume.set_value((receiver_model.headphone_volume() - audio::headphone::volume_range().max).decibel() + 99);
|
||||
field_volume.on_change = [this](int32_t v) {
|
||||
this->on_headphone_volume_changed(v);
|
||||
};
|
||||
field_squelch.set_value(receiver_model.squelch_level());
|
||||
field_squelch.on_change = [this](int32_t v) {
|
||||
receiver_model.set_squelch_level(v);
|
||||
};
|
||||
|
||||
check_ignore.set_value(ignore);
|
||||
check_ignore.on_select = [this](Checkbox&, bool v) {
|
||||
ignore = v;
|
||||
};
|
||||
|
||||
ignore_address = persistent_memory::pocsag_ignore_address();
|
||||
for (size_t c = 0; c < 7; c++) {
|
||||
sym_ignore.set_sym(6 - c, ignore_address % 10);
|
||||
ignore_address /= 10;
|
||||
}
|
||||
button_filter_last.on_select = [this](Button&) {
|
||||
if (settings_.filter_mode == FILTER_NONE)
|
||||
settings_.filter_mode = FILTER_DROP;
|
||||
settings_.filter_address = last_address;
|
||||
refresh_ui();
|
||||
};
|
||||
|
||||
logger = std::make_unique<POCSAGLogger>();
|
||||
if (logger)
|
||||
logger->append("pocsag.txt");
|
||||
button_config.on_select = [this](Button&) {
|
||||
nav_.push<POCSAGSettingsView>(settings_);
|
||||
nav_.set_on_pop([this]() { refresh_ui(); });
|
||||
};
|
||||
|
||||
audio::output::start();
|
||||
audio::output::unmute();
|
||||
refresh_ui();
|
||||
|
||||
baseband::set_pocsag();
|
||||
}
|
||||
if (pmem::beep_on_packets())
|
||||
audio::set_rate(audio::Rate::Hz_24000);
|
||||
|
||||
POCSAGAppView::~POCSAGAppView() {
|
||||
|
||||
// save app settings
|
||||
app_settings.rx_frequency = field_frequency.value();
|
||||
settings.save("rx_pocsag", &app_settings);
|
||||
|
||||
audio::output::stop();
|
||||
|
||||
// Save ignored address
|
||||
persistent_memory::set_pocsag_ignore_address(sym_ignore.value_dec_u32());
|
||||
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
audio::output::start();
|
||||
receiver_model.enable();
|
||||
baseband::set_pocsag();
|
||||
}
|
||||
|
||||
void POCSAGAppView::focus() {
|
||||
field_frequency.focus();
|
||||
field_frequency.focus();
|
||||
}
|
||||
|
||||
void POCSAGAppView::on_headphone_volume_changed(int32_t v) {
|
||||
const auto new_volume = volume_t::decibel(v - 99) + audio::headphone::volume_range().max;
|
||||
receiver_model.set_headphone_volume(new_volume);
|
||||
POCSAGAppView::~POCSAGAppView() {
|
||||
audio::output::stop();
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
|
||||
// Save pmem settings.
|
||||
pmem::set_pocsag_ignore_address(settings_.filter_address);
|
||||
pmem::set_pocsag_last_address(last_address); // For POCSAG TX.
|
||||
}
|
||||
|
||||
void POCSAGAppView::refresh_ui() {
|
||||
// Set console font style.
|
||||
console.set_style(
|
||||
settings_.enable_small_font
|
||||
? Theme::getInstance()->bg_darkest_small
|
||||
: Theme::getInstance()->bg_darkest);
|
||||
|
||||
// Useless ?
|
||||
void POCSAGAppView::set_parent_rect(const Rect new_parent_rect) {
|
||||
View::set_parent_rect(new_parent_rect);
|
||||
// Update filter button text.
|
||||
std::string btn_text = "Filter Last";
|
||||
switch (settings_.filter_mode) {
|
||||
case FILTER_DROP:
|
||||
btn_text = "Ignore Last";
|
||||
break;
|
||||
|
||||
case FILTER_KEEP:
|
||||
btn_text = "Keep Last";
|
||||
break;
|
||||
|
||||
case FILTER_NONE:
|
||||
default:
|
||||
btn_text = "Filter Last";
|
||||
break;
|
||||
}
|
||||
button_filter_last.set_text(btn_text);
|
||||
}
|
||||
|
||||
void POCSAGAppView::on_packet(const POCSAGPacketMessage * message) {
|
||||
std::string alphanum_text = "";
|
||||
|
||||
if (message->packet.flag() != NORMAL)
|
||||
console.writeln("\n\x1B\x0CRC ERROR: " + pocsag::flag_str(message->packet.flag()));
|
||||
else {
|
||||
pocsag_decode_batch(message->packet, &pocsag_state);
|
||||
bool POCSAGAppView::ignore_address(uint32_t address) const {
|
||||
switch (settings_.filter_mode) {
|
||||
case FILTER_DROP:
|
||||
return address == settings_.filter_address;
|
||||
|
||||
if ((ignore) && (pocsag_state.address == sym_ignore.value_dec_u32())) {
|
||||
// Ignore (inform, but no log)
|
||||
//console.write("\n\x1B\x03" + to_string_time(message->packet.timestamp()) +
|
||||
// " Ignored address " + to_string_dec_uint(pocsag_state.address));
|
||||
return;
|
||||
}
|
||||
// Too many errors for reliable decode
|
||||
if ((ignore) && (pocsag_state.errors >= 3)) {
|
||||
return;
|
||||
}
|
||||
case FILTER_KEEP:
|
||||
return address != settings_.filter_address;
|
||||
|
||||
|
||||
std::string console_info;
|
||||
const uint32_t roundVal = 50;
|
||||
const uint32_t bitrate = roundVal * ((message->packet.bitrate() + (roundVal/2))/roundVal);
|
||||
console_info = "\n" + to_string_datetime(message->packet.timestamp(), HM);
|
||||
console_info += " " + to_string_dec_uint(bitrate);
|
||||
console_info += " ADDR:" + to_string_dec_uint(pocsag_state.address);
|
||||
console_info += " F" + to_string_dec_uint(pocsag_state.function);
|
||||
|
||||
// Store last received address for POCSAG TX
|
||||
persistent_memory::set_pocsag_last_address(pocsag_state.address);
|
||||
|
||||
if (pocsag_state.out_type == ADDRESS) {
|
||||
// Address only
|
||||
|
||||
console.write(console_info);
|
||||
|
||||
if (logger && logging) {
|
||||
logger->log_decoded(message->packet, to_string_dec_uint(pocsag_state.address) +
|
||||
" F" + to_string_dec_uint(pocsag_state.function) +
|
||||
" Address only");
|
||||
}
|
||||
|
||||
last_address = pocsag_state.address;
|
||||
} else if (pocsag_state.out_type == MESSAGE) {
|
||||
if (pocsag_state.address != last_address) {
|
||||
// New message
|
||||
console.writeln(console_info);
|
||||
console.write(pocsag_state.output);
|
||||
|
||||
last_address = pocsag_state.address;
|
||||
} else {
|
||||
// Message continues...
|
||||
console.write(pocsag_state.output);
|
||||
}
|
||||
|
||||
if (logger && logging)
|
||||
logger->log_decoded(message->packet, to_string_dec_uint(pocsag_state.address) +
|
||||
" F" + to_string_dec_uint(pocsag_state.function) +
|
||||
" Alpha: " + pocsag_state.output);
|
||||
}
|
||||
}
|
||||
|
||||
// Log raw data whatever it contains
|
||||
if (logger && logging)
|
||||
logger->log_raw_data(message->packet, target_frequency());
|
||||
case FILTER_NONE:
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void POCSAGAppView::set_target_frequency(const uint32_t new_value) {
|
||||
target_frequency_ = new_value;
|
||||
receiver_model.set_tuning_frequency(new_value);
|
||||
void POCSAGAppView::handle_decoded(Timestamp timestamp, const std::string& prefix) {
|
||||
bool bad_data = pocsag_state.errors >= 3;
|
||||
|
||||
// Too many errors for reliable decode.
|
||||
if (bad_data && hide_bad_data()) {
|
||||
console.write("\n" STR_COLOR_MAGENTA + prefix + " Too many decode errors.");
|
||||
last_address = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore address? TODO: could filter earlier.
|
||||
if (ignore_address(pocsag_state.address)) {
|
||||
console.write("\n" STR_COLOR_CYAN + prefix + " Ignored: " + to_string_dec_uint(pocsag_state.address));
|
||||
last_address = pocsag_state.address;
|
||||
return;
|
||||
}
|
||||
|
||||
// Color indicates the message has a lot of decoding errors.
|
||||
std::string color = bad_data ? STR_COLOR_MAGENTA : STR_COLOR_WHITE;
|
||||
|
||||
std::string console_info = "\n" + color + prefix;
|
||||
console_info += " #" + to_string_dec_uint(pocsag_state.address);
|
||||
console_info += " F" + to_string_dec_uint(pocsag_state.function);
|
||||
|
||||
if (pocsag_state.out_type == ADDRESS) {
|
||||
last_address = pocsag_state.address;
|
||||
|
||||
if (!hide_addr_only()) {
|
||||
console.write(console_info);
|
||||
|
||||
if (logging()) {
|
||||
logger.log_decoded(
|
||||
timestamp,
|
||||
to_string_dec_uint(pocsag_state.address) +
|
||||
" F" + to_string_dec_uint(pocsag_state.function));
|
||||
}
|
||||
}
|
||||
|
||||
} else if (pocsag_state.out_type == MESSAGE) {
|
||||
if (pocsag_state.address != last_address) {
|
||||
// New message
|
||||
last_address = pocsag_state.address;
|
||||
console.writeln(console_info);
|
||||
console.write(color + pocsag_state.output);
|
||||
} else {
|
||||
// Message continues...
|
||||
console.write(color + pocsag_state.output);
|
||||
}
|
||||
|
||||
if (logging()) {
|
||||
logger.log_decoded(
|
||||
timestamp,
|
||||
to_string_dec_uint(pocsag_state.address) +
|
||||
" F" + to_string_dec_uint(pocsag_state.function) +
|
||||
" " + pocsag_state.output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t POCSAGAppView::target_frequency() const {
|
||||
return target_frequency_;
|
||||
static Color get_status_color(const POCSAGState& state) {
|
||||
if (state.out_type == IDLE)
|
||||
return Theme::getInstance()->bg_darkest->foreground;
|
||||
|
||||
switch (state.mode) {
|
||||
case STATE_CLEAR:
|
||||
return Theme::getInstance()->fg_cyan->foreground;
|
||||
case STATE_HAVE_ADDRESS:
|
||||
return Theme::getInstance()->fg_yellow->foreground;
|
||||
case STATE_GETTING_MSG:
|
||||
return Theme::getInstance()->fg_green->foreground;
|
||||
}
|
||||
|
||||
// Shouldn't get here...
|
||||
return Theme::getInstance()->fg_red->foreground;
|
||||
}
|
||||
|
||||
void POCSAGAppView::on_packet(const POCSAGPacketMessage* message) {
|
||||
const uint32_t roundVal = 50;
|
||||
const uint32_t bitrate_rounded = roundVal * ((message->packet.bitrate() + (roundVal / 2)) / roundVal);
|
||||
auto bitrate = to_string_dec_uint(bitrate_rounded);
|
||||
auto timestamp = to_string_datetime(message->packet.timestamp(), HM);
|
||||
auto prefix = timestamp + " " + bitrate;
|
||||
|
||||
// Display packet count to be able to tell whether baseband sent a packet for a tone.
|
||||
++packet_count;
|
||||
text_packet_count.set(to_string_dec_uint(packet_count));
|
||||
|
||||
if (logging_raw())
|
||||
logger.log_raw_data(message->packet, receiver_model.target_frequency());
|
||||
|
||||
if (message->packet.flag() != NORMAL) {
|
||||
console.writeln("\n" STR_COLOR_RED + prefix + " CRC ERROR: " + pocsag::flag_str(message->packet.flag()));
|
||||
last_address = 0;
|
||||
} else {
|
||||
// Set color before to be able to see if decode gets stuck.
|
||||
image_status.set_foreground(Theme::getInstance()->fg_magenta->foreground);
|
||||
pocsag_state.codeword_index = 0;
|
||||
pocsag_state.errors = 0;
|
||||
|
||||
// Handle multiple messages (if any).
|
||||
while (pocsag_decode_batch(message->packet, pocsag_state))
|
||||
handle_decoded(message->packet.timestamp(), prefix);
|
||||
|
||||
// Handle the remainder.
|
||||
handle_decoded(message->packet.timestamp(), prefix);
|
||||
}
|
||||
|
||||
// Set status icon color to indicate state machine state.
|
||||
image_status.set_foreground(get_status_color(pocsag_state));
|
||||
|
||||
if (pmem::beep_on_packets()) {
|
||||
baseband::request_audio_beep(1000, 24000, 60);
|
||||
}
|
||||
}
|
||||
|
||||
void POCSAGAppView::on_stats(const POCSAGStatsMessage* stats) {
|
||||
widget_baud.set_rate(stats->baud_rate);
|
||||
widget_bits.set_bits(stats->current_bits);
|
||||
widget_frames.set_frames(stats->current_frames);
|
||||
widget_frames.set_sync(stats->has_sync);
|
||||
}
|
||||
|
||||
void POCSAGAppView::on_freqchg(int64_t freq) {
|
||||
field_frequency.set_value(freq);
|
||||
}
|
||||
|
||||
void BaudIndicator::paint(Painter& painter) {
|
||||
auto p = screen_pos();
|
||||
char top = '-';
|
||||
char bot = '-';
|
||||
|
||||
if (rate_ > 0) {
|
||||
auto r = rate_ / 100;
|
||||
top = (r / 10) + '0';
|
||||
bot = (r % 10) + '0';
|
||||
}
|
||||
|
||||
painter.draw_char(p, *Theme::getInstance()->bg_darkest_small, top);
|
||||
painter.draw_char({p.x(), p.y() + 8}, *Theme::getInstance()->bg_darkest_small, bot);
|
||||
}
|
||||
|
||||
void BitsIndicator::paint(Painter&) {
|
||||
auto p = screen_pos();
|
||||
for (size_t i = 0; i < sizeof(bits_) * 8; ++i) {
|
||||
auto is_set = ((bits_ >> i) & 0x1) == 1;
|
||||
|
||||
int x = p.x() + (i / height);
|
||||
int y = p.y() + (i % height);
|
||||
display.draw_pixel({x, y}, is_set ? Theme::getInstance()->bg_darkest->foreground : Theme::getInstance()->bg_darkest->background);
|
||||
}
|
||||
}
|
||||
|
||||
void FrameIndicator::paint(Painter& painter) {
|
||||
auto p = screen_pos();
|
||||
painter.draw_rectangle({p, {2, height}}, has_sync_ ? Theme::getInstance()->fg_green->foreground : Theme::getInstance()->bg_medium->background);
|
||||
|
||||
for (size_t i = 0; i < height; ++i) {
|
||||
auto p2 = p + Point{2, 15 - (int)i};
|
||||
painter.draw_hline(p2, 2, i < frame_count_ ? Theme::getInstance()->bg_darkest->foreground : Theme::getInstance()->bg_darkest->background);
|
||||
}
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -24,125 +24,298 @@
|
||||
#define __POCSAG_APP_H__
|
||||
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_rssi.hpp"
|
||||
|
||||
#include "log_file.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "log_file.hpp"
|
||||
#include "pocsag.hpp"
|
||||
#include "pocsag_packet.hpp"
|
||||
#include "radio_state.hpp"
|
||||
|
||||
#include <functional>
|
||||
|
||||
class POCSAGLogger {
|
||||
public:
|
||||
Optional<File::Error> append(const std::string& filename) {
|
||||
return log_file.append(filename);
|
||||
}
|
||||
|
||||
void log_raw_data(const pocsag::POCSAGPacket& packet, const uint32_t frequency);
|
||||
void log_decoded(const pocsag::POCSAGPacket& packet, const std::string text);
|
||||
public:
|
||||
Optional<File::Error> append(const std::filesystem::path& filename) {
|
||||
return log_file.append(filename);
|
||||
}
|
||||
|
||||
private:
|
||||
LogFile log_file { };
|
||||
void log_raw_data(const pocsag::POCSAGPacket& packet, const uint32_t frequency);
|
||||
void log_decoded(Timestamp timestamp, const std::string& text);
|
||||
|
||||
private:
|
||||
LogFile log_file{};
|
||||
};
|
||||
|
||||
namespace ui {
|
||||
|
||||
class BaudIndicator : public Widget {
|
||||
public:
|
||||
BaudIndicator(Point position)
|
||||
: Widget{{position, {5, height}}} {}
|
||||
|
||||
void paint(Painter& painter) override;
|
||||
void set_rate(uint16_t rate) {
|
||||
if (rate != rate_) {
|
||||
rate_ = rate;
|
||||
set_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr uint8_t height = 16;
|
||||
uint16_t rate_ = 0;
|
||||
};
|
||||
|
||||
class BitsIndicator : public Widget {
|
||||
public:
|
||||
BitsIndicator(Point position)
|
||||
: Widget{{position, {2, height}}} {}
|
||||
|
||||
void paint(Painter& painter) override;
|
||||
void set_bits(uint32_t bits) {
|
||||
if (bits != bits_) {
|
||||
bits_ = bits;
|
||||
set_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr uint8_t height = 16;
|
||||
uint32_t bits_ = 0;
|
||||
};
|
||||
|
||||
class FrameIndicator : public Widget {
|
||||
public:
|
||||
FrameIndicator(Point position)
|
||||
: Widget{{position, {4, height}}} {}
|
||||
|
||||
void paint(Painter& painter) override;
|
||||
void set_frames(uint8_t frame_count) {
|
||||
if (frame_count != frame_count_) {
|
||||
frame_count_ = frame_count;
|
||||
set_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
void set_sync(bool has_sync) {
|
||||
if (has_sync != has_sync_) {
|
||||
has_sync_ = has_sync;
|
||||
set_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr uint8_t height = 16;
|
||||
uint8_t frame_count_ = 0;
|
||||
bool has_sync_ = false;
|
||||
};
|
||||
|
||||
enum POCSAGFilter : uint8_t {
|
||||
FILTER_NONE,
|
||||
FILTER_DROP,
|
||||
FILTER_KEEP
|
||||
};
|
||||
|
||||
struct POCSAGSettings {
|
||||
bool enable_small_font = false;
|
||||
bool enable_logging = false;
|
||||
bool enable_raw_log = false;
|
||||
bool hide_bad_data = false;
|
||||
bool hide_addr_only = false;
|
||||
uint8_t filter_mode = false;
|
||||
uint32_t filter_address = 0;
|
||||
};
|
||||
|
||||
class POCSAGSettingsView : public View {
|
||||
public:
|
||||
POCSAGSettingsView(NavigationView& nav, POCSAGSettings& settings);
|
||||
|
||||
std::string title() const override { return "POCSAG Config"; };
|
||||
void focus() override { button_save.focus(); }
|
||||
|
||||
private:
|
||||
POCSAGSettings& settings_;
|
||||
|
||||
Labels labels{
|
||||
{{2 * 8, 12 * 16}, "Filter Mode:", Theme::getInstance()->fg_light->foreground},
|
||||
{{2 * 8, 13 * 16}, "Filter Addr:", Theme::getInstance()->fg_light->foreground},
|
||||
};
|
||||
|
||||
Checkbox check_log{
|
||||
{2 * 8, 2 * 16},
|
||||
10,
|
||||
"Enable Log"};
|
||||
|
||||
Checkbox check_log_raw{
|
||||
{2 * 8, 4 * 16},
|
||||
12,
|
||||
"Log Raw Data"};
|
||||
|
||||
Checkbox check_small_font{
|
||||
{2 * 8, 6 * 16},
|
||||
4,
|
||||
"Use Small Font"};
|
||||
|
||||
Checkbox check_hide_bad{
|
||||
{2 * 8, 8 * 16},
|
||||
22,
|
||||
"Hide Bad Data"};
|
||||
|
||||
Checkbox check_hide_addr_only{
|
||||
{2 * 8, 10 * 16},
|
||||
22,
|
||||
"Hide Addr Only"};
|
||||
|
||||
OptionsField opt_filter_mode{
|
||||
{15 * 8, 12 * 16},
|
||||
4,
|
||||
{{"None", FILTER_NONE},
|
||||
{"Drop", FILTER_DROP},
|
||||
{"Keep", FILTER_KEEP}}};
|
||||
|
||||
SymField field_filter_address{
|
||||
{15 * 8, 13 * 16},
|
||||
7,
|
||||
SymField::Type::Dec,
|
||||
true /*explicit_edit*/};
|
||||
|
||||
Button button_save{
|
||||
{11 * 8, 16 * 16, 10 * 8, 2 * 16},
|
||||
"Save"};
|
||||
};
|
||||
|
||||
class POCSAGAppView : public View {
|
||||
public:
|
||||
POCSAGAppView(NavigationView& nav);
|
||||
~POCSAGAppView();
|
||||
public:
|
||||
POCSAGAppView(NavigationView& nav);
|
||||
~POCSAGAppView();
|
||||
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
void focus() override;
|
||||
std::string title() const override { return "POCSAG RX"; };
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "POCSAG RX"; };
|
||||
private:
|
||||
bool logging() const { return settings_.enable_logging; };
|
||||
bool logging_raw() const { return settings_.enable_raw_log; };
|
||||
bool hide_bad_data() const { return settings_.hide_bad_data; };
|
||||
bool hide_addr_only() const { return settings_.hide_addr_only; };
|
||||
|
||||
private:
|
||||
static constexpr uint32_t initial_target_frequency = 466175000;
|
||||
NavigationView& nav_;
|
||||
RxRadioState radio_state_{
|
||||
466175000 /* frequency*/,
|
||||
max283x::filter::bandwidth_minimum /* bandwidth */,
|
||||
3072000 /* sampling rate */
|
||||
};
|
||||
|
||||
// app save settings
|
||||
std::app_settings settings { };
|
||||
std::app_settings::AppSettings app_settings { };
|
||||
// Settings
|
||||
POCSAGSettings settings_{};
|
||||
app_settings::SettingsManager app_settings_{
|
||||
"rx_pocsag"sv,
|
||||
app_settings::Mode::RX,
|
||||
{
|
||||
{"small_font"sv, &settings_.enable_small_font},
|
||||
{"enable_logging"sv, &settings_.enable_logging},
|
||||
{"enable_raw_log"sv, &settings_.enable_raw_log},
|
||||
{"filter_mode"sv, &settings_.filter_mode},
|
||||
{"filter_address"sv, &settings_.filter_address},
|
||||
{"hide_bad_data"sv, &settings_.hide_bad_data},
|
||||
{"hide_addr_only"sv, &settings_.hide_addr_only},
|
||||
}};
|
||||
|
||||
bool logging { true };
|
||||
bool ignore { true };
|
||||
uint32_t last_address = 0xFFFFFFFF;
|
||||
pocsag::POCSAGState pocsag_state { };
|
||||
void refresh_ui();
|
||||
bool ignore_address(uint32_t address) const;
|
||||
void handle_decoded(Timestamp timestamp, const std::string& prefix);
|
||||
void on_packet(const POCSAGPacketMessage* message);
|
||||
void on_stats(const POCSAGStatsMessage* stats);
|
||||
|
||||
RFAmpField field_rf_amp {
|
||||
{ 13 * 8, 0 * 16 }
|
||||
};
|
||||
LNAGainField field_lna {
|
||||
{ 15 * 8, 0 * 16 }
|
||||
};
|
||||
VGAGainField field_vga {
|
||||
{ 18 * 8, 0 * 16 }
|
||||
};
|
||||
RSSI rssi {
|
||||
{ 21 * 8, 0, 6 * 8, 4 },
|
||||
};
|
||||
Channel channel {
|
||||
{ 21 * 8, 5, 6 * 8, 4 },
|
||||
};
|
||||
Audio audio{
|
||||
{ 21 * 8, 10, 6 * 8, 4 },
|
||||
};
|
||||
|
||||
FrequencyField field_frequency {
|
||||
{ 0 * 8, 0 * 8 },
|
||||
};
|
||||
Checkbox check_log {
|
||||
{ 24 * 8, 21 },
|
||||
3,
|
||||
"LOG",
|
||||
true
|
||||
};
|
||||
NumberField field_volume{
|
||||
{ 28 * 8, 0 * 16 },
|
||||
2,
|
||||
{ 0, 99 },
|
||||
1,
|
||||
' ',
|
||||
};
|
||||
|
||||
Checkbox check_ignore {
|
||||
{ 1 * 8, 21 },
|
||||
12,
|
||||
"Ignore addr:",
|
||||
true
|
||||
};
|
||||
SymField sym_ignore {
|
||||
{ 16 * 8, 21 },
|
||||
7,
|
||||
SymField::SYMFIELD_DEC
|
||||
};
|
||||
uint32_t last_address = 0;
|
||||
pocsag::EccContainer ecc{};
|
||||
pocsag::POCSAGState pocsag_state{&ecc};
|
||||
POCSAGLogger logger{};
|
||||
uint16_t packet_count = 0;
|
||||
|
||||
Console console {
|
||||
{ 0, 3 * 16, 240, 256 }
|
||||
};
|
||||
RxFrequencyField field_frequency{
|
||||
{0 * 8, 0 * 8},
|
||||
nav_};
|
||||
|
||||
std::unique_ptr<POCSAGLogger> logger { };
|
||||
RFAmpField field_rf_amp{
|
||||
{11 * 8, 0 * 16}};
|
||||
LNAGainField field_lna{
|
||||
{13 * 8, 0 * 16}};
|
||||
VGAGainField field_vga{
|
||||
{16 * 8, 0 * 16}};
|
||||
|
||||
uint32_t target_frequency_ = initial_target_frequency;
|
||||
|
||||
void update_freq(rf::Frequency f);
|
||||
RSSI rssi{
|
||||
{19 * 8 - 4, 3, 6 * 8, 4}};
|
||||
Audio audio{
|
||||
{19 * 8 - 4, 8, 6 * 8, 4}};
|
||||
|
||||
void on_packet(const POCSAGPacketMessage * message);
|
||||
NumberField field_squelch{
|
||||
{25 * 8, 0 * 16},
|
||||
2,
|
||||
{0, 99},
|
||||
1,
|
||||
' ',
|
||||
true /*wrap*/};
|
||||
AudioVolumeField field_volume{
|
||||
{28 * 8, 0 * 16}};
|
||||
|
||||
void on_headphone_volume_changed(int32_t v);
|
||||
Image image_status{
|
||||
{0 * 8 + 4, 1 * 16 + 2, 16, 16},
|
||||
&bitmap_icon_pocsag,
|
||||
Theme::getInstance()->bg_darkest->foreground,
|
||||
Theme::getInstance()->bg_darkest->background};
|
||||
|
||||
uint32_t target_frequency() const;
|
||||
void set_target_frequency(const uint32_t new_value);
|
||||
|
||||
MessageHandlerRegistration message_handler_packet {
|
||||
Message::ID::POCSAGPacket,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const POCSAGPacketMessage*>(p);
|
||||
this->on_packet(message);
|
||||
}
|
||||
};
|
||||
Text text_packet_count{
|
||||
{3 * 8, 1 * 16 + 2, 5 * 8, 16},
|
||||
"0"};
|
||||
|
||||
BitsIndicator widget_bits{
|
||||
{8 * 8 + 1, 1 * 16 + 2}};
|
||||
|
||||
FrameIndicator widget_frames{
|
||||
{8 * 8 + 4, 1 * 16 + 2}};
|
||||
|
||||
BaudIndicator widget_baud{
|
||||
{8 * 9 + 1, 1 * 16 + 2}};
|
||||
|
||||
Button button_filter_last{
|
||||
{10 * 8, 1 * 16, 12 * 8, 20},
|
||||
"Filter Last"};
|
||||
|
||||
Button button_config{
|
||||
{22 * 8, 1 * 16, 8 * 8, 20},
|
||||
"Config"};
|
||||
|
||||
// 54 == status bar (16) + top controls (2 * 16 + 6).
|
||||
Console console{
|
||||
{0, 2 * 16 + 6, screen_width, screen_height - 54}};
|
||||
|
||||
void on_freqchg(int64_t freq);
|
||||
|
||||
MessageHandlerRegistration message_handler_freqchg{
|
||||
Message::ID::FreqChangeCommand,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const FreqChangeCommandMessage*>(p);
|
||||
this->on_freqchg(message->freq);
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_packet{
|
||||
Message::ID::POCSAGPacket,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const POCSAGPacketMessage*>(p);
|
||||
this->on_packet(message);
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_stats{
|
||||
Message::ID::POCSAGStats,
|
||||
[this](Message* const p) {
|
||||
const auto stats = static_cast<const POCSAGStatsMessage*>(p);
|
||||
this->on_stats(stats);
|
||||
}};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
#endif/*__POCSAG_APP_H__*/
|
||||
#endif /*__POCSAG_APP_H__*/
|
||||
|
||||
@@ -26,271 +26,199 @@
|
||||
|
||||
#include "ui_fileman.hpp"
|
||||
#include "io_file.hpp"
|
||||
#include "cpld_update.hpp"
|
||||
#include "io_convert.hpp"
|
||||
|
||||
#include "baseband_api.hpp"
|
||||
#include "metadata_file.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include "utility.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace ui {
|
||||
|
||||
void ReplayAppView::set_ready() {
|
||||
ready_signal = true;
|
||||
ready_signal = true;
|
||||
}
|
||||
|
||||
void ReplayAppView::on_file_changed(std::filesystem::path new_file_path) {
|
||||
File data_file, info_file;
|
||||
char file_data[257];
|
||||
|
||||
// Get file size
|
||||
auto data_open_error = data_file.open("/" + new_file_path.string());
|
||||
if (data_open_error.is_valid()) {
|
||||
file_error();
|
||||
return;
|
||||
}
|
||||
|
||||
file_path = new_file_path;
|
||||
|
||||
// Get original record frequency if available
|
||||
std::filesystem::path info_file_path = file_path;
|
||||
info_file_path.replace_extension(u".TXT");
|
||||
|
||||
sample_rate = 500000;
|
||||
|
||||
auto info_open_error = info_file.open("/" + info_file_path.string());
|
||||
if (!info_open_error.is_valid()) {
|
||||
memset(file_data, 0, 257);
|
||||
auto read_size = info_file.read(file_data, 256);
|
||||
if (!read_size.is_error()) {
|
||||
auto pos1 = strstr(file_data, "center_frequency=");
|
||||
if (pos1) {
|
||||
pos1 += 17;
|
||||
field_frequency.set_value(strtoll(pos1, nullptr, 10));
|
||||
}
|
||||
|
||||
auto pos2 = strstr(file_data, "sample_rate=");
|
||||
if (pos2) {
|
||||
pos2 += 12;
|
||||
sample_rate = strtoll(pos2, nullptr, 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
text_sample_rate.set(unit_auto_scale(sample_rate, 3, 0) + "Hz");
|
||||
|
||||
auto file_size = data_file.size();
|
||||
auto duration = (file_size * 1000) / (2 * 2 * sample_rate);
|
||||
|
||||
progressbar.set_max(file_size);
|
||||
text_filename.set(file_path.filename().string().substr(0, 12));
|
||||
text_duration.set(to_string_time_ms(duration));
|
||||
|
||||
button_play.focus();
|
||||
void ReplayAppView::on_file_changed(const fs::path& new_file_path) {
|
||||
file_path = new_file_path;
|
||||
File::Size file_size{};
|
||||
|
||||
{ // Get the size of the data file.
|
||||
File data_file;
|
||||
auto error = data_file.open(file_path);
|
||||
if (error) {
|
||||
file_error();
|
||||
return;
|
||||
}
|
||||
|
||||
file_size = data_file.size();
|
||||
}
|
||||
|
||||
// Get original record frequency if available.
|
||||
auto metadata_path = get_metadata_path(file_path);
|
||||
auto metadata = read_metadata_file(metadata_path);
|
||||
|
||||
if (metadata) {
|
||||
field_frequency.set_value(metadata->center_frequency);
|
||||
sample_rate = metadata->sample_rate;
|
||||
} else {
|
||||
// TODO: This is interesting because it implies that the
|
||||
// The capture will just be replayed at the freq set on the
|
||||
// FrequencyField. Is that an intentional behavior?
|
||||
sample_rate = 500000;
|
||||
}
|
||||
|
||||
// UI Fixup.
|
||||
text_sample_rate.set(unit_auto_scale(sample_rate, 3, 0) + "Hz");
|
||||
progressbar.set_max(file_size);
|
||||
text_filename.set(truncate(file_path.filename().string(), 12));
|
||||
|
||||
uint8_t sample_size = capture_file_sample_size(current()->path);
|
||||
auto duration = ms_duration(file_size, sample_rate, sample_size);
|
||||
text_duration.set(to_string_time_ms(duration));
|
||||
|
||||
button_play.focus();
|
||||
}
|
||||
|
||||
void ReplayAppView::on_tx_progress(const uint32_t progress) {
|
||||
progressbar.set_value(progress);
|
||||
progressbar.set_value(progress);
|
||||
}
|
||||
|
||||
void ReplayAppView::focus() {
|
||||
button_open.focus();
|
||||
button_open.focus();
|
||||
}
|
||||
|
||||
void ReplayAppView::file_error() {
|
||||
nav_.display_modal("Error", "File read error.");
|
||||
nav_.display_modal("Error", "File read error.");
|
||||
}
|
||||
|
||||
bool ReplayAppView::is_active() const {
|
||||
return (bool)replay_thread;
|
||||
return (bool)replay_thread;
|
||||
}
|
||||
|
||||
void ReplayAppView::toggle() {
|
||||
if( is_active() ) {
|
||||
stop(false);
|
||||
} else {
|
||||
start();
|
||||
}
|
||||
if (is_active()) {
|
||||
stop(false);
|
||||
} else {
|
||||
start();
|
||||
}
|
||||
}
|
||||
|
||||
void ReplayAppView::start() {
|
||||
stop(false);
|
||||
stop(false);
|
||||
|
||||
std::unique_ptr<stream::Reader> reader;
|
||||
|
||||
auto p = std::make_unique<FileReader>();
|
||||
auto open_error = p->open(file_path);
|
||||
if( open_error.is_valid() ) {
|
||||
file_error();
|
||||
return; // Fixes TX bug if there's a file error
|
||||
} else {
|
||||
reader = std::move(p);
|
||||
}
|
||||
std::unique_ptr<stream::Reader> reader;
|
||||
|
||||
if( reader ) {
|
||||
button_play.set_bitmap(&bitmap_stop);
|
||||
baseband::set_sample_rate(sample_rate * 8);
|
||||
|
||||
replay_thread = std::make_unique<ReplayThread>(
|
||||
std::move(reader),
|
||||
read_size, buffer_count,
|
||||
&ready_signal,
|
||||
[](uint32_t return_code) {
|
||||
ReplayThreadDoneMessage message { return_code };
|
||||
EventDispatcher::send_message(message);
|
||||
}
|
||||
);
|
||||
}
|
||||
field_rfgain.on_change = [this](int32_t v) {
|
||||
tx_gain = v;
|
||||
};
|
||||
field_rfgain.set_value(tx_gain);
|
||||
receiver_model.set_tx_gain(tx_gain);
|
||||
|
||||
auto p = std::make_unique<FileConvertReader>();
|
||||
auto open_error = p->open(file_path);
|
||||
if (open_error.is_valid()) {
|
||||
file_error();
|
||||
return; // Fixes TX bug if there's a file error
|
||||
} else {
|
||||
reader = std::move(p);
|
||||
}
|
||||
|
||||
field_rfamp.on_change = [this](int32_t v) {
|
||||
rf_amp = (bool)v;
|
||||
};
|
||||
field_rfamp.set_value(rf_amp ? 14 : 0);
|
||||
if (reader) {
|
||||
button_play.set_bitmap(&bitmap_stop);
|
||||
baseband::set_sample_rate(sample_rate, OversampleRate::x8);
|
||||
|
||||
//Enable Bias Tee if selected
|
||||
radio::set_antenna_bias(portapack::get_antenna_bias());
|
||||
|
||||
radio::enable({
|
||||
receiver_model.tuning_frequency(),
|
||||
sample_rate * 8,
|
||||
baseband_bandwidth,
|
||||
rf::Direction::Transmit,
|
||||
rf_amp, // previous code line : "receiver_model.rf_amp()," was passing the same rf_amp of all Receiver Apps
|
||||
static_cast<int8_t>(receiver_model.lna()),
|
||||
static_cast<int8_t>(receiver_model.vga())
|
||||
});
|
||||
}
|
||||
replay_thread = std::make_unique<ReplayThread>(
|
||||
std::move(reader),
|
||||
read_size, buffer_count,
|
||||
&ready_signal,
|
||||
[](uint32_t return_code) {
|
||||
ReplayThreadDoneMessage message{return_code};
|
||||
EventDispatcher::send_message(message);
|
||||
});
|
||||
}
|
||||
|
||||
transmitter_model.set_sampling_rate(sample_rate * toUType(OversampleRate::x8));
|
||||
transmitter_model.set_baseband_bandwidth(baseband_bandwidth);
|
||||
transmitter_model.enable();
|
||||
|
||||
if (portapack::persistent_memory::stealth_mode()) {
|
||||
DisplaySleepMessage message;
|
||||
EventDispatcher::send_message(message);
|
||||
}
|
||||
}
|
||||
|
||||
void ReplayAppView::stop(const bool do_loop) {
|
||||
if( is_active() )
|
||||
replay_thread.reset();
|
||||
|
||||
if (do_loop && check_loop.value()) {
|
||||
start();
|
||||
} else {
|
||||
radio::set_antenna_bias(false); //Turn off Bias Tee
|
||||
radio::disable();
|
||||
button_play.set_bitmap(&bitmap_play);
|
||||
}
|
||||
|
||||
ready_signal = false;
|
||||
if (is_active())
|
||||
replay_thread.reset();
|
||||
|
||||
if (do_loop && check_loop.value()) {
|
||||
start();
|
||||
} else {
|
||||
transmitter_model.disable();
|
||||
button_play.set_bitmap(&bitmap_play);
|
||||
}
|
||||
|
||||
ready_signal = false;
|
||||
}
|
||||
|
||||
void ReplayAppView::handle_replay_thread_done(const uint32_t return_code) {
|
||||
if (return_code == ReplayThread::END_OF_FILE) {
|
||||
stop(true);
|
||||
} else if (return_code == ReplayThread::READ_ERROR) {
|
||||
stop(false);
|
||||
file_error();
|
||||
}
|
||||
|
||||
progressbar.set_value(0);
|
||||
if (return_code == ReplayThread::END_OF_FILE) {
|
||||
stop(true);
|
||||
} else if (return_code == ReplayThread::READ_ERROR) {
|
||||
stop(false);
|
||||
file_error();
|
||||
}
|
||||
|
||||
progressbar.set_value(0);
|
||||
}
|
||||
|
||||
ReplayAppView::ReplayAppView(
|
||||
NavigationView& nav
|
||||
) : nav_ (nav)
|
||||
{
|
||||
NavigationView& nav)
|
||||
: nav_(nav) {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_replay);
|
||||
|
||||
tx_gain = 35;field_rfgain.set_value(tx_gain); // Initial default value (-12 dB's max ).
|
||||
field_rfamp.set_value(rf_amp ? 14 : 0); // Initial default value True. (TX RF amp on , +14dB's)
|
||||
add_children({
|
||||
&button_open,
|
||||
&text_filename,
|
||||
&text_sample_rate,
|
||||
&text_duration,
|
||||
&progressbar,
|
||||
&field_frequency,
|
||||
&tx_view, // now it handles previous rfgain, rfamp.
|
||||
&check_loop,
|
||||
&button_play,
|
||||
&waterfall,
|
||||
});
|
||||
|
||||
field_rfamp.on_change = [this](int32_t v) { // allow initial value change just after opened file.
|
||||
rf_amp = (bool)v;
|
||||
};
|
||||
field_rfamp.set_value(rf_amp ? 14 : 0);
|
||||
button_play.on_select = [this](ImageButton&) {
|
||||
this->toggle();
|
||||
};
|
||||
|
||||
field_rfgain.on_change = [this](int32_t v) { // allow initial value change just after opened file.
|
||||
tx_gain = v;
|
||||
};
|
||||
field_rfgain.set_value(tx_gain);
|
||||
|
||||
baseband::run_image(portapack::spi_flash::image_tag_replay);
|
||||
|
||||
add_children({
|
||||
&labels,
|
||||
&button_open,
|
||||
&text_filename,
|
||||
&text_sample_rate,
|
||||
&text_duration,
|
||||
&progressbar,
|
||||
&field_frequency,
|
||||
&field_rfgain,
|
||||
&field_rfamp, // let's not use common rf_amp
|
||||
&check_loop,
|
||||
&button_play,
|
||||
&waterfall,
|
||||
});
|
||||
|
||||
field_frequency.set_value(target_frequency());
|
||||
field_frequency.set_step(receiver_model.frequency_step());
|
||||
field_frequency.on_change = [this](rf::Frequency f) {
|
||||
this->on_target_frequency_changed(f);
|
||||
};
|
||||
field_frequency.on_edit = [this, &nav]() {
|
||||
// TODO: Provide separate modal method/scheme?
|
||||
auto new_view = nav.push<FrequencyKeypadView>(this->target_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
this->on_target_frequency_changed(f);
|
||||
this->field_frequency.set_value(f);
|
||||
};
|
||||
};
|
||||
|
||||
field_frequency.set_step(5000);
|
||||
|
||||
button_play.on_select = [this](ImageButton&) {
|
||||
this->toggle();
|
||||
};
|
||||
|
||||
button_open.on_select = [this, &nav](Button&) {
|
||||
auto open_view = nav.push<FileLoadView>(".C16");
|
||||
open_view->on_changed = [this](std::filesystem::path new_file_path) {
|
||||
on_file_changed(new_file_path);
|
||||
};
|
||||
};
|
||||
button_open.on_select = [this, &nav](Button&) {
|
||||
auto open_view = nav.push<FileLoadView>(".C*");
|
||||
open_view->on_changed = [this](fs::path new_file_path) {
|
||||
on_file_changed(new_file_path);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
ReplayAppView::~ReplayAppView() {
|
||||
radio::disable();
|
||||
|
||||
display.fill_rectangle({ 0,0,240, 320 },Color::black()); //Solving sometimes visible bottom waterfall artifacts, clearing all LCD pixels.
|
||||
chThdSleepMilliseconds(40); // (that happened sometimes if we interrupt the waterfall play at the beggining of the play around 25% and exit )
|
||||
hackrf::cpld::load_sram_no_verify(); // to leave all RX reception ok, without "ghost interference signal problem" at the exit .
|
||||
|
||||
baseband::shutdown(); // better this function at the end, after load_sram(). If not , sometimes produced hang up (now not , it is ok).
|
||||
transmitter_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
void ReplayAppView::on_hide() {
|
||||
stop(false);
|
||||
// TODO: Terrible kludge because widget system doesn't notify Waterfall that
|
||||
// it's being shown or hidden.
|
||||
waterfall.on_hide();
|
||||
View::on_hide();
|
||||
stop(false);
|
||||
// TODO: Terrible kludge because widget system doesn't notify Waterfall that
|
||||
// it's being shown or hidden.
|
||||
waterfall.on_hide();
|
||||
View::on_hide();
|
||||
}
|
||||
|
||||
void ReplayAppView::set_parent_rect(const Rect new_parent_rect) {
|
||||
View::set_parent_rect(new_parent_rect);
|
||||
View::set_parent_rect(new_parent_rect);
|
||||
|
||||
const ui::Rect waterfall_rect { 0, header_height, new_parent_rect.width(), new_parent_rect.height() - header_height };
|
||||
waterfall.set_parent_rect(waterfall_rect);
|
||||
}
|
||||
|
||||
void ReplayAppView::on_target_frequency_changed(rf::Frequency f) {
|
||||
set_target_frequency(f);
|
||||
}
|
||||
|
||||
void ReplayAppView::set_target_frequency(const rf::Frequency new_value) {
|
||||
persistent_memory::set_tuned_frequency(new_value);;
|
||||
}
|
||||
|
||||
rf::Frequency ReplayAppView::target_frequency() const {
|
||||
return persistent_memory::tuned_frequency();
|
||||
const ui::Rect waterfall_rect{0, header_height, new_parent_rect.width(), new_parent_rect.height() - header_height};
|
||||
waterfall.set_parent_rect(waterfall_rect);
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -23,11 +23,15 @@
|
||||
#ifndef __REPLAY_APP_HPP__
|
||||
#define __REPLAY_APP_HPP__
|
||||
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "replay_thread.hpp"
|
||||
#include "ui_spectrum.hpp"
|
||||
#include "ui_transmitter.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <memory>
|
||||
@@ -35,133 +39,108 @@
|
||||
namespace ui {
|
||||
|
||||
class ReplayAppView : public View {
|
||||
public:
|
||||
ReplayAppView(NavigationView& nav);
|
||||
~ReplayAppView();
|
||||
public:
|
||||
ReplayAppView(NavigationView& nav);
|
||||
~ReplayAppView();
|
||||
|
||||
void on_hide() override;
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
void focus() override;
|
||||
void on_hide() override;
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "Replay"; };
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
|
||||
static constexpr ui::Dim header_height = 3 * 16;
|
||||
|
||||
uint32_t sample_rate = 0;
|
||||
int32_t tx_gain { 47 };
|
||||
bool rf_amp { true }; // aux private var to store temporal, Replay App rf_amp user selection.
|
||||
static constexpr uint32_t baseband_bandwidth = 2500000;
|
||||
const size_t read_size { 16384 };
|
||||
const size_t buffer_count { 3 };
|
||||
std::string title() const override { return "Replay"; };
|
||||
|
||||
void on_file_changed(std::filesystem::path new_file_path);
|
||||
void on_target_frequency_changed(rf::Frequency f);
|
||||
void on_tx_progress(const uint32_t progress);
|
||||
|
||||
void set_target_frequency(const rf::Frequency new_value);
|
||||
rf::Frequency target_frequency() const;
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
TxRadioState radio_state_{};
|
||||
app_settings::SettingsManager settings_{
|
||||
"tx_replay", app_settings::Mode::TX};
|
||||
|
||||
void toggle();
|
||||
void start();
|
||||
void stop(const bool do_loop);
|
||||
bool is_active() const;
|
||||
void set_ready();
|
||||
void handle_replay_thread_done(const uint32_t return_code);
|
||||
void file_error();
|
||||
static constexpr ui::Dim header_height = 3 * 16;
|
||||
|
||||
std::filesystem::path file_path { };
|
||||
std::unique_ptr<ReplayThread> replay_thread { };
|
||||
bool ready_signal { false };
|
||||
uint32_t sample_rate = 0;
|
||||
int32_t tx_gain{47};
|
||||
bool rf_amp{true}; // aux private var to store temporal, Replay App rf_amp user selection.
|
||||
static constexpr uint32_t baseband_bandwidth = 2500000;
|
||||
const size_t read_size{16384};
|
||||
const size_t buffer_count{3};
|
||||
|
||||
Labels labels {
|
||||
{ { 10 * 8, 2 * 16 }, "GAIN A:", Color::light_grey() }
|
||||
};
|
||||
|
||||
Button button_open {
|
||||
{ 0 * 8, 0 * 16, 10 * 8, 2 * 16 },
|
||||
"Open file"
|
||||
};
|
||||
|
||||
Text text_filename {
|
||||
{ 11 * 8, 0 * 16, 12 * 8, 16 },
|
||||
"-"
|
||||
};
|
||||
Text text_sample_rate {
|
||||
{ 24 * 8, 0 * 16, 6 * 8, 16 },
|
||||
"-"
|
||||
};
|
||||
|
||||
Text text_duration {
|
||||
{ 11 * 8, 1 * 16, 6 * 8, 16 },
|
||||
"-"
|
||||
};
|
||||
ProgressBar progressbar {
|
||||
{ 18 * 8, 1 * 16, 12 * 8, 16 }
|
||||
};
|
||||
|
||||
FrequencyField field_frequency {
|
||||
{ 0 * 8, 2 * 16 },
|
||||
};
|
||||
|
||||
NumberField field_rfgain {
|
||||
{ 14 * 8, 2 * 16 },
|
||||
2,
|
||||
{ 0, 47 },
|
||||
1,
|
||||
' '
|
||||
};
|
||||
NumberField field_rfamp { // previously I was using "RFAmpField field_rf_amp" but that is general Receiver amp setting.
|
||||
{ 19 * 8, 2 * 16 },
|
||||
2,
|
||||
{ 0, 14 }, // this time we will display GUI , 0 or 14 dBs same as Mic App
|
||||
14,
|
||||
' '
|
||||
};
|
||||
Checkbox check_loop {
|
||||
{ 21 * 8, 2 * 16 },
|
||||
4,
|
||||
"Loop",
|
||||
true
|
||||
};
|
||||
ImageButton button_play {
|
||||
{ 28 * 8, 2 * 16, 2 * 8, 1 * 16 },
|
||||
&bitmap_play,
|
||||
Color::green(),
|
||||
Color::black()
|
||||
};
|
||||
void on_file_changed(const std::filesystem::path& new_file_path);
|
||||
void on_tx_progress(const uint32_t progress);
|
||||
|
||||
spectrum::WaterfallWidget waterfall { };
|
||||
void toggle();
|
||||
void start();
|
||||
void stop(const bool do_loop);
|
||||
bool is_active() const;
|
||||
void set_ready();
|
||||
void handle_replay_thread_done(const uint32_t return_code);
|
||||
void file_error();
|
||||
|
||||
MessageHandlerRegistration message_handler_replay_thread_error {
|
||||
Message::ID::ReplayThreadDone,
|
||||
[this](const Message* const p) {
|
||||
const auto message = *reinterpret_cast<const ReplayThreadDoneMessage*>(p);
|
||||
this->handle_replay_thread_done(message.return_code);
|
||||
}
|
||||
};
|
||||
|
||||
MessageHandlerRegistration message_handler_fifo_signal {
|
||||
Message::ID::RequestSignal,
|
||||
[this](const Message* const p) {
|
||||
const auto message = static_cast<const RequestSignalMessage*>(p);
|
||||
if (message->signal == RequestSignalMessage::Signal::FillRequest) {
|
||||
this->set_ready();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
MessageHandlerRegistration message_handler_tx_progress {
|
||||
Message::ID::TXProgress,
|
||||
[this](const Message* const p) {
|
||||
const auto message = *reinterpret_cast<const TXProgressMessage*>(p);
|
||||
this->on_tx_progress(message.progress);
|
||||
}
|
||||
};
|
||||
std::filesystem::path file_path{};
|
||||
std::unique_ptr<ReplayThread> replay_thread{};
|
||||
bool ready_signal{false};
|
||||
|
||||
Button button_open{
|
||||
{0 * 8, 0 * 16, 10 * 8, 2 * 16},
|
||||
"Open file"};
|
||||
|
||||
Text text_filename{
|
||||
{11 * 8, 0 * 16, 12 * 8, 16},
|
||||
"-"};
|
||||
Text text_sample_rate{
|
||||
{24 * 8, 0 * 16, 6 * 8, 16},
|
||||
"-"};
|
||||
|
||||
Text text_duration{
|
||||
{11 * 8, 1 * 16, 6 * 8, 16},
|
||||
"-"};
|
||||
ProgressBar progressbar{
|
||||
{18 * 8, 1 * 16, 12 * 8, 16}};
|
||||
|
||||
TxFrequencyField field_frequency{
|
||||
{0 * 8, 2 * 16},
|
||||
nav_};
|
||||
|
||||
TransmitterView2 tx_view{
|
||||
{11 * 8, 2 * 16},
|
||||
/*short_ui*/ true};
|
||||
|
||||
Checkbox check_loop{
|
||||
{21 * 8, 2 * 16},
|
||||
4,
|
||||
"Loop",
|
||||
true};
|
||||
ImageButton button_play{
|
||||
{28 * 8, 2 * 16, 2 * 8, 1 * 16},
|
||||
&bitmap_play,
|
||||
Theme::getInstance()->fg_green->foreground,
|
||||
Theme::getInstance()->fg_green->background};
|
||||
|
||||
spectrum::WaterfallView waterfall{};
|
||||
|
||||
MessageHandlerRegistration message_handler_replay_thread_error{
|
||||
Message::ID::ReplayThreadDone,
|
||||
[this](const Message* const p) {
|
||||
const auto message = *reinterpret_cast<const ReplayThreadDoneMessage*>(p);
|
||||
this->handle_replay_thread_done(message.return_code);
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_fifo_signal{
|
||||
Message::ID::RequestSignal,
|
||||
[this](const Message* const p) {
|
||||
const auto message = static_cast<const RequestSignalMessage*>(p);
|
||||
if (message->signal == RequestSignalMessage::Signal::FillRequest) {
|
||||
this->set_ready();
|
||||
}
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_tx_progress{
|
||||
Message::ID::TXProgress,
|
||||
[this](const Message* const p) {
|
||||
const auto message = *reinterpret_cast<const TXProgressMessage*>(p);
|
||||
this->on_tx_progress(message.progress);
|
||||
}};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
#endif/*__REPLAY_APP_HPP__*/
|
||||
#endif /*__REPLAY_APP_HPP__*/
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2016 Furrtek
|
||||
* Copyright (C) 2024 Mark Thompson
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
@@ -25,7 +26,9 @@
|
||||
#include "soundboard_app.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "tonesets.hpp"
|
||||
#include "cpld_update.hpp"
|
||||
#include "ui_tone_key.hpp"
|
||||
#include "audio.hpp"
|
||||
#include "file_path.hpp"
|
||||
|
||||
using namespace tonekey;
|
||||
using namespace portapack;
|
||||
@@ -33,272 +36,269 @@ using namespace portapack;
|
||||
namespace ui {
|
||||
|
||||
bool SoundBoardView::is_active() const {
|
||||
return (bool)replay_thread;
|
||||
return (bool)replay_thread;
|
||||
}
|
||||
|
||||
void SoundBoardView::stop() {
|
||||
if (is_active())
|
||||
replay_thread.reset();
|
||||
|
||||
transmitter_model.disable();
|
||||
tx_view.set_transmitting(false);
|
||||
|
||||
//button_play.set_bitmap(&bitmap_play);
|
||||
ready_signal = false;
|
||||
if (is_active())
|
||||
replay_thread.reset();
|
||||
|
||||
audio::output::stop();
|
||||
transmitter_model.disable();
|
||||
tx_view.set_transmitting(false);
|
||||
|
||||
// button_play.set_bitmap(&bitmap_play);
|
||||
ready_signal = false;
|
||||
}
|
||||
|
||||
void SoundBoardView::handle_replay_thread_done(const uint32_t return_code) {
|
||||
stop();
|
||||
//progressbar.set_value(0);
|
||||
|
||||
if (return_code == ReplayThread::END_OF_FILE) {
|
||||
if (check_random.value()) {
|
||||
lfsr_v = lfsr_iterate(lfsr_v);
|
||||
playing_id = lfsr_v % file_list.size();
|
||||
menu_view.set_highlighted(playing_id);
|
||||
start_tx(playing_id);
|
||||
} else if (check_loop.value()) {
|
||||
start_tx(playing_id);
|
||||
}
|
||||
} else if (return_code == ReplayThread::READ_ERROR) {
|
||||
file_error();
|
||||
}
|
||||
stop();
|
||||
progressbar.set_value(0);
|
||||
|
||||
if (return_code == ReplayThread::END_OF_FILE) {
|
||||
if (check_random.value()) {
|
||||
lfsr_v = lfsr_iterate(lfsr_v);
|
||||
playing_id = lfsr_v % file_list.size();
|
||||
menu_view.set_highlighted(playing_id);
|
||||
start_tx(playing_id);
|
||||
} else if (check_loop.value()) {
|
||||
start_tx(playing_id);
|
||||
}
|
||||
} else if (return_code == ReplayThread::READ_ERROR) {
|
||||
file_error();
|
||||
}
|
||||
}
|
||||
|
||||
void SoundBoardView::set_ready() {
|
||||
ready_signal = true;
|
||||
ready_signal = true;
|
||||
}
|
||||
|
||||
void SoundBoardView::focus() {
|
||||
menu_view.focus();
|
||||
menu_view.focus();
|
||||
}
|
||||
|
||||
void SoundBoardView::file_error() {
|
||||
nav_.display_modal("Error", "File read error.");
|
||||
nav_.display_modal("Error", "File read error.");
|
||||
}
|
||||
|
||||
void SoundBoardView::start_tx(const uint32_t id) {
|
||||
auto reader = std::make_unique<WAVFileReader>();
|
||||
uint32_t tone_key_index = options_tone_key.selected_index();
|
||||
uint32_t sample_rate;
|
||||
|
||||
stop();
|
||||
if (file_list.empty()) {
|
||||
file_error();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!reader->open(u"/WAV/" + file_list[id].native())) {
|
||||
file_error();
|
||||
return;
|
||||
}
|
||||
|
||||
playing_id = id;
|
||||
|
||||
//progressbar.set_max(reader->sample_count());
|
||||
|
||||
//button_play.set_bitmap(&bitmap_stop);
|
||||
|
||||
sample_rate = reader->sample_rate();
|
||||
|
||||
replay_thread = std::make_unique<ReplayThread>(
|
||||
std::move(reader),
|
||||
read_size, buffer_count,
|
||||
&ready_signal,
|
||||
[](uint32_t return_code) {
|
||||
ReplayThreadDoneMessage message { return_code };
|
||||
EventDispatcher::send_message(message);
|
||||
}
|
||||
);
|
||||
|
||||
baseband::set_audiotx_config(
|
||||
1536000 / 20, // Update vu-meter at 20Hz
|
||||
transmitter_model.channel_bandwidth(),
|
||||
0, // Gain is unused
|
||||
8, // shift_bits_s16, default 8 bits, but also unused
|
||||
TONES_F2D(tone_key_frequency(tone_key_index), 1536000),
|
||||
0, //AM
|
||||
0, //DSB
|
||||
0, //USB
|
||||
0 //LSB
|
||||
);
|
||||
baseband::set_sample_rate(sample_rate);
|
||||
|
||||
transmitter_model.set_sampling_rate(1536000);
|
||||
transmitter_model.set_baseband_bandwidth(1750000);
|
||||
transmitter_model.enable();
|
||||
|
||||
tx_view.set_transmitting(true);
|
||||
auto reader = std::make_unique<WAVFileReader>();
|
||||
|
||||
uint32_t tone_key_index = options_tone_key.selected_index();
|
||||
uint32_t sample_rate;
|
||||
uint8_t bits_per_sample;
|
||||
|
||||
stop();
|
||||
|
||||
if (!reader->open(u"/WAV/" + file_list[id].native())) {
|
||||
file_error();
|
||||
return;
|
||||
}
|
||||
|
||||
playing_id = id;
|
||||
|
||||
progressbar.set_max(reader->sample_count());
|
||||
|
||||
// button_play.set_bitmap(&bitmap_stop);
|
||||
|
||||
sample_rate = reader->sample_rate();
|
||||
bits_per_sample = reader->bits_per_sample();
|
||||
|
||||
replay_thread = std::make_unique<ReplayThread>(
|
||||
std::move(reader),
|
||||
read_size, buffer_count,
|
||||
&ready_signal,
|
||||
[](uint32_t return_code) {
|
||||
ReplayThreadDoneMessage message{return_code};
|
||||
EventDispatcher::send_message(message);
|
||||
});
|
||||
|
||||
// TODO: Delete all this and use tx model.
|
||||
baseband::set_audiotx_config(
|
||||
1536000 / 20, // Update vu-meter at 20Hz
|
||||
transmitter_model.channel_bandwidth(),
|
||||
0, // Gain is unused
|
||||
8, // shift_bits_s16, default 8 bits, but also unused
|
||||
bits_per_sample,
|
||||
TONES_F2D(tone_key_frequency(tone_key_index), TONES_SAMPLERATE),
|
||||
false, // AM
|
||||
false, // DSB
|
||||
false, // USB
|
||||
false // LSB
|
||||
);
|
||||
baseband::set_sample_rate(sample_rate);
|
||||
|
||||
transmitter_model.enable();
|
||||
|
||||
tx_view.set_transmitting(true);
|
||||
|
||||
if (tone_key_index == 0)
|
||||
audio::output::start();
|
||||
}
|
||||
|
||||
/*void SoundBoardView::show_infos() {
|
||||
if (!reader->open(file_list[menu_view.highlighted_index()]))
|
||||
return;
|
||||
|
||||
text_duration.set(to_string_time_ms(reader->ms_duration()));
|
||||
text_title.set(reader->title().substr(0, 15));
|
||||
if (!reader->open(file_list[menu_view.highlighted_index()]))
|
||||
return;
|
||||
|
||||
text_duration.set(to_string_time_ms(reader->ms_duration()));
|
||||
text_title.set(reader->title().substr(0, 15));
|
||||
}*/
|
||||
|
||||
void SoundBoardView::on_tx_progress(const uint32_t progress) {
|
||||
(void)progress ; // avoid warning
|
||||
//progressbar.set_value(progress);
|
||||
progressbar.set_value(progress);
|
||||
}
|
||||
|
||||
void SoundBoardView::on_select_entry() {
|
||||
tx_view.focus();
|
||||
tx_view.focus();
|
||||
}
|
||||
|
||||
void SoundBoardView::refresh_list() {
|
||||
auto reader = std::make_unique<WAVFileReader>();
|
||||
|
||||
file_list.clear();
|
||||
c_page = page;
|
||||
|
||||
// List directories and files, put directories up top
|
||||
uint32_t count = 0;
|
||||
for (const auto& entry : std::filesystem::directory_iterator(u"WAV", u"*")) {
|
||||
if (std::filesystem::is_regular_file(entry.status())) {
|
||||
if (entry.path().string().length()) {
|
||||
|
||||
auto entry_extension = entry.path().extension().string();
|
||||
|
||||
for (auto &c: entry_extension)
|
||||
c = toupper(c);
|
||||
|
||||
if (entry_extension == ".WAV") {
|
||||
|
||||
if (reader->open(u"/WAV/" + entry.path().native())) {
|
||||
if ((reader->channels() == 1) && (reader->bits_per_sample() == 8)) {
|
||||
//sounds[c].ms_duration = reader->ms_duration();
|
||||
//sounds[c].path = u"WAV/" + entry.path().native();
|
||||
if (count >= (page - 1) * 100 && count < page * 100){
|
||||
file_list.push_back(entry.path());
|
||||
if (file_list.size() == 100){
|
||||
page++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
auto reader = std::make_unique<WAVFileReader>();
|
||||
|
||||
if (!file_list.size()) {
|
||||
// Hide widgets, show warning
|
||||
if (page == 1){
|
||||
menu_view.hidden(true);
|
||||
text_empty.hidden(false);
|
||||
set_dirty();
|
||||
}else{
|
||||
page = 1;
|
||||
refresh_list();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Hide warning, show widgets
|
||||
menu_view.hidden(false);
|
||||
text_empty.hidden(true);
|
||||
set_dirty();
|
||||
|
||||
menu_view.clear();
|
||||
|
||||
for (size_t n = 0; n < file_list.size(); n++) {
|
||||
menu_view.add_item({
|
||||
file_list[n].string().substr(0, 30),
|
||||
ui::Color::white(),
|
||||
nullptr,
|
||||
[this](){
|
||||
on_select_entry();
|
||||
}
|
||||
});
|
||||
}
|
||||
file_list.clear();
|
||||
c_page = page;
|
||||
|
||||
page_info.set("Page: " + to_string_dec_uint(c_page) + " Sounds: " + to_string_dec_uint(file_list.size()));
|
||||
menu_view.set_highlighted(0); // Refresh
|
||||
}
|
||||
// List directories and files, put directories up top
|
||||
uint32_t count = 0;
|
||||
for (const auto& entry : std::filesystem::directory_iterator(wav_dir, u"*")) {
|
||||
if (std::filesystem::is_regular_file(entry.status())) {
|
||||
if (entry.path().string().length()) {
|
||||
auto entry_extension = entry.path().extension().string();
|
||||
|
||||
if (file_list.size() < 100){
|
||||
page = 1;
|
||||
}
|
||||
for (auto& c : entry_extension)
|
||||
c = toupper(c);
|
||||
|
||||
if (entry_extension == ".WAV") {
|
||||
if (reader->open(wav_dir / entry.path())) {
|
||||
if ((reader->channels() == 1) && ((reader->bits_per_sample() == 8) || (reader->bits_per_sample() == 16))) {
|
||||
// sounds[c].ms_duration = reader->ms_duration();
|
||||
// sounds[c].path = u"WAV/" + entry.path().native();
|
||||
if (count >= (page - 1) * 100 && count < page * 100) {
|
||||
file_list.push_back(entry.path());
|
||||
if (file_list.size() == 100) {
|
||||
page++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!file_list.size()) {
|
||||
// Hide widgets, show warning
|
||||
if (page == 1) {
|
||||
menu_view.hidden(true);
|
||||
text_empty.hidden(false);
|
||||
set_dirty();
|
||||
} else {
|
||||
page = 1;
|
||||
refresh_list();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Hide warning, show widgets
|
||||
menu_view.hidden(false);
|
||||
text_empty.hidden(true);
|
||||
set_dirty();
|
||||
|
||||
menu_view.clear();
|
||||
|
||||
for (size_t n = 0; n < file_list.size(); n++) {
|
||||
menu_view.add_item({file_list[n].string().substr(0, 30),
|
||||
ui::Theme::getInstance()->fg_magenta->foreground,
|
||||
nullptr,
|
||||
[this](KeyEvent) {
|
||||
on_select_entry();
|
||||
}});
|
||||
}
|
||||
|
||||
page_info.set("Page: " + to_string_dec_uint(c_page) + " Sounds: " + to_string_dec_uint(file_list.size()));
|
||||
menu_view.set_highlighted(0); // Refresh
|
||||
}
|
||||
|
||||
if (file_list.size() < 100) {
|
||||
page = 1;
|
||||
}
|
||||
}
|
||||
|
||||
SoundBoardView::SoundBoardView(
|
||||
NavigationView& nav
|
||||
) : nav_ (nav)
|
||||
{
|
||||
baseband::run_image(portapack::spi_flash::image_tag_audio_tx);
|
||||
|
||||
add_children({
|
||||
&labels,
|
||||
&menu_view,
|
||||
&text_empty,
|
||||
&options_tone_key,
|
||||
//&text_title,
|
||||
//&text_duration,
|
||||
//&progressbar,
|
||||
&page_info,
|
||||
&check_loop,
|
||||
&check_random,
|
||||
&button_prev_page,
|
||||
&button_next_page,
|
||||
&tx_view
|
||||
});
|
||||
NavigationView& nav)
|
||||
: nav_(nav) {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_audio_tx);
|
||||
|
||||
// load app settings
|
||||
auto rc = settings.load("tx_soundboard", &app_settings);
|
||||
if(rc == SETTINGS_OK) {
|
||||
transmitter_model.set_rf_amp(app_settings.tx_amp);
|
||||
transmitter_model.set_channel_bandwidth(app_settings.channel_bandwidth);
|
||||
transmitter_model.set_tuning_frequency(app_settings.tx_frequency);
|
||||
transmitter_model.set_tx_gain(app_settings.tx_gain);
|
||||
}
|
||||
|
||||
refresh_list();
|
||||
add_children({&labels,
|
||||
&menu_view,
|
||||
&text_empty,
|
||||
&options_tone_key,
|
||||
//&text_title,
|
||||
//&text_duration,
|
||||
&progressbar,
|
||||
&field_volume,
|
||||
&text_volume_disabled,
|
||||
&page_info,
|
||||
&check_loop,
|
||||
&check_random,
|
||||
&button_prev_page,
|
||||
&button_next_page,
|
||||
&tx_view});
|
||||
|
||||
button_next_page.on_select = [this](Button&) {
|
||||
this->refresh_list();
|
||||
};
|
||||
refresh_list();
|
||||
|
||||
button_prev_page.on_select = [this](Button&) {
|
||||
if (c_page == 1) return;
|
||||
if (c_page == 2) page = 1;
|
||||
page = c_page - 1;
|
||||
refresh_list();
|
||||
};
|
||||
|
||||
//text_title.set(to_string_dec_uint(file_list.size()));
|
||||
|
||||
tone_keys_populate(options_tone_key);
|
||||
options_tone_key.set_selected_index(0);
|
||||
|
||||
check_loop.set_value(false);
|
||||
check_random.set_value(false);
|
||||
button_next_page.on_select = [this](Button&) {
|
||||
this->refresh_list();
|
||||
};
|
||||
|
||||
tx_view.on_edit_frequency = [this, &nav]() {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(receiver_model.tuning_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
transmitter_model.set_tuning_frequency(f);
|
||||
};
|
||||
};
|
||||
|
||||
tx_view.on_start = [this]() {
|
||||
start_tx(menu_view.highlighted_index());
|
||||
};
|
||||
|
||||
tx_view.on_stop = [this]() {
|
||||
tx_view.set_transmitting(false);
|
||||
stop();
|
||||
};
|
||||
button_prev_page.on_select = [this](Button&) {
|
||||
if (c_page == 1) return;
|
||||
if (c_page == 2) page = 1;
|
||||
page = c_page - 1;
|
||||
refresh_list();
|
||||
};
|
||||
|
||||
// text_title.set(to_string_dec_uint(file_list.size()));
|
||||
|
||||
tone_keys_populate(options_tone_key);
|
||||
options_tone_key.set_selected_index(0);
|
||||
|
||||
text_volume_disabled.hidden(true);
|
||||
options_tone_key.on_change = [this](size_t index, OptionsField::value_t) {
|
||||
bool tone_key_enabled = (index != 0);
|
||||
text_volume_disabled.hidden(!tone_key_enabled);
|
||||
field_volume.hidden(tone_key_enabled);
|
||||
};
|
||||
|
||||
check_loop.set_value(false);
|
||||
check_random.set_value(false);
|
||||
|
||||
tx_view.on_edit_frequency = [this, &nav]() {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(transmitter_model.target_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
transmitter_model.set_target_frequency(f);
|
||||
};
|
||||
};
|
||||
|
||||
tx_view.on_start = [this]() {
|
||||
start_tx(menu_view.highlighted_index());
|
||||
};
|
||||
|
||||
tx_view.on_stop = [this]() {
|
||||
tx_view.set_transmitting(false);
|
||||
stop();
|
||||
};
|
||||
}
|
||||
|
||||
SoundBoardView::~SoundBoardView() {
|
||||
// save app settings
|
||||
app_settings.tx_frequency = transmitter_model.tuning_frequency();
|
||||
settings.save("tx_soundboard", &app_settings);
|
||||
|
||||
stop();
|
||||
transmitter_model.disable();
|
||||
hackrf::cpld::load_sram_no_verify(); // to leave all RX ok, without ghost signal problem at the exit.
|
||||
baseband::shutdown(); // better this function at the end, not load_sram() that sometimes produces hang up.
|
||||
stop();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
}
|
||||
} // namespace ui
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2016 Furrtek
|
||||
* Copyright (C) 2024 Mark Thompson
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
@@ -29,154 +30,151 @@
|
||||
#include "baseband_api.hpp"
|
||||
#include "lfsr_random.hpp"
|
||||
#include "io_wave.hpp"
|
||||
#include "tone_key.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
|
||||
namespace ui {
|
||||
|
||||
class SoundBoardView : public View {
|
||||
public:
|
||||
SoundBoardView(NavigationView& nav);
|
||||
~SoundBoardView();
|
||||
public:
|
||||
SoundBoardView(NavigationView& nav);
|
||||
~SoundBoardView();
|
||||
|
||||
SoundBoardView(const SoundBoardView&) = delete;
|
||||
SoundBoardView(SoundBoardView&&) = delete;
|
||||
SoundBoardView& operator=(const SoundBoardView&) = delete;
|
||||
SoundBoardView& operator=(SoundBoardView&&) = delete;
|
||||
SoundBoardView(const SoundBoardView&) = delete;
|
||||
SoundBoardView(SoundBoardView&&) = delete;
|
||||
SoundBoardView& operator=(const SoundBoardView&) = delete;
|
||||
SoundBoardView& operator=(SoundBoardView&&) = delete;
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "Soundbrd TX"; };
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
void focus() override;
|
||||
|
||||
// app save settings
|
||||
std::app_settings settings { };
|
||||
std::app_settings::AppSettings app_settings { };
|
||||
|
||||
enum tx_modes {
|
||||
NORMAL = 0,
|
||||
RANDOM
|
||||
};
|
||||
|
||||
tx_modes tx_mode = NORMAL;
|
||||
|
||||
uint32_t playing_id { };
|
||||
uint32_t page = 1;
|
||||
uint32_t c_page = 1;
|
||||
|
||||
std::vector<std::filesystem::path> file_list { };
|
||||
std::string title() const override { return "Soundbrd TX"; };
|
||||
|
||||
const size_t read_size { 2048 }; // Less ?
|
||||
const size_t buffer_count { 3 };
|
||||
std::unique_ptr<ReplayThread> replay_thread { };
|
||||
bool ready_signal { false };
|
||||
lfsr_word_t lfsr_v = 1;
|
||||
|
||||
//void show_infos();
|
||||
void start_tx(const uint32_t id);
|
||||
//void on_ctcss_changed(uint32_t v);
|
||||
void stop();
|
||||
bool is_active() const;
|
||||
void set_ready();
|
||||
void handle_replay_thread_done(const uint32_t return_code);
|
||||
void file_error();
|
||||
void on_tx_progress(const uint32_t progress);
|
||||
void refresh_list();
|
||||
void on_select_entry();
|
||||
|
||||
Labels labels {
|
||||
//{ { 0, 20 * 8 + 4 }, "Title:", Color::light_grey() },
|
||||
{ { 0, 180 }, "Key:", Color::light_grey() }
|
||||
};
|
||||
private:
|
||||
TxRadioState radio_state_{
|
||||
0 /* frequency */,
|
||||
1750000 /* bandwidth */,
|
||||
1536000 /* sampling rate */
|
||||
};
|
||||
app_settings::SettingsManager settings_{
|
||||
"tx_soundboard", app_settings::Mode::TX};
|
||||
|
||||
Button button_next_page {
|
||||
{ 30 * 7, 25 * 8, 10 * 3, 2 * 14 },
|
||||
"=>"
|
||||
};
|
||||
NavigationView& nav_;
|
||||
|
||||
Button button_prev_page {
|
||||
{ 17 * 10, 25 * 8, 10 * 3, 2 * 14 },
|
||||
"<="
|
||||
};
|
||||
enum tx_modes {
|
||||
NORMAL = 0,
|
||||
RANDOM
|
||||
};
|
||||
|
||||
Text page_info {
|
||||
{ 0, 30 * 8 - 4, 30 * 8, 16 }
|
||||
};
|
||||
tx_modes tx_mode = NORMAL;
|
||||
|
||||
MenuView menu_view {
|
||||
{ 0, 0, 240, 175 },
|
||||
true
|
||||
};
|
||||
Text text_empty {
|
||||
{ 7 * 8, 12 * 8, 16 * 8, 16 },
|
||||
"Empty directory !",
|
||||
};
|
||||
|
||||
/*Text text_title {
|
||||
{ 6 * 8, 20 * 8 + 4, 15 * 8, 16 }
|
||||
};*/
|
||||
|
||||
/*Text text_duration {
|
||||
{ 22 * 8, 20 * 8 + 4, 6 * 8, 16 }
|
||||
};*/
|
||||
|
||||
OptionsField options_tone_key {
|
||||
{ 32 , 180 },
|
||||
18,
|
||||
{ }
|
||||
};
|
||||
|
||||
Checkbox check_loop {
|
||||
{ 0, 25 * 8 + 4 },
|
||||
4,
|
||||
"Loop"
|
||||
};
|
||||
|
||||
Checkbox check_random {
|
||||
{ 10 * 7, 25 * 8 + 4 },
|
||||
6,
|
||||
"Random"
|
||||
};
|
||||
|
||||
//ProgressBar progressbar {
|
||||
// { 0 * 8, 30 * 8 - 4, 30 * 8, 16 }
|
||||
//};
|
||||
|
||||
TransmitterView tx_view {
|
||||
16 * 16,
|
||||
5000,
|
||||
12
|
||||
};
|
||||
|
||||
MessageHandlerRegistration message_handler_replay_thread_error {
|
||||
Message::ID::ReplayThreadDone,
|
||||
[this](const Message* const p) {
|
||||
const auto message = *reinterpret_cast<const ReplayThreadDoneMessage*>(p);
|
||||
this->handle_replay_thread_done(message.return_code);
|
||||
}
|
||||
};
|
||||
|
||||
MessageHandlerRegistration message_handler_fifo_signal {
|
||||
Message::ID::RequestSignal,
|
||||
[this](const Message* const p) {
|
||||
const auto message = static_cast<const RequestSignalMessage*>(p);
|
||||
if (message->signal == RequestSignalMessage::Signal::FillRequest) {
|
||||
this->set_ready();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
MessageHandlerRegistration message_handler_tx_progress {
|
||||
Message::ID::TXProgress,
|
||||
[this](const Message* const p) {
|
||||
const auto message = *reinterpret_cast<const TXProgressMessage*>(p);
|
||||
this->on_tx_progress(message.progress);
|
||||
}
|
||||
};
|
||||
uint32_t playing_id{};
|
||||
uint32_t page = 1;
|
||||
uint32_t c_page = 1;
|
||||
|
||||
std::vector<std::filesystem::path> file_list{};
|
||||
|
||||
const size_t read_size{2048}; // Less ?
|
||||
const size_t buffer_count{3};
|
||||
std::unique_ptr<ReplayThread> replay_thread{};
|
||||
bool ready_signal{false};
|
||||
lfsr_word_t lfsr_v = 1;
|
||||
|
||||
// void show_infos();
|
||||
void start_tx(const uint32_t id);
|
||||
// void on_ctcss_changed(uint32_t v);
|
||||
void stop();
|
||||
bool is_active() const;
|
||||
void set_ready();
|
||||
void handle_replay_thread_done(const uint32_t return_code);
|
||||
void file_error();
|
||||
void on_tx_progress(const uint32_t progress);
|
||||
void refresh_list();
|
||||
void on_select_entry();
|
||||
|
||||
Labels labels{
|
||||
{{24 * 8, 180}, "Vol:", Theme::getInstance()->fg_light->foreground},
|
||||
{{0, 180}, "Key:", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
Button button_next_page{
|
||||
{30 * 7, 25 * 8, 10 * 3, 2 * 14},
|
||||
"=>"};
|
||||
|
||||
Button button_prev_page{
|
||||
{17 * 10, 25 * 8, 10 * 3, 2 * 14},
|
||||
"<="};
|
||||
|
||||
Text page_info{
|
||||
{0, 29 * 8, 30 * 8, 16}};
|
||||
|
||||
MenuView menu_view{
|
||||
{0, 0, 240, 175},
|
||||
true};
|
||||
Text text_empty{
|
||||
{7 * 8, 12 * 8, 16 * 8, 16},
|
||||
"Empty directory !",
|
||||
};
|
||||
|
||||
/*Text text_title {
|
||||
{ 6 * 8, 20 * 8 + 4, 15 * 8, 16 }
|
||||
};*/
|
||||
|
||||
/*Text text_duration {
|
||||
{ 22 * 8, 20 * 8 + 4, 6 * 8, 16 }
|
||||
};*/
|
||||
|
||||
OptionsField options_tone_key{
|
||||
{4 * 8, 180},
|
||||
18,
|
||||
{}};
|
||||
|
||||
AudioVolumeField field_volume{
|
||||
{28 * 8, 180}};
|
||||
Text text_volume_disabled{
|
||||
{28 * 8, 180, 3 * 8, 16},
|
||||
"--"};
|
||||
|
||||
Checkbox check_loop{
|
||||
{0, 25 * 8 + 4},
|
||||
4,
|
||||
"Loop"};
|
||||
|
||||
Checkbox check_random{
|
||||
{10 * 7, 25 * 8 + 4},
|
||||
6,
|
||||
"Random"};
|
||||
|
||||
ProgressBar progressbar{
|
||||
{0 * 8, 31 * 8 + 2, 30 * 8, 4}};
|
||||
|
||||
TransmitterView tx_view{
|
||||
16 * 16,
|
||||
5000,
|
||||
12};
|
||||
|
||||
MessageHandlerRegistration message_handler_replay_thread_error{
|
||||
Message::ID::ReplayThreadDone,
|
||||
[this](const Message* const p) {
|
||||
const auto message = *reinterpret_cast<const ReplayThreadDoneMessage*>(p);
|
||||
this->handle_replay_thread_done(message.return_code);
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_fifo_signal{
|
||||
Message::ID::RequestSignal,
|
||||
[this](const Message* const p) {
|
||||
const auto message = static_cast<const RequestSignalMessage*>(p);
|
||||
if (message->signal == RequestSignalMessage::Signal::FillRequest) {
|
||||
this->set_ready();
|
||||
}
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_tx_progress{
|
||||
Message::ID::TXProgress,
|
||||
[this](const Message* const p) {
|
||||
const auto message = *reinterpret_cast<const TXProgressMessage*>(p);
|
||||
this->on_tx_progress(message.progress);
|
||||
}};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
#endif/*__UI_SOUNDBOARD_H__*/
|
||||
#endif /*__UI_SOUNDBOARD_H__*/
|
||||
|
||||
@@ -25,10 +25,10 @@
|
||||
using namespace portapack;
|
||||
|
||||
SpectrumAnalysisModel::SpectrumAnalysisModel() {
|
||||
receiver_model.set_baseband_configuration({
|
||||
.mode = 4,
|
||||
.sampling_rate = 20000000,
|
||||
.decimation_factor = 1,
|
||||
});
|
||||
receiver_model.set_baseband_bandwidth(12000000);
|
||||
receiver_model.set_baseband_configuration({
|
||||
.mode = 4,
|
||||
.sampling_rate = 20000000,
|
||||
.decimation_factor = 1,
|
||||
});
|
||||
receiver_model.set_baseband_bandwidth(12000000);
|
||||
}
|
||||
|
||||
@@ -26,19 +26,18 @@
|
||||
#include "ui_spectrum.hpp"
|
||||
|
||||
class SpectrumAnalysisModel {
|
||||
public:
|
||||
SpectrumAnalysisModel();
|
||||
public:
|
||||
SpectrumAnalysisModel();
|
||||
};
|
||||
|
||||
namespace ui {
|
||||
|
||||
class SpectrumAnalysisView : public spectrum::WaterfallWidget {
|
||||
public:
|
||||
|
||||
private:
|
||||
SpectrumAnalysisModel model;
|
||||
class SpectrumAnalysisView : public spectrum::WaterfallView {
|
||||
public:
|
||||
private:
|
||||
SpectrumAnalysisModel model;
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
#endif/*__SPECTRUM_ANALYSIS_APP_H__*/
|
||||
#endif /*__SPECTRUM_ANALYSIS_APP_H__*/
|
||||
|
||||
@@ -1,283 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "tpms_app.hpp"
|
||||
|
||||
#include "baseband_api.hpp"
|
||||
|
||||
#include "portapack.hpp"
|
||||
using namespace portapack;
|
||||
|
||||
#include "string_format.hpp"
|
||||
|
||||
#include "utility.hpp"
|
||||
|
||||
namespace tpms {
|
||||
|
||||
namespace format {
|
||||
|
||||
static bool use_kpa = true;
|
||||
|
||||
std::string type(Reading::Type type) {
|
||||
return to_string_dec_uint(toUType(type), 2);
|
||||
}
|
||||
|
||||
std::string id(TransponderID id) {
|
||||
return to_string_hex(id.value(), 8);
|
||||
}
|
||||
|
||||
std::string pressure(Pressure pressure) {
|
||||
if(use_kpa){
|
||||
return to_string_dec_int(pressure.kilopascal(), 3);
|
||||
}
|
||||
return to_string_dec_int(pressure.psi(), 3);
|
||||
|
||||
}
|
||||
|
||||
std::string temperature(Temperature temperature) {
|
||||
return to_string_dec_int(temperature.celsius(), 3);
|
||||
}
|
||||
|
||||
std::string flags(Flags flags) {
|
||||
return to_string_hex(flags, 2);
|
||||
}
|
||||
|
||||
static std::string signal_type(SignalType signal_type) {
|
||||
switch(signal_type) {
|
||||
case SignalType::FSK_19k2_Schrader: return "FSK 38400 19200 Schrader";
|
||||
case SignalType::OOK_8k192_Schrader: return "OOK - 8192 Schrader";
|
||||
case SignalType::OOK_8k4_Schrader: return "OOK - 8400 Schrader";
|
||||
default: return "- - - -";
|
||||
}
|
||||
}
|
||||
|
||||
} /* namespace format */
|
||||
|
||||
} /* namespace tpms */
|
||||
|
||||
void TPMSLogger::on_packet(const tpms::Packet& packet, const uint32_t target_frequency) {
|
||||
const auto hex_formatted = packet.symbols_formatted();
|
||||
|
||||
// TODO: function doesn't take uint64_t, so when >= 1<<32, weirdness will ensue!
|
||||
const auto tuning_frequency_str = to_string_dec_uint(target_frequency, 10);
|
||||
|
||||
std::string entry = tuning_frequency_str + " " + tpms::format::signal_type(packet.signal_type()) + " " + hex_formatted.data + "/" + hex_formatted.errors;
|
||||
log_file.write_entry(packet.received_at(), entry);
|
||||
}
|
||||
|
||||
const TPMSRecentEntry::Key TPMSRecentEntry::invalid_key = { tpms::Reading::Type::None, 0 };
|
||||
|
||||
void TPMSRecentEntry::update(const tpms::Reading& reading) {
|
||||
received_count++;
|
||||
|
||||
if( reading.pressure().is_valid() ) {
|
||||
last_pressure = reading.pressure();
|
||||
}
|
||||
if( reading.temperature().is_valid() ) {
|
||||
last_temperature = reading.temperature();
|
||||
}
|
||||
if( reading.flags().is_valid() ) {
|
||||
last_flags = reading.flags();
|
||||
}
|
||||
}
|
||||
|
||||
namespace ui {
|
||||
|
||||
template<>
|
||||
void RecentEntriesTable<TPMSRecentEntries>::draw(
|
||||
const Entry& entry,
|
||||
const Rect& target_rect,
|
||||
Painter& painter,
|
||||
const Style& style
|
||||
) {
|
||||
std::string line = tpms::format::type(entry.type) + " " + tpms::format::id(entry.id);
|
||||
|
||||
if( entry.last_pressure.is_valid() ) {
|
||||
line += " " + tpms::format::pressure(entry.last_pressure.value());
|
||||
} else {
|
||||
line += " " " ";
|
||||
}
|
||||
|
||||
if( entry.last_temperature.is_valid() ) {
|
||||
line += " " + tpms::format::temperature(entry.last_temperature.value());
|
||||
} else {
|
||||
line += " " " ";
|
||||
}
|
||||
|
||||
if( entry.received_count > 999 ) {
|
||||
line += " +++";
|
||||
} else {
|
||||
line += " " + to_string_dec_uint(entry.received_count, 3);
|
||||
}
|
||||
|
||||
if( entry.last_flags.is_valid() ) {
|
||||
line += " " + tpms::format::flags(entry.last_flags.value());
|
||||
} else {
|
||||
line += " " " ";
|
||||
}
|
||||
|
||||
line.resize(target_rect.width() / 8, ' ');
|
||||
painter.draw_string(target_rect.location(), style, line);
|
||||
}
|
||||
|
||||
TPMSAppView::TPMSAppView(NavigationView&) {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_tpms);
|
||||
|
||||
add_children({
|
||||
&rssi,
|
||||
&channel,
|
||||
&options_band,
|
||||
&field_rf_amp,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&options_type,
|
||||
});
|
||||
|
||||
// load app settings
|
||||
auto rc = settings.load("rx_tpms", &app_settings);
|
||||
if(rc == SETTINGS_OK) {
|
||||
field_lna.set_value(app_settings.lna);
|
||||
field_vga.set_value(app_settings.vga);
|
||||
field_rf_amp.set_value(app_settings.rx_amp);
|
||||
options_band.set_by_value(app_settings.rx_frequency);
|
||||
}
|
||||
else options_band.set_by_value(receiver_model.tuning_frequency());
|
||||
|
||||
receiver_model.set_tuning_frequency(tuning_frequency());
|
||||
receiver_model.set_sampling_rate(sampling_rate);
|
||||
receiver_model.set_baseband_bandwidth(baseband_bandwidth);
|
||||
receiver_model.enable(); // Before using radio::enable(), but not updating Ant.DC-Bias.
|
||||
|
||||
/* radio::enable({
|
||||
tuning_frequency(),
|
||||
sampling_rate,
|
||||
baseband_bandwidth,
|
||||
rf::Direction::Receive,
|
||||
receiver_model.rf_amp(),
|
||||
static_cast<int8_t>(receiver_model.lna()),
|
||||
static_cast<int8_t>(receiver_model.vga()),
|
||||
}); */
|
||||
|
||||
options_band.on_change = [this](size_t, OptionsField::value_t v) {
|
||||
this->on_band_changed(v);
|
||||
};
|
||||
options_band.set_by_value(target_frequency());
|
||||
|
||||
options_type.on_change = [this](size_t, int32_t i) {
|
||||
if (i == 0){
|
||||
tpms::format::use_kpa = true;
|
||||
} else if (i == 1){
|
||||
tpms::format::use_kpa = false;
|
||||
}
|
||||
update_type();
|
||||
};
|
||||
|
||||
options_type.set_selected_index(0, true);
|
||||
|
||||
logger = std::make_unique<TPMSLogger>();
|
||||
if( logger ) {
|
||||
logger->append(u"tpms.txt");
|
||||
}
|
||||
}
|
||||
|
||||
TPMSAppView::~TPMSAppView() {
|
||||
|
||||
|
||||
// save app settings
|
||||
app_settings.rx_frequency = target_frequency_;
|
||||
settings.save("rx_tpms", &app_settings);
|
||||
|
||||
receiver_model.disable(); // to switch off all, including DC bias and change flag enabled_
|
||||
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
void TPMSAppView::focus() {
|
||||
options_band.focus();
|
||||
}
|
||||
|
||||
void TPMSAppView::update_type() {
|
||||
if (tpms::format::use_kpa){
|
||||
remove_child(&recent_entries_view_psi);
|
||||
add_child(&recent_entries_view_kpa);
|
||||
recent_entries_view_kpa.set_parent_rect(view_normal_rect);
|
||||
} else {
|
||||
remove_child(&recent_entries_view_kpa);
|
||||
add_child(&recent_entries_view_psi);
|
||||
recent_entries_view_psi.set_parent_rect(view_normal_rect);
|
||||
}
|
||||
}
|
||||
|
||||
void TPMSAppView::set_parent_rect(const Rect new_parent_rect) {
|
||||
View::set_parent_rect(new_parent_rect);
|
||||
|
||||
view_normal_rect = { 0, header_height, new_parent_rect.width(), new_parent_rect.height() - header_height };
|
||||
|
||||
update_type();
|
||||
}
|
||||
|
||||
void TPMSAppView::on_packet(const tpms::Packet& packet) {
|
||||
if( logger ) {
|
||||
logger->on_packet(packet, target_frequency());
|
||||
}
|
||||
|
||||
const auto reading_opt = packet.reading();
|
||||
if( reading_opt.is_valid() ) {
|
||||
const auto reading = reading_opt.value();
|
||||
auto& entry = ::on_packet(recent, TPMSRecentEntry::Key { reading.type(), reading.id() });
|
||||
entry.update(reading);
|
||||
|
||||
if(tpms::format::use_kpa){
|
||||
recent_entries_view_kpa.set_dirty();
|
||||
} else {
|
||||
recent_entries_view_psi.set_dirty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TPMSAppView::on_show_list() {
|
||||
if(tpms::format::use_kpa){
|
||||
recent_entries_view_kpa.hidden(false);
|
||||
recent_entries_view_kpa.focus();
|
||||
} else {
|
||||
recent_entries_view_psi.hidden(false);
|
||||
recent_entries_view_psi.focus();
|
||||
}
|
||||
}
|
||||
|
||||
void TPMSAppView::on_band_changed(const uint32_t new_band_frequency) {
|
||||
set_target_frequency(new_band_frequency);
|
||||
}
|
||||
|
||||
void TPMSAppView::set_target_frequency(const uint32_t new_value) {
|
||||
target_frequency_ = new_value;
|
||||
radio::set_tuning_frequency(tuning_frequency());
|
||||
}
|
||||
|
||||
uint32_t TPMSAppView::target_frequency() const {
|
||||
return target_frequency_;
|
||||
}
|
||||
|
||||
uint32_t TPMSAppView::tuning_frequency() const {
|
||||
return target_frequency() - (sampling_rate / 4);
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
@@ -1,207 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __TPMS_APP_H__
|
||||
#define __TPMS_APP_H__
|
||||
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_rssi.hpp"
|
||||
#include "ui_channel.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "event_m0.hpp"
|
||||
|
||||
#include "log_file.hpp"
|
||||
|
||||
#include "recent_entries.hpp"
|
||||
|
||||
#include "tpms_packet.hpp"
|
||||
|
||||
namespace std {
|
||||
|
||||
constexpr bool operator==(const tpms::TransponderID& lhs, const tpms::TransponderID& rhs) {
|
||||
return (lhs.value() == rhs.value());
|
||||
}
|
||||
|
||||
} /* namespace std */
|
||||
|
||||
struct TPMSRecentEntry {
|
||||
using Key = std::pair<tpms::Reading::Type, tpms::TransponderID>;
|
||||
|
||||
static const Key invalid_key;
|
||||
|
||||
tpms::Reading::Type type { invalid_key.first };
|
||||
tpms::TransponderID id { invalid_key.second };
|
||||
|
||||
size_t received_count { 0 };
|
||||
|
||||
Optional<Pressure> last_pressure { };
|
||||
Optional<Temperature> last_temperature { };
|
||||
Optional<tpms::Flags> last_flags { };
|
||||
|
||||
TPMSRecentEntry(
|
||||
const Key& key
|
||||
) : type { key.first },
|
||||
id { key.second }
|
||||
{
|
||||
}
|
||||
|
||||
Key key() const {
|
||||
return { type, id };
|
||||
}
|
||||
|
||||
void update(const tpms::Reading& reading);
|
||||
};
|
||||
|
||||
using TPMSRecentEntries = RecentEntries<TPMSRecentEntry>;
|
||||
|
||||
class TPMSLogger {
|
||||
public:
|
||||
Optional<File::Error> append(const std::filesystem::path& filename) {
|
||||
return log_file.append(filename);
|
||||
}
|
||||
|
||||
void on_packet(const tpms::Packet& packet, const uint32_t target_frequency);
|
||||
|
||||
private:
|
||||
LogFile log_file { };
|
||||
};
|
||||
|
||||
namespace ui {
|
||||
|
||||
using TPMSRecentEntriesView = RecentEntriesView<TPMSRecentEntries>;
|
||||
|
||||
class TPMSAppView : public View {
|
||||
public:
|
||||
TPMSAppView(NavigationView& nav);
|
||||
~TPMSAppView();
|
||||
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
|
||||
// Prevent painting of region covered entirely by a child.
|
||||
// TODO: Add flag to View that specifies view does not need to be cleared before painting.
|
||||
void paint(Painter&) override { };
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "TPMS RX"; };
|
||||
|
||||
private:
|
||||
static constexpr uint32_t initial_target_frequency = 315000000;
|
||||
static constexpr uint32_t sampling_rate = 2457600;
|
||||
static constexpr uint32_t baseband_bandwidth = 1750000;
|
||||
|
||||
// app save settings
|
||||
std::app_settings settings { };
|
||||
std::app_settings::AppSettings app_settings { };
|
||||
|
||||
MessageHandlerRegistration message_handler_packet {
|
||||
Message::ID::TPMSPacket,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const TPMSPacketMessage*>(p);
|
||||
const tpms::Packet packet { message->packet, message->signal_type };
|
||||
this->on_packet(packet);
|
||||
}
|
||||
};
|
||||
|
||||
static constexpr ui::Dim header_height = 1 * 16;
|
||||
|
||||
ui::Rect view_normal_rect { };
|
||||
|
||||
RSSI rssi {
|
||||
{ 21 * 8, 0, 6 * 8, 4 },
|
||||
};
|
||||
|
||||
Channel channel {
|
||||
{ 21 * 8, 5, 6 * 8, 4 },
|
||||
};
|
||||
|
||||
OptionsField options_band {
|
||||
{ 0 * 8, 0 * 16 },
|
||||
3,
|
||||
{
|
||||
{ "315", 315000000 },
|
||||
{ "433", 433920000 },
|
||||
}
|
||||
};
|
||||
|
||||
OptionsField options_type {
|
||||
{ 5 * 8, 0 * 16 },
|
||||
3,
|
||||
{
|
||||
{ "kPa", 0 },
|
||||
{ "PSI", 1 }
|
||||
}
|
||||
};
|
||||
|
||||
RFAmpField field_rf_amp {
|
||||
{ 13 * 8, 0 * 16 }
|
||||
};
|
||||
|
||||
LNAGainField field_lna {
|
||||
{ 15 * 8, 0 * 16 }
|
||||
};
|
||||
|
||||
VGAGainField field_vga {
|
||||
{ 18 * 8, 0 * 16 }
|
||||
};
|
||||
|
||||
TPMSRecentEntries recent { };
|
||||
std::unique_ptr<TPMSLogger> logger { };
|
||||
|
||||
const RecentEntriesColumns columns_kpa { {
|
||||
{ "Tp", 2 },
|
||||
{ "ID", 8 },
|
||||
{ "kPa", 3 },
|
||||
{ "C", 3 },
|
||||
{ "Cnt", 3 },
|
||||
{ "Fl", 2 },
|
||||
} };
|
||||
TPMSRecentEntriesView recent_entries_view_kpa { columns_kpa, recent };
|
||||
|
||||
const RecentEntriesColumns columns_psi { {
|
||||
{ "Tp", 2 },
|
||||
{ "ID", 8 },
|
||||
{ "PSI", 3 },
|
||||
{ "C", 3 },
|
||||
{ "Cnt", 3 },
|
||||
{ "Fl", 2 },
|
||||
} };
|
||||
TPMSRecentEntriesView recent_entries_view_psi { columns_psi, recent };
|
||||
|
||||
uint32_t target_frequency_ = initial_target_frequency;
|
||||
|
||||
void on_packet(const tpms::Packet& packet);
|
||||
void on_show_list();
|
||||
void update_type();
|
||||
|
||||
void on_band_changed(const uint32_t new_band_frequency);
|
||||
|
||||
uint32_t target_frequency() const;
|
||||
void set_target_frequency(const uint32_t new_value);
|
||||
|
||||
uint32_t tuning_frequency() const;
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
#endif/*__TPMS_APP_H__*/
|
||||
@@ -40,106 +40,99 @@ namespace ui {
|
||||
|
||||
// This is pretty much WaterfallView but in the opposite direction
|
||||
CreditsWidget::CreditsWidget(
|
||||
Rect parent_rect
|
||||
) : Widget { parent_rect }
|
||||
{
|
||||
Rect parent_rect)
|
||||
: Widget{parent_rect} {
|
||||
}
|
||||
|
||||
void CreditsWidget::paint(Painter&) {
|
||||
}
|
||||
|
||||
void CreditsWidget::on_show() {
|
||||
clear();
|
||||
clear();
|
||||
|
||||
const auto screen_r = screen_rect();
|
||||
display.scroll_set_area(screen_r.top(), screen_r.bottom());
|
||||
const auto screen_r = screen_rect();
|
||||
display.scroll_set_area(screen_r.top(), screen_r.bottom());
|
||||
}
|
||||
|
||||
void CreditsWidget::on_hide() {
|
||||
display.scroll_disable();
|
||||
display.scroll_disable();
|
||||
}
|
||||
|
||||
void CreditsWidget::new_row(
|
||||
const std::array<Color, 240>& pixel_row
|
||||
) {
|
||||
// Glitch be here (see comment in main.cpp)
|
||||
const auto draw_y = display.scroll(-1);
|
||||
|
||||
display.draw_pixels(
|
||||
{ { 0, draw_y - 1 }, { 240, 1 } },
|
||||
pixel_row
|
||||
);
|
||||
const std::array<Color, 240>& pixel_row) {
|
||||
// Glitch be here (see comment in main.cpp)
|
||||
const auto draw_y = display.scroll(-1);
|
||||
|
||||
display.draw_pixels(
|
||||
{{0, draw_y - 1}, {240, 1}},
|
||||
pixel_row);
|
||||
}
|
||||
|
||||
void CreditsWidget::clear() {
|
||||
display.fill_rectangle(
|
||||
screen_rect(),
|
||||
Color::black()
|
||||
);
|
||||
display.fill_rectangle(
|
||||
screen_rect(),
|
||||
Theme::getInstance()->bg_darkest->background);
|
||||
}
|
||||
|
||||
void AboutView::update() {
|
||||
size_t i = 0;
|
||||
std::array<Color, 240> pixel_row;
|
||||
|
||||
slow_down++;
|
||||
if (slow_down % 3 < 2) return;
|
||||
|
||||
if (!timer) {
|
||||
if (loop) {
|
||||
credits_index = 0;
|
||||
loop = false;
|
||||
}
|
||||
|
||||
text = credits[credits_index].text;
|
||||
timer = credits[credits_index].delay;
|
||||
start_pos = credits[credits_index].start_pos;
|
||||
|
||||
if (timer < 0) {
|
||||
timer = 240;
|
||||
loop = true;
|
||||
} else
|
||||
timer += 16;
|
||||
|
||||
render_line = 0;
|
||||
credits_index++;
|
||||
} else
|
||||
timer--;
|
||||
|
||||
if (render_line < 16) {
|
||||
for (const auto c : text) {
|
||||
const auto glyph = style().font.glyph(c);
|
||||
|
||||
const size_t start = (glyph.size().width() / 8) * render_line;
|
||||
for (Dim c = 0; c < glyph.size().width(); c++) {
|
||||
const auto pixel = glyph.pixels()[start + (c >> 3)] & (1U << (c & 0x7));
|
||||
pixel_row[start_pos + i + c] = pixel ? Color::white() : Color::black();
|
||||
}
|
||||
|
||||
const auto advance = glyph.advance();
|
||||
i += advance.x();
|
||||
}
|
||||
render_line++;
|
||||
}
|
||||
|
||||
credits_display.new_row(pixel_row);
|
||||
size_t i = 0;
|
||||
std::array<Color, 240> pixel_row;
|
||||
|
||||
slow_down++;
|
||||
if (slow_down % 3 < 2) return;
|
||||
|
||||
if (!timer) {
|
||||
if (loop) {
|
||||
credits_index = 0;
|
||||
loop = false;
|
||||
}
|
||||
|
||||
text = credits[credits_index].text;
|
||||
timer = credits[credits_index].delay;
|
||||
start_pos = credits[credits_index].start_pos;
|
||||
|
||||
if (timer < 0) {
|
||||
timer = 240;
|
||||
loop = true;
|
||||
} else
|
||||
timer += 16;
|
||||
|
||||
render_line = 0;
|
||||
credits_index++;
|
||||
} else
|
||||
timer--;
|
||||
|
||||
if (render_line < 16) {
|
||||
for (const auto c : text) {
|
||||
const auto glyph = style().font.glyph(c);
|
||||
|
||||
const size_t start = (glyph.size().width() / 8) * render_line;
|
||||
for (Dim c = 0; c < glyph.size().width(); c++) {
|
||||
const auto pixel = glyph.pixels()[start + (c >> 3)] & (1U << (c & 0x7));
|
||||
pixel_row[start_pos + i + c] = pixel ? Theme::getInstance()->bg_darkest->foreground : Theme::getInstance()->bg_darkest->background;
|
||||
}
|
||||
|
||||
const auto advance = glyph.advance();
|
||||
i += advance.x();
|
||||
}
|
||||
render_line++;
|
||||
}
|
||||
|
||||
credits_display.new_row(pixel_row);
|
||||
}
|
||||
|
||||
AboutView::AboutView(
|
||||
NavigationView& nav
|
||||
) {
|
||||
add_children({
|
||||
&credits_display,
|
||||
&button_ok
|
||||
});
|
||||
NavigationView& nav) {
|
||||
add_children({&credits_display,
|
||||
&button_ok});
|
||||
|
||||
button_ok.on_select = [&nav](Button&){
|
||||
nav.pop();
|
||||
};
|
||||
button_ok.on_select = [&nav](Button&) {
|
||||
nav.pop();
|
||||
};
|
||||
}
|
||||
|
||||
void AboutView::focus() {
|
||||
button_ok.focus();
|
||||
button_ok.focus();
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -25,101 +25,95 @@
|
||||
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_font_fixed_8x16.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace ui {
|
||||
|
||||
class CreditsWidget : public Widget {
|
||||
public:
|
||||
CreditsWidget(Rect parent_rect);
|
||||
|
||||
void on_show() override;
|
||||
void on_hide() override;
|
||||
public:
|
||||
CreditsWidget(Rect parent_rect);
|
||||
|
||||
void paint(Painter&) override;
|
||||
|
||||
void new_row(const std::array<Color, 240>& pixel_row);
|
||||
void on_show() override;
|
||||
void on_hide() override;
|
||||
|
||||
private:
|
||||
void clear();
|
||||
void paint(Painter&) override;
|
||||
|
||||
void new_row(const std::array<Color, 240>& pixel_row);
|
||||
|
||||
private:
|
||||
void clear();
|
||||
};
|
||||
|
||||
class AboutView : public View {
|
||||
public:
|
||||
AboutView(NavigationView& nav);
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "About"; };
|
||||
public:
|
||||
AboutView(NavigationView& nav);
|
||||
|
||||
private:
|
||||
void update();
|
||||
void focus() override;
|
||||
|
||||
uint8_t credits_index { 0 };
|
||||
uint8_t render_line { 0 };
|
||||
Coord start_pos { 0 };
|
||||
uint8_t slow_down { 0 };
|
||||
int32_t timer { 0 };
|
||||
bool loop { false };
|
||||
|
||||
std::string text { };
|
||||
|
||||
typedef struct credits_t {
|
||||
size_t start_pos;
|
||||
std::string text;
|
||||
int32_t delay;
|
||||
} credits_t;
|
||||
|
||||
// TODO: Make this dinamically centered and parse \n as the delay value so it is easy to maintain
|
||||
const credits_t credits[26] = {
|
||||
// 012345678901234567890123456789
|
||||
{ 60, "PortaPack Mayhem", 0 },
|
||||
{ 60, "PortaPack|HAVOC", 0 },
|
||||
{ 11 * 8, "Gurus J. Boone", 0 },
|
||||
{ 18 * 8, "M. Ossmann", 16 },
|
||||
{ 11 * 8, "HAVOC Furrtek", 16 },
|
||||
{ 7 * 8, "POCSAG rx T. Sailer", 0 },
|
||||
{ 18 * 8, "E. Oenal", 16 },
|
||||
{ 0 * 8, "Radiosonde infos F4GMU", 0 },
|
||||
{ 18 * 8, "RS1729", 16 },
|
||||
{ 4 * 8, "RDS waveform C. Jacquet", 16 },
|
||||
{ 7 * 8, "Xy. infos cLx", 16 },
|
||||
{ 2 * 8, "OOK scan trick Samy Kamkar", 16 },
|
||||
{ 7 * 8, "World map NASA", 16 },
|
||||
{ 0 * 8, "TouchTunes infos Notpike", 16 },
|
||||
{ 4 * 8, "Subaru infos Tom", 0 },
|
||||
{ 18 * 8, "Wimmenhove", 16 },
|
||||
{ 1 * 8, "GPS,TV,BTLE,NRF Shao", 24 },
|
||||
{ 6 * 8, "Thanks & donators", 16 },
|
||||
{ 1 * 8, "Rainer Matla Keld Norman", 0 },
|
||||
{ 1 * 8, " Giorgio C. DC1RDB", 0 },
|
||||
{ 1 * 8, " Sigmounte Waax", 0 },
|
||||
{ 1 * 8, " Windyoona Channels", 0 },
|
||||
{ 1 * 8, " F4GEV Pyr3x", 0 },
|
||||
{ 1 * 8, " HB3YOE", 24 },
|
||||
{ 11 * 8, "MMXVIII", -1 }
|
||||
};
|
||||
|
||||
CreditsWidget credits_display {
|
||||
{ 0, 16, 240, 240 }
|
||||
};
|
||||
std::string title() const override { return "About"; };
|
||||
|
||||
Button button_ok {
|
||||
{ 72, 272, 96, 24 },
|
||||
"OK"
|
||||
};
|
||||
|
||||
MessageHandlerRegistration message_handler_update {
|
||||
Message::ID::DisplayFrameSync,
|
||||
[this](const Message* const) {
|
||||
this->update();
|
||||
}
|
||||
};
|
||||
|
||||
private:
|
||||
void update();
|
||||
|
||||
uint8_t credits_index{0};
|
||||
uint8_t render_line{0};
|
||||
Coord start_pos{0};
|
||||
uint8_t slow_down{0};
|
||||
int32_t timer{0};
|
||||
bool loop{false};
|
||||
|
||||
std::string text{};
|
||||
|
||||
typedef struct credits_t {
|
||||
size_t start_pos;
|
||||
std::string text;
|
||||
int32_t delay;
|
||||
} credits_t;
|
||||
|
||||
// TODO: Make this dinamically centered and parse \n as the delay value so it is easy to maintain
|
||||
const credits_t credits[26] = {
|
||||
// 012345678901234567890123456789
|
||||
{60, "PortaPack Mayhem", 0},
|
||||
{60, "PortaPack|HAVOC", 0},
|
||||
{11 * 8, "Gurus J. Boone", 0},
|
||||
{18 * 8, "M. Ossmann", 16},
|
||||
{11 * 8, "HAVOC Furrtek", 16},
|
||||
{7 * 8, "POCSAG rx T. Sailer", 0},
|
||||
{18 * 8, "E. Oenal", 16},
|
||||
{0 * 8, "Radiosonde infos F4GMU", 0},
|
||||
{18 * 8, "RS1729", 16},
|
||||
{4 * 8, "RDS waveform C. Jacquet", 16},
|
||||
{7 * 8, "Xy. infos cLx", 16},
|
||||
{2 * 8, "OOK scan trick Samy Kamkar", 16},
|
||||
{7 * 8, "World map NASA", 16},
|
||||
{0 * 8, "TouchTunes infos Notpike", 16},
|
||||
{4 * 8, "Subaru infos Tom", 0},
|
||||
{18 * 8, "Wimmenhove", 16},
|
||||
{1 * 8, "GPS,TV,BTLE,NRF Shao", 24},
|
||||
{6 * 8, "Thanks & donators", 16},
|
||||
{1 * 8, "Rainer Matla Keld Norman", 0},
|
||||
{1 * 8, " Giorgio C. DC1RDB", 0},
|
||||
{1 * 8, " Sigmounte Waax", 0},
|
||||
{1 * 8, " Windyoona Channels", 0},
|
||||
{1 * 8, " F4GEV Pyr3x", 0},
|
||||
{1 * 8, " HB3YOE", 24},
|
||||
{11 * 8, "MMXVIII", -1}};
|
||||
|
||||
CreditsWidget credits_display{
|
||||
{0, 16, 240, 240}};
|
||||
|
||||
Button button_ok{
|
||||
{72, 272, 96, 24},
|
||||
"OK"};
|
||||
|
||||
MessageHandlerRegistration message_handler_update{
|
||||
Message::ID::DisplayFrameSync,
|
||||
[this](const Message* const) {
|
||||
this->update();
|
||||
}};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
#endif/*__UI_ABOUT_H__*/
|
||||
#endif /*__UI_ABOUT_H__*/
|
||||
|
||||
@@ -45,374 +45,369 @@ using namespace lpc43xx;
|
||||
using namespace portapack;
|
||||
|
||||
namespace ui {
|
||||
|
||||
|
||||
void AboutView::on_show() {
|
||||
transmitter_model.set_tuning_frequency(1337000000); // TODO: Change
|
||||
transmitter_model.set_baseband_configuration({
|
||||
.mode = 0,
|
||||
.sampling_rate = 1536000,
|
||||
.decimation_factor = 1,
|
||||
});
|
||||
transmitter_model.set_rf_amp(true);
|
||||
transmitter_model.set_lna(40);
|
||||
transmitter_model.set_vga(40);
|
||||
transmitter_model.set_baseband_bandwidth(1750000);
|
||||
transmitter_model.enable();
|
||||
|
||||
baseband::set_audiotx_data(32, 50, false, 0);
|
||||
|
||||
//audio::headphone::set_volume(volume_t::decibel(0 - 99) + audio::headphone::volume_range().max);
|
||||
transmitter_model.set_target_frequency(1337000000); // TODO: Change
|
||||
transmitter_model.set_baseband_configuration({
|
||||
.mode = 0,
|
||||
.sampling_rate = 1536000,
|
||||
.decimation_factor = 1,
|
||||
});
|
||||
transmitter_model.set_rf_amp(true);
|
||||
transmitter_model.set_lna(40);
|
||||
transmitter_model.set_vga(40);
|
||||
transmitter_model.enable();
|
||||
|
||||
baseband::set_audiotx_data(32, 50, false, 0);
|
||||
|
||||
// audio::headphone::set_volume(volume_t::decibel(0 - 99) + audio::headphone::volume_range().max);
|
||||
}
|
||||
|
||||
void AboutView::render_video() {
|
||||
uint8_t p, r, luma, chroma, cy;
|
||||
ui::Color cc;
|
||||
char ch;
|
||||
float s;
|
||||
|
||||
// Send framebuffer to LCD. Gotta go fast !
|
||||
display.render_box({30, 112}, {180, 72}, framebuffer);
|
||||
uint8_t p, r, luma, chroma, cy;
|
||||
ui::Color cc;
|
||||
char ch;
|
||||
float s;
|
||||
|
||||
// Clear framebuffer to black
|
||||
memset(framebuffer, 0, 180 * 72 * sizeof(ui::Color));
|
||||
// Send framebuffer to LCD. Gotta go fast !
|
||||
display.render_box({30, 112}, {180, 72}, framebuffer);
|
||||
|
||||
// Drum hit palette animation
|
||||
if (drum > 1) drum--;
|
||||
// Clear framebuffer to black
|
||||
memset(framebuffer, 0, 180 * 72 * sizeof(ui::Color));
|
||||
|
||||
// Render copper bars from Y buffer
|
||||
for (p = 0; p < 72; p++) {
|
||||
luma = copperbuffer[p] & 0x0F; // 0 is transparent
|
||||
if (luma) {
|
||||
chroma = copperbuffer[p]>>4;
|
||||
cc = ui::Color(std::min((coppercolor[chroma][0]/luma)*drum,255), std::min((coppercolor[chroma][1]/luma)*drum,255), std::min((coppercolor[chroma][2]/luma)*drum,255));
|
||||
for (r = 0; r < 180; r++)
|
||||
framebuffer[(p*180)+r] = cc;
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll in/out state machine
|
||||
if (anim_state == 0) {
|
||||
// Scroll in
|
||||
if (ofy < 8) {
|
||||
ofy++;
|
||||
anim_state = 0;
|
||||
} else {
|
||||
anim_state = 1;
|
||||
}
|
||||
if (ofx < (int16_t)(180 - (strlen(credits[credits_index].name) * 16) - 8)) {
|
||||
ofx += 8;
|
||||
anim_state = 0;
|
||||
}
|
||||
} else if (anim_state == 1) {
|
||||
// Just wait
|
||||
if (credits_timer == (30*3)) {
|
||||
credits_timer = 0;
|
||||
anim_state = 2;
|
||||
} else {
|
||||
credits_timer++;
|
||||
}
|
||||
} else {
|
||||
// Scroll out
|
||||
if (credits[credits_index].change == true) {
|
||||
if (ofy > -24) {
|
||||
ofy--;
|
||||
anim_state = 2;
|
||||
} else {
|
||||
anim_state = 0;
|
||||
}
|
||||
} else {
|
||||
anim_state = 0;
|
||||
}
|
||||
if (ofx < 180) {
|
||||
ofx += 8;
|
||||
anim_state = 2;
|
||||
}
|
||||
|
||||
// Switch to next text
|
||||
if (anim_state == 0) {
|
||||
if (credits_index == 9)
|
||||
credits_index = 0;
|
||||
else
|
||||
credits_index++;
|
||||
ofx = -(strlen(credits[credits_index].name) * 16) - 16;
|
||||
}
|
||||
}
|
||||
|
||||
// Sine text ("role")
|
||||
p = 0;
|
||||
while ((ch = credits[credits_index].role[p])) {
|
||||
draw_demoglyph({(ui::Coord)(8+(p*16)), (ui::Coord)(ofy+(sine_table_f32[((p*16)+(phase>>5))&0xFF] * 8))}, ch, paletteA);
|
||||
p++;
|
||||
}
|
||||
|
||||
// Scroll text (name)
|
||||
p = 0;
|
||||
while ((ch = credits[credits_index].name[p])) {
|
||||
draw_demoglyph({(ui::Coord)(ofx+(p*16)), 56}, ch, paletteB);
|
||||
p++;
|
||||
}
|
||||
|
||||
// Clear bars Y buffer
|
||||
memset(copperbuffer, 0, 72);
|
||||
|
||||
// Render bars to Y buffer
|
||||
for (p = 0; p < 5; p++) {
|
||||
cy = copperbars[p];
|
||||
for (r = 0; r < 16; r++)
|
||||
copperbuffer[cy+r] = copperluma[r] + (p<<4);
|
||||
}
|
||||
|
||||
// Animate bars positions
|
||||
for (p = 0; p < 5; p++) {
|
||||
s = sine_table_f32[((p*32)+(phase/24))&0xFF];
|
||||
s += sine_table_f32[((p*16)+(phase/35))&0xFF];
|
||||
copperbars[p] = 28+(uint8_t)(s * 14);
|
||||
}
|
||||
|
||||
phase += 128;
|
||||
// Drum hit palette animation
|
||||
if (drum > 1) drum--;
|
||||
|
||||
// Render copper bars from Y buffer
|
||||
for (p = 0; p < 72; p++) {
|
||||
luma = copperbuffer[p] & 0x0F; // 0 is transparent
|
||||
if (luma) {
|
||||
chroma = copperbuffer[p] >> 4;
|
||||
cc = ui::Color(std::min((coppercolor[chroma][0] / luma) * drum, 255), std::min((coppercolor[chroma][1] / luma) * drum, 255), std::min((coppercolor[chroma][2] / luma) * drum, 255));
|
||||
for (r = 0; r < 180; r++)
|
||||
framebuffer[(p * 180) + r] = cc;
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll in/out state machine
|
||||
if (anim_state == 0) {
|
||||
// Scroll in
|
||||
if (ofy < 8) {
|
||||
ofy++;
|
||||
anim_state = 0;
|
||||
} else {
|
||||
anim_state = 1;
|
||||
}
|
||||
if (ofx < (int16_t)(180 - (strlen(credits[credits_index].name) * 16) - 8)) {
|
||||
ofx += 8;
|
||||
anim_state = 0;
|
||||
}
|
||||
} else if (anim_state == 1) {
|
||||
// Just wait
|
||||
if (credits_timer == (30 * 3)) {
|
||||
credits_timer = 0;
|
||||
anim_state = 2;
|
||||
} else {
|
||||
credits_timer++;
|
||||
}
|
||||
} else {
|
||||
// Scroll out
|
||||
if (credits[credits_index].change == true) {
|
||||
if (ofy > -24) {
|
||||
ofy--;
|
||||
anim_state = 2;
|
||||
} else {
|
||||
anim_state = 0;
|
||||
}
|
||||
} else {
|
||||
anim_state = 0;
|
||||
}
|
||||
if (ofx < 180) {
|
||||
ofx += 8;
|
||||
anim_state = 2;
|
||||
}
|
||||
|
||||
// Switch to next text
|
||||
if (anim_state == 0) {
|
||||
if (credits_index == 9)
|
||||
credits_index = 0;
|
||||
else
|
||||
credits_index++;
|
||||
ofx = -(strlen(credits[credits_index].name) * 16) - 16;
|
||||
}
|
||||
}
|
||||
|
||||
// Sine text ("role")
|
||||
p = 0;
|
||||
while ((ch = credits[credits_index].role[p])) {
|
||||
draw_demoglyph({(ui::Coord)(8 + (p * 16)), (ui::Coord)(ofy + (sine_table_f32[((p * 16) + (phase >> 5)) & 0xFF] * 8))}, ch, paletteA);
|
||||
p++;
|
||||
}
|
||||
|
||||
// Scroll text (name)
|
||||
p = 0;
|
||||
while ((ch = credits[credits_index].name[p])) {
|
||||
draw_demoglyph({(ui::Coord)(ofx + (p * 16)), 56}, ch, paletteB);
|
||||
p++;
|
||||
}
|
||||
|
||||
// Clear bars Y buffer
|
||||
memset(copperbuffer, 0, 72);
|
||||
|
||||
// Render bars to Y buffer
|
||||
for (p = 0; p < 5; p++) {
|
||||
cy = copperbars[p];
|
||||
for (r = 0; r < 16; r++)
|
||||
copperbuffer[cy + r] = copperluma[r] + (p << 4);
|
||||
}
|
||||
|
||||
// Animate bars positions
|
||||
for (p = 0; p < 5; p++) {
|
||||
s = sine_table_f32[((p * 32) + (phase / 24)) & 0xFF];
|
||||
s += sine_table_f32[((p * 16) + (phase / 35)) & 0xFF];
|
||||
copperbars[p] = 28 + (uint8_t)(s * 14);
|
||||
}
|
||||
|
||||
phase += 128;
|
||||
}
|
||||
|
||||
void AboutView::draw_demoglyph(ui::Point p, char ch, ui::Color * pal) {
|
||||
uint8_t x, y, c, cl, cr;
|
||||
uint16_t che;
|
||||
int16_t lbx, il;
|
||||
void AboutView::draw_demoglyph(ui::Point p, char ch, ui::Color* pal) {
|
||||
uint8_t x, y, c, cl, cr;
|
||||
uint16_t che;
|
||||
int16_t lbx, il;
|
||||
|
||||
// Map ASCII to font bitmap
|
||||
if ((ch >= 32) || (ch < 96))
|
||||
che = char_map[ch-32];
|
||||
else
|
||||
che = 0xFF;
|
||||
|
||||
if (che < 0xFF) {
|
||||
che = (che * 128) + 48; // Start in bitmap
|
||||
// Map ASCII to font bitmap
|
||||
if ((ch >= 32) && (ch < 96))
|
||||
che = char_map[ch - 32];
|
||||
else
|
||||
che = 0xFF;
|
||||
|
||||
il = (180 * p.y) + p.x; // Start il framebuffer
|
||||
|
||||
for (y = 0; y < 16; y++) {
|
||||
if (p.y + y >= 72) break; // Over bottom of framebuffer, abort
|
||||
if (p.y + y >= 0) {
|
||||
for (x = 0; x < 8; x++) {
|
||||
c = demofont_bin[x+(y*8)+che]; // Split byte in 2 4BPP pixels
|
||||
cl = c >> 4;
|
||||
cr = c & 0x0F;
|
||||
lbx = p.x + (x*2);
|
||||
if (cl && (lbx < 180) && (lbx >= 0)) framebuffer[il] = pal[cl];
|
||||
lbx++;
|
||||
il++;
|
||||
if (cr && (lbx < 180) && (lbx >= 0)) framebuffer[il] = pal[cr];
|
||||
il++;
|
||||
}
|
||||
il += 180-16;
|
||||
} else {
|
||||
il += 180;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (che < 0xFF) {
|
||||
che = (che * 128) + 48; // Start in bitmap
|
||||
|
||||
il = (180 * p.y) + p.x; // Start il framebuffer
|
||||
|
||||
for (y = 0; y < 16; y++) {
|
||||
if (p.y + y >= 72) break; // Over bottom of framebuffer, abort
|
||||
if (p.y + y >= 0) {
|
||||
for (x = 0; x < 8; x++) {
|
||||
c = demofont_bin[x + (y * 8) + che]; // Split byte in 2 4BPP pixels
|
||||
cl = c >> 4;
|
||||
cr = c & 0x0F;
|
||||
lbx = p.x + (x * 2);
|
||||
if (cl && (lbx < 180) && (lbx >= 0)) framebuffer[il] = pal[cl];
|
||||
lbx++;
|
||||
il++;
|
||||
if (cr && (lbx < 180) && (lbx >= 0)) framebuffer[il] = pal[cr];
|
||||
il++;
|
||||
}
|
||||
il += 180 - 16;
|
||||
} else {
|
||||
il += 180;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AboutView::render_audio() {
|
||||
uint8_t i, ymdata;
|
||||
uint16_t ym_render_cnt;
|
||||
|
||||
// This is heavily inspired by MAME's ay8910.cpp and the YM2149's datasheet
|
||||
|
||||
// Render 1024 music samples
|
||||
for (ym_render_cnt = 0; ym_render_cnt < 1024; ym_render_cnt++) {
|
||||
uint8_t i, ymdata;
|
||||
uint16_t ym_render_cnt;
|
||||
|
||||
// Update registers at 48000/960 = 50Hz
|
||||
if (ym_sample_cnt == 0) {
|
||||
// "Decompress" on the fly and update YM registers
|
||||
for (i = 0; i < 14; i++) {
|
||||
if (!ym_regs[i].cnt) {
|
||||
// New run
|
||||
ymdata = ymdata_bin[ym_regs[i].ptr++];
|
||||
ym_regs[i].cnt = ymdata & 0x7F;
|
||||
if (ymdata & 0x80) {
|
||||
ym_regs[i].same = true;
|
||||
ym_regs[i].value = ymdata_bin[ym_regs[i].ptr++];
|
||||
} else {
|
||||
ym_regs[i].same = false;
|
||||
}
|
||||
// Detect drum on channel B
|
||||
if (i == 3)
|
||||
if (ym_regs[3].value > 2) drum = 4;
|
||||
}
|
||||
if (ym_regs[i].same == false) {
|
||||
ym_regs[i].value = ymdata_bin[ym_regs[i].ptr++];
|
||||
if (i == 13) {
|
||||
// Update envelope attributes
|
||||
ym_env_att = (ym_regs[13].value & 4) ? 0x1F : 0x00;
|
||||
if (!(ym_regs[13].value & 8)) {
|
||||
ym_env_hold = 1;
|
||||
ym_env_alt = ym_env_att;
|
||||
} else {
|
||||
ym_env_hold = ym_regs[13].value & 1;
|
||||
ym_env_alt = ym_regs[13].value & 2;
|
||||
}
|
||||
// Reset envelope counter
|
||||
ym_env_step = 0x1F;
|
||||
ym_env_holding = 0;
|
||||
ym_env_vol = (ym_env_step ^ ym_env_att);
|
||||
}
|
||||
}
|
||||
ym_regs[i].cnt--;
|
||||
}
|
||||
ym_frame++;
|
||||
}
|
||||
|
||||
// Square wave oscillators
|
||||
// 2457600/16/48000 = 3.2, but 4 sounds better than 3...
|
||||
for (i = 0; i < 3; i++) {
|
||||
ym_osc_cnt[i] += 4;
|
||||
if (ym_osc_cnt[i] >= (ym_regs[i*2].value | ((ym_regs[(i*2)+1].value & 0x0f) << 8))) {
|
||||
ym_osc_cnt[i] = 0;
|
||||
ym_osc_out[i] ^= 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Noise generator
|
||||
ym_noise_cnt += 4;
|
||||
if (ym_noise_cnt >= ((ym_regs[6].value & 0x1F) * 2)) {
|
||||
ym_noise_cnt = 0;
|
||||
ym_rng ^= (((ym_rng & 1) ^ ((ym_rng >> 3) & 1)) << 17);
|
||||
ym_rng >>= 1;
|
||||
}
|
||||
|
||||
// Mix tones and noise
|
||||
for (i = 0; i < 3; i++)
|
||||
ym_ch[i] = (ym_osc_out[i] | ((ym_regs[7].value >> i) & 1)) & ((ym_rng & 1) | ((ym_regs[7].value >> (i + 3)) & 1));
|
||||
|
||||
// Envelope generator
|
||||
if (!ym_env_holding) {
|
||||
ym_env_cnt += 8;
|
||||
if (ym_env_cnt >= (ym_regs[11].value | (ym_regs[12].value<<8))) {
|
||||
ym_env_cnt = 0;
|
||||
ym_env_step--;
|
||||
if (ym_env_step < 0) {
|
||||
if (ym_env_hold) {
|
||||
if (ym_env_alt)
|
||||
ym_env_att ^= 0x1F;
|
||||
ym_env_holding = 1;
|
||||
ym_env_step = 0;
|
||||
} else {
|
||||
if (ym_env_alt && (ym_env_step & 0x20))
|
||||
ym_env_att ^= 0x1F;
|
||||
ym_env_step &= 0x1F;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ym_env_vol = (ym_env_step ^ ym_env_att);
|
||||
// This is heavily inspired by MAME's ay8910.cpp and the YM2149's datasheet
|
||||
|
||||
ym_out = 0;
|
||||
for (i = 0; i < 3; i++) {
|
||||
if (ym_regs[i + 8].value & 0x10) {
|
||||
// Envelope mode
|
||||
ym_out += (ym_ch[i] ? ym_env_vol : 0);
|
||||
} else {
|
||||
// Fixed mode
|
||||
ym_out += (ym_ch[i] ? (ym_regs[i + 8].value & 0x0F) : 0);
|
||||
}
|
||||
}
|
||||
|
||||
ym_buffer[ym_render_cnt] = (ym_out * 2) - 45;
|
||||
// Render 1024 music samples
|
||||
for (ym_render_cnt = 0; ym_render_cnt < 1024; ym_render_cnt++) {
|
||||
// Update registers at 48000/960 = 50Hz
|
||||
if (ym_sample_cnt == 0) {
|
||||
// "Decompress" on the fly and update YM registers
|
||||
for (i = 0; i < 14; i++) {
|
||||
if (!ym_regs[i].cnt) {
|
||||
// New run
|
||||
ymdata = ymdata_bin[ym_regs[i].ptr++];
|
||||
ym_regs[i].cnt = ymdata & 0x7F;
|
||||
if (ymdata & 0x80) {
|
||||
ym_regs[i].same = true;
|
||||
ym_regs[i].value = ymdata_bin[ym_regs[i].ptr++];
|
||||
} else {
|
||||
ym_regs[i].same = false;
|
||||
}
|
||||
// Detect drum on channel B
|
||||
if (i == 3)
|
||||
if (ym_regs[3].value > 2) drum = 4;
|
||||
}
|
||||
if (ym_regs[i].same == false) {
|
||||
ym_regs[i].value = ymdata_bin[ym_regs[i].ptr++];
|
||||
if (i == 13) {
|
||||
// Update envelope attributes
|
||||
ym_env_att = (ym_regs[13].value & 4) ? 0x1F : 0x00;
|
||||
if (!(ym_regs[13].value & 8)) {
|
||||
ym_env_hold = 1;
|
||||
ym_env_alt = ym_env_att;
|
||||
} else {
|
||||
ym_env_hold = ym_regs[13].value & 1;
|
||||
ym_env_alt = ym_regs[13].value & 2;
|
||||
}
|
||||
// Reset envelope counter
|
||||
ym_env_step = 0x1F;
|
||||
ym_env_holding = 0;
|
||||
ym_env_vol = (ym_env_step ^ ym_env_att);
|
||||
}
|
||||
}
|
||||
ym_regs[i].cnt--;
|
||||
}
|
||||
ym_frame++;
|
||||
}
|
||||
|
||||
if (ym_sample_cnt < 960) {
|
||||
ym_sample_cnt++;
|
||||
} else {
|
||||
ym_sample_cnt = 0;
|
||||
}
|
||||
|
||||
// Loop
|
||||
if (ym_frame == ym_frames) ym_init();
|
||||
}
|
||||
// Square wave oscillators
|
||||
// 2457600/16/48000 = 3.2, but 4 sounds better than 3...
|
||||
for (i = 0; i < 3; i++) {
|
||||
ym_osc_cnt[i] += 4;
|
||||
if (ym_osc_cnt[i] >= (ym_regs[i * 2].value | ((ym_regs[(i * 2) + 1].value & 0x0f) << 8))) {
|
||||
ym_osc_cnt[i] = 0;
|
||||
ym_osc_out[i] ^= 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Noise generator
|
||||
ym_noise_cnt += 4;
|
||||
if (ym_noise_cnt >= ((ym_regs[6].value & 0x1F) * 2)) {
|
||||
ym_noise_cnt = 0;
|
||||
ym_rng ^= (((ym_rng & 1) ^ ((ym_rng >> 3) & 1)) << 17);
|
||||
ym_rng >>= 1;
|
||||
}
|
||||
|
||||
// Mix tones and noise
|
||||
for (i = 0; i < 3; i++)
|
||||
ym_ch[i] = (ym_osc_out[i] | ((ym_regs[7].value >> i) & 1)) & ((ym_rng & 1) | ((ym_regs[7].value >> (i + 3)) & 1));
|
||||
|
||||
// Envelope generator
|
||||
if (!ym_env_holding) {
|
||||
ym_env_cnt += 8;
|
||||
if (ym_env_cnt >= (ym_regs[11].value | (ym_regs[12].value << 8))) {
|
||||
ym_env_cnt = 0;
|
||||
ym_env_step--;
|
||||
if (ym_env_step < 0) {
|
||||
if (ym_env_hold) {
|
||||
if (ym_env_alt)
|
||||
ym_env_att ^= 0x1F;
|
||||
ym_env_holding = 1;
|
||||
ym_env_step = 0;
|
||||
} else {
|
||||
if (ym_env_alt && (ym_env_step & 0x20))
|
||||
ym_env_att ^= 0x1F;
|
||||
ym_env_step &= 0x1F;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ym_env_vol = (ym_env_step ^ ym_env_att);
|
||||
|
||||
ym_out = 0;
|
||||
for (i = 0; i < 3; i++) {
|
||||
if (ym_regs[i + 8].value & 0x10) {
|
||||
// Envelope mode
|
||||
ym_out += (ym_ch[i] ? ym_env_vol : 0);
|
||||
} else {
|
||||
// Fixed mode
|
||||
ym_out += (ym_ch[i] ? (ym_regs[i + 8].value & 0x0F) : 0);
|
||||
}
|
||||
}
|
||||
|
||||
ym_buffer[ym_render_cnt] = (ym_out * 2) - 45;
|
||||
|
||||
if (ym_sample_cnt < 960) {
|
||||
ym_sample_cnt++;
|
||||
} else {
|
||||
ym_sample_cnt = 0;
|
||||
}
|
||||
|
||||
// Loop
|
||||
if (ym_frame == ym_frames) ym_init();
|
||||
}
|
||||
}
|
||||
|
||||
void AboutView::update() {
|
||||
if (framebuffer) {
|
||||
// Update 1 out of 2 frames, 60Hz is very laggy
|
||||
if (refresh_cnt & 1) render_video();
|
||||
refresh_cnt++;
|
||||
}
|
||||
|
||||
// Slowly increase volume to avoid jumpscare
|
||||
if (headphone_vol < (70 << 2)) {
|
||||
audio::headphone::set_volume(volume_t::decibel((headphone_vol/4) - 99) + audio::headphone::volume_range().max);
|
||||
headphone_vol++;
|
||||
}
|
||||
if (framebuffer) {
|
||||
// Update 1 out of 2 frames, 60Hz is very laggy
|
||||
if (refresh_cnt & 1) render_video();
|
||||
refresh_cnt++;
|
||||
}
|
||||
|
||||
// Slowly increase volume to avoid jumpscare
|
||||
if (headphone_vol < (70 << 2)) {
|
||||
audio::headphone::set_volume(volume_t::decibel((headphone_vol / 4) - 99) + audio::headphone::volume_range().max);
|
||||
headphone_vol++;
|
||||
}
|
||||
}
|
||||
|
||||
void AboutView::ym_init() {
|
||||
uint8_t reg;
|
||||
|
||||
for (reg = 0; reg < 14; reg++) {
|
||||
ym_regs[reg].cnt = 0;
|
||||
// Pick up start pointers for each YM registers RLE blocks
|
||||
ym_regs[reg].ptr = ((uint16_t)(ymdata_bin[(reg*2)+3])<<8) + ymdata_bin[(reg*2)+2];
|
||||
ym_regs[reg].same = false; // Useless ?
|
||||
ym_regs[reg].value = 0; // Useless ?
|
||||
}
|
||||
|
||||
ym_frame = 0;
|
||||
uint8_t reg;
|
||||
|
||||
for (reg = 0; reg < 14; reg++) {
|
||||
ym_regs[reg].cnt = 0;
|
||||
// Pick up start pointers for each YM registers RLE blocks
|
||||
ym_regs[reg].ptr = ((uint16_t)(ymdata_bin[(reg * 2) + 3]) << 8) + ymdata_bin[(reg * 2) + 2];
|
||||
ym_regs[reg].same = false; // Useless ?
|
||||
ym_regs[reg].value = 0; // Useless ?
|
||||
}
|
||||
|
||||
ym_frame = 0;
|
||||
}
|
||||
|
||||
AboutView::AboutView(
|
||||
NavigationView& nav
|
||||
)
|
||||
{
|
||||
uint8_t p, c;
|
||||
|
||||
baseband::run_image(portapack::spi_flash::image_tag_audio_tx);
|
||||
|
||||
add_children({ {
|
||||
&text_title,
|
||||
&text_firmware,
|
||||
&text_cpld_hackrf,
|
||||
&text_cpld_hackrf_status,
|
||||
&button_ok,
|
||||
} });
|
||||
|
||||
if( cpld_hackrf_verify_eeprom() ) {
|
||||
text_cpld_hackrf_status.set(" OK");
|
||||
} else {
|
||||
text_cpld_hackrf_status.set("BAD");
|
||||
}
|
||||
|
||||
// Politely ask for about 26kB
|
||||
framebuffer = (ui::Color *)chHeapAlloc(0x0, 180 * 72 * sizeof(ui::Color));
|
||||
|
||||
if (framebuffer) {
|
||||
memset(framebuffer, 0, 180 * 72 * sizeof(ui::Color));
|
||||
|
||||
// Copy original font palette
|
||||
c = 0;
|
||||
for (p = 0; p < 48; p+=3)
|
||||
paletteA[c++] = ui::Color(demofont_bin[p], demofont_bin[p+1], demofont_bin[p+2]);
|
||||
|
||||
// Increase red in another one
|
||||
c = 0;
|
||||
for (p = 0; p < 48; p+=3)
|
||||
paletteB[c++] = ui::Color(std::min(demofont_bin[p]+64, 255), demofont_bin[p+1], demofont_bin[p+2]);
|
||||
}
|
||||
NavigationView& nav) {
|
||||
uint8_t p, c;
|
||||
|
||||
// Init YM synth
|
||||
ym_frames = ((uint16_t)(ymdata_bin[1])<<8) + ymdata_bin[0];
|
||||
ym_init();
|
||||
|
||||
baseband::run_image(portapack::spi_flash::image_tag_audio_tx);
|
||||
|
||||
button_ok.on_select = [this,&nav](Button&){
|
||||
if (framebuffer) chHeapFree(framebuffer); // Do NOT forget this
|
||||
nav.pop();
|
||||
};
|
||||
add_children({{
|
||||
&text_title,
|
||||
&text_firmware,
|
||||
&text_cpld_hackrf,
|
||||
&text_cpld_hackrf_status,
|
||||
&button_ok,
|
||||
}});
|
||||
|
||||
if (cpld_hackrf_verify_eeprom()) {
|
||||
text_cpld_hackrf_status.set(" OK");
|
||||
} else {
|
||||
text_cpld_hackrf_status.set("BAD");
|
||||
}
|
||||
|
||||
// Politely ask for about 26kB
|
||||
framebuffer = (ui::Color*)chHeapAlloc(0x0, 180 * 72 * sizeof(ui::Color));
|
||||
|
||||
if (framebuffer) {
|
||||
memset(framebuffer, 0, 180 * 72 * sizeof(ui::Color));
|
||||
|
||||
// Copy original font palette
|
||||
c = 0;
|
||||
for (p = 0; p < 48; p += 3)
|
||||
paletteA[c++] = ui::Color(demofont_bin[p], demofont_bin[p + 1], demofont_bin[p + 2]);
|
||||
|
||||
// Increase red in another one
|
||||
c = 0;
|
||||
for (p = 0; p < 48; p += 3)
|
||||
paletteB[c++] = ui::Color(std::min(demofont_bin[p] + 64, 255), demofont_bin[p + 1], demofont_bin[p + 2]);
|
||||
}
|
||||
|
||||
// Init YM synth
|
||||
ym_frames = ((uint16_t)(ymdata_bin[1]) << 8) + ymdata_bin[0];
|
||||
ym_init();
|
||||
|
||||
button_ok.on_select = [this, &nav](Button&) {
|
||||
if (framebuffer) chHeapFree(framebuffer); // Do NOT forget this
|
||||
nav.pop();
|
||||
};
|
||||
}
|
||||
|
||||
AboutView::~AboutView() {
|
||||
transmitter_model.disable();
|
||||
baseband::shutdown();
|
||||
transmitter_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
void AboutView::focus() {
|
||||
button_ok.focus();
|
||||
button_ok.focus();
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -34,141 +34,135 @@
|
||||
namespace ui {
|
||||
|
||||
class AboutView : public View {
|
||||
public:
|
||||
AboutView(NavigationView& nav);
|
||||
~AboutView();
|
||||
|
||||
void on_show() override;
|
||||
void focus() override;
|
||||
public:
|
||||
AboutView(NavigationView& nav);
|
||||
~AboutView();
|
||||
|
||||
private:
|
||||
void ym_init();
|
||||
void update();
|
||||
void render_video();
|
||||
void render_audio();
|
||||
void draw_demoglyph(ui::Point p, char ch, ui::Color * pal);
|
||||
uint16_t debug_cnt = 0;
|
||||
|
||||
typedef struct ymreg_t {
|
||||
uint8_t value;
|
||||
uint8_t cnt;
|
||||
uint16_t ptr;
|
||||
bool same;
|
||||
} ymreg_t;
|
||||
|
||||
uint16_t headphone_vol = 5 << 2;
|
||||
|
||||
ymreg_t ym_regs[14];
|
||||
uint16_t ym_frames;
|
||||
uint16_t ym_frame;
|
||||
uint8_t drum = 0;
|
||||
uint16_t ym_osc_cnt[3];
|
||||
uint32_t ym_rng = 1;
|
||||
uint16_t ym_noise_cnt;
|
||||
uint8_t ym_env_att, ym_env_hold, ym_env_alt, ym_env_holding, ym_env_vol;
|
||||
int8_t ym_env_step;
|
||||
uint16_t ym_env_cnt;
|
||||
uint8_t ym_osc_out[3];
|
||||
uint8_t ym_ch[3];
|
||||
uint8_t ym_out;
|
||||
uint16_t ym_sample_cnt = 0;
|
||||
|
||||
int8_t ym_buffer[1024];
|
||||
void on_show() override;
|
||||
void focus() override;
|
||||
|
||||
uint8_t refresh_cnt;
|
||||
ui::Color paletteA[16];
|
||||
ui::Color paletteB[16];
|
||||
ui::Color * framebuffer;
|
||||
uint32_t phase = 0;
|
||||
uint8_t copperbars[5] = { 0 };
|
||||
uint8_t copperbuffer[72] = { 0 };
|
||||
|
||||
uint8_t anim_state = 0;
|
||||
uint8_t credits_index = 0;
|
||||
uint16_t credits_timer = 0;
|
||||
|
||||
int16_t ofx = -180, ofy = -24;
|
||||
private:
|
||||
void ym_init();
|
||||
void update();
|
||||
void render_video();
|
||||
void render_audio();
|
||||
void draw_demoglyph(ui::Point p, char ch, ui::Color* pal);
|
||||
uint16_t debug_cnt = 0;
|
||||
|
||||
const uint8_t char_map[64] = { 0xFF, 27, 46, 0xFF, 0xFF, 0xFF, 28, 45,
|
||||
58, 59, 0xFF, 43, 40, 57, 26, 42,
|
||||
29, 30, 31, 32, 33, 34, 35, 36,
|
||||
37, 38, 41, 0xFF, 0xFF, 0xFF, 0xFF, 44,
|
||||
0xFF, 0, 1, 2, 3, 4, 5, 6,
|
||||
7, 8, 9, 10, 11, 12, 13, 14,
|
||||
15, 16, 17, 18, 19, 20, 21, 22,
|
||||
23, 24, 25, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };
|
||||
|
||||
const uint8_t copperluma[16] = { 8,7,6,5,4,3,2,1,1,2,3,4,5,6,7,8 };
|
||||
const uint8_t coppercolor[5][3] = { { 255,0,0 },
|
||||
{ 0,255,0 },
|
||||
{ 0,0,255 },
|
||||
{ 255,0,255 },
|
||||
{ 255,255,0 } };
|
||||
|
||||
typedef struct credits_t {
|
||||
char role[12];
|
||||
char name[12];
|
||||
bool change;
|
||||
} credits_t;
|
||||
|
||||
// 0123456789A 0123456789A
|
||||
const credits_t credits[10] = { {"GURUS", "J. BOONE", false},
|
||||
{"GURUS", "M. OSSMANN", true},
|
||||
{"BUGS", "FURRTEK", true},
|
||||
{"RDS WAVE", "C. JACQUET", true},
|
||||
{"POCSAG RX", "T. SAILER", false},
|
||||
{"POCSAG RX", "E. OENAL", true},
|
||||
{"XYLOS DATA", "CLX", true},
|
||||
{"GREETS TO", "SIGMOUNTE", false},
|
||||
{"GREETS TO", "WINDYOONA", true},
|
||||
{"THIS MUSIC", "BIG ALEC", true}
|
||||
};
|
||||
typedef struct ymreg_t {
|
||||
uint8_t value;
|
||||
uint8_t cnt;
|
||||
uint16_t ptr;
|
||||
bool same;
|
||||
} ymreg_t;
|
||||
|
||||
Text text_title {
|
||||
{ 100, 32, 40, 16 },
|
||||
"About",
|
||||
};
|
||||
uint16_t headphone_vol = 5 << 2;
|
||||
|
||||
Text text_firmware {
|
||||
{ 0, 236, 240, 16 },
|
||||
"Version " VERSION_STRING,
|
||||
};
|
||||
ymreg_t ym_regs[14];
|
||||
uint16_t ym_frames;
|
||||
uint16_t ym_frame;
|
||||
uint8_t drum = 0;
|
||||
uint16_t ym_osc_cnt[3];
|
||||
uint32_t ym_rng = 1;
|
||||
uint16_t ym_noise_cnt;
|
||||
uint8_t ym_env_att, ym_env_hold, ym_env_alt, ym_env_holding, ym_env_vol;
|
||||
int8_t ym_env_step;
|
||||
uint16_t ym_env_cnt;
|
||||
uint8_t ym_osc_out[3];
|
||||
uint8_t ym_ch[3];
|
||||
uint8_t ym_out;
|
||||
uint16_t ym_sample_cnt = 0;
|
||||
|
||||
Text text_cpld_hackrf {
|
||||
{ 0, 252, 11*8, 16 },
|
||||
"HackRF CPLD",
|
||||
};
|
||||
int8_t ym_buffer[1024];
|
||||
|
||||
Text text_cpld_hackrf_status {
|
||||
{ 240 - 3*8, 252, 3*8, 16 },
|
||||
"???"
|
||||
};
|
||||
uint8_t refresh_cnt;
|
||||
ui::Color paletteA[16];
|
||||
ui::Color paletteB[16];
|
||||
ui::Color* framebuffer;
|
||||
uint32_t phase = 0;
|
||||
uint8_t copperbars[5] = {0};
|
||||
uint8_t copperbuffer[72] = {0};
|
||||
|
||||
Button button_ok {
|
||||
{ 72, 272, 96, 24 },
|
||||
"OK"
|
||||
};
|
||||
|
||||
MessageHandlerRegistration message_handler_update {
|
||||
Message::ID::DisplayFrameSync,
|
||||
[this](const Message* const) {
|
||||
this->update();
|
||||
}
|
||||
};
|
||||
|
||||
MessageHandlerRegistration message_handler_fifo_signal {
|
||||
Message::ID::FIFOSignal,
|
||||
[this](const Message* const p) {
|
||||
const auto message = static_cast<const FIFOSignalMessage*>(p);
|
||||
if (message->signaltype == 1) {
|
||||
this->render_audio();
|
||||
baseband::set_fifo_data(ym_buffer);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
uint8_t anim_state = 0;
|
||||
uint8_t credits_index = 0;
|
||||
uint16_t credits_timer = 0;
|
||||
|
||||
int16_t ofx = -180, ofy = -24;
|
||||
|
||||
const uint8_t char_map[64] = {0xFF, 27, 46, 0xFF, 0xFF, 0xFF, 28, 45,
|
||||
58, 59, 0xFF, 43, 40, 57, 26, 42,
|
||||
29, 30, 31, 32, 33, 34, 35, 36,
|
||||
37, 38, 41, 0xFF, 0xFF, 0xFF, 0xFF, 44,
|
||||
0xFF, 0, 1, 2, 3, 4, 5, 6,
|
||||
7, 8, 9, 10, 11, 12, 13, 14,
|
||||
15, 16, 17, 18, 19, 20, 21, 22,
|
||||
23, 24, 25, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||
|
||||
const uint8_t copperluma[16] = {8, 7, 6, 5, 4, 3, 2, 1, 1, 2, 3, 4, 5, 6, 7, 8};
|
||||
const uint8_t coppercolor[5][3] = {{255, 0, 0},
|
||||
{0, 255, 0},
|
||||
{0, 0, 255},
|
||||
{255, 0, 255},
|
||||
{255, 255, 0}};
|
||||
|
||||
typedef struct credits_t {
|
||||
char role[12];
|
||||
char name[12];
|
||||
bool change;
|
||||
} credits_t;
|
||||
|
||||
// 0123456789A 0123456789A
|
||||
const credits_t credits[10] = {{"GURUS", "J. BOONE", false},
|
||||
{"GURUS", "M. OSSMANN", true},
|
||||
{"BUGS", "FURRTEK", true},
|
||||
{"RDS WAVE", "C. JACQUET", true},
|
||||
{"POCSAG RX", "T. SAILER", false},
|
||||
{"POCSAG RX", "E. OENAL", true},
|
||||
{"XYLOS DATA", "CLX", true},
|
||||
{"GREETS TO", "SIGMOUNTE", false},
|
||||
{"GREETS TO", "WINDYOONA", true},
|
||||
{"THIS MUSIC", "BIG ALEC", true}};
|
||||
|
||||
Text text_title{
|
||||
{100, 32, 40, 16},
|
||||
"About",
|
||||
};
|
||||
|
||||
Text text_firmware{
|
||||
{0, 236, 240, 16},
|
||||
"Version " VERSION_STRING,
|
||||
};
|
||||
|
||||
Text text_cpld_hackrf{
|
||||
{0, 252, 11 * 8, 16},
|
||||
"HackRF CPLD",
|
||||
};
|
||||
|
||||
Text text_cpld_hackrf_status{
|
||||
{240 - 3 * 8, 252, 3 * 8, 16},
|
||||
"???"};
|
||||
|
||||
Button button_ok{
|
||||
{72, 272, 96, 24},
|
||||
"OK"};
|
||||
|
||||
MessageHandlerRegistration message_handler_update{
|
||||
Message::ID::DisplayFrameSync,
|
||||
[this](const Message* const) {
|
||||
this->update();
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_fifo_signal{
|
||||
Message::ID::FIFOSignal,
|
||||
[this](const Message* const p) {
|
||||
const auto message = static_cast<const FIFOSignalMessage*>(p);
|
||||
if (message->signaltype == 1) {
|
||||
this->render_audio();
|
||||
baseband::set_fifo_data(ym_buffer);
|
||||
}
|
||||
}};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
#endif/*__UI_ABOUT_H__*/
|
||||
#endif /*__UI_ABOUT_H__*/
|
||||
|
||||
@@ -1,85 +1,88 @@
|
||||
#include "ui_about_simple.hpp"
|
||||
|
||||
namespace ui
|
||||
{
|
||||
AboutView::AboutView(NavigationView &nav)
|
||||
{
|
||||
add_children({&console, &button_ok});
|
||||
|
||||
button_ok.on_select = [&nav](Button &)
|
||||
{
|
||||
nav.pop();
|
||||
};
|
||||
|
||||
console.writeln("\x1B\x07List of contributors:\x1B\x10");
|
||||
console.writeln("");
|
||||
}
|
||||
|
||||
void AboutView::update()
|
||||
{
|
||||
if (++timer > 200)
|
||||
{
|
||||
timer = 0;
|
||||
|
||||
switch (++frame)
|
||||
{
|
||||
case 1:
|
||||
// TODO: Generate this automatically from github
|
||||
// https://github.com/eried/portapack-mayhem/graphs/contributors?to=2022-01-01&from=2020-04-12&type=c
|
||||
console.writeln("\x1B\x06Mayhem:\x1B\x10");
|
||||
console.writeln("eried,euquiq,gregoryfenton");
|
||||
console.writeln("johnelder,jwetzell,nnemanjan00");
|
||||
console.writeln("N0vaPixel,klockee,GullCode");
|
||||
console.writeln("jamesshao8,ITAxReal,rascafr");
|
||||
console.writeln("mcules,dqs105,strijar");
|
||||
console.writeln("zhang00963,RedFox-Fr,aldude999");
|
||||
console.writeln("East2West,fossum,ArjanOnwezen");
|
||||
console.writeln("vXxOinvizioNxX,teixeluis");
|
||||
console.writeln("Brumi-2021,texasyojimbo");
|
||||
console.writeln("heurist1,intoxsick,ckuethe");
|
||||
console.writeln("notpike,jLynx,zigad");
|
||||
console.writeln("MichalLeonBorsuk,jimilinuxguy");
|
||||
console.writeln("");
|
||||
break;
|
||||
|
||||
case 2:
|
||||
// https://github.com/eried/portapack-mayhem/graphs/contributors?to=2020-04-12&from=2015-07-31&type=c
|
||||
console.writeln("\x1B\x06Havoc:\x1B\x10");
|
||||
console.writeln("furrtek,mrmookie,NotPike");
|
||||
console.writeln("mjwaxios,ImDroided,Giorgiofox");
|
||||
console.writeln("F4GEV,z4ziggy,xmycroftx");
|
||||
console.writeln("troussos,silascutler");
|
||||
console.writeln("nickbouwhuis,msoose,leres");
|
||||
console.writeln("joakar,dhoetger,clem-42");
|
||||
console.writeln("brianlechthaler,ZeroChaos-...");
|
||||
console.writeln("");
|
||||
break;
|
||||
|
||||
case 3:
|
||||
// https://github.com/eried/portapack-mayhem/graphs/contributors?from=2014-07-05&to=2015-07-31&type=c
|
||||
console.writeln("\x1B\x06PortaPack:\x1B\x10");
|
||||
console.writeln("jboone,argilo");
|
||||
console.writeln("");
|
||||
break;
|
||||
|
||||
case 4:
|
||||
// https://github.com/mossmann/hackrf/graphs/contributors
|
||||
console.writeln("\x1B\x06HackRF:\x1B\x10");
|
||||
console.writeln("mossmann,dominicgs,bvernoux");
|
||||
console.writeln("bgamari,schneider42,miek");
|
||||
console.writeln("willcode,hessu,Sec42");
|
||||
console.writeln("yhetti,ckuethe,smunaut");
|
||||
console.writeln("wishi,mrbubble62,scateu...");
|
||||
console.writeln("");
|
||||
frame = 0; // Loop
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AboutView::focus()
|
||||
{
|
||||
button_ok.focus();
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
#include "ui_about_simple.hpp"
|
||||
|
||||
namespace ui {
|
||||
|
||||
// TODO: Generate this automatically from github
|
||||
// Information: a line starting with a '#' will be yellow coloured
|
||||
const std::string authors_list[] = {
|
||||
"# * List of contributors * ",
|
||||
" ",
|
||||
"#Mayhem:",
|
||||
"eried,euquiq,gregoryfenton",
|
||||
"johnelder,jwetzell,nnemanjan00",
|
||||
"N0vaPixel,klockee,gullradriel",
|
||||
"jamesshao8,ITAxReal,rascafr",
|
||||
"mcules,dqs105,strijar",
|
||||
"zhang00963,RedFox-Fr,aldude999",
|
||||
"East2West,fossum,ArjanOnwezen",
|
||||
"vXxOinvizioNxX,teixeluis",
|
||||
"Brumi-2021,texasyojimbo",
|
||||
"heurist1,intoxsick,ckuethe",
|
||||
"notpike,jLynx,zigad",
|
||||
"MichalLeonBorsuk,jimilinuxguy",
|
||||
"kallanreed,bernd-herzog",
|
||||
"NotherNgineer,zxkmm,u-foka",
|
||||
"Netro,HTotoo",
|
||||
" ",
|
||||
"#Havoc:",
|
||||
"furrtek,mrmookie,NotPike",
|
||||
"mjwaxios,ImDroided,Giorgiofox",
|
||||
"F4GEV,z4ziggy,xmycroftx",
|
||||
"troussos,silascutler",
|
||||
"nickbouwhuis,msoose,leres",
|
||||
"joakar,dhoetger,clem-42",
|
||||
"brianlechthaler,ZeroChaos-...",
|
||||
" ",
|
||||
"#PortaPack:",
|
||||
"jboone,argilo",
|
||||
" ",
|
||||
"#HackRF:",
|
||||
"mossmann,dominicgs,bvernoux",
|
||||
"bgamari,schneider42,miek",
|
||||
"willcode,hessu,Sec42",
|
||||
"yhetti,ckuethe,smunaut",
|
||||
"wishi,mrbubble62,scateu..."};
|
||||
|
||||
AboutView::AboutView(NavigationView& nav) {
|
||||
add_children({&menu_view,
|
||||
&button_ok});
|
||||
|
||||
button_ok.on_select = [&nav](Button&) {
|
||||
nav.pop();
|
||||
};
|
||||
|
||||
menu_view.on_left = [this]() {
|
||||
button_ok.focus();
|
||||
};
|
||||
|
||||
menu_view.on_right = [this]() {
|
||||
button_ok.focus();
|
||||
};
|
||||
|
||||
for (const std::string& authors_line : authors_list) {
|
||||
// if it's starting with #, it's a title and we have to substract the '#' and paint yellow
|
||||
if (authors_line.size() > 0) {
|
||||
if (authors_line[0] == '#') {
|
||||
menu_view.add_item(
|
||||
{authors_line.substr(1, authors_line.size() - 1),
|
||||
ui::Theme::getInstance()->fg_yellow->foreground,
|
||||
nullptr,
|
||||
nullptr});
|
||||
} else {
|
||||
menu_view.add_item(
|
||||
{authors_line,
|
||||
Theme::getInstance()->bg_darkest->foreground,
|
||||
nullptr,
|
||||
nullptr});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AboutView::focus() {
|
||||
menu_view.focus();
|
||||
// put focus on last text line to make it more obvious that list is scrollable
|
||||
menu_view.set_highlighted(10);
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -3,38 +3,26 @@
|
||||
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_font_fixed_8x16.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace ui
|
||||
{
|
||||
class AboutView : public View
|
||||
{
|
||||
public:
|
||||
AboutView(NavigationView &nav);
|
||||
void focus() override;
|
||||
std::string title() const override { return "About"; };
|
||||
int32_t timer{180};
|
||||
short frame{0};
|
||||
namespace ui {
|
||||
class AboutView : public View {
|
||||
public:
|
||||
AboutView(NavigationView& nav);
|
||||
void focus() override;
|
||||
std::string title() const override { return "About"; };
|
||||
|
||||
private:
|
||||
void update();
|
||||
private:
|
||||
MenuView menu_view{
|
||||
{0, 0, 240, 264},
|
||||
true};
|
||||
|
||||
Console console{
|
||||
{0, 10, 240, 240}};
|
||||
|
||||
Button button_ok{
|
||||
{240/3, 270, 240/3, 24},
|
||||
"OK",
|
||||
};
|
||||
|
||||
MessageHandlerRegistration message_handler_update{
|
||||
Message::ID::DisplayFrameSync,
|
||||
[this](const Message *const) {
|
||||
this->update();
|
||||
}};
|
||||
Button button_ok{
|
||||
{240 / 3, 270, 240 / 3, 24},
|
||||
"OK",
|
||||
};
|
||||
} // namespace ui
|
||||
};
|
||||
} // namespace ui
|
||||
|
||||
#endif /*__UI_ABOUT_SIMPLE_H__*/
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,9 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2017 Furrtek
|
||||
*
|
||||
* Copyright (C) 2023 Kyle Reed
|
||||
* Copyright (C) 2024 Mark Thompson
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
@@ -21,410 +23,431 @@
|
||||
*/
|
||||
|
||||
#include "ui.hpp"
|
||||
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_geomap.hpp"
|
||||
#include "ui_font_fixed_8x16.hpp"
|
||||
#include "string_format.hpp"
|
||||
|
||||
#include "file.hpp"
|
||||
#include "database.hpp"
|
||||
#include "recent_entries.hpp"
|
||||
#include "log_file.hpp"
|
||||
#include "adsb.hpp"
|
||||
#include "message.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "crc.hpp"
|
||||
#include "database.hpp"
|
||||
#include "file.hpp"
|
||||
#include "log_file.hpp"
|
||||
#include "message.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "recent_entries.hpp"
|
||||
#include "string_format.hpp"
|
||||
|
||||
using namespace adsb;
|
||||
|
||||
namespace ui {
|
||||
#define AIRCRAFT_ID_L 1 // aircraft ID message type (lowest type id)
|
||||
#define AIRCRAFT_ID_H 4 // aircraft ID message type (highest type id)
|
||||
|
||||
#define ADSB_CURRENT 10 // Seconds
|
||||
#define ADSB_RECENT 30 // Seconds
|
||||
#define ADSB_REMOVE 300 // Used for removing old entries
|
||||
#define SURFACE_POS_L 5 // surface position (lowest type id)
|
||||
#define SURFACE_POS_H 8 // surface position (highest type id)
|
||||
|
||||
#define AIRCRAFT_ID_L 1 // aircraft ID message type (lowest type id)
|
||||
#define AIRCRAFT_ID_H 4 // aircraft ID message type (highest type id)
|
||||
#define AIRBORNE_POS_BARO_L 9 // airborne position (lowest type id)
|
||||
#define AIRBORNE_POS_BARO_H 18 // airborne position (highest type id)
|
||||
|
||||
#define SURFACE_POS_L 5 // surface position (lowest type id)
|
||||
#define SURFACE_POS_H 8 // surface position (highest type id)
|
||||
#define AIRBORNE_VEL 19 // airborne velocities
|
||||
|
||||
#define AIRBORNE_POS_BARO_L 9 // airborne position (lowest type id)
|
||||
#define AIRBORNE_POS_BARO_H 18 // airborne position (highest type id)
|
||||
#define AIRBORNE_POS_GPS_L 20 // airborne position (lowest type id)
|
||||
#define AIRBORNE_POS_GPS_H 22 // airborne position (highest type id)
|
||||
|
||||
#define AIRBORNE_VEL 19 // airborne velocities
|
||||
#define AIRBORNE_OP_STATUS 31 // Aircraft operation status
|
||||
|
||||
#define AIRBORNE_POS_GPS_L 20 // airborne position (lowest type id)
|
||||
#define AIRBORNE_POS_GPS_H 22 // airborne position (highest type id)
|
||||
#define RESERVED_L 23 // reserved for other uses
|
||||
#define RESERVED_H 31 // reserved for other uses
|
||||
|
||||
#define RESERVED_L 23 // reserved for other uses
|
||||
#define RESERVED_H 31 // reserved for other uses
|
||||
#define VEL_GND_SUBSONIC 1
|
||||
#define VEL_GND_SUPERSONIC 2
|
||||
#define VEL_AIR_SUBSONIC 3
|
||||
#define VEL_AIR_SUPERSONIC 4
|
||||
|
||||
#define VEL_GND_SUBSONIC 1
|
||||
#define VEL_GND_SUPERSONIC 2
|
||||
#define VEL_AIR_SUBSONIC 3
|
||||
#define VEL_AIR_SUPERSONIC 4
|
||||
#define O_E_FRAME_TIMEOUT 20 // timeout between odd and even frames
|
||||
#define MARKER_UPDATE_SECONDS 5 // "other" map marker redraw interval
|
||||
|
||||
#define O_E_FRAME_TIMEOUT 20 // timeout between odd and even frames
|
||||
|
||||
struct AircraftRecentEntry {
|
||||
using Key = uint32_t;
|
||||
|
||||
static constexpr Key invalid_key = 0xffffffff;
|
||||
|
||||
uint32_t ICAO_address { };
|
||||
uint16_t hits { 0 };
|
||||
|
||||
uint16_t age_state { 1 };
|
||||
uint32_t age { 0 };
|
||||
uint32_t amp { 0 };
|
||||
adsb_pos pos { false, 0, 0, 0 };
|
||||
adsb_vel velo { false, 0, 999, 0 };
|
||||
ADSBFrame frame_pos_even { };
|
||||
ADSBFrame frame_pos_odd { };
|
||||
|
||||
std::string icaoStr {" "};
|
||||
std::string callsign { " " };
|
||||
std::string info_string { "" };
|
||||
|
||||
AircraftRecentEntry(
|
||||
const uint32_t ICAO_address
|
||||
) : ICAO_address { ICAO_address }
|
||||
{
|
||||
this->icaoStr = to_string_hex(ICAO_address, 6);
|
||||
}
|
||||
|
||||
Key key() const {
|
||||
return ICAO_address;
|
||||
}
|
||||
|
||||
void set_callsign(std::string& new_callsign) {
|
||||
callsign = new_callsign;
|
||||
}
|
||||
|
||||
void inc_hit() {
|
||||
hits++;
|
||||
}
|
||||
|
||||
void set_frame_pos(ADSBFrame& frame, uint32_t parity) {
|
||||
if (!parity)
|
||||
frame_pos_even = frame;
|
||||
else
|
||||
frame_pos_odd = frame;
|
||||
|
||||
if (!frame_pos_even.empty() && !frame_pos_odd.empty()) {
|
||||
if (abs(frame_pos_even.get_rx_timestamp() - frame_pos_odd.get_rx_timestamp()) < O_E_FRAME_TIMEOUT)
|
||||
pos = decode_frame_pos(frame_pos_even, frame_pos_odd);
|
||||
}
|
||||
}
|
||||
|
||||
void set_frame_velo(ADSBFrame& frame){
|
||||
velo = decode_frame_velo(frame);
|
||||
}
|
||||
|
||||
void set_info_string(std::string& new_info_string) {
|
||||
info_string = new_info_string;
|
||||
}
|
||||
|
||||
void reset_age() {
|
||||
age = 0;
|
||||
}
|
||||
|
||||
void inc_age(int delta) {
|
||||
age+=delta;
|
||||
if (age < ADSB_CURRENT){
|
||||
age_state = pos.valid ? 0 : 1;
|
||||
} else if(age < ADSB_RECENT){
|
||||
age_state = 2;
|
||||
} else if(age < ADSB_REMOVE){
|
||||
age_state = 3;
|
||||
} else{
|
||||
age_state = 4;
|
||||
}
|
||||
}
|
||||
/* Thresholds (in seconds) that define the transition between ages. */
|
||||
struct ADSBAgeLimit {
|
||||
static constexpr int Current = 10;
|
||||
static constexpr int Recent = 30;
|
||||
static constexpr int Expired = 300;
|
||||
};
|
||||
|
||||
/* Age states used for sorting and drawing recent entries. */
|
||||
enum class ADSBAgeState : uint8_t {
|
||||
Invalid,
|
||||
Current,
|
||||
Recent,
|
||||
Old,
|
||||
Expired,
|
||||
};
|
||||
|
||||
/* Data extracted from ADSB frames. */
|
||||
struct AircraftRecentEntry {
|
||||
using Key = uint32_t;
|
||||
|
||||
static constexpr Key invalid_key = 0xffffffff;
|
||||
|
||||
uint32_t ICAO_address{};
|
||||
uint16_t hits{0};
|
||||
|
||||
ADSBAgeState state{ADSBAgeState::Invalid};
|
||||
uint32_t age{0}; // In seconds
|
||||
uint32_t amp{0};
|
||||
adsb_pos pos{false, 0, 0, 0};
|
||||
adsb_vel velo{false, 0, 999, 0};
|
||||
ADSBFrame frame_pos_even{};
|
||||
ADSBFrame frame_pos_odd{};
|
||||
|
||||
std::string icao_str{};
|
||||
std::string callsign{};
|
||||
std::string info_string{};
|
||||
|
||||
uint8_t sil{0}; // Surveillance integrity level
|
||||
|
||||
AircraftRecentEntry(const uint32_t ICAO_address)
|
||||
: ICAO_address{ICAO_address} {
|
||||
this->icao_str = to_string_hex(ICAO_address, 6);
|
||||
}
|
||||
|
||||
/* RecentEntries helpers expect a "key" on every item. */
|
||||
Key key() const {
|
||||
return ICAO_address;
|
||||
}
|
||||
|
||||
void set_callsign(std::string new_callsign) {
|
||||
callsign = std::move(new_callsign);
|
||||
}
|
||||
|
||||
void inc_hit() {
|
||||
hits++;
|
||||
}
|
||||
|
||||
void set_frame_pos(ADSBFrame& frame, uint32_t parity) {
|
||||
if (!parity)
|
||||
frame_pos_even = frame;
|
||||
else
|
||||
frame_pos_odd = frame;
|
||||
|
||||
if (!frame_pos_even.empty() && !frame_pos_odd.empty()) {
|
||||
if (abs(frame_pos_even.get_rx_timestamp() - frame_pos_odd.get_rx_timestamp()) < O_E_FRAME_TIMEOUT)
|
||||
pos = decode_frame_pos(frame_pos_even, frame_pos_odd);
|
||||
}
|
||||
}
|
||||
|
||||
void set_frame_velo(ADSBFrame& frame) {
|
||||
velo = decode_frame_velo(frame);
|
||||
}
|
||||
|
||||
void set_info_string(std::string new_info_string) {
|
||||
info_string = std::move(new_info_string);
|
||||
}
|
||||
|
||||
void reset_age() {
|
||||
age = 0;
|
||||
}
|
||||
|
||||
void inc_age(int delta) {
|
||||
age += delta;
|
||||
|
||||
if (age < ADSBAgeLimit::Current)
|
||||
state = pos.valid ? ADSBAgeState::Invalid
|
||||
: ADSBAgeState::Current;
|
||||
|
||||
else if (age < ADSBAgeLimit::Recent)
|
||||
state = ADSBAgeState::Recent;
|
||||
|
||||
else if (age < ADSBAgeLimit::Expired)
|
||||
state = ADSBAgeState::Old;
|
||||
|
||||
else
|
||||
state = ADSBAgeState::Expired;
|
||||
}
|
||||
};
|
||||
|
||||
// NB: uses std::list underneath so assuming refs are NOT invalidated.
|
||||
using AircraftRecentEntries = RecentEntries<AircraftRecentEntry>;
|
||||
|
||||
/* Holds data for logging. */
|
||||
struct ADSBLogEntry {
|
||||
std::string raw_data{};
|
||||
std::string icao{};
|
||||
std::string callsign{};
|
||||
adsb_pos pos{};
|
||||
adsb_vel vel{};
|
||||
uint8_t vel_type{};
|
||||
uint8_t sil{};
|
||||
};
|
||||
|
||||
// TODO: Make logging optional.
|
||||
/* Logs entries to a log file. */
|
||||
class ADSBLogger {
|
||||
public:
|
||||
Optional<File::Error> append(const std::filesystem::path& filename) {
|
||||
return log_file.append(filename);
|
||||
}
|
||||
void log_str(std::string& logline);
|
||||
public:
|
||||
Optional<File::Error> append(const std::filesystem::path& filename) {
|
||||
return log_file.append(filename);
|
||||
}
|
||||
void log(const ADSBLogEntry& log_entry);
|
||||
|
||||
private:
|
||||
LogFile log_file { };
|
||||
private:
|
||||
LogFile log_file{};
|
||||
};
|
||||
|
||||
|
||||
/* Shows detailed information about an aircraft. */
|
||||
class ADSBRxAircraftDetailsView : public View {
|
||||
public:
|
||||
ADSBRxAircraftDetailsView(NavigationView&, const AircraftRecentEntry& entry, const std::function<void(void)> on_close);
|
||||
~ADSBRxAircraftDetailsView();
|
||||
public:
|
||||
ADSBRxAircraftDetailsView(
|
||||
NavigationView&,
|
||||
const AircraftRecentEntry& entry);
|
||||
|
||||
ADSBRxAircraftDetailsView(const ADSBRxAircraftDetailsView&) = delete;
|
||||
ADSBRxAircraftDetailsView(ADSBRxAircraftDetailsView&&) = delete;
|
||||
ADSBRxAircraftDetailsView& operator=(const ADSBRxAircraftDetailsView&) = delete;
|
||||
ADSBRxAircraftDetailsView& operator=(ADSBRxAircraftDetailsView&&) = delete;
|
||||
|
||||
void focus() override;
|
||||
|
||||
void update(const AircraftRecentEntry& entry);
|
||||
|
||||
std::string title() const override { return "AC Details"; };
|
||||
void focus() override;
|
||||
std::string title() const override { return "AC Details"; }
|
||||
|
||||
AircraftRecentEntry get_current_entry() { return entry_copy; }
|
||||
private:
|
||||
Labels labels{
|
||||
{{0 * 8, 1 * 16}, "ICAO:", Theme::getInstance()->fg_light->foreground},
|
||||
{{0 * 8, 2 * 16}, "Registration:", Theme::getInstance()->fg_light->foreground},
|
||||
{{0 * 8, 3 * 16}, "Manufacturer:", Theme::getInstance()->fg_light->foreground},
|
||||
{{0 * 8, 5 * 16}, "Model:", Theme::getInstance()->fg_light->foreground},
|
||||
{{0 * 8, 7 * 16}, "Type:", Theme::getInstance()->fg_light->foreground},
|
||||
{{0 * 8, 8 * 16}, "Number of engines:", Theme::getInstance()->fg_light->foreground},
|
||||
{{0 * 8, 9 * 16}, "Engine type:", Theme::getInstance()->fg_light->foreground},
|
||||
{{0 * 8, 11 * 16}, "Owner:", Theme::getInstance()->fg_light->foreground},
|
||||
{{0 * 8, 13 * 16}, "Operator:", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
std::database::AircraftDBRecord aircraft_record = {};
|
||||
|
||||
private:
|
||||
AircraftRecentEntry entry_copy { 0 };
|
||||
std::function<void(void)> on_close_ { };
|
||||
bool send_updates { false };
|
||||
std::database db = { };
|
||||
std::string icao_code = "";
|
||||
int return_code = 0;
|
||||
Text text_icao_address{
|
||||
{5 * 8, 1 * 16, 6 * 8, 16},
|
||||
"-"};
|
||||
|
||||
Labels labels {
|
||||
{ { 0 * 8, 1 * 16 }, "ICAO:", Color::light_grey() },
|
||||
{ { 0 * 8, 2 * 16 }, "Registration:", Color::light_grey() },
|
||||
{ { 0 * 8, 3 * 16 }, "Manufacturer:", Color::light_grey() },
|
||||
{ { 0 * 8, 5 * 16 }, "Model:", Color::light_grey() },
|
||||
{ { 0 * 8, 7 * 16 }, "Type:", Color::light_grey() },
|
||||
{ { 0 * 8, 8 * 16 }, "Number of engines:", Color::light_grey() },
|
||||
{ { 0 * 8, 9 * 16 }, "Engine type:", Color::light_grey() },
|
||||
{ { 0 * 8, 11 * 16 }, "Owner:", Color::light_grey() },
|
||||
{ { 0 * 8, 13 * 16 }, "Operator:", Color::light_grey() }
|
||||
};
|
||||
|
||||
Text text_icao_address {
|
||||
{ 5 * 8, 1 * 16, 6 * 8, 16},
|
||||
"-"
|
||||
};
|
||||
Text text_registration{
|
||||
{13 * 8, 2 * 16, 8 * 8, 16},
|
||||
"-"};
|
||||
|
||||
Text text_registration {
|
||||
{ 13 * 8, 2 * 16, 8 * 8, 16 },
|
||||
"-"
|
||||
};
|
||||
|
||||
Text text_manufacturer {
|
||||
{ 0 * 8, 4 * 16, 19 * 8, 16 },
|
||||
"-"
|
||||
};
|
||||
|
||||
Text text_model {
|
||||
{ 0 * 8, 6 * 16, 30 * 8, 16 },
|
||||
"-"
|
||||
};
|
||||
|
||||
Text text_type {
|
||||
{ 5 * 8, 7 * 16, 22 * 8, 16 },
|
||||
"-"
|
||||
};
|
||||
|
||||
Text text_number_of_engines {
|
||||
{ 18 * 8, 8 * 16, 30 * 8, 16 },
|
||||
"-"
|
||||
};
|
||||
Text text_manufacturer{
|
||||
{0 * 8, 4 * 16, 19 * 8, 16},
|
||||
"-"};
|
||||
|
||||
Text text_engine_type {
|
||||
{ 0 * 8, 10 * 16, 30 * 8, 16},
|
||||
"-"
|
||||
};
|
||||
Text text_model{
|
||||
{0 * 8, 6 * 16, 30 * 8, 16},
|
||||
"-"};
|
||||
|
||||
Text text_owner {
|
||||
{ 0 * 8, 12 * 16, 30 * 8, 16 },
|
||||
"-"
|
||||
};
|
||||
Text text_type{
|
||||
{5 * 8, 7 * 16, 22 * 8, 16},
|
||||
"-"};
|
||||
|
||||
Text text_operator {
|
||||
{ 0 * 8, 14 * 16, 30 * 8, 16 },
|
||||
"-"
|
||||
};
|
||||
Text text_number_of_engines{
|
||||
{18 * 8, 8 * 16, 30 * 8, 16},
|
||||
"-"};
|
||||
|
||||
Button button_close {
|
||||
{ 9 * 8, 16 * 16, 12 * 8, 3 * 16 },
|
||||
"Back"
|
||||
};
|
||||
Text text_engine_type{
|
||||
{0 * 8, 10 * 16, 30 * 8, 16},
|
||||
"-"};
|
||||
|
||||
Text text_owner{
|
||||
{0 * 8, 12 * 16, 30 * 8, 16},
|
||||
"-"};
|
||||
|
||||
Text text_operator{
|
||||
{0 * 8, 14 * 16, 30 * 8, 16},
|
||||
"-"};
|
||||
|
||||
Button button_close{
|
||||
{9 * 8, 16 * 16, 12 * 8, 3 * 16},
|
||||
"Back"};
|
||||
};
|
||||
|
||||
|
||||
/* Shows detailed information about an aircraft's flight. */
|
||||
class ADSBRxDetailsView : public View {
|
||||
public:
|
||||
ADSBRxDetailsView(NavigationView&, const AircraftRecentEntry& entry, const std::function<void(void)> on_close);
|
||||
~ADSBRxDetailsView();
|
||||
public:
|
||||
ADSBRxDetailsView(NavigationView&, const AircraftRecentEntry& entry);
|
||||
|
||||
ADSBRxDetailsView(const ADSBRxDetailsView&) = delete;
|
||||
ADSBRxDetailsView(ADSBRxDetailsView&&) = delete;
|
||||
ADSBRxDetailsView& operator=(const ADSBRxDetailsView&) = delete;
|
||||
ADSBRxDetailsView& operator=(ADSBRxDetailsView&&) = delete;
|
||||
|
||||
void focus() override;
|
||||
|
||||
void update(const AircraftRecentEntry& entry);
|
||||
|
||||
std::string title() const override { return "Details"; };
|
||||
ADSBRxDetailsView(const ADSBRxDetailsView&) = delete;
|
||||
ADSBRxDetailsView& operator=(const ADSBRxDetailsView&) = delete;
|
||||
|
||||
AircraftRecentEntry get_current_entry() { return entry_copy; }
|
||||
void focus() override;
|
||||
void update(const AircraftRecentEntry& entry);
|
||||
|
||||
/* Calls forwarded to map view if shown. */
|
||||
bool map_active() const { return geomap_view_; }
|
||||
void clear_map_markers();
|
||||
/* Adds a marker for the entry to the map. Returns true on success. */
|
||||
bool add_map_marker(const AircraftRecentEntry& entry);
|
||||
|
||||
std::database::AirlinesDBRecord airline_record = {};
|
||||
|
||||
GeoMapView* geomap_view { nullptr };
|
||||
private:
|
||||
AircraftRecentEntry entry_copy { 0 };
|
||||
std::function<void(void)> on_close_ { };
|
||||
ADSBRxAircraftDetailsView* aircraft_details_view { nullptr };
|
||||
bool send_updates { false };
|
||||
std::database db = { };
|
||||
std::string airline_code = "";
|
||||
int return_code = 0;
|
||||
|
||||
Labels labels {
|
||||
{ { 0 * 8, 1 * 16 }, "ICAO:", Color::light_grey() },
|
||||
{ { 13 * 8, 1 * 16 }, "Callsign:", Color::light_grey() },
|
||||
{ { 0 * 8, 2 * 16 }, "Last seen:", Color::light_grey() },
|
||||
{ { 0 * 8, 3 * 16 }, "Airline:", Color::light_grey() },
|
||||
{ { 0 * 8, 5 * 16 }, "Country:", Color::light_grey() },
|
||||
{ { 0 * 8, 13 * 16 }, "Even position frame:", Color::light_grey() },
|
||||
{ { 0 * 8, 15 * 16 }, "Odd position frame:", Color::light_grey() }
|
||||
};
|
||||
|
||||
Text text_icao_address {
|
||||
{ 5 * 8, 1 * 16, 6 * 8, 16},
|
||||
"-"
|
||||
};
|
||||
std::string title() const override { return "Details"; }
|
||||
|
||||
Text text_callsign {
|
||||
{ 22 * 8, 1 * 16, 8 * 8, 16 },
|
||||
"-"
|
||||
};
|
||||
|
||||
Text text_last_seen {
|
||||
{ 11 * 8, 2 * 16, 19 * 8, 16 },
|
||||
"-"
|
||||
};
|
||||
|
||||
Text text_airline {
|
||||
{ 0 * 8, 4 * 16, 30 * 8, 16 },
|
||||
"-"
|
||||
};
|
||||
|
||||
Text text_country {
|
||||
{ 8 * 8, 5 * 16, 22 * 8, 16 },
|
||||
"-"
|
||||
};
|
||||
|
||||
Text text_infos {
|
||||
{ 0 * 8, 6 * 16, 30 * 8, 16 },
|
||||
"-"
|
||||
};
|
||||
private:
|
||||
void refresh_ui();
|
||||
void on_gps(const GPSPosDataMessage* msg);
|
||||
void on_orientation(const OrientationDataMessage* msg);
|
||||
|
||||
Text text_info2 {
|
||||
{0*8, 7*16, 30*8, 16},
|
||||
"-"
|
||||
};
|
||||
GeoMapView* geomap_view_{nullptr};
|
||||
ADSBRxAircraftDetailsView* aircraft_details_view_{nullptr};
|
||||
|
||||
Text text_frame_pos_even {
|
||||
{ 0 * 8, 14 * 16, 30 * 8, 16 },
|
||||
"-"
|
||||
};
|
||||
Text text_frame_pos_odd {
|
||||
{ 0 * 8, 16 * 16, 30 * 8, 16 },
|
||||
"-"
|
||||
};
|
||||
|
||||
Button button_aircraft_details {
|
||||
{ 2 * 8, 9 * 16, 12 * 8, 3 * 16 },
|
||||
"A/C details"
|
||||
};
|
||||
// NB: Keeping a copy so that it doesn't end up dangling
|
||||
// if removed from the recent entries list.
|
||||
AircraftRecentEntry entry_{AircraftRecentEntry::invalid_key};
|
||||
bool airline_checked{false};
|
||||
|
||||
Button button_see_map {
|
||||
{ 16 * 8, 9 * 16, 12 * 8, 3 * 16 },
|
||||
"See on map"
|
||||
};
|
||||
Labels labels{
|
||||
{{0 * 8, 1 * 16}, "ICAO:", Theme::getInstance()->fg_light->foreground},
|
||||
{{13 * 8, 1 * 16}, "Callsign:", Theme::getInstance()->fg_light->foreground},
|
||||
{{0 * 8, 2 * 16}, "Last seen:", Theme::getInstance()->fg_light->foreground},
|
||||
{{0 * 8, 3 * 16}, "Airline:", Theme::getInstance()->fg_light->foreground},
|
||||
{{0 * 8, 5 * 16}, "Country:", Theme::getInstance()->fg_light->foreground},
|
||||
{{0 * 8, 13 * 16}, "Even position frame:", Theme::getInstance()->fg_light->foreground},
|
||||
{{0 * 8, 15 * 16}, "Odd position frame:", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
Text text_icao_address{
|
||||
{5 * 8, 1 * 16, 6 * 8, 16},
|
||||
"-"};
|
||||
|
||||
Text text_callsign{
|
||||
{22 * 8, 1 * 16, 8 * 8, 16},
|
||||
"-"};
|
||||
|
||||
Text text_last_seen{
|
||||
{11 * 8, 2 * 16, 19 * 8, 16},
|
||||
"-"};
|
||||
|
||||
Text text_airline{
|
||||
{0 * 8, 4 * 16, 30 * 8, 16},
|
||||
"-"};
|
||||
|
||||
Text text_country{
|
||||
{8 * 8, 5 * 16, 22 * 8, 16},
|
||||
"-"};
|
||||
|
||||
Text text_infos{
|
||||
{0 * 8, 6 * 16, 30 * 8, 16},
|
||||
"-"};
|
||||
|
||||
Text text_info2{
|
||||
{0 * 8, 7 * 16, 30 * 8, 16},
|
||||
"-"};
|
||||
|
||||
Text text_frame_pos_even{
|
||||
{0 * 8, 14 * 16, 30 * 8, 16},
|
||||
"-"};
|
||||
Text text_frame_pos_odd{
|
||||
{0 * 8, 16 * 16, 30 * 8, 16},
|
||||
"-"};
|
||||
|
||||
Button button_aircraft_details{
|
||||
{2 * 8, 9 * 16, 12 * 8, 3 * 16},
|
||||
"A/C details"};
|
||||
|
||||
Button button_see_map{
|
||||
{16 * 8, 9 * 16, 12 * 8, 3 * 16},
|
||||
"See on map"};
|
||||
|
||||
MessageHandlerRegistration message_handler_gps{
|
||||
Message::ID::GPSPosData,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const GPSPosDataMessage*>(p);
|
||||
this->on_gps(message);
|
||||
}};
|
||||
MessageHandlerRegistration message_handler_orientation{
|
||||
Message::ID::OrientationData,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const OrientationDataMessage*>(p);
|
||||
this->on_orientation(message);
|
||||
}};
|
||||
};
|
||||
|
||||
|
||||
/* Main ADSB application view and message dispatch. */
|
||||
class ADSBRxView : public View {
|
||||
public:
|
||||
ADSBRxView(NavigationView& nav);
|
||||
~ADSBRxView();
|
||||
|
||||
ADSBRxView(const ADSBRxView&) = delete;
|
||||
ADSBRxView(ADSBRxView&&) = delete;
|
||||
ADSBRxView& operator=(const ADSBRxView&) = delete;
|
||||
ADSBRxView& operator=(ADSBRxView&&) = delete;
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "ADS-B RX"; };
|
||||
public:
|
||||
ADSBRxView(NavigationView& nav);
|
||||
~ADSBRxView();
|
||||
|
||||
void replace_entry(AircraftRecentEntry & entry);
|
||||
void remove_old_entries();
|
||||
AircraftRecentEntry find_or_create_entry(uint32_t ICAO_address);
|
||||
void sort_entries_by_state();
|
||||
ADSBRxView(const ADSBRxView&) = delete;
|
||||
ADSBRxView(ADSBRxView&&) = delete;
|
||||
ADSBRxView& operator=(const ADSBRxView&) = delete;
|
||||
ADSBRxView& operator=(ADSBRxView&&) = delete;
|
||||
|
||||
private:
|
||||
rf::Frequency prevFreq = { 0 };
|
||||
std::unique_ptr<ADSBLogger> logger { };
|
||||
void on_frame(const ADSBFrameMessage * message);
|
||||
void on_tick_second();
|
||||
int updateState = { 0 };
|
||||
void updateRecentEntries();
|
||||
void updateDetailsAndMap(int ageStep);
|
||||
void focus() override;
|
||||
std::string title() const override { return "ADS-B RX"; };
|
||||
|
||||
#define MARKER_UPDATE_SECONDS (5)
|
||||
int ticksSinceMarkerRefresh { MARKER_UPDATE_SECONDS-1 };
|
||||
// app save settings
|
||||
std::app_settings settings { };
|
||||
std::app_settings::AppSettings app_settings { };
|
||||
|
||||
const RecentEntriesColumns columns { {
|
||||
{ "ICAO/Call", 9 },
|
||||
{ "Lvl", 3 },
|
||||
{ "Spd", 3 },
|
||||
{ "Amp", 3 },
|
||||
{ "Hit", 3 },
|
||||
{ "Age", 4 }
|
||||
} };
|
||||
AircraftRecentEntries recent { };
|
||||
RecentEntriesView<RecentEntries<AircraftRecentEntry>> recent_entries_view { columns, recent };
|
||||
|
||||
SignalToken signal_token_tick_second { };
|
||||
ADSBRxDetailsView* details_view { nullptr };
|
||||
uint32_t detailed_entry_key { 0 };
|
||||
bool send_updates { false };
|
||||
|
||||
Labels labels {
|
||||
{ { 0 * 8, 0 * 8 }, "LNA: VGA: AMP:", Color::light_grey() }
|
||||
};
|
||||
|
||||
LNAGainField field_lna {
|
||||
{ 4 * 8, 0 * 16 }
|
||||
};
|
||||
private:
|
||||
RxRadioState radio_state_{
|
||||
1'090'000'000 /* frequency */,
|
||||
2'500'000 /* bandwidth */,
|
||||
2'000'000 /* sampling rate */,
|
||||
ReceiverModel::Mode::SpectrumAnalysis};
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_adsb", app_settings::Mode::RX};
|
||||
|
||||
VGAGainField field_vga {
|
||||
{ 11 * 8, 0 * 16 }
|
||||
};
|
||||
|
||||
RFAmpField field_rf_amp {
|
||||
{ 18 * 8, 0 * 16 }
|
||||
};
|
||||
|
||||
RSSI rssi {
|
||||
{ 20 * 8, 4, 10 * 8, 8 },
|
||||
};
|
||||
|
||||
MessageHandlerRegistration message_handler_frame {
|
||||
Message::ID::ADSBFrame,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const ADSBFrameMessage*>(p);
|
||||
this->on_frame(message);
|
||||
}
|
||||
};
|
||||
std::unique_ptr<ADSBLogger> logger{};
|
||||
|
||||
/* Event Handlers */
|
||||
void on_frame(const ADSBFrameMessage* message);
|
||||
void on_tick_second();
|
||||
void refresh_ui();
|
||||
|
||||
SignalToken signal_token_tick_second{};
|
||||
uint8_t tick_count = 0;
|
||||
uint16_t ticks_since_marker_refresh{MARKER_UPDATE_SECONDS};
|
||||
|
||||
/* Max number of entries that can be updated in a single pass.
|
||||
* 16 is one screen of recent entries. */
|
||||
static constexpr uint8_t max_update_entries = 16;
|
||||
|
||||
/* Recent Entries */
|
||||
const RecentEntriesColumns columns{
|
||||
{{"ICAO/Call", 9},
|
||||
{"Lvl", 3},
|
||||
{"Spd", 3},
|
||||
{"Amp", 3},
|
||||
{"Hit", 3},
|
||||
{"Age", 4}}};
|
||||
AircraftRecentEntries recent{};
|
||||
RecentEntriesView<AircraftRecentEntries> recent_entries_view{columns, recent};
|
||||
|
||||
/* Entry Management */
|
||||
void update_recent_entries(int age_delta);
|
||||
AircraftRecentEntry& find_or_create_entry(uint32_t ICAO_address);
|
||||
void sort_entries_by_state();
|
||||
void remove_expired_entries();
|
||||
|
||||
/* The key of the entry in the details view if shown. */
|
||||
AircraftRecentEntry::Key detail_key{AircraftRecentEntry::invalid_key};
|
||||
ADSBRxDetailsView* details_view{nullptr};
|
||||
|
||||
Labels labels{
|
||||
{{0 * 8, 0 * 8}, "LNA: VGA: AMP:", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
LNAGainField field_lna{
|
||||
{4 * 8, 0 * 16}};
|
||||
|
||||
VGAGainField field_vga{
|
||||
{11 * 8, 0 * 16}};
|
||||
|
||||
RFAmpField field_rf_amp{
|
||||
{18 * 8, 0 * 16}};
|
||||
|
||||
RSSI rssi{
|
||||
{20 * 8, 4, 7 * 8, 8},
|
||||
};
|
||||
|
||||
ActivityDot status_frame{
|
||||
{27 * 8 + 2, 5, 2, 2},
|
||||
Theme::getInstance()->bg_darkest->foreground,
|
||||
};
|
||||
|
||||
ActivityDot status_good_frame{
|
||||
{27 * 8 + 2, 9, 2, 2},
|
||||
Theme::getInstance()->fg_green->foreground,
|
||||
};
|
||||
|
||||
AudioVolumeField field_volume{
|
||||
{28 * 8, 0 * 16}};
|
||||
|
||||
MessageHandlerRegistration message_handler_frame{
|
||||
Message::ID::ADSBFrame,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const ADSBFrameMessage*>(p);
|
||||
this->on_frame(message);
|
||||
}};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -1,375 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2016 Furrtek
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "ui_adsb_tx.hpp"
|
||||
#include "ui_alphanum.hpp"
|
||||
|
||||
#include "manchester.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "cpld_update.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <stdio.h>
|
||||
|
||||
using namespace adsb;
|
||||
using namespace portapack;
|
||||
|
||||
namespace ui {
|
||||
|
||||
Compass::Compass(
|
||||
const Point parent_pos
|
||||
) : Widget { { parent_pos, { 64, 64 } } }
|
||||
{
|
||||
}
|
||||
|
||||
void Compass::set_value(uint32_t new_value) {
|
||||
Point center = screen_pos() + Point(32, 32);
|
||||
|
||||
new_value = range.clip(new_value);
|
||||
|
||||
display.draw_line(
|
||||
center,
|
||||
center + polar_to_point(value_, 28),
|
||||
Color::dark_grey()
|
||||
);
|
||||
|
||||
display.draw_line(
|
||||
center,
|
||||
center + polar_to_point(new_value, 28),
|
||||
Color::green()
|
||||
);
|
||||
|
||||
value_ = new_value;
|
||||
}
|
||||
|
||||
void Compass::paint(Painter&) {
|
||||
display.fill_circle(screen_pos() + Point(32, 32), 32, Color::dark_grey(), Color::black());
|
||||
|
||||
display.fill_rectangle({ screen_pos() + Point(32 - 2, 0), { 4, 4 } }, Color::black()); // N
|
||||
display.fill_rectangle({ screen_pos() + Point(32 - 2, 64 - 4), { 4, 4 } }, Color::black()); // S
|
||||
display.fill_rectangle({ screen_pos() + Point(0, 32 - 2), { 4, 4 } }, Color::black()); // W
|
||||
display.fill_rectangle({ screen_pos() + Point(64 - 4, 32 - 2), { 4, 4 } }, Color::black()); // E
|
||||
|
||||
set_value(value_);
|
||||
}
|
||||
|
||||
ADSBPositionView::ADSBPositionView(
|
||||
NavigationView& nav, Rect parent_rect
|
||||
) : OptionTabView(parent_rect)
|
||||
{
|
||||
set_type("position");
|
||||
|
||||
add_children({
|
||||
&geopos,
|
||||
&button_set_map
|
||||
});
|
||||
|
||||
geopos.set_altitude(36000);
|
||||
|
||||
button_set_map.on_select = [this, &nav](Button&) {
|
||||
nav.push<GeoMapView>(
|
||||
geopos.altitude(),
|
||||
GeoPos::alt_unit::FEET,
|
||||
geopos.lat(),
|
||||
geopos.lon(),
|
||||
[this](int32_t altitude, float lat, float lon) {
|
||||
geopos.set_altitude(altitude);
|
||||
geopos.set_lat(lat);
|
||||
geopos.set_lon(lon);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
void ADSBPositionView::collect_frames(const uint32_t ICAO_address, std::vector<ADSBFrame>& frame_list) {
|
||||
if (!enabled) return;
|
||||
|
||||
ADSBFrame temp_frame;
|
||||
|
||||
encode_frame_pos(temp_frame, ICAO_address, geopos.altitude(),
|
||||
geopos.lat(), geopos.lon(), 0);
|
||||
|
||||
frame_list.emplace_back(temp_frame);
|
||||
|
||||
encode_frame_pos(temp_frame, ICAO_address, geopos.altitude(),
|
||||
geopos.lat(), geopos.lon(), 1);
|
||||
|
||||
frame_list.emplace_back(temp_frame);
|
||||
}
|
||||
|
||||
ADSBCallsignView::ADSBCallsignView(
|
||||
NavigationView& nav, Rect parent_rect
|
||||
) : OptionTabView(parent_rect)
|
||||
{
|
||||
set_type("callsign");
|
||||
|
||||
add_children({
|
||||
&labels_callsign,
|
||||
&button_callsign
|
||||
});
|
||||
|
||||
set_enabled(true);
|
||||
|
||||
button_callsign.set_text(callsign);
|
||||
|
||||
button_callsign.on_select = [this, &nav](Button&) {
|
||||
text_prompt(
|
||||
nav,
|
||||
callsign,
|
||||
8,
|
||||
[this](std::string& s) {
|
||||
button_callsign.set_text(s);
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
void ADSBCallsignView::collect_frames(const uint32_t ICAO_address, std::vector<ADSBFrame>& frame_list) {
|
||||
if (!enabled) return;
|
||||
|
||||
ADSBFrame temp_frame;
|
||||
|
||||
encode_frame_id(temp_frame, ICAO_address, callsign);
|
||||
|
||||
frame_list.emplace_back(temp_frame);
|
||||
}
|
||||
|
||||
ADSBSpeedView::ADSBSpeedView(
|
||||
Rect parent_rect
|
||||
) : OptionTabView(parent_rect)
|
||||
{
|
||||
set_type("speed");
|
||||
|
||||
add_children({
|
||||
&labels_speed,
|
||||
&labels_vert_rate,
|
||||
&compass,
|
||||
&field_angle,
|
||||
&field_speed,
|
||||
&field_vert_rate
|
||||
});
|
||||
|
||||
field_angle.set_value(0);
|
||||
field_speed.set_value(400);
|
||||
field_vert_rate.set_value(0);
|
||||
|
||||
field_angle.on_change = [this](int32_t v) {
|
||||
compass.set_value(v);
|
||||
};
|
||||
}
|
||||
|
||||
void ADSBSpeedView::collect_frames(const uint32_t ICAO_address, std::vector<ADSBFrame>& frame_list) {
|
||||
if (!enabled) return;
|
||||
|
||||
ADSBFrame temp_frame;
|
||||
|
||||
encode_frame_velo(temp_frame, ICAO_address, field_speed.value(),
|
||||
field_angle.value(), field_vert_rate.value()); // Added v_rate , ft/min , (+) climb , (-) descend .
|
||||
|
||||
frame_list.emplace_back(temp_frame);
|
||||
}
|
||||
|
||||
ADSBSquawkView::ADSBSquawkView(
|
||||
Rect parent_rect
|
||||
) : OptionTabView(parent_rect)
|
||||
{
|
||||
set_type("squawk");
|
||||
|
||||
add_children({
|
||||
&labels_squawk,
|
||||
&field_squawk
|
||||
});
|
||||
}
|
||||
|
||||
void ADSBSquawkView::collect_frames(const uint32_t ICAO_address, std::vector<ADSBFrame>& frame_list) {
|
||||
if (!enabled) return;
|
||||
|
||||
ADSBFrame temp_frame;
|
||||
(void)ICAO_address;
|
||||
|
||||
encode_frame_squawk(temp_frame, field_squawk.concatenate_4_octal_u16());
|
||||
|
||||
frame_list.emplace_back(temp_frame);
|
||||
}
|
||||
|
||||
ADSBTXThread::ADSBTXThread(
|
||||
std::vector<ADSBFrame> frames
|
||||
) : frames_ { std::move(frames) }
|
||||
{
|
||||
thread = chThdCreateFromHeap(NULL, 1024, NORMALPRIO + 10, ADSBTXThread::static_fn, this);
|
||||
}
|
||||
|
||||
ADSBTXThread::~ADSBTXThread() {
|
||||
if( thread ) {
|
||||
chThdTerminate(thread);
|
||||
chThdWait(thread);
|
||||
thread = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
msg_t ADSBTXThread::static_fn(void* arg) {
|
||||
auto obj = static_cast<ADSBTXThread*>(arg);
|
||||
obj->run();
|
||||
return 0;
|
||||
}
|
||||
|
||||
void ADSBTXThread::run() {
|
||||
uint8_t * bin_ptr = shared_memory.bb_data.data;
|
||||
uint8_t * raw_ptr;
|
||||
uint32_t frame_index = 0; //, plane_index = 0;
|
||||
//uint32_t regen = 0;
|
||||
//float offs = 0;
|
||||
|
||||
while( !chThdShouldTerminate() ) {
|
||||
/*if (!regen) {
|
||||
regen = 10;
|
||||
|
||||
encode_frame_id(frames[0], plane_index, "DEMO" + to_string_dec_uint(plane_index));
|
||||
encode_frame_pos(frames[1], plane_index, 5000, plane_lats[plane_index]/8 + offs + 38.5, plane_lons[plane_index]/8 + 125.8, 0);
|
||||
encode_frame_pos(frames[2], plane_index, 5000, plane_lats[plane_index]/8 + offs + 38.5, plane_lons[plane_index]/8 + 125.8, 1);
|
||||
encode_frame_identity(frames[3], plane_index, 1337);
|
||||
|
||||
if (plane_index == 11)
|
||||
plane_index = 0;
|
||||
else
|
||||
plane_index++;
|
||||
|
||||
offs += 0.001;
|
||||
}*/
|
||||
|
||||
memset(bin_ptr, 0, 256); // 112 bits * 2 parts = 224 should be enough
|
||||
|
||||
raw_ptr = frames_[frame_index].get_raw_data();
|
||||
|
||||
// The preamble isn't manchester encoded
|
||||
memcpy(bin_ptr, adsb_preamble, 16);
|
||||
|
||||
// Convert to binary (1 byte per bit, faster for baseband code)
|
||||
manchester_encode(bin_ptr + 16, raw_ptr, 112, 0);
|
||||
|
||||
// Display in hex for debug
|
||||
//text_frame.set(to_string_hex_array(frames[0].get_raw_data(), 14));
|
||||
|
||||
baseband::set_adsb();
|
||||
|
||||
chThdSleepMilliseconds(50);
|
||||
|
||||
frame_index++;
|
||||
if (frame_index >= frames_.size()) {
|
||||
frame_index = 0;
|
||||
//if (regen)
|
||||
// regen--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ADSBTxView::focus() {
|
||||
tab_view.focus();
|
||||
}
|
||||
|
||||
ADSBTxView::~ADSBTxView() {
|
||||
|
||||
// save app settings
|
||||
app_settings.tx_frequency = transmitter_model.tuning_frequency();
|
||||
settings.save("tx_adsb", &app_settings);
|
||||
|
||||
transmitter_model.disable();
|
||||
hackrf::cpld::load_sram_no_verify(); // to leave all RX ok, withouth ghost signal problem at the exit .
|
||||
baseband::shutdown(); // better this function at the end, not load_sram() that sometimes produces hang up.
|
||||
}
|
||||
|
||||
void ADSBTxView::generate_frames() {
|
||||
const uint32_t ICAO_address = sym_icao.value_hex_u64();
|
||||
|
||||
frames.clear();
|
||||
|
||||
/* This scheme kinda sucks. Each "tab"'s collect_frames method
|
||||
* is called to generate its related frame(s). Getting values
|
||||
* from each widget of each tab would be better ?
|
||||
* */
|
||||
view_position.collect_frames(ICAO_address, frames);
|
||||
view_callsign.collect_frames(ICAO_address, frames);
|
||||
view_speed.collect_frames(ICAO_address, frames);
|
||||
view_squawk.collect_frames(ICAO_address, frames);
|
||||
|
||||
// Show how many frames were generated
|
||||
//text_frame.set(to_string_dec_uint(frames.size()) + " frame(s).");
|
||||
}
|
||||
|
||||
void ADSBTxView::start_tx() {
|
||||
generate_frames();
|
||||
|
||||
transmitter_model.set_sampling_rate(4000000U);
|
||||
transmitter_model.set_baseband_bandwidth(10000000);
|
||||
transmitter_model.enable();
|
||||
|
||||
baseband::set_adsb();
|
||||
|
||||
tx_thread = std::make_unique<ADSBTXThread>(frames);
|
||||
}
|
||||
|
||||
ADSBTxView::ADSBTxView(
|
||||
NavigationView& nav
|
||||
) : nav_ { nav }
|
||||
{
|
||||
baseband::run_image(portapack::spi_flash::image_tag_adsb_tx);
|
||||
|
||||
add_children({
|
||||
&tab_view,
|
||||
&labels,
|
||||
&sym_icao,
|
||||
&view_position,
|
||||
&view_callsign,
|
||||
&view_speed,
|
||||
&view_squawk,
|
||||
&text_frame,
|
||||
&tx_view
|
||||
});
|
||||
|
||||
// load app settings
|
||||
auto rc = settings.load("tx_adsb", &app_settings);
|
||||
if(rc == SETTINGS_OK) {
|
||||
transmitter_model.set_tuning_frequency(app_settings.tx_frequency);
|
||||
transmitter_model.set_rf_amp(app_settings.tx_amp);
|
||||
transmitter_model.set_tx_gain(app_settings.tx_gain);
|
||||
}
|
||||
|
||||
tx_view.on_edit_frequency = [this, &nav]() {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(receiver_model.tuning_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
transmitter_model.set_tuning_frequency(f);
|
||||
};
|
||||
};
|
||||
|
||||
tx_view.on_start = [this]() {
|
||||
start_tx();
|
||||
tx_view.set_transmitting(true);
|
||||
};
|
||||
|
||||
tx_view.on_stop = [this]() {
|
||||
tx_view.set_transmitting(false);
|
||||
transmitter_model.disable();
|
||||
};
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
@@ -1,251 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2016 Furrtek
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "adsb.hpp"
|
||||
#include "ui_textentry.hpp"
|
||||
#include "ui_geomap.hpp"
|
||||
#include "ui_tabview.hpp"
|
||||
#include "ui_transmitter.hpp"
|
||||
#include "message.hpp"
|
||||
#include "transmitter_model.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "portapack.hpp"
|
||||
|
||||
using namespace adsb;
|
||||
|
||||
namespace ui {
|
||||
|
||||
class Compass : public Widget {
|
||||
public:
|
||||
Compass(const Point parent_pos);
|
||||
|
||||
void set_value(uint32_t new_value);
|
||||
|
||||
void paint(Painter&) override;
|
||||
|
||||
private:
|
||||
const range_t<uint32_t> range { 0, 359 };
|
||||
uint32_t value_ { 0 };
|
||||
};
|
||||
|
||||
class ADSBPositionView : public OptionTabView {
|
||||
public:
|
||||
ADSBPositionView(NavigationView& nav, Rect parent_rect);
|
||||
|
||||
void collect_frames(const uint32_t ICAO_address, std::vector<ADSBFrame>& frame_list);
|
||||
|
||||
private:
|
||||
GeoPos geopos {
|
||||
{ 0, 2 * 16 },
|
||||
GeoPos::FEET
|
||||
};
|
||||
|
||||
Button button_set_map {
|
||||
{ 8 * 8, 6 * 16, 14 * 8, 2 * 16 },
|
||||
"Set from map"
|
||||
};
|
||||
};
|
||||
|
||||
class ADSBCallsignView : public OptionTabView {
|
||||
public:
|
||||
ADSBCallsignView(NavigationView& nav, Rect parent_rect);
|
||||
|
||||
void collect_frames(const uint32_t ICAO_address, std::vector<ADSBFrame>& frame_list);
|
||||
|
||||
private:
|
||||
std::string callsign = "TEST1234";
|
||||
|
||||
Labels labels_callsign {
|
||||
{ { 2 * 8, 5 * 8 }, "Callsign:", Color::light_grey() }
|
||||
};
|
||||
|
||||
Button button_callsign {
|
||||
{ 12 * 8, 2 * 16, 10 * 8, 2 * 16 },
|
||||
""
|
||||
};
|
||||
};
|
||||
|
||||
class ADSBSpeedView : public OptionTabView {
|
||||
public:
|
||||
ADSBSpeedView(Rect parent_rect);
|
||||
|
||||
void collect_frames(const uint32_t ICAO_address, std::vector<ADSBFrame>& frame_list);
|
||||
|
||||
private:
|
||||
Labels labels_speed {
|
||||
{ { 1 * 8, 6 * 16 }, "Speed: kn Bearing: *", Color::light_grey() }
|
||||
};
|
||||
|
||||
Labels labels_vert_rate {
|
||||
{ { 1 * 8, 8 * 16 }, "Vert. rate: ft/min, (+/-)", Color::light_grey() }
|
||||
};
|
||||
|
||||
Compass compass {
|
||||
{ 21 * 8, 2 * 16 }
|
||||
};
|
||||
|
||||
NumberField field_angle {
|
||||
{ 21 * 8 + 20, 6 * 16 }, 3, { 0, 359 }, 1, ' ', true
|
||||
};
|
||||
|
||||
NumberField field_speed {
|
||||
{ 8 * 8, 6 * 16 }, 3, { 0, 999 }, 5, ' '
|
||||
};
|
||||
|
||||
NumberField field_vert_rate {
|
||||
{ 11 * 8, 8 * 16 }, 5, { -4096, 4096 }, 64, ' ' // Let's limit to +/-5k aprox , Ex. max safe descent vertical rate aprox -1000 ft/min on an instrument approach. , std step is 64
|
||||
};
|
||||
};
|
||||
|
||||
class ADSBSquawkView : public OptionTabView {
|
||||
public:
|
||||
ADSBSquawkView(Rect parent_rect);
|
||||
|
||||
void collect_frames(const uint32_t ICAO_address, std::vector<ADSBFrame>& frame_list);
|
||||
|
||||
private:
|
||||
Labels labels_squawk {
|
||||
{ { 2 * 8, 2 * 16 }, "Squawk:", Color::light_grey() }
|
||||
};
|
||||
|
||||
SymField field_squawk {
|
||||
{ 10 * 8, 2 * 16 },
|
||||
4,
|
||||
SymField::SYMFIELD_OCT
|
||||
};
|
||||
};
|
||||
|
||||
class ADSBTXThread {
|
||||
public:
|
||||
ADSBTXThread(std::vector<ADSBFrame> frames);
|
||||
~ADSBTXThread();
|
||||
|
||||
ADSBTXThread(const ADSBTXThread&) = delete;
|
||||
ADSBTXThread(ADSBTXThread&&) = delete;
|
||||
ADSBTXThread& operator=(const ADSBTXThread&) = delete;
|
||||
ADSBTXThread& operator=(ADSBTXThread&&) = delete;
|
||||
|
||||
private:
|
||||
std::vector<ADSBFrame> frames_ { };
|
||||
Thread* thread { nullptr };
|
||||
|
||||
static msg_t static_fn(void* arg);
|
||||
|
||||
void run();
|
||||
};
|
||||
|
||||
class ADSBTxView : public View {
|
||||
public:
|
||||
ADSBTxView(NavigationView& nav);
|
||||
~ADSBTxView();
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "ADS-B TX"; };
|
||||
|
||||
private:
|
||||
/*enum tx_modes {
|
||||
IDLE = 0,
|
||||
SINGLE,
|
||||
SEQUENCE
|
||||
};*/
|
||||
|
||||
/*const float plane_lats[12] = {
|
||||
0,
|
||||
-1,
|
||||
-2,
|
||||
-3,
|
||||
-4,
|
||||
-5,
|
||||
-4.5,
|
||||
-5,
|
||||
-4,
|
||||
-3,
|
||||
-2,
|
||||
-1
|
||||
};
|
||||
const float plane_lons[12] = {
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
2,
|
||||
1,
|
||||
0,
|
||||
-1,
|
||||
-2,
|
||||
-1,
|
||||
-1,
|
||||
-1
|
||||
};*/
|
||||
|
||||
// app save settings
|
||||
std::app_settings settings { };
|
||||
std::app_settings::AppSettings app_settings { };
|
||||
|
||||
//tx_modes tx_mode = IDLE;
|
||||
NavigationView& nav_;
|
||||
std::vector<ADSBFrame> frames { };
|
||||
|
||||
void start_tx();
|
||||
void generate_frames();
|
||||
|
||||
Rect view_rect = { 0, 7 * 8, 240, 192 };
|
||||
|
||||
ADSBPositionView view_position { nav_, view_rect };
|
||||
ADSBCallsignView view_callsign { nav_, view_rect };
|
||||
ADSBSpeedView view_speed { view_rect };
|
||||
ADSBSquawkView view_squawk { view_rect };
|
||||
|
||||
TabView tab_view {
|
||||
{ "Position", Color::cyan(), &view_position },
|
||||
{ "Callsign", Color::green(), &view_callsign },
|
||||
{ "Speed", Color::yellow(), &view_speed },
|
||||
{ "Squawk", Color::orange(), &view_squawk }
|
||||
};
|
||||
|
||||
Labels labels {
|
||||
{ { 2 * 8, 4 * 8 }, "ICAO24:", Color::light_grey() }
|
||||
};
|
||||
|
||||
SymField sym_icao {
|
||||
{ 10 * 8, 4 * 8 },
|
||||
6,
|
||||
SymField::SYMFIELD_HEX
|
||||
};
|
||||
|
||||
Text text_frame {
|
||||
{ 1 * 8, 29 * 8, 14 * 8, 16 },
|
||||
"-"
|
||||
};
|
||||
|
||||
TransmitterView tx_view {
|
||||
16 * 16,
|
||||
1000000,
|
||||
0
|
||||
};
|
||||
|
||||
std::unique_ptr<ADSBTXThread> tx_thread { };
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
@@ -1,186 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2017 Furrtek
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "ui_afsk_rx.hpp"
|
||||
#include "ui_modemsetup.hpp"
|
||||
|
||||
#include "modems.hpp"
|
||||
#include "audio.hpp"
|
||||
#include "rtc_time.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
using namespace modems;
|
||||
|
||||
void AFSKLogger::log_raw_data(const std::string& data) {
|
||||
rtc::RTC datetime;
|
||||
rtcGetTime(&RTCD1, &datetime);
|
||||
|
||||
log_file.write_entry(datetime, data);
|
||||
}
|
||||
|
||||
namespace ui {
|
||||
|
||||
void AFSKRxView::focus() {
|
||||
field_frequency.focus();
|
||||
}
|
||||
|
||||
void AFSKRxView::update_freq(rf::Frequency f) {
|
||||
receiver_model.set_tuning_frequency(f);
|
||||
}
|
||||
|
||||
AFSKRxView::AFSKRxView(NavigationView& nav) {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_afsk_rx);
|
||||
|
||||
add_children({
|
||||
&rssi,
|
||||
&channel,
|
||||
&field_rf_amp,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&field_frequency,
|
||||
&text_debug,
|
||||
&button_modem_setup,
|
||||
&record_view,
|
||||
&console
|
||||
});
|
||||
|
||||
// load app settings
|
||||
auto rc = settings.load("rx_afsk", &app_settings);
|
||||
if(rc == SETTINGS_OK) {
|
||||
field_lna.set_value(app_settings.lna);
|
||||
field_vga.set_value(app_settings.vga);
|
||||
field_rf_amp.set_value(app_settings.rx_amp);
|
||||
}
|
||||
|
||||
|
||||
// DEBUG
|
||||
record_view.on_error = [&nav](std::string message) {
|
||||
nav.display_modal("Error", message);
|
||||
};
|
||||
record_view.set_sampling_rate(24000);
|
||||
|
||||
// Auto-configure modem for LCR RX (will be removed later)
|
||||
update_freq(467225500); // 462713300
|
||||
auto def_bell202 = &modem_defs[0];
|
||||
persistent_memory::set_modem_baudrate(def_bell202->baudrate);
|
||||
serial_format_t serial_format;
|
||||
serial_format.data_bits = 7;
|
||||
serial_format.parity = EVEN;
|
||||
serial_format.stop_bits = 1;
|
||||
serial_format.bit_order = LSB_FIRST;
|
||||
persistent_memory::set_serial_format(serial_format);
|
||||
|
||||
field_frequency.set_value(receiver_model.tuning_frequency());
|
||||
field_frequency.set_step(100);
|
||||
field_frequency.on_change = [this](rf::Frequency f) {
|
||||
update_freq(f);
|
||||
};
|
||||
field_frequency.on_edit = [this, &nav]() {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(receiver_model.tuning_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
update_freq(f);
|
||||
field_frequency.set_value(f);
|
||||
};
|
||||
};
|
||||
|
||||
button_modem_setup.on_select = [&nav](Button&) {
|
||||
nav.push<ModemSetupView>();
|
||||
};
|
||||
|
||||
logger = std::make_unique<AFSKLogger>();
|
||||
if (logger)
|
||||
logger->append("AFSK_LOG.TXT");
|
||||
|
||||
// Auto-configure modem for LCR RX (will be removed later)
|
||||
baseband::set_afsk(persistent_memory::modem_baudrate(), 8, 0, false);
|
||||
|
||||
audio::set_rate(audio::Rate::Hz_24000);
|
||||
audio::output::start();
|
||||
|
||||
receiver_model.set_sampling_rate(3072000);
|
||||
receiver_model.set_baseband_bandwidth(1750000);
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::NarrowbandFMAudio);
|
||||
receiver_model.enable();
|
||||
}
|
||||
|
||||
void AFSKRxView::on_data(uint32_t value, bool is_data) {
|
||||
std::string str_console = "\x1B";
|
||||
std::string str_byte = "";
|
||||
|
||||
if (is_data) {
|
||||
// Colorize differently after message splits
|
||||
str_console += (char)((console_color & 3) + 9);
|
||||
|
||||
//value = deframe_word(value);
|
||||
|
||||
value &= 0xFF; // ABCDEFGH
|
||||
value = ((value & 0xF0) >> 4) | ((value & 0x0F) << 4); // EFGHABCD
|
||||
value = ((value & 0xCC) >> 2) | ((value & 0x33) << 2); // GHEFCDAB
|
||||
value = ((value & 0xAA) >> 1) | ((value & 0x55) << 1); // HGFEDCBA
|
||||
value &= 0x7F; // Ignore parity, which is the MSB now
|
||||
|
||||
if ((value >= 32) && (value < 127)) {
|
||||
str_console += (char)value; // Printable
|
||||
str_byte += (char)value;
|
||||
} else {
|
||||
str_console += "[" + to_string_hex(value, 2) + "]"; // Not printable
|
||||
str_byte += "[" + to_string_hex(value, 2) + "]";
|
||||
}
|
||||
|
||||
//str_byte = to_string_bin(value & 0xFF, 8) + " ";
|
||||
|
||||
console.write(str_console);
|
||||
|
||||
if (logger) str_log += str_byte;
|
||||
|
||||
if ((value != 0x7F) && (prev_value == 0x7F)) {
|
||||
// Message split
|
||||
console.writeln("");
|
||||
console_color++;
|
||||
|
||||
if (logger) {
|
||||
logger->log_raw_data(str_log);
|
||||
str_log = "";
|
||||
}
|
||||
}
|
||||
prev_value = value;
|
||||
} else {
|
||||
// Baudrate estimation
|
||||
text_debug.set("~" + to_string_dec_uint(value));
|
||||
}
|
||||
}
|
||||
|
||||
AFSKRxView::~AFSKRxView() {
|
||||
|
||||
// save app settings
|
||||
app_settings.rx_frequency = field_frequency.value();
|
||||
settings.save("rx_afsk", &app_settings);
|
||||
|
||||
audio::output::stop();
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
@@ -1,125 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2017 Furrtek
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __UI_AFSK_RX_H__
|
||||
#define __UI_AFSK_RX_H__
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_record_view.hpp" // DEBUG
|
||||
#include "app_settings.hpp"
|
||||
#include "log_file.hpp"
|
||||
#include "utility.hpp"
|
||||
|
||||
class AFSKLogger {
|
||||
public:
|
||||
Optional<File::Error> append(const std::string& filename) {
|
||||
return log_file.append(filename);
|
||||
}
|
||||
|
||||
void log_raw_data(const std::string& data);
|
||||
|
||||
private:
|
||||
LogFile log_file { };
|
||||
};
|
||||
|
||||
namespace ui {
|
||||
|
||||
class AFSKRxView : public View {
|
||||
public:
|
||||
AFSKRxView(NavigationView& nav);
|
||||
~AFSKRxView();
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "AFSK RX"; };
|
||||
|
||||
private:
|
||||
void on_data(uint32_t value, bool is_data);
|
||||
|
||||
// app save settings
|
||||
std::app_settings settings { };
|
||||
std::app_settings::AppSettings app_settings { };
|
||||
|
||||
uint8_t console_color { 0 };
|
||||
uint32_t prev_value { 0 };
|
||||
std::string str_log { "" };
|
||||
|
||||
RFAmpField field_rf_amp {
|
||||
{ 13 * 8, 0 * 16 }
|
||||
};
|
||||
LNAGainField field_lna {
|
||||
{ 15 * 8, 0 * 16 }
|
||||
};
|
||||
VGAGainField field_vga {
|
||||
{ 18 * 8, 0 * 16 }
|
||||
};
|
||||
RSSI rssi {
|
||||
{ 21 * 8, 0, 6 * 8, 4 },
|
||||
};
|
||||
Channel channel {
|
||||
{ 21 * 8, 5, 6 * 8, 4 },
|
||||
};
|
||||
|
||||
FrequencyField field_frequency {
|
||||
{ 0 * 8, 0 * 16 },
|
||||
};
|
||||
|
||||
Text text_debug {
|
||||
{ 0 * 8, 1 * 16, 10 * 8, 16 },
|
||||
"DEBUG"
|
||||
};
|
||||
|
||||
|
||||
Button button_modem_setup {
|
||||
{ 12 * 8, 1 * 16, 96, 24 },
|
||||
"Modem setup"
|
||||
};
|
||||
|
||||
// DEBUG
|
||||
RecordView record_view {
|
||||
{ 0 * 8, 3 * 16, 30 * 8, 1 * 16 },
|
||||
u"AFS_????", RecordView::FileType::WAV, 4096, 4
|
||||
};
|
||||
|
||||
Console console {
|
||||
{ 0, 4 * 16, 240, 240 }
|
||||
};
|
||||
|
||||
void update_freq(rf::Frequency f);
|
||||
void on_data_afsk(const AFSKDataMessage& message);
|
||||
|
||||
std::unique_ptr<AFSKLogger> logger { };
|
||||
|
||||
MessageHandlerRegistration message_handler_packet {
|
||||
Message::ID::AFSKData,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const AFSKDataMessage*>(p);
|
||||
this->on_data(message->value, message->is_data);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
#endif/*__UI_AFSK_RX_H__*/
|
||||
@@ -27,396 +27,350 @@
|
||||
#include "baseband_api.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include "file_path.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
|
||||
void APRSLogger::log_raw_data(const std::string& data) {
|
||||
rtc::RTC datetime;
|
||||
rtcGetTime(&RTCD1, &datetime);
|
||||
|
||||
log_file.write_entry(datetime, data);
|
||||
log_file.write_entry(data);
|
||||
}
|
||||
|
||||
namespace ui {
|
||||
|
||||
template<>
|
||||
template <>
|
||||
void RecentEntriesTable<APRSRecentEntries>::draw(
|
||||
const Entry& entry,
|
||||
const Rect& target_rect,
|
||||
Painter& painter,
|
||||
const Style& style
|
||||
) {
|
||||
char aged_color;
|
||||
Color target_color;
|
||||
// auto entry_age = entry.age;
|
||||
|
||||
target_color = Color::green();
|
||||
const Entry& entry,
|
||||
const Rect& target_rect,
|
||||
Painter& painter,
|
||||
const Style& style) {
|
||||
Color target_color;
|
||||
// auto entry_age = entry.age;
|
||||
|
||||
aged_color = 0x10;
|
||||
std::string entry_string = "\x1B";
|
||||
entry_string += aged_color;
|
||||
|
||||
entry_string += entry.source_formatted;
|
||||
entry_string.append(10 - entry.source_formatted.size(),' ');
|
||||
entry_string += " ";
|
||||
entry_string += (entry.hits <= 999 ? to_string_dec_uint(entry.hits, 4) : "999+");
|
||||
entry_string += " ";
|
||||
entry_string += entry.time_string;
|
||||
|
||||
painter.draw_string(
|
||||
target_rect.location(),
|
||||
style,
|
||||
entry_string
|
||||
);
|
||||
|
||||
if (entry.has_position){
|
||||
painter.draw_bitmap(target_rect.location() + Point(12 * 8, 0), bitmap_target, target_color, style.background);
|
||||
}
|
||||
target_color = Theme::getInstance()->fg_green->foreground;
|
||||
|
||||
std::string entry_string = "";
|
||||
|
||||
entry_string += entry.source_formatted;
|
||||
entry_string.append(10 - entry.source_formatted.size(), ' ');
|
||||
entry_string += " ";
|
||||
entry_string += (entry.hits <= 999 ? to_string_dec_uint(entry.hits, 4) : "999+");
|
||||
entry_string += " ";
|
||||
entry_string += entry.time_string;
|
||||
|
||||
painter.draw_string(
|
||||
target_rect.location(),
|
||||
style,
|
||||
entry_string);
|
||||
|
||||
if (entry.has_position) {
|
||||
painter.draw_bitmap(target_rect.location() + Point(12 * 8, 0), bitmap_target, target_color, style.background);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void APRSRxView::focus() {
|
||||
options_region.focus();
|
||||
options_region.focus();
|
||||
}
|
||||
|
||||
void APRSRxView::update_freq(rf::Frequency f) {
|
||||
receiver_model.set_tuning_frequency(f);
|
||||
APRSRxView::APRSRxView(NavigationView& nav, Rect parent_rect)
|
||||
: View(parent_rect), nav_{nav} {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_aprs_rx);
|
||||
|
||||
add_children({&rssi,
|
||||
&channel,
|
||||
&field_rf_amp,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&field_volume,
|
||||
&options_region,
|
||||
&field_frequency,
|
||||
&record_view,
|
||||
&console});
|
||||
|
||||
// DEBUG
|
||||
record_view.on_error = [&nav](std::string message) {
|
||||
nav.display_modal("Error", message);
|
||||
};
|
||||
|
||||
record_view.set_sampling_rate(24000);
|
||||
|
||||
options_region.on_change = [this](size_t, int32_t i) {
|
||||
if (i == 0) {
|
||||
field_frequency.set_value(144390000);
|
||||
} else if (i == 1) {
|
||||
field_frequency.set_value(144800000);
|
||||
} else if (i == 2) {
|
||||
field_frequency.set_value(145175000);
|
||||
} else if (i == 3) {
|
||||
field_frequency.set_value(144575000);
|
||||
} else if (i == 4) {
|
||||
field_frequency.set_value(145825000);
|
||||
}
|
||||
};
|
||||
|
||||
field_frequency.set_step(100);
|
||||
options_region.set_selected_index(0, true);
|
||||
|
||||
logger = std::make_unique<APRSLogger>();
|
||||
if (logger)
|
||||
logger->append(logs_dir / u"APRS.TXT");
|
||||
|
||||
baseband::set_aprs(1200);
|
||||
|
||||
audio::set_rate(audio::Rate::Hz_24000);
|
||||
audio::output::start();
|
||||
|
||||
receiver_model.enable();
|
||||
}
|
||||
|
||||
APRSRxView::APRSRxView(NavigationView& nav, Rect parent_rect) : View(parent_rect) {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_aprs_rx);
|
||||
|
||||
add_children({
|
||||
&rssi,
|
||||
&channel,
|
||||
&field_rf_amp,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&options_region,
|
||||
&field_frequency,
|
||||
&record_view,
|
||||
&console
|
||||
});
|
||||
|
||||
// load app settings
|
||||
auto rc = settings.load("rx_aprs", &app_settings);
|
||||
if(rc == SETTINGS_OK) {
|
||||
field_lna.set_value(app_settings.lna);
|
||||
field_vga.set_value(app_settings.vga);
|
||||
field_rf_amp.set_value(app_settings.rx_amp);
|
||||
}
|
||||
|
||||
|
||||
// DEBUG
|
||||
record_view.on_error = [&nav](std::string message) {
|
||||
nav.display_modal("Error", message);
|
||||
};
|
||||
|
||||
record_view.set_sampling_rate(24000);
|
||||
|
||||
options_region.on_change = [this](size_t, int32_t i) {
|
||||
if (i == 0){
|
||||
field_frequency.set_value(144390000);
|
||||
} else if(i == 1){
|
||||
field_frequency.set_value(144800000);
|
||||
} else if(i == 2){
|
||||
field_frequency.set_value(145175000);
|
||||
} else if(i == 3){
|
||||
field_frequency.set_value(144575000);
|
||||
}
|
||||
};
|
||||
|
||||
field_frequency.set_value(receiver_model.tuning_frequency());
|
||||
field_frequency.set_step(100);
|
||||
field_frequency.on_change = [this](rf::Frequency f) {
|
||||
update_freq(f);
|
||||
};
|
||||
field_frequency.on_edit = [this, &nav]() {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(receiver_model.tuning_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
update_freq(f);
|
||||
field_frequency.set_value(f);
|
||||
};
|
||||
};
|
||||
|
||||
options_region.set_selected_index(0, true);
|
||||
|
||||
logger = std::make_unique<APRSLogger>();
|
||||
if (logger)
|
||||
logger->append("APRS_RX_LOG.TXT");
|
||||
|
||||
baseband::set_aprs(1200);
|
||||
|
||||
audio::set_rate(audio::Rate::Hz_24000);
|
||||
audio::output::start();
|
||||
|
||||
receiver_model.set_sampling_rate(3072000);
|
||||
receiver_model.set_baseband_bandwidth(1750000);
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::NarrowbandFMAudio);
|
||||
receiver_model.enable();
|
||||
void APRSRxView::on_freqchg(int64_t freq) {
|
||||
field_frequency.set_value(freq);
|
||||
}
|
||||
|
||||
void APRSRxView::on_packet(const APRSPacketMessage* message){
|
||||
std::string str_console = "\x1B";
|
||||
void APRSRxView::on_packet(const APRSPacketMessage* message) {
|
||||
std::string str_console = "\x1B";
|
||||
|
||||
aprs::APRSPacket packet = message->packet;
|
||||
aprs::APRSPacket packet = message->packet;
|
||||
|
||||
std::string stream_text = packet.get_stream_text();
|
||||
str_console += (char)((console_color++ & 3) + 9);
|
||||
str_console += stream_text;
|
||||
std::string stream_text = packet.get_stream_text();
|
||||
str_console += (char)((console_color++ & 3) + 9);
|
||||
str_console += stream_text;
|
||||
|
||||
if(logger){
|
||||
logger->log_raw_data(stream_text);
|
||||
}
|
||||
|
||||
//if(reset_console){ //having more than one console causes issues when switching tabs where one is disabled, and the other enabled breaking the scoll setup.
|
||||
// console.on_hide();
|
||||
// console.on_show();
|
||||
// reset_console = false;
|
||||
//}
|
||||
if (logger) {
|
||||
logger->log_raw_data(stream_text);
|
||||
}
|
||||
|
||||
console.writeln(str_console);
|
||||
// if(reset_console){ //having more than one console causes issues when switching tabs where one is disabled, and the other enabled breaking the scoll setup.
|
||||
// console.on_hide();
|
||||
// console.on_show();
|
||||
// reset_console = false;
|
||||
// }
|
||||
|
||||
console.writeln(str_console);
|
||||
}
|
||||
|
||||
void APRSRxView::on_data(uint32_t value, bool is_data) {
|
||||
std::string str_console = "\x1B";
|
||||
std::string str_byte = "";
|
||||
|
||||
if (is_data) {
|
||||
// Colorize differently after message splits
|
||||
str_console += (char)((console_color & 3) + 9);
|
||||
std::string str_console = "\x1B";
|
||||
std::string str_byte = "";
|
||||
|
||||
if (value == '\n') {
|
||||
// Message split
|
||||
console.writeln("");
|
||||
console_color++;
|
||||
|
||||
if (logger) {
|
||||
logger->log_raw_data(str_log);
|
||||
str_log = "";
|
||||
}
|
||||
}
|
||||
else {
|
||||
if ((value >= 32) && (value < 127)) {
|
||||
str_console += (char)value; // Printable
|
||||
str_byte += (char)value;
|
||||
} else {
|
||||
str_console += "[" + to_string_hex(value, 2) + "]"; // Not printable
|
||||
str_byte += "[" + to_string_hex(value, 2) + "]";
|
||||
}
|
||||
if (is_data) {
|
||||
// Colorize differently after message splits
|
||||
str_console += (char)((console_color & 3) + 9);
|
||||
|
||||
console.write(str_console);
|
||||
|
||||
if (logger) str_log += str_byte;
|
||||
}
|
||||
} else {
|
||||
if (value == '\n') {
|
||||
// Message split
|
||||
console.writeln("");
|
||||
console_color++;
|
||||
|
||||
}
|
||||
if (logger) {
|
||||
logger->log_raw_data(str_log);
|
||||
str_log = "";
|
||||
}
|
||||
} else {
|
||||
if ((value >= 32) && (value < 127)) {
|
||||
str_console += (char)value; // Printable
|
||||
str_byte += (char)value;
|
||||
} else {
|
||||
str_console += "[" + to_string_hex(value, 2) + "]"; // Not printable
|
||||
str_byte += "[" + to_string_hex(value, 2) + "]";
|
||||
}
|
||||
|
||||
console.write(str_console);
|
||||
|
||||
if (logger) str_log += str_byte;
|
||||
}
|
||||
} else {
|
||||
}
|
||||
}
|
||||
|
||||
void APRSRxView::on_show(){
|
||||
//some bug where the display scroll area is set to the entire screen when switching from the list tab with details showing back to the stream view.
|
||||
//reset_console = true;
|
||||
void APRSRxView::on_show() {
|
||||
// some bug where the display scroll area is set to the entire screen when switching from the list tab with details showing back to the stream view.
|
||||
// reset_console = true;
|
||||
}
|
||||
|
||||
APRSRxView::~APRSRxView() {
|
||||
|
||||
// save app settings
|
||||
settings.save("rx_aprs", &app_settings);
|
||||
|
||||
audio::output::stop();
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
audio::output::stop();
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
void APRSTableView::on_show_list() {
|
||||
details_view.hidden(true);
|
||||
recent_entries_view.hidden(false);
|
||||
send_updates = false;
|
||||
focus();
|
||||
details_view.hidden(true);
|
||||
recent_entries_view.hidden(false);
|
||||
send_updates = false;
|
||||
focus();
|
||||
}
|
||||
|
||||
void APRSTableView::on_show_detail(const APRSRecentEntry& entry) {
|
||||
recent_entries_view.hidden(true);
|
||||
details_view.hidden(false);
|
||||
details_view.set_entry(entry);
|
||||
details_view.update();
|
||||
details_view.focus();
|
||||
detailed_entry_key = entry.key();
|
||||
send_updates = true;
|
||||
recent_entries_view.hidden(true);
|
||||
details_view.hidden(false);
|
||||
details_view.set_entry(entry);
|
||||
details_view.update();
|
||||
details_view.focus();
|
||||
detailed_entry_key = entry.key();
|
||||
send_updates = true;
|
||||
}
|
||||
|
||||
APRSTableView::APRSTableView(NavigationView& nav, Rect parent_rec) : View(parent_rec), nav_ {nav} {
|
||||
add_children({
|
||||
&recent_entries_view,
|
||||
&details_view
|
||||
});
|
||||
APRSTableView::APRSTableView(NavigationView& nav, Rect parent_rec)
|
||||
: View(parent_rec), nav_{nav} {
|
||||
add_children({&recent_entries_view,
|
||||
&details_view});
|
||||
|
||||
hidden(true);
|
||||
hidden(true);
|
||||
|
||||
details_view.hidden(true);
|
||||
|
||||
recent_entries_view.set_parent_rect({0, 0, 240, 280});
|
||||
details_view.set_parent_rect({0, 0, 240, 280});
|
||||
|
||||
recent_entries_view.on_select = [this](const APRSRecentEntry& entry) {
|
||||
this->on_show_detail(entry);
|
||||
};
|
||||
details_view.hidden(true);
|
||||
|
||||
details_view.on_close = [this]() {
|
||||
this->on_show_list();
|
||||
};
|
||||
recent_entries_view.set_parent_rect({0, 0, 240, 280});
|
||||
details_view.set_parent_rect({0, 0, 240, 280});
|
||||
|
||||
recent_entries_view.on_select = [this](const APRSRecentEntry& entry) {
|
||||
this->on_show_detail(entry);
|
||||
};
|
||||
|
||||
/* for(size_t i = 0; i <32 ; i++){
|
||||
std::string id = "test" + i;
|
||||
auto& entry = ::on_packet(recent, i);
|
||||
entry.set_source_formatted(id);
|
||||
}
|
||||
details_view.on_close = [this]() {
|
||||
this->on_show_list();
|
||||
};
|
||||
|
||||
recent_entries_view.set_dirty(); */
|
||||
/* for(size_t i = 0; i <32 ; i++){
|
||||
std::string id = "test" + i;
|
||||
auto& entry = ::on_packet(recent, i);
|
||||
entry.set_source_formatted(id);
|
||||
}
|
||||
|
||||
/*
|
||||
recent_entries_view.set_dirty(); */
|
||||
|
||||
std::string str1 = "test1";
|
||||
std::string str12 = "test2";
|
||||
std::string str13 = "test2";
|
||||
/*
|
||||
|
||||
auto& entry = ::on_packet(recent, 0x1);
|
||||
entry.set_source_formatted(str1);
|
||||
std::string str1 = "test1";
|
||||
std::string str12 = "test2";
|
||||
std::string str13 = "test2";
|
||||
|
||||
auto& entry2 = ::on_packet(recent, 0x2);
|
||||
entry2.set_source_formatted(str12);
|
||||
auto& entry = ::on_packet(recent, 0x1);
|
||||
entry.set_source_formatted(str1);
|
||||
|
||||
auto& entry3 = ::on_packet(recent, 0x2);
|
||||
entry2.set_source_formatted(str13);
|
||||
auto& entry2 = ::on_packet(recent, 0x2);
|
||||
entry2.set_source_formatted(str12);
|
||||
|
||||
recent_entries_view.set_dirty();
|
||||
auto& entry3 = ::on_packet(recent, 0x2);
|
||||
entry2.set_source_formatted(str13);
|
||||
|
||||
*/
|
||||
recent_entries_view.set_dirty();
|
||||
|
||||
*/
|
||||
}
|
||||
|
||||
void APRSTableView::on_pkt(const APRSPacketMessage* message){
|
||||
aprs::APRSPacket packet = message->packet;
|
||||
rtc::RTC datetime;
|
||||
std::string str_timestamp;
|
||||
std::string source_formatted = packet.get_source_formatted();
|
||||
std::string info_string = packet.get_stream_text();
|
||||
void APRSTableView::on_pkt(const APRSPacketMessage* message) {
|
||||
aprs::APRSPacket packet = message->packet;
|
||||
rtc::RTC datetime;
|
||||
std::string str_timestamp;
|
||||
std::string source_formatted = packet.get_source_formatted();
|
||||
std::string info_string = packet.get_stream_text();
|
||||
|
||||
rtcGetTime(&RTCD1, &datetime);
|
||||
auto& entry = ::on_packet(recent, packet.get_source());
|
||||
entry.reset_age();
|
||||
entry.inc_hit();
|
||||
str_timestamp = to_string_datetime(datetime, HMS);
|
||||
entry.set_time_string(str_timestamp);
|
||||
entry.set_info_string(info_string);
|
||||
rtc_time::now(datetime);
|
||||
auto& entry = ::on_packet(recent, packet.get_source());
|
||||
entry.reset_age();
|
||||
entry.inc_hit();
|
||||
str_timestamp = to_string_datetime(datetime, HMS);
|
||||
entry.set_time_string(str_timestamp);
|
||||
entry.set_info_string(info_string);
|
||||
|
||||
entry.set_source_formatted(source_formatted);
|
||||
entry.set_source_formatted(source_formatted);
|
||||
|
||||
if(entry.has_position && !packet.has_position()){
|
||||
//maintain position info
|
||||
}
|
||||
else {
|
||||
entry.set_has_position(packet.has_position());
|
||||
entry.set_pos(packet.get_position());
|
||||
}
|
||||
if (entry.has_position && !packet.has_position()) {
|
||||
// maintain position info
|
||||
} else {
|
||||
entry.set_has_position(packet.has_position());
|
||||
entry.set_pos(packet.get_position());
|
||||
}
|
||||
|
||||
if( entry.key() == details_view.entry().key() ) {
|
||||
details_view.set_entry(entry);
|
||||
details_view.update();
|
||||
}
|
||||
if (entry.key() == details_view.entry().key()) {
|
||||
details_view.set_entry(entry);
|
||||
details_view.update();
|
||||
}
|
||||
|
||||
recent_entries_view.set_dirty();
|
||||
recent_entries_view.set_dirty();
|
||||
}
|
||||
|
||||
void APRSTableView::on_show(){
|
||||
on_show_list();
|
||||
void APRSTableView::on_show() {
|
||||
on_show_list();
|
||||
}
|
||||
|
||||
void APRSTableView::on_hide(){
|
||||
details_view.hidden(true);
|
||||
void APRSTableView::on_hide() {
|
||||
details_view.hidden(true);
|
||||
}
|
||||
|
||||
void APRSTableView::focus(){
|
||||
recent_entries_view.focus();
|
||||
void APRSTableView::focus() {
|
||||
recent_entries_view.focus();
|
||||
}
|
||||
|
||||
APRSTableView::~APRSTableView(){
|
||||
|
||||
APRSTableView::~APRSTableView() {
|
||||
}
|
||||
|
||||
void APRSDetailsView::focus() {
|
||||
button_done.focus();
|
||||
button_done.focus();
|
||||
}
|
||||
|
||||
void APRSDetailsView::set_entry(const APRSRecentEntry& entry){
|
||||
entry_copy = entry;
|
||||
void APRSDetailsView::set_entry(const APRSRecentEntry& entry) {
|
||||
entry_copy = entry;
|
||||
}
|
||||
|
||||
void APRSDetailsView::update() {
|
||||
if(!hidden()){
|
||||
//uint32_t age = entry_copy.age;
|
||||
|
||||
console.clear(true);
|
||||
console.write(entry_copy.info_string);
|
||||
void APRSDetailsView::update() {
|
||||
if (!hidden()) {
|
||||
// uint32_t age = entry_copy.age;
|
||||
|
||||
button_see_map.hidden(!entry_copy.has_position);
|
||||
}
|
||||
|
||||
if (send_updates)
|
||||
geomap_view->update_position(entry_copy.pos.latitude, entry_copy.pos.longitude, 0, 0);
|
||||
console.clear(true);
|
||||
console.write(entry_copy.info_string);
|
||||
|
||||
button_see_map.hidden(!entry_copy.has_position);
|
||||
}
|
||||
|
||||
if (send_updates)
|
||||
geomap_view->update_position(entry_copy.pos.latitude, entry_copy.pos.longitude, 0, 0, 0);
|
||||
}
|
||||
|
||||
APRSDetailsView::~APRSDetailsView() {
|
||||
}
|
||||
|
||||
APRSDetailsView::APRSDetailsView(
|
||||
NavigationView& nav
|
||||
)
|
||||
{
|
||||
add_children({
|
||||
&console,
|
||||
&button_done,
|
||||
&button_see_map
|
||||
});
|
||||
NavigationView& nav) {
|
||||
add_children({&console,
|
||||
&button_done,
|
||||
&button_see_map});
|
||||
|
||||
button_done.on_select = [this, &nav](Button&) {
|
||||
console.clear(true);
|
||||
this->on_close();
|
||||
};
|
||||
button_done.on_select = [this, &nav](Button&) {
|
||||
console.clear(true);
|
||||
this->on_close();
|
||||
};
|
||||
|
||||
button_see_map.on_select = [this, &nav](Button&) {
|
||||
geomap_view = nav.push<GeoMapView>(
|
||||
entry_copy.source_formatted,
|
||||
0, //entry_copy.pos.altitude,
|
||||
GeoPos::alt_unit::FEET,
|
||||
entry_copy.pos.latitude,
|
||||
entry_copy.pos.longitude,
|
||||
0, /*entry_copy.velo.heading,*/
|
||||
[this]() {
|
||||
send_updates = false;
|
||||
hidden(false);
|
||||
update();
|
||||
});
|
||||
send_updates = true;
|
||||
hidden(true);
|
||||
|
||||
};
|
||||
button_see_map.on_select = [this, &nav](Button&) {
|
||||
geomap_view = nav.push<GeoMapView>(
|
||||
entry_copy.source_formatted,
|
||||
0, // entry_copy.pos.altitude,
|
||||
GeoPos::alt_unit::FEET,
|
||||
GeoPos::spd_unit::HIDDEN,
|
||||
entry_copy.pos.latitude,
|
||||
entry_copy.pos.longitude,
|
||||
0, /*entry_copy.velo.heading,*/
|
||||
[this]() {
|
||||
send_updates = false;
|
||||
hidden(false);
|
||||
update();
|
||||
});
|
||||
send_updates = true;
|
||||
hidden(true);
|
||||
};
|
||||
};
|
||||
|
||||
APRSRXView::APRSRXView(NavigationView& nav) : nav_ {nav} {
|
||||
add_children({
|
||||
&tab_view,
|
||||
&view_stream,
|
||||
&view_table
|
||||
});
|
||||
APRSRXView::APRSRXView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
add_children({&tab_view,
|
||||
&view_stream,
|
||||
&view_table});
|
||||
}
|
||||
|
||||
void APRSRXView::focus(){
|
||||
tab_view.focus();
|
||||
void APRSRXView::focus() {
|
||||
tab_view.focus();
|
||||
}
|
||||
|
||||
APRSRXView::~APRSRXView() {
|
||||
|
||||
}
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -26,258 +26,261 @@
|
||||
#include "ui.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_record_view.hpp" // DEBUG
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "ui_record_view.hpp"
|
||||
#include "ui_geomap.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "recent_entries.hpp"
|
||||
#include "ui_tabview.hpp"
|
||||
|
||||
#include "log_file.hpp"
|
||||
#include "utility.hpp"
|
||||
#include "file_path.hpp"
|
||||
|
||||
class APRSLogger {
|
||||
public:
|
||||
Optional<File::Error> append(const std::string& filename) {
|
||||
return log_file.append(filename);
|
||||
}
|
||||
|
||||
void log_raw_data(const std::string& data);
|
||||
public:
|
||||
Optional<File::Error> append(const std::filesystem::path& filename) {
|
||||
return log_file.append(filename);
|
||||
}
|
||||
|
||||
private:
|
||||
LogFile log_file { };
|
||||
void log_raw_data(const std::string& data);
|
||||
|
||||
private:
|
||||
LogFile log_file{};
|
||||
};
|
||||
|
||||
namespace ui {
|
||||
|
||||
struct APRSRecentEntry {
|
||||
using Key = uint64_t;
|
||||
|
||||
static constexpr Key invalid_key = 0xffffffffffffffff;
|
||||
|
||||
uint16_t hits { 0 };
|
||||
uint32_t age { 0 };
|
||||
|
||||
uint64_t source { 0 };
|
||||
std::string source_formatted { " " };
|
||||
std::string time_string { "" };
|
||||
std::string info_string { "" };
|
||||
|
||||
aprs::aprs_pos pos { 0 , 0 , 0 , 0 };
|
||||
bool has_position = false;
|
||||
APRSRecentEntry(uint64_t src)
|
||||
{
|
||||
source = src;
|
||||
}
|
||||
using Key = uint64_t;
|
||||
|
||||
Key key() const {
|
||||
return source;
|
||||
}
|
||||
|
||||
void set_source_formatted(std::string& new_source) {
|
||||
source_formatted = new_source;
|
||||
}
|
||||
|
||||
void inc_hit() {
|
||||
hits++;
|
||||
}
|
||||
|
||||
void set_info_string(std::string& new_info_string) {
|
||||
info_string = new_info_string;
|
||||
}
|
||||
|
||||
void set_time_string(std::string& new_time_string) {
|
||||
time_string = new_time_string;
|
||||
}
|
||||
static constexpr Key invalid_key = 0xffffffffffffffff;
|
||||
|
||||
void set_pos(aprs::aprs_pos pos_in){
|
||||
pos = pos_in;
|
||||
}
|
||||
|
||||
void set_has_position(bool has_pos){
|
||||
has_position = has_pos;
|
||||
}
|
||||
uint16_t hits{0};
|
||||
uint32_t age{0};
|
||||
|
||||
void reset_age() {
|
||||
age = 0;
|
||||
}
|
||||
|
||||
void inc_age() {
|
||||
age++;
|
||||
}
|
||||
uint64_t source{0};
|
||||
std::string source_formatted{" "};
|
||||
std::string time_string{""};
|
||||
std::string info_string{""};
|
||||
|
||||
aprs::aprs_pos pos{0, 0, 0, 0};
|
||||
bool has_position = false;
|
||||
APRSRecentEntry(uint64_t src) {
|
||||
source = src;
|
||||
}
|
||||
|
||||
Key key() const {
|
||||
return source;
|
||||
}
|
||||
|
||||
void set_source_formatted(std::string& new_source) {
|
||||
source_formatted = new_source;
|
||||
}
|
||||
|
||||
void inc_hit() {
|
||||
hits++;
|
||||
}
|
||||
|
||||
void set_info_string(std::string& new_info_string) {
|
||||
info_string = new_info_string;
|
||||
}
|
||||
|
||||
void set_time_string(std::string& new_time_string) {
|
||||
time_string = new_time_string;
|
||||
}
|
||||
|
||||
void set_pos(aprs::aprs_pos pos_in) {
|
||||
pos = pos_in;
|
||||
}
|
||||
|
||||
void set_has_position(bool has_pos) {
|
||||
has_position = has_pos;
|
||||
}
|
||||
|
||||
void reset_age() {
|
||||
age = 0;
|
||||
}
|
||||
|
||||
void inc_age() {
|
||||
age++;
|
||||
}
|
||||
};
|
||||
|
||||
class APRSDetailsView : public View {
|
||||
public:
|
||||
APRSDetailsView(NavigationView&);
|
||||
~APRSDetailsView();
|
||||
public:
|
||||
APRSDetailsView(NavigationView&);
|
||||
~APRSDetailsView();
|
||||
|
||||
APRSDetailsView(const APRSDetailsView&) = delete;
|
||||
APRSDetailsView(APRSDetailsView&&) = delete;
|
||||
APRSDetailsView& operator=(const APRSDetailsView&) = delete;
|
||||
APRSDetailsView& operator=(APRSDetailsView&&) = delete;
|
||||
|
||||
void focus() override;
|
||||
|
||||
void update();
|
||||
void set_entry(const APRSRecentEntry& entry);
|
||||
APRSDetailsView(const APRSDetailsView&) = delete;
|
||||
APRSDetailsView(APRSDetailsView&&) = delete;
|
||||
APRSDetailsView& operator=(const APRSDetailsView&) = delete;
|
||||
APRSDetailsView& operator=(APRSDetailsView&&) = delete;
|
||||
|
||||
const APRSRecentEntry& entry() const { return entry_copy; };
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "Details"; };
|
||||
std::function<void(void)> on_close { };
|
||||
|
||||
private:
|
||||
APRSRecentEntry entry_copy { 0 };
|
||||
GeoMapView* geomap_view { nullptr };
|
||||
bool send_updates { false };
|
||||
|
||||
void update();
|
||||
void set_entry(const APRSRecentEntry& entry);
|
||||
|
||||
Console console {
|
||||
{ 0, 0 * 16, 240, 224 }
|
||||
};
|
||||
const APRSRecentEntry& entry() const { return entry_copy; };
|
||||
|
||||
Button button_done {
|
||||
{ 160, 14 * 16, 8 * 8, 3 * 16 },
|
||||
"Close"
|
||||
};
|
||||
|
||||
Button button_see_map {
|
||||
{ 80, 14 * 16, 8 * 8, 3 * 16 },
|
||||
"Map"
|
||||
};
|
||||
std::string title() const override { return "Details"; };
|
||||
std::function<void(void)> on_close{};
|
||||
|
||||
private:
|
||||
APRSRecentEntry entry_copy{0};
|
||||
GeoMapView* geomap_view{nullptr};
|
||||
bool send_updates{false};
|
||||
|
||||
Console console{
|
||||
{0, 0 * 16, 240, 224}};
|
||||
|
||||
Button button_done{
|
||||
{160, 14 * 16, 8 * 8, 3 * 16},
|
||||
"Close"};
|
||||
|
||||
Button button_see_map{
|
||||
{80, 14 * 16, 8 * 8, 3 * 16},
|
||||
"Map"};
|
||||
};
|
||||
|
||||
using APRSRecentEntries = RecentEntries<APRSRecentEntry>;
|
||||
|
||||
class APRSTableView: public View {
|
||||
public:
|
||||
APRSTableView(NavigationView& nav, Rect parent_rec);
|
||||
~APRSTableView();
|
||||
class APRSTableView : public View {
|
||||
public:
|
||||
APRSTableView(NavigationView& nav, Rect parent_rec);
|
||||
~APRSTableView();
|
||||
|
||||
void on_show() override;
|
||||
void on_hide() override;
|
||||
void focus() override;
|
||||
void on_pkt(const APRSPacketMessage* message);
|
||||
void on_show() override;
|
||||
void on_hide() override;
|
||||
void focus() override;
|
||||
void on_pkt(const APRSPacketMessage* message);
|
||||
|
||||
std::string title() const override { return "Stations"; };
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
const RecentEntriesColumns columns { {
|
||||
{ "Source", 9 },
|
||||
{ "Loc", 6 },
|
||||
{ "Hits", 4 },
|
||||
{ "Time", 8 }
|
||||
} };
|
||||
APRSRecentEntries recent { };
|
||||
RecentEntriesView<RecentEntries<APRSRecentEntry>> recent_entries_view { columns, recent };
|
||||
APRSDetailsView details_view { nav_ };
|
||||
uint32_t detailed_entry_key { 0 };
|
||||
bool send_updates { false };
|
||||
std::string title() const override { return "Stations"; };
|
||||
|
||||
void on_show_list();
|
||||
void on_show_detail(const APRSRecentEntry& entry);
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
const RecentEntriesColumns columns{{{"Source", 9},
|
||||
{"Loc", 6},
|
||||
{"Hits", 4},
|
||||
{"Time", 8}}};
|
||||
APRSRecentEntries recent{};
|
||||
RecentEntriesView<RecentEntries<APRSRecentEntry>> recent_entries_view{columns, recent};
|
||||
APRSDetailsView details_view{nav_};
|
||||
uint32_t detailed_entry_key{0};
|
||||
bool send_updates{false};
|
||||
|
||||
void on_show_list();
|
||||
void on_show_detail(const APRSRecentEntry& entry);
|
||||
};
|
||||
|
||||
class APRSRxView : public View {
|
||||
public:
|
||||
APRSRxView(NavigationView& nav, Rect parent_rect);
|
||||
~APRSRxView();
|
||||
public:
|
||||
APRSRxView(NavigationView& nav, Rect parent_rect);
|
||||
~APRSRxView();
|
||||
|
||||
void on_show() override;
|
||||
void focus() override;
|
||||
void on_show() override;
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "APRS RX"; };
|
||||
void on_packet(const APRSPacketMessage* message);
|
||||
|
||||
private:
|
||||
void on_data(uint32_t value, bool is_data);
|
||||
bool reset_console = false;
|
||||
|
||||
// app save settings
|
||||
std::app_settings settings { };
|
||||
std::app_settings::AppSettings app_settings { };
|
||||
std::string title() const override { return "APRS RX"; };
|
||||
void on_packet(const APRSPacketMessage* message);
|
||||
void on_freqchg(int64_t freq);
|
||||
|
||||
private:
|
||||
void on_data(uint32_t value, bool is_data);
|
||||
bool reset_console = false;
|
||||
|
||||
uint8_t console_color { 0 };
|
||||
std::string str_log { "" };
|
||||
NavigationView& nav_;
|
||||
RxRadioState radio_state_{
|
||||
144390000 /* frequency */,
|
||||
1750000 /* bandwidth */,
|
||||
3072000 /* sampling rate */
|
||||
};
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_aprs", app_settings::Mode::RX};
|
||||
|
||||
RFAmpField field_rf_amp {
|
||||
{ 13 * 8, 0 * 16 }
|
||||
};
|
||||
LNAGainField field_lna {
|
||||
{ 15 * 8, 0 * 16 }
|
||||
};
|
||||
VGAGainField field_vga {
|
||||
{ 18 * 8, 0 * 16 }
|
||||
};
|
||||
RSSI rssi {
|
||||
{ 21 * 8, 0, 6 * 8, 4 },
|
||||
};
|
||||
Channel channel {
|
||||
{ 21 * 8, 5, 6 * 8, 4 },
|
||||
};
|
||||
uint8_t console_color{0};
|
||||
std::string str_log{""};
|
||||
|
||||
OptionsField options_region {
|
||||
{ 0 * 8, 0 * 8 },
|
||||
3,
|
||||
{
|
||||
{ "NA ", 0 },
|
||||
{ "EUR", 1 },
|
||||
{ "AUS", 2 },
|
||||
{ "NZ ", 3 }
|
||||
}
|
||||
};
|
||||
|
||||
FrequencyField field_frequency {
|
||||
{ 3 * 8, 0 * 16 },
|
||||
};
|
||||
|
||||
// DEBUG
|
||||
RecordView record_view {
|
||||
{ 0 * 8, 1 * 16, 30 * 8, 1 * 16 },
|
||||
u"AFS_????", RecordView::FileType::WAV, 4096, 4
|
||||
};
|
||||
|
||||
Console console {
|
||||
{ 0, 2 * 16, 240, 240 }
|
||||
};
|
||||
RFAmpField field_rf_amp{
|
||||
{13 * 8, 0 * 16}};
|
||||
LNAGainField field_lna{
|
||||
{15 * 8, 0 * 16}};
|
||||
VGAGainField field_vga{
|
||||
{18 * 8, 0 * 16}};
|
||||
RSSI rssi{
|
||||
{21 * 8, 0, 6 * 8, 4}};
|
||||
Channel channel{
|
||||
{21 * 8, 5, 6 * 8, 4}};
|
||||
|
||||
void update_freq(rf::Frequency f);
|
||||
|
||||
std::unique_ptr<APRSLogger> logger { };
|
||||
AudioVolumeField field_volume{
|
||||
{28 * 8, 0 * 16}};
|
||||
|
||||
OptionsField options_region{
|
||||
{0 * 8, 0 * 8},
|
||||
3,
|
||||
{{"NA ", 0},
|
||||
{"EUR", 1},
|
||||
{"AUS", 2},
|
||||
{"NZ ", 3},
|
||||
{"ISS", 4}}};
|
||||
|
||||
RxFrequencyField field_frequency{
|
||||
{3 * 8, 0 * 16},
|
||||
nav_};
|
||||
|
||||
// DEBUG
|
||||
RecordView record_view{
|
||||
{0 * 8, 1 * 16, 30 * 8, 1 * 16},
|
||||
u"AFS_????.WAV",
|
||||
aprs_dir,
|
||||
RecordView::FileType::WAV,
|
||||
4096,
|
||||
4};
|
||||
|
||||
Console console{
|
||||
{0, 2 * 16, 240, 240}};
|
||||
|
||||
std::unique_ptr<APRSLogger> logger{};
|
||||
};
|
||||
|
||||
class APRSRXView : public View {
|
||||
public:
|
||||
APRSRXView(NavigationView& nav);
|
||||
~APRSRXView();
|
||||
public:
|
||||
APRSRXView(NavigationView& nav);
|
||||
~APRSRXView();
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "APRS RX"; };
|
||||
void focus() override;
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
Rect view_rect = { 0, 3 * 8, 240, 280 };
|
||||
|
||||
APRSRxView view_stream { nav_, view_rect };
|
||||
APRSTableView view_table { nav_, view_rect };
|
||||
|
||||
TabView tab_view {
|
||||
{ "Stream", Color::cyan(), &view_stream },
|
||||
{ "List", Color::yellow(), &view_table }
|
||||
};
|
||||
std::string title() const override { return "APRS RX"; };
|
||||
|
||||
MessageHandlerRegistration message_handler_packet {
|
||||
Message::ID::APRSPacket,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const APRSPacketMessage*>(p);
|
||||
this->view_stream.on_packet(message);
|
||||
this->view_table.on_pkt(message);
|
||||
}
|
||||
};
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
Rect view_rect = {0, 3 * 8, 240, 280};
|
||||
|
||||
APRSRxView view_stream{nav_, view_rect};
|
||||
APRSTableView view_table{nav_, view_rect};
|
||||
|
||||
TabView tab_view{
|
||||
{"Stream", Theme::getInstance()->fg_cyan->foreground, &view_stream},
|
||||
{"List", Theme::getInstance()->fg_yellow->foreground, &view_table}};
|
||||
|
||||
MessageHandlerRegistration message_handler_packet{
|
||||
Message::ID::APRSPacket,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const APRSPacketMessage*>(p);
|
||||
this->view_stream.on_packet(message);
|
||||
this->view_table.on_pkt(message);
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_freqchg{
|
||||
Message::ID::FreqChangeCommand,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const FreqChangeCommandMessage*>(p);
|
||||
this->view_stream.on_freqchg(message->freq);
|
||||
}};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
#endif/*__UI_APRS_RX_H__*/
|
||||
#endif /*__UI_APRS_RX_H__*/
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2017 Furrtek
|
||||
*
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
@@ -26,7 +26,6 @@
|
||||
#include "aprs.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "cpld_update.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "portapack_shared_memory.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
@@ -40,103 +39,82 @@ using namespace portapack;
|
||||
namespace ui {
|
||||
|
||||
void APRSTXView::focus() {
|
||||
tx_view.focus();
|
||||
tx_view.focus();
|
||||
}
|
||||
|
||||
APRSTXView::~APRSTXView() {
|
||||
// save app settings
|
||||
app_settings.tx_frequency = transmitter_model.tuning_frequency();
|
||||
settings.save("tx_aprs", &app_settings);
|
||||
|
||||
transmitter_model.disable();
|
||||
hackrf::cpld::load_sram_no_verify(); // to leave all RX ok, without ghost signal problem at the exit.
|
||||
baseband::shutdown(); // better this function at the end, not load_sram() that sometimes produces hang up.
|
||||
transmitter_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
void APRSTXView::start_tx() {
|
||||
make_aprs_frame(
|
||||
sym_source.value_string().c_str(), num_ssid_source.value(),
|
||||
sym_dest.value_string().c_str(), num_ssid_dest.value(),
|
||||
payload);
|
||||
|
||||
//uint8_t * bb_data_ptr = shared_memory.bb_data.data;
|
||||
//text_payload.set(to_string_hex_array(bb_data_ptr + 56, 15));
|
||||
|
||||
transmitter_model.set_tuning_frequency(persistent_memory::tuned_frequency());
|
||||
transmitter_model.set_sampling_rate(AFSK_TX_SAMPLERATE);
|
||||
transmitter_model.set_baseband_bandwidth(1750000);
|
||||
transmitter_model.enable();
|
||||
|
||||
baseband::set_afsk_data(
|
||||
AFSK_TX_SAMPLERATE / 1200,
|
||||
1200,
|
||||
2200,
|
||||
1,
|
||||
10000, //APRS uses fixed 10k bandwidth
|
||||
8
|
||||
);
|
||||
// TODO: Clean up this API to take string_views to avoid allocations.
|
||||
make_aprs_frame(
|
||||
sym_source.to_string().c_str(), num_ssid_source.value(),
|
||||
sym_dest.to_string().c_str(), num_ssid_dest.value(),
|
||||
payload);
|
||||
|
||||
// uint8_t * bb_data_ptr = shared_memory.bb_data.data;
|
||||
// text_payload.set(to_string_hex_array(bb_data_ptr + 56, 15));
|
||||
|
||||
transmitter_model.enable();
|
||||
|
||||
baseband::set_afsk_data(
|
||||
AFSK_TX_SAMPLERATE / 1200,
|
||||
1200,
|
||||
2200,
|
||||
1,
|
||||
10000, // APRS uses fixed 10k bandwidth
|
||||
8);
|
||||
}
|
||||
|
||||
void APRSTXView::on_tx_progress(const uint32_t progress, const bool done) {
|
||||
(void)progress;
|
||||
|
||||
if (done) {
|
||||
transmitter_model.disable();
|
||||
tx_view.set_transmitting(false);
|
||||
}
|
||||
(void)progress;
|
||||
|
||||
if (done) {
|
||||
transmitter_model.disable();
|
||||
tx_view.set_transmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
APRSTXView::APRSTXView(NavigationView& nav) {
|
||||
|
||||
baseband::run_image(portapack::spi_flash::image_tag_afsk);
|
||||
baseband::run_image(portapack::spi_flash::image_tag_afsk);
|
||||
|
||||
add_children({
|
||||
&labels,
|
||||
&sym_source,
|
||||
&num_ssid_source,
|
||||
&sym_dest,
|
||||
&num_ssid_dest,
|
||||
&text_payload,
|
||||
&button_set,
|
||||
&tx_view
|
||||
});
|
||||
|
||||
add_children({&labels,
|
||||
&sym_source,
|
||||
&num_ssid_source,
|
||||
&sym_dest,
|
||||
&num_ssid_dest,
|
||||
&text_payload,
|
||||
&button_set,
|
||||
&tx_view});
|
||||
|
||||
// load app settings
|
||||
auto rc = settings.load("tx_aprs", &app_settings);
|
||||
if(rc == SETTINGS_OK) {
|
||||
transmitter_model.set_rf_amp(app_settings.tx_amp);
|
||||
transmitter_model.set_tuning_frequency(app_settings.tx_frequency);
|
||||
transmitter_model.set_tx_gain(app_settings.tx_gain);
|
||||
}
|
||||
button_set.on_select = [this, &nav](Button&) {
|
||||
text_prompt(
|
||||
nav,
|
||||
payload,
|
||||
30,
|
||||
[this](std::string& s) {
|
||||
text_payload.set(s);
|
||||
});
|
||||
};
|
||||
|
||||
button_set.on_select = [this, &nav](Button&) {
|
||||
text_prompt(
|
||||
nav,
|
||||
payload,
|
||||
30,
|
||||
[this](std::string& s) {
|
||||
text_payload.set(s);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
tx_view.on_edit_frequency = [this, &nav]() {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(receiver_model.tuning_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
receiver_model.set_tuning_frequency(f);
|
||||
};
|
||||
};
|
||||
|
||||
tx_view.on_start = [this]() {
|
||||
start_tx();
|
||||
tx_view.set_transmitting(true);
|
||||
};
|
||||
|
||||
tx_view.on_stop = [this]() {
|
||||
tx_view.set_transmitting(false);
|
||||
transmitter_model.disable();
|
||||
};
|
||||
tx_view.on_edit_frequency = [this, &nav]() {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(transmitter_model.target_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
transmitter_model.set_target_frequency(f);
|
||||
};
|
||||
};
|
||||
|
||||
tx_view.on_start = [this]() {
|
||||
start_tx();
|
||||
tx_view.set_transmitting(true);
|
||||
};
|
||||
|
||||
tx_view.on_stop = [this]() {
|
||||
tx_view.set_transmitting(false);
|
||||
transmitter_model.disable();
|
||||
};
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2017 Furrtek
|
||||
*
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
@@ -24,93 +24,91 @@
|
||||
#include "ui_textentry.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_font_fixed_8x16.hpp"
|
||||
#include "ui_transmitter.hpp"
|
||||
|
||||
#include "message.hpp"
|
||||
#include "modems.hpp"
|
||||
#include "transmitter_model.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "portapack.hpp"
|
||||
|
||||
namespace ui {
|
||||
|
||||
class APRSTXView : public View {
|
||||
public:
|
||||
APRSTXView(NavigationView& nav);
|
||||
~APRSTXView();
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "APRS TX"; };
|
||||
public:
|
||||
APRSTXView(NavigationView& nav);
|
||||
~APRSTXView();
|
||||
|
||||
private:
|
||||
|
||||
// app save settings
|
||||
std::app_settings settings { };
|
||||
std::app_settings::AppSettings app_settings { };
|
||||
void focus() override;
|
||||
|
||||
std::string payload { "" };
|
||||
|
||||
void start_tx();
|
||||
void generate_frame();
|
||||
void generate_frame_pos();
|
||||
void on_tx_progress(const uint32_t progress, const bool done);
|
||||
|
||||
Labels labels {
|
||||
{ { 0 * 8, 1 * 16 }, "Source: SSID:", Color::light_grey() }, // 6 alphanum + SSID
|
||||
{ { 0 * 8, 2 * 16 }, " Dest.: SSID:", Color::light_grey() },
|
||||
{ { 0 * 8, 4 * 16 }, "Info field:", Color::light_grey() },
|
||||
};
|
||||
|
||||
SymField sym_source {
|
||||
{ 7 * 8, 1 * 16 },
|
||||
6,
|
||||
SymField::SYMFIELD_ALPHANUM
|
||||
};
|
||||
NumberField num_ssid_source {
|
||||
{ 19 * 8, 1 * 16 },
|
||||
2,
|
||||
{ 0, 15 },
|
||||
1,
|
||||
' '
|
||||
};
|
||||
|
||||
SymField sym_dest {
|
||||
{ 7 * 8, 2 * 16 },
|
||||
6,
|
||||
SymField::SYMFIELD_ALPHANUM
|
||||
};
|
||||
|
||||
NumberField num_ssid_dest {
|
||||
{ 19 * 8, 2 * 16 },
|
||||
2,
|
||||
{ 0, 15 },
|
||||
1,
|
||||
' '
|
||||
};
|
||||
|
||||
Text text_payload {
|
||||
{ 0 * 8, 5 * 16, 30 * 8, 16 },
|
||||
"-"
|
||||
};
|
||||
Button button_set {
|
||||
{ 0 * 8, 6 * 16, 80, 32 },
|
||||
"Set"
|
||||
};
|
||||
|
||||
TransmitterView tx_view {
|
||||
16 * 16,
|
||||
5000,
|
||||
0 // disable setting bandwith, since APRS used fixed 10k bandwidth
|
||||
};
|
||||
|
||||
MessageHandlerRegistration message_handler_tx_progress {
|
||||
Message::ID::TXProgress,
|
||||
[this](const Message* const p) {
|
||||
const auto message = *reinterpret_cast<const TXProgressMessage*>(p);
|
||||
this->on_tx_progress(message.progress, message.done);
|
||||
}
|
||||
};
|
||||
std::string title() const override { return "APRS TX"; };
|
||||
|
||||
private:
|
||||
TxRadioState radio_state_{
|
||||
144390000 /* frequency */,
|
||||
1750000 /* bandwidth */,
|
||||
AFSK_TX_SAMPLERATE /* sampling rate */
|
||||
};
|
||||
app_settings::SettingsManager settings_{
|
||||
"tx_aprs", app_settings::Mode::TX};
|
||||
|
||||
std::string payload{""};
|
||||
|
||||
void start_tx();
|
||||
void generate_frame();
|
||||
void generate_frame_pos();
|
||||
void on_tx_progress(const uint32_t progress, const bool done);
|
||||
|
||||
Labels labels{
|
||||
{{0 * 8, 1 * 16}, "Source: SSID:", Theme::getInstance()->fg_light->foreground}, // 6 alphanum + SSID
|
||||
{{0 * 8, 2 * 16}, " Dest.: SSID:", Theme::getInstance()->fg_light->foreground},
|
||||
{{0 * 8, 4 * 16}, "Info field:", Theme::getInstance()->fg_light->foreground},
|
||||
};
|
||||
|
||||
SymField sym_source{
|
||||
{7 * 8, 1 * 16},
|
||||
6,
|
||||
SymField::Type::Alpha};
|
||||
|
||||
NumberField num_ssid_source{
|
||||
{19 * 8, 1 * 16},
|
||||
2,
|
||||
{0, 15},
|
||||
1,
|
||||
' '};
|
||||
|
||||
SymField sym_dest{
|
||||
{7 * 8, 2 * 16},
|
||||
6,
|
||||
SymField::Type::Alpha};
|
||||
|
||||
NumberField num_ssid_dest{
|
||||
{19 * 8, 2 * 16},
|
||||
2,
|
||||
{0, 15},
|
||||
1,
|
||||
' '};
|
||||
|
||||
Text text_payload{
|
||||
{0 * 8, 5 * 16, 30 * 8, 16},
|
||||
"-"};
|
||||
Button button_set{
|
||||
{0 * 8, 6 * 16, 80, 32},
|
||||
"Set"};
|
||||
|
||||
TransmitterView tx_view{
|
||||
16 * 16,
|
||||
5000,
|
||||
0 // disable setting bandwith, since APRS used fixed 10k bandwidth
|
||||
};
|
||||
|
||||
MessageHandlerRegistration message_handler_tx_progress{
|
||||
Message::ID::TXProgress,
|
||||
[this](const Message* const p) {
|
||||
const auto message = *reinterpret_cast<const TXProgressMessage*>(p);
|
||||
this->on_tx_progress(message.progress, message.done);
|
||||
}};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2016 Furrtek
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "ui_battinfo.hpp"
|
||||
|
||||
#include "event_m0.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "battery.hpp"
|
||||
#include <cstring>
|
||||
|
||||
using namespace portapack;
|
||||
|
||||
namespace ui {
|
||||
|
||||
void BattinfoView::focus() {
|
||||
button_exit.focus();
|
||||
}
|
||||
|
||||
// called each 1/60th of second, so 6 = 100ms
|
||||
void BattinfoView::on_timer() {
|
||||
if (++timer_counter == timer_period) {
|
||||
timer_counter = 0;
|
||||
update_result();
|
||||
}
|
||||
}
|
||||
|
||||
void BattinfoView::update_result() {
|
||||
if (!battery::BatteryManagement::isDetected()) {
|
||||
text_percent.set("UNKNOWN");
|
||||
text_voltage.set("UNKNOWN");
|
||||
text_current.set("-");
|
||||
text_charge.set("-");
|
||||
text_cycles.set("-");
|
||||
text_ttef.set("-");
|
||||
text_method.set("-");
|
||||
text_warn.set("");
|
||||
return;
|
||||
}
|
||||
bool uichg = false;
|
||||
uint8_t valid_mask = 0;
|
||||
battery::BatteryManagement::getBatteryInfo(valid_mask, percent, voltage, current);
|
||||
// update text fields
|
||||
if (percent <= 100 && (valid_mask & battery::BatteryManagement::BATT_VALID_VOLTAGE) == battery::BatteryManagement::BATT_VALID_VOLTAGE)
|
||||
text_percent.set(to_string_dec_uint(percent) + " %");
|
||||
else
|
||||
text_percent.set("UNKNOWN");
|
||||
if (voltage > 1 && (valid_mask & battery::BatteryManagement::BATT_VALID_VOLTAGE) == battery::BatteryManagement::BATT_VALID_VOLTAGE) {
|
||||
text_voltage.set(to_string_decimal(voltage / 1000.0, 3) + " V");
|
||||
} else {
|
||||
text_voltage.set("UNKNOWN");
|
||||
}
|
||||
if ((valid_mask & battery::BatteryManagement::BATT_VALID_CURRENT) == battery::BatteryManagement::BATT_VALID_CURRENT) {
|
||||
if (labels_opt.hidden()) uichg = true;
|
||||
labels_opt.hidden(false);
|
||||
text_current.hidden(false);
|
||||
text_charge.hidden(false);
|
||||
text_current.set(to_string_dec_int(current) + " mA");
|
||||
text_charge.set(current >= 0 ? "Charging" : "Discharging");
|
||||
labels_opt.hidden(false);
|
||||
|
||||
text_ttef.hidden(false);
|
||||
} else {
|
||||
if (!labels_opt.hidden()) uichg = true;
|
||||
labels_opt.hidden(true);
|
||||
text_current.hidden(true);
|
||||
text_charge.hidden(true);
|
||||
text_cycles.hidden(true);
|
||||
text_ttef.hidden(true);
|
||||
text_warn.set("");
|
||||
}
|
||||
if ((valid_mask & battery::BatteryManagement::BATT_VALID_CYCLES) == battery::BatteryManagement::BATT_VALID_CYCLES) {
|
||||
text_cycles.hidden(false);
|
||||
uint16_t cycles = battery::BatteryManagement::get_cycles();
|
||||
if (cycles < 2)
|
||||
text_warn.set("SoC improves after 2 cycles");
|
||||
else
|
||||
text_warn.set("");
|
||||
text_cycles.set(to_string_dec_uint(cycles));
|
||||
} else {
|
||||
text_cycles.hidden(true);
|
||||
text_warn.set("");
|
||||
}
|
||||
if ((valid_mask & battery::BatteryManagement::BATT_VALID_TTEF) == battery::BatteryManagement::BATT_VALID_TTEF) {
|
||||
text_ttef.hidden(false);
|
||||
float ttef = 0;
|
||||
if (current <= 0) {
|
||||
ttef = battery::BatteryManagement::get_tte();
|
||||
} else {
|
||||
ttef = battery::BatteryManagement::get_ttf();
|
||||
}
|
||||
|
||||
// Convert ttef to hours and minutes
|
||||
uint8_t hours = static_cast<uint8_t>(ttef);
|
||||
uint8_t minutes = static_cast<uint8_t>((ttef - hours) * 60 + 0.5); // +0.5 for rounding
|
||||
|
||||
// Create the formatted string
|
||||
std::string formatted_time;
|
||||
if (hours > 0) {
|
||||
formatted_time += to_string_dec_uint(hours) + "h ";
|
||||
}
|
||||
formatted_time += to_string_dec_uint(minutes) + "m";
|
||||
|
||||
text_ttef.set(formatted_time);
|
||||
} else {
|
||||
text_ttef.hidden(true);
|
||||
}
|
||||
if ((valid_mask & battery::BatteryManagement::BATT_VALID_PERCENT) == battery::BatteryManagement::BATT_VALID_PERCENT) {
|
||||
text_method.set("IC");
|
||||
button_mode.set_text("Volt");
|
||||
} else {
|
||||
text_method.set("Voltage");
|
||||
button_mode.set_text("IC");
|
||||
}
|
||||
if (uichg) set_dirty();
|
||||
// to update status bar too, send message in behalf of batt manager
|
||||
BatteryStateMessage msg{valid_mask, percent, current >= 0, voltage};
|
||||
EventDispatcher::send_message(msg);
|
||||
}
|
||||
|
||||
BattinfoView::BattinfoView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
add_children({&labels,
|
||||
&labels_opt,
|
||||
&text_percent,
|
||||
&text_voltage,
|
||||
&text_current,
|
||||
&text_charge,
|
||||
&text_method,
|
||||
&button_mode,
|
||||
&button_exit,
|
||||
&text_cycles,
|
||||
&text_warn,
|
||||
&text_ttef});
|
||||
|
||||
button_exit.on_select = [this, &nav](Button&) {
|
||||
nav.pop();
|
||||
};
|
||||
button_mode.on_select = [this, &nav](Button&) {
|
||||
if (button_mode.text() == "IC") {
|
||||
battery::BatteryManagement::set_calc_override(false);
|
||||
persistent_memory::set_ui_override_batt_calc(false);
|
||||
button_mode.set_text("Volt");
|
||||
} else {
|
||||
battery::BatteryManagement::set_calc_override(true);
|
||||
persistent_memory::set_ui_override_batt_calc(true);
|
||||
button_mode.set_text("IC");
|
||||
}
|
||||
};
|
||||
update_result();
|
||||
if (thread == nullptr) thread = chThdCreateFromHeap(NULL, 1024, NORMALPRIO + 10, BattinfoView::static_fn, this);
|
||||
}
|
||||
|
||||
msg_t BattinfoView::static_fn(void* arg) {
|
||||
auto obj = static_cast<BattinfoView*>(arg);
|
||||
while (!chThdShouldTerminate()) {
|
||||
chThdSleepMilliseconds(16);
|
||||
obj->on_timer();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
BattinfoView::~BattinfoView() {
|
||||
if (thread) {
|
||||
chThdTerminate(thread);
|
||||
chThdWait(thread);
|
||||
thread = nullptr;
|
||||
}
|
||||
}
|
||||
} // namespace ui
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2016 Furrtek
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __UI_BATTINFO_H__
|
||||
#define __UI_BATTINFO_H__
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "string_format.hpp"
|
||||
|
||||
namespace ui {
|
||||
class BattinfoView : public View {
|
||||
public:
|
||||
~BattinfoView();
|
||||
BattinfoView(NavigationView& nav);
|
||||
BattinfoView(const BattinfoView&) = delete;
|
||||
BattinfoView(BattinfoView&&) = delete;
|
||||
BattinfoView& operator=(const BattinfoView&) = delete;
|
||||
BattinfoView& operator=(BattinfoView&&) = delete;
|
||||
|
||||
void focus() override;
|
||||
std::string title() const override { return "Battery"; };
|
||||
|
||||
private:
|
||||
void update_result();
|
||||
void on_timer();
|
||||
NavigationView& nav_;
|
||||
uint16_t timer_period = 60;
|
||||
uint16_t timer_counter = 0;
|
||||
uint8_t percent = 0;
|
||||
uint16_t voltage = 0;
|
||||
int32_t current = 0;
|
||||
|
||||
Labels labels{
|
||||
{{2 * 8, 1 * 16}, "Percent:", Theme::getInstance()->fg_light->foreground},
|
||||
{{2 * 8, 2 * 16}, "Voltage:", Theme::getInstance()->fg_light->foreground},
|
||||
{{2 * 8, 3 * 16}, "Method:", Theme::getInstance()->fg_light->foreground},
|
||||
};
|
||||
|
||||
Labels labels_opt{
|
||||
{{2 * 8, 4 * 16}, "Current:", Theme::getInstance()->fg_light->foreground},
|
||||
{{2 * 8, 5 * 16}, "Charge:", Theme::getInstance()->fg_light->foreground},
|
||||
{{2 * 8, 6 * 16}, "TTF/E:", Theme::getInstance()->fg_light->foreground},
|
||||
{{2 * 8, 7 * 16}, "Cycles:", Theme::getInstance()->fg_light->foreground},
|
||||
{{2 * 8, 10 * 16}, "Change method:", Theme::getInstance()->fg_light->foreground},
|
||||
};
|
||||
|
||||
Text text_percent{
|
||||
{13 * 8, 1 * 16, 10 * 16, 16},
|
||||
"-"};
|
||||
Text text_voltage{
|
||||
{13 * 8, 2 * 16, 10 * 16, 16},
|
||||
"-"};
|
||||
Text text_method{
|
||||
{13 * 8, 3 * 16, 10 * 16, 16},
|
||||
"-"};
|
||||
Text text_current{
|
||||
{13 * 8, 4 * 16, 10 * 16, 16},
|
||||
"-"};
|
||||
Text text_charge{
|
||||
{13 * 8, 5 * 16, 10 * 16, 16},
|
||||
"-"};
|
||||
Text text_ttef{
|
||||
{13 * 8, 6 * 16, 10 * 16, 16},
|
||||
"-"};
|
||||
Text text_cycles{
|
||||
{13 * 8, 7 * 16, 10 * 16, 16},
|
||||
"-"};
|
||||
|
||||
Text text_warn{
|
||||
{2 * 8, 8 * 16, 30 * 8, 2 * 16},
|
||||
""};
|
||||
|
||||
Button button_mode{
|
||||
{2 * 8, 11 * 16 + 5, 5 * 16, 32},
|
||||
"Volt"};
|
||||
|
||||
Button button_exit{
|
||||
{72, 17 * 16, 96, 32},
|
||||
"Back"};
|
||||
static msg_t static_fn(void* arg);
|
||||
Thread* thread{nullptr};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
#endif /*__UI_BATTINFO__*/
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2016 Furrtek
|
||||
*
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
@@ -24,7 +24,6 @@
|
||||
#include "string_format.hpp"
|
||||
|
||||
#include "baseband_api.hpp"
|
||||
#include "cpld_update.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
@@ -32,341 +31,312 @@ using namespace portapack;
|
||||
namespace ui {
|
||||
|
||||
void BHTView::focus() {
|
||||
tx_view.focus();
|
||||
tx_view.focus();
|
||||
}
|
||||
|
||||
void BHTView::start_tx() {
|
||||
baseband::shutdown();
|
||||
|
||||
transmitter_model.set_baseband_bandwidth(1750000);
|
||||
|
||||
if (target_system == XYLOS) {
|
||||
|
||||
baseband::run_image(portapack::spi_flash::image_tag_tones);
|
||||
|
||||
view_xylos.generate_message();
|
||||
|
||||
//if (tx_mode == SINGLE) {
|
||||
progressbar.set_max(XY_TONE_COUNT);
|
||||
/*} else if (tx_mode == SCAN) {
|
||||
progressbar.set_max(XY_TONE_COUNT * view_xylos.get_scan_remaining());
|
||||
}*/
|
||||
|
||||
transmitter_model.set_sampling_rate(TONES_SAMPLERATE);
|
||||
transmitter_model.enable();
|
||||
|
||||
// Setup tones
|
||||
for (size_t c = 0; c < ccir_deltas.size(); c++)
|
||||
baseband::set_tone(c, ccir_deltas[c], XY_TONE_DURATION);
|
||||
|
||||
baseband::set_tones_config(transmitter_model.channel_bandwidth(), XY_SILENCE, XY_TONE_COUNT, false, false);
|
||||
|
||||
} else if (target_system == EPAR) {
|
||||
|
||||
baseband::run_image(portapack::spi_flash::image_tag_ook);
|
||||
|
||||
auto bitstream_length = view_EPAR.generate_message();
|
||||
|
||||
//if (tx_mode == SINGLE) {
|
||||
progressbar.set_max(2 * EPAR_REPEAT_COUNT);
|
||||
/*} else if (tx_mode == SCAN) {
|
||||
progressbar.set_max(2 * EPAR_REPEAT_COUNT * view_EPAR.get_scan_remaining());
|
||||
}*/
|
||||
|
||||
transmitter_model.set_sampling_rate(OOK_SAMPLERATE);
|
||||
transmitter_model.enable();
|
||||
baseband::shutdown();
|
||||
|
||||
baseband::set_ook_data(
|
||||
bitstream_length,
|
||||
EPAR_BIT_DURATION,
|
||||
EPAR_REPEAT_COUNT,
|
||||
encoder_defs[ENCODER_UM3750].pause_symbols
|
||||
);
|
||||
}
|
||||
transmitter_model.set_baseband_bandwidth(1750000);
|
||||
|
||||
if (target_system == XYLOS) {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_tones);
|
||||
|
||||
view_xylos.generate_message();
|
||||
|
||||
// if (tx_mode == SINGLE) {
|
||||
progressbar.set_max(XY_TONE_COUNT);
|
||||
/*} else if (tx_mode == SCAN) {
|
||||
progressbar.set_max(XY_TONE_COUNT * view_xylos.get_scan_remaining());
|
||||
}*/
|
||||
|
||||
transmitter_model.set_sampling_rate(TONES_SAMPLERATE);
|
||||
transmitter_model.enable();
|
||||
|
||||
// Setup tones
|
||||
for (size_t c = 0; c < ccir_deltas.size(); c++)
|
||||
baseband::set_tone(c, ccir_deltas[c], XY_TONE_DURATION);
|
||||
|
||||
baseband::set_tones_config(transmitter_model.channel_bandwidth(), XY_SILENCE, XY_TONE_COUNT, false, false);
|
||||
|
||||
} else if (target_system == EPAR) {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_ook);
|
||||
|
||||
auto bitstream_length = view_EPAR.generate_message();
|
||||
|
||||
// if (tx_mode == SINGLE) {
|
||||
progressbar.set_max(2 * EPAR_REPEAT_COUNT);
|
||||
/*} else if (tx_mode == SCAN) {
|
||||
progressbar.set_max(2 * EPAR_REPEAT_COUNT * view_EPAR.get_scan_remaining());
|
||||
}*/
|
||||
|
||||
transmitter_model.set_sampling_rate(OOK_SAMPLERATE);
|
||||
transmitter_model.enable();
|
||||
|
||||
baseband::set_ook_data(
|
||||
bitstream_length,
|
||||
EPAR_BIT_DURATION,
|
||||
EPAR_REPEAT_COUNT,
|
||||
encoder_defs[ENCODER_UM3750].pause_symbols);
|
||||
}
|
||||
}
|
||||
|
||||
void BHTView::stop_tx() {
|
||||
transmitter_model.disable();
|
||||
baseband::shutdown();
|
||||
tx_mode = IDLE;
|
||||
tx_view.set_transmitting(false);
|
||||
progressbar.set_value(0);
|
||||
transmitter_model.disable();
|
||||
baseband::shutdown();
|
||||
tx_mode = IDLE;
|
||||
tx_view.set_transmitting(false);
|
||||
progressbar.set_value(0);
|
||||
}
|
||||
|
||||
void BHTView::on_tx_progress(const uint32_t progress, const bool done) {
|
||||
if (target_system == XYLOS) {
|
||||
if (done) {
|
||||
if (tx_mode == SINGLE) {
|
||||
if (checkbox_flashing.value()) {
|
||||
// TODO: Thread !
|
||||
chThdSleepMilliseconds(field_speed.value() * 1000); // Dirty :(
|
||||
view_xylos.flip_relays();
|
||||
start_tx();
|
||||
} else
|
||||
stop_tx();
|
||||
} else if (tx_mode == SCAN) {
|
||||
if (view_xylos.increment_address())
|
||||
start_tx();
|
||||
else
|
||||
stop_tx(); // Reached end of scan range
|
||||
}
|
||||
} else
|
||||
progressbar.set_value(progress);
|
||||
} else if (target_system == EPAR) {
|
||||
if (done) {
|
||||
|
||||
if (!view_EPAR.half) {
|
||||
view_EPAR.half = true;
|
||||
start_tx(); // Start second half of transmission
|
||||
} else {
|
||||
view_EPAR.half = false;
|
||||
if (tx_mode == SINGLE) {
|
||||
if (checkbox_flashing.value()) {
|
||||
// TODO: Thread !
|
||||
chThdSleepMilliseconds(field_speed.value() * 1000); // Dirty :(
|
||||
view_EPAR.flip_relays();
|
||||
start_tx();
|
||||
} else
|
||||
stop_tx();
|
||||
} else if (tx_mode == SCAN) {
|
||||
if (view_EPAR.increment_address())
|
||||
start_tx();
|
||||
else
|
||||
stop_tx(); // Reached end of scan range
|
||||
}
|
||||
}
|
||||
} else
|
||||
progressbar.set_value(progress);
|
||||
}
|
||||
if (target_system == XYLOS) {
|
||||
if (done) {
|
||||
if (tx_mode == SINGLE) {
|
||||
if (checkbox_flashing.value()) {
|
||||
// TODO: Thread !
|
||||
chThdSleepMilliseconds(field_speed.value() * 1000); // Dirty :(
|
||||
view_xylos.flip_relays();
|
||||
start_tx();
|
||||
} else
|
||||
stop_tx();
|
||||
} else if (tx_mode == SCAN) {
|
||||
if (view_xylos.increment_address())
|
||||
start_tx();
|
||||
else
|
||||
stop_tx(); // Reached end of scan range
|
||||
}
|
||||
} else
|
||||
progressbar.set_value(progress);
|
||||
} else if (target_system == EPAR) {
|
||||
if (done) {
|
||||
if (!view_EPAR.half) {
|
||||
view_EPAR.half = true;
|
||||
start_tx(); // Start second half of transmission
|
||||
} else {
|
||||
view_EPAR.half = false;
|
||||
if (tx_mode == SINGLE) {
|
||||
if (checkbox_flashing.value()) {
|
||||
// TODO: Thread !
|
||||
chThdSleepMilliseconds(field_speed.value() * 1000); // Dirty :(
|
||||
view_EPAR.flip_relays();
|
||||
start_tx();
|
||||
} else
|
||||
stop_tx();
|
||||
} else if (tx_mode == SCAN) {
|
||||
if (view_EPAR.increment_address())
|
||||
start_tx();
|
||||
else
|
||||
stop_tx(); // Reached end of scan range
|
||||
}
|
||||
}
|
||||
} else
|
||||
progressbar.set_value(progress);
|
||||
}
|
||||
}
|
||||
|
||||
BHTView::~BHTView() {
|
||||
// save app settings
|
||||
app_settings.tx_frequency = transmitter_model.tuning_frequency();
|
||||
settings.save("tx_bht", &app_settings);
|
||||
|
||||
transmitter_model.disable();
|
||||
hackrf::cpld::load_sram_no_verify(); // to leave all RX ok, without ghost signal problem at the exit .
|
||||
baseband::shutdown(); // better this function at the end, not load_sram() that sometimes produces hang up.
|
||||
transmitter_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
BHTView::BHTView(NavigationView& nav) {
|
||||
add_children({
|
||||
&tab_view,
|
||||
&labels,
|
||||
&view_xylos,
|
||||
&view_EPAR,
|
||||
&checkbox_scan,
|
||||
&checkbox_flashing,
|
||||
&field_speed,
|
||||
&progressbar,
|
||||
&tx_view
|
||||
});
|
||||
|
||||
// load app settings
|
||||
auto rc = settings.load("tx_bht", &app_settings);
|
||||
if(rc == SETTINGS_OK) {
|
||||
transmitter_model.set_rf_amp(app_settings.tx_amp);
|
||||
transmitter_model.set_channel_bandwidth(app_settings.channel_bandwidth);
|
||||
transmitter_model.set_tuning_frequency(app_settings.tx_frequency);
|
||||
transmitter_model.set_tx_gain(app_settings.tx_gain);
|
||||
}
|
||||
add_children({&tab_view,
|
||||
&labels,
|
||||
&view_xylos,
|
||||
&view_EPAR,
|
||||
&checkbox_scan,
|
||||
&checkbox_flashing,
|
||||
&field_speed,
|
||||
&progressbar,
|
||||
&tx_view});
|
||||
|
||||
field_speed.set_value(1);
|
||||
|
||||
tx_view.on_edit_frequency = [this, &nav]() {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(receiver_model.tuning_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
transmitter_model.set_tuning_frequency(f);
|
||||
};
|
||||
};
|
||||
|
||||
tx_view.on_start = [this]() {
|
||||
if (tx_mode == IDLE) {
|
||||
if (checkbox_scan.value()) {
|
||||
tx_mode = SCAN;
|
||||
} else {
|
||||
tx_mode = SINGLE;
|
||||
}
|
||||
|
||||
progressbar.set_value(0);
|
||||
tx_view.set_transmitting(true);
|
||||
target_system = (target_system_t)tab_view.selected();
|
||||
view_EPAR.half = false;
|
||||
|
||||
start_tx();
|
||||
}
|
||||
};
|
||||
|
||||
tx_view.on_stop = [this]() {
|
||||
stop_tx();
|
||||
};
|
||||
field_speed.set_value(1);
|
||||
|
||||
tx_view.on_edit_frequency = [this, &nav]() {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(transmitter_model.target_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
transmitter_model.set_target_frequency(f);
|
||||
};
|
||||
};
|
||||
|
||||
tx_view.on_start = [this]() {
|
||||
if (tx_mode == IDLE) {
|
||||
if (checkbox_scan.value()) {
|
||||
tx_mode = SCAN;
|
||||
} else {
|
||||
tx_mode = SINGLE;
|
||||
}
|
||||
|
||||
progressbar.set_value(0);
|
||||
tx_view.set_transmitting(true);
|
||||
target_system = (target_system_t)tab_view.selected();
|
||||
view_EPAR.half = false;
|
||||
|
||||
start_tx();
|
||||
}
|
||||
};
|
||||
|
||||
tx_view.on_stop = [this]() {
|
||||
stop_tx();
|
||||
};
|
||||
}
|
||||
|
||||
bool EPARView::increment_address() {
|
||||
auto city_code = field_city.value();
|
||||
|
||||
if (city_code < EPAR_MAX_CITY) {
|
||||
field_city.set_value(city_code + 1);
|
||||
return true;
|
||||
} else
|
||||
return false;
|
||||
auto city_code = field_city.value();
|
||||
|
||||
if (city_code < EPAR_MAX_CITY) {
|
||||
field_city.set_value(city_code + 1);
|
||||
return true;
|
||||
} else
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t EPARView::get_scan_remaining() {
|
||||
return EPAR_MAX_CITY - field_city.value();
|
||||
return EPAR_MAX_CITY - field_city.value();
|
||||
}
|
||||
|
||||
void EPARView::flip_relays() {
|
||||
// Invert first relay's state
|
||||
relay_states[0].set_selected_index(relay_states[0].selected_index() ^ 1);
|
||||
// Invert first relay's state
|
||||
relay_states[0].set_selected_index(relay_states[0].selected_index() ^ 1);
|
||||
}
|
||||
|
||||
size_t EPARView::generate_message() {
|
||||
// R2, then R1
|
||||
return gen_message_ep(field_city.value(), field_group.selected_index_value(),
|
||||
half ? 0 : 1, relay_states[half].selected_index());
|
||||
// R2, then R1
|
||||
return gen_message_ep(field_city.value(), field_group.selected_index_value(),
|
||||
half ? 0 : 1, relay_states[half].selected_index());
|
||||
}
|
||||
|
||||
EPARView::EPARView(
|
||||
Rect parent_rect
|
||||
) : View(parent_rect) {
|
||||
|
||||
hidden(true);
|
||||
|
||||
add_children({
|
||||
&labels,
|
||||
&field_city,
|
||||
&field_group
|
||||
});
|
||||
Rect parent_rect)
|
||||
: View(parent_rect) {
|
||||
hidden(true);
|
||||
|
||||
field_city.set_value(0);
|
||||
field_group.set_selected_index(2);
|
||||
|
||||
field_city.on_change = [this](int32_t) { generate_message(); };
|
||||
field_group.on_change = [this](size_t, int32_t) { generate_message(); };
|
||||
|
||||
const auto relay_state_fn = [this](size_t, OptionsField::value_t) {
|
||||
generate_message();
|
||||
};
|
||||
|
||||
size_t n = 0;
|
||||
for (auto& relay_state : relay_states) {
|
||||
relay_state.on_change = relay_state_fn;
|
||||
relay_state.set_parent_rect({
|
||||
static_cast<Coord>(90 + (n * 36)),
|
||||
80,
|
||||
24, 24
|
||||
});
|
||||
relay_state.set_options(relay_options);
|
||||
add_child(&relay_state);
|
||||
n++;
|
||||
}
|
||||
add_children({&labels,
|
||||
&field_city,
|
||||
&field_group});
|
||||
|
||||
field_city.set_value(0);
|
||||
field_group.set_selected_index(2);
|
||||
field_city.on_change = [this](int32_t) { generate_message(); };
|
||||
field_group.on_change = [this](size_t, int32_t) { generate_message(); };
|
||||
|
||||
const auto relay_state_fn = [this](size_t, OptionsField::value_t) {
|
||||
generate_message();
|
||||
};
|
||||
|
||||
size_t n = 0;
|
||||
for (auto& relay_state : relay_states) {
|
||||
relay_state.set_parent_rect({static_cast<Coord>(90 + (n * 36)),
|
||||
80,
|
||||
24, 24});
|
||||
relay_state.set_options(relay_options);
|
||||
relay_state.on_change = relay_state_fn; // NB: set after set_options to avoid startup call.
|
||||
add_child(&relay_state);
|
||||
n++;
|
||||
}
|
||||
}
|
||||
|
||||
void EPARView::focus() {
|
||||
field_city.focus();
|
||||
field_city.focus();
|
||||
}
|
||||
|
||||
void XylosView::flip_relays() {
|
||||
// Invert first relay's state if not ignored
|
||||
size_t rs = relay_states[0].selected_index();
|
||||
|
||||
if (rs > 0)
|
||||
relay_states[0].set_selected_index(rs ^ 3);
|
||||
// Invert first relay's state if not ignored
|
||||
size_t rs = relay_states[0].selected_index();
|
||||
|
||||
if (rs > 0)
|
||||
relay_states[0].set_selected_index(rs ^ 3);
|
||||
}
|
||||
|
||||
bool XylosView::increment_address() {
|
||||
auto city_code = field_city.value();
|
||||
|
||||
if (city_code < XY_MAX_CITY) {
|
||||
field_city.set_value(city_code + 1);
|
||||
return true;
|
||||
} else
|
||||
return false;
|
||||
auto city_code = field_city.value();
|
||||
|
||||
if (city_code < XY_MAX_CITY) {
|
||||
field_city.set_value(city_code + 1);
|
||||
return true;
|
||||
} else
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t XylosView::get_scan_remaining() {
|
||||
return XY_MAX_CITY - field_city.value();
|
||||
return XY_MAX_CITY - field_city.value();
|
||||
}
|
||||
|
||||
void XylosView::generate_message() {
|
||||
gen_message_xy(field_header_a.value(), field_header_b.value(), field_city.value(), field_family.value(),
|
||||
checkbox_wcsubfamily.value(), field_subfamily.value(), checkbox_wcid.value(), field_receiver.value(),
|
||||
relay_states[0].selected_index(), relay_states[1].selected_index(),
|
||||
relay_states[2].selected_index(), relay_states[3].selected_index());
|
||||
gen_message_xy(field_header_a.value(), field_header_b.value(), field_city.value(), field_family.value(),
|
||||
checkbox_wcsubfamily.value(), field_subfamily.value(), checkbox_wcid.value(), field_receiver.value(),
|
||||
relay_states[0].selected_index(), relay_states[1].selected_index(),
|
||||
relay_states[2].selected_index(), relay_states[3].selected_index());
|
||||
}
|
||||
|
||||
XylosView::XylosView(
|
||||
Rect parent_rect
|
||||
) : View(parent_rect) {
|
||||
|
||||
hidden(true);
|
||||
|
||||
add_children({
|
||||
&labels,
|
||||
&field_header_a,
|
||||
&field_header_b,
|
||||
&field_city,
|
||||
&field_family,
|
||||
&field_subfamily,
|
||||
&checkbox_wcsubfamily,
|
||||
&field_receiver,
|
||||
&checkbox_wcid,
|
||||
//&button_seq,
|
||||
});
|
||||
Rect parent_rect)
|
||||
: View(parent_rect) {
|
||||
hidden(true);
|
||||
|
||||
field_header_a.set_value(0);
|
||||
field_header_b.set_value(0);
|
||||
field_city.set_value(10);
|
||||
field_family.set_value(1);
|
||||
field_subfamily.set_value(1);
|
||||
field_receiver.set_value(1);
|
||||
|
||||
const auto field_fn = [this](int32_t) {
|
||||
generate_message();
|
||||
};
|
||||
|
||||
field_header_a.on_change = field_fn;
|
||||
field_header_b.on_change = [this](int32_t) { generate_message(); };
|
||||
field_city.on_change = [this](int32_t) { generate_message(); };
|
||||
field_family.on_change = [this](int32_t) { generate_message(); };
|
||||
field_subfamily.on_change = [this](int32_t) { generate_message(); };
|
||||
field_receiver.on_change = [this](int32_t) { generate_message(); };
|
||||
|
||||
checkbox_wcsubfamily.on_select = [this](Checkbox&, bool v) {
|
||||
field_subfamily.set_focusable(!v);
|
||||
generate_message();
|
||||
};
|
||||
|
||||
checkbox_wcid.on_select = [this](Checkbox&, bool v) {
|
||||
field_receiver.set_focusable(!v);
|
||||
generate_message();
|
||||
};
|
||||
|
||||
checkbox_wcsubfamily.set_value(true);
|
||||
checkbox_wcid.set_value(true);
|
||||
|
||||
const auto relay_state_fn = [this](size_t, OptionsField::value_t) {
|
||||
generate_message();
|
||||
};
|
||||
|
||||
size_t n = 0;
|
||||
for (auto& relay_state : relay_states) {
|
||||
relay_state.on_change = relay_state_fn;
|
||||
relay_state.set_parent_rect({
|
||||
static_cast<Coord>(54 + (n * 36)),
|
||||
134,
|
||||
24, 24
|
||||
});
|
||||
relay_state.set_options(relay_options);
|
||||
add_child(&relay_state);
|
||||
n++;
|
||||
}
|
||||
add_children({
|
||||
&labels,
|
||||
&field_header_a,
|
||||
&field_header_b,
|
||||
&field_city,
|
||||
&field_family,
|
||||
&field_subfamily,
|
||||
&checkbox_wcsubfamily,
|
||||
&field_receiver,
|
||||
&checkbox_wcid,
|
||||
//&button_seq,
|
||||
});
|
||||
|
||||
field_header_a.set_value(0);
|
||||
field_header_b.set_value(0);
|
||||
field_city.set_value(10);
|
||||
field_family.set_value(1);
|
||||
field_subfamily.set_value(1);
|
||||
field_receiver.set_value(1);
|
||||
|
||||
const auto field_fn = [this](int32_t) {
|
||||
generate_message();
|
||||
};
|
||||
|
||||
field_header_a.on_change = field_fn;
|
||||
field_header_b.on_change = field_fn;
|
||||
field_city.on_change = field_fn;
|
||||
field_family.on_change = field_fn;
|
||||
field_subfamily.on_change = field_fn;
|
||||
field_receiver.on_change = field_fn;
|
||||
|
||||
checkbox_wcsubfamily.on_select = [this](Checkbox&, bool v) {
|
||||
field_subfamily.set_focusable(!v);
|
||||
generate_message();
|
||||
};
|
||||
|
||||
checkbox_wcid.on_select = [this](Checkbox&, bool v) {
|
||||
field_receiver.set_focusable(!v);
|
||||
generate_message();
|
||||
};
|
||||
|
||||
checkbox_wcsubfamily.set_value(true);
|
||||
checkbox_wcid.set_value(true);
|
||||
|
||||
const auto relay_state_fn = [this](size_t, OptionsField::value_t) {
|
||||
generate_message();
|
||||
};
|
||||
|
||||
size_t n = 0;
|
||||
for (auto& relay_state : relay_states) {
|
||||
relay_state.set_parent_rect({static_cast<Coord>(54 + (n * 36)),
|
||||
134,
|
||||
24, 24});
|
||||
relay_state.set_options(relay_options);
|
||||
relay_state.on_change = relay_state_fn; // NB: set after set_options to avoid startup call.
|
||||
add_child(&relay_state);
|
||||
n++;
|
||||
}
|
||||
}
|
||||
|
||||
void XylosView::focus() {
|
||||
field_city.focus();
|
||||
field_city.focus();
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2016 Furrtek
|
||||
*
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
@@ -25,7 +25,6 @@
|
||||
#include "ui_tabview.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_transmitter.hpp"
|
||||
#include "ui_font_fixed_8x16.hpp"
|
||||
|
||||
#include "bht.hpp"
|
||||
#include "bitmap.hpp"
|
||||
@@ -33,228 +32,206 @@
|
||||
#include "transmitter_model.hpp"
|
||||
#include "encoders.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "portapack.hpp"
|
||||
|
||||
namespace ui {
|
||||
|
||||
class XylosView : public View {
|
||||
public:
|
||||
XylosView(Rect parent_rect);
|
||||
|
||||
void focus() override;
|
||||
|
||||
void flip_relays();
|
||||
void generate_message();
|
||||
bool increment_address();
|
||||
uint32_t get_scan_remaining();
|
||||
|
||||
private:
|
||||
Labels labels {
|
||||
{ { 8 * 8, 1 * 8 }, "Header:", Color::light_grey() },
|
||||
{ { 4 * 8, 3 * 8 }, "City code:", Color::light_grey() },
|
||||
{ { 7 * 8, 5 * 8 }, "Family:", Color::light_grey() },
|
||||
{ { 2 * 8, 7 * 8 + 2 }, "Subfamily:", Color::light_grey() },
|
||||
{ { 2 * 8, 11 * 8 }, "Receiver ID:", Color::light_grey() },
|
||||
{ { 2 * 8, 14 * 8 }, "Relay:", Color::light_grey() }
|
||||
};
|
||||
|
||||
NumberField field_header_a {
|
||||
{ 16 * 8, 1 * 8 },
|
||||
2,
|
||||
{ 0, 99 },
|
||||
1,
|
||||
'0'
|
||||
};
|
||||
NumberField field_header_b {
|
||||
{ 18 * 8, 1 * 8 },
|
||||
2,
|
||||
{ 0, 99 },
|
||||
1,
|
||||
'0'
|
||||
};
|
||||
|
||||
NumberField field_city {
|
||||
{ 16 * 8, 3 * 8 },
|
||||
2,
|
||||
{ 0, XY_MAX_CITY },
|
||||
1,
|
||||
' '
|
||||
};
|
||||
|
||||
NumberField field_family {
|
||||
{ 16 * 8, 5 * 8 },
|
||||
1,
|
||||
{ 0, 9 },
|
||||
1,
|
||||
' '
|
||||
};
|
||||
public:
|
||||
XylosView(Rect parent_rect);
|
||||
|
||||
NumberField field_subfamily {
|
||||
{ 16 * 8, 7 * 8 + 2 },
|
||||
1,
|
||||
{ 0, 9 },
|
||||
1,
|
||||
' '
|
||||
};
|
||||
|
||||
Checkbox checkbox_wcsubfamily {
|
||||
{ 20 * 8, 6 * 8 + 6 },
|
||||
3,
|
||||
"All"
|
||||
};
|
||||
|
||||
NumberField field_receiver {
|
||||
{ 16 * 8, 11 * 8 },
|
||||
2,
|
||||
{ 0, 99 },
|
||||
1,
|
||||
'0'
|
||||
};
|
||||
Checkbox checkbox_wcid {
|
||||
{ 20 * 8, 10 * 8 + 4 },
|
||||
3,
|
||||
"All"
|
||||
};
|
||||
|
||||
std::array<ImageOptionsField, 4> relay_states { };
|
||||
|
||||
ImageOptionsField::options_t relay_options = {
|
||||
{ &bitmap_bulb_ignore, 0 },
|
||||
{ &bitmap_bulb_off, 1 },
|
||||
{ &bitmap_bulb_on, 2 }
|
||||
};
|
||||
void focus() override;
|
||||
|
||||
void flip_relays();
|
||||
void generate_message();
|
||||
bool increment_address();
|
||||
uint32_t get_scan_remaining();
|
||||
|
||||
private:
|
||||
Labels labels{
|
||||
{{8 * 8, 1 * 8}, "Header:", Theme::getInstance()->fg_light->foreground},
|
||||
{{4 * 8, 3 * 8}, "City code:", Theme::getInstance()->fg_light->foreground},
|
||||
{{7 * 8, 5 * 8}, "Family:", Theme::getInstance()->fg_light->foreground},
|
||||
{{2 * 8, 7 * 8 + 2}, "Subfamily:", Theme::getInstance()->fg_light->foreground},
|
||||
{{2 * 8, 11 * 8}, "Receiver ID:", Theme::getInstance()->fg_light->foreground},
|
||||
{{2 * 8, 14 * 8}, "Relay:", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
NumberField field_header_a{
|
||||
{16 * 8, 1 * 8},
|
||||
2,
|
||||
{0, 99},
|
||||
1,
|
||||
'0'};
|
||||
NumberField field_header_b{
|
||||
{18 * 8, 1 * 8},
|
||||
2,
|
||||
{0, 99},
|
||||
1,
|
||||
'0'};
|
||||
|
||||
NumberField field_city{
|
||||
{16 * 8, 3 * 8},
|
||||
2,
|
||||
{0, XY_MAX_CITY},
|
||||
1,
|
||||
' '};
|
||||
|
||||
NumberField field_family{
|
||||
{16 * 8, 5 * 8},
|
||||
1,
|
||||
{0, 9},
|
||||
1,
|
||||
' '};
|
||||
|
||||
NumberField field_subfamily{
|
||||
{16 * 8, 7 * 8 + 2},
|
||||
1,
|
||||
{0, 9},
|
||||
1,
|
||||
' '};
|
||||
|
||||
Checkbox checkbox_wcsubfamily{
|
||||
{20 * 8, 6 * 8 + 6},
|
||||
3,
|
||||
"All"};
|
||||
|
||||
NumberField field_receiver{
|
||||
{16 * 8, 11 * 8},
|
||||
2,
|
||||
{0, 99},
|
||||
1,
|
||||
'0'};
|
||||
Checkbox checkbox_wcid{
|
||||
{20 * 8, 10 * 8 + 4},
|
||||
3,
|
||||
"All"};
|
||||
|
||||
std::array<ImageOptionsField, 4> relay_states{};
|
||||
|
||||
ImageOptionsField::options_t relay_options = {
|
||||
{&bitmap_bulb_ignore, 0},
|
||||
{&bitmap_bulb_off, 1},
|
||||
{&bitmap_bulb_on, 2}};
|
||||
};
|
||||
|
||||
class EPARView : public View {
|
||||
public:
|
||||
EPARView(Rect parent_rect);
|
||||
|
||||
void focus() override;
|
||||
|
||||
void flip_relays();
|
||||
size_t generate_message();
|
||||
bool increment_address();
|
||||
uint32_t get_scan_remaining();
|
||||
public:
|
||||
EPARView(Rect parent_rect);
|
||||
|
||||
bool half { false };
|
||||
|
||||
private:
|
||||
Labels labels {
|
||||
{ { 4 * 8, 1 * 8 }, "City code:", Color::light_grey() },
|
||||
{ { 8 * 8, 3 * 8 }, "Group:", Color::light_grey() },
|
||||
{ { 8 * 8, 7 * 8 }, "Relay:", Color::light_grey() }
|
||||
};
|
||||
|
||||
NumberField field_city {
|
||||
{ 16 * 8, 1 * 8 },
|
||||
3,
|
||||
{ 0, EPAR_MAX_CITY },
|
||||
1,
|
||||
'0'
|
||||
};
|
||||
|
||||
OptionsField field_group {
|
||||
{ 16 * 8, 3 * 8 },
|
||||
2,
|
||||
{
|
||||
{ "A ", 2 }, // See receiver PCB
|
||||
{ "B ", 1 },
|
||||
{ "C ", 0 },
|
||||
{ "TP", 3 }
|
||||
}
|
||||
};
|
||||
|
||||
std::array<ImageOptionsField, 2> relay_states { };
|
||||
|
||||
ImageOptionsField::options_t relay_options = {
|
||||
{ &bitmap_bulb_off, 0 },
|
||||
{ &bitmap_bulb_on, 1 }
|
||||
};
|
||||
void focus() override;
|
||||
|
||||
void flip_relays();
|
||||
size_t generate_message();
|
||||
bool increment_address();
|
||||
uint32_t get_scan_remaining();
|
||||
|
||||
bool half{false};
|
||||
|
||||
private:
|
||||
Labels labels{
|
||||
{{4 * 8, 1 * 8}, "City code:", Theme::getInstance()->fg_light->foreground},
|
||||
{{8 * 8, 3 * 8}, "Group:", Theme::getInstance()->fg_light->foreground},
|
||||
{{8 * 8, 7 * 8}, "Relay:", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
NumberField field_city{
|
||||
{16 * 8, 1 * 8},
|
||||
3,
|
||||
{0, EPAR_MAX_CITY},
|
||||
1,
|
||||
'0'};
|
||||
|
||||
OptionsField field_group{
|
||||
{16 * 8, 3 * 8},
|
||||
2,
|
||||
{{"A ", 2}, // See receiver PCB
|
||||
{"B ", 1},
|
||||
{"C ", 0},
|
||||
{"TP", 3}}};
|
||||
|
||||
std::array<ImageOptionsField, 2> relay_states{};
|
||||
|
||||
ImageOptionsField::options_t relay_options = {
|
||||
{&bitmap_bulb_off, 0},
|
||||
{&bitmap_bulb_on, 1}};
|
||||
};
|
||||
|
||||
class BHTView : public View {
|
||||
public:
|
||||
BHTView(NavigationView& nav);
|
||||
~BHTView();
|
||||
public:
|
||||
BHTView(NavigationView& nav);
|
||||
~BHTView();
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "BHT TX"; };
|
||||
void focus() override;
|
||||
|
||||
private:
|
||||
// app save settings
|
||||
std::app_settings settings { };
|
||||
std::app_settings::AppSettings app_settings { };
|
||||
std::string title() const override { return "BHT TX"; };
|
||||
|
||||
void on_tx_progress(const uint32_t progress, const bool done);
|
||||
void start_tx();
|
||||
void stop_tx();
|
||||
|
||||
enum target_system_t {
|
||||
XYLOS = 0,
|
||||
EPAR = 1
|
||||
};
|
||||
|
||||
target_system_t target_system = { };
|
||||
|
||||
enum tx_modes {
|
||||
IDLE = 0,
|
||||
SINGLE,
|
||||
SCAN
|
||||
};
|
||||
|
||||
tx_modes tx_mode = IDLE;
|
||||
|
||||
Rect view_rect = { 0, 3 * 8, 240, 176 };
|
||||
|
||||
XylosView view_xylos { view_rect };
|
||||
EPARView view_EPAR { view_rect };
|
||||
|
||||
TabView tab_view {
|
||||
{ "Xylos", Color::cyan(), &view_xylos },
|
||||
{ "EPAR", Color::green(), &view_EPAR }
|
||||
};
|
||||
|
||||
Labels labels {
|
||||
{ { 29 * 8, 14 * 16 + 4 }, "s", Color::light_grey() }
|
||||
};
|
||||
|
||||
Checkbox checkbox_scan {
|
||||
{ 1 * 8, 25 * 8 },
|
||||
4,
|
||||
"Scan"
|
||||
};
|
||||
|
||||
Checkbox checkbox_flashing {
|
||||
{ 16 * 8, 25 * 8 },
|
||||
8,
|
||||
"Flashing"
|
||||
};
|
||||
NumberField field_speed {
|
||||
{ 26 * 8, 25 * 8 + 4 },
|
||||
2,
|
||||
{ 1, 99 },
|
||||
1,
|
||||
' '
|
||||
};
|
||||
|
||||
ProgressBar progressbar {
|
||||
{ 0 * 8, 29 * 8, 30 * 8, 16 },
|
||||
};
|
||||
|
||||
TransmitterView tx_view {
|
||||
16 * 16,
|
||||
10000,
|
||||
12
|
||||
};
|
||||
|
||||
MessageHandlerRegistration message_handler_tx_progress {
|
||||
Message::ID::TXProgress,
|
||||
[this](const Message* const p) {
|
||||
const auto message = *reinterpret_cast<const TXProgressMessage*>(p);
|
||||
this->on_tx_progress(message.progress, message.done);
|
||||
}
|
||||
};
|
||||
private:
|
||||
TxRadioState radio_state_{};
|
||||
app_settings::SettingsManager settings_{
|
||||
"tx_bht", app_settings::Mode::TX};
|
||||
|
||||
void on_tx_progress(const uint32_t progress, const bool done);
|
||||
void start_tx();
|
||||
void stop_tx();
|
||||
|
||||
enum target_system_t {
|
||||
XYLOS = 0,
|
||||
EPAR = 1
|
||||
};
|
||||
|
||||
target_system_t target_system = {};
|
||||
|
||||
enum tx_modes {
|
||||
IDLE = 0,
|
||||
SINGLE,
|
||||
SCAN
|
||||
};
|
||||
|
||||
tx_modes tx_mode = IDLE;
|
||||
|
||||
Rect view_rect = {0, 3 * 8, 240, 176};
|
||||
|
||||
XylosView view_xylos{view_rect};
|
||||
EPARView view_EPAR{view_rect};
|
||||
|
||||
TabView tab_view{
|
||||
{"Xylos", Theme::getInstance()->fg_cyan->foreground, &view_xylos},
|
||||
{"EPAR", Theme::getInstance()->fg_green->foreground, &view_EPAR}};
|
||||
|
||||
Labels labels{
|
||||
{{29 * 8, 14 * 16 + 4}, "s", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
Checkbox checkbox_scan{
|
||||
{1 * 8, 25 * 8},
|
||||
4,
|
||||
"Scan"};
|
||||
|
||||
Checkbox checkbox_flashing{
|
||||
{16 * 8, 25 * 8},
|
||||
8,
|
||||
"Flashing"};
|
||||
NumberField field_speed{
|
||||
{26 * 8, 25 * 8 + 4},
|
||||
2,
|
||||
{1, 99},
|
||||
1,
|
||||
' '};
|
||||
|
||||
ProgressBar progressbar{
|
||||
{0 * 8, 29 * 8, 30 * 8, 16},
|
||||
};
|
||||
|
||||
TransmitterView tx_view{
|
||||
16 * 16,
|
||||
10000,
|
||||
12};
|
||||
|
||||
MessageHandlerRegistration message_handler_tx_progress{
|
||||
Message::ID::TXProgress,
|
||||
[this](const Message* const p) {
|
||||
const auto message = *reinterpret_cast<const TXProgressMessage*>(p);
|
||||
this->on_tx_progress(message.progress, message.done);
|
||||
}};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright (C) 2024 HTotoo
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "ui_bmp_file_viewer.hpp"
|
||||
|
||||
extern ui::SystemView* system_view_ptr;
|
||||
|
||||
using namespace portapack;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace ui {
|
||||
|
||||
BMPFileViewer::BMPFileViewer(
|
||||
NavigationView& nav,
|
||||
const std::filesystem::path& path)
|
||||
: nav_{nav},
|
||||
path_{path} {
|
||||
add_children(
|
||||
{&bmp});
|
||||
bmp.set_enter_pass(true); // pass the enter key to me, so i can exit. this will disable zoom + pos reset
|
||||
set_focusable(true);
|
||||
system_view_ptr->set_app_fullscreen(true);
|
||||
}
|
||||
|
||||
BMPFileViewer::~BMPFileViewer() {
|
||||
system_view_ptr->set_app_fullscreen(false);
|
||||
}
|
||||
|
||||
void BMPFileViewer::focus() {
|
||||
bmp.focus();
|
||||
}
|
||||
|
||||
bool BMPFileViewer::on_key(KeyEvent k) {
|
||||
if (k == KeyEvent::Select) {
|
||||
nav_.pop();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void BMPFileViewer::paint(Painter&) {
|
||||
bmp.load_bmp(path_);
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright (C) 2024 HTotoo
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __UI_BMP_FILE_VIEWER_H__
|
||||
#define __UI_BMP_FILE_VIEWER_H__
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_painter.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
#include "file.hpp"
|
||||
#include "ui_bmpview.hpp"
|
||||
|
||||
namespace ui {
|
||||
|
||||
class BMPFileViewer : public View {
|
||||
public:
|
||||
BMPFileViewer(NavigationView& nav, const std::filesystem::path& path);
|
||||
~BMPFileViewer();
|
||||
bool on_key(KeyEvent key) override;
|
||||
void paint(Painter& painter) override;
|
||||
void focus() override;
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
std::filesystem::path path_{};
|
||||
BMPViewer bmp{{0, 0, 240, 320}};
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
|
||||
#endif // __UI_BMP_FILE_VIEWER_H__
|
||||
@@ -37,138 +37,94 @@ using namespace modems;
|
||||
namespace ui {
|
||||
|
||||
void BTLERxView::focus() {
|
||||
field_frequency.focus();
|
||||
field_frequency.focus();
|
||||
}
|
||||
|
||||
void BTLERxView::update_freq(rf::Frequency f) {
|
||||
receiver_model.set_tuning_frequency(f);
|
||||
}
|
||||
BTLERxView::BTLERxView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_btle_rx);
|
||||
|
||||
BTLERxView::BTLERxView(NavigationView& nav) {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_btle_rx);
|
||||
|
||||
add_children({
|
||||
&rssi,
|
||||
&channel,
|
||||
&field_rf_amp,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&field_frequency,
|
||||
&text_debug,
|
||||
&button_modem_setup,
|
||||
&record_view,
|
||||
&console
|
||||
});
|
||||
|
||||
// load app settings
|
||||
auto rc = settings.load("rx_btle", &app_settings);
|
||||
if(rc == SETTINGS_OK) {
|
||||
field_lna.set_value(app_settings.lna);
|
||||
field_vga.set_value(app_settings.vga);
|
||||
field_rf_amp.set_value(app_settings.rx_amp);
|
||||
}
|
||||
add_children({&rssi,
|
||||
&channel,
|
||||
&field_rf_amp,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&field_frequency,
|
||||
&button_modem_setup,
|
||||
&console});
|
||||
|
||||
// Auto-configure modem for LCR RX (TODO: remove later)
|
||||
auto def_bell202 = &modem_defs[0];
|
||||
persistent_memory::set_modem_baudrate(def_bell202->baudrate);
|
||||
serial_format_t serial_format;
|
||||
serial_format.data_bits = 7;
|
||||
serial_format.parity = EVEN;
|
||||
serial_format.stop_bits = 1;
|
||||
serial_format.bit_order = LSB_FIRST;
|
||||
persistent_memory::set_serial_format(serial_format);
|
||||
|
||||
// DEBUG
|
||||
record_view.on_error = [&nav](std::string message) {
|
||||
nav.display_modal("Error", message);
|
||||
};
|
||||
record_view.set_sampling_rate(24000);
|
||||
|
||||
// Auto-configure modem for LCR RX (will be removed later)
|
||||
update_freq(2426000000);
|
||||
auto def_bell202 = &modem_defs[0];
|
||||
persistent_memory::set_modem_baudrate(def_bell202->baudrate);
|
||||
serial_format_t serial_format;
|
||||
serial_format.data_bits = 7;
|
||||
serial_format.parity = EVEN;
|
||||
serial_format.stop_bits = 1;
|
||||
serial_format.bit_order = LSB_FIRST;
|
||||
persistent_memory::set_serial_format(serial_format);
|
||||
|
||||
field_frequency.set_value(receiver_model.tuning_frequency());
|
||||
field_frequency.set_step(100);
|
||||
field_frequency.on_change = [this](rf::Frequency f) {
|
||||
update_freq(f);
|
||||
};
|
||||
field_frequency.on_edit = [this, &nav]() {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(receiver_model.tuning_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
update_freq(f);
|
||||
field_frequency.set_value(f);
|
||||
};
|
||||
};
|
||||
field_frequency.set_step(100);
|
||||
|
||||
button_modem_setup.on_select = [&nav](Button&) {
|
||||
nav.push<ModemSetupView>();
|
||||
};
|
||||
|
||||
|
||||
// Auto-configure modem for LCR RX (will be removed later)
|
||||
baseband::set_btle(persistent_memory::modem_baudrate(), 8, 0, false);
|
||||
|
||||
audio::set_rate(audio::Rate::Hz_24000);
|
||||
audio::output::start();
|
||||
|
||||
receiver_model.set_sampling_rate(4000000);
|
||||
receiver_model.set_baseband_bandwidth(4000000);
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::WidebandFMAudio);
|
||||
receiver_model.enable();
|
||||
button_modem_setup.on_select = [&nav](Button&) {
|
||||
nav.push<ModemSetupView>();
|
||||
};
|
||||
|
||||
// Auto-configure modem for LCR RX (will be removed later)
|
||||
baseband::set_btlerx(channel_number);
|
||||
|
||||
audio::set_rate(audio::Rate::Hz_24000);
|
||||
audio::output::start();
|
||||
|
||||
receiver_model.enable();
|
||||
}
|
||||
|
||||
void BTLERxView::on_data(uint32_t value, bool is_data) {
|
||||
//std::string str_console = "\x1B";
|
||||
std::string str_console = "";
|
||||
if (is_data) {
|
||||
// Colorize differently after message splits
|
||||
//str_console += (char)((console_color & 3) + 9);
|
||||
|
||||
//value &= 0xFF; // ABCDEFGH
|
||||
//value = ((value & 0xF0) >> 4) | ((value & 0x0F) << 4); // EFGHABCD
|
||||
//value = ((value & 0xCC) >> 2) | ((value & 0x33) << 2); // GHEFCDAB
|
||||
//value = ((value & 0xAA) >> 1) | ((value & 0x55) << 1); // HGFEDCBA
|
||||
//value &= 0x7F; // Ignore parity, which is the MSB now
|
||||
|
||||
//if ((value >= 32) && (value < 127)) {
|
||||
// str_console += (char)value; // Printable
|
||||
//}
|
||||
|
||||
//str_console += (char)'A';
|
||||
//str_console += (char)value;
|
||||
//str_console += "[" + to_string_hex(value, 2) + "]";
|
||||
str_console += ":" + to_string_hex(value, 2) ;
|
||||
console.write(str_console);
|
||||
|
||||
|
||||
|
||||
/*if ((value != 0x7F) && (prev_value == 0x7F)) {
|
||||
// Message split
|
||||
console.writeln("");
|
||||
console_color++;
|
||||
|
||||
|
||||
}*/
|
||||
//prev_value = value;
|
||||
} else {
|
||||
// Baudrate estimation
|
||||
//text_debug.set("~" + to_string_dec_uint(value));
|
||||
if (value == 'A')
|
||||
{console.write("mac");}
|
||||
else if (value == 'B')
|
||||
{console.writeln("");}
|
||||
//console.writeln("");
|
||||
}
|
||||
// std::string str_console = "\x1B";
|
||||
std::string str_console = "";
|
||||
if (is_data) {
|
||||
// Colorize differently after message splits
|
||||
// str_console += (char)((console_color & 3) + 9);
|
||||
|
||||
// value &= 0xFF; // ABCDEFGH
|
||||
// value = ((value & 0xF0) >> 4) | ((value & 0x0F) << 4); // EFGHABCD
|
||||
// value = ((value & 0xCC) >> 2) | ((value & 0x33) << 2); // GHEFCDAB
|
||||
// value = ((value & 0xAA) >> 1) | ((value & 0x55) << 1); // HGFEDCBA
|
||||
// value &= 0x7F; // Ignore parity, which is the MSB now
|
||||
|
||||
// if ((value >= 32) && (value < 127)) {
|
||||
// str_console += (char)value; // Printable
|
||||
// }
|
||||
|
||||
// str_console += (char)'A';
|
||||
// str_console += (char)value;
|
||||
// str_console += "[" + to_string_hex(value, 2) + "]";
|
||||
str_console += ":" + to_string_hex(value, 2);
|
||||
console.write(str_console);
|
||||
|
||||
/*if ((value != 0x7F) && (prev_value == 0x7F)) {
|
||||
// Message split
|
||||
console.writeln("");
|
||||
console_color++;
|
||||
|
||||
|
||||
}*/
|
||||
// prev_value = value;
|
||||
} else {
|
||||
// Baudrate estimation
|
||||
// text_debug.set("~" + to_string_dec_uint(value));
|
||||
if (value == 'A') {
|
||||
console.write("mac");
|
||||
} else if (value == 'B') {
|
||||
console.writeln("");
|
||||
}
|
||||
// console.writeln("");
|
||||
}
|
||||
}
|
||||
|
||||
BTLERxView::~BTLERxView() {
|
||||
|
||||
// save app settings
|
||||
app_settings.rx_frequency = field_frequency.value();
|
||||
settings.save("rx_btle", &app_settings);
|
||||
|
||||
audio::output::stop();
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
audio::output::stop();
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -27,87 +27,71 @@
|
||||
#include "ui.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "ui_record_view.hpp" // DEBUG
|
||||
#include "radio_state.hpp"
|
||||
#include "ui_record_view.hpp"
|
||||
|
||||
#include "utility.hpp"
|
||||
|
||||
namespace ui {
|
||||
|
||||
class BTLERxView : public View {
|
||||
public:
|
||||
BTLERxView(NavigationView& nav);
|
||||
~BTLERxView();
|
||||
public:
|
||||
BTLERxView(NavigationView& nav);
|
||||
~BTLERxView();
|
||||
|
||||
void focus() override;
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "BTLE RX"; };
|
||||
|
||||
private:
|
||||
void on_data(uint32_t value, bool is_data);
|
||||
|
||||
std::string title() const override { return "BTLE RX"; };
|
||||
|
||||
// app save settings
|
||||
std::app_settings settings { };
|
||||
std::app_settings::AppSettings app_settings { };
|
||||
private:
|
||||
void on_data(uint32_t value, bool is_data);
|
||||
|
||||
uint8_t console_color { 0 };
|
||||
uint32_t prev_value { 0 };
|
||||
std::string str_log { "" };
|
||||
NavigationView& nav_;
|
||||
RxRadioState radio_state_{
|
||||
2426000000 /* frequency */,
|
||||
4000000 /* bandwidth */,
|
||||
4000000 /* sampling rate */,
|
||||
ReceiverModel::Mode::WidebandFMAudio};
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_btle", app_settings::Mode::RX};
|
||||
|
||||
RFAmpField field_rf_amp {
|
||||
{ 13 * 8, 0 * 16 }
|
||||
};
|
||||
LNAGainField field_lna {
|
||||
{ 15 * 8, 0 * 16 }
|
||||
};
|
||||
VGAGainField field_vga {
|
||||
{ 18 * 8, 0 * 16 }
|
||||
};
|
||||
RSSI rssi {
|
||||
{ 21 * 8, 0, 6 * 8, 4 },
|
||||
};
|
||||
Channel channel {
|
||||
{ 21 * 8, 5, 6 * 8, 4 },
|
||||
};
|
||||
|
||||
FrequencyField field_frequency {
|
||||
{ 0 * 8, 0 * 16 },
|
||||
};
|
||||
|
||||
Text text_debug {
|
||||
{ 0 * 8, 1 * 16, 10 * 8, 16 },
|
||||
"DEBUG"
|
||||
};
|
||||
|
||||
|
||||
Button button_modem_setup {
|
||||
{ 12 * 8, 1 * 16, 96, 24 },
|
||||
"Modem setup"
|
||||
};
|
||||
|
||||
// DEBUG
|
||||
RecordView record_view {
|
||||
{ 0 * 8, 3 * 16, 30 * 8, 1 * 16 },
|
||||
u"AFS_????", RecordView::FileType::WAV, 4096, 4
|
||||
};
|
||||
|
||||
Console console {
|
||||
{ 0, 4 * 16, 240, 240 }
|
||||
};
|
||||
uint8_t console_color{0};
|
||||
uint32_t prev_value{0};
|
||||
|
||||
void update_freq(rf::Frequency f);
|
||||
//void on_data_afsk(const AFSKDataMessage& message);
|
||||
|
||||
MessageHandlerRegistration message_handler_packet {
|
||||
Message::ID::AFSKData,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const AFSKDataMessage*>(p);
|
||||
this->on_data(message->value, message->is_data);
|
||||
}
|
||||
};
|
||||
static constexpr uint8_t channel_number = 37;
|
||||
|
||||
RFAmpField field_rf_amp{
|
||||
{13 * 8, 0 * 16}};
|
||||
LNAGainField field_lna{
|
||||
{15 * 8, 0 * 16}};
|
||||
VGAGainField field_vga{
|
||||
{18 * 8, 0 * 16}};
|
||||
RSSI rssi{
|
||||
{21 * 8, 0, 6 * 8, 4}};
|
||||
Channel channel{
|
||||
{21 * 8, 5, 6 * 8, 4}};
|
||||
|
||||
RxFrequencyField field_frequency{
|
||||
{0 * 8, 0 * 16},
|
||||
nav_};
|
||||
|
||||
Button button_modem_setup{
|
||||
{240 - 12 * 8, 1 * 16, 96, 24},
|
||||
"Modem setup"};
|
||||
|
||||
Console console{
|
||||
{0, 4 * 16, 240, 240}};
|
||||
|
||||
MessageHandlerRegistration message_handler_packet{
|
||||
Message::ID::AFSKData,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const AFSKDataMessage*>(p);
|
||||
this->on_data(message->value, message->is_data);
|
||||
}};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
#endif/*__UI_BTLE_RX_H__*/
|
||||
#endif /*__UI_BTLE_RX_H__*/
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2016 Furrtek
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "ui_coasterp.hpp"
|
||||
|
||||
#include "baseband_api.hpp"
|
||||
#include "cpld_update.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <stdio.h>
|
||||
|
||||
using namespace portapack;
|
||||
|
||||
namespace ui {
|
||||
|
||||
void CoasterPagerView::focus() {
|
||||
sym_data.focus();
|
||||
}
|
||||
|
||||
CoasterPagerView::~CoasterPagerView() {
|
||||
// save app settings
|
||||
app_settings.tx_frequency = transmitter_model.tuning_frequency();
|
||||
settings.save("tx_coaster", &app_settings);
|
||||
|
||||
transmitter_model.disable();
|
||||
hackrf::cpld::load_sram_no_verify(); // to leave all RX ok, without ghost signal problem at the exit .
|
||||
baseband::shutdown(); // better this function at the end, not load_sram() that sometimes produces hang up.
|
||||
}
|
||||
|
||||
void CoasterPagerView::generate_frame() {
|
||||
uint8_t frame[19];
|
||||
uint32_t c;
|
||||
|
||||
// Preamble (8 bytes)
|
||||
for (c = 0; c < 8; c++)
|
||||
frame[c] = 0x55; // Isn't this 0xAA ?
|
||||
|
||||
// Sync word
|
||||
frame[8] = 0x2D;
|
||||
frame[9] = 0xD4;
|
||||
|
||||
// Data length
|
||||
frame[10] = 8;
|
||||
|
||||
// Data
|
||||
for (c = 0; c < 8; c++)
|
||||
frame[c + 11] = (sym_data.get_sym(c * 2) << 4) | sym_data.get_sym(c * 2 + 1);
|
||||
|
||||
// Copy for baseband
|
||||
memcpy(shared_memory.bb_data.data, frame, 19);
|
||||
}
|
||||
|
||||
void CoasterPagerView::start_tx() {
|
||||
generate_frame();
|
||||
|
||||
transmitter_model.set_sampling_rate(2280000);
|
||||
transmitter_model.set_baseband_bandwidth(1750000);
|
||||
transmitter_model.enable();
|
||||
|
||||
baseband::set_fsk_data(19 * 8, 2280000 / 1000, 5000, 32);
|
||||
}
|
||||
|
||||
void CoasterPagerView::on_tx_progress(const uint32_t progress, const bool done) {
|
||||
(void)progress;
|
||||
|
||||
uint16_t address = 0;
|
||||
uint32_t c;
|
||||
|
||||
if (done) {
|
||||
if (tx_mode == SINGLE) {
|
||||
transmitter_model.disable();
|
||||
tx_mode = IDLE;
|
||||
tx_view.set_transmitting(false);
|
||||
} else if (tx_mode == SCAN) {
|
||||
// Increment address
|
||||
|
||||
for (c = 0; c < 4; c++) {
|
||||
address <<= 4;
|
||||
address |= sym_data.get_sym(12 + c);
|
||||
}
|
||||
|
||||
address++;
|
||||
|
||||
for (c = 0; c < 4; c++) {
|
||||
sym_data.set_sym(15 - c, address & 0x0F);
|
||||
address >>= 4;
|
||||
}
|
||||
|
||||
start_tx();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CoasterPagerView::CoasterPagerView(NavigationView& nav) {
|
||||
const uint8_t data_init[8] = { 0x44, 0x01, 0x3B, 0x30, 0x30, 0x30, 0x34, 0xBC };
|
||||
uint32_t c;
|
||||
|
||||
baseband::run_image(portapack::spi_flash::image_tag_fsktx);
|
||||
|
||||
add_children({
|
||||
&labels,
|
||||
&sym_data,
|
||||
&checkbox_scan,
|
||||
&text_message,
|
||||
&tx_view
|
||||
});
|
||||
|
||||
// load app settings
|
||||
auto rc = settings.load("tx_coaster", &app_settings);
|
||||
if(rc == SETTINGS_OK) {
|
||||
transmitter_model.set_rf_amp(app_settings.tx_amp);
|
||||
transmitter_model.set_channel_bandwidth(app_settings.channel_bandwidth);
|
||||
transmitter_model.set_tuning_frequency(app_settings.tx_frequency);
|
||||
transmitter_model.set_tx_gain(app_settings.tx_gain);
|
||||
}
|
||||
|
||||
// Bytes to nibbles
|
||||
for (c = 0; c < 16; c++)
|
||||
sym_data.set_sym(c, (data_init[c >> 1] >> ((c & 1) ? 0 : 4)) & 0x0F);
|
||||
|
||||
checkbox_scan.set_value(false);
|
||||
|
||||
generate_frame();
|
||||
|
||||
tx_view.on_edit_frequency = [this, &nav]() {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(receiver_model.tuning_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
receiver_model.set_tuning_frequency(f);
|
||||
};
|
||||
};
|
||||
|
||||
tx_view.on_start = [this]() {
|
||||
if (tx_mode == IDLE) {
|
||||
if (checkbox_scan.value())
|
||||
tx_mode = SCAN;
|
||||
else
|
||||
tx_mode = SINGLE;
|
||||
tx_view.set_transmitting(true);
|
||||
start_tx();
|
||||
}
|
||||
};
|
||||
|
||||
tx_view.on_stop = [this]() {
|
||||
tx_view.set_transmitting(false);
|
||||
tx_mode = IDLE;
|
||||
};
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
@@ -1,102 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2016 Furrtek
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_transmitter.hpp"
|
||||
#include "ui_font_fixed_8x16.hpp"
|
||||
|
||||
#include "message.hpp"
|
||||
#include "transmitter_model.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "portapack.hpp"
|
||||
|
||||
namespace ui {
|
||||
|
||||
class CoasterPagerView : public View {
|
||||
public:
|
||||
CoasterPagerView(NavigationView& nav);
|
||||
~CoasterPagerView();
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "BurgerPgrTX"; };
|
||||
|
||||
private:
|
||||
enum tx_modes {
|
||||
IDLE = 0,
|
||||
SINGLE,
|
||||
SCAN
|
||||
};
|
||||
|
||||
tx_modes tx_mode = IDLE;
|
||||
|
||||
// app save settings
|
||||
std::app_settings settings { };
|
||||
std::app_settings::AppSettings app_settings { };
|
||||
|
||||
void start_tx();
|
||||
void generate_frame();
|
||||
void on_tx_progress(const uint32_t progress, const bool done);
|
||||
|
||||
Labels labels {
|
||||
{ { 1 * 8, 3 * 8 }, "Syscall pager TX beta", Color::light_grey() },
|
||||
{ { 1 * 8, 8 * 8 }, "Data:", Color::light_grey() }
|
||||
};
|
||||
|
||||
SymField sym_data {
|
||||
{ 7 * 8, 8 * 8 },
|
||||
16, // 14 ? 12 ?
|
||||
SymField::SYMFIELD_HEX
|
||||
};
|
||||
|
||||
Checkbox checkbox_scan {
|
||||
{ 10 * 8, 14 * 8 },
|
||||
4,
|
||||
"Scan"
|
||||
};
|
||||
|
||||
/*ProgressBar progressbar {
|
||||
{ 5 * 8, 12 * 16, 20 * 8, 16 },
|
||||
};*/
|
||||
Text text_message {
|
||||
{ 5 * 8, 13 * 16, 20 * 8, 16 },
|
||||
""
|
||||
};
|
||||
|
||||
TransmitterView tx_view {
|
||||
16 * 16,
|
||||
10000,
|
||||
12
|
||||
};
|
||||
|
||||
MessageHandlerRegistration message_handler_tx_progress {
|
||||
Message::ID::TXProgress,
|
||||
[this](const Message* const p) {
|
||||
const auto message = *reinterpret_cast<const TXProgressMessage*>(p);
|
||||
this->on_tx_progress(message.progress, message.done);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
@@ -1,5 +1,7 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2024 Mark Thompson
|
||||
* Copyright (C) 2024 u-foka
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
@@ -20,15 +22,21 @@
|
||||
*/
|
||||
|
||||
#include "ui_debug.hpp"
|
||||
#include "debug.hpp"
|
||||
|
||||
#include "ch.h"
|
||||
|
||||
#include "radio.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "crc.hpp"
|
||||
|
||||
#include "audio.hpp"
|
||||
|
||||
#include "ui_sd_card_debug.hpp"
|
||||
#include "ui_font_fixed_8x16.hpp"
|
||||
#include "ui_painter.hpp"
|
||||
#include "ui_external_items_menu_loader.hpp"
|
||||
#include "ui_debug_battery.hpp"
|
||||
|
||||
#include "portapack.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
@@ -41,357 +49,625 @@ namespace ui {
|
||||
/* DebugMemoryView *******************************************************/
|
||||
|
||||
DebugMemoryView::DebugMemoryView(NavigationView& nav) {
|
||||
add_children({
|
||||
&text_title,
|
||||
&text_label_m0_core_free,
|
||||
&text_label_m0_core_free_value,
|
||||
&text_label_m0_heap_fragmented_free,
|
||||
&text_label_m0_heap_fragmented_free_value,
|
||||
&text_label_m0_heap_fragments,
|
||||
&text_label_m0_heap_fragments_value,
|
||||
&button_done
|
||||
});
|
||||
add_children({&text_title,
|
||||
&text_label_m0_core_free,
|
||||
&text_label_m0_core_free_value,
|
||||
&text_label_m0_heap_fragmented_free,
|
||||
&text_label_m0_heap_fragmented_free_value,
|
||||
&text_label_m0_heap_fragments,
|
||||
&text_label_m0_heap_fragments_value,
|
||||
&button_done});
|
||||
|
||||
const auto m0_core_free = chCoreStatus();
|
||||
text_label_m0_core_free_value.set(to_string_dec_uint(m0_core_free, 5));
|
||||
const auto m0_core_free = chCoreStatus();
|
||||
text_label_m0_core_free_value.set(to_string_dec_uint(m0_core_free, 5));
|
||||
|
||||
size_t m0_fragmented_free_space = 0;
|
||||
const auto m0_fragments = chHeapStatus(NULL, &m0_fragmented_free_space);
|
||||
text_label_m0_heap_fragmented_free_value.set(to_string_dec_uint(m0_fragmented_free_space, 5));
|
||||
text_label_m0_heap_fragments_value.set(to_string_dec_uint(m0_fragments, 5));
|
||||
size_t m0_fragmented_free_space = 0;
|
||||
const auto m0_fragments = chHeapStatus(NULL, &m0_fragmented_free_space);
|
||||
text_label_m0_heap_fragmented_free_value.set(to_string_dec_uint(m0_fragmented_free_space, 5));
|
||||
text_label_m0_heap_fragments_value.set(to_string_dec_uint(m0_fragments, 5));
|
||||
|
||||
button_done.on_select = [&nav](Button&){ nav.pop(); };
|
||||
button_done.on_select = [&nav](Button&) { nav.pop(); };
|
||||
}
|
||||
|
||||
void DebugMemoryView::focus() {
|
||||
button_done.focus();
|
||||
button_done.focus();
|
||||
}
|
||||
|
||||
/* TemperatureWidget *****************************************************/
|
||||
|
||||
void TemperatureWidget::paint(Painter& painter) {
|
||||
const auto logger = portapack::temperature_logger;
|
||||
const auto logger = portapack::temperature_logger;
|
||||
|
||||
const auto rect = screen_rect();
|
||||
const Color color_background { 0, 0, 64 };
|
||||
const Color color_foreground = Color::green();
|
||||
const Color color_reticle { 128, 128, 128 };
|
||||
const auto rect = screen_rect();
|
||||
const Color color_background{0, 0, 64};
|
||||
const Color color_foreground = Theme::getInstance()->fg_green->foreground;
|
||||
const Color color_reticle{128, 128, 128};
|
||||
|
||||
const auto graph_width = static_cast<int>(logger.capacity()) * bar_width;
|
||||
const Rect graph_rect {
|
||||
rect.left() + (rect.width() - graph_width) / 2, rect.top() + 8,
|
||||
graph_width, rect.height()
|
||||
};
|
||||
const Rect frame_rect {
|
||||
graph_rect.left() - 1, graph_rect.top() - 1,
|
||||
graph_rect.width() + 2, graph_rect.height() + 2
|
||||
};
|
||||
painter.draw_rectangle(frame_rect, color_reticle);
|
||||
painter.fill_rectangle(graph_rect, color_background);
|
||||
const auto graph_width = static_cast<int>(logger.capacity()) * bar_width;
|
||||
const Rect graph_rect{
|
||||
rect.left() + (rect.width() - graph_width) / 2, rect.top() + 8,
|
||||
graph_width, rect.height()};
|
||||
const Rect frame_rect{
|
||||
graph_rect.left() - 1, graph_rect.top() - 1,
|
||||
graph_rect.width() + 2, graph_rect.height() + 2};
|
||||
painter.draw_rectangle(frame_rect, color_reticle);
|
||||
painter.fill_rectangle(graph_rect, color_background);
|
||||
|
||||
const auto history = logger.history();
|
||||
for(size_t i=0; i<history.size(); i++) {
|
||||
const Coord x = graph_rect.right() - (history.size() - i) * bar_width;
|
||||
const auto sample = history[i];
|
||||
const auto temp = temperature(sample);
|
||||
const auto y = screen_y(temp, graph_rect);
|
||||
const Dim bar_height = graph_rect.bottom() - y;
|
||||
painter.fill_rectangle({ x, y, bar_width, bar_height }, color_foreground);
|
||||
}
|
||||
const auto history = logger.history();
|
||||
for (size_t i = 0; i < history.size(); i++) {
|
||||
const Coord x = graph_rect.right() - (history.size() - i) * bar_width;
|
||||
const auto sample = history[i];
|
||||
const auto temp = temperature(sample);
|
||||
const auto y = screen_y(temp, graph_rect);
|
||||
const Dim bar_height = graph_rect.bottom() - y;
|
||||
painter.fill_rectangle({x, y, bar_width, bar_height}, color_foreground);
|
||||
}
|
||||
|
||||
if( !history.empty() ) {
|
||||
const auto sample = history.back();
|
||||
const auto temp = temperature(sample);
|
||||
const auto last_y = screen_y(temp, graph_rect);
|
||||
const Coord x = graph_rect.right() + 8;
|
||||
const Coord y = last_y - 8;
|
||||
if (!history.empty()) {
|
||||
const auto sample = history.back();
|
||||
const auto temp = temperature(sample);
|
||||
const auto last_y = screen_y(temp, graph_rect);
|
||||
const Coord x = graph_rect.right() + 8;
|
||||
const Coord y = last_y - 8;
|
||||
|
||||
painter.draw_string({ x, y }, style(), temperature_str(temp));
|
||||
}
|
||||
painter.draw_string({x, y}, style(), temperature_str(temp));
|
||||
}
|
||||
|
||||
const auto display_temp_max = display_temp_min + (graph_rect.height() / display_temp_scale);
|
||||
for(auto temp=display_temp_min; temp<=display_temp_max; temp+=10) {
|
||||
const int32_t tick_length = 6;
|
||||
const auto tick_x = graph_rect.left() - tick_length;
|
||||
const auto tick_y = screen_y(temp, graph_rect);
|
||||
painter.fill_rectangle({ tick_x, tick_y, tick_length, 1 }, color_reticle);
|
||||
const auto text_x = graph_rect.left() - temp_len * 8 - 8;
|
||||
const auto text_y = tick_y - 8;
|
||||
painter.draw_string({ text_x, text_y }, style(), temperature_str(temp));
|
||||
}
|
||||
const auto display_temp_max = display_temp_min + (graph_rect.height() / display_temp_scale);
|
||||
for (auto temp = display_temp_min; temp <= display_temp_max; temp += 10) {
|
||||
const int32_t tick_length = 6;
|
||||
const auto tick_x = graph_rect.left() - tick_length;
|
||||
const auto tick_y = screen_y(temp, graph_rect);
|
||||
painter.fill_rectangle({tick_x, tick_y, tick_length, 1}, color_reticle);
|
||||
const auto text_x = graph_rect.left() - temp_len * 8 - 8;
|
||||
const auto text_y = tick_y - 8;
|
||||
painter.draw_string({text_x, text_y}, style(), temperature_str(temp));
|
||||
}
|
||||
}
|
||||
|
||||
TemperatureWidget::temperature_t TemperatureWidget::temperature(const sample_t sensor_value) const {
|
||||
/*It seems to be a temperature difference of 25C*/
|
||||
return -40 +(sensor_value * 4.31)+25; //max2837 datasheet temp 25ºC has sensor value: 15
|
||||
// Scaling is different for MAX2837 vs MAX2839 so it's now done in the respective chip-specific module
|
||||
return sensor_value;
|
||||
}
|
||||
|
||||
std::string TemperatureWidget::temperature_str(const temperature_t temperature) const {
|
||||
return to_string_dec_int(temperature, temp_len - 1) + "C";
|
||||
return to_string_dec_int(temperature, temp_len - 2) + STR_DEGREES_C;
|
||||
}
|
||||
|
||||
Coord TemperatureWidget::screen_y(
|
||||
const temperature_t temperature,
|
||||
const Rect& rect
|
||||
) const {
|
||||
int y_raw = rect.bottom() - ((temperature - display_temp_min) * display_temp_scale);
|
||||
const auto y_limit = std::min(rect.bottom(), std::max(rect.top(), y_raw));
|
||||
return y_limit;
|
||||
const temperature_t temperature,
|
||||
const Rect& rect) const {
|
||||
int y_raw = rect.bottom() - ((temperature - display_temp_min) * display_temp_scale);
|
||||
const auto y_limit = std::min(rect.bottom(), std::max(rect.top(), y_raw));
|
||||
return y_limit;
|
||||
}
|
||||
|
||||
/* TemperatureView *******************************************************/
|
||||
|
||||
TemperatureView::TemperatureView(NavigationView& nav) {
|
||||
add_children({
|
||||
&text_title,
|
||||
&temperature_widget,
|
||||
&button_done,
|
||||
});
|
||||
add_children({
|
||||
&text_title,
|
||||
&temperature_widget,
|
||||
&button_done,
|
||||
});
|
||||
|
||||
button_done.on_select = [&nav](Button&){ nav.pop(); };
|
||||
button_done.on_select = [&nav](Button&) { nav.pop(); };
|
||||
}
|
||||
|
||||
void TemperatureView::focus() {
|
||||
button_done.focus();
|
||||
button_done.focus();
|
||||
}
|
||||
|
||||
/* RegistersWidget *******************************************************/
|
||||
|
||||
RegistersWidget::RegistersWidget(
|
||||
RegistersWidgetConfig&& config,
|
||||
std::function<uint32_t(const size_t register_number)>&& reader
|
||||
) : Widget { },
|
||||
config(std::move(config)),
|
||||
reader(std::move(reader))
|
||||
{
|
||||
RegistersWidgetConfig&& config)
|
||||
: Widget{}, config(std::move(config)), page_number(0) {
|
||||
}
|
||||
|
||||
void RegistersWidget::update() {
|
||||
set_dirty();
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
void RegistersWidget::paint(Painter& painter) {
|
||||
const Coord left = (size().width() - config.row_width()) / 2;
|
||||
const Coord left = (size().width() - config.row_width()) / 2;
|
||||
|
||||
draw_legend(left, painter);
|
||||
draw_values(left, painter);
|
||||
draw_legend(left, painter);
|
||||
draw_values(left, painter);
|
||||
}
|
||||
|
||||
void RegistersWidget::draw_legend(const Coord left, Painter& painter) {
|
||||
const auto pos = screen_pos();
|
||||
const auto pos = screen_pos();
|
||||
const std::string spaces(config.legend_length(), ' ');
|
||||
|
||||
for(size_t i=0; i<config.registers_count; i+=config.registers_per_row()) {
|
||||
const Point offset {
|
||||
left, static_cast<int>((i / config.registers_per_row()) * row_height)
|
||||
};
|
||||
for (uint32_t i = 0; i < config.registers_per_page; i += config.registers_per_row()) {
|
||||
uint32_t r = page_number * config.registers_per_page + i;
|
||||
|
||||
const auto text = to_string_hex(i, config.legend_length());
|
||||
painter.draw_string(
|
||||
pos + offset,
|
||||
style().invert(),
|
||||
text
|
||||
);
|
||||
}
|
||||
const Point offset{
|
||||
left, static_cast<int>((i / config.registers_per_row()) * row_height)};
|
||||
|
||||
const auto text = (r >= config.registers_count) ? spaces : to_string_hex(r, config.legend_length());
|
||||
painter.draw_string(
|
||||
pos + offset,
|
||||
style().invert(),
|
||||
text);
|
||||
}
|
||||
}
|
||||
|
||||
void RegistersWidget::draw_values(
|
||||
const Coord left,
|
||||
Painter& painter
|
||||
) {
|
||||
const auto pos = screen_pos();
|
||||
const Coord left,
|
||||
Painter& painter) {
|
||||
const auto pos = screen_pos();
|
||||
const std::string spaces(config.value_length(), ' ');
|
||||
|
||||
for(size_t i=0; i<config.registers_count; i++) {
|
||||
const Point offset = {
|
||||
static_cast<int>(left + config.legend_width() + 8 + (i % config.registers_per_row()) * (config.value_width() + 8)),
|
||||
static_cast<int>((i / config.registers_per_row()) * row_height)
|
||||
};
|
||||
for (uint32_t i = 0; i < config.registers_per_page; i++) {
|
||||
uint32_t r = page_number * config.registers_per_page + i;
|
||||
|
||||
const auto value = reader(i);
|
||||
const Point offset = {
|
||||
static_cast<int>(left + config.legend_width() + 8 + (i % config.registers_per_row()) * (config.value_width() + 8)),
|
||||
static_cast<int>((i / config.registers_per_row()) * row_height)};
|
||||
|
||||
const auto text = to_string_hex(value, config.value_length());
|
||||
painter.draw_string(
|
||||
pos + offset,
|
||||
style(),
|
||||
text
|
||||
);
|
||||
}
|
||||
const auto text = (r >= config.registers_count) ? spaces : to_string_hex(reg_read(r), config.value_length());
|
||||
painter.draw_string(
|
||||
pos + offset,
|
||||
style(),
|
||||
text);
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t RegistersWidget::reg_read(const uint32_t register_number) {
|
||||
if (register_number < config.registers_count) {
|
||||
switch (config.chip_type) {
|
||||
case CT_PMEM:
|
||||
return portapack::persistent_memory::pmem_data_word(register_number / 4) >> (register_number % 4 * 8);
|
||||
case CT_RFFC5072:
|
||||
return radio::debug::first_if::register_read(register_number);
|
||||
case CT_MAX283X:
|
||||
return radio::debug::second_if::register_read(register_number);
|
||||
case CT_SI5351:
|
||||
return portapack::clock_generator.read_register(register_number);
|
||||
case CT_BATTERY:
|
||||
return battery::BatteryManagement::read_register(register_number);
|
||||
case CT_AUDIO:
|
||||
return audio::debug::reg_read(register_number);
|
||||
}
|
||||
}
|
||||
return 0xFFFF;
|
||||
}
|
||||
|
||||
void RegistersWidget::reg_write(const uint32_t register_number, const uint32_t value) {
|
||||
if (register_number < config.registers_count) {
|
||||
switch (config.chip_type) {
|
||||
case CT_PMEM:
|
||||
break;
|
||||
case CT_RFFC5072:
|
||||
radio::debug::first_if::register_write(register_number, value);
|
||||
break;
|
||||
case CT_MAX283X:
|
||||
radio::debug::second_if::register_write(register_number, value);
|
||||
break;
|
||||
case CT_SI5351:
|
||||
portapack::clock_generator.write_register(register_number, value);
|
||||
break;
|
||||
case CT_BATTERY:
|
||||
battery::BatteryManagement::write_register(register_number, value);
|
||||
break;
|
||||
case CT_AUDIO:
|
||||
audio::debug::reg_write(register_number, value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* RegistersView *********************************************************/
|
||||
|
||||
RegistersView::RegistersView(
|
||||
NavigationView& nav,
|
||||
const std::string& title,
|
||||
RegistersWidgetConfig&& config,
|
||||
std::function<uint32_t(const size_t register_number)>&& reader
|
||||
) : registers_widget { std::move(config), std::move(reader) }
|
||||
{
|
||||
add_children({
|
||||
&text_title,
|
||||
®isters_widget,
|
||||
&button_update,
|
||||
&button_done,
|
||||
});
|
||||
NavigationView& nav,
|
||||
const std::string& title,
|
||||
RegistersWidgetConfig&& config)
|
||||
: registers_widget{std::move(config)} {
|
||||
add_children({
|
||||
&text_title,
|
||||
®isters_widget,
|
||||
&button_update,
|
||||
&button_done,
|
||||
&labels,
|
||||
&field_write_reg_num,
|
||||
&field_write_data_val,
|
||||
&button_write,
|
||||
});
|
||||
|
||||
button_update.on_select = [this](Button&){
|
||||
this->registers_widget.update();
|
||||
};
|
||||
button_done.on_select = [&nav](Button&){ nav.pop(); };
|
||||
button_update.on_select = [this](Button&) {
|
||||
this->registers_widget.update();
|
||||
};
|
||||
button_done.on_select = [&nav](Button&) { nav.pop(); };
|
||||
|
||||
registers_widget.set_parent_rect({ 0, 48, 240, 192 });
|
||||
registers_widget.set_parent_rect({0, 48, 240, 192});
|
||||
registers_widget.set_page(0);
|
||||
|
||||
text_title.set_parent_rect({
|
||||
(240 - static_cast<int>(title.size()) * 8) / 2, 16,
|
||||
static_cast<int>(title.size()) * 8, 16
|
||||
});
|
||||
text_title.set(title);
|
||||
text_title.set_parent_rect({(240 - static_cast<int>(title.size()) * 8) / 2, 16,
|
||||
static_cast<int>(title.size()) * 8, 16});
|
||||
text_title.set(title);
|
||||
|
||||
field_write_reg_num.on_change = [this](SymField&) {
|
||||
field_write_data_val.set_value(this->registers_widget.reg_read(field_write_reg_num.to_integer()));
|
||||
field_write_data_val.set_dirty();
|
||||
};
|
||||
|
||||
const auto value = registers_widget.reg_read(0);
|
||||
field_write_data_val.set_value(value);
|
||||
|
||||
button_write.set_style(Theme::getInstance()->fg_red);
|
||||
button_write.on_select = [this](Button&) {
|
||||
this->registers_widget.reg_write(field_write_reg_num.to_integer(), field_write_data_val.to_integer());
|
||||
this->registers_widget.update();
|
||||
};
|
||||
}
|
||||
|
||||
void RegistersView::focus() {
|
||||
button_done.focus();
|
||||
button_done.focus();
|
||||
}
|
||||
|
||||
bool RegistersView::on_encoder(const EncoderEvent delta) {
|
||||
registers_widget.set_page(std::max(0ul, std::min(registers_widget.page_count() - 1, registers_widget.page() + delta)));
|
||||
registers_widget.update();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ControlsSwitchesWidget ************************************************/
|
||||
|
||||
void ControlsSwitchesWidget::on_show() {
|
||||
display.fill_rectangle(
|
||||
screen_rect(),
|
||||
Color::black()
|
||||
);
|
||||
display.fill_rectangle(
|
||||
screen_rect(),
|
||||
Theme::getInstance()->bg_darkest->background);
|
||||
}
|
||||
|
||||
bool ControlsSwitchesWidget::on_key(const KeyEvent key) {
|
||||
key_event_mask = 1 << toUType(key);
|
||||
return true;
|
||||
key_event_mask = 1 << toUType(key);
|
||||
long_press_key_event_mask = key_is_long_pressed(key) ? key_event_mask : 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ControlsSwitchesWidget::on_encoder(const EncoderEvent delta) {
|
||||
last_delta = delta;
|
||||
return true;
|
||||
}
|
||||
|
||||
void ControlsSwitchesWidget::paint(Painter& painter) {
|
||||
const std::array<Rect, 7> button_rects { {
|
||||
{ 64, 32, 16, 16 }, // Right
|
||||
{ 0, 32, 16, 16 }, // Left
|
||||
{ 32, 64, 16, 16 }, // Down
|
||||
{ 32, 0, 16, 16 }, // Up
|
||||
{ 32, 32, 16, 16 }, // Select
|
||||
{ 16, 96, 16, 16 }, // Encoder phase 0
|
||||
{ 48, 96, 16, 16 }, // Encoder phase 1
|
||||
} };
|
||||
const auto pos = screen_pos();
|
||||
auto switches_raw = control::debug::switches();
|
||||
auto switches_debounced = get_switches_state().to_ulong();
|
||||
auto switches_event = key_event_mask;
|
||||
const auto pos = screen_pos();
|
||||
|
||||
for(const auto r : button_rects) {
|
||||
const auto c =
|
||||
((switches_event & 1) ?
|
||||
Color::red() :
|
||||
((switches_debounced & 1) ?
|
||||
Color::green() :
|
||||
((switches_raw & 1) ?
|
||||
Color::yellow() :
|
||||
Color::blue()
|
||||
)
|
||||
)
|
||||
);
|
||||
painter.fill_rectangle(r + pos, c);
|
||||
switches_raw >>= 1;
|
||||
switches_debounced >>= 1;
|
||||
switches_event >>= 1;
|
||||
}
|
||||
const std::array<Rect, 9> button_rects{{
|
||||
{64, 32, 16, 16}, // Right
|
||||
{0, 32, 16, 16}, // Left
|
||||
{32, 64, 16, 16}, // Down
|
||||
{32, 0, 16, 16}, // Up
|
||||
{32, 32, 16, 16}, // Select
|
||||
{96, 0, 16, 16}, // Dfu
|
||||
{16, 96, 16, 16}, // Encoder phase 0
|
||||
{48, 96, 16, 16}, // Encoder phase 1
|
||||
{96, 64, 16, 16}, // Touch
|
||||
}};
|
||||
|
||||
for (const auto r : button_rects) {
|
||||
painter.fill_rectangle(r + pos, Theme::getInstance()->fg_blue->foreground);
|
||||
}
|
||||
|
||||
if (get_touch_frame().touch)
|
||||
painter.fill_rectangle(button_rects[8] + pos, Theme::getInstance()->fg_yellow->foreground);
|
||||
|
||||
const std::array<Rect, 8> raw_rects{{
|
||||
{64 + 1, 32 + 1, 16 - 2, 16 - 2}, // Right
|
||||
{0 + 1, 32 + 1, 16 - 2, 16 - 2}, // Left
|
||||
{32 + 1, 64 + 1, 16 - 2, 16 - 2}, // Down
|
||||
{32 + 1, 0 + 1, 16 - 2, 16 - 2}, // Up
|
||||
{32 + 1, 32 + 1, 16 - 2, 16 - 2}, // Select
|
||||
{96 + 1, 0 + 1, 16 - 2, 16 - 2}, // Dfu
|
||||
{16 + 1, 96 + 1, 16 - 2, 16 - 2}, // Encoder phase 0
|
||||
{48 + 1, 96 + 1, 16 - 2, 16 - 2}, // Encoder phase 1
|
||||
}};
|
||||
|
||||
auto switches_raw = control::debug::switches();
|
||||
for (const auto r : raw_rects) {
|
||||
if (switches_raw & 1)
|
||||
painter.fill_rectangle(r + pos, Theme::getInstance()->fg_yellow->foreground);
|
||||
|
||||
switches_raw >>= 1;
|
||||
}
|
||||
|
||||
const std::array<Rect, 6> debounced_rects{{
|
||||
{64 + 2, 32 + 2, 16 - 4, 16 - 4}, // Right
|
||||
{0 + 2, 32 + 2, 16 - 4, 16 - 4}, // Left
|
||||
{32 + 2, 64 + 2, 16 - 4, 16 - 4}, // Down
|
||||
{32 + 2, 0 + 2, 16 - 4, 16 - 4}, // Up
|
||||
{32 + 2, 32 + 2, 16 - 4, 16 - 4}, // Select
|
||||
{96 + 2, 0 + 2, 16 - 4, 16 - 4}, // Dfu
|
||||
}};
|
||||
|
||||
auto switches_debounced = get_switches_state().to_ulong();
|
||||
for (const auto r : debounced_rects) {
|
||||
if (switches_debounced & 1)
|
||||
painter.fill_rectangle(r + pos, Theme::getInstance()->fg_green->foreground);
|
||||
|
||||
switches_debounced >>= 1;
|
||||
}
|
||||
|
||||
const std::array<Rect, 6> events_rects{{
|
||||
{64 + 3, 32 + 3, 16 - 6, 16 - 6}, // Right
|
||||
{0 + 3, 32 + 3, 16 - 6, 16 - 6}, // Left
|
||||
{32 + 3, 64 + 3, 16 - 6, 16 - 6}, // Down
|
||||
{32 + 3, 0 + 3, 16 - 6, 16 - 6}, // Up
|
||||
{32 + 3, 32 + 3, 16 - 6, 16 - 6}, // Select
|
||||
{96 + 3, 0 + 3, 16 - 6, 16 - 6}, // Dfu
|
||||
}};
|
||||
|
||||
auto switches_event = key_event_mask;
|
||||
for (const auto r : events_rects) {
|
||||
if (switches_event & 1)
|
||||
painter.fill_rectangle(r + pos, Theme::getInstance()->fg_red->foreground);
|
||||
|
||||
switches_event >>= 1;
|
||||
}
|
||||
|
||||
switches_event = long_press_key_event_mask;
|
||||
for (const auto r : events_rects) {
|
||||
if (switches_event & 1)
|
||||
painter.fill_rectangle(r + pos, Theme::getInstance()->fg_cyan->foreground);
|
||||
|
||||
switches_event >>= 1;
|
||||
}
|
||||
|
||||
painter.draw_string({5 * 8, 12 * 16}, *Theme::getInstance()->fg_light, to_string_dec_int(last_delta, 3));
|
||||
}
|
||||
|
||||
void ControlsSwitchesWidget::on_frame_sync() {
|
||||
set_dirty();
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
/* DebugControlsView *****************************************************/
|
||||
|
||||
DebugControlsView::DebugControlsView(NavigationView& nav) {
|
||||
add_children({
|
||||
&text_title,
|
||||
&switches_widget,
|
||||
&button_done,
|
||||
});
|
||||
add_children({
|
||||
&labels,
|
||||
&switches_widget,
|
||||
&options_switches_mode,
|
||||
&button_done,
|
||||
});
|
||||
|
||||
button_done.on_select = [&nav](Button&){ nav.pop(); };
|
||||
button_done.on_select = [&nav](Button&) {
|
||||
set_switches_long_press_config(0);
|
||||
nav.pop();
|
||||
};
|
||||
|
||||
options_switches_mode.on_change = [this](size_t, OptionsField::value_t v) {
|
||||
(void)v;
|
||||
set_switches_long_press_config(options_switches_mode.selected_index_value());
|
||||
};
|
||||
}
|
||||
|
||||
void DebugControlsView::focus() {
|
||||
switches_widget.focus();
|
||||
switches_widget.focus();
|
||||
}
|
||||
|
||||
/* DebugPeripheralsMenuView **********************************************/
|
||||
|
||||
DebugPeripheralsMenuView::DebugPeripheralsMenuView(NavigationView& nav) {
|
||||
const char * max283x = hackrf_r9 ? "MAX2839" : "MAX2837";
|
||||
const char * si5351x = hackrf_r9 ? "Si5351A" : "Si5351C";
|
||||
add_items({
|
||||
{ "RFFC5072", ui::Color::dark_cyan(), &bitmap_icon_peripherals_details, [&nav](){ nav.push<RegistersView>(
|
||||
"RFFC5072", RegistersWidgetConfig { 31, 16 },
|
||||
[](const size_t register_number) { return radio::debug::first_if::register_read(register_number); }
|
||||
); } },
|
||||
{ max283x, ui::Color::dark_cyan(), &bitmap_icon_peripherals_details, [&nav, max283x](){ nav.push<RegistersView>(
|
||||
max283x, RegistersWidgetConfig { 32, 10 },
|
||||
[](const size_t register_number) { return radio::debug::second_if::register_read(register_number); }
|
||||
); } },
|
||||
{ si5351x, ui::Color::dark_cyan(), &bitmap_icon_peripherals_details, [&nav, si5351x](){ nav.push<RegistersView>(
|
||||
si5351x, RegistersWidgetConfig { 96, 8 },
|
||||
[](const size_t register_number) { return portapack::clock_generator.read_register(register_number); }
|
||||
); } },
|
||||
{ audio::debug::codec_name(), ui::Color::dark_cyan(), &bitmap_icon_peripherals_details, [&nav](){ nav.push<RegistersView>(
|
||||
audio::debug::codec_name(), RegistersWidgetConfig { audio::debug::reg_count(), audio::debug::reg_bits() },
|
||||
[](const size_t register_number) { return audio::debug::reg_read(register_number); }
|
||||
); } },
|
||||
});
|
||||
set_max_rows(2); // allow wider buttons
|
||||
DebugPeripheralsMenuView::DebugPeripheralsMenuView(NavigationView& nav)
|
||||
: nav_(nav) {
|
||||
set_max_rows(2); // allow wider buttons
|
||||
}
|
||||
|
||||
void DebugPeripheralsMenuView::on_populate() {
|
||||
const char* max283x = hackrf_r9 ? "MAX2839" : "MAX2837";
|
||||
const char* si5351x = hackrf_r9 ? "Si5351A" : "Si5351C";
|
||||
add_items({
|
||||
{"RFFC5072", Theme::getInstance()->fg_darkcyan->foreground, &bitmap_icon_peripherals_details, [this]() { nav_.push<RegistersView>("RFFC5072", RegistersWidgetConfig{CT_RFFC5072, 31, 31, 16}); }},
|
||||
{max283x, Theme::getInstance()->fg_darkcyan->foreground, &bitmap_icon_peripherals_details, [this, max283x]() { nav_.push<RegistersView>(max283x, RegistersWidgetConfig{CT_MAX283X, 32, 32, 10}); }},
|
||||
{si5351x, Theme::getInstance()->fg_darkcyan->foreground, &bitmap_icon_peripherals_details, [this, si5351x]() { nav_.push<RegistersView>(si5351x, RegistersWidgetConfig{CT_SI5351, 188, 96, 8}); }},
|
||||
{audio::debug::codec_name(), Theme::getInstance()->fg_darkcyan->foreground, &bitmap_icon_peripherals_details, [this]() { nav_.push<RegistersView>(audio::debug::codec_name(), RegistersWidgetConfig{CT_AUDIO, audio::debug::reg_count(), audio::debug::reg_count(), audio::debug::reg_bits()}); }},
|
||||
});
|
||||
if (battery::BatteryManagement::detectedModule() == battery::BatteryManagement::BatteryModules::BATT_MAX17055) {
|
||||
add_item(
|
||||
{"MAX17055", Theme::getInstance()->fg_darkcyan->foreground, &bitmap_icon_peripherals_details, [this]() { nav_.push<RegistersView>("MAX17055", RegistersWidgetConfig{CT_BATTERY, 256, 16, 16}); }});
|
||||
}
|
||||
set_max_rows(2); // allow wider buttons
|
||||
}
|
||||
|
||||
/* DebugReboot **********************************************/
|
||||
|
||||
DebugReboot::DebugReboot(NavigationView& nav) {
|
||||
(void)nav;
|
||||
|
||||
LPC_RGU->RESET_CTRL[0] = (1 << 0);
|
||||
|
||||
while (1)
|
||||
__WFE();
|
||||
}
|
||||
|
||||
void DebugReboot::on_populate() {
|
||||
}
|
||||
|
||||
/* DebugMenuView *********************************************************/
|
||||
|
||||
DebugMenuView::DebugMenuView(NavigationView& nav) {
|
||||
if( portapack::persistent_memory::show_gui_return_icon() )
|
||||
{
|
||||
add_items( { { "..", ui::Color::light_grey(),&bitmap_icon_previous, [&nav](){ nav.pop(); } } } );
|
||||
}
|
||||
add_items({
|
||||
{ "Memory", ui::Color::dark_cyan(), &bitmap_icon_memory, [&nav](){ nav.push<DebugMemoryView>(); } },
|
||||
//{ "Radio State", ui::Color::white(), nullptr, [&nav](){ nav.push<NotImplementedView>(); } },
|
||||
{ "SD Card", ui::Color::dark_cyan(), &bitmap_icon_sdcard, [&nav](){ nav.push<SDCardDebugView>(); } },
|
||||
{ "Peripherals", ui::Color::dark_cyan(), &bitmap_icon_peripherals, [&nav](){ nav.push<DebugPeripheralsMenuView>(); } },
|
||||
{ "Temperature", ui::Color::dark_cyan(), &bitmap_icon_temperature, [&nav](){ nav.push<TemperatureView>(); } },
|
||||
{ "Buttons Test", ui::Color::dark_cyan(), &bitmap_icon_controls, [&nav](){ nav.push<DebugControlsView>(); } },
|
||||
});
|
||||
set_max_rows(2); // allow wider buttons
|
||||
DebugMenuView::DebugMenuView(NavigationView& nav)
|
||||
: nav_(nav) {
|
||||
set_max_rows(2); // allow wider buttons
|
||||
}
|
||||
|
||||
void DebugMenuView::on_populate() {
|
||||
if (portapack::persistent_memory::show_gui_return_icon()) {
|
||||
add_items({{"..", ui::Theme::getInstance()->fg_light->foreground, &bitmap_icon_previous, [this]() { nav_.pop(); }}});
|
||||
}
|
||||
add_items({
|
||||
{"Buttons Test", ui::Theme::getInstance()->fg_darkcyan->foreground, &bitmap_icon_controls, [this]() { nav_.push<DebugControlsView>(); }},
|
||||
{"Debug Dump", ui::Theme::getInstance()->fg_darkcyan->foreground, &bitmap_icon_memory, [this]() { portapack::persistent_memory::debug_dump(); }},
|
||||
{"M0 Stack Dump", ui::Theme::getInstance()->fg_darkcyan->foreground, &bitmap_icon_memory, [this]() { stack_dump(); }},
|
||||
{"Memory Dump", ui::Theme::getInstance()->fg_darkcyan->foreground, &bitmap_icon_memory, [this]() { nav_.push<DebugMemoryDumpView>(); }},
|
||||
//{"Memory Usage", ui::Theme::getInstance()->fg_darkcyan->foreground, &bitmap_icon_memory, [this]() { nav_.push<DebugMemoryView>(); }},
|
||||
{"Peripherals", ui::Theme::getInstance()->fg_darkcyan->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<DebugPeripheralsMenuView>(); }},
|
||||
{"Pers. Memory", ui::Theme::getInstance()->fg_darkcyan->foreground, &bitmap_icon_memory, [this]() { nav_.push<DebugPmemView>(); }},
|
||||
//{ "Radio State", ui::Theme::getInstance()->bg_darkest->foreground, nullptr, [this](){ nav_.push<NotImplementedView>(); } },
|
||||
{"SD Card", ui::Theme::getInstance()->fg_darkcyan->foreground, &bitmap_icon_sdcard, [this]() { nav_.push<SDCardDebugView>(); }},
|
||||
{"Temperature", ui::Theme::getInstance()->fg_darkcyan->foreground, &bitmap_icon_temperature, [this]() { nav_.push<TemperatureView>(); }},
|
||||
{"Touch Test", ui::Theme::getInstance()->fg_darkcyan->foreground, &bitmap_icon_notepad, [this]() { nav_.push<DebugScreenTest>(); }},
|
||||
{"Reboot", ui::Theme::getInstance()->fg_darkcyan->foreground, &bitmap_icon_setup, [this]() { nav_.push<DebugReboot>(); }},
|
||||
});
|
||||
|
||||
if (battery::BatteryManagement::detectedModule() == battery::BatteryManagement::BatteryModules::BATT_MAX17055) {
|
||||
add_item(
|
||||
{"Battery", ui::Theme::getInstance()->fg_darkcyan->foreground, &bitmap_icon_batt_icon, [this]() { nav_.push<BatteryCapacityView>(); }});
|
||||
}
|
||||
|
||||
for (auto const& gridItem : ExternalItemsMenuLoader::load_external_items(app_location_t::DEBUG, nav_)) {
|
||||
add_item(gridItem);
|
||||
};
|
||||
}
|
||||
|
||||
/* DebugMemoryDumpView *********************************************************/
|
||||
|
||||
DebugMemoryDumpView::DebugMemoryDumpView(NavigationView& nav) {
|
||||
add_children({
|
||||
&button_dump,
|
||||
&button_read,
|
||||
&button_write,
|
||||
&button_done,
|
||||
&labels,
|
||||
&field_starting_address,
|
||||
&field_byte_count,
|
||||
&field_rw_address,
|
||||
&field_data_value,
|
||||
});
|
||||
|
||||
button_done.on_select = [&nav](Button&) { nav.pop(); };
|
||||
|
||||
button_dump.on_select = [this](Button&) {
|
||||
if (field_byte_count.to_integer() != 0)
|
||||
memory_dump((uint32_t*)field_starting_address.to_integer(), ((uint32_t)field_byte_count.to_integer() + 3) / 4, false);
|
||||
};
|
||||
|
||||
button_read.on_select = [this](Button&) {
|
||||
field_data_value.set_value(*(uint32_t*)field_rw_address.to_integer());
|
||||
field_data_value.set_dirty();
|
||||
};
|
||||
|
||||
button_write.set_style(Theme::getInstance()->fg_red);
|
||||
button_write.on_select = [this](Button&) {
|
||||
*(uint32_t*)field_rw_address.to_integer() = (uint32_t)field_data_value.to_integer();
|
||||
};
|
||||
}
|
||||
|
||||
void DebugMemoryDumpView::focus() {
|
||||
button_done.focus();
|
||||
}
|
||||
|
||||
/* DebugPmemView *********************************************************/
|
||||
|
||||
DebugPmemView::DebugPmemView(NavigationView& nav)
|
||||
: registers_widget(RegistersWidgetConfig{CT_PMEM, PMEM_SIZE_BYTES, page_size, 8}) {
|
||||
add_children({®isters_widget, &text_checksum, &text_checksum2, &button_ok});
|
||||
|
||||
registers_widget.set_parent_rect({0, 32, 240, 192});
|
||||
|
||||
text_checksum.set("Size: " + to_string_dec_uint(portapack::persistent_memory::data_size(), 3) + " CRC: " + to_string_hex(portapack::persistent_memory::pmem_stored_checksum(), 8));
|
||||
text_checksum2.set("Calculated CRC: " + to_string_hex(portapack::persistent_memory::pmem_calculated_checksum(), 8));
|
||||
|
||||
button_ok.on_select = [&nav](Button&) {
|
||||
nav.pop();
|
||||
};
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
bool DebugPmemView::on_encoder(const EncoderEvent delta) {
|
||||
registers_widget.set_page(std::max(0ul, std::min((uint32_t)page_count - 1, registers_widget.page() + delta)));
|
||||
|
||||
update();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void DebugPmemView::focus() {
|
||||
button_ok.focus();
|
||||
}
|
||||
|
||||
void DebugPmemView::update() {
|
||||
registers_widget.update();
|
||||
}
|
||||
|
||||
/* DebugScreenTest ****************************************************/
|
||||
|
||||
DebugScreenTest::DebugScreenTest(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
set_focusable(true);
|
||||
std::srand(LPC_RTC->CTIME0);
|
||||
}
|
||||
|
||||
bool DebugScreenTest::on_key(const KeyEvent key) {
|
||||
Painter painter;
|
||||
switch (key) {
|
||||
case KeyEvent::Select:
|
||||
nav_.pop();
|
||||
break;
|
||||
case KeyEvent::Down:
|
||||
painter.fill_rectangle({0, 0, screen_width, screen_height}, std::rand());
|
||||
break;
|
||||
case KeyEvent::Left:
|
||||
pen_color = std::rand();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DebugScreenTest::on_encoder(EncoderEvent delta) {
|
||||
pen_size = clip<int32_t>(pen_size + delta, 1, screen_width);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DebugScreenTest::on_touch(const TouchEvent event) {
|
||||
Painter painter;
|
||||
pen_pos = event.point;
|
||||
painter.fill_rectangle({pen_pos.x() - pen_size / 2, pen_pos.y() - pen_size / 2, pen_size, pen_size}, pen_color);
|
||||
return true;
|
||||
}
|
||||
|
||||
void DebugScreenTest::paint(Painter& painter) {
|
||||
painter.fill_rectangle({0, 16, screen_width, screen_height - 16}, Theme::getInstance()->bg_darkest->foreground);
|
||||
painter.draw_string({10 * 8, screen_height / 2}, *Theme::getInstance()->bg_darkest, "Use Stylus");
|
||||
pen_color = std::rand();
|
||||
}
|
||||
|
||||
/* DebugLCRView *******************************************************/
|
||||
|
||||
/*DebugLCRView::DebugLCRView(NavigationView& nav, std::string lcr_string) {
|
||||
|
||||
std::string debug_text;
|
||||
|
||||
add_children({
|
||||
&console,
|
||||
&button_exit
|
||||
});
|
||||
|
||||
for(const auto c : lcr_string) {
|
||||
if ((c < 32) || (c > 126))
|
||||
debug_text += "[" + to_string_dec_uint(c) + "]";
|
||||
else
|
||||
debug_text += c;
|
||||
}
|
||||
|
||||
debug_text += "\n\n";
|
||||
debug_text += "Length: " + to_string_dec_uint(lcr_string.length()) + '\n';
|
||||
debug_text += "Checksum: " + to_string_dec_uint(lcr_string.back()) + '\n';
|
||||
|
||||
console.write(debug_text);
|
||||
|
||||
button_exit.on_select = [this, &nav](Button&){
|
||||
nav.pop();
|
||||
};
|
||||
std::string debug_text;
|
||||
|
||||
add_children({
|
||||
&console,
|
||||
&button_exit
|
||||
});
|
||||
|
||||
for(const auto c : lcr_string) {
|
||||
if ((c < 32) || (c > 126))
|
||||
debug_text += "[" + to_string_dec_uint(c) + "]";
|
||||
else
|
||||
debug_text += c;
|
||||
}
|
||||
|
||||
debug_text += "\n\n";
|
||||
debug_text += "Length: " + to_string_dec_uint(lcr_string.length()) + '\n';
|
||||
debug_text += "Checksum: " + to_string_dec_uint(lcr_string.back()) + '\n';
|
||||
|
||||
console.write(debug_text);
|
||||
|
||||
button_exit.on_select = [this, &nav](Button&){
|
||||
nav.pop();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
void DebugLCRView::focus() {
|
||||
button_exit.focus();
|
||||
button_exit.focus();
|
||||
}*/
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
|
||||
* Copyright (C) 2024 Mark Thompson
|
||||
* Copyright (C) 2024 u-foka
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
@@ -30,6 +32,8 @@
|
||||
|
||||
#include "rffc507x.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "memory_map.hpp"
|
||||
#include "irq_controls.hpp"
|
||||
|
||||
#include <functional>
|
||||
#include <utility>
|
||||
@@ -37,273 +41,413 @@
|
||||
namespace ui {
|
||||
|
||||
class DebugMemoryView : public View {
|
||||
public:
|
||||
DebugMemoryView(NavigationView& nav);
|
||||
public:
|
||||
DebugMemoryView(NavigationView& nav);
|
||||
|
||||
void focus() override;
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "Memory"; };
|
||||
std::string title() const override { return "Memory"; };
|
||||
|
||||
private:
|
||||
Text text_title {
|
||||
{ 96, 96, 48, 16 },
|
||||
"Memory",
|
||||
};
|
||||
private:
|
||||
Text text_title{
|
||||
{72, 96, 96, 16},
|
||||
"Memory Usage",
|
||||
};
|
||||
|
||||
Text text_label_m0_core_free {
|
||||
{ 0, 128, 144, 16 },
|
||||
"M0 Core Free Bytes",
|
||||
};
|
||||
Text text_label_m0_core_free{
|
||||
{0, 128, 144, 16},
|
||||
"M0 Core Free Bytes",
|
||||
};
|
||||
|
||||
Text text_label_m0_core_free_value {
|
||||
{ 200, 128, 40, 16 },
|
||||
};
|
||||
Text text_label_m0_core_free_value{
|
||||
{200, 128, 40, 16},
|
||||
};
|
||||
|
||||
Text text_label_m0_heap_fragmented_free {
|
||||
{ 0, 144, 184, 16 },
|
||||
"M0 Heap Fragmented Free",
|
||||
};
|
||||
Text text_label_m0_heap_fragmented_free{
|
||||
{0, 144, 184, 16},
|
||||
"M0 Heap Fragmented Free",
|
||||
};
|
||||
|
||||
Text text_label_m0_heap_fragmented_free_value {
|
||||
{ 200, 144, 40, 16 },
|
||||
};
|
||||
Text text_label_m0_heap_fragmented_free_value{
|
||||
{200, 144, 40, 16},
|
||||
};
|
||||
|
||||
Text text_label_m0_heap_fragments {
|
||||
{ 0, 160, 136, 16 },
|
||||
"M0 Heap Fragments",
|
||||
};
|
||||
Text text_label_m0_heap_fragments{
|
||||
{0, 160, 136, 16},
|
||||
"M0 Heap Fragments",
|
||||
};
|
||||
|
||||
Text text_label_m0_heap_fragments_value {
|
||||
{ 200, 160, 40, 16 },
|
||||
};
|
||||
Text text_label_m0_heap_fragments_value{
|
||||
{200, 160, 40, 16},
|
||||
};
|
||||
|
||||
Button button_done {
|
||||
{ 72, 192, 96, 24 },
|
||||
"Done"
|
||||
};
|
||||
Button button_done{
|
||||
{72, 192, 96, 24},
|
||||
"Done"};
|
||||
};
|
||||
|
||||
class TemperatureWidget : public Widget {
|
||||
public:
|
||||
explicit TemperatureWidget(
|
||||
Rect parent_rect
|
||||
) : Widget { parent_rect }
|
||||
{
|
||||
}
|
||||
public:
|
||||
explicit TemperatureWidget(
|
||||
Rect parent_rect)
|
||||
: Widget{parent_rect} {
|
||||
}
|
||||
|
||||
void paint(Painter& painter) override;
|
||||
void paint(Painter& painter) override;
|
||||
|
||||
private:
|
||||
using sample_t = uint32_t;
|
||||
using temperature_t = int32_t;
|
||||
private:
|
||||
using sample_t = uint32_t;
|
||||
using temperature_t = int32_t;
|
||||
|
||||
temperature_t temperature(const sample_t sensor_value) const;
|
||||
Coord screen_y(const temperature_t temperature, const Rect& screen_rect) const;
|
||||
temperature_t temperature(const sample_t sensor_value) const;
|
||||
Coord screen_y(const temperature_t temperature, const Rect& screen_rect) const;
|
||||
|
||||
std::string temperature_str(const temperature_t temperature) const;
|
||||
std::string temperature_str(const temperature_t temperature) const;
|
||||
|
||||
static constexpr temperature_t display_temp_min = -10; //Accomodate negative values, present in cold startup cases
|
||||
static constexpr temperature_t display_temp_scale = 3;
|
||||
static constexpr int bar_width = 1;
|
||||
static constexpr int temp_len = 4; //Now scale shows up to 4 chars ("-10C")
|
||||
static constexpr temperature_t display_temp_min = -10; // Accomodate negative values, present in cold startup cases
|
||||
static constexpr temperature_t display_temp_scale = 3;
|
||||
static constexpr int bar_width = 1;
|
||||
static constexpr int temp_len = 5; // Now scale shows up to 5 chars ("-10ºC")
|
||||
};
|
||||
|
||||
class TemperatureView : public View {
|
||||
public:
|
||||
explicit TemperatureView(NavigationView& nav);
|
||||
public:
|
||||
explicit TemperatureView(NavigationView& nav);
|
||||
|
||||
void focus() override;
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "Temperature"; };
|
||||
std::string title() const override { return "Temperature"; };
|
||||
|
||||
private:
|
||||
Text text_title {
|
||||
{ 76, 16, 240, 16 },
|
||||
"Temperature",
|
||||
};
|
||||
private:
|
||||
Text text_title{
|
||||
{76, 16, 240, 16},
|
||||
"Temperature",
|
||||
};
|
||||
|
||||
TemperatureWidget temperature_widget {
|
||||
{ 0, 40, 240, 180 },
|
||||
};
|
||||
TemperatureWidget temperature_widget{
|
||||
{0, 40, 240, 180},
|
||||
};
|
||||
|
||||
Button button_done {
|
||||
{ 72, 264, 96, 24 },
|
||||
"Done"
|
||||
};
|
||||
Button button_done{
|
||||
{72, 264, 96, 24},
|
||||
"Done"};
|
||||
};
|
||||
|
||||
typedef enum {
|
||||
CT_PMEM,
|
||||
CT_RFFC5072,
|
||||
CT_MAX283X,
|
||||
CT_SI5351,
|
||||
CT_AUDIO,
|
||||
CT_BATTERY,
|
||||
} chip_type_t;
|
||||
|
||||
struct RegistersWidgetConfig {
|
||||
size_t registers_count;
|
||||
size_t register_bits;
|
||||
chip_type_t chip_type;
|
||||
uint32_t registers_count;
|
||||
uint32_t registers_per_page;
|
||||
uint32_t register_bits;
|
||||
|
||||
constexpr size_t legend_length() const {
|
||||
return (registers_count >= 0x10) ? 2 : 1;
|
||||
}
|
||||
constexpr size_t legend_length() const {
|
||||
return (registers_count >= 0x10) ? 2 : 1;
|
||||
}
|
||||
|
||||
constexpr size_t legend_width() const {
|
||||
return legend_length() * 8;
|
||||
}
|
||||
constexpr size_t legend_width() const {
|
||||
return legend_length() * 8;
|
||||
}
|
||||
|
||||
constexpr size_t value_length() const {
|
||||
return (register_bits + 3) / 4;
|
||||
}
|
||||
constexpr size_t value_length() const {
|
||||
return (register_bits + 3) / 4;
|
||||
}
|
||||
|
||||
constexpr size_t value_width() const {
|
||||
return value_length() * 8;
|
||||
}
|
||||
constexpr size_t value_width() const {
|
||||
return value_length() * 8;
|
||||
}
|
||||
|
||||
constexpr size_t registers_per_row() const {
|
||||
return (value_length() >= 3) ? 4 : 8;
|
||||
}
|
||||
constexpr size_t registers_per_row() const {
|
||||
return (value_length() >= 3) ? 4 : 8;
|
||||
}
|
||||
|
||||
constexpr size_t registers_row_length() const {
|
||||
return (registers_per_row() * (value_length() + 1)) - 1;
|
||||
}
|
||||
constexpr size_t registers_row_length() const {
|
||||
return (registers_per_row() * (value_length() + 1)) - 1;
|
||||
}
|
||||
|
||||
constexpr size_t registers_row_width() const {
|
||||
return registers_row_length() * 8;
|
||||
}
|
||||
constexpr size_t registers_row_width() const {
|
||||
return registers_row_length() * 8;
|
||||
}
|
||||
|
||||
constexpr size_t row_width() const {
|
||||
return legend_width() + 8 + registers_row_width();
|
||||
}
|
||||
constexpr size_t row_width() const {
|
||||
return legend_width() + 8 + registers_row_width();
|
||||
}
|
||||
|
||||
constexpr size_t rows() const {
|
||||
return registers_count / registers_per_row();
|
||||
}
|
||||
constexpr size_t rows() const {
|
||||
return registers_count / registers_per_row();
|
||||
}
|
||||
};
|
||||
|
||||
class RegistersWidget : public Widget {
|
||||
public:
|
||||
RegistersWidget(
|
||||
RegistersWidgetConfig&& config,
|
||||
std::function<uint32_t(const size_t register_number)>&& reader
|
||||
);
|
||||
public:
|
||||
RegistersWidget(RegistersWidgetConfig&& config);
|
||||
|
||||
void update();
|
||||
void update();
|
||||
|
||||
void paint(Painter& painter) override;
|
||||
void paint(Painter& painter) override;
|
||||
|
||||
private:
|
||||
const RegistersWidgetConfig config;
|
||||
const std::function<uint32_t(const size_t register_number)> reader;
|
||||
uint32_t reg_read(const uint32_t register_number);
|
||||
void reg_write(const uint32_t register_number, const uint32_t value);
|
||||
|
||||
static constexpr size_t row_height = 16;
|
||||
void set_page(int32_t value) { page_number = value; }
|
||||
uint32_t page(void) { return page_number; }
|
||||
uint32_t page_count(void) { return (config.registers_count + config.registers_per_page - 1) / config.registers_per_page; }
|
||||
|
||||
void draw_legend(const Coord left, Painter& painter);
|
||||
void draw_values(const Coord left, Painter& painter);
|
||||
private:
|
||||
const RegistersWidgetConfig config;
|
||||
uint32_t page_number;
|
||||
|
||||
static constexpr size_t row_height = 16;
|
||||
|
||||
void draw_legend(const Coord left, Painter& painter);
|
||||
void draw_values(const Coord left, Painter& painter);
|
||||
};
|
||||
|
||||
class RegistersView : public View {
|
||||
public:
|
||||
RegistersView(
|
||||
NavigationView& nav,
|
||||
const std::string& title,
|
||||
RegistersWidgetConfig&& config,
|
||||
std::function<uint32_t(const size_t register_number)>&& reader
|
||||
);
|
||||
public:
|
||||
RegistersView(NavigationView& nav, const std::string& title, RegistersWidgetConfig&& config);
|
||||
void focus() override;
|
||||
bool on_encoder(const EncoderEvent delta) override;
|
||||
|
||||
void focus();
|
||||
private:
|
||||
Text text_title{};
|
||||
|
||||
private:
|
||||
Text text_title { };
|
||||
RegistersWidget registers_widget;
|
||||
|
||||
RegistersWidget registers_widget;
|
||||
Button button_update{
|
||||
{16, 280, 96, 24},
|
||||
"Update"};
|
||||
|
||||
Button button_update {
|
||||
{ 16, 256, 96, 24 },
|
||||
"Update"
|
||||
};
|
||||
Button button_done{
|
||||
{128, 280, 96, 24},
|
||||
"Done"};
|
||||
|
||||
Button button_done {
|
||||
{ 128, 256, 96, 24 },
|
||||
"Done"
|
||||
};
|
||||
Button button_write{
|
||||
{144, 248, 80, 20},
|
||||
"Write"};
|
||||
|
||||
Labels labels{
|
||||
{{1 * 8, 248}, "Reg:", Theme::getInstance()->fg_light->foreground},
|
||||
{{8 * 8, 248}, "Data:", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
SymField field_write_reg_num{
|
||||
{5 * 8, 248},
|
||||
2,
|
||||
SymField::Type::Hex};
|
||||
|
||||
SymField field_write_data_val{
|
||||
{13 * 8, 248},
|
||||
4,
|
||||
SymField::Type::Hex};
|
||||
};
|
||||
|
||||
class ControlsSwitchesWidget : public Widget {
|
||||
public:
|
||||
ControlsSwitchesWidget(
|
||||
Rect parent_rect
|
||||
) : Widget { parent_rect },
|
||||
key_event_mask(0)
|
||||
{
|
||||
set_focusable(true);
|
||||
}
|
||||
public:
|
||||
ControlsSwitchesWidget(
|
||||
Rect parent_rect)
|
||||
: Widget{parent_rect},
|
||||
key_event_mask(0),
|
||||
long_press_key_event_mask{0},
|
||||
last_delta{0} {
|
||||
set_focusable(true);
|
||||
}
|
||||
|
||||
void on_show() override;
|
||||
bool on_key(const KeyEvent key) override;
|
||||
void on_show() override;
|
||||
bool on_key(const KeyEvent key) override;
|
||||
bool on_encoder(const EncoderEvent delta) override;
|
||||
|
||||
void paint(Painter& painter) override;
|
||||
void paint(Painter& painter) override;
|
||||
|
||||
private:
|
||||
uint8_t key_event_mask;
|
||||
private:
|
||||
uint8_t key_event_mask;
|
||||
uint8_t long_press_key_event_mask;
|
||||
EncoderEvent last_delta;
|
||||
|
||||
MessageHandlerRegistration message_handler_frame_sync {
|
||||
Message::ID::DisplayFrameSync,
|
||||
[this](const Message* const) {
|
||||
this->on_frame_sync();
|
||||
}
|
||||
};
|
||||
MessageHandlerRegistration message_handler_frame_sync{
|
||||
Message::ID::DisplayFrameSync,
|
||||
[this](const Message* const) {
|
||||
this->on_frame_sync();
|
||||
}};
|
||||
|
||||
void on_frame_sync();
|
||||
void on_frame_sync();
|
||||
};
|
||||
|
||||
class DebugControlsView : public View {
|
||||
public:
|
||||
explicit DebugControlsView(NavigationView& nav);
|
||||
public:
|
||||
explicit DebugControlsView(NavigationView& nav);
|
||||
|
||||
void focus() override;
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "Buttons Test"; };
|
||||
std::string title() const override { return "Buttons Test"; };
|
||||
|
||||
private:
|
||||
Text text_title {
|
||||
{ 64, 16, 184, 16 },
|
||||
"Controls State",
|
||||
};
|
||||
private:
|
||||
Labels labels{
|
||||
{{8 * 8, 1 * 16}, "Controls State", Theme::getInstance()->bg_darkest->foreground},
|
||||
{{0 * 8, 11 * 16}, "Dial:", Theme::getInstance()->fg_medium->foreground},
|
||||
{{0 * 8, 14 * 16}, "Long-Press Mode:", Theme::getInstance()->fg_medium->foreground}};
|
||||
|
||||
ControlsSwitchesWidget switches_widget {
|
||||
{ 80, 80, 80, 112 },
|
||||
};
|
||||
ControlsSwitchesWidget switches_widget{
|
||||
{80, 80, 80, 112},
|
||||
};
|
||||
|
||||
Button button_done {
|
||||
{ 72, 264, 96, 24 },
|
||||
"Done"
|
||||
};
|
||||
OptionsField options_switches_mode{
|
||||
{17 * 8, 14 * 16},
|
||||
8,
|
||||
{
|
||||
{"Disabled", 0},
|
||||
{"Enabled", 0xFF}, // all KeyEvent bits to long-press mode
|
||||
}};
|
||||
|
||||
Button button_done{
|
||||
{72, 264, 96, 24},
|
||||
"Done"};
|
||||
};
|
||||
|
||||
class DebugMemoryDumpView : public View {
|
||||
public:
|
||||
DebugMemoryDumpView(NavigationView& nav);
|
||||
void focus() override;
|
||||
std::string title() const override { return "Memory Dump"; };
|
||||
|
||||
private:
|
||||
Button button_dump{
|
||||
{72, 4 * 16, 96, 24},
|
||||
"Dump"};
|
||||
|
||||
Button button_read{
|
||||
{16, 11 * 16, 96, 24},
|
||||
"Read"};
|
||||
|
||||
Button button_write{
|
||||
{128, 11 * 16, 96, 24},
|
||||
"Write"};
|
||||
|
||||
Button button_done{
|
||||
{128, 240, 96, 24},
|
||||
"Done"};
|
||||
|
||||
Labels labels{
|
||||
{{5 * 8, 1 * 16}, "Dump Range to File", Theme::getInstance()->fg_yellow->foreground},
|
||||
{{0 * 8, 2 * 16}, "Starting Address: 0x", Theme::getInstance()->fg_light->foreground},
|
||||
{{0 * 8, 3 * 16}, "Byte Count: 0x", Theme::getInstance()->fg_light->foreground},
|
||||
{{3 * 8, 8 * 16}, "Read/Write Single Word", Theme::getInstance()->fg_yellow->foreground},
|
||||
{{0 * 8, 9 * 16}, "Memory Address: 0x", Theme::getInstance()->fg_light->foreground},
|
||||
{{0 * 8, 10 * 16}, "Data Value: 0x", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
SymField field_starting_address{
|
||||
{20 * 8, 2 * 16},
|
||||
8,
|
||||
SymField::Type::Hex};
|
||||
|
||||
SymField field_byte_count{
|
||||
{20 * 8, 3 * 16},
|
||||
8,
|
||||
SymField::Type::Hex};
|
||||
|
||||
SymField field_rw_address{
|
||||
{20 * 8, 9 * 16},
|
||||
8,
|
||||
SymField::Type::Hex};
|
||||
|
||||
SymField field_data_value{
|
||||
{20 * 8, 10 * 16},
|
||||
8,
|
||||
SymField::Type::Hex};
|
||||
};
|
||||
|
||||
class DebugPmemView : public View {
|
||||
public:
|
||||
DebugPmemView(NavigationView& nav);
|
||||
void focus() override;
|
||||
bool on_encoder(const EncoderEvent delta) override;
|
||||
std::string title() const override { return "P.Mem Debug"; }
|
||||
|
||||
private:
|
||||
static constexpr uint8_t page_size{96}; // Must be multiply of 4 otherwise bit shifting for register view wont work properly
|
||||
static constexpr uint8_t page_count{(portapack::memory::map::backup_ram.size() + page_size - 1) / page_size};
|
||||
|
||||
RegistersWidget registers_widget;
|
||||
|
||||
Text text_checksum{{16, 232, 208, 16}};
|
||||
Text text_checksum2{{16, 248, 208, 16}};
|
||||
|
||||
Button button_ok{
|
||||
{240 / 3, 270, 240 / 3, 24},
|
||||
"OK",
|
||||
};
|
||||
|
||||
void update();
|
||||
};
|
||||
|
||||
class DebugScreenTest : public View {
|
||||
public:
|
||||
DebugScreenTest(NavigationView& nav);
|
||||
bool on_key(KeyEvent key) override;
|
||||
bool on_encoder(EncoderEvent delta) override;
|
||||
bool on_touch(TouchEvent event) override;
|
||||
void paint(Painter& painter) override;
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
Point pen_pos{};
|
||||
Color pen_color{0};
|
||||
int16_t pen_size{10};
|
||||
};
|
||||
|
||||
/*class DebugLCRView : public View {
|
||||
public:
|
||||
DebugLCRView(NavigationView& nav, std::string lcrstring);
|
||||
DebugLCRView(NavigationView& nav, std::string lcrstring);
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "LCR debug"; };
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "LCR debug"; };
|
||||
|
||||
private:
|
||||
Console console {
|
||||
{ 8, 16, 224, 240 }
|
||||
};
|
||||
|
||||
Button button_exit {
|
||||
{ 72, 264, 96, 32 },
|
||||
"Exit"
|
||||
};
|
||||
Console console {
|
||||
{ 8, 16, 224, 240 }
|
||||
};
|
||||
|
||||
Button button_exit {
|
||||
{ 72, 264, 96, 32 },
|
||||
"Exit"
|
||||
};
|
||||
};*/
|
||||
|
||||
class DebugPeripheralsMenuView : public BtnGridView {
|
||||
public:
|
||||
DebugPeripheralsMenuView(NavigationView& nav);
|
||||
std::string title() const override { return "Peripherals"; };
|
||||
public:
|
||||
DebugPeripheralsMenuView(NavigationView& nav);
|
||||
std::string title() const override { return "Peripherals"; };
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
void on_populate() override;
|
||||
};
|
||||
|
||||
class DebugReboot : public BtnGridView {
|
||||
public:
|
||||
DebugReboot(NavigationView& nav);
|
||||
|
||||
private:
|
||||
void on_populate() override;
|
||||
};
|
||||
|
||||
class DebugMenuView : public BtnGridView {
|
||||
public:
|
||||
DebugMenuView(NavigationView& nav);
|
||||
std::string title() const override { return "Debug"; };
|
||||
public:
|
||||
DebugMenuView(NavigationView& nav);
|
||||
std::string title() const override { return "Debug"; };
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
void on_populate() override;
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
#endif/*__UI_DEBUG_H__*/
|
||||
#endif /*__UI_DEBUG_H__*/
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
#include "ui_debug_battery.hpp"
|
||||
#include "string_format.hpp"
|
||||
|
||||
namespace ui {
|
||||
|
||||
BatteryCapacityView::RegisterEntry BatteryCapacityView::get_entry(size_t index) {
|
||||
if (index < battery::max17055::MAX17055::entries_count) {
|
||||
return battery::max17055::MAX17055::entries[index];
|
||||
}
|
||||
return {"", 0, "", 0, false, "", false, 0, false, false, false, 0, false};
|
||||
}
|
||||
|
||||
BatteryCapacityView::BatteryCapacityView(NavigationView& nav) {
|
||||
for (size_t i = 0; i < ENTRIES_PER_PAGE; ++i) {
|
||||
name_texts[i].set_parent_rect({0 * 8, static_cast<int>((i + 1) * 16), 8 * 8, 16});
|
||||
addr_texts[i].set_parent_rect({9 * 8, static_cast<int>((i + 1) * 16), 4 * 8, 16});
|
||||
hex_texts[i].set_parent_rect({14 * 8, static_cast<int>((i + 1) * 16), 6 * 8, 16});
|
||||
value_texts[i].set_parent_rect({21 * 8, static_cast<int>((i + 1) * 16), 10 * 8, 16});
|
||||
|
||||
add_child(&name_texts[i]);
|
||||
add_child(&addr_texts[i]);
|
||||
add_child(&hex_texts[i]);
|
||||
add_child(&value_texts[i]);
|
||||
}
|
||||
|
||||
add_children({&labels, &page_text, &button_done});
|
||||
|
||||
button_done.on_select = [&nav](Button&) { nav.pop(); };
|
||||
|
||||
populate_page(0);
|
||||
update_page_text();
|
||||
}
|
||||
|
||||
void BatteryCapacityView::focus() {
|
||||
button_done.focus();
|
||||
}
|
||||
|
||||
bool BatteryCapacityView::on_encoder(const EncoderEvent delta) {
|
||||
int32_t new_page = current_page + delta;
|
||||
if (new_page >= 0 && new_page < ((int32_t)battery::max17055::MAX17055::entries_count + ENTRIES_PER_PAGE - 1) / ENTRIES_PER_PAGE) {
|
||||
current_page = new_page;
|
||||
populate_page(current_page * ENTRIES_PER_PAGE);
|
||||
update_page_text();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void BatteryCapacityView::update_values() {
|
||||
for (size_t i = 0; i < ENTRIES_PER_PAGE; ++i) {
|
||||
size_t entry_index = current_page * ENTRIES_PER_PAGE + i;
|
||||
if (entry_index < battery::max17055::MAX17055::entries_count) {
|
||||
const auto entry = get_entry(entry_index);
|
||||
uint16_t raw_value = battery::BatteryManagement::read_register(entry.address);
|
||||
|
||||
hex_texts[i].set("0x" + to_string_hex(raw_value, 4));
|
||||
|
||||
float scaled_value;
|
||||
if (entry.is_signed) {
|
||||
int16_t signed_value = static_cast<int16_t>(raw_value);
|
||||
scaled_value = signed_value * entry.scalar;
|
||||
} else {
|
||||
scaled_value = raw_value * entry.scalar;
|
||||
}
|
||||
|
||||
// Format the value with appropriate decimal places
|
||||
std::string formatted_value;
|
||||
if (entry.resolution > 0) {
|
||||
formatted_value = to_string_decimal(scaled_value, std::min(entry.resolution, 3));
|
||||
} else {
|
||||
formatted_value = to_string_dec_int(scaled_value); // Show up to 3 decimal places
|
||||
}
|
||||
|
||||
value_texts[i].set(formatted_value + " " + entry.unit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BatteryCapacityView::populate_page(int start_index) {
|
||||
for (size_t i = 0; i < ENTRIES_PER_PAGE; ++i) {
|
||||
size_t entry_index = start_index + i;
|
||||
if (entry_index < battery::max17055::MAX17055::entries_count) {
|
||||
const auto entry = get_entry(entry_index);
|
||||
name_texts[i].set(entry.name);
|
||||
addr_texts[i].set("0x" + to_string_hex(entry.address, 2));
|
||||
name_texts[i].hidden(false);
|
||||
addr_texts[i].hidden(false);
|
||||
hex_texts[i].hidden(false);
|
||||
value_texts[i].hidden(false);
|
||||
} else {
|
||||
name_texts[i].hidden(true);
|
||||
addr_texts[i].hidden(true);
|
||||
hex_texts[i].hidden(true);
|
||||
value_texts[i].hidden(true);
|
||||
}
|
||||
}
|
||||
update_values();
|
||||
}
|
||||
|
||||
void BatteryCapacityView::update_page_text() {
|
||||
int total_pages = (battery::max17055::MAX17055::entries_count + ENTRIES_PER_PAGE - 1) / ENTRIES_PER_PAGE;
|
||||
page_text.set("Page " + to_string_dec_uint(current_page + 1) + "/" + to_string_dec_uint(total_pages));
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
@@ -0,0 +1,48 @@
|
||||
#ifndef __UI_DEBUG_BATTERY_HPP__
|
||||
#define __UI_DEBUG_BATTERY_HPP__
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "battery.hpp"
|
||||
#include "max17055.hpp"
|
||||
|
||||
namespace ui {
|
||||
|
||||
class BatteryCapacityView : public View {
|
||||
public:
|
||||
BatteryCapacityView(NavigationView& nav);
|
||||
void focus() override;
|
||||
std::string title() const override { return "Battery Registers"; }
|
||||
|
||||
bool on_encoder(const EncoderEvent delta) override;
|
||||
|
||||
using RegisterEntry = battery::max17055::RegisterEntry;
|
||||
|
||||
private:
|
||||
static RegisterEntry get_entry(size_t index);
|
||||
|
||||
Labels labels{
|
||||
{{0 * 8, 0 * 16}, "Reg", Theme::getInstance()->fg_yellow->foreground},
|
||||
{{9 * 8, 0 * 16}, "Addr", Theme::getInstance()->fg_yellow->foreground},
|
||||
{{14 * 8, 0 * 16}, "Hex", Theme::getInstance()->fg_yellow->foreground},
|
||||
{{21 * 8, 0 * 16}, "Value", Theme::getInstance()->fg_yellow->foreground},
|
||||
};
|
||||
std::array<Text, 16> name_texts = {};
|
||||
std::array<Text, 16> addr_texts = {};
|
||||
std::array<Text, 16> hex_texts = {};
|
||||
std::array<Text, 16> value_texts = {};
|
||||
|
||||
Text page_text{{144, 284, 80, 16}, "Page 1/1"};
|
||||
Button button_done{{16, 280, 96, 24}, "Done"};
|
||||
|
||||
void update_values();
|
||||
void populate_page(int start_index);
|
||||
void update_page_text();
|
||||
int current_page = 0;
|
||||
static constexpr int ENTRIES_PER_PAGE = 16;
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
|
||||
#endif // __UI_DEBUG_BATTERY_HPP__
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Bernd Herzog
|
||||
* Copyright (C) 2023 Mark Thompson
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "ui_dfu_menu.hpp"
|
||||
#include "portapack_shared_memory.hpp"
|
||||
#include "performance_counter.hpp"
|
||||
#include "portapack.hpp"
|
||||
|
||||
namespace ui {
|
||||
|
||||
DfuMenu::DfuMenu(NavigationView& nav)
|
||||
: nav_(nav) {
|
||||
add_children({&text_head,
|
||||
&labels,
|
||||
&text_info_line_1,
|
||||
&text_info_line_2,
|
||||
&text_info_line_3,
|
||||
&text_info_line_4,
|
||||
&text_info_line_5,
|
||||
&text_info_line_6,
|
||||
&text_info_line_7,
|
||||
&text_info_line_8,
|
||||
&text_info_line_9,
|
||||
&text_info_line_10});
|
||||
|
||||
if (battery::BatteryManagement::isDetected()) {
|
||||
add_child(&voltage_label);
|
||||
add_child(&text_info_line_11);
|
||||
}
|
||||
}
|
||||
|
||||
void DfuMenu::paint(Painter& painter) {
|
||||
auto utilisation = get_cpu_utilisation_in_percent();
|
||||
size_t m0_fragmented_free_space = 0;
|
||||
const auto m0_fragments = chHeapStatus(NULL, &m0_fragmented_free_space);
|
||||
|
||||
auto lines = (battery::BatteryManagement::isDetected() ? 11 : 10) + 2;
|
||||
|
||||
text_info_line_1.set(to_string_dec_uint(chCoreStatus(), 6));
|
||||
text_info_line_2.set(to_string_dec_uint(m0_fragmented_free_space, 6));
|
||||
text_info_line_3.set(to_string_dec_uint(m0_fragments, 6));
|
||||
text_info_line_4.set(to_string_dec_uint((uint32_t)get_free_stack_space(), 6));
|
||||
text_info_line_5.set(to_string_dec_uint(utilisation, 6));
|
||||
text_info_line_6.set(to_string_dec_uint(shared_memory.m4_heap_usage, 6));
|
||||
text_info_line_7.set(to_string_dec_uint(shared_memory.m4_stack_usage, 6));
|
||||
text_info_line_8.set(to_string_dec_uint(shared_memory.m4_performance_counter, 6));
|
||||
text_info_line_9.set(to_string_dec_uint(shared_memory.m4_buffer_missed, 6));
|
||||
text_info_line_10.set(to_string_dec_uint(chTimeNow() / 1000, 6));
|
||||
if (battery::BatteryManagement::isDetected()) {
|
||||
text_info_line_11.set(to_string_decimal_padding((float)battery::BatteryManagement::getVoltage() / 1000.0, 3, 6));
|
||||
}
|
||||
|
||||
constexpr auto margin = 5;
|
||||
|
||||
painter.fill_rectangle(
|
||||
{{6 * CHARACTER_WIDTH - margin, 3 * LINE_HEIGHT - margin},
|
||||
{15 * CHARACTER_WIDTH + margin * 2, lines * LINE_HEIGHT + margin * 2}},
|
||||
Theme::getInstance()->bg_darkest->background);
|
||||
|
||||
painter.fill_rectangle(
|
||||
{{5 * CHARACTER_WIDTH - margin, 3 * LINE_HEIGHT - margin},
|
||||
{CHARACTER_WIDTH, lines * LINE_HEIGHT + margin * 2}},
|
||||
ui::Theme::getInstance()->fg_darkcyan->foreground);
|
||||
|
||||
painter.fill_rectangle(
|
||||
{{21 * CHARACTER_WIDTH + margin, 3 * LINE_HEIGHT - margin},
|
||||
{CHARACTER_WIDTH, lines * LINE_HEIGHT + margin * 2}},
|
||||
ui::Theme::getInstance()->fg_darkcyan->foreground);
|
||||
|
||||
painter.fill_rectangle(
|
||||
{{5 * CHARACTER_WIDTH - margin, 3 * LINE_HEIGHT - margin - 8},
|
||||
{17 * CHARACTER_WIDTH + margin * 2, 8}},
|
||||
ui::Theme::getInstance()->fg_darkcyan->foreground);
|
||||
|
||||
painter.fill_rectangle(
|
||||
{{5 * CHARACTER_WIDTH - margin, (lines + 3) * LINE_HEIGHT + margin},
|
||||
{17 * CHARACTER_WIDTH + margin * 2, 8}},
|
||||
ui::Theme::getInstance()->fg_darkcyan->foreground);
|
||||
}
|
||||
|
||||
DfuMenu2::DfuMenu2(NavigationView& nav)
|
||||
: nav_(nav) {
|
||||
add_children({&text_head,
|
||||
&labels,
|
||||
&text_info_line_1,
|
||||
&text_info_line_2,
|
||||
&text_info_line_3,
|
||||
&text_info_line_4,
|
||||
&text_info_line_5,
|
||||
&text_info_line_6,
|
||||
&text_info_line_7,
|
||||
&text_info_line_8,
|
||||
&text_info_line_9,
|
||||
&text_info_line_10,
|
||||
&text_info_line_11});
|
||||
}
|
||||
|
||||
void DfuMenu2::paint(Painter& painter) {
|
||||
text_info_line_1.set(to_string_dec_uint(portapack::receiver_model.target_frequency(), 10));
|
||||
text_info_line_2.set(to_string_dec_uint(portapack::receiver_model.baseband_bandwidth(), 10));
|
||||
text_info_line_3.set(to_string_dec_uint(portapack::receiver_model.sampling_rate(), 10));
|
||||
text_info_line_4.set(to_string_dec_uint(((uint32_t)shared_memory.m4_performance_counter) * 100 / 127, 10));
|
||||
text_info_line_5.set(to_string_dec_uint((uint32_t)portapack::receiver_model.modulation(), 10));
|
||||
text_info_line_6.set(to_string_dec_uint(portapack::receiver_model.am_configuration(), 10));
|
||||
text_info_line_7.set(to_string_dec_uint(portapack::receiver_model.nbfm_configuration(), 10));
|
||||
text_info_line_8.set(to_string_dec_uint(portapack::receiver_model.wfm_configuration(), 10));
|
||||
text_info_line_9.set(to_string_dec_uint(portapack::transmitter_model.target_frequency(), 10));
|
||||
text_info_line_10.set(to_string_dec_uint(portapack::transmitter_model.baseband_bandwidth(), 10));
|
||||
text_info_line_11.set(to_string_dec_uint(portapack::transmitter_model.sampling_rate(), 10));
|
||||
|
||||
constexpr auto margin = 5;
|
||||
constexpr auto lines = 11 + 2;
|
||||
|
||||
painter.fill_rectangle(
|
||||
{{5 * CHARACTER_WIDTH - margin, 3 * LINE_HEIGHT - margin},
|
||||
{19 * CHARACTER_WIDTH + margin * 2, lines * LINE_HEIGHT + margin * 2}},
|
||||
Theme::getInstance()->bg_darkest->background);
|
||||
|
||||
painter.fill_rectangle(
|
||||
{{4 * CHARACTER_WIDTH - margin, 3 * LINE_HEIGHT - margin},
|
||||
{CHARACTER_WIDTH, lines * LINE_HEIGHT + margin * 2}},
|
||||
ui::Theme::getInstance()->fg_darkcyan->foreground);
|
||||
|
||||
painter.fill_rectangle(
|
||||
{{24 * CHARACTER_WIDTH + margin, 3 * LINE_HEIGHT - margin},
|
||||
{CHARACTER_WIDTH, lines * LINE_HEIGHT + margin * 2}},
|
||||
ui::Theme::getInstance()->fg_darkcyan->foreground);
|
||||
|
||||
painter.fill_rectangle(
|
||||
{{4 * CHARACTER_WIDTH - margin, 3 * LINE_HEIGHT - margin - 8},
|
||||
{21 * CHARACTER_WIDTH + margin * 2, 8}},
|
||||
ui::Theme::getInstance()->fg_darkcyan->foreground);
|
||||
|
||||
painter.fill_rectangle(
|
||||
{{4 * CHARACTER_WIDTH - margin, (lines + 3) * LINE_HEIGHT + margin},
|
||||
{21 * CHARACTER_WIDTH + margin * 2, 8}},
|
||||
ui::Theme::getInstance()->fg_darkcyan->foreground);
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Bernd Herzog
|
||||
* Copyright (C) 2023 Mark Thompson
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __UI_DFU_MENU_H__
|
||||
#define __UI_DFU_MENU_H__
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "ui_widget.hpp"
|
||||
#include "event_m0.hpp"
|
||||
#include "debug.hpp"
|
||||
#include "string_format.hpp"
|
||||
|
||||
#define LINE_HEIGHT 16
|
||||
#define CHARACTER_WIDTH 8
|
||||
|
||||
namespace ui {
|
||||
class NavigationView;
|
||||
|
||||
class DfuMenu : public View {
|
||||
public:
|
||||
DfuMenu(NavigationView& nav);
|
||||
~DfuMenu() = default;
|
||||
|
||||
void paint(Painter& painter) override;
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
|
||||
Text text_head{{6 * CHARACTER_WIDTH, 3 * LINE_HEIGHT, 11 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, "Performance"};
|
||||
|
||||
Labels labels{
|
||||
{{6 * CHARACTER_WIDTH, 5 * LINE_HEIGHT}, "M0 core:", Theme::getInstance()->fg_darkcyan->foreground},
|
||||
{{6 * CHARACTER_WIDTH, 6 * LINE_HEIGHT}, "M0 heap:", Theme::getInstance()->fg_darkcyan->foreground},
|
||||
{{6 * CHARACTER_WIDTH, 7 * LINE_HEIGHT}, "M0 frags:", Theme::getInstance()->fg_darkcyan->foreground},
|
||||
{{6 * CHARACTER_WIDTH, 8 * LINE_HEIGHT}, "M0 stack:", Theme::getInstance()->fg_darkcyan->foreground},
|
||||
{{6 * CHARACTER_WIDTH, 9 * LINE_HEIGHT}, "M0 cpu %:", Theme::getInstance()->fg_darkcyan->foreground},
|
||||
{{6 * CHARACTER_WIDTH, 10 * LINE_HEIGHT}, "M4 heap:", Theme::getInstance()->fg_darkcyan->foreground},
|
||||
{{6 * CHARACTER_WIDTH, 11 * LINE_HEIGHT}, "M4 stack:", Theme::getInstance()->fg_darkcyan->foreground},
|
||||
{{6 * CHARACTER_WIDTH, 12 * LINE_HEIGHT}, "M4 cpu %:", Theme::getInstance()->fg_darkcyan->foreground},
|
||||
{{6 * CHARACTER_WIDTH, 13 * LINE_HEIGHT}, "M4 miss:", Theme::getInstance()->fg_darkcyan->foreground},
|
||||
{{6 * CHARACTER_WIDTH, 14 * LINE_HEIGHT}, "Uptime:", Theme::getInstance()->fg_darkcyan->foreground}};
|
||||
|
||||
Labels voltage_label{{{6 * CHARACTER_WIDTH, 15 * LINE_HEIGHT}, "Voltage:", Theme::getInstance()->fg_darkcyan->foreground}};
|
||||
|
||||
Text text_info_line_1{{15 * CHARACTER_WIDTH, 5 * LINE_HEIGHT, 6 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_2{{15 * CHARACTER_WIDTH, 6 * LINE_HEIGHT, 6 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_3{{15 * CHARACTER_WIDTH, 7 * LINE_HEIGHT, 6 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_4{{15 * CHARACTER_WIDTH, 8 * LINE_HEIGHT, 6 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_5{{15 * CHARACTER_WIDTH, 9 * LINE_HEIGHT, 6 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_6{{15 * CHARACTER_WIDTH, 10 * LINE_HEIGHT, 6 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_7{{15 * CHARACTER_WIDTH, 11 * LINE_HEIGHT, 6 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_8{{15 * CHARACTER_WIDTH, 12 * LINE_HEIGHT, 6 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_9{{15 * CHARACTER_WIDTH, 13 * LINE_HEIGHT, 6 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_10{{15 * CHARACTER_WIDTH, 14 * LINE_HEIGHT, 6 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_11{{15 * CHARACTER_WIDTH, 15 * LINE_HEIGHT, 6 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
};
|
||||
|
||||
class DfuMenu2 : public View {
|
||||
public:
|
||||
DfuMenu2(NavigationView& nav);
|
||||
~DfuMenu2() = default;
|
||||
|
||||
void paint(Painter& painter) override;
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
|
||||
Text text_head{{6 * CHARACTER_WIDTH, 3 * LINE_HEIGHT, 14 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, "Radio Settings"};
|
||||
|
||||
Labels labels{
|
||||
{{5 * CHARACTER_WIDTH, 5 * LINE_HEIGHT}, "RX Freq:", Theme::getInstance()->fg_darkcyan->foreground},
|
||||
{{5 * CHARACTER_WIDTH, 6 * LINE_HEIGHT}, "RX BW:", Theme::getInstance()->fg_darkcyan->foreground},
|
||||
{{5 * CHARACTER_WIDTH, 7 * LINE_HEIGHT}, "RX SampR:", Theme::getInstance()->fg_darkcyan->foreground},
|
||||
{{5 * CHARACTER_WIDTH, 8 * LINE_HEIGHT}, "RX Satu%:", Theme::getInstance()->fg_darkcyan->foreground},
|
||||
{{5 * CHARACTER_WIDTH, 9 * LINE_HEIGHT}, "Modulatn:", Theme::getInstance()->fg_darkcyan->foreground},
|
||||
{{5 * CHARACTER_WIDTH, 10 * LINE_HEIGHT}, "AM cfg:", Theme::getInstance()->fg_darkcyan->foreground},
|
||||
{{5 * CHARACTER_WIDTH, 11 * LINE_HEIGHT}, "NBFM cfg:", Theme::getInstance()->fg_darkcyan->foreground},
|
||||
{{5 * CHARACTER_WIDTH, 12 * LINE_HEIGHT}, "WFM cfg:", Theme::getInstance()->fg_darkcyan->foreground},
|
||||
{{5 * CHARACTER_WIDTH, 13 * LINE_HEIGHT}, "TX Freq:", Theme::getInstance()->fg_darkcyan->foreground},
|
||||
{{5 * CHARACTER_WIDTH, 14 * LINE_HEIGHT}, "TX BW:", Theme::getInstance()->fg_darkcyan->foreground},
|
||||
{{5 * CHARACTER_WIDTH, 15 * LINE_HEIGHT}, "TX SampR:", Theme::getInstance()->fg_darkcyan->foreground},
|
||||
};
|
||||
|
||||
Text text_info_line_1{{14 * CHARACTER_WIDTH, 5 * LINE_HEIGHT, 10 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_2{{14 * CHARACTER_WIDTH, 6 * LINE_HEIGHT, 10 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_3{{14 * CHARACTER_WIDTH, 7 * LINE_HEIGHT, 10 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_4{{14 * CHARACTER_WIDTH, 8 * LINE_HEIGHT, 10 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_5{{14 * CHARACTER_WIDTH, 9 * LINE_HEIGHT, 10 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_6{{14 * CHARACTER_WIDTH, 10 * LINE_HEIGHT, 10 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_7{{14 * CHARACTER_WIDTH, 11 * LINE_HEIGHT, 10 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_8{{14 * CHARACTER_WIDTH, 12 * LINE_HEIGHT, 10 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_9{{14 * CHARACTER_WIDTH, 13 * LINE_HEIGHT, 10 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_10{{14 * CHARACTER_WIDTH, 14 * LINE_HEIGHT, 10 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
Text text_info_line_11{{14 * CHARACTER_WIDTH, 15 * LINE_HEIGHT, 10 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
#endif /*__UI_DFU_MENU_H__*/
|
||||
@@ -24,306 +24,293 @@
|
||||
|
||||
#include "baseband_api.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "cpld_update.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
|
||||
namespace ui {
|
||||
|
||||
EncodersConfigView::EncodersConfigView(
|
||||
NavigationView&, Rect parent_rect
|
||||
) {
|
||||
using option_t = std::pair<std::string, int32_t>;
|
||||
std::vector<option_t> enc_options;
|
||||
size_t i;
|
||||
NavigationView&,
|
||||
Rect parent_rect) {
|
||||
using option_t = std::pair<std::string, int32_t>;
|
||||
std::vector<option_t> enc_options;
|
||||
size_t i;
|
||||
|
||||
set_parent_rect(parent_rect);
|
||||
hidden(true);
|
||||
set_parent_rect(parent_rect);
|
||||
hidden(true);
|
||||
|
||||
// Default encoder def
|
||||
encoder_def = &encoder_defs[0];
|
||||
// Default encoder def
|
||||
encoder_def = &encoder_defs[0];
|
||||
|
||||
add_children({
|
||||
&labels,
|
||||
&options_enctype,
|
||||
&field_repeat_min,
|
||||
&field_clk,
|
||||
&field_clk_step,
|
||||
&field_frameduration,
|
||||
&field_frameduration_step,
|
||||
&symfield_word,
|
||||
&text_format,
|
||||
&waveform
|
||||
});
|
||||
add_children({&labels,
|
||||
&options_enctype,
|
||||
&field_repeat_min,
|
||||
&field_clk,
|
||||
&field_clk_step,
|
||||
&field_frameduration,
|
||||
&field_frameduration_step,
|
||||
&text_format,
|
||||
&waveform});
|
||||
|
||||
// Load encoder types in option field
|
||||
for (i = 0; i < ENC_TYPES_COUNT; i++)
|
||||
enc_options.emplace_back(std::make_pair(encoder_defs[i].name, i));
|
||||
// Load encoder types in option field
|
||||
for (i = 0; i < ENC_TYPES_COUNT; i++)
|
||||
enc_options.emplace_back(std::make_pair(encoder_defs[i].name, i));
|
||||
|
||||
options_enctype.on_change = [this](size_t index, int32_t) {
|
||||
on_type_change(index);
|
||||
};
|
||||
options_enctype.on_change = [this](size_t index, int32_t) {
|
||||
on_type_change(index);
|
||||
};
|
||||
|
||||
options_enctype.set_options(enc_options);
|
||||
options_enctype.set_selected_index(0);
|
||||
options_enctype.set_options(enc_options);
|
||||
options_enctype.set_selected_index(0);
|
||||
|
||||
symfield_word.on_change = [this]() {
|
||||
generate_frame();
|
||||
};
|
||||
// Selecting input clock changes symbol and word duration
|
||||
field_clk.on_change = [this](int32_t value) {
|
||||
// value is in kHz, new_value is in us
|
||||
int32_t new_value = (encoder_def->clk_per_symbol * 1000000) / (value * 1000);
|
||||
if (new_value != field_frameduration.value())
|
||||
field_frameduration.set_value(new_value * encoder_def->word_length, false);
|
||||
};
|
||||
|
||||
// Selecting input clock changes symbol and word duration
|
||||
field_clk.on_change = [this](int32_t value) {
|
||||
// value is in kHz, new_value is in us
|
||||
int32_t new_value = (encoder_def->clk_per_symbol * 1000000) / (value * 1000);
|
||||
if (new_value != field_frameduration.value())
|
||||
field_frameduration.set_value(new_value * encoder_def->word_length, false);
|
||||
};
|
||||
field_clk_step.on_change = [this](size_t, int32_t value) {
|
||||
field_clk.set_step(value);
|
||||
};
|
||||
|
||||
field_clk_step.on_change = [this](size_t, int32_t value) {
|
||||
field_clk.set_step(value);
|
||||
};
|
||||
// Selecting word duration changes input clock and symbol duration
|
||||
field_frameduration.on_change = [this](int32_t value) {
|
||||
// value is in us, new_value is in kHz
|
||||
int32_t new_value = (value * 1000) / (encoder_def->word_length * encoder_def->clk_per_symbol);
|
||||
if (new_value != field_clk.value())
|
||||
field_clk.set_value(1000000 / new_value, false);
|
||||
};
|
||||
|
||||
// Selecting word duration changes input clock and symbol duration
|
||||
field_frameduration.on_change = [this](int32_t value) {
|
||||
// value is in us, new_value is in kHz
|
||||
int32_t new_value = (value * 1000) / (encoder_def->word_length * encoder_def->clk_per_symbol);
|
||||
if (new_value != field_clk.value())
|
||||
field_clk.set_value(1000000 / new_value, false);
|
||||
};
|
||||
|
||||
field_frameduration_step.on_change = [this](size_t, int32_t value) {
|
||||
field_frameduration.set_step(value);
|
||||
};
|
||||
field_frameduration_step.on_change = [this](size_t, int32_t value) {
|
||||
field_frameduration.set_step(value);
|
||||
};
|
||||
}
|
||||
|
||||
void EncodersConfigView::focus() {
|
||||
options_enctype.focus();
|
||||
options_enctype.focus();
|
||||
}
|
||||
|
||||
void EncodersConfigView::on_type_change(size_t index) {
|
||||
std::string format_string = "";
|
||||
size_t word_length;
|
||||
char symbol_type;
|
||||
// Remove existing SymField controls.
|
||||
for (auto& symfield : symfields_word)
|
||||
remove_child(symfield.get());
|
||||
symfields_word.clear();
|
||||
|
||||
encoder_def = &encoder_defs[index];
|
||||
encoder_def = &encoder_defs[index];
|
||||
field_clk.set_value(encoder_def->default_speed / 1000);
|
||||
field_repeat_min.set_value(encoder_def->repeat_min);
|
||||
|
||||
field_clk.set_value(encoder_def->default_speed / 1000);
|
||||
field_repeat_min.set_value(encoder_def->repeat_min);
|
||||
// Add new SymFields.
|
||||
Point pos{2 * 8, 9 * 8};
|
||||
std::string format_string;
|
||||
uint8_t word_length = encoder_def->word_length;
|
||||
auto on_change_handler = [this](SymField&) {
|
||||
generate_frame();
|
||||
};
|
||||
|
||||
// SymField setup
|
||||
word_length = encoder_def->word_length;
|
||||
symfield_word.set_length(word_length);
|
||||
size_t n = 0, i = 0;
|
||||
while (n < word_length) {
|
||||
symbol_type = encoder_def->word_format[i++];
|
||||
if (symbol_type == 'A') {
|
||||
symfield_word.set_symbol_list(n++, encoder_def->address_symbols);
|
||||
format_string += 'A';
|
||||
} else if (symbol_type == 'D') {
|
||||
symfield_word.set_symbol_list(n++, encoder_def->data_symbols);
|
||||
format_string += 'D';
|
||||
}
|
||||
}
|
||||
for (uint8_t i = 0; i < word_length; i++) {
|
||||
auto symbol_type = encoder_def->word_format[i];
|
||||
symfields_word.push_back(std::make_unique<SymField>(pos, 1));
|
||||
auto& symfield = symfields_word.back();
|
||||
symfield->on_change = on_change_handler;
|
||||
|
||||
// Ugly :( Pad to erase
|
||||
format_string.append(24 - format_string.size(), ' ');
|
||||
switch (symbol_type) {
|
||||
case 'A':
|
||||
symfield->set_symbol_list(encoder_def->address_symbols);
|
||||
format_string += 'A';
|
||||
break;
|
||||
case 'D':
|
||||
symfield->set_symbol_list(encoder_def->data_symbols);
|
||||
format_string += 'D';
|
||||
break;
|
||||
}
|
||||
|
||||
text_format.set(format_string);
|
||||
add_child(symfield.get());
|
||||
pos += Point{8, 0};
|
||||
}
|
||||
|
||||
generate_frame();
|
||||
// Ugly :( Pad to erase
|
||||
format_string.append(24 - format_string.size(), ' ');
|
||||
text_format.set(format_string);
|
||||
|
||||
generate_frame();
|
||||
}
|
||||
|
||||
void EncodersConfigView::on_show() {
|
||||
options_enctype.set_selected_index(0);
|
||||
on_type_change(0);
|
||||
options_enctype.set_selected_index(0);
|
||||
on_type_change(0);
|
||||
}
|
||||
|
||||
void EncodersConfigView::draw_waveform() {
|
||||
size_t length = frame_fragments.length();
|
||||
size_t length = frame_fragments.length();
|
||||
|
||||
for (size_t n = 0; n < length; n++)
|
||||
waveform_buffer[n] = (frame_fragments[n] == '0') ? 0 : 1;
|
||||
for (size_t n = 0; n < length; n++)
|
||||
waveform_buffer[n] = (frame_fragments[n] == '0') ? 0 : 1;
|
||||
|
||||
waveform.set_length(length);
|
||||
waveform.set_dirty();
|
||||
waveform.set_length(length);
|
||||
waveform.set_dirty();
|
||||
}
|
||||
|
||||
void EncodersConfigView::generate_frame() {
|
||||
size_t i = 0;
|
||||
frame_fragments.clear();
|
||||
|
||||
frame_fragments.clear();
|
||||
size_t i = 0;
|
||||
for (auto c : encoder_def->word_format) {
|
||||
if (c == 'S')
|
||||
frame_fragments += encoder_def->sync;
|
||||
else if (c == '\0')
|
||||
break;
|
||||
else {
|
||||
auto offset = symfields_word[i++]->get_offset(0);
|
||||
frame_fragments += encoder_def->bit_format[offset];
|
||||
}
|
||||
}
|
||||
|
||||
for (auto c : encoder_def->word_format) {
|
||||
if (c == 'S')
|
||||
frame_fragments += encoder_def->sync;
|
||||
else if (!c)
|
||||
break;
|
||||
else
|
||||
frame_fragments += encoder_def->bit_format[symfield_word.get_sym(i++)];
|
||||
}
|
||||
|
||||
draw_waveform();
|
||||
draw_waveform();
|
||||
}
|
||||
|
||||
uint8_t EncodersConfigView::repeat_min() {
|
||||
return field_repeat_min.value();
|
||||
return field_repeat_min.value();
|
||||
}
|
||||
|
||||
uint32_t EncodersConfigView::samples_per_bit() {
|
||||
return OOK_SAMPLERATE / ((field_clk.value() * 1000) / encoder_def->clk_per_fragment);
|
||||
return OOK_SAMPLERATE / ((field_clk.value() * 1000) / encoder_def->clk_per_fragment);
|
||||
}
|
||||
|
||||
uint32_t EncodersConfigView::pause_symbols() {
|
||||
return encoder_def->pause_symbols;
|
||||
return encoder_def->pause_symbols;
|
||||
}
|
||||
|
||||
void EncodersScanView::focus() {
|
||||
field_length.focus();
|
||||
field_length.focus();
|
||||
}
|
||||
|
||||
EncodersScanView::EncodersScanView(
|
||||
NavigationView&, Rect parent_rect
|
||||
) {
|
||||
set_parent_rect(parent_rect);
|
||||
hidden(true);
|
||||
NavigationView&,
|
||||
Rect parent_rect) {
|
||||
set_parent_rect(parent_rect);
|
||||
hidden(true);
|
||||
|
||||
add_children({
|
||||
&labels,
|
||||
&field_length,
|
||||
&bit_length_10,
|
||||
&bit_length
|
||||
});
|
||||
add_children({&labels,
|
||||
&field_length,
|
||||
&bit_length_10,
|
||||
&bit_length});
|
||||
|
||||
field_length.set_value(8);
|
||||
bit_length_10.set_value(40);
|
||||
bit_length.set_value(0);
|
||||
field_length.set_value(8);
|
||||
bit_length_10.set_value(40);
|
||||
bit_length.set_value(0);
|
||||
}
|
||||
|
||||
void EncodersView::focus() {
|
||||
tab_view.focus();
|
||||
tab_view.focus();
|
||||
}
|
||||
|
||||
EncodersView::~EncodersView() {
|
||||
// save app settings
|
||||
app_settings.tx_frequency = transmitter_model.tuning_frequency();
|
||||
settings.save("tx_ook", &app_settings);
|
||||
|
||||
transmitter_model.disable();
|
||||
hackrf::cpld::load_sram_no_verify(); // ghost signal c/m to the problem at the exit .
|
||||
baseband::shutdown(); // better this function after load_sram()
|
||||
transmitter_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
void EncodersView::update_progress() {
|
||||
std::string str_buffer;
|
||||
std::string str_buffer;
|
||||
|
||||
if (tx_mode == SINGLE || tx_mode == SCAN) {
|
||||
str_buffer = to_string_dec_uint(repeat_index) + "/" + to_string_dec_uint(repeat_min);
|
||||
text_status.set(str_buffer);
|
||||
progressbar.set_value(repeat_index);
|
||||
} else {
|
||||
text_status.set("Ready");
|
||||
progressbar.set_value(0);
|
||||
}
|
||||
if (tx_mode == SINGLE || tx_mode == SCAN) {
|
||||
str_buffer = to_string_dec_uint(repeat_index) + "/" + to_string_dec_uint(repeat_min);
|
||||
text_status.set(str_buffer);
|
||||
progressbar.set_value(repeat_index);
|
||||
} else {
|
||||
text_status.set("Ready");
|
||||
progressbar.set_value(0);
|
||||
}
|
||||
}
|
||||
|
||||
void EncodersView::on_tx_progress(const uint32_t progress, const bool done) {
|
||||
if (!done) {
|
||||
// Repeating...
|
||||
repeat_index = progress + 1;
|
||||
update_progress();
|
||||
} else {
|
||||
// make sure all samples are transmitted before disabling radio
|
||||
chThdSleepMilliseconds(10);
|
||||
if (!done) {
|
||||
// Repeating...
|
||||
repeat_index = progress + 1;
|
||||
update_progress();
|
||||
} else {
|
||||
// make sure all samples are transmitted before disabling radio
|
||||
chThdSleepMilliseconds(10);
|
||||
|
||||
// Done transmitting
|
||||
transmitter_model.disable();
|
||||
tx_mode = IDLE;
|
||||
text_status.set("Done");
|
||||
progressbar.set_value(0);
|
||||
tx_view.set_transmitting(false);
|
||||
}
|
||||
// Done transmitting
|
||||
transmitter_model.disable();
|
||||
tx_mode = IDLE;
|
||||
text_status.set("Done");
|
||||
progressbar.set_value(0);
|
||||
tx_view.set_transmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
void EncodersView::start_tx(const bool scan) {
|
||||
size_t bitstream_length = 0;
|
||||
int scan_width = 0;
|
||||
uint32_t samples_per_bit;
|
||||
size_t bitstream_length = 0;
|
||||
int scan_width = 0;
|
||||
uint32_t samples_per_bit;
|
||||
|
||||
if (scan) {
|
||||
tx_mode = SCAN;
|
||||
scan_width = view_scan.field_length.value();
|
||||
samples_per_bit =
|
||||
((view_scan.bit_length_10.value() * 10 + view_scan.bit_length.value()) * OOK_SAMPLERATE) / 1000000UL;
|
||||
constexpr auto sym_len = 4;
|
||||
const uint32_t seq_len = ((1 << (scan_width - 1)) * 2) * ((uint64_t) samples_per_bit) * sym_len / 2048UL;
|
||||
progressbar.set_max(seq_len);
|
||||
repeat_min = seq_len;
|
||||
} else {
|
||||
tx_mode = SINGLE;
|
||||
samples_per_bit = view_config.samples_per_bit();
|
||||
view_config.generate_frame();
|
||||
bitstream_length = make_bitstream(view_config.frame_fragments);
|
||||
progressbar.set_max(repeat_min);
|
||||
repeat_min = view_config.repeat_min();
|
||||
}
|
||||
if (scan) {
|
||||
tx_mode = SCAN;
|
||||
scan_width = view_scan.field_length.value();
|
||||
samples_per_bit =
|
||||
((view_scan.bit_length_10.value() * 10 + view_scan.bit_length.value()) * OOK_SAMPLERATE) / 1000000UL;
|
||||
constexpr auto sym_len = 4;
|
||||
const uint32_t seq_len = ((1 << (scan_width - 1)) * 2) * ((uint64_t)samples_per_bit) * sym_len / 2048UL;
|
||||
progressbar.set_max(seq_len);
|
||||
repeat_min = seq_len;
|
||||
} else {
|
||||
tx_mode = SINGLE;
|
||||
samples_per_bit = view_config.samples_per_bit();
|
||||
view_config.generate_frame();
|
||||
bitstream_length = make_bitstream(view_config.frame_fragments);
|
||||
progressbar.set_max(repeat_min);
|
||||
repeat_min = view_config.repeat_min();
|
||||
}
|
||||
|
||||
repeat_index = 1;
|
||||
update_progress();
|
||||
repeat_index = 1;
|
||||
update_progress();
|
||||
|
||||
transmitter_model.set_sampling_rate(OOK_SAMPLERATE);
|
||||
transmitter_model.set_baseband_bandwidth(1750000);
|
||||
transmitter_model.enable();
|
||||
/* Setting TX LPF 1M75 in this TX OOK App , We got same results as fw 1.7.4
|
||||
* Looking max BW of this app, we tested , Selecting OOK type 145026 with CLK 455K and max DEV. 150k,
|
||||
* and we got BW +-2Mhz , with that TX LPF 1M75, it is fine.*/
|
||||
transmitter_model.enable();
|
||||
|
||||
baseband::set_ook_data(
|
||||
bitstream_length,
|
||||
samples_per_bit,
|
||||
repeat_min,
|
||||
view_config.pause_symbols(),
|
||||
scan_width
|
||||
);
|
||||
baseband::set_ook_data(
|
||||
bitstream_length,
|
||||
samples_per_bit,
|
||||
repeat_min,
|
||||
view_config.pause_symbols(),
|
||||
scan_width);
|
||||
}
|
||||
|
||||
EncodersView::EncodersView(
|
||||
NavigationView& nav
|
||||
) : nav_ { nav }
|
||||
{
|
||||
baseband::run_image(portapack::spi_flash::image_tag_ook);
|
||||
NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_ook);
|
||||
|
||||
add_children({
|
||||
&tab_view,
|
||||
&view_config,
|
||||
&view_scan,
|
||||
&text_status,
|
||||
&progressbar,
|
||||
&tx_view
|
||||
});
|
||||
add_children({&tab_view,
|
||||
&view_config,
|
||||
&view_scan,
|
||||
&text_status,
|
||||
&progressbar,
|
||||
&tx_view});
|
||||
|
||||
// load app settings
|
||||
auto rc = settings.load("tx_ook", &app_settings);
|
||||
if (rc == SETTINGS_OK) {
|
||||
transmitter_model.set_rf_amp(app_settings.tx_amp);
|
||||
transmitter_model.set_channel_bandwidth(app_settings.channel_bandwidth);
|
||||
transmitter_model.set_tuning_frequency(app_settings.tx_frequency);
|
||||
transmitter_model.set_tx_gain(app_settings.tx_gain);
|
||||
}
|
||||
tx_view.on_edit_frequency = [this, &nav]() {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(transmitter_model.target_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
transmitter_model.set_target_frequency(f);
|
||||
};
|
||||
};
|
||||
|
||||
tx_view.on_edit_frequency = [this, &nav]() {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(transmitter_model.tuning_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
transmitter_model.set_tuning_frequency(f);
|
||||
};
|
||||
};
|
||||
tx_view.on_start = [this]() {
|
||||
tx_view.set_transmitting(true);
|
||||
start_tx(tab_view.selected());
|
||||
};
|
||||
|
||||
tx_view.on_start = [this]() {
|
||||
tx_view.set_transmitting(true);
|
||||
start_tx(tab_view.selected());
|
||||
};
|
||||
|
||||
tx_view.on_stop = [this]() {
|
||||
tx_view.set_transmitting(false);
|
||||
baseband::kill_ook();
|
||||
transmitter_model.disable();
|
||||
};
|
||||
tx_view.on_stop = [this]() {
|
||||
tx_view.set_transmitting(false);
|
||||
baseband::kill_ook();
|
||||
transmitter_model.disable();
|
||||
};
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -27,236 +27,207 @@
|
||||
#include "encoders.hpp"
|
||||
#include "de_bruijn.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
using namespace encoders;
|
||||
|
||||
namespace ui {
|
||||
|
||||
class EncodersConfigView : public View {
|
||||
public:
|
||||
EncodersConfigView(NavigationView& nav, Rect parent_rect);
|
||||
public:
|
||||
EncodersConfigView(NavigationView& nav, Rect parent_rect);
|
||||
|
||||
EncodersConfigView(const EncodersConfigView&) = delete;
|
||||
EncodersConfigView(EncodersConfigView&&) = delete;
|
||||
EncodersConfigView& operator=(const EncodersConfigView&) = delete;
|
||||
EncodersConfigView& operator=(EncodersConfigView&&) = delete;
|
||||
EncodersConfigView(const EncodersConfigView&) = delete;
|
||||
EncodersConfigView(EncodersConfigView&&) = delete;
|
||||
EncodersConfigView& operator=(const EncodersConfigView&) = delete;
|
||||
EncodersConfigView& operator=(EncodersConfigView&&) = delete;
|
||||
|
||||
void focus() override;
|
||||
void on_show() override;
|
||||
void focus() override;
|
||||
void on_show() override;
|
||||
|
||||
uint8_t repeat_min();
|
||||
uint32_t samples_per_bit();
|
||||
uint32_t pause_symbols();
|
||||
void generate_frame();
|
||||
uint8_t repeat_min();
|
||||
uint32_t samples_per_bit();
|
||||
uint32_t pause_symbols();
|
||||
void generate_frame();
|
||||
|
||||
std::string frame_fragments = "0";
|
||||
std::string frame_fragments = "0";
|
||||
|
||||
private:
|
||||
int16_t waveform_buffer[550];
|
||||
const encoder_def_t * encoder_def { };
|
||||
private:
|
||||
int16_t waveform_buffer[550];
|
||||
const encoder_def_t* encoder_def{};
|
||||
|
||||
void draw_waveform();
|
||||
void on_bitfield();
|
||||
void on_type_change(size_t index);
|
||||
void draw_waveform();
|
||||
void on_bitfield();
|
||||
void on_type_change(size_t index);
|
||||
|
||||
Labels labels {
|
||||
{ { 1 * 8, 0 }, "Type:", Color::light_grey() },
|
||||
{ { 17 * 8, 0 }, "Repeat:", Color::light_grey() },
|
||||
{ { 1 * 8, 2 * 8 }, "Clk:", Color::light_grey() },
|
||||
{ { 10 * 8, 2 * 8 }, "kHz", Color::light_grey() },
|
||||
{ { 17 * 8, 2 * 8 }, "Step:", Color::light_grey() },
|
||||
{ { 1 * 8, 4 * 8 }, "Frame:", Color::light_grey() },
|
||||
{ { 13 * 8, 4 * 8 }, "us", Color::light_grey() },
|
||||
{ { 17 * 8, 4 * 8 }, "Step:", Color::light_grey() },
|
||||
{ { 2 * 8, 7 * 8 }, "Symbols:", Color::light_grey() },
|
||||
{ { 1 * 8, 14 * 8 }, "Waveform:", Color::light_grey() }
|
||||
};
|
||||
Labels labels{
|
||||
{{1 * 8, 0}, "Type:", Theme::getInstance()->fg_light->foreground},
|
||||
{{17 * 8, 0}, "Repeat:", Theme::getInstance()->fg_light->foreground},
|
||||
{{1 * 8, 2 * 8}, "Clk:", Theme::getInstance()->fg_light->foreground},
|
||||
{{10 * 8, 2 * 8}, "kHz", Theme::getInstance()->fg_light->foreground},
|
||||
{{17 * 8, 2 * 8}, "Step:", Theme::getInstance()->fg_light->foreground},
|
||||
{{1 * 8, 4 * 8}, "Frame:", Theme::getInstance()->fg_light->foreground},
|
||||
{{13 * 8, 4 * 8}, "us", Theme::getInstance()->fg_light->foreground},
|
||||
{{17 * 8, 4 * 8}, "Step:", Theme::getInstance()->fg_light->foreground},
|
||||
{{2 * 8, 7 * 8}, "Symbols:", Theme::getInstance()->fg_light->foreground},
|
||||
{{1 * 8, 14 * 8}, "Waveform:", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
OptionsField options_enctype { // Options are loaded at runtime
|
||||
{ 6 * 8, 0 },
|
||||
7,
|
||||
{
|
||||
}
|
||||
};
|
||||
OptionsField options_enctype{// Options are loaded at runtime
|
||||
{6 * 8, 0},
|
||||
7,
|
||||
{}};
|
||||
|
||||
NumberField field_clk {
|
||||
{ 5 * 8, 2 * 8 },
|
||||
4,
|
||||
{ 1, 1000 },
|
||||
1,
|
||||
' '
|
||||
};
|
||||
NumberField field_clk{
|
||||
{5 * 8, 2 * 8},
|
||||
4,
|
||||
{1, 1000},
|
||||
1,
|
||||
' '};
|
||||
|
||||
NumberField field_repeat_min {
|
||||
{ 24 * 8, 0 },
|
||||
2,
|
||||
{ 1, 99 },
|
||||
1,
|
||||
' '
|
||||
};
|
||||
NumberField field_repeat_min{
|
||||
{24 * 8, 0},
|
||||
2,
|
||||
{1, 99},
|
||||
1,
|
||||
' '};
|
||||
|
||||
OptionsField field_clk_step {
|
||||
{ 22 * 8, 2 * 8 },
|
||||
7,
|
||||
{
|
||||
{ "1", 1 },
|
||||
{ "10", 10 },
|
||||
{ "100", 100 }
|
||||
}
|
||||
};
|
||||
OptionsField field_clk_step{
|
||||
{22 * 8, 2 * 8},
|
||||
7,
|
||||
{{"1", 1},
|
||||
{"10", 10},
|
||||
{"100", 100}}};
|
||||
|
||||
NumberField field_frameduration {
|
||||
{ 7 * 8, 4 * 8 },
|
||||
5,
|
||||
{ 300, 99999 },
|
||||
100,
|
||||
' '
|
||||
};
|
||||
NumberField field_frameduration{
|
||||
{7 * 8, 4 * 8},
|
||||
5,
|
||||
{300, 99999},
|
||||
100,
|
||||
' '};
|
||||
|
||||
OptionsField field_frameduration_step {
|
||||
{ 22 * 8, 4 * 8 },
|
||||
7,
|
||||
{
|
||||
{ "1", 1 },
|
||||
{ "10", 10 },
|
||||
{ "100", 100 },
|
||||
{ "1000", 1000 }
|
||||
}
|
||||
};
|
||||
OptionsField field_frameduration_step{
|
||||
{22 * 8, 4 * 8},
|
||||
7,
|
||||
{{"1", 1},
|
||||
{"10", 10},
|
||||
{"100", 100},
|
||||
{"1000", 1000}}};
|
||||
|
||||
SymField symfield_word {
|
||||
{ 2 * 8, 9 * 8 },
|
||||
20,
|
||||
SymField::SYMFIELD_DEF
|
||||
};
|
||||
std::vector<std::unique_ptr<SymField>> symfields_word{};
|
||||
|
||||
Text text_format {
|
||||
{ 2 * 8, 11 * 8, 24 * 8, 16 },
|
||||
""
|
||||
};
|
||||
Text text_format{
|
||||
{2 * 8, 11 * 8, 24 * 8, 16},
|
||||
""};
|
||||
|
||||
Waveform waveform {
|
||||
{ 0, 17 * 8, 240, 32 },
|
||||
waveform_buffer,
|
||||
0,
|
||||
0,
|
||||
true,
|
||||
Color::yellow()
|
||||
};
|
||||
Waveform waveform{
|
||||
{0, 17 * 8, 240, 32},
|
||||
waveform_buffer,
|
||||
0,
|
||||
0,
|
||||
true,
|
||||
Theme::getInstance()->fg_yellow->foreground};
|
||||
};
|
||||
|
||||
|
||||
class EncodersScanView : public View {
|
||||
public:
|
||||
EncodersScanView(NavigationView& nav, Rect parent_rect);
|
||||
public:
|
||||
EncodersScanView(NavigationView& nav, Rect parent_rect);
|
||||
|
||||
NumberField field_length {
|
||||
{ 8 * 8, 0 },
|
||||
2,
|
||||
{ 3, 24 },
|
||||
1,
|
||||
' '
|
||||
};
|
||||
NumberField field_length{
|
||||
{8 * 8, 0},
|
||||
2,
|
||||
{3, 24},
|
||||
1,
|
||||
' '};
|
||||
|
||||
NumberField bit_length_10 {
|
||||
{ 12 * 8, 2 * 8 },
|
||||
2,
|
||||
{ 1, 88 },
|
||||
1,
|
||||
' '
|
||||
};
|
||||
NumberField bit_length_10{
|
||||
{12 * 8, 2 * 8},
|
||||
2,
|
||||
{1, 88},
|
||||
1,
|
||||
' '};
|
||||
|
||||
NumberField bit_length {
|
||||
{ 14 * 8, 2 * 8 },
|
||||
1,
|
||||
{ 0, 9 },
|
||||
1,
|
||||
' '
|
||||
};
|
||||
NumberField bit_length{
|
||||
{14 * 8, 2 * 8},
|
||||
1,
|
||||
{0, 9},
|
||||
1,
|
||||
' '};
|
||||
|
||||
void focus() override;
|
||||
void focus() override;
|
||||
|
||||
private:
|
||||
Labels labels {
|
||||
{ { 1 * 8, 0 * 8 }, "Length:", Color::light_grey() },
|
||||
{ { 1 * 8, 2 * 8 }, "Bit length:", Color::light_grey() },
|
||||
{ { 16 * 8, 2 * 8 }, "us", Color::light_grey() },
|
||||
};
|
||||
private:
|
||||
Labels labels{
|
||||
{{1 * 8, 0 * 8}, "Length:", Theme::getInstance()->fg_light->foreground},
|
||||
{{1 * 8, 2 * 8}, "Bit length:", Theme::getInstance()->fg_light->foreground},
|
||||
{{16 * 8, 2 * 8}, "us", Theme::getInstance()->fg_light->foreground},
|
||||
};
|
||||
};
|
||||
|
||||
class EncodersView : public View {
|
||||
public:
|
||||
EncodersView(NavigationView& nav);
|
||||
~EncodersView();
|
||||
public:
|
||||
EncodersView(NavigationView& nav);
|
||||
~EncodersView();
|
||||
|
||||
void focus() override;
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "OOK TX"; };
|
||||
std::string title() const override { return "OOK TX"; };
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
|
||||
enum tx_modes {
|
||||
IDLE = 0,
|
||||
SINGLE,
|
||||
SCAN
|
||||
};
|
||||
enum tx_modes {
|
||||
IDLE = 0,
|
||||
SINGLE,
|
||||
SCAN
|
||||
};
|
||||
|
||||
// app save settings
|
||||
std::app_settings settings { };
|
||||
std::app_settings::AppSettings app_settings { };
|
||||
TxRadioState radio_state_{
|
||||
433920000 /* frequency */,
|
||||
1750000 /* bandwidth */,
|
||||
OOK_SAMPLERATE /* sampling rate */
|
||||
};
|
||||
app_settings::SettingsManager settings_{
|
||||
"tx_ook", app_settings::Mode::TX};
|
||||
|
||||
tx_modes tx_mode = IDLE;
|
||||
uint32_t repeat_index { 0 };
|
||||
uint32_t repeat_min { 0 };
|
||||
tx_modes tx_mode = IDLE;
|
||||
uint32_t repeat_index{0};
|
||||
uint32_t repeat_min{0};
|
||||
|
||||
void update_progress();
|
||||
void start_tx(const bool scan);
|
||||
void on_tx_progress(const uint32_t progress, const bool done);
|
||||
void update_progress();
|
||||
void start_tx(const bool scan);
|
||||
void on_tx_progress(const uint32_t progress, const bool done);
|
||||
|
||||
/*const Style style_address {
|
||||
.font = font::fixed_8x16,
|
||||
.background = Color::black(),
|
||||
.foreground = Color::red(),
|
||||
};
|
||||
const Style style_data {
|
||||
.font = font::fixed_8x16,
|
||||
.background = Color::black(),
|
||||
.foreground = Color::blue(),
|
||||
};*/
|
||||
Rect view_rect = {0, 4 * 8, 240, 168};
|
||||
|
||||
Rect view_rect = { 0, 4 * 8, 240, 168 };
|
||||
EncodersConfigView view_config{nav_, view_rect};
|
||||
EncodersScanView view_scan{nav_, view_rect};
|
||||
|
||||
EncodersConfigView view_config { nav_, view_rect };
|
||||
EncodersScanView view_scan { nav_, view_rect };
|
||||
TabView tab_view{
|
||||
{"Config", Theme::getInstance()->fg_cyan->foreground, &view_config},
|
||||
{"de Bruijn", Theme::getInstance()->fg_green->foreground, &view_scan},
|
||||
};
|
||||
|
||||
TabView tab_view {
|
||||
{ "Config", Color::cyan(), &view_config },
|
||||
{ "de Bruijn", Color::green(), &view_scan },
|
||||
};
|
||||
Text text_status{
|
||||
{2 * 8, 13 * 16, 128, 16},
|
||||
"Ready"};
|
||||
|
||||
Text text_status {
|
||||
{ 2 * 8, 13 * 16, 128, 16 },
|
||||
"Ready"
|
||||
};
|
||||
ProgressBar progressbar{
|
||||
{2 * 8, 13 * 16 + 20, 208, 16}};
|
||||
|
||||
ProgressBar progressbar {
|
||||
{ 2 * 8, 13 * 16 + 20, 208, 16 }
|
||||
};
|
||||
TransmitterView tx_view{
|
||||
16 * 16,
|
||||
50000,
|
||||
9};
|
||||
|
||||
TransmitterView tx_view {
|
||||
16 * 16,
|
||||
50000,
|
||||
9
|
||||
};
|
||||
|
||||
MessageHandlerRegistration message_handler_tx_progress {
|
||||
Message::ID::TXProgress,
|
||||
[this](const Message* const p) {
|
||||
const auto message = *reinterpret_cast<const TXProgressMessage*>(p);
|
||||
this->on_tx_progress(message.progress, message.done);
|
||||
}
|
||||
};
|
||||
MessageHandlerRegistration message_handler_tx_progress{
|
||||
Message::ID::TXProgress,
|
||||
[this](const Message* const p) {
|
||||
const auto message = *reinterpret_cast<const TXProgressMessage*>(p);
|
||||
this->on_tx_progress(message.progress, message.done);
|
||||
}};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user