Реаниматор модуля HC-12

В своем проекте использую HC-12 радио модуль. Давно игрался с ним и когда настало время использовать его столкнулся с тем что я не знаю какие в нем параметры установлены. Через ПК по UART можно подключаиться но это долго и не удобно. Решил написать код, который сканирует скорость, выводит в лог на экран и сбрасывает на дефолтные настройки модуль.

Железо

  • Arduino Nano
  • LCD 5110Nokia (красная плата)
  • HC-12

Питание на модуль и LCD - 5В

Мой LCD включает подсветку если сигнал “light/led” кидать на землю.

Собрано на макетке

Подключение указано в коде

#include <Arduino.h>
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_PCD8544.h>
#include <SoftwareSerial.h>

#define HC12_TX_PIN   2   // TX  модуля HC-12 -> D2 Nano
#define HC12_RX_PIN   3   // RX  модуля HC-12 -> D3 Nano
#define HC12_SET_PIN  4   // SET модуля HC-12 -> D4 Nano

// Инициализация дисплея: (CLK=8, DIN=9, D/C=10, CE=11, RST=12)
Adafruit_PCD8544 display = Adafruit_PCD8544(8, 9, 10, 11, 12);

SoftwareSerial hc12Serial(HC12_TX_PIN, HC12_RX_PIN);

const uint32_t bauds[] = {1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200};
const uint8_t baudsCount = 8;

char respBuf[64]; // Увеличенный буфер для приема длинных ответов (например, AT+RX)

// Функция для быстрой печати 5 строк 
void updateScreen(const String& line1, const String& line2 = "", const String& line3 = "", const String& line4 = "", const String& line5 = "") {
    display.clearDisplay();
    display.setCursor(0, 0);
    display.println(line1);
    display.println(line2);
    display.println(line3);
    display.println(line4);
    display.println(line5);
    display.display();
}

// Функция чистого чтения ответа модуля без символов \r и \n
uint8_t readResponse(uint16_t timeoutMs) {
    uint8_t idx = 0;
    unsigned long startTime = millis();
    while (millis() - startTime < timeoutMs && idx < 63) {
        if (hc12Serial.available()) {
            char c = hc12Serial.read();
            if (c != '\r' && c != '\n') {
                respBuf[idx++] = c;
            }
        }
    }
    respBuf[idx] = '\0';
    return idx;
}

// Функция для красивого парсинга и вывода конфигурации на дисплей
void printFormattedConfig(const String& title, String configStr) {
    display.clearDisplay();
    display.setCursor(0, 0);
    display.println(title); // Печатаем наш кастомный заголовок

    // Ищем позиции ключевых меток в строке
    int posB  = configStr.indexOf("OK+B");
    int posRC = configStr.indexOf("OK+RC");
    int posRP = configStr.indexOf("OK+RP:");
    int posFU = configStr.indexOf("OK+FU");

    // 1. Вывод скорости (Baud)
    if (posB != -1 && posRC != -1) {
      String baud = configStr.substring(posB + 4, posRC);
      display.print("Baud: "); display.print(baud); display.println("bps"); 
    }
    
    // 2. Вывод канала (Chan)
    if (posRC != -1 && posRP != -1) {
      String chan = configStr.substring(posRC + 5, posRP);
      display.print("Chan: "); display.println(chan);
    }
    
    // 3. Вывод мощности (Pwr) и перевод в мВт (mW)
    if (posRP != -1 && posFU != -1) {
      String pwr = configStr.substring(posRP + 6, posFU);
      pwr.trim(); 
      
      display.print("Pwr:  "); display.println(pwr); 

      display.print("      "); 
      if (pwr == "-1dBm" || pwr == "-1 dBm") display.println("(0.8mW)");
      else if (pwr == "2dBm"  || pwr == "2 dBm"  || pwr == "+2dBm"  || pwr == "+2 dBm")  display.println("(1.6mW)");
      else if (pwr == "5dBm"  || pwr == "5 dBm"  || pwr == "+5dBm"  || pwr == "+5 dBm")  display.println("(3.2mW)");
      else if (pwr == "8dBm"  || pwr == "8 dBm"  || pwr == "+8dBm"  || pwr == "+8 dBm")  display.println("(6.3mW)");
      else if (pwr == "11dBm" || pwr == "11 dBm" || pwr == "+11dBm" || pwr == "+11 dBm") display.println("(12mW)");
      else if (pwr == "14dBm" || pwr == "14 dBm" || pwr == "+14dBm" || pwr == "+14 dBm") display.println("(25mW)");
      else if (pwr == "17dBm" || pwr == "17 dBm" || pwr == "+17dBm" || pwr == "+17 dBm") display.println("(50mW)");
      else if (pwr == "20dBm" || pwr == "20 dBm" || pwr == "+20dBm" || pwr == "+20 dBm") display.println("(100mW)");
      else display.println("(unknown)"); 
    }
    
    // 4. Вывод режима работы (Mode) и тока потребления
    if (posFU != -1) {
      String mode = configStr.substring(posFU + 5);
      mode.trim(); 
      
      display.print("Mode: FU"); display.print(mode); 
      
      if (mode == "1")      display.println(" 3.6mA");
      else if (mode == "2") display.println(" 80uA");
      else if (mode == "3") display.println(" 16mA");
      else if (mode == "4") display.println(" 16mA");
      else                  display.println("NaN");
    }

    // Резервный вывод, если строка пришла в битом формате
    if (posB == -1 && posRC == -1) {
      display.println(configStr);
    }

    display.display();
}

void setup() {
    // Настройка пина SET (D4) через регистры
    DDRD |= (1 << HC12_SET_PIN);    // Выход
    PORTD |= (1 << HC12_SET_PIN);   // Исходно HIGH

    // --- АППАРАТНАЯ БЛОКИРОВКА РОМАШКИ ---
    DDRB |= (1 << DDB3);     // Пин D11 на ВЫХОД
    PORTB |= (1 << PORTB3);  // Устанавливаем HIGH (запрет приема данных дисплеем)
 
    // Инициализация дисплея
    display.begin();
    display.clearDisplay(); 

    // Разрешаем дисплею принимать данные (опускаем CE в LOW)
    PORTB &= ~(1 << PORTB3); 
    display.display(); 

    display.setContrast(55); 
    display.setTextSize(1);
    display.setTextColor(BLACK);

    Serial.begin(9600);
    delay(500);

    Serial.println(F("=================================================="));
    Serial.println(F("       HC-12 SETTINGS REANIMATOR START            "));
    Serial.println(F("=================================================="));

    Serial.println(F("[SYSTEM] Welcome screen..."));
    updateScreen("HC-12", "REANIMATOR", "v2.1", "Initializing..");
    delay(2000);

    // Перебор скоростей
    bool speedFound = false;
    uint32_t correctBaud = 0;
    String hc12Version = "Ver: Unknown"; 

    Serial.println(F("[SCAN] Starting brute-force scan..."));
    for (uint8_t i = 0; i < baudsCount; i++) {
        uint32_t testBaud = bauds[i];
        
        Serial.print(F("[SCAN] Testing: ")); 
        Serial.print(testBaud); 
        Serial.println(F(" baud..."));

        updateScreen("Scanning...", "Baudrate:", String(testBaud));

        hc12Serial.begin(testBaud);
        delay(50);

        PORTD &= ~(1 << HC12_SET_PIN);  
        delay(100); 

        while (hc12Serial.available()) { hc12Serial.read(); }

        hc12Serial.print("AT");
        uint8_t len = readResponse(200);

        if (len >= 2 && strncmp(respBuf, "OK", 2) == 0) {
            Serial.print(F("[SCAN] SUCCESS! Module responded OK at "));
            Serial.println(testBaud);
            correctBaud = testBaud;
            speedFound = true;

            while(hc12Serial.available()) hc12Serial.read(); 
            hc12Serial.print("AT+V");
            readResponse(1000);
            Serial.println(respBuf);

            hc12Version = "Ver: " + String(respBuf);
            break; 
        } else {
            PORTD |= (1 << HC12_SET_PIN);
            delay(50);
            hc12Serial.end();
        }
    }

    if (!speedFound) {
        Serial.println(F("[CRITICAL ERROR] No connection. Check the wires."));
        updateScreen("No connection.", "Check the", "wires.");
        while (true); 
    }

    updateScreen("Connected!", "Baud: " + String(correctBaud), hc12Version);
    delay(5000);
   
    // Запросим настройки командой AT+RX
    Serial.println(F("[SYSTEM] Requesting settings: AT+RX"));
    while (hc12Serial.available()) { hc12Serial.read(); }
    hc12Serial.print("AT+RX");
    
    uint8_t len = readResponse(1000);
    Serial.print(F("[RX]: "));
    Serial.println(respBuf);

    if (strstr(respBuf, "OK") == NULL) {
        Serial.println(F("[ERROR] Command failed. No 'OK' in response."));
        updateScreen("ERROR COMMAND", "Sent:", "AT+RX");
        while (true);
    }

    String firstConfig = String(respBuf);

    // Сверяемся с дефолтными настройками
    if (firstConfig.indexOf("B9600") != -1 && firstConfig.indexOf("C001") != -1 && firstConfig.indexOf("+20dBm") != -1 && firstConfig.indexOf("FU3") != -1) {
        Serial.println(F("[STATUS] SETTINGS IS DEFAULT"));
        updateScreen("SETTINGS IS", "DEFAULT");
        delay(2000);

        Serial.println(F("[SYSTEM] Displaying config. Freezing."));
        
        // ВЫЗОВ ФУНКЦИИ ФОРМАТИРОВАНИЯ ДЛЯ ТЕКУЩИХ НАСТРОЕК
        printFormattedConfig("CURRENT CONFIG", firstConfig);
        
        while (true);
    } 
    else {
        // Если настройки не дефолтные -> отправляем AT+DEFAULT
        Serial.println(F("[STATUS] Custom settings found. Resetting..."));
        updateScreen("SETTINGS IS", "RESETTING");
        
        while (hc12Serial.available()) { hc12Serial.read(); }
        hc12Serial.print("AT+DEFAULT");
        
        len = readResponse(1000);
        Serial.print(F("[RX]: "));
        Serial.println(respBuf);

        if (strstr(respBuf, "OK") == NULL) {
            Serial.println(F("[ERROR] Command failed. No 'OK' in response."));
            updateScreen("ERROR COMMAND", "Sent:", "AT+DEFAULT");
            while (true);
        }

        if (strcmp(respBuf, "OK+DEFAULT") == 0) {
            Serial.println(F("[SYSTEM] RESET SUCCESSFULY"));
            updateScreen("RESET", "SUCCESSFULY");
            delay(2000);

            // После AT+DEFAULT скорость HC-12 переключается на 9600.
            // Переинициализируем SoftwareSerial на дефолтную скорость 9600, чтобы успешно считать ответ.
            hc12Serial.end();
            delay(50);
            hc12Serial.begin(9600);
            delay(50);

            // Запрашиваем настройки заново командой AT+RX
            Serial.println(F("[SYSTEM] Requesting final settings..."));
            while (hc12Serial.available()) { hc12Serial.read(); }
            hc12Serial.print("AT+RX");
            
            len = readResponse(1000);
            Serial.print(F("[RX Final]: "));
            Serial.println(respBuf);

            // ВЫЗОВ ФУНКЦИИ ФОРМАТИРОВАНИЯ ДЛЯ СБРОШЕННЫХ НАСТРОЕК
            printFormattedConfig("DEFAULT CONFIG", String(respBuf));
            
            while (true);
        } 
        else {
            Serial.println(F("[ERROR] Unexpected response on reset."));
            updateScreen("ERROR COMMAND", "Sent:", "AT+DEFAULT");
            while (true);
        }
    }
}

void loop() {}

Если дисплей не подключать то вывод можно увидеть в консоли.

Реаниматор проверен в работе.

Подключение произведено к ArdunioNano (ATmega328P).

На фото HС-12 (а то можно подумать что это “демка”)

а для чего это ? определять стандартную скорость модуля ? что бы в коде ее указывать при подключении… может она там сбивается после перезагрузки… или для чего ?

Я делаю систему безопасности. В нем используется HC-12. Смоделирую случаи когда это нужно:

  1. нужно убедиться (после покупки) что модули рабочие

1.1) глянуть версию

  1. нужно сбросить в дефолт настройки (заодно вспомнить какие они) для того чтобы вставить в плату где в коде прошита работа на дефолт

В целом, этот подход можно интегрировать в рабочий код где кнопка сброса до заводских настроек платы (Вашей) также сбросит и настройки HC-12.

В правильных проектах должна быть кнопка сброса до заводских настроек (особенно удобно при отладке)

Т.е. найти ардуину, найти дисплей, спаять - это всё быстрее выходит?! :thinking:
И чтобы поменять настройки всё равно надо к компу подключать ибо кнопок чёй-то не наблюдается?

То есть в итоге все равно надо подключать ПК по UART, только теперь к вашей поделке, а не к модулю напрямую? :slight_smile: За что боролись?

Хотя в плане самообучения ничего против подобного проекта не имею :slight_smile:

ЗЫ Код не смотрел, если чо

Я в процессе разработки. У меня всё под рукой. Я на макетке сделал (НЕ ПАЯЛ) и пользую. Мне надо штук 5 ставить. Купил 10. 2 только проверил. Остальные проверить будет удобно. Настройки менять, в данной версии нет. Задача специфична.

Ну, я при разработке питалово подаю от USB а раз так, то и консоль под рукой. Просто для терминала надо прогу отдельно запускать и вспоминать че там. У нас у всех ПК включен :grin:

не понял. А в чем разница в данном случае между “консолью” и “терминалом”?
Что мешает общаться модулем через ардуино-консоль?

В среде разработки (Arduino IDE) уже есть консоль. Нет нужды запускать отдельно прогу.

вот именно
Вот прям в Ардуино и соединяйтесь с модулем, зачем еще что-то?

Попробую ответить иначе. В разработке была задача "поработать"с настройками. потом выросла идея сделать реаниматор как побочный продукт общей задачи. И вот опубликована тема. Как было сказано выше этот код можно имплементировать в ваши проекты. У меня ещё задача была, как узнать зашумленность канала, накидал сканнер. Тоже, считайте, побочный продукт разработки основного проекта.

лучше не надо. Код расходует оперативу, как будто ее на Ардуине гигабайты, а не всего 2К

В моем проекте используется ATmega2560)

ну это не повод выкидывать память.
Особенно, если вы предлагаете свой код для использования другим.

Это специфичная задача. С успехом можно использовать код или его часть. Кому как надо. Главное идея. Например, код проверки связи с модулем. Отправил команду AT получил OK

Во-первых, тогда его лучше оформить библиотекой. И во-вторых - куча литералов в динамической памяти никуда не годится. Все текстовые строки таки нужно упаковать в PROGMEM - во флеше им самое место

По стилю гайвера. Но, это уже другая история. Главное идея. Конечно можно в F запихать.

Да-да, и ответы тоже в его стиле.
Вместо того чтоб прислушаться, когда вам указывают на очевидные косяки - вы будете спорить и доказывать. что все так и задумано.

Если вы не в курсе, выражение “стиль гайвера” на этом форуме - синоним кривого ненадежного софта, от которого в любой момент можно ждать подставы.
“Тяп-ляп - в продакшен!”

И какая же тут идея? Нагородить кучу софта и железа для элементарной операции? :slight_smile:
вы реально думаете, что в вашей “идее” есть хоть что-то новое? :slight_smile:

да что же вы так не любите то его ?)))
он может быть гений который специально делает так что бы коды сложно модернизировались, и от них страдали те кто лентяй…
и он и не обещал вроде что основа кода будет офигенная, он вроде только видосики готового проекта выкладывает, а дальше у кого будет желание будет предлагать свои версии… кто не лентяй и найдет время сделает лучше…

https://community.alexgyver.ru/forums/gyver/

все проблемы от лени! но это не точно)))