@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/src/ft8/decode.ts CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  } from "../util/constants.js";
14
14
  import { decode174_91 } from "../util/decode174_91.js";
15
15
  import { fftComplex, nextPow2 } from "../util/fft.js";
16
+ import type { HashCallBook } from "../util/hashcall.js";
16
17
  import { unpack77 } from "../util/unpack_jt77.js";
17
18
 
18
19
  export interface DecodedMessage {
@@ -34,6 +35,14 @@ export interface DecodeOptions {
34
35
  depth?: number;
35
36
  /** Maximum candidates to process */
36
37
  maxCandidates?: number;
38
+ /**
39
+ * Hash call book for resolving hashed callsigns.
40
+ * When provided, decoded standard callsigns are saved into the book,
41
+ * and hashed callsigns (e.g. `<...>`) are resolved from it.
42
+ * Pass the same instance across multiple `decode` calls to accumulate
43
+ * callsign knowledge over time.
44
+ */
45
+ hashCallBook?: HashCallBook;
37
46
  }
38
47
 
39
48
  /**
@@ -50,6 +59,7 @@ export function decode(
50
59
  const syncmin = options.syncMin ?? 1.2;
51
60
  const depth = options.depth ?? 2;
52
61
  const maxCandidates = options.maxCandidates ?? 300;
62
+ const book = options.hashCallBook;
53
63
 
54
64
  // Resample to 12000 Hz if needed
55
65
  let dd: Float64Array;
@@ -77,7 +87,7 @@ export function decode(
77
87
  const seenMessages = new Set<string>();
78
88
 
79
89
  for (const cand of candidates) {
80
- const result = ft8b(dd, cxRe, cxIm, cand.freq, cand.dt, sbase, depth);
90
+ const result = ft8b(dd, cxRe, cxIm, cand.freq, cand.dt, sbase, depth, book);
81
91
  if (!result) continue;
82
92
 
83
93
  if (seenMessages.has(result.msg)) continue;
@@ -312,17 +322,18 @@ interface Ft8bResult {
312
322
  }
313
323
 
314
324
  function ft8b(
315
- dd0: Float64Array,
325
+ _dd0: Float64Array,
316
326
  cxRe: Float64Array,
317
327
  cxIm: Float64Array,
318
328
  f1: number,
319
329
  xdt: number,
320
330
  _sbase: Float64Array,
321
331
  depth: number,
332
+ book: HashCallBook | undefined,
322
333
  ): Ft8bResult | null {
323
334
  const NFFT2 = 3200;
324
335
  const NP2 = 2812;
325
- const NFFT1_LONG = 192000;
336
+ const _NFFT1_LONG = 192000;
326
337
  const fs2 = SAMPLE_RATE / NDOWN;
327
338
  const dt2 = 1.0 / fs2;
328
339
  const twopi = 2 * Math.PI;
@@ -541,7 +552,7 @@ function ft8b(
541
552
  if (i3v === 0 && n3v === 2) return null;
542
553
 
543
554
  // Unpack
544
- const { msg, success } = unpack77(message77);
555
+ const { msg, success } = unpack77(message77, book);
545
556
  if (!success || msg.trim().length === 0) return null;
546
557
 
547
558
  // Estimate SNR
package/src/index.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export { type DecodedMessage, type DecodeOptions, decode as decodeFT8 } from "./ft8/decode.js";
2
2
  export { encode as encodeFT8 } from "./ft8/encode.js";
3
+ export { HashCallBook } from "./util/hashcall.js";
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Hash call table – TypeScript port of the hash call storage from packjt77.f90
3
+ *
4
+ * In FT8, nonstandard callsigns are transmitted as hashes (10-, 12-, or 22-bit).
5
+ * When a full callsign is decoded from a standard message, it is stored in this
6
+ * table so that future hashed references to it can be resolved.
7
+ *
8
+ * Mirrors Fortran: save_hash_call, hash10, hash12, hash22, ihashcall
9
+ */
10
+
11
+ import { C38 } from "./constants.js";
12
+
13
+ const MAGIC = 47055833459n;
14
+ const MAX_HASH22_ENTRIES = 1000;
15
+
16
+ function ihashcall(c0: string, m: number): number {
17
+ const s = c0.padEnd(11, " ").slice(0, 11).toUpperCase();
18
+ let n8 = 0n;
19
+ for (let i = 0; i < 11; i++) {
20
+ const j = C38.indexOf(s[i] ?? " ");
21
+ n8 = 38n * n8 + BigInt(j < 0 ? 0 : j);
22
+ }
23
+ const prod = BigInt.asUintN(64, MAGIC * n8);
24
+ return Number(prod >> BigInt(64 - m)) & ((1 << m) - 1);
25
+ }
26
+
27
+ /**
28
+ * Maintains a callsign ↔ hash lookup table for resolving hashed FT8 callsigns.
29
+ *
30
+ * Usage:
31
+ * ```ts
32
+ * const book = new HashCallBook();
33
+ * const decoded = decodeFT8(samples, sampleRate, { hashCallBook: book });
34
+ * // `book` now contains callsigns learned from decoded messages.
35
+ * // Subsequent calls reuse the same book to resolve hashed callsigns:
36
+ * const decoded2 = decodeFT8(samples2, sampleRate, { hashCallBook: book });
37
+ * ```
38
+ *
39
+ * You can also pre-populate the book with known callsigns:
40
+ * ```ts
41
+ * book.save("W9XYZ");
42
+ * book.save("PJ4/K1ABC");
43
+ * ```
44
+ */
45
+ export class HashCallBook {
46
+ private readonly calls10 = new Map<number, string>();
47
+ private readonly calls12 = new Map<number, string>();
48
+ private readonly hash22Entries: { hash: number; call: string }[] = [];
49
+
50
+ /**
51
+ * Store a callsign in all three hash tables (10, 12, 22-bit).
52
+ * Strips angle brackets if present. Ignores `<...>` and blank/short strings.
53
+ */
54
+ save(callsign: string): void {
55
+ let cw = callsign.trim().toUpperCase();
56
+ if (cw === "" || cw === "<...>") return;
57
+ if (cw.startsWith("<")) cw = cw.slice(1);
58
+ const gt = cw.indexOf(">");
59
+ if (gt >= 0) cw = cw.slice(0, gt);
60
+ cw = cw.trim();
61
+ if (cw.length < 3) return;
62
+
63
+ const n10 = ihashcall(cw, 10);
64
+ if (n10 >= 0 && n10 <= 1023) this.calls10.set(n10, cw);
65
+
66
+ const n12 = ihashcall(cw, 12);
67
+ if (n12 >= 0 && n12 <= 4095) this.calls12.set(n12, cw);
68
+
69
+ const n22 = ihashcall(cw, 22);
70
+ const existing = this.hash22Entries.findIndex((e) => e.hash === n22);
71
+ if (existing >= 0) {
72
+ this.hash22Entries[existing]!.call = cw;
73
+ } else {
74
+ if (this.hash22Entries.length >= MAX_HASH22_ENTRIES) {
75
+ this.hash22Entries.pop();
76
+ }
77
+ this.hash22Entries.unshift({ hash: n22, call: cw });
78
+ }
79
+ }
80
+
81
+ /** Look up a callsign by its 10-bit hash. Returns `null` if not found. */
82
+ lookup10(n10: number): string | null {
83
+ if (n10 < 0 || n10 > 1023) return null;
84
+ return this.calls10.get(n10) ?? null;
85
+ }
86
+
87
+ /** Look up a callsign by its 12-bit hash. Returns `null` if not found. */
88
+ lookup12(n12: number): string | null {
89
+ if (n12 < 0 || n12 > 4095) return null;
90
+ return this.calls12.get(n12) ?? null;
91
+ }
92
+
93
+ /** Look up a callsign by its 22-bit hash. Returns `null` if not found. */
94
+ lookup22(n22: number): string | null {
95
+ const entry = this.hash22Entries.find((e) => e.hash === n22);
96
+ return entry?.call ?? null;
97
+ }
98
+
99
+ /** Number of entries in the 22-bit hash table. */
100
+ get size(): number {
101
+ return this.hash22Entries.length;
102
+ }
103
+
104
+ /** Remove all stored entries. */
105
+ clear(): void {
106
+ this.calls10.clear();
107
+ this.calls12.clear();
108
+ this.hash22Entries.length = 0;
109
+ }
110
+ }
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  import { A1, A2, A3, A4, C38, FTALPH, MAX22, MAXGRID4, NTOKENS } from "./constants.js";
12
+ import type { HashCallBook } from "./hashcall.js";
12
13
 
13
14
  function bitsToUint(bits: number[], start: number, len: number): number {
14
15
  let val = 0;
@@ -18,7 +19,7 @@ function bitsToUint(bits: number[], start: number, len: number): number {
18
19
  return val;
19
20
  }
20
21
 
21
- function unpack28(n28: number): { call: string; success: boolean } {
22
+ function unpack28(n28: number, book: HashCallBook | undefined): { call: string; success: boolean } {
22
23
  if (n28 < 0 || n28 >= 268435456) return { call: "", success: false };
23
24
 
24
25
  if (n28 === 0) return { call: "DE", success: true };
@@ -31,7 +32,6 @@ function unpack28(n28: number): { call: string; success: boolean } {
31
32
  }
32
33
 
33
34
  if (n28 >= 1003 && n28 < NTOKENS) {
34
- // CQ with 4-letter directed call
35
35
  let m = n28 - 1003;
36
36
  let chars = "";
37
37
  for (let i = 3; i >= 0; i--) {
@@ -45,7 +45,9 @@ function unpack28(n28: number): { call: string; success: boolean } {
45
45
  }
46
46
 
47
47
  if (n28 >= NTOKENS && n28 < NTOKENS + MAX22) {
48
- // Hashed call we don't have a hash table, so show <...>
48
+ const n22 = n28 - NTOKENS;
49
+ const resolved = book?.lookup22(n22);
50
+ if (resolved) return { call: `<${resolved}>`, success: true };
49
51
  return { call: "<...>", success: true };
50
52
  }
51
53
 
@@ -127,8 +129,11 @@ function unpackText77(bits71: number[]): string {
127
129
 
128
130
  /**
129
131
  * Unpack a 77-bit FT8 message into a human-readable string.
132
+ *
133
+ * When a {@link HashCallBook} is provided, hashed callsigns are resolved from
134
+ * the book, and newly decoded standard callsigns are saved into it.
130
135
  */
131
- export function unpack77(bits77: number[]): { msg: string; success: boolean } {
136
+ export function unpack77(bits77: number[], book?: HashCallBook): { msg: string; success: boolean } {
132
137
  const n3 = bitsToUint(bits77, 71, 3);
133
138
  const i3 = bitsToUint(bits77, 74, 3);
134
139
 
@@ -148,8 +153,8 @@ export function unpack77(bits77: number[]): { msg: string; success: boolean } {
148
153
  const ir = bits77[58]!;
149
154
  const igrid4 = bitsToUint(bits77, 59, 15);
150
155
 
151
- const { call: call1, success: ok1 } = unpack28(n28a);
152
- const { call: call2Raw, success: ok2 } = unpack28(n28b);
156
+ const { call: call1, success: ok1 } = unpack28(n28a, book);
157
+ const { call: call2Raw, success: ok2 } = unpack28(n28b, book);
153
158
  if (!ok1 || !ok2) return { msg: "", success: false };
154
159
 
155
160
  let c1 = call1;
@@ -164,6 +169,8 @@ export function unpack77(bits77: number[]): { msg: string; success: boolean } {
164
169
  if (c2.indexOf("<") < 0) {
165
170
  if (ipb === 1 && i3 === 1 && c2.length >= 3) c2 += "/R";
166
171
  if (ipb === 1 && i3 === 2 && c2.length >= 3) c2 += "/P";
172
+ // Save the "from" call (call_2) into the hash book
173
+ if (book && c2.length >= 3) book.save(c2);
167
174
  }
168
175
 
169
176
  if (igrid4 <= MAXGRID4) {
@@ -191,6 +198,7 @@ export function unpack77(bits77: number[]): { msg: string; success: boolean } {
191
198
 
192
199
  if (i3 === 4) {
193
200
  // Type 4: One nonstandard call
201
+ const n12 = bitsToUint(bits77, 0, 12);
194
202
  let n58 = 0n;
195
203
  for (let i = 0; i < 58; i++) {
196
204
  n58 = n58 * 2n + BigInt(bits77[12 + i] ?? 0);
@@ -199,7 +207,6 @@ export function unpack77(bits77: number[]): { msg: string; success: boolean } {
199
207
  const nrpt = bitsToUint(bits77, 71, 2);
200
208
  const icq = bits77[73]!;
201
209
 
202
- // Decode n58 to 11-char string using C38 alphabet
203
210
  const c11chars: string[] = [];
204
211
  let remain = n58;
205
212
  for (let i = 10; i >= 0; i--) {
@@ -209,13 +216,15 @@ export function unpack77(bits77: number[]): { msg: string; success: boolean } {
209
216
  }
210
217
  const c11 = c11chars.join("").trim();
211
218
 
212
- const call3 = "<...>"; // We don't have a hash table for n12
219
+ const resolved = book?.lookup12(n12);
220
+ const call3 = resolved ? `<${resolved}>` : "<...>";
213
221
 
214
222
  let call1: string;
215
223
  let call2: string;
216
224
  if (iflip === 0) {
217
225
  call1 = call3;
218
226
  call2 = c11;
227
+ if (book) book.save(c11);
219
228
  } else {
220
229
  call1 = c11;
221
230
  call2 = call3;