Source file: /~heha/mb-iwp/Bergwerk/fba-fw-230825.zip/fernbed.cpp

#include "fba.h"

/* PWM-Generierung (im wesentlichen in „isr.S“) sowie
Kommunikation mit der „Controller“ genannten Funkfernbedienung.
Diese ist bidirektional und kann Messwerte des Fahrzeugs anzeigen.
Kommunikation über UART mit 115200 Baud.
(Diese Baudrate erzwingt entweder einen Baudratenquarz
oder eine PLL mit 32-KHz-Uhrenquarz.)
*/

byte rxRingBuf[40] NOINIT;	// kann nicht ganz voll werden
byte rxWrIdx,rxRdIdx;	// Indizes für rxRingBuf
byte rxItIdx;		// Index der letzten Empfangslücke bzw. Paketende
static byte wrapIdx(byte i) {if (i>=sizeof rxRingBuf) i-=sizeof rxRingBuf; return i;}
static byte getRxByte(byte i){return rxRingBuf[wrapIdx(i+rxRdIdx)];}
static byte getRxFill()      {return wrapIdx(rxWrIdx+sizeof rxRingBuf-rxRdIdx);}
static unsigned getRxWord(byte i) {return getRxByte(i)|getRxByte(i+1)<<8;}

byte txBuf[8] NOINIT;
byte txIdx;	// Leseindex für txBuf

L2 l2;		// niemals cNaN! cNaN wird I²C bei pwmok≡0 vorgespielt.
L3 l3 NOINIT;	// Wird mit cNaN initialisiert
unsigned i1ts NOINIT;	// Timerwert bei INT1 zur genaueren Frequenzmessung

L3::L3() {memset(values,cNaN,sizeof values);}

byte L2::autopuls[6];
byte L3::fbaus;	// !=0: Fernbedienung unwirksam, zählt mit 150 Hz runter

// 16-Bit-Prüfsumme aus den Bytes eines Puffers,
// beginnend mit Gesamtlängenbyte, berechnen.
// Das Gesamtlängenbyte muss >=4 sein!
static unsigned checksum(const byte*buf) {
 byte len=buf[0]-2;
 unsigned ret=0;
 for (byte i=0; i<len; i++) ret+=buf[i];
 return ret;
}

static bool set_checksum(byte*buf) {
 byte l=buf[0];
 if (l<4 || l>8) return false;	// sicherheitshalber!! Bug in Programmlogik!
 auto&cs=*reinterpret_cast<unsigned*>(buf+l-2);
 cs=~checksum(buf);
 return true;
}

static bool check_checksum(byte l) {
 unsigned sum=l;
 for (byte i=1; i<l-2; i++) sum+=getRxByte(i);
 return ~sum==getRxWord(l-2);
}

void L3::onSensorQuery() {
// Bei Hochlauf gibt es noch den Query 04-F0 (Antwort unbekannt)
// Nur 3 Zeilen passen auf die Anzeige, so muss man vertikal scrollen
// Blöd dass Anzeige-Einheiten und Kommastellen nicht konfigurierbar sind.
// Eine Null bei Bordspannung wird ignoriert, sonst Unterspannungspieps.
 byte b1=getRxByte(1);
 byte k=(b1&0x0F)-1;	// Sensor-Kanal (Lo-Nibble), nullbasiert
 if (k<6) {		// 6 Sensoren = 6 (scrollbare) Zeilen auf dem Display
  txBuf[0] = 6;		// Standard-Antwort: 6 Bytes
  txBuf[1] = b1;	// Standard-Antwort: Echo
  switch (b1&0xF0) {	// Hi-Nibble = Aktion
   case 0x80: {		// “discover sensor”
    txBuf[0] = 4;	// Kurze Antwort: Sensor vorhanden
   }break;
   case 0x90: {		// “get sensor type”
/*https://github.com/adis1313/iBUSTelemetry-Arduino/blob/master/iBUSTelemetry.cpp
Die Kanalzuordnung auf dem Display ist rätselhaft!!
Außer man belässt alles bei „Spannung“, dann bleibt die Reihenfolge bestehen.
x-mal probiert: Mit kanalweisen Typzuweisungen kommt nur Müll 'raus!
Scheint ein Firmware-Bug des Controllers zu sein.
Daher wird hier nur „Spannung“ ausgegeben.
0	Interne Spannung			Ext.V| | -0.01V
1	(Temperatur/°C - 40) * 10		Temp.| | 15.0C
2	Drehzahl/(U/min) * 100, auch negativ	Mot. | |-856RPM
3 	Externe Spannung/V * 100		Ext.V| |-20.00V
4 // Avg cell voltage * 100/V		wird nicht angezeigt
5 // Battery current * 100/A			"
6 // Fuel / Remaining battery percentage	"
7 // RPM / Throttle value / battery capacity	"
8 // CMP-Heading
9 // Climb rate * 100/m? Fuß?
10 // Course over ground
11 // GPS status (2 values)
12 // Acc X * 100/m/s²
13 // Acc Y * 100/m/s²
14 // Acc Z * 100/m/s²
15 // Roll * 100/°/s				"
16 // Pitch * 100/°/s
17 // Yaw * 100/°/s
18 // Vertical speed * 100/m?/s
19 // Ground Speed * 100/m/s
20 // GPS Distance from home /m?
21 // Armed / unarmed
22 // Flight mode				"
0x41 // Pressure * 100/bar?
0x7e // Speed *10/km/h
// 4 byte sensors
GPS_LAT 0x80 // WGS84 * 1E7/°
GPS_LON 0x81 // WGS84 in degrees * 1E7/°
GPS_ALT 0x82 // GPS altitude * 100/m?
0x83 // Altitude * 100/m?
0x84 // Max altitude * 100/m?
*/
    txBuf[2]=0;		// 0 = Spannung
    txBuf[3]=2;		// muss 2 sein (Bytes Messwert?)
   }break;
   case 0xA0: {		// “query value”
    int v = 0;
    if (!k) v = l4.lin[4];	// Auf Kanal 0 immer Bordspannung
    else{
     --k;		// wiederum nullbasiert
     char sw = l3.values[9];	// Kanal 10 = Schalter SWB
     if (sw<-16) {	// SWB auf Normalstellung (oben) oder cNaN
      v = (k==4) ? (l4.kmz*168>>8)*eedata.fz.onestep>>16	// in cm, mit 2 Kommastellen in Meter
	// eigentlich kmz*onestep/100000, 168 ≈ 2**24/100000, Division vermeiden
      : l4.lin[k];	// 0: Lenkpoti in °
			// 1: Geschwindigkeit in cm/s
			// 2: Hubarm/Mulde in °
			// 3: Löffel/Verwindung in °
     }else if (sw>16) {	// SWB zum Bediener: Raw-Werte ADC
      v = l4.ad[k]; 	// 0: Lenkpoti
			// 1: Hubarm/Mulde
			// 2: Löffel/Verwindung
			// 3: Bordspannung
			// 4: Temperatur
     }else switch (k) {	// SWB in Mittelstellung: Sonstige Werte
      case 0: v = l4.lin[5]; break;	// Temperatur (°C)
      case 1: v = l4.lin[6]; break;	// Ultraschall-Entfernungsmesser
      case 2: v = *(int*)&l4.flags; break;	// flags + lamps
      case 3: v = l4.kmz; break;	// für Kreisfahrt (lo+hi getrennt)
      default: v = l4.kmz>>16; break;	// und Ermittlung von eedata.fz.q[]
     }
    }
    *(int*)(txBuf+2) = v;
   }break;
   default: return;
  }
  set_checksum(txBuf);
  UCSR0B= 0b00101000;
// Empfänger abschalten, Sender einschalten und ersten Interrupt auslösen lassen
 }
// An der Fernbedienung muss man für Kanal 1 (Batterieanzeige)
// Setup aktivieren und dann auf die Batterien (rechts oben) touchen.
// Danach ist für externe Batterie ein Häkchen (kein Kreuz) zu setzen.
// Dort kann man auch die Alarmgrenzen einstellen.
}

enum{
 k7hold_trigger=70,	// reichlich 1 s
};
byte L2::k7hold;	// Zeitmesser für PWM!=0 an Kanal 7 (Lichtsteuerung)
void L2::k7hold_handler() {
 byte h = k7hold;
 char c = l2.pwms[6];
 if (c) {		// aktiv?
  if (!++h) return;	// Auf 255 begrenzen
  if (h==k7hold_trigger) {	// 1 Sekunde vergangen?
   if (c<0) {
    byte lamps = l4.lamps+8;
    if ((lamps&0x18)==0x18) lamps&=0xE7;	// zählt 0-1-2
    eedata.lamps = l4.lamps = lamps;
   }else l4.flags^=0x10;	// Nur Sound Ein oder Sound Aus (wozu auch immer getrennt; nicht im EEPROM)
  }
 }else h=0;
 k7hold = h;
}

// TODO: Setzfunktion und Completion-Funktion auftrennen!
void L2::set(byte i, char c) {
 if (c==cNaN) return;	// Nicht setzen sondern (diesen Kanal) übergehen
 byte k = i&0x0F;
 if (k>=9) return;	// Kanalnummer falsch
 char b = pwms[k];	// Vorheriger Wert
 if (i&fAdd) c = char(limit<int>(b+c,125));
 byte slow = i>>bSlow;
 if (slow) {
  char Δmax = 5 << slow-1;
// 0b01 =>  5: Von 0 bis 125 in 25 Stufen = 500 ms
// 0b10 => 10: Von 0 bis 125 in 13 Stufen = 260 ms
// 0b11 => 20: Von 0 bis 125 in  7 Stufen = 140 ms
  c = limit<int>(c-b,Δmax)+b;	// Wichtig: Über <int> gehen, Überläufe drohen
 }
 if (!(i&fRegler) && k<4) {
  regler[k].reset(0xFF);
  L7::purge(k);	// Controllereingriff oder I²C-Direktsteuerung kanalspezifisch
 }
// TODO: Keine Mehrfachabfrage des (mglw. prellenden) PIND.
 if (!notaus::released()) return;
 if (k==4) return;	// Kanal 5 (Hydraulikpumpe) nicht setzbar
// Bei Mittelstellung von SWB, ohne Offset, und ausgeschalteter Zündung
 bool directpwm = !l3.values[9] && !(l4.flags&1<<3);
 if (directpwm) {
  if (k==1) k=8;	// Nicht den Motor sondern Soundgenerator (und damit Setup-Funktion) steuern
 }else{
  if (k==8) return;	// Kanal 9 (Geräuschgenerator) nicht setzbar
// Hintertür: Setup des Geräuschgenerators statt Licht
// Die Unterdrückung der Sprachausgabe erfordert,
// dass der Ein/Aus-Zustand des Motorgeräuschgenerators bekannt ist.
// DAS IST NICHT SICHER BEKANNT!
// Dazu werden hier alle analogen Geber digitalisiert, die nur digital genutzt werden.
  if (k>4) {
   if (c<=-25) c=-125;
   else if (c>=25) c=125;
   else c=0;		// ternär machen
  }
 }
 pwmok = 10+60*3;	// Laufzeit bis zum Toten Mann (3 Minuten)
 if (b==c) return;	// Raus wenn nichts zu tun ist
 if (!directpwm) switch (k) {
// Über Kanal 6 (Index 5) wird der Status der Zündung mitgeschnitten.
// Dazu muss l4.flags bei PowerOn/BrownOut mit 0 initialisiert sein.
  case 5: if (c<0 && b>=0) {	// Zündung ein/aus verfolgen: Kurzer Puls schaltet
   l4.flags^=1<<3;	// Bit 3 kippen
  }break;
// Über Kanal 7 (Index 6) der Zustand der vom Magic-Controller gesteuerten Lampen
// Dazu muss l4.lamps bei PowerOn/BrownOut vom EEPROM initialisiert sein.
// Der Fall b≡c ist bereits ausgefiltert, b und c sind ternär
  case 6: if (b) {	// Lampen-Schaltzustand verfolgen
   if (k7hold<k7hold_trigger) {	// Konstanter Pegel ±125 „kurze“ Zeit?
    byte lamps = l4.lamps;
    if (b<0) {
     lamps++;
     if ((lamps&7)>=6) lamps&=0xF8;	// 6 Schaltzustände 0..5
    }else{
     lamps+=0x20;
     if (lamps>=5<<5) lamps&=0x1F;	// 5 Schaltzustände 0..4
    }
    eedata.lamps = l4.lamps = lamps;
   }
   k7hold = 0;		// Zeitmessung beenden (c≡0) oder neu starten (c≡-b)
  }break;
// Über Kanal 8 (Index 7) der Zustand der Rundumleuchte.
// Dazu müssen die oberen 3 Bits von l4.flags bei PowerOn/BrownOut mit 001
// initialisiert sein.
  case 7: if (b<=0) {	// Zustand der Rundumleuchte verfolgen
   byte lamp = l4.flags + 0x20;
   if (lamp>=5<<5) lamp&=0x1F;		// 5 Schaltzustände 0..4
   l4.flags = lamp;	// sonstige Flags unverändert
  }break;
 }//switch
// Der Magic Controller scheint sich 2 aufeinanderfolgende Flanken zu merken,
// auch wenn diese zügig während des „Herunterfahrens“ gegeben werden.
 pwms[k]=c;	// hier auch ±127 zulassen, nicht schlimm
 if ((k<4 || k==5) && !directpwm) {
// Hydraulikpumpenberechnung gesondert: Hier als Summe: Sinnvoll weil
// bei Parallelarbeit mehrerer Verbraucher die Pumpe mehr leisten muss.
// 230825: Für geringe Lenkgeschwindigkeiten trotzdem Pumpe schneller laufen lassen
// Das dürfte die Lenkeinsatzschwellen deutlich verändern (= Einfluss auf Regelung)
// 230825: Zurück, das ist Mist.
  unsigned hy=cabs(pwms[0])+cabs(pwms[2])+cabs(pwms[3])<<0;
  pwms[4] = hy>125 ? 125 : (char)hy;	// Summe begrenzen und Hydraulikpumpe (nur positiv) setzen
// Motorgeräusch-Berechnung: Beide Motoren fließen ein!  
// Sonst klingt die Hydraulik „elektrisch“.
  if (l4.flags&1<<3) {	// Zündung ein?
   int v = pwms[1];
   if (v<0) v-=hy; else v+=hy;	// Hydraulik in der gleichen Richtung wie Fahren dem Sound hinzufügen
   pwms[8] = limit(v,-125,+125);// Kanal 9 setzen
  }else pwms[8]=0;		// Nicht ins 汉语 Setup purzeln!
 }
}

// Aufruf nach erledigter PWM-Ausgabe, alle 20 ms, generiert 8 gleiche Pulse
// Der Rundumleuchte genügen Einzelpulse, der Magic Controller will mehr
void L2::autopuls_handler() {
// Indizes 1..4 dürfen nicht gleichzeitig abgearbeitet werden,
// da sie gemeinsam auf PWM-Kanal 7 (Index 6) wirken!
 bool K7busy = false;
 for (byte i=0; i<elemof(autopuls); i++) {
  static byte longwait;
  byte p=autopuls[i];
  if (!p) continue;	// nichts zu tun
  if (1<=i && i<5) {
   if (K7busy) continue;// Diesen Autopuls-Generator aufschieben
   K7busy=true;
   if (longwait && --longwait) continue;	// Aktion verzögern
  }
  autopuls[i]=--p;	// Nächster Schritt
  if ((p&7)==7) switch (i) {	// 8 Telegramme gleich (für Zündung >4 erforderlich) : 160 ms (TODO: Stimmt nicht!!)
   case 0: l2.set(5,p&8?-125:0); break;
   case 3: longwait=70; nobreak;
   case 1: l2.set(6,p&8?-125:0); break;
   case 4: longwait=70; nobreak;
   case 2: l2.set(6,p&8?+125:0); break;
   case 5: l2.set(7,p&8?+125:-125); break;
  }
 }
}

void L2::countdown() {
// Vor dem „kontrollierten Ableben“ des PWM-Generators
// 5× 1,5-ms-Impluse (Nullstellung) ausgeben
 byte ok=l2.pwmok;
 if (ok>6 && !notaus::released()) ok=6;	// als nächstes memset()
 if (ok) {
  if (ok>=10) {
   static byte pwmokl;	// Sub-Countdown solange pwmok>=10
   byte l=pwmokl;
   if (!l) l=50;
   if (!--l) --ok;	// Langsamer Countdown mit Periode 50
   pwmokl=l;
  }else if (--ok==5)	// Schneller Countdown; bei Zählerstand 5 …
    memset(l2.pwms,0,sizeof l2.pwms);	// 1,5-ms-Pulse ausgeben lassen
  l2.pwmok=ok;
 }
}


/* Aufbau IBUS-Paket „Servo“
0  1  2   4   6   8   10  12  14  16  18  20   22    24    26    28    30     32
20 40 {K1}{K2}{K3}{K4}{K5}{K6}{K7}{K8}{K9}{K10}{1500}{1500}{1500}{1500}{cksum}
Damit beim Einschalten/Reset des ATmega328 nicht mitten in einem
laufenden Telegramm mit dem Empfang bekonnen wird,
legt die Empfangs-ISR erst nach einem Timeout los,
und bedient sich dabei Timer2, OCR2A und GPIOR0.0.
Das vermeidet verwirrende Fehlermeldungen.
(eedata.debug sollte möglichst komplett Null bleiben.)
*/
void L3::checkIbusRx() {
 byte f=getRxFill();	// liefert 0..39, nie 40
 if (!f) return;
 byte b0 = getRxByte(0), lücke = rxItIdx;
 if (b0<4 || b0>32) {	// Falsche Länge?
  eedata.debug[7]++;	// Falsches Längenbyte
  cli();
  rxRdIdx = rxWrIdx = rxItIdx = 0;	// Puffer leeren
  GPIOR0&=~(1<<0);	// erneut auf Lücke warten
  sei();
  return;		// Beim nächsten Poll mit neuem rxRdIdx
 }
 if (f<b0) return;	// Daten nicht ausreichend
// Lücke am Anfang? An das Paketende setzen, um Test unten passieren zu lassen
 if (lücke==rxRdIdx) rxItIdx = lücke = wrapIdx(lücke+b0);
// IBUS-Paket „Sensor“ oder „Servo“ vollständig angekommen
// Der Ringpuffer wird/wurde von der ISR weiter gefüllt …
 if (check_checksum(b0)) switch (getRxByte(1)) {
  case 0x40: if (b0>=24) {	// Mindestens 10 Kanal-Worte, dazu Header und Prüfsumme
   for (byte i=0; i<10; i++) {
    int µs = getRxWord(1+i<<1);	// Werte zwischen 1000 (-125) und 2000 (+125)
    char c = limit(µs-1500,-500,500) >> 2;	// ±125 bietet glatte Umrechnung
// Es kann sein, dass nach RESET (values[i]==cNaN) der Fernbedienungsempfänger
// noch munter seine letzten Daten sendet, und man beim Abschalten der
// Controllerbatterie einen Hebel betätigt hatte bzw. von Null verschiedene Daten kommen.
// Daher für diesen Fall noch kein l2.set aufrufen. (230406)
    char b = values[i];
    if (b==c) continue;		// Ohne kanalweise Änderung nichts tun
    values[i] = c;
    if (i==4 && !c && b)	// Fernbedienung geht aus?
      l2.pwmok = 10+3;		// In 3 s PWM abschalten (I²C-Slave kann eingreifen)
    if (!fbaus && b!=cNaN)	// Änderung?
      l2.set(i,c);		// setzen; Regler werden ggf. dort abgeschaltet
   }
// Bei ausgefallenem Controller (Batterie 'raus) sendet der Empfänger
// munter weiter, immer die _gleichen_ Reports
// 230905: Falsch! Das kann man einstellen, unter Failsafe.
// Dort habe ich für Kanal 5 (Taste links) das Senden von 0% festgelegt.
// (Das geht via temporären Mixbetrieb auf Kanal 5.)
// Beim Abschalten des Senders kommt so die 0 als Kennung für „aus“.
// Sonst -125 für nicht gedrückt und +125 für gedrückt.
   byte aus = fbaus;
   if (!values[4]) aus=0;
   else if (aus) --aus;	// mit ca. 150 Hz
   fbaus = aus;
  }break;
  default: onSensorQuery();
 }else{
  eedata.debug[4]++;
 }
 rxRdIdx=wrapIdx(rxRdIdx+b0);	// Lesezeiger vorrücken = Lesepuffer „leeren“
 if (lücke!=rxRdIdx) {	// Nicht am Paketende?
  eedata.debug[5]++;	// melden
 }
}
Detected encoding: UTF-80