package main import ( "bufio" "bytes" "context" "encoding/json" "fmt" "github.com/joho/godotenv" "io" "log" "net/http" "os" "os/signal" "strconv" "strings" "sync" "syscall" "time" "godoor/hash" ) const wiegand_a_default = 17 const wiegand_b_default = 18 const wiegand_bit_timeout = time.Millisecond * 8 const solenoid_default = 21 type card struct { UidHash string `json:"uid_hash"` } type cardList struct { AllowedUids []struct { Token card `json:"token"` } `json:"allowed_uids"` KeepOpenUntil *time.Time `json:"keep_open_until,omitempty"` } type ValidUids map[string]bool // bool has no meaning type Config struct { door string uidSalt string doorOpenTime int mock bool api struct { allowed string longpoll string swipe string key string } pins struct { wiegandA int wiegandB int solenoid int } } type KeepDoorOpen struct { until time.Time timer *time.Timer } type OpenedTimestamp struct { Opened time.Time Closed *time.Time } var config Config var globalLock sync.Mutex var validUids ValidUids var wiegand Wiegand var keepDoorOpen KeepDoorOpen var lastSyncedTimestamp *time.Time var openDoorTimestamps []OpenedTimestamp func main() { ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer cancel() loadConfig() go func() { setup() }() <-ctx.Done() log.Printf("Cleanup\n") // cleanup } func loadConfig() { var err error log.Println("Loading .env config") err = godotenv.Load() if err != nil { log.Println("Failed to load .env config, using internal defaults") } config.door = os.Getenv("KDOORPI_DOOR") config.api.allowed = os.Getenv("KDOORPI_API_ALLOWED") config.api.longpoll = os.Getenv("KDOORPI_API_LONGPOLL") config.api.swipe = os.Getenv("KDOORPI_API_SWIPE") config.api.key = os.Getenv("KDOORPI_API_KEY") config.uidSalt = os.Getenv("KDOORPI_UID_SALT") config.doorOpenTime, err = strconv.Atoi(os.Getenv("KDOORPI_OPEN_TIME")) if err != nil { config.doorOpenTime = 3 } _, config.mock = os.LookupEnv("KDOORPI_MOCK_HW") config.pins.wiegandA, err = strconv.Atoi(os.Getenv("KDOORPI_PIN_WIEGAND_A")) if err != nil { config.pins.wiegandA = wiegand_a_default } config.pins.wiegandB, err = strconv.Atoi(os.Getenv("KDOORPI_PIN_WIEGAND_B")) if err != nil { config.pins.wiegandB = wiegand_b_default } config.pins.solenoid, err = strconv.Atoi(os.Getenv("KDOORPI_PIN_SOLENOID")) if err != nil { config.pins.solenoid = solenoid_default } } func setup() { log.Println("Started Setup") if config.mock { log.Println("MOCK mode enabled") if config.door == "" { config.door = "mockdoor" } wiegand = &WiegandMock{} } else { wiegand = WiegandSetup(config.pins.wiegandA, config.pins.wiegandB, wiegand_bit_timeout, config.pins.solenoid) } log.Println("HW Setup done") http.DefaultClient.Timeout = 120 * time.Second go func() { for { err := waitEvents() if err != nil { log.Printf("LongPoll for events failed: %v", err) log.Println("Will try to LongPoll again in 120 seconds") time.Sleep(120 * time.Second) go reloadTokens() } time.Sleep(1 * time.Second) } }() log.Println("Initialized longpoll event loop") for { log.Println("Start initial token population") err := reloadTokens() if err == nil { break } log.Printf("Initial token population failed. err: %v", err) log.Println("Retrying in 10 seconds...") time.Sleep(10 * time.Second) } go runHttpServer() log.Println("Initial token population success") go cardRunner(wiegand) log.Println("Setup completed") } func runHttpServer() { http.HandleFunc("/lastsync", func(w http.ResponseWriter, r *http.Request) { e := json.NewEncoder(w) e.Encode(map[string]any{ "last_synced": lastSyncedTimestamp, }) }) http.HandleFunc("/opened", func(w http.ResponseWriter, r *http.Request) { e := json.NewEncoder(w) e.Encode(map[string]any{ "open_timestamps": openDoorTimestamps, }) }) http.HandleFunc("/isopen", func(w http.ResponseWriter, r *http.Request) { e := json.NewEncoder(w) open, _ := wiegand.IsDoorOpen() e.Encode(map[string]any{ "open": open, }) }) http.ListenAndServe(":3334", nil) } func OpenAndCloseDoor(w Wiegand) error { err := OpenDoor(w) if err != nil { return err } if keepDoorOpen.until.After(time.Now()) { fmt.Println("Door is already open") return nil } fmt.Println("Door is now open") time.Sleep(time.Duration(config.doorOpenTime) * time.Second) err = CloseDoor(w) if err != nil { return err } fmt.Println("Door is now closed") return nil } func OpenDoor(w Wiegand) error { open, _ := w.IsDoorOpen() if open { return nil } w.OpenDoor() openDoorTimestamps = append(openDoorTimestamps, OpenedTimestamp{Opened: time.Now(), Closed: nil}) return nil } func CloseDoor(w Wiegand) error { open, _ := w.IsDoorOpen() if !open { return nil } w.CloseDoor() t := time.Now() openDoorTimestamps[len(openDoorTimestamps)-1].Closed = &t return nil } func cardRunner(w Wiegand) { for { card, err := w.GetCardUid() if err != nil { continue } printCardId(card) hashedHex := hash.HashCardUid(card) log.Println(hashedHex) globalLock.Lock() ok := validUids[hashedHex] globalLock.Unlock() go func() { err := sendSwipeEvent(hashedHex, ok) if err != nil { log.Println("Failed to send swipe event: %v", err) } }() if ok { log.Println("Opening door") err := OpenAndCloseDoor(w) if err != nil { log.Println("There was an error opening and closing the Door") } } else { log.Println("Unknown card") } } } func ParseNextMessage(r *bufio.Reader) (string, error) { var message string for { s, err := r.ReadString('\n') if err != nil { return message, err } message = message + s nextBytes, err := r.ReadByte() if err != nil { return message, err } if nextBytes == '\n' { return message, nil } r.UnreadByte() } } func waitEvents() error { req, err := http.NewRequest(http.MethodGet, config.api.longpoll, nil) if err != nil { return err } req.Header.Add("KEY", config.api.key) resp, err := http.DefaultClient.Do(req) if err != nil { return err } log.Printf("%v\n", resp) reader := bufio.NewReader(resp.Body) for { msg, err := ParseNextMessage(reader) if err != nil { return err } for _, line := range strings.Split(msg, "\n") { data, found_data := strings.CutPrefix(line, "data:") if !found_data { continue } log.Printf("got server data: %q\n", data) if strings.TrimSpace(data) == config.door { err := OpenAndCloseDoor(wiegand) if err != nil { log.Println("There was an error opening and closing the Door") } } } } log.Printf("%v\n", resp) return nil } func reloadTokens() error { req, err := http.NewRequest(http.MethodGet, config.api.allowed, nil) if err != nil { return err } req.Header.Add("KEY", config.api.key) resp, err := http.DefaultClient.Do(req) if err != nil { return err } if resp.StatusCode != 200 { log.Printf("%v\n", resp) } var cl cardList body, err := io.ReadAll(resp.Body) if err != nil { return err } err = json.Unmarshal(body, &cl) if err != nil { return err } globalLock.Lock() defer globalLock.Unlock() validUids = make(ValidUids) var totalCardCount int = 0 for i, val := range cl.AllowedUids { //log.Printf("%d: %+v\n", i, val.Token.UidHash) validUids[val.Token.UidHash] = true totalCardCount = i } log.Printf("Got %d cards from server", totalCardCount) if cl.KeepOpenUntil != nil { updateKeepOpenDoor(*cl.KeepOpenUntil) } t := time.Now() lastSyncedTimestamp = &t return nil } func updateKeepOpenDoor(newKeepOpenTime time.Time) { // 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) OpenDoor(wiegand) timer := time.AfterFunc(time.Until(newKeepOpenTime), handleKeepDoorOpenCloseCleanup) keepDoorOpen = KeepDoorOpen{ timer: timer, until: newKeepOpenTime, } } else { CloseDoor(wiegand) } } func handleKeepDoorOpenCloseCleanup() { fmt.Println("Keep door open time is reached!") CloseDoor(wiegand) keepDoorOpen = KeepDoorOpen{} } func sendSwipeEvent(cardUidHash string, success bool) error { swipeEvent := map[string]string{ "uid_hash": cardUidHash, "door": config.door, "timestamp": time.Now().Format(time.DateTime), "success": fmt.Sprint(success), } data, err := json.Marshal(swipeEvent) if err != nil { return err } req, err := http.NewRequest(http.MethodPost, config.api.swipe, bytes.NewReader(data)) if err != nil { return err } req.Header.Add("KEY", config.api.key) resp, err := http.DefaultClient.Do(req) if err != nil { return err } if !(resp.StatusCode >= 200 && resp.StatusCode < 300) { return fmt.Errorf("Server responded with %d", resp.StatusCode) } return nil }