package main import ( "fmt" "log" "sync" "time" ) var keepDoorOpenLock sync.Mutex // keepDoorOpenGen identifies the currently armed hold timer. It is bumped (under // keepDoorOpenLock) every time a new timer is installed so that a superseded // timer's callback can recognise it is stale and do nothing. var keepDoorOpenGen uint64 func updateKeepOpenDoor(newKeepOpenTime time.Time) { keepDoorOpenLock.Lock() defer keepDoorOpenLock.Unlock() // Hold unchanged since the last poll: keep the existing timer rather than // rebuilding it every poll, which would float the close time forward by up // to one poll interval and re-pulse OpenDoor needlessly. if keepDoorOpen.timer != nil && newKeepOpenTime.Equal(keepDoorOpen.until) { return } // is there one active? if keepDoorOpen.timer != nil { keepDoorOpen.timer.Stop() keepDoorOpen = KeepDoorOpen{} } if newKeepOpenTime.After(time.Now()) { log.Printf("Keeping door open until %v", newKeepOpenTime) if err := OpenDoor(wiegand); err != nil { // Don't commit the hold if the relay didn't actually open: leaving // keepDoorOpen empty (timer nil) means the next poll retries instead // of latching the Equal early-return on a door that never opened. log.Printf("ERROR opening door for hold: %v", err) return } keepDoorOpenGen++ gen := keepDoorOpenGen timer := time.AfterFunc(time.Until(newKeepOpenTime), func() { handleKeepDoorOpenCloseCleanup(gen) }) keepDoorOpen = KeepDoorOpen{ timer: timer, until: newKeepOpenTime, } } else { if err := CloseDoor(wiegand); err != nil { log.Printf("ERROR closing door: %v", err) } } } func cancelKeepOpenDoor() { keepDoorOpenLock.Lock() defer keepDoorOpenLock.Unlock() if keepDoorOpen.timer == nil { return } keepDoorOpen.timer.Stop() if err := CloseDoor(wiegand); err != nil { // Keep keepDoorOpen non-nil so the next poll's cancel retries the close; // clearing it now would strand the door OPEN with no retry path. log.Printf("ERROR closing door on hold cancel: %v", err) return } keepDoorOpen = KeepDoorOpen{} } func handleKeepDoorOpenCloseCleanup(gen uint64) { keepDoorOpenLock.Lock() defer keepDoorOpenLock.Unlock() // Timer.Stop() can return after this callback has already started and is // blocked here on the lock; by the time we acquire it, updateKeepOpenDoor // may have installed a newer hold. Only act if we are still the current // generation, otherwise we would close the door and clear a live hold. if gen != keepDoorOpenGen { return } fmt.Println("Keep door open time is reached!") if err := CloseDoor(wiegand); err != nil { // Leave keepDoorOpen intact so the next poll (which will see // keep_open_until=null and call cancelKeepOpenDoor) retries the close // instead of stranding the door OPEN. log.Printf("ERROR closing door at hold expiry: %v", err) return } keepDoorOpen = KeepDoorOpen{} }