Initial Budibase Documentation Lessons

Going from Budibase “Quickstart” to a basic understanding of application structure was a bigger step than I had expected, but I think I’m there now. Here are some notable lessons from my first (and second) passes through Budibase documentation with the caveatI wouldn’t know for sure I got this right until I get some hands-on app-building experience.

Blocks Are Not Components

When reading Budibase documentation on user interface elements, I came across terms like “Form”, “Form component”, and “Form block”. I thought they all referred to the same thing, that words “block” and “component” were synonyms. I was wrong, which caused a lot of confusion. A component in Budibase parlance is a single interface unit (example: textbox) and a block is a predefined composition built from multiple components to address common scenarios. (example: form block contains many textboxes.)

The ability to “Eject block” was mentioned in “Quickstart” but I didn’t understand what it meant at a time. I had understood it as a way to break up into smaller pieces so I can access its internals for customization, which should be great for learning how it was built under the hood. But most of the time when I wanted to look inside something I couldn’t find that “Eject” button. Eventually I figured out I had been trying to eject components and that’s why I got nowhere.

Data Provider, Repeater, and Binding

I’ve learned that things are a little more complex than ‘database stores data, components display data, binding connect them together.’ A few additional pieces are required. First is the fact we don’t want everything in the database all at once. For tasks like filtering or pagination, there are “data provider” components that fetch a desired subset from the database. The subset is still a collection of data, so a “repeater” component is deployed as an enumerator. Child components of a repeater will receive that data one row at a time, and that’s when UI components can use a binding to pick up the exact information they want. A hypothetical example:

  • Database: “Employees” table
  • Data provider: “Name” of first 50 employees sorted by name.
  • Repeater: for each of those employees…
  • Paragraph: display text of “name” binding.

When we use a Table block (like the auto-generated screens in the tutorial) these steps are handled automatically. But if we want to built tailored UI, implementing these details become our job.

App State

Sometimes an app will need to convey data between parts outside of the standard data flow hierarchy. (Provider, binding, etc.) I was curious how it was done and hit “eject” on an existing block to see its implementation details. The answer is app state, an application-wide key/value store. Or in other words: global variables!


With these lessons in mind, I am ready to start building my Budibase app to work with my existing data on personal transactions. And wow, it’s a mess.

Notes on PEP 492 Coroutines with async and await syntax

As I’m reading through several Python Enhancement Proposal (PEP) documents, I have to keep in mind that I don’t have to understand everything on my first read through. And I also have to keep a goal in mind so I don’t get too distracted. For this session, the motivation was copy/pasting “async with” from example code without having any idea what those keywords meant. PEP 380 was the last in my list of self-assigned prerequisite readings, but now it’s time for the main attraction: PEP 492 Coroutines with async and await syntax. After completing this study session, I have a much better understanding of what “async with” does.

It was interesting to follow Python evolution on this front. My prerequisite PEP reading all discussed generators and coroutines as a specific type of generator. In PEP 492 they laid out the reasons for separating out coroutines as its own concept. The underlying implementation of coroutines still uses many pieces of generator infrastructure, but the language will now treat them as different things. As part of this evolution, a large part of this document discussed is devoted to “Design Considerations” of alternative approaches, and “Transition Plan” for maintaining compatibility with pre-492 coroutine syntax.

One decision I found disappointing was that debugging features are disabled by default. I understand the motivation to ensure debugging features do not impact production code, but I think leaving them out completely is going too far. Beginners who most need feedback from Python runtime are not going to know they need to set an OS environment variable PYTHONASYNCIODEBUG.

But I am in full support of another decision: there’s no comprehension shorthand:

Syntax for asynchronous comprehensions could be provided, but this construct is outside of the scope of this PEP.

I don’t like Python comprehension shorthands and I’m glad there to see this PEP did not add one. It’s possible this text meant only exactly what it says, but in my professional career I’ve used “out of scope” as a polite rephrase of “I don’t like this idea and I’m not doing it.” It made me smile to think the PEP author might be doing the same here.

Reading through PEP 492 concludes this particular study session, and the knowledge informed updated goals for my MX340 CircuitPython project.

Notes on PEP 380 Syntax for Delegating to a Subgenerator

I’ve dipped my toes into writing Python code for asynchronous execution, and encountered a lot of new concepts that I felt I need to study up on. One of the keywords I wanted to understand better was “with“, which took me to context managers. Another item on the list of mystery was “yield from“. I recently learned “yieldin the context of coroutines, and I knew “from” in the context of loading Python modules, but they didn’t make sense together. Thankfully I found PEP 380 Syntax for Delegating to a Subgenerator, where my confusion was explained by the fact it had nothing to do with loading modules.

When I read up on coroutines, it took effort for my brain to absorb the concept of code execution interleaved between caller and callee. It was a foreign enough concept my brain didn’t flag a consequence of Python’s special treatment: the “yield” relationship is limited to one layer of interaction. What if we wanted to refactor a chunk of code, that included a “yield“, into another coroutine? Things quickly get messy. If only there’s a way to extend yield to multiple levels, and this is the problem PEP 380 wants to solve with “yield from“:

The rationale behind most of the semantics presented above stems from the desire to be able to refactor generator code.

Reading through the PEP, I was not happy to see “StopIteration” exception was used to convey a return value out of “yield from“. I was taught in the school of “exceptions should be exceptional” and here it is just a normal non-exceptional code return path. My initial reaction was tempered by learning StopIteration is how Python iterators (which are used all the time) signal a halt. I think my instinctive negativity came from experience with languages where exception handling incurs a significant performance overhead. Judging from what I’ve learned here, either Python exceptions incur no significant penalty or Python designers feel it is an acceptable cost.

For what it’s worth, I was not alone in my negative impression. Using StopIteration to convey return value was also disclosed under “Criticisms” sections of PEP 380 and dismissed as “without any concrete justification”. Shrug. But I was thankful another criticism was dismissed: looks like there was a suggestion to use syntax of “yield *” and I’m glad it didn’t go in that direction because it’d end up as another special syntax very difficult to look up. Searching on * would be a disaster as it is popularly used as a query wildcard character. From this perspective “yield from” is far superior and it only meant typing three more characters. I’m much happier with this approach and I’m glad to see it as I continue my PEP study session with PEP 492.

Notes on PEP 343 The “with” Statement

I don’t like Python shorthands that are so short, it leaves a beginner up a creek without any keywords they could use for searching and learning. But I’m OK with shorthands that clean up code while still leaving something for a beginner to look up. Such is the case with PEP 343 The “with” Statement. I’ve been using “with” in Python for a while, but never really sat down to understand what’s going on when I do. Thankfully there is a keyword I could use to find appropriate documentation.

My introduction was in the context of example #2:

with opened("/etc/passwd") as f:
    for line in f:
        print line.rstrip()

Opening a file for data operations, “with” guarantees all cleanup will also be handled behind the scenes. PEP 343 explains the problems it intended to solve, and explaining how this convenience is handled behind the scenes. There were two explanations that I could follow. The first explains using an implementation specified with a set of methods with special names “__enter__()” and “__exit__()“. I understood Python will look for these names under conditions specified in PEP 343. Then the same concept was rewritten in a way that built upon PEP 342: a context manager called upon via with can be implemented as a generator that calls “yield” at a single point within a “try/finally” block. This neatly packages all associated components together. Any setup code runs before “yield“. Within the “try” block, “yield” hands control over to client code. (In the above example, the “for” loop reading text in a file line by line.) Then code inside either “except:” or “finally:” can cleanup after client code completes.

I like this pattern, ensuring setup and cleanup code can be kept together while allowing other code to run in between them. While I have not yet fully absorbed Python generators, I think I understand enough of this particular application to appreciate it.

Coverage of this topic in the official Python tutorial is under “Predefined Clean-up Actions” within the “Errors and Exceptions” section. As appropriate for a tutorial, it focuses on how to use it and how it’s useful and leaves all the history and design thinking and implementation nuts and bolts to PEP 343.

Next lesson: what did it mean when I saw yield from” instead of just “yield?

Not A Fan Of Python Succinct Syntax

When learning about a Python feature like coroutines and generators, I found it instructive to flip back and forth between different ways a feature is represented. It’s nice to get the context of a feature and its evolution by reading its associated Python Enhancement Proposal, and it’s good to see how the official Python tutorial presents the same concept after the PEP process was all said and done. However, I want to take a side detour because the generator tutorial section was immediately followed by generator expressions and that made me grumpy.

Some simple generators can be coded succinctly as expressions using a syntax similar to list comprehensions but with parentheses instead of square brackets.

I am personally against such alternate syntax for the reason they are extremely hostile to people who don’t already know it. When I came across a generator and didn’t know what it was, I was able to search for keywords “yield” and “send” and get pointed in the right direction. But if someone comes across a generator expression or a list comprehension and didn’t know what it was, they have nothing to search on. The expression is enclosed by parentheses or square brackets, commonly used throughout the language. Inside the expression are normal Python syntax, and searching on “for” would just get to those features and not anywhere close to an explanation for generator expression or list comprehension.

I get that whoever pushed this through Python loved the option to “code succinctly” but my counter position is: No! Go type a few more characters. It won’t kill you, but it’ll be tremendously helpful to anyone reading your code later.

Here’s an excerpt from the list comprehension section of Python tutorial:

[(x, y) for x in [1,2,3] for y in [3,1,4] if x != y]

As a Python beginner, I had no idea what this syntax meant when I first saw it. I see two “for” loops, but this isn’t just a “for” loop, I see an “if” statement, but I didn’t know what that decision affected. (There’s no obvious “then” in this if/then.) Beyond those keywords, there are just variables and parentheses and square brackets. I had nothing to put into a search to point me towards “list comprehension”.

Python list comprehension tutorial said the above was equivalent to this:

combs = []
for x in [1,2,3]:
    for y in [3,1,4]:
        if x != y:
            combs.append((x, y))

If I didn’t understand this code, I could search “for” and learn that. Same with the “if“, and I can see the result of the decision affected whether the value was appended to a list. This is way more readable, it wasn’t even that much more typing.

Was list comprehension (and generator expression) worth adding to Python? I guess enough people in decision-making positions thought yes, but I disagree. I don’t understand why Python is considered beginner-friendly when horribly beginner-hostile things like this exist. I don’t think they should be in the language at all, but that ship has long since sailed. I can only shake my fist, yell at cloud, then return to my study.


Related: I made a similar rant on impossible to search JavaScript short hands

Notes on PEP 342 “Coroutines via Enhanced Generators”

I think I’ve worked my sparkly distraction out of my system, time to return to my Python study. This was motivated my CircuitPython experiments running on RP2040 microcontrollers. CircuitPython may be a reduced subset of Python, but it nevertheless incorporated many concepts that I have yet to grasp. Thus the study session, which dug through multiple PEP (Python Enhancement Proposal) design documents. Here are my notes after reading PEP 342 Coroutines via Enhanced Generators.

I was not familiar with coroutines, but I found a helpful explanation within Python glossary: “Coroutines are a more generalized form of subroutines. Subroutines are entered at one point and exited at another point. Coroutines can be entered, exited, and resumed at many different points.” I was familiar with functions, which has a single entry point and multiple exit points. (Each return is an exit.) When I read about “resume” in context, my first thought was of a function calling another. The parent caller function pauses execution waiting for the child callee function to run. Which is true, but Python coroutines have more points of interaction with each other. The caller can send additional data to the callee, which receives that data via yield. And they each maintain their internal state while this went on.

Why do we even want this? At surface I thought the same could be accomplished by standard nested loops, but example #2 in the PEP (JPEG contact sheet creator) helped me understand. Yes, maybe the pattern of execution could be replicated by nested loops, but that meant a single function has to track all variables involved in every nested loop. A coroutine, in contrast, can be written to encapsulate information for just one layer.

Here’s my pseudocode to replicate example #2 with nested loops:

for each page of contact sheet
  for each thumbnail on contact sheet
    for each JPEG
      generate thumbnail
    add thumbnail to contact sheet
  write page of contact sheet

If I want to process a bunch of JPEG differently, resulting in a different summary output JPEG, I would write a new function that has the different second loop but has the same innermost and outermost loops. With coroutines, I can get the same result by swapping out the thumbnail_pager coroutine and continue using the rest without making any changes to them or duplicate code.

I think I see the advantage here for independent code modules, but it’ll take a while for my brain to adapt and add this tool to my toolbox. During this transition period I’m likely to continue writing my code as nested loops. But at least this understanding helped me understand Python context managers. But before that, a complaint from grumpy Python student.

ESP8266 MicroPython Exception Handling Helps Robustness

I had to solve a few problems encountered publishing data to MQTT using ESP8266 MicroPython, running into MQTTException raised by the library. On the upside, dealing with MQTTException reminded me that I don’t usually have the luxury of exception handling on microcontrollers.

Exception handling in Python is my favorite part so far of using MicroPython on a microcontroller. I’m no stranger to calling APIs and checking error codes in typical C programming style and I can certainly work in that environment, but I do enjoy using a language like Python with exception handling mechanisms because it allows me to structure code in a way I find much more readable. This is important, especially for small projects where I don’t expect to look at the code on a regular basis. By the time I need to come back and modify the code months or years later, I’m looking at it with essentially fresh eyes. Comments are critical, but a good structure is very helpful too!

If I don’t have any exception handlers, an error would stop execution of my program and break into REPL awaiting diagnosis and repair. This is great while I’m developing the code, but I won’t want that later. During runtime I expect errors to be one of three types:

  1. Failing to connect to WiFi. This could happen if my WiFi router is in the middle of a firmware update, and for such harmless scenarios the best thing is to go to sleep and try again later.
  2. Failing to connect to MQTT broker. This could happen if I took down my Mosquitto docker container, again probably for an update.
  3. Failure to publish ADC data. This could happen if the WiFi router or Mosquitto went down in between connection and data publishing.

For all of these cases, the best thing to do is to try again later. Which for this project is actually the exact same thing I want to do even when everything is successful: go to sleep for a minute and repeat everything upon wake.

My first implementation caught all exceptions and proceeded to deep sleep for retry in one minute, but this is a problem: if I encounter a problem outside of the expected errors, or if I want to break into REPL for any other reason like updating the program with a new feature, I have only a very narrow window of time to do so. In fact, it was too fast for me to catch it awake!

So I actually want to do something different in case of error: keep the ESP8266 awake for 30 seconds or so. Long enough for me to connect a serial terminal and hit Control-C to break into REPL. I could trigger this path by taking down my Mosquitto docker container causing scenario #2 above.

This is an improvement over my first implementation, but I couldn’t upload my improved code. The ESP8266 wakes up, try to report ADC, and immediately go to deep sleep no matter what happens. After some time tearing my hair out trying to break into this narrow time window, I resorted to reflashing the ESP8266 with fresh MicroPython. Now I could actually get into REPL and upload the new code. It’s a good thing I keep these little code projects publicly accessible on GitHub where I could get a copy for my own use if I had to erase it.

I really like what I’ve seen of MicroPython so far, and it’ll definitely be a consideration for future projects. But for this project I’m changing course for no fault of MicroPython.

Second ESP8266 Voltage Monitor is Directly Wired to Buck Converter

Once I got my MicroPython ESP8266 connected to my home network, I expect to continue working with it over the network instead of an USB cable. Which meant it was time for me to take this development board and wire it to a DC voltage buck converter as I did earlier. However, this time I’m going to skip on the perforated prototype circuit board and going for direct wiring. (Sometimes called deadbug style due to folded pins and wires.)

But without the prototype board, I have to handle my own spacing. I cut up an expired credit card and placed the sheet of plastic in between Wemos D1 Mini clone (*) and its MP1584EN DC buck converter (*). Wires looped around the outside of this sheet to carry power lines 3.3V and GND, as well as the pair of 1 Megaohm resistors in series to ADC input pin for measuring voltage.

And relative to the previous iteration, I added one more wire: connecting ESP8266 GPIO16 (labeled D0 on a Wemos D1 Mini board) to the reset (RST) pin. This is required for an ESP8266 to wake from deep sleep, and this requirement is the very first sentence on MicroPython section for ESP8266 deep sleep. I’m going to guess that it is front and center because enough people forgot to do this critical step and their ESP8266 wouldn’t wake from sleep.

Once this package was tested to function over MicroPython WebREPL, I wrapped the whole thing up in clear heat shrink tube(*) (not pictured in title image) for a nice compact package. I could now query ADC value representing input voltage over WebREPL, but that’s not useful until I could report that value via MQTT.


(*) Disclosure: As an Amazon Associate I earn from qualifying purchases.

ESP8266 MicroPython Automatically Remembers WiFi

There were a few speed bumps on my way to a MicroPython interactive prompt, also known as REPL the read, evaluate, print loop. But once I got there, I was pretty impressed. It was much friendlier to iterative experimentation than Arduino on an ESP8266, because I don’t have to reflash and reboot every time. And since the ESP8266 has WiFi capabilities, getting REPL over the network (WebREPL) is even cooler. Now I can experiment while it runs on another power source, completely independent of USB for either power of data.

Before I got there, though, I needed to get this ESP8266 on my home WiFi network. By default, MicroPython sets up an access point for its own network so I need to turn “AP mode” off. Then I turn on “station mode” which allows connection to my WiFi router given its SSID and password.

import network

ap = network.WLAN(network.AP_IF)
ap.active(False)

sta = network.WLAN(network.STA_IF)
sta.active(True)
sta.config(dhcp_hostname='my hostname')
sta.connect('my wifi ssid','my wifi password')

I added one optional element: the dhcp_hostname parameter. This is the name shown to my router and probably other devices on my home network. If I don’t set this, the default name is “ESP-” followed by six hexadecimal digits of the ESP8266’s MAC address. That’s not a particularly memorable name so I wanted something I could remember and recognize.

And then, to my surprise, MicroPython remembered the network settings upon restart. I wrote a piece of Python code to perform this routine that I could run whenever I rebooted the board. But when I set out to test it by rebooting the board, it automatically reconnected to WiFi. This tells me a successful WiFi connection would cause a write to flash memory, which implies I should not run my WiFi connection code upon every startup. I expect to make this board go to deep sleep frequently and, if it writes WiFi information to flash every time it wakes up, I will quickly wear out the flash.

But that is just a hypothesis. As MicroPython is an open source project, it should be possible for me to dig into the code and figure out exactly when MicroPython writes WiFi connection information to flash. Perhaps it isn’t as bad as I feared it would be. Until then, however, I will hold off running my WiFi connection script.

A downside of not running my script is the DHCP hostname, which is not remembered upon reboot and this board reverted back to the default ESP-prefix name. But I can live with that for now, the next step is to set up my hardware for playing with deep sleep under battery power.

A Few Speed Bumps on the Road to ESP8266 MicroPython

I decided to play with MicroPython on an ESP8266 and started with MicroPython documentation page appropriately titled Quick reference for the ESP8266. It was almost (but not entirely) smooth sailing with the inexpensive Wemos D1 Mini clone(*) I had on hand.

I had recently switched desktop computers, with a fresh installation of Windows, so everything had to be reinstalled. Starting with Python, since I need that to run esptool tool to flash Espressif devices. It got its own virtual Python environment with venv and I could start working with the ESP8266.

I verified that flash size matched 4MB as per Amazon product listing with esptool.py --port COM4 flash_id

Then the first step in MicroPython directions: erase whatever might be in flash: esptool.py --port COM4 erase_flash

Followed by flashing the board with MicroPython, version 1.17 was the latest as of this writing: esptool.py --port COM4 --baud 460800 write_flash --flash_size=detect 0 esp8266-20210902-v1.17.bin

esptool.py v3.1
Serial port COM4
Connecting....
Detecting chip type... ESP8266
Chip is ESP8266EX
Features: WiFi
Crystal is 26MHz
Uploading stub...
Running stub...
Stub running...
Changing baud rate to 460800
Changed.
Configuring flash size...
Auto-detected Flash size: 4MB
Flash will be erased from 0x00000000 to 0x0009afff...
Flash params set to 0x0040
Compressed 633688 bytes to 416262...
Wrote 633688 bytes (416262 compressed) at 0x00000000 in 9.4 seconds (effective 537.1 kbit/s)...
Hash of data verified.

Leaving...
Hard resetting via RTS pin...

That looked good! But I thought I’d verify anyway: esptool.py --port COM4 --baud 460800 verify_flash --flash_size=detect 0 esp8266-20210902-v1.17.bin

esptool.py v3.1
Serial port COM4
Connecting....
Detecting chip type... ESP8266
Chip is ESP8266EX
Features: WiFi
Crystal is 26MHz
Uploading stub...
Running stub...
Stub running...
Changing baud rate to 460800
Changed.
Configuring flash size...
Auto-detected Flash size: 4MB
Flash params set to 0x0040
Verifying 0x9ab58 (633688) bytes @ 0x00000000 in flash against esp8266-20210902-v1.17.bin...
-- verify OK (digest matched)
Hard resetting via RTS pin...

This all looked good, but during this process I found communication with my board was unreliable. Occasionally I would fail to connect:

esptool.py v3.1
Serial port COM4
Connecting........_____....._____....._____....._____....._____....._____....._____

A fatal error occurred: Failed to connect to Espressif device: Invalid head of packet (0x08)

The frustrating part is that I don’t know what causes this, all I could do is retry until it worked. I didn’t notice anything I did differently between the times that worked and the times that failed. Is it the ESP8266? Is it the CH340 serial port bridge? Is it my USB cable? I can’t tell. The good news with MicroPython is that, once it is flashed, I could work via serial port without further headaches with esptool.

I remembered that PlatformIO Visual Studio Code had a serial port monitor, and it was indeed able to connect. But as the name stated, it was only a monitor and while I could see a MicroPython prompt I couldn’t type any commands back. Looking around Visual Studio extension marketplace I found a serial terminal extension published by Nordic Semiconductor. This allowed me to type commands into the MicroPython prompt and verify it worked, but frustratingly I could not copy/paste in this terminal. So much for a modern integrated environment! I returned to trusty old PuTTY for my MicroPython serial terminal needs and got to work.


(*) Disclosure: As an Amazon Associate I earn from qualifying purchases.

Notes on Codecademy “Build Deep Learning Models with TensorFlow”

Once I upgraded to a Codecademy Pro membership, I started taking courses from its Python catalog with the goal of building a foundation to understand deep learning neural networks. Aside from a few scenic detours, most of my course choices were intended to build upon each other to fulfill what I consider prerequisites for a Codecademy “Skill Path”: Build Deep Learning Models with TensorFlow

This was the first “Skill Path” I took, and I wasn’t quite sure what to expect as Codecademy implied they are different than the courses I took before. But once I got into this “skill path”… it feels pretty much like another course. Just a longer one, with more sessions. It picked up where the “Learn the Basics of Machine Learning” course left off with neural perceptrons, and dived deeper into neural networks.

In contrast to earlier courses that taught various concepts by using them to solve regression problems, this course spent more time on classification problems. We are still using scikit-learn a lot, but as promised by the title we’re also using TensorFlow. Note the course work mostly stayed in the Keras subset of TensorFlow 2 API. Keras used to be a separate library for making it easier to work with TensorFlow version 1, but it has since been merged into TensorFlow version 2 as part of the big revamp between versions.

I want to call attention to an item linked as “additional resources” for the skill path: a book titled “Deep Learning with Python” by François Chollet. (Author, or at least one of the primary people, behind Keras.) Following various links associated with the title, I found that there’s since been a second edition and the first chapter of the book is available to read online for free! I loved reading this chapter, which managed to condense a lot of background on deep learning into a concise history of the field. If the rest of the book is as good as the first chapter, I will learn a lot. The only reason I haven’t bought the book (yet) is that, based on the index, the book doesn’t get into unsupervised reinforcement learning like the type I want to put into my robot projects.

Back to the Codecademy course…. err, skill path: we get a lot of hands-on exercises using Keras to build TensorFlow models and train them on data for various types of problems. This is great, but I felt there was a significant gap in the material. I appreciated learning that different loss functions and optimizers will be used for regression versus classification problems, and we put them to work in their respective domains. But we were merely told which function to use for each exercise, the course doesn’t go into why they were chosen for the problem. I had hoped that the Keras documentation Optimizers Overview page would describe relative strengths and weaknesses of each optimizer, but it was merely a list of optimizers by name. I feel like such a comparison chart must exist somewhere, but it’s not here.

I didn’t quite finish this skill path. I lost motivation to finish the “Portfolio Project” portion of the skill path where we are directed to create a forest cover classification model. My motivation for deep learning lies in reinforcement learning, not classification or regression problems, so my attention has wandered elsewhere. At this point I believe I’ve exhausted all the immediately applicable resources on Codecademy as there are no further deep learning material nor is there anything on reinforcement learning. So I bid a grateful farewell to Codecademy for teaching me many important basics over the past few months and started looking elsewhere.

Notes on Codecademy Intermediate Python Courses

I thought Codecademy’s course “Getting Started Off Platform for Data Science” really deserved more focus than it did when I initially browsed the catalog, regretting that I saw it at the end of my perusal of beginner friendly Python courses. But life moves on. I started going through some intermediate courses with an eye on future studies in machine learning. Here are some notes:

  • Learn Recursion with Python I took purely for fun and curiosity with no expectation of applicability to modern machine learning. In school I learned recursion with Lisp, a language ideally suited for the task. Python wasn’t as good of a fit for the subject, but it was alright. Lisp was also the darling of artificial intelligence research for a while, but I guess the focus has since evolved.
  • Learn Data Visualization with Python gave me more depth on two popular Python graphing libraries: Matplotlib and Seaborn. These are both libraries with lots of functionality so “more depth” is still only a brief overview. Still, I anticipate skills here to be useful in the future and not just in machine learning adventures.
  • Learn Statistics with NumPy was expected to be a direct follow-up to the beginner-friendly Statistics with Python course, but it was not a direct sequel and there’s more overlap than I thought there’d be. This course is shorter, with less coverage on statistics but more about NumPy. After taking the course I think I had parsed the course title as “(Learn Statistics) with NumPy” but I think it’s more accurate to think of it as “Learn (Statistics with NumPy)”
  • Linear Regression in Python is a small but important step up the foothills on the way to climbing the mountain of machine learning. Finding the best line to fit a set of data teaches important concepts like loss functions. And doing it on a 2D plot of points gives us an intuitive grasp of what the process looks like before we start adding variables and increasing the number of dimensions involved. Many concepts are described and we get exercises using the scikit-learn library which implements those algorithms.
  • Learn the Basics of Machine Learning was the obvious follow-up, diving deeper into machine learning fundamentals. All of my old friends are here: Pandas, NumPy, scikit-learn, and more. It’s a huge party of Python libraries! I see this course as a survey of major themes in machine learning, of which neural networks was only a part. It describes a broader context which I believe is a good thing to have in the back of my head. I hope it helps me avoid the trap of trying to use neural nets to solve everything a.k.a. “When I get a shiny new hammer everything looks like a nail”.

Several months after I started reorienting myself with Python 3, I felt like I had the foundation I needed to start digging into the current state of the art of deep learning research. I have no illusions about being able to contribute anything, I’m just trying to learn enough to apply what I can read in papers. My next step is to learn to build a deep learning model.

Notes on Codecademy “Getting Started Off Platform for Data Science”

I like Codecademy’s format of having a bit of information that is followed immediately by an opportunity to try it myself. I like learn-by-doing as a beginner, even if the teaching/learning environment can be limited at times. But one thing that I didn’t like was the fact if I am to put my Python knowledge to use, I would have to venture outside of the learning environment and Codecademy didn’t used to provide information how.

The Learn Python 3 course made effort to help students work outside of the Codecademy environment with “Off-Platform Project”. These came in the form of Jupyter notebooks that I could download, and a page with some instructions on how to use them: a link to Codecademy’s command line course, a link to instructions for installing Python on my own computer, and a link on installing Jupyter notebooks. It’s a bit scattered.

What I didn’t know at the time was that Codecademy had already assembled an entire course covering these points. Getting Started Off Platform for Data Science is an orientation for everyone as we eventually venture off Codecademy’s learning platform. It starts with an introduction to the command line, then Python development tools like Jupyter Notebooks and other IDEs, wrapping up with an introduction to Github. This is great! Why didn’t they put more emphasis on this earlier? I think it would have been super helpful to beginners.

Though admittedly, I didn’t follow those installation instructions anyway. Python isn’t very good about library version management and the community has sidestepped the issue by using virtual environments to keep Python libraries separated in different per-project worlds. I’ve used venv and Anaconda to do this, and recently I’ve also started playing with Docker containers. For my own trip through Codecademy’s off-platform projects using Jupyter notebooks, I ran Jupyter Lab using their jupyter/datascience-notebook Docker image. That turned out to be sheer overkill and I probably could have just used the much lighter-weight jupyter/base-notebook image.

In hindsight I think it would have been useful for me to review Getting Started Off Platform for Data Science before I started reorienting myself with Python. I wouldn’t have followed it by the letter, but it had information that would have been useful beforehand. But as fate had it, it became the final course I took in the beginner-friendly section before I started trying intermediate courses.

Codecademy Beginner Friendly Python Fields

Once Codecademy got me reoriented with the Python programming language, I looked at some of their other beginner-friendly courses under the Python umbrella. I wanted to get some practice using Python, but I didn’t want to go through exercises for the sake of exercises. I wanted to make some effort at keeping things focused on my ultimate goal of learning about modern advances in machine learning.

  1. Learn Data Analysis with Pandas was my first choice, because I recognized “Pandas” as the name of a popular Python library for preparing data for machine learning. Making it relevant to the direction I am aiming for. The course title has “Data Analysis” and not “Machine Learning” but that was fine because it was only an introduction to the library. Not enough to get into field-specific knowledge, but more than enough to teach me Pandas vocabulary so I could navigate Pandas references and find my own answers in the future.
  2. How to Clean Data with Python followed up with more examples of Pandas in action. Again the course is nominally focused for data analytics but all the same concepts apply to cleaning data before feeding into machine learning algorithms.
  3. Exploratory Data Analysis in Python is a longer course with more ways to apply Pandas, including a machine learning specific section. Relative to other courses, this one is heavy on reading and light on hands-on practice, a consequence of the more general nature of the topic. And finally, this course let me dip my toes in another popular Python library I wanted to learn: NumPy.
  4. Learn Statistics with Python was how I dove into NumPy waters. After barely skating by some statics and number crunching in the previous course, I wanted a refresher in basic statistics. Alongside the refresher I also learn how to calculate common statistics using the NumPy library. And after the statistics calculations are done, we want to visualize them! Enter yet another popular Python library: matplotlib.
  5. Probability is the natural course to follow a refresher in basic statistics. They cover only the most basic and common applications of statistics and probability for data analysis, we’re on our own to explore in further depth outside of the class. I anticipate probability to play a role in machine learning, as some answers are going to be vague with room for interpretation. I foresee a poor (or misleadingly confident) grasp of probability will lead me astray.
  6. Differential Calculus was a course I poked my head into. I remembered it was quite a complex subject in school and was surprised Codecademy claimed anyone could learn it in two hours. It turns out the course should be more accurately titled “an introduction to numpy.gradient()“. Which… yes, it is a numerical application of differential calculus but it is definitely not the entirety of differential calculus. I guess it follows the trend of these courses: overly simplfied titles that skim the basics of a few things. Teach just enough for us to learn more on our own later.
  7. Linear Algebra starts to get into Python code that has direct relevance to machine learning. I know linear regression is a starting point and I knew I needed an introduction to linear algebra before I could grasp how linear regression algorithms work.
  8. Learn How to Get Started with Natural Language Processing was a disappointment to me, but it was not the fault of the course. It’s just that the machine learning systems in this field aren’t usually reinforcement learning systems. Which was the subfield of machine learning that most interested me. At least the course was short, and taught me enough so I know to skip other Codecademy natural language courses for myself.

The final Codecademy “Beginner friendly” Python course I took was titled “Getting Started Off Platform for Data Science.” I don’t think Codecademy put enough emphasis on this one.

Getting Reacquainted with Python via Codecademy

A few years ago I started learning Python and applied that knowledge to write control software for SGVHAK rover. I haven’t done very much with Python since, and my skills have become rusty. Since Python is very popular in modern machine learning research, a field that I am interested in exploring, I knew I had to get back to studying Python eventually.

I remember that I enjoyed learning Python from Codecademy, so I returned to see what courses had been added since my visit years ago. The Codecademy Python catalog has definitely grown, and I was not surprised to see most of it are only accessible to the paid Pro tier. If I want to make a serious run at this, I’ll have to pay up. Fortunately, like a lot of digital content on the internet, it’s not terribly difficult to find discounts for Codecademy Pro. Armed with one of these discount venues, I upgraded to the Pro tier and got to work. Here are some notes on a few introductory courses:

  • Learn Python 2 was where I started before, because SGVHAK rover used RoboClaw motor controllers and their Python library at the time was not compatible with Python 3. I couldn’t finish the course earlier because it was a mix of free and Pro content, and I wasn’t a Codecademy Pro subscriber at the time. I’m not terribly interested in finishing this course now. Python 2 was officially history as of January 1st, 2020. The only reason I might revisit this course is if I tackle the challenge of working in an old Python 2 codebase.
  • Right now I’m more interested in the future, so for my refresher course I started with Learn Python 3. This course has no prerequisites and starts at the very beginning with printing Hello World to the console and building up from there. I found the progression reasonable with one glaring exception: At the end of the course there were some coding challenges, and the one regarding Python classes required students to create base classes and derived classes. Problem: class inheritance was never covered in course instructions! I don’t think they expected students to learn this on their own. It feels like an instruction chapter got moved to the intermediate course, but its corresponding exercise was left in place. Other than that, the class was pretty good.
  • Inheritance and other related concepts weren’t covered until the “Object-Oriented Programming” section of Learn Intermediate Python 3, which didn’t have as smooth or logical of a progression. It felt more like a grab-bag of intermediate concepts that they decided to cut out of the beginner course. This class was not terrible, but it did diminish the advantage of learning through Codecademy versus reading bits and pieces on my own. Still, I learned a lot of useful bits about Python that I hadn’t known before. I’m glad I spent time here.

With some Python basics down — some I knew from before and some that were new to me — I poked around other beginner-friendly Codecademy Python courses.

Reworking Sawppy Ackermann Math in a Jupyter Notebook

The biggest difference between driving Sawppy and most other robotic platforms is the calculation behind operating the six-wheel-drive, four-wheel-steering chassis. Making tight turns in such a platform demands proper handling of Ackermann steering geometry calculations. While Sawppy’s original code (adapted from SGVHAK rover) was functional, I thought it was more complex than necessary.

So when I decided to rewrite Sawppy code for ROS Melodic (since abandoned) I also wanted to rework the math involved. I’ve done this a few times, most recently to make the calculations in C for an Arduino implementation of Sawppy control code, and it always starts with a sketch on paper so I can visualize the problem and keep critical components in mind.

Once satisfied with the layout on paper, I translate them into code. And as typically happens, that code would not work properly on the first try. The test/debug/repeat loop is a lot more pleasant in Python than it was in C, so I was happy to work with the tools I knew. But if the iterative process was even faster, I was convinced I could write even better code.

Thus I had my first real world use of a Jupyter notebook: my Sawppy Python Ackermann code. I could document my thinking in Markdown right alongside the code, and I could test ideas for simplification right in the notebook and see their results in numerical form.

But I’m not limited to numerical form: Jupyter notebooks can access a tremendous library of data visualization tools. It was quite overwhelming to wade through all of my options, I ended up using matplotlib‘s quiver plot. It plots a 2D field of arrows, and I used arrow direction to represent steering angle and arrow length to represent rolling speed. This plot gave a quick visual confirmation those numbers made sense.

In the Jupyter notebook I could work freely without worrying about whether I was adhering properly to style guides. It made the iterative work faster, but that did mean spending time to rework the code to satisfy wemake style guides. The basic logic remains identical between the two implementations.

I think this calculation is better than what I had used on SGVHAK rover, but it feels like there’s still room for improvement. I don’t know exactly how to improve just yet, but when I have ideas, I know I can bring up the Jupyter notebook for some quick experiments.

Inviting wemake to Nitpick My Python Code Style

I’m very happy Rhys Mainwaring released a ROS Melodic software stack for their Curio rover, a sibling of my Sawppy rover. It looks good, so I’ve abandoned my own ROS Melodic project, but not before writing down some notes. Part 1 dealt with ROS itself, many of which Rhys covered nicely. This post about Python Style is part 2, something I had hoped to do for the sake of my own learning and I’ll revisit on my next Python project. (Which may or may not be a robotic project.)

The original motivation was to get more proficient at writing Python code that conforms to recommended best practices. It’s not something I can yet do instinctively, so every time I tackle a new Python project I have to keep PEP8 open in a browser window for reference. And the items not explicitly covered by PEP8 are probably covered by another style guide like Google’s Python style guide.

But the rules are useless without enforcement. While it’s perfectly acceptable for a personal project to stop with “looks good to me” I wanted to practice going a step further with static code analysis tools called “linter“s. For PEP8 rules, the canonical linter is Flake8 which is a Python source code analysis tool packaged with a set of default rules for enforcing PEP8. But as mentioned earlier, PEP8 doesn’t cover everything, so Flake8 has option for additional modules for enforcing even more style rules. While browsing these packages, I was amused to find the wemake Python style guide which called itself “the strictest and most opinionated python linter ever.”

I installed wemake packages so that I can make Python code in my abandoned ROS Melodic project compliant with wemake. While I can’t say I was thrilled by all of the rules (it did get quite tedious!) I can confirm it does result in very consistent code. I’m glad I’ve given it a try, and I’m still undecided if I’m going to commit to wemake for future Python projects. No matter the final decision, I’ll definitely keep running at least plain flake8.

But while consistent code structure is useful for ease of maintenance, during the initial prototyping and algorithm design it’s nice to have something with more flexibility and immediate feedback. And I’ve only just discovered Jupyter notebooks for that purpose.

SGVHAK Rover Steering Trim Adjustment

One of the changes we made to our SGVHAK Rover relative to the baseline design was a decision to omit absolute encoders for our corner steering motors. The baseline design used absolute encoders so it knows every wheel’s steering angle upon system startup, but we decided to sacrifice that ability in exchange for cost savings (~$50/ea * 4 corners = ~$200.) Since there is no free lunch, this decision also means we have additional work to do upon our system startup.

Our control software launches with the assumption that all steerable wheels are pointed straight front-back, which is of course not always true. We could manually push the wheels to straight front-back orientation before turning on power, but that’s not great for the gearbox. Also, the shaft coupler used to connect our motor gearbox to our steering mechanism is liable to slip. (This is considered a bug even though it has a potential feature: in theory sharp jolts to the system would slip the coupler instead of breaking something else in the system. In practice, we found that it didn’t slip enough to avoid breaking a gearbox.)

Given both of these scenarios, we need a software-based solution to adjust steering trim on the fly. Once our RoboClaw motor controller is told where zero degree position is, it is quite capable of holding a commanded angle until the coupler slips or the system reboots, whichever comes first.

Steering Trim

Steering trim adjustment took the form of a multi-stage HTML form designed for our specific workflow. First our user chooses a wheel to work with, and they are directed to do so because every other UI element on the page is inactive until one wheel is chosen.

Once a wheel is chosen, the remaining UI activates and wheel selection UI deactivates to guarantee we’re working on a single wheel at a time. In this stage, steering angle can be adjusted in 1- and 5-degree increments. When we’re satisfied our wheel has been returned to zero degrees deflection, we can select “Accept Current Angle as New Zero” to commit. We also have the option to abort adjustment and retain previous zero position. Either way, wheel selection activates to allow user to select another wheel, and the rest of the page deactivates until a wheel is chosen.

We have no scenarios where multiple steering motors need to be trimmed at the same time, so this user experience specifically focus on one wheel at a time makes the process straightforward. It also becomes impossible to make mistakes caused by user thinking they’re adjusting wheel when they’re actually adjusting another, which is a good thing for positive user experience.

SGVHAK Rover Control With Cartesian Coordinates

Having learned some valuable lessons from trying to drive our SGVHAK rover using an interface based on polar coordinates, we returned to the drawing board to create an alternative. Thankfully, the abstracted command structure meant we could experiment with front-end user interface without any changes to the back-end rover calculations.

Our first-hand experience taught us polar coordinate math are more trouble than it was worth, so our next attempt will stay in the Cartesian coordinate space. Furthermore, we’ll rearrange the layout so a position going full speed ahead would never be adjacent to a position going full speed reverse. (This was the case for polar coordinate system when doing full deflection turns.) Third, we’ll have to make sure low speed maneuvering has just as much room to fine-tune its angle as going high speed. And lastly, when someone releases their finger out of caution, we need to stop the rover but we don’t need to reset steering angles.

It turned out to be very easy to satisfy all of the above desires: map X-axis left-right dimension to steering angle, and Y-axis up-down dimension to speed.

Cartesian Pad

Full speed ahead is now along the top edge of the blue rectangle, which is far from the bottom edge representing full speed reverse. There is no longer any risk of suddenly jerking between them.

At any given speed, we now have the full width for designating steering angle, we are no longer frustrated by a tiny range of adjustments when moving slow speed versus high.

And finally, when the user releases their finger, the dot will snap to vertical center (stop moving) but will remain at its most recent horizontal position, preserving the steering angle most recently used by our rover driver. This makes fine position adjustments much easier by allowing slow incremental inching forward/reverse movements.

Most users who tried both UI design prefer the Cartesian coordinate system. It was used for most of our driving at SCaLE 16X.

SGVHAK Rover Control With Polar Coordinates

Once an abstraction layer was defined for communication between user interface and rover chassis calculations, I could start trying out various ideas for building a rover driving UI. The baseline rover UI presents two touchscreen controls mimicking joysticks on a traditional remote control vehicle: one moves up/down for controlling forward/back movement, and the other moves left/right for controlling steering. Driving the baseline rover using its UI is a two-thumb affair.

That is a perfectly valid and functional UI, but my ambitions grew beyond copying it. I wanted to explore ideas around single-finger operation. This decision held firm even after the person who created the UI for baseline rover warned us that it could be tricky to implement – single-point operation was already tried before going with the two-thumb approach. I may yet bow to their wisdom, but I wanted to give it a shot first!

Polar Pad

The first implementation of a one-finger driving control is based around polar coordinates. It seemed like an obvious match to rover mechanics: polar coordinates on a two-dimensional plane is an angle and a distance, which maps directly to rover control’s angle and velocity. The user puts their finger down on the red central control dot, and they can drag it around inside the blue circle to drive the rover. Move the dot up to drive forward, rotate the dot left to steer left, and so forth.

As I got into it, though, problems started to mount.

The first and most obvious problem is that our underlying infrastructure – HTML, <canvas> tag, and JavaScript input events – use cartesian coordinates. In order to implement polar coordinate control we need trigonometry math to convert between the two systems. Errors creep in as we perform multiple operations and conversions between coordinate spaces.

The second problem was a consequence of mapping upper semicircle to forward velocity and lower semicircle to moving backwards. This became problematic when the user wishes to steer at maximum angle.  Turning far left means moving straight left, but at that point small movements can flip between upper and lower semicircle, causing the rover to jerk suddenly between forward and backward movement.

To mitigate this, a band of dead space between the semicircles was introduced. The user is no longer able to use the full 180 degrees of a semicircle for movement, they are restricted to the middle 140 degrees with the far left 20 and far right 20 degrees blocked off. This was effective to prevent sudden transitions between full speed forward/backwards motion but it wasted screen real estate.

The third problem of polar coordinates was exposed when trying to maneuver the rover at low-speed around obstacles. When our finger is near the center of the circle, small left-right movement translates into large changes in steering angle. It was more difficult than it should be to make small steering changes at low speed.

The last problem is makes the above problem worse. When somebody runs into a problem, their first instinct is to lift their finger off the touchscreen. In order to retreat to a known safe condition, our control dot pops back to the center position representing wheels stopped and all steerable wheels pointing straight front-back. This is fine, but when people are frustrated by problem #3 of low-speed maneuverability, this behavior snapping to straight front-back erases whatever angle they were previously driving at, making low-speed maneuvers even more frustrating.

And what happens when a rover driver is trying to perform delicate maneuvers but find themselves fighting a bad UI that gets in the way and cause frustration? The rover ends up running into things and risk breaking gearboxes.

Now we have experimental evidence polar coordinate driving doesn’t work as originally thought, let’s take that lesson and create a new UI.