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.

311 lines
10 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[MSG_LEN];
  12. using LED = uint16_t;
  13. // clang-format off
  14. // Bit patterns that are used for making a front panel LED light up.
  15. // These patterns can be bitwise OR-ed to target multiple LEDs.
  16. static const LED LED_NONE = 0b0000110000000000;
  17. static const LED LED_POWER = 0b0100110000000000;
  18. static const LED LED_COLOR = 0b0001110000000000;
  19. static const LED LED_1 = 0b0000111000000000;
  20. static const LED LED_2 = 0b0000110100000000;
  21. static const LED LED_3 = 0b0000110010000000;
  22. static const LED LED_4 = 0b0000110001000000;
  23. static const LED LED_5 = 0b0000110000100000;
  24. static const LED LED_6 = 0b0000110000010000;
  25. static const LED LED_7 = 0b0000110000001000;
  26. static const LED LED_8 = 0b0000110000000100;
  27. static const LED LED_9 = 0b0000110000000010;
  28. static const LED LED_10 = 0b0000110000000001;
  29. // Combinations of LEDs that are use by the original firmware to
  30. // indicate the current brightness setting of the lamp..
  31. static const LED LED_LEVEL_0 = LED_NONE;
  32. static const LED LED_LEVEL_1 = LED_POWER|LED_COLOR|LED_1;
  33. static const LED LED_LEVEL_2 = LED_POWER|LED_COLOR|LED_1|LED_2;
  34. static const LED LED_LEVEL_3 = LED_POWER|LED_COLOR|LED_1|LED_2|LED_3;
  35. static const LED LED_LEVEL_4 = LED_POWER|LED_COLOR|LED_1|LED_2|LED_3|LED_4;
  36. static const LED LED_LEVEL_5 = LED_POWER|LED_COLOR|LED_1|LED_2|LED_3|LED_4|LED_5;
  37. static const LED LED_LEVEL_6 = LED_POWER|LED_COLOR|LED_1|LED_2|LED_3|LED_4|LED_5|LED_6;
  38. static const LED LED_LEVEL_7 = LED_POWER|LED_COLOR|LED_1|LED_2|LED_3|LED_4|LED_5|LED_6|LED_7;
  39. static const LED LED_LEVEL_8 = LED_POWER|LED_COLOR|LED_1|LED_2|LED_3|LED_4|LED_5|LED_6|LED_7|LED_8;
  40. static const LED LED_LEVEL_9 = LED_POWER|LED_COLOR|LED_1|LED_2|LED_3|LED_4|LED_5|LED_6|LED_7|LED_8|LED_9;
  41. static const LED LED_LEVEL_10 = LED_POWER|LED_COLOR|LED_1|LED_2|LED_3|LED_4|LED_5|LED_6|LED_7|LED_8|LED_9|LED_10;
  42. // Commands for the I2C interface.
  43. static const MSG READY_FOR_EV = {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01};
  44. static const MSG SET_LEDS = {0x02, 0x03, 0x00, 0x00, 0x64, 0x00, 0x00};
  45. using EVENT = uint16_t;
  46. // Bit flags that are used for specifying an event.
  47. // Events are registered using the following bit pattern
  48. // (bit 1 being the least significant bit):
  49. //
  50. // BITS INDICATE PATTERN RESULT
  51. // 1 status 0 parsing event failed
  52. // 1 parsing event successful
  53. // 2-4 part 000 part unknown
  54. // 001 power button
  55. // 010 color button
  56. // 100 slider
  57. // 5-6 type 00 type unknown
  58. // 01 touch
  59. // 10 release
  60. // 7-11 slider 00000 level known (or part is not "slider")
  61. // level 00001 level 1
  62. // ... up to
  63. // 10101 level 21
  64. //
  65. static const EVENT FLAG_INIT = 0b00000000000;
  66. static const EVENT FLAG_ERR = 0b00000000000;
  67. static const EVENT FLAG_OK = 0b00000000001;
  68. static const EVENT FLAG_PART_SHIFT = 1;
  69. static const EVENT FLAG_PART_MASK = 0b00000001110;
  70. static const EVENT FLAG_PART_UNKNOWN = 0b00000000000;
  71. static const EVENT FLAG_PART_POWER = 0b00000000010;
  72. static const EVENT FLAG_PART_COLOR = 0b00000000100;
  73. static const EVENT FLAG_PART_SLIDER = 0b00000001000;
  74. static const EVENT FLAG_TYPE_SHIFT = 4;
  75. static const EVENT FLAG_TYPE_MASK = 0b00000110000;
  76. static const EVENT FLAG_TYPE_UNKNOWN = 0b00000000000;
  77. static const EVENT FLAG_TYPE_TOUCH = 0b00000010000;
  78. static const EVENT FLAG_TYPE_RELEASE = 0b00000100000;
  79. static const EVENT FLAG_LEVEL_SHIFT = 6;
  80. static const EVENT FLAG_LEVEL_MASK = 0b11111000000;
  81. static const EVENT FLAG_LEVEL_UNKNOWN = 0b00000000000;
  82. // clang-format on
  83. /**
  84. * This class implements a parser that translates event byte codes from the
  85. * Xiaomi Mijia Bedside Lamp 2 into usable events.
  86. */
  87. class FrontPanelEventParser {
  88. public:
  89. /**
  90. * Parse the provided event byte code (7 bytes long).
  91. * Returns a unique integer event code that describes the parsed event.
  92. */
  93. EVENT parse(uint8_t *m) {
  94. EVENT ev = FLAG_INIT;
  95. // All events use the prefix [04:04:01:00].
  96. if (m[0] != 0x04 || m[1] != 0x04 || m[2] != 0x01 || m[3] != 0x00) {
  97. return error_(ev, m, "prefix is not 04:04:01:00");
  98. }
  99. // The next byte determines the part that is touched.
  100. // All remaining bytes specify the event for that part.
  101. switch (m[4]) {
  102. case 0x01: // power button
  103. case 0x02: // color button
  104. ev |= (m[4] == 0x01 ? FLAG_PART_POWER : FLAG_PART_COLOR);
  105. if (m[5] == 0x01 && m[6] == (0x02 + m[4]))
  106. ev |= FLAG_TYPE_TOUCH;
  107. else if (m[5] == 0x02 && m[6] == (0x03 + m[4]))
  108. ev |= FLAG_TYPE_RELEASE;
  109. else
  110. return error_(ev, m, "invalid event type for button");
  111. break;
  112. case 0x03: // slider touch
  113. case 0x04: // slider release
  114. ev |= FLAG_PART_SLIDER;
  115. ev |= (m[4] == 0x03 ? FLAG_TYPE_TOUCH : FLAG_TYPE_RELEASE);
  116. if ((m[6] - m[5] - m[4] - 0x01) != 0)
  117. return error_(ev, m, "invalid slider level crc");
  118. else if (m[5] > 0x16 || m[5] < 0x01)
  119. return error_(ev, m, "out of bounds slider value");
  120. else {
  121. auto level = 0x17 - m[5];
  122. ev |= (level << FLAG_LEVEL_SHIFT);
  123. }
  124. break;
  125. default:
  126. return error_(ev, m, "invalid part id");
  127. return ev;
  128. }
  129. // All parsing rules passed. This event is valid.
  130. ESP_LOGD(TAG, "Front panel I2C event parsed: code=%d", ev);
  131. ev |= FLAG_OK;
  132. return ev;
  133. }
  134. protected:
  135. bool has_(EVENT ev, EVENT mask, EVENT flag) { return (ev & mask) == flag; }
  136. EVENT error_(EVENT ev, uint8_t *m, const char *msg) {
  137. ESP_LOGE(TAG, "Front panel I2C event error:");
  138. ESP_LOGE(TAG, " Error: %s", msg);
  139. 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]);
  140. ESP_LOGE(TAG, " Parsed part: %s", format_part(ev));
  141. ESP_LOGE(TAG, " Parsed event type: %s", format_event_type(ev));
  142. if (has_(ev, FLAG_PART_MASK, FLAG_PART_SLIDER)) {
  143. auto level = (ev & FLAG_LEVEL_MASK) >> FLAG_LEVEL_SHIFT;
  144. if (level > 0) {
  145. ESP_LOGE(TAG, " Parsed slider level: %d", level);
  146. }
  147. }
  148. return ev;
  149. }
  150. const char *format_part(EVENT ev) {
  151. if (has_(ev, FLAG_PART_MASK, FLAG_PART_POWER))
  152. return "power button";
  153. if (has_(ev, FLAG_PART_MASK, FLAG_PART_COLOR))
  154. return "color button";
  155. if (has_(ev, FLAG_PART_MASK, FLAG_PART_SLIDER))
  156. return "slider";
  157. return "n/a";
  158. }
  159. const char *format_event_type(EVENT ev) {
  160. if (has_(ev, FLAG_TYPE_MASK, FLAG_TYPE_TOUCH))
  161. return "touch";
  162. if (has_(ev, FLAG_TYPE_MASK, FLAG_TYPE_RELEASE))
  163. return "release";
  164. return "n/a";
  165. }
  166. };
  167. /**
  168. * This is a hardware abstraction layer that communicates with with front
  169. * panel of the Xiaomi Mijia Bedside Lamp 2.
  170. *
  171. * It serves as a hub component for other components that implement
  172. * the actual buttons and slider components.
  173. */
  174. class FrontPanelHAL : public Component, public i2c::I2CDevice {
  175. public:
  176. FrontPanelEventParser event;
  177. /**
  178. * Set the GPIO pin that is used by the front panel to notify the ESP
  179. * that a touch/release event can be read using I2C.
  180. */
  181. void set_trigger_pin(GPIOPin *pin) { trigger_pin_ = pin; }
  182. void add_on_event_callback(std::function<void(EVENT)> &&callback) { event_callback_.add(std::move(callback)); }
  183. void setup() {
  184. ESP_LOGCONFIG(TAG, "Setting up I2C trigger pin interrupt...");
  185. trigger_pin_->setup();
  186. trigger_pin_->attach_interrupt(FrontPanelHAL::isr, this, FALLING);
  187. }
  188. void dump_config() {
  189. ESP_LOGCONFIG(TAG, "FrontPanelHAL:");
  190. LOG_PIN(" I2C interrupt pin: ", trigger_pin_);
  191. }
  192. void loop() {
  193. // Read and publish front panel events.
  194. auto current_event_id = event_id_;
  195. if (current_event_id != last_event_id_) {
  196. last_event_id_ = current_event_id;
  197. MSG message;
  198. if (write_bytes_raw(READY_FOR_EV, MSG_LEN) && read_bytes_raw(message, MSG_LEN)) {
  199. auto ev = event.parse(message);
  200. if (ev & FLAG_OK) {
  201. event_callback_.call(ev);
  202. }
  203. }
  204. }
  205. }
  206. /**
  207. * Enables the LEDs according to the provided input.
  208. * The input value is a bitwise OR-ed set of LED constants.
  209. * E.g. LED_POWER|LED_1|LED2
  210. */
  211. void set_leds(uint16_t leds) {
  212. MSG msg;
  213. msg[0] = SET_LEDS[0];
  214. msg[1] = SET_LEDS[1];
  215. msg[2] = leds >> 8;
  216. msg[3] = leds & 0xff;
  217. msg[4] = SET_LEDS[4];
  218. msg[5] = SET_LEDS[5];
  219. msg[6] = SET_LEDS[6];
  220. write_bytes_raw(msg, MSG_LEN);
  221. }
  222. /**
  223. * Sets the front panel illumination to the provided level (0.0 - 1.0).
  224. *
  225. * This implements the behavior of the original firmware for representing
  226. * the lamp's brightness.
  227. *
  228. * Level 0.0 means: turn off the front panel illumination.
  229. * The other levels are translated to one of the available levels,
  230. * represented by the level indicator (i.e. the illumination of the
  231. * slider bar.)
  232. */
  233. void set_light_level(float level) {
  234. if (level == 0.0f)
  235. set_leds(LED_LEVEL_0);
  236. else if (level < 0.15)
  237. set_leds(LED_LEVEL_1);
  238. else if (level < 0.25)
  239. set_leds(LED_LEVEL_2);
  240. else if (level < 0.35)
  241. set_leds(LED_LEVEL_3);
  242. else if (level < 0.45)
  243. set_leds(LED_LEVEL_4);
  244. else if (level < 0.55)
  245. set_leds(LED_LEVEL_5);
  246. else if (level < 0.65)
  247. set_leds(LED_LEVEL_6);
  248. else if (level < 0.75)
  249. set_leds(LED_LEVEL_7);
  250. else if (level < 0.85)
  251. set_leds(LED_LEVEL_8);
  252. else if (level < 0.95)
  253. set_leds(LED_LEVEL_9);
  254. else
  255. set_leds(LED_LEVEL_10);
  256. }
  257. protected:
  258. GPIOPin *trigger_pin_;
  259. static void isr(FrontPanelHAL *store);
  260. volatile int event_id_ = 0;
  261. int last_event_id_ = 0;
  262. CallbackManager<void(EVENT)> event_callback_{};
  263. };
  264. /**
  265. * This ISR is used to handle IRQ triggers from the front panel.
  266. *
  267. * The front panel pulls the trigger pin low for a short period of time
  268. * when a new event is available. All we do here to handle the interrupt,
  269. * is increment a simple event id counter. The main loop of the component
  270. * will take care of actually reading and processing the event.
  271. */
  272. void ICACHE_RAM_ATTR HOT FrontPanelHAL::isr(FrontPanelHAL *store) { store->event_id_++; }
  273. } // namespace bslamp2
  274. } // namespace xiaomi
  275. } // namespace esphome