#!/usr/bin/env python3

"""
FILE: hack-temp.py

DESCRIPTION:
Incremental firmware patcher with minimal, conservative steps to bias readings.

BRIEF:
Provides three modes:
- pool_only: change only the literal pool constant (scaling effect; least invasive)
- temp_only_vadd: convert temperature path to vadd.f32 and set a small negative pool
- both_vadd: convert both paths to vadd.f32 and set a small negative pool

Intended to let you test changes incrementally, keeping humidity stable while
you evaluate temperature bias first.

AUTHOR: Kevin Thomas
CREATION DATE: November 1, 2025
UPDATE DATE: November 1, 2025
"""

import sys
import os
import struct

BIN_PATH = "build/0x001a_operators.bin"

# Offsets
OFF_410 = 0x410
OFF_414 = 0x414
OFF_POOL = 0x42C

# Original encodings (for visibility only)
ORIG_410 = bytes.fromhex("a6ee257a")  # vfma.f32 s14, s12, s11
ORIG_414 = bytes.fromhex("e6eea57a")  # vfma.f32 s15, s13, s11
ORIG_POOL = bytes.fromhex("cccccc3d")  # 0.1f

# Encodings for vadd.f32 s14, s14, s11 and vadd.f32 s15, s15, s11
VADD_S14_S11 = struct.pack("<I", 0xEEB47A06)  # 06 7a b4 ee
VADD_S15_S11 = struct.pack("<I", 0xEEB47A86)  # 86 7a b4 ee


def read_at(path, offset, n):
    """Read bytes from a binary file at a given offset.

    Parameters
    ----------
    path : str
        Filesystem path to the binary file.
    offset : int
        Byte offset from the start of the file.
    n : int
        Number of bytes to read.

    Returns
    -------
    bytes
        The raw bytes read from the file.

    Raises
    ------
    IOError
        If the file cannot be opened or read.
    """
    with open(path, "rb") as f:
        f.seek(offset)
        return f.read(n)


def write_at(path, offset, data):
    """Write bytes into a binary file at a given offset.

    Parameters
    ----------
    path : str
        Filesystem path to the binary file.
    offset : int
        Byte offset from the start of the file.
    data : bytes
        Bytes to write into the file.

    Raises
    ------
    IOError
        If the file cannot be opened or written.
    """
    with open(path, "rb+") as f:
        f.seek(offset)
        f.write(data)


def fmt_float(b):
    """Format a 4-byte sequence as hex and float if possible.

    Parameters
    ----------
    b : bytes
        A 4-byte sequence.

    Returns
    -------
    str
        Hexadecimal string with float interpretation if valid.
    """
    hx = b.hex()
    try:
        val = struct.unpack("<f", b)[0]
        return f"{hx} (float {val:.6f})"
    except Exception:
        return hx


def patch_pool_only(new_pool_float=-1.0):
    """Patch only the pool constant to a new float value.

    Parameters
    ----------
    new_pool_float : float, optional
        New float value to write into the pool constant (default -1.0).

    Returns
    -------
    None
    """
    print("[mode] pool_only")
    curp = read_at(BIN_PATH, OFF_POOL, 4)
    print(f"Current @0x1000042C: {fmt_float(curp)} (expected {ORIG_POOL.hex()} == 0.1f)")
    newb = struct.pack("<f", new_pool_float)
    write_at(BIN_PATH, OFF_POOL, newb)
    chkp = read_at(BIN_PATH, OFF_POOL, 4)
    print(f"Patched @0x1000042C: {fmt_float(chkp)} (target {newb.hex()} == {new_pool_float}f)")


def patch_temp_only_vadd(new_pool_float=-2.0):
    """Patch temperature path to vadd and set pool to a small negative.

    Parameters
    ----------
    new_pool_float : float, optional
        New float value to write into the pool constant (default -2.0).

    Returns
    -------
    None
    """
    print("[mode] temp_only_vadd")
    cur414 = read_at(BIN_PATH, OFF_414, 4)
    curp = read_at(BIN_PATH, OFF_POOL, 4)
    print(f"Current @0x10000414: {cur414.hex()} (expected {ORIG_414.hex()})")
    print(f"Current @0x1000042C: {fmt_float(curp)}")
    write_at(BIN_PATH, OFF_414, VADD_S15_S11)
    write_at(BIN_PATH, OFF_POOL, struct.pack("<f", new_pool_float))
    chk414 = read_at(BIN_PATH, OFF_414, 4)
    chkp = read_at(BIN_PATH, OFF_POOL, 4)
    print(f"Patched @0x10000414: {chk414.hex()} (should be {VADD_S15_S11.hex()})")
    print(f"Patched @0x1000042C: {fmt_float(chkp)} (target {new_pool_float}f)")


def patch_both_vadd(new_pool_float=-2.0):
    """Patch both humidity and temperature paths to vadd and set pool.

    Parameters
    ----------
    new_pool_float : float, optional
        New float value to write into the pool constant (default -2.0).

    Returns
    -------
    None
    """
    print("[mode] both_vadd")
    cur410 = read_at(BIN_PATH, OFF_410, 4)
    cur414 = read_at(BIN_PATH, OFF_414, 4)
    curp = read_at(BIN_PATH, OFF_POOL, 4)
    print(f"Current @0x10000410: {cur410.hex()} (expected {ORIG_410.hex()})")
    print(f"Current @0x10000414: {cur414.hex()} (expected {ORIG_414.hex()})")
    print(f"Current @0x1000042C: {fmt_float(curp)}")
    write_at(BIN_PATH, OFF_410, VADD_S14_S11)
    write_at(BIN_PATH, OFF_414, VADD_S15_S11)
    write_at(BIN_PATH, OFF_POOL, struct.pack("<f", new_pool_float))
    chk410 = read_at(BIN_PATH, OFF_410, 4)
    chk414 = read_at(BIN_PATH, OFF_414, 4)
    chkp = read_at(BIN_PATH, OFF_POOL, 4)
    print(f"Patched @0x10000410: {chk410.hex()} (should be {VADD_S14_S11.hex()})")
    print(f"Patched @0x10000414: {chk414.hex()} (should be {VADD_S15_S11.hex()})")
    print(f"Patched @0x1000042C: {fmt_float(chkp)} (target {new_pool_float}f)")


def main():
    """Dispatch patch mode and perform incremental modifications.

    Returns
    -------
    None

    Raises
    ------
    FileNotFoundError
        If the binary file specified by BIN_PATH does not exist.
    """
    if not os.path.exists(BIN_PATH):
        raise FileNotFoundError(f"Binary not found: {BIN_PATH}")
    mode = (sys.argv[1] if len(sys.argv) > 1 else "pool_only").strip().lower()
    if mode == "pool_only":
        patch_pool_only(new_pool_float=-1.0)
    elif mode == "temp_only_vadd":
        patch_temp_only_vadd(new_pool_float=-2.0)
    elif mode == "both_vadd":
        patch_both_vadd(new_pool_float=-2.0)
    else:
        print(f"Unknown mode: {mode}")
        print("Use: pool_only | temp_only_vadd | both_vadd")
        sys.exit(2)


if __name__ == "__main__":
    main()
    print("Patch complete. Convert to UF2 and flash. Test each mode incrementally.")
