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