Skip to content

Instantly share code, notes, and snippets.

@cema-sp
Created July 26, 2017 20:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save cema-sp/3c3382282cd3f64bed736071878e9730 to your computer and use it in GitHub Desktop.
Save cema-sp/3c3382282cd3f64bed736071878e9730 to your computer and use it in GitHub Desktop.
Part of TCR driver utility
package cli
import (
"bufio"
"encoding/json"
"fmt"
"os"
"quantum/hart/tcr/hart"
"quantum/hart/tcr/messages"
"strings"
)
func Welcome() {
fmt.Println("\n" +
"\t\tTCR CLI\n" +
"\tFor help type 'help'\n" +
"\tFor quit type 'quit'\n" +
"\n")
}
func Help() {
fmt.Println("\n" +
"\thelp\t\t- this message;\n" +
"\tquit | exit\t- quit CLI;\n" +
"\tlist\t\t- print Hart commands;\n" +
"\t<cmd> <params>\t- send <cmd> command with <params> to TCR,\n" +
"\t\t\t <params> should be in JSON format.\n" +
"\n")
}
// Parse JSON to cmdParams
func stringToParams(str string) (parsed messages.Params, err error) {
err = json.Unmarshal([]byte(str), &parsed)
return
}
func Loop(recycler *hart.HartTCR, out chan messages.Response) {
go func() {
for response := range out {
fmt.Println("---->", response, "<----")
}
}()
Welcome()
cliReader := bufio.NewReader(os.Stdin)
Loop:
for {
fmt.Print("> ")
line, err := cliReader.ReadString('\n')
if err != nil {
fmt.Println(err)
break
}
// Remove newline
line = strings.TrimSpace(line)
// Check for empty string
if len(line) < 1 {
continue
}
var pars messages.Params
lineParts := strings.SplitN(line, " ", 2)
cmdName, parsStr := lineParts[0], ""
if len(lineParts) > 1 {
parsStr = lineParts[1]
pars, err = stringToParams(parsStr)
if err != nil {
fmt.Printf("Invalid parameters: %s\n", parsStr)
continue
}
}
switch cmdName {
case "help":
Help()
case "quit", "exit":
break Loop
case "list":
fmt.Println(recycler.PrintCommands())
default:
recycler.Perform(messages.Command{cmdName, pars})
}
}
}
package hart
import (
"fmt"
"log"
"os"
"quantum/hart/tcr/connection"
"quantum/hart/tcr/messages"
"strings"
"time"
)
const (
STX = "\x02" // Start of message
ETX = "\x03" // End of message
ACK = "\x06" // Success
NAK = "\x15" // Failure
)
// "HartTCR" represents Hart Cashfenix THR250
type HartTCR struct {
connection.Connectable
username string
password string
accessLevel string
logLevel int
logger *log.Logger
// messageHeader string
emulate bool
inBuffer []byte
toPerformer chan messages.Command
sentCommands chan messages.Command
toDevice chan string
inACKNAK chan string
inMSG chan string
out chan messages.Response
}
func New(con connection.Connection, freq time.Duration, logLevel int, emulate bool) (*HartTCR, chan messages.Response) {
t := &HartTCR{}
t.Connection = con
t.logLevel = logLevel
t.logger = log.New(os.Stdout, "Hart: ", log.LstdFlags)
t.toPerformer = make(chan messages.Command, 10)
t.sentCommands = make(chan messages.Command, 10)
t.toDevice = make(chan string, 10)
t.inACKNAK = make(chan string, 10)
t.inMSG = make(chan string, 10)
t.out = make(chan messages.Response, 10)
hartCommandCodes = make(map[string]string, len(hartCommands))
for k, v := range hartCommands {
hartCommandCodes[v] = k
}
t.emulate = emulate
go t.Sender()
go t.Receiver(freq)
go t.Performer()
go t.acknakListener()
go t.msgListener()
return t, t.out
}
// "Login" sets login & password for TCR headers.
func (t *HartTCR) Login(username string, password string, accessLevel string) {
t.username = username
t.password = password
t.accessLevel = accessLevel
}
// "Sender" sends string from "toDevice" channel to connection.
func (t *HartTCR) Sender() {
t.log(3, "Sender started")
for command := range t.toDevice {
t.log(2, "->\tOUT\t\t: %s", command)
if t.emulate {
continue
}
err := t.Connection.Send([]byte(command))
if err != nil {
switch err.(type) {
case *connection.ErrOpenPort:
t.out <- messages.Response{Error: NewInternalError("01", err.Error())}
t.log(1, err.Error())
default:
t.out <- messages.Response{Error: NewInternalError("99", err.Error())}
t.log(1, err.Error())
}
}
}
}
// "Receiver" reads data from connection and calls "parseInBuffer()".
func (t *HartTCR) Receiver(freq time.Duration) {
t.log(3, "Receiver started")
for _ = range time.Tick(freq) {
if t.emulate {
continue
}
data, err := t.Connection.Receive()
if err != nil {
switch err.(type) {
case *connection.ErrOpenPort:
t.out <- messages.Response{Error: NewInternalError("01", err.Error())}
t.log(1, err.Error())
default:
t.out <- messages.Response{Error: NewInternalError("99", err.Error())}
t.log(1, err.Error())
}
continue
}
t.log(2, "<-\tIN\t\t: %s", string(data))
t.inBuffer = append(t.inBuffer, data...)
t.parseInBuffer()
}
}
func (t *HartTCR) acknakListener() {
t.log(3, "acknakListener started")
for msg := range t.inACKNAK {
cmd := messages.Command{Name: "Unknown"}
select {
case cmd = <-t.sentCommands: // set cmd to previously sent
default: // leave cmd empty
}
resp := messages.Response{Command: cmd}
switch msg {
case ACK: // leave err nil
resp.Result = messages.Params{"Acknowledgment": "ACK"}
t.out <- resp
logSuccess(resp)
case NAK: // leave err nil
resp.Result = messages.Params{"Acknowledgment": "NAK"}
t.out <- resp
logFailure(resp)
}
}
}
func (t *HartTCR) msgListener() {
t.log(3, "msgListener started")
for msg := range t.inMSG {
event, prefix, data, err := t.parseMessageAndBody(msg)
if err != nil {
t.log(1, "Received invalid message: {\"Event\": %v, \"Prefix\": %q, \"Data\": %q, \"Error\": %q}",
event,
prefix,
data,
err)
continue
}
resp := messages.Response{
Command: messages.Command{
Name: hartCommandCodes[prefix],
},
Event: event}
switch resp.Command.Name {
case "getState":
resp = t.getStateCheckResult(resp, data)
case "detail":
resp = t.detailCheckResult(resp, data)
case "startSession":
resp = t.startSessionCheckResult(resp, data)
case "endSession":
resp = t.endSessionCheckResult(resp, data)
case "occupy":
resp = t.occupyCheckResult(resp, data)
case "cancelOccupy":
resp = t.cancelOccupyCheckResult(resp, data)
case "startDeposit":
resp = t.startDepositCheckResult(resp, data)
case "deposit":
resp = t.depositCheckResult(resp, data)
case "endDeposit":
resp = t.endDepositCheckResult(resp, data)
case "cancelDeposit":
resp = t.cancelDepositCheckResult(resp, data)
case "dispense":
resp = t.dispenseCheckResult(resp, data)
case "cancelDispense":
resp = t.cancelDispenseCheckResult(resp, data)
case "present":
resp = t.presentCheckResult(resp, data)
case "retract":
resp = t.retractCheckResult(resp, data)
case "reset":
resp = t.resetCheckResult(resp, data)
case "sessionError":
// do nothing
default: // for "" prefix
resp.Error = NewInternalError("90")
}
t.out <- resp
logResult(resp)
}
}
// "parseInBuffer" iterates through inBuffer and parses ACK, NAK and
// valid messages.
// This function puts ACK & NAK in inACKNAK channel and messages to inMSG.
func (t *HartTCR) parseInBuffer() {
var (
insideMessage bool
stxIndex int
etxIndex int
messageBuffer []byte
)
resetVars := func() {
insideMessage = false
stxIndex = -1
etxIndex = -1
messageBuffer = make([]byte, 0)
}
resetVars()
for i, value := range t.inBuffer {
if insideMessage {
messageBuffer = append(messageBuffer, value)
switch {
case etxIndex > stxIndex:
t.inMSG <- string(messageBuffer)
t.inBuffer = t.inBuffer[i+1:]
resetVars()
case string(value) == ETX:
etxIndex = i
}
} else {
switch string(value) {
case ACK, NAK:
t.inACKNAK <- string(value)
t.inBuffer = t.inBuffer[i+1:]
case STX:
insideMessage = true
stxIndex = i
messageBuffer = append(messageBuffer, value)
default:
// pass value
}
}
}
}
// "Perform" puts Command in channel for Performer to take it.
func (t *HartTCR) Perform(cmd messages.Command) {
t.toPerformer <- cmd
}
// "Performer" takes one Command from channel and performs actins.
func (t *HartTCR) Performer() {
t.log(3, "Performer started")
for cmd := range t.toPerformer {
if _, ok := hartCommands[cmd.Name]; ok {
commandData := ""
switch cmd.Name {
case "getState":
commandData = t.getStateData(cmd.Pars)
case "detail":
commandData = t.detailData(cmd.Pars)
case "startSession":
commandData = t.startSessionData(cmd.Pars)
case "endSession":
commandData = t.endSessionData(cmd.Pars)
case "occupy":
commandData = t.occupyData(cmd.Pars)
case "cancelOccupy":
commandData = t.cancelOccupyData(cmd.Pars)
case "startDeposit":
commandData = t.startDepositData(cmd.Pars)
// t.sentCommands <- cmd
case "deposit":
commandData = t.depositData(cmd.Pars)
case "endDeposit":
commandData = t.endDepositData(cmd.Pars)
case "cancelDeposit":
commandData = t.cancelDepositData(cmd.Pars)
case "dispense":
commandData = t.dispenseData(cmd.Pars)
case "cancelDispense":
commandData = t.cancelDispenseData(cmd.Pars)
case "present":
commandData = t.presentData(cmd.Pars)
case "retract":
commandData = t.retractData(cmd.Pars)
case "reset":
commandData = t.resetData(cmd.Pars)
default:
break
}
// Send message
t.toDevice <- t.composeMessage(hartCommands[cmd.Name], commandData)
logStart(cmd)
if t.emulate {
t.emulateResponse(cmd.Name, cmd.Pars)
}
} else {
switch cmd.Name {
case "cashOut":
t.Perform(messages.Command{Name: "startSession"})
t.Perform(messages.Command{Name: "occupy"})
t.Perform(messages.Command{Name: "cancelOccupy"})
t.Perform(messages.Command{Name: "endSession"})
default:
t.out <- messages.Response{
Command: cmd,
Error: NewInternalError("02")}
}
}
}
}
// Returns event, prefix, data & error.
func (t *HartTCR) parseMessageAndBody(message string) (messages.Event, string, string, error) {
event, body, err := t.parseMessage(message)
if err != nil {
switch err.(type) {
case *ErrEventInvalid: // do not process error here
t.log(2, "<-\tERROR:\t%v", err)
default:
return event, "", message, err
}
}
prefix, data, err := t.parseBody(body)
return event, prefix, data, err
}
// "composeHeader" joins all header fields.
// "withResponseType" flag states if responseType should be in header or not.
// Returns composed message header.
func (t HartTCR) composeHeader(withResponseType ...bool) string {
station := "0123" // indicates the Device identifier
side := "R" // defines de Side (L or R)/ Operator
operator := t.username
accessLevel := t.accessLevel
alarmFlag := "0" // software alarm signal
responseType := "" // specifies the response type that it is required to the command
if len(withResponseType) > 0 && withResponseType[0] {
responseType = "0"
}
return strings.Join(
[]string{
station,
side,
operator,
accessLevel,
alarmFlag,
responseType,
},
"")
}
// "trimHeader" validates and trims header from message.
// If error occurs, initial message returned.
// Returns trimmed message and error.
func (t *HartTCR) trimHeader(message string) (string, error) {
headerExpected := t.composeHeader()
headerPresent := message[:len(headerExpected)]
if headerPresent != headerExpected {
return message, &ErrInvalidHeader{headerPresent, headerExpected}
}
return message[len(headerPresent):], nil
}
// "composeBody" joins prefix, data length and data.
// Reverse method is "parseBody".
// "prefix" - may be command code or anything else.
// Returns composed body.
func (t HartTCR) composeBody(prefix, data string) string {
dataLength := fmt.Sprintf("%04d", len(data))
body := strings.Join(
[]string{
prefix,
dataLength,
data,
},
"")
return body
}
// "parseBody" validates length and extracts data from body.
// Reverse method is "composeBody".
// "prefixes" - may be slice of command codes or anything else,
// even "" - empty prefix.
// Returns prefix, message body data and error.
func (t *HartTCR) parseBody(body string) (string, string, error) {
prefix := ""
hasPrefix := false
if len(body) >= 6 && t.lengthValid(body[6:]) {
for pr := range hartCommandCodes {
if strings.HasPrefix(body, pr) {
if len(pr) > len(prefix) {
prefix = pr
}
hasPrefix = true
}
}
}
if !hasPrefix && !t.lengthValid(body) {
err := ErrDataLength(body)
return "", body, err
}
bodyUnprefixed := strings.TrimPrefix(body, prefix)
return prefix, bodyUnprefixed[4:], nil
}
// "lengthValid" validates body length value and returns true/false.
func (t *HartTCR) lengthValid(body string) bool {
return body[:4] == fmt.Sprintf("%04d", len(body[4:]))
}
// "composeMessage" joins STX, header, body and ETX. After join it
// calls "signatureFor" and signs message.
// Reverse method is "parseMessage".
// Returns signed message.
func (t HartTCR) composeMessage(bodyPrefix string, bodyData string) string {
messageUnsigned := strings.Join(
[]string{
STX,
t.composeHeader(true),
t.composeBody(bodyPrefix, bodyData),
ETX,
},
"")
return messageUnsigned + t.signatureFor(messageUnsigned)
}
func (t HartTCR) composeResponse(bodyPrefix string, bodyData string) string {
messageUnsigned := strings.Join(
[]string{
STX,
t.composeHeader(),
"00000",
t.composeBody(bodyPrefix, bodyData),
ETX,
},
"")
return messageUnsigned + t.signatureFor(messageUnsigned)
}
// "parseMessage" parses flags, BCC, header and event.
// Reverse method is "composeMessage".
// Returns event, message body and error.
func (t *HartTCR) parseMessage(message string) (messages.Event, string, error) {
event := messages.Event{}
messageTrimmed, err := t.trimFlagsAndBCC(message)
if err != nil {
return event, messageTrimmed, err
}
messageEventBody, err := t.trimHeader(messageTrimmed)
if err != nil {
return event, messageEventBody, err
}
return t.trimEvent(messageEventBody)
}
// "trimEvent" check event type and number and generates errors for Hart TCR
// native errors.
// "body" should be longer than 5 code points.
// Returns trimmed message body and error.
func (t HartTCR) trimEvent(body string) (event messages.Event, data string, err error) {
if len(body) <= 5 {
err = ErrEventNotPresent(body)
return
}
eventN := body[:4]
eventType := body[4:5]
data = body[5:]
event = messages.Event{
Type: messages.CodeAndMessage{
eventType,
hartErrorTypes[eventType]},
Message: messages.CodeAndMessage{
eventN,
hartErrorCodes[eventN]}}
switch eventType {
case "0":
if eventN != "0000" {
// Event type doesn't match event number
err = &ErrEventInvalid{eventType, eventN}
}
case "1":
// Warning
t.log(1, "<-\tWARNING:\t%v", event)
case "4":
// Long response
t.log(1, "<-\tLONG:\t%v", event)
}
return
}
// "trimFlagsAndBCC" validates message signatire and trims it & ETX, STX flags.
// If error occurs, initial message returned.
// Returns trimmed message and error.
func (t *HartTCR) trimFlagsAndBCC(message string) (string, error) {
// +2 for ETX flag and BCC
err := t.validateSignature(message)
if err != nil {
return message, err
}
// Trim flags & BCC
return message[1 : len(message)-2], nil
}
// "signatureFor" calculates BCC control sum for message.
// Returns BCC signature.
func (t HartTCR) signatureFor(messageUnsigned string) string {
var messageBCC rune
for _, chr := range messageUnsigned {
messageBCC = messageBCC ^ chr
}
return string(messageBCC)
}
// "validateSignature" checks if message signature is valid or not.
// Returns error if signature is invalid.
func (t HartTCR) validateSignature(message string) (err error) {
signaturePresent := string(message[len(message)-1])
signatureExpected := t.signatureFor(message[:len(message)-1])
if signaturePresent != signatureExpected {
err = &ErrInvalidSignature{signaturePresent, signatureExpected}
}
return err
}
// "log" implements simple logger with log levels.
func (t *HartTCR) log(level int, format string, a ...interface{}) {
if t.logLevel >= level {
t.logger.Printf(format, a...)
}
}
func (t *HartTCR) emulateResponse(commandName string, commandPars messages.Params) {
time.Sleep(2 * time.Second)
data := ""
switch commandName {
case "getState":
data = "001" + "200" + "1414111455555" + "0" + "RRRRRR" +
"1" + "0" + "0" + "000000000000" + "RRRRRRRRRRRRRRRR"
case "detail":
break
case "startSession":
data = "ADMINUSER"
case "endSession":
data = "ADMINUSER"
case "occupy":
data = "2" + "0000000"
case "cancelOccupy":
data = "0" + "0000000"
case "startDeposit":
data = "0" + "000000000"
case "deposit":
data = "000" + "B" + "0001010050" + "0001040010" + "UFS"
case "endDeposit":
data = "0000000000"
case "cancelDeposit":
data = "B" + "0001010050" + "0001040010" + "UFS"
case "dispense":
data = "0055" + "0000" + "000000"
case "cancelDispense":
data = "00000"
case "present":
data = "00000"
case "retract":
data = "00000"
case "reset":
data = "001"
default:
break
}
msg := t.composeResponse(hartCommands[commandName], data)
// t.log(0, "Data: % x", []byte(msg))
t.inBuffer = append(t.inBuffer, []byte(msg)...)
t.parseInBuffer()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment