From 75035e2751c109821f9db3d325821ae6f5b23397 Mon Sep 17 00:00:00 2001 From: "kayos@tcp.direct" Date: Sat, 23 Dec 2023 16:36:41 -0800 Subject: [PATCH] Successful RFID implementation --- go.mod | 11 +- go.sum | 41 ++-- main.go | 152 +++++++------- pkg/access/rfid.go | 379 ++++++++++++++++++++++++++++++++++ pkg/access/rfid_test.go | 122 +++++++++++ pkg/access/timeseries.go | 60 ++++++ pkg/config/toml.go | 42 ++++ pkg/iot/types.go | 32 +++ {logger => pkg/logger}/log.go | 5 +- 9 files changed, 739 insertions(+), 105 deletions(-) create mode 100644 pkg/access/rfid.go create mode 100644 pkg/access/rfid_test.go create mode 100644 pkg/access/timeseries.go create mode 100644 pkg/config/toml.go create mode 100644 pkg/iot/types.go rename {logger => pkg/logger}/log.go (84%) diff --git a/go.mod b/go.mod index 6a4a30e..2217924 100644 --- a/go.mod +++ b/go.mod @@ -4,15 +4,18 @@ go 1.21.4 require ( git.tcp.direct/kayos/zwrap v0.4.2 - github.com/cretz/bine v0.2.0 - github.com/davecgh/go-spew v1.1.0 + github.com/clausecker/nfc/v2 v2.1.4 + github.com/davecgh/go-spew v1.1.1 + github.com/pelletier/go-toml/v2 v2.1.0 github.com/rs/zerolog v1.31.0 - golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a + github.com/stianeikeland/go-rpio/v4 v4.6.0 + periph.io/x/conn/v3 v3.7.0 + periph.io/x/devices/v3 v3.7.1 + periph.io/x/host/v3 v3.8.0 ) require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect - golang.org/x/net v0.0.0-20210525063256-abc453219eb5 // indirect golang.org/x/sys v0.12.0 // indirect ) diff --git a/go.sum b/go.sum index c5fcf3b..849d653 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,14 @@ git.tcp.direct/kayos/zwrap v0.4.2 h1:yXO21VNkAb+iMi3dOAythw42dvv1bVzXw+TJJXENVdQ= git.tcp.direct/kayos/zwrap v0.4.2/go.mod h1:SA9+Sww1LBKMw54Gjot5t4AhwEgRX1lFhy6pv5Mm78Q= +github.com/clausecker/nfc/v2 v2.1.4 h1:zw2Cnny7pxPnuxVMBo+DXqXYETzUN7pMhNEA61yT5gY= +github.com/clausecker/nfc/v2 v2.1.4/go.mod h1:BjRBQUQTQmiwh2tEfQ+xBM5xY05sV2gnZ0JRYEHog/o= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo= -github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI= -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/jonboulle/clockwork v0.3.0 h1:9BSCMi8C+0qdApAp4auwX0RkLGUjs956h0EkuQymUhg= +github.com/jonboulle/clockwork v0.3.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= @@ -13,6 +16,8 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= +github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -20,26 +25,28 @@ github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w= github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/stianeikeland/go-rpio/v4 v4.6.0 h1:eAJgtw3jTtvn/CqwbC82ntcS+dtzUTgo5qlZKe677EY= +github.com/stianeikeland/go-rpio/v4 v4.6.0/go.mod h1:A3GvHxC1Om5zaId+HqB3HKqx4K/AqeckxB7qRjxMK7o= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc= -golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5 h1:wjuX4b5yYQnEQHzd+CBcrcC6OVR2J1CN6mUy0oSxIPo= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +periph.io/x/conn/v3 v3.7.0 h1:f1EXLn4pkf7AEWwkol2gilCNZ0ElY+bxS4WE2PQXfrA= +periph.io/x/conn/v3 v3.7.0/go.mod h1:ypY7UVxgDbP9PJGwFSVelRRagxyXYfttVh7hJZUHEhg= +periph.io/x/devices/v3 v3.7.1 h1:BsExlfYJlZUZoawzpMF7ksgC9f1eBAdqvKRCGvb+VYw= +periph.io/x/devices/v3 v3.7.1/go.mod h1:ezQOe8WknDaMbKZXVwQUQkIauyLyJshwAHkIohHXA94= +periph.io/x/host/v3 v3.8.0 h1:T5ojZ2wvnZHGPS4h95N2ZpcCyHnsvH3YRZ1UUUiv5CQ= +periph.io/x/host/v3 v3.8.0/go.mod h1:rzOLH+2g9bhc6pWZrkCrmytD4igwQ2vxFw6Wn6ZOlLY= diff --git a/main.go b/main.go index 8ad951b..f20baa3 100644 --- a/main.go +++ b/main.go @@ -1,104 +1,92 @@ -//go:build linux - package main import ( - "context" - "net" + "os" + "path/filepath" "time" - "git.tcp.direct/kayos/zwrap" - "github.com/cretz/bine/tor" "github.com/davecgh/go-spew/spew" + "github.com/stianeikeland/go-rpio/v4" + "git.tcp.direct/kayos/door5/config" + "git.tcp.direct/kayos/door5/iot" "git.tcp.direct/kayos/door5/logger" ) -type Knobs struct { - conf tor.ListenConf - log zwrap.Logger - tor.Tor -} -type BlueDoor struct { - conf *Knobs - listeners []net.Listener - localPaths []string +var conf *config.Config + +func OpenDoor() { + pin := rpio.Pin(conf.Pins[iot.Door]) + pin.Output() + pin.High() } -func MemoryMapOSFolder() (path string, err error) { - // create memory mapped folder for tor data directory (linux) - // un - return "", nil +func CloseDoor() { + pin := rpio.Pin(conf.Pins[iot.Door]) + pin.Output() + pin.Low() } -func NewListener(listen string) (net.Listener, error) { - l, err := net.Listen("tcp", listen) - if err != nil { - - } -} - -func NewOnion() (*tor.Tor, error) { - log := logger.Get() - - log.Logger.Info().Msg("starting and registering onion service...") - - t, err := tor.Start(nil, &tor.StartConf{ - RetainTempDataDir: false, - DebugWriter: logger.Get().With().Str("module", "tor").Logger(), - - NoAutoSocksPort: false, - GeoIPFileReader: nil, - }) - if err != nil { - log.Panicf("Unable to start Tor: %v", err) - } - defer func(t *tor.Tor) { - if err = t.Close(); err != nil { - log.Panicf("Unable to close Tor: %v", err) +func watchButton() { + logger.Get().Logger.Debug().Uint8("pin", uint8(conf.Pins[iot.Button])).Msg("exit button daemon started") + defer logger.Get().Logger.Debug().Msg("exit button daemon stopped") + pin := rpio.Pin(conf.Pins[iot.Button]) + pin.Input() + pin.PullUp() + count := 0 + for { + if pin.Read() != rpio.Low { + time.Sleep(5 * time.Millisecond) + if count > 0 { + count-- + } + continue } - }(t) - - // Wait at most a few minutes to publish the service - listenCtx, listenCancel := context.WithTimeout(context.Background(), 2*time.Minute) - defer listenCancel() - - // Create a v3 onion service to listen on any port but show as 80 - onion, err := t.Listen(listenCtx, &tor.ListenConf{ - LocalPort: 0, - LocalListener: nil, - RemotePorts: []int{80}, - Key: nil, - Version3: false, - ClientAuths: nil, - MaxStreams: 0, - DiscardKey: false, - Detach: false, - NonAnonymous: false, - MaxStreamsCloseCircuit: false, - NoWait: false, - }) - if err != nil { - log.Panicf("Unable to create onion service: %v", err) - } - defer func(onion *tor.OnionService) { - if err = onion.Close(); err != nil { - log.Panicf("Unable to close onion service: %v", err) + count++ + if count >= 100 { + logger.Get().Logger.Info().Int("count", count).Msg("door opened") + OpenDoor() + time.Sleep(5 * time.Second) + CloseDoor() + count = 0 } - }(onion) + time.Sleep(5 * time.Millisecond) + } +} - log.Logger.Info().Msgf("live: %v.onion", onion.ID) - return t, nil +func loadConfig() { + var err error + path, _ := os.UserConfigDir() + path = filepath.Join(path, "door5", "config.toml") + if conf, err = config.ReadConfig(path); err != nil { + println(err.Error()) + println("Writing default config") + _ = os.MkdirAll(filepath.Dir(path), 0755) + c := &config.Config{ + Pins: config.Pins{ + iot.Door: 22, + iot.Button: 5, + }, + } + if err = c.WriteConfig(path); err != nil { + println(err.Error()) + os.Exit(1) + } + conf = c + } } func main() { - o, e := NewOnion() - if e != nil { - panic(e) + logger.Get().Logger.Info().Msg("starting door5") + logger.Get().Logger.Trace().Msg("loading config") + loadConfig() + logger.Get().Logger.Debug().Msg(spew.Sdump(conf)) + logger.Get().Logger.Info().Msg("initializing gpio") + if err := rpio.Open(); err != nil { + logger.Get().Logger.Fatal().Msg(err.Error()) } - pi, err := o.Control.ProtocolInfo() - if err != nil { - panic(err) - } - spew.Dump(pi) + + go watchButton() + + select {} } diff --git a/pkg/access/rfid.go b/pkg/access/rfid.go new file mode 100644 index 0000000..76902ae --- /dev/null +++ b/pkg/access/rfid.go @@ -0,0 +1,379 @@ +package access + +// built for PN53x + +import ( + "context" + "encoding/hex" + "fmt" + "net" + "time" + + "github.com/clausecker/nfc/v2" + "github.com/rs/zerolog" + + "git.tcp.direct/kayos/door5/logger" +) + +type ConnectionMode uint8 + +const ( + I2C ConnectionMode = iota + SPI +) + +type Keys struct { + *nfc.Device + AccessLog *CardTimeSeries + incoming chan nfc.Target + connectivity ConnectionMode + infoString string + readDeadline time.Time + writeDeadline time.Time + resetPin int +} + +type localAddr struct { + *Keys +} + +func (la *localAddr) String() string { + return la.Keys.infoString +} + +func (la *localAddr) Network() string { + switch la.Keys.connectivity { + case I2C: + return "i2c" + case SPI: + return "spi" + } + return "unknown" +} + +func (rfid *Keys) Write(b []byte) (n int, err error) { + logger.Get().Logger.Panic().Msg("not implemented") + return 0, nil +} + +func (rfid *Keys) Network() string { + switch rfid.connectivity { + case I2C: + return "i2c" + case SPI: + return "spi" + } + return "unknown" +} + +func (rfid *Keys) LocalAddr() net.Addr { + return &localAddr{rfid} +} + +func (rfid *Keys) RemoteAddr() net.Addr { + return rfid +} + +func (rfid *Keys) SetDeadline(t time.Time) error { + rfid.readDeadline = t + rfid.writeDeadline = t + return nil +} + +func (rfid *Keys) SetReadDeadline(t time.Time) error { + rfid.readDeadline = t + return nil +} + +func (rfid *Keys) SetWriteDeadline(t time.Time) error { + rfid.writeDeadline = t + return nil +} + +func (rfid *Keys) HasIRQ() bool { + return rfid.connectivity == SPI && rfid.resetPin != 0 +} + +//nolint:typecheck +func NewRFID(connString string, mode ConnectionMode, ResetPin ...int) (*Keys, error) { + log := logger.Get().Logger + + lg := log.With().Str("conn_string", connString).Logger() + log = &lg + + reader := &Keys{ + AccessLog: NewCardTimeSeries(), + } + + reader.incoming = make(chan nfc.Target, 1) + reader.connectivity = mode + if len(ResetPin) > 0 { + lg = log.With().Int("reset_pin", ResetPin[0]).Logger() + log = &lg + reader.resetPin = ResetPin[0] + } + + tries := 0 + + lg = log.With().Int("tries", tries).Bool("hasIRQ", reader.HasIRQ()).Logger() + log = &lg + +try: + log.Trace().Msg("opening connection to NFC reader") + + dev, err := nfc.Open(connString) + switch { + case err != nil && reader.HasIRQ(): + if tries > 3 { + return nil, fmt.Errorf("failed to open device: %w", err) + } + log.Debug().Msg("errored during open, have IRQ, resetting and trying again...") + reader.Reset() + tries++ + goto try + case err != nil: + log.Debug().Err(err).Msg("FUBAR") + return nil, fmt.Errorf("failed to open device: %w", err) + default: + } + + reader.Device = &dev + + if reader.infoString, err = reader.Information(); err != nil { + log.Warn().Err(err).Msg("failed to get device info, continuing anyway...") + } + + if reader.infoString != "" { + log.Debug().Str("info", reader.infoString).Msg("got device info") + } + + log.Trace().Msg("initializing NFC reader") + if err = reader.InitiatorInit(); err != nil { + log.Debug().Err(err).Msg("FUBAR") + return nil, fmt.Errorf("failed to initialize reader: %w", err) + } + + return reader, nil +} + +func (rfid *Keys) Reset() { + log := logger.Get().Logger + log.Debug().Msg("Resetting the RFID chip...") + log.Panic().Msg("haha jk not implemented yet") +} + +func (rfid *Keys) Close() error { + if rfid.HasIRQ() { + rfid.Reset() + } + return rfid.Device.Close() +} + +type tagReadStatus struct { + err error + tagCount int + currentTarget nfc.Target + found []*Card +} + +func (trs *tagReadStatus) Run(e *zerolog.Event, level zerolog.Level, message string) { + stringers := make([]fmt.Stringer, len(trs.found)) + for i, v := range trs.found { + stringers[i] = v + } + e.Int("tagCount", trs.tagCount).Stringers("found", stringers) + if trs.err != nil { + e.Err(trs.err) + } +} + +func (trs *tagReadStatus) Error() string { + return trs.err.Error() +} + +type Card struct { + nfc.Target + Type string `json:"type"` + Details map[string]interface{} `json:"details"` + UID string `json:"uid"` +} + +func (c *Card) String() string { + return fmt.Sprintf("%s (%s)", c.UID, c.Type) +} + +func (rfid *Keys) ListenForTags(ctx context.Context, want int) ([]*Card, error) { + var status = &tagReadStatus{ + err: nil, + tagCount: 0, + found: make([]*Card, 0), + } + + log := logger.Get().Logger.Hook(status) + + var modulations = []nfc.Modulation{ + {Type: nfc.ISO14443a, BaudRate: nfc.Nbr106}, + {Type: nfc.ISO14443b, BaudRate: nfc.Nbr106}, + {Type: nfc.Felica, BaudRate: nfc.Nbr212}, + {Type: nfc.Felica, BaudRate: nfc.Nbr424}, + {Type: nfc.Jewel, BaudRate: nfc.Nbr106}, + {Type: nfc.ISO14443biClass, BaudRate: nfc.Nbr106}, + } + + var ( + errChan = make(chan error, want) + ) + + var cancel context.CancelFunc + ctx, cancel = context.WithCancel(ctx) + defer cancel() + + go func() { + for { + select { + case <-ctx.Done(): + var err = ctx.Err() + if rfid.LastError() != nil { + err = fmt.Errorf("error from NFC reader: %w + context canceled: %s", rfid.LastError(), ctx.Err()) + } + errChan <- err + return + default: + } + + var found int + + if found, status.currentTarget, status.err = + rfid.InitiatorPollTarget(modulations, 1, 300*time.Millisecond); status.err != nil || found == 0 { + log.Warn().Err(status.err).Int("found", found).Msg("error polling for target") + continue + } + + status.tagCount++ + + log.Debug().Msgf("got tag: %s", status.currentTarget.String()) + + var ( + card *Card + ) + + // Transform the target to a specific tag Type and send the UID to the channel + switch status.currentTarget.Modulation() { + + case nfc.Modulation{Type: nfc.ISO14443a, BaudRate: nfc.Nbr106}: + cardCasted := status.currentTarget.(*nfc.ISO14443aTarget) + card = &Card{ + UID: hex.EncodeToString(cardCasted.UID[:cardCasted.UIDLen]), + Type: "ISO14443a", + Target: cardCasted, + } + + case nfc.Modulation{Type: nfc.ISO14443b, BaudRate: nfc.Nbr106}: + cardCasted := status.currentTarget.(*nfc.ISO14443bTarget) + card = &Card{ + UID: hex.EncodeToString(cardCasted.ApplicationData[:len(cardCasted.ApplicationData)]), + Type: "ISO14443b", + Target: cardCasted, + Details: map[string]interface{}{ + "ProtocolInfo": cardCasted.ProtocolInfo, + "BaudRate": cardCasted.Modulation().BaudRate, + "ApplicationData": cardCasted.ApplicationData, + "PUPI": cardCasted.Pupi, + "CardIdentifier": cardCasted.CardIdentifier, + }, + } + + case nfc.Modulation{Type: nfc.Felica, BaudRate: nfc.Nbr212}, + nfc.Modulation{Type: nfc.Felica, BaudRate: nfc.Nbr424}: + cardCasted := status.currentTarget.(*nfc.FelicaTarget) + card = &Card{ + UID: hex.EncodeToString(cardCasted.ID[:cardCasted.Len]), + Type: "Felica", + Target: cardCasted, + Details: map[string]interface{}{ + "Len": cardCasted.Len, + "Pad": cardCasted.Pad, + "SysCode": cardCasted.SysCode, + "ResCode": cardCasted.ResCode, + }, + } + switch cardCasted.Modulation().BaudRate { + case nfc.Nbr212: + card.Details["kbps"] = 212 + case nfc.Nbr424: + card.Details["kbps"] = 424 + default: + } + + case nfc.Modulation{Type: nfc.Jewel, BaudRate: nfc.Nbr106}: + cardCasted := status.currentTarget.(*nfc.JewelTarget) + card = &Card{ + UID: hex.EncodeToString(cardCasted.ID[:len(cardCasted.ID)]), + Type: "Jewel", + Target: cardCasted, + Details: map[string]interface{}{ + "SensRes": cardCasted.SensRes, + }, + } + + case nfc.Modulation{Type: nfc.ISO14443biClass, BaudRate: nfc.Nbr106}: + cardCasted := status.currentTarget.(*nfc.ISO14443biClassTarget) + card = &Card{ + UID: hex.EncodeToString(cardCasted.UID[:len(cardCasted.UID)]), + Type: "ISO14443biClass", + Target: cardCasted, + Details: map[string]interface{}{ + "Baud": cardCasted.Baud, + }, + } + } + + cardLog := log.With().Fields(card.Details).Str("uid", card.UID).Str("type", card.Type).Logger() + + cardLog.Info().Msg("+1 card") + + cardLog.Trace().Msg("checking card into timeseries log") + rfid.AccessLog.CheckIn(card) + cardLog.Trace().Msg("checked card into timeseries log") + status.found = append(status.found, card) + + if status.tagCount >= want { + cancel() + return + } + + time.Sleep(300 * time.Millisecond) + } + }() + + select { + case e := <-errChan: + return status.found, e + case <-ctx.Done(): + switch status.tagCount { + case 0: + return nil, fmt.Errorf("failed to read any tags") + case want: + return status.found, nil + default: + return status.found, fmt.Errorf("failed to read %d tags, only read %d", want, status.tagCount) + } + } +} + +func (rfid *Keys) ListenForOneTag(ctx context.Context) (*Card, error) { + if cards, err := rfid.ListenForTags(ctx, 1); err != nil { + return nil, err + } else { + return cards[0], nil + } +} + +func (rfid *Keys) Read(buf []byte) (int, error) { + found, err := rfid.ListenForOneTag(context.Background()) + if err != nil { + return 0, err + } + return copy(buf, []byte(found.UID)), nil + +} diff --git a/pkg/access/rfid_test.go b/pkg/access/rfid_test.go new file mode 100644 index 0000000..c043e38 --- /dev/null +++ b/pkg/access/rfid_test.go @@ -0,0 +1,122 @@ +package access + +import ( + "context" + "os" + "reflect" + "testing" + "time" +) + +// Helper function to create a mock card +func mockCard(uid string, cardType string) *Card { + return &Card{ + UID: uid, + Type: cardType, + } +} + +// TestNewCardTimeSeries tests the NewCardTimeSeries constructor. +func TestNewCardTimeSeries(t *testing.T) { + cts := NewCardTimeSeries() + if cts.Map == nil || cts.crossRef == nil || cts.mu == nil { + t.Error("NewCardTimeSeries() failed to initialize all fields") + } +} + +// TestCardTimeSeries_CheckIn tests the CheckIn method. +func TestCardTimeSeries_CheckIn(t *testing.T) { + cts := NewCardTimeSeries() + card := mockCard("12345", "TestType") + cts.CheckIn(card) + + if _, ok := cts.Map[cts.last]; !ok { + t.Errorf("CheckIn() failed to add card to Map") + } + + if _, ok := cts.crossRef[card]; !ok { + t.Errorf("CheckIn() failed to add card to crossRef") + } +} + +// TestCardTimeSeries_Get tests the Get method. +func TestCardTimeSeries_Get(t *testing.T) { + cts := NewCardTimeSeries() + card := mockCard("12345", "TestType") + now := time.Now() + cts.Map[now] = card + + if got := cts.Get(now); !reflect.DeepEqual(got, card) { + t.Errorf("Get() = %v, want %v", got, card) + } +} + +// TestCardTimeSeries_GetClosest tests the GetClosest method. +func TestCardTimeSeries_GetClosest(t *testing.T) { + cts := NewCardTimeSeries() + card1 := mockCard("12345", "TestType") + card2 := mockCard("67890", "TestType") + now := time.Now() + cts.Map[now] = card1 + cts.Map[now.Add(-1*time.Hour)] = card2 + + if got := cts.GetClosest(now.Add(-30 * time.Minute)); !reflect.DeepEqual(got, card2) { + t.Errorf("GetClosest() = %v, want %v", got, card2) + } +} + +// TestCardTimeSeries_GetLast tests the GetLast method. +func TestCardTimeSeries_GetLast(t *testing.T) { + cts := NewCardTimeSeries() + card := mockCard("12345", "TestType") + cts.CheckIn(card) + + if got := cts.GetLast(); !reflect.DeepEqual(got, card) { + t.Errorf("GetLast() = %v, want %v", got, card) + } +} + +// TestKeys_ListenForTags tests the ListenForTags method. +func TestKeys_ListenForTags(t *testing.T) { + // This test may require mocking of the NFC reader and its methods + // Assuming a mock implementation is available, this test would simulate the behavior of the ListenForTags method +} + +// TestKeys_ListenForOneTag tests the ListenForOneTag method. +func TestKeys_ListenForOneTag(t *testing.T) { + connString := os.Getenv("TEST_RFID") + if connString == "" { + t.Skip("TEST_RFID environment variable not set") + } + + // Assuming NewRFID function and related dependencies are properly mocked + rfid, err := NewRFID(connString, SPI) // Or I2C, depending on the test setup + if err != nil { + t.Fatalf("Failed to create RFID instance: %v", err) + } + defer rfid.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + card, err := rfid.ListenForOneTag(ctx) + if err != nil { + t.Errorf("ListenForOneTag() returned an error: %v", err) + } + + if card == nil { + t.Errorf("ListenForOneTag() returned nil, expected a card") + } + + t.Run("CheckIn", func(t *testing.T) { + if rfid.AccessLog.GetLast() != card { + t.Errorf("ListenForOneTag() failed to check card into timeseries log") + } + if rfid.AccessLog.GetClosest(time.Now()) != card { + t.Errorf("ListenForOneTag() failed to check card into timeseries log") + } + }) + +} + +// Note: Additional mocking and setup may be required to test methods that interact with external dependencies like NFC reader. diff --git a/pkg/access/timeseries.go b/pkg/access/timeseries.go new file mode 100644 index 0000000..e5f6ea9 --- /dev/null +++ b/pkg/access/timeseries.go @@ -0,0 +1,60 @@ +package access + +import ( + "sync" + "time" +) + +type CardTimeSeries struct { + Map map[time.Time]*Card `json:"nfc_checkins"` + crossRef map[*Card][]time.Time + last time.Time + mu *sync.RWMutex +} + +func NewCardTimeSeries() *CardTimeSeries { + return &CardTimeSeries{ + Map: make(map[time.Time]*Card), + crossRef: make(map[*Card][]time.Time), + mu: &sync.RWMutex{}, + } +} + +func (cts *CardTimeSeries) CheckIn(c *Card) { + cts.mu.Lock() + defer cts.mu.Unlock() + tnow := time.Now() + cts.Map[tnow] = c + if cts.crossRef[c] == nil { + cts.crossRef[c] = make([]time.Time, 0) + } + cts.crossRef[c] = append(cts.crossRef[c], tnow) + cts.last = tnow +} + +func (cts *CardTimeSeries) Get(t time.Time) *Card { + cts.mu.RLock() + defer cts.mu.RUnlock() + return cts.Map[t] +} + +func (cts *CardTimeSeries) GetClosest(t time.Time) *Card { + cts.mu.RLock() + defer cts.mu.RUnlock() + if cts.Map[t] != nil { + return cts.Map[t] + } + var closest time.Time + for k := range cts.Map { + if k.Before(t) && k.After(closest) { + closest = k + } + } + return cts.Map[closest] +} + +func (cts *CardTimeSeries) GetLast() *Card { + cts.mu.RLock() + defer cts.mu.RUnlock() + return cts.Map[cts.last] +} diff --git a/pkg/config/toml.go b/pkg/config/toml.go new file mode 100644 index 0000000..2936f61 --- /dev/null +++ b/pkg/config/toml.go @@ -0,0 +1,42 @@ +package config + +import ( + "fmt" + "os" + + "github.com/pelletier/go-toml/v2" + + "git.tcp.direct/kayos/door5/iot" +) + +type Pins map[iot.Thing]iot.GPIO + +type Config struct { + Pins Pins `toml:"pins"` +} + +func (p Pins) MarshalTOML() ([]byte, error) { + var rendered string + for thing, pin := range p { + rendered += fmt.Sprintf("%s = %d\n", thing.String(), pin) + } + return []byte(rendered), nil +} + +func ReadConfig(path string) (*Config, error) { + var config = &Config{} + b, e := os.ReadFile(path) + if e != nil { + return nil, e + } + err := toml.Unmarshal(b, config) + return config, err +} + +func (c *Config) WriteConfig(path string) error { + b, e := toml.Marshal(c) + if e != nil { + return e + } + return os.WriteFile(path, b, 0644) +} diff --git a/pkg/iot/types.go b/pkg/iot/types.go new file mode 100644 index 0000000..69e9f06 --- /dev/null +++ b/pkg/iot/types.go @@ -0,0 +1,32 @@ +package iot + +type ( + Thing uint16 + GPIO uint8 +) + +const ( + Door Thing = iota + Button + NFC + Webcam +) + +func (t Thing) MarshalText() ([]byte, error) { + return []byte(t.String()), nil +} + +func (t Thing) String() string { + switch t { + case Door: + return "Door" + case Button: + return "Button" + case NFC: + return "NFC" + case Webcam: + return "Webcam" + default: + return "Unknown" + } +} diff --git a/logger/log.go b/pkg/logger/log.go similarity index 84% rename from logger/log.go rename to pkg/logger/log.go index 95a5967..41980d6 100644 --- a/logger/log.go +++ b/pkg/logger/log.go @@ -3,6 +3,7 @@ package logger import ( "io" "os" + "path/filepath" "runtime" "sync" @@ -17,7 +18,7 @@ var ( func Get() *zwrap.Logger { setupOnce.Do(func() { - if err := Setup("", zerolog.InfoLevel); err != nil { + if err := Setup("", zerolog.TraceLevel); err != nil { println(err.Error()) os.Exit(1) } @@ -42,7 +43,7 @@ func fileWriter(path string) (io.Writer, error) { return nil, err } var f *os.File - if f, err = os.OpenFile(path+"/openspa.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err != nil { + if f, err = os.OpenFile(filepath.Join(path, "door5.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err != nil { return nil, err } return f, nil