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.

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