Add interactive slew mode and fix joystick coordinate mapping

- New `dwarfctl interactive` command: persistent WS connection with
  real-time slew commands on a single session (no reconnect per command).
  Decodes DeviceAttitude notifications (cmd 15295) when available.
- Fixed coordinate mapping: 90°=UP (toward sky), 270°=DOWN (toward ground),
  confirmed by live testing on DWARF II in alt-az config.
- Monitor now decodes attitude notifications inline.
- Note: IMU attitude (15295) is NOT pushed during free slew — only during
  calibration/EQ solving. Position feedback requires astro calibration.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
This commit is contained in:
Jacquin Antoine
2026-07-12 15:54:05 +02:00
parent 5e23257822
commit 871021027b
2 changed files with 73 additions and 5 deletions

View File

@ -105,9 +105,9 @@ Tests en condition réelle, télescope posé verticalement sur trépied (pas en
| Angle | Axe | Direction observée |
|-------|-----|-------------------|
| 0° | Azimut (horizontal) | Rotation horaire vue de dessus |
| 90° | Altitude | **Descente** (vers le sol) |
| 90° | Altitude | **Montée** (vers le ciel) |
| 180° | Azimut | Rotation anti-horaire (opposé de 0°) |
| 270° | Altitude | **Montée** (vers le ciel) |
| 270° | Altitude | **Descente** (vers le sol) |
**IMPORTANT** : ces directions sont validées pour une config alt-az simple (télescope vertical sur trépied). En mode équatorial (après EQ calibration), le mapping peut changer car les moteurs changent de référentiel.

View File

@ -1,6 +1,7 @@
package main
import (
"bufio"
"fmt"
"math"
"os"
@ -9,7 +10,9 @@ import (
"time"
"github.com/antitbone/dwarfctl/internal/api"
pb "github.com/antitbone/dwarfctl/proto"
"github.com/spf13/cobra"
"google.golang.org/protobuf/proto"
)
var (
@ -39,6 +42,7 @@ func main() {
cmdSystem(),
cmdPower(),
cmdMonitor(),
cmdInteractive(),
cmdPano(),
)
@ -667,7 +671,8 @@ func cmdPano() *cobra.Command {
// --- monitor ---
func cmdMonitor() *cobra.Command { return &cobra.Command{
func cmdMonitor() *cobra.Command {
return &cobra.Command{
Use: "monitor",
Short: "Listen for telescope notifications (Ctrl-C to stop)",
Run: func(_ *cobra.Command, _ []string) {
@ -676,8 +681,71 @@ func cmdMonitor() *cobra.Command { return &cobra.Command{
ch := scope.Notifications()
fmt.Println("Monitoring notifications (Ctrl-C to stop)...")
for pkt := range ch {
fmt.Printf("[NOTIFY] cmd=%d module=%d type=%d data_len=%d\n",
pkt.GetCmd(), pkt.GetModuleId(), pkt.GetType(), len(pkt.GetData()))
cmdID := pkt.GetCmd()
extra := ""
if cmdID == 15295 && len(pkt.GetData()) > 0 {
att := &pb.DeviceAttitude{}
if proto.Unmarshal(pkt.GetData(), att) == nil {
extra = fmt.Sprintf(" pitch=%.1f yaw=%.1f roll=%.1f", att.GetPitch(), att.GetYaw(), att.GetRoll())
}
}
fmt.Printf("[NOTIFY] cmd=%d module=%d type=%d data=%d%s\n",
cmdID, pkt.GetModuleId(), pkt.GetType(), len(pkt.GetData()), extra)
}
},
}
}
func cmdInteractive() *cobra.Command {
return &cobra.Command{
Use: "interactive",
Short: "Interactive slew with real-time attitude (Ctrl-C to exit)",
Run: func(_ *cobra.Command, _ []string) {
scope, cleanup := dial()
defer cleanup()
ch := scope.Notifications()
go func() {
for pkt := range ch {
if pkt.GetCmd() == 15295 && len(pkt.GetData()) > 0 {
att := &pb.DeviceAttitude{}
if proto.Unmarshal(pkt.GetData(), att) == nil {
fmt.Printf("\r [ATTITUDE] pitch=%.1f yaw=%.1f roll=%.1f \n", att.GetPitch(), att.GetYaw(), att.GetRoll())
}
}
}
}()
fmt.Println("Interactive slew. Commands: <angle> <speed> [duration_s], stop, photo, state, q")
scanner := bufio.NewScanner(os.Stdin)
for {
fmt.Print("> ")
if !scanner.Scan() { break }
line := strings.TrimSpace(scanner.Text())
if line == "" { continue }
parts := strings.Fields(line)
switch parts[0] {
case "q", "quit", "exit":
return
case "stop":
must(scope.MotorJoystickStop())
fmt.Println("stopped")
case "photo":
must(scope.TakePhoto(api.CameraTele))
fmt.Println("photo taken")
case "state":
st, err := scope.GetDeviceState()
must(err)
fmt.Printf("battery=%d%% focus=%d\n", st.GetDeviceStateInfo().GetBatteryInfo().GetPercentage(), st.GetFocusMotorStateInfo().GetFocusPosition().GetPos())
default:
if len(parts) < 2 { fmt.Println("usage: <angle> <speed> [duration]"); continue }
angle := parseFloat(parts[0])
speed := parseFloat(parts[1])
dur := 1.0
if len(parts) > 2 { dur = parseFloat(parts[2]) }
must(scope.SlewJoystick(angle, speed))
time.Sleep(time.Duration(dur * float64(time.Second)))
must(scope.SlewJoystick(0, 0))
fmt.Println("done")
}
}
},
}