#!/usr/bin/python3

###################################################
# Undervoltage Evidence Logger 1.01 (20-07-2026)  #
# Temporary - PSU case Comfortica-Hub 2024000061  #
###################################################

# Samples the on-board INA219 (0x40, same library the firmware uses) at ~10Hz
# plus the SoC's own undervoltage alarm. Every line is fsync'd so the last one
# survives each power cut. Writes stay small: one summary line per minute and
# one line per sag event (rate-limited). Remove after the PSU swap is proven.
#
# 1.01: also records the LED controller's (ESP32) uptime via the protocol's
# get_uptime (0x06), on the BOOT line and every heartbeat. That one number
# separates the two remaining power theories: esp uptime ~= Pi uptime at each
# boot means both reset together (PSU / mains side); esp uptime carrying on
# through Pi deaths means only the Pi's supply branch is dropping (cable /
# connector). An esp reset to ~0 mid-heartbeats while the Pi stays up is a
# rail event the Pi survived - also worth knowing.

import os
import re
import sys
import time

sys.path.insert(0, '/opt/artiled')
from ina219 import INA219
import artiled

LOG   = '/var/log/undervolt_evidence.log'
ALARM = '/sys/class/hwmon/hwmon1/in0_lcrit_alarm'
SAG_V = 4.85          # log immediately below this; the SoC resets around 4.63


def note(msg):
    with open(LOG, 'a') as f:
        f.write(time.strftime('%Y-%m-%d %H:%M:%S ') + msg + '\n')
        f.flush()
        os.fsync(f.fileno())


def alarm():
    try:
        with open(ALARM) as f:
            return f.read().strip()
    except OSError:
        return 'n/a'


def uptime():
    with open('/proc/uptime') as f:
        return int(float(f.read().split()[0]))


def esp_uptime():
    # Controller uptime in seconds via artiled_server (TCP 1080). Short
    # timeout and a fresh connection per query: the logger must never stall
    # the sampling loop or hold a connection across the controller's own
    # resets. Reply looks like '(12345)'.
    try:
        ctl = artiled.Controller('uvlog', '127.0.0.1', 1080, timeout=2)
        raw = str(ctl.get_uptime())
        m = re.search(r'(\d+)', raw)
        return m.group(1) + 's' if m else 'n/a'
    except Exception:
        return 'n/a'


ina = INA219(address=0x40, debug=False)
note('BOOT  uptime=%ss alarm=%s v=%.3f esp=%s'
     % (uptime(), alarm(), ina.getBusVoltage_V(), esp_uptime()))

prev_alarm = alarm()
vmin, vmax, n, isum = 99.0, 0.0, 0, 0.0
last_beat = time.time()
last_sag = 0.0

while True:
    try:
        v = ina.getBusVoltage_V()
        i = ina.getCurrent_mA() / 1000.0
    except Exception:
        time.sleep(1)
        continue

    n += 1
    isum += i
    if v < vmin:
        vmin = v
    if v > vmax:
        vmax = v

    if v < SAG_V and time.time() - last_sag > 1:
        note('SAG   v=%.3f i=%.3fA alarm=%s uptime=%ss' % (v, i, alarm(), uptime()))
        last_sag = time.time()

    a = alarm()
    if a != prev_alarm:
        note('ALARM %s->%s v=%.3f uptime=%ss' % (prev_alarm, a, v, uptime()))
        prev_alarm = a

    if time.time() - last_beat >= 60:
        note('MIN/MAX v=%.3f/%.3f avg_i=%.3fA samples=%d alarm=%s uptime=%ss esp=%s'
             % (vmin, vmax, isum / max(n, 1), n, a, uptime(), esp_uptime()))
        vmin, vmax, n, isum = 99.0, 0.0, 0, 0.0
        last_beat = time.time()

    time.sleep(0.1)
