Category: computers suck

  • Raspberry Pi vs used Thin Client

    I saw Jenny List’s post on Mastodon the other day:

    I need a small server to do a thing.

    I totted up the price of a Raspberry Pi 5 with all the accessories I would need, and came up at more cash than an equivalent x86 box.

    I’m sorry to say this, but there’s no reason for me to use a Pi there.

    along with Jonty Wareing’s reply:

    … Other than hats or pins I find there is rarely a good reason for a pi these days. You can get cheap x86 thin clients that beat the pants off them – the dell wyse ones are very cheap on ebay and excellent, the 5070 has an m2 slot and upgradable ram.

    I’ve had Raspberry Pis since they were launched. I used to work for an Official Reseller. I’ve been hired for my expertise with them. They’re so much part of the woodwork around here that I’ve never really considered them expensive. So how do they compare to an ex-corporate thin client box?

    Dell Wyse 5070

    ebay sold item page for "Dell Wyse 5070 Thin Client J5005 @1.5GHz 8GB RAM 128GB AC Adapter- NO OS/Stand" with picture of a small thin client computer box

    I found one on eBay from a local reseller, GREENSTAR💻⭐. For $68.44 including sales tax (that’s €42), I got a used thin client box including:

    • a great big power supply brick;
    • Intel j5005 quad core cpu, fanless;
    • 8 GB of DDR4 RAM (dated 2021);
    • 128 GB SATA M.2 SSD;
    • 3× DisplayPort video ports, 1920×1080 at 60 Hz;
    • 5× USB 3 ports, 1× USB C port and 2× USB 2 ports;
    • a real 9-pin serial port;
    • no wifi!

    This isn’t a detailed hardware review: for those, I suggest you read Gough Lui and David Parkinson. To get this machine up to a usable spec, I added:

    • a DisplayPort → HDMI cable (about $20);
    • a replacement BIOS backup battery ($1);
    • a cheap USB wifi adapter. I’m still finding old RTL8188CUS dongles about the house from the early Raspberry Pi days, some still in original packaging. These work, but aren’t great, but I can’t beat the price.

    All in — excluding monitor, keyboard and mouse — I’ll say I brought it in for $100 inclusive (about €61).

    Raspberry Pi 5

    To come up with an equivalent system (bought from an Official Reseller that I didn’t work for) I’d need:

    Description Price
    Raspberry Pi 5 8GB $114.95
    Raspberry Pi 45W USB-C Power Supply $21.99
    Case (with fan) $13.95
    MicroHDMI to HDMI Cable (2 m) $9.95
    128GB SD Card $24.95
    RTC Battery $7.00
    Subtotal $192.79
    Sales Tax $25.06
    Total $217.85

    (or €134)

    Not all of these items are available from the one reseller, particularly the 128 GB SD Card and RTC battery. I’ve included the RTC battery so you can do timed power-on tricks as with a regular PC. All the parts are from Raspberry Pi themselves. Curiously, you can pay more for non-official accessories with the CanaKit Raspberry Pi 5 Starter Kit at $224.95 + tax.

    Raspberry Pi 4

    An equivalent 8 GB Raspberry Pi 4 system breaks down like this:

    Description Price
    Raspberry Pi 4 8GB $104.95
    Raspberry Pi 15W USB-C Power Supply $10.95
    Case $6.95
    Case fan $6.75
    MicroHDMI to HDMI Cable (2 m) $9.95
    128GB SD Card $24.95
    DS3231 Real Time Clock Module for Raspberry Pi $9.95
    Subtotal $174.45
    Sales Tax $22.68
    Total $197.13

    (or €121.)

    This is surprisingly expensive, and unless you must have this particular SoC, likely better to go with a Raspberry Pi 5. Again, the RTC is optional, but timed power-on can be handy in a small computer. Most of the “RTC for Pi” boards use a cheaper DS3231M clock chip which can’t issue alarms for power control. You might have to shop around a bit to get this particular part.

    For roughly $2 more, you could go for the official Raspberry Pi 4 Desktop Kit (tiny 16 GB SD card, two HDMI cables, guidebook, no fan, no RTC — but includes the surprisingly lovely Raspberry Pi Keyboard and Hub and Mouse). For a whole lot more ($259.95), you could go with the CanaKit Raspberry Pi 4 EXTREME Kit.

    (As a former employee of a reseller, I suspect I’m permanently blocked from sharing why official resellers bundle third-party bits with their kits, always with a considerable price bump. Let’s just say that, during the Pandemic Chip Shortage, it was very galling to get a rare shipment of Raspberry Pi boards, go to extreme lengths to cancel multiple orders [oh the javascript injection hack attempts that I saw] and hurry to ship the boards out. The next day, we’d see what had to be the same hardware appearing on eBay at a 300% markup. And there was nothing we could do about it …)

    Testing

    I’m not interested in testing:

    1. Network throughput — Beyond having a working connection, I don’t have the skill or attention span to test networking stuff
    2. Video performance — I don’t really do video things. Raspberry Pis and thin clients are going to struggle with full screen 60 fps video anyway, and optimizing this is not my jam
    3. Power consumption — I don’t have the right kit for this. All I have is a 20 year old Kill-a-Watt clone which doesn’t have the necessary resolution.

    I’m going to have to rely on benchmarks. Benchmark results are notoriously easy to fiddle and give only a rough idea of how a system will perform in real life. I’m going to present the results of three systems (Raspberry Pi 4 and 5, Dell Wyse 5070: all running stock but up-to-date Raspberry Pi OS or Debian) in three tests, in decreasing order of arbitrariness.

    1: MP3 Encoding

    The time, in seconds, to encode Aphex Twin’s minimalist opus aisatsana [102] (5′ 21″) from a 55MB WAV file to a 6.8MB MP3 with:

    time lame -V 2 aphex_twin-aisatsana.wav
    System Time
    Raspberry Pi 4 14.2 s
    Dell Wyse 5070 8.6 s
    Raspberry Pi 5 5.7 s

    The thin client comes out between the two Raspberry Pis. It’s not a bad result at all: 8.6 s is still 37× real-time encoding.

    2: pichart

    pichart is a processor benchmark developed by Eric Olson for ranking numeric processing power of various computers against Raspberry Pi boards. It’s documented here: A Pi Pie Chart.

    It’s possible to tweak this benchmark endlessly with compiler options, but I stuck with whatever version of gcc the system came with. I also used exceptionally conservative compiler options of -O2. I reckon that if your compiler has got to version 12, it won’t be producing terrible code with simple options. Anyway:

    info graphic comparing multi core numeric performance of several small computers, including Raspberry Pi 4, Raspberry Pi 5 and Dell Wyse 5070.

The centre is dominated by a large pie chart, and there are individual results around the corners for Prime Sieve, Fourier Transform, Merge Sort and Lorenz.

With some variation in relative results, the Raspberry Pi 5 has the best performance, followed by the Wyse 5070 and then the Raspberry Pi 4

    (raw results, if you must: Wyse5070-vs-Pi.txt)

    All of these are OpenMP multi-core, multi-thread results. The Wyse 5070 holds a pretty solid second place to the Raspberry Pi 5.

    3: UnixBench 6.0.0

    byte-unixbench must be a very serious benchmark because it wraps a whole suite of results into one impenetrable number. We’re supposed to believe that This Number has some respectable heft. It certainly takes a long time to run (almost half an hour) and if your computer has fans, things can get loud.

    Since all three machines have four cores, it’ll save a lot of words to report only the multi-core System Benchmarks Index Score:

    For all its purported repeatability, this set of scores surprised me most. The Wyse 5070 doesn’t feel much slower than either Raspberry Pi board. Could the small SATA SSD be a bottleneck? I’d have to spend money to find out.


    (I also ran sbc-bench, but the results are even less enlightening. The only thing I could discern was that the Wyse 5070 was running some kind of custom thermal regime. Since it has no fan and only a modest heat-pipe cooler, this is no surprise. My results, have at ’em: sbc-bench)

    Conclusions

    I can get two Wyse 5070 systems for the cost of one Raspberry Pi 4 or 5. This is what makes the decision for me, and every other issue is window dressing. So much of “I made a thing with a Raspberry Pi!” is really “I made a thing with a small Linux computer!” these days, and the 5070 and other thin clients excel at this.

    Yes, the Raspberry Pi 5 is likely to be slightly faster that the Wyse 5070. And if you’re locked-in to their cameras, HATs or GPIO layout, you’re probably going to stick with Raspberry Pi. Likewise, if you’re kitting out a classroom, Raspberry Pis are all repeatable, from the same vendor, and have a proper warranty. Nobody in education got fired for buying Raspberry Pi — even if the micro:bit is the STEM board of choice round these parts.

    Even if the supply can be a bit variable, and you can’t be quite sure you’ll be able to get the same spec every time, the Wyse 5070 represents great value for money. I’ll definitely think twice about buying a Raspberry Pi next time.

    … about those GPIO pins

    If you’re not constrained to using Raspberry Pi’s 40-pin header or specific HAT hardware, you’ve still got options, including but not limited to:

    • an FT232H Breakout and PyFtdi. It may be possible (with some fiddling) to make the FT232H appear as a Linux gpio chip directly;
    • u2if (USB to interfaces) running on a Raspberry Pi Pico, talking to Python on the Linux end;
    • and of course, Firmata running on an Arduino is exactly where you left it in 2011. It still works, it hasn’t gone away, and is still at the heart of many custom interactive installations.

    Parting thought

    raspberry pi computer” is an anagram of “temporary price burps”. This, I feel, is important for you to know.

  • img2blocks

    this image is supposed to be made almost entirely of sextant blocks, the Unicode characters around U+1FB00 – U+1FB1E made out of two columns of three blocks. They’re originally from broadcast teletext, and were made to build low-resolution images on a text screen

    a blocky image of a large flightless bird with the text "Cassowary Detected"
    redrawn from an image in Artificial intelligence used to reduce cassowary road deaths in Queensland

    And here’s the original tiny image:

    a small image of a large flightless bird with the text "Cassowary Detected"
    “Cassowary Detected”, at actual size

    Making the pixel to character map is quite tricky. The Sextant character block isn’t contiguous, and it’s not in the order we need. It’s also missing four characters: empty block, full block, left half block and right half block. These have to be pulled in from other Unicode blocks.

    This is the map I came up with, from 0–63 with LSB at bottom right and MSB at top left:

     🬞🬏🬭🬇🬦🬖🬵🬃🬢🬓🬱🬋🬩🬚🬹🬁🬠🬑🬯🬉▐🬘🬷🬅🬤🬔🬳🬍🬫🬜🬻🬀🬟🬐🬮🬈🬧🬗🬶🬄🬣▌🬲🬌🬪🬛🬺🬂🬡🬒🬰🬊🬨🬙🬸🬆🬥🬕🬴🬎🬬🬝█

    After that, it’s a small matter of bashing something together in Python with PIL and Numpy. Here’s the source:

    or if you want to take a look first: img2blocks – bitmap to Unicode sextants

    blocky criss-crossed bars make a pseudo woven pattern
    X11’s wide_weave, scaled up

    (yes, there are clever things like Chafa that can do more, but it can’t do exactly what this does)

  • lightsleep vs deepsleep on Raspberry Pi Pico

    Many micro-controllers have timed energy-saving modes you can engage when they are idle. These are typically one of:

    • light sleep: where memory contents are retained, but some parts of the CPU and peripherals are turned off to reduce current;
    • deep sleep: memory is cleared, most of the CPU and peripherals are powered off. The CPU will reset fully on restart, so your program has to reload.

    While MicroPython on the RP2040 has both machine.lightsleep() and machine.deepsleep() functions, there’s not much difference between them. In fact, the deepsleep() routine is merely lightsleep() followed by reset(). So there isn’t any efficiency gain in using deepsleep over lightsleep.

    The functions take one argument: the sleep time, given in milliseconds. The largest value that is accepted is 4294966, or (2**32 // 10**3) - 1. That’s 71′ 34″. If you give a larger number, this exception is thrown:
    ValueError: sleep too long, and the function returns immediately.

    If you’ve used machine.deepsleep(), you might want to know whether your micro-controller was started by applying power, or started from the reset() after deepsleep(). The machine.reset_cause() function returns one of two values:

    • machine.PWRON_RESET: if the CPU was started from power on, or by briefly grounding the RUN pin;
    • machine.WDT_RESET: if the CPU was soft reset, either by a watchdog timer or other software reset. This is the state returned after deepsleep().

    Other MicroPython ports have more nuanced ways of handling sleep and reset states with better power saving.

    If you’re running a tight polling loop and still wish to save a little power, machine.idle() is the recommended method.

  • For 4 MB ESP32-S3 users

    If you have a Wemos/LOLIN S3 MINI PRO board, you might find that firmware images don’t flash so well. That’s because the ESP32-S3FH4R2 has 4 MB of flash storage, and most ESP32-S3 boards have 8 MB.

    glenn20/mp-image-tool-esp32 might be your new friend:

    mp-image-tool-esp32 -f 4M --resize vfs=2M ESP32_GENERIC_S3-20250415-v1.25.0.bin

    This trims down a standard MicroPython ESP32-S3 firmware from a 4 MB filesystem partition down to 2 MB, and sets the overall flash size to 4 MB. Upload that to your board, and all will be well.

    Alternatively, v1.26 supports “4MiB and larger” flash chips. I have confirmed that ESP32_GENERIC_S3-20250724-v1.26.0-preview.bin works as expected:

    $ mpremote a1 run boardstats.py 
    Board : Generic ESP32S3 module with ESP32S3
    Frequency : 160 MHz
    Free Memory : 2061232
    File storage: 2036 / 2048 K
  • plotter stuff on that Brother printer

    That Brother laser printer you bought can also pretend it’s a plotter. One of the requirements embedded in a PCL-compatible printer is an implementation of HP-GL/2. This is a slightly modified version of the page description language used by HP’s pen plotters. With care, you can make proofs on a laser printer.

    Take, for example, this figure drawn in HP-GL:

    [decorative] a spiralling figure made of scaled and rotated equilateral triangles
    just some plotter stuff …

    It’s made up of familiar commands:

    IN;SP1;PU4318,5988;
    PD3971,5388,4664,5388,4318,5988;
    PU4279,6026;
    PD3957,5335,4716,5402,4279,6026;

    But add some magic header bytes (0x1b, 0x45, 0x1b, 0x25, 0x30, 0x42) and some trailer bytes (0x1b, 0x25, 0x30, 0x41, 0x1b, 0x45), and your printer understands it’s a PCL file.

    The file, complete with header and trailer, is here:

    You can print it like this:

    lp -o raw hpgl-rotatey.hpgl

    which produces a page like this:

    [decorative] a spiralling figure made of scaled and rotated equilateral triangles
    full page scan of that HP-GL file as printed on a Brother MFC-L2750DW

    HP-GL/2, on mono lasers at least, has some differences to the version used on plotters. The biggest difference is that there’s just one pen. You can change the pattern and line attributes of this pen, but you don’t get to change to multiple pens with different colours.

    The manual for Brother’s HP-GL implementation lives here: Chapter 4: HP-GL/2 Graphics Language. Happy plotting!

  • SparkFun Retired Products Archive Reference

    In late 2024, SparkFun Electronics relaunched their website. In doing so, they deleted roughly 20 years of archived product information, along with all associated datasheets, schematics and tutorials. Luckily, the Internet Archive’s Wayback Machine has good records of the site, and I was able to recover links to 5934 deleted products.

    So here’s the list: SparkFun Retired Products Archive Reference.

  • MicroPython Benchmarks

    Somewhat predictably, my Parallel MicroPython Benchmarking thing got out of hand, and I’ve been scrabbling around jamming the benchmark code on every MicroPython board I can find.

    So despite WordPress’s best efforts in thwarting me from having a table here, my results are as follows, from fastest to slowest:

    Board Interpreter CPU @ Frequency / MHz Time / s
    DevEBox STM32H7xx micropython 1.20.0 STM32H743VIT6 @ 400 3.7
    Metro M7 micropython 1.24.1 MIMXRT1011DAE5A @ 500 4.3
    S3 PRO micropython 1.25.0.preview ESP32S3 @ 240 8.9
    Raspberry Pi Pico 2 W micropython 1.25.0.preview RP2350 @ 150 10.3
    ItsyBitsy M4 Express micropython 1.24.1 SAMD51G19A @ 120 12.3
    pyboard v1.1 micropython 1.24.1 STM32F405RG @ 168 13.0
    C3 mini micropython 1.25.0.preview ESP32-C3FH4 @ 160 13.2
    HUZZAH32 – ESP32 micropython 1.24.1 ESP32 @ 160 15.4
    S2 mini micropython 1.25.0.preview ESP32-S2FN4R2 @ 160 17.4
    Raspberry Pi Pico W micropython 1.24.1 RP2040 @ 125 19.8
    WeAct BlackPill STM32F411CEU6 micropython 1.24.0.preview STM32F411CE @ 96 21.4
    W600-PICO micropython 1.25.0.preview W600-B8 @ 80 30.7
    LOLIN D1 mini micropython 1.24.1 ESP8266 @ 80 45.6

    Yes, I was very surprised that the DevEBox STM32H7 at 400 MHz was faster than the 500 MHz MIMXRT1011 in the Metro M7. What was even more impressive is that the STM32H7 board was doing all the calculations in double precision, while all the others were working in single.

    As for the other boards, the ESP32 variants performed solidly, but the ESP8266 in last place should be retired. The Raspberry Pi Pico 2 W was fairly nippy, but the original Raspberry Pi Pico is still a lowly Cortex-M0+, no matter how fast you clock it. The STM32F4 boards were slower than I expected them to be, frankly. And yay! to the plucky little W600: it comes in second last, but it’s the cheapest thing out there.

    All of these benchmarks were made with the same code, but with two lines changed:

    1. The I2C specification, which is a minor syntax change for each board;
    2. The input trigger pin. Some boards like these as numbers, some take them as strings. Pro tip for W600 users: don’t use D0 for an input that’s tied to ground, unless you want the board to go into bootloader mode …

    I’d hoped to run these tests on the SAMD21 little micro-controllers (typically 48 MHz Cortex-M0), but they don’t have enough memory for MicroPython’s framebuf module, so it’s omitted from the build. They would likely have been very slow, though.

    In the spirit of fairness, I also benchmarked CircuitPython on a Arduino Nano RP2040 Connect, which has the same processor as a Raspberry Pi Pico:

    Board Interpreter CPU @ Frequency / MHz Time / s
    Arduino Nano RP2040 Connect circuitpython 9.2.3 RP2040 @ 125 18.0

    So it’s about 10% quicker than MicroPython, but I had to muck around for ages fighting with CircuitPython’s all-over-the-shop documentation and ninny syntax changes. For those that like that sort of thing, I guess that’s the sort of thing they like.

  • The 100 Doors Problem, on a very small computer

    The 100 Doors problem running on an (emulated) unexpanded ZX81

    I learned about this simple computer problem from Michael Doornbos: Just for fun, the 100 door problem on several different systems

    Yeah, it’s pretty neat to be able to do that on a Commodore VIC-20 with 5K of RAM. But how about a ZX81 with only 1K? With screen memory that moves around depending on how much stuff you have on the screen? No problem:

    ZX81 screendump showing program listing (program is listed at text elsewhere)
    that’s it: that’s the whole program

    The tricky part is printing just enough to the screen that you have enough memory to store the array and still have enough memory for your program. I did that by printing four lines of “🮐” characters (CHR$ 136 on the ZX81, U+1FB90) and moving the cursor down just far enough that later output wouldn’t zap my data. The screen address (given by the D_FILE pointer at 16396) is used as an array of 100 characters.

    The ZX81’s (non-ASCII) character set has a nice quirk that Space is CHR$ 0, and inverse video Space (“█”) is at CHR$ 128. So you can use NOT to toggle the value.

    Here’s the program listing, with Unicode characters:

       10 REM 100DOORS1K SCRUSS 2025
      20 FOR I=1 TO 128
      30 PRINT "🮐";
      40 NEXT I
      50 PRINT AT 3,0;"🮐"
      60 LET D=PEEK 16396+PEEK 16397*256
      70 FOR J=1 TO 100
      80 POKE D+J,0
      90 NEXT J
     100 FOR I=1 TO 100
     110 FOR J=I TO 100 STEP I
     120 POKE D+J,128*NOT PEEK (D+J)
     130 NEXT J
     140 NEXT I
     150 FOR I=1 TO 100
     160 IF PEEK (D+I) THEN PRINT I,
     170 NEXT I
    

    The ZX81 program image plus the listing in zmakebas format is included here:

  • Parallel MicroPython Benchmarking

    On the left, a Raspberry Pi Pico 2W. On the right, a Raspberry Pi Pico. Each is connected to its own small OLED screen. When a button is pressed, both boards calculate and display the Mandelbrot set, along with its completion time. Needless to say, the Pico 2 W is quite a bit quicker.
    two small OLED screens side by side on a breadboard. They're the type that are surplus from pulse oximeter machines, so the top 16 pixels are yellow, and the rest of the rows are blue.

The left screen displays: "micropython 1.25.0.preview RP2350 150 MHz 128*64; 120", while the screen on the right shows "micropython 1.24.1 RP2040 125 MHz 128*64; 120"
    the before screens …
    The same two OLED screens, this time showing a complete Mandelbrot set and an elapsed time for each microcontroller. Pico 2 comes in at 10.3 seconds, original Pico at 19.8 seconds
    Pico 2 comes in at 10.3 seconds, original Pico at 19.8 seconds

    Stuff I found out setting this up:

    • some old OLEDs, like these surplus pulse oximeter ones, don’t have pull-up resistors on their data lines. These I’ve carefully hidden behind the displays, but they’re there.
    • Some MicroPython ports don’t include the complex type, so I had to lose the elegant z→z²+C mapping to some ugly code.
    • Some MicroPython ports don’t have os.uname(), but sys.implementation seems to cover most of the data I need.
    • On some boards, machine.freq() is an integer value representing the CPU frequency. On others, it’s a list. Aargh.

    These displays came from the collection of the late Tom Luff, a Toronto maker who passed away late 2024 after a long illness. Tom had a huge component collection, and my way of remembering him is to show off his stuff being used.

    Source:

    # benchmark Mandelbrot set (aka Brooks-Matelski set) on OLED
    # scruss, 2025-01
    # MicroPython
    # -*- coding: utf-8 -*-
    
    from machine import Pin, I2C, idle, reset, freq
    
    # from os import uname
    from sys import implementation
    from ssd1306 import SSD1306_I2C
    from time import ticks_ms, ticks_diff
    
    # %%% These are the only things you should edit %%%
    startpin = 16  # pin for trigger configured with external pulldown
    # I2C connection for display
    i2c = machine.I2C(1, freq=400000, scl=19, sda=18, timeout=50000)
    # %%% Stop editing here - I mean it!!!1! %%%
    
    
    # maps value between istart..istop to range ostart..ostop
    def valmap(value, istart, istop, ostart, ostop):
        return ostart + (ostop - ostart) * (
            (value - istart) / (istop - istart)
        )
    
    
    WIDTH = 128
    HEIGHT = 64
    TEXTSIZE = 8  # 16x8 text chars
    maxit = 120  # DO NOT CHANGE!
    # value of 120 gives roughly 10 second run time for Pico 2W
    
    # get some information about the board
    # thanks to projectgus for the sys.implementation tip
    if type(freq()) is int:
        f_mhz = freq() // 1_000_000
    else:
        # STM32 has freq return a tuple
        f_mhz = freq()[0] // 1_000_000
    sys_id = (
        implementation.name,
        ".".join([str(x) for x in implementation.version]).rstrip(
            "."
        ),  # version
        implementation._machine.split()[-1],  # processor
        "%d MHz" % (f_mhz),  # frequency
        "%d*%d; %d" % (WIDTH, HEIGHT, maxit),  # run parameters
    )
    
    p = Pin(startpin, Pin.IN)
    
    # displays I have are yellow/blue, have no pull-up resistors
    #  and have a confusing I2C address on the silkscreen
    oled = SSD1306_I2C(WIDTH, HEIGHT, i2c)
    oled.contrast(31)
    oled.fill(0)
    # display system info
    ypos = (HEIGHT - TEXTSIZE * len(sys_id)) // 2
    for s in sys_id:
        ts = s[: WIDTH // TEXTSIZE]
        xpos = (WIDTH - TEXTSIZE * len(ts)) // 2
        oled.text(ts, xpos, ypos)
        ypos = ypos + TEXTSIZE
    
    oled.show()
    
    while p.value() == 0:
        # wait for button press
        idle()
    
    oled.fill(0)
    oled.show()
    start = ticks_ms()
    # NB: oled.pixel() is *slow*, so only refresh once per row
    for y in range(HEIGHT):
        # complex range reversed because display axes wrong way up
        cc = valmap(float(y + 1), 1.0, float(HEIGHT), 1.2, -1.2)
        for x in range(WIDTH):
            cr = valmap(float(x + 1), 1.0, float(WIDTH), -2.8, 2.0)
            # can't use complex type as small boards don't have it dammit)
            zr = 0.0
            zc = 0.0
            for k in range(maxit):
                t = zr
                zr = zr * zr - zc * zc + cr
                zc = 2 * t * zc + cc
                if zr * zr + zc * zc > 4.0:
                    oled.pixel(x, y, k % 2)  # set pixel if escaped
                    break
        oled.show()
    elapsed = ticks_diff(ticks_ms(), start) / 1000
    elapsed_str = "%.1f s" % elapsed
    # oled.text(" " * len(elapsed_str), 0, HEIGHT - TEXTSIZE)
    oled.rect(
        0, HEIGHT - TEXTSIZE, TEXTSIZE * len(elapsed_str), TEXTSIZE, 0, True
    )
    
    oled.text(elapsed_str, 0, HEIGHT - TEXTSIZE)
    oled.show()
    
    # we're done, so clear screen and reset after the button is pressed
    while p.value() == 0:
        idle()
    oled.fill(0)
    oled.show()
    reset()
    
    

    (also here: benchmark Mandelbrot set (aka Brooks-Matelski set) on OLED – MicroPython)

    I will add more tests as I get to wiring up the boards. I have so many (too many?) MicroPython boards!

    Results are here: MicroPython Benchmarks

  • BASIC-52 on tiny CH552 boards via Linux

    draft post, published for usefulness not polish

    • Hackaday project that introduced me to the CH552 and BASIC-52: Single Chip Computer | Hackaday.io
    • Boards that I’ve got this working with:
    • You’ll need an external USB UART with 5 V power, as this port doesn’t support serial over the USB
    • Project source (yes, it’s a Google Drive link, and any docs are in Japanese): CH55x. If that doesn’t work, the original redirect is from CH552Eでモニタプログラムを動かしてみました | きょうのかんぱぱ. If neither of those work, here’s my download of the archive: CH552T&G-20240805T141304Z-001.zip
    • Binary file (zip): basic52s.bin (hiyodori5, version 7th May 2023, converted from hex so you don’t have to mess with srec_cat or makebin)
    • Build cjacker/ch55x-isptool to upload the code (which doesn’t accept hex)
    • Plug in the CH552 board. You may have to do something with the boot/reset button to make it turn up as the right USB ID (4348:55e0 WinChipHead).
    • Program the board:
      sudo ch55xisptool basic52s.bin
    • Disconnect the board, and wire it up to the USB UART (5 V, GND, TX → RXD [P3.0], RX → TXD [P3.1])
    • Hit return a few times to get a prompt
    • PWM is on P1.2, INT1 is on P3.3. See Hackaday project to see how to access I²C, and also do things with SFR values using RDSFR / WRSFR. PORT3 is at SFR(0B0H)

    please ignore the following for now …

    Who wouldn’t want to run a solid BASIC interpreter on a $3 development board? So maybe there are a couple of drawbacks:

    1. there’s no way to save the program to non-volatile storage: you have to be connected through a serial terminal at all times; and
    2. you’ve got about 600 bytes for the whole program, with no way to expand it.

    Despite these limitations, there’s some futile fun to be had. I’ll show you how to flash BASIC-52 onto one of these development boards, and give a quick intro to what you can do with it.

    BASIC-52

    Intel released the first version of BASIC-52 for their 8051 family of microcontrollers in 1984. They produced a chip (8052AH-BASIC) with the interpreter burnt into mask ROM in 1985. The source code was released into the public domain, and various features such as I²C support were added by the community around 2000.

    As befits an embedded language, BASIC-52 supports pin management, timers and interrupts. It’s also a fairly full-featured BASIC interpreter with floating point support and mostly familiar keywords and functions. Because it’s designed for very limited memory use, its string handling is quite unlike any other BASIC dialect. It has one character array that you can treat as a string, and a few functions to work with characters, but that’s about all.

    A most useful reference is Intel’s MCS BASIC-52 Versions 1 & 1.1 Operating and Reference Manual. Another helpful guide is Jan Axelson’s The Microcontroller Idea Book. I found out about both of these references from Single Chip Computer ­— Hackaday.io, which also introduced me the possibilities of running BASIC-52 on the CH552.

    CH552

    a small purple pcb with a 16-pin surface mount chip in the middle: a WCH CH552G micro-controller
    Deqing Sun’s CH552 Stick, from the ch55xduino project

    You might know WCH (aka QinHeng Electronics) from their inexpensive CH341 USB serial adapters and other interface boards. What you might not realize is that all of their older interface chips are based on an optimized 8051 design

  • Cheap NeoPixels at the Dollar Store

    Exhibit A:

    box of "Monster BASICS" Sound reactive RGB+IC Color Flow LED strip

    also known as “Monster BASICS Sound reactive RGB+IC Color Flow LED strip”. It’s $5 or so at Dollarama, and includes a USB cable for power and a remote control. It’s two metres long and includes 60 RGB LEDs. Are these really super-cheap NeoPixel clones?

    I’m going to keep the USB power so I can power it from a power bank, but otherwise convert it to a string of smart LEDs. We lose the remote control capability.

    Pull back the heatshrink at the USB end:

    led strip with shrink tubing pulled back to show the +5 V, Din and GND solder tterminals

    … and there are our connectors. We want to disconnect the blue Din (Data In) line from the built in controller, and solder new wires to Din and GND to run from a microcontroller board.

    led strip with additional wires soldered to Din and GND contacts

    Maybe not the best solder job, but there are new wires feeding through the heatshrink and soldered onto the strip.

    led strip with two additional wires soldered in and heatshrink pushed back, all held in place by a cable tie

    Here’s the heatshrink pushed back, and everything secured with a cable tie.

    Now to feed it from standard MicroPython NeoPixel code, suitably jazzed up for 60 pixels.

    a glowing multicolour reel of of LEDs

    A pretty decent result for $5!

  • Brother Canada laser cartridge return label

    April 2025: got a slightly different label, so maybe use this:

    So you bought that Brother laser printer like everyone told you to. And now it’s out of toner, so you replaced the cartridge. If you were in the USA, you could return the cartridge for free using the included label. But in Canada … it’s a whole deal including registering with Brother and giving away your contact details and, and, and …

    I’m pretty sure this is a generic label, since:

    1. the file is dated sometime in 2023, and wasn’t generated directly for my download;
    2. I got a different tracking number when I handed the thing in at the post office.

    But if they do complain, you know what to do: brother.ca/en/environment

  • Crickets in February

    It’s mid-February in Toronto: -10 °C and snowy. The memory of chirping summer fields is dim. But in my heart there is always a cricket-loud meadow.

    Short of moving somewhere warmer, I’m going to have to make my own midwinter crickets. I have micro-controllers and tiny speakers: how hard can this be?

    more fun than a bucket of simulated crickets
    (video description: a plastic box containing three USB power banks, each with USB cable leading to a Raspberry Pi Pico board. Each board has a small electromagnetic speaker attached between ground and a data pin)

    I could have merely made these beep away at a fixed rate, but I know that real crickets tend to chirp faster as the day grows warmer. This relationship is frequently referred to as Dolbear’s law. The American inventor Amos Dolbear published his observation (without data or species identification) in The American Naturalist in 1897: The Cricket as a Thermometer

    journal text:

The rate of chirp seems to be entirely determined by the temperature and this to such a degree that one may easily compute the temperature when the number of chirps per minute is known.

Thus at 60° F. the rate is 80 per minute.

At 70° F. the rate is 120 a minute, a change of four chirps a minute for each change of one degree. Below a temperature
of 50° the cricket has no energy to waste in music and there would be but 40 chirps per minute.
One may express this relation between temperature and chirp rate thus.
Let T. stand for temperature and N,  the rate per minute.

(typeset equation)
T. = 50 + (N - 40) / 4
    pretty bold assertions there without data eh, Amos old son …?

    When emulating crickets I’m less interested in the rate of chirps per minute, but rather in the period between chirps. I could also care entirely less about barbarian units, so I reformulated it in °C (t) and milliseconds (p):

    t = ⅑ × (40 + 75000 ÷ p)

    Since I know that the micro-controller has an internal temperature sensor, I’m particularly interested in the inverse relationship:

    p = 15000 ÷ (9 * t ÷ 5 – 8)

    I can check this against one of Dolbear’s observations for 70°F (= 21⅑ °C, or 190/9) and 120 chirps / minute (= 2 Hz, or a period of 500 ms):

    p = 15000 ÷ (9 * t ÷ 5 – 8)
       = 15000 ÷ (9 * (190 ÷ 9) ÷ 5 – 8)
       = 15000 ÷ (190 ÷ 5 – 8)
       = 15000 ÷ 30
       = 500

    Now I’ve got the timing worked out, how about the chirp sound. From a couple of recordings of cricket meadows I’ve made over the years, I observed:

    1. The total duration of a chirp is about ⅛ s
    2. A chirp is made up of four distinct events:
      • a quieter short tone;
      • a longer louder tone of a fractionally higher pitch;
      • the same longer louder tone repeated;
      • the first short tone repeated
    3. There is a very short silence between each tone
    4. Each cricket appears to chirp at roughly the same pitch: some slightly lower, some slightly higher
    5. The pitch of the tones is in the range 4500–5000 Hz: around D8 on the music scale

    I didn’t attempt to model the actual stridulating mechanism of a particular species of cricket. I made what sounded sort of right to me. Hey, if Amos Dolbear could make stuff up and get it accepted as a “law”, I can at least get away with pulse width modulation and tiny tinny speakers …

    This is the profile I came up with:

    • 21 ms of 4568 Hz at 25% duty cycle
    • 7 ms of silence
    • 28 ms of 4824 Hz at 50% duty cycle
    • 7 ms of silence
    • 28 ms of 4824 Hz at 50% duty cycle
    • 7 ms of silence
    • 21 ms of 4568 Hz at 25% duty cycle
    • 7 ms of silence

    That’s a total of 126 ms, or ⅛ish seconds. In the code I made each instance play at a randomly-selected relative pitch of ±200 Hz on the above numbers.

    For the speaker, I have a bunch of cheap PC motherboard beepers. They have a Dupont header that spans four pins on a Raspberry Pi Pico header, so if you put one on the ground pin at pin 23, the output will be connected to pin 26, aka GPIO 20:

    Raspberry Pi Pico with small piezo speaker connected to pins 23 (ground) and 26 (GPIO 20)
    from a post where I did a very, very bad thing: Nyan Cat, except it gets faster — RTTTL on the Raspberry Pi Pico

    So — finally — here’s the MicroPython code:

    # cricket thermometer simulator - scruss, 2024-02
    # uses a buzzer on GPIO 20 to make cricket(ish) noises
    # MicroPython - for Raspberry Pi Pico
    # -*- coding: utf-8 -*-
    
    from machine import Pin, PWM, ADC, freq
    from time import sleep_ms, ticks_ms, ticks_diff
    from random import seed, randrange
    
    freq(125000000)  # use default CPU freq
    seed()  # start with a truly random seed
    pwm_out = PWM(Pin(20), freq=10, duty_u16=0)  # can't do freq=0
    led = Pin("LED", Pin.OUT)
    sensor_temp = machine.ADC(4)  # adc channel for internal temperature
    TOO_COLD = 10.0  # crickets don't chirp below 10 °C (allegedly)
    temps = []  # for smoothing out temperature sensor noise
    personal_freq_delta = randrange(400) - 199  # different pitch every time
    chirp_data = [
        # cadence, duty_u16, freq
        # there is a cadence=1 silence after each of these
        [3, 16384, 4568 + personal_freq_delta],
        [4, 32768, 4824 + personal_freq_delta],
        [4, 32768, 4824 + personal_freq_delta],
        [3, 16384, 4568 + personal_freq_delta],
    ]
    cadence_ms = 7  # length multiplier for playback
    
    
    def chirp_period_ms(t_c):
        # for a given temperature t_c (in °C), returns the
        # estimated cricket chirp period in milliseconds.
        #
        # Based on
        # Dolbear, Amos (1897). "The cricket as a thermometer".
        #   The American Naturalist. 31 (371): 970–971. doi:10.1086/276739
        #
        # The inverse function is:
        #     t_c = (75000 / chirp_period_ms + 40) / 9
        return int(15000 / (9 * t_c / 5 - 8))
    
    
    def internal_temperature(temp_adc):
        # see pico-micropython-examples / adc / temperature.py
        return (
            27
            - ((temp_adc.read_u16() * (3.3 / (65535))) - 0.706) / 0.001721
        )
    
    
    def chirp(pwm_channel):
        for peep in chirp_data:
            pwm_channel.freq(peep[2])
            pwm_channel.duty_u16(peep[1])
            sleep_ms(cadence_ms * peep[0])
            # short silence
            pwm_channel.duty_u16(0)
            pwm_channel.freq(10)
            sleep_ms(cadence_ms)
    
    
    led.value(0)  # led off at start; blinks if chirping
    ### Start: pause a random amount (less than 2 s) before starting
    sleep_ms(randrange(2000))
    
    while True:
        loop_start_ms = ticks_ms()
        sleep_ms(5)  # tiny delay to stop the main loop from thrashing
        temps.append(internal_temperature(sensor_temp))
        if len(temps) > 5:
            temps = temps[1:]
        avg_temp = sum(temps) / len(temps)
        if avg_temp >= TOO_COLD:
            led.value(1)
            loop_period_ms = chirp_period_ms(avg_temp)
            chirp(pwm_out)
            led.value(0)
            loop_elapsed_ms = ticks_diff(ticks_ms(), loop_start_ms)
            sleep_ms(loop_period_ms - loop_elapsed_ms)
    

    There are a few more details in the code that I haven’t covered here:

    1. The program pauses for a short random time on starting. This is to ensure that if you power up a bunch of these at the same time, they don’t start exactly synchronized
    2. The Raspberry Pi Pico’s temperature sensor can be slightly noisy, so the chirping frequency is based on the average of (up to) the last five readings
    3. There’s no chirping below 10 °C, because Amos Dolbear said so
    4. The built-in LED also flashes if the board is chirping. It doesn’t mimic the speaker’s PWM cadence, though.

    Before I show you the next video, I need to say: no real crickets were harmed in the making of this post. I took the bucket outside (roughly -5 °C) and the “crickets” stopped chirping as they cooled down. Don’t worry, they started back up chirping again when I took them inside.

    “If You’re Cold They’re Cold, Bring Them Inside”
    (video description: a plastic box containing three USB power banks, each with USB cable leading to a Raspberry Pi Pico board. Each board has a small electromagnetic speaker attached between ground and a data pin)

  • We used to have to *wait* for our memes …

    It must be Wednesday somewhere

    If you really must try this on your on Amstrad CPC:

  • “… that Chinese instruction manual font”

    (this is an old post from 2021 that got caught in my drafts somehow)

    Mike asked:

    To which I suggested:

    English text in MOESongUN monospaced serif font"
"You mean this one?
This is MOESongUN from Taiwan"

    Not very helpful links, more of a thought-dump:

    First PostScript font: STSong (华文宋体) was released in 1991, making it the first PostScript font by a Chinese foundry [ref: Typekit blog — Pan-CJK Partner Profile: SinoType]. But STSong looks like Garamond(ish).

    A table of the latin characters @, A-Z, [, \, ], ^, _, `, a-z and { in STSong half-width latin, taken from fontforge

    Maybe source: GB 5007.1-85 24×24 Bitmap Font Set of Chinese Characters for Information Exchange. Originally from 1985, this is a more recent version: GB 5007.1-2010: Information technology—Chinese ideogram coded character set (basic set)—24 dot matrix font.

    half-width Latin text table from Chinese standard GB 5007.1-85
  • The Potato

    … is a thing to help soldering DIN connectors. I had some made at JLCPCB, and will have them for sale at World of Commodore tomorrow.

    a rectangular green circuit board with markings for various DIN connectors, and holes for the connector pins to fin through. One of the sets of holes is filled by a 7-pin 270° DIN connector, ready for soldering.

There are various pinouts for retrocomputers on the board. There is also the slogan THE POTATO, with a smiling cartoon potato next to it
    Sven Petersen’s “The Potato” – front. DIN7 connector not included
    a rectangular green circuit board with markings for various DIN connectors, and holes for the connector pins to fin through. One of the sets of holes is filled by a 7-pin 270° DIN connector, ready for soldering.

There are various pinouts for retrocomputers on the board. There is also the slogan DE KANTÜFFEL, with a smiling cartoon potato next to it
    Sven Petersen’s “The Potato” – back

    You can get the source from svenpetersen1965/DIN-connector_soldering-aid-The-Potato. I had the file Rev. 0/Gerber /gerber_The_Potato_noFrame_v0a.zip made, and it seems to fit connector pins well.

    Each Potato is made up of two PCBs, spaced apart by a nylon washer and held together by M3 nylon screws.

  • can we…?

    This is a mini celebratory post to say that I’ve fixed the database encoding problems on this blog. It looks like I will have to go through the posts manually to correct the errors still, but at least I can enter, store and display UTF-8 characters as expected.

    “? µ ° × — – ½ ¾ £ é?êè”, he said with some relief.

    Postmortem: For reasons I cannot explain or remember, the database on this blog flipped to an archaic character set: latin1, aka ISO/IEC 8859-1. A partial fix was effected by downloading the entire site’s database backup, and changing all the following references in the SQL:

    • CHARSET=latin1 → CHARSET=utf8mb4
    • COLLATE=latin1_german2_ci → COLLATE=utf8mb4_general_ci
    • COLLATE utf8mb4_general_ci → COLLATE utf8mb4_general_ci
    • latin1_general_ci → utf8mb4_general_ci
    • COLLATE latin1_german2_ci → COLLATE utf8mb4_general_ci
    • CHARACTER SET latin1 → CHARACTER SET utf8mb4

    For additional annoyance, the entire SQL dump was too big to load back into phpmyadmin, so I had to split it by table. Thank goodness for awk!

    #!/usr/bin/awk -f
    
    BEGIN {
        outfile = "nothing.sql";
    }
    
    /^# Table: / {
        # very special comment in WP backup that introduces a new table
        # last field is table_name,
        # which we use to create table_name.sql
        t = $NF
        gsub(/`/, "", t);
        outfile = t ".sql";
    }
    
    {
        print > outfile;
    }
    

    The data still appears to be confused. For example, in the post Compose yourself, Raspberry Pi!, what should appear as “That little key marked “Compose”” appears as “That little key marked “Compose””. This isn’t a straight conversion of one character set to another. It appears to have been double-encoded, and wrongly too.

    Still, at least I can now write again and have whatever new things I make turn up the way I like. Editing 20 years of blog posts awaits … zzz

  • Autumn in Canada: NAPLPS

    NAPLPS rendered in PP3

    My OpenProcessing demo “autumn in canada”, redone as a NAPLPS playback file. Yes, it would have been nice to have outlined leaves, but I’ve only got four colours to play with that are vaguely autumnal in NAPLPS’s limited 2-bit RGB.

    Played back via dosbox and PP3, with help from John Durno‘s very useful Displaying NAPLPS Graphics on a Modern Computer: Technical Note.

    This file only displays 64 leaves, as more leaves caused the emulated Commodore 64 NAPLPS viewer I was running to crash.

  • The glorious futility of generating NAPLPS in 2023

    Yeah! Actual real NAPLPS made by me!

    NAPLPS — an almost-forgotten videotex vector graphics format with a regrettable pronunciation (/nap-lips/, no really) — was really hard to create. Back in the early days when it was a worthwhile Canadian initiative called Telidon (see Inter/Access’s exhibit Remember Tomorrow: A Telidon Story) it required a custom video workstation costing $$$$$$. It got cheaper by the time the 1990s rolled round, but it was never easy and so interest waned.

    I don’t claim what I made is particularly interesting:

    a lo-res red maple leaf in the bottom left corner of a black screen
    suspiciously canadian

    but even decoding the tutorial and standards material was hard. NAPLPS made heavy use of bitfields interleaved and packed into 7 and 8-bit characters. It was kind of a clever idea (lower resolution data could be packed into fewer bytes) but the implementation is quite unpleasant.

    A few of the references/tools/resources I relied on:

    Here’s the fragment of code I wrote to generate the NAPLPS:

    #!/usr/bin/env python3
    # -*- coding: utf-8 -*-
    # draw a disappointing maple leaf in NAPLPS - scruss, 2023-09
    
    # stylized maple leaf polygon, quite similar to
    # the coordinates used in the Canadian flag ...
    maple = [
        [62, 2],
        [62, 35],
        [94, 31],
        [91, 41],
        [122, 66],
        [113, 70],
        [119, 90],
        [100, 86],
        [97, 96],
        [77, 74],
        [85, 114],
        [73, 108],
        [62, 130],
        [51, 108],
        [39, 114],
        [47, 74],
        [27, 96],
        [24, 86],
        [5, 90],
        [11, 70],
        [2, 66],
        [33, 41],
        [30, 31],
        [62, 35],
    ]
    
    
    def colour(r, g, b):
        # r, g and b are limited to the range 0-3
        return chr(0o74) + chr(
            64
            + ((g & 2) << 4)
            + ((r & 2) << 3)
            + ((b & 2) << 2)
            + ((g & 1) << 2)
            + ((r & 1) << 1)
            + (b & 1)
        )
    
    
    def coord(x, y):
        # if you stick with 256 x 192 integer coordinates this should be okay
        xsign = 0
        ysign = 0
        if x < 0:
            xsign = 1
            x = x * -1
            x = ((x ^ 255) + 1) & 255
        if y < 0:
            ysign = 1
            y = y * -1
            y = ((y ^ 255) + 1) & 255
        return (
            chr(
                64
                + (xsign << 5)
                + ((x & 0xC0) >> 3)
                + (ysign << 2)
                + ((y & 0xC0) >> 6)
            )
            + chr(64 + ((x & 0x38)) + ((y & 0x38) >> 3))
            + chr(64 + ((x & 7) << 3) + (y & 7))
        )
    
    
    f = open("maple.nap", "w")
    f.write(chr(0x18) + chr(0x1B))  # preamble
    
    f.write(chr(0o16))  # SO: into graphics mode
    
    f.write(colour(0, 0, 0))  # black
    f.write(chr(0o40) + chr(0o120))  # clear screen to current colour
    
    f.write(colour(3, 0, 0))  # red
    
    # *** STALK ***
    f.write(
        chr(0o44) + coord(maple[0][0], maple[0][1])
    )  # point set absolute
    f.write(
        chr(0o51)
        + coord(maple[1][0] - maple[0][0], maple[1][1] - maple[0][1])
    )  # line relative
    
    # *** LEAF ***
    f.write(
        chr(0o67) + coord(maple[1][0], maple[1][1])
    )  # set polygon filled
    # append all the relative leaf vertices
    for i in range(2, len(maple)):
        f.write(
            coord(
                maple[i][0] - maple[i - 1][0], maple[i][1] - maple[i - 1][1]
            )
        )
    
    f.write(chr(0x0F) + chr(0x1A))  # postamble
    f.close()
    

    There are a couple of perhaps useful routines in there:

    1. colour(r, g, b) spits out the code for two bits per component RGB. Inputs are limited to the range 0–3 without error checking
    2. coord(x, y) converts integer coordinates to a NAPLPS output stream. Best limited to a 256 × 192 size. Will also work with positive/negative relative coordinates.

    Here’s the generated file: