Line data Source code
1 : // SPDX-FileCopyrightText: 2022-2023 Paul Colby <git@colby.id.au> 2 : // SPDX-License-Identifier: LGPL-3.0-or-later 3 : 4 : /*! 5 : * \file 6 : * Defines the GenericAccessService and GenericAccessServicePrivate classes. 7 : */ 8 : 9 : #include <qtpokit/genericaccessservice.h> 10 : #include "genericaccessservice_p.h" 11 : 12 : #include <QtEndian> 13 : 14 : /*! 15 : * \class GenericAccessService 16 : * 17 : * The GenericAccessService class accesses the `Generic Access` service of Pokit devices. 18 : * 19 : * \cond internal 20 : * \pokitApi Pokit API 1.00 (and 0.02) claims support for the `Generic Access` (`0x1800`) service, 21 : * however the neither the Pokit Meter, nor the Pokit Pro report any support for this service, but 22 : * both report support for an undocumented `Generic Attribute` (`0x1801`) service instead. 23 : * \endcond 24 : */ 25 : 26 : /*! 27 : * Constructs a new Pokit service with \a parent. 28 : */ 29 190 : GenericAccessService::GenericAccessService(QLowEnergyController * const controller, QObject * parent) 30 190 : : AbstractPokitService(new GenericAccessServicePrivate(controller, this), parent) 31 : { 32 : 33 190 : } 34 : 35 : /*! 36 : * \cond internal 37 : * Constructs a new Pokit service with \a parent, and private implementation \a d. 38 : */ 39 0 : GenericAccessService::GenericAccessService( 40 0 : GenericAccessServicePrivate * const d, QObject * const parent) 41 0 : : AbstractPokitService(d, parent) 42 : { 43 : 44 0 : } 45 : /// \endcond 46 : 47 : /*! 48 : * Destroys this GenericAccessService object. 49 : */ 50 171 : GenericAccessService::~GenericAccessService() 51 : { 52 : 53 171 : } 54 : 55 19 : bool GenericAccessService::readCharacteristics() 56 : { 57 13 : const bool r1 = readDeviceNameCharacteristic(); 58 13 : const bool r2 = readAppearanceCharacteristic(); 59 19 : return (r1 && r2); 60 : } 61 : 62 : /*! 63 : * Read the `Generic Access` service's `Appearance` characteristic. 64 : * 65 : * Returns `true` if the read request is succesfully queued, `false` otherwise (ie if the 66 : * underlying controller it not yet connected to the Pokit device, or the device's services have 67 : * not yet been discovered). 68 : * 69 : * Emits appearanceRead() if/when the characteristic has been read successfully. 70 : */ 71 32 : bool GenericAccessService::readAppearanceCharacteristic() 72 : { 73 : Q_D(GenericAccessService); 74 38 : return d->readCharacteristic(CharacteristicUuids::appearance); 75 : } 76 : 77 : /*! 78 : * Read the `Generic Access` service's `Device Name` characteristic. 79 : * 80 : * Returns `true` if the read request is succesfully queued, `false` otherwise (ie if the 81 : * underlying controller it not yet connected to the Pokit device, or the device's services have 82 : * not yet been discovered). 83 : * 84 : * Emits deviceNameRead() if/when the characteristic has been read successfully. 85 : */ 86 32 : bool GenericAccessService::readDeviceNameCharacteristic() 87 : { 88 : Q_D(GenericAccessService); 89 38 : return d->readCharacteristic(CharacteristicUuids::deviceName); 90 : } 91 : 92 : /*! 93 : * Returns the most recent value of the `Generic Access` services's `Appearance` characteristic. 94 : * 95 : * The returned value, if any, is from the underlying Bluetooth stack's cache. If no such value is 96 : * currently available (ie the serviceDetailsDiscovered signal has not been emitted yet), `0xFFFF` 97 : * is returned. 98 : * 99 : * \note Pokit's Bluetooth API suggests the device's `Appearance` will always be 0 aka "Unknown 100 : * Appearance", so this is probably not a very useful characteristic if you already know you are 101 : * dealing with a Pokit device. 102 : */ 103 19 : quint16 GenericAccessService::appearance() const 104 : { 105 : Q_D(const GenericAccessService); 106 : const QLowEnergyCharacteristic characteristic = 107 19 : d->getCharacteristic(CharacteristicUuids::appearance); 108 19 : return (characteristic.isValid()) ? GenericAccessServicePrivate::parseAppearance(characteristic.value()) 109 19 : : std::numeric_limits<quint16>::max(); 110 19 : } 111 : 112 : /*! 113 : * Returns the most recent value of the `Generic Access` services's `Device Name` characteristic. 114 : * 115 : * The returned value, if any, is from the underlying Bluetooth stack's cache. If no such value is 116 : * currently available (ie the serviceDetailsDiscovered signal has not been emitted yet), then a 117 : * null QString is returned. 118 : */ 119 19 : QString GenericAccessService::deviceName() const 120 : { 121 : Q_D(const GenericAccessService); 122 : const QLowEnergyCharacteristic characteristic = 123 19 : d->getCharacteristic(CharacteristicUuids::deviceName); 124 38 : return (characteristic.isValid()) ? QString::fromUtf8(characteristic.value()) : QString(); 125 19 : } 126 : 127 : /*! 128 : * Set's the Pokit device's name to \a name. 129 : * 130 : * Returns `true` if the write request was successfully queued, `false` otherwise. 131 : * 132 : * Emits deviceNameWritten() if/when the \a name has been set. 133 : */ 134 19 : bool GenericAccessService::setDeviceName(const QString &name) 135 : { 136 : Q_D(const GenericAccessService); 137 : const QLowEnergyCharacteristic characteristic = 138 19 : d->getCharacteristic(CharacteristicUuids::deviceName); 139 19 : if (!characteristic.isValid()) { 140 : return false; 141 : } 142 : 143 : const QByteArray value = name.toUtf8(); 144 0 : if (value.length() > 11) { 145 0 : qCWarning(d->lc).noquote() << tr(R"(Device name "%1" is too long (%2 > 11 bytes): 0x%3)") 146 0 : .arg(name).arg(value.length()).arg(QLatin1String(value.toHex())); 147 0 : return false; 148 : } 149 : 150 0 : d->service->writeCharacteristic(characteristic, value); 151 0 : return (d->service->error() != QLowEnergyService::ServiceError::CharacteristicWriteError); 152 19 : } 153 : 154 : 155 : /*! 156 : * \fn GenericAccessService::appearanceRead 157 : * 158 : * This signal is emitted when the `Appearance` characteristic has been read successfully. 159 : * 160 : * \see readAppearanceCharacteristic 161 : * \see appearance 162 : */ 163 : 164 : /*! 165 : * \fn GenericAccessService::deviceNameRead 166 : * 167 : * This signal is emitted when the `Device Name` characteristic has been read successfully. 168 : * 169 : * \see readDeviceName 170 : */ 171 : 172 : /*! 173 : * \fn GenericAccessService::deviceNameWritten 174 : * 175 : * This signal is emitted when the `Device Name` characteristic has been written successfully. 176 : * 177 : * \see setDeviceName 178 : */ 179 : 180 : /*! 181 : * \cond internal 182 : * \class GenericAccessServicePrivate 183 : * 184 : * The GenericAccessServicePrivate class provides private implementation for GenericAccessService. 185 : */ 186 : 187 : /*! 188 : * \internal 189 : * Constructs a new GenericAccessServicePrivate object with public implementation \a q. 190 : */ 191 130 : GenericAccessServicePrivate::GenericAccessServicePrivate( 192 190 : QLowEnergyController * controller, GenericAccessService * const q) 193 190 : : AbstractPokitServicePrivate(GenericAccessService::serviceUuid, controller, q) 194 : { 195 : 196 130 : } 197 : 198 : /*! 199 : * Parses the `Appearance` \a value. Returns `0xFFFF` if not valid. 200 : */ 201 152 : quint16 GenericAccessServicePrivate::parseAppearance(const QByteArray &value) 202 : { 203 192 : if (!checkSize(QLatin1String("Appearance"), value, 2, 2)) { 204 : return std::numeric_limits<quint16>::max(); 205 : } 206 : const quint16 appearance = qFromLittleEndian<quint16>(value.constData()); 207 126 : qCDebug(lc).noquote() << tr("Appearance: %1.").arg(appearance); 208 12 : return appearance; 209 : } 210 : 211 : /*! 212 : * Implements AbstractPokitServicePrivate::characteristicRead to parse \a value, then emit a 213 : * specialised signal, for each supported \a characteristic. 214 : */ 215 19 : void GenericAccessServicePrivate::characteristicRead(const QLowEnergyCharacteristic &characteristic, 216 : const QByteArray &value) 217 : { 218 19 : AbstractPokitServicePrivate::characteristicRead(characteristic, value); 219 : 220 : Q_Q(GenericAccessService); 221 19 : if (characteristic.uuid() == GenericAccessService::CharacteristicUuids::appearance) { 222 0 : Q_EMIT q->appearanceRead(parseAppearance(value)); 223 0 : return; 224 : } 225 : 226 19 : if (characteristic.uuid() == GenericAccessService::CharacteristicUuids::deviceName) { 227 0 : const QString deviceName = QString::fromUtf8(value); 228 0 : qCDebug(lc).noquote() << tr(R"(Device name: "%1")").arg(deviceName); 229 0 : Q_EMIT q->deviceNameRead(deviceName); 230 : return; 231 0 : } 232 : 233 59 : qCWarning(lc).noquote() << tr("Unknown characteristic read for Generic Access service") 234 25 : << serviceUuid << characteristic.name() << characteristic.uuid(); 235 : } 236 : 237 : /*! 238 : * Implements AbstractPokitServicePrivate::characteristicWritten to parse \a newValue, then emit a 239 : * specialised signal, for each supported \a characteristic. 240 : */ 241 19 : void GenericAccessServicePrivate::characteristicWritten(const QLowEnergyCharacteristic &characteristic, 242 : const QByteArray &newValue) 243 : { 244 19 : AbstractPokitServicePrivate::characteristicWritten(characteristic, newValue); 245 : 246 : Q_Q(GenericAccessService); 247 19 : if (characteristic.uuid() == GenericAccessService::CharacteristicUuids::appearance) { 248 0 : qCWarning(lc).noquote() << tr("Appearance haracteristic is read-only, but somehow written") 249 0 : << serviceUuid << characteristic.name() << characteristic.uuid(); 250 0 : return; 251 : } 252 : 253 19 : if (characteristic.uuid() == GenericAccessService::CharacteristicUuids::deviceName) { 254 0 : Q_EMIT q->deviceNameWritten(); 255 0 : return; 256 : } 257 : 258 59 : qCWarning(lc).noquote() << tr("Unknown characteristic written for Generic Access service") 259 25 : << serviceUuid << characteristic.name() << characteristic.uuid(); 260 : } 261 : 262 : /// \endcond