2024-04-18
md
A Second Look at the W600-PICO Development Board
<-A First Look at the Winner Micro W600
Beginners' W600-PICO Tutorial by a Random Neophyte.

The subtitle is appropriate; the author's knowledge about the WEMOS W600-PICO development board and the MicroPython language is at best superficial. The proof is in the fact that all the example scripts found below do one thing only, flash the on-board LED. More telling, in my first post on the W600-PICO, the default MicroPython firmware as delivered from the Lolin store was used and not updated. Unfortunately, version 1.10 was already quite old and proved incompatible with many examples available on the Web. Thankfully, I was informed that a fork of the current version of MicroPython was available for the W60x microcontrollers and it was kindly suggested it would be a good thing to update everything. This new post is an attempt to do this. The older post will remain available just in case someone wants to play with the W600-PICO without updating the firmware, although I can't think of any good reason to do that. Furthermore, I do not think there is anything to be gained by reading the first post; this new post is self-contained.

Table of Contents

  1. The W600-PICO Development Board
  2. MicroPython vs Python
  3. The Default MicroPython Firmware
  4. Flashing the MicroPython Firmware
  5. Restoring the MicroPython Firmware
  6. The Read-eval-print Loop (REPL)
    1. Controlling the Built In LED
    2. Connecting to a Wi-Fi Network
    3. Starting the FTP Server
  7. Quick but Dirty Wi-Fi Connection
  8. Using the FTP Server
  9. Using Thonny
  10. Blinking the LED Again
  11. Running the blink Script
  12. Blinking the LED, Again and Again
  13. Monitoring a Button
  14. A Custom boot.py
  15. Simple Web Server
  16. Controling an LED with MQTT
  17. A Wi-Fi Switch?
  18. More?
  19. A Signal Comment (2024-04-16)

The W600-PICO Development Board toc

Is it too obvious to say that the W600-PICO dev board by WEMOS is based on the Winner Micro W600 microcontroller? The controller is an 80 Mhz 32-bit ARM M3 with 288 KiB of RAM and 1 MiB of integrated flash memory. Presenting itself as a cheaper alternative to the Espressif ESP8266 or rather the ESP8285 given its integrated flash memory, it has 2.4 GHz Wi-Fi (802.11b/g/n) wireless connectivity. It also has multiple interfaces: GPIO, PWM, SDIO, UART, I²C, SPI, RTC and so on. Here is the pinout of the W600-PICO based on the description in the WEMOS documentation.

W600-PICO pinout

As can be seen, this is a relatively simple device, with a PCB antenna, an LED, a 40 MHz crystal, the microcontroller, a 3.3 volts LDO regulator, a USB to serial chip, a microUSB connector, a reset push button plus some passive components (see the schematic for details). The microcontroller is labelled W600-B800. According to the datasheet, this identifies a revision B chip with 1 MiByte of integrated flash memory.

When boards based on the W600 appeared around 2018, they garnered some interest for a year or so which then waned considerably as far as I can make out. Now, contrary to the ESP8266 (or other products from Espressif), there is only a small community coalesced around this chip creating a classic chicken and egg scenario. Hobbyists are less attracted to the device because few hobbyists are using it. Consequently in my first look at the board, I relied mostly on two, rather old, sources for information about the W600-PICO board and another two sources for general information about the W600 microcontroller.

The first two references assume that development will be done with the version of MicroPython installed at the factory. The pre-installed version is a Jan 25, 2019 release of a Winner Micro fork of the version 1.10 MicroPython implementation. About 3 months later, wdyichen made a "pull" request to the MicroPython team asking that the Micro Winner W600 be added to the supported ports. From what I can gather, nothing came of that because of the lack of interest in the W600 at the time and the difficulty in implementing TLS in 1 MB of memory. Consequently, the pre-installed version of MicroPython has fallen behind the current version of the language. In my first look at the W600-PICO, I used the original firmware which proved somewhat complicated. The good news is that Robert Hammelrath (robert-hh) has continued development of the wdyichen fork with the help of other and actively maintains a MicroPython port to the W60X. Initially I thought it would be necessary to install the toolchain in order to compile the firmware something which I was not interested in doing. However LexxM3 recently informed me that Robert Hammelrath has released and maintains image files of various ports of the current MicroPython version in his Shared-Stuff repository. How to install that version of the firmware will be explained below.

MicroPython vs Python toc

MicroPython is an implementation of Python 3 designed to run on microcontrollers. The project was created by Damien George starting in 2013 and build around an "official" reference hardware platform, the pyboard which contains an ARM Cortex M based microcontroller. New versions of the pyboard are readily available. Over the years, MicroPython has been officially ported to other development boards based on the ARM architecture and others such as the Xtensa (ESP8286, ESP32), STM32, Raspberry Pi RP2. There are many other "unofficial" ports and forks, including a fork for the W600-PICO by Robert Hammelrath and others (see Awesome MicroPython although that list is long in the tooth now).

Python is a computer language designed to run on top of an operating system such as Windows or Linux. It is the language used most often used for programs meant to run on the Raspberry Pi OS. Compared to desktops and Raspberry Pi models, most microcontrollers have very limited resources compared to a desktop computer and do not run an operating system. Instead, their firmware controls the hardware directly. Ports, such as the one for the ESP32, designed to run as tasks in FreeRTOS are notable exceptions. No matter which port of MicroPython is used, the firmware provides a minimal kernel or operating system which includes a file system and standard modules. These are precompiled Python scripts included as frozen .mpy bytecode in the firmware itself.

Frozen MicroPython Bytecode ? Show

The MicroPython file system is very important because the microcontroller runs Python scripts saved as text files within that system. Furthermore, it is possible to leverage other libraries or modules by downloading them into the file system. By convention, MicroPython executes two scripts in the root of the file system (more accurately in the /flash directory), boot.py and then main.py in that order. Here is my mental representation of the flash memory of the W600-PICO with the latest version of MicroPython.

Flash Memory (1 megabyte)
Micropython Core Python compiler to bytecode Runtime bytcode interpreter Language shell (REPL: read-eveal-print loop)
Frozen Modules __main__ micropython uasyncio/stream urandom _boot neopixel ubinascii ure _onewire network ucollections uselect _uasyncio ntptime uctypes usocket bdevice pye uerrno ustruct builtins sdcard uhashlib usys cmath uarray uheapq utime easyw600 uasyncio/__init__ uio utimeq flash_spi uasyncio/core ujson uwebsocket framebuf uasyncio/event umachine uzlib gc uasyncio/funcs uos w600 math uasyncio/lock upysh
Flash File System /lib/Button.py /mqtt/simple.py /boot.py /main.py /secrets.py /blink.py ...

For those familiar with Python, the frozen module names that start with a "u" prefix (meant to represent "µ" or "micro") are MicryPython versions of the standard Python module. Just to confuse things, the documentation refers to these modules without the prefix.

Instead of uploading firmware which is a binary file containing compiled C/C++ code, to an Arduino board, the W600-PICO is programmed by uploading a new main.py Python script and perhaps a new boot.py script plus any supplementary scripts and libraries to the board's file system. Just how to go about this is investigated in the rest of this post through a few examples. However, it is best to update the on-board MicroPython to the latest available version.

The Default MicroPython Firmware toc

Before anything else, let's communicate with the development board to find out, at the very least, which version of MicroPython was installed at the factory. Start monitoring the device manager and then plug a USB cable from the Linux system to the W600-PICO. Make sure that it is a full power and data cable because we need a serial link between the device and the computer.

michel@hp:~$ udevadm monitor --udev monitor will print the received events for: UDEV - the event which udev sends out after rule processing UDEV [548140.048780] add /devices/pci0000:00/0000:00:14.0/usb3/3-9/3-9.2 (usb) UDEV [548140.052689] add /devices/pci0000:00/0000:00:14.0/usb3/3-9/3-9.2/3-9.2:1.0 (usb) UDEV [548140.056808] add /devices/pci0000:00/0000:00:14.0/usb3/3-9/3-9.2/3-9.2:1.0/ttyUSB0 (usb-serial) UDEV [548140.061882] add /devices/pci0000:00/0000:00:14.0/usb3/3-9/3-9.2/3-9.2:1.0/ttyUSB0/tty/ttyUSB0 (tty) UDEV [548140.063349] bind /devices/pci0000:00/0000:00:14.0/usb3/3-9/3-9.2/3-9.2:1.0/ttyUSB0 (usb-serial) UDEV [548140.064931] bind /devices/pci0000:00/0000:00:14.0/usb3/3-9/3-9.2/3-9.2:1.0 (usb) UDEV [548140.079270] bind /devices/pci0000:00/0000:00:14.0/usb3/3-9/3-9.2 (usb) ^C

Press the CtrlC key combination to close the monitor. Clearly, a USB device attached to ttyUSB0 was added. That clever way of finding the device assigned to the USB-serial converter on the board was suggested by Les Pounder. The display diagnostic messages command, dmseg could just as easily be used.

michel@hp:~$ sudo dmesg -C michel@hp:~$ dmesg -w [2289218.344604] usb 3-10: new full-speed USB device number 17 using xhci_hcd [2289218.493451] usb 3-10: New USB device found, idVendor=1a86, idProduct=7523, bcdDevice= 2.63 [2289218.493453] usb 3-10: New USB device strings: Mfr=0, Product=2, SerialNumber=0 [2289218.493455] usb 3-10: Product: USB2.0-Serial [2289218.495117] ch341 3-10:1.0: ch341-uart converter detected [2289218.495453] usb 3-10: ch341-uart converter now attached to ttyUSB0 ^C

Note that the first command clear the log so that the second command will only show additional messages when the USB cable is connected. The udev and dmesg examples were run months apart which explains why the usb device number is not the same.

Alternatively, serial devices can be found in the usual fashion. The onboard USB to serial converter is a CH340 and these usually show up in the list of connected USB devices and as a ttyUSBx device. Sometimes, microcontrollers show up as a ttyACMx device, so let's check both.

michel@hp:~$ lsusb ... Bus 003 Device 027: ID 1a86:7523 QinHeng Electronics HL-340 USB-Serial adapter ... michel@hp:~$ ls /dev/ttyACM* /dev/ttyUSB* ls: impossible d'accéder à '/dev/ttyACM*': Aucun fichier ou dossier de ce type /dev/ttyUSB0

I used cu from the venerable uucp package as a terminal, others may prefer minicom, screen and so on. Once the serial connection is established, I was running in the MicroPyton read-eval-print loop (REPL).

michel@hp:~$ cu -l /dev/ttyUSB0 -s 115200 Connected. __ __   \ \ /\ / /   \ \ / \ / /   \ \/ /\ \/ /   \ / \ /   / /\ / /\   / /\ \/ /\ \   / / \ / \ \   /_/ \/ \_\   Traceback (most recent call last): File "boot.py", line 1, in NameError: name '����������������������������������������������������������������������������������������������������������������������������������������������' isn't defined Traceback (most recent call last): File "main.py", line 1, in NameError: name '����������������������������������' isn't defined MicroPython v1.10-282-g6a9b3cb-dirty on 2019-09-17; WinnerMicro module with W600 Type "help()" for more information.

Do not worry about the errors. As far as I can make out, the file system on the W600-PICO contains a number of empty subdirectories and three files: boot.py and main.py that are filled with 0xFFs (hence the isn't defined errors) and an empty easyw600.py. What is important is that communication was possible and that we have confirmation that the default MicroPython firmware is very old. If you want to continue with this older version of the interpreter, then go back to the first post about this board. My suggestion is to avoid my mistake and to update to a newer version immediately as explained next.

Flashing the MicroPython Firmware toc

Start by installing the firmware flashing tool w600tool by Volodymyr Shymanskyy (vshymanskyy) a Ukranian developer who is also the co-founder of the Blynk IoT platform. The tool is a Python script with some prerequisites. As usual in such a case, I prefer installing the tool in a virtual environment. Below I use my bash script from Python 3 virtual environments, but there are other ways of implementing this same idea.

  1. Create a virtual environment.
    michel@hp:~$ mkvenv w600tool creating virtual environment /home/michel/w600tool updating virtual environment /home/michel/w600tool
  2. Enable the virtual environment and install prerequisites.
    michel@hp:~$ ve w600tool (w600tool) michel@hp:~$ cd w600tool/ (w600tool) michel@hp:~/w600tool$ pip install pyserial PyPrind xmodem Collecting pyserial Using cached pyserial-3.5-py2.py3-none-any.whl (90 kB) Collecting PyPrind Downloading PyPrind-2.11.3-py2.py3-none-any.whl (8.4 kB) Collecting xmodem Downloading xmodem-0.4.6.tar.gz (32 kB) Preparing metadata (setup.py) ... done Building wheels for collected packages: xmodem Building wheel for xmodem (setup.py) ... done Created wheel for xmodem: filename=xmodem-0.4.6-py3-none-any.whl size=34564 sha256=9bf5b6a98f495bba128bc93fd6865e113b12b4aeaa78aff340b3a41692e95768 Stored in directory: /home/michel/.cache/pip/wheels/8a/46/14/833413574281b7009c9180fce7c595a7cc1b538e43fcd8b7e7 Successfully built xmodem Installing collected packages: xmodem, pyserial, PyPrind Successfully installed PyPrind-2.11.3 pyserial-3.5 xmodem-0.4.6
  3. Download the script from the GitHub repository.
    (w600tool) michel@hp:~/w600tool$ wget https://raw.githubusercontent.com/vshymanskyy/w600tool/master/w600tool.py ... 2022-04-21 17:08:44 (6,11 MB/s) - «w600tool.py» enregistré [6441/6441]
  4. Make the script executable.
    (w600tool) michel@hp:~/w600tool$ chmod +x w600tool.py (w600tool) michel@hp:~/w600tool$ ls -l w6* -rwxrwxr-x 1 michel michel 6441 avr 21 17:08 w600tool.py
  5. Check that the script can reach the W600-PICO.
    (w600tool) michel@hp:~/w600tool$ ./w600tool.py -p /dev/ttyUSB0 -b 115200 --get-mac Opening device: /dev/ttyUSB0 MAC: 286DCD2C7E89
  6. Obtain the latest Micropython firmware for the board from Robert Hammelrath. The repository contains two versions (wm_w600_lfs.fls and (wm_w600_lfs_threading.fls) while the README file talks of four versions. I used the non-threading version, multithreading support remains "highly experimental".
    michel@hp:~/w600tool$ wget https://github.com/robert-hh/Shared-Stuff/raw/master/wm_w600_lfs.fls --2022-12-15 15:28:24-- https://github.com/robert-hh/Shared-Stuff/raw/master/wm_w600_lfs.fls Résolution de github.com (github.com)… 140.82.113.4 Connexion à github.com (github.com)|140.82.113.4|:443… connecté. requête HTTP transmise, en attente de la réponse… 302 Found Emplacement : https://raw.githubusercontent.com/robert-hh/Shared-Stuff/master/wm_w600_lfs.fls [suivant] --2022-12-15 15:28:24-- https://raw.githubusercontent.com/robert-hh/Shared-Stuff/master/wm_w600_lfs.fls Résolution de raw.githubusercontent.com (raw.githubusercontent.com)… 185.199.111.133, 185.199.109.133, 185.199.110.133, ... Connexion à raw.githubusercontent.com (raw.githubusercontent.com)|185.199.111.133|:443… connecté. requête HTTP transmise, en attente de la réponse… 200 OK Taille : 608660 (594K) [application/octet-stream] Enregistre : «wm_w600_lfs.fls» wm_w600_lfs.fls 100%[===================>] 594,39K --.-KB/s ds 0,1s 2022-12-15 15:28:24 (4,42 MB/s) - «wm_w600_lfs.fls» enregistré [608660/608660]
    As this is being rewritten (April 18, 2024), the W600_firmware branch of the repository contains a single version of the firmware for the W600-PICO: wm_w600_WEMOS_W600.fls. Consequently, use that wm_w600_WEMOS_W600.fls instead of wm_w600_lfs.fls.
    michel@hp:~/w600tool$ wget https://github.com/robert-hh/Shared-Stuff/raw/master/w600_firmware/wm_w600_WEMOS_W600.fls --2024-04-18 15:59:08-- https://github.com/robert-hh/Shared-Stuff/raw/master/w600_firmware/wm_w600_WEMOS_W600.fls Résolution de github.com (github.com)… 140.82.112.4 Connexion à github.com (github.com)|140.82.112.4|:443… connecté. requête HTTP transmise, en attente de la réponse… 302 Found Emplacement : https://raw.githubusercontent.com/robert-hh/Shared-Stuff/master/w600_firmware/wm_w600_WEMOS_W600.fls [suivant] --2024-04-18 15:59:08-- https://raw.githubusercontent.com/robert-hh/Shared-Stuff/master/w600_firmware/wm_w600_WEMOS_W600.fls Résolution de raw.githubusercontent.com (raw.githubusercontent.com)… 185.199.111.133, 185.199.109.133, 185.199.110.133, ... Connexion à raw.githubusercontent.com (raw.githubusercontent.com)|185.199.111.133|:443… connecté. requête HTTP transmise, en attente de la réponse… 200 OK Taille : 738100 (721K) [application/octet-stream] Enregistre : ‘wm_w600_WEMOS_W600.fls’ wm_w600_WEMOS_W600.fls 100%[===============================>] 720,80K --.-KB/s ds 0,1s 2024-04-18 15:59:08 (7,20 MB/s) - ‘wm_w600_WEMOS_W600.fls’ enregistré [738100/738100]

    The firmware is much bigger (738,100 bytes vs 608,660 bytes). According to R. Hammelrath, w600_firmware: Firmware for a couple of W600 boards. These differ only in the definition of the boas [sic] pins in Pin.board. They all have threading and SSL enabled and use LFS for the internal file system. That's surprising as I did not think SSL could be squeezed on the W600 PICO with only 1 Mbytes of flash memory. Hopefully, changes will not affect materially anything that follows. Soon, I hope to find time to completely update the post and verify this.
  7. Upload the firmware to the board.
    (w600tool) michel@hp:~/w600tool$ ./w600tool.py -p /dev/ttyUSB0 -b 115200 -e --upload-baud 115200 -u wm_w600_WEMOS_W600.fls Opening device: /dev/ttyUSB0 Erasing secboot Erasing image Uploading wm_w600.fls 0% [══════════════════════════════] 100% | ETA: 23:59:59 Total time elapsed: 00:00:55 Reset board to run user code... (w600tool) michel@hp:~/w600tool$

    It may happen that it will be necessary to reset the board. A prompt will appear in that case.

    (w600tool) michel@hp:~/w600tool$ ./w600tool.py -p /dev/ttyUSB0 -b 115200 -e --upload-baud 115200 -u wm_w600_WEMOS_W600.fls Opening device: /dev/ttyUSB0 Push reset button to enter bootloader...
  8. Check that the newer firmware was uploaded.
    Press the reset (RST) button and then open a serial connection with a terminal program.
    (w600tool) michel@hp:~/w600tool$ cu -l /dev/ttyUSB0 -s 115200 Connected. CCCCCCCCCCCCCCC../../lib/littlefsNo Wifi setting found WinnerMicro W600 MicroPython v1.19.1-784-g913e397b1-dirty on 2022-12-15; WinnerMicro module with W600 Type "help()" for more information.

    The opening screen may be cleaner in subsequent connections as shown next.

    (w600tool) michel@hp:~/w600tool$ cu -l /dev/ttyUSB0 -s 115200 Connected. No Wifi setting found WinnerMicro W600 MicroPython v1.19.1-784-g913e397b1-dirty on 2022-12-15; WinnerMicro module with W600 Type "help()" for more information. >>>

Restoring the MicroPython Firmware toc

Rather clumsily, I have often put the W600-PICO into a tight loop where it seemed impossible to get control back. At various times the situation seemed hopeless, and I had to erase the flash memory and upload the MicroPython firmware. Luckily, this is simple to do once the flashing tool has been installed as explained above. It is not even necessary to enable the virtual environment to execute the flash tool. All that is needed to execute the W600 tool is to launch the correct Python interpreter found the virtual environment. Here is a little test of what I mean.

michel@hp:~$ /home/michel/w600tool/bin/python /home/michel/w600tool/w600tool.py -p /dev/ttyUSB0 -b 115200 --get-mac Opening device: /dev/ttyUSB0 MAC: 286DXXXXXXXX

Using that fact, I wrote the following short little bash script to avoid having to type that very long command.

#!/bin/bash pushd /home/michel/w600tool > /dev/null bin/python w600tool.py -p /dev/ttyUSB0 -b 115200 -e --upload-baud 115200 -u wm_w600_WEMOS_W600.fls popd > /dev/null

That script, named flashW600, made executable, and stored in the search path, provides a simple means to flash the MicroPython firmware. Of course, it may be necessary to adjust the serial device name and the name of the binary file containing MicroPython. Again, a prompt will be displayed if it necessary to reset the PICO-W600. With that knowledge, it is now safe to embark on an exploration of the board confident that it is possible to recover from any mistake.

The Read-eval-print Loop (REPL) toc

This will be a very short introduction to the The MicroPython Interactive Interpreter Mode (aka REPL) which has already been encountered. Basically, it is a command-line interface provided by the flashed MicroPython firmware on the W600-PICO. As soon as a serial connection with the board is made, the >>> prompt is available and accepts commands. Interactive REPL sessions will be shown with a turquoise background colour, but they are simply terminal sessions.

michel@hp:~$ cu -l /dev/ttyUSB0 -s 115200
Connected. No Wifi setting found WinnerMicro W600 MicroPython v1.19.1-784-g913e397b1-dirty on 2022-12-15; WinnerMicro module with W600 Type "help()" for more information. >>> help() Welcome to MicroPython on the W600! For generic online docs please visit http://docs.micropython.org/ For access to the hardware use the 'machine' module: import machine pb26 = machine.Pin(machine.Pin.PB_26, machine.Pin.OUT, machine.Pin.PULL_DOWN) pb26.value(1) pb27 = machine.Pin(machine.Pin.PB_27, machine.Pin.IN, machine.Pin.PULL_UP) print(pb27.value()) Basic WiFi configuration: import network sta_if = network.WLAN(network.STA_IF) sta_if.active(True) sta_if.scan() # Scan for available access points sta_if.connect("<AP_name>", "<password>") # Connect to an AP sta_if.isconnected() # Check for successful connection Control commands: CTRL-A -- on a blank line, enter raw REPL mode CTRL-B -- on a blank line, enter normal REPL mode CTRL-C -- interrupt a running program CTRL-D -- on a blank line, do a soft reset of the board CTRL-E -- on a blank line, enter paste mode For further help on a specific object, type help(obj) For a list of available modules, type help('modules') >>>

As a starting point, let's look at the two suggested examples displayed by the help command.

Controlling the Built In LED toc

Let's turn on the built-in LED following the suggestions displayed in the help() screen. Note that we import only the subset of the machine module needed for digital input/output.

>>> from machine import Pin >>> help(Pin) object <class 'Pin'> is of type type init -- <function> value -- <function> off -- <function> on -- <function> irq -- <function> OPEN_DRAIN -- 3 IN -- 1 OUT -- 0 PULL_UP -- 1 PULL_DOWN -- 2 PULL_NONE -- 0 IRQ_RISING -- 1 IRQ_FALLING -- 2 IRQ_HIGH_LEVEL -- 4 IRQ_LOW_LEVEL -- 5 PA_00 -- 0 PA_01 -- 1 ... PB_30 -- 46 PB_31 -- 47 >>> LED = Pin(Pin.PA_00, Pin.OUT) >>> LED.value(0) >>> LED.on() >>>

The help(Pin) command displays information about the functions and constants of the Pin object. The next command creates an instance of the Pin object that will be used to access the built-in LED. The LED is connected to bit 0 of I/O Port A, labelled PA0 on the W600-PICO. That pin must be a digital output in order to turn the LED on or off. The third command turns the LED on by setting the output pin PA_00 low (i.e. to 0 volts).

circuit of built-in LED

The LED connections taken from the board's schematic shows why a "reverse" logic must be used to turn the LED on. If PA0 is set to HIGH then both the anode and cathode of the LED are at the same potential: 3.3 volts. There is no difference, hence no current flows across the diode which will remain off. By setting PA0 to LOW, a current can flow since the anode is at 3.3 volts and the cathode is at 0 volts.

Instead of explicitly writing a 0 to the output pin, the off function can be used. So LED.off() could have been used instead of LED.value(0) to turn the LED on. By the same token, the last command, LED.on() is the same as LED.value(1) and it turns the LED off.

Let's put these functions into a for loop. Here is a MicroPython script that would flash the built in led on for 2 tenths of a second and the off for 8 tenths of a second 20 times.

from machine import Pin LED = Pin(Pin.PA_00, Pin.OUT) import time for i in range(20): LED.off() time.sleep(0.2) LED.on() time.sleep(0.8)

Trigger warning: the following statement may offend some, but it is just a harmless opinion. Remember, Python uses an archaic syntax where all members of a compound statements must be indented and all begin on the same column; it’s reminiscent of column alignment on Fortran punch cards in the 1970s and earlier. The Auto-indent feature of the REPL makes this somewhat simpler. As soon as a line is terminated with a colon (:), the REPL indents all subsequent lines by four spaces.

>>> import time >>> for i in range(20): ... LED.off() ... time.sleep(0.2) ... LED.on() ... time.sleep(0.8) ... BackspaceReturn >>>

Since Pin was already imported and LED already defined, there is no need to reenter these commands. To exit the auto-indent mode, press the Backspace at the start of a blank indented line. Press Enter again to execute the for loop. Once the loop is completed, the REPL prompt >>> is displayed again. Instead of using the Backspace, the Return key can be pressed twice. So in that case, it will be necessary to press Return four times after time.sleep(0.8) to execute the loop.

It is also possible to flash the LED continuously, by creating a never-ending loop. In that case, to get back to the REPL, the board must be reset which can be done by entering a Ctrl+C key combination.

>>> while True: ... LED.off() ... time.sleep(0.1) ... LED.on() ... time.sleep(0.4) ... BackspaceReturn >>> ... ... CtrlC Traceback (most recent call last): File "", line 5, in KeyboardInterrupt: >>> >>> MicroPython v1.19.1-784-g913e397b1-dirty on 2022-12-15; WinnerMicro module with W600 Type "help()" for more information. >>> MPY: soft reboot No Wifi setting found WinnerMicro W600 MicroPython v1.19.1-784-g913e397b1-dirty on 2022-12-15; WinnerMicro module with W600 Type "help()" for more information. >>>

If this is all very new, the reader may want to read the Adafruit Blink LED tutorial.

Connecting to a Wi-Fi Network toc

One of the valuable characteristics of the W600 is its TCP stack over Wi-Fi. It is quite easy to connect to a Wi-Fi hotspot with the network module. Let's follow the procedure outlined in the REPL help page.

>>> import network >>> sta = network.WLAN(network.STA_IF) >>> sta.active(True) True >>> sta.scan() [(b'playteck', b'\xd4\xe2\xcbLq\xf9', 1, -81, 32, False), (b'rosebud', b'\x00\xfc\x8dOq\xb8', 11, -61, 40, False)] >>> sta.connect("rosebud", "87654321") wait a few seconds!! >>> sta.isconnected() True >>> sta.ifconfig() ('192.168.1.146', '255.255.255.0', '192.168.1.1', '192.168.1.1')

The ifconfig() function returns a tuple containing four elements: (ip, subnet, gateway, dns) which described the network connection. Note the suggestion to wait a few seconds. If one types quickly and checks if the Wi-Fi connection is established immediately after issuing the connect command, the result will be false. If the following list of statements is pasted into the REPL,

import network sta = network.WLAN(network.STA_IF) sta.active(True) sta.connect("rosebud", "87654321") sta.isconnected()

then it is guaranteed that the connection will appear to fail. However checking after a few seconds will show that the board is connected to the Wi-Fi network.

>>>Ctrl-E paste mode; Ctrl-C to cancel, Ctrl-D to finish === import network === sta = network.WLAN(network.STA_IF) === sta.active(True) === sta.connect("rosebud", "87654321") === sta.isconnected() ===Ctrl-D True response to sta.active() False response to sta.isconnected() after waiting four or five seconds enter the command again >>> sta.isconnected() True >>> sta.ifconfig() ('192.168.1.146', '255.255.255.0', '192.168.1.1', '192.168.1.1')

Experimentation has shown that the time required to connect to a Wi-Fi network can vary considerably and it is necessary to allow for that possibility. Once connected to my Wi-Fi router, the PICO-W600 is listed among other connected devices but its host name, uart-wifi, is rather cryptic.

Device connected to Wi-Fi router

As seen from the output of the ifconfig() command or from the router list of connected devices, the dynamically assigned IP address of the board was 192.168.1.146. It uses 192.168.1.1 as a gateway (that's the IP address of the Wi-Fi router) and it obtained a DNS server IP address of '192.168.1.1'. ping will confirm that the board is connected to the network and nmap will show additional information about the board including the MAC address of the Wi-Fi radio.

michel@hp:~$ ping 192.168.1.146 -c 3 PING 192.168.1.146 (192.168.1.146) 56(84) bytes of data. 64 octets de 192.168.1.146 : icmp_seq=1 ttl=255 temps=373 ms 64 octets de 192.168.1.146 : icmp_seq=2 ttl=255 temps=28.5 ms 64 octets de 192.168.1.146 : icmp_seq=3 ttl=255 temps=50.3 ms --- statistiques ping 192.168.1.146 --- 3 paquets transmis, 3 reçus, 0 % paquets perdus, temps 2002 ms rtt min/moy/max/mdev = 28,466/150,671/373,275/157,656 ms michel@hp:~$ sudo nmap -vv 192.168.1.146 Starting Nmap 7.80 ( https://nmap.org ) at 2022-12-20 14:10 AST Initiating ARP Ping Scan at 14:10 Scanning 192.168.1.146 [1 port] Completed ARP Ping Scan at 14:10, 0.16s elapsed (1 total hosts) Initiating Parallel DNS resolution of 1 host. at 14:10 Completed Parallel DNS resolution of 1 host. at 14:10, 0.08s elapsed Initiating SYN Stealth Scan at 14:10 Scanning 192.168.1.146 [1000 ports] Completed SYN Stealth Scan at 14:10, 0.90s elapsed (1000 total ports) Nmap scan report for 192.168.1.146 Host is up, received arp-response (0.034s latency). All 1000 scanned ports on 192.168.1.146 are closed because of 1000 resets MAC Address: 28:6D:xx:xx:xx:xx (Beijing Winner Microelectronics) Read data files from: /usr/bin/../share/nmap Nmap done: 1 IP address (1 host up) scanned in 1.35 seconds Raw packets sent: 1001 (44.028KB) | Rcvd: 1001 (40.028KB)

Note how nmap confirms that no TCP port from 0 to 1023 is open.

Starting The FTP Server toc

Let's go further and start an FTP server on the W600-PICO. With such a server in place, it will be possible to copy files to or from the board, in other words, it will be possible to modify the firmware. Looking at the list of modules, it may not be obvious that there is an FTP server, but it is present in the w600 module which will therefore have to be imported. So after connecting to the Wi-Fi network as explained in the previous subsection, import the w600 module and start the server.

>>> import w600 >>> w600.run_ftpserver(port=21,username="user",password="12345678")

Of course, a different user name and password should be used. It is not a good idea to run a server that has a dynamically assigned IP address. The network.WLAN object (instantiated as sta in this example) contains a configuration function that can be used to set a static IP address.

>>> sta.ifconfig(('192.168.1.59', '255.255.255.0', '192.168.1.1', '8.8.8.8')) >>> sta.ifconfig() ('192.168.1.59', '255.255.255.0', '192.168.1.1', '8.8.8.8')

As seen before, the same ifconfig() command, but without parameters, displays the current IP address, subnet mask, gateway and DNS server IP.

Quick but Dirty Wi-Fi Connection toc

In his recent MicroPython port for the W600, Robert Hammelrath added a quick and convenient means of connecting to a Wi-Fi network and starting an FTP server. Once the startup code is completed but just before the boot.py and then main.py scripts are executed, a frozen module called _boot.py is executed. It looks for a file named wifi_config.py which should contain the name of the wireless network and its password. If found, then an attempt is made to connect to the network and to start an FTP server. If that fails then the No Wifi setting found message is printed to the console. On failing or not, normal execution is resumed.

It may look as there is an egg and chicken conundrum: isn't a running FTP server needed to copy the credentials file to the W600-PICO which is in turn needed to connect to the Wi-Fi network and start the server? The answer is no. There are other ways of copying files using the serial link with the board as will be seen later. One of these can be done within the REPL. First I used a text editor on the desktop to create the following text file.

file = open('wifi_config.py', 'w') file.write('WIFI_SSID = "rosebud"' + '\r\n') file.write('WIFI_PASSWD = "87654321"' + '\r\n') file.close()

These four MicroPython commands create a file in write mode, write two lines of text to it and then close it. Of course the Wi-Fi name or SSID and its password will have to be adapted in actual use. Then I connected to the board with a serial link and then copied and pasted the short Python script to the REPL.

michel@hp:~$ cu -l /dev/ttyUSB1 -s 115200 Connected.
No Wifi setting found WinnerMicro W600 MicroPython v1.19.1-784-g913e397b1-dirty on 2022-12-15; WinnerMicro module with W600 Type "help()" for more information. >>>Ctrl-E paste mode; Ctrl-C to cancel, Ctrl-D to finish === file = open('wifi_config.py', 'w') === file.write('WIFI_SSID = "rosebud"' + '\r\n') === file.write('WIFI_PASSWD = "87654321"' + '\r\n') === file.close() === ===Ctrl-D 26 28

The wifi_config.py file was created alongside the boot.py and main.py files in the /flash directory where _boot.py will find it. Pressing the reset button on the board for a "hard" reset or pressing the CtrlD combination at the REPL prompt for a "soft" reset, we can see the typical outcome if everything goes well.

>>> connecting... connected, ip is 192.168.1.145 ftp server port is 21, username is root, password is root WinnerMicro W600 MicroPython v1.19.1-784-g913e397b1-dirty on 2022-12-15; WinnerMicro module with W600 Type "help()" for more information. >>>

In my humble opinion using this method to connect to the Wi-Fi network should be avoided except in the initial testing of new firmware when a serial connection to the board is in place. Close down the Wi-Fi network and start (or reset) the W600-PICO. The board will hang after displaying the connecting... message. Restart the network and even if the credentials in the wifi_config.py file are correct it will not connect. I think this scenario is pretty much guaranteed to occur when there's a power blackout because when power returns the microcontroller will boot much faster than a Wi-Fi router. The W600-PICO will be trapped in the connection loop and the only way to get out of it will require physical access to the board to either press the reset button or to turn off power to the board for a short while. There is another reason to avoid using the _boot.py connection. An FTP server with a dynamic IP address is not very practical although it is not impossible to find the assigned IP address as shown above. This problem is easily fixed in the boot.py script as will be seen in a later section.

Using the FTP Server toc

Using these credentials I was able to log into the FTP server with the command line ftp client that I assume was installed by default on my Linux Mint desktop.

michel@hp:~$ ftp ftp> open 192.168.1.145 Connected to 192.168.1.145. 220-= welcome on W600 FTP server =- 220 Name (192.168.1.145:michel): root 331 Password required for user Password: root not echoed 331 Password required for root Password: 230 User logged in Remote system type is UNIX. List all files and directories in the root directory of the board ftp> ls 200 Port Command Successful. 150 Opening Binary mode connection for file list. drwxrwxrwx 0 root root 0 Jan 1 2018 . drwxrwxrwx 0 root root 0 Jan 1 2018 .. -rwxrwxrwx 0 root root 139 Jan 1 2018 boot.py -rwxrwxrwx 0 root root 34 Jan 1 2018 main.py 226 Transfer complete. ftp>

I don't suppose many are familiar with the ftp utility. I had to consult its help command to bring back hazy memories of how to start an FTP session (open) and list files (ls) as shown above. To illustrate other basic commands, here is an FTP session with a board that contained directories and more files than what was shown above.

List all files and directories in the root directory of the board ftp> pwd 257 "/" is current directory. ftp> ls 200 Port Command Successful. 150 Opening Binary mode connection for file list. drwxrwxrwx 0 root root 0 Jan 1 2018 sys drwxrwxrwx 0 root root 0 Jan 1 2018 lib drwxrwxrwx 0 root root 0 Jan 1 2018 cert -rwxrwxrwx 0 root root 540 Jan 1 2018 boot.py -rwxrwxrwx 0 root root 3997 Jan 1 2018 main.py -rwxrwxrwx 0 root root 1720 Jan 1 2018 easyw600.py -rwxrwxrwx 0 root root 39 Jan 1 2018 secrets.py -rwxrwxrwx 0 root root 1517 Jan 1 2018 button.py 226 Transfert Complete. Navigate the board's file system ftp> cd lib 250 Changed to directory "//lib" ftp> pwd 257 "//lib/.." is current directory. ftp> cd / 250 Changed to directory "/" Execute desktop commands with ! ftp> !pwd /home/michel ftp> !ls top_level* top_level.txt Copy a file from the desktop to the board ftp> put top_level.txt local: top_level.txt remote: top_level.txt 200 Port Command Successful. 150 Opening binary mode data connection for "//top_level.txt". 226 Finished. 392 bytes sent in 0.00 secs (92.4891 kB/s) ftp> ls 200 Port Command Successful. 150 Opening Binary mode connection for file list. drwxrwxrwx 0 root root 0 Jan 1 2018 sys drwxrwxrwx 0 root root 0 Jan 1 2018 lib drwxrwxrwx 0 root root 0 Jan 1 2018 cert -rwxrwxrwx 0 root root 540 Jan 1 2018 boot.py -rwxrwxrwx 0 root root 3997 Jan 1 2018 main.py -rwxrwxrwx 0 root root 1720 Jan 1 2018 easyw600.py -rwxrwxrwx 0 root root 39 Jan 1 2018 secrets.py -rwxrwxrwx 0 root root 1517 Jan 1 2018 button.py -rwxrwxrwx 0 root root 392 Jan 1 2018 top_level.txt <-- there's the copied file 226 Transfert Complete. Delete a file is the board's file system ftp> delete top_level.txt 250 Successfully deleted file "//top_level.txt". Copy a file from the board to the desktop ftp> get secrets.py local: secrets.py remote: secrets.py 200 Port Command Successful. 150 Opening binary mode data connection for "//secrets.py" (39 bytes). WARNING! 2 bare linefeeds received in ASCII mode File may not have transferred correctly. 226 Finished. 39 bytes received in 0.00 secs (8.8325 kB/s) ftp> !ls secrets.py secrets.py <-- there's the copied file on the desktp Removing that copied file on the desktop ftp> !rm secrets.py ftp> !ls secrets.py ls: impossible d'accéder à 'secrets.py': Aucun fichier ou dossier de ce type Exiting the application ftp> quit 221 Bye! michel@hp:~$

Many more would probably prefer to log into the FTP server with Filezilla as described in section 5 of the MicroPython User Guide by Winner Micro. Here is how to "quickconnected" with the server, filing in the Host, Username and Password fields.

Filezilla Quick Connect

Note the warning about the absence of TLS encryption and the use of the default FTP port (i.e. port 21). On connecting Filezilla displayed a popup dialog warning that the password and files would be sent in cleartext. There was also a checkbox to allow insecure connections to this server in the future.

The server can also be defined in the application's site manager.

Filezilla Site Manager

Note the selection for the method of encryption, again there is none. Once the entry is created, connecting to the FTP server is done by selecting the board in the site manager and clicking on the Connect button.

It is also possible to connect to the FTP server with the Caja file manager (and probably others) so that the file system on the W600 shows up as a remote directory. To do this, go to a location as shown below.

Caja Go to Location

Enter the IP address of the board and specify the ftp:// protocol in the Go To: box as partly shown next.

Enter ftp address

Enter the user name and password required by the FTP server on the W600 board.

Enter password

The Remember forever item is not checked because I shun setting passwords permanently especially for connections that are mostly temporary, but that is a personal choice, of course. As can be seen below, the root directory of the W600 file system is now available as a networked drive.

W600 file system as a networked drive

When I open a script in the remote file system into an editor (done by clicking with the right mouse button and choosing Open with...) and modify the script, the changes are uploaded to the W600 after closing the editor. Once used to this, I found this approach to be a fluid work environment. The only thing that I could not do with this method was deleting a directory in the remote (W600) file system. This could be done with FileZilla.

Don't forget, setting up the FTP server is more than a programming environment; it is an over-the-air (OTA) mechanism. No need to add libraries and additional code as done with the ESP chips, although it will be necessary to modify the boot.py script to always enable the server. More on that later.

Using Thonny toc

Instead of relying on the FTP server, I use Thonny which is billed as a Python IDE for beginners and it has proven very useful when developing a new W600 project. The version in the Debian repository is rather old. The easiest way to get the current version of the IDE is to set up a virtual environment and then use pip to install the program.

michel@hp:~$ mkvenv thonny creating virtual environment /home/michel/thonny updating virtual environment /home/michel/thonny michel@hp:~$ ve thonny (thonny) michel@hp:~$ pip install thonny Collecting thonny ... Successfully installed Send2Trash-1.8.0 astroid-2.13.2 asttokens-2.2.1 dill-0.3.6 docutils-0.19 isort-5.11.4 jedi-0.18.2 lazy-object-proxy-1.9.0 mccabe-0.7.0 mypy-0.991 mypy-extensions-0.4.3 parso-0.8.3 platformdirs-2.6.2 pylint-2.15.10 pyserial-3.5 six-1.16.0 thonny-4.0.2 tomli-2.0.1 tomlkit-0.11.6 typing-extensions-4.4.0 wrapt-1.14.1 (thonny) michel@hp:~$ thonny

The last command launches the IDE. Note that we are in the thonny virtual environment which must be invoked whenever the program is started. A one-time screen to set initial settings is shown.

Initial settings

I accepted the defaut settings and pressed the Let's go! screen to continue with a minimal setup. Go to Options in the Tools menu and then select the Interpreter tab. One could also select Configure interpreter... in the Run menu.

Interpreter Options

Thonny can work with forks of MicroPython for specific devices, but the W600 is not among those. So set the kind of interpreter to MicroPython (generic). The IDE must also know which serial port to use to communicate with the W600-PICO. Depending on the situation, letting the IDE try to detect the port works well, but I have to specifically set the port when another serial device is plugged is using /dev/ttyUSB0. With those two settings, the IDE could be started by pressing the OK button.

IDE interface

The main parts of the IDE are a multi-tab editor and a Shell window underneath which is the REPL console. In addition there is a typical top-level menu and speed bar with buttons to execute common commands. One can go from the editor to the REPL prompt by clicking in the respective window. Edited files can be saved to the desktop computer or to the MicroPython device. Python scripts (.py extension) can be loaded from the flash memory of the board into the editor. The editor can also work with files from the desktop, but it can handle more file formats.

I would suggest checking the Files option in the View menu. That way it will be easier to manage files. If a dark theme is used on the desktop, and if it is difficult to see which items are checked in the View menu, then select a UI theme in the Theme & Font tab of the Tools/Options menu. Here is how have the IDE set up.

Editor and REPL

There is a odd quirk when starting the IDE. It reports that No Wifi setting found even if the wifi_config.py file is present. And checking in the REPL, it is obvious that the module is connected to the Wi-Fi network.

connecting... No Wifi setting found WinnerMicro W600 MicroPython v1.19.1-784-g913e397b1-dirty on 2022-12-15; WinnerMicro module with W600 Type "help()" for more information. >>> >>> import network >>> wlan = network.WLAN(network.STA_IF) >>> wlan.active() True >>> wlan.isconnected() True >>>

I verified with a ping from the desktop that the board was indeed connected. Note that I had to search in the list of connected devices on the Wi-Fi router to find the IP of the board.

michel@hp:~$ ping 192.168.1.205 -c 3 PING 192.168.1.205 (192.168.1.205) 56(84) bytes of data. 64 octets de 192.168.1.205 : icmp_seq=1 ttl=255 temps=149 ms 64 octets de 192.168.1.205 : icmp_seq=2 ttl=255 temps=165 ms 64 octets de 192.168.1.205 : icmp_seq=3 ttl=255 temps=188 ms --- statistiques ping 192.168.1.205 --- 3 paquets transmis, 3 reçus, 0 % paquets perdus, temps 2003 ms rtt min/moy/max/mdev = 149,435/167,682/188,117/15,867 ms

I will not go into more details about using Thonny; there are many guides on the Internet. There are other options. Some mention uPyCraft by DFRobot which does look quite similar to Thonny. While not even mentioned in the introduction, it seems that it is available for Linux assuming the prerequisites are in place (see the source code README.md file). Mu, a simple Python editor for beginner programmers appears to be a similar project worth investigating.

Blinking the LED Again toc

Let's blink the built-in LED with a delay function as already done in the REPL. This time, though, it is done as an obligatory blink.py script where timing is done with a delay function.

from machine import Pin from time import sleep_ms as delay print() print('Executing blink.py') print('------------------') print() RUNS = 5 # number of flash cycles to run CYCLES = 8 # number of quick flashes per cycle FLASH_TIME = 100 # 100 ms on/off time of quick flashes OFF_TIME = 1500 # 1500 ms off time between series of flashes LED_ON = 0 # LOW turns the built in LED ON LED_OFF = 1 # HIGH turns the built in LED OFF # Built in blue LED connected to pin labeled PA0 on the board Led = Pin(Pin.PA_00, Pin.OUT) # Ensure the built in LED is off to start (it should be after reset) Led.value(LED_OFF) for j in range(RUNS): print(j+1, '/', RUNS,' ', end='') for i in range(CYCLES): Led.value(LED_ON) print('ON ', end='') delay(FLASH_TIME) Led.value(not Led.value()) delay(FLASH_TIME) print('OFF.') delay(OFF_TIME) print("Done.")

It was cute on my part to rename the sleep_ms to delay which is the name of the similar function in Arduino. Beside that, the script is pretty straight forward. We have already seen that the on-board blue LED is connected to the I/O pin labelled PA0 and that the pin must be grounded to turn on the LED. The only other thing worth mentioning is that the blinking does not go on indefinitely so as to continue on after a while.

In the older version of this script that worked with MicroPython 1.10, the Pin constructor required three variables. So the Led object was instantiated with the following statement in the older script.
Led = Pin(Pin.PA_00, Pin.OUT, Pin.PULL_FLOATING) # v 1.10

In version 1.19, an output pin object can be created without the third parameter. If one wants to include the third parameter, then be aware that Pin.PULL_FLOATING is no longer defined but it can be replaced with Pin.PULL_NONE.

Led = Pin(Pin.PA_00, Pin.OUT) # v 1.19 # or Led = Pin(Pin.PA_00, Pin.OUT, Pin.PULL_NONE) # v 1.19

Running the blink Script toc

The blink script is ready to go, but how can it be executed? First the file must be uploaded to the W600-PICO file system. We have already seen that there are a number of ways of doing that: using FTP (either the command line ftp utility or the more ameanable FileZilla or integration of the W600 file system as a remote drive) or using Thonny (or its equivalent) with its file management functionality. Copy blink.py alongside boot.py and main.py. Do not forget the .py extension. MicroPython is not a Linux environment, the extension is mandatory.

Remember that MicroPython executes, in order, the boot.py and main.py scripts when the microcontroller boots. To run blink.py automatically when the board is powered up, the by default empty main.py script could be deleted and blink.py could be renamed main.py. Indeed one could have uploaded blink.py as main.py to achieve the same goal. Managing files (renaming and deleting them and so on) is easy with FileZilla or with the desktop file manager if the board's file system is mounted as a remote drive. It is not quite straight forward with Thonny or if in the REPL. One thing that can be done to help in the latter two cases is to import upysh.py. I think the name is an acronym of MicroPython shell.

>>> from upysh import * upysh is intended to be imported using: from upysh import * To see this help text again, type "man". upysh commands: clear, ls, ls(...), head(...), cat(...), newfile(...) cp('src', 'dest'), mv('old', 'new'), rm(...) pwd, cd(...), mkdir(...), rmdir(...) >>> <dir> lib 1066 blink.py 84 boot.py 17 main.py 132k free rm('main.py') >>> <dir> lib 1066 blink.py 84 boot.py 133k free mv('blink.py', 'main.py') >>> <dir> lib 84 boot.py 522 demo.py 133k free

Now the board has to be restarted to execute the script. A soft reset can be done from the REPL with the CtrlD keyboard shortcut. Alternatively, pressing the RST button on the board will initiate a hard reset.

MPY: soft reboot Not shown on hard reset No Wifi setting found Executing blink.py ------------------ 1 / 5 ON ON ON ON ON ON ON ON OFF. 2 / 5 ON ON ON ON ON ON ON ON OFF. 3 / 5 ON ON ON ON ON ON ON ON OFF. 4 / 5 ON ON ON ON ON ON ON ON OFF. 5 / 5 ON ON ON ON ON ON ON ON OFF. Done. MicroPython v1.19.1-784-g913e397b1-dirty on 2022-12-15; WinnerMicro module with W600 Type "help()" for more information. >>>

Of course, the on-board LED should turn on and off in rhythm to what is shown in the terminal.

It makes sense to modify the main.py script as done above when developing "production type" firmware that will be run permanently and automatically by the microcontroller board. For the purpose of this tutorial, I suggest that another approach be used. Instead of copying blink.py over main.py, modify the latter so that its content is a single line importing the blink module. Now there should be 3 files plus a directory on the board.

>>> <dir> lib 1066 blink.py 84 boot.py 17 main.py

The main.py script should contain the following line importing blink (without the .py extension).

import blink

Restart the board, and the script will be executed. Since there are many modules in this tutorial, it will be easier to modify the main.py script instead of playing around with renaming files. By the way this is the content of main.py in the GitHub repository linked to this tutorial.

import demo

The demo.py script imports a number of modules one after the other.

from os import uname print() print('===========') print('= demo.py =') print('===========') print() print("os.uname:") print(uname()) print() print("Ctrl-C to halt demonstration") # connect to network import customboot # network connection not needed import blink import blink2 import blink3 import blink4 import pollbutton import pollbutton2 import intrbutton # network connection needed import webserver import mqtttest import wifiswitch print() print() print('Back to REPL') print()

As can be seen there are a lot more blinkies in the set of demonstration scripts. That's because this first demonstration program can do only one thing, and blinking an LED is not very useful.

Blinking the LED, Again and Again toc

The second blink program avoids the time wasting delay function by using a hardware timer to toggle the state of the LED.

from machine import Pin from time import sleep_ms, ticks_ms import random print() print('Executing blink2.py') print('-------------------') print() RUNTIME = 15000 # flash for 15 seconds PERIOD = 250 # flash on/off time LED_ON = 0 # LOW turns the built in LED ON LED_OFF = 1 # HIGH turns the built in LED OFF # Built in blue LED connected to pin labeled PA0 on the board Led = Pin(Pin.PA_00, Pin.OUT) # Ensure the built in LED is off to start (it should be after reset) Led.value(LED_OFF) def LedToggle(): lv = 1 - Led.value() Led.value(lv) def RandomDelay(min, max): return min + (max - min)*random.random() start_time = ticks_ms() # overall timer led_ticks = ticks_ms() # Execute tasks that require random time and blink LED at same time print('LED should blink at constant rate for next {} seconds.'.format(int(RUNTIME/1000))) while True: if ticks_ms() - start_time > RUNTIME: break if ticks_ms() - led_ticks > PERIOD: LedToggle() led_ticks = ticks_ms() task_delay = int(RandomDelay(PERIOD/4, 3*PERIOD/4)) sleep_ms(task_delay) print('Did something requiring {} milliseconds'.format(task_delay)) Led.value(LED_OFF) start_time = ticks_ms() # overall timer led_ticks = ticks_ms() print('LED should blink at variable rate for next {} seconds.'.format(int(RUNTIME/1000))) while True: if ticks_ms() - start_time > RUNTIME: break if ticks_ms() - led_ticks > PERIOD: LedToggle() led_ticks = ticks_ms() task_delay = int(RandomDelay(3*PERIOD/4, 4*PERIOD)) sleep_ms(task_delay) print('Did something requiring {} milliseconds'.format(task_delay)) Led.value(LED_OFF) print('Done.')

Is it presumptuous to say that this is typical cooperative multitasking à la Arduino; the while True: at the end is the equivalent of the loop() function. What happens if some of the other tasks performed in the loop do not play nice? The second while True: shows that the LED will not blink at a fixed frequency if some tasks take longer to execute than the blink period. A classic way of handling this problem is to use a hardware timer as shown in the next script

from machine import Pin, Timer from time import ticks_ms, sleep_ms import random print() print('Executing blink3.py') print('-------------------') print() RUNTIME = 6000 # flash for 6 seconds PERIOD = 250 # flash on/off time LED_ON = 0 # LOW turns the built in LED ON LED_OFF = 1 # HIGH turns the built in LED OFF # Built in blue LED connected to pin labeled PA0 on the board Led = Pin(Pin.PA_00, Pin.OUT) # Ensure the built in LED is off to start (it should be after reset) Led.value(LED_OFF) # Create timer instance that will toggle the built in LED timer1 = Timer(1) # timer1 callback function def LedToggle(t): lv = 1 - Led.value() Led.value(lv) # function to initialize timer1 def StartLedBlinking(): print("Start blinking LED") # inititate a periodic timer which will exectute LedToggle after PERIOD ms timer1.init(period=PERIOD, mode=Timer.PERIODIC, callback=LedToggle) # function to stop timer1 def StopLedBlinking(): if Led.value() == LED_ON: print('OFF.') timer1.deinit() Led.value(LED_OFF) print("Stop blinking") def RandomDelay(min, max): return min + (max - min)*random.random() # Execute tasks that require random time while blinking the LED print('LED should blink at constant rate for next {} seconds.'.format(int(RUNTIME/1000))) start_time = ticks_ms() # overall timer StartLedBlinking() while True: if ticks_ms() - start_time > RUNTIME: break task_delay = int(RandomDelay(3*PERIOD/4, 4*PERIOD)) sleep_ms(task_delay) print('Did something requiring {} milliseconds'.format(task_delay)) print('Done.') StopLedBlinking() Led.value(LED_OFF)

No matter the length of time taken by any task performed in the loop, the LED blinks at its specified frequency. At the specified period, the hardware timer, timer1 here, interrupts the flow of the program and performs its callback function and once that is completed, the program resumes where it was interrupted by the timer. Unfortunately, this is not a mechanism that can always be used to achieve concurrency. As will be shown later, it seems that hardware timers have a lower priority than network functions meaning their functioning is suspended during blocking socket operations.

An idea popped in my head at the last minute. Could hardware PWM (pulse width modulation) be used to blink the LED? Turns out the answer is yes.

from machine import Pin, PWM from time import sleep_ms print() print('Executing blink4.py') print('-------------------') print() RUNTIME = 6000 # flash for 6 seconds # Built in blue LED connected to pin labeled PA0 on the board LED_ON = 0 # LOW turns the built in LED ON LED_OFF = 1 # HIGH turns the built in LED OFF # start blinking pwm0 = PWM(Pin(Pin.PA_00), channel=0, freq=4, duty=128) # do something else for i in range(int(RUNTIME/1000)): sleep_ms(1000) print('Second', i+1) # stop blinking pwm0.deinit() # Ensure the built in LED is off at end Pin(Pin.PA_00, Pin.OUT).on() # Equivalent to # Led = Pin(Pin.PA_00, Pin.OUT) # Led.value(LED_OFF)

It is harder to obtain the desired blinking rate with PWM. Just noodling about with the script seems to confirm that a period greater than 200 ms would be difficult to achieve, but this will require delving into the documentation to confirm the actual minimum frequency available. Interestingly, PWM is not suspended during blocking socket operations.

Monitoring a Button toc

Push button circuit This will be a first example where a third party module is used. In this case, it is MicroPython-Button by Ubi de Feo (udidefeo) which works primarly for temporary contact push buttons. This is a very good library because it takes care of debouncing without using timers and consequently there is no need to define debounce parameters. Normally open or normally closed buttons can be used. For this test, I decided to use a normally open push button with one pole connected to the PA1 button (as labelled on the board) and the other to 3.3 volts (the maximum that should ever be applied to an I/O pin). The I/O pin cannot be left floating which is what would happen when the push button is in its normal open state, so a "pull up" resistor (could be just about any value from 4.7K ohms to 100K or even 1000K) could be added to the circuit as shown at the top of the image to the right. However this is not necessary as the W600 does have internal pull up resistors. If I had wanted the I/O pin to be normally at 0 volts, then the wiring shown at the bottom could have been used, but in that case the external pull down resistor would have been necessary as the W600 does not have internal pull down resistors on its I/O pins (see a MicroPython forum reply by Robert Hammelrath (Roberthh)).

The button object constructor has three positional arguments.

  1. An I/O pin number. This must be the pin constant as defined in machine.Pin. The alias for the pin labelled PA1 on the board is Pin.PA_01. Alternatively 1 could be used as it is the literal pin number used by MicroPython, which is probably an index into a table of pin definitions.
  2. The I/O pin rest state when the button is not pressed, which is also the state in which the button should be when the object is created. Set to True if the I/O pin is connected to Vcc by an internal or external pull up resistor. Set to False if the I/O pin is connected to ground via an external pull down resistor.
  3. The callback function invoked when the button changes state.

There are also two keyword arguments.

If either internal_pulldown or internal_pullup is set to False, it is ignored. If both internal_pullup and internal_pulldown are set to True the latter (internal_pulldown) takes precedence. If neither internal_pullup and internal_pulldown are specified, then no internal pull resistor is activated and the I/0 pin must be pulled to Vcc or ground as appropriate by an external resistor and the positional rest state argument is used.

The callback has the following signature.

def button_change(button, event):

The button parameter will be the MicroPython pin number of the I/O pin connected to the button. In our example, the button parameter will be set to Pin.PA_01 = 1. The event parameter will be either "pressed" or "released" when the push button is first pressed and then when it is released and returns to its normal position.

MicroPython has recently added a package management module called mip which is similar to Python's pip. Unfortunately, I have not been able to use it with the W600 seemingly because of the lack of SSL/TLS support. Consequently, the library was installed manually as source code by first downloading to the desktop which can be done in a number of ways such as cloning the repository or downloading a zip archive of the repository. I chose to create a lib subdirectory in the directory that contains all the scripts for this project on my desktop. The Button.py MicroPython script could then be downloaded directly into that empty subdirectory.

michel@hp:~/Documents/MicroPython$ mkdir lib michel@hp:~/Documents/MicroPython$ cd lib michel@hp:~/Documents/MicroPython/w600-pico-2/lib$ wget https://raw.githubusercontent.com/ubidefeo/MicroPython-Button/main/Button.py ... Taille : 1281 (1,3K) [text/plain] Enregistre : «Button.py» 2023-01-25 00:20:09 (24,8 MB/s) - «Button.py» enregistré [1281/1281]

Copy the Button.py script into a lib directory in the W600 file system. Since I downloaded the script into a lib directory of my desktop, I just copied the desktop lib directory to the W600 file system and that took care of creating the directory on the board. This can be done in Thonny or with ftp or FileZilla etc.

MicroPython 1.10 could not find libraries stored in the lib/ directory, consequently Button.py was saved in the same directory as boot.py and main.py. This can also be done in version 1.19.

Here is a short script that uses that library to report when a normally open button is pressed and released and track the number of times it has been released.

from Button import Button # from https://github.com/ubidefeo/MicroPython-Button from machine import Pin # builtin from time import sleep_ms # builtin print() print('Executing pollbutton.py') print('-----------------------') print() BUTTON_PIN = Pin.PA_01 ACTIVE_LOW = True released_count = 0 def button_change(button, event): global released_count if event == Button.RELEASED: released_count += 1 print('Button {} {}. Count: {}'.format(button, event, released_count)) else: print('Button {} {}.'.format(button, event)) # Create a Button class instance if ACTIVE_LOW: button_one = Button(BUTTON_PIN, True, button_change, internal_pullup = True) else: # WARNING: the W600 does not have internal pull down resistor button_one = Button(BUTTON_PIN, False, button_change, internal_pulldown = True) # Run for 30 seconds. HALF_MINUTES = 1 # Time taken by other tasks OTHER_TIME = 50 # ms LOOPS = int(HALF_MINUTES*30*1000/OTHER_TIME) print('Setup completed, will run for {} seconds. Start pressing button.'.format(HALF_MINUTES*30)) for i in range(LOOPS): button_one.update() sleep_ms(OTHER_TIME) # do other tasks # fall back into REPL print("Done.")

The button change callback function reports both button pressed and button released events, and it keeps a count of the number of releases.

Setup completed, will run for 30 seconds. Start pressing button. event pressed Button released. Count: 1 event pressed Button released. Count: 2 event pressed Button released. Count: 3 event pressed Button released. Count: 4 ...

In the original version of this script, I stated that the external pullup resistor was needed at all times. I am sorry about that error. Clearly, my brain was not fully engaged at the time, everything about the active high vs active low button is backwards in the original script. A pull down resistor is needed if the push button shorts the input pin to Vcc as shown above in the second schema with a gray background.

Note how the button library is "pumped" with the button_one.update() method at the start of each iteration of the final loop. This may create a problem similar to the one encountered in blink2.py. Try running the script setting OTHER_TIME to 250 or 500 ms. Response to the button presses will feel sluggish. In that case, one solution is to call on Button.update()) at regular, constant, and short enough intervals with a timer and thus guarantee that the button remains responsive no matter what the other task do. This is done in pollbutton2.py.

from Button import Button # from https://github.com/ubidefeo/MicroPython-Button from machine import Pin, Timer # builtin from time import sleep_ms # builtin print() print('Executing pollbutton2.py') print('------------------------') print() BUTTON_PIN = Pin.PA_01 ACTIVE_LOW = True released_count = 0 def button_change(button, event): global released_count if event == Button.RELEASED: released_count += 1 print('Button {} {}. Count: {}'.format(button, event, released_count)) else: print('Button {} {}.'.format(button, event)) # Create a Button class instance if ACTIVE_LOW: button_one = Button(BUTTON_PIN, True, button_change, internal_pullup = True) else: # WARNING: the W600 does not have internal pull down resistor button_one = Button(BUTTON_PIN, False, button_change, internal_pulldown = True) # button_one will be updated every 50 ms seconds by timer1 BUTTON_PERIOD = 50 # ms # Create timer instance that will toggle the built in LED timer1 = Timer(1) # timer1 callback function def ButtonUpdate(t): button_one.update() # function to initialize timer1 def StartButton(): print("Start checking button state") # inititate a periodic timer which will exectute LedToggle after PERIOD ms timer1.init(period=BUTTON_PERIOD, mode=Timer.PERIODIC, callback=ButtonUpdate) def StopButton(): timer1.deinit() # Run for 30 seconds. HALF_MINUTES = 1 # Time taken by other tasks OTHER_TIME = 2500 # ms LOOPS = int(HALF_MINUTES*30*1000/OTHER_TIME) print('Setup completed, will run for {} seconds. Start pressing button.'.format(HALF_MINUTES*30)) StartButton() for i in range(LOOPS): sleep_ms(OTHER_TIME) # do other tasks print('{} ms task {} of {} completed'.format(OTHER_TIME, i+1, LOOPS)) StopButton() # fall back into REPL print("Done.")

Just like in the blink3.py script, a hardware timer is used to feed the button "pump" button.update(). As explained before, this means that button events will not be reported during blocking socket operations.

Anouar way to monitor a digital input is to enable hardware interrupts. Here is a rudimentary example.

from machine import Pin # builtin from time import ticks_ms # builtin print() print('Executing intrbutton.py') print('-----------------------') print() BUTTON_PIN = Pin.PA_01 interrupt_flag = 0 def pinISR(pin): global interrupt_flag interrupt_flag += 1 button_one = Pin(BUTTON_PIN, Pin.IN, Pin.PULL_UP) button_one.irq(trigger=Pin.IRQ_FALLING, handler=pinISR) # Run for milli seconds. RUNTIME = 15000 starttick = ticks_ms() print('Setup completed, will run for {} seconds. Start pressing button.'.format(RUNTIME/1000)) while ticks_ms() - starttick < RUNTIME: if interrupt_flag: print("Button pressed {} times".format(interrupt_flag)) interrupt_flag = 0 # fall back into REPL print("Done.")

It is ironic that a hardware interrupt is used to respond as quickly as possible to the button press or release, but polling is used to handle the event. We are usually told that interrupt service routines (ISR) should be very short and that it may be necessary to avoid calling on other functions that could not be reentrant. As an example of the problem of reentrancy, assume that pinISR contains the code print('In button ISR'). Assume that the button is pressed just as print('Current value: {}.somevar)), somewhere else in the code, is about to print 'v'. Finally, assume that the print function first builds up a complete string of what it must print, "Current value: 32.8943", and puts that string in a fixed static buffer. In that case, the content of the buffer will be changed to 'In button ISR' by the interrupt's print statement. When the original print statement resumes after the ISR is completed, its buffer will have been mangled and the final result will be something like the following.

Current in button ISR in button ISR

That would be a good result. If print calculated the length of the final string to print instead of testing for a terminal 0x00, things could go very badly indeed.

I have not looked at reentrancy in MicroPython in general and in the W60x port in particular. Note that reentrancy is only one problem linked to interrupts, see the full document cited above.

A Custom boot.py toc

So far, nothing has required access to the network. But the upcoming scripts will blink the LED remotely. So this is a good time to reconsider the whole question of establishing a connection with the local Wi-Fi network. Like many, I decided that this was the best place to connect to the Wi-Fi network and to start the FTP server in a safe way would be in the boot.py script that is automatically executed when the board is booted. The default boot.py script does very little.

# boot.py -- run on boot-up # can run arbitrary Python, but best to keep it minimal print("") print(" WinnerMicro W600") print("")

Instead of modifying the script itself, I wrote a customboot.py script which is saved in the root directory beside the boot.py module or in the lib/ directory. In a real setting I would then import that custom boot in boot.py.

import customboot

Again for the purpose of this tutorial, I did not do that and instead customboot is explicitly imported in demo.py or in a few other scripts that are not included in the demonstration script. Of course wifi_config.py should be removed so that the frozen module _boot.py does not try to connect to the network and possibly hang the microcontroller. Just like _boot.py, this script reads the network parameters from a file, named secrets.py. Here is an example, secrets_templates.py, that should be modified as needed and saved as secrets.py.

# Connection to Wi-Fi network CONNECT_TRIES = 5 # number of attempts to connect to Wi-Fi CONNECT_TIMEOUT = 20 # seconds to wait for a connection STA_SSID = "rosebud" STA_PSWD = "87654321" STA_FIXED_IP = "192.168.1.59" STA_SUBNET = "255.255.255.0" STA_GATEWAY = "192.168.1.1" STA_DNS = "192.168.1.1" # Access point parameters if connection as station not possible AP_UP_TIME = 600 # leaving AP on for 10 minutes line before reset AP_SSID = "W600_AP" # access point name AP_PSWD = None # access point password # FTP server created in station or access point mode FTP_PORT = 21 # ftp port 21 is default FTP_USER = "user" # ftp user name FTP_PWD = "12345678" # ftp password

When customboot cannot connect to the Wi-Fi network, it goes into an endless loop.

No Wifi setting found W600-PICO Boot Connect attempt 1... Connect attempt 2... Connect attempt 3... Connect attempt 4... Connect attempt 5... Could not connect to rosebud Access point w600AP started at IP 192.168.43.1 FTP server on port 21, username is user, password is 12345678 delay of many minutes Restarting No Wifi setting found W600-PICO Boot Connect attempt 1... Connect attempt 2... Connect attempt 3... Connect attempt 4... Connect attempt 5... Could not connect to rosebud Access point w600AP started at IP 192.168.43.1 FTP server on port 21, username is user, password is 12345678 delay of many minutes Restarting No Wifi setting found W600-PICO Boot Connect attempt 1... ...

A successful connection to the Wi-Fi network is the only way to break out of the loop. Hopefully, the access point will be reachable long enough to transfer corrected files via the FTP server to the board so that in the following restart it will connect to the Wi-Fi network. Here is a dialog showing how I updated the secrets.py file after the Wi-Fi network password had been changed and customboot could no longer connect. I waited until the access point came up, connected to w600AP network with my desktop machine and then started the ftp utility.

michel@hp:~$ ftp ftp> open 192.168.43.1 Connected to 192.168.43.1. 220-= welcome on W600 FTP server =- 220 Name (192.168.43.1:michel): user 331 Password required for user Password: 12345678 not echoed to screen 230 User logged in Remote system type is UNIX. ftp> lcd /home/michel/Documents/MicroPython/w600-pico-2/ Local directory now /home/michel/Documents/MicroPython/w600-pico-2 ftp> put secrets.py local: secrets.py remote: secrets.py 200 Port Command Successful. 150 Opening binary mode data connection for "/secrets.py". 226 Finished. 748 bytes sent in 0.00 secs (11.8891 MB/s) ftp> close 221 Bye! ftp> exit michel@hp:~$

Of course it would be easier to do this with FileZilla or another similar FTP client. Then I just waited until the board reset itself and, if the upload was successful, the connection to the Wi-Fi network was established. Here is customboot.py in action when everything goes smoothly.

No Wifi setting found W600-PICO Boot Connect attempt 1... Connect attempt 2... Success on attempt 2 after 8 seconds Connected to rosebud, IP is 192.168.1.59 FTP server on port 21, username is user, password is 12345678

The _boot.py module continues to complain that it cannot find wifi_config.py. When a connection to the Wi-Fi network is made, an FTP server is also created before the rest of the firmware (main.py) script is executed. That FTP server allows for easy over-the-air updating of the board's firmware. Because most OTA updates will be ignored unless the board is restarted, it may be a good idea to include a periodic system reset in main.py or somewhere else in the rest of the firmware.

Here is the source of customboot.py.

from time import sleep from network import WLAN, STA_IF, AP_IF, AUTH_OPEN, AUTH_WPA2_PSK_AES from machine import reset from w600 import run_ftpserver from secrets import * print("") print("W600-PICO Boot") print("") def startFTP(): run_ftpserver(port=FTP_PORT,username=FTP_USER,password=FTP_PSWD) print("FTP server on port {}, username is {}, password is {}".format(FTP_PORT, FTP_USER, FTP_PSWD)) wlan = WLAN(STA_IF) for attempt in range(CONNECT_TRIES): wlan.active(True) print('Connect attempt {}...'.format(attempt+1)) wlan.connect(STA_SSID, STA_PSWD) for i in range(CONNECT_TIMEOUT): if wlan.isconnected(): print("Success on attempt {} after {} seconds".format(attempt+1, i)) break # from inner loop else: sleep(1) if wlan.isconnected(): break # from outer loop else: wlan.active(False) if not wlan.isconnected(): print("Could not connect to {}".format(STA_SSID)) ap_if = WLAN(AP_IF) ap_if.active(True) if AP_PSWD is None: ap_if.config(essid=AP_SSID,authmode=AUTH_OPEN,channel=11) else: ap_if.config(essid=AP_SSID,password=AP_PSWD,authmode=AUTH_WPA2_PSK_AES,channel=11) print("Access point {} started at IP 192.168.43.1".format(AP_SSID)) startFTP() sleep(AP_UP_TIME) ap_if.active(False) print("Restarting") reset() # wlan connected if not STA_FIXED_IP == "0.0.0.0": wlan.ifconfig((STA_FIXED_IP, STA_SUBNET, STA_GATEWAY, STA_DNS)) print("Connected to {}, IP is {}".format(STA_SSID, wlan.ifconfig()[0])) startFTP()

Credit and thanks go to G21-Goose who creates an access point when the Wi-Fi network cannot be reached in the boot.py of his W600_Smart_Plug project.

Simple Web Server toc

We will create a simple web server that returns one of two pages as shown below. The first is the default page which can be used to control the state of the LED on the board by clicking on the Toggle button. The second page displays a 404 error when an unknown page URL is entered directly in the navigator's address bar and a button to return to the default page.

index web page 404 web page

The Quit button on the first page is a bit unusual. Click on it to close down the web server and terminate the Python script itself.

The code is spread over two modules. The webresponses.py script contains the function HttpPage(is404, Status) that builds the two pages which share a common <head> section with shared CSS styles and a common title. It also returns the response header for each type of page. The Status parameter is use to display the LED status (ON or OFF) above the Toggle button.

def HttpResponseHeader(is404): if is404: return 'HTTP/1.1 404 Not Found\n', 'Content-Type: text/html\n', 'Connection: close\n\n' else: return 'HTTP/1.1 200 OK\n', 'Content-Type: text/html\n', 'Connection: keep-alive\n\n' def HttpPage(is404, Status): # The web server will serve either an index.html page or a 404 error page. # Both pages share common elements htmlTop = """<!DOCTYPE html> <html> <head> <title>W600-PICO Web Server Example</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <link rel="icon" href="data:,"> <style> html{font-family: Helvetica; display:inline-block; margin: 0px auto; text-align: center;} h1{color: #0F3376; padding: 2vh;} p{font-size: 1.5rem;} .button{display: inline-block; background-color: blue; border: none; border-radius: 6px; color: white; font-size: 1.5rem; width: 5em; height: 3em; text-decoration: none; margin: 2px; cursor: pointer;} .button2{background-color: silver;} </style> </head> <body> <h1>W600-PICO Web Server Example</h1>""" htmlBottom = "</body></html>" if is404: html = htmlTop + """<h1 style="color: red">404 Error</h1><p><a href="/"><button class="button button2">Return</button></a></p>""" + htmlBottom print('404 response') else: html = htmlTop + """<p>LED: <strong>""" + ("ON" if Status else "OFF") + """</strong></p> <p><a href="/?led=toggle"><button class="button">Toggle</button></a></p> <p><a href="/quit"><button class="button button2">Quit</button></a></p>""" + htmlBottom print('index.html response') return html

The web server itself is just a reworked example found in many sites on the Web such as RANDOM NERD TUTORIALS. Most of these examples are for the ESP8266 or ESP32, but they work without modification with the W600-PICO.

from network import WLAN, STA_IF from machine import Pin import socket import webresponses print() print('Executing webserver.py') print('----------------------') print() LED_ON = 0 # LOW turns the built in LED ON LED_OFF = 1 # HIGH turns the built in LED OFF # Built in blue LED connected to pin labeled PA0 on the board Led = Pin(Pin.PA_00, Pin.OUT) Status = LED_OFF def SetLed(value): global Status Led.value(Status) Status = 1-value # inverse logic of built in LED # Ensure the built in LED is off to start (it should be after reset) SetLed(LED_OFF) wlan = WLAN(STA_IF) if wlan.isconnected(): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # Binding to all interfaces - server will be accessible to other hosts! s.bind(('0.0.0.0', 80)) s.listen(3) print('Web server started at http://' + wlan.ifconfig()[0]) print('Click on Quit button to stop the script') while True: conn, addr = s.accept() print() print('Client connection from {}'.format(addr)) request = conn.recv(1024) request = str(request) print('Resquest {}...'.format(request[0:31])) if request.find('/quit ') == 6: reply = 1 # quit elif request.find('/?led=toggle ') == 6: SetLed(Status) # toggle led reply = 0 # show index.html elif request.find('/ ') == 6: reply = 0 # show index.html else: reply = -1 # not found error rh1, rh2, rh3 = webresponses.HttpResponseHeader(reply < 0) #print('rh1: {}, rh2: {}, rh3: {}'.format(rh1, rh2, rh3)) conn.send(rh1) if reply < 1: conn.send(rh2) conn.send(rh3) conn.sendall(webresponses.HttpPage(reply < 0, Status)) conn.close() if reply == 1: break else: #wlan not connected print('Not connected... exiting to REPL')

Note that the script begins by testing that the board is connected to the network because otherwise there is no point in trying to start a web server. The first step is to create an IPv4 Internet socket of type stream meaning a TCP connection (see man socket on a Linux system or the socket -- Linux manual page). After that the socket is bound to IP address 0.0.0.0 on port 80. That means the socket will accept incoming requests from any IP address on port 80. The third line s.listen(3) puts an upper limit on the number of requests that can be queued. If the web server receives a fourth request while it is still handling three previous requests, it will return an ECONREFUSED indication.

Then the script enters an (almost) endless loop. It starts with a blocking function that waits for an HTTP request to come in. When it does, accept returns the pair (conn, addr) where conn is a socket and addr is its end point IP and port address. From there it's a matter of reading the incoming request, parsing it and acting in accordance with the request. The following shows the output printed to the serial terminal. It is possible to break out of the loop by clicking on the Quit button which will send a <ip</quit request to the web server.

Starting web server at http://192.168.1.59 Got a connection from ('192.168.1.103', 35740) Resquest b'GET / HTTP/1.1\r\nHost: 192.1... reply by sending the index page Got a connection from ('192.168.1.103', 57722) Resquest b'GET /?led=toggle HTTP/1.1\r\n... the user clicked on Toggle, so toggle the LED reply by sending the index page Got a connection from ('192.168.1.103', 59472) Resquest b'GET /unknown HTTP/1.1\r\nHost... the user entered an invalid URL reply by sending the 404 error page Got a connection from ('192.168.1.103', 41218) Resquest b'GET / HTTP/1.1\r\nHost: 192.1... the clicked on Return in the 404 page reply by sending the index page Got a connection from ('192.168.1.103', 41228) Resquest b'GET /quit HTTP/1.1\r\nHost: 1... the user clicked on Quit, break out of the loop Back to REPL

Comparing that output with the comments and print statements in the source should make the parsing and response logic evident.

There is another version of the script. It exploits a micropyton optimization wherein the socket returned by the socket.accept() can be used as a read and write stream. Below we only show the while True: loop of webserver2.py. Everything else is the same.

while True: conn, addr = s.accept() print("Connection from client", addr) # MicroPython socket objects support stream interface directly req = conn.readline() req = str(req) if SHOW_FULL_REQUEST: print("Request:") print(req) while True: h = conn.readline() if h == b"" or h == b"\r\n": break print(h) else: print("Request", req) if req.find('/quit ') == 6: reply = 1 # quit elif req.find('/?led=toggle ') == 6: SetLed(Status) # toggle led reply = 0 # show index.html elif req.find('/ ') == 6: reply = 0 # show index.html else: reply = -1 # not found error #print('reply', reply) rh1, rh2, rh3 = webresponses.HttpResponseHeader(reply < 0) conn.write(rh1) if reply < 1: conn.write(rh2+rh3+webresponses.HttpPage(reply < 0, Status)) conn.close() if reply == 1: break print()

This second version cannot be used "as is" in Python scripts.

Controlling an LED with MQTT toc

In this example, the LED on the W600-PICO will be turned on and off with MQTT messages. To give the impression that this example could be of actual use, the MQTT messages will be generated by a virtual switch in my Domoticz home automation server.

Domoticz virtual switch

When the light bulb icon is clicked, the virtual device will toggle its state and then send an MQTT message advertising the new state. The script will be subscribed to those messages from Domoticz and set the LED on the board accordingly. No need to worry, it is not necessary to install Domoticz to try this example as will be shown later. On the other hand, an MQTT broker must be available on the local network. I have Moquitto which is installed on a single-board computer (see 3. Installing the mosquitto MQTT Broker in Home Automation System on a Raspberry Pi and The Mosquitto MQTT Broker in Raspberry Pi OS (Bullseye)).

The MicroPython MQTT client module found in the micropython-lib repository can be used with the W60x microcontrollers with just one slight change.

import usocket as socket import ustruct as struct #from ubinascii import hexlify - not used! MD ...

Make sure to comment out the importing of the hexlify function from ubinascii. The frozen module by that name in the W60x port of MicroPython does not contain the function. Thankfully, those statements that use hexlify are for debug purposes and have all been commented out of the library. To install the library, create in folder lib a subdirectory named umqtt and then copy the script simple.py into the newly created directory. It is also necessary to provide information about the MQTT broker. Edit the file named mqttdata_template.py and save it as mqttdata.py in the /lib or root directory of the board.

# mqtt server parameters, 1 MB W600 does not support TLS/SSL MQTT_SERVER="192.168.1.22" MQTT_PORT=1883 MQTT_USER=None MQTT_PSWD=None MQTT_KEEPALIVE=0

Here is the module called mqtttest.py.

from network import WLAN, STA_IF from machine import Pin from time import sleep_ms, ticks_ms as ticks import json from umqtt.simple import MQTTClient from mqttdata import * print() print('Executing mqtttest.py') print('---------------------') print() # User modifiable parameters RUNTIME = 2*60*1000 # run demo for 2 minutes DOMO_IDX = 195 # Index of W600 virtual device # Constants LED_PIN = Pin.PA_00 # Built in LED connected to I/O pin PA0 LED_ON = 0 # LOW turns the built in LED ON LED_OFF = 1 # HIGH turns the built in LED OFF SUB_TOPIC = "domoticz/out" # MQTT topic to which Domoticz publishes messages wlan = WLAN(STA_IF) if not wlan.isconnected(): import customboot # Built in blue LED connected to pin labeled PA0 on the board Led = Pin(LED_PIN, Pin.OUT) Status = LED_OFF def SetLed(value): global Status print('SetLed(LED_{})'.format('ON' if (value == LED_ON) else 'OFF')) Status = value; Led.value(Status) # Ensure the built in LED is off to start (it should be after reset) SetLed(LED_OFF) # Create MQTT client using values from mqttdata.py mc = MQTTClient("umqtttest", MQTT_SERVER, port=MQTT_PORT, user=MQTT_USER, password=MQTT_PSWD, keepalive=MQTT_KEEPALIVE) print('Created MQTT client, server: {}, port: {}'.format(MQTT_SERVER, MQTT_PORT)) # MQTT callback on received messages on the subscribed topic def sub_cb(topic, msg): json_msg = json.loads(msg) if json_msg["idx"] == DOMO_IDX: print('Rx: topic {}, payload {}..., idx={}, nvalue={}'.format(topic, msg[0:16], json_msg['idx'], json_msg['nvalue'])) if json_msg["nvalue"] == 0: SetLed(LED_OFF) elif json_msg["nvalue"] == 1: SetLed(LED_ON) start_time = ticks() # overall timer wlan = WLAN(STA_IF) if wlan.isconnected(): print("Starting mqtt client & subscribing to", SUB_TOPIC); mc.set_callback(sub_cb) mc.connect() mc.subscribe(SUB_TOPIC) print() print("This script will run for {} minutes.".format(int(RUNTIME/60000))) print("Toggle the LED on/off with the Domoticz virtual switch.") try: while True: if ticks() - start_time > RUNTIME: break; mc.check_msg() sleep_ms(50) # need some delay otherwise this does not work finally: mc.disconnect()

Assuming the W600-PICO is connected to the network, that the MQTT broker is running, and that the MQTT parameters specified in the mqttdata.py configuration file are correct, then the following will be displayed in the serial terminal as the virtual button is activated.

Executing mqtttest.py --------------------- SetLed(LED_OFF) Created MQTT client, server: 192.168.1.22, port: 1883 Starting mqtt client & subscribing to domoticz/out self.server: 192.168.1.22, self.port: 1883, add: ('192.168.1.22', 1883) This script will run for 2 minutes. Toggle the LED on/off with the Domoticz virtual switch. Rx: topic b'domoticz/out', payload b'{\n\t"Battery" : 2'..., idx=195, nvalue=1 SetLed(LED_ON) Rx: topic b'domoticz/out', payload b'{\n\t"Battery" : 2'..., idx=195, nvalue=0 SetLed(LED_OFF)

The full MQTT message sent by Domoticz when turning off the LED is quite long.

{ "Battery" : 255, "LastUpdate" : "2023-01-27 15:18:53", "RSSI" : 12, "description" : "", "dtype" : "Light/Switch", "hwid" : "2", "id" : "00014113", "idx" : 195, "name" : "w600", "nvalue" : 0, "stype" : "Switch", "svalue1" : "0", "switchType" : "On/Off", "unit" : 1 }

As stated, one does not have a running instance of Domoticz to send an MQTT message and the message does not have to be that long. The mosquitto_pub utility, often installed along with an MQTT broker, can be used as shown next.

michel@hp:~$ mosquitto_pub -h 192.168.1.22 -t 'domoticz/out' -m '{"idx": 195, "nvalue":1}' michel@hp:~$ mosquitto_pub -h 192.168.1.22 -t 'domoticz/out' -m '{"idx": 195, "nvalue":0}'

The first command turns on the LED, the second turns it off.

A Wi-Fi Switch? toc

By adding a button as done before to this last MQTT example and by connecting another I/O pin to a relay module one can create a Wi-Fi switch that works with Domoticz. It would probably be relatively easy to modify the script to work with any other home automation system that supports the MQTT protocol.

Wi-Fi Switch schematic

To test this idea, I didn't actually connect a relay to the pin labelled PB12 but replaced it with an external LED that will be turned on when the pin is set high. I was happy to see that the external LED (i.e., the relay) could be toggled on and off with the push button switch and that both the on-board LED and the virtual switch in Domoticz correctly displayed the state of the relay. And, of course, it worked the other way. Clicking on the bulb icon of the Domoticz virtual switch caused the relay and on board LED to change their state. Here is the code.

from network import WLAN, STA_IF from machine import Pin from time import sleep_ms, ticks_ms as ticks import json from Button import Button # from https://github.com/ubidefeo/MicroPython-Button from umqtt.simple import MQTTClient # https://github.com/micropython/micropython-lib/tree/master/micropython/umqtt.simple from mqttdata import * wlan = WLAN(STA_IF) if not wlan.isconnected(): import customboot # Will be connected to the network from here print() print('Executing wifiswitch.py') print('-----------------------') print() # User modifiable parameters RUNTIME = 2*60*1000 # run demo for 2 minutes, 0 to run indefinitely DOMO_IDX = 195 # Index of W600 virtual device RELAY_PIN = Pin.PB_12 # I/O pin (PB12) connected to a normally open relay BUTTON_PIN = Pin.PA_01 # I/O pin (PA1) connected to normally open push button BUTTON_ACTIVE_LOW = True # other push button pole connected to ground LOCAL_CONTROL = True # The button controls the on board LED and Relay directly and updates Domoticz # if false the button publishes a message to Domoticz which # then replies with an MQTT message and it is this message that # will control the LED and relay. # Constants LED_PIN = Pin.PA_00 # Built in LED connected to I/O pin PA0 LED_ON = 0 # LOW turns the built in LED ON LED_OFF = 1 # HIGH turns the built in LED OFF SUB_TOPIC = "domoticz/out" # MQTT topic to which Domoticz publishes messages PUB_TOPIC = "domoticz/in" # MQTT topic to which Domoticz subscribes Status = LED_OFF # Assume Led is off def SetLed(value): global Status print('SetLed(LED_{})'.format('ON' if (value == LED_ON) else 'OFF')) Led.value(value) # Status = 1-value # inverse logic of built in LED Relay.value(Status) # Ensure the built in LED is off to start (it should be after reset) # and that the relay is open def button_change(button, event): global Status if event == Button.RELEASED: print('Button released, toggle LED') Status = 1 - Status; # toggle state if LOCAL_CONTROL: SetLed(1-Status) publish(Status) # update Domoticz # Create a Button class instance if BUTTON_ACTIVE_LOW: button_one = Button(BUTTON_PIN, True, button_change, internal_pullup = True) else: print('WARNING: the W600 does not have internal pull down resistor, an external resistor must be in place') button_one = Button(BUTTON_PIN, False, button_change, internal_pulldown = True) # Built in blue LED connected to pin labeled PA0 on the board Led = Pin(LED_PIN, Pin.OUT) Relay = Pin(RELAY_PIN, Pin.OUT) SetLed(LED_OFF) # assume the relay is open # Create MQTT client using values from mqttdata.py mc = MQTTClient("umqtttest", MQTT_SERVER, port=MQTT_PORT, user=MQTT_USER, password=MQTT_PSWD, keepalive=MQTT_KEEPALIVE) def publish(ledstatus): global mc if LOCAL_CONTROL: action = "On" if ledstatus else "Off" msg = '{"command": "switchlight", "idx":'+str(DOMO_IDX)+', "switchcmd": "' + action +'" }' else: action = '1' if ledstatus else '0' msg = '{"command": "udevice", "idx":'+str(DOMO_IDX)+', "nvalue": ' + action + ' }' mc.publish(PUB_TOPIC, msg) print("Tx: topic {}, payload {}".format(str(PUB_TOPIC), msg)) # MQTT callback on received messages on the subscribed topic def sub_cb(topic, msg): json_msg = json.loads(msg) if json_msg["idx"] == DOMO_IDX: print('Rx: topic {}, payload {}..., idx={}, nvalue={}'.format(topic, msg[0:16], json_msg['idx'], json_msg['nvalue'])) if json_msg["nvalue"] == 0: SetLed(LED_OFF) elif json_msg["nvalue"] == 1: SetLed(LED_ON) start_time = ticks() # overall timer print("Starting mqtt client & subscribing to", SUB_TOPIC); mc.set_callback(sub_cb) mc.connect() mc.subscribe(SUB_TOPIC) print() if RUNTIME > 0: print("This script will run for {} minutes.".format(int(RUNTIME/60000))) print("Toggle the LED on/off and the relay with the Domoticz virtual switch or the push button.") try: while True: if RUNTIME > 0 and ticks() - start_time > RUNTIME: break; mc.check_msg() button_one.update() sleep_ms(50) # need some delay otherwise it seems mc.check_msg gets overwhelmed finally: mc.disconnect()

And here is the serial output of the script.

Executing wifiswitch.py ----------------------- SetLed(LED_OFF) Starting mqtt client & subscribing to domoticz/out self.server: 192.168.0.45, self.port: 1883, add: ('192.168.0.45', 1883) This script will run for 2 minutes. Toggle the LED on/off and the relay with the Domoticz virtual switch or the push button. toggling the LED/Relay with the Domoticz virtual switch Rx: topic b'domoticz/out', payload b'{\n\t"Battery" : 2'..., idx=195, nvalue=1 SetLed(LED_ON) Rx: topic b'domoticz/out', payload b'{\n\t"Battery" : 2'..., idx=195, nvalue=0 SetLed(LED_OFF) toggling the LED/Relay with button with LOCAL_CONTROL = True Button released, toggle LED SetLed(LED_ON) Tx: topic domoticz/in, payload {"command": "switchlight", "idx":195, "switchcmd": "On" } Rx: topic b'domoticz/out', payload b'{\n\t"Battery" : 2'..., idx=195, nvalue=1 SetLed(LED_ON) Button released, toggle LED SetLed(LED_OFF) Tx: topic domoticz/in, payload {"command": "switchlight", "idx":195, "switchcmd": "Off" } Rx: topic b'domoticz/out', payload b'{\n\t"Battery" : 2'..., idx=195, nvalue=0 SetLed(LED_OFF) with LOCAL_CONTROL = False Button released, toggle LED Tx: topic domoticz/in, payload {"command": "udevice", "idx":195, "nvalue": 1 } Rx: topic b'domoticz/out', payload b'{\n\t"Battery" : 2'..., idx=195, nvalue=1 SetLed(LED_ON) Button released, toggle LED Tx: topic domoticz/in, payload {"command": "udevice", "idx":195, "nvalue": 0 } Rx: topic b'domoticz/out', payload b'{\n\t"Battery" : 2'..., idx=195, nvalue=0 SetLed(LED_OFF)

There's much more to Theo Arend's Tasmota firmware which is what is running on all the Wi-Fi switches in my home automation system. However, all communication between these IoT devices and the home automation software is done using the two above-mentioned MQTT topics. Consequently, it seems reasonable to conclude that the W600-PICO can be used as the basis of a Wi-Fi switch.

More? toc

To have something even modestly approaching a Wi-Fi switch running Tasmota, I tried to add the web server to wifiswitch.py but failed. Hints about the problem have already appeared. Run webserver3.py to see the problem explicitly. The socket.accept() function which waits for a connection from a remote client in the web server is blocking. That means that nothing happens until a connection is obtained. As the program shows, Timer objects and interrupts are suspended when accept() is waiting for a connection. Hours if not days or weeks can elapse between uses of the web interface rendering the Wi-Fi switch useless.

The obvious solution would be to use a non-blocking socket.accept. According to the latest MicroPython documentation, a time-out can be set on blocking socket operations. However [not] every MicroPython port supports this method. A more portable and generic solution is to use select.poll object. Unfortunately, I have not had success with either of these two approaches. Is this because of errors on my part, or is it a feature of the W60x port? Perhaps the threading version of the MicroPython for the W60x should be used. What about the use of the asyncio libraries that is often used for asynchronous modules?

While I should investigate this problem in more detail, it has not prevented me from using the W600-PICO in a role for which it is particularly well suited: as a wireless remote button in the TV room. Actually, three buttons are connected to the board as well as two external LEDS. The buttons activate home automation scenes. Whenever a button is pressed, positive feed back is given by a flashing green LED. The red LED flashes when the garage door is open which is quite useful as there is no way of seeing if that is the case from the house. This device has been running for months and is based on the old 1.10 MicroPython initially installed on the W600-PICO. I intend to add a temperature and humidity sensor and, perhaps, a tiny OLED to display the time. There may be another post about the board.

Up-to-date versions of all the scripts shown in this post are available in a GitHub repository: w600_micropython_1_19_examples.

A Signal Comment (2024-04-16) toc

Stewart Russel emailed a kind comment: If you find the inverted nature of the LED annoying, this is exactly what machine.Signal fixes. Luckily, there was more.

from machine import Pin, Signal LED = Signal(Pin(Pin.PA_00, Pin.OUT), invert=True)

This allows you to use LED.on(), LED.off() and LED.value() in a completely logical manner.

Of course, this had to be tried. Thonny is not yet installed on my recently updated desktop, so I resorted to the REPL and found that I had a W600 with a recent enough version of MicroPython on hand. So it was a simple matter to run the example.

MicroPython v1.19.1-1019-g84040f230-dirty on 2023-04-20; WinnerMicro module with W600 Type "help()" for more information. >>> from machine import Pin, Signal >>> LED = Signal(Pin(Pin.PA_00, Pin.OUT), invert=True) ... the LED is off >>> LED.on() ... the LED is on >>> print(LED.value()) 1 >>> LED.off() ... the LED is off >>> print(LED.value()) 0 >>> LED.value(1) ... the LED is on >>> print(LED.value()) 1 >>> LED.value(0) >>>

So that worked exactly as predicted by S. Russel. Being curious, I searched the MicroPython documentation and found class Signal – control and sense external I/O devices and looking at older versions determined that the class was introduced in version 1.9.

I managed to get myself a bit twisted around looking at that example, coming to the incorrect conclusion that Signal somehow modified the behaviour of Pin. In my mind that meant that the latter exposed the on value (normally 1) which Signal would change to 0. That is not the case, and the following proves it.

>>> from machine import Pin, Signal >>> pinled = Pin(Pin.PA_00, Pin.OUT) >>> pinled.value() 1 the LED is off >>> pinled.value(0) the LED is now on >>> pinled.value(1) the LED is now off >>> SigLED = Signal(pinled, invert=True) >>> pinled.on() the LED remains off - "inverted" logic still in effect >>> SigLED.on() the LED is now on >>> pinled.off() the LED remains on - "inverted" logic again >>> SigLED.off() the LED is now off >>> pinled.value() 1 the actual value of the Pin.PA_00 which turns off the LED >>> SigLED.value() 0 the logical value of the Signal pin that turns the LED off >>>

I plan on rewriting the scripts at a later date using this newly acquired knowledge. Thank you Stewart Russel. Readers should look at my fellow countryman's blog. It's quirky (as in delightful) and informative.

<-A First Look at the Winner Micro W600