diff --git a/include/dsd.h b/include/dsd.h index 33f0bf0..3190cb0 100644 --- a/include/dsd.h +++ b/include/dsd.h @@ -654,11 +654,18 @@ typedef struct uint8_t nxdn_sacch_frame_segcrc[4]; uint8_t nxdn_alias_block_number; char nxdn_alias_block_segment[4][4][8]; + //site/srv/cch info char nxdn_location_category[14]; uint32_t nxdn_location_sys_code; uint16_t nxdn_location_site_code; + //channel access information + uint8_t nxdn_rcn; + uint8_t nxdn_base_freq; + uint8_t nxdn_step; + uint8_t nxdn_bw; + //multi-key array unsigned long long int rkey_array[0xFFFF]; diff --git a/src/dsd_main.c b/src/dsd_main.c index 7d2d4b6..53ee6c8 100644 --- a/src/dsd_main.c +++ b/src/dsd_main.c @@ -234,6 +234,12 @@ noCarrier (dsd_opts * opts, dsd_state * state) state->nxdn_location_sys_code = 0; sprintf (state->nxdn_location_category, "%s", " "); + //channel access information + state->nxdn_rcn = 0; + state->nxdn_base_freq = 0; + state->nxdn_step = 0; + state->nxdn_bw = 0; + //dmr mfid branding and site parms sprintf(state->dmr_branding_sub, "%s", ""); sprintf(state->dmr_branding, "%s", ""); @@ -881,11 +887,18 @@ initState (dsd_state * state) memset (state->nxdn_sacch_frame_segment, 1, sizeof(state->nxdn_sacch_frame_segment)); state->nxdn_alias_block_number = 0; memset (state->nxdn_alias_block_segment, 0, sizeof(state->nxdn_alias_block_segment)); + //site/srv/cch info state->nxdn_location_site_code = 0; state->nxdn_location_sys_code = 0; sprintf (state->nxdn_location_category, "%s", " "); + //channel access information + state->nxdn_rcn = 0; + state->nxdn_base_freq = 0; + state->nxdn_step = 0; + state->nxdn_bw = 0; + //multi-key array memset (state->rkey_array, 0, sizeof(state->rkey_array)); diff --git a/src/dsd_ncurses.c b/src/dsd_ncurses.c index 0da3105..1f2c813 100644 --- a/src/dsd_ncurses.c +++ b/src/dsd_ncurses.c @@ -2290,6 +2290,9 @@ ncursesPrinter (dsd_opts * opts, dsd_state * state) printw ("Site Code [%d] ", state->nxdn_location_site_code); } + //if system supports Direct Frequency Assignment + if (state->nxdn_rcn == 1) printw ("DFA "); + printw ("\n"); printw ("| "); printw ("TGT: [%5d] ", tgn); diff --git a/src/nxdn_element.c b/src/nxdn_element.c index c95f121..efef69c 100644 --- a/src/nxdn_element.c +++ b/src/nxdn_element.c @@ -197,6 +197,153 @@ void NXDN_Elements_Content_decode(dsd_opts * opts, dsd_state * state, } /* End NXDN_Elements_Content_decode() */ +//externalize multiple sub-element handlers +void nxdn_location_id_handler (dsd_state * state, uint32_t location_id) +{ + //6.5.2 Location ID + uint8_t category_bit = location_id >> 22; + uint32_t sys_code = 0; + uint16_t site_code = 0; + + char category[14]; //G, R, or L + + if (category_bit == 0) + { + sys_code = ( (location_id & 0x3FFFFF) >> 12); //10 bits + site_code = location_id & 0x3FF; //12 bits + sprintf (category, "%s", "Global"); + } + else if (category_bit == 2) + { + sys_code = ( (location_id & 0x3FFFFF) >> 8); //14 bits + site_code = location_id & 0xFF; //8 bits + sprintf (category, "%s", "Regional"); + } + else if (category_bit == 1) + { + sys_code = ( (location_id & 0x3FFFFF) >> 5); //17 bits + site_code = location_id & 0x1F; //5 bits + sprintf (category, "%s", "Local"); + } + else + { + //err, or we shouldn't ever get here + sprintf (category, "%s", "Reserved/Err"); + } + + //not entirely convinced that site code and ran are the exact same thing due to bit len differences + //site code is meant to be a color code (but this could also be like the P25 p1 nac and p2 cc being the same) + state->nxdn_last_ran = site_code; //this one may drop on nocarrier + if (site_code != 0) state->nxdn_location_site_code = site_code; //this one won't + if (sys_code != 0) state->nxdn_location_sys_code = sys_code; + sprintf (state->nxdn_location_category, "%s", category); + + fprintf (stderr, "\n Location Information - Cat: %s - Sys Code: %d - Site Code %d ", category, sys_code, site_code); + +} + +void nxdn_srv_info_handler (dsd_state * state, uint16_t svc_info) +{ + //handle the service information elements + //Part 1-A Common Air Interface Ver.2.0 + //6.5.33. Service Information + fprintf (stderr, "\n Services:"); + //check each SIF 1-bit element + if (svc_info & 0x8000) fprintf (stderr, " Multi-Site;"); + if (svc_info & 0x4000) fprintf (stderr, " Multi-System;"); + if (svc_info & 0x2000) fprintf (stderr, " Location Registration;"); + if (svc_info & 0x1000) fprintf (stderr, " Group Registration;"); + + if (svc_info & 0x800) fprintf (stderr, " Authentication;"); + if (svc_info & 0x400) fprintf (stderr, " Composite Control Channel;"); + if (svc_info & 0x200) fprintf (stderr, " Voice Call;"); + if (svc_info & 0x100) fprintf (stderr, " Data Call;"); + + if (svc_info & 0x80) fprintf (stderr, " Short Data Call;"); + if (svc_info & 0x40) fprintf (stderr, " Status Call & Remote Control;"); + if (svc_info & 0x20) fprintf (stderr, " PSTN Network Connection;"); + if (svc_info & 0x10) fprintf (stderr, " IP Network Connection;"); + + //last 4-bits are spares + +} + +void nxdn_rst_info_handler (dsd_state * state, uint32_t rst_info) +{ + //handle the restriction information elements + //Part 1-A Common Air Interface Ver.2.0 + //6.5.34. Restriction Information + fprintf (stderr, "\n RST -"); + + //Mobile station operation information (Octet 0, Bits 7 to 4) + fprintf (stderr, " MS:"); + if (rst_info & 0x800000) fprintf (stderr, " Access Restriction;"); + else if (rst_info & 0x400000) fprintf (stderr, " Maintenance Restriction;"); + // else fprintf (stderr, " No Restriction;"); + + //Access cycle interval (Octet 0, Bits 3 to 0) + fprintf (stderr, " ACI:"); + uint8_t frames = (rst_info >> 16) & 0xF; + if (frames) fprintf (stderr, " %d Frame Restriction;", frames * 20); + // else fprintf (stderr, " No Restriction;"); + + //Restriction group specification (Octet 1, Bits 7 to 4) + fprintf (stderr, " RGS:"); + uint8_t uid = (rst_info >> 12) & 0x7; //MSB is a spare, so only evaluate 3-bits + fprintf (stderr, " Lower 3 bits of Unit ID = %d %d %d", uid & 1, (uid >> 1) & 1, (uid >> 2) & 1); + + //Restriction Information (Octet 1, Bits 3 to 0) + fprintf (stderr, " RI:"); + if (rst_info & 0x800) fprintf (stderr, " Location Restriction;"); + else if (rst_info & 0x400) fprintf (stderr, " Call Restriction;"); + else if (rst_info & 0x200) fprintf (stderr, " Short Data Restriction;"); + // else fprintf (stderr, " No Restriction;"); + + //Restriction group ratio specification (Octet 2, Bits 7 to 6) + fprintf (stderr, " RT:"); + uint8_t ratio = (rst_info >> 22) & 0x3; + if (ratio == 1) fprintf (stderr, " 50 Restriction;"); + else if (ratio == 2) fprintf (stderr, " 75 Restriction;"); + else if (ratio == 3) fprintf (stderr, " 87.5 Restriction;"); + // else fprintf (stderr, " No Restriction;"); + + //Delay time extension specification (Octet 2, Bits 5 to 4) + fprintf (stderr, " DT:"); + uint8_t dt = (rst_info >> 20) & 0x3; + if (dt == 0) fprintf (stderr, " Timer T2 max x 1;"); + else if (dt == 1) fprintf (stderr, " Timer T2 max x 2;"); + else if (dt == 2) fprintf (stderr, " Timer T2 max x 3;"); + else fprintf (stderr, " Timer T2 max x 4;"); + + //ISO Temporary Isolation Site -- This is valid only if the SIF 1 of Service Information is set to 1. + if (rst_info & 0x0001) fprintf (stderr, " - Site Isolation;"); + + //what a pain... +} + +void nxdn_ca_info_handler (dsd_state * state, uint32_t ca_info) +{ + //handle the channel access info for channel or dfa + //Part 1-A Common Air Interface Ver.2.0 + //6.5.36. Channel Access Information + //this element only seems to appear in the SITE_INFO message + uint32_t RCN = ca_info >> 23; //Radio Channel Notation + uint32_t step = (ca_info >> 21) & 0x3; //Stepping + uint32_t base = (ca_info >> 19) & 0x7; //Base Frequency + uint32_t spare = ca_info & 0x3FF; + + //set state variable here to tell us to use DFA or Channel Versions + if (RCN == 1) + { + state->nxdn_rcn = RCN; + state->nxdn_step = step; + state->nxdn_base_freq = base; + } + +} + +//end sub-element handlers + void NXDN_decode_VCALL_ASSGN(dsd_opts * opts, dsd_state * state, uint8_t * Message) { //just using 'short form' M only data, not the optional data @@ -212,6 +359,11 @@ void NXDN_decode_VCALL_ASSGN(dsd_opts * opts, dsd_state * state, uint8_t * Messa uint8_t DuplexMode[32] = {0}; uint8_t TransmissionMode[32] = {0}; + //DFA specific variables + uint8_t bw = 0; + uint16_t OFN = 0; + uint16_t IFN = 0; + /* Decode "CC Option" */ CCOption = (uint8_t)ConvertBitIntoBytes(&Message[8], 8); state->NxdnElementsContent.CCOption = CCOption; @@ -238,9 +390,14 @@ void NXDN_decode_VCALL_ASSGN(dsd_opts * opts, dsd_state * state, uint8_t * Messa /* Decode "Channel" */ Channel = (uint16_t)ConvertBitIntoBytes(&Message[62], 10); - /* Decode "Location ID Option" */ //is in the optional field, probably best not to run this one - //LocationIDOption = (uint8_t)ConvertBitIntoBytes(&Message[72], 5); - + /* Decode DFA-only variables*/ + if (state->nxdn_rcn == 1) + { + bw = (uint8_t)ConvertBitIntoBytes(&Message[62], 2); + OFN = (uint16_t)ConvertBitIntoBytes(&Message[64], 16); + IFN = (uint16_t)ConvertBitIntoBytes(&Message[80], 16); + } + /* Print the "CC Option" */ if(CCOption & 0x80) fprintf(stderr, "Emergency "); if(CCOption & 0x40) fprintf(stderr, "Visitor "); @@ -257,12 +414,18 @@ void NXDN_decode_VCALL_ASSGN(dsd_opts * opts, dsd_state * state, uint8_t * Messa /* Print Source ID and Destination ID (Talk Group or Unit ID) */ fprintf(stderr, "Src=%u - Dst/TG=%u ", SourceUnitID & 0xFFFF, DestinationID & 0xFFFF); - /* Print Channel (Skipping Call Timer)*/ - fprintf(stderr, "- Channel [%03X][%04d] ", Channel & 0x3FF, Channel & 0x3FF); + /* Print Channel */ + if (state->nxdn_rcn == 0) + fprintf(stderr, "- Channel [%03X][%04d] ", Channel & 0x3FF, Channel & 0x3FF); + if (state->nxdn_rcn == 1) + fprintf(stderr, "- DFA Channel [%04X][%05d] ", OFN, OFN); - //run process to figure out frequency value from the channel, or from lcn import array + //run process to figure out frequency value from the channel import or from DFA long int freq = 0; - freq = nxdn_channel_to_frequency(opts, state, Channel); + if (state->nxdn_rcn == 0) + freq = nxdn_channel_to_frequency(opts, state, Channel); + if (state->nxdn_rcn == 1) + freq = nxdn_channel_to_frequency(opts, state, OFN); //check the rkey array for a scrambler key value //TGT ID and Key ID could clash though if csv or system has both with different keys @@ -288,7 +451,6 @@ void NXDN_decode_VCALL_ASSGN(dsd_opts * opts, dsd_state * state, uint8_t * Messa } } - //run group/source analysis and tune if available/desired //group list mode so we can look and see if we need to block tuning any groups, etc char mode[8]; //allow, block, digital, enc, etc @@ -430,6 +592,7 @@ void NXDN_decode_Alias(dsd_opts * opts, dsd_state * state, uint8_t * Message) void NXDN_decode_cch_info(dsd_opts * opts, dsd_state * state, uint8_t * Message) { + //6.4.3.3. Control Channel Information (CCH_INFO) for more information uint32_t location_id = 0; uint8_t channel1sts = 0; uint16_t channel1 = 0; @@ -437,6 +600,14 @@ void NXDN_decode_cch_info(dsd_opts * opts, dsd_state * state, uint8_t * Message) uint16_t channel2 = 0; long int freq1 = 0; long int freq2 = 0; + + //DFA + uint8_t bw1 = 0; + uint16_t OFN1 = 0; + uint16_t IFN1 = 0; + uint8_t bw2 = 0; + uint16_t OFN2 = 0; + uint16_t IFN2 = 0; location_id = (uint32_t)ConvertBitIntoBytes(&Message[8], 24); channel1sts = (uint8_t)ConvertBitIntoBytes(&Message[32], 6); @@ -446,24 +617,43 @@ void NXDN_decode_cch_info(dsd_opts * opts, dsd_state * state, uint8_t * Message) fprintf (stderr, "%s", KYEL); nxdn_location_id_handler (state, location_id); + fprintf (stderr, "\n Control Channel Information \n"); - fprintf (stderr, " Location ID [%06X] CC1 [%03X][%04d] CC2 [%03X][%04d] Status: ", location_id, channel1, channel1, channel2, channel2); - //check the sts bits to determine if current, new, add, or delete - if (channel1sts & 0x20) fprintf (stderr, "Current "); - else if (channel1sts & 0x10) fprintf (stderr, "New "); - else if (channel1sts & 0x8) fprintf (stderr, "Candidate Added "); - else if (channel1sts & 0x4) fprintf (stderr, "Candidate Deleted "); - freq1 = nxdn_channel_to_frequency (opts, state, channel1); - freq2 = nxdn_channel_to_frequency (opts, state, channel2); + //non-DFA version + if (state->nxdn_rcn == 0) + { + fprintf (stderr, " Location ID [%06X] CC1 [%03X][%04d] CC2 [%03X][%04d] Status: ", location_id, channel1, channel1, channel2, channel2); + //check the sts bits to determine if current, new, add, or delete + if (channel1sts & 0x20) fprintf (stderr, "Current "); + if (channel1sts & 0x10) fprintf (stderr, "New "); + if (channel1sts & 0x08) fprintf (stderr, "Candidate Added "); + if (channel1sts & 0x04) fprintf (stderr, "Candidate Deleted "); + freq1 = nxdn_channel_to_frequency (opts, state, channel1); + freq2 = nxdn_channel_to_frequency (opts, state, channel2); + } + + //DFA version + if (state->nxdn_rcn == 1) + { + bw1 = (uint8_t)ConvertBitIntoBytes(&Message[38], 2); + OFN1 = (uint16_t)ConvertBitIntoBytes(&Message[40], 16); + IFN1 = (uint16_t)ConvertBitIntoBytes(&Message[56], 16); - //removed code below, may have been causing issues with current CC frequency assignment - //removed code is unneccesary for trunking purposes + //facch1 will not have the below items + // bw2 = (uint8_t)ConvertBitIntoBytes(&Message[78], 2); + // OFN2 = (uint16_t)ConvertBitIntoBytes(&Message[80], 16); + // IFN2 = (uint16_t)ConvertBitIntoBytes(&Message[96], 16); + if (channel1sts & 0x10) fprintf (stderr, "New "); + if (channel1sts & 0x02) fprintf (stderr, "Current1 "); + if (channel1sts & 0x01) fprintf (stderr, "Current2 "); + freq1 = nxdn_channel_to_frequency (opts, state, OFN1); - //if a delete, let's not bother trying to remove it from the list for now - SKIP: ; //do nothing - //channel 2 should only be valid on 'current' status, but not sure what value it would contain vs channel 1 + //facch1 does not carry this value, so let's not look at it + // freq2 = nxdn_channel_to_frequency (opts, state, OFN2); + } + fprintf (stderr, "%s", KNRM); } @@ -481,6 +671,13 @@ void NXDN_decode_srv_info(dsd_opts * opts, dsd_state * state, uint8_t * Message) fprintf (stderr, "\n Service Information - "); fprintf (stderr, "Location ID [%06X] SVC [%04X] RST [%06X] ", location_id, svc_info, rst_info); nxdn_location_id_handler (state, location_id); + + //run the srv info + nxdn_srv_info_handler (state, svc_info); + + //run the rst info, if not zero + if (rst_info) nxdn_rst_info_handler (state, rst_info); + fprintf (stderr, "%s", KNRM); //poll for current frequency, will always be the control channel @@ -504,51 +701,6 @@ void NXDN_decode_srv_info(dsd_opts * opts, dsd_state * state, uint8_t * Message) } -//externalize the location handler to its own function -void nxdn_location_id_handler (dsd_state * state, uint32_t location_id) -{ - //6.5.2 Location ID - uint8_t category_bit = location_id >> 22; - uint32_t sys_code = 0; - uint16_t site_code = 0; - - char category[14]; //G, R, or L - - if (category_bit == 0) - { - sys_code = ( (location_id & 0x3FFFFF) >> 12); //10 bits - site_code = location_id & 0x3FF; //12 bits - sprintf (category, "%s", "Global"); - } - else if (category_bit == 2) - { - sys_code = ( (location_id & 0x3FFFFF) >> 8); //14 bits - site_code = location_id & 0xFF; //8 bits - sprintf (category, "%s", "Regional"); - } - else if (category_bit == 1) - { - sys_code = ( (location_id & 0x3FFFFF) >> 5); //17 bits - site_code = location_id & 0x1F; //5 bits - sprintf (category, "%s", "Local"); - } - else - { - //err, or we shouldn't ever get here - sprintf (category, "%s", "Reserved/Err"); - } - - //not entirely convinced that site code and ran are the exact same thing due to bit len differences - //site code is meant to be a color code (but this could also be like the P25 p1 nac and p2 cc being the same) - state->nxdn_last_ran = site_code; //this one may drop on nocarrier - if (site_code != 0) state->nxdn_location_site_code = site_code; //this one won't - if (sys_code != 0) state->nxdn_location_sys_code = sys_code; - sprintf (state->nxdn_location_category, "%s", category); - - fprintf (stderr, "\n Location Information - Cat: %s - Sys Code: %d - Site Code %d ", category, sys_code, site_code); - -} - void NXDN_decode_site_info(dsd_opts * opts, dsd_state * state, uint8_t * Message) { uint32_t location_id = 0; @@ -574,28 +726,41 @@ void NXDN_decode_site_info(dsd_opts * opts, dsd_state * state, uint8_t * Message channel1 = (uint16_t)ConvertBitIntoBytes(&Message[124], 10); channel2 = (uint16_t)ConvertBitIntoBytes(&Message[134], 10); + //check the channel access information first + nxdn_ca_info_handler (state, ca_info); + fprintf (stderr, "%s", KYEL); fprintf (stderr, "\n Location ID [%06X] CSC [%04X] SVC [%04X] RST [%06X] \n CA [%06X] V[%X] ADJ [%01X] ", location_id, cs_info, svc_info, rst_info, ca_info, version_num, adj_alloc); nxdn_location_id_handler(state, location_id); - if (channel1 != 0) + + //run the srv info + nxdn_srv_info_handler (state, svc_info); + + //run the rst info, if not zero + if (rst_info) nxdn_rst_info_handler (state, rst_info); + + //only get frequencies if using channel version of message and not dfa + if (state->nxdn_rcn == 0) { - fprintf (stderr, "\n Control Channel 1 [%03X][%04d] ", channel1, channel1 ); - freq1 = nxdn_channel_to_frequency (opts, state, channel1); + if (channel1 != 0) + { + fprintf (stderr, "\n Control Channel 1 [%03X][%04d] ", channel1, channel1 ); + freq1 = nxdn_channel_to_frequency (opts, state, channel1); + } + if (channel2 != 0) + { + fprintf (stderr, "\n Control Channel 2 [%03X][%04d] ", channel2, channel2 ); + freq2 = nxdn_channel_to_frequency (opts, state, channel2); + } } - if (channel2 != 0) + else { - fprintf (stderr, "\n Control Channel 2 [%03X][%04d] ", channel2, channel2 ); - freq2 = nxdn_channel_to_frequency (opts, state, channel2); + ; //DFA version does not carry an OFN/IFN value, so no freqs } fprintf (stderr, "%s", KNRM); - //removed code below, may have been causing issues with current CC frequency assignment - //removed code is unneccesary for trunking purposes - - - SKIP: ; //do nothing } diff --git a/src/p25_frequency.c b/src/p25_frequency.c index 93227bc..e08b505 100644 --- a/src/p25_frequency.c +++ b/src/p25_frequency.c @@ -2,7 +2,7 @@ * p25_frequency.c * P25 Channel to Frequency Calculator * - * NXDN Channel to Frequency, Courtesy of IcomIcR20 and EricCottrell (base values) + * NXDN Channel to Frequency Calculator * * LWVMOBILE * 2022-11 DSD-FME Florida Man Edition @@ -59,16 +59,16 @@ long int process_channel_to_freq (dsd_opts * opts, dsd_state * state, int channe } -//NXDN Channel to Frequency, Courtesy of IcomIcR20 on RR Forums long int nxdn_channel_to_frequency(dsd_opts * opts, dsd_state * state, uint16_t channel) { long int freq; long int base = 0; + long int step = 0; - //the base freq value is per EricCottrell in the Understanding NXDN Trunking Forum Thread - //will need to do some research or tests to confirm these values are accurate + //reworked to include Direct Frequency Assignment if available - //first, check channel map + //first, check channel map for imported value, DFA systems most likely won't need an import, + //unless it has 'system definable' attributes if (state->trunk_chan_map[channel] != 0) { freq = state->trunk_chan_map[channel]; @@ -76,42 +76,41 @@ long int nxdn_channel_to_frequency(dsd_opts * opts, dsd_state * state, uint16_t return (freq); } + //then, let's see if its DFA instead -- 6.5.36 + else if (state->nxdn_rcn == 1) + { + //determine the base frequency in Hz + if (state->nxdn_base_freq == 1) base = 100000000; //100 MHz + else if (state->nxdn_base_freq == 2) base = 330000000; //330 Mhz + else if (state->nxdn_base_freq == 3) base = 400000000; //400 Mhz + else if (state->nxdn_base_freq == 4) base = 750000000; //750 Mhz + else base = 0; //just set to zero, will be system definable most likely and won't be able to calc + + //determine the step value in Hz + if (state->nxdn_step = 2) step = 1250; //1.25 kHz + else if (state->nxdn_step = 3) step = 3125; //3.125 kHz + else step = 0; //just set to zero, will be system definable most likely and won't be able to calc + + //if we have a valid base and step, then calc frequency + //6.5.45. Outbound/Inbound Frequency Number (OFN/IFN) + if (base && step) + { + freq = base + (channel * step); + fprintf (stderr, "\n DFA Frequency [%.6lf] MHz", (double)freq/1000000); + return (freq); + } + else + { + fprintf(stderr, "\n Custom DFA Settings -- Unknown Freq;"); + return (0); + } + + } + else { fprintf(stderr, "\n Channel not found in import file"); return (0); } - //if not found, attempt to find it via calculation - //disabled, frequency 'band plan' isn't standard - //like originally believed - - // else - // { - // if ((channel > 0) && (channel <= 400)) - // { - // if (opts->frame_nxdn48 == 1) base = 450000000; - // else base = 451000000; - - // freq = base + (channel - 1) * 12500; - // fprintf (stderr, "\n Frequency [%.6lf] MHz", (double)freq/1000000); - // return (freq); - // } - // else if ((channel >= 401) && (channel <= 800)) - // { - // if (opts->frame_nxdn48 == 1) base = 460000000; - // else base = 461000000; - - // freq = base + (channel - 401) * 12500; - // fprintf (stderr, "\n Frequency [%.6lf] MHz", (double)freq/1000000); - // return (freq); - // } - // else - // { - // fprintf(stderr, "\n Non-standard frequency or channel not found in import file"); - // return (0); - // } - // } - - } \ No newline at end of file