Skip to content
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
10 changes: 10 additions & 0 deletions .github/workflows/goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/dist
_release/attiny-firmware.hex
_release/attiny-firmware.hex.sha256
_release/mpy
/tc2-hat-controller
2 changes: 2 additions & 0 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
184 changes: 53 additions & 131 deletions internal/tc2-hat-comms/trap-control.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"encoding/json"
"fmt"
"strconv"
"sync"
"time"

"github.com/TheCacophonyProject/event-reporter/v3/eventclient"
Expand All @@ -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{}
Expand All @@ -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
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
}
Loading
Loading