Add initial files
This commit is contained in:
commit
ef35cb6f65
8 changed files with 399 additions and 0 deletions
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# SPDX-FileCopyrightText: None
|
||||||
|
# SPDX-License-Identifier: CC0-1.0
|
||||||
|
|
||||||
|
.clang-format
|
||||||
|
/build*/
|
||||||
|
CMakeLists.txt.user
|
||||||
|
compile_commands.json
|
||||||
|
.cache
|
||||||
|
src/resources.generated.qrc
|
||||||
|
.flatpak-builder/
|
36
CMakeLists.txt
Normal file
36
CMakeLists.txt
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
project(desk_applet)
|
||||||
|
|
||||||
|
set(QT_MIN_VERSION "6.5.0")
|
||||||
|
set(KF_MIN_VERSION "6.0.0")
|
||||||
|
|
||||||
|
find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE)
|
||||||
|
set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH})
|
||||||
|
|
||||||
|
include(KDEInstallDirs)
|
||||||
|
include(KDECMakeSettings)
|
||||||
|
include(KDECompilerSettings NO_POLICY_SCOPE)
|
||||||
|
include(FeatureSummary)
|
||||||
|
|
||||||
|
find_package(KF6 ${KF6_MIN_VERSION} REQUIRED COMPONENTS
|
||||||
|
I18n
|
||||||
|
Config
|
||||||
|
)
|
||||||
|
find_package(Qt6 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS
|
||||||
|
Quick
|
||||||
|
Core
|
||||||
|
Bluetooth
|
||||||
|
)
|
||||||
|
find_package(Plasma REQUIRED)
|
||||||
|
|
||||||
|
add_library(com.redstrate.desk-control MODULE deskcontrol.cpp)
|
||||||
|
|
||||||
|
target_link_libraries(com.redstrate.desk-control
|
||||||
|
Qt6::Gui
|
||||||
|
Qt6::Bluetooth
|
||||||
|
Plasma::Plasma
|
||||||
|
KF6::I18n)
|
||||||
|
|
||||||
|
install(TARGETS com.redstrate.desk-control DESTINATION ${KDE_INSTALL_PLUGINDIR}/plasma/applets)
|
||||||
|
|
||||||
|
plasma_install_package(package com.redstrate.desk-control)
|
7
README.md
Normal file
7
README.md
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
# Tisch
|
||||||
|
|
||||||
|
This is an applet to control your [IKEA IDÅSEN](https://www.ikea.com/us/en/p/idasen-desk-sit-stand-black-dark-gray-s79280998/) desk. Still WIP but does work.
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
* [Stephen Lineker-Miller's ideasen-desk-lib](https://github.com/stephen-slm/idasen-desk-lib) for protocol reference.
|
146
deskcontrol.cpp
Normal file
146
deskcontrol.cpp
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
#include "deskcontrol.h"
|
||||||
|
|
||||||
|
#include <KLocalizedString>
|
||||||
|
#include <QBluetoothLocalDevice>
|
||||||
|
|
||||||
|
const auto DESK_UUID_KEY = QStringLiteral("DeskUUID");
|
||||||
|
|
||||||
|
const auto ADV_SVC = QBluetoothUuid("99fa0001-338a-1024-8a49-009c0215f78a");
|
||||||
|
|
||||||
|
const auto COMMAND_CHARACTERISTIC = QBluetoothUuid("99fa0002-338a-1024-8a49-009c0215f78a");
|
||||||
|
const auto POSITION_CHARACTERISTIC = QBluetoothUuid("99fa0021-338a-1024-8a49-009c0215f78a");
|
||||||
|
|
||||||
|
const auto upCommand = QByteArray::fromHex("4700");
|
||||||
|
const auto downCommand = QByteArray::fromHex("4600");
|
||||||
|
|
||||||
|
constexpr auto MAX_HEIGHT = 1.27;
|
||||||
|
constexpr auto MIN_HEIGHT = 0.62;
|
||||||
|
|
||||||
|
DeskControl::DeskControl(QObject *parent, const KPluginMetaData &data, const QVariantList &args)
|
||||||
|
: Applet(parent, data, args) {
|
||||||
|
m_localDevice.powerOn();
|
||||||
|
// If we have connected to the desk previously, connect. Otherwise discover one.
|
||||||
|
if (config().hasKey(DESK_UUID_KEY)) {
|
||||||
|
connectToDesk();
|
||||||
|
} else {
|
||||||
|
searchForDesk();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DeskControl::~DeskControl() = default;
|
||||||
|
|
||||||
|
DeskControl::DeskState DeskControl::state() const {
|
||||||
|
return DeskState::NotFound;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeskControl::up() const {
|
||||||
|
Q_ASSERT(m_commandService);
|
||||||
|
Q_ASSERT(m_commandCharacteristic.isValid());
|
||||||
|
|
||||||
|
m_commandService->writeCharacteristic(m_commandCharacteristic, upCommand);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeskControl::down() const {
|
||||||
|
Q_ASSERT(m_commandService);
|
||||||
|
Q_ASSERT(m_commandCharacteristic.isValid());
|
||||||
|
|
||||||
|
m_commandService->writeCharacteristic(m_commandCharacteristic, downCommand);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeskControl::connectToDesk() {
|
||||||
|
const auto lastDeskUUID = config().readEntry(DESK_UUID_KEY);
|
||||||
|
qInfo() << "Connecting to desk:" << lastDeskUUID;
|
||||||
|
|
||||||
|
m_controller = QLowEnergyController::createCentral(QBluetoothDeviceInfo(QBluetoothAddress(lastDeskUUID), QStringLiteral("Desk"), 0));
|
||||||
|
|
||||||
|
connect(m_controller, &QLowEnergyController::connected, this, [this]() {
|
||||||
|
m_controller->discoverServices();
|
||||||
|
});
|
||||||
|
connect(m_controller, &QLowEnergyController::discoveryFinished, this, [this]() {
|
||||||
|
for (auto service: m_controller->services()) {
|
||||||
|
if (service.toString() == QStringLiteral("{99fa0001-338a-1024-8a49-009c0215f78a}")) {
|
||||||
|
m_commandService = m_controller->createServiceObject(service);
|
||||||
|
m_commandService->discoverDetails();
|
||||||
|
|
||||||
|
connect(m_commandService, &QLowEnergyService::stateChanged, this, [this](QLowEnergyService::ServiceState state) {
|
||||||
|
m_commandCharacteristic = m_commandService->characteristic(COMMAND_CHARACTERISTIC);
|
||||||
|
});
|
||||||
|
connect(m_commandService, &QLowEnergyService::errorOccurred, [](QLowEnergyService::ServiceError error) {
|
||||||
|
qWarning() << error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (service.toString() == QStringLiteral("{99fa0020-338a-1024-8a49-009c0215f78a}")) {
|
||||||
|
m_positionCommandService = m_controller->createServiceObject(service);
|
||||||
|
m_positionCommandService->discoverDetails();
|
||||||
|
|
||||||
|
connect(m_positionCommandService, &QLowEnergyService::errorOccurred, [](QLowEnergyService::ServiceError error) {
|
||||||
|
qWarning() << error;
|
||||||
|
});
|
||||||
|
connect(m_positionCommandService, &QLowEnergyService::stateChanged, this, [this](QLowEnergyService::ServiceState state) {
|
||||||
|
QLowEnergyCharacteristic batteryLevel = m_positionCommandService->characteristic(POSITION_CHARACTERISTIC);
|
||||||
|
if (!batteryLevel.isValid())
|
||||||
|
return;
|
||||||
|
|
||||||
|
QLowEnergyDescriptor notification = batteryLevel.descriptor(
|
||||||
|
QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration);
|
||||||
|
if (!notification.isValid())
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_positionCommandService->writeDescriptor(notification, QByteArray::fromHex("0100"));
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(m_positionCommandService, &QLowEnergyService::characteristicChanged, this, [this](const QLowEnergyCharacteristic &info, const QByteArray &value) {
|
||||||
|
const int32_t highByte = value.at(1);
|
||||||
|
const int32_t lowByte = value.at(0);
|
||||||
|
const int32_t number = (highByte << 8) + lowByte;
|
||||||
|
|
||||||
|
m_height = (static_cast<float>(number) / 1000.0) + MIN_HEIGHT;
|
||||||
|
Q_EMIT heightChanged();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
m_controller->connectToDevice();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeskControl::searchForDesk() {
|
||||||
|
qInfo() << "Searching for desk...";
|
||||||
|
|
||||||
|
QList<QBluetoothAddress> remotes = m_localDevice.connectedDevices();
|
||||||
|
if (remotes.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto &address : remotes) {
|
||||||
|
auto controller = QLowEnergyController::createCentral(QBluetoothDeviceInfo(address, QStringLiteral("Desk"), 0));
|
||||||
|
|
||||||
|
qInfo() << controller->services();
|
||||||
|
|
||||||
|
connect(controller, &QLowEnergyController::connected, this, [this, controller]() {
|
||||||
|
controller->discoverServices();
|
||||||
|
});
|
||||||
|
connect(controller, &QLowEnergyController::discoveryFinished, this, [this, controller, address]() {
|
||||||
|
controller->deleteLater();
|
||||||
|
|
||||||
|
for (auto service: controller->services()) {
|
||||||
|
if (service == ADV_SVC) {
|
||||||
|
qInfo() << "Found desk:" << DESK_UUID_KEY;
|
||||||
|
config().writeEntry(DESK_UUID_KEY, address.toString());
|
||||||
|
connectToDesk();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
controller->connectToDevice();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
K_PLUGIN_CLASS(DeskControl)
|
||||||
|
|
||||||
|
#include "deskcontrol.moc"
|
||||||
|
#include "moc_deskcontrol.cpp"
|
50
deskcontrol.h
Normal file
50
deskcontrol.h
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Plasma/Applet>
|
||||||
|
|
||||||
|
#include <QBluetoothDeviceDiscoveryAgent>
|
||||||
|
#include <QBluetoothLocalDevice>
|
||||||
|
#include <QLowEnergyController>
|
||||||
|
|
||||||
|
class DeskControl : public Plasma::Applet
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_PROPERTY(DeskState state READ state NOTIFY stateChanged)
|
||||||
|
Q_PROPERTY(qreal height MEMBER m_height NOTIFY heightChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit DeskControl(QObject *parent, const KPluginMetaData &data, const QVariantList &args);
|
||||||
|
~DeskControl() override;
|
||||||
|
|
||||||
|
enum DeskState {
|
||||||
|
NotFound,
|
||||||
|
Searching,
|
||||||
|
Connected
|
||||||
|
};
|
||||||
|
Q_ENUM(DeskState)
|
||||||
|
|
||||||
|
DeskState state() const;
|
||||||
|
|
||||||
|
public Q_SLOTS:
|
||||||
|
void up() const;
|
||||||
|
void down() const;
|
||||||
|
void connectToDesk();
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void stateChanged();
|
||||||
|
void heightChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void searchForDesk();
|
||||||
|
|
||||||
|
QLowEnergyController *m_controller = nullptr;
|
||||||
|
QLowEnergyService *m_commandService = nullptr;
|
||||||
|
QLowEnergyService *m_positionCommandService = nullptr;
|
||||||
|
qreal m_height = 0.0;
|
||||||
|
QLowEnergyCharacteristic m_commandCharacteristic;
|
||||||
|
QBluetoothDeviceDiscoveryAgent * m_discoveryAgent = nullptr;
|
||||||
|
QBluetoothLocalDevice m_localDevice;
|
||||||
|
};
|
66
package/contents/desk.svg
Normal file
66
package/contents/desk.svg
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 64 64"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
sodipodi:docname="desk.svg"
|
||||||
|
width="64"
|
||||||
|
height="64"
|
||||||
|
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
showgrid="true"
|
||||||
|
inkscape:zoom="8.0212426"
|
||||||
|
inkscape:cx="41.016089"
|
||||||
|
inkscape:cy="40.455079"
|
||||||
|
inkscape:window-width="1351"
|
||||||
|
inkscape:window-height="943"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="svg1">
|
||||||
|
<inkscape:grid
|
||||||
|
id="grid1"
|
||||||
|
units="px"
|
||||||
|
originx="0"
|
||||||
|
originy="0"
|
||||||
|
spacingx="1"
|
||||||
|
spacingy="1"
|
||||||
|
empcolor="#0099e5"
|
||||||
|
empopacity="0.30196078"
|
||||||
|
color="#0099e5"
|
||||||
|
opacity="0.14901961"
|
||||||
|
empspacing="5"
|
||||||
|
dotted="false"
|
||||||
|
gridanglex="30"
|
||||||
|
gridanglez="30"
|
||||||
|
visible="true" />
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<defs
|
||||||
|
id="defs3051">
|
||||||
|
<style
|
||||||
|
type="text/css"
|
||||||
|
id="current-color-scheme">
|
||||||
|
.ColorScheme-Text {
|
||||||
|
color:#232629;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<path
|
||||||
|
style="fill:currentColor"
|
||||||
|
d="M 6.0142432,21.291237 V 27.61424 H 59.205161 v -6.323003 z m 1,1 H 58.205161 v 3.323003 1 H 7.0142432 v -1 z M 11,28 h -1 v 28 h 1 z m 44,0 h -1 v 28 h 1 z"
|
||||||
|
class="ColorScheme-Text"
|
||||||
|
id="path1"
|
||||||
|
sodipodi:nodetypes="cccccccccccccccccccccc" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.8 KiB |
64
package/contents/ui/main.qml
Normal file
64
package/contents/ui/main.qml
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import org.kde.plasma.plasmoid
|
||||||
|
import org.kde.plasma.components as PlasmaComponents3
|
||||||
|
import org.kde.kirigami as Kirigami
|
||||||
|
|
||||||
|
PlasmoidItem {
|
||||||
|
fullRepresentation: StackLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
currentIndex: Plasmoid.state
|
||||||
|
|
||||||
|
// No desk discovered
|
||||||
|
Item {
|
||||||
|
Kirigami.PlaceholderMessage {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width - (Kirigami.Units.largeSpacing * 4)
|
||||||
|
|
||||||
|
text: "No Desk Connected"
|
||||||
|
explanation: "Manually connect to the desk in the Bluetooth applet."
|
||||||
|
icon.name: "network-bluetooth-inactive-symbolic"
|
||||||
|
|
||||||
|
helpfulAction: Kirigami.Action {
|
||||||
|
icon.name: "view-refresh-symbolic"
|
||||||
|
text: "Refresh"
|
||||||
|
onTriggered: Plasmoid.connectToDesk()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Searching for desk
|
||||||
|
Item {
|
||||||
|
Kirigami.LoadingPlaceholder {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width - (Kirigami.Units.largeSpacing * 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal
|
||||||
|
ColumnLayout {
|
||||||
|
Image {
|
||||||
|
source: "../desk.svg"
|
||||||
|
}
|
||||||
|
PlasmaComponents3.Label {
|
||||||
|
text: Plasmoid.height
|
||||||
|
}
|
||||||
|
PlasmaComponents3.Button {
|
||||||
|
text: "Up"
|
||||||
|
icon.name: "arrow-up-symbolic"
|
||||||
|
autoRepeat: true
|
||||||
|
onClicked: Plasmoid.up()
|
||||||
|
}
|
||||||
|
PlasmaComponents3.Button {
|
||||||
|
text: "Down"
|
||||||
|
icon.name: "arrow-down-symbolic"
|
||||||
|
autoRepeat: true
|
||||||
|
onClicked: Plasmoid.down()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
20
package/metadata.json
Normal file
20
package/metadata.json
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"KPackageStructure": "Plasma/Applet",
|
||||||
|
"X-Plasma-API-Minimum-Version": "6.0",
|
||||||
|
"KPlugin": {
|
||||||
|
"Authors": [
|
||||||
|
{
|
||||||
|
"Email": "josh@redstrate.com",
|
||||||
|
"Name": "Joshua Goins"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Category": "Utilities",
|
||||||
|
"Description": "what your app does in a few words",
|
||||||
|
"Icon": "applications-system",
|
||||||
|
"Id": "com.redstrate.desk-control",
|
||||||
|
"License": "LGPL-2.1+",
|
||||||
|
"Name": "Desk",
|
||||||
|
"Version": "1.0",
|
||||||
|
"Website": "https://plasma.kde.org/"
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue