Sunday, 23 March 2025

Time to start behaving

Well, the endless rabbitholing seems determined to persist. The Waveshare ESP32S3 boards I got from Amazon seem to indeed "share" a lot in common with their ESP32 siblings. Namely I struggle to get them to behave at all. Even when following the manufacturer's instructions to the tee, I can't get the fuckers the work. And no, it's not just that The Stupid Fat Block doesn't know what he's doing - that surely can't be helping - but on the myriad forums, users of all capabilities and none are sharing the same frustration. I've got a life to live, so the ESP32 family can shove it.

So I'm going to fall back on the "proven" solutions ie "official Arduino boards made and sold by Arduino" (through Amazon) using microcontrollers of know parentage.

What happened to the ESP32S3, then?

Well, firstly the S3 and C3 boards are "more different" than I'd expected. For one thing, the S3 has more pins and for another, the board layout is completely different, as in the pins are on opposite sides of the board. TBH, all I was hoping for was a dual core board in the same form / footprint / pinout, so that the stepper speed control could run independently to the pulsing function. But then I completely failed to even get it to talk to me. I seemed  to have bricked the damned thing. Probably not bricked , technically speaking, but either way it was no use to man nor beast - and certainly not to The Stupid Fat Bloke.

So, to start with, I'm gong to immediately break the "official Arduino boards made and sold by Arduino" notion by instead using a Waveshare RP2040 Zero "mini" board using an RP2040. What???

Let's get this Waveshare RP2040 Zero running, then:

The whole point of this dual core shenanigan is to run the motor speed control in one core and the pulsing control in another, with minimal impact of one on the other. Can we finally, finally get there?

The setup for the Waveshare board appears to be pretty much the same as that required for the Arduino Pico The installation of the RP2040 into the board manager needs to be done first and seems to also download some relevant RP2040 examples.

Once you've got the board set up as a "Waveshare RP2040 Zero" in the Arduino IDE board manager, it's time to try out multi core processing. This code seems to work:

// Core0 setup
void setup() 
{
  Serial.begin(115200);
}
void loop() 
{
  Serial.printf("Core 0-1...\n");
}
// Core1 setup
void setup1() 
{
  Serial.printf("Core 1-1...\n");
}
void loop1() 
{
  Serial.printf("Core 1-2...\n");
}

This does almost nothing but at least it compiles and works. It's basically setup(), loop(), setup1(), loop1(), with relevant content for core0 and core1 within each segment. Pretty simple on the face of it.

So the next steps are: 
  • graft the pulsing software into one of the cores and convince myself it works.
  • graft the stepper speed control onto one of the cores etc etc
  • try to get both functions to coexist.

One issue with the Zero is that the RGB onboard LED isn't just a simple LED, so it's not possible to just turn it on. No problem - I'll rely on the serial monitor, although it's a bit of an annoyance.

Let's get the pin allocations decided. Here's the Zero's layout:


Looks as if I can use A0...A2 for the analogue inputs and D29 for the step output, keeping the external connections grouped together for no obviously compelling reason.

This works:

#include <AccelStepper.h>
// constants to set pin numbers = 13 - use built in LED for test / dev
const int pwmPin =  13; // the number of the green LED pin
const int dutyCycleAnalogIn = A0;  // Analog input- PWM duty
const int periodAnalogIn = A1;  // Analog input - period (ms)
const int speedAnalogIn = A2;  // Analog input - speed (Hz)
const int maxPeriod = 5000;   // hard coded max on time
const int dirPin = 5;  // pin 5 used for DIR output
const int stepPin = 4;  // pin 4 used for STEP output
// Variables will change
int pwmState = HIGH; //ledState for PWM output
int pwmSensorValue = 0;  // value read from the pot
int periodSensorValue = 0;
int speedValue = 0;
long plot = 0;
AccelStepper stepper1(AccelStepper::DRIVER, stepPin, dirPin); // (Type of driver: with 2 pins, STEP, DIR)
long previousMillisPwm = 0;  //last time PWM output was updated
long previousMillisPeriod = 0;
// pwmPeriod is time between spots
long pwmPeriod = 2000; //interval between spots (milliseconds)
// must be long to prevent overflow
long pwmDuration = 2000; //interval for PWM output (milliseconds)
unsigned long currentMillis = 0; // initialise
void setup() 
{
  // Set maximum speed value for the stepper:
  stepper1.setMaxSpeed(1000);
  // set the pins to output mode
  Serial.begin(9600);
  pinMode(pwmPin, OUTPUT);
  digitalWrite(pwmPin, pwmState);
  currentMillis = millis();
}
// Core1 setup
void setup1() 
{
//  Serial.printf("Core 1-1...\n");
}
void loop() 
{
  analogRead(0); // could be any channel
  periodSensorValue = analogRead(periodAnalogIn); // 0 - 1023 count
  pwmPeriod = (maxPeriod * periodSensorValue / 1023); // map input to pwmPeriod - time between spots
  pwmSensorValue = analogRead(dutyCycleAnalogIn); // 0 - 1023 count
  pwmDuration = map(pwmSensorValue, 0, 1023, 0, pwmPeriod); // map input to pwmDuration - on time
  currentMillis = millis(); // capture the current time
  managePwm();
  managePeriod();
  serial_Plotter();  
  reportStatus();
}
void loop1() 
{
//   speedValue = analogRead(speedAnalogIn); // Define setSpeed() according to input A2
   speedValue = map(analogRead(speedAnalogIn), 0, 4095, 400, 4095); // Define setSpeed() according to input A2
   stepper1.setSpeed(speedValue); // Step the motor with a constant speed previously set by setSpeed();
   stepper1.runSpeed();
   Serial.print("Speed ");
   Serial.println(speedValue);
}
void reportStatus() 
{
  Serial.print("Duty time is");
  Serial.print("\t");
  Serial.print(pwmDuration);
  Serial.print("\t");
  Serial.print("Period time is");
  Serial.print("\t");
  Serial.print(pwmPeriod);
  Serial.print("\t");
  Serial.print("Output");
  Serial.print("\t");
  Serial.print(plot);
  Serial.println("\t");
}
void serial_Plotter() 
{
  if(pwmState == HIGH) 
  {
    plot=1;
  } else 
  {
    plot=0;
  }
}
void managePwm() 
{
  //check if it's time to change the PWM output yet 
  if(currentMillis - previousMillisPwm > pwmDuration) 
  {
    //store the time of this change
    pwmState = LOW;
    digitalWrite(pwmPin, pwmState);
  }
}
void managePeriod() 
{
  //check if it's time to reset the output yet 
  if(currentMillis - previousMillisPeriod > pwmPeriod) 
  {
    previousMillisPeriod = currentMillis;
    previousMillisPwm = currentMillis;
    pwmState = HIGH;
    digitalWrite(pwmPin, pwmState);
  }
}

...but the text outputs from core0 and core1 arrive on top of each other. That's not an issue really. 
 
20:21:07.245 -> 
20:21:07.245 -> Duty time is Speed 242604
20:21:07.245 -> Period time isSpeed 6041094
20:21:07.245 -> Speed Output604
20:21:07.245 -> 0Speed 604
20:21:07.245 -> 
20:21:07.245 -> Duty time isSpeed 604241
20:21:07.245 -> Speed Period time is604
20:21:07.245 -> 1089Speed 603Output
20:21:07.245 -> Speed 0604
20:21:07.245 -> 
20:21:07.245 -> Speed 603Duty time is
20:21:07.245 -> 241Speed 605Period time is
20:21:07.245 -> Speed 1089605
20:21:07.245 -> OutputSpeed 6050
20:21:07.245 -> Speed 
20:21:07.245 -> 605
20:21:07.245 -> Duty time isSpeed 605243
20:21:07.245 -> Speed Period time is605
etc etc

Next - check the physical inputs and outputs actually function as expected. I seem to be making progress finally....

Wednesday, 19 March 2025

MIG pulse control software for Arduino ESP32 C3/S3 Super Mini

Any resolution on the hardware yet?

After endless rabbitholing, I've gone for the smallest, simplest microcontroller in the end. No need for pointless displays or masses of IO here.

Currently, I'm using the "DUBEUYEW ESP32-C3 Development Board Mini " obtained through Amazon. 


Being a C3 version of the ESP32, it has a single core micro. Here is some useful background on it, including the pinout etc.

So obviously I've ordered an "S3" equivalent "Waveshare ESP32-S3 Mini Development Board, based on ESP32-S3FH4R2" which has 2 cores. 


This dual core S3 version should allow me to run the "MIG pulsing" software independently to the "torch motion" code, as I found the stepper code was rather lumpy and didn't get on well alongside the pulsing code. To be fair, the Arduino Uno R4 isn't the same as the ESP32, so I can't compare their speeds directly and can't be bothered to do any testing myself. 

NB: One lesson I learned when running my existing (Arduino Uno) code in the ESP32C3 is that the ADC has 4095 count instead of 1023. I guess (without checking) that it has an extra couple of bits. This required me to rescale the input by a factor of 4.

Got anywhere with the motor and driver?

Well, I've decided to ditch the stepper driver software for now, unless it turns out it's the only way to go. Instead, I'm hoping to pass the bulk of the task off to a pukka servo device like this, also from Waveshare

Although you can't see it easily from this angle, it's double ended and comes with metal flanges for each end. So I should be able to make up wheels with o-ring tyres, rather like the little bogey I previously constructed.

The datasheet for this ST3215 looks encouraging, although it uses an RS485 serial bus for control, so I'll need to look into that more closely. 

There are some examples to help applying this servo, although the default expectation is that you will use one of their ESP32-based "driver boards". They refer obliquely to "secondary development" where you can drive the servo yourself without the driver board. This is the route I'd want to go, as I don't want / need an additional ESP32 just to drive the servo.

But - how's the software coming along, Fatty?

Well at least that seems to have largely resolved itself for now. I've got the pulse frequency / duty cycle functions working nicely now:

  • Pulse width setting is managed within function managePwm();
  • Pulse frequency (period) is managed within function managePeriod();
  • These use "non-blocking" operations (rather than "delay" which causes the micro to go on hold for the duration)
  • Currently, motorSpeedIn is simply set by reading a pot and generating a value from 0 to 4095. I will use this to control the servo, once I've figured out what I need to do.
  • For debug purposes, I have a reportStatus() function to drive the serial monitor, showing the current values of pwmDuration, pwmPeriod and motorSpeedIn.

Here's the code in its entirety:

#include <Arduino.h>
// Muzzer 18th March 2025
// Select "LOLIN C3 Pico" to compile for the tiny ESP32
const int pwmPin =  8;            // the number of the (blue) LED pin on the ESP32 C3 Pico
const int dutyCycleAnalogIn = A0; // Analog input- PWM duty
const int periodAnalogIn = A1;    // Analog input - period (ms)
const int speedAnalogIn = A2;     // Analog input - speed (Hz)
const int maxPeriod = 3000;       // hard coded max pulse time in ms
const int minPeriod = 500;        // hard coded min pulse time in ms
const int minDuty = 20;           // hard coded min duty %
int pwmState = HIGH;              // Led state for PWM output
int pwmSensorValue = 0;           // Value read from the pot - for debug only
int periodSensorValue = 0;        // Input from pot - for debug only
int motorSpeedIn = 0;             //  
long previousMillisPwm = 0;       // Last time PWM output was updated
long previousMillisPeriod = 0;    // 
long pwmDuration = 2000;          // Interval for PWM output (milliseconds)
long pwmPeriod = 2000;            // Interval between spots (milliseconds)
unsigned long currentMillis = 0;  // Initialise current time
void setup() 
{
  Serial.begin(9600);             // Start the Serial Monitor
  pinMode(pwmPin, OUTPUT);        // Set the selected PWM output pin as an OUTPUT
}
void loop() 
{
  currentMillis = millis();                                 // capture the current time
  managePwm();                                              // Terminate mark when the time is right
  managePeriod();                                           // Start new mark at the end of the period
  reportStatus();                                           // Debug output to Serial Monitor
  motorSpeedIn = analogRead(speedAnalogIn);                 // Define setSpeed() according to input A2 (0...4095)
}
void reportStatus() 
{
  Serial.print("Duty time is");
  Serial.print("\t");
  Serial.print(pwmDuration);
  Serial.print("\t");
  Serial.print("Period time is");
  Serial.print("\t");
  Serial.print(pwmPeriod);
  Serial.print("\t");
  Serial.print("Output");
  Serial.print("\t");
  Serial.print(pwmState);
  Serial.print("\t");
  Serial.print("Speed Input");
  Serial.print("\t");
  Serial.println(motorSpeedIn);
}
void managePwm() 
{
  pwmDuration = map(analogRead(dutyCycleAnalogIn), 0, 4095, (pwmPeriod*minDuty/100), pwmPeriod); // map input to pwmDuration - on time
  //check if it's time to terminate the PWM output yet 
  if(currentMillis - previousMillisPwm > pwmDuration) 
  {
    //store the time of this change
    // LOW = OFF: end the pulse. NB: LOW actually lights up the LED!
    pwmState = LOW;
    digitalWrite(pwmPin, pwmState);
  }
}
void managePeriod() 
{
  pwmPeriod = map(analogRead(periodAnalogIn), 0, 4095, minPeriod, maxPeriod); // pwmPeriod = time between spots
  //check if it's time to reset the output to start the next pulse yet 
  if(currentMillis - previousMillisPeriod > pwmPeriod) 
  {
    previousMillisPeriod = currentMillis;
    previousMillisPwm = currentMillis;
    // HIGH = ON: reset the output for next pulse
    pwmState = HIGH;
    digitalWrite(pwmPin, pwmState);
  }
}

Obviously, Blogger has screwed up the formatting so that the tab settings (spaces) are all over the place. But it's all there for posterity.

Next:
  • Figure out how to drive this ST3215 servo thing from the ESP32 C3 / S3
  • Do I need to go to dual core operation to get the pulse and motion functions to play nicely?
  • Does the S3 work the same as the C3 or will I need to mess with the code? Should be relatively quick and easy to find out...

Monday, 17 March 2025

Arduino travails and rabbit holes

Last time we heard, I was looking into the practicalities of implementing the pulsing and movement functions for a motorised welding torch. This stared out innocently enough with a revisit to the Arduino IDE and its associated Arduino microcontroller family. Since it first started out with Atmel micros, the family has expanded to include a whole range of different form factors, micro suppliers etc.

Too many choices!

Obvs this triggered a spate of unprovoked purchases of all manner of possible options for this application. From right to left:

I previously got my torch pulsing feature working on the Uno R4 - but the processor load seemed to be enough to interfere with the travel (stepper motor drive) so that the stepper was very lumpy. I guess the thread duration was long enough to delay the ideal pulse output. This might be an argument for a dual core processor or perhaps a more powerful device, Having said that, the Uno R4 has a reasonable powerful 32 bit Renesas processor.

Note for reference:



Graphics complications:

The reason for getting the "diymore ESP32 ESP-WROOM-32 Development Board Type-C1.96 inch OLED Module" was the excitement of possibly being able to display some form of info about the pulse and speed settings. That might be rather daft, as the simpler the torch controls the better and there are no brownie points for coding machismo last time we looked. Furthermore, there's very little info about the part - it's not recognised on the diymore website. 

One point to note is that this is not an OLED display at all - it's a backlit TFT LCD display. The driver IC is an ST7789 and the interface is a fairly standard "12 pin" arrangement.


This github page offers ESP32 1.9" LCD code but after wasting several hours of my life wondering why the display was permanently black, I was finally able to figure out that it requires the backlight at pin 32 to be enabled!! I inserted a couple of lines to turn on digital output 32 ("D32" as identified in the pic above) and "there was light".

I couldn't find the exact display but it looks very similar to this Similarly, although this device has fewer pins, it has some useful(?) inks 

Finally, to generate a suitable "C style data array" for the device driver, there's a handy tool for converting JPG files to the correct format. Note the required settings are shown in the code linked above ie:

a. Palette mod: 16bit RRRRRGGGGGGBBBBB(2byte/pixel)
b. Resize:  170x320
c. Data type: uint16_t
d. Keep the default settings for other parameters
e. Copy the result into the following imageData array

The code that came with the board turns out to be the same as on the github page, showing some form of Sci Fi spaceship.

Do I really want to bugger about with fancy graphics on this gadget? On reflection, although it's interesting from a geeky POV, I think I just need to behave myself and focus on the knitting. 3 pots and no display would be perfectly fine.

Unresponsive ESP32 boards

Some of these boards have the Nano or Super Mini ("half Nano"?) form factor but come with no documentation to speak of. The pin mapping seems to be a mystery and the USB driver gets upset with the Arduino IDE. This resulted in much swearing and hair pulling. 

The resolution is to avoid these "off piste" Chinesium products if there is no documentation. The onboard LED for the "C3 Super Mini" is on pin 8, FWIW. But confusingly, it seems there are quite a few apparently similar products out there with different pin mapping, so be careful where you look for code and which board you select in the IDE Board Manager. For reference, that one responds OK to being addressed as "LOLIN C3 MINI".

Painfully slooooow compiling on the Dell laptop - RESOLVED

This was another vast waste of my life. The record was set when it took 20 mins to compile a really simple program (below). Something was up - but what exactly? Although the Dell, laptop is getting on abt (9 years?), it's got an i7 9550 processor, 16BG of RAM, 500GB of SSD and a graphics card, so shouldn't be struggling with a simple Arduino program. Interestingly, it also ground to a near halt when running PlatformIO within VSCode. WTF??


As an aside, this was another (aborted) experiment aimed at implementing the stepper speed control without ending up in a fight with the pulse function. Turns out the accelStepper functions also suffer from contention with the pulse functions. Given the complexity of the accelStepper library and the poor, almost nonexistent documentation for the STP/DIR implementation, I gave it up as a bad job. Although the software is apparently quite powerful and reasonably well written, the author is clearly a twat, as he couldn't be arsed to write it up and doesn't take well to requests for help. Sod him.

After a lot of buggering about, searching etc, I finally found a post about interference of the Arduino IDE by the IBM "Trusteer" security app. I think I installed this some years ago after being invited to do so by my bank and it's stayed there ever since, periodically notifying me of the current security status. Sure enough, uninstalling Trusteer transformed the compilation time for both Arduino IDE and PlatformIO/VSC. Thank god I spotted that post. To be fair, most "slow compilation" posts refer to anti virus as the root cause but as I already eliminated the core AV app, I'd discounted that line of thought.

Auto (not) darkening helmet!

Yes, I've owned a Lincoln Electric auto darkening welding helmet for the past decade or so. Generally I've been pretty happy with it. It has adjustable sensitivity (9-13) and has a photo cell array on the front, so that it doesn't rely on battery replacement to keep running.


Or so I thought. I dug it out recently for use with the brace of Arc Captain welders I rashly acquired and found that the "auto darkening" wasn't remotely happening. The upside of this was that I was able to rush out and get one of the new "True Colour" helmets from Yeswelder (Chinese) that claim to show colour (rather than dark green).

However, I'm pigged off that it stopped working. So what was going on here? If it has photo cells on the front, it shouldn't "run out of battery" should it? Well it turns out they are sneaky fuckers. Read on to find out....

Let's take a closer look. The adjustment knob simply pulls out and the pot can then be released:

And the cartridge is held in with a spring clip.

Quite a simple, self-contained assembly

There are a couple of holes either side of the photo cell array. These are (UV) light detectors, used to trigger the darkening.

But it's glued together. However, we have the technology...

And there you have it. 2 lithium coin cells, both dead as a fart, with a pair of solar cell arrays. It seems that these cartridges have a dual redundant circuit using 2 batteries, 2 photo cell arrays and presumably 2 largely independent control circuits. There's a reference to some sort of safety standard for welding helmets on the inside of the helmet. I couldn't be arsed to look up the details but I assume it requires some degree of failsafe operation.

Anyway, once these cells go flat it is evident that the whole cartridge ceases to operate. They didn't advertise that when I was buying the (expensive) helmet in the first instance but I expect they thought you'd simply go out and buy another one. Well it turned out they were wrong on that count.

The modern replacement for the cartridge is a mere $291 MSRP and the equivalent full helmet is about the same.

Those button cells are simply non-rechargeable lithium cells. Annoyingly:

  • They are not the "normal"  CR2032 but a weird CR2335 ie 23mm diameter and 3.5mm thick. 
  • There is no battery "holder" - these are soldered directly to the PCBA using solder tags. It's not obvious that I'd want to solder wires directly to these boys - even if they survive / don't burst, I doubt it would do them any good.

If you look carefully, you can see the UV detectors at the bottom of the photo. Also 2 sets of wires from the (redundant) photo cells.

The circuit must be pretty simple, as they have gone to the bother of grinding off the part numbers from the 2 ICs. I expect the small device is a comparator and the bigger device is probably just a CMOS or LS logic device. How pathetic, although it wouldn't take much detective work to figure out what they are from the tracking around them.



So I managed to find some similar-ish BR2330 cells at CPC Farnell for £1.55 each (plus ~£2.50 delivery, IIRC). These are simply CR2330 cells with solder tabs - but no insulation. Being slightly thinner and perhaps 20% lower capacity won't be a massive problem. Certainly, it'll be a massive improvement on what's currently in there.

Some polyester tape and tinned copper wire did the trick:


Then some more nasty yellow tape to hold the cartridge body together and the job is done. 

And yes it works, although the photo detectors on the front are clearly looking for UV light. Shining a high intensity LED floodlight at it does nothing, yet even a small arc from the MIG darkens the lens. There's then a few hundred milliseconds before the lens clears.

Friday, 28 February 2025

Spitfire, sh1tfire!

Ooof. I have this WW2 clock from a Spitfire that I acquired back in the late 70s. Looks in pretty good nick for its age and also seems to be in reasonable semi-running order, as in it almost keeps ticking when you set it off. I guess it needs a clean and some watch oil.


It's marked "AM" on the back, which stands for "Air Ministry".




The movement is Swiss but the watch was manufactured by "Smith and Sons (MA) Ltd" in London. The date is 11th November, 1938, which ties in.


That spring and lever allow the time to be set by rotating the bezel, rather than wind it up.




That looks like an adjustor for the timekeeping, possibly missing a nub? The curved thing connects the external button to the sprung lever which allows time setting. Once released, the bezel defaults to winding it up. This is an "8 day" clock.


Anyway, what about those suspicious looking "luminous" hands? Out with the Radiacode radiation detector.

Hmm. Should I be worried? I'm seeing perhaps 6mR/h, which is about 60uS/h. Sounds as if I shouldn't crush up the paint and put it on my cornflakes. However, once it's a few feet away, the count rate falls right off. 


I need to get some watch oil and lube it up a bit. It almost works but stops ticking after a few seconds. I guess 90 year old oil possibly isn't quite as effective as it was when it left the factory.

Well that was a bit of fun. Finally I have something to wake up the Radiacode with.

Tuesday, 25 February 2025

Tiny OLED display for tiny Arduino ESP32C3

To get this little generic 128 x 32 pixel OLED display working with the ESP32C3 Dev board, need to load the correct library and also connect up the clock and data pins on the display to the right pins on the ESP32C3.

Good little video showing how to do this by DroneBot Workshop:


There's an example (demo) sketch in the std library at Files > Manage libraries... > Examples from Custom Libraries (right near the bottom) > Adafruit SSD1306 > ssd1306_128x32_i2c:


/**************************************************************************
 This is an example for our Monochrome OLEDs based on SSD1306 drivers
 Pick one up today in the adafruit shop!
 ------> http://www.adafruit.com/category/63_98
 This example is for a 128x32 pixel display using I2C to communicate
 3 pins are required to interface (two I2C and one reset).
 Adafruit invests time and resources providing this open
 source code, please support Adafruit and open-source
 hardware by purchasing products from Adafruit!
 Written by Limor Fried/Ladyada for Adafruit Industries,
 with contributions from the open source community.
 BSD license, check license.txt for more information
 All text above, and the splash screen below must be
 included in any redistribution.
 **************************************************************************/
// #include <SPI.h> probably not needed
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 32 // OLED display height, in pixels
// Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)
// The pins for I2C are defined by the Wire-library. 
// On an arduino UNO:       A4(SDA), A5(SCL)
// On an arduino MEGA 2560: 20(SDA), 21(SCL)
// On an arduino LEONARDO:   2(SDA),  3(SCL), ...
#define OLED_RESET     -1 // Reset pin # (or -1 if sharing Arduino reset pin)
#define SCREEN_ADDRESS 0x3C ///< See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
#define NUMFLAKES     10 // Number of snowflakes in the animation example
#define LOGO_HEIGHT   16
#define LOGO_WIDTH    16
static const unsigned char PROGMEM logo_bmp[] =
{ 0b00000000, 0b11000000,
  0b00000001, 0b11000000,
  0b00000001, 0b11000000,
  0b00000011, 0b11100000,
  0b11110011, 0b11100000,
  0b11111110, 0b11111000,
  0b01111110, 0b11111111,
  0b00110011, 0b10011111,
  0b00011111, 0b11111100,
  0b00001101, 0b01110000,
  0b00011011, 0b10100000,
  0b00111111, 0b11100000,
  0b00111111, 0b11110000,
  0b01111100, 0b11110000,
  0b01110000, 0b01110000,
  0b00000000, 0b00110000 };
void setup() {
  Serial.begin(9600);
  // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
  if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
    Serial.println(F("SSD1306 allocation failed"));
    for(;;); // Don't proceed, loop forever
  }
  // Show initial display buffer contents on the screen --
  // the library initializes this with an Adafruit splash screen.
  display.display();
  delay(2000); // Pause for 2 seconds
  // Clear the buffer
  display.clearDisplay();
  // Draw a single pixel in white
  display.drawPixel(10, 10, SSD1306_WHITE);
  // Show the display buffer on the screen. You MUST call display() after
  // drawing commands to make them visible on screen!
  display.display();
  delay(2000);
  // display.display() is NOT necessary after every single drawing command,
  // unless that's what you want...rather, you can batch up a bunch of
  // drawing operations and then update the screen all at once by calling
  // display.display(). These examples demonstrate both approaches...
  testdrawline();      // Draw many lines
  testdrawrect();      // Draw rectangles (outlines)
  testfillrect();      // Draw rectangles (filled)
  testdrawcircle();    // Draw circles (outlines)
  testfillcircle();    // Draw circles (filled)
  testdrawroundrect(); // Draw rounded rectangles (outlines)
  testfillroundrect(); // Draw rounded rectangles (filled)
  testdrawtriangle();  // Draw triangles (outlines)
  testfilltriangle();  // Draw triangles (filled)
  testdrawchar();      // Draw characters of the default font
  testdrawstyles();    // Draw 'stylized' characters
  testscrolltext();    // Draw scrolling text
  testdrawbitmap();    // Draw a small bitmap image
  // Invert and restore display, pausing in-between
  display.invertDisplay(true);
  delay(1000);
  display.invertDisplay(false);
  delay(1000);
  testanimate(logo_bmp, LOGO_WIDTH, LOGO_HEIGHT); // Animate bitmaps
}
void loop() {
}
void testdrawline() {
  int16_t i;
  display.clearDisplay(); // Clear display buffer
  for(i=0; i<display.width(); i+=4) {
    display.drawLine(0, 0, i, display.height()-1, SSD1306_WHITE);
    display.display(); // Update screen with each newly-drawn line
    delay(1);
  }
  for(i=0; i<display.height(); i+=4) {
    display.drawLine(0, 0, display.width()-1, i, SSD1306_WHITE);
    display.display();
    delay(1);
  }
  delay(250);
  display.clearDisplay();
  for(i=0; i<display.width(); i+=4) {
    display.drawLine(0, display.height()-1, i, 0, SSD1306_WHITE);
    display.display();
    delay(1);
  }
  for(i=display.height()-1; i>=0; i-=4) {
    display.drawLine(0, display.height()-1, display.width()-1, i, SSD1306_WHITE);
    display.display();
    delay(1);
  }
  delay(250);
  display.clearDisplay();
  for(i=display.width()-1; i>=0; i-=4) {
    display.drawLine(display.width()-1, display.height()-1, i, 0, SSD1306_WHITE);
    display.display();
    delay(1);
  }
  for(i=display.height()-1; i>=0; i-=4) {
    display.drawLine(display.width()-1, display.height()-1, 0, i, SSD1306_WHITE);
    display.display();
    delay(1);
  }
  delay(250);
  display.clearDisplay();
  for(i=0; i<display.height(); i+=4) {
    display.drawLine(display.width()-1, 0, 0, i, SSD1306_WHITE);
    display.display();
    delay(1);
  }
  for(i=0; i<display.width(); i+=4) {
    display.drawLine(display.width()-1, 0, i, display.height()-1, SSD1306_WHITE);
    display.display();
    delay(1);
  }
  delay(2000); // Pause for 2 seconds
}
void testdrawrect(void) {
  display.clearDisplay();
  for(int16_t i=0; i<display.height()/2; i+=2) {
    display.drawRect(i, i, display.width()-2*i, display.height()-2*i, SSD1306_WHITE);
    display.display(); // Update screen with each newly-drawn rectangle
    delay(1);
  }
  delay(2000);
}
void testfillrect(void) {
  display.clearDisplay();
  for(int16_t i=0; i<display.height()/2; i+=3) {
    // The INVERSE color is used so rectangles alternate white/black
    display.fillRect(i, i, display.width()-i*2, display.height()-i*2, SSD1306_INVERSE);
    display.display(); // Update screen with each newly-drawn rectangle
    delay(1);
  }
  delay(2000);
}
void testdrawcircle(void) {
  display.clearDisplay();
  for(int16_t i=0; i<max(display.width(),display.height())/2; i+=2) {
    display.drawCircle(display.width()/2, display.height()/2, i, SSD1306_WHITE);
    display.display();
    delay(1);
  }
  delay(2000);
}
void testfillcircle(void) {
  display.clearDisplay();
  for(int16_t i=max(display.width(),display.height())/2; i>0; i-=3) {
    // The INVERSE color is used so circles alternate white/black
    display.fillCircle(display.width() / 2, display.height() / 2, i, SSD1306_INVERSE);
    display.display(); // Update screen with each newly-drawn circle
    delay(1);
  }
  delay(2000);
}
void testdrawroundrect(void) {
  display.clearDisplay();
  for(int16_t i=0; i<display.height()/2-2; i+=2) {
    display.drawRoundRect(i, i, display.width()-2*i, display.height()-2*i,
      display.height()/4, SSD1306_WHITE);
    display.display();
    delay(1);
  }
  delay(2000);
}
void testfillroundrect(void) {
  display.clearDisplay();
  for(int16_t i=0; i<display.height()/2-2; i+=2) {
    // The INVERSE color is used so round-rects alternate white/black
    display.fillRoundRect(i, i, display.width()-2*i, display.height()-2*i,
      display.height()/4, SSD1306_INVERSE);
    display.display();
    delay(1);
  }
  delay(2000);
}
void testdrawtriangle(void) {
  display.clearDisplay();
  for(int16_t i=0; i<max(display.width(),display.height())/2; i+=5) {
    display.drawTriangle(
      display.width()/2  , display.height()/2-i,
      display.width()/2-i, display.height()/2+i,
      display.width()/2+i, display.height()/2+i, SSD1306_WHITE);
    display.display();
    delay(1);
  }
  delay(2000);
}
void testfilltriangle(void) {
  display.clearDisplay();
  for(int16_t i=max(display.width(),display.height())/2; i>0; i-=5) {
    // The INVERSE color is used so triangles alternate white/black
    display.fillTriangle(
      display.width()/2  , display.height()/2-i,
      display.width()/2-i, display.height()/2+i,
      display.width()/2+i, display.height()/2+i, SSD1306_INVERSE);
    display.display();
    delay(1);
  }
  delay(2000);
}
void testdrawchar(void) {
  display.clearDisplay();
  display.setTextSize(1);      // Normal 1:1 pixel scale
  display.setTextColor(SSD1306_WHITE); // Draw white text
  display.setCursor(0, 0);     // Start at top-left corner
  display.cp437(true);         // Use full 256 char 'Code Page 437' font
  // Not all the characters will fit on the display. This is normal.
  // Library will draw what it can and the rest will be clipped.
  for(int16_t i=0; i<256; i++) {
    if(i == '\n') display.write(' ');
    else          display.write(i);
  }
  display.display();
  delay(2000);
}
void testdrawstyles(void) {
  display.clearDisplay();
  display.setTextSize(1);             // Normal 1:1 pixel scale
  display.setTextColor(SSD1306_WHITE);        // Draw white text
  display.setCursor(0,0);             // Start at top-left corner
  display.println(F("Hello, world!"));
  display.setTextColor(SSD1306_BLACK, SSD1306_WHITE); // Draw 'inverse' text
  display.println(3.141592);
  display.setTextSize(2);             // Draw 2X-scale text
  display.setTextColor(SSD1306_WHITE);
  display.print(F("0x")); display.println(0xDEADBEEF, HEX);
  display.display();
  delay(2000);
}
void testscrolltext(void) {
  display.clearDisplay();
  display.setTextSize(2); // Draw 2X-scale text
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(10, 0);
  display.println(F("scroll"));
  display.display();      // Show initial text
  delay(100);
  // Scroll in various directions, pausing in-between:
  display.startscrollright(0x00, 0x0F);
  delay(2000);
  display.stopscroll();
  delay(1000);
  display.startscrollleft(0x00, 0x0F);
  delay(2000);
  display.stopscroll();
  delay(1000);
  display.startscrolldiagright(0x00, 0x07);
  delay(2000);
  display.startscrolldiagleft(0x00, 0x07);
  delay(2000);
  display.stopscroll();
  delay(1000);
}
void testdrawbitmap(void) {
  display.clearDisplay();
  display.drawBitmap(
    (display.width()  - LOGO_WIDTH ) / 2,
    (display.height() - LOGO_HEIGHT) / 2,
    logo_bmp, LOGO_WIDTH, LOGO_HEIGHT, 1);
  display.display();
  delay(1000);
}
#define XPOS   0 // Indexes into the 'icons' array in function below
#define YPOS   1
#define DELTAY 2
void testanimate(const uint8_t *bitmap, uint8_t w, uint8_t h) {
  int8_t f, icons[NUMFLAKES][3];
  // Initialize 'snowflake' positions
  for(f=0; f< NUMFLAKES; f++) {
    icons[f][XPOS]   = random(1 - LOGO_WIDTH, display.width());
    icons[f][YPOS]   = -LOGO_HEIGHT;
    icons[f][DELTAY] = random(1, 6);
    Serial.print(F("x: "));
    Serial.print(icons[f][XPOS], DEC);
    Serial.print(F(" y: "));
    Serial.print(icons[f][YPOS], DEC);
    Serial.print(F(" dy: "));
    Serial.println(icons[f][DELTAY], DEC);
  }
  for(;;) { // Loop forever...
    display.clearDisplay(); // Clear the display buffer
    // Draw each snowflake:
    for(f=0; f< NUMFLAKES; f++) {
      display.drawBitmap(icons[f][XPOS], icons[f][YPOS], bitmap, w, h, SSD1306_WHITE);
    }
    display.display(); // Show the display buffer on the screen
    delay(200);        // Pause for 1/10 second
    // Then update coordinates of each flake...
    for(f=0; f< NUMFLAKES; f++) {
      icons[f][YPOS] += icons[f][DELTAY];
      // If snowflake is off the bottom of the screen...
      if (icons[f][YPOS] >= display.height()) {
        // Reinitialize to a random position, just off the top
        icons[f][XPOS]   = random(1 - LOGO_WIDTH, display.width());
        icons[f][YPOS]   = -LOGO_HEIGHT;
        icons[f][DELTAY] = random(1, 6);
      }
    }
  }
}

And checking the ESP32C3 pinout for the clock (SCL or SLC?) pin 12 and data (SDA) pin 13, I powered it up and I seem to have a goer. It's upside down of course.


That's a hopeful start. Obvs I don't want all the demo code in my project but I can strip that out and replace it with my setpoint data.

Time to start behaving

Well, the endless rabbitholing seems determined to persist. The Waveshare ESP32S3 boards I got from Amazon seem to indeed "share" ...