Dave Azcarate
Published © GPL3+

My old betabrite controlled with esp8266

Transform your old BetaBrite LED sign into a modern, WiFi-enabled display! With just an ESP8266, this project gives you a slick web interfac

IntermediateFull instructions provided3 hours36
My old betabrite controlled with esp8266

Things used in this project

Hardware components

ESP8266 ESP-12E
Espressif ESP8266 ESP-12E
×1
Telephone Modular Cable, RJ12 Plug to RJ12 Plug
Telephone Modular Cable, RJ12 Plug to RJ12 Plug
×1
max232
×1

Software apps and online services

Arduino IDE
Arduino IDE

Story

Read more

Schematics

Cable to ttl

rx an tx betabrite

gnd betabrite

Gnd

MAX232

WIRING

db9 pinout

Example

Code

betabrite esp8266 control

Arduino
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <SoftwareSerial.h>
#include <FS.h>
#include <WiFiUdp.h>
#include <NTPClient.h>
#include <EEPROM.h>
#include <time.h>

// --- EEPROM parameters ---
#define EEPROM_SIZE 192                
#define STA_SSID_ADDR   0
#define STA_PASS_ADDR   48
#define AP_SSID_ADDR    96
#define AP_PASS_ADDR    144
#define WIFI_CRED_MAXLEN 47             

// --- Efectos y colores ---
struct Effect { String name; String code; };
Effect effects[] = {
  {"ROTACION", "a"}, {"FIJO", "b"}, {"PARPADEO", "c"},
  {"ARRIBA", "e"}, {"ABAJO", "f"}, {"IZQUIERDA", "g"}, {"DERECHA", "h"},
  {"BARRIDO ARRIBA", "i"}, {"BARRIDO ABAJO", "j"}, {"BARRIDO IZQ", "k"}, {"BARRIDO DER", "l"},
  {"DESPLAZAMIENTO", "m"}, {"AUTO", "o"}, {"CENTRO IN", "p"}, {"CENTRO OUT", "q"},
  {"EXPLOSION", "u"}, {"ROT. CORTO", "t"}, {"ESTRELLAS", "n7"}, {"BRILLO", "n1"},
  {"NIEVE", "n2"}, {"BIENVENIDA", "n8"}, {"NOTICIAS", "A"}
};
struct ColorOption { String name; String code; };
ColorOption colors[] = { {"Automatico", "0"}, {"Rojo", "1"}, {"Verde", "2"}, {"Ambar", "3"} };

SoftwareSerial alphaSign(D2, D1);

const int numMessages = 10;
bool messageEnabled[numMessages] = {true, true, true, true, true, true, true, true, true, true};
String messages[numMessages] = { "Mensaje 1 por defecto", "Mensaje 2 por defecto", "Mensaje 3 por defecto",
  "Mensaje 4 por defecto", "Mensaje 5 por defecto", "Mensaje 6 por defecto",
  "Mensaje 7 por defecto", "Mensaje 8 por defecto", "Mensaje 9 por defecto", "Mensaje 10 por defecto" };
String selectedEffects[numMessages] = {"a", "b", "c", "e", "f", "g", "h", "i", "j", "k"};
String selectedColors[numMessages]  = {"0", "0", "0", "0", "0", "0", "0", "0", "0", "0"};
int intervalSeconds = 5;
int displaySpeed = 3;

String wifiSsid = "your_wifi";
String wifiPass = "yourpass";
String apSsid = "BetaBrite_AP";
String apPass = "12345678";
String relojOpcion = "ambos"; // "reloj", "fecha", "ambos", "ninguno"

ESP8266WebServer server(80);
WiFiUDP ntpUDP;
NTPClient* timeClient = nullptr;

volatile bool configurando = false;
bool mostrarIP = false;
unsigned long tiempoInicioIP = 0;
#define TIEMPO_MOSTRAR_IP 10000UL // 1 min
bool apFallback = false;

// ------- Prototipos ----------
void saveConfig();
void loadConfig();
void saveWiFiEEPROM();
void loadWiFiEEPROM();
String quitarAcentos(String texto);
void enviarTexto(String texto, String efecto, String color);
void enviarFechaHoraBetaBrite();
void handleRoot();
void handleSave();
void mostrarIpEnBetabrite(const char* ip);

// --- Funciones EEPROM para strings ---
// Guarda un String en EEPROM desde la dirección y hasta WIFI_CRED_MAXLEN caracteres
void saveStringToEEPROM(int addr, const String& val) {
  byte len = min((int)val.length(), WIFI_CRED_MAXLEN);
  EEPROM.write(addr, len); // longitud
  for (int i=0; i<len; i++) EEPROM.write(addr + 1 + i, val[i]);
  EEPROM.write(addr + 1 + len, 0); // null terminator extra, para seguridad
}

// Lee un String desde EEPROM
String readStringFromEEPROM(int addr) {
  int len = EEPROM.read(addr);
  if (len > WIFI_CRED_MAXLEN) len = WIFI_CRED_MAXLEN; // protección básica
  char data[WIFI_CRED_MAXLEN + 1];
  for (int i=0; i<len; i++) data[i] = EEPROM.read(addr + 1 + i);
  data[len] = 0;
  return String(data);
}

// Guarda credenciales de WiFi y AP en EEPROM
void saveWiFiEEPROM() {
  EEPROM.begin(EEPROM_SIZE);
  saveStringToEEPROM(STA_SSID_ADDR, wifiSsid);
  saveStringToEEPROM(STA_PASS_ADDR, wifiPass);
  saveStringToEEPROM(AP_SSID_ADDR, apSsid);
  saveStringToEEPROM(AP_PASS_ADDR, apPass);
  EEPROM.commit();
  EEPROM.end();
}

// Recupera credenciales de WiFi y AP desde EEPROM sólo si las cadenas actuales están vacías
void loadWiFiEEPROM() {
  EEPROM.begin(EEPROM_SIZE);
  if (wifiSsid.length() == 0)    wifiSsid = readStringFromEEPROM(STA_SSID_ADDR);
  if (wifiPass.length() == 0)    wifiPass = readStringFromEEPROM(STA_PASS_ADDR);
  if (apSsid.length() == 0)      apSsid = readStringFromEEPROM(AP_SSID_ADDR);
  if (apPass.length() == 0)      apPass = readStringFromEEPROM(AP_PASS_ADDR);
  EEPROM.end();
}

void setup() {
  SPIFFS.begin();
  loadConfig(); // SPIFFS
  loadWiFiEEPROM(); // Si SPIFFS 'olvida', recupera de EEPROM

  alphaSign.begin(9600);
  Serial.begin(115200);

  bool intentarSTA = wifiSsid.length() > 0 && wifiPass.length() >= 8;
  if (intentarSTA) {
    WiFi.mode(WIFI_STA);
    WiFi.begin(wifiSsid.c_str(), wifiPass.c_str());
    Serial.print("Intentando conectar a WiFi STA: ");
    Serial.println(wifiSsid);
    int wifiTimer = 0;
    while (WiFi.status() != WL_CONNECTED && wifiTimer < 20000) {
      delay(500);
      Serial.print(".");
      wifiTimer += 500;
    }
    Serial.println();
  }

  if (WiFi.status() == WL_CONNECTED) {
    Serial.print("WiFi conectada. IP: ");
    Serial.println(WiFi.localIP());
    mostrarIP = true;
    tiempoInicioIP = millis();
    timeClient = new NTPClient(ntpUDP, "pool.ntp.org", -6 * 3600, 60000);
    timeClient->begin();
    apFallback = false;
  } else {
    Serial.println("No se pudo conectar como STA. Lanzando AP de configuracion.");
    WiFi.disconnect(true);
    delay(200);
    WiFi.mode(WIFI_AP);
    delay(500);
    IPAddress local_ip(192, 168, 4, 1), gateway(192, 168, 4, 1), subnet(255, 255, 255, 0);
    WiFi.softAPConfig(local_ip, gateway, subnet);
    WiFi.softAP(apSsid.c_str(), apPass.c_str());
    apFallback = true;
  }

  server.on("/", handleRoot);
  server.on("/save", handleSave);

  // --- Handler de RESET seguro ---
  server.on("/reboot", []() {
    server.send(200, "text/html", "<h1>Reiniciando...</h1>");
    delay(1000);
    ESP.restart();
  });

  server.begin();
  Serial.println("Servidor web listo.");
}

void loop() {
  server.handleClient();
  if (mostrarIP && !apFallback && WiFi.status() == WL_CONNECTED) {
    if (millis() - tiempoInicioIP < TIEMPO_MOSTRAR_IP) {
      mostrarIpEnBetabrite(WiFi.localIP().toString().c_str());
      delay(3000);
      return;
    } else {
      mostrarIP = false;
    }
  }

  if (!configurando) {
    if (!apFallback && WiFi.status() == WL_CONNECTED && timeClient != nullptr &&
        relojOpcion != "ninguno") {
      enviarFechaHoraBetaBrite();
      delay(intervalSeconds * 1000);
      server.handleClient();
      yield();
    }
    for (int i = 0; i < numMessages; i++) {
      if (messageEnabled[i]) {
        String mensajeSinAcentos = quitarAcentos(messages[i]);
        enviarTexto(mensajeSinAcentos, selectedEffects[i], selectedColors[i]);
        delay(intervalSeconds * 1000);
        server.handleClient();
        yield();
      }
    }
  }
}

void mostrarIpEnBetabrite(const char* ip) {
  String ipmsg = ip;
  enviarTexto(ipmsg, "t", "0");
}

void enviarFechaHoraBetaBrite() {
  if (!timeClient) return;
  timeClient->update();
  time_t now = timeClient->getEpochTime();
  struct tm *tmstruct = localtime(&now);
  int hour   = tmstruct->tm_hour;
  int minute = tmstruct->tm_min;
  int month  = tmstruct->tm_mon + 1;
  int day    = tmstruct->tm_mday;
  int year   = tmstruct->tm_year % 100;
  char fechaText[11], horaText[6];
  sprintf(fechaText, "%02d/%02d/%02d", day, month, year);
  sprintf(horaText, "%02d:%02d", hour, minute);

  if (relojOpcion == "fecha") {
    enviarTexto(fechaText, "b", "0");
  } else if (relojOpcion == "reloj") {
    enviarTexto(horaText, "b", "0");
  } else if (relojOpcion == "ambos") {
    String todo = String(horaText) + " - " + String(fechaText);
    enviarTexto(todo, "t", "0");
  }
}

String quitarAcentos(String texto) {
  String limpio = "";
  for (unsigned int i = 0; i < texto.length(); i++) {
    char c = texto.charAt(i);
    switch (c) {
      case 'á': case 'Á': c = 'a'; break;
      case 'é': case 'É': c = 'e'; break;
      case 'í': case 'Í': c = 'i'; break;
      case 'ó': case 'Ó': c = 'o'; break;
      case 'ú': case 'Ú': c = 'u'; break;
      case 'ü': case 'Ü': c = 'u'; break;
      case 'ñ': case 'Ñ': c = 'n'; break;
      case 'ç': case 'Ç': c = 'c'; break;
    }
    if ((unsigned char)c > 127) c = '?';
    limpio += c;
  }
  return limpio;
}

void enviarTexto(String texto, String efecto, String color) {
  const byte velocidades[5] = {0x15, 0x16, 0x17, 0x18, 0x19};
  uint8_t speedIndex = (displaySpeed >= 1 && displaySpeed <= 5) ? displaySpeed - 1 : 2;
  for (int i = 0; i < 5; i++) alphaSign.write(0x00);
  alphaSign.write(0x01);
  alphaSign.write('Z');
  alphaSign.write('0');
  alphaSign.write('0');
  alphaSign.write(0x02);
  alphaSign.write('A');
  alphaSign.write('A');
  alphaSign.write(0x1B);
  alphaSign.write(0x20);
  alphaSign.print(efecto);
  alphaSign.write(velocidades[speedIndex]);
  if (color != "0") {
    alphaSign.write(0x1C);
    alphaSign.write(color.c_str()[0]);
  }
  alphaSign.print(texto);
  alphaSign.write(0x04);
}

void handleRoot() {
  String html = "<!DOCTYPE html><html>";
  html += "<head><style>";
  html += "body { font-family: Arial, sans-serif; background: linear-gradient(to bottom, #74ABE2, #5563DE); color: white; margin: 0; padding: 20px; }";
  html += "h1 { text-align: center; font-size: 36px; margin-bottom: 20px; }";
  html += "form { max-width: 600px; margin: auto; background: #2F3C87; padding: 20px; border-radius: 8px; }";
  html += "h3 { color: #F4D03F; font-size: 22px; }";
  html += "label { display: block; margin-top: 10px; }";
  html += "input[type='text'], input[type='number'], select { width: calc(100% - 20px); padding: 8px; margin: 10px 0; }";
  html += "input[type='checkbox'] { margin-right: 10px; }";
  html += "input[type='submit'], button { background: #F4D03F; border: none; padding: 10px 20px; color: black; font-size: 16px; cursor: pointer; border-radius: 4px; margin-top:8px; }";
  html += "input[type='submit']:hover, button:hover { background: #FFC300; }";
  html += "</style></head><body>";
  html += "<h1>Configuracion BetaBrite 213c-1</h1>";
  html += "<form action='/save' method='POST'>";
  for (int i = 0; i < numMessages; i++) {
    html += "<h3>Mensaje " + String(i + 1) + "</h3>";
    html += "<label><input type='checkbox' name='enable" + String(i) + "' " + (messageEnabled[i] ? "checked" : "") + "> Habilitar</label>";
    html += "<label for='message" + String(i) + "'>Texto:</label>";
    html += "<input type='text' name='message" + String(i) + "' value='" + quitarAcentos(messages[i]) + "'>";
    html += "<label for='effect" + String(i) + "'>Efecto:</label>";
    html += "<select name='effect" + String(i) + "'>";
    for (unsigned int e = 0; e < sizeof(effects)/sizeof(effects[0]); e++) {
      html += "<option value='" + effects[e].code + "' " + (selectedEffects[i] == effects[e].code ? "selected" : "") + ">" + effects[e].name + "</option>";
    }
    html += "</select>";
    html += "<label for='color" + String(i) + "'>Color:</label>";
    html += "<select name='color" + String(i) + "'>";
    for (unsigned int c = 0; c < sizeof(colors)/sizeof(colors[0]); c++) {
      html += "<option value='" + colors[c].code + "' " + (selectedColors[i] == colors[c].code ? "selected" : "") + ">" + colors[c].name + "</option>";
    }
    html += "</select>";
  }
  html += "<h3>Intervalo entre mensajes (segundos):</h3>";
  html += "<input type='number' name='interval' value='" + String(intervalSeconds) + "' min='1'><br><br>";
  html += "<h3>Velocidad del mensaje (1=lento, 5=rapido):</h3>";
  html += "<input type='number' name='speed' min='1' max='5' value='" + String(displaySpeed) + "'><br><br>";
  html += "<h3>Configuracion de reloj/fecha</h3>";
  html += "<label for='relojOpcion'>Mostrar en pantalla:</label>";
  html += "<select name='relojOpcion'>";
  html += "<option value='reloj' " + (relojOpcion == "reloj" ? String("selected") : String("")) + ">Solo reloj</option>";
  html += "<option value='fecha' " + (relojOpcion == "fecha" ? String("selected") : String("")) + ">Solo fecha</option>";
  html += "<option value='ambos' " + (relojOpcion == "ambos" ? String("selected") : String("")) + ">Reloj y Fecha</option>";
  html += "<option value='ninguno' " + (relojOpcion == "ninguno" ? String("selected") : String("")) + ">Ninguno</option>";
  html += "</select><br><br>";
  html += "<h3>Wifi a conectar (STA)</h3>";
  html += "<label for='wifiSsid'>SSID:</label>";
  html += "<input type='text' name='wifiSsid' value='" + wifiSsid + "'><br>";
  html += "<label for='wifiPass'>Password:</label>";
  html += "<input type='password' name='wifiPass' value='" + wifiPass + "'><br>";
  html += "<h3>Wifi de respaldo (AP propio)</h3>";
  html += "<label for='apSsid'>SSID AP:</label>";
  html += "<input type='text' name='apSsid' value='" + apSsid + "'><br>";
  html += "<label for='apPass'>Password AP:</label>";
  html += "<input type='password' name='apPass' value='" + apPass + "'><br>";
  html += "<input type='submit' value='Guardar'>";
  html += "<button type='button' style='background:#D32F2F; color:white; font-weight:bold; margin-left:10px;' onclick=\"if(confirm('¿Seguro que quieres reiniciar el dispositivo?')){fetch('/reboot').then(()=>window.location.reload())}\">Reiniciar dispositivo</button>";
  html += "</form></body></html>";
  server.send(200, "text/html", html);
}
void handleSave() {
  configurando = true;
  for (int i = 0; i < numMessages; i++) {
    String txt = "";
    if (server.hasArg("message" + String(i))) {
      txt = server.arg("message" + String(i));
      txt.trim();
      txt = quitarAcentos(txt);
      if (txt.length()) messages[i] = txt;
    }
    if (server.hasArg("effect" + String(i))) {
      String tmp = server.arg("effect" + String(i));
      tmp.trim();
      selectedEffects[i] = tmp;
    }
    if (server.hasArg("color" + String(i))) {
      String tmp = server.arg("color" + String(i));
      tmp.trim();
      selectedColors[i] = tmp;
    }
    messageEnabled[i] = server.hasArg("enable" + String(i));
  }
  if (server.hasArg("interval")) intervalSeconds = server.arg("interval").toInt();
  if (server.hasArg("speed")) displaySpeed = constrain(server.arg("speed").toInt(), 1, 5);
  if (server.hasArg("relojOpcion")) {
    String tmp = server.arg("relojOpcion");
    tmp.trim();
    relojOpcion = tmp;
  }
  if (server.hasArg("wifiSsid")) {
    String tmp = server.arg("wifiSsid");
    tmp.trim();
    wifiSsid = tmp;
  }
  if (server.hasArg("wifiPass")) {
    String tmp = server.arg("wifiPass");
    tmp.trim();
    wifiPass = tmp;
  }
  if (server.hasArg("apSsid")) {
    String tmp = server.arg("apSsid");
    tmp.trim();
    apSsid = tmp;
  }
  if (server.hasArg("apPass")) {
    String tmp = server.arg("apPass");
    tmp.trim();
    apPass = tmp;
  }
  saveConfig();       // SPIFFS
  saveWiFiEEPROM();   // EEPROM
  server.send(200, "text/html", "<h1>Configuracion guardada!</h1><a href='/'>Regresar</a>");
  configurando = false;
}

void saveConfig() {
  File f = SPIFFS.open("/config.txt", "w");
  if (!f) { Serial.println("Error al abrir config.txt para escritura!"); return; }
  for (int i = 0; i < numMessages; i++) {
    f.printf("%s|%d|%s|%s\n", messages[i].c_str(), messageEnabled[i], selectedEffects[i].c_str(), selectedColors[i].c_str()); yield();
  }
  f.printf("INT:%d\n", intervalSeconds);
  f.printf("SPD:%d\n", displaySpeed);
  f.printf("SSID_STA:%s\n", wifiSsid.c_str());
  f.printf("PW_STA:%s\n", wifiPass.c_str());
  f.printf("SSID_AP:%s\n", apSsid.c_str());
  f.printf("PW_AP:%s\n", apPass.c_str());
  f.printf("RELOJ:%s\n", relojOpcion.c_str());
  f.flush();
  f.close();
  Serial.println("Configuracion guardada en SPIFFS."); yield();
}

void loadConfig() {
  File f = SPIFFS.open("/config.txt", "r");
  if (!f) return;
  int idx = 0;
  wifiSsid = "";
  wifiPass = "";
  apSsid = "";
  apPass = "";
  while (f.available() && idx < numMessages) {
    String line = f.readStringUntil('\n');
    if (line.startsWith("INT:")) { intervalSeconds = line.substring(4).toInt(); continue; }
    if (line.startsWith("SPD:")) { displaySpeed = line.substring(4).toInt(); continue; }
    if (line.startsWith("SSID_STA:")) { String tmp = line.substring(9); tmp.trim(); wifiSsid = tmp; continue; }
    if (line.startsWith("PW_STA:")) { String tmp = line.substring(7); tmp.trim(); wifiPass = tmp; continue; }
    if (line.startsWith("SSID_AP:")) { String tmp = line.substring(8); tmp.trim(); apSsid = tmp; continue; }
    if (line.startsWith("PW_AP:")) { String tmp = line.substring(6); tmp.trim(); apPass = tmp; continue; }
    if (line.startsWith("RELOJ:")) { String tmp = line.substring(6); tmp.trim(); relojOpcion = tmp; continue; }
    int sep1 = line.indexOf('|'); int sep2 = line.indexOf('|', sep1+1); int sep3 = line.indexOf('|', sep2+1);
    if (sep1 > 0 && sep2 > sep1 && sep3 > sep2) {
      String msg = line.substring(0, sep1); msg.trim(); msg = quitarAcentos(msg);
      if (msg.length()) messages[idx] = msg;
      messageEnabled[idx] = line.substring(sep1+1, sep2).toInt() ? true : false;
      String e = line.substring(sep2+1, sep3); e.trim();
      selectedEffects[idx] = e;
      String c = line.substring(sep3+1); c.trim();
      selectedColors[idx] = c;
      idx++;
    }
  }
  f.close();
}

Credits

Dave Azcarate
2 projects • 0 followers

Comments