├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .gitmodules ├── .travis.yml ├── I2CAnalogClock ├── .gitignore ├── fuses.py ├── include │ └── README ├── platformio.ini ├── src │ ├── I2CACVersion.h │ ├── I2CAnalogClock.cpp │ └── I2CAnalogClock.h └── test │ └── README ├── LICENSE ├── NTPTest └── src │ ├── Arduino.h │ ├── IPAddress.cpp │ ├── IPAddress.h │ ├── Logger.cpp │ ├── Logger.h │ ├── NTP.cpp │ ├── NTP.h │ ├── NTPPrivate.h │ ├── NTPTest.cpp │ ├── Ping.cpp │ ├── Ping.h │ ├── String.cpp │ ├── String.h │ ├── Timer.cpp │ ├── Timer.h │ ├── Types.h │ ├── UDPWrapper.cpp │ ├── UDPWrapper.h │ ├── UnixWiFi.cpp │ └── UnixWiFi.h ├── README.md ├── SynchroClock ├── .gitignore ├── include │ ├── README │ ├── SynchroClock.h │ ├── SynchroClockPins.h │ └── SynchroClockVersion.h ├── lib │ ├── Clock │ │ └── src │ │ │ ├── Clock.cpp │ │ │ └── Clock.h │ ├── ConfigParam │ │ └── src │ │ │ ├── ConfigParam.cpp │ │ │ └── ConfigParam.h │ ├── DS3231 │ │ └── src │ │ │ ├── DS3231.cpp │ │ │ ├── DS3231.h │ │ │ ├── DS3231DateTime.cpp │ │ │ └── DS3231DateTime.h │ ├── FeedbackLED │ │ └── src │ │ │ ├── FeedbackLED.cpp │ │ │ └── FeedbackLED.h │ ├── Logger │ │ └── src │ │ │ └── Logger.h │ ├── NTP │ │ └── src │ │ │ ├── NTP.cpp │ │ │ ├── NTP.h │ │ │ └── NTPPrivate.h │ ├── SimplePing │ │ └── src │ │ │ ├── SimplePing.cpp │ │ │ └── SimplePing.h │ ├── TimeUtils │ │ └── src │ │ │ ├── TimeUtils.cpp │ │ │ └── TimeUtils.h │ ├── Timer │ │ └── src │ │ │ ├── Timer.cpp │ │ │ └── Timer.h │ ├── UDPWrapper │ │ └── src │ │ │ ├── UDPWrapper.cpp │ │ │ └── UDPWrapper.h │ └── WireUtils │ │ └── src │ │ ├── WireUtils.cpp │ │ └── WireUtils.h ├── mkdata.py ├── platformio.ini ├── src │ └── SynchroClock.cpp └── test │ ├── ClockTest │ └── ClockTest.cpp │ ├── DS3231Test │ └── DS3231Test.cpp │ └── README ├── eagle └── SynchroClock │ ├── SynchroClock.brd │ ├── SynchroClock.lbr │ ├── SynchroClock.sch │ ├── SynchroClockBOM.txt │ └── eagle.epf └── images └── SynchroClock.png /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: PlatformIO CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | build: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Cache pip 15 | uses: actions/cache@v2 16 | with: 17 | path: ~/.cache/pip 18 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} 19 | restore-keys: | 20 | ${{ runner.os }}-pip- 21 | - name: Cache PlatformIO 22 | uses: actions/cache@v2 23 | with: 24 | path: ~/.platformio 25 | key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} 26 | - name: Set up Python 27 | uses: actions/setup-python@v2 28 | - name: Install PlatformIO 29 | run: | 30 | python -m pip install --upgrade pip 31 | pip install --upgrade platformio 32 | - name: Build I2CAnalogClock 33 | run: | 34 | cd I2CAnalogClock 35 | pio run 36 | - name: Build SynchroClock 37 | run: | 38 | cd SynchroClock 39 | pio run 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | AnalogClock.code-workspace 3 | SynchroClock/data 4 | .pio 5 | .vscode 6 | *.bak 7 | *-bak 8 | eagle/SynchroClock/SynchroClock.pro 9 | eagle/SynchroClock/SynchroClock.b#* 10 | eagle/SynchroClock/SynchroClock.l#* 11 | eagle/SynchroClock/SynchroClock.s#* 12 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liebman/AnalogClock/a207802240f1244dee30748a2ee371fe407f0ef4/.gitmodules -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.9" 4 | 5 | sudo: false 6 | 7 | cache: 8 | directories: 9 | - "~/.platformio" 10 | 11 | install: 12 | - pip install -U platformio 13 | - platformio update 14 | 15 | script: 16 | - cd SynchroClock 17 | - platformio run 18 | - cd ../I2CAnalogClock 19 | - platformio run 20 | -------------------------------------------------------------------------------- /I2CAnalogClock/.gitignore: -------------------------------------------------------------------------------- 1 | /local.ini 2 | -------------------------------------------------------------------------------- /I2CAnalogClock/fuses.py: -------------------------------------------------------------------------------- 1 | Import('env') 2 | 3 | # the uplaod flags for attiny85 adds -e that erases the flash and when we don't 4 | # want that to happen when we are setting the fuses, espically when we change 5 | # the reset pin to an IO pin. The '-e' is actually added twice, once via extra 6 | # upload flags in the board definition and once in the setup for the fuses target 7 | def fuses_command(source, target, env): 8 | env['UPLOADERFLAGS'].remove('-e') 9 | cmd = " ".join( 10 | ["avrdude", "$UPLOADERFLAGS"] + 11 | ["-U%s:w:%s:m" % (k, v) 12 | for k, v in env.BoardConfig().get("fuses", {}).items()] 13 | ) 14 | return env.Execute(cmd) 15 | 16 | env.Replace(FUSESCMD=fuses_command) 17 | -------------------------------------------------------------------------------- /I2CAnalogClock/include/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for project header files. 3 | 4 | A header file is a file containing C declarations and macro definitions 5 | to be shared between several project source files. You request the use of a 6 | header file in your project source file (C, C++, etc) located in `src` folder 7 | by including it, with the C preprocessing directive `#include'. 8 | 9 | ```src/main.c 10 | 11 | #include "header.h" 12 | 13 | int main (void) 14 | { 15 | ... 16 | } 17 | ``` 18 | 19 | Including a header file produces the same results as copying the header file 20 | into each source file that needs it. Such copying would be time-consuming 21 | and error-prone. With a header file, the related declarations appear 22 | in only one place. If they need to be changed, they can be changed in one 23 | place, and programs that include the header file will automatically use the 24 | new version when next recompiled. The header file eliminates the labor of 25 | finding and changing all the copies as well as the risk that a failure to 26 | find one copy will result in inconsistencies within a program. 27 | 28 | In C, the usual convention is to give header files names that end with `.h'. 29 | It is most portable to use only letters, digits, dashes, and underscores in 30 | header file names, and at most one dot. 31 | 32 | Read more about using header files in official GCC documentation: 33 | 34 | * Include Syntax 35 | * Include Operation 36 | * Once-Only Headers 37 | * Computed Includes 38 | 39 | https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html 40 | -------------------------------------------------------------------------------- /I2CAnalogClock/platformio.ini: -------------------------------------------------------------------------------- 1 | [platformio] 2 | default_envs = default 3 | extra_configs = 4 | local.ini ; can be used for localized envs 5 | 6 | [env] 7 | board = attiny85 8 | platform = atmelavr@4.2.0 9 | framework = arduino 10 | build_flags = -D__AVR_ATtiny85__ 11 | board_build.f_cpu = 1000000L ; we run at 1Mhz 12 | lib_deps = 13 | https://github.com/NicoHood/PinChangeInterrupt.git#ed1c1f4 ; current head as of Jul 27, 2019 14 | extra_scripts=fuses.py ; fix fuses target not to erase flash! 15 | 16 | [env:default] 17 | ; fuses for 1Mhz/bod=1.8/EESAVE/RSTDISABLE 18 | board_fuses.hfuse = 0x56 19 | board_fuses.lfuse = 0x62 20 | 21 | [env:testing] 22 | ; fuses for 1Mhz/bod=1.8/EESAVE 23 | board_fuses.hfuse = 0xd6 24 | board_fuses.lfuse = 0x62 25 | -------------------------------------------------------------------------------- /I2CAnalogClock/src/I2CACVersion.h: -------------------------------------------------------------------------------- 1 | /* 2 | * I2CACVersion.h 3 | * 4 | * Created on: Mar 28, 2018 5 | * Author: chris.l 6 | */ 7 | 8 | #ifndef I2CACVERSION_H_ 9 | #define I2CACVERSION_H_ 10 | 11 | #define I2C_ANALOG_CLOCK_VERSION 1 12 | 13 | #endif /* I2CACVERSION_H_ */ 14 | -------------------------------------------------------------------------------- /I2CAnalogClock/src/I2CAnalogClock.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * I2CAnalogClock.cpp 3 | * 4 | * Copyright 2017 Christopher B. Liebman 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * 19 | * Created on: May 26, 2017 20 | * Author: liebman 21 | */ 22 | 23 | #include "I2CAnalogClock.h" 24 | #include "I2CACVersion.h" 25 | #include 26 | 27 | volatile uint16_t position; // This is the position that we believe the clock is in. 28 | volatile uint16_t adjustment; // This is the adjustment to be made. 29 | volatile uint8_t command; // This is which "register" to be read/written. 30 | volatile uint8_t status; // status register (has tick bit) 31 | volatile uint8_t control; // This is our control "register". 32 | volatile unsigned int pwm_duration; // PWM cycle count down. 33 | volatile bool adjust_active; // adjustment is active. 34 | volatile bool save_config; // set if the config was updated. 35 | volatile bool factory_reset; // set if factory reset is active 36 | volatile Config config; // Configuration 37 | uint8_t reset_reason; // 38 | 39 | #if defined(PWRFAIL_PIN) 40 | volatile bool power_failed; // power has failed, we need to save NOW! 41 | volatile uint8_t pwrfail_control; // saved control register during power fail 42 | #endif 43 | 44 | #ifdef DEBUG_I2CAC 45 | volatile unsigned int ticks; 46 | #endif 47 | 48 | // i2c receive handler 49 | void i2creceive(int size) 50 | { 51 | command = Wire.read(); 52 | --size; 53 | // check for a write command (or a command that does not read/write just action) 54 | if (size > 0) 55 | { 56 | switch (command) 57 | { 58 | case CMD_POSITION: 59 | position = Wire.read() | Wire.read() << 8; 60 | break; 61 | case CMD_ADJUSTMENT: 62 | adjustment = Wire.read() | Wire.read() << 8; 63 | // adjustment will start on the next tick! 64 | break; 65 | case CMD_TP_DURATION: 66 | config.tp_duration = Wire.read(); 67 | break; 68 | case CMD_TP_DUTY: 69 | config.tp_duty = Wire.read(); 70 | break; 71 | case CMD_AP_DURATION: 72 | config.ap_duration = Wire.read(); 73 | break; 74 | case CMD_AP_DUTY: 75 | config.ap_duty = Wire.read(); 76 | break; 77 | case CMD_AP_DELAY: 78 | config.ap_delay = Wire.read(); 79 | break; 80 | case CMD_AP_START: 81 | config.ap_start_duration = Wire.read(); 82 | break; 83 | case CMD_PWMTOP: 84 | config.pwm_top = Wire.read(); 85 | break; 86 | case CMD_CONTROL: 87 | control = Wire.read(); 88 | break; 89 | case CMD_SAVE_CONFIG: 90 | (void)Wire.read(); // we ignore as its just a placeholder 91 | save_config = true; 92 | break; 93 | case CMD_RESET: 94 | (void)Wire.read(); // we ignore as its just a placeholder 95 | factory_reset = true; 96 | } 97 | command = 0xff; 98 | } 99 | } 100 | 101 | // i2c request handler 102 | void i2crequest() 103 | { 104 | uint16_t value; 105 | switch (command) 106 | { 107 | case CMD_ID: 108 | Wire.write(ID_VALUE); 109 | break; 110 | case CMD_POSITION: 111 | value = position; 112 | Wire.write(value & 0xff); 113 | Wire.write(value >> 8); 114 | break; 115 | case CMD_ADJUSTMENT: 116 | value = adjustment; 117 | Wire.write(value & 0xff); 118 | Wire.write(value >> 8); 119 | break; 120 | case CMD_TP_DURATION: 121 | value = config.tp_duration; 122 | Wire.write(value); 123 | break; 124 | case CMD_TP_DUTY: 125 | value = config.tp_duty; 126 | Wire.write(value); 127 | break; 128 | case CMD_AP_DURATION: 129 | value = config.ap_duration; 130 | Wire.write(value); 131 | break; 132 | case CMD_AP_DUTY: 133 | value = config.ap_duty; 134 | Wire.write(value); 135 | break; 136 | case CMD_AP_DELAY: 137 | value = config.ap_delay; 138 | Wire.write(value); 139 | break; 140 | case CMD_AP_START: 141 | value = config.ap_start_duration; 142 | Wire.write(value); 143 | break; 144 | case CMD_PWMTOP: 145 | value = config.pwm_top; 146 | Wire.write(value); 147 | break; 148 | case CMD_RST_REASON: 149 | Wire.write(reset_reason); 150 | break; 151 | case CMD_VERSION: 152 | Wire.write(I2C_ANALOG_CLOCK_VERSION); 153 | break; 154 | 155 | case CMD_CONTROL: 156 | Wire.write(control); 157 | break; 158 | case CMD_STATUS: 159 | Wire.write(status); 160 | break; 161 | } 162 | command = 0xff; 163 | } 164 | 165 | void reboot() 166 | { 167 | wdt_reset(); 168 | wdt_enable(WDTO_15MS); 169 | while (true) 170 | { 171 | } 172 | } 173 | 174 | void (*timer_cb)(); 175 | volatile bool timer_running; 176 | volatile unsigned int start_time; 177 | 178 | void clearTimer() 179 | { 180 | #if defined(__AVR_ATtinyX5__) 181 | TCCR1 = 0; 182 | GTCCR = 0; 183 | TIMSK &= ~(_BV(TOIE1) | _BV(OCIE1A) | _BV(OCIE1A)); 184 | #else 185 | TCCR1A = 0; 186 | TCCR1B = 0; 187 | TIMSK1 = 0; 188 | #endif 189 | OCR1A = 0; 190 | OCR1B = 0; 191 | } 192 | 193 | ISR(TIMER1_OVF_vect) 194 | { 195 | if (pwm_duration == 0) 196 | { 197 | return; 198 | } 199 | 200 | pwm_duration -= 1; 201 | if (pwm_duration == 0) 202 | { 203 | clearTimer(); 204 | timer_running = false; 205 | if (timer_cb != NULL) 206 | { 207 | timer_cb(); 208 | } 209 | } 210 | } 211 | 212 | ISR(TIMER1_COMPA_vect) 213 | { 214 | unsigned int int_time = millis(); 215 | 216 | // why to we get an immediate interrupt? (ignore it) 217 | if (int_time == start_time || int_time == start_time+1) 218 | { 219 | return; 220 | } 221 | 222 | #if defined(__AVR_ATtinyX5__) 223 | TIMSK &= ~(1 << OCIE1A); // disable timer1 interrupts as we only want this one. 224 | #else 225 | TIMSK1 &= ~(1 << OCIE1A); // disable timer1 interrupts as we only want this one. 226 | #endif 227 | timer_running = false; 228 | if (timer_cb != NULL) 229 | { 230 | timer_cb(); 231 | } 232 | } 233 | 234 | void startTimer(int ms, void (*func)()) 235 | { 236 | start_time = millis(); 237 | uint16_t timer = ms2Timer(ms); 238 | // initialize timer1 239 | noInterrupts(); 240 | // disable all interrupts 241 | timer_cb = func; 242 | #if defined(__AVR_ATtinyX5__) 243 | TCCR1 = 0; 244 | TCNT1 = 0; 245 | 246 | OCR1A = timer; // compare match register 247 | TCCR1 |= (1 << CTC1);// CTC mode 248 | TCCR1 |= PRESCALE_BITS; 249 | TIMSK |= (1 << OCIE1A);// enable timer compare interrupt 250 | // clear any already pending interrupt? does not work :-( 251 | TIFR &= ~(1 << OCIE1A); 252 | #else 253 | TCCR1A = 0; 254 | TCCR1B = 0; 255 | TCNT1 = 0; 256 | 257 | OCR1A = timer; // compare match register 258 | TCCR1B |= (1 << WGM12); // CTC mode 259 | TCCR1B |= PRESCALE_BITS; 260 | TIMSK1 |= (1 << OCIE1A); // enable timer compare interrupt 261 | // clear any already pending interrupt? does not work :-( 262 | TIFR1 &= ~(1 << OCIE1A); 263 | #endif 264 | timer_running = true; 265 | interrupts(); 266 | // enable all interrupts 267 | } 268 | 269 | void startPWM(unsigned int duration, unsigned int duty, void (*func)()) 270 | { 271 | noInterrupts(); 272 | timer_cb = func; 273 | 274 | clearTimer(); 275 | 276 | OCR1C = config.pwm_top-1; 277 | 278 | if (isTick()) 279 | { 280 | OCR1A = duty2pwm(duty); 281 | TCNT1 = 0; // needed??? 282 | #if defined(__AVR_ATtinyX5__) 283 | TCCR1 = _BV(COM1A1) | _BV(PWM1A) | PWM_PRESCALE_BITS; 284 | #else 285 | TCCR1A = _BV(COM1A1) | _BV(WGM10); 286 | #endif 287 | } 288 | else 289 | { 290 | OCR1B = duty2pwm(duty); 291 | TCNT1 = 0; // needed??? 292 | #if defined(__AVR_ATtinyX5__) 293 | TCCR1 = PWM_PRESCALE_BITS; 294 | GTCCR = _BV(COM1B1) | _BV(PWM1B); 295 | #else 296 | TCCR1A = _BV(COM1B1) | _BV(WGM10); 297 | #endif 298 | } 299 | 300 | #if defined(__AVR_ATtinyX5__) 301 | TIMSK |= _BV(TOIE1); 302 | #else 303 | TCCR1B = _BV(WGM12) | PWM_PRESCALE_BITS; 304 | TIMSK1 = _BV(TOIE1); 305 | #endif 306 | 307 | pwm_duration = ms2PWMCount(duration); 308 | timer_running = true; 309 | interrupts(); 310 | } 311 | 312 | void endTick() 313 | { 314 | #ifdef TEST_MODE 315 | digitalWrite(LED_PIN, !digitalRead(LED_PIN)); 316 | #endif 317 | 318 | timer_cb = NULL; 319 | toggleTick(); 320 | 321 | if (adjustment != 0) 322 | { 323 | adjustment--; 324 | if (adjustment != 0) 325 | { 326 | startTimer(config.ap_delay, &adjustClock); 327 | } 328 | else 329 | { 330 | // 331 | // we are done with adjustment, stop the timer 332 | // and schedule the sleep. 333 | // 334 | adjust_active = false; 335 | } 336 | } 337 | else 338 | { 339 | if (adjust_active) 340 | { 341 | adjust_active = false; 342 | } 343 | } 344 | } 345 | 346 | // advance the position 347 | void advancePosition() 348 | { 349 | position += 1; 350 | if (position >= MAX_SECONDS) 351 | { 352 | position = 0; 353 | } 354 | } 355 | 356 | void adjustClock() 357 | { 358 | if (adjustment == 1) 359 | { 360 | // last adjustment uses tick pulse settings 361 | advanceClock(config.tp_duration, config.tp_duty); 362 | } 363 | else 364 | { 365 | advanceClock(config.ap_duration, config.ap_duty); 366 | } 367 | } 368 | 369 | // 370 | // Advance the clock by one second. 371 | // 372 | void advanceClock(uint16_t duration, uint8_t duty) 373 | { 374 | advancePosition(); 375 | startPWM(duration, duty, &endTick); 376 | } 377 | 378 | void startAdjust() 379 | { 380 | if (!adjust_active) 381 | { 382 | adjust_active = true; 383 | 384 | // 385 | // the first adjustment uses the adjust start duration timing with tp duty 386 | advanceClock(config.ap_start_duration, config.tp_duty); 387 | } 388 | } 389 | 390 | // 391 | // ISR for 1hz interrupt 392 | // 393 | void tick() 394 | { 395 | #ifdef DEBUG_I2CAC 396 | ++ticks; 397 | #endif 398 | #if defined(LED_PIN) 399 | digitalWrite(LED_PIN, !digitalRead(LED_PIN)); 400 | #endif 401 | 402 | if (isEnabled()) 403 | { 404 | if (adjustment != 0) 405 | { 406 | ++adjustment; 407 | startAdjust(); 408 | } 409 | else 410 | { 411 | advanceClock(config.tp_duration, config.tp_duty); 412 | } 413 | } 414 | else 415 | { 416 | if (adjustment != 0) 417 | { 418 | startAdjust(); 419 | } 420 | } 421 | } 422 | 423 | #if defined(PWRFAIL_PIN) 424 | void powerFail() 425 | { 426 | power_failed = true; 427 | 428 | // 429 | // save & clear the clock enabled bit 430 | // 431 | pwrfail_control = control; 432 | control &= ~BIT_ENABLE; 433 | 434 | // 435 | // force any adjustment to finish up. 436 | // 437 | if (adjustment > 1) 438 | { 439 | adjustment = 1; 440 | } 441 | } 442 | #endif 443 | 444 | // 445 | // if we are in a factory reset then clear powerFail data 446 | // and Configuration data and hang out till we are reset with 447 | // 448 | void factoryReset() 449 | { 450 | clearPowerFailData(); 451 | clearConfig(); 452 | reboot(); 453 | } 454 | 455 | void setup() 456 | { 457 | reset_reason = MCUSR; 458 | MCUSR = 0; // reset status flag 459 | wdt_disable(); 460 | 461 | #if defined(SERIAL_BAUD) 462 | Serial.begin(SERIAL_BAUD); 463 | Serial.println(""); 464 | Serial.println("Startup!"); 465 | #endif 466 | 467 | #if defined(LED_PIN) 468 | digitalWrite(LED_PIN, LOW); 469 | pinMode(LED_PIN, OUTPUT); 470 | #endif 471 | 472 | #ifndef TEST_MODE 473 | // 474 | // We don't use ADC features so we disable them to save power 475 | // 476 | ADCSRA &= ~(1 << ADEN); // Disable ADC as we don't use it 477 | PRR |= (1 << PRADC); // Turn off ADC clock 478 | #endif 479 | 480 | config.pwm_top = PWM_TOP; 481 | config.tp_duration = DEFAULT_TP_DURATION_MS; 482 | config.tp_duty = DEFAULT_TP_DUTY; 483 | config.ap_duration = DEFAULT_AP_DURATION_MS; 484 | config.ap_duty = DEFAULT_AP_DUTY; 485 | config.ap_delay = DEFAULT_AP_DELAY_MS; 486 | config.ap_start_duration = DEFAULT_AP_START_MS; 487 | 488 | loadConfig(); 489 | 490 | adjustment = 0; 491 | adjust_active = false; 492 | save_config = false; 493 | factory_reset = false; 494 | 495 | #if defined(PWRFAIL_PIN) 496 | // 497 | // restore clock state if there is power fail data 498 | // 499 | if (!loadPowerFailData()) 500 | { 501 | status |= STATUS_BIT_PWFBAD; 502 | 503 | // 504 | // set up defaults if power save load failed 505 | // 506 | #endif 507 | #if defined(TEST_MODE) || defined(START_ENABLED) 508 | control = BIT_ENABLE; 509 | #else 510 | control = 0; 511 | #endif 512 | 513 | #ifdef SKIP_INITIAL_ADJUST 514 | position = 0; 515 | adjustment = 0; 516 | #else 517 | // 518 | // we need a single adjust at startup to insure that the clock motor 519 | // is synched as a tick/tock. This first tick will "misfire" if the motor 520 | // is out of sync and after that will be in sync. 521 | position = MAX_SECONDS - 1; 522 | adjustment = 1; 523 | #endif 524 | #if defined(PWRFAIL_PIN) 525 | } 526 | 527 | // 528 | // setup the power fail interrupt 529 | // 530 | power_failed = false; 531 | pinMode(PWRFAIL_PIN, INPUT); 532 | attachPinChangeInterrupt(digitalPinToPinChangeInterrupt(PWRFAIL_PIN), &powerFail, FALLING); 533 | #endif 534 | 535 | digitalWrite(A_PIN, TICK_OFF); 536 | digitalWrite(B_PIN, TICK_OFF); 537 | 538 | pinMode(A_PIN, OUTPUT); 539 | pinMode(B_PIN, OUTPUT); 540 | #ifdef TEST_MODE 541 | digitalWrite(LED_PIN, LOW); 542 | pinMode(LED_PIN, OUTPUT); 543 | #else 544 | pinMode(INT_PIN, INPUT); 545 | attachPinChangeInterrupt(digitalPinToPinChangeInterrupt(INT_PIN), &tick, FALLING); 546 | #endif 547 | 548 | #ifndef TEST_MODE 549 | Wire.begin(I2C_ADDRESS); 550 | Wire.onReceive(&i2creceive); 551 | Wire.onRequest(&i2crequest); 552 | #endif 553 | 554 | } 555 | 556 | #ifdef DEBUG_I2CAC 557 | unsigned int last_print; 558 | unsigned int sleep_count; 559 | #endif 560 | 561 | void loop() 562 | { 563 | #ifdef USE_SLEEP 564 | #ifdef USE_POWER_DOWN_MODE 565 | // 566 | // conserve power if i2c is not active and 567 | // there is no timer/PWM running 568 | // 569 | if (!timer_running && !Wire.isActive()) { 570 | set_sleep_mode(SLEEP_MODE_PWR_DOWN); 571 | } 572 | else 573 | { 574 | set_sleep_mode(SLEEP_MODE_IDLE); 575 | } 576 | #endif 577 | 578 | #ifdef DEBUG_I2CAC 579 | sleep_count += 1; 580 | #endif 581 | // sleep! 582 | sleep_enable(); 583 | sleep_cpu(); 584 | sleep_disable(); 585 | #endif 586 | 587 | if (factory_reset) 588 | { 589 | factoryReset(); 590 | } 591 | 592 | if (save_config) 593 | { 594 | save_config = false; 595 | saveConfig(); 596 | } 597 | 598 | #if defined(PWRFAIL_PIN) 599 | // 600 | // power failed and any running timers have finished 601 | // 602 | if (power_failed && !timer_running) 603 | { 604 | savePowerFailData(); 605 | 606 | // 607 | // Now wait for power to return. Its more likely that the 608 | // capacitor will run out first and the we will run from start up. 609 | // 610 | while (digitalRead(PWRFAIL_PIN) == 0) 611 | { 612 | sleep_enable(); 613 | sleep_cpu(); 614 | sleep_disable(); 615 | } 616 | 617 | // 618 | // we can resume!!! 619 | // 620 | cli(); 621 | power_failed = false; 622 | control = pwrfail_control; 623 | sei(); 624 | } 625 | #endif 626 | 627 | #ifdef DEBUG_I2CAC 628 | unsigned int now = ticks; 629 | char buffer[256]; 630 | if (now != last_print && (now%10)==0) 631 | { 632 | last_print = now; 633 | #ifdef DEBUG_I2C 634 | snprintf(buffer, 127, "receives:%d requests:%d errors: %d\n", 635 | receives, requests, errors); 636 | Serial.print(buffer); 637 | #endif 638 | } 639 | #endif 640 | 641 | #ifndef USE_SLEEP 642 | #ifdef TEST_MODE 643 | if (!timer_running) 644 | { 645 | tick(); 646 | delay(1000-tp_duration); 647 | } 648 | #else 649 | delay(100); 650 | #endif 651 | #endif 652 | } 653 | 654 | uint32_t calculateCRC32(const uint8_t *data, size_t length) 655 | { 656 | uint32_t crc = 0xffffffff; 657 | while (length--) 658 | { 659 | uint8_t c = *data++; 660 | for (uint32_t i = 0x80; i > 0; i >>= 1) 661 | { 662 | bool bit = crc & 0x80000000; 663 | if (c & i) 664 | { 665 | bit = !bit; 666 | } 667 | crc <<= 1; 668 | if (bit) 669 | { 670 | crc ^= 0x04c11db7; 671 | } 672 | } 673 | } 674 | return crc; 675 | } 676 | 677 | void clearConfig() 678 | { 679 | for (unsigned int i = 0; i < sizeof(EEConfig); ++i) 680 | { 681 | EEPROM.update(CONFIG_ADDRESS+i, 0xff); 682 | } 683 | } 684 | 685 | boolean loadConfig() 686 | { 687 | EEConfig cfg; 688 | // Read struct from EEPROM 689 | unsigned int i; 690 | uint8_t* p = (uint8_t*) &cfg; 691 | for (i = 0; i < sizeof(cfg); ++i) 692 | { 693 | p[i] = EEPROM.read(CONFIG_ADDRESS+i); 694 | } 695 | uint32_t crcOfData = calculateCRC32(((uint8_t*) &cfg.data), sizeof(cfg.data)); 696 | if (crcOfData != cfg.crc) 697 | { 698 | return false; 699 | } 700 | memcpy((void*)&config, &cfg.data, sizeof(config)); 701 | return true; 702 | } 703 | 704 | void saveConfig() 705 | { 706 | EEConfig cfg; 707 | memcpy(&cfg.data, (const void*)&config, sizeof(cfg.data)); 708 | cfg.crc = calculateCRC32(((uint8_t*) &cfg.data), sizeof(cfg.data)); 709 | 710 | unsigned int i; 711 | uint8_t* p = (uint8_t*) &cfg; 712 | for (i = 0; i < sizeof(cfg); ++i) 713 | { 714 | EEPROM.update(CONFIG_ADDRESS+i, p[i]); 715 | } 716 | } 717 | 718 | #if defined(PWRFAIL_PIN) 719 | void clearPowerFailData() 720 | { 721 | position = MAX_SECONDS; 722 | savePowerFailData(); 723 | } 724 | 725 | boolean loadPowerFailData() 726 | { 727 | EEPowerFailData pfd; 728 | // Read struct from EEPROM 729 | unsigned int i; 730 | uint8_t* p = (uint8_t*) &pfd; 731 | for (i = 0; i < sizeof(pfd); ++i) 732 | { 733 | p[i] = EEPROM.read(POWER_FAIL_ADDRESS+i); 734 | } 735 | 736 | uint32_t crc = calculateCRC32(((uint8_t*) &pfd.data), sizeof(pfd.data)); 737 | if (crc != pfd.crc || pfd.data.position >= MAX_SECONDS || (pfd.data.control & ~BIT_ENABLE) || (pfd.data.status & ~STATUS_BIT_TICK)) 738 | { 739 | return false; 740 | } 741 | 742 | position = pfd.data.position; 743 | control = pfd.data.control; 744 | status = pfd.data.status; 745 | 746 | return true; 747 | } 748 | 749 | void savePowerFailData() 750 | { 751 | EEPowerFailData pfd; 752 | pfd.data.position = position; 753 | pfd.data.control = pwrfail_control; 754 | pfd.data.status = status & STATUS_BIT_TICK; 755 | 756 | pfd.crc = calculateCRC32(((uint8_t*) &pfd.data), sizeof(pfd.data)); 757 | 758 | unsigned int i; 759 | uint8_t* p = (uint8_t*) &pfd; 760 | for (i = 0; i < sizeof(pfd); ++i) 761 | { 762 | EEPROM.update(POWER_FAIL_ADDRESS+i, p[i]); 763 | } 764 | } 765 | 766 | #endif 767 | -------------------------------------------------------------------------------- /I2CAnalogClock/src/I2CAnalogClock.h: -------------------------------------------------------------------------------- 1 | /* 2 | * I2CAnalogClock.h 3 | * 4 | * Copyright 2017 Christopher B. Liebman 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * 19 | * Created on: May 26, 2017 20 | * Author: liebman 21 | */ 22 | 23 | #ifndef _I2CAnalogClock_H_ 24 | #define _I2CAnalogClock_H_ 25 | #include "Arduino.h" 26 | #include "PinChangeInterrupt.h" 27 | #include 28 | #include "Wire.h" 29 | #include "EEPROM.h" 30 | 31 | #if !defined(__AVR_ATtinyX5__) 32 | //#define DEBUG_I2CAC 33 | //#define DEBUG_POSITION 34 | #define SERIAL_BAUD 115200 35 | #endif 36 | 37 | //#define START_ENABLED 38 | //#define SKIP_INITIAL_ADJUST 39 | //#define TEST_MODE 40 | 41 | 42 | #ifndef TEST_MODE 43 | #define USE_SLEEP 44 | #define USE_POWER_DOWN_MODE 45 | #endif 46 | 47 | #ifdef __AVR_ATtinyX5__ 48 | #ifdef TEST_MODE 49 | #define LED_PIN 3 50 | #else 51 | #define INT_PIN 3 52 | #endif 53 | #define A_PIN 1 54 | #define B_PIN 4 55 | #define PWRFAIL_PIN 5 56 | #else 57 | #define INT_PIN 3 58 | #define A_PIN 9 59 | #define B_PIN 10 60 | #define PWRFAIL_PIN 2 61 | #define LED_PIN LED_BUILTIN 62 | #endif 63 | 64 | #define TICK_ON HIGH 65 | #define TICK_OFF LOW 66 | 67 | #define MAX_SECONDS 43200 68 | 69 | 70 | #define I2C_ADDRESS 0x09 71 | 72 | #define CMD_ID 0x00 73 | #define CMD_POSITION 0x01 74 | #define CMD_ADJUSTMENT 0x02 75 | #define CMD_CONTROL 0x03 76 | #define CMD_STATUS 0x04 77 | #define CMD_TP_DURATION 0x05 78 | #define CMD_SAVE_CONFIG 0x06 79 | #define CMD_AP_DURATION 0x07 80 | #define CMD_AP_START 0x08 81 | #define CMD_AP_DELAY 0x09 82 | #define CMD_PWMTOP 0x0a 83 | #define CMD_TP_DUTY 0x0b 84 | #define CMD_AP_DUTY 0x0c 85 | #define CMD_RESET 0x0d 86 | #define CMD_RST_REASON 0x0e 87 | #define CMD_VERSION 0x0f 88 | 89 | // control register bits 90 | #define BIT_ENABLE 0x80 91 | 92 | // status register bits 93 | #define STATUS_BIT_TICK 0x01 94 | #define STATUS_BIT_PWFBAD 0x80 95 | 96 | #define ID_VALUE 0x42 97 | 98 | #define isEnabled() (control & BIT_ENABLE) 99 | 100 | #define isTick() (status & STATUS_BIT_TICK) 101 | #define toggleTick() (status ^= STATUS_BIT_TICK) 102 | 103 | // 104 | // Timing defaults 105 | // 106 | 107 | 108 | #if defined(__AVR_ATtinyX5__) 109 | #if F_CPU == 1000000L 110 | #define PWM_PRESCALE 1 111 | #define PWM_TOP 250 // 4khz 112 | #define PWM_PRESCALE_BITS (_BV(CS10)) 113 | #elif F_CPU == 8000000L 114 | #define PWM_PRESCALE 8 115 | #define PWM_TOP 250 // 4khz 116 | #define PWM_PRESCALE_BITS (_BV(CS12)) 117 | #endif 118 | #else 119 | #define PWM_TOP 250 // assuming 16mhz CPU its 8khz 120 | #define PWM_PRESCALE 8 121 | #define PWM_PRESCALE_BITS (_BV(CS11)) 122 | #endif 123 | 124 | #define ms2PWMCount(x) (F_CPU / PWM_PRESCALE / config.pwm_top / (1000.0 / (double)(x))) 125 | #define duty2pwm(x) ((x)*config.pwm_top/100) 126 | 127 | #ifdef __AVR_ATtinyX5__ 128 | #if F_CPU == 8000000L 129 | #define PRESCALE 4096 130 | #define PRESCALE_BITS ((1 << CS13) | (1 << CS12) | (1 < 11 | #include 12 | #include 13 | #include "Types.h" 14 | #include "String.h" 15 | 16 | class IPAddress 17 | { 18 | public: 19 | IPAddress(); 20 | IPAddress(uint32_t address) 21 | { 22 | sin_addr.s_addr = address; 23 | } 24 | 25 | operator uint32_t() const { 26 | return sin_addr.s_addr; 27 | } 28 | 29 | IPAddress& operator=(const char *address) { 30 | memcpy(&sin_addr, address, sizeof(sin_addr)); 31 | return *this; 32 | } 33 | 34 | IPAddress& operator=(uint32_t address) { 35 | sin_addr.s_addr = address; 36 | return *this; 37 | } 38 | 39 | String toString() const 40 | { 41 | char szRet[16]; 42 | uint8_t *a =(uint8_t*)(&sin_addr.s_addr); 43 | sprintf(szRet,"%u.%u.%u.%u", a[0], a[1], a[2], a[3]); 44 | return String(szRet); 45 | } 46 | private: 47 | struct in_addr sin_addr; // Server address data structure. 48 | }; 49 | 50 | #endif /* IPADDRESS_H_ */ 51 | -------------------------------------------------------------------------------- /NTPTest/src/Logger.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Logger.cpp 3 | * 4 | * Created on: Jul 6, 2017 5 | * Author: chris.l 6 | */ 7 | 8 | #include "Logger.h" 9 | #include 10 | #include 11 | 12 | Logger::Logger() 13 | { 14 | } 15 | 16 | void Logger::begin(long int baud) 17 | { 18 | } 19 | 20 | void Logger::end() 21 | { 22 | } 23 | 24 | void Logger::setNetworkLogger(const char* host, unsigned short port) 25 | { 26 | } 27 | 28 | void Logger::println(const char* message) 29 | { 30 | printf("%s\n", message); 31 | } 32 | 33 | void Logger::printf(const char* fmt, ...) 34 | { 35 | va_list argp; 36 | va_start(argp, fmt); 37 | 38 | vprintf(fmt, argp); 39 | va_end(argp); 40 | } 41 | 42 | void Logger::flush() 43 | { 44 | fflush(stdout); 45 | } 46 | 47 | 48 | Logger logger; 49 | -------------------------------------------------------------------------------- /NTPTest/src/Logger.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Logger.h 3 | * 4 | * Copyright 2017 Christopher B. Liebman 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * 19 | * Created on: May 31, 2017 20 | * Author: liebman 21 | */ 22 | 23 | #ifndef LOGGER_H_ 24 | #define LOGGER_H_ 25 | 26 | class Logger { 27 | public: 28 | Logger(); 29 | void begin(long int baud = 115200L); 30 | void end(); 31 | void setNetworkLogger(const char* host, unsigned short port); 32 | void println(const char*message); 33 | void printf(const char*message, ...); 34 | void flush(); 35 | }; 36 | 37 | extern Logger logger; 38 | 39 | #define DEBUG 40 | #ifdef DEBUG 41 | #define dbprintf(...) logger.printf(__VA_ARGS__) 42 | #define dbprint64(l,v) logger.printf("%s %08x:%08x (%Lf)\n", l, (uint32_t)(v>>32), (uint32_t)(v & 0xffffffff), ((long double)v / 4294967296.)) 43 | #define dbprint64s(l,v) logger.printf("%s %08x:%08x (%Lf)\n", l, (int32_t)(v>>32), (uint32_t)(v & 0xffffffff), ((long double)v / 4294967296.)) 44 | #define dbprintln(x) logger.println(x) 45 | #define dbflush() logger.flush() 46 | #else 47 | #define dbprintf(...) 48 | #define dbprint64(l,v) 49 | #define dbprint64s(l,v) 50 | #define dbprintln(x) 51 | #define dbflush() 52 | #endif 53 | 54 | #endif /* LOGGER_H_ */ 55 | -------------------------------------------------------------------------------- /NTPTest/src/NTP.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * NTPProto.cpp 3 | * 4 | * Created on: Jul 6, 2017 5 | * Author: chris.l 6 | */ 7 | 8 | #include "NTP.h" 9 | 10 | #include "NTPPrivate.h" 11 | #include 12 | #include 13 | #ifdef ESP8266 14 | #include // htonl() & ntohl() 15 | #endif 16 | 17 | #define DEBUG 18 | //#define NTP_DEBUG_PACKET 19 | #include "Logger.h" 20 | 21 | 22 | #ifdef NTP_DEBUG_PACKET 23 | void dumpNTPPacket(NTPPacket* ntp) 24 | { 25 | dbprintf("size: %u\n", sizeof(*ntp)); 26 | dbprintf("firstbyte: 0x%02x\n", *(uint8_t*)ntp); 27 | dbprintf("li: %u\n", getLI(ntp->flags)); 28 | dbprintf("version: %u\n", getVERS(ntp->flags)); 29 | dbprintf("mode: %u\n", getMODE(ntp->flags)); 30 | dbprintf("stratum: %u\n", ntp->stratum); 31 | dbprintf("poll: %u\n", ntp->poll); 32 | dbprintf("precision: %d\n", ntp->precision); 33 | dbprintf("delay: %u\n", ntp->delay); 34 | dbprintf("dispersion: %u\n", ntp->dispersion); 35 | dbprintf("ref_id: %02x:%02x:%02x:%02x\n", ntp->ref_id[0], ntp->ref_id[1], ntp->ref_id[2], ntp->ref_id[3]); 36 | dbprintf("ref_time: %08x:%08x\n", ntp->ref_time.seconds, ntp->ref_time.fraction); 37 | dbprintf("orig_time: %08x:%08x\n", ntp->orig_time.seconds, ntp->orig_time.fraction); 38 | dbprintf("recv_time: %08x:%08x\n", ntp->recv_time.seconds, ntp->recv_time.fraction); 39 | dbprintf("xmit_time: %08x:%08x\n", ntp->xmit_time.seconds, ntp->xmit_time.fraction); 40 | } 41 | #else 42 | #define dumpNTPPacket(x) 43 | #endif 44 | 45 | NTP::NTP(NTPRunTime *runtime, NTPPersist *persist, void (*savePersist)(), int factor) 46 | { 47 | _runtime = runtime; 48 | _persist = persist; 49 | _savePersist = savePersist; 50 | _port = NTP_PORT; 51 | _factor = factor; 52 | dbprintf("****** sizeof(NTPRunTime): %d\n", sizeof(NTPRunTime)); 53 | } 54 | 55 | void NTP::begin(int port) 56 | { 57 | _port = port; 58 | _udp.begin(port); 59 | dbprintf("NTP::begin: nsamples: %d nadjustments: %d, drift: %f\n", _runtime->nsamples, _persist->nadjustments, _persist->drift); 60 | if (_runtime->nsamples == 0 && _runtime->drifted == 0.0) 61 | { 62 | // if we have no samples and drifted is 0 then we probably had a power cycle so invalidate the 63 | // most recent adjustment timestamp. 64 | _persist->adjustments[0].timestamp = 0; 65 | dbprintln("power cycle detected! marking last adjustment as invalid for drift!"); 66 | } 67 | } 68 | 69 | IPAddress NTP::getAddress() 70 | { 71 | return _runtime->ip; 72 | } 73 | 74 | int NTP::getLastOffset(double *offset) 75 | { 76 | if (_runtime->nsamples > 0) 77 | { 78 | *offset = _runtime->samples[0].offset; 79 | return 0; 80 | } 81 | return -1; 82 | } 83 | 84 | uint32_t NTP::getPollInterval() 85 | { 86 | double seconds = 3600/_factor; 87 | 88 | dbprintf("NTP::getPollInterval: drift_estimate: %0.16f poll_interval: %0.16f\n", _runtime->drift_estimate, _runtime->poll_interval); 89 | 90 | //if (_runtime->poll_interval > 0.0) 91 | if (_runtime->drift_estimate != 0.0) 92 | { 93 | // 94 | // estimate the time till we apply the next offset 95 | // 96 | if (_runtime->samples[0].timestamp == _runtime->update_timestamp) 97 | { 98 | seconds = _runtime->poll_interval; 99 | } 100 | else 101 | { 102 | seconds = fabs(_runtime->samples[0].offset) / NTP_OFFSET_THRESHOLD * _runtime->poll_interval; 103 | } 104 | dbprintf("NTP::getPollInterval: seconds: %f\n", seconds); 105 | 106 | if (seconds > (259200/_factor)) // 3 days! 107 | { 108 | dbprintln("NTP::getPollInterval: maxing interval out at 3 days!"); 109 | seconds = 259200/_factor; 110 | } 111 | else if (seconds < (900/_factor)) 112 | { 113 | dbprintln("NTP::getPollInterval: min interval is 15 min!!"); 114 | seconds = 900/_factor; 115 | } 116 | } 117 | 118 | if (_runtime->nsamples < NTP_SAMPLE_COUNT) 119 | { 120 | // 121 | // if we don't have all the samples yet, use a very short interval 122 | // 123 | dbprintln("NTP::getPollInterval: samples not full, 15 minutes!"); 124 | seconds = 900 / _factor; // 15 minutes 125 | } 126 | else if ((_runtime->reach & 0x07) == 0) 127 | { 128 | // 129 | // if the last three polls failed use a very short interval 130 | // 131 | dbprintln("NTP::getPollInterval: last three polls failed, using 15 minutes!"); 132 | seconds = 900 / _factor; 133 | } 134 | else if ((_runtime->reach & 0x01) == 0) 135 | { 136 | // 137 | // if the last poll failed then use a shorter interval 138 | // 139 | dbprintln("NTP::getPollInterval: last poll failed, using 1 hour!"); 140 | seconds = 3600 / _factor; 141 | } 142 | 143 | return (uint32_t)seconds; 144 | } 145 | 146 | int NTP::getOffsetUsingDrift(double *offset_result, int (*getTime)(uint32_t *result)) 147 | { 148 | if (_persist->drift == 0.0) 149 | { 150 | dbprintln("NTP::getOffsetUsingDrift: not enough data to compute/use drift!"); 151 | return -1; 152 | } 153 | 154 | uint32_t now; 155 | if (getTime(&now)) 156 | { 157 | dbprintln("NTP::getOffsetUsingDrift: failed to getTime() failed!"); 158 | return -1; 159 | } 160 | 161 | if (_runtime->drift_timestamp == 0) 162 | { 163 | dbprintln("NTP::getOffsetUsingDrift: first time, setting initial timestamp!"); 164 | _runtime->drift_timestamp = now; 165 | return -1; 166 | } 167 | 168 | if (_runtime->drift_timestamp >= now) 169 | { 170 | dbprintln("NTP::getOffsetUsingDrift: timewarped! resetting timestamp!"); 171 | _runtime->drift_timestamp = now; 172 | return -1; 173 | } 174 | 175 | uint32_t interval = now - _runtime->drift_timestamp; 176 | double offset = (double)interval * _persist->drift / 1000000.0; 177 | dbprintf("NTP::getOffsetUsingDrift: interval: %u drift: %f offset: %f\n", interval, _persist->drift, offset); 178 | 179 | // 180 | // don't use this offset if it does not meet the threshold 181 | // 182 | if (fabs(offset) < NTP_OFFSET_THRESHOLD) 183 | { 184 | dbprintln("NTP::getOffsetUsingDrift: offset not big enough for adjust!"); 185 | return -1; 186 | } 187 | 188 | *offset_result = offset; 189 | _runtime->drift_timestamp = now; 190 | _runtime->drifted += offset; 191 | return 0; 192 | } 193 | 194 | // return next poll delay or -1 on error. 195 | int NTP::getOffset(const char* server, double *offset, int (*getTime)(uint32_t *result)) 196 | { 197 | _runtime->reach <<= 1; 198 | 199 | IPAddress address = _runtime->ip; 200 | 201 | // 202 | // if we don't have an ip address or the server name does not match the the one we have one or 203 | // its not reachable then lookup a new one. 204 | // 205 | if (_runtime->ip == 0 || strncmp(server, _runtime->server, NTP_SERVER_LENGTH) != 0 || _runtime->reach == 0) 206 | { 207 | //dbprintln("NTP::getOffset: updating server and address!"); 208 | if (!WiFi.hostByName(server, address)) 209 | { 210 | dbprintf("NTP::getOffset: DNS lookup on %s failed!\n", server); 211 | return -1; 212 | } 213 | 214 | memset((void*)_runtime->server, 0, sizeof(_runtime->server )); 215 | strncpy(_runtime->server, server, NTP_SERVER_LENGTH-1); 216 | _runtime->ip = address; 217 | 218 | dbprintf("NTP::getOffset: NEW server: %s address: %s\n", server, address.toString().c_str()); 219 | 220 | // we forget the existing data when we change NTP servers 221 | _runtime->nsamples = 0; 222 | } 223 | 224 | // 225 | // Ping the server first, we don't care about the result. This updates any 226 | // ARP cache etc. Without this we see a varying 20ms -> 80ms delay on the 227 | // NTP packet. 228 | // 229 | Ping ping; 230 | ping.ping(address); 231 | 232 | dbflush(); 233 | 234 | Timer timer; 235 | NTPTime now; 236 | NTPPacket ntp; 237 | 238 | memset((void*) &ntp, 0, sizeof(ntp)); 239 | ntp.flags = setLI(LI_NONE) | setVERS(NTP_VERSION) | setMODE(MODE_CLIENT); 240 | ntp.poll = MINPOLL; 241 | 242 | uint32_t start; 243 | if (getTime(&start)) 244 | { 245 | dbprintln("NTP::getOffset: failed to getTime() failed!"); 246 | return -1; 247 | } 248 | 249 | timer.start(); 250 | 251 | now.seconds = toNTP(start); 252 | now.fraction = 0; 253 | // put non-zero timestamps in network byte order 254 | ntp.orig_time.seconds = htonl(ntp.orig_time.seconds); 255 | ntp.orig_time.fraction = htonl(ntp.orig_time.fraction); 256 | 257 | ntp.orig_time = now; 258 | 259 | dumpNTPPacket(&ntp); 260 | 261 | 262 | _udp.open(address, _port); 263 | _udp.send(&ntp, sizeof(ntp)); 264 | 265 | memset(&ntp, 0, sizeof(ntp)); 266 | 267 | int size = _udp.recv(&ntp, sizeof(ntp), 1000); 268 | uint32_t duration = timer.stop(); 269 | 270 | dbprintf("NTP::getOffset: used server: %s address: %s\n", _runtime->server, address.toString().c_str()); 271 | dbprintf("NTP::getOffset: packet size: %d\n", size); 272 | dbprintf("NTP::getOffset: duration %ums\n", duration); 273 | 274 | if (size != 48) 275 | { 276 | dbprintln("NTP::getOffset: bad packet!"); 277 | return -1; 278 | } 279 | 280 | ntp.delay = ntohl(ntp.delay); 281 | ntp.dispersion = ntohl(ntp.dispersion); 282 | ntp.ref_time.seconds = ntohl(ntp.ref_time.seconds); 283 | ntp.ref_time.fraction = ntohl(ntp.ref_time.fraction); 284 | ntp.recv_time.seconds = ntohl(ntp.recv_time.seconds); 285 | ntp.recv_time.fraction = ntohl(ntp.recv_time.fraction); 286 | ntp.xmit_time.seconds = ntohl(ntp.xmit_time.seconds); 287 | ntp.xmit_time.fraction = ntohl(ntp.xmit_time.fraction); 288 | ntp.orig_time = now; 289 | 290 | dumpNTPPacket(&ntp); 291 | 292 | // 293 | // assumes start at second start and less than 1 second duration 294 | // 295 | now.fraction = ms2fraction(duration); 296 | 297 | int err = packet(&ntp, now); 298 | if (err) 299 | { 300 | dbprintf("NTP::getOffset: packet returns: %d\n", err); 301 | return err; 302 | } 303 | 304 | *offset = _runtime->samples[0].offset; 305 | 306 | // 307 | // set the update and drift timestamps. 308 | // 309 | _runtime->update_timestamp = _runtime->samples[0].timestamp; 310 | _runtime->drift_timestamp = _runtime->samples[0].timestamp; 311 | return 0; 312 | } 313 | 314 | int NTP::packet(NTPPacket* ntp, NTPTime now) 315 | { 316 | dbprintf("NTP::packet: nsamples: %d nadjustments: %d\n", _runtime->nsamples, _persist->nadjustments); 317 | if (ntp->stratum == 0) 318 | { 319 | dbprintln("NTP::packet: bad stratum!"); 320 | return -1; 321 | } 322 | 323 | if (getLI(ntp->flags) == LI_NOSYNC) 324 | { 325 | dbprintln("NTP::packet: leap indicator indicates NOSYNC!"); 326 | return -1; /* unsynchronized */ 327 | } 328 | 329 | /* 330 | * Verify valid root distance. 331 | */ 332 | //dbprintf("root distance: %lf reftime:%u xmit_time:%u\n", p->rootdelay / 2 + p->rootdisp, p->reftime_s, ntp->xmit_time.seconds); 333 | //if (p->rootdelay / 2 + p->rootdisp >= MAXDISP || p->reftime_s > ntp->xmit_time.seconds) 334 | //{ 335 | // dbprintln("invalid root distance or new time before prev time!"); 336 | // return -1; /* invalid header values */ 337 | //} 338 | 339 | uint64_t T1 = toUINT64(ntp->orig_time); 340 | uint64_t T2 = toUINT64(ntp->recv_time); 341 | uint64_t T3 = toUINT64(ntp->xmit_time); 342 | uint64_t T4 = toUINT64(now); 343 | 344 | double offset = LFP2D(((int64_t)(T2 - T1) + (int64_t)(T3 - T4)) / 2); 345 | double delay = LFP2D( (int64_t)(T4 - T1) - (int64_t)(T3 - T2)); 346 | 347 | dbprintf("NTP::packet: offset: %0.6lf delay: %0.6lf\n", offset, delay); 348 | 349 | int i; 350 | for (i = _runtime->nsamples - 1; i >= 0; --i) 351 | { 352 | if (i == NTP_SAMPLE_COUNT - 1) 353 | { 354 | continue; 355 | } 356 | _runtime->samples[i + 1] = _runtime->samples[i]; 357 | dbprintf("NTP::packet: samples[%d]: %lf delay:%lf timestamp:%u\n", 358 | i + 1, _runtime->samples[i+1].offset, _runtime->samples[i+1].delay, _runtime->samples[i+1].timestamp); 359 | } 360 | 361 | _runtime->samples[0].timestamp = now.seconds; 362 | _runtime->samples[0].offset = offset; 363 | _runtime->samples[0].delay = delay; 364 | dbprintf("NTP::packet: samples[%d]: %lf delay:%lf timestamp:%u\n", 365 | 0, _runtime->samples[0].offset, _runtime->samples[0].delay, _runtime->samples[0].timestamp); 366 | 367 | if (_runtime->nsamples < NTP_SAMPLE_COUNT) 368 | { 369 | _runtime->nsamples += 1; 370 | } 371 | 372 | // compute the delay mean and std deviation 373 | 374 | double mean = 0.0; 375 | for (i = 0; i < _runtime->nsamples; ++i) 376 | { 377 | mean = mean + _runtime->samples[i].delay; 378 | } 379 | mean = mean / _runtime->nsamples; 380 | double delay_std = 0.0; 381 | for (i = 0; i < _runtime->nsamples; ++i) 382 | { 383 | delay_std = delay_std + pow(_runtime->samples[i].delay - mean, 2); 384 | } 385 | delay_std = SQRT(delay_std / _runtime->nsamples); 386 | dbprintf("NTP::packet: delay STD DEV: %lf, mean: %lf\n", delay_std, mean); 387 | 388 | // 389 | // don't use this offset if its off of the mean by moth than one std deviation 390 | if ((fabs(_runtime->samples[0].delay) - mean) > delay_std) 391 | { 392 | dbprintln("NTP::packet: sample delay too big!"); 393 | return -1; 394 | } 395 | 396 | // 397 | // good delay - we can mark this as reachable 398 | // 399 | _runtime->reach |= 1; 400 | 401 | // 402 | // update drift estimate 403 | // 404 | updateDriftEstimate(); 405 | 406 | // 407 | // don't use this offset if it does not meet the threshold 408 | // 409 | if (fabs(offset) < NTP_OFFSET_THRESHOLD) 410 | { 411 | dbprintln("NTP::packet: offset not big enough for adjust!"); 412 | return -1; 413 | } 414 | 415 | // 416 | // save adjustment samples & compute drift 417 | // 418 | clock(); 419 | 420 | // 421 | // sample ok to use. 422 | // 423 | return 0; 424 | } 425 | 426 | // 427 | // process local clock, return drift 428 | // 429 | void NTP::clock() 430 | { 431 | if (_runtime->nsamples >= NTP_SAMPLE_COUNT ) 432 | { 433 | for (int i = _persist->nadjustments - 1; i >= 0; --i) 434 | { 435 | if (i == NTP_ADJUSTMENT_COUNT - 1) 436 | { 437 | continue; 438 | } 439 | _persist->adjustments[i + 1] = _persist->adjustments[i]; 440 | dbprintf("NTP::clock: adjustments[%d]: %lf timestamp:%u\n", 441 | i + 1, _persist->adjustments[i+1].adjustment, _persist->adjustments[i+1].timestamp); 442 | } 443 | 444 | // use the newest sample and include any drift we have applied. 445 | _persist->adjustments[0].timestamp = _runtime->samples[0].timestamp; 446 | _persist->adjustments[0].adjustment = _runtime->samples[0].offset + _runtime->drifted; 447 | _runtime->drifted = 0.0; 448 | dbprintf("NTP::clock: adjustments[%d]: %lf timestamp:%u\n", 449 | 0, _persist->adjustments[0].adjustment, _persist->adjustments[0].timestamp); 450 | 451 | if (_persist->nadjustments < NTP_ADJUSTMENT_COUNT) 452 | { 453 | _persist->nadjustments += 1; 454 | } 455 | 456 | // 457 | // calculate drift if we have some NTP_ADJUSTMENT_COUNT adjustments 458 | // 459 | if (_persist->nadjustments >= 4) 460 | { 461 | computeDrift(&_persist->drift); 462 | dbprintf("NTP::clock: drift: %f\n", _persist->drift); 463 | } 464 | dbprintln("NTP::clock: saving 'persist' data!"); 465 | _savePersist(); 466 | } 467 | } 468 | 469 | // 470 | // drift is the adjustment "per second" converted to parts per million 471 | // 472 | int NTP::computeDrift(double* drift_result) 473 | { 474 | double a = 0.0; 475 | 476 | uint32_t seconds = 0; 477 | for(int i = 0; i <= _persist->nadjustments-2; ++i) 478 | { 479 | if (_persist->adjustments[i].timestamp != 0 && _persist->adjustments[i+1].timestamp != 0) 480 | { 481 | // valid sample! 482 | seconds += _persist->adjustments[i].timestamp - _persist->adjustments[i+1].timestamp; 483 | a += _persist->adjustments[i].adjustment; 484 | dbprintf("NTP::computeDrift: using adjustment %d and %d delta: %d adj:%f\n", i, i+1, _persist->adjustments[i].timestamp - _persist->adjustments[i+1].timestamp, _persist->adjustments[i].adjustment); 485 | } 486 | } 487 | a = a / (double)seconds; 488 | double drift = a * 1000000; 489 | dbprintf("NTP::computeDrift: seconds: %d a: %f drift: %f\n", seconds, a, drift); 490 | 491 | dbprintf("computeDrift: drift: %f PPM\n", drift); 492 | if (drift_result != NULL) 493 | { 494 | *drift_result = drift; 495 | } 496 | 497 | return 0; 498 | } 499 | 500 | void NTP::updateDriftEstimate() 501 | { 502 | uint32_t timebase = _runtime->update_timestamp; // we only look at timestamps after this. 503 | // no update yet! 504 | if (timebase == 0) 505 | { 506 | timebase = _runtime->samples[_runtime->nsamples-1].timestamp; 507 | } 508 | double sx = 0.0; 509 | double sy = 0.0; 510 | double sxy = 0.0; 511 | double sxx = 0.0; 512 | int n = 0; 513 | for (int i = 0; i < _runtime->nsamples && _runtime->samples[i].timestamp >= timebase; ++i) 514 | { 515 | double x = (double)(_runtime->samples[i].timestamp - timebase); 516 | double y = _runtime->samples[i].offset; 517 | dbprintf("NTP::computeDriftEstimate: x:%-0.8f y:%-0.8f\n", x, y); 518 | sx += x; 519 | sy += y; 520 | sxy += x*y; 521 | sxx += x*x; 522 | ++n; 523 | } 524 | 525 | dbprintf("NTP::computeDriftEstimate: found %d samples\n", n); 526 | 527 | if (n < 4) 528 | { 529 | dbprintln("NTP::computeDriftEstimate: not enough points!"); 530 | } 531 | else 532 | { 533 | double slope = ( sx*sy - n*sxy ) / ( sx*sx - n*sxx ); 534 | dbprintf("NTP::computeDriftEstimate: slope: %0.16f\n", slope); 535 | 536 | _runtime->drift_estimate = slope * 1000000; 537 | 538 | _runtime->poll_interval = NTP_OFFSET_THRESHOLD / (fabs(_runtime->drift_estimate) / 1000000.0); 539 | dbprintf("NTP::updateDriftEstimate: poll interval: %f\n", _runtime->poll_interval); 540 | } 541 | 542 | dbprintf("NTP::computeDriftEstimate: ESTIMATED DRIFT: %0.16f\n", _runtime->drift_estimate); 543 | } 544 | -------------------------------------------------------------------------------- /NTPTest/src/NTP.h: -------------------------------------------------------------------------------- 1 | /* 2 | * NTPProto.h 3 | * 4 | * Created on: Jul 6, 2017 5 | * Author: chris.l 6 | */ 7 | 8 | #ifndef NTP_H_ 9 | #define NTP_H_ 10 | 11 | #include "Arduino.h" 12 | #include "Ping.h" 13 | #include "Timer.h" 14 | #include "UDPWrapper.h" 15 | 16 | #define NTP_PORT 123 17 | 18 | typedef struct ntp_sample 19 | { 20 | uint32_t timestamp; 21 | double offset; 22 | double delay; 23 | } NTPSample; 24 | 25 | typedef struct ntp_adjustment 26 | { 27 | uint32_t timestamp; 28 | double adjustment; 29 | } NTPAdjustment; 30 | 31 | #define NTP_SERVER_LENGTH 64 // max length+1 of ntp server name 32 | #define NTP_SAMPLE_COUNT 8 // number of NTP samples to keep for std devation filtering 33 | #define NTP_ADJUSTMENT_COUNT 8 // number of NTP adjustments to keep for least squares drift 34 | #define NTP_OFFSET_THRESHOLD 0.02 // 20ms offset minimum for adjust! 35 | 36 | // 37 | // Long term persisted data includes drift an last adjustment information 38 | // so that we don't have to wait for a long time after power loss for drift 39 | // to be active once we have computed it the first time. 40 | // 41 | typedef struct ntp_persist 42 | { 43 | NTPAdjustment adjustments[NTP_ADJUSTMENT_COUNT]; 44 | int nadjustments; 45 | double drift; // computed drift in parts per million 46 | } NTPPersist; 47 | 48 | // 49 | // This is used to validate new NTP responses and compute the clock drift 50 | // 51 | typedef struct ntp_runtime 52 | { 53 | NTPSample samples[NTP_SAMPLE_COUNT]; 54 | int nsamples; 55 | uint32_t drift_timestamp; // last time drift was applied 56 | double drifted; // how much drift we have applied since the last NTP poll. 57 | uint32_t update_timestamp; // last time an update was applied 58 | double drift_estimate; // used to compute the poll interval 59 | double poll_interval; // estimated time between adjustments based on estimated drift 60 | // cache these to know when we need to lookup the host again and if its been unreachable. 61 | char server[NTP_SERVER_LENGTH]; // cached server name 62 | uint32_t ip; // cached server ip address (only works for tcp v4) 63 | uint8_t reach; 64 | } NTPRunTime; 65 | 66 | typedef struct ntp_time 67 | { 68 | uint32_t seconds; 69 | uint32_t fraction; 70 | } NTPTime; 71 | 72 | typedef struct ntp_packet 73 | { 74 | uint8_t flags; 75 | uint8_t stratum; 76 | uint8_t poll; 77 | int8_t precision; 78 | uint32_t delay; 79 | uint32_t dispersion; 80 | uint8_t ref_id[4]; 81 | NTPTime ref_time; 82 | NTPTime orig_time; 83 | NTPTime recv_time; 84 | NTPTime xmit_time; 85 | } NTPPacket; 86 | 87 | class NTP 88 | { 89 | public: 90 | NTP(NTPRunTime *runtime, NTPPersist *persist, void (*savePersist)(), int factor=1); 91 | void begin(int port = NTP_PORT); 92 | 93 | uint32_t getPollInterval(); 94 | int getOffsetUsingDrift(double *offset, int (*getTime)(uint32_t *result)); 95 | // return next poll delay or -1 on error. 96 | int getOffset(const char* server, double* offset, int (*getTime)(uint32_t *result)); 97 | int getLastOffset(double* offset); 98 | IPAddress getAddress(); 99 | private: 100 | NTPRunTime *_runtime; 101 | NTPPersist *_persist; 102 | void (*_savePersist)(); 103 | UDPWrapper _udp; 104 | int _port; 105 | int _factor; // only used when testing to reduce fixed poll interval values by factor 106 | int packet(NTPPacket* packet, NTPTime now); 107 | void clock(); 108 | int computeDrift(double* drift_result); 109 | void updateDriftEstimate(); 110 | }; 111 | 112 | 113 | #endif /* NTP_H_ */ 114 | -------------------------------------------------------------------------------- /NTPTest/src/NTPPrivate.h: -------------------------------------------------------------------------------- 1 | /* 2 | * NTPPrivate.h 3 | * 4 | * Created on: Jul 6, 2017 5 | * Author: chris.l 6 | */ 7 | 8 | #ifndef NTPPRIVATE_H_ 9 | #define NTPPRIVATE_H_ 10 | 11 | #define LI_NONE 0 12 | #define LI_SIXTY_ONE 1 13 | #define LI_FIFTY_NINE 2 14 | #define LI_NOSYNC 3 15 | 16 | #define MODE_RESERVED 0 17 | #define MODE_ACTIVE 1 18 | #define MODE_PASSIVE 2 19 | #define MODE_CLIENT 3 20 | #define MODE_SERVER 4 21 | #define MODE_BROADCAST 5 22 | #define MODE_CONTROL 6 23 | #define MODE_PRIVATE 7 24 | 25 | #define NTP_VERSION 4 26 | 27 | #define MINPOLL 6 // % minimum poll interval (64 s) 28 | #define MAXPOLL 17 /* % maximum poll interval (36.4 h) */ 29 | 30 | 31 | #define setLI(value) ((value&0x03)<<6) 32 | #define setVERS(value) ((value&0x07)<<3) 33 | #define setMODE(value) ((value&0x07)) 34 | 35 | #define getLI(value) ((value>>6)&0x03) 36 | #define getVERS(value) ((value>>3)&0x07) 37 | #define getMODE(value) (value&0x07) 38 | 39 | #define SEVENTY_YEARS 2208988800L 40 | #define toEPOCH(t) ((uint32_t)t-SEVENTY_YEARS) 41 | #define toNTP(t) ((uint32_t)t+SEVENTY_YEARS) 42 | #define toUINT64(x) (((uint64_t)(x.seconds)<<32) + x.fraction) 43 | 44 | #define FP2D(x) ((double)(x)/65536) 45 | #define LFP2D(x) (((double)(x))/4294967296L) 46 | #define ms2fraction(x) ((uint32_t)((double)(x) / 1000.0 * (double)4294967296L)) 47 | #define LOG2D(a) ((a) < 0 ? 1. / (1L << -(a)) : 1L << (a)) 48 | #define SQUARE(x) ((x) * (x)) 49 | #define SQRT(x) (sqrt(x)) 50 | 51 | // simple versions - we don't worry about side effects 52 | #define max(a, b) ((a) < (b) ? (b) : (a)) 53 | #define min(a, b) ((a) < (b) ? (a) : (b)) 54 | 55 | #endif /* NTPPRIVATE_H_ */ 56 | -------------------------------------------------------------------------------- /NTPTest/src/NTPTest.cpp: -------------------------------------------------------------------------------- 1 | //============================================================================ 2 | // Name : NTPTest.cpp 3 | // Author : 4 | // Version : 5 | // Copyright : Your copyright notice 6 | // Description : Hello World in C++, Ansi-style 7 | //============================================================================ 8 | 9 | #include "NTP.h" 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | #define SPEEDUP_FACTOR 100 16 | #define MAX_SLEEP (3600 / SPEEDUP_FACTOR) 17 | #define PERSIST_FILE "/tmp/ntp_persist.data" 18 | 19 | uint32_t start_time = 0; 20 | uint32_t last_time = 0; 21 | double current_offset = 0.0; 22 | double fake_drift_ppm = 1.0*SPEEDUP_FACTOR; 23 | uint32_t sleep_left = 0; 24 | 25 | NTPPersist persist; 26 | 27 | void loadPersist() 28 | { 29 | printf("loadPersist()\n"); 30 | FILE *fp = fopen(PERSIST_FILE, "r"); 31 | if (fp == NULL) 32 | { 33 | printf("loadPersist: failed to open '%s'!\n", PERSIST_FILE); 34 | return; 35 | } 36 | if (fread(&persist, sizeof(persist), 1, fp) != 1) 37 | { 38 | printf("loadPersist: fread() failed!!!\n"); 39 | memset(&persist, 0, sizeof(persist)); 40 | } 41 | fclose(fp); 42 | } 43 | 44 | void savePersist() 45 | { 46 | printf("savePersist()\n"); 47 | FILE *fp = fopen(PERSIST_FILE, "w"); 48 | if (fp == NULL) 49 | { 50 | printf("savePersist: failed to open '%s'!\n", PERSIST_FILE); 51 | return; 52 | } 53 | if (fwrite(&persist, sizeof(persist), 1, fp) != 1) 54 | { 55 | printf("savePersist: fwrite() failed!!!\n"); 56 | } 57 | fclose(fp); 58 | } 59 | 60 | void adjustOffsetByDrift() 61 | { 62 | struct timeval tp; 63 | gettimeofday(&tp, NULL); 64 | 65 | if (!start_time) 66 | { 67 | start_time = (uint32_t)tp.tv_sec; 68 | } 69 | 70 | // add fake drift to offset 71 | if (last_time) 72 | { 73 | double drift = fake_drift_ppm * (((double)tp.tv_sec - (double)last_time) / 1000000.); 74 | current_offset += drift; 75 | double hours = (double)(tp.tv_sec - start_time) / (3600.0 / SPEEDUP_FACTOR); 76 | printf("HOURS: %f applying fake drift: %lfms for %ld seconds current_offset: %f\n", hours, drift, tp.tv_sec - last_time, current_offset); 77 | } 78 | last_time = (uint32_t)tp.tv_sec; 79 | } 80 | 81 | int getTime(uint32_t *result) 82 | { 83 | struct timeval tp; 84 | gettimeofday(&tp, NULL); 85 | 86 | // apply the current offset 87 | double x = (double)tp.tv_sec + (double)tp.tv_usec / 1000000.; 88 | x += current_offset; 89 | tp.tv_sec = (long)x; 90 | tp.tv_usec = (uint32_t)((x-(double)tp.tv_sec) * 1000000.); 91 | 92 | // sync to the next second boundary 93 | if (tp.tv_usec != 0) 94 | { 95 | usleep(1000000-tp.tv_usec); 96 | } 97 | *result = tp.tv_sec+1; 98 | return 0; 99 | } 100 | 101 | int main(int argc, char**argv) 102 | { 103 | const char *server = "192.168.0.31"; 104 | printf("argc: %d\n", argc); 105 | if (argc > 1) 106 | { 107 | server = argv[1]; 108 | } 109 | NTPRunTime runtime; 110 | memset(&persist, 0, sizeof(persist)); 111 | memset(&runtime, 0, sizeof(runtime)); 112 | loadPersist(); 113 | NTP test(&runtime, &persist, &savePersist, SPEEDUP_FACTOR); 114 | test.begin(); 115 | for (int i = 0; i < 1000; ++i) 116 | { 117 | adjustOffsetByDrift(); 118 | 119 | double offset = 0.0; 120 | int err = test.getOffsetUsingDrift(&offset, &getTime); 121 | if (!err) 122 | { 123 | current_offset += offset; 124 | printf("****** DRIFT: %f current_offset: %f\n", offset, current_offset); 125 | } 126 | 127 | if (sleep_left == 0) 128 | { 129 | int err = test.getOffset(server, &offset, &getTime); 130 | if (!err) 131 | { 132 | current_offset += offset; 133 | printf("****** OFFSET: %f current_offset: %f\n", offset, current_offset); 134 | } 135 | sleep_left = test.getPollInterval(); 136 | } 137 | int interval = MAX_SLEEP; 138 | if (sleep_left > MAX_SLEEP) 139 | { 140 | sleep_left -= MAX_SLEEP; 141 | } 142 | else 143 | { 144 | interval = sleep_left; 145 | sleep_left = 0; 146 | } 147 | printf("sleeping %d seconds (sleep_left: %u)\n", interval, sleep_left); 148 | fflush(stdout); 149 | sleep(interval); 150 | } 151 | 152 | printf("Done!\n"); 153 | return 0; 154 | } 155 | -------------------------------------------------------------------------------- /NTPTest/src/Ping.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Ping.cpp 3 | * 4 | * Created on: Jul 7, 2017 5 | * Author: chris.l 6 | */ 7 | 8 | #include "Ping.h" 9 | 10 | Ping::Ping() 11 | { 12 | // TODO Auto-generated constructor stub 13 | 14 | } 15 | 16 | -------------------------------------------------------------------------------- /NTPTest/src/Ping.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Ping.h 3 | * 4 | * Created on: Jul 7, 2017 5 | * Author: chris.l 6 | */ 7 | 8 | #ifndef PING_H_ 9 | #define PING_H_ 10 | #include "Arduino.h" 11 | 12 | class Ping 13 | { 14 | public: 15 | Ping(); 16 | void ping(IPAddress address) { 17 | } 18 | }; 19 | 20 | #endif /* PING_H_ */ 21 | -------------------------------------------------------------------------------- /NTPTest/src/String.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * String.cpp 3 | * 4 | * Created on: Jul 8, 2017 5 | * Author: liebman 6 | */ 7 | 8 | #include "String.h" 9 | #include 10 | #include 11 | 12 | String::String() 13 | { 14 | value = NULL; 15 | } 16 | String::String(const char* str) 17 | { 18 | value = (char*)malloc(strlen(str)+1); 19 | strcpy(value, str); 20 | } 21 | 22 | String::~String() 23 | { 24 | if (value != NULL) 25 | { 26 | free(value); 27 | value = NULL; 28 | } 29 | } 30 | 31 | const char* String::c_str() 32 | { 33 | return value; 34 | } 35 | -------------------------------------------------------------------------------- /NTPTest/src/String.h: -------------------------------------------------------------------------------- 1 | /* 2 | * String.h 3 | * 4 | * Created on: Jul 8, 2017 5 | * Author: liebman 6 | */ 7 | 8 | #ifndef STRING_H_ 9 | #define STRING_H_ 10 | 11 | class String 12 | { 13 | public: 14 | String(); 15 | String(const char* str); 16 | virtual ~String(); 17 | const char* c_str(); 18 | private: 19 | char* value; 20 | }; 21 | 22 | #endif /* STRING_H_ */ 23 | -------------------------------------------------------------------------------- /NTPTest/src/Timer.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Timer.cpp 3 | * 4 | * Created on: Jul 7, 2017 5 | * Author: chris.l 6 | */ 7 | 8 | #include "Timer.h" 9 | #include 10 | #include "Logger.h" 11 | 12 | uint32_t Timer::_epoch = 0; 13 | 14 | Timer::Timer() 15 | { 16 | _start = 0; 17 | } 18 | 19 | uint32_t Timer::getMillis() 20 | { 21 | struct timeval tp; 22 | gettimeofday(&tp, NULL); 23 | if (_epoch == 0) 24 | { 25 | _epoch = tp.tv_sec; 26 | } 27 | //dbprintf("getMillis() %u:%u\n", tp.tv_sec, tp.tv_usec); 28 | 29 | uint32_t ms = (tp.tv_sec-_epoch) * 1000 + tp.tv_usec / 1000; 30 | return ms; 31 | } 32 | 33 | void Timer::start() { 34 | _start = getMillis(); 35 | } 36 | 37 | uint32_t Timer::stop() { 38 | uint32_t stop = getMillis(); 39 | return (stop - _start); 40 | } 41 | -------------------------------------------------------------------------------- /NTPTest/src/Timer.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Timer.h 3 | * 4 | * Created on: Jul 7, 2017 5 | * Author: chris.l 6 | */ 7 | 8 | #ifndef TIMER_H_ 9 | #define TIMER_H_ 10 | #include "Arduino.h" 11 | 12 | class Timer 13 | { 14 | public: 15 | Timer(); 16 | 17 | static uint32_t getMillis(); 18 | void start(); 19 | uint32_t stop(); 20 | 21 | private: 22 | static uint32_t _epoch; 23 | uint32_t _start; 24 | }; 25 | 26 | #endif /* TIMER_H_ */ 27 | -------------------------------------------------------------------------------- /NTPTest/src/Types.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Types.h 3 | * 4 | * Created on: Jul 7, 2017 5 | * Author: chris.l 6 | */ 7 | 8 | #ifndef TYPES_H_ 9 | #define TYPES_H_ 10 | 11 | 12 | 13 | //typedef char int8_t; 14 | typedef unsigned char uint8_t; 15 | typedef short int16_t; 16 | typedef unsigned short uint16_t; 17 | typedef int int32_t; 18 | typedef unsigned int uint32_t; 19 | //typedef long int64_t; 20 | //typedef unsigned long uint64_t; 21 | 22 | #endif /* TYPES_H_ */ 23 | -------------------------------------------------------------------------------- /NTPTest/src/UDPWrapper.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * UDP.cpp 3 | * 4 | * Created on: Jul 8, 2017 5 | * Author: liebman 6 | */ 7 | 8 | #include "UDPWrapper.h" 9 | 10 | #include "Logger.h" 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | UDPWrapper::UDPWrapper() 17 | { 18 | _sockfd = -1; 19 | _local_port = -1; 20 | } 21 | 22 | UDPWrapper::~UDPWrapper() 23 | { 24 | close(); 25 | } 26 | 27 | int UDPWrapper::begin(int local_port) 28 | { 29 | _local_port = local_port; 30 | return 0; 31 | } 32 | 33 | int UDPWrapper::open(IPAddress address, uint16_t port) 34 | { 35 | struct sockaddr_in serv_addr; // Server address data structure. 36 | 37 | if (_local_port == -1) 38 | { 39 | dbprintln("UDP::open: begin() not called!"); 40 | return -1; 41 | } 42 | 43 | _sockfd = socket( AF_INET, SOCK_DGRAM, IPPROTO_UDP ); // Create a UDP socket. 44 | 45 | if ( _sockfd < 0 ) 46 | { 47 | dbprintln("socket() failed!"); 48 | return -1; 49 | } 50 | 51 | bzero( ( char* ) &serv_addr, sizeof( serv_addr ) ); 52 | 53 | serv_addr.sin_family = AF_INET; 54 | 55 | // Copy the server's IP address to the server address structure. 56 | 57 | serv_addr.sin_addr.s_addr = address; 58 | 59 | // Convert the port number integer to network big-endian style and save it to the server address structure. 60 | 61 | serv_addr.sin_port = htons(port); 62 | 63 | // Call up the server using its IP address and port number. 64 | 65 | if ( connect( _sockfd, ( struct sockaddr * ) &serv_addr, sizeof( serv_addr) ) < 0 ) 66 | { 67 | dbprintf("connect() failed: %s\n", strerror(errno)); 68 | return -1; 69 | } 70 | 71 | return 0; 72 | } 73 | 74 | int UDPWrapper::send(void* buffer, size_t size) 75 | { 76 | int n = ::write( _sockfd, ( char* ) buffer, size ); 77 | 78 | if ( n < 0 ) 79 | { 80 | dbprintf("write failed! expected %d got %d\n", size, n); 81 | } 82 | return n; 83 | } 84 | 85 | int UDPWrapper::recv(void* buffer, size_t size, unsigned int timeout_ms) 86 | { 87 | struct pollfd fd; 88 | int n; 89 | 90 | fd.fd = _sockfd; 91 | fd.events = POLLIN; 92 | n = ::poll(&fd, 1, timeout_ms); 93 | 94 | if (n == 0) 95 | { 96 | dbprintln("UDP::recv timeout!"); 97 | return n; 98 | } 99 | 100 | n = ::read(_sockfd, buffer, size); 101 | return n; 102 | } 103 | 104 | int UDPWrapper::close() 105 | { 106 | if (_sockfd != -1) 107 | { 108 | ::close(_sockfd); 109 | } 110 | return 0; 111 | } 112 | -------------------------------------------------------------------------------- /NTPTest/src/UDPWrapper.h: -------------------------------------------------------------------------------- 1 | /* 2 | * UDP.h 3 | * 4 | * Created on: Jul 8, 2017 5 | * Author: liebman 6 | */ 7 | 8 | #ifndef UDPWRAPPER_H_ 9 | #define UDPWRAPPER_H_ 10 | #include "Arduino.h" 11 | 12 | class UDPWrapper 13 | { 14 | public: 15 | UDPWrapper(); 16 | virtual ~UDPWrapper(); 17 | int begin(int local_port); 18 | int open(IPAddress address, uint16_t port); 19 | int send(void* buffer, size_t size); 20 | int recv(void* buffer, size_t size, unsigned int timeout_ms); 21 | int close(); 22 | private: 23 | int _local_port; 24 | int _sockfd; 25 | }; 26 | 27 | #endif /* UDPWRAPPER_H_ */ 28 | -------------------------------------------------------------------------------- /NTPTest/src/UnixWiFi.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * UnixWiFi.cpp 3 | * 4 | * Created on: Jul 7, 2017 5 | * Author: chris.l 6 | */ 7 | 8 | #include "UnixWiFi.h" 9 | #include 10 | #include 11 | 12 | UnixWiFi::UnixWiFi() 13 | { 14 | // TODO Auto-generated constructor stub 15 | 16 | } 17 | 18 | int UnixWiFi::hostByName(const char* aHostname, IPAddress& aResult) 19 | { 20 | struct hostent *server; // Server data structure. 21 | server = gethostbyname( aHostname ); // Convert URL to IP. 22 | if (server == NULL) 23 | { 24 | return 0; 25 | } 26 | aResult = server->h_addr_list[0]; 27 | return 1; 28 | } 29 | UnixWiFi WiFi; 30 | -------------------------------------------------------------------------------- /NTPTest/src/UnixWiFi.h: -------------------------------------------------------------------------------- 1 | /* 2 | * UnixWiFi.h 3 | * 4 | * Created on: Jul 7, 2017 5 | * Author: chris.l 6 | */ 7 | 8 | #ifndef UNIXWIFI_H_ 9 | #define UNIXWIFI_H_ 10 | 11 | #include "Types.h" 12 | #include "IPAddress.h" 13 | 14 | class UnixWiFi 15 | { 16 | public: 17 | UnixWiFi(); 18 | int hostByName(const char* aHostname, IPAddress& aResult); 19 | 20 | }; 21 | 22 | extern UnixWiFi WiFi; 23 | #endif /* UNIXWIFI_H_ */ 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AnalogClock 2.X 2 | 3 | Project to sync analog clocks to a few milliseconds using NTP for time synchronization. Details of operation can be found in this [blog post](https://blog.taboola.com/analog-clocks-ntp/) 4 | 5 | [![Build Status](https://travis-ci.org/liebman/AnalogClock.svg?branch=master)](https://travis-ci.org/liebman/AnalogClock) 6 | 7 | [I2CAnalogClock](I2CAnalogClock) contains the code for the ATTiny85 as an I2C based Analog Clock controller. I am now using [PlatformIO](https://platformio.org/) for development. 8 | 9 | [SynchroClock](SynchroClock) contains the code for the ESP8266 module. I am now using [PlatformIO](https://platformio.org/) for development. 10 | 11 | [NTPTest](NTPTest) contains a framework for testing the NTP class in an accelerated manor on linux or MacOS saving days of waiting for results. 12 | 13 | [eagle](eagle) contains the [Eagle](https://www.autodesk.com/products/eagle/overview) design files and the BOM. 14 | 15 | ## Features 16 | 17 | * Automatic daylight saving time adjustments 18 | * Clock position & configuration saved on power fail 19 | * Low power consumption: approx. 0.25ma in early testing 20 | * adjustable tick/adjust pulse width/duty cycle/delay should support most one second "tick" (non-sweep) clocks. 21 | * NTP implementation computes drift and uses that to increase accuricy between NTP updates 22 | 23 | ## Configuration 24 | 25 | When the clock is initially powered on it creates a wifi captive portal. It will show up in the list of available wifi networks as SynchroClockXXXXXXX (where the X’s are some number). Configuration mode can be forced by holding down the reset button until the LED is turns on and stays on for at least 2 seconds then releasing it. NOTE that if you conrinue holding the reset button for more than 10 seconds the LED will go back off initiating a factory reset to default settings. When you connect to this you are given a menu that lets you set many configuration options: 26 | 27 | * Wifi Network (SSID) 28 | * Wifi Network password 29 | * Clock Position - enter the current time shown on the clock as HH:MM:SS. 30 | * NTP Server to sync with 31 | * 1st time change as 6 fields (US/Pacific would be: 2 0 0 3 2 -25200 meaning the second Sunday in March at 2am we change to UTC-7 hours, and Israel: -1 0 -2 3 2 10800 meaning the Friday before the last Sunday of in March at 2am we change to UTC+3) 32 | * occurrence - 2 would be the second occurrence of the day of week specified, -1 would be the last one. 33 | * day of week - where 0 = Sunday 34 | * days offset - +|- days, used for "the Friday before the last Sunday" 35 | * month - where 1 = January 36 | * hour - where 0 = midnight 37 | * time offset - this is the offset in seconds from UTC 38 | * 2nd time change as 5 fields as described above (US/Pacific: 1 0 0 11 2 -28800 meaning the first Sunday in November at 2am we change to UTC-8 hours, and Israel: -1 0 0 10 2 7200 meaning the last Sunday of October we change to UTC-2) 39 | 40 | Advanced options: 41 | 42 | * Stay Awake - when set true the ESP8266 will not use deep sleep and will run a small web servers allowing various operations to be performed with an http interface. 43 | * Tick Pulse - this is the duration in milliseconds of the “tick”. 44 | * Tick Duty Cycle - the percentage of time that the tick is on using PWM 45 | * Adjust Start Pulse - this uis the duration in milliseconds of the initial pulse of an adjustment 46 | * Adjust Pulse - this is the duration in milliseconds of the “tick” used to advance the clock rapidly. 47 | * Adjust Duty Cycle - the percentage of time that the tick is on using PWM 48 | * Adjust Delay - this is the delay in milliseconds between “ticks” when advancing the clock rapidly. 49 | * Network Logger Host - (optional) hostname to send log lines to. 50 | * Network Logger Port - (optional) tcp port to send log lines to. 51 | * Clear NTP Persist - when set 'true' clears any saved adjustments and drift calculations. 52 | 53 | ## Schematic 54 | 55 | ![Schematic](images/SynchroClock.png) 56 | -------------------------------------------------------------------------------- /SynchroClock/.gitignore: -------------------------------------------------------------------------------- 1 | /local.ini 2 | -------------------------------------------------------------------------------- /SynchroClock/include/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for project header files. 3 | 4 | A header file is a file containing C declarations and macro definitions 5 | to be shared between several project source files. You request the use of a 6 | header file in your project source file (C, C++, etc) located in `src` folder 7 | by including it, with the C preprocessing directive `#include'. 8 | 9 | ```src/main.c 10 | 11 | #include "header.h" 12 | 13 | int main (void) 14 | { 15 | ... 16 | } 17 | ``` 18 | 19 | Including a header file produces the same results as copying the header file 20 | into each source file that needs it. Such copying would be time-consuming 21 | and error-prone. With a header file, the related declarations appear 22 | in only one place. If they need to be changed, they can be changed in one 23 | place, and programs that include the header file will automatically use the 24 | new version when next recompiled. The header file eliminates the labor of 25 | finding and changing all the copies as well as the risk that a failure to 26 | find one copy will result in inconsistencies within a program. 27 | 28 | In C, the usual convention is to give header files names that end with `.h'. 29 | It is most portable to use only letters, digits, dashes, and underscores in 30 | header file names, and at most one dot. 31 | 32 | Read more about using header files in official GCC documentation: 33 | 34 | * Include Syntax 35 | * Include Operation 36 | * Once-Only Headers 37 | * Computed Includes 38 | 39 | https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html 40 | -------------------------------------------------------------------------------- /SynchroClock/include/SynchroClock.h: -------------------------------------------------------------------------------- 1 | /* 2 | * SynchroClock.h 3 | * 4 | * Copyright 2017 Christopher B. Liebman 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * 19 | * Created on: May 26, 2017 20 | * Author: liebman 21 | */ 22 | 23 | #ifndef _SynchroClock_H_ 24 | #define _SynchroClock_H_ 25 | #include 26 | #include 27 | #include 28 | #include "ESP8266httpUpdate.h" 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include "FeedbackLED.h" 34 | #include "NTP.h" 35 | #include "Clock.h" 36 | #include "DS3231.h" 37 | #include "WireUtils.h" 38 | #include "TimeUtils.h" 39 | #include "ConfigParam.h" 40 | #include "Logger.h" 41 | #include "DLogPrintWriter.h" 42 | #include "DLogSyslogWriter.h" 43 | #include "SynchroClockPins.h" 44 | #include 45 | #include 46 | 47 | #define USE_DRIFT // apply drift 48 | #define USE_NTP_POLL_ESTIMATE // use ntp estimated drift for sleep duration calculation 49 | #define USE_STOP_THE_CLOCK // if defined then stop the clock for small negative adjustments 50 | #define STOP_THE_CLOCK_MAX 60 // maximum difference where we will use stop the clock 51 | #define STOP_THE_CLOCK_EXTRA 2 // extra seconds to leave the clock stopped 52 | 53 | #define DEFAULT_TZ_OFFSET 0 // default timzezone offset in seconds 54 | #ifndef DEFAULT_NTP_SERVER 55 | #define DEFAULT_NTP_SERVER "0.zoddotcom.pool.ntp.org" 56 | #endif 57 | #define DEFAULT_SLEEP_DURATION 28800 // default is 8hrs when we are not using the poll estimate 58 | 59 | #define CLOCK_STRETCH_LIMIT 100000 // i2c clock stretch timeout in microseconds 60 | #define MAX_SLEEP_DURATION 3600 // we do multiple sleep of this to handle bigger sleeps 61 | #define CONNECTION_TIMEOUT 30 // wifi connection timeout - we will deep sleep and try again later 62 | #define CONFIG_DELAY 1000 // how long to hold the button for config mode - light comes on after this time. 63 | #define FACTORY_RESET_DELAY 10000 // how long to hold the button for factory reset after LED is ON - 10 seconds (10,000 milliseconds) 64 | 65 | #define UPDATE_URL_FILENAME "updateurl.txt" 66 | 67 | #define offset2longDouble(x) ((long double)x / 4294967296L) 68 | 69 | // default time change definitions 70 | #if !defined(DEFAULT_TC0_OCCUR) 71 | #warning "DEFAULT_TC0_OCCUR not configured!" 72 | #define DEFAULT_TC0_OCCUR 1 73 | #endif 74 | 75 | #if !defined(DEFAULT_TC0_DOW) 76 | #warning "DEFAULT_TC0_DOW not configured!" 77 | #define DEFAULT_TC0_DOW 0 78 | #endif 79 | 80 | #if !defined(DEFAULT_TC0_DOFF) 81 | #warning "DEFAULT_TC0_DOFF not configured!" 82 | #define DEFAULT_TC0_DOFF 0 83 | #endif 84 | 85 | #if !defined(DEFAULT_TC0_MONTH) 86 | #warning "DEFAULT_TC0_MONTH not configured!" 87 | #define DEFAULT_TC0_MONTH 0 88 | #endif 89 | 90 | #if !defined(DEFAULT_TC0_HOUR) 91 | #warning "DEFAULT_TC0_HOUR not configured!" 92 | #define DEFAULT_TC0_HOUR 0 93 | #endif 94 | 95 | #if !defined(DEFAULT_TC0_OFFSET) 96 | #warning "DEFAULT_TC0_OFFSET not configured!" 97 | #define DEFAULT_TC0_OFFSET 0 98 | #endif 99 | 100 | #if !defined(DEFAULT_TC1_OCCUR) 101 | #warning "DEFAULT_TC1_OCCUR not configured!" 102 | #define DEFAULT_TC1_OCCUR 1 103 | #endif 104 | 105 | #if !defined(DEFAULT_TC1_DOW) 106 | #warning "DEFAULT_TC1_DOW not configured!" 107 | #define DEFAULT_TC1_DOW 0 108 | #endif 109 | 110 | #if !defined(DEFAULT_TC1_DOFF) 111 | #warning "DEFAULT_TC1_DOFF not configured!" 112 | #define DEFAULT_TC1_DOFF 0 113 | #endif 114 | 115 | #if !defined(DEFAULT_TC1_MONTH) 116 | #warning "DEFAULT_TC1_MONTH not configured!" 117 | #define DEFAULT_TC1_MONTH 0 118 | #endif 119 | 120 | #if !defined(DEFAULT_TC1_HOUR) 121 | #warning "DEFAULT_TC1_HOUR not configured!" 122 | #define DEFAULT_TC1_HOUR 0 123 | #endif 124 | 125 | #if !defined(DEFAULT_TC1_OFFSET) 126 | #warning "DEFAULT_TC1_OFFSET not configured!" 127 | #define DEFAULT_TC1_OFFSET 0 128 | #endif 129 | 130 | // error codes for setRTCfromNTP() 131 | #define ERROR_DNS -1 132 | #define ERROR_RTC -2 133 | #define ERROR_NTP -3 134 | 135 | #define TIME_CHANGE_COUNT 2 136 | 137 | typedef struct config 138 | { 139 | uint32_t sleep_duration; // deep sleep duration in seconds 140 | int tz_offset; // time offset in seconds from UTC 141 | uint16_t syslog_port; // port for network logging 142 | TimeChange tc[TIME_CHANGE_COUNT]; // time change description 143 | char ntp_server[64]; // host to use for ntp 144 | char syslog_host[64]; // host for network logging 145 | NTPPersist ntp_persist; // ntp persisted data 146 | } Config; 147 | 148 | typedef struct ee_config 149 | { 150 | uint32_t crc; 151 | uint8_t data[sizeof(Config)]; 152 | } EEConfig; 153 | 154 | typedef struct deep_sleep_data 155 | { 156 | uint32_t sleep_delay_left; // number seconds still to sleep 157 | NTPRunTime ntp_runtime; // NTP runtime data 158 | bool run_update; // do update if true 159 | } DeepSleepData; 160 | 161 | typedef struct rtc_deep_sleep_data 162 | { 163 | uint32_t crc; 164 | uint8_t data[sizeof(DeepSleepData)]; 165 | } RTCDeepSleepData; 166 | 167 | typedef std::shared_ptr ConfigParamPtr; 168 | 169 | boolean parseBoolean(const char* value); 170 | uint8_t parseDuty(const char* value); 171 | int getValidOffset(String name); 172 | uint16_t getValidPosition(String name); 173 | uint8_t getValidDuration(String name); 174 | uint8_t getValidDuty(String name); 175 | boolean getValidBoolean(String name); 176 | void handleOffset(); 177 | void handleAdjustment(); 178 | void handlePosition(); 179 | void handleTPDuration(); 180 | void handleTPDuty(); 181 | void handleTPCount(); 182 | void handleAPDuration(); 183 | void handleAPDuty(); 184 | void handleAPCount(); 185 | void handleAPDelay(); 186 | void handleEnable(); 187 | void handleRTC(); 188 | void handleNTP(); 189 | void handleSave(); 190 | void sleepFor(uint32_t sleep_duration); 191 | int getEdgeSyncedTime(DS3231DateTime& dt, unsigned int retries); 192 | int setRTCfromOffset(double offset_ms, bool sync); 193 | int getTime(uint32_t *result); 194 | int setRTCfromDrift(); 195 | int setRTCfromNTP(const char* server, bool sync, double* result_offset, IPAddress* result_address); 196 | int setCLKfromRTC(); 197 | void saveConfig(); 198 | boolean loadConfig(); 199 | void eraseConfig(); 200 | boolean readDeepSleepData(); 201 | boolean writeDeepSleepData(); 202 | 203 | extern unsigned int snprintf(char*, unsigned int, ...); // because esp8266 does not declare it in a header. 204 | 205 | #endif /* _SynchroClock_H_ */ 206 | -------------------------------------------------------------------------------- /SynchroClock/include/SynchroClockPins.h: -------------------------------------------------------------------------------- 1 | /* 2 | * SynchroClock.h 3 | * 4 | * Copyright 2017 Christopher B. Liebman 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * 19 | * Created on: May 26, 2017 20 | * Author: liebman 21 | */ 22 | 23 | #ifndef _SynchroClockPins_H_ 24 | #define _SynchroClockPins_H_ 25 | 26 | // pin definitions 27 | #define LED_PIN 13 // (GPIO13) LED on pin, active low 28 | #define SYNC_PIN 14 // (GPIO14) pin tied to 1hz square wave from RTC 29 | #define CONFIG_PIN 12 // (GPIO12) button tied to pin 30 | 31 | #endif /* _SynchroClockPins_H_ */ 32 | -------------------------------------------------------------------------------- /SynchroClock/include/SynchroClockVersion.h: -------------------------------------------------------------------------------- 1 | /* 2 | * SynchroClockVersion.h 3 | * 4 | * Created on: Mar 24, 2018 5 | * Author: chris.l 6 | */ 7 | 8 | #ifndef SYNCHROCLOCKVERSION_H_ 9 | #define SYNCHROCLOCKVERSION_H_ 10 | 11 | #define STRINGIZE_NX(A) #A 12 | #define STRINGIZE(A) STRINGIZE_NX(A) 13 | 14 | #ifndef VERSION_INFO 15 | #define VERSION_INFO UNKNOWN 16 | #endif 17 | 18 | const char* SYNCHRO_CLOCK_VERSION = STRINGIZE(VERSION_INFO); 19 | 20 | #endif /* SYNCHROCLOCKVERSION_H_ */ 21 | -------------------------------------------------------------------------------- /SynchroClock/lib/Clock/src/Clock.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Clock.cpp 3 | * 4 | * Copyright 2017 Christopher B. Liebman 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * 19 | * Created on: Mar 26, 2017 20 | * Author: liebman 21 | */ 22 | 23 | #include "Clock.h" 24 | #include "WireUtils.h" 25 | 26 | static PROGMEM const char TAG[] = "Clock"; 27 | 28 | Clock::Clock(int _pin) 29 | { 30 | pin = _pin; 31 | } 32 | 33 | int Clock::begin() 34 | { 35 | if (isClockPresent()) 36 | { 37 | return 0; 38 | } 39 | return -1; 40 | } 41 | 42 | int Clock::begin(unsigned int retries) 43 | { 44 | while(retries-- > 0) 45 | { 46 | if (begin() == 0) 47 | { 48 | return 0; 49 | } 50 | 51 | dlog.warning(FPSTR(TAG), F("::begin: failed detect clock, %d retries left"), retries); 52 | WireUtils.clearBus(); 53 | } 54 | return -1; 55 | } 56 | 57 | // 58 | // access to analog clock controller via i2c 59 | // 60 | 61 | bool Clock::isClockPresent() 62 | { 63 | uint8_t id; 64 | if (read(CMD_ID, &id)) 65 | { 66 | return false; 67 | } 68 | return (id == CLOCK_ID_VALUE); 69 | } 70 | 71 | int Clock::readAdjustment(uint16_t *value) 72 | { 73 | return read(CMD_ADJUSTMENT, value); 74 | } 75 | 76 | int Clock::writeAdjustment(uint16_t value) 77 | { 78 | if (value >= CLOCK_MAX) 79 | { 80 | dlog.error(FPSTR(TAG), F("::isClockPresent: invalid value: %u"), value); 81 | return -1; 82 | } 83 | return write(CMD_ADJUSTMENT, value); 84 | } 85 | 86 | int Clock::readPosition(uint16_t *value) 87 | { 88 | int err = read(CMD_POSITION, value); 89 | if (err) 90 | { 91 | return err; 92 | } 93 | if (*value >= CLOCK_MAX) 94 | { 95 | dlog.error(FPSTR(TAG), F("::readPosition: INVALID VALUE RETURNED: %u"), *value); 96 | return -1; 97 | } 98 | return 0; 99 | } 100 | 101 | int Clock::readPosition(uint16_t *value, unsigned int retries) 102 | { 103 | while(retries-- > 0) 104 | { 105 | if (readPosition(value) == 0) 106 | { 107 | return 0; 108 | } 109 | 110 | dlog.warning(FPSTR(TAG), F("::readPosition: failed, %d retries left"), retries); 111 | WireUtils.clearBus(); 112 | } 113 | return -1; 114 | } 115 | 116 | int Clock::writePosition(uint16_t value) 117 | { 118 | return write(CMD_POSITION, value); 119 | } 120 | 121 | int Clock::readTPDuration(uint8_t *value) 122 | { 123 | return read(CMD_TP_DURATION, value); 124 | } 125 | 126 | int Clock::writeTPDuration(uint8_t value) 127 | { 128 | return write(CMD_TP_DURATION, value); 129 | } 130 | 131 | int Clock::readTPDuty(uint8_t *value) 132 | { 133 | return read(CMD_TP_DUTY, value); 134 | } 135 | 136 | int Clock::writeTPDuty(uint8_t value) 137 | { 138 | return write(CMD_TP_DUTY, value); 139 | } 140 | 141 | int Clock::readAPDuration(uint8_t* value) 142 | { 143 | return read(CMD_AP_DURATION, value); 144 | } 145 | 146 | int Clock::writeAPDuration(uint8_t value) 147 | { 148 | return write(CMD_AP_DURATION, value); 149 | } 150 | 151 | int Clock::readAPDuty(uint8_t *value) 152 | { 153 | return read(CMD_AP_DUTY, value); 154 | } 155 | 156 | int Clock::writeAPDuty(uint8_t value) 157 | { 158 | return write(CMD_AP_DUTY, value); 159 | } 160 | 161 | int Clock::readAPDelay(uint8_t* value) 162 | { 163 | return read(CMD_AP_DELAY, value); 164 | } 165 | 166 | int Clock::writeAPDelay(uint8_t value) 167 | { 168 | return write(CMD_AP_DELAY, value); 169 | } 170 | 171 | int Clock::readAPStartDuration(uint8_t* value) 172 | { 173 | return read(CMD_AP_START, value); 174 | } 175 | 176 | int Clock::writeAPStartDuration(uint8_t value) 177 | { 178 | return write(CMD_AP_START, value); 179 | } 180 | 181 | int Clock::readPWMTop(uint8_t* value) 182 | { 183 | return read(CMD_PWMTOP, value); 184 | } 185 | 186 | int Clock::writePWMTop(uint8_t value) 187 | { 188 | return write(CMD_PWMTOP, value); 189 | } 190 | 191 | int Clock::readStatus(uint8_t* value) 192 | { 193 | return read(CMD_STATUS, value); 194 | } 195 | 196 | int Clock::readStatus(uint8_t* value, unsigned int retries) 197 | { 198 | while(retries-- > 0) 199 | { 200 | if (readStatus(value) == 0) 201 | { 202 | return 0; 203 | } 204 | 205 | dlog.warning(FPSTR(TAG), F("::readStatus: failed, %d retries left"), retries); 206 | WireUtils.clearBus(); 207 | } 208 | return -1; 209 | } 210 | 211 | int Clock::factoryReset() 212 | { 213 | return write(CMD_RESET, (uint8_t)0); 214 | } 215 | 216 | int Clock::readResetReason(uint8_t* value) 217 | { 218 | return read(CMD_RST_REASON, value); 219 | } 220 | 221 | int Clock::readResetReason(uint8_t* value, unsigned int retries) 222 | { 223 | while(retries-- > 0) 224 | { 225 | if (readResetReason(value) == 0) 226 | { 227 | return 0; 228 | } 229 | 230 | dlog.warning(FPSTR(TAG), F("::readResetReason: failed, %d retries left"), retries); 231 | WireUtils.clearBus(); 232 | } 233 | return -1; 234 | } 235 | 236 | int Clock::readVersion(uint8_t* value) 237 | { 238 | return read(CMD_VERSION, value); 239 | } 240 | 241 | int Clock::readVersion(uint8_t* value, unsigned int retries) 242 | { 243 | while(retries-- > 0) 244 | { 245 | if (readVersion(value) == 0) 246 | { 247 | return 0; 248 | } 249 | 250 | dlog.warning(FPSTR(TAG), F("::readVersion: failed, %d retries left"), retries); 251 | WireUtils.clearBus(); 252 | } 253 | return -1; 254 | } 255 | 256 | bool Clock::getEnable() 257 | { 258 | return getCommandBit(BIT_ENABLE); 259 | } 260 | 261 | void Clock::setEnable(bool enable) 262 | { 263 | setCommandBit(enable, BIT_ENABLE); 264 | } 265 | 266 | int Clock::saveConfig() 267 | { 268 | return write(CMD_SAVE_CONFIG, (uint8_t)0); 269 | } 270 | 271 | bool Clock::getCommandBit(uint8_t bit) 272 | { 273 | Wire.beginTransmission((uint8_t) I2C_ADDRESS); 274 | Wire.write(CMD_CONTROL); 275 | int err = Wire.endTransmission(true); 276 | if (err != 0) 277 | { 278 | dlog.error(FPSTR(TAG), F("::getCommandBit endTransmission returned: %d"), err); 279 | } 280 | 281 | int size = Wire.requestFrom((uint8_t) I2C_ADDRESS, (uint8_t) 1); 282 | if (size != 1) 283 | { 284 | dlog.error(FPSTR(TAG), F("::getCommandBit requestFrom did not return 1, size:%u"), size); 285 | } 286 | uint8_t value = Wire.read(); 287 | return ((value & bit) == bit); 288 | } 289 | 290 | int Clock::setCommandBit(bool onoff, uint8_t bit) 291 | { 292 | Wire.beginTransmission(I2C_ADDRESS); 293 | Wire.write(CMD_CONTROL); 294 | int err = Wire.endTransmission(); 295 | if (err != 0) 296 | { 297 | dlog.error(FPSTR(TAG), F("::setCommandBit endTransmission returned: %d"), err); 298 | return -1; 299 | } 300 | 301 | size_t count = Wire.requestFrom((uint8_t) I2C_ADDRESS, (uint8_t) 1); 302 | uint8_t value = Wire.read(); 303 | 304 | if (count != 1) 305 | { 306 | dlog.error(FPSTR(TAG), F("::setCommandBit: Wire.requestFrom failed!")); 307 | return -1; 308 | } 309 | 310 | if (onoff) 311 | { 312 | value |= bit; 313 | } 314 | else 315 | { 316 | value &= ~bit; 317 | } 318 | 319 | 320 | Wire.beginTransmission(I2C_ADDRESS); 321 | count = 0; 322 | count += Wire.write(CMD_CONTROL); 323 | count += Wire.write(value); 324 | if (count != 2) 325 | { 326 | Wire.endTransmission(); 327 | dlog.error(FPSTR(TAG), F("::setCommandBit: Wire.write command & value failed!")); 328 | return -1; 329 | } 330 | err = Wire.endTransmission(); 331 | if (err) 332 | { 333 | dlog.error(FPSTR(TAG), F("::setCommandBit: Wire.endTransmission() returned: %d"), err); 334 | return -1; 335 | } 336 | 337 | return 0; 338 | } 339 | 340 | int Clock::read(uint8_t command, uint8_t *value) 341 | { 342 | Wire.beginTransmission(I2C_ADDRESS); 343 | if (Wire.write(command) != 1) 344 | { 345 | Wire.endTransmission(); 346 | dlog.error(FPSTR(TAG), F("::read: Wire.write(command) failed!")); 347 | return -1; 348 | } 349 | int err = Wire.endTransmission(); 350 | if (err) 351 | { 352 | dlog.error(FPSTR(TAG), F("::read: Wire.endTransmission() returned: %d"), err); 353 | return -1; 354 | } 355 | size_t count; 356 | count = Wire.requestFrom(I2C_ADDRESS, 1); 357 | *value = Wire.read(); 358 | if (count != 1) 359 | { 360 | dlog.error(FPSTR(TAG), F("::read: Wire.requestFrom() returns %u, expected 1"), count); 361 | return -1; 362 | } 363 | return 0; 364 | } 365 | 366 | int Clock::write(uint8_t command, uint8_t value) 367 | { 368 | Wire.beginTransmission(I2C_ADDRESS); 369 | if (Wire.write(command) != 1) 370 | { 371 | Wire.endTransmission(); 372 | dlog.error(FPSTR(TAG), F("::write: Wire.write(command=%d) failed!"), command); 373 | return -1; 374 | } 375 | size_t count; 376 | count = Wire.write(value); 377 | if (count != 1) 378 | { 379 | dlog.error(FPSTR(TAG), F("::write: Wire.write() returns %u, expected 1"), count); 380 | return -1; 381 | } 382 | int err = Wire.endTransmission(); 383 | if (err) 384 | { 385 | dlog.error(FPSTR(TAG), F("::read: Wire.endTransmission() returned: %d"), err); 386 | return -1; 387 | } 388 | return 0; 389 | } 390 | 391 | int Clock::read(uint8_t command, uint16_t *value) 392 | { 393 | Wire.beginTransmission(I2C_ADDRESS); 394 | if (Wire.write(command) != 1) 395 | { 396 | Wire.endTransmission(); 397 | dlog.error(FPSTR(TAG), F("::read: Wire.write(I2C_ADDRESS) failed!")); 398 | return -1; 399 | } 400 | int err = Wire.endTransmission(); 401 | if (err) 402 | { 403 | dlog.error(FPSTR(TAG), F("::read: Wire.endTransmission() returned: %d"), err); 404 | return -1; 405 | } 406 | size_t count; 407 | count = Wire.requestFrom(I2C_ADDRESS, 2); 408 | *value = (Wire.read() & 0xff) | (Wire.read() & 0xff) << 8; 409 | if (count != 2) 410 | { 411 | dlog.error(FPSTR(TAG), F("::read: Wire.requestFrom() returns %u, expected 2"), count); 412 | return -1; 413 | } 414 | return 0; 415 | } 416 | 417 | int Clock::write(uint8_t command, uint16_t value) 418 | { 419 | Wire.beginTransmission(I2C_ADDRESS); 420 | if (Wire.write(command) != 1) 421 | { 422 | Wire.endTransmission(); 423 | dlog.error(FPSTR(TAG), F("::write: Wire.write(command=%d) failed!"), command); 424 | return -1; 425 | } 426 | size_t count = 0; 427 | count += Wire.write(value & 0xff); 428 | count += Wire.write(value >> 8); 429 | if (count != 2) 430 | { 431 | dlog.error(FPSTR(TAG), F("::write: Wire.write() returns %u, expected 2"), count); 432 | return -1; 433 | } 434 | int err = Wire.endTransmission(); 435 | if (err) 436 | { 437 | dlog.error(FPSTR(TAG), F("::read: Wire.endTransmission() returned: %d"), err); 438 | return -1; 439 | } 440 | return 0; 441 | } 442 | 443 | void Clock::waitForEdge(int edge) 444 | { 445 | while (digitalRead(pin) == edge) 446 | { 447 | delay(1); 448 | } 449 | while (digitalRead(pin) != edge) 450 | { 451 | delay(1); 452 | } 453 | } 454 | -------------------------------------------------------------------------------- /SynchroClock/lib/Clock/src/Clock.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Clock.h 3 | * 4 | * Copyright 2017 Christopher B. Liebman 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * 19 | * Created on: Mar 26, 2017 20 | * Author: liebman 21 | */ 22 | 23 | #ifndef CLOCK_H_ 24 | #define CLOCK_H_ 25 | 26 | #include 27 | #include 28 | #include "WireUtils.h" 29 | #include "Logger.h" 30 | 31 | #define I2C_ADDRESS 0x09 32 | 33 | #define CMD_ID 0x00 34 | #define CMD_POSITION 0x01 35 | #define CMD_ADJUSTMENT 0x02 36 | #define CMD_CONTROL 0x03 37 | #define CMD_STATUS 0x04 38 | #define CMD_TP_DURATION 0x05 39 | #define CMD_SAVE_CONFIG 0x06 40 | #define CMD_AP_DURATION 0x07 41 | #define CMD_AP_START 0x08 42 | #define CMD_AP_DELAY 0x09 43 | #define CMD_PWMTOP 0x0a 44 | #define CMD_TP_DUTY 0x0b 45 | #define CMD_AP_DUTY 0x0c 46 | #define CMD_RESET 0x0d // factory reset 47 | #define CMD_RST_REASON 0x0e // last reset reason 48 | #define CMD_VERSION 0x0f // Clock firmware version 49 | 50 | // control register bits 51 | #define BIT_ENABLE 0x80 52 | 53 | // status register bits 54 | #define STATUS_BIT_TICK 0x01 55 | #define STATUS_BIT_PWFBAD 0x80 56 | 57 | 58 | #define CLOCK_ID_VALUE 0x42 59 | 60 | #define CLOCK_EDGE_RISING 1 61 | #define CLOCK_EDGE_FALLING 0 62 | 63 | #define CLOCK_ERROR 0xffff 64 | #define CLOCK_MAX 43200 65 | 66 | class Clock 67 | { 68 | public: 69 | Clock(int _pin); 70 | int begin(); 71 | int begin(unsigned int retries); 72 | bool isClockPresent(); 73 | int readAdjustment(uint16_t *value); 74 | int writeAdjustment(uint16_t value); 75 | int readPosition(uint16_t* value); 76 | int readPosition(uint16_t* value, unsigned int retries); 77 | int writePosition(uint16_t value); 78 | int readTPDuration(uint8_t* value); 79 | int writeTPDuration(uint8_t value); 80 | int readTPDuty(uint8_t* value); 81 | int writeTPDuty(uint8_t value); 82 | int readAPDuration(uint8_t* value); 83 | int writeAPDuration(uint8_t value); 84 | int readAPDuty(uint8_t* value); 85 | int writeAPDuty(uint8_t value); 86 | int readAPDelay(uint8_t* value); 87 | int writeAPDelay(uint8_t value); 88 | int readAPStartDuration(uint8_t* value); 89 | int writeAPStartDuration(uint8_t value); 90 | int readPWMTop(uint8_t* value); 91 | int writePWMTop(uint8_t value); 92 | int readStatus(uint8_t* value); 93 | int readStatus(uint8_t* value, unsigned int retries); 94 | int factoryReset(); 95 | int readResetReason(uint8_t* value); 96 | int readResetReason(uint8_t* value, unsigned int retries); 97 | int readVersion(uint8_t* value); 98 | int readVersion(uint8_t* value, unsigned int retries); 99 | 100 | bool getEnable(); 101 | void setEnable(bool enable); 102 | int saveConfig(); 103 | bool getCommandBit(uint8_t); 104 | int setCommandBit(bool value, uint8_t bit); 105 | void waitForEdge(int edge); 106 | private: 107 | int pin; 108 | int read(uint8_t command, uint16_t *value); 109 | int write(uint8_t command, uint16_t value); 110 | int read(uint8_t command, uint8_t *value); 111 | int write(uint8_t command, uint8_t value); 112 | }; 113 | 114 | #endif /* CLOCK_H_ */ 115 | -------------------------------------------------------------------------------- /SynchroClock/lib/ConfigParam/src/ConfigParam.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * ConfigParam.cpp 3 | * 4 | * Copyright 2017 Christopher B. Liebman 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * 19 | * Created on: Jun 6, 2017 20 | * Author: chris.l 21 | */ 22 | 23 | #include "ConfigParam.h" 24 | 25 | static PROGMEM const char TAG[] = "ConfigParam"; 26 | 27 | ConfigParam::ConfigParam(WiFiManager &wifi, const char *label) 28 | { 29 | _apply = nullptr; 30 | _wmp = new WiFiManagerParameter(label); 31 | dlog.trace(FPSTR(TAG), F("ConfigParam: create label='%s'"), label); 32 | _value[0] = '\0'; 33 | wifi.addParameter(_wmp); 34 | } 35 | 36 | ConfigParam::ConfigParam(WiFiManager &wifi, const char *id, const char *placeholder, const char* value, int length, void (*applyCB)(const char*)) 37 | { 38 | _apply = applyCB; 39 | strncpy(_value, value, MAX_VALUE_LENGTH); 40 | _value[MAX_VALUE_LENGTH] = 0; 41 | init(wifi, id, placeholder, length); 42 | } 43 | 44 | ConfigParam::ConfigParam(WiFiManager &wifi, const char *id, const char *placeholder, int value, int length, void (*applyCB)(const char*)) 45 | { 46 | _apply = applyCB; 47 | snprintf(_value, MAX_VALUE_LENGTH, "%d", value); 48 | init(wifi, id, placeholder, length); 49 | } 50 | 51 | ConfigParam::ConfigParam(WiFiManager &wifi, const char *id, const char *placeholder, uint32_t value, int length, void (*applyCB)(const char*)) 52 | { 53 | _apply = applyCB; 54 | snprintf(_value, MAX_VALUE_LENGTH, "%u", value); 55 | init(wifi, id, placeholder, length); 56 | } 57 | 58 | ConfigParam::ConfigParam(WiFiManager &wifi, const char *id, const char *placeholder, uint16_t value, int length, void (*applyCB)(const char*)) 59 | { 60 | _apply = applyCB; 61 | snprintf(_value, MAX_VALUE_LENGTH, "%u", value); 62 | init(wifi, id, placeholder, length); 63 | } 64 | 65 | ConfigParam::ConfigParam(WiFiManager &wifi, const char *id, const char *placeholder, uint8_t value, int length, void (*applyCB)(const char*)) 66 | { 67 | _apply = applyCB; 68 | snprintf(_value, MAX_VALUE_LENGTH, "%u", value); 69 | init(wifi, id, placeholder, length); 70 | } 71 | 72 | ConfigParam::ConfigParam(WiFiManager &wifi, const char *id, const char *placeholder, double value, int length, void (*applyCB)(const char*)) 73 | { 74 | _apply = applyCB; 75 | snprintf(_value, MAX_VALUE_LENGTH, "%lf", value); 76 | init(wifi, id, placeholder, length); 77 | } 78 | 79 | ConfigParam::~ConfigParam() 80 | { 81 | if (_wmp != NULL) 82 | { 83 | const char* id = ""; 84 | if (_wmp->getID() != nullptr) 85 | { 86 | id = _wmp->getID(); 87 | } 88 | else if (_wmp->getCustomHTML() != nullptr) 89 | { 90 | id = _wmp->getCustomHTML(); 91 | } 92 | dlog.trace(FPSTR(TAG), F("~ConfigParam: destroying '%s'"), id); 93 | delete _wmp; 94 | _wmp = nullptr; 95 | } 96 | } 97 | 98 | void ConfigParam::init(WiFiManager &wifi, const char *id, const char *placeholder, int length) 99 | { 100 | dlog.debug(FPSTR(TAG), F("::init: id:%s value:'%s'"), id, _value); 101 | _wmp = new WiFiManagerParameter(id, placeholder, _value, length); 102 | wifi.addParameter(_wmp); 103 | } 104 | 105 | boolean ConfigParam::isChanged() 106 | { 107 | dlog.trace(FPSTR(TAG), F("::isChanged: _wmp: 0x%08x"), (uint32_t)_wmp); 108 | 109 | if (_wmp != nullptr && _wmp->getValue() != nullptr) 110 | { 111 | dlog.trace(FPSTR(TAG), F("::isChanged: id:%s old:'%s', new:'%s'"), _wmp->getID(), _value, _wmp->getValue()); 112 | if (strcmp(_wmp->getValue(), _value)) 113 | { 114 | dlog.debug(FPSTR(TAG), F("::isChanged: true!")); 115 | return true; 116 | } 117 | } 118 | return false; 119 | } 120 | 121 | void ConfigParam::apply() 122 | { 123 | if (_apply != nullptr) 124 | { 125 | dlog.debug(FPSTR(TAG), F("::apply: id:%s value:'%s'"), _wmp->getID(), _wmp->getValue()); 126 | _apply(_wmp->getValue()); 127 | } 128 | } 129 | 130 | void ConfigParam::applyIfChanged() 131 | { 132 | if (isChanged()) 133 | { 134 | apply(); 135 | } 136 | } 137 | 138 | const char* ConfigParam::getId() 139 | { 140 | return _wmp->getID(); 141 | } 142 | 143 | const char* ConfigParam::getValue() 144 | { 145 | return _wmp->getValue(); 146 | } 147 | -------------------------------------------------------------------------------- /SynchroClock/lib/ConfigParam/src/ConfigParam.h: -------------------------------------------------------------------------------- 1 | /* 2 | * ConfigParam.h 3 | * 4 | * Copyright 2017 Christopher B. Liebman 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * 19 | * Created on: Jun 6, 2017 20 | * Author: chris.l 21 | */ 22 | 23 | #ifndef CONFIGPARAM_H_ 24 | #define CONFIGPARAM_H_ 25 | #include 26 | #include "Logger.h" 27 | 28 | #include 29 | typedef std::function ConfigParamApply; 30 | 31 | #define MAX_VALUE_LENGTH 63 32 | 33 | class ConfigParam 34 | { 35 | public: 36 | ConfigParam(WiFiManager &wifi, const char *label); 37 | ConfigParam(WiFiManager &wifi, const char *id, const char *placeholder, int value, int length, void (*applyCB)(const char* result)); 38 | ConfigParam(WiFiManager &wifi, const char *id, const char *placeholder, uint32_t value, int length, void (*applyCB)(const char* result)); 39 | ConfigParam(WiFiManager &wifi, const char *id, const char *placeholder, uint16_t value, int length, void (*applyCB)(const char* result)); 40 | ConfigParam(WiFiManager &wifi, const char *id, const char *placeholder, uint8_t value, int length, void (*applyCB)(const char* result)); 41 | ConfigParam(WiFiManager &wifi, const char *id, const char *placeholder, double value, int length, void (*applyCB)(const char* result)); 42 | ConfigParam(WiFiManager &wifi, const char *id, const char *placeholder, const char* value, int length, void (*applyCB)(const char* result)); 43 | ~ConfigParam(); 44 | void init(WiFiManager &wifi, const char *id, const char *placeholder, int length); 45 | boolean isChanged(); 46 | const char* getValue(); 47 | const char* getId(); 48 | void apply(); 49 | void applyIfChanged(); 50 | 51 | private: 52 | WiFiManagerParameter* _wmp; 53 | ConfigParamApply _apply; 54 | char _value[MAX_VALUE_LENGTH+1]; 55 | }; 56 | 57 | #endif /* CONFIGPARAM_H_ */ 58 | -------------------------------------------------------------------------------- /SynchroClock/lib/DS3231/src/DS3231.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * DS3231.cpp 3 | * 4 | * Copyright 2017 Christopher B. Liebman 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * 19 | * Created on: May 21, 2017 20 | * Author: chris.l 21 | */ 22 | 23 | #include "DS3231.h" 24 | 25 | static PROGMEM const char TAG[] = "DS3231"; 26 | 27 | DS3231::DS3231() 28 | { 29 | } 30 | 31 | int DS3231::begin() 32 | { 33 | dlog.info(FPSTR(TAG),F("::begin: enable OSC/no BBS/no CONV/1hz SQWV on/no ALRM")); 34 | 35 | uint8_t ctrl = 0b00000000; // enable osc/no BBS/no CONV/1hz/SQWV on/no ALRM 36 | int err = write(DS3231_CONTROL_REG, ctrl); //CONTROL Register Address 37 | if (err) 38 | { 39 | dlog.error(FPSTR(TAG), F("::begin: write(DS3231_CONTROL_REG) failed: %d"), err); 40 | return -1; 41 | } 42 | 43 | dlog.info(FPSTR(TAG),F("::begin: small delay")); 44 | delay(100); 45 | 46 | dlog.info(FPSTR(TAG),F("::begin: reading HOUR register to insure 24hr format")); 47 | // set the clock to 24hr format 48 | uint8_t hr; 49 | if (read(DS3231_HOUR_REG, &hr)) 50 | { 51 | dlog.error(FPSTR(TAG), F("::begin(): read(DS3231_HOUR_REG) failed!")); 52 | return -1; 53 | } 54 | 55 | // switch to 24hr mode if its not already 56 | if (hr & _BV(DS3231_AMPM)) 57 | { 58 | dlog.info(FPSTR(TAG),F("::begin: writing HOUR register to set 24hr format")); 59 | 60 | hr &= ~_BV(DS3231_AMPM); 61 | 62 | if(write(DS3231_HOUR_REG, hr)) 63 | { 64 | dlog.error(FPSTR(TAG), F("::begin(): write(DS3231_HOUR_REG, 0x%02x) failed!"), hr); 65 | return -1; 66 | } 67 | } 68 | 69 | dlog.info(FPSTR(TAG), F("::begin: done")); 70 | return 0; 71 | } 72 | 73 | uint8_t DS3231::fromBCD(uint8_t val) 74 | { 75 | return val - 6 * (val >> 4); 76 | } 77 | 78 | uint8_t DS3231::toBCD(uint8_t val) 79 | { 80 | return val + 6 * (val / 10); 81 | } 82 | 83 | int DS3231::readTime(DS3231DateTime& dt) 84 | { 85 | 86 | uint8_t count = setupRead(DS3231_SEC_REG, 7); 87 | if (count != 7) 88 | { 89 | dlog.error(FPSTR(TAG), F("::readTime: setupRead failed! count:%u != 7"), count); 90 | Wire.clearWriteError(); 91 | Wire.flush(); 92 | return -1; 93 | } 94 | 95 | uint8_t seconds = Wire.read(); 96 | uint8_t minutes = Wire.read(); 97 | uint8_t hours = Wire.read(); 98 | uint8_t day = Wire.read(); 99 | uint8_t date = Wire.read(); 100 | uint8_t month = Wire.read(); 101 | uint8_t year = Wire.read(); 102 | 103 | dlog.trace(FPSTR(TAG), F("::readTime raw month: 0x%02x"), month); 104 | 105 | dt.seconds = fromBCD(seconds); 106 | dt.minutes = fromBCD(minutes); 107 | dt.hours = fromBCD(hours & DS3231_24HR_MASK); 108 | dt.day = fromBCD(day); 109 | dt.date = fromBCD(date); 110 | dt.month = fromBCD(month & DS3231_MONTH_MASK); 111 | dt.year = fromBCD(year); 112 | dt.century = month&DS3231_CENTURY? 1 : 0; 113 | 114 | if (!dt.isValid()) 115 | { 116 | dlog.error(FPSTR(TAG), F("::readTime: result not valid!!!")); 117 | Wire.clearWriteError(); 118 | Wire.flush(); 119 | return -1; 120 | } 121 | 122 | dlog.debug(FPSTR(TAG), F("::readTime %04u-%02u-%02u %02u:%02u:%02u day:%u century:%u"), 123 | dt.year, 124 | dt.month, 125 | dt.date, 126 | dt.hours, 127 | dt.minutes, 128 | dt.seconds, 129 | dt.day, 130 | dt.century); 131 | return 0; 132 | } 133 | 134 | int DS3231::writeTime(DS3231DateTime &dt) 135 | { 136 | dlog.debug(FPSTR(TAG), F("::writeTime %04u-%02u-%02u %02u:%02u:%02u day:%u century:%u"), 137 | dt.year, 138 | dt.month, 139 | dt.date, 140 | dt.hours, 141 | dt.minutes, 142 | dt.seconds, 143 | dt.day, 144 | dt.century); 145 | 146 | Wire.beginTransmission(DS3231_ADDRESS); 147 | if (Wire.write(DS3231_SEC_REG) != 1) 148 | { 149 | Wire.endTransmission(); 150 | dlog.error(FPSTR(TAG), F("::writeTime: Wire.write(reg=DS3231_SEC_REG) failed!")); 151 | return -1; 152 | } 153 | 154 | int count; 155 | count = Wire.write(toBCD(dt.seconds)); 156 | count += Wire.write(toBCD(dt.minutes)); 157 | count += Wire.write(toBCD(dt.hours)); 158 | count += Wire.write(toBCD(dt.day)); 159 | count += Wire.write(toBCD(dt.date)); 160 | count += Wire.write(toBCD(dt.month) | (dt.century ? _BV(DS3231_CENTURY) : 0)); 161 | count += Wire.write(toBCD(dt.year)); 162 | if (count != 7) 163 | { 164 | dlog.error(FPSTR(TAG), F("::writeTime: Wire.write() all fields, expected 7 got %u"), count); 165 | return -1; 166 | } 167 | int err = Wire.endTransmission(); 168 | if (err) 169 | { 170 | dlog.error(FPSTR(TAG), F("::writeTime: Wire.endTransmission() returned: %d"), err); 171 | return -1; 172 | } 173 | return(0); 174 | } 175 | 176 | int DS3231::setupRead(uint8_t reg, uint8_t size) 177 | { 178 | Wire.beginTransmission(DS3231_ADDRESS); 179 | if (Wire.write(reg) != 1) 180 | { 181 | Wire.endTransmission(); 182 | dlog.error(FPSTR(TAG), F("::setupRead: Wire.write(DS3231_CONTROL_REG) failed!")); 183 | return -1; 184 | } 185 | int err = Wire.endTransmission(); 186 | 187 | if (err) 188 | { 189 | dlog.error(FPSTR(TAG), F("::setupRead: Wire.endTransmission() returned: %d"), err); 190 | return -1; 191 | } 192 | 193 | return Wire.requestFrom(DS3231_ADDRESS, (size_t)size); 194 | } 195 | 196 | int DS3231::read(uint8_t reg, uint8_t *value) 197 | { 198 | size_t count = setupRead(reg, 1); 199 | count = Wire.requestFrom(DS3231_ADDRESS, (size_t)1); 200 | *value = Wire.read(); 201 | if (count != 1) 202 | { 203 | dlog.error(FPSTR(TAG), F("::read: Wire.requestFrom() returns %u, expected 1"), count); 204 | return -1; 205 | } 206 | return 0; 207 | } 208 | 209 | 210 | int DS3231::write(uint8_t reg, uint8_t value) 211 | { 212 | Wire.beginTransmission(DS3231_ADDRESS); 213 | if (Wire.write(reg) != 1) 214 | { 215 | Wire.endTransmission(); 216 | dlog.error(FPSTR(TAG), F("::write: Wire.write(reg=%d, value=%d) failed!"), reg, value); 217 | return -1; 218 | } 219 | size_t count; 220 | count = Wire.write(value); 221 | if (count != 1) 222 | { 223 | dlog.error(FPSTR(TAG), F("::write: Wire.write(value=%u) returns %u"), value, count); 224 | return -1; 225 | } 226 | int err = Wire.endTransmission(); 227 | if (err) 228 | { 229 | dlog.error(FPSTR(TAG), F("::write: Wire.endTransmission() returned: %d"), err); 230 | return -1; 231 | } 232 | return(0); 233 | } 234 | -------------------------------------------------------------------------------- /SynchroClock/lib/DS3231/src/DS3231.h: -------------------------------------------------------------------------------- 1 | /* 2 | * DS3231.h 3 | * 4 | * Copyright 2017 Christopher B. Liebman 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * 19 | * Created on: May 21, 2017 20 | * Author: chris.l 21 | */ 22 | 23 | #ifndef DS3231_H_ 24 | #define DS3231_H_ 25 | #include 26 | #include 27 | #include "WireUtils.h" 28 | #include "DS3231DateTime.h" 29 | #include "Logger.h" 30 | 31 | // I2C address 32 | const uint8_t DS3231_ADDRESS = 0x68; 33 | 34 | // Registers 35 | const uint8_t DS3231_SEC_REG = 0x00; 36 | const uint8_t DS3231_MIN_REG = 0x01; 37 | const uint8_t DS3231_HOUR_REG = 0x02; 38 | const uint8_t DS3231_WDAY_REG = 0x03; 39 | const uint8_t DS3231_MDAY_REG = 0x04; 40 | const uint8_t DS3231_MONTH_REG = 0x05; 41 | const uint8_t DS3231_YEAR_REG = 0x06; 42 | 43 | const uint8_t DS3231_A1S_REG = 0x07; 44 | const uint8_t DS3231_A1M_REG = 0x08; 45 | const uint8_t DS3231_A1H_REG = 0x09; 46 | const uint8_t DS3231_A1W_REG = 0x0A; 47 | 48 | const uint8_t DS3231_AL2M_REG = 0x0B; 49 | const uint8_t DS3231_AL2H_REG = 0x0C; 50 | const uint8_t DS3231_AL2W_REG = 0x0D; 51 | 52 | const uint8_t DS3231_CONTROL_REG = 0x0E; 53 | const uint8_t DS3231_STATUS_REG = 0x0F; 54 | const uint8_t DS3231_AGING_REG = 0x0F; 55 | const uint8_t DS3231_TEMP_UP_REG = 0x11; 56 | const uint8_t DS3231_TEMP_LOW_REG = 0x12; 57 | 58 | // DS3231 Control Register Bits 59 | const uint8_t DS3231_CTL_A1IE = 0; 60 | const uint8_t DS3231_CTL_A2IE = 1; 61 | const uint8_t DS3231_CTL_INTCN = 2; 62 | const uint8_t DS3231_CTL_RS1 = 3; 63 | const uint8_t DS3231_CTL_RS2 = 4; 64 | const uint8_t DS3231_CTL_CONV = 5; 65 | const uint8_t DS3231_CTL_BBSQW = 6; 66 | const uint8_t DS3231_CTL_EOSC = 7; 67 | 68 | // DS3231 Status Register Bits 69 | const uint8_t DS3231_STS_A1F = 0; 70 | const uint8_t DS3231_STS_A2F = 1; 71 | const uint8_t DS3231_STS_BSY = 2; 72 | const uint8_t DS3231_STS_EN32KHZ = 3; 73 | const uint8_t DS3231_STS_OSF = 7; 74 | 75 | // DS3231 Hour register 76 | const uint8_t DS3231_AMPM = 6; // AMPM bit 77 | const uint8_t DS3231_24HR_MASK = 0x3f; // mask for 24 hour value 78 | 79 | // DS3231 Month register 80 | const uint8_t DS3231_CENTURY = 7; 81 | const uint8_t DS3231_MONTH_MASK = 0x1f; 82 | 83 | #define RTC_POSITION_ERROR 0xffff 84 | 85 | 86 | class DS3231 87 | { 88 | public: 89 | DS3231(); 90 | int begin(); 91 | int readTime(DS3231DateTime& dt); // return 0 if ok 92 | int writeTime(DS3231DateTime& dt); // return 0 if ok 93 | 94 | private: 95 | uint8_t fromBCD(uint8_t val); 96 | uint8_t toBCD(uint8_t val); 97 | int setupRead(uint8_t reg, uint8_t size); 98 | int write(uint8_t reg, uint8_t value); 99 | int read(uint8_t reg, uint8_t* value); 100 | }; 101 | 102 | #endif /* DS3231_H_ */ 103 | -------------------------------------------------------------------------------- /SynchroClock/lib/DS3231/src/DS3231DateTime.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * DS3231DateTime.cpp 3 | * 4 | * Copyright 2017 Christopher B. Liebman 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * 19 | * Created on: May 22, 2017 20 | * Author: chris.l 21 | */ 22 | 23 | #include "DS3231DateTime.h" 24 | 25 | static PROGMEM const char TAG[] = "DS3231DateTime"; 26 | 27 | #define dbvalue(prefix) { \ 28 | dlog.debug(FPSTR(TAG), F("%s position:%u (%04u-%02u-%02u %02u:%02u:%02u) weekday:%u century:%d unix:%lu"), \ 29 | prefix, \ 30 | getPosition(), \ 31 | year+1900+100, \ 32 | month, \ 33 | date, \ 34 | hours, \ 35 | minutes, \ 36 | seconds, \ 37 | day, \ 38 | century, \ 39 | getUnixTime());} 40 | 41 | 42 | DS3231DateTime::DS3231DateTime() 43 | { 44 | seconds = 0; 45 | minutes = 0; 46 | hours = 0; 47 | date = 0; 48 | day = 0; 49 | month = 0; 50 | year = 0; 51 | century = 0; 52 | } 53 | 54 | boolean DS3231DateTime::isValid() 55 | { 56 | dbvalue("DS3231DateTime::isValid"); 57 | 58 | if (seconds > 59) 59 | { 60 | dlog.error(FPSTR(TAG), F("::isValid: invalid seconds %d"), seconds); 61 | return false; 62 | } 63 | 64 | if (minutes > 59) 65 | { 66 | dlog.error(FPSTR(TAG), F("::isValid: invalid minutes %d"), minutes); 67 | return false; 68 | } 69 | 70 | if (hours > 23) 71 | { 72 | dlog.error(FPSTR(TAG), F("::isValid: invalid hours %d"), hours); 73 | return false; 74 | } 75 | 76 | if (date > 31) 77 | { 78 | dlog.error(FPSTR(TAG), F("::isValid: invalid hours %d"), hours); 79 | return false; 80 | } 81 | 82 | if ((month > 12) || (month < 1)) 83 | { 84 | dlog.error(FPSTR(TAG), F("::isValid: invalid month %d"), month); 85 | return false; 86 | } 87 | 88 | if (year > 99) 89 | { 90 | dlog.error(FPSTR(TAG), F("::isValid: invalid year %d"), year); 91 | return false; 92 | } 93 | 94 | return true; 95 | } 96 | 97 | void DS3231DateTime::setUnixTime(unsigned long time) 98 | { 99 | struct tm tm; 100 | TimeUtils::gmtime_r((time_t*)&time, &tm); 101 | 102 | dlog.debug(FPSTR(TAG), F("::setUnixTime month: %d"), tm.tm_mon); 103 | 104 | seconds = tm.tm_sec; 105 | minutes = tm.tm_min; 106 | hours = tm.tm_hour; 107 | day = tm.tm_wday; 108 | date = tm.tm_mday; 109 | month = tm.tm_mon + 1; 110 | year = tm.tm_year - 100; 111 | century = 0; 112 | dbvalue("setUnixTime new value:"); 113 | } 114 | 115 | unsigned long DS3231DateTime::getUnixTime() 116 | { 117 | struct tm tm; 118 | tm.tm_sec = seconds; 119 | tm.tm_min = minutes; 120 | tm.tm_hour = hours; 121 | tm.tm_mday = date; 122 | tm.tm_mon = month - 1; 123 | tm.tm_year = year + 100; 124 | tm.tm_isdst = 0; 125 | tm.tm_wday = 0; 126 | tm.tm_yday = 0; 127 | unsigned long unix = TimeUtils::mktime(&tm); 128 | dlog.debug(FPSTR(TAG), F("::getUnixTime: returning unix time: %lu"), unix); 129 | return unix; 130 | } 131 | 132 | void DS3231DateTime::applyOffset(int offset) 133 | { 134 | unsigned long now = getUnixTime(); 135 | now += offset; 136 | setUnixTime(now); 137 | } 138 | 139 | uint16_t DS3231DateTime::getPosition() 140 | { 141 | int h = hours; 142 | if (h > 12) 143 | { 144 | h -= 12; 145 | } 146 | return h*3600 + minutes*60 + seconds; 147 | } 148 | 149 | uint16_t DS3231DateTime::getPosition(int offset) 150 | { 151 | int signed_position = getPosition(); 152 | dlog.debug(FPSTR(TAG), F("::getPosition: position before offset: %d"), signed_position); 153 | signed_position += offset; 154 | dlog.debug(FPSTR(TAG), F("::getPosition: position after offset: %d"), signed_position); 155 | if (signed_position < 0) 156 | { 157 | signed_position += MAX_POSITION; 158 | dlog.debug(FPSTR(TAG), F("::getPosition: position corrected+: %d"), signed_position); 159 | } 160 | else if (signed_position >= MAX_POSITION) 161 | { 162 | signed_position -= MAX_POSITION; 163 | dlog.debug(FPSTR(TAG), F("::getPosition: position corrected-: %d"), signed_position); 164 | } 165 | uint16_t position = (uint16_t) signed_position; 166 | return position; 167 | } 168 | 169 | char value_buf[128]; 170 | 171 | const char* DS3231DateTime::string() 172 | { 173 | snprintf(value_buf, 127, 174 | "%04u-%02u-%02u %02u:%02u:%02u", 175 | year+1900+100, 176 | month, 177 | date, 178 | hours, 179 | minutes, 180 | seconds); 181 | return value_buf; 182 | } 183 | 184 | uint8_t DS3231DateTime::getDay() 185 | { 186 | return day; 187 | } 188 | 189 | uint8_t DS3231DateTime::getDate() 190 | { 191 | return date; 192 | } 193 | 194 | uint8_t DS3231DateTime::getHour() 195 | { 196 | return hours; 197 | } 198 | 199 | uint8_t DS3231DateTime::getMonth() 200 | { 201 | return month; 202 | } 203 | 204 | uint16_t DS3231DateTime::getYear() 205 | { 206 | return year+2000; 207 | } 208 | 209 | -------------------------------------------------------------------------------- /SynchroClock/lib/DS3231/src/DS3231DateTime.h: -------------------------------------------------------------------------------- 1 | /* 2 | * DS3231DateTime.h 3 | * 4 | * Copyright 2017 Christopher B. Liebman 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * 19 | * Created on: May 22, 2017 20 | * Author: chris.l 21 | */ 22 | 23 | #ifndef DS3231DATETIME_H_ 24 | #define DS3231DATETIME_H_ 25 | #include 26 | #include "TimeUtils.h" 27 | #include "Logger.h" 28 | 29 | #define MAX_POSITION 43200 // seconds in 12 hours 30 | 31 | class DS3231DateTime 32 | { 33 | public: 34 | DS3231DateTime(); 35 | boolean isValid(); 36 | void setUnixTime(unsigned long time); 37 | unsigned long getUnixTime(); 38 | void applyOffset(int offset); 39 | uint16_t getPosition(); 40 | uint16_t getPosition(int offset); 41 | const char* string(); 42 | 43 | uint8_t getDay(); 44 | uint8_t getDate(); 45 | uint8_t getHour(); 46 | uint8_t getMonth(); 47 | uint16_t getYear(); 48 | 49 | friend class DS3231; 50 | protected: 51 | uint8_t seconds; 52 | uint8_t minutes; 53 | uint8_t hours; 54 | uint8_t date; 55 | uint8_t day; 56 | uint8_t month; 57 | uint8_t year; 58 | uint8_t century; 59 | }; 60 | 61 | #endif /* DS3231DATETIME_H_ */ 62 | -------------------------------------------------------------------------------- /SynchroClock/lib/FeedbackLED/src/FeedbackLED.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * 4 | * Copyright 2017 Christopher B. Liebman 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * 19 | */ 20 | #include "Arduino.h" 21 | #include "FeedbackLED.h" 22 | 23 | FeedbackLED::FeedbackLED(int pin) { 24 | pinMode(pin, OUTPUT); 25 | _pin = pin; 26 | } 27 | 28 | void FeedbackLED::on() { 29 | ticker.detach(); 30 | digitalWrite(_pin, LOW); 31 | } 32 | 33 | void FeedbackLED::off() { 34 | ticker.detach(); 35 | digitalWrite(_pin, HIGH); 36 | } 37 | 38 | void FeedbackLED::toggle() { 39 | ticker.detach(); 40 | int state = digitalRead(_pin); // get the current state of LED pin 41 | digitalWrite(_pin, !state); // set pin to the opposite state 42 | } 43 | 44 | void FeedbackLED::blink(float rate) { 45 | digitalWrite(_pin, LOW); 46 | ticker.attach(rate, tick, this); 47 | } 48 | 49 | void FeedbackLED::tick(FeedbackLED* fb) { 50 | int state = digitalRead(fb->_pin); // get the current state of LED pin 51 | digitalWrite(fb->_pin, !state); // set pin to the opposite state 52 | } 53 | 54 | -------------------------------------------------------------------------------- /SynchroClock/lib/FeedbackLED/src/FeedbackLED.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Christopher B. Liebman 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | #ifndef FeedbackLED_h 18 | #define FeedbackLED_h 19 | 20 | #include 21 | 22 | #define FEEDBACK_LED_FAST 0.2 23 | #define FEEDBACK_LED_MEDIUM 0.4 24 | #define FEEDBACK_LED_SLOW 0.6 25 | 26 | class FeedbackLED { 27 | public: 28 | FeedbackLED(int pin); 29 | void on(); 30 | void off(); 31 | void toggle(); 32 | 33 | void blink(float rate); 34 | 35 | private: 36 | int _pin; 37 | Ticker ticker; 38 | // static callback for ticker 39 | static void tick(FeedbackLED *fb); 40 | }; 41 | 42 | #endif 43 | 44 | -------------------------------------------------------------------------------- /SynchroClock/lib/Logger/src/Logger.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Logger.h 3 | * 4 | * Copyright 2017 Christopher B. Liebman 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * 19 | * Created on: May 31, 2017 20 | * Author: liebman 21 | */ 22 | 23 | #ifndef LOGGER_H_ 24 | #define LOGGER_H_ 25 | 26 | #include "DLog.h" 27 | 28 | extern DLog& dlog; 29 | 30 | #endif /* LOGGER_H_ */ 31 | -------------------------------------------------------------------------------- /SynchroClock/lib/NTP/src/NTP.h: -------------------------------------------------------------------------------- 1 | /* 2 | * NTPProto.h 3 | * 4 | * Created on: Jul 6, 2017 5 | * Author: chris.l 6 | */ 7 | 8 | #ifndef NTP_H_ 9 | #define NTP_H_ 10 | 11 | #include "Arduino.h" 12 | #include "SimplePing.h" 13 | #include "Timer.h" 14 | #include "UDPWrapper.h" 15 | #include "Logger.h" 16 | 17 | #define NTP_PORT 123 18 | 19 | #ifndef NTP_REQUEST_COUNT 20 | #define NTP_REQUEST_COUNT 1 21 | #endif 22 | 23 | typedef struct ntp_sample 24 | { 25 | uint32_t timestamp; 26 | double offset; 27 | double delay; 28 | } NTPSample; 29 | 30 | typedef struct ntp_adjustment 31 | { 32 | uint32_t timestamp; 33 | double adjustment; 34 | } NTPAdjustment; 35 | 36 | #define NTP_SERVER_LENGTH 64 // max length+1 of ntp server name 37 | #define NTP_SAMPLE_COUNT 10 // number of NTP samples to keep for std devation filtering 38 | #define NTP_ADJUSTMENT_COUNT 8 // number of NTP adjustments to keep for least squares drift 39 | #define NTP_OFFSET_THRESHOLD 0.02 // 20ms offset minimum for adjust! 40 | #ifndef NTP_MAX_INTERVAL 41 | #define NTP_MAX_INTERVAL 129600 // 36 hours 42 | #endif 43 | #ifndef NTP_MIN_INTERVAL 44 | #define NTP_MIN_INTERVAL 3600 // minimum computed interval 45 | #endif 46 | #ifndef NTP_SAMPLE_INTERVAL 47 | #define NTP_SAMPLE_INTERVAL (5*3600/(NTP_SAMPLE_COUNT)) // default to get the first set of samples in 4 hours 48 | #endif 49 | #ifndef NTP_UNREACH_LAST_INTERVAL 50 | #define NTP_UNREACH_LAST_INTERVAL 3600 // last NTP was unreachable 51 | #endif 52 | #ifndef NTP_UNREACH_INTERVAL 53 | #define NTP_UNREACH_INTERVAL 900 // last few NTP unreachable 54 | #endif 55 | 56 | // 57 | // Long term persisted data includes drift an last adjustment information 58 | // so that we don't have to wait for a long time after power loss for drift 59 | // to be active once we have computed it the first time. 60 | // 61 | typedef struct ntp_persist 62 | { 63 | NTPAdjustment adjustments[NTP_ADJUSTMENT_COUNT]; 64 | int nadjustments; 65 | double drift; // computed drift in parts per million 66 | } NTPPersist; 67 | 68 | // 69 | // This is used to validate new NTP responses and compute the clock drift 70 | // 71 | typedef struct ntp_runtime 72 | { 73 | NTPSample samples[NTP_SAMPLE_COUNT]; 74 | int nsamples; 75 | uint32_t drift_timestamp; // last time drift was applied 76 | double drifted; // how much drift we have applied since the last NTP poll. 77 | uint32_t update_timestamp; // last time an update was applied 78 | double drift_estimate; // used to compute the poll interval 79 | double poll_interval; // estimated time between adjustments based on estimated drift 80 | double delay_mean; // mean value of sample delay 81 | double delay_stddev; // standard deviation of sample delay 82 | // cache these to know when we need to lookup the host again and if its been unreachable. 83 | char server[NTP_SERVER_LENGTH]; // cached server name 84 | uint32_t ip; // cached server ip address (only works for tcp v4) 85 | uint8_t reach; 86 | } NTPRunTime; 87 | 88 | typedef struct ntp_time 89 | { 90 | uint32_t seconds; 91 | uint32_t fraction; 92 | } NTPTime; 93 | 94 | typedef struct ntp_packet 95 | { 96 | uint8_t flags; 97 | uint8_t stratum; 98 | uint8_t poll; 99 | int8_t precision; 100 | uint32_t delay; 101 | uint32_t dispersion; 102 | uint8_t ref_id[4]; 103 | NTPTime ref_time; 104 | NTPTime orig_time; 105 | NTPTime recv_time; 106 | NTPTime xmit_time; 107 | } NTPPacket; 108 | 109 | class NTP 110 | { 111 | public: 112 | NTP(NTPRunTime *runtime, NTPPersist *persist, void (*savePersist)(), int factor=1); 113 | void begin(int port = NTP_PORT); 114 | 115 | uint32_t getPollInterval(); 116 | int getOffsetUsingDrift(double *offset, int (*getTime)(uint32_t *result)); 117 | // return next poll delay or -1 on error. 118 | int getOffset(const char* server, double* offset, int (*getTime)(uint32_t *result)); 119 | int getLastOffset(double* offset); 120 | IPAddress getAddress(); 121 | protected: 122 | int makeRequest(IPAddress address, double *offset, double *delay, uint32_t *timestamp, int (*getTime)(uint32_t *result)); 123 | int makeRequest(IPAddress address, double *offset, double *delay, uint32_t *timestamp, int (*getTime)(uint32_t *result), const unsigned int bestof); 124 | int process(uint32_t timestamp, double offset, double delay); 125 | void clock(); 126 | void computeDrift(double* drift_result); 127 | void updateDriftEstimate(); 128 | private: 129 | NTPRunTime *_runtime; 130 | NTPPersist *_persist; 131 | void (*_savePersist)(); 132 | UDPWrapper _udp; 133 | int _port; 134 | int _factor; // only used when testing to reduce fixed poll interval values by factor 135 | }; 136 | 137 | 138 | #endif /* NTP_H_ */ 139 | -------------------------------------------------------------------------------- /SynchroClock/lib/NTP/src/NTPPrivate.h: -------------------------------------------------------------------------------- 1 | /* 2 | * NTPPrivate.h 3 | * 4 | * Created on: Jul 6, 2017 5 | * Author: chris.l 6 | */ 7 | 8 | #ifndef NTPPRIVATE_H_ 9 | #define NTPPRIVATE_H_ 10 | 11 | #define LI_NONE 0 12 | #define LI_SIXTY_ONE 1 13 | #define LI_FIFTY_NINE 2 14 | #define LI_NOSYNC 3 15 | 16 | #define MODE_RESERVED 0 17 | #define MODE_ACTIVE 1 18 | #define MODE_PASSIVE 2 19 | #define MODE_CLIENT 3 20 | #define MODE_SERVER 4 21 | #define MODE_BROADCAST 5 22 | #define MODE_CONTROL 6 23 | #define MODE_PRIVATE 7 24 | 25 | #define NTP_VERSION 4 26 | 27 | #define MINPOLL 6 // % minimum poll interval (64 s) 28 | #define MAXPOLL 17 /* % maximum poll interval (36.4 h) */ 29 | 30 | 31 | #define setLI(value) ((value&0x03)<<6) 32 | #define setVERS(value) ((value&0x07)<<3) 33 | #define setMODE(value) ((value&0x07)) 34 | 35 | #define getLI(value) ((value>>6)&0x03) 36 | #define getVERS(value) ((value>>3)&0x07) 37 | #define getMODE(value) (value&0x07) 38 | 39 | #define SEVENTY_YEARS 2208988800L 40 | #define toEPOCH(t) ((uint32_t)t-SEVENTY_YEARS) 41 | #define toNTP(t) ((uint32_t)t+SEVENTY_YEARS) 42 | #define toUINT64(x) (((uint64_t)(x.seconds)<<32) + x.fraction) 43 | 44 | #define FP2D(x) ((double)(x)/65536) 45 | #define LFP2D(x) (((double)(x))/4294967296L) 46 | #define ms2fraction(x) ((uint32_t)((double)(x) / 1000.0 * (double)4294967296L)) 47 | #define LOG2D(a) ((a) < 0 ? 1. / (1L << -(a)) : 1L << (a)) 48 | #define SQUARE(x) ((x) * (x)) 49 | #define SQRT(x) (sqrt(x)) 50 | 51 | // simple versions - we don't worry about side effects 52 | #define max(a, b) ((a) < (b) ? (b) : (a)) 53 | #define min(a, b) ((a) < (b) ? (a) : (b)) 54 | 55 | #endif /* NTPPRIVATE_H_ */ 56 | -------------------------------------------------------------------------------- /SynchroClock/lib/SimplePing/src/SimplePing.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * SimplePing.cpp 3 | * 4 | * Created on: Jul 7, 2017 5 | * Author: chris.l 6 | */ 7 | 8 | #include "SimplePing.h" 9 | #include 10 | 11 | SimplePing::SimplePing() 12 | { 13 | // TODO Auto-generated constructor stub 14 | 15 | } 16 | 17 | 18 | struct ping_option _po; 19 | void SimplePing::ping(IPAddress server) 20 | { 21 | _po.ip = server; 22 | _po.count = 1; 23 | _po.coarse_time = 1; 24 | _po.sent_function = NULL; 25 | _po.recv_function = NULL; 26 | ping_start(&_po); 27 | delay(100); // time for arp and xmit 28 | } 29 | -------------------------------------------------------------------------------- /SynchroClock/lib/SimplePing/src/SimplePing.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Ping.h 3 | * 4 | * Created on: Jul 7, 2017 5 | * Author: chris.l 6 | */ 7 | 8 | #ifndef _SIMPLE_PING_H_ 9 | #define _SIMPLE_PING_H_ 10 | #include "Arduino.h" 11 | #include 12 | 13 | class SimplePing 14 | { 15 | public: 16 | SimplePing(); 17 | void ping(IPAddress address); 18 | }; 19 | #endif /* _SIMPLE_PING_H_ */ 20 | -------------------------------------------------------------------------------- /SynchroClock/lib/TimeUtils/src/TimeUtils.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * TimeUtils.cpp 3 | * 4 | * Copyright 2017 Christopher B. Liebman 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * 19 | * Created on: Jun 7, 2017 20 | * Author: liebman 21 | */ 22 | 23 | #include "TimeUtils.h" 24 | 25 | static PROGMEM const char TAG[] = "TimeUtils"; 26 | 27 | uint8_t TimeUtils::parseSmallDuration(const char* value) 28 | { 29 | int i = atoi(value); 30 | if (i < 0 || i > 255) 31 | { 32 | dlog.warning(FPSTR(TAG), F("::parseSmallDuration: invalid value %s: using 32 instead!"), value); 33 | i = 32; 34 | } 35 | return (uint8_t) i; 36 | } 37 | 38 | uint8_t TimeUtils::parseOccurrence(const char* occurrence_string) 39 | { 40 | int i = atoi(occurrence_string); 41 | if (i < -5 || i == 0 || i > 5) 42 | { 43 | dlog.warning(FPSTR(TAG), F("::parseOccurrence: invalid value %s: using 1 instead!"), occurrence_string); 44 | i = 1; 45 | } 46 | return (uint8_t) i; 47 | } 48 | 49 | uint8_t TimeUtils::parseDayOfWeek(const char* dow_string) 50 | { 51 | int i = atoi(dow_string); 52 | if (i < 0 || i > 6) 53 | { 54 | dlog.warning(FPSTR(TAG), F("::parseDayOfWeek: invalid value %s: using 0 (Sunday) instead!"), dow_string); 55 | i = 1; 56 | } 57 | return (uint8_t) i; 58 | } 59 | 60 | uint8_t TimeUtils::parseMonth(const char* month_string) 61 | { 62 | int i = atoi(month_string); 63 | if (i < 0 || i > 12) 64 | { 65 | dlog.warning(FPSTR(TAG), F("::parseMonth: invalid value '%s': using 3 (Mar) instead!"), month_string); 66 | i = 1; 67 | } 68 | return (uint8_t) i; 69 | } 70 | 71 | uint8_t TimeUtils::parseHour(const char* hour_string) 72 | { 73 | int i = atoi(hour_string); 74 | if (i < 0 || i > 23) 75 | { 76 | dlog.warning(FPSTR(TAG), F("::parseMonth: invalid value '%s': using 2 instead!"), hour_string); 77 | i = 1; 78 | } 79 | return (uint8_t) i; 80 | } 81 | 82 | int TimeUtils::parseOffset(const char* offset_string) 83 | { 84 | int result = 0; 85 | char value[11]; 86 | strncpy(value, offset_string, 10); 87 | if (strchr(value, ':') != NULL) 88 | { 89 | int sign = 1; 90 | char* s; 91 | 92 | if (value[0] == '-') 93 | { 94 | sign = -1; 95 | s = strtok(&(value[1]), ":"); 96 | } 97 | else 98 | { 99 | s = strtok(value, ":"); 100 | } 101 | if (s != NULL) 102 | { 103 | int h = atoi(s); 104 | while (h > 11) 105 | { 106 | h -= 12; 107 | } 108 | 109 | result += h * 3600; // hours to seconds 110 | s = strtok(NULL, ":"); 111 | } 112 | if (s != NULL) 113 | { 114 | result += atoi(s) * 60; // minutes to seconds 115 | s = strtok(NULL, ":"); 116 | } 117 | if (s != NULL) 118 | { 119 | result += atoi(s); 120 | } 121 | // apply sign 122 | result *= sign; 123 | } 124 | else 125 | { 126 | result = atoi(value); 127 | if (result < -43199 || result > 43199) 128 | { 129 | result = 0; 130 | } 131 | } 132 | return result; 133 | } 134 | 135 | uint16_t TimeUtils::parsePosition(const char* position_string) 136 | { 137 | int result = 0; 138 | char value[10]; 139 | strncpy(value, position_string, 9); 140 | if (strchr(value, ':') != NULL) 141 | { 142 | char* s = strtok(value, ":"); 143 | 144 | if (s != NULL) 145 | { 146 | int h = atoi(s); 147 | while (h > 11) 148 | { 149 | h -= 12; 150 | } 151 | 152 | result += h * 3600; // hours to seconds 153 | s = strtok(NULL, ":"); 154 | } 155 | if (s != NULL) 156 | { 157 | result += atoi(s) * 60; // minutes to seconds 158 | s = strtok(NULL, ":"); 159 | } 160 | if (s != NULL) 161 | { 162 | result += atoi(s); 163 | } 164 | } 165 | else 166 | { 167 | result = atoi(value); 168 | if (result < 0 || result > 43199) 169 | { 170 | result = 0; 171 | } 172 | } 173 | return result; 174 | } 175 | 176 | 177 | // 178 | // Modified code from: http://www.jbox.dk/sanos/source/lib/time.c.html 179 | // 180 | 181 | #define YEAR0 1900 182 | #define EPOCH_YR 1970 183 | #define SECS_DAY (24L * 60L * 60L) 184 | #define LEAPYEAR(year) (!((year) % 4) && (((year) % 100) || !((year) % 400))) 185 | #define YEARSIZE(year) (LEAPYEAR(year) ? 366 : 365) 186 | #define TIME_MAX 2147483647L 187 | 188 | const int _ytab[2][12] = 189 | { 190 | { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }, 191 | { 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 } }; 192 | 193 | // esp version is broken :-( 194 | time_t TimeUtils::mktime(struct tm *tmbuf) 195 | { 196 | long day, year; 197 | int tm_year; 198 | int yday, month; 199 | /*unsigned*/long seconds; 200 | int overflow; 201 | 202 | tmbuf->tm_min += tmbuf->tm_sec / 60; 203 | tmbuf->tm_sec %= 60; 204 | if (tmbuf->tm_sec < 0) 205 | { 206 | tmbuf->tm_sec += 60; 207 | tmbuf->tm_min--; 208 | } 209 | tmbuf->tm_hour += tmbuf->tm_min / 60; 210 | tmbuf->tm_min = tmbuf->tm_min % 60; 211 | if (tmbuf->tm_min < 0) 212 | { 213 | tmbuf->tm_min += 60; 214 | tmbuf->tm_hour--; 215 | } 216 | day = tmbuf->tm_hour / 24; 217 | tmbuf->tm_hour = tmbuf->tm_hour % 24; 218 | if (tmbuf->tm_hour < 0) 219 | { 220 | tmbuf->tm_hour += 24; 221 | day--; 222 | } 223 | tmbuf->tm_year += tmbuf->tm_mon / 12; 224 | tmbuf->tm_mon %= 12; 225 | if (tmbuf->tm_mon < 0) 226 | { 227 | tmbuf->tm_mon += 12; 228 | tmbuf->tm_year--; 229 | } 230 | day += (tmbuf->tm_mday - 1); 231 | while (day < 0) 232 | { 233 | if (--tmbuf->tm_mon < 0) 234 | { 235 | tmbuf->tm_year--; 236 | tmbuf->tm_mon = 11; 237 | } 238 | day += _ytab[LEAPYEAR(YEAR0 + tmbuf->tm_year)][tmbuf->tm_mon]; 239 | } 240 | while (day >= _ytab[LEAPYEAR(YEAR0 + tmbuf->tm_year)][tmbuf->tm_mon]) 241 | { 242 | day -= _ytab[LEAPYEAR(YEAR0 + tmbuf->tm_year)][tmbuf->tm_mon]; 243 | if (++(tmbuf->tm_mon) == 12) 244 | { 245 | tmbuf->tm_mon = 0; 246 | tmbuf->tm_year++; 247 | } 248 | } 249 | tmbuf->tm_mday = day + 1; 250 | year = EPOCH_YR; 251 | if (tmbuf->tm_year < year - YEAR0) 252 | return (time_t) -1; 253 | seconds = 0; 254 | day = 0; // Means days since day 0 now 255 | overflow = 0; 256 | 257 | // Assume that when day becomes negative, there will certainly 258 | // be overflow on seconds. 259 | // The check for overflow needs not to be done for leapyears 260 | // divisible by 400. 261 | // The code only works when year (1970) is not a leapyear. 262 | tm_year = tmbuf->tm_year + YEAR0; 263 | 264 | if (TIME_MAX / 365 < tm_year - year) 265 | overflow++; 266 | day = (tm_year - year) * 365; 267 | if (TIME_MAX - day < (tm_year - year) / 4 + 1) 268 | overflow++; 269 | day += (tm_year - year) / 4 + ((tm_year % 4) && tm_year % 4 < year % 4); 270 | day -= (tm_year - year) / 100 271 | + ((tm_year % 100) && tm_year % 100 < year % 100); 272 | day += (tm_year - year) / 400 273 | + ((tm_year % 400) && tm_year % 400 < year % 400); 274 | 275 | yday = month = 0; 276 | while (month < tmbuf->tm_mon) 277 | { 278 | yday += _ytab[LEAPYEAR(tm_year)][month]; 279 | month++; 280 | } 281 | yday += (tmbuf->tm_mday - 1); 282 | if (day + yday < 0) 283 | overflow++; 284 | day += yday; 285 | 286 | tmbuf->tm_yday = yday; 287 | tmbuf->tm_wday = (day + 4) % 7; // Day 0 was thursday (4) 288 | 289 | seconds = ((tmbuf->tm_hour * 60L) + tmbuf->tm_min) * 60L + tmbuf->tm_sec; 290 | 291 | if ((TIME_MAX - seconds) / SECS_DAY < day) 292 | overflow++; 293 | seconds += day * SECS_DAY; 294 | 295 | if (overflow) 296 | return (time_t) -1; 297 | 298 | if ((time_t) seconds != seconds) 299 | return (time_t) -1; 300 | return (time_t) seconds; 301 | } 302 | 303 | struct tm *TimeUtils::gmtime_r(const time_t *timer, struct tm *tmbuf) 304 | { 305 | time_t time = *timer; 306 | unsigned long dayclock, dayno; 307 | int year = EPOCH_YR; 308 | 309 | dayclock = (unsigned long) time % SECS_DAY; 310 | dayno = (unsigned long) time / SECS_DAY; 311 | 312 | tmbuf->tm_sec = dayclock % 60; 313 | tmbuf->tm_min = (dayclock % 3600) / 60; 314 | tmbuf->tm_hour = dayclock / 3600; 315 | tmbuf->tm_wday = (dayno + 4) % 7; // Day 0 was a thursday 316 | while (dayno >= (unsigned long) YEARSIZE(year)) { 317 | dayno -= YEARSIZE(year); 318 | year++; 319 | } 320 | tmbuf->tm_year = year - YEAR0; 321 | tmbuf->tm_yday = dayno; 322 | tmbuf->tm_mon = 0; 323 | while (dayno >= (unsigned long) _ytab[LEAPYEAR(year)][tmbuf->tm_mon]) { 324 | dayno -= _ytab[LEAPYEAR(year)][tmbuf->tm_mon]; 325 | tmbuf->tm_mon++; 326 | } 327 | tmbuf->tm_mday = dayno + 1; 328 | tmbuf->tm_isdst = 0; 329 | return tmbuf; 330 | } 331 | 332 | char* TimeUtils::time2str(const time_t t) 333 | { 334 | static char time_storage[30]; 335 | char* s = ctime_r(&t, time_storage); 336 | if (s != nullptr && strlen(s) > 0) 337 | { 338 | s[strlen(s)-1] = '\0'; 339 | } 340 | return s; 341 | } 342 | 343 | // 344 | // The functions findDOW & findNthDate are from: 345 | // 346 | // http://hackaday.com/2012/07/16/automatic-daylight-savings-time-compensation-for-your-clock-projects 347 | // 348 | 349 | /*-------------------------------------------------------------------------- 350 | FUNC: 6/11/11 - Returns day of week for any given date 351 | PARAMS: year, month, date 352 | RETURNS: day of week (0-7 is Sun-Sat) 353 | NOTES: Sakamoto's Algorithm 354 | http://en.wikipedia.org/wiki/Calculating_the_day_of_the_week#Sakamoto.27s_methods 355 | Altered to use char when possible to save microcontroller ram 356 | --------------------------------------------------------------------------*/ 357 | uint8_t TimeUtils::findDOW(uint16_t y, uint8_t m, uint8_t d) 358 | { 359 | static char t[] = {0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4}; 360 | y -= m < 3; 361 | return (y + y/4 - y/100 + y/400 + t[m-1] + d) % 7; 362 | } 363 | 364 | /*-------------------------------------------------------------------------- 365 | http://hackaday.com/2012/07/16/automatic-daylight-savings-time-compensation-for-your-clock-projects 366 | FUNC: 6/11/11 - Returns the date for Nth day of month. For instance, 367 | it will return the numeric date for the 2nd Sunday of April 368 | PARAMS: year, month, day of week, Nth occurrence of that day in that month 369 | RETURNS: date 370 | NOTES: There is no error checking for invalid inputs. 371 | --------------------------------------------------------------------------*/ 372 | uint8_t TimeUtils::findNthDate(uint16_t year, uint8_t month, uint8_t dow, uint8_t nthWeek) 373 | { 374 | dlog.debug(FPSTR(TAG), F("::findNthDate: year:%u month:%u, dow:%u nthWeek:%d"), year, month, dow, nthWeek); 375 | 376 | uint8_t targetDate = 1; 377 | uint8_t firstDOW = findDOW(year,month,targetDate); 378 | while (firstDOW != dow) { 379 | firstDOW = (firstDOW+1)%7; 380 | targetDate++; 381 | } 382 | //Adjust for weeks 383 | targetDate += (nthWeek-1)*7; 384 | return targetDate; 385 | } 386 | 387 | uint8_t TimeUtils::daysInMonth(uint16_t year, uint8_t month) 388 | { 389 | uint8_t days = 31; 390 | 391 | switch(month) 392 | { 393 | case 2: 394 | days = 28; 395 | if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) 396 | { 397 | days = 29; 398 | } 399 | break; 400 | case 4: 401 | case 6: 402 | case 9: 403 | case 11: 404 | days = 30; 405 | break; 406 | } 407 | return days; 408 | } 409 | 410 | uint8_t TimeUtils::findDateForWeek(uint16_t year, uint8_t month, uint8_t dow, int8_t week) 411 | { 412 | dlog.debug(FPSTR(TAG), F("::findDateForWeek: year:%u month:%u, dow:%u week:%d"), year, month, dow, week); 413 | 414 | uint8_t weeks[5]; 415 | uint8_t max_day = daysInMonth(year, month); 416 | int last = 0; 417 | 418 | if (week >= 0) 419 | { 420 | return findNthDate(year, month, dow, week); 421 | } 422 | 423 | // 424 | // find all times this weekday shows up in the month 425 | // Note that 'last' will end up pointing 1 past the last 426 | // valid occurrence. -1 will give the last one. 427 | // 428 | for(last = 0; last <= 5; ++last) 429 | { 430 | weeks[last] = findNthDate(year, month, dow, last+1); 431 | 432 | dlog.debug(FPSTR(TAG), F("::findDateForWeek: last:%d date:%u"), last, weeks[last]); 433 | 434 | if (weeks[last] > max_day) 435 | { 436 | break; 437 | } 438 | } 439 | 440 | return weeks[last+week]; 441 | } 442 | 443 | int TimeUtils::computeUTCOffset(time_t now, int tz_offset, TimeChange* tc, int tc_count) 444 | { 445 | struct tm tm; 446 | 447 | // 448 | // get the current year 449 | // 450 | gmtime_r(&now, &tm); 451 | int year = tm.tm_year; 452 | 453 | // 454 | // pre-set the offset to the last timechange of the year 455 | // 456 | int offset = tc[tc_count-1].tz_offset; 457 | 458 | // 459 | // loop thru each time change entry, converting it to the time in seconds for the 460 | // current year. If now is greater/equal to the time change the use the new offset. 461 | // We return the last offset that is greater/equal now. 462 | // 463 | for(int i = 0; i < tc_count; ++i) 464 | { 465 | dlog.debug(FPSTR(TAG), F("::computeUTCOffset: index:%d offset:%d month:%u dow:%u occurrence:%d hour:%u day_offset:%d"), 466 | i, 467 | tc[i].tz_offset, 468 | tc[i].month, 469 | tc[i].day_of_week, 470 | tc[i].occurrence, 471 | tc[i].hour, 472 | tc[i].day_offset); 473 | 474 | tm.tm_sec = 0; 475 | tm.tm_min = 0; 476 | tm.tm_hour = tc[i].hour; 477 | tm.tm_mday = TimeUtils::findDateForWeek(year+1900, tc[i].month, tc[i].day_of_week, tc[i].occurrence); 478 | tm.tm_mon = tc[i].month-1; 479 | tm.tm_year = year; 480 | 481 | dlog.debug(FPSTR(TAG), F("::computeUTCOffset: tm: %04d/%02d/%02d %02d:%02d:%02d + %d days"), tm.tm_year+1900, tm.tm_mon+1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec, tc[i].day_offset); 482 | 483 | // convert to seconds 484 | time_t tc_time = mktime(&tm); 485 | // convert to UTC 486 | tc_time -= tz_offset; 487 | dlog.debug(FPSTR(TAG), F("::computeUTCOffset: tc_time: %ld (UTC)"), tc_time); 488 | // add in days offset 489 | tc_time += tc[i].day_offset*86400; 490 | 491 | dlog.debug(FPSTR(TAG), F("::computeUTCOffset: now: %ld tc_time: %ld"), now, tc_time); 492 | 493 | if (now >= tc_time) 494 | { 495 | offset = tc[i].tz_offset; 496 | dlog.debug(FPSTR(TAG), F("::computeUTCOffset: now > tc_time, offset: %d"), offset); 497 | } 498 | } 499 | 500 | return offset; 501 | } 502 | -------------------------------------------------------------------------------- /SynchroClock/lib/TimeUtils/src/TimeUtils.h: -------------------------------------------------------------------------------- 1 | /* 2 | * TimeUtils.h 3 | * 4 | * Copyright 2017 Christopher B. Liebman 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * 19 | * Created on: Jun 7, 2017 20 | * Author: liebman 21 | */ 22 | 23 | #ifndef TIMEUTILS_H_ 24 | #define TIMEUTILS_H_ 25 | #include 26 | #include 27 | #include "Logger.h" 28 | 29 | typedef struct 30 | { 31 | int tz_offset; // seconds offset from UTC 32 | uint8_t month; // starting month that this takes effect. 33 | int8_t occurrence; // 2 is second occurrence of the given day, -1 is last 34 | uint8_t day_of_week; // 0 = Sunday 35 | uint8_t hour; 36 | int8_t day_offset; // +/- days (for Friday before last Sunday type) 37 | } TimeChange; 38 | 39 | class TimeUtils 40 | { 41 | public: 42 | static uint8_t parseSmallDuration(const char* value); 43 | static int parseOffset(const char* offset_string); 44 | static uint16_t parsePosition(const char* position_string); 45 | static uint8_t parseOccurrence(const char* occurrence_string); 46 | static uint8_t parseDayOfWeek(const char* dow_string); 47 | static uint8_t parseMonth(const char* month_string); 48 | static uint8_t parseHour(const char* hour_string); 49 | static time_t mktime(struct tm *tmbuf); 50 | static struct tm* gmtime_r(const time_t *timer, struct tm *tmbuf); 51 | static char* time2str(const time_t t); 52 | static int computeUTCOffset(time_t now, int tz_offset, TimeChange* tc, int tc_count); 53 | static uint8_t findDOW(uint16_t y, uint8_t m, uint8_t d); 54 | static uint8_t findNthDate(uint16_t year, uint8_t month, uint8_t dow, uint8_t nthWeek); 55 | static uint8_t daysInMonth(uint16_t year, uint8_t month); 56 | static uint8_t findDateForWeek(uint16_t year, uint8_t month, uint8_t dow, int8_t nthWeek); 57 | }; 58 | #endif /* TIMEUTILS_H_ */ 59 | -------------------------------------------------------------------------------- /SynchroClock/lib/Timer/src/Timer.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Timer.cpp 3 | * 4 | * Created on: Jul 7, 2017 5 | * Author: chris.l 6 | */ 7 | 8 | #include "Timer.h" 9 | #include 10 | #include "Logger.h" 11 | 12 | uint32_t Timer::_epoch = 0; 13 | 14 | Timer::Timer() 15 | { 16 | _start = 0; 17 | } 18 | 19 | uint32_t Timer::getMillis() 20 | { 21 | uint32_t ms = millis(); 22 | return ms; 23 | } 24 | 25 | void Timer::start() { 26 | _start = getMillis(); 27 | } 28 | 29 | uint32_t Timer::stop() { 30 | uint32_t stop = getMillis(); 31 | return (stop - _start); 32 | } 33 | -------------------------------------------------------------------------------- /SynchroClock/lib/Timer/src/Timer.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Timer.h 3 | * 4 | * Created on: Jul 7, 2017 5 | * Author: chris.l 6 | */ 7 | 8 | #ifndef TIMER_H_ 9 | #define TIMER_H_ 10 | #include "Arduino.h" 11 | 12 | class Timer 13 | { 14 | public: 15 | Timer(); 16 | 17 | static uint32_t getMillis(); 18 | void start(); 19 | uint32_t stop(); 20 | 21 | private: 22 | static uint32_t _epoch; 23 | uint32_t _start; 24 | }; 25 | 26 | #endif /* TIMER_H_ */ 27 | -------------------------------------------------------------------------------- /SynchroClock/lib/UDPWrapper/src/UDPWrapper.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * UDP.cpp 3 | * 4 | * Created on: Jul 8, 2017 5 | * Author: liebman 6 | */ 7 | 8 | #include "UDPWrapper.h" 9 | 10 | static PROGMEM const char TAG[] = "UDPWrapper"; 11 | 12 | UDPWrapper::UDPWrapper() 13 | { 14 | _local_port = -1; 15 | } 16 | 17 | UDPWrapper::~UDPWrapper() 18 | { 19 | close(); 20 | } 21 | 22 | int UDPWrapper::begin(int local_port) 23 | { 24 | _local_port = local_port; 25 | return _udp.begin(local_port); 26 | } 27 | 28 | int UDPWrapper::open(IPAddress address, uint16_t port) 29 | { 30 | dlog.debug(FPSTR(TAG), F("::open address:%u.%u.%u.%u:%u (local port: %d)"), 31 | address[0], address[1], address[2], address[3], port, _local_port); 32 | 33 | if (!_udp.beginPacket(address, port)) 34 | { 35 | dlog.error(FPSTR(TAG), F("::open: beginPacket failed!")); 36 | return 1; 37 | } 38 | return 0; 39 | } 40 | 41 | int UDPWrapper::send(void* buffer, size_t size) 42 | { 43 | dlog.debug(FPSTR(TAG), F("::send: size:%u"), size); 44 | size_t n = _udp.write((const uint8_t *) buffer, size); 45 | 46 | if ( n != size ) 47 | { 48 | dlog.error(FPSTR(TAG), F("::send: write failed! expected %d got %d"), size, n); 49 | } 50 | 51 | if (!_udp.endPacket()) 52 | { 53 | dlog.error(FPSTR(TAG), F("::send: endPacket failed!")); 54 | return n; 55 | } 56 | return n; 57 | } 58 | 59 | int UDPWrapper::recv(void* buffer, size_t wanted, unsigned int timeout_ms) 60 | { 61 | unsigned int start = millis(); 62 | // wait for a packet for at most 1 second 63 | size_t size = 0; 64 | while ((size = _udp.parsePacket()) == 0) 65 | { 66 | yield(); 67 | if (millis() - start > timeout_ms) 68 | { 69 | break; 70 | } 71 | } 72 | 73 | if (size != wanted) 74 | { 75 | dlog.error(FPSTR(TAG), F("::recv: failed wanted:%d != size:%d"), wanted, size); 76 | return size; 77 | } 78 | 79 | _udp.read((char *)buffer, size); 80 | 81 | return size; 82 | } 83 | 84 | int UDPWrapper::close() 85 | { 86 | dlog.debug(FPSTR(TAG), F("::close called!")); 87 | _udp.stop(); 88 | return 0; 89 | } 90 | -------------------------------------------------------------------------------- /SynchroClock/lib/UDPWrapper/src/UDPWrapper.h: -------------------------------------------------------------------------------- 1 | /* 2 | * UDP.h 3 | * 4 | * Created on: Jul 8, 2017 5 | * Author: liebman 6 | */ 7 | 8 | #ifndef UDPWRAPPER_H_ 9 | #define UDPWRAPPER_H_ 10 | #include "Arduino.h" 11 | #include 12 | #include 13 | #include "Logger.h" 14 | 15 | class UDPWrapper 16 | { 17 | public: 18 | UDPWrapper(); 19 | virtual ~UDPWrapper(); 20 | int begin(int local_port); 21 | int open(IPAddress address, uint16_t port); 22 | int send(void* buffer, size_t size); 23 | int recv(void* buffer, size_t size, unsigned int timeout_ms); 24 | int close(); 25 | private: 26 | int _local_port; 27 | WiFiUDP _udp; 28 | }; 29 | 30 | #endif /* UDPWRAPPER_H_ */ 31 | -------------------------------------------------------------------------------- /SynchroClock/lib/WireUtils/src/WireUtils.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * WireUtils.cpp 3 | * 4 | * Copyright 2017 Christopher B. Liebman 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * 19 | * Created on: May 26, 2017 20 | * Author: liebman 21 | */ 22 | 23 | #include "WireUtils.h" 24 | 25 | static PROGMEM const char TAG[] = "WireUtils"; 26 | 27 | WireUtilsC::WireUtilsC() 28 | { 29 | } 30 | 31 | /** 32 | * This routine turns off the I2C bus and clears it 33 | * on return SCA and SCL pins are tri-state inputs. 34 | * You need to call Wire.begin() after this to re-enable I2C 35 | * This routine does NOT use the Wire library at all. 36 | * 37 | * returns 0 if bus cleared 38 | * 1 if SCL held low. 39 | * 2 if SDA held low by slave clock stretch for > 2sec 40 | * 3 if SDA held low after 20 clocks. 41 | */ 42 | int WireUtilsC::clearBus() 43 | { 44 | dlog.info(FPSTR(TAG), F("::ClearBus: attempting to clean i2c bus")); 45 | #if defined(TWCR) && defined(TWEN) 46 | TWCR &= ~(_BV(TWEN)); //Disable the Atmel 2-Wire interface so we can control the SDA and SCL pins directly 47 | #endif 48 | pinMode(SDA, INPUT_PULLUP); // Make SDA (data) and SCL (clock) pins Inputs with pullup. 49 | pinMode(SCL, INPUT_PULLUP); 50 | 51 | boolean SCL_LOW = (digitalRead(SCL) == LOW); // Check is SCL is Low. 52 | if (SCL_LOW) 53 | { //If it is held low Arduno cannot become the I2C master. 54 | dlog.error(FPSTR(TAG), F("::ClearBus: Failed! SCL held low!")); 55 | return 1; //I2C bus error. Could not clear SCL clock line held low 56 | } 57 | 58 | boolean SDA_LOW = (digitalRead(SDA) == LOW); // vi. Check SDA input. 59 | int clockCount = 20; // > 2x9 clock 60 | 61 | while (SDA_LOW && (clockCount > 0)) 62 | { // vii. If SDA is Low, 63 | clockCount--; 64 | // Note: I2C bus is open collector so do NOT drive SCL or SDA high. 65 | pinMode(SCL, INPUT); // release SCL pullup so that when made output it will be LOW 66 | pinMode(SCL, OUTPUT); // then clock SCL Low 67 | delayMicroseconds(10); // for >5uS 68 | pinMode(SCL, INPUT); // release SCL LOW 69 | pinMode(SCL, INPUT_PULLUP); // turn on pullup resistors again 70 | // do not force high as slave may be holding it low for clock stretching. 71 | delayMicroseconds(10); // for >5uS 72 | // The >5uS is so that even the slowest I2C devices are handled. 73 | SCL_LOW = (digitalRead(SCL) == LOW); // Check if SCL is Low. 74 | int counter = 20; 75 | while (SCL_LOW && (counter > 0)) 76 | { // loop waiting for SCL to become High only wait 2sec. 77 | counter--; 78 | delay(100); 79 | SCL_LOW = (digitalRead(SCL) == LOW); 80 | } 81 | if (SCL_LOW) 82 | { // still low after 2 sec error 83 | dlog.error(FPSTR(TAG), F("::ClearBus: Failed! SCL clock line held low by slave clock stretch for >2sec")); 84 | return 2; // I2C bus error. Could not clear. SCL clock line held low by slave clock stretch for >2sec 85 | } 86 | SDA_LOW = (digitalRead(SDA) == LOW); // and check SDA input again and loop 87 | } 88 | if (SDA_LOW) 89 | { // still low 90 | dlog.error(FPSTR(TAG), F("::ClearBus: Failed! SDA data line still held low")); 91 | return 3; // I2C bus error. Could not clear. SDA data line held low 92 | } 93 | 94 | // else pull SDA line low for Start or Repeated Start 95 | pinMode(SDA, INPUT); // remove pullup. 96 | pinMode(SDA, OUTPUT); // and then make it LOW i.e. send an I2C Start or Repeated start control. 97 | // When there is only one I2C master a Start or Repeat Start has the same function as a Stop and clears the bus. 98 | /// A Repeat Start is a Start occurring after a Start with no intervening Stop. 99 | delayMicroseconds(10); // wait >5uS 100 | pinMode(SDA, INPUT); // remove output low 101 | pinMode(SDA, INPUT_PULLUP); // and make SDA high i.e. send I2C STOP control. 102 | delayMicroseconds(10); // x. wait >5uS 103 | pinMode(SDA, INPUT); // and reset pins as tri-state inputs which is the default state on reset 104 | pinMode(SCL, INPUT); 105 | dlog.info(FPSTR(TAG), F("::ClearBus: Success!")); 106 | return 0; // all ok 107 | } 108 | 109 | // act like wire library (even though I don't like it) 110 | WireUtilsC WireUtils = WireUtilsC(); 111 | 112 | -------------------------------------------------------------------------------- /SynchroClock/lib/WireUtils/src/WireUtils.h: -------------------------------------------------------------------------------- 1 | /* 2 | * WireUtils.h 3 | * 4 | * Copyright 2017 Christopher B. Liebman 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | * 18 | * 19 | * Created on: May 26, 2017 20 | * Author: liebman 21 | */ 22 | 23 | #ifndef WIREUTILS_H_ 24 | #define WIREUTILS_H_ 25 | #include 26 | #include "Logger.h" 27 | 28 | class WireUtilsC { 29 | public: 30 | WireUtilsC(); 31 | int clearBus(); 32 | }; 33 | 34 | extern WireUtilsC WireUtils; 35 | 36 | #endif /* WIREUTILS_H_ */ 37 | -------------------------------------------------------------------------------- /SynchroClock/mkdata.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | Import("env", "projenv") 4 | import csv 5 | import os 6 | from subprocess import Popen, PIPE, call 7 | from urllib.request import urlopen 8 | from io import StringIO 9 | 10 | # 11 | # Dump build environment (for debug purpose) 12 | # print env.Dump() 13 | # 14 | 15 | 16 | def generate_ssl_data(source, target, env): 17 | print("generate_ssl_data") 18 | 19 | # Mozilla's URL for the CSV file with included PEM certs 20 | mozurl = "https://ccadb-public.secure.force.com/mozilla/IncludedCACertificateReportPEMCSV" 21 | 22 | # Load the manes[] and pems[] array from the URL 23 | names = [] 24 | pems = [] 25 | response = urlopen(mozurl) 26 | csvData = response.read() 27 | csvReader = csv.reader(StringIO(csvData)) 28 | for row in csvReader: 29 | names.append(row[0]+":"+row[1]+":"+row[2]) 30 | pems.append(row[30]) 31 | del names[0] # Remove headers 32 | del pems[0] # Remove headers 33 | 34 | # Try and make ./data, skip if present 35 | try: 36 | os.mkdir("data") 37 | except: 38 | pass 39 | 40 | derFiles = [] 41 | idx = 0 42 | # Process the text PEM using openssl into DER files 43 | for i in range(0, len(pems)): 44 | certName = "data/ca_%03d.der" % (idx) 45 | thisPem = pems[i].replace("'", "") 46 | print(names[i] + " -> " + certName) 47 | ssl = Popen(['openssl','x509','-inform','PEM','-outform','DER','-out', certName], shell = False, stdin = PIPE) 48 | pipe = ssl.stdin 49 | pipe.write(thisPem) 50 | pipe.close() 51 | ssl.wait() 52 | if os.path.exists(certName): 53 | derFiles.append(certName) 54 | idx = idx + 1 55 | 56 | if os.path.exists("data/certs.ar"): 57 | os.unlink("data/certs.ar"); 58 | 59 | arCmd = '$AR mcs data/certs.ar ' + " ".join(derFiles); 60 | print(arCmd) 61 | env.Execute(arCmd) 62 | 63 | for der in derFiles: 64 | os.unlink(der) 65 | 66 | 67 | print("Current build targets", list(map(str, BUILD_TARGETS))) 68 | 69 | # custom action before building SPIFFS image. For example, compress HTML, etc. 70 | env.AddPreAction("$BUILD_DIR/spiffs.bin", generate_ssl_data) 71 | -------------------------------------------------------------------------------- /SynchroClock/platformio.ini: -------------------------------------------------------------------------------- 1 | [platformio] 2 | default_envs = la 3 | extra_configs = 4 | local.ini ; can be used for localized envs 5 | 6 | [version] 7 | base = 1.1.12 8 | info = $PIOENV 9 | 10 | [env] 11 | board = esp12e 12 | framework = arduino 13 | upload_resetmethod = nodemcu 14 | upload_speed = 115200 15 | board_build.f_cpu = 80000000L 16 | board_build.f_flash = 40000000L 17 | board_build.flash_mode = qio 18 | src_build_flags = -DVERSION_INFO="${version.base}-${version.info}" 19 | build_flags = 20 | -Wall -Wextra 21 | -Wl,-Teagle.flash.4m1m.ld 22 | -DPIO_FRAMEWORK_ARDUINO_LWIP2_LOW_MEMORY_LOW_FLASH 23 | -DBEARSSL_SSL_BASIC 24 | -DVTABLES_IN_FLASH 25 | -DDLOG_SYSLOG_DELAY=10 26 | ; -DNTP_REQUEST_COUNT=3 27 | monitor_speed = 76800 28 | lib_deps = 29 | https://github.com/liebman/DLog.git 30 | https://github.com/liebman/DLogNet.git 31 | https://github.com/tzapu/WiFiManager.git#e25277b 32 | https://github.com/arcao/Syslog.git#e9c2eea 33 | extra_scripts = post:mkdata.py 34 | platform = espressif8266@2.6.3 35 | 36 | [env:la] 37 | build_flags = ${env.build_flags} 38 | -DDEFAULT_TC0_OCCUR=2 39 | -DDEFAULT_TC0_DOW=0 40 | -DDEFAULT_TC0_DOFF=0 41 | -DDEFAULT_TC0_MONTH=3 42 | -DDEFAULT_TC0_HOUR=2 43 | -DDEFAULT_TC0_OFFSET=-25200 44 | -DDEFAULT_TC1_OCCUR=1 45 | -DDEFAULT_TC1_DOW=0 46 | -DDEFAULT_TC1_DOFF=0 47 | -DDEFAULT_TC1_MONTH=11 48 | -DDEFAULT_TC1_HOUR=2 49 | -DDEFAULT_TC1_OFFSET=-28800 50 | 51 | [env:nyc] 52 | build_flags = ${env.build_flags} 53 | -DDEFAULT_TC0_OCCUR=2 54 | -DDEFAULT_TC0_DOW=0 55 | -DDEFAULT_TC0_DOFF=0 56 | -DDEFAULT_TC0_MONTH=3 57 | -DDEFAULT_TC0_HOUR=2 58 | -DDEFAULT_TC0_OFFSET=-14400 59 | -DDEFAULT_TC1_OCCUR=1 60 | -DDEFAULT_TC1_DOW=0 61 | -DDEFAULT_TC1_DOFF=0 62 | -DDEFAULT_TC1_MONTH=11 63 | -DDEFAULT_TC1_HOUR=2 64 | -DDEFAULT_TC1_OFFSET=-18000 65 | 66 | [env:bkk] 67 | build_flags = ${env.build_flags} 68 | -DDEFAULT_TC0_OCCUR=1 69 | -DDEFAULT_TC0_DOW=0 70 | -DDEFAULT_TC0_DOFF=0 71 | -DDEFAULT_TC0_MONTH=0 72 | -DDEFAULT_TC0_HOUR=0 73 | -DDEFAULT_TC0_OFFSET=25200 74 | -DDEFAULT_TC1_OCCUR=1 75 | -DDEFAULT_TC1_DOW=0 76 | -DDEFAULT_TC1_DOFF=0 77 | -DDEFAULT_TC1_MONTH=0 78 | -DDEFAULT_TC1_HOUR=0 79 | -DDEFAULT_TC1_OFFSET=25200 80 | 81 | [env:uk] 82 | build_flags = ${env.build_flags} 83 | -DDEFAULT_TC0_OCCUR=-1 84 | -DDEFAULT_TC0_DOW=0 85 | -DDEFAULT_TC0_DOFF=0 86 | -DDEFAULT_TC0_MONTH=3 87 | -DDEFAULT_TC0_HOUR=1 88 | -DDEFAULT_TC0_OFFSET=3600 89 | -DDEFAULT_TC1_OCCUR=-1 90 | -DDEFAULT_TC1_DOW=0 91 | -DDEFAULT_TC1_DOFF=0 92 | -DDEFAULT_TC1_MONTH=10 93 | -DDEFAULT_TC1_HOUR=2 94 | -DDEFAULT_TC1_OFFSET=0 95 | 96 | [env:tlv] 97 | build_flags = ${env.build_flags} 98 | -DDEFAULT_TC0_OCCUR=-1 99 | -DDEFAULT_TC0_DOW=0 100 | -DDEFAULT_TC0_DOFF=-2 101 | -DDEFAULT_TC0_MONTH=3 102 | -DDEFAULT_TC0_HOUR=2 103 | -DDEFAULT_TC0_OFFSET=10800 104 | -DDEFAULT_TC1_OCCUR=-1 105 | -DDEFAULT_TC1_DOW=0 106 | -DDEFAULT_TC1_DOFF=0 107 | -DDEFAULT_TC1_MONTH=10 108 | -DDEFAULT_TC1_HOUR=2 109 | -DDEFAULT_TC1_OFFSET=7200 110 | 111 | [env:staging] 112 | platform = https://github.com/platformio/platform-espressif8266.git#feature/stage 113 | platform_packages = 114 | ; use upstream Git version 115 | framework-arduinoespressif8266 @ https://github.com/esp8266/Arduino.git 116 | build_flags = ${env.build_flags} -DUSE_CERT_STORE 117 | -DDEFAULT_TC0_OCCUR=2 118 | -DDEFAULT_TC0_DOW=0 119 | -DDEFAULT_TC0_DOFF=0 120 | -DDEFAULT_TC0_MONTH=3 121 | -DDEFAULT_TC0_HOUR=2 122 | -DDEFAULT_TC0_OFFSET=-25200 123 | -DDEFAULT_TC1_OCCUR=1 124 | -DDEFAULT_TC1_DOW=0 125 | -DDEFAULT_TC1_DOFF=0 126 | -DDEFAULT_TC1_MONTH=11 127 | -DDEFAULT_TC1_HOUR=2 128 | -DDEFAULT_TC1_OFFSET=-28800 129 | -------------------------------------------------------------------------------- /SynchroClock/test/ClockTest/ClockTest.cpp: -------------------------------------------------------------------------------- 1 | #include "Clock.h" 2 | #include "SynchroClockPins.h" 3 | #include "unity.h" 4 | #include "DLogPrintWriter.h" 5 | #include "DS3231.h" 6 | 7 | DLog& dlog = DLog::getLog(); 8 | Clock clk(SYNC_PIN); 9 | DS3231 ds; 10 | 11 | void test_factory_reset() 12 | { 13 | TEST_ASSERT_EQUAL(0, clk.factoryReset()); 14 | } 15 | 16 | void test_begin() 17 | { 18 | TEST_ASSERT_EQUAL(0, clk.begin(2)); 19 | } 20 | 21 | void test_is_present() 22 | { 23 | TEST_ASSERT(clk.isClockPresent()); 24 | } 25 | 26 | void test_version() 27 | { 28 | uint8_t version; 29 | TEST_ASSERT_EQUAL(0, clk.readVersion(& version)); 30 | TEST_ASSERT_EQUAL_UINT8(1, version); 31 | } 32 | 33 | void test_position(uint16_t pos) 34 | { 35 | TEST_ASSERT_EQUAL(0, clk.writePosition(pos)); 36 | uint16_t position; 37 | TEST_ASSERT_EQUAL(0, clk.readPosition(&position)); 38 | TEST_ASSERT_EQUAL(pos, position); 39 | } 40 | 41 | void test_position_0() 42 | { 43 | test_position(0); 44 | } 45 | 46 | void test_position_half() 47 | { 48 | test_position(CLOCK_MAX/2); 49 | } 50 | 51 | void test_position_max() 52 | { 53 | test_position(CLOCK_MAX-1); 54 | } 55 | 56 | void test_adjust(uint16_t adj) 57 | { 58 | TEST_ASSERT_EQUAL(0, clk.writePosition(0)); 59 | TEST_ASSERT_EQUAL(0, clk.writeAdjustment(adj)); 60 | delay(3000); 61 | uint16_t position; 62 | TEST_ASSERT_EQUAL(0, clk.readPosition(&position)); 63 | TEST_ASSERT_EQUAL(10, position); 64 | } 65 | 66 | void test_adjust_10() 67 | { 68 | test_adjust(10); 69 | } 70 | 71 | void test_reads() 72 | { 73 | int errors = 0; 74 | uint16_t position; 75 | for (int i = 0; i < 5000; ++i) 76 | { 77 | errors += clk.readPosition(&position) == 0 ? 0 : 1; 78 | delay(1); 79 | } 80 | TEST_ASSERT_EQUAL(0, errors); 81 | } 82 | 83 | void setup() 84 | { 85 | Wire.begin(); 86 | Wire.setClockStretchLimit(1000000); 87 | ds.begin(); // need this for the tick signal 88 | delay(2000); 89 | UNITY_BEGIN(); 90 | dlog.begin(new DLogPrintWriter(Serial)); 91 | RUN_TEST(test_begin); 92 | delay(10); 93 | RUN_TEST(test_factory_reset); 94 | delay(1000); 95 | RUN_TEST(test_is_present); 96 | RUN_TEST(test_version); 97 | RUN_TEST(test_position_0); 98 | RUN_TEST(test_position_half); 99 | RUN_TEST(test_position_max); 100 | RUN_TEST(test_adjust_10); 101 | RUN_TEST(test_reads); 102 | UNITY_END(); 103 | } 104 | 105 | void loop() 106 | { 107 | delay(1000); 108 | } 109 | -------------------------------------------------------------------------------- /SynchroClock/test/DS3231Test/DS3231Test.cpp: -------------------------------------------------------------------------------- 1 | #include "DS3231.h" 2 | #include "SynchroClockPins.h" 3 | #include "unity.h" 4 | 5 | #define NO_RETRIES 6 | //#define VERBOSE_OUTPUT 7 | 8 | DLog& dlog = DLog::getLog(); 9 | DS3231 ds; 10 | 11 | #ifndef NO_RETRIES 12 | int clear_bus_count; 13 | void clearBus() 14 | { 15 | ++clear_bus_count; 16 | Serial.printf("ClearBus: %d\n", clear_bus_count); 17 | WireUtils.clearBus(); 18 | } 19 | #endif 20 | 21 | #define EDGE_FALLING 0 22 | #define EDGE_RISING 1 23 | 24 | // 25 | // wait for an "edge" of the 1hz signal from the ds3231 26 | // 27 | uint32_t waitForEdge(int edge, uint32_t timeout, uint32_t &start) 28 | { 29 | uint32_t duration = 0; 30 | while(digitalRead(SYNC_PIN) != edge) 31 | { 32 | duration = millis() - start; 33 | if (duration > timeout) 34 | { 35 | return duration; 36 | } 37 | delay(0); 38 | } 39 | uint32_t end = millis(); 40 | duration = end - start; 41 | start = end; 42 | return duration; 43 | } 44 | 45 | #define MIN_MS 499 46 | #define MAX_MS 501 47 | 48 | void test_1hz() 49 | { 50 | uint32_t start = millis(); 51 | // 52 | // The first 2 waits are to sync with the 1hz signal and we can only test for too long a wait 53 | // 54 | uint32_t duration = waitForEdge(EDGE_RISING, MAX_MS, start); 55 | TEST_ASSERT_LESS_OR_EQUAL(MAX_MS, duration); // can't be longer than max 56 | duration = waitForEdge(EDGE_FALLING, MAX_MS, start); 57 | TEST_ASSERT_LESS_OR_EQUAL(MAX_MS, duration); // can't be longer than max 58 | // 59 | // Now we can test for min and max wait times 60 | // 61 | duration = waitForEdge(EDGE_RISING, MAX_MS, start); 62 | TEST_ASSERT_LESS_OR_EQUAL(MAX_MS, duration); 63 | TEST_ASSERT_GREATER_OR_EQUAL(MIN_MS, duration); 64 | duration = waitForEdge(EDGE_FALLING, MAX_MS, start); 65 | TEST_ASSERT_LESS_OR_EQUAL(MAX_MS, duration); 66 | TEST_ASSERT_GREATER_OR_EQUAL(MIN_MS, duration); 67 | } 68 | 69 | void test_begin() 70 | { 71 | TEST_ASSERT_EQUAL(0, ds.begin()); 72 | } 73 | 74 | int readTime(DS3231DateTime& dt, unsigned int retries) 75 | { 76 | #ifdef NO_RETRIES 77 | (void)retries; 78 | return ds.readTime(dt); 79 | #else 80 | while(retries-- > 0) 81 | { 82 | if (ds.readTime(dt) == 0) 83 | { 84 | return 0; 85 | } 86 | clearBus(); 87 | } 88 | return -1; 89 | #endif 90 | } 91 | 92 | #define READ_COUNT 10000 93 | 94 | void test_read() 95 | { 96 | DS3231DateTime dt; 97 | uint32_t read_errors = 0; 98 | uint32_t min_read_time = 0; 99 | uint32_t max_read_time = 0; 100 | for (int i = 0; i max_read_time) 110 | { 111 | max_read_time = duration; 112 | } 113 | delay(0); 114 | } 115 | Serial.printf("read: %d errors: %u min_ms: %u max_ms: %u\n", READ_COUNT, read_errors, min_read_time, max_read_time); 116 | TEST_ASSERT_EQUAL(0, read_errors); 117 | } 118 | 119 | void setup() 120 | { 121 | Wire.begin(); 122 | Wire.setClockStretchLimit(100000); 123 | pinMode(SYNC_PIN, INPUT); 124 | delay(2000); 125 | UNITY_BEGIN(); 126 | RUN_TEST(test_begin); 127 | RUN_TEST(test_1hz); 128 | RUN_TEST(test_read); 129 | #ifndef NO_RETRIES 130 | if (clear_bus_count >= 0) 131 | { 132 | Serial.printf("Total cleanBus: %d\n", clear_bus_count); 133 | clear_bus_count = -1; 134 | } 135 | #endif 136 | UNITY_END(); 137 | } 138 | 139 | void loop() 140 | { 141 | delay(1000); 142 | } 143 | -------------------------------------------------------------------------------- /SynchroClock/test/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for PIO Unit Testing and project tests. 3 | 4 | Unit Testing is a software testing method by which individual units of 5 | source code, sets of one or more MCU program modules together with associated 6 | control data, usage procedures, and operating procedures, are tested to 7 | determine whether they are fit for use. Unit testing finds problems early 8 | in the development cycle. 9 | 10 | More information about PIO Unit Testing: 11 | - https://docs.platformio.org/page/plus/unit-testing.html 12 | -------------------------------------------------------------------------------- /eagle/SynchroClock/SynchroClockBOM.txt: -------------------------------------------------------------------------------- 1 | Partlist exported from /Users/liebman/src/AnalogClock/eagle/SynchroClock/SynchroClock.sch at 10/7/17 9:38 AM 2 | 3 | Part Value Device Package Description 4 | B1 CR1220 CR1220-HOLDER HARWIN-CR1220 5 | C1 10uf C5/2.5 C5B2.5 CAPACITOR 6 | C2 10uf C5/2.5 C5B2.5 CAPACITOR 7 | C3 10uf C5/2.5 C5B2.5 CAPACITOR 8 | C4 100uf CPOL-EUE2.5-5 E2,5-5 POLARIZED CAPACITOR, European symbol 9 | C5 0.1uf C2.5/2 C2.5-2 CAPACITOR 10 | C6 510uf CPOL-USE3.5-8 E3,5-8 POLARIZED CAPACITOR, American symbol 11 | D1 BAT46 BAT46 DO35-10 Schottky Diode 12 | H1 MOUNT-PAD-ROUND2.8 MOUNT-PAD-ROUND2.8 2,8-PAD MOUNTING PAD, round 13 | H2 MOUNT-PAD-ROUND2.8 MOUNT-PAD-ROUND2.8 2,8-PAD MOUNTING PAD, round 14 | H3 MOUNT-PAD-ROUND2.8 MOUNT-PAD-ROUND2.8 2,8-PAD MOUNTING PAD, round 15 | H4 MOUNT-PAD-ROUND2.8 MOUNT-PAD-ROUND2.8 2,8-PAD MOUNTING PAD, round 16 | IC1 DS3231/SO DS3231/SO SO16W 17 | J1 VIN 2_PIN_JST-XH 2_PIN_JST-XH Corrected for XH 2 pin 18 | JP1 !SHDN JUMPER-2PTH 1X02 Jumper 19 | JP2 PWRGD JUMPER-2PTH 1X02 Jumper 20 | JP3 CLOCK PINHD-1X2 1X02 PIN HEADER 21 | LED1 RED LED3MM LED3MM LED 22 | MDL1 ESP12E ESP8266-12EESP8266-ESP12E ESP8266-ESP12E ESP8266-12E with additional I/O and GPIO04/05 corrected 23 | R1 10k R-US_0204/2V 0204V RESISTOR, American symbol 24 | R2 1k R-US_0204/2V 0204V RESISTOR, American symbol 25 | R3 10k R-US_0204/2V 0204V RESISTOR, American symbol 26 | R4 1k R-US_0204/2V 0204V RESISTOR, American symbol 27 | R5 220 R-US_0204/2V 0204V RESISTOR, American symbol 28 | R6 10k R-US_0204/2V 0204V RESISTOR, American symbol 29 | R7 1k R-US_0204/2V 0204V RESISTOR, American symbol 30 | R8 100k R-US_0204/2V 0204V RESISTOR, American symbol 31 | R9 10k R-US_0204/2V 0204V RESISTOR, American symbol 32 | R10 4.7k R-US_0204/2V 0204V RESISTOR, American symbol 33 | R11 4.7k R-US_0204/2V 0204V RESISTOR, American symbol 34 | R12 10k R-US_0204/2V 0204V RESISTOR, American symbol 35 | R13 10k R-US_0204/2V 0204V RESISTOR, American symbol 36 | S1 CONFIG MOMENTARY-SWITCH-SPST-PTH-6.0MM TACTILE_SWITCH_PTH_6.0MM Momentary Switch (Pushbutton) - SPST 37 | SV1 Serial FE06-1 FE06 FEMALE HEADER 38 | U1 MCP1825-33 MCP1825-3302E/AT TO170P482X1024X3124-5P IC, LDO, ADJ, 500MA 39 | U2 ATTINY85-P ATTINY85-P PDIP08_300MIL ATMEL ATtiny 85 40 | -------------------------------------------------------------------------------- /eagle/SynchroClock/eagle.epf: -------------------------------------------------------------------------------- 1 | [Eagle] 2 | Version="09 00 00" 3 | Platform="Mac OS X" 4 | Globals="Globals" 5 | Desktop="Desktop" 6 | 7 | [Globals] 8 | AutoSaveProject=1 9 | UsedLibraryUrn="urn:adsk.eagle:library:79" 10 | UsedLibraryUrn="urn:adsk.eagle:library:88" 11 | UsedLibraryUrn="urn:adsk.eagle:library:178" 12 | UsedLibraryUrn="urn:adsk.eagle:library:179" 13 | UsedLibraryUrn="urn:adsk.eagle:library:180" 14 | UsedLibraryUrn="urn:adsk.eagle:library:181" 15 | UsedLibraryUrn="urn:adsk.eagle:library:182" 16 | UsedLibraryUrn="urn:adsk.eagle:library:183" 17 | UsedLibraryUrn="urn:adsk.eagle:library:184" 18 | UsedLibraryUrn="urn:adsk.eagle:library:185" 19 | UsedLibraryUrn="urn:adsk.eagle:library:186" 20 | UsedLibraryUrn="urn:adsk.eagle:library:187" 21 | UsedLibraryUrn="urn:adsk.eagle:library:89" 22 | UsedLibraryUrn="urn:adsk.eagle:library:188" 23 | UsedLibraryUrn="urn:adsk.eagle:library:189" 24 | UsedLibraryUrn="urn:adsk.eagle:library:190" 25 | UsedLibraryUrn="urn:adsk.eagle:library:191" 26 | UsedLibraryUrn="urn:adsk.eagle:library:192" 27 | UsedLibraryUrn="urn:adsk.eagle:library:193" 28 | UsedLibraryUrn="urn:adsk.eagle:library:194" 29 | UsedLibraryUrn="urn:adsk.eagle:library:195" 30 | UsedLibraryUrn="urn:adsk.eagle:library:196" 31 | UsedLibraryUrn="urn:adsk.eagle:library:197" 32 | UsedLibraryUrn="urn:adsk.eagle:library:90" 33 | UsedLibraryUrn="urn:adsk.eagle:library:198" 34 | UsedLibraryUrn="urn:adsk.eagle:library:199" 35 | UsedLibraryUrn="urn:adsk.eagle:library:200" 36 | UsedLibraryUrn="urn:adsk.eagle:library:201" 37 | UsedLibraryUrn="urn:adsk.eagle:library:202" 38 | UsedLibraryUrn="urn:adsk.eagle:library:203" 39 | UsedLibraryUrn="urn:adsk.eagle:library:204" 40 | UsedLibraryUrn="urn:adsk.eagle:library:205" 41 | UsedLibraryUrn="urn:adsk.eagle:library:206" 42 | UsedLibraryUrn="urn:adsk.eagle:library:207" 43 | UsedLibraryUrn="urn:adsk.eagle:library:91" 44 | UsedLibraryUrn="urn:adsk.eagle:library:208" 45 | UsedLibraryUrn="urn:adsk.eagle:library:209" 46 | UsedLibraryUrn="urn:adsk.eagle:library:210" 47 | UsedLibraryUrn="urn:adsk.eagle:library:211" 48 | UsedLibraryUrn="urn:adsk.eagle:library:212" 49 | UsedLibraryUrn="urn:adsk.eagle:library:213" 50 | UsedLibraryUrn="urn:adsk.eagle:library:214" 51 | UsedLibraryUrn="urn:adsk.eagle:library:215" 52 | UsedLibraryUrn="urn:adsk.eagle:library:216" 53 | UsedLibraryUrn="urn:adsk.eagle:library:217" 54 | UsedLibraryUrn="urn:adsk.eagle:library:92" 55 | UsedLibraryUrn="urn:adsk.eagle:library:218" 56 | UsedLibraryUrn="urn:adsk.eagle:library:219" 57 | UsedLibraryUrn="urn:adsk.eagle:library:220" 58 | UsedLibraryUrn="urn:adsk.eagle:library:221" 59 | UsedLibraryUrn="urn:adsk.eagle:library:222" 60 | UsedLibraryUrn="urn:adsk.eagle:library:223" 61 | UsedLibraryUrn="urn:adsk.eagle:library:224" 62 | UsedLibraryUrn="urn:adsk.eagle:library:225" 63 | UsedLibraryUrn="urn:adsk.eagle:library:226" 64 | UsedLibraryUrn="urn:adsk.eagle:library:227" 65 | UsedLibraryUrn="urn:adsk.eagle:library:93" 66 | UsedLibraryUrn="urn:adsk.eagle:library:228" 67 | UsedLibraryUrn="urn:adsk.eagle:library:229" 68 | UsedLibraryUrn="urn:adsk.eagle:library:230" 69 | UsedLibraryUrn="urn:adsk.eagle:library:231" 70 | UsedLibraryUrn="urn:adsk.eagle:library:232" 71 | UsedLibraryUrn="urn:adsk.eagle:library:233" 72 | UsedLibraryUrn="urn:adsk.eagle:library:234" 73 | UsedLibraryUrn="urn:adsk.eagle:library:235" 74 | UsedLibraryUrn="urn:adsk.eagle:library:236" 75 | UsedLibraryUrn="urn:adsk.eagle:library:237" 76 | UsedLibraryUrn="urn:adsk.eagle:library:94" 77 | UsedLibraryUrn="urn:adsk.eagle:library:238" 78 | UsedLibraryUrn="urn:adsk.eagle:library:239" 79 | UsedLibraryUrn="urn:adsk.eagle:library:240" 80 | UsedLibraryUrn="urn:adsk.eagle:library:241" 81 | UsedLibraryUrn="urn:adsk.eagle:library:242" 82 | UsedLibraryUrn="urn:adsk.eagle:library:243" 83 | UsedLibraryUrn="urn:adsk.eagle:library:244" 84 | UsedLibraryUrn="urn:adsk.eagle:library:245" 85 | UsedLibraryUrn="urn:adsk.eagle:library:246" 86 | UsedLibraryUrn="urn:adsk.eagle:library:247" 87 | UsedLibraryUrn="urn:adsk.eagle:library:95" 88 | UsedLibraryUrn="urn:adsk.eagle:library:248" 89 | UsedLibraryUrn="urn:adsk.eagle:library:249" 90 | UsedLibraryUrn="urn:adsk.eagle:library:250" 91 | UsedLibraryUrn="urn:adsk.eagle:library:251" 92 | UsedLibraryUrn="urn:adsk.eagle:library:252" 93 | UsedLibraryUrn="urn:adsk.eagle:library:253" 94 | UsedLibraryUrn="urn:adsk.eagle:library:254" 95 | UsedLibraryUrn="urn:adsk.eagle:library:255" 96 | UsedLibraryUrn="urn:adsk.eagle:library:256" 97 | UsedLibraryUrn="urn:adsk.eagle:library:257" 98 | UsedLibraryUrn="urn:adsk.eagle:library:96" 99 | UsedLibraryUrn="urn:adsk.eagle:library:258" 100 | UsedLibraryUrn="urn:adsk.eagle:library:259" 101 | UsedLibraryUrn="urn:adsk.eagle:library:260" 102 | UsedLibraryUrn="urn:adsk.eagle:library:261" 103 | UsedLibraryUrn="urn:adsk.eagle:library:262" 104 | UsedLibraryUrn="urn:adsk.eagle:library:263" 105 | UsedLibraryUrn="urn:adsk.eagle:library:264" 106 | UsedLibraryUrn="urn:adsk.eagle:library:265" 107 | UsedLibraryUrn="urn:adsk.eagle:library:266" 108 | UsedLibraryUrn="urn:adsk.eagle:library:267" 109 | UsedLibraryUrn="urn:adsk.eagle:library:97" 110 | UsedLibraryUrn="urn:adsk.eagle:library:268" 111 | UsedLibraryUrn="urn:adsk.eagle:library:269" 112 | UsedLibraryUrn="urn:adsk.eagle:library:270" 113 | UsedLibraryUrn="urn:adsk.eagle:library:271" 114 | UsedLibraryUrn="urn:adsk.eagle:library:272" 115 | UsedLibraryUrn="urn:adsk.eagle:library:273" 116 | UsedLibraryUrn="urn:adsk.eagle:library:274" 117 | UsedLibraryUrn="urn:adsk.eagle:library:275" 118 | UsedLibraryUrn="urn:adsk.eagle:library:276" 119 | UsedLibraryUrn="urn:adsk.eagle:library:277" 120 | UsedLibraryUrn="urn:adsk.eagle:library:80" 121 | UsedLibraryUrn="urn:adsk.eagle:library:98" 122 | UsedLibraryUrn="urn:adsk.eagle:library:278" 123 | UsedLibraryUrn="urn:adsk.eagle:library:279" 124 | UsedLibraryUrn="urn:adsk.eagle:library:280" 125 | UsedLibraryUrn="urn:adsk.eagle:library:281" 126 | UsedLibraryUrn="urn:adsk.eagle:library:282" 127 | UsedLibraryUrn="urn:adsk.eagle:library:283" 128 | UsedLibraryUrn="urn:adsk.eagle:library:284" 129 | UsedLibraryUrn="urn:adsk.eagle:library:285" 130 | UsedLibraryUrn="urn:adsk.eagle:library:286" 131 | UsedLibraryUrn="urn:adsk.eagle:library:287" 132 | UsedLibraryUrn="urn:adsk.eagle:library:99" 133 | UsedLibraryUrn="urn:adsk.eagle:library:288" 134 | UsedLibraryUrn="urn:adsk.eagle:library:289" 135 | UsedLibraryUrn="urn:adsk.eagle:library:290" 136 | UsedLibraryUrn="urn:adsk.eagle:library:291" 137 | UsedLibraryUrn="urn:adsk.eagle:library:292" 138 | UsedLibraryUrn="urn:adsk.eagle:library:293" 139 | UsedLibraryUrn="urn:adsk.eagle:library:294" 140 | UsedLibraryUrn="urn:adsk.eagle:library:295" 141 | UsedLibraryUrn="urn:adsk.eagle:library:296" 142 | UsedLibraryUrn="urn:adsk.eagle:library:297" 143 | UsedLibraryUrn="urn:adsk.eagle:library:100" 144 | UsedLibraryUrn="urn:adsk.eagle:library:298" 145 | UsedLibraryUrn="urn:adsk.eagle:library:299" 146 | UsedLibraryUrn="urn:adsk.eagle:library:300" 147 | UsedLibraryUrn="urn:adsk.eagle:library:301" 148 | UsedLibraryUrn="urn:adsk.eagle:library:302" 149 | UsedLibraryUrn="urn:adsk.eagle:library:303" 150 | UsedLibraryUrn="urn:adsk.eagle:library:304" 151 | UsedLibraryUrn="urn:adsk.eagle:library:305" 152 | UsedLibraryUrn="urn:adsk.eagle:library:306" 153 | UsedLibraryUrn="urn:adsk.eagle:library:307" 154 | UsedLibraryUrn="urn:adsk.eagle:library:101" 155 | UsedLibraryUrn="urn:adsk.eagle:library:308" 156 | UsedLibraryUrn="urn:adsk.eagle:library:309" 157 | UsedLibraryUrn="urn:adsk.eagle:library:310" 158 | UsedLibraryUrn="urn:adsk.eagle:library:311" 159 | UsedLibraryUrn="urn:adsk.eagle:library:312" 160 | UsedLibraryUrn="urn:adsk.eagle:library:313" 161 | UsedLibraryUrn="urn:adsk.eagle:library:314" 162 | UsedLibraryUrn="urn:adsk.eagle:library:315" 163 | UsedLibraryUrn="urn:adsk.eagle:library:316" 164 | UsedLibraryUrn="urn:adsk.eagle:library:317" 165 | UsedLibraryUrn="urn:adsk.eagle:library:102" 166 | UsedLibraryUrn="urn:adsk.eagle:library:318" 167 | UsedLibraryUrn="urn:adsk.eagle:library:319" 168 | UsedLibraryUrn="urn:adsk.eagle:library:320" 169 | UsedLibraryUrn="urn:adsk.eagle:library:321" 170 | UsedLibraryUrn="urn:adsk.eagle:library:322" 171 | UsedLibraryUrn="urn:adsk.eagle:library:323" 172 | UsedLibraryUrn="urn:adsk.eagle:library:324" 173 | UsedLibraryUrn="urn:adsk.eagle:library:325" 174 | UsedLibraryUrn="urn:adsk.eagle:library:326" 175 | UsedLibraryUrn="urn:adsk.eagle:library:327" 176 | UsedLibraryUrn="urn:adsk.eagle:library:103" 177 | UsedLibraryUrn="urn:adsk.eagle:library:328" 178 | UsedLibraryUrn="urn:adsk.eagle:library:329" 179 | UsedLibraryUrn="urn:adsk.eagle:library:330" 180 | UsedLibraryUrn="urn:adsk.eagle:library:331" 181 | UsedLibraryUrn="urn:adsk.eagle:library:332" 182 | UsedLibraryUrn="urn:adsk.eagle:library:333" 183 | UsedLibraryUrn="urn:adsk.eagle:library:334" 184 | UsedLibraryUrn="urn:adsk.eagle:library:335" 185 | UsedLibraryUrn="urn:adsk.eagle:library:336" 186 | UsedLibraryUrn="urn:adsk.eagle:library:337" 187 | UsedLibraryUrn="urn:adsk.eagle:library:104" 188 | UsedLibraryUrn="urn:adsk.eagle:library:338" 189 | UsedLibraryUrn="urn:adsk.eagle:library:339" 190 | UsedLibraryUrn="urn:adsk.eagle:library:340" 191 | UsedLibraryUrn="urn:adsk.eagle:library:341" 192 | UsedLibraryUrn="urn:adsk.eagle:library:342" 193 | UsedLibraryUrn="urn:adsk.eagle:library:343" 194 | UsedLibraryUrn="urn:adsk.eagle:library:344" 195 | UsedLibraryUrn="urn:adsk.eagle:library:345" 196 | UsedLibraryUrn="urn:adsk.eagle:library:346" 197 | UsedLibraryUrn="urn:adsk.eagle:library:347" 198 | UsedLibraryUrn="urn:adsk.eagle:library:105" 199 | UsedLibraryUrn="urn:adsk.eagle:library:348" 200 | UsedLibraryUrn="urn:adsk.eagle:library:349" 201 | UsedLibraryUrn="urn:adsk.eagle:library:350" 202 | UsedLibraryUrn="urn:adsk.eagle:library:351" 203 | UsedLibraryUrn="urn:adsk.eagle:library:352" 204 | UsedLibraryUrn="urn:adsk.eagle:library:353" 205 | UsedLibraryUrn="urn:adsk.eagle:library:354" 206 | UsedLibraryUrn="urn:adsk.eagle:library:355" 207 | UsedLibraryUrn="urn:adsk.eagle:library:356" 208 | UsedLibraryUrn="urn:adsk.eagle:library:357" 209 | UsedLibraryUrn="urn:adsk.eagle:library:106" 210 | UsedLibraryUrn="urn:adsk.eagle:library:358" 211 | UsedLibraryUrn="urn:adsk.eagle:library:359" 212 | UsedLibraryUrn="urn:adsk.eagle:library:360" 213 | UsedLibraryUrn="urn:adsk.eagle:library:361" 214 | UsedLibraryUrn="urn:adsk.eagle:library:362" 215 | UsedLibraryUrn="urn:adsk.eagle:library:363" 216 | UsedLibraryUrn="urn:adsk.eagle:library:364" 217 | UsedLibraryUrn="urn:adsk.eagle:library:365" 218 | UsedLibraryUrn="urn:adsk.eagle:library:366" 219 | UsedLibraryUrn="urn:adsk.eagle:library:367" 220 | UsedLibraryUrn="urn:adsk.eagle:library:107" 221 | UsedLibraryUrn="urn:adsk.eagle:library:368" 222 | UsedLibraryUrn="urn:adsk.eagle:library:369" 223 | UsedLibraryUrn="urn:adsk.eagle:library:370" 224 | UsedLibraryUrn="urn:adsk.eagle:library:371" 225 | UsedLibraryUrn="urn:adsk.eagle:library:372" 226 | UsedLibraryUrn="urn:adsk.eagle:library:373" 227 | UsedLibraryUrn="urn:adsk.eagle:library:374" 228 | UsedLibraryUrn="urn:adsk.eagle:library:375" 229 | UsedLibraryUrn="urn:adsk.eagle:library:376" 230 | UsedLibraryUrn="urn:adsk.eagle:library:377" 231 | UsedLibraryUrn="urn:adsk.eagle:library:81" 232 | UsedLibraryUrn="urn:adsk.eagle:library:108" 233 | UsedLibraryUrn="urn:adsk.eagle:library:378" 234 | UsedLibraryUrn="urn:adsk.eagle:library:379" 235 | UsedLibraryUrn="urn:adsk.eagle:library:380" 236 | UsedLibraryUrn="urn:adsk.eagle:library:381" 237 | UsedLibraryUrn="urn:adsk.eagle:library:382" 238 | UsedLibraryUrn="urn:adsk.eagle:library:383" 239 | UsedLibraryUrn="urn:adsk.eagle:library:384" 240 | UsedLibraryUrn="urn:adsk.eagle:library:385" 241 | UsedLibraryUrn="urn:adsk.eagle:library:386" 242 | UsedLibraryUrn="urn:adsk.eagle:library:387" 243 | UsedLibraryUrn="urn:adsk.eagle:library:109" 244 | UsedLibraryUrn="urn:adsk.eagle:library:388" 245 | UsedLibraryUrn="urn:adsk.eagle:library:389" 246 | UsedLibraryUrn="urn:adsk.eagle:library:390" 247 | UsedLibraryUrn="urn:adsk.eagle:library:391" 248 | UsedLibraryUrn="urn:adsk.eagle:library:392" 249 | UsedLibraryUrn="urn:adsk.eagle:library:393" 250 | UsedLibraryUrn="urn:adsk.eagle:library:394" 251 | UsedLibraryUrn="urn:adsk.eagle:library:395" 252 | UsedLibraryUrn="urn:adsk.eagle:library:396" 253 | UsedLibraryUrn="urn:adsk.eagle:library:397" 254 | UsedLibraryUrn="urn:adsk.eagle:library:110" 255 | UsedLibraryUrn="urn:adsk.eagle:library:398" 256 | UsedLibraryUrn="urn:adsk.eagle:library:399" 257 | UsedLibraryUrn="urn:adsk.eagle:library:400" 258 | UsedLibraryUrn="urn:adsk.eagle:library:401" 259 | UsedLibraryUrn="urn:adsk.eagle:library:402" 260 | UsedLibraryUrn="urn:adsk.eagle:library:403" 261 | UsedLibraryUrn="urn:adsk.eagle:library:404" 262 | UsedLibraryUrn="urn:adsk.eagle:library:405" 263 | UsedLibraryUrn="urn:adsk.eagle:library:406" 264 | UsedLibraryUrn="urn:adsk.eagle:library:407" 265 | UsedLibraryUrn="urn:adsk.eagle:library:111" 266 | UsedLibraryUrn="urn:adsk.eagle:library:408" 267 | UsedLibraryUrn="urn:adsk.eagle:library:409" 268 | UsedLibraryUrn="urn:adsk.eagle:library:410" 269 | UsedLibraryUrn="urn:adsk.eagle:library:411" 270 | UsedLibraryUrn="urn:adsk.eagle:library:412" 271 | UsedLibraryUrn="urn:adsk.eagle:library:413" 272 | UsedLibraryUrn="urn:adsk.eagle:library:414" 273 | UsedLibraryUrn="urn:adsk.eagle:library:415" 274 | UsedLibraryUrn="urn:adsk.eagle:library:416" 275 | UsedLibraryUrn="urn:adsk.eagle:library:417" 276 | UsedLibraryUrn="urn:adsk.eagle:library:112" 277 | UsedLibraryUrn="urn:adsk.eagle:library:418" 278 | UsedLibraryUrn="urn:adsk.eagle:library:419" 279 | UsedLibraryUrn="urn:adsk.eagle:library:113" 280 | UsedLibraryUrn="urn:adsk.eagle:library:114" 281 | UsedLibraryUrn="urn:adsk.eagle:library:115" 282 | UsedLibraryUrn="urn:adsk.eagle:library:116" 283 | UsedLibraryUrn="urn:adsk.eagle:library:117" 284 | UsedLibraryUrn="urn:adsk.eagle:library:82" 285 | UsedLibraryUrn="urn:adsk.eagle:library:118" 286 | UsedLibraryUrn="urn:adsk.eagle:library:119" 287 | UsedLibraryUrn="urn:adsk.eagle:library:120" 288 | UsedLibraryUrn="urn:adsk.eagle:library:121" 289 | UsedLibraryUrn="urn:adsk.eagle:library:122" 290 | UsedLibraryUrn="urn:adsk.eagle:library:123" 291 | UsedLibraryUrn="urn:adsk.eagle:library:124" 292 | UsedLibraryUrn="urn:adsk.eagle:library:125" 293 | UsedLibraryUrn="urn:adsk.eagle:library:126" 294 | UsedLibraryUrn="urn:adsk.eagle:library:127" 295 | UsedLibraryUrn="urn:adsk.eagle:library:83" 296 | UsedLibraryUrn="urn:adsk.eagle:library:128" 297 | UsedLibraryUrn="urn:adsk.eagle:library:129" 298 | UsedLibraryUrn="urn:adsk.eagle:library:130" 299 | UsedLibraryUrn="urn:adsk.eagle:library:131" 300 | UsedLibraryUrn="urn:adsk.eagle:library:132" 301 | UsedLibraryUrn="urn:adsk.eagle:library:133" 302 | UsedLibraryUrn="urn:adsk.eagle:library:134" 303 | UsedLibraryUrn="urn:adsk.eagle:library:135" 304 | UsedLibraryUrn="urn:adsk.eagle:library:136" 305 | UsedLibraryUrn="urn:adsk.eagle:library:137" 306 | UsedLibraryUrn="urn:adsk.eagle:library:84" 307 | UsedLibraryUrn="urn:adsk.eagle:library:138" 308 | UsedLibraryUrn="urn:adsk.eagle:library:139" 309 | UsedLibraryUrn="urn:adsk.eagle:library:140" 310 | UsedLibraryUrn="urn:adsk.eagle:library:141" 311 | UsedLibraryUrn="urn:adsk.eagle:library:142" 312 | UsedLibraryUrn="urn:adsk.eagle:library:143" 313 | UsedLibraryUrn="urn:adsk.eagle:library:144" 314 | UsedLibraryUrn="urn:adsk.eagle:library:145" 315 | UsedLibraryUrn="urn:adsk.eagle:library:146" 316 | UsedLibraryUrn="urn:adsk.eagle:library:147" 317 | UsedLibraryUrn="urn:adsk.eagle:library:85" 318 | UsedLibraryUrn="urn:adsk.eagle:library:148" 319 | UsedLibraryUrn="urn:adsk.eagle:library:149" 320 | UsedLibraryUrn="urn:adsk.eagle:library:150" 321 | UsedLibraryUrn="urn:adsk.eagle:library:151" 322 | UsedLibraryUrn="urn:adsk.eagle:library:152" 323 | UsedLibraryUrn="urn:adsk.eagle:library:153" 324 | UsedLibraryUrn="urn:adsk.eagle:library:154" 325 | UsedLibraryUrn="urn:adsk.eagle:library:155" 326 | UsedLibraryUrn="urn:adsk.eagle:library:156" 327 | UsedLibraryUrn="urn:adsk.eagle:library:157" 328 | UsedLibraryUrn="urn:adsk.eagle:library:86" 329 | UsedLibraryUrn="urn:adsk.eagle:library:158" 330 | UsedLibraryUrn="urn:adsk.eagle:library:159" 331 | UsedLibraryUrn="urn:adsk.eagle:library:160" 332 | UsedLibraryUrn="urn:adsk.eagle:library:161" 333 | UsedLibraryUrn="urn:adsk.eagle:library:162" 334 | UsedLibraryUrn="urn:adsk.eagle:library:163" 335 | UsedLibraryUrn="urn:adsk.eagle:library:164" 336 | UsedLibraryUrn="urn:adsk.eagle:library:165" 337 | UsedLibraryUrn="urn:adsk.eagle:library:166" 338 | UsedLibraryUrn="urn:adsk.eagle:library:167" 339 | UsedLibraryUrn="urn:adsk.eagle:library:87" 340 | UsedLibraryUrn="urn:adsk.eagle:library:168" 341 | UsedLibraryUrn="urn:adsk.eagle:library:169" 342 | UsedLibraryUrn="urn:adsk.eagle:library:170" 343 | UsedLibraryUrn="urn:adsk.eagle:library:171" 344 | UsedLibraryUrn="urn:adsk.eagle:library:172" 345 | UsedLibraryUrn="urn:adsk.eagle:library:173" 346 | UsedLibraryUrn="urn:adsk.eagle:library:174" 347 | UsedLibraryUrn="urn:adsk.eagle:library:175" 348 | UsedLibraryUrn="urn:adsk.eagle:library:176" 349 | UsedLibraryUrn="urn:adsk.eagle:library:177" 350 | UsedLibrary="SynchroClock.lbr" 351 | 352 | [Win_1] 353 | Type="Library Editor" 354 | Number=3 355 | File="SynchroClock.lbr" 356 | View="-18.8305 -15.494 12.0233 10.922" 357 | WireWidths=" 0.0762 0.1016 0.15 0.2 0.2032 0.3048 0.4064 0.508 0.6096 0.8128 1.016 1.27 2.54 0.1524 0.254 0.127" 358 | PadDiameters=" 0.254 0.3048 0.4064 0.6096 0.8128 1.016 1.27 1.4224 1.6764 1.778 1.9304 2.1844 2.54 3.81 6.4516 0" 359 | PadDrills=" 0.2 0.25 0.3 0.35 0.4 0.45 0.5 0.55 0.65 0.7 0.75 0.8 0.85 0.9 1 0.6" 360 | ViaDiameters=" 0.55 0.6 0.65 0.7 0.75 0.8 0.85 0.9 0.95 1 1.05 1.1 1.15 1.2 1.3 0" 361 | ViaDrills=" 0.2 0.25 0.3 0.4 0.45 0.5 0.55 0.6 0.65 0.7 0.75 0.8 0.85 0.9 1 0.35" 362 | HoleDrills=" 0.2 0.25 0.3 0.4 0.45 0.5 0.55 0.6 0.65 0.7 0.75 0.8 0.85 0.9 1 0.35" 363 | TextSizes=" 0.254 0.3048 0.4064 0.6096 0.8128 1.016 1.4224 1.6764 1.9304 2.1844 2.54 3.81 5.08 6.4516 1.778 1.27" 364 | PolygonSpacings=" 0.254 0.3048 0.4064 0.6096 0.8128 1.016 1.4224 1.6764 1.778 1.9304 2.1844 2.54 3.81 5.08 6.4516 1.27" 365 | PolygonIsolates=" 0.254 0.3048 0.4064 0.6096 0.8128 1.016 1.27 1.4224 1.6764 1.778 1.9304 2.1844 2.54 3.81 6.4516 0" 366 | MiterRadiuss=" 0.254 0.3175 0.635 1.27 2.54 1 2 2.5 5 7.5 10 0" 367 | DimensionWidths=" 0 0.127 0.254 0.1 0.26 0.13" 368 | DimensionExtWidths=" 0.127 0.254 0.1 0.13 0.26 0" 369 | DimensionExtLengths=" 1.27 2.54 1 2 3 0" 370 | DimensionExtOffsets=" 1.27 2.54 1 2 3 0" 371 | SmdSizes=" 0.3048 0.1524 0.4064 0.2032 0.6096 0.3048 0.8128 0.4064 1.016 0.508 1.27 0.6604 1.4224 0.7112 1.6764 0.8128 1.778 0.9144 1.9304 0.9652 2.1844 1.0668 2.54 1.27 3.81 1.9304 5.08 2.54 6.4516 3.2512 1.27 0.635" 372 | WireBend=0 373 | WireBendSet=0 374 | WireCap=1 375 | MiterStyle=0 376 | PadShape=0 377 | ViaShape=1 378 | PolygonPour=0 379 | PolygonRank=0 380 | PolygonThermals=1 381 | PolygonOrphans=0 382 | TextRatio=8 383 | DimensionUnit=1 384 | DimensionPrecision=2 385 | DimensionShowUnit=0 386 | PinDirection=3 387 | PinFunction=0 388 | PinLength=2 389 | PinVisible=3 390 | SwapLevel=0 391 | ArcDirection=0 392 | AddLevel=2 393 | PadsSameType=0 394 | Layer=21 395 | 396 | [Win_2] 397 | Type="Schematic Editor" 398 | Number=2 399 | File="SynchroClock.sch" 400 | View="-6.11216 -4.84216 285.304 220.534" 401 | WireWidths=" 0.0762 0.1016 0.127 0.15 0.2 0.2032 0.254 0.3048 0.4064 0.508 0.6096 0.8128 1.016 1.27 2.54 0.1524" 402 | PadDiameters=" 0.254 0.3048 0.4064 0.6096 0.8128 1.016 1.27 1.4224 1.6764 1.778 1.9304 2.1844 2.54 3.81 6.4516 0" 403 | PadDrills=" 0.2 0.25 0.3 0.35 0.4 0.45 0.5 0.55 0.65 0.7 0.75 0.8 0.85 0.9 1 0.6" 404 | ViaDiameters=" 0.55 0.6 0.65 0.7 0.75 0.8 0.85 0.9 0.95 1 1.05 1.1 1.15 1.2 1.3 0" 405 | ViaDrills=" 0.2 0.25 0.3 0.4 0.45 0.5 0.55 0.6 0.65 0.7 0.75 0.8 0.85 0.9 1 0.35" 406 | HoleDrills=" 0.2 0.25 0.3 0.4 0.45 0.5 0.55 0.6 0.65 0.7 0.75 0.8 0.85 0.9 1 0.35" 407 | TextSizes=" 0.254 0.3048 0.4064 0.6096 0.8128 1.016 1.27 1.4224 1.6764 1.9304 2.1844 2.54 3.81 5.08 6.4516 1.778" 408 | PolygonSpacings=" 0.254 0.3048 0.4064 0.6096 0.8128 1.016 1.4224 1.6764 1.778 1.9304 2.1844 2.54 3.81 5.08 6.4516 1.27" 409 | PolygonIsolates=" 0.254 0.3048 0.4064 0.6096 0.8128 1.016 1.27 1.4224 1.6764 1.778 1.9304 2.1844 2.54 3.81 6.4516 0" 410 | MiterRadiuss=" 0.254 0.3175 0.635 1.27 2.54 1 2 2.5 5 7.5 10 0" 411 | DimensionWidths=" 0 0.127 0.254 0.1 0.26 0.13" 412 | DimensionExtWidths=" 0.127 0.254 0.1 0.13 0.26 0" 413 | DimensionExtLengths=" 1.27 2.54 1 2 3 0" 414 | DimensionExtOffsets=" 1.27 2.54 1 2 3 0" 415 | SmdSizes=" 0.3048 0.1524 0.4064 0.2032 0.6096 0.3048 0.8128 0.4064 1.016 0.508 1.27 0.6604 1.4224 0.7112 1.6764 0.8128 1.778 0.9144 1.9304 0.9652 2.1844 1.0668 2.54 1.27 3.81 1.9304 5.08 2.54 6.4516 3.2512 1.27 0.635" 416 | WireBend=0 417 | WireBendSet=31 418 | WireCap=1 419 | MiterStyle=0 420 | PadShape=0 421 | ViaShape=1 422 | PolygonPour=0 423 | PolygonRank=0 424 | PolygonThermals=1 425 | PolygonOrphans=0 426 | TextRatio=8 427 | DimensionUnit=1 428 | DimensionPrecision=2 429 | DimensionShowUnit=0 430 | PinDirection=3 431 | PinFunction=0 432 | PinLength=2 433 | PinVisible=3 434 | SwapLevel=0 435 | ArcDirection=0 436 | AddLevel=2 437 | PadsSameType=0 438 | Layer=91 439 | Views=" 1: -6.11216 -4.84216 285.304 220.534" 440 | Sheet="1" 441 | 442 | [Win_3] 443 | Type="Board Editor" 444 | Number=1 445 | File="SynchroClock.brd" 446 | View="-0.887263 -2.21983 86.7743 47.1492" 447 | WireWidths=" 0.0762 0.1016 0.127 0.15 0.2 0.2032 0.254 0.3048 0.4064 0.6096 0.8128 1.016 1.27 2.54 0.508 0.1524" 448 | PadDiameters=" 0.254 0.3048 0.4064 0.6096 0.8128 1.016 1.27 1.4224 1.6764 1.778 1.9304 2.1844 2.54 3.81 6.4516 0" 449 | PadDrills=" 0.2 0.25 0.3 0.35 0.4 0.45 0.5 0.55 0.65 0.7 0.75 0.8 0.85 0.9 1 0.6" 450 | ViaDiameters=" 0.55 0.6 0.65 0.7 0.75 0.8 0.85 0.9 0.95 1 1.05 1.1 1.15 1.2 1.3 0" 451 | ViaDrills=" 0.2 0.25 0.3 0.4 0.45 0.5 0.55 0.6 0.65 0.7 0.75 0.8 0.85 0.9 1 0.35" 452 | HoleDrills=" 0.2 0.25 0.3 0.4 0.45 0.5 0.55 0.6 0.65 0.7 0.75 0.8 0.85 0.9 1 0.35" 453 | TextSizes=" 0.254 0.3048 0.4064 0.6096 0.8128 1.016 1.27 1.4224 1.6764 1.9304 2.1844 2.54 3.81 5.08 6.4516 1.778" 454 | PolygonSpacings=" 0.254 0.3048 0.4064 0.6096 0.8128 1.016 1.4224 1.6764 1.778 1.9304 2.1844 2.54 3.81 5.08 6.4516 1.27" 455 | PolygonIsolates=" 0.254 0.3048 0.4064 0.6096 0.8128 1.016 1.27 1.4224 1.6764 1.778 1.9304 2.1844 2.54 3.81 6.4516 0" 456 | MiterRadiuss=" 0.254 0.3175 0.635 1.27 2.54 1 2 2.5 5 7.5 10 0" 457 | DimensionWidths=" 0 0.127 0.254 0.1 0.26 0.13" 458 | DimensionExtWidths=" 0.127 0.254 0.1 0.13 0.26 0" 459 | DimensionExtLengths=" 1.27 2.54 1 2 3 0" 460 | DimensionExtOffsets=" 1.27 2.54 1 2 3 0" 461 | SmdSizes=" 0.3048 0.1524 0.4064 0.2032 0.6096 0.3048 0.8128 0.4064 1.016 0.508 1.27 0.6604 1.4224 0.7112 1.6764 0.8128 1.778 0.9144 1.9304 0.9652 2.1844 1.0668 2.54 1.27 3.81 1.9304 5.08 2.54 6.4516 3.2512 1.27 0.635" 462 | WireBend=1 463 | WireBendSet=0 464 | WireCap=1 465 | MiterStyle=0 466 | PadShape=0 467 | ViaShape=1 468 | PolygonPour=0 469 | PolygonRank=1 470 | PolygonThermals=1 471 | PolygonOrphans=0 472 | TextRatio=8 473 | DimensionUnit=1 474 | DimensionPrecision=2 475 | DimensionShowUnit=0 476 | PinDirection=3 477 | PinFunction=0 478 | PinLength=2 479 | PinVisible=3 480 | SwapLevel=0 481 | ArcDirection=0 482 | AddLevel=2 483 | PadsSameType=0 484 | Layer=1 485 | 486 | [Win_4] 487 | Type="Control Panel" 488 | Number=0 489 | 490 | [Desktop] 491 | Screen="4480 1440" 492 | Window="Win_1" 493 | Window="Win_2" 494 | Window="Win_3" 495 | Window="Win_4" 496 | -------------------------------------------------------------------------------- /images/SynchroClock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liebman/AnalogClock/a207802240f1244dee30748a2ee371fe407f0ef4/images/SynchroClock.png --------------------------------------------------------------------------------