/*! * * Copyright (c) 2023 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 UpdateProtocol.c * \author (last) Phil Braica * \date (last) 27-Apr-2023 * \author (original) Phil Braica * \date (original) 27-Apr-2023 * */ #include "VSwUpdate.h" #include "ApplicationController.h" #include "CanInterface.h" #include "Logger.h" #include "MessageDispatcher.h" #include #include #include #include #include #include #include #include #include "crc.h" // Enables (1) or disables (!1) reboot to allow for better testing. // Default is to reboot when this is finished. #ifndef REBOOT_ENABLED #define REBOOT_ENABLED (0) #endif #define MSG_ID_ENTER_BOOTLOADER_NOW (0x8093) ///< HD Jump to the bootloader application #define MSG_ID_REBOOT_NOW (0x8094) ///< HD Reboot RM46 immediately #define MSG_ID_SET_ENTER_BOOTLOADER (0x8095) ///< HD Set the flag to stay /*! * \brief Constructor. * * \param parent Parent object pointer, optional. */ VSWUpdate::VSWUpdate(QObject* parent) : QAbstractTableModel(parent), _pubKey(""), _persistent(""), _oldUpdates(""), _factoryImg(""), _wifiDir(""), _maxOld(3), _updating(false), _percent(0), _selected(-1), _stateMsg(" "), // Not "", so notify occurs. _buttonText(" "), // Not "Start", so notify occurs. _mpBackButton(nullptr), _isVisible(false), _progressVisible(false), _postWasOk(false), _settingsOk(false) { setButtonText("Start"); setState(""); controlUSB(true); // Setup the directories and parameters. QStringList usbDirs; usbDirs.append("/media/usb/"); usbDirs.append("/home/denali/Projects/update/usbTest"); QString factoryImage = "/home/denali/Projects/update/factory"; QString wifiDir = "/home/denali/Projects/update/wifi"; QString cacheDir = "/home/denali/Projects/update/cache"; setup( usbDirs, factoryImage, wifiDir, cacheDir, 3, "/home/denali/Projects/update/public.pem"); // Determine if this was a trial boot after an install or not. // If we fail and it was a trial boot, U-Boot will try the last version. // // That will likely be incompatible with HD and DG and require an install BUT // it is likely a better more stable state to try to do an install from. QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); _wasTrialBoot = env.value("upgrade_available") == "1"; // Initial scan for updates. scanForUpdates(); // Setup periodic scanning. connect(&_timer, &QTimer::timeout, this, &VSWUpdate::handle_update); connect(&_ApplicationController, &ApplicationController::didSettingsDone, this , &VSWUpdate::onSettingsDone); connect(&_ApplicationController, &ApplicationController::didPOSTDone, this , &VSWUpdate::onPostDone); // Tell the package code this class will send the enter bootloader command, // if not set, this is skipped. _package.setEnterBootloader(this); // Must be > 1 Hz. _timer.start(500); } /*! * \brief Destructor. */ VSWUpdate::~VSWUpdate() { _package.abort(); } /*! * \brief Setup the UI for production. * * \note * Call just before showing the widget for production. * * factoryImageDir can be either the folder path containing the factory golden * original image or be the file fullname/path. * * wifiImageDir is the directory of the wifi downloaded package images. * * \param directories Directories to scan * \param factoryImageDir Directory containing the factory image. * \param wifiImageDir Wifi downloaded image directory. * \param oldUpdateDir Which directory is where old applied ones are stored. * \param maxOldUpdates How many updates to keep in "oldUpdateDir". * \param publicKey The public key string. */ void VSWUpdate::setup( const QStringList directories, const QString factoryImageDir, const QString wifiImageDir, const QString oldUpdateDir, uint32 maxOldUpdates, const QString publicKeyFile) { QFile f(publicKeyFile); if (!f.open(QFile::ReadOnly | QFile::Text)) { return; } QTextStream in(&f); _pubKey = in.readAll(); _updateDirs = directories; _factoryImg = factoryImageDir.toStdString(); _wifiDir = wifiImageDir.toStdString(); _oldUpdates = oldUpdateDir.toStdString(); _maxOld = maxOldUpdates; // We remove duplicates, add oldUpdateDir if not in directories. // It's ok if oldUpdateDir is in that list already or not, or a subdirectory of that list. if (!_updateDirs.contains(oldUpdateDir)) { _updateDirs.append(oldUpdateDir); } if (!_updateDirs.contains(factoryImageDir)) { _updateDirs.append(factoryImageDir); } if (!_updateDirs.contains(wifiImageDir)) { _updateDirs.append(wifiImageDir); } } /*! * \brief Turn timepoint into std::time_t. * * \param tp Timepoint. * * \return std::time_t. */ template std::time_t to_time_t(TP tp) { using namespace std::chrono; auto sctp = time_point_cast(tp - TP::clock::now() + system_clock::now()); return system_clock::to_time_t(sctp); } /*! * \brief Get table data. * * \param index Table model index. * \param role Kind of data (text, color). * * \return Depending on the role, either text or color. */ QVariant VSWUpdate::data(const QModelIndex& index, int role) const { int row = index.row(); int col = index.column(); if (!index.isValid() || row < 0 || row >= (int)_available.size() || col < 0 || col >= columnCount()) { return QVariant(); } if (role == Qt::DisplayRole) { std::string txt = (col == 0) ? _available[row].fileName : (col == 2) ? _available[row].reserved : (col == 3) ? _available[row].packageName : ""; if (col == 1) { std::stringstream stringstream; std::time_t tt = to_time_t(_available[row].lastWritten); std::tm* gmt = std::gmtime(&tt); std::stringstream buffer; stringstream << std::put_time(gmt, "%d %B %Y %H:%M"); txt = stringstream.str(); } return QString::fromStdString(txt); } // Cell color. if (role == Qt::UserRole + 1) { bool selected = _selected == row; QColor color(0xBFC9CA); if (_available.size() > (unsigned int)row) { std::string rsvd = _available[row].reserved; color = selected ? QColor(0xBFC9CA) : QColor(0x95A5A6); // Gray. if (rsvd == "Factory") { color = selected ? QColor(0xF8C471) : QColor(0xF39C12); // Gold } if (rsvd == "WiFi") { color = selected ? QColor(0x7DCEA0) : QColor(0x27AE60); // Greenish. } if (rsvd == "USB") { color = selected ? QColor(0x7FB3D5) : QColor(0x2980B9); // Blueish. } } if (col == 4) { // scroll color. color = QColor(0x95A5A6); // Gray } return color; } if (row == Qt::UserRole + 2) { return (col < 4) ? 1 : 0; } return QVariant(); } /*! * \brief IEnterBootLoader interface. * * \param asHd If true HD processor if false DG processor. */ void VSWUpdate::sendEnterBootloader(bool asHd) { sendAppCommand(MSG_ID_ENTER_BOOTLOADER_NOW, asHd); } /*! * \brief The start/stop button was clicked. */ void VSWUpdate::startStopBtnClicked() { // Either we are updating or not. if (_updating) { // Abort. _CanInterface.setSWUpdateMode(false); _package.abort(); set_progress("Aborted."); startStopUpdate("Aborted update", false); _ApplicationController.enableKeepAlive(true); } else { // Start. int row_index = (int)_selected; if (row_index >= 0) { std::vector update; std::string package_location = _available[row_index].fullPath; // Copy / cache it (if not already in there). // // The UI has already checked the package as ok before allowing the user to pick it. bool ok = _package.copyUpdate(package_location, _oldUpdates, _maxOld); if (!ok) { writeLog("Failed to cache: " + QString::fromStdString(package_location), true); } // As part of starting, it re-checks and then leaves the file open during the rest of the process // and thus not-modifiable as a stream as a security feature. _ApplicationController.enableKeepAlive(false); _CanInterface.setSWUpdateMode(true); // Tell it to enter bootloader, this is done using the application protocol but from here. // // If these did fail which is extremely unlikely it feels to the user like they didn't quite // press the start button. ok = _package.start_hasKey(package_location, update); if (ok) { // If we got an ok, we are no longer allowed to trust the DG or HD image // that exists so we'll never use MSG_ID_HD_REBOOT_NOW QString info = QString::fromStdString(_package.get_info()); set_progress(info); startStopUpdate("Started update" + info, true); } else { // This is the only point in an update that can fail and just go to the existing image. // Because we didn't write MSG_ID_HD_SET_ENTER_BOOTLOADER this should reboot to the app. // // This should put both into the FW bootloader, which timesout into launching the app if // nothing else is commanded. _package.rebootFW(); set_progress("Failed to start " + QString::fromStdString(_available[row_index].fileName)); writeLog("Failed to start " + QString::fromStdString(package_location), false); writeLog(QString::fromStdString(_package.get_info()), true); } } } // Falling through, success or failure ensure these are set correctly. _ApplicationController.enableKeepAlive(!_updating); _mpBackButton->setEnabled(!_updating); } void VSWUpdate::rowClicked(int row) { // When !_updating it's used to show details when table is clicked. if (!_updating) { if ((row >= 0) && (row < (int)_available.size())) { this->beginResetModel(); _selected = row; QString location = "Cached prior update, last applied "; if (_available[row].reserved == "Factory") { location = "Original factory image, created "; } if (_available[row].reserved == "WiFi") { location = "Downloaded via WiFi, on "; } if (_available[row].reserved == "USB") { location = "From USB drive, written "; } std::time_t tt = to_time_t(_available[row].lastWritten); std::tm* gmt = std::gmtime(&tt); std::stringstream buffer; buffer << std::put_time(gmt, "%A, %d %B %Y %H:%M"); location += QString::fromStdString(buffer.str()); setState( QString::fromStdString(_available[row].fileName) + "\n" + location + "\n" + QString::fromStdString(_available[row].packageName) + "\n" + QString::fromStdString(_available[row].packageVersionInfo)); this->endResetModel(); } else { setState(""); } } } /*! * \brief Shown or hidden. * * \param visible It has become visible or hidden. */ void VSWUpdate::visibilityChanged(bool visible, QObject * qml) { if (_mpBackButton == nullptr) { _mpBackButton = qml->parent()->findChild("_backButton"); } _isVisible = visible; } /*! * \brief Did post pass? * * \param vPass It passed, else failed. */ void VSWUpdate::onPostDone(bool vPass) { _postWasOk = vPass; if (!vPass) { if (_wasTrialBoot) { writeLog("Post failed after update, U-Boot will automatically revert.", false); // Revert by allowing the user to choose to power cycle or not. // On next boot the U-Boot configuration will revert to the other image if this one // is the speculative image. // Basically do nothing, an option would be to sleep and call reboot(). } } } /*! * \brief Were settings fetched ok? */ void VSWUpdate::onSettingsDone() { _settingsOk = true; // Only got here if it was ok. if (_postWasOk) { if (_wasTrialBoot) { // This is a first time boot attempt of this image // make it permanent else it will auto-revert on next reboot. // Both POST and fetch of settings worked, make this the boot image if it isn't already. QProcess* pd = new QProcess(); pd->start(QString::fromStdString("setenv upgrade_available 0; saveenv"), QStringList()); pd->waitForFinished(); // If we got here, we are likely failing at rebooting for some reason and should log it. // User can always manually reboot. writeLog("Reboot to new image successful, new image is now the always boot to image.", false); } } // If it wasn't ok or we never get here, the next power cycle // will auto revert if this is a speculative try once upgrade boot. } /*! * \brief Send an APP layer style message to both FW nodes. * * cmd: 0x808F Enter bootloader (MSG_ID_HD_ENTER_BOOTLOADER_NOW) * 0x8090 Reboot now (MSG_ID_HD_REBOOT_NOW) * 0x8091 Set boot bit to stay in bootloader after reboot (MSG_ID_HD_SET_ENTER_BOOTLOADER) * * \param cmd 16 bit command. * \param asHd As the HD (true) or else the DG (false) */ void VSWUpdate::sendAppCommand(uint16 cmd, bool asHd) { // Do twice. for (std::size_t ii = 0; ii < 2; ii++) { QByteArray msg; msg.append(ePayload_Sync); // 0xA5 // 2 bytes sequence #. uint16_t seqNo = _MessageDispatcher.getSequenceNumber(); msg.append((uint8)(seqNo >> 8)); msg.append((uint8)(seqNo & 0xFF)); // 2 byte msgID. msg.append((uint8)(cmd & 0xFF)); msg.append((uint8)((cmd >> 8) & 0xFF)); // 1 byte payload size = 0. msg.append((uint8)0); // 1 byte app protocol CRC. msg.append(crc8(msg.mid(1))); // Pad 2 bytes zero. msg.append((uint8)0); msg.append((uint8)0); // Send it. // eChlid_UI_HD = 0x100, ///< UI => HD [Out] // eChlid_UI_DG = 0x110, ///< UI => DG [Out] _CanInterface.sendSWUpdateMsg(asHd ? eChlid_UI_HD : eChlid_UI_DG, (uint8_t *)msg.data(), 8); // Sleep long enough to ensure FW acted on it. // // If it fails to act on it, the update will fail almost imediately and // the user can retry, the likelyhood it fails is extremely small, likely 1:1k // or less judging by seeing few RTRs in CAN bus traffic. QThread::currentThread()->msleep(10); } } /*! * \brief Log a message. * * \param msg What to log, human readable text. * \param isDebug Is a debug message. * * \note * Events are used so that the Update code does not tie * to a current logger or prohibit a future logger inplimentation. * * \details * For update, there are really only two levels of logging needed: *
    *
  • The big "event" like things, of starting, * completing, aborting, etc.
  • *
  • The details that are useful for debug, that really * are only worth keeping if a problem was detected.
  • *
      * * The raw data isn't logged as it's generated at times * because the context (effect) is seen a bit after the event. * * To make the logs useful the messages are created with the * extra context information needed to make sense of them. */ void VSWUpdate::writeLog(const QString& msg, bool isDebug) { if (isDebug) { LOG_DEBUG(msg); } else { LOG_APPED_UI(msg); } } // Virtual to make testing easier. void VSWUpdate::reboot() { // TODO: cause reboot, note this function is virtual. // For now just log. writeLog("We are rebooting!", false); #if (REBOOT_ENABLED == 1) QProcess* pd = new QProcess(); pd->start(QString::fromStdString("systemctl --message=\"Software upgrade\" reboot; sleep 1s"), QStringList()); // We want the process we spawned to finish causing the reboot even if the CPU was busy. // Sleep 1 second (just in case) then spin wait for finished. There is a sleep of 1s in the command line. QThread::currentThread()->msleep(1000); pd->waitForFinished(); // If we got here, we are likely failing at rebooting for some reason and should log it. // User can always manually reboot. writeLog("We failed to reboot???", false); setState(_stateMsg + "\nFailed to update, please power cycle!"); #endif } // State. void VSWUpdate::startStopUpdate(const QString& msg, bool asStart) { writeLog(msg, false); // Not debug message emit. _updating = asStart; _progressVisible |= asStart; // Once shown keep. setButtonText(asStart ? "Abort" : "Start"); _CanInterface.setSWUpdateMode(asStart); } // Utility. void VSWUpdate::set_progress(const QString& txt) { _persistent = txt; setState(txt + "\n"); setPercent(0); } void VSWUpdate::update_progress(const QString& txt, float percent) { setState(_persistent + txt + "\n"); setPercent(percent); } void VSWUpdate::clear_progress() { _persistent = ""; setState(""); setPercent(0); } // Periodic update poll. void VSWUpdate::handle_update() { if (!_isVisible) { // Widget is dormant, not even being shown so // do no background scanning. } // While not performing an update, scan and return. if (!_updating) { // Not doing an update, allow CAN setting to happen. scanForUpdates(); return; } std::vector stats = _package.progress(); float percent = 0; float sum = 0; QString update = ""; bool nextUpdate = false; QStringList logMsgs; QStringList dbgMsgs; bool cyberStop = false; QString cyberConcerns = ""; for (std::size_t ii = 0; ii < stats.size(); ii++) { SwUpdate::UiUpdateStatus & s = stats[ii]; if (s.stepName == "Cyber attack") { if (cyberConcerns.size() != 0) { cyberConcerns += ", "; } cyberConcerns += QString::fromStdString(s.targetName); logMsgs.append(QString::fromStdString("Possible cyber attack on " + s.targetName + ", haulting update.")); cyberStop = true; } if (s.inProgress) { update += QString::fromStdString(s.targetName + ": " + s.stepName + " : " + std::to_string(s.stepIndex) + " of " + std::to_string(s.totalSteps) + "\n"); percent += stats[ii].percentTotal; sum += 1; nextUpdate = true; } else { update += QString::fromStdString(s.targetName + ": " + s.stepName + "\n"); if (s.stepName == "Completed") { percent += 100.0; sum += 1; logMsgs.append("Completed update of " + QString::fromStdString(s.targetName)); } } } // Update state, turns update off if done. _updating = nextUpdate; // Handle state things. if (!_updating) { QString info = QString::fromStdString(_package.get_info()); // If there is a script to run do it. std::string script = _package.getScriptIfAny(); if (script.size() > 0) { // Run it. QProcess* pd = new QProcess(); pd->start(QString::fromStdString("." + script), QStringList()); pd->waitForFinished(); } // Signal done. startStopUpdate("Update completed " + info, false); } // Log all messages now. for (const QString& m : logMsgs) { writeLog(m, false); } update_progress(update, sum <= 0 ? 0 : percent / sum); if (cyberStop) { _package.abort(); startStopUpdate("Aborted update, cyber security problem", false); set_progress("Aborted, cyber security problem."); emit securityCompromised("Security concerns while updating: " + cyberConcerns); } if ((!cyberStop) && (!_updating)) { bool allCompleted = true; bool hadUi = false; for (SwUpdate::UiUpdateStatus s : stats) { hadUi |= s.targetName == "Files"; allCompleted &= (s.stepName == "Completed"); } if (allCompleted && hadUi) { emit aboutToReboot(); this->reboot(); } else { std::string msg = std::string("No reboot."); if (!allCompleted) { msg = msg + std::string(" Not all transfers completed ok."); } if (!hadUi) { msg = msg + std::string(" No UI files transfered, no need."); } writeLog(QString::fromStdString(msg), false); } } // Ensure these match up with _updating state. _ApplicationController.enableKeepAlive(!_updating); _mpBackButton->setEnabled(!_updating); } // Utility functions. void VSWUpdate::scanForUpdates() { // Get drive access if not already. checkMountUsb(); // Get the latest key, if it hasn't changed this is fast. _package.set_public(_pubKey.toStdString()); // Look in the folder: updateDir std::vector dirs; for (int ii = 0; ii < _updateDirs.size(); ii++) { QString d = _updateDirs[ii]; dirs.push_back(d.toStdString()); } this->beginResetModel(); int row_selected = _selected; // Local copy. // Scan all the directories. std::vector oldAvailable = _available; _available = _package.get_available(dirs); ui_sort(_available); if (oldAvailable != _available) { // Replace. int newRowSelected = -1; for (std::size_t ii = 0; ii < _available.size(); ii++) { // This is the new row that was previously selected, it // could be a different index but it's the right data. if ((row_selected >= 0) && (_available[ii] == oldAvailable[row_selected])) { newRowSelected = (int)ii; } } // Redo selection so things just magically add. if (newRowSelected >= 0) { _selected = newRowSelected; } } this->endResetModel(); } // Sort packages found for UI table. void VSWUpdate::ui_sort(std::vector& pkgs) { std::size_t index = 0; // Find all factory images. if (_factoryImg.size() > 0) { for (std::size_t ii = 0; ii < pkgs.size(); ii++) { if ((pkgs[ii].fullPath.find(_factoryImg) != std::string::npos) || (pkgs[ii].fullPath == _factoryImg)) { // Swap. if (index != ii) { PackageInfo tmp = pkgs[index]; pkgs[index] = pkgs[ii]; pkgs[ii] = tmp; } pkgs[index].reserved = "Factory"; index++; } } } // Find the wifi. if (_wifiDir.size() > 0) { for (std::size_t ii = index; ii < pkgs.size(); ii++) { if (pkgs[ii].fullPath.find(_wifiDir) != std::string::npos) { // Swap. if (index != ii) { PackageInfo tmp = pkgs[index]; pkgs[index] = pkgs[ii]; pkgs[ii] = tmp; } pkgs[index].reserved = "WiFi"; index++; } } } // USB / old. if (_oldUpdates.size() > 0) { for (std::size_t ii = index; ii < pkgs.size(); ii++) { if (pkgs[ii].reserved.size() == 0) { if (pkgs[ii].fullPath.find(_oldUpdates) == std::string::npos) { // Swap. if (index != ii) { PackageInfo tmp = pkgs[index]; pkgs[index] = pkgs[ii]; pkgs[ii] = tmp; } pkgs[index].reserved = "USB"; index++; } } } } // Rest are Cache. for (std::size_t ii = index; ii < pkgs.size(); ii++) { if (pkgs[ii].reserved.size() == 0) { pkgs[ii].reserved = "Cache"; } } } // Control the USB, both enable and mounting. bool VSWUpdate::controlUSB(bool enable) { /* if (QSysInfo::productType().contains("windows")) { // Development environment, do nothing, just return. return true; } // Disable case: if (!enable) { QProcess* pd = new QProcess(); pd->start("echo 0 > /sys/bus/usb/devices/usb1/authorized", QStringList()); pd->waitForFinished(); return true; } // Enable case: QProcess* pe = new QProcess(); pe->start("echo 1 > /sys/bus/usb/devices/usb1/authorized", QStringList()); pe->waitForFinished(); // Let it kick in for sure. QThread::msleep(250); return true;*/ (void)enable; return true; } void VSWUpdate::checkMountUsb() { static bool mounted = false; static QString oldDevice; QString dev = "/dev/sd"; QString device = ""; for (char a = 'a'; a <= 'z'; a++) { device = dev + a + '1'; if (QFileInfo::exists(device) && (device != oldDevice) && (!mounted)) { #if 0 const char* _usbDrive = ""; bool ok; _usbDrive = vDevice.toLatin1().constData(); ok = ::mount(_usbDrive, USB_Mount_Point, USB_File_System, MS_SYNCHRONOUS | MS_NOEXEC, "") == 0; if (ok) { #endif QProcess* pm = new QProcess(); pm->start("mount " + device + " /media/usb", QStringList()); pm->waitForFinished(); oldDevice = device; mounted = true; return; } } mounted = false; oldDevice = ""; }