//===========================================================================// // L4KEYLIGHT.cpp - Windows Dynamic Lighting keyboard mirror. // // COMPILED APART FROM THE ENGINE: /std:c++17, DEFAULT struct packing, // conformance mode (see the per-file settings in Munga_L4.vcxproj). // No engine headers may be included here - the engine compiles /Zp1 // and its types would take a different layout in this translation // unit. The l4keylight.h interface is scalars only for that reason. // // Everything WinRT runs on a private worker thread: device watcher, // keyboard claiming, and a 100 ms paint loop that mirrors the lamp // bytes (same flash formula as the on-screen cockpit buttons in // L4MFDVIEW, so the board and the screen blink in step). //===========================================================================// #include #include #include #include #include #include #include #include #include #include #include #include #include #include #pragma comment(lib, "windowsapp.lib") #include "l4keylight.h" namespace { using namespace winrt; using namespace winrt::Windows::Devices::Enumeration; using namespace winrt::Windows::Devices::Lights; using winrt::Windows::UI::Color; using winrt::Windows::System::VirtualKey; struct KeyEntry { int virtualKey; int address; bool yellow; }; struct ClaimedArray { hstring id; LampArray array{ nullptr }; bool perKey = false; bool baseCoated = false; }; std::mutex gLock; std::vector gMap; unsigned char gLamps[64] = {}; void (*gLogger)(const char *) = nullptr; std::atomic gRunning{ false }; std::thread gWorker; void Log(const char *line) { void (*logger)(const char *); { std::lock_guard hold(gLock); logger = gLogger; } if (logger != nullptr) { logger(line); } } void Logf(const char *format, ...) { char line[256]; va_list args; va_start(args, format); _vsnprintf_s(line, sizeof(line), _TRUNCATE, format, args); va_end(args); Log(line); } //--------------------------------------------------------------- // Lamp byte -> brightness level 0-3, animating the flash modes. // Identical to LampLevel in L4MFDVIEW.cpp so the keyboard and the // on-screen buttons blink together. //--------------------------------------------------------------- int LampLevel(int lamp_state) { int mode = lamp_state & 0x03; int level1 = (lamp_state >> 2) & 0x03; int level2 = (lamp_state >> 4) & 0x03; if (mode == 0) { return level1; } static const int half_period[4] = { 0, 500, 250, 125 }; return ((GetTickCount() / half_period[mode]) & 1) ? level2 : level1; } //--------------------------------------------------------------- // The panel palette (vRIO's KeyboardLampMirror shades): red for // the banks, yellow for Secondary/Screen; the off shade keeps the // bound keys faintly visible so the board reads as a button field. //--------------------------------------------------------------- Color Shade(int level, bool yellow) { Color color; color.A = 255; if (yellow) { if (level >= 3) { color.R = 245; color.G = 210; color.B = 60; } else if (level >= 1) { color.R = 140; color.G = 118; color.B = 38; } else { color.R = 70; color.G = 60; color.B = 24; } } else { if (level >= 3) { color.R = 230; color.G = 70; color.B = 70; } else if (level >= 1) { color.R = 120; color.G = 50; color.B = 50; } else { color.R = 64; color.G = 40; color.B = 40; } } return color; } bool SameColor(const Color &a, const Color &b) { return a.A == b.A && a.R == b.R && a.G == b.G && a.B == b.B; } //--------------------------------------------------------------- // The worker: watcher + claim + paint loop //--------------------------------------------------------------- void Worker() { try { init_apartment(); } catch (...) { // apartment already set on this thread; carry on } std::mutex claimedLock; std::vector claimed; bool anySeen = false; DeviceWatcher watcher{ nullptr }; try { watcher = DeviceInformation::CreateWatcher(LampArray::GetDeviceSelector()); watcher.Added([&](DeviceWatcher const &, DeviceInformation const &info) { try { LampArray array = LampArray::FromIdAsync(info.Id()).get(); if (array == nullptr || array.LampArrayKind() != LampArrayKind::Keyboard) { return; // mice / strips / cases stay untouched } anySeen = true; ClaimedArray entry; entry.id = info.Id(); entry.array = array; entry.perKey = array.SupportsVirtualKeys(); { std::lock_guard hold(claimedLock); claimed.push_back(entry); } Logf(entry.perKey ? "KeyLight: + %ls (%d LEDs, per-key)" : "KeyLight: + %ls (%d zones - board-wide mirror)", info.Name().c_str(), (int) array.LampCount()); } catch (...) { Log("KeyLight: could not open a lamp array"); } }); watcher.Removed([&](DeviceWatcher const &, DeviceInformationUpdate const &update) { std::lock_guard hold(claimedLock); for (size_t i = 0; i < claimed.size(); ++i) { if (claimed[i].id == update.Id()) { claimed.erase(claimed.begin() + i); Log("KeyLight: keyboard disconnected"); break; } } }); watcher.Updated([](DeviceWatcher const &, DeviceInformationUpdate const &) { // required for the watcher to progress }); watcher.Start(); } catch (...) { Log("KeyLight: Dynamic Lighting unavailable on this system"); return; } // // Paint loop: 100 ms cadence, repaint only on change // std::vector lastColors; int waited = 0; while (gRunning.load()) { Sleep(50); waited += 50; if (waited < 100) { continue; } waited = 0; std::vector map; unsigned char lamps[64]; { std::lock_guard hold(gLock); map = gMap; memcpy(lamps, gLamps, sizeof(lamps)); } std::vector colors(map.size()); std::vector keys(map.size()); int bestLevel = 0; bool bestYellow = false; bool changed = (lastColors.size() != map.size()); for (size_t i = 0; i < map.size(); ++i) { int address = map[i].address; int level = (address >= 0 && address < 64) ? LampLevel(lamps[address]) : 0; if (level > bestLevel) { bestLevel = level; bestYellow = map[i].yellow; } colors[i] = Shade(level, map[i].yellow); keys[i] = (VirtualKey) map[i].virtualKey; if (!changed && !SameColor(colors[i], lastColors[i])) { changed = true; } } std::lock_guard hold(claimedLock); bool freshClaim = false; for (ClaimedArray &entry : claimed) { if (!entry.baseCoated) { freshClaim = true; } } if (!changed && !freshClaim) { continue; } Color aggregate = Shade(bestLevel, bestYellow); for (ClaimedArray &entry : claimed) { try { if (entry.perKey) { if (!entry.baseCoated) { Color black; black.A = 255; black.R = 0; black.G = 0; black.B = 0; entry.array.SetColor(black); entry.baseCoated = true; } if (!map.empty()) { entry.array.SetColorsForKeys( array_view(colors.data(), colors.data() + colors.size()), array_view(keys.data(), keys.data() + keys.size())); } } else { entry.baseCoated = true; entry.array.SetColor(aggregate); } } catch (...) { // device wobble; the watcher handles removal } } lastColors = colors; } // // Releasing the arrays hands the LEDs back to Windows // try { watcher.Stop(); } catch (...) { } { std::lock_guard hold(claimedLock); claimed.clear(); } if (!anySeen) { Log("KeyLight: no Dynamic Lighting keyboard was found this session"); } } } //######################################################################## // The scalar interface (safe across the packing boundary) //######################################################################## void KeyLight_SetLogger(void (*logger)(const char *line)) { std::lock_guard hold(gLock); gLogger = logger; } void KeyLight_SetMap( const int *virtual_keys, const int *addresses, const unsigned char *yellow, int count ) { std::lock_guard hold(gLock); gMap.clear(); gMap.reserve(count); for (int i = 0; i < count; ++i) { KeyEntry entry; entry.virtualKey = virtual_keys[i]; entry.address = addresses[i]; entry.yellow = (yellow[i] != 0); gMap.push_back(entry); } } void KeyLight_UpdateLamps(const unsigned char *lamp_state, int count) { if (count > 64) { count = 64; } std::lock_guard hold(gLock); memcpy(gLamps, lamp_state, count); } void KeyLight_Start() { if (gRunning.exchange(true)) { return; } gWorker = std::thread(Worker); } void KeyLight_Stop() { if (!gRunning.exchange(false)) { return; } if (gWorker.joinable()) { gWorker.join(); } }