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.

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