├── .gitignore
├── .vscode
├── extensions.json
└── settings.json
├── LICENSE
├── README.md
├── data
└── wxStation_conf.json
├── images
└── Wx_Station_Side.jpeg
├── include
└── README
├── lib
└── README
├── platformio.ini
├── src
├── LoRa_APRS_WX_Station.cpp
├── bh1750_utils.cpp
├── bh1750_utils.h
├── bme280_utils.cpp
├── bme280_utils.h
├── boards_pinout.h
├── configuration.cpp
├── configuration.h
├── display.cpp
├── display.h
├── gps_utils.cpp
├── gps_utils.h
├── lora_utils.cpp
├── lora_utils.h
├── rain_utils.cpp
├── rain_utils.h
├── utils.cpp
├── utils.h
├── wind_rs485_utils.cpp
├── wind_rs485_utils.h
├── wx_utils.cpp
└── wx_utils.h
└── test
└── README
/.gitignore:
--------------------------------------------------------------------------------
1 | .pio
2 | .vscode/.browse.c_cpp.db*
3 | .vscode/c_cpp_properties.json
4 | .vscode/launch.json
5 | .vscode/ipch
6 | .DS_Store
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.tabSize": 4,
3 | "editor.formatOnSave": false
4 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Ricardo Guzman (Richonguzman)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CA2RXU LoRa APRS Wx Station
2 |
3 | This firmware is for using ESP32 based boards with LoRa Modules to send Wx Data into APRS by adding Temperature, Pressure, Humidity, Luminance, Rain, Wind Speed and Direction sensor.
4 |
5 | ____________________________________________________
6 |
7 | ## You can support this project to continue to grow:
8 |
9 | [
](https://github.com/sponsors/richonguzman) [
](http://paypal.me/richonguzman)
10 |
11 |
12 | ____________________________________________________
13 |
14 | # WIKI
15 |
16 | ### HOME --> here
17 |
18 | ### 1. Recommended Sensors for each task and buying links --> here
19 |
20 | ### 2. How to configure the LoRa APRS Wx Station --> here
21 |
22 | ### 3. Instructions how to connect the sensors --> here
23 |
24 |
25 | 
26 |
27 | (Image Credits: Giovanni Ratto IW1QAF)
28 |
29 | ____________________________________________________
30 | ## Timeline (Versions):
31 |
32 | - 2024.09.20 Packet Length Fix and minor bugs correction.
33 | - 2024.08.19 Start as _Alpha_.
34 |
35 | ____________________________________________________
36 |
37 | Some code from _Patrick TK5EP_ with his great repository https://github.com/tk5ep/WX-station-LoRa-WiFi was used for controlling the RS485 sensors
38 |
39 | ____________________________________________________
40 |
41 | # Hope You Enjoy this, 73 !! CA2RXU , Valparaiso, Chile
--------------------------------------------------------------------------------
/data/wxStation_conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "callsign": "NOCALL-13",
3 | "beacon": {
4 | "latitude": 0.0,
5 | "longitude": 0.0,
6 | "comment": "Wx Station",
7 | "interval": 10,
8 | "overlay": "L",
9 | "symbol": "_",
10 | "path": "WIDE1-1"
11 | },
12 | "lora": {
13 | "txFreq": 433775000,
14 | "spreadingFactor": 12,
15 | "signalBandwidth": 125000,
16 | "codingRate4": 5,
17 | "power": 20
18 | },
19 | "display": {
20 | "alwaysOn": true,
21 | "timeout": 4,
22 | "turn180": false
23 | },
24 | "sensors": {
25 | "bme280Active": true,
26 | "bme280HeightCorrection": 0,
27 | "bme280TemperatureCorrection": 0.0,
28 | "bh1750Active": false,
29 | "windDirectionActive": false,
30 | "windSpeedActive": false,
31 | "rainActive": false
32 | }
33 | }
--------------------------------------------------------------------------------
/images/Wx_Station_Side.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/richonguzman/LoRa_APRS_Wx_Station/38dbbd85bb7b6110a799f88f77cf0d70584ae8c3/images/Wx_Station_Side.jpeg
--------------------------------------------------------------------------------
/include/README:
--------------------------------------------------------------------------------
1 |
2 | This directory is intended for project header files.
3 |
4 | A header file is a file containing C declarations and macro definitions
5 | to be shared between several project source files. You request the use of a
6 | header file in your project source file (C, C++, etc) located in `src` folder
7 | by including it, with the C preprocessing directive `#include'.
8 |
9 | ```src/main.c
10 |
11 | #include "header.h"
12 |
13 | int main (void)
14 | {
15 | ...
16 | }
17 | ```
18 |
19 | Including a header file produces the same results as copying the header file
20 | into each source file that needs it. Such copying would be time-consuming
21 | and error-prone. With a header file, the related declarations appear
22 | in only one place. If they need to be changed, they can be changed in one
23 | place, and programs that include the header file will automatically use the
24 | new version when next recompiled. The header file eliminates the labor of
25 | finding and changing all the copies as well as the risk that a failure to
26 | find one copy will result in inconsistencies within a program.
27 |
28 | In C, the usual convention is to give header files names that end with `.h'.
29 | It is most portable to use only letters, digits, dashes, and underscores in
30 | header file names, and at most one dot.
31 |
32 | Read more about using header files in official GCC documentation:
33 |
34 | * Include Syntax
35 | * Include Operation
36 | * Once-Only Headers
37 | * Computed Includes
38 |
39 | https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html
40 |
--------------------------------------------------------------------------------
/lib/README:
--------------------------------------------------------------------------------
1 |
2 | This directory is intended for project specific (private) libraries.
3 | PlatformIO will compile them to static libraries and link into executable file.
4 |
5 | The source code of each library should be placed in a an own separate directory
6 | ("lib/your_library_name/[here are source files]").
7 |
8 | For example, see a structure of the following two libraries `Foo` and `Bar`:
9 |
10 | |--lib
11 | | |
12 | | |--Bar
13 | | | |--docs
14 | | | |--examples
15 | | | |--src
16 | | | |- Bar.c
17 | | | |- Bar.h
18 | | | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
19 | | |
20 | | |--Foo
21 | | | |- Foo.c
22 | | | |- Foo.h
23 | | |
24 | | |- README --> THIS FILE
25 | |
26 | |- platformio.ini
27 | |--src
28 | |- main.c
29 |
30 | and a contents of `src/main.c`:
31 | ```
32 | #include
33 | #include
34 |
35 | int main (void)
36 | {
37 | ...
38 | }
39 |
40 | ```
41 |
42 | PlatformIO Library Dependency Finder will find automatically dependent
43 | libraries scanning project source files.
44 |
45 | More information about PlatformIO Library Dependency Finder
46 | - https://docs.platformio.org/page/librarymanager/ldf.html
47 |
--------------------------------------------------------------------------------
/platformio.ini:
--------------------------------------------------------------------------------
1 | ; PlatformIO Project Configuration File
2 | ;
3 | ; Build options: build flags, source filter
4 | ; Upload options: custom upload port, speed and extra flags
5 | ; Library options: dependencies, extra library storages
6 | ; Advanced options: extra scripting
7 | ;
8 | ; Please visit documentation for the other options and examples
9 | ; https://docs.platformio.org/page/projectconf.html
10 |
11 | [platformio]
12 | default_envs = ttgo-lora32-v21
13 |
14 | [env]
15 | platform = espressif32 @ 6.7.0
16 | framework = arduino
17 | monitor_speed = 115200
18 | lib_deps =
19 | jgromes/RadioLib @ 6.6.0
20 | bblanchon/ArduinoJson @ 6.21.3
21 | adafruit/Adafruit GFX Library @ 1.11.5
22 | adafruit/Adafruit SSD1306 @ 2.5.7
23 | adafruit/Adafruit Unified Sensor@^1.1.9
24 | adafruit/Adafruit BME280 Library@^2.2.2
25 | claws/BH1750@^1.3.0
26 |
27 | [env:ttgo-lora32-v21]
28 | board = ttgo-lora32-v21
29 | build_flags =
30 | -Werror -Wall
31 | -DTTGO_T_LORA32_V2_1
32 | -DHAS_SX1278
--------------------------------------------------------------------------------
/src/LoRa_APRS_WX_Station.cpp:
--------------------------------------------------------------------------------
1 | /*_____________________________________________________________________________________
2 |
3 | ██╗ ██████╗ ██████╗ █████╗ █████╗ ██████╗ ██████╗ ███████╗
4 | ██║ ██╔═══██╗██╔══██╗██╔══██╗ ██╔══██╗██╔══██╗██╔══██╗██╔════╝
5 | ██║ ██║ ██║██████╔╝███████║ ███████║██████╔╝██████╔╝███████╗
6 | ██║ ██║ ██║██╔══██╗██╔══██║ ██╔══██║██╔═══╝ ██╔══██╗╚════██║
7 | ███████╗╚██████╔╝██║ ██║██║ ██║ ██║ ██║██║ ██║ ██║███████║
8 | ╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝╚══════╝
9 |
10 | ██╗ ██╗██╗ ██╗ ███████╗████████╗ █████╗ ████████╗██╗ ██████╗ ███╗ ██╗
11 | ██║ ██║╚██╗██╔╝ ██╔════╝╚══██╔══╝██╔══██╗╚══██╔══╝██║██╔═══██╗████╗ ██║
12 | ██║ █╗ ██║ ╚███╔╝ ███████╗ ██║ ███████║ ██║ ██║██║ ██║██╔██╗ ██║
13 | ██║███╗██║ ██╔██╗ ╚════██║ ██║ ██╔══██║ ██║ ██║██║ ██║██║╚██╗██║
14 | ╚███╔███╔╝██╔╝ ██╗ ███████║ ██║ ██║ ██║ ██║ ██║╚██████╔╝██║ ╚████║
15 | ╚══╝╚══╝ ╚═╝ ╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝
16 |
17 | Ricardo Guzman - CA2RXU
18 | https://github.com/richonguzman/LoRa_APRS_Tracker
19 | (donations : http://paypal.me/richonguzman)
20 | _____________________________________________________________________________________*/
21 |
22 | #include
23 | #include "wind_rs485_utils.h"
24 | #include "boards_pinout.h"
25 | #include "configuration.h"
26 | #include "lora_utils.h"
27 | #include "gps_utils.h"
28 | #include "wx_utils.h"
29 | #include "display.h"
30 | #include "utils.h"
31 |
32 |
33 | String versionDate = "2024.09.20";
34 | Configuration Config;
35 | HardwareSerial rs485Serial(1);
36 |
37 | String firstLine, secondLine, thirdLine, fourthLine, fifthLine, sixthLine, seventhLine;
38 |
39 |
40 | void setup() {
41 | Serial.begin(115200);
42 | delay(4000);
43 | displaySetup();
44 | displayShow(" APRS LoRa", "", " WX Station", "", ""," ", " CA2RXU " + versionDate, 4000);
45 | Serial.println("\nStarting Weather LoRa APRS Station\n");
46 |
47 | Utils::pinDeclarations();
48 | Utils::checkSwitchesStates();
49 |
50 | Utils::getI2CAddresses();
51 | WX_Utils::setupSensors();
52 | LoRa_Utils::setup();
53 | GPS_Utils::generateBeacon();
54 | }
55 |
56 | void loop() {
57 | WX_Utils::loop();
58 | displayShow(firstLine, secondLine, thirdLine, fourthLine, fifthLine, sixthLine, seventhLine, 0);
59 | }
--------------------------------------------------------------------------------
/src/bh1750_utils.cpp:
--------------------------------------------------------------------------------
1 | #include "boards_pinout.h"
2 | #include
3 | #include
4 | #include "display.h"
5 | #include "utils.h"
6 |
7 | #define luminousEfficacy (112.0) // Luminous efficacy for sunlight in lumens per watt
8 |
9 | extern uint8_t bh1750Addr;
10 | extern String seventhLine;
11 |
12 | bool bh1750SensorFound = false;
13 | String Luminosity = "...";
14 |
15 | namespace BH1750_Utils {
16 |
17 | BH1750 lightMeter;
18 |
19 | void setup() {
20 | bool status;
21 | if (bh1750Addr != 0x00) {
22 | status = lightMeter.begin(BH1750::CONTINUOUS_HIGH_RES_MODE, bh1750Addr, &Wire);
23 | if (!status) {
24 | Serial.println("Could not initialize BH1750 sensor, check sensor!");
25 | displayShow("ERROR", "", "BH1750 found but ", "could not init ...", 2000);
26 | } else {
27 | Serial.println("init : BH1750 Module ... done!");
28 | bh1750SensorFound = true;
29 | }
30 | } else {
31 | Serial.println("Could not find a BH1750 sensor, check wiring!");
32 | displayShow("ERROR", "", "BH1750 NOT FOUND !!!", "", 2000);
33 | }
34 | }
35 |
36 | String generateLumString(float lum) {
37 | String light = "";
38 | light = String((int)lum);
39 | switch (light.length()) {
40 | case 1:
41 | return "00" + light;
42 | break;
43 | case 2:
44 | return "0" + light;
45 | break;
46 | case 3:
47 | return light;
48 | break;
49 | default:
50 | return "...";
51 | }
52 | }
53 |
54 | void readSensor() { // BH1750 / GY-302 light sensor
55 | float lux = lightMeter.readLightLevel();
56 | if (lux > 40000) Serial.println("Lux > 40000!!!!");
57 | /*if (lux > 40000.0) {
58 | if (lightMeter.setMTreg(32)) { // reduce measurement time - needed in direct sun light
59 | Serial.println("Setting MTReg to low value for high light environment");
60 | } else {
61 | Serial.println("Error setting MTReg to low value for high light environment");
62 | }
63 | } else {
64 | if (lux > 10.0) {
65 | if (lightMeter.setMTreg(69)) { // typical light environment
66 | Serial.println("Setting MTReg to default value for normal light environment");
67 | } else {
68 | Serial.println("Error setting MTReg to default value for normal light environment");
69 | }
70 | } else {
71 | if (lux <= 10.0) { // very low light environment
72 | if (lightMeter.setMTreg(138)) {
73 | Serial.println("Setting MTReg to high value for low light environment");
74 | } else {
75 | Serial.println("Error setting MTReg to high value for low light environment"));
76 | }
77 | }
78 | }
79 | }*/
80 | Luminosity = generateLumString(lux/luminousEfficacy);
81 | seventhLine = "Luminosity : ";
82 | seventhLine += Luminosity;
83 | seventhLine += " W/m2";
84 | }
85 |
86 | }
--------------------------------------------------------------------------------
/src/bh1750_utils.h:
--------------------------------------------------------------------------------
1 | #ifndef BH1750_UTILS_H_
2 | #define BH1750_UTILS_H_
3 |
4 | #include
5 |
6 | namespace BH1750_Utils {
7 |
8 | void setup();
9 | void readSensor();
10 |
11 | }
12 |
13 | #endif
--------------------------------------------------------------------------------
/src/bme280_utils.cpp:
--------------------------------------------------------------------------------
1 | #include "boards_pinout.h"
2 | #include
3 | #include
4 | #include
5 | #include "configuration.h"
6 | #include "display.h"
7 | #include "utils.h"
8 |
9 | #define SEALEVELPRESSURE_HPA (1013.25)
10 | #define heightCorrectionFactor (8.2296) // for meters
11 |
12 |
13 | extern Configuration Config;
14 |
15 | /*********** TO BE ADDED FROM CONFIGURATION ***********/
16 | //int heightCorrection = 0;
17 | //float temperatureCorrection = 0.0;
18 | /******************************************************/
19 |
20 | extern String secondLine;
21 | extern String thirdLine;
22 | extern String fourthLine;
23 |
24 | extern uint8_t bme280Addr;
25 |
26 | bool bme280SensorFound = false;
27 | String Temperature = "...";
28 | String Humidity = "..";
29 | String BarometricPressure = ".....";
30 |
31 |
32 | namespace BME280_Utils {
33 |
34 | Adafruit_BME280 bme280;
35 |
36 | void setup() {
37 | if (bme280Addr != 0x00) {
38 | bool status;
39 | status = bme280.begin(bme280Addr);
40 | if (!status) {
41 | Serial.println("Could not initialize BME280 , check sensor!");
42 | displayShow("ERROR", "", "BME280 found but", "could not init ...", 2000);
43 | } else {
44 | bme280.setSampling(Adafruit_BME280::MODE_FORCED,
45 | Adafruit_BME280::SAMPLING_X1,
46 | Adafruit_BME280::SAMPLING_X1,
47 | Adafruit_BME280::SAMPLING_X1,
48 | Adafruit_BME280::FILTER_OFF
49 | );
50 | Serial.println("init : BME280 Module ... done!");
51 | bme280SensorFound = true;
52 | }
53 | } else {
54 | Serial.println("Could not find a BME280 sensor, check wiring!");
55 | displayShow("ERROR", "", "BME280 NOT FOUND !!!", "", 2000);
56 | }
57 | }
58 |
59 | String generateTempString(float bmeTemp) {
60 | String strTemp;
61 | strTemp = String((int)bmeTemp);
62 | switch (strTemp.length()) {
63 | case 1:
64 | return "00" + strTemp;
65 | break;
66 | case 2:
67 | return "0" + strTemp;
68 | break;
69 | case 3:
70 | return strTemp;
71 | break;
72 | default:
73 | return "-999";
74 | }
75 | }
76 |
77 | String generateHumString(float bmeHum) {
78 | String strHum;
79 | strHum = String((int)bmeHum);
80 | switch (strHum.length()) {
81 | case 1:
82 | return "0" + strHum;
83 | break;
84 | case 2:
85 | return strHum;
86 | break;
87 | case 3:
88 | if ((int)bmeHum == 100) {
89 | return "00";
90 | } else {
91 | return "-99";
92 | }
93 | break;
94 | default:
95 | return "-99";
96 | }
97 | }
98 |
99 | String generatePresString(float bmePress) {
100 | String strPress = String((int)bmePress);
101 | String decPress = String(int((bmePress - int(bmePress)) * 10));
102 | switch (strPress.length()) {
103 | case 1:
104 | return "000" + strPress + "0";
105 | break;
106 | case 2:
107 | return "00" + strPress + "0";
108 | break;
109 | case 3:
110 | return "0" + strPress + "0";
111 | break;
112 | case 4:
113 | return strPress + "0";
114 | break;
115 | case 5:
116 | return strPress;
117 | break;
118 | default:
119 | return "-99999";
120 | }
121 | }
122 |
123 | void readSensor() {
124 | float newHum, newTemp, newPress;
125 | bme280.takeForcedMeasurement();
126 | newTemp = bme280.readTemperature();
127 | newPress = (bme280.readPressure() / 100.0F);
128 | newHum = bme280.readHumidity();
129 | if (isnan(newTemp) || isnan(newHum) || isnan(newPress)) {
130 | Serial.println("BME280 Module data failed");
131 | Temperature = "...";
132 | Humidity = "..";
133 | BarometricPressure = ".....";
134 | } else {
135 | Temperature = generateTempString(((newTemp + Config.sensors.bem280TemperatureCorrection) * 1.8) + 32);
136 | Humidity = generateHumString(newHum);
137 | BarometricPressure = generatePresString(newPress + (Config.sensors.bme280HeightCorrection/heightCorrectionFactor));
138 | }
139 | secondLine = "Temp : ";
140 | secondLine += Temperature;
141 | secondLine += " F";
142 |
143 | thirdLine = "Humidity : ";
144 | thirdLine += Humidity;
145 | thirdLine += " %";
146 |
147 | fourthLine = "Pressure : ";
148 | fourthLine += BarometricPressure.substring(0,BarometricPressure.length() - 1);
149 | fourthLine += " hPA";
150 | }
151 |
152 | }
--------------------------------------------------------------------------------
/src/bme280_utils.h:
--------------------------------------------------------------------------------
1 | #ifndef BME280_UTILS_H_
2 | #define BME280_UTILS_H_
3 |
4 | #include
5 |
6 | namespace BME280_Utils {
7 |
8 | void setup();
9 | void readSensor();
10 |
11 | }
12 |
13 | #endif
--------------------------------------------------------------------------------
/src/boards_pinout.h:
--------------------------------------------------------------------------------
1 | #ifndef PINS_CONFIG_H_
2 | #define PINS_CONFIG_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_RESET -1
11 |
12 | #define LedPin 25
13 | #define windInfoAddrSwitchPin 13
14 | #define windChangeAddrSwitchPin 14
15 |
16 | // LoRA SX1278
17 | #define RADIO_SCLK_PIN 5
18 | #define RADIO_MISO_PIN 19
19 | #define RADIO_MOSI_PIN 27
20 | #define RADIO_CS_PIN 18
21 | #define RADIO_RST_PIN 14
22 | #define RADIO_BUSY_PIN 26
23 |
24 | // RS485 to TTL
25 | #define RS485_RXD 34 // UART1 RXD pin
26 | #define RS485_TXD 4 // UART1 TXD pin
27 |
28 | // RAIN
29 | #define rainSwitchPin 15
30 |
31 | #endif
32 |
33 | // ***** Connections ******
34 | /* BH1750 / GY-302 ESP32/LoRa32 v1.6/v2.1
35 |
36 | VCC 3.3V
37 | GND GND
38 | SDA io_21
39 | SCL io_22
40 | ADDR ---
41 |
42 | */
43 |
44 | /* BME280 uses the same I2C pinouts as BH1750 */
45 |
46 | /* RS485 to TTLWind Direction Sensor
47 |
48 | ESP32 RS485/TTL | RS485/TTL RS485-Wind/DirectionSensor
49 | 3.3V VCC | --- Brown (to 10-30 VDC external supply)
50 | 34 RXD | A+ Yellow
51 | 4 TXD | B- Blue
52 | GND GND | GND Black
53 |
54 | */
--------------------------------------------------------------------------------
/src/configuration.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include "configuration.h"
4 | #include "display.h"
5 |
6 |
7 | bool Configuration::readFile() {
8 | Serial.println("Reading config..");
9 |
10 | File configFile = SPIFFS.open("/wxStation_conf.json", "r");
11 |
12 | if (configFile) {
13 | StaticJsonDocument<2560> data;
14 |
15 | DeserializationError error = deserializeJson(data, configFile);
16 | if (error) {
17 | Serial.println("Failed to read file, using default configuration");
18 | }
19 |
20 | callsign = data["callsign"] | "NOCALL-10";
21 |
22 | beacon.latitude = data["beacon"]["latitude"] | 0.0;
23 | beacon.longitude = data["beacon"]["longitude"] | 0.0;
24 | beacon.comment = data["beacon"]["comment"] | "Experimental LoRa APRS Wx Station";
25 | beacon.interval = data["beacon"]["interval"] | 10;
26 | beacon.overlay = data["beacon"]["overlay"] | "L";
27 | beacon.symbol = data["beacon"]["symbol"] | "_";
28 | beacon.path = data["beacon"]["path"] | "WIDE1-1";
29 |
30 | loramodule.txFreq = data["lora"]["txFreq"] | 433775000;
31 | loramodule.spreadingFactor = data["lora"]["spreadingFactor"] | 12;
32 | loramodule.signalBandwidth = data["lora"]["signalBandwidth"] | 125000;
33 | loramodule.codingRate4 = data["lora"]["codingRate4"] | 5;
34 | loramodule.power = data["lora"]["power"] | 20;
35 |
36 | display.alwaysOn = data["display"]["alwaysOn"] | true;
37 | display.timeout = data["display"]["timeout"] | 4;
38 | display.turn180 = data["display"]["turn180"] | false;
39 |
40 | sensors.bme280Active = data["sensors"]["bme280Active"] | true;
41 | sensors.bme280HeightCorrection = data["sensors"]["bme280HeightCorrection"] | 0;
42 | sensors.bem280TemperatureCorrection = data["sensors"]["bme280TemperatureCorrection"] | 0.0;
43 | sensors.bh1750Active = data["sensors"]["bh1750Active"] | true;
44 | sensors.windDirectionActive = data["sensors"]["windDirectionActive"] | true;
45 | sensors.windSpeedActive = data["sensors"]["windSpeedActive"] | true;
46 | sensors.rainActive = data["sensors"]["rainActive"] | true;
47 |
48 |
49 | configFile.close();
50 | Serial.println("Config read successfuly");
51 | return true;
52 | } else {
53 | Serial.println("Config file not found");
54 | return false;
55 | }
56 | }
57 |
58 | Configuration::Configuration() {
59 | if (!SPIFFS.begin(false)) {
60 | Serial.println("SPIFFS Mount Failed");
61 | return;
62 | } else {
63 | Serial.println("SPIFFS Mounted");
64 | }
65 | readFile();
66 | }
--------------------------------------------------------------------------------
/src/configuration.h:
--------------------------------------------------------------------------------
1 | #ifndef CONFIGURATION_H_
2 | #define CONFIGURATION_H_
3 |
4 | #include
5 | #include
6 | #include
7 |
8 | class Beacon {
9 | public:
10 | double latitude;
11 | double longitude;
12 | String comment;
13 | int interval;
14 | String overlay;
15 | String symbol;
16 | String path;
17 | };
18 |
19 |
20 | class LoraModule {
21 | public:
22 | long txFreq;
23 | int spreadingFactor;
24 | long signalBandwidth;
25 | int codingRate4;
26 | int power;
27 | };
28 |
29 | class Display {
30 | public:
31 | bool alwaysOn;
32 | int timeout;
33 | bool turn180;
34 | };
35 |
36 | class SENSORS {
37 | public:
38 | bool bme280Active;
39 | int bme280HeightCorrection;
40 | float bem280TemperatureCorrection;
41 | bool bh1750Active;
42 | bool windDirectionActive;
43 | bool windSpeedActive;
44 | bool rainActive;
45 | };
46 |
47 |
48 | class Configuration {
49 | public:
50 | String callsign;
51 | Beacon beacon;
52 | LoraModule loramodule;
53 | Display display;
54 | SENSORS sensors;
55 |
56 | void init();
57 | void writeFile();
58 | Configuration();
59 |
60 | private:
61 | bool readFile();
62 | String _filePath;
63 | };
64 |
65 | #endif
--------------------------------------------------------------------------------
/src/display.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 | #include "configuration.h"
5 | #include "boards_pinout.h"
6 | #include "display.h"
7 |
8 | extern Configuration Config;
9 |
10 | Adafruit_SSD1306 display(128, 64, &Wire, OLED_RESET);
11 |
12 | void displaySetup() {
13 | Wire.begin(OLED_SDA, OLED_SCL);
14 |
15 | if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
16 | Serial.println(F("SSD1306 allocation failed"));
17 | for(;;);
18 | }
19 | if (Config.display.turn180) {
20 | display.setRotation(2);
21 | }
22 | display.clearDisplay();
23 | display.setTextColor(WHITE);
24 | display.setTextSize(1);
25 | display.setCursor(0, 0);
26 | display.ssd1306_command(SSD1306_SETCONTRAST);
27 | display.ssd1306_command(1);
28 | display.display();
29 | delay(1000);
30 | }
31 |
32 | void displayToggle(bool toggle) {
33 | if (toggle) {
34 | display.ssd1306_command(SSD1306_DISPLAYON);
35 | } else {
36 | display.ssd1306_command(SSD1306_DISPLAYOFF);
37 | }
38 | }
39 |
40 | void displayShow(const String& header, const String& line1, const String& line2, const String& line3, int wait) {
41 | display.clearDisplay();
42 | display.setTextColor(WHITE);
43 | display.setTextSize(1);
44 | display.setCursor(0, 0);
45 | display.println(header);
46 |
47 | const String* const lines[] = {&line1, &line2, &line3};
48 | for (int i = 0; i < 3; i++) {
49 | display.setCursor(0, 8 + (8 * i));
50 | display.println(*lines[i]);
51 | }
52 |
53 | display.ssd1306_command(SSD1306_SETCONTRAST);
54 | display.ssd1306_command(1);
55 | display.display();
56 | delay(wait);
57 | }
58 |
59 | void displayShow(const String& header, const String& line1, const String& line2, const String& line3, const String& line4, const String& line5, const String& line6, int wait) {
60 | display.clearDisplay();
61 | display.setTextColor(WHITE);
62 | display.setTextSize(2);
63 | display.setCursor(0, 0);
64 | display.println(header);
65 | display.setTextSize(1);
66 |
67 | const String* const lines[] = {&line1, &line2, &line3, &line4, &line5, &line6};
68 | for (int i = 0; i < 6; i++) {
69 | display.setCursor(0, 16 + (8 * i));
70 | display.println(*lines[i]);
71 | }
72 | display.ssd1306_command(SSD1306_SETCONTRAST);
73 | display.ssd1306_command(1);
74 | display.display();
75 | delay(wait);
76 | }
--------------------------------------------------------------------------------
/src/display.h:
--------------------------------------------------------------------------------
1 | #ifndef DISPLAY_H_
2 | #define DISPLAY_H_
3 |
4 | #include
5 |
6 | #define SCREEN_ADDRESS 0x3C ///< See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32
7 |
8 | void displaySetup();
9 | void displayToggle(bool toggle);
10 |
11 | void displayShow(const String& header, const String& line1, const String& line2, const String& line3, int wait = 0);
12 | void displayShow(const String& header, const String& line1, const String& line2, const String& line3, const String& line4, const String& line5, const String& line6, int wait = 0);
13 |
14 | #endif
--------------------------------------------------------------------------------
/src/gps_utils.cpp:
--------------------------------------------------------------------------------
1 | #include "configuration.h"
2 | #include "gps_utils.h"
3 |
4 |
5 | extern Configuration Config;
6 |
7 | String beaconPacket;
8 |
9 |
10 | namespace GPS_Utils {
11 |
12 | char *ax25_base91enc(char *s, uint8_t n, uint32_t v) {
13 | for(s += n, *s = '\0'; n; n--) {
14 | *(--s) = v % 91 + 33;
15 | v /= 91;
16 | }
17 | return(s);
18 | }
19 |
20 | String encodeGPS(float latitude, float longitude, const String& overlay, const String& symbol) {
21 | String encodedData = overlay;
22 | uint32_t aprs_lat, aprs_lon;
23 | aprs_lat = 900000000 - latitude * 10000000;
24 | aprs_lat = aprs_lat / 26 - aprs_lat / 2710 + aprs_lat / 15384615;
25 | aprs_lon = 900000000 + longitude * 10000000 / 2;
26 | aprs_lon = aprs_lon / 26 - aprs_lon / 2710 + aprs_lon / 15384615;
27 |
28 | String Ns, Ew, helper;
29 | if(latitude < 0) { Ns = "S"; } else { Ns = "N"; }
30 | if(latitude < 0) { latitude= -latitude; }
31 |
32 | if(longitude < 0) { Ew = "W"; } else { Ew = "E"; }
33 | if(longitude < 0) { longitude= -longitude; }
34 |
35 | char helper_base91[] = {"0000\0"};
36 | int i;
37 | ax25_base91enc(helper_base91, 4, aprs_lat);
38 | for (i = 0; i < 4; i++) {
39 | encodedData += helper_base91[i];
40 | }
41 | ax25_base91enc(helper_base91, 4, aprs_lon);
42 | for (i = 0; i < 4; i++) {
43 | encodedData += helper_base91[i];
44 | }
45 | encodedData += symbol + " wX";
46 | return encodedData;
47 | }
48 |
49 | void generateBeacon() {
50 | beaconPacket = Config.callsign;
51 | beaconPacket += ">APLRW1";
52 | if (Config.beacon.path != "") {
53 | beaconPacket += ",";
54 | beaconPacket += Config.beacon.path;
55 | }
56 | beaconPacket += ":!";
57 | beaconPacket += encodeGPS(Config.beacon.latitude, Config.beacon.longitude, "L", "_");
58 | }
59 |
60 | }
--------------------------------------------------------------------------------
/src/gps_utils.h:
--------------------------------------------------------------------------------
1 | #ifndef GPS_UTILS_H_
2 | #define GPS_UTILS_H_
3 |
4 | #include
5 |
6 |
7 | namespace GPS_Utils {
8 |
9 | char *ax25_base91enc(char *s, uint8_t n, uint32_t v);
10 | String encodeGPS(float latitude, float longitude, const String& overlay, const String& symbol);
11 | void generateBeacon();
12 |
13 | }
14 |
15 | #endif
--------------------------------------------------------------------------------
/src/lora_utils.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include "configuration.h"
4 | #include "boards_pinout.h"
5 |
6 | extern Configuration Config;
7 |
8 | #ifdef HAS_SX1278
9 | SX1278 radio = new Module(RADIO_CS_PIN, RADIO_BUSY_PIN, RADIO_RST_PIN);
10 | #endif
11 |
12 | bool operationDone = true;
13 | bool transmitFlag = true;
14 |
15 | namespace LoRa_Utils {
16 |
17 | void setFlag(void) {
18 | operationDone = true;
19 | }
20 |
21 | void setup() {
22 | //Serial.println("LoRa Set SPI pins!");
23 | SPI.begin(RADIO_SCLK_PIN, RADIO_MISO_PIN, RADIO_MOSI_PIN);
24 | float freq = (float)Config.loramodule.txFreq / 1000000;
25 | int state = radio.begin(freq);
26 | if (state == RADIOLIB_ERR_NONE) {
27 | //Serial.println("Initializing LoRa Module");
28 | } else {
29 | Serial.println("Starting LoRa failed! State: " + String(state));
30 | while (true);
31 | }
32 | #if defined(HAS_SX1278)
33 | radio.setDio0Action(setFlag, RISING);
34 | #endif
35 | radio.setSpreadingFactor(Config.loramodule.spreadingFactor);
36 | float signalBandwidth = Config.loramodule.signalBandwidth/1000;
37 | radio.setBandwidth(signalBandwidth);
38 | radio.setCodingRate(Config.loramodule.codingRate4);
39 | radio.setCRC(true);
40 | state = radio.setOutputPower(Config.loramodule.power);
41 | radio.setCurrentLimit(100);
42 | if (state == RADIOLIB_ERR_NONE) {
43 | Serial.println("init : LoRa Module ... done!");
44 | } else {
45 | Serial.println("Starting LoRa failed! State: " + String(state));
46 | while (true);
47 | }
48 | }
49 |
50 | void sendNewPacket(const String& newPacket) {
51 | digitalWrite(LedPin, HIGH);
52 | int state = radio.transmit("\x3c\xff\x01" + newPacket);
53 | transmitFlag = true;
54 | if (state == RADIOLIB_ERR_NONE) {
55 | //Serial.print("---> LoRa Packet Tx : ");
56 | //Serial.println(newPacket);
57 | } else {
58 | Serial.print(F("failed, code "));
59 | Serial.println(String(state));
60 | }
61 | digitalWrite(LedPin, LOW);
62 | }
63 |
64 | }
--------------------------------------------------------------------------------
/src/lora_utils.h:
--------------------------------------------------------------------------------
1 | #ifndef LORA_UTILS_H_
2 | #define LORA_UTILS_H_
3 |
4 | #include
5 |
6 | namespace LoRa_Utils {
7 |
8 | void setup();
9 | void sendNewPacket(const String& newPacket);
10 |
11 | }
12 |
13 | #endif
--------------------------------------------------------------------------------
/src/rain_utils.cpp:
--------------------------------------------------------------------------------
1 | #include "boards_pinout.h"
2 | #include
3 |
4 | extern String sixthLine;
5 |
6 | float rain60MinArray[60] = {0.0};
7 | float rain24HArray[24] = {0.0};
8 | int rainTippingCounter = 0;
9 | float rainBucketMM = 0.2794; // mm watter or 0.3537
10 | int rain60MinIndex = -1;
11 | int rain24HIndex = 0;
12 | int rainSwitchState = HIGH;
13 | int rainLastSwitchState = HIGH;
14 | uint32_t lastDebounceTime = 0;
15 | int debounceDelay = 50;
16 |
17 | String RainLastHr, RainLast24Hr;
18 |
19 | namespace RAIN_Utils {
20 |
21 | String generateRain1HString() { // last 1 Hr
22 | float rain1H = 0;
23 | for (int i = 0; i < 60; i++) rain1H += rain60MinArray[i];
24 | String rain1HStr = String((int)(rain1H * 3.93701)); // mmToHundredthsInch
25 | switch (rain1HStr.length()) {
26 | case 1:
27 | return "00" + rain1HStr;
28 | break;
29 | case 2:
30 | return "0" + rain1HStr;
31 | break;
32 | case 3:
33 | return rain1HStr;
34 | break;
35 | default:
36 | return "000";
37 | break;
38 | }
39 | }
40 |
41 | String generateRain24HString() { // last 24 Hr
42 | float rain24H = 0;
43 | for (int i = 0; i < 24; i++) rain24H += rain24HArray[i];
44 | String rain24HStr = String((int)(rain24H * 3.93701)); // mmToHundredthsInch
45 | switch (rain24HStr.length()) {
46 | case 1:
47 | return "00" + rain24HStr;
48 | break;
49 | case 2:
50 | return "0" + rain24HStr;
51 | break;
52 | case 3:
53 | return rain24HStr;
54 | break;
55 | default:
56 | return "000";
57 | break;
58 | }
59 | }
60 |
61 | void generateData() {
62 | RainLastHr = generateRain1HString();
63 | RainLast24Hr = generateRain24HString();
64 | sixthLine = "R1h: ";
65 | sixthLine += RainLastHr;
66 | sixthLine += " / R24hr: ";
67 | sixthLine += RainLast24Hr;
68 | }
69 |
70 | void processMinute() {
71 | if (rain60MinIndex > -1) {
72 | rain60MinArray[rain60MinIndex] = rainTippingCounter * rainBucketMM;
73 | rain24HArray[rain24HIndex] += rainTippingCounter * rainBucketMM;
74 | }
75 | if (rain60MinIndex == 59) {
76 | rain24HIndex = (rain24HIndex + 1) % 24;
77 | rain24HArray[rain24HIndex] = 0.0;
78 | }
79 | rain60MinIndex = (rain60MinIndex + 1) % 60;
80 | rainTippingCounter = 0;
81 | }
82 |
83 | void loop() {
84 | int rainSwitchReading = digitalRead(rainSwitchPin);
85 | if (rainSwitchReading != rainLastSwitchState) { // Check if the button state has changed
86 | lastDebounceTime = millis(); // Reset the debounce timer
87 | }
88 | if ((millis() - lastDebounceTime) > debounceDelay) { // Check if the debounce delay has elapsed
89 | if (rainSwitchReading != rainSwitchState) { // Update the button state only if the button state has been stable for the debounce delay
90 | rainSwitchState = rainSwitchReading;
91 | if (rainSwitchState == LOW) {
92 | rainTippingCounter++;
93 | }
94 | }
95 | }
96 | rainLastSwitchState = rainSwitchReading; // Save the current button state for comparison in the next iteration
97 | }
98 |
99 | }
--------------------------------------------------------------------------------
/src/rain_utils.h:
--------------------------------------------------------------------------------
1 | #ifndef RAIN_UTILS_H_
2 | #define RAIN_UTILS_H_
3 |
4 | #include
5 |
6 | namespace RAIN_Utils {
7 |
8 | void generateData();
9 | void processMinute();
10 | void loop();
11 |
12 | }
13 |
14 | #endif
--------------------------------------------------------------------------------
/src/utils.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include "wind_rs485_utils.h"
3 | #include "boards_pinout.h"
4 | #include "utils.h"
5 | #include "display.h"
6 |
7 |
8 | uint8_t bh1750Addr = 0x00;
9 | uint8_t bme280Addr = 0x00;
10 |
11 |
12 | namespace Utils {
13 |
14 | void pinDeclarations() {
15 | pinMode(LedPin, OUTPUT);
16 | pinMode(rainSwitchPin,INPUT_PULLUP);
17 | pinMode(windInfoAddrSwitchPin, INPUT_PULLDOWN);
18 | pinMode(windChangeAddrSwitchPin, INPUT_PULLDOWN);
19 | delay(500);
20 | }
21 |
22 | void checkWindDireccionSensorAddress() {
23 | displayShow("SENSOR ID", "Starting :", "RS485 Wind Direction", "Sensor Address", "Identifier...", "check -->", "Serial output");
24 | Serial.println("Starting : RS485 'Wind Direction' Sensor Address Identifier...");
25 | WIND_RS485_Utils::setup();
26 | delay(1000);
27 | while(1) {
28 | WIND_RS485_Utils::checkSensorAddress();
29 | delay(4000);
30 | }
31 | }
32 |
33 | void changeWindDireccionSensorAddress() {
34 | displayShow("SENSOR ID", "Starting :", "RS485 Wind Direction", "Sensor Address", "Change Procedure...", "check -->", "Serial output");
35 | Serial.println("RS485 Sensor address change procedure.");
36 | WIND_RS485_Utils::setup();
37 | delay(1000);
38 | WIND_RS485_Utils::changeSensorAddress();
39 | }
40 |
41 | void checkSwitchesStates() {
42 | if (digitalRead(windInfoAddrSwitchPin) == HIGH && digitalRead(windChangeAddrSwitchPin) == LOW) {
43 | checkWindDireccionSensorAddress();
44 | } else if (digitalRead(windInfoAddrSwitchPin) == LOW && digitalRead(windChangeAddrSwitchPin) == HIGH) {
45 | changeWindDireccionSensorAddress();
46 | }
47 | }
48 |
49 | void getI2CAddresses() {
50 | uint8_t err, addr;
51 | for(addr = 1; addr < 0x7F; addr++) {
52 | Wire.beginTransmission(addr);
53 | err = Wire.endTransmission();
54 | if (err == 0) {
55 | //Serial.println(addr); this shows any connected board to I2C
56 | if (addr == 0x23) {
57 | bh1750Addr = addr;
58 | } else if (addr == 0x76 || addr == 0x77) {
59 | bme280Addr = addr;
60 | }
61 | }
62 | }
63 | }
64 | }
--------------------------------------------------------------------------------
/src/utils.h:
--------------------------------------------------------------------------------
1 | #ifndef UTILS_H_
2 | #define UTILS_H_
3 |
4 | #include
5 |
6 | namespace Utils {
7 |
8 | void pinDeclarations();
9 | void checkWindDireccionSensorAddress();
10 | void changeWindDireccionSensorAddress();
11 | void checkSwitchesStates();
12 | void getI2CAddresses();
13 |
14 | }
15 |
16 | #endif
--------------------------------------------------------------------------------
/src/wind_rs485_utils.cpp:
--------------------------------------------------------------------------------
1 | #include "boards_pinout.h"
2 | #include "wind_rs485_utils.h"
3 | #include "display.h"
4 |
5 |
6 | extern HardwareSerial rs485Serial;
7 | extern String fifthLine;
8 |
9 | int windArrayIndex = 0;
10 | float windSpeedArray[10] = {0.0};
11 | int windDirectionArray[10] = {0};
12 | uint8_t OldSensorAddress = 0x01;
13 | uint8_t NewSensorAddress = 0x02;
14 |
15 | String WindSpeedMs, WindSpeedKmH, WindSpeedMpH, WindGust, WindAngle, WindDirection;
16 |
17 |
18 | namespace WIND_RS485_Utils {
19 |
20 | void setup() {
21 | rs485Serial.begin(4800,SERIAL_8N1,RS485_RXD,RS485_TXD); // default speed in bauds
22 | Serial.println("init : RS485 Module ... done!");
23 | }
24 |
25 | void generateWindSpeedString() {
26 | float speedSum = 0;
27 | for (int i = 0; i < 10; i++) {
28 | speedSum += windSpeedArray[i];
29 | }
30 | WindSpeedMs = String((speedSum/10),1); // meter/seg
31 | WindSpeedKmH = String((int)((speedSum/10) * 3.6)); // Kilometer/Hour
32 | WindSpeedMpH = String((int)((speedSum/10) * 2.23694)); // Miles/Hour
33 | switch (WindSpeedMpH.length()) {
34 | case 1:
35 | WindSpeedMpH = "00" + WindSpeedMpH;
36 | break;
37 | case 2:
38 | WindSpeedMpH = "0" + WindSpeedMpH;
39 | break;
40 | default:
41 | break;
42 | }
43 | }
44 |
45 | void generateWindGustString() {
46 | float temp = 0;
47 | for (int i = 0; i < 10; i++) {
48 | if (windSpeedArray[i] > temp) {
49 | temp = windSpeedArray[i];
50 | }
51 | }
52 | WindGust = String((int)(temp * 2.23694)); // Miles/Hour
53 | switch (WindGust.length()) {
54 | case 1:
55 | WindGust = "00" + WindGust;
56 | break;
57 | case 2:
58 | WindGust = "0" + WindGust;
59 | break;
60 | default:
61 | break;
62 | }
63 | }
64 |
65 | void generateWindDirectionString() {
66 | double sumSin = 0.0;
67 | double sumCos = 0.0;
68 |
69 | for (int j = 0; j < 10; j++) { // Convert angles to radians and sum up sin and cos components
70 | double angleRad = windDirectionArray[j] * PI / 180.0; // Convert to radians
71 | sumSin += sin(angleRad);
72 | sumCos += cos(angleRad);
73 | }
74 | double meanRad = atan2((sumSin / 10), (sumCos / 10)); // Calculate mean direction in radians using atan2
75 |
76 | int meanDeg = (int)(meanRad * 180.0 / PI); // Convert mean direction back to degrees
77 |
78 | meanDeg = (meanDeg + 360) % 360; // Ensure the result is within [0, 360) range
79 |
80 | String directionString = String((int)meanDeg);
81 | switch (directionString.length()) {
82 | case 1:
83 | WindAngle = "00" + directionString;
84 | break;
85 | case 2:
86 | WindAngle = "0" + directionString;
87 | break;
88 | case 3:
89 | WindAngle = directionString;
90 | break;
91 | }
92 |
93 | if ((meanDeg >= 338 && meanDeg <= 360) || (meanDeg >= 0 && meanDeg < 23)) {
94 | WindDirection = "N";
95 | } else if (meanDeg >= 23 && meanDeg < 68) {
96 | WindDirection = "NE";
97 | } else if (meanDeg >= 68 && meanDeg < 113) {
98 | WindDirection = "E";
99 | } else if (meanDeg >= 113 && meanDeg < 158) {
100 | WindDirection = "SE";
101 | } else if (meanDeg >= 158 && meanDeg < 203) {
102 | WindDirection = "S";
103 | } else if (meanDeg >= 203 && meanDeg < 248) {
104 | WindDirection = "SW";
105 | } else if (meanDeg >= 248 && meanDeg < 293) {
106 | WindDirection = "W";
107 | } else if (meanDeg >= 293 && meanDeg < 338) {
108 | WindDirection = "NW";
109 | }
110 | }
111 |
112 | size_t readN(uint8_t *buf, size_t len) {
113 | size_t offset = 0, left = len;
114 | int16_t Tineout = 1500;
115 | uint8_t *buffer = buf;
116 | long curr = millis();
117 | while (left) {
118 | if (rs485Serial.available()) {
119 | buffer[offset] = rs485Serial.read();
120 | offset++;
121 | left--;
122 | }
123 | if (millis() - curr > Tineout) {
124 | break;
125 | }
126 | }
127 | return offset;
128 | }
129 |
130 | void addCRC(uint8_t *buf, int len) {
131 | uint16_t crc = 0xFFFF;
132 | for (int pos = 0; pos < len; pos++) {
133 | crc ^= (uint16_t)buf[pos];
134 | for (int i = 8; i != 0; i--) {
135 | if ((crc & 0x0001) != 0) {
136 | crc >>= 1;
137 | crc^= 0xA001;
138 | } else {
139 | crc >>= 1;
140 | }
141 | }
142 | }
143 | buf[len] = crc % 0x100; // LSB CRC
144 | buf[len + 1] = crc / 0x100; // MSB CRC
145 | }
146 |
147 | uint16_t CRC16_2(uint8_t *buf, int16_t len) {
148 | uint16_t crc = 0xFFFF;
149 | for (int pos = 0; pos < len; pos++) {
150 | crc ^= (uint16_t)buf[pos];
151 | for (int i = 8; i != 0; i--) {
152 | if ((crc & 0x0001) != 0) {
153 | crc >>= 1;
154 | crc^= 0xA001;
155 | } else {
156 | crc >>= 1;
157 | }
158 | }
159 | }
160 | crc = ((crc & 0x00ff) << 8) | ((crc & 0xff00) >> 8);
161 | return crc;
162 | }
163 |
164 | float readWindSpeed(uint8_t address) {
165 | uint8_t Data[7] = {0}; //Store the original data packet returned by the sensor
166 | uint8_t COM[8] = {0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00}; //Command for reading wind speed, adjust for your sensor
167 | boolean ret = false; //Wind speed acquisition success flag
168 | float windSpeed = 0;
169 | long curr = millis();
170 | long curr1 = curr;
171 | uint8_t ch = 0;
172 | COM[0] = address; //Add the complete command package with reference to the communication protocol.
173 | addCRC(COM , 6); //Add CRC_16 check for reading wind speed commandpacket
174 | rs485Serial.write(COM, 8); //Send the command of reading the wind speed
175 |
176 | while (!ret) {
177 | if (millis() - curr > 1000) {
178 | windSpeed = -1; //If the wind speed has not been read for more than 1000 milliseconds, it will be regarded as a timeout and return -1.
179 | break;
180 | }
181 | if (millis() - curr1 > 100) {
182 | rs485Serial.write(COM, 8); //If the last command to read the wind speed is sent for more than 100 milliseconds and the return command has not been received, the command to read the wind speed will be re-sent
183 | curr1 = millis();
184 | }
185 | if ((readN(&ch, 1) == 1) && (ch == address)) { //Read and judge the packet header.
186 | Data[0] = ch;
187 | if ((readN(&ch, 1) == 1) && (ch == 0x03)) { //Read and judge the packet header.
188 | Data[1] = ch;
189 | if ((readN(&ch, 1) == 1) && (ch == 0x02)) { //Read and judge the packet header.
190 | Data[2] = ch;
191 | if ((readN(&Data[3], 4) == 4) && (CRC16_2(Data, 5) == (Data[5] * 256 + Data[6]))) { // Check CRC data packet
192 | ret = true;
193 | windSpeed = (Data[3] * 256 + Data[4]) / 10.00; // Calculate the wind speed
194 | }
195 | }
196 | }
197 | }
198 | }
199 | return windSpeed;
200 | }
201 |
202 | int readWindDirection(uint8_t address) {
203 | uint8_t Data[7] = {0}; //Store the original data packet returned by the sensor
204 | uint8_t COM[8] = {0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00}; // Command for reading wind direction, adjust to your sensor
205 | boolean ret = false; //Wind direction acquisition success flag
206 | int windDirection = 0;
207 | long curr = millis();
208 | long curr1 = curr;
209 | uint8_t ch = 0;
210 | COM[0] = address; //Add the complete command package with reference to the communication protocol.
211 | addCRC(COM , 6); //Add CRC_16 check for reading wind direction commandpacket
212 | rs485Serial.write(COM, 8); //Send the command of reading the wind direction
213 | while (!ret) {
214 | if (millis() - curr > 1000) {
215 | windDirection = -1; //If the wind direction has not been read for more than 1000 milliseconds, it will be regarded as a timeout and return -1.
216 | break;
217 | }
218 | if (millis() - curr1 > 100) {
219 | rs485Serial.write(COM, 8); //If the last command to read the wind directionissent for more than 100 milliseconds and the return command has not beenreceived, the command to read the wind direction will be re-sent
220 | curr1 = millis();
221 | }
222 | if ((readN(&ch, 1) == 1) && (ch == address)) { //Read and judge the packet header.
223 | Data[0] = ch;
224 | if ((readN(&ch, 1) == 1) && (ch == 0x03)) { //Read and judge the packet header.
225 | Data[1] = ch;
226 | if ((readN(&ch, 1) == 1) && (ch == 0x02)) { //Read and judge the packet header.
227 | Data[2] = ch;
228 | if ((readN(&Data[3], 4) == 4) && (CRC16_2(Data, 5) == (Data[5] * 256 + Data[6]))) { //Checkdata packet
229 | ret = true;
230 | windDirection = Data[3] * 256 + Data[4]; //Calculatethewind direction
231 | }
232 | }
233 | }
234 | }
235 | }
236 | return windDirection * 45;
237 | }
238 |
239 | void readSensor() {
240 | windSpeedArray[windArrayIndex] = readWindSpeed(0x01);
241 | delay(200);
242 | windDirectionArray[windArrayIndex] = readWindDirection(0x02);
243 | windArrayIndex = (windArrayIndex + 1) % 10;
244 | }
245 |
246 | void generateData() {
247 | generateWindSpeedString();
248 | generateWindGustString();
249 | generateWindDirectionString();
250 | fifthLine = "Wind: ";
251 | fifthLine += WindSpeedMs;
252 | fifthLine += "(";
253 | fifthLine += WindGust;
254 | fifthLine += ")m/s ";
255 | fifthLine += WindDirection;
256 | }
257 |
258 | void checkSensorAddress() {
259 | Serial.print("Sensor 0x00 --> ");
260 | if ((readWindDirection(0x00)/45) == -1) {
261 | Serial.println("not detected)");
262 | } else {
263 | Serial.println("detected)");
264 | }
265 | delay(500);
266 |
267 | Serial.print("Sensor 0x01 --> ");
268 | if ((readWindDirection(0x01)/45) == -1) {
269 | Serial.println("not detected)");
270 | } else {
271 | Serial.println("detected)");
272 | }
273 | delay(500);
274 |
275 | Serial.print("Sensor 0x02 --> ");
276 | if ((readWindDirection(0x02)/45) == -1) {
277 | Serial.println("not detected)");
278 | } else {
279 | Serial.println("detected)");
280 | }
281 | }
282 |
283 | boolean modifySensorAddress(uint8_t Address1, uint8_t Address2) {
284 | Serial.println("iniciando cambio addrress");
285 | uint8_t ModifyAddressCOM[8] = {0x00, 0x06, 0x07, 0xD0, 0x00, 0x00, 0x00, 0x00};
286 | boolean ret = false;
287 | long curr = millis();
288 | long curr1 = curr;
289 | uint8_t ch = 0;
290 | ModifyAddressCOM[0] = Address1;
291 | ModifyAddressCOM[5] = Address2;
292 | addCRC(ModifyAddressCOM , 6);
293 | /*Serial.println(ModifyAddressCOM[0]);
294 | Serial.println(ModifyAddressCOM[5]);
295 | Serial.println(ModifyAddressCOM[6]);
296 | Serial.println(ModifyAddressCOM[7]);*/
297 |
298 | rs485Serial.write(ModifyAddressCOM, 8); // sends the register modification request
299 | if (Address1 != 0x00) { // if original address != 0x00, check echoed command is ok = command adressed to 0x00 are not echoed
300 | while (!ret) {
301 | if (millis() - curr > 1000) {
302 | break;
303 | }
304 | if (millis() - curr1 > 100) {
305 | rs485Serial.write(ModifyAddressCOM, 8);
306 | curr1 = millis();
307 | }
308 | if ((readN(&ch, 1) == 1) && (ch == Address1)) {
309 | if ((readN(&ch, 1) == 1) && (ch == 0x06)) {
310 | if ((readN(&ch, 1) == 1) && (ch == 0x07)) {
311 | if ((readN(&ch, 1) == 1) && (ch == 0xD0)) {
312 | if ((readN(&ch, 1) == 1) && (ch == 0x00)) {
313 | if ((readN(&ch, 1) == 1) && (ch == Address2)) {
314 | ret = true ;
315 | } else {
316 | ret = false;
317 | }
318 | }
319 | }
320 | }
321 | }
322 | }
323 | }
324 | } else {
325 | ret = true; // return true if Address1 is 0x00
326 | }
327 | return ret;
328 | }
329 |
330 | void changeSensorAddress() {
331 | digitalWrite(LedPin, HIGH);
332 | if (!modifySensorAddress(OldSensorAddress,NewSensorAddress)) {
333 | Serial.println("RS485 No communication with sensor");
334 | displayShow("__RS485__", "", " NO Comunication", " with Sensor...", "", "", "", 0);
335 | } else {
336 | Serial.println("RS485 Address change OK");
337 | Serial.println("RS485 Release switch and repower the sensor !");
338 | displayShow("__RS485__","Address changed","","to 0x" + String(NewSensorAddress,HEX));
339 | }
340 | for (;;);
341 | }
342 |
343 | }
--------------------------------------------------------------------------------
/src/wind_rs485_utils.h:
--------------------------------------------------------------------------------
1 | #ifndef WIND_RS485_UTILS_H_
2 | #define WIND_RS485_UTILS_H_
3 |
4 | #include
5 |
6 | namespace WIND_RS485_Utils {
7 |
8 | void setup();
9 | void readSensor_WindSpeed();
10 | void readSensor_WindDirection();
11 | void readSensor();
12 | void generateData();
13 |
14 | void checkSensorAddress();
15 |
16 | boolean modifySensorAddress(uint8_t Address1, uint8_t Address2);
17 | void changeSensorAddress();
18 |
19 | }
20 |
21 | #endif
--------------------------------------------------------------------------------
/src/wx_utils.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include "wind_rs485_utils.h"
3 | #include "boards_pinout.h"
4 | #include "configuration.h"
5 | #include "bh1750_utils.h"
6 | #include "bme280_utils.h"
7 | #include "lora_utils.h"
8 | #include "rain_utils.h"
9 | #include "gps_utils.h"
10 | #include "wx_utils.h"
11 |
12 |
13 | extern Configuration Config;
14 | extern String Temperature;
15 | extern String Humidity;
16 | extern String BarometricPressure;
17 | extern String Luminosity;
18 | extern String WindAngle;
19 | extern String WindDirection;
20 | extern String WindSpeedMpH;
21 | extern String WindGust;
22 | extern String RainLastHr;
23 | extern String RainLast24Hr;
24 | extern String firstLine;
25 | extern bool bme280SensorFound;
26 | extern bool bh1750SensorFound;
27 | extern String beaconPacket;
28 | extern String versionDate;
29 |
30 | int sensorReadingInterval = 1; // min
31 | uint32_t lastSensorReading = 10000;
32 | uint32_t lastBeaconTx = 0;
33 | bool beaconUpdate = true; // deberia ser false por que no hay promedio!
34 | bool statusAfterBoot = true;
35 |
36 |
37 | namespace WX_Utils {
38 |
39 | String buildDataPacket() {
40 |
41 | if (Config.sensors.bh1750Active && bh1750SensorFound) {
42 | BH1750_Utils::readSensor(); // "L" si es menor que 1000 W/m2 y "l" si es >= 1000 W/m2 y reemplaza algunos de los campos de lluvia.
43 | } else {
44 | Luminosity = "...";
45 | }
46 |
47 | if (Config.sensors.bme280Active && bme280SensorFound) {
48 | BME280_Utils::readSensor();
49 | } else {
50 | Temperature = "...";
51 | Humidity = "..";
52 | BarometricPressure = ".....";
53 | }
54 |
55 | if (Config.sensors.windDirectionActive || Config.sensors.windSpeedActive) {
56 | WIND_RS485_Utils::generateData();
57 | } else {
58 | WindAngle = "...";
59 | WindSpeedMpH = "...";
60 | WindGust = "...";
61 | }
62 |
63 | if (Config.sensors.rainActive) {
64 | RAIN_Utils::generateData();
65 | } else {
66 | RainLastHr = "...";
67 | RainLast24Hr = "...";
68 | }
69 |
70 | String wxPacket = beaconPacket;
71 |
72 | wxPacket += WindAngle;
73 | wxPacket += "/";
74 | wxPacket += WindSpeedMpH;
75 | wxPacket += "g";
76 | wxPacket += WindGust;
77 |
78 | wxPacket += "t";
79 | wxPacket += Temperature;
80 |
81 | if (Config.sensors.bme280Active && bme280SensorFound) {
82 | wxPacket += "h";
83 | wxPacket += Humidity;
84 | wxPacket += "b";
85 | wxPacket += BarometricPressure;
86 | }
87 |
88 | if (Config.sensors.rainActive) {
89 | wxPacket += "r";
90 | wxPacket += RainLastHr;
91 | wxPacket += "p";
92 | wxPacket += RainLast24Hr;
93 | }
94 |
95 | if (Config.sensors.bh1750Active && bh1750SensorFound) {
96 | wxPacket += "L";
97 | wxPacket += Luminosity;
98 | }
99 |
100 | return wxPacket + Config.beacon.comment;
101 | }
102 |
103 | void processStatus() {
104 | delay(4000);
105 | String status = Config.callsign;
106 | status += ">APLRW1";
107 | if (Config.beacon.path != "") {
108 | status += ",";
109 | status += Config.beacon.path;
110 | }
111 | status += ":>https://github.com/richonguzman/LoRa_APRS_Wx_Station ";
112 | status += versionDate;
113 | LoRa_Utils::sendNewPacket(status);
114 | statusAfterBoot = false;
115 | }
116 |
117 | void loop() {
118 | if (Config.sensors.rainActive) RAIN_Utils::loop();
119 |
120 | uint32_t lastWind = millis() - lastSensorReading;
121 | if (lastWind >= sensorReadingInterval * 60 * 1000) {
122 | if (Config.sensors.windDirectionActive || Config.sensors.windSpeedActive) WIND_RS485_Utils::readSensor();
123 | if (Config.sensors.rainActive) RAIN_Utils::processMinute();
124 | lastSensorReading = millis();
125 | }
126 |
127 | uint32_t lastTx = millis() - lastBeaconTx;
128 | if (lastTx >= Config.beacon.interval * 60 * 1000) {
129 | beaconUpdate = true;
130 | }
131 | if (beaconUpdate) {
132 | String wxPacket = buildDataPacket();
133 | Serial.println("Sending LoRa APRS Packet ---> " + wxPacket);
134 | LoRa_Utils::sendNewPacket(wxPacket);
135 | lastBeaconTx = millis();
136 | beaconUpdate = false;
137 | }
138 | if (statusAfterBoot) {
139 | processStatus();
140 | }
141 | }
142 |
143 | void setupSensors() {
144 | Serial.println("Sensors INI...");
145 | if (Config.sensors.bme280Active) BME280_Utils::setup();
146 | if (Config.sensors.bh1750Active) BH1750_Utils::setup();
147 | if (Config.sensors.windDirectionActive || Config.sensors.windSpeedActive) WIND_RS485_Utils::setup();
148 | firstLine = Config.callsign;
149 | }
150 |
151 | }
--------------------------------------------------------------------------------
/src/wx_utils.h:
--------------------------------------------------------------------------------
1 | #ifndef WX_UTILS_H_
2 | #define WX_UTILS_H_
3 |
4 | #include
5 |
6 | namespace WX_Utils {
7 |
8 | void loop();
9 | void setupSensors();
10 |
11 | }
12 |
13 | #endif
--------------------------------------------------------------------------------
/test/README:
--------------------------------------------------------------------------------
1 |
2 | This directory is intended for PlatformIO Test Runner and project tests.
3 |
4 | Unit Testing is a software testing method by which individual units of
5 | source code, sets of one or more MCU program modules together with associated
6 | control data, usage procedures, and operating procedures, are tested to
7 | determine whether they are fit for use. Unit testing finds problems early
8 | in the development cycle.
9 |
10 | More information about PlatformIO Unit Testing:
11 | - https://docs.platformio.org/en/latest/advanced/unit-testing/index.html
12 |
--------------------------------------------------------------------------------