Umbau einer defekten Copal Flip-Clock auf NTP


Bild zu Post 'Umbau einer defekten Copal Flip-Clock auf NTP'

Bei dieser schönen 60er-Jahre Uhr war der Synchronmotor defekt (lief nur selten an, Versatz von 2h pro Tag). Ein Ersatz für diesen war nicht zu finden, daher die High-Tech-Variante.

Herausforderungen

Lösungen

Schrittmotor

AliExpress sei Dank: Stepper-Motor mit 12 Zähnen und Modul 0,25.

StepStick

Natürlich gibt es was von Trinamic auf Basis des TMC2225. Zum Beispiel hier.

Platine produzieren

Millimetergenaues Ausmessen, KiCad, Aisler:

Bild zu Post 'Umbau einer defekten Copal Flip-Clock auf NTP'
Bild zu Post 'Umbau einer defekten Copal Flip-Clock auf NTP'

Schritte zählen

Aufgrund des doch sehr beschränkten Platzes, blieb nur die Möglichkeit übrig, die bestehende Glimmmlampe auszubauen und stattdessen eine kleine Reflex-Lichtschranke (TCRT-5000) anzubringen. Diese löst recht zuverlässig auf, wenn sich ein Blatt der Minuten-Anzeige im Fallen befindet.

Bild zu Post 'Umbau einer defekten Copal Flip-Clock auf NTP'

Spontane Resets

Ein 100uF Kondensator auf der 5V Schiene wirkt Wunder:

Bild zu Post 'Umbau einer defekten Copal Flip-Clock auf NTP'

Fertiger Aufbau

Bild zu Post 'Umbau einer defekten Copal Flip-Clock auf NTP'
Bild zu Post 'Umbau einer defekten Copal Flip-Clock auf NTP'
Bild zu Post 'Umbau einer defekten Copal Flip-Clock auf NTP'
Bild zu Post 'Umbau einer defekten Copal Flip-Clock auf NTP'

Zusammenbau

Bild zu Post 'Umbau einer defekten Copal Flip-Clock auf NTP'

Software

Kleines C++ Projekt mit platformio auf Basis der Bibliotheken: WiFiManager, AceTime, ESP_DoubleResetDetector.

Lösung ist straight-forward: Intern wird die vermutlich angezeigte Zeit (durch das Zählen via IR-Reflex-Lichtschranke) mit der via NTP empfangenen Zeit verglichen und solange der Stepper-Motor aktiviert, bis diese übereinstimmen.

Dabei eine Sonderbehandlung für kleine Abweichungen (bis 10 Minuten wird einfach gewartet, bis die Zeit übereinstimmt) und in der Nacht (z.B. Bei der Sommer-/Winterzeit-Umstellung müssten 23 Stunden nach vorne gestellt werden).

Über eine minimale Web-Oberfläche kann bei Stromausfall oder ähnlichem die tatsächlich angezeigte Uhrzeit angegeben werden.


void calculateBaseline()
{
    int base = 0;
    for (int x = 0; x < 10; x++) {
        base = base + analogRead(A0);
        delayMicroseconds(10);
    }
    irPhotoDiodeBaseLine = base / 10;
}

void advance()
{
    int16_t minValue = 1024;
    int16_t maxValue = 0;
    bool minuteDisplayFlipped = false;
    uint16_t triggerCount = 0;
    uint16_t minimumAt = 0;
    uint16_t maximumAt = 0;
    upTriggeredAt = -1;
    downTriggeredAt = -1;
    int16_t currentIrReading = 0;

    digitalWrite(ENABLE, LOW);
    digitalWrite(DIR, HIGH);
    calculateBaseline();
    maxValue = 0;
    minValue = 1024;

    irValues.clear();
    irValues.resize(0);

    for (uint16_t stepperMotorSteps = 0; stepperMotorSteps < 60; stepperMotorSteps++) {
        if (!minuteDisplayFlipped) {
            digitalWrite(STEP, HIGH);
            delayMicroseconds(1);
            digitalWrite(STEP, LOW);
        }
        for (int count = 0; count < 30; count++) {
            currentIrReading = analogRead(A0);
            irValues.push_back(currentIrReading);
            if (currentIrReading < minValue) {
                minimumAt = stepperMotorSteps;
                minValue = currentIrReading;
            }
            if (currentIrReading > maxValue) {
                maximumAt = stepperMotorSteps;
                maxValue = currentIrReading;
            }
            int historyElements = irValues.size();
            if (historyElements > 300) {
                // small rising edge if at least 60% of highest swing so far but also at least 10 ticks high
                bool smallUp = downTriggeredAt == -1 && // no falling edge detected so far
                    stepperMotorSteps > 20 && // at least some steps already done
                    10 * (currentIrReading - irValues[historyElements - 50]) > 6 * max(10, maxValue - minValue);
                bool up = smallUp || currentIrReading - irValues[historyElements - 100] > 50;
                bool down = irValues[historyElements - 100] - currentIrReading > 50;
                if (up || down) {
                    triggerCount++;
                    if (down && downTriggeredAt == -1) {
                        downTriggeredAt = historyElements;
                    }
                    if (up && upTriggeredAt == -1) {
                        upTriggeredAt = historyElements;
                    }
                    minuteDisplayFlipped = true;
                }
            }
            delayMicroseconds(100);
        }
    }

    if (triggerCount <= 5) {
        globalStats.skipped++;
    } else {
        currentDisplayedTime++;
    }

    for (std::vector<int16_t>::iterator it = irValues.begin(); it != irValues.end(); it++) {
        *it = *it - minValue;
    }

    if (currentDisplayedTime >= 1440) {
        currentDisplayedTime -= 1440;
    }
#ifdef DEBUG
    time_t localTime = time(nullptr);
    ZonedDateTime zonedDateTime = ZonedDateTime::forUnixSeconds(
        localTime, localZone);
    ace_common::PrintStr<64> currentTimeStr;
    zonedDateTime.printTo(currentTimeStr);

    logger.printf("%s - %02d:%02d - triggerCount=%d up=%d down=%d val=%d min=%d@%d max=%d@%d base=%d\n",
        currentTimeStr.getCstr(),
        (((1440 + currentDisplayedTime) / 60) % 24),
        (1440 + currentDisplayedTime) % 60,
        triggerCount, upTriggeredAt, downTriggeredAt, currentIrReading, minValue, minimumAt, maxValue, maximumAt, irPhotoDiodeBaseLine);
#endif
}

template <int T>
void runEvery(void (*f)())
{
    static unsigned long lastMillis = millis();
    unsigned long now = millis();
    if (now >= lastMillis + T) {
        // run every T milli-seconds
        f();
        lastMillis = millis();
    }
}

void loop()
{
    // WebServer should react..
    server.handleClient();
#ifdef OTA
    // OTA the same
    ArduinoOTA.handle();
#endif

    // The main time-handling code, wait at least 1 second between flips
    runEvery<1000>([]() {
        if (currentDisplayedTime < currentTime) {
            advance();
        } else if (currentDisplayedTime == currentTime) {
            digitalWrite(ENABLE, HIGH);
            fastMode = false;
        } else { // currentDisplayedTime > currentTime
            if (currentTime >= 1 * 60 && currentTime <= 6 * 60) {
                // do nothing in the night
                fastMode = false;
            } else if (currentDisplayedTime > currentTime + 10) {
                fastMode = true;
                currentDisplayedTime -= 1440;
            }
        }
    });

    // every half second -> housekeeping of time and name
    runEvery<500>([]() {
        globalStats.uptimeSeconds = (millis() / 1000);
        globalStats.uptimeSecondsTotal = globalStats.previousSecondsTotal + globalStats.uptimeSeconds;
        globalStats.skippedTotal = globalStats.previousSkippedTotal + globalStats.skipped;
        setCurrentTime();
        drd.loop();
        MDNS.update();
    });

    // every 15 minutes -> store stats
    runEvery<1000 * 15 * 60>([]() {
        EEPROM.put(STATS_ADDRESS, globalStats);
        EEPROM.commit();
    });
}
Kommentare per Mail an post@wolfgang-jung.net.