@e04/ft8ts 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ft8ts.mjs CHANGED
@@ -684,7 +684,7 @@ function bitsToUint(bits, start, len) {
684
684
  }
685
685
  return val;
686
686
  }
687
- function unpack28(n28) {
687
+ function unpack28(n28, book) {
688
688
  if (n28 < 0 || n28 >= 268435456)
689
689
  return { call: "", success: false };
690
690
  if (n28 === 0)
@@ -698,7 +698,6 @@ function unpack28(n28) {
698
698
  return { call: `CQ ${nqsy.toString().padStart(3, "0")}`, success: true };
699
699
  }
700
700
  if (n28 >= 1003 && n28 < NTOKENS) {
701
- // CQ with 4-letter directed call
702
701
  let m = n28 - 1003;
703
702
  let chars = "";
704
703
  for (let i = 3; i >= 0; i--) {
@@ -712,7 +711,10 @@ function unpack28(n28) {
712
711
  return { call: "CQ", success: true };
713
712
  }
714
713
  if (n28 >= NTOKENS && n28 < NTOKENS + MAX22) {
715
- // Hashed call we don't have a hash table, so show <...>
714
+ const n22 = n28 - NTOKENS;
715
+ const resolved = book?.lookup22(n22);
716
+ if (resolved)
717
+ return { call: `<${resolved}>`, success: true };
716
718
  return { call: "<...>", success: true };
717
719
  }
718
720
  // Standard callsign
@@ -792,8 +794,11 @@ function unpackText77(bits71) {
792
794
  }
793
795
  /**
794
796
  * Unpack a 77-bit FT8 message into a human-readable string.
797
+ *
798
+ * When a {@link HashCallBook} is provided, hashed callsigns are resolved from
799
+ * the book, and newly decoded standard callsigns are saved into it.
795
800
  */
796
- function unpack77(bits77) {
801
+ function unpack77(bits77, book) {
797
802
  const n3 = bitsToUint(bits77, 71, 3);
798
803
  const i3 = bitsToUint(bits77, 74, 3);
799
804
  if (i3 === 0 && n3 === 0) {
@@ -811,8 +816,8 @@ function unpack77(bits77) {
811
816
  const ipb = bits77[57];
812
817
  const ir = bits77[58];
813
818
  const igrid4 = bitsToUint(bits77, 59, 15);
814
- const { call: call1, success: ok1 } = unpack28(n28a);
815
- const { call: call2Raw, success: ok2 } = unpack28(n28b);
819
+ const { call: call1, success: ok1 } = unpack28(n28a, book);
820
+ const { call: call2Raw, success: ok2 } = unpack28(n28b, book);
816
821
  if (!ok1 || !ok2)
817
822
  return { msg: "", success: false };
818
823
  let c1 = call1;
@@ -830,6 +835,9 @@ function unpack77(bits77) {
830
835
  c2 += "/R";
831
836
  if (ipb === 1 && i3 === 2 && c2.length >= 3)
832
837
  c2 += "/P";
838
+ // Save the "from" call (call_2) into the hash book
839
+ if (book && c2.length >= 3)
840
+ book.save(c2);
833
841
  }
834
842
  if (igrid4 <= MAXGRID4) {
835
843
  const { grid, success: gridOk } = toGrid4(igrid4);
@@ -862,6 +870,7 @@ function unpack77(bits77) {
862
870
  }
863
871
  if (i3 === 4) {
864
872
  // Type 4: One nonstandard call
873
+ const n12 = bitsToUint(bits77, 0, 12);
865
874
  let n58 = 0n;
866
875
  for (let i = 0; i < 58; i++) {
867
876
  n58 = n58 * 2n + BigInt(bits77[12 + i] ?? 0);
@@ -869,7 +878,6 @@ function unpack77(bits77) {
869
878
  const iflip = bits77[70];
870
879
  const nrpt = bitsToUint(bits77, 71, 2);
871
880
  const icq = bits77[73];
872
- // Decode n58 to 11-char string using C38 alphabet
873
881
  const c11chars = [];
874
882
  let remain = n58;
875
883
  for (let i = 10; i >= 0; i--) {
@@ -878,12 +886,15 @@ function unpack77(bits77) {
878
886
  c11chars.unshift(C38[j] ?? " ");
879
887
  }
880
888
  const c11 = c11chars.join("").trim();
881
- const call3 = "<...>"; // We don't have a hash table for n12
889
+ const resolved = book?.lookup12(n12);
890
+ const call3 = resolved ? `<${resolved}>` : "<...>";
882
891
  let call1;
883
892
  let call2;
884
893
  if (iflip === 0) {
885
894
  call1 = call3;
886
895
  call2 = c11;
896
+ if (book)
897
+ book.save(c11);
887
898
  }
888
899
  else {
889
900
  call1 = c11;
@@ -918,6 +929,7 @@ function decode(samples, sampleRate = SAMPLE_RATE, options = {}) {
918
929
  const syncmin = options.syncMin ?? 1.2;
919
930
  const depth = options.depth ?? 2;
920
931
  const maxCandidates = options.maxCandidates ?? 300;
932
+ const book = options.hashCallBook;
921
933
  // Resample to 12000 Hz if needed
922
934
  let dd;
923
935
  if (sampleRate === SAMPLE_RATE) {
@@ -942,7 +954,7 @@ function decode(samples, sampleRate = SAMPLE_RATE, options = {}) {
942
954
  const decoded = [];
943
955
  const seenMessages = new Set();
944
956
  for (const cand of candidates) {
945
- const result = ft8b(dd, cxRe, cxIm, cand.freq, cand.dt, sbase, depth);
957
+ const result = ft8b(dd, cxRe, cxIm, cand.freq, cand.dt, sbase, depth, book);
946
958
  if (!result)
947
959
  continue;
948
960
  if (seenMessages.has(result.msg))
@@ -1126,7 +1138,7 @@ function computeBaseline(savg, nfa, nfb, df, nh1) {
1126
1138
  }
1127
1139
  return sbase;
1128
1140
  }
1129
- function ft8b(dd0, cxRe, cxIm, f1, xdt, _sbase, depth) {
1141
+ function ft8b(_dd0, cxRe, cxIm, f1, xdt, _sbase, depth, book) {
1130
1142
  const NFFT2 = 3200;
1131
1143
  const NP2 = 2812;
1132
1144
  const fs2 = SAMPLE_RATE / NDOWN;
@@ -1334,7 +1346,7 @@ function ft8b(dd0, cxRe, cxIm, f1, xdt, _sbase, depth) {
1334
1346
  if (i3v === 0 && n3v === 2)
1335
1347
  return null;
1336
1348
  // Unpack
1337
- const { msg, success } = unpack77(message77);
1349
+ const { msg, success } = unpack77(message77, book);
1338
1350
  if (!success || msg.trim().length === 0)
1339
1351
  return null;
1340
1352
  // Estimate SNR
@@ -2201,5 +2213,111 @@ function encode(msg, options = {}) {
2201
2213
  return generateFT8Waveform(encodeMessage(msg), options);
2202
2214
  }
2203
2215
 
2204
- export { decode as decodeFT8, encode as encodeFT8 };
2216
+ /**
2217
+ * Hash call table – TypeScript port of the hash call storage from packjt77.f90
2218
+ *
2219
+ * In FT8, nonstandard callsigns are transmitted as hashes (10-, 12-, or 22-bit).
2220
+ * When a full callsign is decoded from a standard message, it is stored in this
2221
+ * table so that future hashed references to it can be resolved.
2222
+ *
2223
+ * Mirrors Fortran: save_hash_call, hash10, hash12, hash22, ihashcall
2224
+ */
2225
+ const MAGIC = 47055833459n;
2226
+ const MAX_HASH22_ENTRIES = 1000;
2227
+ function ihashcall(c0, m) {
2228
+ const s = c0.padEnd(11, " ").slice(0, 11).toUpperCase();
2229
+ let n8 = 0n;
2230
+ for (let i = 0; i < 11; i++) {
2231
+ const j = C38.indexOf(s[i] ?? " ");
2232
+ n8 = 38n * n8 + BigInt(j < 0 ? 0 : j);
2233
+ }
2234
+ const prod = BigInt.asUintN(64, MAGIC * n8);
2235
+ return Number(prod >> BigInt(64 - m)) & ((1 << m) - 1);
2236
+ }
2237
+ /**
2238
+ * Maintains a callsign ↔ hash lookup table for resolving hashed FT8 callsigns.
2239
+ *
2240
+ * Usage:
2241
+ * ```ts
2242
+ * const book = new HashCallBook();
2243
+ * const decoded = decodeFT8(samples, sampleRate, { hashCallBook: book });
2244
+ * // `book` now contains callsigns learned from decoded messages.
2245
+ * // Subsequent calls reuse the same book to resolve hashed callsigns:
2246
+ * const decoded2 = decodeFT8(samples2, sampleRate, { hashCallBook: book });
2247
+ * ```
2248
+ *
2249
+ * You can also pre-populate the book with known callsigns:
2250
+ * ```ts
2251
+ * book.save("W9XYZ");
2252
+ * book.save("PJ4/K1ABC");
2253
+ * ```
2254
+ */
2255
+ class HashCallBook {
2256
+ calls10 = new Map();
2257
+ calls12 = new Map();
2258
+ hash22Entries = [];
2259
+ /**
2260
+ * Store a callsign in all three hash tables (10, 12, 22-bit).
2261
+ * Strips angle brackets if present. Ignores `<...>` and blank/short strings.
2262
+ */
2263
+ save(callsign) {
2264
+ let cw = callsign.trim().toUpperCase();
2265
+ if (cw === "" || cw === "<...>")
2266
+ return;
2267
+ if (cw.startsWith("<"))
2268
+ cw = cw.slice(1);
2269
+ const gt = cw.indexOf(">");
2270
+ if (gt >= 0)
2271
+ cw = cw.slice(0, gt);
2272
+ cw = cw.trim();
2273
+ if (cw.length < 3)
2274
+ return;
2275
+ const n10 = ihashcall(cw, 10);
2276
+ if (n10 >= 0 && n10 <= 1023)
2277
+ this.calls10.set(n10, cw);
2278
+ const n12 = ihashcall(cw, 12);
2279
+ if (n12 >= 0 && n12 <= 4095)
2280
+ this.calls12.set(n12, cw);
2281
+ const n22 = ihashcall(cw, 22);
2282
+ const existing = this.hash22Entries.findIndex((e) => e.hash === n22);
2283
+ if (existing >= 0) {
2284
+ this.hash22Entries[existing].call = cw;
2285
+ }
2286
+ else {
2287
+ if (this.hash22Entries.length >= MAX_HASH22_ENTRIES) {
2288
+ this.hash22Entries.pop();
2289
+ }
2290
+ this.hash22Entries.unshift({ hash: n22, call: cw });
2291
+ }
2292
+ }
2293
+ /** Look up a callsign by its 10-bit hash. Returns `null` if not found. */
2294
+ lookup10(n10) {
2295
+ if (n10 < 0 || n10 > 1023)
2296
+ return null;
2297
+ return this.calls10.get(n10) ?? null;
2298
+ }
2299
+ /** Look up a callsign by its 12-bit hash. Returns `null` if not found. */
2300
+ lookup12(n12) {
2301
+ if (n12 < 0 || n12 > 4095)
2302
+ return null;
2303
+ return this.calls12.get(n12) ?? null;
2304
+ }
2305
+ /** Look up a callsign by its 22-bit hash. Returns `null` if not found. */
2306
+ lookup22(n22) {
2307
+ const entry = this.hash22Entries.find((e) => e.hash === n22);
2308
+ return entry?.call ?? null;
2309
+ }
2310
+ /** Number of entries in the 22-bit hash table. */
2311
+ get size() {
2312
+ return this.hash22Entries.length;
2313
+ }
2314
+ /** Remove all stored entries. */
2315
+ clear() {
2316
+ this.calls10.clear();
2317
+ this.calls12.clear();
2318
+ this.hash22Entries.length = 0;
2319
+ }
2320
+ }
2321
+
2322
+ export { HashCallBook, decode as decodeFT8, encode as encodeFT8 };
2205
2323
  //# sourceMappingURL=ft8ts.mjs.map