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
7 changes: 4 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ ARG TARGETOS
ARG TARGETARCH
ARG TARGETVARIANT

# build the binary (Docker mode, no GUI - default build excludes gui tag)
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} GOARM=${TARGETVARIANT#v} \
go build -ldflags="-w -s" -o /bridge ./cmd/bridge
# Read version from VERSION file and embed it at build time
RUN VERSION=$(cat VERSION | tr -d '[:space:]') && \
CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} GOARM=${TARGETVARIANT#v} \
go build -ldflags="-w -s -X bridge/pkg/bridge.Version=${VERSION}" -o /bridge ./cmd/bridge

FROM alpine:latest

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.7.3
0.7.35
49 changes: 1 addition & 48 deletions cmd/bridge/run_cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ func runCLI() {
serialPort := "/dev/ttyUSB0"

// Check if it exists, if not give the user a semi-helpful error message
// TODO: probably come back and re-word this, im not sure it it's 100% clear to non-technical users..
if _, err := os.Stat(serialPort); os.IsNotExist(err) {
log.Printf(`
Oops! Looks like the serial port is not configured correctly.
Expand Down Expand Up @@ -118,8 +117,6 @@ Ex: /dev/<YOUR_SERIAL_PORT>:/dev/ttyUSB0
lastSuccessTime = time.Now()

errChan := make(chan error, 1)
healthCheckTicker := time.NewTicker(60 * time.Second) // Increased from 30s to give MQTT more time
defer healthCheckTicker.Stop()

go func() {
err := b.Start(nil, func(msg string) {
Expand All @@ -128,10 +125,7 @@ Ex: /dev/<YOUR_SERIAL_PORT>:/dev/ttyUSB0
errChan <- err
}()

// Health monitoring loop
healthCheckFailures := 0
maxHealthCheckFailures := 10 // Increased from 5 to be more tolerant

// Main event loop - just handle shutdown and errors
for {
select {
case <-sigChan:
Expand Down Expand Up @@ -176,47 +170,6 @@ Ex: /dev/<YOUR_SERIAL_PORT>:/dev/ttyUSB0
// Break out of inner loop to attempt reconnection
goto reconnectLoop
}
case <-healthCheckTicker.C:
// Periodic health check with smarter MQTT reconnection awareness
if !b.IsHealthy() {
healthCheckFailures++

// Check if MQTT is actively attempting to auto-reconnect
isMQTTReconnecting := b.IsMQTTReconnecting()
disconnectedDuration, reconnectAttempts := b.GetMQTTReconnectionInfo()

// Be more tolerant if MQTT is actively reconnecting
effectiveMaxFailures := maxHealthCheckFailures
if isMQTTReconnecting {
// Give MQTT more time to recover during session takeover scenarios
effectiveMaxFailures = maxHealthCheckFailures * 2
log.Printf("Health check failed (%d/%d) - MQTT reconnecting (disconnected for %v, attempt %d)",
healthCheckFailures, effectiveMaxFailures, disconnectedDuration, reconnectAttempts)
} else {
log.Printf("Health check failed (%d/%d)", healthCheckFailures, effectiveMaxFailures)
}

if healthCheckFailures >= effectiveMaxFailures {
if isMQTTReconnecting && disconnectedDuration < 10*time.Minute {
// MQTT is still trying and hasn't been disconnected too long
// Log but don't force reconnection yet
log.Printf("MQTT actively reconnecting for %v - allowing more time before forcing reset", disconnectedDuration)
} else {
log.Printf("Device appears to be in unrecoverable state after %d failed health checks", healthCheckFailures)
b.Disconnect()

// Force a longer delay and reset to try recovery
consecutiveFailures = maxConsecutiveFailures - 1 // Trigger near-circuit-breaker behavior
goto reconnectLoop
}
}
} else {
// Reset health check failures on successful check
if healthCheckFailures > 0 {
log.Printf("Health check recovered")
healthCheckFailures = 0
}
}
}
}

Expand Down
62 changes: 28 additions & 34 deletions pkg/bridge/bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ const (
SerialReadTimeout = 100 * time.Millisecond
)

// Version can be set at build time via ldflags:
// go build -ldflags="-X bridge/pkg/bridge.Version=1.0.0"
var Version = ""

// Message structures for MQTT communication
type SettingsMessage struct {
ConnectedBaud int `json:"connected_baud"`
Expand Down Expand Up @@ -90,23 +94,27 @@ func isUnusablePort(name string) bool {
return strings.Contains(low, "bluetooth") && !strings.Contains(low, "usb")
}

// getAppVersion reads the version from the VERSION file
// getAppVersion returns the application version
func getAppVersion() string {
// Get the executable path to help locate VERSION file
// First check if version was set at build time
if Version != "" {
return Version
}

// Fall back to reading from VERSION file (for local development)
execPath, _ := os.Executable()
execDir := filepath.Dir(execPath)

// Try multiple possible locations for the VERSION file
possiblePaths := []string{
"VERSION", // Current working directory
filepath.Join(execDir, "VERSION"), // Same directory as executable
filepath.Join(execDir, "..", "VERSION"), // Parent of executable directory
filepath.Join(execDir, "..", "..", "VERSION"), // Two levels up from executable
filepath.Join(execDir, "..", "..", "..", "VERSION"), // Three levels up
"../VERSION", // Relative parent directory
"../../VERSION", // Two levels up relative
"../../../VERSION", // Three levels up relative
"./VERSION", // Explicit current directory
"VERSION",
filepath.Join(execDir, "VERSION"),
filepath.Join(execDir, "..", "VERSION"),
filepath.Join(execDir, "..", "..", "VERSION"),
filepath.Join(execDir, "..", "..", "..", "VERSION"),
"../VERSION",
"../../VERSION",
"../../../VERSION",
"./VERSION",
}

for _, path := range possiblePaths {
Expand All @@ -118,7 +126,6 @@ func getAppVersion() string {
}
}

// Fallback version if file not found
return "unknown"
}

Expand Down Expand Up @@ -274,11 +281,6 @@ type Bridge struct {
statusMutex sync.Mutex // Changed from RWMutex for simplicity
dataTimeoutDuration time.Duration
dataActivityStopChan chan struct{}

// Health monitoring fields
lastHealthyTime time.Time
healthCheckInterval time.Duration
maxUnhealthyDuration time.Duration
}

func New(bridgeID string) *Bridge {
Expand All @@ -292,26 +294,28 @@ func New(bridgeID string) *Bridge {
currentStatus: "offline",
dataTimeoutDuration: DataActivityTimeout,
dataActivityStopChan: make(chan struct{}),
lastHealthyTime: time.Now(),
healthCheckInterval: 30 * time.Second,
maxUnhealthyDuration: 5 * time.Minute,
}
}

func (b *Bridge) SetConnectionLostHandler(h func(error)) { b.onConnectionLost = h }

// IsHealthy checks if the bridge is in a healthy state
// A bridge is healthy if both serial and MQTT connections are active
func (b *Bridge) IsHealthy() bool {
b.mu.RLock()
defer b.mu.RUnlock()

// Check if we have both serial and MQTT connections
if b.serialPort == nil || b.mqttClient == nil || !b.mqttClient.IsConnected() {
if b.serialPort == nil {
return false
}

if b.mqttClient == nil {
return false
}

// Check if we've been unhealthy for too long
if time.Since(b.lastHealthyTime) > b.maxUnhealthyDuration {
// MQTT connected or actively reconnecting counts as healthy
if !b.mqttClient.IsConnected() && !b.mqttClient.IsReconnecting() {
return false
}

Expand Down Expand Up @@ -349,13 +353,6 @@ func (b *Bridge) GetMQTTReconnectionInfo() (disconnectedDuration time.Duration,
return disconnectedDuration, attempts
}

// markHealthy updates the last healthy timestamp
func (b *Bridge) markHealthy() {
b.mu.Lock()
b.lastHealthyTime = time.Now()
b.mu.Unlock()
}

// updateStatus updates the bridge status with proper retention control
// LOCK ORDER: Always acquire mu.RLock BEFORE statusMutex to prevent deadlock with Disconnect()
func (b *Bridge) updateStatus(status string, retained bool) error {
Expand Down Expand Up @@ -451,9 +448,6 @@ func (b *Bridge) onDataReceived() {
}
}()

// Mark bridge as healthy when receiving data
b.markHealthy()

// If we were waiting, transition back to online
if b.getCurrentStatus() == "waiting" {
if err := b.updateStatus("online", true); err != nil {
Expand Down
7 changes: 7 additions & 0 deletions pkg/ui/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,7 @@ func (t *ShadcnTheme) Color(n fyne.ThemeColorName, v fyne.ThemeVariant) color.Co
secondary := color.RGBA{103, 103, 228, 255}
zinc50 := color.RGBA{250, 250, 250, 255}
zinc800 := color.RGBA{39, 39, 42, 255}
zinc900 := color.RGBA{24, 24, 27, 255}
switch n {
case theme.ColorNameBackground:
return black
Expand All @@ -360,6 +361,12 @@ func (t *ShadcnTheme) Color(n fyne.ThemeColorName, v fyne.ThemeVariant) color.Co
return secondary
case theme.ColorNameButton:
return zinc800
case theme.ColorNameInputBackground:
return zinc900
case theme.ColorNamePlaceHolder:
return color.RGBA{113, 113, 122, 255}
case theme.ColorNameDisabled:
return color.RGBA{113, 113, 122, 255}
}
return theme.DefaultTheme().Color(n, v)
}
Expand Down
Loading