Created
July 26, 2017 20:37
-
-
Save cema-sp/3c3382282cd3f64bed736071878e9730 to your computer and use it in GitHub Desktop.
Part of TCR driver utility
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}) | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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