/* * LM: Convert received serial messages to RCA TV antenna rotator IR codes * Compiled for Heltec WiFi Kit 32(V3) using Arduino IDE 2.2.1 * * Based in part on the HelTec Automation(TM) WIFI_Kit_32 factory test code * by Aaron.Lee from HelTec AutoMation, ChengDu, China * https://github.com/HelTecAutomation/Heltec_ESP32 * * IR code is based on the IRremote IR SimpleSender example. * * Lloyd Milligan (WA4EFS) Oct/Nov 2023 * © CC attribution - https://creativecommons.org/licenses/by/3.0/us/ * */ #include "heltec.h" #include "PinDefinitionsAndMore.h" #include #define VERSION 1.0 const unsigned long HALFSEC = 500; const unsigned long ONESEC = 1000; const unsigned long TWOSEC = 2000; const unsigned long FIVESEC = 5000; const unsigned long GO_DARK_TIME = 120000; // LM: Save screen unsigned long oledDisplayStartTime = millis(); #define DISABLE_CODE_FOR_RECEIVER // IR uint8_t rAddress = 0x6; uint8_t rCommand = 0x4; uint8_t rRepeats = 0; // Serial HardwareSerial azSerial(2); // Serial channel that receives azimuths #define RXD 47 #define TXD -1 #define DEFAULT_SER_BAUD 9600 #define AZ_SER_BAUD 9600 const int RX_BUFFER_SIZE = 16; byte azRxBuffer[RX_BUFFER_SIZE]; unsigned int rxBufferNdx = 0; const char CR = (char) 0x0D; const byte bSP = 0x20; const char SP = (char) 0x20; const char ZERO = '0'; boolean irEmitterTest = false; boolean irCodesTest = false; boolean rxBufferTest = false; boolean nRepeatsTest = false; boolean arrowKeysTest = false; boolean oledTextTest = false; boolean newArrowKeysRepetitionsTest = false; // Message parsing / processing const byte ROTATE_CCW = 0x11; const byte ROTATE_CW = 0x12; const unsigned long AZ_REV_TIME = 60000; // Approximate time for one 360° revolution of the antenna (57 seconds) const unsigned long DELAY_PER_REPEAT = AZ_REV_TIME/(17*30); const unsigned long DELAY_PER_ARROW_BUTTON_PRESS = HALFSEC; const unsigned long DELAY_PER_SMOOTH_DEGREE = 200; const int MIN_AZ_DELTA = 5; // Minimum change in azimuth (degrees) that will produce a rotation const float DEG_PER_ARROW_BUTTON_PRESS = .75; // Experimentally determined (40 presses = 30 degrees) const int PRESSES_PER_DEGREE_NUMERATOR = 4; // Reciprocal of DEG_PER_ARROW_BUTTON_PRESS expressed as fraction const int PRESSES_PER_DEGREE_DENOMINATOR = 3; unsigned long azCmdStartTime = 0; int lastProcessedAzimuth = -9; // -9 indicates no azimuth has been processed for current satellite byte lastLettercode; // Last 30-degree mark set boolean azRotationEnabled = false; // int rotorInterfaceType = 1; // Enum 0 = Custom Mxxx message // 1 = EASYCOMM-1 format AZx 0x0 ELx // OLED #define OLED_BG BLACK #define OLED_FG WHITE #define OLED_WIDTH 128 #define OLED_HEIGHT 64 const unsigned int LINESPACE = 12; // Minimum vertical line-spacing for text const unsigned int TEXT_ROWS = 5; // Max text rows that fit vertically at default font int rowPointer = 0; // For displaying OLED text in successive rows void setup() { pinMode(LED,OUTPUT); digitalWrite(LED,HIGH); delay (ONESEC); // LM: Do not leave illuminated (hard on eyes) digitalWrite(LED, LOW); Heltec.begin(true /*DisplayEnable Enable*/, false /*LoRa Enable*/, true /*Serial Enable*/); delay(ONESEC); Heltec.display->clear(); delay(TWOSEC); oledDisplaySplash(); delay(FIVESEC); Heltec.display->clear(); Serial.begin(DEFAULT_SER_BAUD); azSerial.begin(AZ_SER_BAUD, SERIAL_8N1, RXD, TXD); clearAzSerialInterface(); initRxSerialBuffer(); IrSender.begin(DISABLE_LED_FEEDBACK); // Init infrared emitter (Antenna rotator) processTestFunctions(); Serial.println("Setup complete!"); delay(TWOSEC); oledClearScreen(); } void loop() { if (azSerial.available()) readAzSerial(); if (millis() - oledDisplayStartTime > GO_DARK_TIME) { oledClearScreen(); } } // -----------------------------------Serial----------------------------------- void clearAzSerialInterface() { while (azSerial.available()) azSerial.read(); } void initRxSerialBuffer() { for (unsigned int i=0; i= 32 && c < 127) { rxBufferNdx = rxBufferNdx % RX_BUFFER_SIZE; Serial.print("["); Serial.print(rxBufferNdx); Serial.print("] -> "); Serial.print((char) c); azRxBuffer[rxBufferNdx++] = c; } Serial.println(); } } // -------------------------------IR Interface--------------------------------- byte deg2IR(int iDeg) { // Convert azimuth to neareast 30-deg IR code that is less than iDeg return (iDeg % 360) / 30 + 0x4; } byte nRepeats(int iDeg) { // Convert number of repeats of arrow key <- or -> code to move iDeg [0 < iDeg < 30] return nPresses(iDeg) - 1; } byte nPresses(int iDeg) { // Convert number of of arrow key <- or -> button presses to move iDeg [0 < iDeg < 30] // return (iDeg % 30) / DEG_PER_ARROW_BUTTON_PRESS; return (iDeg % 30) * PRESSES_PER_DEGREE_NUMERATOR / PRESSES_PER_DEGREE_DENOMINATOR; } void smoothRotateCW(int iDeg) { // Pending additional calibration /* Serial.println("In smoothRotateCW"); Serial.print("Parameter: "); Serial.println(iDeg); */ byte nRepetitions = 0x0; // Kludge for (unsigned int i=0; i 15 && iDeg < 330) jDeg += 30; return jDeg; } void initAzimuth(int iDeg) { Serial.println(); Serial.print("In initAzimuth("); Serial.print(iDeg); Serial.println(")"); azRotationEnabled = true; azCmdStartTime = millis(); int jDeg = nearest30degMult(iDeg); Serial.print("Sending code: "); Serial.print(deg2IR(jDeg), HEX); Serial.print(" - Target: "); Serial.print(jDeg); Serial.println("."); displayApproxAzimuth(jDeg); azCmdStartTime = millis(); IrSender.sendNEC(rAddress, deg2IR(jDeg), 0x0); // Worst case time allowance for initial setup while(millis() - azCmdStartTime < (AZ_REV_TIME / 2)) delay(1); Serial.print("Initial rotation time: "); Serial.print(millis() - azCmdStartTime); Serial.println(" mSec"); lastProcessedAzimuth = jDeg; Serial.print("Initialization complete: "); Serial.print("Last processed azimuth = "); Serial.print(lastProcessedAzimuth); Serial.println(" degrees."); updateAzimuth(iDeg); } boolean updateThresholdMet(int iDeg) { int i = iDeg < 0 ? -iDeg : iDeg; int j = lastProcessedAzimuth - i; j = j < 0 ? -j : j; return j >= MIN_AZ_DELTA; } void updateAzimuth(int iDeg) { Serial.println(); Serial.print("In updateAzimuth("); Serial.print(iDeg); Serial.println(")"); if (!updateThresholdMet(iDeg) ) { Serial.println("No update required."); return; } displayTargetAzimuth(iDeg); int jDeg = nearest30degMult(iDeg); int kDeg = nearest30degMult(lastProcessedAzimuth); int numP; if (jDeg != kDeg) { Serial.print("[Re]setting to "); Serial.print(jDeg); Serial.println(" degrees."); displayApproxAzimuth(jDeg); azCmdStartTime = millis(); // Assume at most one letter difference for pacing IrSender.sendNEC(rAddress, deg2IR(jDeg), 0x0); while(millis() - azCmdStartTime < FIVESEC) delay(1); lastProcessedAzimuth = jDeg; } // int deltaAz = iDeg - lastProcessedAzimuth; Serial.print("deltaAz: "); Serial.println(deltaAz); /* int absDeltaAz = (deltaAz < 0) ? 30+deltaAz : deltaAz; Serial.print("absDeltaAz: "); Serial.println(absDeltaAz); */ if (deltaAz > 0) { numP = nPresses(deltaAz); Serial.print("Rotating clockwise approximately "); Serial.print(deltaAz); Serial.print(" degrees. "); /* Serial.print(numP); Serial.println(" button presses."); for (unsigned int i=0; isetColor(OLED_BG); Heltec.display->fillRect(0, y0, OLED_WIDTH, y1); Heltec.display -> display(); } void oledDisplayText(int rowNumber, String sTxt) { int16_t y0 = rowNumber*LINESPACE; Heltec.display->setColor(OLED_FG); // Alignment? Font? Heltec.display->drawString(0, y0, sTxt); Heltec.display -> display(); oledDisplayStartTime = millis(); } void oledDisplayVertialTicker(String sTxt) { // Display text at rowPointer (current row), increment rowPointer, erase next row. int nextRow = (rowPointer +1) % TEXT_ROWS; oledDisplayStartTime = millis(); oledEraseRow(nextRow); // Erase before draw to protect letter stems oledDisplayText(rowPointer, sTxt); rowPointer = nextRow; } void oledClearScreen() { Heltec.display -> clear(); Heltec.display -> display(); } void oledDisplaySplash() { oledDisplayText(1, " IR Azimuth Controller"); oledDisplayText(3, " W A 4 E F S"); oledDisplayText(4, " https://www.lloydm.net"); } // ---------------------------------Debug/Test--------------------------------- void testIRemitter() { Serial.println(); Serial.println(F("START " __FILE__ " from " __DATE__ "\r\nUsing library version " VERSION_IRREMOTE)); Serial.print(F("Send IR signals at pin ")); Serial.println(IR_SEND_PIN); Heltec.display -> drawString(0, 4*9, "IR Emitter Test"); Heltec.display -> display(); for (int i=0; i<5; i++) { Serial.print("Sending IR command: "); Serial.print(rCommand, HEX); Serial.print(" to address: "); Serial.println(rAddress, HEX); IrSender.sendNEC(rAddress, rCommand, rRepeats); if (rCommand == 0x4) rCommand = 0x5; else rCommand = 0x4; delay(TWOSEC); } oledClearScreen(); Serial.println(); } void processTestRxBuffer() { for (unsigned int i=0; i drawString(0, 5*9, "RxSerial Test"); Heltec.display -> display(); } if (nRepeatsTest) testNrepeats(10); if (arrowKeysTest) testArrowKeys(); if (oledTextTest) { oledClearScreen(); testDisplayEraseText(); } if (newArrowKeysRepetitionsTest) { oledClearScreen(); newArrowKeysTest(10); } } // Heltec OLED display functions not included in factory test //https://www.sabulo.com/sb/esp32-development-board/how-to-use-the-heltec-oled-display-on-the-esp32/ void fillRect(void) { uint8_t color = 1; for (int16_t i = 0; i < DISPLAY_HEIGHT / 2; i += 3) { // alternate colors Heltec.display->setColor((color % 2 == 0) ? BLACK : WHITE); Heltec.display->fillRect(i, i, DISPLAY_WIDTH - i * 2, DISPLAY_HEIGHT - i * 2); Heltec.display->display(); delay(10); color++; } // Reset back to WHITE Heltec.display->setColor(WHITE); }