2023-07-08 20:04:12 +00:00
/*
* Copyright ( C ) 2014 Jared Boone , ShareBrained Technology , Inc .
* Copyright ( C ) 2016 Furrtek
* Copyright ( C ) 2023 gullradriel , Nilorea Studio Inc .
* Copyright ( C ) 2023 Kyle Reed
*
* This file is part of PortaPack .
*
* This program 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 2 , or ( at your option )
* any later version .
*
* This program 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 this program ; see the file COPYING . If not , write to
* the Free Software Foundation , Inc . , 51 Franklin Street ,
* Boston , MA 02110 - 1301 , USA .
*/
# include "convert.hpp"
# include "file.hpp"
# include "file_reader.hpp"
# include "freqman_db.hpp"
# include "string_format.hpp"
# include "tone_key.hpp"
2023-07-11 20:48:36 +00:00
# include "utility.hpp"
2023-07-08 20:04:12 +00:00
# include <array>
# include <cctype>
# include <string_view>
# include <vector>
namespace fs = std : : filesystem ;
2023-07-11 20:48:36 +00:00
const std : : filesystem : : path freqman_dir { u " /FREQMAN " } ;
const std : : filesystem : : path freqman_extension { u " .TXT " } ;
// NB: Don't include UI headers to keep this code unit testable.
2023-07-08 20:04:12 +00:00
using option_t = std : : pair < std : : string , int32_t > ;
using options_t = std : : vector < option_t > ;
options_t freqman_modulations = {
{ " AM " , 0 } ,
{ " NFM " , 1 } ,
{ " WFM " , 2 } ,
{ " SPEC " , 3 } ,
} ;
options_t freqman_bandwidths [ 4 ] = {
{
// AM
{ " DSB 9k " , 0 } ,
{ " DSB 6k " , 1 } ,
{ " USB+3k " , 2 } ,
{ " LSB-3k " , 3 } ,
{ " CW " , 4 } ,
} ,
{
// NFM
{ " 8k5 " , 0 } ,
{ " 11k " , 1 } ,
{ " 16k " , 2 } ,
} ,
{
// WFM
{ " 40k " , 2 } ,
{ " 180k " , 1 } ,
{ " 200k " , 0 } ,
} ,
{
// SPEC -- TODO: these should be indexes.
2023-07-31 15:46:07 +00:00
{ " 12k5 " , 12500 } ,
2023-07-08 20:04:12 +00:00
{ " 16k " , 16000 } ,
{ " 25k " , 25000 } ,
{ " 50k " , 50000 } ,
{ " 100k " , 100000 } ,
2023-08-12 14:20:15 +00:00
{ " 150k " , 150000 } ,
2023-07-08 20:04:12 +00:00
{ " 250k " , 250000 } ,
{ " 500k " , 500000 } , /* Previous Limit bandwith Option with perfect micro SD write .C16 format operaton.*/
2023-08-06 19:54:24 +00:00
{ " 600k " , 600000 } , /* We doubled x2 previous REC BW limit , now extended BW from 600k to 1M with fast enough SD card in C16 or C8 format .*/
2023-07-08 20:04:12 +00:00
{ " 650k " , 650000 } ,
2023-08-06 19:54:24 +00:00
{ " 750k " , 750000 } ,
{ " 1000k " , 1000000 } , /* New limit bandwith option for recording in C16 (in fast SD card) or in C8 */
{ " 1500k " , 1500000 } , /* From this BW onwards, the LCD is ok, but M4 CPU is having periodical sample rec dropps, (not real file size, accelerated replay) */
2023-07-08 20:04:12 +00:00
{ " 1750k " , 1750000 } ,
{ " 2000k " , 2000000 } ,
{ " 2500k " , 2500000 } ,
{ " 2750k " , 2750000 } , // That is our max Capture option, to keep using later / 8 decimation (22Mhz sampling ADC)
} ,
} ;
2023-07-15 01:46:39 +00:00
// TODO: these should be indexes.
2023-07-08 20:04:12 +00:00
options_t freqman_steps = {
{ " 0.1kHz " , 100 } ,
{ " 1kHz " , 1000 } ,
{ " 5kHz (SA AM) " , 5000 } ,
{ " 6.25kHz(NFM) " , 6250 } ,
{ " 8.33kHz(AIR) " , 8330 } ,
{ " 9kHz (EU AM) " , 9000 } ,
{ " 10kHz(US AM) " , 10000 } ,
{ " 12.5kHz(NFM) " , 12500 } ,
{ " 15kHz (HFM) " , 15000 } ,
{ " 25kHz (N1) " , 25000 } ,
{ " 30kHz (OIRT) " , 30000 } ,
{ " 50kHz (FM1) " , 50000 } ,
{ " 100kHz (FM2) " , 100000 } ,
{ " 250kHz (N2) " , 250000 } ,
{ " 500kHz (WFM) " , 500000 } ,
{ " 1MHz " , 1000000 } ,
} ;
2023-07-15 01:46:39 +00:00
// TODO: these should be indexes.
2023-07-08 20:04:12 +00:00
options_t freqman_steps_short = {
{ " 0.1kHz " , 100 } ,
{ " 1kHz " , 1000 } ,
{ " 5kHz " , 5000 } ,
{ " 6.25kHz " , 6250 } ,
{ " 8.33kHz " , 8330 } ,
{ " 9kHz " , 9000 } ,
{ " 10kHz " , 10000 } ,
{ " 12.5kHz " , 12500 } ,
{ " 15kHz " , 15000 } ,
{ " 25kHz " , 25000 } ,
{ " 30kHz " , 30000 } ,
{ " 50kHz " , 50000 } ,
{ " 100kHz " , 100000 } ,
{ " 250kHz " , 250000 } ,
{ " 500kHz " , 500000 } ,
{ " 1MHz " , 1000000 } ,
} ;
uint8_t find_by_name ( const options_t & options , std : : string_view name ) {
for ( auto ix = 0u ; ix < options . size ( ) ; + + ix )
if ( options [ ix ] . first = = name )
return ix ;
return freqman_invalid_index ;
}
2023-07-11 20:48:36 +00:00
const option_t * find_by_index ( const options_t & options , freqman_index_t index ) {
if ( index < options . size ( ) )
return & options [ index ] ;
else
return nullptr ;
}
2023-07-08 20:04:12 +00:00
/* Impl for next round of changes.
* template < typename T , size_t N >
* const T * find_by_name ( const std : : array < T , N > & info , std : : string_view name ) {
* for ( const auto & it : info ) {
* if ( it . name = = name )
* return & it ;
* }
*
* return nullptr ;
* }
*/
2023-07-17 18:43:37 +00:00
bool operator = = ( const freqman_entry & lhs , const freqman_entry & rhs ) {
auto equal = lhs . type = = rhs . type & &
lhs . frequency_a = = rhs . frequency_a & &
lhs . description = = rhs . description & &
lhs . modulation = = rhs . modulation & &
lhs . bandwidth = = rhs . bandwidth ;
if ( ! equal )
return false ;
if ( lhs . type = = freqman_type : : Range ) {
equal = lhs . frequency_b = = rhs . frequency_b & &
lhs . step = = rhs . step ;
} else if ( lhs . type = = freqman_type : : HamRadio ) {
equal = lhs . frequency_b = = rhs . frequency_b & &
lhs . tone = = rhs . tone ;
}
return equal ;
}
2023-07-11 20:48:36 +00:00
std : : string freqman_entry_get_modulation_string ( freqman_index_t modulation ) {
if ( auto opt = find_by_index ( freqman_modulations , modulation ) )
return opt - > first ;
return { } ;
}
std : : string freqman_entry_get_bandwidth_string ( freqman_index_t modulation , freqman_index_t bandwidth ) {
if ( modulation < freqman_modulations . size ( ) ) {
if ( auto opt = find_by_index ( freqman_bandwidths [ modulation ] , bandwidth ) )
return opt - > first ;
}
return { } ;
}
std : : string freqman_entry_get_step_string ( freqman_index_t step ) {
if ( auto opt = find_by_index ( freqman_steps , step ) )
return opt - > first ;
return { } ;
}
std : : string freqman_entry_get_step_string_short ( freqman_index_t step ) {
if ( auto opt = find_by_index ( freqman_steps_short , step ) )
return opt - > first ;
return { } ;
}
2023-07-17 18:43:37 +00:00
const std : : filesystem : : path get_freqman_path ( const std : : string & stem ) {
return freqman_dir / stem + freqman_extension ;
}
bool create_freqman_file ( const std : : string & file_stem ) {
auto fs_error = make_new_file ( get_freqman_path ( file_stem ) ) ;
return fs_error . ok ( ) ;
}
bool load_freqman_file ( const std : : string & file_stem , freqman_db & db , freqman_load_options options ) {
return parse_freqman_file ( get_freqman_path ( file_stem ) , db , options ) ;
}
void delete_freqman_file ( const std : : string & file_stem ) {
delete_file ( get_freqman_path ( file_stem ) ) ;
}
2023-07-11 20:48:36 +00:00
std : : string pretty_string ( const freqman_entry & entry , size_t max_length ) {
std : : string str ;
switch ( entry . type ) {
case freqman_type : : Single :
str = to_string_short_freq ( entry . frequency_a ) + " M: " + entry . description ;
break ;
case freqman_type : : Range :
2023-07-17 18:43:37 +00:00
str = to_string_rounded_freq ( entry . frequency_a , 1 ) + " M- " +
to_string_rounded_freq ( entry . frequency_b , 1 ) + " M: " + entry . description ;
2023-07-11 20:48:36 +00:00
break ;
case freqman_type : : HamRadio :
2023-07-17 18:43:37 +00:00
str = " R: " + to_string_rounded_freq ( entry . frequency_a , 1 ) + " M,T: " +
to_string_rounded_freq ( entry . frequency_b , 1 ) + " M: " + entry . description ;
2023-07-11 20:48:36 +00:00
break ;
2023-07-13 06:28:27 +00:00
case freqman_type : : Raw :
str = entry . description ;
break ;
2023-07-11 20:48:36 +00:00
default :
str = " UNK: " + entry . description ;
break ;
}
// Truncate. '+' indicates if string has been truncated.
if ( str . size ( ) > max_length )
return str . substr ( 0 , max_length - 1 ) + " + " ;
return str ;
}
std : : string to_freqman_string ( const freqman_entry & entry ) {
std : : string serialized ;
serialized . reserve ( 0x80 ) ;
// Append a key=value to the string.
auto append_field = [ & serialized ] ( std : : string_view name , std : : string_view value ) {
if ( ! serialized . empty ( ) )
serialized + = " , " ;
serialized + = std : : string { name } + " = " + std : : string { value } ;
} ;
switch ( entry . type ) {
case freqman_type : : Single :
append_field ( " f " , to_string_dec_uint64 ( entry . frequency_a ) ) ;
break ;
case freqman_type : : Range :
append_field ( " a " , to_string_dec_uint64 ( entry . frequency_a ) ) ;
append_field ( " b " , to_string_dec_uint64 ( entry . frequency_b ) ) ;
if ( is_valid ( entry . step ) )
append_field ( " s " , freqman_entry_get_step_string_short ( entry . step ) ) ;
break ;
case freqman_type : : HamRadio :
append_field ( " r " , to_string_dec_uint64 ( entry . frequency_a ) ) ;
append_field ( " t " , to_string_dec_uint64 ( entry . frequency_b ) ) ;
if ( is_valid ( entry . tone ) )
append_field ( " c " , tonekey : : tone_key_value_string ( entry . tone ) ) ;
break ;
2023-07-13 06:28:27 +00:00
case freqman_type : : Raw :
return entry . description ;
2023-07-11 20:48:36 +00:00
default :
2023-07-13 06:28:27 +00:00
return { } ;
2023-07-11 20:48:36 +00:00
} ;
if ( is_valid ( entry . modulation ) & & entry . modulation < freqman_modulations . size ( ) ) {
append_field ( " m " , freqman_entry_get_modulation_string ( entry . modulation ) ) ;
if ( is_valid ( entry . bandwidth ) & & ( unsigned ) entry . bandwidth < freqman_bandwidths [ entry . modulation ] . size ( ) )
append_field ( " bw " , freqman_entry_get_bandwidth_string ( entry . modulation , entry . bandwidth ) ) ;
}
if ( entry . description . size ( ) > 0 )
append_field ( " d " , entry . description ) ;
serialized . shrink_to_fit ( ) ;
return serialized ;
}
freqman_index_t parse_tone_key ( std : : string_view value ) {
// Split into whole and fractional parts.
auto parts = split_string ( value , ' . ' ) ;
int32_t tone_freq = 0 ;
int32_t whole_part = 0 ;
parse_int ( parts [ 0 ] , whole_part ) ;
// Tones are stored as frequency / 100 for some reason.
// E.g. 14572 would be 145.7 (NB: 1s place is dropped).
// TODO: Might be easier to just store the codes?
// Multiply the whole part by 100 to get the tone frequency.
tone_freq = whole_part * 100 ;
// Add the fractional part, if present.
if ( parts . size ( ) > 1 ) {
auto c = parts [ 1 ] . front ( ) ;
auto digit = std : : isdigit ( c ) ? c - ' 0 ' : 0 ;
tone_freq + = digit * 10 ;
}
return static_cast < freqman_index_t > ( tonekey : : tone_key_index_by_value ( tone_freq ) ) ;
}
2023-07-08 20:04:12 +00:00
bool parse_freqman_entry ( std : : string_view str , freqman_entry & entry ) {
if ( str . empty ( ) | | str [ 0 ] = = ' # ' )
return false ;
entry = freqman_entry { } ;
2023-07-09 18:15:14 +00:00
auto cols = split_string ( str , ' , ' ) ;
2023-07-08 20:04:12 +00:00
for ( auto col : cols ) {
if ( col . empty ( ) )
continue ;
auto pair = split_string ( col , ' = ' ) ;
if ( pair . size ( ) ! = 2 )
continue ;
auto key = pair [ 0 ] ;
auto value = pair [ 1 ] ;
if ( key = = " a " ) {
entry . type = freqman_type : : Range ;
parse_int ( value , entry . frequency_a ) ;
} else if ( key = = " b " ) {
parse_int ( value , entry . frequency_b ) ;
} else if ( key = = " bw " ) {
// NB: Requires modulation to be set first
if ( entry . modulation < std : : size ( freqman_bandwidths ) ) {
entry . bandwidth = find_by_name ( freqman_bandwidths [ entry . modulation ] , value ) ;
}
} else if ( key = = " c " ) {
2023-07-11 20:48:36 +00:00
entry . tone = parse_tone_key ( value ) ;
2023-07-08 20:04:12 +00:00
} else if ( key = = " d " ) {
entry . description = trim ( value ) ;
} else if ( key = = " f " ) {
entry . type = freqman_type : : Single ;
parse_int ( value , entry . frequency_a ) ;
} else if ( key = = " m " ) {
entry . modulation = find_by_name ( freqman_modulations , value ) ;
} else if ( key = = " r " ) {
entry . type = freqman_type : : HamRadio ;
parse_int ( value , entry . frequency_a ) ;
} else if ( key = = " s " ) {
entry . step = find_by_name ( freqman_steps_short , value ) ;
} else if ( key = = " t " ) {
parse_int ( value , entry . frequency_b ) ;
}
}
2023-07-15 01:46:39 +00:00
return is_valid ( entry ) ;
2023-07-08 20:04:12 +00:00
}
bool parse_freqman_file ( const fs : : path & path , freqman_db & db , freqman_load_options options ) {
2023-07-17 18:43:37 +00:00
FreqmanDB freqman_db ;
freqman_db . set_read_raw ( false ) ; // Don't return malformed lines.
if ( ! freqman_db . open ( path ) )
2023-07-08 20:04:12 +00:00
return false ;
// Attempt to avoid a re-alloc if possible.
db . clear ( ) ;
2023-07-17 18:43:37 +00:00
db . reserve ( freqman_db . entry_count ( ) ) ;
2023-07-08 20:04:12 +00:00
2023-07-17 18:43:37 +00:00
for ( auto entry : freqman_db ) {
2023-07-08 20:04:12 +00:00
// Filter by entry type.
2023-07-17 18:43:37 +00:00
if ( entry . type = = freqman_type : : Unknown | |
( entry . type = = freqman_type : : Single & & ! options . load_freqs ) | |
2023-07-08 20:04:12 +00:00
( entry . type = = freqman_type : : Range & & ! options . load_ranges ) | |
( entry . type = = freqman_type : : HamRadio & & ! options . load_hamradios ) ) {
continue ;
}
// Use previous entry's mod/band if current's aren't set.
if ( ! db . empty ( ) ) {
if ( is_invalid ( entry . modulation ) )
entry . modulation = db . back ( ) - > modulation ;
if ( is_invalid ( entry . bandwidth ) )
entry . bandwidth = db . back ( ) - > bandwidth ;
}
// Move the entry onto the heap and push.
db . push_back ( std : : make_unique < freqman_entry > ( std : : move ( entry ) ) ) ;
// Limit to max_entries when specified.
if ( options . max_entries > 0 & & db . size ( ) > = options . max_entries )
break ;
}
db . shrink_to_fit ( ) ;
return true ;
2023-07-11 20:48:36 +00:00
}
2023-07-15 01:46:39 +00:00
bool is_valid ( const freqman_entry & entry ) {
// No valid frequency combination was set.
if ( entry . type = = freqman_type : : Unknown )
return false ;
// Frequency A must be set for all types
if ( entry . frequency_a = = 0 )
return false ;
// Frequency B must be set for type Range or Ham Radio
if ( entry . type = = freqman_type : : Range | | entry . type = = freqman_type : : HamRadio ) {
if ( entry . frequency_b = = 0 )
return false ;
}
// Ranges should have frequencies A <= B.
if ( entry . type = = freqman_type : : Range ) {
if ( entry . frequency_a > entry . frequency_b )
return false ;
}
// TODO: Consider additional validation:
// - Tone only on HamRadio.
2023-07-17 18:43:37 +00:00
// - Step only on Range
2023-07-15 01:46:39 +00:00
// - Fail on failed parse_int.
// - Fail if bandwidth set before modulation.
return true ;
}
2023-07-11 20:48:36 +00:00
/* FreqmanDB ***********************************/
2023-07-17 18:43:37 +00:00
bool FreqmanDB : : open ( const std : : filesystem : : path & path , bool create ) {
auto result = FileWrapper : : open ( path , create ) ;
2023-07-11 20:48:36 +00:00
if ( ! result )
return false ;
wrapper_ = * std : : move ( result ) ;
return true ;
}
void FreqmanDB : : close ( ) {
wrapper_ . reset ( ) ;
}
2023-07-17 18:43:37 +00:00
freqman_entry FreqmanDB : : operator [ ] ( Index index ) const {
auto length = wrapper_ - > line_length ( index ) ;
auto line_text = wrapper_ - > get_text ( index , 0 , length ) ;
2023-07-11 20:48:36 +00:00
if ( line_text ) {
freqman_entry entry ;
if ( parse_freqman_entry ( * line_text , entry ) )
return entry ;
2023-07-17 18:43:37 +00:00
else if ( read_raw_ ) {
2023-07-13 06:28:27 +00:00
entry . type = freqman_type : : Raw ;
entry . description = trim ( * line_text ) ;
return entry ;
}
2023-07-11 20:48:36 +00:00
}
return { } ;
}
2023-07-17 18:43:37 +00:00
void FreqmanDB : : insert_entry ( Index index , const freqman_entry & entry ) {
index = clip < uint32_t > ( index , 0u , entry_count ( ) ) ;
wrapper_ - > insert_line ( index ) ;
replace_entry ( index , entry ) ;
}
void FreqmanDB : : append_entry ( const freqman_entry & entry ) {
insert_entry ( entry_count ( ) , entry ) ;
2023-07-11 20:48:36 +00:00
}
2023-07-17 18:43:37 +00:00
void FreqmanDB : : replace_entry ( Index index , const freqman_entry & entry ) {
auto range = wrapper_ - > line_range ( index ) ;
2023-07-11 20:48:36 +00:00
if ( ! range )
2023-07-13 06:28:27 +00:00
return ;
2023-07-11 20:48:36 +00:00
// Don't overwrite the '\n'.
range - > end - - ;
wrapper_ - > replace_range ( * range , to_freqman_string ( entry ) ) ;
}
2023-07-17 18:43:37 +00:00
void FreqmanDB : : delete_entry ( Index index ) {
wrapper_ - > delete_line ( index ) ;
}
bool FreqmanDB : : delete_entry ( const freqman_entry & entry ) {
auto it = find_entry ( entry ) ;
if ( it = = end ( ) )
return false ;
delete_entry ( it . index ( ) ) ;
return true ;
}
FreqmanDB : : iterator FreqmanDB : : find_entry ( const freqman_entry & entry ) {
return find_entry ( [ & entry ] ( const auto & other ) {
return entry = = other ;
} ) ;
2023-07-11 20:48:36 +00:00
}
uint32_t FreqmanDB : : entry_count ( ) const {
// FileWrapper always presents a single line even for empty files.
return empty ( ) ? 0u : wrapper_ - > line_count ( ) ;
}
bool FreqmanDB : : empty ( ) const {
// FileWrapper always presents a single line even for empty files.
// A DB is only really empty if the file size is 0.
return ! wrapper_ | | wrapper_ - > size ( ) = = 0 ;
2023-07-12 18:27:02 +00:00
}