diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml index 2ea7ab8..38d6c7d 100644 --- a/.github/workflows/goreleaser.yml +++ b/.github/workflows/goreleaser.yml @@ -5,6 +5,9 @@ env: ATTINY_MINOR: 0 ATTINY_PATCH: 10 RELEASE_DOWNLOAD: https://github.com/TheCacophonyProject/attiny1616/releases/download/v${ATTINY_MAJOR}.${ATTINY_MINOR}.${ATTINY_PATCH} + SPOOL_PY_VERSION: 0.0.1 + SPOOL_MPY_VERSION: 1.28.0rc0.post2 + SPOOL_PY_DOWNLOAD: https://github.com/TheCacophonyProject/spool-py/releases/download/v${SPOOL_PY_VERSION}/spool-mpy-${SPOOL_MPY_VERSION}.zip on: push: @@ -33,6 +36,13 @@ jobs: wget -O _release/attiny-firmware.hex ${{ env.RELEASE_DOWNLOAD }}/firmware.hex wget -O _release/attiny-firmware.hex.sha256 ${{ env.RELEASE_DOWNLOAD }}/firmware.hex.sha256 + - name: Download spool-py MicroPython files + run: | + rm -rf _release/mpy + wget -O spool-mpy.zip ${{ env.SPOOL_PY_DOWNLOAD }} + unzip spool-mpy.zip -d _release/mpy/ + rm spool-mpy.zip + - name: Set ATTINY_HASH run: echo "ATTINY_HASH=$(cut -d ' ' -f 1 < _release/attiny-firmware.hex.sha256)" >> $GITHUB_ENV diff --git a/.gitignore b/.gitignore index f0b6b21..25532b8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /dist _release/attiny-firmware.hex _release/attiny-firmware.hex.sha256 +_release/mpy /tc2-hat-controller diff --git a/.goreleaser.yml b/.goreleaser.yml index b88333a..a7f2371 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -79,6 +79,8 @@ nfpms: dst: /usr/bin/tc2-hat-temp - src: _release/tc2-hat-trap-cli dst: /usr/bin/tc2-hat-trap-cli + - src: _release/mpy/* + dst: /etc/cacophony/mpy dependencies: #- python3-pip diff --git a/internal/tc2-hat-comms/trap-control.go b/internal/tc2-hat-comms/trap-control.go index 19a251c..94484d8 100644 --- a/internal/tc2-hat-comms/trap-control.go +++ b/internal/tc2-hat-comms/trap-control.go @@ -7,7 +7,6 @@ import ( "encoding/json" "fmt" "strconv" - "sync" "time" "github.com/TheCacophonyProject/event-reporter/v3/eventclient" @@ -18,6 +17,56 @@ import ( // processTrapControl communicates the trap enabled/disabled state by writing // the "enable" variable over UART instead of setting a digital pin. func processTrapControl(config *CommsConfig, eventSignals chan event) error { + // Open the serial port so we can send/receive messages from the trap. + port, err := serialhelper.OpenSerial(gpio.High, gpio.Low, config.BaudRate) + if err != nil { + return fmt.Errorf("failed to open serial port: %v", err) + } + defer port.Close() + + messenger := NewTrapMessenger(port) + messenger.UnsolicitedHandler = parseMessageFromTrap + messenger.Start() + + // Wait to get PING response from trap + log.Info("Waiting for PING response from trap...") + for { + if err := messenger.Ping(); err == nil { + break + } + log.Warnf("Failed to get PING response from trap: %v", err) + time.Sleep(time.Second) + } + eventclient.AddEvent(eventclient.Event{ + Timestamp: time.Now(), + Type: "trapPing", + }) + log.Info("PING response received from trap.") + + // Make sure it is running the latest software + log.Info("Checking trap software is up to date") + fileUpdated, err := messenger.CopyDir("/etc/cacophony/mpy", "/", false) + if err != nil { + log.Error("Error in uploading the latest software to the trap") + return err + } + if fileUpdated { + log.Info("Updated software on trap") + } else { + log.Info("Software already up to date on trap") + } + + // Setup loop for monitoring classifications and enabling/disabling the trap + if err := classificationChecks(config, eventSignals, messenger); err != nil { + log.Errorf("Failed to run classification checks: %v", err) + return err + } + + return nil +} + +func classificationChecks(config *CommsConfig, eventSignals chan event, messenger *TrapMessenger) error { + trapEnabled := false previousTrapEnabled := false lastProtectSpeciesSighting := time.Time{} @@ -30,17 +79,6 @@ func processTrapControl(config *CommsConfig, eventSignals chan event) error { triggerAnimal := "" var confidence int32 - // Open the serial port so we can send/receive messages from the trap. - port, err := serialhelper.OpenSerial(gpio.High, gpio.Low, config.BaudRate) - if err != nil { - return fmt.Errorf("failed to open serial port: %v", err) - } - defer port.Close() - - // Create the messenger that tracks sending/receiving messages - messenger := NewUartMessenger(port) - messenger.Start() - for { now := time.Now() trapEnabled = config.TrapEnabledByDefault @@ -54,7 +92,7 @@ func processTrapControl(config *CommsConfig, eventSignals chan event) error { if trapEnabled != previousTrapEnabled { if trapEnabled { log.Infof("Enabling trap, reason: %s", enablingReason) - success, err := messenger.setEnable(true) + success, err := messenger.SetEnable(true) if err != nil { return fmt.Errorf("failed to enable trap: %v", err) } @@ -81,7 +119,7 @@ func processTrapControl(config *CommsConfig, eventSignals chan event) error { }) } else { log.Info("Disabling trap, reason: ", disablingReason) - success, err := messenger.setEnable(false) + success, err := messenger.SetEnable(false) if err != nil { return fmt.Errorf("failed to disable trap: %v", err) } @@ -151,6 +189,7 @@ func processTrapControl(config *CommsConfig, eventSignals chan event) error { log.Debug("Scheduled check") } } + } // Message represents the data structure for communication with a device connected on UART. @@ -202,69 +241,6 @@ type Write struct { Val any `json:"val,omitempty"` } -// UartMessenger manages bidirectional communication with the RP2040 over UART. -// It holds a persistent serial port and routes incoming messages to either -// pending response waiters (matched by ID) or an unsolicited message channel. -type UartMessenger struct { - port *serialhelper.SerialPort - pendingMu sync.Mutex - pending map[int]chan *Message - nextID int - baudRate int -} - -// NewUartMessenger creates a UartMessenger using an already-open SerialPort. -func NewUartMessenger(port *serialhelper.SerialPort) *UartMessenger { - return &UartMessenger{ - port: port, - pending: make(map[int]chan *Message), - } -} - -// Start begins the background routing goroutine. Unsolicited messages from the RP2040 -// (i.e. not responses to a request we sent) are delivered to the unsolicited channel. -// Pass nil to discard unsolicited messages. -func (u *UartMessenger) Start() { - go u.routeMessages() -} - -// routeMessages reads lines from the serial port, parses them, and routes them: -// TODO: Maybe separate this for routing messages -// - Response messages are matched to a pending sendMessage call by ID. -// - If not a response then it is a notification from the trap. -func (u *UartMessenger) routeMessages() { - for line := range u.port.Lines { - // Parse the line - msg, err := ParseLine(line) - if err != nil { - log.Warnf("Failed to parse incoming message %q: %v", line, err) - continue - } - - // Check if the message was a response - if msg.Response() { - u.pendingMu.Lock() - ch, ok := u.pending[msg.ID] - if !ok && len(u.pending) == 1 { - // Fallback for RP2040 firmware that doesn't echo message IDs yet. - for _, c := range u.pending { - ch = c - ok = true - break - } - } - u.pendingMu.Unlock() - if ok { - ch <- msg - continue - } - } - - // If not a response then it is a notification from the trap. - parseMessageFromTrap(msg) - } -} - func parseMessageFromTrap(msg *Message) { log.Printf("Trap message: %+v", msg) @@ -385,57 +361,3 @@ func computeChecksum(message []byte) int { } return checksum % 256 } - -// sendMessage sends a request and waits for a matching response. -// It assigns a unique ID to the message for correlation. -func (u *UartMessenger) sendMessage(message Message) (*Message, error) { - u.pendingMu.Lock() - u.nextID++ - id := u.nextID - message.ID = id - ch := make(chan *Message, 1) - u.pending[id] = ch - u.pendingMu.Unlock() - - defer func() { - u.pendingMu.Lock() - delete(u.pending, id) - u.pendingMu.Unlock() - }() - - line := message.ToUARTLine() - log.Infof("Message: '%s'", line) - - if err := u.port.Write([]byte(line)); err != nil { - return nil, err - } - - select { - case response := <-ch: - log.Println("Response:", response) - return response, nil - case <-time.After(5 * time.Second): - return nil, fmt.Errorf("timeout waiting for response to message ID %d", id) - } -} - -func (u *UartMessenger) setEnable(enable bool) (bool, error) { - message := Message{} - if enable { - message.Type = "ENABLE" - } else { - message.Type = "DISABLE" - } - response, err := u.sendMessage(message) - if err != nil { - return false, err - } - if response.Type == "NACK" { - return false, fmt.Errorf("NACK response") - } - if response.Type == "BAD_KEY" { - log.Warn("Got BAD_KEY response, was trying to set a key that doesn't exist") - return false, nil - } - return true, nil -} diff --git a/internal/tc2-hat-comms/trap-messenger.go b/internal/tc2-hat-comms/trap-messenger.go new file mode 100644 index 0000000..53422c9 --- /dev/null +++ b/internal/tc2-hat-comms/trap-messenger.go @@ -0,0 +1,293 @@ +package comms + +import ( + "bytes" + "compress/flate" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "os" + "path/filepath" + "sync" + "time" + + "github.com/TheCacophonyProject/tc2-hat-controller/serialhelper" +) + +// TrapMessenger manages bidirectional communication with the RP2040 over UART. +// It holds a persistent serial port and routes incoming messages to either +// pending response waiters (matched by ID) or an unsolicited message handler. +type TrapMessenger struct { + port *serialhelper.SerialPort + pendingMu sync.Mutex + pending map[int]chan *Message + nextID int + UnsolicitedHandler func(*Message) +} + +// NewTrapMessenger creates a TrapMessenger using an already-open SerialPort. +func NewTrapMessenger(port *serialhelper.SerialPort) *TrapMessenger { + return &TrapMessenger{ + port: port, + pending: make(map[int]chan *Message), + } +} + +// Start begins the background routing goroutine. +func (u *TrapMessenger) Start() { + go u.routeMessages() +} + +func (u *TrapMessenger) routeMessages() { + for line := range u.port.Lines { + msg, err := ParseLine(line) + if err != nil { + log.Warnf("Failed to parse incoming message %q: %v", line, err) + continue + } + + if msg.Response() { + u.pendingMu.Lock() + ch, ok := u.pending[msg.ID] + if !ok && len(u.pending) == 1 { + // Fallback for RP2040 firmware that doesn't echo message IDs yet. + for _, c := range u.pending { + ch = c + ok = true + break + } + } + u.pendingMu.Unlock() + if ok { + ch <- msg + continue + } + } + + if u.UnsolicitedHandler != nil { + u.UnsolicitedHandler(msg) + } + } +} + +// SendMessage sends a request and waits for a matching response. +// It assigns a unique ID to the message for correlation. +func (u *TrapMessenger) SendMessage(message Message) (*Message, error) { + u.pendingMu.Lock() + u.nextID++ + id := u.nextID + message.ID = id + ch := make(chan *Message, 1) + u.pending[id] = ch + u.pendingMu.Unlock() + + defer func() { + u.pendingMu.Lock() + delete(u.pending, id) + u.pendingMu.Unlock() + }() + + line := message.ToUARTLine() + log.Debugf("Message: '%s'", line) + + if err := u.port.Write([]byte(line)); err != nil { + return nil, err + } + + select { + case response := <-ch: + log.Debug("Response:", response) + return response, nil + case <-time.After(5 * time.Second): + return nil, fmt.Errorf("timeout waiting for response to message ID %d", id) + } +} + +func (u *TrapMessenger) Ping() error { + resp, err := u.SendMessage(Message{Type: "PING"}) + if err != nil { + return err + } + if resp.Type != "ACK" { + return fmt.Errorf("unexpected ping response: %s", resp.Type) + } + return nil +} + +func (u *TrapMessenger) SetEnable(enable bool) (bool, error) { + message := Message{} + if enable { + message.Type = "ENABLE" + } else { + message.Type = "DISABLE" + } + response, err := u.SendMessage(message) + if err != nil { + return false, err + } + if response.Type == "NACK" { + return false, fmt.Errorf("NACK response") + } + if response.Type == "BAD_KEY" { + log.Warn("Got BAD_KEY response, was trying to set a key that doesn't exist") + return false, nil + } + return true, nil +} + +func HandleResponse(response *Message, err error) error { + if err != nil { + return err + } + if response.Type == "NACK" { + return fmt.Errorf("NACK response: %s", response.Payload) + } + log.Infof("Response: type=%s, payload=%s", response.Type, response.Payload) + return nil +} + +func (u *TrapMessenger) Restart() error { + return HandleResponse(u.SendMessage(Message{Type: "RESTART"})) +} + +func (u *TrapMessenger) ReadTime() error { + return HandleResponse(u.SendMessage(Message{Type: "READ_TIME"})) +} + +func (u *TrapMessenger) WriteTime(timeStr string) error { + if timeStr == "" { + timeStr = time.Now().UTC().Format(time.DateTime) + } + log.Printf("Writing UTC time: '%s'", timeStr) + return HandleResponse(u.SendMessage(Message{Type: "WRITE_TIME", Payload: timeStr})) +} + +func (u *TrapMessenger) CommitFiles() error { + log.Println("Committing all .tmp files...") + return HandleResponse(u.SendMessage(Message{Type: "COMMIT"})) +} + +// CopyDir uploads all files from sourceDir to destDir on the RP2040, then commits them. +// Returns true if any file was updated. +// Only files that don't match the hash will be updated unless force is true. +func (u *TrapMessenger) CopyDir(sourceDir, destDir string, force bool) (bool, error) { + aFileWasUpdated := false + entries, err := os.ReadDir(sourceDir) + if err != nil { + return false, fmt.Errorf("failed to read directory %s: %v", sourceDir, err) + } + for _, entry := range entries { + if entry.IsDir() { + // TODO: recursively copy subdirectories + continue + } + localFile := filepath.Join(sourceDir, entry.Name()) + destFile := filepath.Join(destDir, entry.Name()) + fileUpdated, err := u.CopyFile(localFile, destFile, force) + if err != nil { + return false, fmt.Errorf("failed to copy %s: %v", entry.Name(), err) + } + aFileWasUpdated = aFileWasUpdated || fileUpdated + } + return aFileWasUpdated, u.CommitFiles() +} + +// CopyFile uploads a file to the RP2040. +// The file will be written to a .tmp file on the RP2040. Once you want to commit the file change use the COMMIT command. +// It returns a bool that indicates whether the file needed to be updated. +// Only files that don't match the hash will be updated unless force is true. +func (u *TrapMessenger) CopyFile(localFile, destFile string, force bool) (bool, error) { + destBase := filepath.Base(destFile) + compressedBase := destBase + ".ztmp" + tmpBase := destBase + ".tmp" + log.Printf("Uploading '%s' as '%s'", destFile, tmpBase) + + localData, err := os.ReadFile(localFile) + if err != nil { + return false, fmt.Errorf("failed to read local file %s: %v", localFile, err) + } + + h := sha256.Sum256(localData) + localHash := hex.EncodeToString(h[:])[:10] + + lsResp, err := u.SendMessage(Message{Type: "LS", Payload: destBase + "," + compressedBase + "," + tmpBase}) + if err != nil { + return false, fmt.Errorf("failed to list files: %v", err) + } + var fileHashes map[string]string + if err := json.Unmarshal([]byte(lsResp.Payload), &fileHashes); err != nil { + return false, fmt.Errorf("failed to parse LS response: %v", err) + } + + if fileHashes[destBase] == localHash { + log.Printf("\tFile is already up to date.") + if !force { + return false, nil + } + log.Println("\tForce flag is set, still uploading.") + } + if fileHashes[tmpBase] == localHash { + log.Printf("\t.tmp file is already up to date.") + if !force { + return false, nil + } + log.Println("\tForce flag is set, still uploading.") + } + + if _, ok := fileHashes[compressedBase]; ok { + if err := HandleResponse(u.SendMessage(Message{Type: "DELETE", Payload: compressedBase})); err != nil { + return false, fmt.Errorf("failed to delete temp file: %v", err) + } + } + + var compressed bytes.Buffer + fw, err := flate.NewWriter(&compressed, flate.HuffmanOnly) + if err != nil { + return false, fmt.Errorf("failed to create compressor: %v", err) + } + if _, err := fw.Write(localData); err != nil { + return false, fmt.Errorf("failed to compress file: %v", err) + } + if err := fw.Close(); err != nil { + return false, fmt.Errorf("failed to finalize compression: %v", err) + } + encoded := base64.StdEncoding.EncodeToString(compressed.Bytes()) + log.Infof("\t%d bytes -> %d bytes compressed (%.0f%%)", len(localData), compressed.Len(), float64(compressed.Len())/float64(len(localData))*100) + + const chunkSize = 500 + totalChunks := (len(encoded) + chunkSize - 1) / chunkSize + for i := 0; i < len(encoded); i += chunkSize { + chunkNum := i/chunkSize + 1 + log.Infof("\t%s: %d/%d", filepath.Base(localFile), chunkNum, totalChunks) + chunk, err := json.Marshal([]string{encoded[i:min(i+chunkSize, len(encoded))]}) + if err != nil { + return false, fmt.Errorf("failed to marshal chunk: %v", err) + } + if err := HandleResponse(u.SendMessage(Message{Type: "WRITE", Payload: compressedBase + "," + string(chunk)})); err != nil { + return false, fmt.Errorf("failed to write chunk at offset %d: %v", i, err) + } + } + + log.Println("\tDecompressing...") + if err := HandleResponse(u.SendMessage(Message{Type: "DECOMPRESS", Payload: compressedBase + "," + tmpBase})); err != nil { + return false, fmt.Errorf("failed to decompress file: %v", err) + } + + log.Println("\tVerifying...") + lsResp2, err := u.SendMessage(Message{Type: "LS", Payload: tmpBase}) + if err != nil { + return false, fmt.Errorf("failed to verify file: %v", err) + } + var fileHashes2 map[string]string + if err := json.Unmarshal([]byte(lsResp2.Payload), &fileHashes2); err != nil { + return false, fmt.Errorf("failed to parse verify LS response: %v", err) + } + if fileHashes2[tmpBase] != localHash { + return false, fmt.Errorf("file verification failed: hash mismatch") + } + + log.Printf("\tFile '%s' copied successfully.", tmpBase) + return true, nil +} diff --git a/internal/tc2-hat-trap-cli/main.go b/internal/tc2-hat-trap-cli/main.go index 7bddc2f..5978762 100644 --- a/internal/tc2-hat-trap-cli/main.go +++ b/internal/tc2-hat-trap-cli/main.go @@ -1,12 +1,9 @@ package trapcli import ( - "encoding/json" "errors" "fmt" "os" - "strings" - "time" comms "github.com/TheCacophonyProject/tc2-hat-controller/internal/tc2-hat-comms" @@ -23,12 +20,14 @@ var ( ) type Args struct { - Command *Command `arg:"subcommand:command" help:"Send a command."` - Read *Read `arg:"subcommand:read" help:"Read from a variable."` - Write *Write `arg:"subcommand:write" help:"Write to a variable."` - Listen *Listen `arg:"subcommand:listen" help:"Continuously listen for messages from the RP2040."` - Message *CMDMessage `arg:"subcommand:msg" help:"Send a message to the RP2040."` - BaudRate int `arg:"--baud-rate" help:"Baud rate for UART communication."` + Listen *Listen `arg:"subcommand:listen" help:"Continuously listen for messages from the RP2040."` + Message *CMDMessage `arg:"subcommand:msg" help:"Send a message to the RP2040."` + CopyFile *CopyFile `arg:"subcommand:copy-file" help:"Copy a file to the RP2040."` + CopyDir *CopyDir `arg:"subcommand:copy-dir" help:"Copy all files from a directory to the RP2040."` + Restart *Restart `arg:"subcommand:restart" help:"Restart the RP2040."` + ReadTime *ReadTime `arg:"subcommand:read-time" help:"Read the time from the RP2040."` + WriteTime *WriteTime `arg:"subcommand:write-time" help:"Write the time to the RP2040."` + BaudRate int `arg:"--baud-rate" help:"Baud rate for UART communication."` goconfig.ConfigArgs logging.LogArgs } @@ -39,44 +38,32 @@ type CMDMessage struct { Payload string `arg:"--payload,required" help:"The payload of the message to send."` } -type Command struct { - Command string `arg:"--command,required" help:"The command to run."` +type CopyFile struct { + Source string `arg:"--source,required" help:"The source file to copy."` + Dest string `arg:"--dest,required" help:"The destination file to copy to."` + Force bool `arg:"--force" help:"Force overwrite of the destination file."` } -type Read struct { - Variable string `arg:"--variable,required" help:"The variable to read from."` +type CopyDir struct { + Source string `arg:"--source,required" help:"The source directory to copy files from."` + Dest string `arg:"--dest,required" help:"The destination directory on the RP2040."` + Force bool `arg:"--force" help:"Force overwrite of the files."` } -type Write struct { - Variable string `arg:"--variable,required" help:"The variable to write to."` - Value string `arg:"--value,required" help:"The value to write."` +type ReadTime struct{} + +type WriteTime struct { + Time string `arg:"--time" help:"The time to write."` } type Listen struct{} +type Restart struct{} + var defaultArgs = Args{ BaudRate: 9600, } -func sendMessage(msg comms.Message, port *serialhelper.SerialPort) (*comms.Message, error) { - line := msg.ToUARTLine() - log.Println("Sending:", strings.TrimSpace(line)) - - if err := port.Write([]byte(line)); err != nil { - return nil, err - } - - select { - case line, ok := <-port.Lines: - if !ok { - return nil, fmt.Errorf("serial port closed while waiting for response") - } - return comms.ParseLine(line) - case <-time.After(5 * time.Second): - return nil, fmt.Errorf("timeout waiting for response") - } -} - func procArgs(input []string) (Args, error) { args := defaultArgs @@ -90,7 +77,7 @@ func procArgs(input []string) (Args, error) { os.Exit(0) } if errors.Is(err, arg.ErrVersion) { - fmt.Println(version) + log.Infoln(version) os.Exit(0) } return args, err @@ -111,57 +98,62 @@ func Run(inputArgs []string, ver string) error { } defer port.Close() - switch { - case args.Listen != nil: - fmt.Println("Listening for messages from RP2040 (Ctrl+C to stop)...") + if args.Listen != nil { + log.Info("Listening for messages from RP2040 (Ctrl+C to stop)...") for line := range port.Lines { msg, err := comms.ParseLine(line) if err != nil { - fmt.Printf("raw: %s\n", line) + log.Infof("raw: %s\n", line) log.Warnf("Failed to parse incoming message %q: %v", line, err) continue } log.Println("Received:", msg) } return nil + } - case args.Command != nil: - data, err := json.Marshal(map[string]string{"command": args.Command.Command}) - if err != nil { - return err - } - return respond(sendMessage(comms.Message{Type: "command", Payload: string(data)}, port)) + messenger := comms.NewTrapMessenger(port) + messenger.Start() - case args.Read != nil: - data, err := json.Marshal(map[string]string{"var": args.Read.Variable}) + switch { + case args.Message != nil: + message := comms.Message{Type: args.Message.Type, Payload: args.Message.Payload} + return comms.HandleResponse(messenger.SendMessage(message)) + + case args.Restart != nil: + return messenger.Restart() + + case args.CopyFile != nil: + fileUpdated, err := messenger.CopyFile(args.CopyFile.Source, args.CopyFile.Dest, args.CopyFile.Force) if err != nil { return err } - return respond(sendMessage(comms.Message{Type: "read", Payload: string(data)}, port)) + if fileUpdated { + log.Info("File updated.") + } else { + log.Info("File is already up to date.") + } + return messenger.CommitFiles() - case args.Write != nil: - data, err := json.Marshal(map[string]string{"var": args.Write.Variable, "val": args.Write.Value}) + case args.CopyDir != nil: + fileUpdated, err := messenger.CopyDir(args.CopyDir.Source, args.CopyDir.Dest, args.CopyDir.Force) if err != nil { return err } - return respond(sendMessage(comms.Message{Type: "write", Payload: string(data)}, port)) + if fileUpdated { + log.Info("Files updated.") + } else { + log.Info("Files are already up to date.") + } + return nil - case args.Message != nil: - message := comms.Message{ID: args.Message.ID, Type: args.Message.Type, Payload: args.Message.Payload} - return respond(sendMessage(message, port)) + case args.ReadTime != nil: + return messenger.ReadTime() + + case args.WriteTime != nil: + return messenger.WriteTime(args.WriteTime.Time) default: return fmt.Errorf("no subcommand given") } } - -func respond(response *comms.Message, err error) error { - if err != nil { - return err - } - if response.Type == "NACK" { - return fmt.Errorf("NACK response: %s", response.Payload) - } - fmt.Printf("type=%s payload=%s\n", response.Type, response.Payload) - return nil -}