1+ using MakerPrompt . Shared . Infrastructure ;
2+ using MakerPrompt . Shared . Models ;
3+ using MakerPrompt . Shared . Utils ;
4+ using System . Text ;
5+ using System . Threading . Tasks . Dataflow ;
6+ using UsbSerialForMacOS ;
7+ namespace MakerPrompt . MAUI . Services
8+ {
9+ public class SerialService : BaseSerialService , ISerialService
10+ {
11+ private UsbSerialManager ? _manager = new ( ) ;
12+ private readonly BufferBlock < string > _commandQueue = new ( ) ;
13+ private CancellationTokenSource ? _cts ;
14+ private Task ? _sendTask ;
15+ private Task ? _receiveTask ;
16+ private bool _disposed = false ;
17+ public bool IsSupported => true ;
18+
19+ public SerialService ( ) { }
20+
21+ public async Task < bool > ConnectAsync ( PrinterConnectionSettings connectionSettings )
22+ {
23+ if ( IsConnected ) return IsConnected ;
24+ if ( connectionSettings . ConnectionType != ConnectionType || connectionSettings . Serial == null )
25+ throw new ArgumentException ( "Invalid connection settings" ) ;
26+
27+ var portName = connectionSettings . Serial . PortName ;
28+ var baudRate = connectionSettings . Serial . BaudRate ;
29+
30+ try
31+ {
32+ _manager ??= new UsbSerialManager ( ) ;
33+ _cts ? . Dispose ( ) ;
34+ _cts = new CancellationTokenSource ( ) ;
35+
36+ IsConnected = _manager . Open ( portName , baudRate ) ;
37+ ConnectionName = portName ;
38+
39+ _sendTask = Task . Run ( ( ) => SendLoopAsync ( _cts . Token ) ) ;
40+ _receiveTask = Task . Run ( ( ) => ReceiveLoopAsync ( _cts . Token ) ) ;
41+ RaiseConnectionChanged ( ) ;
42+ }
43+ catch ( Exception ex )
44+ {
45+ IsConnected = false ;
46+ throw new SerialException ( "Connection failed" , ex ) ;
47+ }
48+
49+ return IsConnected ;
50+ }
51+
52+ public async Task DisconnectAsync ( )
53+ {
54+ if ( ! IsConnected ) return ;
55+ _cts ? . Cancel ( ) ;
56+
57+ try
58+ {
59+ if ( _sendTask != null )
60+ await _sendTask . ConfigureAwait ( false ) ;
61+ if ( _receiveTask != null )
62+ await _receiveTask . ConfigureAwait ( false ) ;
63+ }
64+ finally
65+ {
66+ try
67+ {
68+ _manager ? . Close ( ) ;
69+ }
70+ catch
71+ {
72+ // swallow close exceptions on shutdown
73+ }
74+
75+ _manager = null ;
76+ IsConnected = false ;
77+ RaiseConnectionChanged ( ) ;
78+ }
79+ }
80+
81+ public override async Task WriteDataAsync ( string data )
82+ {
83+ if ( ! IsConnected ) return ;
84+ await _commandQueue . SendAsync ( data ) ;
85+ }
86+
87+ public async Task < IEnumerable < string > > GetAvailablePortsAsync ( )
88+ {
89+ return _manager ? . AvailablePorts ( ) . OrderBy ( p => p ) . ToList ( ) ?? [ ] ;
90+ }
91+
92+ private async Task SendLoopAsync ( CancellationToken ct )
93+ {
94+ try
95+ {
96+ while ( IsConnected && ! ct . IsCancellationRequested )
97+ {
98+ var command = await _commandQueue . ReceiveAsync ( ct ) ;
99+ var manager = _manager ;
100+ if ( manager == null || ct . IsCancellationRequested )
101+ {
102+ break ;
103+ }
104+ manager . Write ( command ) ;
105+ await Task . Delay ( 10 , ct ) ;
106+ }
107+ }
108+ catch ( OperationCanceledException )
109+ {
110+ // Normal shutdown
111+ }
112+ }
113+
114+ private async Task ReceiveLoopAsync ( CancellationToken ct )
115+ {
116+ while ( ! ct . IsCancellationRequested && IsConnected )
117+ {
118+ try
119+ {
120+ var manager = _manager ;
121+ if ( manager == null || ct . IsCancellationRequested )
122+ {
123+ break ;
124+ }
125+ var bytesRead = manager . Read ( 4096 ) ;
126+ if ( bytesRead . Length > 0 )
127+ {
128+ var received = Encoding . UTF8 . GetString ( bytesRead . ToArray ( ) ) ;
129+ ProcessReceivedData ( received ) ;
130+ }
131+ await Task . Delay ( 10 , ct ) ;
132+ }
133+ catch ( OperationCanceledException )
134+ {
135+ break ;
136+ }
137+ catch ( Exception ex )
138+ {
139+ Console . WriteLine ( $ "Receive error: { ex . Message } ") ;
140+ IsConnected = false ;
141+ await DisposeAsync ( ) ;
142+ }
143+ }
144+ }
145+
146+ public override async ValueTask DisposeAsync ( )
147+ {
148+ if ( _disposed ) return ;
149+ _disposed = true ;
150+
151+ try
152+ {
153+ await DisconnectAsync ( ) ;
154+ _cts ? . Dispose ( ) ;
155+ }
156+ catch ( ObjectDisposedException )
157+ {
158+ // Ignore if already disposed to ensure idempotent disposal
159+ }
160+ }
161+
162+ public Task < bool > CheckSupportedAsync ( ) => Task . FromResult ( true ) ;
163+
164+ public Task RequestPortAsync ( ) => Task . CompletedTask ;
165+ }
166+ }
0 commit comments