Quinta Entrega: Exercícios de Redes Industriais
Quinta Entrega: Exercícios de Redes Industriais (PMR3412-2021)
Esta quinta entrega visa criar familiaridade com a configuração e o funcionamento de redes MODBUS. Nesta entrega será utilizada a biblioteca pymodbus do Python, que é uma implementação completa do protocolo Modbus. Para completar os exercícios, será necessário ter o Python instalado e outras bibliotecas poderão ser necessárias, siga as instruções dos exercícios para instalá-las.
1. Preparação do ambiente
Nesta entrega duas aplicações serão desenvolvidas para simular os dispositivos da nossa rede industrial usando o protocolo MODBUS sobre TCP/IP. Para cada aplicação serão implementados dois tipos de dispositivos sendo um para representar o mestre e a outro para representar os escravos. O mestre como sendo um monitor ou processador de dados e os escravos como sendo sensores e atuadores da planta industrial. Seguem as etapas comuns para o desenvolvimento dessas aplicações.
1.1 Pre-requisitos: instalação do módulo pymodbus
Instale a biblioteca pymodbus com o comando
$ pip install pymodbus==2.5.3
Nesta entrega vamos trabalhar com a versão síncrona da implementação do protocolo e suas funções de leitura e escrita em variáveis discretas e analógicas. Vamos também utilizar as funções de leitura de status da exceção.
1.2 Aplicação distribuída segundo modelo cliente-servidor
Essa aplicação de rede MODBUS deve seguir o modelo cliente-servidor implementados pela biblioteca pymodbus:
from pymodbus.client.sync import ModbusTcpClient
from pymodbus.server.sync import StartTcpServer
Para a função de cliente da nossa da aplicação deverá ser importado o módulo ModbusTcpClient e para a função de servidor deverá ser importado o módulo StartTcpServer.
⚠️Lembrando que apesar da biblioteca pymodbus possuir diversas versões tanto de client quanto server somente serão avaliadas as implementações das versões sync.
1.3 Amazenamento de dados
Cada aplicação deverá implementar seu próprio armazenamento de dados. Os dados são armazenados em dispositivos escravos em quatro tabelas diferentes.
Dois deles armazenam valores booleanos (1 bit) chamados Bobinas (saídas discretas) e Entradas Discretas, sendo definidos como co e di respectivamente. Dois tipos de valores numéricos como palavras de 16 bits chamados Registradores de Retenção e Registros de Entrada (entradas analógicas), sendo definidos como hr e ir respectivamente.
O conjunto destes dados é definido em um contexto. A criação do contexto do servidor é implementada através da cllasse ModbusServerContext, e para criar o contexto de cada escravo a classe ModbusSlaveContext, ambos importados da seguinte forma:
from pymodbus.datastore import ModbusServerContext, ModbusSlaveContext
Cada tabela pode ser inicializada com até 9999 endereços que só respondem adequadamente aos endereços aos que são inicializados. Portanto, se você inicializar um bloco de dados para os endereços de 0x00 até 0xFF, uma solicitação para 0x100 responderá com uma exceção de endereço inválido. A tabela a seguir mostra os tipos de variáveis com tamanho, endereços padrão dos registradores MODBUS:
Endereços | Tipo de variável (Acesso) | Tamanho |
---|---|---|
00001 - 09999 | Bobinas (leitura/escrita): | 1 bit |
10001 - 19999 | Entradas discretas (leitura) | 1 bit |
30001 - 39999 | Registros de entrada (leitura) | 16 bits |
40001 - 49999 | Registros de retenção (leitura/escrita) | 16 bits |
Estes blocos serão definidos a partir da classe ModbusSequentialDataBlock importadas a seguir:
from pymodbus.datastore import ModbusSequentialDataBlock
Inicialização das tabelas
A Inicialização das tabelas deverá ser realizada de acordo com a necessidade de cada aplicação descrita a seguir. Um exemplo de tabela contendo um bloco de dados inicializado com somente uma sequência de 2 valores iguais a 0 nos primeiros endereços das Entradas Discretas. Para isso, use o resultado da função ModbusSlaveContext() com argumento di como argumento slaves da função ModbusServerContext() da seguinte forma.
loja = ModbusSlaveContext(
di=ModbusSequentialDataBlock(0, [0]*2), zero_mode=True)
contexto = ModbusServerContext(slaves=loja)
⚠️Vale lembrar que do ponto de vista do dispositivo escravo todas as variáveis possuem permissões de leitura. Porém, somente as Bobinas e os Registros de Retenção são permitidos as funções de escrita. Além disso, todos os endereços da tabela têm o início referenciados a partir do endereço acessado pelo valor 0, independentemente do tipo de variável.
2. Primeiro exercício: Simulação de monitor de sensores (total 5 pontos)
No primeiro exercício desta entrega será implementado um monitor de sensores instalados em um conjunto de tanques de armazenamento de óleo. O dispositivo mestre deverá implementar um monitor de variáveis como sendo o cliente. Os escravos serão os sensores de transbordo e de temperatura de cada tanque como sendo o servidor.
2.1 Testando as funções de inicialização e leitura de dados
Para o exercício de simulação básica do protocolo MODBUS serão implementados dois programas: um como servidor e outro como cliente. O cliente fará o papel de interface homem-máquina (IHM) e o servidor fará o papel de cada sensor da planta.
Funções de inicialização das tabelas
Inicialize um contexto explicitamente contendo somente duas variáveis de cada tipo Entradas Discretas (di) e Registros de entrada(ir). O primeiro argumento da função de inicialização de servidor deve ser contexto, podendo ser chamado com uma variável no argumento address.
Os endereços serão fornecidos com IP definido pelo localhost e as portas utilizadas deverão usar a partir da PORT=5020. Dessa forma, na rotina do arquivo servidor.py deve-se utilizar:
StartTcpServer(contexto, address=("localhost", PORT))
Funções de leitura endereçadas
Para que o código do cliente.py abra uma conexão, conecte e faça leitura das variáveis com o servidor, rode o seguinte exemplo:
cliente = ModbusTcpClient("localhost", PORT)
cliente.connect()
cliente.read_discrete_inputs(0).getBit(0)
cliente.read_input_registers(0).getRegister(0)
Dessa forma, o cliente fará uma leitura de somente um elemento a partir do primeiro endereço das Entradas Discretas e Registros de Entrada.
Nossa aplicação distribuída representará a conexão entre um dispositivo mestre e vários escravos. Os sensores devem fazer a inicialização das tabelas com suas variáveis discretas e analógicas. O monitor deverá fazer uma sequência de solicitações síncronas das variáves de entrada dos sensores conectados.
2.2 Enunciado parte 1.1 (1 ponto)
Seguem os requisitos do programa, que deve ser implementado no arquivo server.py:
- Deve armazenar em variáveis os dois argumentos principais: as variáveis de campo das Entradas Discretas e Registros de entradacom somente um valor constante pré-definido para cada tabela. Declarar as variáveis, cujos nomes devem ser todos em maiúscula, logo após os imports.
- Somente dois sensores da planta representados por estes servidores devem rodar em processos separados utilizando endereços diferentes (somente portas diferentes) e armazenar valores iguais nas variáveis de campo.
- Deve imprimir os valores inicializados das suas variáveis de campos.
Seguem os requisitos do programa, que deve ser implementado no arquivo client.py:
- Deve armazenar em variáveis os dois argumentos principais: os endereços válidos para solicitação da tabela do escravo e os endereços (IP e porta) de cada servidor. Declarar as variáveis, cujos nomes devem ser todos em maiúscula, logo após os imports.
- Somente um dispositivo IHM representado por este cliente deve obter em somente uma execução.
- Deve obter todas as variáveis de campo através de solicitações de leitura endereçada para cada sensor ativo da planta.
- Deve imprimir os valores dos endereços das tabelas, o endereço de cada servidor e os resultados obtidos das solicitações de leitura.
No relatório, apresente:
- Listagem do código em Python dos arquivos server.py e client.py.
- Screenshot da execução dos servidores. Nesta imagem, é necessário mostrar somente os valores das variáveis pré-definidas devidamente identificados para inicialização dos contextos de cada servidor.
- Screenshot da execução do cliente. Nesta imagem, é necessário mostrar no início os valores de endereçamento definidas para as solicitações de leitura, em seguida os valores das variáveis de campo e os endereços (IP e porta) de cada servidor.
2.3 Testando as funções de exceção e escrita de dados
Nesta etapa será explorada a utilização das funções de escrita para que dispositivo mestre altere os valores de saídas discretas. Além disso, serão tratados os erros comuns referentes a dados inválidos de endereçamento e quantidade solicitado. Para tal, será utilzado a classe que definem as exceções do protocolo MODBUS através da linha: from pymodbus.pdu import ModbusExceptions.
Funções de escrita de dados
As funções de escritas de dados são aquelas que fazem solicitação de alteração das saídas discretas ou analógicas. Um exemplo seria a configuração de uma variável de setpoint de temperatura para um sistema de controle de um aquecedor. A partir da conexão aberta de um dispositivo mestre com um escravo inicializado a variável valoralvo pode ser escrita no endereço endereco da seguinte forma:
from pymodbus.client.sync import ModbusTcpClient
cliente = ModbusTcpClient()
cliente.connect()
cliente.write_register(endereco,valoralvo)
O mesmo vale para escrita nos endereços da tabela das Bobinas com o método write_coil(). Outras opções de escrita em endereços múltiplos, assim como as de leitura, também são definidas e podem ser encontrados neste link https://pymodbus.readthedocs.io/en/latest/source/library/pymodbus.client.html. Estas funções implementadas no protocolo MODBUS TCP possuem um limite de 248 bytes por solicitação e a configuração inválida deve gerar exceções que precisam ser tratadas.
Funções de exceção
O teste das funções que geram exceções pode ser realizado através de uma solicitação de escrita - por exemplo write_coils(). A seguinte rotina exemplifica a atribuição de um exception_code a uma variável codigo:
requisicao = cliente.write_coils(endereco,[0]*quantidade)
codigo = requisicao.exception_code
É possível extrair a descrição da exceção ocorrida com método de decodificação ModbusExceptions.decode(codigo), caso a combinação dos parâmetros endereco e quantidade resultem em uma solicitação de dados inválida.
O primeiro (0x02) indica que o endereço de dados recebido na solicitação não é um endereço permitido. O segundo (0x03) que um valor contido no campo de dados da consulta não é um valor permitido. Isso indica uma falha na estrutura do restante de uma solicitação complexa, como o comprimento implícito está incorreto.
O tratamento das exceções deve acontecer durante o processo de configuração da rede e seus respectivos dispositivos. Como parte do exercício será exigido que o cliente faça solicitações de escrita nas Entradas Discretas e trate as exceções e propague suas descrições.
⚠️Importante notar que os códigos de exceção podem também ser gerados através de requisições de leitura através do argumento count sendo passado com valor não permitido. Além disso, somente as tabelas Bobinas e Registros de Retenção são permitidas as funções de escrita.
2.4 Enunciado parte 1.2 (1 ponto)
Seguem os requisitos do programa, que deve ser implementado no arquivo server.py:
- Deve armazenar em variáveis o argumento principal: as variáveis de campo das Bobinas inicializadas com apenas dois valores constantes pré-definido. Declarar as variáveis, cujos nomes devem ser todos em maiúscula, logo após os imports.
- Somente um sensor da planta representado por este servidor deve rodar em processo único utilizando somente um endereço.
- Deve imprimir os valores inicializados das suas variáveis de campos.
Seguem os requisitos do programa, que deve ser implementado no arquivo client.py:
- Deve armazenar em variáveis os dois argumentos principais: os conjuntos de endereço inicial endereco e quantidade de variáveis e o valor das variáveis de campo das Bobinas a ser solicitada para escrita da tabela do escravo. Declarar as variáveis, cujos nomes devem ser todos em maiúscula, logo após os imports.
- Somente um dispositivo IHM representado por este cliente deve deve rodar em processo único.
- Deve fazer uma sequência de apenas quatro solicitações, sendo duas de escrita inválidas para obter as mensagens de exceção, uma de escrita válida e uma de leitura.
- Deve imprimir dois conjuntos de valores dos parâmetros inválidos que vão gerar as exceções 0x02 e 0x03, respectivamente, seguido do resultado obtido pela sua respectiva decodificação.
- Deve imprimir um conjunto de valores dos parâmetros válidos para solicitação de escrita.
- Deve imprimir os resultados obtidos da solicitação de leitura igual ao valor escrito no mesmo endereço.
No relatório, apresente:
- Listagem do código em Python dos arquivos server.py e client.py.
- Screenshot da execução do servidor. Nesta imagem, é necessário mostrar somente os valores das variáveis pré-definidas devidamente identificados para inicialização dos contextos.
- Screenshot da execução do cliente. Nesta imagem, é necessário mostrar no início somente os valores de endereçamento (válidos e inválidos) definidos para as solicitações de escrita e suas respectivas descrições de exceção, em seguida os valores das variáveis de campo obtidos pela solicitação de leitura.
2.5 Testando as estruturas de escravos e simulação de processos
Nesta etapa os diversos escravos que foram implementados através de servidores separados serão substituídos por somente um servidor representando os diversos dispositivos da nossa planta. Também será implementado a simulação de processos que atualizam as variáveis definidas no contexto. Para isso, as variáveis serão atualizadas executando uma tarefa paralela sendo o contexto o seu argumento.
Estrutura de escravos
Os dispositivos escravos podem ser inicializados fornecendo um dicionário contendo id do escravo e seu respectivo mapeamento de contexto. Para exemplificar serão criados 2 contextos de escravos (id 0 e 1) cada um com inicialização padrão da função ModbusSlaveContext(). Por último, o contexto do servidor é inicializado com o argumento single igual a False da seguinte forma:
escravos = {
0x00: ModbusSlaveContext(),
0x01: ModbusSlaveContext(),
}
contexto = ModbusServerContext(slaves=escravos, single=False)
Dessa forma, a função de inicialização retorna uma lista na variável contexto. Cada item desta lista contém um contexto de escravo como definido anteriormente. Para enviar um comando de leitura através um cliente deve-se utilizar as funções com os argumentos unit igual ao id_do_escravo - por exemplo read_discrete_inputs(0, unit=0x00).
Simulação de processos
A simulação de processos deve ocorrer através da definição de uma função que atualiza o processo que recebe como argumento a variável contexto. A classe Thread deve ser importada, inicializada com os parâmetros que simulam o processo e ativada da seguinte forma:
from threading import Thread
t = Thread(target=atualizar_processo, args=(contexto,))
t.start()
A partir deste estado, uma função atualizar_processo() começa a rodar em paralelo com o restante da rotina principal do código definido em seguida. Para simular um processo que tenha uma constante de tempo τ>constante_de_tempo, pode-se usar a função sleep(constante_de_tempo) importada do módulo time. Por último, a taxa de mudança das variáveis a cada iteração pode ser definida de forma arbitrária, porém constante.
Além disso, os métodos da classe ModbusSlaveContext com os argumentos getValues(registrador, endereco) e setValues(registrador, endereco, valores) para acessar e modificar as variáveis. Por último, a variável registrador se refere a uma das funções em sequência de acordo com as tabelas (fx=1,co), (fx=2,di), (fx=3,hr) e (fx=4,ir). Para maiores informações acesse https://pymodbus.readthedocs.io/en/latest/source/library/pymodbus.html.
ℹ️Dica para terminar o thread:
Podemos enviar algum sinal para o thread que desejamos encerrar enquanto uma outra função é chamada após o seu início. O sinal mais simples é a variável global aqui descrita como _CONTINUAR.
Uma vez que a atividade do thread t foi iniciada com t.start() a variável global pode ser atualizada chamando t.join(). O seguinte código na função principal auxilia no encerramento do thread já inicializado logo após uma função rodar_servidor() ser finalizada pelo usuário:
global _CONTINUAR
try:
rodar_servidor()
finally:
CONTINUAR = False
t.join()
Dessa forma, dentro de uma função atualizar_processo() podemos comparar a variável global enquanto o loop de atualização de contexto roda uma função atualizar_contexto(), com o seguinte código:
while _CONTINUAR:
atualizar_contexto()
2.6 Enunciado parte 1.3 (3 pontos)
Seguem os requisitos do programa, que deve ser implementado no arquivo server.py:
- Deve armazenar em variáveis os três argumentos principais: a constante_de_tempo comum do simulador de processo, a taxa de mudança e a variável de campo armazenada nos Registros de entrada com apenas um valor incial pré-definido para cada contexto de escravo. Declarar as variáveis, cujos nomes devem ser todos em maiúscula, logo após os imports.
- Somente dois sensores da planta representados por este servidor deve rodar em processo único utilizando somente um endereço.
- Deve atualizar as variáveis do contexto válidas da tabela Registros de entrada de maneira cíclica para cada contexto de escravo, com constante de tempo e taxa de mudança pré-definidas.
- Deve imprimir os valores de suas variáveis de campos atualizadas a cada iteração.
Seguem os requisitos do programa, que deve ser implementado no arquivo client.py:
- Deve armazenar em variáveis o argumento principal: os endereços válidos para solicitação de leitura das variáveis de campo da tabela do escravo. Declarar as variáveis, cujos nomes devem ser todos em maiúscula, logo após os imports.
- Somente um dispositivo IHM representado por este cliente deve deve rodar em processo único.
- Deve obter as variáveis do contexto válidas da tabela Registros de entrada do dispositivo escravo através de uma sequência de somente duas solicitações repetidas. Os valores obtidos a cada solicitação devem ser diferentes para cada contexto de escravo.
- Deve imprimir os valores obtidos para cada solicitação de leitura das variáveis de campo.
No relatório, apresente:
- Listagem do código em Python dos arquivos server.py e client.py.
- Screenshot da execução do servidor. Nesta imagem, é necessário mostrar no início somente os valores das variáveis pré-definidas de inicialização dos contextos e em seguida uma a sequência mínima das variáveis de campo resultante do simulador de processos.
- Screenshot da execução do cliente. Nesta imagem, é necessário mostrar no início somente os valores de endereçamento definidas para as solicitações de leitura e em seguida uma a sequência das variáveis de campo resultante das solicitações de leitura do monitoramento do processo.
3 Segundo Exercício: Controle de Processos (total 5 pontos)
No segundo exercício desta entrega será implementado um controlador de nível de um conjunto de tanques de armazenamento de óleo. O dispositivo mestre deverá implementar um processador de dados como sendo o cliente. Os escravos serão os sensores de nível e válvula de fluxo de cada tanque como sendo o servidor.
3.1 Testando as funções de varredura do processador
As funções do processador da aplicação de controle de processos devem ser simuladas utilizando a biblioteca asyncio para processamentos múltiplos através da linha: import asyncio. Através da sintaxe async def pode-se definir funções corrotinas que irão rodar as funções de scan do processador da seguinte forma:
async def rodar_processador():
while _CONTINUAR:
await asyncio.gather(
atualizar_processador(),
temporizador()
)
Neste caso, as duas funções atualizar_processador() e temporizador() também são definidas como corrotina e a função await asyncio.gather() atua de forma que se todos os itens a serem aguardados forem concluídos com sucesso, o resultado é uma lista agregada de valores retornados de cada corrotina.
A função temporizadora deve utilizar asyncio.sleep(tempo_de_scan) que bloquea por segundos de atraso e sempre suspende a tarefa atual, permitindo que outras tarefas sejam executadas. Em seguida, na rotina principal do programa use a função asyncio.run( rodar_processador()) para rodar a função corrotina principal. Para maiores informações sobre o funciamento da biblioteca asyncio acessar ao link https://docs.python.org/3/library/asyncio-task.html.
As funções de varredura do processador de dados do dispositivo mestre da aplicação de controle de processos deverão fazer solicitações de leitura das entradas dos escravos. Estas solicitações devem ocorrer de maneira síncrona, por tempo indeterminado e com temporizador configurado com um tempo de varredura pré-definido: tempo_de_scan.
Os escravos devem implementar simulação de processos de acordo com o primeiro exercício desta entrega e configurado com constante de tempo e taxa de mudança das variáveis. Os Registros de entrada de cada escravo devem armazenar os valores referentes ao sensor de nível e as Entradas Discretas servirão para armazenar o estado da botoeira ativação da válvula de fluxo.
3.2 Enunciado parte 2.1 (2 pontos)
Seguem os requisitos do programa, que deve ser implementado no arquivo server.py:
- Deve armazenar em variáveis os dois argumentos principais: as variáveis de campo armazenadas nas Entradas Discretas e Registros de entrada com apenas um valor inicial pré-definido para somente dois contextos de escravo. Declarar as variáveis, cujos nomes devem ser todos em maiúscula, logo após os imports.
- Somente dois sensores da planta representados por este servidor deve rodar em processo único utilizando somente um endereço.
- Deve imprimir as variáveis do contexto válidas da tabela Entradas Discretas a cada iteração do simulador de processos indicando o estado fixo da botoeira de ativação para cada contexto de escravo.
- Deve atualizar as variáveis do contexto válidas da tabela Registros de entrada de maneira cíclica de acordo com o estado atual da botoeira de ativação para cada contexto de escravo, com constante de tempo e taxa de mudança pré-definidas.
Seguem os requisitos do programa, que deve ser implementado no arquivo client.py:
- Deve armazenar em variáveis os dois argumentos principais: a constante_de_tempo associado ao temporizador da varredura e os endereços válidos para solicitação de leitura das variáveis de campo da tabela do escravo. Declarar as variáveis, cujos nomes devem ser todos em maiúscula, logo após os imports.
- Somente um dispositivo processador representado por este cliente deve deve rodar em processo único.
- Deve obter as variáveis do contexto válidas das tabelas Entradas Discretas e Registros de Entrada dos dispositivos de maneira cíclica para cada contexto de escravo. Os valores obtidos a cada solicitação devem ser diferentes para cada contexto de escravo.
- Deve imprimir os valores obtidos para cada solicitação de leitura das variáveis de campo.
No relatório, apresente:
- Listagem do código em Python dos arquivos server.py e client.py.
- Screenshot da execução do servidor. Nesta imagem, é necessário mostrar no início somente os valores das variáveis pré-definidas de inicialização dos contextos e em seguida uma a sequência mínima das variáveis de campo resultante do simulador de processos.
- Screenshot da execução do cliente. Nesta imagem, é necessário mostrar no início somente os valores de endereçamento definidas para as solicitações de leitura e em seguida uma a sequência mínima das variáveis de campo resultante do monitoramento do processo.
3.3 Projetando o algoritmo de controle de processo
Nesta etapa é apresentado o projeto do controle de processo. Será necessário que o dispositivo mestre comande a válvula de fluxo para suspender o processo de acordo com o monitoramento dos níveis do tanque de armazenamento. Cada iteração da varredura do processador deverá comparar os valores deRegistros de entradacom um valor pré-definido e fazer solicitações de escrita de acordo com seu resultado.
Para o contexto de cada escravo as solicitações de escrita endereçadas devem alterar os valores da tabela referente aos Registros de Retenção. Estas solicitações tem por objetivo alterar os valores das taxas de mudança aplicadas no simulador de processos. É possível simular o controle através da reconfiguração da variável que define a dinâmica do processo através das funções getValues() e setValues() para acessar e modificar os contextos de escravo.
Lógica do algoritmo
O resultado final deverá seguir a lógica de que a variável da taxa de mudança deve ser nula durante o simulador de processo sempre que o valor do sensor de nível ultrapassar um determinado patamar. Independentemente do valor da taxa de mudança, o comportamento do simulador de processo durante este estado e o seu estado inicial deve permanecer idêntico às implementações anteriores do primeiro exercício.
⚠️Importante notar que da forma como este exercício foi elaborado há uma liberdade grande para simular lógicas de controle relativamente complexas. No entanto, o objetivo principal desta entrega é somente compreender o funcionamento do protocolo de redes industriais. Para atingir este objetivo será exigido que seja elaborado uma sequência de solicitações no processador de dados além do algoritmo do simulador de processo mínimos.
3.4 Enunciado parte 2.2 (3 pontos)
Seguem os requisitos do programa, que deve ser implementado no arquivo server.py:
- Deve armazenar em variáveis os dois argumentos principais: as variáveis de campo armazenada nas Registros de entrada e Registros de Retenção com apenas um valor incial pré-definido para apenas um contexto de escravo. Declarar as variáveis, cujos nomes devem ser todos em maiúscula, logo após os imports.
- Somente dois sensores da planta representados por este servidor deve rodar em processo único utilizando somente um endereço.
- Deve atualizar as variáveis do contexto válidas da tabela Registros de entrada de acordo com os valores de taxa de mudança definidos nas variáveis do Registros de Retenção segundo a lógica do algoritmo projetado de maneira cíclica para o contexto de escravo, com constante de tempo e taxa de mudança pré-definidas.
- Deve imprimir as variáveis do contexto válidas das tabelas Registros de entrada e Registros de Retenção a cada iteração do simulador de processos.
Seguem os requisitos do programa, que deve ser implementado no arquivo client.py:
- Deve armazenar em variáveis os dois argumentos principais: o valor pré-definido para comparar com o valor obtido dosRegistros de entrada e os endereços válidos para solicitação de escrita das variáveis de campo da tabela do escravo. Declarar as variáveis, cujos nomes devem ser todos em maiúscula, logo após os imports.
- Somente um dispositivo processador representado por este cliente deve deve rodar em processo único.
- Deve obter as variáveis do contexto válidas da tabela Registros de entrada de maneira cíclica para cada contexto de escravo. Os valores obtidos a cada solicitação devem ser diferentes para cada contexto de escravo.
- Deve fazer uma solicitação de escrita das variáveis do contexto válidas da tabela Registros de Retenção somente quando for identificado a mudança de estado para cada contexto de escravo segundo a lógica do algoritmo projetado.
- Deve imprimir os valores obtidos para cada solicitação de leitura das variáveis de campo.
No relatório, apresente:
- Listagem do código em Python dos arquivos server.py e client.py.
- Screenshot da execução do servidor. Nesta imagem, é necessário mostrar no início somente os valores das variáveis pré-definidas de inicialização dos contextos e em seguida uma a sequência mínima das variáveis de campo de maneira que seja explícito a mudança de estado do algoritmo no simulador de processo.
- Screenshot da execução do cliente. Nesta imagem, é necessário mostrar no início somente os valores de endereçamento definidas para as solicitações de leitura e em seguida uma a sequência mínima das variáveis de campo de maneira que seja explícito a mudança de estado do algoritmo no controle do processo.