DJMFix.cpp
author František Kučera <franta-hg@frantovo.cz>
Sat, 06 Sep 2025 23:49:23 +0200
branchv_0
changeset 22 de678a266ab8
parent 19 4ed672cecc25
permissions -rw-r--r--
improved sendKeepAlive logic
     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 model = 0x17;
    67 
    68 	void run() {
    69 		while (!stopped) {
    70 			logger->log(L::FINE, "DJMFixImpl::run()");
    71 			if (sendKeepAlive) {
    72 				send({
    73 					0xf0, 0x00, 0x40, 0x05,
    74 					0x00, 0x00, 0x00, model,
    75 					0x00, 0x50, 0x01, 0xf7
    76 				});
    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 			std::this_thread::sleep_for(chro::milliseconds(keepAliveInterval));
    84 		}
    85 	}
    86 
    87 	void send(const Message& msg) {
    88 		logger->log(L::FINE, "<!-- Sent message: -->" + msg.toString());
    89 		send(codec.encode(msg));
    90 	}
    91 
    92 	void send(const MidiMessage& midiMessage) {
    93 		std::lock_guard<std::recursive_mutex> lock(midiMutex);
    94 		midiSender->send(midiMessage);
    95 	}
    96 
    97 	std::string toString(const Bytes& midiMessage) {
    98 		std::stringstream result;
    99 		for (uint8_t b : midiMessage)
   100 			result << std::hex << std::setw(2) << std::setfill('0') << (int) b;
   101 		return result.str();
   102 	}
   103 
   104 	Bytes normalize(const Bytes& data) {
   105 		if (data.size() % 2) throw std::invalid_argument(
   106 				"Data before normalization must have even number of bytes.");
   107 		Bytes result;
   108 		result.reserve(data.size() / 2);
   109 		for (size_t i = 0; i < data.size() / 2; i++) result.push_back(
   110 				(data[i * 2] & 0x0F) << 4 | (data[i * 2 + 1] & 0x0F));
   111 		return result;
   112 	}
   113 
   114 	Bytes denormalize(const Bytes& data) {
   115 		Bytes result;
   116 		result.reserve(data.size()*2);
   117 		for (size_t i = 0; i < data.size(); i++) {
   118 			result.push_back(data[i] >> 4);
   119 			result.push_back(data[i] & 0x0F);
   120 		}
   121 		return result;
   122 	}
   123 
   124 	uint32_t fnv32hash(const Bytes& buff) {
   125 		uint32_t hash = 0x811c9dc5;
   126 		for (uint8_t b : buff) hash = ((b^hash) * 0x1000193);
   127 		return hash;
   128 	}
   129 
   130 	Bytes toBytes(const uint32_t value) {
   131 		Bytes result;
   132 		result.reserve(4);
   133 		result.push_back(value >> 24);
   134 		result.push_back(value >> 16);
   135 		result.push_back(value >> 8);
   136 		result.push_back(value >> 0);
   137 		return result;
   138 	}
   139 
   140 	bool equals(Bytes a, Bytes b) {
   141 		if (a.size() != b.size()) return false;
   142 		for (size_t i = 0; i < a.size(); i++) if (a[i] != b[i]) return false;
   143 		return true;
   144 	}
   145 
   146 	template<typename T> std::vector<T> concat(
   147 			const std::vector<T>& a,
   148 			const std::vector<T>& b,
   149 			const std::vector<T>& c = {}) //
   150 	{
   151 		std::vector<T> result;
   152 		result.reserve(a.size() + b.size() + c.size());
   153 		for (size_t i = 0; i < a.size(); i++) result.push_back(a[i]);
   154 		for (size_t i = 0; i < b.size(); i++) result.push_back(b[i]);
   155 		for (size_t i = 0; i < c.size(); i++) result.push_back(c[i]);
   156 		return result;
   157 	}
   158 
   159 	template<typename T>
   160 	std::vector<T> xOR(const std::vector<T>& a, const std::vector<T>& b) {
   161 		if (a.size() != b.size()) throw std::invalid_argument(
   162 				"Both must be the same length when doing XOR.");
   163 		std::vector<T> result;
   164 		result.reserve(a.size());
   165 		for (size_t i = 0; i < a.size(); i++) result.push_back(a[i] ^ b[i]);
   166 		return result;
   167 	}
   168 
   169 public:
   170 
   171 	DJMFixImpl(djmfix::logging::Logger* logger)
   172 	: logger(logger ? logger : djmfix::logging::blackhole()) {
   173 	}
   174 
   175 	virtual ~DJMFixImpl() override {
   176 		logger->log(L::FINER, "~DJMFixImpl()");
   177 		if (running) stop();
   178 	}
   179 
   180 	void setDeviceName(std::string name) override {
   181 		logger->log(L::FINE, "DJMFixImpl::setDeviceName(" + name + ")");
   182 
   183 		std::regex djm250pattern("Pioneer DJ Corporation DJM-250MK2.*");
   184 		std::regex djm450pattern("Pioneer DJ Corporation DJM-450.*");
   185 
   186 		if (std::regex_match(name, djm250pattern)) {
   187 			// DJM-250MK2:
   188 			model = 0x17;
   189 			seed3 = {
   190 				0x59, 0xb5, 0x4b, 0xfe, 0xe4,
   191 				0x4a, 0x5a, 0xc8, 0xe4, 0xc5
   192 			};
   193 			logger->log(L::FINE, "Switched to DJM-250MK2 mode");
   194 		} else if (std::regex_match(name, djm450pattern)) {
   195 			// DJM-450:
   196 			// DJM-450 - not tested yet:
   197 			model = 0x13;
   198 			seed3 = {
   199 				0x99, 0xd5, 0x55, 0x43, 0x2c,
   200 				0x70, 0x53, 0x7a, 0x6f, 0x02
   201 			};
   202 			logger->log(L::FINE, "Switched to DJM-450 mode");
   203 		} else {
   204 			// DJM-V10:
   205 			model = 0x34;
   206 			seed3 = {
   207 				0x70, 0x01, 0x4d, 0x05, 0xbe,
   208 				0xf2, 0xe4, 0xde, 0x60, 0xd6
   209 			};
   210 			logger->log(L::FINE, "Switched to DJM-V10 mode");
   211 		}
   212 	}
   213 
   214 	void setMidiSender(MidiSender* midiSender) override {
   215 		logger->log(L::FINER, "DJMFixImpl::setMidiSender()");
   216 		this->midiSender = midiSender;
   217 	}
   218 
   219 	virtual void receive(const MidiMessage& msg) override {
   220 		// TODO: remove try/catch - there should be no unknown messages
   221 		try {
   222 			receive0(msg);
   223 		} catch (const std::exception& e) {
   224 			logger->log(L::SEVERE,
   225 					std::string("Message receiving failed: ") + e.what());
   226 		}
   227 	}
   228 
   229 	virtual void receive0(const MidiMessage& msg) {
   230 		logger->log(L::FINE, "Received a message: "
   231 				"size = " + std::to_string(msg.size()) + " "
   232 				"data = " + toString(msg));
   233 		std::lock_guard<std::recursive_mutex> lock(midiMutex);
   234 		Message msgIn = codec.decode(msg);
   235 
   236 		logger->log(L::FINE, "<!-- Received message: -->" + msgIn.toString());
   237 
   238 		if (msgIn.type == MessageType::D11_GREETING && !greetingReceived) {
   239 			logger->log(L::INFO, "Received greeting message.");
   240 			Message msgOut(MessageType::H12_SEED1, model,{
   241 				{FieldType::F01, name1},
   242 				{FieldType::F02, name2},
   243 				{FieldType::F03, denormalize(seed1)}
   244 			});
   245 			send(msgOut);
   246 			greetingReceived = true;
   247 			logger->log(L::INFO, "Sent message with seed1.");
   248 		} else if (msgIn.type == MessageType::D13_HASH1_SEED2) {
   249 			std::vector<Field> hash1F = msgIn.findFields(FieldType::F04);
   250 			std::vector<Field> seed2F = msgIn.findFields(FieldType::F03);
   251 
   252 			if (hash1F.empty()) throw std::logic_error("hash1 not found");
   253 			if (seed2F.empty()) throw std::logic_error("seed2 not found");
   254 
   255 			hash1 = normalize(hash1F[0].data);
   256 			seed2 = normalize(seed2F[0].data);
   257 
   258 			logger->log(L::INFO, "Received message with "
   259 					"hash1 = " + toString(hash1) + " and "
   260 					"seed2 = " + toString(seed2));
   261 
   262 			Bytes hash1check =
   263 					toBytes(fnv32hash(concat(seed1, xOR(seed0, seed2))));
   264 
   265 			if (equals(hash1, hash1check)) {
   266 				logger->log(L::INFO, "Verification of hash1 was successful.");
   267 				hash2 = toBytes(fnv32hash(concat(seed2, xOR(seed0, seed2))));
   268 
   269 				Message msgOut(MessageType::H14_HASH2, model,{
   270 					{FieldType::F01, name1},
   271 					{FieldType::F02, name2},
   272 					{FieldType::F04, denormalize(hash2)},
   273 					{FieldType::F05, denormalize(seed3)}
   274 				});
   275 				send(msgOut);
   276 				logger->log(L::INFO, "Sent message with hash2.");
   277 			} else {
   278 				std::stringstream logMessage;
   279 				logMessage
   280 						<< "Verification of hash1 failed: "
   281 						<< " midiMessage = " << toString(msg)
   282 						<< " seed0 = " << toString(seed0)
   283 						<< " seed1 = " << toString(seed1)
   284 						<< " seed2 = " << toString(seed2)
   285 						<< " hash1 = " << toString(hash1)
   286 						<< " hash1check = " << toString(hash1check);
   287 				logger->log(L::SEVERE, logMessage.str());
   288 				// TODO: graceful death
   289 			}
   290 		} else if (msgIn.type == MessageType::D15_CONFIRMATION) {
   291 			sendKeepAlive = true;
   292 			logger->log(L::INFO, "Received acknowledgment message. "
   293 					"Started sending keep-alive messages. "
   294 					"LINE/PHONO channels should work now.");
   295 		} else {
   296 			logger->log(L::SEVERE, "Received unexpected message type.");
   297 		}
   298 
   299 	}
   300 
   301 	void start() override {
   302 		logger->log(L::FINE, "DJMFixImpl::start()");
   303 		if (midiSender == nullptr)
   304 			throw std::logic_error("Need a midiSender when starting DJMFix");
   305 
   306 		send({
   307 			0xf0, 0x00, 0x40, 0x05,
   308 			0x00, 0x00, 0x00, model,
   309 			0x00, 0x50, 0x01, 0xf7
   310 		});
   311 
   312 		// TODO: check whether this second message is neccessary for V10:
   313 		send({
   314 			0xf0, 0x00, 0x40, 0x05,
   315 			0x00, 0x00, 0x00, model,
   316 			0x00, 0x03, 0x01, 0xf7
   317 		});
   318 		logger->log(L::INFO, "Sent greeting message.");
   319 
   320 		keepAliveThread = std::thread(&DJMFixImpl::run, this);
   321 		running = true;
   322 
   323 	}
   324 
   325 	void stop() override {
   326 		stopped = true;
   327 		keepAliveThread.join();
   328 		running = false;
   329 		logger->log(L::FINE, "DJMFixImpl::stop()");
   330 	}
   331 };
   332 
   333 DJMFix* create(djmfix::logging::Logger* logger) {
   334 	return new DJMFixImpl(logger);
   335 }
   336 
   337 }