fix(dispenser): retry on transient prepare failure
This commit is contained in:
parent
1969aaa1d2
commit
aeb86f55b9
@ -33,7 +33,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
buildVersion = "v2.0.1"
|
buildVersion = "v2.0.2"
|
||||||
serviceName = "hardlink"
|
serviceName = "hardlink"
|
||||||
pollingFrequency = 8 * time.Second
|
pollingFrequency = 8 * time.Second
|
||||||
)
|
)
|
||||||
|
|||||||
@ -201,10 +201,14 @@ func (c *Client) handle(req cmdReq) {
|
|||||||
|
|
||||||
case cmdToEncoder:
|
case cmdToEncoder:
|
||||||
err := cardToEncoderPosition(c.port)
|
err := cardToEncoderPosition(c.port)
|
||||||
|
// A movement command makes any previously cached position unreliable.
|
||||||
|
c.invalidateStatusCache()
|
||||||
req.respCh <- cmdResp{err: err}
|
req.respCh <- cmdResp{err: err}
|
||||||
|
|
||||||
case cmdOutOfMouth:
|
case cmdOutOfMouth:
|
||||||
err := cardOutOfMouth(c.port)
|
err := cardOutOfMouth(c.port)
|
||||||
|
// A movement command makes any previously cached position unreliable.
|
||||||
|
c.invalidateStatusCache()
|
||||||
req.respCh <- cmdResp{err: err}
|
req.respCh <- cmdResp{err: err}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@ -246,6 +250,13 @@ func (c *Client) CheckStatus(ctx context.Context) ([]byte, error) {
|
|||||||
return c.do(ctx, cmdStatus)
|
return c.do(ctx, cmdStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) invalidateStatusCache() {
|
||||||
|
c.mu.Lock()
|
||||||
|
c.lastStatus = nil
|
||||||
|
c.lastStatusT = time.Time{}
|
||||||
|
c.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) ToEncoder(ctx context.Context) error {
|
func (c *Client) ToEncoder(ctx context.Context) error {
|
||||||
_, err := c.do(ctx, cmdToEncoder)
|
_, err := c.do(ctx, cmdToEncoder)
|
||||||
return err
|
return err
|
||||||
@ -339,6 +350,27 @@ func preparationStatus(operation string, status []byte) (bool, error) {
|
|||||||
if isPreparationMoving(status) {
|
if isPreparationMoving(status) {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// independent hard errors reported in the other status bytes.
|
||||||
|
if status[0] == 0x32 {
|
||||||
|
statusWithoutPrepareFailure := append([]byte(nil), status...)
|
||||||
|
statusWithoutPrepareFailure[0] = 0x30
|
||||||
|
if err := dispenserStatusError(statusWithoutPrepareFailure); err != nil {
|
||||||
|
return false, fmt.Errorf("[%s] %w", operation, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Warnf(
|
||||||
|
"[%s] transient Preparing card fails; waiting for encoder position, raw status: % X",
|
||||||
|
operation,
|
||||||
|
status,
|
||||||
|
)
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
if err := dispenserStatusError(status); err != nil {
|
if err := dispenserStatusError(status); err != nil {
|
||||||
return false, fmt.Errorf("[%s] %w", operation, err)
|
return false, fmt.Errorf("[%s] %w", operation, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -147,6 +147,40 @@ func TestPrepareCurrentCardAcceptsEncoderPositionWithStaleDiagnostics(t *testing
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPrepareCurrentCardToleratesTransientPrepareFailureUntilEncoder(t *testing.T) {
|
||||||
|
client, device := newSequenceTestClient(t,
|
||||||
|
cmdResp{status: []byte{0x32, 0x30, 0x30, 0x30}},
|
||||||
|
cmdResp{status: []byte{0x32, 0x30, 0x30, 0x30}},
|
||||||
|
cmdResp{status: []byte{0x32, 0x30, 0x30, 0x30}},
|
||||||
|
cmdResp{status: []byte{0x32, 0x30, 0x30, 0x33}},
|
||||||
|
)
|
||||||
|
|
||||||
|
stock, err := client.PrepareCurrentCard(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if stock != "Preparing card fails" {
|
||||||
|
t.Fatalf("stock status = %q, want Preparing card fails", stock)
|
||||||
|
}
|
||||||
|
if got := commandCount(device.commands, cmdToEncoder); got != 1 {
|
||||||
|
t.Fatalf("to-encoder commands = %d, want 1", got)
|
||||||
|
}
|
||||||
|
if got := commandCount(device.commands, cmdStatus); got != 4 {
|
||||||
|
t.Fatalf("status reads = %d, want 4", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrepareCurrentCardDoesNotMaskHardErrorAlongsideTransientPrepareFailure(t *testing.T) {
|
||||||
|
client, _ := newSequenceTestClient(t,
|
||||||
|
cmdResp{status: []byte{0x32, 0x30, 0x32, 0x30}},
|
||||||
|
)
|
||||||
|
|
||||||
|
_, err := client.PrepareCurrentCard(context.Background())
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "Card jammed") {
|
||||||
|
t.Fatalf("error = %v, want Card jammed", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPrepareCurrentCardPollingAllowsTransientMovementWithStaleErrors(t *testing.T) {
|
func TestPrepareCurrentCardPollingAllowsTransientMovementWithStaleErrors(t *testing.T) {
|
||||||
client, device := newSequenceTestClient(t,
|
client, device := newSequenceTestClient(t,
|
||||||
cmdResp{status: status(0x34)},
|
cmdResp{status: status(0x34)},
|
||||||
|
|||||||
@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
builtVersion is a const in main.go
|
builtVersion is a const in main.go
|
||||||
|
|
||||||
|
#### v2.0.2 - 22 September 2026
|
||||||
|
fix(dispenser): retry on transient prepare failure
|
||||||
|
|
||||||
#### v2.0.1 - 10 September 2026
|
#### v2.0.1 - 10 September 2026
|
||||||
feat: add CreditCall preauth streaming
|
feat: add CreditCall preauth streaming
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user