"""MH-DAQ firmware 1.1-reed. MicroPython, Raspberry Pi Pico RP2040.
Measurement only: no fuel outputs, no motor drive. Hardware not yet bench-tested.
JSONL slow telemetry; 'capture' over USB REPL runs a timestamped raw ADC burst.
All external analog signals must be 0-5V and share the isolated DAQ ground.
"""
from machine import Pin, SPI, I2C, ADC
import time, json, sys
VERSION='1.1-reed'
SPIBUS=SPI(1,baudrate=1000000,polarity=0,phase=1,sck=Pin(10),mosi=Pin(11),miso=Pin(12))
I2CBUS=I2C(0,sda=Pin(4),scl=Pin(5),freq=100000)
ADCS=[ADC(26),ADC(27),ADC(28)]
class Thermocouple:
 def __init__(self,cs):
  self.cs=Pin(cs,Pin.OUT,value=1)
  self.write(0x00,0x90) # continuous, open-circuit check (Rs <5k), 60 Hz rejection
  self.write(0x01,0x03) # one sample average, type K
  self.write(0x02,0x00) # unmask FAULT output
  if self.read(0,2) != b'\x90\x03':raise OSError('MAX31856 configuration readback failed')
 def write(self,reg,value):
  self.cs(0)
  try:SPIBUS.write(bytes([reg|0x80,value]))
  finally:self.cs(1)
 def read(self,reg,n=1):
  self.cs(0)
  try:SPIBUS.write(bytes([reg]));return SPIBUS.read(n)
  finally:self.cs(1)
 def sample(self):
  if self.read(0,2) != b'\x90\x03':raise OSError('MAX31856 configuration lost')
  fault=self.read(0x0f)[0];v=int.from_bytes(self.read(0x0c,3),'big')
  if v&0x800000:v-=1<<24
  return {'C':None if fault else v/4096,'fault':fault}
def thermocouple(cs):
 try:return Thermocouple(cs)
 except OSError:return None
HOT=thermocouple(13);COLD=thermocouple(14)
class Power:
 def __init__(self,address=0x40,shunt_ohm=.015):
  self.address=address;self.shunt=shunt_ohm
  I2CBUS.writeto_mem(address,0,b'\x00\x00') # ADCRANGE=0, ±163.84 mV
 def sample(self):
  bus=int.from_bytes(I2CBUS.readfrom_mem(self.address,5,3),'big')>>4
  raw=int.from_bytes(I2CBUS.readfrom_mem(self.address,4,3),'big')>>4
  if raw&0x80000:raw-=1<<20
  volts=bus*195.3125e-6;amps=raw*312.5e-9/self.shunt
  return {'bus_V':volts,'current_A':amps,'terminal_W':volts*amps}
try:POWER=Power()
except OSError:POWER=None

def sample():
 # Raw ADC values and timestamps are retained. 3.3V reference is nominal;
 # calibrate reference/divider and sensor transfer functions before engineering use.
 raw=[adc.read_u16() for adc in ADCS]
 row={'version':VERSION,'ticks_ms':time.ticks_ms(),'adc_raw':raw,'input_nominal_V':[x/65535*3.3*1.75 for x in raw]}
 for name,sensor in [('hot',HOT),('cold',COLD)]:
  try:row[name]=sensor.sample() if sensor else {'C':None,'error':'MAX31856 absent or readback failed at boot'}
  except OSError as e:row[name]={'C':None,'error':'SPI read failed'}
 try:row['power']=POWER.sample() if POWER else {'error':'INA228 absent at boot'}
 except OSError:row['power']={'error':'I2C read failed'}
 return row

def capture(samples=4000,period_us=250):
 """Buffer an ADC burst, then print CSV. Timing is measured, not promised.
 The three ADC conversions are sequential (not simultaneous); compensate
 channel skew when estimating PV work/phase. Pico ADC is not metrology grade.
 """
 from array import array
 stamps=array('I',[0]*samples);raw=array('H',[0]*(samples*3));deadline=time.ticks_us();start=deadline;late=0
 for i in range(samples):
  while time.ticks_diff(time.ticks_us(),deadline)<0:pass
  now=time.ticks_us();stamps[i]=time.ticks_diff(now,start)
  if time.ticks_diff(now,deadline)>period_us:late+=1
  raw[i*3]=ADCS[0].read_u16();raw[i*3+1]=ADCS[1].read_u16();raw[i*3+2]=ADCS[2].read_u16();deadline=time.ticks_add(deadline,period_us)
 print('# '+json.dumps({'version':VERSION,'samples':samples,'requested_period_us':period_us,'late_samples':late}))
 print('t_us,piston_adc,displacer_adc,pressure_adc')
 for i in range(samples):print('%d,%d,%d,%d'%(stamps[i],raw[i*3],raw[i*3+1],raw[i*3+2]))
time.sleep_ms(250) # allow first thermocouple conversions
print(json.dumps({'version':VERSION,'status':'measurement only','i2c_addresses':I2CBUS.scan()}))
try:
 while True:print(json.dumps(sample()));time.sleep_ms(500)
except KeyboardInterrupt:print('DAQ paused. capture() is available at REPL; Ctrl-D restarts telemetry.')
