Get-Service TeamViewer | Select-Object Status, Name, StartType
This commit is contained in:
parent
4dfe139c3b
commit
cb5a09e710
1
.gitignore
vendored
1
.gitignore
vendored
@ -29,6 +29,7 @@ _obj
|
||||
_test
|
||||
.vscode/
|
||||
ChipDNAClient/
|
||||
docs/
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
package dispenser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -27,6 +30,13 @@ const (
|
||||
CardWellEmptyMessage = "Card well is empty"
|
||||
)
|
||||
|
||||
const (
|
||||
positionPreDispense = 0x01
|
||||
positionEncoder = 0x02
|
||||
positionMouth = 0x04
|
||||
positionEmpty = 0x08
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCardWellEmpty = errors.New(CardWellEmptyMessage)
|
||||
|
||||
@ -59,20 +69,21 @@ var (
|
||||
0x31: "Card pre-empty",
|
||||
0x30: "Normal",
|
||||
}
|
||||
statusPos3 = map[byte]string{
|
||||
0x38: "Card empty",
|
||||
0x34: "Card ready position",
|
||||
0x33: "Card at encoder position",
|
||||
0x32: "Card at hold card position",
|
||||
0x31: "Card out of card mouth position",
|
||||
0x30: "Normal",
|
||||
}
|
||||
)
|
||||
|
||||
// --------------------
|
||||
// Status helpers
|
||||
// --------------------
|
||||
|
||||
// decodePositionStatus decodes the manual's 0x30 + combined sensor/empty flags.
|
||||
// Sensor 2 (0x02) is the read position; sensor 1 and sensor 2 together give 0x33.
|
||||
func decodePositionStatus(value byte) (flags byte, valid bool) {
|
||||
if value&0xF0 != 0x30 {
|
||||
return 0, false
|
||||
}
|
||||
return value & 0x0F, true
|
||||
}
|
||||
|
||||
func statusDescription(statusBytes []byte) string {
|
||||
if len(statusBytes) < 4 {
|
||||
return fmt.Sprintf("<invalid len=%d>", len(statusBytes))
|
||||
@ -86,7 +97,6 @@ func statusDescription(statusBytes []byte) string {
|
||||
{pos: 1, value: statusBytes[0], mapper: statusPos0},
|
||||
{pos: 2, value: statusBytes[1], mapper: statusPos1},
|
||||
{pos: 3, value: statusBytes[2], mapper: statusPos2},
|
||||
{pos: 4, value: statusBytes[3], mapper: statusPos3},
|
||||
}
|
||||
|
||||
var result strings.Builder
|
||||
@ -99,6 +109,24 @@ func statusDescription(statusBytes []byte) string {
|
||||
result.WriteString(statusMsg + "; ")
|
||||
}
|
||||
}
|
||||
flags, valid := decodePositionStatus(statusBytes[3])
|
||||
if !valid {
|
||||
fmt.Fprintf(&result, "Unknown status 0x%X at position 4; ", statusBytes[3])
|
||||
} else {
|
||||
for _, position := range []struct {
|
||||
mask byte
|
||||
text string
|
||||
}{
|
||||
{positionPreDispense, "Card at pre-dispense position"},
|
||||
{positionEncoder, "Card at encoder position"},
|
||||
{positionMouth, "Card at mouth position"},
|
||||
{positionEmpty, "Card empty"},
|
||||
} {
|
||||
if flags&position.mask != 0 {
|
||||
result.WriteString(position.text + "; ")
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.String()
|
||||
}
|
||||
|
||||
@ -107,7 +135,11 @@ func logStatus(statusBytes []byte) {
|
||||
}
|
||||
|
||||
func isAtEncoderPosition(statusBytes []byte) bool {
|
||||
return len(statusBytes) >= 4 && statusBytes[3] == 0x33
|
||||
if len(statusBytes) < 4 {
|
||||
return false
|
||||
}
|
||||
flags, valid := decodePositionStatus(statusBytes[3])
|
||||
return valid && flags&positionEncoder != 0
|
||||
}
|
||||
|
||||
func validateDispenserStatusData(statusBytes []byte) error {
|
||||
@ -115,12 +147,15 @@ func validateDispenserStatusData(statusBytes []byte) error {
|
||||
return fmt.Errorf("malformed dispenser status: got %d bytes, want 4", len(statusBytes))
|
||||
}
|
||||
|
||||
statusMaps := []map[byte]string{statusPos0, statusPos1, statusPos2, statusPos3}
|
||||
statusMaps := []map[byte]string{statusPos0, statusPos1, statusPos2}
|
||||
for position, mapper := range statusMaps {
|
||||
if _, ok := mapper[statusBytes[position]]; !ok {
|
||||
return fmt.Errorf("unknown dispenser status 0x%X at position %d", statusBytes[position], position+1)
|
||||
}
|
||||
}
|
||||
if _, valid := decodePositionStatus(statusBytes[3]); !valid {
|
||||
return fmt.Errorf("unknown dispenser status 0x%X at position 4", statusBytes[3])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -162,14 +197,18 @@ func stockTake(statusBytes []byte) string {
|
||||
if statusBytes[2] != 0x30 {
|
||||
status = statusPos2[statusBytes[2]]
|
||||
}
|
||||
if statusBytes[3] == 0x38 {
|
||||
status = statusPos3[statusBytes[3]]
|
||||
if isCardWellEmpty(statusBytes) {
|
||||
status = "Card empty"
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
func isCardWellEmpty(statusBytes []byte) bool {
|
||||
return len(statusBytes) >= 4 && statusBytes[3] == 0x38
|
||||
if len(statusBytes) < 4 {
|
||||
return false
|
||||
}
|
||||
flags, valid := decodePositionStatus(statusBytes[3])
|
||||
return valid && flags&positionEmpty != 0
|
||||
}
|
||||
|
||||
func checkACK(statusResp []byte) error {
|
||||
@ -208,20 +247,99 @@ func createPacket(address []byte, command []byte) []byte {
|
||||
|
||||
func buildCheckAP(address []byte) []byte { return createPacket(address, []byte{STX, 0x41, 0x50}) }
|
||||
|
||||
func sendAndReceive(port *serial.Port, packet []byte, delay time.Duration) ([]byte, error) {
|
||||
_, err := port.Write(packet)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error writing to port: %w", err)
|
||||
}
|
||||
// serialTransport is used only by the serial-port owner.
|
||||
type serialTransport interface {
|
||||
io.Reader
|
||||
io.Writer
|
||||
}
|
||||
|
||||
time.Sleep(delay)
|
||||
|
||||
buf := make([]byte, 128)
|
||||
n, err := port.Read(buf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading from port: %w", err)
|
||||
func writePacket(ctx context.Context, port serialTransport, packet []byte) error {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
return buf[:n], nil
|
||||
n, err := port.Write(packet)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write dispenser packet: %w", err)
|
||||
}
|
||||
if n != len(packet) {
|
||||
return fmt.Errorf("write dispenser packet (%d/%d bytes): %w", n, len(packet), io.ErrShortWrite)
|
||||
}
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
func readExact(ctx context.Context, port serialTransport, data []byte) error {
|
||||
for len(data) > 0 {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
n, err := port.Read(data)
|
||||
data = data[n:]
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
return ctxErr
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("read dispenser response: %w", err)
|
||||
}
|
||||
if n == 0 {
|
||||
return fmt.Errorf("read dispenser response: %w", io.ErrNoProgress)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func sendAndReadACK(ctx context.Context, port serialTransport, packet []byte, processingDelay time.Duration) error {
|
||||
if err := writePacket(ctx, port, packet); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := waitForSequence(ctx, processingDelay); err != nil {
|
||||
return err
|
||||
}
|
||||
response := make([]byte, 3)
|
||||
if err := readExact(ctx, port, response); err != nil {
|
||||
return fmt.Errorf("read ACK: %w", err)
|
||||
}
|
||||
return checkACK(response)
|
||||
}
|
||||
|
||||
// queryStatus accepts only the fixed RF/AP payload sizes, before reading a body.
|
||||
func queryStatus(ctx context.Context, port serialTransport, command []byte, statusCount int, processingDelay time.Duration) ([]byte, error) {
|
||||
if err := sendAndReadACK(ctx, port, createPacket(Address, command), processingDelay); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := writePacket(ctx, port, append([]byte{ENQ}, Address...)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := waitForSequence(ctx, processingDelay); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
header := make([]byte, 5)
|
||||
if err := readExact(ctx, port, header); err != nil {
|
||||
return nil, fmt.Errorf("read status header: %w", err)
|
||||
}
|
||||
if header[0] != STX {
|
||||
return nil, fmt.Errorf("invalid status STX: % X", header)
|
||||
}
|
||||
if len(Address) != 2 || header[1] != Address[0] || header[2] != Address[1] {
|
||||
return nil, fmt.Errorf("unexpected status address: % X", header[1:3])
|
||||
}
|
||||
length := int(binary.BigEndian.Uint16(header[3:5]))
|
||||
if length != statusCount+2 {
|
||||
return nil, fmt.Errorf("invalid status payload length: got %d, want %d", length, statusCount+2)
|
||||
}
|
||||
frame := append(header, make([]byte, length+2)...)
|
||||
if err := readExact(ctx, port, frame[5:]); err != nil {
|
||||
return nil, fmt.Errorf("read status body: %w", err)
|
||||
}
|
||||
if frame[len(frame)-2] != ETX {
|
||||
return nil, fmt.Errorf("invalid status ETX: % X", frame)
|
||||
}
|
||||
if calculateBCC(frame[:len(frame)-1]) != frame[len(frame)-1] {
|
||||
return nil, fmt.Errorf("invalid status BCC: % X", frame)
|
||||
}
|
||||
if frame[5] != 'S' || frame[6] != 'F' {
|
||||
return nil, fmt.Errorf("unexpected status response type: % X", frame[5:7])
|
||||
}
|
||||
return frame[7 : 7+statusCount], nil
|
||||
}
|
||||
|
||||
// --------------------
|
||||
@ -271,83 +389,28 @@ func InitializeDispenser() (*serial.Port, error) {
|
||||
// --------------------
|
||||
|
||||
// checkDispenserStatus talks to the device and returns the 4 status bytes [pos0..pos3].
|
||||
func checkDispenserStatus(port *serial.Port) ([]byte, error) {
|
||||
checkCmd := buildCheckAP(Address)
|
||||
enq := append([]byte{ENQ}, Address...)
|
||||
|
||||
statusResp, err := sendAndReceive(port, checkCmd, delay)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error sending check command: %w", err)
|
||||
}
|
||||
if len(statusResp) == 0 {
|
||||
return nil, fmt.Errorf("no response from dispenser")
|
||||
}
|
||||
if err := checkACK(statusResp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
statusResp, err = sendAndReceive(port, enq, delay)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error sending ENQ: %w", err)
|
||||
}
|
||||
if len(statusResp) < 13 {
|
||||
return nil, fmt.Errorf("incomplete status response from dispenser: % X", statusResp)
|
||||
}
|
||||
return statusResp[7:11], nil
|
||||
func checkDispenserStatus(ctx context.Context, port serialTransport) ([]byte, error) {
|
||||
return queryStatus(ctx, port, []byte{0x02, 'A', 'P'}, 4, delay)
|
||||
}
|
||||
|
||||
func cardToEncoderPosition(port *serial.Port) error {
|
||||
enq := append([]byte{ENQ}, Address...)
|
||||
// dispatchCommand confirms ACK and sends ENQ; it does not wait for movement.
|
||||
func dispatchCommand(ctx context.Context, port serialTransport, command []byte, processingDelay time.Duration) error {
|
||||
if err := sendAndReadACK(ctx, port, createPacket(Address, command), processingDelay); err != nil {
|
||||
return err
|
||||
}
|
||||
return writePacket(ctx, port, append([]byte{ENQ}, Address...))
|
||||
}
|
||||
|
||||
dispenseCmd := createPacket(Address, commandFC7)
|
||||
func cardToEncoderPosition(ctx context.Context, port serialTransport) error {
|
||||
log.Println("Send card to encoder position")
|
||||
|
||||
statusResp, err := sendAndReceive(port, dispenseCmd, delay)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error sending card to encoder position: %w", err)
|
||||
}
|
||||
if err := checkACK(statusResp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = port.Write(enq)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error sending ENQ to prompt device: %w", err)
|
||||
}
|
||||
return nil
|
||||
return dispatchCommand(ctx, port, commandFC7, delay)
|
||||
}
|
||||
|
||||
func resetDispenser(port *serial.Port) error {
|
||||
response, err := sendAndReceive(port, createPacket(Address, commandRS), delay)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error sending reset command: %w", err)
|
||||
}
|
||||
if err := checkACK(response); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := port.Write(append([]byte{ENQ}, Address...)); err != nil {
|
||||
return fmt.Errorf("error sending ENQ to reset device: %w", err)
|
||||
}
|
||||
return nil
|
||||
func resetDispenser(ctx context.Context, port serialTransport) error {
|
||||
return dispatchCommand(ctx, port, commandRS, delay)
|
||||
}
|
||||
|
||||
func cardOutOfMouth(port *serial.Port) error {
|
||||
enq := append([]byte{ENQ}, Address...)
|
||||
|
||||
dispenseCmd := createPacket(Address, commandFC0)
|
||||
func cardOutOfMouth(ctx context.Context, port serialTransport) error {
|
||||
log.Println("Send card to out mouth position")
|
||||
|
||||
statusResp, err := sendAndReceive(port, dispenseCmd, delay)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error sending out of mouth command: %w", err)
|
||||
}
|
||||
if err := checkACK(statusResp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = port.Write(enq)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error sending ENQ to prompt device: %w", err)
|
||||
}
|
||||
return nil
|
||||
return dispatchCommand(ctx, port, commandFC0, delay)
|
||||
}
|
||||
|
||||
@ -44,7 +44,7 @@ const (
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
port *serial.Port
|
||||
port serialTransport
|
||||
|
||||
reqCh chan cmdReq
|
||||
done chan struct{}
|
||||
@ -189,7 +189,7 @@ func (c *Client) handle(req cmdReq) {
|
||||
|
||||
switch req.typ {
|
||||
case cmdStatus:
|
||||
st, err := checkDispenserStatus(c.port)
|
||||
st, err := checkDispenserStatus(req.ctx, c.port)
|
||||
if err == nil && len(st) == 4 {
|
||||
c.mu.Lock()
|
||||
c.lastStatus = append([]byte(nil), st...)
|
||||
@ -202,18 +202,18 @@ func (c *Client) handle(req cmdReq) {
|
||||
req.respCh <- cmdResp{status: st, err: err}
|
||||
|
||||
case cmdToEncoder:
|
||||
err := cardToEncoderPosition(c.port)
|
||||
err := cardToEncoderPosition(req.ctx, c.port)
|
||||
// A movement command makes any previously cached position unreliable.
|
||||
c.invalidateStatusCache()
|
||||
req.respCh <- cmdResp{err: err}
|
||||
|
||||
case cmdReset:
|
||||
err := resetDispenser(c.port)
|
||||
err := resetDispenser(req.ctx, c.port)
|
||||
c.invalidateStatusCache()
|
||||
req.respCh <- cmdResp{err: err}
|
||||
|
||||
case cmdOutOfMouth:
|
||||
err := cardOutOfMouth(c.port)
|
||||
err := cardOutOfMouth(req.ctx, c.port)
|
||||
// A movement command makes any previously cached position unreliable.
|
||||
c.invalidateStatusCache()
|
||||
req.respCh <- cmdResp{err: err}
|
||||
@ -340,9 +340,7 @@ func (c *Client) readSequenceStatus(ctx context.Context, operation string) ([]by
|
||||
}
|
||||
|
||||
func preparationStatus(operation string, status []byte, allowCombinedFailure bool) (bool, error) {
|
||||
if len(status) != 4 {
|
||||
return false, fmt.Errorf("[%s] %w", operation, validateDispenserStatusData(status))
|
||||
}
|
||||
// A confirmed read sensor takes precedence over every other diagnostic.
|
||||
if isAtEncoderPosition(status) {
|
||||
if hasPreparationDiagnostics(status) {
|
||||
log.Warnf(
|
||||
@ -367,7 +365,7 @@ func preparationStatus(operation string, status []byte, allowCombinedFailure boo
|
||||
// Some dispenser firmware briefly reports 0x32 ("Preparing card fails")
|
||||
// while the card is still travelling to the encoder. Treat that one
|
||||
// diagnostic as transient during an active preparation sequence and let
|
||||
// pollForEncoderPosition decide success (0x33) or timeout. Do not mask
|
||||
// pollForEncoderPosition decide encoder success or timeout. Do not mask
|
||||
// independent hard errors reported in the other status bytes.
|
||||
// Combined command rejection is also recoverable before the one retry.
|
||||
if status[0] == 0x32 || (status[0] == 0x36 && allowCombinedFailure) {
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
@ -172,7 +173,7 @@ func TestPreparationHardErrorsNeverReset(t *testing.T) {
|
||||
{0x36, 0x32, 0x30, 0x30}, // Independent dispense error.
|
||||
{0x32, 0x31, 0x30, 0x30}, // Independent capture error.
|
||||
{0x34, 0x30, 0x30, 0x30}, // Command rejection alone.
|
||||
{0x30, 0x30, 0x30, 0x39}, // Unknown.
|
||||
{0x30, 0x30, 0x30, 0x40}, // Unknown.
|
||||
{0x32}, // Malformed.
|
||||
} {
|
||||
responses := prepareFailureResponses(0x32, 7)
|
||||
@ -482,7 +483,7 @@ func TestPrepareCurrentCardRejectsFailureWithoutEncoderOrMovement(t *testing.T)
|
||||
},
|
||||
{
|
||||
name: "unknown status",
|
||||
response: cmdResp{status: []byte{0x30, 0x30, 0x30, 0x39}},
|
||||
response: cmdResp{status: []byte{0x30, 0x30, 0x30, 0x40}},
|
||||
wantError: "unknown dispenser status",
|
||||
},
|
||||
{
|
||||
@ -696,3 +697,131 @@ func TestPrepareCardAtEncoderRequiresConfirmedEncoderState(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPositionFlags(t *testing.T) {
|
||||
for position := byte(0x30); position <= 0x3F; position++ {
|
||||
t.Run(fmt.Sprintf("%02X", position), func(t *testing.T) {
|
||||
st := status(position)
|
||||
wantEncoder := position&0x02 != 0
|
||||
wantEmpty := position&0x08 != 0
|
||||
if got := isAtEncoderPosition(st); got != wantEncoder {
|
||||
t.Errorf("isAtEncoderPosition(% X) = %t, want %t", st, got, wantEncoder)
|
||||
}
|
||||
if got := isCardWellEmpty(st); got != wantEmpty {
|
||||
t.Errorf("isCardWellEmpty(% X) = %t, want %t", st, got, wantEmpty)
|
||||
}
|
||||
if err := validateDispenserStatusData(st); err != nil {
|
||||
t.Errorf("validateDispenserStatusData(% X) = %v, want nil", st, err)
|
||||
}
|
||||
ready, err := preparationStatus("test", st, true)
|
||||
if ready != wantEncoder {
|
||||
t.Errorf("preparationStatus(% X) ready = %t, want %t", st, ready, wantEncoder)
|
||||
}
|
||||
wantEmptyError := wantEmpty && !wantEncoder
|
||||
if errors.Is(err, ErrCardWellEmpty) != wantEmptyError || (err != nil && !wantEmptyError) {
|
||||
t.Errorf("preparationStatus(% X) error = %v, want empty error %t", st, err, wantEmptyError)
|
||||
}
|
||||
wantStock := ""
|
||||
if wantEmpty {
|
||||
wantStock = "Card empty"
|
||||
}
|
||||
if got := stockTake(st); got != wantStock {
|
||||
t.Errorf("stockTake(% X) = %q, want %q", st, got, wantStock)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncoderPositionPrecedesDiagnostics(t *testing.T) {
|
||||
for _, position := range []byte{0x32, 0x33, 0x36, 0x37, 0x3A, 0x3B, 0x3E, 0x3F} {
|
||||
for _, diagnostics := range [][]byte{
|
||||
{0x30, 0x30, 0x30},
|
||||
{0x32, 0x30, 0x30},
|
||||
{0x36, 0x30, 0x30},
|
||||
{0x34, 0x32, 0x32},
|
||||
{0x30, 0x31, 0x34},
|
||||
{0xFF, 0xFF, 0xFF},
|
||||
} {
|
||||
st := append(append([]byte(nil), diagnostics...), position)
|
||||
for _, operation := range []string{"current", "next"} {
|
||||
t.Run(fmt.Sprintf("%s/% X", operation, st), func(t *testing.T) {
|
||||
client, device := newSequenceTestClient(t, cmdResp{status: st})
|
||||
prepare := client.PrepareCurrentCard
|
||||
if operation == "next" {
|
||||
prepare = client.PrepareNextCard
|
||||
}
|
||||
if _, err := prepare(context.Background()); err != nil {
|
||||
t.Errorf("prepare(% X) = %v, want success", st, err)
|
||||
}
|
||||
want := []cmdType{cmdStatus}
|
||||
if !reflect.DeepEqual(device.commands, want) {
|
||||
t.Errorf("prepare(% X) commands = %v, want %v", st, device.commands, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCombinedEncoderPositionStopsRecovery(t *testing.T) {
|
||||
for _, afterReset := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("afterReset=%t", afterReset), func(t *testing.T) {
|
||||
count := 7 // Encoder confirmation at the recovery threshold, before RS.
|
||||
wantResets := 0
|
||||
if afterReset {
|
||||
count = 8 // Encoder confirmation on the fresh post-reset read.
|
||||
wantResets = 1
|
||||
}
|
||||
responses := append(prepareFailureResponses(0x32, count), cmdResp{status: []byte{0x36, 0x32, 0x34, 0x3F}})
|
||||
client, device := newSequenceTestClient(t, responses...)
|
||||
if _, err := client.PrepareCurrentCard(context.Background()); err != nil {
|
||||
t.Errorf("PrepareCurrentCard(combined encoder) = %v, want success", err)
|
||||
}
|
||||
if got := commandCount(device.commands, cmdReset); got != wantResets {
|
||||
t.Errorf("PrepareCurrentCard(combined encoder) RS count = %d, want %d", got, wantResets)
|
||||
}
|
||||
if got := commandCount(device.commands, cmdToEncoder); got != 1 {
|
||||
t.Errorf("PrepareCurrentCard(combined encoder) FC7 count = %d, want 1", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidPositionEncoding(t *testing.T) {
|
||||
for _, st := range [][]byte{nil, {0x30, 0x30, 0x30}, status(0x02), status(0x2F), status(0x40), status(0x72), status(0xFF)} {
|
||||
if isAtEncoderPosition(st) || isCardWellEmpty(st) {
|
||||
t.Errorf("invalid status % X confirmed encoder or empty, want neither", st)
|
||||
}
|
||||
if ready, err := preparationStatus("test", st, true); ready || err == nil {
|
||||
t.Errorf("preparationStatus(% X) = (%t, %v), want false and error", st, ready, err)
|
||||
}
|
||||
}
|
||||
st := []byte{0xFF, 0xFF, 0xFF, 0x37, 0xFF}
|
||||
if ready, err := preparationStatus("test", st, false); !ready || err != nil {
|
||||
t.Errorf("preparationStatus(% X) = (%t, %v), want encoder precedence", st, ready, err)
|
||||
}
|
||||
st = append(status(0x35), 0xFF)
|
||||
if ready, err := preparationStatus("test", st, true); ready || err == nil {
|
||||
t.Errorf("preparationStatus(% X) = (%t, %v), want malformed error", st, ready, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPositionDescriptions(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
position byte
|
||||
want string
|
||||
}{
|
||||
{0x30, ""},
|
||||
{0x31, "Card at pre-dispense position; "},
|
||||
{0x32, "Card at encoder position; "},
|
||||
{0x34, "Card at mouth position; "},
|
||||
{0x37, "Card at pre-dispense position; Card at encoder position; Card at mouth position; "},
|
||||
{0x39, "Card at pre-dispense position; Card empty; "},
|
||||
{0x3F, "Card at pre-dispense position; Card at encoder position; Card at mouth position; Card empty; "},
|
||||
{0x40, "Unknown status 0x40 at position 4; "},
|
||||
} {
|
||||
if got := statusDescription(status(test.position)); got != test.want {
|
||||
t.Errorf("statusDescription(position %X) = %q, want %q", test.position, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
272
internal/dispenser/transport_test.go
Normal file
272
internal/dispenser/transport_test.go
Normal file
@ -0,0 +1,272 @@
|
||||
package dispenser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Golden frames reconstructed from K720_Dll.dll's big-endian length and XOR
|
||||
// algorithm (SendCmd 0x100050C0, Query 0x10005280, SensorQuery 0x10005420).
|
||||
var vendorFrames = []struct {
|
||||
name string
|
||||
command []byte
|
||||
frame []byte
|
||||
}{
|
||||
{"AP", []byte{2, 'A', 'P'}, []byte{2, 0x30, 0x30, 0, 2, 0x41, 0x50, 3, 0x12}},
|
||||
{"RF", []byte{2, 'R', 'F'}, []byte{2, 0x30, 0x30, 0, 2, 0x52, 0x46, 3, 0x17}},
|
||||
{"FC7", commandFC7, []byte{2, 0x30, 0x30, 0, 3, 0x46, 0x43, 0x37, 3, 0x30}},
|
||||
{"FC0", commandFC0, []byte{2, 0x30, 0x30, 0, 3, 0x46, 0x43, 0x30, 3, 0x37}},
|
||||
{"RS", commandRS, []byte{2, 0x30, 0x30, 0, 2, 0x52, 0x53, 3, 2}},
|
||||
}
|
||||
|
||||
func TestVendorOutboundFrames(t *testing.T) {
|
||||
for _, tc := range vendorFrames {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := createPacket([]byte("00"), tc.command); !bytes.Equal(got, tc.frame) {
|
||||
t.Errorf("createPacket(%s) = % X, want % X", tc.name, got, tc.frame)
|
||||
}
|
||||
if got := calculateBCC(tc.frame[:len(tc.frame)-1]); got != tc.frame[len(tc.frame)-1] {
|
||||
t.Errorf("calculateBCC(%s) = %02X, want %02X", tc.name, got, tc.frame[len(tc.frame)-1])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type scriptedTransport struct {
|
||||
chunks [][]byte
|
||||
writes [][]byte
|
||||
readErr error
|
||||
writeErr error
|
||||
shortWrite int
|
||||
afterRead func()
|
||||
afterWrite func()
|
||||
}
|
||||
|
||||
func (p *scriptedTransport) Read(b []byte) (int, error) {
|
||||
if len(p.chunks) == 0 {
|
||||
if p.readErr != nil {
|
||||
return 0, p.readErr
|
||||
}
|
||||
return 0, io.EOF
|
||||
}
|
||||
n := copy(b, p.chunks[0])
|
||||
p.chunks[0] = p.chunks[0][n:]
|
||||
if len(p.chunks[0]) == 0 {
|
||||
p.chunks = p.chunks[1:]
|
||||
}
|
||||
if p.afterRead != nil {
|
||||
p.afterRead()
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
func (p *scriptedTransport) Write(b []byte) (int, error) {
|
||||
p.writes = append(p.writes, append([]byte(nil), b...))
|
||||
if p.afterWrite != nil {
|
||||
p.afterWrite()
|
||||
}
|
||||
if p.writeErr != nil {
|
||||
return 0, p.writeErr
|
||||
}
|
||||
if p.shortWrite == len(p.writes) {
|
||||
return len(b) - 1, nil
|
||||
}
|
||||
return len(b), nil
|
||||
}
|
||||
func transportAddress(t *testing.T) {
|
||||
t.Helper()
|
||||
old := Address
|
||||
Address = []byte("00")
|
||||
t.Cleanup(func() { Address = old })
|
||||
}
|
||||
|
||||
// Independent SF response vectors: status is 30 30 30 [33].
|
||||
var vendorAP = []byte{2, 0x30, 0x30, 0, 6, 'S', 'F', 0x30, 0x30, 0x30, 0x33, 3, 0x11}
|
||||
var vendorRF = []byte{2, 0x30, 0x30, 0, 5, 'S', 'F', 0x30, 0x30, 0x30, 3, 0x21}
|
||||
var vendorACK = []byte{6, 0x30, 0x30}
|
||||
|
||||
func TestVendorQueryFragmentation(t *testing.T) {
|
||||
transportAddress(t)
|
||||
for i, frame := range [][]byte{vendorAP, vendorRF} {
|
||||
wire := append(append([]byte(nil), vendorACK...), frame...)
|
||||
for split := 1; split < len(wire); split++ {
|
||||
t.Run(fmt.Sprintf("%s/split%d", vendorFrames[i].name, split), func(t *testing.T) {
|
||||
p := &scriptedTransport{chunks: [][]byte{wire[:split], wire[split:]}}
|
||||
got, err := queryStatus(context.Background(), p, vendorFrames[i].command, 4-i, 0)
|
||||
if err != nil || !bytes.Equal(got, frame[7:len(frame)-2]) {
|
||||
t.Fatalf("queryStatus(split=%d) = % X, %v, want % X, nil", split, got, err, frame[7:len(frame)-2])
|
||||
}
|
||||
if len(p.writes) != 2 || !bytes.Equal(p.writes[0], vendorFrames[i].frame) || !bytes.Equal(p.writes[1], []byte{5, 0x30, 0x30}) {
|
||||
t.Errorf("queryStatus writes = % X, want command then ENQ", p.writes)
|
||||
}
|
||||
})
|
||||
}
|
||||
t.Run(vendorFrames[i].name+"/one-byte", func(t *testing.T) {
|
||||
p := &scriptedTransport{}
|
||||
for _, b := range wire {
|
||||
p.chunks = append(p.chunks, []byte{b})
|
||||
}
|
||||
if _, err := queryStatus(context.Background(), p, vendorFrames[i].command, 4-i, 0); err != nil {
|
||||
t.Errorf("queryStatus(one-byte reads) = %v, want nil", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVendorQueryRejectsInvalidFrames(t *testing.T) {
|
||||
transportAddress(t)
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
offset int
|
||||
value byte
|
||||
}{
|
||||
{"STX", 0, 1}, {"address high", 1, '1'}, {"address low", 2, '1'},
|
||||
{"zero length", 4, 0}, {"short length", 4, 5}, {"long length", 4, 7}, {"high length", 3, 0xff},
|
||||
{"type S", 5, 'X'}, {"type F", 6, 'X'}, {"ETX", 11, 4}, {"BCC", 12, 0},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
frame := append([]byte(nil), vendorAP...)
|
||||
frame[tc.offset] = tc.value
|
||||
// Keep checksum valid when testing type/ETX to isolate those checks.
|
||||
if tc.offset == 5 || tc.offset == 6 || tc.offset == 11 {
|
||||
frame[12] = 0
|
||||
for _, b := range frame[:12] {
|
||||
frame[12] ^= b
|
||||
}
|
||||
}
|
||||
p := &scriptedTransport{chunks: [][]byte{vendorACK, frame}}
|
||||
if got, err := queryStatus(context.Background(), p, vendorFrames[0].command, 4, 0); err == nil || got != nil {
|
||||
t.Errorf("queryStatus(%s) = % X, %v, want nil/error", tc.name, got, err)
|
||||
}
|
||||
if len(p.writes) != 2 {
|
||||
t.Errorf("queryStatus(%s) writes=%d, want 2 without resend", tc.name, len(p.writes))
|
||||
}
|
||||
})
|
||||
}
|
||||
for n := 0; n < len(vendorAP); n++ {
|
||||
t.Run(fmt.Sprintf("truncated%d", n), func(t *testing.T) {
|
||||
p := &scriptedTransport{chunks: [][]byte{vendorACK, vendorAP[:n]}}
|
||||
if _, err := queryStatus(context.Background(), p, vendorFrames[0].command, 4, 0); err == nil {
|
||||
t.Errorf("queryStatus(%d-byte frame) succeeded, want error", n)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransportACKAndMechanicalDispatch(t *testing.T) {
|
||||
transportAddress(t)
|
||||
for _, tc := range vendorFrames[2:] {
|
||||
p := &scriptedTransport{chunks: [][]byte{{6}, {'0'}, {'0'}}}
|
||||
if err := dispatchCommand(context.Background(), p, tc.command, 0); err != nil {
|
||||
t.Fatalf("dispatchCommand(%s)=%v, want nil", tc.name, err)
|
||||
}
|
||||
if len(p.writes) != 2 || !bytes.Equal(p.writes[0], tc.frame) || !bytes.Equal(p.writes[1], []byte{5, '0', '0'}) {
|
||||
t.Errorf("dispatchCommand(%s) writes=% X, want command then ENQ", tc.name, p.writes)
|
||||
}
|
||||
}
|
||||
for _, ack := range [][]byte{{0x15, '0', '0'}, {6, '1', '0'}, {6, '0', '1'}, {6}, {6, '0'}, {}} {
|
||||
p := &scriptedTransport{chunks: [][]byte{ack}}
|
||||
if err := dispatchCommand(context.Background(), p, commandFC7, 0); err == nil {
|
||||
t.Errorf("dispatchCommand(ACK=% X) succeeded, want error", ack)
|
||||
}
|
||||
if len(p.writes) != 1 {
|
||||
t.Errorf("dispatchCommand(ACK=% X) writes=%d, want 1", ack, len(p.writes))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransportShortWritesAndErrors(t *testing.T) {
|
||||
transportAddress(t)
|
||||
failure := errors.New("serial failure")
|
||||
for _, stage := range []int{1, 2} {
|
||||
p := &scriptedTransport{chunks: [][]byte{vendorACK}, shortWrite: stage}
|
||||
if err := dispatchCommand(context.Background(), p, commandFC7, 0); !errors.Is(err, io.ErrShortWrite) {
|
||||
t.Errorf("dispatchCommand(short write %d)=%v, want ErrShortWrite", stage, err)
|
||||
}
|
||||
if len(p.writes) != stage {
|
||||
t.Errorf("short write %d writes=%d, want %d", stage, len(p.writes), stage)
|
||||
}
|
||||
}
|
||||
for _, p := range []*scriptedTransport{{writeErr: failure}, {readErr: failure}, {chunks: [][]byte{{}}}} {
|
||||
if err := dispatchCommand(context.Background(), p, commandRS, 0); err == nil {
|
||||
t.Error("dispatchCommand(I/O failure) succeeded, want error")
|
||||
}
|
||||
if len(p.writes) != 1 {
|
||||
t.Errorf("dispatchCommand(I/O failure) writes=%d, want 1", len(p.writes))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransportCancellation(t *testing.T) {
|
||||
transportAddress(t)
|
||||
for _, stage := range []string{"before write", "processing wait", "after ACK", "after ENQ", "after header"} {
|
||||
t.Run(stage, func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
p := &scriptedTransport{chunks: [][]byte{vendorACK, vendorAP}}
|
||||
wantWrites := 1
|
||||
switch stage {
|
||||
case "before write":
|
||||
cancel()
|
||||
wantWrites = 0
|
||||
case "processing wait":
|
||||
p.afterWrite = cancel
|
||||
case "after ACK":
|
||||
p.afterRead = cancel
|
||||
case "after ENQ":
|
||||
wantWrites = 2
|
||||
p.afterWrite = func() {
|
||||
if len(p.writes) == 2 {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
case "after header":
|
||||
wantWrites = 2
|
||||
reads := 0
|
||||
p.afterRead = func() {
|
||||
reads++
|
||||
if reads == 2 {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
if _, err := queryStatus(ctx, p, vendorFrames[0].command, 4, 0); !errors.Is(err, context.Canceled) {
|
||||
t.Errorf("queryStatus(cancel %s)=%v, want Canceled", stage, err)
|
||||
}
|
||||
if len(p.writes) != wantWrites {
|
||||
t.Errorf("queryStatus(cancel %s) writes=%d, want %d", stage, len(p.writes), wantWrites)
|
||||
}
|
||||
})
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
|
||||
defer cancel()
|
||||
p := &scriptedTransport{}
|
||||
if err := dispatchCommand(ctx, p, commandRS, time.Second); !errors.Is(err, context.DeadlineExceeded) {
|
||||
t.Errorf("dispatchCommand(deadline in wait)=%v, want DeadlineExceeded", err)
|
||||
}
|
||||
if len(p.writes) != 1 {
|
||||
t.Errorf("dispatchCommand(deadline) writes=%d, want 1", len(p.writes))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransportTrailingDataNotConsumedAsStatus(t *testing.T) {
|
||||
transportAddress(t)
|
||||
frame := append(append([]byte(nil), vendorAP...), 0xff, 0xfe, 0xfd)
|
||||
p := &scriptedTransport{chunks: [][]byte{vendorACK, frame}}
|
||||
if _, err := queryStatus(context.Background(), p, vendorFrames[0].command, 4, 0); err != nil {
|
||||
t.Fatalf("queryStatus(frame with trailing bytes)=%v, want nil", err)
|
||||
}
|
||||
if len(p.chunks) != 1 || !bytes.Equal(p.chunks[0], []byte{0xff, 0xfe, 0xfd}) {
|
||||
t.Fatalf("remaining bytes=% X, want FF FE FD", p.chunks)
|
||||
}
|
||||
if err := dispatchCommand(context.Background(), p, commandFC7, 0); err == nil {
|
||||
t.Error("dispatchCommand(trailing garbage) succeeded, want invalid ACK")
|
||||
}
|
||||
if len(p.writes) != 3 {
|
||||
t.Errorf("writes after trailing garbage=%d, want 3 (no ENQ/resend)", len(p.writes))
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user