view OSTC3Operations.cpp @ 2:177f640940f2

Update exception class and cleanup redifinitions During firmware download and exception caused the application to stop. Rootcause was the defference between QT5 and QT6 exception and string handling which is updated now. In addition some old definitions were removed to avoid compiler warnings.
author Ideenmodellierer
date Fri, 28 Nov 2025 19:57:35 +0100
parents 0b3630a29ad8
children
line wrap: on
line source

//////////////////////////////////////////////////////////////////////////////
/// \file   OSTC3Operations.cpp
/// \brief  Implementing various operations for OSTC3 dive computer
/// \author JD Gascuel.
///
/// \copyright (c) 2011-2016 JD Gascuel. All rights reserved.
/// $Id$
//////////////////////////////////////////////////////////////////////////////
//
// BSD 2-Clause License:
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
// 1. Redistributions of source code must retain the above copyright notice,
//    this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice,
//    this list of conditions and the following disclaimer in the documentation
//    and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
// THE POSSIBILITY OF SUCH DAMAGE.
//
//////////////////////////////////////////////////////////////////////////////

#include "OSTC3Operations.h"

#include "Utils/Exception.h"
#include "Utils/Log.h"
#include "Utils/ProgressEvent.h"

#include "SettingsDialog.h"
#include "HexFile.h"

#include <QApplication>
#include <QDateTime>
#include <QDir>
#include <QRegularExpression>

// Byte extration, compatible littleendian or bigendian.
#define LOW(x)      ((unsigned char)((x) % 256))
#define HIGH(x)     ((unsigned char)((x / (1<<8)) % 256))
#define UPPER(x)    ((unsigned char)((x / (1<<16)) % 256))
#define UP32(x)     ((unsigned char)((x / (1<<24)) % 256))

#define FIRMWARE_AREA   0x3E0000
#define FIRMWARE_SIZE   0x01E000        // 120KB
#define FIRMWARE_BLOCK    0x1000        //   4KB

//////////////////////////////////////////////////////////////////////////////

OSTC3Operations::OSTC3Operations()
  : descriptionString(""),
    emulatorName("OSTC3"),
    _computerFirmware(0),
    _computerSerial(0),
    _connectMode(CLOSED_MODE)
{
    memset(computerText, 0, sizeof computerText);
}

OSTC3Operations::~OSTC3Operations()
{
    if( _connectMode != CLOSED_MODE )
        disconnect(true);
}

//////////////////////////////////////////////////////////////////////////////

QStringList OSTC3Operations::listPorts() const
{
    return listUSBPorts();
}

//////////////////////////////////////////////////////////////////////////////

bool OSTC3Operations::connect()
{
    LOG_TRACE( "Enter download mode..." );

    try {
        _connectMode = CLOSED_MODE;
        _serial.open( Settings::port, emulatorName);
        _serial.sleep(333);    // Initial 1/3 sec. delay to first comm.

        for(int retry=0; retry < 10; ++retry)
        {
            // Allow for 0.1sec extra delay
            try {
                _serial.writeByte(0xBB);
            } catch(const WriteTimeout& ) {
                // Bluetooth not present: one can open the pseudo COM port,
                // but we will have a timeout on first write byte...
                if( retry < 9 ) {
                    LOG_INFO("Cannot connect to " << Settings::port <<" (" << (retry+1) << "/10)...");
                    _serial.sleep(1000);
                    continue;
                }
                LOG_THROW("Cannot connect to " << model() <<".");
                return false;
            }

            _serial.sleep(100);

            //---- Check acknowledge, w/o fatal timeouts.
            unsigned char ok   = 0;
            unsigned char echo = 0;
            try {
                echo = _serial.readByte();

                // Already in connect() mode ???
                if( echo == 'M' )
                    break;

                ok = _serial.readByte();
            }
            catch(const ReadTimeout&) {
                LOG_INFO("Retry " << (retry+1) << "/10...");
            }

            if( echo != 0xBB || ok != 0x4D ) {  // DOWNLOAD modes only.
                if( retry < 9 )
                {
                    _serial.purge();
                    _serial.sleep(400);
                    continue;
                }
                LOG_THROW("Unable to enter hwOS service mode");
                return false;
            }
            break;
        }
        getIdentity();

        QString banner = Log::applicationName();
        writeText(banner);
        LOG_TRACE("Connected.");
        _connectMode = DOWNLOAD_MODE;
        return true;
    }
    catch(const Exception& e) {
        disconnect();
        LOG_THROW("Port " << Settings::port << ": " << e.what());
    }
    return false;
}

//////////////////////////////////////////////////////////////////////////////

bool OSTC3Operations::disconnect(bool /*closing*/)
{
    if( _connectMode == CLOSED_MODE ) return false;

    descriptionString.clear();  // cleanup for interface updateStatus()
    _connectMode = CLOSED_MODE;

    _serial.purge();
    _serial.writeByte(0xFF);    // Exit communications, just in case...
    _serial.sleep(100);

    _serial.purge();
    _serial.close();

    return true;
}

//////////////////////////////////////////////////////////////////////////////

void OSTC3Operations::getIdentity()
{
    descriptionString.clear();

    LOG_TRACE("Getting model...");
    HardwareDescriptor hw = hardwareDescriptor();
 //   if( hw != HW_OSTC3 )
 //        LOG_THROW("Not an OSTC3.");

    LOG_TRACE("Getting identity...");
    getCommonIdentity();

    LOG_INFO("Found " << descriptionString.trimmed());
}

//////////////////////////////////////////////////////////////////////////////

void OSTC3Operations::getCommonIdentity()
{
    unsigned char echo = retryCommand(_serial, 'i'); // 0x69
    if( echo != 'i' )
        LOG_THROW("Bad identity reply (1)");

    unsigned char header[4 + 60 + 1] = {0};
    _serial.readBlock(header, sizeof header);
    if( header[64] != 0x4D )    // DOWNLOAD modes only.
        LOG_THROW("Bad identity reply (2)");

    _computerSerial   = header[0] + header[1]*256;
    _computerFirmware = header[2] * 100 + header[3];
    memcpy(computerText, header+4, sizeof computerText);

    descriptionString = QString("%1 #%2, fw %3.%4, %5")
            .arg(model())
            .arg(serialNumber(), 4, 10, QChar('0'))
            .arg(firmware() / 100).arg(firmware() % 100, 2, 10, QChar('0'))
            .arg( QString::fromLatin1((char*)(computerText), 60)
                .replace(QChar('\0'), " ") );
}

//////////////////////////////////////////////////////////////////////////////

int OSTC3Operations::firmware() const
{
    return _computerFirmware;
}

int OSTC3Operations::serialNumber() const
{
    return _computerSerial;
}

QString OSTC3Operations::customText() const
{
    return QString::fromLatin1(computerText, sizeof computerText);
}

//////////////////////////////////////////////////////////////////////////////

QSize OSTC3Operations::nameSize() const
{
    return QSize(12, 5);
}

//////////////////////////////////////////////////////////////////////////////

void OSTC3Operations::writeText(const QString& msg)
{
    QByteArray buffer = msg.leftJustified(16, ' ', true).toLatin1();
    LOG_TRACE("Echoing string '" << buffer << "'");

    // 2014-10-27 jDG: On OSTC3 v1.60, after an ERASE AREA, we do get
    //                 a spurious L here (instead of n)...
    unsigned char echo = retryCommand(_serial, 'n'); // 0x6E Echo string.
    if( echo != 'n' )
        LOG_THROW("Bad message reply (1)");

    _serial.writeBlock((const unsigned char*)buffer.data(), 16);
    _serial.sleep(25);  // Allow 25msec to display the message...

    unsigned char ok = _serial.readByte();
    if( ok != 0x4C && ok != 0x4D )      // DOWNLOAD or SERVICE modes.
        LOG_THROW("Bad message reply (2)");
}

//////////////////////////////////////////////////////////////////////////////

void OSTC3Operations::setDate(const QDateTime &date)
{
    LOG_TRACE("Set Date " << date.toString("MM/dd/yyyy hh:mm"));

    unsigned char buffer[6];
    buffer[0] = date.time().hour();
    buffer[1] = date.time().minute();
    buffer[2] = date.time().second();
    buffer[3] = date.date().month();
    buffer[4] = date.date().day();
    buffer[5] = date.date().year() % 100;

    unsigned char echo = retryCommand(_serial, 'b'); // 0x62 Sync date
    if( echo != 'b' )
        LOG_THROW("Bad clock reply (1)");

    _serial.writeBlock(buffer, sizeof buffer);
    _serial.sleep(5);

    unsigned char ok = _serial.readByte();
    if( ok != 0x4D )    // DOWNLOAD mode only.
        LOG_THROW("Bad clock reply (2)");

    writeText( "Set " + date.toString("MM/dd hh:mm") );
}

//////////////////////////////////////////////////////////////////////////////

void OSTC3Operations::setName(const QString &newName)
{
    LOG_TRACE("Set Name '" << newName << "'");

    char buffer[60];
    memset(buffer, 0, sizeof buffer);
    strncpy(buffer, newName.toLatin1().constData(), sizeof buffer);

    unsigned char echo = retryCommand(_serial, 'c'); // 0x63 Send custom text
    if( echo != 'c' )
        LOG_THROW("Bad text reply (1)");

    _serial.writeBlock((unsigned char*)buffer, sizeof buffer);
    _serial.sleep(5);

    unsigned char ok = _serial.readByte();
    if( ok != 0x4D )    // DOWNLOAD modes only.
        LOG_THROW("Bad text reply (2)");

    getIdentity();
    // Echo the first line of customtext:
    writeText(newName.left(12).trimmed());
}

//////////////////////////////////////////////////////////////////////////////

void OSTC3Operations::setIcons(const QString &)
{
    LOG_THROW("Set icons: Not yet implemented");
}

QImage OSTC3Operations::dumpScreen() const
{
    LOG_THROW("Dump screen: Not yet implemented");
    return QImage();
}

//////////////////////////////////////////////////////////////////////////////

static unsigned char ostc3SecretKey[16] = {
    241,233, 176, 48,
     69,111, 190, 85,
    255,231, 248, 49,
     19,108, 242,254
};

//////////////////////////////////////////////////////////////////////////////

void OSTC3Operations::eraseRange(unsigned int addr, unsigned int size)
{
    // Convert size to number of pages, rounded up.
    size = ((size + 4095) / 4096);

    // Erase just the needed pages.
    unsigned char buffer[4];
    buffer[0] = UPPER(addr);
    buffer[1] = HIGH(addr);
    buffer[2] = LOW(addr);
    buffer[3] = LOW(size);

    unsigned char reply = retryCommand(_serial, 'B');    // Command 'B'
    if( reply != 0x42 )
        LOG_THROW("eraseRange (1)");

    _serial.writeBlock(buffer, 4);

    // Wait (120/4)ms by block of 4K, plus 3% VAT to be sure.
    _serial.sleep(40 + size * 31);

    unsigned char ok = _serial.readByte();
    if( ok != 0x4c )                    // SERVICE MODE acknowledge.
        LOG_THROW("eraseRange (2)");
}

//////////////////////////////////////////////////////////////////////////////

void OSTC3Operations::writeBlock(unsigned int addr,
                                 const unsigned char *data,
                                 unsigned int size)
{
    unsigned char buffer[3];
    buffer[0] = UPPER(addr);
    buffer[1] = HIGH(addr);
    buffer[2] = LOW(addr);

    unsigned char reply = retryCommand(_serial, '0');    // 0x30
    if( reply != '0' )
        LOG_THROW("startWrite");

    _serial.writeBlock(buffer, sizeof buffer);
    _serial.sleep(2);

    _serial.writeBlock(data, size);
    // Approximated EEPROM write time some 1sec timeout ??
    // 1KB = 240 + 1000 = 1240 : Ok.
    // 4KB = 1.1sec            : Ok.
    _serial.sleep(1100);

    unsigned char ok = _serial.readByte();
    if( ok != 0x4c && ok != 0x4d )      // DOWNLOAD or SERVICE modes.
        LOG_THROW("stopWrite");
}


//////////////////////////////////////////////////////////////////////////////

void OSTC3Operations::readBlock(unsigned int addr, unsigned char* ptr, unsigned int size)
{
    unsigned char buffer[6];
    buffer[0] = UPPER(addr);
    buffer[1] = HIGH(addr);
    buffer[2] = LOW(addr);
    buffer[3] = UPPER(size);
    buffer[4] = HIGH(size);
    buffer[5] = LOW(size);

    unsigned char reply = retryCommand(_serial, 0x20);   // Command ' '
    if( reply != 0x20 )
        LOG_THROW("readBytes");

    _serial.writeBlock(buffer, sizeof buffer);
    _serial.sleep(500);         // Allow some time to start emitting...
    _serial.readBlock(ptr, size);

    // Note: in that case, the OK byte is send AFTER the data block.
    unsigned char ok = _serial.readByte();
    if( ok != 0x4C )                    // SERVICE modes only.
        LOG_THROW("readBytes (2)");
}

//////////////////////////////////////////////////////////////////////////////

void OSTC3Operations::upgradeFirmware(unsigned int checksum)
{
    unsigned char buffer[5];
    buffer[0] = LOW(checksum);
    buffer[1] = HIGH(checksum);
    buffer[2] = UPPER(checksum);
    buffer[3] = UP32(checksum);

    // Compute magic checksum's checksum.
    buffer[4] = 0x55;
    buffer[4] ^= buffer[0]; buffer[4] =(buffer[4]<<1 | buffer[4]>>7);
    buffer[4] ^= buffer[1]; buffer[4] =(buffer[4]<<1 | buffer[4]>>7);
    buffer[4] ^= buffer[2]; buffer[4] =(buffer[4]<<1 | buffer[4]>>7);
    buffer[4] ^= buffer[3]; buffer[4] =(buffer[4]<<1 | buffer[4]>>7);

    unsigned char reply = retryCommand(_serial, 0x50);    // 'P' : send FW to bootloader
    if( reply != 0x50 )
        LOG_THROW("Flashing start (1)");

    _serial.writeBlock(buffer, sizeof buffer);
   unsigned char ok = _serial.readByte();
    if( ok != 0x4C )                    // SERVICE modes only.
        LOG_THROW("Flashing start (2)");

    // NOTE: the device never return, because it always do a reset,
    //       with ot without reprogramming...
    _serial.sleep(500);
    _serial.close();
    descriptionString.clear();
}

QString OSTC3Operations::firmwareTemplate() const
{
    return "*_firmware.hex";
}

#if 0
QRegExp OSTC3Operations::portTemplate() const
{
#if defined(Q_OS_MAC)
    return QRegExp("tty.usbserial-.*", Qt::CaseInsensitive);
#elif defined(Q_OS_LINUX)
    // Seems ok for debian, ubuntu, redhat, CentOS and SUSE (google dixit)
    return QRegExp("ttyUSB.*", Qt::CaseSensitive);
#elif defined(Q_OS_WIN)
    return QRegExp("COM.*", Qt::CaseSensitive);
#endif
}
#endif

QRegularExpression OSTC3Operations::portTemplate() const
{
#if defined(Q_OS_MAC)
    return QRegularExpression("tty.usbserial-.*",
                              QRegularExpression::CaseInsensitiveOption);
#elif defined(Q_OS_LINUX)
    // Debian, Ubuntu, RedHat, CentOS, SUSE
    return QRegularExpression("ttyUSB.*"); // default: case-sensitive
#elif defined(Q_OS_WIN)
    return QRegularExpression("COM.*");    // default: case-sensitive
#endif
}
//////////////////////////////////////////////////////////////////////////////

void OSTC3Operations::connectServiceMode()
{
    LOG_TRACE( "Enter service mode..." );

    // NOTE: Service mode requires a special starting sequence, different from
    //       the usual download mode state.
    //       Also, the general acknowledge byte is changed to 0x4c 'L'.

    assert( _connectMode != SERVICE_MODE );
    _serial.open( Settings::port, emulatorName);
    _serial.sleep(333); // Initial 1/3 sec before trying service mode...

    for(int retry=0; retry < 10; ++retry)
    {
        unsigned char serviceMode[] = { 0xAA, 0xAB, 0xCD, 0xEF };

        try {
            _serial.writeBlock(serviceMode, sizeof serviceMode);
        }
        catch(const WriteTimeout&) {
            // Bluetooth not present: one can open the pseudo COM port,
            // but we will have a timeout on first write byte...
            if( retry < 9 ) {
                LOG_INFO("Cannot connect to " << Settings::port <<" (" << (retry+1) << "/10)...");
                _serial.sleep(1000);
                continue;
            }
            LOG_THROW("Cannot connect to " << model() << ".");
            return;
        }

        // Allow for 0.1sec extra delay
        _serial.sleep(100);

        //---- Check acknowledge:
        unsigned char echo = 0;
        try {
            echo = _serial.readByte();
        }
        catch(...) {}

        if( echo != 0x4b ) {
serviceModeFailed:
            if( retry < 9 )
            {
                _serial.purge();
                _serial.sleep(400);
                continue;
            }
            LOG_THROW("Unable to enter " << model() <<" service mode");
        }

        echo = _serial.readByte();
        if( echo != 0xAB )
            goto serviceModeFailed;
        echo = _serial.readByte();
        if( echo != 0xCD )
            goto serviceModeFailed;
        echo = _serial.readByte();
        if( echo != 0xEF )
            goto serviceModeFailed;
        echo = _serial.readByte();
        if( echo != 0x4c )              // SERVICE modes only.
            goto serviceModeFailed;
        break;
    }
    _connectMode = SERVICE_MODE;
}

//////////////////////////////////////////////////////////////////////////////

void OSTC3Operations::upgradeFW(const QString &fileName)
{
    //---- Load and check firmware -------------------------------------------
    LOG_INFO("Loading firmware.");
    HexFile hex;
    loadFirmware(hex, fileName);

    //---- Enter Service Mode ------------------------------------------------
    connectServiceMode();

    //---- Erase old Firmware ------------------------------------------------
    PROGRESS(0, FIRMWARE_SIZE);
    LOG_INFO("Erasing Firmware.");

    writeText(" Erasing FW...");
    eraseRange(FIRMWARE_AREA, FIRMWARE_SIZE);

    //---- Upload Firmware ---------------------------------------------------
    LOG_INFO("Uploading firmware.");
    writeText(" Uploading...");

    for(int len = 0x00000; len < FIRMWARE_SIZE; len += FIRMWARE_BLOCK)
    {
        unsigned char percent = int(len * 100.0f / FIRMWARE_SIZE + 0.5f);
        writeText( QString(" Uploading %1%")
                       .arg(percent, 2) );
        PROGRESS(percent, 100);

        writeBlock(FIRMWARE_AREA+len, hex.data()+len, FIRMWARE_BLOCK);
    }
    PROGRESS(100, 100);

    //---- Verify firmware ---------------------------------------------------
    LOG_INFO("Verify firmware.");
    writeText(" Verifying...");
    {
        unsigned char* buffer = new unsigned char[FIRMWARE_SIZE];
        Q_CHECK_PTR(buffer);

        for(int len = 0x00000; len < FIRMWARE_SIZE; len += FIRMWARE_BLOCK)
        {
            unsigned char percent = int(len * 100.0f / FIRMWARE_SIZE + 0.5f);
            writeText( QString(" Verifying %1%")
                        .arg(percent, 2) );
            PROGRESS(percent, 100);

            readBlock(FIRMWARE_AREA+len, buffer+len, FIRMWARE_BLOCK);
        }
        PROGRESS(100, 100);
        qApp->processEvents(QEventLoop::ExcludeUserInputEvents, 10);

        for(int i=0; i<FIRMWARE_SIZE; ++i)
            if( buffer[i] != hex.data()[i] )
            {
                writeText(" Verify FAILED");
                LOG_THROW("readback is different");
            }

        delete[] buffer;
    }
    PROGRESS_THROTTLE();

    //---- Flashing firmware -------------------------------------------------
    LOG_INFO("Programming.");
    writeText(" Programming...");
    upgradeFirmware( hex.checksum() );

    //---- Done --------------------------------------------------------------
    // Low-level close, to avoid trying to send a 0xFF byte...
    _serial.close();
    _connectMode = CLOSED_MODE;
    LOG_INFO("Upgrade done.");
    PROGRESS_RESET();
}

void OSTC3Operations::loadFirmware(HexFile& hex, const QString& fileName) const
{
    hex.allocate(FIRMWARE_SIZE);
    hex.loadEncrypted(fileName, ostc3SecretKey);
}

//////////////////////////////////////////////////////////////////////////////

QString OSTC3Operations::model() const
{
    return "OSTC hwOS (USB)";
}

QString OSTC3Operations::description()
{
    return descriptionString;
}

//////////////////////////////////////////////////////////////////////////////

HardwareOperations::CompanionFeatures OSTC3Operations::supported() const
{
    // No ICON, no DUMPSCREEN, no VPM
    return CompanionFeatures(PARAMETERS|DATE|NAME|FIRMWARE
                     |HELIUM_DIVE|CCR_DIVE);
}

//////////////////////////////////////////////////////////////////////////////
void OSTC3Operations::getSignal()
{
    return;
}
void OSTC3Operations::getAllHeader(unsigned char* pBuffer)
{
    return;
}
void OSTC3Operations::writeAllHeader(unsigned char* pBuffer)
{
    return;
}

void OSTC3Operations::getAllSamples(unsigned char* pBuffer)
{
    return;
}
void OSTC3Operations::writeAllSamples(unsigned char* pBuffer)
{
    return;
}