Compare commits

..

10 Commits

Author SHA1 Message Date
4b991729ea Added Reading of Json with Protocol Definition
Some checks failed
Build / Build (push) Has been cancelled
2025-05-31 18:47:03 +02:00
a743e940c0 Added Testing of Callback Message for Parsing messages 2025-05-31 18:25:03 +02:00
730e16cf5d Adjusted gitignore 2025-05-29 22:26:33 +02:00
ea69ddc25d Removed old code and adjustet to new system 2025-05-29 22:25:36 +02:00
571d32b467 Added Message Hanlder Boilerplate 2025-05-29 22:24:50 +02:00
80a2caf29e Fixed Parser Interface 2025-05-29 22:24:38 +02:00
f7a5f595ab Added Parser with Tests 2025-05-29 22:11:16 +02:00
23e67a801e Actions 2025-03-22 16:04:05 +01:00
1b59a0cd55 Workflow1 2025-03-22 16:00:57 +01:00
18c57b8c6c Working an Action 2025-03-22 16:00:00 +01:00
14 changed files with 578 additions and 221 deletions

View File

@ -1,33 +1,19 @@
name: Build and Test Golang
on:
push:
branches:
- master
name: Build
on: [push]
jobs:
build:
Build:
runs-on: ubuntu-latest
container:
image: golang:1.22 # Aktuelle Version verwenden
options: --network host
steps:
# Schritt 1: Repository klonen
- name: Checkout Code
- run: echo "The job was automatically triggered by a ${{ gitea.event_name }} event."
- run: echo "This job is now running on a ${{ runner.os }} server hosted by Gitea!"
- run: echo "The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}."
- name: Check out repository code
uses: actions/checkout@v4
# Schritt 2: Abhängigkeiten installieren
- name: Install Dependencies
run: go mod tidy
# Schritt 3: Code bauen
- name: Build Application
run: go build .
- name: Upload Build Artifacts
uses: actions/upload-artifact@v4
with:
name: serialToWebsocket
path: .
- run: echo "The ${{ gitea.repository }} repository has been cloned to the runner."
- run: echo "The workflow is now ready to test your code on the runner."
- name: List files in the repository
run: |
ls ${{ gitea.workspace }}
- run: go build .
- run: echo "This job's status is ${{ job.status }}."

View File

@ -1,19 +0,0 @@
name: Gitea Actions Demo
run-name: ${{ gitea.actor }} is testing out Gitea Actions
on: [push]
jobs:
Explore-Gitea-Actions:
runs-on: ubuntu-latest
steps:
- run: echo " The job was automatically triggered by a ${{ gitea.event_name }} event."
- run: echo " This job is now running on a ${{ runner.os }} server hosted by Gitea!"
- run: echo " The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}."
- name: Check out repository code
uses: actions/checkout@v4
- run: echo " The ${{ gitea.repository }} repository has been cloned to the runner."
- run: echo " The workflow is now ready to test your code on the runner."
- name: List files in the repository
run: |
ls ${{ gitea.workspace }}
- run: echo " This job's status is ${{ job.status }}."

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
serialAlox

View File

@ -1,139 +0,0 @@
package MessageHandler
import (
"fmt"
"log"
"net/http"
"strconv"
"strings"
"sync"
"github.com/gorilla/websocket"
)
type SerialInteraction interface {
ListComports() []string
Connect(string) error
Disconnect()
Write([]byte) (int, error)
Read([]byte) (int, error)
GetPortPath() string
}
type MessageHandler struct {
Serial SerialInteraction
socket *websocket.Conn
socketLock sync.Mutex
}
const (
VERSION = "1"
)
func (mh *MessageHandler) WebsocketHandle(w http.ResponseWriter, r *http.Request, c *websocket.Conn) {
for {
mt, message, err := c.ReadMessage()
if err != nil {
log.Printf("read: %v", err)
break
}
log.Printf("recv: %s", message)
answer := mh.messageDispatcher(string(message))
mh.socketLock.Lock()
err = c.WriteMessage(mt, []byte(answer))
mh.socketLock.Unlock()
if err != nil {
log.Printf("write: %v", err)
break
}
}
}
// message format, ";" seperated message version;command;argc;args....?
func (mh *MessageHandler) messageDispatcher(message string) string {
messageParts := strings.Split(message, ";")
if len(messageParts) < 3 {
log.Printf("Message to Short, %v", message)
return ""
}
switch messageParts[0] {
case VERSION:
break
default:
log.Printf("Unknow Version %v in Message %v", messageParts[0], message)
return ""
}
switch messageParts[1] {
case "listports":
return mh.ListComports()
case "connect":
return mh.Connect(messageParts[4])
case "disconnect":
return mh.Disconnect()
case "write":
return mh.Write(messageParts[4])
case "read":
return mh.Read()
default:
log.Printf("unknow command %v", message)
break
}
return ""
}
func (mh *MessageHandler) ListComports() string {
ports := mh.Serial.ListComports()
message := MessageBuilder("portlist", ports...)
return message
}
func (mh *MessageHandler) Connect(portname string) string {
err := mh.Serial.Connect(portname)
message := ""
if err != nil {
message = MessageBuilder("error", "connection")
} else {
message = MessageBuilder("connected", "portname")
}
return message
}
func (mh *MessageHandler) Disconnect() string {
mh.Serial.Disconnect()
return MessageBuilder("disconnected")
}
func (mh *MessageHandler) Write(msg string) string {
_, err := mh.Serial.Write([]byte(msg))
if err != nil {
return MessageBuilder("write", fmt.Sprintf("write error %v", err))
}
return MessageBuilder("write")
}
func (mh *MessageHandler) Read() string {
buffer := make([]byte, 1024)
_, err := mh.Serial.Read(buffer)
if err != nil {
return MessageBuilder("read", fmt.Sprintf("read error %v", err))
}
return MessageBuilder("read", string(buffer))
}
// version;command;argc;argv;
// recv: 1;connect;1;/dev/ttyUSB0;
// send: 1;connected;0; // no args can be used as a acc command
// send: 1;connected;1;error: port not found;
// recv: 1;discoonect;0;
// send: 1;discoonect;0;
// recv: 1;write;1;viele viele bunte bytes;
// send: 1;write;0;
// send: 1;write;1;error: port not connected;
// send: 1:read:1;das hab ich gelesen junge;
func MessageBuilder(command string, args ...string) string {
return VERSION + ";" + command + ";" + strconv.Itoa(len(args)) + ";" + strings.Join(args, ";")
}

147
Parser/parser.go Normal file
View File

@ -0,0 +1,147 @@
package parser
import (
"fmt"
)
type ParsedMessage struct {
typeByte byte
payloadBytes []byte
}
type ParserConfig struct {
StartByte byte
}
func NewParserConfig() *ParserConfig {
return &ParserConfig{
StartByte: 0xAA,
}
}
type ParserErrorCodes int
type ParserError struct {
Code ParserErrorCodes
Message string
}
const (
ChecksumError ParserErrorCodes = iota
)
func (e *ParserError) Error() string {
return e.Message
}
type ParserState int
const (
StateWaitForStartByte ParserState = iota
StateReadType
StateReadLength
StateReadPayload
StateCheckChecksum
)
type Parser struct {
messages []ParsedMessage
currentMessageBuffer []byte
currentMsgType byte
readLength byte
readIndex byte
parserState ParserState
parserConfig ParserConfig
checksum byte
}
func NewParser(cfg ParserConfig) *Parser {
return &Parser{
messages: []ParsedMessage{},
currentMessageBuffer: []byte{},
currentMsgType: 0,
readLength: 0,
readIndex: 0,
parserState: StateWaitForStartByte,
parserConfig: cfg,
checksum: 0,
}
}
func (pa *Parser) ParseBytes(p []byte) (n int, err error) {
for i, b := range p {
err := pa.parseByte(b)
if err != nil {
return i, err
}
}
return len(p), nil
}
func (pa *Parser) parseByte(p byte) error {
switch pa.parserState {
case StateWaitForStartByte:
if p == pa.parserConfig.StartByte {
pa.parserState = StateReadType
pa.readIndex = 0
pa.checksum = 0
}
break
case StateReadType:
pa.currentMsgType = p
pa.checksum ^= p
pa.parserState = StateReadLength
case StateReadLength:
pa.readLength = p
pa.checksum ^= p
pa.parserState = StateReadPayload
break
case StateReadPayload:
if pa.readIndex < pa.readLength {
pa.currentMessageBuffer = append(pa.currentMessageBuffer, p)
pa.checksum ^= p
pa.readIndex++
if pa.readIndex == pa.readLength {
pa.parserState = StateCheckChecksum
}
}
break
case StateCheckChecksum:
if pa.checksum != p {
return &ParserError{
Code: ChecksumError,
Message: fmt.Sprintf("Checksum does not Match, want %v, got %v", pa.checksum, p),
}
}
pm := &ParsedMessage{
typeByte: pa.currentMsgType,
payloadBytes: pa.currentMessageBuffer,
}
pa.messages = append(pa.messages, *pm)
pa.reset()
break
}
return nil
}
func (pa *Parser) reset() {
pa.currentMessageBuffer = nil
pa.currentMsgType = 0
pa.readIndex = 0
pa.readLength = 0
pa.checksum = 0
pa.parserState = StateWaitForStartByte
}
func (pa *Parser) IsMessageAvailable() bool {
return len(pa.messages) > 0
}
func (pa *Parser) GetNextMessage() (typeByte byte, payload []byte, err error) {
if !pa.IsMessageAvailable() {
return 0x00, nil, fmt.Errorf("No message available")
}
lastMessage := pa.messages[0]
pa.messages = pa.messages[1:]
return lastMessage.typeByte, lastMessage.payloadBytes, nil
}

118
Parser/parser_test.go Normal file
View File

@ -0,0 +1,118 @@
package parser
import (
"testing"
)
// Hilfsfunktion zum Berechnen der Checksumme
func calcChecksum(msgType byte, length byte, payload []byte) byte {
cs := msgType ^ length
for _, b := range payload {
cs ^= b
}
return cs
}
func TestParser_ParseBytes(t *testing.T) {
tests := []struct {
name string
input []byte
expectErr bool
expectMessages []ParsedMessage
}{
{
name: "valid single message",
input: func() []byte {
msgType := byte(0x01)
payload := []byte{0x10, 0x20}
length := byte(len(payload))
cs := calcChecksum(msgType, length, payload)
return []byte{0xAA, msgType, length, payload[0], payload[1], cs}
}(),
expectErr: false,
expectMessages: []ParsedMessage{
{typeByte: 0x01, payloadBytes: []byte{0x10, 0x20}},
},
},
{
name: "invalid checksum",
input: []byte{
0xAA, 0x02, 0x01, 0x33, 0x00, // falsche Checksumme (letztes Byte)
},
expectErr: true,
expectMessages: nil,
},
{
name: "no start byte",
input: []byte{
0x01, 0x02, 0x03,
},
expectErr: false,
expectMessages: nil,
},
{
name: "multiple valid messages",
input: func() []byte {
payload1 := []byte{0x11}
payload2 := []byte{0x22, 0x23}
cs1 := calcChecksum(0x01, 1, payload1)
cs2 := calcChecksum(0x02, 2, payload2)
return append(
[]byte{0xAA, 0x01, 0x01, payload1[0], cs1,
0xAA, 0x02, 0x02, payload2[0], payload2[1], cs2},
)
}(),
expectErr: false,
expectMessages: []ParsedMessage{
{typeByte: 0x01, payloadBytes: []byte{0x11}},
{typeByte: 0x02, payloadBytes: []byte{0x22, 0x23}},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
parser := NewParser(ParserConfig{StartByte: 0xAA})
n, err := parser.ParseBytes(tt.input)
if tt.expectErr && err == nil {
t.Errorf("expected error, got none")
}
if !tt.expectErr && err != nil {
t.Errorf("unexpected error: %v", err)
}
if !tt.expectErr && n != len(tt.input) {
t.Errorf("expected to read %d bytes, read %d", len(tt.input), n)
}
for i, expected := range tt.expectMessages {
if !parser.IsMessageAvailable() {
t.Errorf("expected message %d to be available", i)
break
}
msgtype, msgpayload, err := parser.GetNextMessage()
if err != nil {
t.Errorf("unexpected error when getting message: %v", err)
}
if msgtype != expected.typeByte {
t.Errorf("message %d: expected type 0x%02X, got 0x%02X", i, expected.typeByte, msgtype)
}
if !equalBytes(msgpayload, expected.payloadBytes) {
t.Errorf("message %d: expected payload %v, got %v", i, expected.payloadBytes, msgpayload)
}
}
})
}
}
func equalBytes(a, b []byte) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}

View File

@ -22,10 +22,7 @@ func (sc *SerialConnection) GetPortPath() string {
return sc.portPath
}
func (sc *SerialConnection) Connect(portPath string) error {
mode := &serial.Mode{
BaudRate: 115200,
}
func (sc *SerialConnection) Connect(portPath string, mode *serial.Mode) error {
port, err := serial.Open(portPath, mode)
if err != nil {
return err

4
go.mod
View File

@ -1,9 +1,9 @@
module serialToWebsocket
module serialAlox
go 1.23.6
require (
github.com/gorilla/websocket v1.5.3
github.com/micmonay/keybd_event v1.1.2
go.bug.st/serial v1.6.2
)

4
go.sum
View File

@ -2,8 +2,8 @@ github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0
github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/micmonay/keybd_event v1.1.2 h1:RpgvPJKOh4Jc+ZYe0OrVzGd2eNMCfuVg3dFTCsuSah4=
github.com/micmonay/keybd_event v1.1.2/go.mod h1:CGMWMDNgsfPljzrAWoybUOSKafQPZpv+rLigt2LzNGI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=

72
main.go
View File

@ -1,44 +1,62 @@
package main
import (
"fmt"
"log"
"net/http"
"serialToWebsocket/MessageHandler"
serialinteraction "serialToWebsocket/SerialInteraction"
"runtime"
parser "serialAlox/Parser"
serialinteraction "serialAlox/SerialInteraction"
"time"
"github.com/gorilla/websocket"
"github.com/micmonay/keybd_event"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true // for debuggin allow all
},
type MainConfig struct {
Debug bool
Port int
}
type Main struct {
handle *MessageHandler.MessageHandler
func NewMainConfig() *MainConfig {
return &MainConfig{
Debug: false,
Port: 8080,
}
func (m *Main) websock(w http.ResponseWriter, r *http.Request) {
c, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("upgrade: %v", err)
return
}
defer c.Close()
m.handle.WebsocketHandle(w, r, c)
}
func main() {
handlerOb := MessageHandler.MessageHandler{
Serial: &serialinteraction.SerialConnection{},
}
ma := Main{
handle: &handlerOb,
kb, err := keybd_event.NewKeyBonding()
if err != nil {
panic(err)
}
http.HandleFunc("/ws", ma.websock)
log.Printf("Server gestaret auf :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
if runtime.GOOS == "linux" {
time.Sleep(2 * time.Second)
}
mc := NewMainConfig()
mc.Debug = true
sc := serialinteraction.SerialConnection{}
msConf := NewMessageHandlerConfig()
msParserConf := parser.NewParserConfig()
msParser := parser.NewParser(*msParserConf)
msHandler := NewMessageHandler(&sc, *msConf, msParser)
msHandler.RegisterMessageCallback(0x01, func(b1 byte, b2 []byte) {
log.Printf("ICH BIN DER HANDLER % X, % X", b1, b2)
kb.SetKeys(keybd_event.VK_A, keybd_event.VK_B)
kb.Press()
time.Sleep(10 * time.Millisecond)
kb.Release()
})
go msHandler.ListenAndServe()
msHandler.MessageParser.ParseBytes([]byte{0xAA, 0x01, 0x02, 0x10, 0x20, 0x33})
log.Printf("Started Server on %d", mc.Port)
connectString := fmt.Sprintf(":%d", mc.Port)
log.Fatal(http.ListenAndServe(connectString, nil))
}

86
message_handler.go Normal file
View File

@ -0,0 +1,86 @@
package main
import (
"context"
"log"
)
type ReaderWriter interface {
Write(p []byte) (n int, err error)
Read(b []byte) (n int, err error)
}
type Parser interface {
ParseBytes(p []byte) (n int, err error)
IsMessageAvailable() bool
GetNextMessage() (typeByte byte, payload []byte, err error)
}
type HandlerFunc func(byte, []byte)
type MessageHandler struct {
WriterReader ReaderWriter
Conf MessageHandlerConfig
MessageParser Parser
handlers map[byte]HandlerFunc
}
type MessageHandlerConfig struct {
}
func NewMessageHandlerConfig() *MessageHandlerConfig {
return &MessageHandlerConfig{}
}
func NewMessageHandler(con ReaderWriter, conf MessageHandlerConfig, msgParser Parser) *MessageHandler {
return &MessageHandler{
WriterReader: con,
Conf: conf,
MessageParser: msgParser,
handlers: make(map[byte]HandlerFunc),
}
}
func (mh *MessageHandler) RegisterMessageCallback(typeByte byte, fn HandlerFunc) error {
mh.handlers[typeByte] = fn
return nil
}
func (mh *MessageHandler) ListenAndServe() {
ctx, cancle := context.WithCancel(context.Background())
go mh.readBytes(ctx)
defer cancle()
for {
if mh.MessageParser.IsMessageAvailable() {
msgType, msgPayload, err := mh.MessageParser.GetNextMessage()
if err != nil {
log.Printf("Error getting message %v", err)
return
}
log.Printf("Got messagetype % X, with payload % X", msgType, msgPayload)
if mh.handlers[msgType] != nil {
mh.handlers[msgType](msgType, msgPayload)
}
}
}
}
func (mh *MessageHandler) readBytes(ctx context.Context) error {
buffer := make([]byte, 1024)
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
// Proceed with reading
n, err := mh.WriterReader.Read(buffer)
if err != nil {
return err
}
// Process the read bytes
mh.MessageParser.ParseBytes(buffer[:n])
//time.Sleep(100 * time.Millisecond)
}
}
}

47
proto/proto.go Normal file
View File

@ -0,0 +1,47 @@
package proto
import (
"encoding/json"
"strconv"
)
type HexByte byte
type ProtoComplete struct {
Header ProtocolHeader `json:"protocol"`
Protocols map[string][]ProtoMessage `json:"protocols"`
}
type ProtocolHeader struct {
StartByte HexByte `json:"start_byte"`
MessageLength int `json:"message_length"`
MaxPayload int `json:"max_payload"`
Checksum string `json:"checksum"`
}
type ProtoMessage struct {
Name string `json:"name"`
ID HexByte `json:"id"`
Payload []ProtoMessagePayload `json:"payload,omitempty"` // optional falls kein payload
}
type ProtoMessagePayload struct {
Name string `json:"name"`
DataType string `json:"type"`
ArrayLength int `json:"array,omitempty"` // optional falls kein array
}
func (h *HexByte) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil { // turn into go string
return err
}
i, err := strconv.ParseUint(s, 0, 8)
if err != nil {
return err
}
*h = HexByte(i)
return nil
}

23
proto/proto_test.go Normal file
View File

@ -0,0 +1,23 @@
package proto
import (
"encoding/json"
"log"
"os"
"testing"
)
func TestReadJson(t *testing.T) {
con, err := os.ReadFile("../testdata/prot1.json")
if err != nil {
log.Fatalf("Could not read file %v", err)
}
pc := &ProtoComplete{}
json.Unmarshal(con, pc)
log.Printf("%v", pc)
t.Fail()
}

92
testdata/prot1.json vendored Normal file
View File

@ -0,0 +1,92 @@
{
"protocol": {
"start_byte": "0xAA",
"message_length": 0,
"max_payload": 255,
"checksum": "xor"
},
"protocols": {
"messages_esp_to_pc": [
{
"name": "Clients",
"id": "0xE1",
"payload": [
{
"name": "clientCount",
"type": "uint8_t"
},
{
"name": "clientAvaiableBitMask",
"type": "uint32_t"
}
]
},
{
"name": "Status",
"id": "0xE2",
"payload": [
{
"name": "clientId",
"type": "uint8_t"
},
{
"name": "mac",
"type": "uint8_t",
"array": 6
}
]
},
{
"name": "Pong",
"id": "0xD1",
"payload": [
{
"name": "clientId",
"type": "uint8_t"
},
{
"name": "ping",
"type": "uint32_t"
}
]
}
],
"messages_pc_to_esp": [
{
"name": "RequestPing",
"id": "0xE1",
"payload": [
{
"name": "clientId",
"type": "uint8_t"
}
]
},
{
"name": "RequestStatus",
"id": "0xE2",
"payload": [
{
"name": "clientId",
"type": "uint8_t"
}
]
},
{
"name": "PrepareFirmwareUpdate",
"id": "0xF1"
},
{
"name": "FirmwareUpdateLine",
"id": "0xF2",
"payload": [
{
"name": "data",
"type": "uint8_t",
"array": 240
}
]
}
]
}
}