@@ -24,9 +24,74 @@ interface Marker {
2424
2525export default function HeadModule ( ) {
2626 const { isConnected, getRos, robotNamespace, ensureConnection } = useRos ( ) ;
27- const [ roll , setRoll ] = useState ( 0 ) ; // -80 to 80
28- const [ pitch , setPitch ] = useState ( 0 ) ; // 0 to 90
27+ const [ roll , setRoll ] = useState ( 0 ) ;
28+ const [ pitch , setPitch ] = useState ( 0 ) ;
2929 const [ override , setOverride ] = useState ( false ) ;
30+ // Dynamic limits for roll and pitch
31+ const [ rollMax , setRollMax ] = useState < string > ( "80" ) ;
32+ const [ rollMin , setRollMin ] = useState < string > ( "-80" ) ;
33+ const [ pitchMax , setPitchMax ] = useState < string > ( "60" ) ;
34+ const [ pitchMin , setPitchMin ] = useState < string > ( "-60" ) ;
35+
36+ // Read limits from ROS2 params on mount/connection
37+ useEffect ( ( ) => {
38+ if ( ! isConnected ) return ;
39+ try {
40+ const ros = getRos ( ) ;
41+ const paramClient = new ROSLIB . Service ( {
42+ ros : ros ,
43+ name : "/head_controller/get_parameters" ,
44+ serviceType : "rcl_interfaces/srv/GetParameters" ,
45+ } ) ;
46+ const request = new ROSLIB . ServiceRequest ( {
47+ names : [
48+ "limit.roll_max" ,
49+ "limit.roll_min" ,
50+ "limit.pitch_max" ,
51+ "limit.pitch_min" ,
52+ ] ,
53+ } ) ;
54+ paramClient . callService ( request , ( result ) => {
55+ // result.values is an array of parameter values
56+ if ( result && result . values && Array . isArray ( result . values ) ) {
57+ result . values . forEach ( ( param : any , idx : number ) => {
58+ // param.double_value is the value
59+ if ( typeof param . double_value === "number" ) {
60+ switch ( idx ) {
61+ case 0 : setRollMax ( String ( param . double_value ) ) ; break ;
62+ case 1 : setRollMin ( String ( param . double_value ) ) ; break ;
63+ case 2 : setPitchMax ( String ( param . double_value ) ) ; break ;
64+ case 3 : setPitchMin ( String ( param . double_value ) ) ; break ;
65+ }
66+ }
67+ } ) ;
68+ }
69+ } , ( ) => { } ) ;
70+ } catch ( e ) { }
71+ // eslint-disable-next-line react-hooks/exhaustive-deps
72+ } , [ isConnected ] ) ;
73+
74+ // Set parameter helper
75+ const setLimitParam = ( paramName : string , value : number ) => {
76+ if ( ! isConnected ) return ;
77+ try {
78+ const ros = getRos ( ) ;
79+ const paramClient = new ROSLIB . Service ( {
80+ ros : ros ,
81+ name : "/head_controller/set_parameters" ,
82+ serviceType : "rcl_interfaces/srv/SetParameters" ,
83+ } ) ;
84+ const parameter = new ROSLIB . Message ( {
85+ name : paramName ,
86+ value : { type : 3 , double_value : value } ,
87+ } ) ;
88+ const request = new ROSLIB . ServiceRequest ( {
89+ parameters : [ parameter ] ,
90+ } ) ;
91+ paramClient . callService ( request , ( ) => { } , ( ) => { } ) ;
92+ } catch ( e ) { }
93+ } ;
94+ const [ showSettings , setShowSettings ] = useState ( false ) ;
3095
3196 // Ensure connection is maintained when component mounts
3297 useEffect ( ( ) => {
@@ -215,6 +280,95 @@ export default function HeadModule() {
215280 </ header >
216281 < ConnectionStatusBar showFullControls = { false } />
217282 < div className = "max-w-xl mx-auto mt-12 bg-white border border-gray-200 shadow-sm rounded-lg p-8" >
283+ { /* Settings Section */ }
284+ < div className = "flex justify-end mb-6" >
285+ < button
286+ onClick = { ( ) => setShowSettings ( ( prev ) => ! prev ) }
287+ className = "px-4 py-2 rounded-lg bg-gray-200 hover:bg-gray-300 text-gray-800 font-semibold border border-gray-300 shadow-sm transition-colors"
288+ >
289+ { showSettings ? 'Close Settings' : 'Open Settings' }
290+ </ button >
291+ </ div >
292+ { showSettings && (
293+ < div className = "mb-10 p-4 border border-gray-300 rounded-lg bg-gray-50" >
294+ < h3 className = "text-lg font-semibold mb-4 text-gray-900" > Head Limits Settings</ h3 >
295+ < div className = "grid grid-cols-2 gap-4 mb-2" >
296+ < div >
297+ < label className = "block text-sm font-medium text-gray-700 mb-1" > Roll Max (°)</ label >
298+ < input
299+ type = "text"
300+ inputMode = "decimal"
301+ step = "any"
302+ value = { rollMax }
303+ onChange = { e => {
304+ const val = e . target . value ;
305+ setRollMax ( val ) ;
306+ const v = Number ( val ) ;
307+ if ( ! isNaN ( v ) && val !== "-" ) {
308+ setLimitParam ( "limit.roll_max" , v ) ;
309+ }
310+ } }
311+ className = "w-full border rounded px-2 py-1"
312+ />
313+ </ div >
314+ < div >
315+ < label className = "block text-sm font-medium text-gray-700 mb-1" > Roll Min (°)</ label >
316+ < input
317+ type = "text"
318+ inputMode = "decimal"
319+ step = "any"
320+ value = { rollMin }
321+ onChange = { e => {
322+ const val = e . target . value ;
323+ setRollMin ( val ) ;
324+ const v = Number ( val ) ;
325+ if ( ! isNaN ( v ) && val !== "-" ) {
326+ setLimitParam ( "limit.roll_min" , v ) ;
327+ }
328+ } }
329+ className = "w-full border rounded px-2 py-1"
330+ />
331+ </ div >
332+ < div >
333+ < label className = "block text-sm font-medium text-gray-700 mb-1" > Pitch Max (°)</ label >
334+ < input
335+ type = "text"
336+ inputMode = "decimal"
337+ step = "any"
338+ value = { pitchMax }
339+ onChange = { e => {
340+ const val = e . target . value ;
341+ setPitchMax ( val ) ;
342+ const v = Number ( val ) ;
343+ if ( ! isNaN ( v ) && val !== "-" ) {
344+ setLimitParam ( "limit.pitch_max" , v ) ;
345+ }
346+ } }
347+ className = "w-full border rounded px-2 py-1"
348+ />
349+ </ div >
350+ < div >
351+ < label className = "block text-sm font-medium text-gray-700 mb-1" > Pitch Min (°)</ label >
352+ < input
353+ type = "text"
354+ inputMode = "decimal"
355+ step = "any"
356+ value = { pitchMin }
357+ onChange = { e => {
358+ const val = e . target . value ;
359+ setPitchMin ( val ) ;
360+ const v = Number ( val ) ;
361+ if ( ! isNaN ( v ) && val !== "-" ) {
362+ setLimitParam ( "limit.pitch_min" , v ) ;
363+ }
364+ } }
365+ className = "w-full border rounded px-2 py-1"
366+ />
367+ </ div >
368+ </ div >
369+ < div className = "text-xs text-gray-500 mt-2" > These limits affect the range of the sliders below.</ div >
370+ </ div >
371+ ) }
218372 { /* Search Mode Buttons */ }
219373 < div className = "mb-10 flex flex-col items-center" >
220374 < label className = "block text-lg font-semibold text-gray-900 mb-4" > Search Mode</ label >
@@ -249,32 +403,32 @@ export default function HeadModule() {
249403 < label className = "block text-lg font-semibold text-gray-900 mb-4" > Roll / Pan</ label >
250404 < input
251405 type = "range"
252- min = { - 80 }
253- max = { 80 }
406+ min = { Number ( rollMin ) }
407+ max = { Number ( rollMax ) }
254408 value = { roll }
255409 onChange = { e => handleRollChange ( Number ( e . target . value ) ) }
256410 className = "w-full accent-blue-600"
257411 />
258412 < div className = "flex justify-between text-sm text-gray-600 mt-2" >
259- < span > -80 °</ span >
413+ < span > { rollMin } °</ span >
260414 < span > { roll } °</ span >
261- < span > 80 °</ span >
415+ < span > { rollMax } °</ span >
262416 </ div >
263417 </ div >
264418 < div className = "mb-10" >
265419 < label className = "block text-lg font-semibold text-gray-900 mb-4" > Pitch / Tilt</ label >
266420 < input
267421 type = "range"
268- min = { - 60 }
269- max = { 60 }
422+ min = { Number ( pitchMin ) }
423+ max = { Number ( pitchMax ) }
270424 value = { pitch }
271425 onChange = { e => handlePitchChange ( Number ( e . target . value ) ) }
272426 className = "w-full accent-green-600"
273427 />
274428 < div className = "flex justify-between text-sm text-gray-600 mt-2" >
275- < span > -60 °</ span >
429+ < span > { pitchMin } °</ span >
276430 < span > { pitch } °</ span >
277- < span > 60 °</ span >
431+ < span > { pitchMax } °</ span >
278432 </ div >
279433 </ div >
280434 < div className = "flex justify-center mt-8" >
0 commit comments