home
erste Version am 10.10.2021
letzte Änderung am 15.10.2021

MausBeweger


Das Ziel dieses Projekts ist eine aktive Abstellfläche für die Maus. Sobald eine Maus darauf abgestellt wird, bekommt sie regelmäßig einen virtuellen Stubs und simuliert somit Anwesenheit am Rechner.
Der Einsatzweck dafür ist das HomeOffice, in dem parallel an zwei Rechnern gearbeitet wird.
Auf dem Firmen-Laptop läuft üblicherweise der ganze Kommunikations-Kram bzw. speziell das Chat-Tool.
Die eigentliche Arbeit wird jedoch gerne mal an einem anderen -angenehmeren- Rechner (mit FullSize-Tastatur und größerem Monitor) via ssh oder ssh-X-Forwarding erledigt.
Weil die Maus am Firmen-Laptop bei dieser Konstellation jedoch längere Zeit unbewegt bleibt, schaltet das Chat-Tool seinen Status ständig von anwesend auf abwesend um. Echt lästig...

Sicher könnte man das Problem auch komplett in Software lösen - jedoch wäre diese Software dann sicher ständig aktiv und würde nerven, wenn man dann tatsächlich doch mal am Laptop zugange wäre.
Bei der angedachten mechanischen Lösung kann/muss die Maus bei Bedarf auf eine spezielle Park-Position geschoben werden, um per Chat erreichbar zu bleiben.

Es gibt durchaus fertige Lösungen für diese Aufgabe. Deren Werbebilder sind jedoch gelegentlich ziemlich grenzwertig: immer wieder chillende Yuppies mit Mops und Apple-Device (alias Super-Checker), die dank MausBeweger grinsend und tiefenentspannt Kaffee schlüfen können (während der Chef mit Stasi-Mentalität dann wohl annimmt, sie würden arbeiten). Lustigerweise zeugen die Amazon-Bewertungen oft von ungenügender Qualität dieser Devices.
Ich hoffe doch mal, dass jeder, der überhaupt auf meiner Seite landet, Spaß am Gerät hat, gerne das tut, was er tut .... und den Maus-Beweger eben nicht dazu einsetzt, derart erbärmlichen Arbeitszeit-Betrug zu begehen.


Inhaltsverzeichnis

Vorüberlegungen
Erste Tests
Und schon funktioniert es
Das Unterteil
Die Firmware (für den ATmega328)
Alles beisammen


Vorüberlegungen

Die Maus soll real nicht bewegt werden. Vielmehr soll der Boden unter dem optischen Sensor bewegt werden.
Zwei relativ kostengünstige Optionen fallen mir ein (und eine dritte, die aber schnell wieder verworfen wurde):
  1. ) per 5V-Steppermotor (28byj48) eine Stroboskop-Scheibe drehen lassen    oder
  2. ) per 5V-Modellbau-Servo einen weissen Arm vor einem schwarzen Hintergrund bewegen   oder
  3. ) per geplustem LED- oder Laser-Gegenlicht....oder auch nicht....wegen dem hier....ist mir viel zu kompliziert.
Weil der Maus-Cursor nicht konstant in eine Richtung wandern soll, ist eine Drehscheibe (bei Variante 1.) dann doch keine so gute Idee.
Variante 2. würde weniger Platz beanspruchen, Variante 1. wäre voraussichtlich etwas billiger.
Der Stepper braucht vier Pins zur Ansteuerung, der Servo nur einen. Die bevorzugte CPU ist ein ATtiny85 - also Pin-Knappheit.
Denkbar wäre nämlich auch noch eine Art Einschalter - also sodass nur bei erkannter Maus-Lagerung etwas passiert.
Andererseits wird es vollkommen reichen, wenn einmal pro Minute Maus-Wackeln erfolgt. Also Strom-technisch egal. Aber vielleicht Lärm-technisch doch sinnvoll....wir werden sehen.

Das Gehäuse wird aus dem 3D-Drucker kommen und keilförmig sein. Ein 30°-Winkel vielleicht.
Unterschiedliche Maus-Abmessungen und Sensor-Positionen könnten eine Herausforderung werden.
Unter der Annahme, dass der Sensor immer mittig zum Maus-Chassis sitzen wird, wäre eine simulierte Seitwärtsbewegung geschickter.
Und an der tieferen Kante bräuchte es ein einzeln druckbares Maus-Ausrichtungs-Element. Damit müsste höchstens dieses Teil neu entworfen und gedruckt werden, wenn eine Maus mit anderen Abmessungen zum Einsatz kommen sollte.

Oder die Maus steht waagerecht auf der Fläche. Das wäre wahrscheinlich einfacher bzw. leichter für unterschiedliche Maus-Modelle nutzbar.
Die Höhe wäre dann mindestens 35mm (MicroServo) oder 32mm (28byj48-Stepper).


Erste Tests

Nun habe ich erste Tests mit einem MicroServo und der Servo-Library gemacht. Tatsächlich lautlos wird der Servo erst nach einem servo.detach().
Und zu allem Übel steht hier noch, dass die Library nicht am ATtiny85 funktioniert.

Den 28byj48-Stepper per ATtiny85 ansteuern, kann ich schon.
Jedoch habe ich gerade eine Seite mit Doku gefunden, wie es ganz ohne Library funktioniert. Das klappt sehr schön an einem ArduinoUNO.
Damit ist der Servo raus: der Stepper ist leiser, billiger, flacher und einfacher ansteuerbar.

Dann stellt sich noch die Frage, ob es einen Ausrichtungs-Modus braucht. Schließlich reagieren diverse Programme auf MouseOver-Events mit PopUp-Fenstern - oder Schlimmerem. Daher ist die Idee, die Maus zunächst in einen diesbezüglich unkritischen Bereich des Bildschirms zu fahren, bevor sie auf den MausBeweger gesetzt wird. Dort soll sie nur noch kleine Hin- und Her-Bewegungen ausführen, um im unkritischen Bereich zu bleiben.
Dort abgesetzt, möchte man wahrscheinlich zeitnah wissen, ob sie korrekt über dem bewegten Bereich steht. Bei nur einem kleinen Ruckler pro Minute, wird das aber nix mit zeitnah. Es bräuchte entweder eine Erkennung von "Maus wurde bewegt" oder einen Taster zum Ankündigen der Ausrichtungs-Wunsches.
Oder wieder zurück zu der Idee vom keilförmigen Gehäuse: ein justierbares Maus-Ausrichtungs-Element. Das ist wohl einfacher - speziell bei der späteren Nutzung.

Der erste Gehäuse-Entwurf sieht erstmal so aus .... noch ohne Maus-Ausrichtungs-Element:
Screenshot aus openSCAD      Entwurf mit Maus

In ABS gedruckt soll das 1:18 Stunden dauern und 19g Filament benötigen.
Das ist zwar hart an der Schmerzgrenze für einen Test-Druck, aber vielleicht braucht es dann nur noch ein passend hohes Unterteil .... und das Gehäuse wäre durch.
Auf jeden Fall wird sich damit testen lassen, ob es grundsätzlich funktioniert.


Und schon funktioniert es

Wie geil. Klappt auf Anhieb. Noch ohne Unterteil und mit ArduinoUNO - aber klappt.
Die Maus wandert per Magie ca. 1/10-Bildschirmbreite von links nach rechts ... und wieder zurück.
Wenn man sich einmal gemerkt hat, wie die Maus auf dem MausBeweger abgestellt werden muss, braucht es auch kein Maus-Ausrichtungs-Element mehr.

Nun fehlt nur noch der Gehäuse-Boden, die Umstellung auf den ATtiny85 und eine Stromversorgung per USB.
Sobald das Gehäuse final ist, werde ich es auf Thingiverse hochladen und das Thing hier verlinken.


Das Unterteil

Im Unterteil braucht es mindestens einen USB-Anschluß zur Stromversorgung.
Vielleicht auch eine Power-LED und vielleicht sogar noch einen Taster für den Ausrichtungstest ... und bei langem Druck zum Abschalten.

Damit würden dann sechs steuerbare Pins benötigt werden.
Hmmmm..... und wie leitet man dann eine Re-Programmierung des Chips ein!?
Laut Datasheet vom ATtiny85 gilt für den Reset-Pin (1) "can also be used as a (weak) I/O pin".
Bei Heise heisst es hingegen: "Neben GND und Spannungsversorgung ist Pin 1 als Reset-Pin für den Flash-Vorgang reserviert. Fünf GPIO-Pins (also als Ein- oder Ausgang) bleiben für eigene Anwendungen übrig."
Irgendwie geht es wohl. Aber in die Fuse-Abgründe wollte ich eigentlich nicht eintauchen.

Daher nehme ich vielleicht lieber einen ATtiny861, von dem ich vor Urzeiten (Jan. 2018) fünf Exemplare gekauft habe.
Oder auch nicht. Der Nullkraft-Sockel des Diamex kann den nicht.
Dann war das offenbar ein Fehlkauf. Auf sowas hier habe ich keinen Bock mehr...

Somit bliebe mir nur noch der ATmega328 (samt Quartz - weil der OptiFix-Bootloader den braucht), wenn ich nicht auf eine Lieferung von Reichelt warten will.
Auch doof. Weil Overkill.

Die LED könnte parallel zu einer Stepper-Spule geschaltet werden. Wäre aber Stromverschwendung und vielleicht auch ungesund für den Stepper.
Oder ein 74HC795-Schieberegister vor Stepper und LED. Damit hätte ich dann aus drei ATtiny85-Pins acht Ausgangs-Pins gemacht.

Ach was solls. Dann wird eben ein weiteres Exemplar meines großen ATmega328-Vorrats eine Bestimmung bekommen.


Die Firmware (für den ATmega328)

Drei Komponenten müssen gesteuert / abgefragt werden.
  1. ) der Stepper
  2. ) die LED
  3. ) der Taster
Sobald Saft via USB da ist, leuchtet die LED. Bei aktivem Stepper blinkt die LED schnell. Im Schlafmodus blinkt die LED im Sekundentakt - oder ist aus...mal schauen.
Wird der Taster kurz betätigt, läuft der Stepper an und führt drei Hin-/Her-Bewegungen aus. Wird er mindestens eine Sekunde lang gedrückt gehalten, wechselt das Gerät in den Schlafmodus.
Der Stepper ist an oder aus und führt, wenn an, immer gleich weite Hin-/Her-Bewegungen aus.

Sodele...das hier erfüllt diese Vorgaben:
/*
* MausBeweger
*
* Stepper-Ansteuerung inspiriert von: https://elektro.turanis.de/html/prj143/index.html
*
* D.Ahlgrimm 15.10.2021
*/

#include <TimerOne.h> // https://www.arduino.cc/reference/en/libraries/timerone/

#define PIN_COIL_IN1 8 // Spule blue
#define PIN_COIL_IN2 9 // Spule pink
#define PIN_COIL_IN3 10 // Spule yellow
#define PIN_COIL_IN4 11 // Spule orange
#define PIN_BUTTON 3 // Taster
#define PIN_LED 13 // LED

#define MOTOR_SPEED 10 // in Millisekunden

#define BUTTON_OPEN 0
#define BUTTON_PRESSED_SHORT 1
#define BUTTON_PRESSED_LONG 2
#define BUTTON_RELEASED_SHORT 3
#define BUTTON_RELEASED_LONG 4
volatile byte button=BUTTON_OPEN; // aktueller Zustand des Tasters

#define LED_OFF 0 // nicht verwendet
#define LED_ON 1 // Geraet an - aber Stepper gerade inaktiv
#define LED_BLINK_FAST 2 // Stepper aktiv
#define LED_BLINK_SLOW 3 // Schlafmodus
volatile byte led_preset=LED_ON; // aktueller LED-Modus

#define DEBUG false

const byte coil_pin[4] = { PIN_COIL_IN1, PIN_COIL_IN2, PIN_COIL_IN3, PIN_COIL_IN4 };

const bool coil_pwr[9][4] = { // Soll-Zustaende der Spulen am Stepper
{ LOW, LOW, LOW, HIGH },
{ LOW, LOW, HIGH, HIGH },
{ LOW, LOW, HIGH, LOW },
{ LOW, HIGH, HIGH, LOW },
{ LOW, HIGH, LOW, LOW },
{ HIGH, HIGH, LOW, LOW },
{ HIGH, LOW, LOW, LOW },
{ HIGH, LOW, LOW, HIGH },
{ LOW, LOW, LOW, LOW } // Stepper = aus
};

volatile bool stepper_on=true; // steuert den Ein/Aus-Zustand des Steppers
volatile byte stepper_force_cnt=0; // fuer Stepper 3x Hin-/Her nach kurzem Betaetigen des Tasters

// --------------------------------------------------------------------------------
// Wird einmal pro Millisekunde aufgerufen.
void per_ms(void) {
static bool led_state; // An/Aus-Zustand der LED
static bool led_slow_on=true; // zum asynchronen Blinkverhaeltnis beim langsamen Blinken
static int led_slow_on_cnt; // Zaehler fuer das asynchrone Blinkverhaeltnis
static byte but_last=1; // Aenderungserkennung beim Taster
static byte but_state=HIGH; // Zustand des Tasters, HIGH=offen, LOW=gedrueckt
static unsigned long button_pressed=0L; // Timeout-Wert fuer den Taster (Erkennung fuer "lange gedrueckt")

// - - - - - - - - - - - - - - - - - - - - -
// LED-Steuerung
// - - - - - - - - - - - - - - - - - - - - -
switch(led_preset) {
case LED_OFF:
led_state=LOW;
break;
case LED_ON:
led_state=HIGH;
break;
case LED_BLINK_FAST:
if(millis()%50==0) {
led_state=!led_state; // schnelles Flackern
}
break;
case LED_BLINK_SLOW:
if(millis()%3000==0) {
led_slow_on=!led_slow_on; // asynchrone Blinken (alle drei Sekunden kurz aufblinken)
if(led_slow_on) {
led_slow_on_cnt=0;
led_state=HIGH;
} else {
led_state=LOW;
}
}
break;
}
if(led_preset==LED_BLINK_SLOW && led_slow_on) {
led_slow_on_cnt++;
if(led_slow_on_cnt>30) { // beim asynchronen Blinken wird die An-Phase nach 30ms...
led_state=LOW; // ...beendet.
}
}
digitalWrite(PIN_LED, led_state);

// - - - - - - - - - - - - - - - - - - - - -
// Abfrage und Verarbeitung des Tasters
// - - - - - - - - - - - - - - - - - - - - -
but_state=digitalRead(PIN_BUTTON);
if(but_state==LOW && button==BUTTON_PRESSED_SHORT) { // wenn der Taster gedrueckt ist und davor auch schon gedrueckt war...
if(button_pressed>0L && millis()>button_pressed) { // ...und der Taster bereits seit einer Sekunde gedrueckt gehalten wird...
button=BUTTON_PRESSED_LONG; // ...diesen Umstand vermerken...
button_pressed=0L;
#if DEBUG
Serial.println("long detected");
#endif
}
}
if(but_state!=but_last) { // wenn sich der Status am Taster geaendert hat...
but_last=but_state;
#if DEBUG
Serial.println("but changed");
#endif
switch(button) {
case BUTTON_OPEN: // wenn der Taster nicht betaetigt war und jetzt betaetigt wurde...
button=BUTTON_PRESSED_SHORT; // ...merken (erstmal als "kurz gedrueckt")...
button_pressed=millis()+1000; // ...und die Lang-Drueck-Erkennung aufsetzen
break;
case BUTTON_PRESSED_SHORT: // wenn der Taster kurz betaetigt war und jetzt losgelassen wurde...
button=BUTTON_RELEASED_SHORT; // ...mal kurz merken... wird nach Verarbeitung auf BUTTON_OPEN gesetzt
break;
case BUTTON_PRESSED_LONG: // wenn der Taster lange betaetigt war und jetzt losgelassen wurde...
button=BUTTON_RELEASED_LONG; // ...wie zuvor
break;
}
}

if(button==BUTTON_RELEASED_SHORT) { // wenn der Taster kurz betaetigt war und jetzt wieder losgelassen wurde...
#if DEBUG
Serial.println("BUTTON_RELEASED_SHORT");
#endif
stepper_on=true; // ...Stepper einschalten
stepper_force_cnt=3;
led_preset=LED_BLINK_FAST;
button=BUTTON_OPEN;
} else if(button==BUTTON_RELEASED_LONG) { // wenn der Taster lange betaetigt war und jetzt wieder losgelassen wurde...
#if DEBUG
Serial.println("BUTTON_RELEASED_LONG");
#endif
stepper_on=false; // Stepper aus und "Schlafmodus" einleiten
led_preset=LED_BLINK_SLOW;
button=BUTTON_OPEN;
} else if(button==BUTTON_PRESSED_LONG) { // wenn der Taster jetzt eine Sekunde lang betaetigt wurde...
if(stepper_on) { // ...und der Stepper grade an ist - sofort ausschalten
stepper_on=false;
led_preset=LED_BLINK_SLOW;
}
}
}

// --------------------------------------------------------------------------------
// Init.
void setup() {
for(int c=0; c<4; c++) {
pinMode(coil_pin[c], OUTPUT);
}
pinMode(PIN_BUTTON, INPUT_PULLUP);
pinMode(PIN_LED, OUTPUT);

Timer1.initialize(1000); // 1x pro Millisekunde
Timer1.attachInterrupt(per_ms);

#if DEBUG
Serial.begin(115200);
#endif
}

// --------------------------------------------------------------------------------
// Mainloop.
void loop() {
static int i;
static bool stepper_last=true; // Aenderungserkennung am Stepper

if(stepper_on) {
moveMouse();
for(i=0; i<3000 && stepper_on && stepper_force_cnt==0; i++) { // 3000*10ms = 30 Sekunden Pause
delay(10);
}
if(stepper_force_cnt>0) {
stepper_force_cnt--;
#if DEBUG
Serial.print("stepper_force_cnt=");
Serial.println(stepper_force_cnt);
#endif
}
} else {
if(stepper_last!=stepper_on) {
stepper_last=stepper_on;
stopMotor();
#if DEBUG
Serial.println("stopMotor()");
#endif
}
}
delay(10);
}

// --------------------------------------------------------------------------------
// Einen Hin-/Her-Dreher ausfuehren.
void moveMouse(void) {
led_preset=LED_BLINK_FAST;
for(int i=0; i<50; i++) { // Drehung rechts
rotateRight();
}
led_preset=LED_ON;
stopMotor(); // Strom sparen
delay(100);

led_preset=LED_BLINK_FAST;
for(int i=0; i<50; i++) { // Drehung links
rotateLeft();
}
led_preset=LED_ON;
stopMotor(); // Strom sparen
}

// --------------------------------------------------------------------------------
// Acht Stepper-Schritte nach links.
void rotateRight(void) {
if(!stepper_on) { return; }
for(int c=0; c<8; c++) {
//setMotor(coil_pwr[c]);
setMotor(coil_pwr[c][0], coil_pwr[c][1], coil_pwr[c][2], coil_pwr[c][3]);
}
}

// --------------------------------------------------------------------------------
// Acht Stepper-Schritte nach rechts.
void rotateLeft(void) {
if(!stepper_on) { return; }
for(int c=7; c>=0; c--) {
//setMotor(coil_pwr[c]);
setMotor(coil_pwr[c][0], coil_pwr[c][1], coil_pwr[c][2], coil_pwr[c][3]);
}
}

// --------------------------------------------------------------------------------
// Die Stepper-Spulen stromlos machen.
void stopMotor() {
//setMotor(coil_pwr[8]);
setMotor(coil_pwr[8][0], coil_pwr[8][1], coil_pwr[8][2], coil_pwr[8][3]);
}

// --------------------------------------------------------------------------------
// Einen Stepper-Schritt ausfuehren.
// ...funktioniert...liefert aber Warnings.
void setMotor_alt(bool *coil_num) {
for(int c=0; c<4; c++) {
digitalWrite(coil_pin[c], coil_num[c]);
}
delay(MOTOR_SPEED);
}

// --------------------------------------------------------------------------------
// Einen Stepper-Schritt ausfuehren.
void setMotor(bool in1, bool in2, bool in3, bool in4) {
digitalWrite(PIN_COIL_IN1, in1);
digitalWrite(PIN_COIL_IN2, in2);
digitalWrite(PIN_COIL_IN3, in3);
digitalWrite(PIN_COIL_IN4, in4);
delay(MOTOR_SPEED);
}


Die IDE meldet dafür:
Der Sketch verwendet 2202 Bytes (6%) des Programmspeicherplatzes. Das Maximum sind 32256 Bytes.
Globale Variablen verwenden 64 Bytes (3%) des dynamischen Speichers, 1984 Bytes für lokale Variablen verbleiben. Das Maximum sind 2048 Bytes.


Alles beisammen

Nun brauchts das Unterteil und Rumgelöte. Mal schauen, wann ich mich dazu motivieren kann.....Morgen vielleicht.