Twitter icon
Facebook icon
LinkedIn icon
Google icon
Reddit icon
StumbleUpon icon icon

Prototype a Smart Curtain Robot-Tuya Developer

Added to IoTplaybook or last updated on: 11/20/2021
Prototype a Smart Curtain Robot-Tuya Developer



A smart curtain robot can make your current curtains motorized and smart with easy installation. It offers the ultimate smart control with a mobile phone or voice assistant. With a shout or a tap, your curtains can give you privacy on-demand and open or close right at sunrise or sunset with preset automations. This little robot will not only make your curtain smart but will surely add some convenience to your day-to-day living.

Things used in this project

Hardware components

Tuya's Bluetooth LE module
× 1  
Lithium battery
× 1  
Battery management chip
× 1  
LDO regulator
× 1  
Ambient light sensor
× 1  
Linear accelerometer
× 1  

Software apps and online services

Sample code in GitHub


  • Automatically open or close the curtain based on the preset threshold of light intensity.
  • View the light intensity on the mobile app.
  • Set the accurate opening/closing position on the mobile app.
  • Manually pulling the curtain can trigger the motor to operate automatically.


Battery management unit

Power supply

Use a rechargeable 4000 mAh lithium battery pack (3.7V) to power the curtain motor without trailing wires or cords.

Battery management chip

Use a linear battery management chip XT2052 from Natlinear. This chip has the following features.

  • The component includes an internal power transistor and does not need an external current sense resistor and blocking diode.
  • Programmable charge current is up to 1A.
  • Constant-current and constant-voltage operation with thermal regulation maximizes charge rate without risk of overheating.
  • The chip automatically terminates the charge cycle when the charge current drops to 1/10th the programmed value after the final float voltage is reached.
  • 40 μA current is supplied when the IC is shut down.
  • Using an over-voltage protection circuit to prevent the device from damage when the supply voltage is higher than the OVP threshold of 6.8V.

Use an LDO regulator TLV700F33 from 3PEAK to provide 3.3V voltage. This LDO has the following features.

  • Maximum output current: 300 mA
  • Low dropout voltage: 200 mV at 300 mA
  • Low quiescent current: 50 μA

Circuit diagram

  • The maximum charge current is 1A.
  • The two LEDs indicate the progress of charging. Steady red light means that the battery is charging. Steady green light means that the battery is fully charged.
  • VBat is the voltage of the battery and is used to power the motor. The LDO steps down the power source to 3.3V DC to supply the Bluetooth LE module and sensor.

Ambient light sensor

The photoresistor, photodiode, and ambient light sensor are the common components used to sense light intensity. Both the photoresistor and photodiode are fundamentally analog components. The output analog signals must first be converted into a digital value before a microcontroller can use it. The ambient light sensor OPT3004 can directly output the measured lux value. The ambient light sensor OPT3004 trumps the other two components in the following aspects.

  • Low operating current: 1.8 µA (Typical)
  • Measurements: 0.01 lux to 83k lux
  • Direct output of lux value without conversion.

Circuit diagram

Linear accelerometer

To detect the manual pulling action, we use a three-axis linear accelerometer LIS2DW12 from STMicroelectronics to achieve portrait/landscape orientation detections. It uses less than 1 µA in low-power mode.

Circuit diagram

I2C is chosen as the digital output interface. It can use the CS pin to select I2C or SPI mode.

Since the accelerometer LIS2DW12 and the ambient light sensor OPT3004 use a different address, these two sensors can share one I2C bus interface.

Motor driver

The robot is driven by the friction between the pulleys and rail. This requires high torque output at lower shaft speeds to push up the curtain and the robot. Therefore, we choose a geared motor.A geared motor is a combination of a gear system or gearbox and an electric motor. Gears transform shaft speed and torque at specific ratios, with minimum efficiency losses.

We use a 1-channel H-bridge driver HT7K1201 from Holtek Semiconductor. This driver has the following features.

  • Wide VDD input voltage range of 1.8V to 6.0V.
  • Maximum 1.3A motor peak current.
  • Sleep current of less than 0.1 μA.
  • Low MOSFET on-resistance of 0.5Ω (HS+LS).
  • Four operation modes: forward, reverse, brake, and standby.
  • VDD under-voltage lock-out (1.5V). Over-current protection (1.3A).
  • Output short circuit protection (1.9A). Thermal shutdown protection.

Microcontroller unit

Tuya’s Bluetooth LE module works as the microcontroller. We develop with this module to achieve Bluetooth connectivity, motor control, and data collection.In the circuit, an LED and a reset button are added to indicate network status and reset the module.

Schematic diagram and PCB



Step 2: Create a product

1. Log in to the Tuya IoT Platform.

2. Click Create.

3. On the Standard Category tab, select Small Home Appliances > Motor > Curtain.

4. On the Custom Solution tab, select Curtain.

5. Complete the product details and select Bluetooth as the protocol.

6. Click Create.

7. In the second step of the Device Panel tab, select the desired panel.

8. In the third step of Hardware Development, select Tuya Standard Module SDK.

9. Click Get 10 Free Licenses on the right side of the screen. You will get the UUID, key, and MAC address used in coding.

Step 3: Get the SDK and set up IDE

  • GitHub repoClone this repo tuya_ble_sdk_Demo_Project_tlsr8253 to your local computer and check out the
  • Set up IDEThe chip on the module is TLSR825x so we use Telink IDE for development. Go IDE for TLSR8 Chips and download the IDE and check out the project import guide.

After you have the IDE installed, import your project.

  • Develop with project

1. Edit the product ID.

2. Edit the auth_keydevice_id, and mac.


4. Compile code.

1. Select 8258 as the chip type and EVK as the download mode. Click File and select the bin file to be downloaded into the board, which is located in tuya_ble_sdk_Demo_Project_tlsr8253\telink_kite_ble_sdk_v3.4.0_20190816\ble_sdk_multimode\8258_module\8258_module.bin.

2. After downloading, click Reset to run it.

3. We use Telink’s writer. Connect the SWM header on the board to the SWM header on the writer.


  • When the GPIO reads high, it returns a value greater than 1, which might be 1, 2, or 128.
  • The pin of log printing defaults to TL_C2, with a baud rate of 230400. Since we do not have enough I/Os, we change the pin of log printing to TL_D3. You can compile code after the editing. The configuration file is located in tuya_ble_sdk_Demo_Project_tlsr8253\telink_kite_ble_sdk_v3.4.0_20190816\ble_sdk_multimode\vendor\8258_module\app_config.n

In line 47:


Changed to:


Step 4: Software design





Required features

Implementation: normal control and percentage control

To implement both the control methods, we must get the length of the curtain. Since the rotational speed of the motor is constant, we can count how long it takes for the motor to completely close the curtain from 0% to 100% and then calculate the length of the curtain. To pull the robot to the set position, calculate the travel time based on the starting and stopping position of the motor.

After receiving the command of the percentage control from the mobile app, the application calls void curtain_percent_control(unsigned char current_position, unsigned char target_position) to pull the robot to the corresponding direction and save the required travel time to the global variable that the task termination function will use. void curtain_percent_control_stop_task(void) will run until the motor travel stops. Then, this function will save the current position of the motor to the flash memory and report the motor status to the cloud accordingly.

Sample code for percentage control

void curtain_percent_control(unsigned char current_position, unsigned char target_position)
unsigned long total_time;
if ((current_position == target_position) || \
(current_position < 0) || (current_position > 100) || \
(target_position < 0) || (target_position > 100)) {
TUYA_APP_LOG_ERROR("input error");
*curtain_robot.dp_percent_state.dp_data = current_position;
if (curtain_robot.motor_run_mode == RUN_MODE_IDLE) {
curtain_robot.motor_run_mode = RUN_MODE_PERCENT_CONTROL;
} else {
TUYA_APP_LOG_ERROR("motor_run_mode != RUN_MODE_IDLE");
*curtain_robot.dp_percent_state.dp_data = current_position;
total_time = curtain_robot.dp_time_total.dp_data[0];
total_time = (total_time << 8) | curtain_robot.dp_time_total.dp_data[1];
if (total_time == 0) {
*curtain_robot.dp_percent_state.dp_data = current_position;
if (current_position < target_position) {
curtain_robot.run_end_time = ((target_position - current_position) * (total_time * 10)); //*10=*1000/100,ms->us
curtain_robot.run_start_time = clock_time();
} else {
curtain_robot.run_end_time = (current_position - target_position) * (total_time * 10);
curtain_robot.run_start_time = clock_time();
/* percent control state target position */
percent_target_value = target_position;
*curtain_robot.dp_percent_control.dp_data = target_position;
/* percent control */
dp_update(curtain_robot.dp_percent_control.dp_id,	\
curtain_robot.dp_percent_control.dp_type,	\
curtain_robot.dp_percent_control.dp_data,	\
void curtain_percent_control_stop_task(void)
if (((curtain_robot.motor_run_mode == RUN_MODE_PERCENT_CONTROL)) && \
(clock_time_exceed(curtain_robot.run_start_time, curtain_robot.run_end_time))) {
curtain_robot.motor_run_mode = RUN_MODE_IDLE;
/* percent state */
*curtain_robot.dp_percent_state.dp_data = percent_target_value;
dp_update(curtain_robot.dp_percent_state.dp_id,	\
curtain_robot.dp_percent_state.dp_type,	\
curtain_robot.dp_percent_state.dp_data,	\

Implementation: auto measurement of curtain length

To get the length of the curtain automatically, we need to measure the length of the curtain track. The key step is to determine whether the robot arrives at the travel destination. All the motor-dependent features require the travel time of the motor.

We provide two solutions here.

  • Given that the voltage of the MO_CUR pin is higher in motor stalling than that in normal operation, we can determine whether the robot arrives at the travel destination by the voltage change due to stalling.

  • Alternatively, we can use the accelerometer LIS2DW12 to determine the completion of motor travel. According to the placement of LIS2DW12, the acceleration values measured from the X-axis can be a determinant of the motor status. The following figure shows the data measured from the X-axis when the robot runs. The signal is flat when the robot is still and starts fluctuating after the robot moves. The signal surges on a collision. When the robot arrives at the destination, the signal is higher than that in the stationary state, with smaller fluctuation.

The first solution provides an at-a-glance way to determine the completion of motor travel. The accelerometer can help you check the current motor status. Choose a solution as needed.

Implementation: motion detection

The values measured when you starting pulling the drapes correlates with the curtain moving direction. We can use the values at the starting point to determine the pulling direction.

Sample code

short x_data_buf[100] = {0};
unsigned int clean_x_buf_count = 0;
unsigned char x_data_index = 0;
void auto_power_task(void)
short x_axis_data = 0;
unsigned char i = 0;
unsigned char open_count = 0, close_count = 0;
if (*(curtain_robot.dp_auto_power.dp_data) == TRUE) {
if (clock_time_exceed(curtain_robot.get_lis2dw12_data_time, 10 * 1000)) {
curtain_robot.get_lis2dw12_data_time = clock_time();
if (curtain_robot.motor_run_mode != RUN_MODE_IDLE) {
x_axis_data = get_lis2dw12_x_value();
if (x_axis_data > 100 || x_axis_data < -100) {
x_data_buf[x_data_index] = x_axis_data;
clean_x_buf_count = 0;
} else {
if ((clean_x_buf_count > 20) && (x_data_index >= 20)) { //At this point it has levelled off
for (i=0; i < 7; i++) {
if (x_data_buf[i] >= 0) {
} else {
if (*curtain_robot.dp_percent_state.dp_data == 0) {
curtain_robot.auto_power_state = AUTO_POWER_CLOSE;
} else if (*curtain_robot.dp_percent_state.dp_data == 100) {
curtain_robot.auto_power_state = AUTO_POWER_OPEN;
} else if (open_count < close_count) {
curtain_robot.auto_power_state = AUTO_POWER_CLOSE;
} else {
curtain_robot.auto_power_state = AUTO_POWER_OPEN;
//clean flag
clean_x_buf_count= 0;
if (curtain_robot.auto_power_state != AUTO_POWER_IDLE && clean_x_buf_count>= 100) {
if (curtain_robot.auto_power_state == AUTO_POWER_OPEN) {
curtain_percent_control(*curtain_robot.dp_percent_state.dp_data, 0);
} else if (curtain_robot.auto_power_state == AUTO_POWER_CLOSE) {
curtain_percent_control(*curtain_robot.dp_percent_state.dp_data, 100);
clean_x_buf_count = 0;
if (clean_x_buf_count > 500) {
clean_x_buf_count = 0;

Implementation: light intensity display

The ambient light sensor OPT3004 can communicate with the module through the I2C protocol. We can read the result register to get the current lux value.

Sample code

#define I2C_CLK_SPEED 200000
short lsb_size_tab[] = {1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048};
unsigned char opt3004_init(void)
unsigned char device_id_data_buf[2] = {0};
unsigned char manufacturer_id_data_buf[2] = {0};
unsigned char write_data[2] = {0xcc, 0x10};
unsigned long delay_time = 0;
i2c_master_init(OPT3004_I2C_ADDR_W, (unsigned char)(CLOCK_SYS_CLOCK_HZ / (4 * I2C_CLK_SPEED)));
i2c_write_series(OPT3004_CONFIG_REGISTER_ADDR, 1, write_data, 2);
i2c_master_init(OPT3004_I2C_ADDR_R, (unsigned char)(CLOCK_SYS_CLOCK_HZ / (4 * I2C_CLK_SPEED)));
i2c_read_series(OPT3004_DEVICE_ID_REGISTER_ADDR, 1, device_id_data_buf, 2);
delay_time = clock_time();
while (!clock_time_exceed(delay_time, 100)); //100us delay
i2c_read_series(OPT3004_MANUFACTURER_ID_REGISTER_ADDR, 1, manufacturer_id_data_buf, 2);
if ((((device_id_data_buf[0]<<8) + device_id_data_buf[1]) != DEVICE_ID) || \
(((manufacturer_id_data_buf[0]<<8) + manufacturer_id_data_buf[1]) != MANUFACTURER_ID)) {
return 0;
return 1;
short get_opt3004_value(void)
short ret_value = -1;
int result_value = 0;
short result_data_e = 0, result_data_r = 0;
unsigned char opt3004_cfg_data[2] = {0};
unsigned char opt3004_result_data[2] = {0};
i2c_master_init(OPT3004_I2C_ADDR_R, (unsigned char)(CLOCK_SYS_CLOCK_HZ / (4 * I2C_CLK_SPEED)));
i2c_read_series(OPT3004_CONFIG_REGISTER_ADDR, 1, opt3004_cfg_data, 2);
if (opt3004_cfg_data[1]&0x80) {
i2c_read_series(OPT3004_RESULT_REGISTER_ADDR, 1, opt3004_result_data, 2);
result_value = (opt3004_result_data[0]<<8) + opt3004_result_data[1];
result_data_e = ((result_value & 0xF000) >> 12);
result_data_r = (result_value & 0x0FFF);
ret_value = lsb_size_tab[result_data_e] * result_data_r / 100;
TUYA_APP_LOG_DEBUG("ret_value:%d", ret_value);
return ret_value;

Control panel

This all-in-one panel provides the user interface for controlling the robot on a mobile phone.


So far, a basic smart curtain robot prototype is made. We provide some improvements that you can make to your project.


Code Snippet #5

Code Snippet #6

Code Snippet #7

Code Snippet #8

Code Snippet #9

Code Snippet #10



Sandwich IoT

Sandwich IoT

This content is provided by our content partner, an Avnet developer community for learning, programming, and building hardware. Visit them online for more great content like this.

This article was originally published at It was added to IoTplaybook or last modified on 11/20/2021.