DJMFix.cpp
author František Kučera <franta-hg@frantovo.cz>
Tue, 15 Apr 2025 22:45:25 +0200
branchv_0
changeset 16 63154f9d24a2
parent 13 334b727f7516
child 18 358a601bfe81
permissions -rw-r--r--
code formatting
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@1
    26
franta-hg@1
    27
#include "DJMFix.h"
franta-hg@1
    28
franta-hg@1
    29
namespace djmfix {
franta-hg@1
    30
franta-hg@12
    31
using L = djmfix::logging::Level;
franta-hg@6
    32
using Bytes = std::vector<uint8_t>;
franta-hg@16
    33
namespace chro = std::chrono;
franta-hg@6
    34
franta-hg@1
    35
class DJMFixImpl : public DJMFix {
franta-hg@1
    36
private:
franta-hg@2
    37
	MidiSender* midiSender;
franta-hg@12
    38
	djmfix::logging::Logger* logger;
franta-hg@13
    39
	const int keepAliveInterval = 200;
franta-hg@13
    40
	int keepAliveCounter = 0;
franta-hg@2
    41
	std::thread keepAliveThread;
franta-hg@6
    42
	std::recursive_mutex midiMutex;
franta-hg@5
    43
	std::atomic<bool> running{false};
franta-hg@5
    44
	std::atomic<bool> stopped{false};
franta-hg@8
    45
	std::atomic<bool> sendKeepAlive{false};
franta-hg@8
    46
	Bytes seed2;
franta-hg@2
    47
franta-hg@2
    48
	void run() {
franta-hg@2
    49
		while (!stopped) {
franta-hg@12
    50
			logger->log(L::FINE, "DJMFixImpl::run()");
franta-hg@16
    51
			if (sendKeepAlive) send({
franta-hg@16
    52
					0xf0, 0x00, 0x40, 0x05,
franta-hg@16
    53
					0x00, 0x00, 0x00, 0x17,
franta-hg@16
    54
					0x00, 0x50, 0x01, 0xf7
franta-hg@16
    55
				});
franta-hg@16
    56
			std::this_thread::sleep_for(chro::milliseconds(keepAliveInterval));
franta-hg@13
    57
			keepAliveCounter++;
franta-hg@16
    58
			if (keepAliveCounter % (60 * 1000 / keepAliveInterval) == 0)
franta-hg@16
    59
				logger->log(L::INFO,
franta-hg@16
    60
					"Still sending periodic keep-alive messages "
franta-hg@16
    61
					"(each " + std::to_string(keepAliveInterval) + " ms).");
franta-hg@2
    62
		}
franta-hg@2
    63
	}
franta-hg@2
    64
franta-hg@6
    65
	void send(const MidiMessage& midiMessage) {
franta-hg@6
    66
		std::lock_guard<std::recursive_mutex> lock(midiMutex);
franta-hg@6
    67
		midiSender->send(midiMessage);
franta-hg@6
    68
	}
franta-hg@6
    69
franta-hg@6
    70
	std::string toString(const Bytes& midiMessage) {
franta-hg@5
    71
		std::stringstream result;
franta-hg@16
    72
		for (uint8_t b : midiMessage)
franta-hg@16
    73
			result << std::hex << std::setw(2) << std::setfill('0') << (int) b;
franta-hg@5
    74
		return result.str();
franta-hg@5
    75
	}
franta-hg@5
    76
franta-hg@6
    77
	Bytes normalize(const Bytes& data) {
franta-hg@16
    78
		if (data.size() % 2) throw std::invalid_argument(
franta-hg@16
    79
				"Data before normalization must have even number of bytes.");
franta-hg@6
    80
		Bytes result;
franta-hg@6
    81
		result.reserve(data.size() / 2);
franta-hg@16
    82
		for (size_t i = 0; i < data.size() / 2; i++) result.push_back(
franta-hg@16
    83
				(data[i * 2] & 0x0F) << 4 | (data[i * 2 + 1] & 0x0F));
franta-hg@6
    84
		return result;
franta-hg@6
    85
	}
franta-hg@6
    86
franta-hg@8
    87
	Bytes denormalize(const Bytes& data) {
franta-hg@8
    88
		Bytes result;
franta-hg@8
    89
		result.reserve(data.size()*2);
franta-hg@8
    90
		for (size_t i = 0; i < data.size(); i++) {
franta-hg@8
    91
			result.push_back(data[i] >> 4);
franta-hg@8
    92
			result.push_back(data[i] & 0x0F);
franta-hg@8
    93
		}
franta-hg@8
    94
		return result;
franta-hg@8
    95
	}
franta-hg@8
    96
franta-hg@6
    97
	uint32_t fnv32hash(const Bytes& buff) {
franta-hg@6
    98
		uint32_t hash = 0x811c9dc5;
franta-hg@6
    99
		for (uint8_t b : buff) hash = ((b^hash) * 0x1000193);
franta-hg@6
   100
		return hash;
franta-hg@6
   101
	}
franta-hg@6
   102
franta-hg@6
   103
	Bytes toBytes(const uint32_t value) {
franta-hg@6
   104
		Bytes result;
franta-hg@6
   105
		result.reserve(4);
franta-hg@6
   106
		result.push_back(value >> 24);
franta-hg@6
   107
		result.push_back(value >> 16);
franta-hg@6
   108
		result.push_back(value >> 8);
franta-hg@6
   109
		result.push_back(value >> 0);
franta-hg@6
   110
		return result;
franta-hg@6
   111
	}
franta-hg@6
   112
franta-hg@6
   113
	bool equals(Bytes a, Bytes b) {
franta-hg@6
   114
		if (a.size() != b.size()) return false;
franta-hg@6
   115
		for (size_t i = 0; i < a.size(); i++) if (a[i] != b[i]) return false;
franta-hg@6
   116
		return true;
franta-hg@6
   117
	}
franta-hg@6
   118
franta-hg@16
   119
	template<typename T> std::vector<T> concat(
franta-hg@16
   120
			const std::vector<T>& a,
franta-hg@16
   121
			const std::vector<T>& b,
franta-hg@16
   122
			const std::vector<T>& c = {}) //
franta-hg@16
   123
	{
franta-hg@6
   124
		std::vector<T> result;
franta-hg@6
   125
		result.reserve(a.size() + b.size() + c.size());
franta-hg@6
   126
		for (size_t i = 0; i < a.size(); i++) result.push_back(a[i]);
franta-hg@6
   127
		for (size_t i = 0; i < b.size(); i++) result.push_back(b[i]);
franta-hg@6
   128
		for (size_t i = 0; i < c.size(); i++) result.push_back(c[i]);
franta-hg@6
   129
		return result;
franta-hg@6
   130
	}
franta-hg@6
   131
franta-hg@16
   132
	template<typename T>
franta-hg@16
   133
	std::vector<T> xOR(const std::vector<T>& a, const std::vector<T>& b) {
franta-hg@16
   134
		if (a.size() != b.size()) throw std::invalid_argument(
franta-hg@16
   135
				"Both must be the same length when doing XOR.");
franta-hg@6
   136
		std::vector<T> result;
franta-hg@6
   137
		result.reserve(a.size());
franta-hg@6
   138
		for (size_t i = 0; i < a.size(); i++) result.push_back(a[i] ^ b[i]);
franta-hg@6
   139
		return result;
franta-hg@6
   140
	}
franta-hg@6
   141
franta-hg@1
   142
public:
franta-hg@1
   143
franta-hg@16
   144
	DJMFixImpl(djmfix::logging::Logger* logger)
franta-hg@16
   145
	: logger(logger ? logger : djmfix::logging::blackhole()) {
franta-hg@12
   146
	}
franta-hg@12
   147
franta-hg@1
   148
	virtual ~DJMFixImpl() override {
franta-hg@13
   149
		logger->log(L::FINER, "~DJMFixImpl()");
franta-hg@2
   150
		if (running) stop();
franta-hg@2
   151
	}
franta-hg@2
   152
franta-hg@2
   153
	void setMidiSender(MidiSender* midiSender) {
franta-hg@13
   154
		logger->log(L::FINER, "DJMFixImpl::setMidiSender()");
franta-hg@2
   155
		this->midiSender = midiSender;
franta-hg@1
   156
	}
franta-hg@1
   157
franta-hg@16
   158
	virtual void receive(const MidiMessage& msg) override {
franta-hg@16
   159
		logger->log(L::FINE, "Received a message: "
franta-hg@16
   160
				"size = " + std::to_string(msg.size()) + " "
franta-hg@16
   161
				"data = " + toString(msg));
franta-hg@6
   162
		std::lock_guard<std::recursive_mutex> lock(midiMutex);
franta-hg@5
   163
franta-hg@7
   164
franta-hg@16
   165
		if (msg.size() == 12 && msg[9] == 0x11) {
franta-hg@13
   166
			logger->log(L::INFO, "Received greeting message.");
franta-hg@16
   167
			send({
franta-hg@16
   168
				0xf0, 0x00, 0x40, 0x05,
franta-hg@16
   169
				0x00, 0x00, 0x00, 0x17,
franta-hg@16
   170
				0x00, 0x12, 0x2a, 0x01,
franta-hg@16
   171
				0x0b, 0x50, 0x69, 0x6f,
franta-hg@16
   172
				0x6e, 0x65, 0x65, 0x72,
franta-hg@16
   173
				0x44, 0x4a, 0x02, 0x0b,
franta-hg@16
   174
				0x72, 0x65, 0x6b, 0x6f,
franta-hg@16
   175
				0x72, 0x64, 0x62, 0x6f,
franta-hg@16
   176
				0x78, 0x03, 0x12, 0x02,
franta-hg@16
   177
				0x09, 0x00, 0x00, 0x00,
franta-hg@16
   178
				0x00, 0x00, 0x00, 0x02,
franta-hg@16
   179
				0x03, 0x04, 0x08, 0x00,
franta-hg@16
   180
				0x00, 0x00, 0x00, 0xf7
franta-hg@16
   181
			});
franta-hg@13
   182
			logger->log(L::INFO, "Sent message with seed1.");
franta-hg@16
   183
		} else if (msg.size() == 54
franta-hg@16
   184
				&& msg[9] == 0x13
franta-hg@16
   185
				&& msg[33] == 0x04
franta-hg@16
   186
				&& msg[43] == 0x03) //
franta-hg@16
   187
		{
franta-hg@16
   188
			Bytes hash1(msg.begin() + 35, msg.begin() + 35 + 8);
franta-hg@16
   189
			seed2 = Bytes(msg.begin() + 45, msg.begin() + 45 + 8);
franta-hg@6
   190
			hash1 = normalize(hash1);
franta-hg@6
   191
			seed2 = normalize(seed2);
franta-hg@16
   192
			logger->log(L::INFO, "Received message with "
franta-hg@16
   193
					"hash1 = " + toString(hash1) + " and "
franta-hg@16
   194
					"seed2 = " + toString(seed2));
franta-hg@6
   195
franta-hg@6
   196
			Bytes seed0 = {0x68, 0x01, 0x31, 0xFB};
franta-hg@6
   197
			Bytes seed1 = {0x29, 0x00, 0x00, 0x00, 0x23, 0x48, 0x00, 0x00};
franta-hg@6
   198
franta-hg@16
   199
			Bytes hash1check =
franta-hg@16
   200
					toBytes(fnv32hash(concat(seed1, xOR(seed0, seed2))));
franta-hg@6
   201
franta-hg@6
   202
			if (equals(hash1, hash1check)) {
franta-hg@13
   203
				logger->log(L::INFO, "Verification of hash1 was successful.");
franta-hg@16
   204
				Bytes hash2 =
franta-hg@16
   205
						toBytes(fnv32hash(concat(seed2, xOR(seed0, seed2))));
franta-hg@16
   206
				send(concat({
franta-hg@16
   207
					0xf0, 0x00, 0x40, 0x05,
franta-hg@16
   208
					0x00, 0x00, 0x00, 0x17,
franta-hg@16
   209
					0x00, 0x14, 0x38, 0x01,
franta-hg@16
   210
					0x0b, 0x50, 0x69, 0x6f,
franta-hg@16
   211
					0x6e, 0x65, 0x65, 0x72,
franta-hg@16
   212
					0x44, 0x4a, 0x02, 0x0b,
franta-hg@16
   213
					0x72, 0x65, 0x6b, 0x6f,
franta-hg@16
   214
					0x72, 0x64, 0x62, 0x6f,
franta-hg@16
   215
					0x78, 0x04, 0x0a
franta-hg@16
   216
				}, concat(denormalize(hash2),{
franta-hg@16
   217
					0x05, 0x16, 0x05, 0x09,
franta-hg@16
   218
					0x0b, 0x05, 0x04, 0x0b,
franta-hg@16
   219
					0x0f, 0x0e, 0x0e, 0x04,
franta-hg@16
   220
					0x04, 0x0a, 0x05, 0x0a,
franta-hg@16
   221
					0x0c, 0x08, 0x0e, 0x04,
franta-hg@16
   222
					0x0c, 0x05, 0xf7
franta-hg@16
   223
				})));
franta-hg@13
   224
				logger->log(L::INFO, "Sent message with hash2.");
franta-hg@6
   225
			} else {
franta-hg@12
   226
				std::stringstream logMessage;
franta-hg@12
   227
				logMessage
franta-hg@13
   228
						<< "Verification of hash1 failed: "
franta-hg@16
   229
						<< " midiMessage = " << toString(msg)
franta-hg@8
   230
						<< " seed0 = " << toString(seed0)
franta-hg@8
   231
						<< " seed1 = " << toString(seed1)
franta-hg@8
   232
						<< " seed2 = " << toString(seed2)
franta-hg@8
   233
						<< " hash1 = " << toString(hash1)
franta-hg@12
   234
						<< " hash1check = " << toString(hash1check);
franta-hg@12
   235
				logger->log(L::SEVERE, logMessage.str());
franta-hg@8
   236
				// TODO: graceful death
franta-hg@6
   237
			}
franta-hg@16
   238
		} else if (msg.size() == 12 && msg[9] == 0x15) {
franta-hg@8
   239
			sendKeepAlive = true;
franta-hg@16
   240
			logger->log(L::INFO, "Received acknowledgment message. "
franta-hg@16
   241
					"Started sending keep-alive messages. "
franta-hg@16
   242
					"LINE/PHONO channels should work now.");
franta-hg@5
   243
		}
franta-hg@5
   244
franta-hg@1
   245
	}
franta-hg@1
   246
franta-hg@1
   247
	void start() override {
franta-hg@12
   248
		logger->log(L::FINE, "DJMFixImpl::start()");
franta-hg@16
   249
		if (midiSender == nullptr)
franta-hg@16
   250
			throw std::logic_error("Need a midiSender when starting DJMFix");
franta-hg@5
   251
franta-hg@5
   252
		// TODO: methods for parsing and constructing messages from parts (TLV)
franta-hg@16
   253
		send({
franta-hg@16
   254
			0xf0, 0x00, 0x40, 0x05,
franta-hg@16
   255
			0x00, 0x00, 0x00, 0x17,
franta-hg@16
   256
			0x00, 0x50, 0x01, 0xf7
franta-hg@16
   257
		});
franta-hg@13
   258
		logger->log(L::INFO, "Sent greeting message.");
franta-hg@2
   259
franta-hg@2
   260
		keepAliveThread = std::thread(&DJMFixImpl::run, this);
franta-hg@2
   261
		running = true;
franta-hg@2
   262
franta-hg@1
   263
	}
franta-hg@1
   264
franta-hg@1
   265
	void stop() override {
franta-hg@2
   266
		stopped = true;
franta-hg@2
   267
		keepAliveThread.join();
franta-hg@2
   268
		running = false;
franta-hg@12
   269
		logger->log(L::FINE, "DJMFixImpl::stop()");
franta-hg@1
   270
	}
franta-hg@1
   271
};
franta-hg@1
   272
franta-hg@12
   273
DJMFix* create(djmfix::logging::Logger* logger) {
franta-hg@12
   274
	return new DJMFixImpl(logger);
franta-hg@1
   275
}
franta-hg@1
   276
franta-hg@1
   277
}