/*
 * File:   main.c
 * Author: boos
 *
 * Created on November 1, 2025, 3:00 PM
 */

// CONFIG1
#pragma config FOSC = INTOSC    // Oscillator Selection Bits (INTOSC oscillator: I/O function on CLKIN pin)
#pragma config WDTE = OFF       // Watchdog Timer Enable (WDT disabled)
#pragma config PWRTE = OFF      // Power-up Timer Enable (PWRT disabled)
#pragma config MCLRE = ON       // MCLR Pin Function Select (MCLR/VPP pin function is MCLR)
#pragma config CP = OFF         // Flash Program Memory Code Protection (Program memory code protection is disabled)
#pragma config BOREN = OFF      // Brown-out Reset Enable (Brown-out Reset disabled)
#pragma config CLKOUTEN = OFF   // Clock Out Enable (CLKOUT function is disabled. I/O or oscillator function on the CLKOUT pin)
#pragma config IESO = OFF       // Internal/External Switchover Mode (Internal/External Switchover Mode is disabled)
#pragma config FCMEN = OFF      // Fail-Safe Clock Monitor Enable (Fail-Safe Clock Monitor is disabled)

// CONFIG2
#pragma config WRT = OFF        // Flash Memory Self-Write Protection (Write protection off)
#pragma config CPUDIV = NOCLKDIV// CPU System Clock Selection Bit (NO CPU system divide)
#pragma config USBLSCLK = 48MHz // USB Low Speed Clock Selection bit (System clock expects 48 MHz, FS/LS USB CLKENs divide-by is set to 8.)
#pragma config PLLMULT = 3x     // PLL Multipler Selection Bit (3x Output Frequency Selected)
#pragma config PLLEN = ENABLED  // PLL Enable Bit (3x or 4x PLL Enabled)
#pragma config STVREN = OFF     // Stack Overflow/Underflow Reset Enable (Stack Overflow or Underflow will not cause a Reset)
#pragma config BORV = LO        // Brown-out Reset Voltage Selection (Brown-out Reset Voltage (Vbor), low trip point selected.)
#pragma config LPBOR = OFF      // Low-Power Brown Out Reset (Low-Power BOR is disabled)
#pragma config LVP = ON         // Low-Voltage Programming Enable (Low-voltage programming enabled)

#include <xc.h>

// allows the use of __delay_ms()
#define _XTAL_FREQ 48000000


// rotary encoder

// connections
#define ENC_SW      (!RA1)
#define ENC_A       (!RA5)
#define ENC_B       (!RA4)
#define ENC_SW_TRIS TRISA1
#define ENC_A_TRIS  TRISA5
#define ENC_B_TRIS  TRISA4

// global variables used for the rotary encoder (we add "volatile" wherever they are used and updated in the ISR)
volatile int rotary_pos, rotary_pos_before, rotary_diff;
volatile int rotary_value = 0;
int rotary_value_old = 0, rotary_value_change = 0;

// global debounce variable for the rotary encoder's pushbutton
// (is altered in the ISR, so we make it "volatile")
volatile int sw_buffer;

// function that converts Gray code into a binary number (for rotary encoder)
int gray_to_binary (void);


// DS1302 real-time clock IC

// connections
#define DS1302_CLK      RC3
#define DS1302_IO       RC4
#define DS1302_CE       RC5
#define DS1302_CLK_TRIS TRISC3
#define DS1302_IO_TRIS  TRISC4
#define DS1302_CE_TRIS  TRISC5

// functions for the DS1302 real-time clock
void DS1302_send (int value);
unsigned char DS1302_get (void);
void DS1302_set_bcd (int address, int data);
int DS1302_get_bcd (int address);
void DS1302_set_seconds (int data);
void DS1302_set_minutes (int data);
void DS1302_set_hours (int data);
int DS1302_get_seconds (void);
int DS1302_get_minutes (void);
int DS1302_get_hours (void);
void DS1302_start (void);
void DS1302_stop (void);
void DS1302_set24 (void);


// WS2812 LED

// connection
#define WS2812_DATA         RC2
#define WS2812_DATA_TRIS    TRISC2

// functions to control the WS2812 NeoPixel LEDs
#define WS2812_send_bit(b) WS2812_DATA=1; NOP(); NOP(); NOP(); WS2812_DATA=b; NOP(); NOP(); NOP(); NOP(); WS2812_DATA=0; NOP(); NOP(); NOP(); NOP();
void WS2812_send_byte (unsigned char b);
void WS2812_send_RGB (unsigned char r, unsigned char g, unsigned char b);


// MAX7219

// connections
#define MAX7219_CLK         RC3
#define MAX7219_DATA        RC0
#define MAX7219_LOAD        RC1
#define MAX7219_CLK_TRIS    TRISC3
#define MAX7219_DATA_TRIS   TRISC0
#define MAX7219_LOAD_TRIS   TRISC1

// define MAX7219 register addresses (for convenience)
#define MAX7219_MODE_NOP       0b00000000
#define MAX7219_MODE_DECODE    0b00001001
#define MAX7219_MODE_INTENSITY 0b00001010
#define MAX7219_MODE_SCANLIMIT 0b00001011
#define MAX7219_MODE_SHUTDOWN  0b00001100
#define MAX7219_MODE_TEST      0b00001111

// define MAX7219 commands (for convenience)
#define MAX7219_NO_DECODE      0b00000000
#define MAX7219_7SEG_DECODE    0b11111111

// define functions
void MAX7219_send (unsigned char a, unsigned char d);
void MAX7219_update (void);

// 7-segment code for the numbers 0-9
//                            .abcdefg
unsigned char get7seg[] = { 0b01111110,
                            0b00110000,
                            0b01101101,
                            0b01111001,
                            0b00110011,
                            0b01011011,
                            0b01011111,
                            0b01110000,
                            0b01111111,
                            0b01111011 };


// Christmas star-specific

// helpful function that allows us to sweep over a lot of colors with a single variable h (taking values from 0..255)
// (inspired by the curves presented on https://github.com/FastLED/FastLED/wiki/FastLED-HSV-Colors)
// Results are stored in the varibales rr, gg, and bb after calling calc_hsv(hue);
void calc_hsv (int h);
int rr, gg, bb;

// variables to control the menu
volatile int menu_timeout = 0;

// time information is global for simplicity
int hours, minutes, seconds;
int hours_on, minutes_on, hours_off, minutes_off;


// *****
// main function
void main (void) {
    
    
    // *****
    // set up internal oscillator to run at 48 MHz
    
    // set the SCS settings (system clock select)
    // to use the frequency specified in configuration word (line 22)
    SCS0 = 0;
    SCS1 = 0;
    
    // set to 16 MHz configuration
    IRCF0 = 1;
    IRCF1 = 1;
    IRCF2 = 1;
    IRCF3 = 1;
    
    // enable PLL to result in 48MHz
    // (PLL is set to 3x in line 23)
    SPLLEN = 1;
    
    
    // *****
    // set up peripherals
    
    // WS2812_DATA is an output
    WS2812_DATA_TRIS = 0;
    
    // DS1302_CLK is an output
    DS1302_CLK_TRIS = 0;
    
    // DS1302_IO is usually an input
    // (unless we are reading in data)
    DS1302_IO_TRIS = 1;
    
    // DS1302_CE is an output
    DS1302_CE_TRIS = 0;
    
    // MAX7219 CLK is shared with DS1302
    MAX7219_CLK_TRIS = 0;
    
    // MAX7219 DATA is an output
    MAX7219_DATA_TRIS = 0;
    
    // MAX7219 LOAD is an output
    MAX7219_LOAD_TRIS = 0;
    
    // turn off all analog to digital converters
    ANSA4 = 0;
    ANSC0 = 0;
    ANSC1 = 0;
    ANSC2 = 0;
    ANSC3 = 0;
    
    // the rotary encoder pins are all inputs
    // (RA1 can only be an input, so we don't have to set its tristate register)
    ENC_A_TRIS = 1;
    ENC_B_TRIS = 1;
    
    // enable the weak internal pullup resistors on the rotary encoder inputs RA4, RA5 (RA1 needs an external pullup resistor)
    WPUA4 = 1;
    WPUA5 = 1;
    nWPUEN = 0;

    
    // *****
    // configure TIMER0 (we use it for reading out the rotary encoder)
	// (see Sec. 19 in the PIC16F1455 datasheet for specifications)
    TMR0CS = 0;                 // internal oscillator (Fosc/4)
    PSA = 0;                    // prescaler ON
    PS2 = 0; PS1 = 1; PS0 = 1;  // set to 1:16
    TMR0IE = 1;                 // enable interrupt on overflow
    GIE = 1;                    // enable global interrupts
    

    // *****
    // variables
    
    
    // 16 LED brightness levels
    // (We use this as a lookup table, feel free to change your brightness levels between 0 and 1.)
    float brightness_table[] = {0, 0.01, 0.02, 0.03, 0.04, 0.05, 0.06, 0.08, 0.1, 0.19, 0.25, 0.34, 0.42, 0.52, 0.7, 1};
    unsigned char brightness_max = 15;
    
    // variable for the menu
    unsigned char menu_index = 0;
    
    // variables for the color information
    unsigned char hue1, hue2, hue3, hue4, mode_index;
    unsigned char brightness_factor = 0, speed_factor = 0;
    unsigned char R1red = 0, R1green = 0, R1blue = 0;
    unsigned char R2red = 0, R2green = 0, R2blue = 0;
    unsigned char R3red = 0, R3green = 0, R3blue = 0;
    unsigned char R4red = 0, R4green = 0, R4blue = 0;
    unsigned char dimmedR1red = 0, dimmedR1green = 0, dimmedR1blue = 0;
    unsigned char dimmedR2red = 0, dimmedR2green = 0, dimmedR2blue = 0;
    unsigned char dimmedR3red = 0, dimmedR3green = 0, dimmedR3blue = 0;
    unsigned char dimmedR4red = 0, dimmedR4green = 0, dimmedR4blue = 0;

    // variables for the animation
    unsigned char animation_on = 1;
    int R1timebase = 0, R1on, R1off, R1brightness_index;
    int R2timebase = 0, R2on, R2off, R2brightness_index;
    int R3timebase = 0, R3on, R3off, R3brightness_index;
    int R4timebase = 0, R4on, R4off, R4brightness_index;
    
    
    // *****
    // set up DS1302 real-time clock IC
    
    // clear the WRITE PROTECTION bit
    DS1302_CE = 1;
    DS1302_send(0x8e);
    DS1302_send(0);
    DS1302_CE = 0;
    
    // read stored brightness preference from DS1302 custom memory
    DS1302_CE = 1;
    DS1302_send(0xc1);
    mode_index = DS1302_get();
    DS1302_CE = 0;
    rotary_value = 4*mode_index;
    menu_timeout = 0;
    
    // read stored hue preferences from DS1302 custom memory
    DS1302_CE = 1;
    DS1302_send(0xc3);
    hue1 = DS1302_get();
    DS1302_CE = 0;
    
    DS1302_CE = 1;
    DS1302_send(0xc5);
    hue2 = DS1302_get();
    DS1302_CE = 0;
    
    DS1302_CE = 1;
    DS1302_send(0xc7);
    hue3 = DS1302_get();
    DS1302_CE = 0;
    
    DS1302_CE = 1;
    DS1302_send(0xc9);
    hue4 = DS1302_get();
    DS1302_CE = 0;
    
    DS1302_CE = 1;
    DS1302_send(0xcb);
    hours_on = DS1302_get();
    DS1302_CE = 0;
    
    DS1302_CE = 1;
    DS1302_send(0xcd);
    minutes_on = DS1302_get();
    DS1302_CE = 0;
    
    DS1302_CE = 1;
    DS1302_send(0xcf);
    hours_off = DS1302_get();
    DS1302_CE = 0;
    
    DS1302_CE = 1;
    DS1302_send(0xd1);
    minutes_off = DS1302_get();
    DS1302_CE = 0;
    
    // always set DS1302 to 24-hour mode
    // (the conversion to 12-hour mode, if required, is done by us in the code.)
    DS1302_set24();
    
    
    // *****
    // set up MAX7219 in 7-segment module
	
    // scan all eight rows
    MAX7219_send(MAX7219_MODE_SCANLIMIT, 7);
    MAX7219_update();
    
    // set MAX7219 to no-decoding mode
	// (we are specifying the pattern manually)
    MAX7219_send(MAX7219_MODE_DECODE, MAX7219_NO_DECODE);
    MAX7219_update();
    
    // set MAX7219 brightness to minimum
    // (any number from 0-15 works)
    MAX7219_send(MAX7219_MODE_INTENSITY, 1);
    MAX7219_update();
    
    // turn ON
    MAX7219_send(MAX7219_MODE_SHUTDOWN, 1);
    MAX7219_update();
    
    
    // *****
    // main loop
    while (1) {
        
        
        // *****
        // extract 'style' parameters for the animnation mode
        brightness_factor = (unsigned char) ((mode_index & 1) << 1);
        speed_factor = (mode_index >> 1);

        
        // *****
        // animation
        
        // set the animation speed for each LED ring
        // (ring #1 = innermost LED, ring #4 = outermost LEDs)
        // (time conversion: 1 second = appr. 220 ticks)
        
        // slow settings
        if (speed_factor == 0) {
            
            R1on =  693; R1off = 489;
            R2on = 1201; R2off = 433;
            R3on =  989; R3off = 327;
            R4on = 1695; R4off = 234;
            
        // fast settings
        } else {
            
            R1on = 330; R1off = 120;
            R2on = 330; R2off = 120;
            R3on = 248; R3off = 110;
            R4on = 128; R4off = 165;
            
        }

        // run the animation
        if (animation_on == 1) {
        
            R1timebase++;
            R2timebase++;
            R3timebase++;
            R4timebase++;
            
            if (speed_factor == 0) {
                
                if (R1timebase < R1on) {
                    if ((R1brightness_index < brightness_max) && (R1timebase % 2 == 0)) {
                        R1brightness_index++;
                    }
                } else if (R1timebase > R1on + R1off)  {
                    R1timebase = 0;
                } else if (R1timebase > R1on)  {
                    if ((R1brightness_index > 0) && (R1timebase % 2 == 0)) {
                        R1brightness_index--;
                    }
                }
                if (R2timebase < R2on) {
                    if ((R2brightness_index < brightness_max) && (R2timebase % 2 == 0)) {
                        R2brightness_index++;
                    }
                } else if (R2timebase > R2on + R2off)  {
                    R2timebase = 0;
                } else if (R2timebase > R2on)  {
                    if ((R2brightness_index > 0) && (R2timebase % 2 == 0)) {
                        R2brightness_index--;
                    }
                }
                if (R3timebase < R3on) {
                    if ((R3brightness_index < brightness_max) && (R3timebase % 2 == 0)) {
                        R3brightness_index++;
                    }
                } else if (R3timebase > R3on + R3off)  {
                    R3timebase = 0;
                } else if (R3timebase > R3on)  {
                    if ((R3brightness_index > 0) && (R3timebase % 2 == 0)) {
                        R3brightness_index--;
                    }
                }
                if (R4timebase < R4on) {
                    if ((R4brightness_index < brightness_max) && (R4timebase % 2 == 0)) {
                        R4brightness_index++;
                    }
                } else if (R4timebase > R4on + R4off)  {
                    R4timebase = 0;
                } else if (R4timebase > R4on)  {
                    if ((R4brightness_index > 0) && (R4timebase % 2 == 0)) {
                        R4brightness_index--;
                    }
                }
                
            } else {

                if (R1timebase > R1on + R1off) {
                    R1brightness_index = 0;
                    R1timebase = 0;
                } else if (R1timebase > R1on) {
                    R1brightness_index = brightness_max;
                }
                if (R2timebase > R2on + R2off) {
                    R2brightness_index = 0;
                    R2timebase = 0;
                } else if (R2timebase > R2on) {
                    R2brightness_index = brightness_max;
                }
                if (R3timebase > R3on + R3off) {
                    R3brightness_index = 0;
                    R3timebase = 0;
                } else if (R3timebase > R3on) {
                    R3brightness_index = brightness_max;
                }
                if (R4timebase > R4on + R4off) {
                    R4brightness_index = 0;
                    R4timebase = 0;
                } else if (R4timebase > R4on) {
                    R4brightness_index = brightness_max;
                }
                
            }
        
        // no animation when we are in one of the menus
        } else {
         
            R1brightness_index = brightness_max;
            R2brightness_index = brightness_max;
            R3brightness_index = brightness_max;
            R4brightness_index = brightness_max;
            
        }
        
        
        // *****
        // read out data from DS1302
        
        // retrieve time
        minutes = DS1302_get_minutes();
        hours = DS1302_get_hours();
        
        
        // *****
        // Did the rotary encoder turn?
        // (We check for this so that we only update the information stored
        // in the DS1302 whenever something actually has been changed. This
        // reduces the amount of unnecessary write operations to the DS1302.)
        if (rotary_value != rotary_value_old) {
            rotary_value_change = 1;
            rotary_value_old = rotary_value;
        } else {
            rotary_value_change = 0;
        }
        
        
        // *****
        // menu functionality
        
        // standard operation, rotary encoder changes operation mode
        if (menu_index == 0) {
            
            // rotary encoder value is mapped to 0..3
            mode_index = (unsigned char) (rotary_value / 4) % 4;
            if (mode_index < 0) {
                mode_index = 3;
                rotary_value = 4*mode_index;
            }
            
            // update value if rotary encoder has been turned
            if (rotary_value_change) {
                DS1302_CE = 1;
                DS1302_send(0xc0);
                DS1302_send(mode_index);
                DS1302_CE = 0;
                menu_timeout = 15000;
            }

            // show the time in normal operation
            if (menu_timeout == 0) {
            
                //                .abcdefg
                MAX7219_send(8, 0); MAX7219_update();
                MAX7219_send(7, 0); MAX7219_update();
                MAX7219_send(6, get7seg[hours / 10]); MAX7219_update();
                MAX7219_send(5, 0b10000000 | get7seg[hours % 10]); MAX7219_update();
                MAX7219_send(4, get7seg[minutes / 10]); MAX7219_update();
                MAX7219_send(3, get7seg[minutes % 10]); MAX7219_update();
                MAX7219_send(2, 0); MAX7219_update();
                MAX7219_send(1, 0); MAX7219_update();
            
            // otherwise, show the style menu
            } else {
                
                //                .abcdefg
                MAX7219_send(8, 0b01011011); MAX7219_update();
                MAX7219_send(7, 0b00001111); MAX7219_update();
                MAX7219_send(6, 0b00111011); MAX7219_update();
                MAX7219_send(5, 0b00001110); MAX7219_update();
                MAX7219_send(4, 0b01001111); MAX7219_update();
                MAX7219_send(3, 0); MAX7219_update();
                if (mode_index == 0) {
                    MAX7219_send(2, 0b01011011); MAX7219_update();
                    MAX7219_send(1, 0b00110111); MAX7219_update();
                } else if (mode_index == 1) {
                    MAX7219_send(2, 0b01011011); MAX7219_update();
                    MAX7219_send(1, 0b00001110); MAX7219_update();
                } else if (mode_index == 2) {
                    MAX7219_send(2, 0b01000111); MAX7219_update();
                    MAX7219_send(1, 0b00110111); MAX7219_update();
                } else {
                    MAX7219_send(2, 0b01000111); MAX7219_update();
                    MAX7219_send(1, 0b00001110); MAX7219_update();
                }
                    
                
            }
        
        // rotary encoder sets color #1
        } else if (menu_index == 1) {
            
            // rotary encoder value is mapped to 0..255
            // (if rotary encoder value becomes negative, reset values)
            hue1 = (unsigned char) (rotary_value / 2) % 256;
            
            // update value if rotary encoder has been turned
            if (rotary_value_change) {
                DS1302_CE = 1;
                DS1302_send(0xc2);
                DS1302_send(hue1);
                DS1302_CE = 0;
            }
            
            //                .abcdefg
            MAX7219_send(8, 0b00001101); MAX7219_update();
            MAX7219_send(7, 0b00011101); MAX7219_update();
            MAX7219_send(6, 0b00000110); MAX7219_update();
            MAX7219_send(5, 0b00011101); MAX7219_update();
            MAX7219_send(4, 0b00000101); MAX7219_update();
            MAX7219_send(3, get7seg[1]); MAX7219_update();
            MAX7219_send(2, 0); MAX7219_update();
            MAX7219_send(1, 0); MAX7219_update();
            
        // rotary encoder sets color #2
        } else if (menu_index == 2) {
        
            // rotary encoder value is mapped to 0..255
            // (if rotary encoder value becomes negative, reset values)
            hue2 = (unsigned char) (rotary_value / 2) % 256;
            
            // update value if rotary encoder has been turned
            if (rotary_value_change) {
                DS1302_CE = 1;
                DS1302_send(0xc4);
                DS1302_send(hue2);
                DS1302_CE = 0;
            }
            
            //                .abcdefg
            MAX7219_send(8, 0b00001101); MAX7219_update();
            MAX7219_send(7, 0b00011101); MAX7219_update();
            MAX7219_send(6, 0b00000110); MAX7219_update();
            MAX7219_send(5, 0b00011101); MAX7219_update();
            MAX7219_send(4, 0b00000101); MAX7219_update();
            MAX7219_send(3, get7seg[2]); MAX7219_update();
            MAX7219_send(2, 0); MAX7219_update();
            MAX7219_send(1, 0); MAX7219_update();
            
        // rotary encoder sets color #3
        } else if (menu_index == 3) {
        
            // rotary encoder value is mapped to 0..255
            // (if rotary encoder value becomes negative, reset values)
            hue3 = (unsigned char) (rotary_value / 2) % 256;
            
            // update value if rotary encoder has been turned
            if (rotary_value_change) {
                DS1302_CE = 1;
                DS1302_send(0xc6);
                DS1302_send(hue3);
                DS1302_CE = 0;
            }
            
            //                .abcdefg
            MAX7219_send(8, 0b00001101); MAX7219_update();
            MAX7219_send(7, 0b00011101); MAX7219_update();
            MAX7219_send(6, 0b00000110); MAX7219_update();
            MAX7219_send(5, 0b00011101); MAX7219_update();
            MAX7219_send(4, 0b00000101); MAX7219_update();
            MAX7219_send(3, get7seg[3]); MAX7219_update();
            MAX7219_send(2, 0); MAX7219_update();
            MAX7219_send(1, 0); MAX7219_update();
            
        // rotary encoder sets color #4
        } else if (menu_index == 4) {
        
            // rotary encoder value is mapped to 0..255
            // (if rotary encoder value becomes negative, reset values)
            hue4 = (unsigned char) (rotary_value / 2) % 256;
            
            // update value if rotary encoder has been turned
            if (rotary_value_change) {
                DS1302_CE = 1;
                DS1302_send(0xc8);
                DS1302_send(hue4);
                DS1302_CE = 0;
            }
            
            //                .abcdefg
            MAX7219_send(8, 0b00001101); MAX7219_update();
            MAX7219_send(7, 0b00011101); MAX7219_update();
            MAX7219_send(6, 0b00000110); MAX7219_update();
            MAX7219_send(5, 0b00011101); MAX7219_update();
            MAX7219_send(4, 0b00000101); MAX7219_update();
            MAX7219_send(3, get7seg[4]); MAX7219_update();
            MAX7219_send(2, 0); MAX7219_update();
            MAX7219_send(1, 0); MAX7219_update();
        
        // rotary encoder sets ON time (hours)
        } else if (menu_index == 5) {
        
            // rotary encoder value is mapped to 0..23
            // (if rotary encoder value becomes negative, reset values)
            hours_on = (rotary_value % 96) / 4;
            if (hours_on < 0) {
                hours_on = 23;
                rotary_value = 4*hours_on;
            }
            
            // update value if rotary encoder has been turned
            if (rotary_value_change) {
                DS1302_CE = 1;
                DS1302_send(0xca);
                DS1302_send(hours_on);
                DS1302_CE = 0;
            }
            
            //                .abcdefg
            MAX7219_send(8, 0b00011101); MAX7219_update();
            MAX7219_send(7, 0b00010101); MAX7219_update();
            MAX7219_send(6, 0); MAX7219_update();
            MAX7219_send(5, 0); MAX7219_update();
            MAX7219_send(4, 0b10000000 | get7seg[hours_on / 10]); MAX7219_update();
            MAX7219_send(3, 0b10000000 | get7seg[hours_on % 10]); MAX7219_update();
            MAX7219_send(2, get7seg[minutes_on / 10]); MAX7219_update();
            MAX7219_send(1, get7seg[minutes_on % 10]); MAX7219_update();
            
        // rotary encoder sets ON time (minutes)
        } else if (menu_index == 6) {
        
            // rotary encoder value is mapped to 0..59
            // (if rotary encoder value becomes negative, reset values)
            minutes_on = (rotary_value % 240) / 4;
            if (minutes_on < 0) {
                minutes_on = 59;
                rotary_value = 4*minutes_on;
            }
            
            // update value if rotary encoder has been turned
            if (rotary_value_change) {
                DS1302_CE = 1;
                DS1302_send(0xcc);
                DS1302_send(minutes_on);
                DS1302_CE = 0;
            }
            
            //                .abcdefg
            MAX7219_send(8, 0b00011101); MAX7219_update();
            MAX7219_send(7, 0b00010101); MAX7219_update();
            MAX7219_send(6, 0); MAX7219_update();
            MAX7219_send(5, 0); MAX7219_update();
            MAX7219_send(4, get7seg[hours_on / 10]); MAX7219_update();
            MAX7219_send(3, get7seg[hours_on % 10]); MAX7219_update();
            MAX7219_send(2, 0b10000000 | get7seg[minutes_on / 10]); MAX7219_update();
            MAX7219_send(1, 0b10000000 | get7seg[minutes_on % 10]); MAX7219_update();
            
        // rotary encoder sets OFF time (hours)
        } else if (menu_index == 7) {
        
            // rotary encoder value is mapped to 0..23
            // (if rotary encoder value becomes negative, reset values)
            hours_off = (rotary_value % 96) / 4;
            if (hours_off < 0) {
                hours_off = 23;
                rotary_value = 4*hours_off;
            }
            
            // update value if rotary encoder has been turned
            if (rotary_value_change) {
                DS1302_CE = 1;
                DS1302_send(0xce);
                DS1302_send(hours_off);
                DS1302_CE = 0;
            }
            
            //                .abcdefg
            MAX7219_send(8, 0b00011101); MAX7219_update();
            MAX7219_send(7, 0b01000111); MAX7219_update();
            MAX7219_send(6, 0b01000111); MAX7219_update();
            MAX7219_send(5, 0); MAX7219_update();
            MAX7219_send(4, 0b10000000 | get7seg[hours_off / 10]); MAX7219_update();
            MAX7219_send(3, 0b10000000 | get7seg[hours_off % 10]); MAX7219_update();
            MAX7219_send(2, get7seg[minutes_off / 10]); MAX7219_update();
            MAX7219_send(1, get7seg[minutes_off % 10]); MAX7219_update();
            
        // rotary encoder sets OFF time (minutes)
        } else if (menu_index == 8) {
        
            // rotary encoder value is mapped to 0..59
            // (if rotary encoder value becomes negative, reset values)
            minutes_off = (rotary_value % 240) / 4;
            if (minutes_off < 0) {
                minutes_off = 59;
                rotary_value = 4*minutes_off;
            }
            
            // update value if rotary encoder has been turned
            if (rotary_value_change) {
                DS1302_CE = 1;
                DS1302_send(0xd0);
                DS1302_send(minutes_off);
                DS1302_CE = 0;
            }
            
            //                .abcdefg
            MAX7219_send(8, 0b00011101); MAX7219_update();
            MAX7219_send(7, 0b01000111); MAX7219_update();
            MAX7219_send(6, 0b01000111); MAX7219_update();
            MAX7219_send(5, 0); MAX7219_update();
            MAX7219_send(4, get7seg[hours_off / 10]); MAX7219_update();
            MAX7219_send(3, get7seg[hours_off % 10]); MAX7219_update();
            MAX7219_send(2, 0b10000000 | get7seg[minutes_off / 10]); MAX7219_update();
            MAX7219_send(1, 0b10000000 | get7seg[minutes_off % 10]); MAX7219_update();
        
        // rotary encoder sets time (hours)
        } else if (menu_index == 9) {
        
            // rotary encoder value is mapped to 0..23
            // (if rotary encoder value becomes negative, reset values)
            hours = (rotary_value % 96) / 4;
            if (hours < 0) {
                hours = 23;
                rotary_value = 4*hours;
            }
            
            // update value if rotary encoder has been turned
            if (rotary_value_change) {
                DS1302_set_hours(hours);
            }
            
            //                .abcdefg
            MAX7219_send(8, 0b00001111); MAX7219_update();
            MAX7219_send(7, get7seg[0]); MAX7219_update();
            MAX7219_send(6, 0); MAX7219_update();
            MAX7219_send(5, 0); MAX7219_update();
            MAX7219_send(4, 0b10000000 | get7seg[hours / 10]); MAX7219_update();
            MAX7219_send(3, 0b10000000 | get7seg[hours % 10]); MAX7219_update();
            MAX7219_send(2, get7seg[minutes / 10]); MAX7219_update();
            MAX7219_send(1, get7seg[minutes % 10]); MAX7219_update();
            
        // rotary encoder sets time (minutes)
        } else if (menu_index == 10) {
        
            // rotary encoder value is mapped to 0..59
            // (if rotary encoder value becomes negative, reset values)
            minutes = (rotary_value % 240) / 4;
            if (minutes < 0) {
                minutes = 59;
                rotary_value = 4*minutes;
            }
            
            // update value if rotary encoder has been turned
            if (rotary_value_change) {
                DS1302_set_minutes(minutes);
            }
            
            //                .abcdefg
            MAX7219_send(8, 0b00001111); MAX7219_update();
            MAX7219_send(7, get7seg[0]); MAX7219_update();
            MAX7219_send(6, 0); MAX7219_update();
            MAX7219_send(5, 0); MAX7219_update();
            MAX7219_send(4, get7seg[hours / 10]); MAX7219_update();
            MAX7219_send(3, get7seg[hours % 10]); MAX7219_update();
            MAX7219_send(2, 0b10000000 | get7seg[minutes / 10]); MAX7219_update();
            MAX7219_send(1, 0b10000000 | get7seg[minutes % 10]); MAX7219_update();
            
        }
        
        
        // *****
        // react to the rotary encoder's pushbutton
        if (ENC_SW && (sw_buffer == 0)) {
        
            // refill debounce buffer
            sw_buffer = 500;
            
            // advance through menu options
            menu_index += 1;
            if (menu_index > 10) {
                menu_index = 0;
            }
            
            // initialize normal mode
            if (menu_index == 0) {
                
                // reset seconds (convenient after setting the time)
                seconds = 0;
                DS1302_set_seconds(seconds);
                
                // restart the clock
                DS1302_start();
                
                // reset menu timeout
                menu_timeout = 15000;
                
                // resume animations
                animation_on = 1;
                
                // prime the rotary encoder value to currently selected dimming level
                // (multiply by 4 because our rotary encoder has 4 increments per tactile "step")
                rotary_value = 4*mode_index;
            
            // initialize "set color" mode
            } else if (menu_index == 1) {

                // pause animation and reset timebases
                animation_on = 0; R1timebase = 0; R2timebase = 0; R3timebase = 0; R4timebase = 0;
                
                // prime the rotary encoder to currently selected color
                rotary_value = 2*hue1;
                
            // initialize "set color" mode
            } else if (menu_index == 2) {

                // pause animation and reset timebases
                animation_on = 0; R1timebase = 0; R2timebase = 0; R3timebase = 0; R4timebase = 0;
                
                // prime the rotary encoder to currently selected color
                rotary_value = 2*hue2;
                
            // initialize "set color" mode
            } else if (menu_index == 3) {

                // pause animation and reset timebases
                animation_on = 0; R1timebase = 0; R2timebase = 0; R3timebase = 0; R4timebase = 0;
                
                // prime the rotary encoder to currently selected color
                rotary_value = 2*hue3;
                
            // initialize "set color" mode
            } else if (menu_index == 4) {

                // pause animation and reset timebases
                animation_on = 0; R1timebase = 0; R2timebase = 0; R3timebase = 0; R4timebase = 0;
                
                // prime the rotary encoder to currently selected color
                rotary_value = 2*hue4;
                
            // initialize "set ON hours" mode
            } else if (menu_index == 5) {

                // prime the rotary encoder value to current hours
                // (multiply by 4 because our rotary encoder has 4 increments per tactile "step")
                rotary_value = 4*hours_on;
                
            // initialize "set ON minutes" mode
            } else if (menu_index == 6) {

                // prime the rotary encoder value to current minutes
                // (multiply by 4 because our rotary encoder has 4 increments per tactile "step")
                rotary_value = 4*minutes_on;
                
            // initialize "set OFF hours" mode
            } else if (menu_index == 7) {

                // prime the rotary encoder value to current hours
                // (multiply by 4 because our rotary encoder has 4 increments per tactile "step")
                rotary_value = 4*hours_off;
                
            // initialize "set OFF minutes" mode
            } else if (menu_index == 8) {

                // prime the rotary encoder value to current minutes
                // (multiply by 4 because our rotary encoder has 4 increments per tactile "step")
                rotary_value = 4*minutes_off;
                
            // initialize "set hours" mode
            } else if (menu_index == 9) {

                // prime the rotary encoder value to current hours
                // (multiply by 4 because our rotary encoder has 4 increments per tactile "step")
                rotary_value = 4*hours;
                
            // initialize "set minutes" mode
            } else if (menu_index == 10) {

                // prime the rotary encoder value to current minutes
                // (multiply by 4 because our rotary encoder has 4 increments per tactile "step")
                rotary_value = 4*minutes;
                
            }

        }

        
        // *****
        // calculate LED RGB colors from their hue
        calc_hsv(hue1); R1red = (unsigned char) rr; R1green = (unsigned char) gg; R1blue = (unsigned char) bb;
        calc_hsv(hue2); R2red = (unsigned char) rr; R2green = (unsigned char) gg; R2blue = (unsigned char) bb;
        calc_hsv(hue3); R3red = (unsigned char) rr; R3green = (unsigned char) gg; R3blue = (unsigned char) bb;
        calc_hsv(hue4); R4red = (unsigned char) rr; R4green = (unsigned char) gg; R4blue = (unsigned char) bb;
        
        // Should the LEDs be ON?
        if ( (menu_index >= 1) || (menu_timeout > 0) || ( ((hours == hours_on && minutes >= minutes_on) || (hours > hours_on)) && ((hours == hours_off && minutes < minutes_off) || (hours < hours_off)) )) {
            
            dimmedR1red = (unsigned char) (R1red*brightness_table[R1brightness_index]) >> brightness_factor; dimmedR1green = (unsigned char) (R1green*brightness_table[R1brightness_index]) >> brightness_factor; dimmedR1blue = (unsigned char) (R1blue*brightness_table[R1brightness_index]) >> brightness_factor;
            dimmedR2red = (unsigned char) (R2red*brightness_table[R2brightness_index]) >> brightness_factor; dimmedR2green = (unsigned char) (R2green*brightness_table[R2brightness_index]) >> brightness_factor; dimmedR2blue = (unsigned char) (R2blue*brightness_table[R2brightness_index]) >> brightness_factor;
            dimmedR3red = (unsigned char) (R3red*brightness_table[R3brightness_index]) >> brightness_factor; dimmedR3green = (unsigned char) (R3green*brightness_table[R3brightness_index]) >> brightness_factor; dimmedR3blue = (unsigned char) (R3blue*brightness_table[R3brightness_index]) >> brightness_factor;
            dimmedR4red = (unsigned char) (R4red*brightness_table[R4brightness_index]) >> brightness_factor; dimmedR4green = (unsigned char) (R4green*brightness_table[R4brightness_index]) >> brightness_factor; dimmedR4blue = (unsigned char) (R4blue*brightness_table[R4brightness_index]) >> brightness_factor;
        
        // turn all LEDs off
        } else {
            
            dimmedR1red = 0; dimmedR1green = 0; dimmedR1blue = 0;
            dimmedR2red = 0; dimmedR2green = 0; dimmedR2blue = 0;
            dimmedR3red = 0; dimmedR3green = 0; dimmedR3blue = 0;
            dimmedR4red = 0; dimmedR4green = 0; dimmedR4blue = 0;
            
        }
        
        // *****
        // send time data to WS2812 LEDs
        // (we do this without loops because it would delay execution time, and this part is time critical)
        
        // we need to turn interrupts OFF during data transmission because this part is time critical
        GIE = 0;
        
        // innermost LED
        WS2812_send_RGB(dimmedR1red, dimmedR1green, dimmedR1blue);
        
        // rings #1 (16 LEDs)
        WS2812_send_RGB(dimmedR2red, dimmedR2green, dimmedR2blue); WS2812_send_RGB(dimmedR2red, dimmedR2green, dimmedR2blue); WS2812_send_RGB(dimmedR2red, dimmedR2green, dimmedR2blue); WS2812_send_RGB(dimmedR2red, dimmedR2green, dimmedR2blue);
        WS2812_send_RGB(dimmedR2red, dimmedR2green, dimmedR2blue); WS2812_send_RGB(dimmedR2red, dimmedR2green, dimmedR2blue); WS2812_send_RGB(dimmedR2red, dimmedR2green, dimmedR2blue); WS2812_send_RGB(dimmedR2red, dimmedR2green, dimmedR2blue);
        WS2812_send_RGB(dimmedR2red, dimmedR2green, dimmedR2blue); WS2812_send_RGB(dimmedR2red, dimmedR2green, dimmedR2blue); WS2812_send_RGB(dimmedR2red, dimmedR2green, dimmedR2blue); WS2812_send_RGB(dimmedR2red, dimmedR2green, dimmedR2blue);
        WS2812_send_RGB(dimmedR2red, dimmedR2green, dimmedR2blue); WS2812_send_RGB(dimmedR2red, dimmedR2green, dimmedR2blue); WS2812_send_RGB(dimmedR2red, dimmedR2green, dimmedR2blue); WS2812_send_RGB(dimmedR2red, dimmedR2green, dimmedR2blue);
        
        // ring #2 (16 LEDs)
        WS2812_send_RGB(dimmedR3red, dimmedR3green, dimmedR3blue); WS2812_send_RGB(dimmedR3red, dimmedR3green, dimmedR3blue); WS2812_send_RGB(dimmedR3red, dimmedR3green, dimmedR3blue); WS2812_send_RGB(dimmedR3red, dimmedR3green, dimmedR3blue);
        WS2812_send_RGB(dimmedR3red, dimmedR3green, dimmedR3blue); WS2812_send_RGB(dimmedR3red, dimmedR3green, dimmedR3blue); WS2812_send_RGB(dimmedR3red, dimmedR3green, dimmedR3blue); WS2812_send_RGB(dimmedR3red, dimmedR3green, dimmedR3blue);
        WS2812_send_RGB(dimmedR3red, dimmedR3green, dimmedR3blue); WS2812_send_RGB(dimmedR3red, dimmedR3green, dimmedR3blue); WS2812_send_RGB(dimmedR3red, dimmedR3green, dimmedR3blue); WS2812_send_RGB(dimmedR3red, dimmedR3green, dimmedR3blue);
        WS2812_send_RGB(dimmedR3red, dimmedR3green, dimmedR3blue); WS2812_send_RGB(dimmedR3red, dimmedR3green, dimmedR3blue); WS2812_send_RGB(dimmedR3red, dimmedR3green, dimmedR3blue); WS2812_send_RGB(dimmedR3red, dimmedR3green, dimmedR3blue);
        
        // ring #3 (16 LEDs)
        WS2812_send_RGB(dimmedR4red, dimmedR4green, dimmedR4blue); WS2812_send_RGB(dimmedR4red, dimmedR4green, dimmedR4blue); WS2812_send_RGB(dimmedR4red, dimmedR4green, dimmedR4blue); WS2812_send_RGB(dimmedR4red, dimmedR4green, dimmedR4blue);
        WS2812_send_RGB(dimmedR4red, dimmedR4green, dimmedR4blue); WS2812_send_RGB(dimmedR4red, dimmedR4green, dimmedR4blue); WS2812_send_RGB(dimmedR4red, dimmedR4green, dimmedR4blue); WS2812_send_RGB(dimmedR4red, dimmedR4green, dimmedR4blue);
        WS2812_send_RGB(dimmedR4red, dimmedR4green, dimmedR4blue); WS2812_send_RGB(dimmedR4red, dimmedR4green, dimmedR4blue); WS2812_send_RGB(dimmedR4red, dimmedR4green, dimmedR4blue); WS2812_send_RGB(dimmedR4red, dimmedR4green, dimmedR4blue);
        WS2812_send_RGB(dimmedR4red, dimmedR4green, dimmedR4blue); WS2812_send_RGB(dimmedR4red, dimmedR4green, dimmedR4blue); WS2812_send_RGB(dimmedR4red, dimmedR4green, dimmedR4blue); WS2812_send_RGB(dimmedR4red, dimmedR4green, dimmedR4blue);
        
        // now it is safe to turn interrupts back on
        GIE = 1;
        
        
    }
    
    return;
    
}

// interrupt service routine (is called approximately 2930 times per second)
// (48MHz/4 clock speed, 8-bit counter, with 1:16 prescaler = 2930 Hz)
void __interrupt () isr (void) {  
 
    // timer overflow?
    if (TMR0IF) {
        
        // read out the current rotary encoder position
        rotary_pos = gray_to_binary();
        
        // calculate the difference to previous position
        rotary_diff = rotary_pos_before - rotary_pos;

        // turned clockwise
        if (((rotary_diff == -1) || (rotary_diff == 3))) {
            rotary_pos_before = rotary_pos;
            rotary_value++;
            
        // turned counter-clockwise
        } else if (((rotary_diff == 1) || (rotary_diff == -3))) {
            rotary_pos_before = rotary_pos;
            rotary_value--;
        
        // in this case, a step has been missed
        // (best practice: ignore it!)
        } else if ((rotary_diff == 2) || (rotary_diff == -2)) {
            
        }
        
        // clear the debounce buffer for the rotary encoder pushbutton,
        // but only if the pushbutton is not pressed
        if ((sw_buffer > 0) && !ENC_SW) {
            sw_buffer--;
        }
        
        // clear the menu timeout buffer
        if (menu_timeout > 0) {
            menu_timeout--;
        }
        
        // clear timer flag
        TMR0IF = 0;
        
    }
    
}

// convert Gray code into a binary number
// 00 -> 0, 01 -> 1, 11 -> 2, 10 -> 3
int gray_to_binary () {
 
    if (ENC_A == 0 && ENC_B == 0) {
        return 0;
    } else if (ENC_A == 0 && ENC_B == 1) {
        return 1;
    } else if (ENC_A == 1 && ENC_B == 1) {
        return 2;
    } else {
        return 3;
    }
    
}

// send the byte "value" to the DS1302
void DS1302_send (int value) {
    
    // set data port as output
    DS1302_IO_TRIS = 0;
    
    // auxiliary index variable
	int n = 0;
    
    // loop over all eight bits
	for (n = 0; n < 8; n++) {
        DS1302_IO = (value >> n) & 1;
        NOP(); // important!
        DS1302_CLK = 1;
        DS1302_CLK = 0;
    }

}

// receive a byte from the DS1302
unsigned char DS1302_get (void) {
    
    // set data port as input
    DS1302_IO_TRIS = 1;
    
    // dummy variables
    unsigned char value = 0;
    char n = 0;
    
    // collect all eight bits
    for (n = 0; n < 8; n++) {
        NOP(); // important!
        value |= (DS1302_IO << n);
        DS1302_CLK = 1;
        DS1302_CLK = 0;
    }
    return value;

}

// convert decimal value "data" into BCD
// and store it in DS1302 at "address"
void DS1302_set_bcd (int address, int data) {
    
    int data10 = data / 10;
    int data1 = data % 10;
    DS1302_CE = 1;
    DS1302_send(address);
    DS1302_send((data10 << 4) | data1);
    DS1302_CE = 0;
    
}

// read a BCD value from DS1302 from "address"
// and convert it back into decimal
int DS1302_get_bcd (int address) {
    
    int tmp = 0, data10 = 0, data1 = 0;
    DS1302_CE = 1;
    DS1302_send(address);
    tmp = DS1302_get();
    DS1302_CE = 0;
    data10 = (tmp & 0b01110000) >> 4;
    data1  = tmp & 0b00001111;
    return 10*data10 + data1;
    
}

// short-hand functions that set the time directly
void DS1302_set_hours   (int data) { DS1302_set_bcd(0x84, data); }
void DS1302_set_minutes (int data) { DS1302_set_bcd(0x82, data); }
void DS1302_set_seconds (int data) { DS1302_set_bcd(0x80, data); }

// short-hand functions that read the time
int DS1302_get_hours   (void) { return DS1302_get_bcd(0x85); }
int DS1302_get_minutes (void) { return DS1302_get_bcd(0x83); }
int DS1302_get_seconds (void) { return DS1302_get_bcd(0x81); }

// initialize the DS1302
void DS1302_start (void) {
    
    // get the current seconds
    // seconds = DS1302_get_seconds();
    
    // make sure DS1302 is running by clearing the CLOCK HALT signal
    // at bit 7 in the "seconds" register
    // (we need to extract the seconds first so they don't get deleted)
    DS1302_CE = 1;
    DS1302_send(0x80);
    // DS1302_send(0<<7 | ((seconds / 10) << 4) | (seconds % 10));
    DS1302_send(0);
    DS1302_CE = 0;
    
}

// stop the DS1302
void DS1302_stop (void) {
    
    // get the current seconds
    seconds = DS1302_get_seconds();
    
    // make sure DS1302 is stopped by setting the CLOCK HALT signal
    // at bit 7 in the "seconds" register
    // (we need to extract the seconds first so they don't get deleted)
    DS1302_CE = 1;
    DS1302_send(0x80);
    DS1302_send(1<<7 | ((seconds / 10) << 4) | (seconds % 10));
    DS1302_CE = 0;
    
}

// set the DS1302 to 24-hour mode
void DS1302_set24 (void) {
    
    // get the current hours
    hours = DS1302_get_hours();
    
    // make sure DS1302 is running in 24-hour mode by clearing bit 7 
    // in the "hours" register
    // (we need to extract the hours first so they don't get deleted)
    DS1302_CE = 1;
    DS1302_send(0x84);
    DS1302_send(((hours / 10) << 4) | (hours % 10) );
    DS1302_CE = 0;
    
}

// send out a byte b in WS2812 protocol
void WS2812_send_byte (unsigned char b) {

    if (b & 0b10000000) { WS2812_send_bit(1); } else { WS2812_send_bit(0); }
    if (b & 0b01000000) { WS2812_send_bit(1); } else { WS2812_send_bit(0); }
    if (b & 0b00100000) { WS2812_send_bit(1); } else { WS2812_send_bit(0); }
    if (b & 0b00010000) { WS2812_send_bit(1); } else { WS2812_send_bit(0); }
    if (b & 0b00001000) { WS2812_send_bit(1); } else { WS2812_send_bit(0); }
    if (b & 0b00000100) { WS2812_send_bit(1); } else { WS2812_send_bit(0); }
    if (b & 0b00000010) { WS2812_send_bit(1); } else { WS2812_send_bit(0); }
    if (b & 0b00000001) { WS2812_send_bit(1); } else { WS2812_send_bit(0); }
    
}

// send red, green, and blue values in WS2812 protocol
void WS2812_send_RGB (unsigned char r, unsigned char g, unsigned char b) {

    WS2812_send_byte(g);
    WS2812_send_byte(r);
    WS2812_send_byte(b);
    
}

// This function sends out the address byte "a" and the data byte "d" in the MAX7219 format
// (Sequence of bits is a7-a6-a5-a4-a3-a2-a1-a0-d7-d6-d5-d4-d3-d2-d1-d0.)
void MAX7219_send (unsigned char a, unsigned char d) {

	// send out address byte, start with most significant bit and work backwards
    for (int i=7; i>=0; i--) {
        MAX7219_DATA = (a >> i) & 1;
        MAX7219_CLK = 1;
        MAX7219_CLK = 0;
    }
	
	// send out data byte, start with most significant bit and work backwards
    for (int i=7; i>=0; i--) {
       MAX7219_DATA = (d >> i) & 1;
       MAX7219_CLK = 1;
       MAX7219_CLK = 0;
    }
	
	// reset the data pin back to zero
	// (so that it is not left ON if the last sent bit was a 1)
    MAX7219_DATA = 0;

}

// This function pulls the LOAD pin high and then back to zero, so that the transmitted data
// appears in the MAX7219's output stage.
void MAX7219_update (void) {
	
    MAX7219_LOAD = 1;
    MAX7219_LOAD = 0;
	
}

// This function takes a hue value (variable h) and computes the corresponding red, green, and blue values.
// This is inspired by the curves presented on https://github.com/FastLED/FastLED/wiki/FastLED-HSV-Colors .
void calc_hsv (int h) {
 
    // make sure the hue is between 0..255
    h = h & 0xff;
    
    // expensive floating point computations
    int v2 = (int) (5.31*h);
    int v1 = v2 / 2;
    
    // red channel
    if ((h >= 0) && (h <= 32)) {
        rr = (int) (255 - v1);
    } else if (h <= 64) {
        rr = 170;
    } else if (h <= 96) {
        rr = (int) (510 - v2);
    } else if (h <= 160) {
        rr = 0;
    } else {
        rr = (int) (v1 - 425);
    }
    
    // green channel
    if ((h >= 0) && (h <= 96)) {
        gg = (int) v1;
    } else if (h <= 128) {
        gg = (int) (510 - v1);
    } else if (h <= 160) {
        gg = (int) (850 - v2);
    } else {
        gg = 0;
    }
    
    // blue channel
    if ((h >= 0) && (h <= 96)) {
        bb = 0;
    } else if (h <= 128) {
        bb = (int) (v1 - 255);
    } else if (h <= 160) {
        bb = (int) (v2 - 595);
    } else {
        bb = (int) (680 - v1);
    }
    
}