FluxSand 1.0
FluxSand - Interactive Digital Hourglass
Loading...
Searching...
No Matches
comp_gui.hpp
1#pragma once
2
3#include <array>
4#include <semaphore>
5#include <string>
6#include <unordered_map>
7
8#include "comp_sand.hpp"
9#include "max7219.hpp"
10
15class CompGuiX {
16 public:
17 // NOLINTNEXTLINE
18 enum class Orientation { Landscape, Portrait };
19
20 // NOLINTNEXTLINE
21 enum class RegionID {
22 SCREEN_0_LANDSCAPE,
23 SCREEN_1_LANDSCAPE,
24 SCREEN_0_PORTRAIT,
25 SCREEN_1_PORTRAIT
26 };
27
33 explicit CompGuiX(Max7219<8>& display,
34 Orientation ori = Orientation::Portrait)
35 : display_(display), orientation_(ori) {
36 thread_ = std::thread(&CompGuiX::ThreadFun, this);
37 }
38
40 void SetOrientation(Orientation ori) { orientation_ = ori; }
41
47 void Draw(RegionID region, int value) {
48 int tens = (value / 10) % 10;
49 int ones = value % 10;
50
51 auto [baseX, baseY] = REGION_OFFSETSS[static_cast<int>(region)];
52 DrawDigit(baseX, baseY, FONT[tens], region);
53 DrawDigit(baseX + 5, baseY, FONT[ones], region); // Right-aligned
54 }
55
57 void Clear() { display_.Clear(); }
58
63 void SetGravityDegree(float gravity_angle) {
64 float deg = fmodf(
65 630.0f - gravity_angle * 180.0f / static_cast<float>(M_PI), 360.0f);
66 gravity_deg_ = deg;
67 }
68
69 bool sand_enable_ = false;
70 SandGrid grid_up_, grid_down_;
71 float gravity_deg_ = 0.0f;
72
74 void SandEnable() { sand_enable_ = true; }
75
77 void SandDisable() { sand_enable_ = false; }
78
80 void ThreadFun() {
81 std::this_thread::sleep_for(std::chrono::milliseconds(500));
82 init:
83 grid_up_.Clear();
84 grid_down_.Clear();
85
86 // Initial fill animation
87 for (int i = 0; i < 128;) {
88 if (grid_up_.AddNewSand()) {
89 i++;
90 }
91 grid_up_.StepOnce(0);
92 RenderHourglass(&grid_up_, &grid_down_);
93 std::this_thread::sleep_for(std::chrono::milliseconds(25));
94 }
95
96 // Settling phase
97 for (int i = 0; i < 16; i++) {
98 grid_up_.StepOnce(0);
99 RenderHourglass(&grid_up_, &grid_down_);
100 std::this_thread::sleep_for(std::chrono::milliseconds(25));
101 }
102
103 // Main simulation loop
104 while (1) {
105 if (sand_enable_) {
106 if (reset_) {
107 reset_ = false;
108 goto init;
109 }
110 grid_up_.StepOnce(gravity_deg_);
111 grid_down_.StepOnce(gravity_deg_);
112 RenderHourglass(&grid_up_, &grid_down_);
113 }
114 std::this_thread::sleep_for(std::chrono::milliseconds(25));
115 }
116 }
117
118 private:
119 // Hardware interface
121 Orientation orientation_;
122 bool reset_ = false;
123 std::thread thread_;
124
125 // 7-segment font definitions
126 static constexpr std::array<std::array<std::string, 7>, 10> FONT = {{
127 {"01110", "10001", "10011", "10101", "11001", "10001", "01110"}, // 0
128 {"00100", "01100", "00100", "00100", "00100", "00100", "01110"}, // 1
129 {"01110", "10001", "00001", "00010", "00100", "01000", "11111"}, // 2
130 {"01110", "10001", "00001", "00110", "00001", "10001", "01110"}, // 3
131 {"00010", "00110", "01010", "10010", "11111", "00010", "00010"}, // 4
132 {"11111", "10000", "11110", "00001", "00001", "10001", "01110"}, // 5
133 {"00110", "01000", "10000", "11110", "10001", "10001", "01110"}, // 6
134 {"11111", "00001", "00010", "00100", "01000", "01000", "01000"}, // 7
135 {"01110", "10001", "10001", "01110", "10001", "10001", "01110"}, // 8
136 {"01110", "10001", "10001", "01111", "00001", "00010", "01100"}, // 9
137 }};
138
139 // Region coordinate offsets
140 static constexpr std::array<std::pair<int, int>, 4> REGION_OFFSETSS = {{
141 {-2, 1}, // SCREEN_0_LANDSCAPE
142 {3, -3}, // SCREEN_1_LANDSCAPE
143 {0, 1}, // SCREEN_0_PORTRAIT
144 {0, 1}, // SCREEN_1_PORTRAIT
145 }};
146
154 void DrawDigit(int leftX, int topY, const std::array<std::string, 7>& bitmap,
155 RegionID region) {
156 for (int dy = 0; dy < 7; ++dy) {
157 for (int dx = 0; dx < 5; ++dx) {
158 if (bitmap[6 - dy][dx] == '1') {
159 PlotRotated45(leftX + dx, topY + dy, region, true);
160 }
161 }
162 }
163 }
164
172 void PlotRotated45(int lx, int ly, RegionID region, bool on) {
173 int row = 0, col = 0;
174
175 if (orientation_ != Orientation::Landscape) {
176 // Portrait mode transform
177 row = lx + ly;
178 col = -lx + ly;
179
180 if (region == RegionID::SCREEN_0_PORTRAIT) {
181 col += 8;
182 } else if (region == RegionID::SCREEN_1_PORTRAIT) {
183 col += 24;
184 }
185 } else {
186 // Landscape mode transform
187 row = lx - ly;
188 col = lx + ly;
189
190 if (region == RegionID::SCREEN_0_LANDSCAPE) {
191 row += 8;
192 } else if (region == RegionID::SCREEN_1_LANDSCAPE) {
193 col += 16;
194 }
195 }
196
197 if (row >= 0 && row < 16 && col >= 0 && col < 32) {
198 display_.DrawPixelMatrix2(row, col, on);
199 }
200 }
201
202 public:
204 void SetLight(uint8_t light) { display_.SetIntensity(light); }
205
207 void RenderTimeLandscape(uint8_t hour, uint8_t minute) {
208 display_.Lock();
209 Clear();
210 SetOrientation(Orientation::Landscape);
211 display_.DrawPixel(3, 7, 4, true); // Colon top
212 display_.DrawPixel(3, 4, 7, true); // Colon bottom
213 Draw(RegionID::SCREEN_0_LANDSCAPE, hour);
214 Draw(RegionID::SCREEN_1_LANDSCAPE, minute);
215 display_.Unlock();
216 }
217
219 void RenderTimeLandscapeMS(uint8_t minutes, uint8_t seconds) {
220 display_.Lock();
221 Clear();
222 SetOrientation(Orientation::Landscape);
223 if (seconds % 2 == 1) {
224 display_.DrawPixel(3, 7, 4, true);
225 display_.DrawPixel(3, 4, 7, true);
226 }
227 Draw(RegionID::SCREEN_0_LANDSCAPE, minutes);
228 Draw(RegionID::SCREEN_1_LANDSCAPE, seconds);
229 display_.Unlock();
230 }
231
233 void RenderTimePortrait(uint8_t hour, uint8_t minute) {
234 display_.Lock();
235 Clear();
236 SetOrientation(Orientation::Portrait);
237
238 // Complex colon design
239 display_.DrawPixel(4, 2, 0, true);
240 display_.DrawPixel(4, 3, 1, true);
241 display_.DrawPixel(4, 4, 2, true);
242 display_.DrawPixel(4, 2, 2, true);
243 display_.DrawPixel(4, 0, 2, true);
244 display_.DrawPixel(4, 1, 3, true);
245 display_.DrawPixel(4, 2, 4, true);
246
247 display_.DrawPixel(0, 4, 0, true);
248 display_.DrawPixel(0, 4, 1, true);
249 display_.DrawPixel(0, 4, 2, true);
250 display_.DrawPixel(0, 3, 2, true);
251 display_.DrawPixel(0, 2, 2, true);
252 display_.DrawPixel(0, 2, 3, true);
253 display_.DrawPixel(0, 2, 4, true);
254 display_.DrawPixel(0, 1, 4, true);
255 display_.DrawPixel(0, 0, 4, true);
256
257 Draw(RegionID::SCREEN_0_PORTRAIT, minute);
258 Draw(RegionID::SCREEN_1_PORTRAIT, hour);
259 display_.Unlock();
260 }
261
263 void RenderTimePortraitMS(uint8_t minutes, uint8_t seconds) {
264 display_.Lock();
265 Clear();
266 SetOrientation(Orientation::Portrait);
267
268 if (seconds % 2 == 1) {
269 display_.DrawPixel(4, 4, 0, true);
270 display_.DrawPixel(4, 4, 1, true);
271 display_.DrawPixel(4, 4, 2, true);
272 display_.DrawPixel(4, 3, 2, true);
273 display_.DrawPixel(4, 2, 2, true);
274 display_.DrawPixel(4, 2, 3, true);
275 display_.DrawPixel(4, 2, 4, true);
276 display_.DrawPixel(4, 1, 4, true);
277 display_.DrawPixel(4, 0, 4, true);
278 }
279
280 if (seconds % 2 != 1) {
281 display_.DrawPixel(0, 0, 0, true);
282 display_.DrawPixel(0, 1, 0, true);
283 display_.DrawPixel(0, 1, 1, true);
284 display_.DrawPixel(0, 1, 2, true);
285 display_.DrawPixel(0, 2, 2, true);
286 }
287
288 Draw(RegionID::SCREEN_0_PORTRAIT, seconds);
289 Draw(RegionID::SCREEN_1_PORTRAIT, minutes);
290 display_.Unlock();
291 }
292
293 void RenderHumidity(uint8_t humidity) {
294 static constexpr bool ICON[16][16] = {
295 {0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
296 {1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
297 {1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
298 {0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0},
299 {0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0},
300 {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
301 {0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0},
302 {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
303 {0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
304 {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0},
305 {0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0},
306 {0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0},
307 {0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0},
308 {0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0},
309 {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0},
310 {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
311 };
312 display_.Lock();
313 Clear();
314 SetOrientation(Orientation::Portrait);
315 for (int i = 0; i < 2; i++) {
316 for (int j = 0; j < 2; j++) {
317 for (int k = 0; k < 8; k++) {
318 for (int l = 0; l < 8; l++) {
319 display_.DrawPixel(i * 2 + j + 4, k, l, ICON[k + i * 8][l + j * 8]);
320 }
321 }
322 }
323 }
324 Draw(RegionID::SCREEN_0_PORTRAIT, humidity);
325 display_.Unlock();
326 }
327
328 void RenderTemperature(uint8_t temperature) {
329 static constexpr bool ICON[16][16] = {
330 {0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
331 {0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0},
332 {1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0},
333 {1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0},
334 {1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0},
335 {0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0},
336 {0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0},
337 {0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0},
338 {0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0},
339 {0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0},
340 {0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0},
341 {0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0},
342 {0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0},
343 {0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0},
344 {0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0},
345 {0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
346 };
347 display_.Lock();
348 Clear();
349 SetOrientation(Orientation::Portrait);
350 for (int i = 0; i < 2; i++) {
351 for (int j = 0; j < 2; j++) {
352 for (int k = 0; k < 8; k++) {
353 for (int l = 0; l < 8; l++) {
354 display_.DrawPixel(i * 2 + j + 4, k, l, ICON[k + i * 8][l + j * 8]);
355 }
356 }
357 }
358 }
359 Draw(RegionID::SCREEN_0_PORTRAIT, temperature);
360 display_.Unlock();
361 }
362
363 void RenderHourglass(SandGrid* up, SandGrid* down) {
364 display_.Lock();
365 Clear();
366 for (int i = 0; i < 2; i++) {
367 for (int j = 0; j < 2; j++) {
368 for (int k = 0; k < 8; k++) {
369 for (int l = 0; l < 8; l++) {
370 display_.DrawPixel(i * 2 + j + 4, k, l,
371 up->GetGrid()[k + i * 8][l + j * 8]);
372 }
373 }
374 }
375 }
376
377 for (int i = 0; i < 2; i++) {
378 for (int j = 0; j < 2; j++) {
379 for (int k = 0; k < 8; k++) {
380 for (int l = 0; l < 8; l++) {
381 display_.DrawPixel(i * 2 + j, k, l,
382 down->GetGrid()[k + i * 8][l + j * 8]);
383 }
384 }
385 }
386 }
387 display_.Unlock();
388 }
389
391 void Reset() { reset_ = true; }
392
393 void RunUnitTest() {
394 std::cout << "[CompGuiX::UnitTest] Starting GUI unit test...\n";
395
396 SetLight(5); // Set medium brightness
397 SetOrientation(Orientation::Portrait);
398
399 // Test digit drawing
400 std::cout << "[Test] Draw number 42 in portrait...\n";
401 Draw(RegionID::SCREEN_0_PORTRAIT, 42);
402 std::this_thread::sleep_for(std::chrono::milliseconds(500));
403
404 std::cout << "[Test] Draw time 12:34 in portrait...\n";
405 RenderTimePortrait(12, 34);
406 std::this_thread::sleep_for(std::chrono::milliseconds(500));
407
408 std::cout << "[Test] Draw time 12:34 in landscape...\n";
409 RenderTimeLandscape(12, 34);
410 std::this_thread::sleep_for(std::chrono::milliseconds(500));
411
412 std::cout << "[Test] Render humidity icon...\n";
413 RenderHumidity(65);
414 std::this_thread::sleep_for(std::chrono::milliseconds(500));
415
416 std::cout << "[Test] Render temperature icon...\n";
417 RenderTemperature(23);
418 std::this_thread::sleep_for(std::chrono::milliseconds(500));
419
420 std::cout << "[Test] Enable sand...\n";
421 SandEnable();
422 std::this_thread::sleep_for(std::chrono::milliseconds(500));
423 std::cout << "[Test] Reset sand...\n";
424 Reset();
425 std::this_thread::sleep_for(std::chrono::milliseconds(500));
426 std::cout << "[Test] Disable sand...\n";
427 SandDisable();
428
429 std::cout << "[CompGuiX::UnitTest] ✅ Test complete.\n";
430 }
431};
LED Matrix GUI Controller with Dual Orientation Support and Sand Animation Physics.
Definition comp_gui.hpp:15
bool reset_
Reset flag.
Definition comp_gui.hpp:122
void RenderTimePortraitMS(uint8_t minutes, uint8_t seconds)
Render MM:SS with dynamic colon.
Definition comp_gui.hpp:263
void Reset()
Reset sand simulation.
Definition comp_gui.hpp:391
void RenderTimeLandscapeMS(uint8_t minutes, uint8_t seconds)
Render MM:SS with blinking colon.
Definition comp_gui.hpp:219
std::thread thread_
Animation thread.
Definition comp_gui.hpp:123
void PlotRotated45(int lx, int ly, RegionID region, bool on)
Coordinate transformation for 45° rotated displays.
Definition comp_gui.hpp:172
void SandDisable()
Disable sand simulation.
Definition comp_gui.hpp:77
CompGuiX(Max7219< 8 > &display, Orientation ori=Orientation::Portrait)
Construct with MAX7219 display reference.
Definition comp_gui.hpp:33
Orientation orientation_
Current orientation.
Definition comp_gui.hpp:121
bool sand_enable_
Sand animation toggle.
Definition comp_gui.hpp:69
void Clear()
Clear display buffer.
Definition comp_gui.hpp:57
void RenderTimePortrait(uint8_t hour, uint8_t minute)
Render HH:MM in portrait orientation.
Definition comp_gui.hpp:233
void Draw(RegionID region, int value)
Draw 2-digit number in specified region.
Definition comp_gui.hpp:47
void RenderTimeLandscape(uint8_t hour, uint8_t minute)
Render HH:MM in landscape orientation.
Definition comp_gui.hpp:207
void SandEnable()
Enable sand simulation.
Definition comp_gui.hpp:74
SandGrid grid_down_
Sand particle containers.
Definition comp_gui.hpp:70
void DrawDigit(int leftX, int topY, const std::array< std::string, 7 > &bitmap, RegionID region)
Render digit using 5x7 bitmap.
Definition comp_gui.hpp:154
void SetOrientation(Orientation ori)
Update display orientation.
Definition comp_gui.hpp:40
float gravity_deg_
Gravity direction (degrees)
Definition comp_gui.hpp:71
void SetLight(uint8_t light)
Set display brightness (0-15)
Definition comp_gui.hpp:204
void SetGravityDegree(float gravity_angle)
Set gravity direction for sand physics.
Definition comp_gui.hpp:63
void ThreadFun()
Animation thread entry point.
Definition comp_gui.hpp:80
Max7219< 8 > & display_
LED matrix driver.
Definition comp_gui.hpp:120