#!/usr/bin/env python3
"""Hardware diagnostics for the RM3100 / CH347 link.

Walks the signal chain from USB inwards and stops at the first thing that is
broken, so a failure points at a specific wire rather than "it doesn't work":

    adapter -> I2C bus -> sensor identity -> registers -> analog section

Usage:  ./.venv/bin/python diagnose-comms.py
"""

import sys
import time

import ch347
import rm3100

# BIST with STE=1, BW=11 (4 sleep oscillation cycles), BP=11 (4 LR periods):
# the most forgiving self-test settings (manual Tables 5-6, 5-7).
BIST_RUN = 0x8F

# Matches the CH347I2C default; named here only so the report can state it.
BUS_KHZ = 750


def check(label, ok, detail=""):
    print(f"  [{'OK ' if ok else 'FAIL'}] {label}" + (f" -- {detail}" if detail else ""))
    return ok


def main():
    print("1. USB adapter")
    try:
        bus = ch347.CH347I2C()
    except IOError as exc:
        check("open CH347", False, str(exc))
        print("\n-> Run ./setup.sh, or check the adapter is plugged in and in Mode 1.")
        return 1
    check("open CH347 and set I2C speed", True,
          f"interface {ch347.INTERFACE} claimed, {BUS_KHZ} kHz")

    with bus:
        print("\n2. I2C bus")
        found = bus.scan()
        if not check("devices respond", bool(found),
                     ", ".join(f"0x{a:02x}" for a in found) if found else "bus is silent"):
            print("\n-> Nothing ACKs. In order of likelihood:")
            print("   - I2CEN not tied HIGH, so the chip is in SPI mode (most common)")
            print("   - SDA/SCL swapped")
            print("   - missing pull-up resistors on SDA and SCL")
            print("   - module not powered")
            return 1

        candidates = [a for a in found if a in rm3100.RM3100.ADDRESSES]
        if not check("an address in the RM3100 range 0x20-0x23", bool(candidates),
                     ", ".join(f"0x{a:02x}" for a in candidates) if candidates
                     else "responders are not RM3100 addresses"):
            return 1

        sensor = rm3100.RM3100(bus, candidates[0])
        print(f"\n3. Sensor identity (at 0x{sensor.address:02x})")
        revid = sensor.revid()
        check("REVID", revid == rm3100.EXPECTED_REVID,
              f"0x{revid:02x}" + ("" if revid == rm3100.EXPECTED_REVID
                                  else f", expected 0x{rm3100.EXPECTED_REVID:02x}"))

        print("\n4. Register read/write")
        sensor.stop_cmm()
        time.sleep(0.05)
        original = sensor.get_cycle_counts()
        sensor.set_cycle_counts(100)
        readback = sensor.get_cycle_counts()
        ok = readback == (100, 100, 100)
        check("cycle counts write then read back", ok, f"wrote 100, read {readback}")
        sensor.set_cycle_counts(original[0] or rm3100.DEFAULT_CYCLE_COUNT)
        if not ok:
            print("\n-> Registers are unreliable. Try a lower bus speed or shorter wires.")
            return 1

        print("\n5. Analog section (BIST -- drives the sensor coils)")
        sensor.write_reg(rm3100.REG_BIST, [BIST_RUN])
        sensor.write_reg(rm3100.REG_POLL, [rm3100.POLL_ALL_AXES])
        time.sleep(0.5)
        bist = sensor.read_reg(rm3100.REG_BIST)[0][0]
        sensor.write_reg(rm3100.REG_BIST, [0x00])

        axes = {"X": (bist >> 4) & 1, "Y": (bist >> 5) & 1, "Z": (bist >> 6) & 1}
        all_ok = all(axes.values())
        check("LR oscillators", all_ok,
              f"BIST=0x{bist:02x} " + " ".join(f"{k}OK={v}" for k, v in axes.items()))
        if not all_ok:
            print("\n-> The coils are not oscillating, though I2C is fine. Check:")
            if not any(axes.values()):
                print("   - AVDD/AVSS not connected (most common when ALL axes fail).")
                print("     Registers run on DVDD alone, so I2C works while the analog")
                print("     section is dead. Tie AVDD to the same 3V3 rail as DVDD")
                print("     (they must stay within 0.1 V), and AVSS to ground.")
                print("   - REXT timing resistor (33k) missing")
            else:
                print(f"   - coil wiring for {', '.join(k for k, v in axes.items() if not v)}")
            return 1

        print("\n6. Live measurement")
        sensor.configure()
        # BIST zeroes the measurement registers and leaves DRDY set. With
        # DRC1=1 only a results read clears it, so discard one -- otherwise
        # wait_for_data() returns instantly on the stale flag and reports zeros.
        sensor.read_raw()
        sensor.set_rate(0x96)
        sensor.start_cmm()
        try:
            ready = sensor.wait_for_data(timeout=2.0)
            if not check("DRDY asserts", ready, "" if ready else "no data within 2 s"):
                return 1
            counts, tesla = sensor.read_measurements()
            magnitude = sum(v * v for v in tesla) ** 0.5
            nt = [v * rm3100.NT_PER_TESLA for v in tesla]
            plausible = check(
                "field magnitude is plausible", 25e-6 <= magnitude <= 65e-6,
                f"{magnitude * rm3100.NT_PER_TESLA:,.0f} nT "
                "(Earth's field is 25,000-65,000)")
            print(f"       raw {counts}")
            print(f"       X {nt[0]:+,.0f}  Y {nt[1]:+,.0f}  Z {nt[2]:+,.0f} nT")
            if not plausible:
                print("\n-> Communication works but the numbers are wrong. A reading of\n"
                      "   exactly zero means the results registers were read without a\n"
                      "   fresh measurement behind them; anything else suggests the gain\n"
                      "   or the int24 decode.")
                return 1
        finally:
            sensor.stop_cmm()

    print("\nAll checks passed -- the link is fully working.")
    return 0


if __name__ == "__main__":
    sys.exit(main())
