ソースコードは、重力振り子用の構成になっています。
//*** WIFI SETTING の下2行がWiFiのSSIDとパスコードの設定部分です。
EPS32がインストールされているArduino IDEで、ボードとして「WEMOS LOLIN32」を選択してコンパイルします。
起動後、OLEDに表示されるIPアドレスに、ブラウザ(ChromeとFireFox以外は試してないです)からアクセスし、「Config」メニューの「Pendulum XX cycles = YY sec」で時計のギヤ比(YY秒間で振り子がXX回往復)を、「Pend Cycle between Tune: ZZ」で修正間隔(振り子ZZ回往復ごとにコイル電流を更新、1分に1回程度で良いと思います)を設定して「Apply」し、再度「Config」から「Reboot」すれば、とりあえずは動くはずです。
回転振り子については、#define GRAVITYをコメントアウトし、#define TORSIONを生かしてコンパイルします。センサーの設置位置などが微妙で、条件が変わるとソースコード中の定数の調整も必要と思われます。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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); | |
} |