Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature add the possibility to send pcic commands #17

Merged
merged 6 commits into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
{
"name": "Go",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "ghcr.io/graugans/golang:latest"
"image": "mcr.microsoft.com/devcontainers/go:1.22",

// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
Expand All @@ -23,9 +23,7 @@
"golang.go"
]
}
},


}
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"cSpell.words": [
"PCIC"
"PCIC",
"swupdater"
]
}
63 changes: 56 additions & 7 deletions cmd/ovp8xx/cmd/pcic.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ Copyright © 2024 Christian Ege <[email protected]>
package cmd

import (
"context"
"fmt"
"strconv"
"sync"
"time"

"github.com/graugans/go-ovp8xx/v2/pkg/pcic"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -48,6 +52,14 @@ func (r *PCICReceiver) Notification(msg pcic.NotificationMessage) {
func pcicCommand(cmd *cobra.Command, args []string) error {
var testHandler *PCICReceiver = &PCICReceiver{}
var err error

// Retrieve the slice of commands
cmds, err := cmd.Flags().GetStringSlice("cmd")
if err != nil {
// Handle the error
return err
}

helper, err := NewHelper(cmd)
if err != nil {
return err
Expand All @@ -56,16 +68,52 @@ func pcicCommand(cmd *cobra.Command, args []string) error {
pcic, err := pcic.NewPCICClient(
pcic.WithTCPClient(helper.hostname(), helper.remotePort()),
)
if err != nil {
return err
}
for {
err = pcic.ProcessIncomming(testHandler)
var wg sync.WaitGroup
wg.Add(1) // We're going to wait for one goroutine

go func() {
defer wg.Done() // This will be called when the goroutine finishes
for {
err = pcic.ProcessIncomming(testHandler)
if err != nil {
// An error occured, we break the loop
break
}
}
}()

// execute the commands
for _, cmd := range cmds {
prefix := fmt.Sprintf(" %s # ", cmd)
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
response, err := pcic.Send(ctx, []byte(cmd))
if err != nil {
// An error occured, we break the loop
break
cancel()
return fmt.Errorf("failed to send command: %v", err)

}
if len(response) >= 9 { // Ensure there are at least 9 bytes
lengthStr := string(response[:9]) // Convert the first 9 bytes to a string
length, err := strconv.Atoi(lengthStr) // Convert the string to an integer
if err != nil {
// Response does not start with the length, print the whole response
fmt.Println(prefix, (response))
} else {
if len(response) >= 9+length {
// Strip the first 9 bytes and print the rest up to the specified length
fmt.Println(string(response[9 : 9+length]))
} else {
cancel()
return fmt.Errorf("response too short: %s", string(response))
}
}
} else {
fmt.Println(prefix, string(response))
}
cancel()
}
// Wait for the goroutine to be finished
wg.Wait()
return err
}

Expand All @@ -79,4 +127,5 @@ var pcicCmd = &cobra.Command{
func init() {
rootCmd.AddCommand(pcicCmd)
pcicCmd.Flags().Uint16("port", 50010, "The port to connect to")
pcicCmd.Flags().StringSlice("cmd", []string{}, "Commands to be send to the device, can be specified multiple times. All commands will be executed in order")
}
55 changes: 53 additions & 2 deletions cmd/ovp8xx/cmd/swupdate.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,60 @@ The command establishes a connection to the device, uploads the firmware file, a
RunE: swupdateCommand,
}

// restartCmd represents the restart command.
// It restarts the device using the SWUpdater service.
// Depending on the state of the device, this command will either reboot to productive mode
// or restart the SWUpdater service again.
// If a previous update was initiated but not successful, the device will restart the SWUpdater service again.
var restartCmd = &cobra.Command{
Use: "restart",
Short: "Restart the device",
Long: `This command restarts the device using the SWUpdater service.
Depending on the state of the device this will reboot to productive mode
or restart the SWUpdater service again.

In case a previous update was initiated but not successful the device will restart
the SWUpdater service again.
`,
RunE: func(cmd *cobra.Command, args []string) error {
// Retrieve host and port from the parent command's flags
host, err := rootCmd.PersistentFlags().GetString("ip")
if err != nil {
return fmt.Errorf("cannot get host: %w", err)
}

port, err := cmd.Parent().Flags().GetUint16("port")
if err != nil {
// If the port is not set on the parent, use a default value or handle the error
return fmt.Errorf("cannot get port: %w", err)
}

connectionTimeout, err := cmd.Flags().GetDuration("online")
if err != nil {
return fmt.Errorf("cannot get timeout: %w", err)
}

updater := swupdater.NewSWUpdater(host, port, nil)

// Call the Restart method on the SWUpdater instance
if err := updater.Restart(connectionTimeout); err != nil {
return fmt.Errorf("failed to restart the device: %w", err)
}

fmt.Println("Device restart initiated successfully.")
return nil
},
}

func init() {
rootCmd.AddCommand(swupdateCmd)
swupdateCmd.Flags().Uint16("port", 8080, "Port number for SWUpdate")
swupdateCmd.Flags().Duration("timeout", 5*time.Minute, "The timeout for the upload")

swupdateCmd.PersistentFlags().Uint16("port", 8080, "Port number for SWUpdate")
swupdateCmd.PersistentFlags().Duration("online", 2*time.Minute, "The time to wait for the device to become available")
swupdateCmd.Flags().Duration("online", 2*time.Minute, "The time to wait for the device to become available")
swupdateCmd.Flags().Duration("timeout", 5*time.Minute, "The timeout for the upload")

// The restart sub command
swupdateCmd.AddCommand(restartCmd)
restartCmd.Flags().Duration("online", 3*time.Second, "The time to wait for the device to become available")
}
139 changes: 134 additions & 5 deletions pkg/pcic/protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,23 @@ package pcic
import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"io"
"math/rand"
"net"
"strconv"
"strings"
"sync"
)

type (
PCICClient struct {
reader *bufio.Reader
writer *bufio.Writer
reader *bufio.Reader
writer *bufio.Writer
responseChans map[string]chan Response
mu sync.Mutex
}
PCICClientOption func(c *PCICClient) error
)
Expand Down Expand Up @@ -59,9 +66,17 @@ type ErrorMessage struct {
Message string
}

type Response struct {
Ticket string
Data []byte
}

func NewPCICClient(options ...PCICClientOption) (*PCICClient, error) {
var err error
pcic := &PCICClient{}
pcic := &PCICClient{
responseChans: make(map[string]chan Response),
}

// Apply options
for _, opt := range options {
if err = opt(pcic); err != nil {
Expand Down Expand Up @@ -94,6 +109,23 @@ func WithTCPClient(hostname string, port uint16) PCICClientOption {
}
}

// generateTicket generates a unique ticket and associates it with the provided reply channel.
// It uses a random number generator to generate a 4-digit ticket number.
// If the generated ticket already exists in the responseChans map, it continues generating a new ticket until a unique one is found.
// Once a unique ticket is found, it adds the ticket and reply channel to the responseChans map and returns the ticket.
func (p *PCICClient) generateTicket(replyChan chan Response) string {
for {
ticket := fmt.Sprintf("%04d", rand.Intn(8999)+1000)
p.mu.Lock()
if _, exists := p.responseChans[ticket]; !exists {
p.responseChans[ticket] = replyChan
p.mu.Unlock()
return ticket
}
p.mu.Unlock()
}
}

func (p *PCICClient) ProcessIncomming(handler MessageHandler) error {
reader := p.reader
if reader == nil {
Expand All @@ -105,10 +137,12 @@ func (p *PCICClient) ProcessIncomming(handler MessageHandler) error {
return err
}
firstTicket := header[:ticketFieldLength]
ticketStr := string(firstTicket)

secondTicket := header[secondTicketOffset:dataOffset]
if !bytes.Equal(firstTicket, secondTicket) {
return fmt.Errorf("mismatch in the tickets %s != %s ",
string(firstTicket),
string(ticketStr),
string(secondTicket),
)
}
Expand All @@ -132,7 +166,20 @@ func (p *PCICClient) ProcessIncomming(handler MessageHandler) error {
if !bytes.Equal(trailer, []byte{'\r', '\n'}) {
return errors.New("invalid trailer detected")
}
if bytes.Equal(resultTicket, firstTicket) {
var ticketNum = 0
if ticketStr != "0000" {
ticketNum, err = strconv.Atoi(strings.TrimLeft(ticketStr, "0"))
if err != nil {
return fmt.Errorf("unable to convert the ticket number %s to an integer", ticketStr)
}
}
if ticketNum > 100 {
err := p.responseParser(ticketStr, data)
if err != nil {
return fmt.Errorf("unable to parse the response: %w", err)
}
return nil
} else if bytes.Equal(resultTicket, firstTicket) {
frame, err := asyncResultParser(data)
handler.Result(frame)
return err
Expand All @@ -144,6 +191,82 @@ func (p *PCICClient) ProcessIncomming(handler MessageHandler) error {
return fmt.Errorf("unknown ticket received: %s", string(firstTicket))
}

func (p *PCICClient) Send(ctx context.Context, data []byte) ([]byte, error) {
var res []byte
if p.writer == nil {
return res, errors.New("no bufio.Writer provided, please instantiate the object")
}
respChan := make(chan Response)
// Let's generate a random ticket number
ticket := p.generateTicket(respChan)

defer func() {
p.mu.Lock()
delete(p.responseChans, ticket)
p.mu.Unlock()
}()

// Create a new buffer to aggregate the message
var buf bytes.Buffer
var delimter = []byte("\r\n")
// Convert ticket to a 4-digit string and then to bytes
ticketBytes := []byte(ticket)
buf.Write(ticketBytes)
// A Command message is composed like this
// <ticket><length>CRLF<ticket><content>CRLF
// <ticket> is a 4-digit number in the range 100-9999
// <length> is a character string starting with an 'L' followed by 9 digits
// interpreted as a decimal value. The number is the length of data that follows
// <content> is the actual data that is being sent
length := len(ticketBytes) /*<ticket>*/ + len(data) + 2 /*CRLF*/
lengthStr := fmt.Sprintf("L%09d", length)
lengthBytes := []byte(lengthStr)
buf.Write(lengthBytes)
buf.Write(delimter)
buf.Write(ticketBytes)
buf.Write(data)
buf.Write(delimter)

// Write the buffer to the underlying writer
_, err := p.writer.Write(buf.Bytes())
if err != nil {
return res, fmt.Errorf("unable to write to the buffer: %w", err)
}
// This is necessary to flush the buffer to the underlying writer
// Otherwise, the data will not be sent over the network
err = p.writer.Flush()
if err != nil {
return res, fmt.Errorf("unable to flush to the buffer: %w", err)
}

// Wait for the response or timeout
select {
case resp := <-respChan:
res = resp.Data
case <-ctx.Done():
return res, fmt.Errorf("request with ticket: %s timed out or canceled", ticket)
}

return res, nil
}

func (p *PCICClient) responseParser(ticket string, data []byte) error {
var err error
res := Response{}
if len(data) <= delimiterFieldLength {
return fmt.Errorf("the data is too short to be a valid frame: %d", len(data))
}
res.Ticket = ticket
res.Data = data[:len(data)-delimiterFieldLength]

p.mu.Lock()
if ch, ok := p.responseChans[ticket]; ok {
ch <- res
}
p.mu.Unlock()
return err
}

func errorParser(data []byte) (ErrorMessage, error) {
var err error
errorStatus := ErrorMessage{}
Expand All @@ -162,7 +285,13 @@ func errorParser(data []byte) (ErrorMessage, error) {
func asyncResultParser(data []byte) (Frame, error) {
frame := Frame{}
var err error
if len(data) <= delimiterFieldLength {
return frame, fmt.Errorf("the data is too short to be a valid frame: %d", len(data))
}
contentDecorated := data[:len(data)-delimiterFieldLength]
if len(contentDecorated)-len(endMarker) < 0 {
return frame, fmt.Errorf("the data is too short to be a valid frame: %d: content: %s", len(data), string(data))
}
content := contentDecorated[len(endMarker) : len(contentDecorated)-len(endMarker)]
if len(content) == 0 {
// no content is available
Expand Down
Loading
Loading