Design de software OO para interface de hardware

5

Minha pergunta é: Como posso maximizar o encapsulamento para classes de wrapper que fazem interface com o hardware.

O hardware está conectado a um PC através de portas COM ou portas USB e eu estou lendo / escrevendo diretamente ou através de uma API. Qualquer um dos quais eu quero envolver em uma aula.

Eu tenho lido sobre desenvolvimento de software e arquitetura em geral (em C #), mas ainda não tenho a sensação de como classes de arquitetura e objetos em torno de hardware.

As coisas que estão passando pela minha cabeça a esse respeito são os seguintes aspectos:

  1. Quando e como devo inicializar o hardware, para que os clientes da classe de encapsulamento não precisem se preocupar com isso (ou o menor possível), por exemplo: É OK inicializar o hardware / porta uma vez na inicialização do programa e fechá-lo no final (que é o que estou fazendo atualmente) ou devo sempre abrir e fechar o hardware / porta conforme necessário.
  2. Relacionado ao primeiro ponto: Quando e como devo liberar e destruir o hardware e sua classe de embalagem / relacionada.
  3. Devo armazenar o estado do hardware na classe ou sempre consultar o dispositivo?
  4. Como lidar com erros / exceções de hardware (normalmente)? Agora, desde que eu estou inicializando o hardware na inicialização, se algum deles não for encontrado, meu programa não será iniciado corretamente. Eu preferiria que a inicialização e sinalizar que algum hardware não foi encontrado, para que possa ser conectado. Mas não consigo pensar em uma arquitetura decente para conseguir isso.
  5. Como posso ajustar isso com a inversão de dependências: atualmente, estou usando fábricas para inicializar o hardware e distribuir as classes de invólucro por meio de um contêiner de DI. No entanto, isso significa que estou inicializando apenas uma vez na inicialização.

Eu percebo que este é um tópico amplo com vários subtópicos, mas esses pontos estão intimamente relacionados entre si e eu acho difícil encontrar informações boas e coerentes sobre esse tópico.

FYI: Sou um desenvolvedor de C #, mas estou interessado nos conceitos gerais.

    
por packoman 26.07.2017 / 10:20
fonte

2 respostas

5

Você mencionou as portas COM, portanto, abordarei especificamente esse caso.

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

Existem duas maneiras de utilizar as portas COM, Command / Response e Streaming. Como você projeta sua interface dependerá de como você planeja usar a porta. Você só estará lendo e gravando no dispositivo ocasionalmente ou estará abrindo um fluxo constante de dados de um dispositivo para outro?

Para o comando / resposta eu criaria uma interface assíncrona mais ou menos assim.

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);
}

Para o fluxo de dados, normalmente aproveito a classe Stream juntamente com um StreamWriter ou StreamReader personalizado.

Em ambos os casos, você quer ter certeza de que interfaces ou abstract classes dependem, porque você vai querer ridicularizar o hardware para testar outras partes do seu código.

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

Você tem basicamente duas opções de inicialização aqui. Você pode abrir o fluxo no construtor ou fornecer um método Open() . Ou vai funcionar e ambos têm seus prós e contras. Abrir a conexão no ctor significa que você pode obter exceções do ctor e algumas pessoas não gostam disso. Por outro lado, ter um método que deve ser chamado antes de usar uma classe é um tipo de acoplamento temporal que muitas pessoas não se importam. É fácil esquecer de chamar o método para colocar a classe em um estado utilizável. Então, escolha aquele que se encaixa melhor com a sensibilidade da sua equipe.

Para o de struction, você desejará implementar a interface IDisposable para poder fechar e liberar a porta de maneira controlada e confiável.

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

Quando saímos do bloco using , o método Dispose() que você implementou é chamado, liberando a 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).

Eu normalmente não armazeno muito estado. Normalmente, estamos consultando o hardware para qualquer estado atual e refutando isso.

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

Apenas trate o hardware como você trataria um banco de dados. Código para interfaces em vez de implementações. Criamos um interface que representa nosso hardware e uma implementação dessa interface, portanto, qualquer código de cliente depende da nossa interface IDevice . Você apenas construtor injetar a implementação como você faria com qualquer outra classe.

public class Foo
{
      private readonly IDevice _device;

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

      //...
}

Na verdade, eu normalmente injetamos uma fábrica em vez de uma instância quando estou lidando com IDisposable para tornar o teste mais fácil.

    
por 26.07.2017 / 12:14
fonte
1

Pode haver dependências no seu hardware específico. Por exemplo. no nosso caso, enviamos um comando na inicialização para que o hardware execute algumas etapas de inicialização / calibragem. Nosso hardware não aceita novos comandos antes de terminar de enviar sua resposta ao comando anterior (embora a ação iniciada pelo comando possa durar "para sempre" - mas o acknowledge deve ser recebido antes do próximo comando). Eu prefiro os comandos Start e Stop na interface (por isso, também funciona com injeção de dependência).

Para comunicação via porta serial, extraímos uma interface simples ISerialPort e adicionamos um SerialPortWrapper que contém uma instância .Net SerialPort e expõe a interface (o que nos permite escrever alguns dummies que podem enviar erro de hardware específico respostas para testes).

Além disso, um aplicativo multi-threaded deve garantir que os comandos não sejam enviados ao mesmo tempo pela porta. Tomamos o cuidado de que exista apenas um wrapper por porta COM e de um mecanismo de sincronização apropriado nessa instância do wrapper.

Erros enviados do hardware são agrupados em ErrorEvent quando um thread para monitorar o dispositivo é usado. Você não pode usar um Exception nesse contexto, pois isso mataria apenas o thread de monitoramento.

O estado do dispositivo é melhor consultado a partir do dispositivo - se ele lidar com ele (eu tive problemas com consultas de alta frequência, pode haver interrupt problemas no dispositivo); alternativamente, um cache pode decorar a classe de comunicação e reemitir uma consulta dependendo do tempo desde que o valor foi consultado por último.

    
por 31.07.2017 / 10:18
fonte