summaryrefslogtreecommitdiff
path: root/stacks/crdop/tools/max25-signal-sniffer.py
blob: fed95f773758c242653f9209dd9c4838eab42160 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#!/usr/bin/env python3
"""MAX25 signal sniffer — analyze Bell 202 AFSK on ALSA capture or PCM file."""
from __future__ import annotations

import argparse
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parents[3]
LIB = ROOT / "stacks" / "crdop" / "lib"
sys.path.insert(0, str(LIB))

from acoustic_engine import AcousticEngine  # noqa: E402
from sound_proxy import SoundConfig, SoundProxy  # noqa: E402


def read_wav_pcm(path: Path) -> bytes:
    import wave

    with wave.open(str(path), "rb") as wf:
        if wf.getsampwidth() != 2 or wf.getnchannels() != 1:
            raise SystemExit("WAV must be mono S16_LE")
        return wf.readframes(wf.getnframes())


def main() -> int:
    ap = argparse.ArgumentParser(description="MAX25 acoustic signal sniffer (Bell 202 1200 bd)")
    ap.add_argument("-D", "--device", default="", help="ALSA capture device (hw: or plughw:)")
    ap.add_argument("-r", "--rate", type=int, default=48000)
    ap.add_argument("-f", "--file", type=Path, help="Read mono S16 WAV instead of ALSA")
    ap.add_argument("-t", "--seconds", type=float, default=2.0, help="ALSA capture duration")
    ap.add_argument("--loopback", action="store_true", help="Internal DSP loopback (no audio hw)")
    ap.add_argument("--mark", action="store_true", help="Play mark calibration tone (1200 Hz)")
    ap.add_argument("--space", action="store_true", help="Play space calibration tone (2200 Hz)")
    args = ap.parse_args()

    cfg = SoundConfig(capture=args.device or "default", sample_rate=args.rate)
    eng = AcousticEngine(sample_rate=args.rate, sound=cfg)

    if args.loopback:
        rep = eng.loopback_self_test()
        print(f"loopback samples={rep.samples} symbols={rep.symbols} transitions={rep.transitions}")
        print(f"mark={rep.mark_ratio:.2%} space={rep.space_ratio:.2%}")
        for line in rep.decode_lines:
            print(line)
        return 0 if rep.decode_lines else 1

    if args.mark:
        eng.play_mark_calibration(args.seconds)
        print(f"played mark tone {args.seconds}s")
        return 0
    if args.space:
        eng.play_space_calibration(args.seconds)
        print(f"played space tone {args.seconds}s")
        return 0

    if args.file:
        pcm = read_wav_pcm(args.file)
    else:
        if not args.device:
            print("error: -D hw:... or --file or --loopback required", file=sys.stderr)
            return 2
        proxy = SoundProxy(cfg)
        proxy.start_capture()
        nbytes = int(args.rate * args.seconds) * 2
        pcm = proxy.read_capture(nbytes)
        proxy.close()

    rep = eng.analyze_pcm(pcm)
    print(f"sniff samples={rep.samples} symbols={rep.symbols} transitions={rep.transitions}")
    print(f"mark={rep.mark_ratio:.2%} space={rep.space_ratio:.2%} frames={len(rep.frames)}")
    for line in rep.decode_lines:
        print(line)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
git clone -b <branch> https://cgit.mode42.com/<repo>.git
git clone -b <branch> git://cgit.mode42.com/<repo>.git

info@mode42.com