#ifdef DEBUGPIN
# include "heha_gpio.h"
#endif
#include "heha_tui.h"
#include <signal.h> // usleep(), ...
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/i2c-dev.h>
#include <semaphore.h>
#include <cerrno> // errno
#include <cstdio> // sscanf()
#include <cstring> // strerror()
#include <chrono> // std::chrono
//#include <ctime>
/* I²C-Abfrage- und Steuerprogramm (TUI-Version) für
Fernbedien-Adapter der BGA-Fahrzeuge mit ATmega328 (etwa Arduino Uno)
230207 heha
*/
// Tabellenartige Bildschirmaufteilung; Spaltenbreiten:
enum{
R1C1 = 18, // " Ultraschall-Echo"
R1C2 = 20,
R1C3 = 17, // " Licht Fahrerhaus"
R2C1 = 12, // " Kanaldaten "
};
/******************
* Hex/ASCII-Dump *
******************/
// Hex-Dump: Gesamtbreite = (l0+l1+l2)*3
static void dumpHex(byte const*p,unsigned l0, unsigned l1, unsigned l2,const ChrColor*colors=0) {
for (unsigned i=0; i<l0; i++) heha::print(" ");
if (l1) for (unsigned i=0;;) {
if (colors) colors[i].out();
heha::print("%02X",p[i]);
if (colors) ChrColor::clr();
if (++i==l1) break;
putchar(' ');
}
for (unsigned i=0; i<l2; i++) heha::print(" ");
}
// ASCII-Dump: Gesamtbreite = l0+l1+l2
static void dumpAsc(byte const*p,unsigned l0, unsigned l1, unsigned l2) {
auto printable=[](char c) {return 32<=c && c<127 ? c : '.';};
// TODO: UTF8-Sonderbehandlung
for (unsigned i=0; i<l0; i++) heha::print(" ");
for (unsigned i=0; i<l1; i++) heha::print("%c", printable(p[i]));
for (unsigned i=0; i<l2; i++) heha::print(" ");
}
// Hexdump: Adresse und bis zu 16 Bytes pro Zeile
static void dump(void const*pv,size_t l,unsigned a=0, unsigned linelen=16, unsigned l0=0) {
auto p=reinterpret_cast<byte const*>(pv);
while (l) {
unsigned l1 = linelen-l0; if (l1>l) l1=(unsigned)l; // "Fleisch"
unsigned l2 = linelen-l1-l0;
csi(1,36,'m');
heha::print("%04X ",a-l0); // Adresse türkis
csi(32,'m');
dumpHex(p,l0,l1,l2); // Hexbytes grün
putchar(' ');
csi(33,'m');
dumpAsc(p,l0,l1,l2); // ASCII gelb
p+=l1; l-=l1; a+=l1; l0=0; // weitere Zeilen von vorn füllen
nextline();
}
}
/*********************
* I²C-Kommunikation *
*********************/
namespace OneWire{
byte crc8(const byte*p,byte l) {
byte r = 0;
const byte poly = 0x8C;
do{
r^=*p++;
byte bits=8; do{
if (r&1) r = r>>1 ^ poly;
else r>>=1;
}while(--bits);
}while (--l);
return r;
}
}
// Die Funktionen dieser Klasse sind geschwätzig bei Fehler
static struct I2C{
int f; // I²C-Handle
::sem_t*sem;
char semname[16];
I2C():f(-1) {} // Handle ungültig initialisieren
bool open(byte,byte);
bool send(const void*p,int l,const char*caller) const {
return queryLong(p,l,0,0,caller);
// dump(p,l);
// lock();
// bool ret=write(p,l,caller);
// unlock();
// return ret;
}
bool queryLong(const void*s,int sl,void*r,int rl,const char*caller) const {
if (sl<0 || rl<0) return false; // Negative Längen: Fehler
if (sl && !s || rl && !r) return false; // Länge ohne Zeiger: Fehler
if (!sl && !rl) return true; // Durchläufer: Nichts zu tun
auto sb = reinterpret_cast<const byte*>(s);
auto rb = reinterpret_cast<byte*>(r);
if (!rl) return queryShort(sb,sl,0,0,caller);
bool ret = true, first = true;
byte o = sl>=2 ? sb[1] : 0;
while (rl) {
byte buf[65];
int ll = rl>64 ? 64 : rl; // max. 64 Bytes + CRC lesen
byte sn[2]={sb[0],o};
if (queryShort(first?sb:sn,first?sl:2,buf,ll+1,caller)
&& checkCRC(buf,ll,caller)) {
memcpy(rb,buf,ll);
rb+=ll;
rl-=ll;
o+=ll;
}else return false;
first=false;
}
return ret;
}
bool query(byte b,void*q,int ql,const char*func) const{
return queryLong(&b,1,q,ql,func);
}
void close() {::close(f); f=-1; sem_unlink(semname); sem=0;}
private:
// bool write(const void*,int,const char*) const;
bool queryShort(const byte*,int,byte*,int,const char*) const;
bool checkCRC(const byte*,int,const char*) const;
int lock() const;
int unlock() const {return ::sem_post(sem);}
}i2c;
int I2C::lock() const {
timespec ts;
clock_gettime(CLOCK_REALTIME,&ts);
ts.tv_nsec+=100'000'000; // 100 ms;
if (ts.tv_nsec>=1'000'000'000) { // 1 s überschritten?
ts.tv_nsec-=1'000'000'000;
ts.tv_sec++;
}
return ::sem_timedwait(sem,&ts); // Was für ein Linux-Mumpitz!
// Bei springender Uhrzeit kann diese Funktion ewig warten!
// Denn welche Art von <clock> sem_timedwait() will ist nicht dokumentiert.
}
bool I2C::open(byte nr,byte i2cslave) {
char devname[32];
heha::snprint(devname,sizeof devname,"/dev/i2c-%u",nr);
f=::open(devname,O_RDWR);
if (f<0) {
heha::print(stderr,"Kann %s nicht öffnen: %d: %s",
devname,errno,strerror(errno));
nextline();
return false;
}
if (ioctl(f,I2C_SLAVE,i2cslave)<0) {
close();
heha::print(stderr,"Kann I²C-Slave-Adresse %02x (AVR:%#02X) nicht setzen, errno=%d: %s",
i2cslave,i2cslave<<1,errno,strerror(errno));
nextline();
return false;
}
// "Wohlgeformter" Name aus I²C-Nummer und i2cdetect-mäßiger Slave-Adresse.
// Muss im Python-Programm übereinstimmen, wenn beide Programme
// (Prozesse) nebeneinander laufen dürfen sollen.
// Dazwischen funkende I²C-Aktionen auf andere Slaves
// (bspw. Echtzeituhr, Inertialsensor) stören nicht,
// sofern diese der Software-Treiber gefälligst atomar behandelt.
heha::snprint(semname,sizeof semname,"/i2c%u.%02x",nr,i2cslave);
sem = sem_open(semname,O_CREAT,0660,1);
if (!sem) {
close();
heha::print(stderr,"Kann Semaphore %s nicht erstellen oder öffnen, errno=%d: %s!",
semname,errno,strerror(errno));
nextline();
return false;
}
return true;
}
static void Fehlermeldung(const char*typ, int ist, int soll, const char*caller) {
heha::print(stderr,typ);
if (ist<0) heha::print(stderr,
" (%d Bytes): errno=%d: %s,",
soll,errno,strerror(errno),caller);
else heha::print(stderr,
": %d≠%d Bytes!",
ist,soll);
heha::print(stderr," Aufrufer=%s",caller);
nextline();
}
bool I2C::queryShort(const byte*sb,int sl,byte*rb,int rl,const char*caller) const{
if (sl<0 || rl<0) return false;
if (sl && !sb || rl && !rb) return false;
if (!sl && !rl) return true;
if (rl>65) return false;
bool ret=true;
lock();
#ifdef DEBUGPIN
gpio::clr(DEBUGPIN);
#endif
if (sl) {
int versuche=5, r=0;
do{
using namespace std::chrono;
auto start = steady_clock::now();
while (steady_clock::now()-start<100us);
r=::write(f,sb,sl);
} while (r<0 && (errno==121 || errno==5) && --versuche);
if (r!=sl) {
ret=false;
#ifdef DEBUGPIN
gpio::set(DEBUGPIN);
#endif
Fehlermeldung("Schreibfehler",r,sl,caller);
}
}
if (rl && ret) {
int versuche=5, r=0;
do{
//Dem AVR etwas Zeit geben
using namespace std::chrono;
auto start = steady_clock::now();
while (steady_clock::now()-start<100us);
#ifdef DEBUGPIN
if (r<0) gpio::clr(DEBUGPIN);
#endif
// Der Fehler 121 ist Timeout durch unresponsiven AVR, d.h.
// er hat noch mit Rechnen zu tun, macht Clock-Stretch
// und antwortet dem Hardwaretreiber zu langsam.
// 230726: Beim von Felix eingerichteten Raspberry Pi 4
// Compute Module kommt stattdessen Fehlerkode 5.
r=::read(f,rb,rl);
#ifdef DEBUGPIN
// Clock-Stretch-Versuche ebenfalls am Debugpin oszillografieren
if (r<0) gpio::set(DEBUGPIN);
#endif
} while (r<0 && (errno==121 || errno==5) && --versuche);
if (r!=rl) {
ret=false;
#ifdef DEBUGPIN
// Nur für den (seltenen) Fall dass r≥0 aber r≠rl ist
gpio::set(DEBUGPIN);
#endif
Fehlermeldung("Lesefehler",r,rl,caller);
}
}
unlock();
return ret;
}
bool I2C::checkCRC(const byte*rb,int rl,const char*caller) const{
if (rl>64) {
heha::print(stderr,"%s:%d: Interner Programmfehler! Aufrufer=%s",__FILE__,__LINE__,caller);
nextline();
return false;
}
byte crc=~OneWire::crc8(rb,rl);
if (crc!=rb[rl]) {
#ifdef DEBUGPIN
gpio::set(DEBUGPIN); // Auch CRC-Fehler anzeigen
#endif
heha::print(stderr,"%s: CRC-Fehler, %02X!=%02X! Aufrufer=%s",__FUNCTION__,crc,rb[rl],caller);
nextline();
return false;
}
return true;
}
static void writeaction(unsigned a, const char*pw, unsigned l) {
char*cmd=new char[2+l];
cmd[0]=char(a>>8);
cmd[1]=char(a);
memcpy(cmd+2,pw,l);
i2c.send(cmd,2+l,__FUNCTION__);
}
static void emitLinks2(const char*title) {
if (screen.paintAll) heha::print("║%*s │ ",R2C1-1,title); else csi(R2C1+3,'C');
}
static void frameRechts() {
if (screen.paintAll) {csi(screen.w,'C'); heha::print("║");}
// Bewirkt keinen Zeilenumbruch und kein Schreiben außerhalb des Terminalfensters
}
/****************
* Ebene 0: Uhr *
****************/
// War ursprünglich als Echtzeituhr für den Raspberry geplant,
// aber das mit dem Uhrenquarz und der PLL hat nie ordentlich funktioniert,
// es gab (auch bei unbeeinflusstem OSCCAL) immer wieder Probleme
// mit der seriellen Schnittstelle sowie Instabilitäten beim PWM-Output.
// 230221: Vermutlich war es (noch) ein anderes Problem,
// denn die gelegentlichen Prüfsummenfehler sind immer noch da
// und stören nicht mehr.
// Die verwendete Raspberry-USV hat einen (ungenutzten) Echtzeituhr-Chip.
static struct __attribute__((packed)) L0{
byte ms10; // Hundertstel-Sekunde
uint32_t losec;// Ganze Sekunden seit 1.1.1970
char hisec;// Überlauf-Byte (kommt erst im nächsten Jahrhundert)
bool query() {return i2c.query(0x60,this,sizeof*this,"L0::query");}
void show() const;
L0() = default;
L0(std::chrono::system_clock::time_point t) {set(t,false);}
static int find(const Mouse&);
void set(std::chrono::system_clock::time_point,bool writeout=true);
operator char*() {return (char*)this;}
}l0;
void L0::show() const{
struct LONGLONG{ // Nur für Little Endian (Big Endian ist ausgestorben)
uint32_t lo;
int32_t hi;
operator int64_t() const {return *(int64_t*)this;}
}s = {losec,hisec};
::time_t time = s;
tm t;
::localtime_r(&time,&t);
heha::print("%4d-%02u-%02u %02u:%02u:%02u,%02u", // 21 Zeichen
t.tm_year+1900,
t.tm_mon+1,
t.tm_mday,
t.tm_hour,
t.tm_min,
t.tm_sec,
ms10);
}
int L0::find(const Mouse&mouse) {
if (mouse.y!=13) return -1;
int c = mouse.x-51;
if (c<0) return -1;
if (c>23) return -1;
return 0;
}
void L0::set(std::chrono::system_clock::time_point t, bool writeout) {
using namespace std::chrono;
milliseconds ms = duration_cast<milliseconds>(t.time_since_epoch());
int64_t s = duration_cast<seconds>(ms).count();
ms10 = ms.count()/10%100; // 0..99
losec = s;
hisec = s>>32; // 5 Bytes für Sekunden genügen auf sehr lange Sicht
if (writeout) writeaction(0x6000,(char*)this,sizeof*this);
}
/*****************************************
* Ebene 1: EEPROM (Konfigurationsdaten) *
*****************************************/
template<byte N>struct __attribute__((packed)) CalibData{
byte n;
struct __attribute__((packed)) A{
int16_t adcvalue;
int8_t per125;
}a[N];
};
static struct __attribute__((packed)) L1{
byte lamps,
spiaddr; // 0xBA: Radlader, 0xBC: Muldenkipper
CalibData<11>lenkung;
CalibData<3>hubarm,löffel;
int16_t t0,ts,rsv[2]; // für (nutzlosen) Temperatursensor
struct __attribute__((packed)) PID{
char Kp[2],Ki[2],Kd[2], // PID-Koeffizienten je nach Richtung (normalerweise gleich)
Z[4], // Spreizung: lo-hi festklebend, lo-hi losgerissen
L[4]; // Limit Integralwert, lo-hi je nach Richtung
byte T,M; // Zeit; Schwellwert zur "Losgerissen"-Erkennung (> Rauschen)
}pi[4]; // PI-Reglerdaten
struct __attribute__((packed)) Fz{
int8_t sp[2]; // Maximalgeschwindigkeit in cm/s (zurück/vorwärts)
int16_t onestep; // 0,1 µm pro Schritt: 2553 beim Muldenkipper, positiv
int16_t q[4]; // Winkeländerung pro Schritt für 10°, 20°, 30° und max. Lenkwinkel, positiv
int16_t rsv[2];
}fz; // Fahrzeugparameter
byte debug[32];
bool query() {return i2c.query(0x61,this,sizeof*this,"L1::query");}
bool queryDebug() {byte q[]={0x61,debug-(byte*)this}; return i2c.queryLong(q,2,&debug,sizeof debug,"L1::queryDebug");}
void show() const;
static int find(const Mouse&);
}l1;
static_assert(sizeof l1==0xB0);
void L1::show() const{
static byte odebug[sizeof debug];
static ChrColor hilite[sizeof debug];
for (unsigned i=0; i<sizeof debug; i++) {
if (odebug[i]!=debug[i]) {
odebug[i]=debug[i];
hilite[i].b=33; // (33-17) / 10Hz = 2 s
}else if (hilite[i].b && --hilite[i].b==16) hilite[i].b=0;
}
emitLinks2("Debug");
byte len=(screen.w-R2C1-3)/3; // Anzahl Bytes in Zeile bei Fensterbreite
if (len>sizeof debug) len=sizeof debug;
dumpHex(debug,0,len,0,hilite);
frameRechts();
nextline();
}
int L1::find(const Mouse&mouse) {
if (mouse.y!=15) return -1;
int c = mouse.x-(R2C1+3);
if (c<0) return -1;
if (c%3==2) return -1;
return c/3;
}
/************************
* Ebene 4: A/D-Wandler *
************************/
int MulDiv(int a, int b, int c) {return (long long)a*b/c;}
unsigned uMulDiv(unsigned a, unsigned b, unsigned c) {
return (unsigned long long)a*b/c;
}
static struct L4{
int16_t lin[7]; // "Menschenlesbare" linearisierte Werte
uint8_t flags,lamps;
int16_t raw[8];
uint32_t kmz;
bool query() {return i2c.query(0x64,this,sizeof*this,"L4::query");}
void show() const;
static int find(const Mouse&); // Gesetzt werden diverse Zähler
static void set(char,char); // Flags setzen als Firmware-Hintertür
}l4;
static const char*utf8next(const char*s) {
while (*++s<-64); // Trailbytes (ohne weitere Prüfung) übergehen
return s;
}
static void emitHorizontalDivider(const char*s,...) {
va_list va;
va_start(va,s);
while (*s) {
auto p=utf8next(s);
fwrite(s,p-s,1,stdout);
if (!*p) break;
auto q=utf8next(p);
int count=va_arg(va,int);
for (int i=0; i<count; i++) fwrite(p,q-p,1,stdout);
s=q;
}
va_end(va);
}
static void emitLinks(byte k, const char*name,const char*unit,short value,unsigned short raw) {
if (screen.paintAll) {
heha::print("║");
byte spc = R1C1;
if (k) {
csi(1,'m');
heha::print("K%u",k);
csi('m');
spc-=2;
}
heha::print("%*s:",spc,name);
}else{
csi(1+R1C1+1,'C');
}
heha::print("%7,2hj %-5s(%04X)",value,unit,raw);
if (screen.paintAll) heha::print(" ║"); else csi(2,'C');
}
static void emitRechts(const char*name,unsigned selector,const char*sel) {
if (screen.paintAll) heha::print("%*s: ",R1C3,name); else csi(R1C3+2,'C');
auto flags=selector; selector&=7;
char fmt[64];
heha::snprint(fmt,sizeof fmt,"%%8(%s) ",sel);
static const ChrColor clr[2]={{0,1},{0,2}};
if (!(flags&0x10)) clr[flags&0x40?0:1].out(); // Bit 4 = unbunt, Bit 6 = rot statt grün
heha::print(fmt,selector); // 9 Zeichen breit
if (!(flags&0x10)) ChrColor::clr();
if (!(flags&0x20)) heha::print(" (%u)",selector); // Bit 5 = keine Zahl dahinter
frameRechts();
nextline();
}
void L4::show() const{
if (screen.paintAll) {
emitHorizontalDivider("╔═╦═╗",R1C1+1+R1C2,screen.w-R1C2-R1C1-4);
}
nextline();
const bool isMk = l1.spiaddr==0xBC; // ist Muldenkipper
emitLinks(1,"Lenkung", "°", lin[0],raw[0]);
emitRechts("Zündung", flags>>3&1, "aus|ein");
emitLinks(2,"Geschwindigkeit", "cm/s", lin[1],raw[6]);
emitRechts("Rundumleuchte", flags>>5, "aus|ein|langsam|blitzend|blinkend");
emitLinks(3,isMk?"Ladefläche":"Hubarm", "°", lin[2],raw[1]);
emitRechts("Licht Fahrerhaus", lamps>>5, "aus|ein|-*|....--|blinkend");
emitLinks(4,isMk?"Verwindung":"Löffel", "°", lin[3],raw[2]);
emitRechts("Licht Fahrzeug", lamps&7, "aus|vorn|beide|aus|hinten|beide");
emitLinks(0,"Traktionsakku", "V", lin[4],raw[3]);
emitRechts("Blinklicht", lamps>>3&3, "aus|blitzend|blinkend");
emitLinks(0,"Chiptemperatur", "°C", lin[5],raw[4]);
emitRechts(isMk?"Blinker":"Blinkgeräusch", flags>>4&1, "aus|ein");
emitLinks(0,"Ultraschall-Echo", "dm", lin[6],raw[5]);
emitRechts(isMk?"Rückfahrtaster":"funktionslos",flags&1|0x30, "aus|ein");
if (screen.paintAll) heha::print("║%*s:",R1C1-5,"Fahrstrecke"); else csi(R1C1-3,'C');
heha::print("%'12,2u m (%08X)║",
uMulDiv(kmz,l1.fz.onestep,100000U),
kmz);
emitRechts("Notaus", flags>>1&3|0x40,"nein|ja|intern|remote");
if (screen.paintAll) {
emitHorizontalDivider("╠═╤═╩═╣",R2C1,R1C1+R1C2-R2C1,screen.w-R1C1-R1C2-4);
}
nextline();
}
int L4::find(const Mouse&mouse) {
char relX=screen.mouse.x-(1+R1C1+1+R1C2+1+R1C3+2);
if ((unsigned)relX<9) switch (screen.mouse.y) {
case 1: return 0; // Zündung
case 2: return 5; // Rundumleuchte
case 3: return 2; // Licht Fahrerhaus, nicht beim Muldenkipper
case 4: return 1; // Licht Fahrzeug
case 5: return 3; // Blinklicht
case 6: return 4; // Blinkgeräusch, beim Muldenkipper Blinker (links+rechts gleich)
case 8: return 6; // Externes Zwangs-Notaus
}
if (12<=mouse.x && mouse.x<40 && mouse.y==8) return 7; // Kilometerzähler rücksetzen
return -1;
}
void L4::set(char k, char count) {
char buf[]={0x64,k,count}; // Schaltet einen der Schalter (Zündung, Licht, Licht, Rundum) weiter,
i2c.send(buf,sizeof buf,"L4::set"); // um das Timing kümmert sich die Firmware (1 Puls reicht dem jeweiligen µC)
}
/*******************
* Ebene 2: Servos *
*******************/
static struct L2{ // Servos
char pwms[9];
byte pwmok;
bool query() {return i2c.query(0x62,this,sizeof*this,"L2::query");}
void show() const;
static void showRuler();
enum{
REL=0x72,
ABS=0x62,
};
static int find(const Mouse&);
static void move(char, char, char prefix=REL);
static void set(char k, char to) {move(k,to,ABS);}
}l2;
void L2::showRuler() {
emitLinks2("Kanaldaten");
if (screen.paintAll) for (int i=0; i<10; i++) {
if (i<4) csi(1,'m'); // Die Kanäle mit Steuerknüppeln hervorheben
heha::print(" K%2u ",i+1);
if (i<4) csi('m');
}
frameRechts();
nextline();
}
void L2::show() const{
emitLinks2("Servos");
for (int i=0; i<sizeof pwms; i++) { // 6x9 = 54 Spalten
csi(i==4||i==8?100:44,'m'); // 44 = Hintergrund blau weil editierbar
// 100 = grau weil nicht sinnvoll editierbar
heha::print("%4hhj ",pwms[i]); // max. '-127' = 4 Spalten
csi('m');
heha::print(" ");
}
heha::print("%4u",pwmok);
frameRechts();
nextline();
}
int L2::find(const Mouse&mouse) {
if (mouse.y!=11) return -1; // 10. Zeile von oben
int c = mouse.x-R2C1-3; // Breite für Titel
if (c<0) return -1;
if (c>=9*6) return -1;
if (c%6==5) return -1;
return c/6;
}
void L2::move(char k, char by, char prefix) {
char buf[]={prefix,k,by}; // Fernbedienung stets(!) entreißen; Bit 4: Addieren
i2c.send(buf,sizeof buf,"L2::send");
}
/***********************
* Ebene 3: Controller *
***********************/
static struct L3{ // Fernbedienung
char values[10];
bool query(bool steal=false);
// Mit <true> wird die mikrocontroller-interne Weiterleitung
// von Controller-Meldungen zum PWM-Ausgang für reichlich 1 s gesperrt.
// Dies ist dazu gedacht, dass der I²C-Master den Controller
// (durch zyklische Abfrage) kapert und irgendetwas anderes mit den Daten macht.
// Wird in diesem Programm nicht benutzt.
void show() const;
}l3;
bool L3::query(bool steal) {
return i2c.query(steal?0x73:0x63,this,sizeof*this,"L3::query");
}
void L3::show() const{
emitLinks2("Controller");
for (int i=0; i<sizeof values; i++) { // 6x10 = 60 Spalten
heha::print("%4hhj ",values[i]); // max. '-127' = 4 Spalten
}
frameRechts();
nextline();
}
/*******************
* Ebene 5: Regler *
*******************/
static struct Regler{ // Regler
char so; // Sollwert
struct F{
byte vmax:7; // Geschwindigkeit (beim Fahrregler Beschleunigung??)
bool losgerissen:1;
}f;
uint16_t to; // Timeout
int16_t eΣ; // Integralwert
int16_t eΩ; // Letzter Wert (für Differenzbildung), geglättet
static bool query();
static void showSoll(); // ohne Newline
static void showSpeed();
enum{
REL=0x75,
ABS=0x65,
};
static void move(char,char,char cmd=REL);
static void set(char k, char c);
static MiniEdit*edit[4];
static void createWindows();
void showReglerData() const;
static char firstActive();
}regler[4];
MiniEdit*Regler::edit[4];
void Regler::createWindows() {
for (int i=0; i<4; i++) edit[i]=new MiniEdit(0x016500|i,R2C1+3+i*6,13); // nullbasiert
}
bool Regler::query() {return i2c.query(0x65,regler,sizeof regler,"Regler::query");}
static void showText(MiniEdit*ed,const char*fmt,...) {
if (Window::focus!=ed) {
va_list va;
va_start(va,fmt);
heha::snvprint(ed->text,31,fmt,va);
va_end(va);
ed->onNewText();
}
ed->paint();
}
void Regler::showSoll() {
emitLinks2("Regler");
for (int i=0; i<4; i++) {
showText(edit[i],"%hhj",regler[i].so); // max. '-127' = 4 Spalten
// csi(44,'m');
// csi('m');
heha::print(i==1?" ":"°");
} // kein frameRechts, Uhr kommt daneben
}
void Regler::move(char k, char by, char prefix) {
char buf[]={prefix,k,by};
i2c.send(buf,sizeof buf,"Regler::send"); // TODO (Firmware): Von NaN aus ist „relativ“ vom Istwert
}
void Regler::set(char k, char c) {
move(k,c,ABS);
}
char Regler::firstActive() {
for (char i=0; i<4; i++) if (regler[i].so!=heha::cNaN) return i;
return heha::cNaN;
}
void Regler::showReglerData() const{
if (screen.paintAll) csi('K');
emitLinks2("ReglerData");
heha::print("so=%hhj, v=%u, %(festklebend|losgerissen), to=%,2u, eΣ=%,2hj, eΩ=%,2hj ",
so,f.vmax,f.losgerissen,to<<1,eΣ*100>>8,eΩ);
frameRechts();
nextline();
}
/**********************
* Ebene 6: Odometrie *
**********************/
static struct Odo{
int32_t x,y,α; // Neu: α = 256 Vollkreise und 24 Nachkomma-Bits
int16_t q,λ;
bool query() {return i2c.query(0x66,this,sizeof*this,"Odo::query");}
void show() const;
enum{
REL=0x76,
ABS=0x66,
};
static void move(char,char,char cmd=REL);
static void set(char k, char c);
static MiniEdit*edit[5];
static void createWindows();
}odo;
static_assert(sizeof odo==16);
MiniEdit*Odo::edit[5];
void Odo::createWindows() {
edit[0]=new MiniEdit(0x046600,R2C1+5, 14,9,1,0,pal1);
edit[1]=new MiniEdit(0x046604,R2C1+5+13, 14,9,1,0,pal1);
edit[2]=new MiniEdit(0x046608,R2C1+5+13+13, 14,9,1,0,pal1);
edit[3]=new MiniEdit(0x02660C,R2C1+5+13+13+13, 14,7,1,0,pal1);
edit[3]->style|=1<<Window::DISABLED;
edit[4]=new MiniEdit(0x02660E,R2C1+5+13+13+13+11,14,7,1,0,pal1);
edit[4]->style|=1<<Window::DISABLED;
}
void Odo::show() const{
emitLinks2("Odometrie");
if (screen.paintAll) heha::print("x="); else csi(2,'C');
showText(edit[0],"%,3j",x/10000); // max. '-127' = 4 Spalten
if (screen.paintAll) heha::print("m y="); else csi(4,'C');
showText(edit[1],"%,3j",y/10000);
if (screen.paintAll) heha::print("m α="); else csi(4,'C');
showText(edit[2],"%,3j",(α>>8)*1000>>16);
if (screen.paintAll) heha::print("U q="); else csi(4,'C');
showText(edit[3],"%hj",q);
if (screen.paintAll) heha::print("µ λ="); else csi(4,'C');
showText(edit[4],"%,2hj",λ);
if (screen.paintAll) heha::print("°");
frameRechts();
nextline();
}
/*************************
* Ebene 7: Fahraufträge *
*************************/
struct __attribute__((packed)) L7{
char soll[4],speed[4];
int32_t s;
uint16_t w;
L7();
bool fromString(const char*);
};
L7::L7():s(0),w(0) {
memset(soll,heha::cNaN,8);
}
static bool parseerror(const char*fmt,...) {
va_list va;
va_start(va,fmt);
heha::vprint(fmt,va);
nextline();
va_end(va);
return false;
}
bool L7::fromString(const char*s) {
char*q;
byte items=0;
for(;;) {
while (*s==' ') ++s;
if (!*s) break;
unsigned sel = strtoul(s,&q,10);
if (q==s) return parseerror("Keine Zahl als Selektor, ungültiges Zeichen!");
s=q;
if (sel>=6) return parseerror("Selektor zu groß! Nur 0..5");
if (items&1<<sel) return parseerror("Selektor mehrfach!");
items|=1<<sel;
if ((items&0b010010)==0b010010) return parseerror("Selektor 1 und 4 dürfen nicht gemeinsam gegeben werden!");
if (*s!=':') return parseerror("Nach Selektor muss Doppelpunkt folgen!");
int zahl=strtol(++s,&q,10);
if (q==s) return parseerror("Zahlenangabe fehlt oder ungültiges Zeichen!");
s=q;
switch (sel) {
case 5:
if (zahl<=0) return parseerror("Zahl muss positiv sein!");
if (zahl>50*60*3) return parseerror("Wartezeit zu groß! Max. 3 Minuten");
w=zahl; break;
case 4: this->s=zahl; break;
default:
if (zahl>125 || zahl<-125) return parseerror("Zielangabe außerhalb ±125");
soll[sel]=zahl;
}
if (sel<5 && *s==':') {
zahl=strtol(++s,&q,10);
if (q==s) return parseerror("Zusatzzahl nach Doppelpunkt ungültig");
s=q;
switch (sel) {
case 4:
if (zahl<-125 || zahl>125) return parseerror("Geschwindigkeit außerhalb ±125");
soll[1]=zahl;
if (*s==':') {
zahl=strtol(++s,&q,10);
if (q==s) return parseerror("Beschleunigung nach Doppelpunkt ungültig");
s=q;
if (!zahl || zahl>125) return parseerror("Beschleunigung muss im Bereich 1..125 liegen");
speed[1]=zahl;
}break;
default:
if (!zahl || zahl>125) return parseerror("Regeltempo muss im Bereich 1..125 liegen");
speed[sel]=zahl; break;
}
}
}
if (!items) return parseerror("Keine Aufträge, leerer String");
if (items&2 && !(items&0x20)) return parseerror("Geschwindigkeits-Fahrauftrag ohne Zeitlimit fraglich");
return true;
}
namespace Auftrag{
static MiniEdit*edit;
static void createWindow() {
edit=new MiniEdit(0x0E6700,R2C1+5,16,60,1,0,pal0);
edit->style|=1<<MiniEdit::LEFTALIGN;
edit->onNewText(); // neu ausrichten
}
static void show() {
emitLinks2("Auftrag");
static bool dirty;
if (screen.paintAll || Window::focus==edit) {
edit->paint();
dirty=true;
}else if (dirty) {
edit->paint(); // nichtfokussierten Zustand 1x malen
dirty=false;
}
frameRechts();
nextline();
}
static Window::LPARAM onEnter(Window::UINT msg) {
if (msg!=MiniEdit::EN_ENTER) return 0;
edit->text[edit->tlen]=0;
L7 auftrag;
bool r = auftrag.fromString(edit->text);
edit->text[edit->tlen]=' ';
if (!r) return 0;
byte buf[2+sizeof auftrag];
buf[0]=0x67;
buf[1]=0;
memcpy(buf+2,&auftrag,sizeof auftrag);
i2c.send(buf,sizeof buf,"Auftrag::onEnter");
return 1;
}
}// namespace
static int showMouseHelp() {
int c=L0::find(screen.mouse);
if (c>=0) return heha::print("Uhr: Links = Uhr stellen");
c=L1::find(screen.mouse);
if (c>=0) return heha::print("Debug-Byte an Adresse %u = %#02X, siehe <fba.h>",c,c);
c=L2::find(screen.mouse);
if (c>=0) return heha::print("Servo %d: Links = +125, Mitte = 0, rechts = -125, Scrollrad = ±5, mit Strg ±10",c+1);
c=L4::find(screen.mouse);
if (c>=0) return c==7
? heha::print("(Kilo-)Meterzähler: Rechtsklick = nullsetzen")
: !c || c==6
? heha::print("%(Zündung|Notaus): Linksklick = schalten, Rechtsklick = nullsetzen ohne schalten",c)
: heha::print("%(Licht|Licht|Blinklicht|Blinkgeräusch|Rundumleuchte): Linksklick = weiter, Rechtsklick = nullsetzen ohne schalten",c-1);
auto w = screen.find(screen.mouse);
if (w) {
c=w->id;
switch (c>>8&0xFF) {
case 0x65: return heha::print("Regler %d: Mitte = Stopp, Scrollrad = ±5, mit Strg ±10",(c&0xFF)+1);
case 0x66: if ((c&0xFF)<12) return heha::print("Mitte: Odometrie-Parameter nullsetzen");
}
}
return heha::print("(Keine Mausaktion)");
}
void Screen::onMouse(const Mouse&m) {
if ((m.code&0x60)==0x40) mousehelp=false;
auto w = capture ? capture : find(m);
// heha::print(w?w==this?"x":"y":"z");
if (w) {
if (w!=this) w->onMouse(m);
else switch (m.code&0x63) {
case 0x20: // lButtonDown
case 0x21: // mButtonDown
case 0x22: { // rButtonDown
int c = L0::find(m); // Uhr
if (c>=0) return l0.set(std::chrono::system_clock::now());
c = L2::find(m);
if (c>=0) return L2::set(c,m.buttons.L ? 125 : m.buttons.R ? -125 : 0);
c = L4::find(m);
if (c>=0) return L4::set(c,m.buttons.L);
if (Window::focus) Window::focus->killFocus();
heha::print("Mausklick ohne Ziel (%d,%d,CASMRL=%06b)",m.x,m.y,m.buttons);
nextline();
}break;
case 0x60: // wheelUp
case 0x61: { // wheelDown
int Δ = 5;
if (m.buttons.C) Δ = 1; // Control = Feinkontrolle
if (m.buttons.S) Δ<<=1; // Shift verdoppelt
if (m.code&1) Δ = -Δ; // WheelDown invertiert
int c = L2::find(m);
if (c>=0) return L2::move(c,Δ);
}break;
}
// Niemals Capture ohne gedrückte Maustaste!
if (!(m.buttons&7) && Window::capture) Window::capture->releaseCapture();
}
mouse = m; // alt = neu (Struktur aus 4 Bytes kopieren)
// Bei k<0 Schalter nullsetzen (zum Synchronisieren eines außer Tritt gekommenen Schalters)
/*
c = Regler::find(mouse);
if (c>=0) {
if (k==125) {
auto ed=Regler::edit[c];
ed->setFocus();
ed->setsel(mouse.x-ed->x);
}else Regler::set(c,mouse.buttons.L?0:heha::cNaN);
return;
}
*/
}
/*
static void onButtonUp() {}
static void onDrag() {
heha::print("Unverarbeitetes Maus-Zieh-Ereignis, x=%d, y=%d, CASMRL=%06b",
mouse.x,mouse.y,mouse.buttons);
nextline();
}
static void onWheel(int Δ) {
int c = L2::find();
if (c>=0) return L2::move(c,Δ);
c = Regler::find();
if (c>=0) return Regler::move(c,Δ);
heha::print("Unverarbeitetes Mausrad-Ereignis, x=%d, y=%d, CASMRL=%06b, Δ=%d",
mouse.x,mouse.y,mouse.buttons,Δ);
nextline();
}
*/
static bool inputhandler() {
fflush(stdout);
char keybuf[16];
int k=::read(0,keybuf,sizeof keybuf-1); // blockiert 1/10 s oder bis Tastenkode/Mausereignis vorhanden
if (k>0) {
keybuf[k]=0; // zur Auswertung von ESC allein
return screen.onKey(keybuf);
}
return true;
}
// Aufruf (nur) mit m.code==0x20,0x21 oder 0x22:
void Screen::checkDblClk(Mouse&m) {
using namespace std::chrono; // Literal "ms" zugreifbar machen
static byte lastButton = 3;
static steady_clock::time_point lastCall;
auto jetzt = steady_clock::now();
if ((m.code&3) == lastButton) {// gleiche Taste?
if (jetzt-lastCall < 250ms) {
m.code = 0x80 | m.code&3; // in Mehrfachklick-Kode wandeln
m.buttons.Z++; // Klicks zählen (maximal 4)
}else{
m.buttons.Z=0; // Kein Mehrfachklick, zu langsam
}
}else{
lastButton = m.code&3; // Taste hat gewechselt, neu messen
m.buttons.Z=0; // Kein Mehrfachklick
}
lastCall = jetzt;
}
// Hier: false = Programmende!
bool Screen::onKey(const char*input) {
if (Window::focus && Window::focus->onKey(input)) return true;
auto e = input+strlen(input);
bool uneaten=false;
unsigned loopcount=0;
while (input<e) {
char d=5; // Standard-Servoweg
if (input+1==e) switch (*input++) { // ++: Unschön bei UTF-8
case '\e': // Escape
case 'q':
case 3: return false; // ^C
#ifdef DEBUGPIN
case '#': gpio::set(DEBUGPIN); break;// Zum Test Pin auf High
#endif
case 'W': d=10; nobreak;
case 'w': L2::move(1,+d); break; // 25 Schritte bis voll
case 'A': d=10; nobreak;
case 'a': L2::move(0,-d); break;
case 'S': d=10; nobreak;
case 's': L2::move(1,-d); break;
case 'D': d=10; nobreak;
case 'd': L2::move(0,+d); break;
case 'X':
case 'x': L2::set(1,0); break; // Stopp Fahren
case 'Y':
case 'y': L2::set(0,0); break; // Stopp Lenken
case 'r': L2::set(5,+100); break; // Hupe
case 'f': L2::set(5,0); break; // Hupe aus
case '\t': if (screen.sub) screen.sub->setFocus(); break;
case 'L'-'@': screen.w=0; break; // ^L: Refresh
default: uneaten=true;
}else if (input+3<=e && *input=='\e' && input[1]=='[') {
byte argv[2], argc=csidecode(input,argv,2); // rückt <input> ans Ende der CSI-Sequenz
switch (input[-1]) { // Letztes Zeichen
case 'A': L2::move(2,+d); break; // Pfeil hoch: Ladefläche hoch, Arm runter
case 'B': L2::move(2,-d); break; // Pfeil runter: Ladefläche runter, Arm hoch
case 'C': L2::move(3,+d); break; // Pfeil rechts: Löffel auskippen
case 'D': L2::move(3,-d); break; // Pfeil links: Löffel
// Quelle der Zuordnung: Eurosteuerung für Bagger
case '~': if (argc==1) switch (argv[0]) {
case 15: screen.w=0; break; // F5: Refresh
// heha::print("%c:%u,%u,%u\n",pkeybuf[-1],argc,argv[0],argv[1]);
}break;
case 'M': if (argc==0 && input+3<=e) {
// heha::print("Mausereignis %02X",input[0]); nextline();
Mouse m={
byte(input[1] - '!' - x), // hier: nullbasiert
byte(input[2] - '!' - y),
// Bit 4 = Strg-Taste gedrückt → Bit 5
// Bit 3 = Alt-Taste gedrückt → Bit 4
// Bit 2 = Shift-Taste gedrückt → Bit 3
// Tasten Ctrl+Alt+Shift übernehmen
// Alte Maustastenzustände übernehmen → Bit 2:0
// Mehrfachklick-Zähler übernehmen → Bit 7:6; 0b01 = Doppelklick, 0b10 = Dreifach usw.
mouse.buttons&0xC7,
byte(input[0])}; // Aktionskode, v.a. Bit 7:5:
// 001 = ButtonDown, ButtonUp
// 010 = Move / Drag
// 011 = Mausrad
// 100 = Mehrfachklick (nur "down")
m.buttons.orModKeys(input[0]>>2);
input+=3;
switch (m.code&0b1110'0011) {
case 0x20: m.buttons.L=1; checkDblClk(m); break; //lButtonDown
case 0x21: m.buttons.M=1; checkDblClk(m); break;
case 0x22: m.buttons.R=1; checkDblClk(m); break; //rbuttondown
case 0x23: m.buttons&=~7; break; //buttonup (welcher ist unklar, hier: alle)
case 0x40: m.buttons.L=1; break;
case 0x41: m.buttons.M=1; break;
case 0x42: m.buttons.R=1; break;
case 0x43: m.buttons&=~7; break;
case 0x60: break; // Mausrad-Drehung
case 0x61: break;
default: uneaten=true;
// Alle anderen Kodes verraten nicht den Zustand von Maustasten.
// Insbesondere ist es nicht möglich, zwischen den (unter Linux gängigen)
// Klickgesten L▼ ~ R▼ ~ L▲ ~ R▲ und L▼ ~ R▼ ~ R▲ ~ L▲ zu unterscheiden.
}
onMouse(m);
}break;
default: uneaten=true;
}
}
if (++loopcount>5) {uneaten=true; break;}
}
if (uneaten) {
static unsigned dumpcount;
dump(input,e-input,++dumpcount);
}
return true;
}
static Window::LPARAM onChildInput(Window::HWND,Window::UINT msg,Window::WPARAM wParam,Window::LPARAM lParam) {
switch (msg) { // Unterschied zu Win32: Kein WM_COMMAND
case MiniEdit::EN_UP:
case MiniEdit::EN_DOWN:
case MiniEdit::EN_ENTER: { // Beim Drücken auf ENTER
MiniEdit*sender=(MiniEdit*)lParam;
if (sender==Auftrag::edit) return Auftrag::onEnter(msg);
int v = atoi(sender->text); // Unsauber!! Kommaangabe bei Position, NaN
if (msg!=MiniEdit::EN_ENTER) {
v+= msg==MiniEdit::EN_UP ? 1 : -1;
heha::snprint(sender->text,31,"%j",v); // Unsauber bei Kommaangabe
sender->onNewText();
}
byte l=wParam>>16; // Unterschied zu Win32: ID = 32 Bit; hier: High-Teil = Anzahl Bytes
byte buf[2+l]={byte(wParam>>8),byte(wParam)};
memcpy(buf+2,&v,l);
i2c.send(buf,sizeof buf,"onChildInput");
}break;
}
return 0;
}
/**************
* Alter Mist *
**************/
// unused:
static void readaction(unsigned a, unsigned l) {
char cmd[]={char(a>>8),char(a)};
char*buf=new char[l];
if (i2c.queryLong(cmd,sizeof cmd,buf,l,__FUNCTION__)) {
// usleep(100); // Dem ATmega etwas Zeit zum Verdauen geben
if (a==0x6000 && l>6) l=6;
dump(buf,l,a);
if ((a>>8 & 0xE0)==0x60) switch (a>>8 & 0x0F) {
case 0: if (!(char)a && l>=6) {
byte s100 = buf[0];
long long s = 0;
memcpy(&s,buf+1,5);
std::time_t t = s;
heha::print("%lld.%02u %s",s,s100,std::ctime(&t));
}break;
case 1:
case 2:
case 3: if ((byte)a+l<=13) for (int i=0; i<l;++i) {
heha::print("%4d%c",(signed char)buf[i],i+1==l?'\n':' ');
}break;
case 4: if (!((a|l)&1) && (byte)a+l<=32) for (int i=0; i<l;i+=2) {
short z=*(short*)(buf+i); // Vektor aus int16 (avr-gcc: int)
if ((byte)a+i<14) heha::print("%,2hj",z);
else heha::print("%d",z);
heha::print("%c",i+2>=l?'\n':' ');
}break;
case 5:
case 6: if (!(byte)a && l==12) for (int i=0; i<3; i++) {
heha::print("%7f%c",*(float*)(buf+4*i),i+1==3?'\n':'\t');
}break;
}
}
delete[]buf;
}
// ASCII-Zeichen in äquivalenten Ziffernwert umwandeln
// Returnwert >= Zahlenbasis bedeutet: Zeichen ist als Ziffer ungültig
// Böse Falle arm-gcc: char ist nicht signed!
static byte digit(signed char c) {
if (c<='9') return c-'0'; // auch alles negative bleibt ungültig
c|=0x20; // in Kleinbuchstabe umwandeln
if (c<'a') return c; // ungültig als Ziffer (0x3A..0x3F,0x60)
return c-'a'+10; // '[' => '{' => 36 = ungültig
}
/*******************
* Hier geht's los *
*******************/
int main(int argc,char**argv) {
#ifdef DEBUGPIN
gpio::fsel(DEBUGPIN,1); // DEBUG: Pin zum Ausgang machen
gpio::clr(DEBUGPIN); // mit L anfangen (Achtung! GPIO4 aktiviert Notaus!)
#endif
unsigned a=0x6000,l=8; // Adresse und Länge
char*pw=0;
if (argc>=2) {
if (!strcmp(argv[1],"settime")) {
// Argument der Form "settime"
L0 time(std::chrono::system_clock::now());
pw=time; l=sizeof time;
}else if (pw=strchr(argv[1],'W')) {
// Argument der Form "0x2000W123456789ABCDEF0" (Hex-Bytes)
sscanf(argv[1],"%i",&a);
char buf[64],i;
for (i=0; i<sizeof buf;) {
byte j=digit(*++pw);
if (j>=16) break;
byte k=digit(*++pw);
if (k<16) j = j<<4|k;
buf[i++]=j;
if (k>=16) break;
}
pw=buf; l=i;
}else if (pw=strchr(argv[1],'F')) {
// Argument der Form "0x5000F1.23,456.7,8E-9"
sscanf(argv[1],"%i",&a);
char buf[64],i;
for (i=0; i<sizeof buf; i+=sizeof(float)) {
float f;
int k,j=sscanf(++pw,"%f%n",&f,&k);
if (!j) break;
pw+=k; // auf das Komma oder sonstiges Trennzeichen
*(float*)(buf+i)=f;
}
pw=buf; l=i;
}else sscanf(argv[1],"%iL%i",&a,&l);
// Argument der Form "0x8000L16" = Lesemodus (ohne "L" 16 Bytes)
}
unsigned i2cnr = 1;
if (argc>=3) sscanf(argv[2],"%i",&i2cnr);
unsigned i2cslave = 0x5d;
if (argc>=4) sscanf(argv[3],"%i",&i2cslave);
if (!i2c.open(i2cnr,i2cslave)) return 1;
if (pw) writeaction(a,pw,l);
else if (l1.query()) { // eedata (128 Byte) komplett lesen
screen.wndproc = onChildInput;
Regler::createWindows(); // Erst <screen> initialisieren lassen ...
Odo::createWindows(); // ... dann Fenster erzeugen
Auftrag::createWindow();
// heha::print("%p\n",screen.sub);
// dump((char*)&l1,sizeof l1);
// line=0;
Terminal terminal;
terminal.start();
cursortop();
do{ // Erst Werte holen und Fehler ausspucken lassen
if (screen.paintAll) {
csi(19,screen.h,'r'); // Scrollbereich für Fehlermeldungen unten
}
l0.query(); // Uhr
l1.queryDebug();
l2.query(); // PWM-Generator
l3.query(); // Controller
l4.query(); // A/D-Wandler und verschiedenes
Regler::query(); // l5 = Regler
if (l2.pwms[0] || l2.pwms[1]) odo.query(); // l6 = Odometrie (nur beim Fahren oder Lenken)
if (!screen.paintAll) csi('s');
cursortop(); // Dann Kursor bewegen
l4.show();
L2::showRuler();
l2.show(); // PWM-Generatorwerte, NaN = keine Pulserzeugung
l3.show(); // Werte von Fernbedienung, NaN = noch nie eingeschaltet
Regler::showSoll(); // NaN = Regler außer Betrieb (automatisch nach Timeout)
heha::print("%10s: ","Uhr");
csi(42,30,'m'); // "editierbares" Feld
heha::print(" ");
l0.show();
heha::print(" "); // Interessant: Scrollt nicht am rechten Rand!
csi('m'); // Feld beenden wenn Fenster größer ist als 80 Spalten
frameRechts();
nextline();
odo.show(); // Odometriedaten (TODO: Nur bei Änderung)
static bool regelDataShown;
char activeRegler = Regler::firstActive();
bool pushed_paintAll = screen.paintAll;
// Wenn sich Titel und Weißraum ändert
if (regelDataShown != (activeRegler>=0)) screen.paintAll = true;
if (activeRegler>=0) {
regler[activeRegler].showReglerData();
regelDataShown=true;
}else{
l1.show(); // Hexdump Debug-Bereich (TODO: Nur geänderte Bytes)
regelDataShown=false;
}
screen.paintAll = pushed_paintAll;
Auftrag::show();
if (screen.paintAll) emitHorizontalDivider("╚═╧═╝",R2C1,screen.w-R2C1-3);
nextline();
if (!screen.mousehelp) {
ChrColor statuscolor(0,7);
statuscolor.out(); // (CSI 7 m funktioniert nicht mit CSI K!)
csi('K');
heha::print(" ");
showMouseHelp();
screen.mousehelp=true;
}
nextline();
if (!screen.paintAll) csi('u');
screen.paintAll=false;
}while(inputhandler()); // blockiert 1/10 s
terminal.stop();
csi('s');
csi('r'); // Scrollbereich aufgeben
csi('u');
}
i2c.close();
}
Detected encoding: UTF-8 | 0
|