Progettazione software OO per interfacciamento hardware

5

La mia domanda è: come posso massimizzare l'incapsulamento per le classi wrapper che si interfacciano con l'hardware.

L'hardware è collegato a un PC tramite COM-Ports o porte USB e sto leggendo / scrivendo direttamente o tramite un'API. In entrambi i casi voglio concludere un corso.

Ho letto dello sviluppo del software e dell'architettura in generale (in C #), ma non ho ancora un'idea di come le classi e gli oggetti dell'architettura possano essere collegati all'hardware.

Le cose che mi sono passate per la testa in questo senso sono i seguenti aspetti:

  1. Quando e come devo inizializzare l'hardware, in modo che i client della classe incapsulante non debbano preoccuparsi di questo (o il più piccolo possibile), ad esempio: è OK inizializzare l'hardware / port una volta all'avvio del programma e chiuderlo alla fine (che è quello che sto facendo attualmente) o dovrei sempre aprire e chiudere l'hardware / la porta secondo necessità.
  2. In relazione al primo punto: quando e come dovrei rilasciare e distruggere l'hardware e il relativo pacchetto / classe correlata.
  3. Devo memorizzare lo stato dell'hardware nella classe o eseguire sempre una query sul dispositivo?
  4. Come gestire errori / eccezioni hardware (con garbo)? In questo momento, dal momento che sto inizializzando l'hardware all'avvio, se non viene trovato un po ', il mio programma non si avvierà correttamente. Preferirei l'avvio e segnalare che non è stato trovato alcun hardware, in modo che possa essere collegato. Ma non riesco a pensare ad un'architettura decente per raggiungere questo obiettivo.
  5. Come posso adattarmi all'inversione di dipendenza: attualmente sto usando le fabbriche per inizializzare l'hardware e distribuire le classi wrapper attraverso un DI-container. Ciò tuttavia significa che sto inizializzando solo una volta all'avvio.

Mi rendo conto che questo è un argomento ampio con vari argomenti secondari, ma questi punti sono tutti strettamente correlati tra loro e trovo difficile trovare informazioni valide e coerenti su questo argomento.

FYI: Sono uno sviluppatore C #, ma sono interessato ai concetti generali.

    
posta packoman 26.07.2017 - 10:20
fonte

2 risposte

5

Hai menzionato le porte COM, quindi parlerò specificamente di quel caso.

  • Designing and structuring classes around hardware interfaces to maximize encapsulation.

Ci sono due modi per utilizzare le porte COM, Comando / Risposta e Streaming. La modalità di progettazione dell'interfaccia dipenderà dal modo in cui si prevede di utilizzare la porta. A volte leggerete e scrivete sul dispositivo o aprirete un flusso costante di dati da un dispositivo a un altro?

Per Comando / Risposta creerei un'interfaccia asincrona come questa.

public interface IDevice : IDisposable
{
    // Sends command without listening for a response
    Task Send(Command command);

    // Sends Command and waits for response
    Task<Response> Send(Command command);

    // Sends Command and listens for Response. Cancellable
    Task<Response> Send(Command command, CancellationToken token);
}

Per lo streaming di dati, solitamente utilizzo la classe Stream con un StreamWriter personalizzato o StreamReader .

In entrambi i casi, vuoi essere sicuro di avere interfaces o abstract su cui contare, perché ti consigliamo di prendere in giro l'hardware per testare altre parti del tuo codice.

  • How to handle initialization and releasing/destroying of the hardware and their wrapping/related class.

Hai fondamentalmente due opzioni per l'inizializzazione qui. Puoi aprire lo stream nel costruttore o fornire un metodo Open() . O funzionerà e entrambi hanno i loro pro e contro. Aprire la connessione nel Ctor significa che potresti ottenere delle eccezioni dal Ctor e ad alcune persone questo non piace. D'altra parte, avere un metodo che deve essere chiamato prima di usare una classe è un tipo di accoppiamento temporale a cui molte persone non tengono conto. È facile dimenticare di chiamare il metodo per mettere effettivamente la classe in uno stato utilizzabile. Quindi, scegli quello che si adatta meglio alla sensibilità della tua squadra.

Per la de struttura, ti consigliamo di implementare l'interfaccia IDisposable in modo da poter chiudere e rilasciare la porta in modo controllato e affidabile.

using( var device = new Device("COM5")
{
    // do stuff with port
} 

Quando esci dal blocco using , viene chiamato il metodo Dispose() che hai implementato, rilasciando la porta.

  • How to store hardware state (for example storing the state in the hardware wrapper-class, a context vs. always querying the hardware, when a state is requested).

Di solito non immagazzino molto stato. Normalmente stiamo interrogando l'hardware per qualunque sia lo stato corrente e per il rifiuto.

  • How to make this fit in with OO best practices, in particular dependency inversion.

Tratta l'hardware come se trattassi un database. Codice per interfacce anziché implementazioni. Abbiamo creato un interface che rappresenta il nostro hardware e un'implementazione di tale interfaccia, quindi qualsiasi codice client si basa sulla nostra interfaccia IDevice . Il costruttore ti inietta l'implementazione come faresti con qualsiasi altra classe.

public class Foo
{
      private readonly IDevice _device;

      public Foo(IDevice device)
      {
           _device = device;
      }

      //...
}

In realtà, in genere iniettare una fabbrica anziché un esempio in cui ho a che fare con IDisposable per semplificare i test.

    
risposta data 26.07.2017 - 12:14
fonte
1

Potrebbero esserci delle dipendenze sul tuo hardware specifico. Per esempio. nel nostro caso, inviamo un comando all'avvio affinché l'hardware esegua alcuni passaggi di inizializzazione / calibrazione. Il nostro hardware non accetta i nuovi comandi prima che abbia finito di inviare la sua risposta al comando precedente (sebbene l'azione avviata dal comando possa durare "per sempre" - ma il acknowledge deve essere ricevuto prima del comando successivo). Preferisco i comandi Start e Stop nell'interfaccia (quindi funziona anche con l'iniezione delle dipendenze).

Per comunicare via porta seriale, abbiamo estratto un'interfaccia semplice ISerialPort e aggiunto un SerialPortWrapper che contiene un'istanza .Net SerialPort e espone l'interfaccia (che ci consente di scrivere alcuni manichini che potrebbero inviare un errore hardware specifico risposte per i test).

Anche un'applicazione multi-thread deve assicurarsi che i comandi non vengano inviati contemporaneamente sulla porta. Facciamo attenzione che esista un solo wrapper per porta COM e un meccanismo di sincronizzazione appropriato in quell'istanza wrapper.

Gli errori inviati dall'hardware sono inclusi in ErrorEvent quando viene utilizzato un thread per il monitoraggio del dispositivo. Non puoi utilizzare Exception in quel contesto poiché ucciderebbe solo il thread di monitoraggio.

Lo stato del dispositivo viene interrogato meglio dal dispositivo, se riesce a farcela (ho riscontrato problemi con le query ad alta frequenza, potrebbero esserci problemi di interrupt nel dispositivo); in alternativa, una cache può decorare la classe di comunicazione e riemettere una query a seconda dell'ora in cui il valore è stato interrogato per ultimo.

    
risposta data 31.07.2017 - 10:18
fonte

Leggi altre domande sui tag