sql commands for querying the frequency bands and segments
This commit is contained in:
+112
-2
@@ -4,6 +4,7 @@
|
|||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
#include <sqlite3.h>
|
#include <sqlite3.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
#include <hammy/types.h>
|
#include <hammy/types.h>
|
||||||
|
|
||||||
@@ -15,14 +16,30 @@
|
|||||||
|
|
||||||
#define HAMMY_CALLSIGN_MAX 32
|
#define HAMMY_CALLSIGN_MAX 32
|
||||||
#define HAMMY_ENTITY_NAME_MAX 64
|
#define HAMMY_ENTITY_NAME_MAX 64
|
||||||
|
#define HAMMY_COUNTRY_MAX 4
|
||||||
|
|
||||||
// Longest code in the morse table is '$' ('...-..-'), seven characters.
|
// Longest code in the morse table is '$' ('...-..-'), seven characters.
|
||||||
#define HAMMY_MORSE_MAX 16
|
#define HAMMY_MORSE_MAX 16
|
||||||
|
|
||||||
|
// A country has at most a handful of licence classes, each with at most a
|
||||||
|
// couple of segments covering one exact frequency. 24 is generous.
|
||||||
|
#define HAMMY_FREQ_PRIVS_MAX 24
|
||||||
|
#define HAMMY_FREQ_IARU_MAX 8
|
||||||
|
#define HAMMY_FREQ_COUNTRIES_MAX 64
|
||||||
|
|
||||||
struct hammy_refdb_t {
|
struct hammy_refdb_t {
|
||||||
sqlite3* handle;
|
sqlite3* handle;
|
||||||
|
|
||||||
sqlite3_stmt* stExact;
|
sqlite3_stmt* stExact;
|
||||||
sqlite3_stmt* stPrefix;
|
sqlite3_stmt* stPrefix;
|
||||||
sqlite3_stmt* stMorse;
|
sqlite3_stmt* stMorse;
|
||||||
|
sqlite3_stmt* stFreqMain;
|
||||||
|
sqlite3_stmt* stFreqIaru;
|
||||||
|
sqlite3_stmt* stFreqNearest;
|
||||||
|
sqlite3_stmt* stFreqSegEdge;
|
||||||
|
sqlite3_stmt* stCountryKnown;
|
||||||
|
sqlite3_stmt* stCountryList;
|
||||||
|
|
||||||
char morseCode[HAMMY_MORSE_MAX]; // Scratch for the last hammy_refdb_get_morse() hit
|
char morseCode[HAMMY_MORSE_MAX]; // Scratch for the last hammy_refdb_get_morse() hit
|
||||||
char version[64];
|
char version[64];
|
||||||
};
|
};
|
||||||
@@ -39,6 +56,64 @@ struct hammy_dxcc_t {
|
|||||||
char matchedPrefix[HAMMY_CALLSIGN_MAX];
|
char matchedPrefix[HAMMY_CALLSIGN_MAX];
|
||||||
bool exact;
|
bool exact;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Frequency lookup
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
struct hammy_freq_priv_t {
|
||||||
|
char code[8]; // 'E', 'G', 'T'
|
||||||
|
char name[48]; // 'Amateur Extra'
|
||||||
|
int rank;
|
||||||
|
bool permitted; // false = this class may NOT transmit here
|
||||||
|
char modes[64]; // 'CW,DATA' - empty when !permitted
|
||||||
|
int64_t segLowHz;
|
||||||
|
int64_t segHighHz;
|
||||||
|
int maxPowerW; // 0 = national default, no specific limit
|
||||||
|
char notes[160];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct hammy_freq_iaru_t {
|
||||||
|
int region; // 1, 2 or 3
|
||||||
|
int64_t lowHz;
|
||||||
|
int64_t highHz;
|
||||||
|
char modes[32];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct hammy_freq_t {
|
||||||
|
int64_t freqHz;
|
||||||
|
|
||||||
|
bool inBand;
|
||||||
|
char band[16]; // '20m'
|
||||||
|
int64_t bandLowHz;
|
||||||
|
int64_t bandHighHz;
|
||||||
|
|
||||||
|
// The frequency sits exactly on a band or segment boundary. Worth saying
|
||||||
|
// out loud: a signal of any width centred there straddles both sides.
|
||||||
|
bool atBandEdge;
|
||||||
|
bool atSegmentEdge;
|
||||||
|
|
||||||
|
// False when the bundle carries no licence data for this country at all.
|
||||||
|
// The band and IARU results are still valid; only the privilege table is
|
||||||
|
// missing. Say so rather than showing an empty table.
|
||||||
|
bool countryKnown;
|
||||||
|
char country[HAMMY_COUNTRY_MAX];
|
||||||
|
|
||||||
|
hammy_freq_priv_t privs[HAMMY_FREQ_PRIVS_MAX];
|
||||||
|
size_t nPrivs;
|
||||||
|
|
||||||
|
hammy_freq_iaru_t iaru[HAMMY_FREQ_IARU_MAX];
|
||||||
|
size_t nIaru;
|
||||||
|
|
||||||
|
// Only meaningful when !in_band.
|
||||||
|
char nearestBand[16];
|
||||||
|
int64_t nearestLowHz;
|
||||||
|
int64_t nearestHighHz;
|
||||||
|
int64_t nearestDistanceHz;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Lifecycle
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
// Opens the bundle read-only and prepares the hot statements.
|
// Opens the bundle read-only and prepares the hot statements.
|
||||||
// Returns NULL and logs on failure.
|
// Returns NULL and logs on failure.
|
||||||
@@ -47,6 +122,13 @@ hammy_refdb_t* hammy_refdb_open(const char* path);
|
|||||||
// Finalises statements and closes the connection. NULLs the passing reference.
|
// Finalises statements and closes the connection. NULLs the passing reference.
|
||||||
bool hammy_refdb_close(hammy_refdb_t** db);
|
bool hammy_refdb_close(hammy_refdb_t** db);
|
||||||
|
|
||||||
|
// Bundle version string from ref_meta, or NULL. Owned by the refdb.
|
||||||
|
const char* hammy_refdb_version(hammy_refdb_t* db);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Queries
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
// Resolves a callsign to a DXCC entity.
|
// Resolves a callsign to a DXCC entity.
|
||||||
//
|
//
|
||||||
// Uses candidate-prefix equality seeks rather than "? GLOB prefix || '*'". The
|
// Uses candidate-prefix equality seeks rather than "? GLOB prefix || '*'". The
|
||||||
@@ -63,8 +145,36 @@ bool hammy_refdb_dxcc(hammy_refdb_t* db, const char* callsign, hammy_dxcc_t* out
|
|||||||
// On a hit *out points at storage owned by the refdb and is only valid until
|
// On a hit *out points at storage owned by the refdb and is only valid until
|
||||||
// the NEXT call on the same handle - copy it if it has to outlive that.
|
// the NEXT call on the same handle - copy it if it has to outlive that.
|
||||||
bool hammy_refdb_get_morse(hammy_refdb_t* db, char c, const char** out);
|
bool hammy_refdb_get_morse(hammy_refdb_t* db, char c, const char** out);
|
||||||
|
|
||||||
|
// What band is freq_hz in, and who may transmit there.
|
||||||
|
//
|
||||||
|
// country is an ISO 3166-1 alpha-2 code; NULL or empty means "US". Always
|
||||||
|
// returns true unless the arguments are bad: "not in a band" and "no data for
|
||||||
|
// that country" are results, not failures. Check out->in_band and
|
||||||
|
// out->country_known.
|
||||||
|
bool hammy_refdb_freq(hammy_refdb_t* db, int64_t freqHz, const char* country,
|
||||||
|
hammy_freq_t* out);
|
||||||
|
|
||||||
// Bundle version string from ref_meta, or NULL. Owned by the refdb.
|
// Which countries have licence data, for the "no data for XX yet" message.
|
||||||
const char* hammy_refdb_version(hammy_refdb_t* db);
|
// Writes up to cap codes and returns how many were written.
|
||||||
|
size_t hammy_refdb_countries(hammy_refdb_t* db,
|
||||||
|
char out[][HAMMY_COUNTRY_MAX], size_t cap);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Input parsing
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Parses a user-supplied frequency into integer Hz. Accepts "14.150",
|
||||||
|
// "14150 kHz", "146.52 MHz", "1.2 GHz", "14150000 Hz". A bare number with no
|
||||||
|
// unit is read as MHz, which is what people type.
|
||||||
|
//
|
||||||
|
// Deliberately avoids floating point: "14.150" is converted digit by digit to
|
||||||
|
// 14150000 exactly. Going via double gives 14150000.000000002, which compares
|
||||||
|
// wrong against an integer column at exactly a band edge - the one case where
|
||||||
|
// being right matters most.
|
||||||
|
//
|
||||||
|
// Returns false on unparseable input or a value outside 1 Hz .. 300 GHz.
|
||||||
|
bool hammy_freq_parse(const char* text, int64_t* outHz);
|
||||||
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -8,5 +8,8 @@ typedef struct hammy_command_t hammy_command_t;
|
|||||||
typedef struct hammy_bot_t hammy_bot_t;
|
typedef struct hammy_bot_t hammy_bot_t;
|
||||||
typedef struct hammy_refdb_t hammy_refdb_t;
|
typedef struct hammy_refdb_t hammy_refdb_t;
|
||||||
typedef struct hammy_dxcc_t hammy_dxcc_t;
|
typedef struct hammy_dxcc_t hammy_dxcc_t;
|
||||||
|
typedef struct hammy_freq_priv_t hammy_freq_priv_t;
|
||||||
|
typedef struct hammy_freq_iaru_t hammy_freq_iaru_t;
|
||||||
|
typedef struct hammy_freq_t hammy_freq_t;
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#include <concord/discord.h>
|
||||||
|
#include <inttypes.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include <hammy/command.h>
|
||||||
|
#include <hammy/job.h>
|
||||||
|
#include <hammy/refdb.h>
|
||||||
|
|
||||||
|
// Instant command: runs on the gateway thread, sends a fresh response.
|
||||||
|
void hammy_cmd_freq(const hammy_job_t* job, struct discord* client, const hammy_refdb_t* refdb) {
|
||||||
|
// Get the text argument from the job
|
||||||
|
const char* freq = hammy_job_get_arg(job, "frequency");
|
||||||
|
if (!freq) {
|
||||||
|
hammy_job_respond(job, client, "Error", "No frequency provided for lookup.", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
char* endPtr;
|
||||||
|
float freqMHz = strtof(freq, &endPtr);
|
||||||
|
|
||||||
|
if (*endPtr != '\0') {
|
||||||
|
// Partial conversion, just error out
|
||||||
|
hammy_job_respond(job, client, "Error", "An internal error occurred. Please try again.", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t freqPlain = (uint64_t)freqMHz * 1000000; // Convert to raw Hz
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
// table.
|
// table.
|
||||||
void hammy_cmd_ping(const hammy_job_t* job, struct discord* client, const hammy_refdb_t* refdb);
|
void hammy_cmd_ping(const hammy_job_t* job, struct discord* client, const hammy_refdb_t* refdb);
|
||||||
void hammy_cmd_morse(const hammy_job_t* job, struct discord* client, const hammy_refdb_t* refdb);
|
void hammy_cmd_morse(const hammy_job_t* job, struct discord* client, const hammy_refdb_t* refdb);
|
||||||
|
void hammy_cmd_freq(const hammy_job_t* job, struct discord* client, const hammy_refdb_t* refdb);
|
||||||
|
|
||||||
// TODO: Temporary - move to a proper options system; e.g. a vector of heap-allocated options or something. For now, just point at static storage.
|
// TODO: Temporary - move to a proper options system; e.g. a vector of heap-allocated options or something. For now, just point at static storage.
|
||||||
static struct discord_application_command_option morse_opts[] = {
|
static struct discord_application_command_option morse_opts[] = {
|
||||||
@@ -20,6 +21,18 @@ static struct discord_application_command_options morse_opts_struct = {
|
|||||||
.array = morse_opts
|
.array = morse_opts
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static struct discord_application_command_option freq_opts[] = {
|
||||||
|
{ .type = DISCORD_APPLICATION_OPTION_STRING, .name = "frequency",
|
||||||
|
.description = "Frequency in MHz", .required = true },
|
||||||
|
{ .type = DISCORD_APPLICATION_OPTION_STRING, .name = "country",
|
||||||
|
.description = "Country code to look up (e.g. US) - Defaults to US if none specified.", .required = false },
|
||||||
|
};
|
||||||
|
|
||||||
|
static struct discord_application_command_options freq_opts_struct = {
|
||||||
|
.size = 2,
|
||||||
|
.array = freq_opts
|
||||||
|
};
|
||||||
|
|
||||||
// The command table. Pure data - no allocation, no lifetime, no destructor.
|
// The command table. Pure data - no allocation, no lifetime, no destructor.
|
||||||
// Copying an entry into the bot's vector is a plain struct copy, and the vector
|
// Copying an entry into the bot's vector is a plain struct copy, and the vector
|
||||||
// may be created with a NULL destructor hook.
|
// may be created with a NULL destructor hook.
|
||||||
@@ -28,6 +41,7 @@ static struct discord_application_command_options morse_opts_struct = {
|
|||||||
static const hammy_command_t hammy_builtin_commands[] = {
|
static const hammy_command_t hammy_builtin_commands[] = {
|
||||||
{ "ping", "Check whether Hammy is alive and how fast it is responding", NULL, &hammy_cmd_ping, true },
|
{ "ping", "Check whether Hammy is alive and how fast it is responding", NULL, &hammy_cmd_ping, true },
|
||||||
{ "morse", "Text to and from Morse code", &morse_opts_struct, &hammy_cmd_morse, true },
|
{ "morse", "Text to and from Morse code", &morse_opts_struct, &hammy_cmd_morse, true },
|
||||||
|
{ "freq", "Band, segment and who can transmit", &freq_opts_struct, &hammy_cmd_freq, true }
|
||||||
|
|
||||||
// Tier 0 commands go here as they land. All pure computation, so instant:
|
// Tier 0 commands go here as they land. All pure computation, so instant:
|
||||||
// { "grid", "Maidenhead locator conversions", &grid_opts, &hammy_cmd_grid, true },
|
// { "grid", "Maidenhead locator conversions", &grid_opts, &hammy_cmd_grid, true },
|
||||||
|
|||||||
@@ -28,6 +28,61 @@ static const char* SQL_VERSION =
|
|||||||
static const char* SQL_MORSE =
|
static const char* SQL_MORSE =
|
||||||
"SELECT code FROM morse WHERE character = UPPER(?1) LIMIT 1";
|
"SELECT code FROM morse WHERE character = UPPER(?1) LIMIT 1";
|
||||||
|
|
||||||
|
// Band plus per-class privileges in one pass.
|
||||||
|
//
|
||||||
|
// Both LEFT JOINs matter. They are what makes "Technician: not permitted here"
|
||||||
|
// appear as a row rather than vanishing - a user excluded from a segment needs
|
||||||
|
// to be told so, not shown a shorter list.
|
||||||
|
//
|
||||||
|
// Boundary convention differs between the tables on purpose:
|
||||||
|
// bands inclusive at both ends. 14.350 is still "20m".
|
||||||
|
// band_segments half-open [low, high). 14.150 is the START of the phone
|
||||||
|
// segment, not the end of the CW one. BETWEEN would match
|
||||||
|
// both and the command would print contradictory modes.
|
||||||
|
static const char* SQL_FREQ_MAIN =
|
||||||
|
"SELECT b.name, b.edge_low_hz, b.edge_high_hz,"
|
||||||
|
" lc.code, lc.name, lc.rank,"
|
||||||
|
" s.modes, s.low_hz, s.high_hz, s.max_power_w, s.notes"
|
||||||
|
" FROM bands b"
|
||||||
|
" LEFT JOIN license_classes lc"
|
||||||
|
" ON lc.country = ?2"
|
||||||
|
" LEFT JOIN band_segments s"
|
||||||
|
" ON s.band_id = b.id"
|
||||||
|
" AND s.class_id = lc.id"
|
||||||
|
" AND s.country = ?2"
|
||||||
|
" AND s.low_hz <= ?1"
|
||||||
|
" AND ?1 < s.high_hz"
|
||||||
|
" WHERE b.edge_low_hz <= ?1 AND ?1 <= b.edge_high_hz"
|
||||||
|
" ORDER BY lc.rank DESC, s.low_hz";
|
||||||
|
|
||||||
|
// Regional allocation, independent of national licensing. country = '' and
|
||||||
|
// class_id IS NULL mark these rows: they say what the band IS in a region, not
|
||||||
|
// who may use it.
|
||||||
|
static const char* SQL_FREQ_IARU =
|
||||||
|
"SELECT s.iaru_region, s.low_hz, s.high_hz, s.modes"
|
||||||
|
" FROM band_segments s"
|
||||||
|
" WHERE s.country = '' AND s.class_id IS NULL"
|
||||||
|
" AND s.low_hz <= ?1 AND ?1 < s.high_hz"
|
||||||
|
" ORDER BY s.iaru_region";
|
||||||
|
|
||||||
|
// min() with two arguments is SQLite's scalar min, not the aggregate.
|
||||||
|
static const char* SQL_FREQ_NEAREST =
|
||||||
|
"SELECT name, edge_low_hz, edge_high_hz,"
|
||||||
|
" min(abs(edge_low_hz - ?1), abs(edge_high_hz - ?1))"
|
||||||
|
" FROM bands ORDER BY 4 LIMIT 1";
|
||||||
|
|
||||||
|
// Is the frequency exactly on a segment boundary for this country?
|
||||||
|
static const char* SQL_FREQ_SEG_EDGE =
|
||||||
|
"SELECT 1 FROM band_segments"
|
||||||
|
" WHERE country = ?2 AND (low_hz = ?1 OR high_hz = ?1) LIMIT 1";
|
||||||
|
|
||||||
|
static const char* SQL_countryKnown =
|
||||||
|
"SELECT 1 FROM license_classes WHERE country = ?1 LIMIT 1";
|
||||||
|
|
||||||
|
static const char* SQL_COUNTRY_LIST =
|
||||||
|
"SELECT DISTINCT country FROM license_classes"
|
||||||
|
" WHERE country <> '' ORDER BY country";
|
||||||
|
|
||||||
// Authorizer: the connection may read, and nothing else.
|
// Authorizer: the connection may read, and nothing else.
|
||||||
//
|
//
|
||||||
// SQLITE_OPEN_READONLY protects the MAIN database file. It does not stop
|
// SQLITE_OPEN_READONLY protects the MAIN database file. It does not stop
|
||||||
@@ -300,3 +355,244 @@ bool hammy_refdb_get_morse(hammy_refdb_t* db, char c, const char** out) {
|
|||||||
|
|
||||||
return hit;
|
return hit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Frequency parsing
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Copies a TEXT column into a fixed buffer, tolerating NULL.
|
||||||
|
static void copy_text(sqlite3_stmt* st, int col, char* dst, size_t cap) {
|
||||||
|
const unsigned char* v = sqlite3_column_text(st, col);
|
||||||
|
|
||||||
|
snprintf(dst, cap, "%s", v ? (const char*)v : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hammy_freq_parse(const char* text, int64_t* outHz) {
|
||||||
|
if (!text || !outHz) { return false; }
|
||||||
|
|
||||||
|
while (*text == ' ' || *text == '\t') { text++; }
|
||||||
|
|
||||||
|
// Integer part.
|
||||||
|
int64_t whole = 0;
|
||||||
|
bool anyDigit = false;
|
||||||
|
|
||||||
|
while (*text >= '0' && *text <= '9') {
|
||||||
|
if (whole > INT64_MAX / 10 - 9) { return false; } // absurd input
|
||||||
|
whole = whole * 10 + (*text - '0');
|
||||||
|
anyDigit = true;
|
||||||
|
text++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fractional part, accumulated as digits rather than a double. Six decimal
|
||||||
|
// places of MHz is exactly 1 Hz, so anything beyond that is discarded.
|
||||||
|
int64_t frac = 0;
|
||||||
|
int fracDigits = 0;
|
||||||
|
|
||||||
|
if (*text == '.' || *text == ',') {
|
||||||
|
text++;
|
||||||
|
|
||||||
|
while (*text >= '0' && *text <= '9') {
|
||||||
|
if (fracDigits < 9) {
|
||||||
|
frac = frac * 10 + (*text - '0');
|
||||||
|
fracDigits++;
|
||||||
|
}
|
||||||
|
anyDigit = true;
|
||||||
|
text++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!anyDigit) { return false; }
|
||||||
|
|
||||||
|
while (*text == ' ' || *text == '\t') { text++; }
|
||||||
|
|
||||||
|
// Unit. A bare number means MHz, which is what people type.
|
||||||
|
int64_t mult = 1000000; // Hz per unit
|
||||||
|
|
||||||
|
if (*text) {
|
||||||
|
char unit[8] = {0};
|
||||||
|
size_t i = 0;
|
||||||
|
|
||||||
|
while (text[i] && i + 1 < sizeof(unit) && text[i] != ' ') {
|
||||||
|
char c = text[i];
|
||||||
|
unit[i] = (c >= 'A' && c <= 'Z') ? (char)(c - 'A' + 'a') : c;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!strcmp(unit, "hz")) { mult = 1; }
|
||||||
|
else if (!strcmp(unit, "khz") || !strcmp(unit, "k")) { mult = 1000; }
|
||||||
|
else if (!strcmp(unit, "mhz") || !strcmp(unit, "m")) { mult = 1000000; }
|
||||||
|
else if (!strcmp(unit, "ghz") || !strcmp(unit, "g")) { mult = 1000000000; }
|
||||||
|
else { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale the fraction to the unit without ever touching a double.
|
||||||
|
int64_t scale = 1;
|
||||||
|
for (int i = 0; i < fracDigits; i++) {
|
||||||
|
if (scale > INT64_MAX / 10) { return false; }
|
||||||
|
scale *= 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (whole > INT64_MAX / mult) { return false; }
|
||||||
|
|
||||||
|
int64_t hz = whole * mult + (frac * mult) / scale;
|
||||||
|
|
||||||
|
if (hz <= 0 || hz > 300000000000LL) { return false; } // 1 Hz .. 300 GHz
|
||||||
|
|
||||||
|
*outHz = hz;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Frequency lookup
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
static void normalise_country(const char* in, char* out, size_t cap) {
|
||||||
|
size_t j = 0;
|
||||||
|
|
||||||
|
if (!in || !*in) {
|
||||||
|
snprintf(out, cap, "US"); // default
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; in[i] && j + 1 < cap; i++) {
|
||||||
|
char c = in[i];
|
||||||
|
|
||||||
|
if (c >= 'a' && c <= 'z') { c = (char)(c - 'a' + 'A'); }
|
||||||
|
if (c >= 'A' && c <= 'Z') { out[j++] = c; }
|
||||||
|
}
|
||||||
|
|
||||||
|
out[j] = '\0';
|
||||||
|
|
||||||
|
if (!out[0]) { snprintf(out, cap, "US"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool step_bool(sqlite3_stmt* st) {
|
||||||
|
bool got = (sqlite3_step(st) == SQLITE_ROW);
|
||||||
|
sqlite3_reset(st);
|
||||||
|
|
||||||
|
return got;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hammy_refdb_freq(hammy_refdb_t* db, int64_t freqHz, const char* country,
|
||||||
|
hammy_freq_t* out) {
|
||||||
|
if (!db || !out || freqHz <= 0) { return false; }
|
||||||
|
|
||||||
|
memset(out, 0, sizeof(*out));
|
||||||
|
out->freqHz = freqHz;
|
||||||
|
normalise_country(country, out->country, sizeof(out->country));
|
||||||
|
|
||||||
|
// Does the bundle know this country at all? Only US privileges are seeded so
|
||||||
|
// far, so this is the common path and the message matters.
|
||||||
|
sqlite3_reset(db->stCountryKnown);
|
||||||
|
sqlite3_clear_bindings(db->stCountryKnown);
|
||||||
|
sqlite3_bind_text(db->stCountryKnown, 1, out->country, -1, SQLITE_TRANSIENT);
|
||||||
|
out->countryKnown = step_bool(db->stCountryKnown);
|
||||||
|
|
||||||
|
// Band and privileges.
|
||||||
|
sqlite3_stmt* st = db->stFreqMain;
|
||||||
|
sqlite3_reset(st);
|
||||||
|
sqlite3_clear_bindings(st);
|
||||||
|
sqlite3_bind_int64(st, 1, freqHz);
|
||||||
|
sqlite3_bind_text(st, 2, out->country, -1, SQLITE_TRANSIENT);
|
||||||
|
|
||||||
|
while (sqlite3_step(st) == SQLITE_ROW) {
|
||||||
|
if (!out->inBand) {
|
||||||
|
out->inBand = true;
|
||||||
|
copy_text(st, 0, out->band, sizeof(out->band));
|
||||||
|
out->bandLowHz = sqlite3_column_int64(st, 1);
|
||||||
|
out->bandHighHz = sqlite3_column_int64(st, 2);
|
||||||
|
out->atBandEdge = (freqHz == out->bandLowHz ||
|
||||||
|
freqHz == out->bandHighHz);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A NULL class means the country has no licence classes at all; the row
|
||||||
|
// still carried the band, which is why it is read above first.
|
||||||
|
if (sqlite3_column_type(st, 3) == SQLITE_NULL) { continue; }
|
||||||
|
if (out->nPrivs >= HAMMY_FREQ_PRIVS_MAX) { continue; }
|
||||||
|
|
||||||
|
hammy_freq_priv_t* p = &out->privs[out->nPrivs++];
|
||||||
|
|
||||||
|
copy_text(st, 3, p->code, sizeof(p->code));
|
||||||
|
copy_text(st, 4, p->name, sizeof(p->name));
|
||||||
|
p->rank = sqlite3_column_int(st, 5);
|
||||||
|
|
||||||
|
p->permitted = (sqlite3_column_type(st, 6) != SQLITE_NULL);
|
||||||
|
|
||||||
|
if (p->permitted) {
|
||||||
|
copy_text(st, 6, p->modes, sizeof(p->modes));
|
||||||
|
p->segLowHz = sqlite3_column_int64(st, 7);
|
||||||
|
p->segHighHz = sqlite3_column_int64(st, 8);
|
||||||
|
p->maxPowerW = sqlite3_column_int(st, 9); // 0 when NULL
|
||||||
|
copy_text(st, 10, p->notes, sizeof(p->notes));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_reset(st);
|
||||||
|
|
||||||
|
// Not in any band: report the nearest one so the user can see how far off
|
||||||
|
// they are. Usually a typo - 15.000 instead of 14.150.
|
||||||
|
if (!out->inBand) {
|
||||||
|
st = db->stFreqNearest;
|
||||||
|
sqlite3_reset(st);
|
||||||
|
sqlite3_clear_bindings(st);
|
||||||
|
sqlite3_bind_int64(st, 1, freqHz);
|
||||||
|
|
||||||
|
if (sqlite3_step(st) == SQLITE_ROW) {
|
||||||
|
copy_text(st, 0, out->nearestBand, sizeof(out->nearestBand));
|
||||||
|
out->nearestLowHz = sqlite3_column_int64(st, 1);
|
||||||
|
out->nearestHighHz = sqlite3_column_int64(st, 2);
|
||||||
|
out->nearestDistanceHz = sqlite3_column_int64(st, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_reset(st);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// IARU regional allocations. Independent of country, so worth showing even
|
||||||
|
// when the privilege table is empty.
|
||||||
|
st = db->stFreqIaru;
|
||||||
|
sqlite3_reset(st);
|
||||||
|
sqlite3_clear_bindings(st);
|
||||||
|
sqlite3_bind_int64(st, 1, freqHz);
|
||||||
|
|
||||||
|
while (sqlite3_step(st) == SQLITE_ROW && out->nIaru < HAMMY_FREQ_IARU_MAX) {
|
||||||
|
hammy_freq_iaru_t* r = &out->iaru[out->nIaru++];
|
||||||
|
|
||||||
|
r->region = sqlite3_column_int(st, 0);
|
||||||
|
r->lowHz = sqlite3_column_int64(st, 1);
|
||||||
|
r->highHz = sqlite3_column_int64(st, 2);
|
||||||
|
copy_text(st, 3, r->modes, sizeof(r->modes));
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_reset(st);
|
||||||
|
|
||||||
|
// Sitting exactly on a segment boundary is worth flagging: a signal of any
|
||||||
|
// width centred there straddles both sides.
|
||||||
|
st = db->stFreqSegEdge;
|
||||||
|
sqlite3_reset(st);
|
||||||
|
sqlite3_clear_bindings(st);
|
||||||
|
sqlite3_bind_int64(st, 1, freqHz);
|
||||||
|
sqlite3_bind_text(st, 2, out->country, -1, SQLITE_TRANSIENT);
|
||||||
|
out->atSegmentEdge = step_bool(st);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t hammy_refdb_countries(hammy_refdb_t* db,
|
||||||
|
char out[][HAMMY_COUNTRY_MAX], size_t cap) {
|
||||||
|
if (!db || !out || cap == 0) return 0;
|
||||||
|
|
||||||
|
size_t n = 0;
|
||||||
|
|
||||||
|
sqlite3_reset(db->stCountryList);
|
||||||
|
|
||||||
|
while (n < cap && sqlite3_step(db->stCountryList) == SQLITE_ROW) {
|
||||||
|
copy_text(db->stCountryList, 0, out[n++], HAMMY_COUNTRY_MAX);
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlite3_reset(db->stCountryList);
|
||||||
|
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|||||||
Vendored
+1122
-2
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user