#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();
}
}