view OSTC3Operations.cpp @ 1:0b3630a29ad8

Initial version based on previous repository. Project was ported to QT6 and in now cmake based.
author Ideenmodellierer <tiefenrauscher@web.de>
date Thu, 27 Nov 2025 18:40:28 +0100
parents
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;
}