diff --git a/.gitignore b/.gitignore index b12a48c7..7b2fc96b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,10 @@ build/* CMakeUserPresets.json cppcheck-* *.cppcheck + +# Tim Littlefair, October 2024 +# When I created a kdevelop project, kdevelop created a directory +# .kdev4 which contains a single file .kdev4/offa-plug.kdev4. +# As this file contains the full path my local build directory +# I'm guessing that it should not be committed to git. +.kdev4 \ No newline at end of file diff --git a/README.md b/README.md index bb40dbf3..aa71bb56 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,12 @@ Please see [Contributing](CONTRIBUTING.md) for how to contribute to this project - [**Qt6**](https://www.qt.io/) - [**libusb-1.0**](http://libusb.info/) +For developers on Ubuntu or other Debian-based distributions, appropriate versions of several of the required packages are unlikely to be installed by default. +As of Ubuntu 24.04 Noble Numbat, the following command is recommended to find the packages required: +''' +sudo apt-get install -y cmake qtcreator qtbase6-dev qt6-qmake libusb-1.0-0-dev \ + cmake-googletest googletest libgtest-dev google-mock libgmock-dev +''' ## Building diff --git a/cmake/50-mustang.rules b/cmake/50-mustang.rules index fae07028..3a8973af 100644 --- a/cmake/50-mustang.rules +++ b/cmake/50-mustang.rules @@ -16,4 +16,9 @@ ATTRS{idVendor}=="1ed8", ATTRS{idProduct}=="0015", ENV{ID_AUDIO_MODELING_AMP}="1 ATTRS{idVendor}=="1ed8", ATTRS{idProduct}=="0016", ENV{ID_AUDIO_MODELING_AMP}="1" ATTRS{idVendor}=="1ed8", ATTRS{idProduct}=="0017", ENV{ID_AUDIO_MODELING_AMP}="1" +# Mustang/Rumble LT series (experimental, even less warranty than usual) +ATTRS{idVendor}=="1ed8", ATTRS{idProduct}=="0037", ENV{ID_AUDIO_MODELING_AMP}="1" +ATTRS{idVendor}=="1ed8", ATTRS{idProduct}=="0038", ENV{ID_AUDIO_MODELING_AMP}="1" +ATTRS{idVendor}=="1ed8", ATTRS{idProduct}=="0046", ENV{ID_AUDIO_MODELING_AMP}="1" + LABEL="mustang_plug_rules_end" diff --git a/doc/USB.md b/doc/USB.md index 73a8a110..cc62077f 100644 --- a/doc/USB.md +++ b/doc/USB.md @@ -27,3 +27,4 @@ | Mustang LT 40S | 1ed8 | 0046 | | | Rumble LT 25 | 1ed8 | 0038 | | | Mustang GT 40 | 1ed8 | 0032 | | +| Mustang Micro | 1ed8 | 0043 | | diff --git a/include/DeviceModel.h b/include/DeviceModel.h index 1fa59bb5..25d0c5cf 100644 --- a/include/DeviceModel.h +++ b/include/DeviceModel.h @@ -31,6 +31,20 @@ namespace plug { MustangV1, MustangV2, + + // Mustang LT 25, LT 40S, LT 50, Rumble LT 25 + // All of these are interoperable over USB with + // Windows/macOS Fender Tone + MustangV3_USB, + + // Mustang GT/GTX series, also Mustang Micro Plus + // (all theoretical - no test devices available) + // According to Fender marketing material, these are interoperable + // over Bluetooth with iOS/Android Fender TONE 4.0 app. + // No support yet, defining the symbol for future use + MustangV3_BT, + + Other }; diff --git a/include/com/ConnectionFactory.h b/include/com/ConnectionFactory.h index 66125e6e..ac219d57 100644 --- a/include/com/ConnectionFactory.h +++ b/include/com/ConnectionFactory.h @@ -27,5 +27,5 @@ namespace plug::com class Mustang; - std::unique_ptr connect(); + std::unique_ptr connect(bool v3usb_devices_enabled); } diff --git a/include/com/PacketSerializer.h b/include/com/PacketSerializer.h index a3d9d259..d9ba17e9 100644 --- a/include/com/PacketSerializer.h +++ b/include/com/PacketSerializer.h @@ -59,5 +59,5 @@ namespace plug::com Packet serializeApplyCommand(fx_pedal_settings effect); std::array, 2> serializeInitCommand(); - + std::array, 3> serializeInitCommand_V3_USB(); } diff --git a/include/com/UsbDevice.h b/include/com/UsbDevice.h index 37517637..36c16cf2 100644 --- a/include/com/UsbDevice.h +++ b/include/com/UsbDevice.h @@ -67,6 +67,7 @@ namespace plug::com::usb std::size_t write(std::uint8_t endpoint, std::uint8_t* data, std::size_t dataSize); std::vector receive(std::uint8_t endpoint, std::size_t dataSize); + std::vector receive_more(std::uint8_t endpoint, std::size_t dataSize); Device& operator=(Device&&) = default; diff --git a/include/ui/mainwindow.h b/include/ui/mainwindow.h index e71bbfce..cadc4716 100644 --- a/include/ui/mainwindow.h +++ b/include/ui/mainwindow.h @@ -61,6 +61,8 @@ namespace plug MainWindow(const MainWindow&) = delete; ~MainWindow() override; + static void enable_v3usb_devices(); + MainWindow& operator=(const MainWindow&) = delete; public slots: @@ -96,6 +98,9 @@ namespace plug SaveToFile* saver; QuickPresets* quickpres; + static bool v3usb_devices_enabled; + + private slots: void about(); void showEffect(std::uint8_t slot); diff --git a/offa-plug.kdev4 b/offa-plug.kdev4 new file mode 100755 index 00000000..d477bb26 --- /dev/null +++ b/offa-plug.kdev4 @@ -0,0 +1,4 @@ +[Project] +CreatedFrom=CMakeLists.txt +Manager=KDevCMakeManager +Name=offa-plug diff --git a/script/manage_nonroot_udev_access.sh b/script/manage_nonroot_udev_access.sh new file mode 100644 index 00000000..ffba7a46 --- /dev/null +++ b/script/manage_nonroot_udev_access.sh @@ -0,0 +1,54 @@ +#!/bin/sh + +usage() { + cat < /dev/null + +sudo udevadm control --reload + +exit 0 + +elif [ "$1" = "disable" ] +then + +sudo rm $udev_rules_filename + +sudo udevadm control --reload + +exit 0 + +else + +usage + +exit 1 + +fi \ No newline at end of file diff --git a/src/Main.cpp b/src/Main.cpp index b14c9805..5dbf90e7 100644 --- a/src/Main.cpp +++ b/src/Main.cpp @@ -24,6 +24,8 @@ #include "ui/mainwindow.h" #include "Version.h" #include +#include +#include int main(int argc, char* argv[]) { @@ -32,9 +34,31 @@ int main(int argc, char* argv[]) QCoreApplication::setApplicationName("Plug"); QCoreApplication::setApplicationVersion(QString::fromStdString(plug::version())); + // following https://doc.qt.io/qt-6/qcommandlineparser.html + QCommandLineParser parser; + parser.setApplicationDescription("Linux program to provide similar services to Fender Fuse/Fender Tone"); + parser.addHelpOption(); + parser.addVersionOption(); + QCommandLineOption enableV3UsbDevicesOption( + "enable-v3usb-devices", + QCoreApplication::translate( + "main", + "Enable incomplete support for USB connected V3 devices controllable with Windows/macOS FenderTone applications " + "(Mustang LT25/LT40S/LT50, Rumble LT 25)." + // TODO: Mustang LT50 PID not integrated in DeviceModel.cpp yet + )); + parser.addOption(enableV3UsbDevicesOption); + parser.process(app); + plug::com::usb::Context context{}; + if (parser.isSet(enableV3UsbDevicesOption)) + { + plug::MainWindow::enable_v3usb_devices(); + } plug::MainWindow window; + + window.show(); return app.exec(); diff --git a/src/com/ConnectionFactory.cpp b/src/com/ConnectionFactory.cpp index 6c52c31b..5f7bf7e7 100644 --- a/src/com/ConnectionFactory.cpp +++ b/src/com/ConnectionFactory.cpp @@ -41,6 +41,11 @@ namespace plug::com inline constexpr std::uint16_t mustangFloor{0x0012}; inline constexpr std::uint16_t mustangI_II_v2{0x0014}; inline constexpr std::uint16_t mustangIII_IV_V_v2{0x0016}; + + inline constexpr std::uint16_t mustangLT25{0x0037}; + inline constexpr std::uint16_t rumbleLT25{0x0038}; + inline constexpr std::uint16_t mustangLT40S{0x0046}; + inline constexpr std::uint16_t mustangMicro{0x0043}; } inline constexpr std::initializer_list pids{ @@ -50,7 +55,13 @@ namespace plug::com usbPID::mustangMini, usbPID::mustangFloor, usbPID::mustangI_II_v2, - usbPID::mustangIII_IV_V_v2}; + usbPID::mustangIII_IV_V_v2, + + usbPID::mustangMicro, + usbPID::mustangLT25, + usbPID::rumbleLT25, + usbPID::mustangLT40S, + }; DeviceModel getModel(std::uint16_t pid) { @@ -70,6 +81,41 @@ namespace plug::com return DeviceModel{"Mustang I/II", DeviceModel::Category::MustangV2, 24}; case usbPID::mustangIII_IV_V_v2: return DeviceModel{"Mustang III/IV/V", DeviceModel::Category::MustangV2, 100}; + + // The economical LT series Mustang/Rumble series, released 2019 to 2020s speak a + // quite different USB protocol from offa-plug. Fender issue applications + // branded Fender Tone for Windows and macOS which offers comparable features + // to offa-plug and the original (now obsolete) Windows/macOS Fender Plug applications + // which was provided to control the devices identified by offa-plug as + // with the DeviceModel::Category::MustangV1 and ..::MustangV2 constants. + // If offa-plug runs with argument --enable-v3usb_devices these will be detected + // and some data will be exchanged. + // A lot more work will be required to interpret this data and implement commands which can + // be triggered from the offa-plug GUI. + case usbPID::mustangLT25: + return DeviceModel{"Mustang LT 25", DeviceModel::Category::MustangV3_USB, 50}; + case usbPID::mustangLT40S: + return DeviceModel{"Mustang LT 40S", DeviceModel::Category::MustangV3_USB, 50}; + + // TODO: add mustangLT50 support when PID is known + + // The Rumble LT25 is believed to be similar protocol wise to the Mustang LT series + case usbPID::rumbleLT25: + return DeviceModel{"Rumble LT 25", DeviceModel::Category::MustangV3_USB, 50}; + + // Testing to date suggest that the Mustang Micro does not respond + // to any of the USB commands sent by the Fender Tone USB version, which is disappointing + // but not surprising given that Fender Tone doesn't interact with this device. + // case usbPID::mustangMicro: + // return DeviceModel{"Mustang Micro", DeviceModel::Category::MustangV3_USB, 0}; + + // The premium GT and GTX series, released from around 2017 are designed to be + // controlled over Bluetooth by iOS/Android mobile applications rather than + // over USB by Windows/macOS applications. + // It is unlikely that offa-plug will ever become interoperable with these, but the + // enumeration value DeviceModel::Category::MustangV3_BT has been reserved for use + // in the event that this should ever happen. + default: throw CommunicationException{"Unknown device pid: " + std::to_string(pid)}; } @@ -77,7 +123,7 @@ namespace plug::com } - std::unique_ptr connect() + std::unique_ptr connect(bool v3usb_devices_enabled) { auto devices = usb::listDevices(); @@ -89,7 +135,12 @@ namespace plug::com { throw CommunicationException{"No device found"}; } - return std::make_unique(getModel(itr->productId()), std::make_shared(std::move(*itr))); + std::unique_ptr retval = std::make_unique(getModel(itr->productId()), std::make_shared(std::move(*itr))); + if (v3usb_devices_enabled == false && retval->getDeviceModel().category() == DeviceModel::Category::MustangV3_USB) + { + throw CommunicationException{"V3 USB device found but not enabled"}; + } + return retval; } } diff --git a/src/com/Mustang.cpp b/src/com/Mustang.cpp index bd7c25c8..bdf4fba6 100644 --- a/src/com/Mustang.cpp +++ b/src/com/Mustang.cpp @@ -19,6 +19,7 @@ * along with this program. If not, see . */ + #include "com/Mustang.h" #include "com/PacketSerializer.h" #include "com/CommunicationException.h" @@ -27,19 +28,39 @@ namespace plug::com { - SignalChain decode_data(const std::array& data) + SignalChain decode_data(const std::array& data, DeviceModel model) { - const auto name = decodeNameFromData(fromRawData(data[0])); - const auto amp = decodeAmpFromData(fromRawData(data[1]), fromRawData(data[6])); - const auto effects = decodeEffectsFromData({{fromRawData(data[2]), fromRawData(data[3]), - fromRawData(data[4]), fromRawData(data[5])}}); + switch (model.category()) + { + case DeviceModel::Category::MustangV1: + case DeviceModel::Category::MustangV2: + { + const auto name = decodeNameFromData(fromRawData(data[0])); + const auto amp = decodeAmpFromData(fromRawData(data[1]), fromRawData(data[6])); + const auto effects = decodeEffectsFromData({{fromRawData(data[2]), fromRawData(data[3]), + fromRawData(data[4]), fromRawData(data[5])}}); + + return SignalChain{name, amp, effects}; + } + + case DeviceModel::Category::MustangV3_USB: + { + const auto name = decodeNameFromData(fromRawData(data[0])); + const amp_settings amp{}; + const std::vector effects; + return SignalChain{name, amp, effects}; + } - return SignalChain{name, amp, effects}; + case DeviceModel::Category::MustangV3_BT: + default: + throw new CommunicationException("Amplifier does not belong to a supported category"); + } } std::vector receivePacket(Connection& conn) { - return conn.receive(packetRawTypeSize); + std::vector retval = conn.receive(packetRawTypeSize); + return retval; } @@ -131,7 +152,7 @@ namespace plug::com SignalChain Mustang::load_memory_bank(std::uint8_t slot) { - return decode_data(loadBankData(*conn, slot)); + return decode_data(loadBankData(*conn, slot), model); } void Mustang::save_effects(std::uint8_t slot, std::string_view name, const std::vector& effects) @@ -181,13 +202,22 @@ namespace plug::com std::array presetData{{}}; std::copy(std::next(recieved_data.cbegin(), numPresetPackets), std::next(recieved_data.cbegin(), numPresetPackets + 7), presetData.begin()); - return {decode_data(presetData), presetNames}; + return {decode_data(presetData, model), presetNames}; } void Mustang::initializeAmp() { - const auto packets = serializeInitCommand(); - std::for_each(packets.cbegin(), packets.cend(), [this](const auto& p) - { sendCommand(*conn, p.getBytes()); }); + if (model.category() == DeviceModel::Category::MustangV3_USB) + { + const auto packets = serializeInitCommand_V3_USB(); + std::for_each(packets.cbegin(), packets.cend(), [this](const auto& p) + { sendCommand(*conn, p.getBytes()); }); + } + else + { + const auto packets = serializeInitCommand(); + std::for_each(packets.cbegin(), packets.cend(), [this](const auto& p) + { sendCommand(*conn, p.getBytes()); }); + } } } diff --git a/src/com/PacketSerializer.cpp b/src/com/PacketSerializer.cpp index 33b2e180..517a2e78 100644 --- a/src/com/PacketSerializer.cpp +++ b/src/com/PacketSerializer.cpp @@ -132,7 +132,6 @@ namespace plug::com } } - std::string decodeNameFromData(const Packet& packet) { return packet.getPayload().getName(); @@ -681,4 +680,62 @@ namespace plug::com header1.setDSP(DSP::none); return {{Packet{header0, EmptyPayload{}}, Packet{header1, EmptyPayload{}}}}; } + + std::array, 3> serializeInitCommand_V3_USB() + { + Header header0{}; + std::array header0Bytes = { + 0x35, + 0x09, + 0x08, + 0x00, + 0x8a, + 0x07, + 0x04, + 0x08, + 0x00, + 0x10, + }; + header0.fromBytes(header0Bytes); + + Header header1{}; + std::array header1Bytes = { + 0x35, + 0x07, + 0x08, + 0x00, + 0xb2, + 0x06, + 0x02, + 0x08, + 0x01, + 0x00, + 0x10, + }; + header1.fromBytes(header1Bytes); + + Header header2{}; + std::array header2Bytes = { + 0x35, + 0x07, + 0x08, + 0x00, + 0xca, + 0x06, + 0x02, + 0x08, + 0x01, + 0x01, + 0x00, + 0x10, + }; + header2.fromBytes(header2Bytes); + + return {{ + Packet{header0, EmptyPayload{}}, + Packet{header1, EmptyPayload{}}, + Packet{header2, EmptyPayload{}}, + }}; + } + } diff --git a/src/com/UsbComm.cpp b/src/com/UsbComm.cpp index 811ba712..6191f038 100644 --- a/src/com/UsbComm.cpp +++ b/src/com/UsbComm.cpp @@ -23,6 +23,31 @@ #include #include +#include +#include + +static void debug_dump(const char* label, std::vector bytes, int* pRetval = NULL) +{ +#ifndef NDEBUG + std::cout << label << ": {"; + std::cout << std::setfill('0') << std::resetiosflags(std::ios::dec) << std::setiosflags(std::ios::hex); + for ( + std::vector::const_iterator pByte = bytes.begin(); + pByte != bytes.end(); + ++pByte) + { + std::cout << " " << std::setw(2) << static_cast(*pByte); + } + std::cout << std::setw(0) << std::setfill(' ') << std::resetiosflags(std::ios::hex) << std::setiosflags(std::ios::dec); + std::cout << " }"; + if (pRetval != NULL) + { + std::cout << "returning " << *pRetval; + } + std::cout << std::endl; +#endif +} + namespace plug::com { namespace @@ -54,7 +79,11 @@ namespace plug::com std::vector UsbComm::receive(std::size_t recvSize) { - return device_.receive(endpointRecv, recvSize); + auto retval = device_.receive(endpointRecv, recvSize); + + debug_dump("UsbComm::receive", retval); + + return retval; } std::string UsbComm::name() const @@ -64,6 +93,10 @@ namespace plug::com std::size_t UsbComm::sendImpl(std::uint8_t* data, std::size_t size) { - return device_.write(endpointSend, data, size); + auto retval = device_.write(endpointSend, data, size); + + debug_dump("UsbComm::sendImpl", std::vector(data, data + size)); + + return retval; } } diff --git a/src/com/UsbDevice.cpp b/src/com/UsbDevice.cpp index bfe56d46..abe033c5 100644 --- a/src/com/UsbDevice.cpp +++ b/src/com/UsbDevice.cpp @@ -18,12 +18,15 @@ * along with this program. If not, see . */ + #include "com/UsbDevice.h" #include "com/UsbException.h" #include #include #include +#include + namespace plug::com::usb { namespace @@ -120,11 +123,17 @@ namespace plug::com::usb std::vector buffer(dataSize); int transfered{0}; - if (const auto result = libusb_interrupt_transfer(handle_.get(), endpoint, buffer.data(), dataSize, &transfered, usbTimeout.count()); (result != LIBUSB_SUCCESS) && (result != LIBUSB_ERROR_TIMEOUT)) + const auto result = libusb_interrupt_transfer(handle_.get(), endpoint, buffer.data(), dataSize, &transfered, usbTimeout.count()); +#ifndef NDEBUG + std::cout << "Device::receive libusb_interrupt_transfer returned " + << result << ", " << transfered << " bytes received" << std::endl; +#endif + if ((result != LIBUSB_SUCCESS) && (result != LIBUSB_ERROR_TIMEOUT)) { throw UsbException{result}; } buffer.resize(transfered); + return buffer; } diff --git a/src/ui/mainwindow.cpp b/src/ui/mainwindow.cpp index 42d065ce..cbb92ddc 100644 --- a/src/ui/mainwindow.cpp +++ b/src/ui/mainwindow.cpp @@ -75,6 +75,7 @@ namespace plug } + bool MainWindow::v3usb_devices_enabled = false; MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), @@ -239,6 +240,11 @@ namespace plug settings.setValue("Windows/mainWindowState", saveState()); } + void MainWindow::enable_v3usb_devices() + { + v3usb_devices_enabled = true; + } + void MainWindow::about() { const QString title{tr("About %1").arg(QCoreApplication::applicationName())}; @@ -271,19 +277,25 @@ namespace plug try { - amp_ops = plug::com::connect(); + amp_ops = plug::com::connect(v3usb_devices_enabled); const auto [signalChain, presets] = amp_ops->start_amp(); + name = QString::fromStdString(signalChain.name()); amplifier_set = signalChain.amp(); effects_set = signalChain.effects(); presetNames = presets; } - catch (const std::exception& ex) + catch (plug::com::CommunicationException& ex) { qWarning() << "ERROR: " << ex.what(); - ui->statusBar->showMessage(QString(tr("Error: %1")).arg(ex.what()), 5000); + ui->statusBar->showMessage(QString(tr("Error: %1")).arg(ex.what()), 0); return; } + catch (std::invalid_argument& ex) + { + qWarning() << "WARNING: " << ex.what(); + ui->statusBar->showMessage(QString(tr("Warning: %1")).arg(ex.what()), 5000); + } load->load_names(presetNames); save->load_names(presetNames); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index c19bb5ab..8ae53b1b 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -13,6 +13,7 @@ add_subdirectory(mocks) add_executable(MustangTest MustangTest.cpp + MustangV3UsbTest.cpp PacketSerializerTest.cpp PacketTest.cpp FxSlotTest.cpp diff --git a/test/ConnectionFactoryTest.cpp b/test/ConnectionFactoryTest.cpp index e844cb4a..b82ba6a2 100644 --- a/test/ConnectionFactoryTest.cpp +++ b/test/ConnectionFactoryTest.cpp @@ -55,7 +55,7 @@ namespace plug::test { EXPECT_CALL(*contextMock, listDevices).WillOnce(Return(ByMove(std::vector{}))); - EXPECT_THROW(connect(), CommunicationException); + EXPECT_THROW(connect(false), CommunicationException); } TEST_F(ConnectionFactoryTest, connectReturnsFirstDeviceFound) @@ -74,8 +74,9 @@ namespace plug::test .WillOnce(Return(0xff04)) .WillRepeatedly(Return(0x0005)); - auto device = connect(); + auto device = connect(false); EXPECT_THAT(device, NotNull()); } + // TODO Add tests to check whether V3USB filtering works as expected } diff --git a/test/MustangV3UsbTest.cpp b/test/MustangV3UsbTest.cpp new file mode 100644 index 00000000..665a4074 --- /dev/null +++ b/test/MustangV3UsbTest.cpp @@ -0,0 +1,660 @@ +/* + * PLUG - software to operate Fender Mustang amplifier + * Linux replacement for Fender FUSE software + * + * Copyright (C) 2017-2024 offa + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "com/Mustang.h" +#include "com/Packet.h" +#include "com/PacketSerializer.h" +#include "com/CommunicationException.h" +#include "mocks/MockConnection.h" +#include "matcher/Matcher.h" +#include "matcher/TypeMatcher.h" +#include +#include + + +// This test class is just a placeholder for the moment, so that someone +// can come back and establish a test to cover the logic specific to the +// MUSTANG_V3_USB category. +// When this happens the #define in the next line can be updated to value 1 +// or the #if blocks referencing it can be made unconditional +#define MUSTANG_V3USB_TESTS_IMPLEMENTED 0 + +namespace plug::test +{ + using namespace plug::test::matcher; + using namespace plug::com; + using namespace testing; + +#if MUSTANG_V3USB_TESTS_IMPLEMENTED + + class MustangV3UsbTest : public testing::Test + { + protected: + void SetUp() override + { + conn = std::make_shared(); + m = std::make_unique(DeviceModel{"Test Device", DeviceModel::Category::MustangV3_USB, 100}, conn); + } + + void TearDown() override + { + } + + [[nodiscard]] std::vector createEmptyPacketData() const + { + return std::vector(packetRawTypeSize, 0x00); + } + + template + [[nodiscard]] auto asBuffer(const Container& c) const + { + return std::vector{std::cbegin(c), std::cend(c)}; + } + + + std::shared_ptr conn; + std::unique_ptr m; + const std::vector noData{}; + const std::vector ignoreData = std::vector(packetRawTypeSize); + const std::vector ignoreAmpData = [] + { std::vector d(packetRawTypeSize, 0x00); d[16] = 0x5e; return d; }(); + const PacketRawType loadCmd = serializeLoadCommand().getBytes(); + const PacketRawType applyCmd = serializeApplyCommand().getBytes(); + static inline constexpr std::size_t numPresetPackets{200}; + static inline constexpr int slot{5}; + }; + + TEST_F(MustangV3UsbTest, startInitializesDevice) + { + const auto [initPacket1, initPacket2] = serializeInitCommand(); + const auto initCmd1 = initPacket1.getBytes(); + const auto initCmd2 = initPacket2.getBytes(); + + InSequence s; + EXPECT_CALL(*conn, isOpen()).WillOnce(Return(true)); + + // Init commands + EXPECT_CALL(*conn, sendImpl(BufferIs(initCmd1), initCmd1.size())).WillOnce(Return(initCmd1.size())); + EXPECT_CALL(*conn, receive(packetRawTypeSize)).WillOnce(Return(ignoreData)); + EXPECT_CALL(*conn, sendImpl(BufferIs(initCmd2), initCmd2.size())).WillOnce(Return(initCmd2.size())); + EXPECT_CALL(*conn, receive(packetRawTypeSize)).WillOnce(Return(ignoreData)); + + // Load cmd + EXPECT_CALL(*conn, sendImpl(BufferIs(loadCmd), loadCmd.size())).WillOnce(Return(loadCmd.size())); + + // Preset names data + EXPECT_CALL(*conn, receive(packetRawTypeSize)).Times(numPresetPackets).WillRepeatedly(Return(ignoreData)); + + // Data + EXPECT_CALL(*conn, receive(packetRawTypeSize)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(ignoreAmpData)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(noData)); + + + m->start_amp(); + } + + TEST_F(MustangV3UsbTest, startThrowsIfConnectionNotReady) + { + EXPECT_CALL(*conn, isOpen()).WillOnce(Return(false)); + EXPECT_THROW(m->start_amp(), plug::com::CommunicationException); + } + + TEST_F(MustangV3UsbTest, startRequestsCurrentPresetName) + { + const auto [initPacket1, initPacket2] = serializeInitCommand(); + const auto initCmd1 = initPacket1.getBytes(); + const auto initCmd2 = initPacket2.getBytes(); + + InSequence s; + EXPECT_CALL(*conn, isOpen()).WillOnce(Return(true)); + + // Init commands + EXPECT_CALL(*conn, sendImpl(BufferIs(initCmd1), initCmd1.size())).WillOnce(Return(initCmd1.size())); + EXPECT_CALL(*conn, receive(packetRawTypeSize)).WillOnce(Return(ignoreData)); + EXPECT_CALL(*conn, sendImpl(BufferIs(initCmd2), initCmd2.size())).WillOnce(Return(initCmd2.size())); + EXPECT_CALL(*conn, receive(packetRawTypeSize)).WillOnce(Return(ignoreData)); + + // Load cmd + EXPECT_CALL(*conn, sendImpl(BufferIs(loadCmd), loadCmd.size())).WillOnce(Return(loadCmd.size())); + + // Preset names data + EXPECT_CALL(*conn, receive(packetRawTypeSize)).Times(numPresetPackets).WillRepeatedly(Return(ignoreData)); + + const std::string actualName{"abc"}; + const auto nameData = asBuffer(serializeName(0, actualName).getBytes()); + + // Data + EXPECT_CALL(*conn, receive(packetRawTypeSize)) + .WillOnce(Return(nameData)) + .WillOnce(Return(ignoreAmpData)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(noData)); + + + const auto [signalChain, presets] = m->start_amp(); + +#if MUSTANG_V3USB_TESTS_IMPLEMENTED + EXPECT_THAT(signalChain.name(), StrEq(actualName)); + + static_cast(presets); +#endif + } + +#if MUSTANG_V3USB_TESTS_IMPLEMENTED + + TEST_F(MustangV3UsbTest, startRequestsCurrentAmp) + { + constexpr amp_settings amp{amps::BRITISH_60S, 4, 8, 5, 9, 1, + cabinets::cabBSSMN, 5, 3, 4, 7, 4, 2, 6, 1, + true, 17}; + const auto recvData = asBuffer(serializeAmpSettings(amp).getBytes()); + const auto extendedData = asBuffer(serializeAmpSettingsUsbGain(amp).getBytes()); + const auto [initPacket1, initPacket2] = serializeInitCommand(); + const auto initCmd1 = initPacket1.getBytes(); + const auto initCmd2 = initPacket2.getBytes(); + + InSequence s; + EXPECT_CALL(*conn, isOpen()).WillOnce(Return(true)); + + // Init commands + EXPECT_CALL(*conn, sendImpl(BufferIs(initCmd1), initCmd1.size())).WillOnce(Return(initCmd1.size())); + EXPECT_CALL(*conn, receive(packetRawTypeSize)).WillOnce(Return(ignoreData)); + EXPECT_CALL(*conn, sendImpl(BufferIs(initCmd2), initCmd2.size())).WillOnce(Return(initCmd2.size())); + EXPECT_CALL(*conn, receive(packetRawTypeSize)).WillOnce(Return(ignoreData)); + + // Load cmd + EXPECT_CALL(*conn, sendImpl(BufferIs(loadCmd), loadCmd.size())).WillOnce(Return(loadCmd.size())); + + // Preset names data + EXPECT_CALL(*conn, receive(packetRawTypeSize)).Times(numPresetPackets).WillRepeatedly(Return(ignoreData)); + + // Data + EXPECT_CALL(*conn, receive(packetRawTypeSize)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(recvData)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(extendedData)) + .WillOnce(Return(noData)); + + + const auto [signalChain, presets] = m->start_amp(); + EXPECT_THAT(signalChain.amp(), AmpIs(amp)); + + static_cast(presets); + } + + TEST_F(MustangV3UsbTest, startRequestsCurrentEffects) + { + constexpr fx_pedal_settings e0{FxSlot{0x00}, effects::TRIANGLE_FLANGER, 10, 20, 30, 40, 50, 0}; + constexpr fx_pedal_settings e1{FxSlot{0x01}, effects::TRIANGLE_CHORUS, 0, 0, 0, 1, 1, 1}; + constexpr fx_pedal_settings e2{FxSlot{0x02}, effects::EMPTY, 0, 0, 0, 0, 0, 0}; + constexpr fx_pedal_settings e3{FxSlot{0x03}, effects::TAPE_DELAY, 1, 2, 3, 4, 5, 6}; + const auto recvData0 = asBuffer(serializeEffectSettings(e0).getBytes()); + const auto recvData1 = asBuffer(serializeEffectSettings(e1).getBytes()); + const auto recvData2 = asBuffer(serializeEffectSettings(e2).getBytes()); + const auto recvData3 = asBuffer(serializeEffectSettings(e3).getBytes()); + const auto [initPacket1, initPacket2] = serializeInitCommand(); + const auto initCmd1 = initPacket1.getBytes(); + const auto initCmd2 = initPacket2.getBytes(); + + + InSequence s; + EXPECT_CALL(*conn, isOpen()).WillOnce(Return(true)); + + // Init commands + EXPECT_CALL(*conn, sendImpl(BufferIs(initCmd1), initCmd1.size())).WillOnce(Return(initCmd1.size())); + EXPECT_CALL(*conn, receive(packetRawTypeSize)).WillOnce(Return(ignoreData)); + EXPECT_CALL(*conn, sendImpl(BufferIs(initCmd2), initCmd2.size())).WillOnce(Return(initCmd2.size())); + EXPECT_CALL(*conn, receive(packetRawTypeSize)).WillOnce(Return(ignoreData)); + + // Load cmd + EXPECT_CALL(*conn, sendImpl(BufferIs(loadCmd), loadCmd.size())).WillOnce(Return(loadCmd.size())); + + // Preset names data + EXPECT_CALL(*conn, receive(packetRawTypeSize)).Times(numPresetPackets).WillRepeatedly(Return(ignoreData)); + + // Data + EXPECT_CALL(*conn, receive(packetRawTypeSize)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(ignoreAmpData)) + .WillOnce(Return(recvData0)) + .WillOnce(Return(recvData1)) + .WillOnce(Return(recvData2)) + .WillOnce(Return(recvData3)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(noData)); + + + const auto [signalChain, presets] = m->start_amp(); + + EXPECT_THAT(signalChain.effects()[0], EffectIs(e0)); + + static_cast(presets); + } + + TEST_F(MustangV3UsbTest, startRequestsAmpPresetList) + { + const auto [initPacket1, initPacket2] = serializeInitCommand(); + const auto initCmd1 = initPacket1.getBytes(); + const auto initCmd2 = initPacket2.getBytes(); + const auto recvData0 = asBuffer(serializeName(0, "abc").getBytes()); + const auto recvData1 = asBuffer(serializeName(0, "def").getBytes()); + const auto recvData2 = asBuffer(serializeName(0, "ghi").getBytes()); + + + InSequence s; + EXPECT_CALL(*conn, isOpen()).WillOnce(Return(true)); + + // Init commands + EXPECT_CALL(*conn, sendImpl(BufferIs(initCmd1), initCmd1.size())).WillOnce(Return(initCmd1.size())); + EXPECT_CALL(*conn, receive(packetRawTypeSize)).WillOnce(Return(ignoreData)); + EXPECT_CALL(*conn, sendImpl(BufferIs(initCmd2), initCmd2.size())).WillOnce(Return(initCmd2.size())); + EXPECT_CALL(*conn, receive(packetRawTypeSize)).WillOnce(Return(ignoreData)); + + // Load cmd + EXPECT_CALL(*conn, sendImpl(BufferIs(loadCmd), loadCmd.size())).WillOnce(Return(loadCmd.size())); + + // Preset names data + EXPECT_CALL(*conn, receive(packetRawTypeSize)) + .WillOnce(Return(recvData0)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(recvData1)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(recvData2)) + .WillOnce(Return(ignoreData)); + EXPECT_CALL(*conn, receive(packetRawTypeSize)).Times(numPresetPackets - 6).WillRepeatedly(Return(ignoreData)); + + // Data + EXPECT_CALL(*conn, receive(packetRawTypeSize)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(ignoreAmpData)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(noData)); + + + const auto [signalChain, presetList] = m->start_amp(); + + EXPECT_THAT(presetList.size(), Eq(numPresetPackets / 2)); + EXPECT_THAT(presetList[0], StrEq("abc")); + EXPECT_THAT(presetList[1], StrEq("def")); + EXPECT_THAT(presetList[2], StrEq("ghi")); + + static_cast(signalChain); + } + + TEST_F(MustangV3UsbTest, startUsesFullInitialTransmissionSizeIfOverThreshold) + { + const auto [initPacket1, initPacket2] = serializeInitCommand(); + const auto initCmd1 = initPacket1.getBytes(); + const auto initCmd2 = initPacket2.getBytes(); + + InSequence s; + EXPECT_CALL(*conn, isOpen()).WillOnce(Return(true)); + + // Init commands + EXPECT_CALL(*conn, sendImpl(BufferIs(initCmd1), initCmd1.size())).WillOnce(Return(initCmd1.size())); + EXPECT_CALL(*conn, receive(packetRawTypeSize)).WillOnce(Return(ignoreData)); + EXPECT_CALL(*conn, sendImpl(BufferIs(initCmd2), initCmd2.size())).WillOnce(Return(initCmd2.size())); + EXPECT_CALL(*conn, receive(packetRawTypeSize)).WillOnce(Return(ignoreData)); + + // Load cmd + EXPECT_CALL(*conn, sendImpl(BufferIs(loadCmd), loadCmd.size())).WillOnce(Return(loadCmd.size())); + + // Preset names data + EXPECT_CALL(*conn, receive(packetRawTypeSize)).Times(numPresetPackets).WillRepeatedly(Return(ignoreData)); + + // Data + EXPECT_CALL(*conn, receive(packetRawTypeSize)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(ignoreAmpData)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(noData)); + + + m->start_amp(); + } + + TEST_F(MustangV3UsbTest, stopAmpClosesConnection) + { + EXPECT_CALL(*conn, close()); + m->stop_amp(); + } + + TEST_F(MustangV3UsbTest, loadMemoryBankSendsBankSelectionCommandAndReceivesPacket) + { + const auto loadSlotCmd = serializeLoadSlotCommand(slot).getBytes(); + + InSequence s; + // Load cmd + EXPECT_CALL(*conn, sendImpl(BufferIs(loadSlotCmd), loadSlotCmd.size())).WillOnce(Return(loadSlotCmd.size())); + + // Data + EXPECT_CALL(*conn, receive(packetRawTypeSize)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(ignoreAmpData)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(noData)); + + + m->load_memory_bank(slot); + } + + TEST_F(MustangV3UsbTest, loadMemoryBankReceivesName) + { + const auto recvData = asBuffer(serializeName(0, "abc").getBytes()); + + InSequence s; + // Load cmd + EXPECT_CALL(*conn, sendImpl(_, _)).WillOnce(Return(packetRawTypeSize)); + + // Data + EXPECT_CALL(*conn, receive(packetRawTypeSize)) + .WillOnce(Return(recvData)) + .WillOnce(Return(ignoreAmpData)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(noData)); + + const auto signalChain = m->load_memory_bank(slot); + EXPECT_THAT(signalChain.name(), StrEq("abc")); + } + + TEST_F(MustangV3UsbTest, loadMemoryBankReceivesAmpValues) + { + + constexpr amp_settings as{amps::BRITISH_80S, 2, 1, 3, 4, 5, + cabinets::cab4x12M, 0, 9, 10, 11, + 0, 0x80, 13, 1, false, 0xab}; + + const auto recvData = asBuffer(serializeAmpSettings(as).getBytes()); + const auto extendedData = asBuffer(serializeAmpSettingsUsbGain(as).getBytes()); + + InSequence s; + // Load cmd + EXPECT_CALL(*conn, sendImpl(_, _)).WillOnce(Return(packetRawTypeSize)); + + // Data + EXPECT_CALL(*conn, receive(packetRawTypeSize)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(recvData)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(extendedData)) + .WillOnce(Return(noData)); + + + const auto signalChain = m->load_memory_bank(slot); + EXPECT_THAT(signalChain.amp(), AmpIs(as)); + } + + TEST_F(MustangV3UsbTest, loadMemoryBankReceivesEffectValues) + { + constexpr fx_pedal_settings e0{FxSlot{0x00}, effects::TRIANGLE_FLANGER, 10, 20, 30, 40, 50, 0}; + constexpr fx_pedal_settings e1{FxSlot{0x01}, effects::TRIANGLE_CHORUS, 0, 0, 0, 1, 1, 0}; + constexpr fx_pedal_settings e2{FxSlot{0x02}, effects::EMPTY, 0, 0, 0, 0, 0, 0}; + constexpr fx_pedal_settings e3{FxSlot{0x03}, effects::TAPE_DELAY, 1, 2, 3, 4, 5, 6}; + const auto recvData0 = asBuffer(serializeEffectSettings(e0).getBytes()); + const auto recvData1 = asBuffer(serializeEffectSettings(e1).getBytes()); + const auto recvData2 = asBuffer(serializeEffectSettings(e2).getBytes()); + const auto recvData3 = asBuffer(serializeEffectSettings(e3).getBytes()); + + + InSequence s; + // Load cmd + EXPECT_CALL(*conn, sendImpl(_, _)).WillOnce(Return(packetRawTypeSize)); + + // Data + EXPECT_CALL(*conn, receive(packetRawTypeSize)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(ignoreAmpData)) + .WillOnce(Return(recvData0)) + .WillOnce(Return(recvData1)) + .WillOnce(Return(recvData2)) + .WillOnce(Return(recvData3)) + .WillOnce(Return(ignoreData)) + .WillOnce(Return(noData)); + + + const auto signalChain = m->load_memory_bank(slot); + + EXPECT_THAT(signalChain.effects(), ElementsAre(EffectIs(e0), EffectIs(e1), EffectIs(e2), EffectIs(e3))); + } + + TEST_F(MustangV3UsbTest, setAmpSendsValues) + { + constexpr amp_settings settings{amps::BRITISH_70S, 8, 9, 1, 2, 3, + cabinets::cab4x12G, 3, 5, 3, 2, 1, + 4, 1, 5, true, 4}; + + const auto data = serializeAmpSettings(settings).getBytes(); + const auto data2 = serializeAmpSettingsUsbGain(settings).getBytes(); + + + InSequence s; + // Data #1 + EXPECT_CALL(*conn, sendImpl(BufferIs(data), data.size())).WillOnce(Return(data.size())); + EXPECT_CALL(*conn, receive(packetRawTypeSize)).WillOnce(Return(ignoreData)); + + // Apply command + EXPECT_CALL(*conn, sendImpl(BufferIs(applyCmd), applyCmd.size())).WillOnce(Return(applyCmd.size())); + EXPECT_CALL(*conn, receive(packetRawTypeSize)).WillOnce(Return(ignoreData)); + + // Data #2 + EXPECT_CALL(*conn, sendImpl(BufferIs(data2), data2.size())).WillOnce(Return(data2.size())); + EXPECT_CALL(*conn, receive(packetRawTypeSize)).WillOnce(Return(ignoreData)); + + // Apply command + EXPECT_CALL(*conn, sendImpl(BufferIs(applyCmd), applyCmd.size())).WillOnce(Return(applyCmd.size())); + EXPECT_CALL(*conn, receive(packetRawTypeSize)).WillOnce(Return(ignoreData)); + + + m->set_amplifier(settings); + } + + TEST_F(MustangV3UsbTest, setEffectSendsValue) + { + constexpr fx_pedal_settings settings{FxSlot{3}, effects::OVERDRIVE, 8, 7, 6, 5, 4, 3}; + const auto data = serializeEffectSettings(settings).getBytes(); + const PacketRawType clearEffect = serializeClearEffectSettings(settings).getBytes(); + + InSequence s; + + // Clear effect command + EXPECT_CALL(*conn, sendImpl(BufferIs(clearEffect), clearEffect.size())).WillOnce(Return(clearEffect.size())); + EXPECT_CALL(*conn, receive(packetRawTypeSize)).WillOnce(Return(ignoreData)); + + // Apply command + EXPECT_CALL(*conn, sendImpl(BufferIs(applyCmd), applyCmd.size())).WillOnce(Return(applyCmd.size())); + EXPECT_CALL(*conn, receive(packetRawTypeSize)).WillOnce(Return(ignoreData)); + + // Data + EXPECT_CALL(*conn, sendImpl(BufferIs(data), data.size())).WillOnce(Return(data.size())); + EXPECT_CALL(*conn, receive(packetRawTypeSize)).WillOnce(Return(ignoreData)); + + // Apply command + EXPECT_CALL(*conn, sendImpl(BufferIs(applyCmd), applyCmd.size())).WillOnce(Return(applyCmd.size())); + EXPECT_CALL(*conn, receive(packetRawTypeSize)).WillOnce(Return(ignoreData)); + + m->set_effect(settings); + } + + TEST_F(MustangV3UsbTest, setEffectDoesNotSendValueIfDisabled) + { + constexpr fx_pedal_settings settings{FxSlot{3}, effects::OVERDRIVE, 8, 7, 6, 5, 4, 3, false}; + const PacketRawType clearEffect = serializeClearEffectSettings(settings).getBytes(); + + InSequence s; + + // Clear effect command + EXPECT_CALL(*conn, sendImpl(BufferIs(clearEffect), clearEffect.size())).WillOnce(Return(clearEffect.size())); + EXPECT_CALL(*conn, receive(packetRawTypeSize)).WillOnce(Return(ignoreData)); + + // Apply command + EXPECT_CALL(*conn, sendImpl(BufferIs(applyCmd), applyCmd.size())).WillOnce(Return(applyCmd.size())); + EXPECT_CALL(*conn, receive(packetRawTypeSize)).WillOnce(Return(ignoreData)); + + m->set_effect(settings); + } + + TEST_F(MustangV3UsbTest, setEffectClearsEffectIfEmptyEffect) + { + constexpr fx_pedal_settings settings{FxSlot{2}, effects::EMPTY, 0, 0, 0, 0, 0, 0}; + const PacketRawType clearCmd = serializeClearEffectSettings(settings).getBytes(); + + + InSequence s; + // Clear command + EXPECT_CALL(*conn, sendImpl(BufferIs(clearCmd), clearCmd.size())).WillOnce(Return(clearCmd.size())); + EXPECT_CALL(*conn, receive(packetRawTypeSize)).WillOnce(Return(ignoreData)); + + // Apply command + EXPECT_CALL(*conn, sendImpl(BufferIs(applyCmd), applyCmd.size())).WillOnce(Return(applyCmd.size())); + EXPECT_CALL(*conn, receive(packetRawTypeSize)).WillOnce(Return(ignoreData)); + + + m->set_effect(settings); + } + + TEST_F(MustangV3UsbTest, saveEffectsSendsValues) + { + const std::vector settings{fx_pedal_settings{FxSlot{1}, effects::MONO_DELAY, 0, 1, 2, 3, 4, 5}, + fx_pedal_settings{FxSlot{2}, effects::SINE_FLANGER, 6, 7, 8, 0, 0, 0}}; + const std::string name = "abcd"; + const auto dataName = serializeSaveEffectName(slot, name, settings).getBytes(); + const auto cmdExecute = serializeApplyCommand(settings[0]).getBytes(); + const auto packets = serializeSaveEffectPacket(slot, settings); + + + InSequence s; + // Save effect name cmd + EXPECT_CALL(*conn, sendImpl(BufferIs(dataName), dataName.size())).WillOnce(Return(0)); + EXPECT_CALL(*conn, receive(packetRawTypeSize)).WillOnce(Return(noData)); + + // Effect #0 + const auto effect0 = packets[0].getBytes(); + EXPECT_CALL(*conn, sendImpl(BufferIs(effect0), effect0.size())).WillOnce(Return(0)); + EXPECT_CALL(*conn, receive(packetRawTypeSize)).WillOnce(Return(noData)); + + // Effect #1 + const auto effect1 = packets[1].getBytes(); + EXPECT_CALL(*conn, sendImpl(BufferIs(effect1), effect1.size())).WillOnce(Return(0)); + EXPECT_CALL(*conn, receive(packetRawTypeSize)).WillOnce(Return(noData)); + + // Apply cmd + EXPECT_CALL(*conn, sendImpl(BufferIs(cmdExecute), cmdExecute.size())).WillOnce(Return(0)); + EXPECT_CALL(*conn, receive(packetRawTypeSize)).WillOnce(Return(noData)); + + + m->save_effects(slot, name, settings); + } + + TEST_F(MustangV3UsbTest, saveEffectsLimitsNumberOfValues) + { + const std::vector settings{fx_pedal_settings{FxSlot{1}, effects::MONO_DELAY, 0, 1, 2, 3, 4, 5}, + fx_pedal_settings{FxSlot{2}, effects::SINE_FLANGER, 6, 7, 8, 0, 0, 0}, + fx_pedal_settings{FxSlot{3}, effects::SINE_FLANGER, 1, 2, 2, 1, 0, 4}}; + const std::string name = "abcd"; + const auto dataName = serializeSaveEffectName(slot, name, settings).getBytes(); + const auto cmdExecute = serializeApplyCommand(settings[0]).getBytes(); + const auto packets = serializeSaveEffectPacket(slot, settings); + + + InSequence s; + // Save effect cmd + EXPECT_CALL(*conn, sendImpl(BufferIs(dataName), dataName.size())).WillOnce(Return(0)); + EXPECT_CALL(*conn, receive(packetRawTypeSize)).WillOnce(Return(noData)); + + // Effect #0 + const auto effect0 = packets[0].getBytes(); + EXPECT_CALL(*conn, sendImpl(BufferIs(effect0), effect0.size())).WillOnce(Return(0)); + EXPECT_CALL(*conn, receive(packetRawTypeSize)).WillOnce(Return(noData)); + + // Apply cmd + EXPECT_CALL(*conn, sendImpl(BufferIs(cmdExecute), cmdExecute.size())).WillOnce(Return(0)); + EXPECT_CALL(*conn, receive(packetRawTypeSize)).WillOnce(Return(noData)); + + m->save_effects(slot, name, settings); + } + + TEST_F(MustangV3UsbTest, saveEffectsDoesNothingOnInvalidEffect) + { + const std::vector settings{fx_pedal_settings{FxSlot{1}, effects::COMPRESSOR, 0, 1, 2, 3, 4, 5}}; + + EXPECT_THROW(m->save_effects(slot, "abcd", settings), std::invalid_argument); + } + + TEST_F(MustangV3UsbTest, saveOnAmp) + { + const std::string name(30, 'x'); + const auto saveNamePacket = serializeName(slot, name).getBytes(); + const auto loadSlotCmd = serializeLoadSlotCommand(slot).getBytes(); + + InSequence s; + EXPECT_CALL(*conn, sendImpl(BufferIs(saveNamePacket), saveNamePacket.size())).WillOnce(Return(saveNamePacket.size())); + EXPECT_CALL(*conn, receive(packetRawTypeSize)).WillOnce(Return(noData)); + EXPECT_CALL(*conn, sendImpl(BufferIs(loadSlotCmd), loadSlotCmd.size())).WillOnce(Return(0)); + + m->save_on_amp(name, slot); + } + + TEST_F(MustangV3UsbTest, getDeviceModelReturnsInfos) + { + const auto model = m->getDeviceModel(); + EXPECT_THAT(model.name(), Eq("Test Device")); + EXPECT_THAT(model.category(), Eq(DeviceModel::Category::MustangV1)); + EXPECT_THAT(model.numberOfPresets(), Eq(100)); + } + +#endif // MUSTANG_V3_USB_TESTS_IMPLEMENTED + +#endif // MUSTANG_V3_USB_TESTS_IMPLEMENTED + +}