/* Copyright 2001-2018 John Wiseman G8BPQ This file is part of LinBPQ/BPQ32. LinBPQ/BPQ32 is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. LinBPQ/BPQ32 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with LinBPQ/BPQ32. If not, see http://www.gnu.org/licenses */ // Module for writing ADIF compatible log records #define WIN32_LEAN_AND_MEAN #define _CRT_SECURE_NO_WARNINGS #define _CRT_SECURE_NO_DEPRECATE #include #include #include "time.h" #include "CHeaders.h" #include "tncinfo.h" #include "adif.h" #include "telnetserver.h" /* Typical File Header Written by RMS Trimode ver: 1.3.21.0 2.2.6 Example TriMode Record VA2ROR 20190201 140801 140958 JG28DK 20M 14.109000 0|2019-02-01 14:09:58|RMS Trimode|1.3.21.0|CMS|ZS1RS|VA2ROR|[AirMail-3.5.036-B2FHIM$] |Pactor 3|14109000|1957|311|FQ|1|0|4098|93|117|JG28DK|JF96HD Info Used by RMS Analyser, Logged as an adif field 0||||||||||||||||| in the format YYYY-MM-DD HH:MM:SS and always in UTC. is the name of the program posting this record. is the version number of the program. is the city name assigned to the CMS. is the radio callsign used by the RMS server including any SSID. is the radio callsign used by the user including any SSID. is the SID used by the user's client program. is the air protocol used such as "Pactor 3" or "Packet 1200". At the present time modes used or expected include Pactor 1, Pactor 2, Pactor 3, Pactor 4, Packet 1200, Packet 9600, Winmor 500, Winmor 1600, and Robust Packet. is the frequency used in Hertz. if the distance to the user's site (if known). is the bearing to the user's site (if known). is the last command sent or received. Normally there will be "FQ" but could be "FF", "FC" "PR", "PQ", "FS", "F>", or ">" or one of the keyboard commands. It should limited to one or two characters. through are integers and may be 0 but not empty. An example: 0|2011-05-20 12:34.56|RMS Packet|1.2.3.4|San Diego|KN6KB-10|W1ABC| [Paclink-4.2.0.0-N00B2FIHM$]| Packet 1200|145090000|45|244|FQ| 1|0|2454|120|135 */ VOID FromLOC(char * Locator, double * pLat, double * pLon); double Distance(double laa, double loa, double lah, double loh, BOOL KM); double Bearing(double lat2, double lon2, double lat1, double lon1); extern UCHAR LogDirectory[260]; extern char LOC[7]; extern char TextVerstring[50]; struct WL2KInfo * WL2KReports; extern char WL2KModes[55][18]; BOOL ADIFLogEnabled = FALSE; char ADIFLogName[80] = "ADIF.adi"; char * stristr (char *ch1, char *ch2); VOID CountMessages(ADIF * ADIF) { while (ADIF->FBBIndex) { // We have messages if (ADIF->FBBLen[ADIF->FBBIndex - 1]) // don't count rejected messages { if (ADIF->Dirn == 'S') { ADIF->Sent++; ADIF->BytesSent += ADIF->FBBLen[ADIF->FBBIndex - 1]; } else { ADIF->Received++; ADIF->BytesReceived += ADIF->FBBLen[ADIF->FBBIndex - 1]; } } ADIF->FBBIndex--; } } BOOL UpdateADIFRecord(ADIF * ADIF, char * Msg, char Dirn) { // Entered each time a text message is received on CMS session // To get transfer stats we have to monitor the B2 protocol messages, tracking proposals, accept/reject and completion size_t Len; // Always keep info so we can sent as Winlink Session Record // even if ADIF logging is disabled. // if (ADIFLogEnabled == FALSE) // return TRUE; if (ADIF == NULL) return TRUE; if (ADIF->StartTime == 0) { ADIF->StartTime = time(NULL); // ADIF->Mode = 49; // Unused value (unforunately 0 = PKT1200) } // Try to build complete lines if (Dirn == 'R') { Len = strlen(&ADIF->PartMessageRX[0]); if (Len + strlen(Msg) < 512) strcat(ADIF->PartMessageRX, Msg); if (strstr(Msg, "")) Msg = ADIF->PartMessageRX; else return TRUE; } else { Len = strlen(&ADIF->PartMessageTX[0]); if (Len + strlen(Msg) < 256) strcat(ADIF->PartMessageTX, Msg); if (strstr(Msg, "")) Msg = ADIF->PartMessageTX; else return TRUE; } switch (Dirn) { case 'R': if (Msg[0] == '[') { if (ADIF->ServerSID[0] == 0) { char * endsid = strchr(Msg, ']'); if (endsid && (endsid - Msg) < 78) memcpy(ADIF->ServerSID, Msg, ++endsid - Msg); } Msg[0] = 0; return TRUE; } break; case 'S': if (Msg[0] == '[') { if (ADIF->UserSID[0] == 0) { char * endsid = strchr(Msg, ']'); if (endsid && (endsid - Msg) < 78) memcpy(ADIF->UserSID, Msg, ++endsid - Msg); } Msg[0] = 0; return TRUE; } if (ADIF->LOC[0] == 0 && Msg[0] == ';' && stristr(Msg, "DE ")) { // Look for ; GM8BPQ-10 DE G8BPQ (IO92KX) // AirMail EA8URF de KG5VSG (GK86qo) QTC: 1 209 // Paclink-Unix Sends // ; VE7SPR-10 DE N7NIX QTC 1 char * StartLoc = strchr(Msg, '('); char * EndLoc = strchr(Msg, ')'); if (StartLoc == NULL || EndLoc == NULL) return TRUE; if ((EndLoc - StartLoc) < 10) memcpy(ADIF->LOC, StartLoc + 1, (EndLoc - StartLoc) - 1); Msg[0] = 0; return TRUE; } } // Look for Proposals // FC EM 3909_GM8BPQ 3520 1266 0 if (memcmp(Msg, "FC EM ", 6) == 0) { char * ptr, *Context; // We need to detect first of a block of proposals so we can count any acked messages if (ADIF->GotFC == 0) // Last was not FC CountMessages(ADIF); // This acks last batch ADIF->GotFC = 1; ptr = strtok_s(&Msg[6], " \r", &Context); // BID if (ptr) { ptr = strtok_s(NULL, " \r", &Context); if (ptr) { ADIF->FBBLen[ADIF->FBBIndex] = atoi(ptr); // Not really sure we need lengths, but no harm if (ADIF->FBBIndex++ == 5) ADIF->FBBIndex = 4; // Proect ourselves } ADIF->Dirn = Dirn; } strcpy(ADIF->Termination, "FC"); Msg[0] = 0; return TRUE; } if (memcmp(Msg, "FS ", 3) == 0) { // As we only count sent messages must check for rejections; // FS YYY int i = 0; char c; ADIF->GotFC = 0; // Ready for next batch while (i < ADIF->FBBIndex) { c = Msg[i + 3]; if ((c == '-') || (c == 'N') || (c == 'R') || (c == 'E')) // Not wanted { ADIF->FBBLen[i] = 0; // Clear corresponding length } i++; } strcpy(ADIF->Termination, "FS"); Msg[0] = 0; return TRUE; } if (strcmp(Msg, "FF") == 0 || strcmp(Msg, "FQ") == 0) { // Need to count any complete messages CountMessages(ADIF); // This acks last batch memcpy(ADIF->Termination, Msg, 2); Msg[0] = 0; return TRUE; } if (Msg[0] == ';' && Msg[3] == ':' ) memcpy(ADIF->Termination, &Msg[1],2); Msg[0] = 0; return TRUE; } typedef struct BandLimits { char Name[10]; double lower; double upper; } BandLimits; BandLimits Bands[] = { {"2190m", .1357, .1378}, {"630m", .472, .479}, {"560m", .501, .504}, {"160m", 1.8, 2.0}, {"80m", 3.5, 4.0}, {"60m", 5.06, 5.45}, {"40m", 7.0, 7.3}, {"30m", 10.1, 10.15}, {"20m", 14.0, 14.35}, {"17m", 18.068, 18.168}, {"15m", 21.0, 21.45}, {"12m", 24.890, 24.99}, {"10m", 28.0, 29.7}, {"6m", 50, 54}, {"4m", 70, 72}, {"2m", 144, 148}, {"1.25m", 222, 225}, {"70cm", 420, 450}, {"33cm", 902, 928}, {"23cm", 1240, 1300}, {"13cm", 2300, 2450}, {"9cm", 3300, 3500}, {"6cm", 5650, 5925}, {"3cm", 10000, 10500}, {"6mm", 47000, 47200}, {"4mm", 75500, 81000}, {"2.5mm", 119980, 120020}, {"2mm", 142000, 149000}, {"1mm", 241000, 250000}, {"unknown", 0, 9999999999} }; int FreqCount = sizeof(Bands)/sizeof(struct BandLimits); char ADIFModes [55][18] = { "PKT", "PKT", "PKT", "PKT", "PKT", "PKT", "PKT", "", "", "", // 0 - 9 "", "PAC", "", "", "PAC/PAC2", "", "PAC/PAC3", "", "", "", "PAC/PAK4", // 10 - 20 "WINMOR", "WINMOR", "", "", "", "", "", "", "", // 21 - 29 "Robust Packet", "", "", "", "", "", "", "", "", "", // 30 - 39 "ARDOP", "ARDOP", "ARDOP", "ARDOP", "ARDOP", "", "", "", "", "", // 40 - 49 "VARA", "VARAFM", "VARAFM96", "VARA500", "VARA2750"}; BOOL WriteADIFRecord(ADIF * ADIF) { UCHAR Value[MAX_PATH]; time_t T; struct tm * tm; struct tm * starttm; struct tm endtm; FILE * Handle; char Comment[256] = ""; int CommentLen; char Date[32]; int Dist = 0; int intBearing = 0; struct stat STAT; double Lat, Lon; double myLat, myLon; if (ADIFLogEnabled == FALSE) return TRUE; if (ADIF == NULL) return TRUE; T = time(NULL); tm = gmtime(&T); memcpy(&endtm, tm, sizeof(endtm)); if (LogDirectory[0] == 0) { strcpy(Value, "logs/BPQ_CMS_ADIF"); } else { strcpy(Value, LogDirectory); strcat(Value, "/"); strcat(Value, "logs/BPQ_CMS_ADIF"); } sprintf(Value, "%s_%04d%02d.adi", Value, tm->tm_year +1900, tm->tm_mon+1); STAT.st_size = 0; stat(Value, &STAT); Handle = fopen(Value, "ab"); if (Handle == NULL) return FALSE; if (STAT.st_size == 0) { // New File - Write Header char Header[256]; int Len; Len = sprintf(Header, "Written by BPQ32 ver: %s 2.2.6\r\n", TextVerstring); fwrite(Header, 1, Len, Handle); } // Extract Info we need // Distance and Bearing if (LOC[0] && ADIF->LOC[0]) { FromLOC(LOC, &myLat, &myLon); FromLOC(ADIF->LOC, &Lat, &Lon); Dist = (int)Distance(myLat, myLon, Lat, Lon, 0); intBearing = (int)Bearing(Lat, Lon, myLat, myLon); } starttm = gmtime(&ADIF->StartTime); //VA2ROR fprintf(Handle, "%s", (int)strlen(ADIF->Call), ADIF->Call); //20190201 //140801 //140958 fprintf(Handle, "%04d%02d%02d%02d%02d%02d%02d%02d%02d", starttm->tm_year + 1900, starttm->tm_mon + 1, starttm->tm_mday, starttm->tm_hour, starttm->tm_min, starttm->tm_sec, endtm.tm_hour, endtm.tm_min, endtm.tm_sec); // if (ADIFModes[ADIF->Mode][0]) { // Send Mode, and Submode if present char Mode[32]; char * SubMode; strcpy(Mode, ADIFModes[ADIF->Mode]); SubMode = strlop(Mode, '/'); fprintf(Handle, "%s", (int)strlen(Mode), Mode); if (SubMode) fprintf(Handle, "%s", (int)strlen(SubMode), SubMode); } //JG28DK. Doc wants it even if empty fprintf(Handle, "%s", (int)strlen(ADIF->LOC), ADIF->LOC); //20M //14.109000 if (ADIF->Freq > 1500) { char Freqstr[32]; int i = 0; BandLimits * Band = &Bands[0]; double Freq = ADIF->Freq / 1000000.0; while (i++ < FreqCount) { if (Band->lower <= Freq && Band->upper >= Freq) break; Band++; } sprintf(Freqstr, "%.6f", ADIF->Freq / 1000000.0); fprintf(Handle, "%s", (int)strlen(Band->Name), Band->Name); fprintf(Handle, "%s", (int)strlen(Freqstr), Freqstr); } else fprintf(Handle, ""); // Do Comment //0|2019-02-01 14:09:58|RMS Trimode|1.3.21.0|CMS|ZS1RS|VA2ROR|[AirMail-3.5.036-B2FHIM$] //|Pactor 3|14109000|1957|311|FQ|1|0|4098|93|117|JG28DK|JF96HD sprintf (Date, "%04d-%02d-%02d %02d:%02d:%02d", endtm.tm_year + 1900, endtm.tm_mon + 1, endtm.tm_mday, endtm.tm_hour, endtm.tm_min, endtm.tm_sec); CommentLen = sprintf(Comment, "0|%s|%s|%s|%s|%s|%s|%s|%s|%lld|%d|%d|%s|%d|%d|%d|%d|%d|%s|%s", Date, "BPQ32", TextVerstring, "CMS", ADIF->CMSCall, ADIF->Call, ADIF->UserSID, WL2KModes[ADIF->Mode], ADIF->Freq, Dist, intBearing, ADIF->Termination, ADIF->Sent, ADIF->Received, ADIF->BytesSent, ADIF->BytesReceived, (int)(T - ADIF->StartTime), ADIF->LOC, LOC); fprintf(Handle, "%s", CommentLen, Comment); fprintf(Handle, "\r\n"); fclose(Handle); return TRUE; } VOID ADIFWriteFreqList() { // Write info needed for RMS Analyser to a file in similar format to Trimode UCHAR Value[MAX_PATH]; FILE * Handle; struct WL2KInfo * WL2KReport = WL2KReports; char Locator[16]; char Call[16]; int i, freqCount = 0; long long Freqs[100] = {0}; if (WL2KReport == NULL) return; if (LogDirectory[0] == 0) { strcpy(Value, "logs/BPQAnalyser.ini"); } else { strcpy(Value, LogDirectory); strcat(Value, "/"); strcat(Value, "logs/BPQAnalyser.ini"); } Handle = fopen(Value, "wb"); if (Handle == NULL) return; while (WL2KReport) { strcpy(Call, WL2KReport->BaseCall); strcpy(Locator, WL2KReport->GridSquare); // if freq not in list add it i = 0; while (i < freqCount) { if (WL2KReport->Freq == Freqs[i]) break; i++; } if (i == freqCount) Freqs[freqCount++] = WL2KReport->Freq; WL2KReport = WL2KReport->Next; } fprintf(Handle, "[Site Properties]\r\n"); fprintf(Handle, "Grid Square=%s\r\n", Locator); fprintf(Handle, "[Registration]\r\n"); fprintf(Handle, "Base Callsign=%s\r\n", Call); fprintf(Handle, "[Channels]\r\n"); for (i = 0; i < freqCount; i++) fprintf(Handle, "Frequency %d=%lld\r\n" , i + 1, Freqs[i]); fclose(Handle); }