Compare commits

..
9 Commits
22 changed files with 531 additions and 1168 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
+1 -1
View File
@@ -111,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;
+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
+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_abbr(const hammy_job_t* job, struct discord* client, hammy_refdb_
return; return;
} }
hammy_to_uppercase(abbr); 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")); snprintf(body, sizeof(body), "Abbreviation: `%s`\nMeaning: `%s`\nUsage Context: `%s`", abbr, (str ? str : "Not Specified"), (context ? context : "Not Specified"));
+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; }
+3 -2
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; }
@@ -129,7 +130,7 @@ 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];
+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);
+23 -18
View File
@@ -2,21 +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); // 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
// 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 = {
@@ -25,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 = {
@@ -37,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 = {
@@ -47,8 +42,8 @@ static struct discord_application_command_options q_opts_struct = {
}; };
static struct discord_application_command_option abbr_opts[] = { static struct discord_application_command_option abbr_opts[] = {
{ .type = DISCORD_APPLICATION_OPTION_STRING, .name = "abbreviation", { .type = DISCORD_APPLICATION_OPTION_STRING, .name = (char*)"abbreviation",
.description = "Abbreviation to convert to Meaning", .required = true }, .description = (char*)"Abbreviation to convert to Meaning", .required = true },
}; };
static struct discord_application_command_options abbr_opts_struct = { static struct discord_application_command_options abbr_opts_struct = {
@@ -57,8 +52,8 @@ static struct discord_application_command_options abbr_opts_struct = {
}; };
static struct discord_application_command_option phonetic_opts[] = { static struct discord_application_command_option phonetic_opts[] = {
{ .type = DISCORD_APPLICATION_OPTION_STRING, .name = "text", { .type = DISCORD_APPLICATION_OPTION_STRING, .name = (char*)"text",
.description = "Text 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 = {
@@ -66,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.
@@ -77,6 +81,7 @@ static const hammy_command_t hammy_builtin_commands[] = {
{ "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 Text 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 } { "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:
+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
+1 -1
View File
@@ -192,4 +192,4 @@ void hammy_pool_stats(hammy_pool_t* pool, size_t* outQueued, size_t* outBusy) {
if (outBusy) { *outBusy = pool->busy; } if (outBusy) { *outBusy = pool->busy; }
if (outQueued) { *outQueued = pool->count; } if (outQueued) { *outQueued = pool->count; }
pthread_mutex_unlock(&pool->lock); pthread_mutex_unlock(&pool->lock);
} }
+4 -1
View File
@@ -196,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; }
@@ -272,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; }
@@ -818,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]);
} }
} }
+2 -1122
View File
File diff suppressed because it is too large Load Diff