Apart Concept 1 Serial Controller Program with modern UI

This commit is contained in:
Tronax 2026-06-13 09:26:58 +02:00
commit c4bde4c18c
103 changed files with 38385 additions and 0 deletions

View 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;
}
}
}