Reorganize code to support multiple protocols. Initial (partial) secplus1
implementation.
This commit is contained in:
parent
a6e0259551
commit
608e0faeb2
|
@ -95,7 +95,8 @@ async def to_code(config):
|
|||
version=None,
|
||||
)
|
||||
|
||||
if config[CONF_PROTOCOL] == PROTOCOL_SECPLUSV1:
|
||||
if config[CONF_PROTOCOL] == PROTOCOL_SECPLUSV1:
|
||||
cg.add_define("PROTOCOL_SECPLUSV1")
|
||||
elif config[CONF_PROTOCOL] == PROTOCOL_SECPLUSV2:
|
||||
cg.add_define("PROTOCOL_SECPLUSV2")
|
||||
cg.add_define("PROTOCOL_SECPLUSV2")
|
||||
cg.add(var.init_protocol())
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include "observable.h"
|
||||
|
||||
#define ESP_LOG1 ESP_LOGD
|
||||
#define ESP_LOG2 ESP_LOGD
|
||||
|
||||
|
||||
namespace esphome {
|
||||
namespace ratgdo {
|
||||
|
||||
struct SetRollingCodeCounter { uint32_t counter; };
|
||||
struct GetRollingCodeCounter {};
|
||||
struct RollingCodeCounter { observable<uint32_t>* counter; };
|
||||
struct IncrementRollingCodeCounter { uint32_t increment; };
|
||||
struct SetClientID { uint64_t client_id; };
|
||||
struct ActivateLearn {};
|
||||
struct InactivateLearn {};
|
||||
|
||||
|
||||
// a poor man's sum-type, because C++
|
||||
class ProtocolArgs {
|
||||
public:
|
||||
union {
|
||||
SetRollingCodeCounter set_rolling_code_counter;
|
||||
GetRollingCodeCounter get_rolling_code_counter;
|
||||
RollingCodeCounter rolling_code_counter;
|
||||
IncrementRollingCodeCounter increment_rolling_code_counter;
|
||||
SetClientID set_client_id;
|
||||
ActivateLearn activate_learn;
|
||||
InactivateLearn inactivate_learn;
|
||||
} value;
|
||||
|
||||
enum class Tag {
|
||||
set_rolling_code_counter,
|
||||
get_rolling_code_counter,
|
||||
rolling_code_counter,
|
||||
increment_rolling_code_counter,
|
||||
set_client_id,
|
||||
activate_learn,
|
||||
inactivate_learn,
|
||||
void_,
|
||||
} tag;
|
||||
|
||||
ProtocolArgs(): tag(Tag::void_) {
|
||||
}
|
||||
|
||||
ProtocolArgs(GetRollingCodeCounter&& arg): tag(Tag::get_rolling_code_counter) {
|
||||
value.get_rolling_code_counter = std::move(arg);
|
||||
}
|
||||
ProtocolArgs(SetRollingCodeCounter&& arg): tag(Tag::set_rolling_code_counter) {
|
||||
value.set_rolling_code_counter = std::move(arg);
|
||||
}
|
||||
ProtocolArgs(RollingCodeCounter&& arg): tag(Tag::rolling_code_counter) {
|
||||
value.rolling_code_counter = std::move(arg);
|
||||
}
|
||||
ProtocolArgs(IncrementRollingCodeCounter&& arg): tag(Tag::increment_rolling_code_counter) {
|
||||
value.increment_rolling_code_counter = std::move(arg);
|
||||
}
|
||||
ProtocolArgs(SetClientID&& arg): tag(Tag::set_client_id) {
|
||||
value.set_client_id = std::move(arg);
|
||||
}
|
||||
ProtocolArgs(ActivateLearn&& arg): tag(Tag::activate_learn) {
|
||||
value.activate_learn = std::move(arg);
|
||||
}
|
||||
ProtocolArgs(InactivateLearn&& arg): tag(Tag::inactivate_learn) {
|
||||
value.inactivate_learn = std::move(arg);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
} // namespace ratgdo
|
||||
} // namespace esphome
|
|
@ -31,7 +31,6 @@ namespace ratgdo {
|
|||
|
||||
void RATGDOLightOutput::set_state(esphome::ratgdo::LightState state)
|
||||
{
|
||||
|
||||
bool is_on = state == LightState::ON;
|
||||
this->light_state_->current_values.set_state(is_on);
|
||||
this->light_state_->remote_values.set_state(is_on);
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
#pragma once
|
||||
|
||||
#include "ratgdo_state.h"
|
||||
#include "common.h"
|
||||
|
||||
namespace esphome {
|
||||
|
||||
class Scheduler;
|
||||
class InternalGPIOPin;
|
||||
|
||||
namespace ratgdo {
|
||||
|
||||
class RATGDOComponent;
|
||||
|
||||
class Protocol {
|
||||
public:
|
||||
virtual void setup(RATGDOComponent* ratgdo, Scheduler* scheduler, InternalGPIOPin* rx_pin, InternalGPIOPin* tx_pin);
|
||||
virtual void loop();
|
||||
virtual void dump_config();
|
||||
|
||||
virtual void light_action(LightAction action);
|
||||
virtual void lock_action(LockAction action);
|
||||
virtual void door_action(DoorAction action);
|
||||
virtual void query_action(QueryAction action);
|
||||
|
||||
virtual ProtocolArgs call(ProtocolArgs args);
|
||||
};
|
||||
|
||||
} // namespace ratgdo
|
||||
} // namespace esphome
|
|
@ -13,11 +13,14 @@
|
|||
|
||||
#include "ratgdo.h"
|
||||
#include "ratgdo_state.h"
|
||||
#include "common.h"
|
||||
#include "secplus1.h"
|
||||
#include "secplus2.h"
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
#define ESP_LOG1 ESP_LOGV
|
||||
#define ESP_LOG2 ESP_LOGV
|
||||
|
||||
namespace esphome {
|
||||
namespace ratgdo {
|
||||
|
@ -53,32 +56,33 @@ namespace ratgdo {
|
|||
this->input_obst_pin_->pin_mode(gpio::FLAG_INPUT);
|
||||
this->input_obst_pin_->attach_interrupt(RATGDOStore::isr_obstruction, &this->isr_store_, gpio::INTERRUPT_FALLING_EDGE);
|
||||
}
|
||||
#ifdef PROTOCOL_SECPLUSV2
|
||||
this->sw_serial_.begin(9600, SWSERIAL_8N1, this->input_gdo_pin_->get_pin(), this->output_gdo_pin_->get_pin(), true);
|
||||
#endif
|
||||
#ifdef PROTOCOL_SECPLUSV1
|
||||
this->sw_serial_.begin(1200, SWSERIAL_8E1, this->input_gdo_pin_->get_pin(), this->output_gdo_pin_->get_pin(), false);
|
||||
#endif
|
||||
this->sw_serial_.enableIntTx(false);
|
||||
this->sw_serial_.enableAutoBaud(true);
|
||||
|
||||
ESP_LOGV(TAG, "Syncing rolling code counter after reboot...");
|
||||
this->protocol_->setup(this, &App.scheduler, this->input_gdo_pin_, this->output_gdo_pin_);
|
||||
|
||||
// many things happening at startup, use some delay for sync
|
||||
set_timeout(SYNC_DELAY, [=] { this->sync(); });
|
||||
}
|
||||
|
||||
|
||||
// initializing protocol, this gets called before setup() because
|
||||
// the protocol_ member must be initialized before setup() because it children
|
||||
// components might require that
|
||||
void RATGDOComponent::init_protocol()
|
||||
{
|
||||
#ifdef PROTOCOL_SECPLUSV2
|
||||
this->protocol_ = new secplus2::Secplus2();
|
||||
#endif
|
||||
#ifdef PROTOCOL_SECPLUSV1
|
||||
this->protocol_ = new secplus1::Secplus1();
|
||||
#endif
|
||||
}
|
||||
|
||||
void RATGDOComponent::loop()
|
||||
{
|
||||
if (this->transmit_pending_) {
|
||||
if (!this->transmit_packet()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!this->obstruction_from_status_) {
|
||||
this->obstruction_loop();
|
||||
}
|
||||
this->gdo_state_loop();
|
||||
this->protocol_->loop();
|
||||
}
|
||||
|
||||
void RATGDOComponent::dump_config()
|
||||
|
@ -91,201 +95,194 @@ namespace ratgdo {
|
|||
} else {
|
||||
LOG_PIN(" Input Obstruction Pin: ", this->input_obst_pin_);
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Rolling Code Counter: %d", *this->rolling_code_counter);
|
||||
ESP_LOGCONFIG(TAG, " Client ID: %d", this->client_id_);
|
||||
#ifdef PROTOCOL_SECPLUSV2
|
||||
ESP_LOGCONFIG(TAG, " Protocol: SEC+ v2");
|
||||
#endif
|
||||
#ifdef PROTOCOL_SECPLUSV1
|
||||
ESP_LOGCONFIG(TAG, " Protocol: SEC+ v1");
|
||||
#endif
|
||||
this->protocol_->dump_config();
|
||||
}
|
||||
|
||||
uint16_t RATGDOComponent::decode_packet(const WirePacket& packet)
|
||||
|
||||
void RATGDOComponent::received(const DoorState door_state)
|
||||
{
|
||||
uint32_t rolling = 0;
|
||||
uint64_t fixed = 0;
|
||||
uint32_t data = 0;
|
||||
auto prev_door_state = *this->door_state;
|
||||
|
||||
decode_wireline(packet, &rolling, &fixed, &data);
|
||||
|
||||
uint16_t cmd = ((fixed >> 24) & 0xf00) | (data & 0xff);
|
||||
data &= ~0xf000; // clear parity nibble
|
||||
|
||||
if ((fixed & 0xFFFFFFFF) == this->client_id_) { // my commands
|
||||
ESP_LOG1(TAG, "[%ld] received mine: rolling=%07" PRIx32 " fixed=%010" PRIx64 " data=%08" PRIx32, millis(), rolling, fixed, data);
|
||||
return static_cast<uint16_t>(Command::UNKNOWN);
|
||||
} else {
|
||||
ESP_LOG1(TAG, "[%ld] received rolling=%07" PRIx32 " fixed=%010" PRIx64 " data=%08" PRIx32, millis(), rolling, fixed, data);
|
||||
// opening duration calibration
|
||||
if (*this->opening_duration == 0) {
|
||||
if (door_state == DoorState::OPENING && prev_door_state == DoorState::CLOSED) {
|
||||
this->start_opening = millis();
|
||||
}
|
||||
if (door_state == DoorState::OPEN && prev_door_state == DoorState::OPENING && this->start_opening > 0) {
|
||||
auto duration = (millis() - this->start_opening) / 1000;
|
||||
this->set_opening_duration(round(duration * 10) / 10);
|
||||
}
|
||||
if (door_state == DoorState::STOPPED) {
|
||||
this->start_opening = -1;
|
||||
}
|
||||
}
|
||||
// closing duration calibration
|
||||
if (*this->closing_duration == 0) {
|
||||
if (door_state == DoorState::CLOSING && prev_door_state == DoorState::OPEN) {
|
||||
this->start_closing = millis();
|
||||
}
|
||||
if (door_state == DoorState::CLOSED && prev_door_state == DoorState::CLOSING && this->start_closing > 0) {
|
||||
auto duration = (millis() - this->start_closing) / 1000;
|
||||
this->set_closing_duration(round(duration * 10) / 10);
|
||||
}
|
||||
if (door_state == DoorState::STOPPED) {
|
||||
this->start_closing = -1;
|
||||
}
|
||||
}
|
||||
|
||||
Command cmd_enum = to_Command(cmd, Command::UNKNOWN);
|
||||
uint8_t nibble = (data >> 8) & 0xff;
|
||||
uint8_t byte1 = (data >> 16) & 0xff;
|
||||
uint8_t byte2 = (data >> 24) & 0xff;
|
||||
|
||||
ESP_LOG1(TAG, "cmd=%03x (%s) byte2=%02x byte1=%02x nibble=%01x", cmd, Command_to_string(cmd_enum), byte2, byte1, nibble);
|
||||
|
||||
if (cmd == Command::STATUS) {
|
||||
|
||||
auto door_state = to_DoorState(nibble, DoorState::UNKNOWN);
|
||||
auto prev_door_state = *this->door_state;
|
||||
|
||||
// opening duration calibration
|
||||
if (*this->opening_duration == 0) {
|
||||
if (door_state == DoorState::OPENING && prev_door_state == DoorState::CLOSED) {
|
||||
this->start_opening = millis();
|
||||
}
|
||||
if (door_state == DoorState::OPEN && prev_door_state == DoorState::OPENING && this->start_opening > 0) {
|
||||
auto duration = (millis() - this->start_opening) / 1000;
|
||||
this->set_opening_duration(round(duration * 10) / 10);
|
||||
}
|
||||
if (door_state == DoorState::STOPPED) {
|
||||
this->start_opening = -1;
|
||||
}
|
||||
}
|
||||
// closing duration calibration
|
||||
if (*this->closing_duration == 0) {
|
||||
if (door_state == DoorState::CLOSING && prev_door_state == DoorState::OPEN) {
|
||||
this->start_closing = millis();
|
||||
}
|
||||
if (door_state == DoorState::CLOSED && prev_door_state == DoorState::CLOSING && this->start_closing > 0) {
|
||||
auto duration = (millis() - this->start_closing) / 1000;
|
||||
this->set_closing_duration(round(duration * 10) / 10);
|
||||
}
|
||||
if (door_state == DoorState::STOPPED) {
|
||||
this->start_closing = -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (door_state == DoorState::OPENING) {
|
||||
// door started opening
|
||||
if (prev_door_state == DoorState::CLOSING) {
|
||||
this->door_position_update();
|
||||
this->cancel_position_sync_callbacks();
|
||||
this->door_move_delta = DOOR_DELTA_UNKNOWN;
|
||||
}
|
||||
this->door_start_moving = millis();
|
||||
this->door_start_position = *this->door_position;
|
||||
if (this->door_move_delta == DOOR_DELTA_UNKNOWN) {
|
||||
this->door_move_delta = 1.0 - this->door_start_position;
|
||||
}
|
||||
this->schedule_door_position_sync();
|
||||
} else if (door_state == DoorState::CLOSING) {
|
||||
// door started closing
|
||||
if (prev_door_state == DoorState::OPENING) {
|
||||
this->door_position_update();
|
||||
this->cancel_position_sync_callbacks();
|
||||
this->door_move_delta = DOOR_DELTA_UNKNOWN;
|
||||
}
|
||||
this->door_start_moving = millis();
|
||||
this->door_start_position = *this->door_position;
|
||||
if (this->door_move_delta == DOOR_DELTA_UNKNOWN) {
|
||||
this->door_move_delta = 0.0 - this->door_start_position;
|
||||
}
|
||||
this->schedule_door_position_sync();
|
||||
} else if (door_state == DoorState::STOPPED) {
|
||||
if (door_state == DoorState::OPENING) {
|
||||
// door started opening
|
||||
if (prev_door_state == DoorState::CLOSING) {
|
||||
this->door_position_update();
|
||||
if (*this->door_position == DOOR_POSITION_UNKNOWN) {
|
||||
this->door_position = 0.5; // best guess
|
||||
}
|
||||
this->cancel_position_sync_callbacks();
|
||||
} else if (door_state == DoorState::OPEN) {
|
||||
this->door_position = 1.0;
|
||||
this->cancel_position_sync_callbacks();
|
||||
} else if (door_state == DoorState::CLOSED) {
|
||||
this->door_position = 0.0;
|
||||
this->door_move_delta = DOOR_DELTA_UNKNOWN;
|
||||
}
|
||||
this->door_start_moving = millis();
|
||||
this->door_start_position = *this->door_position;
|
||||
if (this->door_move_delta == DOOR_DELTA_UNKNOWN) {
|
||||
this->door_move_delta = 1.0 - this->door_start_position;
|
||||
}
|
||||
this->schedule_door_position_sync();
|
||||
} else if (door_state == DoorState::CLOSING) {
|
||||
// door started closing
|
||||
if (prev_door_state == DoorState::OPENING) {
|
||||
this->door_position_update();
|
||||
this->cancel_position_sync_callbacks();
|
||||
this->door_move_delta = DOOR_DELTA_UNKNOWN;
|
||||
}
|
||||
|
||||
this->door_state = door_state;
|
||||
this->door_state_received(door_state);
|
||||
this->light_state = static_cast<LightState>((byte2 >> 1) & 1); // safe because it can only be 0 or 1
|
||||
this->lock_state = static_cast<LockState>(byte2 & 1); // safe because it can only be 0 or 1
|
||||
this->motion_state = MotionState::CLEAR; // when the status message is read, reset motion state to 0|clear
|
||||
this->motor_state = MotorState::OFF; // when the status message is read, reset motor state to 0|off
|
||||
|
||||
auto learn_state = static_cast<LearnState>((byte2 >> 5) & 1);
|
||||
if (*this->learn_state != learn_state) {
|
||||
if (learn_state == LearnState::INACTIVE) {
|
||||
this->query_paired_devices();
|
||||
}
|
||||
this->learn_state = learn_state;
|
||||
this->door_start_moving = millis();
|
||||
this->door_start_position = *this->door_position;
|
||||
if (this->door_move_delta == DOOR_DELTA_UNKNOWN) {
|
||||
this->door_move_delta = 0.0 - this->door_start_position;
|
||||
}
|
||||
|
||||
if (this->obstruction_from_status_) {
|
||||
// ESP_LOGD(TAG, "Obstruction: reading from byte2, bit2, status=%d", ((byte2 >> 2) & 1) == 1);
|
||||
this->obstruction_state = static_cast<ObstructionState>((byte1 >> 6) & 1);
|
||||
// This isn't very fast to update, but its still better
|
||||
// than nothing in the case the obstruction sensor is not
|
||||
// wired up.
|
||||
ESP_LOGD(TAG, "Obstruction: reading from GDO status byte1, bit6=%s", ObstructionState_to_string(*this->obstruction_state));
|
||||
this->schedule_door_position_sync();
|
||||
} else if (door_state == DoorState::STOPPED) {
|
||||
this->door_position_update();
|
||||
if (*this->door_position == DOOR_POSITION_UNKNOWN) {
|
||||
this->door_position = 0.5; // best guess
|
||||
}
|
||||
this->cancel_position_sync_callbacks();
|
||||
} else if (door_state == DoorState::OPEN) {
|
||||
this->door_position = 1.0;
|
||||
this->cancel_position_sync_callbacks();
|
||||
} else if (door_state == DoorState::CLOSED) {
|
||||
this->door_position = 0.0;
|
||||
this->cancel_position_sync_callbacks();
|
||||
}
|
||||
|
||||
if (door_state == DoorState::CLOSED && door_state != prev_door_state) {
|
||||
this->send_command(Command::GET_OPENINGS);
|
||||
}
|
||||
if (door_state == DoorState::OPEN || door_state == DoorState::CLOSED || door_state == DoorState::STOPPED) {
|
||||
this->motor_state = MotorState::OFF;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Status: door=%s light=%s lock=%s learn=%s",
|
||||
DoorState_to_string(*this->door_state),
|
||||
LightState_to_string(*this->light_state),
|
||||
LockState_to_string(*this->lock_state),
|
||||
LearnState_to_string(*this->learn_state));
|
||||
if (door_state == DoorState::CLOSED && door_state != prev_door_state) {
|
||||
this->query_openings();
|
||||
}
|
||||
|
||||
} else if (cmd == Command::LIGHT) {
|
||||
if (nibble == 0) {
|
||||
this->light_state = LightState::OFF;
|
||||
} else if (nibble == 1) {
|
||||
this->light_state = LightState::ON;
|
||||
} else if (nibble == 2) { // toggle
|
||||
this->light_state = light_state_toggle(*this->light_state);
|
||||
}
|
||||
ESP_LOGD(TAG, "Light: action=%s state=%s",
|
||||
nibble == 0 ? "OFF" : nibble == 1 ? "ON"
|
||||
: "TOGGLE",
|
||||
LightState_to_string(*this->light_state));
|
||||
} else if (cmd == Command::MOTOR_ON) {
|
||||
this->motor_state = MotorState::ON;
|
||||
ESP_LOGD(TAG, "Motor: state=%s", MotorState_to_string(*this->motor_state));
|
||||
} else if (cmd == Command::DOOR_ACTION) {
|
||||
this->button_state = (byte1 & 1) == 1 ? ButtonState::PRESSED : ButtonState::RELEASED;
|
||||
ESP_LOGD(TAG, "Open: button=%s", ButtonState_to_string(*this->button_state));
|
||||
} else if (cmd == Command::OPENINGS) {
|
||||
// nibble==0 if it's our request
|
||||
// update openings only from our request or if it's not unknown state
|
||||
if (nibble == 0 || *this->openings != 0) {
|
||||
this->openings = (byte1 << 8) | byte2;
|
||||
ESP_LOGD(TAG, "Openings: %d", *this->openings);
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Ignoring openings, not from our request");
|
||||
}
|
||||
} else if (cmd == Command::MOTION) {
|
||||
this->motion_state = MotionState::DETECTED;
|
||||
this->door_state = door_state;
|
||||
this->door_state_received(door_state);
|
||||
|
||||
ESP_LOGD(TAG, "Door state=%s", DoorState_to_string(door_state));
|
||||
}
|
||||
|
||||
void RATGDOComponent::received(const LearnState learn_state)
|
||||
{
|
||||
this->learn_state = learn_state;
|
||||
ESP_LOGD(TAG, "Learn state=%s", LearnState_to_string(learn_state));
|
||||
}
|
||||
|
||||
void RATGDOComponent::received(const LightState light_state)
|
||||
{
|
||||
this->light_state = light_state;
|
||||
ESP_LOGD(TAG, "Light state=%s", LightState_to_string(light_state));
|
||||
}
|
||||
|
||||
void RATGDOComponent::received(const LockState lock_state)
|
||||
{
|
||||
this->lock_state = lock_state;
|
||||
ESP_LOGD(TAG, "Lock state=%s", LockState_to_string(lock_state));
|
||||
}
|
||||
|
||||
void RATGDOComponent::received(const ObstructionState obstruction_state)
|
||||
{
|
||||
if (this->obstruction_from_status_) {
|
||||
this->obstruction_state = obstruction_state;
|
||||
// This isn't very fast to update, but its still better
|
||||
// than nothing in the case the obstruction sensor is not
|
||||
// wired up.
|
||||
ESP_LOGD(TAG, "Obstruction: reading from GDO status byte1, bit6=%s", ObstructionState_to_string(*this->obstruction_state));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void RATGDOComponent::received(const MotorState motor_state)
|
||||
{
|
||||
this->motor_state = motor_state;
|
||||
ESP_LOGD(TAG, "Motor: state=%s", MotorState_to_string(*this->motor_state));
|
||||
}
|
||||
|
||||
void RATGDOComponent::received(const ButtonState button_state)
|
||||
{
|
||||
this->button_state = button_state;
|
||||
ESP_LOGD(TAG, "Button state=%s", ButtonState_to_string(*this->button_state));
|
||||
}
|
||||
|
||||
void RATGDOComponent::received(const MotionState motion_state)
|
||||
{
|
||||
this->motion_state = motion_state;
|
||||
if (motion_state == MotionState::DETECTED) {
|
||||
this->set_timeout("clear_motion", 3000, [=] {
|
||||
this->motion_state = MotionState::CLEAR;
|
||||
});
|
||||
if (*this->light_state == LightState::OFF) {
|
||||
this->send_command(Command::GET_STATUS);
|
||||
}
|
||||
ESP_LOGD(TAG, "Motion: %s", MotionState_to_string(*this->motion_state));
|
||||
} else if (cmd == Command::SET_TTC) {
|
||||
auto seconds = (byte1 << 8) | byte2;
|
||||
ESP_LOGD(TAG, "Time to close (TTC): %ds", seconds);
|
||||
} else if (cmd == Command::PAIRED_DEVICES) {
|
||||
if (nibble == static_cast<uint8_t>(PairedDevice::ALL)) {
|
||||
this->paired_total = byte2;
|
||||
} else if (nibble == static_cast<uint8_t>(PairedDevice::REMOTE)) {
|
||||
this->paired_remotes = byte2;
|
||||
} else if (nibble == static_cast<uint8_t>(PairedDevice::KEYPAD)) {
|
||||
this->paired_keypads = byte2;
|
||||
} else if (nibble == static_cast<uint8_t>(PairedDevice::WALL_CONTROL)) {
|
||||
this->paired_wall_controls = byte2;
|
||||
} else if (nibble == static_cast<uint8_t>(PairedDevice::ACCESSORY)) {
|
||||
this->paired_accessories = byte2;
|
||||
this->query_status();
|
||||
}
|
||||
}
|
||||
ESP_LOGD(TAG, "Motion: %s", MotionState_to_string(*this->motion_state));
|
||||
}
|
||||
|
||||
return cmd;
|
||||
void RATGDOComponent::received(const LightAction light_action)
|
||||
{
|
||||
if (light_action == LightAction::OFF) {
|
||||
this->light_state = LightState::OFF;
|
||||
} else if (light_action == LightAction::ON) {
|
||||
this->light_state = LightState::ON;
|
||||
} else if (light_action == LightAction::TOGGLE) {
|
||||
this->light_state = light_state_toggle(*this->light_state);
|
||||
}
|
||||
ESP_LOGD(TAG, "Light cmd=%s state=%s",
|
||||
LightAction_to_string(light_action),
|
||||
LightState_to_string(*this->light_state)
|
||||
);
|
||||
}
|
||||
|
||||
void RATGDOComponent::received(const Openings openings)
|
||||
{
|
||||
if (openings.flag == 0 || *this->openings != 0) {
|
||||
this->openings = openings.count;
|
||||
ESP_LOGD(TAG, "Openings: %d", *this->openings);
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Ignoring openings, not from our request");
|
||||
}
|
||||
}
|
||||
|
||||
void RATGDOComponent::received(const PairedDeviceCount pdc)
|
||||
{
|
||||
if (pdc.kind == PairedDevice::ALL) {
|
||||
this->paired_total = pdc.count;
|
||||
} else if (pdc.kind == PairedDevice::REMOTE) {
|
||||
this->paired_remotes = pdc.count;
|
||||
} else if (pdc.kind == PairedDevice::KEYPAD) {
|
||||
this->paired_keypads = pdc.count;
|
||||
} else if (pdc.kind == PairedDevice::WALL_CONTROL) {
|
||||
this->paired_wall_controls = pdc.count;
|
||||
} else if (pdc.kind == PairedDevice::ACCESSORY) {
|
||||
this->paired_accessories = pdc.count;
|
||||
}
|
||||
}
|
||||
|
||||
void RATGDOComponent::received(const TimeToClose ttc)
|
||||
{
|
||||
ESP_LOGD(TAG, "Time to close (TTC): %ds", ttc.seconds);
|
||||
}
|
||||
|
||||
void RATGDOComponent::schedule_door_position_sync(float update_period)
|
||||
|
@ -312,19 +309,6 @@ namespace ratgdo {
|
|||
this->door_position = clamp(position, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
void RATGDOComponent::encode_packet(Command command, uint32_t data, bool increment, WirePacket& packet)
|
||||
{
|
||||
auto cmd = static_cast<uint64_t>(command);
|
||||
uint64_t fixed = ((cmd & ~0xff) << 24) | this->client_id_;
|
||||
uint32_t send_data = (data << 8) | (cmd & 0xff);
|
||||
|
||||
ESP_LOG2(TAG, "[%ld] Encode for transmit rolling=%07" PRIx32 " fixed=%010" PRIx64 " data=%08" PRIx32, millis(), *this->rolling_code_counter, fixed, send_data);
|
||||
encode_wireline(*this->rolling_code_counter, fixed, send_data, packet);
|
||||
|
||||
if (increment) {
|
||||
this->increment_rolling_code_counter();
|
||||
}
|
||||
}
|
||||
|
||||
void RATGDOComponent::set_opening_duration(float duration)
|
||||
{
|
||||
|
@ -340,37 +324,14 @@ namespace ratgdo {
|
|||
|
||||
void RATGDOComponent::set_rolling_code_counter(uint32_t counter)
|
||||
{
|
||||
|
||||
ESP_LOGV(TAG, "Set rolling code counter to %d", counter);
|
||||
this->rolling_code_counter = counter;
|
||||
this->protocol_->call(SetRollingCodeCounter{counter});
|
||||
}
|
||||
|
||||
void RATGDOComponent::increment_rolling_code_counter(int delta)
|
||||
void RATGDOComponent::set_client_id(uint64_t client_id)
|
||||
{
|
||||
this->rolling_code_counter = (*this->rolling_code_counter + delta) & 0xfffffff;
|
||||
}
|
||||
|
||||
void RATGDOComponent::print_packet(const WirePacket& packet) const
|
||||
{
|
||||
ESP_LOG2(TAG, "Packet: [%02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X]",
|
||||
packet[0],
|
||||
packet[1],
|
||||
packet[2],
|
||||
packet[3],
|
||||
packet[4],
|
||||
packet[5],
|
||||
packet[6],
|
||||
packet[7],
|
||||
packet[8],
|
||||
packet[9],
|
||||
packet[10],
|
||||
packet[11],
|
||||
packet[12],
|
||||
packet[13],
|
||||
packet[14],
|
||||
packet[15],
|
||||
packet[16],
|
||||
packet[17],
|
||||
packet[18]);
|
||||
this->protocol_->call(SetClientID{client_id});
|
||||
}
|
||||
|
||||
/*************************** OBSTRUCTION DETECTION ***************************/
|
||||
|
@ -416,198 +377,27 @@ namespace ratgdo {
|
|||
}
|
||||
}
|
||||
|
||||
void RATGDOComponent::gdo_state_loop()
|
||||
{
|
||||
static bool reading_msg = false;
|
||||
static uint32_t msg_start = 0;
|
||||
static uint16_t byte_count = 0;
|
||||
static WirePacket rx_packet;
|
||||
static uint32_t last_read = 0;
|
||||
|
||||
if (!reading_msg) {
|
||||
while (this->sw_serial_.available()) {
|
||||
uint8_t ser_byte = this->sw_serial_.read();
|
||||
last_read = millis();
|
||||
|
||||
if (ser_byte != 0x55 && ser_byte != 0x01 && ser_byte != 0x00) {
|
||||
ESP_LOG2(TAG, "Ignoring byte (%d): %02X, baud: %d", byte_count, ser_byte, this->sw_serial_.baudRate());
|
||||
byte_count = 0;
|
||||
continue;
|
||||
}
|
||||
msg_start = ((msg_start << 8) | ser_byte) & 0xffffff;
|
||||
byte_count++;
|
||||
|
||||
// if we are at the start of a message, capture the next 16 bytes
|
||||
if (msg_start == 0x550100) {
|
||||
ESP_LOG1(TAG, "Baud: %d", this->sw_serial_.baudRate());
|
||||
rx_packet[0] = 0x55;
|
||||
rx_packet[1] = 0x01;
|
||||
rx_packet[2] = 0x00;
|
||||
|
||||
reading_msg = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (reading_msg) {
|
||||
while (this->sw_serial_.available()) {
|
||||
uint8_t ser_byte = this->sw_serial_.read();
|
||||
last_read = millis();
|
||||
rx_packet[byte_count] = ser_byte;
|
||||
byte_count++;
|
||||
// ESP_LOG2(TAG, "Received byte (%d): %02X, baud: %d", byte_count, ser_byte, this->sw_serial_.baudRate());
|
||||
|
||||
if (byte_count == PACKET_LENGTH) {
|
||||
reading_msg = false;
|
||||
byte_count = 0;
|
||||
this->print_packet(rx_packet);
|
||||
this->decode_packet(rx_packet);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (millis() - last_read > 100) {
|
||||
// if we have a partial packet and it's been over 100ms since last byte was read,
|
||||
// the rest is not coming (a full packet should be received in ~20ms),
|
||||
// discard it so we can read the following packet correctly
|
||||
ESP_LOGW(TAG, "Discard incomplete packet, length: %d", byte_count);
|
||||
reading_msg = false;
|
||||
byte_count = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RATGDOComponent::query_status()
|
||||
{
|
||||
send_command(Command::GET_STATUS);
|
||||
ESP_LOG2(TAG, "Query status action");
|
||||
this->protocol_->query_action(QueryAction::STATUS);
|
||||
}
|
||||
|
||||
void RATGDOComponent::query_openings()
|
||||
{
|
||||
send_command(Command::GET_OPENINGS);
|
||||
}
|
||||
|
||||
void RATGDOComponent::query_paired_devices()
|
||||
{
|
||||
const auto kinds = {
|
||||
PairedDevice::ALL,
|
||||
PairedDevice::REMOTE,
|
||||
PairedDevice::KEYPAD,
|
||||
PairedDevice::WALL_CONTROL,
|
||||
PairedDevice::ACCESSORY
|
||||
};
|
||||
uint32_t timeout = 0;
|
||||
for (auto kind : kinds) {
|
||||
timeout += 200;
|
||||
set_timeout(timeout, [=] { this->query_paired_devices(kind); });
|
||||
}
|
||||
}
|
||||
|
||||
void RATGDOComponent::query_paired_devices(PairedDevice kind)
|
||||
{
|
||||
ESP_LOGD(TAG, "Query paired devices of type: %s", PairedDevice_to_string(kind));
|
||||
this->send_command(Command::GET_PAIRED_DEVICES, static_cast<uint8_t>(kind));
|
||||
}
|
||||
|
||||
// wipe devices from memory based on get paired devices nibble values
|
||||
void RATGDOComponent::clear_paired_devices(PairedDevice kind)
|
||||
{
|
||||
if (kind == PairedDevice::UNKNOWN) {
|
||||
return;
|
||||
}
|
||||
ESP_LOGW(TAG, "Clear paired devices of type: %s", PairedDevice_to_string(kind));
|
||||
if (kind == PairedDevice::ALL) {
|
||||
set_timeout(200, [=] { this->send_command(Command::CLEAR_PAIRED_DEVICES, static_cast<uint8_t>(PairedDevice::REMOTE)-1); }); // wireless
|
||||
set_timeout(400, [=] { this->send_command(Command::CLEAR_PAIRED_DEVICES, static_cast<uint8_t>(PairedDevice::KEYPAD)-1); }); // keypads
|
||||
set_timeout(600, [=] { this->send_command(Command::CLEAR_PAIRED_DEVICES, static_cast<uint8_t>(PairedDevice::WALL_CONTROL)-1); }); // wall controls
|
||||
set_timeout(800, [=] { this->send_command(Command::CLEAR_PAIRED_DEVICES, static_cast<uint8_t>(PairedDevice::ACCESSORY)-1); }); // accessories
|
||||
set_timeout(1000, [=] { this->query_status(); });
|
||||
set_timeout(1200, [=] { this->query_paired_devices(); });
|
||||
} else {
|
||||
this->send_command(Command::CLEAR_PAIRED_DEVICES, static_cast<uint8_t>(kind) - 1); // just requested device
|
||||
set_timeout(200, [=] { this->query_status(); });
|
||||
set_timeout(400, [=] { this->query_paired_devices(kind); });
|
||||
}
|
||||
}
|
||||
|
||||
void RATGDOComponent::send_command(Command command, uint32_t data, bool increment)
|
||||
{
|
||||
ESP_LOG1(TAG, "Send command: %s, data: %08" PRIx32, Command_to_string(command), data);
|
||||
if (!this->transmit_pending_) { // have an untransmitted packet
|
||||
this->encode_packet(command, data, increment, this->tx_packet_);
|
||||
} else {
|
||||
// unlikely this would happed (unless not connected to GDO), we're ensuring any pending packet
|
||||
// is transmitted each loop before doing anyting else
|
||||
if (this->transmit_pending_start_ > 0) {
|
||||
ESP_LOGW(TAG, "Have untransmitted packet, ignoring command: %s", Command_to_string(command));
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Not connected to GDO, ignoring command: %s", Command_to_string(command));
|
||||
}
|
||||
}
|
||||
this->transmit_packet();
|
||||
}
|
||||
|
||||
void RATGDOComponent::send_command(Command command, uint32_t data, bool increment, std::function<void()>&& on_sent)
|
||||
{
|
||||
this->command_sent.then(on_sent);
|
||||
this->send_command(command, data, increment);
|
||||
}
|
||||
|
||||
bool RATGDOComponent::transmit_packet()
|
||||
{
|
||||
auto now = micros();
|
||||
|
||||
while (micros() - now < 1300) {
|
||||
if (this->input_gdo_pin_->digital_read()) {
|
||||
if (!this->transmit_pending_) {
|
||||
this->transmit_pending_ = true;
|
||||
this->transmit_pending_start_ = millis();
|
||||
ESP_LOGD(TAG, "Collision detected, waiting to send packet");
|
||||
} else {
|
||||
if (millis() - this->transmit_pending_start_ < 5000) {
|
||||
ESP_LOGD(TAG, "Collision detected, waiting to send packet");
|
||||
} else {
|
||||
this->transmit_pending_start_ = 0; // to indicate GDO not connected state
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
delayMicroseconds(100);
|
||||
}
|
||||
|
||||
ESP_LOG2(TAG, "Sending packet");
|
||||
this->print_packet(this->tx_packet_);
|
||||
|
||||
#ifdef PROTOCOL_SECPLUSV2
|
||||
// indicate the start of a frame by pulling the 12V line low for at leat 1 byte followed by
|
||||
// one STOP bit, which indicates to the receiving end that the start of the message follows
|
||||
// The output pin is controlling a transistor, so the logic is inverted
|
||||
this->output_gdo_pin_->digital_write(true); // pull the line low for at least 1 byte
|
||||
delayMicroseconds(1300);
|
||||
this->output_gdo_pin_->digital_write(false); // line high for at least 1 bit
|
||||
delayMicroseconds(130);
|
||||
|
||||
this->sw_serial_.write(this->tx_packet_, PACKET_LENGTH);
|
||||
#endif
|
||||
#ifdef PROTOCOL_SECPLUSV1
|
||||
// TODO: implement
|
||||
#endif
|
||||
|
||||
this->transmit_pending_ = false;
|
||||
this->transmit_pending_start_ = 0;
|
||||
this->command_sent();
|
||||
return true;
|
||||
this->protocol_->query_action(QueryAction::OPENINGS);
|
||||
}
|
||||
|
||||
void RATGDOComponent::sync()
|
||||
{
|
||||
auto sync_step = [=]() {
|
||||
if (*this->door_state == DoorState::UNKNOWN) {
|
||||
this->send_command(Command::GET_STATUS);
|
||||
this->query_status();
|
||||
return RetryResult::RETRY;
|
||||
}
|
||||
if (*this->openings == 0) {
|
||||
this->send_command(Command::GET_OPENINGS);
|
||||
this->query_openings();
|
||||
return RetryResult::RETRY;
|
||||
}
|
||||
if (*this->paired_total == PAIRED_DEVICES_UNKNOWN) {
|
||||
|
@ -640,7 +430,7 @@ namespace ratgdo {
|
|||
if (result == RetryResult::RETRY) {
|
||||
if (r == MAX_ATTEMPTS - 2 && *this->door_state == DoorState::UNKNOWN) { // made a few attempts and no progress (door state is the first sync request)
|
||||
// increment rolling code counter by some amount in case we crashed without writing to flash the latest value
|
||||
this->increment_rolling_code_counter(MAX_CODES_WITHOUT_FLASH_WRITE);
|
||||
this->protocol_->call(IncrementRollingCodeCounter{MAX_CODES_WITHOUT_FLASH_WRITE});
|
||||
}
|
||||
if (r == 0) {
|
||||
// this was last attempt, notify of sync failure
|
||||
|
@ -659,7 +449,7 @@ namespace ratgdo {
|
|||
return; // gets ignored by opener
|
||||
}
|
||||
|
||||
this->door_command(data::DOOR_OPEN);
|
||||
this->protocol_->door_action(DoorAction::OPEN);
|
||||
}
|
||||
|
||||
void RATGDOComponent::close_door()
|
||||
|
@ -670,10 +460,10 @@ namespace ratgdo {
|
|||
|
||||
if (*this->door_state == DoorState::OPENING) {
|
||||
// have to stop door first, otherwise close command is ignored
|
||||
this->door_command(data::DOOR_STOP);
|
||||
this->protocol_->door_action(DoorAction::STOP);
|
||||
this->door_state_received.then([=](DoorState s) {
|
||||
if (s == DoorState::STOPPED) {
|
||||
this->door_command(data::DOOR_CLOSE);
|
||||
this->protocol_->door_action(DoorAction::CLOSE);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Door did not stop, ignoring close command");
|
||||
}
|
||||
|
@ -681,7 +471,7 @@ namespace ratgdo {
|
|||
return;
|
||||
}
|
||||
|
||||
this->door_command(data::DOOR_CLOSE);
|
||||
this->protocol_->door_action(DoorAction::CLOSE);
|
||||
}
|
||||
|
||||
void RATGDOComponent::stop_door()
|
||||
|
@ -690,18 +480,18 @@ namespace ratgdo {
|
|||
ESP_LOGW(TAG, "The door is not moving.");
|
||||
return;
|
||||
}
|
||||
this->door_command(data::DOOR_STOP);
|
||||
this->protocol_->door_action(DoorAction::STOP);
|
||||
}
|
||||
|
||||
void RATGDOComponent::toggle_door()
|
||||
{
|
||||
this->door_command(data::DOOR_TOGGLE);
|
||||
this->protocol_->door_action(DoorAction::TOGGLE);
|
||||
}
|
||||
|
||||
void RATGDOComponent::door_move_to_position(float position)
|
||||
{
|
||||
if (*this->door_state == DoorState::OPENING || *this->door_state == DoorState::CLOSING) {
|
||||
this->door_command(data::DOOR_STOP);
|
||||
this->protocol_->door_action(DoorAction::STOP);
|
||||
this->door_state_received.then([=](DoorState s) {
|
||||
if (s == DoorState::STOPPED) {
|
||||
this->door_move_to_position(position);
|
||||
|
@ -726,9 +516,9 @@ namespace ratgdo {
|
|||
this->door_move_delta = delta;
|
||||
ESP_LOGD(TAG, "Moving to position %.2f in %.1fs", position, operation_time / 1000.0);
|
||||
|
||||
this->door_command(delta > 0 ? data::DOOR_OPEN : data::DOOR_CLOSE);
|
||||
this->protocol_->door_action(delta > 0 ? DoorAction::OPEN : DoorAction::CLOSE);
|
||||
set_timeout("move_to_position", operation_time, [=] {
|
||||
this->ensure_door_command(data::DOOR_STOP);
|
||||
this->ensure_door_action(DoorAction::STOP);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -745,85 +535,68 @@ namespace ratgdo {
|
|||
}
|
||||
}
|
||||
|
||||
void RATGDOComponent::door_command(uint32_t data)
|
||||
void RATGDOComponent::ensure_door_action(DoorAction action, uint32_t delay)
|
||||
{
|
||||
#ifdef PROTOCOL_SECPLUSV2
|
||||
data |= (1 << 16); // button 1 ?
|
||||
data |= (1 << 8); // button press
|
||||
this->send_command(Command::DOOR_ACTION, data, false, [=]() {
|
||||
set_timeout(100, [=] {
|
||||
auto data2 = data & ~(1 << 8); // button release
|
||||
this->send_command(Command::DOOR_ACTION, data2);
|
||||
});
|
||||
});
|
||||
#endif
|
||||
#ifdef PROTOCOL_SECPLUSV1
|
||||
// TODO: implement
|
||||
#endif
|
||||
}
|
||||
|
||||
void RATGDOComponent::ensure_door_command(uint32_t data, uint32_t delay)
|
||||
{
|
||||
if (data == data::DOOR_TOGGLE) {
|
||||
ESP_LOGW(TAG, "It's not recommended to use ensure_door_command with non-idempotent commands such as DOOR_TOGGLE");
|
||||
if (action == DoorAction::TOGGLE) {
|
||||
ESP_LOGW(TAG, "It's not recommended to use ensure_door_action with non-idempotent commands such as DOOR_TOGGLE");
|
||||
}
|
||||
auto prev_door_state = *this->door_state;
|
||||
this->door_state_received.then([=](DoorState s) {
|
||||
if ((data == data::DOOR_STOP) && (s != DoorState::STOPPED) && !(prev_door_state == DoorState::OPENING && s == DoorState::OPEN) && !(prev_door_state == DoorState::CLOSING && s == DoorState::CLOSED)) {
|
||||
if ((action == DoorAction::STOP) && (s != DoorState::STOPPED) && !(prev_door_state == DoorState::OPENING && s == DoorState::OPEN) && !(prev_door_state == DoorState::CLOSING && s == DoorState::CLOSED)) {
|
||||
return;
|
||||
}
|
||||
if (data == data::DOOR_OPEN && !(s == DoorState::OPENING || s == DoorState::OPEN)) {
|
||||
if (action == DoorAction::OPEN && !(s == DoorState::OPENING || s == DoorState::OPEN)) {
|
||||
return;
|
||||
}
|
||||
if (data == data::DOOR_CLOSE && !(s == DoorState::CLOSED || s == DoorState::CLOSING)) {
|
||||
if (action == DoorAction::CLOSE && !(s == DoorState::CLOSED || s == DoorState::CLOSING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOG1(TAG, "Received door status, cancel door command retry");
|
||||
cancel_timeout("door_command_retry");
|
||||
});
|
||||
this->door_command(data);
|
||||
this->protocol_->door_action(action);
|
||||
ESP_LOG1(TAG, "Ensure door command, setup door command retry");
|
||||
set_timeout("door_command_retry", delay, [=]() {
|
||||
this->ensure_door_command(data);
|
||||
this->ensure_door_action(action);
|
||||
});
|
||||
}
|
||||
|
||||
void RATGDOComponent::light_on()
|
||||
{
|
||||
this->light_state = LightState::ON;
|
||||
this->send_command(Command::LIGHT, data::LIGHT_ON);
|
||||
this->protocol_->light_action(LightAction::ON);
|
||||
}
|
||||
|
||||
void RATGDOComponent::light_off()
|
||||
{
|
||||
this->light_state = LightState::OFF;
|
||||
this->send_command(Command::LIGHT, data::LIGHT_OFF);
|
||||
this->protocol_->light_action(LightAction::OFF);
|
||||
}
|
||||
|
||||
void RATGDOComponent::toggle_light()
|
||||
{
|
||||
this->light_state = light_state_toggle(*this->light_state);
|
||||
this->send_command(Command::LIGHT, data::LIGHT_TOGGLE);
|
||||
this->protocol_->light_action(LightAction::TOGGLE);
|
||||
}
|
||||
|
||||
// Lock functions
|
||||
void RATGDOComponent::lock()
|
||||
{
|
||||
this->lock_state = LockState::LOCKED;
|
||||
this->send_command(Command::LOCK, data::LOCK_ON);
|
||||
this->protocol_->lock_action(LockAction::LOCK);
|
||||
}
|
||||
|
||||
void RATGDOComponent::unlock()
|
||||
{
|
||||
this->lock_state = LockState::UNLOCKED;
|
||||
this->send_command(Command::LOCK, data::LOCK_OFF);
|
||||
this->protocol_->lock_action(LockAction::UNLOCK);
|
||||
}
|
||||
|
||||
void RATGDOComponent::toggle_lock()
|
||||
{
|
||||
this->lock_state = lock_state_toggle(*this->lock_state);
|
||||
this->send_command(Command::LOCK, data::LOCK_TOGGLE);
|
||||
this->protocol_->lock_action(LockAction::TOGGLE);
|
||||
}
|
||||
|
||||
LightState RATGDOComponent::get_light_state() const
|
||||
|
@ -834,25 +607,22 @@ namespace ratgdo {
|
|||
// Learn functions
|
||||
void RATGDOComponent::activate_learn()
|
||||
{
|
||||
// Send LEARN with nibble = 0 then nibble = 1 to mimic wall control learn button
|
||||
this->send_command(Command::LEARN, 0);
|
||||
set_timeout(150, [=] { this->send_command(Command::LEARN, 1); });
|
||||
set_timeout(500, [=] { this->send_command(Command::GET_STATUS); });
|
||||
this->protocol_->call(ActivateLearn{});
|
||||
}
|
||||
|
||||
void RATGDOComponent::inactivate_learn()
|
||||
{
|
||||
// Send LEARN twice with nibble = 0 to inactivate learn and get status to update switch state
|
||||
this->send_command(Command::LEARN, 0);
|
||||
set_timeout(150, [=] { this->send_command(Command::LEARN, 0); });
|
||||
set_timeout(500, [=] { this->send_command(Command::GET_STATUS); });
|
||||
this->protocol_->call(InactivateLearn{});
|
||||
}
|
||||
|
||||
void RATGDOComponent::subscribe_rolling_code_counter(std::function<void(uint32_t)>&& f)
|
||||
{
|
||||
// change update to children is defered until after component loop
|
||||
// if multiple changes occur during component loop, only the last one is notified
|
||||
this->rolling_code_counter.subscribe([=](uint32_t state) { defer("rolling_code_counter", [=] { f(state); }); });
|
||||
auto counter = this->protocol_->call(GetRollingCodeCounter{});
|
||||
if (counter.tag==ProtocolArgs::Tag::rolling_code_counter) {
|
||||
counter.value.rolling_code_counter.counter->subscribe([=](uint32_t state) { defer("rolling_code_counter", [=] { f(state); }); });
|
||||
}
|
||||
}
|
||||
void RATGDOComponent::subscribe_opening_duration(std::function<void(float)>&& f)
|
||||
{
|
||||
|
|
|
@ -12,145 +12,28 @@
|
|||
************************************/
|
||||
|
||||
#pragma once
|
||||
#include "SoftwareSerial.h" // Using espsoftwareserial https://github.com/plerup/espsoftwareserial
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
|
||||
#include "observable.h"
|
||||
#include "callbacks.h"
|
||||
#include "enum.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
#include "observable.h"
|
||||
|
||||
extern "C" {
|
||||
#include "secplus.h"
|
||||
}
|
||||
|
||||
#include "ratgdo_state.h"
|
||||
#include "protocol.h"
|
||||
|
||||
namespace esphome {
|
||||
class InternalGPIOPin;
|
||||
namespace ratgdo {
|
||||
|
||||
class RATGDOComponent;
|
||||
typedef Parented<RATGDOComponent> RATGDOClient;
|
||||
|
||||
#ifdef PROTOCOL_SECPLUSV2
|
||||
static const uint8_t PACKET_LENGTH = 19;
|
||||
#endif
|
||||
|
||||
#ifdef PROTOCOL_SECPLUSV1
|
||||
// TODO: these are wrong and copied from secplusv2
|
||||
static const uint8_t PACKET_LENGTH = 19;
|
||||
#endif
|
||||
typedef uint8_t WirePacket[PACKET_LENGTH];
|
||||
|
||||
const float DOOR_POSITION_UNKNOWN = -1.0;
|
||||
const float DOOR_DELTA_UNKNOWN = -2.0;
|
||||
const uint16_t PAIRED_DEVICES_UNKNOWN = 0xFF;
|
||||
|
||||
#ifdef PROTOCOL_SECPLUSV2
|
||||
namespace data {
|
||||
const uint32_t LIGHT_OFF = 0;
|
||||
const uint32_t LIGHT_ON = 1;
|
||||
const uint32_t LIGHT_TOGGLE = 2;
|
||||
const uint32_t LIGHT_TOGGLE2 = 3;
|
||||
|
||||
const uint32_t LOCK_OFF = 0;
|
||||
const uint32_t LOCK_ON = 1;
|
||||
const uint32_t LOCK_TOGGLE = 2;
|
||||
|
||||
const uint32_t DOOR_CLOSE = 0;
|
||||
const uint32_t DOOR_OPEN = 1;
|
||||
const uint32_t DOOR_TOGGLE = 2;
|
||||
const uint32_t DOOR_STOP = 3;
|
||||
}
|
||||
|
||||
ENUM(Command, uint16_t,
|
||||
(UNKNOWN, 0x000),
|
||||
(GET_STATUS, 0x080),
|
||||
(STATUS, 0x081),
|
||||
(OBST_1, 0x084), // sent when an obstruction happens?
|
||||
(OBST_2, 0x085), // sent when an obstruction happens?
|
||||
(PAIR_3, 0x0a0),
|
||||
(PAIR_3_RESP, 0x0a1),
|
||||
|
||||
(LEARN, 0x181),
|
||||
(LOCK, 0x18c),
|
||||
(DOOR_ACTION, 0x280),
|
||||
(LIGHT, 0x281),
|
||||
(MOTOR_ON, 0x284),
|
||||
(MOTION, 0x285),
|
||||
|
||||
(GET_PAIRED_DEVICES, 0x307), // nibble 0 for total, 1 wireless, 2 keypads, 3 wall, 4 accessories.
|
||||
(PAIRED_DEVICES, 0x308), // byte2 holds number of paired devices
|
||||
(CLEAR_PAIRED_DEVICES, 0x30D), // nibble 0 to clear remotes, 1 keypads, 2 wall, 3 accessories (offset from above)
|
||||
|
||||
(LEARN_1, 0x391),
|
||||
(PING, 0x392),
|
||||
(PING_RESP, 0x393),
|
||||
|
||||
(PAIR_2, 0x400),
|
||||
(PAIR_2_RESP, 0x401),
|
||||
(SET_TTC, 0x402), // ttc_in_seconds = (byte1<<8)+byte2
|
||||
(CANCEL_TTC, 0x408), // ?
|
||||
(TTC, 0x40a), // Time to close
|
||||
(GET_OPENINGS, 0x48b),
|
||||
(OPENINGS, 0x48c), // openings = (byte1<<8)+byte2
|
||||
)
|
||||
|
||||
#endif
|
||||
|
||||
#ifdef PROTOCOL_SECPLUSV1
|
||||
|
||||
// TODO: these are wrong and copied from secplusv2
|
||||
namespace data {
|
||||
const uint32_t LIGHT_OFF = 0;
|
||||
const uint32_t LIGHT_ON = 1;
|
||||
const uint32_t LIGHT_TOGGLE = 2;
|
||||
const uint32_t LIGHT_TOGGLE2 = 3;
|
||||
|
||||
const uint32_t LOCK_OFF = 0;
|
||||
const uint32_t LOCK_ON = 1;
|
||||
const uint32_t LOCK_TOGGLE = 2;
|
||||
|
||||
const uint32_t DOOR_CLOSE = 0;
|
||||
const uint32_t DOOR_OPEN = 1;
|
||||
const uint32_t DOOR_TOGGLE = 2;
|
||||
const uint32_t DOOR_STOP = 3;
|
||||
}
|
||||
|
||||
ENUM(Command, uint16_t,
|
||||
(UNKNOWN, 0x000),
|
||||
(GET_STATUS, 0x080),
|
||||
(STATUS, 0x081),
|
||||
(OBST_1, 0x084), // sent when an obstruction happens?
|
||||
(OBST_2, 0x085), // sent when an obstruction happens?
|
||||
(PAIR_3, 0x0a0),
|
||||
(PAIR_3_RESP, 0x0a1),
|
||||
|
||||
(LEARN_2, 0x181),
|
||||
(LOCK, 0x18c),
|
||||
(DOOR_ACTION, 0x280),
|
||||
(LIGHT, 0x281),
|
||||
(MOTOR_ON, 0x284),
|
||||
(MOTION, 0x285),
|
||||
|
||||
(LEARN_1, 0x391),
|
||||
(PING, 0x392),
|
||||
(PING_RESP, 0x393),
|
||||
|
||||
(PAIR_2, 0x400),
|
||||
(PAIR_2_RESP, 0x401),
|
||||
(SET_TTC, 0x402), // ttc_in_seconds = (byte1<<8)+byte2
|
||||
(CANCEL_TTC, 0x408), // ?
|
||||
(TTC, 0x40a), // Time to close
|
||||
(GET_OPENINGS, 0x48b),
|
||||
(OPENINGS, 0x48c), // openings = (byte1<<8)+byte2
|
||||
)
|
||||
|
||||
#endif
|
||||
inline bool operator==(const uint16_t cmd_i, const Command& cmd_e) { return cmd_i == static_cast<uint16_t>(cmd_e); }
|
||||
inline bool operator==(const Command& cmd_e, const uint16_t cmd_i) { return cmd_i == static_cast<uint16_t>(cmd_e); }
|
||||
|
||||
struct RATGDOStore {
|
||||
int obstruction_low_count = 0; // count obstruction low pulses
|
||||
|
||||
|
@ -166,7 +49,9 @@ namespace ratgdo {
|
|||
void loop() override;
|
||||
void dump_config() override;
|
||||
|
||||
observable<uint32_t> rolling_code_counter { 0 };
|
||||
void init_protocol();
|
||||
|
||||
void obstruction_loop();
|
||||
|
||||
float start_opening { -1 };
|
||||
observable<float> opening_duration { 0 };
|
||||
|
@ -196,30 +81,32 @@ namespace ratgdo {
|
|||
observable<LearnState> learn_state { LearnState::UNKNOWN };
|
||||
|
||||
OnceCallbacks<void(DoorState)> door_state_received;
|
||||
OnceCallbacks<void()> command_sent;
|
||||
|
||||
observable<bool> sync_failed { false };
|
||||
|
||||
void set_output_gdo_pin(InternalGPIOPin* pin) { this->output_gdo_pin_ = pin; }
|
||||
void set_input_gdo_pin(InternalGPIOPin* pin) { this->input_gdo_pin_ = pin; }
|
||||
void set_input_obst_pin(InternalGPIOPin* pin) { this->input_obst_pin_ = pin; }
|
||||
void set_client_id(uint64_t client_id) { this->client_id_ = client_id & 0xFFFFFFFF; }
|
||||
|
||||
void gdo_state_loop();
|
||||
uint16_t decode_packet(const WirePacket& packet);
|
||||
void obstruction_loop();
|
||||
void send_command(Command command, uint32_t data = 0, bool increment = true);
|
||||
void send_command(Command command, uint32_t data, bool increment, std::function<void()>&& on_sent);
|
||||
bool transmit_packet();
|
||||
void encode_packet(Command command, uint32_t data, bool increment, WirePacket& packet);
|
||||
void print_packet(const WirePacket& packet) const;
|
||||
|
||||
void increment_rolling_code_counter(int delta = 1);
|
||||
|
||||
// security+2.0 specific
|
||||
void set_rolling_code_counter(uint32_t code);
|
||||
void set_client_id(uint64_t client_id);
|
||||
|
||||
void received(const DoorState door_state);
|
||||
void received(const LightState light_state);
|
||||
void received(const LockState lock_state);
|
||||
void received(const ObstructionState obstruction_state);
|
||||
void received(const LightAction light_action);
|
||||
void received(const MotorState motor_state);
|
||||
void received(const ButtonState button_state);
|
||||
void received(const MotionState motion_state);
|
||||
void received(const LearnState light_state);
|
||||
void received(const Openings openings);
|
||||
void received(const TimeToClose ttc);
|
||||
void received(const PairedDeviceCount pdc);
|
||||
|
||||
// door
|
||||
void door_command(uint32_t data);
|
||||
void ensure_door_command(uint32_t data, uint32_t delay = 1500);
|
||||
void ensure_door_action(DoorAction action, uint32_t delay = 1500);
|
||||
void toggle_door();
|
||||
void open_door();
|
||||
void close_door();
|
||||
|
@ -276,21 +163,14 @@ namespace ratgdo {
|
|||
void subscribe_learn_state(std::function<void(LearnState)>&& f);
|
||||
|
||||
protected:
|
||||
// tx data
|
||||
bool transmit_pending_ { false };
|
||||
uint32_t transmit_pending_start_ { 0 };
|
||||
WirePacket tx_packet_;
|
||||
|
||||
RATGDOStore isr_store_ {};
|
||||
SoftwareSerial sw_serial_;
|
||||
|
||||
Protocol* protocol_;
|
||||
bool obstruction_from_status_ { false };
|
||||
|
||||
InternalGPIOPin* output_gdo_pin_;
|
||||
InternalGPIOPin* input_gdo_pin_;
|
||||
InternalGPIOPin* input_obst_pin_;
|
||||
uint64_t client_id_ { 0x539 };
|
||||
|
||||
}; // RATGDOComponent
|
||||
|
||||
} // namespace ratgdo
|
||||
|
|
|
@ -79,5 +79,44 @@ namespace ratgdo {
|
|||
(ACCESSORY, 4),
|
||||
(UNKNOWN, 0xff))
|
||||
|
||||
// actions
|
||||
ENUM(LightAction, uint8_t,
|
||||
(OFF, 0),
|
||||
(ON, 1),
|
||||
(TOGGLE, 2),
|
||||
(UNKNOWN, 3))
|
||||
|
||||
ENUM(LockAction, uint8_t,
|
||||
(UNLOCK, 0),
|
||||
(LOCK, 1),
|
||||
(TOGGLE, 2),
|
||||
(UNKNOWN, 3))
|
||||
|
||||
ENUM(DoorAction, uint8_t,
|
||||
(CLOSE, 0),
|
||||
(OPEN, 1),
|
||||
(TOGGLE, 2),
|
||||
(STOP, 3),
|
||||
(UNKNOWN, 4))
|
||||
|
||||
ENUM(QueryAction, uint8_t,
|
||||
(STATUS, 0),
|
||||
(OPENINGS, 1),
|
||||
(UNKNOWN, 2))
|
||||
|
||||
struct Openings {
|
||||
uint16_t count;
|
||||
uint8_t flag;
|
||||
};
|
||||
|
||||
struct PairedDeviceCount {
|
||||
PairedDevice kind;
|
||||
uint16_t count;
|
||||
};
|
||||
|
||||
struct TimeToClose {
|
||||
uint16_t seconds;
|
||||
};
|
||||
|
||||
} // namespace ratgdo
|
||||
} // namespace esphome
|
||||
|
|
|
@ -0,0 +1,265 @@
|
|||
|
||||
#include "ratgdo.h"
|
||||
#include "secplus1.h"
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/scheduler.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace ratgdo {
|
||||
namespace secplus1 {
|
||||
|
||||
static const char* const TAG = "ratgdo_secplus1";
|
||||
|
||||
void Secplus1::setup(RATGDOComponent* ratgdo, Scheduler* scheduler, InternalGPIOPin* rx_pin, InternalGPIOPin* tx_pin)
|
||||
{
|
||||
this->ratgdo_ = ratgdo;
|
||||
this->scheduler_ = scheduler;
|
||||
this->tx_pin_ = tx_pin;
|
||||
this->rx_pin_ = rx_pin;
|
||||
|
||||
this->sw_serial_.begin(1200, SWSERIAL_8E1, rx_pin->get_pin(), tx_pin->get_pin(), true);
|
||||
this->sw_serial_.enableIntTx(false);
|
||||
this->sw_serial_.enableAutoBaud(true);
|
||||
}
|
||||
|
||||
|
||||
void Secplus1::loop()
|
||||
{
|
||||
auto cmd = this->read_command();
|
||||
if (cmd) {
|
||||
this->handle_command(cmd.value());
|
||||
}
|
||||
}
|
||||
|
||||
void Secplus1::dump_config()
|
||||
{
|
||||
ESP_LOGCONFIG(TAG, " Protocol: SEC+ v1");
|
||||
}
|
||||
|
||||
void Secplus1::light_action(LightAction action)
|
||||
{
|
||||
if (action == LightAction::UNKNOWN) {
|
||||
return;
|
||||
}
|
||||
if (this->light_state == LightState::UNKNOWN) {
|
||||
ESP_LOG1(TAG, "Unknown current light state, ignoring command: %s", LightAction_to_string(action));
|
||||
// TODO: request state?
|
||||
}
|
||||
if (action == LightAction::TOGGLE ||
|
||||
(this->light_state == LightState::OFF && action == LightAction::ON) ||
|
||||
(this->light_state == LightState::ON && action == LightAction::OFF)) {
|
||||
this->transmit_packet(toggle_light);
|
||||
}
|
||||
}
|
||||
|
||||
void Secplus1::lock_action(LockAction action)
|
||||
{
|
||||
if (action == LockAction::UNKNOWN) {
|
||||
return;
|
||||
}
|
||||
if (this->lock_state == LockState::UNKNOWN) {
|
||||
ESP_LOG1(TAG, "Unknown current lock state, ignoring command: %s", LockAction_to_string(action));
|
||||
// TODO: request state?
|
||||
}
|
||||
if (action == LockAction::TOGGLE ||
|
||||
(this->lock_state == LockState::UNLOCKED && action == LockAction::LOCK) ||
|
||||
(this->lock_state == LockState::LOCKED && action == LockAction::UNLOCK)) {
|
||||
this->transmit_packet(toggle_lock);
|
||||
}
|
||||
}
|
||||
|
||||
void Secplus1::door_action(DoorAction action)
|
||||
{
|
||||
if (action == DoorAction::UNKNOWN) {
|
||||
return;
|
||||
}
|
||||
if (this->door_state == DoorState::UNKNOWN) {
|
||||
ESP_LOG1(TAG, "Unknown current door state, ignoring command: %s", DoorAction_to_string(action));
|
||||
// TODO: request state?
|
||||
}
|
||||
|
||||
if (action == DoorAction::TOGGLE ||
|
||||
(this->door_state == DoorState::CLOSED && action == DoorAction::OPEN) ||
|
||||
(this->door_state == DoorState::CLOSING && action == DoorAction::OPEN) ||
|
||||
(this->door_state == DoorState::OPEN && action != DoorAction::CLOSE) ||
|
||||
(this->door_state == DoorState::OPENING && action == DoorAction::STOP)) {
|
||||
this->transmit_packet(toggle_door);
|
||||
}
|
||||
// if ((this->door_state == DoorState::CLOSING && action == DoorAction::STOP) ||
|
||||
// (this->door_state == DoorState::OPENING && action == DoorAction::CLOSE)) {
|
||||
// this->transmit_packet(toggle_door);
|
||||
// this->scheduler_->set_timeout(this->ratgdo_, "", 150, [=] {
|
||||
// this->transmit_packet(toggle_door);
|
||||
// });
|
||||
// }
|
||||
}
|
||||
|
||||
void Secplus1::query_action(QueryAction action)
|
||||
{
|
||||
ESP_LOG2(TAG, "Query action: %s", QueryAction_to_string(action));
|
||||
if (action == QueryAction::STATUS) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ProtocolArgs Secplus1::call(ProtocolArgs args)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
optional<Command> Secplus1::read_command()
|
||||
{
|
||||
static bool reading_msg = false;
|
||||
static uint32_t msg_start = 0;
|
||||
static uint16_t byte_count = 0;
|
||||
static RxPacket rx_packet;
|
||||
|
||||
if (!reading_msg) {
|
||||
while (this->sw_serial_.available()) {
|
||||
uint8_t ser_byte = this->sw_serial_.read();
|
||||
this->last_rx_ = millis();
|
||||
|
||||
if(ser_byte < 0x30 || ser_byte > 0x3A){
|
||||
ESP_LOG2(TAG, "Ignoring byte (%d): %02X, baud: %d", byte_count, ser_byte, this->sw_serial_.baudRate());
|
||||
byte_count = 0;
|
||||
continue;
|
||||
}
|
||||
rx_packet[byte_count++] = ser_byte;
|
||||
reading_msg = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (reading_msg) {
|
||||
while (this->sw_serial_.available()) {
|
||||
uint8_t ser_byte = this->sw_serial_.read();
|
||||
this->last_rx_ = millis();
|
||||
rx_packet[byte_count++] = ser_byte;
|
||||
|
||||
if (byte_count == RX_LENGTH) {
|
||||
reading_msg = false;
|
||||
byte_count = 0;
|
||||
this->print_rx_packet(rx_packet);
|
||||
return this->decode_packet(rx_packet);
|
||||
}
|
||||
}
|
||||
|
||||
if (millis() - this->last_rx_ > 100) {
|
||||
// if we have a partial packet and it's been over 100ms since last byte was read,
|
||||
// the rest is not coming (a full packet should be received in ~20ms),
|
||||
// discard it so we can read the following packet correctly
|
||||
ESP_LOGW(TAG, "Discard incomplete packet, length: %d", byte_count);
|
||||
reading_msg = false;
|
||||
byte_count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void Secplus1::print_rx_packet(const RxPacket& packet) const
|
||||
{
|
||||
ESP_LOG2(TAG, "Received packet: [%02X %02X]", packet[0], packet[1]);
|
||||
}
|
||||
|
||||
void Secplus1::print_tx_packet(const TxPacket& packet) const
|
||||
{
|
||||
ESP_LOG2(TAG, "Sending packet: [%02X %02X %02X %02X]", packet[0], packet[1], packet[2], packet[3]);
|
||||
}
|
||||
|
||||
|
||||
optional<Command> Secplus1::decode_packet(const RxPacket& packet) const
|
||||
{
|
||||
CommandType cmd_type = to_CommandType(packet[0], CommandType::UNKNOWN);
|
||||
return Command{cmd_type, packet[1]};
|
||||
}
|
||||
|
||||
|
||||
void Secplus1::handle_command(const Command& cmd)
|
||||
{
|
||||
if (cmd.type == CommandType::DOOR_STATUS) {
|
||||
|
||||
DoorState door_state;
|
||||
auto val = cmd.value & 0x7;
|
||||
// 000 0x0 stopped
|
||||
// 001 0x1 opening
|
||||
// 010 0x2 open
|
||||
// 100 0x4 closing
|
||||
// 101 0x5 closed
|
||||
// 110 0x6 stopped
|
||||
|
||||
if (val == 0x2){
|
||||
door_state = DoorState::OPEN;
|
||||
} else if (val == 0x5){
|
||||
door_state = DoorState::CLOSED;
|
||||
} else if (val == 0x0 || val == 0x6){
|
||||
door_state = DoorState::STOPPED;
|
||||
} else if (val == 0x1){
|
||||
door_state = DoorState::OPENING;
|
||||
} else if(val == 0x4){
|
||||
door_state = DoorState::CLOSING;
|
||||
} else{
|
||||
door_state = DoorState::UNKNOWN;
|
||||
}
|
||||
|
||||
if (this->door_state != door_state) {
|
||||
this->prev_door_state = this->door_state;
|
||||
this->door_state = door_state;
|
||||
} else {
|
||||
this->ratgdo_->received(door_state);
|
||||
}
|
||||
}
|
||||
else if (cmd.type == CommandType::OTHER_STATUS) {
|
||||
LightState light_state = to_LightState((cmd.value >> 2) & 1, LightState::UNKNOWN);
|
||||
if (this->light_state != light_state) {
|
||||
this->light_state = light_state;
|
||||
} else {
|
||||
this->ratgdo_->received(light_state);
|
||||
}
|
||||
|
||||
LockState lock_state = to_LockState((cmd.value >> 3) & 1, LockState::UNKNOWN);
|
||||
if (this->lock_state != lock_state) {
|
||||
this->lock_state = lock_state;
|
||||
} else {
|
||||
this->ratgdo_->received(lock_state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Secplus1::transmit_packet(const uint8_t packet[], uint32_t len)
|
||||
{
|
||||
this->transmit_packet_delayed(packet, len, 25);
|
||||
}
|
||||
|
||||
void Secplus1::transmit_packet(const TxPacket& packet)
|
||||
{
|
||||
this->print_tx_packet(packet);
|
||||
|
||||
auto delay = this->last_rx_ + 250 - millis();
|
||||
if (delay > 0) {
|
||||
this->scheduler_->set_timeout(this->ratgdo_, "", delay, [=] {
|
||||
this->transmit_packet_delayed(packet, TX_LENGTH, 25);
|
||||
});
|
||||
} else {
|
||||
this->transmit_packet_delayed(packet, TX_LENGTH, 25);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void Secplus1::transmit_packet_delayed(const uint8_t* packet, uint32_t len, uint32_t delay)
|
||||
{
|
||||
if (len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this->scheduler_->set_timeout(this->ratgdo_, "", delay, [=] {
|
||||
this->sw_serial_.write(packet[0]);
|
||||
this->transmit_packet_delayed(packet+1, len-1, delay);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
} // namespace secplus1
|
||||
} // namespace ratgdo
|
||||
} // namespace esphome
|
|
@ -0,0 +1,96 @@
|
|||
#pragma once
|
||||
|
||||
#include "SoftwareSerial.h" // Using espsoftwareserial https://github.com/plerup/espsoftwareserial
|
||||
#include "esphome/core/optional.h"
|
||||
|
||||
#include "ratgdo_state.h"
|
||||
#include "protocol.h"
|
||||
#include "callbacks.h"
|
||||
#include "observable.h"
|
||||
|
||||
namespace esphome {
|
||||
|
||||
class Scheduler;
|
||||
class InternalGPIOPin;
|
||||
|
||||
namespace ratgdo {
|
||||
namespace secplus1 {
|
||||
|
||||
static const uint8_t RX_LENGTH = 2;
|
||||
typedef uint8_t RxPacket[RX_LENGTH];
|
||||
|
||||
static const uint8_t TX_LENGTH = 4;
|
||||
typedef uint8_t TxPacket[TX_LENGTH];
|
||||
|
||||
static const TxPacket toggle_door = {0x30, 0x31, 0x31, 0xFE};
|
||||
static const TxPacket toggle_light = {0x32, 0x33, 0x33, 0xFE};
|
||||
static const TxPacket toggle_lock = {0x34, 0x35, 0x35, 0xFE};
|
||||
|
||||
static const uint8_t secplus1_states[] = {0x35,0x35,0x35,0x35,0x33,0x33,0x53,0x53,0x38,0x3A,0x3A,0x3A,0x39,0x38,0x3A, 0x38,0x3A,0x39,0x3A};
|
||||
|
||||
ENUM(CommandType, uint16_t,
|
||||
(DOOR_STATUS, 0x38),
|
||||
(OBSTRUCTION, 0x39), //
|
||||
(OTHER_STATUS, 0x3A),
|
||||
(UNKNOWN, 0xFF),
|
||||
)
|
||||
|
||||
struct Command {
|
||||
CommandType type;
|
||||
uint8_t value;
|
||||
|
||||
Command(): type(CommandType::UNKNOWN) {}
|
||||
Command(CommandType type_, uint8_t value_ = 0) : type(type_), value(value_) {}
|
||||
};
|
||||
|
||||
|
||||
class Secplus1 : public Protocol {
|
||||
public:
|
||||
void setup(RATGDOComponent* ratgdo, Scheduler* scheduler, InternalGPIOPin* rx_pin, InternalGPIOPin* tx_pin);
|
||||
void loop();
|
||||
void dump_config();
|
||||
|
||||
void light_action(LightAction action);
|
||||
void lock_action(LockAction action);
|
||||
void door_action(DoorAction action);
|
||||
void query_action(QueryAction action);
|
||||
|
||||
ProtocolArgs call(ProtocolArgs args);
|
||||
|
||||
protected:
|
||||
friend class RATGDOComponent;
|
||||
|
||||
optional<Command> read_command();
|
||||
void handle_command(const Command& cmd);
|
||||
|
||||
void print_rx_packet(const RxPacket& packet) const;
|
||||
void print_tx_packet(const TxPacket& packet) const;
|
||||
optional<Command> decode_packet(const RxPacket& packet) const;
|
||||
|
||||
void transmit_packet(const uint8_t packet[], uint32_t len);
|
||||
void transmit_packet(const TxPacket& packet);
|
||||
void transmit_packet_delayed(const uint8_t* packet, uint32_t len, uint32_t delay);
|
||||
|
||||
LightState light_state { LightState::UNKNOWN };
|
||||
LockState lock_state { LockState::UNKNOWN };
|
||||
DoorState door_state { DoorState::UNKNOWN };
|
||||
DoorState prev_door_state { DoorState::UNKNOWN };
|
||||
|
||||
bool transmit_pending_ { false };
|
||||
uint32_t transmit_pending_start_ { 0 };
|
||||
TxPacket tx_packet_;
|
||||
|
||||
uint32_t last_rx_ { 0 };
|
||||
|
||||
SoftwareSerial sw_serial_;
|
||||
|
||||
InternalGPIOPin* tx_pin_;
|
||||
InternalGPIOPin* rx_pin_;
|
||||
|
||||
RATGDOComponent* ratgdo_;
|
||||
Scheduler* scheduler_;
|
||||
};
|
||||
|
||||
} // namespace secplus1
|
||||
} // namespace ratgdo
|
||||
} // namespace esphome
|
|
@ -0,0 +1,472 @@
|
|||
|
||||
#include "ratgdo.h"
|
||||
#include "secplus2.h"
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/scheduler.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
|
||||
extern "C" {
|
||||
#include "secplus.h"
|
||||
}
|
||||
|
||||
namespace esphome {
|
||||
namespace ratgdo {
|
||||
namespace secplus2 {
|
||||
|
||||
static const char* const TAG = "ratgdo_secplus2";
|
||||
|
||||
void Secplus2::setup(RATGDOComponent* ratgdo, Scheduler* scheduler, InternalGPIOPin* rx_pin, InternalGPIOPin* tx_pin)
|
||||
{
|
||||
this->ratgdo_ = ratgdo;
|
||||
this->scheduler_ = scheduler;
|
||||
this->tx_pin_ = tx_pin;
|
||||
this->rx_pin_ = rx_pin;
|
||||
|
||||
this->sw_serial_.begin(9600, SWSERIAL_8N1, rx_pin->get_pin(), tx_pin->get_pin(), true);
|
||||
this->sw_serial_.enableIntTx(false);
|
||||
this->sw_serial_.enableAutoBaud(true);
|
||||
}
|
||||
|
||||
|
||||
void Secplus2::loop()
|
||||
{
|
||||
if (this->transmit_pending_) {
|
||||
if (!this->transmit_packet()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto cmd = this->read_command();
|
||||
if (cmd) {
|
||||
this->handle_command(*cmd);
|
||||
}
|
||||
}
|
||||
|
||||
void Secplus2::dump_config()
|
||||
{
|
||||
ESP_LOGCONFIG(TAG, " Rolling Code Counter: %d", *this->rolling_code_counter_);
|
||||
ESP_LOGCONFIG(TAG, " Client ID: %d", this->client_id_);
|
||||
ESP_LOGCONFIG(TAG, " Protocol: SEC+ v2");
|
||||
}
|
||||
|
||||
void Secplus2::light_action(LightAction action)
|
||||
{
|
||||
if (action == LightAction::UNKNOWN) {
|
||||
return;
|
||||
}
|
||||
this->send_command(Command(CommandType::LIGHT, static_cast<uint8_t>(action)));
|
||||
}
|
||||
|
||||
void Secplus2::lock_action(LockAction action)
|
||||
{
|
||||
if (action == LockAction::UNKNOWN) {
|
||||
return;
|
||||
}
|
||||
this->send_command(Command(CommandType::LOCK, static_cast<uint8_t>(action)));
|
||||
}
|
||||
|
||||
void Secplus2::door_action(DoorAction action)
|
||||
{
|
||||
if (action == DoorAction::UNKNOWN) {
|
||||
return;
|
||||
}
|
||||
this->door_command(action);
|
||||
}
|
||||
|
||||
void Secplus2::query_action(QueryAction action)
|
||||
{
|
||||
if (action == QueryAction::STATUS) {
|
||||
this->send_command(CommandType::GET_STATUS);
|
||||
} else if (action == QueryAction::OPENINGS) {
|
||||
this->send_command(CommandType::GET_OPENINGS);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ProtocolArgs Secplus2::call(ProtocolArgs args)
|
||||
{
|
||||
using Tag = ProtocolArgs::Tag;
|
||||
|
||||
if (args.tag == Tag::get_rolling_code_counter) {
|
||||
return ProtocolArgs(RollingCodeCounter{std::addressof(this->rolling_code_counter_)});
|
||||
} else if (args.tag == Tag::set_rolling_code_counter) {
|
||||
this->set_rolling_code_counter(args.value.set_rolling_code_counter.counter);
|
||||
} else if (args.tag == Tag::increment_rolling_code_counter) {
|
||||
this->increment_rolling_code_counter(args.value.increment_rolling_code_counter.increment);
|
||||
} else if (args.tag == Tag::set_client_id) {
|
||||
this->set_client_id(args.value.set_client_id.client_id);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void Secplus2::door_command(DoorAction action)
|
||||
{
|
||||
this->send_command(Command(CommandType::DOOR_ACTION, static_cast<uint8_t>(action), 1, 1), false, [=]() {
|
||||
this->scheduler_->set_timeout(this->ratgdo_, "", 150, [=] {
|
||||
this->send_command(Command(CommandType::DOOR_ACTION, static_cast<uint8_t>(action), 0, 1));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
void Secplus2::query_status()
|
||||
{
|
||||
this->send_command(CommandType::GET_STATUS);
|
||||
}
|
||||
|
||||
void Secplus2::query_openings()
|
||||
{
|
||||
this->send_command(CommandType::GET_OPENINGS);
|
||||
}
|
||||
|
||||
void Secplus2::query_paired_devices()
|
||||
{
|
||||
const auto kinds = {
|
||||
PairedDevice::ALL,
|
||||
PairedDevice::REMOTE,
|
||||
PairedDevice::KEYPAD,
|
||||
PairedDevice::WALL_CONTROL,
|
||||
PairedDevice::ACCESSORY
|
||||
};
|
||||
uint32_t timeout = 0;
|
||||
for (auto kind : kinds) {
|
||||
timeout += 200;
|
||||
this->scheduler_->set_timeout(this->ratgdo_, "", timeout, [=] { this->query_paired_devices(kind); });
|
||||
}
|
||||
}
|
||||
|
||||
void Secplus2::query_paired_devices(PairedDevice kind)
|
||||
{
|
||||
ESP_LOGD(TAG, "Query paired devices of type: %s", PairedDevice_to_string(kind));
|
||||
this->send_command(CommandType::GET_PAIRED_DEVICES, static_cast<uint8_t>(kind));
|
||||
}
|
||||
|
||||
// wipe devices from memory based on get paired devices nibble values
|
||||
void Secplus2::clear_paired_devices(PairedDevice kind)
|
||||
{
|
||||
if (kind == PairedDevice::UNKNOWN) {
|
||||
return;
|
||||
}
|
||||
ESP_LOGW(TAG, "Clear paired devices of type: %s", PairedDevice_to_string(kind));
|
||||
if (kind == PairedDevice::ALL) {
|
||||
this->scheduler_->set_timeout(this->ratgdo_, "", 200, [=] { this->send_command(CommandType::CLEAR_PAIRED_DEVICES, static_cast<uint8_t>(PairedDevice::REMOTE)-1); }); // wireless
|
||||
this->scheduler_->set_timeout(this->ratgdo_, "", 400, [=] { this->send_command(CommandType::CLEAR_PAIRED_DEVICES, static_cast<uint8_t>(PairedDevice::KEYPAD)-1); }); // keypads
|
||||
this->scheduler_->set_timeout(this->ratgdo_, "", 600, [=] { this->send_command(CommandType::CLEAR_PAIRED_DEVICES, static_cast<uint8_t>(PairedDevice::WALL_CONTROL)-1); }); // wall controls
|
||||
this->scheduler_->set_timeout(this->ratgdo_, "", 800, [=] { this->send_command(CommandType::CLEAR_PAIRED_DEVICES, static_cast<uint8_t>(PairedDevice::ACCESSORY)-1); }); // accessories
|
||||
this->scheduler_->set_timeout(this->ratgdo_, "", 1000, [=] { this->query_status(); });
|
||||
this->scheduler_->set_timeout(this->ratgdo_, "", 1200, [=] { this->query_paired_devices(); });
|
||||
} else {
|
||||
this->send_command(CommandType::CLEAR_PAIRED_DEVICES, static_cast<uint8_t>(kind) - 1); // just requested device
|
||||
this->scheduler_->set_timeout(this->ratgdo_, "", 200, [=] { this->query_status(); });
|
||||
this->scheduler_->set_timeout(this->ratgdo_, "", 400, [=] { this->query_paired_devices(kind); });
|
||||
}
|
||||
}
|
||||
|
||||
// Learn functions
|
||||
void Secplus2::activate_learn()
|
||||
{
|
||||
// Send LEARN with nibble = 0 then nibble = 1 to mimic wall control learn button
|
||||
this->learn_poll_status_ = true;
|
||||
this->send_command(Command{CommandType::LEARN, 0});
|
||||
this->scheduler_->set_timeout(this->ratgdo_, "", 150, [=] { this->send_command(Command{CommandType::LEARN, 1}); });
|
||||
this->scheduler_->set_timeout(this->ratgdo_, "", 500, [=] { this->query_status(); });
|
||||
}
|
||||
|
||||
void Secplus2::inactivate_learn()
|
||||
{
|
||||
// Send LEARN twice with nibble = 0 to inactivate learn and get status to update switch state
|
||||
this->send_command(Command{CommandType::LEARN, 0});
|
||||
this->scheduler_->set_timeout(this->ratgdo_, "", 150, [=] { this->send_command(Command{CommandType::LEARN, 0}); });
|
||||
this->scheduler_->set_timeout(this->ratgdo_, "", 500, [=] { this->query_status(); });
|
||||
}
|
||||
|
||||
|
||||
optional<Command> Secplus2::read_command()
|
||||
{
|
||||
static bool reading_msg = false;
|
||||
static uint32_t msg_start = 0;
|
||||
static uint16_t byte_count = 0;
|
||||
static WirePacket rx_packet;
|
||||
static uint32_t last_read = 0;
|
||||
|
||||
if (!reading_msg) {
|
||||
while (this->sw_serial_.available()) {
|
||||
uint8_t ser_byte = this->sw_serial_.read();
|
||||
last_read = millis();
|
||||
|
||||
if (ser_byte != 0x55 && ser_byte != 0x01 && ser_byte != 0x00) {
|
||||
ESP_LOG2(TAG, "Ignoring byte (%d): %02X, baud: %d", byte_count, ser_byte, this->sw_serial_.baudRate());
|
||||
byte_count = 0;
|
||||
continue;
|
||||
}
|
||||
msg_start = ((msg_start << 8) | ser_byte) & 0xffffff;
|
||||
byte_count++;
|
||||
|
||||
// if we are at the start of a message, capture the next 16 bytes
|
||||
if (msg_start == 0x550100) {
|
||||
ESP_LOG1(TAG, "Baud: %d", this->sw_serial_.baudRate());
|
||||
rx_packet[0] = 0x55;
|
||||
rx_packet[1] = 0x01;
|
||||
rx_packet[2] = 0x00;
|
||||
|
||||
reading_msg = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (reading_msg) {
|
||||
while (this->sw_serial_.available()) {
|
||||
uint8_t ser_byte = this->sw_serial_.read();
|
||||
last_read = millis();
|
||||
rx_packet[byte_count] = ser_byte;
|
||||
byte_count++;
|
||||
// ESP_LOG2(TAG, "Received byte (%d): %02X, baud: %d", byte_count, ser_byte, this->sw_serial_.baudRate());
|
||||
|
||||
if (byte_count == PACKET_LENGTH) {
|
||||
reading_msg = false;
|
||||
byte_count = 0;
|
||||
this->print_packet("Received packet: ", rx_packet);
|
||||
return this->decode_packet(rx_packet);
|
||||
}
|
||||
}
|
||||
|
||||
if (millis() - last_read > 100) {
|
||||
// if we have a partial packet and it's been over 100ms since last byte was read,
|
||||
// the rest is not coming (a full packet should be received in ~20ms),
|
||||
// discard it so we can read the following packet correctly
|
||||
ESP_LOGW(TAG, "Discard incomplete packet, length: %d", byte_count);
|
||||
reading_msg = false;
|
||||
byte_count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
void Secplus2::print_packet(const char* prefix, const WirePacket& packet) const
|
||||
{
|
||||
ESP_LOG2(TAG, "%s: [%02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X]",
|
||||
prefix,
|
||||
packet[0],
|
||||
packet[1],
|
||||
packet[2],
|
||||
packet[3],
|
||||
packet[4],
|
||||
packet[5],
|
||||
packet[6],
|
||||
packet[7],
|
||||
packet[8],
|
||||
packet[9],
|
||||
packet[10],
|
||||
packet[11],
|
||||
packet[12],
|
||||
packet[13],
|
||||
packet[14],
|
||||
packet[15],
|
||||
packet[16],
|
||||
packet[17],
|
||||
packet[18]);
|
||||
}
|
||||
|
||||
optional<Command> Secplus2::decode_packet(const WirePacket& packet) const
|
||||
{
|
||||
uint32_t rolling = 0;
|
||||
uint64_t fixed = 0;
|
||||
uint32_t data = 0;
|
||||
|
||||
decode_wireline(packet, &rolling, &fixed, &data);
|
||||
|
||||
uint16_t cmd = ((fixed >> 24) & 0xf00) | (data & 0xff);
|
||||
data &= ~0xf000; // clear parity nibble
|
||||
|
||||
if ((fixed & 0xFFFFFFFF) == this->client_id_) { // my commands
|
||||
ESP_LOG1(TAG, "[%ld] received mine: rolling=%07" PRIx32 " fixed=%010" PRIx64 " data=%08" PRIx32, millis(), rolling, fixed, data);
|
||||
return {};
|
||||
} else {
|
||||
ESP_LOG1(TAG, "[%ld] received rolling=%07" PRIx32 " fixed=%010" PRIx64 " data=%08" PRIx32, millis(), rolling, fixed, data);
|
||||
}
|
||||
|
||||
CommandType cmd_type = to_CommandType(cmd, CommandType::UNKNOWN);
|
||||
uint8_t nibble = (data >> 8) & 0xff;
|
||||
uint8_t byte1 = (data >> 16) & 0xff;
|
||||
uint8_t byte2 = (data >> 24) & 0xff;
|
||||
|
||||
ESP_LOG1(TAG, "cmd=%03x (%s) byte2=%02x byte1=%02x nibble=%01x", cmd, CommandType_to_string(cmd_type), byte2, byte1, nibble);
|
||||
|
||||
return Command{cmd_type, nibble, byte1, byte2};
|
||||
}
|
||||
|
||||
|
||||
void Secplus2::handle_command(const Command& cmd)
|
||||
{
|
||||
if (cmd.type == CommandType::STATUS) {
|
||||
this->ratgdo_->received(to_DoorState(cmd.nibble, DoorState::UNKNOWN));
|
||||
this->ratgdo_->received(to_LightState((cmd.byte2 >> 1) & 1, LightState::UNKNOWN));
|
||||
this->ratgdo_->received(to_LockState((cmd.byte2 & 1), LockState::UNKNOWN));
|
||||
// ESP_LOGD(TAG, "Obstruction: reading from byte2, bit2, status=%d", ((byte2 >> 2) & 1) == 1);
|
||||
this->ratgdo_->received(to_ObstructionState((cmd.byte1 >> 6) & 1, ObstructionState::UNKNOWN));
|
||||
|
||||
auto learn_state = to_LearnState((cmd.byte2 >> 5) & 1, LearnState::UNKNOWN);
|
||||
if (this->learn_state_ != learn_state) {
|
||||
if (learn_state == LearnState::ACTIVE && this->learn_poll_status_) {
|
||||
this->scheduler_->set_timeout(this->ratgdo_, "learn_poll", 1000, [=] {
|
||||
this->query_status();
|
||||
});
|
||||
} else {
|
||||
this->scheduler_->cancel_timeout(this->ratgdo_, "learn_poll");
|
||||
this->learn_poll_status_ = true;
|
||||
}
|
||||
if (learn_state == LearnState::INACTIVE) {
|
||||
this->query_paired_devices();
|
||||
}
|
||||
this->learn_state_ = learn_state;
|
||||
this->ratgdo_->received(learn_state);
|
||||
}
|
||||
}
|
||||
else if (cmd.type == CommandType::LIGHT) {
|
||||
this->ratgdo_->received(to_LightAction(cmd.nibble, LightAction::UNKNOWN));
|
||||
}
|
||||
else if (cmd.type == CommandType::MOTOR_ON) {
|
||||
this->ratgdo_->received(MotorState::ON);
|
||||
}
|
||||
else if (cmd.type == CommandType::DOOR_ACTION) {
|
||||
auto button_state = (cmd.byte1 & 1) == 1 ? ButtonState::PRESSED : ButtonState::RELEASED;
|
||||
this->ratgdo_->received(button_state);
|
||||
}
|
||||
else if (cmd.type == CommandType::MOTION) {
|
||||
this->ratgdo_->received(MotionState::DETECTED);
|
||||
}
|
||||
else if (cmd.type == CommandType::OPENINGS) {
|
||||
this->ratgdo_->received(Openings { static_cast<uint16_t>((cmd.byte1 << 8) | cmd.byte2), cmd.nibble});
|
||||
}
|
||||
else if (cmd.type == CommandType::SET_TTC) {
|
||||
this->ratgdo_->received(TimeToClose { static_cast<uint16_t>((cmd.byte1 << 8) | cmd.byte2) });
|
||||
}
|
||||
else if (cmd.type == CommandType::PAIRED_DEVICES) {
|
||||
PairedDeviceCount pdc;
|
||||
pdc.kind = to_PairedDevice(cmd.nibble, PairedDevice::UNKNOWN);
|
||||
if (pdc.kind == PairedDevice::ALL) {
|
||||
pdc.count = cmd.byte2;
|
||||
} else if (pdc.kind == PairedDevice::REMOTE) {
|
||||
pdc.count = cmd.byte2;
|
||||
} else if (pdc.kind == PairedDevice::KEYPAD) {
|
||||
pdc.count = cmd.byte2;
|
||||
} else if (pdc.kind == PairedDevice::WALL_CONTROL) {
|
||||
pdc.count = cmd.byte2;
|
||||
} else if (pdc.kind == PairedDevice::ACCESSORY) {
|
||||
pdc.count = cmd.byte2;
|
||||
}
|
||||
this->ratgdo_->received(pdc);
|
||||
}
|
||||
else if (cmd.type == CommandType::LEARN) {
|
||||
if (cmd.nibble == 1) { // LEARN sent from wall control, it will poll status every second.
|
||||
// otherwise if ratgdo or gdo initiated ratgdo needs to poll
|
||||
this->learn_poll_status_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void Secplus2::send_command(Command command, bool increment)
|
||||
{
|
||||
ESP_LOG1(TAG, "Send command: %s, data: %02X%02X%02X", CommandType_to_string(command.type), command.byte2, command.byte1, command.nibble);
|
||||
if (!this->transmit_pending_) { // have an untransmitted packet
|
||||
this->encode_packet(command, this->tx_packet_);
|
||||
if (increment) {
|
||||
this->increment_rolling_code_counter();
|
||||
}
|
||||
} else {
|
||||
// unlikely this would happed (unless not connected to GDO), we're ensuring any pending packet
|
||||
// is transmitted each loop before doing anyting else
|
||||
if (this->transmit_pending_start_ > 0) {
|
||||
ESP_LOGW(TAG, "Have untransmitted packet, ignoring command: %s", CommandType_to_string(command.type));
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Not connected to GDO, ignoring command: %s", CommandType_to_string(command.type));
|
||||
}
|
||||
}
|
||||
this->transmit_packet();
|
||||
}
|
||||
|
||||
|
||||
void Secplus2::send_command(Command command, bool increment, std::function<void()>&& on_sent)
|
||||
{
|
||||
this->command_sent_.then(on_sent);
|
||||
this->send_command(command, increment);
|
||||
}
|
||||
|
||||
void Secplus2::encode_packet(Command command, WirePacket& packet)
|
||||
{
|
||||
auto cmd = static_cast<uint64_t>(command.type);
|
||||
uint64_t fixed = ((cmd & ~0xff) << 24) | this->client_id_;
|
||||
uint32_t data = (static_cast<uint64_t>(command.byte2) << 24) | (static_cast<uint64_t>(command.byte1) << 16) | (static_cast<uint64_t>(command.nibble) << 8) | (cmd & 0xff);
|
||||
|
||||
ESP_LOG2(TAG, "[%ld] Encode for transmit rolling=%07" PRIx32 " fixed=%010" PRIx64 " data=%08" PRIx32, millis(), *this->rolling_code_counter_, fixed, data);
|
||||
encode_wireline(*this->rolling_code_counter_, fixed, data, packet);
|
||||
}
|
||||
|
||||
bool Secplus2::transmit_packet()
|
||||
{
|
||||
auto now = micros();
|
||||
|
||||
while (micros() - now < 1300) {
|
||||
if (this->rx_pin_->digital_read()) {
|
||||
if (!this->transmit_pending_) {
|
||||
this->transmit_pending_ = true;
|
||||
this->transmit_pending_start_ = millis();
|
||||
ESP_LOGD(TAG, "Collision detected, waiting to send packet");
|
||||
} else {
|
||||
if (millis() - this->transmit_pending_start_ < 5000) {
|
||||
ESP_LOGD(TAG, "Collision detected, waiting to send packet");
|
||||
} else {
|
||||
this->transmit_pending_start_ = 0; // to indicate GDO not connected state
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
delayMicroseconds(100);
|
||||
}
|
||||
|
||||
this->print_packet("Sending packet", this->tx_packet_);
|
||||
|
||||
// indicate the start of a frame by pulling the 12V line low for at leat 1 byte followed by
|
||||
// one STOP bit, which indicates to the receiving end that the start of the message follows
|
||||
// The output pin is controlling a transistor, so the logic is inverted
|
||||
this->tx_pin_->digital_write(true); // pull the line low for at least 1 byte
|
||||
delayMicroseconds(1300);
|
||||
this->tx_pin_->digital_write(false); // line high for at least 1 bit
|
||||
delayMicroseconds(130);
|
||||
|
||||
this->sw_serial_.write(this->tx_packet_, PACKET_LENGTH);
|
||||
|
||||
this->transmit_pending_ = false;
|
||||
this->transmit_pending_start_ = 0;
|
||||
this->command_sent_();
|
||||
return true;
|
||||
}
|
||||
|
||||
void Secplus2::increment_rolling_code_counter(int delta)
|
||||
{
|
||||
this->rolling_code_counter_ = (*this->rolling_code_counter_ + delta) & 0xfffffff;
|
||||
}
|
||||
|
||||
void Secplus2::set_rolling_code_counter(uint32_t counter)
|
||||
{
|
||||
ESP_LOGV(TAG, "Set rolling code counter to %d", counter);
|
||||
this->rolling_code_counter_ = counter;
|
||||
}
|
||||
|
||||
observable<uint32_t>& Secplus2::get_rolling_code_counter()
|
||||
{
|
||||
return this->rolling_code_counter_;
|
||||
}
|
||||
|
||||
void Secplus2::set_client_id(uint64_t client_id)
|
||||
{
|
||||
this->client_id_ = client_id & 0xFFFFFFFF;
|
||||
}
|
||||
|
||||
} // namespace secplus2
|
||||
} // namespace ratgdo
|
||||
} // namespace esphome
|
|
@ -0,0 +1,133 @@
|
|||
#pragma once
|
||||
|
||||
#include "SoftwareSerial.h" // Using espsoftwareserial https://github.com/plerup/espsoftwareserial
|
||||
#include "esphome/core/optional.h"
|
||||
|
||||
#include "ratgdo_state.h"
|
||||
#include "protocol.h"
|
||||
#include "callbacks.h"
|
||||
#include "observable.h"
|
||||
#include "common.h"
|
||||
|
||||
namespace esphome {
|
||||
|
||||
class Scheduler;
|
||||
class InternalGPIOPin;
|
||||
|
||||
namespace ratgdo {
|
||||
class RATGDOComponent;
|
||||
|
||||
namespace secplus2 {
|
||||
|
||||
static const uint8_t PACKET_LENGTH = 19;
|
||||
typedef uint8_t WirePacket[PACKET_LENGTH];
|
||||
|
||||
ENUM(CommandType, uint16_t,
|
||||
(UNKNOWN, 0x000),
|
||||
(GET_STATUS, 0x080),
|
||||
(STATUS, 0x081),
|
||||
(OBST_1, 0x084), // sent when an obstruction happens?
|
||||
(OBST_2, 0x085), // sent when an obstruction happens?
|
||||
(PAIR_3, 0x0a0),
|
||||
(PAIR_3_RESP, 0x0a1),
|
||||
|
||||
(LEARN, 0x181),
|
||||
(LOCK, 0x18c),
|
||||
(DOOR_ACTION, 0x280),
|
||||
(LIGHT, 0x281),
|
||||
(MOTOR_ON, 0x284),
|
||||
(MOTION, 0x285),
|
||||
|
||||
(GET_PAIRED_DEVICES, 0x307), // nibble 0 for total, 1 wireless, 2 keypads, 3 wall, 4 accessories.
|
||||
(PAIRED_DEVICES, 0x308), // byte2 holds number of paired devices
|
||||
(CLEAR_PAIRED_DEVICES, 0x30D), // nibble 0 to clear remotes, 1 keypads, 2 wall, 3 accessories (offset from above)
|
||||
|
||||
(LEARN_1, 0x391),
|
||||
(PING, 0x392),
|
||||
(PING_RESP, 0x393),
|
||||
|
||||
(PAIR_2, 0x400),
|
||||
(PAIR_2_RESP, 0x401),
|
||||
(SET_TTC, 0x402), // ttc_in_seconds = (byte1<<8)+byte2
|
||||
(CANCEL_TTC, 0x408), // ?
|
||||
(TTC, 0x40a), // Time to close
|
||||
(GET_OPENINGS, 0x48b),
|
||||
(OPENINGS, 0x48c), // openings = (byte1<<8)+byte2
|
||||
)
|
||||
|
||||
inline bool operator==(const uint16_t cmd_i, const CommandType& cmd_e) { return cmd_i == static_cast<uint16_t>(cmd_e); }
|
||||
inline bool operator==(const CommandType& cmd_e, const uint16_t cmd_i) { return cmd_i == static_cast<uint16_t>(cmd_e); }
|
||||
|
||||
|
||||
struct Command {
|
||||
CommandType type;
|
||||
uint8_t nibble;
|
||||
uint8_t byte1;
|
||||
uint8_t byte2;
|
||||
|
||||
Command(): type(CommandType::UNKNOWN) {}
|
||||
Command(CommandType type_, uint8_t nibble_ = 0, uint8_t byte1_ = 0, uint8_t byte2_ = 0) : type(type_), nibble(nibble_), byte1(byte1_), byte2(byte2_) {}
|
||||
};
|
||||
|
||||
class Secplus2 : public Protocol {
|
||||
public:
|
||||
void setup(RATGDOComponent* ratgdo, Scheduler* scheduler, InternalGPIOPin* rx_pin, InternalGPIOPin* tx_pin);
|
||||
void loop();
|
||||
void dump_config();
|
||||
|
||||
void light_action(LightAction action);
|
||||
void lock_action(LockAction action);
|
||||
void door_action(DoorAction action);
|
||||
void query_action(QueryAction action);
|
||||
|
||||
ProtocolArgs call(ProtocolArgs args);
|
||||
|
||||
void increment_rolling_code_counter(int delta = 1);
|
||||
void set_rolling_code_counter(uint32_t counter);
|
||||
observable<uint32_t>& get_rolling_code_counter();
|
||||
void set_client_id(uint64_t client_id);
|
||||
|
||||
protected:
|
||||
optional<Command> read_command();
|
||||
void handle_command(const Command& cmd);
|
||||
|
||||
void send_command(Command cmd, bool increment = true);
|
||||
void send_command(Command cmd, bool increment, std::function<void()>&& on_sent);
|
||||
void encode_packet(Command cmd, WirePacket& packet);
|
||||
bool transmit_packet();
|
||||
|
||||
void door_command(DoorAction action);
|
||||
|
||||
void query_status();
|
||||
void query_openings();
|
||||
void query_paired_devices();
|
||||
void query_paired_devices(PairedDevice kind);
|
||||
void clear_paired_devices(PairedDevice kind);
|
||||
void activate_learn();
|
||||
void inactivate_learn();
|
||||
|
||||
void print_packet(const char* prefix, const WirePacket& packet) const;
|
||||
optional<Command> decode_packet(const WirePacket& packet) const;
|
||||
|
||||
LearnState learn_state_ { LearnState::UNKNOWN };
|
||||
bool learn_poll_status_;
|
||||
|
||||
observable<uint32_t> rolling_code_counter_ { 0 };
|
||||
uint64_t client_id_ { 0x539 };
|
||||
|
||||
bool transmit_pending_ { false };
|
||||
uint32_t transmit_pending_start_ { 0 };
|
||||
WirePacket tx_packet_;
|
||||
OnceCallbacks<void()> command_sent_;
|
||||
|
||||
SoftwareSerial sw_serial_;
|
||||
|
||||
InternalGPIOPin* tx_pin_;
|
||||
InternalGPIOPin* rx_pin_;
|
||||
|
||||
RATGDOComponent* ratgdo_;
|
||||
Scheduler* scheduler_;
|
||||
};
|
||||
} // namespace secplus2
|
||||
} // namespace ratgdo
|
||||
} // namespace esphome
|
Loading…
Reference in New Issue