# ==============================
# RAK4630 Holographic Lattice Node
# ==============================
# Multi-node, DAC analog superposition + phyllotaxis networking
# ------------------------------

import time
import math
import struct
import urandom as random
from machine import Timer, DAC, Pin
from network import LoRa

# -----------------------------
# Config
# -----------------------------
NODE_ID = 0
STRANDS, SLOTS = 8, 4
TICK_INTERVAL = 0.05  # 50 ms per lattice tick
MOD_INDEX = 0.1       # modulation depth for DAC
DAC_PIN = 25          # DAC output pin
CARRIER_FREQ = 1000.0 # base carrier Hz for DAC output
SAMPLE_RATE = 5000    # DAC update rate
NEIGHBOR_COUNT = 4    # Phyllotaxis neighbors

# -----------------------------
# DAC setup
# -----------------------------
dac = DAC(Pin(DAC_PIN))

# -----------------------------
# LoRa setup
# -----------------------------
lora = LoRa(mode=LoRa.LORA, freq=915e6, sf=7, bw=125e3)
lora_sock = lora.socket()
lora_sock.setblocking(False)

# -----------------------------
# Lattice initialization
# -----------------------------
lattice = [[0.5 for _ in range(SLOTS)] for _ in range(STRANDS)]

# -----------------------------
# Phyllotaxis positions & neighbors
# -----------------------------
NODE_COUNT = 16  # total nodes in network

def phyllotaxis_positions(n, c=1.0):
    golden_angle = math.pi * (3 - math.sqrt(5))
    positions = []
    for k in range(n):
        r = c * math.sqrt(k)
        theta = k * golden_angle
        x, y = r * math.cos(theta), r * math.sin(theta)
        positions.append((x, y))
    return positions

positions = phyllotaxis_positions(NODE_COUNT)

def compute_neighbors(node_idx):
    px, py = positions[node_idx]
    distances = []
    for i, (x, y) in enumerate(positions):
        if i == node_idx:
            continue
        d = math.sqrt((px - x)**2 + (py - y)**2)
        distances.append((d, i))
    distances.sort()
    return [idx for _, idx in distances[:NEIGHBOR_COUNT]]

neighbors = compute_neighbors(NODE_ID)

# -----------------------------
# DAC FM modulation
# -----------------------------
dac_phase = 0.0
dac_dt = 1.0 / SAMPLE_RATE

def lattice_to_dac(lattice, neighbor_lattices):
    global dac_phase
    # Combine local and neighbors' lattices
    combined = [[val for val in strand] for strand in lattice]
    for n_lattice in neighbor_lattices:
        for s in range(STRANDS):
            for slot in range(SLOTS):
                combined[s][slot] += 0.25 * (n_lattice[s][slot] - combined[s][slot])  # blend weight
    flat = [val for strand in combined for val in strand]
    fm_sum = sum([(v - 0.5) * MOD_INDEX for v in flat])
    dac_phase += 2 * math.pi * (CARRIER_FREQ + fm_sum) * dac_dt
    dac_phase %= 2 * math.pi
    dac_value = int((math.sin(dac_phase) + 1.0) * 127.5)
    return dac_value

# -----------------------------
# Lattice transmission
# -----------------------------
def transmit_lattice():
    flat = [val for strand in lattice for val in strand]
    data = b''.join([struct.pack('<f', v) for v in flat])
    for n_idx in neighbors:
        lora_sock.send(data)

# -----------------------------
# Lattice reception
# -----------------------------
def receive_lattice():
    neighbor_lattices = []
    try:
        while True:
            data = lora_sock.recv(256)
            if not data:
                break
            new_vals = [struct.unpack('<f', data[i*4:i*4+4])[0] for i in range(STRANDS*SLOTS)]
            n_lattice = [[0.0 for _ in range(SLOTS)] for _ in range(STRANDS)]
            for s in range(STRANDS):
                for slot in range(SLOTS):
                    idx = s*SLOTS + slot
                    n_lattice[s][slot] = new_vals[idx]
            neighbor_lattices.append(n_lattice)
    except:
        pass
    return neighbor_lattices

# -----------------------------
# Node tick
# -----------------------------
def node_tick(timer):
    # Random small fluctuation
    for s in range(STRANDS):
        for slot in range(SLOTS):
            lattice[s][slot] += 0.01 * (random.getrandbits(1)*2 - 1)
            lattice[s][slot] = max(0.0, min(1.0, lattice[s][slot]))

    # Transmit lattice to neighbors
    transmit_lattice()

    # Receive and blend neighbors
    neighbor_lattices = receive_lattice()
    
    # DAC output
    dac.write(lattice_to_dac(lattice, neighbor_lattices))

# -----------------------------
# Timer setup
# -----------------------------
timer = Timer.Alarm(node_tick, TICK_INTERVAL, periodic=True)

# -----------------------------
# Keep alive
# -----------------------------
while True:
    time.sleep(1)
