From 795afd69ad771d5bd34dc6c1855fa12ac89b3b16 Mon Sep 17 00:00:00 2001 From: Djordje Mandic <6750655+DjordjeMandic@users.noreply.github.com> Date: Thu, 26 Dec 2024 05:47:41 +0100 Subject: [PATCH] [remote_base] WIP: nec protocol fix --- esphome/components/remote_base/__init__.py | 8 +- .../components/remote_base/nec_protocol.cpp | 121 ++++++++++++------ esphome/components/remote_base/nec_protocol.h | 4 +- esphome/const.py | 1 + 4 files changed, 90 insertions(+), 44 deletions(-) diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 3d1c10a092..ba70f93a50 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -750,7 +750,7 @@ NEC_SCHEMA = cv.Schema( { cv.Required(CONF_ADDRESS): cv.hex_uint16_t, cv.Required(CONF_COMMAND): cv.hex_uint16_t, - cv.Optional(CONF_COMMAND_REPEATS, default=1): cv.uint16_t, + cv.Optional(CONF_COMMAND_REPEATS, default=1): cv.uint16_t, # // TODO: command_repeats -> repeats (repeat as repeat code, not whole message frame) } ) @@ -763,7 +763,7 @@ def nec_binary_sensor(var, config): NECData, ("address", config[CONF_ADDRESS]), ("command", config[CONF_COMMAND]), - ("command_repeats", config[CONF_COMMAND_REPEATS]), + ("command_repeats", config[CONF_COMMAND_REPEATS]), # // TODO: command_repeats -> repeats (repeat as repeat code, not whole message frame) ) ) ) @@ -785,8 +785,8 @@ async def nec_action(var, config, args): cg.add(var.set_address(template_)) template_ = await cg.templatable(config[CONF_COMMAND], args, cg.uint16) cg.add(var.set_command(template_)) - template_ = await cg.templatable(config[CONF_COMMAND_REPEATS], args, cg.uint16) - cg.add(var.set_command_repeats(template_)) + template_ = await cg.templatable(config[CONF_COMMAND_REPEATS], args, cg.uint16) # // TODO: command_repeats -> repeats (repeat as repeat code, not whole message frame) + cg.add(var.set_command_repeats(template_)) # // TODO: command_repeats -> repeats (repeat as repeat code, not whole message frame) # Pioneer diff --git a/esphome/components/remote_base/nec_protocol.cpp b/esphome/components/remote_base/nec_protocol.cpp index 6ea9a8583c..46a2014fce 100644 --- a/esphome/components/remote_base/nec_protocol.cpp +++ b/esphome/components/remote_base/nec_protocol.cpp @@ -6,93 +6,138 @@ namespace remote_base { static const char *const TAG = "remote.nec"; -static const uint32_t HEADER_HIGH_US = 9000; -static const uint32_t HEADER_LOW_US = 4500; -static const uint32_t BIT_HIGH_US = 560; -static const uint32_t BIT_ONE_LOW_US = 1690; -static const uint32_t BIT_ZERO_LOW_US = 560; +// NEC IR Protocol +// Protocol Reference: https://techdocs.altium.com/display/FPGA/NEC%2bInfrared%2bTransmission%2bProtocol +// Timing constants in microseconds +static const uint32_t HEADER_HIGH_US = 9000; // AGC burst: 9ms HIGH +static const uint32_t HEADER_LOW_US = 4500; // AGC burst: 4.5ms LOW +static const uint32_t BIT_HIGH_US = 562; // Bit HIGH duration: 562.5µs +static const uint32_t BIT_ONE_LOW_US = 1687; // Logic '1': 562.5µs HIGH + 1687.5µs LOW +static const uint32_t BIT_ZERO_LOW_US = 562; // Logic '0': 562.5µs HIGH + 562.5µs LOW +static const uint32_t SPACE_INTER_FRAME_US = 40000; // Inter-frame space: 40ms +static const uint32_t SPACE_AGC_REPEAT_US = 96187; // AGC Repeat space: ~96.1875ms + +// TODO: command_repeats -> repeats (repeat as repeat code, not whole message frame) void NECProtocol::encode(RemoteTransmitData *dst, const NECData &data) { - ESP_LOGD(TAG, "Sending NEC: address=0x%04X, command=0x%04X command_repeats=%d", data.address, data.command, - data.command_repeats); + ESP_LOGD(TAG, "Sending NEC: address=0x%04X, command=0x%04X, command_repeats=%d", + data.address, data.command, data.command_repeats); - dst->reserve(2 + 32 + 32 * data.command_repeats + 2); - dst->set_carrier_frequency(38000); + // Reserve: AGC Header (2) + Address bits (32) + Command bits (32) + Final mark (2) + Repeat codes (4 per repeat) + dst->reserve(2 + 32 + 32 + 2 + data.command_repeats * 4); + dst->set_carrier_frequency(38222); + // Send the AGC Header (start of frame) dst->item(HEADER_HIGH_US, HEADER_LOW_US); + // Encode 16-bit Address for (uint16_t mask = 1; mask; mask <<= 1) { if (data.address & mask) { - dst->item(BIT_HIGH_US, BIT_ONE_LOW_US); + dst->item(BIT_HIGH_US, BIT_ONE_LOW_US); // Logic '1' } else { - dst->item(BIT_HIGH_US, BIT_ZERO_LOW_US); + dst->item(BIT_HIGH_US, BIT_ZERO_LOW_US); // Logic '0' } } - for (uint16_t repeats = 0; repeats < data.command_repeats; repeats++) { - for (uint16_t mask = 1; mask; mask <<= 1) { - if (data.command & mask) { - dst->item(BIT_HIGH_US, BIT_ONE_LOW_US); - } else { - dst->item(BIT_HIGH_US, BIT_ZERO_LOW_US); - } + // Encode 16-bit Command + for (uint16_t mask = 1; mask; mask <<= 1) { + if (data.command & mask) { + dst->item(BIT_HIGH_US, BIT_ONE_LOW_US); // Logic '1' + } else { + dst->item(BIT_HIGH_US, BIT_ZERO_LOW_US); // Logic '0' } } + // Final mark to end the message frame dst->mark(BIT_HIGH_US); + + // Space between message frame and first AGC repeat code + if (data.command_repeats > 0) { + dst->space(SPACE_INTER_FRAME_US); + } + + // Send AGC Repeat Codes if requested + for (uint16_t repeats = 0; repeats < data.command_repeats; repeats++) { + // AGC Repeat header (shorter version of the initial AGC header) + dst->item(HEADER_HIGH_US, HEADER_LOW_US / 2); // Shortened AGC header + dst->mark(BIT_HIGH_US); // Mark to complete the repeat code + + // Add space after repeat code, except after the final repeat + if (repeats < data.command_repeats - 1) { + dst->space(SPACE_AGC_REPEAT_US); + } + } } + +// TODO: command_repeats -> repeats (repeat as repeat code, not whole message frame) optional NECProtocol::decode(RemoteReceiveData src) { NECData data{ .address = 0, .command = 0, - .command_repeats = 1, + .command_repeats = 0, // Start with 0, as the first frame is counted explicitly }; - if (!src.expect_item(HEADER_HIGH_US, HEADER_LOW_US)) + + // Validate the AGC header (start of frame) + if (!src.expect_item(HEADER_HIGH_US, HEADER_LOW_US)) { return {}; + } + // Decode 16-bit Address for (uint16_t mask = 1; mask; mask <<= 1) { if (src.expect_item(BIT_HIGH_US, BIT_ONE_LOW_US)) { - data.address |= mask; + data.address |= mask; // Logic '1' } else if (src.expect_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) { - data.address &= ~mask; + data.address &= ~mask; // Logic '0' } else { return {}; } } + // Decode 16-bit Command for (uint16_t mask = 1; mask; mask <<= 1) { if (src.expect_item(BIT_HIGH_US, BIT_ONE_LOW_US)) { - data.command |= mask; + data.command |= mask; // Logic '1' } else if (src.expect_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) { - data.command &= ~mask; + data.command &= ~mask; // Logic '0' } else { return {}; } } - while (src.peek_item(BIT_HIGH_US, BIT_ONE_LOW_US) || src.peek_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) { - uint16_t command = 0; - for (uint16_t mask = 1; mask; mask <<= 1) { - if (src.expect_item(BIT_HIGH_US, BIT_ONE_LOW_US)) { - command |= mask; - } else if (src.expect_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) { - command &= ~mask; - } else { - return {}; - } + // Validate the final mark (end of the message frame) + if (!src.expect_mark(BIT_HIGH_US)) { + return {}; + } + + // Check for inter-frame space (only if repeats are expected) + if (src.peek_space(SPACE_INTER_FRAME_US)) { + src.expect_space(SPACE_INTER_FRAME_US); // Consume inter-frame space + } + + // Decode AGC Repeat Codes + while (src.peek_item(HEADER_HIGH_US, HEADER_LOW_US / 2)) { + // Validate the repeat header + if (!src.expect_item(HEADER_HIGH_US, HEADER_LOW_US / 2)) { + break; } - // Make sure the extra/repeated data matches original command - if (command != data.command) { - return {}; + // Validate the repeat mark + if (!src.expect_mark(BIT_HIGH_US)) { + break; } + // Increment the repeat count data.command_repeats += 1; + + // Check for space between repeat codes + if (src.peek_space(SPACE_AGC_REPEAT_US)) { + src.expect_space(SPACE_AGC_REPEAT_US); // Consume space between repeats + } } - src.expect_mark(BIT_HIGH_US); return data; } + void NECProtocol::dump(const NECData &data) { ESP_LOGI(TAG, "Received NEC: address=0x%04X, command=0x%04X command_repeats=%d", data.address, data.command, data.command_repeats); diff --git a/esphome/components/remote_base/nec_protocol.h b/esphome/components/remote_base/nec_protocol.h index 71e1bccba8..2c2d2d5b64 100644 --- a/esphome/components/remote_base/nec_protocol.h +++ b/esphome/components/remote_base/nec_protocol.h @@ -8,7 +8,7 @@ namespace remote_base { struct NECData { uint16_t address; uint16_t command; - uint16_t command_repeats; + uint16_t command_repeats; // TODO: command_repeats -> repeats (repeat as repeat code, not whole message frame) bool operator==(const NECData &rhs) const { return address == rhs.address && command == rhs.command; } }; @@ -26,7 +26,7 @@ template class NECAction : public RemoteTransmitterActionBase repeats (repeat as repeat code, not whole message frame) void encode(RemoteTransmitData *dst, Ts... x) override { NECData data{}; diff --git a/esphome/const.py b/esphome/const.py index 0f41dc1aec..e3a50d3fef 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -153,6 +153,7 @@ CONF_COLOR_TEMPERATURE = "color_temperature" CONF_COLORS = "colors" CONF_COMMAND = "command" CONF_COMMAND_REPEATS = "command_repeats" +# TODO: CONF_REPEATS = "repeats" CONF_COMMAND_RETAIN = "command_retain" CONF_COMMAND_TOPIC = "command_topic" CONF_COMMENT = "comment"