/*! * * Copyright (c) 2019-2020 Diality Inc. - All Rights Reserved. * \copyright * THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN * WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. * * \file DeviceController.cpp * \author (last) Behrouz NematiPour * \date (last) 29-May-2021 * \author (original) Behrouz NematiPour * \date (original) 17-Jul-2020 * */ #include "DeviceController.h" // Linux #include #include // Qt #include #include #include // Project #include "StorageGlobals.h" #include "Logger.h" #include "ApplicationController.h" #include "FileHandler.h" #include "DeviceModels.h" #include "DeviceError.h" // namespace using namespace Model; using namespace Device; using namespace Storage; /*! * \brief DeviceController::DeviceController * \details Constructor * \param parent - QObject parent owner object. * Qt handles the children destruction by their parent objects life-cycle. */ DeviceController::DeviceController(QObject *parent) : QObject(parent) { _fileSystemWatcher.setParent(this); DEVICE_DEV_PARENT_LIST } /*! * \brief DeviceController::init * \details Initializes the class by setting the connections and starting the timer * \return False if it has been called before. */ bool DeviceController::init() { if ( _init ) return false; _init = true; // runs in DeviceController thread initConnections(); startTimer(_interval); return true; } /*! * \brief DeviceController::init * \details Initialized the Class by calling the init() method first * And initializes the thread vThread by calling initThread * on success init(). * \param vThread - the thread * \return returns the return value of the init() method */ bool DeviceController::init(QThread &vThread) { if ( ! init() ) return false; initThread(vThread); return true; } /*! * \brief DeviceController::quit * \details quits the class * Calls quitThread */ void DeviceController::quit() { // coco begin validated: Application termination is not correctly done in coco!!! // it has been tested and works perfectly fine in normal run. quitThread(); // validated } // coco end void DeviceController::onRemoveLogs(bool vInProgress) { // coco begin validated: The log in progress requires user interaction // it has been tested and works fine in normal run. _pauseSpaceCheck = vInProgress; } // coco end /*! * \brief DeviceController::initConnections * \details Initializes the required signal/slot connection between this class and other objects * to be able to communicate. */ void DeviceController::initConnections() { connect(&_ApplicationController , SIGNAL(didUSBDriveUmount()), this , SLOT( onUSBDriveUmount())); connect(&_Logger , SIGNAL(didRemoveLogs(bool)), this , SLOT( onRemoveLogs(bool))); connect(this , SIGNAL(didScreenshot(const QImage &, const QString &)), this , SLOT( onScreenshot(const QImage &, const QString &))); connect(&_fileSystemWatcher , SIGNAL( fileChanged(const QString &)), this , SLOT( onWatchFileChanged(const QString &))); DEVICE_DEV_INIT_CONNECTIONS_LIST } /*! * \brief ApplicationController::initThread * \details Moves this object into the thread vThread. * And checks that this method is called from main thread. * Also connects quitThread to application aboutToQuit. * \param vThread - the thread */ void DeviceController::initThread(QThread &vThread) { // runs in main thread Q_ASSERT_X(QThread::currentThread() == qApp->thread() , __func__, "The Class initialization must be done in Main Thread" ); _thread = &vThread; _thread->setObjectName(QString("%1_Thread").arg(metaObject()->className())); connect(qApp, SIGNAL(aboutToQuit()), this, SLOT(quit())); _thread->start(); moveToThread(_thread); } /*! * \brief DeviceController::quitThread * \details Moves this object to main thread to be handled by QApplication * And to be destroyed there. */ void DeviceController::quitThread() { // coco begin validated: Application termination is not correctly done in coco!!! // it has been tested and works perfectly fine in normal run. if ( ! _thread ) return; // runs in thread moveToThread(qApp->thread()); // validated } // coco end /*! * \brief DeviceController::usbSeek * \details Tries to look for the available USB devices * Starts from sda1 to sdz1. * \note will only look for the first partition if there is any * \param vDevice - Found device (/dev/sda1) * \return true if a device found */ bool DeviceController::usbSeek(QString &vDevice) { // coco begin validated: Needed User Interaction so tested manually // This function cannot be tested on any device other than target // because only on device the usb is /dev/sdX# and others are mmblk___ // but on VM and normal Linuxes all drives are /dev/sdX# and cannot be identified as USB. // And the difference between how USB is detected and sd-card is that // for the USB we need to look into /dev/sdX# // but for the sd-card we look for the mounted path which is much easier. // That's why indication of the sd-card is more accurate than USB at least on VM. QString dev = "/dev/sd"; QString device = ""; for (char a = 'a'; a <= 'z'; a++) { device = dev + a + '1'; if (QFileInfo::exists(device)) { vDevice = device; return true; // application is deciding on the first existing drive } } vDevice = device; return false; // coco end } /*! * \brief DeviceController::driveSpaceCheck * \param vPath - Device mount point * \param vTotalBytes - Total volume size in bytes * \param vAvailableBytes - Size (in bytes) available for the current user * \param vPercent - The percentage of available space * \return - The drive mounted and ready * \note if device ejected manually system assumes it's still ready. */ bool DeviceController::driveSpaceCheck(const QString &vPath, qint64 &vTotalBytes, qint64 &vAvailableBytes, bool *vIsReadOnly) { // coco begin validated: Needed User Interaction to make the device not ready so tested manually bool isReadOnly; if (! FileHandler::isMounted(vPath, &isReadOnly)) return false; if (vIsReadOnly) *vIsReadOnly = isReadOnly; QStorageInfo storage(vPath); bool isReady = storage.isReady(); if (isReady) { vTotalBytes = storage.bytesTotal(); vAvailableBytes = storage.bytesAvailable(); } return isReady; } // coco end /*! * \brief DeviceController::timerEvent * \details This event handler has been re-implemented in here * to receive timer events for the object * for the timer which has been set to _checkInterval * Runs the usbCheck on interval */ void DeviceController::timerEvent(QTimerEvent *) { usbCheck(); sdcardSpaceCheck(); } /*! * \brief DeviceController::usbCheck * \details Runs usbSeek to mount or umount or remove it * regarding the state it's in. */ void DeviceController::usbCheck() { // coco begin validated: Needed User Interaction so tested manually QString device = ""; if (usbSeek(device)) { if (! _umounted ) { usbMount(device); } else { usbUmount(USB_Mount_Point); } } else { if ( ! _removed ) { usbRemove(); } } // coco end } /*! * \brief DeviceController::sdcardFreeSpaceCheck * \details Checks the drivers for available free space. */ void DeviceController::sdcardSpaceCheck() { // coco begin validated: Needed User Interaction so tested manually // Old Info ; // Current info static bool mOIsReady ; bool mCIsReady ; static bool mOIsReadOnly; bool mCIsReadOnly ; static qint64 mOTotal ; qint64 mCTotal ; static qint64 mOAvailable ; qint64 mCAvailable ; mCIsReady = driveSpaceCheck(Storage::SDCard_Base_Path_Name, mCTotal, mCAvailable, &mCIsReadOnly); /// DEBUG: qDebug() << " ===== " << Storage::SDCard_Base_Path_Name << mCIsReady; if (mOIsReadOnly != mCIsReadOnly || mOIsReady != mCIsReady) { mOIsReadOnly = mCIsReadOnly; emit didSDCardStateChange(mCIsReady, mCIsReadOnly); } if (! mCIsReady ) { mOIsReady = mCIsReady; mOTotal = 0; mOAvailable = 0; return; } quint8 mPercent = mCTotal ? ((100 * mCAvailable) / mCTotal) : 0; if (mPercent < _minRequiredAvailableSpacePercent) { LOG_DEBUG(QString("SD-CARD space lower than %1%").arg(_minRequiredAvailableSpacePercent)); emit didSDCardSpaceChange(mCIsReady, mCTotal, mCAvailable, mPercent); emit didSDCardSpaceTooLow(_minRequiredAvailableSpacePercent); } /// DEBUG: qDebug() << Storage::SDCard_Base_Path_Name << mCIsReady << mOTotal << mCTotal << (mOTotal == mCTotal) << mOAvailable << mCAvailable << (mOAvailable == mCAvailable) << mPercent << mCIsReadOnly; if (mOTotal == mCTotal && mOAvailable == mCAvailable) { return; } mOIsReady = mCIsReady ; mOTotal = mCTotal ; mOAvailable = mCAvailable ; if (_pauseSpaceCheck) return; emit didSDCardSpaceChange(mCIsReady, mCTotal, mCAvailable, mPercent); /// DEBUG: qDebug() << Storage::SDCard_Base_Path_Name << mCIsReady << mCTotal << mCAvailable << mPercent ; } // coco end /*! * \brief DeviceController::usbError * \details Logs any error which has been happened * On USB device vDevice * \note When this method has been called error number will be read from errno variable, * Which has been set by umount or mount. * \param vDevice */ void DeviceController::usbError(const QString &vDevice) { // coco begin validated: This needs user interaction to plug-in/out the USB device // has been tested manually QString error; static QString lastError; switch (errno) { case EBUSY: error = tr("%1 - Device or resource busy (%2)").arg(errno).arg(vDevice); _mounted = true; break; default: error = tr("%1 - %2 (%3 , %4)").arg(errno).arg(strerror(errno)).arg(vDevice).arg(USB_Mount_Point); break; } if (error != lastError) { LOG_DEBUG("USB: " + error); lastError = error; } } // coco end /*! * \brief DeviceController::onUSBDriveUmount * \details This is the slot connected to the _ApplicationController's didUSBDriveUmount SIGNAL, * To notify the USB drive detach. */ void DeviceController::onUSBDriveUmount() { // coco begin validated: This needs user interaction to plug-in/out the USB device // has been tested manually _umounted = true; } // coco end /*! * \brief DeviceController::usbMount * \details Mounts the USB device vDevice * \note Emits didUSBDriveMount signal * \param vDevice - USB device to be mounted (e.g. /dev/sda1) * \return true on successful mount */ bool DeviceController::usbMount(const QString &vDevice) { // coco begin validated: This needs user interaction to plug-in the USB device // has been tested manually bool ok; _usbDrive = vDevice.toLatin1().constData(); ok = ::mount(_usbDrive, USB_Mount_Point, USB_File_System, MS_SYNCHRONOUS, "") == 0; if (ok) { _mounted = true; _removed = false; LOG_DEBUG(QString("USB flash drive %1 has been mounted on %2").arg(vDevice).arg(USB_Mount_Point)); emit didUSBDriveMount(); } else { usbError(vDevice); } return ok; } // coco end /*! * \brief DeviceController::usbUmount * \details Unmounts the USB device vDevice * \note Emits didUSBDriveUmount signal * \param vDevice - USB device to be unmounted (e.g. /dev/sda1) * \return true on successful unmount */ bool DeviceController::usbUmount(const QString &vDevice) { // coco begin validated: This needs user interaction to plug-out the USB device // has been tested manually bool ok; ok = ::umount(vDevice.toLatin1().constData()) == 0; if (ok) { _mounted = false; LOG_DEBUG(QString("USB drive %2 unmounted").arg(vDevice)); emit didUSBDriveUmount(); } else { // the error is irrelevant, commented out for now //usbError(vDevice); } return ok; } // coco end /*! * \brief DeviceController::usbRemove * \details Removed the USB mount point * So next time it is not mounted as next device. * \note Emits didUSBDriveRemove signal */ void DeviceController::usbRemove() { // coco begin validated: This needs user interaction to plug-out the USB device // has been tested manually usbUmount(USB_Mount_Point); _umounted = false; _removed = true; LOG_DEBUG("USB drive removed"); emit didUSBDriveRemove(); } // coco end /*! * \brief DeviceController::checkError * \details check if has error, then sets the base model accept to false and the reason to the error. * in that case logs the error message and emit the didAttributeResponse to notify the GUI. * \param vError - the error code, this can be the Gui enum or system number * \param vExtraLogInfo - any extra information to be logged. Not display to user. * \return */ template bool DeviceController::checkError(DeviceError::Scripts_Error_Enum vError, TModel &vModel, QString vExtraLogInfo) { if ( vError ) { QString src = (vError > DeviceError::eDevice_Scripts_Error_Start ? MAbstract::unitText(MAbstract::Unit_Enum::eUI) : MAbstract::unitText(MAbstract::Unit_Enum::eDV)) + ","; vModel._data.mAccepted = false ; vModel._data.mReason = vError ; vModel._data.mMessage = DeviceError::deviceErrorText(vError, vError); LOG_EVENT(src + vModel._data.mMessage + " " + vExtraLogInfo); emit didAttributeResponse(vModel._data); return true; } return false; } /*! * \brief DeviceController::doBrightness * \details Sets the brightness level * \param vBrightness */ void DeviceController::onAttributeRequest(const DeviceBrightnessRequestData &vData) { // ----- initializing the member variable models _deviceBrightnessRequest ._data = vData; _deviceBrightnessResponse._data.mBrightnessPercent = _deviceBrightnessRequest._data.mBrightness_old * 10; // set to old value in case of error // ----- extract the required data _deviceBrightnessRequest._data.mBrightness_val = vData.mBrightnessPercent / 10; // convert the GUI value(percent) to device value LOG_EVENT(_deviceBrightnessRequest.toString()); // ----- check that script exists. QString script; if ( checkError( DeviceError::checkScript(script, vData.mRead ? Brightness_Get : Brightness_Set), _deviceBrightnessResponse, script) ) return; // ----- check if the process is not running if ( _processBrightness.state() != QProcess::NotRunning ) { checkError(DeviceError::eDevice_Scripts_Error_IsRunning, _deviceBrightnessResponse); return; } // ----- check the data is valid // TODO : this should be in the model if ( _deviceBrightnessRequest.mBrightness_min > _deviceBrightnessRequest._data.mBrightness_val || _deviceBrightnessRequest._data.mBrightness_val > _deviceBrightnessRequest.mBrightness_max ) { checkError(DeviceError::eDevice_Scripts_Error_OutOfRange, _deviceBrightnessResponse); return; } // ----- check the value is in the list of correct resolution // create the list of correct values // TODO : this should be in the model QList _allowableValues; for ( quint8 i = _deviceBrightnessRequest.mBrightness_min; i <= _deviceBrightnessRequest.mBrightness_max; i += _deviceBrightnessRequest.mBrightness_res ) _allowableValues += i; // check the value is in the list if ( ! _allowableValues.contains(_deviceBrightnessRequest._data.mBrightness_val) ) { checkError(DeviceError::eDevice_Scripts_Error_Incorrect_Req, _deviceBrightnessResponse); return; } // ----- run the process _processBrightness.start(script, QStringList() << QString::number(_deviceBrightnessRequest._data.mBrightness_val)); } /*! * \brief DeviceController::onProcessBrightnessFinished * \details Called when the process to set the brightness has finished * \param vExitCode (int) the exit code * \note exit code -> 0 : set Accept [MBase] -> Log -> emit * !0 : set Attrib [MBrgh] -> Log -> emit * 1 - get an error when in onAttributeRequest : scriptErrorText([Gui Enum ]) * 2 - get an error when in onProcessBrightnessExitCode : scriptErrorText([vExitCode]) * 3 - get no error when in onProcessBrightnessExitCode : MDeviceResponse.toString() * - in case 3 the specific model _data has to be filled prior to the toString to have it in the log. */ void DeviceController::onProcessBrightnessExitCode(int vExitCode, QProcess::ExitStatus) { // TODO: Move this into the model. _deviceBrightnessResponse._data.mBrightnessPercent = _deviceBrightnessRequest._data.mBrightness_old * 10; // set to old value in case of error if ( ! checkError(static_cast(vExitCode), _deviceBrightnessResponse, _deviceBrightnessResponse.toString()) ) { // has no error if (_deviceBrightnessRequest._data.mRead) { bool ok = false; int brightness = _processBrightness.readLine().toInt(&ok); if (ok) { _deviceBrightnessResponse._data.mBrightnessPercent = brightness * 10; } else { checkError(DeviceError::eDevice_Scripts_Error_Incorrect_Rsp,_deviceBrightnessResponse, _deviceBrightnessResponse.toString()); return; } } else { _deviceBrightnessResponse._data.mBrightnessPercent = _deviceBrightnessRequest._data.mBrightness_val * 10; _deviceBrightnessResponse._data.mMessage = _deviceBrightnessResponse.toString(); } LOG_EVENT(_deviceBrightnessResponse._data.mMessage); emit didAttributeResponse(_deviceBrightnessResponse._data); } } void DeviceController::onAttributeRequest(const DeviceBluetoothPairedResetRequestData &) { // ----- check that script exists. QString script; if ( checkError( DeviceError::checkScript(script, Bluetooth_Paired_Reset), _deviceBluetoothPairedResetResponse, script) ) return; // ----- check if the process is not running if ( _processBluetoothPairedReset.state() != QProcess::NotRunning ) { checkError(DeviceError::eDevice_Scripts_Error_IsRunning, _deviceBluetoothPairedResetResponse); return; } // ----- run the process TimedProcess *timedProcess = new TimedProcess(&_processBluetoothPairedReset, script, 2000); timedProcess->start(); } void DeviceController::onProcessBluetoothPairedResetExitCode(int vExitCode, QProcess::ExitStatus vStatus) { // TODO: review the usage and definition of this object _deviceBluetoothPairedResetResponse. do we need it any more? MDeviceBluetoothPairedResetResponse model; QByteArray deviceInfo; if ( vStatus ) vExitCode = Device::DeviceError::eDevice_Scripts_Error_Status; else deviceInfo = _processBluetoothPairedReset.readAll(); model.fromByteArray( deviceInfo, &vExitCode ); LOG_EVENT_UI(model.data().mMessage); didAttributeResponse(model.data()); } void DeviceController::onAttributeRequest(const DeviceBluetoothPairedQueryRequestData &) { // ----- check that script exists. QString script; if ( checkError( DeviceError::checkScript(script, Bluetooth_Paired_Query), _deviceBluetoothPairedQueryResponse, script) ) return; // ----- check if the process is not running if ( _processBluetoothPairedQuery.state() != QProcess::NotRunning ) { checkError(DeviceError::eDevice_Scripts_Error_IsRunning, _deviceBluetoothPairedQueryResponse); return; } // ----- run the process TimedProcess *timedProcess = new TimedProcess(&_processBluetoothPairedQuery, script, 1000); timedProcess->start(); } void DeviceController::onProcessBluetoothPairedQueryExitCode(int vExitCode, QProcess::ExitStatus vStatus) { // The Exit code in this script is not used and is not used. // any other checking is done by UI Software at the moment this script is called. // The only thing matters is the pared device info in text and it will be empty string if error happens. MDeviceBluetoothPairedQueryResponse model; QByteArray deviceInfo; if ( vStatus ) vExitCode = Device::DeviceError::eDevice_Scripts_Error_Status; else deviceInfo = _processBluetoothPairedQuery.readAll(); model.fromByteArray( deviceInfo, &vExitCode ); didAttributeResponse(model.data()); LOG_EVENT_UI(model.data().mMessage); } /*! * \brief DeviceController::doScreenshot * \details emit the screenshot signal to run that in Device controller thread * \param vCurrentDateTime */ void DeviceController::doScreenshot(const QImage &vImage, const QString &vFileName) { emit didScreenshot(vImage, vFileName); } /*! * \brief DeviceController::onScreenshot * \details The function to save the image vImage in the file vFileName * \param vImage - The image source object * \param vFileName - The filename to same the image to */ void DeviceController::onScreenshot(const QImage &vImage, const QString &vFileName) { vImage.save(vFileName); LOG_DEBUG("Screenshot saved in " + vFileName); } /*! * \brief DeviceController::ondoAddWatch * \details The thread safe add file watch method * \param vFile - The file to add to watch. */ void DeviceController::ondoAddWatch(const QString &vFile) { if ( ! QFileInfo::exists(vFile)) { if ( ! FileHandler::write(vFile, "", false) ) { LOG_DEBUG(DeviceError::deviceErrorText(DeviceError::eDevice_Watch_Error_NotFound, 0)); return; } } if ( ! _fileSystemWatcher.addPath(vFile) ) { LOG_DEBUG(DeviceError::deviceErrorText(DeviceError::eDevice_Watch_Error_NotAdded, 0)); } } /*! * \brief DeviceController::onWatchFileChanged * \details This slot is called once the files being watched is updated. * \param vFile - the file name. */ void DeviceController::onWatchFileChanged(const QString &vFile) { emit didWatchFileChange(vFile); }