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 <qwen-coder@alibabacloud.com>
This commit is contained in:
ua1zbe
2026-04-04 20:59:12 +00:00
committed by UA1ZBE
commit b48f588921
9 changed files with 517 additions and 0 deletions

0
.gitignore vendored Normal file
View File

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# LoRa_APRS_Beacon
LoRa APRS Beacon для T-Beam V1.2 (AXP2101) с дисплеем SH1106 1.3. Статический маяк с координатами, отправка каждые 10 минут, мониторинг батареи, кнопка для ручной отправки. Позывной UA1ZBE-TS, частота 433.775 MHz.

23
platformio.ini Normal file
View File

@@ -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

72
src/display.cpp Normal file
View File

@@ -0,0 +1,72 @@
#include <SH1106Wire.h>
#include <Wire.h>
#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);
}

11
src/display.h Normal file
View File

@@ -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

227
src/main.cpp Normal file
View File

@@ -0,0 +1,227 @@
#include <Arduino.h>
#include <LoRa.h>
#include <OneButton.h>
#include <WiFi.h>
#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");
}
}

24
src/pins.h Normal file
View File

@@ -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

98
src/power_management.cpp Normal file
View File

@@ -0,0 +1,98 @@
#include <XPowersAXP2101.tpp>
#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();
}

59
src/power_management.h Normal file
View File

@@ -0,0 +1,59 @@
#ifndef POWER_MANAGEMENT_H_
#define POWER_MANAGEMENT_H_
#include <Wire.h>
#include <XPowersLibInterface.hpp>
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