Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
7ae4ddd851
|
|||
0cc89cefab
|
|||
14755cd5b4
|
|||
d2efe566c5
|
|||
b058b6aee0
|
|||
bdb0f9911b
|
18
DCPlugin.pro
18
DCPlugin.pro
@@ -30,7 +30,14 @@ contains( CONFIG, PTU5 ) {
|
|||||||
QMAKE_CXXFLAGS += -std=c++11
|
QMAKE_CXXFLAGS += -std=c++11
|
||||||
linux-clang { QMAKE_CXXFLAGS += -Qunused-arguments }
|
linux-clang { QMAKE_CXXFLAGS += -Qunused-arguments }
|
||||||
CONFIG += link_pkgconfig
|
CONFIG += link_pkgconfig
|
||||||
|
PTU5BASEPATH = /opt/devel/ptu5
|
||||||
ARCH = PTU5
|
ARCH = PTU5
|
||||||
|
|
||||||
|
INCLUDEPATH += $$PTU5BASEPATH/qt/libs/deviceController/include/
|
||||||
|
LIBS += -L$$PTU5BASEPATH/qt/libs/deviceController/library
|
||||||
|
|
||||||
|
LIBS += -lCAslave
|
||||||
|
LIBS += -lCAmaster
|
||||||
}
|
}
|
||||||
|
|
||||||
contains( CONFIG, PTU5_YOCTO ) {
|
contains( CONFIG, PTU5_YOCTO ) {
|
||||||
@@ -38,8 +45,8 @@ contains( CONFIG, PTU5_YOCTO ) {
|
|||||||
PTU5BASEPATH = /opt/devel/ptu5
|
PTU5BASEPATH = /opt/devel/ptu5
|
||||||
ARCH = PTU5
|
ARCH = PTU5
|
||||||
|
|
||||||
# add qmqtt lib
|
LIBS += -lCAslave
|
||||||
#LIBS += -lQt5Qmqtt
|
LIBS += -lCAmaster
|
||||||
}
|
}
|
||||||
|
|
||||||
TARGET = ATBDeviceControllerPlugin
|
TARGET = ATBDeviceControllerPlugin
|
||||||
@@ -69,7 +76,6 @@ DEFINES += QT_DEPRECATED_WARNINGS
|
|||||||
|
|
||||||
# ATBAPP interface
|
# ATBAPP interface
|
||||||
HEADERS += \
|
HEADERS += \
|
||||||
include/interfaces.h \
|
|
||||||
src/ATBAPP/ATBAPPplugin.h \
|
src/ATBAPP/ATBAPPplugin.h \
|
||||||
src/ATBAPP/DeviceControllerDiag.h \
|
src/ATBAPP/DeviceControllerDiag.h \
|
||||||
src/ATBAPP/DeviceControllerInterface.h \
|
src/ATBAPP/DeviceControllerInterface.h \
|
||||||
@@ -80,7 +86,8 @@ HEADERS += \
|
|||||||
src/ATBAPP/support/CashUtils.h \
|
src/ATBAPP/support/CashUtils.h \
|
||||||
src/ATBAPP/support/DBusControllerInterface.h \
|
src/ATBAPP/support/DBusControllerInterface.h \
|
||||||
src/ATBAPP/support/JSON.h \
|
src/ATBAPP/support/JSON.h \
|
||||||
src/ATBAPP/support/PTUSystem.h
|
src/ATBAPP/support/PTUSystem.h \
|
||||||
|
src/ATBAPP/support/PersistentData.h
|
||||||
|
|
||||||
SOURCES += \
|
SOURCES += \
|
||||||
src/ATBAPP/ATBHealthEvent.cpp \
|
src/ATBAPP/ATBHealthEvent.cpp \
|
||||||
@@ -91,7 +98,8 @@ SOURCES += \
|
|||||||
src/ATBAPP/support/CashUtils.cpp \
|
src/ATBAPP/support/CashUtils.cpp \
|
||||||
src/ATBAPP/support/DBusControllerInterface.cpp \
|
src/ATBAPP/support/DBusControllerInterface.cpp \
|
||||||
src/ATBAPP/support/JSON.cpp \
|
src/ATBAPP/support/JSON.cpp \
|
||||||
src/ATBAPP/support/PTUSystem.cpp
|
src/ATBAPP/support/PTUSystem.cpp \
|
||||||
|
src/ATBAPP/support/PersistentData.cpp
|
||||||
|
|
||||||
DISTFILES += \
|
DISTFILES += \
|
||||||
generate-version.sh
|
generate-version.sh
|
||||||
|
@@ -60,11 +60,12 @@ ATBDeviceControllerPlugin::ATBDeviceControllerPlugin(QObject *parent)
|
|||||||
dynamic_cast<QObject*>(hw)->moveToThread(hwThread);
|
dynamic_cast<QObject*>(hw)->moveToThread(hwThread);
|
||||||
hwThread->start();
|
hwThread->start();
|
||||||
|
|
||||||
|
QString persistentDataFile = "/mnt/system_data/dc_persistentData.dat";
|
||||||
|
this->persistentData = new PersistentData(persistentDataFile);
|
||||||
|
|
||||||
this->diag = new DeviceControllerDiag(this);
|
this->diag = new DeviceControllerDiag(this->persistentData, this);
|
||||||
connect(this->diag, &DeviceControllerDiag::newVoltage, this, &ATBDeviceControllerPlugin::onNewVoltage);
|
connect(this->diag, &DeviceControllerDiag::newVoltage, this, &ATBDeviceControllerPlugin::onNewVoltage);
|
||||||
|
|
||||||
|
|
||||||
this->currentSelectedTicketType = 0;
|
this->currentSelectedTicketType = 0;
|
||||||
this->currentCashState = CASH_STATE::CACHE_EMPTY;
|
this->currentCashState = CASH_STATE::CACHE_EMPTY;
|
||||||
|
|
||||||
@@ -101,6 +102,11 @@ PLUGIN_STATE ATBDeviceControllerPlugin::initDCPlugin(QObject *eventReceiver, con
|
|||||||
// this is necessary to init the CashAgentLib (!)
|
// this is necessary to init the CashAgentLib (!)
|
||||||
hw->vend_failed();
|
hw->vend_failed();
|
||||||
|
|
||||||
|
// read sw-version and store it in persistentData, if changed
|
||||||
|
QString dc_fw_version = hw->dc_getSWversion().remove(QChar('\0'));
|
||||||
|
qCritical() << "ATBDeviceControllerPlugin: DC firmware version: " << dc_fw_version;
|
||||||
|
this->persistentData->setDCFirmwareVersion(dc_fw_version);
|
||||||
|
this->persistentData->serializeToFile();
|
||||||
|
|
||||||
// text encoding for printer
|
// text encoding for printer
|
||||||
this->codec = QTextCodec::codecForName(printerEncoding);
|
this->codec = QTextCodec::codecForName(printerEncoding);
|
||||||
@@ -133,6 +139,11 @@ void ATBDeviceControllerPlugin::startPhysicalLayer()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
qCritical() << "ATBDeviceControllerPlugin::startPhysicalLayer() " << endl
|
||||||
|
<< " -> use master lib " << endl
|
||||||
|
<< " -> start physical layer";
|
||||||
|
|
||||||
|
|
||||||
// open serial port
|
// open serial port
|
||||||
hw->dc_openSerial(5, "115200", this->serialPortName, 1);
|
hw->dc_openSerial(5, "115200", this->serialPortName, 1);
|
||||||
|
|
||||||
@@ -141,6 +152,10 @@ void ATBDeviceControllerPlugin::startPhysicalLayer()
|
|||||||
|
|
||||||
void ATBDeviceControllerPlugin::stopPhysicalLayer()
|
void ATBDeviceControllerPlugin::stopPhysicalLayer()
|
||||||
{
|
{
|
||||||
|
// store persistent data
|
||||||
|
this->persistentData->serializeToFile();
|
||||||
|
|
||||||
|
// skip, if we use slave lib
|
||||||
if (!this->isMaster) return;
|
if (!this->isMaster) return;
|
||||||
|
|
||||||
if (this->pluginState == PLUGIN_STATE::NOT_INITIALIZED)
|
if (this->pluginState == PLUGIN_STATE::NOT_INITIALIZED)
|
||||||
@@ -659,6 +674,7 @@ void ATBDeviceControllerPlugin::requestPrintReceipt(const QString & printingStri
|
|||||||
{
|
{
|
||||||
QByteArray ba = printingString.toUtf8();
|
QByteArray ba = printingString.toUtf8();
|
||||||
hw->prn_switchPower(true);
|
hw->prn_switchPower(true);
|
||||||
|
hw->prn_setFonts(8,12,0,0);
|
||||||
hw->prn_sendText(&ba);
|
hw->prn_sendText(&ba);
|
||||||
QTimer::singleShot(4000, this, SLOT(onPrinterWaitForPrintingReceipt()));
|
QTimer::singleShot(4000, this, SLOT(onPrinterWaitForPrintingReceipt()));
|
||||||
|
|
||||||
@@ -1104,10 +1120,24 @@ void ATBDeviceControllerPlugin::onCashChangerState()
|
|||||||
amountCoinsChangedInt = amountInt - this->cashStartAmountInt;
|
amountCoinsChangedInt = amountInt - this->cashStartAmountInt;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
amountCoinsChangedInt = 0;
|
amountCoinsChangedInt = 0;
|
||||||
}
|
}
|
||||||
QString amountCoinsChangedString = QString::number(amountCoinsChangedInt);
|
QString amountCoinsChangedString = QString::number(amountCoinsChangedInt);
|
||||||
|
|
||||||
|
// if we do not need to give change:
|
||||||
|
if (amountCoinsChangedInt == 0) {
|
||||||
|
emit this->cashPaymentFinished(nsDeviceControllerInterface::RESULT_STATE::SUCCESS,
|
||||||
|
amountString,
|
||||||
|
amountCoinsString, // coins
|
||||||
|
amountNotesString, // notes
|
||||||
|
amountCoinsChangedString, // change
|
||||||
|
"",
|
||||||
|
"");
|
||||||
|
changerStateRequestCounter = 0;
|
||||||
|
lastChangerResult = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// get changer state ------------------------------------------------
|
// get changer state ------------------------------------------------
|
||||||
// Note: 'returnedAmount'-parameter is missleading here!
|
// Note: 'returnedAmount'-parameter is missleading here!
|
||||||
@@ -1293,6 +1323,10 @@ bool ATBDeviceControllerPlugin::private_loadCashAgentLib(QString pluginName)
|
|||||||
|
|
||||||
qCritical() << "ATBDeviceControllerPlugin: loaded CashAgentLib";
|
qCritical() << "ATBDeviceControllerPlugin: loaded CashAgentLib";
|
||||||
|
|
||||||
|
if (this->isMaster) {
|
||||||
|
QTimer::singleShot(500, this, &ATBDeviceControllerPlugin::startPhysicalLayer);
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -10,9 +10,9 @@
|
|||||||
|
|
||||||
#include "version.h"
|
#include "version.h"
|
||||||
|
|
||||||
|
#include "support/PersistentData.h"
|
||||||
|
|
||||||
|
#include <DeviceController/interfaces.h>
|
||||||
#include "interfaces.h"
|
|
||||||
|
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
@@ -112,6 +112,8 @@ private:
|
|||||||
|
|
||||||
DeviceControllerDiag* diag;
|
DeviceControllerDiag* diag;
|
||||||
|
|
||||||
|
PersistentData *persistentData;
|
||||||
|
|
||||||
uint32_t cashStartAmountInt;
|
uint32_t cashStartAmountInt;
|
||||||
|
|
||||||
|
|
||||||
|
@@ -5,13 +5,14 @@
|
|||||||
#include <QUuid>
|
#include <QUuid>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
|
||||||
DeviceControllerDiag::DeviceControllerDiag(QObject *parent)
|
DeviceControllerDiag::DeviceControllerDiag(PersistentData *pData, QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, coinProcessorType(nsDeviceControllerInterface::COIN_PROCESSOR::ESCROW)
|
, coinProcessorType(nsDeviceControllerInterface::COIN_PROCESSOR::ESCROW)
|
||||||
, billAcceptor(nsDeviceControllerInterface::BILL_ACCEPTOR::NO)
|
, billAcceptor(nsDeviceControllerInterface::BILL_ACCEPTOR::NO)
|
||||||
, eventReceiver(nullptr)
|
, eventReceiver(nullptr)
|
||||||
, isRequestRunning(false)
|
, isRequestRunning(false)
|
||||||
, flagInterruptDiag(false)
|
, flagInterruptDiag(false)
|
||||||
|
, pData(pData)
|
||||||
{
|
{
|
||||||
diagRequestTimeoutTimer = new QTimer(this);
|
diagRequestTimeoutTimer = new QTimer(this);
|
||||||
diagRequestTimeoutTimer->setInterval(1000*20); // 20s
|
diagRequestTimeoutTimer->setInterval(1000*20); // 20s
|
||||||
@@ -42,6 +43,16 @@ void DeviceControllerDiag::diagRequest()
|
|||||||
this->diagRequestTimeoutTimer->start();
|
this->diagRequestTimeoutTimer->start();
|
||||||
|
|
||||||
this->private_startDiag();
|
this->private_startDiag();
|
||||||
|
|
||||||
|
// read dc-fw-version:
|
||||||
|
/* note: dc_getSWVersion() returns always 32 characters (QString)...
|
||||||
|
* if no version string could be read it will contain 32 null-characters:
|
||||||
|
* "\u0000\u0000..."
|
||||||
|
*/
|
||||||
|
QString dc_fw_version = hw->dc_getSWversion().remove(QChar('\0'));
|
||||||
|
qCritical() << "ATBDeviceControllerPlugin: DC firmware version: " << dc_fw_version;
|
||||||
|
this->pData->setDCFirmwareVersion(dc_fw_version);
|
||||||
|
this->pData->serializeToFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@@ -5,9 +5,12 @@
|
|||||||
#include <QSet>
|
#include <QSet>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
|
||||||
#include "ATBMachineEvent.h"
|
#include <DeviceController/interfaces.h>
|
||||||
#include "interfaces.h"
|
|
||||||
#include "DeviceControllerInterface.h"
|
#include "DeviceControllerInterface.h"
|
||||||
|
#include "ATBMachineEvent.h"
|
||||||
|
|
||||||
|
#include "support/PersistentData.h"
|
||||||
|
|
||||||
|
|
||||||
namespace DeviceController {
|
namespace DeviceController {
|
||||||
@@ -60,7 +63,7 @@ class DeviceControllerDiag : public QObject
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
DeviceControllerDiag(QObject *parent = nullptr);
|
DeviceControllerDiag(PersistentData *pData, QObject *parent = nullptr);
|
||||||
|
|
||||||
void init(hwinf* hw, QObject* eventReceiver);
|
void init(hwinf* hw, QObject* eventReceiver);
|
||||||
|
|
||||||
@@ -90,7 +93,7 @@ private:
|
|||||||
|
|
||||||
QSet<DeviceController::State> machineEventSet;
|
QSet<DeviceController::State> machineEventSet;
|
||||||
|
|
||||||
|
PersistentData* pData;
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onDiagRequestTimeoutTimerTimeout();
|
void onDiagRequestTimeoutTimerTimeout();
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
#define CASHUTILS_H
|
#define CASHUTILS_H
|
||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include "interfaces.h"
|
#include <DeviceController/interfaces.h>
|
||||||
|
|
||||||
namespace CashUtils {
|
namespace CashUtils {
|
||||||
|
|
||||||
|
140
src/ATBAPP/support/PersistentData.cpp
Normal file
140
src/ATBAPP/support/PersistentData.cpp
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
#include "PersistentData.h"
|
||||||
|
|
||||||
|
#include <QFile>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QDataStream>
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
|
PersistentData::PersistentData(const QString &datafileName, QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
, isChangedFlag(false)
|
||||||
|
{
|
||||||
|
// load persistant data, if available
|
||||||
|
this->filename = datafileName;
|
||||||
|
|
||||||
|
QFileInfo dataFileInfo(this->filename);
|
||||||
|
QString dataFilePath = dataFileInfo.path();
|
||||||
|
|
||||||
|
QDir dir;
|
||||||
|
if ( ! dir.exists(dataFilePath)) {
|
||||||
|
qCritical() << "Persistent data file does not exist!";
|
||||||
|
qCritical() << " --> create new: " << this->filename;
|
||||||
|
dir.mkpath(dataFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
this->read();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void PersistentData::serializeToFile()
|
||||||
|
{
|
||||||
|
this->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PersistentData::save()
|
||||||
|
{
|
||||||
|
QFile fileOut(this->filename);
|
||||||
|
if (fileOut.open(QIODevice::WriteOnly))
|
||||||
|
{
|
||||||
|
QDataStream out(&fileOut);
|
||||||
|
out.setVersion(QDataStream::Qt_4_6);
|
||||||
|
|
||||||
|
out << this->hash;
|
||||||
|
fileOut.flush();
|
||||||
|
fileOut.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PersistentData::read()
|
||||||
|
{
|
||||||
|
QFile fileIn(this->filename);
|
||||||
|
if (fileIn.open(QIODevice::ReadOnly))
|
||||||
|
{
|
||||||
|
QDataStream in(&fileIn);
|
||||||
|
in.setVersion(QDataStream::Qt_4_6);
|
||||||
|
|
||||||
|
in >> hash;
|
||||||
|
|
||||||
|
fileIn.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
QVariant PersistentData::getParameter(const QString & key) const {
|
||||||
|
#if defined (ARCH_DesktopLinux)
|
||||||
|
// note: QVariant.toString() returns empty string for custom types
|
||||||
|
qDebug() << "VendingData::getParameter() key = " << key << " value = " << hash.value(key).toString();
|
||||||
|
#endif
|
||||||
|
return hash.value(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant PersistentData::getParameter(const QString & key)
|
||||||
|
{
|
||||||
|
#if defined (ARCH_DesktopLinux)
|
||||||
|
// note: QVariant.toString() returns empty string for custom types
|
||||||
|
qDebug() << "VendingData::getParameter() key = " << key << " value = " << hash.value(key).toString();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return hash.value(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PersistentData::setParameter(const QString & key, QVariant value)
|
||||||
|
{
|
||||||
|
this->isChangedFlag = true;
|
||||||
|
|
||||||
|
#if defined (ARCH_DesktopLinux)
|
||||||
|
// note: QVariant.toString() returns empty string for custom types
|
||||||
|
qDebug() << "VendingData::setParameter() key = " << key << " value = " << value.toString();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
this->hash.insert(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PersistentData::clearParameter(const QString & key)
|
||||||
|
{
|
||||||
|
this->isChangedFlag = true;
|
||||||
|
this->hash.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PersistentData::hasParameter(const QString & key) const
|
||||||
|
{
|
||||||
|
return hash.contains(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
uint PersistentData::getUintParameter(const QString & key) const
|
||||||
|
{
|
||||||
|
qDebug() << "PersistentData::getUintParameter() key = " << key << " value = " << hash.value(key).toString();
|
||||||
|
|
||||||
|
uint returnValue = 0;
|
||||||
|
bool ok;
|
||||||
|
|
||||||
|
returnValue = hash.value(key).toString().toUInt(&ok);
|
||||||
|
if (!ok) returnValue = 0;
|
||||||
|
|
||||||
|
return returnValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<QString> PersistentData::uniqueKeys() const {
|
||||||
|
return hash.uniqueKeys();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void PersistentData::setDCFirmwareVersion(const QString & fw_version)
|
||||||
|
{
|
||||||
|
// there must be a version string!
|
||||||
|
if (fw_version.size() < 1) return;
|
||||||
|
|
||||||
|
if (this->hash["dc_fw_version"].toString() != fw_version) {
|
||||||
|
this->isChangedFlag = true;
|
||||||
|
this->hash.insert("dc_fw_version", fw_version);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString PersistentData::getDCFirmwareVersion()
|
||||||
|
{
|
||||||
|
return this->hash["dc_fw_version"].toString();
|
||||||
|
}
|
49
src/ATBAPP/support/PersistentData.h
Normal file
49
src/ATBAPP/support/PersistentData.h
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
#ifndef PERSISTENTDATA_H
|
||||||
|
#define PERSISTENTDATA_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QHash>
|
||||||
|
#include <QVariant>
|
||||||
|
#include <QList>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
class PersistentData : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit PersistentData(const QString &datafileName, QObject *parent = nullptr);
|
||||||
|
|
||||||
|
void setDCFirmwareVersion(const QString & fw_version);
|
||||||
|
QString getDCFirmwareVersion();
|
||||||
|
|
||||||
|
|
||||||
|
QVariant getParameter(const QString & key);
|
||||||
|
QVariant getParameter(const QString & key) const;
|
||||||
|
void setParameter(const QString & key, QVariant value);
|
||||||
|
void clearParameter(const QString & key);
|
||||||
|
bool hasParameter(const QString & key) const;
|
||||||
|
|
||||||
|
uint getUintParameter(const QString & key) const;
|
||||||
|
QList<QString> uniqueKeys() const;
|
||||||
|
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void serializeToFile();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
|
||||||
|
|
||||||
|
private:
|
||||||
|
QHash<QString, QVariant> hash;
|
||||||
|
|
||||||
|
QString dc_fw_version;
|
||||||
|
|
||||||
|
QString filename;
|
||||||
|
|
||||||
|
void save();
|
||||||
|
void read();
|
||||||
|
|
||||||
|
bool isChangedFlag;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // PERSISTENTDATA_H
|
Reference in New Issue
Block a user