<|start_filename|>nixie_clock_code/main/include/list.h<|end_filename|>
/**
Copyright (c) 2020 <NAME>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@file list.h
@author <NAME>
@brief Simple C linked list

@see https://idyl.io
@see https://github.com/tonyp7/esp32-nixie-clock

*/

#ifndef MAIN_LIST_H_
#define MAIN_LIST_H_

#include "clock.h" /* for sleep_event_t */

#ifdef __cplusplus
extern "C" {
#endif

#define LIST_DATA_TYPE sleep_event_t

struct node_t {
    LIST_DATA_TYPE data;
    struct node_t* next;
};
typedef struct node_t node_t;

typedef struct list_t {
    unsigned int count;
    node_t* first;
    node_t* last;
}list_t;


/**
 * @brief generate a list object
 */
list_t* list_create();

/**
 * @brief frees all memory allocated to the list and its elements
 */
void list_free(list_t* list);

/**
 * @brief clear and frees all elements of a list, without destroying the list itself
 */
void list_clear(list_t* list);


/**
 * @brief add data to the (end) tail of the list
 */
void list_push(list_t* list, LIST_DATA_TYPE data);


/**
 * @brief add data in the list as per the comparison function provided
 */
void list_add_ordered(list_t* list, LIST_DATA_TYPE data, int (*comp)(LIST_DATA_TYPE*, LIST_DATA_TYPE*));


/**
 * @brief Remove an item from the beginning of the list
 */
int list_shift(list_t* list, LIST_DATA_TYPE * data);


/**
 * @brief Remove an item at the end of the list
 */
int list_pop(list_t* list, LIST_DATA_TYPE * data);


/**
 * @brief Get the first element of the list
 */
int list_peek(list_t* list, LIST_DATA_TYPE * data);


#ifdef __cplusplus
}
#endif

#endif

<|start_filename|>nixie_clock_code/main/i2c.c<|end_filename|>
/*
Copyright (c) 2020 <NAME>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@file i2c.c
@author <NAME>

Contains helper functions to communicate over the I2C bus

*/

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/i2c.h"


#include "i2c.h"


esp_err_t i2c_master_init(){
	esp_err_t ret;
    int i2c_master_port = I2C_MASTER_NUM;
    i2c_config_t conf;
    conf.mode = I2C_MODE_MASTER;
    conf.sda_io_num = I2C_MASTER_SDA_IO;
    conf.sda_pullup_en = GPIO_PULLUP_DISABLE;
    conf.scl_io_num = I2C_MASTER_SCL_IO;
    conf.scl_pullup_en = GPIO_PULLUP_DISABLE;
    conf.master.clk_speed = I2C_MASTER_FREQ_HZ;
    ret = i2c_param_config(i2c_master_port, &conf);
    if(ret == ESP_OK){
    	return i2c_driver_install(i2c_master_port, conf.mode, I2C_MASTER_RX_BUF_DISABLE, I2C_MASTER_TX_BUF_DISABLE, 0);
    }
    return ret;

}


esp_err_t i2c_write_bytes(const uint8_t slave_address, const uint8_t register_address, uint8_t *data, size_t data_len){
	esp_err_t ret;
	i2c_cmd_handle_t cmd = i2c_cmd_link_create();
	i2c_master_start(cmd);
	i2c_master_write_byte(cmd, slave_address << 1 | WRITE_BIT, ACK_CHECK_EN);
	i2c_master_write_byte(cmd, register_address, ACK_CHECK_EN);

	for(int i=0; i<data_len; i++){
		i2c_master_write_byte(cmd, data[i], ACK_CHECK_EN);
	}

	i2c_master_stop(cmd);
	ret = i2c_master_cmd_begin(I2C_MASTER_NUM, cmd, pdMS_TO_TICKS( 1000 ) );
	i2c_cmd_link_delete(cmd);
	return ret;
}


esp_err_t i2c_read_bytes(const uint8_t slave_address, const uint8_t register_address, uint8_t *data, size_t data_len){

	/* I don't know why you'd ever call read_bytes with only one byte to read but this is an edge case we can easily handle
	 * by re-routing this call.
	 */
	if(data_len == 1) return i2c_read_byte(slave_address, register_address, &data[0]);

	esp_err_t ret;
	i2c_cmd_handle_t cmd = i2c_cmd_link_create();
	i2c_master_start(cmd);
	i2c_master_write_byte(cmd, slave_address << 1 | WRITE_BIT, ACK_CHECK_EN);
	i2c_master_write_byte(cmd, register_address, ACK_CHECK_EN);
	i2c_master_stop(cmd);
	ret = i2c_master_cmd_begin(I2C_MASTER_NUM, cmd, pdMS_TO_TICKS( 1000 ) );
	i2c_cmd_link_delete(cmd);
	if (ret != ESP_OK) {
		return ret;
	}

	cmd = i2c_cmd_link_create();
	i2c_master_start(cmd);
	i2c_master_write_byte(cmd, slave_address << 1 | READ_BIT, ACK_CHECK_EN);
	for(int i=0; i < data_len-1; i++){
		i2c_master_read_byte(cmd, &data[i], ACK_VAL);
	}
	i2c_master_read_byte(cmd, &data[data_len-1], NACK_VAL);
	i2c_master_stop(cmd);
	ret = i2c_master_cmd_begin(I2C_MASTER_NUM , cmd, pdMS_TO_TICKS( 1000 ) );
	i2c_cmd_link_delete(cmd);

	return ret;

}

esp_err_t i2c_read_byte(const uint8_t slave_address, const uint8_t register_address, uint8_t *value){

	esp_err_t ret;
	i2c_cmd_handle_t cmd = i2c_cmd_link_create();
	i2c_master_start(cmd);
	i2c_master_write_byte(cmd, slave_address << 1 | WRITE_BIT, ACK_CHECK_EN);
	i2c_master_write_byte(cmd, register_address, ACK_CHECK_EN);
	i2c_master_stop(cmd);
	ret = i2c_master_cmd_begin(I2C_MASTER_NUM, cmd, pdMS_TO_TICKS( 1000 ) );
	i2c_cmd_link_delete(cmd);
	if (ret != ESP_OK) {
		return ret;
	}

	cmd = i2c_cmd_link_create();
	i2c_master_start(cmd);
	i2c_master_write_byte(cmd, slave_address << 1 | READ_BIT, ACK_CHECK_EN);
	i2c_master_read_byte(cmd, value, NACK_VAL);
	i2c_master_stop(cmd);
	ret = i2c_master_cmd_begin(I2C_MASTER_NUM , cmd, pdMS_TO_TICKS( 1000 ) );
	i2c_cmd_link_delete(cmd);

	return ret;

}


esp_err_t i2c_write_byte(const uint8_t slave_address, const uint8_t register_address, const uint8_t value){
	esp_err_t ret;
	i2c_cmd_handle_t cmd = i2c_cmd_link_create();
	i2c_master_start(cmd);
	i2c_master_write_byte(cmd, slave_address << 1 | WRITE_BIT, ACK_CHECK_EN);
	i2c_master_write_byte(cmd, register_address, ACK_CHECK_EN);
	i2c_master_write_byte(cmd, value, ACK_CHECK_EN);
	i2c_master_stop(cmd);
	ret = i2c_master_cmd_begin(I2C_MASTER_NUM, cmd, pdMS_TO_TICKS( 1000 ) );
	i2c_cmd_link_delete(cmd);
	return ret;
}


<|start_filename|>nixie_clock_code/main/clock.js<|end_filename|>
var colorPicker = null;
var sleepMode = null;
var selectedItem = -1;

const gel = (e) => document.getElementById(e);
const zeroPad = (num, places) => String(num).padStart(places, '0');
const week = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"];
const MAX_SLEEPMODES = 4;


function truncate (num, places) {
	return Math.trunc(num * Math.pow(10, places)) / Math.pow(10, places);
}

function docReady(fn) {
	// see if DOM is already available
	if (
		document.readyState === "complete" ||
		document.readyState === "interactive"
	) {
		// call on next available tick
		setTimeout(fn, 1);
	} 
	else {
		document.addEventListener("DOMContentLoaded", fn);
	}
}



function buildDayHTML(day){

	let ret = "";
	let bit = 0x01;
	for (let i = 0; i < 7; i++) {

		if( (day & bit) > 0){
			ret += "<b>" + week[i] + "</b>";
		}
		else{
			ret += "<i>" + week[i] + "</i>";
		}

		if(i!=6) ret += " ";

		bit <<= 1;
	}

	return ret;
}


function timeFromStr(str){

	var a = str.split(":");
	var hours = parseInt(a[0], 10);
	var minutes = parseInt(a[1], 10);
	
	return Math.round(  hours * 3600 + minutes * 60  );
}

function buildTime(t){

	let hours =  truncate(t / 3600, 0);
	let minutes = Math.round((t % 3600) / 60);

	return zeroPad(hours.toString(), 2) + ":" + zeroPad(minutes.toString(), 2);
}


function colorChangeCallback(color) {

	console.log(color.rgb);

	try{
		res = fetch("backlights/", {
			method: "POST",
			headers: {
			  "Content-Type": "application/json",
			},
			body: JSON.stringify(color.rgb),
		  });
	}
	catch (e) {
		console.info("error in colorChangeCallback");
	}
	
}

async function getTimezones(){

	try{

		let restz = await fetch("timezone/");
		let timezone = await restz.text();

		let res = await fetch("timezones.json");
		let timezones = await res.json();

		let sel = gel("timezone-select");
		sel.innerHTML = "";

		timezones.forEach(function(entry) {
			let opt = document.createElement('option');
			opt.value = entry;
			opt.selected = entry == timezone;
			opt.innerHTML = entry;
			sel.appendChild(opt);
		});


	}
	catch (e){
		console.info("error" + e);
	}

}

async function getSleepMode(url = "sleepmode/") {
	try {


		var res;
		
		if(sleepMode == null){
			res = await fetch(url);
		}
		else{
			res = await fetch(url, {
				method: "POST",
				headers: {
				  "Content-Type": "application/json",
				},
				body: JSON.stringify(sleepMode),
			  });
		}
		var mode = await res.json();

		//create list of sleep modes

		let chkbx = gel("sleepmode-enable");
		chkbx.checked = mode.enabled;
		if(mode.enabled){

			let activeSM = 0;
			for (let i = 0; i < mode.data.length; i++) {
				let sleepItem = mode.data[i];
				let domSleepItem = gel("sm" + i.toString());

				if(sleepItem.enabled){
					activeSM++;
					domSleepItem.style.display = 'block';

					let d = buildDayHTML(sleepItem.days);
					let from = buildTime(sleepItem.from);
					let to = buildTime(sleepItem.to);

					domSleepItem.innerHTML = d + ", <b>" + from + "</b> to <b>" + to + "</b>";
				}
				else{
					domSleepItem.style.display = 'none';
				}
			}

			//hide or show new button
			let newSM = gel("sm" + MAX_SLEEPMODES.toString());
			if(activeSM == MAX_SLEEPMODES){
				newSM.style.display = 'none';
			}
			else{
				newSM.style.display = 'block';
			}

			//show the sleepmodes
			gel("sleepmodes").style.display = 'block';
		}
		else{
			//hide the whole thing
			gel("sleepmodes").style.display = 'none';
		}



		sleepMode = mode;
		
	} 
	catch (e) {
		console.info("cannot access sleepmode");
	}
}


async function sleepModeDel(){

	if(selectedItem >= 0){


		let data = sleepMode.data[selectedItem];

		data.enabled = false;
		data.days = 0;
		data.from = 0;
		data.to = 0;

		sleepMode.data.splice(selectedItem, 1);
		sleepMode.data.push(data);

		await getSleepMode();
	}
	gel("diag-sleepmode").style.display = "none";
	gel("clock-wrap").classList.remove("blur");
	

}

async function changeTimezone(){
	
	let sel = gel("timezone-select");
	let data = { "timezone": sel.value};

	console.log(data);

	try{
		let res = await fetch("timezone/", {
			method: "POST",
			headers: {
			  "Content-Type": "application/json",
			},
			body: JSON.stringify(data),
		  });
	}
	catch (e) {
		console.info("error in changeTimezone");
	}

}

async function sleepModeOk(){

	if(selectedItem >= 0){

		let data = sleepMode.data[selectedItem];

		//build days bitfield based on checkboxes
		let days = 0;
		let bit = 0x01;
		for (let i = 0; i < 7; i++) {
			let chkbx = gel(week[i]);
			if(chkbx.checked){
				days |= bit;
			}
			bit <<= 1;
		}

		//convert time strings to seconds
		let tf = gel("time-from");
		let tt = gel("time-to");
		let from = timeFromStr(tf.value);
		let to = timeFromStr(tt.value);

		data.enabled =  (days != 0) && ( from != to);

		if(data.enabled){
			data.days = days;
			data.from = from;
			data.to = to;
		}
		else{
			//invalid entry
			data.days = 0;
			data.from = 0;
			data.to = 0;
		}

		await getSleepMode();

	}
	gel("diag-sleepmode").style.display = "none";
	gel("clock-wrap").classList.remove("blur");
}

function changeSleepMode(e){

	//fill in data
	let id = parseInt(e.currentTarget.id.charAt(2));
	let sleepItem = null;
	if(id == MAX_SLEEPMODES){

		//new sleepmode: -- find the first empty one
		for(let i=0;i<MAX_SLEEPMODES;i++){
			if( !sleepMode.data[i].enabled ){
				sleepItem = sleepMode.data[i];

				sleepItem.enabled = false;
				sleepItem.days = 0;
				sleepItem.from = 0;
				sleepItem.to = 0;

				id = i;

				break;
			}
		}

		//hide delete button
		gel("del-sleepmode").style.display = "none";
	}
	else{
		sleepItem = sleepMode.data[id];
		//show delete button
		gel("del-sleepmode").style.display = "block";
	}


	if(sleepItem != null){
		//save selected item for when user clicks delete or ok
		selectedItem = id;

		//build checkboxes
		let bit = 0x01;
		for (let i = 0; i < 7; i++) {
			let chkbx = gel(week[i]);
			chkbx.checked = (sleepItem.days & bit) > 0
			bit <<= 1;
		}

		//build times
		let tf = gel("time-from");
		let tt = gel("time-to");
		tf.value = buildTime(sleepItem.from);
		tt.value = buildTime(sleepItem.to);

		gel("diag-sleepmode").style.display = "block";
		gel("clock-wrap").classList.add("blur");
	}
}

docReady(async function () {
	console.log("ready!");

	await getSleepMode();
	await getTimezones();

	var colorPicker = new iro.ColorPicker('#color-picker-container');
	colorPicker.on("color:change", colorChangeCallback);

	for(let i=0; i < MAX_SLEEPMODES+1; i++){
		gel("sm" + i.toString()).addEventListener(
		"click",
			(e) => {
				changeSleepMode(e);
			},
			false
		);
	}



	gel("sleepmode-enable").addEventListener(
	"click",
		async (e) => {
			
			let chkd = gel("sleepmode-enable").checked;

			if(sleepMode != null){

				if(chkd != sleepMode.enabled){
					sleepMode.enabled = chkd;
					await getSleepMode();
				}
			}
		},
		false
	);


	gel("timezone-select").addEventListener('change', async (event) => {
		await changeTimezone();
	});

	gel("cancel-sleepmode").addEventListener(
	"click",
		(e) => {
			gel("diag-sleepmode").style.display = "none";
			gel("clock-wrap").classList.remove("blur");
		},
		false
	);

	gel("del-sleepmode").addEventListener(
	"click",
		(e) => {
			sleepModeDel();
		},
		false
	);

	gel("ok-sleepmode").addEventListener(
	"click",
		(e) => {
			sleepModeOk();
		},
		false
	);
});



    

<|start_filename|>nixie_clock_code/main/main.c<|end_filename|>
/*
Copyright (c) 2017-2019 <NAME>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@file main.c
@author <NAME>
@brief Entry point for the ESP32 application.
@see https://idyl.io
@see https://github.com/tonyp7/esp32-wifi-manager
*/



#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "driver/gpio.h"
#include "driver/spi_master.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_spi_flash.h"
#include "esp_wifi.h"
#include "esp_system.h"
#include "nvs_flash.h"
#include "esp_log.h"
#include "freertos/event_groups.h"
#include "mdns.h"
#include "lwip/api.h"
#include "lwip/err.h"
#include "lwip/netdb.h"
#include "cJSON.h"


#include "wifi_manager.h"

#include "clock.h"
#include "i2c.h"
#include "ds3231.h"
#include "display.h"
#include "ws2812.h"
#include "webapp.h"



/* @brief tag used for ESP serial console messages */
static const char TAG[] = "main";

/**
 * @brief RTOS task that periodically prints the heap memory available.
 * @note Pure debug information, should not be ever started on production code
 */
void monitoring_task(void *pvParameter)
{
  for(;;){
    ESP_LOGI(TAG, "free heap: %d",esp_get_free_heap_size());
    vTaskDelay( pdMS_TO_TICKS(20000) );
  }
}


void app_main()
{


	/* GPIO/RMT init for the WS2812 driver */
	ESP_ERROR_CHECK(ws2812_init());
	
	/* GPIO init for SPI transactions & GPIOs used to control the display */
	ESP_ERROR_CHECK(display_init());

	/* start the wifi manager */
	wifi_manager_start();
  webapp_register_handlers();

	/* register cb for internet connectivity */
	wifi_manager_set_callback(WM_EVENT_STA_GOT_IP, &clock_notify_sta_got_ip);

	/* clock task */
  xTaskCreatePinnedToCore(&clock_task, "clock_task", 16384, NULL, CLOCK_TASK_PRIORITY, NULL, 1);

	//xTaskCreatePinnedToCore(&monitoring_task, "monitoring_task", 2048, NULL, 1, NULL, 1);
}


<|start_filename|>nixie_clock_code/main/clock.css<|end_filename|>
#color-picker-container{
	margin:0 auto;
    width:300px;
    display:flex;
    justify-content:center;
    align-items:center;
    padding-top:10px;
    padding-bottom:10px;
}

#color-picker, #sleepmode{
    margin-top:30px;
}

select {
    font: 1.1em tahoma, arial, sans-serif;
}

#timezone-select{
	border:none;
	width:300px;
}

body {
    background-color: #eee;
    border: 0;
    margin: 0;
    font: 1.1em tahoma, arial, sans-serif;
}
a {
    color: darkblue;
    transition: color .2s ease-out;
    text-decoration: none
}
a:hover {
    color: red;
}
input {
    display: none;
    font: 1.1em tahoma, arial, sans-serif;
}
input:focus,
select:focus,
textarea:focus,
button:focus {
    outline: none;
}
input[type="button"] {
    width: 100px;
    padding: 5px;
    text-align: center;
    display: block;
}

input[type="time"]{
    display: inline;
}

input[type="checkbox"]{
    display: inline;
}

p {
    padding: 10px;
}

.diag-box {
    position: absolute;
    top: 0;
    left: 0;
    bottom: 0;
    right: 0;
    height: 100%;
    width: 100%;
    display: none;
}
.diag-box-win {
    position: absolute;
    left: 5%;
    width: 90%;
    text-align: center;
    border: 2px outset #888;
    background-color: #fff;
    border-radius: 10px;
    top: 20%;
}
.blur {
    -webkit-filter: blur(2px);
    -moz-filter: blur(2px);
    -ms-filter: blur(2px);
    -o-filter: blur(2px);
    filter: blur(2px);
}
.ape {
    margin-left: 20px;
    padding: 10px 0px 10px 10px;
}
.ape:hover {
    cursor: pointer;
}
.brdb {
    border-bottom: 1px solid #888;
}
header {
    background-color: #fff;
    border-top: 2px outset #888;
    border-bottom: 1px solid #888;
}
section {
    background-color: #fff;
    border-bottom: 1px solid #888;
    border-top: 1px solid #888;
}
h1 {
    display: block;
    text-align: center;
    margin: 0;
    padding: 15px;
    font-size: 1.4em
}
h2 {
    margin: 0;
    margin-top: 20px;
    padding: 10px;
    text-transform: uppercase;
    color: #888;
    font-size: 1.0em
}
h3 {
    margin: 0;
    text-align: center;
    padding: 20px 0px 20px 0px;
}
.gr {
    color: green;
}
.rd {
    color: red;
}

.buttons {
    padding: 15px;
    display:flex;
    justify-content:space-evenly;
}

.ctr {
    margin: 0 auto;
}
.tctr {
    text-align: center;
}

.fr {
    float: right;
    margin-right: 20px;
}

#diag-sleepmode {
    display: none;
}



<|start_filename|>nixie_clock_code/main/include/ds3231.h<|end_filename|>
/*
Copyright (c) 2019 <NAME>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@file ds3231.h
@author <NAME>
@brief Defines all functions necessary to query a DS3231 IC

@see https://idyl.io
@see https://github.com/tonyp7/esp32-nixie-clock

*/

#ifndef MAIN_DS3231_H_
#define MAIN_DS3231_H_

#include <esp_err.h>
#include <stdint.h>
#include <time.h>

#ifdef __cplusplus
extern "C" {
#endif

#define DS3231_ADDR						   	0x68


#define DS3231_SECONDS_REGISTER				0x00
#define DS3231_MINUTES_REGISTER				0x01
#define DS3231_HOURS_REGISTER				0x02
#define DS3231_DAY_REGISTER					0x03
#define DS3231_DATE_REGISTER				0x04
#define DS3231_MONTH_REGISTER				0x05
#define DS3231_YEAR_REGISTER				0x06
#define DS3231_TIME_REGISTERS_COUNT			7			/* 7 registers from 0x00 to 0x06 */

#define DS3231_CONTROL_REGISTER				0x0E
#define DS3231_CONTROL_STATUS_REGISTER		0x0F
#define DS3231_AGING_OFFSET_REGISTER		0x10
#define DS3231_TEMP_MSB_REGISTER			0x11
#define DS3231_TEMP_LSB_REGISTER			0x12



void ds3231_task(void *pvParameter);

esp_err_t ds3231_enable_square_wave();

float ds3231_get_temperature();
void ds3231_i2c_init();
int ds3231_gettimeofday();
void ds3231_set_datetime(time_t datetime);

esp_err_t ds3231_get_time(struct tm *timeinfo);
esp_err_t ds3231_set_time(const struct tm *timeinfo);
uint8_t ds3231_bcd2dec (uint8_t val);
uint8_t ds3231_dec2bcd (uint8_t val);


#ifdef __cplusplus
}
#endif


#endif /* MAIN_DS3231_H_ */


<|start_filename|>nixie_clock_code/main/include/display.h<|end_filename|>
/*
Copyright (c) 2020 <NAME>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@file display.h
@author <NAME>

Contains functions to control the nixie clock display over SPI

*/


#ifndef MAIN_DISPLAY_H_
#define MAIN_DISPLAY_H_

#include <time.h>
#include <stdbool.h>
#include "ws2812.h"

#ifdef __cplusplus
extern "C" {
#endif

#define	DISPLAY_SPI_CS_GPIO				15
#define DISPLAY_SPI_MOSI_GPIO			13
#define DISPLAY_SPI_SCLK_GPIO			14
#define DISPLAY_OE_GPIO					27
#define DISPLAY_HVEN_GPIO				26
#define DEBUG_USB_POWER_ON_GPIO			17

#define DISPLAY_DIGIT_COUNT				6

#define DISPLAY_TOP_DOT_MASK			(uint16_t)(1<<10)
#define DISPLAY_BOTTOM_DOT_MASK			(uint16_t)(1<<11)



typedef enum display_leading_zero_t{
	DISPLAY_LEADING_ZERO_HIDE = 0,
	DISPLAY_LEADING_ZERO_SHOW = 1
}display_leading_zero_t;

typedef enum display_dot_mode_t{
	DISPLAY_DOT_MODE_BLINK_ON_EVEN_SECONDS = 0,
	DISPLAY_DOT_MODE_BLINK_ON_ODDS_SECONDS = 1,
	DISPLAY_DOT_MODE_BLINK_LEFT_RIGHT = 2,
	DISPLAY_DOT_MODE_BLINK_WHEEL_CW = 3,
	DISPLAY_DOT_MODE_PM_INDICATOR = 4,
	DISPLAY_DOT_MODE_PM_INDICATOR_BLINK = 5,
	DISPLAY_DOT_MODE_OFF = 0x7fffffff
}display_dot_mode_t;

typedef struct display_config_t{
	display_dot_mode_t dot_mode;
	display_leading_zero_t leading_zero;
	bool twelve_hours_format;
	float led_brightness;
	rgb_t led_color;
}display_config_t;

esp_err_t display_init();
esp_err_t display_write_time(struct tm *time);
uint16_t* display_get_vram();
esp_err_t display_write_vram();
esp_err_t display_register_usb_power_interrupt();

/**
 * @brief set the clock display configuration that will be used while displaying the time
 * @see display_write_time
 */
void display_set_config(display_config_t* config);

void display_turn_on();
void display_turn_off();


#ifdef __cplusplus
}
#endif

#endif /* MAIN_DISPLAY_H_ */


<|start_filename|>nixie_clock_code/main/list.c<|end_filename|>
/**
Copyright (c) 2020 <NAME>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@file list.c
@author <NAME>
@brief Simple C linked list

@see https://idyl.io
@see https://github.com/tonyp7/esp32-nixie-clock

*/

#include <stdlib.h>
#include "list.h"

list_t* list_create()
{

	list_t* list = malloc(sizeof(list_t));

	list->first = NULL;
	list->last = NULL;
	list->count = 0;

	return list;
}


void list_free(list_t* list) 
{

	node_t* next;
	node_t* curr = list->first;

	while (curr != NULL) {
		next = curr->next;
		free(curr);
		curr = next;
	}

	free(list);
}

void list_clear(list_t* list)
{
    node_t* next;
	node_t* curr = list->first;

	while (curr != NULL) {
		next = curr->next;
		free(curr);
		curr = next;
	}

    list->count = 0;
    list->first = NULL;
    list->last = NULL;
}



void list_push(list_t* list, LIST_DATA_TYPE data) 
{
	node_t* node = malloc(sizeof(node_t));
	node->data = data;
	node->next = NULL; /* sent to the end so there is no next */

	if ( list->last != NULL) {
		node_t* last = list->last;
		last->next = node;
		list->last = node;
	}
	else {

		list->first = node;
		list->last = node;
	}

	list->count++;
}



void list_add_ordered(list_t* list, LIST_DATA_TYPE data, int (*comp)(LIST_DATA_TYPE*, LIST_DATA_TYPE*))
{

	if (list->count == 0) return list_push(list, data);

	node_t* current = list->first;
	node_t* previous = NULL;

	/* create the node to add to the list */
	node_t* node = malloc(sizeof(node_t));
	node->data = data;
	

	/* find spot in list to add the new element */
	while (current != NULL && comp( &(current->data), &data) < 0) {
		previous = current;
		current = current->next;
	}
	node->next = current;

	if (previous == NULL) {
		/* added at the beginning of the list */
		list->first = node;

		/* in case this was the first element to be added */
		if (list->last == NULL) {
			list->last = node;
		}
	}
	else {

		/* the end of the list is the newly inserted node ? */
		if (list->last == previous) {
			list->last = node;
		}
		previous->next = node;
	}


	list->count++;

}


int list_shift(list_t* list, LIST_DATA_TYPE * data) 
{
	if (list->first) {
		node_t* first = list->first;
		LIST_DATA_TYPE shift_data = first->data;
		
		if (list->last == list->first) { /*edge case where list contains one element */
			list->first = NULL;
			list->last = NULL;
		}
		else {
			list->first = first->next;
		}

		free(first);
		list->count--;

		/* NULL can be passed as data. In that case value isn't sent back to caller */
		if(data){
			*data = shift_data;
		}
        
        return 0;
	}
	else {
		return -1;
	}
}


int list_peek(list_t* list, LIST_DATA_TYPE * data)
{
    if (list->first) {
        *data = list->first->data;
        return 0;
    }
    else{
        return -1;
    }
}

int list_pop(list_t* list, LIST_DATA_TYPE * data)
{
	if (list->last) {

		node_t* last = list->last;
		LIST_DATA_TYPE pop_data = last->data;

		if (list->last == list->first) { /*edge case where list contains one element */
			list->first = NULL;
			list->last = NULL;
		}
		else {
			node_t* current = list->first;
			while (current != NULL && current->next != list->last) {
				current = current->next;
			}
			current->next = NULL;
			list->last = current;
		}

		free(last);
		list->count--;

        *data = pop_data;
        return 0;

	}
	else {
		return -1;
	}
}

<|start_filename|>nixie_clock_code/main/include/i2c.h<|end_filename|>
/**
Copyright (c) 2020 <NAME>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@file i2c.h
@author <NAME>

Contains helper functions to communicate over the I2C bus

*/

#ifndef MAIN_I2C_H_
#define MAIN_I2C_H_

#include <esp_err.h>
#include <stdint.h>

#ifdef __cplusplus
extern "C" {
#endif


#define I2C_MASTER_SCL_IO					22               /*!< gpio number for I2C master clock */
#define I2C_MASTER_SDA_IO					21               /*!< gpio number for I2C master data  */
#define I2C_MASTER_NUM             			I2C_NUM_0        /*!< I2C port number for master dev */
#define I2C_MASTER_TX_BUF_DISABLE  			0                /*!< I2C master do not need buffer */
#define I2C_MASTER_RX_BUF_DISABLE 	 		0                /*!< I2C master do not need buffer */
#define I2C_MASTER_FREQ_HZ      	   		400000           /*!< I2C master clock frequency */



#define WRITE_BIT                          	I2C_MASTER_WRITE	/*!< I2C master write */
#define READ_BIT                           	I2C_MASTER_READ 	/*!< I2C master read */
#define ACK_CHECK_EN                       	0x1             	/*!< I2C master will check ack from slave*/
#define ACK_CHECK_DIS                      	0x0              	/*!< I2C master will not check ack from slave */
#define ACK_VAL                            	0x0             	/*!< I2C ack value */
#define NACK_VAL 							0x1 				/*!< I2C nack value */




esp_err_t i2c_master_init();




/**
 * @brief I2C helper function to write a single byte to a register
 *
 */
esp_err_t i2c_write_byte(const uint8_t slave_address, const uint8_t register_address, const uint8_t value);

/**
 * @brief I2C helper function to read a single byte from a register
 *
 */
esp_err_t i2c_read_byte(const uint8_t slave_address, const uint8_t register_address, uint8_t *value);


/**
 * @brief I2C helper function to read bytes starting from specified register
 * @warning: it is the caller responsability to ensure data buffer is big enough to store data_len bytes.
 */
esp_err_t i2c_read_bytes(const uint8_t slave_address, const uint8_t register_address, uint8_t *data, size_t data_len);


esp_err_t i2c_write_bytes(const uint8_t slave_address, const uint8_t register_address, uint8_t *data, size_t data_len);

#ifdef __cplusplus
}
#endif

#endif /* MAIN_I2C_H_ */


<|start_filename|>nixie_clock_code/main/display.c<|end_filename|>
/*
Copyright (c) 2020 <NAME>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@file display.c
@author <NAME>

Contains functions to control the nixie clock display over SPI

*/

#include <stdlib.h>
#include <string.h>
#include <byteswap.h>

#include <esp_err.h>
#include <driver/gpio.h>
#include <driver/spi_master.h>
#include <driver/gpio.h>
#include <esp_intr_alloc.h>

#include "display.h"


static uint16_t *display_vram;
static spi_device_handle_t spi;
static spi_transaction_t t;
static display_config_t display_config;



static void IRAM_ATTR gpio_usb_power_isr_handler(void* arg){

	/* disable display when USB is on as a measure of security for the whole board. USB powering HV power supply after going through a tiny diode = bad idea */
	gpio_set_level(DISPLAY_OE_GPIO, 1);
	return;
}


void display_turn_on(){
	/* turn on does not do anything if USB power is connected */
	if(gpio_get_level(DEBUG_USB_POWER_ON_GPIO) == 0){
		gpio_set_level(DISPLAY_HVEN_GPIO, 1);
		gpio_set_level(DISPLAY_OE_GPIO, 0);
	}
}

void display_turn_off(){
	gpio_set_level(DISPLAY_OE_GPIO, 1);
	gpio_set_level(DISPLAY_HVEN_GPIO, 0);
}


esp_err_t display_register_usb_power_interrupt(){

	/* setup DEBUG_USB_POWER_ON_GPIO as INTERRUPT on RISING EGDE */
	gpio_config_t io_conf;
	io_conf.intr_type = GPIO_PIN_INTR_POSEDGE;
	/* GPIO 4 bit mask */
	io_conf.pin_bit_mask = (1ULL << DEBUG_USB_POWER_ON_GPIO);
	/* Input */
	io_conf.mode = GPIO_MODE_INPUT;
	/* Disable pu/pd as this is tied to a voltage divider from 5V rail */
	io_conf.pull_up_en = GPIO_PULLUP_DISABLE;
	io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
	gpio_config(&io_conf);

	/* install ISR service */
	gpio_install_isr_service(ESP_INTR_FLAG_LEVEL1);
	return gpio_isr_handler_add(DEBUG_USB_POWER_ON_GPIO, gpio_usb_power_isr_handler, (void*)DEBUG_USB_POWER_ON_GPIO);
}



esp_err_t display_init(){

	esp_err_t ret;

	display_vram = (uint16_t*)malloc(sizeof(uint16_t) * DISPLAY_DIGIT_COUNT);
	memset(display_vram, 0x00, sizeof(uint16_t) * DISPLAY_DIGIT_COUNT);

	memset(&display_config, 0x00, sizeof(display_config_t));

	/* setup all GPIOs */
	gpio_set_direction(DISPLAY_SPI_CS_GPIO, GPIO_MODE_OUTPUT);
	gpio_set_direction(DISPLAY_OE_GPIO, GPIO_MODE_OUTPUT);
	gpio_set_direction(DISPLAY_HVEN_GPIO, GPIO_MODE_OUTPUT);
	gpio_set_direction(DEBUG_USB_POWER_ON_GPIO, GPIO_MODE_INPUT);

	/* by default display is off but HV power supply is enabled, idling */
	gpio_set_level(DISPLAY_OE_GPIO, 1); /* output enable is reversed logic. This effectively has no effect since the pin is physically pulled-up to 3v3 */
	gpio_set_level(DISPLAY_HVEN_GPIO, 1);

	/* register interrupt on USB power */
	//ret = display_register_usb_power_interrupt();
	//if(ret != ESP_OK) return ret;

	spi_bus_config_t buscfg={
		.miso_io_num=-1, /* -1 == not used */
		.mosi_io_num=DISPLAY_SPI_MOSI_GPIO,
		.sclk_io_num=DISPLAY_SPI_SCLK_GPIO,
		.quadwp_io_num=-1,
		.quadhd_io_num=-1
	};
	spi_device_interface_config_t devcfg={
		.clock_speed_hz=100000,					/* clock at 100Khz => about 1000 frames per seconds! */
		.mode=0,                    			/* SPI mode 0 */
		.spics_io_num=DISPLAY_SPI_CS_GPIO,      /* CS pin CS GPIO pin for this device, or -1 if not used. */
		.queue_size=4                           /* Queue size is not really relevant for the nixie display so its kept low */
		//.pre_cb=ili_spi_pre_transfer_callback,  //Specify pre-transfer callback to handle D/C line
	};


	/* Initialize the SPI bus */
	ret = spi_bus_initialize(HSPI_HOST, &buscfg, 1);
	if(ret!=ESP_OK) return ret;

	/* Attach the display to the SPI bus */
	ret=spi_bus_add_device(HSPI_HOST, &devcfg, &spi);
	if(ret!=ESP_OK) return ret;

	/* prepare transaction */
	memset(&t, 0x00, sizeof(t));
	t.length = DISPLAY_DIGIT_COUNT * 16; /* 6 digits, 16 bits per digits */
	t.tx_buffer = display_vram;
	t.user = NULL;

	return ret;
}



uint16_t* display_get_vram(){
	return display_vram;
}

void display_set_config(display_config_t* config){
	display_config = *config;
}

esp_err_t display_write_vram(){
	esp_err_t ret;


	/* due to esp32 endianess, we need to swipe bytes. The hardware assumes big endian but the esp is little endian. */
	for(int i=0;i<6;i++){
		display_vram[i] = __bswap_16(display_vram[i]);
	}


	/* safe guard. Enable display only if USB is disconnected */
	if(gpio_get_level(DEBUG_USB_POWER_ON_GPIO) == 0){
		gpio_set_level(DISPLAY_OE_GPIO, 0);
	}

	gpio_set_level(DISPLAY_SPI_CS_GPIO, 0);
	ret=spi_device_transmit(spi, &t);
	gpio_set_level(DISPLAY_SPI_CS_GPIO, 1);

	return ret;
}

esp_err_t display_write_time(struct tm *time){

	if(time){

		/* seconds */
		display_vram[0] =  (uint16_t) (1 << (time->tm_sec % 10));
		display_vram[1] =  (uint16_t) (1 << (time->tm_sec / 10));

		/* minutes */
		display_vram[2] =  (uint16_t) (1 << (time->tm_min % 10));
		display_vram[3] =  (uint16_t) (1 << (time->tm_min / 10));

		/* hours */
		int hours = time->tm_hour;
		if(display_config.twelve_hours_format){

			if(hours > 12){
				hours -= 12;
			}
			else if(hours == 0){ /* in the complete BS that is the 12 hours format, midnight is 12 */
				hours = 12; 
			}
			
		}
		display_vram[4] =  (uint16_t) (1 << (hours % 10));


		int hours_tens = hours / 10;
		if(hours_tens == 0 && display_config.leading_zero == DISPLAY_LEADING_ZERO_HIDE){
			display_vram[5] =  (uint16_t) 0;
		}
		else{
			display_vram[5] =  (uint16_t) (1 << hours_tens);
		}
		


		if(time->tm_sec % 2 == 0){
			display_vram[2] |= DISPLAY_TOP_DOT_MASK;
			display_vram[2] |= DISPLAY_BOTTOM_DOT_MASK;
			display_vram[4] |= DISPLAY_TOP_DOT_MASK;
			display_vram[4] |= DISPLAY_BOTTOM_DOT_MASK;
		}


		return display_write_vram();
	}
	else{

		display_vram[0] = (uint16_t) (1 << 0);
		display_vram[1] = (uint16_t) (1 << 0);
		display_vram[2] = (uint16_t) (1 << 0);
		display_vram[3] = (uint16_t) (1 << 0);
		display_vram[4] = (uint16_t) (1 << 0);
		display_vram[5] = (uint16_t) (1 << 0);

		return display_write_vram();
	}


}


<|start_filename|>nixie_clock_code/main/include/http_client.h<|end_filename|>
/*
Copyright (c) 2019 <NAME>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@file http_client.h
@author <NAME>

Contains wrappers around the esp http client for the mclk.org time API.

*/


#ifndef MAIN_INCLUDE_HTTP_CLIENT_H_
#define MAIN_INCLUDE_HTTP_CLIENT_H_

#include <esp_http_client.h> /* esp_http_client_handle_t */
#include "time.h" /* time_t */
#include "clock.h" /* timezone_t */

#ifdef __cplusplus
extern "C" {
#endif

#define HTTP_CLIENT_MAX_REQUEST_SIZE	256

esp_err_t http_client_init();
void http_client_cleanup(esp_http_client_handle_t client);
void http_client_get_api_time(char* timezone);
void http_client_get_transitions(timezone_t timezone, time_t now);
void http_rest();
//void http_client_task(void *pvParameter);


#ifdef __cplusplus
}
#endif


#endif /* MAIN_INCLUDE_HTTP_CLIENT_H_ */


<|start_filename|>nixie_clock_code/main/include/clock.h<|end_filename|>
/*
Copyright (c) 2019 <NAME>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@file clock.h
@author <NAME>
@brief Describes the main task driving the rest of the nixie clock

@see https://idyl.io
@see https://github.com/tonyp7/esp32-nixie-clock

For a comprehensive documentation on how the timezone strings work:
@see https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html
@see ftp://ftp.iana.org/tz/tz-how-to.html
Unfortunately, it is broken on the ESP32. Things like CEST+2 simply do not work
which makes it unusable. The clock instead references everything from UTC and
apply offset manually to the timestamp.

*/


#ifndef MAIN_CLOCK_H_
#define MAIN_CLOCK_H_


#include <stdint.h> /* for int32_t, etc. */
#include <time.h> /* for time_t */
#include <stdbool.h> /* for bool */
#include <esp_err.h> /* for esp_err_t */
#include "freertos/FreeRTOS.h" /* TickType_t */
#include "cJSON.h" /* for cJSON */
#include "display.h" /* display_config_t */

#ifdef __cplusplus
extern "C" {
#endif

/** @brief Defines the task priority of the clock (main task).
 *  This should be the highest priority task unless very specific reason. Default: 10.
 */
#define CLOCK_TASK_PRIORITY				CONFIG_CLOCK_TASK_PRIORITY


/**
 * @brief Store the timezone in its readable way, such as "America/New_York"
 * At the time of writing this there are three 30 char long zones; which are:
 * 	America/Argentina/Buenos_Aires
 * 	America/Argentina/Rio_Gallegos
 * 	America/North_Dakota/New_Salem
 * 40 is rounded up to the nearest 10, to include the \0.
 */
#define CLOCK_MAX_TZ_STRING_LENGTH			40


/** in seconds, the maximum drift allowed before we force a refresh of the time */
#define CLOCK_MAX_ACCEPTABLE_TIME_DRIFT		60.0

/** number of transitions that will be stored in advance. Most timezones have 0 or 2 (summer time) so the default of 3 is plenty. */
#define CLOCK_MAX_TRANSITIONS				3

/** maximum numbers of sleepmodes a user can set */
#define CLOCK_MAX_SLEEPMODES				4

typedef enum clock_message_t{
	CLOCK_MESSAGE_NONE = 0,
	CLOCK_MESSAGE_TICK = 1,
	CLOCK_MESSAGE_STA_GOT_IP = 2,
	CLOCK_MESSAGE_STA_DISCONNECTED = 3,
	CLOCK_MESSAGE_RECEIVE_TIME_API = 4,
	CLOCK_MESSAGE_RECEIVE_TRANSITIONS_API = 5,
	CLOCK_MESSAGE_REQUEST_TRANSITIONS_API_CALL = 6,
	CLOCK_MESSAGE_REQUEST_TIME_API = 7,
	CLOCK_MESSAGE_SLEEPMODE_CONFIG = 8,
	CLOCK_MESSAGE_TIMEZONE = 9,
	CLOCK_MESSAGE_SLEEP_EVENT = 10,
	CLOCK_MESSAGE_BACKLIGHTS_CONFIG = 11,
	CLOCK_MESSAGE_MAX = 0x7fffffff
}clock_message_t;

/** @brief type that is processed by the clock queue */
typedef struct clock_queue_message_t{
	clock_message_t message;
	void *param;
}clock_queue_message_t;

typedef struct timezone_t{
	int32_t offset;
	char name[CLOCK_MAX_TZ_STRING_LENGTH];
}timezone_t;

typedef struct transition_t{
	int32_t offset;
	time_t timestamp;
}transition_t;

/**
 * @brief defines a structure holding sleepmode information: which days to apply, and from what time to what time
 */
typedef struct sleepmode_t{
	bool enabled;
	uint8_t days;
	time_t from;
	time_t to;
}sleepmode_t;


typedef struct sleepmodes_t{
	bool enable_sleepmode;
	sleepmode_t sleepmode[CLOCK_MAX_SLEEPMODES];
}sleepmodes_t;

typedef enum sleep_action_t{
	SLEEP_ACTION_UNKNOWN = 0,
	SLEEP_ACTION_WAKE = 1,
	SLEEP_ACTION_SLEEP = 2,
	SLEEP_ACTION_MAX = 0x7fffffff
}sleep_action_t;

typedef struct sleep_event_t{
	time_t timestamp;
	sleep_action_t action;
}sleep_event_t;


/**
 * @brief defines a structure holding the gloabl configuration of the clock */
typedef struct clock_config_t{
	timezone_t timezone;
	sleepmodes_t sleepmodes;
	display_config_t display;
}clock_config_t;


#define GPIO_INPUT_IO_4 				4


void clock_notify_new_timezone(char* timezone);
void clock_notify_new_sleepmodes(sleepmodes_t sleepmodes);
void clock_notify_sta_got_ip(void* pvArgument);
void clock_notify_sta_disconnected();
void clock_notify_new_backlight_color(rgb_t rgb);
void clock_notify_time_api_response(cJSON *json);
void clock_notify_transitions_api_response(cJSON *json);
void clock_tick();
void clock_task(void *pvParameter);
esp_err_t clock_register_sqw_interrupt();

time_t clock_get_current_time_utc();



/**
 * @brief saves the current configuration to NVS memory
 */
esp_err_t clock_save_config(clock_config_t *conf);


/**
 * @brief retrieves the entire clock configuration 
 */
clock_config_t clock_get_config();

/**
 * @brief retrieves only the timezone_t component of the clock configuration
 * This function is helpful if only a portion of the config is needed and dumping
 * the entire configuration on the stack would be wasteful.
 * @see clock_get_config
 */
timezone_t clock_get_config_timezone();

/**
 * @brief retrieves only the display_config_t component of the clock configuration
 * This function is helpful if only a portion of the config is needed and dumping
 * the entire configuration on the stack would be wasteful.
 * @see clock_get_config
 */
display_config_t clock_get_config_display();



/**
 * @brief Retrieves the configuration from NVS memory
 */
esp_err_t clock_get_nvs_config(clock_config_t *conf);

/**
 * Mutex lock to avoid resource sharing issues when accessing the NVS
 */
bool clock_nvs_lock(TickType_t xTicksToWait);

/**
 * @see clock_nvs_lock
 */
void clock_nvs_unlock();


esp_err_t clock_save_timezone(timezone_t *tz);


//void clock_save_timezone_task(void *pvParameter);

void clock_change_timezone(timezone_t tz);
esp_err_t clock_get_nvs_timezone(timezone_t *tz);

bool clock_realign(time_t new_t);


#ifdef __cplusplus
}
#endif

#endif /* MAIN_CLOCK_H_ */


<|start_filename|>nixie_clock_code/main/webapp.c<|end_filename|>
/**
Copyright (c) 2020 <NAME>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@file webapp.c
@author <NAME>

Contains page logic handling web app functionalities

*/

#include <stdio.h>
#include <string.h>
#include <esp_log.h>
#include <esp_system.h>
#include <esp_http_server.h>
#include <sys/param.h> /* for the MIN macro */
#include <esp_err.h>
#include <cJSON.h>
#include <http_app.h>

#include "ws2812.h"
#include "display.h"
#include "clock.h"
#include "webapp.h"


/**
 * @brief embedded binary data.
 * @see file "CMakeLists.txt"
 * @see https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/build-system.html#embedding-binary-data
 */
extern const uint8_t clock_css_start[] asm("_binary_clock_css_start");
extern const uint8_t clock_css_end[]   asm("_binary_clock_css_end");
extern const uint8_t clock_js_start[] asm("_binary_clock_js_start");
extern const uint8_t clock_js_end[] asm("_binary_clock_js_end");
extern const uint8_t clock_html_start[] asm("_binary_clock_html_start");
extern const uint8_t clock_html_end[] asm("_binary_clock_html_end");
extern const uint8_t iro_js_start[] asm("_binary_iro_js_start");
extern const uint8_t iro_js_end[] asm("_binary_iro_js_end");
extern const uint8_t timezones_json_start[] asm("_binary_timezones_json_start");
extern const uint8_t timezones_json_end[] asm("_binary_timezones_json_end");


/* const httpd related values stored in ROM */
const static char http_200_hdr[] = "200 OK";
const static char http_400_hdr[] = "400 Bad Request";
//const static char http_503_hdr[] = "503 Service Unavailable";
const static char http_content_type_html[] = "text/html";
const static char http_content_type_txt[] = "text/plain";
const static char http_content_type_js[] = "text/javascript";
const static char http_content_type_css[] = "text/css";
const static char http_content_type_json[] = "application/json";
const static char http_cache_control_hdr[] = "Cache-Control";
const static char http_cache_control_no_cache[] = "no-store, no-cache, must-revalidate, max-age=0";
const static char http_cache_control_cache[] = "public, max-age=31536000";
const static char http_pragma_hdr[] = "Pragma";
const static char http_pragma_no_cache[] = "no-cache";

const static char TAG[] = "webapp";





static cJSON* webapp_get_display_cjson(display_config_t *display){

    cJSON *root = cJSON_CreateObject();

    cJSON *rgb = cJSON_CreateObject();
    cJSON_AddNumberToObject( rgb, "r", display->led_color.r);
    cJSON_AddNumberToObject( rgb, "g", display->led_color.g);
    cJSON_AddNumberToObject( rgb, "b", display->led_color.b);
    cJSON_AddItemToObject(root, "led_color", rgb);

    return root;
}

static cJSON* webapp_get_timezone_cjson(timezone_t *timezone){

    /* format as following
        {
            "name": "Asia/Singapore"
        }
    */

   cJSON *root = cJSON_CreateObject();
   cJSON_AddStringToObject( root, "name", timezone->name );

   return root;
}

static cJSON* webapp_get_sleemodes_cjson(sleepmodes_t *sleepmodes){

	/* format as following:

	{
    "enabled":true,
    "data":[
        {"enabled":true,"days":3,"from":50400,"to":50460},
        {"enabled":false,"days":0,"from":0,"to":0},
        {"enabled":false,"days":0,"from":0,"to":0},
        {"enabled":false,"days":0,"from":0,"to":0}
	}
	*/

    cJSON *root = cJSON_CreateObject();
	cJSON *data = cJSON_CreateArray();

	cJSON_AddBoolToObject( root, "enabled", sleepmodes->enable_sleepmode );
	for(int i=0; i < CLOCK_MAX_SLEEPMODES; i++){

		cJSON *sleepmode = cJSON_CreateObject();
		cJSON_AddBoolToObject( sleepmode, "enabled", sleepmodes->sleepmode[i].enabled );
		cJSON_AddNumberToObject( sleepmode, "days", sleepmodes->sleepmode[i].days );
		cJSON_AddNumberToObject( sleepmode, "from", sleepmodes->sleepmode[i].from );
		cJSON_AddNumberToObject( sleepmode, "to", sleepmodes->sleepmode[i].to );

		cJSON_AddItemToArray( data, sleepmode);
	}
	cJSON_AddItemToObject(root, "data", data);

    return root;
}

static cJSON* webapp_get_config_cjson(clock_config_t *config){

    cJSON *root = cJSON_CreateObject();

    cJSON *timezone = webapp_get_timezone_cjson(  &((*config).timezone) );
    cJSON *sleepmodes = webapp_get_sleemodes_cjson(  &((*config).sleepmodes) );
    cJSON *display = webapp_get_display_cjson(  &((*config).display) );


    cJSON_AddItemToObject(root, "timezone", timezone);
    cJSON_AddItemToObject(root, "sleepmodes", sleepmodes);
    cJSON_AddItemToObject(root, "display", display);

    return root;
}

/**
 * @brief transforms a clock_config_t struct into its json string representation
 */
static char* webapp_get_config_json(clock_config_t config){

    cJSON *conf = webapp_get_config_cjson( &config );
    char* str_json = cJSON_Print(conf);
    cJSON_Delete(conf);

    return str_json;
}

/**
 * @brief transform a sleepmodes_t struct into its json string representation
 */
static char* webapp_get_sleepmodes_json(sleepmodes_t sleepmodes){

	char* str_json = NULL;

	/* format as following:

	{
    "enabled":true,
    "data":[
        {"enabled":true,"days":3,"from":50400,"to":50460},
        {"enabled":false,"days":0,"from":0,"to":0},
        {"enabled":false,"days":0,"from":0,"to":0},
        {"enabled":false,"days":0,"from":0,"to":0}
	}
	*/

	cJSON *root = cJSON_CreateObject();
	cJSON *data = cJSON_CreateArray();

	cJSON_AddBoolToObject( root, "enabled", sleepmodes.enable_sleepmode );
	for(int i=0; i < CLOCK_MAX_SLEEPMODES; i++){

		cJSON *sleepmode = cJSON_CreateObject();
		cJSON_AddBoolToObject( sleepmode, "enabled", sleepmodes.sleepmode[i].enabled );
		cJSON_AddNumberToObject( sleepmode, "days", sleepmodes.sleepmode[i].days );
		cJSON_AddNumberToObject( sleepmode, "from", sleepmodes.sleepmode[i].from );
		cJSON_AddNumberToObject( sleepmode, "to", sleepmodes.sleepmode[i].to );

		cJSON_AddItemToArray( data, sleepmode);
	}
	cJSON_AddItemToObject(root, "data", data);

	str_json = cJSON_Print(root);
	cJSON_Delete(root);

	return str_json;
}

static esp_err_t webapp_get_hander(httpd_req_t *req){

    if(strcmp(req->uri, "/") == 0){

        httpd_resp_set_status(req, http_200_hdr);
		httpd_resp_set_type(req, http_content_type_html);
		httpd_resp_send(req, (char*)clock_html_start, clock_html_end - clock_html_start);
    }
    else if(strcmp(req->uri, "/clock.js") == 0){

        httpd_resp_set_status(req, http_200_hdr);
		httpd_resp_set_type(req, http_content_type_js);
        httpd_resp_set_hdr(req, http_cache_control_hdr, http_cache_control_cache);
		httpd_resp_send(req, (char*)clock_js_start, clock_js_end - clock_js_start);
    }
    else if(strcmp(req->uri, "/iro.min.js") == 0){

        httpd_resp_set_status(req, http_200_hdr);
		httpd_resp_set_type(req, http_content_type_js);
        httpd_resp_set_hdr(req, http_cache_control_hdr, http_cache_control_cache);
		httpd_resp_send(req, (char*)iro_js_start, iro_js_end - iro_js_start);
    }
    else if(strcmp(req->uri, "/clock.css") == 0){

        httpd_resp_set_status(req, http_200_hdr);
		httpd_resp_set_type(req, http_content_type_css);
        httpd_resp_set_hdr(req, http_cache_control_hdr, http_cache_control_cache);
		httpd_resp_send(req, (char*)clock_css_start, clock_css_end - clock_css_start);
    }
    else if(strcmp(req->uri, "/timezones.json") == 0){

        httpd_resp_set_status(req, http_200_hdr);
		httpd_resp_set_type(req, http_content_type_json);
		httpd_resp_send(req, (char*)timezones_json_start, timezones_json_end - timezones_json_start);
    }
    else if(strcmp(req->uri, "/timezone/") == 0){

        clock_config_t conf = clock_get_config();

        httpd_resp_set_status(req, http_200_hdr);
		httpd_resp_set_type(req, http_content_type_txt);
        httpd_resp_set_hdr(req, http_cache_control_hdr, http_cache_control_no_cache);
        httpd_resp_set_hdr(req, http_pragma_hdr, http_pragma_no_cache);
        httpd_resp_send(req, conf.timezone.name, strlen( conf.timezone.name ));
        
    }
    else if(strcmp(req->uri, "/config/") == 0){

        char* str_json = webapp_get_config_json( clock_get_config() );

        if(str_json){

            httpd_resp_set_status(req, http_200_hdr);
            httpd_resp_set_type(req, http_content_type_json);
            httpd_resp_set_hdr(req, http_cache_control_hdr, http_cache_control_no_cache);
            httpd_resp_set_hdr(req, http_pragma_hdr, http_pragma_no_cache);
            httpd_resp_send(req, str_json, strlen(str_json));

            free(str_json);

            return ESP_OK;
        }
        else{
            httpd_resp_send_500(req);
            return ESP_FAIL;
        }

    }
    else if(strcmp(req->uri, "/sleepmode/") == 0){

        clock_config_t conf = clock_get_config();
        char* str_json = webapp_get_sleepmodes_json( conf.sleepmodes );

        if(str_json){

            httpd_resp_set_status(req, http_200_hdr);
            httpd_resp_set_type(req, http_content_type_json);
            httpd_resp_set_hdr(req, http_cache_control_hdr, http_cache_control_no_cache);
            httpd_resp_set_hdr(req, http_pragma_hdr, http_pragma_no_cache);
            httpd_resp_send(req, str_json, strlen(str_json));

            free(str_json);

            return ESP_OK;
        }
        else{
            httpd_resp_send_500(req);
            return ESP_FAIL;
        }
    }
    else{

        httpd_resp_send_404(req);
    }

    return ESP_OK;
}

/**
 * @brief Parses a json string into a sleepmodes_t struct 
 */
static esp_err_t webapp_parse_sleemodes(char *content, sleepmodes_t *sleepmodes){

    sleepmodes_t sm;
    memset(&sm, 0x00, sizeof(sleepmodes_t));
    
    cJSON *json = cJSON_Parse(content);
    if(json == NULL){

        const char *error_ptr = cJSON_GetErrorPtr();
        ESP_LOGE(TAG, "cJSON parsing error: %s", error_ptr);
        return ESP_FAIL;
    }

    cJSON *enable_sleepmode = cJSON_GetObjectItemCaseSensitive(json, "enabled");
    sm.enable_sleepmode = (bool)enable_sleepmode->valueint;

    cJSON *data = cJSON_GetObjectItemCaseSensitive(json, "data");
    cJSON *sleepmode;
    int i = 0;

    cJSON_ArrayForEach(sleepmode, data){
        cJSON *enabled = cJSON_GetObjectItemCaseSensitive(sleepmode, "enabled");
        cJSON *days = cJSON_GetObjectItemCaseSensitive(sleepmode, "days");
        cJSON *from = cJSON_GetObjectItemCaseSensitive(sleepmode, "from");
        cJSON *to = cJSON_GetObjectItemCaseSensitive(sleepmode, "to");

        sm.sleepmode[i].enabled = (bool)enabled->valueint;
        sm.sleepmode[i].days = days->valueint;
        sm.sleepmode[i].from = from->valueint;
        sm.sleepmode[i].to = to->valueint;
        i++;
        if(i >= CLOCK_MAX_SLEEPMODES) break;
    }

    cJSON_Delete(json);
    *sleepmodes = sm;

    return ESP_OK;
}

static esp_err_t webapp_post_handler(httpd_req_t *req){

    if(strcmp(req->uri, "/timezone/") == 0){
        /* process a timezone change. Expected content is in the format: 
            { "timezone" : "America\/Indiana\/Indianapolis" }
        */
        clock_config_t conf = clock_get_config();
        const size_t buffer_size = 60; 
        char content[buffer_size]; /* 60 byte is OK to just put on the stack */
        esp_err_t ret = ESP_OK;
        memset(content, 0x00, buffer_size);

        /* Truncate if content length larger than the buffer. If we fall on this case it means the request is probably buggy */
        size_t recv_size = MIN(req->content_len, sizeof(content));

        int read_count = httpd_req_recv(req, content, recv_size);
        if (read_count <= 0) {  /* 0 return value indicates connection closed */
            /* Check if timeout occurred */
            if (read_count == HTTPD_SOCK_ERR_TIMEOUT) {
                /* In case of timeout one can choose to retry calling
                    * httpd_req_recv(), but to keep it simple, here we
                    * respond with an HTTP 408 (Request Timeout) error */
                httpd_resp_send_408(req);
            }
            else {
                httpd_resp_send_500(req);
            }
            return ESP_FAIL;
        }

        /* avoids a buffer overflow parsing the string if the content is just the size of the buffer */
        content[buffer_size - 1] = '\0';

        /* parse to json */
        cJSON *json = cJSON_Parse(content);

        if(json == NULL){
            const char *error_ptr = cJSON_GetErrorPtr();
            ESP_LOGE(TAG, "cJSON parsing error: %s", error_ptr);

            /* bad request */
            httpd_resp_set_status(req, http_400_hdr);
            httpd_resp_send(req, NULL, 0);
            return ESP_FAIL;
        }

        cJSON *timezone = cJSON_GetObjectItemCaseSensitive(json, "timezone");

        if(strcmp(timezone->valuestring, conf.timezone.name) != 0){
            /* timezone change!*/
            clock_notify_new_timezone(timezone->valuestring);
        }

        cJSON_Delete(json);
        return ret;

    }
    else if(strcmp(req->uri, "/sleepmode/") == 0){


        const size_t max_buffer_size = 511;
        sleepmodes_t sleepmodes;
        esp_err_t ret = ESP_OK;
        
        /** read body buffer. The message should be in this format:
            {
                "enabled":	false,
                "data":	[{
                        "enabled":	false,
                        "days":	255,
                        "from":	99999,
                        "to":	99999
                    }, {
                        "enabled":	false,
                        "days":	255,
                        "from":	99999,
                        "to":	99999
                    }, {
                        "enabled":	false,
                        "days":	255,
                        "from":	99999,
                        "to":	99999
                    }, {
                        "enabled":	false,
                        "days":	255,
                        "from":	99999,
                        "to":	99999
                    }]
            }
         * that is to say never more than 400 chars.
         * because this is pretty big to be on the stack
         * it is heap allocated
         */

        /* Truncate if content length larger than the buffer */
        size_t recv_size = MIN(req->content_len, max_buffer_size);
        char* content = malloc(sizeof(char) * (recv_size+1));
        memset(content, 0x00, (recv_size+1));


        int read_count = httpd_req_recv(req, content, recv_size);
        if (read_count <= 0) {  /* 0 return value indicates connection closed */
            /* Check if timeout occurred */
            if (read_count == HTTPD_SOCK_ERR_TIMEOUT) {
                /* In case of timeout one can choose to retry calling
                    * httpd_req_recv(), but to keep it simple, here we
                    * respond with an HTTP 408 (Request Timeout) error */
                httpd_resp_send_408(req);
            }
            else {
                httpd_resp_send_500(req);
            }

            free(content);
            return ESP_FAIL;
        }

        
        /* processing -- build the sleepomodes_t object */
        ret = webapp_parse_sleemodes(content, &sleepmodes);
        if(ret == ESP_OK){

            /* send sleepmode over to the clock */
            clock_notify_new_sleepmodes(sleepmodes);

            /* web answer */
            httpd_resp_set_status(req, http_200_hdr);
            httpd_resp_set_type(req, http_content_type_json);
            httpd_resp_set_hdr(req, http_cache_control_hdr, http_cache_control_no_cache);
            httpd_resp_set_hdr(req, http_pragma_hdr, http_pragma_no_cache);
            httpd_resp_send(req, content, strlen(content));
            free(content);
            return ESP_OK;
        }
        else{
            httpd_resp_send_408(req);
            free(content);
            return ret;
        }

    }
    else if(strcmp(req->uri, "/backlights/") == 0){

        /* read body buffer. The message should be { r: 123, g: 123, b: 123 } that is to say never more than 30 chars. */
        const size_t buffer_size = 30;
        char content[buffer_size];
        esp_err_t ret = ESP_OK;
        memset(content, 0x00, buffer_size);

        /* Truncate if content length larger than the buffer */
        size_t recv_size = MIN(req->content_len, sizeof(content));

        int read_count = httpd_req_recv(req, content, recv_size);
        if (read_count <= 0) {  /* 0 return value indicates connection closed */
            /* Check if timeout occurred */
            if (read_count == HTTPD_SOCK_ERR_TIMEOUT) {
                /* In case of timeout one can choose to retry calling
                    * httpd_req_recv(), but to keep it simple, here we
                    * respond with an HTTP 408 (Request Timeout) error */
                httpd_resp_send_408(req);
            }
            else {
                httpd_resp_send_500(req);
            }
            return ESP_FAIL;
        }


        /* avoids a buffer overflow parsing the string if the content is bigger than the buffer size */
        content[buffer_size - 1] = '\0';

        /* parse content to json */
        rgb_t rgb = {.num = (uint32_t)0};
        cJSON *json = cJSON_Parse(content);
        const cJSON *r = NULL;
        const cJSON *g = NULL;
        const cJSON *b = NULL;

        if(json == NULL){

            const char *error_ptr = cJSON_GetErrorPtr();
            ESP_LOGE(TAG, "cJSON parsing error: %s", error_ptr);

            /* bad request */
            httpd_resp_set_status(req, http_400_hdr);
            httpd_resp_send(req, NULL, 0);
            return ESP_FAIL;

        }

        r = cJSON_GetObjectItemCaseSensitive(json, "r");
        g = cJSON_GetObjectItemCaseSensitive(json, "g");
        b = cJSON_GetObjectItemCaseSensitive(json, "b");

        if(r != NULL && g != NULL && b != NULL && cJSON_IsNumber(r) && cJSON_IsNumber(g) && cJSON_IsNumber(b)){

            rgb.r = (uint8_t)r->valueint;
            rgb.g = (uint8_t)g->valueint;
            rgb.b = (uint8_t)b->valueint;

            /* free json object */
            cJSON_Delete(json);

            ret = ws2812_set_backlight_color(rgb);
            clock_notify_new_backlight_color(rgb);
            if(ret == ESP_OK){

                httpd_resp_set_status(req, http_200_hdr);
                httpd_resp_send(req, NULL, 0);
                return ESP_OK;
            }
            else{
                httpd_resp_send_500(req);
                return ESP_FAIL;
            }
        }
        else{

            /* free json object */
            cJSON_Delete(json);

            /* bad request */
            httpd_resp_set_status(req, http_400_hdr);
            httpd_resp_send(req, NULL, 0);
            return ESP_FAIL;
        }

    } /* if(strcmp(req->uri, "/backlights/") == 0) */

    return ESP_OK;
}

esp_err_t webapp_register_handlers(){

    esp_err_t ret;
    ret = http_app_set_handler_hook(HTTP_GET, &webapp_get_hander);

    if(ret != ESP_OK){
        return ret;
    }

    ret = http_app_set_handler_hook(HTTP_POST, &webapp_post_handler);
    if(ret != ESP_OK){
        http_app_set_handler_hook(HTTP_POST, NULL);
        return ret;
    }

    return ESP_OK;
}



<|start_filename|>nixie_clock_code/main/http_client.c<|end_filename|>
/*
Copyright (c) 2019 <NAME>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@file http_client.c
@author <NAME>

Contains wrappers around the esp http client for the mclk.org time API.

*/

#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"
#include "esp_log.h"
#include "esp_system.h"
#include "esp_http_client.h"
#include "cJSON.h"

#include "clock.h"
#include "http_client.h"



static const char TAG[] = "HTTP_CLIENT";
static const char *HTTP_CLIENT_TIME_API_URL = "https://api.mclk.org/time";
static const char *HTTP_CLIENT_TRANSITIONS_API_URL = "https://api.mclk.org/transitions";



static QueueHandle_t http_client_queue = NULL;

/* used to keep track of request bodies that eventually need to be freed */
static char *http_client_body_str = NULL;

/* used to keep track of response */
static char *http_client_response_str = NULL;

static esp_http_client_handle_t http_client_handle = NULL;




void http_client_process_data(esp_http_client_event_t *evt){
	if(evt->user_data == (void*)HTTP_CLIENT_TIME_API_URL){

		/* process json answer */
		if(http_client_response_str){
			cJSON *json = cJSON_Parse(http_client_response_str);
			clock_notify_time_api_response(json);
		}

	}
	else if(evt->user_data == (void*)HTTP_CLIENT_TRANSITIONS_API_URL){

		/* process json answer */
		if(http_client_response_str){
			cJSON *json = cJSON_Parse(http_client_response_str);
			clock_notify_transitions_api_response(json);
		}
	}
}




void http_client_cleanup(esp_http_client_handle_t client){

	if(http_client_body_str){
		free(http_client_body_str);
		http_client_body_str = NULL;
	}

	if(http_client_response_str){
		free(http_client_response_str);
		http_client_response_str = NULL;
	}

	esp_http_client_cleanup(client);
}

static esp_err_t _http_event_handler(esp_http_client_event_t *evt){


	switch(evt->event_id) {
			case HTTP_EVENT_ERROR:
				ESP_LOGI(TAG, "HTTP_EVENT_ERROR");
				break;
			case HTTP_EVENT_ON_CONNECTED:
				ESP_LOGI(TAG, "HTTP_EVENT_ON_CONNECTED");
				break;
			case HTTP_EVENT_HEADER_SENT:
				ESP_LOGI(TAG, "HTTP_EVENT_HEADER_SENT");
				break;
			case HTTP_EVENT_ON_HEADER:
				ESP_LOGD(TAG, "HTTP_EVENT_ON_HEADER, key=%s, value=%s", evt->header_key, evt->header_value);
				break;
			case HTTP_EVENT_ON_DATA:
				ESP_LOGI(TAG, "HTTP_EVENT_ON_DATA, len=%d", evt->data_len);
				if(evt->data_len > 0){
					if(http_client_response_str == NULL){
						/* allocate memory */
						int sz = esp_http_client_get_content_length(evt->client) + 1;
						http_client_response_str = (char*)malloc(sizeof(char) * sz);
						memset(http_client_response_str, '\0', sz);
					}
					/* copy data over */
					char* tmp = (char*)malloc(sizeof(char) * (evt->data_len + 1));
					memset(tmp, '\0', evt->data_len + 1);
					memcpy(tmp, evt->data, evt->data_len);
					strcat(http_client_response_str, tmp);
					free(tmp);
				}
				break;
			case HTTP_EVENT_ON_FINISH:
				ESP_LOGI(TAG, "HTTP_EVENT_ON_FINISH");
				http_client_process_data(evt);
				break;
			case HTTP_EVENT_DISCONNECTED:
				ESP_LOGI(TAG, "HTTP_EVENT_DISCONNECTED");
				break;
		}

    return ESP_OK;
}


static void http_client_api_time_process(char *timezone){

	esp_err_t err;
	ESP_LOGI(TAG, "tz: %s", timezone);

	esp_http_client_config_t config = {
					.url = HTTP_CLIENT_TIME_API_URL,
					.event_handler = _http_event_handler,
					.is_async = true,
					.timeout_ms = 10000,
					.user_data = (void*)HTTP_CLIENT_TIME_API_URL
	};
	http_client_handle = esp_http_client_init(&config);


	if(timezone != NULL){
		cJSON *body = NULL;
		cJSON *tz = NULL;
		char* body_str = NULL;

		/* generate the request body */
		body = cJSON_CreateObject();
		tz = cJSON_CreateString(timezone);
		cJSON_AddItemToObject(body, "timezone", tz);
		body_str = cJSON_Print(body);
		cJSON_Delete(body);

		/* save pointer for later cleanup */
		http_client_body_str = body_str;

		/* set body */
		esp_http_client_set_post_field(http_client_handle, body_str, strlen(body_str));
	}


	for(;;) {
		err = esp_http_client_perform(http_client_handle);
		if (err != ESP_ERR_HTTP_EAGAIN) {
			break;
		}
		vTaskDelay( pdMS_TO_TICKS(10) ); /* avoid watchdog trigger */
	}
	if (err == ESP_OK) {
		ESP_LOGI(TAG, "HTTPS Status = %d, content_length = %d",
				esp_http_client_get_status_code(http_client_handle),
				esp_http_client_get_content_length(http_client_handle));
	}
	else {
		ESP_LOGE(TAG, "Error perform http request %s", esp_err_to_name(err));
	}

	http_client_cleanup(http_client_handle);

}


static void http_client_api_transitions_process(void *pvParameter){
	timezone_t timezone = clock_get_config_timezone();
	time_t now = clock_get_current_time_utc();

	esp_http_client_config_t config = {
			.url = HTTP_CLIENT_TRANSITIONS_API_URL,
			.event_handler = _http_event_handler,
			.is_async = true,
			.timeout_ms = 10000,
			.user_data = (void*)HTTP_CLIENT_TRANSITIONS_API_URL
	};
	http_client_handle = esp_http_client_init(&config);


	if(timezone.name != NULL){
		cJSON *body = NULL;
		cJSON *tz = NULL;
		cJSON *from = NULL;
		cJSON *to = NULL;
		esp_err_t err;

		char* body_str = NULL;

		/* generate the request body */
		body = cJSON_CreateObject();

		/* timezone */
		tz = cJSON_CreateString(timezone.name);
		cJSON_AddItemToObject(body, "timezone", tz);

		/* from */
		time_t from_time = now - (60*60*24) ; /* -1 day back to avoid some weird edge cases by getting transitions strictly on now timestamp */
		from = cJSON_CreateNumber(from_time);
		cJSON_AddItemToObject(body, "from", from);

		/* to */
		time_t to_time = now + 60*60*24*365; /* +1 year */
		to = cJSON_CreateNumber(to_time);
		cJSON_AddItemToObject(body, "to", to);


		/* transform to json string then clean up cJSON object */
		body_str = cJSON_Print(body);
		cJSON_Delete(body);

		/* save pointer for later cleanup */
		http_client_body_str = body_str;

		/* set body */
		esp_http_client_set_post_field(http_client_handle, body_str, strlen(body_str));

		for(;;) {
			err = esp_http_client_perform(http_client_handle);
			if (err != ESP_ERR_HTTP_EAGAIN) {
				break;
			}
			vTaskDelay( pdMS_TO_TICKS(10) ); /* avoid watchdog trigger */
		}
		if (err == ESP_OK) {
			ESP_LOGI(TAG, "HTTPS Status = %d, content_length = %d",
					esp_http_client_get_status_code(http_client_handle),
					esp_http_client_get_content_length(http_client_handle));
		}
		else {
			ESP_LOGE(TAG, "Error perform http request %s", esp_err_to_name(err));
		}

		http_client_cleanup(http_client_handle);
	}
}


/**
 * @brief freeRTOS task that processes URL requests. 
 * 
 * Because this is processed in a queue, it ensures only one call at a time can be made
 */
static void http_client_task(void *pvParameter){

	clock_queue_message_t msg;

	for(;;) {
		if(xQueueReceive(http_client_queue, &msg, portMAX_DELAY)) {

			switch(msg.message){

				case CLOCK_MESSAGE_REQUEST_TIME_API:{
					char* tz = (char*)msg.param;
					http_client_api_time_process( tz );
					if(tz != NULL){
						free(tz);
					}
					}
					break;

				case CLOCK_MESSAGE_REQUEST_TRANSITIONS_API_CALL:
					http_client_api_transitions_process( NULL );
					break;

				default:
					break;
			}
		}	
	}
}


esp_err_t http_client_init(){
	http_client_queue = xQueueCreate(10, sizeof(clock_queue_message_t));
	xTaskCreatePinnedToCore(&http_client_task, "http_client_task", 16384, NULL, CLOCK_TASK_PRIORITY-1, NULL, 1);
	return ESP_OK;
}


void http_client_get_transitions(timezone_t timezone, time_t now){
	clock_queue_message_t msg;
	msg.message = CLOCK_MESSAGE_REQUEST_TRANSITIONS_API_CALL;
	msg.param = NULL;
	xQueueSend(http_client_queue, &msg, portMAX_DELAY);
}


void http_client_get_api_time(char* timezone){
	clock_queue_message_t msg;
	size_t sz = strlen(timezone) + 1;
	char* param = malloc(sizeof(char) * sz);
	memset(param, 0x00, sz);
	strcpy(param, timezone);
	msg.message = CLOCK_MESSAGE_REQUEST_TIME_API;
	msg.param = (void*)param;
	xQueueSend(http_client_queue, &msg, portMAX_DELAY);
}



<|start_filename|>nixie_clock_code/main/ws2812.c<|end_filename|>
/*
Copyright (c) 2019 <NAME>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@file ws2812.c
@author <NAME>
@brief Provides a WS2812 "neopixel" software driver

@see https://idyl.io
@see https://github.com/tonyp7/esp32-nixie-clock

This is based of FozzTexx's public domain code on WS2812
@see https://github.com/FozzTexx/ws2812-demo

*/

#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/queue.h>
#include <freertos/task.h>
#include <soc/rmt_struct.h>
#include <soc/dport_reg.h>
#include <driver/gpio.h>
#include <soc/gpio_sig_map.h>
#include <esp_intr_alloc.h>
#include <driver/rmt.h>
#include "esp_log.h"


#include "ws2812.h"

#define ETS_RMT_CTRL_INUM		18
#define ESP_RMT_CTRL_DISABLE	ESP_RMT_CTRL_DIABLE /* Typo in esp_intr.h */

#define DIVIDER		4 /* Above 4, timings start to deviate*/
#define DURATION	12.5 /* minimum time of a single RMT duration
				in nanoseconds based on clock */

#define PULSE_T0H						(  350 / (DURATION * DIVIDER));
#define PULSE_T1H						(  900 / (DURATION * DIVIDER));
#define PULSE_T0L						(  900 / (DURATION * DIVIDER));
#define PULSE_T1L						(  350 / (DURATION * DIVIDER));
#define PULSE_TRS						(50000 / (DURATION * DIVIDER));

#define MAX_PULSES						32

#define WS2812_RMT_CHANNEL				0

#define WS2812_QUEUE_SIZE				3

typedef union {
  struct {
    uint32_t duration0:15;
    uint32_t level0:1;
    uint32_t duration1:15;
    uint32_t level1:1;
  };
  uint32_t val;
} rmt_pulse_pair_t;

static uint8_t *ws2812_buffer = NULL;
static unsigned int ws2812_pos, ws2812_len, ws2812_half;
static xSemaphoreHandle ws2812_sem = NULL;
static intr_handle_t rmt_intr_handle = NULL;
static rmt_pulse_pair_t ws2812_bits[2];
static SemaphoreHandle_t ws2812_mutex = NULL;

static const char TAG[] = "ws2812";

QueueHandle_t ws2812_queue = NULL; 

/* the actual array holding the values of each backlight pixel */
static rgb_t *ws2812_pixels = NULL;

float clamp(float d, float min, float max) {
	const float t = d < min ? min : d;
	return t > max ? max : t;
}

/**
 * @brief RTOS task processing backlight color changes
 */
static void ws2812_task(void *pvParameters)
{
	ws2812_message_t msg;
	const uint8_t pixel_count = WS2812_STRIP_SIZE;
	rgb_t *pixels = malloc(sizeof(rgb_t) * pixel_count);
	

	for(;;) {
		if(xQueueReceive(ws2812_queue, &msg, portMAX_DELAY)) {

			ESP_LOGI(TAG, "Received R:%d G:%d B:%d", msg.rgb.r, msg.rgb.g, msg.rgb.b);

			for (uint8_t i = 0; i < pixel_count; i++) {
				pixels[i] = msg.rgb;
			}

			ws2812_set_colors(pixel_count, pixels);
		}
	}

}



float impulse( float k, float x ){
    float h = k*x;
    return h*expf(1.0f-h);
}

/**
 * @brief exponential step interp. Typical k: 10.0f. Typical n: 1.0f
 */
float exp_step(float x, float k, float n){
	return expf( -k * powf(x, n) );
}



esp_err_t ws2812_set_backlight_color(rgb_t c){

	ws2812_message_t msg;
	msg.rgb = c;

	BaseType_t ret = xQueueSend( ws2812_queue, &msg, pdMS_TO_TICKS(1000) );

	if(ret == pdTRUE){
		return ESP_OK;
	}
	else{
		return ESP_FAIL; /* ret could be errQUEUE_FULL */
	}

}


/**
 * @brief smoothstep performs smooth Hermite interpolation between 0 and 1 when edge0 < x < edge1.
 * This is useful in cases where a threshold function with a smooth transition is desired.
 */
float smoothstep(const float edge0, const float edge1, const float x){
	const float t = clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f);
	return t * t * (3.0f - 2.0f * t);
}


/**
 * @brief initialize the RMT channel to be suitable to control a WS2812 LED
 */
void ws2812_init_rmt_channel(int rmt_channel)
{
	RMT.apb_conf.fifo_mask = 1;  /* enable memory access, instead of FIFO mode. */
	RMT.apb_conf.mem_tx_wrap_en = 1; /* wrap around when hitting end of buffer */
	RMT.conf_ch[rmt_channel].conf0.div_cnt = DIVIDER;
	RMT.conf_ch[rmt_channel].conf0.mem_size = 1;
	RMT.conf_ch[rmt_channel].conf0.carrier_en = 0;
	RMT.conf_ch[rmt_channel].conf0.carrier_out_lv = 1;
	RMT.conf_ch[rmt_channel].conf0.mem_pd = 0;

	RMT.conf_ch[rmt_channel].conf1.rx_en = 0;
	RMT.conf_ch[rmt_channel].conf1.mem_owner = 0;
	RMT.conf_ch[rmt_channel].conf1.tx_conti_mode = 0;    /* loop back mode */
	RMT.conf_ch[rmt_channel].conf1.ref_always_on = 1;    /* use APB 80Mhz clock */
	RMT.conf_ch[rmt_channel].conf1.idle_out_en = 1;
	RMT.conf_ch[rmt_channel].conf1.idle_out_lv = 0;

	return;
}

void ws2812_copy()
{
  unsigned int i, j, offset, len, bit;


  offset = ws2812_half * MAX_PULSES;
  ws2812_half = !ws2812_half;

  len = ws2812_len - ws2812_pos;
  if (len > (MAX_PULSES / 8))
    len = (MAX_PULSES / 8);

  if (!len) {
    for (i = 0; i < MAX_PULSES; i++)
      RMTMEM.chan[WS2812_RMT_CHANNEL].data32[i + offset].val = 0;
    return;
  }

  for (i = 0; i < len; i++) {
    bit = ws2812_buffer[i + ws2812_pos];
    for (j = 0; j < 8; j++, bit <<= 1) {
      RMTMEM.chan[WS2812_RMT_CHANNEL].data32[j + i * 8 + offset].val =
	ws2812_bits[(bit >> 7) & 0x01].val;
    }
    if (i + ws2812_pos == ws2812_len - 1)
      RMTMEM.chan[WS2812_RMT_CHANNEL].data32[7 + i * 8 + offset].duration1 = PULSE_TRS;
  }

  for (i *= 8; i < MAX_PULSES; i++)
    RMTMEM.chan[WS2812_RMT_CHANNEL].data32[i + offset].val = 0;

  ws2812_pos += len;
  return;
}

void ws2812_handle_interrupt(void *arg){
	BaseType_t task_awoken = 0;

  if (RMT.int_st.ch0_tx_thr_event) {
    ws2812_copy();
    RMT.int_clr.ch0_tx_thr_event = 1;
  }
  else if (RMT.int_st.ch0_tx_end && ws2812_sem) {
    xSemaphoreGiveFromISR(ws2812_sem, &task_awoken);
    RMT.int_clr.ch0_tx_end = 1;
  }

  return;
}

esp_err_t ws2812_init(){

	esp_err_t ret;

	/* mutex for the pixels memory */
	ws2812_mutex = xSemaphoreCreateMutex();

	DPORT_SET_PERI_REG_MASK(DPORT_PERIP_CLK_EN_REG, DPORT_RMT_CLK_EN);
	DPORT_CLEAR_PERI_REG_MASK(DPORT_PERIP_RST_EN_REG, DPORT_RMT_RST);

	ret = rmt_set_pin((rmt_channel_t)WS2812_RMT_CHANNEL, RMT_MODE_TX, (gpio_num_t)WS2818_DATA_GPIO);
	if(ret != ESP_OK) return ret;

	ws2812_init_rmt_channel(WS2812_RMT_CHANNEL);

	RMT.tx_lim_ch[WS2812_RMT_CHANNEL].limit = MAX_PULSES;
	RMT.int_ena.ch0_tx_thr_event = 1;
	RMT.int_ena.ch0_tx_end = 1;

	ws2812_bits[0].level0 = 1;
	ws2812_bits[0].level1 = 0;
	ws2812_bits[0].duration0 = PULSE_T0H;
	ws2812_bits[0].duration1 = PULSE_T0L;
	ws2812_bits[1].level0 = 1;
	ws2812_bits[1].level1 = 0;
	ws2812_bits[1].duration0 = PULSE_T1H;
	ws2812_bits[1].duration1 = PULSE_T1L;

	/* allocate memory for the pixels */
	ws2812_pixels = (rgb_t*)malloc(sizeof(rgb_t) * WS2812_STRIP_SIZE);


	ret = esp_intr_alloc(ETS_RMT_INTR_SOURCE, 0, ws2812_handle_interrupt, NULL, &rmt_intr_handle);

	/* create the queue */
	ws2812_queue = xQueueCreate(WS2812_QUEUE_SIZE, sizeof(ws2812_message_t));
	if(ws2812_queue == NULL){
		return ESP_ERR_NO_MEM;
	}

	if(ret == ESP_OK){
		/* spawn the task that will handle the LED backlights */
		xTaskCreatePinnedToCore(ws2812_task, "ws2812_task", 4096, NULL, 10, NULL, 0);
	}

	return ret;

}

void ws2812_set_colors(unsigned int length, rgb_t *array){
	unsigned int i;


	ws2812_len = (length * 3) * sizeof(uint8_t);
	ws2812_buffer = malloc(ws2812_len);

	for (i = 0; i < length; i++) {
		ws2812_buffer[i * 3 + 0] = array[i].g;
		ws2812_buffer[i * 3 + 1] = array[i].r;
		ws2812_buffer[i * 3 + 2] = array[i].b;
	}

	ws2812_pos = 0;
	ws2812_half = 0;

	ws2812_copy();

	if (ws2812_pos < ws2812_len)
	ws2812_copy();

	ws2812_sem = xSemaphoreCreateBinary();

	RMT.conf_ch[WS2812_RMT_CHANNEL].conf1.mem_rd_rst = 1;
	RMT.conf_ch[WS2812_RMT_CHANNEL].conf1.tx_start = 1;



	xSemaphoreTake(ws2812_sem, portMAX_DELAY);
	vSemaphoreDelete(ws2812_sem);
	ws2812_sem = NULL;

	free(ws2812_buffer);

	return;
}


<|start_filename|>nixie_clock_code/main/include/ws2812.h<|end_filename|>
/**
Copyright (c) 2020 <NAME>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@file ws2812.h
@author <NAME>
@brief Provides a WS2812 "neopixel" software driver

@see https://idyl.io
@see https://github.com/tonyp7/esp32-nixie-clock

This is based of FozzTexx's public domain code on WS2812
@see https://github.com/FozzTexx/ws2812-demo

*/

#ifndef MAIN_WS2812_H_
#define MAIN_WS2812_H_

#include <stdint.h>
#include <esp_err.h>


#ifdef __cplusplus
extern "C" {
#endif

#define WS2818_DATA_GPIO		23
#define WS2812_STRIP_SIZE		6


/**
 * structure holding a RGB 24 bit colors information in a 32 bit int
 */
typedef union {
  struct __attribute__ ((packed)) {
    uint8_t r, g, b;
  };
  uint32_t num;
} rgb_t;


/**
 * @brief defines the type of message to be sent to the queue.
 * The WS2812 queue can only process color information for simplicity
 */
typedef struct ws2812_message_t {
	rgb_t rgb;
} ws2812_message_t;

esp_err_t ws2812_init();



/**
 * @brief Helper to generate a rgb_t based on r, g, b parameters
 */
inline rgb_t ws2812_create_rgb(uint8_t r, uint8_t g, uint8_t b){
	rgb_t v;

	v.r = r;
	v.g = g;
	v.b = b;
	return v;
}

/**
 * @brief Sends the message to the ws2812 queue to process a color change
 * @return ESP_OK if success, ESP_FAIL if the queue was full or any other error
 */
esp_err_t ws2812_set_backlight_color(rgb_t c);

/**
 * @brief helper for ws2812_set_backlight_color
 * @see ws2812_set_backlight_color
 */
inline esp_err_t ws2812_set_backlight_color_rgb(uint8_t r, uint8_t g, uint8_t b){
	return ws2812_set_backlight_color(ws2812_create_rgb(r, g, b));
}


void ws2812_set_colors(unsigned int length, rgb_t *array);


#ifdef __cplusplus
}
#endif

#endif /* MAIN_WS2812_H_ */


<|start_filename|>nixie_clock_code/main/clock.c<|end_filename|>
/*
Copyright (c) 2019 <NAME>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@file clock.c
@author <NAME>
@brief Describes the main task driving the rest of the nixie clock

@see https://idyl.io
@see https://github.com/tonyp7/esp32-nixie-clock

*/

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#include <time.h>
#include <math.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "esp_log.h"
#include "esp_system.h"
#include "esp_err.h"
#include "nvs.h"
#include "nvs_flash.h"
#include "driver/gpio.h"
#include "lwip/err.h"
#include "lwip/apps/sntp.h"
#include "esp_http_client.h"
#include "cJSON.h"


#include "ds3231.h"
#include "i2c.h"
#include "http_client.h"
#include "display.h"
#include "ws2812.h"
#include "list.h"
#include "clock.h"




/** @brief tag used for ESP serial console messages */
static const char TAG[] = "clock";

/** @brief flash memory namespace for the clock */
const char clock_nvs_namespace[] = "clock";

/** @brief storage for timestamp transitions */
static transition_t clock_transitions[CLOCK_MAX_TRANSITIONS];

/** @brief mutex to prevent several read/write to nvs at the same time */
static SemaphoreHandle_t clock_nvs_mutex = NULL;

time_t timestamp_utc;
time_t timestamp_local;
time_t timestamp_transitions_check = 0;
struct tm *clock_time_tm_ptr = NULL; /*used for localtime function which returns a static pointer */
struct tm clock_time_tm;

//static timezone_t clock_timezone;
static clock_config_t clock_config;

static QueueHandle_t clock_queue = NULL;
static bool time_set = false;

/** @brief List holding the sleep/wake events */
static list_t* clock_list_sleepevents = NULL;


/** @brief Save task handle to notify it of saving */
static TaskHandle_t clock_task_save_nvs = NULL;


time_t clock_get_current_time_utc(){
	return timestamp_utc;
}


/**
 * @brief task the will save the config in NVS when it is notified and it is safe to do so
 */
static void clock_save_config_task(void *pvParameter){

	ESP_LOGI(TAG, "clock_save_config_task started");
	for(;;){

		if(ulTaskNotifyTake(pdTRUE, portMAX_DELAY)){

			/* this prevent saving the config more than once per 10s
			 * While this means some data could be lost, in a typical user use it is a lot better
			 * to do this rather than saving EVERYTIME there's small change, most notably
			 * with the RGB change which can happen extremely quickly
			 */
			vTaskDelay(  pdMS_TO_TICKS(10000) );

			clock_config_t cfg = clock_get_config();
			if(clock_nvs_lock( portMAX_DELAY )){
				clock_save_config(&cfg);
				clock_nvs_unlock();
			}
		}
	}

	vTaskDelete( NULL );
}

static int comp_sleep_event(sleep_event_t *a, sleep_event_t *b){

	if(a && b){

		if(a->timestamp == b->timestamp) return 0;
		else if(a->timestamp < b->timestamp) return -1;
		else return 1;
	}
	else{
		return a - b;
	}
}

static void clock_build_new_sleepmodes(sleepmodes_t sleepmodes){

    time_t time_now_utc = clock_get_current_time_utc();
    struct tm tm_now;
    int offset = clock_config.timezone.offset;

    /* initialize the event */
    time_t time_now_local = time_now_utc + offset; /* apply local timezone then build the tm now object */
    gmtime_r(&time_now_local, &tm_now);
	/* gmtime_s(&tm_now, &time_now_local); on Windows */

	/* clear the previous events */
	list_clear(clock_list_sleepevents);

    if (sleepmodes.enable_sleepmode) {

        /* get current day of the week at midnight */
        time_t today_at_midight = time_now_local - (time_t)tm_now.tm_sec - ((time_t)tm_now.tm_min * 60) - ((time_t)tm_now.tm_hour * 3600);

        /* process all the sleepmodes */
        for (int i = 0; i < CLOCK_MAX_SLEEPMODES; i++) {

            /* to save some typing and code clarity */
            sleepmode_t sm = sleepmodes.sleepmode[i];

            /* skip if this one is disabled */
            if (!sm.enabled) continue;

            /* transform a sleepmode into UTC timestamp and add it to the list */

            int weekday = 1; /* the mask starts on mondays which is 1 on a struct tm */
            uint8_t days = sm.days;
            for (int j = 0; j < 7; j++) {
               
                if (days & (uint8_t)0x01) {
                    /* when is the next weekday matching ?
                        tm_wday = 1 (Monday), weekday = 1 => offset is 0
                        tm_wday = 1 (Monday), weekday = 2 => offset is 1 (tomorrow)
                        tm_wday = 3 (Wednesday), weekday = 1 (Next Monday) => offset is  (7-3 + 1) = 5 days
                    */
                    time_t day_offset;
                    if (tm_now.tm_wday <= weekday) {
                        day_offset = ((time_t)weekday - tm_now.tm_wday) * (time_t)86400; /* 24h * 3600 */
                    }
                    else {
                        day_offset = ((time_t)7 - tm_now.tm_wday + weekday) * (time_t)86400; /* 24h * 3600 */
                    }
 
                    /* build both events
                    if the TO hour is less than from, we consider it's for the next day and so add further offset */
                    sleep_event_t from = { .timestamp = today_at_midight + day_offset + sm.from, .action = SLEEP_ACTION_SLEEP };
                    sleep_event_t to = { .timestamp = today_at_midight + day_offset +  ((sm.to < sm.from)?(time_t)86400:0) + sm.to, .action = SLEEP_ACTION_WAKE };

					list_add_ordered(clock_list_sleepevents, from, &comp_sleep_event);
					list_add_ordered(clock_list_sleepevents, to, &comp_sleep_event);

					/* if the day is today we also build the sleep event for next week */
					if(day_offset == 0){
						from.timestamp += (time_t)86400*7;
						to.timestamp += (time_t)86400*7;
						list_add_ordered(clock_list_sleepevents, from, &comp_sleep_event);
						list_add_ordered(clock_list_sleepevents, to, &comp_sleep_event);
					}

					/* debug -- can be deleted in production code */
					struct tm debug;
					static char strftime_buf[64];
					localtime_r(&from.timestamp, &debug);
					strftime(strftime_buf, sizeof(strftime_buf), "%c", &debug);
					ESP_LOGI(TAG, "CLOCK WILL SLEEP AT: %s", strftime_buf);
					localtime_r(&to.timestamp, &debug);
					strftime(strftime_buf, sizeof(strftime_buf), "%c", &debug);
					ESP_LOGI(TAG, "CLOCK WILL WAKE AT: %s", strftime_buf);


                }

                days = days >> 1;
                weekday++; if (weekday == 7) weekday = 0; /* wrap around for sunday */

            } /* for (int j = 0; j < 7; j++) */
        } /* for (int i = 0; i < CLOCK_MAX_SLEEPMODES; i++) */
    } /* if (sleepmodes.enable_sleepmode) */
}


timezone_t clock_get_config_timezone(){
	return clock_config.timezone;
}

display_config_t clock_get_config_display(){
	return clock_config.display;
}

void clock_notify_sta_got_ip(void *pvArgument){
	if(clock_queue){
		clock_queue_message_t msg;
		msg.message = CLOCK_MESSAGE_STA_GOT_IP;
		xQueueSend(clock_queue, &msg, portMAX_DELAY);
	}
}

void clock_notify_new_backlight_color(rgb_t rgb){
	if(clock_queue){
		clock_queue_message_t msg;
		msg.message = CLOCK_MESSAGE_BACKLIGHTS_CONFIG;
		msg.param = (void*)rgb.num;
		xQueueSend(clock_queue, &msg, portMAX_DELAY);
	}
}

void clock_notify_sta_disconnected(){
	if(clock_queue){
		clock_queue_message_t msg;
		msg.message = CLOCK_MESSAGE_STA_DISCONNECTED;
		xQueueSend(clock_queue, &msg, portMAX_DELAY);
	}
}

void clock_notify_time_api_response(cJSON *json){
	if(clock_queue){
		clock_queue_message_t msg;
		msg.message = CLOCK_MESSAGE_RECEIVE_TIME_API;
		msg.param = (void*)json;
		xQueueSend(clock_queue, &msg, portMAX_DELAY);
	}
}


void clock_notify_transitions_api_response(cJSON *json){
	if(clock_queue){
		clock_queue_message_t msg;
		msg.message = CLOCK_MESSAGE_RECEIVE_TRANSITIONS_API;
		msg.param = (void*)json;
		xQueueSend(clock_queue, &msg, portMAX_DELAY);
	}
}

void clock_notify_new_sleepmodes(sleepmodes_t sleepmodes){

	clock_queue_message_t msg;
	sleepmodes_t* sm = malloc(sizeof(sleepmodes_t));
	memset(sm, 0x00, sizeof(sleepmodes_t));
	*sm = sleepmodes;
	msg.message = CLOCK_MESSAGE_SLEEPMODE_CONFIG;
	msg.param = (void*)sm;

	xQueueSend(clock_queue, &msg, portMAX_DELAY);
}


void clock_notify_new_timezone(char* timezone){

	clock_queue_message_t msg;
	timezone_t* tz = malloc(sizeof(timezone_t));
	memset(tz, 0x00, sizeof(timezone_t));
	strncpy(  tz->name, timezone, sizeof(tz->name) - 1 );
	msg.message = CLOCK_MESSAGE_TIMEZONE;
	msg.param = (void*)tz;

	xQueueSend(clock_queue, &msg, portMAX_DELAY);
}


static void IRAM_ATTR gpio_isr_handler(void* arg){
    //uint32_t gpio_num = (uint32_t) arg;

	clock_queue_message_t msg;
	msg.message = CLOCK_MESSAGE_TICK;
	msg.param = NULL;
    xQueueSendFromISR(clock_queue, &msg, NULL);
	return;
}


void clock_transitions_shift_left(){

	int i = CLOCK_MAX_TRANSITIONS - 1;
	while(i > 1){
		clock_transitions[i - 1] = clock_transitions[i];
		i--;
	}
}

void clock_tick(){

	timezone_t* clock_timezone = &(clock_config.timezone);

	/* +1 second */
	timestamp_utc++;

	/* check for transitions */
	int new_offset = clock_timezone->offset;
	while(clock_transitions[0].timestamp && (timestamp_utc >= clock_transitions[0].timestamp)){
		/* save found offset */
		new_offset = clock_transitions[0].offset;

		/* shift transitions table left */
		clock_transitions_shift_left();
	}

	/* found a new timezone offset ? */
	if(new_offset != clock_timezone->offset){
		ESP_LOGI(TAG, "Saving new offset: %d vs old: %d", new_offset, clock_timezone->offset);
		clock_timezone->offset = new_offset;

		/* save new conf in memory */
		xTaskNotifyGive( clock_task_save_nvs );

		/* if a transition was processed it's a good idea to force a refresh soon */
		timestamp_transitions_check = timestamp_utc + (time_t)(60*60*24*15);
	}


	/* is it time to check for transitions? */
	if(timestamp_transitions_check && (timestamp_utc >= timestamp_transitions_check)){
		clock_queue_message_t msg;
		msg.message = CLOCK_MESSAGE_REQUEST_TRANSITIONS_API_CALL;
		msg.param = NULL;
		xQueueSend(clock_queue, &msg, portMAX_DELAY);
		timestamp_transitions_check = timestamp_utc + (time_t)(60*60*24*15); /* do another check in 15 days */
	}

	timestamp_local = timestamp_utc + clock_timezone->offset;

	/* check for sleep / wake event */
	bool process_sleep_event = false;
	sleep_event_t sleep_event = {
		.action = SLEEP_ACTION_UNKNOWN,
		.timestamp = 0x7fffffff
	};
	while(  (list_peek(clock_list_sleepevents, &sleep_event) == 0) &&  ( timestamp_local >= sleep_event.timestamp )  ){
		list_shift(clock_list_sleepevents, NULL);
		process_sleep_event = true;
	}
	if(process_sleep_event){

		clock_queue_message_t msg;
		msg.message = CLOCK_MESSAGE_SLEEP_EVENT;
		msg.param = (void*)sleep_event.action;
		xQueueSend(clock_queue, &msg, portMAX_DELAY);

		/* if a sleepevent was processed AND the count of the list is now 0, it means its time to refresh the list and
		 * recalculate new sleep events 
		 * Processing an item (eg sleep_event.action != SLEEP_ACTION_UNKNOWN) is important because the list can be empty
		 * simply because user has not set any sleepmode.
		 * */
		if(clock_list_sleepevents->count == 0){
			clock_build_new_sleepmodes(clock_config.sleepmodes);
		}
	}




	clock_time_tm_ptr = localtime(&timestamp_local);
}



bool clock_realign(time_t new_t){

	double d = fabs(difftime(timestamp_utc, new_t));
	if(d > CLOCK_MAX_ACCEPTABLE_TIME_DRIFT){
		ESP_LOGI(TAG, "Re-alignment of the clock");
		timestamp_utc = new_t;

		/* set time on RTC */
		ESP_ERROR_CHECK(ds3231_set_time(localtime(&timestamp_utc)));

		return true;
	}

	return false;

}


esp_err_t clock_register_sqw_interrupt(){
	/* setup GPIO 4 as INTERRUPT on RISING EGDE */
	gpio_config_t io_conf;
	io_conf.intr_type = GPIO_PIN_INTR_POSEDGE;
	/* GPIO 4 bit mask */
	io_conf.pin_bit_mask = (1ULL<<GPIO_INPUT_IO_4);
	/* Input */
	io_conf.mode = GPIO_MODE_INPUT;
	/* No pull up since there is already a 10k external pull up */
	io_conf.pull_up_en = GPIO_PULLUP_DISABLE;
	//io_conf.pull_down_en = GPIO_PULLDOWN_ENABLE;
	gpio_config(&io_conf);


	/* install ISR service */
	gpio_install_isr_service(ESP_INTR_FLAG_LEVEL2);
	return gpio_isr_handler_add(GPIO_INPUT_IO_4, gpio_isr_handler, (void*) GPIO_INPUT_IO_4);
}


esp_err_t clock_get_nvs_config(clock_config_t *conf){

	nvs_handle handle;
	esp_err_t esp_err;

	esp_err = nvs_open(clock_nvs_namespace, NVS_READONLY, &handle);
	if(esp_err == ESP_OK){
		ESP_LOGI(TAG, "Getting config from NVS memory");

		size_t sz = sizeof(clock_config_t);
		esp_err = nvs_get_blob(handle, "conf", conf, &sz);
		if(esp_err != ESP_OK){
			return esp_err;
		}

		nvs_close(handle);
	}
	else if(esp_err == ESP_ERR_NVS_NOT_FOUND){
		/* first time launching the program! */
		ESP_LOGI(TAG, "Cannot find clock nvs namespace. Attempt to create it");
		nvs_close(handle);

		esp_err = nvs_open(clock_nvs_namespace, NVS_READWRITE, &handle);
		if(esp_err == ESP_OK){

			size_t sz = sizeof(clock_config_t);
			ESP_ERROR_CHECK(nvs_set_blob(handle, "conf", conf, sz));
			ESP_ERROR_CHECK(nvs_commit(handle));
			nvs_close(handle);
		}

	}

	return esp_err;
}

esp_err_t clock_get_nvs_timezone(timezone_t *tz){
	nvs_handle handle;
	esp_err_t esp_err;

	esp_err = nvs_open(clock_nvs_namespace, NVS_READONLY, &handle);
	if(esp_err == ESP_OK){
		ESP_LOGI(TAG, "Getting timezone from NVS memory");

		/* timezone name */
		size_t sz = sizeof(timezone_t);
		esp_err = nvs_get_blob(handle, "tz", tz, &sz);
		if(esp_err != ESP_OK){
			return esp_err;
		}

		nvs_close(handle);
	}
	else if(esp_err == ESP_ERR_NVS_NOT_FOUND){
		/* first time launching the program! */
		ESP_LOGI(TAG, "Cannot find clock nvs namespace. Attempt to create it");
		nvs_close(handle);

		esp_err = nvs_open(clock_nvs_namespace, NVS_READWRITE, &handle);
		if(esp_err == ESP_OK){

			size_t sz = sizeof(timezone_t);
			ESP_ERROR_CHECK(nvs_set_blob(handle, "tz", tz, sz));
			ESP_ERROR_CHECK(nvs_commit(handle));
			nvs_close(handle);
		}

	}

	return esp_err;

}


clock_config_t clock_get_config(){
	clock_config_t conf = clock_config;
	return conf;
}

bool clock_nvs_lock(TickType_t xTicksToWait){
	if(clock_nvs_mutex){
		if( xSemaphoreTake( clock_nvs_mutex, xTicksToWait ) == pdTRUE ) {
			return true;
		}
		else{
			return false;
		}
	}
	else{
		return false;
	}

}
void clock_nvs_unlock(){
	xSemaphoreGive( clock_nvs_mutex );
}


esp_err_t clock_save_config(clock_config_t *conf){
	nvs_handle handle;
	esp_err_t esp_err;


	esp_err = nvs_open(clock_nvs_namespace, NVS_READWRITE, &handle);
	if(esp_err == ESP_OK){

		/* set timezone */
		size_t sz = sizeof(clock_config_t);
		esp_err = nvs_set_blob(handle, "conf", conf, sz);
		if(esp_err != ESP_OK){
			ESP_LOGE(TAG, "clock_save_config nvs_set_blob failed with error: %s", esp_err_to_name(esp_err) );
			return esp_err;
		}

		esp_err = nvs_commit(handle);
		if (esp_err != ESP_OK) return esp_err;

		nvs_close(handle);

	}
	else{
		ESP_LOGE(TAG, "Could not get a handle to NVS as NVS_READWRITE");
	}

	return esp_err;
}

esp_err_t clock_save_timezone(timezone_t *tz){
	nvs_handle handle;
	esp_err_t esp_err;


	esp_err = nvs_open(clock_nvs_namespace, NVS_READWRITE, &handle);
	if(esp_err == ESP_OK){

		/* set timezone */
		size_t sz = sizeof(timezone_t);
		esp_err = nvs_set_blob(handle, "tz", tz, sz);
		if(esp_err != ESP_OK){
			ESP_LOGE(TAG, "nvs_set_blob failed with error: %d", esp_err);
			return esp_err;
		}

		esp_err = nvs_commit(handle);
		if (esp_err != ESP_OK) return esp_err;

		nvs_close(handle);

	}
	else{
		ESP_LOGE(TAG, "Could not get a handle to NVS as NVS_READWRITE");
	}

	return esp_err;
}

/**
 * @brief this is the main RTOS task that controls everything in the clock
 */
void clock_task(void *pvParameter){


	

	/* debug, should be cut in prod code */
	char strftime_buf[64];

	/* memory init */
	memset(&clock_transitions, 0x00, sizeof(CLOCK_MAX_TRANSITIONS) * CLOCK_MAX_TRANSITIONS);
	clock_nvs_mutex = xSemaphoreCreateMutex();

	/* create the task that is used to save config in memory */
	xTaskCreate( &clock_save_config_task, "task_save_cfg", 4096, NULL, tskIDLE_PRIORITY+1, &clock_task_save_nvs );

	/* register clock queue */
	clock_queue = xQueueCreate(10, sizeof(clock_queue_message_t));

	/* initialized I2C */
	ESP_ERROR_CHECK(i2c_master_init());

	/* enabled 1Hz square wave */
	ESP_ERROR_CHECK(ds3231_enable_square_wave());

	/* HTTP client is needed for the clock task */
	ESP_ERROR_CHECK(http_client_init());

	/* get RTC time */
	memset(&clock_time_tm, 0x00, sizeof(struct tm));
	ESP_ERROR_CHECK(ds3231_get_time(&clock_time_tm));
	strftime(strftime_buf, sizeof(strftime_buf), "%c", &clock_time_tm);
	ESP_LOGI(TAG, "The current RTC time is: %s", strftime_buf);
	if(clock_time_tm.tm_year < 71){ /* 1971 */
		/* time is not available: first run or battery of the RTC was dead */
		timestamp_utc = 1;
	}
	else{
		/* otherwise: we are good to go! */
		timestamp_utc = mktime(&clock_time_tm);
		time_set = true;
	}
	clock_time_tm_ptr = localtime(&timestamp_utc);

	/* initialize configuration */
	memset(&clock_config, 0x00, sizeof(clock_config));
	clock_config.timezone.offset = 0;
	strcpy(clock_config.timezone.name, "UTC");
	clock_config.sleepmodes.enable_sleepmode = false;
	ESP_ERROR_CHECK(clock_get_nvs_config(&clock_config));

	/* generate the list of sleep events */
	clock_list_sleepevents = list_create();
	clock_notify_new_sleepmodes(clock_config.sleepmodes);

	/* initialize the display */
	display_set_config(  &(clock_config.display)  );
	display_write_time(NULL); /* write 00:00:00 on display */
	display_turn_on();
	ws2812_set_backlight_color(clock_config.display.led_color);

	/* register interrupt on the 1Hz sqw signal coming from the DS3231 */
	ESP_ERROR_CHECK(clock_register_sqw_interrupt());

	clock_queue_message_t msg;

	for(;;) {
		if(xQueueReceive(clock_queue, &msg, pdMS_TO_TICKS(11001))) { /* portMAX_DELAY */

			switch(msg.message){
				case CLOCK_MESSAGE_STA_GOT_IP:
					ESP_LOGI(TAG, "CLOCK_MESSAGE_STA_GOT_IP");
					http_client_get_api_time(clock_config.timezone.name);
					break;
				case CLOCK_MESSAGE_TIMEZONE:
					ESP_LOGI(TAG, "CLOCK_MESSAGE_TIMEZONE");
					timezone_t* tz = (timezone_t*)msg.param;
					http_client_get_api_time(tz->name);
					free(tz);
					break;
				case CLOCK_MESSAGE_TICK:
					//ESP_LOGI(TAG, "CLOCK_MESSAGE_TICK");
					if(time_set){
						clock_tick();
						display_write_time(clock_time_tm_ptr);
						strftime(strftime_buf, sizeof(strftime_buf), "%c", clock_time_tm_ptr);
						ESP_LOGI(TAG, "TICK! date/time is: %s", strftime_buf);
					}
					break;
				case CLOCK_MESSAGE_REQUEST_TRANSITIONS_API_CALL:
					http_client_get_transitions(clock_config.timezone, timestamp_utc);
					break;
				case CLOCK_MESSAGE_RECEIVE_TRANSITIONS_API:{


					cJSON *json = (cJSON*)msg.param;

					if(json != NULL){
						char *json_str = cJSON_Print(json);
						ESP_LOGI(TAG, "%s", json_str);

						cJSON *transition = NULL;
						cJSON *transitions = cJSON_GetObjectItemCaseSensitive(json, "transitions");
						if(transitions){

							int i=0;
							cJSON_ArrayForEach(transition, transitions){
								if(transition){
									cJSON *transitionTimestamp = cJSON_GetObjectItemCaseSensitive(transition, "transitionTimestamp");
									cJSON *toOffset = cJSON_GetObjectItemCaseSensitive(transition, "toOffset");

									if (transitionTimestamp && toOffset && cJSON_IsNumber(transitionTimestamp) && cJSON_IsNumber(toOffset)){

										/* overflow protection */
										if(!(i >= CLOCK_MAX_TRANSITIONS)){
											/* add transitions */
											clock_transitions[i].offset = toOffset->valueint;
											clock_transitions[i].timestamp = (time_t)transitionTimestamp->valuedouble;
											i++;
										}
									}
								}
							}

							if(i==0){
								/* no transitions were processed. Run another check in a few days */
								timestamp_transitions_check = timestamp_utc + (time_t)(60*60*24*15);
							}
						}

						free(json_str);
						cJSON_Delete(json);

					}
					}
					break;
				case CLOCK_MESSAGE_RECEIVE_TIME_API:
					;cJSON *json = (cJSON*)msg.param;
					bool updateNVS = false;
					if(json){

						/* log response for debug */
						char *json_str = cJSON_Print(json);
						ESP_LOGI(TAG, "%s", json_str);

						/* extract time from json, and set it if necessary */
						cJSON *timestamp = cJSON_GetObjectItemCaseSensitive(json, "timestamp");
						if(cJSON_IsNumber(timestamp)){
							time_t t = timestamp->valueint;
							clock_realign(t);
							time_set = true;
						}

						/* check timezone, save in memory if it's different than what was saved previously */
						cJSON *timezone = cJSON_GetObjectItemCaseSensitive(json, "timezone");
						if(cJSON_IsObject(timezone)){
							cJSON *timezoneName = cJSON_GetObjectItemCaseSensitive(timezone, "name");
							if(cJSON_IsString(timezoneName) && strcmp(clock_config.timezone.name, timezoneName->valuestring) != 0){
								/* realign clock_timezone */
								updateNVS = true;
								strcpy(clock_config.timezone.name, timezoneName->valuestring);
								ESP_LOGI(TAG, "Timezone set to: %s", clock_config.timezone.name);
							}

							cJSON *offset = cJSON_GetObjectItemCaseSensitive(timezone, "offset");
							if(cJSON_IsNumber(offset) && clock_config.timezone.offset != offset->valueint){
								/* realign offset if needed */
								updateNVS = true;
								clock_config.timezone.offset = offset->valueint;
								ESP_LOGI(TAG, "Offset set to: %d", clock_config.timezone.offset);
							}
						}

						/* memory clean up */
						free(json_str);
						cJSON_Delete(json);

						/* NVS needs to updated: notify the task that handles that */
						if(updateNVS){
							xTaskNotifyGive( clock_task_save_nvs );
						}

						/* finally, enqueue a timezone transitions call with the set timezone */
						clock_queue_message_t m;
						m.message = CLOCK_MESSAGE_REQUEST_TRANSITIONS_API_CALL;
						m.param = NULL;
						xQueueSend(clock_queue, &m, portMAX_DELAY);

					}
					break;
				case CLOCK_MESSAGE_SLEEPMODE_CONFIG:{
					ESP_LOGI(TAG, "CLOCK_MESSAGE_SLEEPMODE_CONFIG");
					sleepmodes_t* sleepmodes = (sleepmodes_t*)msg.param;
					clock_build_new_sleepmodes(*sleepmodes);

					/* if config needs to be updated then we do so and notify the task to save in flash 
						NOTE: we can do a simple memory comparison here because all the configurations objects
						are religiously bzero'd before using.
						This otherwise would be a problem because of struct alignments.
					*/
					if( memcmp( sleepmodes, &(clock_config.sleepmodes), sizeof(sleepmodes_t) ) != 0 ){
						clock_config.sleepmodes = *sleepmodes;
						xTaskNotifyGive( clock_task_save_nvs );
					}

					free(sleepmodes);
					}
					break;
				case CLOCK_MESSAGE_SLEEP_EVENT:{
					ESP_LOGI(TAG, "CLOCK_MESSAGE_SLEEP_EVENT");
					sleep_action_t a = (sleep_action_t)msg.param;
					if(a == SLEEP_ACTION_WAKE){
						display_turn_on();
						ws2812_set_backlight_color(clock_config.display.led_color);
					}
					else if(a == SLEEP_ACTION_SLEEP){
						display_turn_off();
						ws2812_set_backlight_color_rgb( (uint8_t)0, (uint8_t)0, (uint8_t)0);
					}

					}
					break;
				case CLOCK_MESSAGE_BACKLIGHTS_CONFIG:{
					;rgb_t rgb;
					rgb.num = (uint32_t)msg.param;
					if(clock_config.display.led_color.num != rgb.num){
						clock_config.display.led_color = rgb;
						xTaskNotifyGive( clock_task_save_nvs );
					}
					}
					break;

				default:
					ESP_LOGE(TAG, "Unknown task message received: %d", msg.message);
					break;
			}

		}
		taskYIELD();
	}


	vTaskDelete( NULL );

}
