2019年6月19日水曜日

Automatic Clock Tuner V2.14

//
// Automatic Clock Tuner
// by Kiraku Labo
//
#include <SPIFFS.h>
#include <WiFiUdp.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <SSD1306Wire.h>
#include <ArduinoOTA.h>
#include <ESPAsyncWebServer.h>
#include <AsyncTCP.h>
const String verNum="214";
const String fileVer="D"; //if file ver is different, load default setting
const String project="cTune";
const char* apPass = "87654321";
#define TZ_STR0 "JST-9"
#define TZ_STR1 "CET-1CEST-2,M3.5.0/02:00:00,M10.5.0/03:00:00"
#define TZ_STR2 "CST6CDT5,M3.2.0/02:00:00,M11.1.0/02:00:00"
#define TZ_STR3 "NZST-12NZDT-13,M9.5.0/02:00:00,M4.1.0/2:00:00"
#if !defined(DEVELOP)
#define WEMOS //CPU board
//#define HELTEC //CPU board
#define DISP_INVERTED
#define A3144 //Mag sensor (Hall)
//#define SK8552 //Mag sensor (Hall)
//#define HMC5883 //Mag sensor (Digital compass)
//#define LSM303 //Mag sensor (Digital compass)
#define DEFAULT_PEND_CYCLE 1
#define DEFAULT_PEND_DUR 1
#define DEFAULT_TUNE_CYCLE 60
#define DEFAULT_PEND_LF 1
#define DEFAULT_KP 150
#define DEFAULT_KI 20
#define DEFAULT_NTP_BY_NAME true
#define DEFAULT_STA_IP_FIX "0" //1:fix 0:DNS
#define DEFAULT_DATE_FORMAT 2 //European
const char* DEFAULT_STA_GW="192.168.0.1";
const char* DEFAULT_SUBNET="255.255.255.0";
const char* DEFAULT_STA_DNS="192.168.0.1";
const char* DEFAULT_NTP_IP="";
const char* DEFAULT_NTP_NAME="pool.ntp.org";
const char* DEFAULT_TZ_STR=TZ_STR1; //;
const char* DEFAULT_WIFI_SSID="";
const char* DEFAULT_WIFI_PASS="";
const char* DEFAULT_STA_IP="192.168.0.123";
const IPAddress apIP(192,168,123,123);
const char* apSsid= "cTune";
const String verLetter="V";
#endif
#define DEFAULT_NTP_MIN_INTERVAL 3 //minute
#define DEFAULT_NTP_MAX_INTERVAL 60 //minute
#define MAX_NTP_TIMEOUT 1000 //msec
#define INITIAL_NTP_VALID 100 //msec
#define N_NTP_IP_HISTORY 20
#define BUF_SIZE 200
#define N_NTP_HISTORY 300
#define PRE_CYCLE_MS 8000 //8 sec worth of cycles before setRef
#define N_HISTORY 1500
#define MIN_HEAP 15000
#define NUM_GRAPH_SPAN 1440 //points, must be less than N_HISTORY
#define NTP_PACKET_SIZE 48
#define SEVENTY_YEARS 2208988800UL
#define SCREEN_W 128
#define SCREEN_H 64
#define CENTER_W (SCREEN_W/2)
#define CHAR_W 8
#define CHAR_H 16
#define GTOP 33
#define GBOTTOM 63
#define GYCENTER 48 // y axis 0
#define GXORIGIN 2
#define GRAPH_W (SCREEN_W-GXORIGIN-3)
#define OLEDFONT ArialMT_Plain_16
#define CONFIG_FILE "/config.file"
#define G2_INTERVAL 15 //points
#define G2_THRESH 1500 //points
#define ADJ_MAX 1023 //must be 63 or more, see ##1
#define ADJ_MIN -1023
#define ONOFF_MODE_THRESH 500 //csec, If difference is greater than this value, coil is driven in full power.
#define PRECISE_MODE_THRESH 100 //In Catchup mode, when the error become within this csec, catchup mode ends.
#define ERROR_LIMIT -43200 //=12H If error (in sec) <this value (big delay), coil power is disabled
#define NTP_OFFSET_THRESH_H 20000 //usec
#define NTP_OFFSET_THRESH_L 10000 //usec
#if defined(HELTEC)
#define SENS_PIN 12
#define LED_PIN 25 //internal LED
#define DRV1_PIN 14
#define DRV2_PIN 27
#define SDA_PIN 4
#define SCL_PIN 15
#elif defined(WEMOS)
#if defined(LSM303)
#define SENS_PIN 16
#else
#define SENS_PIN 15
#endif
#define LED_PIN 13
#define DRV1_PIN 12
#define DRV2_PIN 14
#define SDA_PIN 5
#define SCL_PIN 4
#endif
#if defined(A3144)
#define MAG_ON LOW
#define MAG_OFF HIGH
#else
#define MAG_ON HIGH
#define MAG_OFF LOW
#endif
#if defined(HMC5883)
#define MAG_ADDR 0x1E
#define MAG_THRESH_H 3000
#define MAG_THRESH_L 2000
#elif defined(LSM303)
#define MAG_ADDR 0x1E
#define MAG_THRESH_HB 16
#define MAG_THRESH_LB 0
#endif
int32_t ntpOffsetHistory[N_NTP_HISTORY]; //usec
int32_t ntpDelayHistory[N_NTP_HISTORY]; //usec
int32_t ntpData3History[N_HISTORY];
int32_t ntpData4History[N_HISTORY];
time_t ntpTimeHistory[N_NTP_HISTORY]; //sec
byte ntpPassHistory[N_NTP_HISTORY]; //pass=1 ,fail=0
int32_t ntpNumHistory=-1;
struct tm tmAtRef0Local, tmAtSetTimeLocal, tmAtResetMMLocal, tmSynchLocal;
struct timeval tvAtNtpSuccess, tvAtSensTrig, tvAtRef0, tvAtResetMM, tvAtNtpFail, tvGmtNow, tvAtNtpServerSet;
struct timeval tvNtpRef, tvNtpOrig, tvNtpRecv, tvNtpTrans, tvStart, tvEnd;
int32_t ntpDelay, ntpOffset, ntpOffsetPerMin=0;
float ntpDelayAve;
unsigned portBASE_TYPE stzckSize;
boolean serialMon=false;
const String textHtml="text/html";
int32_t errorHistory[2][N_HISTORY]; //csec
int16_t adjHistory[2][N_HISTORY];
time_t tHistory[2][N_HISTORY]; //sec
time_t gmtLast=0, gmtAtSec=0, gmtNow;
int16_t adjLevel=0, adjLevelMax=0, adjLevelMin=0;
float cadjLevelAve=0.0;
int32_t numHistory[2]={-1, -1};
boolean adjOn=true;
boolean onoffMode=false;
boolean timeWasSynched=false, setRefReady=false;
int16_t timeBuf[6];
int16_t adjGraphFactor=5*(ADJ_MAX+1)/64;
uint16_t ntpIPHistoryIdx=0;
volatile boolean refSet=false;
volatile int32_t countPend=0;
volatile boolean sensTrig=false, trigWindow=false;
volatile boolean sensRelease=false;
volatile int16_t pendDir=0;
volatile uint32_t milliSensLast=0, milliSensTrig=0, milliSensRelease=0, milliSensDur=0;
volatile int64_t csecPend=0;
volatile boolean sensWithinWindow=false;
char strBuf[BUF_SIZE];
byte packetBuffer[NTP_PACKET_SIZE];
boolean shortPush=false, longPush=false, longlongPush=false;
uint32_t buttonMilli=0;
time_t tLocal;
time_t tzStart=0, tzEnd=0;
int32_t tzOffset=0;
boolean tzDstObs=false;
int tzDst=0;
int32_t csecPendError=0, csecPendErrorMax=0, csecPendErrorMin=0, csecPendErrorLast=0;
int32_t cadjLevel=0;
uint32_t ntpTurnaround;
byte gSpanUnitIdx=1; //0:h 1:d 2:w
byte gYEscaleIdx=0;
byte gYCscaleIdx=0;
uint32_t graphSpan=24*3600; //in seconds
String gYEscaleMM, gYCscaleMM;
const uint32_t gSpanUnit[] = {3600, 3600*24, 24*7*3600};
const String gSpanUnitStr[] = {"h", "d", "w"};
const String gYEscaleStr[] = {"Auto", "1s", "5s ", "20s", "100s"};
const String gYCscaleStr[] = {"Auto", "20", "100", "500", "2000"};
const int16_t gYEscaleNum[] = {0,1,5,20,100};
const int16_t gYCscaleNum[] = {0,20,100,500,2000};
const int16_t ngY1scaleNum[] = {10,20,50,100};
byte ngY1scaleIdx=0;
int16_t gYEscale=0, gYCscale=0;
byte ntpPacketId=0;
byte ntpInvalidCount=0;
boolean ntpByName=DEFAULT_NTP_BY_NAME;
uint16_t countConsecutiveDec=0;
WiFiUDP udp;
HTTPClient http;
AsyncWebServer server(80);
SSD1306Wire oled(0x3c, SDA_PIN, SCL_PIN);
const IPAddress apSNM(255,255,255,0);
IPAddress staIP;
IPAddress staGateway;
IPAddress staSNM;
IPAddress staDNS;
IPAddress ntpIP;
IPAddress ntpIPbyIP;
IPAddress ntpIPHistory[N_NTP_IP_HISTORY];
time_t ntpIPTimeHistory[N_NTP_IP_HISTORY];
const String msgHead ="<html><head><meta charset='UTF-8'><title>Kiraku Labo</title>\
<style>body{width:95%;margin:0;padding:30;background-color:#dddddd;text-align:center;font-size:36pt}\
h1{font-size:40pt} h2{font-size:36pt} p{font-size:36pt} button{font-size:50pt}\
input[type=submit]{font-size:50pt}\
input[type=radio]{width:50px;height:50px;vertical-align:middle}\
input[type=text]{width:500px;font-size:36pt;margin-right:50px}\
input[type=number]{width:130px;font-size:36pt;margin-right:0px}\
span{display:inline-block;width:350px;text-align:right}\
form{display:inline}</style>\
</head><body><h1>Kiraku Labo " + project + " " + verLetter+verNum +"</h1>";
const String msgHome = msgHead +
"<form action='CF'><input type='submit' value='Config'></form> \
<form action='SA'><input type='submit' value='Sens Adj'></form> \
<form action='MR'><input type='submit' value='Rst M/M'></form><br>\
<form action='H0'><input type='submit' value='Hand 0s'></form>\
<form action='EZ'><input type='submit' value='Err 0'></form>\
<form id='form2' action='SE'><button type='submit' form='form2'>Err +/-</button>\
<input id='EV' name='EV' type='number' step='0.1' value='0' style='font-size:50pt'>s</form><br>\
<form action='TC'><input type='submit' value='Tgl Ctrl'></form>\
<form id='form3' action='SO'><button type='submit' form='form3'>Set Current</button>\
<input id='OV' name='OV' type='number' value='0' max='1023' min='-1023' style='font-size:50pt;width:200px'></form><br>\
<form action='ST'><input type='submit' value='Home'></form> \
<form action='GR'><input type='submit' value='Accuracy'></form> \
<form action='GN'><input type='submit' value=' NTP '></form>";
const String msgEnd = "</body></html>";
const String msgYesNo= "<form action='YS'><input type='submit' value='Yes'></form>\
<form action='NO'><input type='submit' value=' No '>";
const String msgChartHead = "<canvas id='myChart' width='400' height='250'></canvas>\
<script src='https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.1/Chart.bundle.min.js'></script>\
<script>Chart.defaults.global.defaultFontSize=30;";
int32_t pendCycle, pendDur;
String msgConf;
String wifiSSID, wifiPass, ntpServerName, tzStr;
String strVer, strPC, strPD, strPT, strKP, strKI, strLF, strWS, strWP, strIF, strIL, strIG, strID, strIS, strNF, strNN, strNI, strNV, strNX, strTZ, strDF;
boolean staIpFixed=false, apOnly=false, wifiDefined=false;
byte dateFormat=0;
const String dateFmtYMD[4]={"%Y/%m/%d", "%Y/%m/%d", "%d/%m/%Y", "%m/%d/%Y"};
const String dateFmtMD[4]={"%m/%d", "%m/%d", "%d/%m", "%m/%d"};
const String dateStr[4]={"", "yyyy/mm/dd", "dd/mm/yyyy", "mm/dd/yyyy"};
volatile uint32_t microSensTrig=0;
volatile boolean sensAdjMode=false;
byte lcdGmode=0; //0:alternate, 1:error, 2:Current
int32_t ntpDelayLimitUs=INITIAL_NTP_VALID*1000;
int16_t magX, magY, magZ, magLast;
int32_t minSensOffDuration, minSensOnDuration, trigWindowSpan, pendWindow;
int16_t kP, kI, liftFactor;
byte preCount;
int32_t tuneInterval; //in sec
int32_t tuneIntervalMs; //in msec
int16_t pendTuneCycle; //cycles per tune
uint32_t pendPeriodMs;
int32_t secTmAdj, usecTmAdj;
int16_t ntpInterval, ntpMinInterval, ntpMaxInterval;
boolean ntpFirstOffset=true;
int32_t ntpInterporateAccum=0, ntpInterporateCount=0;
int64_t trigDelay=0;
uint32_t msecInterporate=0, msecNTP=0;
uint32_t msecTune=0;
uint32_t ntpSynchCount=0, ntpIntervalInc=0, ntpIntervalDec=0;
uint16_t ntpY1Scale=20, ntpY2Scale=200;
const int32_t gscaleE[]={10,20,50,100,200,500,1000,2000,5000,10000,20000,50000,100000,200000,500000}; //x0.01sec
const int16_t gscaleA[]={5,10,20,50,100,200,500,1000};
const int16_t numgE=sizeof(gscaleE)/sizeof(gscaleE[0]);
const int16_t numgA=sizeof(gscaleA)/sizeof(gscaleA[0]);
const uint32_t i2cTimeout=10; //msec
byte i2cStatus;
//boolean compassTimeout;
void (*funcPtrYes)(AsyncWebServerRequest *request)=NULL;
void (*funcPtrNo)(AsyncWebServerRequest *request)=NULL;
void (*funcPtrWeb)(AsyncWebServerRequest *request)=NULL;
AsyncWebServerRequest *funcReqWeb;
void sensInt() {
uint32_t milliAtInt=millis();
int16_t sns=digitalRead(SENS_PIN);
if (pendDir==0 && sns==MAG_ON && (milliAtInt-milliSensRelease)>minSensOffDuration) {
pendDir=1;
if (!sensAdjMode) digitalWrite(LED_PIN, HIGH);
sensTrig=true;
microSensTrig=micros();
milliSensTrig=milliAtInt;
if (refSet) {
countPend++;
csecPend=(int64_t)countPend * (int64_t)pendDur * 100 / (int64_t)pendCycle;
}
}
else if (pendDir==1 && sns==MAG_OFF && (milliAtInt-milliSensTrig)>minSensOnDuration) {
pendDir=0;
if (!sensAdjMode) digitalWrite(LED_PIN, LOW);
sensRelease=true;
milliSensRelease=milliAtInt;
}
}
void setup() {
SPIFFS.begin(true);
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
#ifdef HELTEC
pinMode(16, OUTPUT);
digitalWrite(16, LOW); // set GPIO16 low to reset OLED
delay(50);
digitalWrite(16, HIGH);
#endif
setupOled();
Wire.setClock(50000); //50khz
#ifdef DISP_INVERTED
oled.flipScreenVertically();
#endif
oled.setTextAlignment(TEXT_ALIGN_LEFT);
lcdDispText(0, 0, project+" "+verLetter+verNum);
#if defined(HMC5883)
setupHMC5883();
#elif defined(LSM303)
setupLSM303();
#endif
boolean loadConfErr=false;
if (SPIFFS.exists(CONFIG_FILE)) {
if (!loadConfigFile()) {
loadDefaultConfig();
loadConfErr=true;
}
}
else {
loadDefaultConfig();
loadConfErr=true;
}
setConfigPara();
setConfigMsg();
if (loadConfErr) saveConfigFile();
ntpInterval=ntpMinInterval;
setupMechTiming();
preCount=PRE_CYCLE_MS/pendPeriodMs;
if (preCount<3) preCount=3;
setupAP();
byte tryCount=0;
do {
#ifdef DEVELOP
selectWifi();
#endif
setupWifi();
tryCount++;
} while(wifiConnected()==false && tryCount<3);
if (!wifiConnected()) {
apOnly=true;
lcdDispText(0,0, F("AP Only "));
lcdDispText(0,1, apSsid);
lcdDispText(0,2, apPass);
lcdDispText(0,3, WiFi.softAPIP().toString());
}
setupServer();
while(!wifiConnected()); //if no wifi, infinite loop here
setupOta();
setupNtp();
while (!synchTime(1)) { //use transmit timestamp
delay(10000);
}
tvAtNtpServerSet=tvAtNtpSuccess;
ntpIPHistory[0]=ntpIP;
ntpIPTimeHistory[0]=tvAtNtpServerSet.tv_sec;
ntpDelayAve=(float)ntpDelay;
updateNtpValidDelay();
while (!synchTime(0)) { //use offset
updateNtpValidDelay();
delay(10000);
}
updateNtpValidDelay();
lcdDispText(0,0, "NTP delay=" + String(ntpDelay/1000));
delay(3000);
setDst();
#if !defined(DRV8830)
ledcSetup(1, 100, 10); //LEDC_CHANNEL, LEDC_FREQUENCY, LEDC_RESOLUTION_BITS
ledcSetup(2, 100, 10);
ledcAttachPin(DRV1_PIN, 1); //PIN, LEDC_CHANNEL
ledcAttachPin(DRV2_PIN, 2);
ledcWrite(1,0);
ledcWrite(2,0);
#endif
pinMode(SENS_PIN, INPUT_PULLUP);
#if !defined(HMC5883)
attachInterrupt(digitalPinToInterrupt(SENS_PIN), sensInt, CHANGE);
#endif
#if defined(DEVELOP)
devSetup();
#endif
}
void loop() {
#if defined(HMC5883)
getHMC5883();
if (sensAdjMode) {
char lcdbuf[13];
snprintf(lcdbuf, 19, "X= %s%05d", magX>=0?" ":"-", abs(magX));
lcdDispText2(0,1, lcdbuf, 80);
snprintf(lcdbuf, 19, "Y= %s%05d", magY>=0?" ":"-", abs(magY));
lcdDispText2(0,2, lcdbuf, 80);
snprintf(lcdbuf, 19, "Z= %s%05d", magZ>=0?" ":"-", abs(magZ));
lcdDispText2(0,3, lcdbuf, 80);
}
int16_t magValue=abs(magX)+abs(magY)+abs(magZ);
if (magValue>MAG_THRESH_H) magTrigH();
else if (magValue<MAG_THRESH_L) magTrigL();
#elif defined(LSM303)
if (sensAdjMode) {
char lcdbuf[13];
magX=readLSM303(0x28);
snprintf(lcdbuf, 19, "X= %s%05d", magX>=0?" ":"-", abs(magX));
lcdDispText2(0,1, lcdbuf, 80);
magY=readLSM303(0x2A);
snprintf(lcdbuf, 19, "Y= %s%05d", magY>=0?" ":"-", abs(magY));
lcdDispText2(0,2, lcdbuf, 80);
magZ=readLSM303(0x2C);
snprintf(lcdbuf, 19, "Z= %s%05d", magZ>=0?" ":"-", abs(magZ));
lcdDispText2(0,3, lcdbuf, 80);
if (digitalRead(SENS_PIN)==MAG_ON) pendIndicator(WHITE);
else pendIndicator(BLACK);
}
#endif
#if !defined(HMC5883)
if (sensAdjMode) {
if (digitalRead(SENS_PIN)==MAG_ON) digitalWrite(LED_PIN, HIGH);
else digitalWrite(LED_PIN, LOW);
}
#endif
if (sensTrig) {
sensTrig=false;
#if !defined(HMC5883)
trigDelay=(int64_t)(micros()-microSensTrig);
if (trigDelay<0) trigDelay=0;
#endif
gettimeofday(&tvAtSensTrig, NULL);
convUsec2Tv(convTv2Usec(&tvAtSensTrig) - trigDelay, &tvAtSensTrig);
pendIndicator(WHITE);
if (refSet) {
if (countPend%pendTuneCycle==0) {
updateError();
lcdDispError();
tune();
lcdDispGraph();
}
else {
updateError();
if (!sensAdjMode) {
lcdDispError();
lcdDispGraph();
}
}
}
if (preCount>0) {
preCount--;
if (preCount==0) {
setRef0();
tune();
}
}
if (refSet) lcdDispNum4n(0,0,countPend%pendTuneCycle);
}
else if (sensRelease) {
sensRelease=false;
pendIndicator(BLACK);
}
gettimeofday(&tvGmtNow, NULL);
gmtNow=tvGmtNow.tv_sec;
if (gmtNow>gmtLast) { //every 1 sec
gmtAtSec=gmtNow;
gmtLast=gmtNow;
int sec=gmtNow%60;
if (preCount>0) lcdDispTime(0,0,1); //date & time
else lcdDispTime(5,0,2); //time only
if (millis()-milliSensTrig>pendWindow && refSet) {
tvAtSensTrig=tvGmtNow;
updateError();
if (!sensAdjMode) lcdDispError();
}
if (sec==30) {
if (wifiConnected()) {
if (gmtAtSec>=tvAtNtpSuccess.tv_sec + ntpInterval*60) {
if (millis()-msecNTP>30000) {
msecNTP=millis();
if (synchTime(0)) updateNtpValidDelay();
else if (!ntpFirstOffset) interporateTime();
}
}
else if (!ntpFirstOffset) interporateTime();
}
if (millis()-milliSensTrig>pendWindow && refSet) tune();
}
if (refSet) lcdDispGraph();
#if defined(DEVELOP)
devOneSec();
#endif
}
if (funcPtrWeb) {
funcPtrWeb(funcReqWeb);
funcPtrWeb=NULL;
}
senseButton();
ArduinoOTA.handle();
yield();
}
void setRef0() {
countPend=0;
refSet=true;
tvAtRef0=tvAtSensTrig;
localtime_r(&tvAtRef0.tv_sec, &tmAtRef0Local);
tvAtResetMM=tvAtRef0;
tmAtResetMMLocal=tmAtRef0Local;
numHistory[0]=0;
numHistory[1]=0;
tHistory[0][0]=tvAtRef0.tv_sec;
tHistory[1][0]=tvAtRef0.tv_sec;
milliSensLast=milliSensTrig;
csecPendError=0;
csecPendErrorLast=0;
adjLevelMax=0;
adjLevelMin=0;
csecPendErrorMax=0;
csecPendErrorMin=0;
sendStat();
}
void sendStat() {
milliSensDur=milliSensTrig-milliSensLast;
milliSensLast=milliSensTrig;
struct tm tl;
localtime_r(&tvAtSensTrig.tv_sec, &tl);
snprintf(strBuf, BUF_SIZE, "PEND:%02d/%02d %02d:%02d:%02d.%03d Intv=%04d Cnt=%d Hst=%d TrigD=%d Err=%.2f Out=%d Ave=%.2f",
tl.tm_mon+1, tl.tm_mday, tl.tm_hour, tl.tm_min, tl.tm_sec, tvAtSensTrig.tv_usec/1000,
milliSensDur, countPend, numHistory[0], (int32_t)trigDelay, (float)csecPendError/100.0, adjLevel, cadjLevelAve/100.0);
dbgPrintln(strBuf);
}
void pendIndicator(OLEDDISPLAY_COLOR color) {
oled.setColor(color);
oled.fillCircle(124, 25, 3);
oled.display();
}
void tune() {
numHistory[0]++;
int16_t idx=numHistory[0] % N_HISTORY;
if (adjOn) {
adjPend();
lcdDispNum4n(9,1,adjLevel);
}
csecPendErrorLast=csecPendError;
errorHistory[0][idx]=csecPendError;
if (!onoffMode) cadjLevelAve = cadjLevelAve * 0.97 + (float)cadjLevel * 0.03;
adjHistory[0][idx]=adjLevel;
tHistory[0][idx]=gmtAtSec;
if (numHistory[0]%G2_INTERVAL==0) {
numHistory[1]++;
int16_t idx2=numHistory[1] % N_HISTORY;
errorHistory[1][idx2]=csecPendError;
adjHistory[1][idx2]=adjLevel;
tHistory[1][idx2]=gmtAtSec;
}
sendStat();
}
void updateError() {
// if (abs(csecPendError/100)>30*24*3600) resetRef(); //more than 30 day difference
csecPendError=(int32_t)(csecPend - (100*(int64_t)(tvAtSensTrig.tv_sec-tvAtRef0.tv_sec) +(int64_t)(tvAtSensTrig.tv_usec-tvAtRef0.tv_usec)/10000));
if (csecPendError>csecPendErrorMax) {
csecPendErrorMax=csecPendError;
// sendStat();
}
if (csecPendError<csecPendErrorMin) {
csecPendErrorMin=csecPendError;
// sendStat();
}
}
void lcdDispError() {
lcdDispNumF(0,1, csecPendError);
}
void adjPend() {
msecTune=millis();
if (abs(csecPendError)>ONOFF_MODE_THRESH) onoffMode=true;
if (onoffMode) {
if (csecPendError>PRECISE_MODE_THRESH) cadjLevel=ADJ_MIN*100/liftFactor;
else if (csecPendError<-PRECISE_MODE_THRESH) cadjLevel=ADJ_MAX*100;
else {
onoffMode=false;
cadjLevel=(int32_t)cadjLevelAve;
}
}
else {
int32_t cadjLevelDelta = (int32_t) ((csecPendError-csecPendErrorLast)*6*kP/tuneInterval + csecPendError*kI/10); //use per-min rate factor of 60/tuneInterval
cadjLevelDelta *= (ADJ_MAX+1)/64; //##1
cadjLevel = constrain(cadjLevel-cadjLevelDelta, ADJ_MIN*100/liftFactor, ADJ_MAX*100);
}
if (csecPendError<ERROR_LIMIT*100) cadjLevel=0;
adjLevel=cadjLevel/100;
drvCoil(adjLevel);
if (adjLevel>adjLevelMax) adjLevelMax=adjLevel;
if (adjLevel<adjLevelMin) adjLevelMin=adjLevel;
}
#if !defined(DRV8830)
void drvCoil(int16_t pwr) {
if (pwr==0) digitalWrite(LED_PIN, LOW);
else digitalWrite(LED_PIN, HIGH);
if (pwr>=0) {
ledcWrite(1, 0);
ledcWrite(2, pwr);
}
else {
ledcWrite(2, 0);
ledcWrite(1, -pwr);
}
}
#endif
void senseButton() {
int button=digitalRead(0);
if (button==LOW) {
if (shortPush==false) {
shortPush=true;
longPush=false;
longlongPush=false;
buttonMilli=millis();
}
else {
if (millis()-buttonMilli>1000) {
if (!longPush) { //long push = enable/disable Current
longPush=true;
if (adjOn) {
adjOn=false;
adjLevel=0;
cadjLevel=0;
drvCoil(0);
lcdDispText(9,1, "---");
}
else {
adjOn=true;
lcdDispText(9,1, " ");
}
}
else if (millis()-buttonMilli>2000) {
if (!longlongPush) { //long long push
longlongPush=true;
}
}
}
}
}
else {
if (shortPush==true) {
shortPush=false;
if (!longPush && !longlongPush) { //short push = lcd graph mode
lcdGmode = (lcdGmode+1)%3;
}
}
}
}
int64_t convTv2Usec(struct timeval* tv) {
return (int64_t)(tv->tv_sec)*1000000 + (int64_t)(tv->tv_usec);
}
void convUsec2Tv(int64_t t, struct timeval* tv) {
tv->tv_sec=(time_t)(t/1000000);
tv->tv_usec=(uint32_t)(t%1000000);
}
boolean synchTime(byte synchMode) { //syncMode 0= offset synch, 1=Transmit timestamp sync
// lcdDispText(0,0, "NTP mode"+String(synchMode)+"...");
struct timeval tv1, tv2;
int ret=getNTPTime(synchMode);
if (ret==0) { //delay validity is OK
if (synchMode==1) { //transfer
settimeofday(&tvNtpTrans, NULL);
tvAtNtpSuccess=tvNtpTrans;
}
else if (synchMode==0) { //offset mode
ntpSynchCount++;
if (ntpFirstOffset) {
ntpInterval=ntpMinInterval;
ntpOffset /= ntpInterval;
ntpOffsetPerMin=ntpOffset;
ntpDelayAve=(float)ntpDelay;
ntpFirstOffset=false;
}
else {
int16_t increment;
ntpOffsetPerMin=(ntpInterporateAccum+ntpOffset)/(ntpInterporateCount+1);
if (abs(ntpOffset)<NTP_OFFSET_THRESH_L*3 && abs(ntpOffsetPerMin)<NTP_OFFSET_THRESH_L) {
increment=ntpInterval*3/10;
if (increment==0) increment=1;
ntpIntervalInc++;
countConsecutiveDec=0;
}
else if (abs(ntpOffset)>=NTP_OFFSET_THRESH_H*3 || abs(ntpOffsetPerMin)>=NTP_OFFSET_THRESH_H) {
increment=-ntpInterval*3/13;
if (increment==0) increment=-1;
ntpIntervalDec++;
countConsecutiveDec++;
if (countConsecutiveDec>=3 && ntpByName) ntpChange();
}
else {
increment=0;
countConsecutiveDec=0;
}
ntpInterval += increment;
if (ntpInterval>ntpMaxInterval) ntpInterval=ntpMaxInterval;
if (ntpInterval<ntpMinInterval) ntpInterval=ntpMinInterval;
if (ntpInterval<=0) ntpInterval=1;
dbgPrintln("NTP interval+=" + String(increment) + " =" + String(ntpInterval) + " InterporateCt=" + String(ntpInterporateCount) + " Offset=" + String(ntpOffset/1000) + "ms"); //$$$$$
ntpInterporateCount=0;
ntpInterporateAccum=0;
}
gettimeofday(&tv1, NULL);
convUsec2Tv(convTv2Usec(&tv1) + +ntpOffset + ntpOffsetPerMin, &tv2); //##2
settimeofday(&tv2, NULL);
tvAtNtpSuccess=tv2;
}
if (timeWasSynched==false) {
timeWasSynched=true;
}
}
else {
gettimeofday(&tvAtNtpFail, NULL);
if (ntpInvalidCount>=3) {
if (ntpDelayLimitUs*2<MAX_NTP_TIMEOUT*1000) ntpDelayLimitUs *= 2;
else ntpDelayLimitUs=MAX_NTP_TIMEOUT*1000;
if (ntpByName) {
if (ntpInvalidCount>=6) {
ntpInvalidCount=0;
ntpIntervalInc=0;
ntpIntervalDec=0;
ntpDelayLimitUs=INITIAL_NTP_VALID*1000;
ntpChange();
}
}
}
}
if (synchMode==0) updateNtpHistory(ret);
localtime_r(&tv1.tv_sec, &tmSynchLocal);
snprintf(strBuf, BUF_SIZE, "NTP:%d.%d.%d.%d PID=%03d Code=%d Settime=%02d/%02d %02d:%02d:%02d.%06d offset=%d offset/min=%d delay=%d delayAve=%.2f %s",
ntpIP[0], ntpIP[1], ntpIP[2], ntpIP[3], ntpPacketId, ret, tmSynchLocal.tm_mon+1, tmSynchLocal.tm_mday, tmSynchLocal.tm_hour, tmSynchLocal.tm_min,
tmSynchLocal.tm_sec, tv1.tv_usec, ntpOffset, ntpOffsetPerMin, ntpDelay, ntpDelayAve, ret==0?"Synch Success":"Validity Fail");
dbgPrintln(strBuf);
if (ret==0) return true;
else return false;
}
void ntpChange() {
IPAddress temp=ntpIP;
if (ntpByName) WiFi.hostByName(ntpServerName.c_str(), ntpIP);
else ntpIP=ntpIPbyIP;
if (temp[0]==ntpIP[0] && temp[1]==ntpIP[1] && temp[2]==ntpIP[2] && temp[3]==ntpIP[3]) return;
gettimeofday(&tvAtNtpServerSet, NULL);
ntpFirstOffset=true;
ntpInterval=ntpMinInterval;
countConsecutiveDec=0;
ntpIPHistoryIdx++;
ntpIPHistory[ntpIPHistoryIdx%N_NTP_IP_HISTORY]=ntpIP;
ntpIPTimeHistory[ntpIPHistoryIdx%N_NTP_IP_HISTORY]=tvAtNtpServerSet.tv_sec;
}
void updateNtpHistory(byte ret) {
ntpNumHistory++;
int32_t idx=ntpNumHistory%N_NTP_HISTORY;
ntpOffsetHistory[idx]=ntpOffsetPerMin;
ntpDelayHistory[idx]=ntpDelay;
ntpPassHistory[idx]=(ret==0?1:0);
ntpData3History[idx]=ntpOffset;
ntpData4History[idx]=ntpDelayLimitUs;
if (ret==0) ntpTimeHistory[idx]=tvAtNtpSuccess.tv_sec;
else ntpTimeHistory[idx]=tvAtNtpFail.tv_sec;
}
void updateNtpValidDelay() {
ntpDelayAve = ntpDelayAve * 0.9 + 0.1*(float)ntpDelay;
ntpDelayLimitUs=(int32_t)(ntpDelayAve*2.0);
if (ntpDelayLimitUs>MAX_NTP_TIMEOUT*1000) ntpDelayLimitUs=MAX_NTP_TIMEOUT*1000;
}
void interporateTime() {
if (ntpMinInterval>ntpMaxInterval) return; //no interporation
if (millis()-msecInterporate<30000) return;
msecInterporate=millis();
struct timeval tv1, tv2;
delay(10); //prevent sec going back
gettimeofday(&tv1, NULL);
convUsec2Tv(convTv2Usec(&tv1) + ntpOffsetPerMin, &tv2);
settimeofday(&tv2, NULL);
ntpInterporateCount++;
ntpInterporateAccum+=ntpOffsetPerMin;
dbgPrintln("Time interporation:" + String(ntpInterporateCount) + " present usec=" + String(tv1.tv_usec) + " adj=" + String(ntpOffsetPerMin) + "usec");
}
int getNTPTime(byte synchMode) {
int16_t pktSize;
if (udp.parsePacket()>0) udp.read(packetBuffer, NTP_PACKET_SIZE); //clear buffer
ntpPacketId++;
if (ntpPacketId==0) ntpPacketId=1; //1 to 255 are used
uint32_t msec=millis();
gettimeofday(&tvStart, NULL);
sendNTPpacket(); // send an NTP packet to a time server
do {
pktSize = udp.parsePacket();
ntpTurnaround=millis()-msec;
} while (pktSize<NTP_PACKET_SIZE && ntpTurnaround<MAX_NTP_TIMEOUT);
if (pktSize>0) udp.read(packetBuffer, NTP_PACKET_SIZE); // read the packet into the buffer
gettimeofday(&tvEnd, NULL);
getNtpTv(32, &tvNtpRecv);
getNtpTv(40, &tvNtpTrans);
int64_t t1=convTv2Usec(&tvStart);
int64_t t2=convTv2Usec(&tvNtpRecv);
int64_t t3=convTv2Usec(&tvNtpTrans);
int64_t t4=convTv2Usec(&tvEnd);
ntpOffset = (int32_t)( t2 - t1 + t3 - t4 )/2;
ntpDelay = (int32_t)((t4 - t1) - (t3 - t2));
if (ntpTurnaround>=MAX_NTP_TIMEOUT) {
ntpInvalidCount++;
return 2;
}
else if ((synchMode==0?ntpDelay/1000:ntpTurnaround)>=ntpDelayLimitUs/1000) { //##3
ntpInvalidCount++;
return 1;
}
else if (packetBuffer[31]!=ntpPacketId) return 3; //packet not match
else if (tvNtpTrans.tv_sec>2082726000UL) return 4; //if tv_sec>2036/1/1 do not record (NTP overflows in this year)
else {
ntpInvalidCount=0;
return 0;
}
}
void getNtpTv(int adr, struct timeval* tvp) {
uint32_t secSince1900 = packetBuffer[adr+0]<<24 | packetBuffer[adr+1]<<16 | packetBuffer[adr+2]<<8 | packetBuffer[adr+3];
tvp->tv_sec =secSince1900 - SEVENTY_YEARS;
tvp->tv_usec = (uint32_t)packetBuffer[adr+4]*1000000/256 + (uint32_t)packetBuffer[adr+5]*1000000/65536 +
(uint32_t)packetBuffer[adr+6]*1000000/65536/256;
}
void sendNTPpacket(){
memset(packetBuffer, 0, NTP_PACKET_SIZE);// set all bytes in the buffer to 0
packetBuffer[0] = 0b11100011; // LI, Version, Mode
packetBuffer[1] = 0; // Stratum, or type of clock
packetBuffer[2] = 6; // Polling Interval
packetBuffer[3] = 0xEC; // Peer Clock Precision
packetBuffer[12] = 49; // 8 bytes of zero for Root Delay & Root Dispersion
packetBuffer[13] = 0x4E;
packetBuffer[14] = 49;
packetBuffer[15] = 52;
packetBuffer[47] = ntpPacketId; //Transmit timestamp
udp.beginPacket(ntpIP, 123);
udp.write(packetBuffer, NTP_PACKET_SIZE);
udp.endPacket();
}
void setDst() {
setenv("TZ", tzStr.c_str(), 1);
tzset();
String s=String(tzname[0]);
if (tzStr.indexOf(',')==-1) {
tzDstObs=false;
lcdDispText(0,3, "DST not observed");
}
else {
tzDstObs=true;
s+= "/"+String(tzname[1]);
}
lcdDispText(0,2, "TZ="+s);
}
void setupAP() {
WiFi.softAP(apSsid, apPass);
delay(200);
WiFi.softAPConfig(apIP, apIP, apSNM);
}
void setupWifi() {
uint32_t milli;
lcdDispText(0,1,"");
clearGraph();
lcdDispText(0,2, wifiSSID);
WiFi.disconnect(true);
WiFi.config(0u, 0u, 0u); //clear static IP
WiFi.mode(WIFI_AP_STA);
if (staIpFixed) WiFi.config(staIP, staGateway, staSNM, staDNS);
if (wifiSSID.length()>0) {
WiFi.begin(wifiSSID.c_str(), wifiPass.c_str());
milli=millis();
int16_t st;
do {
st=WiFi.status();
delay(20);
} while (st!= WL_CONNECTED && millis()-milli<15000);
if (st==WL_CONNECTED) {
lcdDispText(12,2, " OK");
lcdDispText(0,3, WiFi.localIP().toString());
delay(5000);
}
else {
lcdDispText(12,2, " NG");
delay(1000);
}
}
}
void setupNtp() {
if (ntpByName) {
lcdDispText(0,2, ntpServerName);
WiFi.hostByName(ntpServerName.c_str(), ntpIP);
}
else {
lcdDispText(0,2, "NTP by IP");
ntpIP=ntpIPbyIP;
}
ntpFirstOffset=true;
lcdDispText(0,3, ntpIP.toString());
delay(4000);
}
boolean wifiConnected() {
if (WiFi.status()==WL_CONNECTED) return true;
else return false;
}
#if !defined(DEVELOP)
void dbgPrint(String s) {
if (serialMon) Serial.print(s);
}
#endif
void dbgPrintln(String s) {
dbgPrint(s+F("\r\n"));
}
void handleNotFound(AsyncWebServerRequest *request){
request->send(404, "text/plain", F("File Not Found\n\n"));
}
void dispStatus(AsyncWebServerRequest *request) {
String fmt=dateFmtMD[dateFormat] + " %H:%M:%S.";
struct tm tim;
struct timeval tv;
char buf[40]={0};
String s= msgHome +
"<p>Pendulum Count=" + String(countPend) + " History=" + String(numHistory[0]) +
"<br>Error=" + csecToSecF(csecPendError) + " [" + csecToSecF(csecPendErrorMin) + ", " + csecToSecF(csecPendErrorMax) + "] sec" +
"<br>Current=" + String(adjLevel) + " [" + String(adjLevelMin) + ", " + String(adjLevelMax) + "] Ave=" + String(cadjLevelAve/100.0) +
"<br>Local IP=" + WiFi.localIP().toString() + "<br>NTP IP=" + ntpIP.toString() + (ntpByName?" (Pool)":"") +
"<br>Control Enabled=" + adjOn + " OnOff Mode=" + onoffMode +
"<br>Sensor: Min Off=" + String(minSensOffDuration) + " Min On=" + String(minSensOnDuration) + " msec" +
"<br>Penddulum window=" + String(pendWindow) + " msec";
strftime(buf, 20, fmt.c_str(), &tmAtRef0Local);
s+= "<br>Reference 0=" + String(buf) + preZero3(tvAtRef0.tv_usec/1000);
strftime(buf, 20, fmt.c_str(), &tmAtResetMMLocal);
s+= "<br>M/M. Reset=" + String(buf) + preZero3(tvAtResetMM.tv_usec/1000);
gettimeofday(&tv, NULL);
localtime_r(&tv.tv_sec, &tim);
strftime(buf, 20, fmt.c_str(), &tim);
s+= "<br>Now=" + String(buf) + preZero3(tv.tv_usec/1000);
#if defined(DEVELOP)
s+= "<br>Free Heap=" + String((int)esp_get_free_heap_size()) + " bytes" +
"<br>Task1 Free Stack=" + uxTaskGetStackHighWaterMark(tskHandle1) + " bytes" +
"<br>Loop Free Stack=" + uxTaskGetStackHighWaterMark(xTaskGetCurrentTaskHandle()) + " bytes" +
#endif
"</p>" + msgEnd;
request->send (200, textHtml, s);
}
String csecToSecF(int32_t csec) {
String s;
if (csec<0) s="-";
else s="";
return s + String(abs(csec)/100) + "." + preZero2((int)(abs(csec)%100));
}
String preZero2(int n) {
String s=String(n);
if (s.length()==1) return "0"+s;
else return s;
}
String preZero3(int n) {
String s=String(n);
int len=s.length();
if (len==1) return "00"+s;
else if (len==2) return "0"+s;
else return s;
}
void hand0Sec(AsyncWebServerRequest *request) {
String st;
if (!refSet) {
st=F("<h2>Time Ref not set</h2>");
request->send (200, textHtml, msgHome + st + msgEnd);
}
else {
funcPtrYes=hand0SecExe;
funcPtrNo=dispStatus;
st=F("<h2>Set 0 second?</h2>");
request->send (200, textHtml, msgHead + st + msgYesNo + msgEnd);
}
}
void hand0SecExe(AsyncWebServerRequest *request) {
struct tm tl;
struct timeval tv;
gettimeofday(&tv, NULL);
localtime_r(&tv.tv_sec, &tl);
time_t secDiff;
if (tl.tm_sec>40) secDiff=60-tl.tm_sec; //Clock hand ahead
else secDiff=-tl.tm_sec; //Clock hand behind
if (tvAtRef0.tv_usec<tv.tv_usec) tvAtRef0.tv_sec -= 1;
tvAtRef0.tv_sec += secDiff;
tvAtRef0.tv_usec -= tv.tv_usec;
updateError();
csecPendErrorLast=csecPendError;
String st="<h2>Ref0 adjusted</h2>";
request->send (200, textHtml, msgHome + st + msgEnd);
}
void resetMinMax(AsyncWebServerRequest *request) {
funcPtrYes=resetMinMaxExe;
funcPtrNo=dispStatus;
String st=F("<h2>Reset Min/Max for Error and Adj level?</h2>");
request->send (200, textHtml, msgHead + st + msgYesNo + msgEnd);
}
void resetMinMaxExe(AsyncWebServerRequest *request) {
adjLevelMax=adjLevel;
adjLevelMin=adjLevel;
csecPendErrorMax=csecPendError;
csecPendErrorMin=csecPendError;
gettimeofday(&tvAtResetMM, NULL);
localtime_r(&tvAtResetMM.tv_sec, &tmAtResetMMLocal);
String st= "<h2> Error M/M=" + csecToSecF(csecPendError) + " Adj M/M=" + String(adjLevel) + "</h2>";
request->send(200, textHtml, msgHome + st + msgEnd);
}
void zeroError(AsyncWebServerRequest *request) {
funcPtrYes=zeroErrorExe;
funcPtrNo=dispStatus;
String st=F("<h2>Error set to 0?</h2>");
request->send (200, textHtml, msgHead + st + msgYesNo + msgEnd);
}
void zeroErrorExe(AsyncWebServerRequest *request) {
time_t secErr = csecPendError/100;
int32_t usecErr = (csecPendError%100)*10000;
int32_t temp=tvAtRef0.tv_usec - usecErr;
int32_t carry=0;
if (temp>=1000000) {
temp -= 1000000;
carry=1;
}
else if (temp<0) {
temp += 1000000;
carry=-1;
}
tvAtRef0.tv_usec=temp;
tvAtRef0.tv_sec+=carry-secErr;
updateError();
csecPendErrorLast=csecPendError;
String st=F("<h2>Ref0 adjusted and Error set to 0</h2>");
request->send (200, textHtml, msgHome + st + msgEnd);
}
void graphNtp(AsyncWebServerRequest *request) {
String fmt=dateFmtMD[dateFormat] + " %H:%M:%S";
struct tm tim;
struct timeval tv;
char buf[40]={0};
int32_t hEnd=ntpNumHistory;
int32_t hStart=hEnd-N_NTP_HISTORY+1;
if (hStart<0) hStart=0;
String offsetData = "[";
String line3Data = "[";
String line4Data = "[";
String delayData = "[";
String passFailData= "[";
String secLabel="[";
for (int32_t i=hStart; i<=hEnd; i++) {
int32_t j=i%N_NTP_HISTORY;
offsetData += String(ntpOffsetHistory[j]) + ",";
line3Data += String(ntpData3History[j]) + ",";
line4Data += String(ntpData4History[j]) + ",";
delayData += String(ntpDelayHistory[j]) + ",";
passFailData += String(ntpPassHistory[j]) + ",";
secLabel += String(ntpTimeHistory[j]-ntpTimeHistory[hStart%N_NTP_HISTORY]) + ",";
if ((int)esp_get_free_heap_size()<MIN_HEAP) break;
}
offsetData += "];";
line3Data += "];";
line4Data += "];";
delayData += "];";
passFailData += "];";
secLabel += "];";
String stButton = "<form action='GYN' name='formGYN' onchange='changeVal()'><input type='submit' value='Reload'>\
Y1:<select name='Y1' style='font-size:50pt;width:230px;margin:0px'>";
String sel;
for (byte i=0; i<4; i++) {
sel=(ngY1scaleIdx==i)?"selected":"";
stButton += "<option " + sel + " value='" + String(i) + "'>" + String(ngY1scaleNum[i])+"ms" + "</option>";
}
stButton += "</select></form>&nbsp;";
stButton += "<form action='GR'><input type='submit' value='Accuracy'></form> \
<form action='MM'><input type='submit' value='Home'></form>";
localtime_r(&ntpTimeHistory[hStart%N_NTP_HISTORY], &tim);
strftime(buf, 40, fmt.c_str(), &tim);
String stgT = "<br>" + verLetter+verNum + " NTP status from "+ buf;
String stg = msgChartHead + "var ctx = document.getElementById('myChart').getContext('2d');";
stg += "var offsetData=" + offsetData;
stg += "var line3Data=" + line3Data;
stg += "var line4Data=" + line4Data;
stg += "var delayData=" + delayData;
stg += "var passFailData=" + passFailData;
stg += "var secLabel=" + secLabel;
stg += "var xLabel=[];var ptType=[]; var ptDelay=[]; var ptLine3=[]; var ptLine4=[]; var i; var y1max=" + String(ngY1scaleNum[ngY1scaleIdx]) + "; var y2max=" + String(ngY1scaleNum[ngY1scaleIdx]*10) +";";
stg += "for(i=0; i<=" + String(hEnd-hStart) + "; i++) {xLabel.push(" + String(ntpTimeHistory[hStart%N_NTP_HISTORY]) + "*1000 + secLabel[i]*1000);";
stg += "delayData[i] /=1000; offsetData[i] /=1000; line3Data[i] /=1000; line4Data[i] /=1000;";
stg += "ptType.push('circle');ptDelay.push('circle');ptLine3.push('circle');ptLine4.push('circle');";
stg += "if(offsetData[i]>y1max){offsetData[i]=y1max;ptType[i]='triangle';} if(offsetData[i]<-y1max){offsetData[i]=-y1max;ptType[i]='triangle';}";
stg += "if(delayData[i]>y2max){delayData[i]=y2max;ptDelay[i]='triangle';}";
stg += "if(line3Data[i]>y2max){line3Data[i]=y2max;ptLine3[i]='triangle';} if(line3Data[i]<-y2max){line3Data[i]=-y2max;ptLine3[i]='triangle';}";
stg += "if(line4Data[i]>y2max){line4Data[i]=y2max;ptLine4[i]='triangle';}";
stg += "if(passFailData[i]==0){ptDelay[i]='crossRot';}}";
stg += "var offsetLine={label:'<-Offset/min', showLine:true, pointRadius:1, lineTension:0, fill:false, borderWidth:3, borderColor:'blue', pointStyle:ptType, data:offsetData, yAxisID:'y1'};";
stg += "var delayLine={label:'Delay->', showLine:false, pointRadius:4, lineTension:0, fill:false, borderWidth:3, borderColor:'red', pointStyle:ptDelay, data:delayData, yAxisID:'y2'};";
stg += "var line3={label:'Offset->', showLine:false, pointRadius:4, lineTension:0, fill:false, borderWidth:3, borderColor:'green', pointStyle:ptLine3, data:line3Data, yAxisID:'y2'};";
stg += "var line4={label:'Limit->', showLine:true, pointRadius:1, lineTension:0, fill:false, borderWidth:3, borderColor:'red', pointStyle:ptLine4, data:line4Data, yAxisID:'y2'};";
stg += "var opt={scales: {xAxes: [{type:'time', time:{displayFormats:{millisecond:'kk:mm', second:'kk:mm', minute:'kk:mm', hour:'M/D ha'}}}],";
stg += "yAxes: [{id:'y1',type:'linear',position:'left',ticks:{min:-y1max, max:y1max}}, {id:'y2', type:'linear', position:'right', ticks:{min:-y2max, max:y2max}}]}};";
stg += "var config={type:'line', options: opt, data:{datasets: [offsetLine, line3, delayLine, line4], labels:xLabel}};";
stg += "var ntpChart = new Chart(ctx,config);";
stg += "function changeVal() {";
stg += "document.formGYN.submit();";
stg += "}";
stg += "</script>";
String st = "NTP IP=" + ntpIP.toString() + (ntpByName?" (Pool)":"");
st += "<br>NTP Sync=" + String(ntpSynchCount) + " Inc=" + String(ntpIntervalInc) + " Dec=" + String(ntpIntervalDec);
st += "<br>NTP Interval=" + String(ntpInterval) + "min";
snprintf(buf, 40, "<br>NTP Delay Limit=%6.2fms", ((float)ntpDelayLimitUs)/1000.0);
st += String(buf);
snprintf(buf, 40, "<br>Offset=%6.2f, Offset/min=%6.2fms", ((float)ntpOffset)/1000.0, ((float)ntpOffsetPerMin)/1000.0);
st += String(buf);
snprintf(buf, 40, "<br>Delay=%6.2f, Average=%6.2fms", ((float)ntpDelay)/1000.0, ntpDelayAve/1000.0);
st += String(buf);
localtime_r(&tvAtNtpFail.tv_sec, &tim);
strftime(buf, 20, fmt.c_str(), &tim);
st += "<br>NTP Fail=" + String(buf);
localtime_r(&tvAtNtpSuccess.tv_sec, &tim);
strftime(buf, 40, fmt.c_str(), &tim);
st += "<br>NTP Success=" + String(buf);
gettimeofday(&tv, NULL);
localtime_r(&tv.tv_sec, &tim);
strftime(buf, 40, fmt.c_str(), &tim);
st += "<br>Now=" + String(buf) + "<br>-- NTP Server History --";
for (uint16_t i=0; i<N_NTP_IP_HISTORY; i++) {
uint16_t idx=(ntpIPHistoryIdx-i)%N_NTP_IP_HISTORY;
localtime_r(&ntpIPTimeHistory[idx], &tim);
strftime(buf, 40, fmt.c_str(), &tim);
st += "<br>" + String(buf) + " " + ntpIPHistory[idx].toString();
if (idx==0) break;
}
#if defined(DEVELOP)
st += "<br>Free Heap=" + String((int)esp_get_free_heap_size()) + " bytes" + "<br>Free Stack=" + String(uxTaskGetStackHighWaterMark(xTaskGetCurrentTaskHandle())) + " bytes";
#endif
request->send (200, textHtml, msgHead + stButton + stgT + stg + st + msgEnd);
}
void setNtpGraphScale(AsyncWebServerRequest *request) {
if (request->hasParam("Y1")) {
String rq=request->getParam("Y1")->value();
ngY1scaleIdx=(byte)atoi(rq.c_str());
}
graphNtp(request);
}
void graphError(AsyncWebServerRequest *request) {
char buf[20]={0};
struct tm tsl, tel;
time_t tEnd=time(NULL);
time_t tStart=tEnd-graphSpan;
byte dataSet;
String sel;
gYCscaleMM="suggestedMin:-30, suggestedMax:30";
String msgG = "<form action='GU' name='formGU' onchange='changeVal()'>";
msgG += "X:<select name='XS' style='font-size:50pt;width:110px;margin:0px'>";
for (byte i=1; i<=24; i++) {
sel=(graphSpan/gSpanUnit[gSpanUnitIdx]==i)?"selected":"";
msgG += "<option " + sel + " value='" + String(i) + "'>" + String(i) + "</option>";
}
msgG += "</select><select name='XU' style='font-size:50pt;width:100px;margin:0px'>";
for (byte i=0; i<3; i++) {
sel=(gSpanUnitIdx==i)?"selected":"";
msgG += "<option " + sel + " value='" + String(i) + "'>" + gSpanUnitStr[i] + "</option>";
}
msgG += "</select>&nbsp;&nbsp;A:<select name='YE' style='font-size:50pt;width:200px;margin:0px'>";
for (byte i=0; i<5; i++) {
sel=(gYEscaleIdx==i)?"selected":"";
msgG += "<option " + sel + " value='" + String(i) + "'>" + gYEscaleStr[i] + "</option>";
}
msgG += "</select>&nbsp;&nbsp;C:<select name='YC' style='font-size:50pt;width:200px'>";
for (byte i=0; i<5; i++) {
sel=(gYCscaleIdx==i)?"selected":"";
msgG += "<option " + sel + " value='" + String(i) + "'>" + gYCscaleStr[i] + "</option>";
}
msgG += "</select><br><input type='submit' value='Reload'></form> \
<form action='GN'><input type='submit' value=' NTP '></form> \
<form action='MM'><input type='submit' value='Home'></form>";
if (gYEscaleIdx==0) gYEscaleMM="suggestedMin:-5, suggestedMax:5";
else gYEscaleMM="min:-" + String(gYEscale) + ", max:" + String(gYEscale);
if (gYCscaleIdx==0) gYCscaleMM="suggestedMin:-500, suggestedMax:500";
else gYCscaleMM="min:-" + String(gYCscale) + ", max:" + String(gYCscale);
if (numHistory[0]<N_HISTORY) dataSet=0;
else if (tHistory[0][(numHistory[0]+1)%N_HISTORY]<tStart) dataSet=0;
else dataSet=1;
int32_t hEnd=numHistory[dataSet];
int32_t hStart;
if (graphSpan>14*24*3600) { //longest
if (numHistory[dataSet]<N_HISTORY) hStart=0;
else hStart=hEnd-N_HISTORY;
}
else {
int16_t count=N_HISTORY;
if (numHistory[dataSet]<N_HISTORY) count=numHistory[dataSet]+1;
hStart=hEnd;
for (int16_t i=0; i<count; i++) {
if (tHistory[dataSet][(N_HISTORY+hEnd-i)%N_HISTORY]<tStart) break;
hStart=hEnd-i;
}
}
tStart=tHistory[dataSet][hStart%N_HISTORY];
String outData = "[";
String errData = "[";
String secLabel="[";
for (int32_t i=hStart; i<=hEnd; i++) {
int32_t j=i%N_HISTORY;
outData += String(adjHistory[dataSet][j]) + ",";
errData += String(errorHistory[dataSet][j]) + ",";
secLabel += String(tHistory[dataSet][j]-tStart) + ",";
if ((int)esp_get_free_heap_size()<MIN_HEAP) break;
}
outData += "];";
errData += "];";
secLabel += "];";
localtime_r(&tStart, &tsl);
strftime(buf, 20, (dateFmtMD[dateFormat] + " %H:%M").c_str(), &tsl);
String stgT = "<br>" + verLetter+verNum + " From "+ buf + " Dataset " + String(dataSet);
String stg = msgChartHead;
stg += "var ctx = document.getElementById('myChart').getContext('2d');";
stg += "var errData=" + errData;
stg += "var outData=" + outData;
stg += "var secLabel=" + secLabel;
stg += "var xLabel=[]; var i;";
stg += "for (i=0; i<=" + String(hEnd-hStart) + "; i++) {xLabel.push(" + String(tStart) + "*1000 + secLabel[i]*1000);errData[i]/=100;}";
stg += "var errLine={label:'<-Accuracy(sec)', showLine:true, lineTension:0, fill:false, borderWidth:4, borderColor:'blue', pointRadius:0, data:errData, yAxisID:'yE'};";
stg += "var outLine={label:'Current->', showLine:true, lineTension:0, fill:false, borderWidth:2, borderColor:'red', pointRadius:0, data:outData, yAxisID:'yA'};";
stg += "var opt={scales: {xAxes: [{type: 'time', time:{min:" + String(gmtNow-graphSpan) + "*000,displayFormats:{millisecond:'kk:mm',second:'kk:mm',minute:'kk:mm',hour:'M/D ha',}}}],";
stg += "yAxes: [{id:'yE',type:'linear',position:'left',ticks:{" + gYEscaleMM + "}},{id:'yA',type:'linear',position:'right',ticks:{" + gYCscaleMM + "}}]}};";
stg += "var config={type:'line', options: opt, data: {datasets: [errLine, outLine], labels: xLabel}};";
stg += "if (errChart) errChart.destroy();";
stg += "else var errChart = new Chart(ctx,config);";
stg += "function changeVal() {";
stg += "document.formGU.submit();";
stg += "}";
stg += "</script>";
String st = "Pend Ct=" + String(countPend) + " Hist=" + String(numHistory[0]) +
"<br>Error=" + csecToSecF(csecPendError) + " [" + csecToSecF(csecPendErrorMin) + ", " + csecToSecF(csecPendErrorMax) + "] sec" +
"<br>Current=" + String(adjLevel) + " [" + String(adjLevelMin) + ", " + String(adjLevelMax) + "] Ave=" + String(cadjLevelAve/100.0);
#if defined(DEVELOP)
st += "<br>Free Heap=" + String((int)esp_get_free_heap_size()) + " bytes" + "<br>Free Stack=" + String(uxTaskGetStackHighWaterMark(xTaskGetCurrentTaskHandle())) + " bytes";
#endif
request->send(200, textHtml, msgHead + msgG + stgT + stg + st + msgEnd);
}
void updateErroGraph(AsyncWebServerRequest *request) {
if (request->hasParam("YC")) {
String rq=request->getParam("XU")->value();
gSpanUnitIdx=(byte)atoi(rq.c_str());
rq=request->getParam("XS")->value();
graphSpan=atoi(rq.c_str())*gSpanUnit[gSpanUnitIdx];
rq=request->getParam("YE")->value();
gYEscaleIdx=atoi(rq.c_str());
gYEscale=gYEscaleNum[gYEscaleIdx];
rq=request->getParam("YC")->value();
gYCscaleIdx=atoi(rq.c_str());
gYCscale=gYCscaleNum[gYCscaleIdx];
}
graphError(request);
}
void adjControl(AsyncWebServerRequest *request) {
funcPtrWeb=adjControlExe;
funcReqWeb=request;
}
void adjControlExe(AsyncWebServerRequest *request) {
if (adjOn) {
adjOn=false;
lcdDispText(0,1, "OUT= ");
}
else {
adjOn=true;
lcdDispText(0,1, " ");
}
String st = "<h2>Control= " + String(adjOn) + "</h2>";
request->send (200, textHtml, msgHome + st + msgEnd);
}
void adjUp(AsyncWebServerRequest *request){
if (adjLevel<=ADJ_MAX-5*(ADJ_MAX+1)*5/512) adjLevel += 5*(ADJ_MAX+1)*5/512;
cadjLevel=adjLevel*100;
drvCoil(adjLevel);
dispAdj(request);
}
void adjDwn(AsyncWebServerRequest *request){
if (adjLevel>=ADJ_MIN+5*(1-ADJ_MIN)*5/512) adjLevel -= 5*(1-ADJ_MIN)*5/512;
cadjLevel=adjLevel*100;
drvCoil(adjLevel);
dispAdj(request);
}
void dispAdj(AsyncWebServerRequest *request) {
funcPtrWeb=dispAdjExe;
funcReqWeb=request;
}
void dispAdjExe(AsyncWebServerRequest *request) {
lcdDispNum4n(9,1,adjLevel);
String st = "<h2>Current= " + String(adjLevel) + " [" + String(adjLevelMin) + ", " + String(adjLevelMax) + "]</h2>";
request->send (200, textHtml, msgHome + st + msgEnd);
}
void confMenu(AsyncWebServerRequest *request) {
setConfigStr();
setConfigMsg();
request->send (200, textHtml, msgConf + msgEnd);
}
void setOutput(AsyncWebServerRequest *request) {
if (request->hasParam("OV")) {
String rq=request->getParam("OV")->value();
adjLevel = atoi(rq.c_str());
cadjLevel=adjLevel*100;
drvCoil(adjLevel);
dispAdj(request);
}
else request->send (200, textHtml, msgConf + "Illegal format" + msgEnd);
}
void setError(AsyncWebServerRequest *request) {
if (request->hasParam("EV")) {
funcPtrYes=setErrorExe;
funcPtrNo=dispStatus;
String rq=request->getParam("EV")->value();
float fnum=atof(rq.c_str());
secTmAdj=(int32_t)fnum;
usecTmAdj=(int32_t)((fnum-(float)secTmAdj)*1000000);
String st="<h2>Adjust Error by " + rq + "sec?</h2>";
request->send (200, textHtml, msgHead + st + msgYesNo + msgEnd);
}
else request->send (200, textHtml, msgConf + "Illegal format" + msgEnd);
}
void setErrorExe(AsyncWebServerRequest *request) {
int32_t temp=tvAtRef0.tv_usec + usecTmAdj;
int32_t carry=0;
if (temp>=1000000) {
temp -= 1000000;
carry=1;
}
else if (temp<0) {
temp += 1000000;
carry=-1;
}
tvAtRef0.tv_usec=temp;
tvAtRef0.tv_sec += carry+secTmAdj;
updateError();
csecPendErrorLast=csecPendError;
String st="<h2>Ref0 adjusted and Error set to " + csecToSecF(csecPendError) + "</h2>";
request->send (200, textHtml, msgHome + st + msgEnd);
}
void applyConfig(AsyncWebServerRequest *request) {
if (request->hasParam("DF")) { //last parameter (date format)
pendCycle = (int32_t)atoi(request->getParam("PC")->value().c_str());
pendDur = (int32_t)atoi(request->getParam("PD")->value().c_str());
pendTuneCycle = atoi(request->getParam("PT")->value().c_str());
setupMechTiming();
kP = atoi(request->getParam("KP")->value().c_str());
kI = atoi(request->getParam("KI")->value().c_str());
liftFactor = atoi(request->getParam("LF")->value().c_str());
wifiSSID = request->getParam("WS")->value();
wifiPass = request->getParam("WP")->value();
if (request->getParam("IF")->value().charAt(0)=='1') {
staIpFixed=true;
}
else {
staIpFixed=false;
}
staIP.fromString(request->getParam("IL")->value());
staGateway.fromString(request->getParam("IG")->value());
staDNS.fromString(request->getParam("ID")->value());
staSNM.fromString(request->getParam("IS")->value());
boolean ntpByNameLast=ntpByName;
if (request->getParam("NF")->value().charAt(0)=='0') ntpByName=true;
else ntpByName=false;
ntpIPbyIP.fromString(request->getParam("NI")->value());
ntpServerName=request->getParam("NN")->value();
ntpMinInterval=(uint16_t)atoi(request->getParam("NV")->value().c_str());
ntpMaxInterval=(uint16_t)atoi(request->getParam("NX")->value().c_str());
if (ntpInterval>ntpMaxInterval) ntpInterval=ntpMaxInterval;
if (ntpInterval<ntpMinInterval) ntpInterval=ntpMinInterval;
if (ntpByNameLast != ntpByName) ntpChange();
String tzStrLast=tzStr;
tzStr=request->getParam("TZ")->value();
if (strcmp(tzStrLast.c_str(), tzStr.c_str())!=0) setDst();
dateFormat=(byte)atoi(request->getParam("DF")->value().c_str());
saveConfigFile();
request->send (200, textHtml, msgHome + "<br><h2>Done</h2>" + msgEnd);
}
else {
request->send (200, textHtml, msgHome + "<br><h2>Transmission Error</h2>" + msgEnd);
}
}
boolean saveConfigFile() {
File file = SPIFFS.open(CONFIG_FILE, FILE_WRITE);
if (!file) return false;
setConfigStr();
file.println(fileVer);
file.println(strPC);
file.println(strPD);
file.println(strPT);
file.println(strKP);
file.println(strKI);
file.println(strLF);
file.println(strWS);
file.println(strWP);
file.println(strIF);
file.println(strIL);
file.println(strIG);
file.println(strID);
file.println(strIS);
file.println(strNF);
file.println(strNI);
file.println(strNN);
file.println(strNV);
file.println(strNX);
file.println(strTZ);
file.println(strDF);
file.close();
return true;
}
void setConfigStr() {
strPC=String(pendCycle);
strPD=String(pendDur);
strPT=String(pendTuneCycle);
strKP=String(kP);
strKI=String(kI);
strLF=String(liftFactor);
strWS=wifiSSID;
strWP=wifiPass;
strIF=staIpFixed?"1":"0";
strIL=staIP.toString();
strIG=staGateway.toString();
strID=staDNS.toString();
strIS=staSNM.toString();
strNF=ntpByName?"0":"1";
strNI=ntpIPbyIP.toString();
strNN=ntpServerName;
strNV=String(ntpMinInterval);
strNX=String(ntpMaxInterval);
strTZ=tzStr;
strDF=String(dateFormat);
}
void confDefault(AsyncWebServerRequest *request) {
funcPtrYes=confDefaultExe;
funcPtrNo=dispStatus;
request->send (200, textHtml, msgHead + "<h2>Use Factory Setting?</h2><br><br>" + msgYesNo + msgEnd);
}
void confDefaultExe(AsyncWebServerRequest *request) {
loadDefaultConfig();
setConfigMsg();
request->send (200, textHtml, msgConf + "<br><h2>Default loaded but NOT applied</h2>" + msgEnd);
}
void setConfigPara() {
pendCycle=(int32_t)(atoi(strPC.c_str()));
pendDur=(int32_t)(atoi(strPD.c_str()));
pendTuneCycle=(int16_t)(atoi(strPT.c_str()));
kP=(int16_t)(atoi(strKP.c_str()));
kI=(int16_t)(atoi(strKI.c_str()));
liftFactor=(int16_t)(atoi(strLF.c_str()));
wifiSSID=strWS;
wifiPass=strWP;
staIpFixed=(strIF.charAt(0)=='1');
staIP.fromString(strIL);
staGateway.fromString(strIG);
staDNS.fromString(strID);
staSNM.fromString(strIS);
ntpByName=(strNF.charAt(0)=='0');
ntpIPbyIP.fromString(strNI);
ntpIP=ntpIPbyIP;
ntpServerName=strNN;
ntpMinInterval=(int16_t)(atoi(strNV.c_str()));
ntpMaxInterval=(int16_t)(atoi(strNX.c_str()));
tzStr=strTZ;
dateFormat=(byte)atoi(strDF.c_str());
}
void loadDefaultConfig() {
strPC=String(DEFAULT_PEND_CYCLE);
strPD=String(DEFAULT_PEND_DUR);
strPT=String(DEFAULT_TUNE_CYCLE);
strKP=String(DEFAULT_KP);
strKI=String(DEFAULT_KI);
strLF=String(DEFAULT_PEND_LF);
strWS=DEFAULT_WIFI_SSID;
strWP=DEFAULT_WIFI_PASS;
strIF=DEFAULT_STA_IP_FIX;
strIL=DEFAULT_STA_IP;
strIG=DEFAULT_STA_GW;
strID=DEFAULT_STA_DNS;
strIS=DEFAULT_SUBNET;
strNF=DEFAULT_NTP_BY_NAME?"0":"1";
strNI=DEFAULT_NTP_IP;
strNN=DEFAULT_NTP_NAME;
strNV=String(DEFAULT_NTP_MIN_INTERVAL);
strNX=String(DEFAULT_NTP_MAX_INTERVAL);
strTZ=DEFAULT_TZ_STR;
strDF=String(DEFAULT_DATE_FORMAT);
}
void setConfigMsg() {
msgConf= msgHead + "<script type='text/javascript'>\
function funcFixed() {document.getElementById('IF').style.display = 'inline';\
document.getElementById('radio1').checked = true;}\
function funcDHCP() {document.getElementById('IF').style.display = 'none';\
document.getElementById('radio2').checked = true;}\
function funcNtpF() {document.getElementById('NF').style.display = 'inline';\
document.getElementById('NN').style.display = 'none';\
document.getElementById('radio3').checked = true;}\
function funcNtpN() {document.getElementById('NF').style.display = 'none';\
document.getElementById('NN').style.display = 'inline';\
document.getElementById('radio4').checked = true;}\
window.onload = function() {" + String(strIF.charAt(0)=='1'?"funcFixed();":"funcDHCP();") +
String(strNF.charAt(0)=='1'?"funcNtpF();":"funcNtpN();") +
"document.getElementById('DF').value='" + strDF.charAt(0) + "';};</script>\
<form action='MM'><input type='submit' value='Home'></form> \
<form action='CD'><input type='submit' value='Factory'></form> \
<form action='CB'><input type='submit' value='Reboot'></form> \
<form id='form1' action='CA'><button type=submit' form='form1'>Apply</button><br>\
<hr width='800px'>\
Pendulum:<input id='PC' type='number' min='1' name='PC' value='" + strPC + "'>cycles=\
<input type='number' min='1' name='PD' value='" + strPD + "'>sec\
<br>Pend Cycle between Tune:<input type='number' min='1' name='PT' value='" + strPT + "'>\
<br>KP:<input type='number' min='0' name='KP' value='" + strKP + "'>&nbsp;\
KI:<input type='number' min='0' name='KI' value='" + strKI + "'>\
Lift Limit:1/<input style='width:50px' type='number' min='1' max='9' name='LF' value='" + strLF + "'>\
<br><hr width='800px'>\
<span>WiFi SSID:</span><input type='text' name='WS' value='" + strWS + "'><br>\
<span>WiFi Pass:</span><input type='text' name='WP' value='" + strWP + "'><br>\
<hr width='800px'>\
<input type='radio' id='radio1' name='IF' value='1' onclick='funcFixed()' />\
<label for='radio1'><span style='width:250px;text-align:left'>Fixed IP</span></label>\
<input type='radio' id='radio2' name='IF' value='0' onclick='funcDHCP()' />\
<label for='radio2'><span style='width:300px;text-align:left'>DHCP</span></label>\
<span id='IF'><br>\
<span>Local IP:</span><input type='text' name='IL' value='" + strIL + "'><br>\
<span>Gateway IP:</span><input type='text' name='IG' value='" + strIG + "'><br>\
<span>DNS IP:</span><input type='text' name='ID' value='" + strID + "'><br>\
<span>Subnet Mask:</span><input type='text' name='IS' value='" + strIS + "'></span><br>\
<hr width='800px'>\
<input type='radio' id='radio3' name='NF' value='1' onclick='funcNtpF()' />\
<label for='radio3'><span style='width:250px;text-align:left'>NTP IP</span></label>\
<input type='radio' id='radio4' name='NF' value='0' onclick='funcNtpN()' />\
<label for='radio4'><span style='width:300px;text-align:left'>NTP Name</span></label>\
<span id='NF'><br><span>NTP IP:</span><input type='text' name='NI' value='" + strNI + "'></span>\
<span id='NN'><br><span>NTP Name:</span><input type='text' name='NN' value='" +strNN + "'></span><br>\
Interval Min:<input type='number' min='1' max='1440' name='NV' value='" + strNV + "'> \
Max:<input type='number' min='1' max='1440' step='1' name='NX' value='" + strNX + "'>min<br>\
<hr width='800px'>\
<span>TZ/DST:</span><input type='text' list='tzList' name='TZ' value='" + strTZ + "' placeholder='Select or type in'><br>\
<datalist id='tzList'>\
<option value='" + TZ_STR0 + "'>\
<option value='" + TZ_STR1 + "'>\
<option value='" + TZ_STR2 + "'>\
<option value='" + TZ_STR3 + "'>\
</datalist>\
<span>Date Format:</span><select id='DF' name='DF' form='form1' style='width:500px;font-size:36pt;margin-right:50px'>\
<option value='0'>Select format</option>\
<option value='1'>" + dateStr[1] + "</option>\
<option value='2'>" + dateStr[2] + "</option>\
<option value='3'>" + dateStr[3] + "</option></select><br></form>"; //when changed, make sure to check that bottom alignment
#if defined(DEVELOP)
msgConf += "<h2>Free Heap=" + String((int)esp_get_free_heap_size()) + " bytes" +
"<br>Free Stack=" + String(uxTaskGetStackHighWaterMark(xTaskGetCurrentTaskHandle())) + " bytes</h2>";
#endif
}
boolean loadConfigFile() {
File file = SPIFFS.open(CONFIG_FILE, FILE_READ);
if (!file) return false;
if (file.available()) {
strVer=file.readStringUntil('\r');file.read();
if (strVer.compareTo(fileVer)!=0) {
file.close();
return false;
}
strPC=file.readStringUntil('\r');file.read();
strPD=file.readStringUntil('\r');file.read();
strPT=file.readStringUntil('\r');file.read();
strKP=file.readStringUntil('\r');file.read();
strKI=file.readStringUntil('\r');file.read();
strLF=file.readStringUntil('\r');file.read();
strWS=file.readStringUntil('\r');file.read();
strWP=file.readStringUntil('\r');file.read();
strIF=file.readStringUntil('\r');file.read();
strIL=file.readStringUntil('\r');file.read();
strIG=file.readStringUntil('\r');file.read();
strID=file.readStringUntil('\r');file.read();
strIS=file.readStringUntil('\r');file.read();
strNF=file.readStringUntil('\r');file.read();
strNI=file.readStringUntil('\r');file.read();
strNN=file.readStringUntil('\r');file.read();
strNV=file.readStringUntil('\r');file.read();
strNX=file.readStringUntil('\r');file.read();
strTZ=file.readStringUntil('\r');file.read();
strDF=file.readStringUntil('\r');
file.close();
return true;
}
}
void confLoad(AsyncWebServerRequest *request) {
loadConfigFile();
setConfigMsg();
request->send (200, textHtml, msgConf + msgEnd);
}
void confReboot(AsyncWebServerRequest *request) {
funcPtrYes=espReset;
funcPtrNo=confMenu;
request->send (200, textHtml, msgHead + "<h2>CPU Reset?</h2><br><br>" + msgYesNo + msgEnd);
}
void espReset(AsyncWebServerRequest *request) {
funcPtrYes=dispStatus;
request->send (200, textHtml, msgHead + "<h2>CPU Resetting...</h2>" + msgEnd);
ESP.restart();
}
void ansYes(AsyncWebServerRequest *request) {
if (funcPtrYes) funcPtrYes(request);
else dispStatus(request);
}
void ansNo(AsyncWebServerRequest *request) {
if (funcPtrNo) funcPtrNo(request);
else dispStatus(request);
}
void sensAdjOn(AsyncWebServerRequest *request) {
sensAdjMode=true;
oled.clear();
funcPtrYes=sensAdjOff;
funcPtrNo=sensAdjOn;
request->send (200, textHtml, msgHead + "<h2>End Sensor Adj mode?</h2><br><br>" + msgYesNo + msgEnd);
}
void sensAdjOff(AsyncWebServerRequest *request) {
sensAdjMode=false;
dispStatus(request);
}
void setupServer() {
server.on("/", dispStatus);
server.on("/TC", adjControl);
server.on("/SO", setOutput);
server.on("/H0", hand0Sec);
server.on("/SE", setError);
server.on("/EZ", zeroError);
server.on("/MR", resetMinMax);
server.on("/ST", dispStatus);
server.on("/GU", updateErroGraph);
server.on("/GR", graphError);
server.on("/GYN", setNtpGraphScale);
server.on("/CF", confMenu);
server.on("/CA", applyConfig);
server.on("/CD", confDefault);
server.on("/MM", dispStatus);
server.on("/GN", graphNtp);
server.on("/CB", confReboot);
server.on("/SA", sensAdjOn);
server.on("/YS", ansYes);
server.on("/NO", ansNo);
server.onNotFound(handleNotFound);
server.begin();
}
void lcdDispText(int16_t col, int16_t row, String s) {
lcdDispText2(col, row, s, 128);
}
void lcdDispText2(int16_t col, int16_t row, String s, int16_t width) {
oled.setColor(BLACK);
oled.fillRect(col*CHAR_W, row*CHAR_H, width, 18);
oled.setColor(WHITE);
oled.drawString(col*CHAR_W, row*CHAR_H, s);
oled.display();
}
void lcdDispNumF(int16_t col, int16_t row, int32_t n) {
char pNum[10]={0};
if (n>9999999) snprintf(pNum, 10, "%s", "+>");
else if (n<-999999) snprintf(pNum, 10, "%s", "-<");
else snprintf(pNum, 10, "%08.2f", ((float)n)/100.0);
oled.setColor(BLACK);
oled.fillRect(col*CHAR_W, row*CHAR_H, 72, 16);
oled.setColor(WHITE);
oled.drawString(col*CHAR_W, row*CHAR_H, pNum);
oled.display();
}
void lcdDispNum3n(int16_t col, int16_t row, int32_t n) {
char pNum[5]={0};
if (abs(n)<=999) snprintf(pNum, 5, "%3d", n);
else snprintf(pNum, 5, "%s", "***");
oled.setColor(BLACK);
oled.fillRect(col*CHAR_W, row*CHAR_H, 30, 16);
oled.setColor(WHITE);
oled.drawString(col*CHAR_W, row*CHAR_H, pNum);
oled.display();
}
void lcdDispNum4n(int16_t col, int16_t row, int32_t n) {
char pNum[6]={0};
if (abs(n)<=9999) snprintf(pNum, 6, "%4d", n);
else snprintf(pNum, 6, "%s", "***");
oled.setColor(BLACK);
oled.fillRect(col*CHAR_W, row*CHAR_H, 44, 16);
oled.setColor(WHITE);
oled.drawString(col*CHAR_W, row*CHAR_H, pNum);
oled.display();
}
void lcdDispTime(int16_t col, int16_t row, byte mode) { //mode1=data and time, mode2=time only
struct tm tt;
localtime_r(&gmtAtSec, &tt);
tzDst=tt.tm_isdst;
char buf[20]={0};
char tim[20]={0};
if (mode==1) strftime(buf, 20, dateFmtMD[dateFormat].c_str(), &tt);
snprintf(tim, 20, "%s %02d:%02d:%02d", buf, tt.tm_hour, tt.tm_min, tt.tm_sec);
oled.setColor(BLACK);
oled.fillRect(col*CHAR_W, row*CHAR_H, 128, 16);
oled.setColor(WHITE);
oled.drawString(col*CHAR_W, row*CHAR_H, tim);
if (tzDst==0 && tzDstObs) oled.drawCircle(123, 9, 4);
else if (tzDst>0) oled.fillCircle(123, 9, 4);
oled.display();
}
void lcdDispGraph() {
#if defined(HMC5883) || defined(LSM303)
if (sensAdjMode) return;
#endif
int32_t eMax=1;
int16_t aMax=1, diff;
byte idxA, idxE;
int32_t hStart=numHistory[0]-30; //SCREEN_W; //use the last 30 points for scaling
if (hStart<0) hStart=0;
for (int32_t i=hStart; i<=numHistory[0]; i++) {
if (abs(errorHistory[0][i%N_HISTORY])>eMax) eMax=abs(errorHistory[0][i%N_HISTORY]);
diff=abs(adjHistory[0][i%N_HISTORY]-adjLevel);
if (diff>aMax) aMax=diff;
}
for (idxE=0; idxE<numgE; idxE++) {
if (gscaleE[idxE]>=eMax) break;
}
if (idxE>=numgE) idxE=numgE-1;
for (idxA=0; idxA<numgA; idxA++) {
if (gscaleA[idxA]>=aMax) break;
}
if (idxA>=numgA) idxA=numgA-1;
clearGraph();
oled.setColor(WHITE);
oled.drawLine(0, GYCENTER, GXORIGIN, GYCENTER);
oled.drawLine(GXORIGIN, GYCENTER-16, GXORIGIN, GYCENTER+16);
oled.drawLine(GXORIGIN, GYCENTER-10, GXORIGIN+2, GYCENTER-10);
oled.drawLine(GXORIGIN, GYCENTER+10, GXORIGIN+2, GYCENTER+10);
oled.setFont(ArialMT_Plain_10);
String scaleMark;
if ((lcdGmode==0) && (gmtAtSec%10<5) || (lcdGmode==1)) {
if (gscaleE[idxE]>=100) scaleMark=String(gscaleE[idxE]/100)+"s";
else scaleMark=String(gscaleE[idxE]*10)+"ms";
oled.drawString(GXORIGIN+4, GYCENTER-16, "+"+scaleMark);
oled.drawString(GXORIGIN+4, GYCENTER+4, "-"+scaleMark);
}
else if ((lcdGmode==0) && (gmtAtSec%10>=5) || (lcdGmode==2)) {
oled.drawString(GXORIGIN+4, GYCENTER-16, String(gscaleA[idxA]+(adjLevel/10)*10));
oled.drawString(GXORIGIN+4, GYCENTER+4, String(-gscaleA[idxA]+(adjLevel/10)*10));
}
oled.setFont(OLEDFONT);
time_t endSec=tHistory[0][numHistory[0]%N_HISTORY];
int16_t dotPerHour=3600/tuneInterval;
for (int i=1; i<=GRAPH_W; i++) {
if (i%4==0) {
oled.setPixel(GRAPH_W-i, GTOP);
oled.setPixel(GRAPH_W-i, GBOTTOM);
}
if ((i-1)%dotPerHour==0) { //every 1 hour back from now
for (int y=GTOP; y<=GBOTTOM; y++) {
if (y%4==0) oled.setPixel(GRAPH_W-i, y);
}
}
}
oled.setColor(WHITE);
int32_t yE;
int16_t yA;
hStart=numHistory[0]-GRAPH_W;
int16_t offset=0;
if (hStart<0) {
hStart=0;
offset=GRAPH_W-numHistory[0];
}
if ((lcdGmode==0) && (gmtAtSec%10<5) || (lcdGmode==1)) {
for (int32_t i=hStart; i<=numHistory[0]; i++) {
yE=errorHistory[0][i%N_HISTORY]*10/gscaleE[idxE];
if (abs(yE)<=GBOTTOM-GYCENTER) oled.setPixel(GXORIGIN+i-hStart+offset, GYCENTER-yE);
if (i==numHistory[0]) oled.drawCircle(GXORIGIN+i-hStart+offset, constrain(GYCENTER-csecPendError*10/gscaleE[idxE], GTOP, GBOTTOM), 2);
}
for (int x=GXORIGIN; x<GXORIGIN+GRAPH_W; x++) {
if (x%2==0) oled.setPixel(x, GYCENTER);
}
}
else if ((lcdGmode==0) && (gmtAtSec%10>=5) || (lcdGmode==2)) {
for (int32_t i=hStart; i<=numHistory[0]; i++) {
yA=(adjHistory[0][i%N_HISTORY]-adjLevel)*10/gscaleA[idxA];
if (abs(yA)<=GBOTTOM-GYCENTER) oled.setPixel(GXORIGIN+i-hStart+offset, GYCENTER-yA);
}
if (adjLevel>=0) oled.fillRect(0, GYCENTER-16*adjLevel/ADJ_MAX, GXORIGIN, 16*adjLevel/ADJ_MAX);
else oled.fillRect(0, GYCENTER, GXORIGIN, -16*adjLevel/ADJ_MAX);
int16_t yZeroPos=GYCENTER+10*adjLevel/gscaleA[idxA];
if (yZeroPos>=GTOP && yZeroPos<=GBOTTOM) {
for (int x=GXORIGIN; x<GXORIGIN+GRAPH_W; x++) {
if (x%2==0) oled.setPixel(x, yZeroPos);
}
}
}
}
void clearGraph() {
oled.setColor(BLACK);
oled.fillRect(0, GTOP-2, 128, GBOTTOM-GTOP+3);
}
void setupOled() {
oled.init();
oled.setFont(ArialMT_Plain_16);
}
void setupOta() {
ArduinoOTA.begin();
ArduinoOTA.onStart([]() {
detachInterrupt(digitalPinToInterrupt(SENS_PIN)); //######### most likely not need this.
oled.clear();
oled.setFont(ArialMT_Plain_10);
oled.setTextAlignment(TEXT_ALIGN_CENTER_BOTH);
oled.drawString(oled.getWidth()/2, oled.getHeight()/2 - 10, "OTA Update");
oled.display();
});
ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
oled.drawProgressBar(4, 32, 120, 8, progress / (total / 100) );
oled.display();
});
ArduinoOTA.onEnd([]() {
oled.clear();
oled.setFont(ArialMT_Plain_10);
oled.setTextAlignment(TEXT_ALIGN_CENTER_BOTH);
oled.drawString(oled.getWidth()/2, oled.getHeight()/2, "Restart");
oled.display();
});
}
void setupFile() {
SPIFFS.begin();
if (!SPIFFS.exists(CONFIG_FILE)) {
SPIFFS.format();
}
}
#ifdef HMC5883
void magTrigH() {
if (pendDir==0 && (millis()-milliSensRelease)>minSensOffDuration) {
pendDir=1;
if (!sensAdjMode) {
if (tzDst>0) digitalWrite(LED_PIN, LOW); //indicates DST
else digitalWrite(LED_PIN, HIGH);
}
sensTrig=true;
milliSensTrig=millis();
if (refSet) {
countPend++;
csecPend=(int64_t)countPend * (int64_t)pendDur * 100 / (int64_t)pendCycle;
}
}
}
void magTrigL() {
if (pendDir==1 && (millis()-milliSensTrig)>minSensOnDuration) {
pendDir=0;
if (!sensAdjMode) {
if (tzDst>0) digitalWrite(LED_PIN, HIGH);
else digitalWrite(LED_PIN, LOW);
}
sensRelease=true;
milliSensRelease=millis();
}
}
void setupHMC5883() {
Wire.beginTransmission(MAG_ADDR); //start talking
Wire.write(0x02); // Set the Register
Wire.write(0x00); // Tell the HMC5883 to Continuously Measure
Wire.endTransmission();
}
void getHMC5883() {
Wire.beginTransmission(MAG_ADDR);
Wire.write(0x03); //start with register 3.
Wire.endTransmission();
Wire.requestFrom(MAG_ADDR, 6);
if(Wire.available()>=6) {
magX = Wire.read()<<8; //MSB x
magX |= Wire.read(); //LSB x
magZ = Wire.read()<<8; //MSB z
magZ |= Wire.read(); //LSB z
magY = Wire.read()<<8; //MSB y
magY |= Wire.read(); //LSB y
}
}
byte readHMC5883(byte reg) {
Wire.beginTransmission(MAG_ADDR);
Wire.write(reg);
Wire.endTransmission();
Wire.requestFrom(MAG_ADDR, 1);
return Wire.read();
}
#endif
#ifdef LSM303
void setupLSM303() {
writeLSM303(0x20, 0x5C); //XY high perforamnce 80hz conversion
writeLSM303(0x21, 0x60); //full scale +/- 16 gauss
writeLSM303(0x22, 0x00); //eable continuous conversion
writeLSM303(0x23, 0x00); //Z low-power mode, little endian
writeLSM303(0x30, 0x8D); //interrupt by Y:4D X:8D, high true, not latched (manual typo)
writeLSM303(0x32, MAG_THRESH_LB); //thresh low byte
writeLSM303(0x33, MAG_THRESH_HB); //thresh high byte, thresh=HB*256+LB
}
void writeLSM303(byte reg, byte value) {
Wire.beginTransmission(MAG_ADDR);
Wire.write(reg);
Wire.write(value);
i2cStatus = Wire.endTransmission();
}
int16_t readLSM303(byte addr) {
Wire.beginTransmission(MAG_ADDR);
Wire.write(addr);
i2cStatus = Wire.endTransmission();
Wire.requestFrom(MAG_ADDR, 2);
uint32_t mstart = millis();
while (Wire.available() < 2) {
if (i2cTimeout > 0 && (millis() - mstart) > i2cTimeout) return 0;
}
return (int16_t)(Wire.read() | Wire.read()<<8);
}
#endif
void setupMechTiming() {
float pendPeriod=(float)pendDur * 1000.0 / (float)pendCycle; //msec/cycle
pendPeriodMs=(uint32_t)pendPeriod;
minSensOffDuration = (int32_t)(pendPeriod * (pendPeriod<4000?0.5:0.75));
minSensOnDuration = (int32_t)(pendPeriod / 100.0);
pendWindow = (int32_t)(pendPeriod * 1.3 + 1000.0);
tuneIntervalMs = (int32_t)pendPeriod * (int32_t)pendTuneCycle; //in msec
tuneInterval=tuneIntervalMs / 1000; //in sec
}
view raw cTune214.ino hosted with ❤ by GitHub

0 件のコメント:

コメントを投稿