mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-17 01:50:47 -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()
|
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)
|
autoCharge := roverd.NewAutoChargeController(adapter, eventStream, logger)
|
||||||
go autoCharge.Run(ctx, sensorSamples)
|
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
|
retryDelay := time.Second
|
||||||
for ctx.Err() == nil {
|
for ctx.Err() == nil {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ type helloMessage struct {
|
|||||||
Media MediaConfig `json:"media"`
|
Media MediaConfig `json:"media"`
|
||||||
CameraServo CameraServoConfig `json:"cameraServo"`
|
CameraServo CameraServoConfig `json:"cameraServo"`
|
||||||
Audio AudioConfig `json:"audio"`
|
Audio AudioConfig `json:"audio"`
|
||||||
|
NightVision NightVisionConfig `json:"nightVision"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type sensorMessage struct {
|
type sensorMessage struct {
|
||||||
@@ -26,6 +27,7 @@ type inboundMessage struct {
|
|||||||
Media *mediaCommand `json:"media,omitempty"`
|
Media *mediaCommand `json:"media,omitempty"`
|
||||||
Servo *servoPayload `json:"servo,omitempty"`
|
Servo *servoPayload `json:"servo,omitempty"`
|
||||||
TTS *ttsPayload `json:"tts,omitempty"`
|
TTS *ttsPayload `json:"tts,omitempty"`
|
||||||
|
NightVision *nightVisionPayload `json:"nightVision,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type driveDirectPayload struct {
|
type driveDirectPayload struct {
|
||||||
@@ -61,6 +63,10 @@ type ttsPayload struct {
|
|||||||
Speak bool `json:"speak,omitempty"`
|
Speak bool `json:"speak,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type nightVisionPayload struct {
|
||||||
|
Action string `json:"action"`
|
||||||
|
}
|
||||||
|
|
||||||
type ackMessage struct {
|
type ackMessage struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
|
|||||||
@@ -95,6 +95,13 @@ type CameraServoConfig struct {
|
|||||||
AllowRawPulse bool `yaml:"allowRawPulse" json:"allowRawPulse"`
|
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 {
|
type Config struct {
|
||||||
Name string `yaml:"name"`
|
Name string `yaml:"name"`
|
||||||
ServerURL string `yaml:"serverUrl"`
|
ServerURL string `yaml:"serverUrl"`
|
||||||
@@ -105,6 +112,7 @@ type Config struct {
|
|||||||
Media MediaConfig `yaml:"media"`
|
Media MediaConfig `yaml:"media"`
|
||||||
CameraServo CameraServoConfig `yaml:"cameraServo"`
|
CameraServo CameraServoConfig `yaml:"cameraServo"`
|
||||||
Audio AudioConfig `yaml:"audio"`
|
Audio AudioConfig `yaml:"audio"`
|
||||||
|
NightVision NightVisionConfig `yaml:"nightVision" json:"nightVision"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadConfig(path string) (*Config, error) {
|
func LoadConfig(path string) (*Config, error) {
|
||||||
@@ -154,6 +162,12 @@ func LoadConfig(path string) (*Config, error) {
|
|||||||
DefaultVoice: "rms",
|
DefaultVoice: "rms",
|
||||||
DefaultPitch: 50,
|
DefaultPitch: 50,
|
||||||
},
|
},
|
||||||
|
NightVision: NightVisionConfig{
|
||||||
|
Enabled: true,
|
||||||
|
GPIOPin: 22,
|
||||||
|
GPIOChip: "gpiochip0",
|
||||||
|
InitialOn: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -214,6 +228,9 @@ func LoadConfig(path string) (*Config, error) {
|
|||||||
if err := validateServoConfig(&cfg.CameraServo); err != nil {
|
if err := validateServoConfig(&cfg.CameraServo); err != nil {
|
||||||
return nil, fmt.Errorf("cameraServo: %w", err)
|
return nil, fmt.Errorf("cameraServo: %w", err)
|
||||||
}
|
}
|
||||||
|
if err := validateNightVisionConfig(&cfg.NightVision); err != nil {
|
||||||
|
return nil, fmt.Errorf("nightVision: %w", err)
|
||||||
|
}
|
||||||
validateAudioConfig(&cfg.Audio)
|
validateAudioConfig(&cfg.Audio)
|
||||||
return &cfg, nil
|
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) {
|
func derivePublishURL(serverURL, streamName string, port int) (string, error) {
|
||||||
if streamName == "" {
|
if streamName == "" {
|
||||||
return "", errors.New("missing stream name for publishUrl")
|
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
|
defaultEngine: flite
|
||||||
defaultVoice: rms
|
defaultVoice: rms
|
||||||
defaultPitch: 50
|
defaultPitch: 50
|
||||||
|
nightVision:
|
||||||
|
enabled: true
|
||||||
|
gpioPin: 22
|
||||||
|
gpioChip: gpiochip0
|
||||||
|
initialOn: true
|
||||||
|
|||||||
@@ -18,10 +18,11 @@ type WSClient struct {
|
|||||||
events <-chan RoverEvent
|
events <-chan RoverEvent
|
||||||
media *MediaSupervisor
|
media *MediaSupervisor
|
||||||
servo *CameraServo
|
servo *CameraServo
|
||||||
|
nightVision *NightVisionLight
|
||||||
log *log.Logger
|
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{
|
return &WSClient{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
adapter: adapter,
|
adapter: adapter,
|
||||||
@@ -29,6 +30,7 @@ func NewWSClient(cfg *Config, adapter *SerialAdapter, frames <-chan []byte, even
|
|||||||
events: events,
|
events: events,
|
||||||
media: media,
|
media: media,
|
||||||
servo: servo,
|
servo: servo,
|
||||||
|
nightVision: nightVision,
|
||||||
log: logger,
|
log: logger,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,6 +74,7 @@ func (c *WSClient) sendHello(ctx context.Context, conn *websocket.Conn) error {
|
|||||||
Media: c.cfg.Media,
|
Media: c.cfg.Media,
|
||||||
CameraServo: c.cfg.CameraServo,
|
CameraServo: c.cfg.CameraServo,
|
||||||
Audio: c.cfg.Audio,
|
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)
|
c.log.Printf("sending hello (camera servo enabled=%v pin=%d)", msg.CameraServo.Enabled, msg.CameraServo.Pin)
|
||||||
return writeJSON(ctx, conn, msg)
|
return writeJSON(ctx, conn, msg)
|
||||||
@@ -153,6 +156,11 @@ func (c *WSClient) dispatch(ctx context.Context, msg *inboundMessage) error {
|
|||||||
return c.handleServoCommand(msg.Servo)
|
return c.handleServoCommand(msg.Servo)
|
||||||
case msg.TTS != nil:
|
case msg.TTS != nil:
|
||||||
return c.handleTTSPayload(msg.TTS)
|
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:
|
default:
|
||||||
return fmt.Errorf("unsupported command type: %s", msg.Type)
|
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-status-bar-style" content="black-translucent" />
|
||||||
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
|
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
|
||||||
<title>Multi Roomba Rover</title>
|
<title>Multi Roomba Rover</title>
|
||||||
<script type="module" crossorigin src="/assets/index-CnvHZcbU.js"></script>
|
<script type="module" crossorigin src="/assets/index-D8Lb2mUY.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-B1UrCI1T.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-DQ1jpCiS.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ function getRoster() {
|
|||||||
media: record.meta?.media,
|
media: record.meta?.media,
|
||||||
cameraServo: record.meta?.cameraServo,
|
cameraServo: record.meta?.cameraServo,
|
||||||
audio: record.meta?.audio,
|
audio: record.meta?.audio,
|
||||||
|
nightVision: record.meta?.nightVision,
|
||||||
locked: record.locked,
|
locked: record.locked,
|
||||||
lockReason: record.lockReason,
|
lockReason: record.lockReason,
|
||||||
lastSeen: record.lastSeen,
|
lastSeen: record.lastSeen,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const KEY_ACTIONS = [
|
|||||||
{ id: 'auxAllForward', label: 'All Aux Forward', group: 'Aux Motors' },
|
{ id: 'auxAllForward', label: 'All Aux Forward', group: 'Aux Motors' },
|
||||||
{ id: 'cameraUp', label: 'Camera Up', group: 'Camera' },
|
{ id: 'cameraUp', label: 'Camera Up', group: 'Camera' },
|
||||||
{ id: 'cameraDown', label: 'Camera Down', 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: 'driveMacro', label: 'Drive Macro', group: 'Macros' },
|
||||||
{ id: 'dockMacro', label: 'Dock Macro', group: 'Macros' },
|
{ id: 'dockMacro', label: 'Dock Macro', group: 'Macros' },
|
||||||
{ id: 'chatFocus', label: 'Toggle Chat', group: 'Chat' },
|
{ id: 'chatFocus', label: 'Toggle Chat', group: 'Chat' },
|
||||||
|
|||||||
@@ -133,11 +133,13 @@ function FloatingJoystick({ disabled, layout, radius, onMove, onStop }) {
|
|||||||
function MobileJoystickPanel({ layout }) {
|
function MobileJoystickPanel({ layout }) {
|
||||||
const {
|
const {
|
||||||
state: { roverId, camera },
|
state: { roverId, camera },
|
||||||
actions: { setDriveVector, registerInputState, stopAllMotion, setServoAngle },
|
pipeline,
|
||||||
|
actions: { setDriveVector, registerInputState, stopAllMotion, setServoAngle, toggleNightVision },
|
||||||
} = useControlSystem();
|
} = useControlSystem();
|
||||||
const disabled = !roverId;
|
const disabled = !roverId;
|
||||||
const cameraConfig = camera?.config;
|
const cameraConfig = camera?.config;
|
||||||
const cameraEnabled = Boolean(roverId && camera?.enabled && cameraConfig);
|
const cameraEnabled = Boolean(roverId && camera?.enabled && cameraConfig);
|
||||||
|
const nightVisionAvailable = Boolean(roverId && pipeline?.nightVision);
|
||||||
const cameraMin = typeof cameraConfig?.minAngle === 'number' ? cameraConfig.minAngle : -45;
|
const cameraMin = typeof cameraConfig?.minAngle === 'number' ? cameraConfig.minAngle : -45;
|
||||||
const cameraMax = typeof cameraConfig?.maxAngle === 'number' ? cameraConfig.maxAngle : 45;
|
const cameraMax = typeof cameraConfig?.maxAngle === 'number' ? cameraConfig.maxAngle : 45;
|
||||||
const cameraValue =
|
const cameraValue =
|
||||||
@@ -198,6 +200,16 @@ function MobileJoystickPanel({ layout }) {
|
|||||||
<div className="flex flex-col gap-0.5 text-slate-100">
|
<div className="flex flex-col gap-0.5 text-slate-100">
|
||||||
|
|
||||||
<DriveModeToggle size="compact" />
|
<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 && (
|
{cameraEnabled && (
|
||||||
<div className="bg-zinc-950 p-0.5 text-xs">
|
<div className="bg-zinc-950 p-0.5 text-xs">
|
||||||
<div className="flex items-center justify-between text-[0.75rem] text-slate-400">
|
<div className="flex items-center justify-between text-[0.75rem] text-slate-400">
|
||||||
|
|||||||
@@ -254,6 +254,10 @@ export function ControlSystemProvider({ children }) {
|
|||||||
[pipeline],
|
[pipeline],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const toggleNightVision = useCallback(() => {
|
||||||
|
pipeline.sendNightVision('toggle');
|
||||||
|
}, [pipeline]);
|
||||||
|
|
||||||
const registerInputState = useCallback((source, data) => {
|
const registerInputState = useCallback((source, data) => {
|
||||||
dispatch({ type: 'control/register-input-state', payload: { source, state: data } });
|
dispatch({ type: 'control/register-input-state', payload: { source, state: data } });
|
||||||
}, []);
|
}, []);
|
||||||
@@ -274,6 +278,7 @@ export function ControlSystemProvider({ children }) {
|
|||||||
stopAllMotion,
|
stopAllMotion,
|
||||||
sendOiCommand,
|
sendOiCommand,
|
||||||
setSensorStream,
|
setSensorStream,
|
||||||
|
toggleNightVision,
|
||||||
updateKeyBinding,
|
updateKeyBinding,
|
||||||
resetKeyBindings,
|
resetKeyBindings,
|
||||||
registerInputState,
|
registerInputState,
|
||||||
@@ -292,6 +297,7 @@ export function ControlSystemProvider({ children }) {
|
|||||||
stopAllMotion,
|
stopAllMotion,
|
||||||
sendOiCommand,
|
sendOiCommand,
|
||||||
setSensorStream,
|
setSensorStream,
|
||||||
|
toggleNightVision,
|
||||||
updateKeyBinding,
|
updateKeyBinding,
|
||||||
resetKeyBindings,
|
resetKeyBindings,
|
||||||
registerInputState,
|
registerInputState,
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ export function useCommandPipeline() {
|
|||||||
return rosterEntry.cameraServo;
|
return rosterEntry.cameraServo;
|
||||||
}, [rosterEntry]);
|
}, [rosterEntry]);
|
||||||
|
|
||||||
|
const nightVision = useMemo(() => {
|
||||||
|
if (!rosterEntry?.nightVision || !rosterEntry.nightVision.enabled) return null;
|
||||||
|
return rosterEntry.nightVision;
|
||||||
|
}, [rosterEntry]);
|
||||||
|
|
||||||
const emitCommand = useCallback(
|
const emitCommand = useCallback(
|
||||||
(payload, cb) => {
|
(payload, cb) => {
|
||||||
if (!roverId) return;
|
if (!roverId) return;
|
||||||
@@ -134,29 +139,45 @@ export function useCommandPipeline() {
|
|||||||
[roverId, sendOiCommand, sendDriveDirect, sendAuxMotors, sendServoAngle],
|
[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(
|
return useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
roverId,
|
roverId,
|
||||||
rosterEntry,
|
rosterEntry,
|
||||||
servoConfig,
|
servoConfig,
|
||||||
|
nightVision,
|
||||||
emitCommand,
|
emitCommand,
|
||||||
enableSensorStream,
|
enableSensorStream,
|
||||||
sendDriveDirect,
|
sendDriveDirect,
|
||||||
sendAuxMotors,
|
sendAuxMotors,
|
||||||
sendServoAngle,
|
sendServoAngle,
|
||||||
sendOiCommand,
|
sendOiCommand,
|
||||||
|
sendNightVision,
|
||||||
runMacroSteps,
|
runMacroSteps,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
roverId,
|
roverId,
|
||||||
rosterEntry,
|
rosterEntry,
|
||||||
servoConfig,
|
servoConfig,
|
||||||
|
nightVision,
|
||||||
emitCommand,
|
emitCommand,
|
||||||
enableSensorStream,
|
enableSensorStream,
|
||||||
sendDriveDirect,
|
sendDriveDirect,
|
||||||
sendAuxMotors,
|
sendAuxMotors,
|
||||||
sendServoAngle,
|
sendServoAngle,
|
||||||
sendOiCommand,
|
sendOiCommand,
|
||||||
|
sendNightVision,
|
||||||
runMacroSteps,
|
runMacroSteps,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export const DEFAULT_KEYMAP = {
|
|||||||
auxAllForward: [","],
|
auxAllForward: [","],
|
||||||
cameraUp: ['u'],
|
cameraUp: ['u'],
|
||||||
cameraDown: ['j'],
|
cameraDown: ['j'],
|
||||||
|
nightVisionToggle: ['e'],
|
||||||
driveMacro: ['f'],
|
driveMacro: ['f'],
|
||||||
dockMacro: ['g'],
|
dockMacro: ['g'],
|
||||||
chatFocus: ['enter'],
|
chatFocus: ['enter'],
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ export default function KeyboardInputManager() {
|
|||||||
runMacro,
|
runMacro,
|
||||||
stopAllMotion,
|
stopAllMotion,
|
||||||
registerInputState,
|
registerInputState,
|
||||||
|
toggleNightVision,
|
||||||
},
|
},
|
||||||
} = useControlSystem();
|
} = useControlSystem();
|
||||||
const { focusChat, blurChat, isChatFocused } = useChat();
|
const { focusChat, blurChat, isChatFocused } = useChat();
|
||||||
@@ -208,6 +209,8 @@ export default function KeyboardInputManager() {
|
|||||||
} else if (newlyPressed.some((token) => keymap.dockMacro?.has(token))) {
|
} else if (newlyPressed.some((token) => keymap.dockMacro?.has(token))) {
|
||||||
setMode('dock');
|
setMode('dock');
|
||||||
runMacro('seek-dock');
|
runMacro('seek-dock');
|
||||||
|
} else if (newlyPressed.some((token) => keymap.nightVisionToggle?.has(token))) {
|
||||||
|
toggleNightVision();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user