Les Carnets de Byfeel domotique , Objets connectés , DIY , Programmation, Nouvelles Technologies ….

Flasher facilement un NotifHeure

Pour tous ceux qui ont du mal à compiler , soit avec ARDUINO IDE ou PlatformIO, voici une méthode plus simple pour profiter de la nouvelle interface et des nouvelles fonctionnalités du NotifheureXL , ou tout simplement pour s’installer son premier notifheure.

Afin de vous épargner la phase de compilation , la recherche des bibliothèques , j’ai préparé les fichiers afin que vous n’ayez qu’a “les poussé” dans votre ESP favoris.

Ci-dessous une méthode simple pour flasher directement en ligne de commande, les sources déjà compilés.

Mise a jour du 23 Mai : Comment flasher dans un second temps en OTA avec le script espota.py

Pré-requis : installation python

Cette méthode , fonctionne aussi bien sur Windows 10 , Mac OSX ou encore Linux.

Etape 1 : Télécharger la dernière version de python .

Si vous avez déjà python, vous pouvez garder la version en cours. J’ai testé avec la 2.7 , et la 3.8 c’est Ok pour les deux.


Pendant l’installation, pensez à activer les options d’environnement global, afin de pouvoir lancer la commande depuis n’importe quel dossier . l’option py, si elle existe permet de lancer python avec le raccourci py.

Je vérifie que tout fonctionne, j’ouvre le terminal ( sous Mac ou linux ) ou l’invite de commande ( sous windows ) , et je tape ;

python
Python 3.8.2 (v3.8.2:7b3ab5921f, Feb 24 2020, 17:52:18) 
[Clang 6.0 (clang-600.0.57)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> quit()    // pour sortir

Il est tout à fait possible de faire cohabiter deux versions de python ( par exemple la 2.7 et la 3.8 ). Par exemple lacommande python2 lancera la version 2 x et la commande python lancera par défaut la version 3.x. exemple de mon fichier profile sous Mac, pour mes commandes par défaut en créant un alias dans le fichier .bash profile ( exemple ci-dessous de mon fichier profile)

# Setting PATH for Python 3.7
# The original version is saved in .bash_profile.pysave
PATH="/Library/Frameworks/Python.framework/Versions/3.7/bin:${PATH}"
export PATH
# Setting PATH for Python 2.7
# The original version is saved in .bash_profile.pysave
PATH="/Library/Frameworks/Python.framework/Versions/2.7/bin:${PATH}"
export PATH
# Setting PATH for Python 3.8
# The original version is saved in .bash_profile.pysave
PATH="/Library/Frameworks/Python.framework/Versions/3.8/bin:${PATH}"
export PATH
alias python=python3.8
alias python2=python2.7
alias pip=pip3

Etape 2 : installer esptool

C’est l’utilitaire , qui nous aidera à pousser les fichiers binaires dans l’ESP. La documentation complète de l’outil est disponible sur le github d’espressif.
Pour installer esptool , selon le système MAC OSX , Linux ou WINDOWS , les commandes peuvent êtres différentes selon la version de python ( 2.x ) ou ( 3.x ) , par exemple pour pip ( pip3 pour la version 3 ) . Il est possible, comme indiqué sur l’etape 1, de créer des alias , afin que pip lance la commande pip3 automatiquement , si python en version 3.x.

pip3 install esptool
Collecting esptool
  Downloading https://files.pythonhosted.org/packages/68/91/08c182f66fa3f12a96e754ae8ec7762abb2d778429834638f5746f81977a/esptool-2.8.tar.gz (84kB)
     |████████████████████████████████| 92kB 1.1MB/s 
Collecting pyserial>=3.0 (from esptool)
  Downloading https://files.pythonhosted.org/packages/0d/e4/2a744dd9e3be04a0c0907414e2a01a7c88bb3915cbe3c8cc06e209f59c30/pyserial-3.4-py2.py3-none-any.whl (193kB)
     |████████████████████████████████| 194kB 2.1MB/s 
Collecting pyaes (from esptool)
  Downloading https://files.pythonhosted.org/packages/44/66/2c17bae31c906613795711fc78045c285048168919ace2220daa372c7d72/pyaes-1.6.1.tar.gz
Collecting ecdsa (from esptool)
  Downloading https://files.pythonhosted.org/packages/b8/11/4b4d30e4746584684c758d8f1ddc1fa5ab1470b6bf70bce4d9b235965e99/ecdsa-0.15-py2.py3-none-any.whl (100kB)
     |████████████████████████████████| 102kB 2.3MB/s 
Collecting six>=1.9.0 (from ecdsa->esptool)
  Downloading https://files.pythonhosted.org/packages/65/eb/1f97cb97bfc2390a276969c6fae16075da282f5058082d4cb10c6c5c1dba/six-1.14.0-py2.py3-none-any.whl
Installing collected packages: pyserial, pyaes, six, ecdsa, esptool
  Running setup.py install for pyaes ... done
  Running setup.py install for esptool ... done
Successfully installed ecdsa-0.15 esptool-2.8 pyaes-1.6.1 pyserial-3.4 six-1.14.0
WARNING: You are using pip version 19.2.3, however version 20.0.2 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.

il est possible de mettre a jour , aussi sa version de pip par la commande ( pip install –upgrade pip ).

Si l’installation bloque , il est possible d’utiliser l’option -m avec la commande python ou py ( selon le type d’installation effectué ).

py -m pip install esptool
ou
python -m pip install esptool

Je teste la commande esptool.py , afin de vérifier son fonctionnement.

esptool.py
esptool.py v2.8
usage: esptool [-h] [--chip {auto,esp8266,esp32}] [--port PORT] [--baud BAUD]
               [--before {default_reset,no_reset,no_reset_no_sync}]
               [--after {hard_reset,soft_reset,no_reset}] [--no-stub]
               [--trace] [--override-vddsdio [{1.8V,1.9V,OFF}]]
               {load_ram,dump_mem,read_mem,write_mem,write_flash,run,image_info,make_image,elf2image,read_mac,chip_id,flash_id,read_flash_status,write_flash_status,read_flash,verify_flash,erase_flash,erase_region,version}
               ...
esptool.py v2.8 - ESP8266 ROM Bootloader Utility
positional arguments:
  {load_ram,dump_mem,read_mem,write_mem,write_flash,run,image_info,make_image,elf2image,read_mac,chip_id,flash_id,read_flash_status,write_flash_status,read_flash,verify_flash,erase_flash,erase_region,version}
                        Run esptool {command} -h for additional help
    load_ram            Download an image to RAM and execute
    dump_mem            Dump arbitrary memory to disk
    read_mem            Read arbitrary memory location
    write_mem           Read-modify-write to arbitrary memory location
    write_flash         Write a binary blob to flash
    run                 Run application code in flash
    image_info          Dump headers from an application image
    make_image          Create an application image from binary files
    elf2image           Create an application image from ELF file
    read_mac            Read MAC address from OTP ROM
    chip_id             Read Chip ID from OTP ROM
    flash_id            Read SPI flash manufacturer and device ID
    read_flash_status   Read SPI flash status register
    write_flash_status  Write SPI flash status register
    read_flash          Read SPI flash content
    verify_flash        Verify a binary blob against flash
    erase_flash         Perform Chip Erase on SPI flash
    erase_region        Erase a region of the flash
    version             Print esptool version
optional arguments:
  -h, --help            show this help message and exit
  --chip {auto,esp8266,esp32}, -c {auto,esp8266,esp32}
                        Target chip type
  --port PORT, -p PORT  Serial port device
  --baud BAUD, -b BAUD  Serial port baud rate used when flashing/reading
  --before {default_reset,no_reset,no_reset_no_sync}
                        What to do before connecting to the chip
  --after {hard_reset,soft_reset,no_reset}, -a {hard_reset,soft_reset,no_reset}
                        What to do after esptool.py is finished
  --no-stub             Disable launching the flasher stub, only talk to ROM
                        bootloader. Some features will not be available.
  --trace, -t           Enable trace-level output of esptool.py interactions.
  --override-vddsdio [{1.8V,1.9V,OFF}]
                        Override ESP32 VDDSDIO internal voltage regulator (use
                        with care)

Maintenant que l’utilitaire est installé, je vais pouvoir transférer mes firmwares compilés directement dans mon ESP.

Les FIRMWARES compilés

Pour commencer , il va falloir récupérer les fichiers binaires compilés. J’ai mis a disposition, les firmwares déjà compilés . Ces firmwares ont été compilés pour des modules WemosD1, D2 ou Mini ou équivalent, ayant 4 Mo de mémoire.

Deux versions de firmwares , selon le type de matrices ( ICSStation et FC16 ) :
Lors de l’écriture de cet article , je suis à la version 0.8.6.0

Une version commune pour la mémoire SPIFFs ( contenant le dossier data , pour la configuration , fichier de config , etc)

Pour les firmwares , suivez ce lien.

Décompresser et déposer les fichiers dans le même dossier , et se placer à l’intérieur de ce dossier via le terminal ou l’invite de commande selon son Système d’exploitation.

Transfert des firmwares

Il suffit de suivre ces étapes :

  • Brancher le module ESP , sur le port série :

Sous Windows , le numéro de port peut etre récupéré via la commande mode.

Microsoft Windows [version 10.0.17134.1]
(c) 2018 Microsoft Corporation. Tous droits réservés.
C:\Users\SPARE>mode
Statut du périphérique COM6:
----------------------------
    Baud :            1200
    Parité :          None
    Bits de données : 8
    Bits d’arrêt :    1
    Temporisation :   OFF
    XON/XOFF :        OFF
    Protocole CTS :   OFF
    Protocole DSR :   OFF
    Sensibilité DSR : OFF
    Circuit DTR :     ON
    Circuit RTS :     ON
Statut du périphérique CON:
---------------------------
    Lignes :          300
    Colonnes :        80
    Vitesse clavier : 31
    Délai clavier :   1
    Page de codes :   850

Dans cet exemple , le numéro de port Windows est le COM6.
Sous MAC OSX , il faudra utiliser la commande ls /dev/tty.*

 ls /dev/tty.*
/dev/tty.Bluetooth-Incoming-Port	/dev/tty.wchusbserialfa1230

Sous mac , mon port est : /dev/tty.wchusbserialfa1230

  • flasher le module ESP
  • Sous Mac OSX
esptool.py --port /dev/tty.wchusbserialfa1230 write_flash -fm dio 0x00000 notifheureXL_ICS_0.8.6.0.bin 0x300000 notifheureXL_SPIFFS_0.8.bin
  • Sous Windows
esptool.py --port com6 write_flash -fm dio 0x00000 notifheureXL_ICS_0.8.6.0.bin 0x300000 notifheureXL_SPIFFS_0.8.bin

ce qui donne :

esptool.py v2.8
Serial port /dev/tty.wchusbserialfa1240
Connecting....
Detecting chip type... ESP8266
Chip is ESP8266EX
Features: WiFi
Crystal is 26MHz
MAC: a0:20:a6:12:55:23
Uploading stub...
Running stub...
Stub running...
Configuring flash size...
Auto-detected Flash size: 4MB
Compressed 502016 bytes to 348644...
Wrote 502016 bytes (348644 compressed) at 0x00000000 in 31.2 seconds (effective 128.9 kbit/s)...
Hash of data verified.
Compressed 1024000 bytes to 71788...
Wrote 1024000 bytes (71788 compressed) at 0x00300000 in 6.7 seconds (effective 1226.8 kbit/s)...
Hash of data verified.
Leaving...
Hard resetting via RTS pin...

Quelques options utiles

Flasher uniquement la memoire SPIFFS

esptool.py --port /dev/tty.wchusbserialfa1230 write_flash -fm dio 0x300000 notifheureXL_SPIFFS_0.8.bin

Flasher uniquement le firmware

esptool.py --port /dev/tty.wchusbserialfa1230 write_flash -fm dio 0x00000 notifheureXL_ICS_0.8.6.0.bin

pour information comment effacer la memoire complete

esptool.py --port COM6 erase_flash

Mettre à jour les firmwares en OTA .

Maintenant que le notifHeure à été flashé, il est possible de le mettre à jour à distance via le WIFI. Pour cela je vais utiliser le script espota.py.

Il suffit simplement de renseigner l’adresse IP du notifHeure ( déjà flashé avec un firmware compatible OTA ) et de lui indiquer le chemin du firmware à uploader.. Par exemple si mon notifheure à l’adresse : 192.168.1.2 , j’execute le code via la commande python ( ou python3 ou py selon l’environnement utilisé ).

Placez tous les fichiers nécessaires dans le même dossier , pour faciliter l’écriture de la commande.

python espota.py -i 192.168.1.2 -f firmwaresketch.bin     

-i adresse IP du module ESP

f fichier à uploader dans le module ESP ( par exemple firmware.bin , si dans le même dossier ou /path/to/file/firmware.bin pour un dossier différent.)

Par défaut remplace le sketch existant. Pour télécharge le firmware dédié à l’espace SPIFFS ( dossier data ) , il faudra utiliser l’option -s .

python espota.py -i 192.168.1.2 -s -f spiffs.bin

Script espota.py complet ( avec les différentes options )

#!/usr/bin/env python3
#
# Original espota.py by Ivan Grokhotkov:
# https://gist.github.com/igrr/d35ab8446922179dc58c
#
# Modified since 2015-09-18 from Pascal Gollor (https://github.com/pgollor)
# Modified since 2015-11-09 from Hristo Gochkov (https://github.com/me-no-dev)
# Modified since 2016-01-03 from Matthew O'Gorman (https://githumb.com/mogorman)
#
# This script will push an OTA update to the ESP
# use it like: python3 espota.py -i <ESP_IP_address> -I <Host_IP_address> -p <ESP_port> -P <Host_port> [-a password] -f <sketch.bin>
# Or to upload SPIFFS image:
# python3 espota.py -i <ESP_IP_address> -I <Host_IP_address> -p <ESP_port> -P <HOST_port> [-a password] -s -f <spiffs.bin>
#
# Changes
# 2015-09-18:
# - Add option parser.
# - Add logging.
# - Send command to controller to differ between flashing and transmitting SPIFFS image.
#
# Changes
# 2015-11-09:
# - Added digest authentication
# - Enhanced error tracking and reporting
#
# Changes
# 2016-01-03:
# - Added more options to parser.
#
from __future__ import print_function
import socket
import sys
import os
import optparse
import logging
import hashlib
import random
# Commands
FLASH = 0
SPIFFS = 100
AUTH = 200
PROGRESS = False
# update_progress() : Displays or updates a console progress bar
## Accepts a float between 0 and 1. Any int will be converted to a float.
## A value under 0 represents a 'halt'.
## A value at 1 or bigger represents 100%
def update_progress(progress):
  if (PROGRESS):
    barLength = 60 # Modify this to change the length of the progress bar
    status = ""
    if isinstance(progress, int):
      progress = float(progress)
    if not isinstance(progress, float):
      progress = 0
      status = "error: progress var must be float\r\n"
    if progress < 0:
      progress = 0
      status = "Halt...\r\n"
    if progress >= 1:
      progress = 1
      status = "Done...\r\n"
    block = int(round(barLength*progress))
    text = "\rUploading: [{0}] {1}% {2}".format( "="*block + " "*(barLength-block), int(progress*100), status)
    sys.stderr.write(text)
    sys.stderr.flush()
  else:
    sys.stderr.write('.')
    sys.stderr.flush()
def serve(remoteAddr, localAddr, remotePort, localPort, password, filename, command = FLASH):
  # Create a TCP/IP socket
  sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  server_address = (localAddr, localPort)
  logging.info('Starting on %s:%s', str(server_address[0]), str(server_address[1]))
  try:
    sock.bind(server_address)
    sock.listen(1)
  except:
    logging.error("Listen Failed")
    return 1
  # Check whether Signed Update is used.
  if ( os.path.isfile(filename + '.signed') ):
    filename = filename + '.signed'
    file_check_msg = 'Detected Signed Update. %s will be uploaded instead.' % (filename)
    sys.stderr.write(file_check_msg + '\n')
    sys.stderr.flush()
    logging.info(file_check_msg)
  
  content_size = os.path.getsize(filename)
  f = open(filename,'rb')
  file_md5 = hashlib.md5(f.read()).hexdigest()
  f.close()
  logging.info('Upload size: %d', content_size)
  message = '%d %d %d %s\n' % (command, localPort, content_size, file_md5)
  # Wait for a connection
  logging.info('Sending invitation to: %s', remoteAddr)
  sock2 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  remote_address = (remoteAddr, int(remotePort))
  sent = sock2.sendto(message.encode(), remote_address)
  sock2.settimeout(10)
  try:
    data = sock2.recv(128).decode()
  except:
    logging.error('No Answer')
    sock2.close()
    return 1
  if (data != "OK"):
    if(data.startswith('AUTH')):
      nonce = data.split()[1]
      cnonce_text = '%s%u%s%s' % (filename, content_size, file_md5, remoteAddr)
      cnonce = hashlib.md5(cnonce_text.encode()).hexdigest()
      passmd5 = hashlib.md5(password.encode()).hexdigest()
      result_text = '%s:%s:%s' % (passmd5 ,nonce, cnonce)
      result = hashlib.md5(result_text.encode()).hexdigest()
      sys.stderr.write('Authenticating...')
      sys.stderr.flush()
      message = '%d %s %s\n' % (AUTH, cnonce, result)
      sock2.sendto(message.encode(), remote_address)
      sock2.settimeout(10)
      try:
        data = sock2.recv(32).decode()
      except:
        sys.stderr.write('FAIL\n')
        logging.error('No Answer to our Authentication')
        sock2.close()
        return 1
      if (data != "OK"):
        sys.stderr.write('FAIL\n')
        logging.error('%s', data)
        sock2.close()
        sys.exit(1)
        return 1
      sys.stderr.write('OK\n')
    else:
      logging.error('Bad Answer: %s', data)
      sock2.close()
      return 1
  sock2.close()
  logging.info('Waiting for device...')
  try:
    sock.settimeout(10)
    connection, client_address = sock.accept()
    sock.settimeout(None)
    connection.settimeout(None)
  except:
    logging.error('No response from device')
    sock.close()
    return 1
  received_ok = False
  try:
    f = open(filename, "rb")
    if (PROGRESS):
      update_progress(0)
    else:
      sys.stderr.write('Uploading')
      sys.stderr.flush()
    offset = 0
    while True:
      chunk = f.read(1460)
      if not chunk: break
      offset += len(chunk)
      update_progress(offset/float(content_size))
      connection.settimeout(10)
      try:
        connection.sendall(chunk)
        if connection.recv(32).decode().find('O') >= 0:
          # connection will receive only digits or 'OK'
          received_ok = True
      except:
        sys.stderr.write('\n')
        logging.error('Error Uploading')
        connection.close()
        f.close()
        sock.close()
        return 1
    sys.stderr.write('\n')
    logging.info('Waiting for result...')
    # libraries/ArduinoOTA/ArduinoOTA.cpp L311 L320
    # only sends digits or 'OK'. We must not not close
    # the connection before receiving the 'O' of 'OK'
    try:
      connection.settimeout(60)
      received_ok = False
      received_error = False
      while not (received_ok or received_error):
        reply = connection.recv(64).decode()
        # Look for either the "E" in ERROR or the "O" in OK response
        # Check for "E" first, since both strings contain "O"
        if reply.find('E') >= 0:
          sys.stderr.write('\n')
          logging.error('%s', reply)
          received_error = True
        elif reply.find('O') >= 0:
          logging.info('Result: OK')
          received_ok = True
      connection.close()
      f.close()
      sock.close()
      if received_ok:
        return 0
      return 1
    except:
      logging.error('No Result!')
      connection.close()
      f.close()
      sock.close()
      return 1
  finally:
    connection.close()
    f.close()
  sock.close()
  return 1
# end serve
def parser(unparsed_args):
  parser = optparse.OptionParser(
    usage = "%prog [options]",
    description = "Transmit image over the air to the esp8266 module with OTA support."
  )
  # destination ip and port
  group = optparse.OptionGroup(parser, "Destination")
  group.add_option("-i", "--ip",
    dest = "esp_ip",
    action = "store",
    help = "ESP8266 IP Address.",
    default = False
  )
  group.add_option("-I", "--host_ip",
    dest = "host_ip",
    action = "store",
    help = "Host IP Address.",
    default = "0.0.0.0"
  )
  group.add_option("-p", "--port",
    dest = "esp_port",
    type = "int",
    help = "ESP8266 ota Port. Default 8266",
    default = 8266
  )
  group.add_option("-P", "--host_port",
    dest = "host_port",
    type = "int",
    help = "Host server ota Port. Default random 10000-60000",
    default = random.randint(10000,60000)
  )
  parser.add_option_group(group)
  # auth
  group = optparse.OptionGroup(parser, "Authentication")
  group.add_option("-a", "--auth",
    dest = "auth",
    help = "Set authentication password.",
    action = "store",
    default = ""
  )
  parser.add_option_group(group)
  # image
  group = optparse.OptionGroup(parser, "Image")
  group.add_option("-f", "--file",
    dest = "image",
    help = "Image file.",
    metavar="FILE",
    default = None
  )
  group.add_option("-s", "--spiffs",
    dest = "spiffs",
    action = "store_true",
    help = "Use this option to transmit a SPIFFS image and do not flash the module.",
    default = False
  )
  parser.add_option_group(group)
  # output group
  group = optparse.OptionGroup(parser, "Output")
  group.add_option("-d", "--debug",
    dest = "debug",
    help = "Show debug output. And override loglevel with debug.",
    action = "store_true",
    default = False
  )
  group.add_option("-r", "--progress",
    dest = "progress",
    help = "Show progress output. Does not work for ArduinoIDE",
    action = "store_true",
    default = False
  )
  parser.add_option_group(group)
  (options, args) = parser.parse_args(unparsed_args)
  return options
# end parser
def main(args):
  # get options
  options = parser(args)
  # adapt log level
  loglevel = logging.WARNING
  if (options.debug):
    loglevel = logging.DEBUG
  # end if
  # logging
  logging.basicConfig(level = loglevel, format = '%(asctime)-8s [%(levelname)s]: %(message)s', datefmt = '%H:%M:%S')
  logging.debug("Options: %s", str(options))
  # check options
  global PROGRESS
  PROGRESS = options.progress
  if (not options.esp_ip or not options.image):
    logging.critical("Not enough arguments.")
    return 1
  # end if
  command = FLASH
  if (options.spiffs):
    command = SPIFFS
  # end if
  return serve(options.esp_ip, options.host_ip, options.esp_port, options.host_port, options.auth, options.image, command)
# end main
if __name__ == '__main__':
  sys.exit(main(sys.argv))
# end if

Laisser un commentaire

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.

11 commentaires sur “Flasher facilement un NotifHeure”

%d blogueurs aiment cette page :