kWh i ackumulatortankar
Experiment med HC-12 transceivers.
Uppdaterad kod till mottagaren.

Koden har nu en timer som räknar sekunder sedan senaste mottagna data.
Shunten är nu kalibrerad och visar nu procent istället värde.
Har nu ändrat från vinter till sommar och stängt av en tank.
Temperaraturen är nu satt på elpatronen till 65 grader.
Laddningstiden för elpatronen är nu satt till kl. 00:00 till 06:00.
Elpatronen schemaläggs med ett annant system.
Shunten är nu kalibrerad och visar nu procent istället värde.
Har nu ändrat från vinter till sommar och stängt av en tank.
Temperaraturen är nu satt på elpatronen till 65 grader.
Laddningstiden för elpatronen är nu satt till kl. 00:00 till 06:00.
Elpatronen schemaläggs med ett annant system.
Kalibrera kWh är inte klar ännu.
Den undre givaren är monterad under elpatronen i höjd med returvatten från elementen.
När hela tankens vatten har cirkulerat ett varv kommer den energin att användas, tror jag.
Om inte elpatronen är på förstås.
Den undre givaren är monterad under elpatronen i höjd med returvatten från elementen.
När hela tankens vatten har cirkulerat ett varv kommer den energin att användas, tror jag.
Om inte elpatronen är på förstås.
PlaytformIO, mottagaren Version 2. fil: 'main.cpp'
main.cpp
#include <Arduino.h>
#include <U8g2lib.h>
#include <Arduino_GFX_Library.h>
/*
* MCU är en ATmega2560 (Arduino Mega)
* 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
*/
/*
* MCU är en ATmega2560 (Arduino Mega) är på 5 Volt.
* Displayen är en 1.8" TFT med ST7735-drivrutin, som vanligtvis kräver 3.3V logik.
* Glöm inte att använda nivåomvandlare för att skydda displayen.
*/
#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
// Sensor- och displaykonstanter. Justera efter ditt system.
#define WINTER_MODE 0 // 0 = sommar (1 tank) | 1 = vinter (2 tankar)
#define LITER_PER_SENSOR 150.0
#define MIN_TEMP 35.0 //Under denna temperartur räknas som ingen användingsbar energi,
#define MAX_TEMP_VINTER 83.0 //vintertid
#define MAX_TEMP_SOMMAR 65.0 //sommartid
#define WATER_CP 4.186 // kJ/kg°C
#define SHUNT_MIN 224 // 100%
#define SHUNT_MAX 559 // 0%
/* 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;
// D3 används som nätindikator (1 = nät närvarande, 0 = ingen nätström)
// Övriga används inte i displayen
// men kan vara användbara för felsökning eller framtida funktioner.
int D1=0,D2=0,D3=0;
/* ---------- Globals ---------- */
unsigned long lastReceiveTime = 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 ackumulator. 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 Returnerar maxtemperaturen som gäller för aktuell säsong.
*
* Funktion väljer mellan MAX_TEMP_VINTER och MAX_TEMP_SOMMAR beroende på
* den globala flaggan WINTER_MODE (icke-noll = vinter).
*
* @return float Maxtemperatur i samma enhet som MAX_TEMP_VINTER/MAX_TEMP_SOMMAR.
*/
float getMaxTemp(){
return WINTER_MODE ? MAX_TEMP_VINTER : MAX_TEMP_SOMMAR;
}
/**
* Konverterar ett rått shuntvärde till en procentuell nivå (0.0 - 100.0).
*
* - Värdet klipps till intervallet [SHUNT_MIN, SHUNT_MAX].
* - SHUNT_MIN motsvarar 100% och SHUNT_MAX motsvarar 0%.
* - Skyddar mot division med noll om SHUNT_MAX == SHUNT_MIN.
*
* @param raw Rått shuntvärde (t.ex. ADC-värde).
* @return Flyttal i intervallet [0.0, 100.0].
*/
float calcShuntPercent(int raw) {
if (SHUNT_MAX == SHUNT_MIN) return 0.0f; // skydd mot division med noll
if (raw < SHUNT_MIN) raw = SHUNT_MIN;
else if (raw > SHUNT_MAX) raw = SHUNT_MAX;
float denom = (float)(SHUNT_MAX - SHUNT_MIN);
float percent = (float)(SHUNT_MAX - raw) * 100.0f / denom;
if (percent < 0.0f) percent = 0.0f;
else if (percent > 100.0f) percent = 100.0f;
return percent;
}
/**
* Beräknar energin som krävs för att värma vattenmängden kopplad till en sensor
* från MIN_TEMP upp till den angivna temperaturen.
*
* - Temperatur klipps till intervallet [MIN_TEMP, getMaxTemp()].
* - Temperaturdifferensen delta = clamped_temp - MIN_TEMP.
* - Antal tankar bestäms av WINTER_MODE (2.0 vid vinter, annars 1.0).
* - Massan i kg beräknas som: mass = LITER_PER_SENSOR * tanks (1 L ≈ 1 kg).
* - Värme i kJ: kJ = mass * WATER_CP * delta
* - Returneras i kWh: return kJ / 3600.0
*
* @param temp Måltemperatur i °C.
* @return Energi i kilowattimmar (kWh) som krävs för uppvärmning från MIN_TEMP.
*
* Notera:
* - Om temp <= MIN_TEMP returneras 0 (ingen energi behövs).
* - Funktionen använder globala konstanter: MIN_TEMP, LITER_PER_SENSOR, WATER_CP,
* WINTER_MODE och getMaxTemp().
*/
float calcEnergy(float temp){
float maxTemp = getMaxTemp();
if(temp < MIN_TEMP) temp = MIN_TEMP;
if(temp > maxTemp) temp = maxTemp;
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;
}
/**
* drawTimer()
*
* Uppdaterar ett litet område på displayen för att visa tid sedan sista
* mottagna händelse, eller en timeout-meddelande om ingen ny signal finns.
*
* Beteende:
* - Beräknar förfluten tid i sekunder som (millis() - lastReceiveTime) / 1000.
* - Rensar en fast rektangel (0, 68, 75, 10) genom att fylla med BLACK.
* - Sätter textkursorn till (0, 69).
* - Om förfluten tid >= 99: sätter textfärg till RED och skriver "No signal!".
* - Annars: sätter textfärg till WHITE och skriver "Last RX: <elapsed>s".
*
* Antaganden och sidoeffekter:
* - gfx är en pekare/objekt för display som erbjuder:
* fillRect(x, y, w, h, color), setCursor(x, y), setTextColor(color),
* och print(...) metoder.
* - Färgkonstanterna BLACK, RED och WHITE är definierade och förstås av gfx.
* - lastReceiveTime och millis() är unsigned long tidsstämplar i ms.
* - Funktionen returnerar inget; den uppdaterar endast displayen.
*
* Noteringar:
* - Heltalsdivision används för att få hela sekunder; sub-sekunders precision försvinner.
* - Timeoutgränsen (99 s), rektangelns koordinater och texter är hårdkodade.
* - Överväg att klippa eller formatera värdet för att undvika layoutproblem vid mycket stora tal.
*/
void drawTimer(){
unsigned long elapsed = (millis() - lastReceiveTime)/1000;
// rensa bara timer-ytan
gfx->fillRect(0,68,75,11,BLACK);
gfx->setCursor(0,69);
// röd text om timeout
if(elapsed >= 99){
gfx->setTextColor(RED);
gfx->print("No signal!");
} else {
gfx->setTextColor(WHITE);
gfx->print("Last RX: ");
gfx->print(elapsed);
gfx->print("s");
}
}
/**
* @brief Ritar huvudstatusskärmen på gfx-displayen.
*
* Denna funktion tömmer skärmen och ritar en sammanfattning av tanktemperaturer,
* per-sensor och total lagrad energi, en progressbar för total energi vs beräknad
* maxkapacitet, shuntprocent och en nätindikator (El).
*
* Placering / beteende:
* - Rensar skärmen och sätter textfärg till WHITE.
* - Skriver rubriken "Tank:" uppe till vänster (kursorn 0,0).
* - Itererar över 5 temperatursensorer (T[0]..T[4]):
* - Beräknar per-sensor-energi med calcEnergy(T[i]) och summerar totalEnergy.
* - Adderar maxPerSensor till maxTotal (maxPerSensor beräknas från massan,
* WATER_CP och temperaturskillnaden).
* - Skriver varje sensors temperatur (1 decimal) och energi (1 decimal)
* som "XX.XC YY.YkWh" på y-positions startande vid y=12, med 20 px mellanrum.
* - Ritar en röd stapel till höger (drawBar vid x=80) som visar temperaturen
* mappad till 0..100 (klippt).
* - Beräknar percent = (totalEnergy / maxTotal) * 100 (0 om maxTotal == 0).
* - Skriver TOTAL: med totalEnergy (1 decimal, kWh) och percent (0 decimaler)
* på y=105 och ritar en grön progressbar över x=0..158 vid y=115 som visar percent
* (klippt till [0,100]).
* - Beräknar och skriver shuntprocent (calcShuntPercent(shuntRaw)) vid y=80.
* - Ritar nätindikatorn "El:" vid y=93 och en cirkel vid (30,95) fylld med WHITE
* när D3 är sann (nät närvarande) eller RED när D3 är falsk.
*
* Enheter och beräkningar:
* - tanks = WINTER_MODE ? 2 : 1
* - mass = LITER_PER_SENSOR * tanks (liter ≈ kg)
* - maxPerSensor = (mass * WATER_CP * (maxTemp - MIN_TEMP)) / 3600.0f
* - Antas att WATER_CP är i kJ/(kg*K) så delning med 3600 ger kWh per sensor,
* i enlighet med calcEnergy().
*
* Formatering och klippning:
* - Temperaturer skrivs med en decimal.
* - Energier skrivs med en decimal och märks "kWh".
* - Procent skrivs utan decimaler.
* - Temperatuvärden för varje stapel klipps till [0,100] via constrain().
* - Total percent klipps till [0,100] innan visning.
*
* Beroenden / använda globala:
* - gfx (fillScreen, setTextColor, setCursor, print/println, fillCircle)
* - Konstanter/flaggor: BLACK, WHITE, RED, GREEN, WINTER_MODE, LITER_PER_SENSOR,
* WATER_CP, MIN_TEMP
* - Funktioner: getMaxTemp(), calcEnergy(float), drawBar(...), calcShuntPercent(...)
* - Globala värden: float T[5], shuntRaw, D3
*
* Bieffekter:
* - Uppdaterar displayen via gfx-anrop.
* - Läser flera globala variabler och hjälpfunktioner.
*
* Noteringar:
* - Funktionen använder alltid 5 sensorer och fasta layout-koordinater.
* - Se till att calcEnergy() och WATER_CP använder enhet som ger kWh för meningsfulla procentsatser.
*
* Komplextet:
* - Tidskomplexitet O(N) för N = 5 (fast).
*
* Returnerar:
* - void (renderar direkt till displayen).
*/
void drawScreen(){
gfx->fillScreen(BLACK);
gfx->setTextColor(WHITE);
gfx->setCursor(0,0);
gfx->println("Tank:");
float totalEnergy = 0.0f;
float maxTotal = 0.0f;
float tanks = WINTER_MODE ? 2.0f : 1.0f;
float mass = LITER_PER_SENSOR * tanks;
float maxTemp = getMaxTemp();
float maxPerSensor = (mass * WATER_CP * (maxTemp - MIN_TEMP)) / 3600.0f;
// Temperatures + per-sensor energy
for(int i = 0; i < 5; ++i){
float e = calcEnergy(T[i]);
totalEnergy += e;
maxTotal += maxPerSensor;
// Text line
gfx->setCursor(0, 12 + i * 10);
gfx->print(T[i], 1);
gfx->print("C ");
gfx->print(e, 1);
gfx->println("kWh");
// Temperatur till stapel: mappa temperatur till 0-100 för visning
int tempVis = (int)constrain(T[i], 0.0f, 100.0f);
drawBar(80, i * 20 + 2, 80, 12, tempVis, 100, RED);
}
// Total energy och procent
float percent = (maxTotal > 0.0f) ? (totalEnergy / maxTotal) * 100.0f : 0.0f;
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, (int)constrain(percent, 0.0f, 100.0f), 100, GREEN);
// Shunt
float shuntPercent = calcShuntPercent(shuntRaw);
gfx->setCursor(0, 80);
gfx->print("Shunt:");
gfx->print(shuntPercent, 0);
gfx->println("%");
// Digital / mains indicator
gfx->setCursor(0, 93);
gfx->print("El:");
gfx->fillCircle(30, 95, 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;
static unsigned long lastDraw = 0; // <-- NY
while(HC12.available()){
char c = HC12.read();
if(c=='<') idx=0;
else if(c=='>'){
buf[idx]=0;
lastReceiveTime = millis(); // nollställ timer
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(); // direkt uppdatering vid paket
idx=0;
}
else if(idx<179){
buf[idx++]=c;
}
}
// <-- NY PERIODISK SKÄRMUPPDATERING
if(millis() - lastDraw >= 1000){
lastDraw = millis();
drawTimer(); // bara timer ritas om
}
}
