|
// |
|
// Automatic Clock Tuner |
|
// by Kiraku Labo |
|
// |
|
|
|
#include <SPIFFS.h> |
|
#include <WiFiUdp.h> |
|
#include <HTTPClient.h> |
|
#include <ArduinoOTA.h> |
|
#include <ESPAsyncWebServer.h> |
|
#include <AsyncTCP.h> |
|
#include <SSD1306Wire.h> |
|
#include <WiFi.h> |
|
|
|
#define GRAVITY //Gravity pendulum clock |
|
//#define TORSION //Torsion pendulum clock |
|
|
|
#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" |
|
|
|
#ifdef GRAVITY |
|
#define DISP_INVERTED |
|
#define L9110 |
|
#define A3144 //Mag sensor (Hall) |
|
#define DEFAULT_PEND_CYCLE 748 //PEND_CYCLE pendulum cycles per PEND_DUR seconds |
|
#define DEFAULT_PEND_DUR 735 |
|
#define DEFAULT_TUNE_CYCLE 60 |
|
#define DEFAULT_PEND_LF 1 //lift factor |
|
#define DEFAULT_KP 900 |
|
#define DEFAULT_KI 200 |
|
#endif |
|
|
|
#ifdef TORSION |
|
#define DISP_INVERTED |
|
#define L9110 |
|
#define HMC5883 //Mag sensor (Digital compass) |
|
#define MAG_THRESH_H 3000 |
|
#define MAG_THRESH_L 2000 |
|
#define DEFAULT_PEND_CYCLE 1 //PEND_CYCLE pendulum cycles per PEND_DUR seconds |
|
#define DEFAULT_PEND_DUR 10 |
|
#define DEFAULT_TUNE_CYCLE 15 |
|
#define DEFAULT_PEND_LF 270 //swing angle |
|
#define DEFAULT_KP 250 |
|
#define DEFAULT_KI 50 |
|
#endif |
|
|
|
#define DEFAULT_NTP_BY_NAME true |
|
#define DEFAULT_STA_IP_FIX "0" //1:Fix 0:DNS |
|
#define DEFAULT_DATE_FORMAT 1 //1:JP, 2:EU, 3:US |
|
|
|
//*** WIFI SETTING |
|
const char* DEFAULT_WIFI_SSID="xxxxxxxx"; |
|
const char* DEFAULT_WIFI_PASS="yyyyyyyy"; |
|
//*** |
|
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="210.173.160.87"; |
|
const char* DEFAULT_NTP_NAME="ntp.nict.jp"; |
|
const char* DEFAULT_TZ_STR=TZ_STR0; |
|
const char* DEFAULT_STA_IP="192.168.0.112"; |
|
const char* apSsid="cTune"; |
|
const char* apPass="12345678"; |
|
const String verLetter="V"; |
|
const String fileVer="A"+verLetter; //if file ver is different, load default setting |
|
const String project="cTune"; |
|
const String verNum="323p"; |
|
|
|
#define DEFAULT_NTP_MIN_INTERVAL 3 //minute |
|
#define DEFAULT_NTP_MAX_INTERVAL 60 //minute |
|
|
|
#ifdef HMC5883 |
|
#define MAX_NTP_TIMEOUT 1000 //msec |
|
#else |
|
#define MAX_NTP_TIMEOUT 3000 //msec |
|
#endif |
|
#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 (1/100 sec), If difference is greater than this value, coil is driven in full power. |
|
#define PRECISE_MODE_THRESH 100 //csec, In Onoff mode, when the error becomes within this range, Onoff 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 |
|
#define LIFT_SWING 100 //Lift Factor vs Swing Angle threshold |
|
|
|
#define SENS_PIN 15 |
|
#define LED_PIN 13 |
|
#ifdef COIL_POLARITY |
|
#define DRV1_PIN 14 |
|
#define DRV2_PIN 12 |
|
#else |
|
#define DRV1_PIN 12 |
|
#define DRV2_PIN 14 |
|
#endif |
|
#define SDA_PIN 5 |
|
#define SCL_PIN 4 |
|
#define DMARGIN 0 |
|
#define BTN_PIN 0 |
|
#define LED_ON HIGH |
|
#define LED_OFF LOW |
|
|
|
#if defined(A3144) |
|
#define SENSOR_ON LOW |
|
#define SENSOR_OFF HIGH |
|
#else |
|
#define SENSOR_ON HIGH |
|
#define SENSOR_OFF LOW |
|
#endif |
|
|
|
#if defined(HMC5883) |
|
#define MAG_ADDR 0x1E |
|
#endif |
|
|
|
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=INITIAL_NTP_VALID*500; |
|
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 adjGraphFactor=5*(ADJ_MAX+1)/64; |
|
uint16_t ntpIPHistoryIdx=0; |
|
uint16_t lcdGrAltSec=10; |
|
|
|
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; |
|
|
|
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; |
|
float pendPeriod; |
|
int32_t partial0Start; |
|
int32_t partial0End; |
|
|
|
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>"; |
|
|
|
String msgHome = |
|
"<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>"; |
|
|
|
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; |
|
int32_t minSensOffDuration, minSensOnDuration, trigWindowSpan, pendWindow; |
|
int16_t kP, kI, liftFactor; |
|
|
|
int32_t 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; |
|
|
|
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==SENSOR_ON && (milliAtInt-milliSensRelease)>minSensOffDuration) { |
|
pendDir=1; |
|
if (!sensAdjMode) ledOn(); |
|
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==SENSOR_OFF && (milliAtInt-milliSensTrig)>minSensOnDuration) { |
|
pendDir=0; |
|
if (!sensAdjMode) ledOff(); |
|
sensRelease=true; |
|
milliSensRelease=milliAtInt; |
|
} |
|
} |
|
|
|
void setup() { |
|
disableCore0WDT(); |
|
disableCore1WDT(); |
|
SPIFFS.begin(true); |
|
Serial.begin(115200); |
|
Wire.begin(SDA_PIN, SCL_PIN); |
|
pinMode(BTN_PIN, INPUT_PULLUP); |
|
pinMode(LED_PIN, OUTPUT); |
|
ledOff(); |
|
setupOled(); |
|
#ifdef DISP_INVERTED |
|
oled.flipScreenVertically(); |
|
#endif |
|
oled.setTextAlignment(TEXT_ALIGN_LEFT); |
|
Wire.setClock(50000); //external i2c 50khz |
|
lcdDispText(0, 0, project+" "+verLetter+verNum); |
|
#if defined(HMC5883) |
|
setupHMC5883(); |
|
#endif |
|
boolean errConfig=false; |
|
if (SPIFFS.exists(CONFIG_FILE)) { |
|
if (!loadConfigFile()) { |
|
loadDefaultConfig(); |
|
errConfig=true; |
|
} |
|
} |
|
else { |
|
loadDefaultConfig(); |
|
errConfig=true; |
|
} |
|
setConfigPara(); |
|
setConfigMsg(); |
|
if (errConfig) saveConfigFile(); //saveConfigFile() destroys strXX |
|
ntpInterval=ntpMinInterval; |
|
setupMechTiming(); |
|
preCount=PRE_CYCLE_MS/pendPeriodMs; |
|
if (preCount<3) preCount=3; |
|
setupWifi(); |
|
setupServer(); |
|
while(!wifiConnected()); //if no wifi, infinite loop here |
|
setupOta(); |
|
setupNtp(); |
|
int ret; |
|
int32_t limit; |
|
do { |
|
limit=ntpDelayLimitUs; |
|
ret=synchTime(1); //use transmit timestamp |
|
lcdDispText(0,0, "E" + String(ret) + " TA=" + String(ntpTurnaround) + "/" + String(limit/1000) + " "); |
|
delay(5000); |
|
} while (ret!=0); |
|
tvAtNtpServerSet=tvAtNtpSuccess; |
|
ntpIPHistory[0]=ntpIP; |
|
ntpIPTimeHistory[0]=tvAtNtpServerSet.tv_sec; |
|
do { |
|
limit=ntpDelayLimitUs; |
|
ret=synchTime(0); //use offset |
|
lcdDispText(0,0, "E" + String(ret) + " D=" + String(ntpDelay/1000) + "/" + String(limit/1000) + " "); |
|
delay(5000); |
|
} while (ret!=0); |
|
updateNtpValidDelay(); |
|
setDst(); |
|
#if defined(HMC5883) |
|
ledcSetup(1, 50000, 10); //LEDC_CHANNEL, LEDC_FREQUENCY, LEDC_RESOLUTION_BITS |
|
ledcSetup(2, 50000, 10); |
|
#else |
|
ledcSetup(1, 100, 10); //LEDC_CHANNEL, LEDC_FREQUENCY, LEDC_RESOLUTION_BITS |
|
ledcSetup(2, 100, 10); |
|
#endif |
|
ledcAttachPin(DRV1_PIN, 1); //PIN, LEDC_CHANNEL |
|
ledcAttachPin(DRV2_PIN, 2); |
|
drvCoil(0); |
|
pinMode(SENS_PIN, INPUT_PULLUP); |
|
#if !defined(HMC5883) |
|
attachInterrupt(digitalPinToInterrupt(SENS_PIN), sensInt, CHANGE); |
|
#endif |
|
} |
|
|
|
void loop() { |
|
int sec; |
|
#if defined(HMC5883) |
|
getHMC5883(); |
|
if (sensAdjMode) dispMag(); |
|
int16_t magValue=abs16(magX)+abs16(magY)+abs16(magZ); |
|
if (magValue>MAG_THRESH_H) magTrigH(); |
|
else if (magValue<MAG_THRESH_L) magTrigL(); |
|
#else |
|
if (sensAdjMode) { |
|
if (digitalRead(SENS_PIN)==SENSOR_ON) ledOn(); |
|
else ledOff(); |
|
} |
|
#endif |
|
if (sensTrig) { |
|
sensTrig=false; |
|
gettimeofday(&tvAtSensTrig, NULL); |
|
#if !defined(HMC5883) |
|
trigDelay=(int64_t)(micros()-microSensTrig); |
|
if (trigDelay<0) trigDelay=0; |
|
convUsec2Tv(convTv2Usec(&tvAtSensTrig) - trigDelay, &tvAtSensTrig); |
|
#endif |
|
pendIndicator(WHITE); |
|
ledOn(); |
|
if (refSet) { |
|
if (countPend%pendTuneCycle==0) { |
|
updateError(); |
|
lcdDispError(); |
|
tune(); |
|
lcdDispGraph(); |
|
} |
|
else { |
|
updateError(); |
|
if (!sensAdjMode) { |
|
lcdDispError(); |
|
lcdDispGraph(); |
|
} |
|
} |
|
} |
|
if (preCount>=0) { |
|
if (preCount==0) { |
|
#ifdef HMC5883 |
|
sec=tvAtSensTrig.tv_sec%60; |
|
if (sec>=1 && sec<26) { |
|
preCount=-1; |
|
setRef0(); |
|
tune(); |
|
} |
|
#else |
|
preCount=-1; |
|
setRef0(); |
|
tune(); |
|
#endif |
|
} |
|
else preCount--; |
|
} |
|
if (refSet) lcdDispNum4n(0,0,countPend%pendTuneCycle); |
|
} |
|
else if (sensRelease) { |
|
sensRelease=false; |
|
pendIndicator(BLACK); |
|
ledOff(); |
|
} |
|
gettimeofday(&tvGmtNow, NULL); |
|
gmtNow=tvGmtNow.tv_sec; |
|
if (gmtNow>gmtLast) { //every 1 sec |
|
gmtAtSec=gmtNow; |
|
gmtLast=gmtNow; |
|
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(); |
|
lcdDispGraph(); |
|
} |
|
} |
|
if (sec==30) { |
|
if (wifiConnected()) { |
|
if (gmtAtSec>=tvAtNtpSuccess.tv_sec + ntpInterval*60) { |
|
if (millis()-msecNTP>30000) { |
|
msecNTP=millis(); |
|
if (synchTime(0)==0) { |
|
updateNtpValidDelay(); |
|
} |
|
else if (!ntpFirstOffset) interporateTime(); |
|
} |
|
} |
|
else if (!ntpFirstOffset) interporateTime(); |
|
} |
|
else WiFi.begin(wifiSSID.c_str(), wifiPass.c_str()); |
|
if (millis()-milliSensTrig>pendWindow && refSet) tune(); |
|
} |
|
} |
|
if (funcPtrWeb) { |
|
funcPtrWeb(funcReqWeb); |
|
funcPtrWeb=NULL; |
|
} |
|
if (liftFactor>=LIFT_SWING) { //partial drive anniversary clock |
|
uint32_t msPd=millis()-milliSensTrig; |
|
if (msPd>partial0Start && msPd<partial0End) { |
|
drvCoil(0); |
|
} |
|
else { |
|
drvCoil(adjLevel); |
|
} |
|
} |
|
senseButton(); |
|
ArduinoOTA.handle(); |
|
yield(); |
|
} |
|
|
|
void dispMag() { |
|
char lcdbuf[13]; |
|
snprintf(lcdbuf, 19, "X= %s%05d", magX>=0?" ":"-", abs16(magX)); |
|
lcdDispText2(0,1, lcdbuf, 80); |
|
snprintf(lcdbuf, 19, "Y= %s%05d", magY>=0?" ":"-", abs16(magY)); |
|
lcdDispText2(0,2, lcdbuf, 80); |
|
snprintf(lcdbuf, 19, "Z= %s%05d", magZ>=0?" ":"-", abs16(magZ)); |
|
lcdDispText2(0,3, lcdbuf, 80); |
|
// Serial.print(magX);Serial.print(",");Serial.print(magY);Serial.print(","); |
|
// Serial.println(magZ); |
|
} |
|
|
|
int16_t abs16(int16_t x) { |
|
if (x>=0) return x; |
|
else return -x; |
|
} |
|
|
|
int32_t abs32(int32_t x) { |
|
if (x>=0) return x; |
|
else return -x; |
|
} |
|
|
|
int64_t abs64(int64_t x) { |
|
if (x>=0) return x; |
|
else return -x; |
|
} |
|
|
|
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() { |
|
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 (abs32(csecPendError)>ONOFF_MODE_THRESH) onoffMode=true; |
|
if (onoffMode) { |
|
if (csecPendError>PRECISE_MODE_THRESH) cadjLevel=ADJ_MIN*100/(liftFactor<LIFT_SWING?liftFactor:1); |
|
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)*kP/tuneInterval + csecPendError*kI/100); //use per-min rate factor of 60/tuneInterval |
|
cadjLevelDelta *= (ADJ_MAX+1)/64; //##1 |
|
cadjLevel = constrain(cadjLevel-cadjLevelDelta, ADJ_MIN*100/(liftFactor<LIFT_SWING?liftFactor:1), ADJ_MAX*100); |
|
} |
|
if (csecPendError<ERROR_LIMIT*100) cadjLevel=0; |
|
adjLevel=cadjLevel/100; |
|
if (liftFactor<LIFT_SWING) { //gravity pend |
|
drvCoil(adjLevel); |
|
} |
|
if (adjLevel>adjLevelMax) adjLevelMax=adjLevel; |
|
if (adjLevel<adjLevelMin) adjLevelMin=adjLevel; |
|
} |
|
|
|
void drvCoil(int16_t pwr) { |
|
if (pwr>=0) { |
|
ledcWrite(1, 0); |
|
ledcWrite(2, pwr); |
|
} |
|
else { |
|
ledcWrite(2, 0); |
|
ledcWrite(1, -pwr); |
|
} |
|
} |
|
|
|
void senseButton() { |
|
int button=digitalRead(BTN_PIN); |
|
if (button==LOW) { |
|
if (shortPush==false) { |
|
shortPush=true; |
|
longPush=false; |
|
longlongPush=false; |
|
buttonMilli=millis(); |
|
} |
|
else { |
|
if (millis()-buttonMilli>1000) { |
|
if (!longPush) { //disp WiFi status |
|
longPush=true; |
|
clearGraph(); |
|
if (WiFi.status()==WL_CONNECTED) lcdDispText(0,2, wifiSSID.c_str()); |
|
else lcdDispText(0,2, "WiFi NG "); |
|
lcdDispText(0,3, WiFi.localIP().toString()); |
|
} |
|
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); |
|
} |
|
|
|
int synchTime(byte synchMode) { //syncMode 0= use offset, 1=use Transmit timestamp |
|
struct timeval tv1, tv2; |
|
int ret=getNTPTime(synchMode); |
|
if (ret==0) { //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 (abs32(ntpOffset)<NTP_OFFSET_THRESH_L*3 && abs32(ntpOffsetPerMin)<NTP_OFFSET_THRESH_L) { |
|
increment=ntpInterval*3/10; |
|
if (increment==0) increment=1; |
|
ntpIntervalInc++; |
|
countConsecutiveDec=0; |
|
} |
|
else if (abs32(ntpOffset)>=NTP_OFFSET_THRESH_H*3 || abs32(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(); |
|
} |
|
} |
|
} |
|
} |
|
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); |
|
return ret; |
|
} |
|
|
|
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 ID 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 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 updateNtpValidDelay() { |
|
ntpDelayAve = ntpDelayAve * 0.8 + 0.2*(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"); |
|
} |
|
|
|
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 connectWifi() { |
|
uint32_t milli; |
|
lcdDispText(0,1,""); |
|
clearGraph(); |
|
lcdDispText(0,2, wifiSSID); |
|
WiFi.disconnect(true); |
|
WiFi.config(0u, 0u, 0u); //clear static IP |
|
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 setupWifi() { |
|
byte tryCount=0; |
|
WiFi.mode(WIFI_OFF); |
|
WiFi.mode(WIFI_STA); |
|
do { |
|
connectWifi(); |
|
tryCount++; |
|
} while(wifiConnected()==false && tryCount<3); |
|
if (!wifiConnected()) { |
|
apOnly=true; |
|
WiFi.mode(WIFI_AP); |
|
WiFi.softAP(apSsid, apPass); |
|
lcdDispText(0,0, F("AP Only ")); |
|
lcdDispText(0,1, apSsid); |
|
lcdDispText(0,2, apPass); |
|
lcdDispText(0,3, WiFi.softAPIP().toString()); |
|
} |
|
} |
|
|
|
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; |
|
} |
|
|
|
void dbgPrint(String s) { |
|
if (serialMon) Serial.print(s); |
|
} |
|
|
|
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= msgHead + 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); |
|
"</p>" + msgEnd; |
|
request->send (200, textHtml, s); |
|
} |
|
|
|
String csecToSecF(int32_t csec) { |
|
String s; |
|
if (csec<0) s="-"; |
|
else s=""; |
|
return s + String(abs32(csec)/100) + "." + preZero2((int)(abs32(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, msgHead + 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, msgHead + 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, msgHead + 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, msgHead + msgHome + st + msgEnd); |
|
} |
|
|
|
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> 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> 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='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); |
|
request->send(200, textHtml, msgHead + msgG + stgT + stg + st + msgEnd); |
|
} |
|
|
|
void updateErrorGraph(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, msgHead + msgHome + st + msgEnd); |
|
} |
|
|
|
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, msgHead + 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, msgHead + 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()); |
|
liftFactor = atoi(request->getParam("LF")->value().c_str()); |
|
setupMechTiming(); |
|
kP = atoi(request->getParam("KP")->value().c_str()); |
|
kI = atoi(request->getParam("KI")->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) { |
|
setenv("TZ", tzStr.c_str(), 1); |
|
tzset(); |
|
} |
|
dateFormat=(byte)atoi(request->getParam("DF")->value().c_str()); |
|
saveConfigFile(); |
|
request->send (200, textHtml, msgHead + msgHome + "<br><h2>Done</h2>" + msgEnd); |
|
} |
|
else { |
|
request->send (200, textHtml, msgHead + 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 + "'> \ |
|
KI:<input type='number' min='0' name='KI' value='" + strKI + "'>\ |
|
Lift/Swing:<input type='number' min='1' max='720' 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 |
|
} |
|
|
|
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=dispStatus; |
|
request->send (200, textHtml, msgHead + "<h2>End Sensor Adj mode?</h2><br><br>" + msgYesNo + msgEnd); |
|
} |
|
|
|
void sensAdjOff(AsyncWebServerRequest *request) { |
|
sensAdjMode=false; |
|
ledOff(); |
|
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", updateErrorGraph); |
|
server.on("/GR", graphError); |
|
server.on("/CF", confMenu); |
|
server.on("/CA", applyConfig); |
|
server.on("/CD", confDefault); |
|
server.on("/MM", dispStatus); |
|
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 (abs32(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 (abs32(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) |
|
if (sensAdjMode) return; |
|
#endif |
|
if (digitalRead(BTN_PIN)==LOW) return; |
|
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 (abs32(errorHistory[0][i%N_HISTORY])>eMax) eMax=abs32(errorHistory[0][i%N_HISTORY]); |
|
diff=abs16(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(); |
|
lcdSetColor(WHITE); |
|
lcdDrawLine(0, GYCENTER, GXORIGIN, GYCENTER); |
|
lcdDrawLine(GXORIGIN, GYCENTER-16, GXORIGIN, GYCENTER+16); |
|
lcdDrawLine(GXORIGIN, GYCENTER-10, GXORIGIN+2, GYCENTER-10); |
|
lcdDrawLine(GXORIGIN, GYCENTER+10, GXORIGIN+2, GYCENTER+10); |
|
oled.setFont(ArialMT_Plain_10); |
|
String scaleMark; |
|
if ((lcdGmode==0) && (gmtAtSec%lcdGrAltSec<(lcdGrAltSec/2)) || (lcdGmode==1)) { |
|
if (gscaleE[idxE]>=100) scaleMark=String(gscaleE[idxE]/100)+"s"; |
|
else scaleMark=String(gscaleE[idxE]*10)+"ms"; |
|
lcdDrawString(GXORIGIN+4, GYCENTER-16, "+"+scaleMark); |
|
lcdDrawString(GXORIGIN+4, GYCENTER+4, "-"+scaleMark); |
|
} |
|
else if ((lcdGmode==0) && (gmtAtSec%lcdGrAltSec>=(lcdGrAltSec/2)) || (lcdGmode==2)) { |
|
lcdDrawString(GXORIGIN+4, GYCENTER-16, String(gscaleA[idxA]+(adjLevel/10)*10)); |
|
lcdDrawString(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) { |
|
lcdSetPixel(GRAPH_W-i, GTOP); |
|
lcdSetPixel(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) lcdSetPixel(GRAPH_W-i, y); |
|
} |
|
} |
|
} |
|
lcdSetColor(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%lcdGrAltSec<(lcdGrAltSec/2)) || (lcdGmode==1)) { |
|
for (int32_t i=hStart; i<=numHistory[0]; i++) { |
|
yE=errorHistory[0][i%N_HISTORY]*10/gscaleE[idxE]; |
|
if (abs32(yE)<=GBOTTOM-GYCENTER) lcdSetPixel(GXORIGIN+i-hStart+offset, GYCENTER-yE); |
|
if (i==numHistory[0]) lcdDrawCircle(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) lcdSetPixel(x, GYCENTER); |
|
} |
|
} |
|
else if ((lcdGmode==0) && (gmtAtSec%lcdGrAltSec>=(lcdGrAltSec/2)) || (lcdGmode==2)) { |
|
for (int32_t i=hStart; i<=numHistory[0]; i++) { |
|
yA=(adjHistory[0][i%N_HISTORY]-adjLevel)*10/gscaleA[idxA]; |
|
if (abs16(yA)<=GBOTTOM-GYCENTER) lcdSetPixel(GXORIGIN+i-hStart+offset, GYCENTER-yA); |
|
} |
|
if (adjLevel>=0) lcdFillRect(0, GYCENTER-16*adjLevel/ADJ_MAX, GXORIGIN, 16*adjLevel/ADJ_MAX); |
|
else lcdFillRect(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) lcdSetPixel(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 lcdDrawLine(int x0, int y0, int x1, int y1) { |
|
oled.drawLine(x0, y0, x1, y1); |
|
} |
|
|
|
void lcdSetColor(OLEDDISPLAY_COLOR color) { |
|
oled.setColor(color); |
|
} |
|
|
|
void lcdDrawString(int x0, int y0, String s) { |
|
oled.drawString(x0, y0, s); |
|
} |
|
|
|
void lcdSetPixel(int x0, int y0) { |
|
oled.setPixel(x0, y0); |
|
} |
|
|
|
void lcdDrawCircle(int x0, int y0, int r) { |
|
oled.drawCircle(x0, y0, r); |
|
} |
|
|
|
void lcdFillRect(int x0, int y0, int x1, int y1) { |
|
oled.fillRect(x0, y0, x1, y1); |
|
} |
|
|
|
void setupOta() { |
|
ArduinoOTA.begin(); |
|
ArduinoOTA.onStart([]() { |
|
detachInterrupt(digitalPinToInterrupt(SENS_PIN)); |
|
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; |
|
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; |
|
sensRelease=true; |
|
milliSensRelease=millis(); |
|
} |
|
} |
|
|
|
void setupHMC5883() { |
|
Wire.beginTransmission(MAG_ADDR); |
|
Wire.write(0x02); // Register |
|
Wire.write(0x00); // Continuous 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 |
|
|
|
void setupMechTiming() { |
|
pendPeriod=(float)pendDur * 1000.0 / (float)pendCycle; //msec/cycle |
|
pendPeriodMs=(uint32_t)pendPeriod; |
|
if (pendPeriodMs>5000) lcdGrAltSec=pendPeriodMs/500; |
|
else lcdGrAltSec=10; |
|
float factor; |
|
#ifdef MIN_SENS_DUR_OFF |
|
minSensOffDuration = MIN_SENS_DUR_OFF; |
|
#else |
|
if (pendPeriod<600) factor=0.3; //balance wheele type |
|
else if (pendPeriod>4000) factor=0.75; //torsion pendulum type |
|
else factor=0.5; //gravity pendulum type |
|
minSensOffDuration = (int32_t)(pendPeriod * factor); |
|
#endif |
|
#ifdef MIN_SENS_DUR_ON |
|
minSensOnDuration = MIN_SENS_DUR_ON; |
|
#else |
|
if (pendPeriod<600) factor=0.3; |
|
else factor=0.01; |
|
minSensOnDuration = (int32_t)(pendPeriod * factor); |
|
#endif |
|
pendWindow = (int32_t)(pendPeriod * 1.3 + 1000.0); |
|
tuneIntervalMs = (int32_t)pendPeriod * (int32_t)pendTuneCycle; //in msec |
|
tuneInterval=tuneIntervalMs / 1000; //in sec |
|
if (liftFactor>=LIFT_SWING) { //torsion pend |
|
float factP0; |
|
if (liftFactor<=270) factP0=0.0; //full drive |
|
else factP0=(float)(liftFactor-270)/600.0; //partial drive 360deg=0.4-0.7 |
|
partial0End=(int32_t)(pendPeriod*(0.55+factP0)); |
|
partial0Start=(int32_t)(pendPeriod*(0.55-factP0)); |
|
} |
|
} |
|
|
|
void ledOn() { |
|
digitalWrite(LED_PIN, LED_ON); |
|
} |
|
|
|
void ledOff() { |
|
digitalWrite(LED_PIN, LED_OFF); |
|
} |