Compare commits

..
11 Commits
21 changed files with 663 additions and 52 deletions
+1
View File
@@ -1,5 +1,6 @@
.env .env
build/ build/
build*/
.DS_Store .DS_Store
config.json config.json
.vscode .vscode
+10 -1
View File
@@ -246,7 +246,9 @@ else()
hammy_append_supported_c_flags(HAMMY_ANALYZER_FLAGS hammy_append_supported_c_flags(HAMMY_ANALYZER_FLAGS
-fanalyzer-verbosity=${ANALYZER_VERBOSITY} -fanalyzer-verbosity=${ANALYZER_VERBOSITY}
-fanalyzer -fanalyzer
) --param=analyzer-checker=taint
--Wanalyzer-too-complex
)
endif() endif()
elseif(CMAKE_C_COMPILER_ID MATCHES "Clang") elseif(CMAKE_C_COMPILER_ID MATCHES "Clang")
# Clang-only diagnostics. Clang has no in-compiler equivalent of # Clang-only diagnostics. Clang has no in-compiler equivalent of
@@ -413,6 +415,8 @@ else()
OUTPUT "${CONCORD_INSTALL_SCRIPT}" OUTPUT "${CONCORD_INSTALL_SCRIPT}"
@ONLY @ONLY
CONTENT [[ CONTENT [[
cmake_policy(SET CMP0057 NEW)
file(MAKE_DIRECTORY "@CONCORD_INCLUDE_DIR@/concord" "@CONCORD_PREFIX@/lib") file(MAKE_DIRECTORY "@CONCORD_INCLUDE_DIR@/concord" "@CONCORD_PREFIX@/lib")
# Upstream flattens all three header directories into one include/concord. Check # Upstream flattens all three header directories into one include/concord. Check
@@ -594,6 +598,11 @@ target_include_directories(hammy SYSTEM PRIVATE
# warning set and no second copy under third_party/ is needed. # warning set and no second copy under third_party/ is needed.
target_link_libraries(hammy PRIVATE concord::concord SQLite::SQLite3) target_link_libraries(hammy PRIVATE concord::concord SQLite::SQLite3)
if (NOT MSVC)
target_link_libraries(hammy PRIVATE m)
endif()
if(HAMMY_CONCORD_EXTERNAL) if(HAMMY_CONCORD_EXTERNAL)
add_dependencies(hammy ${HAMMY_CONCORD_EXTERNAL}) add_dependencies(hammy ${HAMMY_CONCORD_EXTERNAL})
endif() endif()
+9
View File
@@ -4,6 +4,15 @@ A Discord Bot for Ham/Amateur Radio
Fun fact: The logo of Hammy is a function! It is: f(x) = e^(-x^2)sin(9x) Fun fact: The logo of Hammy is a function! It is: f(x) = e^(-x^2)sin(9x)
## Features ## Features
Current features:
- /freq <frequency (MHz)> [country (default: US)] - Show the band, segment and who can transmit (for that country).
- /abbr <abbreviation> - Convert Abbreviation to Meaning and Context of Meaning.
- /q <q-code> - Convert Q-Code to Question/Answer.
- /phonetic <text> - Convert Text to Phonetics (useful for callsigns); If text is under 12 characters, also shows pronunciaction.
- /morse <text> - Convert Text to Morse Code.
- /dxcc <callsign> [grid (maidenhead locator)] - Show the DXCC Entity of a Callsign.
- /ping - Ping the Bot.
Currently in-development, many features planned! Currently in-development, many features planned!
## Architecture ## Architecture
+16
View File
@@ -0,0 +1,16 @@
// commands.h
#ifndef HAMMY_COMMANDS_H
#define HAMMY_COMMANDS_H
#include <concord/discord.h>
#include <hammy/types.h>
void hammy_cmd_ping(const hammy_job_t* job, struct discord* client, hammy_refdb_t* refdb); // Ping
void hammy_cmd_morse(const hammy_job_t* job, struct discord* client, hammy_refdb_t* refdb); // Text -> Morse
void hammy_cmd_freq(const hammy_job_t* job, struct discord* client, hammy_refdb_t* refdb); // Frequency lookup
void hammy_cmd_q(const hammy_job_t* job, struct discord* client, hammy_refdb_t* refdb); // Q-Code lookup
void hammy_cmd_abbr(const hammy_job_t* job, struct discord* client, hammy_refdb_t* refdb); // Abbreviation lookup
void hammy_cmd_phonetic(const hammy_job_t* job, struct discord* client, hammy_refdb_t* refdb); // Callsign -> Phonetics
void hammy_cmd_dxcc(const hammy_job_t* job, struct discord* client, hammy_refdb_t* refdb); // Callsign + Grid -> DXCC
#endif
+13 -13
View File
@@ -8,11 +8,11 @@
static inline bool hammy_embeds_genericerror(struct discord *client, struct discord_embed *out) { static inline bool hammy_embeds_genericerror(struct discord *client, struct discord_embed *out) {
if (!out || !client) { return false; } if (!out || !client) { return false; }
static struct discord_embed_footer footer = { .text = "Hammy Bot" }; static struct discord_embed_footer footer = { .text = (char*)"Hammy Bot" };
out[0] = (struct discord_embed){ out[0] = (struct discord_embed){
.title = "An Error Occurred", .title = (char*)"An Error Occurred",
.description = "An error occurred while processing your request.", .description = (char*)"An error occurred while processing your request.",
.color = 0xFF0000, .color = 0xFF0000,
.timestamp = discord_timestamp(client), .timestamp = discord_timestamp(client),
.footer = &footer, .footer = &footer,
@@ -26,8 +26,8 @@ static inline bool hammy_embeds_genericerror(struct discord *client, struct disc
static inline bool hammy_embeds_customerror(struct discord *client, static inline bool hammy_embeds_customerror(struct discord *client,
struct discord_embed *out, struct discord_embed *out,
struct discord_embed_fields *fieldsOut, struct discord_embed_fields *fieldsOut,
char *title, const char *title,
char *description, const char *description,
struct discord_embed_field *fields, struct discord_embed_field *fields,
int fieldCount) int fieldCount)
{ {
@@ -35,7 +35,7 @@ static inline bool hammy_embeds_customerror(struct discord *client,
if (fieldCount < 0 || (fieldCount > 0 && !fields)) { return false; } if (fieldCount < 0 || (fieldCount > 0 && !fields)) { return false; }
if (fieldCount > 0 && !fieldsOut) { return false; } if (fieldCount > 0 && !fieldsOut) { return false; }
static struct discord_embed_footer footer = { .text = "Hammy Bot" }; static struct discord_embed_footer footer = { .text = (char*)"Hammy Bot" };
if (fieldCount > 0) { if (fieldCount > 0) {
*fieldsOut = (struct discord_embed_fields){ *fieldsOut = (struct discord_embed_fields){
@@ -45,8 +45,8 @@ static inline bool hammy_embeds_customerror(struct discord *client,
} }
out[0] = (struct discord_embed){ out[0] = (struct discord_embed){
.title = title, .title = (char*)title,
.description = description, .description = (char*)description,
.color = 0xFF0000, .color = 0xFF0000,
.timestamp = discord_timestamp(client), .timestamp = discord_timestamp(client),
.footer = &footer, .footer = &footer,
@@ -61,8 +61,8 @@ static inline bool hammy_embeds_customerror(struct discord *client,
static inline bool hammy_embeds_customembed(struct discord *client, static inline bool hammy_embeds_customembed(struct discord *client,
struct discord_embed *out, struct discord_embed *out,
struct discord_embed_fields *fieldsOut, struct discord_embed_fields *fieldsOut,
char *title, const char *title,
char *description, const char *description,
struct discord_embed_field *fields, struct discord_embed_field *fields,
int fieldCount, int fieldCount,
int color) int color)
@@ -71,7 +71,7 @@ static inline bool hammy_embeds_customembed(struct discord *client,
if (fieldCount < 0 || (fieldCount > 0 && !fields)) { return false; } if (fieldCount < 0 || (fieldCount > 0 && !fields)) { return false; }
if (fieldCount > 0 && !fieldsOut) { return false; } if (fieldCount > 0 && !fieldsOut) { return false; }
static struct discord_embed_footer footer = { .text = "Hammy Bot" }; static struct discord_embed_footer footer = { .text = (char*)"Hammy Bot" };
if (fieldCount > 0) { if (fieldCount > 0) {
*fieldsOut = (struct discord_embed_fields){ *fieldsOut = (struct discord_embed_fields){
@@ -81,8 +81,8 @@ static inline bool hammy_embeds_customembed(struct discord *client,
} }
out[0] = (struct discord_embed){ out[0] = (struct discord_embed){
.title = title, .title = (char*)title,
.description = description, .description = (char*)description,
.color = color, .color = color,
.timestamp = discord_timestamp(client), .timestamp = discord_timestamp(client),
.footer = &footer, .footer = &footer,
+43
View File
@@ -0,0 +1,43 @@
#ifndef HAMMY_GEO_H
#define HAMMY_GEO_H
#include <stdbool.h>
#include <stddef.h>
// Maidenhead locators and great-circle maths. No dependencies beyond libm, no
// state, no database - safe to call from any thread.
//
// Lives in its own module rather than utils.h because /grid, /beacon and /sat
// will all want it, and mixing coordinate maths in with string helpers makes
// both harder to find.
// 8 characters plus NUL. Longer locators exist in theory; nothing uses them.
#define HAMMY_GRID_MAX 9
// Writes a Maidenhead locator for the given coordinates. precision must be
// 2 (field), 4 (square), 6 (subsquare) or 8 (extended square); 6 is what people
// quote. Returns false on out-of-range coordinates, bad precision, or too small
// a buffer.
bool hammy_grid_from_latlon(double lat, double lon, int precision, char* out, size_t cap);
// Parses a 2, 4, 6 or 8 character locator. Case-insensitive, ignores embedded
// whitespace, rejects anything longer rather than truncating it.
//
// Returns the CENTER of the square, not its south-west corner: using the corner
// biases every distance by up to half a square, which at 4-character precision
// is about 60 km.
bool hammy_grid_to_latlon(const char* grid, double* outLat, double* outLon);
// Haversine distance in km and initial bearing in degrees true. Either output
// pointer may be NULL.
void hammy_great_circle(double lat1, double lon1, double lat2, double lon2,
double* outDistKm, double* outBearingDeg);
// The other way round the planet.
double hammy_long_path_km(double shortPathKm);
double hammy_reciprocal_bearing(double bearingDeg);
// 16-point compass abbreviation ("NNE"). Points at static storage.
const char* hammy_compass_point(double bearingDeg);
#endif
+18 -3
View File
@@ -27,6 +27,10 @@
// The current longest string in the qcodes table is 42; 128 is pretty generous. TODO: Change this if the qcodes table ever changes // The current longest string in the qcodes table is 42; 128 is pretty generous. TODO: Change this if the qcodes table ever changes
#define HAMMY_QCODE_MAX 128 #define HAMMY_QCODE_MAX 128
// Abbreviations
#define HAMMY_ABBR_LONGEST_STR 96
#define HAMMY_ABBR_LONGEST_CTX 16
// A country has at most a handful of licence classes, each with at most a // 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. // couple of segments covering one exact frequency. 24 is generous.
#define HAMMY_FREQ_PRIVS_MAX 24 #define HAMMY_FREQ_PRIVS_MAX 24
@@ -41,6 +45,7 @@ struct hammy_refdb_t {
sqlite3_stmt* stMorse; sqlite3_stmt* stMorse;
sqlite3_stmt* stQCode; sqlite3_stmt* stQCode;
sqlite3_stmt* stPhonetic; sqlite3_stmt* stPhonetic;
sqlite3_stmt* stAbbr;
sqlite3_stmt* stFreqMain; sqlite3_stmt* stFreqMain;
sqlite3_stmt* stFreqIaru; sqlite3_stmt* stFreqIaru;
sqlite3_stmt* stFreqNearest; sqlite3_stmt* stFreqNearest;
@@ -53,6 +58,9 @@ struct hammy_refdb_t {
char phoneticCode[HAMMY_PHONETIC_MAX]; char phoneticCode[HAMMY_PHONETIC_MAX];
char phoneticCodePronunciation[HAMMY_PHONETIC_MAX]; char phoneticCodePronunciation[HAMMY_PHONETIC_MAX];
char abbrStr[HAMMY_ABBR_LONGEST_STR];
char abbrCtx[HAMMY_ABBR_LONGEST_CTX];
char qcodeQuestion[HAMMY_QCODE_MAX]; // Scratch for the last hammy_refdb_get_qcode() hit char qcodeQuestion[HAMMY_QCODE_MAX]; // Scratch for the last hammy_refdb_get_qcode() hit
char qcodeAnswer[HAMMY_QCODE_MAX]; // Scratch for the last hammy_refdb_get_qcode() hit char qcodeAnswer[HAMMY_QCODE_MAX]; // Scratch for the last hammy_refdb_get_qcode() hit
@@ -103,7 +111,7 @@ struct hammy_freq_t {
int64_t bandHighHz; int64_t bandHighHz;
// The frequency sits exactly on a band or segment boundary. Worth saying // The frequency sits exactly on a band or segment boundary. Worth saying
// out loud: a signal of any width centred there straddles both sides. // out loud: a signal of any width centerd there straddles both sides.
bool atBandEdge; bool atBandEdge;
bool atSegmentEdge; bool atSegmentEdge;
@@ -162,19 +170,26 @@ bool hammy_refdb_dxcc(hammy_refdb_t* db, const char* callsign, hammy_dxcc_t* out
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);
// Looks up a character in the Phonetic table. Case-insensitive; non-ASCII bytes // Looks up a character in the Phonetic table. Case-insensitive; non-ASCII bytes
// never match. Returns false if not found, leaving *out untouched. // never match. Returns false if not found, leaving *out(s) untouched.
// //
// 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_phonetic(hammy_refdb_t* db, char c, const char** out, const char** outPronunciation); bool hammy_refdb_get_phonetic(hammy_refdb_t* db, char c, const char** out, const char** outPronunciation);
// Looks up a QSO code in the qcodes table. Case-insensitive; non-ASCII bytes // Looks up a QSO code in the qcodes table. Case-insensitive; non-ASCII bytes
// never match. Returns false if not found, leaving *out untouched. // never match. Returns false if not found, leaving *out(s) untouched.
// //
// On a hit *out points at the storage owned by refdb and is only valid until // On a hit *out points at the storage owned by 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_qcode(hammy_refdb_t* db, const char* code, const char** outQuestion, const char** outAnswer); bool hammy_refdb_get_qcode(hammy_refdb_t* db, const char* code, const char** outQuestion, const char** outAnswer);
// Looks up an abbreviation in the abbreviations table. Case-insensitive; non-ASCII bytes
// never match. Returns false if not found, leaving *out(s) untouched.
//
// On a hit *out points at the storage owned by refdb and is only valid until
// the NEXT call on the same handle - copy it if it has to outlive that.
bool hammy_refdb_get_abbr(hammy_refdb_t* db, const char* code, const char** outStr, const char** outContext);
// What band is freq_hz in, and who may transmit there. // 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 // country is an ISO 3166-1 alpha-2 code; NULL or empty means "US". Always
+1 -1
View File
@@ -86,7 +86,7 @@ SELECT v.band_id, 'US', lc.id, v.low_hz, v.high_hz, v.modes, v.max_power_w, v.no
WHERE lc.country = 'US' AND lc.rank >= 30; WHERE lc.country = 'US' AND lc.rank >= 30;
-- 60m: five discrete channels, General and above. Stored as channel-width -- 60m: five discrete channels, General and above. Stored as channel-width
-- ranges; the USB dial frequency is the channel centre minus 1.5 kHz. -- ranges; the USB dial frequency is the channel center minus 1.5 kHz.
INSERT INTO band_segments (band_id, country, class_id, low_hz, high_hz, modes, max_power_w, notes) INSERT INTO band_segments (band_id, country, class_id, low_hz, high_hz, modes, max_power_w, notes)
SELECT 5, 'US', lc.id, v.low_hz, v.high_hz, 'CW,DATA,PHONE', NULL, v.notes SELECT 5, 'US', lc.id, v.low_hz, v.high_hz, 'CW,DATA,PHONE', NULL, v.notes
FROM license_classes lc FROM license_classes lc
+40
View File
@@ -0,0 +1,40 @@
#include <concord/discord.h>
#include <inttypes.h>
#include <stdio.h>
#include <string.h>
#include <hammy/command.h>
#include <hammy/commands.h>
#include <hammy/job.h>
#include <hammy/refdb.h>
#include <hammy/utils.h>
#define HAMMY_ABBR_ECHO_MAX 16
#define HAMMY_ABBR_CODE_MAX 3072
#define HAMMY_ABBR_TRUNCATED " ... (truncated)"
void hammy_cmd_abbr(const hammy_job_t* job, struct discord* client, hammy_refdb_t* refdb) {
// Get the qcode argument from the job
const char* abbr = hammy_job_get_arg(job, "abbreviation");
if (!abbr) {
hammy_job_respond(job, client, "Error", "No Abbreviation provided for Abbreviation conversion.", true);
return;
}
char body[HAMMY_ABBR_CODE_MAX + sizeof(HAMMY_ABBR_TRUNCATED)]; // Generally, since the bottom pointers can only point to max 128-character strings (set as preprocesor header)
// So yeah - if we ever change the above for some reason to stupid values... yeah. Technically "unsafe" but yeah.
const char* str = NULL;
const char* context = NULL;
if (!hammy_refdb_get_abbr(refdb, abbr, &str, &context)) {
hammy_job_respond(job, client, "Abbreviation Not Found!", "Failed to find the Abbreviation specified. Please check your query!", true);
return;
}
hammy_to_uppercase((char*)abbr);
snprintf(body, sizeof(body), "Abbreviation: `%s`\nMeaning: `%s`\nUsage Context: `%s`", abbr, (str ? str : "Not Specified"), (context ? context : "Not Specified"));
hammy_job_respond(job, client, "Abbreviation Conversion", body, false);
}
+191
View File
@@ -0,0 +1,191 @@
#include <concord/discord.h>
#include <inttypes.h>
#include <math.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <hammy/command.h>
#include <hammy/commands.h>
#include <hammy/geo.h>
#include <hammy/job.h>
#include <hammy/refdb.h>
#include <hammy/utils.h>
#define HAMMY_DXCC_BODY_MAX 2048
#define HAMMY_DXCC_TITLE_MAX 128
// Formats a signed decimal degree as "38.90 N" / "77.04 W".
static void hammy_dxcc_fmt_coord(double value, bool isLatitude, char* out, size_t cap) {
char hemi = isLatitude ? (value >= 0.0 ? 'N' : 'S')
: (value >= 0.0 ? 'E' : 'W');
snprintf(out, cap, "%.2f %c", fabs(value), hemi);
}
// Formats a UTC offset as "UTC+9", "UTC-5", "UTC+5:30". cty.dat carries half
// hour offsets for a handful of entities, so the fractional case is real.
static void hammy_dxcc_fmt_offset(double offset, char* out, size_t cap) {
int totalMinutes = (int)lround(offset * 60.0);
char sign = totalMinutes < 0 ? '-' : '+';
if (totalMinutes < 0) { totalMinutes = -totalMinutes; }
int hours = totalMinutes / 60;
int minutes = totalMinutes % 60;
if (minutes) {
snprintf(out, cap, "UTC%c%d:%02d", sign, hours, minutes);
} else {
snprintf(out, cap, "UTC%c%d", sign, hours);
}
}
// Wall-clock time at the entity. cty.dat gives STANDARD offsets with no notion
// of summer time, so this can be an hour out for part of the year. Said so in
// the output rather than quietly presenting it as exact.
static void hammy_dxcc_fmt_local_time(double offset, char* out, size_t cap) {
time_t now = time(NULL);
if (now == (time_t)-1) {
snprintf(out, cap, "unknown");
return;
}
time_t shifted = now + (time_t)lround(offset * 3600.0);
struct tm tmBuf;
// gmtime_r rather than gmtime: instant commands run on the gateway thread
// and deferred ones on a worker, and gmtime's static buffer is shared.
if (!gmtime_r(&shifted, &tmBuf)) {
snprintf(out, cap, "unknown");
return;
}
snprintf(out, cap, "%02d:%02d", tmBuf.tm_hour, tmBuf.tm_min);
}
// Instant command: runs on the gateway thread, sends a fresh response.
void hammy_cmd_dxcc(const hammy_job_t* job, struct discord* client, hammy_refdb_t* refdb) {
if (!refdb) {
hammy_job_respond(job, client, "Error", "Reference data is unavailable. Please try again later.", true);
return;
}
const char* callsign = hammy_job_get_arg(job, "callsign");
if (!callsign || !*callsign) {
hammy_job_respond(job, client, "Error", "No callsign provided for lookup.", true);
return;
}
hammy_dxcc_t entity;
if (!hammy_refdb_dxcc(refdb, callsign, &entity)) {
char body[HAMMY_DXCC_BODY_MAX];
snprintf(body, sizeof(body),
"No DXCC entity matches `%s`.\n\n"
"Prefixes are matched longest-first, so this usually means a typo "
"or a prefix that has never been allocated.", callsign);
hammy_job_respond(job, client, "Entity Not Found!", body, true);
return;
}
char gridStr[HAMMY_GRID_MAX] = "unknown";
bool haveEntityGrid = hammy_grid_from_latlon(entity.latitude, entity.longitude, 6,
gridStr, sizeof(gridStr));
char latStr[24];
char lonStr[24];
hammy_dxcc_fmt_coord(entity.latitude, true, latStr, sizeof(latStr));
hammy_dxcc_fmt_coord(entity.longitude, false, lonStr, sizeof(lonStr));
char offsetStr[24];
char timeStr[16];
hammy_dxcc_fmt_offset(entity.utcOffset, offsetStr, sizeof(offsetStr));
hammy_dxcc_fmt_local_time(entity.utcOffset, timeStr, sizeof(timeStr));
char title[HAMMY_DXCC_TITLE_MAX];
char upperCallsign[HAMMY_CALLSIGN_MAX];
snprintf(upperCallsign, sizeof(upperCallsign), "%s", callsign);
hammy_to_uppercase(upperCallsign);
snprintf(title, sizeof(title), "%s - %s", upperCallsign, entity.name);
char body[HAMMY_DXCC_BODY_MAX];
int len = 0;
len += snprintf(body + len, sizeof(body) - (size_t)len,
"Entity: `%s` (DXCC `%d`)\n"
"Continent: `%s` CQ zone: `%d` ITU zone: `%d`\n",
entity.name, entity.entityId, entity.continent,
entity.cqZone, entity.ituZone);
if (haveEntityGrid) {
len += snprintf(body + len, sizeof(body) - (size_t)len,
"Location: `%s` (%s, %s)\n", gridStr, latStr, lonStr);
} else {
len += snprintf(body + len, sizeof(body) - (size_t)len,
"Location: %s, %s\n", latStr, lonStr);
}
len += snprintf(body + len, sizeof(body) - (size_t)len,
"Local time: `%s` (`%s`, standard time)\n", timeStr, offsetStr);
// Saying WHICH rule matched matters more than it looks. An exact hit means
// cty.dat carries a whole-callsign override, and a short prefix hit on a
// long callsign is a hint that the entity guess may be loose.
if (entity.exact) {
len += snprintf(body + len, sizeof(body) - (size_t)len,
"Matched: exact callsign rule `%s`\n", entity.matchedPrefix);
} else {
len += snprintf(body + len, sizeof(body) - (size_t)len,
"Matched: prefix `%s`\n", entity.matchedPrefix);
}
// Optional second argument: the asker's own locator, which turns this from
// trivia into something you can point an antenna with.
const char* fromGrid = hammy_job_get_arg(job, "grid");
if (fromGrid && *fromGrid) {
double fromLat = 0.0;
double fromLon = 0.0;
if (!hammy_grid_to_latlon(fromGrid, &fromLat, &fromLon)) {
len += snprintf(body + len, sizeof(body) - (size_t)len,
"\nCould not read `%s` as a Maidenhead locator. "
"Try something like `JN76` or `JN76gb`.\n", fromGrid);
} else {
double distKm = 0.0;
double bearing = 0.0;
hammy_great_circle(fromLat, fromLon, entity.latitude, entity.longitude,
&distKm, &bearing);
double longKm = hammy_long_path_km(distKm);
double longBearing = hammy_reciprocal_bearing(bearing);
char fromNorm[HAMMY_GRID_MAX];
if (!hammy_grid_from_latlon(fromLat, fromLon, 6, fromNorm, sizeof(fromNorm))) {
snprintf(fromNorm, sizeof(fromNorm), "%s", fromGrid);
}
len += snprintf(body + len, sizeof(body) - (size_t)len,
"\n**From %s**\n"
"Short path: `%.0f km` (`%.0f mi`) bearing `%.0f` (%s)\n"
"Long path: `%.0f km` bearing `%.0f` (%s)\n",
fromNorm,
distKm, distKm * 0.621371, bearing, hammy_compass_point(bearing),
longKm, longBearing, hammy_compass_point(longBearing));
}
}
// The entity coordinates are a nominal center for the whole entity, not the
// station's own position. For the US that is a point in Washington DC, so a
// bearing to a California station will be well off. Worth admitting.
len += snprintf(body + len, sizeof(body) - (size_t)len,
"\n-# Coordinates are the entity's nominal center, not the "
"station's actual location.");
(void)len;
hammy_job_respond(job, client, title, body, false);
}
+6 -2
View File
@@ -7,6 +7,7 @@
#include <stdarg.h> #include <stdarg.h>
#include <hammy/command.h> #include <hammy/command.h>
#include <hammy/commands.h>
#include <hammy/job.h> #include <hammy/job.h>
#include <hammy/refdb.h> #include <hammy/refdb.h>
@@ -29,7 +30,10 @@ static void sb_addf(hammy_sb_t* sb, const char* fmt, ...) {
va_list ap; va_list ap;
va_start(ap, fmt); va_start(ap, fmt);
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wformat-nonliteral"
int n = vsnprintf(sb->buf + sb->len, sb->cap - sb->len, fmt, ap); int n = vsnprintf(sb->buf + sb->len, sb->cap - sb->len, fmt, ap);
#pragma clang diagnostic pop
va_end(ap); va_end(ap);
if (n < 0) { if (n < 0) {
@@ -191,9 +195,9 @@ void hammy_cmd_freq(const hammy_job_t* job, struct discord* client, hammy_refdb_
// Boundary warnings // Boundary warnings
if (r.atBandEdge) { if (r.atBandEdge) {
sb_addf(&sb, "\n> This is exactly the band edge. A signal of any width centred here extends outside the band.\n"); sb_addf(&sb, "\n> This is exactly the band edge. A signal of any width centerd here extends outside the band.\n");
} else if (r.atSegmentEdge) { } else if (r.atSegmentEdge) {
sb_addf(&sb, "\n> This is exactly a segment boundary, so a signal centred here straddles both sides.\n"); sb_addf(&sb, "\n> This is exactly a segment boundary, so a signal centerd here straddles both sides.\n");
} }
if (sb.overflow) { if (sb.overflow) {
+2 -1
View File
@@ -4,6 +4,7 @@
#include <string.h> #include <string.h>
#include <hammy/command.h> #include <hammy/command.h>
#include <hammy/commands.h>
#include <hammy/job.h> #include <hammy/job.h>
#include <hammy/refdb.h> #include <hammy/refdb.h>
@@ -28,7 +29,7 @@
// invalid JSON. // invalid JSON.
static bool morse_append(char* buf, size_t cap, size_t* len, bool gap, const char* token) { static bool morse_append(char* buf, size_t cap, size_t* len, bool gap, const char* token) {
size_t n = strlen(token); size_t n = strlen(token);
size_t need = (*len > 0 ? 1 : 0) + (gap ? 2 : 0) + n; size_t need = (*len > 0 ? 1u : 0u) + (gap ? 2u : 0u) + n;
if (*len + need + 1 > cap) { return false; } if (*len + need + 1 > cap) { return false; }
+16 -9
View File
@@ -4,6 +4,7 @@
#include <string.h> #include <string.h>
#include <hammy/command.h> #include <hammy/command.h>
#include <hammy/commands.h>
#include <hammy/job.h> #include <hammy/job.h>
#include <hammy/refdb.h> #include <hammy/refdb.h>
#include <hammy/utils.h> #include <hammy/utils.h>
@@ -29,7 +30,7 @@
// invalid JSON. // invalid JSON.
static bool phonetic_append(char* buf, size_t cap, size_t* len, bool gap, const char* token) { static bool phonetic_append(char* buf, size_t cap, size_t* len, bool gap, const char* token) {
size_t n = strlen(token); size_t n = strlen(token);
size_t need = (*len > 0 ? 1 : 0) + (gap ? 2 : 0) + n; size_t need = (*len > 0 ? 1u : 0u) + (gap ? 2u : 0u) + n;
if (*len + need + 1 > cap) { return false; } if (*len + need + 1 > cap) { return false; }
@@ -53,18 +54,20 @@ static bool phonetic_is_gap(char c) {
// Instant command: runs on the gateway thread, sends a fresh response. // Instant command: runs on the gateway thread, sends a fresh response.
void hammy_cmd_phonetic(const hammy_job_t* job, struct discord* client, hammy_refdb_t* refdb) { void hammy_cmd_phonetic(const hammy_job_t* job, struct discord* client, hammy_refdb_t* refdb) {
// Get the callsign argument from the job // Get the text argument from the job
const char* text = hammy_job_get_arg(job, "callsign"); const char* text = hammy_job_get_arg(job, "text");
if (!text) { if (!text) {
hammy_job_respond(job, client, "Callsign Missing!", "No Callsign provided for Phonetic conversion.", true); hammy_job_respond(job, client, "Text Missing!", "No Text provided for Phonetic conversion.", true);
return; return;
} }
if (strlen(text) > 12) { // Arbitrary limit to prevent abuse and ensure reasonable output size if (strlen(text) >= 128) {
hammy_job_respond(job, client, "Callsign Too Long!", "The provided Callsign exceeds the maximum allowed length.", true); hammy_job_respond(job, client, "Text Too Long!", "Text provided is too long! Max. 128 characters.", true);
return; return;
} }
bool showPronunciation = strlen(text) < 12;
char phonetic[HAMMY_PHONETIC_CODE_MAX + sizeof(HAMMY_PHONETIC_TRUNCATED)]; char phonetic[HAMMY_PHONETIC_CODE_MAX + sizeof(HAMMY_PHONETIC_TRUNCATED)];
char pronunciation[HAMMY_PHONETIC_PRONUNCIATION_MAX + sizeof(HAMMY_PHONETIC_TRUNCATED)]; char pronunciation[HAMMY_PHONETIC_PRONUNCIATION_MAX + sizeof(HAMMY_PHONETIC_TRUNCATED)];
@@ -127,11 +130,15 @@ void hammy_cmd_phonetic(const hammy_job_t* job, struct discord* client, hammy_re
memcpy(pronunciation + pronunciationLen, HAMMY_PHONETIC_TRUNCATED, sizeof(HAMMY_PHONETIC_TRUNCATED)); memcpy(pronunciation + pronunciationLen, HAMMY_PHONETIC_TRUNCATED, sizeof(HAMMY_PHONETIC_TRUNCATED));
} }
hammy_to_uppercase(text); hammy_to_uppercase((char*)text);
// Reply with the phonetic words and how each of them is spoken. // Reply with the phonetic words and how each of them is spoken.
char body[sizeof(phonetic) + sizeof(pronunciation) + 48]; char body[sizeof(phonetic) + sizeof(pronunciation) + 48];
snprintf(body, sizeof(body), "Callsign: `%s`\nPhonetics: `%s`\nPronunciation: `%s`", text, phonetic, pronunciation); if (showPronunciation) {
snprintf(body, sizeof(body), "Text: `%s`\nPhonetics: `%s`\nPronunciation: `%s`", text, phonetic, pronunciation);
} else {
snprintf(body, sizeof(body), "Text: `(truncated)`\nPhonetics: `%s`", phonetic);
}
hammy_job_respond(job, client, "Phonetic Code Conversion", body, false); hammy_job_respond(job, client, "Text to Phonetics Conversion", body, false);
} }
+1
View File
@@ -3,6 +3,7 @@
#include <stdio.h> #include <stdio.h>
#include <hammy/command.h> #include <hammy/command.h>
#include <hammy/commands.h>
#include <hammy/job.h> #include <hammy/job.h>
// Discord epoch, for turning a snowflake back into a wall-clock time. // Discord epoch, for turning a snowflake back into a wall-clock time.
+2 -1
View File
@@ -4,6 +4,7 @@
#include <string.h> #include <string.h>
#include <hammy/command.h> #include <hammy/command.h>
#include <hammy/commands.h>
#include <hammy/job.h> #include <hammy/job.h>
#include <hammy/refdb.h> #include <hammy/refdb.h>
#include <hammy/utils.h> #include <hammy/utils.h>
@@ -31,7 +32,7 @@ void hammy_cmd_q(const hammy_job_t* job, struct discord* client, hammy_refdb_t*
return; return;
} }
hammy_to_uppercase(qcode); hammy_to_uppercase((char*)qcode);
snprintf(body, sizeof(body), "Q-Code: `%s`\nQuestion: `%s`\nAnswer: `%s`", qcode, (questionStr ? questionStr : "Not Specified"), (answerStr ? answerStr : "Not Specified")); snprintf(body, sizeof(body), "Q-Code: `%s`\nQuestion: `%s`\nAnswer: `%s`", qcode, (questionStr ? questionStr : "Not Specified"), (answerStr ? answerStr : "Not Specified"));
+1 -1
View File
@@ -52,7 +52,7 @@ static void hammy_bot_on_interaction(struct discord* client, const struct discor
code = discord_create_interaction_response(client, event->id, event->token, &params, NULL); code = discord_create_interaction_response(client, event->id, event->token, &params, NULL);
} else { } else {
params.data = &(struct discord_interaction_callback_data){ params.data = &(struct discord_interaction_callback_data){
.content = "I don't know that command!" .content = (char*)"I don't know that command!"
}; };
code = discord_create_interaction_response(client, event->id, event->token, &params, NULL); code = discord_create_interaction_response(client, event->id, event->token, &params, NULL);
+33 -16
View File
@@ -2,20 +2,16 @@
#include <string.h> #include <string.h>
#include <hammy/command.h> #include <hammy/command.h>
#include <hammy/commands.h> // Table
// Handlers live in src/hammy/commands/. Declared here rather than in a header // Handlers live in src/hammy/commands/. Declared here rather than in a header
// so adding a command touches exactly two places: its own .c file and this // so adding a command touches exactly two places: its own .c file and this
// table. // table.
void hammy_cmd_ping(const hammy_job_t* job, struct discord* client, hammy_refdb_t* refdb);
void hammy_cmd_morse(const hammy_job_t* job, struct discord* client, hammy_refdb_t* refdb);
void hammy_cmd_freq(const hammy_job_t* job, struct discord* client, hammy_refdb_t* refdb);
void hammy_cmd_q(const hammy_job_t* job, struct discord* client, hammy_refdb_t* refdb);
void hammy_cmd_phonetic(const hammy_job_t* job, struct discord* client, 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[] = {
{ .type = DISCORD_APPLICATION_OPTION_STRING, .name = "text", { .type = DISCORD_APPLICATION_OPTION_STRING, .name = (char*)"text",
.description = "Text to convert to Morse code", .required = true }, .description = (char*)"Text to convert to Morse code", .required = true },
}; };
static struct discord_application_command_options morse_opts_struct = { static struct discord_application_command_options morse_opts_struct = {
@@ -24,10 +20,10 @@ static struct discord_application_command_options morse_opts_struct = {
}; };
static struct discord_application_command_option freq_opts[] = { static struct discord_application_command_option freq_opts[] = {
{ .type = DISCORD_APPLICATION_OPTION_STRING, .name = "frequency", { .type = DISCORD_APPLICATION_OPTION_STRING, .name = (char*)"frequency",
.description = "Frequency in MHz", .required = true }, .description = (char*)"Frequency in MHz", .required = true },
{ .type = DISCORD_APPLICATION_OPTION_STRING, .name = "country", { .type = DISCORD_APPLICATION_OPTION_STRING, .name = (char*)"country",
.description = "Country code to look up (e.g. US) - Defaults to US if none specified.", .required = false }, .description = (char*)"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 = { static struct discord_application_command_options freq_opts_struct = {
@@ -36,8 +32,8 @@ static struct discord_application_command_options freq_opts_struct = {
}; };
static struct discord_application_command_option q_opts[] = { static struct discord_application_command_option q_opts[] = {
{ .type = DISCORD_APPLICATION_OPTION_STRING, .name = "q-code", { .type = DISCORD_APPLICATION_OPTION_STRING, .name = (char*)"q-code",
.description = "Q-Code to convert to Question/Answer", .required = true }, .description = (char*)"Q-Code to convert to Question/Answer", .required = true },
}; };
static struct discord_application_command_options q_opts_struct = { static struct discord_application_command_options q_opts_struct = {
@@ -45,9 +41,19 @@ static struct discord_application_command_options q_opts_struct = {
.array = q_opts .array = q_opts
}; };
static struct discord_application_command_option abbr_opts[] = {
{ .type = DISCORD_APPLICATION_OPTION_STRING, .name = (char*)"abbreviation",
.description = (char*)"Abbreviation to convert to Meaning", .required = true },
};
static struct discord_application_command_options abbr_opts_struct = {
.size = 1,
.array = abbr_opts
};
static struct discord_application_command_option phonetic_opts[] = { static struct discord_application_command_option phonetic_opts[] = {
{ .type = DISCORD_APPLICATION_OPTION_STRING, .name = "callsign", { .type = DISCORD_APPLICATION_OPTION_STRING, .name = (char*)"text",
.description = "Callsign to convert to Phonetics", .required = true }, .description = (char*)"Text to convert to Phonetics", .required = true },
}; };
static struct discord_application_command_options phonetic_opts_struct = { static struct discord_application_command_options phonetic_opts_struct = {
@@ -55,6 +61,15 @@ static struct discord_application_command_options phonetic_opts_struct = {
.array = phonetic_opts .array = phonetic_opts
}; };
static struct discord_application_command_option dxcc_opts[] = {
{ .type = DISCORD_APPLICATION_OPTION_STRING, .name = (char*)"callsign",
.description = (char*)"Callsign or prefix to look up", .required = true },
{ .type = DISCORD_APPLICATION_OPTION_STRING, .name = (char*)"grid",
.description = (char*)"Your Maidenhead locator, for distance and bearing", .required = false },
};
static struct discord_application_command_options dxcc_opts_struct = { .size = 2, .array = dxcc_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.
@@ -65,7 +80,9 @@ static const hammy_command_t hammy_builtin_commands[] = {
{ "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 }, { "freq", "Band, segment and who can transmit", &freq_opts_struct, &hammy_cmd_freq, true },
{ "q", "Convert Q-Code to the corresponding question and answer", &q_opts_struct, &hammy_cmd_q, true }, { "q", "Convert Q-Code to the corresponding question and answer", &q_opts_struct, &hammy_cmd_q, true },
{ "phonetic", "Convert Callsign to Phonetics", &phonetic_opts_struct, &hammy_cmd_phonetic, true } { "phonetic", "Convert Text to Phonetics", &phonetic_opts_struct, &hammy_cmd_phonetic, true },
{ "dxcc", "Look up the DXCC entity for a callsign", &dxcc_opts_struct, &hammy_cmd_dxcc, true },
{ "abbr", "Convert Abbreviation to Meaning and Context", &abbr_opts_struct, &hammy_cmd_abbr, 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 },
+197
View File
@@ -0,0 +1,197 @@
#include <ctype.h>
#include <math.h>
#include <stdbool.h>
#include <stddef.h>
#include <string.h>
#include <hammy/geo.h>
// M_PI is POSIX, not C99, so it is absent under -std=c99 without _GNU_SOURCE.
// Defining it here keeps the build strict and portable.
#define HAMMY_PI 3.14159265358979323846
// IUGG mean Earth radius. Any of the common radii agree to well within the
// error a 6-character grid square already carries.
#define HAMMY_EARTH_RADIUS_KM 6371.0088
#define HAMMY_EARTH_CIRCUM_KM (2.0 * HAMMY_PI * HAMMY_EARTH_RADIUS_KM)
static double deg_to_rad(double deg) { return deg * HAMMY_PI / 180.0; }
static double rad_to_deg(double rad) { return rad * 180.0 / HAMMY_PI; }
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wbad-function-cast" // suppresses the dumb floor() cast to int warning
bool hammy_grid_from_latlon(double lat, double lon, int precision, char* out, size_t cap) {
if (!out || cap < 3) { return false; }
if (lat < -90.0 || lat > 90.0 || lon < -180.0 || lon > 180.0) { return false; }
if (precision != 2 && precision != 4 && precision != 6 && precision != 8) { return false; }
if (cap < (size_t)precision + 1) { return false; }
// Shift into the all-positive space the locator system is defined over.
double lonA = lon + 180.0;
double latA = lat + 90.0;
// Clamp the poles and the antimeridian so floor() cannot walk off the end
// of the field letters at exactly +90 / +180.
if (lonA >= 360.0) { lonA = 359.999999; }
if (latA >= 180.0) { latA = 179.999999; }
int i = 0;
int f1 = (int)floor(lonA / 20.0);
int f2 = (int)floor(latA / 10.0);
out[i++] = (char)('A' + f1);
out[i++] = (char)('A' + f2);
double remLon = lonA - f1 * 20.0;
double remLat = latA - f2 * 10.0;
if (precision >= 4) {
int s1 = (int)floor(remLon / 2.0);
int s2 = (int)floor(remLat / 1.0);
out[i++] = (char)('0' + s1);
out[i++] = (char)('0' + s2);
remLon -= s1 * 2.0;
remLat -= s2 * 1.0;
}
if (precision >= 6) {
// A square is 2 deg of longitude by 1 deg of latitude, divided 24 ways.
int ss1 = (int)floor(remLon / (2.0 / 24.0));
int ss2 = (int)floor(remLat / (1.0 / 24.0));
if (ss1 > 23) { ss1 = 23; }
if (ss2 > 23) { ss2 = 23; }
out[i++] = (char)('a' + ss1);
out[i++] = (char)('a' + ss2);
remLon -= ss1 * (2.0 / 24.0);
remLat -= ss2 * (1.0 / 24.0);
}
if (precision >= 8) {
int e1 = (int)floor(remLon / (2.0 / 240.0));
int e2 = (int)floor(remLat / (1.0 / 240.0));
if (e1 > 9) { e1 = 9; }
if (e2 > 9) { e2 = 9; }
out[i++] = (char)('0' + e1);
out[i++] = (char)('0' + e2);
}
out[i] = '\0';
return true;
}
bool hammy_grid_to_latlon(const char* grid, double* outLat, double* outLon) {
if (!grid || !outLat || !outLon) { return false; }
char g[HAMMY_GRID_MAX];
size_t n = 0;
for (size_t i = 0; grid[i]; i++) {
if (isspace((unsigned char)grid[i])) { continue; }
// Reject rather than truncate. Silently clipping "IO91wm12345" to
// "IO91wm12" would hand back a plausible-looking position for input the
// user clearly got wrong.
if (n + 1 >= sizeof(g)) { return false; }
g[n++] = grid[i];
}
g[n] = '\0';
// Locators come in even-length pairs; 2, 4, 6 and 8 are the useful ones.
if (n != 2 && n != 4 && n != 6 && n != 8) { return false; }
int f1 = toupper((unsigned char)g[0]) - 'A';
int f2 = toupper((unsigned char)g[1]) - 'A';
if (f1 < 0 || f1 > 17 || f2 < 0 || f2 > 17) { return false; }
double lon = f1 * 20.0;
double lat = f2 * 10.0;
double lonSize = 20.0;
double latSize = 10.0;
if (n >= 4) {
if (!isdigit((unsigned char)g[2]) || !isdigit((unsigned char)g[3])) { return false; }
lon += (g[2] - '0') * 2.0;
lat += (g[3] - '0') * 1.0;
lonSize = 2.0;
latSize = 1.0;
}
if (n >= 6) {
int s1 = tolower((unsigned char)g[4]) - 'a';
int s2 = tolower((unsigned char)g[5]) - 'a';
if (s1 < 0 || s1 > 23 || s2 < 0 || s2 > 23) { return false; }
lon += s1 * (2.0 / 24.0);
lat += s2 * (1.0 / 24.0);
lonSize = 2.0 / 24.0;
latSize = 1.0 / 24.0;
}
if (n >= 8) {
if (!isdigit((unsigned char)g[6]) || !isdigit((unsigned char)g[7])) { return false; }
lon += (g[6] - '0') * (2.0 / 240.0);
lat += (g[7] - '0') * (1.0 / 240.0);
lonSize = 2.0 / 240.0;
latSize = 1.0 / 240.0;
}
// Report the CENTER of the square, not its south-west corner. Using the
// corner biases every distance by up to half a square, which at 4-character
// precision is about 60 km.
*outLon = lon + lonSize / 2.0 - 180.0;
*outLat = lat + latSize / 2.0 - 90.0;
return true;
}
void hammy_great_circle(double lat1, double lon1, double lat2, double lon2,
double* outDistKm, double* outBearingDeg) {
double p1 = deg_to_rad(lat1);
double p2 = deg_to_rad(lat2);
double dp = p2 - p1;
double dl = deg_to_rad(lon2 - lon1);
if (outDistKm) {
// Haversine. The spherical law of cosines is shorter but loses precision
// for short distances, which is exactly the "how far to the next town"
// case people try first.
double a = sin(dp / 2.0) * sin(dp / 2.0)
+ cos(p1) * cos(p2) * sin(dl / 2.0) * sin(dl / 2.0);
if (a > 1.0) { a = 1.0; }
*outDistKm = 2.0 * HAMMY_EARTH_RADIUS_KM * asin(sqrt(a));
}
if (outBearingDeg) {
double y = sin(dl) * cos(p2);
double x = cos(p1) * sin(p2) - sin(p1) * cos(p2) * cos(dl);
double b = rad_to_deg(atan2(y, x));
*outBearingDeg = fmod(b + 360.0, 360.0);
}
}
double hammy_long_path_km(double shortPathKm) {
return HAMMY_EARTH_CIRCUM_KM - shortPathKm;
}
double hammy_reciprocal_bearing(double bearingDeg) {
return fmod(bearingDeg + 180.0, 360.0);
}
const char* hammy_compass_point(double bearingDeg) {
static const char* POINTS[16] = {
"N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE",
"S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"
};
double b = fmod(bearingDeg + 360.0, 360.0);
int idx = (int)floor((b + 11.25) / 22.5) % 16;
return POINTS[idx];
}
#pragma clang diagnostic pop
+60 -1
View File
@@ -94,6 +94,10 @@ static const char SQL_QCODE[] =
static const char SQL_PHONETIC[] = static const char SQL_PHONETIC[] =
"SELECT word, pronunciation FROM phonetics WHERE letter = UPPER(?1) LIMIT 1"; "SELECT word, pronunciation FROM phonetics WHERE letter = UPPER(?1) LIMIT 1";
// Resolve abbr to meaning
static const char SQL_ABBR[] =
"SELECT meaning, context FROM abbreviations WHERE abbr = UPPER(?1) LIMIT 1";
// 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
@@ -148,6 +152,7 @@ static const hammy_stmt_def_t HAMMY_STATEMENTS[] = {
HAMMY_STMT(stMorse, SQL_MORSE), HAMMY_STMT(stMorse, SQL_MORSE),
HAMMY_STMT(stQCode, SQL_QCODE), HAMMY_STMT(stQCode, SQL_QCODE),
HAMMY_STMT(stPhonetic, SQL_PHONETIC), HAMMY_STMT(stPhonetic, SQL_PHONETIC),
HAMMY_STMT(stAbbr, SQL_ABBR),
HAMMY_STMT(stFreqMain, SQL_FREQ_MAIN), HAMMY_STMT(stFreqMain, SQL_FREQ_MAIN),
HAMMY_STMT(stFreqIaru, SQL_FREQ_IARU), HAMMY_STMT(stFreqIaru, SQL_FREQ_IARU),
HAMMY_STMT(stFreqNearest, SQL_FREQ_NEAREST), HAMMY_STMT(stFreqNearest, SQL_FREQ_NEAREST),
@@ -191,6 +196,8 @@ static bool prepare(sqlite3* h, const char* sql, sqlite3_stmt** out) {
return true; return true;
} }
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wjump-misses-init" // suppresses the "jumped initialization of varaible" warning, has no effect here
hammy_refdb_t* hammy_refdb_open(const char* path) { hammy_refdb_t* hammy_refdb_open(const char* path) {
if (!path) { return NULL; } if (!path) { return NULL; }
@@ -267,6 +274,7 @@ fail:
hammy_refdb_close(&db); hammy_refdb_close(&db);
return NULL; return NULL;
} }
#pragma GCC diagnostic pop
bool hammy_refdb_close(hammy_refdb_t** db) { bool hammy_refdb_close(hammy_refdb_t** db) {
if (!db || !(*db)) { return false; } if (!db || !(*db)) { return false; }
@@ -479,6 +487,12 @@ bool hammy_refdb_get_phonetic(hammy_refdb_t* db, char c, const char** out, const
bool hit = false; bool hit = false;
if (sqlite3_step(db->stPhonetic) == SQLITE_ROW) { if (sqlite3_step(db->stPhonetic) == SQLITE_ROW) {
if (sqlite3_column_count(db->stPhonetic) < 2) {
// Error
sqlite3_reset(db->stPhonetic);
return false;
}
const unsigned char* code = sqlite3_column_text(db->stPhonetic, 0); const unsigned char* code = sqlite3_column_text(db->stPhonetic, 0);
const unsigned char* codePronunciation = sqlite3_column_text(db->stPhonetic, 1); const unsigned char* codePronunciation = sqlite3_column_text(db->stPhonetic, 1);
@@ -543,6 +557,51 @@ bool hammy_refdb_get_qcode(hammy_refdb_t* db, const char* code, const char** out
return hit; return hit;
} }
bool hammy_refdb_get_abbr(hammy_refdb_t* db, const char* code, const char** outStr, const char** outContext) {
if (!db || !code || !outStr || !outContext) { return false; }
sqlite3_stmt* const needed[] = { db->stAbbr };
if (!stmts_ready("qcodes", needed, 1)) { return false; }
// Read the get_morse comment, I ain't writing this again (entire string edition)
for (const char* p = code; *p != '\0'; p++) {
if ((unsigned char)*p & 0x80u) { return false; }
}
// Query the string
sqlite3_reset(db->stAbbr);
sqlite3_clear_bindings(db->stAbbr);
sqlite3_bind_text(db->stAbbr, 1, code, -1, SQLITE_TRANSIENT); // -1 tells SQLite to figure out the length itself (strlen() call - requires NULL term)
bool hit = false;
if (sqlite3_step(db->stAbbr) == SQLITE_ROW) {
if (sqlite3_column_count(db->stAbbr) < 2) { // Error
sqlite3_reset(db->stAbbr);
return false;
}
const unsigned char* meaningStr = sqlite3_column_text(db->stAbbr, 0);
const unsigned char* ctxStr = sqlite3_column_text(db->stAbbr, 1);
if (meaningStr) {
snprintf(db->abbrStr, sizeof(db->abbrStr), "%s", (const char*)meaningStr);
*outStr = db->abbrStr;
hit = true;
}
if (ctxStr) {
snprintf(db->abbrCtx, sizeof(db->abbrCtx), "%s", (const char*)ctxStr);
*outContext = db->abbrCtx;
hit = true;
}
}
sqlite3_reset(db->stAbbr);
return hit;
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Frequency parsing // Frequency parsing
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -762,7 +821,7 @@ bool hammy_refdb_freq(hammy_refdb_t* db, int64_t freqHz, const char* country,
sqlite3_reset(st); sqlite3_reset(st);
// Sitting exactly on a segment boundary is worth flagging: a signal of any // Sitting exactly on a segment boundary is worth flagging: a signal of any
// width centred there straddles both sides. // width centerd there straddles both sides.
st = db->stFreqSegEdge; st = db->stFreqSegEdge;
sqlite3_reset(st); sqlite3_reset(st);
sqlite3_clear_bindings(st); sqlite3_clear_bindings(st);
+2 -2
View File
@@ -2,12 +2,12 @@
void hammy_to_uppercase(char* str) { void hammy_to_uppercase(char* str) {
for (size_t i = 0; str[i] != '\0'; i++) { for (size_t i = 0; str[i] != '\0'; i++) {
str[i] = toupper((unsigned char)str[i]); str[i] = (char)toupper((unsigned char)str[i]);
} }
} }
void hammy_to_lowercase(char* str) { void hammy_to_lowercase(char* str) {
for (size_t i = 0; str[i] != '\0'; i++) { for (size_t i = 0; str[i] != '\0'; i++) {
str[i] = tolower((unsigned char)str[i]); str[i] = (char)tolower((unsigned char)str[i]);
} }
} }