diff --git a/SupportedModels.md b/SupportedModels.md
index 9ac9f45..18c3d06 100644
--- a/SupportedModels.md
+++ b/SupportedModels.md
@@ -21,6 +21,7 @@ Don't see your model? We may have missed one. Just give it a shot, and if you ru
| 10 | 890
895 | Working | Not working | Not working | |
| 12 | One | Working | Working | Not working | |
| 14 | 700 | Working | Believed working | Not working | Needs kernel > 2.6.37 |
-| 15 | 900
1000
1000i
1100
Xbox | Working | Not working | Not working | Not yet supported on Mac |
+| 15 | 900 | Working | Working | Not working | Not yet supported on Mac |
+| 15 | 1000
1000i
1100
Xbox | Working | Not working | Not working | Not yet supported on Mac |
| 16 | 200
300 | Working | Working | Not working | |
| 17 | Link | Working | Working | Not working | |
diff --git a/libconcord/Makefile.am b/libconcord/Makefile.am
index b3036c5..25ea30a 100644
--- a/libconcord/Makefile.am
+++ b/libconcord/Makefile.am
@@ -1,10 +1,13 @@
# libconcord
ACLOCAL_AMFLAGS = -I m4
lib_LTLIBRARIES = libconcord.la
+AUTOMAKE_OPTIONS = subdir-objects
libconcord_la_SOURCES = remote.cpp remote_z.cpp libconcord.cpp binaryfile.cpp \
web.cpp usblan.cpp binaryfile.h hid.h protocol_z.h \
remote_info.h web.h protocol.h remote.h usblan.h xml_headers.h \
- operationfile.cpp remote_mh.cpp libusbhid.cpp libhidapi.cpp
+ operationfile.cpp remote_mh.cpp libusbhid.cpp libhidapi.cpp \
+ remote_z_learn/data.cpp remote_z_learn/base.cpp \
+ remote_z_learn/single.cpp remote_z_learn/stream.cpp
include_HEADERS = libconcord.h
libconcord_la_CPPFLAGS = -Wall
libconcord_la_LDFLAGS = -version-info 6:0:0 $(LIBCONCORD_LDFLAGS) -lzip -lcurl
diff --git a/libconcord/libconcord.cpp b/libconcord/libconcord.cpp
index 273701f..e5d9ea1 100644
--- a/libconcord/libconcord.cpp
+++ b/libconcord/libconcord.cpp
@@ -1701,6 +1701,22 @@ void delete_key_names(char **key_names, uint32_t key_names_length)
}
}
+/*
+ * set USBNET learn mode / time
+ */
+int set_learning_mode(int mode, uint32_t timeout_ms)
+{
+ if (rmt == NULL){
+ return LC_ERROR_CONNECT;
+ }
+
+ auto *p = dynamic_cast(rmt);
+ if (p == nullptr) {
+ return LC_ERROR;
+ }
+ return p->SetIrMode(mode, timeout_ms);
+}
+
/*
* Fill ir_data with IR code learned from other remote
* via Harmony IR receiver.
diff --git a/libconcord/libconcord.h b/libconcord/libconcord.h
index c0d2ba0..ed58f69 100644
--- a/libconcord/libconcord.h
+++ b/libconcord/libconcord.h
@@ -42,6 +42,7 @@
#define LC_ERROR_UNSUPP 15
#define LC_ERROR_INVALID_CONFIG 16
#define LC_ERROR_IR_OVERFLOW 17
+#define LC_ERROR_IR_TIMEOUT 18
/*
* Filetypes, used by identity_file()
@@ -50,6 +51,11 @@
#define LC_FILE_TYPE_CONFIGURATION 2
#define LC_FILE_TYPE_FIRMWARE 3
#define LC_FILE_TYPE_LEARN_IR 4
+/*
+ * IR Learn mode
+ */
+#define LC_LEARN_SINGLE 0
+#define LC_LEARN_STREAM 1
/*
* Callback counter types
*/
@@ -455,6 +461,17 @@ int get_key_names(char ***key_names, uint32_t *key_names_length);
void delete_key_names(char **key_names, uint32_t key_names_length);
+/*
+ * H900 only
+ * Set mode that will be used for learing. There are two available
+ * - Single: Remote waits a fixed time for a command, and if received
+ * returns the data of one single IR frame
+ * - Stream: Remote records IR data for the time given in _timeout_ms_
+ * Any data within this window is returned, including silence. Data
+ * before the first reception is dropped.
+ */
+int set_learning_mode(int mode, uint32_t timeout_ms);
+
/*
* Fill ir_data with IR code learned from other remote
* via Harmony IR receiver.
diff --git a/libconcord/protocol_z.h b/libconcord/protocol_z.h
index 7c27d80..8bbfa89 100644
--- a/libconcord/protocol_z.h
+++ b/libconcord/protocol_z.h
@@ -109,9 +109,9 @@
#define COMMAND_SEND_LONG_ZWAVE_RESPONSE_DATA 0x8B
#define COMMAND_SEND_LONG_ZWAVE_RESPONSE_DATA_DONE 0x8C
#define COMMAND_INITIATE_LEARNIR_TCP_CHANNEL 0xA0
-#define COMMAND_LEARNIR 0xA1
-#define COMMAND_LEARNIR_HEADER 0xA2
-#define COMMAND_LEARNIR_DATA 0xA3
+#define COMMAND_LEARNIR_START 0xA1
+#define COMMAND_LEARNIR_SINGLE 0xA2
+#define COMMAND_LEARNIR_STREAM 0xA3
#define COMMAND_LEARNIR_DONE 0xA4
#define COMMAND_LEARNIR_STOP 0xA5
// 1000 only
diff --git a/libconcord/remote.h b/libconcord/remote.h
index 370cf53..e3e9b4e 100644
--- a/libconcord/remote.h
+++ b/libconcord/remote.h
@@ -369,6 +369,9 @@ class CRemoteZ_HID : public CRemoteZ_Base
class CRemoteZ_USBNET : public CRemoteZ_Base
{
protected:
+ int ir_learn_mode = LC_LEARN_SINGLE;
+ uint32_t ir_stream_timeout_ms = 1000;
+
int TCPSendAndCheck(uint8_t cmd, uint32_t len=0, uint8_t *data=NULL);
virtual int Write(uint8_t typ, uint8_t cmd, uint32_t len=0,
uint8_t *data=NULL);
@@ -380,6 +383,14 @@ class CRemoteZ_USBNET : public CRemoteZ_Base
| x[2]<<8 | x[3]; };
virtual int ReadRegion(uint8_t region, uint32_t &len, uint8_t *rd,
lc_callback cb, void *cb_arg, uint32_t cb_stage);
+ virtual int SendLearnStart();
+ virtual int SendLearnStop();
+ virtual int ReadIrData(uint32_t *freq, uint32_t **ir_signal,
+ uint32_t *ir_signal_length, lc_callback cb, void *cb_arg,
+ uint32_t cb_stage);
+ virtual int ReadIrStream(uint32_t *freq, uint32_t **ir_signal,
+ uint32_t *ir_signal_length, lc_callback cb, void *cb_arg,
+ uint32_t cb_stage);
public:
CRemoteZ_USBNET() {};
@@ -390,6 +401,7 @@ class CRemoteZ_USBNET : public CRemoteZ_Base
int GetTime(const TRemoteInfo &ri, THarmonyTime &ht);
int SetTime(const TRemoteInfo &ri, const THarmonyTime &ht,
lc_callback cb=NULL, void *cb_arg=NULL, uint32_t cb_stage=0);
+ int SetIrMode(int mode = LC_LEARN_SINGLE, uint32_t stream_timeout_ms = 1000);
int LearnIR(uint32_t *freq, uint32_t **ir_signal,
uint32_t *ir_signal_length, lc_callback cb=NULL,
void *cb_arg=NULL, uint32_t cb_stage=0);
diff --git a/libconcord/remote_z.cpp b/libconcord/remote_z.cpp
index 1af735a..d52843a 100644
--- a/libconcord/remote_z.cpp
+++ b/libconcord/remote_z.cpp
@@ -26,6 +26,11 @@
#include "remote.h"
#include "usblan.h"
#include "protocol_z.h"
+#include "remote_z_learn/single.h"
+#include "remote_z_learn/start.h"
+#include "remote_z_learn/stop.h"
+#include "remote_z_learn/stream.h"
+#include "remote_z_learn/data.h"
/* Have we acked the syn packet yet? */
static bool SYN_ACKED = false;
@@ -579,11 +584,303 @@ int CRemoteZ_USBNET::SetTime(const TRemoteInfo &ri, const THarmonyTime &ht,
return 0;
}
+
+int CRemoteZ_USBNET::SetIrMode(int mode, uint32_t stream_timeout_ms)
+{
+ if (mode == LC_LEARN_SINGLE || mode == LC_LEARN_STREAM) {
+ ir_learn_mode = mode;
+ } else {
+ return -1;
+ }
+ ir_stream_timeout_ms = stream_timeout_ms;
+ return 0;
+}
+
int CRemoteZ_USBNET::LearnIR(uint32_t *freq, uint32_t **ir_signal,
uint32_t *ir_signal_length, lc_callback cb,
void *cb_arg, uint32_t cb_stage)
{
- return LC_ERROR_UNSUPP;
+ int err = 0;
+
+ *freq = 0;
+ *ir_signal_length = 0;
+ *ir_signal = nullptr;
+
+ if ((err = SendLearnStart())) {
+ debug("Failed starting learn mode");
+ return err;
+ }
+
+ if (ir_learn_mode == LC_LEARN_STREAM) {
+ err = ReadIrStream(freq, ir_signal, ir_signal_length, cb, cb_arg,
+ cb_stage);
+ } else {
+ err = ReadIrData(freq, ir_signal, ir_signal_length, cb, cb_arg,
+ cb_stage);
+ }
+ if (err) {
+ SendLearnStop(); //always close!
+ debug("Failed starting learn mode");
+ return err;
+ }
+
+ if ((err = SendLearnStop())) {
+ debug("Failed stopping learn mode");
+ return err;
+ }
+
+ return 0;
+}
+
+int CRemoteZ_USBNET::SendLearnStart()
+{
+ remote_z::Start start;
+ uint8_t rsp[USBNET_MAX_PACKET_SIZE + 3]; /* add standard 3-byte header */
+
+ debug("START_IR_LEARN");
+
+ int err = 0;
+ auto req = start.get();
+ if ((err = Write(
+ TYPE_REQUEST, COMMAND_LEARNIR_START, req.size(), req.data()))) {
+ debug("Failed to write to remote");
+ return LC_ERROR_WRITE;
+ }
+ unsigned int len;
+ uint8_t status;
+ if ((err = Read(status, len, rsp))) {
+ debug("Failed to read to remote");
+ return LC_ERROR_READ;
+ }
+
+ auto ret = start.check(std::vector(rsp, rsp + len));
+ if (ret != remote_z::Start::Status::OK) {
+ debug("Incorrect response type from remote");
+ return LC_ERROR_INVALID_DATA_FROM_REMOTE;
+ }
+ return 0;
+}
+
+int CRemoteZ_USBNET::SendLearnStop()
+{
+ remote_z::Stop stop;
+ uint8_t rsp[USBNET_MAX_PACKET_SIZE + 3]; /* add standard 3-byte header */
+
+ debug("STOP_IR_LEARN");
+
+ int err = 0;
+ auto req = stop.get();
+ if ((err = Write(
+ TYPE_REQUEST, COMMAND_LEARNIR_DONE, req.size(), req.data()))) {
+ debug("Failed to write to remote");
+ return LC_ERROR_WRITE;
+ }
+ unsigned int len;
+ uint8_t status;
+ if ((err = Read(status, len, rsp))) {
+ debug("Failed to read to remote");
+ return LC_ERROR_READ;
+ }
+
+ auto ret = stop.check(std::vector(rsp, rsp + len));
+ if (ret != remote_z::Stop::Status::OK) {
+ debug("Incorrect response type from remote");
+ return LC_ERROR_INVALID_DATA_FROM_REMOTE;
+ }
+ return 0;
+}
+
+int CRemoteZ_USBNET::ReadIrData(uint32_t *freq, uint32_t **ir_signal,
+ uint32_t *ir_signal_length, lc_callback cb,
+ void *cb_arg, uint32_t cb_stage)
+{
+ remote_z::Single single;
+ uint8_t rsp[USBNET_MAX_PACKET_SIZE + 3]; /* add standard 3-byte header */
+ int err = 0;
+ int cb_count = 0;
+ bool firstChunk = true;
+
+ debug("READING_IR_DATA");
+
+ /*
+ * Caller is responsible for deallocation of *ir_signal after use.
+ *
+ * Loop while we haven not:
+ * - any error
+ * - Timeout. Timeout is set by the remote itself answering "timeout", you can't change it.
+ * - EoF
+ */
+ while (err == 0) {
+ auto req = single.get();
+ if ((err = Write(TYPE_REQUEST,
+ COMMAND_LEARNIR_SINGLE,
+ req.size(),
+ req.data()))) {
+ debug("Failed to write to remote");
+ err = LC_ERROR_WRITE;
+ break;
+ }
+ unsigned int len;
+ uint8_t status;
+ if ((err = Read(status, len, rsp))) {
+ debug("Failed to read to remote");
+ err = LC_ERROR_READ;
+ break;
+ }
+
+ debug("DATA %d, read %d bytest", cb_count, len)
+
+ auto ret = single.addChunk(std::vector(rsp, rsp + len),
+ firstChunk);
+ if (ret == remote_z::Single::Status::DONE) break;
+ switch (ret) {
+ case remote_z::Single::Status::OK:
+ break;
+ case remote_z::Single::Status::ERR_TIMEOUT:
+ err = LC_ERROR_IR_TIMEOUT;
+ break;
+ default:
+ debug("Incorrect response type from remote (%d)", (int)ret);
+ err = LC_ERROR_INVALID_DATA_FROM_REMOTE;
+ break;
+ }
+
+ firstChunk = false;
+
+ if (cb) {
+ cb(cb_stage,
+ cb_count++,
+ single.getPayload().size(),
+ 0,
+ LC_CB_COUNTER_TYPE_STEPS,
+ cb_arg,
+ NULL);
+ }
+ }
+
+ if (err != 0) return err;
+
+ debug("DECODING_IR_DATA");
+
+ //standardise
+ auto payload = single.getPayload();
+ if (payload.empty())
+ return 0;
+ auto timingStream = remote_z::TimingStream::fromMarkSegment(payload);
+
+ //convert to libconcord format
+ const auto &t = timingStream.convertMarkPause();
+ *freq = single.getClock();
+ *ir_signal_length = t.size();
+ *ir_signal = new uint32_t[t.size()];
+ for (uint32_t i = 0; i < t.size(); i++) {
+ (*ir_signal)[i] = t[i];
+ }
+
+ debug("READ_IR_DONE");
+
+ return 0;
+}
+
+int CRemoteZ_USBNET::ReadIrStream(uint32_t *freq, uint32_t **ir_signal,
+ uint32_t *ir_signal_length, lc_callback cb,
+ void *cb_arg, uint32_t cb_stage)
+{
+ remote_z::Stream stream;
+ uint8_t rsp[USBNET_MAX_PACKET_SIZE + 3]; /* add standard 3-byte header */
+ int err = 0;
+ int cb_count = 0;
+ bool firstChunk = true;
+ chrono::milliseconds timeout(ir_stream_timeout_ms);
+
+ debug("READING_IR_STREAM");
+
+ auto t_start = chrono::steady_clock::now();
+
+ /*
+ * Caller is responsible for deallocation of *ir_signal after use.
+ *
+ * Loop while we haven not:
+ * - any error
+ * - timeout (not an error, we record for this amount of time)
+ * be aware that timeout is a minimum value. the remote will
+ * always fill a data frame before returning.
+ */
+ while (err == 0) {
+ auto req = stream.get();
+ if ((err = Write(TYPE_REQUEST,
+ COMMAND_LEARNIR_STREAM,
+ req.size(),
+ req.data()))) {
+ debug("Failed to write to remote");
+ err = LC_ERROR_WRITE;
+ break;
+ }
+ unsigned int len;
+ uint8_t status;
+ if ((err = Read(status, len, rsp))) {
+ debug("Failed to read to remote");
+ err = LC_ERROR_READ;
+ break;
+ }
+
+ debug("DATA %d, read %d bytest", cb_count, len)
+
+ auto ret = stream.addChunk(std::vector(rsp, rsp + len),
+ firstChunk);
+ if (ret == remote_z::Single::Status::DONE) break;
+ switch (ret) {
+ case remote_z::Single::Status::OK:
+ break;
+ case remote_z::Single::Status::ERR_TIMEOUT:
+ err = LC_ERROR_IR_TIMEOUT;
+ break;
+ default:
+ debug("Incorrect response type from remote (%d)", (int)ret);
+ err = LC_ERROR_INVALID_DATA_FROM_REMOTE;
+ break;
+ }
+
+ firstChunk = false;
+
+ if (cb) {
+ cb(cb_stage,
+ cb_count++,
+ stream.getPayload().size(),
+ 0,
+ LC_CB_COUNTER_TYPE_STEPS,
+ cb_arg,
+ NULL);
+ }
+
+ auto t_now = chrono::steady_clock::now();
+ if ((t_now - t_start) > timeout) {
+ break;
+ }
+ }
+
+ if (err != 0) return err;
+
+ debug("DECODING_IR_DATA");
+
+ //standardise
+ auto payload = stream.getPayload();
+ if (payload.empty())
+ return 0;
+ auto timingStream = remote_z::TimingStream::fromMarkSegment(payload);
+
+ //convert to libconcord format
+ const auto &t = timingStream.convertMarkPause();
+ *freq = stream.getClock();
+ *ir_signal_length = t.size();
+ *ir_signal = new uint32_t[t.size()];
+ for (uint32_t i = 0; i < t.size(); i++) {
+ (*ir_signal)[i] = t[i];
+ }
+
+ debug("READ_IR_DONE");
+
+ return 0;
}
int CRemoteZ_USBNET::ReadRegion(uint8_t region, uint32_t &rgn_len, uint8_t *rd,
diff --git a/libconcord/remote_z_learn/base.cpp b/libconcord/remote_z_learn/base.cpp
new file mode 100644
index 0000000..7b03c9e
--- /dev/null
+++ b/libconcord/remote_z_learn/base.cpp
@@ -0,0 +1,62 @@
+/*
+ * vim:tw=80:ai:tabstop=4:softtabstop=4:shiftwidth=4:expandtab
+ *
+ * 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, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * (C) Copyright Martin Wagner 2026
+ */
+#include "base.h"
+
+using namespace std;
+
+namespace remote_z
+{
+
+Base::Status Base::check(const std::vector &data)
+{
+ if (data.size() < getHeaderMinSize()) return Status::ERR_SIZE;
+ if ((data[0] != 0x20) || (data[1] != getProtoclCmd()) ||
+ (data[3] != getProtocolRespByte3())) {
+ return Status::ERR_RESPONSE_FORMAT;
+ }
+ switch (static_cast(data[2])) {
+ case ProtocolStatus::OK:
+ return Status::OK;
+ case ProtocolStatus::TIMEOUT:
+ return Status::ERR_TIMEOUT;
+ default:
+ return Status::ERR_UNKNOWN_RETURNCODE;
+ }
+}
+
+uint8_t Base::getErrorByte(const std::vector &data)
+{
+ if (data.size() < getHeaderMinSize()) return 255;
+ return data[2];
+}
+
+void Base::moveExcessBytes(bool check)
+{
+ if (payload.size() < 3) return;
+
+ if (check) {
+ excess.push_back(payload[0]);
+ excess.push_back(payload[2]);
+ payload.erase(payload.begin() + 2);
+ payload.erase(payload.begin() + 0);
+ }
+}
+
+} // namespace remote_z
diff --git a/libconcord/remote_z_learn/base.h b/libconcord/remote_z_learn/base.h
new file mode 100644
index 0000000..702f915
--- /dev/null
+++ b/libconcord/remote_z_learn/base.h
@@ -0,0 +1,93 @@
+/*
+ * vim:tw=80:ai:tabstop=4:softtabstop=4:shiftwidth=4:expandtab
+ *
+ * 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, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * (C) Copyright Martin Wagner 2026
+ */
+
+#pragma once
+
+#include
+#include
+
+#include "protocol_z.h"
+
+namespace remote_z
+{
+
+class Base
+{
+ public:
+ enum class Status {
+ OK,
+ DONE,
+ ERR_SIZE,
+ ERR_UNKNOWN_RETURNCODE, //received unknown return code from remote, see ProtocolStatus
+ ERR_RESPONSE_FORMAT, //response heder, layout format/values not as expected
+ ERR_PAYLOAD_FORMAT, //dito, payload
+ ERR_TERM,
+ ERR_TIMEOUT,
+ ERR_UNKNOWN
+ };
+
+ protected:
+ std::vector payload;
+ double clock = 0;
+ std::vector excess;
+
+ //static values from child classes
+ virtual uint32_t getHeaderMinSize() = 0;
+ virtual uint32_t getProtoclCmd() = 0;
+ virtual uint32_t getProtocolRespByte3() = 0;
+
+ //assume status is byte data[2] in response
+ enum class ProtocolStatus { OK = 0x01, TIMEOUT = 0x02 };
+
+ void moveExcessBytes(bool check);
+
+ public:
+ /** generate request */
+ virtual std::vector get() = 0;
+
+ /** check data block */
+ virtual Status check(const std::vector &data);
+
+ /** get uninterpretet error byte */
+ uint8_t getErrorByte(const std::vector &data);
+
+ /**
+ * append data block
+ *
+ * @param data response data block
+ * @param first this is the first ever block in this transfer (command single/stream doesn't matter)
+ * @return see 'Status'
+ */
+ virtual Status addChunk(const std::vector &data, bool first)
+ {
+ return Status::OK;
+ };
+
+ /** access payload. size() == 0 -> nothing available */
+ virtual const std::vector &getPayload() { return payload; }
+
+ /** get IR clock */
+ virtual const double getClock() { return clock; }
+
+ /** read non-timing related words. word = unknown use */
+ virtual const std::vector &getExcess() { return excess; }
+};
+
+} // namespace remote_z
diff --git a/libconcord/remote_z_learn/binary.h b/libconcord/remote_z_learn/binary.h
new file mode 100644
index 0000000..7501bf1
--- /dev/null
+++ b/libconcord/remote_z_learn/binary.h
@@ -0,0 +1,90 @@
+/*
+ * vim:tw=80:ai:tabstop=4:softtabstop=4:shiftwidth=4:expandtab
+ *
+ * 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, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * (C) Copyright Martin Wagner 2026
+ */
+
+#pragma once
+
+#include
+#include
+
+namespace remote_z
+{
+
+//use on network data stream: parseHarmony16(p[i], p[i+1])
+inline uint16_t parseHarmony16_network(uint8_t h, uint8_t l)
+{
+ return static_cast(l) | (static_cast(h) << 8);
+}
+
+inline bool parseHarmony16_network(const std::vector &in,
+ std::vector &out)
+{
+ if ((in.size() % 2) != 0) return false;
+
+ for (size_t i = 0; i < in.size(); i += 2)
+ out.push_back(parseHarmony16_network(in[i], in[i + 1]));
+ return true;
+}
+
+//use on network data stream: parseHarmony16(p[i], p[i+1])
+inline uint16_t parseHarmony16_file(uint8_t h, uint8_t l)
+{
+ return static_cast(h) | (static_cast(l) << 8);
+}
+
+inline uint32_t parseHarmony32_file(uint8_t h,
+ uint8_t m1,
+ uint8_t m2,
+ uint8_t l)
+{
+ return static_cast(h) | (static_cast(m1) << 8) |
+ (static_cast(m2) << 16) | (static_cast(l) << 24);
+}
+
+inline bool parseHarmony16_file(const std::vector &in,
+ std::vector &out)
+{
+ if ((in.size() % 2) != 0) return false;
+
+ for (size_t i = 0; i < in.size(); i += 2)
+ out.push_back(parseHarmony16_file(in[i], in[i + 1]));
+ return true;
+}
+
+inline void setHarmony16_file(uint16_t data, std::vector &out)
+{
+ out.push_back(data);
+ out.push_back(data >> 8);
+}
+
+inline void setHarmony32_file(uint32_t data, std::vector &out)
+{
+ out.push_back(data);
+ out.push_back(data >> 8);
+ out.push_back(data >> 16);
+ out.push_back(data >> 24);
+}
+
+inline void setHarmony16_file(const std::vector &in,
+ std::vector &out)
+{
+ for (const auto &d : in) setHarmony16_file(d, out);
+}
+
+} // namespace remote_z
diff --git a/libconcord/remote_z_learn/data.cpp b/libconcord/remote_z_learn/data.cpp
new file mode 100644
index 0000000..a856035
--- /dev/null
+++ b/libconcord/remote_z_learn/data.cpp
@@ -0,0 +1,197 @@
+/*
+ * vim:tw=80:ai:tabstop=4:softtabstop=4:shiftwidth=4:expandtab
+ *
+ * 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, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * (C) Copyright Martin Wagner 2026
+ */
+
+#include
+#include
+#include
+
+#include "data.h"
+
+using namespace std;
+
+namespace remote_z
+{
+
+void TimingStream::addMarkSegment(const vector &raw)
+{
+ for (size_t i = 0; i < raw.size(); i += 2)
+ if (i + 1 < raw.size())
+ data.push_back(Block::fromMarkSegment(raw[i], raw[i + 1]));
+}
+
+void TimingStream::addMarkPause(const vector &raw)
+{
+ for (size_t i = 0; i < raw.size(); i += 2)
+ if (i + 1 < raw.size())
+ data.push_back(Block::fromMarkPause(raw[i], raw[i + 1]));
+}
+
+vector TimingStream::convertMarkPause() const
+{
+ vector stream;
+
+ for (const auto &item : data) {
+ stream.push_back(item.mark_us);
+ stream.push_back(item.pause_us);
+ }
+ return stream;
+}
+
+string TimingStream::convertGnuplot(bool activeHigh) const
+{
+ vector> plotData;
+ uint32_t time_us = 0;
+ stringstream str;
+
+ uint32_t markValue = 1;
+ if (!activeHigh) markValue = 0;
+
+ for (const auto &block : data) {
+ // Start of mark (ON state)
+ plotData.push_back({time_us, markValue});
+
+ // End of mark, start of segment (OFF state)
+ time_us += block.mark_us;
+ plotData.push_back({time_us, 0});
+
+ // End of segment (prepare for next mark)
+ time_us += block.pause_us;
+ }
+
+ str << "time(µs) mark" << endl;
+ for (const auto &[time, amp] : plotData) str << time << " " << amp << endl;
+ return str.str();
+}
+
+string TimingStream::convertHexString() const
+{
+ stringstream str;
+
+ for (const auto &block : data) {
+ str << hex << setw(4) << setfill('0') << block.mark_us << " " << hex
+ << setw(4) << setfill('0') << block.segment_us
+ << " "; //raw data, not mark/pause!
+ }
+ str << dec;
+
+ return str.str();
+}
+
+string TimingStream::convertIntString() const
+{
+ stringstream str;
+
+ for (const auto &block : data) {
+ str << "MP" << block.mark_us << ":" << block.pause_us << "; ";
+ //str << "MS" << block.mark_us << ":" << block.segment_us << "; ";
+ }
+
+ return str.str();
+}
+
+string TimingStream::convertAsciiPlot(uint32_t width, bool activeHigh) const
+{
+ //todo active high...
+ string header;
+ string top;
+ string bottom;
+ uint32_t used = 0;
+ uint32_t base = 250; //us per char //todo so am einfachsten.
+ bool level = false;
+ bool truncated = false;
+ auto append = [&](const string &t, const string &b, uint32_t count = 1) {
+ if (used < width) {
+ top += t;
+ bottom += b;
+ used += count;
+ }
+ };
+ string truncationMarker = "...";
+ width = width - 3;
+
+ if (data.empty() || (width < 25)) return "";
+
+ header = "µs per div: " + to_string(base);
+
+ for (const auto &block : data) {
+ // divs, do round. for shorter pulse min 1 div, even on round-down
+ int mark_divs = max(1u, (block.mark_us + base / 2) / base);
+ if (block.mark_us == 0) {
+ //silence block, no pulse
+ mark_divs = 0;
+ }
+ int pause_divs = max(1u, (block.pause_us + base / 2) / base);
+
+ // rising edge
+ if (!level && (mark_divs > 0)) {
+ append("┌", "┘");
+ level = true;
+ if (used >= width) {
+ truncated = true;
+ break;
+ }
+ }
+
+ for (int i = 1; i < mark_divs; i++) {
+ append("─", " ");
+ if (used >= width) {
+ truncated = true;
+ break;
+ }
+ }
+ if (used >= width) {
+ truncated = true;
+ break;
+ }
+
+ // falling edge
+ if (level && (mark_divs > 0)) {
+ append("┐", "└");
+ level = false;
+ if (used >= width) {
+ truncated = true;
+ break;
+ }
+ }
+
+ for (int i = 1; i < pause_divs; i++) {
+ append(" ", "─");
+ if ((i > 4) && (pause_divs > 12)) {
+ //crop empty data
+ append(" ", "...──", 5);
+ break;
+ }
+ if (used >= width) {
+ truncated = true;
+ break;
+ }
+ }
+ if (used >= width) {
+ truncated = true;
+ break;
+ }
+ }
+
+ if (truncated) bottom += "\\++";
+
+ return header + '\n' + top + '\n' + bottom + '\n';
+}
+
+} // namespace remote_z
diff --git a/libconcord/remote_z_learn/data.h b/libconcord/remote_z_learn/data.h
new file mode 100644
index 0000000..e351bb3
--- /dev/null
+++ b/libconcord/remote_z_learn/data.h
@@ -0,0 +1,110 @@
+/*
+ * vim:tw=80:ai:tabstop=4:softtabstop=4:shiftwidth=4:expandtab
+ *
+ * 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, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * (C) Copyright Martin Wagner 2026
+ */
+
+#pragma once
+
+#include
+#include
+#include
+#include
+
+namespace remote_z
+{
+
+/** single data block */
+class Block
+{
+ public:
+ static Block fromMarkSegment(uint16_t mark_us, uint16_t segment_us)
+ {
+ return Block(mark_us, segment_us, segment_us - mark_us);
+ }
+
+ static Block fromMarkPause(uint16_t mark_us, uint16_t pause_us)
+ {
+ return Block(mark_us, pause_us + mark_us, pause_us);
+ }
+
+ protected:
+ Block(uint16_t mark_us, uint16_t segment_us, uint16_t pause_us)
+ : mark_us(mark_us), pause_us(pause_us), segment_us(segment_us)
+ {
+ }
+
+ public:
+ const uint16_t mark_us;
+ const uint16_t pause_us;
+ const uint16_t segment_us;
+
+ std::chrono::microseconds mark() const
+ {
+ return std::chrono::microseconds(mark_us);
+ }
+ std::chrono::microseconds segment() const
+ {
+ return std::chrono::microseconds(segment_us);
+ }
+ std::chrono::microseconds pause() const
+ {
+ return std::chrono::microseconds(pause_us);
+ }
+};
+
+/** entire stream (single frame or actual stream) of
+ * timing data */
+class TimingStream
+{
+ protected:
+ std::vector data;
+
+ public:
+ TimingStream() {}
+
+ /** Stream lesen, Codierung Mark, Mark+Pause (=Periodendauer).
+ * Wir so in irlearn verwendet */
+ static TimingStream fromMarkSegment(const std::vector &raw)
+ {
+ TimingStream ts;
+ ts.addMarkSegment(raw);
+ return ts;
+ }
+
+ /** Stream lesen, Codierung Mark, Pause */
+ static TimingStream fromMarkPause(const std::vector &raw)
+ {
+ TimingStream ts;
+ ts.addMarkPause(raw);
+ return ts;
+ }
+
+ void addMarkSegment(const std::vector &raw);
+ void addMarkPause(const std::vector &raw);
+
+ std::vector convertMarkPause() const;
+ std::string convertGnuplot(bool activeHigh = true) const;
+ std::string convertHexString() const;
+ std::string convertIntString() const;
+ std::string convertAsciiPlot(uint32_t width = 100,
+ bool activeHigh = true) const;
+
+ const std::vector &timings() const { return data; }
+};
+
+} // namespace remote_z
diff --git a/libconcord/remote_z_learn/single.cpp b/libconcord/remote_z_learn/single.cpp
new file mode 100644
index 0000000..eef134b
--- /dev/null
+++ b/libconcord/remote_z_learn/single.cpp
@@ -0,0 +1,68 @@
+/*
+ * vim:tw=80:ai:tabstop=4:softtabstop=4:shiftwidth=4:expandtab
+ *
+ * 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, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * (C) Copyright Martin Wagner 2026
+ */
+
+#include "single.h"
+#include "binary.h"
+
+using namespace std;
+
+namespace remote_z
+{
+
+Single::Status Single::parse(const std::vector &p)
+{
+ //0x02 0xXX 0xXX -> size mod 3
+ if ((p.size() % 3) != 0) return Status::ERR_SIZE;
+
+ //drop 0x02, 0xXX 0xXX big endian
+ for (size_t i = 0; (i + 2) < p.size(); i += 3) {
+ if (p[i] == 0x02) {
+ payload.push_back(
+ remote_z::parseHarmony16_network(p[i + 1], p[i + 2]));
+ } else {
+ return Status::ERR_PAYLOAD_FORMAT;
+ }
+ }
+ return Status::OK;
+}
+
+Single::Status Single::addChunk(const std::vector &data, bool first)
+{
+ auto status = Base::check(data);
+ if (status != Status::OK) return status;
+
+ //data available, check remaining header + size
+ if ((data.size() != VALID_CHUNK_SIZE) || (data[4] != 0x01))
+ return Status::ERR_RESPONSE_FORMAT;
+
+ auto ret = parse({data.begin() + 6, data.end()});
+ if (ret != Status::OK) return ret;
+
+ //move non-timimg words
+ moveExcessBytes(first);
+ //calculate clock
+ if (first) clock = static_cast(excess[1]) * 1000000.0 / payload[0];
+
+ //check for end marker
+ if (data[5] == 0) return Status::DONE;
+ return Status::OK;
+}
+
+} // namespace remote_z
diff --git a/libconcord/remote_z_learn/single.h b/libconcord/remote_z_learn/single.h
new file mode 100644
index 0000000..4eb0b68
--- /dev/null
+++ b/libconcord/remote_z_learn/single.h
@@ -0,0 +1,49 @@
+/*
+ * vim:tw=80:ai:tabstop=4:softtabstop=4:shiftwidth=4:expandtab
+ *
+ * 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, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * (C) Copyright Martin Wagner 2026
+ */
+
+#pragma once
+
+#include "base.h"
+
+namespace remote_z
+{
+
+//see learn.md _read single frame_
+class Single : public Base
+{
+ public:
+ private:
+ const std::vector frameReq = {0x00};
+
+ uint32_t getHeaderMinSize() { return 5; };
+ uint32_t getProtoclCmd() { return COMMAND_LEARNIR_SINGLE; };
+ uint32_t getProtocolRespByte3() { return 5; };
+
+ const uint32_t VALID_CHUNK_SIZE = 18;
+
+ Status parse(const std::vector &p);
+
+ public:
+ std::vector get() { return frameReq; }
+
+ Status addChunk(const std::vector &data, bool first) override;
+};
+
+} // namespace remote_z
diff --git a/libconcord/remote_z_learn/start.h b/libconcord/remote_z_learn/start.h
new file mode 100644
index 0000000..53a1cda
--- /dev/null
+++ b/libconcord/remote_z_learn/start.h
@@ -0,0 +1,41 @@
+/*
+ * vim:tw=80:ai:tabstop=4:softtabstop=4:shiftwidth=4:expandtab
+ *
+ * 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, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * (C) Copyright Martin Wagner 2026
+ */
+
+#pragma once
+
+#include "base.h"
+
+namespace remote_z
+{
+
+class Start : public Base
+{
+ private:
+ const std::vector frameReq = {0x01, 0x01, 0x00};
+
+ uint32_t getHeaderMinSize() { return 4; };
+ uint32_t getProtoclCmd() { return COMMAND_LEARNIR_START; };
+ uint32_t getProtocolRespByte3() { return 0; };
+
+ public:
+ std::vector get() { return frameReq; }
+};
+
+} // namespace remote_z
diff --git a/libconcord/remote_z_learn/stop.h b/libconcord/remote_z_learn/stop.h
new file mode 100644
index 0000000..3a25c0c
--- /dev/null
+++ b/libconcord/remote_z_learn/stop.h
@@ -0,0 +1,41 @@
+/*
+ * vim:tw=80:ai:tabstop=4:softtabstop=4:shiftwidth=4:expandtab
+ *
+ * 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, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * (C) Copyright Martin Wagner 2026
+ */
+
+#pragma once
+
+#include "base.h"
+
+namespace remote_z
+{
+
+class Stop : public Base
+{
+ private:
+ const std::vector frameReq = {0x00};
+
+ uint32_t getHeaderMinSize() { return 4; };
+ uint32_t getProtoclCmd() { return COMMAND_LEARNIR_DONE; };
+ uint32_t getProtocolRespByte3() { return 0; };
+
+ public:
+ std::vector get() { return frameReq; }
+};
+
+} // namespace remote_z
diff --git a/libconcord/remote_z_learn/stream.cpp b/libconcord/remote_z_learn/stream.cpp
new file mode 100644
index 0000000..7b2f553
--- /dev/null
+++ b/libconcord/remote_z_learn/stream.cpp
@@ -0,0 +1,68 @@
+/*
+ * vim:tw=80:ai:tabstop=4:softtabstop=4:shiftwidth=4:expandtab
+ *
+ * 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, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * (C) Copyright Martin Wagner 2026
+ */
+
+#include "stream.h"
+#include "binary.h"
+
+using namespace std;
+
+namespace remote_z
+{
+
+Stream::Status Stream::parse(const std::vector &p)
+{
+ uint16_t v;
+
+ if ((p.size() % 2) != 0) return Status::ERR_SIZE;
+
+ for (size_t i = 0; i < p.size(); i += 2) {
+ uint16_t a = p[i + 1];
+ uint16_t b = p[i];
+ v = a | (b << 8);
+ payload.push_back(v);
+ }
+ return Status::OK;
+}
+
+Stream::Status Stream::addChunk(const std::vector &data, bool first)
+{
+ auto status = Base::check(data);
+ if (status != Status::OK) return status;
+
+ //data available, check remaining header + size
+ if ((data.size() != VALID_CHUNK_SIZE) || (data[4] != 0x70))
+ return Status::ERR_RESPONSE_FORMAT;
+ //terminator
+ if ((data[data.size() - 2] != 0x01) || (data[data.size() - 1] != 0x30))
+ return Status::ERR_TERM;
+
+ auto ret = remote_z::parseHarmony16_network(
+ {data.begin() + 5, data.end() - 2}, payload);
+ if (ret != true) return Status::ERR_SIZE;
+
+ //move non-timing words
+ moveExcessBytes(first);
+ //calculate clock
+ if (first) clock = static_cast(excess[1]) * 1000000.0 / payload[0];
+
+ return Status::OK;
+}
+
+} // namespace remote_z
diff --git a/libconcord/remote_z_learn/stream.h b/libconcord/remote_z_learn/stream.h
new file mode 100644
index 0000000..d354b24
--- /dev/null
+++ b/libconcord/remote_z_learn/stream.h
@@ -0,0 +1,49 @@
+/*
+ * vim:tw=80:ai:tabstop=4:softtabstop=4:shiftwidth=4:expandtab
+ *
+ * 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, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * (C) Copyright Martin Wagner 2026
+ */
+
+#pragma once
+
+#include "base.h"
+
+namespace remote_z
+{
+
+//see learn.md _read stream_
+class Stream : public Base
+{
+ public:
+ private:
+ const std::vector frameReq = {0x00};
+
+ uint32_t getHeaderMinSize() { return 5; };
+ uint32_t getProtoclCmd() { return COMMAND_LEARNIR_STREAM; };
+ uint32_t getProtocolRespByte3() { return 2; };
+
+ const uint32_t VALID_CHUNK_SIZE = 199;
+
+ Status parse(const std::vector &p);
+
+ public:
+ std::vector get() { return frameReq; }
+
+ Status addChunk(const std::vector &data, bool first) override;
+};
+
+} // namespace remote_z