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