summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKevin Wallace <kevin@pentabarf.net>2026-04-26 08:18:53 -0700
committerKevin Wallace <kevin@pentabarf.net>2026-04-26 08:18:53 -0700
commit1c8205cff6432248212a160a7a86b8c896dd2a8d (patch)
treec2b33124837881e20becce13285c3b5a9a2a183b
parentwait for cvend at startup, only redraw on state changes (diff)
cvend wip
-rw-r--r--cmd/orca/display.go4
-rw-r--r--cmd/orca/main.go61
-rw-r--r--cvend/cvend.go181
-rw-r--r--ipp/ipp.go40
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
+ }
+}
diff --git a/ipp/ipp.go b/ipp/ipp.go
index 89036b0..c71b590 100644
--- a/ipp/ipp.go
+++ b/ipp/ipp.go
@@ -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])