#include "SimulatorTransport.h" #include #include 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; 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; } } }