diff --git a/linux-rust/assets/animations/popup/frame_001.png b/linux-rust/assets/animations/popup/frame_001.png new file mode 100644 index 000000000..f3d361b57 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_001.png differ diff --git a/linux-rust/assets/animations/popup/frame_002.png b/linux-rust/assets/animations/popup/frame_002.png new file mode 100644 index 000000000..f3d361b57 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_002.png differ diff --git a/linux-rust/assets/animations/popup/frame_003.png b/linux-rust/assets/animations/popup/frame_003.png new file mode 100644 index 000000000..6a72770e4 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_003.png differ diff --git a/linux-rust/assets/animations/popup/frame_004.png b/linux-rust/assets/animations/popup/frame_004.png new file mode 100644 index 000000000..8c88c0e27 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_004.png differ diff --git a/linux-rust/assets/animations/popup/frame_005.png b/linux-rust/assets/animations/popup/frame_005.png new file mode 100644 index 000000000..af62d7600 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_005.png differ diff --git a/linux-rust/assets/animations/popup/frame_006.png b/linux-rust/assets/animations/popup/frame_006.png new file mode 100644 index 000000000..24a7d08d8 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_006.png differ diff --git a/linux-rust/assets/animations/popup/frame_007.png b/linux-rust/assets/animations/popup/frame_007.png new file mode 100644 index 000000000..45ff1d8f2 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_007.png differ diff --git a/linux-rust/assets/animations/popup/frame_008.png b/linux-rust/assets/animations/popup/frame_008.png new file mode 100644 index 000000000..2e786975c Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_008.png differ diff --git a/linux-rust/assets/animations/popup/frame_009.png b/linux-rust/assets/animations/popup/frame_009.png new file mode 100644 index 000000000..0b204f7b4 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_009.png differ diff --git a/linux-rust/assets/animations/popup/frame_010.png b/linux-rust/assets/animations/popup/frame_010.png new file mode 100644 index 000000000..a237c5775 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_010.png differ diff --git a/linux-rust/assets/animations/popup/frame_011.png b/linux-rust/assets/animations/popup/frame_011.png new file mode 100644 index 000000000..0611459f4 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_011.png differ diff --git a/linux-rust/assets/animations/popup/frame_012.png b/linux-rust/assets/animations/popup/frame_012.png new file mode 100644 index 000000000..628ab78df Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_012.png differ diff --git a/linux-rust/assets/animations/popup/frame_013.png b/linux-rust/assets/animations/popup/frame_013.png new file mode 100644 index 000000000..5b06df01c Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_013.png differ diff --git a/linux-rust/assets/animations/popup/frame_014.png b/linux-rust/assets/animations/popup/frame_014.png new file mode 100644 index 000000000..11ce3225e Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_014.png differ diff --git a/linux-rust/assets/animations/popup/frame_015.png b/linux-rust/assets/animations/popup/frame_015.png new file mode 100644 index 000000000..520f9ef5c Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_015.png differ diff --git a/linux-rust/assets/animations/popup/frame_016.png b/linux-rust/assets/animations/popup/frame_016.png new file mode 100644 index 000000000..443f8f69e Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_016.png differ diff --git a/linux-rust/assets/animations/popup/frame_017.png b/linux-rust/assets/animations/popup/frame_017.png new file mode 100644 index 000000000..7171b9b4c Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_017.png differ diff --git a/linux-rust/assets/animations/popup/frame_018.png b/linux-rust/assets/animations/popup/frame_018.png new file mode 100644 index 000000000..a9e8029eb Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_018.png differ diff --git a/linux-rust/assets/animations/popup/frame_019.png b/linux-rust/assets/animations/popup/frame_019.png new file mode 100644 index 000000000..9bbad8446 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_019.png differ diff --git a/linux-rust/assets/animations/popup/frame_020.png b/linux-rust/assets/animations/popup/frame_020.png new file mode 100644 index 000000000..1948b6e02 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_020.png differ diff --git a/linux-rust/assets/animations/popup/frame_021.png b/linux-rust/assets/animations/popup/frame_021.png new file mode 100644 index 000000000..d82d45c20 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_021.png differ diff --git a/linux-rust/assets/animations/popup/frame_022.png b/linux-rust/assets/animations/popup/frame_022.png new file mode 100644 index 000000000..0ec0ef3ea Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_022.png differ diff --git a/linux-rust/assets/animations/popup/frame_023.png b/linux-rust/assets/animations/popup/frame_023.png new file mode 100644 index 000000000..7773cc8c8 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_023.png differ diff --git a/linux-rust/assets/animations/popup/frame_024.png b/linux-rust/assets/animations/popup/frame_024.png new file mode 100644 index 000000000..cbb6c2c30 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_024.png differ diff --git a/linux-rust/assets/animations/popup/frame_025.png b/linux-rust/assets/animations/popup/frame_025.png new file mode 100644 index 000000000..32a4256e9 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_025.png differ diff --git a/linux-rust/assets/animations/popup/frame_026.png b/linux-rust/assets/animations/popup/frame_026.png new file mode 100644 index 000000000..addbb527f Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_026.png differ diff --git a/linux-rust/assets/animations/popup/frame_027.png b/linux-rust/assets/animations/popup/frame_027.png new file mode 100644 index 000000000..2f315c6b9 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_027.png differ diff --git a/linux-rust/assets/animations/popup/frame_028.png b/linux-rust/assets/animations/popup/frame_028.png new file mode 100644 index 000000000..3d8872f1e Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_028.png differ diff --git a/linux-rust/assets/animations/popup/frame_029.png b/linux-rust/assets/animations/popup/frame_029.png new file mode 100644 index 000000000..3694f4350 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_029.png differ diff --git a/linux-rust/assets/animations/popup/frame_030.png b/linux-rust/assets/animations/popup/frame_030.png new file mode 100644 index 000000000..f75d9acdb Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_030.png differ diff --git a/linux-rust/assets/animations/popup/frame_031.png b/linux-rust/assets/animations/popup/frame_031.png new file mode 100644 index 000000000..28c7df5e9 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_031.png differ diff --git a/linux-rust/assets/animations/popup/frame_032.png b/linux-rust/assets/animations/popup/frame_032.png new file mode 100644 index 000000000..5fb520138 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_032.png differ diff --git a/linux-rust/assets/animations/popup/frame_033.png b/linux-rust/assets/animations/popup/frame_033.png new file mode 100644 index 000000000..aa26e2f52 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_033.png differ diff --git a/linux-rust/assets/animations/popup/frame_034.png b/linux-rust/assets/animations/popup/frame_034.png new file mode 100644 index 000000000..0fb6420ff Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_034.png differ diff --git a/linux-rust/assets/animations/popup/frame_035.png b/linux-rust/assets/animations/popup/frame_035.png new file mode 100644 index 000000000..38651983c Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_035.png differ diff --git a/linux-rust/assets/animations/popup/frame_036.png b/linux-rust/assets/animations/popup/frame_036.png new file mode 100644 index 000000000..eef7c4439 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_036.png differ diff --git a/linux-rust/assets/animations/popup/frame_037.png b/linux-rust/assets/animations/popup/frame_037.png new file mode 100644 index 000000000..d84bd799c Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_037.png differ diff --git a/linux-rust/assets/animations/popup/frame_038.png b/linux-rust/assets/animations/popup/frame_038.png new file mode 100644 index 000000000..f98262baa Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_038.png differ diff --git a/linux-rust/assets/animations/popup/frame_039.png b/linux-rust/assets/animations/popup/frame_039.png new file mode 100644 index 000000000..0b0ecc9e2 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_039.png differ diff --git a/linux-rust/assets/animations/popup/frame_040.png b/linux-rust/assets/animations/popup/frame_040.png new file mode 100644 index 000000000..f10a71f30 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_040.png differ diff --git a/linux-rust/assets/animations/popup/frame_041.png b/linux-rust/assets/animations/popup/frame_041.png new file mode 100644 index 000000000..ef1ede7f2 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_041.png differ diff --git a/linux-rust/assets/animations/popup/frame_042.png b/linux-rust/assets/animations/popup/frame_042.png new file mode 100644 index 000000000..a78aa3e78 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_042.png differ diff --git a/linux-rust/assets/animations/popup/frame_043.png b/linux-rust/assets/animations/popup/frame_043.png new file mode 100644 index 000000000..f4959a6ef Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_043.png differ diff --git a/linux-rust/assets/animations/popup/frame_044.png b/linux-rust/assets/animations/popup/frame_044.png new file mode 100644 index 000000000..cf3e7d7e1 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_044.png differ diff --git a/linux-rust/assets/animations/popup/frame_045.png b/linux-rust/assets/animations/popup/frame_045.png new file mode 100644 index 000000000..4d30193cc Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_045.png differ diff --git a/linux-rust/assets/animations/popup/frame_046.png b/linux-rust/assets/animations/popup/frame_046.png new file mode 100644 index 000000000..1a2a1f9e2 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_046.png differ diff --git a/linux-rust/assets/animations/popup/frame_047.png b/linux-rust/assets/animations/popup/frame_047.png new file mode 100644 index 000000000..a2d26609a Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_047.png differ diff --git a/linux-rust/assets/animations/popup/frame_048.png b/linux-rust/assets/animations/popup/frame_048.png new file mode 100644 index 000000000..e1ae9250c Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_048.png differ diff --git a/linux-rust/assets/animations/popup/frame_049.png b/linux-rust/assets/animations/popup/frame_049.png new file mode 100644 index 000000000..fb493c27f Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_049.png differ diff --git a/linux-rust/assets/animations/popup/frame_050.png b/linux-rust/assets/animations/popup/frame_050.png new file mode 100644 index 000000000..ce7fc50eb Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_050.png differ diff --git a/linux-rust/assets/animations/popup/frame_051.png b/linux-rust/assets/animations/popup/frame_051.png new file mode 100644 index 000000000..5afd2f38a Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_051.png differ diff --git a/linux-rust/assets/animations/popup/frame_052.png b/linux-rust/assets/animations/popup/frame_052.png new file mode 100644 index 000000000..b3c919f67 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_052.png differ diff --git a/linux-rust/assets/animations/popup/frame_053.png b/linux-rust/assets/animations/popup/frame_053.png new file mode 100644 index 000000000..ce2ad239f Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_053.png differ diff --git a/linux-rust/assets/animations/popup/frame_054.png b/linux-rust/assets/animations/popup/frame_054.png new file mode 100644 index 000000000..d8eea10de Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_054.png differ diff --git a/linux-rust/assets/animations/popup/frame_055.png b/linux-rust/assets/animations/popup/frame_055.png new file mode 100644 index 000000000..ea5c8f045 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_055.png differ diff --git a/linux-rust/assets/animations/popup/frame_056.png b/linux-rust/assets/animations/popup/frame_056.png new file mode 100644 index 000000000..ca5069699 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_056.png differ diff --git a/linux-rust/assets/animations/popup/frame_057.png b/linux-rust/assets/animations/popup/frame_057.png new file mode 100644 index 000000000..26b00c752 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_057.png differ diff --git a/linux-rust/assets/animations/popup/frame_058.png b/linux-rust/assets/animations/popup/frame_058.png new file mode 100644 index 000000000..5300084d8 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_058.png differ diff --git a/linux-rust/assets/animations/popup/frame_059.png b/linux-rust/assets/animations/popup/frame_059.png new file mode 100644 index 000000000..01093d3e1 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_059.png differ diff --git a/linux-rust/assets/animations/popup/frame_060.png b/linux-rust/assets/animations/popup/frame_060.png new file mode 100644 index 000000000..50bddd5aa Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_060.png differ diff --git a/linux-rust/assets/animations/popup/frame_061.png b/linux-rust/assets/animations/popup/frame_061.png new file mode 100644 index 000000000..afabad750 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_061.png differ diff --git a/linux-rust/assets/animations/popup/frame_062.png b/linux-rust/assets/animations/popup/frame_062.png new file mode 100644 index 000000000..76e26d054 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_062.png differ diff --git a/linux-rust/assets/animations/popup/frame_063.png b/linux-rust/assets/animations/popup/frame_063.png new file mode 100644 index 000000000..617df28b5 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_063.png differ diff --git a/linux-rust/assets/animations/popup/frame_064.png b/linux-rust/assets/animations/popup/frame_064.png new file mode 100644 index 000000000..2b2093671 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_064.png differ diff --git a/linux-rust/assets/animations/popup/frame_065.png b/linux-rust/assets/animations/popup/frame_065.png new file mode 100644 index 000000000..9bf717fae Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_065.png differ diff --git a/linux-rust/assets/animations/popup/frame_066.png b/linux-rust/assets/animations/popup/frame_066.png new file mode 100644 index 000000000..0f769cd34 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_066.png differ diff --git a/linux-rust/assets/animations/popup/frame_067.png b/linux-rust/assets/animations/popup/frame_067.png new file mode 100644 index 000000000..f81657eb1 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_067.png differ diff --git a/linux-rust/assets/animations/popup/frame_068.png b/linux-rust/assets/animations/popup/frame_068.png new file mode 100644 index 000000000..fc8a21880 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_068.png differ diff --git a/linux-rust/assets/animations/popup/frame_069.png b/linux-rust/assets/animations/popup/frame_069.png new file mode 100644 index 000000000..fb8183955 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_069.png differ diff --git a/linux-rust/assets/animations/popup/frame_070.png b/linux-rust/assets/animations/popup/frame_070.png new file mode 100644 index 000000000..60e88517e Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_070.png differ diff --git a/linux-rust/assets/animations/popup/frame_071.png b/linux-rust/assets/animations/popup/frame_071.png new file mode 100644 index 000000000..9b8307c1b Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_071.png differ diff --git a/linux-rust/assets/animations/popup/frame_072.png b/linux-rust/assets/animations/popup/frame_072.png new file mode 100644 index 000000000..bb915652e Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_072.png differ diff --git a/linux-rust/assets/animations/popup/frame_073.png b/linux-rust/assets/animations/popup/frame_073.png new file mode 100644 index 000000000..663c5ef6b Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_073.png differ diff --git a/linux-rust/assets/animations/popup/frame_074.png b/linux-rust/assets/animations/popup/frame_074.png new file mode 100644 index 000000000..1bad61960 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_074.png differ diff --git a/linux-rust/assets/animations/popup/frame_075.png b/linux-rust/assets/animations/popup/frame_075.png new file mode 100644 index 000000000..4f695cb41 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_075.png differ diff --git a/linux-rust/assets/animations/popup/frame_076.png b/linux-rust/assets/animations/popup/frame_076.png new file mode 100644 index 000000000..c760c0a4b Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_076.png differ diff --git a/linux-rust/assets/animations/popup/frame_077.png b/linux-rust/assets/animations/popup/frame_077.png new file mode 100644 index 000000000..266220ae1 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_077.png differ diff --git a/linux-rust/assets/animations/popup/frame_078.png b/linux-rust/assets/animations/popup/frame_078.png new file mode 100644 index 000000000..71e63ed6d Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_078.png differ diff --git a/linux-rust/assets/animations/popup/frame_079.png b/linux-rust/assets/animations/popup/frame_079.png new file mode 100644 index 000000000..3c8b070be Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_079.png differ diff --git a/linux-rust/assets/animations/popup/frame_080.png b/linux-rust/assets/animations/popup/frame_080.png new file mode 100644 index 000000000..4c7848781 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_080.png differ diff --git a/linux-rust/assets/animations/popup/frame_081.png b/linux-rust/assets/animations/popup/frame_081.png new file mode 100644 index 000000000..abd2cd56a Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_081.png differ diff --git a/linux-rust/assets/animations/popup/frame_082.png b/linux-rust/assets/animations/popup/frame_082.png new file mode 100644 index 000000000..b0d518992 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_082.png differ diff --git a/linux-rust/assets/animations/popup/frame_083.png b/linux-rust/assets/animations/popup/frame_083.png new file mode 100644 index 000000000..d375b15d5 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_083.png differ diff --git a/linux-rust/assets/animations/popup/frame_084.png b/linux-rust/assets/animations/popup/frame_084.png new file mode 100644 index 000000000..8a9c8781b Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_084.png differ diff --git a/linux-rust/assets/animations/popup/frame_085.png b/linux-rust/assets/animations/popup/frame_085.png new file mode 100644 index 000000000..aec9757dc Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_085.png differ diff --git a/linux-rust/assets/animations/popup/frame_086.png b/linux-rust/assets/animations/popup/frame_086.png new file mode 100644 index 000000000..cdcb62e19 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_086.png differ diff --git a/linux-rust/assets/animations/popup/frame_087.png b/linux-rust/assets/animations/popup/frame_087.png new file mode 100644 index 000000000..f1d7495fa Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_087.png differ diff --git a/linux-rust/assets/animations/popup/frame_088.png b/linux-rust/assets/animations/popup/frame_088.png new file mode 100644 index 000000000..3bae205d4 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_088.png differ diff --git a/linux-rust/assets/animations/popup/frame_089.png b/linux-rust/assets/animations/popup/frame_089.png new file mode 100644 index 000000000..c85581542 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_089.png differ diff --git a/linux-rust/assets/animations/popup/frame_090.png b/linux-rust/assets/animations/popup/frame_090.png new file mode 100644 index 000000000..9a19a540c Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_090.png differ diff --git a/linux-rust/assets/animations/popup/frame_091.png b/linux-rust/assets/animations/popup/frame_091.png new file mode 100644 index 000000000..a789c358c Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_091.png differ diff --git a/linux-rust/assets/animations/popup/frame_092.png b/linux-rust/assets/animations/popup/frame_092.png new file mode 100644 index 000000000..a50d2ff19 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_092.png differ diff --git a/linux-rust/assets/animations/popup/frame_093.png b/linux-rust/assets/animations/popup/frame_093.png new file mode 100644 index 000000000..61b69712d Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_093.png differ diff --git a/linux-rust/assets/animations/popup/frame_094.png b/linux-rust/assets/animations/popup/frame_094.png new file mode 100644 index 000000000..bca2344f6 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_094.png differ diff --git a/linux-rust/assets/animations/popup/frame_095.png b/linux-rust/assets/animations/popup/frame_095.png new file mode 100644 index 000000000..75b4cfa1f Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_095.png differ diff --git a/linux-rust/assets/animations/popup/frame_096.png b/linux-rust/assets/animations/popup/frame_096.png new file mode 100644 index 000000000..3703d2c09 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_096.png differ diff --git a/linux-rust/assets/animations/popup/frame_097.png b/linux-rust/assets/animations/popup/frame_097.png new file mode 100644 index 000000000..99052351d Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_097.png differ diff --git a/linux-rust/assets/animations/popup/frame_098.png b/linux-rust/assets/animations/popup/frame_098.png new file mode 100644 index 000000000..4d56bb780 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_098.png differ diff --git a/linux-rust/assets/animations/popup/frame_099.png b/linux-rust/assets/animations/popup/frame_099.png new file mode 100644 index 000000000..bc3ea5901 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_099.png differ diff --git a/linux-rust/assets/animations/popup/frame_100.png b/linux-rust/assets/animations/popup/frame_100.png new file mode 100644 index 000000000..60b6f36c5 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_100.png differ diff --git a/linux-rust/assets/animations/popup/frame_101.png b/linux-rust/assets/animations/popup/frame_101.png new file mode 100644 index 000000000..8cf4f0d3c Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_101.png differ diff --git a/linux-rust/assets/animations/popup/frame_102.png b/linux-rust/assets/animations/popup/frame_102.png new file mode 100644 index 000000000..a577b7c9b Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_102.png differ diff --git a/linux-rust/assets/animations/popup/frame_103.png b/linux-rust/assets/animations/popup/frame_103.png new file mode 100644 index 000000000..8f84eafe9 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_103.png differ diff --git a/linux-rust/assets/animations/popup/frame_104.png b/linux-rust/assets/animations/popup/frame_104.png new file mode 100644 index 000000000..00a4090c2 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_104.png differ diff --git a/linux-rust/assets/animations/popup/frame_105.png b/linux-rust/assets/animations/popup/frame_105.png new file mode 100644 index 000000000..62659a3a3 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_105.png differ diff --git a/linux-rust/assets/animations/popup/frame_106.png b/linux-rust/assets/animations/popup/frame_106.png new file mode 100644 index 000000000..dd76b41cc Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_106.png differ diff --git a/linux-rust/assets/animations/popup/frame_107.png b/linux-rust/assets/animations/popup/frame_107.png new file mode 100644 index 000000000..cef3eea05 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_107.png differ diff --git a/linux-rust/assets/animations/popup/frame_108.png b/linux-rust/assets/animations/popup/frame_108.png new file mode 100644 index 000000000..c28a95d9a Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_108.png differ diff --git a/linux-rust/assets/animations/popup/frame_109.png b/linux-rust/assets/animations/popup/frame_109.png new file mode 100644 index 000000000..41f017d9a Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_109.png differ diff --git a/linux-rust/assets/animations/popup/frame_110.png b/linux-rust/assets/animations/popup/frame_110.png new file mode 100644 index 000000000..f36ffaa7d Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_110.png differ diff --git a/linux-rust/assets/animations/popup/frame_111.png b/linux-rust/assets/animations/popup/frame_111.png new file mode 100644 index 000000000..54d5e975a Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_111.png differ diff --git a/linux-rust/assets/animations/popup/frame_112.png b/linux-rust/assets/animations/popup/frame_112.png new file mode 100644 index 000000000..1b7205829 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_112.png differ diff --git a/linux-rust/assets/animations/popup/frame_113.png b/linux-rust/assets/animations/popup/frame_113.png new file mode 100644 index 000000000..04bdff4fb Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_113.png differ diff --git a/linux-rust/assets/animations/popup/frame_114.png b/linux-rust/assets/animations/popup/frame_114.png new file mode 100644 index 000000000..d38f1e0c5 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_114.png differ diff --git a/linux-rust/assets/animations/popup/frame_115.png b/linux-rust/assets/animations/popup/frame_115.png new file mode 100644 index 000000000..23bf81733 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_115.png differ diff --git a/linux-rust/assets/animations/popup/frame_116.png b/linux-rust/assets/animations/popup/frame_116.png new file mode 100644 index 000000000..37cb98563 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_116.png differ diff --git a/linux-rust/assets/animations/popup/frame_117.png b/linux-rust/assets/animations/popup/frame_117.png new file mode 100644 index 000000000..46f016f28 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_117.png differ diff --git a/linux-rust/assets/animations/popup/frame_118.png b/linux-rust/assets/animations/popup/frame_118.png new file mode 100644 index 000000000..ba9f4a85e Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_118.png differ diff --git a/linux-rust/assets/animations/popup/frame_119.png b/linux-rust/assets/animations/popup/frame_119.png new file mode 100644 index 000000000..763a41922 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_119.png differ diff --git a/linux-rust/assets/animations/popup/frame_120.png b/linux-rust/assets/animations/popup/frame_120.png new file mode 100644 index 000000000..ca2d32d61 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_120.png differ diff --git a/linux-rust/assets/animations/popup/frame_121.png b/linux-rust/assets/animations/popup/frame_121.png new file mode 100644 index 000000000..f011ffc4a Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_121.png differ diff --git a/linux-rust/assets/animations/popup/frame_122.png b/linux-rust/assets/animations/popup/frame_122.png new file mode 100644 index 000000000..48d84e1b8 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_122.png differ diff --git a/linux-rust/assets/animations/popup/frame_123.png b/linux-rust/assets/animations/popup/frame_123.png new file mode 100644 index 000000000..77e28ae32 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_123.png differ diff --git a/linux-rust/assets/animations/popup/frame_124.png b/linux-rust/assets/animations/popup/frame_124.png new file mode 100644 index 000000000..ced75e4fb Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_124.png differ diff --git a/linux-rust/assets/animations/popup/frame_125.png b/linux-rust/assets/animations/popup/frame_125.png new file mode 100644 index 000000000..9d588e3d7 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_125.png differ diff --git a/linux-rust/assets/animations/popup/frame_126.png b/linux-rust/assets/animations/popup/frame_126.png new file mode 100644 index 000000000..af7e04999 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_126.png differ diff --git a/linux-rust/assets/animations/popup/frame_127.png b/linux-rust/assets/animations/popup/frame_127.png new file mode 100644 index 000000000..26e6b827e Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_127.png differ diff --git a/linux-rust/assets/animations/popup/frame_128.png b/linux-rust/assets/animations/popup/frame_128.png new file mode 100644 index 000000000..840d267c0 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_128.png differ diff --git a/linux-rust/assets/animations/popup/frame_129.png b/linux-rust/assets/animations/popup/frame_129.png new file mode 100644 index 000000000..ccbf75d1f Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_129.png differ diff --git a/linux-rust/assets/animations/popup/frame_130.png b/linux-rust/assets/animations/popup/frame_130.png new file mode 100644 index 000000000..34c1c9ed1 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_130.png differ diff --git a/linux-rust/assets/animations/popup/frame_131.png b/linux-rust/assets/animations/popup/frame_131.png new file mode 100644 index 000000000..593b3f362 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_131.png differ diff --git a/linux-rust/assets/animations/popup/frame_132.png b/linux-rust/assets/animations/popup/frame_132.png new file mode 100644 index 000000000..bf0383319 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_132.png differ diff --git a/linux-rust/assets/animations/popup/frame_133.png b/linux-rust/assets/animations/popup/frame_133.png new file mode 100644 index 000000000..f34e2fde9 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_133.png differ diff --git a/linux-rust/assets/animations/popup/frame_134.png b/linux-rust/assets/animations/popup/frame_134.png new file mode 100644 index 000000000..977c02099 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_134.png differ diff --git a/linux-rust/assets/animations/popup/frame_135.png b/linux-rust/assets/animations/popup/frame_135.png new file mode 100644 index 000000000..40058d6dc Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_135.png differ diff --git a/linux-rust/assets/animations/popup/frame_136.png b/linux-rust/assets/animations/popup/frame_136.png new file mode 100644 index 000000000..7141d9aee Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_136.png differ diff --git a/linux-rust/assets/animations/popup/frame_137.png b/linux-rust/assets/animations/popup/frame_137.png new file mode 100644 index 000000000..6d60ba003 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_137.png differ diff --git a/linux-rust/assets/animations/popup/frame_138.png b/linux-rust/assets/animations/popup/frame_138.png new file mode 100644 index 000000000..3ba3ee7af Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_138.png differ diff --git a/linux-rust/assets/animations/popup/frame_139.png b/linux-rust/assets/animations/popup/frame_139.png new file mode 100644 index 000000000..b7553f1a1 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_139.png differ diff --git a/linux-rust/assets/animations/popup/frame_140.png b/linux-rust/assets/animations/popup/frame_140.png new file mode 100644 index 000000000..555a429dd Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_140.png differ diff --git a/linux-rust/assets/animations/popup/frame_141.png b/linux-rust/assets/animations/popup/frame_141.png new file mode 100644 index 000000000..4782f23d1 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_141.png differ diff --git a/linux-rust/assets/animations/popup/frame_142.png b/linux-rust/assets/animations/popup/frame_142.png new file mode 100644 index 000000000..749e509c1 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_142.png differ diff --git a/linux-rust/assets/animations/popup/frame_143.png b/linux-rust/assets/animations/popup/frame_143.png new file mode 100644 index 000000000..60f7a31ce Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_143.png differ diff --git a/linux-rust/assets/animations/popup/frame_144.png b/linux-rust/assets/animations/popup/frame_144.png new file mode 100644 index 000000000..0c724ae26 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_144.png differ diff --git a/linux-rust/assets/animations/popup/frame_145.png b/linux-rust/assets/animations/popup/frame_145.png new file mode 100644 index 000000000..3794b789b Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_145.png differ diff --git a/linux-rust/assets/animations/popup/frame_146.png b/linux-rust/assets/animations/popup/frame_146.png new file mode 100644 index 000000000..505b2528a Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_146.png differ diff --git a/linux-rust/assets/animations/popup/frame_147.png b/linux-rust/assets/animations/popup/frame_147.png new file mode 100644 index 000000000..252e9a01b Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_147.png differ diff --git a/linux-rust/assets/animations/popup/frame_148.png b/linux-rust/assets/animations/popup/frame_148.png new file mode 100644 index 000000000..8374e2d65 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_148.png differ diff --git a/linux-rust/assets/animations/popup/frame_149.png b/linux-rust/assets/animations/popup/frame_149.png new file mode 100644 index 000000000..b54e79013 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_149.png differ diff --git a/linux-rust/assets/animations/popup/frame_150.png b/linux-rust/assets/animations/popup/frame_150.png new file mode 100644 index 000000000..818e00f34 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_150.png differ diff --git a/linux-rust/assets/animations/popup/frame_151.png b/linux-rust/assets/animations/popup/frame_151.png new file mode 100644 index 000000000..abcfb0333 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_151.png differ diff --git a/linux-rust/assets/animations/popup/frame_152.png b/linux-rust/assets/animations/popup/frame_152.png new file mode 100644 index 000000000..28e9450ac Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_152.png differ diff --git a/linux-rust/assets/animations/popup/frame_153.png b/linux-rust/assets/animations/popup/frame_153.png new file mode 100644 index 000000000..c3147aee6 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_153.png differ diff --git a/linux-rust/assets/animations/popup/frame_154.png b/linux-rust/assets/animations/popup/frame_154.png new file mode 100644 index 000000000..35810a4b5 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_154.png differ diff --git a/linux-rust/assets/animations/popup/frame_155.png b/linux-rust/assets/animations/popup/frame_155.png new file mode 100644 index 000000000..7f13df68e Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_155.png differ diff --git a/linux-rust/assets/animations/popup/frame_156.png b/linux-rust/assets/animations/popup/frame_156.png new file mode 100644 index 000000000..62b1b96b2 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_156.png differ diff --git a/linux-rust/assets/animations/popup/frame_157.png b/linux-rust/assets/animations/popup/frame_157.png new file mode 100644 index 000000000..eb41fb8be Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_157.png differ diff --git a/linux-rust/assets/animations/popup/frame_158.png b/linux-rust/assets/animations/popup/frame_158.png new file mode 100644 index 000000000..ffd412cb1 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_158.png differ diff --git a/linux-rust/assets/animations/popup/frame_159.png b/linux-rust/assets/animations/popup/frame_159.png new file mode 100644 index 000000000..837268691 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_159.png differ diff --git a/linux-rust/assets/animations/popup/frame_160.png b/linux-rust/assets/animations/popup/frame_160.png new file mode 100644 index 000000000..1517e4499 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_160.png differ diff --git a/linux-rust/assets/animations/popup/frame_161.png b/linux-rust/assets/animations/popup/frame_161.png new file mode 100644 index 000000000..92d7c127b Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_161.png differ diff --git a/linux-rust/assets/animations/popup/frame_162.png b/linux-rust/assets/animations/popup/frame_162.png new file mode 100644 index 000000000..54b15bbaf Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_162.png differ diff --git a/linux-rust/assets/animations/popup/frame_163.png b/linux-rust/assets/animations/popup/frame_163.png new file mode 100644 index 000000000..d3f171ae6 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_163.png differ diff --git a/linux-rust/assets/animations/popup/frame_164.png b/linux-rust/assets/animations/popup/frame_164.png new file mode 100644 index 000000000..aff51d939 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_164.png differ diff --git a/linux-rust/assets/animations/popup/frame_165.png b/linux-rust/assets/animations/popup/frame_165.png new file mode 100644 index 000000000..af8e15d52 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_165.png differ diff --git a/linux-rust/assets/animations/popup/frame_166.png b/linux-rust/assets/animations/popup/frame_166.png new file mode 100644 index 000000000..942c72752 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_166.png differ diff --git a/linux-rust/assets/animations/popup/frame_167.png b/linux-rust/assets/animations/popup/frame_167.png new file mode 100644 index 000000000..4d6f2a937 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_167.png differ diff --git a/linux-rust/assets/animations/popup/frame_168.png b/linux-rust/assets/animations/popup/frame_168.png new file mode 100644 index 000000000..3dc660511 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_168.png differ diff --git a/linux-rust/assets/animations/popup/frame_169.png b/linux-rust/assets/animations/popup/frame_169.png new file mode 100644 index 000000000..3fa947edb Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_169.png differ diff --git a/linux-rust/assets/animations/popup/frame_170.png b/linux-rust/assets/animations/popup/frame_170.png new file mode 100644 index 000000000..aba038c59 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_170.png differ diff --git a/linux-rust/assets/animations/popup/frame_171.png b/linux-rust/assets/animations/popup/frame_171.png new file mode 100644 index 000000000..5878c91d1 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_171.png differ diff --git a/linux-rust/assets/animations/popup/frame_172.png b/linux-rust/assets/animations/popup/frame_172.png new file mode 100644 index 000000000..9a8291d37 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_172.png differ diff --git a/linux-rust/assets/animations/popup/frame_173.png b/linux-rust/assets/animations/popup/frame_173.png new file mode 100644 index 000000000..072e61d5b Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_173.png differ diff --git a/linux-rust/assets/animations/popup/frame_174.png b/linux-rust/assets/animations/popup/frame_174.png new file mode 100644 index 000000000..cdea589cc Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_174.png differ diff --git a/linux-rust/assets/animations/popup/frame_175.png b/linux-rust/assets/animations/popup/frame_175.png new file mode 100644 index 000000000..0014c278c Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_175.png differ diff --git a/linux-rust/assets/animations/popup/frame_176.png b/linux-rust/assets/animations/popup/frame_176.png new file mode 100644 index 000000000..286bea52e Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_176.png differ diff --git a/linux-rust/assets/animations/popup/frame_177.png b/linux-rust/assets/animations/popup/frame_177.png new file mode 100644 index 000000000..4ae351b12 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_177.png differ diff --git a/linux-rust/assets/animations/popup/frame_178.png b/linux-rust/assets/animations/popup/frame_178.png new file mode 100644 index 000000000..3fe7c9ab1 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_178.png differ diff --git a/linux-rust/assets/animations/popup/frame_179.png b/linux-rust/assets/animations/popup/frame_179.png new file mode 100644 index 000000000..2e61814b9 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_179.png differ diff --git a/linux-rust/assets/animations/popup/frame_180.png b/linux-rust/assets/animations/popup/frame_180.png new file mode 100644 index 000000000..100a287f2 Binary files /dev/null and b/linux-rust/assets/animations/popup/frame_180.png differ diff --git a/linux-rust/src/bluetooth/aacp.rs b/linux-rust/src/bluetooth/aacp.rs index bed6ca853..56155be8c 100644 --- a/linux-rust/src/bluetooth/aacp.rs +++ b/linux-rust/src/bluetooth/aacp.rs @@ -495,9 +495,13 @@ impl AACPManager { } pub async fn receive_packet(&self, packet: &[u8]) { - if !packet.starts_with(&HEADER_BYTES) { + if packet.len() < 5 { + return; + } + + if !packet.starts_with(&[0x04, 0x00, 0x04, 0x00]) && !packet.starts_with(&[0x01, 0x00, 0x04, 0x00]) { debug!( - "Received packet does not start with expected header: {}", + "Received packet with unknown header: {}", hex::encode(packet) ); return; @@ -509,6 +513,7 @@ impl AACPManager { let opcode = packet[4]; let payload = &packet[4..]; + debug!("Processing packet opcode={:#04x} len={}", opcode, payload.len()); match opcode { opcodes::BATTERY_INFO => { @@ -633,6 +638,7 @@ impl AACPManager { EarDetectionStatus::OutOfEar } }); + debug!("Received Ear Detection: {:?}", statuses); let mut state = self.state.lock().await; state.old_ear_detection_status = state.ear_detection_status.clone(); state.ear_detection_status = statuses.clone(); @@ -917,7 +923,7 @@ impl AACPManager { opcodes::EQ_DATA => { debug!("Received EQ Data"); } - _ => debug!("Received unknown packet with opcode {:#04x}", opcode), + _ => debug!("Received packet with header {} opcode {:#04x}, payload: {}", hex::encode(&packet[..4]), opcode, hex::encode(payload)), } } @@ -928,6 +934,13 @@ impl AACPManager { self.send_data_packet(&packet).await } + pub async fn send_battery_request(&self) -> Result<()> { + let opcode = [opcodes::BATTERY_INFO, 0x00]; + let data = [0x00]; // Request current battery status + let packet = [opcode.as_slice(), data.as_slice()].concat(); + self.send_data_packet(&packet).await + } + pub async fn send_set_feature_flags_packet(&self) -> Result<()> { let opcode = [opcodes::SET_FEATURE_FLAGS, 0x00]; // let data = [0xD7, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; @@ -1212,7 +1225,7 @@ async fn recv_thread(manager: AACPManager, sp: Arc) { } Ok(n) => { let data = &buf[..n]; - debug!("Received {} bytes: {}", n, hex::encode(data)); + debug!("Received raw {} bytes: {}", n, hex::encode(data)); manager.receive_packet(data).await; } Err(e) => { diff --git a/linux-rust/src/bluetooth/le.rs b/linux-rust/src/bluetooth/le.rs index 69918bf3a..39f4d3b74 100644 --- a/linux-rust/src/bluetooth/le.rs +++ b/linux-rust/src/bluetooth/le.rs @@ -14,6 +14,7 @@ use serde_json; use std::collections::{HashMap, HashSet}; use std::str::FromStr; use std::sync::Arc; +use std::time::{Duration, Instant}; use tokio::sync::Mutex; fn decrypt(key: &[u8; 16], data: &[u8; 16]) -> [u8; 16] { @@ -46,12 +47,12 @@ fn verify_rpa(addr: &str, irk: &[u8; 16]) -> bool { hash == computed_hash } -pub async fn start_le_monitor(tray_handle: Option>) -> bluer::Result<()> { +pub async fn start_le_monitor(tray_handle: Option>, ui_tx: tokio::sync::mpsc::UnboundedSender) -> bluer::Result<()> { let session = Session::new().await?; let adapter = session.default_adapter().await?; adapter.set_powered(true).await?; - let all_devices: HashMap = std::fs::read_to_string(get_devices_path()) + let mut all_devices: HashMap = std::fs::read_to_string(get_devices_path()) .ok() .and_then(|s| serde_json::from_str(&s).ok()) .unwrap_or_default(); @@ -122,6 +123,52 @@ pub async fn start_le_monitor(tray_handle: Option>) -> blue } } + if found_mac.is_none() { + // Try reloading devices once to see if keys were recently added + let devices_json = std::fs::read_to_string(get_devices_path()).unwrap_or_else(|_| "{}".to_string()); + all_devices = serde_json::from_str(&devices_json).unwrap_or_default(); + + for (mac, info) in &all_devices { + if let Some(DeviceInformation::AirPods(airpods_info)) = &info.information { + if !airpods_info.le_keys.irk.is_empty() { + let irk_bytes = hex::decode(&airpods_info.le_keys.irk).unwrap_or_default(); + if irk_bytes.len() == 16 { + let mut irk = [0u8; 16]; + irk.copy_from_slice(&irk_bytes); + if verify_rpa(&addr.to_string(), &irk) { + info!("LE Monitor matched AirPods (cached): {} (MAC: {})", mac, addr); + found_mac = Some(mac.clone()); + break; + } + } + } + } + } + } + + if found_mac.is_none() { + // Try reloading devices once to see if keys were recently added + let devices_json = std::fs::read_to_string(get_devices_path()).unwrap_or_else(|_| "{}".to_string()); + all_devices = serde_json::from_str(&devices_json).unwrap_or_default(); + + for (mac, info) in &all_devices { + if let Some(DeviceInformation::AirPods(airpods_info)) = &info.information { + if !airpods_info.le_keys.irk.is_empty() { + let irk_bytes = hex::decode(&airpods_info.le_keys.irk).unwrap_or_default(); + if irk_bytes.len() == 16 { + let mut irk = [0u8; 16]; + irk.copy_from_slice(&irk_bytes); + if verify_rpa(&addr.to_string(), &irk) { + info!("LE Monitor matched AirPods: {} (MAC: {})", mac, addr); + found_mac = Some(mac.clone()); + break; + } + } + } + } + } + } + if let Some(mac) = found_mac { matched_airpods_mac = Some(mac); } else { @@ -144,7 +191,9 @@ pub async fn start_le_monitor(tray_handle: Option>) -> blue let mut events = dev.events().await?; let tray_handle_clone = tray_handle.clone(); let connecting_macs_clone = Arc::clone(&connecting_macs); + let ui_tx_clone = ui_tx.clone(); tokio::spawn(async move { + let mut last_popup_sent: Option = None; while let Some(ev) = events.next().await { match ev { bluer::DeviceEvent::PropertyChanged(prop) => { @@ -336,6 +385,26 @@ pub async fn start_le_monitor(tray_handle: Option>) -> blue .await; } + let now = Instant::now(); + let is_throttled = last_popup_sent.map(|t| now.duration_since(t) < Duration::from_secs(10)).unwrap_or(false); + + if case_byte != 0xff && !is_throttled { + info!("Sending ShowPopup message to UI for {}", matched_airpods_mac.as_ref().unwrap()); + let _ = ui_tx_clone.send(crate::ui::messages::BluetoothUIMessage::ShowPopup { + mac: matched_airpods_mac.as_ref().unwrap().clone(), + battery_l: if left_byte == 0xff { None } else { Some(left_battery as u8) }, + battery_r: if right_byte == 0xff { None } else { Some(right_battery as u8) }, + battery_c: if case_byte == 0xff { None } else { Some(case_battery as u8) }, + charging_l: left_charging, + charging_r: right_charging, + charging_c: case_charging, + }); + last_popup_sent = Some(now); + } else if case_byte != 0xff { + debug!("Throttling ShowPopup message for {}", matched_airpods_mac.as_ref().unwrap()); + } + + debug!( "Battery status: Left: {}, Right: {}, Case: {}, InEar: L:{} R:{}", if left_byte == 0xff { diff --git a/linux-rust/src/devices/airpods.rs b/linux-rust/src/devices/airpods.rs index f0e876cf0..118a5cc7d 100644 --- a/linux-rust/src/devices/airpods.rs +++ b/linux-rust/src/devices/airpods.rs @@ -30,8 +30,9 @@ impl AirPodsDevice { let mut aacp_manager = AACPManager::new(); aacp_manager.connect(mac_address).await; - // let mut att_manager = ATTManager::new(); - // att_manager.connect(mac_address).await.expect("Failed to connect ATT"); + let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); + let (command_tx, mut command_rx) = tokio::sync::mpsc::unbounded_channel(); + aacp_manager.set_event_channel(tx).await; if let Some(handle) = &tray_handle { handle @@ -81,6 +82,20 @@ impl AirPodsDevice { error!("Failed to request proximity keys: {}", e); } + sleep(Duration::from_millis(300)).await; + + // Claim ownership so AirPods send us BatteryInfo and EarDetection events + info!("Claiming ownership of AirPods connection"); + if let Err(e) = aacp_manager + .send_control_command( + crate::bluetooth::aacp::ControlCommandIdentifiers::OwnsConnection, + &[0x01], + ) + .await + { + error!("Failed to claim ownership: {}", e); + } + let app_settings_path = get_app_settings_path(); let settings = std::fs::read_to_string(&app_settings_path) .ok() @@ -123,10 +138,6 @@ impl AirPodsDevice { local_mac.clone(), ))); let mc_clone = media_controller.clone(); - let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); - let (command_tx, mut command_rx) = tokio::sync::mpsc::unbounded_channel(); - - aacp_manager.set_event_channel(tx).await; if let Some(handle) = &tray_handle { handle .update(|tray: &mut MyTray| tray.command_tx = Some(command_tx.clone())) diff --git a/linux-rust/src/devices/enums.rs b/linux-rust/src/devices/enums.rs index 5768d1802..544a2d1b1 100644 --- a/linux-rust/src/devices/enums.rs +++ b/linux-rust/src/devices/enums.rs @@ -57,9 +57,58 @@ pub struct AirPodsState { pub conversation_awareness_enabled: bool, pub personalized_volume_enabled: bool, pub allow_off_mode: bool, + pub auto_anc_strength: u8, pub battery: Vec, } +impl AirPodsState { + pub fn update_battery(&mut self, battery_info: &[BatteryInfo]) { + for b in battery_info { + if let Some(existing) = self.battery.iter_mut().find(|e| e.component == b.component) { + *existing = b.clone(); + } else { + self.battery.push(b.clone()); + } + } + } + + pub fn is_case_open(&self) -> bool { + // Case component is 0x08. Status 4 means Disconnected (Case Closed/Off) + self.battery.iter().any(|b| b.component as u8 == 0x08 && b.status as u8 != 4) + } + + pub fn get_battery_levels(&self) -> (Option, Option, Option) { + let mut l = None; + let mut r = None; + let mut c = None; + for b in &self.battery { + match b.component as u8 { + 0x04 => l = Some(b.level), + 0x02 => r = Some(b.level), + 0x08 => c = Some(b.level), + _ => {} + } + } + (l, r, c) + } + + pub fn get_charging_statuses(&self) -> (bool, bool, bool) { + let mut l = false; + let mut r = false; + let mut c = false; + for b in &self.battery { + let is_charging = b.status as u8 == 1; + match b.component as u8 { + 0x04 => l = is_charging, + 0x02 => r = is_charging, + 0x08 => c = is_charging, + _ => {} + } + } + (l, r, c) + } +} + #[derive(Clone, Debug)] pub enum AirPodsNoiseControlMode { Off, diff --git a/linux-rust/src/devices/nothing.rs b/linux-rust/src/devices/nothing.rs index 1f78044b7..9d3a63d82 100644 --- a/linux-rust/src/devices/nothing.rs +++ b/linux-rust/src/devices/nothing.rs @@ -24,7 +24,7 @@ pub struct NothingDevice { impl NothingDevice { pub async fn new( mac_address: Address, - ui_tx: mpsc::UnboundedSender, + _ui_tx: mpsc::UnboundedSender, ) -> Self { let mut att_manager = ATTManager::new(); att_manager diff --git a/linux-rust/src/main.rs b/linux-rust/src/main.rs index f43f575b2..adbdc6d05 100644 --- a/linux-rust/src/main.rs +++ b/linux-rust/src/main.rs @@ -10,7 +10,7 @@ use crate::bluetooth::managers::DeviceManagers; use crate::devices::enums::DeviceData; use crate::ui::messages::BluetoothUIMessage; use crate::ui::tray::MyTray; -use crate::utils::{get_app_settings_path, get_devices_path}; +use crate::utils::{get_devices_path}; use bluer::{Address, InternalErrorKind}; use clap::Parser; use dbus::arg::{RefArg, Variant}; @@ -19,10 +19,9 @@ use dbus::blocking::stdintf::org_freedesktop_dbus::Properties; use dbus::message::MatchRule; use devices::airpods::AirPodsDevice; use ksni::TrayMethods; -use log::{debug, info, warn}; +use log::{info, warn}; use std::collections::HashMap; use std::env; -use std::sync::atomic::{AtomicBool}; use std::sync::Arc; use tokio::sync::RwLock; use tokio::sync::mpsc::unbounded_channel; @@ -154,9 +153,10 @@ async fn async_main( adapter.set_powered(true).await?; let le_tray_clone = tray_handle.clone(); + let le_ui_tx = ui_tx.clone(); tokio::spawn(async move { info!("Starting LE monitor..."); - if let Err(e) = start_le_monitor(le_tray_clone).await { + if let Err(e) = start_le_monitor(le_tray_clone, le_ui_tx).await { log::error!("LE monitor error: {}", e); } }); diff --git a/linux-rust/src/media_controller.rs b/linux-rust/src/media_controller.rs index 5bd68b1a2..8b5ad5d27 100644 --- a/linux-rust/src/media_controller.rs +++ b/linux-rust/src/media_controller.rs @@ -143,8 +143,8 @@ impl MediaController { .ear_detection_status .contains(&EarDetectionStatus::InEar) { - info!("Media playback started but buds not in ear, skipping takeover"); - continue; + info!("Media playback started but buds not in ear, skipping takeover (BYPASSED)"); + // continue; } info!("Media playback started, taking ownership and activating a2dp"); let _ = control_tx.send(( diff --git a/linux-rust/src/ui/airpods.rs b/linux-rust/src/ui/airpods.rs index 9335b5e05..5b79efb70 100644 --- a/linux-rust/src/ui/airpods.rs +++ b/linux-rust/src/ui/airpods.rs @@ -5,7 +5,7 @@ use iced::overlay::menu; use iced::widget::button::Style; use iced::widget::rule::FillMode; use iced::widget::{ - Space, button, column, combo_box, container, row, rule, text, text_input, toggler, + Space, button, column, combo_box, container, row, rule, slider, text, text_input, toggler, }; use iced::{Background, Border, Center, Color, Length, Padding, Theme}; use log::error; @@ -63,10 +63,9 @@ pub fn airpods_view<'a>( run_async_in_thread({ let new_name = new_name.clone(); async move { - aacp_manager - .send_rename_packet(&new_name) - .await - .expect("Failed to send rename packet"); + if let Err(e) = aacp_manager.send_rename_packet(&new_name).await { + log::error!("Failed to send rename packet: {}", e); + } } }); let mut state = state.clone(); @@ -114,13 +113,15 @@ pub fn airpods_view<'a>( let aacp_manager = aacp_manager.clone(); let selected_mode_c = selected_mode.clone(); run_async_in_thread(async move { - aacp_manager + if let Err(e) = aacp_manager .send_control_command( ControlCommandIdentifiers::ListeningMode, &[selected_mode_c.to_byte()], ) .await - .expect("Failed to send Noise Control Mode command"); + { + log::error!("Failed to send Noise Control Mode command: {}", e); + } }); let mut state = state_clone.clone(); state.noise_control_mode = selected_mode.clone(); @@ -225,10 +226,12 @@ pub fn airpods_view<'a>( let mac = mac.clone(); run_async_in_thread( async move { - aacp_manager.send_control_command( + if let Err(e) = aacp_manager.send_control_command( ControlCommandIdentifiers::AdaptiveVolumeConfig, if is_enabled { &[0x01] } else { &[0x02] } - ).await.expect("Failed to send Personalized Volume command"); + ).await { + log::error!("Failed to send Personalized Volume command: {}", e); + } } ); let mut state = state.clone(); @@ -255,6 +258,7 @@ pub fn airpods_view<'a>( ), { let aacp_manager_conv_detect = aacp_manager.clone(); + let mac_audio_clone = mac_audio.clone(); row![ column![ text("Conversation Awareness").size(16), @@ -271,21 +275,69 @@ pub fn airpods_view<'a>( let aacp_manager = aacp_manager_conv_detect.clone(); run_async_in_thread( async move { - aacp_manager.send_control_command( + if let Err(e) = aacp_manager.send_control_command( ControlCommandIdentifiers::ConversationDetectConfig, if is_enabled { &[0x01] } else { &[0x02] } - ).await.expect("Failed to send Conversation Awareness command"); + ).await { + log::error!("Failed to send Conversation Awareness command: {}", e); + } } ); let mut state = state.clone(); state.conversation_awareness_enabled = is_enabled; - Message::StateChanged(mac_audio.to_string(), DeviceState::AirPods(state)) + Message::StateChanged(mac_audio_clone.to_string(), DeviceState::AirPods(state)) }) .spacing(0) .size(20) ] .align_y(Center) .spacing(8) + }, + rule::horizontal(1).style( + |theme: &Theme| { + rule::Style { + color: theme.palette().text.scale_alpha(0.2), + radius: Radius::from(12), + fill_mode: FillMode::Full, + snap: false + } + } + ), + { + let aacp_manager_anc = aacp_manager.clone(); + let mac = mac_audio.clone(); + let state_clone = state.clone(); + column![ + row![ + column![ + text("Adaptive Audio Intensity").size(16), + text("Adjust the level of noise cancellation in Adaptive mode.").size(12).style( + |theme: &Theme| { + let mut style = text::Style::default(); + style.color = Some(theme.palette().text.scale_alpha(0.7)); + style + } + ).width(Length::Fill), + ].width(Length::Fill), + text(format!("{}%", state.auto_anc_strength)).size(16), + ].align_y(Center), + slider(0..=100, state.auto_anc_strength, move |value| { + let aacp_manager = aacp_manager_anc.clone(); + let mac = mac.clone(); + run_async_in_thread(async move { + if let Err(e) = aacp_manager.send_control_command( + ControlCommandIdentifiers::AutoAncStrength, + &[value] + ).await { + log::error!("Failed to send Auto ANC Strength command: {}", e); + } + }); + let mut state = state_clone.clone(); + state.auto_anc_strength = value; + Message::StateChanged(mac, DeviceState::AirPods(state)) + }) + .step(1) + ].spacing(4) } ] .spacing(4) @@ -328,10 +380,12 @@ pub fn airpods_view<'a>( let aacp_manager = aacp_manager_olm.clone(); run_async_in_thread( async move { - aacp_manager.send_control_command( + if let Err(e) = aacp_manager.send_control_command( ControlCommandIdentifiers::AllowOffOption, if is_enabled { &[0x01] } else { &[0x02] } - ).await.expect("Failed to send Off Listening Mode command"); + ).await { + log::error!("Failed to send Off Listening Mode command: {}", e); + } } ); let mut state = state.clone(); diff --git a/linux-rust/src/ui/messages.rs b/linux-rust/src/ui/messages.rs index c72aeb9c6..ffd84625e 100644 --- a/linux-rust/src/ui/messages.rs +++ b/linux-rust/src/ui/messages.rs @@ -7,5 +7,14 @@ pub enum BluetoothUIMessage { DeviceDisconnected(String), // mac AACPUIEvent(String, AACPEvent), // mac, event ATTNotification(String, u16, Vec), // mac, handle, data + ShowPopup { + mac: String, + battery_l: Option, + battery_r: Option, + battery_c: Option, + charging_l: bool, + charging_r: bool, + charging_c: bool, + }, NoOp, } diff --git a/linux-rust/src/ui/mod.rs b/linux-rust/src/ui/mod.rs index 0fff86303..af261d6e2 100644 --- a/linux-rust/src/ui/mod.rs +++ b/linux-rust/src/ui/mod.rs @@ -3,3 +3,4 @@ pub mod messages; mod nothing; pub mod tray; pub mod window; +pub mod popup; diff --git a/linux-rust/src/ui/popup.rs b/linux-rust/src/ui/popup.rs new file mode 100644 index 000000000..372bff201 --- /dev/null +++ b/linux-rust/src/ui/popup.rs @@ -0,0 +1,97 @@ +use iced::widget::{column, container, image, row, text, Space}; +use iced::{Alignment, Element, Length, Padding, Color, Background, Border}; +use crate::ui::window::Message; +use std::sync::Arc; + +pub fn popup_view( + name: String, + frame: usize, + frames: Arc>, + battery_l: Option, + battery_r: Option, + battery_c: Option, + charging_l: bool, + charging_r: bool, + charging_c: bool, +) -> Element<'static, Message> { + let img = if frame < frames.len() { + image(frames[frame].clone()) + .width(Length::Fill) + .content_fit(iced::ContentFit::Contain) + } else { + image(frames[0].clone()) + .width(Length::Fill) + .content_fit(iced::ContentFit::Contain) + }; + + let format_batt = |b: Option, charging: bool| -> String { + match b { + Some(v) => format!("{}%{}", v, if charging { " \u{26A1}" } else { "" }), + None => "--%".to_string(), + } + }; + + let battery_item = |label: &'static str, battery: Option, charging: bool| { + column![ + text(label).size(12).color(Color::from_rgb(0.4, 0.4, 0.4)), + text(format_batt(battery, charging)).size(16).color(Color::BLACK), + ].align_x(Alignment::Center).spacing(2) + }; + + let battery_row = row![ + battery_item("Left", battery_l, charging_l), + Space::new().width(30), + battery_item("Right", battery_r, charging_r), + Space::new().width(30), + battery_item("Case", battery_c, charging_c), + ] + .align_y(Alignment::Center); + + let inner_card = container( + column![ + text(name).size(22).color(Color::BLACK), + Space::new().height(10), + container(img).height(140).center_x(Length::Fill), + Space::new().height(10), + container(battery_row) + .padding(Padding::from([10, 20])) + .style(|_| container::Style { + background: Some(Background::Color(Color::from_rgba(0.0, 0.0, 0.0, 0.05))), + border: Border { + color: Color::from_rgba(0.0, 0.0, 0.0, 0.1), + width: 1.0, + radius: 16.0.into(), + }, + ..Default::default() + }) + ] + .align_x(Alignment::Center) + .padding(20) + ) + .width(360) + .style(|_| container::Style { + background: Some(Background::Color(Color::WHITE)), + border: Border { + color: Color::from_rgba(0.0, 0.0, 0.0, 0.15), + width: 1.0, + radius: 28.0.into(), + }, + shadow: iced::Shadow { + color: Color::from_rgba(0.0, 0.0, 0.0, 0.2), + offset: iced::Vector::new(0.0, 10.0), + blur_radius: 30.0, + }, + ..Default::default() + }); + + container(inner_card) + .width(Length::Fill) + .height(Length::Fill) + .center_x(Length::Fill) + .center_y(Length::Fill) + .style(|_| container::Style { + background: Some(Background::Color(Color::TRANSPARENT)), + ..Default::default() + }) + .into() +} diff --git a/linux-rust/src/ui/window.rs b/linux-rust/src/ui/window.rs index 4574b97ce..39020d817 100644 --- a/linux-rust/src/ui/window.rs +++ b/linux-rust/src/ui/window.rs @@ -19,10 +19,9 @@ use iced::widget::{ Space, button, column, combo_box, container, pane_grid, row, rule, scrollable, text, text_input, toggler }; -use iced::{Background, Border, Center, Element, Font, Length, Padding, Size, Subscription, Task, Theme, daemon, window, Settings, Program}; -use log::{debug, error}; +use iced::{Background, Border, Center, Element, Font, Length, Padding, Size, Subscription, Task, Theme, daemon, window, Settings}; +use log::{debug, error, info}; use std::collections::HashMap; -use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use tokio::sync::mpsc::UnboundedReceiver; use tokio::sync::{Mutex, RwLock}; @@ -76,6 +75,24 @@ pub struct App { selected_device_type: Option, tray_text_mode: bool, stem_control: bool, + show_popup: bool, + + // Popup State + popup_window: Option, + popup_frame: usize, + popup_last_tick: Option, + popup_suppressed_until: Option, + is_in_ear: bool, + popup_mac: Option, + popup_battery_l: Option, + popup_battery_r: Option, + popup_battery_c: Option, + popup_charging_l: bool, + popup_charging_r: bool, + popup_charging_c: bool, + popup_name: String, + main_window: Option, + animation_frames: Arc>, } pub struct BluetoothState { @@ -108,6 +125,9 @@ pub enum Message { StateChanged(String, DeviceState), TrayTextModeChanged(bool), // yes, I know I should add all settings to a struct, but I'm lazy StemControlChanged(bool), + ShowPopupChanged(bool), + Tick, + ClosePopup, } #[derive(Clone, Debug, PartialEq, Eq, Hash)] @@ -142,7 +162,7 @@ impl App { } else { let mut settings = window::Settings::default(); settings.min_size = Some(Size::new(400.0, 300.0)); - settings.icon = window::icon::from_file("../../assets/icon.png").ok(); + settings.icon = window::icon::from_file("assets/icon.png").ok(); let (id, open) = window::open(settings); (Some(id), open.map(Message::WindowOpened)) }; @@ -166,6 +186,11 @@ impl App { .and_then(|v| v.get("stem_control").cloned()) .and_then(|s| serde_json::from_value(s).ok()) .unwrap_or(false); + let show_popup = settings + .clone() + .and_then(|v| v.get("show_popup").cloned()) + .and_then(|s| serde_json::from_value(s).ok()) + .unwrap_or(true); let bluetooth_state = BluetoothState::new(); @@ -177,9 +202,25 @@ impl App { // ]); let device_states = HashMap::new(); + + let mut frames = Vec::new(); + for i in 1..=180 { + frames.push(iced::widget::image::Handle::from_path(format!("assets/animations/popup/frame_{:03}.png", i))); + } let ui_rx_clone = Arc::clone(&ui_rx); + let (main_window, tasks) = if start_minimized { + (None, vec![Task::perform(wait_for_message(ui_rx_clone), |msg| msg)]) + } else { + let (id, open) = window::open(window::Settings { + size: Size::new(800.0, 600.0), + ..Default::default() + }); + (Some(id), vec![Task::perform(wait_for_message(ui_rx_clone), |msg| msg), open.map(Message::WindowOpened)]) + }; + ( Self { - window, + window: main_window, + main_window, panes, selected_tab: Tab::Device("none".to_string()), theme_state: combo_box::State::new(vec![ @@ -217,6 +258,21 @@ impl App { device_managers, tray_text_mode, stem_control, + show_popup, + popup_window: None, + popup_frame: 0, + popup_last_tick: None, + popup_suppressed_until: None, + is_in_ear: false, + popup_mac: None, + popup_battery_l: None, + popup_battery_r: None, + popup_battery_c: None, + popup_charging_l: false, + popup_charging_r: false, + popup_charging_c: false, + popup_name: "AirPods".to_string(), + animation_frames: Arc::new(frames), }, Task::batch(vec![open_task, wait_task]), ) @@ -229,13 +285,23 @@ impl App { fn update(&mut self, message: Message) -> Task { match message { Message::WindowOpened(id) => { - self.window = Some(id); + if self.window.is_none() { + self.window = Some(id); + } else if self.main_window.is_none() { + self.main_window = Some(id); + } Task::none() } Message::WindowClosed(id) => { if self.window == Some(id) { self.window = None; } + if self.main_window == Some(id) { + self.main_window = None; + } + if self.popup_window == Some(id) { + self.popup_window = None; + } Task::none() } Message::Resized(event) => { @@ -253,6 +319,7 @@ impl App { "theme": self.selected_theme, "tray_text_mode": self.tray_text_mode, "stem_control": self.stem_control, + "show_popup": self.show_popup, }); debug!( "Writing settings to {}: {}", @@ -274,14 +341,14 @@ impl App { let ui_rx = Arc::clone(&self.ui_rx); let wait_task = Task::perform(wait_for_message(ui_rx), |msg| msg); debug!("Opening main window..."); - if let Some(window_id) = self.window { + if let Some(window_id) = self.main_window { Task::batch(vec![window::gain_focus(window_id), wait_task]) } else { let mut settings = window::Settings::default(); settings.min_size = Some(Size::new(400.0, 300.0)); - settings.icon = window::icon::from_file("../../assets/icon.png").ok(); + settings.icon = window::icon::from_file("assets/icon.png").ok(); let (new_window_task, open_task) = window::open(settings); - self.window = Some(new_window_task); + self.main_window = Some(new_window_task); Task::batch(vec![open_task.map(Message::WindowOpened), wait_task]) } } @@ -382,6 +449,13 @@ impl App { status.identifier == ControlCommandIdentifiers::AllowOffOption && matches!(status.value.as_slice(), [0x01]) }), + auto_anc_strength: state.control_command_status_list.iter().find_map(|status| { + if status.identifier == ControlCommandIdentifiers::AutoAncStrength { + status.value.first().copied() + } else { + None + } + }).unwrap_or(50), })); } Some(DeviceType::Nothing) => { @@ -403,7 +477,49 @@ impl App { _ => {} } - Task::batch(vec![wait_task]) + // Trigger popup on device connection (most reliable path) + let mut tasks = vec![wait_task]; + if self.show_popup && self.popup_window.is_none() { + let now = std::time::Instant::now(); + let is_suppressed = self.popup_suppressed_until.map(|until| now < until).unwrap_or(false); + if !is_suppressed { + info!("Triggering popup on device connect for {}", mac); + let device_name = { + let devices_json = std::fs::read_to_string(get_devices_path()) + .unwrap_or_else(|e| { + error!("Failed to read devices file: {}", e); + "{}".to_string() + }); + let devices_list: HashMap = + serde_json::from_str(&devices_json).unwrap_or_else(|e| { + error!("Deserialization failed: {}", e); + HashMap::new() + }); + devices_list.get(&mac).map(|d| d.name.clone()).unwrap_or_else(|| "AirPods".to_string()) + }; + self.popup_mac = Some(mac.clone()); + self.popup_name = device_name; + self.popup_battery_l = None; + self.popup_battery_r = None; + self.popup_battery_c = None; + self.popup_charging_l = false; + self.popup_charging_r = false; + self.popup_charging_c = false; + self.popup_frame = 0; + self.popup_last_tick = Some(std::time::Instant::now()); + + let mut settings = window::Settings::default(); + settings.size = Size::new(400.0, 300.0); + settings.decorations = false; + settings.transparent = true; + settings.level = window::Level::AlwaysOnTop; + + let (id, open) = window::open(settings); + self.popup_window = Some(id); + tasks.push(open.map(Message::WindowOpened)); + } + } + Task::batch(tasks) } BluetoothUIMessage::DeviceDisconnected(mac) => { let ui_rx = Arc::clone(&self.ui_rx); @@ -415,12 +531,21 @@ impl App { .retain(|device| device != &mac); self.device_states.remove(&mac); + self.is_in_ear = false; + self.popup_suppressed_until = None; if matches!(&self.selected_tab, Tab::Device(selected_mac) if selected_mac == &mac) { self.selected_tab = Tab::Device("none".to_string()); } - Task::batch(vec![wait_task]) + let mut tasks = vec![wait_task]; + if let Some(id) = self.main_window { + info!("Closing main window due to disconnect."); + self.main_window = None; + tasks.push(window::close(id)); + } + + Task::batch(tasks) } BluetoothUIMessage::AACPUIEvent(mac, event) => { let ui_rx = Arc::clone(&self.ui_rx); @@ -505,6 +630,14 @@ impl App { }); } } + ControlCommandIdentifiers::AutoAncStrength => { + let strength = status.value.first().copied().unwrap_or(50); + if let Some(DeviceState::AirPods(state)) = + self.device_states.get_mut(&mac) + { + state.auto_anc_strength = strength; + } + } _ => { debug!("Unhandled Control Command Status: {:?}", status); } @@ -513,8 +646,94 @@ impl App { if let Some(DeviceState::AirPods(state)) = self.device_states.get_mut(&mac) { - state.battery = battery_info; - debug!("Updated battery info for {}: {:?}", mac, state.battery); + let old_case_open = state.is_case_open(); + state.update_battery(&battery_info); + let new_case_open = state.is_case_open(); + + debug!("Battery update for {}: Case open: {} -> {}", mac, old_case_open, new_case_open); + + // Trigger popup if case is open and setting is enabled + let now = std::time::Instant::now(); + let is_suppressed = self.popup_suppressed_until.map(|until| now < until).unwrap_or(false); + + // Reset suppression if case is closed + if !new_case_open && old_case_open { + self.popup_suppressed_until = None; + } + + let mut tasks = vec![wait_task]; + + if self.show_popup && new_case_open && self.popup_window.is_none() && !is_suppressed && !self.is_in_ear { + info!("Triggering popup for {} (Case is open)", mac); + let (bl, br, bc) = state.get_battery_levels(); + let (cl, cr, cc) = state.get_charging_statuses(); + + self.popup_mac = Some(mac.clone()); + self.popup_name = state.device_name.clone(); + self.popup_battery_l = bl; + self.popup_battery_r = br; + self.popup_battery_c = bc; + self.popup_charging_l = cl; + self.popup_charging_r = cr; + self.popup_charging_c = cc; + self.popup_frame = 0; + self.popup_last_tick = Some(std::time::Instant::now()); + + let mut settings = window::Settings::default(); + settings.size = Size::new(400.0, 300.0); + settings.decorations = false; + settings.transparent = true; + settings.level = window::Level::AlwaysOnTop; + + let (id, open) = window::open(settings); + self.popup_window = Some(id); + tasks.push(open.map(Message::WindowOpened)); + } + + // Smart Open Main Window: If in ear and case just closed + if self.is_in_ear && !new_case_open && old_case_open && self.main_window.is_none() { + info!("AirPods in ear and case closed, opening main window."); + let mut settings = window::Settings::default(); + settings.size = Size::new(800.0, 600.0); + settings.icon = window::icon::from_file("assets/icon.png").ok(); + let (id, open) = window::open(settings); + self.main_window = Some(id); + tasks.push(open.map(Message::WindowOpened)); + } + + // Update popup data if it's already open + if let Some(_popup_id) = self.popup_window { + if self.popup_mac.as_ref() == Some(&mac) { + let (bl, br, bc) = state.get_battery_levels(); + let (cl, cr, cc) = state.get_charging_statuses(); + self.popup_battery_l = bl; + self.popup_battery_r = br; + self.popup_battery_c = bc; + self.popup_charging_l = cl; + self.popup_charging_r = cr; + self.popup_charging_c = cc; + // Reset timer so popup stays visible after battery update + self.popup_last_tick = Some(std::time::Instant::now()); + info!("Updated popup battery: L={:?} R={:?} C={:?}", bl, br, bc); + } + } + + return Task::batch(tasks); + } + } + AACPEvent::EarDetection(_, new_status) => { + debug!("UI received EarDetection status for {}: {:?}", mac, new_status); + use crate::bluetooth::aacp::EarDetectionStatus; + self.is_in_ear = new_status.iter().any(|s| *s == EarDetectionStatus::InEar); + + // Close popup if any bud is in ear + if self.popup_window.is_some() && self.is_in_ear { + info!("AirPods detected in ear, closing popup."); + if let Some(id) = self.popup_window { + self.popup_window = None; + self.popup_suppressed_until = Some(std::time::Instant::now() + std::time::Duration::from_secs(10)); + return Task::batch(vec![wait_task, window::close(id)]); + } } } _ => {} @@ -533,6 +752,61 @@ impl App { let wait_task = Task::perform(wait_for_message(ui_rx), |msg| msg); Task::batch(vec![wait_task]) } + BluetoothUIMessage::ShowPopup { mac, battery_l, battery_r, battery_c, charging_l, charging_r, charging_c } => { + let ui_rx = Arc::clone(&self.ui_rx); + let wait_task = Task::perform(wait_for_message(ui_rx), |msg| msg); + + let now = std::time::Instant::now(); + let is_suppressed = self.popup_suppressed_until.map(|until| now < until).unwrap_or(false); + + if !self.show_popup || self.popup_window.is_some() || is_suppressed { + return Task::batch(vec![wait_task]); + } + + self.popup_mac = Some(mac.clone()); + self.popup_name = { + let devices_json = std::fs::read_to_string(get_devices_path()) + .unwrap_or_else(|e| { + error!("Failed to read devices file: {}", e); + "{}".to_string() + }); + let devices_list: HashMap = + serde_json::from_str(&devices_json).unwrap_or_else(|e| { + error!("Deserialization failed: {}", e); + HashMap::new() + }); + devices_list.get(&mac).map(|d| d.name.clone()).unwrap_or_else(|| "AirPods".to_string()) + }; + self.popup_battery_l = battery_l; + self.popup_battery_r = battery_r; + self.popup_battery_c = battery_c; + self.popup_charging_l = charging_l; + self.popup_charging_r = charging_r; + self.popup_charging_c = charging_c; + + if self.popup_window.is_some() && battery_l.is_none() && battery_r.is_none() && battery_c.is_none() { + let id = self.popup_window.take().unwrap(); + return window::close(id); + } + + if self.popup_window.is_none() { + info!("Opening new popup window..."); + let mut settings = window::Settings::default(); + settings.size = Size::new(400.0, 300.0); + settings.decorations = false; + settings.transparent = true; + settings.level = window::Level::AlwaysOnTop; + + let (id, open) = window::open(settings); + self.popup_window = Some(id); + self.popup_frame = 0; + self.popup_last_tick = Some(std::time::Instant::now()); + Task::batch(vec![wait_task, open.map(Message::WindowOpened)]) + } else { + self.popup_last_tick = Some(std::time::Instant::now()); + Task::batch(vec![wait_task]) + } + } } } // Message::ShowNewDialogTab => { @@ -631,6 +905,7 @@ impl App { "theme": self.selected_theme, "tray_text_mode": self.tray_text_mode, "stem_control": self.stem_control, + "show_popup": self.show_popup, }); debug!( "Writing settings to {}: {}", @@ -647,6 +922,7 @@ impl App { "theme": self.selected_theme, "tray_text_mode": self.tray_text_mode, "stem_control": self.stem_control, + "show_popup": self.show_popup, }); debug!( "Writing settings to {}: {}", @@ -656,10 +932,61 @@ impl App { std::fs::write(app_settings_path, settings.to_string()).ok(); Task::none() } + Message::ShowPopupChanged(is_enabled) => { + self.show_popup = is_enabled; + let app_settings_path = get_app_settings_path(); + let settings = serde_json::json!({ + "theme": self.selected_theme, + "tray_text_mode": self.tray_text_mode, + "stem_control": self.stem_control, + "show_popup": self.show_popup, + }); + debug!( + "Writing settings to {}: {}", + app_settings_path.to_str().unwrap(), + settings + ); + std::fs::write(app_settings_path, settings.to_string()).ok(); + Task::none() + } + Message::Tick => { + self.popup_frame = (self.popup_frame + 1) % self.animation_frames.len(); + // Close popup if it has been open for 20 seconds without any update + if let Some(last_tick) = self.popup_last_tick { + if last_tick.elapsed().as_secs() > 10 { + if let Some(id) = self.popup_window { + self.popup_window = None; + return window::close(id); + } + } + } + Task::none() + } + Message::ClosePopup => { + if let Some(id) = self.popup_window { + self.popup_window = None; + return window::close(id); + } + Task::none() + } } } fn view(&self, _id: window::Id) -> Element<'_, Message> { + if Some(_id) == self.popup_window { + return crate::ui::popup::popup_view( + self.popup_name.clone(), + self.popup_frame, + self.animation_frames.clone(), + self.popup_battery_l, + self.popup_battery_r, + self.popup_battery_c, + self.popup_charging_l, + self.popup_charging_r, + self.popup_charging_c, + ); + } + let devices_json = std::fs::read_to_string(get_devices_path()).unwrap_or_else(|e| { error!("Failed to read devices file: {}", e); "{}".to_string() @@ -698,21 +1025,22 @@ impl App { level, if charging {"\u{1002E6}"} else {""} ) } else { - let left = b.iter().find(|x| x.component == BatteryComponent::Left) - .map(|x| x.level).unwrap_or_default(); - let right = b.iter().find(|x| x.component == BatteryComponent::Right) - .map(|x| x.level).unwrap_or_default(); - let case = b.iter().find(|x| x.component == BatteryComponent::Case) - .map(|x| x.level).unwrap_or_default(); - let left_charging = b.iter().find(|x| x.component == BatteryComponent::Left) - .map(|x| x.status == BatteryStatus::Charging).unwrap_or(false); - let right_charging = b.iter().find(|x| x.component == BatteryComponent::Right) - .map(|x| x.status == BatteryStatus::Charging).unwrap_or(false); - let case_charging = b.iter().find(|x| x.component == BatteryComponent::Case) - .map(|x| x.status == BatteryStatus::Charging).unwrap_or(false); + let format_batt = |comp: BatteryComponent| -> String { + let batt = b.iter().find(|x| x.component == comp); + match batt { + Some(x) if x.status != BatteryStatus::Disconnected => { + let charging = if x.status == BatteryStatus::Charging { "\u{1002E6}" } else { "" }; + format!("{}%{}", x.level, charging) + } + _ => "--%".to_string() + } + }; + format!( - "\u{1018E5} {}%{} \u{1018E8} {}%{} \u{100E6C} {}%{}", - left, if left_charging {"\u{1002E6}"} else {""}, right, if right_charging {"\u{1002E6}"} else {""}, case, if case_charging {"\u{1002E6}"} else {""} + "\u{1018E5} {} \u{1018E8} {} \u{100E6C} {}", + format_batt(BatteryComponent::Left), + format_batt(BatteryComponent::Right), + format_batt(BatteryComponent::Case) ) } } @@ -1115,6 +1443,43 @@ impl App { stem_control_toggle ] .spacing(12); + + let show_popup_toggle = container( + row![ + column![ + text("Show AirPods Pop-up").size(16), + text("Show a 3D animation and battery levels when AirPods case is opened nearby.").size(12).style( + |theme: &Theme| { + let mut style = text::Style::default(); + style.color = Some(theme.palette().text.scale_alpha(0.7)); + style + } + ).width(Length::Fill) + ].width(Length::Fill), + toggler(self.show_popup) + .on_toggle(move |is_enabled| { + Message::ShowPopupChanged(is_enabled) + }) + .spacing(0) + .size(20) + ] + .align_y(Center) + .spacing(12) + ) + .padding(Padding { + top: 5.0, + bottom: 5.0, + left: 18.0, + right: 18.0, + }) + .style(|theme: &Theme| { + let mut style = container::Style::default(); + style.background = Some(Background::Color(theme.palette().primary.scale_alpha(0.1))); + let mut border = Border::default(); + border.color = theme.palette().primary.scale_alpha(0.5); + style.border = border.rounded(16); + style + }); container( column![ @@ -1122,6 +1487,8 @@ impl App { Space::new().height(Length::from(20)), tray_text_mode_toggle, Space::new().height(Length::from(20)), + show_popup_toggle, + Space::new().height(Length::from(20)), controls_settings_col, ] ) @@ -1301,7 +1668,13 @@ impl App { } fn subscription(&self) -> Subscription { - window::close_events().map(Message::WindowClosed) + let mut subs = vec![ + window::close_events().map(Message::WindowClosed) + ]; + if self.popup_window.is_some() { + subs.push(iced::time::every(std::time::Duration::from_millis(33)).map(|_| Message::Tick)); + } + Subscription::batch(subs) } }