Successful RFID implementation

This commit is contained in:
kayos@tcp.direct 2023-12-23 16:36:41 -08:00
parent 2bf5c0ff99
commit 75035e2751
Signed by: kayos
GPG Key ID: 4B841471B4BEE979
9 changed files with 739 additions and 105 deletions

11
go.mod
View File

@ -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
)

41
go.sum
View File

@ -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=

152
main.go
View File

@ -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 {}
}

379
pkg/access/rfid.go Normal file
View File

@ -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
}

122
pkg/access/rfid_test.go Normal file
View File

@ -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.

60
pkg/access/timeseries.go Normal file
View File

@ -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]
}

42
pkg/config/toml.go Normal file
View File

@ -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)
}

32
pkg/iot/types.go Normal file
View File

@ -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"
}
}

View File

@ -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