Pular para o conteúdo

58 · IoT e Arduino

10 exercícios: .

bash
python3 exercicios/run_all.py 58

Os exercícios#

#TítuloEnunciado
388a primeira placaacenda um LED. Sem placa nenhuma: 'conectar_simulada'
389o erro numero 1 do Firmataleia um sensor. A placa NAO responde perguntas sobre o
390do numero cru ao valorum pino analogico devolve 0 a 1023. Ele nao e
391o rele que nao bateuma leitura pura treme (512, 509, 514, 511). Num
392o meio-termo que nao existeum pino digital so tem dois valores. "Meio brilho" e o
393quem conhece a placa e a placacada placa tem um mapa diferente — quantos pinos
394dois fios, muitos sensoresI2C liga varios dispositivos em dois fios (SDA e SCL),
395o acento que quebra o protocolono Firmata todo byte de dado viaja PARTIDO em dois de
396quando o programa precisa rodar NA placaFirmata cobre o prototipo. O que precisa rodar sem
397le, decide, ageuma estufa e o projeto de IoT completo mais simples

388 · a primeira placa#

Enunciado. acenda um LED. Sem placa nenhuma: 'conectar_simulada'

exercicios/58-iot/388_a_primeira_placa.df
// poe do outro lado do cabo um simulador que fala o MESMO Firmata,
// com o mapa de pinos de uma placa de verdade.

adopt Arcane.IoT as IoT

out "== 1. conectar =="

placa := IoT.conectar_simulada("uno")
info := placa.info()
assert info["firmware"]["nome"] is "StandardFirmata.ino"
assert info["protocolo"] is "2.5"
assert info["pinos"] is 20
assert info["analogicos"] is 6
out $"   {info['pinos']} pinos, {info['analogicos']} canais analogicos"

out ""
out "== 2. todo pino tem um modo, e ele e declarado =="

// Sem o modo, a placa nao sabe se o pino le ou escreve.
placa.modo(13, "saida")
placa.escrever(13, yes)
assert placa.simulador.pino(13) is 1
placa.escrever(13, no)
assert placa.simulador.pino(13) is 0
out "   o LED acendeu e apagou"

out ""
out "== 3. piscar =="

cycle i from 1 to 3:
    placa.escrever(13, yes)
    sleep(20)
    placa.escrever(13, no)
    sleep(20)
out "   tres piscadas"

out ""
out "== 4. fechar DESLIGA as saidas =="

placa.escrever(13, yes)
assert placa.simulador.pino(13) is 1
placa.fechar()
assert placa.simulador.pino(13) is 0

// Terminar deixando um rele ligado e o defeito mais caro desta area:
// o resto do sistema continua, sem ninguem olhando.
out "   e fechar de novo nao e erro:"
assert placa.fechar() is no

out ""
out "== 5. depois de fechada, ela recusa =="

monitor:
    placa.escrever(13, yes)
    assert no
handle Error as e:
    out $"   {e.message}"

out "exercicio 388 ok"

Acender um LED é o "olá mundo" do hardware. Aqui ele roda sem placa: IoT.conectar_simulada põe do outro lado do cabo um simulador que fala o mesmo Firmata, com o mapa de pinos de uma placa de verdade.

Não é um dublê da API#

O simulador recebe os bytes do protocolo e responde com os bytes que a placa responderia. O parser, a máquina de estados e a partição em sete bits são exercitados — que é onde os erros moram. Um dublê de escrever(pino, valor) não provaria nada disso.

`fechar` desliga as saídas#

Um programa que termina deixando um relé ligado é o defeito mais caro desta área: o resto do sistema continua, sem ninguém olhando. E fechar é idempotente — chamá-lo num defer e de novo no fim não é erro.

Para experimentar#

Troque "uno" por "mega" e confira info()["pinos"]: são 70.

389 · o erro numero 1 do Firmata#

Enunciado. leia um sensor. A placa NAO responde perguntas sobre o

exercicios/58-iot/389_o_relatorio_analogico.df
// analogico — ela envia sozinha, de tempos em tempos, os canais que
// voce mandou relatar. Sem isso, o pino esta certo, o sensor esta
// certo, e a leitura e zero. Sem nenhum erro.

adopt Arcane.IoT as IoT

placa := IoT.conectar_simulada("uno")

out "== 1. o modo nao basta =="

placa.modo(14, "analogico")                 // o A0 e o pino 14
placa.simulador.definir_analogico(0, 900)   // o sensor "vale" 900
sleep(120)
assert placa.analogico(0) is 0
out "   modo declarado, sensor em 900, leitura 0 — e nada acusa"

out ""
out "== 2. e esta a linha que falta =="

placa.relatar_analogico(0)
sleep(150)
assert placa.analogico(0) is 900
out $"   agora chega: {placa.analogico(0)}"

out ""
out "== 3. a amostragem e uma troca =="

// Baixa demais enche a serial e ATRASA as ordens que voce manda;
// alta demais faz o grafico perder detalhe.
assert placa.amostragem(20) is 20
placa.simulador.definir_analogico(0, 512)
sleep(120)
assert placa.analogico(0) is 512

monitor:
    placa.amostragem(1)
    assert no
handle Error as e:
    out $"   {e.message}"

out ""
out "== 4. varios canais ao mesmo tempo =="

cycle canal from 1 to 3:
    placa.modo(14 + canal, "analogico")
    placa.relatar_analogico(canal)
    placa.simulador.definir_analogico(canal, 100 * canal)

sleep(200)
leituras := [placa.analogico(c) cycle c in range(1, 4)]
assert leituras is [100, 200, 300]
out $"   {leituras}"

out ""
out "== 5. o digital e por MUDANCA =="

// Ele nao vem periodicamente: a placa avisa quando o pino muda.
vistos := []
placa.observar(lambda e: vistos.append(e["tipo"]))
placa.modo(2, "entrada_pullup")
placa.simulador.definir_digital(2, 1)
sleep(120)
assert placa.ler(2) is yes
placa.simulador.definir_digital(2, 0)
sleep(120)
assert "digital" in vistos
out "   o observador viu a mudanca"

placa.fechar()
out "exercicio 389 ok"

A placa não responde perguntas sobre o analógico: ela envia sozinha, de tempos em tempos, os canais que você mandou relatar. Sem relatar_analogico(canal), o pino está certo, o sensor está certo, e a leitura é zero — sem nenhum erro.

É o defeito que mais custa tempo a quem começa, porque não há nada para depurar: o programa está correto do ponto de vista de qualquer leitura.

A amostragem é uma troca#

Muito baixaMuito alta
enche a serial e atrasa as ordens que você mandao gráfico perde detalhe
o LED demora a responder ao cliqueum botão lido por relatório parece que "não pegou"

Para temperatura, 500 ms é generoso. Para um potenciômetro que move um servo, 20 ms.

O digital é por mudança#

Ele não vem periodicamente: a placa avisa quando o pino muda de estado. placa.observar(acao) recebe esses eventos; placa.ler(pino) devolve o último valor relatado, e não uma pergunta ao vivo.

390 · do numero cru ao valor#

Enunciado. um pino analogico devolve 0 a 1023. Ele nao e

exercicios/58-iot/390_escala_e_tensao.df
// temperatura, nem luz: e o quanto da tensao de referencia chegou
// ali. Transformar isso em grandeza e a parte que todo projeto
// refaz — e onde os mesmos erros acontecem.

adopt Arcane.IoT as IoT

out "== 1. escala LIMITA por padrao =="

meio := IoT.escala(512, 0, 1023, 0, 100)
assert meio > 50.0 and meio < 50.1

// O 'map()' do Arduino NAO limita, e ruido faz um ADC de 10 bits
// devolver 1024 de vez em quando: no painel isso vira 101%.
assert IoT.escala(2000, 0, 1023, 0, 100) is 100
assert IoT.escala(0 - 5, 0, 1023, 0, 100) is 0
out "   fora da faixa, ela para nas pontas"

// E quem QUER extrapolar pede.
assert IoT.escala(2000, 0, 1023, 0, 100, no) > 100
out "   com limitar := no, ela extrapola"

out ""
out "== 2. uma faixa de largura zero e recusada =="

monitor:
    IoT.escala(1, 5, 5, 0, 10)
    assert no
handle Error as e:
    out $"   {e.message}"

out ""
out "== 3. a tensao depende da PLACA =="

assert IoT.tensao(1023) is 5.0                      // UNO: 10 bits, 5 V
assert round(IoT.tensao(4095, 3.3, 12), 2) is 3.3   // ESP32: 12 bits
assert round(IoT.tensao(2048, 3.3, 12), 2) is 1.65
out "   a mesma leitura em duas placas e duas tensoes"

out ""
out "== 4. TMP36: uma reta, com um deslocamento =="

// A folha de dados: 750 mV a 25 graus, 10 mV por grau. O
// deslocamento de 500 mV e o que quase todo mundo esquece.
leitura := 0.75 / 5.0 * 1023
t := IoT.tmp36(leitura)
assert t > 24.5 and t < 25.5
out $"   {round(t, 1)} graus"

out ""
out "== 5. NTC: e a ponta da escala nao e temperatura =="

// Com o termistor na resistencia nominal, o divisor le a metade.
assert IoT.ntc(1023 / 2) > 24.7 and IoT.ntc(1023 / 2) < 25.3

// Em 0 ou 1023 a equacao divide por zero ou tira log de zero, e o
// que sai e um numero absurdo com cara de temperatura. Um
// termostato que ve -273 liga o aquecedor e nao desliga mais.
cycle ponta in [0, 1023]:
    monitor:
        IoT.ntc(ponta)
        assert no
    handle Error as e:
        out $"   {ponta}: {e.message}"

out ""
out "== 6. divisor: quando o sensor e uma resistencia =="

// LDR, umidade de solo, potenciometro: tudo vira resistencia.
r := IoT.divisor(1023 / 2)
assert r > 9900 and r < 10100
out $"   {round(r)} ohms"

out "exercicio 390 ok"

Um pino analógico devolve 0 a 1023. Ele não é temperatura, nem luz: é o quanto da tensão de referência chegou ali. Transformar isso em grandeza é a parte que todo projeto refaz — e onde os mesmos erros acontecem.

`escala` limita por padrão#

O map() do Arduino não limita, e ruído faz um ADC de 10 bits devolver 1024 de vez em quando: no painel isso vira 101%, e num controle de motor vira um valor fora da faixa do PWM. Aqui o padrão é limitar, e limitar := no é escolha explícita de quem quer extrapolar.

A tensão depende da placa#

PlacaBitsReferência
UNO, Nano, Mega10 (0–1023)5 V
ESP3212 (0–4095)3,3 V

A mesma leitura nas duas é uma tensão diferente — e ligar um sensor de 5 V direto num ESP32 costuma matar o pino.

A ponta da escala não é temperatura#

Com a leitura em 0 ou 1023, a equação do termistor divide por zero ou tira log de zero, e o que sai é um número absurdo com cara de temperatura. Um termostato que vê −273 °C liga o aquecedor e não desliga mais. Por isso ntc recusa as pontas: ali não é frio nem calor, é fio solto ou curto.

391 · o rele que nao bate#

Enunciado. uma leitura pura treme (512, 509, 514, 511). Num

exercicios/58-iot/391_media_e_histerese.df
// grafico isso e grama; num controle, e o rele chaveando dezenas de
// vezes por minuto — e e assim que um contato se queima.

adopt Arcane.IoT as IoT

out "== 1. a media movel ESQUECE =="

media := IoT.media_movel(3)
assert media(10) is 10
assert media(20) is 15
assert media(30) is 20
assert media(40) is 30              // o 10 saiu da janela
out "   janela de 3: a quarta leitura empurra a primeira para fora"

out ""
out "== 2. uma soleira so faz o rele bater =="

// Ligar acima de 30 e desligar abaixo de 30: com a temperatura EM
// 30, cada tremor de leitura e um chaveamento.
simples := lambda v: v bigger 30
batidas := 0
anterior := no
cycle v in [29.9, 30.1, 29.8, 30.2, 29.9, 30.1]:
    atual := simples(v)
    given atual is not anterior:
        batidas += 1
    anterior := atual
assert batidas is 5
out $"   seis leituras em volta de 30, {batidas} chaveamentos"

out ""
out "== 3. com DUAS soleiras, nenhum =="

ventoinha := IoT.histerese(30, 28)
batidas := 0
anterior := no
cycle v in [29.9, 30.1, 29.8, 30.2, 29.9, 30.1]:
    atual := ventoinha(v)
    given atual is not anterior:
        batidas += 1
    anterior := atual
assert batidas is 1
out $"   as mesmas leituras, {batidas} chaveamento"

out ""
out "== 4. ela SEGURA o estado no meio =="

termostato := IoT.histerese(30, 28)
assert termostato(29) is no         // subindo, ainda nao
assert termostato(31) is yes
assert termostato(29) is yes        // no meio, segura
assert termostato(27) is no
out "   liga em 30, so desliga em 28"

out ""
out "== 5. invertendo, ela liga ABAIXO =="

// E o umidificador, o aquecedor, a bomba.
umidificador := IoT.histerese(40, 50)
assert umidificador(45) is no
assert umidificador(35) is yes
assert umidificador(45) is yes
assert umidificador(55) is no
out "   liga abaixo de 40, so desliga em 50"

out ""
out "== 6. duas soleiras iguais sao UMA =="

monitor:
    IoT.histerese(30, 30)
    assert no
handle Error as e:
    out $"   {e.message}"

out "exercicio 391 ok"

Uma leitura pura treme: 512, 509, 514, 511. Num gráfico isso é grama; num controle, é o relé chaveando dezenas de vezes por minuto — e é assim que um contato se queima.

Média móvel: a resposta mais barata#

Ela esquece: numa janela de 3, a quarta leitura empurra a primeira para fora. Janela grande suaviza mais e responde mais devagar; é uma troca, e não uma melhoria.

Histerese: duas soleiras, não uma#

Ligar acima de 30 e desligar abaixo de 30 faz o relé chavear a cada tremor quando a temperatura fica em 30. Medido no exercício: as mesmas seis leituras dão 5 chaveamentos com uma soleira e 1 com duas.

No meio da faixa ela segura o estado — e é isso que a torna um controle, e não um comparador.

Invertendo, ela liga abaixo#

IoT.histerese(40, 50) liga abaixo de 40 e só desliga em 50: é o umidificador, o aquecedor, a bomba. Duas soleiras iguais são recusadas, porque isso é uma soleira só com outro nome.

392 · o meio-termo que nao existe#

Enunciado. um pino digital so tem dois valores. "Meio brilho" e o

exercicios/58-iot/392_pwm_e_servo.df
// pino ligando e desligando rapido o bastante para o olho — ou o
// motor — nao notar. E o PWM.

adopt Arcane.IoT as IoT

placa := IoT.conectar_simulada("uno")

out "== 1. o modo e DECLARADO, nunca adivinhado =="

// Declarar por conta propria seria conveniente e errado: um pino em
// 'saida' recebendo PWM acende no talo, e quem le o programa nao
// veria onde o modo mudou.
monitor:
    placa.pwm(9, 128)
    assert no
handle Error as e:
    out $"   {e.message}"

placa.modo(9, "pwm")
assert placa.pwm(9, 128) is 128
out "   com o modo declarado, passa"

out ""
out "== 2. a faixa e 0 a 255 =="

cycle brilho in [0, 64, 128, 192, 255]:
    placa.pwm(9, brilho)
assert placa.simulador.pino(9) is 255

monitor:
    placa.pwm(9, 300)
    assert no
handle Error as e:
    out $"   {e.message}"

out ""
out "== 3. e so alguns pinos fazem PWM =="

// Num UNO: 3, 5, 6, 9, 10 e 11. E quem diz isso e a PLACA.
assert "pwm" in placa.capacidades(9)
assert not ("pwm" in placa.capacidades(13))

monitor:
    placa.modo(13, "pwm")
    assert no
handle Error as e:
    out $"   {e.message}"
    out $"   {e.nota}"

out ""
out "== 4. servo: posicao, nao velocidade =="

placa.modo(3, "servo")
cycle graus in [0, 90, 180, 90]:
    placa.servo(3, graus)
assert placa.simulador.servo(3) is 90

monitor:
    placa.servo(3, 200)
    assert no
handle Error as e:
    out $"   {e.message}"

out ""
out "== 5. cada servo tem o seu fim de curso =="

// Quando o braco treme numa ponta, e isto.
assert placa.configurar_servo(3, 600, 2300) is "servo"
placa.servo(3, 0)
assert placa.simulador.servo(3) is 0
out "   os microssegundos das duas pontas, ajustados"

placa.fechar()
out "exercicio 392 ok"

Um pino digital só tem dois valores. "Meio brilho" é o pino ligando e desligando rápido o bastante para o olho — ou o motor — não notar. É o PWM, de 0 a 255.

O modo é declarado, nunca adivinhado#

Declarar por conta própria seria conveniente e errado: um pino em saida recebendo PWM acende no talo, e quem lê o programa não veria onde o modo mudou. Por isso pwm e servo exigem o modo já declarado.

Quem diz o que o pino faz é a placa#

Num UNO, só 3, 5, 6, 9, 10 e 11 fazem PWM — e essa lista vem da resposta de capacidade da placa, não de uma tabela escrita na linguagem. Uma tabela envelheceria na primeira placa nova, e UNO, Mega e ESP32 têm mapas diferentes.

Servo é posição#

servo(3, 90) não é "gire até 90"; é "fique em 90". E cada servo tem os seus microssegundos de fim de curso: quando o braço treme numa ponta, é configurar_servo que resolve.

393 · quem conhece a placa e a placa#

Enunciado. cada placa tem um mapa diferente — quantos pinos

exercicios/58-iot/393_as_capacidades.df
// digitais, quais fazem PWM, quantos canais analogicos. Escrever
// esse mapa na linguagem seria errar na primeira placa nova.

adopt Arcane.IoT as IoT

out "== 1. os modelos que o simulador conhece =="

m := IoT.modelos()
assert m["uno"]["digitais"] is 14 and m["uno"]["analogicos"] is 6
assert m["mega"]["digitais"] is 54
assert m["esp32"]["tensao"] is 3.3
out $"   {len(m)} modelos: {sorted(keys(m))}"

out ""
out "== 2. o PWM nao esta em todo pino =="

assert 11 in m["uno"]["pwm"]
assert not (4 in m["uno"]["pwm"])
out $"   numa UNO: {m['uno']['pwm']}"

out ""
out "== 3. e a PLACA confirma, pelo protocolo =="

uno := IoT.conectar_simulada("uno")
assert "pwm" in uno.capacidades(9)
assert "servo" in uno.capacidades(9)
assert not ("pwm" in uno.capacidades(13))
assert "saida" in uno.capacidades(13)

// Um pino que nao existe e outro erro, com outra mensagem.
monitor:
    uno.modo(99, "saida")
    assert no
handle Error as e:
    out $"   {e.message}"
    out $"   {e.nota}"

out ""
out "== 4. uma Mega tem mais, e o Firmata os ve =="

mega := IoT.conectar_simulada("mega")
assert mega.info()["pinos"] is 70
mega.modo(53, "saida")                  // um pino que a UNO nao tem
mega.escrever(53, yes)
assert mega.simulador.pino(53) is 1
out "   o pino 53 respondeu"

monitor:
    uno.modo(53, "saida")               // e na UNO, nao
    assert no
handle Error as e:
    out $"   na UNO: {e.message}"

out ""
out "== 5. um modelo que nao existe e recusado com a lista =="

monitor:
    IoT.simulador("commodore-64")
    assert no
handle Error as e:
    out $"   {e.message}"

uno.fechar()
mega.fechar()
out "exercicio 393 ok"

Cada placa tem um mapa diferente: quantos pinos digitais, quais fazem PWM, quantos canais analógicos, qual a tensão e quantos bits o conversor tem. Escrever esse mapa na linguagem seria errar na primeira placa nova.

Com a placa ligada, quem responde é ela — pela resposta de capacidade do Firmata, que o cliente pede logo depois de conectar.

Três números que mudam com a placa#

UNO, Nano, MegaESP32
tensão5 V3,3 V
conversor10 bits (0–1023)12 bits (0–4095)
FQBNarduino:avr:unoesp32:esp32:esp32

Ligar um sensor de 5 V direto num ESP32 costuma matar o pino — e a leitura dele vai a 4095, não a 1023. Os dois erros aparecem como número errado, não como falha.

Duas mensagens diferentes#

Um pino que não existe e um pino que não faz aquilo são erros distintos, e a mensagem de cada um diz o que fazer: a primeira mostra a faixa da placa, a segunda lista o que aquele pino aceita.

394 · dois fios, muitos sensores#

Enunciado. I2C liga varios dispositivos em dois fios (SDA e SCL),

exercicios/58-iot/394_i2c.df
// cada um com um endereco. E como quase todo sensor moderno
// conversa: acelerometro, barometro, RTC, display.

adopt Arcane.IoT as IoT

placa := IoT.conectar_simulada("uno")
placa.i2c_configurar()

out "== 1. ler um registro =="

// O MPU6050 em 0x68, registro 0x3B: seis bytes de aceleracao.
placa.simulador.definir_i2c(0x68, 0x3B, [0, 100, 255, 200, 1, 44])
dados := placa.i2c_ler(0x68, 0x3B, 6, 2)
assert len(dados) is 6
assert dados is [0, 100, 255, 200, 1, 44]

// Os valores vem em dois bytes: alto primeiro.
x := dados[0] * 256 + dados[1]
assert x is 100
out $"   aceleracao X (cru): {x}"

out ""
out "== 2. silencio devolve void, e NAO levanta =="

// Endereco errado, fio solto ou sensor sem alimentacao dao o mesmo
// resultado: nada. Levantar aqui obrigaria um 'monitor' em volta de
// cada leitura de um laco que roda mil vezes.
resposta := placa.i2c_ler(0x77, 0xD0, 1, 0.3)
assert resposta is void
out "   ninguem em 0x77 — e o programa decide o que fazer"

given resposta is void:
    out "   confira SDA, SCL, o 3,3 V e os resistores de pull-up"

out ""
out "== 3. escrever num registro =="

// Quase todo sensor nasce em modo de repouso: acordar e a primeira
// escrita de qualquer driver.
assert placa.i2c_escrever(0x68, [0x6B, 0x00]) is 2
out "   dois bytes: o registro e o valor"

out ""
out "== 4. dois dispositivos no MESMO par de fios =="

placa.simulador.definir_i2c(0x76, 0xF7, [80, 0, 0])     // BMP280
placa.simulador.definir_i2c(0x68, 0x75, [104])          // WHO_AM_I

assert placa.i2c_ler(0x76, 0xF7, 3, 2) is [80, 0, 0]
assert placa.i2c_ler(0x68, 0x75, 1, 2) is [104]
out "   o endereco e o que os separa"

out ""
out "== 5. o endereco da folha de dados as vezes vem DESLOCADO =="

// 0xD0 e 0x68 escrito com o bit de leitura/escrita junto. Se o
// sensor nao responde em nenhum endereco, tente a metade.
assert 0xD0 ~/ 2 is 0x68
out "   0xD0 / 2 = 0x68"

placa.fechar()
out "exercicio 394 ok"

I²C liga vários dispositivos em dois fios (SDA e SCL), cada um com um endereço. É como quase todo sensor moderno conversa: acelerômetro, barômetro, RTC, display OLED, expansor de portas.

O silêncio é uma resposta#

Endereço errado, fio solto ou sensor sem alimentação dão o mesmo resultado no barramento: nada. i2c_ler devolve void em vez de levantar — levantar obrigaria um monitor em volta de cada leitura de um laço que roda mil vezes.

E o simulador fica calado de verdade num endereço sem dispositivo. Devolver zeros ali faria ele ensinar o contrário do que acontece com um fio solto: um sensor "presente" medindo nada.

Os três erros de I²C, em ordem de frequência#

SintomaQuase sempre é
nada responde, em nenhum endereçofaltam os pull-ups de 4,7 kΩ (ou há dois módulos com eles)
responde, mas os valores não fazem sentidoo sensor não foi acordado — quase todos nascem em repouso
funciona perto e falha com fio longoI²C é de placa, não de instalação

O endereço deslocado#

0xD0 é 0x68 escrito com o bit de leitura/escrita junto. Se o sensor não responde em nenhum endereço, tente a metade.

395 · o acento que quebra o protocolo#

Enunciado. no Firmata todo byte de dado viaja PARTIDO em dois de

exercicios/58-iot/395_texto_e_sete_bits.df
// sete bits — o oitavo marca comando. Ler so o primeiro de cada par
// devolve a metade baixa: em ASCII ninguem nota, porque ali o bit 7
// e zero. Num acento, "ola" vira "olC!".

adopt Arcane.IoT as IoT

placa := IoT.conectar_simulada("uno")

out "== 1. o sketch fala com Firmata.sendString =="

placa.simulador.mandar_texto("calibracao concluida")
sleep(120)
assert placa.textos()[0] is "calibracao concluida"
out $"   {placa.textos()[0]}"

out ""
out "== 2. e com acento tambem =="

placa.simulador.mandar_texto("temperatura: 21,5 graus — leitura ok")
sleep(120)
assert "—" in placa.textos()[1]
assert "graus" in placa.textos()[1]
out $"   {placa.textos()[1]}"

texto := "ola, ceu de acucar: ção, ã, ê, ü"
placa.simulador.mandar_texto(texto)
sleep(120)
assert placa.textos()[2] is texto
out $"   {placa.textos()[2]}"

out ""
out "== 3. por que isso quebra =="

// 'ç' em UTF-8 sao dois bytes, e cada um deles tem o bit 7 LIGADO.
// Partir em sete bits e obrigatorio; esquecer de juntar de volta
// devolve a metade baixa, e o defeito fica escondido ate a primeira
// mensagem em portugues.
bytes_de_c := len("ç")
assert bytes_de_c is 1               // um caractere...
out "   um caractere, dois bytes no fio, quatro no Firmata"

out ""
out "== 4. o observador tambem ve o texto =="

vistos := []
placa.observar(lambda e: vistos.append(e))
placa.simulador.mandar_texto("pronto")
sleep(120)
tipos := [e["tipo"] cycle e in vistos]
assert "texto" in tipos
out "   chega como evento, e nao so na lista"

out ""
out "== 5. e por que isso e o 'println' de quem usa Firmata =="

// Depurar um sketch e escrever na serial. Com o Firmata ocupando a
// porta, 'Serial.println' estragaria o protocolo — a mensagem tem
// de vir DENTRO dele.
assert len(placa.textos()) is 4
out $"   {len(placa.textos())} mensagens, e o protocolo intacto"

placa.fechar()
out "exercicio 395 ok"

No Firmata todo byte de dado viaja partido em dois de sete bits: o oitavo é reservado para marcar comando. Ler só o primeiro de cada par devolve a metade baixa de cada byte.

Em ASCII ninguém nota, porque ali o bit 7 é zero. Num acento, "olá" vira "olC!" — e o defeito fica escondido até a primeira mensagem em português. Foi exatamente assim que ele apareceu neste repositório.

Por que `ç` é o caso#

Em UTF-8, ç são dois bytes, e cada um deles tem o bit 7 ligado. No fio do Firmata viram quatro. Partir é obrigatório; esquecer de juntar de volta é o bug.

`sendString` é o `println` de quem usa Firmata#

Com o Firmata ocupando a porta, um Serial.println do sketch estragaria o protocolo: a mensagem tem de vir dentro dele. placa.textos() devolve o que chegou, e placa.observar entrega como evento.

396 · quando o programa precisa rodar NA placa#

Enunciado. Firmata cobre o prototipo. O que precisa rodar sem

exercicios/58-iot/396_sketch_gerado.df
// computador — um sensor a bateria, um controle com resposta em
// microssegundos — e sketch: C++ na placa. A linguagem escreve o
// sketch, e o arduino-cli compila e grava.

adopt Arcane.IoT as IoT
adopt Arcane.OS as OS
adopt Arcane.IO as IO

out "== 1. os modelos que existem =="

modelos := IoT.sketches()
assert len(modelos) >= 6
cycle nome in sorted(keys(modelos)):
    out $"   {nome}: {modelos[nome]['descricao']}"

out ""
out "== 2. todo modelo gera C++ valido =="

cycle nome in keys(modelos):
    codigo := IoT.sketch(nome)
    assert "void setup()" in codigo
    assert "void loop()" in codigo
    assert not ("{led}" in codigo)      // nada por substituir
out "   setup, loop, e nenhum marcador esquecido"

out ""
out "== 3. as opcoes entram no codigo =="

codigo := IoT.sketch("pisca", {"led": 7, "intervalo": 120})
assert "const int LED = 7;" in codigo
assert "INTERVALO = 120" in codigo
out "   o LED e o intervalo foram para dentro do .ino"

out ""
out "== 4. uma opcao que nao existe e RECUSADA =="

// Um '--lde=7' que passa calado grava o sketch com o LED errado, e
// a pessoa vai procurar o defeito no fio.
monitor:
    IoT.sketch("pisca", {"lde": 7})
    assert no
handle Error as e:
    out $"   {e.message}"

out ""
out "== 5. o arquivo tem o NOME DA PASTA =="

// Um .ino solto nao compila, e o erro do arduino-cli nao diz por que.
pasta := $"{OS.temp_dir()}/df-396-{randint(100000, 999999)}"
caminho := IoT.gravar_sketch(pasta, "sensor", {"canal": 0, "velocidade": 9600})
assert "sensor/sensor.ino" in caminho
assert "Serial.begin(9600)" in IO.read(caminho)
out "   pasta 'sensor', arquivo 'sensor.ino'"
IO.remove_tree(pasta)

out ""
out "== 6. o firmata gerado fala a velocidade certa =="

// 57600 e a do StandardFirmata; 9600 nao conversa com ele.
assert "Firmata.begin(57600)" in IoT.sketch("firmata")
out "   57600, que e a que IoT.conectar usa"

out ""
out "== 7. compilar e gravar chamam o arduino-cli =="

// Reimplementar isso seria refazer o GCC e o avrdude. O projeto
// prefere dizer que depende dele a fingir que nao.
given IoT.tem_arduino_cli():
    out "   ele esta instalado: da para compilar e gravar daqui"
otherwise:
    out "   nao esta instalado — 'brew install arduino-cli'"

out "exercicio 396 ok"

Firmata cobre o protótipo. O que precisa rodar sem computador — um sensor a bateria, um controle com resposta em microssegundos — é sketch: C++ na placa. A linguagem escreve o sketch, e o arduino-cli compila e grava.

O arquivo tem o nome da pasta#

Um .ino solto não compila, e o erro do arduino-cli não diz por quê. gravar_sketch("/tmp/fw", "sensor") cria /tmp/fw/sensor/sensor.ino — a pasta e o arquivo com o mesmo nome, que é o que o Arduino exige.

Uma opção que não existe é recusada#

opcoes.ler de novo: um --lde=7 que passasse calado gravaria o sketch com o LED errado, e a pessoa iria procurar o defeito no fio.

57600, e não 9600#

É a velocidade do StandardFirmata. Um sketch de Firmata com Serial.begin(9600) compila, grava, e não conversa com ninguém.

O que a linguagem não faz#

IoT.compilar e IoT.carregar chamam o arduino-cli. Compilar C++ para AVR, resolver bibliotecas e falar com o bootloader é o que ele faz, e bem; reimplementar isso seria refazer o GCC e o avrdude. O projeto prefere dizer que depende dele a fingir que não.

397 · le, decide, age#

Enunciado. uma estufa e o projeto de IoT completo mais simples

exercicios/58-iot/397_a_estufa.df
// que existe. Ela LE (temperatura e umidade de solo), DECIDE (com
// histerese) e AGE (aquecedor e bomba). Todo problema da area
// aparece nela.

adopt Arcane.IoT as IoT

steady TEMPERATURA := 0        // A0 — TMP36
steady SOLO := 1               // A1 — sensor resistivo
steady AQUECEDOR := 7          // rele
steady BOMBA := 8              // rele

record Estado:
    graus: Float
    umidade: Float
    aquecendo: Boolean
    regando: Boolean

action abrir(modelo):
    p := IoT.conectar_simulada(modelo)
    p.modo(AQUECEDOR, "saida")
    p.modo(BOMBA, "saida")
    p.modo(14 + TEMPERATURA, "analogico")
    p.modo(14 + SOLO, "analogico")
    p.relatar_analogico(TEMPERATURA)
    p.relatar_analogico(SOLO)
    p.amostragem(50)
    yield p

// A decisao mora FORA do laco da placa: sem isso nao da para testar
// um controle sem hardware.
action ciclo(placa, suavizar, aquecedor, bomba):
    graus := IoT.tmp36(suavizar(placa.analogico(TEMPERATURA)))
    umidade := IoT.escala(placa.analogico(SOLO), 0, 1023, 0, 100)
    aquecendo := aquecedor(graus)
    regando := bomba(umidade)
    placa.escrever(AQUECEDOR, aquecendo)
    placa.escrever(BOMBA, regando)
    yield Estado(round(graus, 1), round(umidade, 1), aquecendo, regando)

out "== 1. frio e seco: os dois ligam =="

placa := abrir("uno")
defer:
    placa.fechar()              // os dois reles desligam ao sair

suavizar := IoT.media_movel(5)
aquecedor := IoT.histerese(18, 21)      // liga abaixo de 18
bomba := IoT.histerese(30, 45)          // liga abaixo de 30% de umidade

placa.simulador.definir_analogico(TEMPERATURA, 123)   // ~10 graus
placa.simulador.definir_analogico(SOLO, 200)          // ~20%
sleep(150)

estado := ciclo(placa, suavizar, aquecedor, bomba)
assert estado.aquecendo is yes and estado.regando is yes
assert placa.simulador.pino(AQUECEDOR) is 1
out $"   {estado.graus} graus, solo {estado.umidade}% — aquecedor e bomba ligados"

out ""
out "== 2. aqueceu e molhou: os dois desligam =="

placa.simulador.definir_analogico(TEMPERATURA, 400)   // ~72 graus
placa.simulador.definir_analogico(SOLO, 700)          // ~68%
sleep(150)
cycle i from 1 to 5:                                   // a janela da media
    estado := ciclo(placa, suavizar, aquecedor, bomba)
    sleep(20)
assert estado.aquecendo is no and estado.regando is no
assert placa.simulador.pino(BOMBA) is 0
out $"   {estado.graus} graus, solo {estado.umidade}% — os dois desligados"

out ""
out "== 3. no MEIO da faixa, a histerese segura =="

placa.simulador.definir_analogico(SOLO, 400)          // ~39%: entre 30 e 45
sleep(120)
cycle i from 1 to 3:
    estado := ciclo(placa, suavizar, aquecedor, bomba)
assert estado.regando is no                            // estava desligada
out "   entre as duas soleiras, ela mantem o estado"

out ""
out "== 4. o record evita a chave com erro de digitacao =="

// Num vault solto, estado["aquecndo"] seria void — e void e falso.
monitor:
    out estado.aquecndo
    assert no
handle Error as e:
    out $"   {e.message}"

out ""
out "== 5. o teto duro que a histerese NAO da =="

// Histerese nao protege de um sensor que soltou do vaso: a leitura
// fica em "seco" para sempre, e a bomba nao desliga nunca.
action limitador(maximo_ms):
    gasto := [0]
    action pode(ligar, decorrido):
        given not ligar:
            yield no
        given gasto[0] + decorrido bigger maximo_ms:
            yield no
        gasto[0] := gasto[0] + decorrido
        yield yes
    yield pode

pode := limitador(30000)
assert pode(yes, 10000) is yes
assert pode(yes, 10000) is yes
assert pode(yes, 20000) is no           // passou do teto da hora
out "   30 s por hora, e nem o sensor quebrado alaga a casa"

out ""
out "== 6. e o defer desliga tudo, inclusive no erro =="

monitor:
    placa.escrever(AQUECEDOR, yes)
    trigger "um erro no meio do ciclo"
handle Error:
    out "   falhou no meio — e o programa ainda fecha a placa"

out "exercicio 397 ok"

Uma estufa é o projeto de IoT completo mais simples que existe: ela (temperatura e umidade de solo), decide (com histerese) e age (aquecedor e bomba). Todo problema da área aparece nela.

As cinco decisões que o programa carrega#

DecisãoO que ela evita
defer no topoterminar — inclusive por erro — com a bomba ligada
média móvel antes da histereseo ruído do ADC decidir por conta própria
duas soleiras, não umao relé batendo dezenas de vezes por minuto
a leitura vira recordestado["aquecndo"] num vault é void, e void é falso
a decisão numa ação, longe do hardwarenão dá para testar controle que só existe dentro do laço da placa

O teto duro que a histerese não dá#

Histerese não protege de um sensor que soltou do vaso: a leitura fica em "seco" para sempre, e a bomba não desliga nunca. O limite por tempo (no máximo 30 s por hora) é a proteção que importa, e ela é de outra natureza — não olha o sensor, olha o atuador.

O que falta para isto virar produção#

Sair do Firmata (uma estufa não pode depender do computador ligado), telemetria (sem histórico não há como saber por que a planta morreu) e alerta (o testamento do MQTT avisa quando a placa cai).


Rode um isolado com dataforge run exercicios/58-iot/388_a_primeira_placa.df.