@@ -3,6 +3,14 @@ import { serialDevices, vendorIdNames } from "./devices";
33
44const logHead = "[TAURI SERIAL]" ;
55
6+ /**
7+ * Detects Broken pipe/EPIPE errors across platforms.
8+ */
9+ function isBrokenPipeError ( error ) {
10+ const s = typeof error === "string" ? error : error ?. message || ( error ?. toString ? error . toString ( ) : "" ) || "" ;
11+ return / b r o k e n p i p e | E P I P E | o s e r r o r 3 2 | c o d e : \s * 3 2 / i. test ( s ) ;
12+ }
13+
614/**
715 * Async generator that polls the serial port for incoming data
816 * Similar to streamAsyncIterable in WebSerial but uses polling instead of streams
@@ -25,10 +33,18 @@ async function* pollSerialData(path, keepReadingFlag) {
2533 // Small delay between polls to avoid overwhelming the system
2634 await new Promise ( ( resolve ) => setTimeout ( resolve , 5 ) ) ;
2735 } catch ( error ) {
36+ const msg = error ?. message || ( error ?. toString ? error . toString ( ) : "" ) ;
2837 // Timeout is expected when no data available
29- if ( ! error . toString ( ) . includes ( "no data received" ) ) {
30- console . warn ( `${ logHead } Poll error:` , error ) ;
38+ if ( msg && msg . toLowerCase ( ) . includes ( "no data received" ) ) {
39+ // Continue polling
40+ await new Promise ( ( resolve ) => setTimeout ( resolve , 5 ) ) ;
41+ continue ;
3142 }
43+ if ( isBrokenPipeError ( msg ) ) {
44+ console . error ( `${ logHead } Fatal poll error (broken pipe) on ${ path } :` , error ) ;
45+ throw error ;
46+ }
47+ console . warn ( `${ logHead } Poll error:` , error ) ;
3248 // Continue polling
3349 await new Promise ( ( resolve ) => setTimeout ( resolve , 5 ) ) ;
3450 }
@@ -68,7 +84,12 @@ class TauriSerial extends EventTarget {
6884 // Detect if running on macOS with AT32 (needs batch writes)
6985 this . isNeedBatchWrite = false ;
7086
87+ // Device monitoring
88+ this . monitoringDevices = false ;
89+ this . deviceMonitorInterval = null ;
90+
7191 this . loadDevices ( ) ;
92+ this . startDeviceMonitoring ( ) ;
7293 }
7394
7495 handleReceiveBytes ( info ) {
@@ -79,6 +100,99 @@ class TauriSerial extends EventTarget {
79100 return this . connectionId ;
80101 }
81102
103+ handleFatalSerialError ( error ) {
104+ // On fatal errors (broken pipe, etc.), just disconnect cleanly
105+ // Device monitoring will automatically detect the removal and emit removedDevice
106+ if ( this . connected ) {
107+ this . disconnect ( ) ;
108+ }
109+ }
110+
111+ startDeviceMonitoring ( ) {
112+ if ( this . monitoringDevices ) {
113+ return ;
114+ }
115+
116+ this . monitoringDevices = true ;
117+ // Check for device changes every 1 second
118+ this . deviceMonitorInterval = setInterval ( async ( ) => {
119+ await this . checkDeviceChanges ( ) ;
120+ } , 1000 ) ;
121+
122+ console . log ( `${ logHead } Device monitoring started` ) ;
123+ }
124+
125+ stopDeviceMonitoring ( ) {
126+ if ( this . deviceMonitorInterval ) {
127+ clearInterval ( this . deviceMonitorInterval ) ;
128+ this . deviceMonitorInterval = null ;
129+ }
130+ this . monitoringDevices = false ;
131+ console . log ( `${ logHead } Device monitoring stopped` ) ;
132+ }
133+
134+ async checkDeviceChanges ( ) {
135+ try {
136+ const portsMap = await invoke ( "plugin:serialplugin|available_ports" ) ;
137+
138+ // Convert to our format
139+ const allPorts = Object . entries ( portsMap ) . map ( ( [ path , info ] ) => {
140+ let vendorId = undefined ;
141+ let productId = undefined ;
142+
143+ if ( info . vid ) {
144+ vendorId = typeof info . vid === "number" ? info . vid : parseInt ( info . vid , 10 ) ;
145+ }
146+ if ( info . pid ) {
147+ productId = typeof info . pid === "number" ? info . pid : parseInt ( info . pid , 10 ) ;
148+ }
149+
150+ return {
151+ path,
152+ displayName : this . getDisplayName ( path , vendorId , productId ) ,
153+ vendorId,
154+ productId,
155+ serialNumber : info . serial_number ,
156+ } ;
157+ } ) ;
158+
159+ // Filter to only known devices
160+ const currentPorts = allPorts . filter ( ( port ) => {
161+ if ( ! port . vendorId || ! port . productId ) {
162+ return false ;
163+ }
164+ return serialDevices . some ( ( d ) => d . vendorId === port . vendorId && d . productId === port . productId ) ;
165+ } ) ;
166+
167+ // Check for removed devices
168+ const removedPorts = this . ports . filter (
169+ ( oldPort ) => ! currentPorts . find ( ( newPort ) => newPort . path === oldPort . path ) ,
170+ ) ;
171+
172+ // Check for added devices
173+ const addedPorts = currentPorts . filter (
174+ ( newPort ) => ! this . ports . find ( ( oldPort ) => oldPort . path === newPort . path ) ,
175+ ) ;
176+
177+ // Emit events for removed devices
178+ for ( const removed of removedPorts ) {
179+ this . dispatchEvent ( new CustomEvent ( "removedDevice" , { detail : removed } ) ) ;
180+ console . log ( `${ logHead } Device removed: ${ removed . path } ` ) ;
181+ }
182+
183+ // Emit events for added devices
184+ for ( const added of addedPorts ) {
185+ this . dispatchEvent ( new CustomEvent ( "addedDevice" , { detail : added } ) ) ;
186+ console . log ( `${ logHead } Device added: ${ added . path } ` ) ;
187+ }
188+
189+ // Update our ports list
190+ this . ports = currentPorts ;
191+ } catch ( error ) {
192+ console . warn ( `${ logHead } Error checking device changes:` , error ) ;
193+ }
194+ }
195+
82196 async loadDevices ( ) {
83197 try {
84198 const portsMap = await invoke ( "plugin:serialplugin|available_ports" ) ;
@@ -200,9 +314,7 @@ class TauriSerial extends EventTarget {
200314 }
201315 } catch ( error ) {
202316 console . error ( `${ logHead } Error in read loop:` , error ) ;
203- if ( this . connected ) {
204- this . disconnect ( ) ;
205- }
317+ this . handleFatalSerialError ( error ) ;
206318 }
207319 }
208320
@@ -259,6 +371,10 @@ class TauriSerial extends EventTarget {
259371 } catch ( error ) {
260372 console . error ( `${ logHead } Error sending data:` , error ) ;
261373 this . transmitting = false ;
374+ if ( isBrokenPipeError ( error ) ) {
375+ // Treat as device removal to trigger reconnect flow
376+ this . handleFatalSerialError ( error ) ;
377+ }
262378 const res = { bytesSent : 0 } ;
263379 callback ?. ( res ) ;
264380 return res ;
@@ -329,14 +445,10 @@ class TauriSerial extends EventTarget {
329445 }
330446 }
331447
448+ // Deprecated: addPort is no longer needed since monitoring handles this
332449 addPort ( path ) {
333- // Reload devices to get updated port info
334- this . loadDevices ( ) . then ( ( ) => {
335- const added = this . ports . find ( ( p ) => p . path === path ) ;
336- if ( added ) {
337- this . dispatchEvent ( new CustomEvent ( "addedDevice" , { detail : added } ) ) ;
338- }
339- } ) ;
450+ // Device monitoring will automatically detect and emit addedDevice
451+ console . log ( `${ logHead } addPort called for ${ path } , monitoring will handle detection` ) ;
340452 }
341453}
342454
0 commit comments