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.

270 lines
9.1 KiB

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