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.
+5
View File
@@ -73,6 +73,7 @@ type HornConfig struct {
Device string `yaml:"device" json:"-"`
SineGain float64 `yaml:"sineGain" json:"-"`
SawGain float64 `yaml:"sawGain" json:"-"`
MaxDuration Duration `yaml:"maxDuration" json:"-"`
}
type MediaConfig struct {
@@ -178,6 +179,7 @@ func LoadConfig(path string) (*Config, error) {
Channels: 1,
SineGain: 1.0,
SawGain: 0.7,
MaxDuration: Duration{Duration: 1200 * time.Millisecond},
},
NightVision: NightVisionConfig{
Enabled: true,
@@ -330,6 +332,9 @@ func validateHornConfig(cfg *HornConfig) {
if cfg.SawGain <= 0 {
cfg.SawGain = 0.7
}
if cfg.MaxDuration.Duration <= 0 {
cfg.MaxDuration = Duration{Duration: 1200 * time.Millisecond}
}
}
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()
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)
}
_ = writer.Flush()
@@ -151,7 +155,7 @@ func (h *HornSynth) run(waveform string, freqs []float64, stop <-chan struct{})
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))
increment := make([]float64, len(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++ {
if maxFrames > 0 && sampleIndex >= maxFrames && !stopRequested {
stopRequested = true
releaseStart = sampleIndex
}
env := 1.0
if attackFrames > 0 && sampleIndex < attackFrames {
env = float64(sampleIndex) / float64(attackFrames)
+1
View File
@@ -52,6 +52,7 @@ horn:
channels: 1
sineGain: 1.0
sawGain: 0.7
maxDuration: 1.2s
nightVision:
enabled: true
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-title" content="Multi Roomba Rover" />
<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">
</head>
<body>
+2 -1
View File
@@ -24,6 +24,7 @@ export default function CameraServoPanel() {
const hornAvailable = Boolean(roverId && pipeline?.horn);
const nightVisionKey = formatKeyLabel(keymap?.nightVisionToggle?.[0]);
const hornKey = formatKeyLabel(keymap?.hornHonk?.[0]);
const hornCooldownActive = (horn?.cooldownUntil ?? 0) > Date.now();
const min = typeof config?.minAngle === 'number' ? config.minAngle : -30;
const max = typeof config?.maxAngle === 'number' ? config.maxAngle : 30;
const value =
@@ -106,7 +107,7 @@ export default function CameraServoPanel() {
)}
{hornAvailable && (
<HornControl
disabled={!roverId}
disabled={!roverId || hornCooldownActive}
onStart={startHorn}
onStop={stopHorn}
keyLabel={hornKey}
+2 -1
View File
@@ -85,6 +85,7 @@ export function InlineCameraTilt({ keymap }) {
const nightVisionAvailable = Boolean(roverId && pipeline?.nightVision);
const nightVisionState = pipeline?.nightVisionState;
const hornAvailable = Boolean(roverId && pipeline?.horn);
const hornCooldownActive = (horn?.cooldownUntil ?? 0) > Date.now();
const min = typeof config?.minAngle === 'number' ? config.minAngle : -30;
const max = typeof config?.maxAngle === 'number' ? config.maxAngle : 30;
const value =
@@ -133,7 +134,7 @@ export function InlineCameraTilt({ keymap }) {
)}
{hornAvailable && (
<HornControl
disabled={!roverId}
disabled={!roverId || hornCooldownActive}
onStart={startHorn}
onStop={stopHorn}
keyLabel={hornLabel}
+7 -1
View File
@@ -146,6 +146,7 @@ function MobileJoystickPanel({ layout }) {
const nightVisionAvailable = Boolean(roverId && pipeline?.nightVision);
const nightVisionState = pipeline?.nightVisionState;
const hornAvailable = Boolean(roverId && pipeline?.horn);
const hornCooldownActive = (horn?.cooldownUntil ?? 0) > Date.now();
const cameraMin = typeof cameraConfig?.minAngle === 'number' ? cameraConfig.minAngle : -45;
const cameraMax = typeof cameraConfig?.maxAngle === 'number' ? cameraConfig.maxAngle : 45;
const cameraValue =
@@ -226,7 +227,12 @@ function MobileJoystickPanel({ layout }) {
/>
)}
{hornAvailable && (
<HornControl disabled={disabled} onStart={startHorn} onStop={stopHorn} active={horn?.active} />
<HornControl
disabled={disabled || hornCooldownActive}
onStart={startHorn}
onStop={stopHorn}
active={horn?.active}
/>
)}
{cameraEnabled && (
<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 { computeDifferentialSpeeds, clamp } from './controlMath.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 { useSettingsNamespace } from '../settings/index.js';
import { useSession } from '../context/SessionContext.jsx';
@@ -33,6 +33,8 @@ export function ControlSystemProvider({ children }) {
const prevModeRef = useRef(null);
const pendingLightsRef = useRef(false);
const servoAngleRef = useRef(initialControlState.camera.angle);
const hornAutoStopRef = useRef(null);
const hornCooldownRef = useRef(null);
const {
value: controlSettings,
save: saveControlSettings,
@@ -364,15 +366,40 @@ export function ControlSystemProvider({ children }) {
const startHorn = useCallback(() => {
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 });
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();
}, [dispatch, normalizedHornSettings, pipeline, recordControlIntent]);
return true;
}, [dispatch, normalizedHornSettings, pipeline, recordControlIntent, state.horn?.active, state.horn?.cooldownUntil]);
const stopHorn = useCallback(() => {
if (!pipeline.horn) return;
pipeline.sendHorn({ action: 'stop' });
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]);
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_REPEAT_MS = 250;
export const HORN_MAX_MS = 1200;
export const HORN_COOLDOWN_MS = 2500;
export const OI_COMMANDS = {
start: [128],
safe: [131],
+9
View File
@@ -30,6 +30,7 @@ function createSongState() {
function createHornState() {
return {
active: false,
cooldownUntil: 0,
};
}
@@ -144,6 +145,14 @@ export function controlReducer(state, action) {
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':
return {
...state,
@@ -373,8 +373,8 @@ export default function KeyboardInputManager() {
toggleNightVision();
} else if (newlyPressed.some((token) => keymap.hornHonk?.has(token))) {
if (!hornActiveRef.current) {
hornActiveRef.current = true;
startHorn();
const started = startHorn();
hornActiveRef.current = Boolean(started);
}
} else if (newlyPressed.some((token) => keymap.homeAssistantOn?.has(token))) {
triggerHomeAssistantCycle('on');