corrected all bugs

This commit is contained in:
2026-01-11 15:33:44 +01:00
parent 46ee44c6c9
commit 9837657dd9
10 changed files with 992 additions and 497 deletions

View File

@@ -7,6 +7,7 @@ import (
"git.rouggy.com/rouggy/ShackMaster/internal/config"
"git.rouggy.com/rouggy/ShackMaster/internal/devices/antennagenius"
"git.rouggy.com/rouggy/ShackMaster/internal/devices/flexradio"
"git.rouggy.com/rouggy/ShackMaster/internal/devices/powergenius"
"git.rouggy.com/rouggy/ShackMaster/internal/devices/rotatorgenius"
"git.rouggy.com/rouggy/ShackMaster/internal/devices/tunergenius"
@@ -25,6 +26,7 @@ type DeviceManager struct {
antennaGenius *antennagenius.Client
rotatorGenius *rotatorgenius.Client
ultrabeam *ultrabeam.Client
flexRadio *flexradio.Client
solarClient *solar.Client
weatherClient *weather.Client
@@ -42,6 +44,10 @@ type DeviceManager struct {
ultrabeamDirectionSet bool // True if user has explicitly set a direction
lastFreqUpdateTime time.Time // Last time we sent frequency update
freqUpdateCooldown time.Duration // Minimum time between updates
// Cached Ultrabeam state for FlexRadio interlock (avoid mutex contention)
ultrabeamMotorsMoving int
ultrabeamStateMu sync.RWMutex
}
type SystemStatus struct {
@@ -51,6 +57,7 @@ type SystemStatus struct {
AntennaGenius *antennagenius.Status `json:"antenna_genius"`
RotatorGenius *rotatorgenius.Status `json:"rotator_genius"`
Ultrabeam *ultrabeam.Status `json:"ultrabeam"`
FlexRadio *flexradio.Status `json:"flexradio,omitempty"`
Solar *solar.SolarData `json:"solar"`
Weather *weather.WeatherData `json:"weather"`
Timestamp time.Time `json:"timestamp"`
@@ -60,12 +67,12 @@ func NewDeviceManager(cfg *config.Config, hub *Hub) *DeviceManager {
return &DeviceManager{
config: cfg,
hub: hub,
updateInterval: 1 * time.Second, // Update status every second
updateInterval: 200 * time.Millisecond, // Update status every second
stopChan: make(chan struct{}),
freqThreshold: 25000, // 25 kHz default
autoTrackEnabled: true, // Enabled by default
ultrabeamDirection: 0, // Normal direction by default
freqUpdateCooldown: 2 * time.Second, // Wait 2 seconds between updates
freqThreshold: 25000, // 25 kHz default
autoTrackEnabled: true, // Enabled by default
ultrabeamDirection: 0, // Normal direction by default
freqUpdateCooldown: 500 * time.Millisecond, // 500ms cooldown (was 2sec)
}
}
@@ -107,6 +114,31 @@ func (dm *DeviceManager) Initialize() error {
dm.config.Devices.Ultrabeam.Port,
)
// Initialize FlexRadio if enabled
if dm.config.Devices.FlexRadio.Enabled {
log.Printf("Initializing FlexRadio: host=%s port=%d", dm.config.Devices.FlexRadio.Host, dm.config.Devices.FlexRadio.Port)
dm.flexRadio = flexradio.New(
dm.config.Devices.FlexRadio.Host,
dm.config.Devices.FlexRadio.Port,
dm.config.Devices.FlexRadio.InterlockName,
)
// Set callback to check if transmit is allowed (based on Ultrabeam motors)
// Use cached state to avoid mutex contention with update loop
dm.flexRadio.SetTransmitCheckCallback(func() bool {
dm.ultrabeamStateMu.RLock()
motorsMoving := dm.ultrabeamMotorsMoving
dm.ultrabeamStateMu.RUnlock()
// Block transmit if motors are moving
return motorsMoving == 0
})
// Set callback for immediate frequency changes (no waiting for update cycle)
dm.flexRadio.SetFrequencyChangeCallback(func(freqMHz float64) {
dm.handleFrequencyChange(freqMHz)
})
}
// Initialize Solar data client
dm.solarClient = solar.New()
@@ -154,6 +186,17 @@ func (dm *DeviceManager) Initialize() error {
}()
log.Println("Ultrabeam goroutine launched")
// Start FlexRadio if enabled
if dm.flexRadio != nil {
log.Println("Starting FlexRadio connection...")
go func() {
if err := dm.flexRadio.Start(); err != nil {
log.Printf("Warning: Failed to start FlexRadio: %v", err)
}
}()
log.Println("FlexRadio goroutine launched")
}
log.Println("Device manager initialized")
return nil
}
@@ -164,6 +207,69 @@ func (dm *DeviceManager) Start() error {
return nil
}
// handleFrequencyChange is called immediately when FlexRadio frequency changes
// This provides instant auto-track response instead of waiting for updateStatus cycle
func (dm *DeviceManager) handleFrequencyChange(freqMHz float64) {
// Check if ultrabeam is initialized
if dm.ultrabeam == nil {
return
}
// Check cooldown first
timeSinceLastUpdate := time.Since(dm.lastFreqUpdateTime)
if timeSinceLastUpdate < dm.freqUpdateCooldown {
return
}
// Use cached status instead of calling GetStatus (which can block)
dm.statusMu.RLock()
hasStatus := dm.lastStatus != nil
var ubStatus *ultrabeam.Status
if hasStatus {
ubStatus = dm.lastStatus.Ultrabeam
}
dm.statusMu.RUnlock()
if ubStatus == nil || !ubStatus.Connected {
return
}
// Don't update if motors are already moving
if ubStatus.MotorsMoving != 0 {
return
}
freqKhz := int(freqMHz * 1000)
ultrabeamFreqKhz := ubStatus.Frequency
// Only track if in Ultrabeam range (7-54 MHz)
if freqKhz < 7000 || freqKhz > 54000 {
return
}
freqDiff := freqKhz - ultrabeamFreqKhz
if freqDiff < 0 {
freqDiff = -freqDiff
}
freqDiffHz := freqDiff * 1000
if freqDiffHz >= dm.freqThreshold {
directionToUse := dm.ultrabeamDirection
if !dm.ultrabeamDirectionSet && ubStatus.Direction != 0 {
directionToUse = ubStatus.Direction
}
log.Printf("Auto-track (immediate): Updating to %d kHz (diff=%d kHz)", freqKhz, freqDiff)
if err := dm.ultrabeam.SetFrequency(freqKhz, directionToUse); err != nil {
log.Printf("Auto-track (immediate): Failed: %v", err)
} else {
dm.lastFreqUpdateTime = time.Now()
}
}
}
func (dm *DeviceManager) Stop() {
log.Println("Stopping device manager...")
close(dm.stopChan)
@@ -250,19 +356,57 @@ func (dm *DeviceManager) updateStatus() {
if !dm.ultrabeamDirectionSet {
dm.ultrabeamDirection = ubStatus.Direction
}
// Cache motors state for FlexRadio interlock callback
dm.ultrabeamStateMu.Lock()
previousMotors := dm.ultrabeamMotorsMoving
dm.ultrabeamMotorsMoving = ubStatus.MotorsMoving
dm.ultrabeamStateMu.Unlock()
// Log motor state changes
if previousMotors != ubStatus.MotorsMoving {
if ubStatus.MotorsMoving > 0 {
log.Printf("Ultrabeam: Motors STARTED (bitmask=%d)", ubStatus.MotorsMoving)
} else {
log.Printf("Ultrabeam: Motors STOPPED")
}
}
} else {
log.Printf("Ultrabeam error: %v", err)
}
// Auto frequency tracking: Update Ultrabeam when TunerGenius frequency differs from Ultrabeam
if dm.autoTrackEnabled && status.TunerGenius != nil && status.TunerGenius.Connected && status.Ultrabeam != nil && status.Ultrabeam.Connected {
tunerFreqKhz := int(status.TunerGenius.FreqA) // TunerGenius frequency is already in kHz
// FlexRadio (use direct cache access to avoid mutex contention)
if dm.flexRadio != nil {
// Access lastStatus directly from FlexRadio's internal cache
// The messageLoop updates this in real-time, no need to block on GetStatus
frStatus, err := dm.flexRadio.GetStatus()
if err == nil && frStatus != nil {
status.FlexRadio = frStatus
}
}
// Auto frequency tracking: Update Ultrabeam when radio frequency differs from Ultrabeam
// Priority: FlexRadio (fast) > TunerGenius (slow backup)
var radioFreqKhz int
var radioSource string
if dm.flexRadio != nil && status.FlexRadio != nil && status.FlexRadio.Connected && status.FlexRadio.Frequency > 0 {
// Use FlexRadio frequency (in MHz, convert to kHz)
radioFreqKhz = int(status.FlexRadio.Frequency * 1000)
radioSource = "FlexRadio"
} else if dm.autoTrackEnabled && status.TunerGenius != nil && status.TunerGenius.Connected {
// Fallback to TunerGenius frequency (already in kHz)
radioFreqKhz = int(status.TunerGenius.FreqA)
radioSource = "TunerGenius"
}
if radioFreqKhz > 0 && status.Ultrabeam != nil && status.Ultrabeam.Connected {
ultrabeamFreqKhz := status.Ultrabeam.Frequency // Ultrabeam frequency in kHz
// Only do auto-track if frequency is in Ultrabeam range (40M-6M: 7000-54000 kHz)
// This prevents retraction when slice is closed (FreqA becomes 0) or on out-of-range bands
if tunerFreqKhz >= 7000 && tunerFreqKhz <= 54000 {
freqDiff := tunerFreqKhz - ultrabeamFreqKhz
if radioFreqKhz >= 7000 && radioFreqKhz <= 54000 {
freqDiff := radioFreqKhz - ultrabeamFreqKhz
if freqDiff < 0 {
freqDiff = -freqDiff
}
@@ -284,10 +428,10 @@ func (dm *DeviceManager) updateStatus() {
if timeSinceLastUpdate < dm.freqUpdateCooldown {
log.Printf("Auto-track: Cooldown active (%v remaining), skipping update", dm.freqUpdateCooldown-timeSinceLastUpdate)
} else {
log.Printf("Auto-track: Frequency differs by %d kHz, updating Ultrabeam to %d kHz (direction=%d)", freqDiff, tunerFreqKhz, directionToUse)
log.Printf("Auto-track (%s): Frequency differs by %d kHz, updating Ultrabeam to %d kHz (direction=%d)", radioSource, freqDiff, radioFreqKhz, directionToUse)
// Send to Ultrabeam with saved or current direction
if err := dm.ultrabeam.SetFrequency(tunerFreqKhz, directionToUse); err != nil {
if err := dm.ultrabeam.SetFrequency(radioFreqKhz, directionToUse); err != nil {
log.Printf("Auto-track: Failed to update Ultrabeam: %v (will retry)", err)
} else {
log.Printf("Auto-track: Successfully sent frequency to Ultrabeam")

View File

@@ -26,6 +26,7 @@ type DevicesConfig struct {
AntennaGenius AntennaGeniusConfig `yaml:"antenna_genius"`
RotatorGenius RotatorGeniusConfig `yaml:"rotator_genius"`
Ultrabeam UltrabeamConfig `yaml:"ultrabeam"`
FlexRadio FlexRadioConfig `yaml:"flexradio"`
}
type WebSwitchConfig struct {
@@ -57,6 +58,13 @@ type UltrabeamConfig struct {
Port int `yaml:"port"`
}
type FlexRadioConfig struct {
Enabled bool `yaml:"enabled"`
Host string `yaml:"host"`
Port int `yaml:"port"`
InterlockName string `yaml:"interlock_name"`
}
type WeatherConfig struct {
OpenWeatherMapAPIKey string `yaml:"openweathermap_api_key"`
LightningEnabled bool `yaml:"lightning_enabled"`

View File

@@ -0,0 +1,412 @@
package flexradio
import (
"bufio"
"fmt"
"log"
"net"
"strconv"
"strings"
"sync"
"time"
)
type Client struct {
host string
port int
conn net.Conn
reader *bufio.Reader
connMu sync.Mutex
interlockID string
interlockName string
interlockMu sync.RWMutex
lastStatus *Status
statusMu sync.RWMutex
cmdSeq int
cmdSeqMu sync.Mutex
running bool
stopChan chan struct{}
// Callbacks
checkTransmitAllowed func() bool
onFrequencyChange func(freqMHz float64)
}
func New(host string, port int, interlockName string) *Client {
return &Client{
host: host,
port: port,
interlockName: interlockName,
stopChan: make(chan struct{}),
lastStatus: &Status{
Connected: false,
},
}
}
// SetTransmitCheckCallback sets the callback function to check if transmit is allowed
func (c *Client) SetTransmitCheckCallback(callback func() bool) {
c.checkTransmitAllowed = callback
}
// SetFrequencyChangeCallback sets the callback function called when frequency changes
func (c *Client) SetFrequencyChangeCallback(callback func(freqMHz float64)) {
c.onFrequencyChange = callback
}
func (c *Client) Connect() error {
c.connMu.Lock()
defer c.connMu.Unlock()
if c.conn != nil {
return nil
}
addr := fmt.Sprintf("%s:%d", c.host, c.port)
log.Printf("FlexRadio: Connecting to %s...", addr)
conn, err := net.DialTimeout("tcp", addr, 5*time.Second)
if err != nil {
return fmt.Errorf("failed to connect: %w", err)
}
c.conn = conn
c.reader = bufio.NewReader(conn)
log.Println("FlexRadio: Connected successfully")
return nil
}
func (c *Client) Start() error {
if c.running {
return nil
}
if err := c.Connect(); err != nil {
return err
}
// Update connected status
c.statusMu.Lock()
if c.lastStatus != nil {
c.lastStatus.Connected = true
}
c.statusMu.Unlock()
c.running = true
// Start message listener
go c.messageLoop()
// Create interlock (no sleep needed, connection is synchronous)
if err := c.createInterlock(); err != nil {
log.Printf("FlexRadio: Failed to create interlock: %v", err)
return err
}
return nil
}
func (c *Client) Stop() {
if !c.running {
return
}
c.running = false
close(c.stopChan)
c.connMu.Lock()
if c.conn != nil {
c.conn.Close()
c.conn = nil
c.reader = nil
}
c.connMu.Unlock()
// Update connected status
c.statusMu.Lock()
if c.lastStatus != nil {
c.lastStatus.Connected = false
}
c.statusMu.Unlock()
}
func (c *Client) getNextSeq() int {
c.cmdSeqMu.Lock()
defer c.cmdSeqMu.Unlock()
c.cmdSeq++
return c.cmdSeq
}
func (c *Client) sendCommand(cmd string) (string, error) {
c.connMu.Lock()
defer c.connMu.Unlock()
if c.conn == nil {
return "", fmt.Errorf("not connected")
}
seq := c.getNextSeq()
fullCmd := fmt.Sprintf("C%d|%s\n", seq, cmd)
log.Printf("FlexRadio TX: %s", strings.TrimSpace(fullCmd))
_, err := c.conn.Write([]byte(fullCmd))
if err != nil {
c.conn = nil
c.reader = nil
return "", fmt.Errorf("failed to send command: %w", err)
}
return "", nil
}
func (c *Client) messageLoop() {
log.Println("FlexRadio: Message loop started")
for c.running {
c.connMu.Lock()
if c.conn == nil || c.reader == nil {
c.connMu.Unlock()
time.Sleep(1 * time.Second)
if err := c.Connect(); err != nil {
log.Printf("FlexRadio: Reconnect failed: %v", err)
continue
}
continue
}
// Set read deadline to allow periodic checks
c.conn.SetReadDeadline(time.Now().Add(2 * time.Second))
line, err := c.reader.ReadString('\n')
c.connMu.Unlock()
if err != nil {
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
// Timeout is expected, continue
continue
}
log.Printf("FlexRadio: Read error: %v", err)
c.connMu.Lock()
if c.conn != nil {
c.conn.Close()
c.conn = nil
c.reader = nil
}
c.connMu.Unlock()
// Update connected status
c.statusMu.Lock()
if c.lastStatus != nil {
c.lastStatus.Connected = false
}
c.statusMu.Unlock()
continue
}
line = strings.TrimSpace(line)
if line == "" {
continue
}
c.handleMessage(line)
}
log.Println("FlexRadio: Message loop stopped")
}
func (c *Client) handleMessage(msg string) {
// Response format: R<seq>|<status>|<data>
if strings.HasPrefix(msg, "R") {
c.handleResponse(msg)
return
}
// Status format: S<handle>|<key>=<value> ...
if strings.HasPrefix(msg, "S") {
c.handleStatus(msg)
return
}
// Version/handle format: V<version>|H<handle>
if strings.HasPrefix(msg, "V") {
log.Printf("FlexRadio: Version/Handle received: %s", msg)
return
}
// Message format: M<handle>|<message>
if strings.HasPrefix(msg, "M") {
log.Printf("FlexRadio: Message: %s", msg)
return
}
}
func (c *Client) handleResponse(msg string) {
// Format: R<seq>|<status>|<data>
parts := strings.SplitN(msg, "|", 3)
if len(parts) < 2 {
return
}
status := parts[1]
if status != "0" {
log.Printf("FlexRadio: Command error: status=%s", status)
return
}
// Check if this is interlock create response
if len(parts) >= 3 && parts[2] != "" {
// This is likely the interlock ID
interlockID := parts[2]
c.interlockMu.Lock()
c.interlockID = interlockID
c.interlockMu.Unlock()
log.Printf("FlexRadio: Interlock created with ID: %s", interlockID)
c.statusMu.Lock()
c.lastStatus.InterlockID = interlockID
c.lastStatus.InterlockState = InterlockStateReady
c.statusMu.Unlock()
}
}
func (c *Client) handleStatus(msg string) {
// Format: S<handle>|<key>=<value> ...
parts := strings.SplitN(msg, "|", 2)
if len(parts) < 2 {
return
}
data := parts[1]
// Parse key=value pairs
pairs := strings.Fields(data)
statusMap := make(map[string]string)
for _, pair := range pairs {
kv := strings.SplitN(pair, "=", 2)
if len(kv) == 2 {
statusMap[kv[0]] = kv[1]
}
}
// Check for interlock state changes
if state, ok := statusMap["state"]; ok && strings.Contains(msg, "interlock") {
c.handleInterlockState(state, statusMap)
}
// Check for slice updates (frequency changes)
if strings.Contains(msg, "slice") {
if rfFreq, ok := statusMap["RF_frequency"]; ok {
freq, err := strconv.ParseFloat(rfFreq, 64)
if err == nil {
c.statusMu.Lock()
oldFreq := c.lastStatus.Frequency
c.lastStatus.Frequency = freq
c.statusMu.Unlock()
// Only log significant frequency changes (> 1 kHz)
if oldFreq == 0 || (freq-oldFreq)*(freq-oldFreq) > 0.001 {
log.Printf("FlexRadio: Frequency updated to %.6f MHz", freq)
// Trigger callback for immediate auto-track
if c.onFrequencyChange != nil {
go c.onFrequencyChange(freq)
}
}
}
}
}
}
func (c *Client) handleInterlockState(state string, _ map[string]string) {
log.Printf("FlexRadio: Interlock state changed to: %s", state)
c.statusMu.Lock()
c.lastStatus.InterlockState = state
c.statusMu.Unlock()
// Handle PTT_REQUESTED - this is where we decide to allow or block transmit
if state == "PTT_REQUESTED" {
c.handlePTTRequest()
}
}
func (c *Client) handlePTTRequest() {
log.Println("FlexRadio: PTT requested, checking if transmit is allowed...")
c.interlockMu.RLock()
interlockID := c.interlockID
c.interlockMu.RUnlock()
if interlockID == "" {
log.Println("FlexRadio: No interlock ID, cannot respond to PTT request")
return
}
// Check if transmit is allowed via callback
allowed := true
if c.checkTransmitAllowed != nil {
allowed = c.checkTransmitAllowed()
}
if allowed {
log.Println("FlexRadio: Transmit ALLOWED - sending ready")
c.sendCommand(fmt.Sprintf("interlock ready %s", interlockID))
// Update state immediately for UI
c.statusMu.Lock()
c.lastStatus.InterlockState = InterlockStateReady
c.statusMu.Unlock()
} else {
log.Println("FlexRadio: Transmit BLOCKED - sending not_ready")
c.sendCommand(fmt.Sprintf("interlock not_ready %s", interlockID))
// Update state immediately for UI
c.statusMu.Lock()
c.lastStatus.InterlockState = InterlockStateNotReady
c.statusMu.Unlock()
}
}
func (c *Client) createInterlock() error {
log.Printf("FlexRadio: Creating interlock with name: %s", c.interlockName)
// Format: interlock create type=ant name=<name> serial=<serial>
cmd := fmt.Sprintf("interlock create type=ant name=%s serial=ShackMaster", c.interlockName)
_, err := c.sendCommand(cmd)
if err != nil {
return fmt.Errorf("failed to create interlock: %w", err)
}
// Subscribe to slice updates for frequency tracking
log.Println("FlexRadio: Subscribing to slice updates...")
_, err = c.sendCommand("sub slice all")
if err != nil {
log.Printf("FlexRadio: Warning - failed to subscribe to slices: %v", err)
}
return nil
}
func (c *Client) GetStatus() (*Status, error) {
c.statusMu.RLock()
defer c.statusMu.RUnlock()
if c.lastStatus == nil {
return &Status{Connected: false}, nil
}
// Create a copy
status := *c.lastStatus
// DON'T lock connMu here - it causes 4-second blocking!
// The messageLoop updates Connected status, and we trust the cached value
return &status, nil
}

View File

@@ -0,0 +1,21 @@
package flexradio
// Status represents the FlexRadio status
type Status struct {
Connected bool `json:"connected"`
InterlockID string `json:"interlock_id"`
InterlockState string `json:"interlock_state"`
Frequency float64 `json:"frequency"` // MHz
Model string `json:"model"`
Serial string `json:"serial"`
Version string `json:"version"`
}
// InterlockState represents possible interlock states
const (
InterlockStateReady = "READY"
InterlockStateNotReady = "NOT_READY"
InterlockStatePTTRequested = "PTT_REQUESTED"
InterlockStateTransmitting = "TRANSMITTING"
InterlockStateUnkeyRequested = "UNKEY_REQUESTED"
)

View File

@@ -106,9 +106,12 @@ func (c *Client) pollLoop() {
ticker := time.NewTicker(2 * time.Second) // Increased from 500ms to 2s
defer ticker.Stop()
pollCount := 0
for {
select {
case <-ticker.C:
pollCount++
// Try to connect if not connected
c.connMu.Lock()
@@ -161,6 +164,10 @@ func (c *Client) pollLoop() {
status.ProgressTotal = progress[0]
status.ProgressCurrent = progress[1]
}
} else {
// Motors stopped - reset progress
status.ProgressTotal = 0
status.ProgressCurrent = 0
}
c.statusMu.Lock()
@@ -388,69 +395,6 @@ func (c *Client) queryStatus() (*Status, error) {
return status, nil
}
// queryElementLengths queries element lengths (command 9)
func (c *Client) queryElementLengths() ([]int, error) {
reply, err := c.sendCommand(CMD_READ_BANDS, nil)
if err != nil {
return nil, err
}
// Debug: log raw bytes
log.Printf("Ultrabeam element lengths raw reply (%d bytes): %v", len(reply), reply)
// Try to extract 6 words - the protocol says 6 words (12 bytes)
// But we're receiving 14 bytes, so there might be padding
if len(reply) < 12 {
return nil, fmt.Errorf("element lengths reply too short: %d bytes", len(reply))
}
lengths := make([]int, 6)
// Try different interpretations
log.Printf("=== Attempting different parsings ===")
// Method 1: Standard little-endian from byte 0
log.Printf("Method 1 (little-endian from 0):")
for i := 0; i < 6 && i*2+1 < len(reply); i++ {
lo := int(reply[i*2])
hi := int(reply[i*2+1])
val := lo | (hi << 8)
log.Printf(" Element %d: bytes[%d,%d] = [%d,%d] => %d mm", i, i*2, i*2+1, lo, hi, val)
}
// Method 2: Big-endian from byte 0
log.Printf("Method 2 (big-endian from 0):")
for i := 0; i < 6 && i*2+1 < len(reply); i++ {
hi := int(reply[i*2])
lo := int(reply[i*2+1])
val := lo | (hi << 8)
log.Printf(" Element %d: bytes[%d,%d] = [%d,%d] => %d mm", i, i*2, i*2+1, hi, lo, val)
}
// Method 3: Skip first 2 bytes, then little-endian
log.Printf("Method 3 (skip 2 bytes, little-endian):")
for i := 0; i < 6 && i*2+3 < len(reply); i++ {
lo := int(reply[i*2+2])
hi := int(reply[i*2+3])
val := lo | (hi << 8)
log.Printf(" Element %d: bytes[%d,%d] = [%d,%d] => %d mm", i, i*2+2, i*2+3, lo, hi, val)
}
// For now, use method 1 (original)
for i := 0; i < 6; i++ {
if i*2+1 >= len(reply) {
break
}
lo := int(reply[i*2])
hi := int(reply[i*2+1])
lengths[i] = lo | (hi << 8)
}
log.Printf("Final lengths: %v", lengths)
return lengths, nil
}
// queryProgress queries motor progress (command 10)
func (c *Client) queryProgress() ([]int, error) {
reply, err := c.sendCommand(CMD_PROGRESS, nil)