├── README.md ├── XiaomiMiaoMiaoCeBT ├── XiaomiMiaoMiaoCeBT.cpp ├── XiaomiMiaoMiaoCeBT.h └── examples │ ├── black_and_white_switching │ ├── README.md │ └── main.cpp │ ├── clock │ ├── README.md │ └── main.cpp │ ├── find_segment_positions │ ├── README.md │ └── main.cpp │ └── numbers_and_shapes │ ├── README.md │ └── main.cpp ├── captures ├── ATC_firmware │ ├── MHO-C401_ATC_PowerOn_Init.logicdata │ └── MHO_C401_ATC_PartialUpdates_within_one_minute.logicdata ├── SPI-captures-analysis.xlsx └── original_firmware │ ├── MHO-C401_PowerOn_Init.logicdata │ └── MHO_C401_PartialUpdates_within_one_minute.logicdata └── media ├── MHO-C401-epd-screen-demo.gif ├── PCB.jpg ├── all_segments.png ├── breakout_board.jpg └── digit_segments.png /README.md: -------------------------------------------------------------------------------- 1 | # Reverse Engineering a MHO-C401 2 | 3 | First, I want to thank the owners of the following two GitHub repositories, whose hard work I used as foundation and inspiration when I was reverse engineering the MHO-C401 sensor: 4 | 5 | * https://github.com/jonathonlui/MHO-C201 6 | * https://github.com/GitJer/XiaomiMiaoMiaoCe 7 | 8 | The current repository is a fork of https://github.com/GitJer/XiaomiMiaoMiaoCe due to my desire to use a C++ based approach, but the present README is heavily based on the reverse engineering efforts of https://github.com/jonathonlui/MHO-C201. 9 | 10 | ![example-count](media/MHO-C401-epd-screen-demo.gif) 11 | 12 | The **Mijia MHO-C401** (also sold as **Xiaomi MiaoMiaoCe MMC-C401**) is a Bluetooth-enabled thermometer and hygrometer display with a segmented e-ink screen. 13 | 14 | A multimeter in continuity mode was used to trace the connections between the stock MCU (TLSR8251) and the display controller (which is *unknown* - MHO-C201 seems to be using HT16E07, but its connections and commands differ from those used in MHO-C401). 15 | 16 | To determine the purpose of each connection, a logic analyser was used to capture the signals (see [`./captures`](./captures)) between the MCU and display controller. The captures were compared to a number of e-ink display controllers' datasheets, but a single and exact match was not found. 17 | 18 | The display was disconnected from the MCU board and it was instead connected to a breakout board in order to be able to control it easily with a Wemos D1 mini EPS8266-based board: 19 | 20 | ![breakout-board](media/breakout_board.jpg) 21 | 22 | ## A C++ Library 23 | 24 | An Arduino-compatible C++ library with a few examples to control the display is in: [./XiaomiMiaoMiaoCeBT](./XiaomiMiaoMiaoCeBT). 25 | 26 | ## PCB 27 | ![PCB](media/PCB.jpg) 28 | 29 | ## Components 30 | 31 | ### **Display** at **P1**: Segmented e-paper display controller connected to PCB with 10-pin FPC. 32 | 33 | - Display controller IC is unknown, but unlike that used in MHO-C201, it is *not* HT1607. Some of the commands captured between the MCU and Display match HT16E07 datasheet, some match other display controllers 34 | 35 | ### Pins (from "top") 36 | 37 | 1. VDL - Driver low supply voltage – bypass to GND with 1μF capacitor 38 | - Connected to capacitor at **C3**. 39 | - Measured voltage: 6.5V. 40 | 41 | 2. VDH - Driver high supply voltage – bypass to GND with 1μF capacitor 42 | - Connected to capacitor at **C2**. 43 | - Measured voltage 12.6V 44 | 45 | 3. GND 46 | 47 | 4. VDD - Between 2.4 to 3.6V 48 | 49 | 5. SDA (data) - connected to pin 13 (SPI_DO) of MCU 50 | 51 | 6. SCL (clock) - connected to pin 1 (SPI_CK) of MCU 52 | 53 | 7. CSB (latch) - connected to pin 24 (SPI_CS) of MCU 54 | - Low during data clock pulses, pulses high after 9 clocks pulses 55 | 56 | 8. UNKNOWN (probably SHD_N, connected to pin 17 (PC4) of MCU 57 | - Maybe this is charge pump enable pin – low shutdown 58 | 59 | 9. RST_N - connected to pin 3 (SWS/PA6) of MCU 60 | 61 | 10. BUSY_N - connected to pin 2 (PA5) of MCU 62 | - Busy flag output pin 63 | - BUSY_N="0" – driver is busy, driver is refreshing the display 64 | - BUSY_N="1" – driver is idle, host can send command/data to driver 65 | 66 | 67 | ### **MCU** at **U3**: TLSR8251 [datasheet](http://wiki.telink-semi.cn/doc/ds/DS_TLSR8251-E_Datasheet%20for%20Telink%20BLE+IEEE802.15.4%20Multi-Standard%20Wireless%20SoC%20TLSR8251.pdf) 68 | 69 | ## Startup and Update 70 | 71 | ### On startup the stock MCU: 72 | 73 | The MCU updates the display 3 times on startup: 74 | 75 | 1. MCU powers on 76 | 2. After 100 ms, sets Display SHD_N high 77 | 3. Send low pulse on RST_N for 110 microseconds 78 | 4. Update #1: Full black clear. This update turns on then off all black segments using special LUT values (see below for **Update Display Sequence**) 79 | - BUSY_N is low for about 3400 ms between after sending DRF 80 | 5. Send low pulse on RST_N for 110 microseconds 81 | 6. Update #2: Full white clear. This update turns on then off all white segments using special LUT values (see below for **Update Display Sequence**) 82 | - BUSY_N is low for about 1950 ms between after sending DRF 83 | 7. Send low pulse on RST_N for 110 microseconds 84 | 8. Update #3: Set all black segments on (except the background segment). 85 | - BUSY_N is low for about 1950 ms between after sending DRF 86 | 9. Wait 100ms 87 | 10. During this 100ms the MCU reads temp from Sensor 88 | 11. Update #4: Set some black/white segments on/off (to show temp / humidity) 89 | - BUSY_N is low for about 1950 after DRF 90 | 12. Sends final POWER_OFF command 91 | 92 | ### Update Display Sequence 93 | 94 | After startup the MCU will periodically read the Sensor and sets on/off state of the segments. 95 | 96 | 1. Send low pulse on RST_N for 110 microseconds 97 | 98 | 3. Send: Power On (POWER_ON - 0x04) 99 | 100 | 4. Wait for BUSY_N high 101 | 102 | 5. Send: Panel Setting (PANEL_SETTING - 0x00) 103 | 104 | 6. Send: Power Setting (POWER_SETTING - 0x01) 105 | 106 | 7. Send: Power Off Sequence Setting (POWER_OFF_SEQUENCE_SETTING - 0x03) 107 | 108 | 8. Send: Frame Rate Control (FRAME_RATE_CONTROL - 0x30) 109 | 110 | 9. Send (ONLY when *not* initialising): PARTIAL_DISPLAY_REFRESH - 0x15 111 | 112 | 10.1. Send: LUTV (0x20), LUT_KK (0x23), LUT_KW (0x26) 113 | 114 | - The LUT define the timing and voltages for turning on/off the segments 115 | - There are 3 sets of LUT values 116 | 1. LUT values to fully clear the display by turning on and then off all the segments 117 | 2. LUT values to turn off segments 118 | 3. LUT values to turn on segments 119 | 120 | 10.2. Send LUT (0x24) and LUT (0x25) 121 | 122 | - Send two additional LUT tables when performing update (i.e. *not* during the initialisation phase) 123 | 124 | 11. Send: Data Start Transmission 1 (DATA_START_TRANSMISSION_1 - 0x18) 125 | 126 | - Logic analyzer captures show DATA_START_TRANSMISSION_1 command is 18 bytes. 127 | 128 | 11. Send: Data Start Transmission 2 (DATA_START_TRANSMISSION_2 - 0x1C) 129 | 130 | - Logic analyzer captures show DATA_START_TRANSMISSION_2 command is 18 bytes. 131 | 132 | 12. Send: Display Refresh (DISPLAY_REFRESH - 0x12) 133 | 134 | 13. BUSY_N goes low which means display is refreshing 135 | 136 | 14. Wait for BUSY_N high 137 | 138 | 15. Send: Power Off (POWER_OFF - 0x02) 139 | 140 | ## Segments 141 | 142 | Segments are mapped accross the bits (0-7) of 18 bytes (0-17), sent with DATA_START_TRANSMISSION command. The exact mapping was determined by cycling through all the bits and bytes, using the sample here: [find_segment_positions](./XiaomiMiaoMiaoCeBT/examples/find_segment_positions). 143 | 144 | Following image shows how the segments are mapped: first number is the byte, second number is the bit within that byte: 145 | 146 | ![All segments](media/all_segments.png) 147 | 148 | This convention is used in the code when defining logic groups of segments, like: `top_left`, `bottom_right`, `face` etc. 149 | 150 | #### Segment (16,5) 151 | 152 | Segment (16,5) turns on the parts of the display that are always on during normal usage: 153 | 154 | - Degree symbol and common parts of C and F 155 | - Decimal point 156 | - Side of faces 157 | - Percent sign 158 | 159 | #### Segment (17,7) 160 | 161 | Segment (17,7) is the "background" 162 | 163 | #### Forming symbols 164 | 165 | In order to facilitate the display of Hexadecimal numbers, another two-dimensional array is used - `digits[16][11]`. This array defines how each hexadecimal digit maps to the following group of segments (1 to 11): 166 | 167 | ![Digit segments](media/digit_segments.png) 168 | 169 | You can check the following example, demonstrating how to "draw" numbers: [numbers_and_shapes](./XiaomiMiaoMiaoCeBT/examples/numbers_and_shapes). 170 | -------------------------------------------------------------------------------- /XiaomiMiaoMiaoCeBT/XiaomiMiaoMiaoCeBT.cpp: -------------------------------------------------------------------------------- 1 | #include "XiaomiMiaoMiaoCeBT.h" 2 | 3 | uint8_t T_DTM_init[18] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; 4 | uint8_t T_DTM2_init[18] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; 5 | 6 | //---------------------------------- 7 | // LUTV, LUT_KK and LUT_KW values taken from the actual device with a 8 | // logic analyzer 9 | //---------------------------------- 10 | uint8_t T_LUTV_init[15] = {0x47, 0x47, 0x01, 0x87, 0x87, 0x01, 0x47, 0x47, 0x01, 0x87, 0x87, 0x01, 0x81, 0x81, 0x01}; 11 | uint8_t T_LUT_KK_init[15] = {0x87, 0x87, 0x01, 0x87, 0x87, 0x01, 0x47, 0x47, 0x01, 0x47, 0x47, 0x01, 0x81, 0x81, 0x01}; 12 | uint8_t T_LUT_KW_init[15] = {0x47, 0x47, 0x01, 0x47, 0x47, 0x01, 0x87, 0x87, 0x01, 0x87, 0x87, 0x01, 0x81, 0x81, 0x01}; 13 | uint8_t T_LUT_KK_update[15] = {0x87, 0x87, 0x01, 0x87, 0x87, 0x01, 0x87, 0x87, 0x01, 0x87, 0x87, 0x01, 0x81, 0x81, 0x01}; 14 | uint8_t T_LUT_KW_update[15] = {0x47, 0x47, 0x01, 0x47, 0x47, 0x01, 0x47, 0x47, 0x01, 0x47, 0x47, 0x01, 0x81, 0x81, 0x01}; 15 | 16 | //---------------------------------- 17 | // define segments 18 | // the data in the arrays consists of {byte, bit} pairs of each segment 19 | //---------------------------------- 20 | uint8_t top_left_1[2] = {12, 3}; 21 | uint8_t top_left[22] = {16, 7, 15, 4, 14, 1, 14, 7, 12, 5, 12, 4, 13, 3, 15, 7, 15, 6, 15, 5, 14, 0}; 22 | uint8_t top_middle[22] = {15, 0, 15, 1, 14, 6, 13, 0, 13, 5, 13, 4, 14, 5, 14, 4, 15, 3, 15, 2, 14, 3}; 23 | uint8_t top_right[22] = {13, 1, 13, 7, 12, 1, 12, 7, 11, 5, 11, 2, 12, 6, 12, 0, 13, 6, 13, 2, 12, 2}; 24 | uint8_t bottom_left[22] = {9, 1, 9, 7, 8, 5, 1, 1, 0, 3, 1, 4, 9, 4, 10, 0, 10, 6, 10, 3, 8, 2}; 25 | uint8_t bottom_right[22] = {7, 7, 6, 5, 2, 0, 2, 3, 0, 2, 1, 7, 2, 6, 7, 4, 7, 1, 8, 6, 6, 2}; 26 | uint8_t battery_low[2] = {16, 4}; 27 | uint8_t dashes[4] = {16, 6, 14, 2}; 28 | uint8_t face[14] = {3, 5, 5, 3, 5, 6, 4, 1, 4, 4, 4, 7, 3, 2}; 29 | uint8_t face_smile[8] = {4, 1, 5, 6, 3, 2, 4, 7}; 30 | uint8_t face_frown[6] = {5, 6, 4, 4, 4, 1}; 31 | uint8_t face_neutral[6] = {5, 6, 4, 1, 4, 7}; 32 | uint8_t sun[2] = {5, 0}; 33 | uint8_t fixed[2] = {16, 5}; 34 | uint8_t fixed_deg_C[2] = {14, 2}; 35 | uint8_t fixed_deg_F[2] = {16, 6}; 36 | 37 | // These values closely match times captured with logic analyser 38 | uint8_t delay_SPI_clock_pulse = 8; 39 | uint8_t delay_SPI_end_cycle = 12; 40 | 41 | /* 42 | 43 | Now define how each digit maps to the segments: 44 | 45 | 1 46 | 10 :----------- 47 | | | 48 | 9 | | 2 49 | | 11 | 50 | 8 :-----------: 3 51 | | | 52 | 7 | | 4 53 | | 5 | 54 | 6 :----------- 55 | 56 | */ 57 | 58 | int digits[16][11] = { 59 | {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 0}, // 0 60 | {2, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0}, // 1 61 | {1, 2, 3, 5, 6, 7, 8, 10, 11, 0, 0}, // 2 62 | {1, 2, 3, 4, 5, 6, 10, 11, 0, 0, 0}, // 3 63 | {2, 3, 4, 8, 9, 10, 11, 0, 0, 0, 0}, // 4 64 | {1, 3, 4, 5, 6, 8, 9, 10, 11, 0, 0}, // 5 65 | {1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0}, // 6 66 | {1, 2, 3, 4, 10, 0, 0, 0, 0, 0, 0}, // 7 67 | {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}, // 8 68 | {1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 0}, // 9 69 | {1, 2, 3, 4, 6, 7, 8, 9, 10, 11, 0}, // A 70 | {3, 4, 5, 6, 7, 8, 9, 10, 11, 0, 0}, // b 71 | {5, 6, 7, 8, 11, 0, 0, 0, 0, 0, 0}, // c 72 | {2, 3, 4, 5, 6, 7, 8, 11, 0, 0, 0}, // d 73 | {1, 5, 6, 7, 8, 9, 10, 11, 0, 0, 0}, // E 74 | {1, 6, 7, 8, 9, 10, 11, 0, 0, 0, 0} // F 75 | }; 76 | 77 | void XiaomiMiaoMiaoCeBT::init(uint8_t redraw) 78 | { 79 | // set the pin modes (note: no hardware SPI is used) 80 | pinMode(SPI_ENABLE, OUTPUT); 81 | pinMode(SPI_MOSI, OUTPUT); 82 | pinMode(SPI_CLOCK, OUTPUT); 83 | pinMode(IO_RST_N, OUTPUT); 84 | pinMode(EPD_TO_PC4, OUTPUT); 85 | pinMode(IO_BUSY_N, INPUT); 86 | 87 | // set all outputs to 1 88 | digitalWrite(SPI_ENABLE, HIGH); 89 | digitalWrite(SPI_MOSI, HIGH); 90 | digitalWrite(IO_RST_N, HIGH); 91 | digitalWrite(SPI_CLOCK, HIGH); 92 | digitalWrite(EPD_TO_PC4, HIGH); 93 | 94 | digitalWrite(SPI_MOSI, LOW); 95 | digitalWrite(SPI_CLOCK, LOW); 96 | 97 | if(redraw) 98 | { 99 | // pulse RST_N low for 110 microseconds 100 | digitalWrite(IO_RST_N, LOW); 101 | delayMicroseconds(110); 102 | digitalWrite(IO_RST_N, HIGH); 103 | 104 | // start an initialisation sequence (black - all 0xFF) 105 | send_sequence(T_LUTV_init, T_LUT_KK_init, T_LUT_KW_init, T_DTM_init, 1); 106 | // Original firmware pauses here for about 1500 ms 107 | // in addition to display refresh busy signal 108 | // Might be necessary in order to fully energise the black particles, 109 | // but even without this delay the display seems to be working fine 110 | delay(2000); 111 | 112 | // pulse RST_N low for 110 microseconds 113 | digitalWrite(IO_RST_N, LOW); 114 | delayMicroseconds(110); 115 | digitalWrite(IO_RST_N, HIGH); 116 | 117 | // start an initialisation sequence (white - all 0x00) 118 | send_sequence(T_LUTV_init, T_LUT_KW_update, T_LUT_KK_update, T_DTM2_init, 1); 119 | // Original firmware pauses here for about 100 ms 120 | // in addition to display refresh busy signal. 121 | // Might be dedicated to sensor data aquisition 122 | // delay(100); 123 | 124 | // pulse RST_N low for 110 microseconds 125 | digitalWrite(IO_RST_N, LOW); 126 | delayMicroseconds(110); 127 | digitalWrite(IO_RST_N, HIGH); 128 | } 129 | else 130 | { 131 | // Remove all black segments 132 | //send_sequence(T_LUTV_init, T_LUT_KW_update, T_LUT_KK_update, T_DTM2_init, 1); 133 | } 134 | 135 | } 136 | 137 | void XiaomiMiaoMiaoCeBT::send_sequence(uint8_t *dataV, uint8_t *dataKK, 138 | uint8_t *dataKW, uint8_t *data, 139 | uint8_t is_init) 140 | { 141 | /* 142 | if(is_init || transition) 143 | { 144 | // pulse RST_N low for 110 microseconds 145 | digitalWrite(IO_RST_N, LOW); 146 | delayMicroseconds(110); 147 | digitalWrite(IO_RST_N, HIGH); 148 | } 149 | */ 150 | // send Charge Pump ON command 151 | transmit(0, POWER_ON); 152 | 153 | // wait for the display to become ready to receive new 154 | // commands/data: when ready, the display sets IO_BUSY_N to 1 155 | while (digitalRead(IO_BUSY_N) == 0) 156 | delay(1); 157 | 158 | // Original firmware pauses here for about 100ms - this time is not required by the display, 159 | // but is probably dedicated to sensor data aquisition (temperature, humidity and battery). 160 | //delay(100); 161 | 162 | transmit(0, PANEL_SETTING); 163 | transmit(1, 0x0B); 164 | transmit(0, POWER_SETTING); 165 | transmit(1, 0x46); 166 | transmit(1, 0x46); 167 | transmit(0, POWER_OFF_SEQUENCE_SETTING); 168 | if (is_init) 169 | { 170 | transmit(1, 0x00); 171 | } 172 | else 173 | { 174 | transmit(1, 0x06); 175 | } 176 | 177 | transmit(0, PLL_CONTROL); // Frame Rate Control 178 | if (is_init) 179 | { 180 | transmit(1, 0x02); 181 | } 182 | else 183 | { 184 | transmit(1, 0x03); 185 | } 186 | 187 | // NOTE: Original firmware makes partial refresh on update, but not when initialising the screen. 188 | // Switching the background segment on/off requires full refresh, hence do not send partial refresh 189 | // command when switching between inverted / non-inverted screen mode. 190 | if ( !is_init && !transition ) 191 | { 192 | transmit(0, PARTIAL_DISPLAY_REFRESH); 193 | transmit(1, 0x00); 194 | transmit(1, 0x87); 195 | transmit(1, 0x01); 196 | } 197 | 198 | // send the e-paper voltage settings (waves) 199 | transmit(0, LUT_FOR_VCOM); 200 | for (int i = 0; i < 15; i++) 201 | transmit(1, dataV[i]); 202 | 203 | if(is_init) 204 | { 205 | transmit(0, LUT_CMD_0x23); 206 | for (int i = 0; i < 15; i++) 207 | transmit(1, dataKK[i]); 208 | 209 | transmit(0, LUT_CMD_0x26); 210 | for (int i = 0; i < 15; i++) 211 | transmit(1, dataKW[i]); 212 | } 213 | else 214 | { 215 | transmit(0, LUT_CMD_0x23); 216 | for (int i = 0; i < 15; i++) 217 | transmit(1, dataV[i]); 218 | 219 | transmit(0, LUT_CMD_0x24); 220 | for (int i = 0; i < 15; i++) 221 | transmit(1, dataKK[i]); 222 | 223 | transmit(0, LUT_CMD_0x25); 224 | for (int i = 0; i < 15; i++) 225 | transmit(1, dataKW[i]); 226 | 227 | transmit(0, LUT_CMD_0x26); 228 | for (int i = 0; i < 15; i++) 229 | transmit(1, dataV[i]); 230 | } 231 | 232 | // send the actual data 233 | transmit(0, DATA_START_TRANSMISSION_1); 234 | for (int i = 0; i < 18; i++) 235 | transmit(1, data[i]); 236 | 237 | while (digitalRead(IO_BUSY_N) == 0) 238 | delay(1); 239 | 240 | // Original firmware sends DATA_START_TRANSMISSION_2 command only 241 | // when performing full refresh 242 | if (is_init && !transition) 243 | { 244 | transmit(0, DATA_START_TRANSMISSION_2); 245 | for(int i = 0; i < 18; i++) 246 | transmit(1, data[i]); 247 | 248 | while (digitalRead(IO_BUSY_N) == 0) 249 | delay(1); 250 | } 251 | 252 | if (transition) 253 | { 254 | // NOTE: Original firmware doesn't perform this, but I found through experiments 255 | // that this is the way to clear the black background segment, without a full re-initialisation 256 | // (also partial refresh should not be sent in this case). 257 | { 258 | transmit(0, DATA_START_TRANSMISSION_2); 259 | for(int i = 0; i < 18; i++) 260 | transmit(1, ~data[i]); 261 | } 262 | } 263 | 264 | transmit(0, DISPLAY_REFRESH); 265 | 266 | // wait for the display to become ready to receive new 267 | // commands/data: when ready, the display sets IO_BUSY_N to 1 268 | while (digitalRead(IO_BUSY_N) == 0) 269 | delay(1); 270 | 271 | // send Charge Pump OFF command 272 | transmit(0, POWER_OFF); 273 | transmit(1, 0x03); 274 | 275 | // wait for the display to become ready to receive new 276 | // commands/data: when ready, the display sets IO_BUSY_N to 1 277 | while (digitalRead(IO_BUSY_N) == 0) 278 | delay(1); 279 | } 280 | 281 | void XiaomiMiaoMiaoCeBT::transmit(uint8_t cd, uint8_t data_to_send) 282 | { 283 | #if DEBUG_SERIAL 284 | if (cd == 0) 285 | { 286 | Serial.printf("[%lu] Sending COMMAND: 0x%02x\r\n", millis(), data_to_send); 287 | } 288 | else 289 | { 290 | Serial.printf("[%lu] Sending DATA: 0x%02x\r\n", millis(), data_to_send); 291 | } 292 | #endif 293 | 294 | // enable SPI 295 | digitalWrite(SPI_ENABLE, LOW); 296 | delayMicroseconds(delay_SPI_clock_pulse); 297 | 298 | // send the first bit, this indicates if the following is a command or data 299 | digitalWrite(SPI_CLOCK, LOW); 300 | if (cd != 0) 301 | digitalWrite(SPI_MOSI, HIGH); 302 | else 303 | digitalWrite(SPI_MOSI, LOW); 304 | delayMicroseconds(delay_SPI_clock_pulse * 2 + 1); 305 | digitalWrite(SPI_CLOCK, HIGH); 306 | delayMicroseconds(delay_SPI_clock_pulse); 307 | 308 | // send 8 bytes 309 | for (int i = 0; i < 8; i++) 310 | { 311 | // start the clock cycle 312 | digitalWrite(SPI_CLOCK, LOW); 313 | // set the MOSI according to the data 314 | if (data_to_send & 0x80) 315 | digitalWrite(SPI_MOSI, HIGH); 316 | else 317 | digitalWrite(SPI_MOSI, LOW); 318 | // prepare for the next bit 319 | data_to_send = (data_to_send << 1); 320 | delayMicroseconds(delay_SPI_clock_pulse * 2 + 1); 321 | // the data is read at rising clock (halfway the time MOSI is set) 322 | digitalWrite(SPI_CLOCK, HIGH); 323 | delayMicroseconds(delay_SPI_clock_pulse); 324 | } 325 | 326 | // finish by ending the clock cycle and disabling SPI 327 | digitalWrite(SPI_CLOCK, LOW); 328 | delayMicroseconds(delay_SPI_end_cycle); 329 | digitalWrite(SPI_ENABLE, HIGH); 330 | delayMicroseconds(delay_SPI_end_cycle); 331 | } 332 | 333 | void XiaomiMiaoMiaoCeBT::write_display() 334 | { 335 | // Send update waveforms 336 | send_sequence(T_LUTV_init, T_LUT_KK_update, T_LUT_KW_update, display_data, 0); 337 | } 338 | 339 | void XiaomiMiaoMiaoCeBT::write_display(uint8_t *data) 340 | { 341 | for (int i = 0; i < 18; i++) 342 | { 343 | display_data[i] = data[i]; 344 | } 345 | 346 | write_display(); 347 | } 348 | 349 | void XiaomiMiaoMiaoCeBT::set_digit(uint8_t digit, uint8_t where) 350 | { 351 | // check if the input is valid 352 | if ((digit >= 0) && (digit < 16) && (where >= TOP_LEFT) && (where <= BOTTOM_RIGHT)) 353 | { 354 | // set which segments are to be used 355 | uint8_t *segments; 356 | switch(where) 357 | { 358 | case TOP_LEFT: segments = top_left; break; 359 | case TOP_MIDDLE: segments = top_middle; break; 360 | case TOP_RIGHT: segments = top_right; break; 361 | case BOTTOM_LEFT: segments = bottom_left; break; 362 | case BOTTOM_RIGHT: segments = bottom_right; break; 363 | default: 364 | break; 365 | } 366 | 367 | // set the segments, there are up to 11 segments in a digit 368 | int segment_byte; 369 | int segment_bit; 370 | for (int i = 0; i < 11; i++) 371 | { 372 | // get the segment needed to display the digit 'digit', 373 | // this is stored in the array 'digits' 374 | int segment = digits[digit][i] - 1; 375 | // segment = -1 indicates that there are no more segments to display 376 | if (segment >= 0) 377 | { 378 | segment_byte = segments[2 * segment]; 379 | segment_bit = segments[1 + 2 * segment]; 380 | set_segment(segment_byte, segment_bit, 1); 381 | } 382 | else 383 | // there are no more segments to be displayed 384 | break; 385 | } 386 | } 387 | } 388 | 389 | void XiaomiMiaoMiaoCeBT::set_shape(uint8_t where) 390 | { 391 | int num_of_segments = 0; 392 | uint8_t *segments = NULL; 393 | 394 | // set the number of segments and which segments has to be displayed 395 | switch (where) 396 | { 397 | case TOP_LEFT_1: 398 | num_of_segments = sizeof(top_left_1) / 2; 399 | segments = top_left_1; 400 | break; 401 | case BATTERY_LOW: 402 | num_of_segments = sizeof(battery_low) / 2; 403 | segments = battery_low; 404 | break; 405 | case DASHES: 406 | num_of_segments = sizeof(dashes) / 2; 407 | segments = dashes; 408 | break; 409 | case FACE: 410 | num_of_segments = sizeof(face) / 2; 411 | segments = face; 412 | break; 413 | case FACE_SMILE: 414 | num_of_segments = sizeof(face_smile) / 2; 415 | segments = face_smile; 416 | break; 417 | case FACE_FROWN: 418 | num_of_segments = sizeof(face_frown) / 2; 419 | segments = face_frown; 420 | break; 421 | case FACE_NEUTRAL: 422 | num_of_segments = sizeof(face_neutral) / 2; 423 | segments = face_neutral; 424 | break; 425 | case SUN: 426 | num_of_segments = sizeof(sun) / 2; 427 | segments = sun; 428 | break; 429 | case FIXED: 430 | num_of_segments = sizeof(fixed) / 2; 431 | segments = fixed; 432 | break; 433 | case FIXED_DEG_C: 434 | num_of_segments = sizeof(fixed_deg_C) / 2; 435 | segments = fixed_deg_C; 436 | break; 437 | case FIXED_DEG_F: 438 | num_of_segments = sizeof(fixed_deg_F) / 2; 439 | segments = fixed_deg_F; 440 | break; 441 | default: 442 | return; 443 | } 444 | 445 | // set the segments 446 | for (uint8_t segment = 0; segment < num_of_segments; segment++) 447 | { 448 | uint8_t segment_byte = segments[2 * segment]; 449 | uint8_t segment_bit = segments[1 + 2 * segment]; 450 | set_segment(segment_byte, segment_bit, 1); 451 | } 452 | } 453 | 454 | void XiaomiMiaoMiaoCeBT::set_segment(uint8_t segment_byte, uint8_t segment_bit, 455 | uint8_t value) 456 | { 457 | // depending on whether the display is inverted and the desired value 458 | // the bit needs to be set or cleared 459 | if (((inverted == 0) && (value == 1)) || 460 | ((inverted == 1) && (value == 0))) 461 | // set the bit 462 | display_data[segment_byte] |= (1 << segment_bit); 463 | else 464 | // remove the bit 465 | display_data[segment_byte] &= ~(1 << segment_bit); 466 | } 467 | 468 | void XiaomiMiaoMiaoCeBT::start_new_screen(uint8_t _inverted) 469 | { 470 | // Set transition flag, indicating if the background 471 | // segment needs to be set or cleared 472 | if (_inverted != inverted) 473 | transition = 1; 474 | else 475 | transition = 0; 476 | 477 | if (_inverted == 1) 478 | inverted = 1; 479 | else 480 | inverted = 0; 481 | 482 | // prepare the data to be displayed, assume all segments are either on or off 483 | if (_inverted) 484 | { 485 | for (int i = 0; i < 18; i++) 486 | display_data[i] = 0xFF; 487 | 488 | // set the bit to switch the background to black, 489 | // use value=0 because of inversion 490 | set_segment(17, 7, 0); 491 | } 492 | else 493 | { 494 | for (int i = 0; i < 18; i++) 495 | display_data[i] = 0x00; 496 | } 497 | } 498 | -------------------------------------------------------------------------------- /XiaomiMiaoMiaoCeBT/XiaomiMiaoMiaoCeBT.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | // set to 0 if no debug output is required on the serial terminal 5 | #define DEBUG_SERIAL 0 6 | 7 | //---------------------------------- 8 | // define display commands 9 | //---------------------------------- 10 | #define PANEL_SETTING 0x00 11 | #define POWER_SETTING 0x01 12 | #define POWER_OFF 0x02 13 | #define POWER_OFF_SEQUENCE_SETTING 0x03 14 | #define POWER_ON 0x04 15 | #define DISPLAY_REFRESH 0x12 16 | #define PARTIAL_DISPLAY_REFRESH 0x15 17 | #define DATA_START_TRANSMISSION_1 0x18 18 | #define DATA_START_TRANSMISSION_2 0x1c 19 | #define LUT_FOR_VCOM 0x20 20 | #define LUT_CMD_0x23 0x23 21 | #define LUT_CMD_0x24 0x24 22 | #define LUT_CMD_0x25 0x25 23 | #define LUT_CMD_0x26 0x26 24 | #define PLL_CONTROL 0x30 25 | 26 | //---------------------------------- 27 | // define pins (made such that it can easily connect the ESP8266 Wemos d1 mini 28 | // to the pin-to-flatflex connector) 29 | //---------------------------------- 30 | #define SPI_ENABLE D7 31 | #define SPI_MOSI D6 32 | #define IO_RST_N D5 33 | #define SPI_CLOCK D1 34 | #define IO_BUSY_N D2 35 | // Unknown signal, but should be high 36 | #define EPD_TO_PC4 D8 37 | 38 | 39 | //---------------------------------- 40 | // define groups of segments into logical shapes 41 | //---------------------------------- 42 | #define TOP_LEFT_1 1 43 | #define TOP_LEFT 2 44 | #define TOP_MIDDLE 3 45 | #define TOP_RIGHT 4 46 | #define BOTTOM_LEFT 5 47 | #define BOTTOM_RIGHT 6 48 | #define BACKGROUND 7 49 | #define BATTERY_LOW 8 50 | #define DASHES 9 51 | #define FACE 10 52 | #define FACE_SMILE 11 53 | #define FACE_FROWN 12 54 | #define FACE_NEUTRAL 13 55 | #define SUN 14 56 | #define FIXED 15 57 | #define FIXED_DEG_C 16 58 | #define FIXED_DEG_F 17 59 | 60 | //---------------------------------- 61 | // define the class for driving the Xiaomi MiaoMiaoCe e-ink display 62 | //---------------------------------- 63 | class XiaomiMiaoMiaoCeBT 64 | { 65 | public: 66 | /** 67 | * Initialize the display 68 | * @param redraw should the screen should have an black-white redraw? 69 | * redraw = 0: no redraw 70 | * redraw != 0: do a redraw 71 | * normally redrawing is advisable (redraw != 0), but e.g. 72 | * when using deep-sleep you may want to initialize the 73 | * screen without the black-white transition 74 | */ 75 | void init(uint8_t redraw); 76 | 77 | /** display the data 78 | */ 79 | void write_display(); 80 | 81 | /** 82 | * Send ready data buffer to the display 83 | * @param data the data containing which segments are on or off 84 | */ 85 | void write_display(uint8_t *data); 86 | 87 | /** 88 | * Display a digit at a specific place 89 | * @param number The digit to be displayed [0 to 9, a - f] 90 | * @param where The location where to display the digit, there are 5 91 | * locations: [TOP_LEFT, TOP_MIDDLE, TOP_RIGHT, 92 | * BOTTOM_LEFT, BOTTOM_RIGHT] 93 | */ 94 | void set_digit(uint8_t digit, uint8_t where); 95 | 96 | /** 97 | * Display a defined shape 98 | * @param where Which shapes should be turned on, there are several 99 | * pre-defined shapes: [TOP_LEFT_1, BACKGROUND, 100 | * BATTERY_LOW, DASHES, FACE, FACE_SMILE, FACE_FROWN, 101 | * FACE_NEUTRAL, SUN, FIXED] 102 | * But if you want you can define your own, see the 103 | * #defines above and the variables in XiaomiMiaoMiaoCe.cpp 104 | */ 105 | void set_shape(uint8_t where); 106 | 107 | /** 108 | * Set a segment to a value 109 | * The e-ink display has 73 segments (the driver can handle 120) which are 110 | * distributed over 15 bytes with 8 bits each: 15*8 = 120 111 | * @param segment_byte The byte in which the segments is located 112 | * @param segment_bit The bit in the byte 113 | * @param value 0 = off, 1 = on 114 | */ 115 | void set_segment(uint8_t segment_byte, uint8_t segment_bit, uint8_t value); 116 | 117 | /** 118 | * Start building a new display 119 | * @param inverted 0 = inverted display, 1 = normal 120 | */ 121 | void start_new_screen(uint8_t inverted = 0); 122 | 123 | private: 124 | /** 125 | * Transmit data to the display via SPI 126 | * @param cd is it a command or data? 0 = command, 1 = data 127 | * @param data 1 byte of data 128 | */ 129 | void transmit(uint8_t cd, uint8_t data); 130 | 131 | /** 132 | * Send data to the display 133 | * The e-ink display has 'waveforms' of voltages to be applied to the 134 | * segments when changing from black to white or white to black. These 135 | * differ between initialising and updating the e-ink display 136 | * @param dataV one of the waveforms 137 | * @param dataKK one of the waveforms 138 | * @param dataKW one of the waveforms 139 | * @param data the data containing which segments are on or off 140 | * @param is_init is this an initialisation or an update 141 | */ 142 | void send_sequence(uint8_t *dataV, uint8_t *dataKK, uint8_t *dataKW, 143 | uint8_t *data, uint8_t is_init); 144 | 145 | // boolean to indicate if the screen is inverted or not 146 | uint8_t inverted = 0; 147 | uint8_t transition = 0; 148 | // The array in which the segments to be displayed are placed 149 | uint8_t display_data[18]; 150 | }; -------------------------------------------------------------------------------- /XiaomiMiaoMiaoCeBT/examples/black_and_white_switching/README.md: -------------------------------------------------------------------------------- 1 | Black and white screen 2 | ---------------------- 3 | 4 | This is a simple example of the screen changing from inverted to normal every three seconds. -------------------------------------------------------------------------------- /XiaomiMiaoMiaoCeBT/examples/black_and_white_switching/main.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | This code is for controlling the e-ink screen of the 4 | Xiaomi Mijia MiaoMiaoCe Bluetooth Thermometer and Hygrometer Sensor 5 | 6 | */ 7 | 8 | #include 9 | #include "XiaomiMiaoMiaoCeBT.h" 10 | 11 | XiaomiMiaoMiaoCeBT my_display; 12 | 13 | void setup() 14 | { 15 | // allow serial printing 16 | Serial.begin(115200); 17 | // initialize the e-ink display and do a redraw 18 | my_display.init(1); 19 | } 20 | 21 | int number = 0; 22 | 23 | void loop() 24 | { 25 | my_display.start_new_screen(number % 2); 26 | my_display.write_display(); 27 | delay(3000); 28 | number++; 29 | } -------------------------------------------------------------------------------- /XiaomiMiaoMiaoCeBT/examples/clock/README.md: -------------------------------------------------------------------------------- 1 | Clock 2 | ----- 3 | 4 | This example is a simple, but nifty, clock using the e-ink display of the Xiaomi Miao Miao CE. 5 | 6 | It uses an ESP8266 (Wemos D1 mini clone) to get the time via NTP and control the display. The clock has the following features: 7 | 8 | -   Deep sleep 9 | - Sleeps for 10 seconds (can be adjusted) 10 | -   Refreshes the screen every 10 seconds 11 | - Does NTP synchronization every hour (can be adjusted). The code is taken from [here](https://github.com/PaulStoffregen/Time) 12 | - Has adjustable time-zone (the setenv statement, see e.g. [here](https://remotemonitoringsystems.ca/time-zone-abbreviations.php)), and NTP server 13 | - Continuous in case of wifi connectivity problems 14 | - Gives an indication if the displayed time is valid or not (this is actually not completely true: if the power fails the e-ink will happily keep on displaying the last update) 15 | 16 | In order to run this example, you have to connect D0 to RST, but only *after* uploading the firmware to the Wemos D1 mini. If this connection is not made, the ESP8266 won't get out of deep sleep. 17 | -------------------------------------------------------------------------------- /XiaomiMiaoMiaoCeBT/examples/clock/main.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Clock for the ESP8266 with the MHO-C401 e-ink display 3 | */ 4 | 5 | #include 6 | #include 7 | #include 8 | #include "XiaomiMiaoMiaoCeBT.h" 9 | #include 10 | 11 | XiaomiMiaoMiaoCeBT my_display; 12 | 13 | // ============================================================= CLOCK SETTINGS 14 | // change the clock once every SECONDS_DEEP_SLEEP seconds 15 | #define SECONDS_DEEP_SLEEP 10 16 | // update via NTP once every DO_NTP_UPDATE seconds (typically an hour) 17 | #define DO_NTP_UPDATE 3600 18 | // NTP Server 19 | static const char ntpServerName[] = "uk.pool.ntp.org"; 20 | // Central European Time 21 | const int timeZone = 0; 22 | // function prototypes 23 | 24 | // ============================================================== WIFI SETTINGS 25 | // wifi settings 26 | const char ssid[] = "Your SSID"; // your network SSID (name) 27 | const char pass[] = "Your passwd"; // your network password 28 | // UDP settings 29 | WiFiUDP Udp; 30 | unsigned int localPort = 8888; // local port to listen for UDP packets 31 | 32 | // ============================= STRUCT TO SAVE VARIABLES TO SURVIVE DEEP-SLEEP 33 | struct 34 | { 35 | time_t unix_time; 36 | int secs_since_NTP = 0; // the number of seconds since the last NTP update 37 | int valid_time = 0; // indicate if the NTP update was unsuccessful 38 | } rtc_data; 39 | 40 | // ============================================== FUNCTION TO GET TIME FROM NTP 41 | // part of this comes from: https://github.com/PaulStoffregen/Time 42 | 43 | const int NTP_PACKET_SIZE = 48; // NTP time is in the first 48 bytes of message 44 | byte packetBuffer[NTP_PACKET_SIZE]; //buffer to hold incoming & outgoing packets 45 | 46 | // send an NTP request to the time server at the given address 47 | void sendNTPpacket(IPAddress &address) 48 | { 49 | // set all bytes in the buffer to 0 50 | memset(packetBuffer, 0, NTP_PACKET_SIZE); 51 | // Initialize values needed to form NTP request 52 | // (see URL above for details on the packets) 53 | packetBuffer[0] = 0b11100011; // LI, Version, Mode 54 | packetBuffer[1] = 0; // Stratum, or type of clock 55 | packetBuffer[2] = 6; // Polling Interval 56 | packetBuffer[3] = 0xEC; // Peer Clock Precision 57 | // 8 bytes of zero for Root Delay & Root Dispersion 58 | packetBuffer[12] = 49; 59 | packetBuffer[13] = 0x4E; 60 | packetBuffer[14] = 49; 61 | packetBuffer[15] = 52; 62 | // all NTP fields have been given values, now 63 | // you can send a packet requesting a timestamp: 64 | Udp.beginPacket(address, 123); //NTP requests are to port 123 65 | Udp.write(packetBuffer, NTP_PACKET_SIZE); 66 | Udp.endPacket(); 67 | } 68 | 69 | uint32_t getNtpTime() 70 | { 71 | IPAddress ntpServerIP; // NTP server's ip address 72 | 73 | while (Udp.parsePacket() > 0) 74 | ; // discard any previously received packets 75 | // get a random server from the pool 76 | WiFi.hostByName(ntpServerName, ntpServerIP); 77 | sendNTPpacket(ntpServerIP); 78 | uint32_t beginWait = millis(); 79 | while (millis() - beginWait < 1500) 80 | { 81 | int size = Udp.parsePacket(); 82 | if (size >= NTP_PACKET_SIZE) 83 | { 84 | Udp.read(packetBuffer, NTP_PACKET_SIZE); // read packet into the buffer 85 | unsigned long secsSince1900; 86 | // convert four bytes starting at location 40 to a long integer 87 | secsSince1900 = (unsigned long)packetBuffer[40] << 24; 88 | secsSince1900 |= (unsigned long)packetBuffer[41] << 16; 89 | secsSince1900 |= (unsigned long)packetBuffer[42] << 8; 90 | secsSince1900 |= (unsigned long)packetBuffer[43]; 91 | 92 | // 2208988800UL = the 70 years from 1900 to 1970 93 | rtc_data.unix_time = secsSince1900 - 2208988800UL; 94 | 95 | return 1; // valid response 96 | } 97 | } 98 | return 0; // return 0 if unable to get the time 99 | } 100 | 101 | void do_NTP_update() 102 | { // Get the time via NTP 103 | 104 | // start wifi 105 | WiFi.begin(ssid, pass); 106 | // check if connected 107 | int num_of_tries = 0; 108 | while (WiFi.status() != WL_CONNECTED) 109 | { 110 | delay(500); 111 | num_of_tries++; 112 | if (num_of_tries > 100) // allow 100 retries 113 | { 114 | rtc_data.valid_time = 0; 115 | // failing costs time -> add that to the seconds 116 | rtc_data.unix_time += 50; // 500ms delay * 100 tries = 50 seconds 117 | return; 118 | } 119 | } 120 | 121 | // start UDP 122 | Udp.begin(localPort); 123 | 124 | // get the time in seconds since 1970 125 | uint32_t my_time = getNtpTime(); 126 | if (my_time == 1) 127 | { // there was a valid response from the NTP server 128 | rtc_data.valid_time = 1; 129 | rtc_data.secs_since_NTP = 0; 130 | } 131 | else 132 | { // the NTP server did not respond 133 | rtc_data.valid_time = 0; 134 | } 135 | } 136 | 137 | // =========================================================== DISPLAY SETTINGS 138 | 139 | void update_display() 140 | { // display the time 141 | tm *normal_time; 142 | normal_time = localtime(&rtc_data.unix_time); 143 | 144 | // start building a new screen (0 - normal, 1 - inverted) 145 | my_display.start_new_screen(0); 146 | 147 | 148 | // the 10th of the seconds on the top row 149 | my_display.set_digit(normal_time->tm_sec / 10, TOP_RIGHT); 150 | // the minutes on the bottom row 151 | my_display.set_digit(normal_time->tm_min % 10, BOTTOM_RIGHT); 152 | my_display.set_digit(normal_time->tm_min / 10, BOTTOM_LEFT); 153 | // the hours on the top row 154 | my_display.set_digit(normal_time->tm_hour % 10, TOP_MIDDLE); 155 | my_display.set_digit(normal_time->tm_hour / 10, TOP_LEFT); 156 | // the smiley/frowny indicates if the time is valid 157 | // i.e. the latest NTP update was successful 158 | if (rtc_data.valid_time == 0) 159 | my_display.set_shape(FACE_FROWN); 160 | else 161 | my_display.set_shape(FACE_SMILE); 162 | // write the screen to the display 163 | my_display.write_display(); 164 | } 165 | 166 | // ============================================================= SETUP AND LOOP 167 | 168 | void setup() 169 | { // get the reason for the wake-up 170 | String reset_reason = ESP.getResetReason(); 171 | if (reset_reason != "Deep-Sleep Wake") 172 | { // if it is not a deep sleep wake, then initialize 173 | 174 | // initialize the e-ink display and do a redraw 175 | my_display.init(1); 176 | // init time variables 177 | rtc_data.unix_time = 0; 178 | rtc_data.secs_since_NTP = DO_NTP_UPDATE; 179 | rtc_data.valid_time = 0; 180 | // get the current time via NTP 181 | do_NTP_update(); 182 | // write variables that need to survive deep-sleep to rtc memory 183 | ESP.rtcUserMemoryWrite(0, (uint32_t *)&rtc_data, sizeof(rtc_data)); 184 | } 185 | else 186 | { // it is a wake-up from deep-sleep 187 | 188 | // read variables from rtc memory 189 | ESP.rtcUserMemoryRead(0, (uint32_t *)&rtc_data, sizeof(rtc_data)); 190 | 191 | // is it time for a NTP update? If not: continue with current time values 192 | rtc_data.unix_time += SECONDS_DEEP_SLEEP; 193 | rtc_data.secs_since_NTP += SECONDS_DEEP_SLEEP; 194 | if (rtc_data.secs_since_NTP > DO_NTP_UPDATE) 195 | // get the current time via NTP 196 | do_NTP_update(); 197 | 198 | // initialize the e-ink display without redraw 199 | my_display.init(0); 200 | // write variables that need to survive deep-sleep to rtc memory 201 | ESP.rtcUserMemoryWrite(0, (uint32_t *)&rtc_data, sizeof(rtc_data)); 202 | } 203 | 204 | // Set timezone to British DST 205 | setenv("TZ","GMTGMT-1,M3.4.0/01,M10.4.0/02",1); 206 | tzset(); 207 | 208 | // display the current time (hours, minutes) on the display 209 | update_display(); 210 | 211 | // calculate the change in seconds to make the deep-sleep 212 | // wake up on a whole minute 213 | tm *normal_time; 214 | normal_time = localtime(&rtc_data.unix_time); 215 | 216 | uint32_t seconds_to_add = SECONDS_DEEP_SLEEP - normal_time->tm_sec; 217 | if ((seconds_to_add < 30) and (seconds_to_add > 1)) 218 | { 219 | normal_time->tm_sec = 0; 220 | normal_time->tm_min += 1; 221 | rtc_data.unix_time = mktime(normal_time); 222 | // write variables that need to survive deep-sleep to rtc memory 223 | ESP.rtcUserMemoryWrite(0, (uint32_t *)&rtc_data, sizeof(rtc_data)); 224 | // go to sleep 225 | ESP.deepSleep((SECONDS_DEEP_SLEEP + seconds_to_add) * 1e6, WAKE_RF_DEFAULT); 226 | } 227 | else if ((seconds_to_add < 59) and (seconds_to_add > 1)) 228 | { 229 | normal_time->tm_sec = 0; 230 | rtc_data.unix_time = mktime(normal_time); 231 | 232 | // write variables that need to survive deep-sleep to rtc memory 233 | ESP.rtcUserMemoryWrite(0, (uint32_t *)&rtc_data, sizeof(rtc_data)); 234 | // go to sleep 235 | ESP.deepSleep((seconds_to_add)*1e6, WAKE_RF_DEFAULT); 236 | } 237 | else 238 | // go to sleep 239 | // Note: indicate that the radio should be started at wake-up 240 | // If WAKE_RF_DISABLED is used, you can never start the radio again 241 | // ... weird! 242 | ESP.deepSleep(SECONDS_DEEP_SLEEP * 1e6, WAKE_RF_DEFAULT); 243 | } 244 | 245 | // not needed 246 | void loop() 247 | { 248 | } -------------------------------------------------------------------------------- /XiaomiMiaoMiaoCeBT/examples/find_segment_positions/README.md: -------------------------------------------------------------------------------- 1 | Find segment positions 2 | ---------------------- 3 | 4 | This is an example cycling through all bits (0-7) and bytes (0-17) of the display in order to determine their mapping. 5 | -------------------------------------------------------------------------------- /XiaomiMiaoMiaoCeBT/examples/find_segment_positions/main.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | This code is for controlling the e-ink screen of the 4 | Xiaomi Mijia MiaoMiaoCe Bluetooth Thermometer and Hygrometer Sensor 5 | 6 | */ 7 | 8 | #include "XiaomiMiaoMiaoCeBT.h" 9 | 10 | uint8_t all_segments[18] = {0x0c, 0x92, 0x49, 0x24, 0x92, 0x49, 0x24, 0x92, 0x64, 0x92, 0x49, 0x24, 0xff, 0xff, 0xff, 0xff, 0xf0, 0xff}; 11 | 12 | 13 | XiaomiMiaoMiaoCeBT my_display; 14 | 15 | void setup() 16 | { 17 | // allow serial printing 18 | Serial.begin(115200); 19 | 20 | // Use built-in LED as kind of watchdog indicator... 21 | pinMode(LED_BUILTIN, OUTPUT); 22 | 23 | #if DEBUG_SERIAL 24 | Serial.print("Initialising display, please wait...\r\n"); 25 | #endif 26 | 27 | // initialize the e-ink display 28 | my_display.init(1); 29 | 30 | // show all segments at once. 31 | // Original firmware does this on start-up 32 | my_display.write_display(all_segments); 33 | 34 | #if DEBUG_SERIAL 35 | Serial.print("Display initialised. Continuing to the loop() function!\r\n\r\n"); 36 | #endif 37 | } 38 | 39 | // the number to be displayed 40 | int number = 0; 41 | int led_state = LOW; 42 | 43 | int bytes = 0; 44 | int bits = 0; 45 | bool is_screen_inverted = false; 46 | 47 | void loop() 48 | { 49 | led_state = ~led_state; 50 | digitalWrite(LED_BUILTIN, led_state); 51 | 52 | Serial.printf("Setting segment #%d (%d, %d) to 1\r\n", number, bytes, bits); 53 | 54 | my_display.start_new_screen(is_screen_inverted); 55 | my_display.set_segment(bytes, bits, 1); 56 | 57 | // display the segment 58 | my_display.write_display(); 59 | // increase the number of the segment 60 | number++; 61 | 62 | // Cycle through all bytes and bits 63 | bits++; 64 | if (bits > 7) 65 | { 66 | bytes++; 67 | bits = 0; 68 | } 69 | if (bytes > 17) 70 | { 71 | bytes = 0; 72 | number = 0; 73 | 74 | // Alternate between inverted / non-inverted screen 75 | is_screen_inverted = !is_screen_inverted; 76 | 77 | // Make a full display refresh 78 | my_display.init(1); 79 | } 80 | 81 | // wait some time 3 seconds) 82 | delay(3000); 83 | } -------------------------------------------------------------------------------- /XiaomiMiaoMiaoCeBT/examples/numbers_and_shapes/README.md: -------------------------------------------------------------------------------- 1 | Numbers and shapes 2 | ------------------ 3 | 4 | This is an example of displaying numbers and shapes on the display, changing every 3 seconds. 5 | -------------------------------------------------------------------------------- /XiaomiMiaoMiaoCeBT/examples/numbers_and_shapes/main.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | This code is for controlling the e-ink screen of the 4 | Xiaomi Mijia MiaoMiaoCe Bluetooth Thermometer and Hygrometer Sensor 5 | 6 | */ 7 | 8 | #include "XiaomiMiaoMiaoCeBT.h" 9 | 10 | uint8_t all_segments[18] = {0x0c, 0x92, 0x49, 0x24, 0x92, 0x49, 0x24, 0x92, 0x64, 0x92, 0x49, 0x24, 0xff, 0xff, 0xff, 0xff, 0xf0, 0xff}; 11 | 12 | XiaomiMiaoMiaoCeBT my_display; 13 | 14 | void setup() 15 | { 16 | // allow serial printing 17 | Serial.begin(115200); 18 | 19 | // Use built-in LED as kind of watchdog indicator... 20 | pinMode(LED_BUILTIN, OUTPUT); 21 | 22 | #if DEBUG_SERIAL 23 | Serial.print("Initialising display, please wait...\r\n"); 24 | #endif 25 | 26 | // initialize the e-ink display 27 | my_display.init(1); 28 | 29 | // show all segments at once. 30 | // Original firmware does this on start-up 31 | my_display.write_display(all_segments); 32 | //delay(100); 33 | 34 | #if DEBUG_SERIAL 35 | Serial.print("Display initialised. Continuing to the loop() function!\r\n\r\n"); 36 | #endif 37 | } 38 | 39 | // the number to be displayed 40 | int number = 0; 41 | int led_state = LOW; 42 | 43 | int bytes = 0; 44 | int bits = 0; 45 | bool is_screen_inverted = false; 46 | 47 | void loop() 48 | { 49 | led_state = ~led_state; 50 | digitalWrite(LED_BUILTIN, led_state); 51 | 52 | // start drawing a new screen 53 | my_display.start_new_screen(is_screen_inverted); 54 | 55 | // display the same Hexadecimal digit on top 3 positions 56 | my_display.set_digit(number % 16, TOP_LEFT); 57 | my_display.set_digit(number % 16, TOP_MIDDLE); 58 | my_display.set_digit(number % 16, TOP_RIGHT); 59 | // display the same Decimal digit on bottom 2 positions 60 | my_display.set_digit(number % 10, BOTTOM_LEFT); 61 | my_display.set_digit(number % 10, BOTTOM_RIGHT); 62 | 63 | // display pre-defined shapes one at a time 64 | int remainder = number % 11; 65 | switch (remainder) 66 | { 67 | case 0: my_display.set_shape(TOP_LEFT_1); break; 68 | case 1: my_display.set_shape(BATTERY_LOW); break; 69 | case 2: my_display.set_shape(DASHES); break; 70 | case 3: my_display.set_shape(FACE); break; 71 | case 4: my_display.set_shape(FACE_SMILE); break; 72 | case 5: my_display.set_shape(FACE_FROWN); break; 73 | case 6: my_display.set_shape(FACE_NEUTRAL); break; 74 | case 7: my_display.set_shape(SUN); break; 75 | case 8: my_display.set_shape(FIXED); break; 76 | case 9: my_display.set_shape(FIXED_DEG_C); break; 77 | case 10: my_display.set_shape(FIXED_DEG_F); break; 78 | } 79 | 80 | my_display.write_display(); 81 | number++; 82 | 83 | // Re-initialise display every 16 cycles 84 | // and alternate between inverted / non-inverted display 85 | if ((number % 16) == 0) 86 | { 87 | is_screen_inverted = !is_screen_inverted; 88 | // NOTE: If screen is not inverted from time to time, 89 | // white segments become visible 90 | // To avoid this, re-initialise the display with redraw 91 | // after 15-20 partial update cycles: 92 | //my_display.init(1); 93 | } 94 | 95 | // wait some time (5 seconds) 96 | delay(5000); 97 | } -------------------------------------------------------------------------------- /captures/ATC_firmware/MHO-C401_ATC_PowerOn_Init.logicdata: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/znanev/MHO-C401/957229ad702697386f07ff0ae5ad4df1628a0348/captures/ATC_firmware/MHO-C401_ATC_PowerOn_Init.logicdata -------------------------------------------------------------------------------- /captures/ATC_firmware/MHO_C401_ATC_PartialUpdates_within_one_minute.logicdata: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/znanev/MHO-C401/957229ad702697386f07ff0ae5ad4df1628a0348/captures/ATC_firmware/MHO_C401_ATC_PartialUpdates_within_one_minute.logicdata -------------------------------------------------------------------------------- /captures/SPI-captures-analysis.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/znanev/MHO-C401/957229ad702697386f07ff0ae5ad4df1628a0348/captures/SPI-captures-analysis.xlsx -------------------------------------------------------------------------------- /captures/original_firmware/MHO-C401_PowerOn_Init.logicdata: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/znanev/MHO-C401/957229ad702697386f07ff0ae5ad4df1628a0348/captures/original_firmware/MHO-C401_PowerOn_Init.logicdata -------------------------------------------------------------------------------- /captures/original_firmware/MHO_C401_PartialUpdates_within_one_minute.logicdata: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/znanev/MHO-C401/957229ad702697386f07ff0ae5ad4df1628a0348/captures/original_firmware/MHO_C401_PartialUpdates_within_one_minute.logicdata -------------------------------------------------------------------------------- /media/MHO-C401-epd-screen-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/znanev/MHO-C401/957229ad702697386f07ff0ae5ad4df1628a0348/media/MHO-C401-epd-screen-demo.gif -------------------------------------------------------------------------------- /media/PCB.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/znanev/MHO-C401/957229ad702697386f07ff0ae5ad4df1628a0348/media/PCB.jpg -------------------------------------------------------------------------------- /media/all_segments.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/znanev/MHO-C401/957229ad702697386f07ff0ae5ad4df1628a0348/media/all_segments.png -------------------------------------------------------------------------------- /media/breakout_board.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/znanev/MHO-C401/957229ad702697386f07ff0ae5ad4df1628a0348/media/breakout_board.jpg -------------------------------------------------------------------------------- /media/digit_segments.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/znanev/MHO-C401/957229ad702697386f07ff0ae5ad4df1628a0348/media/digit_segments.png --------------------------------------------------------------------------------