File size: 5,277 Bytes
d37c079
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
#!/usr/bin/env python
"""Socket-level probe — debug 'no data' before touching the GUI.

Binds the local receive port (2244), sends the init sequence (b -> rate -> *) to the
board's command endpoint (192.168.4.1:8086), and prints exactly what comes back and
from where. Shows your local IPs so you can confirm the Mac holds 192.168.4.2.

  python src/acquisition/probe.py
  python src/acquisition/probe.py --start B     # try UPPERCASE start byte
"""
from __future__ import annotations

import argparse
import socket
import sys
import time
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from common.montage import ADC_MICROVOLTS_PER_COUNT  # noqa: E402

RATE = {250: b"1", 500: b"2", 1000: b"3"}


def local_ips():
    ips = set()
    try:
        ips.update(socket.gethostbyname_ex(socket.gethostname())[2])
    except OSError:
        pass
    # also probe the route toward the board
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.connect(("192.168.4.1", 8086)); ips.add(s.getsockname()[0]); s.close()
    except OSError:
        pass
    return sorted(ips)


def main() -> None:
    ap = argparse.ArgumentParser(description=__doc__)
    ap.add_argument("--host", default="192.168.4.1")
    ap.add_argument("--cmd-port", type=int, default=8086)
    ap.add_argument("--local-port", type=int, default=2244)
    ap.add_argument("--sfreq", type=int, default=250)
    ap.add_argument("--seconds", type=float, default=3.0)
    ap.add_argument("--start", default="b", help="start byte (default lowercase 'b'; try 'B')")
    args = ap.parse_args()

    ips = local_ips()
    print(f"local IPs: {ips}")
    if "192.168.4.2" not in ips:
        print("  ⚠ none of your IPs is 192.168.4.2 — the board sends data to .2, so it may")
        print("    not reach you. Join WiFi 'ESPBCI' and check `ifconfig | grep 192.168.4`.")

    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    s.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1 << 20)
    try:
        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    except OSError:
        pass
    s.bind(("0.0.0.0", args.local_port))
    print(f"bound local :{args.local_port}, commands -> {args.host}:{args.cmd_port}")

    for cmd in (args.start.encode(), RATE.get(args.sfreq, b"1"), b"*"):
        try:
            s.sendto(cmd, (args.host, args.cmd_port))
            print(f"  sent {cmd!r}")
        except OSError as e:
            print(f"  ⚠ send {cmd!r} failed: {e}  (route to {args.host}? on the WiFi?)")
        time.sleep(0.2)

    s.settimeout(2.0)
    stream = bytearray()
    ndg, sizes, srcs = 0, set(), set()
    t0 = time.time()
    try:
        while time.time() - t0 < args.seconds:
            pkt, addr = s.recvfrom(65536)
            ndg += 1; sizes.add(len(pkt)); srcs.add(addr); stream.extend(pkt)
    except socket.timeout:
        pass

    print(f"\nreceived {ndg} datagrams ({len(stream)} bytes) in {args.seconds:.0f}s | "
          f"sizes={sorted(sizes)} | sources={srcs}")
    if ndg == 0:
        print("NO DATA. Check: (1) Mac joined WiFi 'ESPBCI' (pw 12345678)? "
              "(2) a local IP == 192.168.4.2? (3) `ping 192.168.4.1` works? "
              "(4) board accepts the start byte — retry with `--start B`.")
        return

    # The board streams MTU-sized chunks; reframe the byte stream into 105-B frames.
    FRAME = 105
    frames, i = [], 0
    while i <= len(stream) - FRAME:
        if stream[i] == 0xA0 and stream[i + FRAME - 1] == 0xC0:
            frames.append(bytes(stream[i:i + FRAME])); i += FRAME
        else:
            i += 1
    print(f"reframed {len(frames)} valid 105-B frames (0xA0…0xC0) from the stream")
    if not frames:
        print("⚠ 收到数据但解不出 105B 帧 —— 通道数可能不是 32,或帧格式不同。")
        return

    import numpy as np
    from common.montage import CAP32_CHANNELS as CH

    def decode(fr):
        v = np.empty(32)
        for c in range(32):
            o = 2 + c * 3
            raw = int.from_bytes(fr[o:o + 3], "big")
            if raw & 0x800000:
                raw -= 16777216
            v[c] = raw * ADC_MICROVOLTS_PER_COUNT
        return v

    M = np.array([decode(fr) for fr in frames])          # (n_frames, 32) µV
    print(f"  seqs (should ++): {[fr[1] for fr in frames[:16]]}")
    print(f"  trigger(frame0): {int.from_bytes(frames[0][100:104], 'big')}")
    raw_mean, raw_std = M.mean(0), M.std(0)
    car = M - np.median(M, axis=1, keepdims=True)        # CAR per timepoint (across channels)
    car_std = car.std(0)

    print("\n  ch    raw_mean(µV)  raw_std(µV) | CAR_std(µV)")
    for c in range(32):
        print(f"  {CH[c]:<4} {raw_mean[c]:11.1f} {raw_std[c]:11.1f} | {car_std[c]:9.1f}")
    print(f"\n  common-mode test: median raw_std={np.median(raw_std):.0f}µV "
          f"-> after CAR={np.median(car_std):.1f}µV")
    if np.median(car_std) < max(1.0, np.median(raw_std) / 3):
        print("  ✅ 大部分是共模(参考漂移);CAR 后大幅下降 —— GUI 已加 CAR,重开应正常。")
    else:
        print("  ⚠ CAR 后仍大 —— 可能不只是共模;看是否某些电极浮空/接触差,或通道顺序/对齐问题。")


if __name__ == "__main__":
    main()