add /freq command - allows lookup of data regarding ham bands for region-specific details

This commit is contained in:
2026-08-31 20:15:06 +02:00
parent 7acb2aabd4
commit c056b1b67a
8 changed files with 435 additions and 100 deletions
+184 -13
View File
@@ -4,30 +4,201 @@
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdarg.h>
#include <hammy/command.h>
#include <hammy/job.h>
#include <hammy/refdb.h>
#define HAMMY_FREQ_BODY_MAX 3600
// Small append-only string builder. Every write is bounds-checked, so a country
// with thirty licence classes overflows into "truncated" rather than the stack.
typedef struct {
char* buf;
size_t cap;
size_t len;
bool overflow;
} hammy_sb_t;
static void sb_addf(hammy_sb_t* sb, const char* fmt, ...) {
if (sb->overflow || sb->len + 1 >= sb->cap) {
sb->overflow = true;
return;
}
va_list ap;
va_start(ap, fmt);
int n = vsnprintf(sb->buf + sb->len, sb->cap - sb->len, fmt, ap);
va_end(ap);
if (n < 0) {
sb->overflow = true;
return;
}
if ((size_t)n >= sb->cap - sb->len) {
sb->len = sb->cap - 1;
sb->overflow = true;
return;
}
sb->len += (size_t)n;
}
// Hz -> MHz with three decimals, which is the resolution every band edge in the
// bundle actually uses. Integer maths so no rounding surprises at an edge.
static void fmt_mhz(int64_t hz, char* out, size_t cap) {
int64_t whole = hz / 1000000;
int64_t frac = (hz % 1000000) / 1000;
snprintf(out, cap, "%" PRId64 ".%03" PRId64, whole, frac);
}
// 'CW,DATA' reads better as 'CW, DATA' in an embed.
static void fmt_modes(const char* modes, char* out, size_t cap) {
size_t j = 0;
for (size_t i = 0; modes[i] && j + 2 < cap; i++) {
out[j++] = modes[i];
if (modes[i] == ',' && j + 1 < cap) { out[j++] = ' '; }
}
out[j] = '\0';
}
// Instant command: runs on the gateway thread, sends a fresh response.
void hammy_cmd_freq(const hammy_job_t* job, struct discord* client, const hammy_refdb_t* refdb) {
// Get the text argument from the job
const char* freq = hammy_job_get_arg(job, "frequency");
if (!freq) {
void hammy_cmd_freq(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* arg = hammy_job_get_arg(job, "frequency");
if (!arg || !*arg) {
hammy_job_respond(job, client, "Error", "No frequency provided for lookup.", true);
return;
}
char* endPtr;
float freqMHz = strtof(freq, &endPtr);
if (*endPtr != '\0') {
// Partial conversion, just error out
hammy_job_respond(job, client, "Error", "An internal error occurred. Please try again.", true);
// Parses "14.150", "14150 kHz", "146.52 MHz". Integer maths throughout: a
// bare (int64_t)cast of a float truncates BEFORE the multiply and turns
// 14.150 into 14.000, which silently reports the wrong segment.
int64_t freqHz = 0;
if (!hammy_freq_parse(arg, &freqHz)) {
hammy_job_respond(job, client, "Error", "I could not read that frequency. Try something like `14.150`, `7025 kHz` or `146.52 MHz`.", true);
return;
}
const char* cc = hammy_job_get_arg(job, "country"); // NULL -> refdb uses US
hammy_freq_t r;
if (!hammy_refdb_freq(refdb, freqHz, cc, &r)) {
hammy_job_respond(job, client, "Error", "Something went wrong looking that up.", true);
return;
}
uint64_t freqPlain = (uint64_t)freqMHz * 1000000; // Convert to raw Hz
char freqStr[32];
fmt_mhz(r.freqHz, freqStr, sizeof(freqStr));
char body[HAMMY_FREQ_BODY_MAX];
hammy_sb_t sb = { .buf = body, .cap = sizeof(body), .len = 0, .overflow = false };
body[0] = '\0';
// Not a valid ham band
if (!r.inBand) {
char lo[32], hi[32], dist[32];
fmt_mhz(r.nearestLowHz, lo, sizeof(lo));
fmt_mhz(r.nearestHighHz, hi, sizeof(hi));
fmt_mhz(r.nearestDistanceHz, dist, sizeof(dist));
sb_addf(&sb, "**%s MHz** is not in a valid amateur band.\n\n", freqStr);
sb_addf(&sb, "Nearest is **%s** (%s - %s MHz), %s MHz away.", r.nearestBand, lo, hi, dist);
hammy_job_respond(job, client, "Not an Amateur Band!", body, false);
return;
}
char bandLo[32], bandHi[32];
fmt_mhz(r.bandLowHz, bandLo, sizeof(bandLo));
fmt_mhz(r.bandHighHz, bandHi, sizeof(bandHi));
char title[128];
snprintf(title, sizeof(title), "Band **%s** (%s - %s MHz)\n", r.band, bandLo, bandHi);
// Privileges
if (!r.countryKnown) {
// The common case - only US data is seeded so far. Say what is missing
// and what does exist, rather than showing an empty table.
char codes[HAMMY_FREQ_COUNTRIES_MAX][HAMMY_COUNTRY_MAX];
size_t n = hammy_refdb_countries(refdb, codes, HAMMY_FREQ_COUNTRIES_MAX);
sb_addf(&sb, "\nNo licence data for **%s** in this bundle yet.\n", r.country);
if (n) {
sb_addf(&sb, "Currently available: ");
for (size_t i = 0; i < n; i++) {
sb_addf(&sb, "%s`%s`", i ? ", " : "", codes[i]);
}
sb_addf(&sb, "\n");
}
sb_addf(&sb, "Band plans are contributed by operators - if you know your regulator's allocations, please help fill this in.\n");
} else if (r.nPrivs == 0) {
sb_addf(&sb, "\nNo license classes are recorded for **%s**.\n", r.country);
} else {
sb_addf(&sb, "\n**Privileges in %s**\n", r.country);
for (size_t i = 0; i < r.nPrivs; i++) {
const hammy_freq_priv_t* p = &r.privs[i];
if (!p->permitted) {
sb_addf(&sb, "`%-14s` not permitted here\n", p->name);
continue;
}
char lo[32], hi[32], modes[80];
fmt_mhz(p->segLowHz, lo, sizeof(lo));
fmt_mhz(p->segHighHz, hi, sizeof(hi));
fmt_modes(p->modes, modes, sizeof(modes));
sb_addf(&sb, "`%-14s` %s (%s - %s", p->name, modes, lo, hi);
if (p->maxPowerW > 0) {
sb_addf(&sb, ", max %d W", p->maxPowerW);
}
sb_addf(&sb, ")\n");
if (p->notes[0]) {
sb_addf(&sb, " *%s*\n", p->notes);
}
}
}
// IARU
if (r.nIaru) {
sb_addf(&sb, "\n**IARU Allocation**\n");
for (size_t i = 0; i < r.nIaru; i++) {
char lo[32], hi[32];
fmt_mhz(r.iaru[i].lowHz, lo, sizeof(lo));
fmt_mhz(r.iaru[i].highHz, hi, sizeof(hi));
sb_addf(&sb, "Region %d: %s - %s MHz\n", r.iaru[i].region, lo, hi);
}
}
// Boundary warnings
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");
} else if (r.atSegmentEdge) {
sb_addf(&sb, "\n> This is exactly a segment boundary, so a signal centred here straddles both sides.\n");
}
if (sb.overflow) {
snprintf(body + sizeof(body) - 20, 20, "\n... truncated");
}
hammy_job_respond(job, client, title, body, false);
}