mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
gowuh
This commit is contained in:
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -69,10 +69,19 @@ func main() {
|
||||
defer cameraServo.Close()
|
||||
}
|
||||
|
||||
var nightVision *roverd.NightVisionLight
|
||||
if cfg.NightVision.Enabled {
|
||||
nightVision, err = roverd.NewNightVisionLight(cfg.NightVision, logger)
|
||||
if err != nil {
|
||||
logger.Fatalf("init night vision: %v", err)
|
||||
}
|
||||
defer nightVision.Close()
|
||||
}
|
||||
|
||||
autoCharge := roverd.NewAutoChargeController(adapter, eventStream, logger)
|
||||
go autoCharge.Run(ctx, sensorSamples)
|
||||
|
||||
client := roverd.NewWSClient(cfg, adapter, sensorFrames, eventStream, mediaSupervisor, cameraServo, logger)
|
||||
client := roverd.NewWSClient(cfg, adapter, sensorFrames, eventStream, mediaSupervisor, cameraServo, nightVision, logger)
|
||||
|
||||
retryDelay := time.Second
|
||||
for ctx.Err() == nil {
|
||||
|
||||
@@ -8,6 +8,7 @@ type helloMessage struct {
|
||||
Media MediaConfig `json:"media"`
|
||||
CameraServo CameraServoConfig `json:"cameraServo"`
|
||||
Audio AudioConfig `json:"audio"`
|
||||
NightVision NightVisionConfig `json:"nightVision"`
|
||||
}
|
||||
|
||||
type sensorMessage struct {
|
||||
@@ -26,6 +27,7 @@ type inboundMessage struct {
|
||||
Media *mediaCommand `json:"media,omitempty"`
|
||||
Servo *servoPayload `json:"servo,omitempty"`
|
||||
TTS *ttsPayload `json:"tts,omitempty"`
|
||||
NightVision *nightVisionPayload `json:"nightVision,omitempty"`
|
||||
}
|
||||
|
||||
type driveDirectPayload struct {
|
||||
@@ -61,6 +63,10 @@ type ttsPayload struct {
|
||||
Speak bool `json:"speak,omitempty"`
|
||||
}
|
||||
|
||||
type nightVisionPayload struct {
|
||||
Action string `json:"action"`
|
||||
}
|
||||
|
||||
type ackMessage struct {
|
||||
Type string `json:"type"`
|
||||
ID string `json:"id"`
|
||||
|
||||
@@ -95,6 +95,13 @@ type CameraServoConfig struct {
|
||||
AllowRawPulse bool `yaml:"allowRawPulse" json:"allowRawPulse"`
|
||||
}
|
||||
|
||||
type NightVisionConfig struct {
|
||||
Enabled bool `yaml:"enabled" json:"enabled"`
|
||||
GPIOPin int `yaml:"gpioPin" json:"gpioPin"`
|
||||
GPIOChip string `yaml:"gpioChip" json:"gpioChip"`
|
||||
InitialOn bool `yaml:"initialOn" json:"initialOn"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Name string `yaml:"name"`
|
||||
ServerURL string `yaml:"serverUrl"`
|
||||
@@ -105,6 +112,7 @@ type Config struct {
|
||||
Media MediaConfig `yaml:"media"`
|
||||
CameraServo CameraServoConfig `yaml:"cameraServo"`
|
||||
Audio AudioConfig `yaml:"audio"`
|
||||
NightVision NightVisionConfig `yaml:"nightVision" json:"nightVision"`
|
||||
}
|
||||
|
||||
func LoadConfig(path string) (*Config, error) {
|
||||
@@ -154,6 +162,12 @@ func LoadConfig(path string) (*Config, error) {
|
||||
DefaultVoice: "rms",
|
||||
DefaultPitch: 50,
|
||||
},
|
||||
NightVision: NightVisionConfig{
|
||||
Enabled: true,
|
||||
GPIOPin: 22,
|
||||
GPIOChip: "gpiochip0",
|
||||
InitialOn: true,
|
||||
},
|
||||
}
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, err
|
||||
@@ -214,6 +228,9 @@ func LoadConfig(path string) (*Config, error) {
|
||||
if err := validateServoConfig(&cfg.CameraServo); err != nil {
|
||||
return nil, fmt.Errorf("cameraServo: %w", err)
|
||||
}
|
||||
if err := validateNightVisionConfig(&cfg.NightVision); err != nil {
|
||||
return nil, fmt.Errorf("nightVision: %w", err)
|
||||
}
|
||||
validateAudioConfig(&cfg.Audio)
|
||||
return &cfg, nil
|
||||
}
|
||||
@@ -278,6 +295,19 @@ func validateAudioConfig(cfg *AudioConfig) {
|
||||
}
|
||||
}
|
||||
|
||||
func validateNightVisionConfig(cfg *NightVisionConfig) error {
|
||||
if !cfg.Enabled {
|
||||
return nil
|
||||
}
|
||||
if cfg.GPIOPin <= 0 {
|
||||
return errors.New("gpioPin must be > 0")
|
||||
}
|
||||
if cfg.GPIOChip == "" {
|
||||
cfg.GPIOChip = "gpiochip0"
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func derivePublishURL(serverURL, streamName string, port int) (string, error) {
|
||||
if streamName == "" {
|
||||
return "", errors.New("missing stream name for publishUrl")
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
//go:build !dummy
|
||||
|
||||
package roverd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
gpiocdev "github.com/warthog618/go-gpiocdev"
|
||||
)
|
||||
|
||||
type NightVisionLight struct {
|
||||
cfg NightVisionConfig
|
||||
logger *log.Logger
|
||||
line *gpiocdev.Line
|
||||
mu sync.Mutex
|
||||
on bool
|
||||
closed bool
|
||||
}
|
||||
|
||||
func NewNightVisionLight(cfg NightVisionConfig, logger *log.Logger) (*NightVisionLight, error) {
|
||||
if !cfg.Enabled {
|
||||
return nil, fmt.Errorf("night vision disabled")
|
||||
}
|
||||
chip := cfg.GPIOChip
|
||||
if chip == "" {
|
||||
chip = "gpiochip0"
|
||||
}
|
||||
initial := 0
|
||||
if cfg.InitialOn {
|
||||
initial = 1
|
||||
}
|
||||
line, err := gpiocdev.RequestLine(
|
||||
chip,
|
||||
cfg.GPIOPin,
|
||||
gpiocdev.AsOutput(initial),
|
||||
gpiocdev.WithConsumer("roverd-nightvision"),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gpio request: %w", err)
|
||||
}
|
||||
nv := &NightVisionLight{
|
||||
cfg: cfg,
|
||||
logger: logger,
|
||||
line: line,
|
||||
on: cfg.InitialOn,
|
||||
}
|
||||
logger.Printf("night vision LED on GPIO %d (initial=%v)", cfg.GPIOPin, cfg.InitialOn)
|
||||
return nv, nil
|
||||
}
|
||||
|
||||
func (n *NightVisionLight) Close() {
|
||||
n.mu.Lock()
|
||||
defer n.mu.Unlock()
|
||||
if n.closed {
|
||||
return
|
||||
}
|
||||
_ = n.line.SetValue(boolToGPIO(n.on))
|
||||
n.line.Close()
|
||||
n.closed = true
|
||||
}
|
||||
|
||||
func (n *NightVisionLight) HandleAction(action string) error {
|
||||
n.mu.Lock()
|
||||
defer n.mu.Unlock()
|
||||
if n.closed {
|
||||
return fmt.Errorf("night vision controller closed")
|
||||
}
|
||||
act := strings.ToLower(strings.TrimSpace(action))
|
||||
switch act {
|
||||
case "", "toggle":
|
||||
return n.setLocked(!n.on)
|
||||
case "on":
|
||||
return n.setLocked(true)
|
||||
case "off":
|
||||
return n.setLocked(false)
|
||||
default:
|
||||
return fmt.Errorf("unknown action %q", action)
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NightVisionLight) setLocked(on bool) error {
|
||||
if err := n.line.SetValue(boolToGPIO(on)); err != nil {
|
||||
return err
|
||||
}
|
||||
n.on = on
|
||||
return nil
|
||||
}
|
||||
|
||||
func boolToGPIO(value bool) int {
|
||||
if value {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
//go:build dummy
|
||||
|
||||
package roverd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
)
|
||||
|
||||
type NightVisionLight struct{}
|
||||
|
||||
func NewNightVisionLight(cfg NightVisionConfig, logger *log.Logger) (*NightVisionLight, error) {
|
||||
return nil, fmt.Errorf("night vision not supported in dummy build")
|
||||
}
|
||||
|
||||
func (n *NightVisionLight) Close() {}
|
||||
|
||||
func (n *NightVisionLight) HandleAction(action string) error {
|
||||
return fmt.Errorf("night vision not supported in dummy build")
|
||||
}
|
||||
@@ -47,3 +47,8 @@ audio:
|
||||
defaultEngine: flite
|
||||
defaultVoice: rms
|
||||
defaultPitch: 50
|
||||
nightVision:
|
||||
enabled: true
|
||||
gpioPin: 22
|
||||
gpioChip: gpiochip0
|
||||
initialOn: true
|
||||
|
||||
@@ -18,10 +18,11 @@ type WSClient struct {
|
||||
events <-chan RoverEvent
|
||||
media *MediaSupervisor
|
||||
servo *CameraServo
|
||||
nightVision *NightVisionLight
|
||||
log *log.Logger
|
||||
}
|
||||
|
||||
func NewWSClient(cfg *Config, adapter *SerialAdapter, frames <-chan []byte, events <-chan RoverEvent, media *MediaSupervisor, servo *CameraServo, logger *log.Logger) *WSClient {
|
||||
func NewWSClient(cfg *Config, adapter *SerialAdapter, frames <-chan []byte, events <-chan RoverEvent, media *MediaSupervisor, servo *CameraServo, nightVision *NightVisionLight, logger *log.Logger) *WSClient {
|
||||
return &WSClient{
|
||||
cfg: cfg,
|
||||
adapter: adapter,
|
||||
@@ -29,6 +30,7 @@ func NewWSClient(cfg *Config, adapter *SerialAdapter, frames <-chan []byte, even
|
||||
events: events,
|
||||
media: media,
|
||||
servo: servo,
|
||||
nightVision: nightVision,
|
||||
log: logger,
|
||||
}
|
||||
}
|
||||
@@ -72,6 +74,7 @@ func (c *WSClient) sendHello(ctx context.Context, conn *websocket.Conn) error {
|
||||
Media: c.cfg.Media,
|
||||
CameraServo: c.cfg.CameraServo,
|
||||
Audio: c.cfg.Audio,
|
||||
NightVision: c.cfg.NightVision,
|
||||
}
|
||||
c.log.Printf("sending hello (camera servo enabled=%v pin=%d)", msg.CameraServo.Enabled, msg.CameraServo.Pin)
|
||||
return writeJSON(ctx, conn, msg)
|
||||
@@ -153,6 +156,11 @@ func (c *WSClient) dispatch(ctx context.Context, msg *inboundMessage) error {
|
||||
return c.handleServoCommand(msg.Servo)
|
||||
case msg.TTS != nil:
|
||||
return c.handleTTSPayload(msg.TTS)
|
||||
case msg.NightVision != nil:
|
||||
if c.nightVision == nil {
|
||||
return fmt.Errorf("night vision disabled")
|
||||
}
|
||||
return c.nightVision.HandleAction(msg.NightVision.Action)
|
||||
default:
|
||||
return fmt.Errorf("unsupported command type: %s", msg.Type)
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -11,8 +11,8 @@
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
|
||||
<title>Multi Roomba Rover</title>
|
||||
<script type="module" crossorigin src="/assets/index-CnvHZcbU.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-B1UrCI1T.css">
|
||||
<script type="module" crossorigin src="/assets/index-D8Lb2mUY.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DQ1jpCiS.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -119,6 +119,7 @@ function getRoster() {
|
||||
media: record.meta?.media,
|
||||
cameraServo: record.meta?.cameraServo,
|
||||
audio: record.meta?.audio,
|
||||
nightVision: record.meta?.nightVision,
|
||||
locked: record.locked,
|
||||
lockReason: record.lockReason,
|
||||
lastSeen: record.lastSeen,
|
||||
|
||||
@@ -19,6 +19,7 @@ const KEY_ACTIONS = [
|
||||
{ id: 'auxAllForward', label: 'All Aux Forward', group: 'Aux Motors' },
|
||||
{ id: 'cameraUp', label: 'Camera Up', group: 'Camera' },
|
||||
{ id: 'cameraDown', label: 'Camera Down', group: 'Camera' },
|
||||
{ id: 'nightVisionToggle', label: 'Toggle Night Vision', group: 'Camera' },
|
||||
{ id: 'driveMacro', label: 'Drive Macro', group: 'Macros' },
|
||||
{ id: 'dockMacro', label: 'Dock Macro', group: 'Macros' },
|
||||
{ id: 'chatFocus', label: 'Toggle Chat', group: 'Chat' },
|
||||
|
||||
@@ -133,11 +133,13 @@ function FloatingJoystick({ disabled, layout, radius, onMove, onStop }) {
|
||||
function MobileJoystickPanel({ layout }) {
|
||||
const {
|
||||
state: { roverId, camera },
|
||||
actions: { setDriveVector, registerInputState, stopAllMotion, setServoAngle },
|
||||
pipeline,
|
||||
actions: { setDriveVector, registerInputState, stopAllMotion, setServoAngle, toggleNightVision },
|
||||
} = useControlSystem();
|
||||
const disabled = !roverId;
|
||||
const cameraConfig = camera?.config;
|
||||
const cameraEnabled = Boolean(roverId && camera?.enabled && cameraConfig);
|
||||
const nightVisionAvailable = Boolean(roverId && pipeline?.nightVision);
|
||||
const cameraMin = typeof cameraConfig?.minAngle === 'number' ? cameraConfig.minAngle : -45;
|
||||
const cameraMax = typeof cameraConfig?.maxAngle === 'number' ? cameraConfig.maxAngle : 45;
|
||||
const cameraValue =
|
||||
@@ -198,6 +200,16 @@ function MobileJoystickPanel({ layout }) {
|
||||
<div className="flex flex-col gap-0.5 text-slate-100">
|
||||
|
||||
<DriveModeToggle size="compact" />
|
||||
{nightVisionAvailable && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleNightVision()}
|
||||
disabled={disabled}
|
||||
className="bg-amber-600 px-0.5 py-1 text-sm font-semibold text-amber-50 transition hover:bg-amber-500 disabled:opacity-40"
|
||||
>
|
||||
Toggle Night Vision
|
||||
</button>
|
||||
)}
|
||||
{cameraEnabled && (
|
||||
<div className="bg-zinc-950 p-0.5 text-xs">
|
||||
<div className="flex items-center justify-between text-[0.75rem] text-slate-400">
|
||||
|
||||
@@ -254,6 +254,10 @@ export function ControlSystemProvider({ children }) {
|
||||
[pipeline],
|
||||
);
|
||||
|
||||
const toggleNightVision = useCallback(() => {
|
||||
pipeline.sendNightVision('toggle');
|
||||
}, [pipeline]);
|
||||
|
||||
const registerInputState = useCallback((source, data) => {
|
||||
dispatch({ type: 'control/register-input-state', payload: { source, state: data } });
|
||||
}, []);
|
||||
@@ -274,6 +278,7 @@ export function ControlSystemProvider({ children }) {
|
||||
stopAllMotion,
|
||||
sendOiCommand,
|
||||
setSensorStream,
|
||||
toggleNightVision,
|
||||
updateKeyBinding,
|
||||
resetKeyBindings,
|
||||
registerInputState,
|
||||
@@ -292,6 +297,7 @@ export function ControlSystemProvider({ children }) {
|
||||
stopAllMotion,
|
||||
sendOiCommand,
|
||||
setSensorStream,
|
||||
toggleNightVision,
|
||||
updateKeyBinding,
|
||||
resetKeyBindings,
|
||||
registerInputState,
|
||||
|
||||
@@ -19,6 +19,11 @@ export function useCommandPipeline() {
|
||||
return rosterEntry.cameraServo;
|
||||
}, [rosterEntry]);
|
||||
|
||||
const nightVision = useMemo(() => {
|
||||
if (!rosterEntry?.nightVision || !rosterEntry.nightVision.enabled) return null;
|
||||
return rosterEntry.nightVision;
|
||||
}, [rosterEntry]);
|
||||
|
||||
const emitCommand = useCallback(
|
||||
(payload, cb) => {
|
||||
if (!roverId) return;
|
||||
@@ -134,29 +139,45 @@ export function useCommandPipeline() {
|
||||
[roverId, sendOiCommand, sendDriveDirect, sendAuxMotors, sendServoAngle],
|
||||
);
|
||||
|
||||
const sendNightVision = useCallback(
|
||||
(action = 'toggle') => {
|
||||
if (!roverId || !nightVision) return null;
|
||||
emitCommand({
|
||||
type: 'nightVision',
|
||||
data: { nightVision: { action } },
|
||||
});
|
||||
return action;
|
||||
},
|
||||
[emitCommand, nightVision, roverId],
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
roverId,
|
||||
rosterEntry,
|
||||
servoConfig,
|
||||
nightVision,
|
||||
emitCommand,
|
||||
enableSensorStream,
|
||||
sendDriveDirect,
|
||||
sendAuxMotors,
|
||||
sendServoAngle,
|
||||
sendOiCommand,
|
||||
sendNightVision,
|
||||
runMacroSteps,
|
||||
}),
|
||||
[
|
||||
roverId,
|
||||
rosterEntry,
|
||||
servoConfig,
|
||||
nightVision,
|
||||
emitCommand,
|
||||
enableSensorStream,
|
||||
sendDriveDirect,
|
||||
sendAuxMotors,
|
||||
sendServoAngle,
|
||||
sendOiCommand,
|
||||
sendNightVision,
|
||||
runMacroSteps,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -36,6 +36,7 @@ export const DEFAULT_KEYMAP = {
|
||||
auxAllForward: [","],
|
||||
cameraUp: ['u'],
|
||||
cameraDown: ['j'],
|
||||
nightVisionToggle: ['e'],
|
||||
driveMacro: ['f'],
|
||||
dockMacro: ['g'],
|
||||
chatFocus: ['enter'],
|
||||
|
||||
@@ -86,6 +86,7 @@ export default function KeyboardInputManager() {
|
||||
runMacro,
|
||||
stopAllMotion,
|
||||
registerInputState,
|
||||
toggleNightVision,
|
||||
},
|
||||
} = useControlSystem();
|
||||
const { focusChat, blurChat, isChatFocused } = useChat();
|
||||
@@ -208,6 +209,8 @@ export default function KeyboardInputManager() {
|
||||
} else if (newlyPressed.some((token) => keymap.dockMacro?.has(token))) {
|
||||
setMode('dock');
|
||||
runMacro('seek-dock');
|
||||
} else if (newlyPressed.some((token) => keymap.nightVisionToggle?.has(token))) {
|
||||
toggleNightVision();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user