Blender 2.60 e Cycles

Há alguns meses eu ouvi falar do Cycles que é um novo renderizador para o Blender 2.6x (versão “estável” do programa depois da grande mudança de interface) e, depois de descobrir que não precisa de uma GPU com CUDA, resolvi testar o seu poder.

O Blender ficou muito mais prático com essa interface, mas é preciso se acostumar com os novos comandos e a posição dos botões e menus. No site Blender Guru tem uma cola dos comandos que é bastante completa.

Sobre o Cycles, ele é um Ray tracer de verdade e produz imagens muito mais reais que o renderizador interno (Internal), além de trazer a possibilidade de editar a cena e produzir imagens ao mesmo tempo.

O modo de funcionamento do Cycles é bem diferente do Internal pois ele não divide a imagens em quadrados e renderiza cada um numa thread. Ele gera a imagem inteira de uma vez e vai aprimorando o resultado a cada iteração, o que é um pouco estranho já que a renderização só acaba após o número de passos que for definido.

Em alguns testes que fiz, de 10 a 50 passos, já é razoável para ver o que está acontecendo na imagem (bom para editar em tempo real), mas para uma boa qualidade de imagem são precisos de 1000 a 4000 passos, algo que pode demorar horas, mesmo para uma cena razoavelmente simples (no meu Core i5).

Pode parecer que o Cycles é uma faca de dois gumes, já que permite visualizar o resultado praticamente em tempo real, mas demora pra produzir uma imagem final com pouca granularidade. Na verdade, o Cycles traz ainda mais um diferencial: a possibilidade de renderizar a imagem via GPU com CUDA/OpenCL, o que pode ter um ganho alto de desempenho, além de deixar a CPU livre para outras tarefas. Pena que minha placa de vídeo é bem ruim (a Intel não aprende mesmo…) e não serve para o propósito.

Abaixo um vídeo do Cycles em ação:

De cara dá pra perceber que a qualidade de imagem é muito maior que se fosse renderizado com o Internal, principalmente quando se pensa em iluminação. Antigamente, você precisaria colocar umas 3 lâmpadas, no mínimo, e fazer centenas de ajustes para conseguir um efeito legal. Agora você pode adicionar apenas um objeto com o material “Emission” e você tem uma fonte de luz que reflete nas superfícies e produz um efeito muito bom.

Outra coisa que sempre foi praticamente impossível de fazer direito no Blender é vidro ou qualquer coisa transparente. No Cycles, ao escolher o material Glass, você tem de cara um vidro de alta qualidade sem precisar fazer qualquer ajuste. Para outros materiais, basta ajustar o índice de refração (IOR) para o valor real do mesmo.

Em cinco minutos coloquei 3 planos, um cubo e um chimpanzé (Suzanne + subsurf) numa cena iluminada por outros dois planos e modifiquei os materiais. Depois de meia hora renderizando em torno de 1000 passos, fiquei com uma imagem que demoraria horas para ajustar a iluminação e ter um efeito parecido.

Não é nenhuma obra de arte, mas mostra como é fácil ter efeitos interessantes sem perder muito tempo.

A única diferença entre o uso do Blender Internal e o Cycles é a janela de edição de materiais que, por enquanto, é um pouco pobre em usabilidade e obriga o uso de Composite Nodes para coisas não básicas. De qualquer forma, não deixa de ser algo bom pois o uso do Node editor não é complicado e acaba sendo prático.

Uma coisa que senti falta é a pré-visualização do material, mas com a renderização em tempo quase real, acaba não sendo tão ruim.

Como o Cycles ainda não está oficialmente incorporado ao Blender, é preciso pegar um build que tenha sido feito recentemente no SVN do projeto para poder usar. No site GraphicAll.org tem vários e só precisa escolher o sistema operacional e arquitetura.

De acordo com o roadmap do Blender, o Cycles deve chegar no Blender 2.61 em dezembro.

Recuperando dados de partições Ext3 e Ext4

Nessa semana tive que recuperar um arquivo deletado acidentalmente num servidor rodando CentOS com sistema de arquivos Ext4.

A fração de segundo depois que você aperta Enter e percebe seu erro, mas é tarde demais, você acabou de excluir um arquivo ou diretório valioso e não existia nenhum backup. Ou talvez você tinha um backup, mas é de um mês atrás…
E em estado de choque que você vê num flash o último mês passar diante de seus olhos e percebe a dor que vai dar para fazer tudo de novo…
Carlo Wood

Recuperar dados de partições Ext3 e 4 é bastante complicado, como explicado com muitos detalhes neste link.

A primeira coisa a fazer é desmontar a partição afetada ou, se não for possível, desligar o computador e usar um LiveCD de alguma distribuição Linux (usei Ubuntu num pendrive) ou colocar o HD dentro de outra máquina.

Para resolver o problema, eu testei sem sucesso duas ferramentas (foremost e scalpel) que fazem análise sequencial dos blocos do HD procurando por padrões conhecidos, algo que demorou uma eternidade para verificar toda a partição de 1TB que eu tinha e “recuperou” centenas de arquivos corrompidos com nomes do tipo “100201234.jpg” e nada do que eu queria.

Talvez o fato da minha partição ser LVM e a real partição Ext4 estivesse dentro do volume lógico os programas não funcionaram, mas de qualquer forma existe uma solução melhor.

Uma ferramenta chamada Extundelete foi quem salvou meu dia. Ela acessa o journal do sistema de arquivos e consegue achar novamente os arquivos com os nomes originais e, no meu caso, até manteve a estrutura de diretórios de onde estava o arquivo.

Pra usar, instale o “Extundelete” pelo gerenciador de pacotes da distribuição usada (para Ubuntu):
# apt-get install extundelete

ou pegue o código fonte do site e compile com
# ./configure
# make
# make install

Para executar, é preciso passar a partição a ser usada para que o programa monte em modo read only (se for LVM, como no meu caso, vai ser necessário seguir mais alguns passos).
# extundelete /dev/nome_da_particao --restore-all

Em menos de um minuto, o comando acima restaurou centenas de arquivos, incluindo o que eu queria.

Escolhendo o canal do Wi-Fi

Algo muito comum é instalar um roteador Wi-Fi num ambiente pequeno e, ainda assim, ter problemas com a velocidade da conexão.

Pode acontecer de existirem vários outros roteadores de vizinhos usando o mesmo canal e possivelmente com um sinal mais forte (aquele povo que compra antenas enormes na Uruguaiana ou Santa Ifigênia) estarem fazendo a sua placa wireless perder muitos pacotes.

Todo roteador Wi-Fi vem com uma página de configuração que permite escolher o canal da faixa de 2.4GHz em que vai operar.
Caso não se lembre como acessar ou se não foi você quem instalou, provavelmente olhando qual o gateway da sua máquina você acha o IP do roteador (algo como 192.168.0.1).

Na hora de escolher o canal ideal para sua conexão, é preciso saber que parte da banda os roteadores mais próximos estão localizados e, para isso, existe um excelente aplicativo gratuito chamado InSSIDer que fornece informações bem detalhadas.

Em uma das abas do programa, ele mostra os canais das redes ao alcance, assim como suas intensidades. Ele também mostra a intensidade ao longo do tempo além de outras informações como fabricante, velocidade da rede, largura do canal etc.

Assim, é fácil escolher o canal do seu Wi-Fi apenas verificando qual está menos utilizado (geralmente as extremidades).

Outra coisa é, se você tem um Wi-Fi 802.11n de 300mpbs (bastante comum nos roteadores de 2011) ele pode ocupar o dobro da banda no espectro por utilizar 2 canais, o que melhora a qualidade geral da sua internet, mas causa mais interferência na dos outros :) .

Esse é um programa que vale a pena ter instalado.

Python 3 – exec e outras mudanças

Já tem um tempo que tento usar somente Python 3 para meus projetos em Python, mas, como muita gente ainda está presa em versões antigas (como o pessoal do CentOS que há um mês mudou do Python 2.4 para o ainda defasado 2.6), sou obrigado a trabalhar com os dois ambientes.

Portar pequenos programas de uma versão para outra pode ser bastante simples (como no Txt2Tags que precisei de umas 2 ou 3hs para fazer funcionar) quando já se conhece os possíveis problemas e suas soluções, mas nem tudo está bem documentado.

Claro que tudo isso seria muito mais difícil se não houvesse script chamado 2to3.py que vem junto com as versões mais novas do interpretador para automatizar o processo. O problema que existem coisas que ele é incapaz de traduzir ou que precisam de mudanças mais bruscas no código para que funcionem.

Por algum motivo, o site “Dive into Python 3” que tinha uma versão online do livro homônimo foi removido do ar e perdi a melhor referência para o tópico que existia, ** Encontrei um novo mirror do Dive into Python 3 **

Também existem outros bons textos por ai.

Algumas das mudanças mais importantes na nova versão são os iteradores usados em lugares que antes retornavam listas, o uso de unicode por padrão (que na minha opinião já vale por todo o trabalho de portar código) e a troca de algumas palavras-chaves (statements) por funções e vice-versa.

Sobre essa última mudança, as palavras-chave tem o poder de mudar o local em que se encontram enquanto outros nomes podem significar qualquer coisa que o programador queira e só podem ser verificados em tempo de execução, não podendo transformar o ambiente.

Por exemplo, o comando yield faz com que o a função que o contenha seja transformada num gerador mesmo que ele nunca seja executado ou tenha bytecode para isso!

def gerador():
    if 0:
        yield

Neste código acima, o interpretador vai ver que o if nunca será executado e não vai gerar bytecode para nada dentro dele (no caso o yield), mas a função não vai retornar o implícito None quando for chamada. Ela vai retornar um gerador vazio que pode ser usado num loop for.

Usando o módulo dis para desmontar bytecode vemos que a função só retorna None, mas ela vai ter um flag para mostrar sua utilidade:

>>> dis.dis(gerador)
  2           0 LOAD_CONST               0 (None)
              3 RETURN_VALUE

Ainda sobre isso, True, False e None agora são palavras reservadas e que não podem ser modificadas, fazendo com que, se o exemplo acima fosse feito com False ao invés de 0, ele geraria bytecode no Python 2, mas não no Python 3!

No sentido contrário, as palavras-chave print e exec do Python 2 agora são funções e não podem mais alterar seu ambiente.

No caso do print, não tem problema algum e a sintaxe ficou bem melhor, mas o exec (que normalmente seu uso não é uma boa prática de programação) pode ter um comportamento bem diferente nas duas versões do interpretador.

Essa informação não vai ser encontrada na documentação do Python (apenas alguns avisos que não explicam bem a situação) e geralmente a pessoa vai procurar saber disso quando bater de cara com esse problema e nada funcionar.

Olhe o código abaixo (Não tome como exemplo de boa programação):

a = 1
def f():
    exec("a = 2")
    print(a)

f()
print(a)

No Python 2, a função vai imprimir “2″ e o outro print vai imprimir “1″, como esperado. No Python 3, o número “1″ vai ser impresso nos dois casos.

O que aconteceu?

O meu amigo dis mostra como a variável “a” é chamada no bytecode para impressão dentro da função:

Python 2:

3            8 LOAD_NAME                0 (a)

Python 3:

3           13 LOAD_GLOBAL              2 (a)

Como o Python 3 não consegue ver nenhum a dentro da função, ele assume que é uma palavra definida globalmente e vai embora. No Python 2, é percebida a existência da palavra exec e transforma a busca por um nome sendo primeiro no namespace local e, se não encontrar, no namespace global.

O que, por um lado traz o resultado “desejado”, também faz todos os acessos a funções e variáveis serem mais lento.

Soluções:
1 – Não usar exec.

Praticamente sempre é possível fazer uma solução sem exec que seja simples, mas se for necessário, é preciso tomar cuidado com essa mudança ou passar um segundo parâmetro para a função dizendo em que namespace você quer se o resultado seja avaliado e usar a informação nele.

Normalmente, ninguém usa exec para situações como a que eu mostrei, mas para gerar conteúdo (funções, classes, subclasses, etc) em tempo de execução e muitas vezes existem ferramentas melhores, como as metaclasses e os decoradores.

ZZ no Python

Pra quem não conhece, existe um conjunto de mini-programas para shell muito legal chamado Funções ZZ, criado pelo Aurélio Jargas (@oreio).

[Atualizado!] – Veja as novas funcionalidades no fim

Não sei se alguém já fez, mas criei um wrapper para essas funções em python, para os que já tem o ícone do Python no desktop e não querem abrir um terminal :) .

Disponível em http://dl.dropbox.com/u/6016495/zz.py. Funciona tanto em Python 2 como em Python 3.

Exemplos:

>>> import zz
>>> int(zz.calcula('19+23'))
42
>>> zz.senha()
'k23Civ'
>>> zz.ramones()
'Come back baby, come back'
>>> zz.dolar
09/05/2011 compra   venda   hora
Comercial   1,618   1,620   17:01   +0,18
Paralelo    1,700   1,820   20/2/2011   0,00
Turismo     1,560   1,730   16:16   +0,58

>>> print(zz.loteria('megasena'))
megasena:
   08 - 11 - 14 - 30 - 36 - 38 
   Concurso 1281 (07/05/2011)
   Acumulado em R$ 2.000.000,00 para 11/05/2011

>>> zz.cpf('11111111111')
'CPF válido'
>>> zz.converte('cf', 32)
'32 C = 89.60 F'

[Novo!]
Agora também funciona com Pipes, podendo encadear funções ZZ e usar STDIN!
Mais exemplos:

>>> '1+2+3+4' | zz.calcula
'10'
>>> print('a\nb\nc\nd\na\nb\nc' | zz.uniq)
a
b
c
d
>>> 'oi, beleza?, oi, e ai, oi io, oi' | zz.contapalavra('oi', pipe=True)
'4'

Note que, se quiser ter uma chamada de função junto com o pipe, tem que adicionar o argumento “pipe=True”, senão ele calcula primeiro a resposta e tenta fazer a operação ‘or’ entre as strings. Podia ser esse segundo o comportamento padrão, mas teria que usar sempre um pipe pra pegar o valor final, que não é minha intenção.

A ideia de usar o operador ‘or’ pra fazer ligação entre chamadas de funções eu peguei desse site: http://dev-tricks.net/pipe-infix-syntax-for-python

Se quiser usar, pode colocar diretamente na pasta de módulos do Python, como por exemplo (no Linux):
/usr/local/lib/pythonX.Y/dist-packages

Bônus:
E, pra quem quiser, apresento o modo mais estranho de calcular 2+2+2+2:

>>> int((zz.calcula('2+2')[:-1]+'+2' | zz.calcula(pipe=True))[:-1] + '+2' | zz.calcula)
8

Python e Qt

Esse post não se chama Python + Arduino (Parte 3) porque agora estou usando outro microcontrolador, um Cortex M3.
Aliás, a ideia aqui é mostrar o uso de PyQt, um binding para usar Qt com Python.

Após baixar e instalar, você pode começar a programar interfaces no método convencional ou usar o QtDesigner (ferramenta do Qt) para fazer isso.

Por exemplo, você pode fazer assim:

Abra o QtDesigner e crie um novo projeto. Pode ser Widget ou MainWindow, faz pouca diferença no arquivo gerado.

Coloque seus botões, labels, caixas, etc:

Junte os sinais que não precisam de código. Aqui eu coloquei a caixa e o slider para um atualizar o valor do outro e o checkbox para desativar ou ativar o frame contendo a caixa e o slider.

Agora, você vai salvar um arquivo no formato .ui, que é um XML que vai ser usado para gerar o nosso programa.

Dentro da pasta de instalação do PyQt, que fica dentro do diretório do Python, existe uma ferramenta chamada pyuic4, que faz a conversão. No Linux, você pode rodar o programa diretamente, mas no Windows é uma boa criar um arquivo .bat contendo o comando que vai ser usado para converter o arquivo XML em código Python.

@”C:\Python32\python” “C:\Python32\Lib\site-packages\PyQt4\uic\pyuic.py” controle.ui > form.py

Verifique a pasta de instalação e escolha o nome do arquivo que você salvou e salve isso num arquivo com extensão .bat.
Agora é só dar dois cliques que deve surgir um arquivo chamado form.py, que contem uma classe para modificar uma janela que você tenha criado.

Agora você tem que criar um outro arquivo Python para ser seu programa a ser executado e criar alguns objetos. Existem bons tutoriais de PyQt4 pela internet, então não vou ficar detalhando muito.

Basicamente, você tem que criar um QApplication, uma janela (QWidget ou QMainWindow) e passar essa janela para a função do arquivo form.py que vai criar os botões, etc.

Após isso, você tem que conectar os sinais e os slots dos seus objetos para criar as funcionalidades desejadas. Neste caso, a cada evento no slider ou na caixa, um sinal serial está sendo enviado para o microcontrolador.

Cada slot é um novo método que você deve criar ou então usar de um artifício que são as funções lambda do Python.
Imagem do programa sendo executado:

Abaixo um exemplo de código:
Qualquer dúvida, faça um comentário!


from PyQt4 import QtGui, QtCore
import sys, serial 
import scan, form

class Main():
    def __init__(self):
        self.app = QtGui.QApplication(sys.argv)
        self.janela = QtGui.QMainWindow()
        self.ui = form.Ui_Form()  #Referências para elementos da janela
        self.ui.setupUi(self.janela)
        self.cria_slots()
        self.encontra_portas()
        self.janela.show()

    def cria_slots(self):
        self.ui.bt_conectar.clicked.connect(self.slot_conexao)
        self.ui.box_valor.valueChanged.connect(self.envia_info)
        self.ui.ck_ativar.clicked.connect(self.set_on_off)

    def encontra_portas(self):
        for porta in scan.scan():
            self.ui.box_porta.addItem(porta[0])

    #Criar outros métodos aqui

if __name__ == '__main__':
    prog = Main()
    prog.app.exec_()

Python + Arduino (Parte 2)

Além de plotar gráficos, com o Python é possível criar interfaces para modificar parâmetros do microcontrolador, enviando dados pela porta serial.

Como já visto na Parte 1 deste assunto, é preciso instalar o PySerial além do Python.

Desta vez eu fiz em uns 30 minutos um programa para enviar ao Arduino um número de 0 a 255 (8-bit) que serve para mudar o valor da resistência de um potenciômetro digital que deveria estar ligado à um amplificador de som.

Estou sem o amplificador no momento, então, para visualizar as mudanças coloquei um led para acender com PWM dentro do programa do arduino.

/*
 * Exemplo de leitura serial com o Arduino
 * Colocar isso dentro de um loop do programa
 */

char palavra[10];
unsigned char valor;

if (Serial.available()) {
    delay(100);
    unsigned char n = 0;
    while (Serial.available() > 0) {
        palavra[n++] = (Serial.read());
        //Cuidado com escrita fora do array
    }
    palavra[n] = '\0';

    valor = atoi(palavra);

    /*Usar o 'valor' para fazer alguma coisa, 
      como acender um Led com PWM*/
}

Outro dia coloco vídeo do sistema funcionando.

Python + Arduino

Não, ainda não dá para programar em Python para o ATmega, mas dá para acessar a porta serial pelo Python.

Recentemente descobri a existência do PySerial, um módulo para fazer acesso às portas seriais pelo Python, que tem me ajudado bastante a processar dados adquiridos pelo meu microcontrolador.

A melhor parte é que você pode coletar dados e, com ferramentas como Numpy e Matplotlib fazer diversas transformações e plotar gráficos bem interessantes.

Por exemplo, tenho um display de LCD cuja intensidade de luz é controlada por PWM provido pelo Arduino. A intensidade é escolhida de acordo com um valor lido num divisor resistivo com um resistor de 4.7K e um foto-resistor (LDR) que varia de 0 a 20K.

Eu escrevi um pouco sobre LDR aqui.

Para fazer o PWM, a tensão é lida numa porta analógica e passa por uma função escolhida empiricamente para a intensidade de luz ficar adequada ao ambiente. Sabe aquela coisa de que você tá querendo dormir e tem sempre um led de computador, televisão, aparelho de DVD ou outros aceso? Neste caso, o display fica com um mínimo de luminosidade quando está escuro.

Abaixo um gráfico gerado pelo sensor, usando Python.

Tensão lida(Azul), PWM (Verde) X Horário do dia:

Coloquei no pastebin um pequeno script em Python para coletar dados e salvar num arquivo.

Modo de uso:
1) Primeiro modifique a porta serial para a que você usa. No Linux é “/dev/ttyUSB“, no Windows é “COM“. Escolha também a taxa de transmissão adequada. O meu Arduino fica variando de /dev/ttyUSB0 para /dev/ttyUSB1, por isso tem uma função que busca a porta aberta.

2) Chame o script passando o número de amostras (o microcontrolador deve mandar quebras de linhas entre as amostras) e o nome do arquivo a ser gerado. Exemplo:
     python meu_script 1234 nome_do_arquivo.txt

Para interromper a amostragem, aperte Ctrl+C e os dados serão salvos no arquivo antes de terminar o programa.

Para número indeterminado de amostras, coloque 0. Vai ficar pegando dados até você apertar Ctrl+C.

Programa foi escrito para Python 3, mas a única modificação necessária para Python 2 é retirar a chamada de str() na função de amostragem, pois a aquisição de dados é feito em strings de bytes e não unicode.

Os dados são salvos no arquivo assim:
['123', '546', '789', ... ]

Para obtê-los novamente no Python, faça:
>>> minha_lista = eval( open(‘nome_do_arquivo.txt’).read() )

Se estiver usando valores inteiros, converta a lista:
>>> minha_lista = [int(i) for i in minha_lista]

Caracteres personalizados

Estava vendo um post do Hack a day que mostrava um relógio num display de texto com os caracteres personalizados.

Cada letra do display de texto, ao menos esses com controlador HD44780 ou similar, é uma pequena matriz de 8 linhas por 5 colunas, sendo que você apenas escolhe o carácter e o controlador preenche a matriz.

Para estilizar os caracteres, você precisa gravar novos na memória volátil do controlador do LCD, sendo que ela tem espaço apenas para 8 (0 a 7 da tabela ASCII, não podem ser impressos). Os outros não podem ser modificados.

A ideia aqui é juntar 3 colunas para formar um número grande. Para isso, é preciso criar os caracteres que vão ser usados para desenhar as partes do número, sendo que precisam ser bem genéricos para que caibam nos 8 espaços da memória.

No caso eu usei 5 caracteres. Os três acima e as versões invertidas dos dois últimos. E criei os números como abaixo:

Para criar cada um dos 5 caracteres estilizados, é preciso enviar ao LCD o formato de cada uma das 8 linhas com números de 0 a 31, onde cada bit acende um ponto da linha. Você pode usar notação binária (por exemplo 0b10101) ou colocar o número decimal correspondente, se achar mais fácil.

Com as bibliotecas do Arduino, pode ser feito algo como:

LiquidCrystal lcd(2, 3, 4, 8, 9, 10, 11);
byte meus_chars[][8] = {{31,31,31,31,31,31,31,31}, 
                        {31,31,0,0,0,0,0,0},   
                        {0,0,0,0,0,0,31,31},
                        {31,31,0,0,0,0,0,31},
                        {31,0,0,0,0,0,31,31}};

for (byte i=0; i<5; i++){
    lcd.createChar(i, meus_chars[i]);
}

//Exemplo de exibição do número 0
lcd.clear();
lcd << '\0' << '\1' << '\0';
lcd.setCursor(0,1);
lcd << '\0' << '\2' << '\0';

Falando nisso, baixe a biblioteca Streaming que faz overload do << para imprimir coisas no LCD ou na Serial.

Abaixo um vídeo dos números no display:
Meu display 16×2 ainda não chegou. Enquanto isso, continuo com esse 8×2.

Projeto com display LCD

Com o advento do Arduino, fazer sistemas caseiros de controle ficou bem mais fácil, pois a IDE desse microcontrolador traz diversas bibliotecas que precisam de pouca configuração para acessar outros hardwares.

No final de semana eu fiz um pequeno projeto onde a hora e temperatura eram apresentadas num display LCD de texto com espaço para 8 caracteres em cada uma das suas duas linhas.

Para medir a temperatura eu usei um termômetro digital da Dallas Semiconductor, o DS18B20, que tem precisão de 0.0625 grau e já vem calibrado de fábrica.

Esse termômetro é um hardware bastante interessante, pois usa um protocolo chamado 1-Wire que permite diversos aparelhos conectados num fio e pode ser alimentado de forma parasita (pelo próprio fio de comunicação). Inclusive eu testei com dois termômetros conectados no mesmo fio e ambos apresentavam sempre a mesma temperatura (com uma diferença de no máximo 0.1 grau).

Basicamente, eles funcionam com um endereçamento de 64 bits, fazendo com que não exista confusão na comunicação. Essa forma de utilização é conhecida como microlan.

Para ajustar o horário, a solução mais simples foi mandar um comando serial para o microcontrolador, já que ele está conectado à USB e não tinha botões para colocar.

Abaixo deixo um vídeo onde eu seguro o termômetro e a temperatura (que já estava alta pelo calor do verão) aumenta um pouco mais.

Esse relógio-termômetro é parte de um projeto bem maior que estou fazendo para uma matéria da faculdade. Em breve mais coisas vão aparecer aqui.

Seguir

Obtenha todo post novo entregue na sua caixa de entrada.