diff --git a/.travis/push.sh b/.travis/push.sh index 9e1d5d0..ff2e613 100755 --- a/.travis/push.sh +++ b/.travis/push.sh @@ -61,6 +61,8 @@ commit_website_files() { git add ${BRANCH}/update.ino.bin echo "${TRAVIS_COMMIT_MESSAGE}" >> ${BRANCH}/${VERSION}-changelog.txt git add ${BRANCH}/${VERSION}-changelog.txt + echo "

${VERSION}

" > ${BRANCH}/update-info.html + git add ${BRANCH}/update-info.html git commit --message "Travis build: $TRAVIS_BUILD_NUMBER" } upload_files() { diff --git a/RX_FSK/RX_FSK.ino b/RX_FSK/RX_FSK.ino index d33f846..0df29b1 100644 --- a/RX_FSK/RX_FSK.ino +++ b/RX_FSK/RX_FSK.ino @@ -59,7 +59,19 @@ WiFiClient client; #define SONDEHUB_MOBILE_STATION_UPDATE_TIME (30*1000) // 30 sec WiFiClient shclient; // Sondehub v2 unsigned long time_last_update = 0; +/* SH_LOC_OFF: never send position information to SondeHub + SH_LOC_FIXED: send fixed position (if specified in config) or GPS position (if there is a GPS fix) as fixed station position (no chase mode) to sondehub + SH_LOC_CHASE: always activate chase mode and send GPS position (if available) + SH_LOC_AUTO: if there is no valid GPS position, or GPS position < MIN_LOC_AUTO_DIST away from known fixed position: use FIXED mode + otherwise, i.e. if there is a valid GPS position and (either no fixed position in config, or GPS position is far away from fixed position), use CHASE mode. +*/ +enum { SH_LOC_OFF, SH_LOC_FIXED, SH_LOC_CHASE, SH_LOC_AUTO }; +/* auto mode is chase if valid GPS position and (no fixed location entered OR valid GPS position and distance in lat/lon deg to fixed location > threshold) */ +#define MIN_LOC_AUTO_DIST 200 /* meter */ +#define SH_LOC_AUTO_IS_CHASE ( gpsPos.valid && ( (isnan(sonde.config.sondehub.lat) || isnan(sonde.config.sondehub.lon) ) || \ + calcLatLonDist( gpsPos.lat, gpsPos.lon, sonde.config.sondehub.lat, sonde.config.sondehub.lon ) > MIN_LOC_AUTO_DIST ) ) #endif +extern float calcLatLonDist(float lat1, float lon1, float lat2, float lon2); // KISS over TCP for communicating with APRSdroid WiFiServer tncserver(14580); @@ -99,6 +111,8 @@ static unsigned long specTimer; void enterMode(int mode); void WiFiEvent(WiFiEvent_t event); +char buffer[85]; +MicroNMEA nmea(buffer, sizeof(buffer)); // Read line from file, independent of line termination (LF or CR LF) String readLine(Stream &stream) { @@ -186,7 +200,6 @@ void setupChannelList() { while (file.available()) { String line = readLine(file); //file.readStringUntil('\n'); String sitename; - if (!file.available()) break; if (line[0] == '#') continue; char *space = strchr(line.c_str(), ' '); if (!space) continue; @@ -417,14 +430,14 @@ const char *createSondeHubMap() { HTMLBODY(ptr, "map.html"); if (!sonde.config.sondehub.active) { strcat(ptr, "
NOTE: SondeHub uploading is not enabled, detected sonde will not be visable on map
"); - if ((*s->ser == 0) && (sonde.config.sondehub.lat[0] != '\0')) { - sprintf(ptr + strlen(ptr), "", sonde.config.sondehub.lat, sonde.config.sondehub.lon); + if ((*s->ser == 0) && ( !isnan(sonde.config.sondehub.lat))) { + sprintf(ptr + strlen(ptr), "", sonde.config.sondehub.lat, sonde.config.sondehub.lon); } else { sprintf(ptr + strlen(ptr), "", s-> ser); } } else { - if ((*s->ser == 0) && (sonde.config.sondehub.lat[0] != '\0')) { - sprintf(ptr, "", sonde.config.sondehub.lat, sonde.config.sondehub.lon); + if ((*s->ser == 0) && (!isnan(sonde.config.sondehub.lat))) { + sprintf(ptr, "", sonde.config.sondehub.lat, sonde.config.sondehub.lon); } else { sprintf(ptr, "", s-> ser); } @@ -516,6 +529,39 @@ const char *createStatusForm() { return message; } +const char *createLiveJson() { + char *ptr = message; + strcpy(ptr, "{"); + + SondeInfo *s = &sonde.sondeList[sonde.currentSonde]; + if (s->validID) { + sprintf(ptr + strlen(ptr), "\"sonde\": {\"id\": \"%s\", \"freq\": %3.3f, \"type\": \"%s\", \"lat\": %.6f, \"lon\": %.6f, \"alt\": %.0f, \"speed\": %.1f, \"dir\": %.0f, \"climb\": %.1f }", s->id, s->freq, sondeTypeStr[s->type], s->lat, s->lon, s->alt, s->hs, s->dir, s->vs); + } else { + sprintf(ptr + strlen(ptr), "\"sonde\": {\"launchsite\": \"%s\",\"freq\": %3.3f, \"type\": \"%s\" }", s->launchsite, s->freq, sondeTypeStr[s->type]); + } + + if (sonde.config.gps_rxd < 0) { + // gps disabled + } else { + long sat = nmea.getNumSatellites(); + long speed = nmea.getSpeed(); + long dir = nmea.getCourse(); + long lat = nmea.getLatitude(); + long lon = nmea.getLongitude(); + long alt = -1; + /*bool b = */nmea.getAltitude(alt); + bool valid = nmea.isValid(); + uint8_t hdop = nmea.getHDOP(); + if (valid) { + strcat(ptr, ","); + sprintf(ptr + strlen(ptr), "\"gps\": {\"lat\": %ld, \"lon\": %ld, \"alt\": %ld, \"sat\": %ld, \"speed\": %ld, \"dir\": %ld, \"hdop\": %d }", lat, lon, alt, sat, speed, dir, hdop); + } + + } + + strcat(ptr, "}"); + return message; +} ///////////////////// Config form @@ -542,10 +588,10 @@ struct st_configitems { struct st_configitems config_list[] = { /* General config settings */ {"", "Software configuration", -5, NULL}, - {"wifi", "Wifi mode (0/1/2/3)", 0, &sonde.config.wifi}, + {"wifi", "Wifi mode (0-Off/1-Client/2-Access Point/3-Debug)", 0, &sonde.config.wifi}, {"debug", "Debug mode (0/1)", 0, &sonde.config.debug}, - {"maxsonde", "Maxsonde", 0, &sonde.config.maxsonde}, - {"screenfile", "Screen config (0=old, 1=OLED, 2=TFT, 3=TFT[port])", 0, &sonde.config.screenfile}, + {"maxsonde", "Maxsonde (max # QRG entries)", 0, &sonde.config.maxsonde}, + {"screenfile", "Screen config (0=automatic; 1-5=predefined; other=custom)", 0, &sonde.config.screenfile}, {"display", "Display screens (scan,default,...)", -6, sonde.config.display}, /* Spectrum display settings */ {"spectrum", "Show spectrum (-1=no, 0=forever, >0=seconds)", 0, &sonde.config.spectrum}, @@ -555,7 +601,6 @@ struct st_configitems config_list[] = { {"noisefloor", "Spectrum noisefloor", 0, &sonde.config.noisefloor}, /* decoder settings */ {"", "Receiver configuration", -5, NULL}, - {"showafc", "Show AFC value", 0, &sonde.config.showafc}, {"freqofs", "RX frequency offset (Hz)", 0, &sonde.config.freqofs}, {"rs41.agcbw", "RS41 AGC bandwidth", 0, &sonde.config.rs41.agcbw}, {"rs41.rxbw", "RS41 RX bandwidth", 0, &sonde.config.rs41.rxbw}, @@ -601,7 +646,7 @@ struct st_configitems config_list[] = { /* Hardware dependeing settings */ {"", "Hardware configuration (requires reboot)", -5, NULL}, - {"disptype", "Display type (0=OLED/SSD1306, 1=TFT/ILI9225, 2=OLED/SH1106)", 0, &sonde.config.disptype}, + {"disptype", "Display type (0=OLED/SSD1306, 1=ILI9225, 2=OLED/SH1106, 3=ILI9341, 4=ILI9342)", 0, &sonde.config.disptype}, {"norx_timeout", "No-RX-Timeout in seconds (-1=disabled)", 0, &sonde.config.norx_timeout}, {"oled_sda", "OLED SDA/TFT SDA", 0, &sonde.config.oled_sda}, {"oled_scl", "OLED SCL/TFT CLK", 0, &sonde.config.oled_scl}, @@ -609,7 +654,7 @@ struct st_configitems config_list[] = { {"tft_rs", "TFT RS", 0, &sonde.config.tft_rs}, {"tft_cs", "TFT CS", 0, &sonde.config.tft_cs}, {"tft_orient", "TFT orientation (0/1/2/3), OLED flip: 3", 0, &sonde.config.tft_orient}, - {"tft_modeflip", "TFT modeflip (usually 0)", 0, &sonde.config.tft_modeflip}, + {"tft_spifreq", "TFT SPI speed", 0, &sonde.config.tft_spifreq}, {"button_pin", "Button input port", -4, &sonde.config.button_pin}, {"button2_pin", "Button 2 input port", -4, &sonde.config.button2_pin}, {"button2_axp", "Use AXP192 PWR as Button 2", 0, &sonde.config.button2_axp}, @@ -618,20 +663,26 @@ struct st_configitems config_list[] = { {"led_pout", "LED output port", 0, &sonde.config.led_pout}, {"gps_rxd", "GPS RXD pin (-1 to disable)", 0, &sonde.config.gps_rxd}, {"gps_txd", "GPS TXD pin (not really needed)", 0, &sonde.config.gps_txd}, +#if 1 + {"sx1278_ss", "SX1278 SS", 0, &sonde.config.sx1278_ss}, + {"sx1278_miso", "SX1278 MISO", 0, &sonde.config.sx1278_miso}, + {"sx1278_mosi", "SX1278 MOSI", 0, &sonde.config.sx1278_mosi}, + {"sx1278_sck", "SX1278 SCK", 0, &sonde.config.sx1278_sck}, +#endif {"mdnsname", "mDNS name", 14, &sonde.config.mdnsname}, #if FEATURE_SONDEHUB - /* Sondehub v2 settings */ - {"", "Sondehub v2 settings", -5, NULL}, - {"sondehub.active", "Sondehub reporting active", 0, &sonde.config.sondehub.active}, - {"sondehub.chase", "Sondehub chase location active", 0, &sonde.config.sondehub.chase}, - {"sondehub.host", "Sondehub host", 63, &sonde.config.sondehub.host}, + /* SondeHub settings */ + {"", "SondeHub settings", -5, NULL}, + {"sondehub.active", "SondeHub reporting (0=disabled, 1=active)", 0, &sonde.config.sondehub.active}, + {"sondehub.chase", "SondeHub location reporting (0=off, 1=fixed, 2=chase/GPS, 3=auto)", 0, &sonde.config.sondehub.chase}, + {"sondehub.host", "SondeHub host (DO NOT CHANGE)", 63, &sonde.config.sondehub.host}, {"sondehub.callsign", "Callsign", 63, &sonde.config.sondehub.callsign}, - {"sondehub.lat", "Latitude", 19, &sonde.config.sondehub.lat}, - {"sondehub.lon", "Longitude", 19, &sonde.config.sondehub.lon}, - {"sondehub.alt", "Altitude", 19, &sonde.config.sondehub.alt}, - {"sondehub.antenna", "Antenna", 63, &sonde.config.sondehub.antenna}, - {"sondehub.email", "Sondehub email", 63, &sonde.config.sondehub.email}, + {"sondehub.lat", "Latitude (optional, required to show station on SondeHub Tracker)", -7, &sonde.config.sondehub.lat}, + {"sondehub.lon", "Longitude (optional, required to show station on SondeHub Tracker)", -7, &sonde.config.sondehub.lon}, + {"sondehub.alt", "Altitude (optional, visible on SondeHub tracker)", 19, &sonde.config.sondehub.alt}, + {"sondehub.antenna", "Antenna (optional, visisble on SondeHub tracker)", 63, &sonde.config.sondehub.antenna}, + {"sondehub.email", "SondeHub email (optional, only used to contact in case of upload errors)", 63, &sonde.config.sondehub.email}, #endif }; const static int N_CONFIG = (sizeof(config_list) / sizeof(struct st_configitems)); @@ -644,6 +695,10 @@ void addConfigNumEntry(char *ptr, int idx, const char *label, int *value) { sprintf(ptr + strlen(ptr), "%s\n", label, idx, *value); } +void addConfigDblEntry(char *ptr, int idx, const char *label, double *value) { + sprintf(ptr + strlen(ptr), "%s\n", + label, idx, *value); +} void addConfigButtonEntry(char *ptr, int idx, const char *label, int *value) { int v = *value, ck = 0; if (v == 255) v = -1; @@ -671,7 +726,7 @@ void addConfigHeading(char *ptr, const char *label) { strcat(ptr, "\n"); } void addConfigInt8List(char *ptr, int idx, const char *label, int8_t *list) { - sprintf(ptr + strlen(ptr), "%s", label); + sprintf(ptr + strlen(ptr), "%s using /screens%d.txt", label, Display::getScreenIndex(sonde.config.screenfile)); for (int i = 0; i < disp.nLayouts; i++) { sprintf(ptr + strlen(ptr), "
%d=%s", i, disp.layouts[i].label); } @@ -718,6 +773,9 @@ const char *createConfigForm() { case -4: addConfigButtonEntry(ptr, i, config_list[i].label, (int *)config_list[i].data); break; + case -7: /* double for lat/lon */ + addConfigDblEntry(ptr, i, config_list[i].label, (double *)config_list[i].data); + break; default: addConfigStringEntry(ptr, i, config_list[i].label, config_list[i].type, (char *)config_list[i].data); break; @@ -1010,7 +1068,11 @@ const char *createUpdateForm(boolean run) { if (run) { strcat(ptr, "

Doing update, wait until reboot

"); } else { + sprintf(ptr + strlen(ptr), "

Currently installed: %s-%c%d

\n", version_id, SPIFFS_MAJOR + 'A' - 1, SPIFFS_MINOR); + strcat(ptr, "

Available master::
" + "Available devel:

"); strcat(ptr, "
"); + strcat(ptr, "

Note: If suffix is the same, update should work fully. If the number is different, update contains changes in the file system. A full re-flash is required to get all new features, but the update should not break anything. If the letter is different, a full re-flash is mandatory, update will not work

"); } strcat(ptr, ""); Serial.printf("Update form: size=%d bytes\n", strlen(message)); @@ -1173,6 +1235,15 @@ void SetupAsyncServer() { server.on("/status.html", HTTP_GET, [](AsyncWebServerRequest * request) { request->send(200, "text/html", createStatusForm()); }); + server.on("/live.json", HTTP_GET, [](AsyncWebServerRequest * request) { + request->send(200, "text/json", createLiveJson()); + }); + server.on("/livemap.html", HTTP_GET, [](AsyncWebServerRequest * request) { + request->send(SPIFFS, "/livemap.html", String(), false, processor); + }); + server.on("/livemap.js", HTTP_GET, [](AsyncWebServerRequest * request) { + request->send(SPIFFS, "/livemap.js", String(), false, processor); + }); server.on("/update.html", HTTP_GET, [](AsyncWebServerRequest * request) { request->send(200, "text/html", createUpdateForm(0)); }); @@ -1218,7 +1289,7 @@ void SetupAsyncServer() { server.on("/edit.html", HTTP_POST, [](AsyncWebServerRequest * request) { const char *ret = handleEditPost(request); if (ret == NULL) - request->send(200, "text/html", "ERROR

Something went wrong. Uploaded file is empty.

"); + request->send(200, "text/html", "ERROR

Something went wrong (probably ESP32 out of memory). Uploaded file is empty.

"); else { String f = request->getParam(0)->value(); request->redirect("/edit.html?file=" + f); @@ -1368,8 +1439,6 @@ void initTouch() { } } -char buffer[85]; -MicroNMEA nmea(buffer, sizeof(buffer)); @@ -1781,6 +1850,7 @@ int scanI2Cdevice(void) extern int initlevels[40]; +extern xSemaphoreHandle globalLock; #ifdef ESP_MEM_DEBUG typedef void (*esp_alloc_failed_hook_t) (size_t size, uint32_t caps, const char * function_name); @@ -1865,7 +1935,14 @@ void setup() Serial.println("AXP192 Begin FAIL"); } axp.setPowerOutPut(AXP192_LDO2, AXP202_ON); - axp.setPowerOutPut(AXP192_LDO3, AXP202_ON); + if(sonde.config.type == TYPE_M5_CORE2) { + // Display backlight on M5 Core2 + axp.setPowerOutPut(AXP192_DCDC3, AXP202_ON); + axp.setDCDC3Voltage(3300); + } else { + // GPS on T-Beam, buzzer on M5 Core2 + axp.setPowerOutPut(AXP192_LDO3, AXP202_ON); + } axp.setPowerOutPut(AXP192_DCDC2, AXP202_ON); axp.setPowerOutPut(AXP192_EXTEN, AXP202_ON); axp.setPowerOutPut(AXP192_DCDC1, AXP202_ON); @@ -1980,9 +2057,30 @@ void setup() } // == show initial values from config.txt ========================= // -#if 0 +#if 1 + + if(sonde.config.type == TYPE_M5_CORE2) { + // Core2 uses Pin 38 for MISO + SPI.begin(18, 38, 23, -1); + } else { + SPI.begin(); + } + //Set most significant bit first + SPI.setBitOrder(MSBFIRST); + //Divide the clock frequency + SPI.setClockDivider(SPI_CLOCK_DIV2); + //Set data mode + SPI.setDataMode(SPI_MODE0); + +sx1278.setup(globalLock); + +uint8_t state = 2; +int i=0; +while(++i<3) { + delay(500); // == check the radio chip by setting default frequency =========== // - if (rs41.setFrequency(402700000) == 0) { + sx1278.ON(); + if (sx1278.setFrequency(402700000) == 0) { Serial.println(F("Setting freq: SUCCESS ")); } else { Serial.println(F("Setting freq: ERROR ")); @@ -1991,6 +2089,7 @@ void setup() Serial.print("Frequency set to "); Serial.println(f); // == check the radio chip by setting default frequency =========== // +} #endif //sx1278.setLNAGain(-48); @@ -2064,9 +2163,12 @@ void enterMode(int mode) { // trigger activation of background task // currentSonde should be set before enterMode() rxtask.activate = ACT_SONDE(sonde.currentSonde); + // + Serial.println("clearing and updating display"); sonde.clearDisplay(); sonde.updateDisplay(); } + printf("enterMode ok\n"); } static char text[40]; @@ -2233,7 +2335,9 @@ void loopDecoder() { } #endif } else { +#if FEATURE_SONDEHUB sondehub_finish_data(&shclient, s, &sonde.config.sondehub); +#endif } // always send data, even if not valid.... if (rdzclient.connected()) { @@ -2435,8 +2539,8 @@ void enableNetwork(bool enable) { #endif #if FEATURE_SONDEHUB if (sonde.config.sondehub.active && wifi_state != WIFI_APMODE) { + time_last_update = millis() + 1000; /* force sending update */ sondehub_station_update(&shclient, &sonde.config.sondehub); - time_last_update = millis(); } #endif configTime(gmtOffset_sec, daylightOffset_sec, ntpServer); @@ -2445,6 +2549,7 @@ void enableNetwork(bool enable) { MDNS.end(); connected = false; } + Serial.println("enableNetwork done"); } // Events used only for debug output right now @@ -2686,7 +2791,7 @@ void loopTouchCalib() { // 2: access point mode in background. directly start initial mode (spectrum or scanner) // 3: traditional sync. WifiScan. Tries to connect to a network, in case of failure activates AP. // Mode 3 shows more debug information on serial port and display. -#define MAXWIFIDELAY 20 +#define MAXWIFIDELAY 40 static const char* _scan[2] = {"/", "\\"}; void loopWifiScan() { if (sonde.config.wifi == 0) { // no Wifi @@ -2701,6 +2806,7 @@ void loopWifiScan() { } if (sonde.config.wifi == 2) { // AP mode, setup in background startAP(); + enableNetwork(true); initialMode(); return; } @@ -2741,17 +2847,6 @@ void loopWifiScan() { while (WiFi.status() != WL_CONNECTED && cnt < MAXWIFIDELAY) { delay(500); Serial.print("."); -#if 0 - if (cnt == 5) { - // my FritzBox needs this for reconnecting - WiFi.disconnect(true); - delay(500); - WiFi.begin(fetchWifiSSID(index), fetchWifiPw(index)); - Serial.print("Reconnecting to: "); Serial.print(fetchWifiSSID(index)); - Serial.print(" with password "); Serial.println(fetchWifiPw(index)); - delay(500); - } -#endif disp.rdis->drawString(15 * dispxs, lastl + dispys, _scan[cnt & 1]); cnt++; } @@ -3005,17 +3100,8 @@ void loop() { #if FEATURE_SONDEHUB if (sonde.config.sondehub.active) { - unsigned long time_now = millis(); - // time_delta will be correct, even if time_now overflows - unsigned long time_delta = time_now - time_last_update; - if ((sonde.config.sondehub.chase == 0) && (time_delta >= SONDEHUB_STATION_UPDATE_TIME) && (wifi_state != WIFI_APMODE)) { // 60 min - sondehub_station_update(&shclient, &sonde.config.sondehub); - time_last_update = time_now; - } - else if ((sonde.config.sondehub.chase == 1) && (time_delta >= SONDEHUB_MOBILE_STATION_UPDATE_TIME) && (wifi_state != WIFI_APMODE)) { // 30 sec - sondehub_station_update(&shclient, &sonde.config.sondehub); - time_last_update = time_now; - } + // interval check moved to sondehub_station_update to avoid having to calculate distance in auto mode twice + sondehub_station_update(&shclient, &sonde.config.sondehub); } #endif } @@ -3025,12 +3111,33 @@ void loop() { /* Update station data to the sondehub v2 DB */ +/* which_pos: 0=none, 1=fixed, 2=gps */ void sondehub_station_update(WiFiClient *client, struct st_sondehub *conf) { #define STATION_DATA_LEN 300 char data[STATION_DATA_LEN]; char *w; + // If there is no connection to some WiFi AP, we cannot upload any data at all.... + if ( wifi_state != WIFI_CONNECTED ) return; + + unsigned long time_now = millis(); + // time_delta will be correct, even if time_now overflows + unsigned long time_delta = time_now - time_last_update; + + int chase = conf->chase; + // automatically decided if CHASE or FIXED mode is used (for config AUTO) + if (chase == SH_LOC_AUTO) { + if (SH_LOC_AUTO_IS_CHASE) chase = SH_LOC_CHASE; else chase = SH_LOC_FIXED; + } + + // Use 30sec update time in chase mode, 60 min in station mode. + unsigned long update_time = (chase == SH_LOC_CHASE) ? SONDEHUB_MOBILE_STATION_UPDATE_TIME : SONDEHUB_STATION_UPDATE_TIME; + + // If it is not yet time to send another update. do nothing.... + if ( (time_delta <= update_time) ) return; + Serial.println("sondehub_station_update()"); + time_last_update = time_now; if (!client->connected()) { if (!client->connect(conf->host, 80)) { @@ -3040,7 +3147,7 @@ void sondehub_station_update(WiFiClient *client, struct st_sondehub *conf) { } w = data; - memset(w, 0, STATION_DATA_LEN); + // not necessary... memset(w, 0, STATION_DATA_LEN); sprintf(w, "{" @@ -3050,31 +3157,35 @@ void sondehub_station_update(WiFiClient *client, struct st_sondehub *conf) { "\"uploader_contact_email\": \"%s\",", version_name, version_id, conf->callsign, conf->email); w += strlen(w); - if ((conf->chase == 0) && (conf->lat[0] != '\0') && (conf->lon[0] != '\0')) { - if (conf->alt[0] != '\0') { + + // We send GPS position: (a) in CHASE mode, (b) in FIXED mode if no fixed location has been specified in config + if (chase == SH_LOC_CHASE || (chase == SH_LOC_FIXED && (isnan(conf->lat) || isnan(conf->lon)) ) ) { + if (gpsPos.valid && gpsPos.lat != 0 && gpsPos.lon != 0) { sprintf(w, - "\"uploader_position\": [%s,%s,%s]," - "\"uploader_antenna\": \"%s\"" + "\"uploader_position\": [%.6f,%.6f,%d]," + "\"uploader_antenna\": \"%s\"," + "\"mobile\": true" "}", - conf->lat, conf->lon, conf->alt, conf->antenna); - } else { - sprintf(w, - "\"uploader_position\": [%s,%s,null]," - "\"uploader_antenna\": \"%s\"" - "}", - conf->lat, conf->lon, conf->antenna); + gpsPos.lat, gpsPos.lon, gpsPos.alt, conf->antenna); } } - else if (gpsPos.valid && gpsPos.lat != 0 && gpsPos.lon != 0) { - sprintf(w, - "\"uploader_position\": [%.6f,%.6f,%d]," - "\"uploader_antenna\": \"%s\"," - "\"mobile\": true" - "}", - gpsPos.lat, gpsPos.lon, gpsPos.alt, conf->antenna); + // Otherweise, in FIXED mode we send the fixed position from config (if specified) + else if (chase == SH_LOC_FIXED) { + if ((!isnan(conf->lat)) && (!isnan(conf->lon))) { + sprintf(w, + "\"uploader_position\": [%.6f,%.6f,%s]," + "\"uploader_antenna\": \"%s\"" + "}", + conf->lat, conf->lon, conf->alt[0] ? conf->alt : "null", conf->antenna); + } } else { - return; + // otherwise (in SH_LOC_NONE mode) we dont include any position info + sprintf(w, + "\"uploader_position\": [null,null,null]," + "\"uploader_antenna\": \"%s\"" + "}", + conf->antenna); } client->println("PUT /listeners HTTP/1.1"); @@ -3092,7 +3203,7 @@ void sondehub_station_update(WiFiClient *client, struct st_sondehub *conf) { String response = client->readString(); Serial.println(response); Serial.println("Response done..."); - //client->stop();r + //client->stop(); } /* @@ -3118,7 +3229,7 @@ const char *dfmSubtypeStrSH[16] = { NULL, NULL, NULL, NULL, NULL, NULL, // in hours.... max allowed diff UTC <-> sonde time #define SONDEHUB_TIME_THRESHOLD (3) -void sondehub_send_data(WiFiClient *client, SondeInfo *s, struct st_sondehub *conf) { +void sondehub_send_data(WiFiClient * client, SondeInfo * s, struct st_sondehub * conf) { Serial.println("sondehub_send_data()"); Serial.printf("shState = %d\n", shState); @@ -3129,6 +3240,10 @@ void sondehub_send_data(WiFiClient *client, SondeInfo *s, struct st_sondehub *co char rs_msg[MSG_SIZE]; char *w; struct tm ts; + uint8_t realtype = s->type; + // config setting M10 and M20 will both decode both types, so use the real type that was decoded + if(TYPE_IS_METEO(realtype)) { realtype = s->subtype==1 ? STYPE_M10 : STYPE_M20; } + // For DFM, s->time is data from subframe DAT8 (gps date/hh/mm), and sec is from DAT1 (gps sec/usec) // For all others, sec should always be 0 and time the exact time in seconds time_t t = s->time + s->sec; @@ -3158,7 +3273,7 @@ void sondehub_send_data(WiFiClient *client, SondeInfo *s, struct st_sondehub *co if (((int)s->lat == 0) && ((int)s->lon == 0)) return; // Sometimes these values are zeroes. Don't send those to the sondehub if ((int)s->alt > 50000) return; // If alt is too high don't send to SondeHub // M20 data does not include #sat information - if ( s->type != STYPE_M20 && (int)s->sats < 4) return; // If not enough sats don't send to SondeHub + if ( realtype != STYPE_M20 && (int)s->sats < 4) return; // If not enough sats don't send to SondeHub // If not connected to sondehub, try reconnecting. // TODO: do this outside of main loop @@ -3185,7 +3300,7 @@ void sondehub_send_data(WiFiClient *client, SondeInfo *s, struct st_sondehub *co // DFM uses UTC. Most of the other radiosondes use GPS time // SondeHub expect datetime to be the same time sytem as the sonde transmits as time stamp - if ( s->type == STYPE_RS41 || s->type == STYPE_RS92 || s->type == STYPE_M20 ) { + if ( realtype == STYPE_RS41 || realtype == STYPE_RS92 || realtype == STYPE_M20 ) { t += 18; // convert back to GPS time from UTC time +18s } @@ -3213,25 +3328,25 @@ void sondehub_send_data(WiFiClient *client, SondeInfo *s, struct st_sondehub *co "\"rssi\": %.1f,", version_name, version_id, conf->callsign, timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday, timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec, - manufacturer_string[s->type], s->ser, + manufacturer_string[realtype], s->ser, ts.tm_year + 1900, ts.tm_mon + 1, ts.tm_mday, ts.tm_hour, ts.tm_min, ts.tm_sec + s->sec, (float)s->lat, (float)s->lon, (float)s->alt, (float)s->freq, (float)s->hs, (float)s->vs, (float)s->dir, -((float)s->rssi / 2) ); w += strlen(w); - - if (s->type != STYPE_M20) { + + if (realtype != STYPE_M20) { sprintf(w, "\"sats\": %d,", (int)s->sats); w += strlen(w); } - if ( TYPE_IS_DFM(s->type) || TYPE_IS_METEO(s->type) || s->type == STYPE_MP3H ) { + if ( TYPE_IS_DFM(realtype) || TYPE_IS_METEO(realtype) || realtype == STYPE_MP3H ) { // send frame as gps timestamp for these sonde, identical to autorx // For M10, this is real GPS time (seconds since Jqn 6 1980, without adjusting for leap seconds) // DFM and MP3H send real UTC (with leap seconds considered), so for them the frame number actually // is gps time plus number of leap seconds since the beginning of GPS time. int frame = (int)(t - 315964800); - if (s->type == STYPE_M10) { + if (realtype == STYPE_M10) { frame += 18; }; sprintf(w, "\"frame\": %d,", frame); @@ -3240,7 +3355,7 @@ void sondehub_send_data(WiFiClient *client, SondeInfo *s, struct st_sondehub *co } w += strlen(w); - sprintf(w, "\"type\": \"%s\",", sondeTypeStrSH[s->type]); + sprintf(w, "\"type\": \"%s\",", sondeTypeStrSH[realtype]); w += strlen(w); /* if there is a subtype (DFM only) */ @@ -3261,17 +3376,17 @@ void sondehub_send_data(WiFiClient *client, SondeInfo *s, struct st_sondehub *co w += strlen(w); } - if ((conf->chase == 0) && (conf->lat[0] != '\0') && (conf->lon[0] != '\0')) { + if ((conf->chase == 0) && (!isnan(conf->lat)) && (!isnan(conf->lon))) { if (conf->alt[0] != '\0') { sprintf(w, - "\"uploader_position\": [%s,%s,%s]," + "\"uploader_position\": [%.6f,%.6f,%s]," "\"uploader_antenna\": \"%s\"" "}", conf->lat, conf->lon, conf->alt, conf->antenna ); } else { sprintf(w, - "\"uploader_position\": [%s,%s,null]," + "\"uploader_position\": [%.6f,%.6f,null]," "\"uploader_antenna\": \"%s\"" "}", conf->lat, conf->lon, conf->antenna @@ -3312,7 +3427,7 @@ void sondehub_send_data(WiFiClient *client, SondeInfo *s, struct st_sondehub *co //Serial.println(response); } -void sondehub_finish_data(WiFiClient *client, SondeInfo *s, struct st_sondehub *conf) { +void sondehub_finish_data(WiFiClient * client, SondeInfo * s, struct st_sondehub * conf) { // If there is an "old" pending collection of JSON data sets, send it even if no now data is received if (shState == SH_CONN_APPENDING) { time_t now; @@ -3325,7 +3440,7 @@ void sondehub_finish_data(WiFiClient *client, SondeInfo *s, struct st_sondehub * } } -void sondehub_send_header(WiFiClient *client, SondeInfo *s, struct st_sondehub *conf) { +void sondehub_send_header(WiFiClient * client, SondeInfo * s, struct st_sondehub * conf) { Serial.print("PUT /sondes/telemetry HTTP/1.1\r\n" "Host: "); Serial.println(conf->host); @@ -3339,7 +3454,7 @@ void sondehub_send_header(WiFiClient *client, SondeInfo *s, struct st_sondehub * "Content-Type: application/json\r\n" "Transfer-Encoding: chunked\r\n"); } -void sondehub_send_next(WiFiClient *client, SondeInfo *s, struct st_sondehub *conf, char *chunk, int chunklen, int first) { +void sondehub_send_next(WiFiClient * client, SondeInfo * s, struct st_sondehub * conf, char *chunk, int chunklen, int first) { // send next chunk of JSON request client->printf("%x\r\n", chunklen + 1); client->write(first ? "[" : ",", 1); @@ -3347,11 +3462,11 @@ void sondehub_send_next(WiFiClient *client, SondeInfo *s, struct st_sondehub *co client->print("\r\n"); Serial.printf("%x\r\n", chunklen + 1); - Serial.write(first ? "[" : ",", 1); - Serial.write(chunk, chunklen); + Serial.write((const uint8_t *)(first ? "[" : ","), 1); + Serial.write((const uint8_t *)chunk, chunklen); Serial.print("\r\n"); } -void sondehub_send_last(WiFiClient *client, SondeInfo *s, struct st_sondehub *conf) { +void sondehub_send_last(WiFiClient * client, SondeInfo * s, struct st_sondehub * conf) { // last chunk. just the closing "]" of the json request client->printf("1\r\n]\r\n0\r\n\r\n"); Serial.printf("1\r\n]\r\n0\r\n\r\n"); diff --git a/RX_FSK/data/config.txt b/RX_FSK/data/config.txt index 4e6e86c..08ef101 100644 --- a/RX_FSK/data/config.txt +++ b/RX_FSK/data/config.txt @@ -25,11 +25,9 @@ #tft_rs=2 #tft_cs=0 tft_orient=1 -#tft_modeflip=0 +#tft_spifreq=40000000 #gps_rxd=-1 #gps_txd=-1 -# Show AFC value (for RS41 and M10/M20, maybe also DFM, but not useful for RS92) -showafc=1 # Frequency correction, in Hz # freqofs=0 #-------------------------------# @@ -42,8 +40,8 @@ wifi=3 # TCP/IP KISS TNC in port 14590 for APRSdroid (0=disabled, 1=enabled) kisstnc.active = 1 -# which screens file to use (0: screens.txt, i>0: screens${i}.txt -# 0: old version; 1: for OLED, 2: for TFT; 3: for TFT (portrait mode) +# which screens file to use (0: automated selection based on display type and orientation, i>0: screens${i}.txt +# predefined: 1: for OLED, 2: for ILI9225; 3: for ILI9225 (portrait mode); 4: for ILI9431; 5: for ILI9431 (portrait mode) # screenfile=2 # display configuration. List of "displays" # first entry: "Scanner" display diff --git a/RX_FSK/data/index.html b/RX_FSK/data/index.html old mode 100644 new mode 100755 index 81e842d..6b4b088 --- a/RX_FSK/data/index.html +++ b/RX_FSK/data/index.html @@ -15,6 +15,7 @@ + @@ -51,6 +52,9 @@ %VERSION_NAME%
Copyright © 2019-2021 by Hansi Reiser, DL9RDZ
(version %VERSION_ID%)

+ + Check for update (requires TTGO internet connection via WiFi)

+ with contributions by Vigor and Xavier (M20 support), Luke Prior and OH3BSG (SondeHub support), Meinhard Guenther, DL2MF, diff --git a/RX_FSK/data/livemap.html b/RX_FSK/data/livemap.html new file mode 100644 index 0000000..779e025 --- /dev/null +++ b/RX_FSK/data/livemap.html @@ -0,0 +1,17 @@ + + + + rdzTTGOSonde Server LiveMap + + + + + + + + + + +
+ + diff --git a/RX_FSK/data/livemap.js b/RX_FSK/data/livemap.js new file mode 100644 index 0000000..9d5c3d3 --- /dev/null +++ b/RX_FSK/data/livemap.js @@ -0,0 +1,478 @@ +$(document).ready(function(){ + + var map = L.map('map', { attributionControl: false, zoomControl: false }); + map.on('mousedown touchstart',function () { follow=false; }); + + L.control.scale().addTo(map); + L.control.attribution({prefix:false}).addTo(map); + + var osm = L.tileLayer('https://{s}.tile.openstreetmap.de/tiles/osmde/{z}/{x}/{y}.png', { + attribution: '
Leaflet · Map: OpenStreetMap
', + minZoom: 1, + maxZoom: 19 + }); + var esri = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { + attribution: '
Leaflet · Map: Esri · Earthstar Geographics
', + minZoom: 1, + maxZoom: 20 + }); + + var basemap = 'osm'; + osm.addTo(map); + + basemap_change = function () { + if (basemap == 'osm') { + map.removeLayer(osm); + map.addLayer(esri); + basemap = 'esri'; + } else { + map.removeLayer(esri); + map.addLayer(osm); + basemap = 'osm'; + } + }; + + map.setView([51.163361,10.447683], 5); // Mitte DE + +var reddot = ' '; +var yellowdot = ' '; +var greendot = ' '; + +$('#map .leaflet-control-container').append(L.DomUtil.create('div', 'leaflet-top leaflet-center leaflet-header')); +var header = ''; +header += '
rdzTTGOSonde LiveMap
🎈 - MHz -
'; +header += '
m | m/s | km/h
'; +header += '
'; +header += '

Prediction-Settings
'; + +header += ' m
'; +header += ' m/s
'; +header += ' m
'; +header += 'after the transmitted descend will be used'; +header += '
   
'; +header += '
'; +$('.leaflet-header').append(header); + + +$('#map .leaflet-control-container').append(L.DomUtil.create('div', 'leaflet-bottom leaflet-center leaflet-footer')); +var footer = ''; +footer += '
Direction: ...Β°
Distance: ...m
'; +$('.leaflet-footer').append(footer); + +var statbar = ''; +headtxt = function(data,stat) { + //var staticon = (stat == '1')?'🟒':'🟑'; + var staticon = (stat == '1')?greendot:yellowdot; + statbar = staticon + statbar; + if ((statbar.length) > 10*greendot.length) { statbar = statbar.substring(0,10*greendot.length); } + if (data.lat == '0.000000') { return false; } + if (data.id) { + $('#sonde_id').html(data.id); + $('#sonde_alt').html(data.alt); + $('#sonde_climb').html(data.climb); + $('#sonde_speed').html( mr(data.speed * 3.6 * 10) / 10 ); + $('#sonde_detail').show(); + } else { + $('#sonde_id').html(data.launchsite.trim()); + $('#sonde_detail').hide(); + } + $('#sonde_freq').html(data.freq); + $('#sonde_type').html(data.type); + $('#sonde_statbar').html(' '+statbar); +}; + + map.addControl(new L.Control.Button([ { position: 'topleft', text: 'πŸ”™', href: 'index.html' } ])); + + L.control.zoom({ position:'topleft' }).addTo(map); + + map.addControl(new L.Control.Button([ { position: 'topleft', text: 'πŸ—ΊοΈ', href: 'javascript:basemap_change();' } ])); + + map.addControl(new L.Control.Button([ { position: 'topright', id: "status", text: 'πŸ”΄', href: 'javascript:get_data();' } ])); + + map.addControl(new L.Control.Button([ + { position:'topright', text: '🎈', href: 'javascript:show(marker,\'marker\');' }, + { text: '〰️', href: 'javascript:show_line();' }, + { text: 'πŸ’₯', href: 'javascript:show(marker_burst,\'burst\');' }, + { text: '🎯', href: 'javascript:show(marker_landing,\'landing\');' } + ])); + + map.addControl(new L.Control.Button([ { position:'topright', text: 'βš™οΈ', href: 'javascript:show_settings();' } ])); + + + show = function(e,p) { + if (p == 'landing') { get_predict(last_data); } + if (e) { + map.closePopup(); + map.setView(map._layers[e._leaflet_id].getLatLng()); + map._layers[e._leaflet_id].openPopup(); + follow = p; + } + }; + + + getTwoBounds = function (a,b) { + var sW = new L.LatLng((a._southWest.lat > b._southWest.lat)?b._southWest.lat:a._southWest.lat, (a._southWest.lng > b._southWest.lng)?b._southWest.lng:a._southWest.lng); + var nE = new L.LatLng((a._northEast.lat < b._northEast.lat)?b._northEast.lat:a._northEast.lat, (a._northEast.lng < b._northEast.lng)?b._northEast.lng:a._northEast.lng); + + return new L.LatLngBounds(sW, nE); + }; + + show_line = function() { + $('.i_position, .i_landing').remove(); + map.closePopup(); + if (line._latlngs.length != 0 && line_predict._latlngs.length != 0) { + map.fitBounds(getTwoBounds(line.getBounds(),line_predict.getBounds())); + } else if (line._latlngs.length != 0) { + map.fitBounds(line.getBounds()); + } else if (line_predict._latlngs.length != 0) { + map.fitBounds(line_predict.getBounds()); + } + }; + + + + last_data = false; + follow = 'marker'; + + marker_landing = false; + icon_landing = L.divIcon({className: 'leaflet-landing'}); + dots_predict = []; + line_predict = L.polyline(dots_predict,{color: 'yellow'}).addTo(map); + marker_burst = false; + icon_burst = L.divIcon({className: 'leaflet-burst'}); + + marker = false; + dots = []; + line = L.polyline(dots).addTo(map); + + draw = function(data) { + var stat; + if (data.id) { + + if ((data.lat != '0.000000' && data.lon != '0.000000') && (JSON.stringify(data) != JSON.stringify(last_data)) ) { + var location = [data.lat,data.lon,data.alt]; + if (!marker) { + map.setView(location, 14); + marker = L.marker(location).addTo(map) + .bindPopup(poptxt('position',data),{closeOnClick:false, autoPan:false}).openPopup(); + get_predict(data); + } else { + marker.slideTo(location, { + duration: 500, + keepAtCenter: (follow=='marker')?true:false + }) + .setPopupContent(poptxt('position',data)); + if (last_data.id != data.id) { + storage_remove(); + dots = []; + get_predict(data); + } + } + dots.push(location); + line.setLatLngs(dots); + storage_write(data); + //$('#status').html('🟒'); + $('#status').html(greendot); + stat = 1; + } else { + //$('#status').html('🟑'); + $('#status').html(yellowdot); + stat = 0; + } + headtxt(data,stat); + last_data = data; + } else { + //$('#status').html('🟑'); + $('#status').html(yellowdot); + headtxt(data,0); + } + }; + + + marker_gps = false; + icon_gps = L.divIcon({className: 'leaflet-gps'}); + circ_gps = false; + + gps = function(e) { + gps_location = [e.lat/1000000,e.lon/1000000]; + gps_accuracy = e.hdop*2; + + if (last_data && last_data.lat != '0.000000') { + if ($('.leaflet-footer').css('display') == 'none') { $('.leaflet-footer').show(); } + + var distance = Math.round(map.distance(gps_location,[last_data.lat, last_data.lon])); + distance = (distance > 1000)?(distance / 1000) + 'k':distance; + $('.leaflet-footer .gps_dist').html(distance); + + $('.leaflet-footer .gps_dir').html( bearing(gps_location,[last_data.lat, last_data.lon]) ); + } + + if (!marker_gps) { + map.addControl(new L.Control.Button([{ position: 'topleft', text: 'πŸ›°οΈ', href: 'javascript:show(marker_gps,\'gps\');' }])); + + marker_gps = L.marker(gps_location,{icon:icon_gps}).addTo(map) + .bindPopup(poptxt('gps',e),{closeOnClick:false, autoPan:false}); + circ_gps = L.circle(gps_location, gps_accuracy).addTo(map); + } else { + marker_gps.slideTo(gps_location, { + duration: 500, + keepAtCenter: (follow=='gps')?true:false + }) + .setPopupContent(poptxt('gps',e)); + circ_gps.slideTo(gps_location, { duration: 500 }); + circ_gps.setRadius(gps_accuracy); + } + }; + + get_data = function() { + //$('#status').html('πŸ”΄'); + $('#status').html(reddot); + $.ajax({url: 'live.json', success: (function( data ) { + if (typeof data != "object") { data = $.parseJSON(data);Β } + if (data.sonde) { + draw(data.sonde); + } else { + //setTimeout(function() {$('#status').html('🟑');},100); + setTimeout(function() {$('#status').html(yellowdot);},100); + } + if (data.gps) { + gps(data.gps); + } + }), + timeout: 1000} + ); + }; + + storage = (typeof(Storage) !== "undefined")?true:false; + + settings_std = { + burst: 32500, + overwrite_descend: 6, + overwrite_descend_till: 12000 + }; + + settings_read = function() { + if (storage) { + if (sessionStorage.settings) { + return JSON.parse(sessionStorage.settings); + } else { + settings_write(settings_std); + return settings_std; + } + } else { + return settings_std; + } + return false; + }; + + settings_write = function (data) { + if (storage) { + sessionStorage.settings = JSON.stringify(data); + settings = data; + } + }; + + settings = settings_read(); + + settings_save = function() { + settings.burst = parseInt($('#settings #burst').val()); + settings.overwrite_descend = parseInt($('#settings #overwrite_descend').val()); + settings.overwrite_descend_till = parseInt($('#settings #overwrite_descend_till').val()); + if (Number.isInteger(settings.burst) && Number.isInteger(settings.overwrite_descend) && Number.isInteger(settings.overwrite_descend_till)) { + settings_write(settings); + $("#settings").slideUp(); + get_predict(last_data); + } else { + alert('Error: only numeric values allowed!'); + } + }; + + settings_reset = function() { + if (confirm('Reset to default?')) { + settings_write(settings_std); + show_settings(); + } + }; + + show_settings = function() { + $('#settings #burst').val(settings.burst); + $('#settings #overwrite_descend').val(settings.overwrite_descend); + $('#settings #overwrite_descend_till').val(settings.overwrite_descend_till); + $("#settings").slideToggle(); + }; + + predictor = false; + get_predict = function(data) { + if (!data) { return; } + var ascent = (data.climb > 0)? data.climb : 15; + var descent = (data.climb > 0)? settings.overwrite_descend : data.climb * -1; + + var burst; + if (data.climb > 0) { + burst = (data.alt > settings.burst )?data.alt + 100 : settings.burst; + } else { + burst = parseInt(data.alt) + 7; + if (data.alt > settings.overwrite_descend_till ) { descent = settings.overwrite_descend; } + } + + var m = new Date(); + var datetime = m.getUTCFullYear() + "-" + az(m.getUTCMonth()+1) + "-" + az(m.getUTCDate()) + "T" + + az(m.getUTCHours()) + ":" + az(m.getUTCMinutes()) + ":" + az(m.getUTCSeconds()) + "Z"; + var url = 'https://predict.cusf.co.uk/api/v1/'; + url += '?launch_latitude='+data.lat + '&launch_longitude='+data.lon; + url += '&launch_altitude='+data.alt + '&launch_datetime='+datetime; + url += '&ascent_rate='+ascent + '&burst_altitude=' + burst + '&descent_rate='+descent; + + $.getJSON(url, function( prediction ) { + draw_predict(prediction,data); + }); + }; + + draw_predict = function(prediction,data) { + var ascending = prediction.prediction[0].trajectory; + var highest = ascending[ascending.length-1]; + var highest_location = [highest.latitude,highest.longitude]; + + var descending = prediction.prediction[1].trajectory; + var landing = descending[descending.length-1]; + var landing_location = [landing.latitude,landing.longitude]; + + if (!marker_landing) { + marker_landing = L.marker(landing_location,{icon: icon_landing}).addTo(map) + .bindPopup(poptxt('landing',landing),{closeOnClick:false, autoPan:false}); + } else { + marker_landing.slideTo(landing_location, { + duration: 500, + keepAtCenter: (follow=='landing')?true:false + }) + .setPopupContent(poptxt('landing',landing)); + } + + dots_predict=[]; + + if (data.climb > 0) { + ascending.forEach(p => dots_predict.push([p.latitude,p.longitude])); + + if (!marker_burst) { + marker_burst = L.marker(highest_location,{icon:icon_burst}).addTo(map).bindPopup(poptxt('burst',highest),{closeOnClick:false, autoPan:false}); + } else { + marker_burst.slideTo(highest_location, { + duration: 500, + keepAtCenter: (follow=='burst')?true:false + }).setPopupContent(poptxt('burst',highest)); + } + } + + descending.forEach(p => dots_predict.push([p.latitude,p.longitude])); + line_predict.setLatLngs(dots_predict); + + if (data.climb > 0) { + predictor_time = 5 * 60; // ascending, every 5 min + } else if (data.climb < 0 && data.alt > 5000) { + predictor_time = 2 * 60; // descending, above 5km, every 2 min + } else { + predictor_time = 30; // descending, below 5km, every 30 sec + } + clearTimeout(predictor); + predictor = setTimeout(function() {get_predict(last_data);}, predictor_time*1000); + }; + + poptxt = function(t,i) { + var lat_input = (i.id)?i.lat:i.latitude; + var lon_input = (i.id)?i.lon:i.longitude; + + var lat = Math.round(lat_input * 1000000) / 1000000; + var lon = Math.round(lon_input * 1000000) / 1000000; + + var add = + '
Position: '+lat+', '+lon+'
'+ + 'Open: GMaps |Β OSM |Β Maps.me'; + + if (t == 'position') { return '
🎈 '+i.id+''+add+'
'; } + if (t == 'burst') { return '
πŸ’₯ Predicted Burst:
'+fd(i.datetime)+' in '+mr(i.altitude)+'m'+add+'
'; } + if (t == 'highest') { return '
πŸ’₯ Burst: '+mr(i.altitude)+'m'+add+'
';} + if (t == 'landing') { return '
🎯 Predicted Landing:
'+fd(i.datetime)+' at '+mr(i.altitude)+'m'+add+'
'; } + if (t == 'gps') { return '
Position: '+(i.lat/1000000)+','+(i.lon/1000000)+'
Altitude: '+mr(i.alt/1000)+'m
Speed: '+mr(i.speed/1000 * 1.852 * 10)/10+'km/h '+mr(i.dir/1000)+'Β°
Sat: '+i.sat+' Hdop:'+(i.hdop/10)+'
'; } + }; + + fd = function(date) { + var d = new Date(Date.parse(date)); + return az(d.getUTCHours()) +':'+ az(d.getUTCMinutes())+' UTC'; + }; + az = function(n) { return (n<10)?'0'+n:n; }; + mr = function(n) { return Math.round(n); }; + + storage = (typeof(Storage) !== "undefined")?true:false; + storage_write = function (data) { + if (storage) { + if (sessionStorage.sonde) { + storage_data = JSON.parse(sessionStorage.sonde); + } else { + storage_data = []; + } + if (JSON.stringify(data) != JSON.stringify(storage_data[storage_data.length - 1])) { + storage_data.push(data); + sessionStorage.sonde = JSON.stringify(storage_data); + } + } + }; + + storage_read = function() { + if (storage) { + if (sessionStorage.sonde) { + storage_data = JSON.parse(sessionStorage.sonde); + return storage_data; + } + } + return false; + }; + + storage_remove = function() { + sessionStorage.removeItem('sonde'); + }; + + session_storage = storage_read(); + if (session_storage) { + session_storage.forEach(function(d) { + dots.push([d.lat,d.lon,d.alt]); + session_storage_last = d; + }); + draw(session_storage_last); + } + + setInterval(get_data,1000); + +}); + +L.Control.Button = L.Control.extend({ + onAdd: function (map) { + var container = L.DomUtil.create('div', 'leaflet-bar leaflet-control'); + options = this.options; + Object.keys(options).forEach(function(key) { + this.link = L.DomUtil.create('a', '', container); + this.link.text = options[key].text; + this.link.href = options[key].href; + this.link.id = options[key].id; + }); + + this.options.position = this.options[0].position; + return container; + } +}); + + +// https://github.com/makinacorpus/Leaflet.GeometryUtil/blob/master/src/leaflet.geometryutil.js#L682 +// modified to fit +function bearing(latlng1, latlng2) { + var rad = Math.PI / 180, + lat1 = latlng1[0] * rad, + lat2 = latlng2[0] * rad, + lon1 = latlng1[1] * rad, + lon2 = latlng2[1] * rad, + y = Math.sin(lon2 - lon1) * Math.cos(lat2), + x = Math.cos(lat1) * Math.sin(lat2) - + Math.sin(lat1) * Math.cos(lat2) * Math.cos(lon2 - lon1); + var bearing = ((Math.atan2(y, x) * 180 / Math.PI) + 360) % 360; + bearing = bearing < 0 ? bearing-360 : bearing; + return Math.round(bearing); +} diff --git a/RX_FSK/data/screens4.txt b/RX_FSK/data/screens4.txt new file mode 100644 index 0000000..8730685 --- /dev/null +++ b/RX_FSK/data/screens4.txt @@ -0,0 +1,574 @@ +## screens2.txt: TFT display (landscape) +# Definition of display content and action behaviour +# +# Timer: (view timer, rx timer, norx timer) +# - value -1: timer is disabled; value>=0: timer fires after (value) seconds +# - view timer: time since current view (display mode and sonde) was started +# - rx timer: time since when sonde data has been received continuously (trigger immediatly after RX) +# - norx timer: time since when no sonde data has been received continuously +# (rx and norx timer is started after tuning a new frequency and receiving a signal or not receiving +# anything for a 1s period) +# +# Actions: +# - W: activate WiFi scan +# - F: activate frequency spectrum display +# - 0: activate "Scan:" display (this is basically just display mode 0) +# - x: (1..N): activate display mode x [deprecated] +# - >: activate next display mode +# - D: activate default receiver display (display mode specified in config) +# - +: advance to next active sonde from QRG config +# - #: no action +# +# Display content (lower/upper case: small/large font) +# line,column=content +# for ILI9225 its also possible to indicate +# line,column,width=content for text within a box of width 'width' +# line,column,-width=content for right-justified text +# +# XText : Text +# F(suffix): frequency (with suffix, e.g., " MHz") +# L latitade +# O lOngitute +# A altitude +# Hm(suffix) hor. speed m/s (suffix: e.g. "m/s"; no suffix=>m/s as 16x8 bitmap for SSD1306 display only) +# Hk(suffix) hor. speed km/h (suffix: e.g. "km/h"; no suffix=>km/h as 16x8 bitmap for SSD1306 display only) +# V(suffix) vert. speef (suffix: e.g. "m/s"; no suffix=>m/s as 16x8 bitmap for SSD1306 display only) +# Ix sonde ID (default/d: dxlaprs; s: short id, n: real serial number) +# RS41,RS92: all identical R1234567 +# DFMx: ID M12345678; short ID and serial 12345678 +# M10: ID ME95231F0; short ID: M95231F0; serial 9062104592 +# Q signal quality statistics bar +# T type string (RS41/DFM9/DFM6/RS92) +# C afC value +# N ip address (only tiny font) +# S scan list entry info: l/empty: launch site name, #=entry nr, t=total entries, a=active entries, /: #/t +# K RS41 kill timer values: Kl launch timer, Kb burst timer, Kc kill countdown +# format: K_4: h:mm k_6: h:mm:ss k_s: sssss, nothing shown for other sonde +# Mx telemetry value x (t temp p preassure h hyg) [not yet implemented, maybe some day in future] +# Gx GPS-related data +# raw data from GPS: GA, GO, GH, GC: LAtitude, lOngitude, Altutide(Height), Course over ground +# relative to sonde: GD, GI, GB: Distance, dIrection (absolute), relative Bearing +# G0 GPS circle diagram e.g. 3,5=g0NCS,50,ff0000,000033,5,ffff00,4,ffffff +# "N" (what is on top: N=north C=course) +# "C" (where does the arrow point to: C=course, S=sonde) +# "S" (what is shown by the bullet: C=course, S=sonde) +# 50: circle radius, followed by fg and bg color +# 5: bullet radius, followed by fg color +# 4: arrow width, followed by fg color +# R RSSI +# B battery(T-Beam 1.0) S=status V=Batt.Volt C=charge current D=discharge current +# U=USB volt I=USB current T=IC temp +# +# fonts=x,y can be used to select font (x=small, y=large) for all items below +# for SSD1306, x and y can be used to select one of those fonts: +# (y should be a 1x2 font (1,5,6,7), x a small font) +# u8x8_font_chroma48medium8_r, // 0 ** default small +# u8x8_font_7x14_1x2_f, // 1 ** default large +# u8x8_font_amstrad_cpc_extended_f, // 2 +# u8x8_font_5x7_f, // 3 +# u8x8_font_5x8_f, // 4 +# u8x8_font_8x13_1x2_f, // 5 +# u8x8_font_8x13B_1x2_f, // 6 +# u8x8_font_7x14B_1x2_f, // 7 +# u8x8_font_artossans8_r, // 8 +# u8x8_font_artosserif8_r, // 9 +# u8x8_font_torussansbold8_r, // 10 +# u8x8_font_victoriabold8_r, // 11 +# u8x8_font_victoriamedium8_r, // 12 +# u8x8_font_pressstart2p_f, // 13 +# u8x8_font_pcsenior_f, // 14 +# u8x8_font_pxplusibmcgathin_f, // 15 +# u8x8_font_pxplusibmcga_f, // 16 +# u8x8_font_pxplustandynewtv_f, // 17 +# +# for ILI9225, these fonts are available: +# Terminal6x8 // 0 +# Terminal11x16 // 1 +# Terminal12x16 // 2 +# FreeMono9pt7b, // 3 +# FreeMono12pt7b, // 4 +# FreeSans9pt7b, // 5 +# FreeSans12pt7b, // 6 +# Picopixel, // 7 +# +# color=rrggbb,rrggbb can be used to select color (foreground, background) +# see https://github.com/Nkawu/TFT_22_ILI9225/wiki#color-reference for example (use without "#"-sign) +# +# for TFT display, coordinates and width are multiplied by xscale,yscale and later used in pixels +# with scale=1,1 you can directly use pixel coordinates. (default: xscale=13,yscale=22 => 8 lines, 16 columns) +########### + +############ +# Scan display for large 2" TFT dispaly +@ScannerTFT +scale=30,18 +timer=-1,0,0 +key1action=D,#,F,W +key2action=D,#,#,# +timeaction=#,D,+ +fonts=5,6 +0,0=XScan +0,5,-3=S#: +0,9,5.5=T +3,0=F MHz +5,0,16=S +7,5=n + +############ +@MainTFT +timer=-1,-1,N +key1action=+,0,F,W +key2action=>,#,#,# +timeaction=#,#,0 +color=FFD700 +0,0,10.5=Is +color=0000FF +0,11,-5.5=f +1,0,4=t +1,10.5,-6=c +color=00ff00 +2,0,7=L +4,0,7=O +color=FFA500 +2,9.5,-7=A +3,9.5,-7=vm/s +color=AA5522 +4,9.5,-7=hkkm/h +color=FFFFFF +6,2=r +6.3,10=Q4 +7,0=xd= +7,2,6=gD +7,12=gI + +############ +@PeilungTFT +timer=-1,-1,N +key1action=+,0,F,W +key2action=>,#,#,# +timeaction=#,#,# +color=ffff00,000033 +color=bbbbbb,000000 +0,2=xN Top: +0,8=xCourse Top: +color=ffff00,000033 +1,0=g0NCS,48,ffff00,000044,6,33ff33,5,eeaa00 +1,8=g0CCS,48,ffff00,000044,6,55ff55,5,eeaa00 +color=ffffff,000000 +6,0=xDirection: +6,8,4=gI +7,0=xCOG: +7,4,4=gC +7,8=xturn: +7,12,4=gB + +############ +@GPSdataTFT +timer=-1,-1,N +key1action=+,0,F,W +key2action=>,#,#,# +timeaction=#,#,# +0,0=xOn-board GPS: +1,0,8=gA +2,0,8=gO +3,0,8=gH +4,0,8=gC +5,0=xGPS vs Sonde: +6,0,8=gD +7,0,8=gI +7,8,8=gB + +############ +@BatteryTFT +timer=-1,-1,-1 +key1action=+,0,F,W +key2action=>,#,#,# +timeaction=#,#,# +0,0=xBattery status: +0,14=bS +1,0=xBatt: +1,5,5=bVV +2,0,16=bCmA(charging) +3,0,16=bDmA(discharging) +4.4,0=xUSB: +4.4,5,5=bUV +5.4,0,10=bImA +6.4,0=xTemp: +6.4,5,5=bT C + +### Alternative display layouts based on https://gist.github.com/bazjo +# Scan display for large 2" TFT dispaly +@Scan.TFT.Bazjo +timer=-1,0,0 +key1action=D,#,F,W +key2action=D,#,#,# +timeaction=#,D,+ +scale=11,10 +fonts=0,2 +color=e0e0e0 +#Row 1 +0.5,0=XScanning... +#Row 2 +3,0=xIndex +4,0,8=S/ +3,9=xSite +4,9=S +#Row 3 +6,0=xType +7,0,6=T +6,9=xFrequency +7,9=F +#Row 4 +9,0=xWeb UI IP +10,0=N +#Row 5 +#Footer +color=6C757D +15,0=xScan Mode +15,18=bVV + +############ +@Decode/General.TFT.Bazjo +timer=-1,-1,N +key1action=+,0,F,W +key2action=>,#,#,# +timeaction=#,#,0 +scale=11,10 +fonts=0,2 +#Row 1 +color=996A06 +0,0=xSerial +0,5=t +color=FFB10B +1,0=Is +color=996A06 +0,11=xFreq. +0,16=c +color=FFB10B +1,11=F +#Row 2 +color=3C5C99 +3,0=xLatitude +color=639AFF +4,0=L +color=3C5C99 +3,11=xLongitude +color=639AFF +4,11=O +#Row 3 +color=3C5C99 +6,0=xHoriz. Speed +color=639AFF +7,0=Hkkm/h +color=3C5C99 +6,11=xVert. Speed +color=639AFF +7,11=Vm/s +#Row 4 +color=99004A +9,0=xAltitude +color=FF007B +10,0=A +color=99004A +9,11=xBearing +color=FF007B +10,11=GB +#Row 5 +color=06998E +12,0=xRSSI +color=0AFFEF +13,0=R +color=06998E +12,11=xHistory +color=0AFFEF +13.5,11=Q4 +#Footer +color=6C757D +15,0=xDecode Mode / General View +15,18=bVV + +############ +@Decode/Battery.TFT.Bazjo +timer=-1,-1,N +key1action=+,0,F,W +key2action=>,#,#,# +timeaction=#,#,# +scale=11,10 +fonts=0,2 +#Row 1 +color=99001F +0,0=xBattery Status +0,11=xBattery Voltage +color=FF0035 +1,0=BS +1,11=BVV +#Row 2 +color=99001F +3,0=xCharge Current +3,11=xDischarge Current +color=FF0035 +4,0=BCmA +4,11=BDmA +#Row 3 +color=99001F +6,0=xUSB Voltage +6,11=xUSB Current +color=FF0035 +7,0=BUV +7,11=BImA +#Row 4 +color=99001F +9,0=xIC Temperature +#9,11=xKey +color=FF0035 +10,0=BTC +#10,11=XValue +#Row 5 +#12,0=xKey +#12,11=xKey +#13,0=XValue +#13,11=XValue +#Footer +color=99001F +15,0=xDecode Mode/Battery View +15,18=bVV + +# based on https://github.com/puspis/rdz_ttgo_sonde +########## +@Scanner.Puspis +timer=-1,0,4 +key1action=D,#,F,W +key2action=D,#,#,# +timeaction=#,D,+ +scale=13,10 +fonts=0,1 +#Row 1 +color=90EE90 +0.5,3=XFREQUENCY SCAN +#Row 2 +color=00FF00 +3,0=xMEMORY +3,9=xLAUNCH SITE +color=639AFF +4,0,9=S/ +4,9=S +#Row 3 +color=00FF00 +6,0=xTYPE +6,9=xFREQUENCY +color=639AFF +7,0,9=T +7,9=F MHz +#Row 4 +fonts=0,5 +color=285454 +11.5,0=xIP ADDRESS: +10.9,7,-15=N +#Footer +color=FF0000 +12.7,18=bVV + +############ +@Main.Puspis +timer=-1,-1,N +key1action=+,0,F,W +key2action=>,#,#,# +timeaction=#,#,0 +scale=11,10 +fonts=0,2 +#Row 1 +color=00FF00 +0,0=xSONDE ID +0,11=xFREQUENCY +color=90EE90 +0,7.4=t +1,0=Is +1,11=F +#Row 2 +fonts=0,1 +color=00FF00 +3,0=xLATITUDE +3,11=xLONGITUDE +color=FF007B +4,0=L +4,11=O +#Row 3 +color=00FF00 +6,0=xWIND SPEED +6,11=xCLIMB RATE +color=639AFF +7,0=Hkkm/h +7,11=Vm/s +#Row 4 +color=00FF00 +9,0=xRX ALTITUDE +9,11=xSONDE ALTITUDE +color=639AFF +10,0=GH +10,11=A +#Row 5 +color=00FF00 +12,0=xDISTANCE +12,11=xFRAMES +color=FFFFFF +13,0=GD +13.5,11=Q4 +#Footer +color=FF0000 +15,0.2=xIP: +15,2.5=n +15,18=bVV + +############ +@JotaEme.Puspis +timer=-1,-1,N +key1action=+,0,F,W +key2action=>,#,#,# +timeaction=#,#,0 +scale=11,10 +fonts=0,1 +#Row 1 +color=90EE90 +0,0=Is +0.7,10.5=t +0,14=F +#Row 2 +color=00FF00 +2,1.2=xWIND SPEED +2,14=xCLIMB RATE +color=639AFF +3,1=Hkkm/h +3,13=Vm/s +#Row 3 +color=00FF00 +5,1=xRX ALTITUDE +5,12.5=xSONDE ALTITUDE +color=639AFF +6,2=GH +6,14=A +#Row 4 +color=00FF00 +8,0=xSONDE POSITION +color=FF007B +9,2=l +10,2=o +#Row 5 +color=00FF00 +11.4,2=xDISTANCE +color=FFFFFF +12.4,1.5=GD +#Circle +color=EEAA00,000033 +8,13.8=g0CCS,28,FFFF00,000033,5,9ACD32,5,EEAA00 +#Footer +color=FF0000 +15,0=n +color=FFFFFF,000000 +15,8.7=Q4 +color=FF0000 +15,18.4=bVV + +############ +@CompassTFT.Puspis +timer=-1,-1,N +key1action=+,0,F,W +key2action=>,#,#,# +timeaction=#,#,# +scale=13,10 +fonts=0,1 +#Row 1 +color=90EE90 +0.5,1.5=XCOMPASS +#Row 2 +color=00FF00 +4,2=xRX HEADING +color=639AFF +5,3.8=GC +#Row 3 +color=00FF00 +9.5,3=xDISTANCE +9.5,13.5=xBEARING +color=639AFF +10.5,3.4=GD +10.5,14.3=GI +#Circle +color=EEAA00,000033 +0.2,10=g0CCS,52,FFFF00,000033,10,9ACD32,6,EEAA00 +#Footer +color=FF0000,000000 +12.7,1=Q4 +12.7,18=bVV + +############ +@GPSdataTFT.Puspis +timer=-1,-1,N +key1action=+,0,F,W +key2action=>,#,#,# +timeaction=#,#,# +scale=13,10 +fonts=0,1 +#Row 1 +color=90EE90 +0.5,0.5=XGPS RECEIVER STATION +#Row 2 +color=00FF00 +3,0=xRX LATITUDE +3,12=xRX LONGITUDE +color=639AFF +4,0=GA +4,12=GO +#Row 3 +color=00FF00 +6,0=xRX ALTITUDE +6,12=xRX HEADING +color=639AFF +7,0=GH +7,12=GC +#Row 4 +color=00FF00 +9,0=xDISTANCE +9,12=xBEARING +color=639AFF +10,0=GD +10,12=GB +#Footer +color=FF0000,000000 +12.7,0.4=Q4 +12.7,18=bVV + +############ +@BatteryTFT.Puspis +timer=-1,-1,-1 +key1action=+,0,F,W +key2action=>,#,#,# +timeaction=#,#,# +scale=13,10 +fonts=0,1 +#Row 1 +color=90EE90 +0.5,4=XBATTERY STATUS +#Row 2 +color=00FF00 +3,0=x(C)HARGE/(B)ATT +3,11.5=xBATTERY VOLTAGE +color=639AFF +4,4=BS +4,11.5=BVV +#Row 3 +color=00FF00 +6,0=xCHARGE CURRENT +6,11.5=xDISCHG. CURRENT +color=639AFF +7,0=BCmA +7,11.5=BDmA +#Row 3 +color=00FF00 +9,0=xIC TEMPERATURE +9,11.5=xFREQ. OFFSET +color=639AFF +10,0=BTC +10,10=C +#Footer +color=FF0000,000000 +12.7,0.4=Q4 +12.7,18=bVV diff --git a/RX_FSK/data/screens5.txt b/RX_FSK/data/screens5.txt new file mode 100644 index 0000000..b2ec5e7 --- /dev/null +++ b/RX_FSK/data/screens5.txt @@ -0,0 +1,217 @@ +## screens3.txt: TFT display (portrait) +## based on http://www.p1337.synology.me/dokuwiki/doku.php?id=public:wettersonden +# Definition of display content and action behaviour +# +# Timer: (view timer, rx timer, norx timer) +# - value -1: timer is disabled; value>=0: timer fires after (value) seconds +# - view timer: time since current view (display mode and sonde) was started +# - rx timer: time since when sonde data has been received continuously (trigger immediatly after RX) +# - norx timer: time since when no sonde data has been received continuously +# (rx and norx timer is started after tuning a new frequency and receiving a signal or not receiving +# anything for a 1s period) +# +# Actions: +# - W: activate WiFi scan +# - F: activate frequency spectrum display +# - 0: activate "Scan:" display (this is basically just display mode 0) +# - x: (1..N): activate display mode x [deprecated] +# - >: activate next display mode +# - D: activate default receiver display (display mode specified in config) +# - +: advance to next active sonde from QRG config +# - #: no action +# +# Display content (lower/upper case: small/large font) +# line,column=content +# for ILI9225 its also possible to indicate +# line,column,width=content for text within a box of width 'width' +# line,column,-width=content for right-justified text +# +# XText : Text +# F(suffix): frequency (with suffix, e.g., " MHz") +# L latitade +# O lOngitute +# A altitude +# Hm(suffix) hor. speed m/s (suffix: e.g. "m/s"; no suffix=>m/s as 16x8 bitmap for SSD1306 display only) +# Hk(suffix) hor. speed km/h (suffix: e.g. "km/h"; no suffix=>km/h as 16x8 bitmap for SSD1306 display only) +# V(suffix) vert. speef (suffix: e.g. "m/s"; no suffix=>m/s as 16x8 bitmap for SSD1306 display only) +# Ix sonde ID (default/d: dxlaprs; s: short id, n: real serial number) +# RS41,RS92: all identical R1234567 +# DFMx: ID M12345678; short ID and serial 12345678 +# M10: ID ME95231F0; short ID: M95231F0; serial 9062104592 +# Q signal quality statistics bar +# T type string (RS41/DFM9/DFM6/RS92) +# C afC value +# N ip address (only tiny font) +# S scan list entry info: l/empty: launch site name, #=entry nr, t=total entries, a=active entries, /: #/t +# K RS41 kill timer values: Kl launch timer, Kb burst timer, Kc kill countdown +# format: K_4: h:mm k_6: h:mm:ss k_s: sssss, nothing shown for other sonde +# Mx telemetry value x (t temp p preassure h hyg) [not yet implemented, maybe some day in future] +# Gx GPS-related data +# raw data from GPS: GA, GO, GH, GC: LAtitude, lOngitude, Altutide(Height), Course over ground +# relative to sonde: GD, GI, GB: Distance, dIrection (absolute), relative Bearing +# G0 GPS circle diagram e.g. 3,5=g0NCS,50,ff0000,000033,5,ffff00,4,ffffff +# "N" (what is on top: N=north C=course) +# "C" (where does the arrow point to: C=course, S=sonde) +# "S" (what is shown by the bullet: C=course, S=sonde) +# 50: circle radius, followed by fg and bg color +# 5: bullet radius, followed by fg color +# 4: arrow width, followed by fg color +# R RSSI +# B battery(T-Beam 1.0) S=status V=Batt.Volt C=charge current D=discharge current +# U=USB volt I=USB current T=IC temp +# +# fonts=x,y can be used to select font (x=small, y=large) for all items below +# for SSD1306, x and y can be used to select one of those fonts: +# (y should be a 1x2 font (1,5,6,7), x a small font) +# u8x8_font_chroma48medium8_r, // 0 ** default small +# u8x8_font_7x14_1x2_f, // 1 ** default large +# u8x8_font_amstrad_cpc_extended_f, // 2 +# u8x8_font_5x7_f, // 3 +# u8x8_font_5x8_f, // 4 +# u8x8_font_8x13_1x2_f, // 5 +# u8x8_font_8x13B_1x2_f, // 6 +# u8x8_font_7x14B_1x2_f, // 7 +# u8x8_font_artossans8_r, // 8 +# u8x8_font_artosserif8_r, // 9 +# u8x8_font_torussansbold8_r, // 10 +# u8x8_font_victoriabold8_r, // 11 +# u8x8_font_victoriamedium8_r, // 12 +# u8x8_font_pressstart2p_f, // 13 +# u8x8_font_pcsenior_f, // 14 +# u8x8_font_pxplusibmcgathin_f, // 15 +# u8x8_font_pxplusibmcga_f, // 16 +# u8x8_font_pxplustandynewtv_f, // 17 +# +# for ILI9225, these fonts are available: +# Terminal6x8 // 0 +# Terminal11x16 // 1 +# Terminal12x16 // 2 +# FreeMono9pt7b, // 3 +# FreeMono12pt7b, // 4 +# FreeSans9pt7b, // 5 +# FreeSans12pt7b, // 6 +# Picopixel, // 7 +# +# color=rrggbb,rrggbb can be used to select color (foreground, background) +# see https://github.com/Nkawu/TFT_22_ILI9225/wiki#color-reference for example (use without "#"-sign) +# +# for TFT display, coordinates and width are multiplied by xscale,yscale and later used in pixels +# with scale=1,1 you can directly use pixel coordinates. (default: xscale=13,yscale=22 => 8 lines, 16 columns) + +########### +# +# Default configuration for "Scanner" display: +# - view timer disabled; rx timer=0; norx timer = 0 +# => after 1 second immediately an action is triggered +# (norx: go to next sonde; rx: go to default receiver display) +# - key1 actions: D,0,F,W +# => Button press activates default receiver view, double press does nothing +# Mid press activates Spectrum display, long press activates Wifi scan +# - key2 has no function +@ScannerPortrait +timer=-1,0,0 +key1action=D,#,F,W +key2action=>,#,#,# +timeaction=#,D,+ +0,0=XScan +0,5=S#: +0,9,4.5=T +6,0=XHoehe +6,5=GH +color=ffff00 +2,0=F MHz +4,0=S +color=00ff00,444444 +7,5=n +7,0=bV + +############ +# Default configuration for "Legacy" display: +# - view timer=-1, rx timer=-1 (disabled); norx timer=20 (or -1 for "old" behaviour) +# => norx timer fires after not receiving a singla for 20 seconds +# - key1 actions: +,0,F,W +# => Button1 press: next sonde; double press => @Scanner display +# => Mid press activates Spectrum display, long press activates Wifi scan +# - key2 actions: 2,#,#,# +# => BUtton2 activates display 2 (@Field) +# - timer actions: #,#,0 +# (norx timer: if no signal for >20 seconds: go back to scanner mode) +# +@LegacyPortrait +timer=-1,-1,N +key1action=+,0,F,W +key2action=>,#,#,# +timeaction=#,#,0 +9,10=f +9,0=r +9,4=Q +5,0=g0NCS,35,ffff00,000044,6,33ff33,5,eeaa00 +5,7=g0CCS,35,ffff00,000044,6,55ff55,5,eeaa00 +0,0=s +0,9=is +2,0=L +3,0=O +color=FFFF00 +1,6=Hk km/h +color=FF0000 +1,0=GD +color=FFFFFF +4,9=GH +3,9=V +4,0=A + +############ +@PeilungTFTPortrait +timer=-1,-1,N +key1action=+,0,F,W +key2action=>,#,#,# +timeaction=#,#,# +color=ffff00,000033 +color=bbbbbb,000000 +0,2=xN Top: +0,8=xCourse Top: +color=ffff00,000033 +1,0=g0NCS,35,ffff00,000044,6,33ff33,5,eeaa00 +1,7=g0CCS,35,ffff00,000044,6,55ff55,5,eeaa00 +color=ffffff,000000 +6,0=xDirection: +6,8,4=gI +7,0=xCOG: +7,4,4=gC +7,8=xturn: +7,12,4=gB + +############ +@GPSdataTFTPortrait +timer=-1,-1,N +key1action=+,0,F,W +key2action=>,#,#,# +timeaction=#,#,# +0,0=xOn-board GPS: +1,0,8=gA +2,0,8=gO +3,0,8=gH +4,0,8=gC +5,0=xGPS vs Sonde: +6,0,8=gD +7,0,8=gI +7,8,8=gB + +############ +@BatteryTFTPortrait +timer=-1,-1,-1 +key1action=+,0,F,W +key2action=>,#,#,# +timeaction=#,#,# +0,0=xBattery status: +0,14=bS +1,0=xBatt: +1,5,5=bVV +2,0,16=bCmA(charging) +3,0,16=bDmA(discharging) +4.4,0=xUSB: +4.4,5,5=bUV +5.4,0,10=bImA +6.4,0=xTemp: +6.4,5,5=bT C + diff --git a/RX_FSK/data/style.css b/RX_FSK/data/style.css old mode 100644 new mode 100755 index 6fb7ea2..224ac72 --- a/RX_FSK/data/style.css +++ b/RX_FSK/data/style.css @@ -44,7 +44,7 @@ td#sfreq { outline: none; cursor: pointer; padding: 10px 10px; - width: 14vw; + width: 12vw; transition: 0.3s; } @@ -128,3 +128,129 @@ p{ margin: 0; display: block; } + +#map { + height: 100%; +} + +.leaflet-popup-content table, .leaflet-popup-content table td { + border:0; + background-color: white; +} + +.leaflet-popup-content table td:nth-child(2),.leaflet-popup-content table td:nth-child(5) { + text-align: right; + padding-left: 3px; +} + +.leaflet-popup-content table td:nth-child(3),.leaflet-popup-content table td:nth-child(6) { + text-align: left; + padding-right: 10px; +} + +.leaflet-gps{animation:fading 1s infinite}@keyframes fading{0%{opacity:0.7}50%{opacity:1}100%{opacity:0.7}} + +.leaflet-gps::after { + content: 'πŸ”΅'; +} +.leaflet-gps { + margin-left: -7px !important; + margin-top: -9px !important; +} + +.leaflet-burst::after { + content: 'πŸ’₯'; +} +.leaflet-burst { + margin-left: -20px !important; + margin-top: -22px !important; + font-weight: bold; + font-size: 30px; +} + +.leaflet-landing::after { + content: 'Γ—'; +} + +.leaflet-landing { + margin-left: -13px !important; + margin-top: -30px !important; + font-weight: bold; + font-size: 40px; +} + + +.leaflet-header { + text-align: center; + width: 250px; + box-shadow: 0 1px 5px rgba(0,0,0,0.65); + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + pointer-events: auto !important; +} + +.leaflet-header #settings { + display: none; +} + +.leaflet-header label { + display: block; + margin-top: 5px; +} +.leaflet-header input { + width: 80px; + margin: 0 auto; +} + +.leaflet-header #submit { + margin: 3px auto; +} + +.leaflet-footer { + display:none; + text-align: center; + width: 180px; + box-shadow: 0 1px 5px rgba(0,0,0,0.65); + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} + +.leaflet-center { + left:0; + right:0; + margin: 0 auto; + padding: 5px; + background: #fff; + background: rgba(255, 255, 255, 0.8); +} + +.leaflet-header #sonde_detail { + display:none; +} + +@media screen and (max-width: 600px) { + .leaflet-control-attribution { + -moz-transform: rotate(-90deg) translateX(100%); + -ms-transform: rotate(-90deg) translateX(100%); + -o-transform: rotate(-90deg) translateX(100%);; + -webkit-transform: rotate(-90deg) translateX(100%); + transform: rotate(-90deg) translateX(100%); + -webkit-transform-origin: 100% 100%; + -moz-transform-origin: 100% 100%; + -ms-transform-origin: 100% 100%; + -o-transform-origin: 100% 100%; + transform-origin: 100% 100%; + } +} + +.ldot { + height: 1em; + width: 1em; + border-radius: 50%; + display: inline-block; +} + +.ybg { background-color: #E0E000; } +.gbg { background-color: green; } +.rbg { background-color: red; } + diff --git a/RX_FSK/version.h b/RX_FSK/version.h index c6d9c0b..40532b1 100644 --- a/RX_FSK/version.h +++ b/RX_FSK/version.h @@ -1,4 +1,4 @@ const char *version_name = "rdzTTGOsonde"; -const char *version_id = "devel20210728"; +const char *version_id = "devel20210815"; const int SPIFFS_MAJOR=2; -const int SPIFFS_MINOR=11; +const int SPIFFS_MINOR=14; diff --git a/libraries/SondeLib/DFM.cpp b/libraries/SondeLib/DFM.cpp index cef9978..ca4ad19 100644 --- a/libraries/SondeLib/DFM.cpp +++ b/libraries/SondeLib/DFM.cpp @@ -585,7 +585,7 @@ int DFM::receiveNew() { delay(2); } } - return RX_TIMEOUT; + return rxframes == 4 ? RX_TIMEOUT : RX_OK; } int DFM::receiveOld() { diff --git a/libraries/SondeLib/Display.cpp b/libraries/SondeLib/Display.cpp index f139495..a73da52 100644 --- a/libraries/SondeLib/Display.cpp +++ b/libraries/SondeLib/Display.cpp @@ -1,3 +1,4 @@ +#include "../../RX_FSK/features.h" #include #include #include @@ -24,9 +25,16 @@ extern AXP20X_Class axp; extern bool axp192_found; extern SemaphoreHandle_t axpSemaphore; +extern xSemaphoreHandle globalLock; +#define SPI_MUTEX_LOCK() \ + do \ + { \ + } while (xSemaphoreTake(globalLock, portMAX_DELAY) != pdPASS) +#define SPI_MUTEX_UNLOCK() xSemaphoreGive(globalLock) + struct GpsPos gpsPos; -SPIClass spiDisp(HSPI); +//SPIClass spiDisp(HSPI); const char *sondeTypeStr[NSondeTypes] = { "DFM ", "DFM9", "RS41", "RS92", "M10 ", "M20 ", "DFM6", "MP3H" }; const char *sondeTypeLongStr[NSondeTypes] = { "DFM (all)", "DFM9 (old)", "RS41", "RS92", "M10 ", "M20 ", "DFM6 (old)", "MP3-H1" }; @@ -312,14 +320,8 @@ void U8x8Display::drawQS(uint8_t x, uint8_t y, uint8_t len, uint8_t /*size*/, ui const GFXfont *gfl[] = { -#ifdef ALT9225 &Terminal11x16Font, // 3 (replacement for 1 or 2 with old library) &Terminal11x16Font, // 4 (replacement for 1 or 2 with old library) -#else - // nobody was using them, so removed with new library - &FreeMono9pt7b, // 3 - &FreeMono12pt7b, // 4 -#endif &FreeSans9pt7b, // 5 &FreeSans12pt7b, // 6 &Picopixel, // 7 @@ -331,13 +333,8 @@ struct gfxoffset_t { // first value: offset: max offset from font glyphs (last column * (-1)) (check /, \, `, $)` // yclear:max height: max of (height in 3rd column) + (yofs + 6th column) (check j) const struct gfxoffset_t gfxoffsets[]={ -#ifdef ALT9225 { 16, 18}, { 16, 18}, -#else - { 11, 15 }, // 13+11-9 "j" - { 15, 20 }, // 19+15-14 -#endif { 13, 18 }, // 17+13-12 "j" { 17, 23 }, // 23+17-17 { 4, 6}, // 6+4-4 @@ -348,55 +345,49 @@ static int ngfx = sizeof(gfl)/sizeof(GFXfont *); #define TFT_LED 0 // 0 if wired to +5V directly #define TFT_BRIGHTNESS 100 // Initial brightness of TFT backlight (optional) -#ifdef ALT9225 Arduino_DataBus *bus; -#endif - + void ILI9225Display::begin() { -#ifdef ALT9225 - Serial.println("ILI9225 init (alt driver)"); - bus = new Arduino_ESP32SPI( sonde.config.tft_rs, sonde.config.tft_cs, - sonde.config.oled_scl, sonde.config.oled_sda, -1, HSPI); - tft = new Arduino_ILI9225(bus, sonde.config.oled_rst); - Serial.println("ILI9225 init (alt driver): done"); - tft->begin(); + Serial.println("ILI9225/ILI9341 init"); + // On the M5, the display and the Lora chip are on the same SPI interface (VSPI default pins), + // we must use the same SPI bus with correct locking + if(sonde.config.type == TYPE_M5_CORE2) { + bus = new Arduino_ESP32SPI( sonde.config.tft_rs, sonde.config.tft_cs, + sonde.config.oled_scl, sonde.config.oled_sda, 38, VSPI); + } else { + bus = new Arduino_ESP32SPI( sonde.config.tft_rs, sonde.config.tft_cs, + sonde.config.oled_scl, sonde.config.oled_sda, -1, HSPI); + } + if(_type == 3) + tft = new Arduino_ILI9341(bus, sonde.config.oled_rst); + else if(_type == 4) + tft = new Arduino_ILI9342(bus, sonde.config.oled_rst); + else + tft = new Arduino_ILI9225(bus, sonde.config.oled_rst); + Serial.println("ILI9225/ILI9341 init: done"); + tft->begin(sonde.config.tft_spifreq); + tft->fillScreen(BLACK); tft->setRotation(sonde.config.tft_orient); tft->setTextWrap(false); -#else - tft = new MY_ILI9225(sonde.config.oled_rst, sonde.config.tft_rs, sonde.config.tft_cs, - sonde.config.oled_sda, sonde.config.oled_scl, TFT_LED, TFT_BRIGHTNESS); - tft->setModeFlip(sonde.config.tft_modeflip); - tft->begin(spiDisp); - tft->setOrientation(sonde.config.tft_orient); -#endif + if(sonde.config.type == TYPE_M5_CORE2) + tft->invertDisplay(true); } void ILI9225Display::clear() { -#ifdef ALT9225 - tft->fillScreen(0); -#else - tft->clear(); -#endif + SPI_MUTEX_LOCK(); + tft->fillScreen(BLACK); + SPI_MUTEX_UNLOCK(); } // for now, 0=small=FreeSans9pt7b, 1=large=FreeSans18pt7b void ILI9225Display::setFont(uint8_t fontindex) { -#ifdef ALT9225 if(fontindex==1 || fontindex==2) { fontindex=3; } -#endif findex = fontindex; switch(fontindex) { -#ifdef ALT9225 case 0: tft->setFont(NULL); tft->setTextSize(1); break; case 1: tft->setFont(NULL); tft->setTextSize(2); break; case 2: tft->setFont(NULL); tft->setTextSize(2); break; default: tft->setFont(gfl[fontindex-3]); -#else - case 0: tft->setFont(Terminal6x8); break; - case 1: tft->setFont(Terminal11x16); break; - case 2: tft->setFont(Terminal12x16); break; - default: tft->setGFXFont(gfl[fontindex-3]); -#endif } } @@ -418,7 +409,6 @@ void ILI9225Display::getDispSize(uint8_t *height, uint8_t *width, uint8_t *lines break; default: // get size from GFX Font { -#ifdef ALT9225 int16_t x, y; uint16_t w, h; tft->getTextBounds("|", 0, 0, &x, &y, &w, &h); @@ -426,13 +416,6 @@ void ILI9225Display::getDispSize(uint8_t *height, uint8_t *width, uint8_t *lines tft->getTextBounds("A", 0, 0, &x, &y, &w, &h); if(colskip) *colskip = w+2; if(lineskip&&colskip) { Serial.printf("skip size from bounds: %d, %d\n", *lineskip, *colskip); } -#else - int16_t w,h,a; - tft->getGFXCharExtent('|',&w,&h,&a); - if(lineskip) *lineskip = h+2; - tft->getGFXCharExtent('A',&w,&h,&a); - if(colskip) *colskip = w+2; // just an approximation -#endif } } } @@ -448,16 +431,12 @@ void ILI9225Display::drawString(uint8_t x, uint8_t y, const char *s, int16_t wid } // Standard font if(findex<3) { + SPI_MUTEX_LOCK(); DebugPrintf(DEBUG_DISPLAY, "Simple Text %s at %d,%d [%d]\n", s, x, y, width); -#ifdef ALT9225 // for gpx fonts and new library, cursor is at baseline!! int h = 6; if(findex>1) h=12; -#else - tft->setBackgroundColor(bg); - int h = tft->getFont().height; -#endif if( alignright ) { -#ifdef ALT9225 +#if 1 //w = tft->getTextWidth(s); /// TODO if( width==WIDTH_AUTO ) { width = w; } @@ -476,7 +455,6 @@ void ILI9225Display::drawString(uint8_t x, uint8_t y, const char *s, int16_t wid tft->drawText(x + width - w, y, s, fg); #endif } else { -#ifdef ALT9225 tft->setCursor(x, y); tft->setTextColor(fg, bg); tft->print(s); @@ -486,25 +464,16 @@ void ILI9225Display::drawString(uint8_t x, uint8_t y, const char *s, int16_t wid //if(curx < x + width) { // tft->fillRectangle(curx, y, x + width - 1, y + h - 1, bg); //} -#else - int curx = tft->drawText(x, y, s, fg); - if( width==WIDTH_AUTO ) { return; } - if(curx < x + width) { - tft->fillRectangle(curx, y, x + width - 1, y + h - 1, bg); - } -#endif } + SPI_MUTEX_UNLOCK(); return; } // GFX font + SPI_MUTEX_LOCK(); int16_t x1, y1; if(1||width==WIDTH_AUTO || alignright) { -#ifdef ALT9225 tft->getTextBounds(s, x, y + gfxoffsets[findex-3].yofs, &x1, &y1, (uint16_t *)&w, (uint16_t *)&h); w += x1 - x + 1; -#else - tft->getGFXTextExtent(s, x, y + gfxoffsets[findex-3].yofs, &w, &h); -#endif if(width==WIDTH_AUTO) { width=w; } if(alignright) { if(w > width) { @@ -530,7 +499,7 @@ void ILI9225Display::drawString(uint8_t x, uint8_t y, const char *s, int16_t wid } #else // Text by drawing bitmap.... => less "flicker" -#ifdef ALT9225 +#if 1 //TODO tft->setCursor( alignright? x+width-w : x, y + gfxoffsets[findex-3].yofs); tft->setTextColor( fg, bg ); @@ -568,11 +537,12 @@ void ILI9225Display::drawString(uint8_t x, uint8_t y, const char *s, int16_t wid free(bitmap); #endif #endif + SPI_MUTEX_UNLOCK(); } void ILI9225Display::drawTile(uint8_t x, uint8_t y, uint8_t cnt, uint8_t *tile_ptr) { -#ifdef ALT9225 int i,j; + SPI_MUTEX_LOCK(); tft->startWrite(); for(i=0; iendWrite(); -#else - tft->drawTile(x, y, cnt, tile_ptr); -#endif + SPI_MUTEX_UNLOCK(); #if 0 int i,j; tft->startWrite(); @@ -602,26 +570,24 @@ void ILI9225Display::drawTile(uint8_t x, uint8_t y, uint8_t cnt, uint8_t *tile_p } void ILI9225Display::drawTriangle(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t x3, uint16_t y3, uint16_t color, boolean fill) { + SPI_MUTEX_LOCK(); if(fill) tft->fillTriangle(x1, y1, x2, y2, x3, y3, color); else tft->drawTriangle(x1, y1, x2, y2, x3, y3, color); + SPI_MUTEX_UNLOCK(); } void ILI9225Display::drawBitmap(uint16_t x1, uint16_t y1, const uint16_t* bitmap, int16_t w, int16_t h) { -#ifdef ALT9225 + SPI_MUTEX_LOCK(); tft->draw16bitRGBBitmap(x1, y1, bitmap, w, h); -#else - tft->drawBitmap(x1, y1, bitmap, w, h); -#endif + SPI_MUTEX_UNLOCK(); } void ILI9225Display::welcome() { -#ifdef ALT9225 + SPI_MUTEX_LOCK(); tft->fillScreen(0); -#else - tft->clear(); -#endif + SPI_MUTEX_UNLOCK(); setFont(6); drawString(0, 0*22, version_name, WIDTH_AUTO, 0xff00); setFont(5); @@ -669,8 +635,9 @@ void ILI9225Display::drawQS(uint8_t x, uint8_t y, uint8_t len, uint8_t size, uin #define pgm_read_pointer(addr) ((void *)pgm_read_dword(addr)) -#ifdef ALT9225 +#if 1 #else +// TO BE REMOVED void MY_ILI9225::drawTile(uint8_t x, uint8_t y, uint8_t cnt, uint8_t *tile_ptr) { int i,j; startWrite(); @@ -731,8 +698,8 @@ RawDisplay *Display::rdis = NULL; //TODO: maybe merge with initFromFile later? void Display::init() { Serial.printf("disptype is %d\n",sonde.config.disptype); - if(sonde.config.disptype==1) { - rdis = new ILI9225Display(); + if(sonde.config.disptype==1 || sonde.config.disptype==3 || sonde.config.disptype==4 ) { + rdis = new ILI9225Display(sonde.config.disptype); } else { rdis = new U8x8Display(sonde.config.disptype); } @@ -972,17 +939,33 @@ int Display::countEntries(File f) { return n; } +int Display::getScreenIndex(int index) { + if(index!=0) return index; + switch(sonde.config.disptype) { + case 1: // ILI9225 + index = 2; // landscape mode (orient=1/3) + if( (sonde.config.tft_orient&0x01)==0 ) index++; // portrait mode (0/2) + break; + case 3: // ILI9341 + case 4: // ILI9342 + index = 4; // landscape mode (orient=1/3) + if( (sonde.config.tft_orient&0x01)==0 ) index++; // portrait mode (0/2) + break; + case 0: case 2: // small OLED display (SD1306/SH1106) + default: + index = 1; break; + } + return index; +} void Display::initFromFile(int index) { File d; - if(index>0) { - char file[20]; - snprintf(file, 20, "/screens%d.txt", index); - Serial.printf("Reading %s\n", file); - d = SPIFFS.open(file, "r"); - if(!d || d.available()==0 ) { Serial.printf("%s not found, using /screens.txt\n", file); } - } - if(!d || d.available()==0 ) d = SPIFFS.open("/screens.txt", "r"); - if(!d) return; + char file[20]; + + index = getScreenIndex(index); // auto selection for index==0 + snprintf(file, 20, "/screens%d.txt", index); + Serial.printf("Reading %s\n", file); + d = SPIFFS.open(file, "r"); + if(!d || d.available()==0 ) { Serial.printf("%s not found\n", file); return; } DispInfo *newlayouts = (DispInfo *)malloc(MAXSCREENS * sizeof(DispInfo)); if(!newlayouts) { @@ -1086,7 +1069,7 @@ void Display::initFromFile(int index) { char text[61]; n=sscanf(s, "%f,%f,%f", &y, &x, &w); sscanf(ptr+1, "%60[^\r\n]", text); - if(sonde.config.disptype==1) { x*=xscale; y*=yscale; w*=xscale; } + if(sonde.config.disptype==1 || sonde.config.disptype==3 || sonde.config.disptype==4 ) { x*=xscale; y*=yscale; w*=xscale; } newlayouts[idx].de[what].x = x; newlayouts[idx].de[what].y = y; newlayouts[idx].de[what].width = n>2 ? w : WIDTH_AUTO; @@ -1244,6 +1227,7 @@ void Display::drawID(DispEntry *de) { } void Display::drawRSSI(DispEntry *de) { rdis->setFont(de->fmt); + // TODO.... 3/4!!!!! if(sonde.config.disptype!=1) { snprintf(buf, 16, "-%d ", sonde.si()->rssi/2); int len=strlen(buf)-3; @@ -1275,10 +1259,7 @@ void Display::drawFreq(DispEntry *de) { drawString(de, buf); } void Display::drawAFC(DispEntry *de) { - if(!sonde.config.showafc) return; rdis->setFont(de->fmt); - //if(sonde.si()->afc==0) { strcpy(buf, " "); } - //else { snprintf(buf, 15, " %+3.2fk", sonde.si()->afc*0.001); } drawString(de, buf+strlen(buf)-8); } @@ -1291,7 +1272,7 @@ void Display::drawSite(DispEntry *de) { switch(de->extra[0]) { case '#': // currentSonde is index in array starting with 0; - // but we draw "1" for the first entrie and so on... + // but we draw "1" for the first entry and so on... snprintf(buf, 3, "%2d", sonde.currentSonde+1); buf[2]=0; break; @@ -1361,6 +1342,15 @@ void Display::drawKilltimer(DispEntry *de) { #define FAKEGPS 0 extern int lastCourse; // from RX_FSK.ino + + +float calcLatLonDist(float lat1, float lon1, float lat2, float lon2) { + float x = radians(lon1-lon2) * cos( radians((lat1+lat2)/2) ); + float y = radians(lat2-lat1); + float d = sqrt(x*x+y*y)*EARTH_RADIUS; + return d; +} + void Display::calcGPS() { // base data #if 0 @@ -1392,12 +1382,7 @@ static int tmpc=0; #endif // distance if( gpsPos.valid && (sonde.si()->validPos&0x03)==0x03 && (layout->usegps&GPSUSE_DIST)) { - float lat1 = gpsPos.lat; - float lat2 = sonde.si()->lat; - float x = radians(gpsPos.lon-sonde.si()->lon) * cos( radians((lat1+lat2)/2) ); - float y = radians(lat2-lat1); - float d = sqrt(x*x+y*y)*EARTH_RADIUS; - gpsDist = (int)d; + gpsDist = (int)calcLatLonDist(gpsPos.lat, gpsPos.lon, sonde.si()->lat, sonde.si()->lon); } else { gpsDist = -1; } @@ -1538,7 +1523,8 @@ void Display::drawGPS(DispEntry *de) { } Serial.printf("GPS0: %c%c%c N=%d, A=%d, B=%d\n", circinfo->top, circinfo->arr, circinfo->bul, angN, angA, angB); // "N" in direction angN -#ifdef ALT9225 +#if 1 + // TODO #else static_cast(rdis)->tft->drawGFXcharBM(x0 + circinfo->radius*sin(angN*PI/180)-6, y0 - circinfo->radius*cos(angN*PI/180)+7, 'N', 0xffff, bitmap, size, size); #endif diff --git a/libraries/SondeLib/Display.h b/libraries/SondeLib/Display.h index e35815f..e2bfece 100644 --- a/libraries/SondeLib/Display.h +++ b/libraries/SondeLib/Display.h @@ -1,5 +1,3 @@ -#define ALT9225 - #ifndef Display_h #define Display_h @@ -7,11 +5,7 @@ #define FONT_SMALL 0 #include -#ifdef ALT9225 #include -#else -#include -#endif #include #include @@ -81,12 +75,12 @@ public: class U8x8Display : public RawDisplay { private: U8X8 *u8x8 = NULL; // initialize later after reading config file - int _type; + uint8_t _type; const uint8_t **fontlist; int nfonts; public: - U8x8Display(int type = 0) { _type = type; } + U8x8Display(uint8_t type = 0) { _type = type; } void begin(); void clear(); void setFont(uint8_t fontindex); @@ -100,24 +94,17 @@ public: void drawQS(uint8_t x, uint8_t y, uint8_t len, uint8_t size, uint8_t *stat, uint16_t fg=0xffff, uint16_t bg=0); }; -#ifdef ALT9225 - typedef Arduino_GFX MY_ILI9225; -#else -class MY_ILI9225 : public TFT22_ILI9225 { - using TFT22_ILI9225::TFT22_ILI9225; -public: - uint16_t drawGFXChar(int16_t x, int16_t y, unsigned char c, uint16_t color); - void drawTile(uint8_t x, uint8_t y, uint8_t cnt, uint8_t *tile_ptr); -}; -#endif +typedef Arduino_GFX MY_ILI9225; class ILI9225Display : public RawDisplay { private: uint8_t yofs=0; uint8_t findex=0; + uint8_t _type; public: MY_ILI9225 *tft = NULL; // initialize later after reading config file + ILI9225Display(int type = 1) { _type = type; } void begin(); void clear(); void setFont(uint8_t fontindex); @@ -161,6 +148,7 @@ private: return ret; } public: + static int getScreenIndex(int index); void initFromFile(int index); int layoutIdx; diff --git a/libraries/SondeLib/M10M20.cpp b/libraries/SondeLib/M10M20.cpp index 4d7722d..c10117d 100644 --- a/libraries/SondeLib/M10M20.cpp +++ b/libraries/SondeLib/M10M20.cpp @@ -276,6 +276,7 @@ int M10M20::decodeframeM10(uint8_t *data) { } Serial.println(crcok?"CRC OK":"CRC NOT OK"); Serial.printf(" repair: %d/%d\n", repl, repairstep); + if(!crcok) return 2; if(data[1]==0x9F && data[2]==0x20) { Serial.println("Decoding..."); @@ -338,7 +339,7 @@ int M10M20::decodeframeM10(uint8_t *data) { Serial.printf("data is %02x %02x %02x\n", data[0], data[1], data[2]); return 0; } - return crcok?1:2; + return 1; } static uint32_t rxdata; @@ -392,12 +393,14 @@ void M10M20::processM10data(uint8_t dt) if(rxp==2 && dataptr[0]==0x45 && dataptr[1]==0x20) { isM20 = true; } if(isM20) { memcpy(sonde.si()->typestr, "M20 ", 5); + sonde.si()->subtype = 2; if(rxp>=M20_FRAMELEN) { rxsearching = true; haveNewFrame = decodeframeM20(dataptr); } } else { memcpy(sonde.si()->typestr, "M10 ", 5); + sonde.si()->subtype = 1; if(rxp>=M10_FRAMELEN) { rxsearching = true; haveNewFrame = decodeframeM10(dataptr); diff --git a/libraries/SondeLib/SX1278FSK.cpp b/libraries/SondeLib/SX1278FSK.cpp index ecb7fdc..ba56638 100644 --- a/libraries/SondeLib/SX1278FSK.cpp +++ b/libraries/SondeLib/SX1278FSK.cpp @@ -14,13 +14,38 @@ #include #include -SX1278FSK::SX1278FSK() + +#define SPI_MUTEX_LOCK() \ + do \ + { \ + } while (xSemaphoreTake(_lock, portMAX_DELAY) != pdPASS) +#define SPI_MUTEX_UNLOCK() xSemaphoreGive(_lock) + +SX1278FSK::SX1278FSK() {} + +void SX1278FSK::setup(xSemaphoreHandle lock) { - // Initialize class variables + _lock = lock; + Serial.println("Setup sx1278"); + if(_lock) SPI_MUTEX_LOCK(); + digitalWrite(sonde.config.sx1278_ss, HIGH); + pinMode(sonde.config.sx1278_ss, OUTPUT); + Serial.printf("Configuing SX1278FSK SPI with miso=%d, mosi=%d, sck=%d, ss=%d\n", sonde.config.sx1278_miso, + sonde.config.sx1278_mosi, sonde.config.sx1278_sck, sonde.config.sx1278_ss); + SPI.begin(sonde.config.sx1278_sck, sonde.config.sx1278_miso, sonde.config.sx1278_mosi, -1); // no hardware CS + // was: SPI.begin(); + + //Set most significant bit first + SPI.setBitOrder(MSBFIRST); + //Divide the clock frequency + SPI.setClockDivider(SPI_CLOCK_DIV2); + //Set data mode + SPI.setDataMode(SPI_MODE0); + if(_lock) SPI_MUTEX_UNLOCK(); }; -static SPISettings spiset = SPISettings(40000000L, MSBFIRST, SPI_MODE0); +static SPISettings spiset = SPISettings(10000000L, MSBFIRST, SPI_MODE0); /* Function: Turns the module ON. @@ -34,19 +59,6 @@ uint8_t SX1278FSK::ON() Serial.println(F("Starting 'ON'")); #endif - // Powering the module - pinMode(SX1278_SS, OUTPUT); - digitalWrite(SX1278_SS, HIGH); - - //Configure the MISO, MOSI, CS, SPCR. - SPI.begin(); - //Set most significant bit first - SPI.setBitOrder(MSBFIRST); - //Divide the clock frequency - SPI.setClockDivider(SPI_CLOCK_DIV2); - //Set data mode - SPI.setDataMode(SPI_MODE0); - // Set Maximum Over Current Protection state = setMaxCurrent(0x1B); if( state == 0 ) @@ -60,7 +72,6 @@ uint8_t SX1278FSK::ON() { return 1; } - // set FSK mode state = setFSK(); return state; @@ -77,10 +88,12 @@ void SX1278FSK::OFF() Serial.println(F("Starting 'OFF'")); #endif - SPI.end(); + //SPI.end(); +#if 0 // Powering the module pinMode(SX1278_SS,OUTPUT); digitalWrite(SX1278_SS,LOW); +#endif #if (SX1278FSK_debug_mode > 1) Serial.println(F("## Setting OFF ##")); @@ -98,15 +111,16 @@ byte SX1278FSK::readRegister(byte address) { byte value = 0x00; + if(_lock) SPI_MUTEX_LOCK(); + digitalWrite(sonde.config.sx1278_ss,LOW); SPI.beginTransaction(spiset); - digitalWrite(SX1278_SS,LOW); //delay(1); bitClear(address, 7); // Bit 7 cleared to write in registers SPI.transfer(address); value = SPI.transfer(0x00); - digitalWrite(SX1278_SS,HIGH); SPI.endTransaction(); + digitalWrite(sonde.config.sx1278_ss,HIGH); #if (SX1278FSK_debug_mode > 1) if(address!=0x3F) { @@ -118,7 +132,7 @@ byte SX1278FSK::readRegister(byte address) Serial.println(); } #endif - + if(_lock) SPI_MUTEX_UNLOCK(); return value; } @@ -131,15 +145,16 @@ Parameters: */ void SX1278FSK::writeRegister(byte address, byte data) { + if(_lock) SPI_MUTEX_LOCK(); + digitalWrite(sonde.config.sx1278_ss,LOW); SPI.beginTransaction(spiset); - digitalWrite(SX1278_SS,LOW); //delay(1); bitSet(address, 7); // Bit 7 set to read from registers SPI.transfer(address); SPI.transfer(data); - digitalWrite(SX1278_SS,HIGH); SPI.endTransaction(); + digitalWrite(sonde.config.sx1278_ss,HIGH); #if (SX1278FSK_debug_mode > 1) Serial.print(F("## Writing: ##\t")); @@ -150,7 +165,7 @@ void SX1278FSK::writeRegister(byte address, byte data) Serial.print(data, HEX); Serial.println(); #endif - + if(_lock) SPI_MUTEX_UNLOCK(); } /* @@ -867,4 +882,5 @@ void SX1278FSK::showRxRegisters() } #endif +xSemaphoreHandle globalLock =xSemaphoreCreateMutex(); SX1278FSK sx1278 = SX1278FSK(); diff --git a/libraries/SondeLib/SX1278FSK.h b/libraries/SondeLib/SX1278FSK.h index 6162be9..ea95b69 100644 --- a/libraries/SondeLib/SX1278FSK.h +++ b/libraries/SondeLib/SX1278FSK.h @@ -35,8 +35,6 @@ #define SX1278FSK_debug_mode 0 -#define SX1278_SS SS - //! MACROS // #define bitRead(value, bit) (((value) >> (bit)) & 0x01) // read a bit #define bitSet(value, bit) ((value) |= (1UL << (bit))) // set bit to '1' @@ -171,7 +169,9 @@ class SX1278FSK { public: // class constructor - SX1278FSK(); + SX1278FSK(); + + void setup(xSemaphoreHandle lock); // Turn on SX1278 module (return 0 on sucess, 1 otherwise) uint8_t ON(); @@ -256,7 +256,7 @@ public: // Receive a packet uint8_t receivePacketTimeout(uint32_t wait, byte *data); - + xSemaphoreHandle _lock = NULL; #if 0 //! It gets the internal temperature of the module. diff --git a/libraries/SondeLib/Scanner.cpp b/libraries/SondeLib/Scanner.cpp index d2ddad9..31a5d7e 100644 --- a/libraries/SondeLib/Scanner.cpp +++ b/libraries/SondeLib/Scanner.cpp @@ -24,6 +24,7 @@ struct scancfg { //struct scancfg scanLCD={ 121, 7, 120/6, 120/6/4, 6000.0/120.0/20.0, 20, 120*20, 1 }; struct scancfg scanLCD={ 121, 7, 120/6, 120/6/4, 6000.0/120.0/10.0, 10, 120*10, 2, 40 }; struct scancfg scanTFT={ 210, 16, 210/6, 210/6/5, 6000.0/210.0/10.0, 10, 210*10, 1, 0 }; +struct scancfg scan9341={ 210, 16, 210/6, 210/6/5, 6000.0/210.0/10.0, 10, 210*10, 1, 0 }; struct scancfg &scanconfig = scanTFT; @@ -65,7 +66,7 @@ void Scanner::fillTiles(uint8_t *row, int value) { ///// unused???? uint8_t tiles[16] = { 0x0f,0x0f,0x0f,0x0f,0xf0,0xf0,0xf0,0xf0, 1, 3, 7, 15, 31, 63, 127, 255}; // type 0: lcd, 1: tft, 2: lcd(sh1106) -#define ISTFT (sonde.config.disptype==1) +#define ISTFT (sonde.config.disptype==1 || sonde.config.disptype==3) void Scanner::plotResult() { int yofs = 0; diff --git a/libraries/SondeLib/Sonde.cpp b/libraries/SondeLib/Sonde.cpp index 96da15f..9f6f6c2 100644 --- a/libraries/SondeLib/Sonde.cpp +++ b/libraries/SondeLib/Sonde.cpp @@ -82,16 +82,21 @@ void Sonde::defaultConfig() { config.power_pout = -1; config.spectrum=10; // Try autodetecting board type + config.type = TYPE_TTGO; // Seems like on startup, GPIO4 is 1 on v1 boards, 0 on v2.1 boards? config.gps_rxd = -1; config.gps_txd = -1; + config.sx1278_ss = SS; // default SS pin, on all TTGOs + config.sx1278_miso = MISO; + config.sx1278_mosi = MOSI; + config.sx1278_sck = SCK; config.oled_rst = 16; config.disptype = 0; config.tft_orient = 1; config.button2_axp = 0; config.norx_timeout = 20; config.screenfile = 1; - config.tft_modeflip = 0; + config.tft_spifreq = SPI_DEFAULT_FREQ; if(initlevels[16]==0) { config.oled_sda = 4; config.oled_scl = 15; @@ -103,32 +108,55 @@ void Sonde::defaultConfig() { } else { config.oled_sda = 21; config.oled_scl = 22; - if(initlevels[17]==0) { // T-Beam + if(initlevels[17]==0) { // T-Beam or M5Stack Core2? int tbeam=7; if(initlevels[12]==0) { tbeam = 10; - Serial.println("Autoconfig: looks like T-Beam 1.0 board"); + Serial.println("Autoconfig: looks like T-Beam 1.0 or M5Stack Core2 board"); } else if ( initlevels[4]==1 && initlevels[12]==1 ) { tbeam = 11; Serial.println("Autoconfig: looks like T-Beam 1.1 board"); } if(tbeam == 10 || tbeam == 11) { // T-Beam v1.0 or T-Beam v1.1 - config.button_pin = 38; - config.button2_pin = 15 + 128; //T4 + 128; // T4 = GPIO13 - // Maybe in future use as default only PWR as button2? - //config.button2_pin = 255; - config.button2_axp = 1; - config.gps_rxd = 34; - config.gps_txd = 12; - // Check for I2C-Display@21,22 -#define SSD1306_ADDRESS 0x3c Wire.begin(21, 22); - Wire.beginTransmission(SSD1306_ADDRESS); - byte err = Wire.endTransmission(); - delay(100); // otherwise its too fast?! - Wire.beginTransmission(SSD1306_ADDRESS); - err = Wire.endTransmission(); - if(err!=0 && fingerprint!=17) { // hmm. 17 after powerup with oled commected and no i2c answer!?!? +#define BM8563_ADDRESS 0x51 + Wire.beginTransmission(BM8563_ADDRESS); + byte err = Wire.endTransmission(); + if(err==0) { + Serial.println("M5stack Core2 board detected\n"); + config.type = TYPE_M5_CORE2; + config.button_pin = 255; + config.button2_pin = 255; + config.button2_axp = 1; + config.disptype = 4; // ILI9342 + config.oled_sda = 23; + config.oled_scl = 18; + config.oled_rst = -1; + config.tft_rs = 15; + config.tft_cs = 5; + config.screenfile = 4; + config.gps_rxd = 13; + config.gps_txd = -1; // 14 + config.sx1278_ss = 33; + config.sx1278_miso = 38; + config.sx1278_mosi = 23; //MOSI; + config.sx1278_sck = 18; // SCK; + } else { // some t-beam... + config.button_pin = 38; + config.button2_pin = 15 + 128; //T4 + 128; // T4 = GPIO13 + // Maybe in future use as default only PWR as button2? + //config.button2_pin = 255; + config.button2_axp = 1; + config.gps_rxd = 34; + config.gps_txd = 12; + // Check for I2C-Display@21,22 +#define SSD1306_ADDRESS 0x3c + Wire.beginTransmission(SSD1306_ADDRESS); + err = Wire.endTransmission(); + delay(100); // otherwise its too fast?! + Wire.beginTransmission(SSD1306_ADDRESS); + err = Wire.endTransmission(); + if(err!=0 && fingerprint!=17) { // hmm. 17 after powerup with oled commected and no i2c answer!?!? fingerprint |= 128; Serial.println("no I2C display found, assuming large TFT display\n"); // CS=0, RST=14, RS=2, SDA=4, CLK=13 @@ -141,10 +169,11 @@ void Sonde::defaultConfig() { config.tft_cs = 0; config.spectrum = -1; // no spectrum for now on large display config.screenfile = 2; - } else { + } else { // OLED display, pins 21,22 ok... config.disptype = 0; Serial.println("... with small OLED display\n"); + } } } else { Serial.println("Autoconfig: looks like T-Beam v0.7 board"); @@ -185,7 +214,6 @@ void Sonde::defaultConfig() { config.startfreq=400; config.channelbw=10; config.marker=0; - config.showafc=0; config.freqofs=0; config.rs41.agcbw=12500; config.rs41.rxbw=6300; @@ -265,12 +293,20 @@ void Sonde::setConfig(const char *cfg) { config.tft_cs = atoi(val); } else if(strcmp(cfg,"tft_orient")==0) { config.tft_orient = atoi(val); - } else if(strcmp(cfg,"tft_modeflip")==0) { - config.tft_modeflip = atoi(val); + } else if(strcmp(cfg,"tft_spifreq")==0) { + config.tft_spifreq = atoi(val); } else if(strcmp(cfg,"gps_rxd")==0) { config.gps_rxd = atoi(val); } else if(strcmp(cfg,"gps_txd")==0) { config.gps_txd = atoi(val); + } else if(strcmp(cfg,"sx1278_ss")==0) { + config.sx1278_ss = atoi(val); + } else if(strcmp(cfg,"sx1278_miso")==0) { + config.sx1278_miso = atoi(val); + } else if(strcmp(cfg,"sx1278_mosi")==0) { + config.sx1278_mosi = atoi(val); + } else if(strcmp(cfg,"sx1278_sck")==0) { + config.sx1278_sck = atoi(val); } else if(strcmp(cfg,"maxsonde")==0) { config.maxsonde = atoi(val); if(config.maxsonde>MAXSONDE) config.maxsonde=MAXSONDE; @@ -305,8 +341,6 @@ void Sonde::setConfig(const char *cfg) { config.spectrum = atoi(val); } else if(strcmp(cfg,"marker")==0) { config.marker = atoi(val); - } else if(strcmp(cfg,"showafc")==0) { - config.showafc = atoi(val); } else if(strcmp(cfg,"freqofs")==0) { config.freqofs = atoi(val); } else if(strcmp(cfg,"rs41.agcbw")==0) { @@ -383,9 +417,11 @@ void Sonde::setConfig(const char *cfg) { } else if(strcmp(cfg, "sondehub.callsign")==0) { strncpy(config.sondehub.callsign, val, 63); } else if(strcmp(cfg, "sondehub.lat")==0) { - strncpy(config.sondehub.lat, val, 19); + config.sondehub.lat = *val==0 ? NAN : atof(val); + Serial.printf("lat is %f\n", config.sondehub.lat); } else if(strcmp(cfg, "sondehub.lon")==0) { - strncpy(config.sondehub.lon, val, 19); + config.sondehub.lon = *val==0 ? NAN : atof(val); + Serial.printf("lon is %f\n", config.sondehub.lon); } else if(strcmp(cfg, "sondehub.alt")==0) { strncpy(config.sondehub.alt, val, 19); } else if(strcmp(cfg, "sondehub.antenna")==0) { diff --git a/libraries/SondeLib/Sonde.h b/libraries/SondeLib/Sonde.h index ab41365..42f2e6e 100644 --- a/libraries/SondeLib/Sonde.h +++ b/libraries/SondeLib/Sonde.h @@ -67,7 +67,7 @@ typedef struct st_sondeinfo { // receiver configuration bool active; SondeType type; - int8_t subtype; /* 0 for none/unknown, hex type for dfm, maybe add 1/2 for M10/M20 as well?*/ + int8_t subtype; /* 0 for none/unknown, hex type for dfm, 1/2 for M10/M20 */ float freq; // decoded ID char typestr[5]; // decoded type (use type if *typestr==0) @@ -187,14 +187,18 @@ struct st_sondehub { int chase; char host[64]; char callsign[64]; - char lat[20]; - char lon[20]; + double lat; + double lon; char alt[20]; char antenna[64]; char email[64]; }; +// to be extended +enum { TYPE_TTGO, TYPE_M5_CORE2 }; + typedef struct st_rdzconfig { + int type; // autodetected type, TTGO or M5_CORE2 // hardware configuration int button_pin; // PIN port number menu button (+128 for touch mode) int button2_pin; // PIN port number menu button (+128 for touch mode) @@ -209,9 +213,13 @@ typedef struct st_rdzconfig { int tft_rs; // TFT RS pin int tft_cs; // TFT CS pin int tft_orient; // TFT orientation (default: 1) - int tft_modeflip; // Hack for Joerg's strange display + int tft_spifreq; // SPI transfer speed (default 40M is out of spec for some TFT) int gps_rxd; // GPS module RXD pin. We expect 9600 baud NMEA data. int gps_txd; // GPS module TXD pin + int sx1278_ss; // SPI slave select for sx1278 + int sx1278_miso; // SPI MISO for sx1278 + int sx1278_mosi; // SPI MOSI for sx1278 + int sx1278_sck; // SPI SCK for sx1278 // software configuration int debug; // show port and config options after reboot int wifi; // connect to known WLAN 0=skip @@ -227,7 +235,6 @@ typedef struct st_rdzconfig { int noisefloor; // for spectrum display char mdnsname[15]; // mDNS-Name, defaults to rdzsonde // receiver configuration - int showafc; // show afc value in rx screen int freqofs; // frequency offset (tuner config = rx frequency + freqofs) in Hz struct st_rs41config rs41; // configuration options specific for RS41 receiver struct st_rs92config rs92; diff --git a/libraries/SondeLib/autodetect-infos b/libraries/SondeLib/autodetect-infos index fe23d05..d93da7f 100644 --- a/libraries/SondeLib/autodetect-infos +++ b/libraries/SondeLib/autodetect-infos @@ -34,6 +34,16 @@ TTGO T-Team 1.0 with IL9225 TFT => fingerprint 23 0:1 1:0 2:0 3:1 4:0 5:1 6:0 7:1 8:0 9:1 10:1 11:1 12:0 13:0 14:1 15:1 16:1 17:0 18:0 19:0 20:0 21:1 22:1 23:1 24:0 25:0 26:0 27:0 28:0 29:0 30:0 31:0 32:0 33:0 34:0 35:0 36:0 37:0 38:0 0:1 1:1 2:0 3:1 4:0 5:1 6:0 7:1 8:0 9:1 10:1 11:1 12:0 13:0 14:1 15:1 16:1 17:0 18:0 19:0 20:0 21:1 22:1 23:1 24:0 25:0 26:0 27:0 28:0 29:0 30:0 31:0 32:0 33:0 34:0 35:0 36:0 37:0 38:0 (before setup) +M5Stack Core2 +0:1 1:0 2:0 3:1 4:1 5:1 6:0 7:1 8:0 9:1 10:1 11:1 12:0 13:1 14:1 15:1 16:1 17:0 18:1 19:0 20:0 21:1 22:1 23:1 24:0 25:0 26:0 27:0 28:0 29:0 30:0 31:0 32:0 33:0 34:0 35:0 36:0 37:0 38:0 +0:1 1:1 2:0 3:1 4:1 5:1 6:0 7:1 8:0 9:1 10:1 11:1 12:0 13:1 14:1 15:1 16:1 17:0 18:1 19:0 20:0 21:1 22:1 23:1 24:0 25:0 26:0 27:0 28:0 29:0 30:0 31:0 32:0 33:0 34:0 35:0 36:0 37:0 38:0 (before setup) +Board fingerprint is 87 (nach power on) +0:1 1:0 2:0 3:1 4:0 5:0 6:0 7:1 8:0 9:1 10:1 11:1 12:0 13:0 14:1 15:0 16:1 17:0 18:0 19:0 20:0 21:1 22:1 23:0 24:0 25:0 26:0 27:0 28:0 29:0 30:0 31:0 32:0 33:0 34:0 35:0 36:0 37:0 38:0 +0:1 1:1 2:0 3:1 4:0 5:0 6:0 7:1 8:0 9:1 10:1 11:1 12:0 13:0 14:1 15:0 16:1 17:0 18:0 19:0 20:0 21:1 22:1 23:0 24:0 25:0 26:0 27:0 28:0 29:0 30:0 31:0 32:0 33:0 34:0 35:0 36:0 37:0 38:0 (before setup) +Board fingerprint is 22 (nach reset) +Autoconfig: looks like T-Beam 1.0 board + + vs 0010111 T-Beam 1.1 seems to be different: 1110111 GPIO4 = 1 (additional pullup) => +64