Dokit
Internal development documentation
Loading...
Searching...
No Matches
abstractpokitservice.cpp
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2022-2024 Paul Colby <git@colby.id.au>
2// SPDX-License-Identifier: LGPL-3.0-or-later
3
4/*!
5 * \file
6 * Defines the AbstractPokitService and AbstractPokitServicePrivate classes.
7 */
8
11#include "pokitproducts_p.h"
12
13#include <qtpokit/pokitdevice.h>
14
15#include <QLowEnergyController>
16
17/*!
18 * \class AbstractPokitService
19 *
20 * The AbstractPokitService class provides a common base for Pokit services classes.
21 */
22
23/*!
24 * \cond internal
25 * Constructs a new Pokit service with \a parent, and private implementation \a d.
26 */
27AbstractPokitService::AbstractPokitService(
28 AbstractPokitServicePrivate * const d, QObject * const parent)
29 : QObject(parent), d_ptr(d)
30{
31
32}
33/// \endcond
34
35/*!
36 * Destroys this AbstractPokitService object.
37 */
42
43/*!
44 * \fn virtual bool AbstractPokitService::readCharacteristics() = 0
45 *
46 * Read all characteristics.
47 *
48 * This convenience function will queue refresh requests of all characteristics supported by this
49 * service.
50 *
51 * Relevant `*Service::*Read` signals will be emitted by derived class objects as each
52 * characteristic is successfully read.
53 */
54
55/*!
56 * Returns `true` if autodiscovery of services and service details is enabled, `false` otherwise.
57 *
58 * \see setAutoDiscover for more information on what autodiscovery provides.
59 */
61{
62 Q_D(const AbstractPokitService);
63 return d->autoDiscover;
64}
65
66/*!
67 * If \a discover is \c true, autodiscovery will be attempted.
68 *
69 * Specifically, this may resulting in automatic invocation of:
70 * * QLowEnergyController::discoverServices if/when the internal controller is connected; and
71 * * QLowEnergyService::discoverDetails if/when an internal service object is created.
72 *
73 * \see autoDiscover
74 */
76{
78 d->autoDiscover = discover;
79}
80
81/*!
82 * Returns the Pokit product this service is attached to.
83 *
84 * \see setPokitProduct
85 */
86std::optional<PokitProduct> AbstractPokitService::pokitProduct() const
87{
88 Q_D(const AbstractPokitService);
89 return d->pokitProduct;
90}
91
92/*!
93 * Sets the current Pokit \a product.
94 *
95 * This must be called to set the product before this object's BLE controller's services are discovered. If
96 * autoDiscover() is enabled, then this should be done before the controller's \c connectToDevice() is called.
97 *
98 * For example:
99 * ```
100 * Q_ASSERT(isPokitProduct(deviceInfo));
101 * auto controller = QLowEnergyController::createCentral(deviceInfo);
102 * auto service = new DsoService(controller);
103 * service->setPokitProduct(pokitProduct(deviceInfo));
104 * controller->connectToDevice();
105 * ```
106 *
107 * \see autoDiscover
108 * \see pokitProduct
109 */
111{
113 d->pokitProduct = product;
114}
115
116/*!
117 * Returns a non-const pointer to the internal service object, if any.
118 */
120{
122 return d->service;
123}
124
125/*!
126 * Returns a const pointer to the internal service object, if any.
127 */
129{
130 Q_D(const AbstractPokitService);
131 return d->service;
132}
133
134/*!
135 * \fn void AbstractPokitService::serviceDetailsDiscovered()
136 *
137 * This signal is emitted when the Pokit service details have been discovered.
138 *
139 * Once this signal has been emitted, cached characteristics values should be immediately available
140 * via derived classes' accessor functions, and refreshes can be queued via readCharacteristics()
141 * and any related read functions provided by derived classes.
142 */
143
144/*!
145 * \fn void AbstractPokitService::serviceErrorOccurred(QLowEnergyService::ServiceError newError)
146 *
147 * This signal is emitted whenever an error occurs on the underlying QLowEnergyService.
148 */
149
150/*!
151 * \cond internal
152 * \class AbstractPokitServicePrivate
153 *
154 * The AbstractPokitServicePrivate class provides private implementation for AbstractPokitService.
155 */
156
157/*!
158 * \internal
159 * Constructs a new AbstractPokitServicePrivate object with public implementation \a q.
160 *
161 * Note, typically the \a serviceUuid should be set validly, however, in the rare case that a
162 * service's UUID can vary (ie the Status Service), \a serviceUuid may be set to a `null`
163 * QBluetoothUuid here, and updated when the correct service UUID is known.
164 *
165 * \see StatusService::ServiceUuids
166 * \see StatusServicePrivate::serviceDiscovered
167 */
185
186/*!
187 * Creates an internal service object from the internal controller.
188 *
189 * Any existing service object will *not* be replaced.
190 *
191 * Returns \c true if a service was created successfully, either now, or sometime previously.
192 */
194{
195 if (!controller) {
196 return false;
197 }
198
199 if (service) {
200 qCDebug(lc).noquote() << tr("Already have service object:") << service;
201 return true;
202 }
203
204 if (serviceUuid.isNull()) {
205 qCDebug(lc).noquote() << tr("Service UUID not assigned yet.");
206 return false;
207 }
208
210 if (!service) {
211 return false;
212 }
213 qCDebug(lc).noquote() << tr("Service object created for %1 device:").arg(toString(*this->pokitProduct)) << service;
214
223
225 [](const QLowEnergyDescriptor &descriptor, const QByteArray &value){
226 qCDebug(lc).noquote() << tr(R"(Descriptor "%1" (%2) read.)")
227 .arg(descriptor.name(), descriptor.uuid().toString());
228 Q_UNUSED(value)
229 });
230
232 [](const QLowEnergyDescriptor &descriptor, const QByteArray &newValue){
233 qCDebug(lc).noquote() << tr(R"(Descriptor "%1" (%2) written.)")
234 .arg(descriptor.name(), descriptor.uuid().toString());
235 Q_UNUSED(newValue)
236 });
237
239 #if (QT_VERSION < QT_VERSION_CHECK(6, 2, 0))
240 QOverload<QLowEnergyService::ServiceError>::of(&QLowEnergyService::error),
241 #else
242 &QLowEnergyService::errorOccurred,
243 #endif
245
246 if (autoDiscover) {
248 }
249 return true;
250}
251
252/*!
253 * Get \a uuid characteristc from the underlying service. This helper function is equivalent to
254 *
255 * ```
256 * return service->characteristic(uuid);
257 * ```
258 *
259 * except that it performs some sanity checks, such as checking the service object pointer has been
260 * assigned first, and also logs failures in a consistent manner.
261 *
262 * \param uuid
263 * \return
264 */
266{
267 if (!service) {
268 qCDebug(lc).noquote() << tr(R"(Characteristic %1 "%2" requested before service assigned.)")
271 }
272
273 if (const QLowEnergyCharacteristic characteristic = service->characteristic(uuid); characteristic.isValid()) {
274 return characteristic;
275 }
276
278 #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
279 ServiceDiscovered
280 #else
281 RemoteServiceDiscovered
282 #endif
283 ) {
284 qCWarning(lc).noquote() << tr(R"(Characteristic %1 "%2" requested before service %3 "%4" discovered.)")
287 qCInfo(lc).noquote() << tr("Current service state:") << service->state();
289 }
290
291 qCWarning(lc).noquote() << tr(R"(Characteristic %1 "%2" not found in service %3 "%4".)")
295}
296
297/*!
298 * Read the \a uuid characteristic.
299 *
300 * If succesful, the `QLowEnergyService::characteristicRead` signal will be emitted by the internal
301 * service object. For convenience, derived classes should implement the characteristicRead()
302 * virtual function to handle the read value.
303 *
304 * Returns \c true if the characteristic read request was successfully queued, \c false otherwise.
305 *
306 * \see AbstractPokitService::readCharacteristics()
307 * \see AbstractPokitServicePrivate::characteristicRead()
308 */
310{
311 const QLowEnergyCharacteristic characteristic = getCharacteristic(uuid);
312 if (!characteristic.isValid()) {
313 return false;
314 }
315 qCDebug(lc).noquote() << tr(R"(Reading characteristic %1 "%2".)")
317 service->readCharacteristic(characteristic);
318 return true;
319}
320
321/*!
322 * Enables client (Pokit device) side notification for characteristic \a uuid.
323 *
324 * Returns \c true if the notication enable request was successfully queued, \c false otherwise.
325 *
326 * \see AbstractPokitServicePrivate::characteristicChanged
327 * \see AbstractPokitServicePrivate::disableCharacteristicNotificatons
328 */
330{
331 qCDebug(lc).noquote() << tr(R"(Enabling CCCD for characteristic %1 "%2".)")
333 QLowEnergyCharacteristic characteristic = getCharacteristic(uuid);
334 if (!characteristic.isValid()) {
335 return false;
336 }
337
338 QLowEnergyDescriptor descriptor = characteristic.descriptor(
339 QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration); // 0x2902
340 if (!descriptor.isValid()) {
341 qCWarning(lc).noquote() << tr(R"(Characteristic %1 "%2" has no client configuration descriptor.)")
343 return false;
344 }
345
346 service->writeDescriptor(descriptor,
347 #if (QT_VERSION >= QT_VERSION_CHECK(6, 2, 0))
348 QLowEnergyCharacteristic::CCCDEnableNotification
349 #else
350 QByteArray::fromHex("0100") // See Qt6's QLowEnergyCharacteristic::CCCDEnableNotification.
351 #endif
352 );
353 return true;
354}
355
356/*!
357 * Disables client (Pokit device) side notification for characteristic \a uuid.
358 *
359 * Returns \c true if the notication disable request was successfully queued, \c false otherwise.
360 *
361 * \see AbstractPokitServicePrivate::characteristicChanged
362 * \see AbstractPokitServicePrivate::enableCharacteristicNotificatons
363 */
365{
366 qCDebug(lc).noquote() << tr(R"(Disabling CCCD for characteristic %1 "%2".)")
368 QLowEnergyCharacteristic characteristic = getCharacteristic(uuid);
369 if (!characteristic.isValid()) {
370 return false;
371 }
372
373 QLowEnergyDescriptor descriptor = characteristic.descriptor(
374 QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration); // 0x2902
375 if (!descriptor.isValid()) {
376 qCWarning(lc).noquote() << tr(R"(Characteristic %1 "%2" has no client configuration descriptor.)")
378 return false;
379 }
380
381 service->writeDescriptor(descriptor,
382 #if (QT_VERSION >= QT_VERSION_CHECK(6, 2, 0))
383 QLowEnergyCharacteristic::CCCDDisable
384 #else
385 QByteArray::fromHex("0000") // See Qt6's QLowEnergyCharacteristic::CCCDDisable.
386 #endif
387 );
388 return true;
389}
390
391/*!
392 * Returns `false` if \a data is smaller than \a minSize, otherwise returns \a failOnMax if \a data
393 * is bigger than \a maxSize, otherwise returns `true`.
394 *
395 * A warning is logged if either \a minSize or \a maxSize is violated, regardless of the returned
396 * value; ie this funcion can be used to simply warn if \a data is too big, or it can be used to
397 * failed (return `false`) in that case.
398 */
400 const int minSize, const int maxSize,
401 const bool failOnMax)
402{
403 if (data.size() < minSize) {
404 qCWarning(lc).noquote() << tr("%1 requires %n byte/s, but only %2 present: %3", nullptr, minSize)
405 .arg(label).arg(data.size()).arg(toHexString(data));
406 return false;
407 }
408 if ((maxSize >= 0) && (data.size() > maxSize)) {
409 qCWarning(lc).noquote() << tr("%1 has %n extraneous byte/s: %2", nullptr, data.size()-maxSize)
410 .arg(label, toHexString(data.mid(maxSize)));
411 return (!failOnMax);
412 }
413 return true;
414}
415
416/*!
417 * Returns up to \a maxSize bytes of \a data as a human readable hexadecimal string. If \a data
418 * exceeds \a maxSize, then \a data is elided in the middle. For example:
419 *
420 * ```
421 * toHex(QBytArray("\x1\x2\x3\x4\x5\x6", 4); // "0x01,02,...,05,06"
422 * ```
423 */
425{
426 return (data.size() <= maxSize)
427 ? QString::fromLatin1("0x%1").arg(QLatin1String(data.toHex(',')))
428 : QString::fromLatin1("0x%1,...,%2").arg(
429 QLatin1String(data.left(maxSize/2-1).toHex(',')),
430 QLatin1String(data.right(maxSize/2-1).toHex(',')));
431}
432
433/*!
434 * Handles `QLowEnergyController::connected` events.
435 *
436 * If `autoDiscover` is enabled, this will begin service discovery on the newly connected contoller.
437 *
438 * \see AbstractPokitService::autoDiscover()
439 */
441{
442 if (!controller) {
443 qCWarning(lc).noquote() << tr("Connected with no controller set") << sender();
444 return;
445 }
446
447 qCDebug(lc).noquote() << tr(R"(Connected to "%1" (%2) at %3.)").arg(
450 if (autoDiscover) {
452 }
453}
454
455/*!
456 * Handles `QLowEnergyController::discoveryFinished` events.
457 *
458 * As this event indicates that the conroller has finished discovering services, this function will
459 * invoke createServiceObject() to create the internal service object (if not already created).
460 */
462{
463 if (!controller) {
464 qCWarning(lc).noquote() << tr("Discovery finished with no controller set") << sender();
465 return;
466 }
467
468 qCDebug(lc).noquote() << tr(R"(Discovery finished for "%1" (%2) at %3.)").arg(
471
472 if (!createServiceObject()) {
473 qCWarning(lc).noquote() << tr("Discovery finished, but service not found.");
475 Q_EMIT q->serviceErrorOccurred(QLowEnergyService::ServiceError::UnknownError);
476 }
477}
478
479/*!
480 * Handles `QLowEnergyController::errorOccurred` events.
481 *
482 * This function simply re-emits \a newError as AbstractPokitService::serviceErrorOccurred.
483 */
485{
487 qCDebug(lc).noquote() << tr("Service error") << newError;
488 Q_EMIT q->serviceErrorOccurred(newError);
489}
490
491/*!
492 * Handles `QLowEnergyController::serviceDiscovered` events.
493 *
494 * If the discovered service is the one this (or rather the derived) class wraps, then
495 * createServiceObject() will be invoked immediately (otherwise it will be invoked after full
496 * service discovery has completed, ie in discoveryFinished()).
497 */
499{
500 if ((!service) && (newService == serviceUuid)) {
501 qCDebug(lc).noquote() << tr("Service discovered") << newService;
503 }
504}
505
506/*!
507 * Handles `QLowEnergyController::stateChanged` events.
508 *
509 * If \a newState indicates that service details have now been discovered, then
510 * AbstractPokitService::serviceDetailsDiscovered will be emitted.
511 *
512 * \see AbstractPokitService::autoDiscover()
513 */
515{
516 qCDebug(lc).noquote() << tr("State changed to") << newState;
517
518 if (lc().isDebugEnabled()) {
519 for (const auto &characteristic: service->characteristics()) {
520 QStringList properties;
521 /// \cond no-doxygen
522 #define QTPOKIT_INTERNAL_TEST_AND_APPEND(property) \
523 if (characteristic.properties().testFlag(QLowEnergyCharacteristic::property)) { \
524 properties.append(QStringLiteral(#property).toLower());\
525 }
526 /// \endcond
527 QTPOKIT_INTERNAL_TEST_AND_APPEND(Broadcasting)
528 QTPOKIT_INTERNAL_TEST_AND_APPEND(Read)
529 QTPOKIT_INTERNAL_TEST_AND_APPEND(WriteNoResponse)
530 QTPOKIT_INTERNAL_TEST_AND_APPEND(Write)
531 QTPOKIT_INTERNAL_TEST_AND_APPEND(Notify)
532 QTPOKIT_INTERNAL_TEST_AND_APPEND(Indicate)
533 QTPOKIT_INTERNAL_TEST_AND_APPEND(WriteSigned)
534 QTPOKIT_INTERNAL_TEST_AND_APPEND(ExtendedProperty)
535 #undef QTPOKIT_INTERNAL_TEST_AND_APPEND
536 qCDebug(lc).noquote() << tr(R"(Characteristic %1 "%2" supports %3.)").arg(characteristic.uuid().toString(),
537 PokitDevice::charcteristicToString(characteristic.uuid()), properties.join(QStringLiteral(", ")));
538 }
539 }
540
541 if (newState == QLowEnergyService::
542 #if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
543 ServiceDiscovered
544 #else
545 RemoteServiceDiscovered
546 #endif
547 ) {
549 qCDebug(lc).noquote() << tr("Service details discovered.");
550 Q_EMIT q->serviceDetailsDiscovered();
551 }
552}
553
554/*!
555 * Handles `QLowEnergyService::characteristicRead` events. This base implementation simply debug
556 * logs the event.
557 *
558 * Derived classes should implement this function to handle the successful reads of
559 * \a characteristic, typically by parsing \a value, then emitting a speciailised signal.
560 */
562 const QLowEnergyCharacteristic &characteristic, const QByteArray &value)
563{
564 qCDebug(lc).noquote() << tr(R"(Characteristic %1 "%2" read %n byte/s: %3)", nullptr, value.size()).arg(
565 characteristic.uuid().toString(), PokitDevice::charcteristicToString(characteristic.uuid()), toHexString(value));
566}
567
568/*!
569 * Handles `QLowEnergyService::characteristicWritten` events. This base implementation simply debug
570 * logs the event.
571 *
572 * Derived classes should implement this function to handle the successful writes of
573 * \a characteristic, typically by parsing \a newValue, then emitting a speciailised signal.
574 */
576 const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue)
577{
578 qCDebug(lc).noquote() << tr(R"(Characteristic %1 "%2" written with %Ln byte/s: %3)", nullptr, newValue.size())
579 .arg(characteristic.uuid().toString(), PokitDevice::charcteristicToString(characteristic.uuid()), toHexString(newValue));
580}
581
582/*!
583 * Handles `QLowEnergyService::characteristicChanged` events. This base implementation simply debug
584 * logs the event.
585 *
586 * If derived classes support characteristics with client-side notification (ie Notify, as opposed
587 * to Read or Write operations), they should implement this function to handle the successful reads of
588 * \a characteristic, typically by parsing \a value, then emitting a speciailised signal.
589 */
591 const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue)
592{
593 qCDebug(lc).noquote() << tr(R"(Characteristic %1 "%2" changed to %Ln byte/s: %3)", nullptr, newValue.size())
594 .arg(characteristic.uuid().toString(), PokitDevice::charcteristicToString(characteristic.uuid()), toHexString(newValue));
595}
596
597/// \endcond
Declares the AbstractPokitService class.
Declares the AbstractPokitServicePrivate class.
The AbstractPokitServicePrivate class provides private implementation for AbstractPokitService.
bool autoDiscover
Whether autodiscovery is enabled or not.
bool disableCharacteristicNotificatons(const QBluetoothUuid &uuid)
Disables client (Pokit device) side notification for characteristic uuid.
bool createServiceObject()
Creates an internal service object from the internal controller.
void discoveryFinished()
Handles QLowEnergyController::discoveryFinished events.
QBluetoothUuid serviceUuid
UUIDs for service.
virtual void characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue)
Handles QLowEnergyService::characteristicChanged events.
virtual void serviceDiscovered(const QBluetoothUuid &newService)
Handles QLowEnergyController::serviceDiscovered events.
AbstractPokitServicePrivate(const QBluetoothUuid &serviceUuid, QLowEnergyController *controller, AbstractPokitService *const q)
bool enableCharacteristicNotificatons(const QBluetoothUuid &uuid)
Enables client (Pokit device) side notification for characteristic uuid.
virtual void characteristicRead(const QLowEnergyCharacteristic &characteristic, const QByteArray &value)
Handles QLowEnergyService::characteristicRead events.
void connected()
Handles QLowEnergyController::connected events.
QLowEnergyCharacteristic getCharacteristic(const QBluetoothUuid &uuid) const
Get uuid characteristc from the underlying service.
virtual void characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue)
Handles QLowEnergyService::characteristicWritten events.
void errorOccurred(const QLowEnergyService::ServiceError newError)
Handles QLowEnergyController::errorOccurred events.
void stateChanged(QLowEnergyService::ServiceState newState)
Handles QLowEnergyController::stateChanged events.
bool readCharacteristic(const QBluetoothUuid &uuid)
Read the uuid characteristic.
QLowEnergyService * service
BLE service to read/write characteristics.
QLowEnergyController * controller
BLE controller to fetch the service from.
static QString toHexString(const QByteArray &data, const int maxSize=20)
Returns up to maxSize bytes of data as a human readable hexadecimal string.
static bool checkSize(const QString &label, const QByteArray &data, const int minSize, const int maxSize=-1, const bool failOnMax=false)
Returns false if data is smaller than minSize, otherwise returns failOnMax if data is bigger than max...
std::optional< PokitProduct > pokitProduct
The Pokit product controller is connected to.
The AbstractPokitService class provides a common base for Pokit services classes.
std::optional< PokitProduct > pokitProduct() const
Returns the Pokit product this service is attached to.
bool autoDiscover() const
Returns true if autodiscovery of services and service details is enabled, false otherwise.
QLowEnergyService * service()
Returns a non-const pointer to the internal service object, if any.
void setAutoDiscover(const bool discover=true)
If discover is true, autodiscovery will be attempted.
void setPokitProduct(const PokitProduct product)
Sets the current Pokit product.
AbstractPokitServicePrivate * d_ptr
Internal d-pointer.
virtual ~AbstractPokitService()
Destroys this AbstractPokitService object.
static QString charcteristicToString(const QBluetoothUuid &uuid)
Returns a human-readable name for the uuid characteristic, or a null QString if unknown.
static QString serviceToString(const QBluetoothUuid &uuid)
Returns a human-readable name for the uuid service, or a null QString if unknonw.
Declares the PokitDevice class.
PokitProduct
Pokit products known to, and supported by, the QtPokit library.
QTPOKIT_EXPORT QString toString(const PokitProduct product)
Returns product as user-friendly string.
QString toString() const const
QByteArray fromHex(const QByteArray &hexEncoded)
QByteArray left(int len) const const
QByteArray mid(int pos, int len) const const
QByteArray right(int len) const const
int size() const const
QByteArray toHex() const const
QLowEnergyDescriptor descriptor(const QBluetoothUuid &uuid) const const
bool isValid() const const
QBluetoothUuid uuid() const const
QLowEnergyService * createServiceObject(const QBluetoothUuid &serviceUuid, QObject *parent)
QBluetoothAddress remoteAddress() const const
QBluetoothUuid remoteDeviceUuid() const const
QString remoteName() const const
void serviceDiscovered(const QBluetoothUuid &newService)
bool isValid() const const
QString name() const const
QBluetoothUuid uuid() const const
QLowEnergyCharacteristic characteristic(const QBluetoothUuid &uuid) const const
void characteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue)
void characteristicRead(const QLowEnergyCharacteristic &characteristic, const QByteArray &value)
void characteristicWritten(const QLowEnergyCharacteristic &characteristic, const QByteArray &newValue)
QList< QLowEnergyCharacteristic > characteristics() const const
void descriptorRead(const QLowEnergyDescriptor &descriptor, const QByteArray &value)
void descriptorWritten(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue)
QLowEnergyService::ServiceError error() const const
void readCharacteristic(const QLowEnergyCharacteristic &characteristic)
QBluetoothUuid serviceUuid() const const
QLowEnergyService::ServiceState state() const const
void stateChanged(QLowEnergyService::ServiceState newState)
void writeDescriptor(const QLowEnergyDescriptor &descriptor, const QByteArray &newValue)
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
QObject * sender() const const
QString tr(const char *sourceText, const char *disambiguation, int n)
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
QString fromLatin1(const char *str, int size)
QString join(const QString &separator) const const
bool isNull() const const
QString toString() const const