@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/README.md +33 -24
- package/dist/ft8ts.cjs +130 -11
- package/dist/ft8ts.cjs.map +1 -1
- package/dist/ft8ts.d.ts +57 -1
- package/dist/ft8ts.mjs +130 -12
- package/dist/ft8ts.mjs.map +1 -1
- package/example/browser/index.html +4 -4
- package/package.json +51 -51
- package/src/ft8/decode.ts +15 -4
- package/src/index.ts +1 -0
- package/src/util/hashcall.ts +110 -0
- package/src/util/unpack_jt77.ts +17 -8
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|