1
Fork 0

Add initial files

This commit is contained in:
Joshua Goins 2024-10-30 22:37:28 -04:00
commit ef35cb6f65
8 changed files with 399 additions and 0 deletions

10
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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

View 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
View 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/"
}
}