2020年5月16日土曜日

ESP32で古時計の精度改善


ソースコードは、重力振り子用の構成になっています。
//*** 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を生かしてコンパイルします。センサーの設置位置などが微妙で、条件が変わるとソースコード中の定数の調整も必要と思われます。


//
// 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>&nbsp;&nbsp;A:<select name='YE' style='font-size:50pt;width:200px;margin:0px'>";
for (byte i=0; i<5; i++) {
sel=(gYEscaleIdx==i)?"selected":"";
msgG += "<option " + sel + " value='" + String(i) + "'>" + gYEscaleStr[i] + "</option>";
}
msgG += "</select>&nbsp;&nbsp;C:<select name='YC' style='font-size:50pt;width:200px'>";
for (byte i=0; i<5; i++) {
sel=(gYCscaleIdx==i)?"selected":"";
msgG += "<option " + sel + " value='" + String(i) + "'>" + gYCscaleStr[i] + "</option>";
}
msgG += "</select><br><input type='submit' value='Reload'></form> \
<form action='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 + "'>&nbsp;\
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);
}
view raw cTune323p.ino hosted with ❤ by GitHub