merging pull request #2 by DL2MF to devel branch
This commit is contained in:
parent
619631f80e
commit
d056ec8b76
20
README.md
20
README.md
|
|
@ -4,14 +4,20 @@ RDZ_TTGO_SONDE
|
|||
This a simple, experimental, not (well) tested, and incomplete decoder for
|
||||
radiosonde RS41 and DFM06/09 on a TTGO LoRa ESP32 with OLED display board.
|
||||
|
||||
There have been made some additions for TTGO LoRa ESP32 with only RST button.
|
||||
Please check also your OLED port settings, both versions use different ports.
|
||||
You can setup the depending ports in config.txt, OLED Setup is depending on hardware of LoRa board
|
||||
- TTGO v1: SDA=4 SCL=15, RST=16
|
||||
- TTGO v2: SDA=21 SCL=22, RST=16
|
||||
|
||||
## Button commands
|
||||
You can use the button on the board (not the reset button, the second one) to
|
||||
issue some commands. The software distinguishes between several inputs:
|
||||
|
||||
SHORT Short button press (<1.5 seconds)
|
||||
DOUBLE Short button press, followed by another button press within 0.5 seconds
|
||||
MID Medium-length button press (2-4 seconds)
|
||||
LONG Long button press (>5 seconds)
|
||||
- SHORT Short button press (<1.5 seconds)
|
||||
- DOUBLE Short button press, followed by another button press within 0.5 seconds
|
||||
- MID Medium-length button press (2-4 seconds)
|
||||
- LONG Long button press (>5 seconds)
|
||||
|
||||
## Wireless configuration
|
||||
|
||||
|
|
@ -39,10 +45,14 @@ for the last 18 frames, if reception was successfull (|) or failed (.)
|
|||
A DOUBLE press will switch to scanning mode.
|
||||
A SHORT press will switch to the next channel in channels.txt
|
||||
|
||||
# Spectrum mode
|
||||
## Spectrum mode
|
||||
|
||||
A medium press will active scan the whole band (400..406 MHz) and display a
|
||||
spectrum diagram (each line == 50 kHz)
|
||||
For TTGO boards without configurable button there are some new parameter in config.txt:
|
||||
- spectrum=10 // 0=off / 1-99 number of seconds to show spectrum after restart
|
||||
- timer=1 // 0=off / 1= show spectrum countdown timer in spectrum display
|
||||
- marker=1 // 0=off / 1= show channel edge freq in spectrum display
|
||||
|
||||
## Setup
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#include <ESPAsyncWebServer.h>
|
||||
#include <SPIFFS.h>
|
||||
#include <U8x8lib.h>
|
||||
#include <U8g2lib.h>
|
||||
#include <SPI.h>
|
||||
|
||||
#include <SX1278FSK.h>
|
||||
|
|
@ -12,11 +13,6 @@
|
|||
|
||||
#define LORA_LED 9
|
||||
|
||||
// I2C OLED Display works with SSD1306 driver
|
||||
//#define OLED_SDA 4
|
||||
//#define OLED_SCL 15
|
||||
//#define OLED_RST 16
|
||||
|
||||
// UNCOMMENT one of the constructor lines below
|
||||
U8X8_SSD1306_128X64_NONAME_SW_I2C *u8x8=NULL; // initialize later after reading config file
|
||||
//U8X8_SSD1306_128X64_NONAME_SW_I2C u8x8(/* clock=*/ OLED_SCL, /* data=*/ OLED_SDA, /* reset=*/ OLED_RST); // Unbuffered, basic graphics, software I2C
|
||||
|
|
@ -29,20 +25,14 @@ AsyncWebServer server(80);
|
|||
|
||||
#define LOCALUDPPORT 9002
|
||||
|
||||
// moved to sonde.config
|
||||
//const char * udpAddress = "192.168.42.20";
|
||||
//const int udpPort = 9002;
|
||||
|
||||
boolean connected = false;
|
||||
WiFiUDP udp;
|
||||
|
||||
|
||||
// Set LED GPIO
|
||||
const int ledPin = 2;
|
||||
int ledPin = 1;
|
||||
// Stores LED state
|
||||
String ledState;
|
||||
|
||||
|
||||
// Replaces placeholder with LED state value
|
||||
String processor(const String& var){
|
||||
Serial.println(var);
|
||||
|
|
@ -91,6 +81,7 @@ void setupChannelList() {
|
|||
}
|
||||
int i=0;
|
||||
sonde.clearSonde();
|
||||
Serial.println("Reading channel config:");
|
||||
while(file.available()) {
|
||||
String line = file.readStringUntil('\n');
|
||||
if(!file.available()) break;
|
||||
|
|
@ -105,8 +96,9 @@ void setupChannelList() {
|
|||
else if (space[1]=='6') { type=STYPE_DFM06; }
|
||||
else continue;
|
||||
int active = space[3]=='+'?1:0;
|
||||
Serial.printf("Adding %f with type %d (active: %d)\n",freq,type,active);
|
||||
sonde.addSonde(freq, type, active);
|
||||
char *launchsite = strchr(line.c_str(), ' ');
|
||||
Serial.printf("Add %f - type %d (on/off: %d)- Site: \n",freq,type,active,launchsite);
|
||||
sonde.addSonde(freq, type, active, launchsite);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
|
@ -114,7 +106,7 @@ void setupChannelList() {
|
|||
const char *createQRGForm() {
|
||||
char *ptr = message;
|
||||
strcpy(ptr,"<html><head><link rel=\"stylesheet\" type=\"text/css\" href=\"style.css\"></head><body><form action=\"qrg.html\" method=\"post\"><table><tr><th>ID</th><th>Active</th><th>Freq</th><th>Mode</th></tr>");
|
||||
for(int i=0; i<10; i++) {
|
||||
for(int i=0; i<sonde.config.maxsonde; i++) {
|
||||
String s = sondeTypeSelect(i>=sonde.nSonde?2:sonde.sondeList[i].type);
|
||||
sprintf(ptr+strlen(ptr), "<tr><td>%d</td><td><input name=\"A%d\" type=\"checkbox\" %s/></td>"
|
||||
"<td><input name=\"F%d\" type=\"text\" value=\"%3.3f\"></td>"
|
||||
|
|
@ -161,6 +153,9 @@ const char *handleQRGPost(AsyncWebServerRequest *request) {
|
|||
f.printf("%3.3f %c %c\n", atof(fstr), typech, active?'+':'-');
|
||||
}
|
||||
f.close();
|
||||
Serial.println("Channel setup finished");
|
||||
Serial.println();
|
||||
|
||||
setupChannelList();
|
||||
}
|
||||
|
||||
|
|
@ -252,9 +247,9 @@ void addSondeStatus(char *ptr, int i)
|
|||
sprintf(ptr+strlen(ptr),"<tr><td id=\"sfreq\">%3.3f MHz, Type: %s</td><tr><td>ID: %s</td></tr><tr><td>QTH: %.6f,%.6f h=%.0fm</td></tr>\n",
|
||||
s->freq, sondeTypeStr[s->type],
|
||||
s->validID?s->id:"<??>",
|
||||
s->lat, s->lon, s->hei);
|
||||
sprintf(ptr+strlen(ptr), "<tr><td><a target=\"_empty\" href=\"geo:%.6f,%.6f\">Geo-Ref</a> -", s->lat, s->lon);
|
||||
sprintf(ptr+strlen(ptr), "<a target=\"_empty\" href=\"https://www.google.com/maps/search/?api=1&query=%.6f,%.6f\">Google map</a> - ", s->lat, s->lon);
|
||||
s->lat, s->lon, s->alt);
|
||||
sprintf(ptr+strlen(ptr), "<tr><td><a target=\"_empty\" href=\"geo:%.6f,%.6f\">GEO-App</a> - ", s->lat, s->lon);
|
||||
sprintf(ptr+strlen(ptr), "<a target=\"_empty\" href=\"https://wx.dl2mf.de/?%s\">WX.DL2MF.de</a> - ", s->id);
|
||||
sprintf(ptr+strlen(ptr), "<a target=\"_empty\" href=\"https://www.openstreetmap.org/?mlat=%.6f&mlon=%.6f&zoom=14\">OSM</a></td></tr>", s->lat, s->lon);
|
||||
strcat(ptr, "</table><p/>\n");
|
||||
}
|
||||
|
|
@ -291,8 +286,12 @@ struct st_configitems {
|
|||
int type; // 0: numeric; i>0 string of length i; -1: separator; -2: type selector
|
||||
void *data;
|
||||
};
|
||||
#define N_CONFIG 16
|
||||
#define N_CONFIG 20
|
||||
struct st_configitems config_list[N_CONFIG] = {
|
||||
{"ShowSpectrum (s)", 0, &sonde.config.spectrum},
|
||||
{"Startfreq (MHz)", 0, &sonde.config.startfreq},
|
||||
{"Bandwidth (kHz)", 0, &sonde.config.channelbw},
|
||||
{"---", -1, NULL},
|
||||
{"Call", 8, sonde.config.call},
|
||||
{"Passcode", 8, sonde.config.passcode},
|
||||
{"---", -1, NULL},
|
||||
|
|
@ -415,11 +414,11 @@ void SetupAsyncServer() {
|
|||
|
||||
const char *fetchWifiPw(const char *id) {
|
||||
for(int i=0; i<nNetworks; i++) {
|
||||
Serial.print("Comparing '");
|
||||
Serial.print(id);
|
||||
Serial.print("' and '");
|
||||
Serial.print(networks[i].id.c_str());
|
||||
Serial.println("'");
|
||||
//Serial.print("Comparing '");
|
||||
//Serial.print(id);
|
||||
//Serial.print("' and '");
|
||||
//Serial.print(networks[i].id.c_str());
|
||||
//Serial.println("'");
|
||||
if(strcmp(id,networks[i].id.c_str())==0) return networks[i].pw.c_str();
|
||||
}
|
||||
return NULL;
|
||||
|
|
@ -471,28 +470,81 @@ int hasKeyPress() {
|
|||
|
||||
void setup()
|
||||
{
|
||||
char buf[12];
|
||||
// Open serial communications and wait for port to open:
|
||||
Serial.begin(115200);
|
||||
pinMode(LORA_LED, OUTPUT);
|
||||
|
||||
aprs_gencrctab();
|
||||
|
||||
pinMode(LORA_LED, OUTPUT);
|
||||
|
||||
// Initialize SPIFFS
|
||||
// Initialize SPIFFS
|
||||
if(!SPIFFS.begin(true)){
|
||||
Serial.println("An Error has occurred while mounting SPIFFS");
|
||||
return;
|
||||
}
|
||||
|
||||
setupWifiList();
|
||||
setupConfigData();
|
||||
button1.pin = sonde.config.button_pin;
|
||||
setupConfigData(); // configuration must be read first due to OLED ports!!!
|
||||
|
||||
u8x8 = new U8X8_SSD1306_128X64_NONAME_SW_I2C(/* clock=*/ sonde.config.oled_scl, /* data=*/ sonde.config.oled_sda, /* reset=*/ sonde.config.oled_rst); // Unbuffered, basic graphics, software I2C
|
||||
u8x8->begin();
|
||||
delay(100);
|
||||
|
||||
u8x8->clear();
|
||||
|
||||
u8x8->setFont(u8x8_font_7x14_1x2_r);
|
||||
u8x8->drawString(1, 1, "RDZ_TTGO_SONDE");
|
||||
u8x8->drawString(2, 3, " V0.1e");
|
||||
u8x8->drawString(1, 5, "Mods by DL2MF");
|
||||
delay(3000);
|
||||
|
||||
sonde.clearDisplay();
|
||||
|
||||
setupWifiList();
|
||||
button1.pin = sonde.config.button_pin;
|
||||
|
||||
// == show initial values from config.txt ========================= //
|
||||
if (sonde.config.debug == 1) {
|
||||
u8x8->setFont(u8x8_font_chroma48medium8_r);
|
||||
u8x8->drawString(0, 0, "Config:");
|
||||
|
||||
delay(500);
|
||||
itoa(sonde.config.oled_sda, buf, 10);
|
||||
u8x8->drawString(0, 1, " SDA:");
|
||||
u8x8->drawString(6, 1, buf);
|
||||
|
||||
delay(500);
|
||||
itoa(sonde.config.oled_scl, buf, 10);
|
||||
u8x8->drawString(0, 2, " SCL:");
|
||||
u8x8->drawString(6, 2, buf);
|
||||
|
||||
delay(500);
|
||||
itoa(sonde.config.oled_rst, buf, 10);
|
||||
u8x8->drawString(0, 3, " RST:");
|
||||
u8x8->drawString(6, 3, buf);
|
||||
|
||||
delay(1000);
|
||||
itoa(sonde.config.led_pin, buf, 10);
|
||||
u8x8->drawString(0, 4, " LED:");
|
||||
u8x8->drawString(6, 4, buf);
|
||||
|
||||
delay(500);
|
||||
itoa(sonde.config.spectrum, buf, 10);
|
||||
u8x8->drawString(0, 5, " SPEC:");
|
||||
u8x8->drawString(6, 5, buf);
|
||||
|
||||
delay(500);
|
||||
itoa(sonde.config.maxsonde, buf, 10);
|
||||
u8x8->drawString(0, 6, " MAX:");
|
||||
u8x8->drawString(6, 6, buf);
|
||||
|
||||
delay(5000);
|
||||
sonde.clearDisplay();
|
||||
}
|
||||
// == show initial values from config.txt ========================= //
|
||||
|
||||
|
||||
#if 0
|
||||
// == check the radio chip by setting default frequency =========== //
|
||||
if(rs41.setFrequency(402700000)==0) {
|
||||
Serial.println(F("Setting freq: SUCCESS "));
|
||||
} else {
|
||||
|
|
@ -501,6 +553,7 @@ void setup()
|
|||
float f = sx1278.getFrequency();
|
||||
Serial.print("Frequency set to ");
|
||||
Serial.println(f);
|
||||
// == check the radio chip by setting default frequency =========== //
|
||||
#endif
|
||||
|
||||
//sx1278.setLNAGain(-48);
|
||||
|
|
@ -511,11 +564,10 @@ void setup()
|
|||
Serial.println(gain);
|
||||
|
||||
// Print a success message
|
||||
Serial.println(F("sx1278 configured finished"));
|
||||
Serial.println();
|
||||
|
||||
Serial.println(F("SX1278 configuration finished"));
|
||||
|
||||
Serial.println("Setup finished");
|
||||
Serial.println();
|
||||
// int returnValue = pthread_create(&wifithread, NULL, wifiloop, (void *)0);
|
||||
|
||||
// if (returnValue) {
|
||||
|
|
@ -526,20 +578,26 @@ void setup()
|
|||
// Handle button press
|
||||
attachInterrupt(button1.pin, buttonISR, CHANGE);
|
||||
|
||||
// == setup default channel list if qrg.txt read fails =========== //
|
||||
setupChannelList();
|
||||
#if 0
|
||||
sonde.clearSonde();
|
||||
sonde.addSonde(402.300, STYPE_RS41);
|
||||
sonde.addSonde(402.700, STYPE_RS41);
|
||||
sonde.addSonde(405.700, STYPE_RS41);
|
||||
sonde.addSonde(405.900, STYPE_RS41);
|
||||
sonde.addSonde(403.450, STYPE_DFM09);
|
||||
Serial.println("No channel config file, using defaults!");
|
||||
Serial.println();
|
||||
#endif
|
||||
/// not here, done by sonde.setup(): rs41.setup();
|
||||
// == setup default channel list if qrg.txt read fails =========== //
|
||||
|
||||
sonde.setup();
|
||||
}
|
||||
|
||||
enum MainState { ST_DECODER, ST_SCANNER, ST_SPECTRUM, ST_WIFISCAN };
|
||||
|
||||
static MainState mainState = ST_DECODER;
|
||||
static MainState mainState = ST_WIFISCAN;
|
||||
|
||||
void enterMode(int mode) {
|
||||
mainState = (MainState)mode;
|
||||
|
|
@ -571,7 +629,7 @@ void loopDecoder() {
|
|||
Serial.println("Sending position via UDP");
|
||||
SondeInfo *s = sonde.si();
|
||||
char raw[201];
|
||||
const char *str = aprs_senddata(s->lat, s->lon, s->hei, s->hs, s->dir, s->vs, sondeTypeStr[s->type], s->id, "TE0ST",
|
||||
const char *str = aprs_senddata(s->lat, s->lon, s->alt, s->hs, s->dir, s->vs, sondeTypeStr[s->type], s->id, "TE0ST",
|
||||
sonde.config.udpfeed.symbol);
|
||||
int rawlen = aprsstr_mon2raw(str, raw, APRS_MAXLEN);
|
||||
Serial.print("Sending: "); Serial.println(raw);
|
||||
|
|
@ -601,7 +659,7 @@ void loopScanner() {
|
|||
}
|
||||
// receiveFrame returns 0 on success, 1 on timeout
|
||||
int res = sonde.receiveFrame(); // Maybe instead of receiveFrame, just detect if right type is present? TODO
|
||||
Serial.print("Scanner: receiveFrame returned");
|
||||
Serial.print("Scanner: receiveFrame returned: ");
|
||||
Serial.println(res);
|
||||
if(res==0) {
|
||||
enterMode(ST_DECODER);
|
||||
|
|
@ -666,44 +724,55 @@ void WiFiEvent(WiFiEvent_t event){
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
static char* _scan[2]={"/","\\"};
|
||||
void loopWifiScan() {
|
||||
u8x8->setFont(u8x8_font_chroma48medium8_r);
|
||||
u8x8->drawString(0,0,"WiFi Scan...");
|
||||
if (sonde.config.wifi != 0) {
|
||||
u8x8->drawString(0,0,"WiFi Scan...");
|
||||
}
|
||||
else if (sonde.config.wifiap != 0) {
|
||||
u8x8->drawString(0,0,"WiFi AP-Mode:");
|
||||
}
|
||||
|
||||
int line=0;
|
||||
int cnt=0;
|
||||
int marker=0;
|
||||
char buf[5];
|
||||
|
||||
WiFi.disconnect(true);
|
||||
WiFi.mode(WIFI_STA);
|
||||
const char *id, *pw;
|
||||
char idstr[64]="test";
|
||||
int n = WiFi.scanNetworks();
|
||||
for (int i = 0; i < n; i++) {
|
||||
Serial.print("Network name: ");
|
||||
Serial.println(WiFi.SSID(i));
|
||||
u8x8->drawString(0,1+line,WiFi.SSID(i).c_str());
|
||||
line = (line+1)%5;
|
||||
Serial.print("Signal strength: ");
|
||||
Serial.println(WiFi.RSSI(i));
|
||||
Serial.print("MAC address: ");
|
||||
Serial.println(WiFi.BSSIDstr(i));
|
||||
Serial.print("Encryption type: ");
|
||||
String encryptionTypeDescription = translateEncryptionType(WiFi.encryptionType(i));
|
||||
Serial.println(encryptionTypeDescription);
|
||||
Serial.println("-----------------------");
|
||||
id=WiFi.SSID(i).c_str();
|
||||
pw=fetchWifiPw(id);
|
||||
if(pw) { strncpy(idstr, id, 63); }
|
||||
}
|
||||
if(!pw) { pw="test"; }
|
||||
Serial.print("Connecting to: "); Serial.println(idstr);
|
||||
u8x8->drawString(0,6, "Conn:");
|
||||
u8x8->drawString(6,6, idstr);
|
||||
//register event handler
|
||||
WiFi.onEvent(WiFiEvent);
|
||||
|
||||
if (sonde.config.wifi != 0) {
|
||||
int n = WiFi.scanNetworks();
|
||||
for (int i = 0; i < n; i++) {
|
||||
Serial.print("Network name: ");
|
||||
Serial.println(WiFi.SSID(i));
|
||||
u8x8->drawString(0,1+line,WiFi.SSID(i).c_str());
|
||||
line = (line+1)%5;
|
||||
Serial.print("Signal strength: ");
|
||||
Serial.println(WiFi.RSSI(i));
|
||||
Serial.print("MAC address: ");
|
||||
Serial.println(WiFi.BSSIDstr(i));
|
||||
Serial.print("Encryption type: ");
|
||||
String encryptionTypeDescription = translateEncryptionType(WiFi.encryptionType(i));
|
||||
Serial.println(encryptionTypeDescription);
|
||||
Serial.println("-----------------------");
|
||||
id=WiFi.SSID(i).c_str();
|
||||
pw=fetchWifiPw(id);
|
||||
if(pw) { strncpy(idstr, id, 63); }
|
||||
}
|
||||
if(!pw) { pw="test"; }
|
||||
Serial.print("Connecting to: "); Serial.println(idstr);
|
||||
u8x8->drawString(0,6, "Conn:");
|
||||
u8x8->drawString(6,6, idstr);
|
||||
//register event handler
|
||||
WiFi.onEvent(WiFiEvent);
|
||||
|
||||
WiFi.begin(idstr, pw);
|
||||
}
|
||||
|
||||
WiFi.begin(idstr, pw);
|
||||
while(WiFi.status() != WL_CONNECTED) {
|
||||
delay(500);
|
||||
Serial.print(".");
|
||||
|
|
@ -718,20 +787,58 @@ void loopWifiScan() {
|
|||
#endif
|
||||
if(cnt==15) {
|
||||
WiFi.disconnect(true);
|
||||
delay(1000);
|
||||
WiFi.softAP(networks[0].id.c_str(),networks[0].pw.c_str());
|
||||
IPAddress myIP = WiFi.softAPIP();
|
||||
Serial.print("AP IP address: ");
|
||||
Serial.println(myIP);
|
||||
u8x8->drawString(0,6, "AP: ");
|
||||
u8x8->drawString(6,6, networks[0].id.c_str());
|
||||
sonde.setIP(myIP.toString().c_str(), true);
|
||||
sonde.updateDisplayIP();
|
||||
SetupAsyncServer();
|
||||
delay(3000);
|
||||
enterMode(ST_DECODER);
|
||||
|
||||
if (sonde.config.wifiap != 0) { // enable WiFi AP mode in config.txt: wifi=1
|
||||
delay(1000);
|
||||
WiFi.softAP(networks[0].id.c_str(),networks[0].pw.c_str());
|
||||
IPAddress myIP = WiFi.softAPIP();
|
||||
Serial.print("AP IP address: ");
|
||||
Serial.println(myIP);
|
||||
u8x8->drawString(0,6, "AP: ");
|
||||
u8x8->drawString(6,6, networks[0].id.c_str());
|
||||
sonde.setIP(myIP.toString().c_str(), true);
|
||||
sonde.updateDisplayIP();
|
||||
SetupAsyncServer();
|
||||
delay(3000);
|
||||
}
|
||||
|
||||
if (sonde.config.spectrum != 0) { // enable Spectrum in config.txt: spectrum=number_of_seconds
|
||||
sonde.clearDisplay();
|
||||
u8x8->setFont(u8x8_font_chroma48medium8_r);
|
||||
u8x8->drawString(0, 0, "Spectrum Scan...");
|
||||
delay(500);
|
||||
|
||||
enterMode(ST_SPECTRUM);
|
||||
|
||||
for (int i = 0; i < sonde.config.spectrum; i++) {
|
||||
scanner.scan();
|
||||
scanner.plotResult();
|
||||
|
||||
if (sonde.config.marker != 0) {
|
||||
itoa((sonde.config.startfreq), buf, 10);
|
||||
u8x8->drawString(0, 1, buf);
|
||||
u8x8->drawString(7, 1, "MHz");
|
||||
itoa((sonde.config.startfreq + 6), buf, 10);
|
||||
u8x8->drawString(13, 1, buf);
|
||||
}
|
||||
|
||||
if (sonde.config.timer != 0) {
|
||||
itoa((sonde.config.spectrum - i), buf, 10);
|
||||
if (sonde.config.marker != 0) {
|
||||
marker = 1;
|
||||
}
|
||||
u8x8->drawString(0, 1+marker, buf);
|
||||
u8x8->drawString(2, 1+marker, "Sec.");
|
||||
}
|
||||
}
|
||||
|
||||
delay(1000);
|
||||
}
|
||||
|
||||
enterMode(ST_SCANNER);
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Serial.println("");
|
||||
|
|
@ -742,9 +849,10 @@ void loopWifiScan() {
|
|||
sonde.updateDisplayIP();
|
||||
SetupAsyncServer();
|
||||
delay(2000);
|
||||
enterMode(ST_DECODER);
|
||||
}
|
||||
|
||||
// enterMode(ST_DECODER); ### 2019-04-20 - changed DL2MF
|
||||
enterMode(ST_SCANNER);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
Serial.println("Running main loop");
|
||||
|
|
|
|||
|
|
@ -1,14 +1,39 @@
|
|||
# Input button
|
||||
#-------------------------------#
|
||||
# Hardware depending settings
|
||||
#-------------------------------#
|
||||
button_pin=0
|
||||
# oled: SDA, SCL, RST (4,15,16 für TTGO v1)
|
||||
oled_sda=4
|
||||
oled_scl=15
|
||||
# LED port
|
||||
led_pin=25
|
||||
# OLED Setup is depending on hardware of LoRa board
|
||||
# TTGO v1: SDA=4 SCL=15, RST=16
|
||||
# TTGO v2: SDA=21 SCL=22, RST=16
|
||||
oled_sda=21
|
||||
oled_scl=22
|
||||
oled_rst=16
|
||||
|
||||
noisefloor=-130
|
||||
call=NOCALL
|
||||
#-------------------------------#
|
||||
# General config settings
|
||||
#-------------------------------#
|
||||
maxsonde=20
|
||||
debug=0
|
||||
wifi=0
|
||||
wifiap=1
|
||||
#-------------------------------#
|
||||
# Spectrum display settings
|
||||
#-------------------------------#
|
||||
startfreq=400
|
||||
channelbw=10
|
||||
spectrum=10
|
||||
timer=1
|
||||
noisefloor=-110
|
||||
marker=1
|
||||
#-------------------------------#
|
||||
# APRS settings
|
||||
#-------------------------------#
|
||||
call=N0CALL
|
||||
passcode=12345
|
||||
#-------------------------------#
|
||||
# axudp for sending to aprsmap
|
||||
#-------------------------------#
|
||||
# local use only, do not feed to public services
|
||||
# data not sanities / quality checked, outliers not filtered out
|
||||
axudp.active=1
|
||||
|
|
@ -17,7 +42,9 @@ axudp.port=9002
|
|||
axudp.symbol=/O
|
||||
axudp.highrate=1
|
||||
axudp.idformat=0
|
||||
#-------------------------------#
|
||||
# maybe some time in the future
|
||||
#-------------------------------#
|
||||
# currently simply not implemented, no need to put anything here anyway
|
||||
tcp.active=0
|
||||
tcp.host=radiosondy.info
|
||||
|
|
@ -25,4 +52,6 @@ tcp.port=14590
|
|||
tcp.symbol=/O
|
||||
tcp.highrate=20
|
||||
tcp.idformat=0
|
||||
|
||||
#-------------------------------#
|
||||
# EOF
|
||||
#-------------------------------#
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>ESP32 Web Server</title>
|
||||
<title>RDZSonde Server</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" href="data:,">
|
||||
<link rel="stylesheet" type="text/css" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>ESP32 Web Server</h1>
|
||||
<h2>RDZSonde Server</h2>
|
||||
<!--
|
||||
<p>GPIO state: <strong> %STATE%</strong></p>
|
||||
<p><a href="/on"><button class="button">ON</button></a></p>
|
||||
|
|
@ -16,19 +16,20 @@
|
|||
|
||||
<div class="tab">
|
||||
<button class="tablinks" onclick="selTab(event,'QRG')" id="defaultTab">QRG</button>
|
||||
<button class="tablinks" onclick="selTab(event,'WIFI')">WLAN</button>
|
||||
<button class="tablinks" onclick="selTab(event,'WLAN')">WLAN</button>
|
||||
<button class="tablinks" onclick="selTab(event,'Data')">Data</button>
|
||||
<button class="tablinks" onclick="selTab(event,'WebWX')">Webwx</button>
|
||||
<button class="tablinks" onclick="selTab(event,'Config')">Config</button>
|
||||
<button class="tablinks" onclick="selTab(event,'About')">About</button>
|
||||
</div>
|
||||
|
||||
<div id="QRG" class="tabcontent">
|
||||
<h3> QRG</h3>
|
||||
<h3> QRG - Setup</h3>
|
||||
<iframe src="qrg.html" style="border:none;" width="100%%" height="100%%"></iframe>
|
||||
</div>
|
||||
|
||||
<div id="WIFI" class="tabcontent">
|
||||
<h3> WIFI</h3>
|
||||
<div id="WLAN" class="tabcontent">
|
||||
<h3> WLAN - Settings</h3>
|
||||
<iframe src="wifi.html" style="border:none;" width="100%%" height="100%%"></iframe>
|
||||
</div>
|
||||
|
||||
|
|
@ -37,14 +38,24 @@
|
|||
<iframe src="status.html" style="border:none;" width="100%%" height="100%%"></iframe>
|
||||
</div>
|
||||
|
||||
<div id="Data" class="tabcontent">
|
||||
<h3>wetterson.de</h3>
|
||||
<iframe src="https://wetterson.de/karte/" style="border:none;" width="100%%" height="100%%"></iframe>
|
||||
</div>
|
||||
|
||||
<div id="Config" class="tabcontent">
|
||||
<h3>Config</h3>
|
||||
<h3>Configuration</h3>
|
||||
<iframe src="config.html" style="border:none;" width="100%%" height="100%%"></iframe>
|
||||
</div>
|
||||
|
||||
<div id="About" class="tabcontent">
|
||||
<h3>About</h3>
|
||||
RDZSonde
|
||||
RDZSonde - OpenSource<br>
|
||||
Copyright © 2019<br>
|
||||
by Hans P. Reiser, DL9RDZ<br>
|
||||
(devel-version 2019-04-23)<br>
|
||||
-------------------------------<br>
|
||||
with mods by Meinhard Guenther, DL2MF<br>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
RDZsonde
|
||||
RDZsonde
|
||||
DinoGast
|
||||
Schokolade
|
||||
AndroidDD
|
||||
dl9rdzhr
|
||||
AUTORX
|
||||
12345678
|
||||
SONDERX
|
||||
radiosonde
|
||||
|
|
@ -1,8 +1,23 @@
|
|||
# Frequency in Mhz (format nnn.nnn)
|
||||
# Type (4=RS41, 6=DFM normal, DFM-06, 9=DFM inverted, DFM-09)
|
||||
#
|
||||
402.700 4 +
|
||||
402.300 4 +
|
||||
403.450 9 +
|
||||
405.100 4 -
|
||||
402.300 4 - Greifswald
|
||||
402.500 4 - Schleswig
|
||||
402.700 4 + HH-Sasel
|
||||
403.000 4 - DeBilt
|
||||
404.100 4 + Norderney
|
||||
404.300 4 - Schleswig_2
|
||||
404.500 4 - Meppen
|
||||
404.700 4 - Greifswald_2
|
||||
405.100 4 - Lindenberg
|
||||
405.700 4 + Bergen
|
||||
405.900 4 + Bergen_2
|
||||
405.100 4 + Meppen_2
|
||||
405.300 4 - Essen
|
||||
403.330 9 - TrUebPl
|
||||
403.450 9 - TrUebPl
|
||||
403.470 9 - TrUebPl
|
||||
403.850 9 - TrUebPl
|
||||
403.870 9 - TrUebPl
|
||||
403.890 9 - TrUebPl
|
||||
# end
|
||||
|
|
|
|||
|
|
@ -228,12 +228,12 @@ int DFM::decodeDAT(uint8_t *dat)
|
|||
break;
|
||||
case 4:
|
||||
{
|
||||
float hei, vv;
|
||||
hei = ((uint32_t)dat[0]<<24) + ((uint32_t)dat[1]<<16) + ((uint32_t)dat[2]<<8) + dat[3];
|
||||
float lat, vv;
|
||||
lat = ((uint32_t)dat[0]<<24) + ((uint32_t)dat[1]<<16) + ((uint32_t)dat[2]<<8) + dat[3];
|
||||
vv = (int16_t)( (dat[4]<<8) | dat[5] );
|
||||
Serial.print("GPS-height: "); Serial.print(hei*0.01);
|
||||
Serial.print("GPS-height: "); Serial.print(lat*0.01);
|
||||
Serial.print(", vv: "); Serial.print(vv*0.01);
|
||||
sonde.si()->hei = hei*0.01;
|
||||
sonde.si()->lat = lat*0.01;
|
||||
sonde.si()->vs = vv*0.01;
|
||||
sonde.si()->validPos |= 0x0C;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -351,7 +351,7 @@ static void posrs41(const byte b[], uint32_t b_len, uint32_t p)
|
|||
Serial.print("m/s ");
|
||||
Serial.print(getcard16(b, b_len, p+18UL)&255UL);
|
||||
Serial.print("Sats");
|
||||
sonde.si()->hei = heig;
|
||||
sonde.si()->alt = heig;
|
||||
sonde.si()->validPos = true;
|
||||
} /* end posrs41() */
|
||||
|
||||
|
|
|
|||
|
|
@ -2,19 +2,26 @@
|
|||
#include <SX1278FSK.h>
|
||||
#include <U8x8lib.h>
|
||||
|
||||
#include "Sonde.h"
|
||||
|
||||
extern U8X8_SSD1306_128X64_NONAME_SW_I2C *u8x8;
|
||||
|
||||
#define CHANBW 10
|
||||
#define PIXSAMPL (50/CHANBW)
|
||||
#define SMOOTH 3
|
||||
#define STARTF 400000000
|
||||
//#define STARTF 401000000
|
||||
#define NCHAN ((int)(6000/CHANBW))
|
||||
|
||||
double STARTF = (sonde.config.startfreq * 1000000);
|
||||
//int CHANBW = (sonde.config.channelbw);
|
||||
//int NCHAN = ((int)(6000/CHANBW));
|
||||
//int PIXSAMPL = (50/CHANBW);
|
||||
|
||||
int scanresult[NCHAN];
|
||||
int scandisp[NCHAN/PIXSAMPL];
|
||||
|
||||
#define PLOT_N 120
|
||||
#define TICK1 (120/6)
|
||||
#define PLOT_N 128
|
||||
#define TICK1 (128/6)
|
||||
#define TICK2 (TICK1/4)
|
||||
#define PLOT_MIN -250
|
||||
#define PLOT_SCALE(x) (x<PLOT_MIN?0:(x-PLOT_MIN)/2)
|
||||
|
|
@ -30,7 +37,7 @@ void Scanner::fillTiles(uint8_t *row, int value) {
|
|||
}
|
||||
/*
|
||||
* There are 16*8 columns to plot, NPLOT must be lower than that
|
||||
* currently, we use 120 * 50kHz channels
|
||||
* currently, we use 128 * 50kHz channels
|
||||
* There are 8*8 values to plot; MIN is bottom end,
|
||||
*/
|
||||
uint8_t tiles[16] = { 0x0f,0x0f,0x0f,0x0f,0xf0,0xf0,0xf0,0xf0, 1, 3, 7, 15, 31, 63, 127, 255};
|
||||
|
|
|
|||
|
|
@ -52,10 +52,18 @@ Sonde::Sonde() {
|
|||
config.oled_sda = 4;
|
||||
config.oled_scl = 15;
|
||||
config.oled_rst = 16;
|
||||
|
||||
config.noisefloor = -130;
|
||||
strcpy(config.call,"NOCALL");
|
||||
strcpy(config.passcode, "---");
|
||||
config.maxsonde=15;
|
||||
config.debug=0;
|
||||
config.wifi=1;
|
||||
config.wifiap=1;
|
||||
config.startfreq=400;
|
||||
config.channelbw=10;
|
||||
config.spectrum=10;
|
||||
config.timer=0;
|
||||
config.marker=0;
|
||||
config.udpfeed.active = 1;
|
||||
config.udpfeed.type = 0;
|
||||
strcpy(config.udpfeed.host, "192.168.42.20");
|
||||
|
|
@ -80,7 +88,7 @@ void Sonde::setConfig(const char *cfg) {
|
|||
char *val = s+1;
|
||||
*s=0; s--;
|
||||
while(s>cfg && (*s==' '||*s=='\t')) { *s=0; s--; }
|
||||
Serial.printf("handling option '%s'='%s'\n", cfg, val);
|
||||
Serial.printf("configuration option '%s'=%s \n", cfg, val);
|
||||
if(strcmp(cfg,"noisefloor")==0) {
|
||||
config.noisefloor = atoi(val);
|
||||
if(config.noisefloor==0) config.noisefloor=-130;
|
||||
|
|
@ -90,12 +98,32 @@ void Sonde::setConfig(const char *cfg) {
|
|||
strncpy(config.passcode, val, 9);
|
||||
} else if(strcmp(cfg,"button_pin")==0) {
|
||||
config.button_pin = atoi(val);
|
||||
} else if(strcmp(cfg,"led_pin")==0) {
|
||||
config.led_pin = atoi(val);
|
||||
} else if(strcmp(cfg,"oled_sda")==0) {
|
||||
config.oled_sda = atoi(val);
|
||||
} else if(strcmp(cfg,"oled_scl")==0) {
|
||||
config.oled_scl = atoi(val);
|
||||
} else if(strcmp(cfg,"oled_rst")==0) {
|
||||
config.oled_rst = atoi(val);
|
||||
} else if(strcmp(cfg,"maxsonde")==0) {
|
||||
config.maxsonde = atoi(val);
|
||||
} else if(strcmp(cfg,"debug")==0) {
|
||||
config.debug = atoi(val);
|
||||
} else if(strcmp(cfg,"wifi")==0) {
|
||||
config.wifi = atoi(val);
|
||||
} else if(strcmp(cfg,"wifiap")==0) {
|
||||
config.wifiap = atoi(val);
|
||||
} else if(strcmp(cfg,"startfreq")==0) {
|
||||
config.startfreq = atoi(val);
|
||||
} else if(strcmp(cfg,"channelbw")==0) {
|
||||
config.channelbw = atoi(val);
|
||||
} else if(strcmp(cfg,"spectrum")==0) {
|
||||
config.spectrum = atoi(val);
|
||||
} else if(strcmp(cfg,"timer")==0) {
|
||||
config.timer = atoi(val);
|
||||
} else if(strcmp(cfg,"marker")==0) {
|
||||
config.marker = atoi(val);
|
||||
} else if(strcmp(cfg,"axudp.active")==0) {
|
||||
config.udpfeed.active = atoi(val)>0;
|
||||
} else if(strcmp(cfg,"axudp.host")==0) {
|
||||
|
|
@ -121,7 +149,7 @@ void Sonde::setConfig(const char *cfg) {
|
|||
} else if(strcmp(cfg,"tcp.idformat")==0) {
|
||||
config.tcpfeed.idformat = atoi(val);
|
||||
} else {
|
||||
Serial.printf("Invalid config option '%s'='%s'\n", cfg, val);
|
||||
Serial.printf("Invalid config option '%s'=%s \n", cfg, val);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -146,21 +174,22 @@ void Sonde::setIP(const char *ip, bool AP) {
|
|||
void Sonde::clearSonde() {
|
||||
nSonde = 0;
|
||||
}
|
||||
void Sonde::addSonde(float frequency, SondeType type, int active) {
|
||||
if(nSonde>=MAXSONDE) {
|
||||
void Sonde::addSonde(float frequency, SondeType type, int active, char *launchsite) {
|
||||
if(nSonde>=config.maxsonde) {
|
||||
Serial.println("Cannot add another sonde, MAXSONDE reached");
|
||||
return;
|
||||
}
|
||||
sondeList[nSonde].type = type;
|
||||
sondeList[nSonde].freq = frequency;
|
||||
sondeList[nSonde].active = active;
|
||||
sondeList[nSonde].launchsite = launchsite;
|
||||
memcpy(sondeList[nSonde].rxStat, "\x3\x3\x3\x3\x3\x3\x3\x3\x3\x3\x3\x3\x3\x3\x3\x3\x3\x3", 18); // unknown/undefined
|
||||
nSonde++;
|
||||
}
|
||||
void Sonde::nextConfig() {
|
||||
currentSonde++;
|
||||
// Skip non-active entries (but don't loop forever if there are no active ones
|
||||
for(int i=0; i<MAXSONDE; i++) {
|
||||
for(int i=0; i<config.maxsonde; i++) {
|
||||
if(!sondeList[currentSonde].active) {
|
||||
currentSonde++;
|
||||
if(currentSonde>=nSonde) currentSonde=0;
|
||||
|
|
@ -230,7 +259,7 @@ void Sonde::updateDisplayPos2() {
|
|||
u8x8->drawString(10,4," ");
|
||||
return;
|
||||
}
|
||||
snprintf(buf, 16, si()->hei>999?" %5.0fm":" %3.1fm", si()->hei);
|
||||
snprintf(buf, 16, si()->alt>999?" %5.0fm":" %3.1fm", si()->alt);
|
||||
u8x8->drawString((10+6-strlen(buf)),2,buf);
|
||||
snprintf(buf, 16, si()->hs>99?" %3.0f":" %2.1f", si()->hs);
|
||||
u8x8->drawString((10+4-strlen(buf)),3,buf);
|
||||
|
|
@ -276,7 +305,9 @@ void Sonde::updateDisplayRXConfig() {
|
|||
u8x8->drawString(0,0, sondeTypeStr[si()->type]);
|
||||
snprintf(buf, 16, "%3.3f MHz", si()->freq);
|
||||
u8x8->drawString(5,0, buf);
|
||||
|
||||
//snprintf(buf, 8, "%s", si()->launchsite);
|
||||
//u8x8->drawString(0,5, buf);
|
||||
u8x8->drawTile(14,3,2,kmh_tiles);
|
||||
}
|
||||
|
||||
void Sonde::updateDisplayIP() {
|
||||
|
|
@ -287,11 +318,13 @@ void Sonde::updateDisplayIP() {
|
|||
// 40x.xxx MHz
|
||||
void Sonde::updateDisplayScanner() {
|
||||
char buf[16];
|
||||
u8x8->setFont(u8x8_font_7x14_1x2_r);
|
||||
u8x8->drawString(0, 0, "Probing");
|
||||
u8x8->setFont(u8x8_font_7x14_1x2_r);
|
||||
u8x8->drawString(0, 0, "Scan:");
|
||||
u8x8->drawString(8, 0, sondeTypeStr[si()->type]);
|
||||
snprintf(buf, 16, "%3.3f MHz", si()->freq);
|
||||
u8x8->drawString(0,3, buf);
|
||||
snprintf(buf, 16, "%3.3f MHz", si()->freq);
|
||||
u8x8->drawString(0,3, buf);
|
||||
//snprintf(buf, 8, "%s", si()->launchsite);
|
||||
//u8x8->drawString(0,5, buf);
|
||||
updateDisplayIP();
|
||||
}
|
||||
|
||||
|
|
@ -304,7 +337,7 @@ void Sonde::updateDisplay()
|
|||
updateDisplayPos2();
|
||||
updateDisplayRSSI();
|
||||
updateStat();
|
||||
updateDisplayIP();
|
||||
updateDisplayIP();
|
||||
}
|
||||
|
||||
void Sonde::clearDisplay() {
|
||||
|
|
|
|||
|
|
@ -13,13 +13,23 @@ enum SondeType { STYPE_DFM06, STYPE_DFM09, STYPE_RS41 };
|
|||
extern const char *sondeTypeStr[5];
|
||||
|
||||
typedef struct st_rdzconfig {
|
||||
int button_pin;
|
||||
int oled_sda;
|
||||
int oled_scl;
|
||||
int oled_rst;
|
||||
int button_pin; // pin number of second button (for some boards)
|
||||
int led_pin; // pin number of LED
|
||||
int oled_sda; // OLED data pin
|
||||
int oled_scl; // OLED clock pin
|
||||
int oled_rst; // OLED reset pin
|
||||
int debug; // show port and config options after reboot
|
||||
int wifi; // connect to known WLAN 0=skip
|
||||
int wifiap; // enable/disable WiFi AccessPoint mode 0=disable
|
||||
int startfreq; // spectrum display start freq (400, 401, ...)
|
||||
int channelbw; // spectrum channel bandwidth (valid: 5, 10, 20, 25, 50, 100 kHz)
|
||||
int spectrum; // show freq spectrum for n seconds 0=disable
|
||||
int timer; // show remaining time in spectrum 0=disable
|
||||
int marker; // show freq marker in spectrum 0=disable
|
||||
int maxsonde; // number of max sonde in scan (range=1-99)
|
||||
int noisefloor; // for spectrum display
|
||||
char call[9];
|
||||
char passcode[9];
|
||||
char call[9]; // APRS callsign
|
||||
char passcode[9]; // APRS passcode
|
||||
// for now, one feed for each type is enough, but might get extended to more?
|
||||
struct st_feedinfo udpfeed; // target for AXUDP messages
|
||||
struct st_feedinfo tcpfeed; // target for APRS-IS TCP connections
|
||||
|
|
@ -27,28 +37,29 @@ typedef struct st_rdzconfig {
|
|||
|
||||
typedef struct st_sondeinfo {
|
||||
// receiver configuration
|
||||
bool active;
|
||||
bool active;
|
||||
SondeType type;
|
||||
float freq;
|
||||
// decoded ID
|
||||
char id[10];
|
||||
bool validID;
|
||||
char *launchsite;
|
||||
// decoded position
|
||||
float lat;
|
||||
float lon;
|
||||
float hei;
|
||||
float vs;
|
||||
float hs;
|
||||
float dir; // 0..360
|
||||
float lat; // latitude
|
||||
float lon; // longitude
|
||||
float alt; // altitude
|
||||
float vs; // vertical speed
|
||||
float hs; // horizontal speed
|
||||
float dir; // 0..360
|
||||
uint8_t validPos; // bit pattern for validity of above 6 fields
|
||||
// RSSI from receiver
|
||||
int rssi;
|
||||
int rssi; // signal strength
|
||||
uint8_t rxStat[20];
|
||||
} SondeInfo;
|
||||
// rxState: 0=undef[empty] 1=timeout[.] 2=errro[E] 3=ok[1]
|
||||
|
||||
|
||||
#define MAXSONDE 10
|
||||
#define MAXSONDE 99
|
||||
|
||||
class Sonde
|
||||
{
|
||||
|
|
@ -63,7 +74,7 @@ public:
|
|||
void setConfig(const char *str);
|
||||
|
||||
void clearSonde();
|
||||
void addSonde(float frequency, SondeType type, int active);
|
||||
void addSonde(float frequency, SondeType type, int active, char *launchsite);
|
||||
void nextConfig();
|
||||
void setup();
|
||||
|
||||
|
|
|
|||
|
|
@ -251,7 +251,7 @@ static uint32_t dao91(double x)
|
|||
char b[201];
|
||||
char raw[201];
|
||||
|
||||
char * aprs_senddata(float lat, float lon, float hei, float speed, float dir, float climb, const char *type, const char *objname, const char *usercall, const char *sym)
|
||||
char * aprs_senddata(float lat, float lon, float alt, float speed, float dir, float climb, const char *type, const char *objname, const char *usercall, const char *sym)
|
||||
{
|
||||
*b=0;
|
||||
aprsstr_append(b, usercall);
|
||||
|
|
@ -281,9 +281,9 @@ char * aprs_senddata(float lat, float lon, float hei, float speed, float dir, fl
|
|||
snprintf(b+i, APRS_MAXLEN-i, "%03d/%03d", realcard(dir+1.5), realcard(speed*1.0/KNOTS+0.5));
|
||||
}
|
||||
#endif
|
||||
if(hei>0.5) {
|
||||
if(alt>0.5) {
|
||||
i=strlen(b);
|
||||
snprintf(b+i, APRS_MAXLEN-i, "/A=%06d", realcard(hei*FEET+0.5));
|
||||
snprintf(b+i, APRS_MAXLEN-i, "/A=%06d", realcard(alt*FEET+0.5));
|
||||
}
|
||||
#if 1
|
||||
int dao=1;
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ typedef struct st_feedinfo {
|
|||
#define APRS_MAXLEN 201
|
||||
void aprs_gencrctab(void);
|
||||
int aprsstr_mon2raw(const char *mon, char raw[], int raw_len);
|
||||
char * aprs_senddata(float lat, float lon, float hei, float speed, float dir, float climb, const char *type, const char *objname, const char *usercall, const char *sym);
|
||||
char * aprs_senddata(float lat, float lon, float alt, float speed, float dir, float climb, const char *type, const char *objname, const char *usercall, const char *sym);
|
||||
|
||||
|
||||
#endif
|
||||
|
|
|
|||
Loading…
Reference in New Issue