@@ -68,6 +68,13 @@ export function App() {
6868 return Math . max ( 500 , Math . min ( 10000 , timeout ) )
6969 } )
7070
71+ // Pending values for monitoring controls (used during editing)
72+ const [ pendingPollingInterval , setPendingPollingInterval ] =
73+ useState < string > ( '' )
74+ const [ pendingRequestTimeout , setPendingRequestTimeout ] = useState < string > ( '' )
75+ const [ showMonitoringApplyCancel , setShowMonitoringApplyCancel ] =
76+ useState ( false )
77+
7178 // Instances (initialized via useEffect)
7279 const [ serialManager ] = useState ( ( ) => new SerialManager ( ) )
7380 const [ modbusClient ] = useState ( ( ) => new ModbusClient ( ) )
@@ -191,6 +198,13 @@ export function App() {
191198
192199 const handleDisconnect = async ( ) => {
193200 try {
201+ // Stop monitoring if currently active
202+ if ( isMonitoring ) {
203+ modbusClient . stopMonitoring ( )
204+ setIsMonitoring ( false )
205+ addLog ( 'Info' , 'Stopped monitoring due to disconnection' )
206+ }
207+
194208 await serialManager . disconnect ( )
195209 } catch ( error ) {
196210 addLog ( 'Error' , `Disconnection error: ${ ( error as Error ) . message } ` )
@@ -283,56 +297,115 @@ export function App() {
283297 addLog ( 'Info' , `Protocol changed to ${ newProtocol . toUpperCase ( ) } ` )
284298 }
285299
286- const handlePollingIntervalChange = ( value : number ) => {
287- // Clamp to valid range
288- const clampedValue = Math . max ( 100 , Math . min ( 60000 , value ) )
289- setPollingInterval ( clampedValue )
290- localStorage . setItem ( 'modbus-polling-interval' , clampedValue . toString ( ) )
291-
292- if ( clampedValue !== value ) {
293- addLog (
294- 'Warning' ,
295- `Polling interval clamped to ${ clampedValue } ms (valid range: 100-60000ms)`
296- )
300+ const handlePendingPollingIntervalChange = ( e : Event ) => {
301+ const target = e . currentTarget as HTMLInputElement
302+ const value = target . value
303+ setPendingPollingInterval ( value )
304+
305+ // Show apply/cancel buttons when value differs from current
306+ if ( value !== pollingInterval . toString ( ) ) {
307+ setShowMonitoringApplyCancel ( true )
308+ } else if (
309+ pendingRequestTimeout === requestTimeout . toString ( ) ||
310+ pendingRequestTimeout === ''
311+ ) {
312+ setShowMonitoringApplyCancel ( false )
297313 }
314+ }
298315
299- // Auto-restart monitoring if currently active
300- if ( isMonitoring ) {
301- const config : ModbusReadConfig = { ...readConfig , slaveId }
302- modbusClient . stopMonitoring ( )
303- modbusClient . startMonitoring ( config , clampedValue , requestTimeout )
304- addLog (
305- 'Info' ,
306- `Monitoring updated with new polling interval: ${ clampedValue } ms`
307- )
316+ const handlePendingRequestTimeoutChange = ( e : Event ) => {
317+ const target = e . currentTarget as HTMLInputElement
318+ const value = target . value
319+ setPendingRequestTimeout ( value )
320+
321+ // Show apply/cancel buttons when value differs from current
322+ if ( value !== requestTimeout . toString ( ) ) {
323+ setShowMonitoringApplyCancel ( true )
324+ } else if (
325+ pendingPollingInterval === pollingInterval . toString ( ) ||
326+ pendingPollingInterval === ''
327+ ) {
328+ setShowMonitoringApplyCancel ( false )
308329 }
309330 }
310331
311- const handleRequestTimeoutChange = ( value : number ) => {
312- // Clamp to valid range
313- const clampedValue = Math . max ( 500 , Math . min ( 10000 , value ) )
314- setRequestTimeout ( clampedValue )
315- localStorage . setItem ( 'modbus-request-timeout' , clampedValue . toString ( ) )
332+ const validateMonitoringValues = ( ) => {
333+ const pollingValue =
334+ pendingPollingInterval === ''
335+ ? pollingInterval
336+ : Number . parseInt ( pendingPollingInterval , 10 )
337+ const timeoutValue =
338+ pendingRequestTimeout === ''
339+ ? requestTimeout
340+ : Number . parseInt ( pendingRequestTimeout , 10 )
341+
342+ return (
343+ ! Number . isNaN ( pollingValue ) &&
344+ ! Number . isNaN ( timeoutValue ) &&
345+ pollingValue >= 100 &&
346+ pollingValue <= 60000 &&
347+ timeoutValue >= 500 &&
348+ timeoutValue <= 10000
349+ )
350+ }
316351
317- if ( clampedValue !== value ) {
352+ const handleApplyMonitoringChanges = ( ) => {
353+ const newPollingInterval =
354+ pendingPollingInterval === ''
355+ ? pollingInterval
356+ : Number . parseInt ( pendingPollingInterval , 10 )
357+ const newRequestTimeout =
358+ pendingRequestTimeout === ''
359+ ? requestTimeout
360+ : Number . parseInt ( pendingRequestTimeout , 10 )
361+
362+ // Clamp values to valid ranges
363+ const clampedPolling = Math . max ( 100 , Math . min ( 60000 , newPollingInterval ) )
364+ const clampedTimeout = Math . max ( 500 , Math . min ( 10000 , newRequestTimeout ) )
365+
366+ // Update state and localStorage
367+ setPollingInterval ( clampedPolling )
368+ setRequestTimeout ( clampedTimeout )
369+ localStorage . setItem ( 'modbus-polling-interval' , clampedPolling . toString ( ) )
370+ localStorage . setItem ( 'modbus-request-timeout' , clampedTimeout . toString ( ) )
371+
372+ // Clear pending values and hide buttons
373+ setPendingPollingInterval ( '' )
374+ setPendingRequestTimeout ( '' )
375+ setShowMonitoringApplyCancel ( false )
376+
377+ // Show warnings if values were clamped
378+ if ( clampedPolling !== newPollingInterval ) {
379+ addLog (
380+ 'Warning' ,
381+ `Polling interval clamped to ${ clampedPolling } ms (valid range: 100-60000ms)`
382+ )
383+ }
384+ if ( clampedTimeout !== newRequestTimeout ) {
318385 addLog (
319386 'Warning' ,
320- `Request timeout clamped to ${ clampedValue } ms (valid range: 500-10000ms)`
387+ `Request timeout clamped to ${ clampedTimeout } ms (valid range: 500-10000ms)`
321388 )
322389 }
323390
324- // Auto-restart monitoring if currently active
391+ // Restart monitoring if currently active
325392 if ( isMonitoring ) {
326393 const config : ModbusReadConfig = { ...readConfig , slaveId }
327394 modbusClient . stopMonitoring ( )
328- modbusClient . startMonitoring ( config , pollingInterval , clampedValue )
395+ modbusClient . startMonitoring ( config , clampedPolling , clampedTimeout )
329396 addLog (
330397 'Info' ,
331- `Monitoring updated with new request timeout: ${ clampedValue } ms`
398+ `Monitoring updated with new settings (interval: ${ clampedPolling } ms, timeout: ${ clampedTimeout } ms) `
332399 )
333400 }
334401 }
335402
403+ const handleCancelMonitoringChanges = ( ) => {
404+ setPendingPollingInterval ( '' )
405+ setPendingRequestTimeout ( '' )
406+ setShowMonitoringApplyCancel ( false )
407+ }
408+
336409 const clearLogs = ( ) => {
337410 setLogs ( [ ] )
338411 setData ( [ ] )
@@ -741,11 +814,13 @@ export function App() {
741814 id = "pollingInterval"
742815 max = "60000"
743816 min = "100"
744- onChange = { ( e ) =>
745- handlePollingIntervalChange ( Number ( e . currentTarget . value ) )
746- }
817+ onChange = { handlePendingPollingIntervalChange }
747818 type = "number"
748- value = { pollingInterval }
819+ value = {
820+ pendingPollingInterval === ''
821+ ? pollingInterval
822+ : pendingPollingInterval
823+ }
749824 />
750825 </ div >
751826
@@ -756,24 +831,53 @@ export function App() {
756831 id = "requestTimeout"
757832 max = "10000"
758833 min = "500"
759- onChange = { ( e ) =>
760- handleRequestTimeoutChange ( Number ( e . currentTarget . value ) )
761- }
834+ onChange = { handlePendingRequestTimeoutChange }
762835 type = "number"
763- value = { requestTimeout }
836+ value = {
837+ pendingRequestTimeout === ''
838+ ? requestTimeout
839+ : pendingRequestTimeout
840+ }
764841 />
765842 </ div >
766843
767- < div className = "form-group" >
768- < button
769- className = "btn btn-secondary"
770- disabled = { ! isConnected }
771- onClick = { handleMonitorToggle }
772- type = "button"
773- >
774- { isMonitoring ? 'Stop Monitor' : 'Start Monitor' }
775- </ button >
776- </ div >
844+ { ! showMonitoringApplyCancel && (
845+ < div className = "form-group" >
846+ < button
847+ className = "btn btn-secondary"
848+ disabled = { ! isConnected }
849+ onClick = { handleMonitorToggle }
850+ type = "button"
851+ >
852+ { isMonitoring ? 'Stop Monitor' : 'Start Monitor' }
853+ </ button >
854+ </ div >
855+ ) }
856+
857+ { showMonitoringApplyCancel && (
858+ < >
859+ < div className = "form-group" >
860+ < button
861+ className = "btn btn-success"
862+ disabled = { ! isConnected || ! validateMonitoringValues ( ) }
863+ onClick = { handleApplyMonitoringChanges }
864+ type = "button"
865+ >
866+ Apply
867+ </ button >
868+ </ div >
869+ < div className = "form-group" >
870+ < button
871+ className = "btn btn-secondary"
872+ disabled = { ! isConnected }
873+ onClick = { handleCancelMonitoringChanges }
874+ type = "button"
875+ >
876+ Cancel
877+ </ button >
878+ </ div >
879+ </ >
880+ ) }
777881 </ div >
778882 </ div >
779883 </ section >
0 commit comments