512 lines
13 KiB
C++
512 lines
13 KiB
C++
/*
|
|
https://github.com/laurivosandi/nixiesp12/blob/master/firmware/main.py
|
|
https://randomnerdtutorials.com/wifimanager-with-esp8266-autoconnect-custom-parameter-and-manage-your-ssid-and-password/
|
|
*/
|
|
#include <LittleFS.h>
|
|
#include <WiFiManager.h>
|
|
#include <ezTime.h>
|
|
#include "cities.h"
|
|
|
|
// #define DEBUG 1
|
|
// #define DIMMING_ENABLED 1
|
|
// #define TEST_SEQUENCE 1
|
|
|
|
#define PIN_CLOCK 3
|
|
#define PIN_DATA 2
|
|
#define PIN_LATCH 0
|
|
|
|
#define SUNRISE 6
|
|
#define SUNSET 22
|
|
|
|
// ezTime structs
|
|
tmElements_t tm;
|
|
Timezone local;
|
|
|
|
int configDimmingDutyCycle = 0;
|
|
|
|
int current_dimming_duty_cycle;
|
|
enum typeOperationMode {
|
|
OPERATION_MODE_NORMAL,
|
|
OPERATION_MODE_DIMMED,
|
|
} operationModeCurrent = OPERATION_MODE_NORMAL;
|
|
|
|
#define DISPLAY_MODE_TIME 1
|
|
#define DISPLAY_MODE_DATE 2
|
|
#define DISPLAY_MODE_DATETIME 3
|
|
|
|
int configDisplayModesEnabled = 1;
|
|
int displayModeCurrent = 1;
|
|
|
|
WiFiManager wm;
|
|
|
|
const char displayModesCombobox[] = R"(
|
|
<br/>
|
|
<label for="displayModeCombobox">Clock display format</label>
|
|
<select name="timeDisplay" id="displayModeCombobox" onchange="document.getElementById('displayMode').value = this.value">
|
|
<option value="1">Time</option>
|
|
<option value="2">Date</option>
|
|
<option value="3">Time and Date</option>
|
|
</select>
|
|
<script>
|
|
document.getElementById("displayModeCombobox").value = document.getElementById("displayMode").value;
|
|
document.querySelector("[for='displayMode']").hidden = true;
|
|
document.getElementById("displayMode").hidden = true;
|
|
</script>)";
|
|
|
|
#ifdef DIMMING_ENABLED
|
|
const char dimmerSliderSnippet[] = R"(
|
|
<br/><label for='dimming_duty_cycle_slider'>Night time dimming</label>
|
|
<input type="range" min="1" max="3840" value="1000" class="slider" id="dimming_duty_cycle_slider" onchange="document.getElementById('dimming_duty_cycle').value = this.value">
|
|
<script>
|
|
document.getElementById('dimming_duty_cycle').hidden = true;
|
|
</script>
|
|
)";
|
|
#endif
|
|
|
|
WiFiManagerParameter paramNetworkTimeServer("networkTimeServer", "Network time server", "ee.pool.ntp.org", 63);
|
|
WiFiManagerParameter paramDisplayMode("displayMode", "Will be hidden", "1", 2);
|
|
WiFiManagerParameter paramDisplayModeCombobox(displayModesCombobox);
|
|
WiFiManagerParameter paramCity(cities);
|
|
WiFiManagerParameter paramTimezone("timezone", "Timezone encoding", "EET-2EEST,M3.5.0/3,M10.5.0/4", 30);
|
|
//WiFiManagerParameter paramLong("long", "Longitude", "26", 10);
|
|
//WiFiManagerParameter paramLat("lat", "Latitude", "59", 10);
|
|
|
|
#ifdef DIMMING_ENABLED
|
|
WiFiManagerParameter paramDimmingDutyCycle("dimming_duty_cycle", "", "1000", 4);
|
|
WiFiManagerParameter paramDimmingDutyCycleSlider(dimmerSliderSnippet);
|
|
|
|
// Dimmer settings
|
|
volatile long displayInterruptCount = 0;
|
|
#endif
|
|
|
|
volatile bool blink = true;
|
|
int lookup[] = {11, 9, 12, 8, 0, 4, 1, 3, 2, 10};
|
|
|
|
void ICACHE_RAM_ATTR bitbang_bit(int value){
|
|
if(value & 1){
|
|
digitalWrite(PIN_DATA, HIGH);
|
|
}
|
|
else{
|
|
digitalWrite(PIN_DATA, LOW);
|
|
}
|
|
digitalWrite(PIN_CLOCK, HIGH);
|
|
digitalWrite(PIN_CLOCK, LOW);
|
|
}
|
|
|
|
void ICACHE_RAM_ATTR bitbang_digit(int digit){
|
|
int i = 0;
|
|
if (!blink && timeStatus() != timeSet) {
|
|
for(i=0;i<4;i++){
|
|
bitbang_bit(1);
|
|
}
|
|
} else {
|
|
for(i=0;i<4;i++){
|
|
bitbang_bit(lookup[digit] << i >> 3);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ICACHE_RAM_ATTR renderTest(int j) {
|
|
for(int i=0; i<6; i++){
|
|
bitbang_bit(0);
|
|
bitbang_digit(j);
|
|
bitbang_bit(0);
|
|
bitbang_bit(0);
|
|
bitbang_bit(0);
|
|
}
|
|
digitalWrite(PIN_LATCH, HIGH);
|
|
digitalWrite(PIN_LATCH, LOW);
|
|
}
|
|
|
|
void ICACHE_RAM_ATTR renderTime(){
|
|
int hour = tm.Hour;
|
|
int minute = tm.Minute;
|
|
int second = tm.Second;
|
|
bitbang_bit(0);
|
|
bitbang_digit(hour / 10);
|
|
bitbang_bit(0);
|
|
bitbang_bit(0);
|
|
bitbang_bit(0);
|
|
|
|
bitbang_bit(0);
|
|
bitbang_digit(hour % 10);
|
|
bitbang_bit(blink);
|
|
bitbang_bit(blink);
|
|
bitbang_bit(blink);
|
|
|
|
bitbang_bit(blink);
|
|
bitbang_digit(minute / 10);
|
|
bitbang_bit(0);
|
|
bitbang_bit(0);
|
|
bitbang_bit(0);
|
|
|
|
bitbang_bit(0);
|
|
bitbang_digit(minute % 10);
|
|
bitbang_bit(blink);
|
|
bitbang_bit(blink);
|
|
bitbang_bit(blink);
|
|
|
|
bitbang_bit(blink);
|
|
bitbang_digit(second / 10);
|
|
bitbang_bit(0);
|
|
bitbang_bit(0);
|
|
bitbang_bit(0);
|
|
|
|
bitbang_bit(0);
|
|
bitbang_digit(second % 10);
|
|
bitbang_bit(0);
|
|
bitbang_bit(0);
|
|
bitbang_bit(0);
|
|
}
|
|
|
|
void ICACHE_RAM_ATTR renderDate(){
|
|
int day = tm.Day;
|
|
int month = tm.Month;
|
|
int year = tm.Year-30;
|
|
bitbang_bit(0);
|
|
bitbang_digit((year) / 10);
|
|
bitbang_bit(0);
|
|
bitbang_bit(0);
|
|
bitbang_bit(0);
|
|
|
|
bitbang_bit(0);
|
|
bitbang_digit((year) % 10);
|
|
bitbang_bit(0);
|
|
bitbang_bit(1);
|
|
bitbang_bit(0);
|
|
|
|
bitbang_bit(0);
|
|
bitbang_digit(month/ 10);
|
|
bitbang_bit(0);
|
|
bitbang_bit(0);
|
|
bitbang_bit(0);
|
|
|
|
bitbang_bit(0);
|
|
bitbang_digit(month % 10);
|
|
bitbang_bit(0);
|
|
bitbang_bit(1);
|
|
bitbang_bit(0);
|
|
|
|
bitbang_bit(0);
|
|
bitbang_digit(day / 10);
|
|
bitbang_bit(0);
|
|
bitbang_bit(0);
|
|
bitbang_bit(0);
|
|
|
|
bitbang_bit(0);
|
|
bitbang_digit(day % 10);
|
|
bitbang_bit(0);
|
|
bitbang_bit(0);
|
|
bitbang_bit(0);
|
|
}
|
|
|
|
void ICACHE_RAM_ATTR renderDisplay() {
|
|
switch (configDisplayModesEnabled) {
|
|
case 1:
|
|
renderTime();
|
|
break;
|
|
case 2:
|
|
renderDate();
|
|
break;
|
|
case 3:
|
|
if (millis() % 30000 < 15000) {
|
|
renderTime();
|
|
} else {
|
|
renderDate();
|
|
}
|
|
break;
|
|
}
|
|
digitalWrite(PIN_LATCH, HIGH);
|
|
digitalWrite(PIN_LATCH, LOW);
|
|
}
|
|
|
|
void ICACHE_RAM_ATTR clearDisplay() {
|
|
for(int i=0; i<6; i++){
|
|
bitbang_bit(1);
|
|
bitbang_bit(0);
|
|
bitbang_bit(0);
|
|
bitbang_bit(0);
|
|
bitbang_bit(0);
|
|
bitbang_bit(1);
|
|
bitbang_bit(1);
|
|
bitbang_bit(1);
|
|
}
|
|
|
|
digitalWrite(PIN_LATCH, HIGH);
|
|
digitalWrite(PIN_LATCH, LOW);
|
|
}
|
|
|
|
int counter = 0;
|
|
|
|
struct timeval tv;
|
|
|
|
#ifdef DIMMING_ENABLED
|
|
void ICACHE_RAM_ATTR dimmerTimerCallback() {
|
|
noInterrupts();
|
|
displayInterruptCount++;
|
|
gettimeofday(&tv, NULL);
|
|
blink = tv.tv_usec < 500000;
|
|
int j = current_dimming_duty_cycle;
|
|
if (j < configDimmingDutyCycle) {
|
|
j = configDimmingDutyCycle;
|
|
}
|
|
|
|
// if (counter == 0) {
|
|
renderDisplay();
|
|
timer1_write(j+1);
|
|
// counter = 1;
|
|
// } else if (counter == 1) {
|
|
// clearDisplay();
|
|
// timer1_write(3840-j+1);
|
|
// counter = 0;
|
|
// }
|
|
interrupts();
|
|
}
|
|
#endif
|
|
|
|
int loadClockConfig() {
|
|
|
|
unsigned char timezone[30] = {'\0'};
|
|
File file = LittleFS.open("/timezone", "r");
|
|
if (!file) { return 1; }
|
|
if (!file.read(timezone, sizeof(timezone))) { return 2; }
|
|
Serial.print("Using timezone: ");
|
|
Serial.println((const char*)timezone);
|
|
|
|
unsigned char timeserver[63] = {'\0'};
|
|
file = LittleFS.open("/timeserver", "r");
|
|
if (!file) { return 3; }
|
|
if (!file.read(timeserver, sizeof(timeserver))) { return 4; }
|
|
Serial.print("Using time server: ");
|
|
Serial.println((const char*)timeserver);
|
|
|
|
setServer((const char*)timeserver);
|
|
local.setPosix((const char*)timezone);
|
|
|
|
unsigned char modes[1] = {'\0'};
|
|
file = LittleFS.open("/modes", "r");
|
|
if (!file) { return 5; }
|
|
if (!file.read(modes, sizeof(modes))) { return 6; }
|
|
configDisplayModesEnabled = atoi((const char*)modes);
|
|
Serial.print("Enabled display modes:");
|
|
if (configDisplayModesEnabled & DISPLAY_MODE_DATE) {
|
|
Serial.print(" DATE");
|
|
}
|
|
if (configDisplayModesEnabled & DISPLAY_MODE_TIME) {
|
|
Serial.print(" TIME");
|
|
}
|
|
Serial.println();
|
|
|
|
#ifdef DIMMING_ENABLED
|
|
unsigned char bufDimming[10] = {'\0'};
|
|
file = LittleFS.open("/dimming", "r");
|
|
if (!file) { return 7; }
|
|
if (!file.read(bufDimming, sizeof(bufDimming))) { return 8; }
|
|
configDimmingDutyCycle = atoi((const char*)bufDimming);
|
|
Serial.print("Night time dimming duty cycle: ");
|
|
Serial.println((const char*)bufDimming);
|
|
#endif
|
|
|
|
return 0;
|
|
}
|
|
|
|
void saveParamsCallback() {
|
|
File file = LittleFS.open("/timeserver", "w");
|
|
file.print(paramNetworkTimeServer.getValue());
|
|
file.close();
|
|
|
|
file = LittleFS.open("/timezone", "w");
|
|
file.print(paramTimezone.getValue());
|
|
file.close();
|
|
|
|
#ifdef DIMMING_ENABLED
|
|
file = LittleFS.open("/dimming", "w");
|
|
file.print(paramDimmingDutyCycle.getValue());
|
|
file.close();
|
|
#endif
|
|
|
|
file = LittleFS.open("/modes", "w");
|
|
file.print(paramDisplayMode.getValue());
|
|
file.close();
|
|
|
|
loadClockConfig();
|
|
}
|
|
|
|
void handleMetrics(){
|
|
char tbuf[30];
|
|
|
|
String buf = "";
|
|
|
|
buf += "nixie_sketch_size_bytes ";
|
|
buf += ESP.getSketchSize();
|
|
buf += "\n";
|
|
|
|
buf += "nixie_flash_space_bytes ";
|
|
buf += ESP.getFlashChipRealSize();
|
|
buf += "\n";
|
|
|
|
buf += "nixie_free_heap_bytes ";
|
|
buf += ESP.getFreeHeap();
|
|
buf += "\n";
|
|
|
|
#ifdef DIMMING_ENABLED
|
|
buf += "nixie_display_interrupt_count ";
|
|
buf += displayInterruptCount;
|
|
buf += "\n";
|
|
#endif
|
|
|
|
buf += "nixie_ntp_last_sync_timestamp_seconds ";
|
|
buf += lastNtpUpdateTime();
|
|
buf += "\n";
|
|
|
|
wm.server->send(200, "text/plain", buf);
|
|
}
|
|
|
|
void initializePins() {
|
|
pinMode(PIN_CLOCK, OUTPUT);
|
|
pinMode(PIN_LATCH, OUTPUT);
|
|
pinMode(PIN_DATA, OUTPUT);
|
|
digitalWrite(PIN_CLOCK, LOW);
|
|
digitalWrite(PIN_LATCH, LOW);
|
|
digitalWrite(PIN_DATA, LOW);
|
|
clearDisplay();
|
|
}
|
|
|
|
void setup() {
|
|
Serial.begin(9600);
|
|
Serial.println("Nixie clock booting up");
|
|
initializePins();
|
|
|
|
#ifdef DEBUG
|
|
setDebug(INFO);
|
|
#endif
|
|
|
|
#ifdef TEST_SEQUENCE
|
|
for(int i = 0; i < 10; i++) {
|
|
renderTest(i);
|
|
delay(3000);
|
|
}
|
|
#endif
|
|
|
|
wm.setDebugOutput(true);
|
|
wm.setMinimumSignalQuality(50);
|
|
|
|
if (!LittleFS.begin()) {
|
|
Serial.println("LittleFS mount failed");
|
|
} else {
|
|
if(loadClockConfig() != 0) {
|
|
Serial.println("Failed to load clock configuration from LittleFS");
|
|
} else {
|
|
Serial.println("Configuration loaded");
|
|
}
|
|
}
|
|
wm.addParameter(¶mNetworkTimeServer);
|
|
wm.addParameter(¶mCity);
|
|
wm.addParameter(¶mTimezone);
|
|
//wm.addParameter(¶mLong);
|
|
//wm.addParameter(¶mLat);
|
|
wm.addParameter(¶mDisplayMode);
|
|
wm.addParameter(¶mDisplayModeCombobox);
|
|
|
|
#ifdef DIMMING_ENABLED
|
|
wm.addParameter(¶mDimmingDutyCycle);
|
|
wm.addParameter(¶mDimmingDutyCycleSlider);
|
|
#endif
|
|
|
|
wm.setSaveParamsCallback(saveParamsCallback);
|
|
wm.setShowInfoUpdate(false); // https://github.com/tzapu/WiFiManager/issues/1262
|
|
wm.setShowInfoErase(false);
|
|
wm.setConfigPortalBlocking(false);
|
|
|
|
Serial.println("Autostarting wireless");
|
|
|
|
wm.autoConnect();
|
|
|
|
Serial.println("Starting config portal");
|
|
|
|
|
|
wm.startConfigPortal();
|
|
|
|
#ifdef DEBUG
|
|
wm.server->on("/metrics", handleMetrics);
|
|
#else
|
|
wm.setDebugOutput(false);
|
|
#endif
|
|
|
|
renderNormal();
|
|
}
|
|
|
|
|
|
void renderNormal() {
|
|
gettimeofday(&tv, NULL);
|
|
blink = tv.tv_usec < 500000;
|
|
breakTime(local.now(), tm);
|
|
|
|
renderDisplay();
|
|
delay((500 - (local.ms(LAST_READ) % 500)) + 1);
|
|
|
|
#ifdef DEBUG
|
|
Serial.print(1970+tm.Year);
|
|
Serial.print("-");
|
|
Serial.print(tm.Month);
|
|
Serial.print("-");
|
|
Serial.print(tm.Day);
|
|
Serial.print(" ");
|
|
Serial.print(tm.Hour);
|
|
Serial.print(":");
|
|
Serial.print(tm.Minute);
|
|
Serial.print(":");
|
|
Serial.print(tm.Second);
|
|
Serial.print(".");
|
|
Serial.println(local.ms(LAST_READ));
|
|
#endif
|
|
}
|
|
|
|
void loop() {
|
|
wm.process();
|
|
events(); // this invokes yield()
|
|
|
|
#ifdef DIMMING_ENABLED
|
|
switch (operationModeCurrent) {
|
|
case OPERATION_MODE_NORMAL:
|
|
|
|
if ((tm.Hour < SUNRISE || tm.Hour > SUNSET) && timeStatus() == timeSet) {
|
|
operationModeCurrent = OPERATION_MODE_DIMMED;
|
|
#ifdef DEBUG
|
|
Serial.println("Clock synchronized, disabling wireless, enabling dimming");
|
|
#endif
|
|
|
|
WiFi.disconnect();
|
|
WiFi.mode(WIFI_OFF);
|
|
|
|
timer1_attachInterrupt(dimmerTimerCallback);
|
|
timer1_isr_init();
|
|
timer1_enable(TIM_DIV256, TIM_EDGE, TIM_SINGLE);
|
|
timer1_write(100);
|
|
} else {
|
|
renderNormal();
|
|
}
|
|
break;
|
|
case OPERATION_MODE_DIMMED:
|
|
if (tm.Hour > SUNRISE && tm.Hour < SUNSET) {
|
|
operationModeCurrent = OPERATION_MODE_NORMAL;
|
|
timer1_detachInterrupt();
|
|
timer1_disable();
|
|
|
|
#ifdef DEBUG
|
|
Serial.println("Disabling dimming");
|
|
#endif
|
|
|
|
WiFi.mode(WIFI_STA);
|
|
} else if (tm.Hour == SUNRISE) {
|
|
current_dimming_duty_cycle = tm.Minute << 6;
|
|
} else if (tm.Hour == SUNSET) {
|
|
current_dimming_duty_cycle = (61 - tm.Minute) << 6;
|
|
}
|
|
break;
|
|
}
|
|
#else
|
|
renderNormal();
|
|
#endif
|
|
}
|