DJMFix.cpp
author František Kučera <franta-hg@frantovo.cz>
Sun, 11 May 2025 00:30:03 +0200
branchv_0
changeset 18 358a601bfe81
parent 16 63154f9d24a2
child 19 4ed672cecc25
permissions -rw-r--r--
support Pioneer DJ DJM-250MK2 and DJM-V10
     1 /**
     2  * DJM-Fix
     3  * Copyright © 2025 František Kučera (Frantovo.cz, GlobalCode.info)
     4  *
     5  * This program is free software: you can redistribute it and/or modify
     6  * it under the terms of the GNU General Public License as published by
     7  * the Free Software Foundation, version 3 of the License.
     8  *
     9  * This program is distributed in the hope that it will be useful,
    10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  * GNU General Public License for more details.
    13  *
    14  * You should have received a copy of the GNU General Public License
    15  * along with this program. If not, see <http://www.gnu.org/licenses/>.
    16  */
    17 #include <iostream>
    18 #include <sstream>
    19 #include <iomanip>
    20 #include <thread>
    21 #include <mutex>
    22 #include <atomic>
    23 #include <chrono>
    24 #include <stdexcept>
    25 #include <vector>
    26 #include <regex>
    27 
    28 #include "DJMFix.h"
    29 #include "MessageCodec.h"
    30 
    31 namespace djmfix {
    32 
    33 using L = djmfix::logging::Level;
    34 using Bytes = std::vector<uint8_t>;
    35 namespace chro = std::chrono;
    36 
    37 class DJMFixImpl : public DJMFix {
    38 private:
    39 	MidiSender* midiSender;
    40 	djmfix::logging::Logger* logger;
    41 	MessageCodec codec;
    42 	const int keepAliveInterval = 200;
    43 	int keepAliveCounter = 0;
    44 	std::thread keepAliveThread;
    45 	std::recursive_mutex midiMutex;
    46 	std::atomic<bool> running{false};
    47 	std::atomic<bool> stopped{false};
    48 	std::atomic<bool> sendKeepAlive{false};
    49 	/**
    50 	 * Device (V10) may send multiple greeting messages.
    51 	 * It works even if we respond multiple times. But one response is enough.
    52 	 */
    53 	std::atomic<bool> greetingReceived{false};
    54 
    55 	Bytes seed0 = {0x68, 0x01, 0x31, 0xFB};
    56 	Bytes seed1 = {0x29, 0x00, 0x00, 0x00, 0x23, 0x48, 0x00, 0x00};
    57 	Bytes seed2;
    58 	Bytes seed3;
    59 
    60 	Bytes name1 = {0x50, 0x69, 0x6f, 0x6e, 0x65, 0x65, 0x72, 0x44, 0x4a};
    61 	Bytes name2 = {0x72, 0x65, 0x6b, 0x6f, 0x72, 0x64, 0x62, 0x6f, 0x78};
    62 
    63 	Bytes hash1;
    64 	Bytes hash2;
    65 
    66 	uint8_t version = 0x17;
    67 
    68 	void run() {
    69 		while (!stopped) {
    70 			logger->log(L::FINE, "DJMFixImpl::run()");
    71 			if (sendKeepAlive) send({
    72 					0xf0, 0x00, 0x40, 0x05,
    73 					0x00, 0x00, 0x00, version,
    74 					0x00, 0x50, 0x01, 0xf7
    75 				});
    76 			std::this_thread::sleep_for(chro::milliseconds(keepAliveInterval));
    77 			keepAliveCounter++;
    78 			if (keepAliveCounter % (60 * 1000 / keepAliveInterval) == 0)
    79 				logger->log(L::INFO,
    80 					"Still sending periodic keep-alive messages "
    81 					"(each " + std::to_string(keepAliveInterval) + " ms).");
    82 		}
    83 	}
    84 
    85 	void send(const Message& msg) {
    86 		logger->log(L::FINE, "<!-- Sent message: -->" + msg.toString());
    87 		send(codec.encode(msg));
    88 	}
    89 
    90 	void send(const MidiMessage& midiMessage) {
    91 		std::lock_guard<std::recursive_mutex> lock(midiMutex);
    92 		midiSender->send(midiMessage);
    93 	}
    94 
    95 	std::string toString(const Bytes& midiMessage) {
    96 		std::stringstream result;
    97 		for (uint8_t b : midiMessage)
    98 			result << std::hex << std::setw(2) << std::setfill('0') << (int) b;
    99 		return result.str();
   100 	}
   101 
   102 	Bytes normalize(const Bytes& data) {
   103 		if (data.size() % 2) throw std::invalid_argument(
   104 				"Data before normalization must have even number of bytes.");
   105 		Bytes result;
   106 		result.reserve(data.size() / 2);
   107 		for (size_t i = 0; i < data.size() / 2; i++) result.push_back(
   108 				(data[i * 2] & 0x0F) << 4 | (data[i * 2 + 1] & 0x0F));
   109 		return result;
   110 	}
   111 
   112 	Bytes denormalize(const Bytes& data) {
   113 		Bytes result;
   114 		result.reserve(data.size()*2);
   115 		for (size_t i = 0; i < data.size(); i++) {
   116 			result.push_back(data[i] >> 4);
   117 			result.push_back(data[i] & 0x0F);
   118 		}
   119 		return result;
   120 	}
   121 
   122 	uint32_t fnv32hash(const Bytes& buff) {
   123 		uint32_t hash = 0x811c9dc5;
   124 		for (uint8_t b : buff) hash = ((b^hash) * 0x1000193);
   125 		return hash;
   126 	}
   127 
   128 	Bytes toBytes(const uint32_t value) {
   129 		Bytes result;
   130 		result.reserve(4);
   131 		result.push_back(value >> 24);
   132 		result.push_back(value >> 16);
   133 		result.push_back(value >> 8);
   134 		result.push_back(value >> 0);
   135 		return result;
   136 	}
   137 
   138 	bool equals(Bytes a, Bytes b) {
   139 		if (a.size() != b.size()) return false;
   140 		for (size_t i = 0; i < a.size(); i++) if (a[i] != b[i]) return false;
   141 		return true;
   142 	}
   143 
   144 	template<typename T> std::vector<T> concat(
   145 			const std::vector<T>& a,
   146 			const std::vector<T>& b,
   147 			const std::vector<T>& c = {}) //
   148 	{
   149 		std::vector<T> result;
   150 		result.reserve(a.size() + b.size() + c.size());
   151 		for (size_t i = 0; i < a.size(); i++) result.push_back(a[i]);
   152 		for (size_t i = 0; i < b.size(); i++) result.push_back(b[i]);
   153 		for (size_t i = 0; i < c.size(); i++) result.push_back(c[i]);
   154 		return result;
   155 	}
   156 
   157 	template<typename T>
   158 	std::vector<T> xOR(const std::vector<T>& a, const std::vector<T>& b) {
   159 		if (a.size() != b.size()) throw std::invalid_argument(
   160 				"Both must be the same length when doing XOR.");
   161 		std::vector<T> result;
   162 		result.reserve(a.size());
   163 		for (size_t i = 0; i < a.size(); i++) result.push_back(a[i] ^ b[i]);
   164 		return result;
   165 	}
   166 
   167 public:
   168 
   169 	DJMFixImpl(djmfix::logging::Logger* logger)
   170 	: logger(logger ? logger : djmfix::logging::blackhole()) {
   171 	}
   172 
   173 	virtual ~DJMFixImpl() override {
   174 		logger->log(L::FINER, "~DJMFixImpl()");
   175 		if (running) stop();
   176 	}
   177 
   178 	void setDeviceName(std::string name) override {
   179 		logger->log(L::FINE, "DJMFixImpl::setDeviceName(" + name + ")");
   180 
   181 		std::regex djm250pattern("Pioneer DJ Corporation DJM-250MK2.*");
   182 		std::regex djm450pattern(".*450.*"); // TODO: correct pattern
   183 
   184 		if (std::regex_match(name, djm250pattern)) {
   185 			// DJM-250MK2:
   186 			version = 0x17;
   187 			seed3 = {
   188 				0x59, 0xb5, 0x4b, 0xfe, 0xe4,
   189 				0x4a, 0x5a, 0xc8, 0xe4, 0xc5
   190 			};
   191 			logger->log(L::FINE, "Switched to DJM-250MK2 mode");
   192 		} else if (std::regex_match(name, djm450pattern)) {
   193 			// DJM-450:
   194 			// DJM-450 - not tested yet:
   195 			version = 0x13;
   196 			seed0 = {0x8c, 0x5b, 0x3f, 0x5d};
   197 			seed3 = {
   198 				0x08, 0xef, 0x3f, 0x2f, 0x1e,
   199 				0x7a, 0x90, 0x17, 0xf6, 0xaf
   200 			};
   201 			logger->log(L::FINE, "Switched to DJM-450 mode");
   202 		} else {
   203 			// DJM-V10:
   204 			version = 0x34;
   205 			seed3 = {
   206 				0x70, 0x01, 0x4d, 0x05, 0xbe,
   207 				0xf2, 0xe4, 0xde, 0x60, 0xd6
   208 			};
   209 			logger->log(L::FINE, "Switched to DJM-V10 mode");
   210 		}
   211 	}
   212 
   213 	void setMidiSender(MidiSender* midiSender) override {
   214 		logger->log(L::FINER, "DJMFixImpl::setMidiSender()");
   215 		this->midiSender = midiSender;
   216 	}
   217 
   218 	virtual void receive(const MidiMessage& msg) override {
   219 		// TODO: remove try/catch - there should be no unknown messages
   220 		try {
   221 			receive0(msg);
   222 		} catch (const std::exception& e) {
   223 			logger->log(L::SEVERE,
   224 					std::string("Message receiving failed: ") + e.what());
   225 		}
   226 	}
   227 
   228 	virtual void receive0(const MidiMessage& msg) {
   229 		logger->log(L::FINE, "Received a message: "
   230 				"size = " + std::to_string(msg.size()) + " "
   231 				"data = " + toString(msg));
   232 		std::lock_guard<std::recursive_mutex> lock(midiMutex);
   233 		Message msgIn = codec.decode(msg);
   234 
   235 		logger->log(L::FINE, "<!-- Received message: -->" + msgIn.toString());
   236 
   237 		if (msgIn.type == MessageType::D11_GREETING && !greetingReceived) {
   238 			logger->log(L::INFO, "Received greeting message.");
   239 			Message msgOut(MessageType::H12_SEED1, version,{
   240 				{FieldType::F01, name1},
   241 				{FieldType::F02, name2},
   242 				{FieldType::F03, denormalize(seed1)}
   243 			});
   244 			send(msgOut);
   245 			greetingReceived = true;
   246 			logger->log(L::INFO, "Sent message with seed1.");
   247 		} else if (msgIn.type == MessageType::D13_HASH1_SEED2) {
   248 			std::vector<Field> hash1F = msgIn.findFields(FieldType::F04);
   249 			std::vector<Field> seed2F = msgIn.findFields(FieldType::F03);
   250 
   251 			if (hash1F.empty()) throw std::logic_error("hash1 not found");
   252 			if (seed2F.empty()) throw std::logic_error("seed2 not found");
   253 
   254 			hash1 = normalize(hash1F[0].data);
   255 			seed2 = normalize(seed2F[0].data);
   256 
   257 			logger->log(L::INFO, "Received message with "
   258 					"hash1 = " + toString(hash1) + " and "
   259 					"seed2 = " + toString(seed2));
   260 
   261 			Bytes hash1check =
   262 					toBytes(fnv32hash(concat(seed1, xOR(seed0, seed2))));
   263 
   264 			if (equals(hash1, hash1check)) {
   265 				logger->log(L::INFO, "Verification of hash1 was successful.");
   266 				hash2 = toBytes(fnv32hash(concat(seed2, xOR(seed0, seed2))));
   267 
   268 				Message msgOut(MessageType::H14_HASH2, version,{
   269 					{FieldType::F01, name1},
   270 					{FieldType::F02, name2},
   271 					{FieldType::F04, denormalize(hash2)},
   272 					{FieldType::F05, denormalize(seed3)}
   273 				});
   274 				send(msgOut);
   275 				logger->log(L::INFO, "Sent message with hash2.");
   276 			} else {
   277 				std::stringstream logMessage;
   278 				logMessage
   279 						<< "Verification of hash1 failed: "
   280 						<< " midiMessage = " << toString(msg)
   281 						<< " seed0 = " << toString(seed0)
   282 						<< " seed1 = " << toString(seed1)
   283 						<< " seed2 = " << toString(seed2)
   284 						<< " hash1 = " << toString(hash1)
   285 						<< " hash1check = " << toString(hash1check);
   286 				logger->log(L::SEVERE, logMessage.str());
   287 				// TODO: graceful death
   288 			}
   289 		} else if (msgIn.type == MessageType::D15_CONFIRMATION) {
   290 			sendKeepAlive = true;
   291 			logger->log(L::INFO, "Received acknowledgment message. "
   292 					"Started sending keep-alive messages. "
   293 					"LINE/PHONO channels should work now.");
   294 		} else {
   295 			logger->log(L::SEVERE, "Received unexpected message type.");
   296 		}
   297 
   298 	}
   299 
   300 	void start() override {
   301 		logger->log(L::FINE, "DJMFixImpl::start()");
   302 		if (midiSender == nullptr)
   303 			throw std::logic_error("Need a midiSender when starting DJMFix");
   304 
   305 		send({
   306 			0xf0, 0x00, 0x40, 0x05,
   307 			0x00, 0x00, 0x00, version,
   308 			0x00, 0x50, 0x01, 0xf7
   309 		});
   310 
   311 		// TODO: check whether this second message is neccessary for V10:
   312 		send({
   313 			0xf0, 0x00, 0x40, 0x05,
   314 			0x00, 0x00, 0x00, version,
   315 			0x00, 0x03, 0x01, 0xf7
   316 		});
   317 		logger->log(L::INFO, "Sent greeting message.");
   318 
   319 		keepAliveThread = std::thread(&DJMFixImpl::run, this);
   320 		running = true;
   321 
   322 	}
   323 
   324 	void stop() override {
   325 		stopped = true;
   326 		keepAliveThread.join();
   327 		running = false;
   328 		logger->log(L::FINE, "DJMFixImpl::stop()");
   329 	}
   330 };
   331 
   332 DJMFix* create(djmfix::logging::Logger* logger) {
   333 	return new DJMFixImpl(logger);
   334 }
   335 
   336 }