├── pics └── Tracker.png ├── .github ├── FUNDING.yml └── workflows │ └── main.yml ├── .gitignore ├── .vscode └── extensions.json ├── test └── send_nmea.py ├── src ├── pins.h ├── BeaconManager.h ├── display.h ├── power_management.h ├── BeaconManager.cpp ├── power_management.cpp ├── configuration.h ├── display.cpp ├── configuration.cpp └── LoRa_APRS_Tracker.cpp ├── platformio.ini ├── data └── tracker.json ├── LICENSE ├── .clang-format └── README.md /pics/Tracker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aprs434/lora.tracker/HEAD/pics/Tracker.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: ['peterus'] 4 | custom: ['paypal.me/peterus07'] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | .vscode/.browse.c_cpp.db* 3 | .vscode/c_cpp_properties.json 4 | .vscode/launch.json 5 | .vscode/ipch 6 | .vscode/settings.json 7 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "platformio.platformio-ide" 6 | ], 7 | "unwantedRecommendations": [ 8 | "ms-vscode.cpptools-extension-pack" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /test/send_nmea.py: -------------------------------------------------------------------------------- 1 | #!/bin/python 2 | 3 | import time 4 | import sys 5 | import serial 6 | 7 | f = open(sys.argv[1], "r") 8 | ser = serial.Serial(sys.argv[2], 115200, timeout=0) 9 | 10 | sleep_count = 0 11 | for x in f: 12 | s = ser.read(100) 13 | if s: 14 | print(s.decode(), end='') 15 | sleep_count = sleep_count + 1 16 | ser.write(x.encode()) 17 | if sleep_count > 2: 18 | time.sleep(1) 19 | sleep_count = 0 20 | ser.close() 21 | f.close() -------------------------------------------------------------------------------- /src/pins.h: -------------------------------------------------------------------------------- 1 | #ifndef PINS_H_ 2 | #define PINS_H_ 3 | 4 | #undef OLED_SDA 5 | #undef OLED_SCL 6 | #undef OLED_RST 7 | 8 | #define OLED_SDA 21 9 | #define OLED_SCL 22 10 | #define OLED_RST 16 11 | 12 | #define BUTTON_PIN 38 // The middle button GPIO on the T-Beam 13 | 14 | #ifdef TTGO_T_Beam_V0_7 15 | #define GPS_RX 15 16 | #define GPS_TX 12 17 | #endif 18 | 19 | #ifdef TTGO_T_Beam_V1_0 20 | #define GPS_RX 12 21 | #define GPS_TX 34 22 | #endif 23 | 24 | #endif 25 | -------------------------------------------------------------------------------- /src/BeaconManager.h: -------------------------------------------------------------------------------- 1 | #ifndef BEACON_MANAGER_H_ 2 | #define BEACON_MANAGER_H_ 3 | 4 | #include "configuration.h" 5 | 6 | class BeaconManager { 7 | public: 8 | BeaconManager(); 9 | 10 | void loadConfig(const std::list &beacon_config); 11 | 12 | std::list::iterator getCurrentBeaconConfig() const; 13 | void loadNextBeacon(); 14 | 15 | private: 16 | std::list _beacon_config; 17 | std::list::iterator _currentBeaconConfig; 18 | }; 19 | 20 | #endif 21 | -------------------------------------------------------------------------------- /src/display.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef DISPLAY_H_ 3 | #define DISPLAY_H_ 4 | 5 | void setup_display(); 6 | 7 | void show_display(String header, int wait = 0); 8 | void show_display(String header, String line1, int wait = 0); 9 | void show_display(String header, String line1, String line2, int wait = 0); 10 | void show_display(String header, String line1, String line2, String line3, int wait = 0); 11 | void show_display(String header, String line1, String line2, String line3, String line4, int wait = 0); 12 | void show_display(String header, String line1, String line2, String line3, String line4, String line5, int wait = 0); 13 | 14 | #endif 15 | -------------------------------------------------------------------------------- /src/power_management.h: -------------------------------------------------------------------------------- 1 | #ifndef POWER_MANAGEMENT_H_ 2 | #define POWER_MANAGEMENT_H_ 3 | 4 | #include 5 | #include 6 | 7 | class PowerManagement { 8 | public: 9 | PowerManagement(); 10 | bool begin(TwoWire &port); 11 | 12 | void activateLoRa(); 13 | void deactivateLoRa(); 14 | 15 | void activateGPS(); 16 | void deactivateGPS(); 17 | 18 | void activateOLED(); 19 | void decativateOLED(); 20 | 21 | void activateMeasurement(); 22 | void deactivateMeasurement(); 23 | 24 | double getBatteryVoltage(); 25 | double getBatteryChargeDischargeCurrent(); 26 | 27 | bool isBatteryConnect(); 28 | 29 | private: 30 | AXP20X_Class axp; 31 | }; 32 | 33 | #endif 34 | -------------------------------------------------------------------------------- /src/BeaconManager.cpp: -------------------------------------------------------------------------------- 1 | #include "BeaconManager.h" 2 | 3 | BeaconManager::BeaconManager() : _currentBeaconConfig(_beacon_config.end()) { 4 | } 5 | 6 | // cppcheck-suppress unusedFunction 7 | void BeaconManager::loadConfig(const std::list &beacon_config) { 8 | _beacon_config = beacon_config; 9 | _currentBeaconConfig = _beacon_config.begin(); 10 | } 11 | 12 | // cppcheck-suppress unusedFunction 13 | std::list::iterator BeaconManager::getCurrentBeaconConfig() const { 14 | return _currentBeaconConfig; 15 | } 16 | 17 | // cppcheck-suppress unusedFunction 18 | void BeaconManager::loadNextBeacon() { 19 | ++_currentBeaconConfig; 20 | if (_currentBeaconConfig == _beacon_config.end()) { 21 | _currentBeaconConfig = _beacon_config.begin(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | [platformio] 2 | default_envs = ttgo-t-beam-v1 3 | 4 | [env] 5 | ;upload_port = /dev/ttyUSB2 6 | platform = espressif32 @ 3.0.0 7 | framework = arduino 8 | lib_ldf_mode = deep+ 9 | monitor_speed = 115200 10 | monitor_filters = esp32_exception_decoder 11 | lib_deps = 12 | adafruit/Adafruit GFX Library @ 1.7.5 13 | adafruit/Adafruit SSD1306 @ 2.4.0 14 | bblanchon/ArduinoJson @ 6.17.0 15 | lewisxhe/AXP202X_Library @ 1.1.2 16 | sandeepmistry/LoRa @ 0.7.2 17 | peterus/APRS-Decoder-Lib @ 0.0.6 18 | mikalhart/TinyGPSPlus @ 1.0.2 19 | paulstoffregen/Time @ 1.6 20 | shaggydog/OneButton @ 1.5.0 21 | peterus/esp-logger @ 0.0.1 22 | check_tool = cppcheck 23 | check_flags = 24 | cppcheck: --suppress=*:*.pio\* --inline-suppr -DCPPCHECK 25 | check_skip_packages = yes 26 | 27 | [env:ttgo-t-beam-v1] 28 | board = ttgo-t-beam 29 | build_flags = -Werror -Wall -DTTGO_T_Beam_V1_0 30 | 31 | [env:ttgo-t-beam-v0_7] 32 | board = ttgo-t-beam 33 | build_flags = -Werror -Wall -DTTGO_T_Beam_V0_7 34 | -------------------------------------------------------------------------------- /data/tracker.json: -------------------------------------------------------------------------------- 1 | { 2 | "debug": false, 3 | "beacons": [ 4 | { 5 | "callsign": "NOCALL-12", 6 | "timeout": 1, 7 | "symbol": "[", 8 | "overlay": "/", 9 | "smart_beacon": { 10 | "active": false, 11 | "turn_min": 25, 12 | "slow_rate": 300, 13 | "slow_speed": 10, 14 | "fast_rate": 60, 15 | "fast_speed": 100, 16 | "min_tx_dist": 100, 17 | "min_bcn": 5 18 | } 19 | } 20 | ], 21 | "button": { 22 | "tx": true, 23 | "alt_message": false 24 | }, 25 | "lora": { 26 | "frequency_rx": 433775000, 27 | "frequency_tx": 433775000, 28 | "power": 20, 29 | "spreading_factor": 12, 30 | "signal_bandwidth": 125000, 31 | "coding_rate4": 5 32 | }, 33 | "ptt_output": { 34 | "active": false, 35 | "io_pin": 4, 36 | "start_delay": 0, 37 | "end_delay": 0, 38 | "reverse": false 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022- Serge Y. Stroobandt, ON4AA 4 | Copyright (c) 2020-2022 Peter Buchegger, OE5BPA 5 | Copyright (c) 2018-2021 Christian Johann Bauer, OE3CJB 6 | Copyright (c) 2018 Bernd Gasser, OE1ACM 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | -------------------------------------------------------------------------------- /src/power_management.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "power_management.h" 3 | 4 | // cppcheck-suppress uninitMemberVar 5 | PowerManagement::PowerManagement() { 6 | } 7 | 8 | // cppcheck-suppress unusedFunction 9 | bool PowerManagement::begin(TwoWire &port) { 10 | bool result = axp.begin(port, AXP192_SLAVE_ADDRESS); 11 | if (!result) { 12 | axp.setDCDC1Voltage(3300); 13 | } 14 | return result; 15 | } 16 | 17 | // cppcheck-suppress unusedFunction 18 | void PowerManagement::activateLoRa() { 19 | axp.setPowerOutPut(AXP192_LDO2, AXP202_ON); 20 | } 21 | 22 | // cppcheck-suppress unusedFunction 23 | void PowerManagement::deactivateLoRa() { 24 | axp.setPowerOutPut(AXP192_LDO2, AXP202_OFF); 25 | } 26 | 27 | // cppcheck-suppress unusedFunction 28 | void PowerManagement::activateGPS() { 29 | axp.setPowerOutPut(AXP192_LDO3, AXP202_ON); 30 | } 31 | 32 | // cppcheck-suppress unusedFunction 33 | void PowerManagement::deactivateGPS() { 34 | axp.setPowerOutPut(AXP192_LDO3, AXP202_OFF); 35 | } 36 | 37 | // cppcheck-suppress unusedFunction 38 | void PowerManagement::activateOLED() { 39 | axp.setPowerOutPut(AXP192_DCDC1, AXP202_ON); 40 | } 41 | 42 | // cppcheck-suppress unusedFunction 43 | void PowerManagement::decativateOLED() { 44 | axp.setPowerOutPut(AXP192_DCDC1, AXP202_OFF); 45 | } 46 | 47 | // cppcheck-suppress unusedFunction 48 | void PowerManagement::activateMeasurement() { 49 | axp.adc1Enable(AXP202_BATT_CUR_ADC1 | AXP202_BATT_VOL_ADC1, true); 50 | } 51 | 52 | // cppcheck-suppress unusedFunction 53 | void PowerManagement::deactivateMeasurement() { 54 | axp.adc1Enable(AXP202_BATT_CUR_ADC1 | AXP202_BATT_VOL_ADC1, false); 55 | } 56 | 57 | // cppcheck-suppress unusedFunction 58 | double PowerManagement::getBatteryVoltage() { 59 | return axp.getBattVoltage() / 1000.0; 60 | } 61 | 62 | // cppcheck-suppress unusedFunction 63 | double PowerManagement::getBatteryChargeDischargeCurrent() { 64 | if (axp.isChargeing()) { 65 | return axp.getBattChargeCurrent(); 66 | } 67 | return -1.0 * axp.getBattDischargeCurrent(); 68 | } 69 | 70 | bool PowerManagement::isBatteryConnect() { 71 | return axp.isBatteryConnect(); 72 | } 73 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | 2 | name: check and build 3 | 4 | on: [push, pull_request] 5 | 6 | jobs: 7 | PlatformIO-Check: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Cache pip 12 | uses: actions/cache@v2 13 | with: 14 | path: ~/.cache/pip 15 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} 16 | restore-keys: ${{ runner.os }}-pip- 17 | - name: Cache PlatformIO 18 | uses: actions/cache@v2 19 | with: 20 | path: ~/.platformio 21 | key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} 22 | - name: Set up Python 23 | uses: actions/setup-python@v2 24 | - name: Install PlatformIO 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install --upgrade platformio 28 | - name: Run PlatformIO Check 29 | run: platformio check --fail-on-defect low --fail-on-defect medium --fail-on-defect high 30 | 31 | PlatformIO-Build: 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v2 35 | - name: Cache pip 36 | uses: actions/cache@v2 37 | with: 38 | path: ~/.cache/pip 39 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} 40 | restore-keys: ${{ runner.os }}-pip- 41 | - name: Cache PlatformIO 42 | uses: actions/cache@v2 43 | with: 44 | path: ~/.platformio 45 | key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} 46 | - name: Set up Python 47 | uses: actions/setup-python@v2 48 | - name: Install PlatformIO 49 | run: | 50 | python -m pip install --upgrade pip 51 | pip install --upgrade platformio 52 | - name: Run PlatformIO CI 53 | run: platformio run 54 | - uses: actions/upload-artifact@v2 55 | with: 56 | name: firmware 57 | path: .pio/build/*/firmware.bin 58 | 59 | formatting-check: 60 | name: Formatting Check 61 | runs-on: ubuntu-latest 62 | steps: 63 | - name: Checkout code 64 | uses: actions/checkout@v2 65 | - name: Run clang-format style check for C/C++ programs. 66 | uses: jidicula/clang-format-action@v3.2.0 67 | with: 68 | clang-format-version: '11' 69 | check-path: src 70 | -------------------------------------------------------------------------------- /src/configuration.h: -------------------------------------------------------------------------------- 1 | #ifndef CONFIGURATION_H_ 2 | #define CONFIGURATION_H_ 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | class Configuration { 10 | public: 11 | class Beacon { 12 | public: 13 | class Smart_Beacon { 14 | public: 15 | Smart_Beacon() : active(false), turn_min(25), slow_rate(300), slow_speed(10), fast_rate(60), fast_speed(100), min_tx_dist(100), min_bcn(5) { 16 | } 17 | 18 | bool active; 19 | int turn_min; 20 | int slow_rate; 21 | int slow_speed; 22 | int fast_rate; 23 | int fast_speed; 24 | int min_tx_dist; 25 | int min_bcn; 26 | }; 27 | 28 | Beacon() : callsign("NOCALL-10"), path("WIDE1-1"), message("LoRa Tracker"), timeout(1), symbol("["), overlay("/"), enhance_precision(true) { 29 | } 30 | 31 | String callsign; 32 | String path; 33 | String message; 34 | int timeout; 35 | String symbol; 36 | String overlay; 37 | Smart_Beacon smart_beacon; 38 | bool enhance_precision; 39 | }; 40 | 41 | class LoRa { 42 | public: 43 | LoRa() : frequencyRx(433775000), frequencyTx(433775000), power(20), spreadingFactor(12), signalBandwidth(125000), codingRate4(5) { 44 | } 45 | 46 | long frequencyRx; 47 | long frequencyTx; 48 | int power; 49 | int spreadingFactor; 50 | long signalBandwidth; 51 | int codingRate4; 52 | }; 53 | 54 | class PTT { 55 | public: 56 | PTT() : active(false), io_pin(4), start_delay(0), end_delay(0), reverse(false) { 57 | } 58 | 59 | bool active; 60 | int io_pin; 61 | int start_delay; 62 | int end_delay; 63 | bool reverse; 64 | }; 65 | 66 | class Button { 67 | public: 68 | Button() : tx(false), alt_message(false) { 69 | } 70 | 71 | bool tx; 72 | int alt_message; 73 | }; 74 | 75 | Configuration() : debug(false) { 76 | } 77 | 78 | bool debug; 79 | std::list beacons; 80 | LoRa lora; 81 | PTT ptt; 82 | Button button; 83 | }; 84 | 85 | class ConfigurationManagement { 86 | public: 87 | explicit ConfigurationManagement(String FilePath); 88 | 89 | Configuration readConfiguration(); 90 | void writeConfiguration(Configuration conf); 91 | 92 | private: 93 | const String mFilePath; 94 | }; 95 | 96 | #endif 97 | -------------------------------------------------------------------------------- /src/display.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include "display.h" 8 | #include "pins.h" 9 | 10 | Adafruit_SSD1306 display(128, 64, &Wire, OLED_RST); 11 | 12 | // cppcheck-suppress unusedFunction 13 | void setup_display() { 14 | pinMode(OLED_RST, OUTPUT); 15 | digitalWrite(OLED_RST, LOW); 16 | delay(20); 17 | digitalWrite(OLED_RST, HIGH); 18 | 19 | Wire.begin(OLED_SDA, OLED_SCL); 20 | if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3c, false, false)) { 21 | logPrintlnE("SSD1306 allocation failed"); 22 | while (true) { 23 | } 24 | } 25 | 26 | display.clearDisplay(); 27 | display.setTextColor(WHITE); 28 | display.setTextSize(1); 29 | display.setCursor(0, 0); 30 | display.print("LORA SENDER "); 31 | display.ssd1306_command(SSD1306_SETCONTRAST); 32 | display.ssd1306_command(1); 33 | display.display(); 34 | } 35 | 36 | // cppcheck-suppress unusedFunction 37 | void show_display(String header, int wait) { 38 | display.clearDisplay(); 39 | display.setTextColor(WHITE); 40 | display.setTextSize(2); 41 | display.setCursor(0, 0); 42 | display.println(header); 43 | display.ssd1306_command(SSD1306_SETCONTRAST); 44 | display.ssd1306_command(1); 45 | display.display(); 46 | delay(wait); 47 | } 48 | 49 | // cppcheck-suppress unusedFunction 50 | void show_display(String header, String line1, int wait) { 51 | display.clearDisplay(); 52 | display.setTextColor(WHITE); 53 | display.setTextSize(2); 54 | display.setCursor(0, 0); 55 | display.println(header); 56 | display.setTextSize(1); 57 | display.setCursor(0, 16); 58 | display.println(line1); 59 | display.ssd1306_command(SSD1306_SETCONTRAST); 60 | display.ssd1306_command(1); 61 | display.display(); 62 | delay(wait); 63 | } 64 | 65 | // cppcheck-suppress unusedFunction 66 | void show_display(String header, String line1, String line2, int wait) { 67 | display.clearDisplay(); 68 | display.setTextColor(WHITE); 69 | display.setTextSize(2); 70 | display.setCursor(0, 0); 71 | display.println(header); 72 | display.setTextSize(1); 73 | display.setCursor(0, 16); 74 | display.println(line1); 75 | display.setCursor(0, 26); 76 | display.println(line2); 77 | display.ssd1306_command(SSD1306_SETCONTRAST); 78 | display.ssd1306_command(1); 79 | display.display(); 80 | delay(wait); 81 | } 82 | 83 | // cppcheck-suppress unusedFunction 84 | void show_display(String header, String line1, String line2, String line3, int wait) { 85 | display.clearDisplay(); 86 | display.setTextColor(WHITE); 87 | display.setTextSize(2); 88 | display.setCursor(0, 0); 89 | display.println(header); 90 | display.setTextSize(1); 91 | display.setCursor(0, 16); 92 | display.println(line1); 93 | display.setCursor(0, 26); 94 | display.println(line2); 95 | display.setCursor(0, 36); 96 | display.println(line3); 97 | display.ssd1306_command(SSD1306_SETCONTRAST); 98 | display.ssd1306_command(1); 99 | display.display(); 100 | delay(wait); 101 | } 102 | 103 | // cppcheck-suppress unusedFunction 104 | void show_display(String header, String line1, String line2, String line3, String line4, int wait) { 105 | display.clearDisplay(); 106 | display.setTextColor(WHITE); 107 | display.setTextSize(2); 108 | display.setCursor(0, 0); 109 | display.println(header); 110 | display.setTextSize(1); 111 | display.setCursor(0, 16); 112 | display.println(line1); 113 | display.setCursor(0, 26); 114 | display.println(line2); 115 | display.setCursor(0, 36); 116 | display.println(line3); 117 | display.setCursor(0, 46); 118 | display.println(line4); 119 | display.ssd1306_command(SSD1306_SETCONTRAST); 120 | display.ssd1306_command(1); 121 | display.display(); 122 | delay(wait); 123 | } 124 | 125 | // cppcheck-suppress unusedFunction 126 | void show_display(String header, String line1, String line2, String line3, String line4, String line5, int wait) { 127 | display.clearDisplay(); 128 | display.setTextColor(WHITE); 129 | display.setTextSize(2); 130 | display.setCursor(0, 0); 131 | display.println(header); 132 | display.setTextSize(1); 133 | display.setCursor(0, 16); 134 | display.println(line1); 135 | display.setCursor(0, 26); 136 | display.println(line2); 137 | display.setCursor(0, 36); 138 | display.println(line3); 139 | display.setCursor(0, 46); 140 | display.println(line4); 141 | display.setCursor(0, 56); 142 | display.println(line5); 143 | display.ssd1306_command(SSD1306_SETCONTRAST); 144 | display.ssd1306_command(1); 145 | display.display(); 146 | delay(wait); 147 | } 148 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | # BasedOnStyle: LLVM 4 | AccessModifierOffset: -2 5 | AlignAfterOpenBracket: Align 6 | AlignConsecutiveMacros: true 7 | AlignConsecutiveAssignments: true 8 | AlignConsecutiveBitFields: true 9 | AlignConsecutiveDeclarations: true 10 | AlignEscapedNewlines: Left 11 | AlignOperands: Align 12 | AlignTrailingComments: true 13 | AllowAllArgumentsOnNextLine: true 14 | AllowAllConstructorInitializersOnNextLine: true 15 | AllowAllParametersOfDeclarationOnNextLine: true 16 | AllowShortEnumsOnASingleLine: false 17 | AllowShortBlocksOnASingleLine: Never 18 | AllowShortCaseLabelsOnASingleLine: false 19 | AllowShortFunctionsOnASingleLine: None 20 | AllowShortLambdasOnASingleLine: None 21 | AllowShortIfStatementsOnASingleLine: Never 22 | AllowShortLoopsOnASingleLine: false 23 | AlwaysBreakAfterDefinitionReturnType: None 24 | AlwaysBreakAfterReturnType: None 25 | AlwaysBreakBeforeMultilineStrings: false 26 | AlwaysBreakTemplateDeclarations: MultiLine 27 | BinPackArguments: true 28 | BinPackParameters: true 29 | BraceWrapping: 30 | AfterCaseLabel: false 31 | AfterClass: false 32 | AfterControlStatement: Never 33 | AfterEnum: false 34 | AfterFunction: false 35 | AfterNamespace: false 36 | AfterObjCDeclaration: false 37 | AfterStruct: false 38 | AfterUnion: false 39 | AfterExternBlock: false 40 | BeforeCatch: false 41 | BeforeElse: false 42 | BeforeLambdaBody: false 43 | BeforeWhile: false 44 | IndentBraces: false 45 | SplitEmptyFunction: true 46 | SplitEmptyRecord: true 47 | SplitEmptyNamespace: true 48 | BreakBeforeBinaryOperators: None 49 | BreakBeforeBraces: Attach 50 | BreakBeforeInheritanceComma: false 51 | BreakInheritanceList: BeforeColon 52 | BreakBeforeTernaryOperators: true 53 | BreakConstructorInitializersBeforeComma: false 54 | BreakConstructorInitializers: BeforeColon 55 | BreakAfterJavaFieldAnnotations: false 56 | BreakStringLiterals: true 57 | ColumnLimit: 500 58 | CommentPragmas: '^ IWYU pragma:' 59 | CompactNamespaces: false 60 | ConstructorInitializerAllOnOneLineOrOnePerLine: false 61 | ConstructorInitializerIndentWidth: 4 62 | ContinuationIndentWidth: 4 63 | Cpp11BracedListStyle: true 64 | DeriveLineEnding: true 65 | DerivePointerAlignment: false 66 | DisableFormat: false 67 | ExperimentalAutoDetectBinPacking: false 68 | FixNamespaceComments: true 69 | ForEachMacros: 70 | - foreach 71 | - Q_FOREACH 72 | - BOOST_FOREACH 73 | IncludeBlocks: Preserve 74 | IncludeCategories: 75 | - Regex: '^"(llvm|llvm-c|clang|clang-c)/' 76 | Priority: 2 77 | SortPriority: 0 78 | - Regex: '^(<|"(gtest|gmock|isl|json)/)' 79 | Priority: 3 80 | SortPriority: 0 81 | - Regex: '.*' 82 | Priority: 1 83 | SortPriority: 0 84 | IncludeIsMainRegex: '(Test)?$' 85 | IncludeIsMainSourceRegex: '' 86 | IndentCaseLabels: false 87 | IndentCaseBlocks: false 88 | IndentGotoLabels: true 89 | IndentPPDirectives: None 90 | IndentExternBlock: AfterExternBlock 91 | IndentWidth: 2 92 | IndentWrappedFunctionNames: false 93 | InsertTrailingCommas: None 94 | JavaScriptQuotes: Leave 95 | JavaScriptWrapImports: true 96 | KeepEmptyLinesAtTheStartOfBlocks: true 97 | MacroBlockBegin: '' 98 | MacroBlockEnd: '' 99 | MaxEmptyLinesToKeep: 1 100 | NamespaceIndentation: None 101 | ObjCBinPackProtocolList: Auto 102 | ObjCBlockIndentWidth: 2 103 | ObjCBreakBeforeNestedBlockParam: true 104 | ObjCSpaceAfterProperty: false 105 | ObjCSpaceBeforeProtocolList: true 106 | PenaltyBreakAssignment: 2 107 | PenaltyBreakBeforeFirstCallParameter: 19 108 | PenaltyBreakComment: 300 109 | PenaltyBreakFirstLessLess: 120 110 | PenaltyBreakString: 1000 111 | PenaltyBreakTemplateDeclaration: 10 112 | PenaltyExcessCharacter: 1000000 113 | PenaltyReturnTypeOnItsOwnLine: 60 114 | PointerAlignment: Right 115 | ReflowComments: true 116 | SortIncludes: true 117 | SortUsingDeclarations: true 118 | SpaceAfterCStyleCast: false 119 | SpaceAfterLogicalNot: false 120 | SpaceAfterTemplateKeyword: true 121 | SpaceBeforeAssignmentOperators: true 122 | SpaceBeforeCpp11BracedList: false 123 | SpaceBeforeCtorInitializerColon: true 124 | SpaceBeforeInheritanceColon: true 125 | SpaceBeforeParens: ControlStatements 126 | SpaceBeforeRangeBasedForLoopColon: true 127 | SpaceInEmptyBlock: false 128 | SpaceInEmptyParentheses: false 129 | SpacesBeforeTrailingComments: 1 130 | SpacesInAngles: false 131 | SpacesInConditionalStatement: false 132 | SpacesInContainerLiterals: true 133 | SpacesInCStyleCastParentheses: false 134 | SpacesInParentheses: false 135 | SpacesInSquareBrackets: false 136 | SpaceBeforeSquareBrackets: false 137 | Standard: Latest 138 | StatementMacros: 139 | - Q_UNUSED 140 | - QT_REQUIRE_VERSION 141 | TabWidth: 8 142 | UseCRLF: false 143 | UseTab: Never 144 | WhitespaceSensitiveMacros: 145 | - STRINGIZE 146 | - PP_STRINGIZE 147 | - BOOST_PP_STRINGIZE 148 | ... 149 | -------------------------------------------------------------------------------- /src/configuration.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #ifndef CPPCHECK 5 | #include 6 | #endif 7 | 8 | #include "configuration.h" 9 | 10 | ConfigurationManagement::ConfigurationManagement(String FilePath) : mFilePath(FilePath) { 11 | if (!SPIFFS.begin(true)) { 12 | logPrintlnE("Mounting SPIFFS was not possible. Trying to format SPIFFS..."); 13 | SPIFFS.format(); 14 | if (!SPIFFS.begin()) { 15 | logPrintlnE("Formating SPIFFS was not okay!"); 16 | } 17 | } 18 | } 19 | 20 | // cppcheck-suppress unusedFunction 21 | Configuration ConfigurationManagement::readConfiguration() { 22 | File file = SPIFFS.open(mFilePath); 23 | if (!file) { 24 | logPrintlnE("Failed to open file for reading..."); 25 | return Configuration(); 26 | } 27 | DynamicJsonDocument data(2048); 28 | DeserializationError error = deserializeJson(data, file); 29 | 30 | if (error) { 31 | logPrintlnE("Failed to read file, using default configuration."); 32 | } 33 | file.close(); 34 | 35 | Configuration conf; 36 | 37 | conf.debug = data["debug"] | false; 38 | 39 | JsonArray beacons = data["beacons"].as(); 40 | for (JsonVariant v : beacons) { 41 | Configuration::Beacon beacon; 42 | 43 | if (v.containsKey("callsign")) 44 | beacon.callsign = v["callsign"].as(); 45 | beacon.path = ""; 46 | beacon.timeout = v["timeout"] | 1; 47 | if (v.containsKey("symbol")) 48 | beacon.symbol = v["symbol"].as(); 49 | if (v.containsKey("overlay")) 50 | beacon.overlay = v["overlay"].as(); 51 | 52 | beacon.smart_beacon.active = v["smart_beacon"]["active"] | false; 53 | beacon.smart_beacon.turn_min = v["smart_beacon"]["turn_min"] | 25; 54 | beacon.smart_beacon.slow_rate = v["smart_beacon"]["slow_rate"] | 300; 55 | beacon.smart_beacon.slow_speed = v["smart_beacon"]["slow_speed"] | 10; 56 | beacon.smart_beacon.fast_rate = v["smart_beacon"]["fast_rate"] | 60; 57 | beacon.smart_beacon.fast_speed = v["smart_beacon"]["fast_speed"] | 100; 58 | beacon.smart_beacon.min_tx_dist = v["smart_beacon"]["min_tx_dist"] | 100; 59 | beacon.smart_beacon.min_bcn = v["smart_beacon"]["min_bcn"] | 5; 60 | 61 | conf.beacons.push_back(beacon); 62 | } 63 | 64 | conf.button.tx = data["button"]["tx"] | false; 65 | conf.button.alt_message = data["button"]["alt_message"] | false; 66 | 67 | conf.lora.frequencyRx = data["lora"]["frequency_rx"] | 433775000; 68 | conf.lora.frequencyTx = data["lora"]["frequency_tx"] | 433775000; 69 | conf.lora.power = data["lora"]["power"] | 20; 70 | conf.lora.spreadingFactor = data["lora"]["spreading_factor"] | 12; 71 | conf.lora.signalBandwidth = data["lora"]["signal_bandwidth"] | 125000; 72 | conf.lora.codingRate4 = data["lora"]["coding_rate4"] | 5; 73 | 74 | conf.ptt.active = data["ptt_output"]["active"] | false; 75 | conf.ptt.io_pin = data["ptt_output"]["io_pin"] | 4; 76 | conf.ptt.start_delay = data["ptt_output"]["start_delay"] | 0; 77 | conf.ptt.end_delay = data["ptt_output"]["end_delay"] | 0; 78 | conf.ptt.reverse = data["ptt_output"]["reverse"] | false; 79 | 80 | return conf; 81 | } 82 | 83 | // cppcheck-suppress unusedFunction 84 | void ConfigurationManagement::writeConfiguration(Configuration conf) { 85 | File file = SPIFFS.open(mFilePath, "w"); 86 | if (!file) { 87 | logPrintlnE("Failed to open file for writing..."); 88 | return; 89 | } 90 | DynamicJsonDocument data(2048); 91 | 92 | JsonArray beacons = data.createNestedArray("beacons"); 93 | for (Configuration::Beacon beacon : conf.beacons) { 94 | JsonObject v = beacons.createNestedObject(); 95 | v["callsign"] = beacon.callsign; 96 | v["path"] = beacon.path; 97 | v["timeout"] = beacon.timeout; 98 | v["symbol"] = beacon.symbol; 99 | v["overlay"] = beacon.overlay; 100 | 101 | v["smart_beacon"]["active"] = beacon.smart_beacon.active; 102 | v["smart_beacon"]["turn_min"] = beacon.smart_beacon.turn_min; 103 | v["smart_beacon"]["slow_rate"] = beacon.smart_beacon.slow_rate; 104 | v["smart_beacon"]["slow_speed"] = beacon.smart_beacon.slow_speed; 105 | v["smart_beacon"]["fast_rate"] = beacon.smart_beacon.fast_rate; 106 | v["smart_beacon"]["fast_speed"] = beacon.smart_beacon.fast_speed; 107 | v["smart_beacon"]["min_tx_dist"] = beacon.smart_beacon.min_tx_dist; 108 | v["smart_beacon"]["min_bcn"] = beacon.smart_beacon.min_bcn; 109 | } 110 | 111 | data["debug"] = conf.debug; 112 | 113 | data["button"]["tx"] = conf.button.tx; 114 | data["button"]["alt_message"] = conf.button.alt_message; 115 | 116 | data["lora"]["frequency_rx"] = conf.lora.frequencyRx; 117 | data["lora"]["frequency_tx"] = conf.lora.frequencyTx; 118 | data["lora"]["power"] = conf.lora.power; 119 | data["lora"]["spreading_factor"] = conf.lora.spreadingFactor; 120 | data["lora"]["signal_bandwidth"] = conf.lora.signalBandwidth; 121 | data["lora"]["coding_rate4"] = conf.lora.codingRate4; 122 | 123 | data["ptt_out"]["active"] = conf.ptt.active; 124 | data["ptt_out"]["io_pin"] = conf.ptt.io_pin; 125 | data["ptt_out"]["start_delay"] = conf.ptt.start_delay; 126 | data["ptt_out"]["end_delay"] = conf.ptt.end_delay; 127 | data["ptt_out"]["reverse"] = conf.ptt.reverse; 128 | 129 | serializeJson(data, file); 130 | file.close(); 131 | } 132 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # APRS 434 — Extend your LoRa range by saving bytes. 2 | Welcome to the home of **APRS 434 tracker**, the 434 MHz LoRa APRS amateur radio GPS tracker that **extends range by saving bytes.** 3 | 4 | Unlike other ham radio LoRa APRS trackers, this tracker aims at **deploying LoRa the way it was intended;** namely by being frugal about the number of bytes put on air. Doing so, results in a number of benefits: 5 | 6 | - Increased battery life, 7 | - Higher chances of good packet reception, 8 | - Hence, increased range, 9 | - Lower probability of packet collisions, 10 | - Therefore, more channel capacity. 11 | 12 | To [**learn more**](https://aprs434.github.io) about APRS 434 data link compression, visit [aprs434.github.io](https://aprs434.github.io). 13 | 14 | 15 | ## Supported Tracker Hardware 16 | The **APRS 434** LoRa tracker firmware is developed for the relatively cheap Espressif ESP32-based LoRa GPS trackers made by TTGO. These are available from Aliexpress, Amazon or eBay. 17 | 18 | Supported 433 MHz LoRa GPS tracker hardware: 19 | - TTGO T-Beam v0.7 433 MHz SX1278 20 | - TTGO T-Beam v1 433 MHz SX1278 21 | 22 | > ⚠ Please, make sure to order a 433 MHz version! 23 | 24 | ![TTGO T-Beam](pics/Tracker.png) 25 | 26 | 27 | ## Firmware Compilation and Configuration 28 | 29 | ### Quick Start Guides 30 | - [German](https://www.lora-aprs.info/docs/LoRa_APRS_iGate/quick-start-guide/) 31 | - [French](http://www.f5kmy.fr/spip.php?article509) 32 | 33 | ### Compilation 34 | The best success is to use PlatformIO (and it is the only platform where I can support you). 35 | 36 | - Go to [PlatformIO](https://platformio.org/) download and install the IDE. 37 | - If installed open the IDE, go to the left side and klick on 'extensions' then search for 'PatformIO' and install. 38 | - When installed click 'the ant head' on the left and choose import the project on the right. 39 | - Just open the folder and you can compile the Firmware. 40 | 41 | ### Configuration 42 | - You can find all nessesary settings to change for your configuration in **data/tracker.json**. 43 | - The `button_tx` setting enables manual triggering of the beacon using the middle button on the T-Beam. 44 | - To upload it to your board you have to do this via **Upload File System image** in PlatformIO! 45 | - To find the 'Upload File System image' click the PlatformIO symbol (the little alien) on the left side, choos your configuration, click on 'Platform' and search for 'Upload File System image'. 46 | 47 | 48 | ## LoRa APRS i-Gate 49 | Currently, the APRS 434 tracker is still compatible with the i-gate developed by Peter Buchegger, OE5BPA. However, this will soon change as more LoRa frame compression is added. 50 | 51 | We feel confident that trackers with the proposed APRS 434 compressed LoRa frame will eventually become dominant because of the longer range merit. To smooth out the transition, we are developing an **i‑gate capable of understanding both formats;** i.e. compressed APRS 434 and longer, legacy OE5BPA. 52 | 53 | It is strongly advised to install [**the accompanying APRS 434 i-gate**](https://github.com/aprs434/lora.igate) as new releases will be automatically pulled over‑the‑air (OTA) via WiFi. 54 | 55 | 56 | ## Development Road Map 57 | 58 | ### Data Link Layer 59 | 60 | |tracker
firmware|completed|feature|LoRa payload|compatible with OE5BPA i‑gate| 61 | |:------------------:|:-------:|:-----:|:----------:|:---------------------------:| 62 | |v0.0.0|✓|original [OE5BPA tracker](https://github.com/lora-aprs/LoRa_APRS_Tracker)|113 bytes|✓| 63 | |v0.1.0|✓|byte-saving [`tracker.json`](https://github.com/aprs434/lora.tracker/blob/master/data/tracker.json)|87 bytes|✓| 64 | |v0.2.0|✓|fork of the [OE5BPA tracker](https://github.com/lora-aprs/LoRa_APRS_Tracker)
with significantly less transmitted bytes|44 bytes|✓| 65 | |v0.3.0|✓|[Base91](https://en.wikipedia.org/wiki/List_of_numeral_systems#Standard_positional_numeral_systems) compression of location, course and speed data|31 bytes|✓| 66 | |v0.4.0|✓|removal of the transmitted [newline](https://en.wikipedia.org/wiki/Newline) `\n` character at frame end|30 bytes|✓| 67 | |||random time jitter between fixed interval packets to avoid repetitive [collisions](https://en.wikipedia.org/wiki/Collision_domain)|30 bytes|✓| 68 | |||[tracker](https://github.com/aprs434/lora.tracker) and [i-gate](https://github.com/aprs434/lora.igate) with frame address compression,
no custom header in payload|18 bytes|use the [APRS 434 i‑gate](https://github.com/aprs434/lora.igate)| 69 | 70 | > Currently, the APRS 434 tracker is still compatible with the i-gate developed by Peter Buchegger, OE5BPA. However, this will soon change as more LoRa frame compression is added. 71 | > 72 | > We feel confident that trackers with the proposed APRS 434 compressed LoRa frame will eventually become dominant because of the longer range merit. To smooth out the transition, we are developing an **i‑gate capable of understanding both formats;** i.e. compressed APRS 434 and longer, legacy OE5BPA. 73 | > 74 | > It is strongly advised to install [**the accompanying APRS 434 i-gate**](https://github.com/aprs434/lora.igate) as new releases will be automatically pulled over‑the‑air (OTA) via WiFi. 75 | 76 | 77 | ### Tracker Hardware 78 | 79 | |tracker
firmware|completed|feature| 80 | |:------------------:|:-------:|:-----:| 81 | |v0.3.1|✓|coordinates displayed on screen| 82 | |||reduced power consumption through [SH1106 OLED sleep](https://bengoncalves.wordpress.com/2015/10/01/oled-display-and-arduino-with-power-save-mode/)| 83 | |||button press to activate OLED screen| 84 | |||ESP32 power reduction| 85 | 86 | ### Messaging 87 | At first, only uplink messaging to an i-gate will be considered. This is useful for status updates, [SOTA self‑spotting](https://www.sotaspots.co.uk/Aprs2Sota_Info.php), or even emergencies. 88 | 89 | On the other hand, bidirectional messaging requires time division multiplexing between the up- and downlink, based on precise GPS timing. That is because channel isolation between different up- and downlink frequencies probably would require costly and bulky resonant cavities. 90 | 91 | |tracker
firmware|completed|feature| 92 | |:------------------:|:-------:|:-----:| 93 | |||add a [library](https://web.archive.org/web/20190316204938/http://cliffle.com/project/chatpad/arduino/) for the [Xbox 360 Chatpad](https://nuxx.net/gallery/v/acquired_stuff/xbox_360_chatpad/) keyboard| 94 | |||[support](https://www.hackster.io/scottpowell69/lora-qwerty-messenger-c0eee6) for the [M5Stack CardKB Mini](https://shop.m5stack.com/products/cardkb-mini-keyboard) keyboard| 95 | 96 | ### WiFi Geolocation 97 | TBD 98 | -------------------------------------------------------------------------------- /src/LoRa_APRS_Tracker.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "BeaconManager.h" 11 | #include "configuration.h" 12 | #include "display.h" 13 | #include "pins.h" 14 | #include "power_management.h" 15 | 16 | Configuration Config; 17 | BeaconManager BeaconMan; 18 | 19 | PowerManagement powerManagement; 20 | OneButton userButton = OneButton(BUTTON_PIN, true, true); 21 | 22 | HardwareSerial ss(1); 23 | TinyGPSPlus gps; 24 | 25 | void load_config(); 26 | void setup_lora(); 27 | void setup_gps(); 28 | 29 | char *ax25_base91enc(char *s, uint8_t n, uint32_t v); 30 | String createDateString(time_t t); 31 | String createTimeString(time_t t); 32 | String getSmartBeaconState(); 33 | String padding(unsigned int number, unsigned int width); 34 | 35 | static bool send_update = true; 36 | 37 | static void handle_tx_click() { 38 | send_update = true; 39 | } 40 | 41 | static void handle_next_beacon() { 42 | BeaconMan.loadNextBeacon(); 43 | show_display(BeaconMan.getCurrentBeaconConfig()->callsign, 2000); 44 | } 45 | 46 | // cppcheck-suppress unusedFunction 47 | void setup() { 48 | Serial.begin(115200); 49 | 50 | #ifdef TTGO_T_Beam_V1_0 51 | Wire.begin(SDA, SCL); 52 | if (!powerManagement.begin(Wire)) { 53 | logPrintlnI("AXP192 init done!"); 54 | } else { 55 | logPrintlnE("AXP192 init failed!"); 56 | } 57 | powerManagement.activateLoRa(); 58 | powerManagement.activateOLED(); 59 | powerManagement.activateGPS(); 60 | powerManagement.activateMeasurement(); 61 | #endif 62 | 63 | delay(500); 64 | logPrintlnI("APRS 434 LoRa Tracker"); 65 | setup_display(); 66 | 67 | show_display("APRS 434", "LoRa Tracker", "v0.4.0", "", "LESS BYTES,", "MORE RANGE", 2000); 68 | load_config(); 69 | 70 | setup_gps(); 71 | setup_lora(); 72 | 73 | if (Config.ptt.active) { 74 | pinMode(Config.ptt.io_pin, OUTPUT); 75 | digitalWrite(Config.ptt.io_pin, Config.ptt.reverse ? HIGH : LOW); 76 | } 77 | 78 | // make sure wifi and bt is off as we don't need it: 79 | WiFi.mode(WIFI_OFF); 80 | btStop(); 81 | 82 | if (Config.button.tx) { 83 | // attach TX action to user button (defined by BUTTON_PIN) 84 | userButton.attachClick(handle_tx_click); 85 | } 86 | 87 | if (Config.button.alt_message) { 88 | userButton.attachLongPressStart(handle_next_beacon); 89 | } 90 | 91 | logPrintlnI("Smart Beacon is " + getSmartBeaconState()); 92 | show_display("INFO", "Smart Beacon is " + getSmartBeaconState(), 1000); 93 | logPrintlnI("setup done..."); 94 | delay(500); 95 | } 96 | 97 | // cppcheck-suppress unusedFunction 98 | void loop() { 99 | userButton.tick(); 100 | 101 | if (Config.debug) { 102 | while (Serial.available() > 0) { 103 | char c = Serial.read(); 104 | // Serial.print(c); 105 | gps.encode(c); 106 | } 107 | } else { 108 | while (ss.available() > 0) { 109 | char c = ss.read(); 110 | // Serial.print(c); 111 | gps.encode(c); 112 | } 113 | } 114 | 115 | bool gps_time_update = gps.time.isUpdated(); 116 | bool gps_loc_update = gps.location.isUpdated(); 117 | static time_t nextBeaconTimeStamp = -1; 118 | 119 | static double currentHeading = 0; 120 | static double previousHeading = 0; 121 | static unsigned int rate_limit_message_text = 0; 122 | 123 | if (gps.time.isValid()) { 124 | setTime(gps.time.hour(), gps.time.minute(), gps.time.second(), gps.date.day(), gps.date.month(), gps.date.year()); 125 | 126 | if (gps_loc_update && nextBeaconTimeStamp <= now()) { 127 | send_update = true; 128 | if (BeaconMan.getCurrentBeaconConfig()->smart_beacon.active) { 129 | currentHeading = gps.course.deg(); 130 | // enforce message text on slowest Config.smart_beacon.slow_rate 131 | rate_limit_message_text = 0; 132 | } else { 133 | // enforce message text every n's Config.beacon.timeout frame 134 | if (BeaconMan.getCurrentBeaconConfig()->timeout * rate_limit_message_text > 30) { 135 | rate_limit_message_text = 0; 136 | } 137 | } 138 | } 139 | } 140 | 141 | static double lastTxLat = 0.0; 142 | static double lastTxLng = 0.0; 143 | static double lastTxdistance = 0.0; 144 | static uint32_t txInterval = 60000L; // Initial 60 secs internal 145 | static uint32_t lastTxTime = millis(); 146 | static int speed_zero_sent = 0; 147 | 148 | static bool BatteryIsConnected = false; 149 | static String batteryVoltage = ""; 150 | static String batteryChargeCurrent = ""; 151 | #ifdef TTGO_T_Beam_V1_0 152 | static unsigned int rate_limit_check_battery = 0; 153 | if (!(rate_limit_check_battery++ % 60)) 154 | BatteryIsConnected = powerManagement.isBatteryConnect(); 155 | if (BatteryIsConnected) { 156 | batteryVoltage = String(powerManagement.getBatteryVoltage(), 2); 157 | batteryChargeCurrent = String(powerManagement.getBatteryChargeDischargeCurrent(), 0); 158 | } 159 | #endif 160 | 161 | if (!send_update && gps_loc_update && BeaconMan.getCurrentBeaconConfig()->smart_beacon.active) { 162 | uint32_t lastTx = millis() - lastTxTime; 163 | currentHeading = gps.course.deg(); 164 | lastTxdistance = TinyGPSPlus::distanceBetween(gps.location.lat(), gps.location.lng(), lastTxLat, lastTxLng); 165 | if (lastTx >= txInterval) { 166 | // Trigger Tx Tracker when Tx interval is reach 167 | // Will not Tx if stationary bcos speed < 5 and lastTxDistance < 20 168 | if (lastTxdistance > 20) { 169 | send_update = true; 170 | } 171 | } 172 | 173 | if (!send_update) { 174 | // Get headings and heading delta 175 | double headingDelta = abs(previousHeading - currentHeading); 176 | 177 | if (lastTx > BeaconMan.getCurrentBeaconConfig()->smart_beacon.min_bcn * 1000) { 178 | // Check for heading more than 25 degrees 179 | if (headingDelta > BeaconMan.getCurrentBeaconConfig()->smart_beacon.turn_min && lastTxdistance > BeaconMan.getCurrentBeaconConfig()->smart_beacon.min_tx_dist) { 180 | send_update = true; 181 | } 182 | } 183 | } 184 | } 185 | 186 | if (send_update && gps_loc_update) { 187 | send_update = false; 188 | 189 | nextBeaconTimeStamp = now() + (BeaconMan.getCurrentBeaconConfig()->smart_beacon.active ? BeaconMan.getCurrentBeaconConfig()->smart_beacon.slow_rate : (BeaconMan.getCurrentBeaconConfig()->timeout * SECS_PER_MIN)); 190 | 191 | APRSMessage msg; 192 | msg.setSource(BeaconMan.getCurrentBeaconConfig()->callsign); 193 | msg.setPath(BeaconMan.getCurrentBeaconConfig()->path); 194 | msg.setDestination("AP"); 195 | 196 | float Tlat, Tlon; 197 | float Tspeed=0, Tcourse=0; 198 | Tlat = gps.location.lat(); 199 | Tlon = gps.location.lng(); 200 | Tcourse = gps.course.deg(); 201 | Tspeed = gps.speed.knots(); 202 | 203 | uint32_t aprs_lat, aprs_lon; 204 | aprs_lat = 900000000 - Tlat * 10000000; 205 | aprs_lat = aprs_lat / 26 - aprs_lat / 2710 + aprs_lat / 15384615; 206 | aprs_lon = 900000000 + Tlon * 10000000 / 2; 207 | aprs_lon = aprs_lon / 26 - aprs_lon / 2710 + aprs_lon / 15384615; 208 | 209 | String Ns, Ew, helper; 210 | if(Tlat < 0) { Ns = "S"; } else { Ns = "N"; } 211 | if(Tlat < 0) { Tlat= -Tlat; } 212 | 213 | if(Tlon < 0) { Ew = "W"; } else { Ew = "E"; } 214 | if(Tlon < 0) { Tlon= -Tlon; } 215 | 216 | String infoField = "!"; // Data Type ID 217 | infoField += BeaconMan.getCurrentBeaconConfig()->overlay; 218 | 219 | char helper_base91[] = {"0000\0"}; 220 | int i; 221 | ax25_base91enc(helper_base91, 4, aprs_lat); 222 | for (i=0; i<4; i++) { 223 | infoField += helper_base91[i]; 224 | } 225 | ax25_base91enc(helper_base91, 4, aprs_lon); 226 | for (i=0; i<4; i++) { 227 | infoField += helper_base91[i]; 228 | } 229 | 230 | infoField += BeaconMan.getCurrentBeaconConfig()->symbol; 231 | 232 | ax25_base91enc(helper_base91, 1, (uint32_t) Tcourse/4 ); 233 | infoField += helper_base91[0]; 234 | ax25_base91enc(helper_base91, 1, (uint32_t) (log1p(Tspeed)/0.07696)); 235 | infoField += helper_base91[0]; 236 | infoField += "\x47"; // Compression Type (T) Byte = \b001 00 110 = 38; 38 + 33 = 72 = \x47 = G 237 | 238 | int speed_int = max(0, min(999, (int)gps.speed.knots())); 239 | if (speed_int == 0) { 240 | /* speed is 0. 241 | we send 3 packets with speed zero (so our friends know we stand still). 242 | After that, we save airtime by not sending speed/course 000/000. 243 | Btw, even if speed we really do not move, measured course is changeing 244 | (-> no useful / even wrong info) 245 | */ 246 | if (speed_zero_sent < 3) { 247 | speed_zero_sent += 1; 248 | } 249 | } else { 250 | speed_zero_sent = 0; 251 | } 252 | 253 | msg.getBody()->setData(infoField); 254 | String data = msg.encode(); 255 | logPrintlnD(data); 256 | show_display(" << TX >>", data); 257 | 258 | if (Config.ptt.active) { 259 | digitalWrite(Config.ptt.io_pin, Config.ptt.reverse ? LOW : HIGH); 260 | delay(Config.ptt.start_delay); 261 | } 262 | 263 | LoRa.beginPacket(); // Explicit LoRa Header 264 | // Custom LoRa Header: 265 | LoRa.write('<'); 266 | LoRa.write(0xFF); 267 | LoRa.write(0x01); 268 | // APRS Data: 269 | LoRa.write((const uint8_t *)data.c_str(), data.length()); 270 | LoRa.endPacket(); 271 | 272 | if (BeaconMan.getCurrentBeaconConfig()->smart_beacon.active) { 273 | lastTxLat = gps.location.lat(); 274 | lastTxLng = gps.location.lng(); 275 | previousHeading = currentHeading; 276 | lastTxdistance = 0.0; 277 | lastTxTime = millis(); 278 | } 279 | 280 | if (Config.ptt.active) { 281 | delay(Config.ptt.end_delay); 282 | digitalWrite(Config.ptt.io_pin, Config.ptt.reverse ? HIGH : LOW); 283 | } 284 | } 285 | 286 | if (gps_time_update) { 287 | 288 | show_display(BeaconMan.getCurrentBeaconConfig()->callsign, createDateString(now()) + " " + createTimeString(now()), 289 | String("Next TX: ") + (BeaconMan.getCurrentBeaconConfig()->smart_beacon.active ? "~" : "") + createTimeString(nextBeaconTimeStamp), 290 | String("Sats: ") + gps.satellites.value() + " HDOP: " + gps.hdop.hdop(), 291 | String(gps.location.lat(), 4) + " " + String(gps.location.lng(), 4), 292 | String("Smart Beacon: " + getSmartBeaconState())); 293 | 294 | if (BeaconMan.getCurrentBeaconConfig()->smart_beacon.active) { 295 | // Change the Tx internal based on the current speed 296 | int curr_speed = (int)gps.speed.kmph(); 297 | if (curr_speed < BeaconMan.getCurrentBeaconConfig()->smart_beacon.slow_speed) { 298 | txInterval = BeaconMan.getCurrentBeaconConfig()->smart_beacon.slow_rate * 1000; 299 | } else if (curr_speed > BeaconMan.getCurrentBeaconConfig()->smart_beacon.fast_speed) { 300 | txInterval = BeaconMan.getCurrentBeaconConfig()->smart_beacon.fast_rate * 1000; 301 | } else { 302 | /* Interval inbetween low and high speed 303 | min(slow_rate, ..) because: if slow rate is 300s at slow speed <= 304 | 10km/h and fast rate is 60s at fast speed >= 100km/h everything below 305 | current speed 20km/h (100*60/20 = 300) is below slow_rate. 306 | -> In the first check, if curr speed is 5km/h (which is < 10km/h), tx 307 | interval is 300s, but if speed is 6km/h, we are landing in this 308 | section, what leads to interval 100*60/6 = 1000s (16.6min) -> this 309 | would lead to decrease of beacon rate in between 5 to 20 km/h. what 310 | is even below the slow speed rate. 311 | */ 312 | txInterval = min(BeaconMan.getCurrentBeaconConfig()->smart_beacon.slow_rate, BeaconMan.getCurrentBeaconConfig()->smart_beacon.fast_speed * BeaconMan.getCurrentBeaconConfig()->smart_beacon.fast_rate / curr_speed) * 1000; 313 | } 314 | } 315 | } 316 | 317 | if ((Config.debug == false) && (millis() > 5000 && gps.charsProcessed() < 10)) { 318 | logPrintlnE("No GPS frames detected! Try to reset the GPS Chip with this " 319 | "firmware: https://github.com/lora-aprs/TTGO-T-Beam_GPS-reset"); 320 | show_display("No GPS frames detected!", "Try to reset the GPS Chip", "https://github.com/lora-aprs/TTGO-T-Beam_GPS-reset", 2000); 321 | } 322 | } 323 | 324 | 325 | /// FUNCTIONS /// 326 | 327 | 328 | void load_config() { 329 | ConfigurationManagement confmg("/tracker.json"); 330 | Config = confmg.readConfiguration(); 331 | BeaconMan.loadConfig(Config.beacons); 332 | if (BeaconMan.getCurrentBeaconConfig()->callsign == "NOCALL-10") { 333 | logPrintlnE("You have to change your settings in 'data/tracker.json' and " 334 | "upload it via \"Upload File System image\"!"); 335 | show_display("ERROR", "You have to change your settings in 'data/tracker.json' and " 336 | "upload it via \"Upload File System image\"!"); 337 | while (true) { 338 | } 339 | } 340 | } 341 | 342 | void setup_lora() { 343 | logPrintlnI("Set SPI pins!"); 344 | SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS); 345 | logPrintlnI("Set LoRa pins!"); 346 | LoRa.setPins(LORA_CS, LORA_RST, LORA_IRQ); 347 | 348 | long freq = Config.lora.frequencyTx; 349 | logPrintI("frequency: "); 350 | logPrintlnI(String(freq)); 351 | if (!LoRa.begin(freq)) { 352 | logPrintlnE("Starting LoRa failed!"); 353 | show_display("ERROR", "Starting LoRa failed!"); 354 | while (true) { 355 | } 356 | } 357 | LoRa.setSpreadingFactor(Config.lora.spreadingFactor); 358 | LoRa.setSignalBandwidth(Config.lora.signalBandwidth); 359 | LoRa.setCodingRate4(Config.lora.codingRate4); 360 | LoRa.enableCrc(); 361 | 362 | LoRa.setTxPower(Config.lora.power); 363 | logPrintlnI("LoRa init done!"); 364 | show_display("INFO", "LoRa init done!", 2000); 365 | } 366 | 367 | void setup_gps() { 368 | ss.begin(9600, SERIAL_8N1, GPS_TX, GPS_RX); 369 | } 370 | 371 | char *ax25_base91enc(char *s, uint8_t n, uint32_t v) 372 | { 373 | /* Creates a Base-91 representation of the value in v in the string */ 374 | /* pointed to by s, n-characters long. String length should be n+1. */ 375 | 376 | for(s += n, *s = '\0'; n; n--) 377 | { 378 | *(--s) = v % 91 + 33; 379 | v /= 91; 380 | } 381 | 382 | return(s); 383 | } 384 | 385 | String createDateString(time_t t) { 386 | return String(padding(year(t), 4) + "-" + padding(month(t), 2) + "-" + padding(day(t), 2)); 387 | } 388 | 389 | String createTimeString(time_t t) { 390 | return String(padding(hour(t), 2) + ":" + padding(minute(t), 2) + ":" + padding(second(t), 2)); 391 | } 392 | 393 | String getSmartBeaconState() { 394 | if (BeaconMan.getCurrentBeaconConfig()->smart_beacon.active) { 395 | return "On"; 396 | } 397 | return "Off"; 398 | } 399 | 400 | String padding(unsigned int number, unsigned int width) { 401 | String result; 402 | String num(number); 403 | if (num.length() > width) { 404 | width = num.length(); 405 | } 406 | for (unsigned int i = 0; i < width - num.length(); i++) { 407 | result.concat('0'); 408 | } 409 | result.concat(num); 410 | return result; 411 | } 412 | --------------------------------------------------------------------------------