
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/*
 *
 *	File		:	luminous.cpp
 *	Release		:	v2.0.0
 *
 *	Created on	:	Fri 12 Nov 2025
 *		Author	:	hii-nice-2-meet-u
 *
 */
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

#include "luminous.h"

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

#if defined(__AVR__) // Only compile this section on AVR boards (Arduino Uno, Nano, Mega, etc.)

//* ===================================================================
//+ LUMINOUS::begin() - Initialize the pins and choose SPI mode
//* ===================================================================

bool LUMINOUS::begin(
    uint8_t sclk, //\ Clock pin  (SCK)
    uint8_t miso, //\ Data from sensor to Arduino (MISO)
    uint8_t mosi, //\ Data from Arduino to sensor (MOSI)
    uint8_t ss0,  //\ Chip-Select for sensor #0
    uint8_t ss1)  //\ Chip-Select for sensor #1
{
	// Save the pin numbers so we can use them later
	_sclk = sclk;
	_miso = miso;
	_mosi = mosi;
	_ss0  = ss0;
	_ss1  = ss1;

	//---------------------------------------------------------------
	/// 1.  Set the direction of each pin (INPUT or OUTPUT)
	//---------------------------------------------------------------

	pinMode(_sclk, OUTPUT); // Clock is generated by us → OUTPUT
	pinMode(_miso, INPUT);  // We only read this pin → INPUT
	pinMode(_mosi, OUTPUT); // We send data on this pin → OUTPUT
	pinMode(_ss0, OUTPUT);  // Chip-select pins are controlled by us
	pinMode(_ss1, OUTPUT);

	//---------------------------------------------------------------
	/// 2.  For speed we work directly with AVR registers (very fast!)
	///     These macros give us the actual memory address of each PORT
	//---------------------------------------------------------------

	PORTX_SCLK = portOutputRegister(digitalPinToPort(_sclk)); // e.g. &PORTB
	PINX_MISO  = portInputRegister(digitalPinToPort(_miso));  // e.g. &PINB
	PORTX_MOSI = portOutputRegister(digitalPinToPort(_mosi));
	PORTX_SS0  = portOutputRegister(digitalPinToPort(_ss0));
	PORTX_SS1  = portOutputRegister(digitalPinToPort(_ss1));

	// Bit mask for the exact bit inside the port (e.g. bit 5 = 0b00100000)
	X_SCLK = digitalPinToBitMask(_sclk);
	X_MISO = digitalPinToBitMask(_miso);
	X_MOSI = digitalPinToBitMask(_mosi);
	X_SS0  = digitalPinToBitMask(_ss0);
	X_SS1  = digitalPinToBitMask(_ss1);

	//---------------------------------------------------------------
	/// 3.  De-select both sensors (Chip-Select = HIGH = inactive)
	//---------------------------------------------------------------

	*PORTX_SS0 |= X_SS0; // Set SS0 pin HIGH
	*PORTX_SS1 |= X_SS1; // Set SS1 pin HIGH

	//---------------------------------------------------------------
	/// 4.  Check if we are using the hardware SPI pins
	///     (13=SCK, 12=MISO, 11=MOSI on Uno/Nano)
	//---------------------------------------------------------------

	if (_sclk == PIN_SPI_SCK || _miso == PIN_SPI_MISO || _mosi == PIN_SPI_MOSI)
	{
		//| Yes → use the super-fast built-in SPI hardware
		SPCR = (1 << SPE) |        // Enable SPI
		       (1 << MSTR) |       // Arduino is the Master
		       (1 << SPR0);        // Clock divider (fosc/16, safe speed)
		SPSR       = (1 << SPI2X); // Double speed → fosc/8 (still safe for most sensors)
		isMode_SPI = true;
	}
	else
	{
		//| No → we will do "bit-banging" (manual control of every pulse)
		SPCR &= ~(1 << SPE); // Disable hardware SPI
		isMode_SPI = false;
	}

	return true; // Initialization always succeeds in this simple version
}

//* ===================================================================
//+ read_MANUAL() - Bit-banged (software) SPI version
//* ===================================================================

uint16_t LUMINOUS::read_MANUAL(uint8_t Channel)
{
	uint16_t result = 0; // Will hold the 10-bit value from the sensor

	//---------------------------------------------------------------
	/// 1.  Select which sensor we talk to (bit 3 of Channel chooses)
	//---------------------------------------------------------------

	if (Channel & 0b00001000)   // If bit 3 is 1 → use sensor 0
		*PORTX_SS0 &= ~(X_SS0); // SS0 LOW = selected
	else
		*PORTX_SS1 &= ~(X_SS1); // SS1 LOW = selected

	*PORTX_SCLK &= ~(X_SCLK); // Start with clock LOW

	// The sensor expects the top 5 bits to be: 00011xxx
	// We force the "read" command by setting bits 3 and 4
	Channel |= 0b00011000;

	//---------------------------------------------------------------
	/// 2.  Send 5 command bits (MSB first)
	//---------------------------------------------------------------

	for (unsigned char ii = 0; ii < 5; ii++)
	{
		// Set MOSI according to current bit
		if (Channel & 0b00010000)  // Check bit 4 (the highest bit we care about)
			*PORTX_MOSI |= X_MOSI; // MOSI HIGH
		else
			*PORTX_MOSI &= ~X_MOSI; // MOSI LOW

		Channel <<= 1; // Shift next bit into position

		// Create a clock pulse (LOW → HIGH → LOW)
		*PORTX_SCLK |= X_SCLK;  // Clock HIGH
		*PORTX_SCLK &= ~X_SCLK; // Clock LOW
	}

	//---------------------------------------------------------------
	/// 3.  Now receive 10 data bits from the sensor +1 invalid data
	//---------------------------------------------------------------

	for (unsigned char ii = 0; ii < 11; ii++)
	{
		// Clock pulse so the sensor puts the next bit on MISO
		*PORTX_SCLK |= X_SCLK;  // HIGH
		*PORTX_SCLK &= ~X_SCLK; // LOW

		result <<= 1;            // Make room for new bit
		if (*PINX_MISO & X_MISO) // Is MISO HIGH?
			result |= 1;         // Set lowest bit to 1
	}

	//---------------------------------------------------------------
	/// 4.  Deselect both sensors and leave clock HIGH (idle state)
	//---------------------------------------------------------------

	*PORTX_SS0 |= X_SS0;   // SS0 HIGH
	*PORTX_SS1 |= X_SS1;   // SS1 HIGH
	*PORTX_SCLK |= X_SCLK; // Clock HIGH (idle)

	return result; // 0–1023 (10-bit value)
}

//* ===================================================================
//+ read_SPI() - Hardware SPI version (much faster)
//* ===================================================================

uint16_t LUMINOUS::read_SPI(uint8_t Channel)
{
	uint16_t ReadValue = 0;

	// Select the correct sensor
	if (Channel & 0b00001000)
		*PORTX_SS0 &= ~X_SS0; // SS0 LOW
	else
		*PORTX_SS1 &= ~X_SS1; // SS1 LOW

	// Prepare command: top 5 bits = 00011xxx  (shifted left by 4 because of SPI 8-bit frames)
	Channel |= 0b00001000; // Force read mode
	Channel <<= 4;         // Move to upper nibble

	//---------------------------------------------------------------
	/// Hardware SPI sends/receives 8 bits at a time
	//---------------------------------------------------------------

	SPDR = 0x01; // Start byte (some sensors need it)
	while (!(SPSR & (1 << SPIF)))
		; // Wait until transmission complete

	SPDR = Channel; // Send the actual command
	while (!(SPSR & (1 << SPIF)))
		;
	uint8_t receivedVal_B = SPDR & 0b00000011; // Only lower 2 bits are valid here

	SPDR = 0xFF; // Dummy byte to clock out the data
	while (!(SPSR & (1 << SPIF)))
		;
	uint8_t receivedVal_A = SPDR; // Full 8 bits of the lower part

	// Deselect sensors again
	*PORTX_SS0 |= X_SS0;
	*PORTX_SS1 |= X_SS1;

	// Combine the two parts:  [xx9876543210]  →  B has bits 10-9, A has bits 8-1 + junk
	ReadValue = (receivedVal_B << 8) | receivedVal_A;

	return ReadValue;
}

//* ===================================================================
//+ Public read() - chooses the correct method automatically
//* ===================================================================

uint16_t LUMINOUS::read(uint8_t Channel)
{
	if (isMode_SPI)
		return read_SPI(Channel); // Use fast hardware SPI
	else
		return read_MANUAL(Channel); // Fall back to software bit-banging
}

#else

//" ===================================================================
//" NON-AVR platforms (ESP32, ESP8266, STM32, etc.)
//" No direct port manipulation → use simple digitalWrite/digitalRead
//" ===================================================================

uint16_t LUMINOUS::read(uint8_t Channel)
{
	bool     SlaveSelect = (Channel & 0b00001000); // true → sensor 0, false → sensor 1
	uint16_t ReadValue   = 0;

	Channel |= 0b00011000; // Force read command bits

	// Select sensor
	digitalWrite(_ss0, SlaveSelect); // SS0 active LOW
	digitalWrite(_ss1, !SlaveSelect);
	digitalWrite(_sclk, LOW);

	// Send 5 command bits
	for (uint8_t ii = 0; ii < 5; ii++)
	{
		digitalWrite(_mosi, (Channel & 0b00010000)); // Send current bit
		Channel <<= 1;

		digitalWrite(_sclk, HIGH); // Clock pulse
		digitalWrite(_sclk, LOW);
	}

	// Receive 11 data bits
	for (uint8_t ii = 0; ii < 11; ii++)
	{
		digitalWrite(_sclk, HIGH); // Clock pulse
		digitalWrite(_sclk, LOW);

		ReadValue <<= 1;
		if (digitalRead(_miso))
			ReadValue++; // Set bit if MISO is HIGH
	}

	// Deselect both sensors and leave clock HIGH
	digitalWrite(_ss0, HIGH);
	digitalWrite(_ss1, HIGH);
	digitalWrite(_sclk, HIGH);

	return ReadValue;
}

#endif

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~