Skip to content

Commit 95db330

Browse files
committed
snapshot
1 parent f338ef8 commit 95db330

3 files changed

Lines changed: 142 additions & 30 deletions

File tree

README.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,14 +70,30 @@ Available actions:
7070
- `turn-left` - Turn the robot left
7171
- `turn-right` - Turn the robot right
7272

73-
#### Create WebRTC session
73+
#### Join WebRTC session
7474

7575
```bash
7676
menlo robot session # Use default robot
7777
menlo robot session --robot-id <robot-id> # Use specific robot
7878
```
7979

80-
This opens a LiveKit meet session with the robot, returning an SFU endpoint, WebRTC token, and a join URL.
80+
#### Set default robot
81+
82+
```bash
83+
menlo robot connect # Interactive selection
84+
menlo robot connect <robot-id> # Set directly
85+
```
86+
87+
Same as `menlo config default-robot`. Sets the default robot for the CLI.
88+
89+
#### Download snapshot
90+
91+
```bash
92+
menlo robot snapshot # Use default robot
93+
menlo robot snapshot --robot-id <robot-id> # Use specific robot
94+
```
95+
96+
Downloads the latest snapshot image from the robot and saves it to `~/.config/menlo/snapshot/{robot-id}/latest.jpeg`.
8197

8298
### menlo config
8399

internal/clients/platform/platform.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@ import (
55
"encoding/json"
66
"errors"
77
"fmt"
8+
"io"
89
"net/http"
910
"net/url"
11+
"os"
12+
"path/filepath"
1013
"time"
1114

1215
"github.com/menloresearch/cli/internal/config"
@@ -224,6 +227,42 @@ func (c *Client) SendSemanticCommand(robotID, command string) error {
224227
return nil
225228
}
226229

230+
// GetSnapshot downloads the latest snapshot image for a robot
231+
func (c *Client) GetSnapshot(robotID string) (string, error) {
232+
resp, err := c.doRequest("GET", "v1/robots/"+robotID+"/snapshot", nil)
233+
if err != nil {
234+
return "", err
235+
}
236+
defer closeBody(resp)
237+
238+
// Get config dir for snapshot storage
239+
configDir, err := config.ConfigDir()
240+
if err != nil {
241+
return "", err
242+
}
243+
snapshotDir := filepath.Join(configDir, "snapshot", robotID)
244+
245+
// Create directory if it doesn't exist
246+
if err := os.MkdirAll(snapshotDir, 0755); err != nil {
247+
return "", err
248+
}
249+
250+
// Save the image
251+
imagePath := filepath.Join(snapshotDir, "latest.jpeg")
252+
outFile, err := os.Create(imagePath)
253+
if err != nil {
254+
return "", err
255+
}
256+
defer func() { _ = outFile.Close() }()
257+
258+
_, err = io.Copy(outFile, resp.Body)
259+
if err != nil {
260+
return "", err
261+
}
262+
263+
return imagePath, nil
264+
}
265+
227266
// CreateSession creates a new session for a robot and returns WebRTC credentials
228267
func (c *Client) CreateSession(robotID string) (*SessionResponse, error) {
229268
resp, err := c.doRequest("POST", "v1/robots/"+robotID+"/session", nil)

internal/commands/robot.go

Lines changed: 85 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -84,35 +84,14 @@ var robotStatusCmd = &cobra.Command{
8484
}
8585

8686
var robotActionCmd = &cobra.Command{
87-
Use: "action <action>",
88-
Short: "Send an action to a robot",
89-
Long: `Available actions:
90-
forward Move the robot forward
91-
backward Move the robot backward
92-
left Move the robot left
93-
right Move the robot right
94-
turn-left Turn the robot left
95-
turn-right Turn the robot right
96-
97-
Examples:
98-
menlo robot action forward
99-
menlo robot action left --robot-id <robot-id>`,
100-
Args: cobra.ExactArgs(1),
87+
Use: "action <action>",
88+
Short: "Send an action to a robot",
89+
ValidArgs: platform.ValidSemanticCommands,
90+
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
91+
ValidArgsFunction: cobra.NoFileCompletions,
10192
RunE: func(cmd *cobra.Command, args []string) error {
10293
action := args[0]
10394

104-
// Validate action
105-
valid := false
106-
for _, v := range platform.ValidSemanticCommands {
107-
if action == v {
108-
valid = true
109-
break
110-
}
111-
}
112-
if !valid {
113-
return fmt.Errorf("invalid action: %s. Valid actions: forward, backward, left, right, turn-left, turn-right", action)
114-
}
115-
11695
robotID, err := cmd.Flags().GetString("robot-id")
11796
if err != nil {
11897
return err
@@ -148,8 +127,8 @@ Examples:
148127

149128
var robotSessionCmd = &cobra.Command{
150129
Use: "session",
151-
Short: "Create a WebRTC session for a robot",
152-
Long: `Create a session to connect to a robot via WebRTC.
130+
Short: "Join a WebRTC session for a robot",
131+
Long: `Join a session to connect to a robot via WebRTC.
153132
Returns an SFU endpoint and WebRTC token for connecting.
154133
155134
Examples:
@@ -188,6 +167,27 @@ Examples:
188167
},
189168
}
190169

170+
var robotConnectCmd = &cobra.Command{
171+
Use: "connect [robot-id]",
172+
Short: "Set or show the default robot",
173+
Long: `Set or show the default robot. Same as 'menlo config default-robot'.
174+
175+
Examples:
176+
menlo robot connect # Interactive selection
177+
menlo robot connect <robot-id> # Set directly`,
178+
Args: cobra.MaximumNArgs(1),
179+
RunE: func(cmd *cobra.Command, args []string) error {
180+
if len(args) == 0 {
181+
// Interactive mode - show selection
182+
return runRobotSelector()
183+
}
184+
185+
// Set robot ID directly
186+
robotID := args[0]
187+
return saveDefaultRobot(robotID)
188+
},
189+
}
190+
191191
// selectRobotInteractive prompts user to select a robot from list
192192
func selectRobotInteractive() (string, error) {
193193
client, err := platform.NewClient()
@@ -280,13 +280,70 @@ func generateMeetLink(sfuEndpoint, token string) string {
280280
return baseURL + "?" + params.Encode()
281281
}
282282

283+
var robotSnapshotCmd = &cobra.Command{
284+
Use: "snapshot",
285+
Short: "Download latest snapshot from a robot",
286+
Long: `Download the latest snapshot image from a robot.
287+
The image is saved to ~/.config/menlo/snapshot/{robot-id}/latest.jpeg
288+
289+
Examples:
290+
menlo robot snapshot
291+
menlo robot snapshot --robot-id <robot-id>`,
292+
RunE: func(cmd *cobra.Command, args []string) error {
293+
robotID, err := cmd.Flags().GetString("robot-id")
294+
if err != nil {
295+
return err
296+
}
297+
298+
// If no robot ID provided, try default
299+
if robotID == "" {
300+
cfg, err := config.Load()
301+
if err != nil {
302+
if !config.IsNotExist(err) {
303+
return err
304+
}
305+
}
306+
if cfg != nil {
307+
robotID = cfg.DefaultRobotID
308+
}
309+
}
310+
311+
// If still no robot ID, ask user to select
312+
if robotID == "" {
313+
robotID, err = selectRobotInteractive()
314+
if err != nil {
315+
return err
316+
}
317+
if robotID == "" {
318+
return nil
319+
}
320+
}
321+
322+
client, err := platform.NewClient()
323+
if err != nil {
324+
return err
325+
}
326+
327+
path, err := client.GetSnapshot(robotID)
328+
if err != nil {
329+
return err
330+
}
331+
332+
fmt.Printf("Snapshot saved to: %s\n", path)
333+
return nil
334+
},
335+
}
336+
283337
func init() {
284338
robotStatusCmd.Flags().String("robot-id", "", "Robot ID")
285339
robotActionCmd.Flags().String("robot-id", "", "Robot ID")
286340
robotSessionCmd.Flags().String("robot-id", "", "Robot ID")
341+
robotSnapshotCmd.Flags().String("robot-id", "", "Robot ID")
287342
robotCmd.AddCommand(robotListCmd)
288343
robotCmd.AddCommand(robotStatusCmd)
289344
robotCmd.AddCommand(robotActionCmd)
290345
robotCmd.AddCommand(robotSessionCmd)
346+
robotCmd.AddCommand(robotSnapshotCmd)
347+
robotCmd.AddCommand(robotConnectCmd)
291348
rootCmd.AddCommand(robotCmd)
292349
}

0 commit comments

Comments
 (0)