Apart Concept 1 Serial Controller Program with modern UI
This commit is contained in:
commit
c4bde4c18c
103 changed files with 38385 additions and 0 deletions
32
src/transport/ITransport.h
Normal file
32
src/transport/ITransport.h
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
|
||||
// Abstract byte-stream transport to the Concept 1.
|
||||
// Concrete implementations: SerialTransport (real RS-232) and
|
||||
// SimulatorTransport (virtual device). The controller talks only to this
|
||||
// interface, so it is agnostic of where the bytes come from.
|
||||
class ITransport : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ITransport(QObject *parent = nullptr) : QObject(parent) {}
|
||||
~ITransport() override = default;
|
||||
|
||||
virtual bool open() = 0;
|
||||
virtual void close() = 0;
|
||||
virtual bool isOpen() const = 0;
|
||||
virtual void write(const QByteArray &data) = 0;
|
||||
|
||||
// Human-readable name of the endpoint (port name or "Simulator").
|
||||
virtual QString endpointName() const = 0;
|
||||
|
||||
signals:
|
||||
// Emitted for every chunk of raw bytes received from the device.
|
||||
void bytesReceived(const QByteArray &data);
|
||||
// Emitted when the link goes up or down.
|
||||
void openedChanged(bool open);
|
||||
// Emitted on a transport-level error (with a human-readable message).
|
||||
void errorOccurred(const QString &message);
|
||||
};
|
||||
103
src/transport/SerialTransport.cpp
Normal file
103
src/transport/SerialTransport.cpp
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
#include "SerialTransport.h"
|
||||
#include "protocol/Concept1Protocol.h"
|
||||
|
||||
#include <QSerialPort>
|
||||
#include <QSerialPortInfo>
|
||||
|
||||
SerialTransport::SerialTransport(const QString &portName, QObject *parent)
|
||||
: ITransport(parent)
|
||||
, m_port(new QSerialPort(this))
|
||||
, m_portName(portName)
|
||||
{
|
||||
m_port->setBaudRate(Concept1::kBaudRate);
|
||||
m_port->setDataBits(QSerialPort::Data8);
|
||||
m_port->setParity(QSerialPort::NoParity);
|
||||
m_port->setStopBits(QSerialPort::OneStop);
|
||||
m_port->setFlowControl(QSerialPort::NoFlowControl);
|
||||
|
||||
connect(m_port, &QSerialPort::readyRead, this, &SerialTransport::onReadyRead);
|
||||
connect(m_port, &QSerialPort::errorOccurred, this,
|
||||
[this](QSerialPort::SerialPortError e) {
|
||||
if (e == QSerialPort::NoError)
|
||||
return;
|
||||
emit errorOccurred(m_port->errorString());
|
||||
if (e == QSerialPort::ResourceError && m_port->isOpen()) {
|
||||
m_port->close();
|
||||
emit openedChanged(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
SerialTransport::~SerialTransport()
|
||||
{
|
||||
if (m_port->isOpen())
|
||||
m_port->close();
|
||||
}
|
||||
|
||||
void SerialTransport::setPortName(const QString &portName)
|
||||
{
|
||||
m_portName = portName;
|
||||
}
|
||||
|
||||
bool SerialTransport::open()
|
||||
{
|
||||
if (m_port->isOpen())
|
||||
return true;
|
||||
m_port->setPortName(m_portName);
|
||||
const bool ok = m_port->open(QIODevice::ReadWrite);
|
||||
if (ok) {
|
||||
m_port->setBaudRate(Concept1::kBaudRate);
|
||||
m_port->setDataBits(QSerialPort::Data8);
|
||||
m_port->setParity(QSerialPort::NoParity);
|
||||
m_port->setStopBits(QSerialPort::OneStop);
|
||||
m_port->setFlowControl(QSerialPort::NoFlowControl);
|
||||
emit openedChanged(true);
|
||||
} else {
|
||||
emit errorOccurred(m_port->errorString());
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
void SerialTransport::close()
|
||||
{
|
||||
if (m_port->isOpen()) {
|
||||
m_port->close();
|
||||
emit openedChanged(false);
|
||||
}
|
||||
}
|
||||
|
||||
bool SerialTransport::isOpen() const
|
||||
{
|
||||
return m_port->isOpen();
|
||||
}
|
||||
|
||||
void SerialTransport::write(const QByteArray &data)
|
||||
{
|
||||
if (m_port->isOpen())
|
||||
m_port->write(data);
|
||||
}
|
||||
|
||||
QString SerialTransport::endpointName() const
|
||||
{
|
||||
return m_portName;
|
||||
}
|
||||
|
||||
void SerialTransport::onReadyRead()
|
||||
{
|
||||
const QByteArray data = m_port->readAll();
|
||||
if (!data.isEmpty())
|
||||
emit bytesReceived(data);
|
||||
}
|
||||
|
||||
QStringList SerialTransport::availablePorts()
|
||||
{
|
||||
QStringList result;
|
||||
const auto ports = QSerialPortInfo::availablePorts();
|
||||
for (const QSerialPortInfo &info : ports) {
|
||||
QString entry = info.portName();
|
||||
if (!info.description().isEmpty())
|
||||
entry += QStringLiteral(" :: ") + info.description();
|
||||
result << entry;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
35
src/transport/SerialTransport.h
Normal file
35
src/transport/SerialTransport.h
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
#pragma once
|
||||
|
||||
#include "ITransport.h"
|
||||
|
||||
#include <QStringList>
|
||||
|
||||
class QSerialPort;
|
||||
|
||||
// Real RS-232 transport using QtSerialPort.
|
||||
// Fixed line settings per the Concept 1 spec: 19200 8N1, no flow control.
|
||||
// Only compiled when Qt6 SerialPort is available (HAVE_QT_SERIALPORT).
|
||||
class SerialTransport : public ITransport {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit SerialTransport(const QString &portName, QObject *parent = nullptr);
|
||||
~SerialTransport() override;
|
||||
|
||||
bool open() override;
|
||||
void close() override;
|
||||
bool isOpen() const override;
|
||||
void write(const QByteArray &data) override;
|
||||
QString endpointName() const override;
|
||||
|
||||
void setPortName(const QString &portName);
|
||||
|
||||
// List the system serial ports as "name :: description".
|
||||
static QStringList availablePorts();
|
||||
|
||||
private slots:
|
||||
void onReadyRead();
|
||||
|
||||
private:
|
||||
QSerialPort *m_port;
|
||||
QString m_portName;
|
||||
};
|
||||
412
src/transport/SimulatorTransport.cpp
Normal file
412
src/transport/SimulatorTransport.cpp
Normal file
|
|
@ -0,0 +1,412 @@
|
|||
#include "SimulatorTransport.h"
|
||||
|
||||
#include <QStringList>
|
||||
#include <QTimer>
|
||||
|
||||
using namespace Concept1;
|
||||
|
||||
SimulatorTransport::SimulatorTransport(QObject *parent)
|
||||
: ITransport(parent)
|
||||
{
|
||||
}
|
||||
|
||||
bool SimulatorTransport::open()
|
||||
{
|
||||
m_open = true;
|
||||
m_inBuf.clear();
|
||||
emit openedChanged(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
void SimulatorTransport::close()
|
||||
{
|
||||
if (m_open) {
|
||||
m_open = false;
|
||||
emit openedChanged(false);
|
||||
}
|
||||
}
|
||||
|
||||
bool SimulatorTransport::isOpen() const
|
||||
{
|
||||
return m_open;
|
||||
}
|
||||
|
||||
QString SimulatorTransport::endpointName() const
|
||||
{
|
||||
return QStringLiteral("Simulator");
|
||||
}
|
||||
|
||||
void SimulatorTransport::write(const QByteArray &data)
|
||||
{
|
||||
if (!m_open)
|
||||
return;
|
||||
m_inBuf.append(data);
|
||||
// Split on CR; ignore LF entirely.
|
||||
int idx;
|
||||
while ((idx = m_inBuf.indexOf('\r')) >= 0) {
|
||||
QByteArray lineBytes = m_inBuf.left(idx);
|
||||
m_inBuf.remove(0, idx + 1);
|
||||
QString line = QString::fromLatin1(lineBytes);
|
||||
line.remove(QLatin1Char('\n'));
|
||||
line = line.trimmed();
|
||||
if (line.startsWith(QLatin1Char('>')))
|
||||
line = line.mid(1).trimmed();
|
||||
if (m_state.echo) {
|
||||
// Echo back the received instruction (without re-framing header).
|
||||
QString e = line;
|
||||
e += QLatin1Char(kCr);
|
||||
if (m_state.lineFeed) e += QLatin1Char(kLf);
|
||||
const QByteArray bytes = e.toLatin1();
|
||||
QTimer::singleShot(0, this, [this, bytes]() {
|
||||
if (m_open) emit bytesReceived(bytes);
|
||||
});
|
||||
}
|
||||
if (!line.isEmpty())
|
||||
processInstruction(line);
|
||||
}
|
||||
emitQueued();
|
||||
}
|
||||
|
||||
QString SimulatorTransport::frame(const QString &line) const
|
||||
{
|
||||
QString s;
|
||||
if (m_state.header && !m_state.echo)
|
||||
s += QLatin1Char('>');
|
||||
s += line;
|
||||
s += QLatin1Char(kCr);
|
||||
if (m_state.lineFeed)
|
||||
s += QLatin1Char(kLf);
|
||||
return s;
|
||||
}
|
||||
|
||||
void SimulatorTransport::reply(const QString &line)
|
||||
{
|
||||
m_outLines << line;
|
||||
}
|
||||
|
||||
void SimulatorTransport::emitQueued()
|
||||
{
|
||||
if (m_outLines.isEmpty())
|
||||
return;
|
||||
QString out;
|
||||
for (const QString &l : m_outLines)
|
||||
out += frame(l);
|
||||
m_outLines.clear();
|
||||
const QByteArray bytes = out.toLatin1();
|
||||
// Slight async delay mimics serial timing.
|
||||
QTimer::singleShot(5, this, [this, bytes]() {
|
||||
if (m_open)
|
||||
emit bytesReceived(bytes);
|
||||
});
|
||||
}
|
||||
|
||||
QString SimulatorTransport::valueString(Attribute attr, Zone zone) const
|
||||
{
|
||||
const int zi = DeviceState::zoneIndex(zone);
|
||||
switch (attr) {
|
||||
case Attribute::MultiZone: return m_state.multiZone ? "ON" : "OFF";
|
||||
case Attribute::ZoneLink: return m_state.zoneLink ? "ON" : "OFF";
|
||||
case Attribute::MusicLevel: return levelToken(m_state.musicLevel[zi]);
|
||||
case Attribute::MicLevel: return levelToken(m_state.micLevel[zi]);
|
||||
case Attribute::MaxMusicLevel: return levelToken(m_state.maxMusicLevel[zi]);
|
||||
case Attribute::MaxMicLevel: return levelToken(m_state.maxMicLevel[zi]);
|
||||
case Attribute::Select: return sourceToken(m_state.select);
|
||||
case Attribute::EqBass: return QString::number(m_state.eqBass);
|
||||
case Attribute::EqTreble: return QString::number(m_state.eqTreble);
|
||||
case Attribute::Standby: return m_state.standby ? "ON" : "OFF";
|
||||
case Attribute::AutoLoudness: return m_state.autoLoudness ? "ON" : "OFF";
|
||||
case Attribute::PagingActive: return m_state.pagingActive[zi] ? "ON" : "OFF";
|
||||
case Attribute::Echo: return m_state.echo ? "ON" : "OFF";
|
||||
case Attribute::LineFeed: return m_state.lineFeed ? "ON" : "OFF";
|
||||
case Attribute::BackSpace: return m_state.backSpace ? "ON" : "OFF";
|
||||
case Attribute::Header: return m_state.header ? "ON" : "OFF";
|
||||
case Attribute::ValueFeedback: return m_state.valueFeedback ? "ON" : "OFF";
|
||||
default: return QString();
|
||||
}
|
||||
}
|
||||
|
||||
void SimulatorTransport::sendValueFeedback(Attribute attr, Zone zone)
|
||||
{
|
||||
if (!m_state.valueFeedback)
|
||||
return;
|
||||
QString line = attributeToken(attr);
|
||||
if (zone != Zone::None)
|
||||
line += QLatin1Char(' ') + zoneToken(zone);
|
||||
line += QLatin1Char(' ') + valueString(attr, zone);
|
||||
reply(line);
|
||||
}
|
||||
|
||||
void SimulatorTransport::sendInfoDump()
|
||||
{
|
||||
// Mirrors the real GET INFO dump. Zone-qualified when in 2-zone mode.
|
||||
reply(QStringLiteral("PAGING OFF"));
|
||||
reply(QStringLiteral("MULTIZONE ") + (m_state.multiZone ? "ON" : "OFF"));
|
||||
reply(QStringLiteral("ZONELINK ") + (m_state.zoneLink ? "ON" : "OFF"));
|
||||
if (m_state.multiZone) {
|
||||
reply(QStringLiteral("PAGACT ZONE1 ") + (m_state.pagingActive[0] ? "ON" : "OFF"));
|
||||
reply(QStringLiteral("PAGACT ZONE2 ") + (m_state.pagingActive[1] ? "ON" : "OFF"));
|
||||
} else {
|
||||
reply(QStringLiteral("PAGACT ") + (m_state.pagingActive[0] ? "ON" : "OFF"));
|
||||
}
|
||||
reply(QStringLiteral("AUTOLD ") + (m_state.autoLoudness ? "ON" : "OFF"));
|
||||
reply(QStringLiteral("SELECT ") + sourceToken(m_state.select));
|
||||
|
||||
const bool mscZone = m_state.musicNeedsZone();
|
||||
const bool micZone = m_state.micNeedsZone();
|
||||
if (mscZone) {
|
||||
reply("MSCLVL ZONE1 " + levelToken(m_state.musicLevel[0]));
|
||||
reply("MSCLVL ZONE2 " + levelToken(m_state.musicLevel[1]));
|
||||
} else {
|
||||
reply("MSCLVL " + levelToken(m_state.musicLevel[0]));
|
||||
}
|
||||
if (micZone) {
|
||||
reply("MICLVL ZONE1 " + levelToken(m_state.micLevel[0]));
|
||||
reply("MICLVL ZONE2 " + levelToken(m_state.micLevel[1]));
|
||||
} else {
|
||||
reply("MICLVL " + levelToken(m_state.micLevel[0]));
|
||||
}
|
||||
if (mscZone) {
|
||||
reply("MAXMSCLVL ZONE1 " + levelToken(m_state.maxMusicLevel[0]));
|
||||
reply("MAXMSCLVL ZONE2 " + levelToken(m_state.maxMusicLevel[1]));
|
||||
} else {
|
||||
reply("MAXMSCLVL " + levelToken(m_state.maxMusicLevel[0]));
|
||||
}
|
||||
if (micZone) {
|
||||
reply("MAXMICLVL ZONE1 " + levelToken(m_state.maxMicLevel[0]));
|
||||
reply("MAXMICLVL ZONE2 " + levelToken(m_state.maxMicLevel[1]));
|
||||
} else {
|
||||
reply("MAXMICLVL " + levelToken(m_state.maxMicLevel[0]));
|
||||
}
|
||||
reply("EQBASS " + QString::number(m_state.eqBass));
|
||||
reply("EQTREB " + QString::number(m_state.eqTreble));
|
||||
}
|
||||
|
||||
// Parse "OFF"/number into a level; returns false if not parseable.
|
||||
static bool parseLevel(const QString &tok, int *out)
|
||||
{
|
||||
const QString t = tok.trimmed().toUpper();
|
||||
if (t == QLatin1String("OFF")) { *out = kLevelOff; return true; }
|
||||
bool ok = false;
|
||||
const int v = t.toInt(&ok);
|
||||
if (ok) { *out = v; return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
void SimulatorTransport::processInstruction(const QString &instruction)
|
||||
{
|
||||
const QStringList parts = instruction.split(QLatin1Char(' '), Qt::SkipEmptyParts);
|
||||
if (parts.isEmpty())
|
||||
return;
|
||||
|
||||
const QString cmdTok = parts.first().toUpper();
|
||||
Command cmd;
|
||||
if (cmdTok == QLatin1String("SET")) cmd = Command::Set;
|
||||
else if (cmdTok == QLatin1String("GET")) cmd = Command::Get;
|
||||
else if (cmdTok == QLatin1String("INC")) cmd = Command::Inc;
|
||||
else if (cmdTok == QLatin1String("DEC")) cmd = Command::Dec;
|
||||
else { reply("ERROR: Command Unknown!"); return; }
|
||||
|
||||
if (parts.size() < 2) { reply("ERROR: Unknown Attribute!"); return; }
|
||||
|
||||
const Attribute attr = attributeFromToken(parts.at(1));
|
||||
if (attr == Attribute::Unknown) { reply("ERROR: Unknown Attribute!"); return; }
|
||||
|
||||
QStringList rest = parts.mid(2);
|
||||
|
||||
// Optional source (for IPGAIN/SOURCENAME) comes right after attribute.
|
||||
std::optional<Source> source;
|
||||
if ((attr == Attribute::InputGain || attr == Attribute::SourceName) &&
|
||||
!rest.isEmpty()) {
|
||||
if (auto s = sourceFromToken(rest.first())) {
|
||||
source = *s;
|
||||
rest.removeFirst();
|
||||
}
|
||||
}
|
||||
// Optional zone.
|
||||
Zone zone = Zone::None;
|
||||
if (!rest.isEmpty()) {
|
||||
if (auto z = zoneFromToken(rest.first())) {
|
||||
zone = *z;
|
||||
rest.removeFirst();
|
||||
}
|
||||
}
|
||||
const QString valueStr = rest.join(QLatin1Char(' '));
|
||||
const int zi = DeviceState::zoneIndex(zone);
|
||||
|
||||
auto stepValue = [&]() -> int {
|
||||
bool ok = false;
|
||||
int s = valueStr.trimmed().toInt(&ok);
|
||||
if (!ok) s = 1;
|
||||
return clampStep(s);
|
||||
};
|
||||
|
||||
switch (cmd) {
|
||||
case Command::Get:
|
||||
switch (attr) {
|
||||
case Attribute::Info:
|
||||
sendInfoDump();
|
||||
return;
|
||||
case Attribute::InputGain:
|
||||
if (!source) { reply("ERROR: Value Invalid!"); return; }
|
||||
reply("IPGAIN " + sourceToken(*source) + " " +
|
||||
QString::number(m_state.inputGain[DeviceState::sourceIndex(*source)]));
|
||||
return;
|
||||
case Attribute::SourceName:
|
||||
if (!source) { reply("ERROR: Value Invalid!"); return; }
|
||||
// Real device returns the bare name string.
|
||||
reply(m_state.sourceName[DeviceState::sourceIndex(*source)]);
|
||||
return;
|
||||
case Attribute::Serial:
|
||||
reply(m_state.serial.isEmpty() ? QStringLiteral("1A2B") : m_state.serial);
|
||||
return;
|
||||
case Attribute::HwVersion:
|
||||
reply(m_state.hwVersion.isEmpty() ? QStringLiteral("ACPT 1V05") : m_state.hwVersion);
|
||||
return;
|
||||
case Attribute::SwVersion:
|
||||
reply(m_state.swVersion.isEmpty() ? QStringLiteral("SW 1V12") : m_state.swVersion);
|
||||
return;
|
||||
default: {
|
||||
QString line = attributeToken(attr);
|
||||
if (zone != Zone::None)
|
||||
line += QLatin1Char(' ') + zoneToken(zone);
|
||||
line += QLatin1Char(' ') + valueString(attr, zone);
|
||||
reply(line);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
case Command::Set:
|
||||
switch (attr) {
|
||||
case Attribute::MultiZone:
|
||||
m_state.multiZone = (valueStr.toUpper() == "ON" || valueStr == "1");
|
||||
sendValueFeedback(attr);
|
||||
return;
|
||||
case Attribute::ZoneLink:
|
||||
m_state.zoneLink = (valueStr.toUpper() == "ON" || valueStr == "1");
|
||||
sendValueFeedback(attr);
|
||||
return;
|
||||
case Attribute::MusicLevel: {
|
||||
int lvl; if (!parseLevel(valueStr, &lvl)) { reply("ERROR: Value Invalid!"); return; }
|
||||
m_state.musicLevel[zi] = clampLevel(lvl);
|
||||
sendValueFeedback(attr, zone);
|
||||
return;
|
||||
}
|
||||
case Attribute::MicLevel: {
|
||||
int lvl; if (!parseLevel(valueStr, &lvl)) { reply("ERROR: Value Invalid!"); return; }
|
||||
m_state.micLevel[zi] = clampLevel(lvl);
|
||||
sendValueFeedback(attr, zone);
|
||||
return;
|
||||
}
|
||||
case Attribute::MaxMusicLevel: {
|
||||
int lvl; if (!parseLevel(valueStr, &lvl)) { reply("ERROR: Value Invalid!"); return; }
|
||||
m_state.maxMusicLevel[zi] = clampLevel(lvl);
|
||||
sendValueFeedback(attr, zone);
|
||||
return;
|
||||
}
|
||||
case Attribute::MaxMicLevel: {
|
||||
int lvl; if (!parseLevel(valueStr, &lvl)) { reply("ERROR: Value Invalid!"); return; }
|
||||
m_state.maxMicLevel[zi] = clampLevel(lvl);
|
||||
sendValueFeedback(attr, zone);
|
||||
return;
|
||||
}
|
||||
case Attribute::Select: {
|
||||
auto s = sourceFromToken(valueStr);
|
||||
if (!s) { reply("ERROR: Value Invalid!"); return; }
|
||||
m_state.select = *s;
|
||||
sendValueFeedback(attr);
|
||||
return;
|
||||
}
|
||||
case Attribute::EqBass: {
|
||||
bool ok=false; int v=valueStr.toInt(&ok);
|
||||
if (!ok) { reply("ERROR: Value Invalid!"); return; }
|
||||
m_state.eqBass = normalizeEq(v);
|
||||
sendValueFeedback(attr);
|
||||
return;
|
||||
}
|
||||
case Attribute::EqTreble: {
|
||||
bool ok=false; int v=valueStr.toInt(&ok);
|
||||
if (!ok) { reply("ERROR: Value Invalid!"); return; }
|
||||
m_state.eqTreble = normalizeEq(v);
|
||||
sendValueFeedback(attr);
|
||||
return;
|
||||
}
|
||||
case Attribute::Standby:
|
||||
m_state.standby = (valueStr.toUpper() == "ON" || valueStr == "1");
|
||||
sendValueFeedback(attr);
|
||||
return;
|
||||
case Attribute::AutoLoudness:
|
||||
m_state.autoLoudness = (valueStr.toUpper() == "ON" || valueStr == "1");
|
||||
sendValueFeedback(attr);
|
||||
return;
|
||||
case Attribute::PagingActive:
|
||||
m_state.pagingActive[zi] = (valueStr.toUpper() == "ON" || valueStr == "1");
|
||||
sendValueFeedback(attr, zone);
|
||||
return;
|
||||
case Attribute::InputGain: {
|
||||
if (!source) { reply("ERROR: Value Invalid!"); return; }
|
||||
bool ok=false; int v=valueStr.toInt(&ok);
|
||||
if (!ok) { reply("ERROR: Value Invalid!"); return; }
|
||||
m_state.inputGain[DeviceState::sourceIndex(*source)] = normalizeGain(v);
|
||||
reply("Command Executed!"); // IPGAIN is not auto-fed back
|
||||
return;
|
||||
}
|
||||
case Attribute::Echo:
|
||||
m_state.echo = (valueStr.toUpper() == "ON" || valueStr == "1");
|
||||
sendValueFeedback(attr);
|
||||
return;
|
||||
case Attribute::LineFeed:
|
||||
m_state.lineFeed = (valueStr.toUpper() == "ON" || valueStr == "1");
|
||||
sendValueFeedback(attr);
|
||||
return;
|
||||
case Attribute::BackSpace:
|
||||
m_state.backSpace = (valueStr.toUpper() == "ON" || valueStr == "1");
|
||||
sendValueFeedback(attr);
|
||||
return;
|
||||
case Attribute::Header:
|
||||
m_state.header = (valueStr.toUpper() == "ON" || valueStr == "1");
|
||||
sendValueFeedback(attr);
|
||||
return;
|
||||
case Attribute::ValueFeedback:
|
||||
m_state.valueFeedback = (valueStr.toUpper() == "ON" || valueStr == "1");
|
||||
// Always acknowledge the change itself.
|
||||
sendValueFeedback(attr);
|
||||
return;
|
||||
case Attribute::SourceName: {
|
||||
if (!source) { reply("ERROR: Value Invalid!"); return; }
|
||||
m_state.sourceName[DeviceState::sourceIndex(*source)] = valueStr;
|
||||
reply("Command Executed!");
|
||||
return;
|
||||
}
|
||||
case Attribute::Restore:
|
||||
if (valueStr.toUpper() == "ON" || valueStr == "1") {
|
||||
m_state = DeviceState{}; // factory defaults
|
||||
reply("Please Cycle Power!");
|
||||
} else {
|
||||
reply("ERROR: Value Invalid!");
|
||||
}
|
||||
return;
|
||||
default:
|
||||
reply("ERROR: Unknown Instruction!");
|
||||
return;
|
||||
}
|
||||
|
||||
case Command::Inc:
|
||||
case Command::Dec: {
|
||||
const int delta = (cmd == Command::Inc ? 1 : -1) * stepValue();
|
||||
int *target = nullptr;
|
||||
switch (attr) {
|
||||
case Attribute::MusicLevel: target = &m_state.musicLevel[zi]; break;
|
||||
case Attribute::MicLevel: target = &m_state.micLevel[zi]; break;
|
||||
case Attribute::MaxMusicLevel: target = &m_state.maxMusicLevel[zi]; break;
|
||||
case Attribute::MaxMicLevel: target = &m_state.maxMicLevel[zi]; break;
|
||||
default: reply("ERROR: Unknown Instruction!"); return;
|
||||
}
|
||||
int base = *target;
|
||||
if (base <= kLevelOff) base = kLevelOff; // OFF acts as -80 for stepping
|
||||
int next = base + delta;
|
||||
*target = clampLevel(next);
|
||||
sendValueFeedback(attr, zone);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
39
src/transport/SimulatorTransport.h
Normal file
39
src/transport/SimulatorTransport.h
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
#pragma once
|
||||
|
||||
#include "ITransport.h"
|
||||
#include "protocol/Concept1State.h"
|
||||
|
||||
#include <QByteArray>
|
||||
|
||||
// A fully in-memory virtual Concept 1. It interprets incoming ASCII
|
||||
// instructions exactly like the real amplifier and replies with value
|
||||
// feedback / acknowledgements / errors, so the whole GUI can be exercised
|
||||
// without any hardware attached.
|
||||
class SimulatorTransport : public ITransport {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit SimulatorTransport(QObject *parent = nullptr);
|
||||
~SimulatorTransport() override = default;
|
||||
|
||||
bool open() override;
|
||||
void close() override;
|
||||
bool isOpen() const override;
|
||||
void write(const QByteArray &data) override;
|
||||
QString endpointName() const override;
|
||||
|
||||
private:
|
||||
void processInstruction(const QString &instruction);
|
||||
void reply(const QString &line); // queue one reply line
|
||||
void emitQueued(); // flush queued lines as bytes
|
||||
QString frame(const QString &line) const; // apply header/CR/LF framing
|
||||
|
||||
void sendValueFeedback(Concept1::Attribute attr,
|
||||
Concept1::Zone zone = Concept1::Zone::None);
|
||||
QString valueString(Concept1::Attribute attr, Concept1::Zone zone) const;
|
||||
void sendInfoDump();
|
||||
|
||||
Concept1::DeviceState m_state;
|
||||
bool m_open = false;
|
||||
QByteArray m_inBuf;
|
||||
QStringList m_outLines;
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue