PchButik.se

kWh i ackumulatortankar

Experiment med HC-12 transceivers



Displayen visar temeperatur pÄ 5 punkter pÄ ackumulatortanken.
Med temperartur och berÀknad kWh.
Shunten visar raw vÀrden direkt frÄn potentiometern.
NÀr jag vet grÀnsvÀrden, kommer den att göras om till procent.
Status frÄn elpatronen visar vitt vid av, och rött vid pÄ.
Total visar hur mÄnga kWh som finns i tanken och procent laddat.


Motor för shunten


Se uppdatering Potentiometer pÄ shunt


En universal shield till Uno har XH2.54 kontakter


Och nÄgra pull-up motstÄnd Àr kopplade till XH2.54 kontakterna pÄ10K


Uno kort med shielden monterat i en lÄda med en step-down converter till 9 Volt
SÀndaren HC-12 Àr monterad stÄende pÄ shielden och en extern antenn pÄ lÄdan.


Den externa antennen med smÀlt lim som stöd.


En av de 5 temperatursensorerna som Àr NTC10K och ett 10K motstÄnd pÄ shielden.


PÄ elpatronen Àr en ljussensensor monterad med skydd för solljus.


Mottagaren Àr en Mega2560 med shield och en display och en HC-12 pÄ toppen.


Systemet Àr inte kalibrerat men ger en indikation pÄ temperatur och kWh.

Koden för sÀndaren vid ackumulator tankarna.
Det Àr en enkel kod som lÀser av data och skickar data via RF.

PlaytformIO, sÀndaren. fil: 'main.cpp'

#include <Arduino.h>
#include <SoftwareSerial.h>
#include <math.h>

/* ---------- FUNCTION PROTOTYPES ---------- */
uint8_t calcCRC(const char *data);
void hc12Command(const char* cmd);
void initHC12();
float readNTC(int pin);
void sendPacket();

/* ---------- PIN ---------- 
* HC12 RX, TX och SET anvÀnds för att kommunicera med HC12 modulen.
* RX och TX anvÀnds för att skicka data till HC12 modulen.
* SET anvÀnds för att sÀtta HC12 modulen i sÀndningslÀge.
* Den ska vara HIGH nÀr man skickar data och LOW nÀr man inte skickar data Àr dÄ i AT-command lÀge.
*/

#define HC12_RX 9
#define HC12_TX 10
#define HC12_SET 4

/* ---------- TEMP ----------
* Vita XH-2.54-2P-kontakter anvÀnds för att ansluta temperaturgivarna.
* Före en NTC 10 kOhm 3950 ska pull-up-motstÄndet normalt vara: 10 kOhm
* 5 temperature sensors Àr NTC 10 kOhm 3950 med pull-up motstÄnd pÄ 10 kOhm frÄn 5 Volt.
* De Àr placerade pÄ olika stÀllen av akumulatortanken.
* Akumulatortanken Àr pÄ 750 liter, sÄ 750 / 5 = 150 liter per sensor.
* Är det vintertid och bĂ„da tankarna Ă€r paralellt ihop kopplade sĂ„ Ă€r 1500 / 5 = 300 liter per sensor.
* Om 80 och efter 35 grader gĂ„r det inte att ta ut mera energi sĂ„ Ă€r det 80 - 35 =  45 grader c
* DÄ Àr det 7.9 kW per sensor vid 150 liter och 15.8 kW per sensor vid 300 liter.
* Eller 7.9 / 45 = 0.175 kW per grad vid 150 liter och 15.8 / 45 = 0.35 kW per grad vid 300 liter.
*/
#define TEMP1 A0
#define TEMP2 A1
#define TEMP3 A2
#define TEMP4 A3
#define TEMP5 A4

#define D1_PIN 5 //BlÄ XH2.54-2P kontakt anvÀnds ej
#define D2_PIN 6 //BlÄ XH2.54-2P kontakt anvÀnds ej
#define D3_PIN 7 //Vit XH2.54-3P kontakt Elpatron pÄ eller av. Ljussensor pÄ elpatronens lampa

/* ---------- ANALOG RANGE ---------- 
* Vit XH2.54-4P kontakt anvÀnds för att lÀsa av shuntens position, en pinne anÀnds inte.
* Denna Àr en extra 10 Kohm potentiometer som anvÀnds för att lÀsa av shuntens position.
* Rörelse 90 grader av potentiometern motsvarar 0-100% öppning av shunten.
* Det Àr inte en exakt avlÀsning, utan mer en uppskattning av hur mycket shunten Àr öppen.
*/
#define ANA1 A5
#define A1_MIN 300
#define A1_MAX 700

/* ---------- NTC CONST ---------- */

#define SERIES_RESISTOR 10000
#define NOMINAL_RESISTANCE 10000
#define NOMINAL_TEMPERATURE 25
#define B_COEFFICIENT 3950

/* ---------- SERIAL ---------- */

SoftwareSerial HC12(HC12_RX, HC12_TX);

/* ---------- TIMER ---------- */
unsigned long lastSend=0;
const unsigned long sendInterval=60000; // 60 sekunder mellan varje sÀndning

/* ---------- D3 Àndras ---------- */

int lastD3State = HIGH;
unsigned long lastChangeTime = 0;
#define CHANGE_DEBOUNCE 50 // Skydd mot kontaktstuds och brus pÄ D3, i millisekunder


/**
 * LÀs temperatur frÄn en NTC-termistor kopplad till angiven analog pinne.
 *
 * - LĂ€ser ADC med analogRead(pin) (10-bit: 0..1023).
 * - Skyddar mot kantvÀrden 0 och 1023 (delning med noll) och returnerar NAN i sÄdana fall.
 * - BerÀknar termistorns resistans i ett spÀnningsdelararrangemang med SERIES_RESISTOR.
 *   Formeln: R_ntc = SERIES_RESISTOR * adc / (1023.0 - adc)
 * - AnvĂ€nder B-parameter-modellen (förenklad Steinhart–Hart) för att rĂ€kna ut temperatur i °C.
 *
 * @param pin Analog ingÄngspin för NTC (A0..An).
 * @return Temperatur i °C (float). Returnerar NAN vid ogiltigt ADC-vÀrde.
 */
float readNTC(int pin) {
  int adc = analogRead(pin);

  // Skydda mot delning med noll eller extremvĂ€rden som ger ogiltig berĂ€kning
  if (adc <= 0 || adc >= 1023) {
    return NAN;
  }

  // R_ntc = R_series * adc / (1023 - adc)
  float resistance = SERIES_RESISTOR * (static_cast<float>(adc) / (1023.0f - static_cast<float>(adc)));

  // Steinhart–Hart (B-parameter) berĂ€kning
  float steinhart = resistance / NOMINAL_RESISTANCE;    // (R/Ro)
  steinhart = log(steinhart);                          // ln(R/Ro)
  steinhart /= B_COEFFICIENT;                          // 1/B * ln(R/Ro)
  steinhart += 1.0f / (NOMINAL_TEMPERATURE + 273.15f);// + 1/To
  steinhart = 1.0f / steinhart;                        // K
  steinhart -= 273.15f;                                // till °C

  return steinhart;
}

/**
 * sendPacket
 *
 * Samlar in sensor- och ingÄngsvÀrden, bygger en textbaserad telemetripayload,
 * berÀknar en 8-bitars CRC och skickar hela paketet över HC-12-modulen.
 * Skriver Àven ut det skickade paketet till Serial-konsolen.
 *
 * Funktionalitet:
 * - LĂ€ser fem temperaturer via readNTC(TEMP1..TEMP5) och konverterar varje
 *   float till kort ASCII med dtostrf().
 * - LÀser en analog vÀrde av shunt potentiometer som raw.
 * - LÀser tre digitala ingÄngar med digitalRead(D1_PIN..D3_PIN).
 * - Formaterar en payload i buffern (payload, storlek 140):
 *     "T1=<t1>;T2=<t2>;T3=<t3>;T4=<t4>;T5=<t5>;A1=<percent>;D1=<d1>;D2=<d2>;D3=<d3>"
 * - BerÀknar 8-bit CRC över payload med calcCRC(payload).
 * - Bygger ett paket i buffern (packet, storlek 160) som omsluter payload och CRC:
 *     "<<payload>;CRC=<XX>>"
 *   dĂ€r <XX> Ă€r CRC i tvĂ„ stora hex-siffror.
 * - SÀtter HC12_SET HIGH, vÀntar 10 ms, skickar paket med HC12.println(packet),
 *   vĂ€ntar 10 ms och sĂ€tter sedan HC12_SET LOW.
 * - Skriver det skickade paketet till Serial för debug.
 *
 * Noteringar och sidoeffekter:
 * - Funktionen blockerar (anvÀnder delay()).
 * - Utför I/O: digitalWrite(HC12_SET), HC12.println(...), Serial.println(...).
 * - AnvÀnder globala konstanter/objekt: TEMP1..TEMP5, ANA1, D1_PIN..D3_PIN, HC12_SET,
 *   HC12 och Serial.
 * - Förlitar sig pÄ hjÀlpfunktioner: readNTC(), calcCRC(), dtostrf(), snprintf().
 * - Buffertstorlekar Àr begrÀnsade (payload: 140 byte, packet: 160 byte). snprintf
 *   trunkerar om strĂ€ngen blir för lĂ„ng — sĂ€kerstĂ€ll att format och strĂ€nglĂ€ngder
 *   hĂ„ller sig inom grĂ€nserna för att undvika informationsförlust.
 *
 * ÅteranvĂ€ndbarhet / trĂ„dsĂ€kerhet:
 * - Ej reentrent: anvÀnder delade hÄrdvaruresurser (HC12, Serial) och globala pinnar.
 *
 * Retur:
 * - void. Transmitterar data som sidoeffekt; ingen felstatus returneras.
 */
void sendPacket(){

  float t1 = readNTC(TEMP1);
  float t2 = readNTC(TEMP2);
  float t3 = readNTC(TEMP3);
  float t4 = readNTC(TEMP4);
  float t5 = readNTC(TEMP5);

  int raw = analogRead(ANA1);

  int d1 = digitalRead(D1_PIN);
  int d2 = digitalRead(D2_PIN);
  int d3 = digitalRead(D3_PIN);

  char packet[160];
  char payload[140];

  char t1s[10], t2s[10], t3s[10], t4s[10], t5s[10];

  dtostrf(t1,0,1,t1s);
  dtostrf(t2,0,1,t2s);
  dtostrf(t3,0,1,t3s);
  dtostrf(t4,0,1,t4s);
  dtostrf(t5,0,1,t5s);

  snprintf(payload,sizeof(payload),
  "T1=%s;T2=%s;T3=%s;T4=%s;T5=%s;A1=%d;D1=%d;D2=%d;D3=%d",
  t1s,t2s,t3s,t4s,t5s,raw,d1,d2,d3);

  uint8_t crc = calcCRC(payload);

  snprintf(packet,sizeof(packet),
  "<%s;CRC=%02X>",payload,crc);

  digitalWrite(HC12_SET,HIGH);
  delay(10);

  HC12.println(packet);

  delay(10);
  digitalWrite(HC12_SET,LOW);

  Serial.println("Sent:");
  Serial.println(packet);
}

/**
 * Skicka en kommandostrÀng till HC-12 medan modulen sÀtts i kommandolÀge.
 *
 * - SÀtter HC12_SET lÄg för att gÄ in i AT-lÀge (HC12_SET mÄste vara definierad och konfigurerad som OUTPUT).
 * - VÀntar 50 ms sÄ att modulen hinner byta lÀge.
 * - Skriver den nullterminerade cmd-strÀngen till HC12 (anvÀnder den globala HC12-objektet).
 * - VÀntar 200 ms efter sÀndning sÄ att modulen hinner bearbeta kommandot.
 *
 * Viktigt:
 * - Funktionen blockerar (anvÀnder delay) och fÄr inte anvÀndas i avbrottshanterare eller andra tidskritiska sammanhang.
 * - HC12_SET lÀmnas LOW nÀr funktionen returnerar; anroparen ansvarar för att sÀtta den HIGH om sÄ önskas.
 * - Funktionen lÀgger inte automatiskt till radslut (CR/LF); inkludera dessa i cmd-strÀngen om modulen krÀver dem.
 * - Ingen felrapportering utförs.
 *
 * FörutsÀttningar:
 * - HC12 (SoftwareSerial) mÄste vara initierad.
 * - HC12_SET mÄste vara definierad och konfigurerad som OUTPUT.
 * - cmd mÄste vara en giltig, nullterminerad C-strÀng (nullptr ger undefined behaviour).
 *
 * Exempel:
 *   hc12Command("AT+VERSION\r\n");
 */
void hc12Command(const char* cmd){
  digitalWrite(HC12_SET,LOW);
  delay(50);
  HC12.print(cmd);
  //HC12.print("\r\n");
  delay(200);
}

/**
 * Initierar och konfigurerar HC-12 trÄdlös transceiver.
 *
 * Beskrivning:
 * - Skriver en initieringsrad till Serial för felsökning.
 * - SÀtter HC12 i AT-lÀge genom att styra HC12_SET-pinnen och vÀntar kort.
 * - Skickar ett antal AT-kommandon via hc12Command() för att stÀlla in kanal,
 *   uteffekt och driftlĂ€ge:
 *     - "AT"      : kontroll/test av anslutning
 *     - "AT+C003" : vĂ€lj kanal 3
 *     - "AT+P5"   : sĂ€tt uteffekt till nivĂ„ ~10 mW
 *     - "AT+FU3"  : vĂ€lj stabilt driftlĂ€ge
 * - LÀmnar slutligen modulen i normalt (mottagnings)lÀge och vÀntar kort.
 *
 * Biverkningar/krav:
 * - Funktionen blockerar körningen p.g.a. anvÀndning av delay().
 * - FörutsÀtter att HC12_SET, Serial och funktionen hc12Command() Àr korrekt
 *   definierade och initialiserade i omgivande kod.
 *
 * Parametrar: inga
 * ReturvÀrde: ingen (void)
 */
void initHC12(){
  Serial.println("Init HC12");

  // Se till att HC12 Ă€r i AT-lĂ€ge innan vi skickar kommandon
  digitalWrite(HC12_SET, LOW);
  delay(50);

  hc12Command("AT");
  hc12Command("AT+C003");   // kanal 3
  hc12Command("AT+P7");     // ~??mW
  hc12Command("AT+FU3");    // stabilt lĂ€ge
  hc12Command("AT+RX");    // get all

  // LĂ€mna HC12 i normalt (mottagnings)lĂ€ge efter init
  digitalWrite(HC12_SET, LOW);
  delay(50);
}

/**
 * BerÀkna enkel 8-bit XOR-checksumma över en nullterminerad C-strÀng.
 *
 * Itererar över varje byte i strÀngen (terminerande '\0' ingÄr inte) och
 * kombinerar dem med bitvis XOR. Om en nullpekare skickas tillbaka 0.
 *
 * @param data Pekare till nullterminerad C-strÀng.
 * @return 8-bit checksum som uint8_t.
 */
uint8_t calcCRC(const char *data){
  if(data == nullptr) return 0;
  uint8_t crc = 0;
  while(*data){
    crc ^= static_cast<uint8_t>(*data++);
  }
  return crc;
}

/**
 * @brief Initiera pins, seriella grÀnssnitt och HC-12.
 *
 * Konfigurerar HC12_SET som OUTPUT och sÀtter den LOW, aktiverar interna pull-ups
 * för D1/D2/D3, startar Serial och HC12-SoftwareSerial, vÀntar kort och kör initHC12().
 * LÀser Àven initialt D3-tillstÄnd för att undvika falsk Àndringsdetektion direkt efter boot.
 */
void setup() {
  pinMode(HC12_SET, OUTPUT);
  digitalWrite(HC12_SET, LOW);

  pinMode(D1_PIN, INPUT_PULLUP);
  pinMode(D2_PIN, INPUT_PULLUP);
  pinMode(D3_PIN, INPUT_PULLUP);

  Serial.begin(9600);
  HC12.begin(9600);

  delay(300);        // lĂ„t hĂ„rdvara och serial stabilisera
  initHC12();

  // LĂ€s initialt D3-tillstĂ„nd och tidsstĂ€mpel för debounce-logiken
  lastD3State = digitalRead(D3_PIN);
  lastChangeTime = millis();

  Serial.println("HC12 Ready");
}


/**
 * loop()
 *
 * Huvudloopen som gör tre saker varje varv:
 *
 * 1) Periodisk sÀndning
 *    - Kontrollerar om tiden sedan senaste sĂ€ndning (millis() - lastSend)
 *      Ă€r >= sendInterval. Om ja sĂ„ ökar lastSend med sendInterval och
 *      anropar sendPacket() för att skicka.
 *    - Att addera intervallet (lastSend += sendInterval) minskar kumulativt
 *      tidsdrift jĂ€mfört med att sĂ€tta lastSend = millis().
 *
 * 2) AvkÀnning av förÀndring pÄ D3 med debounce
 *    - LĂ€ser aktuell pinne och jĂ€mför mot lastD3State.
 *    - Om förĂ€ndring och mer Ă€n CHANGE_DEBOUNCE ms har passerat sĂ„ accepteras
 *      förĂ€ndringen: uppdatera lastChangeTime, lastD3State, skriv debugmeddelande
 *      och anropa sendPacket().
 *
 * 3) Debug/ vidarebefordran av inkommande HC12-data
 *    - Medan HC12.available() lĂ€ser varje byte och skriver den till Serial.
 *
 * Kommentarer:
 *    - TidsjĂ€mförelser anvĂ€nder millis()-differenser och Ă€r robusta mot rullning.
 *    - Debounce-tiden kan behöva finjusteras för den faktiska hĂ„rdvaran.
 */
void loop(){

  /* TIMER SÄNDNING */
  if(millis() - lastSend >= sendInterval){
    lastSend += sendInterval;
    sendPacket();
  }

  /* D3 CHANGE DETECT */
  int current = digitalRead(D3_PIN);

  if(current != lastD3State){

    if(millis() - lastChangeTime > CHANGE_DEBOUNCE){

      lastChangeTime = millis();
      lastD3State = current;

      Serial.println("D3 changed → sending packet");
      sendPacket();
    }
  }

  /* DEBUG RX */
  while(HC12.available())
    Serial.write(HC12.read());
}



Koden för mottagaren.
Det Àr en enkel kod som visar inkommande data pÄ en liten display.

PlaytformIO, mottagaren. fil: 'main.cpp'

#include <Arduino.h>
#include <U8g2lib.h>
#include <Arduino_GFX_Library.h>

/* 
* Koden Àr skapad av PchButik.se med hjÀlp av AI
* HC-12 VCC → 5V
* HC-12 GND → GND
* HC-12 TXD → Pin 19 (RX1)
* HC-12 RXD → Pin 18 (TX1)
* HC-12 SET → pin 4 (SET)
* Query-kommandon anvÀnder R-prefix (RC/RP/RB/RF)
* ?-kommandon stöds inte i denna firmwaregren
* AT+RX fungerar alltid och Àr sÀkraste statusdump 
*/

#define BLACK   0x0000
#define WHITE   0xFFFF
#define RED     0xF800
#define GREEN   0x07E0
#define BLUE    0x001F
#define YELLOW  0xFFE0
#define CYAN    0x07FF
#define MAGENTA 0xF81F
#define ORANGE  0xFD20

/* ---------- HC12 SERIAL ---------- */
#define HC12 Serial1

//AT-kommandon GET
const char* AT_GET_VERSION = "AT+V";
const char* AT_GET_BAUD = "AT+RB";
const char* AT_GET_CHANNEL = "AT+RC";
const char* AT_GET_POWER = "AT+RP";
const char* AT_GET_RADIO_MODE = "AT+RF";
const char* AT_GET_ALL = "AT+RX";

//Pinnar
#define HC12_SET 4

#define WINTER_MODE 1   // 0 = sommar (1 tank) | 1 = vinter (2 tankar)

#define LITER_PER_SENSOR 150.0
#define MIN_TEMP 35.0
#define MAX_TEMP 85.0
#define WATER_CP 4.186   // kJ/kg°C

/* More data bus class: https://github.com/moononournation/Arduino_GFX/wiki/Data-Bus-Class */
Arduino_DataBus *bus = new Arduino_HWSPI(8,10);
Arduino_GFX *gfx = new Arduino_ST7735(bus, 9, 3, false,128, 160, 2, 1, 2, 1,false); //art. 3273 // MÄste anpassas för din skÀrm.

/* ---------- DATA STORAGE ---------- */
float T[5]={0};
int shuntRaw=0;
int D1=0,D2=0,D3=0;

/* ----------Functions------ */

/* ---------- CRC ---------- */
/**
 * @brief BerÀkna en 8-bitars kontrollsumma genom XOR av varje byte i en nullterminerad strÀng.
 *
 * Itererar över bytes i den angivna null-terminerade C-strÀngen och
 * ackumulerar en enkel kontrollsumma genom bitvis XOR av varje byte
 * i en 8-bitars accumulator. Detta Àr en lÀttviktig checksumma (inte
 * en formell CRC) och ger grundlÀggande felupptÀckt.
 *
 * @param data Pekare till en nullterminerad C-strÀng. Bearbetningen
 *             stoppar vid första NUL ('\0'). För binĂ€ra buffertar som
 *             kan innehĂ„lla NUL, anvĂ€nd en variant som tar en lĂ€ngd.
 *
 * @return uint8_t Resultatet av 8-bitars XOR-checksumman. Tom strÀng ger 0.
 *
 * @note Varje char behandlas som en rÄ byte; tecknets signedness hanteras
 *       genom konvertering till uint8_t vid XOR. Komplextet Ă€r O(n).
 */
uint8_t calcCRC(const char *data){
  uint8_t crc=0;
  while(*data) crc ^= *data++;
  return crc;
}

/* ---------- PAKETPARSER ---------- */
/**
 * parsePacket
 *
 * Parsar en skrivbar, null-terminerad ASCII-paketstrÀng som innehÄller sensordata/telemetri
 * och en avslutande CRC-markör (";CRC=xx"). Om CRC stÀmmer extraheras numeriska vÀrden
 * frÄn vÀlkÀnda nycklar och lagras i globala variabler.
 *
 * Parametrar:
 *   msg - pekare till en skrivbar, null-terminerad C-strĂ€ng med paketet. Funktionen
 *         kommer att modifiera bufferten (ersĂ€tter ';' före CRC med '\0').
 *
 * Beteende:
 *   1. Söker efter substringen ";CRC=" i msg. Om den inte finns returneras funktionen omedelbart.
 *   2. LĂ€ser det hexadecimala CRC-vĂ€rdet som följer ";CRC=" med strtol(...,16) till rxCRC (uint8_t).
 *   3. ErsĂ€tter ';' vid CRC-markören med '\0' för att terminera meddelandedelen som ska CRC-kalkyleras.
 *   4. BerĂ€knar CRC över den trunkerade msg med calcCRC(msg) och jĂ€mför mot rxCRC.
 *      Vid avvikelse skrivs "CRC FAIL" till Serial och funktionen returnerar.
 *   5. Om CRC matchar söker funktionen i den trunkerade msg efter följande nycklar (strstr):
 *        - "T1=", "T2=", "T3=", "T4=", "T5="  -> parsas med atof(...) och sparas i T[0..4]
 *        - "A1="                              -> parsas med atoi(...) och sparas i shuntRaw
 *        - "D1=", "D2=", "D3="               -> parsas med atoi(...) och sparas i D1, D2, D3
 *      För varje funnen nyckel startar numerisk konvertering direkt efter '='. Om en nyckel
 *      saknas Ă€ndras den inte.
 *
 * Biverkningar / globala som Àndras:
 *   - Överskriver *crcPos med '\0' (modifierar msg-bufferten).
 *   - Kan skriva "CRC FAIL" till Serial vid CRC-missmatch.
 *   - Uppdaterar globala: T[] (0..4), shuntRaw, D1, D2, D3.
 *
 * FörutsÀttningar / noteringar:
 *   - msg mĂ„ste vara skrivbar (funktionen skriver en NUL).
 *   - msg mĂ„ste vara null-terminerad.
 *   - CRC-vĂ€rdet mĂ„ste vara hexadecimalt; strtol accepterar bĂ„de "0x" och rena hex-siffror.
 *   - calcCRC(msg) förvĂ€ntas vara kompatibel med det insĂ€nda vĂ€rdet.
 *   - atof/atoi anvĂ€nds för konvertering:
 *       - atof accepterar decimalpunkt '.' för brĂ„kdel.
 *       - atoi/atof returnerar 0 vid parse-fel eller om vĂ€rdet Ă€r noll.
 *
 * Exempelpaket:
 *   "T1=23.5;T2=19.0;A1=1023;D1=1;D2=0;CRC=AB"
 *
 * Felhantering:
 *   - Om ";CRC=" saknas eller CRC inte matchar returnerar funktionen tidigt och Ă€ndrar inte
 *     de numeriska globala (förutom modifiering av msg-bufferten).
 */
void parsePacket(char *msg){

  char *crcPos = strstr(msg,";CRC=");
  if(!crcPos) return;

  uint8_t rxCRC = strtol(crcPos+5,NULL,16);
  *crcPos=0;

  if(calcCRC(msg)!=rxCRC){
    Serial.println("CRC FAIL");
    return;
  }

  char *p;

  if((p=strstr(msg,"T1="))) T[0]=atof(p+3);
  if((p=strstr(msg,"T2="))) T[1]=atof(p+3);
  if((p=strstr(msg,"T3="))) T[2]=atof(p+3);
  if((p=strstr(msg,"T4="))) T[3]=atof(p+3);
  if((p=strstr(msg,"T5="))) T[4]=atof(p+3);

  if((p=strstr(msg,"A1="))) shuntRaw=atoi(p+3);
  if((p=strstr(msg,"D1="))) D1=atoi(p+3);
  if((p=strstr(msg,"D2="))) D2=atoi(p+3);
  if((p=strstr(msg,"D3="))) D3=atoi(p+3);
}

/* ---------- DRAW BAR ---------- */
/**
 * @brief Ritar en horisontell progress-/fyllningsstapel med vit kant och fÀrgad inre del.
 *
 * @param x     VĂ€nster pixelkoordinat för ytterrektangeln.
 * @param y     Ă–vre pixelkoordinat för ytterrektangeln.
 * @param w     Bredd i pixlar för ytterrektangeln (bör vara >= 2 för inre yta).
 * @param h     Höjd i pixlar för ytterrektangeln (bör vara >= 2).
 * @param value Aktuellt vÀrde som ska representeras; mappas proportionellt till fyllbredden.
 * @param max   VĂ€rdet som motsvarar fullt fylld stapel (mĂ„ste vara > 0 för korrekt beteende).
 * @param col   16-bit fĂ€rg som anvĂ€nds för fyllningen.
 *
 * Beteende:
 * - Ritar en vit ytterrektangel pÄ (x, y) med storlek (w, h).
 * - BerÀknar inre fyllbredd med map(value, 0, max, 0, w - 2).
 * - Fyller den inre rektangeln pÄ (x+1, y+1) med bredd = fill och höjd = h - 2 i fÀrgen col.
 *
 * Noteringar / förutsÀttningar:
 * - Funktionen förutsÀtter att en global pekare `gfx` och fÀrgkonstanten WHITE finns.
 * - För att undvika extrapolering bör caller klippa value till intervallet [0, max].
 * - Om w < 2 eller h < 2 blir inre yta <= 0; beteendet Àr ej definierat.
 * - Om max == 0 blir mappningen felaktig; sÀkerstÀll max > 0 innan anrop.
 */
void drawBar(int x,int y,int w,int h,int value,int max,uint16_t col){

  gfx->drawRect(x,y,w,h,WHITE);

  int fill = map(value,0,max,0,w-2);
  gfx->fillRect(x+1,y+1,fill,h-2,col);
}

/**
 * @brief BerÀknar energi som krÀvs för att vÀrma vattnet för en sensor.
 *
 * Funktionen:
 *  - BegrĂ€nsar temperaturen till intervallet [MIN_TEMP, MAX_TEMP].
 *  - BerĂ€knar temperaturskillnaden frĂ„n MIN_TEMP.
 *  - BestĂ€mmer antalet tankar: 2.0 om WINTER_MODE Ă€r sann, annars 1.0.
 *  - Konverterar volym till massa: mass = LITER_PER_SENSOR * tanks (1 L ≈ 1 kg).
 *  - BerĂ€knar energi i kilojoule: kJ = mass * WATER_CP * (temp - MIN_TEMP).
 *  - Returnerar energi i kilowattimmar: kWh = kJ / 3600.0.
 *
 * @param temp MÄlvÀrme i samma enhet som MIN_TEMP/MAX_TEMP (vanligtvis °C).
 * @return Energi i kWh. Observera att vÀrdet ibland visas som "kWh" i UI:t,
 *         men den hĂ€r funktionen returnerar kWh.
 *
 * Antaganden/Kommentarer:
 *  - Om temp < MIN_TEMP sĂ€tts temp = MIN_TEMP (resultat = 0).
 *  - Om temp > MAX_TEMP sĂ€tts temp = MAX_TEMP.
 *  - Inga sidoeffekter; ren berĂ€kning som anvĂ€nder globala konstanter:
 *    MIN_TEMP, MAX_TEMP, WINTER_MODE, LITER_PER_SENSOR, WATER_CP.
 */
float calcEnergy(float temp){
  if(temp < MIN_TEMP) temp = MIN_TEMP;
  if(temp > MAX_TEMP) temp = MAX_TEMP;

  float delta = temp - MIN_TEMP;
  float tanks = WINTER_MODE ? 2.0 : 1.0;

  float mass = LITER_PER_SENSOR * tanks;   // kg (1L ≈ 1kg)

  float kJ = mass * WATER_CP * delta;
  return kJ / 3600.0;
}

/**
 * ---------- DRAW SCREEN ----------
 * void drawScreen()
 *
 * Renderar hela UI-skÀrmen som visar tanktemperaturer, per-sensor energi,
 * en aggregerad totalenergibar, shuntens rÄvÀrde och en digital indikator.
 *
 * Beteende:
 *  - Rensar displayen och stĂ€ller in standardkursor/textfĂ€rg.
 *  - Skriver en rubrik "Tank:".
 *  - För varje av 5 temperaturgivare (T[0]..T[4]):
 *      - Anropar calcEnergy(T[i]) för att berĂ€kna energibidraget (kWh).
 *      - Ackumulerar totalEnergy och maxTotal (baserat pĂ„ LITER_PER_SENSOR, WINTER_MODE, WATER_CP och temperaturrange).
 *      - Skriver ut temperaturen (1 decimal) följt av sensorns energi i kW (1 decimal).
 *      - Ritar en horisontell röd stapel som representerar temperaturen med drawBar pĂ„ en fix x-position.
 *  - BerĂ€knar percent = (totalEnergy / maxTotal) * 100 och visar:
 *      - "TOTAL:"-etikett, totalEnergy (1 decimal, kWh) och percent (0 decimaler, %).
 *      - En grön totalenergiprogressbar lĂ€ngst ner.
 *  - Visar "Shunt:" och aktuellt shuntRaw-vĂ€rde pĂ„ en fast position.
 *  - Visar "El:" och en statuscirkel (vit nĂ€r digital ingĂ„ng D3 Ă€r sann, röd nĂ€r falsk).
 *
 * Antaganden / beroenden:
 *  - Pekaren `gfx` finns och erbjuder:
 *      fillScreen(color), setCursor(x,y), setTextColor(color), print(...), println(...), fillCircle(x,y,r,color).
 *  - HjĂ€lpfunktioner och globala som anvĂ€nds:
 *      calcEnergy(float temp) -> float (returnerar kWh),
 *      drawBar(int x,int y,int w,int h,int value,int max,uint16_t col),
 *      Konstanter: BLACK, WHITE, RED, GREEN, WINTER_MODE, LITER_PER_SENSOR, WATER_CP, MAX_TEMP, MIN_TEMP.
 *      Globala: float T[5], int shuntRaw, digitala D3.
 *
 * Koordinat-/layoutnoteringar (sÄ som implementerat):
 *  - Titel och per-sensor text börjar vid (0,0); vertikalt mellanrum ≈ 20 px.
 *  - Per-sensor staplar ritas vid x=80, y = i*20+2, storlek 80x12.
 *  - Shunt skrivs nĂ€ra y=78; "El:" och dess statuscirkel nĂ€ra y=91 med cirkelns centrum i (30,93), radie 5.
 *  - Total summering skrivs vid y=105; totalbar vid y=115 med bredd 158 och höjd 12.
 *
 * Kanthantering & förslag:
 *  - Skydda mot division med noll nĂ€r maxTotal == 0 innan berĂ€kning av percent.
 *  - Byt ut magiska tal (positioner, storlekar, antal sensorer) mot namngivna konstanter för bĂ€ttre underhĂ„ll.
 *  - ÖvervĂ€g klippning/avrundning och kulturberoende formatering för visade vĂ€rden.
 *  - Om uppdateringsfrekvens eller flimmer Ă€r ett problem: uppdatera endast de regioner som Ă€ndrats istĂ€llet för att rensa hela skĂ€rmen varje bildruta.
 *
 * TrÄdsÀkerhet / sidoeffekter:
 *  - Inte trĂ„dsĂ€ker; uppdaterar delad displaystatus och lĂ€ser globala variabler.
 */
void drawScreen(){

  gfx->fillScreen(BLACK);
  gfx->setCursor(0,0);
  gfx->setTextColor(WHITE);

  gfx->setCursor(0,0);
  gfx->println("Tank:");
  
  float totalEnergy=0;
  float maxTotal=0;

  float tanks = WINTER_MODE ? 2.0 : 1.0;
  float mass = LITER_PER_SENSOR * tanks;
  float maxPerSensor = (mass * WATER_CP * (MAX_TEMP-MIN_TEMP))/3600.0;

  /* TEMPERATURES + ENERGY */
  for(int i=0;i<5;i++){

    float e = calcEnergy(T[i]);
    totalEnergy += e;
    maxTotal += maxPerSensor;

    // gfx->print("T");
    // gfx->print(i+1);
    // gfx->print(": ");
    gfx->print(T[i],1);
    gfx->print("C ");

    gfx->print(e,1);
    gfx->println("kWh");

    drawBar(80, i*20+2,80,12,T[i],100,RED);
  }

  /* TOTAL ENERGY BAR */
  float percent = (totalEnergy/maxTotal)*100.0;

  gfx->setCursor(0,105);
  gfx->print("TOTAL:");
  gfx->print(totalEnergy,1);
  gfx->print("kWh ");

  gfx->print(percent,0);
  gfx->println("%");

  drawBar(0,115,158,12,percent,100,GREEN);

  /* SHUNT RAW */
  gfx->setCursor(0,78);
  gfx->print("Shunt:");
  gfx->println(shuntRaw);

  /* DIGITAL */
  gfx->setCursor(0,91);
  gfx->print("El:");
  gfx->fillCircle(30,93,5, D3?WHITE:RED);
}

/**
 * ---------- SETUP ---------- *
 *
 * @brief Initierar hÄrdvaruperiferier och förbereder mottagarens UI.
 *
 * Körs en gÄng vid programstart. Utför följande:
 *  - Initierar hĂ„rdvaru-Serial för debug pĂ„ 9600 baud.
 *  - Initierar HC12-radio pĂ„ 9600 baud.
 *  - Initierar displayobjektet, rensar skĂ€rmen till BLACK och sĂ€tter textstorlek till 1.
 *  - Skriver "Receiver ready" till Serial-konsolen.
 *
 * @note Antas att globala objekt/handtag Serial, HC12 och gfx Àr korrekt konstruerade
 *       och tillgĂ€ngliga innan denna funktion anropas.
 * @pre  Serial, HC12 och gfx mĂ„ste vara giltiga och deras begin()-metoder anropbara.
 * @post Serial och HC12 Àr konfigurerade till 9600 baud. Displayen Àr rensad och klar för text.
 */
void setup(){

  Serial.begin(9600);
  HC12.begin(9600);

  gfx->begin();
  gfx->fillScreen(BLACK);
  gfx->setTextSize(1);

  Serial.println("Receiver ready");
}

/* ---------- LOOP ---------- */

/**
 * @brief Arduino loop-hanterare: ta emot, parsa och validera paket frÄn HC12-radio.
 *
 * LÀs kontinuerligt bytes frÄn HC12-serien och bygg paket avgrÀnsade med
 * '<' (start) och '>' (slut) i en statisk teckenbuffert. Vid slutavgrÀnsare
 * terminera paketet, skriv ut det till Serial, gör en kopia, anropa parsePacket(...)
 * och jÀmför buffern med kopian för att avgöra om paketet klarade CRC/validering.
 * SkÀrmen uppdateras med drawScreen() efter varje paket.
 *
 * Beteende:
 * - AnvÀnder static char buf[180] och static byte idx för ackumulering över anrop.
 * - '<' ÄterstÀller index för att pÄbörja nytt paket.
 * - '>' terminera buffern, skriv ut, kopiera, anropa parsePacket(buf), jÀmför med kopian
 *   och skriv "CRC OK" eller "CRC ERROR". Anropa drawScreen() och nollstĂ€ll idx.
 * - Övriga tecken lĂ€ggs till bufferten om idx < 179 (plats för avslutande NUL).
 *
 * Antaganden och begrÀnsningar:
 * - HC12 implementerar available() och read() (Stream-liknande).
 * - Valideringslogiken förutsĂ€tter att parsePacket muterar buf vid framgĂ„ng; detta Ă€r skört —
 *   bĂ€ttre Ă€r att parsePacket returnerar explicit status.
 * - Ingen timeout eller utförlig hantering av för lÄnga paket utöver buffertstorleken.
 * - Payload som innehÄller '<' eller '>' bryter framer eftersom inga escape-mekanismer finns.
 * - Överflöd (index >=179) ignoreras i dagslĂ€get utan felrapportering.
 *
 * FörbÀttringsförslag:
 * - LÄt parsePacket returnera success/failure i stÀllet för att jÀmföra buffrar.
 * - LÀgg till paket-timeout och explicit överflödes-/felhantering.
 * - Implementera en enkel state-machine som stödjer escaping om payloads kan innehÄlla
 *   avgrĂ€nsartecken.
 * - AnvÀnd lÀngdbaserade och bounds-kontrollerade API:er och undvik tysta truncations.
 *
 * @return void
 */
void loop(){

  static char buf[180];
  static byte idx=0;

  while(HC12.available()){

    char c = HC12.read();

    if(c=='<') idx=0;

    else if(c=='>'){
      buf[idx]=0;

      Serial.print("RAW: ");
      Serial.println(buf);

      char test[180];
      strcpy(test,buf);

      parsePacket(buf);

      if(strcmp(buf,test)==0)
        Serial.println("CRC ERROR");
      else
        Serial.println("CRC OK");

      drawScreen();
      idx=0;
    }

    else if(idx<179){
      buf[idx++]=c;
    }
  }
}



ï»ż