package roomba import ( "context" "errors" "fmt" "io" "slices" "strings" "time" "git.t-juice.club/torjus/oubliette/internal/shell" ) const sessionTimeout = 5 * time.Minute // RoombaShell emulates an iRobot Roomba vacuum robot interface. type RoombaShell struct{} // NewRoombaShell returns a new RoombaShell instance. func NewRoombaShell() *RoombaShell { return &RoombaShell{} } func (r *RoombaShell) Name() string { return "roomba" } func (r *RoombaShell) Description() string { return "iRobot Roomba shell emulator" } func (r *RoombaShell) Handle(ctx context.Context, sess *shell.SessionContext, rw io.ReadWriteCloser) error { ctx, cancel := context.WithTimeout(ctx, sessionTimeout) defer cancel() state := newRoombaState() banner := strings.ReplaceAll(bootBanner(), "\n", "\r\n") fmt.Fprint(rw, banner) for { if _, err := fmt.Fprint(rw, "RoombaOS> "); err != nil { return nil } line, err := shell.ReadLine(ctx, rw) if errors.Is(err, io.EOF) { fmt.Fprint(rw, "logout\r\n") return nil } if err != nil { return nil } trimmed := strings.TrimSpace(line) if trimmed == "" { continue } result := state.dispatch(trimmed) var output string if result.output != "" { output = result.output output = strings.ReplaceAll(output, "\r\n", "\n") output = strings.ReplaceAll(output, "\n", "\r\n") fmt.Fprintf(rw, "%s\r\n", output) } if sess.Store != nil { if err := sess.Store.AppendSessionLog(ctx, sess.SessionID, trimmed, output); err != nil { return fmt.Errorf("append session log: %w", err) } } if sess.OnCommand != nil { sess.OnCommand("roomba") } if result.exit { return nil } } } func bootBanner() string { return ` ____ _ ___ ____ | _ \ ___ ___ _ __ ___ | |__ __ _ / _ \/ ___| | |_) / _ \ / _ \| '_ ` + "`" + ` _ \| '_ \ / _` + "`" + ` | | | \___ \ | _ < (_) | (_) | | | | | | |_) | (_| | |_| |___) | |_| \_\___/ \___/|_| |_| |_|_.__/ \__,_|\___/|____/ iRobot Roomba j7+ | RoombaOS v4.3.7 Serial: RMB-7291-J7P-0482 | Firmware: 4.3.7-stable Battery: 73% | WiFi: Connected (SmartHome-5G) Type 'help' for available commands. ` } type room struct { name string areaSqFt int lastCleaned time.Time } type scheduleEntry struct { day string time string } type historyEntry struct { timestamp time.Time room string duration string note string } type roombaState struct { battery int dustbin int status string rooms []room schedule []scheduleEntry cleanHistory []historyEntry } type commandResult struct { output string exit bool } func newRoombaState() *roombaState { now := time.Now() return &roombaState{ battery: 73, dustbin: 61, status: "Docked", rooms: []room{ {"Kitchen", 180, now.Add(-2 * time.Hour)}, {"Living Room", 320, now.Add(-5 * time.Hour)}, {"Bedroom", 200, now.Add(-26 * time.Hour)}, {"Hallway", 60, now.Add(-5 * time.Hour)}, {"Bathroom", 75, now.Add(-50 * time.Hour)}, {"Cat's Room", 110, now.Add(-3 * time.Hour)}, }, schedule: []scheduleEntry{ {"Monday", "09:00"}, {"Wednesday", "09:00"}, {"Friday", "09:00"}, {"Saturday", "14:00"}, }, cleanHistory: []historyEntry{ {now.Add(-2 * time.Hour), "Kitchen", "23 min", "Completed normally"}, {now.Add(-3 * time.Hour), "Cat's Room", "18 min", "Cat detected - rerouting"}, {now.Add(-5 * time.Hour), "Living Room", "34 min", "Encountered sock near couch"}, {now.Add(-5*time.Hour - 40*time.Minute), "Hallway", "8 min", "Completed normally"}, {now.Add(-26 * time.Hour), "Bedroom", "27 min", "Tangled in phone charger"}, {now.Add(-50 * time.Hour), "Bathroom", "14 min", "Unidentified sticky substance detected"}, }, } } func (s *roombaState) dispatch(input string) commandResult { parts := strings.Fields(input) if len(parts) == 0 { return commandResult{} } cmd := strings.ToLower(parts[0]) args := parts[1:] switch cmd { case "help": return s.cmdHelp() case "status": return s.cmdStatus() case "clean": return s.cmdClean(args) case "dock": return s.cmdDock() case "map": return s.cmdMap() case "schedule": return s.cmdSchedule(args) case "history": return s.cmdHistory() case "diagnostics": return s.cmdDiagnostics() case "alerts": return s.cmdAlerts() case "reboot": return s.cmdReboot() case "exit", "logout": return commandResult{output: "Disconnecting from RoombaOS. Happy cleaning!", exit: true} default: return commandResult{output: fmt.Sprintf("RoombaOS: unknown command '%s'. Type 'help' for available commands.", cmd)} } } func (s *roombaState) cmdHelp() commandResult { help := `Available commands: help - Show this help message status - Show robot status clean - Start full cleaning job clean room - Clean a specific room dock - Return to dock map - Show floor plan and room list schedule - List cleaning schedule schedule add