commit b48f58892118dfbeb7e7004a4c39946c9e4ba44a Author: ua1zbe Date: Sat Apr 4 20:59:12 2026 +0000 Initial commit: LoRa APRS Beacon для T-Beam V1.2 - Статический маяк с координатами 67.369281N, 32.497063E - Отправка каждые 10 минут + при включении - APRS символ L в чёрном ромбике, позывной UA1ZBE-TS - Дисплей SH1106 1.3 повёрнутый - Мониторинг напряжения батареи в сообщении - Кнопка для ручной отправки - Частота 433.775 MHz, SF12, BW125 Co-authored-by: Qwen-Coder diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md new file mode 100644 index 0000000..ede9099 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# LoRa_APRS_Beacon + +LoRa APRS Beacon для T-Beam V1.2 (AXP2101) с дисплеем SH1106 1.3. Статический маяк с координатами, отправка каждые 10 минут, мониторинг батареи, кнопка для ручной отправки. Позывной UA1ZBE-TS, частота 433.775 MHz. \ No newline at end of file diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..83824f8 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,23 @@ +[platformio] +default_envs = ttgo-t-beam-AXP2101-v1_2 + +[env] +platform = espressif32 @ 6.7.0 +framework = arduino +lib_ldf_mode = deep+ +monitor_speed = 115200 +monitor_filters = esp32_exception_decoder +lib_deps = + thingpulse/ESP8266 and ESP32 OLED driver for SSD1306 displays @ ^4.5.0 + bblanchon/ArduinoJson @ 7.0.4 + lewisxhe/XPowersLib @ 0.2.4 + sandeepmistry/LoRa @ 0.8.0 + shaggydog/OneButton @ 1.5.0 +check_tool = cppcheck +check_flags = + cppcheck: --suppress=*:*.pio\* --inline-suppr -DCPPCHECK +check_skip_packages = yes + +[env:ttgo-t-beam-AXP2101-v1_2] +board = ttgo-t-beam +build_flags = -Werror -Wall -DTTGO_T_Beam_V1_2 diff --git a/src/display.cpp b/src/display.cpp new file mode 100644 index 0000000..7d89562 --- /dev/null +++ b/src/display.cpp @@ -0,0 +1,72 @@ +#include +#include + +#include "display.h" +#include "pins.h" + +SH1106Wire display(0x3c, OLED_SDA, OLED_SCL); + +void setup_display() { + pinMode(OLED_RST, OUTPUT); + digitalWrite(OLED_RST, LOW); + delay(20); + digitalWrite(OLED_RST, HIGH); + + // Wire уже инициализирован в main.cpp + display.init(); + display.flipScreenVertically(); + display.clear(); + display.setFont(ArialMT_Plain_10); + display.setColor(WHITE); + display.drawString(0, 0, "LORA APRS BEACON"); + display.display(); +} + +void display_toggle(bool toggle) { + if (toggle) { + display.displayOn(); + } else { + display.displayOff(); + } +} + +void show_display(String line1, int wait) { + display.clear(); + display.setFont(ArialMT_Plain_16); + display.drawString(0, 0, line1); + display.display(); + delay(wait); +} + +void show_display(String line1, String line2, int wait) { + display.clear(); + display.setFont(ArialMT_Plain_16); + display.drawString(0, 0, line1); + display.setFont(ArialMT_Plain_10); + display.drawString(0, 18, line2); + display.display(); + delay(wait); +} + +void show_display(String line1, String line2, String line3, int wait) { + display.clear(); + display.setFont(ArialMT_Plain_16); + display.drawString(0, 0, line1); + display.setFont(ArialMT_Plain_10); + display.drawString(0, 18, line2); + display.drawString(0, 28, line3); + display.display(); + delay(wait); +} + +void show_display(String line1, String line2, String line3, String line4, int wait) { + display.clear(); + display.setFont(ArialMT_Plain_16); + display.drawString(0, 0, line1); + display.setFont(ArialMT_Plain_10); + display.drawString(0, 18, line2); + display.drawString(0, 28, line3); + display.drawString(0, 38, line4); + display.display(); + delay(wait); +} diff --git a/src/display.h b/src/display.h new file mode 100644 index 0000000..0b39f9d --- /dev/null +++ b/src/display.h @@ -0,0 +1,11 @@ +#ifndef DISPLAY_H_ +#define DISPLAY_H_ + +void setup_display(); +void display_toggle(bool toggle); +void show_display(String line1, int wait = 0); +void show_display(String line1, String line2, int wait = 0); +void show_display(String line1, String line2, String line3, int wait = 0); +void show_display(String line1, String line2, String line3, String line4, int wait = 0); + +#endif diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..de5a2c1 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,227 @@ +#include +#include +#include +#include + +#include "display.h" +#include "pins.h" +#include "power_management.h" + +#define VERSION "1.0.0" + +// ===== НАСТРОЙКИ ===== +#define CALLSIGN "UA1ZBE-TS" +#define PATH "WIDE1-1" +#define MESSAGE "Test 433.775MHz Lora aprs" +#define SYMBOL 'L' +#define OVERLAY '\\' +#define BEACON_INTERVAL_SEC 600 // 10 минут + +// Координаты (статические) +#define LAT_DEG 67 +#define LAT_MIN 22.1569 // 67.369281 = 67° 22.1569' +#define LON_DEG 32 +#define LON_MIN 29.8238 // 32.497063 = 32° 29.8238' +#define LAT_DIR 'N' +#define LON_DIR 'E' + +// LoRa настройки +#define LORA_FREQ 433775000 +#define LORA_SF 12 +#define LORA_BW 125000 +#define LORA_CR 5 +#define LORA_POWER 20 + +// ===== ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ ===== +#ifdef TTGO_T_Beam_V1_2 +AXP2101 axp; +PowerManagement *powerManagement = &axp; +#endif + +OneButton userButton = OneButton(BUTTON_PIN, true, true); + +static bool batteryConnected = false; +static String batteryVoltage = ""; +static unsigned long lastBeaconTime = 0; + +// ===== APRS кодирование координат ===== +String create_lat_aprs() { + char buf[20]; + sprintf(buf, "%02d%05.2f%c", LAT_DEG, LAT_MIN, LAT_DIR); + return String(buf); +} + +String create_lon_aprs() { + char buf[20]; + sprintf(buf, "%03d%05.2f%c", LON_DEG, LON_MIN, LON_DIR); + return String(buf); +} + +String createTimeString(unsigned long seconds) { + unsigned long h = (seconds / 3600) % 24; + unsigned long m = (seconds / 60) % 60; + unsigned long s = seconds % 60; + + char buf[20]; + sprintf(buf, "%02lu:%02lu:%02lu", h, m, s); + return String(buf); +} + +// ===== Функция отправки маяка ===== +void send_beacon() { + String lat = create_lat_aprs(); + String lon = create_lon_aprs(); + + // Получаем напряжение батареи + String batInfo = ""; + #ifdef TTGO_T_Beam_V1_2 + float batV = powerManagement->getBatteryVoltage(); + if (batV > 3.0) { + batInfo = " Bat: " + String(batV, 1) + "V"; + } else { + batInfo = " Bat: USB"; + } + #endif + + String aprsmsg = "!" + lat + String(OVERLAY) + lon + String(SYMBOL) + " " + String(MESSAGE) + batInfo; + + // Заголовок LoRa APRS + String lora_header = "<"; + lora_header += char(0xFF); + lora_header += char(0x01); + + // APRS пакет: адрес назначения + адрес источника + путь + данные + String packet = String(CALLSIGN) + ">APLT00," + String(PATH) + ":" + aprsmsg; + + Serial.println("===== TX PACKET ====="); + Serial.println(packet); + Serial.println("====================="); + Serial.println(); + + // LoRa передача + LoRa.beginPacket(); + LoRa.write((const uint8_t *)lora_header.c_str(), lora_header.length()); + LoRa.write((const uint8_t *)packet.c_str(), packet.length()); + LoRa.endPacket(); + + Serial.println(">>> Packet sent via LoRa"); + Serial.println(); + + show_display("<< TX >>", CALLSIGN, "Sent at: " + createTimeString(millis() / 1000)); + + lastBeaconTime = millis(); +} + +// ===== SETUP ===== +void setup() { + Serial.begin(115200); + Serial.flush(); + delay(100); + for (int i = 0; i < 3; i++) { + Serial.println("Booting... " + String(i + 1)); + delay(500); + } + + Serial.println(); + Serial.println("LoRa APRS Beacon v" VERSION); + Serial.println("Starting..."); + + // Инициализация PMU - ВАЖНО: сначала Wire! +#if defined(TTGO_T_Beam_V1_2) + Serial.println("Initializing PMU..."); + Wire.begin(OLED_SDA, OLED_SCL, 100000); + delay(100); + if (powerManagement->begin(Wire)) { + Serial.println("PMU init OK"); + } else { + Serial.println("PMU init FAILED, continuing anyway"); + } + powerManagement->activateLoRa(); + // Не включаем GPS - он нам не нужен + powerManagement->deactivateGPS(); + powerManagement->activateOLED(); + powerManagement->activateMeasurement(); +#endif + + // Выключаем WiFi и BT + // WiFi.mode(WIFI_OFF); + // btStop(); + + // Дисплей + Serial.println("Initializing display..."); + setup_display(); + show_display("LoRa APRS Beacon", "v" VERSION, CALLSIGN, 2000); + + // LoRa + Serial.println("Initializing LoRa..."); + SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS); + LoRa.setPins(LORA_CS, LORA_RST, LORA_IRQ); + + if (!LoRa.begin(LORA_FREQ)) { + Serial.println("ERROR: LoRa init failed!"); + show_display("ERROR", "LoRa init failed!", 0); + while (true) { + delay(1000); + } + } + LoRa.setSpreadingFactor(LORA_SF); + LoRa.setSignalBandwidth(LORA_BW); + LoRa.setCodingRate4(LORA_CR); + LoRa.enableCrc(); + LoRa.setTxPower(LORA_POWER); + Serial.printf("LoRa OK - Freq: %d, SF: %d, BW: %d, CR: %d, PWR: %d\n", + (int)LORA_FREQ, (int)LORA_SF, (int)LORA_BW, (int)LORA_CR, (int)LORA_POWER); + + show_display("LoRa OK", "Freq: 433.775 MHz", "SF: 12", 2000); + + // Кнопка + userButton.attachClick([]() { + Serial.println("Button pressed - manual TX"); + send_beacon(); + }); + + // Отправляем первый маяк сразу при старте + Serial.println(); + Serial.println("=============================="); + Serial.println(" LoRa APRS Beacon starting"); + Serial.println(" Callsign: " CALLSIGN); + Serial.println(" Interval: 10 min"); + Serial.println(" Freq: 433.775 MHz"); + Serial.println("=============================="); + Serial.println(); + + send_beacon(); +} + +// ===== LOOP ===== +void loop() { + userButton.tick(); + + unsigned long now = millis(); + + // Проверка интервала маяка (10 минут) + if (now - lastBeaconTime >= BEACON_INTERVAL_SEC * 1000UL) { + send_beacon(); + } + + // Обновление дисплея каждую секунду + static unsigned long lastDisplayUpdate = 0; + if (now - lastDisplayUpdate >= 1000) { + lastDisplayUpdate = now; + + unsigned long uptime = now / 1000; + unsigned long nextBeacon = (BEACON_INTERVAL_SEC * 1000UL - (now - lastBeaconTime)) / 1000; + + #ifdef TTGO_T_Beam_V1_2 + batteryConnected = powerManagement->isBatteryConnect(); + if (batteryConnected) { + batteryVoltage = String(powerManagement->getBatteryVoltage(), 2); + } + #endif + + show_display(CALLSIGN, + "Uptime: " + createTimeString(uptime), + "Next: ~" + createTimeString(nextBeacon), + batteryConnected ? ("Bat: " + batteryVoltage + "V") : "Powered via USB"); + } +} diff --git a/src/pins.h b/src/pins.h new file mode 100644 index 0000000..4690ece --- /dev/null +++ b/src/pins.h @@ -0,0 +1,24 @@ +#ifndef PINS_H_ +#define PINS_H_ + +#undef OLED_SDA +#undef OLED_SCL +#undef OLED_RST + +#define OLED_SDA 21 +#define OLED_SCL 22 +#define OLED_RST 16 + +#define BUTTON_PIN 38 + +#ifdef TTGO_T_Beam_V0_7 +#define GPS_RX 15 +#define GPS_TX 12 +#endif + +#if defined(TTGO_T_Beam_V1_0) || defined(TTGO_T_Beam_V1_2) +#define GPS_RX 12 +#define GPS_TX 34 +#endif + +#endif diff --git a/src/power_management.cpp b/src/power_management.cpp new file mode 100644 index 0000000..127247a --- /dev/null +++ b/src/power_management.cpp @@ -0,0 +1,98 @@ +#include + +#include "power_management.h" + +AXP2101::AXP2101() { +} + +bool AXP2101::begin(TwoWire &port) { + _pmu = new XPowersAXP2101(port); + if (!_pmu->init()) { + delete _pmu; + _pmu = 0; + return false; + } + + _pmu->disablePowerOutput(XPOWERS_DCDC2); + _pmu->disablePowerOutput(XPOWERS_DCDC3); + _pmu->disablePowerOutput(XPOWERS_DCDC4); + _pmu->disablePowerOutput(XPOWERS_DCDC5); + _pmu->disablePowerOutput(XPOWERS_ALDO1); + _pmu->disablePowerOutput(XPOWERS_ALDO4); + _pmu->disablePowerOutput(XPOWERS_BLDO1); + _pmu->disablePowerOutput(XPOWERS_BLDO2); + _pmu->disablePowerOutput(XPOWERS_DLDO1); + _pmu->disablePowerOutput(XPOWERS_DLDO2); + + _pmu->setPowerChannelVoltage(XPOWERS_VBACKUP, 3300); + _pmu->enablePowerOutput(XPOWERS_VBACKUP); + + _pmu->setPowerChannelVoltage(XPOWERS_ALDO2, 3300); + _pmu->enablePowerOutput(XPOWERS_ALDO2); + + _pmu->setPowerChannelVoltage(XPOWERS_ALDO3, 3300); + _pmu->enablePowerOutput(XPOWERS_ALDO3); + + _pmu->disableIRQ(XPOWERS_AXP2101_ALL_IRQ); + + _pmu->setChargerConstantCurr(XPOWERS_AXP2101_CHG_CUR_800MA); + _pmu->setChargeTargetVoltage(XPOWERS_AXP2101_CHG_VOL_4V2); + _pmu->setChargingLedMode(XPOWERS_CHG_LED_CTRL_CHG); + + return true; +} + +void AXP2101::activateLoRa() { + if (!_pmu) return; + _pmu->enablePowerOutput(XPOWERS_ALDO2); +} + +void AXP2101::deactivateLoRa() { + if (!_pmu) return; + _pmu->disablePowerOutput(XPOWERS_ALDO2); +} + +void AXP2101::activateGPS() { + if (!_pmu) return; + _pmu->enablePowerOutput(XPOWERS_ALDO3); +} + +void AXP2101::deactivateGPS() { + if (!_pmu) return; + _pmu->disablePowerOutput(XPOWERS_ALDO3); +} + +void AXP2101::activateOLED() { + if (!_pmu) return; + _pmu->enablePowerOutput(XPOWERS_DCDC1); +} + +void AXP2101::deactivateOLED() { + if (!_pmu) return; + _pmu->disablePowerOutput(XPOWERS_DCDC1); +} + +void AXP2101::activateMeasurement() { + if (!_pmu) return; + _pmu->enableBattVoltageMeasure(); +} + +void AXP2101::deactivateMeasurement() { + if (!_pmu) return; + _pmu->disableBattVoltageMeasure(); +} + +double AXP2101::getBatteryVoltage() { + if (!_pmu) return 0.0; + return _pmu->getBattVoltage() / 1000.0; +} + +bool AXP2101::isBatteryConnect() { + if (!_pmu) return false; + return _pmu->isBatteryConnect(); +} + +bool AXP2101::isCharging() { + if (!_pmu) return false; + return _pmu->isCharging(); +} diff --git a/src/power_management.h b/src/power_management.h new file mode 100644 index 0000000..a9f8b1d --- /dev/null +++ b/src/power_management.h @@ -0,0 +1,59 @@ +#ifndef POWER_MANAGEMENT_H_ +#define POWER_MANAGEMENT_H_ + +#include +#include + +class PowerManagement { +public: + ~PowerManagement() { + } + + virtual bool begin(TwoWire &port) = 0; + + virtual void activateLoRa() = 0; + virtual void deactivateLoRa() = 0; + + virtual void activateGPS() = 0; + virtual void deactivateGPS() = 0; + + virtual void activateOLED() = 0; + virtual void deactivateOLED() = 0; + + virtual void activateMeasurement() = 0; + virtual void deactivateMeasurement() = 0; + + virtual double getBatteryVoltage() = 0; + + virtual bool isBatteryConnect() = 0; + virtual bool isCharging() = 0; + +protected: + XPowersLibInterface *_pmu = 0; +}; + +class AXP2101 : public PowerManagement { +public: + AXP2101(); + + bool begin(TwoWire &port); + + void activateLoRa(); + void deactivateLoRa(); + + void activateGPS(); + void deactivateGPS(); + + void activateOLED(); + void deactivateOLED(); + + void activateMeasurement(); + void deactivateMeasurement(); + + double getBatteryVoltage(); + + bool isBatteryConnect(); + bool isCharging(); +}; + +#endif