const FONT_8X16: [[u8; 16]; 96] = [
[
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x18, 0x3C, 0x3C, 0x3C, 0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x66, 0x66, 0x66, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x00, 0x6C, 0x6C, 0xFE, 0x6C, 0x6C, 0x6C, 0xFE, 0x6C, 0x6C, 0x00, 0x00, 0x00,
0x00,
],
[
0x18, 0x18, 0x7C, 0xC6, 0xC2, 0xC0, 0x7C, 0x06, 0x06, 0x86, 0xC6, 0x7C, 0x18, 0x18, 0x00,
0x00,
],
[
0x00, 0x00, 0x00, 0x00, 0xC2, 0xC6, 0x0C, 0x18, 0x30, 0x60, 0xC6, 0x86, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x38, 0x6C, 0x6C, 0x38, 0x76, 0xDC, 0xCC, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x30, 0x30, 0x30, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x0C, 0x18, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x18, 0x0C, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x30, 0x18, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x18, 0x30, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x00, 0x00, 0x00, 0x66, 0x3C, 0xFF, 0x3C, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x7E, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x18, 0x30, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x00, 0x00, 0x02, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC0, 0x80, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x38, 0x6C, 0xC6, 0xC6, 0xD6, 0xD6, 0xC6, 0xC6, 0x6C, 0x38, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x18, 0x38, 0x78, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x7E, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x7C, 0xC6, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC0, 0xC6, 0xFE, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x7C, 0xC6, 0x06, 0x06, 0x3C, 0x06, 0x06, 0x06, 0xC6, 0x7C, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x0C, 0x1C, 0x3C, 0x6C, 0xCC, 0xFE, 0x0C, 0x0C, 0x0C, 0x1E, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0xFE, 0xC0, 0xC0, 0xC0, 0xFC, 0x06, 0x06, 0x06, 0xC6, 0x7C, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x38, 0x60, 0xC0, 0xC0, 0xFC, 0xC6, 0xC6, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0xFE, 0xC6, 0x06, 0x06, 0x0C, 0x18, 0x30, 0x30, 0x30, 0x30, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x7C, 0xC6, 0xC6, 0xC6, 0x7C, 0xC6, 0xC6, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x7C, 0xC6, 0xC6, 0xC6, 0x7E, 0x06, 0x06, 0x06, 0x0C, 0x78, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x18, 0x18, 0x30, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x00, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x00, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x7C, 0xC6, 0xC6, 0x0C, 0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x00, 0x7C, 0xC6, 0xC6, 0xDE, 0xDE, 0xDE, 0xDC, 0xC0, 0x7C, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x10, 0x38, 0x6C, 0xC6, 0xC6, 0xFE, 0xC6, 0xC6, 0xC6, 0xC6, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0xFC, 0x66, 0x66, 0x66, 0x7C, 0x66, 0x66, 0x66, 0x66, 0xFC, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x3C, 0x66, 0xC2, 0xC0, 0xC0, 0xC0, 0xC0, 0xC2, 0x66, 0x3C, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0xF8, 0x6C, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x6C, 0xF8, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0xFE, 0x66, 0x62, 0x68, 0x78, 0x68, 0x60, 0x62, 0x66, 0xFE, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0xFE, 0x66, 0x62, 0x68, 0x78, 0x68, 0x60, 0x60, 0x60, 0xF0, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x3C, 0x66, 0xC2, 0xC0, 0xC0, 0xDE, 0xC6, 0xC6, 0x66, 0x3A, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0xC6, 0xC6, 0xC6, 0xC6, 0xFE, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x3C, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0xCC, 0xCC, 0xCC, 0x78, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0xE6, 0x66, 0x66, 0x6C, 0x78, 0x78, 0x6C, 0x66, 0x66, 0xE6, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0xF0, 0x60, 0x60, 0x60, 0x60, 0x60, 0x60, 0x62, 0x66, 0xFE, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0xC6, 0xEE, 0xFE, 0xFE, 0xD6, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0xC6, 0xE6, 0xF6, 0xFE, 0xDE, 0xCE, 0xC6, 0xC6, 0xC6, 0xC6, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x7C, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0xFC, 0x66, 0x66, 0x66, 0x7C, 0x60, 0x60, 0x60, 0x60, 0xF0, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x7C, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xD6, 0xDE, 0x7C, 0x0C, 0x0E, 0x00,
0x00,
],
[
0x00, 0x00, 0xFC, 0x66, 0x66, 0x66, 0x7C, 0x6C, 0x66, 0x66, 0x66, 0xE6, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x7C, 0xC6, 0xC6, 0x60, 0x38, 0x0C, 0x06, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0xFF, 0xDB, 0x99, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0x6C, 0x38, 0x10, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0xC6, 0xC6, 0xC6, 0xC6, 0xD6, 0xD6, 0xD6, 0xFE, 0xEE, 0x6C, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0xC6, 0xC6, 0x6C, 0x7C, 0x38, 0x38, 0x7C, 0x6C, 0xC6, 0xC6, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x66, 0x66, 0x66, 0x66, 0x3C, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0xFE, 0xC6, 0x86, 0x0C, 0x18, 0x30, 0x60, 0xC2, 0xC6, 0xFE, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x3C, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x3C, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x00, 0x80, 0xC0, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x06, 0x02, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x3C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x3C, 0x00, 0x00, 0x00,
0x00,
],
[
0x10, 0x38, 0x6C, 0xC6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00,
0x00,
],
[
0x00, 0x30, 0x18, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x0C, 0x7C, 0xCC, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0xE0, 0x60, 0x60, 0x78, 0x6C, 0x66, 0x66, 0x66, 0x66, 0x7C, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0xC6, 0xC0, 0xC0, 0xC0, 0xC6, 0x7C, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x1C, 0x0C, 0x0C, 0x3C, 0x6C, 0xCC, 0xCC, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0xC6, 0xFE, 0xC0, 0xC0, 0xC6, 0x7C, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x1C, 0x36, 0x32, 0x30, 0x78, 0x30, 0x30, 0x30, 0x30, 0x78, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x00, 0x00, 0x00, 0x76, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0x7C, 0x0C, 0xCC, 0x78,
0x00,
],
[
0x00, 0x00, 0xE0, 0x60, 0x60, 0x6C, 0x76, 0x66, 0x66, 0x66, 0x66, 0xE6, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x18, 0x18, 0x00, 0x38, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x06, 0x06, 0x00, 0x0E, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x66, 0x66, 0x3C,
0x00,
],
[
0x00, 0x00, 0xE0, 0x60, 0x60, 0x66, 0x6C, 0x78, 0x78, 0x6C, 0x66, 0xE6, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x38, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x00, 0x00, 0x00, 0xEC, 0xFE, 0xD6, 0xD6, 0xD6, 0xD6, 0xC6, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x00, 0x00, 0x00, 0xDC, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x00, 0x00, 0x00, 0xDC, 0x66, 0x66, 0x66, 0x66, 0x66, 0x7C, 0x60, 0x60, 0xF0,
0x00,
],
[
0x00, 0x00, 0x00, 0x00, 0x00, 0x76, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0x7C, 0x0C, 0x0C, 0x1E,
0x00,
],
[
0x00, 0x00, 0x00, 0x00, 0x00, 0xDC, 0x76, 0x66, 0x60, 0x60, 0x60, 0xF0, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0xC6, 0x60, 0x38, 0x0C, 0xC6, 0x7C, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x10, 0x30, 0x30, 0xFC, 0x30, 0x30, 0x30, 0x30, 0x36, 0x1C, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x00, 0x00, 0x00, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x00, 0x00, 0x00, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0x6C, 0x38, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x00, 0x00, 0x00, 0xC6, 0xC6, 0xD6, 0xD6, 0xD6, 0xFE, 0x6C, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x00, 0x00, 0x00, 0xC6, 0x6C, 0x38, 0x38, 0x38, 0x6C, 0xC6, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x00, 0x00, 0x00, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0x7E, 0x06, 0x0C, 0xF8,
0x00,
],
[
0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xCC, 0x18, 0x30, 0x60, 0xC6, 0xFE, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x0E, 0x18, 0x18, 0x18, 0x70, 0x18, 0x18, 0x18, 0x18, 0x0E, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x18, 0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x70, 0x18, 0x18, 0x18, 0x0E, 0x18, 0x18, 0x18, 0x18, 0x70, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x76, 0xDC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00,
],
[
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00,
],
];
pub fn draw_rect(
frame: &mut [u8],
width: usize,
height: usize,
x: u32,
y: u32,
w: u32,
h: u32,
r: u8,
g: u8,
b: u8,
thickness: u32,
) {
let color = [r, g, b];
let fw = width;
let fh = height;
let set_pixel = |frame: &mut [u8], px: usize, py: usize| {
if px < fw && py < fh {
let idx = (py * fw + px) * 3;
frame[idx] = color[0];
frame[idx + 1] = color[1];
frame[idx + 2] = color[2];
}
};
let x = x as usize;
let y = y as usize;
let rect_w = w as usize;
let rect_h = h as usize;
for t in 0..thickness as usize {
let ty = y.wrapping_add(t); if ty < fh {
for px in x..x.saturating_add(rect_w).min(fw) {
set_pixel(frame, px, ty);
}
}
let by = y.saturating_add(rect_h).saturating_sub(1).saturating_sub(t);
if by < fh && by != ty {
for px in x..x.saturating_add(rect_w).min(fw) {
set_pixel(frame, px, by);
}
}
let lx = x.wrapping_add(t);
if lx < fw {
for py in y..y.saturating_add(rect_h).min(fh) {
set_pixel(frame, lx, py);
}
}
let rx = x.saturating_add(rect_w).saturating_sub(1).saturating_sub(t);
if rx < fw && rx != lx {
for py in y..y.saturating_add(rect_h).min(fh) {
set_pixel(frame, rx, py);
}
}
}
}
pub fn draw_text(
frame: &mut [u8],
width: usize,
height: usize,
x: u32,
y: u32,
text: &str,
r: u8,
g: u8,
b: u8,
) {
let fw = width;
let fh = height;
for (ci, ch) in text.bytes().enumerate() {
let glyph_idx = if (32..=127).contains(&ch) {
(ch - 32) as usize
} else {
31
};
let glyph = &FONT_8X16[glyph_idx];
let cx = x as usize + ci * 8;
for row in 0..16 {
let py = y as usize + row;
if py >= fh {
break;
}
let bits = glyph[row];
for col in 0..8 {
let px = cx + col;
if px >= fw {
break;
}
if bits & (0x80 >> col) != 0 {
let idx = (py * fw + px) * 3;
frame[idx] = r;
frame[idx + 1] = g;
frame[idx + 2] = b;
}
}
}
}
}
pub fn overlay_detections(
frame: &mut [u8],
width: usize,
height: usize,
detections: &[(f32, f32, f32, f32, f32, &str)],
) {
const PALETTE: [(u8, u8, u8); 10] = [
(255, 0, 0), (0, 255, 0), (0, 128, 255), (255, 255, 0), (255, 0, 255), (0, 255, 255), (255, 128, 0), (128, 0, 255), (0, 128, 0), (128, 128, 128), ];
for &(dx, dy, dw, dh, conf, label) in detections {
let color_idx = label
.bytes()
.fold(0usize, |acc, b| acc.wrapping_add(b as usize))
% PALETTE.len();
let (r, g, b) = PALETTE[color_idx];
let bx = dx as u32;
let by = dy as u32;
let bw = dw as u32;
let bh = dh as u32;
draw_rect(frame, width, height, bx, by, bw, bh, r, g, b, 2);
let label_text = format!("{label} {conf:.2}");
let text_y = if by >= 18 { by - 18 } else { by + bh + 2 };
draw_text(frame, width, height, bx, text_y, &label_text, r, g, b);
}
}
#[derive(Clone, Debug)]
pub struct TelemetryData {
pub battery_voltage: f32,
pub battery_current: f32,
pub altitude_m: f32,
pub speed_ms: f32,
pub lat: f64,
pub lon: f64,
pub heading_deg: f32,
pub ai_detections: u32,
}
impl Default for TelemetryData {
fn default() -> Self {
Self {
battery_voltage: 0.0,
battery_current: 0.0,
altitude_m: 0.0,
speed_ms: 0.0,
lat: 0.0,
lon: 0.0,
heading_deg: 0.0,
ai_detections: 0,
}
}
}
#[derive(Clone)]
pub struct SharedTelemetry(std::sync::Arc<arc_swap::ArcSwap<TelemetryData>>);
impl SharedTelemetry {
pub fn new() -> Self {
Self(std::sync::Arc::new(arc_swap::ArcSwap::from_pointee(
TelemetryData::default(),
)))
}
pub fn from(td: TelemetryData) -> Self {
Self(std::sync::Arc::new(arc_swap::ArcSwap::from_pointee(td)))
}
pub fn store(&self, td: TelemetryData) {
self.0.store(std::sync::Arc::new(td));
}
pub fn load(&self) -> std::sync::Arc<TelemetryData> {
self.0.load_full()
}
pub fn modify<F: FnOnce(&mut TelemetryData)>(&self, f: F) {
let mut copy = (*self.load()).clone();
f(&mut copy);
self.store(copy);
}
}
impl Default for SharedTelemetry {
fn default() -> Self {
Self::new()
}
}
pub fn overlay_telemetry(frame: &mut [u8], width: usize, height: usize, telemetry: &TelemetryData) {
let white = (255u8, 255u8, 255u8);
let green = (0u8, 255u8, 0u8);
let yellow = (255u8, 255u8, 0u8);
let batt_color = if telemetry.battery_voltage > 11.5 {
green
} else if telemetry.battery_voltage > 10.5 {
yellow
} else {
(255, 0, 0)
};
let batt_str = format!(
"BAT {:.1}V {:.1}A",
telemetry.battery_voltage, telemetry.battery_current
);
draw_text(
frame,
width,
height,
4,
4,
&batt_str,
batt_color.0,
batt_color.1,
batt_color.2,
);
let alt_str = format!("ALT {:.1}m", telemetry.altitude_m);
draw_text(
frame, width, height, 4, 22, &alt_str, white.0, white.1, white.2,
);
let gps_str = format!("{:.6},{:.6}", telemetry.lat, telemetry.lon);
let gps_x = width.saturating_sub(gps_str.len() * 8 + 4) as u32;
draw_text(
frame, width, height, gps_x, 4, &gps_str, white.0, white.1, white.2,
);
let spd_str = format!(
"SPD {:.1}m/s HDG {:.0}",
telemetry.speed_ms, telemetry.heading_deg
);
let spd_y = height.saturating_sub(22) as u32;
draw_text(
frame, width, height, 4, spd_y, &spd_str, white.0, white.1, white.2,
);
let ai_str = format!("AI:{}", telemetry.ai_detections);
let ai_x = width.saturating_sub(ai_str.len() * 8 + 4) as u32;
draw_text(
frame, width, height, ai_x, spd_y, &ai_str, green.0, green.1, green.2,
);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn draw_rect_sets_border_pixels() {
let w = 32;
let h = 32;
let mut frame = vec![0u8; w * h * 3];
draw_rect(&mut frame, w, h, 4, 4, 10, 10, 255, 0, 0, 1);
let idx = (4 * w + 4) * 3;
assert_eq!(frame[idx], 255, "R at top-left corner");
assert_eq!(frame[idx + 1], 0, "G at top-left corner");
assert_eq!(frame[idx + 2], 0, "B at top-left corner");
let center_idx = (8 * w + 8) * 3;
assert_eq!(frame[center_idx], 0, "center should be black");
}
#[test]
fn draw_rect_clips_out_of_bounds() {
let w = 16;
let h = 16;
let mut frame = vec![0u8; w * h * 3];
draw_rect(&mut frame, w, h, 10, 10, 20, 20, 255, 255, 255, 2);
}
#[test]
fn draw_text_renders_pixels() {
let w = 64;
let h = 32;
let mut frame = vec![0u8; w * h * 3];
draw_text(&mut frame, w, h, 0, 0, "A", 255, 255, 255);
let has_pixel = frame.iter().any(|&v| v != 0);
assert!(has_pixel, "drawing 'A' should produce visible pixels");
}
#[test]
fn draw_text_clips_gracefully() {
let w = 4;
let h = 4;
let mut frame = vec![0u8; w * h * 3];
draw_text(&mut frame, w, h, 0, 0, "Hello World!", 255, 255, 255);
}
#[test]
fn draw_text_replaces_non_printable() {
let w = 64;
let h = 32;
let mut frame = vec![0u8; w * h * 3];
draw_text(&mut frame, w, h, 0, 0, "\x01", 255, 255, 255);
let has_pixel = frame.iter().any(|&v| v != 0);
assert!(
has_pixel,
"non-printable should render as '?' with visible pixels"
);
}
#[test]
fn overlay_detections_draws_boxes() {
let w = 128;
let h = 128;
let mut frame = vec![0u8; w * h * 3];
let dets = [(10.0f32, 10.0, 50.0, 50.0, 0.95, "person")];
overlay_detections(&mut frame, w, h, &dets);
let nonzero_count = frame.iter().filter(|&&v| v != 0).count();
assert!(
nonzero_count > 0,
"detection overlay should produce visible pixels"
);
}
#[test]
fn overlay_telemetry_renders_text() {
let w = 320;
let h = 240;
let mut frame = vec![0u8; w * h * 3];
let telemetry = TelemetryData {
battery_voltage: 12.4,
battery_current: 2.1,
altitude_m: 45.5,
speed_ms: 8.2,
lat: 55.753215,
lon: 37.622504,
heading_deg: 270.0,
ai_detections: 3,
};
overlay_telemetry(&mut frame, w, h, &telemetry);
let nonzero_count = frame.iter().filter(|&&v| v != 0).count();
assert!(
nonzero_count > 100,
"telemetry overlay should produce many visible pixels"
);
}
#[test]
fn font_table_dimensions() {
assert_eq!(FONT_8X16.len(), 96);
for glyph in &FONT_8X16 {
assert_eq!(glyph.len(), 16);
}
}
#[test]
fn shared_telemetry_default_zero() {
let st = SharedTelemetry::new();
let snap = st.load();
assert_eq!(snap.battery_voltage, 0.0);
assert_eq!(snap.lat, 0.0);
assert_eq!(snap.ai_detections, 0);
}
#[test]
fn shared_telemetry_atomic_swap_observed() {
let st = SharedTelemetry::new();
st.store(TelemetryData {
lat: 55.7558,
lon: 37.6173,
battery_voltage: 12.4,
..TelemetryData::default()
});
let snap = st.load();
assert!((snap.lat - 55.7558).abs() < 1e-9);
assert!((snap.lon - 37.6173).abs() < 1e-9);
assert!((snap.battery_voltage - 12.4).abs() < 1e-3);
}
#[test]
fn shared_telemetry_no_torn_read_under_contention() {
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;
let st = Arc::new(SharedTelemetry::new());
let stop = Arc::new(AtomicBool::new(false));
let observed_torn = Arc::new(AtomicBool::new(false));
let st_w = Arc::clone(&st);
let stop_w = Arc::clone(&stop);
let writer = thread::spawn(move || {
let mut sign = 1.0f64;
while !stop_w.load(Ordering::Relaxed) {
st_w.store(TelemetryData {
lat: sign,
lon: sign,
..TelemetryData::default()
});
sign = -sign;
}
});
for _ in 0..200_000 {
let snap = st.load();
if (snap.lat - snap.lon).abs() > 1e-9 {
observed_torn.store(true, Ordering::Relaxed);
break;
}
}
stop.store(true, Ordering::Relaxed);
writer.join().unwrap();
assert!(
!observed_torn.load(Ordering::Relaxed),
"torn read observed — atomic snapshot broken"
);
}
#[test]
fn shared_telemetry_modify_rmw() {
let st = SharedTelemetry::new();
st.modify(|td| {
td.battery_voltage = 11.5;
td.ai_detections = 7;
});
let snap = st.load();
assert!((snap.battery_voltage - 11.5).abs() < 1e-3);
assert_eq!(snap.ai_detections, 7);
}
}