Community project

Roku Remote With Battery Gauge

Photo of Roku Remote With Battery Gauge
Generated with AI

Martin Herrera

Published November 11, 2026

This project transforms an ESP32 into a WiFi-enabled Roku remote with a real-time battery gauge. The device combines a rechargeable LiPo battery with a TP4056 charger module, multiple I/O expanders for button input, and a 16x2 LCD display to show battery voltage and system status. A 10-LED bar graph provides quick visual feedback of remaining charge.

The guide includes a complete wiring diagram connecting the ADS1115 ADC for battery monitoring, the PCF8574 modules for LCD control, the MCP23017 for button matrix expansion, and logic level shifting between the 3.3V ESP32 and 5V display. Assembly steps cover preparing modules, configuring I2C addresses, wiring buttons and display, mounting the rechargeable power system, and housing everything in an enclosure. Firmware and web interface code enable sending Roku commands over WiFi while displaying battery status on the LCD.

Wiring diagram

Wiring diagram for Roku Remote With Battery Gauge
Loading diagram…

Gather all the parts

QtyComponent
1

LiPo 3.7V 1000mAh Battery

Single-cell LiPo pack, nominal 3.7 V, 1000 mAh. Default rechargeable choice for portable ESP32 / Pico projects. Pair with a TP4056 charger for safe USB recharging.

1

TP4056 Li-Ion/LiPo charger module with protection

TP4056 single-cell Li-Ion/LiPo linear charger module, 5V USB input, 1A charge current (programmable). Common variants ship with DW01 protection. Pair with battery_lipo_storage for the cell.

1

Rocker Switch

Classic compact panel-mount rocker switch for on/off power control in enclosures.

1

Boost Converter

Small adjustable MT3608-style DC-DC boost converter module for stepping a lower DC input up to a higher rail such as 5V, 9V, or 12V. It is a power-path module with VIN/VOUT terminals, not a GPIO peripheral.

1

ADS1115 16-Bit ADC Module

16-bit four-channel I2C analog-to-digital converter with programmable gain amplifier. Common ADS1115 breakouts expose MCU-facing SDA/SCL pins and four analog inputs for single-ended or differential measurements.

1

MCP23017 I/O Expander

Microchip MCP23017 16-bit GPIO expander controlled over I2C. Provides two 8-bit ports for external digital inputs or outputs.

1

PCF8574 configurado a 0x21

PCF8574 direccion 0x21 (A0=1 A1=A2=0)

Modulo PCF8574 con puentes de direccion seleccionados antes de encender.

1

PCF8574 configurado a 0x27

PCF8574 direccion 0x27 (A0=A1=A2=1)

Modulo PCF8574 con puentes de direccion seleccionados antes de encender.

1

BSS138 Logic Level Shifter

Common BSS138 MOSFET-based 4-channel bidirectional logic level converter module. Good for I2C, UART, and slow digital signals between 3.3V and 5V domains; not a strong high-speed SPI/LED data buffer.

1

Steren LCD 16x2 HD44780 5V

LCD paralelo HD44780. DB4 a DB7 son datos del LCD, no GPIO del NodeMCU.

1

10kΩ Potentiometer

10 kΩ linear (B10K) rotary potentiometer in the common 16 mm panel style: metal can, M7 threaded bushing for mounting through a panel, a 6 mm knurled split shaft for push-on knobs, and three PCB pins in a row 5 mm apart. Connect the outer pins across 3.3 V or 5 V and ground; the middle wiper pin then gives an adjustable voltage for an ADC input over about 300° of rotation.

1

Steren ARD-386 barra de 10 LED

Diez LED independientes. Identificar cada anodo A y catodo K con multimetro en prueba de diodos antes de soldar; no asumir numeracion fisica.

1

Push Button

Momentary push button switch

1

Push Button

Momentary push button switch

1

Push Button

Momentary push button switch

1

Push Button

Momentary push button switch

1

Push Button

Momentary push button switch

1

Push Button

Momentary push button switch

1

Push Button

Momentary push button switch

1

Push Button

Momentary push button switch

1

Resistor

330 ohm

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

330 ohm

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

330 ohm

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

330 ohm

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

330 ohm

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

330 ohm

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

330 ohm

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

330 ohm

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

330 ohm

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

330 ohm

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

220 ohm

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

100 kohm 1%

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

100 kohm 1%

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

4.7 kohm

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

4.7 kohm

Through-hole resistor (current-limiting in series with an LED)

1

Ceramic Capacitor

100 nF

Through-hole ceramic capacitor bought by value, such as 22 pF, 10 nF or 100 nF (0.1 µF). It is non-polarised, so either lead can go to either net. Its most common job is decoupling: a 100 nF capacitor between an IC's supply pin and ground, placed as close to the chip as possible.

1

Ceramic Capacitor

100 nF

Through-hole ceramic capacitor bought by value, such as 22 pF, 10 nF or 100 nF (0.1 µF). It is non-polarised, so either lead can go to either net. Its most common job is decoupling: a 100 nF capacitor between an IC's supply pin and ground, placed as close to the chip as possible.

1

Electrolytic Capacitor

470 uF 10 V

Radial aluminium electrolytic capacitor bought by value and voltage rating, such as 10 µF, 100 µF or 1000 µF 10 V. It is polarised: the + lead goes to the higher voltage and the - lead, marked by the stripe on the sleeve, to the lower voltage or ground. It is mostly used for bulk filtering across a power rail, with a voltage rating comfortably above the rail.

Assemble it in 6 steps

1. Preparar las piezas

Usa tu NodeMCU ARD-395, LCD, barra ARD-386 y ocho pulsadores iguales al que tienes (añade los que falten). La otra placa de desarrollo no se utiliza. Trabaja primero fuera del gabinete sobre una placa de montaje aislada.

2. Preparar direcciones de los módulos

Usa módulos con pines rotulados. MCP23017 a 0x20: A0, A1 y A2 a GND, RESET a 3.3 V. PCF8574 de botones a 0x21: A0 a 3.3 V, A1 y A2 a GND. PCF8574 del LCD a 0x27: A0, A1 y A2 a 5 V. Configura sus puentes antes de encender. ADS1115 ADDR a GND, dirección 0x48.

  • El LCD funciona a 5 V. Sus señales I2C deben pasar por el convertidor BSS138, nunca directamente al NodeMCU.

3. Conectar botones y pantallas

D2 es datos y D1 es reloj. Sigue el diagrama. Botones P0 a P7: arriba, abajo, izquierda, derecha, aceptar, inicio, atrás y reproducir/pausa. En cada botón de cuatro patas verifica continuidad: las dos patas del mismo contacto ya están unidas; utiliza dos contactos diferentes. En el LCD conecta RS=P0, RW=P1, EN=P2 y D4..D7=P4..P7 del PCF8574; P3 queda libre. Ajusta el potenciómetro para ver las letras.

4. Identificar los diez segmentos

Con todo desconectado, usa la prueba de diodos del multímetro para localizar el ánodo y el cátodo de cada segmento. A1/K1 es el primer rojo y A10/K10 el azul. El diagrama usa estos nombres funcionales, no números de patas del encapsulado. Conecta cada ánodo a 3.3 V mediante su propia resistencia de 330 ohmios y cada cátodo a la salida indicada del MCP23017.

  • La barra es una estimación de batería por voltaje. El segmento azul puede verse más tenue a 3.3 V.

5. Montar alimentación recargable

Compra una LiPo protegida 1S de 3.7 V y 1000 mAh, que admita carga de 1 A, un TP4056 con protección y bornes B+/B-/OUT+/OUT-, y un elevador MT3608. Conecta batería a B+/B-. OUT+ pasa por el interruptor antes del elevador. OUT- es la tierra común. Ajusta el elevador a 5.00 V con multímetro ANTES de conectarlo al NodeMCU y LCD. El divisor de dos resistencias de 100 kohm va desde la salida del interruptor a GND y su centro a AIN0.

  • Apaga el interruptor para cargar: este TP4056 no tiene reparto de carga para usar y cargar al mismo tiempo.
  • No conectes el USB del NodeMCU mientras el elevador esté conectado a VIN y encendido. Para programar, desconecta la batería y la salida del elevador de VIN.
  • Respeta polaridad, aísla soldaduras y no perfores ni aplastes la batería. Carga sobre superficie no inflamable y con supervisión.

6. Acomodar el gabinete

Prueba la distribución con el gabinete de 10.2 x 4.1 x 7.7 cm antes de perforar. El LCD mide aproximadamente 8 x 3.6 cm en su tarjeta. Deja espacio para batería, botones, cables y acceso USB del cargador. No fuerces las piezas si no caben; coloca las placas en soportes aislantes y mantén libre la antena del NodeMCU.

Review all connections

1. Connections between "battery" and "ESP32"

FunctionbatteryESP32
power+V → TP4056 Li-Ion/LiPo charger module with protection B+EXT
groundGND → TP4056 Li-Ion/LiPo charger module with protection B-EXT

2. Connections between "charger" and "ESP32"

FunctionchargerESP32
powerOUT+ → Rocker Switch T1EXT
groundOUT-GND
powerIN+ → USB 5 V del conector del cargadorEXT
groundIN- → GND del conector USB del cargadorEXT

3. Connections between "switch" and "ESP32"

FunctionswitchESP32
powerT2 → Boost Converter VIN+EXT

4. Connections between "boost" and "ESP32"

FunctionboostESP32
groundVIN-GND
groundVOUT-GND
powerVOUT+VIN

5. Connections between "adc" and "ESP32"

FunctionadcESP32
powerVDD3V3
groundGNDGND
i2cSDAGPIO 4
i2cSCLGPIO 5
groundADDRGND

6. Connections between "ledio" and "ESP32"

FunctionledioESP32
powerVDD3V3
groundVSSGND
i2cSDAGPIO 4
i2cSCLGPIO 5
powerRESET3V3

7. Connections between "keys" and "ESP32"

FunctionkeysESP32
powerVCC3V3
groundGNDGND
i2cSDAGPIO 4
i2cSCLGPIO 5

8. Connections between "level" and "ESP32"

FunctionlevelESP32
powerLV3V3
powerHV → Boost Converter VOUT+EXT
groundGNDGND
digitalLV1 → ADS1115 16-Bit ADC Module SDAEXT
digitalLV2 → ADS1115 16-Bit ADC Module SCLEXT
digitalHV1 → PCF8574 configurado a 0x27 SDAEXT
digitalHV2 → PCF8574 configurado a 0x27 SCLEXT

9. Connections between "lcdio" and "ESP32"

FunctionlcdioESP32
powerVCC → Boost Converter VOUT+EXT
groundGNDGND
digitalP0 → Steren LCD 16x2 HD44780 5V RSEXT
digitalP1 → Steren LCD 16x2 HD44780 5V RWEXT
digitalP2 → Steren LCD 16x2 HD44780 5V ENEXT
digitalP4 → Steren LCD 16x2 HD44780 5V DB4EXT
digitalP5 → Steren LCD 16x2 HD44780 5V DB5EXT
digitalP6 → Steren LCD 16x2 HD44780 5V DB6EXT

10. Connections between "key0" and "ESP32"

Functionkey0ESP32
digitalSIGNAL → PCF8574 configurado a 0x21 P0EXT
groundGNDGND

11. Connections between "key1" and "ESP32"

Functionkey1ESP32
digitalSIGNAL → PCF8574 configurado a 0x21 P1EXT
groundGNDGND

12. Connections between "key2" and "ESP32"

Functionkey2ESP32
digitalSIGNAL → PCF8574 configurado a 0x21 P2EXT
groundGNDGND

13. Connections between "key3" and "ESP32"

Functionkey3ESP32
digitalSIGNAL → PCF8574 configurado a 0x21 P3EXT
groundGNDGND

14. Connections between "key4" and "ESP32"

Functionkey4ESP32
digitalSIGNAL → PCF8574 configurado a 0x21 P4EXT
groundGNDGND

15. Connections between "key5" and "ESP32"

Functionkey5ESP32
digitalSIGNAL → PCF8574 configurado a 0x21 P5EXT
groundGNDGND

16. Connections between "key6" and "ESP32"

Functionkey6ESP32
digitalSIGNAL → PCF8574 configurado a 0x21 P6EXT
groundGNDGND

17. Connections between "key7" and "ESP32"

Functionkey7ESP32
digitalSIGNAL → PCF8574 configurado a 0x21 P7EXT
groundGNDGND

18. Connections between "bar" and "ESP32"

FunctionbarESP32
digitalA1 → Resistor P2EXT
groundK1 → MCP23017 I/O Expander GPA0-GPA7EXT
digitalA2 → Resistor P2EXT
groundK2 → MCP23017 I/O Expander GPA0-GPA7EXT
digitalA3 → Resistor P2EXT
groundK3 → MCP23017 I/O Expander GPA0-GPA7EXT
digitalA4 → Resistor P2EXT
groundK4 → MCP23017 I/O Expander GPA0-GPA7EXT
digitalA5 → Resistor P2EXT
groundK5 → MCP23017 I/O Expander GPA0-GPA7EXT
digitalA6 → Resistor P2EXT
groundK6 → MCP23017 I/O Expander GPA0-GPA7EXT
digitalA7 → Resistor P2EXT
groundK7 → MCP23017 I/O Expander GPA0-GPA7EXT
digitalA8 → Resistor P2EXT
groundK8 → MCP23017 I/O Expander GPA0-GPA7EXT
digitalA9 → Resistor P2EXT
groundK9 → MCP23017 I/O Expander GPB0-GPB7EXT
digitalA10 → Resistor P2EXT
groundK10 → MCP23017 I/O Expander GPB0-GPB7EXT

19. Connections between "r0" and "ESP32"

Functionr0ESP32
powerP13V3

20. Connections between "r1" and "ESP32"

Functionr1ESP32
powerP13V3

21. Connections between "r2" and "ESP32"

Functionr2ESP32
powerP13V3

22. Connections between "r3" and "ESP32"

Functionr3ESP32
powerP13V3

23. Connections between "r4" and "ESP32"

Functionr4ESP32
powerP13V3

24. Connections between "r5" and "ESP32"

Functionr5ESP32
powerP13V3

25. Connections between "r6" and "ESP32"

Functionr6ESP32
powerP13V3

26. Connections between "r7" and "ESP32"

Functionr7ESP32
powerP13V3

27. Connections between "r8" and "ESP32"

Functionr8ESP32
powerP13V3

28. Connections between "r9" and "ESP32"

Functionr9ESP32
powerP13V3

29. Connections between "lcd" and "ESP32"

FunctionlcdESP32
digitalDB7 → PCF8574 configurado a 0x27 P7EXT
powerVCC → Boost Converter VOUT+EXT
groundGNDGND
analogVO → 10kΩ Potentiometer WIPEREXT
groundLED-GND

30. Connections between "contrast" and "ESP32"

FunctioncontrastESP32
powerVCC → Boost Converter VOUT+EXT
groundGNDGND

31. Connections between "backlight" and "ESP32"

FunctionbacklightESP32
digitalP1 → Boost Converter VOUT+EXT
digitalP2 → Steren LCD 16x2 HD44780 5V LED+EXT

32. Connections between "top" and "ESP32"

FunctiontopESP32
digitalP1 → Rocker Switch T2EXT
digitalP2 → ADS1115 16-Bit ADC Module AIN0EXT

33. Connections between "bottom" and "ESP32"

FunctionbottomESP32
digitalP1 → ADS1115 16-Bit ADC Module AIN0EXT
groundP2GND

34. Connections between "pullsda" and "ESP32"

FunctionpullsdaESP32
powerP13V3
digitalP2 → ADS1115 16-Bit ADC Module SDAEXT

35. Connections between "pullscl" and "ESP32"

FunctionpullsclESP32
powerP13V3
digitalP2 → ADS1115 16-Bit ADC Module SCLEXT

36. Connections between "dec" and "ESP32"

FunctiondecESP32
power13V3
ground2GND

37. Connections between "filter" and "ESP32"

FunctionfilterESP32
digital1 → ADS1115 16-Bit ADC Module AIN0EXT
ground2GND

38. Connections between "bulk" and "ESP32"

FunctionbulkESP32
power+ → Boost Converter VOUT+EXT
ground-GND

Deploy the firmware

#include <Arduino.h>
#include <Wire.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <ESP8266HTTPClient.h>
#include <EEPROM.h>
ESP8266WebServer server(80);
struct Settings {uint32_t magic; char ssid[33],pass[65],ip[16];} cfg;
const char* commands[]={"Up","Down","Left","Right","Select","Home","Back","Play"};
String last0,last1,status="Listo"; float voltage=0; bool hardware=true; uint8_t stable=255,previous=255; unsigned long changed=0,tick=0,net=0;
bool put(uint8_t addr,uint8_t v){Wire.beginTransmission(addr);Wire.write(v);return Wire.endTransmission()==0;}
bool reg8(uint8_t addr,uint8_t r,uint8_t v){Wire.beginTransmission(addr);Wire.write(r);Wire.write(v);return Wire.endTransmission()==0;}
void nibble(uint8_t n,bool rs){uint8_t v=(n<<4)|(rs?1:0);put(0x27,v);put(0x27,v|4);delayMicroseconds(2);put(0x27,v);delayMicroseconds(50);}
void lcdByte(uint8_t v,bool rs=false){nibble(v>>4,rs);nibble(v&15,rs);}
void screen(String a,String b){a=a.substring(0,16);b=b.substring(0,16);if(a==last0&&b==last1)return;last0=a;last1=b;while(a.length()<16)a+=' ';while(b.length()<16)b+=' ';lcdByte(0x80);for(char c:a)lcdByte(c,true);lcdByte(0xc0);for(char c:b)lcdByte(c,true);}
void initLCD(){delay(50);nibble(3,0);delay(5);nibble(3,0);delay(5);nibble(3,0);nibble(2,0);lcdByte(0x28);lcdByte(0x0c);lcdByte(6);lcdByte(1);delay(3);}
void page(){server.send(200,"text/html; charset=utf-8","<meta name='viewport' content='width=device-width'><h2>Control Roku</h2><form method='POST' action='/save'>Red Wi-Fi 2.4 GHz:<input name='ssid' maxlength='32' required><br>Clave Wi-Fi:<input name='pass' type='password' maxlength='64'><br>IP del Roku (Ajustes/Red/Acerca de):<input name='ip' maxlength='15' required><br><button>Guardar y reiniciar</button></form>");}
void setup(){
 Serial.begin(115200);Wire.begin(4,5);Wire.setClock(100000);Wire.setClockStretchLimit(1500);EEPROM.begin(256);EEPROM.get(0,cfg);if(cfg.magic!=0x524f4b55)memset(&cfg,0,sizeof(cfg));cfg.ssid[32]=0;cfg.pass[64]=0;cfg.ip[15]=0;
 hardware=reg8(0x20,0,0xff)&&reg8(0x20,1,0xff);reg8(0x20,0x14,0xff);reg8(0x20,0x15,0xff);reg8(0x20,0,0);reg8(0x20,1,0xfc);hardware=put(0x21,255)&&hardware;
 initLCD();screen("Control Roku","Configurando...");
 WiFi.mode(WIFI_AP_STA);WiFi.softAP("Control-Roku","RokuConfig26");if(cfg.ssid[0])WiFi.begin(cfg.ssid,cfg.pass);
 server.on("/",page);server.on("/save",HTTP_POST,[]{IPAddress ip;if(!ip.fromString(server.arg("ip"))||server.arg("ssid").isEmpty()){server.send(400,"text/plain","IP o red invalida");return;}memset(&cfg,0,sizeof(cfg));cfg.magic=0x524f4b55;server.arg("ssid").toCharArray(cfg.ssid,sizeof(cfg.ssid));server.arg("pass").toCharArray(cfg.pass,sizeof(cfg.pass));server.arg("ip").toCharArray(cfg.ip,sizeof(cfg.ip));EEPROM.put(0,cfg);EEPROM.commit();server.send(200,"text/plain","Guardado. Reiniciando.");delay(700);ESP.restart();});server.begin();
}
void sampleBattery(){
 // ADS1115: AIN0-GND, +/-4.096V, single-shot, 128 SPS.
 Wire.beginTransmission(0x48);Wire.write(1);Wire.write(0xc3);Wire.write(0x83);if(Wire.endTransmission()!=0){hardware=false;return;}delay(10);Wire.beginTransmission(0x48);Wire.write(0);Wire.endTransmission();if(Wire.requestFrom(0x48,2)!=2){hardware=false;return;}int16_t raw=(Wire.read()<<8)|Wire.read();float v=raw*0.000125f*2;voltage=voltage==0?v:voltage*0.8f+v*0.2f;
 // Indicador aproximado por tension, no contador de carga.
 const float thresholds[]={3.3,3.5,3.6,3.68,3.74,3.8,3.87,3.95,4.05,4.15};int n=0;for(float t:thresholds)if(voltage>=t)n++;uint16_t bits=(1<<n)-1;reg8(0x20,0x14,(~bits)&255);reg8(0x20,0x15,((~bits)>>8)&255);
}
void loop(){server.handleClient();unsigned long now=millis();if(now-tick>2000){tick=now;sampleBattery();String line=!hardware?"Revisar I2C":(WiFi.status()!=WL_CONNECTED?"192.168.4.1":status);screen("Bat "+String(voltage,2)+"V aprox",line);}
 if(now-net>20){net=now;if(Wire.requestFrom(0x21,1)!=1){hardware=false;return;}uint8_t v=Wire.read();if(v!=previous){previous=v;changed=now;}if(now-changed>35&&v!=stable){uint8_t pressed=stable&~v;stable=v;for(int i=0;i<8;i++)if(pressed&(1<<i)){if(voltage>0&&voltage<3.3){status="Cargar bateria";continue;}if(WiFi.status()!=WL_CONNECTED){status="Sin Wi-Fi";continue;}WiFiClient client;HTTPClient http;http.setTimeout(700);http.begin(client,String("http://")+cfg.ip+":8060/keypress/"+commands[i]);int code=http.POST("");http.end();status=code==200?String(commands[i]):"Error "+String(code);}}}delay(1);}

// Wiring verified: LCD D7 is PCF8574 P7, not a NodeMCU GPIO.
// LCD data names DB4..DB7 match the same PCF pins 4..7.

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace, diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik