Elch zu Weihnachten

Der Elch als Weihnachtsschmuck ist ein kleines Projekt, welches mit einem Arduino Uno oder Klone, einer LED mit Vorwiderstand und einem kleinen Servo auskommt. Die Besonderheit liegt darin, dass der Elch mit dem USB-Kabel, mit dem er über die Arduino IDE programmiert wird, mit dem PC verbunden bleibt. Dann wird das Python3-Skript Elch_1.py gestartet, welches im aktuellen Verzeichnis die Datei Elch.png benötigt. Zwischen dem PC und dem Arduino wird nun eine serielle Verbindung aufgebaut, die als Beispiel für eine fehlertolerante und besonders zuverlässige serielle Datenübertragung herangezogen werden kann.

Jetzt kann man wunderschön ein Setup für den Elch finden, welches einem besonders gefällt: Man schaltet die Automatik aus, fährt den Kopf in die gewünschte obere Position und speichert diese ab und macht dasselbe mit der unteren Position. Anschließend werden noch die Nickfrequenz des Kopfes und die Blinkfrequenz der LED eingestellt. Wer will kann auch im Stillstand einfach nur die Helligkeit der LED regeln. Wenn alles gespeichert ist, dann läuft der Elch auch ohne das Python Skript mit jeder USB-Stromversorgung oder einem Arduino Uno geeigneten Netzteil.

Damit eine serielle Verbindung zustande kommt, muss immer zuerst der Elch eingesteckt sein und dann das Python Skript gestartet werden.

Elch_Bauteile_Schaltung

 

weihnachtsElch.ino

#include <Servo.h>
#include <EEPROM.h>
#include <float.h>

#include "prg.h"

static Servo gelenk;
static constexpr int gelenkPin = 2, nosePin = 3, keyPin = 4;
static unsigned long lastMillis;
static struct {
  int microSecondsTop, microSecondsBottom;
} config;
static float ti;
static bool keyPulse;
static float loop2T, loop2OldGel;
static prgPos myPrgPos;
static bool autoMode;

static int servoclamp(int v)
{
  if (v < 900) return 900;
  if (v > 2100) return 2100;
  return v;
}

// v bleibt in den Schranken l1,l2 wobei egal ist ob l1<l2 oder l1>l2
static float floatclamp(float v, float l1, float l2)
{
  float klein, gross;
  if (l1 < l2) {
    klein = l1;
    gross = l2;
  } else {
    klein = l2;
    gross = l1;
  }
  if (v < klein) v = klein;
  if (v > gross) v = gross;
  return v;
}

static void help()
{
  Serial.println(F(
    "FabLab Neuenstadt - Weihnachten 2022\n"
    "SuperELCH Programmieranleitung\n"
    "k: Kill automatic (zuerst machen, sonst geht nichts manuell)\n"
    "t,b: Fahre zu Top oder Bottom\n"
    "T,B: Merke aktuelle Position als Top oder Bottom\n"
    "a,e: Servo ausschalten / Servo einschalten\n"
    "1,2,3  viel,mittel,wenig nach links fahren\n"
    "4,5,6  wenig,mittel,viel nach rechts fahren\n"
    "w: aktuelle Konfiguration im EEPROM speichern\n"
    "h: Hilfe\n"
    "v: show values\n"));
}

static void loopComm()
{
  const int c = Serial.read();
  if (c=='t') gelenk.writeMicroseconds(servoclamp(config.microSecondsTop));
  if (c=='b') gelenk.writeMicroseconds(servoclamp(config.microSecondsBottom));
  if (c=='T') config.microSecondsTop=servoclamp(gelenk.readMicroseconds());
  if (c=='B') config.microSecondsBottom=servoclamp(gelenk.readMicroseconds());
  if (c=='e') gelenk.attach(gelenkPin);
  if (c=='a') gelenk.detach();
  if (c=='1') gelenk.writeMicroseconds(servoclamp(gelenk.readMicroseconds()+100));
  if (c=='2') gelenk.writeMicroseconds(servoclamp(gelenk.readMicroseconds()+10));
  if (c=='3') gelenk.writeMicroseconds(servoclamp(gelenk.readMicroseconds()+1));
  if (c=='4') gelenk.writeMicroseconds(servoclamp(gelenk.readMicroseconds()-1));
  if (c=='5') gelenk.writeMicroseconds(servoclamp(gelenk.readMicroseconds()-10));
  if (c=='6') gelenk.writeMicroseconds(servoclamp(gelenk.readMicroseconds()-100));
  if (c=='w') EEPROM.put(0, config);
  if (c=='k') autoMode = false;
  if (c=='h') help();
  if (c=='v') {
    Serial.print("Bottom=");
    Serial.print(config.microSecondsBottom);
    Serial.print(" Top=");
    Serial.print(config.microSecondsTop);
    Serial.print(" Current=");
    Serial.print(gelenk.readMicroseconds());
    Serial.print(" Prg=");
    Serial.println(myPrgPos.getProgramNumber());
    Serial.print("loop2OldGel=");
    Serial.println(loop2OldGel);
  }
}

void setup()
{
  Serial.begin(9600);

  pinMode(gelenkPin, OUTPUT);
  digitalWrite(gelenkPin, LOW);
  gelenk.attach(gelenkPin);
  pinMode(nosePin, OUTPUT);
  pinMode(keyPin, INPUT_PULLUP);

  EEPROM.get(0, config);
  if (config.microSecondsBottom <= 0 || config.microSecondsTop <= 0) {
    config.microSecondsBottom = servoclamp(1800);
    config.microSecondsTop = servoclamp(1600);
  }
  config.microSecondsBottom = servoclamp(config.microSecondsBottom);
  config.microSecondsTop = servoclamp(config.microSecondsTop);

  autoMode = true;

  lastMillis = millis();

  help();
}

static void gel(float p) // p=0 bottom / p=1 top
{
  p *= config.microSecondsTop - config.microSecondsBottom;
  p += config.microSecondsBottom;
  p = floatclamp(p, config.microSecondsBottom, config.microSecondsTop);
  gelenk.writeMicroseconds(p);
}

static void nos(float p) // p=0 bottom / p=1 top
{
  p *= 255;
  p = floatclamp(p, 0, 255);
  analogWrite(nosePin, p);
}

static void loopKey()
{
  static float pressed = 0;
  keyPulse = false;
  if ( ! digitalRead(keyPin)) {
    pressed += ti;
    if (pressed > 0.1) {
      keyPulse = true;
      pressed = -FLT_MAX;            
    }
  } else pressed = 0;
}

static void loopNoseServo()
{
  if (autoMode) {
    if (myPrgPos.currentAcceleration() > 0) {
      const float deltaA = ti * myPrgPos.currentAcceleration();
      loop2OldGel = deltaA * myPrgPos.currentServo() + loop2OldGel * (1.f - deltaA);
    } else {
      loop2OldGel = myPrgPos.currentServo();
    }
    gel(loop2OldGel);

    nos(myPrgPos.currentLed());

    if (loop2T > myPrgPos.currentDelay()) {
      loop2T -= myPrgPos.currentDelay();
      myPrgPos.nextCommand();
    }

    loop2T += ti;
  } else {
    nos(1);
  }
}

void loop()
{
  unsigned long nowMillis = millis();
  unsigned long diffMillis = nowMillis - lastMillis; // wraparound ergibt keinen Fehler.
  lastMillis = nowMillis;
  ti = diffMillis;
  ti /= 1000; // ti ist jetzt die Zeitdifferenz zum letzten Mal in Sekunden.

  loopKey(); // ztuerst machen, weil das keyPulse setzt.

  loopComm();

  loopNoseServo();
  
  if (keyPulse) {
    autoMode = true;
//    loop2OldGel = myPrgPos.currentServo() > 0.5 ? 0 : 1; // snap nose
    myPrgPos.nextProgram();
    loop2T = 0;
  }
}
prg.cpp

#include "prg.h"

#include <Arduino.h>
#include <float.h>

// Programme sind kodiert als Gelenk (0..1), Nase (0..1), Delay (s). Programmende mit einzelnem Eintrag FLT_MAX
// Der erste Wert jedes Programmes ist die Acceleration. negative acceleration = instant.
// Listenende mit zusätzlichen FLT_MAX Eintrag

static const float pStore[] PROGMEM = {
  ////////////////////////////////// 0
  2,
  0  , 0, 1,
  0  , 1, 1,
  0  , 0, 1,
  0.1, 1, 1,
  0  , 0, 1,
  0.2, 1, 1,
  0.1, 0, 1,
  0.3, 1, 1,
  0.2, 0, 1,
  0.4, 1, 1,
  0.3, 0, 1,
  0.5, 1, 1,
  0.4, 0, 1,
  0.6, 1, 1,
  0.5, 0, 1,
  0.7, 1, 1,
  0.6, 0, 1,
  0.8, 1, 1,
  0.7, 0, 1,
  0.9, 1, 1,
  0.8, 0, 1,
  1  , 1, 1,
  0.9, 0, 1,
  1  , 1, 1,
  1  , 0, 1,
  FLT_MAX,


  ////////////////////////////////// 1
  -1,
  0.9, 0, 0.5,
  1, 1, 0.5,
  0.9, 0, 0.5,
  1, 1, 0.5,
  0.4, 0, 1,
  1, 1, 30-0.5*4-1,
  FLT_MAX,
  

  ////////////////////////////////// 2
  -1,
  0.9,1,0.25,
  0.9,0,0.25,
  0.9,0,0.25,
  0.8,0,0.25,
  0.9,1,0.25,
  0.9,0,0.25,
  1,0,0.25,
  1,0,0.25,
  1,1,0.25,
  1,0,0.25,
  0.8,0,0.25,
  0.8,0,0.25,
  1,1,0.25,
  1,0,0.25,
  1,0,0.25,
  1,0,0.25,  
  
  1  ,1,0.25,
  1  ,0,0.25,
  1  ,0,0.25,
  1  ,0,0.25,
  1  ,0,0.25,
  1  ,0,0.25,
  1,  0,0.25,
  1,  0,0.25,
  1,  1,0.25,
  1,  0,0.25,
  1  ,0,0.25,
  1  ,0,0.25,
  1,  0,0.25,
  1,  0,0.25,
  1,  0,0.25,
  1,  0,0.25,  

  1  ,1,0.25,
  1  ,1,0.25,
  1  ,0,0.25,
  1  ,0,0.25,
  1  ,1,0.25,
  1  ,1,0.25,
  1,  0,0.25,
  1,  0,0.25,
  1,  1,0.25,
  1,  1,0.25,
  1  ,0,0.25,
  1  ,0,0.25,
  1,  1,0.25,
  1,  1,0.25,
  1,  0,0.25,
  1,  0,0.25,  

  1  ,1,0.25,
  1  ,0,0.25,
  1  ,1,0.25,
  1  ,0,0.25,
  1  ,1,0.25,
  1  ,0,0.25,
  1,  1,0.25,
  1,  0,0.25,
  1,  1,0.25,
  1,  0,0.25,
  1  ,1,0.25,
  1  ,0,0.25,
  1,  1,0.25,
  1,  0,0.25,
  1,  1,0.25,
  1,  0,0.25,  
  FLT_MAX,


  ////////////////////////////////// 3
  -1,
  1  ,1,0.2,
  0.8,1,0.1,
  1  ,1,0.2,
  0.8,1,0.1,
  1  ,1,0.2,
  0.8,1,0.2,
  0  ,0,20-0.2*4-0.1*2,
  FLT_MAX,


  ////////////////////////////////// 4
  1,
  1,0,10,
  0,1,50,
  FLT_MAX,


  ////////////////////////////////// 5
  -1,
  0,1,0.5,
  1,0,0.5,
  0,1,0.5,
  1,0,0.5,
  0,1,0.25,
  1,0,0.25,
  0,1,0.25,
  1,0,0.25,
  0,1,0.25,
  1,0,0.25,
  0,1,0.25,
  1,0,60-0.25*7-0.5*4,
  FLT_MAX,


  ////////////////////////////////// 6
  10,
  1.0,0,2,  
  1.0,1,2,  
  0.9,0,2,  
  0.9,1,2,  
  0.8,0,2,  
  0.8,1,2,  
  0.7,0,2,  
  0.7,1,2,  
  0.6,0,2,  
  0.6,1,2,  
  0.5,0,2,  
  0.5,1,2,  
  0.4,0,2,  
  0.4,1,2,  
  0.3,0,2,  
  0.3,1,2,  
  0.2,0,2,  
  0.2,1,2,  
  0.1,0,2,  
  0.1,1,2,  
  0  ,0,2,  
  0  ,1,2,  
  FLT_MAX,


  -1,
  0,0,1,
  1,1,1,
  FLT_MAX,


  -1,
  0,1,0.5,
  1,0,0.5,
  0.2,1,0.5,
  0.8,0,0.5,
  0.4,1,0.5,
  0.6,0,0.5,
  FLT_MAX,
  

  0.5,
  0,0,30,
  1,1,30,
  FLT_MAX,

  ////////////////////////////////// EOF
  FLT_MAX // end of programs!
};


prgPos::prgPos()
  : p(pStore+1), pCurrentProgram(pStore), programNumber(0)
{
}

void prgPos::nextProgram()
{
  while (pgm_read_float(p) != FLT_MAX) p+=3;
  if (pgm_read_float(++p) == FLT_MAX) {
    p = pStore + 1;
    pCurrentProgram = pStore;
    programNumber = 0;
  } else {
    ++programNumber;
    pCurrentProgram = p++;
  }
}

void prgPos::nextCommand()
{
  p+=3;
  if (pgm_read_float(p) == FLT_MAX) p = pCurrentProgram + 1;
}

float prgPos::currentLed()
{
  return pgm_read_float(p+1);
}

float prgPos::currentServo()
{
  return pgm_read_float(p);
}

float prgPos::currentDelay()
{
  return pgm_read_float(p+2);
}

float prgPos::currentAcceleration()
{
  return pgm_read_float(pCurrentProgram);
}

int prgPos::getProgramNumber()
{
  return programNumber;
}
prg.h

#ifndef __PRG_H
#define __PRG_H

class prgPos {
public:
  prgPos();
  void nextProgram();
  void nextCommand(); // auto wrap around
  float currentLed();
  float currentServo();
  float currentDelay();
  float currentAcceleration();
  int getProgramNumber();
private:
  const float *p, *pCurrentProgram;
  int programNumber;
};

#endif