diff options
| -rw-r--r-- | cmd/orca/display.go | 4 | ||||
| -rw-r--r-- | cmd/orca/main.go | 61 | ||||
| -rw-r--r-- | cvend/cvend.go | 181 | ||||
| -rw-r--r-- | ipp/ipp.go | 40 |
4 files changed, 249 insertions, 37 deletions
diff --git a/cmd/orca/display.go b/cmd/orca/display.go index 5bd4f87..7eaf778 100644 --- a/cmd/orca/display.go +++ b/cmd/orca/display.go @@ -102,7 +102,9 @@ func drawDisplay(img draw.Image, s displayState) { drawText(img, notoSans_Regular, 16, justifyCenter, color.White, fixed.P(400, 25), "V1234567-D98765") draw.Draw(img, orcaImg.Bounds().Add(image.Point{400 - orcaImg.Bounds().Dx()/2, 75}), orcaImg, image.Point{}, draw.Over) if s.cvend.ready { - drawText(img, notoSans_Bold, 48, justifyCenter, color.White, fixed.P(400, 330), "Tap below") + if !s.cvend.hasCard { + drawText(img, notoSans_Bold, 48, justifyCenter, color.White, fixed.P(400, 330), "Tap below") + } draw.Draw(img, belowImg.Bounds().Add(image.Point{400 - belowImg.Bounds().Dx()/2, 380}), belowImg, image.Point{}, draw.Over) } } diff --git a/cmd/orca/main.go b/cmd/orca/main.go index f840d22..a068617 100644 --- a/cmd/orca/main.go +++ b/cmd/orca/main.go @@ -1,6 +1,7 @@ package main import ( + "encoding/hex" "flag" "image" "log" @@ -9,11 +10,11 @@ import ( "pm3.dev/cvend" "pm3.dev/fb" - "pm3.dev/ipp" ) type cvendState struct { - ready bool + ready bool + hasCard bool } type displayState struct { @@ -61,14 +62,14 @@ func wallTicker(d time.Duration) *time.Ticker { return ticker } -func displayTask(stateCh <-chan displayState) { +func displayTask(in <-chan displayState) { fb, err := fb.Open() if err != nil { log.Fatalf("open fb: %v", err) } defer fb.Close() - for state := range stateCh { + for state := range in { start := time.Now() img := image.NewRGBA(fb.Bounds()) drawDisplay(img, state) @@ -81,24 +82,44 @@ func displayTask(stateCh <-chan displayState) { func cvendTask(out chan<- cvendState) { var state cvendState - var cv ipp.Session - var err error - cv, err = cvend.OpenIPP(func(msgType byte, msgData []byte) { - switch msgType { - case 0x05: // StatusReply - if !state.ready { - state.ready = true - out <- state - } - default: - cvend.LogIPP(msgType, msgData) - } - }) + cv, err := cvend.Open() + if err != nil { + log.Fatalf("cvend Open: %s", err) + } + status, err := cv.AwaitStatus() + if err != nil { + log.Fatalf("cvend AwaitStatus: %s", err) + } + log.Printf("cvend status: %q", string(status)) + reply, err := cv.ProxCardFunction(7, true) if err != nil { - panic(err) + log.Fatalf("cvend ProxCardFunction: %s", err) } + log.Printf("enabled DESFire %s", hex.EncodeToString(reply)) + state.ready = true + out <- state + go func() { // send periodic status requests to keep cvend from going to sleep + for { + time.Sleep(30 * time.Second) + if err := cv.SendIPP(0x04, nil); err != nil { + log.Printf("failed to send Status: %s", err) + } + } + }() + for { - cv.SendIPP(0x04, nil) // Status - time.Sleep(1 * time.Second) + cardRaw, err := cv.AwaitCard() + if err != nil { + log.Println("error awaiting card: %s", err) + continue + } + card := cardRaw.(*cvend.DESFireCard) // todo other types + log.Println("got card %+v", card) + state.hasCard = true + out <- state + card.AwaitRemoved() + log.Println("card removed") + state.hasCard = false + out <- state } } diff --git a/cvend/cvend.go b/cvend/cvend.go index 263b1f7..b002629 100644 --- a/cvend/cvend.go +++ b/cvend/cvend.go @@ -4,7 +4,11 @@ import ( "bytes" "encoding/binary" "encoding/hex" + "errors" "log" + "os" + "slices" + "time" "pm3.dev/ipp" ) @@ -66,3 +70,180 @@ func LogIPP(msgType byte, msgData []byte) { log.Printf("ipp %02x\n%s", msgType, hex.Dump(msgData)) } } + +var ( + ErrCardRemoved = errors.New("card removed") +) + +type Device struct { + ipp.Session + closed chan struct{} + + statusReply chan []byte // 0x05 + heartbeat chan []byte // 0x07 + startup chan []byte // 0x0f + desFireRead chan []byte // 0xb9 + desFireCardRemoved chan []byte // 0xbb + desFireCommandReply chan []byte // 0xbd + unhandledCard chan []byte // 0xbe + proxCardFunctionReply chan []byte // 0xe5 +} + +func Open() (*Device, error) { + d := &Device{ + statusReply: make(chan []byte), + heartbeat: make(chan []byte), + startup: make(chan []byte), + desFireRead: make(chan []byte), + desFireCardRemoved: make(chan []byte), + desFireCommandReply: make(chan []byte), + unhandledCard: make(chan []byte), + proxCardFunctionReply: make(chan []byte), + } + var err error + d.Session, err = ipp.Open(Path, func(msgType byte, msgData []byte) { + // if channel receiver waiting, deliver data to it, else log + var ch chan []byte + switch msgType { + case 0x05: + ch = d.statusReply + case 0x07: + ch = d.heartbeat + case 0x0f: + ch = d.startup + case 0xb9: + ch = d.desFireRead + case 0xbb: + ch = d.desFireCardRemoved + case 0xbd: + ch = d.desFireCommandReply + case 0xbe: + ch = d.unhandledCard + case 0xe5: + ch = d.proxCardFunctionReply + } + select { + case ch <- slices.Clone(msgData): + return + default: + } + switch msgType { + case 0x07: + log.Printf("Heartbeat(%s)", hex.EncodeToString(msgData)) + case 0xed: + log.Printf("Log(%d) %s", msgData[0], string(bytes.TrimSuffix(msgData[1:], []byte{0}))) + default: + log.Printf("ipp %02x\n%s", msgType, hex.Dump(msgData)) + } + }) + if err != nil { + return nil, err + } + return d, err +} + +func (d *Device) Close() error { + close(d.closed) + return d.Session.Close() +} + +func (d *Device) AwaitStatus() ([]byte, error) { + for { + if err := d.SendIPP(0x04, nil); err != nil { // Status + return nil, err + } + select { + case <-d.closed: + return nil, os.ErrClosed + case status := <-d.statusReply: + return status, nil + case <-d.startup: + // retry + case <-time.After(1 * time.Second): + // retry + } + } +} + +func (d *Device) ProxCardFunction(cardType uint16, enable bool) ([]byte, error) { + var payload [4]byte + binary.BigEndian.PutUint16(payload[0:2], cardType) + payload[2] = 1 + if enable { + payload[3] = 1 + } + if err := d.SendIPP(0xe4, payload[:]); err != nil { + return nil, err + } + select { + case resp := <-d.proxCardFunctionReply: + return resp, nil + case <-d.closed: + return nil, os.ErrClosed + } +} + +func (d *Device) AwaitCard() (Card, error) { + select { + case data := <-d.desFireRead: + if len(data) < 11 { + log.Printf("short desfire read: %s", hex.EncodeToString(data)) + return nil, errors.New("invalid desfire read") + } + c := &DESFireCard{ + UID: data[4:], + d: d, + removed: make(chan struct{}), + } + go func() { + <-d.desFireCardRemoved + close(c.removed) + }() + return c, nil + case <-d.closed: + return nil, os.ErrClosed + } +} + +type Card interface { + Release() error +} + +type DESFireCard struct { + UID []byte + + d *Device + removed chan struct{} +} + +func (c *DESFireCard) AwaitRemoved() { + <-c.removed +} + +func (c *DESFireCard) Release() error { + select { + case <-c.removed: + return ErrCardRemoved + default: + } + return c.d.SendIPP(0x32, nil) +} + +func (c *DESFireCard) Command(cmd byte, data []byte) ([]byte, error) { + select { + case <-c.removed: + return nil, ErrCardRemoved + default: + } + if err := c.d.SendIPP(0xbc, append([]byte{cmd}, data...)); err != nil { + return nil, err + } + select { + case <-c.removed: + return nil, ErrCardRemoved + case <-c.d.closed: + return nil, os.ErrClosed + case data := <-c.d.desFireCommandReply: + return data, nil + } +} @@ -66,11 +66,16 @@ func Open(path string, handler Handler) (Session, error) { if err != nil { return nil, err } - termios.Iflag &^= unix.IGNBRK | unix.BRKINT | unix.PARMRK | unix.ISTRIP | unix.INLCR | unix.IGNCR | unix.ICRNL | unix.IXON - termios.Oflag &^= unix.OPOST - termios.Lflag &^= unix.ECHO | unix.ECHONL | unix.ICANON | unix.ISIG | unix.IEXTEN - termios.Cflag &^= unix.CBAUD | unix.CSIZE | unix.PARENB | unix.CSTOPB - termios.Cflag |= unix.B115200 | unix.CS8 | unix.CREAD | unix.CLOCAL + termios.Iflag = unix.IGNBRK | unix.IGNPAR + termios.Oflag = 0 + termios.Lflag = 0 + termios.Cflag = unix.B115200 | unix.CS8 | unix.CREAD | unix.CLOCAL + // when VMIN>0, reads can take unacceptably long. + // nx sets VMIN=1 VTIME=10 and uses select + FIONREAD + nonblocking read + // and somehow seems to avoid this but i cannot replicate it here. + // with VMIN=0 VTIME=1, up to 100ms delay, but no more + termios.Cc[unix.VMIN] = 0 + termios.Cc[unix.VTIME] = 1 if err = unix.IoctlSetTermios(fd, unix.TCSETS, termios); err != nil { return nil, err } @@ -84,26 +89,29 @@ func Open(path string, handler Handler) (Session, error) { return s, nil } +func (s *session) isClosed() bool { + s.mu.Lock() + defer s.mu.Unlock() + return s.closed +} + func (s *session) readTask() { var bs [1 << 16]byte var offset int for { - n, err := unix.Read(s.fd, bs[offset:]) + nRead, err := unix.Read(s.fd, bs[offset:]) if err != nil { - s.mu.Lock() - closed := s.closed - s.mu.Unlock() - if closed { + if s.isClosed() { break } panic(err) } - nRead := s.handleMsg(bs[:offset+n]) - offset = copy(bs[:], bs[nRead:offset+n]) + nConsumed := s.handleMsg(bs[:offset+nRead]) + offset = copy(bs[:], bs[nConsumed:offset+nRead]) } } -func (s *session) handleMsg(bs []byte) (nRead int) { +func (s *session) handleMsg(bs []byte) (nConsumed int) { expectedLen := 1 if len(bs) < expectedLen { return @@ -113,7 +121,7 @@ func (s *session) handleMsg(bs []byte) (nRead int) { if *Trace { log.Printf("skipping non-IPP message %s", hex.EncodeToString(bs)) } - nRead = len(bs) + nConsumed = len(bs) return } expectedLen += 6 @@ -128,7 +136,7 @@ func (s *session) handleMsg(bs []byte) (nRead int) { expectedHdrCRC := crc8(bs[0:6]...) if hdrCRC != expectedHdrCRC { log.Printf("skipping IPP message %02x, expected crc8 %02x got %02x", ippType, expectedHdrCRC, hdrCRC) - nRead = len(bs) + nConsumed = len(bs) return } expectedLen += ippLen @@ -139,7 +147,7 @@ func (s *session) handleMsg(bs []byte) (nRead int) { if len(bs) < expectedLen { return } - nRead = expectedLen + nConsumed = expectedLen ippData := bs[7 : 7+ippLen] if hasMsgCRC { msgCRC := binary.LittleEndian.Uint32(bs[7+ippLen : 7+ippLen+4]) |