horn limits and such

This commit is contained in:
legop3
2026-01-31 16:11:24 -05:00
parent 92c9ae4834
commit c24ae71e63
16 changed files with 203 additions and 142 deletions
BIN
View File
Binary file not shown.
Vendored
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+18 -13
View File
@@ -66,13 +66,14 @@ type AudioConfig struct {
} }
type HornConfig struct { type HornConfig struct {
Enabled bool `yaml:"enabled" json:"enabled"` Enabled bool `yaml:"enabled" json:"enabled"`
Volume float64 `yaml:"volume" json:"-"` Volume float64 `yaml:"volume" json:"-"`
SampleRate int `yaml:"sampleRate" json:"-"` SampleRate int `yaml:"sampleRate" json:"-"`
Channels int `yaml:"channels" json:"-"` Channels int `yaml:"channels" json:"-"`
Device string `yaml:"device" json:"-"` Device string `yaml:"device" json:"-"`
SineGain float64 `yaml:"sineGain" json:"-"` SineGain float64 `yaml:"sineGain" json:"-"`
SawGain float64 `yaml:"sawGain" json:"-"` SawGain float64 `yaml:"sawGain" json:"-"`
MaxDuration Duration `yaml:"maxDuration" json:"-"`
} }
type MediaConfig struct { type MediaConfig struct {
@@ -172,12 +173,13 @@ func LoadConfig(path string) (*Config, error) {
DefaultPitch: 50, DefaultPitch: 50,
}, },
Horn: HornConfig{ Horn: HornConfig{
Enabled: false, Enabled: false,
Volume: 0.25, Volume: 0.25,
SampleRate: 48000, SampleRate: 48000,
Channels: 1, Channels: 1,
SineGain: 1.0, SineGain: 1.0,
SawGain: 0.7, SawGain: 0.7,
MaxDuration: Duration{Duration: 1200 * time.Millisecond},
}, },
NightVision: NightVisionConfig{ NightVision: NightVisionConfig{
Enabled: true, Enabled: true,
@@ -330,6 +332,9 @@ func validateHornConfig(cfg *HornConfig) {
if cfg.SawGain <= 0 { if cfg.SawGain <= 0 {
cfg.SawGain = 0.7 cfg.SawGain = 0.7
} }
if cfg.MaxDuration.Duration <= 0 {
cfg.MaxDuration = Duration{Duration: 1200 * time.Millisecond}
}
} }
func validateNightVisionConfig(cfg *NightVisionConfig) error { func validateNightVisionConfig(cfg *NightVisionConfig) error {
+10 -2
View File
@@ -135,7 +135,11 @@ func (h *HornSynth) run(waveform string, freqs []float64, stop <-chan struct{})
h.mu.Unlock() h.mu.Unlock()
writer := bufio.NewWriterSize(stdin, 32*1024) writer := bufio.NewWriterSize(stdin, 32*1024)
if err := h.synthLoop(writer, waveform, freqs, rate, channels, volume, stop); err != nil { maxFrames := 0
if h.cfg.MaxDuration.Duration > 0 {
maxFrames = int(float64(rate) * h.cfg.MaxDuration.Duration.Seconds())
}
if err := h.synthLoop(writer, waveform, freqs, rate, channels, volume, maxFrames, stop); err != nil {
h.log.Printf("horn: synth failed: %v", err) h.log.Printf("horn: synth failed: %v", err)
} }
_ = writer.Flush() _ = writer.Flush()
@@ -151,7 +155,7 @@ func (h *HornSynth) run(waveform string, freqs []float64, stop <-chan struct{})
h.mu.Unlock() h.mu.Unlock()
} }
func (h *HornSynth) synthLoop(writer *bufio.Writer, waveform string, freqs []float64, rate, channels int, volume float64, stop <-chan struct{}) error { func (h *HornSynth) synthLoop(writer *bufio.Writer, waveform string, freqs []float64, rate, channels int, volume float64, maxFrames int, stop <-chan struct{}) error {
phase := make([]float64, len(freqs)) phase := make([]float64, len(freqs))
increment := make([]float64, len(freqs)) increment := make([]float64, len(freqs))
for i, f := range freqs { for i, f := range freqs {
@@ -182,6 +186,10 @@ func (h *HornSynth) synthLoop(writer *bufio.Writer, waveform string, freqs []flo
} }
} }
for i := 0; i < framesPerChunk; i++ { for i := 0; i < framesPerChunk; i++ {
if maxFrames > 0 && sampleIndex >= maxFrames && !stopRequested {
stopRequested = true
releaseStart = sampleIndex
}
env := 1.0 env := 1.0
if attackFrames > 0 && sampleIndex < attackFrames { if attackFrames > 0 && sampleIndex < attackFrames {
env = float64(sampleIndex) / float64(attackFrames) env = float64(sampleIndex) / float64(attackFrames)
+1
View File
@@ -52,6 +52,7 @@ horn:
channels: 1 channels: 1
sineGain: 1.0 sineGain: 1.0
sawGain: 0.7 sawGain: 0.7
maxDuration: 1.2s
nightVision: nightVision:
enabled: true enabled: true
gpioPin: 22 gpioPin: 22
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -11,7 +11,7 @@
<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-CqhqXcs8.js"></script> <script type="module" crossorigin src="/assets/index-CoJQOHy3.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CQCz6Dh4.css"> <link rel="stylesheet" crossorigin href="/assets/index-CQCz6Dh4.css">
</head> </head>
<body> <body>
+2 -1
View File
@@ -24,6 +24,7 @@ export default function CameraServoPanel() {
const hornAvailable = Boolean(roverId && pipeline?.horn); const hornAvailable = Boolean(roverId && pipeline?.horn);
const nightVisionKey = formatKeyLabel(keymap?.nightVisionToggle?.[0]); const nightVisionKey = formatKeyLabel(keymap?.nightVisionToggle?.[0]);
const hornKey = formatKeyLabel(keymap?.hornHonk?.[0]); const hornKey = formatKeyLabel(keymap?.hornHonk?.[0]);
const hornCooldownActive = (horn?.cooldownUntil ?? 0) > Date.now();
const min = typeof config?.minAngle === 'number' ? config.minAngle : -30; const min = typeof config?.minAngle === 'number' ? config.minAngle : -30;
const max = typeof config?.maxAngle === 'number' ? config.maxAngle : 30; const max = typeof config?.maxAngle === 'number' ? config.maxAngle : 30;
const value = const value =
@@ -106,7 +107,7 @@ export default function CameraServoPanel() {
)} )}
{hornAvailable && ( {hornAvailable && (
<HornControl <HornControl
disabled={!roverId} disabled={!roverId || hornCooldownActive}
onStart={startHorn} onStart={startHorn}
onStop={stopHorn} onStop={stopHorn}
keyLabel={hornKey} keyLabel={hornKey}
+2 -1
View File
@@ -85,6 +85,7 @@ export function InlineCameraTilt({ keymap }) {
const nightVisionAvailable = Boolean(roverId && pipeline?.nightVision); const nightVisionAvailable = Boolean(roverId && pipeline?.nightVision);
const nightVisionState = pipeline?.nightVisionState; const nightVisionState = pipeline?.nightVisionState;
const hornAvailable = Boolean(roverId && pipeline?.horn); const hornAvailable = Boolean(roverId && pipeline?.horn);
const hornCooldownActive = (horn?.cooldownUntil ?? 0) > Date.now();
const min = typeof config?.minAngle === 'number' ? config.minAngle : -30; const min = typeof config?.minAngle === 'number' ? config.minAngle : -30;
const max = typeof config?.maxAngle === 'number' ? config.maxAngle : 30; const max = typeof config?.maxAngle === 'number' ? config.maxAngle : 30;
const value = const value =
@@ -133,7 +134,7 @@ export function InlineCameraTilt({ keymap }) {
)} )}
{hornAvailable && ( {hornAvailable && (
<HornControl <HornControl
disabled={!roverId} disabled={!roverId || hornCooldownActive}
onStart={startHorn} onStart={startHorn}
onStop={stopHorn} onStop={stopHorn}
keyLabel={hornLabel} keyLabel={hornLabel}
+7 -1
View File
@@ -146,6 +146,7 @@ function MobileJoystickPanel({ layout }) {
const nightVisionAvailable = Boolean(roverId && pipeline?.nightVision); const nightVisionAvailable = Boolean(roverId && pipeline?.nightVision);
const nightVisionState = pipeline?.nightVisionState; const nightVisionState = pipeline?.nightVisionState;
const hornAvailable = Boolean(roverId && pipeline?.horn); const hornAvailable = Boolean(roverId && pipeline?.horn);
const hornCooldownActive = (horn?.cooldownUntil ?? 0) > Date.now();
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 =
@@ -226,7 +227,12 @@ function MobileJoystickPanel({ layout }) {
/> />
)} )}
{hornAvailable && ( {hornAvailable && (
<HornControl disabled={disabled} onStart={startHorn} onStop={stopHorn} active={horn?.active} /> <HornControl
disabled={disabled || hornCooldownActive}
onStart={startHorn}
onStop={stopHorn}
active={horn?.active}
/>
)} )}
{cameraEnabled && ( {cameraEnabled && (
<div className="bg-zinc-950 p-0.5 text-xs"> <div className="bg-zinc-950 p-0.5 text-xs">
+29 -2
View File
@@ -2,7 +2,7 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useReducer,
import { controlReducer, initialControlState } from './controlReducer.js'; import { controlReducer, initialControlState } from './controlReducer.js';
import { computeDifferentialSpeeds, clamp } from './controlMath.js'; import { computeDifferentialSpeeds, clamp } from './controlMath.js';
import { useCommandPipeline } from './commandPipeline.js'; import { useCommandPipeline } from './commandPipeline.js';
import { DEFAULT_KEYMAP, DEFAULT_MACROS, SONG_DEFAULT_NOTE } from './constants.js'; import { DEFAULT_KEYMAP, DEFAULT_MACROS, HORN_COOLDOWN_MS, HORN_MAX_MS, SONG_DEFAULT_NOTE } from './constants.js';
import { canonicalizeKeyInput } from './keymapUtils.js'; import { canonicalizeKeyInput } from './keymapUtils.js';
import { useSettingsNamespace } from '../settings/index.js'; import { useSettingsNamespace } from '../settings/index.js';
import { useSession } from '../context/SessionContext.jsx'; import { useSession } from '../context/SessionContext.jsx';
@@ -33,6 +33,8 @@ export function ControlSystemProvider({ children }) {
const prevModeRef = useRef(null); const prevModeRef = useRef(null);
const pendingLightsRef = useRef(false); const pendingLightsRef = useRef(false);
const servoAngleRef = useRef(initialControlState.camera.angle); const servoAngleRef = useRef(initialControlState.camera.angle);
const hornAutoStopRef = useRef(null);
const hornCooldownRef = useRef(null);
const { const {
value: controlSettings, value: controlSettings,
save: saveControlSettings, save: saveControlSettings,
@@ -364,15 +366,40 @@ export function ControlSystemProvider({ children }) {
const startHorn = useCallback(() => { const startHorn = useCallback(() => {
if (!pipeline.horn) return; if (!pipeline.horn) return;
const now = Date.now();
const cooldownUntil = state.horn?.cooldownUntil ?? 0;
if (cooldownUntil && now < cooldownUntil) return false;
if (state.horn?.active) return false;
pipeline.sendHorn({ action: 'start', ...normalizedHornSettings }); pipeline.sendHorn({ action: 'start', ...normalizedHornSettings });
dispatch({ type: 'control/set-horn-active', payload: true }); dispatch({ type: 'control/set-horn-active', payload: true });
if (hornAutoStopRef.current) {
clearTimeout(hornAutoStopRef.current);
hornAutoStopRef.current = null;
}
hornAutoStopRef.current = setTimeout(() => {
stopHorn();
}, HORN_MAX_MS);
recordControlIntent(); recordControlIntent();
}, [dispatch, normalizedHornSettings, pipeline, recordControlIntent]); return true;
}, [dispatch, normalizedHornSettings, pipeline, recordControlIntent, state.horn?.active, state.horn?.cooldownUntil]);
const stopHorn = useCallback(() => { const stopHorn = useCallback(() => {
if (!pipeline.horn) return; if (!pipeline.horn) return;
pipeline.sendHorn({ action: 'stop' }); pipeline.sendHorn({ action: 'stop' });
dispatch({ type: 'control/set-horn-active', payload: false }); dispatch({ type: 'control/set-horn-active', payload: false });
if (hornAutoStopRef.current) {
clearTimeout(hornAutoStopRef.current);
hornAutoStopRef.current = null;
}
const cooldownUntil = Date.now() + HORN_COOLDOWN_MS;
dispatch({ type: 'control/set-horn-cooldown', payload: cooldownUntil });
if (hornCooldownRef.current) {
clearTimeout(hornCooldownRef.current);
}
hornCooldownRef.current = setTimeout(() => {
dispatch({ type: 'control/set-horn-cooldown', payload: 0 });
hornCooldownRef.current = null;
}, HORN_COOLDOWN_MS);
}, [dispatch, pipeline]); }, [dispatch, pipeline]);
const registerInputState = useCallback((source, data) => { const registerInputState = useCallback((source, data) => {
+3
View File
@@ -17,6 +17,9 @@ export const SONG_DEFAULT_NOTE = 60;
export const SONG_DEFAULT_DURATION = 8; export const SONG_DEFAULT_DURATION = 8;
export const SONG_REPEAT_MS = 250; export const SONG_REPEAT_MS = 250;
export const HORN_MAX_MS = 1200;
export const HORN_COOLDOWN_MS = 2500;
export const OI_COMMANDS = { export const OI_COMMANDS = {
start: [128], start: [128],
safe: [131], safe: [131],
+9
View File
@@ -30,6 +30,7 @@ function createSongState() {
function createHornState() { function createHornState() {
return { return {
active: false, active: false,
cooldownUntil: 0,
}; };
} }
@@ -144,6 +145,14 @@ export function controlReducer(state, action) {
active: Boolean(action.payload), active: Boolean(action.payload),
}, },
}; };
case 'control/set-horn-cooldown':
return {
...state,
horn: {
...(state.horn || createHornState()),
cooldownUntil: typeof action.payload === 'number' ? action.payload : 0,
},
};
case 'control/record-intent': case 'control/record-intent':
return { return {
...state, ...state,
@@ -373,8 +373,8 @@ export default function KeyboardInputManager() {
toggleNightVision(); toggleNightVision();
} else if (newlyPressed.some((token) => keymap.hornHonk?.has(token))) { } else if (newlyPressed.some((token) => keymap.hornHonk?.has(token))) {
if (!hornActiveRef.current) { if (!hornActiveRef.current) {
hornActiveRef.current = true; const started = startHorn();
startHorn(); hornActiveRef.current = Boolean(started);
} }
} else if (newlyPressed.some((token) => keymap.homeAssistantOn?.has(token))) { } else if (newlyPressed.some((token) => keymap.homeAssistantOn?.has(token))) {
triggerHomeAssistantCycle('on'); triggerHomeAssistantCycle('on');