mirror of
https://github.com/ckb-next/ckb-next.git
synced 2026-10-10 12:27:21 -04:00
899 lines
33 KiB
C++
899 lines
33 KiB
C++
#include <fcntl.h>
|
|
#include <QSet>
|
|
#include <QUrl>
|
|
#include <QMutex>
|
|
#include <QDebug>
|
|
#include "kb.h"
|
|
#include "kbmanager.h"
|
|
|
|
// All active devices
|
|
static QSet<Kb*> activeDevices;
|
|
// Active notification node paths
|
|
static QSet<QString> notifyPaths;
|
|
static QMutex notifyPathMutex;
|
|
|
|
int Kb::_frameRate = 30, Kb::_scrollSpeed = 0;
|
|
bool Kb::_dither = false, Kb::_mouseAccel = true;
|
|
|
|
static inline Kb::pollrate_t stringToPollrate(const QString& str){
|
|
if(str == QLatin1String("8 ms"))
|
|
return Kb::POLLRATE_8MS;
|
|
else if(str == QLatin1String("4 ms"))
|
|
return Kb::POLLRATE_4MS;
|
|
else if(str == QLatin1String("2 ms"))
|
|
return Kb::POLLRATE_2MS;
|
|
else if(str == QLatin1String("1 ms"))
|
|
return Kb::POLLRATE_1MS;
|
|
else if(str == QLatin1String("0.5 ms"))
|
|
return Kb::POLLRATE_05MS;
|
|
else if(str == QLatin1String("0.25 ms"))
|
|
return Kb::POLLRATE_025MS;
|
|
else if(str == QLatin1String("0.1 ms"))
|
|
return Kb::POLLRATE_01MS;
|
|
else
|
|
return Kb::POLLRATE_UNKNOWN;
|
|
}
|
|
|
|
Kb::Kb(QObject *parent, const QString& path) :
|
|
QThread(parent), features(QStringList()), pollrate(POLLRATE_UNKNOWN), maxpollrate(POLLRATE_UNKNOWN), monochrome(false), hwload(false), adjrate(false), firmware(),
|
|
batteryTimer(nullptr), batteryIcon(nullptr), showBatteryIndicator(false), devpath(path), cmdpath(path + "/cmd"), notifyPath(path + "/notify1"), macroPath(path + "/notify2"),
|
|
_currentProfile(nullptr), _currentMode(nullptr), _model(KeyMap::NO_MODEL), batteryLevel(0), batteryStatus(BatteryStatus::BATT_STATUS_UNKNOWN),
|
|
_hwProfile(nullptr), prevProfile(nullptr), prevMode(nullptr),
|
|
cmd(cmdpath), notifyNumber(1), macroNumber(2), _needsSave(false), _layout(KeyMap::NO_LAYOUT), _maxDpi(0),
|
|
deviceIdleTimer()
|
|
{
|
|
memset(iState, 0, sizeof(iState));
|
|
memset(hwLoading, 0, sizeof(hwLoading));
|
|
|
|
// Get the features, model, serial number, FW version (if available), poll rate (if available), and layout from /dev nodes
|
|
QFile ftpath(path + "/features"), mpath(path + "/model"), spath(path + "/serial"), fwpath(path + "/fwversion"), ppath(path + "/pollrate"), prodpath(path + "/productid"), hwlayoutPath(path + "/layout"), dpiPath(path + "/dpi");
|
|
if (ftpath.open(QIODevice::ReadOnly)){
|
|
QString featurestr = ftpath.read(1000).trimmed();
|
|
ftpath.close();
|
|
// Read model from features (first word: vendor, second word: product)
|
|
features = featurestr.split(" ");
|
|
if(features.length() < 2)
|
|
return;
|
|
_model = KeyMap::getModel(features[1]);
|
|
if(_model == KeyMap::NO_MODEL) {
|
|
qDebug() << "could not find valid model information:" << features[1] << "produced" << _model;
|
|
return;
|
|
}
|
|
} else {
|
|
// Bail if features aren't readable
|
|
qDebug() << "Could not open" << ftpath.fileName();
|
|
return;
|
|
}
|
|
if (features.contains("monochrome"))
|
|
monochrome = true;
|
|
if (features.contains("hwload"))
|
|
hwload = true;
|
|
if (mpath.open(QIODevice::ReadOnly)){
|
|
usbModel = mpath.read(100);
|
|
usbModel = usbModel.remove("Corsair", Qt::CaseInsensitive).remove("Gaming").remove("Keyboard").remove("Mouse").remove("Bootloader").remove("Mechanical").replace("LOW PROFILE", "LP").trimmed();
|
|
mpath.close();
|
|
}
|
|
if (usbModel == "")
|
|
usbModel = "Keyboard";
|
|
if (spath.open(QIODevice::ReadOnly)){
|
|
usbSerial = spath.read(100);
|
|
usbSerial = usbSerial.trimmed().toUpper();
|
|
spath.close();
|
|
}
|
|
if (usbSerial == "")
|
|
usbSerial = "Unknown-" + usbModel;
|
|
if (fwpath.open(QIODevice::ReadOnly)) {
|
|
firmware.parse(fwpath.read(100));
|
|
fwpath.close();
|
|
if (prodpath.open(QIODevice::ReadOnly)) {
|
|
productID = prodpath.read(4).toUShort(nullptr, 16);
|
|
// qInfo() << "ProductID of device is" << productID;
|
|
} else {
|
|
qCritical() << "could not open" << prodpath.fileName();
|
|
}
|
|
}
|
|
if (ppath.open(QIODevice::ReadOnly)){
|
|
QStringList sl = QString(ppath.read(100)).trimmed().split(QLatin1Char('\n'));
|
|
ppath.close();
|
|
if(sl.length() > 0)
|
|
pollrate = stringToPollrate(sl.at(0));
|
|
if(sl.length() > 1)
|
|
maxpollrate = stringToPollrate(sl.at(1));
|
|
|
|
if(pollrate > maxpollrate)
|
|
maxpollrate = POLLRATE_UNKNOWN;
|
|
|
|
if(features.contains("adjrate"))
|
|
adjrate = true;
|
|
}
|
|
|
|
if(hwlayoutPath.open(QIODevice::ReadOnly)){
|
|
hwlayout = hwlayoutPath.read(10);
|
|
hwlayout = hwlayout.trimmed();
|
|
hwlayoutPath.close();
|
|
}
|
|
|
|
if(dpiPath.open(QIODevice::ReadOnly)){
|
|
_maxDpi = dpiPath.read(6).trimmed().toUShort();
|
|
dpiPath.close();
|
|
}
|
|
if(!_maxDpi)
|
|
_maxDpi = 12000;
|
|
|
|
prefsPath = "Devices/" + usbSerial;
|
|
|
|
hwModeCount = (_model == KeyMap::K95) ? 3 : 1;
|
|
// Open cmd in non-blocking mode so that it doesn't lock up if nothing is reading
|
|
// (e.g. if the daemon crashed and didn't clean up the node)
|
|
int fd = open(cmdpath.toLatin1().constData(), O_WRONLY | O_NONBLOCK);
|
|
if(!cmd.open(fd, QIODevice::WriteOnly, QFileDevice::AutoCloseHandle))
|
|
return;
|
|
|
|
// Find an available notification node (if none is found, take notify1)
|
|
{
|
|
QMutexLocker locker(¬ifyPathMutex);
|
|
for(int i = 1; i < 10; i++){
|
|
QString notify = QString(path + "/notify%1").arg(i);
|
|
if(!QFile::exists(notify) && !notifyPaths.contains(notify)){
|
|
notifyNumber = i;
|
|
notifyPath = notify;
|
|
break;
|
|
}
|
|
}
|
|
notifyPaths.insert(notifyPath);
|
|
}
|
|
cmd.write(QString("notifyon %1\n").arg(notifyNumber).toLatin1());
|
|
cmd.flush();
|
|
|
|
if(features.contains("battery")){
|
|
batteryIcon = new BatteryStatusTrayIcon(usbModel, this);
|
|
updateBattery();
|
|
batteryTimer = new QTimer(this);
|
|
connect(batteryTimer, &QTimer::timeout, this, &Kb::updateBattery);
|
|
connect(this, &Kb::batteryChanged, batteryIcon, &BatteryStatusTrayIcon::setBattery);
|
|
if (this->currentPerf())
|
|
connect(this, &Kb::batteryChangedLed, this->currentPerf(), &KbPerf::setBattery);
|
|
if(showBatteryIndicator)
|
|
batteryIcon->show();
|
|
batteryTimer->setInterval(10000);
|
|
batteryTimer->start();
|
|
}
|
|
|
|
// Again, find an available notification node for macro definition
|
|
// (if none is found, take notify2)
|
|
{
|
|
QMutexLocker locker(¬ifyPathMutex);
|
|
for(int i = 1; i < 10; i++){
|
|
QString notify = QString(path + "/notify%1").arg(i);
|
|
if(!QFile::exists(notify) && !notifyPaths.contains(notify)){
|
|
macroNumber = i;
|
|
macroPath = notify;
|
|
break;
|
|
}
|
|
}
|
|
notifyPaths.insert(notifyPath); ///< \todo Is adding notify2 to the notifypaths neccessary?
|
|
}
|
|
// Activate device, apply settings, and ask for hardware profile
|
|
cmd.write(QString("fps %1\n").arg(_frameRate).toLatin1());
|
|
cmd.write(QString("dither %1\n").arg(static_cast<int>(_dither)).toLatin1());
|
|
#ifdef Q_OS_MACOS
|
|
// Write ANSI/ISO flag to daemon (OSX only)
|
|
cmd.write("layout ");
|
|
cmd.write(KeyMap::isISO(_layout) ? "iso" : "ansi");
|
|
// Also OSX only: scroll speed and mouse acceleration
|
|
cmd.write(QString("accel %1\n").arg(QString(_mouseAccel ? "on" : "off")).toLatin1());
|
|
cmd.write(QString("scrollspeed %1\n").arg(_scrollSpeed).toLatin1());
|
|
#endif
|
|
cmd.write(QString("\nactive\n@%1 get :hwprofileid").arg(notifyNumber).toLatin1());
|
|
hwLoading[0] = true;
|
|
for(int i = 0; i < hwModeCount; i++){
|
|
cmd.write(QString(" mode %1 get :hwid").arg(i + 1).toLatin1());
|
|
hwLoading[i + 1] = true;
|
|
}
|
|
// Ask for current indicator and key state
|
|
cmd.write(" get :i :keys\n");
|
|
cmd.flush();
|
|
|
|
emit infoUpdated();
|
|
activeDevices.insert(this);
|
|
|
|
// Start a separate thread to read from the notification node
|
|
start();
|
|
}
|
|
|
|
Kb::~Kb(){
|
|
// Save settings first
|
|
save();
|
|
|
|
delete batteryTimer;
|
|
delete batteryIcon;
|
|
|
|
// remove the notify channel from the list of notifyPaths.
|
|
///< \todo I don't think, that notifypaths is used somewhere. So why do we have it?
|
|
/// If we do not need it, searching for an ununsed notify channel can easy be refactored to a private member function.
|
|
notifyPaths.remove(macroPath);
|
|
|
|
// Kill notification thread and remove node
|
|
activeDevices.remove(this);
|
|
// FIXME: https://github.com/ckb-next/ckb-next/pull/1011
|
|
if(QFile::exists(cmdpath) && cmd.isOpen() && notifyNumber > 0){
|
|
cmd.write(QString("idle\nnotifyoff %1\n").arg(notifyNumber).toLatin1());
|
|
// Manually flush so that the daemon closes the notify pipe and the thread can gracefully stop
|
|
cmd.flush();
|
|
}
|
|
if(!wait(1000)){
|
|
terminate();
|
|
qDebug() << "Second wait returned" << wait(1000);
|
|
}
|
|
// This does nothing if cmd isn't open.
|
|
cmd.close();
|
|
}
|
|
|
|
void Kb::frameRate(int newFrameRate){
|
|
KbManager::fps(newFrameRate);
|
|
// If the rate has changed, send to all devices
|
|
if(newFrameRate == _frameRate)
|
|
return;
|
|
_frameRate = newFrameRate;
|
|
foreach(Kb* kb, activeDevices){
|
|
kb->cmd.write(QString("fps %1\n").arg(newFrameRate).toLatin1());
|
|
kb->cmd.flush();
|
|
}
|
|
}
|
|
|
|
void Kb::layout(KeyMap::Layout newLayout, bool stop){
|
|
if(newLayout == KeyMap::NO_LAYOUT || newLayout == _layout)
|
|
return;
|
|
_layout = newLayout;
|
|
// Update the current device
|
|
this->updateLayout(stop);
|
|
}
|
|
|
|
void Kb::updateLayout(bool stop){
|
|
#ifdef Q_OS_MACOS
|
|
// Write ANSI/ISO flag to daemon (OSX only)
|
|
cmd.write("layout ");
|
|
cmd.write(KeyMap::isISO(_layout) ? "iso" : "ansi");
|
|
cmd.write("\n");
|
|
cmd.flush();
|
|
#endif
|
|
foreach(KbProfile* profile, _profiles)
|
|
profile->keyMap(getKeyMap());
|
|
if(_hwProfile && !_profiles.contains(_hwProfile))
|
|
_hwProfile->keyMap(getKeyMap());
|
|
// Stop all animations as they'll need to be restarted, only if requested
|
|
if(stop){
|
|
foreach(KbMode* mode, _currentProfile->modes())
|
|
mode->light()->close();
|
|
}
|
|
emit infoUpdated();
|
|
}
|
|
|
|
void Kb::updateBattery(){
|
|
cmd.write(QString("@%1 get :battery\n").arg(notifyNumber).toLatin1());
|
|
cmd.flush();
|
|
}
|
|
|
|
void Kb::dither(bool newDither){
|
|
if(newDither == _dither)
|
|
return;
|
|
_dither = newDither;
|
|
// Update all devices
|
|
foreach(Kb* kb, activeDevices){
|
|
kb->cmd.write(QString("dither %1\n").arg(static_cast<int>(newDither)).toLatin1());
|
|
kb->cmd.flush();
|
|
}
|
|
}
|
|
|
|
void Kb::mouseAccel(bool newAccel){
|
|
if(newAccel == _mouseAccel)
|
|
return;
|
|
_mouseAccel = newAccel;
|
|
#ifdef Q_OS_MACOS
|
|
// Update all devices
|
|
foreach(Kb* kb, activeDevices){
|
|
kb->cmd.write(QString("accel %1\n").arg(QString(newAccel ? "on" : "off")).toLatin1());
|
|
kb->cmd.flush();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void Kb::scrollSpeed(int newSpeed){
|
|
if(newSpeed == _scrollSpeed)
|
|
return;
|
|
_scrollSpeed = newSpeed;
|
|
#ifdef Q_OS_MACOS
|
|
// Update all devices
|
|
foreach(Kb* kb, activeDevices){
|
|
kb->cmd.write(QString("scrollspeed %1\n").arg(newSpeed).toLatin1());
|
|
kb->cmd.flush();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void Kb::load(){
|
|
if(prefsPath.isEmpty())
|
|
return;
|
|
_needsSave = false;
|
|
CkbSettings settings(prefsPath);
|
|
// Read profiles
|
|
KbProfile* newCurrentProfile = nullptr;
|
|
QString current = settings.value("CurrentProfile").toString().trimmed().toUpper();
|
|
foreach(QString guid, settings.value("Profiles").toString().split(" ")){
|
|
guid = guid.trimmed().toUpper();
|
|
if(guid != ""){
|
|
KbProfile* profile = new KbProfile(this, getKeyMap(), settings, guid);
|
|
_profiles.append(profile);
|
|
if(guid == current || !newCurrentProfile)
|
|
newCurrentProfile = profile;
|
|
}
|
|
}
|
|
showBatteryIndicator = settings.value("batteryIndicator", true).toBool();
|
|
if(newCurrentProfile)
|
|
setCurrentProfile(newCurrentProfile);
|
|
else {
|
|
KeyMap map = getKeyMap();
|
|
// If nothing was loaded, load the appropriate demo profile for each device
|
|
QString demoProfile(":/txt/demoprofile.conf");
|
|
if(map.model() == KeyMap::M95)
|
|
demoProfile = ":/txt/demoprofile_m95.ini";
|
|
else if(map.model() == KeyMap::K55)
|
|
demoProfile = ":/txt/demoprofile_k55.ini";
|
|
else if(map.model() == KeyMap::POLARIS)
|
|
demoProfile = ":/txt/demoprofile_polaris.ini";
|
|
else if(map.model() == KeyMap::ST100)
|
|
demoProfile = ":/txt/demoprofile_st100.ini";
|
|
else if(map.model() == KeyMap::NIGHTSWORD)
|
|
demoProfile = ":/txt/demoprofile_nightsword.ini";
|
|
else if(map.model() == KeyMap::K55PRO)
|
|
demoProfile = ":/txt/demoprofile_k55pro.ini";
|
|
else if(map.model() == KeyMap::MM700)
|
|
demoProfile = ":/txt/demoprofile_mm700.ini";
|
|
else if(map.model() == KeyMap::K55_CORE)
|
|
demoProfile = ":/txt/demoprofile_k55_core.ini";
|
|
QSettings demoSettings(demoProfile, QSettings::IniFormat, this);
|
|
CkbDemoSettings cSettings(demoSettings);
|
|
KbProfile* demo = new KbProfile(this, map, cSettings, "{BA7FC152-2D51-4C26-A7A6-A036CC93D924}");
|
|
_profiles.append(demo);
|
|
setCurrentProfile(demo);
|
|
}
|
|
|
|
emit infoUpdated();
|
|
emit profileAdded();
|
|
}
|
|
|
|
void Kb::save(){
|
|
if(prefsPath.isEmpty())
|
|
return;
|
|
_needsSave = false;
|
|
CkbSettings settings(prefsPath, true);
|
|
QString guids, currentGuid;
|
|
foreach(KbProfile* profile, _profiles){
|
|
guids.append(" " + profile->id().guidString());
|
|
if(profile == _currentProfile)
|
|
currentGuid = profile->id().guidString();
|
|
profile->save(settings);
|
|
}
|
|
settings.setValue("CurrentProfile", currentGuid);
|
|
settings.setValue("Profiles", guids.trimmed());
|
|
settings.setValue("hwLayout", KeyMap::getLayout(_layout));
|
|
settings.setValue("batteryIndicator", showBatteryIndicator);
|
|
}
|
|
|
|
void Kb::autoSave(){
|
|
if(needsSave() && !CkbSettings::isBusy())
|
|
save();
|
|
}
|
|
|
|
void Kb::hwSave(){
|
|
if(!_currentProfile)
|
|
return;
|
|
// Close active lighting (if any)
|
|
if(prevMode){
|
|
prevMode->light()->close();
|
|
deletePrevious();
|
|
}
|
|
hwProfile(_currentProfile);
|
|
_hwProfile->id().hwModified = _hwProfile->id().modified;
|
|
_hwProfile->setNeedsSave();
|
|
// Re-send the current profile from scratch to ensure consistency
|
|
writeProfileHeader();
|
|
// Make sure there are enough modes
|
|
while(_currentProfile->modeCount() < hwModeCount)
|
|
_currentProfile->append(new KbMode(this, getKeyMap()));
|
|
// Write only the base colors of each mode, no animations
|
|
for(int i = 0; i < hwModeCount; i++){
|
|
KbMode* mode = _currentProfile->modes()[i];
|
|
cmd.write(QString("\nmode %1").arg(i + 1).toLatin1());
|
|
KbLight* light = mode->light();
|
|
KbPerf* perf = mode->perf();
|
|
if(mode == _currentMode)
|
|
cmd.write(" switch");
|
|
// Write the mode name and ID
|
|
cmd.write(" name ");
|
|
cmd.write(QUrl::toPercentEncoding(mode->name()));
|
|
cmd.write(" id ");
|
|
cmd.write(mode->id().guidString().toLatin1());
|
|
cmd.write(" ");
|
|
cmd.write(mode->id().modifiedString().toLatin1());
|
|
cmd.write(" ");
|
|
// Write lighting and performance
|
|
light->base(cmd, true, monochrome);
|
|
cmd.write(" ");
|
|
perf->update(cmd, notifyNumber, true, false);
|
|
// Update mode ID
|
|
mode->id().hwModified = mode->id().modified;
|
|
mode->setNeedsSave();
|
|
}
|
|
cmd.write("\n");
|
|
|
|
// Save the profile to memory
|
|
cmd.write("hwsave\n");
|
|
cmd.flush();
|
|
}
|
|
|
|
bool Kb::needsSave() const {
|
|
if(_needsSave)
|
|
return true;
|
|
foreach(const KbProfile* profile, _profiles){
|
|
if(profile->needsSave())
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void Kb::writeProfileHeader(){
|
|
cmd.write("eraseprofile");
|
|
// Write the profile name and ID
|
|
cmd.write(" profilename ");
|
|
cmd.write(QUrl::toPercentEncoding(_currentProfile->name()));
|
|
cmd.write(" profileid ");
|
|
cmd.write(_currentProfile->id().guidString().toLatin1());
|
|
cmd.write(" ");
|
|
cmd.write(_currentProfile->id().modifiedString().toLatin1());
|
|
}
|
|
|
|
void Kb::fwUpdate(const QString& path){
|
|
fwUpdPath = path;
|
|
// Write the active command to ensure it's not ignored
|
|
cmd.write("active");
|
|
cmd.write(QString(" @%1 ").arg(notifyNumber).toLatin1());
|
|
cmd.write("fwupdate ");
|
|
cmd.write(path.toLatin1());
|
|
cmd.write("\n");
|
|
}
|
|
|
|
void Kb::frameUpdate(){
|
|
// Advance animation frame
|
|
if(!_currentMode)
|
|
return;
|
|
KbLight* light = _currentMode->light();
|
|
KbBind* bind = _currentMode->bind();
|
|
KbPerf* perf = _currentMode->perf();
|
|
if(!light->isStarted()){
|
|
// Don't do anything until the animations are started
|
|
light->open();
|
|
return;
|
|
}
|
|
|
|
// Stop animations on the previously active mode (if any)
|
|
bool changed = false;
|
|
if(prevMode != _currentMode){
|
|
if(prevMode){
|
|
prevMode->light()->close();
|
|
disconnect(prevMode, SIGNAL(destroyed()), this, SLOT(deletePrevious()));
|
|
}
|
|
prevMode = _currentMode;
|
|
connect(prevMode, SIGNAL(destroyed()), this, SLOT(deletePrevious()));
|
|
changed = true;
|
|
}
|
|
|
|
// If the profile has changed, update it
|
|
if(prevProfile != _currentProfile){
|
|
writeProfileHeader();
|
|
cmd.write(" ");
|
|
prevProfile = _currentProfile;
|
|
}
|
|
|
|
// Update current mode
|
|
int index = _currentProfile->indexOf(_currentMode);
|
|
// ckb-daemon only has 6 modes: 3 hardware, 3 non-hardware. Beyond mode six, switch back to four.
|
|
// e.g. 1, 2, 3, 4, 5, 6, 4, 5, 6, 4, 5, 6 ...
|
|
if(index >= 6)
|
|
index = 3 + index % 3;
|
|
|
|
// Send lighting/binding to driver
|
|
if(prevMode != _currentMode || changed)
|
|
cmd.write(QString("mode %1 switch ").arg(index + 1).toLatin1());
|
|
perf->applyIndicators(index, iState);
|
|
light->frameUpdate(cmd, monochrome);
|
|
bind->update(cmd, notifyNumber, changed);
|
|
perf->update(cmd, notifyNumber, changed, true);
|
|
cmd.flush();
|
|
}
|
|
|
|
void Kb::deletePrevious(){
|
|
disconnect(prevMode, SIGNAL(destroyed()), this, SLOT(deletePrevious()));
|
|
prevMode = nullptr;
|
|
}
|
|
|
|
void Kb::hwProfile(KbProfile* newHwProfile){
|
|
if(_hwProfile == newHwProfile)
|
|
return;
|
|
if(_hwProfile)
|
|
disconnect(_hwProfile, SIGNAL(destroyed()), this, SLOT(deleteHw()));
|
|
_hwProfile = newHwProfile;
|
|
if(_hwProfile)
|
|
connect(_hwProfile, SIGNAL(destroyed()), this, SLOT(deleteHw()));
|
|
}
|
|
|
|
void Kb::deleteHw(){
|
|
disconnect(_hwProfile, SIGNAL(destroyed()), this, SLOT(deleteHw()));
|
|
_hwProfile = nullptr;
|
|
}
|
|
|
|
void Kb::run(){
|
|
QFile notify(notifyPath);
|
|
// Wait a small amount of time for the node to open (100ms)
|
|
QThread::usleep(100000);
|
|
if(!notify.open(QIODevice::ReadOnly)){
|
|
// If it's still not open, try again before giving up (1s at a time, 10s total)
|
|
QThread::usleep(900000);
|
|
for(int i = 1; i < 10; i++){
|
|
if(notify.open(QIODevice::ReadOnly))
|
|
break;
|
|
QThread::sleep(1);
|
|
}
|
|
if(!notify.isOpen())
|
|
return;
|
|
}
|
|
// Read data from notification node
|
|
QByteArray line;
|
|
while(notify.isOpen() && (line = notify.readLine()).length() > 0){
|
|
QString text = QString::fromUtf8(line);
|
|
metaObject()->invokeMethod(this, "readNotify", Qt::QueuedConnection, Q_ARG(QString, text));
|
|
}
|
|
QMutexLocker locker(¬ifyPathMutex);
|
|
notifyPaths.remove(notifyPath);
|
|
qDebug() << "Notify thread returning. Read" << line.length() << "isOpen()" << notify.isOpen();
|
|
}
|
|
|
|
void Kb::readNotify(const QString& line){
|
|
QStringList components = line.trimmed().split(" ");
|
|
if(components.count() < 2)
|
|
return;
|
|
if(components[0] == "key"){
|
|
// Key event
|
|
QString key = components[1];
|
|
if(key.length() < 2)
|
|
return;
|
|
QString keyName = key.mid(1);
|
|
bool keyPressed = (key[0] == '+');
|
|
KbMode* mode = _currentMode;
|
|
if(mode){
|
|
mode->light()->animKeypress(keyName, keyPressed);
|
|
mode->bind()->keyEvent(keyName, keyPressed);
|
|
}
|
|
deviceIdleTimer.start();
|
|
} else if (components[0] == "battery"){
|
|
QStringList bComponents = components[1].split(':');
|
|
if(bComponents.length() != 2)
|
|
return;
|
|
// Convert battery values into human readable text
|
|
bool ok, ok2;
|
|
uint newBatteryLevel = bComponents[0].toUInt(&ok), newBatteryStatus = bComponents[1].toUInt(&ok2);
|
|
if(!ok || !ok2 || newBatteryStatus >= BatteryStatus::BATT_STATUS_INVALID || (batteryLevel == newBatteryLevel && batteryStatus == newBatteryStatus))
|
|
return;
|
|
batteryLevel = newBatteryLevel;
|
|
batteryStatus = static_cast<BatteryStatus>(newBatteryStatus);
|
|
emit batteryChanged(batteryLevel, batteryStatus);
|
|
emit batteryChangedLed(batteryLevel, batteryStatus);
|
|
} else if(components[0] == "i"){
|
|
// Indicator event
|
|
QString i = components[1];
|
|
if(i.length() < 2)
|
|
return;
|
|
QString iName = i.mid(1);
|
|
bool on = (i[0] == '+');
|
|
if(iName == "num")
|
|
iState[0] = on;
|
|
else if(iName == "caps")
|
|
iState[1] = on;
|
|
else if(iName == "scroll")
|
|
iState[2] = on;
|
|
} else if(components[0] == "hwprofileid"){
|
|
// Hardware profile ID
|
|
if(components.count() < 3)
|
|
return;
|
|
// Find the hardware profile in the list of profiles
|
|
QString guid = components[1];
|
|
QString modified = components[2];
|
|
KbProfile* newProfile = nullptr;
|
|
foreach(KbProfile* profile, _profiles){
|
|
if(profile->id().guid == QUuid::fromString(guid)){
|
|
newProfile = profile;
|
|
break;
|
|
}
|
|
}
|
|
// If it wasn't found, create it
|
|
if(!newProfile){
|
|
newProfile = new KbProfile(this, getKeyMap(), guid, modified);
|
|
hwLoading[0] = true;
|
|
cmd.write(QString("@%1 get :hwprofilename\n").arg(notifyNumber).toLatin1());
|
|
cmd.flush();
|
|
} else {
|
|
// If it's been updated, fetch its name
|
|
if(newProfile->id().hwModifiedString() != modified){
|
|
newProfile->id().modifiedString(modified);
|
|
newProfile->id().hwModifiedString(modified);
|
|
newProfile->setNeedsSave();
|
|
if(hwLoading[0]){
|
|
cmd.write(QString("@%1 get :hwprofilename\n").arg(notifyNumber).toLatin1());
|
|
cmd.flush();
|
|
}
|
|
} else {
|
|
hwLoading[0] = false;
|
|
}
|
|
}
|
|
hwProfile(newProfile);
|
|
emit profileAdded();
|
|
} else if(components[0] == "hwprofilename"){
|
|
// Hardware profile name
|
|
QString name = QUrl::fromPercentEncoding(components[1].toUtf8());
|
|
if(!_hwProfile || !hwLoading[0])
|
|
return;
|
|
QString oldName = _hwProfile->name();
|
|
if(!(oldName.length() >= name.length() && oldName.left(name.length()) == name)){
|
|
// Don't change the name if it's a truncated version of what we already have
|
|
_hwProfile->name(name);
|
|
emit profileRenamed();
|
|
}
|
|
} else if(components[0] == "mode"){
|
|
// Mode-specific data
|
|
if(components.count() < 4)
|
|
return;
|
|
int mode = components[1].toInt() - 1;
|
|
if(components[2] == "hwid"){
|
|
if(components.count() < 5 || mode >= HWMODE_MAX || !_hwProfile)
|
|
return;
|
|
// Hardware mode ID
|
|
QString guid = components[3];
|
|
QString modified = components[4];
|
|
// Look for this mode in the hardware profile
|
|
KbMode* hwMode = nullptr;
|
|
bool isUpdated = false;
|
|
foreach(KbMode* kbMode, _hwProfile->modes()){
|
|
if(kbMode->id().guid == QUuid::fromString(guid)){
|
|
hwMode = kbMode;
|
|
if(kbMode->id().hwModifiedString() != modified){
|
|
// Update modification time
|
|
hwMode->id().modifiedString(modified);
|
|
hwMode->id().hwModifiedString(modified);
|
|
hwMode->setNeedsSave();
|
|
isUpdated = true;
|
|
} else {
|
|
hwLoading[mode + 1] = false;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
// If it wasn't found, add it
|
|
if(!hwMode){
|
|
isUpdated = true;
|
|
hwMode = new KbMode(this, getKeyMap(), guid, modified);
|
|
_hwProfile->append(hwMode);
|
|
// If the hardware profile now contains enough modes to be added to the list, do so
|
|
if(!_profiles.contains(_hwProfile) && _hwProfile->modeCount() >= hwModeCount){
|
|
_profiles.append(_hwProfile);
|
|
_needsSave = true;
|
|
emit profileAdded();
|
|
if(!_currentProfile)
|
|
setCurrentProfile(_hwProfile);
|
|
}
|
|
}
|
|
if(hwLoading[mode + 1] && isUpdated){
|
|
// If the mode isn't in the right place, move it
|
|
int index = _hwProfile->indexOf(hwMode);
|
|
if(mode < _hwProfile->modeCount() && index != mode)
|
|
_hwProfile->move(index, mode);
|
|
// Fetch the updated data
|
|
cmd.write(QString("@%1 mode %2 get :hwname :hwrgb").arg(notifyNumber).arg(mode + 1).toLatin1());
|
|
if(isMouse())
|
|
cmd.write(" :hwdpi :hwdpisel :hwlift :hwsnap");
|
|
cmd.write("\n");
|
|
cmd.flush();
|
|
}
|
|
} else if(components[2] == "hwname"){
|
|
// Mode name - update list
|
|
if(!_hwProfile || _hwProfile->modeCount() <= mode || mode >= HWMODE_MAX || !hwLoading[mode + 1])
|
|
return;
|
|
KbMode* hwMode = _hwProfile->modes()[mode];
|
|
QString name = QUrl::fromPercentEncoding(components[3].toUtf8());
|
|
QString oldName = hwMode->name();
|
|
if(!(oldName.length() >= name.length() && oldName.left(name.length()) == name)){
|
|
// Don't change the name if it's a truncated version of what we already have
|
|
hwMode->name(name);
|
|
}
|
|
} else if(components[2] == "hwrgb"){
|
|
// RGB - set mode lighting
|
|
if(!_hwProfile || _hwProfile->modeCount() <= mode || mode >= HWMODE_MAX || !hwLoading[mode + 1])
|
|
return;
|
|
KbMode* kbmode = _hwProfile->modes()[mode];
|
|
KbLight* light = kbmode->light();
|
|
// Scan the input for colors
|
|
QColor lightColor = QColor();
|
|
for(int i = 3; i < components.count(); i++){
|
|
QString comp = components[i];
|
|
if(comp.indexOf(":") < 0){
|
|
// No ":" - single hex constant
|
|
bool ok;
|
|
int rgb = comp.toInt(&ok, 16);
|
|
if(ok)
|
|
light->color(QColor::fromRgb((QRgb)rgb));
|
|
} else {
|
|
// List of keys ("a,b:xxxxxx"). Parse color first
|
|
QStringList set = comp.split(":");
|
|
bool ok;
|
|
int rgb = set[1].toInt(&ok, 16);
|
|
if(ok){
|
|
QColor color = QColor::fromRgb((QRgb)rgb);
|
|
// Parse keys
|
|
QStringList keys = set[0].split(",");
|
|
foreach(QString key, keys){
|
|
if(key == "light")
|
|
// Extrapolate the Light key to the M-keys and Lock key, since those will be set to black on hwsave
|
|
lightColor = color;
|
|
if(key.startsWith("dpi") && key.length() > 3){
|
|
// DPI levels go to the KbPerf object instead of KbLight
|
|
int index = key.mid(3).toInt(&ok);
|
|
if(ok)
|
|
kbmode->perf()->dpiColor(index, color);
|
|
continue;
|
|
}
|
|
light->color(key, color);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if(lightColor.isValid()){
|
|
light->color("mr", lightColor);
|
|
light->color("m1", lightColor);
|
|
light->color("m2", lightColor);
|
|
if(!(this->model() == KeyMap::K70MK2 || this->model() == KeyMap::STRAFE_MK2))
|
|
light->color("m3", lightColor);
|
|
light->color("lock", lightColor);
|
|
}
|
|
} else if(components[2] == "hwdpi"){
|
|
// DPI settings
|
|
if(!_hwProfile || _hwProfile->modeCount() <= mode || mode >= HWMODE_MAX || !hwLoading[mode + 1])
|
|
return;
|
|
KbPerf* perf = _hwProfile->modes()[mode]->perf();
|
|
// Read the rest of the line as stage:x,y
|
|
foreach(QString comp, components.mid(3)){
|
|
QStringList dpi = comp.split(':');
|
|
if(dpi.length() != 2)
|
|
continue;
|
|
QStringList xy = dpi[1].split(',');
|
|
int x = 0, y = 0;
|
|
bool off = false;
|
|
if(xy.length() < 2){
|
|
// If the right side only has one parameter, set both X and Y
|
|
if(xy[0] == "off")
|
|
off = true;
|
|
else
|
|
x = y = xy[0].toInt();
|
|
} else {
|
|
x = xy[0].toInt();
|
|
y = xy[1].toInt();
|
|
}
|
|
// Set DPI for this stage
|
|
int index = dpi[0].toInt();
|
|
if(off){
|
|
perf->dpiEnabled(index, false);
|
|
// If all DPIs have been disabled, turn them back on
|
|
bool allOff = true;
|
|
for(int i = 1; i < KbPerf::DPI_COUNT; i++){
|
|
if(perf->dpiEnabled(i)){
|
|
allOff = false;
|
|
break;
|
|
}
|
|
}
|
|
if(allOff){
|
|
for(int i = 1; i < KbPerf::DPI_COUNT; i++)
|
|
perf->dpiEnabled(i, true);
|
|
}
|
|
} else {
|
|
perf->dpiEnabled(index, true);
|
|
perf->dpi(index, QPoint(x, y));
|
|
}
|
|
}
|
|
} else if(components[2] == "hwdpisel"){
|
|
// Hardware DPI selection (0...5)
|
|
if(!_hwProfile || _hwProfile->modeCount() <= mode || mode >= HWMODE_MAX || !hwLoading[mode + 1])
|
|
return;
|
|
KbPerf* perf = _hwProfile->modes()[mode]->perf();
|
|
int idx = components[3].toInt();
|
|
if(idx < 1)
|
|
idx = 1;
|
|
if(idx >= KbPerf::DPI_COUNT)
|
|
idx = KbPerf::DPI_COUNT - 1;
|
|
perf->baseDpiIdx(idx);
|
|
} else if(components[2] == "hwlift"){
|
|
// Mouse lift height (1...5)
|
|
if(!_hwProfile || _hwProfile->modeCount() <= mode || mode >= HWMODE_MAX || !hwLoading[mode + 1])
|
|
return;
|
|
KbPerf* perf = _hwProfile->modes()[mode]->perf();
|
|
perf->liftHeight((KbPerf::height)components[3].toInt());
|
|
} else if(components[3] == "hwsnap"){
|
|
// Mouse angle snapping ("on" or "off")
|
|
if(!_hwProfile || _hwProfile->modeCount() <= mode || mode >= HWMODE_MAX || !hwLoading[mode + 1])
|
|
return;
|
|
KbPerf* perf = _hwProfile->modes()[mode]->perf();
|
|
perf->angleSnap(components[3] == "on");
|
|
}
|
|
} else if(components[0] == "fwupdate"){
|
|
// Firmware update progress
|
|
if(components.count() < 3)
|
|
return;
|
|
// Make sure path is the same
|
|
if(components[1] != fwUpdPath)
|
|
return;
|
|
QString res = components[2];
|
|
if(res == "invalid" || res == "fail")
|
|
emit fwUpdateFinished(false);
|
|
else if(res == "ok")
|
|
emit fwUpdateFinished(true);
|
|
else {
|
|
// "xx/yy" indicates progress
|
|
if(!res.contains("/"))
|
|
return;
|
|
QStringList numbers = res.split("/");
|
|
emit fwUpdateProgress(numbers[0].toInt(), numbers[1].toInt());
|
|
}
|
|
}
|
|
}
|
|
|
|
KeyMap Kb::getKeyMap(){
|
|
return KeyMap(_model, _layout);
|
|
}
|
|
|
|
void Kb::setCurrentProfile(KbProfile* profile){
|
|
while(profile->modeCount() < hwModeCount)
|
|
profile->append(new KbMode(this, getKeyMap()));
|
|
|
|
emit profileAboutToChange();
|
|
_currentProfile = profile;
|
|
emit profileChanged();
|
|
// Hack to prevent crash when switching to HW mode on first start with no config file.
|
|
// It happens when called by KbWidget::on_profileBox_activated().
|
|
// The KbWidget event will re-call this after the currentMode has properly been set.
|
|
if(profile->currentMode())
|
|
setCurrentMode(profile->currentMode());
|
|
}
|
|
|
|
void Kb::setCurrentMode(KbMode* mode){
|
|
_currentProfile->currentMode(_currentMode = mode);
|
|
_needsSave = true;
|
|
|
|
if(features.contains("battery") && this->currentPerf())
|
|
connect(this, &Kb::batteryChangedLed, this->currentPerf(), &KbPerf::setBattery);
|
|
emit modeChanged();
|
|
mode->light()->forceFrameUpdate();
|
|
}
|
|
|
|
KbProfile* Kb::newProfileWithBlankMode(){
|
|
KbProfile* p = new KbProfile(this, getKeyMap());
|
|
KbMode* m = newMode();
|
|
p->append(m);
|
|
p->currentMode(m);
|
|
return p;
|
|
}
|
|
|
|
KeyMap::Layout Kb::getCurrentLayout(){
|
|
return _layout;
|
|
}
|
|
|
|
void Kb::setPollRate(const QString& poll){
|
|
cmd.write(QString("\npollrate %1\n").arg(poll).toLatin1());
|
|
}
|