Category Archives: Computer

Mini midi monitor

Two versions of a mini monitor

A version using a Arduino and a Midi shield (Yellow wires are for display)
D0 (RX) is used for the Midi IN signal.

10K pullups SDA/CLK

Above a Teensy 4.0 version. This one uses MIDI over USB.

Next to add: Rotary encoders, to select a CC Channel and display values graphically

CODE for Teensy version

#include <U8g2lib.h>
#include <Wire.h>
#include <MIDIUSB.h>   // Teensy's built-in USB MIDI

// SH1106 128x64 I2C constructor
U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);

void setup() {
  u8g2.begin();
  u8g2.clearBuffer();
  u8g2.setFont(u8g2_font_6x12_tf);
  u8g2.drawStr(0,12,"MIDI Monitor Ready");
  u8g2.sendBuffer();
}

void loop() {
  // Check for incoming MIDI

  while (usbMIDI.read()) {
  u8g2.clearBuffer();
  u8g2.setFont(u8g2_font_6x12_tf);
  u8g2.drawStr(0,12,"MIDI Monitor Ready");
  u8g2.sendBuffer();
    byte type = usbMIDI.getType();
    byte channel = usbMIDI.getChannel();
    byte data1 = usbMIDI.getData1();
    byte data2 = usbMIDI.getData2();

    int y = 24;

    if (type == usbMIDI.NoteOn && data2 > 0) {
      u8g2.setCursor(0, y);      u8g2.print("Note ON     "); // pad
      u8g2.setCursor(0, y+12);   u8g2.printf("Ch:%-3d", channel);  // pad width 3
      u8g2.setCursor(0, y+24);   u8g2.printf("Note:%-3d", data1);
      u8g2.setCursor(0, y+36);   u8g2.printf("Vel:%-3d", data2);
    } 
    else if (type == usbMIDI.NoteOff || (type == usbMIDI.NoteOn && data2 == 0)) {
      u8g2.setCursor(0, y);      u8g2.print("Note OFF    "); // pad
      u8g2.setCursor(0, y+12);   u8g2.printf("Ch:%-3d", channel);
      u8g2.setCursor(0, y+24);   u8g2.printf("Note:%-3d", data1);
    } 
    else if (type == usbMIDI.ControlChange) {
      u8g2.setCursor(0, y);      u8g2.print("Control Chg ");
      u8g2.setCursor(0, y+12);   u8g2.printf("Ch:%-3d", channel);
      u8g2.setCursor(0, y+24);   u8g2.printf("CC#:%-3d", data1);
      u8g2.setCursor(0, y+36);   u8g2.printf("Val:%-3d", data2);
    } 
    else {
      u8g2.setCursor(0, y);      u8g2.print("Other MIDI  ");
      u8g2.setCursor(0, y+12);   u8g2.printf("Type:%-3d", type);
    }

    u8g2.sendBuffer();
  }
}

CODE for Arduino plus shield

#include <U8g2lib.h>
#include <Wire.h>
#include <MIDI.h>   // FortySevenEffects MIDI library

// SH1106 128x64 I2C (page buffer, low RAM)
U8G2_SH1106_128X64_NONAME_1_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);

// MIDI on hardware Serial (RX=D0)
MIDI_CREATE_INSTANCE(HardwareSerial, Serial, MIDI);

char line[12];  // small buffer for formatting

void setup() {
  u8g2.begin();
  u8g2.setFont(u8g2_font_6x12_tf); // proportional font, small size

  // Initial message
  u8g2.firstPage();
  do {
    u8g2.setCursor(0, 12);
    u8g2.print("MIDI Monitor Ready");
  } while (u8g2.nextPage());

  MIDI.begin(MIDI_CHANNEL_OMNI);  // listen to all channels
}

void loop() {
  if (MIDI.read()) {
    byte type    = MIDI.getType();
    byte channel = MIDI.getChannel();
    byte data1   = MIDI.getData1();
    byte data2   = MIDI.getData2();

    // Page buffer redraw
    u8g2.firstPage();
    do {
      // Title
      u8g2.setCursor(0, 12);
      u8g2.print("MIDI Monitor");

      int y = 24; // start lower down

      if (type == midi::NoteOn && data2 > 0) {
        u8g2.setCursor(0, y);      
        u8g2.print("Note ON   ");

        snprintf(line, sizeof(line), "Ch:%-3d", channel);
        u8g2.setCursor(0, y+12);   u8g2.print(line);32

        snprintf(line, sizeof(line), "Note:%-3d", data1);
        u8g2.setCursor(0, y+24);   u8g2.print(line);

        snprintf(line, sizeof(line), "Vel:%-3d", data2);
        u8g2.setCursor(0, y+36);   u8g2.print(line);
      } 
      else if (type == midi::NoteOff || (type == midi::NoteOn && data2 == 0)) {
        u8g2.setCursor(0, y);      
        u8g2.print("Note OFF  ");

        snprintf(line, sizeof(line), "Ch:%-3d", channel);
        u8g2.setCursor(0, y+12);   u8g2.print(line);

        snprintf(line, sizeof(line), "Note:%-3d", data1);
        u8g2.setCursor(0, y+24);   u8g2.print(line);
      } 
      else if (type == midi::ControlChange) {
        u8g2.setCursor(0, y);      
        u8g2.print("Control Chg");

        snprintf(line, sizeof(line), "Ch:%-3d", channel);
        u8g2.setCursor(0, y+12);   u8g2.print(line);

        snprintf(line, sizeof(line), "CC#:%-3d", data1);
        u8g2.setCursor(0, y+24);   u8g2.print(line);

        snprintf(line, sizeof(line), "Val:%-3d", data2);
        u8g2.setCursor(0, y+36);   u8g2.print(line);
      } 
      else {
        u8g2.setCursor(0, y);      
        u8g2.print("Other MIDI");

        snprintf(line, sizeof(line), "Type:%-3d", type);
        u8g2.setCursor(0, y+12);   u8g2.print(line);
      }
    } while (u8g2.nextPage());
  }
}

Mac Address scanner for my home.

I bought 4x Xiao-S3 mini controllers. I want to place these all over my house to scan for Bluetooth and Wifi Clients. So I can do a location search for Mobile Phones, Keys and more.

Also the Bluetooth tags I used for the Scanner Game can be used!

I want to post a location to Home Assistant, But I also played with 3D view.

Using MQTT I can subscribe to the topic locator/scanner1/ble or locator/scanner1/wifi_clients

Problems I ran into.

Too many duplicates, fixed.
Can not scan Wifi when connected, so I connect every 30s.
Could not find all wifi clients, needed to scan all channels!

CODE

#include <WiFi.h>
#include <PubSubClient.h>
#include <BLEDevice.h>
#include <BLEScan.h>
#include <BLEAdvertisedDevice.h>
#include "esp_wifi.h"

const char* mqtt_server = "MQTTSERVER";   // change to your broker
const int   mqtt_port   = 1883;
const char* mqtt_user   = "";
const char* mqtt_pass   = "";

const char* ssid     = "MYWIFI";
const char* password = "MYWIFIPASSWD";

WiFiClient espClient;
PubSubClient client(espClient);

#define MAX_BUFFER 200

struct DeviceRecord {
  String type;   // "wifi_client" or "ble"
  String mac;
  int rssi;
};

DeviceRecord buffer[MAX_BUFFER];
int bufferCount = 0;

// ====== BLE ======
BLEScan* pBLEScan;
const int bleScanTime = 3; // seconds

typedef struct {
  unsigned frame_ctrl:16;
  unsigned duration_id:16;
  uint8_t addr1[6];
  uint8_t addr2[6];
  uint8_t addr3[6];
  uint16_t sequence_ctrl;
  uint8_t addr4[6];
} wifi_ieee80211_mac_hdr_t;

typedef struct {
  wifi_ieee80211_mac_hdr_t hdr;
  uint8_t payload[0];
} wifi_ieee80211_packet_t;

void formatMAC(const uint8_t *addr, char *buf) {
  sprintf(buf, "%02X:%02X:%02X:%02X:%02X:%02X",
          addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]);
}

// ====== Dedup ======
void addToBuffer(String type, String mac, int rssi) {
  for (int i = 0; i < bufferCount; i++) {
    if (buffer[i].mac == mac && buffer[i].type == type) {
      buffer[i].rssi = rssi;  // update latest RSSI
      return;
    }
  }
  if (bufferCount < MAX_BUFFER) {
    buffer[bufferCount].type = type;
    buffer[bufferCount].mac = mac;
    buffer[bufferCount].rssi = rssi;
    bufferCount++;
  }
}

// ====== Sniffer ======
void wifi_sniffer_packet_handler(void* buf, wifi_promiscuous_pkt_type_t type) {
  if (type != WIFI_PKT_MGMT && type != WIFI_PKT_DATA) return;

  const wifi_promiscuous_pkt_t *ppkt = (wifi_promiscuous_pkt_t *)buf;
  const wifi_ieee80211_packet_t *ipkt = (wifi_ieee80211_packet_t *)ppkt->payload;
  const wifi_ieee80211_mac_hdr_t *hdr = &ipkt->hdr;

  char mac[18];
  formatMAC(hdr->addr2, mac);
  int rssi = ppkt->rx_ctrl.rssi;

  addToBuffer("wifi_client", String(mac), rssi);
}

// ====== BLE CALLBACK ======
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
  void onResult(BLEAdvertisedDevice advertisedDevice) {
    String mac  = advertisedDevice.getAddress().toString().c_str();
    int rssi    = advertisedDevice.getRSSI();
    addToBuffer("ble", mac, rssi);
  }
};

void startSniffer() {
  WiFi.mode(WIFI_STA);        // keep STA mode
  WiFi.disconnect();          // ensure not connected

  wifi_promiscuous_filter_t filter = {
    .filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT | WIFI_PROMIS_FILTER_MASK_DATA
  };
  esp_wifi_set_promiscuous_filter(&filter);
  esp_wifi_set_promiscuous_rx_cb(&wifi_sniffer_packet_handler);
  esp_wifi_set_promiscuous(true);

  esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE);
  Serial.println("Sniffer ON (starting on channel 1)");
}

void stopSniffer() {
  esp_wifi_set_promiscuous(false);
  Serial.println("Sniffer OFF");
}

void publishBuffer() {
  Serial.println("Connecting to WiFi for MQTT...");
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  unsigned long startAttempt = millis();
  while (WiFi.status() != WL_CONNECTED && millis() - startAttempt < 10000) {
    delay(200);
    Serial.print(".");
  }
  Serial.println(WiFi.isConnected() ? "\nWiFi connected" : "\nWiFi connect failed");

  if (WiFi.isConnected()) {
    client.setServer(mqtt_server, mqtt_port);
    if (client.connect("ESP32Scanner4", mqtt_user, mqtt_pass)) {
      Serial.println("MQTT connected");
      for (int i = 0; i < bufferCount; i++) {
        String payload = "{";
        payload += "\"type\":\"" + buffer[i].type + "\",";
        payload += "\"mac\":\"" + buffer[i].mac + "\",";
        payload += "\"rssi\":" + String(buffer[i].rssi);
        payload += "}";
        if (buffer[i].type == "ble")
          client.publish("locator/scanner4/ble", payload.c_str());
        else
          client.publish("locator/scanner4/wifi_clients", payload.c_str());
        delay(5);
      }
    } else {
      Serial.println("MQTT connect failed");
    }
  }
  client.disconnect();
  WiFi.disconnect(true, true);
  bufferCount = 0; // clear buffer
  Serial.println("Published & WiFi disconnected");
}

// ====== Setup ======
void setup() {
  Serial.begin(115200);

  // BLE init
  BLEDevice::init("");
  pBLEScan = BLEDevice::getScan();
  pBLEScan->setActiveScan(true);
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());

  startSniffer();
}

unsigned long lastPublish = 0;
const unsigned long publishInterval = 30000; // 30s
unsigned long lastChannelHop = 0;
uint8_t currentChannel = 1;

void loop() {
  // BLE scan while sniffing
  pBLEScan->start(bleScanTime, false);
  pBLEScan->clearResults();

  // Channel hopping !
  if (millis() - lastChannelHop > 500) {
    lastChannelHop = millis();
    currentChannel++;
    if (currentChannel > 13) currentChannel = 1;
    esp_wifi_set_channel(currentChannel, WIFI_SECOND_CHAN_NONE);
    // Serial.printf("Hopped to channel %d\n", currentChannel);
  }

  // Every 30s
  if (millis() - lastPublish > publishInterval) {
    lastPublish = millis();
    stopSniffer();
    publishBuffer();
    startSniffer();
  }
}

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. 🙂

I bought a VNA/Antenna test board from Ali.

Feature:

  1. RF Demo Kit RF test board demo calibration board for learning Vector Analyzer and Antenna Analyzer test calibration.
  2. The board is fully integrated with 18 functional modules.
  3. Equipped with 2 UFL patch cords for convenient use.
  4. Each module is carefully selected for high quality and reliability.
  5. The board is small and lightweight, easy to carry.

Specification:

Product type: RF Demo Kit

  1. Filters:
    • A. Short low-pass filter (LPF): 30 MHz
    • B. FM high-pass filter (HPF): 100 MHz
    • C. Commonly used SAW band-pass filter (BPF): 433 MHz
    • D. Video ceramic notch (band-stop filter, BSF): 6.5 MHz
  2. RLC series and parallel circuits
    • Includes R, L, C and combination circuits
  3. Open/short and load calibration circuit
  4. Attenuation circuit

Package List:

  • 1 × RF Demo Kit Board
  • 2 × UFL patch cables

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.

Cynthcart (c64) midi control

I bought a teensyrom a while ago.

UPDATE 20250712 : Display
UPDATE 20250904 : Added PitchBend and stabilized potmeters.

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)

Knob 1 = volume now, and left of box has centering Pitch Bend.


CODE

#include <MIDIUSB.h>
#include <ResponsiveAnalogRead.h>

const int pitchbend = A14;
const int filterresonance = A0;
const int filtercutoff = A1;
const int voicemode = A2;
const int modulationmode = A3;
const int attack = A4;
const int release = A5;
const int pulsewidth = A6;
const int tremolodepth = A7;
const int tremolospeed = A8;
const int oscwave = A9;
const int oscvoice2 = A10;
const int oscvoice3 = A11;
const int volume = A12;

ResponsiveAnalogRead analogpitchbend(pitchbend, true);

ResponsiveAnalogRead analogfilterresonance(filterresonance, true);
ResponsiveAnalogRead analogfiltercutoff(filtercutoff, true);
ResponsiveAnalogRead analogvoicemode(voicemode, true);
ResponsiveAnalogRead analogmodulationmode(modulationmode, true);
ResponsiveAnalogRead analogattack(attack, true);
ResponsiveAnalogRead analogrelease(release, true);
ResponsiveAnalogRead analogpulsewidth(pulsewidth, true);
ResponsiveAnalogRead analogtremolodepth(tremolodepth, true);
ResponsiveAnalogRead analogtremolospeed(tremolospeed, true);
ResponsiveAnalogRead analogoscwave(oscwave, true);
ResponsiveAnalogRead analogoscvoice2(oscvoice2, true);
ResponsiveAnalogRead analogoscvoice3(oscvoice3, true);
ResponsiveAnalogRead analogvolume(volume, true);


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


int lastfilterresonance = -1;
int lastfiltercutoff = -1;
int lastvoicemode = -1;
int lastmodulationmode = -1;
int lastattack = -1;
int lastrelease = -1;
int lastpulsewidth = -1;
int lasttremolodepth = -1;
int lasttremolospeed = -1;
int lastoscwave = -1;
int lastoscvoice2 = -1;
int lastoscvoice3 = -1;
int lastvolume = -1;


// CC 7 ff niet

// 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,6,8,9,13,14,15};             // 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() {
  Serial.begin(9600);
  for (int i = 0; i < numPots; i++) {
    pinMode(potPins[i], INPUT);
    lastValues[i] = -1;
  }
  pinMode(pitchbend, INPUT);
}

void loop() {
  analogfilterresonance.update();
  if(analogfilterresonance.hasChanged()) {
    int analogValue = analogfilterresonance.getValue();
    int midiValue = analogValue / 8;  // Scale 0–1023 to 0–127
    if (abs(midiValue - lastfilterresonance) > 0){
    usbMIDI.sendControlChange(7, midiValue, 1);
    lastfilterresonance = midiValue;
    }
  }

  analogfiltercutoff.update();
  if(analogfiltercutoff.hasChanged()) {
    int analogValue = analogfiltercutoff.getValue();
    int midiValue = analogValue / 8;  // Scale 0–1023 to 0–127
    if (abs(midiValue - lastvoicemode) > 0){
    usbMIDI.sendControlChange(1, midiValue, 1);
    lastvoicemode = midiValue;
    }  
  }

  analogvoicemode.update();
  if(analogvoicemode.hasChanged()) {
    int analogValue = analogvoicemode.getValue();
    int midiValue = analogValue / 8;  // Scale 0–1023 to 0–127
    if (abs(midiValue - lastvoicemode) > 0){
    usbMIDI.sendControlChange(2, midiValue, 1);
    lastvoicemode = midiValue;
    }
  }

  analogmodulationmode.update();
  if(analogmodulationmode.hasChanged()) {
    int analogValue = analogmodulationmode.getValue();
    int midiValue = analogValue / 8;  // Scale 0–1023 to 0–127
    if (abs(midiValue - lastmodulationmode) > 0){
    usbMIDI.sendControlChange(3, midiValue, 1);
    lastmodulationmode = midiValue;
    }
  }


  analogattack.update();
  if(analogattack.hasChanged()) {
    int analogValue = analogattack.getValue();
    int midiValue = analogValue / 8;  // Scale 0–1023 to 0–127
    if (abs(midiValue - lastattack) > 0){
    usbMIDI.sendControlChange(4, midiValue, 1);
    lastattack = midiValue;
    }
  }


  analogrelease.update();
  if(analogrelease.hasChanged()) {
    int analogValue = analogrelease.getValue();
    int midiValue = analogValue / 8;  // Scale 0–1023 to 0–127
    if (abs(midiValue - lastrelease) > 0){
    usbMIDI.sendControlChange(5, midiValue, 1);
    lastrelease = midiValue;
    }
  }


  analogpulsewidth.update();
  if(analogpulsewidth.hasChanged()) {
    int analogValue = analogpulsewidth.getValue();
    int midiValue = analogValue / 8;  // Scale 0–1023 to 0–127
    if (abs(midiValue - lastpulsewidth) > 0){
    usbMIDI.sendControlChange(6, midiValue, 1);
    lastpulsewidth = midiValue;
    }
  }


  analogtremolodepth.update();
  if(analogtremolodepth.hasChanged()) {
    int analogValue = analogtremolodepth.getValue();
    int midiValue = analogValue / 8;  // Scale 0–1023 to 0–127
    if (abs(midiValue - lasttremolodepth) > 0){
    usbMIDI.sendControlChange(8, midiValue, 1);
    lasttremolodepth = midiValue;
    }
  }


  analogtremolospeed.update();
  if(analogtremolospeed.hasChanged()) {
    int analogValue = analogtremolospeed.getValue();
    int midiValue = analogValue / 8;  // Scale 0–1023 to 0–127
    if (abs(midiValue - lasttremolospeed) > 0){
    usbMIDI.sendControlChange(9, midiValue, 1);
    lasttremolospeed = midiValue;
    }
  }


  analogoscwave.update();
  if(analogoscwave.hasChanged()) {
    int analogValue = analogoscwave.getValue();
    int midiValue = analogValue / 8;  // Scale 0–1023 to 0–127
    if (abs(midiValue - lastoscwave) > 0){
    usbMIDI.sendControlChange(13, midiValue, 1);
    lastoscwave = midiValue;
    }
  }


  analogoscvoice2.update();
  if(analogoscvoice2.hasChanged()) {
    int analogValue = analogoscvoice2.getValue();
    int midiValue = analogValue / 8;  // Scale 0–1023 to 0–127
    if (abs(midiValue - lastoscvoice2) > 0){
    usbMIDI.sendControlChange(14, midiValue, 1);
    lastoscvoice2 = midiValue;
    }
  }


  analogoscvoice3.update();
  if(analogoscvoice3.hasChanged()) {
    int analogValue = analogoscvoice3.getValue();
    int midiValue = analogValue / 8;  // Scale 0–1023 to 0–127
    if (abs(midiValue - lastoscvoice3) > 0){
    usbMIDI.sendControlChange(15, midiValue, 1);
    lastoscvoice3 = midiValue;
    }
  }







// PITCHBEND!
  analogpitchbend.update();
  if(analogpitchbend.hasChanged()) {
    int pitchBend = map(analogpitchbend.getValue(), 0, 1023, 0, 16383); // Map to MIDI Pitch Bend range
    sendPitchBend(pitchBend, 0);
  }
  /*
  int potValue = analogRead(A14);         // Read pot (0–1023)
  int pitchBend = map(potValue, 0, 1023, 0, 16383); // Map to MIDI Pitch Bend range
  //pitchBend = pitchBend + 8192;


  if (abs(pitchBend - lastPitch) > 250) { // Send only on significant change
    sendPitchBend(pitchBend, 0); 
        Serial.println(pitchBend);

    lastPitch = pitchBend;
  }
  */


  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();
}

CODE for display

#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);