You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

227 lines
8.2 KiB

  1. #pragma once
  2. #include <array>
  3. #include "common.h"
  4. #include "esphome/core/component.h"
  5. #include "esphome/core/esphal.h"
  6. #include "esphome/components/i2c/i2c.h"
  7. namespace esphome {
  8. namespace yeelight {
  9. namespace bs2 {
  10. static const uint8_t MSG_LEN = 7;
  11. using MSG = uint8_t[7];
  12. // The commands that are supported by the
  13. static const MSG READY_FOR_EV = { 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 };
  14. static const MSG TURN_ON = { 0x02, 0x03, 0x5E, 0x00, 0x64, 0x00, 0x00 };
  15. static const MSG TURN_OFF = { 0x02, 0x03, 0x0C, 0x00, 0x64, 0x00, 0x00 };
  16. static const MSG SET_LEVEL_1 = { 0x02, 0x03, 0x5E, 0x00, 0x64, 0x00, 0x00 };
  17. static const MSG SET_LEVEL_2 = { 0x02, 0x03, 0x5F, 0x00, 0x64, 0x00, 0x00 };
  18. static const MSG SET_LEVEL_3 = { 0x02, 0x03, 0x5F, 0x80, 0x64, 0x00, 0x00 };
  19. static const MSG SET_LEVEL_4 = { 0x02, 0x03, 0x5F, 0xC0, 0x64, 0x00, 0x00 };
  20. static const MSG SET_LEVEL_5 = { 0x02, 0x03, 0x5F, 0xE0, 0x64, 0x00, 0x00 };
  21. static const MSG SET_LEVEL_6 = { 0x02, 0x03, 0x5F, 0xF0, 0x64, 0x00, 0x00 };
  22. static const MSG SET_LEVEL_7 = { 0x02, 0x03, 0x5F, 0xF8, 0x64, 0x00, 0x00 };
  23. static const MSG SET_LEVEL_8 = { 0x02, 0x03, 0x5F, 0xFC, 0x64, 0x00, 0x00 };
  24. static const MSG SET_LEVEL_9 = { 0x02, 0x03, 0x5F, 0xFE, 0x64, 0x00, 0x00 };
  25. static const MSG SET_LEVEL_10 = { 0x02, 0x03, 0x5F, 0xFF, 0x64, 0x00, 0x00 };
  26. using EVENT = uint16_t;
  27. // Bit flags that are used for specifying an event.
  28. // Events are registered using the following bit pattern
  29. // (bit 1 being the least significant bit):
  30. //
  31. // BITS INDICATE PATTERN RESULT
  32. // 1 status 0 parsing event failed
  33. // 1 parsing event successful
  34. // 2-3 part 00 part unknown
  35. // 01 power button
  36. // 10 color button
  37. // 11 slider
  38. // 4-5 type 00 type unknown
  39. // 01 touch
  40. // 10 release
  41. // 6-10 slider 00000 level known (or part is not "slider")
  42. // level 00001 level 1
  43. // ... up to
  44. // 10101 level 21
  45. //
  46. static const EVENT FLAG_INIT = 0b0000000000;
  47. static const EVENT FLAG_ERR = 0b0000000000;
  48. static const EVENT FLAG_OK = 0b0000000001;
  49. static const EVENT FLAG_PART_MASK = 0b0000000110;
  50. static const EVENT FLAG_PART_UNKNOWN = 0b0000000000;
  51. static const EVENT FLAG_PART_POWER = 0b0000000010;
  52. static const EVENT FLAG_PART_COLOR = 0b0000000100;
  53. static const EVENT FLAG_PART_SLIDER = 0b0000000110;
  54. static const EVENT FLAG_TYPE_MASK = 0b0000011000;
  55. static const EVENT FLAG_TYPE_UNKNOWN = 0b0000000000;
  56. static const EVENT FLAG_TYPE_TOUCH = 0b0000001000;
  57. static const EVENT FLAG_TYPE_RELEASE = 0b0000010000;
  58. static const EVENT FLAG_LEVEL_MASK = 0b1111100000;
  59. static const EVENT FLAG_LEVEL_UNKNOWN = 0b0000000000;
  60. /**
  61. * This class implements a parser that translates event byte codes from the
  62. * Yeelight Bedside Lamp 2 into usable events.
  63. */
  64. class FrontPanelEventParser {
  65. public:
  66. /**
  67. * Parse the provided event byte code (7 bytes long).
  68. * Returns a unique integer event code that describes the parsed event.
  69. */
  70. EVENT parse(uint8_t *m) {
  71. EVENT ev = FLAG_INIT;
  72. // All events use the prefix [04:04:01:00].
  73. if (m[0] != 0x04 || m[1] != 0x04 || m[2] != 0x01 || m[3] != 0x00) {
  74. return error_(ev, m, "prefix is not 04:04:01:00");
  75. }
  76. // The next byte determines the part that is touched.
  77. // All remaining bytes specify the event for that part.
  78. switch (m[4]) {
  79. case 0x01: // power button
  80. case 0x02: // color button
  81. ev |= (m[4] == 0x01 ? FLAG_PART_POWER : FLAG_PART_COLOR);
  82. if (m[5] == 0x01 && m[6] == (0x02 + m[4])) {
  83. ev |= FLAG_TYPE_TOUCH;
  84. } else if (m[5] == 0x02 && m[6] == (0x03 + m[4])) {
  85. ev |= FLAG_TYPE_RELEASE;
  86. } else {
  87. return error_(ev, m, "invalid event type for button");
  88. }
  89. break;
  90. case 0x03: // slider touch
  91. case 0x04: // slider release
  92. ev |= FLAG_PART_SLIDER;
  93. ev |= (m[4] == 0x03 ? FLAG_TYPE_TOUCH : FLAG_TYPE_RELEASE);
  94. if ((m[6] - m[5] - m[4] - 0x01) != 0) {
  95. return error_(ev, m, "invalid slider level crc");
  96. } else if (m[5] > 0x16 || m[5] < 0x01) {
  97. return error_(ev, m, "out of bounds slider value");
  98. } else {
  99. auto level = 0x17 - m[5];
  100. ev |= (level << 5);
  101. }
  102. break;
  103. default:
  104. return error_(ev, m, "invalid part id");
  105. return ev;
  106. }
  107. // All parsing rules passed. This event is valid.
  108. ESP_LOGD(TAG, "Front panel I2C event parsed: code=%d", ev);
  109. ev |= FLAG_OK;
  110. return ev;
  111. }
  112. protected:
  113. bool has_(EVENT ev, EVENT mask, EVENT flag) {
  114. return (ev & mask) == flag;
  115. }
  116. EVENT error_(EVENT ev, uint8_t *m, const char* msg) {
  117. ESP_LOGE(TAG, "Front panel I2C event error:");
  118. ESP_LOGE(TAG, " Error: %s", msg);
  119. ESP_LOGE(TAG, " Event: [%02x:%02x:%02x:%02x:%02x:%02x:%02x]",
  120. m[0], m[1], m[2], m[3], m[4], m[5], m[6]);
  121. ESP_LOGE(TAG, " Parsed part: %s",
  122. (has_(ev, FLAG_PART_MASK, FLAG_PART_POWER) ? "power button" :
  123. has_(ev, FLAG_PART_MASK, FLAG_PART_COLOR) ? "color button" :
  124. has_(ev, FLAG_PART_MASK, FLAG_PART_SLIDER) ? "slider" : "n/a"));
  125. ESP_LOGE(TAG, " Parsed event type: %s",
  126. (has_(ev, FLAG_TYPE_MASK, FLAG_TYPE_TOUCH) ? "touch" :
  127. has_(ev, FLAG_TYPE_MASK, FLAG_TYPE_RELEASE) ? "release" : "n/a"));
  128. if (has_(ev, FLAG_PART_MASK, FLAG_PART_SLIDER)) {
  129. auto level = (ev & FLAG_LEVEL_MASK) >> 5;
  130. if (level > 0) {
  131. ESP_LOGE(TAG, " Parsed slider level: %d", level);
  132. }
  133. }
  134. return ev;
  135. }
  136. };
  137. /**
  138. * This is a hardware abstraction layer that communicates with with front
  139. * panel of the Yeelight Bedside Lamp 2.
  140. *
  141. * It serves as a hub component for other components that implement
  142. * the actual buttons and slider components.
  143. */
  144. class FrontPanelHAL : public Component, public i2c::I2CDevice {
  145. public:
  146. FrontPanelEventParser event;
  147. /**
  148. * Set the GPIO pin that is used by the front panel to notify the ESP
  149. * that a touch/release event can be read using I2C.
  150. */
  151. void set_trigger_pin(GPIOPin *pin) { trigger_pin_ = pin; }
  152. void add_on_event_callback(std::function<void(EVENT)> &&callback) {
  153. event_callback_.add(std::move(callback));
  154. }
  155. void setup() {
  156. ESP_LOGCONFIG(TAG, "Setting up I2C trigger pin interrupt...");
  157. trigger_pin_->setup();
  158. trigger_pin_->attach_interrupt(
  159. FrontPanelHAL::isr, this, FALLING);
  160. }
  161. void dump_config() {
  162. ESP_LOGCONFIG(TAG, "I2C interrupt");
  163. LOG_PIN(" Interrupt pin: ", trigger_pin_);
  164. }
  165. void loop() {
  166. // Read and publish front panel events.
  167. auto current_event_id = event_id_;
  168. if (current_event_id != last_event_id_) {
  169. last_event_id_ = current_event_id;
  170. MSG message;
  171. if (write_bytes_raw(READY_FOR_EV, MSG_LEN) &&
  172. read_bytes_raw(message, MSG_LEN)) {
  173. auto ev = event.parse(message);
  174. if (ev & FLAG_OK) {
  175. event_callback_.call(ev);
  176. }
  177. }
  178. }
  179. }
  180. protected:
  181. GPIOPin *trigger_pin_;
  182. static void isr(FrontPanelHAL *store);
  183. volatile int event_id_ = 0;
  184. int last_event_id_ = 0;
  185. CallbackManager<void(EVENT)> event_callback_{};
  186. };
  187. /**
  188. * This ISR is used to handle IRQ triggers from the front panel.
  189. *
  190. * The front panel pulls the trigger pin low for a short period of time
  191. * when a new event is available. All we do here to handle the interrupt,
  192. * is increment a simple event id counter. The main loop of the component
  193. * will take care of actually reading and processing the event.
  194. */
  195. void ICACHE_RAM_ATTR HOT FrontPanelHAL::isr(FrontPanelHAL *store) {
  196. store->event_id_++;
  197. }
  198. } // namespace bs2
  199. } // namespace yeelight
  200. } // namespace esphome