RS485 Cable Detector

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

// ═══════════════════════════════════════════════════
//  PIN DEFINITIONS
// ═══════════════════════════════════════════════════
#define RS485_RX      16
#define RS485_TX      17

#define PROBE_1       34    // ADC - Wire candidate 1 (needs 10k pull-down)
#define PROBE_2       35    // ADC - Wire candidate 2 (needs 10k pull-down)
#define PROBE_3       32    // ADC - Wire candidate 3 (needs 10k pull-down)

#define BTN_BOOT       0    // Built-in BOOT button (Restart / Confirm)
#define BTN_MODE      25    // External button (Toggle ASCII/HEX)

// ═══════════════════════════════════════════════════
//  OLED DEFINITIONS (DUAL SCREEN)
// ═══════════════════════════════════════════════════
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET    -1

// Bus 1 (OLED 1 on default pins: SDA=21, SCL=22)
Adafruit_SSD1306 display1(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// Bus 2 (OLED 2 on custom pins: SDA=26, SCL=27)
TwoWire I2C_Two = TwoWire(1); 
Adafruit_SSD1306 display2(SCREEN_WIDTH, SCREEN_HEIGHT, &I2C_Two, OLED_RESET);

// ═══════════════════════════════════════════════════
//  BAUD RATES & MODES
// ═══════════════════════════════════════════════════
const long BAUD_RATES[]  = {1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200};
const int  BAUD_COUNT    = sizeof(BAUD_RATES) / sizeof(BAUD_RATES[0]);
int        baudIndex     = 3;     // Start at 9600
bool       isHexMode     = false; // Default to ASCII

// ═══════════════════════════════════════════════════
//  STATE MACHINE
// ═══════════════════════════════════════════════════
enum State {
  STATE_SCAN_VOLTAGE,     
  STATE_CHECK_BIAS,       
  STATE_WAIT_WIRING,      
  STATE_AUTO_BAUD,        
  STATE_PROMPT_SWAP,      
  STATE_LOCKED            
};

State      currentState      = STATE_SCAN_VOLTAGE;
uint32_t   stateTimer        = 0;
uint32_t   lastByteTime      = 0;
uint32_t   bytesReceived     = 0;
uint32_t   validAsciiBytes   = 0;
String     receivedData      = "";
String     statusMsg         = "";
String     subMsg            = "";

// Array to hold 6 lines of data for Screen 2
String     dataLines[6]      = {"", "", "", "", "", ""};

bool       lastBootBtn       = HIGH;
bool       lastModeBtn       = HIGH;

// Voltage readings
float      v1 = 0, v2 = 0, v3 = 0;
int        gndWire   = -1;   
int        aWire     = -1;   
int        bWire     = -1;   

// ═══════════════════════════════════════════════════
//  HELPERS
// ═══════════════════════════════════════════════════
float readVoltage(int pin) {
  long sum = 0;
  for (int i = 0; i < 64; i++) { sum += analogRead(pin); }
  return ((sum / 64.0) / 4095.0) * 3.3; 
}

bool isReadableAscii(char c) {
  return (c >= 32 && c <= 126) || c == '\n' || c == '\r' || c == '\t';
}

void restartSerial(long baud) {
  Serial2.end();
  delay(30);
  Serial2.begin(baud, SERIAL_8N1, RS485_RX, RS485_TX);
}

void flushSerial() {
  while (Serial2.available()) Serial2.read();
  bytesReceived = 0;
  validAsciiBytes = 0;
  receivedData  = "";
}

// Push a new line to Screen 2's scrolling list
void pushLine(String s) {
  for (int i = 0; i < 5; i++) {
    dataLines[i] = dataLines[i+1];
  }
  dataLines[5] = s.substring(0, 21);
}

void processIncomingByte(char c) {
  bytesReceived++;
  lastByteTime = millis();

  if (isReadableAscii(c)) validAsciiBytes++;

  if (isHexMode) {
    char hexStr[4];
    sprintf(hexStr, "%02X ", (uint8_t)c);
    receivedData += String(hexStr);
    
    // Max 7 hex bytes per line (21 chars)
    if (receivedData.length() >= 20) {
      pushLine(receivedData);
      receivedData = "";
    }
  } else {
    // ASCII Mode
    if (c == '\n' || receivedData.length() >= 21) {
      pushLine(receivedData);
      receivedData = "";
    } else if (c != '\r') {
      receivedData += c;
    }
  }
}

// ═══════════════════════════════════════════════════
//  OLED DISPLAY LOGIC
// ═══════════════════════════════════════════════════
void updateDisplay() {
  
  // ── SCREEN 1: CONTROL PANEL ──
  display1.clearDisplay();
  display1.setTextSize(1);
  
  display1.fillRect(0, 0, 128, 10, SSD1306_WHITE);
  display1.setTextColor(SSD1306_BLACK);
  display1.setCursor(2, 1);
  display1.print("RS485 STATUS");
  display1.setTextColor(SSD1306_WHITE);

  display1.setCursor(0, 12); display1.print(statusMsg.substring(0, 21));
  display1.setCursor(0, 22); display1.print(subMsg.substring(0, 21));

  if (currentState == STATE_SCAN_VOLTAGE) {
    display1.setCursor(0, 32); display1.print("W1:"); display1.print(v1, 2); display1.print("V");
    display1.setCursor(0, 42); display1.print("W2:"); display1.print(v2, 2); display1.print("V");
    display1.setCursor(0, 52); display1.print("W3:"); display1.print(v3, 2); display1.print("V");
  } 
  else if (currentState == STATE_WAIT_WIRING || currentState == STATE_PROMPT_SWAP) {
    display1.setCursor(0, 36); display1.print("GND -> Wire "); display1.print(gndWire);
    display1.setCursor(0, 46); display1.print(" A  -> Wire "); display1.print(aWire);
    display1.setCursor(0, 56); display1.print(" B  -> Wire "); display1.print(bWire);
  }
  else if (currentState >= STATE_AUTO_BAUD) {
    display1.setCursor(0, 32); display1.print("GND:W"); display1.print(gndWire); display1.print(" A:W"); display1.print(aWire); display1.print(" B:W"); display1.print(bWire);
    display1.setCursor(0, 42); display1.print("BAUD: "); display1.print(BAUD_RATES[baudIndex]);
    display1.setCursor(0, 52); display1.print("Bytes Rx: "); display1.print(bytesReceived);
  }
  display1.display();

  // ── SCREEN 2: DATA LOG ──
  display2.clearDisplay();
  display2.setTextSize(1);
  
  display2.fillRect(0, 0, 128, 10, SSD1306_WHITE);
  display2.setTextColor(SSD1306_BLACK);
  display2.setCursor(2, 1);
  if (isHexMode) display2.print("DATA LOG [HEX]");
  else           display2.print("DATA LOG [ASCII]");
  display2.setTextColor(SSD1306_WHITE);

  // Print all 6 lines of data
  for (int i = 0; i < 6; i++) {
    display2.setCursor(0, 12 + (i * 9));
    display2.print(dataLines[i]);
  }
  
  // Print the current incoming string buffer if not empty
  if (receivedData.length() > 0) {
     display2.setCursor(0, 12 + (5 * 9)); // Print over the bottom line
     display2.print(receivedData);
  }
  display2.display();
}

// ═══════════════════════════════════════════════════
//  STATE ROUTINES
// ═══════════════════════════════════════════════════
void doScanVoltage() {
  statusMsg = "STEP 1: SCANNING";
  subMsg    = "Connect probes...";

  v1 = readVoltage(PROBE_1);
  v2 = readVoltage(PROBE_2);
  v3 = readVoltage(PROBE_3);

  float minV = min(v1, min(v2, v3));
  float maxV = max(v1, max(v2, v3));

  if (maxV < 0.4) {
    subMsg = "Waiting for voltage...";
  } 
  else if (minV < 0.4 && maxV > 1.0) {
    if (v1 == minV)      gndWire = 1;
    else if (v2 == minV) gndWire = 2;
    else                 gndWire = 3;

    if (gndWire == 1)      { aWire = 2; bWire = 3; }
    else if (gndWire == 2) { aWire = 1; bWire = 3; }
    else                   { aWire = 1; bWire = 2; }

    statusMsg = "GND FOUND! Wire " + String(gndWire);
    subMsg    = "Analyzing bias...";
    updateDisplay();
    delay(1500);
    currentState = STATE_CHECK_BIAS;
  } 
  else if (maxV - minV < 0.3) {
    subMsg = "No clear GND detected";
  }
}

void doCheckBias() {
  statusMsg = "STEP 2: BIAS CHECK";
  updateDisplay();

  float va = 0, vb = 0;
  for (int i = 0; i < 32; i++) {
    int probeA = (aWire == 1) ? PROBE_1 : (aWire == 2) ? PROBE_2 : PROBE_3;
    int probeB = (bWire == 1) ? PROBE_1 : (bWire == 2) ? PROBE_2 : PROBE_3;
    va += (analogRead(probeA) / 4095.0) * 3.3;
    vb += (analogRead(probeB) / 4095.0) * 3.3;
    delayMicroseconds(500);
  }
  va /= 32.0;
  vb /= 32.0;

  float diff = va - vb;
  if (abs(diff) > 0.15 && diff < 0) {
    int tmp = aWire; aWire = bWire; bWire = tmp;
  } 
  currentState = STATE_WAIT_WIRING;
}

void doWaitWiring() {
  statusMsg = "STEP 3: WIRE MODULE";
  subMsg    = "Wire, then press BOOT";
}

void doAutoBaud() {
  statusMsg = "STEP 4: BAUD SCAN";
  subMsg    = "Listening @ " + String(BAUD_RATES[baudIndex]);

  while (Serial2.available()) {
    processIncomingByte((char)Serial2.read());
  }

  if (bytesReceived == 0) {
    if (millis() - stateTimer > 4000) {
      baudIndex = (baudIndex + 1) % BAUD_COUNT;
      if (baudIndex == 3) currentState = STATE_PROMPT_SWAP;
      else {
        restartSerial(BAUD_RATES[baudIndex]);
        flushSerial();
        stateTimer = millis();
      }
    }
  } else {
    // We have data. Wait for 1.5s or a chunk of data to evaluate
    if (millis() - lastByteTime > 1500 || bytesReceived >= 20) {
      bool lockConditionMet = false;

      if (isHexMode) {
        if (bytesReceived >= 15) lockConditionMet = true;
      } else {
        float quality = (float)validAsciiBytes / (float)bytesReceived;
        if (quality > 0.80 && bytesReceived >= 8) lockConditionMet = true;
      }

      if (lockConditionMet) {
        currentState = STATE_LOCKED;
        if (receivedData.length() > 0) {
          pushLine(receivedData);
          receivedData = "";
        }
      } else {
        baudIndex = (baudIndex + 1) % BAUD_COUNT;
        if (baudIndex == 3) currentState = STATE_PROMPT_SWAP;
        else {
          restartSerial(BAUD_RATES[baudIndex]);
          flushSerial();
          stateTimer = millis();
        }
      }
    }
  }
}

void doPromptSwap() {
  statusMsg = "NO VALID DATA";
  subMsg    = "Swap A/B, press BOOT";
}

void doLocked() {
  statusMsg = "LOCKED! BAUD OK";
  subMsg    = String(BAUD_RATES[baudIndex]) + " baud";
  
  while (Serial2.available()) {
    processIncomingByte((char)Serial2.read());
  }
}

// ═══════════════════════════════════════════════════
//  SETUP
// ═══════════════════════════════════════════════════
void setup() {
  Serial.begin(115200);
  
  pinMode(BTN_BOOT, INPUT_PULLUP);
  pinMode(BTN_MODE, INPUT_PULLUP);

  analogReadResolution(12);
  analogSetAttenuation(ADC_11db);

  // Boot up Bus 2 for Screen 2 (Data Log)
  I2C_Two.begin(26, 27, 400000); 

  // Initialize Screen 1
  if (!display1.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    Serial.println("OLED 1 failed");
    while (true) delay(100);
  }
  
  // Initialize Screen 2
  if (!display2.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    Serial.println("OLED 2 failed");
    while (true) delay(100);
  }

  // Splash Screen 1
  display1.clearDisplay();
  display1.setTextSize(1);
  display1.setTextColor(SSD1306_WHITE);
  display1.setCursor(0, 0);
  display1.println("RS485 DETECTIVE");
  display1.println("System Ready");
  display1.display();
  
  // Splash Screen 2
  display2.clearDisplay();
  display2.setTextSize(1);
  display2.setTextColor(SSD1306_WHITE);
  display2.setCursor(0, 0);
  display2.println("DATA LOGGER");
  display2.println("Awaiting Data...");
  display2.display();
  
  delay(1500);

  currentState = STATE_SCAN_VOLTAGE;
}

// ═══════════════════════════════════════════════════
//  LOOP
// ═══════════════════════════════════════════════════
void loop() {
  // ── BTN: BOOT (Context Actions) ──
  bool bootBtn = digitalRead(BTN_BOOT);
  if (bootBtn == LOW && lastBootBtn == HIGH) {
    delay(50); 
    if (currentState == STATE_WAIT_WIRING || currentState == STATE_PROMPT_SWAP) {
      baudIndex = 3; 
      restartSerial(BAUD_RATES[baudIndex]);
      flushSerial();
      
      // Clear data lines for a fresh start
      for(int i=0; i<6; i++) dataLines[i] = ""; 
      
      currentState = STATE_AUTO_BAUD;
      stateTimer = millis();
    } else {
      currentState = STATE_SCAN_VOLTAGE;
      gndWire = aWire = bWire = -1;
      flushSerial();
    }
  }
  lastBootBtn = bootBtn;

  // ── BTN: MODE (Toggle ASCII/HEX) ──
  bool modeBtn = digitalRead(BTN_MODE);
  if (modeBtn == LOW && lastModeBtn == HIGH) {
    delay(50); 
    isHexMode = !isHexMode;
    receivedData = ""; // clear buffer so it doesn't mix hex and ascii
    updateDisplay();   // Instant feedback
  }
  lastModeBtn = modeBtn;

  switch (currentState) {
    case STATE_SCAN_VOLTAGE:  doScanVoltage();   break;
    case STATE_CHECK_BIAS:    doCheckBias();     break;
    case STATE_WAIT_WIRING:   doWaitWiring();    break;
    case STATE_AUTO_BAUD:     doAutoBaud();      break;
    case STATE_PROMPT_SWAP:   doPromptSwap();    break;
    case STATE_LOCKED:        doLocked();        break;
  }

  static uint32_t lastDisp = 0;
  if (millis() - lastDisp > 250) {
    updateDisplay();
    lastDisp = millis();
  }
}

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.