LED String with 1D game

I saw a game like this on the WHY2025, I wanted my own version.

Using Twang32, some other hardware and a tweaked sketch, I got this.

Up = move up
Down = move down
whack the stick to kill red leds (Metal part is a door spring)
(Tilting joystick also works as up/down)

Things changed:

  • Other hardware: MPU6050 for movement detection, Sound generation
    No clock on my LedString
  • Changed sketch, fastled, pins and libraries
  • Using IDE in 2025? .. downgrade ESP32 (expressive) to 2.0.11, and download Fastled to 3.9.0
    2025 libraries won’t work!

Antenna tweaking

A while ago I was playing with LoRa (Long Range radio), I made a simple setup which worked good enough.

After that I installed Meshtastic

I also used OpenMqttGateway with LoRa.

I’ve been using antennas also with SDR(Software Defined Radio.

Not happy with the performance, I bought a Nano-VNA.
(Vector network analyser)

Due to the many options, I was lost at first. Maybe I have to ask Bigred.
Calibrating I get now, but I can’t easily calibrate an antenna with fixed cable.

Much to learn, but that’s what I want. 🙂

WHY2025

In case of doubt .. MORE LEDS!

We went to WHY2025 a hackers camp in the Netherlands.

The first time I went was in 1997, with Bigred.
Many followed after that.
Tyrone, Bigred were also there from our old Crew.
Coline joined me several times since 2005.

I joined the Badge team, and was making spacers for the Badges in bulk using my 3D printer.
Also made some fancy cases.

In case of doubt .. more leds!

Nice weather, good friends. New friends. Booze. Food and Hacking.
We visited a lot of talks and enjoyed the music. (And fire)

I worked on: RSS feed on a epaper display, Midi monitor and the MQTT Pong website.

RSS Feed display

While waiting in line for the Badge:

A stone was passed from behind!
It was a ping request. We passed it forward, and 15 minutes later a TTL time exceeded stone came from the front of the line.
You gotta love those nerds!

The Badge:
This should have got much potential ..
Many misses, much to learn.

Sadly broken:

Our 7M Led string attached to Bigred’s Antenna.

BirdNet installation

I bought Peterson’s Vogelgids, just for fun.
It’s an old version, but that’s on purpose.

Then I saw a little project named BirdNet Pi.
(I used the Android app already)

This is a Raspberry installation which recognises bird sounds. And gives you statistics about the detected birds.
Cool for identifying birds in my garden.

Next to do : Integration in Home Assistant

Bert Visscher has the same book.

Ultrasonic ageing booze, while soldering

While doing other stuff, I experimented with fake barrel ageing booze using my ultrasonic cleaner.

Using this method, you can give a barrel/wood taste to your liquids.

I used a Whisky barrel piece to give a nicer/better taste to generic Vodka.

Next to do, other woods and spirits. Like Cognac woodbarrel and plain white Rum.

I did 3 times 15 minutes, but should be longer!

Look at the colour change!
It smelled better, and the taste was great.
Smooth, round and far less harsh as the plain Vodka.

Cynthcart (c64) midi control

I bought a teensyrom a while ago.

UPDATE 20250712 : Display

Tyrone and I wanted to control the settings using potmeters.
So I grabbed a Teensy 4.1 controller and some 10K potmeters, and worte some code

Code for 12 pots, pitch bend and display, don’t forget to set your USB mode to midi!
Schematic soon, also all tweaks and note sending.

Todo: Nice 3D printed Pitch Bend wheel, rest of display code and extra buttons!

Let’s use an old box to hold the pots!

3D printed wooden knobs (Yes wood filament)
Unstable release pot due too bad wires. Cynthcart has no decay and hold (hold is how long you are pressing key)


#include <MIDIUSB.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
#define OLED_RESET 4
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// A0 -> A3, A6 -> A13
const int numPots = 12;

// A14 - Pitch bend!
int lastPitch = -1;

//A4 A5 I2C display


// Custom mappings:
const int potPins[numPots]     = {A0, A1, A2, A3, A6, A7, A8, A9, A10, A11, A12, A13};  // Analog pins
const int ccNumbers[numPots]   = {0,1,2,3,4,5,19,7,8,9,13,14};             // CC numbers
const int midiChannels[numPots]= {1,1,1,1,1,1,1,1,1,1,1,1};                // MIDI channels (1–16)

int lastValues[numPots];  // Store last values to reduce redundant MIDI messages

void setup() {
  for (int i = 0; i < numPots; i++) {
    pinMode(potPins[i], INPUT);
    lastValues[i] = -1;
  }
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.setTextSize(2);
  display.setTextColor(WHITE);
  display.clearDisplay();
  display.display();
}

void loop() {
  for (int i = 0; i < numPots; i++) {
    int analogValue = analogRead(potPins[i]);
    int midiValue = analogValue / 8;  // Scale 0–1023 to 0–127

    if (abs(midiValue - lastValues[i]) > 1) {
      usbMIDI.sendControlChange(ccNumbers[i], midiValue, midiChannels[i]);
      lastValues[i] = midiValue;
    }
  }
  int potValue = analogRead(A12);         // Read pot (0–1023)
  int pitchBend = map(potValue, 0, 1023, 0, 16383); // Map to MIDI Pitch Bend range

  if (abs(pitchBend - lastPitch) > 5) { // Send only on significant change
    sendPitchBend(pitchBend, 0); // Channel 1
    lastPitch = pitchBend;
  }

  displayInfo();


  delay(5);  // CPU-friendly update rate
}

void sendPitchBend(int value, byte channel) {
  byte lsb = value & 0x7F;
  byte msb = (value >> 7) & 0x7F;

  midiEventPacket_t pitchBendPacket = {0x0E, 0xE0 | (channel & 0x0F), lsb, msb};
  MidiUSB.sendMIDI(pitchBendPacket);
  // Needed? 
  MidiUSB.flush();
}

void displayInfo(){
 byte x0, y0, x1, y1;     // start/end coordinates for drawing lines on OLED
  display.clearDisplay();
  display.setCursor(0, 0);
  display.print("Attack / Release");

  // draw attack line
  x0 = 0;
  y0 = 63;
  x1 = map(attackParam, 0, 127, 0, ((SCREEN_WIDTH / 4) - 1));
  y1 = 20;
  display.drawLine(x0, y0,  x1,  y1, SH110X_WHITE);

   // draw release line
  x0 = x1;  // start line from previous line's final x,y location
  y0 = y1;
  x1 = x0 + map(releaseParam, 0, 127, 0, ((SCREEN_WIDTH / 4) - 1));
  y1 = 63;
  display.drawLine(x0, y0,  x1,  y1, SH110X_WHITE);

  display.display();

}

Webcam image slash command mattermost

I’ve got my own chat server, because WhatsApp sucks.

With this I can play around.

Below is a script to capture an image from a (Reolink) webcam, and show this in a Mattermost channel.

You need to configure your /slash command in Mattermost and a webserver with PHP

When entering
/labcam
in a channel, an image will be shown.

Code:

<?php

// See token from screenshots above
$expected_token = 'YOUR_MATTERMOST_TOKEN';
$token = $_POST['token'] ?? '';

if ($token !== $expected_token) {
    http_response_code(403);
    echo 'Invalid token, go away';
    exit;
}


// Reolink camera settings
$ip = '192.168.1.2'; // Replace with your camera IP
$user = 'admin';       // Camera username
$pass = 'admin';// Camera password
$rs = uniqid();        // Unique request string

$url = "http://$ip/cgi-bin/api.cgi?cmd=Snap&channel=0&rs=$rs&user=$user&password=$pass";

// Temporary image save path (ensure this directory is public and writable)
$image_filename = 'snapshot_' . time() . '.jpg';
$image_path = __DIR__ . '/snapshots/' . $image_filename;  // e.g., public_html/snapshots/
$image_url = 'https://labcam.henriaanstoot.nl/snapshots/' . $image_filename; // Public URL

// Fetch image from Reolink using cURL
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERPWD, "$user:$pass");
$image_data = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($http_code !== 200 || !$image_data) {
    echo json_encode([
        'response_type' => 'ephemeral',
        'text' => 'Failed to get snapshot from Reolink camera.',
    ]);
    exit;
}

// Save image
file_put_contents($image_path, $image_data);

// Respond to Mattermost
$response = [
    'response_type' => 'in_channel',
    'text' => 'Live snapshot from camera:',
    'attachments' => [[
        'image_url' => $image_url,
        'fallback' => 'Reolink snapshot'
    ]]
];

header('Content-Type: application/json');
echo json_encode($response);

"If something is worth doing, it's worth overdoing."