<commit_before>package coin

import (
	"context"
	"encoding/binary"
	"fmt"
	"log"
	"sync"
	"time"

	"github.com/temoto/alive"
	"github.com/temoto/vender/currency"
	"github.com/temoto/vender/hardware/mdb"
	"github.com/temoto/vender/hardware/money"
)

const (
	coinTypeCount = 16

	DelayErr  = 500 * time.Millisecond
	DelayNext = 200 * time.Millisecond
)

//go:generate stringer -type=CoinRouting -trimprefix=Routing
type CoinRouting uint8

const (
	RoutingCashBox CoinRouting = 0
	RoutingTubes   CoinRouting = 1
	RoutingNotUsed CoinRouting = 2
	RoutingReject  CoinRouting = 3
)

//go:generate stringer -type=Features -trimprefix=Feature
type Features uint32

const (
	FeatureAlternativePayout Features = 1 << iota
	FeatureExtendedDiagnostic
	FeatureControlledManualFillPayout
	FeatureFTL
)

type CoinAcceptor struct {
	mdb       mdb.Mdber
	byteOrder binary.ByteOrder

	// Indicates the value of the bill types 0 to 15.
	// These are final values including all scaling factors.
	coinTypeCredit []currency.Nominal

	coinTypeRouting uint16

	featureLevel      uint8
	supportedFeatures Features

	internalScalingFactor int
	batch                 sync.Mutex
	ready                 chan struct{}
}

var (
	packetReset      = mdb.PacketFromHex("08")
	packetSetup      = mdb.PacketFromHex("09")
	packetTubeStatus = mdb.PacketFromHex("0a")
	packetPoll       = mdb.PacketFromHex("0b")
	packetExpIdent   = mdb.PacketFromHex("0f00")
	packetDiagStatus = mdb.PacketFromHex("0f05")
)

var (
	ErrNoCredit      = fmt.Errorf("No Credit")
	ErrDoubleArrival = fmt.Errorf("Double Arrival")
	ErrCoinRouting   = fmt.Errorf("Coin Routing")
	ErrCoinJam       = fmt.Errorf("Coin Jam")
	ErrSlugs         = fmt.Errorf("Slugs")
)

// usage: defer coin.Batch()()
func (self *CoinAcceptor) Batch() func() {
	self.batch.Lock()
	return self.batch.Unlock
}

func (self *CoinAcceptor) Init(ctx context.Context, mdber mdb.Mdber) error {
	// TODO read config
	self.byteOrder = binary.BigEndian
	self.coinTypeCredit = make([]currency.Nominal, coinTypeCount)
	self.mdb = mdber
	self.internalScalingFactor = 1 // FIXME
	self.ready = make(chan struct{})
	// TODO maybe execute CommandReset then wait for StatusWasReset
	err := self.InitSequence()
	if err != nil {
		log.Printf("hardware/mdb/coin/InitSequence error=%s", err)
	}
	return err
}

func (self *CoinAcceptor) SupportedNominals() []currency.Nominal {
	ns := make([]currency.Nominal, 0, len(self.coinTypeCredit))
	for _, n := range self.coinTypeCredit {
		if n > 0 {
			ns = append(ns, n)
		}
	}
	return ns
}

func (self *CoinAcceptor) Run(ctx context.Context, a *alive.Alive, ch chan<- money.PollResult) {
	stopch := a.StopChan()
	for a.IsRunning() {
		// TODO to reuse single PollResult safely, must clone .Items before sending to chan
		pr := money.NewPollResult(mdb.PacketMaxLength)
		if err := self.CommandPoll(pr); err != nil {
			log.Printf("coin.Run CommandPoll err=%v", err)
			if pr.Delay == 0 {
				pr.Delay = DelayErr
			}
		} else {
			select {
			case ch <- *pr:
			case <-stopch:
				return
			}
		}
		select {
		case <-time.After(pr.Delay):
		case <-stopch:
			return
		}
	}
}

func (self *CoinAcceptor) ReadyChan() <-chan struct{} {
	return self.ready
}

func (self *CoinAcceptor) InitSequence() error {
	defer self.Batch()()

	err := self.CommandSetup()
	if err != nil {
		return err
	}
	if err = self.CommandExpansionIdentification(); err != nil {
		return err
	}
	if err = self.CommandFeatureEnable(FeatureExtendedDiagnostic); err != nil {
		return err
	}
	diagResult := new(DiagResult)
	if err = self.CommandExpansionSendDiagStatus(diagResult); err != nil {
		return err
	}
	if err = self.CommandTubeStatus(); err != nil {
		return err
	}
	// TODO read config
	if err = self.CommandCoinType(0xffff, 0xffff); err != nil {
		return err
	}
	return nil
}

func (self *CoinAcceptor) CommandReset() error {
	return self.mdb.Tx(packetReset, new(mdb.Packet))
}

func (self *CoinAcceptor) CommandSetup() error {
	const expectLengthMin = 7
	request := packetSetup
	response := new(mdb.Packet)
	err := self.mdb.Tx(request, response)
	if err != nil {
		log.Printf("mdb request=%s err=%v", request.Format(), err)
		return err
	}
	log.Printf("setup response=(%d)%s", response.Len(), response.Format())
	bs := response.Bytes()
	if len(bs) < expectLengthMin {
		return fmt.Errorf("hardware/mdb/coin SETUP response=%s expected >= %d bytes", response.Format(), expectLengthMin)
	}
	self.featureLevel = bs[0]
	scalingFactor := bs[3]
	self.coinTypeRouting = self.byteOrder.Uint16(bs[5 : 5+2])
	for i, sf := range bs[7:] {
		n := currency.Nominal(sf) * currency.Nominal(scalingFactor) * currency.Nominal(self.internalScalingFactor)
		log.Printf("i=%d sf=%d nominal=%s", i, sf, currency.Amount(n).Format100I())
		self.coinTypeCredit[i] = n
	}
	log.Printf("Changer Feature Level: %d", self.featureLevel)
	log.Printf("Country / Currency Code: %x", bs[1:1+2])
	log.Printf("Coin Scaling Factor: %d", scalingFactor)
	log.Printf("Decimal Places: %d", bs[4])
	log.Printf("Coin Type Routing: %b", self.coinTypeRouting)
	log.Printf("Coin Type Credit: %x %#v", bs[7:], self.coinTypeCredit)
	return nil
}
<commit_msg>mdb coin SETUP limit coin-type-credit byte range<commit_after>package coin

import (
	"context"
	"encoding/binary"
	"fmt"
	"log"
	"sync"
	"time"

	"github.com/temoto/alive"
	"github.com/temoto/vender/currency"
	"github.com/temoto/vender/hardware/mdb"
	"github.com/temoto/vender/hardware/money"
)

const (
	coinTypeCount = 16

	DelayErr  = 500 * time.Millisecond
	DelayNext = 200 * time.Millisecond
)

//go:generate stringer -type=CoinRouting -trimprefix=Routing
type CoinRouting uint8

const (
	RoutingCashBox CoinRouting = 0
	RoutingTubes   CoinRouting = 1
	RoutingNotUsed CoinRouting = 2
	RoutingReject  CoinRouting = 3
)

//go:generate stringer -type=Features -trimprefix=Feature
type Features uint32

const (
	FeatureAlternativePayout Features = 1 << iota
	FeatureExtendedDiagnostic
	FeatureControlledManualFillPayout
	FeatureFTL
)

type CoinAcceptor struct {
	mdb       mdb.Mdber
	byteOrder binary.ByteOrder

	// Indicates the value of the bill types 0 to 15.
	// These are final values including all scaling factors.
	coinTypeCredit []currency.Nominal

	coinTypeRouting uint16

	featureLevel      uint8
	supportedFeatures Features

	internalScalingFactor int
	batch                 sync.Mutex
	ready                 chan struct{}
}

var (
	packetReset      = mdb.PacketFromHex("08")
	packetSetup      = mdb.PacketFromHex("09")
	packetTubeStatus = mdb.PacketFromHex("0a")
	packetPoll       = mdb.PacketFromHex("0b")
	packetExpIdent   = mdb.PacketFromHex("0f00")
	packetDiagStatus = mdb.PacketFromHex("0f05")
)

var (
	ErrNoCredit      = fmt.Errorf("No Credit")
	ErrDoubleArrival = fmt.Errorf("Double Arrival")
	ErrCoinRouting   = fmt.Errorf("Coin Routing")
	ErrCoinJam       = fmt.Errorf("Coin Jam")
	ErrSlugs         = fmt.Errorf("Slugs")
)

// usage: defer coin.Batch()()
func (self *CoinAcceptor) Batch() func() {
	self.batch.Lock()
	return self.batch.Unlock
}

func (self *CoinAcceptor) Init(ctx context.Context, mdber mdb.Mdber) error {
	// TODO read config
	self.byteOrder = binary.BigEndian
	self.coinTypeCredit = make([]currency.Nominal, coinTypeCount)
	self.mdb = mdber
	self.internalScalingFactor = 1 // FIXME
	self.ready = make(chan struct{})
	// TODO maybe execute CommandReset then wait for StatusWasReset
	err := self.InitSequence()
	if err != nil {
		log.Printf("hardware/mdb/coin/InitSequence error=%s", err)
	}
	return err
}

func (self *CoinAcceptor) SupportedNominals() []currency.Nominal {
	ns := make([]currency.Nominal, 0, len(self.coinTypeCredit))
	for _, n := range self.coinTypeCredit {
		if n > 0 {
			ns = append(ns, n)
		}
	}
	return ns
}

func (self *CoinAcceptor) Run(ctx context.Context, a *alive.Alive, ch chan<- money.PollResult) {
	stopch := a.StopChan()
	for a.IsRunning() {
		// TODO to reuse single PollResult safely, must clone .Items before sending to chan
		pr := money.NewPollResult(mdb.PacketMaxLength)
		if err := self.CommandPoll(pr); err != nil {
			log.Printf("coin.Run CommandPoll err=%v", err)
			if pr.Delay == 0 {
				pr.Delay = DelayErr
			}
		} else {
			select {
			case ch <- *pr:
			case <-stopch:
				return
			}
		}
		select {
		case <-time.After(pr.Delay):
		case <-stopch:
			return
		}
	}
}

func (self *CoinAcceptor) ReadyChan() <-chan struct{} {
	return self.ready
}

func (self *CoinAcceptor) InitSequence() error {
	defer self.Batch()()

	err := self.CommandSetup()
	if err != nil {
		return err
	}
	if err = self.CommandExpansionIdentification(); err != nil {
		return err
	}
	if err = self.CommandFeatureEnable(FeatureExtendedDiagnostic); err != nil {
		return err
	}
	diagResult := new(DiagResult)
	if err = self.CommandExpansionSendDiagStatus(diagResult); err != nil {
		return err
	}
	if err = self.CommandTubeStatus(); err != nil {
		return err
	}
	// TODO read config
	if err = self.CommandCoinType(0xffff, 0xffff); err != nil {
		return err
	}
	return nil
}

func (self *CoinAcceptor) CommandReset() error {
	return self.mdb.Tx(packetReset, new(mdb.Packet))
}

func (self *CoinAcceptor) CommandSetup() error {
	const expectLengthMin = 7
	request := packetSetup
	response := new(mdb.Packet)
	err := self.mdb.Tx(request, response)
	if err != nil {
		log.Printf("mdb request=%s err=%v", request.Format(), err)
		return err
	}
	log.Printf("setup response=(%d)%s", response.Len(), response.Format())
	bs := response.Bytes()
	if len(bs) < expectLengthMin {
		return fmt.Errorf("hardware/mdb/coin SETUP response=%s expected >= %d bytes", response.Format(), expectLengthMin)
	}
	self.featureLevel = bs[0]
	scalingFactor := bs[3]
	self.coinTypeRouting = self.byteOrder.Uint16(bs[5 : 5+2])
	for i, sf := range bs[7 : 7+16] {
		n := currency.Nominal(sf) * currency.Nominal(scalingFactor) * currency.Nominal(self.internalScalingFactor)
		log.Printf("i=%d sf=%d nominal=%s", i, sf, currency.Amount(n).Format100I())
		self.coinTypeCredit[i] = n
	}
	log.Printf("Changer Feature Level: %d", self.featureLevel)
	log.Printf("Country / Currency Code: %x", bs[1:1+2])
	log.Printf("Coin Scaling Factor: %d", scalingFactor)
	log.Printf("Decimal Places: %d", bs[4])
	log.Printf("Coin Type Routing: %b", self.coinTypeRouting)
	log.Printf("Coin Type Credit: %x %#v", bs[7:], self.coinTypeCredit)
	return nil
}

{'fraction_non_alphanumeric': 0.13157638821355636, 'fraction_numerical': 0.011378002528445006, 'mean_word_length': 3.9995138551288285, 'pattern_counts': {'":': 0, '<': 21, '<?xml version=': 0, '>': 7, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4860095', 'n_tokens_mistral': 3865, 'n_tokens_neox': 3636, 'n_words': 865}