Compare commits
11
Commits
ae5389a7d5
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d43a65f1f
|
||
|
|
af7ed86424
|
||
|
|
3dcb73aaec
|
||
|
|
623d121c05
|
||
|
|
c7d2052fac
|
||
|
|
fcf510afa4
|
||
|
|
3d6b08d9d5
|
||
|
|
cca190e2f4
|
||
|
|
7ca1f80c01
|
||
|
|
d7b9582cc0
|
||
|
|
ee045cd1dd
|
@@ -1,5 +1,6 @@
|
|||||||
.env
|
.env
|
||||||
build/
|
build/
|
||||||
|
build*/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
config.json
|
config.json
|
||||||
.vscode
|
.vscode
|
||||||
|
|||||||
@@ -246,6 +246,8 @@ 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")
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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
@@ -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) {
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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, ¶ms, NULL);
|
code = discord_create_interaction_response(client, event->id, event->token, ¶ms, 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, ¶ms, NULL);
|
code = discord_create_interaction_response(client, event->id, event->token, ¶ms, NULL);
|
||||||
|
|||||||
+33
-16
@@ -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
@@ -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
@@ -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
@@ -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]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user