diff --git a/package-lock.json b/package-lock.json index 2caec72..36ce298 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "fm-dx-webserver", - "version": "1.3.11", + "version": "1.3.12", "license": "ISC", "dependencies": { "@mapbox/node-pre-gyp": "2.0.0", diff --git a/server/datahandler.js b/server/datahandler.js index 8a2f7bb..270ed79 100644 --- a/server/datahandler.js +++ b/server/datahandler.js @@ -1,213 +1,7 @@ /* Libraries / Imports */ -const fs = require('fs'); -const https = require('https'); -const koffi = require('koffi'); -const path = require('path'); -const os = require('os'); -const platform = os.platform(); -const cpuArchitecture = os.arch(); const { configName, serverConfig, configUpdate, configSave } = require('./server_config'); -let unicode_type; -let shared_Library; - -if (platform === 'win32') { - unicode_type = 'int16_t'; - arch_type = (cpuArchitecture === 'x64' ? 'mingw64' : 'mingw32'); - shared_Library=path.join(__dirname, "libraries", arch_type, "librdsparser.dll"); -} else if (platform === 'linux') { - unicode_type = 'int32_t'; - arch_type = (cpuArchitecture === 'x64' ? 'x86_64' : - (cpuArchitecture === 'ia32' ? 'x86' : - (cpuArchitecture === 'arm64' ? 'aarch64' : cpuArchitecture))); - shared_Library=path.join(__dirname, "libraries", arch_type, "librdsparser.so"); -} else if (platform === 'darwin') { - unicode_type = 'int32_t'; - shared_Library=path.join(__dirname, "libraries", "macos", "librdsparser.dylib"); -} - -const lib = koffi.load(shared_Library); +const RDSDecoder = require("./rds.js"); const { fetchTx } = require('./tx_search.js'); - -koffi.proto('void callback_pi(void *rds, void *user_data)'); -koffi.proto('void callback_pty(void *rds, void *user_data)'); -koffi.proto('void callback_tp(void *rds, void *user_data)'); -koffi.proto('void callback_ta(void *rds, void *user_data)'); -koffi.proto('void callback_ms(void *rds, void *user_data)'); -koffi.proto('void callback_ecc(void *rds, void *user_data)'); -koffi.proto('void callback_country(void *rds, void *user_data)'); -koffi.proto('void callback_af(void *rds, uint32_t af, void *user_data)'); -koffi.proto('void callback_ps(void *rds, void *user_data)'); -koffi.proto('void callback_rt(void *rds, int flag, void *user_data)'); -koffi.proto('void callback_ptyn(void *rds, void *user_data)'); -koffi.proto('void callback_ct(void *rds, void *ct, void *user_data)'); - -const rdsparser = { - new: lib.func('void* rdsparser_new()'), - free: lib.func('void rdsparser_free(void *rds)'), - clear: lib.func('void rdsparser_clear(void *rds)'), - parse_string: lib.func('bool rdsparser_parse_string(void *rds, const char *input)'), - set_text_correction: lib.func('void rdsparser_set_text_correction(void *rds, uint8_t text, uint8_t type, uint8_t error)'), - set_text_progressive: lib.func('void rdsparser_set_text_progressive(void *rds, uint8_t string, uint8_t state)'), - get_pi: lib.func('int32_t rdsparser_get_pi(void *rds)'), - get_pty: lib.func('int8_t rdsparser_get_pty(void *rds)'), - get_tp: lib.func('int8_t rdsparser_get_tp(void *rds)'), - get_ta: lib.func('int8_t rdsparser_get_ta(void *rds)'), - get_ms: lib.func('int8_t rdsparser_get_ms(void *rds)'), - get_ecc: lib.func('int16_t rdsparser_get_ecc(void *rds)'), - get_country: lib.func('int rdsparser_get_country(void *rds)'), - get_ps: lib.func('void* rdsparser_get_ps(void *rds)'), - get_rt: lib.func('void* rdsparser_get_rt(void *rds, int flag)'), - get_ptyn: lib.func('void* rdsparser_get_ptyn(void *rds)'), - register_pi: lib.func('void rdsparser_register_pi(void *rds, void *cb)'), - register_pty: lib.func('void rdsparser_register_pty(void *rds, void *cb)'), - register_tp: lib.func('void rdsparser_register_tp(void *rds, void *cb)'), - register_ta: lib.func('void rdsparser_register_ta(void *rds, void *cb)'), - register_ms: lib.func('void rdsparser_register_ms(void *rds, void *cb)'), - register_ecc: lib.func('void rdsparser_register_ecc(void *rds, void *cb)'), - register_country: lib.func('void rdsparser_register_country(void *rds, void *cb)'), - register_af: lib.func('void rdsparser_register_af(void *rds, void *cb)'), - register_ps: lib.func('void rdsparser_register_ps(void *rds, void *cb)'), - register_rt: lib.func('void rdsparser_register_rt(void *rds, void *cb)'), - register_ptyn: lib.func('void rdsparser_register_ptyn(void *rds, void *cb)'), - register_ct: lib.func('void rdsparser_register_ct(void *rds, void *cb)'), - string_get_content: lib.func(unicode_type + '* rdsparser_string_get_content(void *string)'), - string_get_errors: lib.func('uint8_t* rdsparser_string_get_errors(void *string)'), - string_get_length: lib.func('uint8_t rdsparser_string_get_length(void *string)'), - ct_get_year: lib.func('uint16_t rdsparser_ct_get_year(void *ct)'), - ct_get_month: lib.func('uint8_t rdsparser_ct_get_month(void *ct)'), - ct_get_day: lib.func('uint8_t rdsparser_ct_get_day(void *ct)'), - ct_get_hour: lib.func('uint8_t rdsparser_ct_get_hour(void *ct)'), - ct_get_minute: lib.func('uint8_t rdsparser_ct_get_minute(void *ct)'), - ct_get_offset: lib.func('int8_t rdsparser_ct_get_offset(void *ct)'), - pty_lookup_short: lib.func('const char* rdsparser_pty_lookup_short(int8_t pty, bool rbds)'), - pty_lookup_long: lib.func('const char* rdsparser_pty_lookup_long(int8_t pty, bool rbds)'), - country_lookup_name: lib.func('const char* rdsparser_country_lookup_name(int country)'), - country_lookup_iso: lib.func('const char* rdsparser_country_lookup_iso(int country)') -} - -const callbacks = { - pi: koffi.register(rds => ( - value = rdsparser.get_pi(rds) - //console.log('PI: ' + value.toString(16).toUpperCase()) - ), 'callback_pi*'), - - pty: koffi.register(rds => ( - value = rdsparser.get_pty(rds), - dataToSend.pty = value - ), 'callback_pty*'), - - tp: koffi.register(rds => ( - value = rdsparser.get_tp(rds), - dataToSend.tp = value - ), 'callback_tp*'), - - ta: koffi.register(rds => ( - value = rdsparser.get_ta(rds), - dataToSend.ta = value - ), 'callback_ta*'), - - ms: koffi.register(rds => ( - value = rdsparser.get_ms(rds), - dataToSend.ms = value - ), 'callback_ms*'), - - af: koffi.register((rds, value) => ( - dataToSend.af.push(value) - ), 'callback_af*'), - - ecc: koffi.register(rds => ( - value = rdsparser.get_ecc(rds), - dataToSend.ecc = value - ), 'callback_ecc*'), - - country: koffi.register(rds => ( - value = rdsparser.get_country(rds), - display = rdsparser.country_lookup_name(value), - iso = rdsparser.country_lookup_iso(value), - dataToSend.country_name = display, - dataToSend.country_iso = iso - ), 'callback_country*'), - - ps: koffi.register(rds => ( - ps = rdsparser.get_ps(rds), - dataToSend.ps = decode_unicode(ps), - dataToSend.ps_errors = decode_errors(ps) - ), 'callback_ps*'), - - rt: koffi.register((rds, flag) => { - const rt = rdsparser.get_rt(rds, flag); - - if (flag === 0) { - dataToSend.rt0 = decode_unicode(rt); - dataToSend.rt0_errors = decode_errors(rt); - } - - if (flag === 1) { - dataToSend.rt1 = decode_unicode(rt); - dataToSend.rt1_errors = decode_errors(rt); - } - dataToSend.rt_flag = flag; - }, 'callback_rt*'), - - ptyn: koffi.register((rds, flag) => ( - value = decode_unicode(rdsparser.get_ptyn(rds)) - /*console.log('PTYN: ' + value)*/ - ), 'callback_ptyn*'), - - ct: koffi.register((rds, ct) => ( - year = rdsparser.ct_get_year(ct), - month = String(rdsparser.ct_get_month(ct)).padStart(2, '0'), - day = String(rdsparser.ct_get_day(ct)).padStart(2, '0'), - hour = String(rdsparser.ct_get_hour(ct)).padStart(2, '0'), - minute = String(rdsparser.ct_get_minute(ct)).padStart(2, '0'), - offset = rdsparser.ct_get_offset(ct), - tz_sign = (offset >= 0 ? '+' : '-'), - tz_hour = String(Math.abs(Math.floor(offset / 60))).padStart(2, '0'), - tz_minute = String(Math.abs(offset % 60)).padStart(2, '0') - //console.log('CT: ' + year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ' (' + tz_sign + tz_hour + ':' + tz_minute + ')') - ), 'callback_ct*') -}; - -let rds = rdsparser.new() -rdsparser.set_text_correction(rds, 0, 0, 2); -rdsparser.set_text_correction(rds, 0, 1, 2); -rdsparser.set_text_correction(rds, 1, 0, 2); -rdsparser.set_text_correction(rds, 1, 1, 2); -rdsparser.set_text_progressive(rds, 0, 1) -rdsparser.set_text_progressive(rds, 1, 1) -rdsparser.register_pi(rds, callbacks.pi); -rdsparser.register_pty(rds, callbacks.pty); -rdsparser.register_tp(rds, callbacks.tp); -rdsparser.register_ta(rds, callbacks.ta); -rdsparser.register_ms(rds, callbacks.ms); -rdsparser.register_ecc(rds, callbacks.ecc); -rdsparser.register_country(rds, callbacks.country); -rdsparser.register_af(rds, callbacks.af); -rdsparser.register_ps(rds, callbacks.ps); -rdsparser.register_rt(rds, callbacks.rt); -rdsparser.register_ptyn(rds, callbacks.ptyn); -rdsparser.register_ct(rds, callbacks.ct); - -const decode_unicode = function(string) { - let length = rdsparser.string_get_length(string); - if (length) { - let content = rdsparser.string_get_content(string); - let array = koffi.decode(content, unicode_type + ' [' + length + ']'); - return String.fromCodePoint.apply(String, array); - } - return ''; -}; - -const decode_errors = function(string) { - let length = rdsparser.string_get_length(string); - if (length) { - let errors = rdsparser.string_get_errors(string); - let array = koffi.decode(errors, 'uint8_t [' + length + ']'); - return Uint8Array.from(array).toString(); - } - return ''; -}; - const updateInterval = 75; // Initialize the data object @@ -229,7 +23,9 @@ var dataToSend = { ecc: null, af: [], rt0: '', + rt0_errors: '', rt1: '', + rt1_errors: '', rt_flag: '', ims: 0, eq: 0, @@ -251,6 +47,8 @@ var dataToSend = { users: 0, }; +const rds = new RDSDecoder(dataToSend); + const filterMappings = { 'G11': { eq: 1, ims: 1 }, 'G01': { eq: 0, ims: 1 }, @@ -283,7 +81,7 @@ function rdsReceived() { function rdsReset() { resetToDefault(dataToSend); dataToSend.af.length = 0; - rdsparser.clear(rds); + rds.clear(); if (rdsTimeoutTimer) { clearTimeout(rdsTimeoutTimer); rdsTimeoutTimer = null; @@ -400,11 +198,18 @@ function handleData(wss, receivedData, rdsWss) { data += (((errors & 0x03) == 0) ? modifiedData.slice(12, 16) : '----'); const newDataString = "G:\r\n" + data + "\r\n\r\n"; - const finalBuffer = Buffer.from(newDataString, 'utf-8'); - client.send(finalBuffer); + client.send(newDataString); }); - rdsparser.parse_string(rds, modifiedData); + var data2 = ""; + for(var i = 0; i < modifiedData.length; i++) { + data2 += modifiedData[i] + if ((i + 1) % 4 === 0 && i !== modifiedData.length - 1) data2 += " "; + } + var data = []; + data2.split(" ").forEach((i) => {data.push(parseInt(i, 16))}) + + rds.decodeGroup(data[0], data[1], data[2], data[3], data[4]) legacyRdsPiBuffer = null; break; } diff --git a/server/rds.js b/server/rds.js new file mode 100644 index 0000000..2df2179 --- /dev/null +++ b/server/rds.js @@ -0,0 +1,175 @@ +const { rdsEccLookup, iso, countries } = require("./rds_country.js") + +class RDSDecoder { + constructor(data) { + this.data = data; + this.clear() + } + + clear() { + this.data.pi = '?'; + this.ps = Array(8).fill(' '); + this.ps_errors = Array(8).fill("0"); + this.rt0 = Array(64).fill(' '); + this.rt0_errors = Array(64).fill("0"); + this.rt1 = Array(64).fill(' '); + this.rt1_errors = Array(64).fill("0"); + this.data.ps = ''; + this.data.rt1 = ''; + this.data.rt0 = ''; + this.data.pty = 0; + this.data.tp = 0; + this.data.ta = 0; + this.data.ms = -1; + this.data.rt_flag = 0; + this.rt1_to_clear = false; + this.rt0_to_clear = false; + this.data.ecc = null; + this.data.country_name = "" + this.data.country_iso = "UN" + + this.af_len = 0; + this.data.af = [] + this.af_am_follows = false; + } + + decodeGroup(blockA, blockB, blockC, blockD, error) { + if(error > 2) return; // TODO: handle errors + // console.log(error) + this.data.pi = blockA.toString(16).toUpperCase().padStart(4, '0'); + + const group = (blockB >> 12) & 0xF; + const version = (blockB >> 11) & 0x1; + this.data.tp = Number((blockB >> 10) & 1); + this.data.pty = (blockB >> 5) & 0b11111; + + if (group === 0) { + this.data.ta = (blockB >> 4) & 1; + this.data.ms = (blockB >> 3) & 1; + + if(version === 0) { + var af_high = blockC >> 8; + var af_low = blockC & 0xFF; + var BASE = 224; + var FILLER = 205; + var AM_FOLLOWS = 250; + + if(af_high >= BASE && af_high <= (BASE+25)) { + this.af_len = af_high-BASE; + if(this.af_len !== this.data.af.length) { + this.data.af = []; + this.af_am_follows = false; + + if(af_low != FILLER && af_low != AM_FOLLOWS) this.data.af.push((af_low+875)*100) + else if(af_low == AM_FOLLOWS) this.af_am_follows = true; + } + } else if(this.data.af.length != this.af_len) { + if(!(af_high == AM_FOLLOWS || this.af_am_follows)) { + var freq = (af_high+875)*100; + if(!this.data.af.includes(freq)) this.data.af.push(freq); + } + if(this.af_am_follows) this.af_am_follows = false; + if(!(af_high == AM_FOLLOWS || af_low == FILLER || af_low == AM_FOLLOWS)) { + var freq = (af_low+875)*100; + if(!this.data.af.includes(freq)) this.data.af.push(freq); + } + if(af_low == AM_FOLLOWS) this.af_am_follows = true; + } + } + + const idx = blockB & 0x3; + this.ps[idx * 2] = String.fromCharCode(blockD >> 8); + this.ps[idx * 2 + 1] = String.fromCharCode(blockD & 0xFF); + this.ps_errors[idx * 2] = error; + this.ps_errors[idx * 2 + 1] = error; + this.data.ps = this.ps.join(''); + this.data.ps_errors = this.ps_errors.join(','); + } else if (group === 1 && version === 0) { + var la = Boolean(blockC & 0x8000); + var variant_code = (blockC >> 12) & 0x7; + switch (variant_code) { + case 0: + this.data.ecc = blockC & 0xff; + this.data.country_name = rdsEccLookup(blockA, this.data.ecc); + if(this.data.country_name.length === 0) this.data.country_iso = "UN"; + else this.data.country_iso = iso[countries.indexOf(this.data.country_name)] + break; + default: + break; + } + } else if (group === 2) { + const idx = blockB & 0b1111; + this.rt_ab = Boolean((blockB >> 4) & 1); + // TODO: rt_errors + if(this.rt_ab) { + if(this.rt1_to_clear) { + this.rt1 = Array(64).fill(' '); + this.rt1_errors = Array(64).fill("0"); + this.rt1_to_clear = false; + } + + if(version == 0) { + this.rt1[idx * 4] = String.fromCharCode(blockC >> 8); + this.rt1[idx * 4 + 1] = String.fromCharCode(blockC & 0xFF); + this.rt1[idx * 4 + 2] = String.fromCharCode(blockD >> 8); + this.rt1[idx * 4 + 3] = String.fromCharCode(blockD & 0xFF); + this.rt1_errors[idx * 4] = error; + this.rt1_errors[idx * 4 + 1] = error; + this.rt1_errors[idx * 4 + 2] = error; + this.rt1_errors[idx * 4 + 3] = error; + } else { + this.rt1[idx * 2] = String.fromCharCode(blockD >> 8); + this.rt1[idx * 2 + 1] = String.fromCharCode(blockD & 0xFF); + this.rt1_errors[idx * 2] = error; + this.rt1_errors[idx * 2 + 1] = error; + } + + var i = this.rt1.indexOf("\r") + while(i != -1) { + this.rt1[i] = " "; + i = this.rt1.indexOf("\r"); + } + + this.data.rt1 = this.rt1.join(''); + this.data.rt1_errors = this.rt1_errors.join(','); + this.data.rt_flag = 1; + this.rt0_to_clear = true; + } else { + if(this.rt0_to_clear) { + this.rt0 = Array(64).fill(' '); + this.rt0_errors = Array(64).fill("0"); + this.rt0_to_clear = false; + } + if(version == 0) { + this.rt0[idx * 4] = String.fromCharCode(blockC >> 8); + this.rt0[idx * 4 + 1] = String.fromCharCode(blockC & 0xFF); + this.rt0[idx * 4 + 2] = String.fromCharCode(blockD >> 8); + this.rt0[idx * 4 + 3] = String.fromCharCode(blockD & 0xFF); + this.rt0_errors[idx * 4] = error; + this.rt0_errors[idx * 4 + 1] = error; + this.rt0_errors[idx * 4 + 2] = error; + this.rt0_errors[idx * 4 + 3] = error; + } else { + this.rt0[idx * 2] = String.fromCharCode(blockD >> 8); + this.rt0[idx * 2 + 1] = String.fromCharCode(blockD & 0xFF); + this.rt0_errors[idx * 2] = error; + this.rt0_errors[idx * 2 + 1] = error; + } + + var i = this.rt0.indexOf("\r"); + while(i != -1) { + this.rt0[i] = " "; + i = this.rt0.indexOf("\r"); + } + + this.data.rt0 = this.rt0.join(''); + this.data.rt0_errors = this.rt0_errors.join(','); + this.data.rt_flag = 0; + this.rt1_to_clear = true; + } + } else { + // console.log(group, version) + } + } +} +module.exports = RDSDecoder; \ No newline at end of file diff --git a/server/rds_country.js b/server/rds_country.js new file mode 100644 index 0000000..9573964 --- /dev/null +++ b/server/rds_country.js @@ -0,0 +1,632 @@ +var countries = [ + "Albania", + "Estonia", + "Algeria", + "Ethiopia", + "Andorra", + "Angola", + "Finland", + "Armenia", + "France", + "Ascension Island", + "Gabon", + "Austria", + "Gambia", + "Azerbaijan", + "Georgia", + "Germany", + "Bahrein", + "Ghana", + "Belarus", + "Gibraltar", + "Belgium", + "Greece", + "Benin", + "Guinea", + "Bosnia Herzegovina", + "Guinea-Bissau", + "Botswana", + "Hungary", + "Bulgaria", + "Iceland", + "Burkina Faso", + "Iraq", + "Burundi", + "Ireland", + "Cabinda", + "Israel", + "Cameroon", + "Italy", + "Jordan", + "Cape Verde", + "Kazakhstan", + "Central African Republic", + "Kenya", + "Chad", + "Kosovo", + "Comoros", + "Kuwait", + "DR Congo", + "Kyrgyzstan", + "Republic of Congo", + "Latvia", + "Cote d'Ivoire", + "Lebanon", + "Croatia", + "Lesotho", + "Cyprus", + "Liberia", + "Czechia", + "Libya", + "Denmark", + "Liechtenstein", + "Djiboutia", + "Lithuania", + "Egypt", + "Luxembourg", + "Equatorial Guinea", + "Macedonia", + "Eritrea", + "Madagascar", + "Seychelles", + "Malawi", + "Sierra Leone", + "Mali", + "Slovakia", + "Malta", + "Slovenia", + "Mauritania", + "Somalia", + "Mauritius", + "South Africa", + "Moldova", + "South Sudan", + "Monaco", + "Spain", + "Mongolia", + "Sudan", + "Montenegro", + "Swaziland", + "Morocco", + "Sweden", + "Mozambique", + "Switzerland", + "Namibia", + "Syria", + "Netherlands", + "Tajikistan", + "Niger", + "Tanzania", + "Nigeria", + "Togo", + "Norway", + "Tunisia", + "Oman", + "Turkey", + "Palestine", + "Turkmenistan", + "Poland", + "Uganda", + "Portugal", + "Ukraine", + "Qatar", + "United Arab Emirates", + "Romania", + "United Kingdom", + "Russia", + "Uzbekistan", + "Rwanda", + "Vatican", + "San Marino", + "Western Sahara", + "Sao Tome and Principe", + "Yemen", + "Saudi Arabia", + "Zambia", + "Senegal", + "Zimbabwe", + "Serbia", + "Anguilla", + "Guyana", + "Antigua and Barbuda", + "Haiti", + "Argentina", + "Honduras", + "Aruba", + "Jamaica", + "Bahamas", + "Martinique", + "Barbados", + "Mexico", + "Belize", + "Montserrat", + "Brazil/Bermuda", + "Brazil/AN", + "Bolivia", + "Nicaragua", + "Brazil", + "Panama", + "Canada", + "Paraguay", + "Cayman Islands", + "Peru", + "Chile", + "USA/VI/PR", + "Colombia", + "St. Kitts", + "Costa Rica", + "St. Lucia", + "Cuba", + "St. Pierre and Miquelon", + "Dominica", + "St. Vincent", + "Dominican Republic", + "Suriname", + "El Salvador", + "Trinidad and Tobago", + "Turks and Caicos islands", + "Falkland Islands", + "Greenland", + "Uruguay", + "Grenada", + "Venezuela", + "Guadeloupe", + "Virgin Islands", + "Guatemala", + "Afghanistan", + "South Korea", + "Laos", + "Australia Capital Territory", + "Macao", + "Australia New South Wales", + "Malaysia", + "Australia Victoria", + "Maldives", + "Australia Queensland", + "Marshall Islands", + "Australia South Australia", + "Micronesia", + "Australia Western Australia", + "Myanmar", + "Australia Tasmania", + "Nauru", + "Australia Northern Territory", + "Nepal", + "Bangladesh", + "New Zealand", + "Bhutan", + "Pakistan", + "Brunei Darussalam", + "Papua New Guinea", + "Cambodia", + "Philippines", + "China", + "Samoa", + "Singapore", + "Solomon Islands", + "Fiji", + "Sri Lanka", + "Hong Kong", + "Taiwan", + "India", + "Thailand", + "Indonesia", + "Tonga", + "Iran", + "Vanuatu", + "Japan", + "Vietnam", + "Kiribati", + "North Korea", + "Brazil/Equator" +] + +var iso = [ + "AL", + "EE", + "DZ", + "ET", + "AD", + "AO", + "FI", + "AM", + "FR", + "SH", + "GA", + "AT", + "GM", + "AZ", + "GE", + "DE", + "BH", + "GH", + "BY", + "GI", + "BE", + "GR", + "BJ", + "GN", + "BA", + "GW", + "BW", + "HU", + "BG", + "IS", + "BF", + "IQ", + "BI", + "IE", + "--", + "IL", + "CM", + "IT", + "JO", + "CV", + "KZ", + "CF", + "KE", + "TD", + "XK", + "KM", + "KW", + "CD", + "KG", + "CG", + "LV", + "CI", + "LB", + "HR", + "LS", + "CY", + "LR", + "CZ", + "LY", + "DK", + "LI", + "DJ", + "LT", + "EG", + "LU", + "GQ", + "MK", + "ER", + "MG", + "SC", + "MW", + "SL", + "ML", + "SK", + "MT", + "SI", + "MR", + "SO", + "MU", + "ZA", + "MD", + "SS", + "MC", + "ES", + "MN", + "SD", + "ME", + "SZ", + "MA", + "SE", + "MZ", + "CH", + "NA", + "SY", + "NL", + "TJ", + "NE", + "TZ", + "NG", + "TG", + "NO", + "TN", + "OM", + "TR", + "PS", + "TM", + "PL", + "UG", + "PT", + "UA", + "QA", + "AE", + "RO", + "GB", + "RU", + "UZ", + "RW", + "VA", + "SM", + "EH", + "ST", + "YE", + "SA", + "ZM", + "SN", + "ZW", + "RS", + "AI", + "GY", + "AG", + "HT", + "AR", + "HN", + "AW", + "JM", + "BS", + "MQ", + "BB", + "MX", + "BZ", + "MS", + "--", + "--", + "BO", + "NI", + "BR", + "PA", + "CA", + "PY", + "KY", + "PE", + "CL", + "--", + "CO", + "KN", + "CR", + "LC", + "CU", + "PM", + "DM", + "VC", + "DO", + "SR", + "SN", + "TT", + "TB", + "FK", + "GL", + "UY", + "GD", + "VE", + "GP", + "VG", + "GT", + "AF", + "KR", + "LA", + "AU", + "MO", + "AU", + "MY", + "AU", + "MV", + "AU", + "MH", + "AU", + "FM", + "AU", + "MM", + "AU", + "NR", + "AU", + "NP", + "BD", + "NZ", + "BT", + "PK", + "BN", + "PG", + "KH", + "PH", + "CN", + "WS", + "SG", + "SB", + "FJ", + "LK", + "HK", + "TW", + "IN", + "TH", + "ID", + "TO", + "IR", + "VU", + "JP", + "VN", + "KI", + "KP", + "--" +] + +// RDS ECC Lookup Tables - Converted from C to JavaScript + +const rdsEccA0A6Lut = [ + // A0 + [ + "USA/VI/PR", "USA/VI/PR", "USA/VI/PR", "USA/VI/PR", "USA/VI/PR", + "USA/VI/PR", "USA/VI/PR", "USA/VI/PR", "USA/VI/PR", "USA/VI/PR", + "USA/VI/PR", null, "USA/VI/PR", "USA/VI/PR", null + ], + // A1 + [ + null, null, null, null, null, + null, null, null, null, null, + "Canada", "Canada", "Canada", "Canada", "Greenland" + ], + // A2 + [ + "Anguilla", "Antigua and Barbuda", "Brazil/Equator", "Falkland Islands", "Barbados", + "Belize", "Cayman Islands", "Costa Rica", "Cuba", "Argentina", + "Brazil", "Brazil/Bermuda", "Brazil/AN", "Guadeloupe", "Bahamas" + ], + // A3 + [ + "Bolivia", "Colombia", "Jamaica", "Martinique", null, + "Paraguay", "Nicaragua", null, "Panama", "Dominica", + "Dominican Republic", "Chile", "Grenada", "Turks and Caicos islands", "Guyana" + ], + // A4 + [ + "Guatemala", "Honduras", "Aruba", null, "Montserrat", + "Trinidad and Tobago", "Peru", "Suriname", "Uruguay", "St. Kitts", + "St. Lucia", "El Salvador", "Haiti", "Venezuela", "Virgin Islands" + ], + // A5 + [ + null, null, null, null, null, + null, null, null, null, null, + "Mexico", "St. Vincent", "Mexico", "Mexico", "Mexico" + ], + // A6 + [ + null, null, null, null, null, + null, null, null, null, null, + null, null, null, null, "St. Pierre and Miquelon" + ] +]; + +const rdsEccD0D4Lut = [ + // D0 + [ + "Cameroon", "Central African Republic", "Djiboutia", "Madagascar", "Mali", + "Angola", "Equatorial Guinea", "Gabon", "Guinea", "South Africa", + "Burkina Faso", "Republic of Congo", "Togo", "Benin", "Malawi" + ], + // D1 + [ + "Namibia", "Liberia", "Ghana", "Mauritania", "Sao Tome and Principe", + "Cape Verde", "Senegal", "Gambia", "Burundi", "Ascension Island", + "Botswana", "Comoros", "Tanzania", "Ethiopia", "Nigeria" + ], + // D2 + [ + "Sierra Leone", "Zimbabwe", "Mozambique", "Uganda", "Swaziland", + "Kenya", "Somalia", "Niger", "Chad", "Guinea-Bissau", + "DR Congo", "Cote d'Ivoire", null, "Zambia", "Eritrea" + ], + // D3 + [ + null, null, "Western Sahara", "Cabinda", "Rwanda", + "Lesotho", null, "Seychelles", null, "Mauritius", + null, "Sudan", null, null, null + ], + // D4 + [ + null, null, null, null, null, + null, null, null, null, "South Sudan", + null, null, null, null, null + ] +]; + +const rdsEccE0E5Lut = [ + // E0 + [ + "Germany", "Algeria", "Andorra", "Israel", "Italy", + "Belgium", "Russia", "Palestine", "Albania", "Austria", + "Hungary", "Malta", "Germany", null, "Egypt" + ], + // E1 + [ + "Greece", "Cyprus", "San Marino", "Switzerland", "Jordan", + "Finland", "Luxembourg", "Bulgaria", "Denmark", "Gibraltar", + "Iraq", "United Kingdom", "Libya", "Romania", "France" + ], + // E2 + [ + "Morocco", "Czechia", "Poland", "Vatican", "Slovakia", + "Syria", "Tunisia", null, "Liechtenstein", "Iceland", + "Monaco", "Lithuania", "Serbia", "Spain", "Norway" + ], + // E3 + [ + "Montenegro", "Ireland", "Turkey", null, "Tajikistan", + null, null, "Netherlands", "Latvia", "Lebanon", + "Azerbaijan", "Croatia", "Kazakhstan", "Sweden", "Belarus" + ], + // E4 + [ + "Moldova", "Estonia", "Macedonia", null, null, + "Ukraine", "Kosovo", "Portugal", "Slovenia", "Armenia", + "Uzbekistan", "Georgia", null, "Turkmenistan", "Bosnia Herzegovina" + ], + // E5 + [ + null, null, "Kyrgyzstan", null, null, + null, null, null, null, null, + null, null, null, null, null + ] +]; + +const rdsEccF0F4Lut = [ + // F0 + [ + "Australia Capital Territory", "Australia New South Wales", "Australia Victoria", "Australia Queensland", "Australia South Australia", + "Australia Western Australia", "Australia Tasmania", "Australia Northern Territory", "Saudi Arabia", "Afghanistan", + "Myanmar", "China", "North Korea", "Bahrein", "Malaysia" + ], + // F1 + [ + "Kiribati", "Bhutan", "Bangladesh", "Pakistan", "Fiji", + "Oman", "Nauru", "Iran", "New Zealand", "Solomon Islands", + "Brunei Darussalam", "Sri Lanka", "Taiwan", "South Korea", "Hong Kong" + ], + // F2 + [ + "Kuwait", "Qatar", "Cambodia", "Samoa", "India", + "Macao", "Vietnam", "Philippines", "Japan", "Singapore", + "Maldives", "Indonesia", "United Arab Emirates", "Nepal", "Vanuatu" + ], + // F3 + [ + "Laos", "Thailand", "Tonga", null, null, + null, null, "China", "Papua New Guinea", null, + "Yemen", null, null, "Micronesia", "Mongolia" + ], + // F4 + [ + null, null, null, null, null, + null, null, null, "China", null, + "Marshall Islands", null, null, null, null + ] +]; + +function rdsEccLookup(pi, ecc) { + const PI_UNKNOWN = -1; + + const piCountry = (pi >> 12) & 0xF; + + if (pi === PI_UNKNOWN || piCountry === 0) { + return "" + } + + const piId = piCountry - 1; + + const eccRanges = [ + { min: 0xA0, max: 0xA6, lut: rdsEccA0A6Lut }, + { min: 0xD0, max: 0xD4, lut: rdsEccD0D4Lut }, + { min: 0xE0, max: 0xE5, lut: rdsEccE0E5Lut }, + { min: 0xF0, max: 0xF4, lut: rdsEccF0F4Lut } + ]; + + // Check each range + for (const range of eccRanges) { + if (ecc >= range.min && ecc <= range.max) { + const eccId = ecc - range.min; + return range.lut[eccId][piId]; + } + } + + return "" +} + +module.exports = { + rdsEccLookup, + iso, + countries +}; \ No newline at end of file