Get-Service TeamViewer | Select-Object Status, Name, StartType

This commit is contained in:
yurii 2026-09-26 17:15:02 +01:00
parent 4dfe139c3b
commit cb5a09e710
5 changed files with 570 additions and 107 deletions

1
.gitignore vendored
View File

@ -29,6 +29,7 @@ _obj
_test
.vscode/
ChipDNAClient/
docs/
# Architecture specific extensions/prefixes
*.[568vq]

View File

@ -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
func checkDispenserStatus(ctx context.Context, port serialTransport) ([]byte, error) {
return queryStatus(ctx, port, []byte{0x02, 'A', 'P'}, 4, delay)
}
statusResp, err = sendAndReceive(port, enq, delay)
if err != nil {
return nil, fmt.Errorf("error sending ENQ: %w", err)
// 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
}
if len(statusResp) < 13 {
return nil, fmt.Errorf("incomplete status response from dispenser: % X", statusResp)
}
return statusResp[7:11], nil
return writePacket(ctx, port, append([]byte{ENQ}, Address...))
}
func cardToEncoderPosition(port *serial.Port) error {
enq := 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
return dispatchCommand(ctx, port, commandFC7, delay)
}
_, err = port.Write(enq)
if err != nil {
return fmt.Errorf("error sending ENQ to prompt device: %w", err)
}
return nil
func resetDispenser(ctx context.Context, port serialTransport) error {
return dispatchCommand(ctx, port, commandRS, 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 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)
}

View File

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

View File

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

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