diff --git a/CHANGELOG_LATEST.md b/CHANGELOG_LATEST.md index 29af34c69..fd5ecb609 100644 --- a/CHANGELOG_LATEST.md +++ b/CHANGELOG_LATEST.md @@ -33,6 +33,8 @@ For more details go to [docs.emsesp.org](https://docs.emsesp.org/). - pumpmode enum for HT3 boilers, add commands for manual defrost, chimneysweeper [#2727](https://github.com/emsesp/EMS-ESP32/issues/2727) - pid settings [#2735](https://github.com/emsesp/EMS-ESP32/issues/2735) - refresh MQTT button added to MQTT Settings page +- heating assistance, rounding custum settings [#2763](https://github.com/emsesp/EMS-ESP32/discussions/2763) +- add counter 0..2 for short pulses, high frequency [#2758](https://github.com/emsesp/EMS-ESP32/issues/2758) - added LWT (Last Will and Testament) to MQTT entities in Home Assistant - added api/metrics endpoint for prometheus integration by @gr3enk [#2774](https://github.com/emsesp/EMS-ESP32/pull/2774) diff --git a/docs/Modbus-Entity-Registers.md b/docs/Modbus-Entity-Registers.md index 9d718e583..1d7525a98 100644 --- a/docs/Modbus-Entity-Registers.md +++ b/docs/Modbus-Entity-Registers.md @@ -182,9 +182,9 @@ | dhw.comfoff | comfort switch off | uint8 (>=15<=65) | C | true | DHW | 18 | 1 | 1 | | dhw.ecooff | eco switch off | uint8 (>=15<=65) | C | true | DHW | 19 | 1 | 1 | | dhw.ecoplusoff | eco+ switch off | uint8 (>=48<=63) | C | true | DHW | 20 | 1 | 1 | -| dhw.comfdiff | comfort diff | uint8 (>=4<=12) | K | true | DHW | 21 | 1 | 1 | -| dhw.ecodiff | eco diff | uint8 (>=4<=12) | K | true | DHW | 22 | 1 | 1 | -| dhw.ecoplusdiff | eco+ diff | uint8 (>=6<=12) | K | true | DHW | 23 | 1 | 1 | +| dhw.comfdiff | comfort diff | uint8 (>=4<=15) | K | true | DHW | 21 | 1 | 1 | +| dhw.ecodiff | eco diff | uint8 (>=4<=15) | K | true | DHW | 22 | 1 | 1 | +| dhw.ecoplusdiff | eco+ diff | uint8 (>=4<=15) | K | true | DHW | 23 | 1 | 1 | | dhw.comfstop | comfort stop temp | uint8 (>=0<=254) | C | true | DHW | 24 | 1 | 1 | | dhw.ecostop | eco stop temp | uint8 (>=0<=254) | C | true | DHW | 25 | 1 | 1 | | dhw.ecoplusstop | eco+ stop temp | uint8 (>=0<=254) | C | true | DHW | 26 | 1 | 1 | @@ -2821,9 +2821,9 @@ | dhw.comfoff | comfort switch off | uint8 (>=15<=65) | C | true | DHW | 18 | 1 | 1 | | dhw.ecooff | eco switch off | uint8 (>=15<=65) | C | true | DHW | 19 | 1 | 1 | | dhw.ecoplusoff | eco+ switch off | uint8 (>=48<=63) | C | true | DHW | 20 | 1 | 1 | -| dhw.comfdiff | comfort diff | uint8 (>=4<=12) | K | true | DHW | 21 | 1 | 1 | -| dhw.ecodiff | eco diff | uint8 (>=4<=12) | K | true | DHW | 22 | 1 | 1 | -| dhw.ecoplusdiff | eco+ diff | uint8 (>=6<=12) | K | true | DHW | 23 | 1 | 1 | +| dhw.comfdiff | comfort diff | uint8 (>=4<=15) | K | true | DHW | 21 | 1 | 1 | +| dhw.ecodiff | eco diff | uint8 (>=4<=15) | K | true | DHW | 22 | 1 | 1 | +| dhw.ecoplusdiff | eco+ diff | uint8 (>=4<=15) | K | true | DHW | 23 | 1 | 1 | | dhw.comfstop | comfort stop temp | uint8 (>=0<=254) | C | true | DHW | 24 | 1 | 1 | | dhw.ecostop | eco stop temp | uint8 (>=0<=254) | C | true | DHW | 25 | 1 | 1 | | dhw.ecoplusstop | eco+ stop temp | uint8 (>=0<=254) | C | true | DHW | 26 | 1 | 1 | @@ -3040,9 +3040,9 @@ | dhw.comfoff | comfort switch off | uint8 (>=15<=65) | C | true | DHW | 18 | 1 | 1 | | dhw.ecooff | eco switch off | uint8 (>=15<=65) | C | true | DHW | 19 | 1 | 1 | | dhw.ecoplusoff | eco+ switch off | uint8 (>=48<=63) | C | true | DHW | 20 | 1 | 1 | -| dhw.comfdiff | comfort diff | uint8 (>=4<=12) | K | true | DHW | 21 | 1 | 1 | -| dhw.ecodiff | eco diff | uint8 (>=4<=12) | K | true | DHW | 22 | 1 | 1 | -| dhw.ecoplusdiff | eco+ diff | uint8 (>=6<=12) | K | true | DHW | 23 | 1 | 1 | +| dhw.comfdiff | comfort diff | uint8 (>=4<=15) | K | true | DHW | 21 | 1 | 1 | +| dhw.ecodiff | eco diff | uint8 (>=4<=15) | K | true | DHW | 22 | 1 | 1 | +| dhw.ecoplusdiff | eco+ diff | uint8 (>=4<=15) | K | true | DHW | 23 | 1 | 1 | | dhw.comfstop | comfort stop temp | uint8 (>=0<=254) | C | true | DHW | 24 | 1 | 1 | | dhw.ecostop | eco stop temp | uint8 (>=0<=254) | C | true | DHW | 25 | 1 | 1 | | dhw.ecoplusstop | eco+ stop temp | uint8 (>=0<=254) | C | true | DHW | 26 | 1 | 1 | @@ -5815,6 +5815,8 @@ | heatcnt | heat counter impulses | uint8 (>=0<=254) | | false | DEVICE_DATA | 67 | 1 | 1 | | swapflowtemp | swap flow temperature (TS14) | uint16 (>=0<=3199) | C | false | DEVICE_DATA | 68 | 1 | 1/10 | | swaprettemp | swap return temperature (TS15) | uint16 (>=0<=3199) | C | false | DEVICE_DATA | 69 | 1 | 1/10 | +| heatassiston | heat assistance on | int8 (>=-12<=12) | K | true | DEVICE_DATA | 70 | 1 | 1/10 | +| heatassistoff | heat assistance off | int8 (>=-12<=12) | K | true | DEVICE_DATA | 71 | 1 | 1/10 | ### SM100, MS100 | shortname | fullname | type | uom | writeable | tag type | register offset | register count | scale factor | @@ -5880,6 +5882,8 @@ | heatcnt | heat counter impulses | uint8 (>=0<=254) | | false | DEVICE_DATA | 67 | 1 | 1 | | swapflowtemp | swap flow temperature (TS14) | uint16 (>=0<=3199) | C | false | DEVICE_DATA | 68 | 1 | 1/10 | | swaprettemp | swap return temperature (TS15) | uint16 (>=0<=3199) | C | false | DEVICE_DATA | 69 | 1 | 1/10 | +| heatassiston | heat assistance on | int8 (>=-12<=12) | K | true | DEVICE_DATA | 70 | 1 | 1/10 | +| heatassistoff | heat assistance off | int8 (>=-12<=12) | K | true | DEVICE_DATA | 71 | 1 | 1/10 | ### SM200, MS200 | shortname | fullname | type | uom | writeable | tag type | register offset | register count | scale factor | @@ -5945,6 +5949,8 @@ | heatcnt | heat counter impulses | uint8 (>=0<=254) | | false | DEVICE_DATA | 67 | 1 | 1 | | swapflowtemp | swap flow temperature (TS14) | uint16 (>=0<=3199) | C | false | DEVICE_DATA | 68 | 1 | 1/10 | | swaprettemp | swap return temperature (TS15) | uint16 (>=0<=3199) | C | false | DEVICE_DATA | 69 | 1 | 1/10 | +| heatassiston | heat assistance on | int8 (>=-12<=12) | K | true | DEVICE_DATA | 70 | 1 | 1/10 | +| heatassistoff | heat assistance off | int8 (>=-12<=12) | K | true | DEVICE_DATA | 71 | 1 | 1/10 | ## Devices of type *heatpump* ### HP Module diff --git a/docs/dump_entities.csv b/docs/dump_entities.csv index adc2a7cc1..323997f18 100644 --- a/docs/dump_entities.csv +++ b/docs/dump_entities.csv @@ -166,9 +166,9 @@ device name,device type,product id,shortname,fullname,type [options...] \| (min/ "CS5800i, CS6800i, WLW176i, WLW186i",boiler,8,dhw.comfoff,comfort switch off,uint8 (>=15<=65),C,true,number.boiler_dhw_comfort_switch_off,number.boiler_dhw_comfoff,5,9,1,18,1 "CS5800i, CS6800i, WLW176i, WLW186i",boiler,8,dhw.ecooff,eco switch off,uint8 (>=15<=65),C,true,number.boiler_dhw_eco_switch_off,number.boiler_dhw_ecooff,5,9,1,19,1 "CS5800i, CS6800i, WLW176i, WLW186i",boiler,8,dhw.ecoplusoff,eco+ switch off,uint8 (>=48<=63),C,true,number.boiler_dhw_eco+_switch_off,number.boiler_dhw_ecoplusoff,5,9,1,20,1 -"CS5800i, CS6800i, WLW176i, WLW186i",boiler,8,dhw.comfdiff,comfort diff,uint8 (>=4<=12),K,true,number.boiler_dhw_comfort_diff,number.boiler_dhw_comfdiff,5,9,1,21,1 -"CS5800i, CS6800i, WLW176i, WLW186i",boiler,8,dhw.ecodiff,eco diff,uint8 (>=4<=12),K,true,number.boiler_dhw_eco_diff,number.boiler_dhw_ecodiff,5,9,1,22,1 -"CS5800i, CS6800i, WLW176i, WLW186i",boiler,8,dhw.ecoplusdiff,eco+ diff,uint8 (>=6<=12),K,true,number.boiler_dhw_eco+_diff,number.boiler_dhw_ecoplusdiff,5,9,1,23,1 +"CS5800i, CS6800i, WLW176i, WLW186i",boiler,8,dhw.comfdiff,comfort diff,uint8 (>=4<=15),K,true,number.boiler_dhw_comfort_diff,number.boiler_dhw_comfdiff,5,9,1,21,1 +"CS5800i, CS6800i, WLW176i, WLW186i",boiler,8,dhw.ecodiff,eco diff,uint8 (>=4<=15),K,true,number.boiler_dhw_eco_diff,number.boiler_dhw_ecodiff,5,9,1,22,1 +"CS5800i, CS6800i, WLW176i, WLW186i",boiler,8,dhw.ecoplusdiff,eco+ diff,uint8 (>=4<=15),K,true,number.boiler_dhw_eco+_diff,number.boiler_dhw_ecoplusdiff,5,9,1,23,1 "CS5800i, CS6800i, WLW176i, WLW186i",boiler,8,dhw.comfstop,comfort stop temp,uint8 (>=0<=254),C,true,number.boiler_dhw_comfort_stop_temp,number.boiler_dhw_comfstop,5,9,1,24,1 "CS5800i, CS6800i, WLW176i, WLW186i",boiler,8,dhw.ecostop,eco stop temp,uint8 (>=0<=254),C,true,number.boiler_dhw_eco_stop_temp,number.boiler_dhw_ecostop,5,9,1,25,1 "CS5800i, CS6800i, WLW176i, WLW186i",boiler,8,dhw.ecoplusstop,eco+ stop temp,uint8 (>=0<=254),C,true,number.boiler_dhw_eco+_stop_temp,number.boiler_dhw_ecoplusstop,5,9,1,26,1 @@ -2721,9 +2721,9 @@ device name,device type,product id,shortname,fullname,type [options...] \| (min/ "Enviline, Compress 6000AW, Hybrid 3000-7000iAW, SupraEco/Geo 5xx, WLW196i/WSW196i",boiler,172,dhw.comfoff,comfort switch off,uint8 (>=15<=65),C,true,number.boiler_dhw_comfort_switch_off,number.boiler_dhw_comfoff,5,9,1,18,1 "Enviline, Compress 6000AW, Hybrid 3000-7000iAW, SupraEco/Geo 5xx, WLW196i/WSW196i",boiler,172,dhw.ecooff,eco switch off,uint8 (>=15<=65),C,true,number.boiler_dhw_eco_switch_off,number.boiler_dhw_ecooff,5,9,1,19,1 "Enviline, Compress 6000AW, Hybrid 3000-7000iAW, SupraEco/Geo 5xx, WLW196i/WSW196i",boiler,172,dhw.ecoplusoff,eco+ switch off,uint8 (>=48<=63),C,true,number.boiler_dhw_eco+_switch_off,number.boiler_dhw_ecoplusoff,5,9,1,20,1 -"Enviline, Compress 6000AW, Hybrid 3000-7000iAW, SupraEco/Geo 5xx, WLW196i/WSW196i",boiler,172,dhw.comfdiff,comfort diff,uint8 (>=4<=12),K,true,number.boiler_dhw_comfort_diff,number.boiler_dhw_comfdiff,5,9,1,21,1 -"Enviline, Compress 6000AW, Hybrid 3000-7000iAW, SupraEco/Geo 5xx, WLW196i/WSW196i",boiler,172,dhw.ecodiff,eco diff,uint8 (>=4<=12),K,true,number.boiler_dhw_eco_diff,number.boiler_dhw_ecodiff,5,9,1,22,1 -"Enviline, Compress 6000AW, Hybrid 3000-7000iAW, SupraEco/Geo 5xx, WLW196i/WSW196i",boiler,172,dhw.ecoplusdiff,eco+ diff,uint8 (>=6<=12),K,true,number.boiler_dhw_eco+_diff,number.boiler_dhw_ecoplusdiff,5,9,1,23,1 +"Enviline, Compress 6000AW, Hybrid 3000-7000iAW, SupraEco/Geo 5xx, WLW196i/WSW196i",boiler,172,dhw.comfdiff,comfort diff,uint8 (>=4<=15),K,true,number.boiler_dhw_comfort_diff,number.boiler_dhw_comfdiff,5,9,1,21,1 +"Enviline, Compress 6000AW, Hybrid 3000-7000iAW, SupraEco/Geo 5xx, WLW196i/WSW196i",boiler,172,dhw.ecodiff,eco diff,uint8 (>=4<=15),K,true,number.boiler_dhw_eco_diff,number.boiler_dhw_ecodiff,5,9,1,22,1 +"Enviline, Compress 6000AW, Hybrid 3000-7000iAW, SupraEco/Geo 5xx, WLW196i/WSW196i",boiler,172,dhw.ecoplusdiff,eco+ diff,uint8 (>=4<=15),K,true,number.boiler_dhw_eco+_diff,number.boiler_dhw_ecoplusdiff,5,9,1,23,1 "Enviline, Compress 6000AW, Hybrid 3000-7000iAW, SupraEco/Geo 5xx, WLW196i/WSW196i",boiler,172,dhw.comfstop,comfort stop temp,uint8 (>=0<=254),C,true,number.boiler_dhw_comfort_stop_temp,number.boiler_dhw_comfstop,5,9,1,24,1 "Enviline, Compress 6000AW, Hybrid 3000-7000iAW, SupraEco/Geo 5xx, WLW196i/WSW196i",boiler,172,dhw.ecostop,eco stop temp,uint8 (>=0<=254),C,true,number.boiler_dhw_eco_stop_temp,number.boiler_dhw_ecostop,5,9,1,25,1 "Enviline, Compress 6000AW, Hybrid 3000-7000iAW, SupraEco/Geo 5xx, WLW196i/WSW196i",boiler,172,dhw.ecoplusstop,eco+ stop temp,uint8 (>=0<=254),C,true,number.boiler_dhw_eco+_stop_temp,number.boiler_dhw_ecoplusstop,5,9,1,26,1 @@ -2936,9 +2936,9 @@ device name,device type,product id,shortname,fullname,type [options...] \| (min/ "Geo 5xx",boiler,173,dhw.comfoff,comfort switch off,uint8 (>=15<=65),C,true,number.boiler_dhw_comfort_switch_off,number.boiler_dhw_comfoff,5,9,1,18,1 "Geo 5xx",boiler,173,dhw.ecooff,eco switch off,uint8 (>=15<=65),C,true,number.boiler_dhw_eco_switch_off,number.boiler_dhw_ecooff,5,9,1,19,1 "Geo 5xx",boiler,173,dhw.ecoplusoff,eco+ switch off,uint8 (>=48<=63),C,true,number.boiler_dhw_eco+_switch_off,number.boiler_dhw_ecoplusoff,5,9,1,20,1 -"Geo 5xx",boiler,173,dhw.comfdiff,comfort diff,uint8 (>=4<=12),K,true,number.boiler_dhw_comfort_diff,number.boiler_dhw_comfdiff,5,9,1,21,1 -"Geo 5xx",boiler,173,dhw.ecodiff,eco diff,uint8 (>=4<=12),K,true,number.boiler_dhw_eco_diff,number.boiler_dhw_ecodiff,5,9,1,22,1 -"Geo 5xx",boiler,173,dhw.ecoplusdiff,eco+ diff,uint8 (>=6<=12),K,true,number.boiler_dhw_eco+_diff,number.boiler_dhw_ecoplusdiff,5,9,1,23,1 +"Geo 5xx",boiler,173,dhw.comfdiff,comfort diff,uint8 (>=4<=15),K,true,number.boiler_dhw_comfort_diff,number.boiler_dhw_comfdiff,5,9,1,21,1 +"Geo 5xx",boiler,173,dhw.ecodiff,eco diff,uint8 (>=4<=15),K,true,number.boiler_dhw_eco_diff,number.boiler_dhw_ecodiff,5,9,1,22,1 +"Geo 5xx",boiler,173,dhw.ecoplusdiff,eco+ diff,uint8 (>=4<=15),K,true,number.boiler_dhw_eco+_diff,number.boiler_dhw_ecoplusdiff,5,9,1,23,1 "Geo 5xx",boiler,173,dhw.comfstop,comfort stop temp,uint8 (>=0<=254),C,true,number.boiler_dhw_comfort_stop_temp,number.boiler_dhw_comfstop,5,9,1,24,1 "Geo 5xx",boiler,173,dhw.ecostop,eco stop temp,uint8 (>=0<=254),C,true,number.boiler_dhw_eco_stop_temp,number.boiler_dhw_ecostop,5,9,1,25,1 "Geo 5xx",boiler,173,dhw.ecoplusstop,eco+ stop temp,uint8 (>=0<=254),C,true,number.boiler_dhw_eco+_stop_temp,number.boiler_dhw_ecoplusstop,5,9,1,26,1 @@ -5468,6 +5468,8 @@ device name,device type,product id,shortname,fullname,type [options...] \| (min/ "SM50",solar,162,heatcnt,heat counter impulses,uint8 (>=0<=254), ,false,sensor.solar_heat_counter_impulses,sensor.solar_heatcnt,8,0,1,67,1 "SM50",solar,162,swapflowtemp,swap flow temperature (TS14),uint16 (>=0<=3199),C,false,sensor.solar_swap_flow_temperature_(TS14),sensor.solar_swapflowtemp,8,0,1/10,68,1 "SM50",solar,162,swaprettemp,swap return temperature (TS15),uint16 (>=0<=3199),C,false,sensor.solar_swap_return_temperature_(TS15),sensor.solar_swaprettemp,8,0,1/10,69,1 +"SM50",solar,162,heatassiston,heat assistance on,int8 (>=-12<=12),K,true,number.solar_heat_assistance_on,number.solar_heatassiston,8,0,1/10,70,1 +"SM50",solar,162,heatassistoff,heat assistance off,int8 (>=-12<=12),K,true,number.solar_heat_assistance_off,number.solar_heatassistoff,8,0,1/10,71,1 "SM100, MS100",solar,163,collectortemp,collector temperature (TS1),int16 (>=-3199<=3199),C,false,sensor.solar_collector_temperature_(TS1),sensor.solar_collectortemp,8,0,1/10,0,1 "SM100, MS100",solar,163,cylbottomtemp,cylinder bottom temperature (TS2),int16 (>=-3199<=3199),C,false,sensor.solar_cylinder_bottom_temperature_(TS2),sensor.solar_cylbottomtemp,8,0,1/10,1,1 "SM100, MS100",solar,163,solarpump,pump (PS1),boolean, ,false,binary_sensor.solar_pump_(PS1),binary_sensor.solar_solarpump,8,0,1,2,1 @@ -5529,6 +5531,8 @@ device name,device type,product id,shortname,fullname,type [options...] \| (min/ "SM100, MS100",solar,163,heatcnt,heat counter impulses,uint8 (>=0<=254), ,false,sensor.solar_heat_counter_impulses,sensor.solar_heatcnt,8,0,1,67,1 "SM100, MS100",solar,163,swapflowtemp,swap flow temperature (TS14),uint16 (>=0<=3199),C,false,sensor.solar_swap_flow_temperature_(TS14),sensor.solar_swapflowtemp,8,0,1/10,68,1 "SM100, MS100",solar,163,swaprettemp,swap return temperature (TS15),uint16 (>=0<=3199),C,false,sensor.solar_swap_return_temperature_(TS15),sensor.solar_swaprettemp,8,0,1/10,69,1 +"SM100, MS100",solar,163,heatassiston,heat assistance on,int8 (>=-12<=12),K,true,number.solar_heat_assistance_on,number.solar_heatassiston,8,0,1/10,70,1 +"SM100, MS100",solar,163,heatassistoff,heat assistance off,int8 (>=-12<=12),K,true,number.solar_heat_assistance_off,number.solar_heatassistoff,8,0,1/10,71,1 "SM200, MS200",solar,164,collectortemp,collector temperature (TS1),int16 (>=-3199<=3199),C,false,sensor.solar_collector_temperature_(TS1),sensor.solar_collectortemp,8,0,1/10,0,1 "SM200, MS200",solar,164,cylbottomtemp,cylinder bottom temperature (TS2),int16 (>=-3199<=3199),C,false,sensor.solar_cylinder_bottom_temperature_(TS2),sensor.solar_cylbottomtemp,8,0,1/10,1,1 "SM200, MS200",solar,164,solarpump,pump (PS1),boolean, ,false,binary_sensor.solar_pump_(PS1),binary_sensor.solar_solarpump,8,0,1,2,1 @@ -5590,6 +5594,8 @@ device name,device type,product id,shortname,fullname,type [options...] \| (min/ "SM200, MS200",solar,164,heatcnt,heat counter impulses,uint8 (>=0<=254), ,false,sensor.solar_heat_counter_impulses,sensor.solar_heatcnt,8,0,1,67,1 "SM200, MS200",solar,164,swapflowtemp,swap flow temperature (TS14),uint16 (>=0<=3199),C,false,sensor.solar_swap_flow_temperature_(TS14),sensor.solar_swapflowtemp,8,0,1/10,68,1 "SM200, MS200",solar,164,swaprettemp,swap return temperature (TS15),uint16 (>=0<=3199),C,false,sensor.solar_swap_return_temperature_(TS15),sensor.solar_swaprettemp,8,0,1/10,69,1 +"SM200, MS200",solar,164,heatassiston,heat assistance on,int8 (>=-12<=12),K,true,number.solar_heat_assistance_on,number.solar_heatassiston,8,0,1/10,70,1 +"SM200, MS200",solar,164,heatassistoff,heat assistance off,int8 (>=-12<=12),K,true,number.solar_heat_assistance_off,number.solar_heatassistoff,8,0,1/10,71,1 "HP Module",heatpump,252,airhumidity,relative air humidity,uint8 (>=0<=100),%,false,sensor.heatpump_relative_air_humidity,sensor.heatpump_airhumidity,9,0,1,0,1 "HP Module",heatpump,252,dewtemperature,dew point temperature,uint8 (>=0<=254),C,false,sensor.heatpump_dew_point_temperature,sensor.heatpump_dewtemperature,9,0,1,1,1 "HP Module",heatpump,252,curflowtemp,current flow temperature,int16 (>=-3199<=3199),C,false,sensor.heatpump_current_flow_temperature,sensor.heatpump_curflowtemp,9,0,1/10,2,1 diff --git a/interface/src/app/main/Sensors.tsx b/interface/src/app/main/Sensors.tsx index e79fb23c0..747034730 100644 --- a/interface/src/app/main/Sensors.tsx +++ b/interface/src/app/main/Sensors.tsx @@ -586,6 +586,7 @@ const Sensors = () => { creating={creating} selectedItem={selectedAnalogSensor} analogGPIOList={sensorData.available_gpios} + disabledTypeList={sensorData.exclude_types} validator={analogSensorItemValidation(sensorData.as, selectedAnalogSensor)} /> )} diff --git a/interface/src/app/main/SensorsAnalogDialog.tsx b/interface/src/app/main/SensorsAnalogDialog.tsx index 4e94bf42a..c6af79319 100644 --- a/interface/src/app/main/SensorsAnalogDialog.tsx +++ b/interface/src/app/main/SensorsAnalogDialog.tsx @@ -35,6 +35,7 @@ interface DashboardSensorsAnalogDialogProps { creating: boolean; selectedItem: AnalogSensor; analogGPIOList: number[]; + disabledTypeList: number[]; validator: Schema; } @@ -45,6 +46,7 @@ const SensorsAnalogDialog = ({ creating, selectedItem, analogGPIOList, + disabledTypeList, validator }: DashboardSensorsAnalogDialogProps) => { const { LL } = useI18nContext(); @@ -66,7 +68,16 @@ const SensorsAnalogDialog = ({ // Memoize helper functions to check sensor type conditions const isCounterOrRate = useMemo( - () => editItem.t >= AnalogType.COUNTER && editItem.t <= AnalogType.RATE, + () => + editItem.t === AnalogType.COUNTER || + editItem.t === AnalogType.RATE || + (editItem.t >= AnalogType.CNT_0 && editItem.t <= AnalogType.CNT_2), + [editItem.t] + ); + const isCounter = useMemo( + () => + editItem.t === AnalogType.COUNTER || + (editItem.t >= AnalogType.CNT_0 && editItem.t <= AnalogType.CNT_2), [editItem.t] ); const isFreqType = useMemo( @@ -80,13 +91,13 @@ const SensorsAnalogDialog = ({ editItem.t === AnalogType.PWM_2, [editItem.t] ); - const isDigitalOutGPIO = useMemo( + const isDACOutGPIO = useMemo( () => editItem.t === AnalogType.DIGITAL_OUT && (editItem.g === 25 || editItem.g === 26), [editItem.t, editItem.g] ); - const isDigitalOutNonGPIO = useMemo( + const isDigitalOutGPIO = useMemo( () => editItem.t === AnalogType.DIGITAL_OUT && editItem.g !== 25 && @@ -98,7 +109,11 @@ const SensorsAnalogDialog = ({ const analogTypeMenuItems = useMemo( () => AnalogTypeNames.map((val, i) => ( - + {val} )), @@ -264,7 +279,7 @@ const SensorsAnalogDialog = ({ /> )} - {editItem.t === AnalogType.COUNTER && ( + {isCounter && ( )} - {isDigitalOutGPIO && ( + {isDACOutGPIO && ( )} - {isDigitalOutNonGPIO && ( + {isDigitalOutGPIO && ( <> { > OFF ERR + WARN NOTICE INFO - DEBUG ALL @@ -602,6 +602,7 @@ const ApplicationSettings = () => { {LL.DISABLED(1)} LAN8720 TLK110 + RTL8201 diff --git a/lib/esp32-psram/README.md b/lib/esp32-psram/README.md new file mode 100644 index 000000000..be9b161be --- /dev/null +++ b/lib/esp32-psram/README.md @@ -0,0 +1,91 @@ +# ESP32-PSRAM Library + +[![Arduino Library](https://img.shields.io/badge/Arduino-Library-blue.svg)](https://www.arduino.cc/reference/en/libraries/) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + +A comprehensive memory management library for ESP32 microcontrollers, providing efficient data structures that leverage __PSRAM__ and __HIMEM__ for expanded storage capabilities. + + +## Overview + +The ESP32 can only address 4MB of PSRAM directly, so the remaining 4MB (HIMEM) are usually unsued! + +__esp32-psram__ is a C++ library that helps you manage large amounts of data on ESP32 devices by utilizing the full external PSRAM (SPI RAM) supporting the high memory (HIMEM) regions. This allows your applications to work with much larger datasets than would be possible using just internal RAM. + +- **Use any STL Class in PSRAM**: + - Adapt any [STL class](https://en.wikipedia.org/wiki/Standard_Template_Library) that accepts with an Allocator by using the [AllocatorPSRAM](https://pschatzmann.github.io/esp32-psram/html/classesp32__psram_1_1_p_s_r_a_m_allocator.html) + +- **Memory-Efficient Data Structures**: + - `VectorPSRAM`: Vector implementation that automatically stores data in PSRAM + - `VectorHIMEM`: Vector implementation for extremely large datasets using ESP32's high memory region + +- **File System Abstractions**: + - `FilePSRAM`: File-like interface backed by PSRAM + - `FileHIMEM`: File-like interface backed by high memory + - SD card-like API using familiar file operations to write to PSRAM or HIMEM. + +- **Streaming Data Handling**: + - `RingBufferStreamRAM`: Circular buffer implementation in RAM (Stream-based) + - `RingBufferStreamPSRAM`: Circular buffer implementation in PSRAM (Stream-based) + - `RingBufferStreamHIMEM`: Circular buffer implementation in high memory (Stream-based) + - Fully compatible with Arduino's Stream class + +- **Typed Ring Buffers**: + - `TypedRingBufferRAM`: Type-safe circular buffer for any data type using RAM + - `TypedRingBufferPSRAM`: PSRAM version for storing complex data structures + - `TypedRingBufferHIMEM`: High memory version for storing complex data structures + - Optimized for struct/class storage with proper memory management + + +## Installation + +1. Download the latest release from GitHub +2. Extract the ZIP file +3. Move the folder to your Arduino libraries directory (typically `~/Arduino/libraries/` on Linux/macOS or `Documents\Arduino\libraries\` on Windows) +4. Restart Arduino IDE + + +## API Reference + +- [Class Reference](https://pschatzmann.github.io/esp32-psram/html/namespaceesp32__psram.html) +- [Further Information](https://github.com/pschatzmann/esp32-psram/wiki) + +## Memory Performance Comparison + +| Memory Type | Access Speed | Capacity | Use Case | +|-------------|--------------|----------|----------| +| RAM (DRAM) | Very Fast | ~200-300KB | Small, performance-critical data | +| PSRAM | Fast | Up to 4MB | Medium-sized datasets, frequent access | +| HIMEM | Medium | Up to 8MB | Large datasets, less frequent access | + +## Hardware Compatibility + +| ESP32 Board | PSRAM Size | HIMEM Support | Notes | +|-------------|------------|--------------|-------| +| ESP32 | Up to 4MB | Yes | Most common ESP32 modules | +| ESP32-S2 | Up to 2MB | Limited | Check board specifications | +| ESP32-S3 | Up to 8MB | Yes | Best memory capabilities | +| ESP32-C3 | None | No | Not compatible with this library | + +## Design Philosophy + +The ESP32-PSRAM library is designed with these principles in mind: + +1. **Familiar interfaces**: API design mimics standard C++ containers and Arduino libraries +2. **Memory efficiency**: Automatic use of appropriate memory regions +3. **Type safety**: Template-based to work with any data type +4. **Flexibility**: Configurable for different memory types and use cases +5. **Zero overhead**: Direct memory access with minimal abstraction layers + + +## License + +MIT License + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +## Acknowledgements + +This library was inspired by the need for better memory management solutions on ESP32, especially for audio, image processing, and data logging applications. diff --git a/lib/esp32-psram/library.properties b/lib/esp32-psram/library.properties new file mode 100644 index 000000000..f8c80f991 --- /dev/null +++ b/lib/esp32-psram/library.properties @@ -0,0 +1,10 @@ +name=ESP32-PSRAM +version=0.1.3 +author=Phil Schatzmann +maintainer=https://github.com/pschatzmann +sentence=Memory management library for ESP32 using PSRAM and HIMEM +paragraph=A comprehensive memory management library that provides vector, file, and ring buffer implementations using ESP32's PSRAM and HIMEM. Allows applications to work with large datasets beyond internal memory limitations. +category=Data Storage +url=https://github.com/pschatzmann/esp32-psram +architectures=esp32 +depends= diff --git a/lib/esp32-psram/license.txt b/lib/esp32-psram/license.txt new file mode 100644 index 000000000..376642271 --- /dev/null +++ b/lib/esp32-psram/license.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Phil Schatzmann + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/lib/esp32-psram/src/esp32-psram.h b/lib/esp32-psram/src/esp32-psram.h new file mode 100644 index 000000000..a69d9f4cd --- /dev/null +++ b/lib/esp32-psram/src/esp32-psram.h @@ -0,0 +1,31 @@ +#pragma once + +#ifdef ESP32 +/** + * @file ESP32-PSRAM.h + * @brief Main include file for the ESP32-PSRAM library + * + * This header includes all components of the ESP32-PSRAM library and + * provides a using namespace directive for easier access to library features. + */ + +// Include all library components +#include "esp32-psram/AllocatorPSRAM.h" // PSRAM-backed vector +#include "esp32-psram/VectorPSRAM.h" // PSRAM-backed vector +#include "esp32-psram/VectorHIMEM.h" // HIMEM-backed vector +// #include "esp32-psram/InMemoryFile.h" // File interface using vectors +// #include "esp32-psram/PSRAM.h" // PSRAM file system +// #include "esp32-psram/HIMEM.h" // HIMEM file system +#include "esp32-psram/RingBufferStream.h" // Stream-based ring buffer +#include "esp32-psram/TypedRingBuffer.h" // Typed ring buffer for structured data + +#define stringPSRAM std::basic_string, AllocatorPSRAM> + +#ifndef ESP32_PSRAM_NO_NAMESPACE +using namespace esp32_psram; +#endif + +#else +#error "This library is only compatible with ESP32 platforms." +#endif // ESP32 + diff --git a/lib/esp32-psram/src/esp32-psram/AllocatorPSRAM.h b/lib/esp32-psram/src/esp32-psram/AllocatorPSRAM.h new file mode 100644 index 000000000..e18c7cc65 --- /dev/null +++ b/lib/esp32-psram/src/esp32-psram/AllocatorPSRAM.h @@ -0,0 +1,159 @@ +#pragma once + +#include + +/** + * @namespace esp32_psram + * @brief Namespace containing ESP32 PSRAM-specific implementations + */ +namespace esp32_psram { + +/** + * @class AllocatorPSRAM + * @brief Custom allocator that uses ESP32's PSRAM for memory allocation + * @tparam T Type of elements to allocate + * + * This allocator uses ESP32's heap_caps_malloc with MALLOC_CAP_SPIRAM flag + * to ensure all memory is allocated in PSRAM instead of regular RAM. If PSRAM + * allocation fails, it falls back to regular RAM. + */ +template +class AllocatorPSRAM { + public: + using value_type = T; + using pointer = T*; + using const_pointer = const T*; + using reference = T&; + using const_reference = const T&; + using size_type = std::size_t; + using difference_type = std::ptrdiff_t; + + /** + * @brief Default constructor + */ + AllocatorPSRAM() noexcept {} + + /** + * @brief Copy constructor from another allocator type + * @tparam U Type of the other allocator + * @param other The other allocator + */ + template + AllocatorPSRAM(const AllocatorPSRAM&) noexcept {} + + /** + * @brief Allocate memory from PSRAM + * @param n Number of elements to allocate + * @return Pointer to allocated memory + * @throws std::bad_alloc If allocation fails or size is too large + */ + pointer allocate(size_type n) { + // if (n > std::numeric_limits::max() / sizeof(T)) + // throw std::bad_alloc(); + // in Arduino excepitons are disabled! + assert(n <= std::numeric_limits::max() / sizeof(T)); + + pointer p = static_cast( + heap_caps_malloc(n * sizeof(T), MALLOC_CAP_SPIRAM)); + if (p == nullptr) { + p = static_cast(malloc(n * sizeof(T))); + } + + // if (!p) throw std::bad_alloc(); + + // in Arduino excepitons are disabled! + assert(p); + + return p; + } + + /** + * @brief Deallocate memory + * @param p Pointer to memory to deallocate + * @param size Size of allocation (unused) + */ + void deallocate(pointer p, size_type) noexcept { heap_caps_free(p); } + + /** + * @brief Rebind allocator to another type + * @tparam U Type to rebind the allocator to + */ + template + struct rebind { + using other = AllocatorPSRAM; + }; +}; + +/** + * @class AllocatorOnlyPSRAM + * @brief Custom allocator that uses ESP32's PSRAM for memory allocation + * @tparam T Type of elements to allocate + * + * This allocator uses ESP32's heap_caps_malloc with MALLOC_CAP_SPIRAM flag + * to ensure all memory is allocated in PSRAM instead of regular RAM. + * If PSRAM allocation fails, it does not fall back to regular RAM. + */ +template +class AllocatorOnlyPSRAM { + public: + using value_type = T; + using pointer = T*; + using const_pointer = const T*; + using reference = T&; + using const_reference = const T&; + using size_type = std::size_t; + using difference_type = std::ptrdiff_t; + + /** + * @brief Default constructor + */ + AllocatorOnlyPSRAM() noexcept {} + + /** + * @brief Copy constructor from another allocator type + * @tparam U Type of the other allocator + * @param other The other allocator + */ + template + AllocatorOnlyPSRAM(const AllocatorOnlyPSRAM&) noexcept {} + + /** + * @brief Allocate memory from PSRAM + * @param n Number of elements to allocate + * @return Pointer to allocated memory + * @throws std::bad_alloc If allocation fails or size is too large + */ + pointer allocate(size_type n) { + // if (n > std::numeric_limits::max() / sizeof(T)) + // throw std::bad_alloc(); + // in Arduino excepitons are disabled! + assert(n <= std::numeric_limits::max() / sizeof(T)); + + pointer p = static_cast( + heap_caps_malloc(n * sizeof(T), MALLOC_CAP_SPIRAM)); + // if (!p) throw std::bad_alloc(); + + // in Arduino excepitons are disabled! + assert(p); + + return p; + } + + /** + * @brief Deallocate memory + * @param p Pointer to memory to deallocate + * @param size Size of allocation (unused) + */ + void deallocate(pointer p, size_type) noexcept { heap_caps_free(p); } + + /** + * @brief Rebind allocator to another type + * @tparam U Type to rebind the allocator to + */ + template + struct rebind { + using other = AllocatorOnlyPSRAM; + }; +}; + +} // namespace esp32_psram \ No newline at end of file diff --git a/lib/esp32-psram/src/esp32-psram/HIMEM.h b/lib/esp32-psram/src/esp32-psram/HIMEM.h new file mode 100644 index 000000000..0b9efe4c4 --- /dev/null +++ b/lib/esp32-psram/src/esp32-psram/HIMEM.h @@ -0,0 +1,54 @@ +#pragma once + +#include +#include "InMemoryFS.h" +#include "VectorHIMEM.h" + +namespace esp32_psram { + +/** + * @class HIMEMClass + * @brief Class for managing files stored in ESP32's High Memory (HIMEM) + * + * This class provides an interface similar to SD.h for managing files + * that are stored in HIMEM memory (beyond the 4MB boundary) rather than on an SD card. + * HIMEM offers larger storage capacity but slightly slower access than regular PSRAM. + */ +class HIMEMClass : public InMemoryFS, FileHIMEM> { +public: + /** + * @brief Initialize the HIMEM filesystem + * @return true if initialization was successful, false otherwise + */ + bool begin() override { + if (esp_himem_get_free_size() > 0) { + initialized = true; + return true; + } + initialized = false; + return false; + } + + /** + * @brief Get total space (returns available HIMEM) + * @return Total HIMEM size in bytes + */ + uint64_t totalBytes() override { + return esp_himem_get_phys_size(); + } + + /** + * @brief Get free space (returns free HIMEM) + * @return Free HIMEM size in bytes + */ + uint64_t freeBytes() override { + return esp_himem_get_free_size(); + } +}; + +/** + * @brief Global instance of HIMEMClass for easy access + */ +static HIMEMClass HIMEM; + +} // namespace esp32_psram \ No newline at end of file diff --git a/lib/esp32-psram/src/esp32-psram/HimemBlock.h b/lib/esp32-psram/src/esp32-psram/HimemBlock.h new file mode 100644 index 000000000..c6eb7d68a --- /dev/null +++ b/lib/esp32-psram/src/esp32-psram/HimemBlock.h @@ -0,0 +1,362 @@ +#pragma once + +#include +#include +#include +#include +// ESP32 HIMEM headers - using conditional inclusion for compatibility +#if __has_include("esp32/himem.h") +#include "esp32/himem.h" +#elif __has_include("esp_himem.h") +#include "esp_himem.h" +#else +// Fall back to Arduino ESP32 core path +extern "C" { +#include "esp32/himem.h" +} + +// Define missing constants if needed +#ifndef ESP_HIMEM_BLKSZ +#define ESP_HIMEM_BLKSZ (32 * 1024) +#endif +#endif + +// Make sure ESP_HIMEM_PROT_RW is defined +#ifndef ESP_HIMEM_PROT_RW +#define ESP_HIMEM_PROT_RW 0 +#endif + +namespace esp32_psram { + +static constexpr const char* TAG = "HIMEM"; // Tag for ESP logging + +/** + * @class HimemBlock + * @brief Manages a block of himem memory with mapping functionality + */ +class HimemBlock { + public: + /** + * @brief Default constructor + */ + HimemBlock() { ESP_LOGD(TAG, "HimemBlock constructor called"); } + + /** + * @brief Allocate a block of himem + * @param block_size Size of memory to allocate in bytes + * @return the allocated (block) size in bytes, 0 otherwise + */ + size_t allocate(size_t block_size) { + ESP_LOGI(TAG, "HimemBlock::allocate(%u bytes) - Current handle: %p", + block_size, handle); + + if (handle != 0) { + ESP_LOGW(TAG, "Cannot allocate: Block already allocated"); + return false; // Already allocated + } + + // Round up to the nearest multiple of ESP_HIMEM_BLKSZ (32K) + block_size = ((block_size + ESP_HIMEM_BLKSZ - 1) / ESP_HIMEM_BLKSZ) * + ESP_HIMEM_BLKSZ; + + size = block_size; + esp_err_t err = esp_himem_alloc(block_size, &handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "HIMEM allocation failed - error: %d, size: %d", err, + block_size); + return 0; + } + + ESP_LOGD(TAG, "- Successfully allocated %u bytes, handle: %p", block_size, + handle); + return block_size; + } + + /** + * @brief Read data from HIMEM at specified offset + * @param dest Destination buffer + * @param offset Offset in HIMEM to read from + * @param length Number of bytes to read + * @return Number of bytes actually read + */ + size_t read(void* dest, size_t offset, size_t length) { + ESP_LOGD(TAG, "HimemBlock::read(dst=%p, offset=%u, length=%u)", dest, + offset, length); + + if (!handle || offset >= size) { + ESP_LOGW(TAG, "Read failed: %s", + !handle ? "Invalid handle" : "Offset beyond size"); + return 0; + } + + // Ensure we don't read past the end + length = std::min(length, size - offset); + if (length == 0) { + ESP_LOGW(TAG, "Read failed: Zero length after bounds check"); + return 0; + } + + // Calculate which block this belongs to + size_t block_index = offset / ESP_HIMEM_BLKSZ; + size_t block_offset = offset % ESP_HIMEM_BLKSZ; + size_t bytes_read = 0; + + ESP_LOGD(TAG, "- Reading from block %u, offset %u", block_index, + block_offset); + uint8_t* dest_ptr = static_cast(dest); + + while (bytes_read < length) { + // Ensure the correct block is mapped + if (!ensure_block_mapped(block_index)) { + return bytes_read; + } + + // Calculate how much to read from this block + size_t block_remain = ESP_HIMEM_BLKSZ - block_offset; + size_t to_read = std::min(block_remain, length - bytes_read); + ESP_LOGD(TAG, "- Reading %u bytes from window at offset %u", to_read, + block_offset); + + // Copy the data + memcpy(dest_ptr + bytes_read, + static_cast(mapped_ptr) + block_offset, to_read); + + bytes_read += to_read; + block_index++; + block_offset = 0; // Reset offset for next blocks + } + + ESP_LOGD(TAG, "- Successfully read %u bytes", bytes_read); + return bytes_read; + } + + /** + * @brief Write data to HIMEM at specified offset + * @param src Source buffer + * @param offset Offset in HIMEM to write to + * @param length Number of bytes to write + * @return Number of bytes actually written + */ + size_t write(const void* src, size_t offset, size_t length) { + ESP_LOGD(TAG, "HimemBlock::write(src=%p, offset=%u, length=%u)", src, + offset, length); + + if (!handle || offset >= size) { + ESP_LOGW(TAG, "Write failed: %s", + !handle ? "Invalid handle" : "Offset beyond size"); + return 0; + } + + // Ensure we don't write past the end + length = std::min(length, size - offset); + if (length == 0) { + ESP_LOGW(TAG, "Write failed: Zero length after bounds check"); + return 0; + } + + // Calculate which block this belongs to + size_t block_index = offset / ESP_HIMEM_BLKSZ; + size_t block_offset = offset % ESP_HIMEM_BLKSZ; + size_t bytes_written = 0; + + ESP_LOGD(TAG, "- Writing to block %u, offset %u", block_index, + block_offset); + const uint8_t* src_ptr = static_cast(src); + + while (bytes_written < length) { + // Ensure the correct block is mapped + if (!ensure_block_mapped(block_index)) { + return bytes_written; + } + + // Calculate how much to write to this block + size_t block_remain = ESP_HIMEM_BLKSZ - block_offset; + size_t to_write = std::min(block_remain, length - bytes_written); + ESP_LOGD(TAG, "- Writing %u bytes to window at offset %u", to_write, + block_offset); + + // Copy the data + memcpy(static_cast(mapped_ptr) + block_offset, + src_ptr + bytes_written, to_write); + + bytes_written += to_write; + block_index++; + block_offset = 0; // Reset offset for next blocks + } + + ESP_LOGD(TAG, "- Successfully wrote %u bytes", bytes_written); + return bytes_written; + } + + bool getAddress(size_t offset, void* &result, size_t &available) { + size_t block_index = offset / ESP_HIMEM_BLKSZ; + size_t block_offset = offset % ESP_HIMEM_BLKSZ; + if (!ensure_block_mapped(block_index)) return false; + available = ESP_HIMEM_BLKSZ - block_offset; + result = static_cast(mapped_ptr)+block_offset; + return true; + } + + /** + * @brief Unmap the himem block + */ + void unmap() { + ESP_LOGD(TAG, "HimemBlock::unmap() - mapped_ptr=%p, range=%p", mapped_ptr, + range); + + if (mapped_ptr && range) { + ESP_LOGD(TAG, "- Unmapping memory and freeing range"); + esp_himem_unmap(range, mapped_ptr, ESP_HIMEM_BLKSZ); + esp_himem_free_map_range(range); + mapped_ptr = nullptr; + range = 0; + current_mapped_block = SIZE_MAX; // Reset currently mapped block + ESP_LOGD(TAG, "- Unmapped successfully"); + } else { + ESP_LOGD(TAG, "- Nothing to unmap"); + } + } + + /** + * @brief Free the himem block + */ + void free() { + ESP_LOGD(TAG, "HimemBlock::free() - handle=%p", handle); + + if (handle) { + ESP_LOGD(TAG, "- Unmapping before freeing"); + unmap(); + ESP_LOGD(TAG, "- Freeing HIMEM handle %p", handle); + esp_himem_free(handle); + handle = 0; + size = 0; + ESP_LOGD(TAG, " - Successfully freed HIMEM block"); + } else { + ESP_LOGD(TAG, "- Nothing to free"); + } + } + + /** + * @brief Get the size of the allocated block + * @return Size of the allocated block in bytes + */ + size_t get_size() const { + ESP_LOGD(TAG, "HimemBlock::get_size() = %u", size); + return size; + } + + /** + * @brief Destructor - ensures memory is properly freed + */ + ~HimemBlock() { + ESP_LOGD(TAG, "HimemBlock destructor called"); + free(); + } + + /** + * @brief Copy constructor (deleted) + */ + HimemBlock(const HimemBlock&) = delete; + + /** + * @brief Move constructor + */ + HimemBlock(HimemBlock&& other) noexcept + : handle(other.handle), + range(other.range), + mapped_ptr(other.mapped_ptr), + size(other.size), + current_mapped_block(other.current_mapped_block) { + ESP_LOGD(TAG, "HimemBlock move constructor - moving handle=%p, size=%u", + other.handle, other.size); + other.handle = 0; + other.range = 0; + other.mapped_ptr = nullptr; + other.size = 0; + other.current_mapped_block = SIZE_MAX; + } + + /** + * @brief Copy assignment operator (deleted) + */ + HimemBlock& operator=(const HimemBlock&) = delete; + + /** + * @brief Move assignment operator + */ + HimemBlock& operator=(HimemBlock&& other) noexcept { + ESP_LOGD(TAG, "HimemBlock move assignment - from handle=%p to handle=%p", + other.handle, handle); + + if (this != &other) { + ESP_LOGD(TAG, "Freeing current resources before move assignment"); + free(); + handle = other.handle; + range = other.range; + mapped_ptr = other.mapped_ptr; + size = other.size; + current_mapped_block = other.current_mapped_block; + ESP_LOGD(TAG, "Moved resources, new size=%u", size); + other.handle = 0; + other.range = 0; + other.mapped_ptr = nullptr; + other.size = 0; + other.current_mapped_block = SIZE_MAX; + } else { + ESP_LOGD(TAG, "Self-assignment detected, no action taken"); + } + return *this; + } + + protected: + esp_himem_handle_t handle = 0; + esp_himem_rangehandle_t range = 0; + void* mapped_ptr = nullptr; + size_t size = 0; + size_t current_mapped_block = + SIZE_MAX; // Track which block is currently mapped + + /** + * @brief Ensure a specific block is mapped into memory + * @param block_index The index of the block to map + * @return true if mapping successful, false otherwise + */ + bool ensure_block_mapped(size_t block_index) { + // If the requested block is already mapped, we're done + if (block_index == current_mapped_block) { + return true; + } + + // Unmap previous block if any + if (mapped_ptr) { + ESP_LOGD(TAG, "- Unmapping block %u before mapping new block %u", + current_mapped_block, block_index); + unmap(); // Unmap previous block + } + + // Allocate map range + ESP_LOGD(TAG, "- Allocating map range for block %u", block_index); + esp_err_t err = esp_himem_alloc_map_range(ESP_HIMEM_BLKSZ, &range); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to allocate map range: %d", err); + return false; + } + + // Map the current block + ESP_LOGD(TAG, "- Mapping block %u (offset %u)", block_index, + block_index * ESP_HIMEM_BLKSZ); + err = esp_himem_map(handle, range, block_index * ESP_HIMEM_BLKSZ, 0, + ESP_HIMEM_BLKSZ, ESP_HIMEM_PROT_RW, &mapped_ptr); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to map memory: %d", err); + esp_himem_free_map_range(range); + range = 0; + return false; + } + + current_mapped_block = block_index; + return true; + } +}; + +} // namespace esp32_psram diff --git a/lib/esp32-psram/src/esp32-psram/InMemoryFS.h_ b/lib/esp32-psram/src/esp32-psram/InMemoryFS.h_ new file mode 100644 index 000000000..f12e3a2a0 --- /dev/null +++ b/lib/esp32-psram/src/esp32-psram/InMemoryFS.h_ @@ -0,0 +1,251 @@ +#pragma once + +#include + +#include + +#include "InMemoryFile.h" + +// Define Arduino file mode constants if not already defined +#ifndef FILE_READ +#define FILE_READ 0 +#endif +#ifndef FILE_WRITE +#define FILE_WRITE 1 +#endif +#ifndef FILE_APPEND +#define FILE_APPEND 2 +#endif + +namespace esp32_psram { + +/** + * @class InMemoryFS + * @brief Base class for memory-based file systems like PSRAM and HIMEM + * + * This class provides a common interface for in-memory file systems, + * with methods for file management and traversal. + * + * @tparam VectorType The vector implementation to use (VectorPSRAM or + * VectorHIMEM) + * @tparam FileType The file implementation to return (FilePSRAM or FileHIMEM) + */ +template +class InMemoryFS { + public: + /** + * @brief Initialize the file system + * @return true if initialization was successful, false otherwise + */ + virtual bool begin() = 0; + + /** + * @brief Check if a file exists + * @param filename Name of the file to check + * @return true if the file exists, false otherwise + */ + bool exists(const char* filename) { + if (!initialized) return false; + return fileData.find(std::string(filename)) != fileData.end(); + } + + /** + * @brief Open a file + * @param filename Name of the file to open + * @param mode Mode to open the file in (FILE_READ, FILE_WRITE, etc.) + * @return A file object for the opened file + */ + FileType open(const char* filename, uint8_t mode) { + ESP_LOGD("InMemoryFS", "Opening file %s with mode %d", filename, mode); + + if (!initialized) { + ESP_LOGW("InMemoryFS", "Filesystem not initialized"); + FileType emptyFile; + return emptyFile; + } + + // Convert Arduino file modes to our enum + FileMode fileMode; + if (mode == FILE_READ) { + fileMode = FileMode::READ; + } else if (mode == FILE_WRITE) { + fileMode = FileMode::WRITE; + } else if (mode == FILE_APPEND) { + fileMode = FileMode::APPEND; + } else { + fileMode = FileMode::READ_WRITE; + } + + std::string filenameStr(filename); + auto it = fileData.find(filenameStr); + FileType file; + + if (it != fileData.end()) { + // File exists, create a new file pointing to existing data + ESP_LOGD("InMemoryFS", "File exists, connecting to existing data"); + file.setVector(&(it->second)); + } else if (mode != FILE_READ) { + // File doesn't exist, create it for writing or appending + ESP_LOGD("InMemoryFS", "Creating new file for writing"); + fileData[filenameStr] = VectorType(); // Create empty vector in the map + file.setVector(&(fileData[filenameStr])); + } else { + // File doesn't exist and mode is READ + ESP_LOGW("InMemoryFS", "File doesn't exist and mode is READ"); + return file; // Return empty file + } + + // Configure the file + file.setName(filename); + file.open(fileMode); + + // Set up the next file callback + file.setNextFileCallback( + [this](const char* currentFileName, FileMode currentMode) -> FileType { + ESP_LOGD("InMemoryFS", "NextFile callback called for %s", + currentFileName); + + // Get the next filename + String nextName = this->getNextFileName(currentFileName); + if (nextName.isEmpty()) { + ESP_LOGD("InMemoryFS", "No next file found"); + return FileType(); // Return empty file if no next file + } + + // Convert FileMode to Arduino mode + uint8_t arduinoMode; + if (currentMode == FileMode::READ) + arduinoMode = FILE_READ; + else if (currentMode == FileMode::WRITE) + arduinoMode = FILE_WRITE; + else if (currentMode == FileMode::APPEND) + arduinoMode = FILE_APPEND; + else + arduinoMode = FILE_READ | FILE_WRITE; + + // Open and return the next file + ESP_LOGD("InMemoryFS", "Returning next file: %s", nextName.c_str()); + return this->open(nextName.c_str(), arduinoMode); + }); + + ESP_LOGD("InMemoryFS", "File opened successfully"); + return file; + } + + /** + * @brief Remove a file + * @param filename Name of the file to remove + * @return true if the file was removed, false otherwise + */ + bool remove(const char* filename) { + if (!initialized) return false; + + std::string filenameStr(filename); + auto it = fileData.find(filenameStr); + + if (it != fileData.end()) { + fileData.erase(it); + return true; + } + + return false; + } + + /** + * @brief Create a directory (no-op for compatibility) + * @param dirname Name of the directory + * @return Always returns true for compatibility + */ + bool mkdir(const char* dirname) { + // No directories in this implementation, just for compatibility + return true; + } + + /** + * @brief Remove a directory (no-op for compatibility) + * @param dirname Name of the directory + * @return Always returns true for compatibility + */ + bool rmdir(const char* dirname) { + // No directories in this implementation, just for compatibility + return true; + } + + /** + * @brief Get the name of the next file after the specified file + * @param currentFileName Name of the current file + * @return Name of the next file, or empty string if there are no more files + */ + String getNextFileName(const char* currentFileName) { + if (!initialized || fileData.empty()) { + return String(); + } + + if (currentFileName == nullptr || strlen(currentFileName) == 0 || + strcmp(currentFileName, "/") == 0) { + // Return the first file if current is empty or is root directory + return String(fileData.begin()->first.c_str()); + } + + std::string current(currentFileName); + auto it = fileData.find(current); + + if (it == fileData.end()) { + // Current file not found, find the next one alphabetically + for (auto& entry : fileData) { + if (entry.first > current) { + return String(entry.first.c_str()); + } + } + // No file after the specified name + return String(); + } else { + // Found the current file, get the next one + ++it; + if (it != fileData.end()) { + return String(it->first.c_str()); + } else { + // Was the last file + return String(); + } + } + } + + /** + * @brief Get the first file in the filesystem + * @return Name of the first file, or empty string if there are no files + */ + String getFirstFileName() { + if (!initialized || fileData.empty()) { + return String(); + } + return String(fileData.begin()->first.c_str()); + } + + /** + * @brief Get the total number of files + * @return Number of files in the filesystem + */ + size_t fileCount() const { + if (!initialized) return 0; + return fileData.size(); + } + + /** + * @brief Get total space + * @return Total memory size in bytes + */ + virtual uint64_t totalBytes() = 0; + + /** + * @brief Get free space + * @return Free memory size in bytes + */ + virtual uint64_t freeBytes() = 0; + + protected: + bool initialized = false; + std::map fileData; +}; + +} // namespace esp32_psram \ No newline at end of file diff --git a/lib/esp32-psram/src/esp32-psram/InMemoryFile.h_ b/lib/esp32-psram/src/esp32-psram/InMemoryFile.h_ new file mode 100644 index 000000000..8904ef23b --- /dev/null +++ b/lib/esp32-psram/src/esp32-psram/InMemoryFile.h_ @@ -0,0 +1,421 @@ +#pragma once + +#include + +#include "VectorHIMEM.h" +#include "VectorPSRAM.h" + +namespace esp32_psram { + +// Define file modes +enum class FileMode { READ, WRITE, APPEND, READ_WRITE }; + +/** + * @class InMemoryFile + * @brief A file-like interface for vector-backed storage in memory + * + * This class provides a file-like interface (compatible with Arduino's Stream) + * for reading and writing data to vector-backed storage. It can use either + * standard PSRAM or ESP32's HIMEM (high memory beyond the 4MB boundary) + * as its underlying storage mechanism through the VectorPSRAM and VectorHIMEM + * implementations. + * + * InMemoryFile offers familiar file operations like open, close, read, write, + * seek, and truncate while managing the data in memory rather than on disk. + * This makes it useful for temporary storage, data processing, or situations + * where file operations are needed but filesystem access is not available + * or desirable. + * + * The class can either manage its own internal vector or reference an external + * vector supplied by another component (like PSRAMClass or HIMEMClass). + * + * @tparam VectorType The vector implementation to use for storage (typically + * VectorPSRAM or VectorHIMEM) + * + * @note The performance characteristics will depend on the underlying vector + * implementation. HIMEM operations are generally slower than regular + * PSRAM operations but allow for larger storage capacity. + */ +template +class InMemoryFile : public Stream { + public: + // Define a single callback type that returns the next file directly + using NextFileCallback = + std::function(const char*, FileMode)>; + /** + * @brief Default constructor - initializes with internal vector + */ + InMemoryFile() : data_ptr(&internal_data) { + ESP_LOGD(TAG, "InMemoryFile constructor called"); + } + + /** + * @brief Constructor with filename and mode + * @param filename Name of the file + * @param mode Mode to open the file in + */ + InMemoryFile(const char* filename, FileMode mode) + : data_ptr(&internal_data), name_(filename) { + ESP_LOGD(TAG, "InMemoryFile constructor with name and mode called"); + open(mode); + } + + /** + * @brief Destructor - ensures proper cleanup + */ + virtual ~InMemoryFile() { + ESP_LOGD(TAG, "InMemoryFile destructor called"); + close(); + if (!using_external_vector) { + data_ptr = nullptr; + } + } + + /** + * @brief Set the vector to use for this file + * @param vec Pointer to the vector to use + */ + void setVector(VectorType* vec) { + ESP_LOGD(TAG, "Setting external vector pointer: %p", vec); + if (vec) { + data_ptr = vec; + using_external_vector = true; + } else { + data_ptr = &internal_data; + using_external_vector = false; + } + } + + /** + * @brief Set the name of this file + * @param name The name to set + */ + void setName(const char* name) { + ESP_LOGD(TAG, "Setting file name: %s", name); + name_ = name; + } + + /** + * @brief Open the file with the specified mode + * @param mode Mode to open the file in + * @return true if opened successfully, false otherwise + */ + bool open(FileMode mode) { + ESP_LOGD(TAG, "Opening file '%s' with mode %d", name_.c_str(), + static_cast(mode)); + this->mode = mode; + + if (mode == FileMode::WRITE) { + // Clear the vector for writing + data_ptr->clear(); + position_ = 0; + } else if (mode == FileMode::APPEND) { + // Position at end for appending + position_ = data_ptr->size(); + } else { + // READ or READ_WRITE - position at beginning + position_ = 0; + } + + open_ = true; + return true; + } + + /** + * @brief Close the file + */ + void close() { + ESP_LOGD(TAG, "Closing file '%s'", name_.c_str()); + open_ = false; + } + + /** + * @brief Check if file is open + * @return true if open, false otherwise + */ + bool isOpen() const { return open_; } + + /** + * @brief Get the name of the file + * @return File name + */ + String name() const { return name_; } + + /** + * @brief Get the size of the file + * @return File size in bytes + */ + size_t size() const { return data_ptr->size(); } + + // Stream interface implementation + + /** + * @brief Read a single byte from the file + * @return The next byte, or -1 if no data is available + */ + int read() override { + ESP_LOGD(TAG, "InMemoryFile::read() - position %u", (unsigned)position_); + if (!open_ || (mode != FileMode::READ && mode != FileMode::READ_WRITE)) { + ESP_LOGE(TAG, "read failed: file not open for reading"); + return -1; + } + + if (position_ >= data_ptr->size()) { + ESP_LOGD(TAG, "read at EOF: pos=%u, size=%u", (unsigned)position_, + (unsigned)data_ptr->size()); + return -1; // EOF + } + + uint8_t byte = (*data_ptr)[position_++]; + return byte; + } + + /** + * @brief Read multiple bytes from the file + * @param buffer Buffer to store the read data + * @param size Maximum number of bytes to read + * @return Number of bytes actually read + */ + size_t readBytes(char* buffer, size_t size) override { + ESP_LOGD(TAG, "InMemoryFile::readBytes: %u", (unsigned)size); + if (!open_ || (mode != FileMode::READ && mode != FileMode::READ_WRITE)) { + ESP_LOGE(TAG, "read failed: file not open for reading"); + return 0; + } + + // Calculate how many bytes we can actually read + size_t available_bytes = data_ptr->size() - position_; + size_t bytes_to_read = min(size, available_bytes); + + // Read the bytes + for (size_t i = 0; i < bytes_to_read; i++) { + buffer[i] = (*data_ptr)[position_ + i]; + } + + position_ += bytes_to_read; + return bytes_to_read; + } + + /** + * @brief Get the number of bytes available to read + * @return Number of bytes available + */ + int available() override { + if (!open_) return 0; + return data_ptr->size() - position_; + } + + /** + * @brief Peek at the next byte without advancing the position + * @return The next byte, or -1 if no data is available + */ + int peek() override { + if (!open_ || (mode != FileMode::READ && mode != FileMode::READ_WRITE)) { + return -1; + } + + if (position_ >= data_ptr->size()) { + return -1; // EOF + } + + return (*data_ptr)[position_]; + } + + /** + * @brief Ensure all data is committed to the storage + * No-op for this implementation since data is in memory + */ + void flush() override { + // No-op for in-memory implementation + } + + /** + * @brief Write a single byte to the file + * @param b The byte to write + * @return 1 if the byte was written, 0 otherwise + */ + size_t write(uint8_t b) override { + ESP_LOGD(TAG, "InMemoryFile::write: 1 byte"); + if (!open_ || (mode != FileMode::WRITE && mode != FileMode::APPEND && + mode != FileMode::READ_WRITE)) { + ESP_LOGE(TAG, "write failed: file not open for writing"); + return 0; + } + + if (position_ >= data_ptr->size()) { + // Append to the end + data_ptr->push_back(b); + } else { + // Replace existing byte + (*data_ptr)[position_] = b; + } + + position_++; + return 1; + } + + /** + * @brief Write multiple bytes to the file + * @param buffer Buffer containing the data to write + * @param size Number of bytes to write + * @return Number of bytes actually written + */ + size_t write(const uint8_t* buffer, size_t size) override { + ESP_LOGD(TAG, "InMemoryFile::write: %u", (unsigned)size); + if (!open_ || (mode != FileMode::WRITE && mode != FileMode::APPEND && + mode != FileMode::READ_WRITE)) { + ESP_LOGE(TAG, "write failed: file not open for writing"); + return 0; + } + + if (position_ >= data_ptr->size()) { + // Append to the end + data_ptr->reserve(data_ptr->size() + size); // Optimize capacity + for (size_t i = 0; i < size; i++) { + data_ptr->push_back(buffer[i]); + } + } else { + // Replace/insert bytes + size_t space_available = data_ptr->size() - position_; + size_t bytes_to_replace = min(size, space_available); + size_t bytes_to_append = size - bytes_to_replace; + + // Replace existing bytes + for (size_t i = 0; i < bytes_to_replace; i++) { + (*data_ptr)[position_ + i] = buffer[i]; + } + + // Append remaining bytes + for (size_t i = 0; i < bytes_to_append; i++) { + data_ptr->push_back(buffer[bytes_to_replace + i]); + } + } + + position_ += size; + return size; + } + + // File-specific methods + + /** + * @brief Set the current position in the file + * @param pos The position to seek to + * @return true if successful, false otherwise + */ + bool seek(size_t pos) { + ESP_LOGD(TAG, "InMemoryFile::seek: %u", (unsigned)pos); + if (!open_ || pos > data_ptr->size()) { + ESP_LOGE(TAG, "seek failed: file not open or position beyond size"); + return false; + } + + position_ = pos; + return true; + } + + /** + * @brief Get the current position in the file + * @return Current position + */ + size_t position() const { return position_; } + + /** + * @brief Truncate the file to the current position + */ + void truncate() { + ESP_LOGD(TAG, "InMemoryFile::truncate at position %u", (unsigned)position_); + if (!open_ || (mode != FileMode::WRITE && mode != FileMode::READ_WRITE)) { + ESP_LOGE(TAG, "truncate failed: file not open for writing"); + return; + } + + if (position_ < data_ptr->size()) { + // Create a new vector with just the data up to position_ + VectorType new_data; + new_data.reserve(position_); + for (size_t i = 0; i < position_; i++) { + new_data.push_back((*data_ptr)[i]); + } + + // Replace the old data + *data_ptr = std::move(new_data); + } + } + + operator bool() const { return open_; } + /** + * @brief Set callback for navigating to the next file + * + * This function sets a callback that will be used to retrieve the next + * file when getNextFile() is called. The callback takes the current file name + * and mode, and returns the next file object directly. + * + * @param callback Function that returns the next file + */ + void setNextFileCallback(NextFileCallback callback) { + ESP_LOGD(TAG, "Setting next file callback"); + nextFileCallback = callback; + } + + /** + * @brief Get the next file in the filesystem + * + * This method uses the registered callback to get the next file + * after this one in the filesystem. + * + * @return The next file, or an empty file if there is no next file or + * callback isn't set + */ + InMemoryFile getNextFile() { + if (!nextFileCallback || name_.isEmpty()) { + // Return empty file if callback isn't set or no name + InMemoryFile emptyFile; + return emptyFile; + } + + // Get the next file directly using the callback + return nextFileCallback(name_.c_str(), mode); + } + + /** + * @brief Reserve storage + * @param new_cap The new capacity of the file + */ + bool reserve(size_t new_cap) { + if (data_ptr == nullptr) return false; + data_ptr->reserve(new_cap); + return true; + } + + /** + * @brief Get the number of bytes that can be held in current storage. + * Please note that this might dynamically grow! + * @return The capacity of the file + */ + size_t capacity() const { + if (data_ptr == nullptr) return 0; + return data_ptr->capacity(); + } + + private: + VectorType* data_ptr = + nullptr; // Pointer to vector data (internal or external) + VectorType internal_data; // Internal vector for standalone use + bool using_external_vector = false; + size_t position_ = 0; + bool open_ = false; + FileMode mode = FileMode::READ; + String name_; + + // Single callback for getting the next file + NextFileCallback nextFileCallback = nullptr; + + // Tags for debug logging + static constexpr const char* TAG = "InMemoryFile"; +}; + +// Type aliases for convenience +using FilePSRAM = InMemoryFile>; +using FileHIMEM = InMemoryFile>; + +} // namespace esp32_psram \ No newline at end of file diff --git a/lib/esp32-psram/src/esp32-psram/PSRAM.h_ b/lib/esp32-psram/src/esp32-psram/PSRAM.h_ new file mode 100644 index 000000000..e365c4fa2 --- /dev/null +++ b/lib/esp32-psram/src/esp32-psram/PSRAM.h_ @@ -0,0 +1,54 @@ +#pragma once + +#include +#include "InMemoryFS.h" +#include "VectorPSRAM.h" + + +namespace esp32_psram { + +/** + * @class PSRAMClass + * @brief Class for managing files stored in ESP32's PSRAM + * + * This class provides an interface similar to SD.h for managing files + * that are stored in PSRAM memory rather than on an SD card. + */ +class PSRAMClass : public InMemoryFS, FilePSRAM> { +public: + /** + * @brief Initialize the PSRAM filesystem + * @return true if initialization was successful, false otherwise + */ + bool begin() override { + if (ESP.getFreePsram() > 0) { + initialized = true; + return true; + } + initialized = false; + return false; + } + + /** + * @brief Get total space (returns available PSRAM) + * @return Total PSRAM size in bytes + */ + uint64_t totalBytes() override { + return ESP.getPsramSize(); + } + + /** + * @brief Get free space (returns free PSRAM) + * @return Free PSRAM size in bytes + */ + uint64_t freeBytes() override { + return ESP.getFreePsram(); + } +}; + +/** + * @brief Global instance of PSRAMClass for easy access + */ +static PSRAMClass PSRAM; + +} // namespace esp32_psram \ No newline at end of file diff --git a/lib/esp32-psram/src/esp32-psram/RingBufferStream.h b/lib/esp32-psram/src/esp32-psram/RingBufferStream.h new file mode 100644 index 000000000..b32046789 --- /dev/null +++ b/lib/esp32-psram/src/esp32-psram/RingBufferStream.h @@ -0,0 +1,250 @@ +#pragma once + +#include +#include +#include "VectorPSRAM.h" +#include "VectorHIMEM.h" + +namespace esp32_psram { + +/** + * @class RingBufferStream + * @brief A circular buffer implementation that extends Arduino's Stream + * @tparam VectorType The vector type to use as underlying storage + * + * This class implements a ring buffer (circular buffer) that uses a vector container + * for storage. It extends Arduino's Stream class to provide standard stream functionality. + * The buffer can be used with any vector type, including VectorPSRAM and VectorHIMEM. + */ +template +class RingBufferStream : public Stream { +private: + VectorType buffer; + size_t readIndex = 0; + size_t writeIndex = 0; + bool full = false; + size_t maxSize; + +public: + /** + * @brief Constructor with specified buffer size + * @param size The size of the buffer in bytes + */ + RingBufferStream(size_t size) : maxSize(size) { + buffer.resize(size); + } + + /** + * @brief Get the number of bytes available for reading + * @return Number of bytes available + */ + int available() override { + if (full) { + return maxSize; + } + if (writeIndex >= readIndex) { + return writeIndex - readIndex; + } else { + return maxSize - (readIndex - writeIndex); + } + } + + /** + * @brief Read a byte from the buffer + * @return The byte read, or -1 if the buffer is empty + */ + int read() override { + if (isEmpty()) { + return -1; + } + + uint8_t value = buffer[readIndex]; + readIndex = (readIndex + 1) % maxSize; + full = false; + return value; + } + + /** + * @brief Look at the next byte in the buffer without removing it + * @return The next byte to be read, or -1 if the buffer is empty + */ + int peek() override { + if (isEmpty()) { + return -1; + } + + return buffer[readIndex]; + } + + /** + * @brief Write a byte to the buffer + * @param value The byte to write + * @return 1 if the byte was written, 0 if the buffer is full + */ + size_t write(uint8_t value) override { + if (full) { + return 0; + } + + buffer[writeIndex] = value; + writeIndex = (writeIndex + 1) % maxSize; + + // Check if buffer is now full + if (writeIndex == readIndex) { + full = true; + } + + return 1; + } + + /** + * @brief Write multiple bytes to the buffer + * @param data Pointer to the data to write + * @param size Number of bytes to write + * @return Number of bytes written + */ + size_t write(const uint8_t *data, size_t size) { + size_t bytesWritten = 0; + + for (size_t i = 0; i < size; i++) { + if (write(data[i])) { + bytesWritten++; + } else { + break; + } + } + + return bytesWritten; + } + + /** + * @brief Clear the buffer, removing all content + */ + void flush() override { + readIndex = 0; + writeIndex = 0; + full = false; + } + + /** + * @brief Check if the buffer is empty + * @return true if the buffer is empty, false otherwise + */ + bool isEmpty() const { + return !full && (readIndex == writeIndex); + } + + /** + * @brief Check if the buffer is full + * @return true if the buffer is full, false otherwise + */ + bool isFull() const { + return full; + } + + /** + * @brief Get the number of bytes that can be written without blocking + * @return Number of bytes available for writing + */ + int availableForWrite() override { + if (full) { + return 0; + } + if (readIndex > writeIndex) { + return readIndex - writeIndex; + } else { + return maxSize - (writeIndex - readIndex); + } + } + + /** + * @brief Get the total capacity of the buffer + * @return Total buffer capacity in bytes + */ + size_t size() const { + return maxSize; + } + + /** + * @brief Get the number of bytes currently in the buffer + * @return Number of bytes in the buffer + */ + size_t used() const { + return available(); + } + + /** + * @brief Get the number of bytes still available in the buffer + * @return Number of free bytes in the buffer + */ + size_t free() const { + return maxSize - available(); + } + + /** + * @brief Read multiple bytes from the buffer + * @param buffer Buffer to store the read data + * @param size Maximum number of bytes to read + * @return Number of bytes actually read + */ + size_t readBytes(char* buffer, size_t size) override { + size_t bytesRead = 0; + + for (size_t i = 0; i < size; i++) { + int value = read(); + if (value >= 0) { + buffer[i] = (char)value; + bytesRead++; + } else { + break; + } + } + + return bytesRead; + } + + /** + * @brief Read multiple bytes from the buffer + * @param buffer Buffer to store the read data + * @param size Maximum number of bytes to read + * @return Number of bytes actually read + */ + size_t readBytes(uint8_t* buffer, size_t size) { + return readBytes(reinterpret_cast(buffer), size); + } + + /** + * @brief Get direct access to the underlying vector + * @return Reference to the underlying vector + */ + VectorType& getVector() { + return buffer; + } + + /** + * @brief Get const access to the underlying vector + * @return Const reference to the underlying vector + */ + const VectorType& getVector() const { + return buffer; + } +}; + +/** + * @brief Type alias for a RingBufferStream that uses PSRAM-backed vector storage + */ +using RingBufferStreamPSRAM = RingBufferStream>; + +/** + * @brief Type alias for a RingBufferStream that uses HIMEM-backed vector storage + */ +using RingBufferStreamHIMEM = RingBufferStream>; + +/** + * @brief Type alias for a RingBufferStream that uses std::vector storage + */ + +using RingBufferStreamRAM = RingBufferStream>; + + +} // namespace esp32_psram \ No newline at end of file diff --git a/lib/esp32-psram/src/esp32-psram/TypedRingBuffer.h b/lib/esp32-psram/src/esp32-psram/TypedRingBuffer.h new file mode 100644 index 000000000..84ec9ab21 --- /dev/null +++ b/lib/esp32-psram/src/esp32-psram/TypedRingBuffer.h @@ -0,0 +1,227 @@ + +#pragma once + +#include +#include +#include "VectorPSRAM.h" +#include "VectorHIMEM.h" + +namespace esp32_psram { + +/** + * @class TypedRingBuffer + * @brief A generic ring buffer implementation for any data type + * @tparam T The data type to store in the buffer + * @tparam VectorType The vector type to use as underlying storage + * + * This class implements a ring buffer (circular buffer) that uses a vector container + * for storage. Unlike the Stream-based RingBuffer, this can store any data type. + */ +template +class TypedRingBuffer { +private: + VectorType buffer; + size_t readIndex = 0; + size_t writeIndex = 0; + bool full = false; + size_t maxSize; + +public: + /** + * @brief Constructor with specified buffer capacity + * @param capacity The maximum number of elements the buffer can hold + */ + TypedRingBuffer(size_t capacity) : maxSize(capacity) { + buffer.resize(capacity); + } + + /** + * @brief Push an element to the buffer + * @param value The value to add + * @return true if the element was added, false if the buffer is full + */ + bool push(const T& value) { + if (full) { + return false; + } + + buffer[writeIndex] = value; + advanceWriteIndex(); + return true; + } + + /** + * @brief Push an element to the buffer, overwriting oldest data if full + * @param value The value to add + * @return true if an old element was overwritten, false otherwise + */ + bool pushOverwrite(const T& value) { + bool overwritten = full; + + buffer[writeIndex] = value; + advanceWriteIndex(); + + return overwritten; + } + + /** + * @brief Pop an element from the buffer + * @param value Reference to store the popped value + * @return true if an element was popped, false if the buffer is empty + */ + bool pop(T& value) { + if (isEmpty()) { + return false; + } + + value = buffer[readIndex]; + advanceReadIndex(); + return true; + } + + /** + * @brief Peek at the next element without removing it + * @param value Reference to store the peeked value + * @return true if an element was peeked, false if the buffer is empty + */ + bool peek(T& value) const { + if (isEmpty()) { + return false; + } + + value = buffer[readIndex]; + return true; + } + + /** + * @brief Get the element at a specific index relative to the read position + * @param index The relative index from current read position + * @param value Reference to store the value + * @return true if the element exists, false otherwise + */ + bool peekAt(size_t index, T& value) const { + if (isEmpty() || index >= available()) { + return false; + } + + size_t actualIndex = (readIndex + index) % maxSize; + value = buffer[actualIndex]; + return true; + } + + /** + * @brief Clear the buffer, removing all content + */ + void clear() { + readIndex = 0; + writeIndex = 0; + full = false; + } + + /** + * @brief Check if the buffer is empty + * @return true if the buffer is empty, false otherwise + */ + bool isEmpty() const { + return !full && (readIndex == writeIndex); + } + + /** + * @brief Check if the buffer is full + * @return true if the buffer is full, false otherwise + */ + bool isFull() const { + return full; + } + + /** + * @brief Get the number of elements in the buffer + * @return Number of elements in the buffer + */ + size_t available() const { + if (full) { + return maxSize; + } + if (writeIndex >= readIndex) { + return writeIndex - readIndex; + } else { + return maxSize - (readIndex - writeIndex); + } + } + + /** + * @brief Get the number of empty slots in the buffer + * @return Number of empty slots + */ + size_t availableForWrite() const { + return maxSize - available(); + } + + /** + * @brief Get the total capacity of the buffer + * @return Total buffer capacity + */ + size_t capacity() const { + return maxSize; + } + + /** + * @brief Get direct access to the underlying vector + * @return Reference to the underlying vector + */ + VectorType& getVector() { + return buffer; + } + + /** + * @brief Get const access to the underlying vector + * @return Const reference to the underlying vector + */ + const VectorType& getVector() const { + return buffer; + } + +private: + /** + * @brief Advance the write index + */ + void advanceWriteIndex() { + writeIndex = (writeIndex + 1) % maxSize; + if (writeIndex == readIndex) { + full = true; + } + } + + /** + * @brief Advance the read index + */ + void advanceReadIndex() { + readIndex = (readIndex + 1) % maxSize; + full = false; + } +}; + +/** + * @brief Type alias for a typed ring buffer that uses std::vector (in RAM) + */ +template +using TypedRingBufferRAM = TypedRingBuffer>; +/** + * @brief Type alias for a typed ring buffer that uses HIMEM-backed vector storage + */ +template +using TypedRingBufferHIMEM = TypedRingBuffer>; + +/** + * @brief Type alias for a typed ring buffer that uses PSRAM-backed vector storage + */ +template +using TypedRingBufferPSRAM = TypedRingBuffer>; + +/** + * @brief Type alias for a typed ring buffer that uses HIMEM-backed vector storage + */ +// template +// using TypedRingBufferHIMEM = TypedRingBuffer>; + +} // namespace esp32_psram \ No newline at end of file diff --git a/lib/esp32-psram/src/esp32-psram/VectorHIMEM.h b/lib/esp32-psram/src/esp32-psram/VectorHIMEM.h new file mode 100644 index 000000000..ed00d27d4 --- /dev/null +++ b/lib/esp32-psram/src/esp32-psram/VectorHIMEM.h @@ -0,0 +1,529 @@ +#pragma once + +#include "HimemBlock.h" + +namespace esp32_psram { + + +/** + * @class VectorHIMEM + * @brief Vector implementation that uses ESP32's high memory (himem) for + * storage + * @tparam T Type of elements stored in the vector + */ +template +class VectorHIMEM { + public: + // Type definitions + using value_type = T; + using reference = T&; + using const_reference = const T&; + using pointer = T*; + using const_pointer = const T*; + using size_type = size_t; + using difference_type = std::ptrdiff_t; + + /** + * @brief Default constructor - creates an empty vector + */ + VectorHIMEM() = default; + + /** + * @brief Constructs a vector with the given number of default-initialized + * elements + * @param count The size of the vector + */ + explicit VectorHIMEM(size_type count) { resize(count); } + + /** + * @brief Constructs a vector with the given number of copies of a value + * @param count The size of the vector + * @param value The value to initialize elements with + */ + VectorHIMEM(size_type count, const T& value) { resize(count, value); } + + /** + * @brief Copy constructor + * @param other The vector to copy from + */ + VectorHIMEM(const VectorHIMEM& other) { + if (other.element_count > 0) { + if (reallocate(other.element_count)) { + T temp; + VectorHIMEM& other_ = const_cast(other); + for (size_t i = 0; i < other.element_count; ++i) { + other_.memory.read((void*)&temp, i * sizeof(T), sizeof(T)); + memory.write((void*)&temp, i * sizeof(T), sizeof(T)); + } + element_count = other.element_count; + } + } + } + + /** + * @brief Move constructor + * @param other The vector to move from + */ + VectorHIMEM(VectorHIMEM&& other) noexcept + : memory(std::move(other.memory)), + element_count(other.element_count), + element_capacity(other.element_capacity) { + other.element_count = 0; + other.element_capacity = 0; + } + + /** + * @brief Initializer list constructor + * @param init The initializer list to copy from + */ + VectorHIMEM(std::initializer_list init) { + if (init.size() > 0) { + if (reallocate(init.size())) { + size_t i = 0; + for (const auto& item : init) { + memory.write(&item, i * sizeof(T), sizeof(T)); + ++i; + } + element_count = init.size(); + } + } + } + + /** + * @brief Destructor - ensures all elements are properly destroyed + */ + ~VectorHIMEM() { clear(); } + + /** + * @brief Copy assignment operator + * @param other The vector to copy from + * @return Reference to this vector + */ + VectorHIMEM& operator=(const VectorHIMEM& other) { + if (this != &other) { + clear(); + if (other.element_count > 0) { + if (reallocate(other.element_count)) { + T temp; + for (size_t i = 0; i < other.element_count; ++i) { + const_cast(&other.memory)->read(&temp, i * sizeof(T), sizeof(T)); + memory.write(&temp, i * sizeof(T), sizeof(T)); + } + element_count = other.element_count; + } + } + } + return *this; + } + + /** + * @brief Move assignment operator + * @param other The vector to move from + * @return Reference to this vector + */ + VectorHIMEM& operator=(VectorHIMEM&& other) noexcept { + if (this != &other) { + clear(); + memory = std::move(other.memory); + element_count = other.element_count; + element_capacity = other.element_capacity; + other.element_count = 0; + other.element_capacity = 0; + } + return *this; + } + + /** + * @brief Initializer list assignment operator + * @param ilist The initializer list to copy from + * @return Reference to this vector + */ + VectorHIMEM& operator=(std::initializer_list ilist) { + clear(); + if (ilist.size() > 0) { + if (reallocate(ilist.size())) { + size_t i = 0; + for (const auto& item : ilist) { + memory.write(&item, i * sizeof(T), sizeof(T)); + ++i; + } + element_count = ilist.size(); + } + } + return *this; + } + + /** + * @brief Access element with bounds checking + * @param pos The position of the element + * @return Reference to the element at position pos + * @throws std::out_of_range if pos is not within the range of the vector + */ + reference at(size_type pos) { + if (pos >= element_count) { + throw std::out_of_range("VectorHIMEM: index out of range"); + } + static T temp; + memory.read(&temp, pos * sizeof(T), sizeof(T)); + return temp; + } + + /** + * @brief Access element with bounds checking (const version) + * @param pos The position of the element + * @return Const reference to the element at position pos + * @throws std::out_of_range if pos is not within the range of the vector + */ + const_reference at(size_type pos) const { + if (pos >= element_count) { + throw std::out_of_range("VectorHIMEM: index out of range"); + } + static T temp; + memory.read(&temp, pos * sizeof(T), sizeof(T)); + return temp; + } + + /** + * @brief Access element without bounds checking + * @param pos The position of the element + * @return Reference to the element at position pos + */ + reference operator[](size_type pos) { + static T temp; + memory.read(&temp, pos * sizeof(T), sizeof(T)); + return temp; + } + + // Add const version of operator[] + const_reference operator[](size_type pos) const { + static T result; + // Need to cast away const for the read operation since it doesn't modify content + HimemBlock& non_const_memory = const_cast(memory); + non_const_memory.read(&result, pos * sizeof(T), sizeof(T)); + return result; + } + + + /** + * @brief Access the first element + * @return Reference to the first element + */ + reference front() { + static T temp; + memory.read(&temp, 0, sizeof(T)); + return temp; + } + + /** + * @brief Access the first element (const version) + * @return Const reference to the first element + */ + const_reference front() const { + static T temp; + memory.read(&temp, 0, sizeof(T)); + return temp; + } + + /** + * @brief Access the last element + * @return Reference to the last element + */ + reference back() { + static T temp; + memory.read(&temp, (element_count - 1) * sizeof(T), sizeof(T)); + return temp; + } + + /** + * @brief Access the last element (const version) + * @return Const reference to the last element + */ + const_reference back() const { + static T temp; + memory.read(&temp, (element_count - 1) * sizeof(T), sizeof(T)); + return temp; + } + + /** + * @brief Check if the vector is empty + * @return true if the vector is empty, false otherwise + */ + bool empty() const { return element_count == 0; } + + /** + * @brief Get the number of elements + * @return The number of elements in the vector + */ + size_type size() const { return element_count; } + + /** + * @brief Get the maximum possible number of elements + * @return The maximum possible number of elements the vector can hold + */ + size_type max_size() const { + return std::numeric_limits::max() / sizeof(T); + } + + /** + * @brief Reserve storage + * @param new_cap The new capacity of the vector + */ + void reserve(size_type new_cap) { + if (new_cap > element_capacity) { + reallocate(new_cap); + } + } + + /** + * @brief Get the number of elements that can be held in current storage + * @return The capacity of the vector + */ + size_type capacity() const { return element_capacity; } + + /** + * @brief Clear the contents + */ + void clear() { + // We're using plain memory so no destructors to call + element_count = 0; + } + + /** + * @brief Add an element to the end + * @param value The value to append + */ + void push_back(const T& value) { + if (element_count >= element_capacity) { + size_t new_capacity = + element_capacity == 0 ? min_elements : element_capacity * 2; + if (!reallocate(new_capacity)) { + ESP_LOGE(TAG, "Failed to reallocate for push_back"); + return; + } + } + + // Write the new element at the end + memory.write(&value, element_count * sizeof(T), sizeof(T)); + ++element_count; + } + + /** + * @brief Add an element to the end by moving it + * @param value The value to append + */ + void push_back(T&& value) { + // For POD types, this is the same as the const& version + push_back(static_cast(value)); + } + + /** + * @brief Remove the last element + */ + void pop_back() { + if (element_count > 0) { + --element_count; + } + } + + /** + * @brief Change the number of elements stored + * @param count The new size of the vector + */ + void resize(size_type count) { + if (count > element_capacity) { + if (!reallocate(count)) { + return; + } + } + element_count = count; + } + + /** + * @brief Change the number of elements stored + * @param count The new size of the vector + * @param value The value to initialize new elements with + */ + void resize(size_type count, const value_type& value) { + size_t old_size = element_count; + + if (count > element_count) { + // Need to add elements + if (count > element_capacity) { + if (!reallocate(count)) { + return; + } + } + + // Initialize new elements with value + for (size_t i = old_size; i < count; ++i) { + memory.write(&value, i * sizeof(T), sizeof(T)); + } + } + + element_count = count; + } + + /** + * @brief Erase an element + * @param pos Index of the element to erase + */ + void erase(size_type pos) { + if (pos >= element_count) { + return; + } + + // Move each element down by one, overwriting the erased element + T temp; + for (size_t i = pos + 1; i < element_count; ++i) { + memory.read(&temp, i * sizeof(T), sizeof(T)); + memory.write(&temp, (i - 1) * sizeof(T), sizeof(T)); + } + + --element_count; + } + + /** + * @brief Insert an element at a specific position + * @param pos Position where the element should be inserted + * @param value The value to insert + */ + void insert(size_type pos, const T& value) { + if (pos > element_count) { + return; + } + + if (element_count >= element_capacity) { + size_t new_capacity = + element_capacity == 0 ? min_elements : element_capacity * 2; + if (!reallocate(new_capacity)) { + return; + } + } + + // Move elements up to make space + T temp; + for (size_t i = element_count; i > pos; --i) { + memory.read(&temp, (i - 1) * sizeof(T), sizeof(T)); + memory.write(&temp, i * sizeof(T), sizeof(T)); + } + + // Insert new element + memory.write(&value, pos * sizeof(T), sizeof(T)); + ++element_count; + } + + /** + * @brief Swap the contents of this vector with another + * @param other Vector to swap with + */ + void swap(VectorHIMEM& other) noexcept { + std::swap(memory, other.memory); + std::swap(element_count, other.element_count); + std::swap(element_capacity, other.element_capacity); + } + + protected: + HimemBlock memory; + size_t element_count = 0; + size_t element_capacity = 0; + static constexpr size_t min_elements = 16; // Minimum allocation size + + /** + * @brief Calculate required memory size in bytes for a given number of + * elements + * @param count Number of elements + * @return Size in bytes + */ + static size_t calculate_size_bytes(size_t count) { return count * sizeof(T); } + + /** + * @brief Reallocate memory with a new capacity + * @param new_capacity The new capacity to allocate + * @return true if reallocation was successful, false otherwise + */ + bool reallocate(size_t new_capacity) { + if (new_capacity <= element_capacity) { + return true; // No need to reallocate + } + + // Calculate new size (at least min_elements) + new_capacity = std::max(new_capacity, min_elements); + + // Create a new memory block + HimemBlock new_memory; + element_capacity = new_memory.allocate(calculate_size_bytes(new_capacity)) / sizeof(T); + if (element_capacity == 0) { + ESP_LOGE(TAG, "Failed to allocate new memory block"); + return false; + } + + // Copy existing elements if any + if (element_count > 0) { + T temp; + for (size_t i = 0; i < element_count; ++i) { + // Read from old memory + memory.read(&temp, i * sizeof(T), sizeof(T)); + + // Write to new memory + new_memory.write(&temp, i * sizeof(T), sizeof(T)); + } + } + + // Swap the memory blocks + memory = std::move(new_memory); + + return true; + } + + +}; + +/** + * @brief Equality comparison operator + * @tparam T Type of elements in the vectors + * @param lhs First vector + * @param rhs Second vector + * @return true if the vectors are equal, false otherwise + */ +template +bool operator==(const VectorHIMEM& lhs, const VectorHIMEM& rhs) { + if (lhs.size() != rhs.size()) { + return false; + } + + T lhs_val, rhs_val; + for (size_t i = 0; i < lhs.size(); ++i) { + lhs_val = lhs[i]; // Uses operator[] which calls read() + rhs_val = rhs[i]; + if (!(lhs_val == rhs_val)) { + return false; + } + } + + return true; +} + +/** + * @brief Inequality comparison operator + * @tparam T Type of elements in the vectors + * @param lhs First vector + * @param rhs Second vector + * @return true if the vectors are not equal, false otherwise + */ +template +bool operator!=(const VectorHIMEM& lhs, const VectorHIMEM& rhs) { + return !(lhs == rhs); +} + +/** + * @brief Swap the contents of two vectors + * @tparam T Type of elements in the vectors + * @param lhs First vector + * @param rhs Second vector + */ +template +void swap(VectorHIMEM& lhs, VectorHIMEM& rhs) noexcept { + lhs.swap(rhs); +} + +} // namespace esp32_psram \ No newline at end of file diff --git a/lib/esp32-psram/src/esp32-psram/VectorPSRAM.h b/lib/esp32-psram/src/esp32-psram/VectorPSRAM.h new file mode 100644 index 000000000..07bfce6c8 --- /dev/null +++ b/lib/esp32-psram/src/esp32-psram/VectorPSRAM.h @@ -0,0 +1,521 @@ +#pragma once + +#include +#include +#include +#include +#include "AllocatorPSRAM.h" + +/** + * @namespace esp32_psram + * @brief Namespace containing ESP32 PSRAM-specific implementations + */ +namespace esp32_psram { + + +/** + * @brief Equality comparison operator for allocators + * @tparam T Type of first allocator + * @tparam U Type of second allocator + * @return Always true since all instances are equivalent + */ +template +bool operator==(const AllocatorPSRAM&, const AllocatorPSRAM&) noexcept { + return true; +} + +/** + * @brief Inequality comparison operator for allocators + * @tparam T Type of first allocator + * @tparam U Type of second allocator + * @return Always false since all instances are equivalent + */ +template +bool operator!=(const AllocatorPSRAM&, const AllocatorPSRAM&) noexcept { + return false; +} + +/** + * @class VectorPSRAM + * @brief Vector implementation that uses ESP32's PSRAM for storage + * @tparam T Type of elements stored in the vector + * + * This class provides an interface identical to std::vector but allocates + * all memory in ESP32's PSRAM, which helps preserve the limited internal RAM. + * It wraps std::vector with a custom allocator that uses PSRAM. + */ +template +class VectorPSRAM { +private: + using vector_type = std::vector>; + vector_type vec; + +public: + // Type definitions + using value_type = typename vector_type::value_type; + using allocator_type = typename vector_type::allocator_type; + using size_type = typename vector_type::size_type; + using difference_type = typename vector_type::difference_type; + using reference = typename vector_type::reference; + using const_reference = typename vector_type::const_reference; + using pointer = typename vector_type::pointer; + using const_pointer = typename vector_type::const_pointer; + using iterator = typename vector_type::iterator; + using const_iterator = typename vector_type::const_iterator; + using reverse_iterator = typename vector_type::reverse_iterator; + using const_reverse_iterator = typename vector_type::const_reverse_iterator; + + /** + * @brief Default constructor - creates an empty vector + */ + VectorPSRAM() : vec(AllocatorPSRAM()) {} + + /** + * @brief Constructs a vector with the given number of default-initialized elements + * @param count The size of the vector + */ + explicit VectorPSRAM(size_type count) : vec(count, AllocatorPSRAM()) {} + + /** + * @brief Constructs a vector with the given number of copies of a value + * @param count The size of the vector + * @param value The value to initialize elements with + */ + VectorPSRAM(size_type count, const T& value) : vec(count, value, AllocatorPSRAM()) {} + + /** + * @brief Constructs a vector with the contents of the range [first, last) + * @tparam InputIt Input iterator type + * @param first Iterator to the first element in the range + * @param last Iterator to one past the last element in the range + */ + template + VectorPSRAM(InputIt first, InputIt last) : vec(first, last, AllocatorPSRAM()) {} + + /** + * @brief Copy constructor + * @param other The vector to copy from + */ + VectorPSRAM(const VectorPSRAM& other) : vec(other.vec) {} + + /** + * @brief Move constructor + * @param other The vector to move from + */ + VectorPSRAM(VectorPSRAM&& other) noexcept : vec(std::move(other.vec)) {} + + /** + * @brief Initializer list constructor + * @param init The initializer list to copy from + */ + VectorPSRAM(std::initializer_list init) : vec(init, AllocatorPSRAM()) {} + + /** + * @brief Copy assignment operator + * @param other The vector to copy from + * @return Reference to this vector + */ + VectorPSRAM& operator=(const VectorPSRAM& other) { + vec = other.vec; + return *this; + } + + /** + * @brief Move assignment operator + * @param other The vector to move from + * @return Reference to this vector + */ + VectorPSRAM& operator=(VectorPSRAM&& other) noexcept { + vec = std::move(other.vec); + return *this; + } + + /** + * @brief Initializer list assignment operator + * @param ilist The initializer list to copy from + * @return Reference to this vector + */ + VectorPSRAM& operator=(std::initializer_list ilist) { + vec = ilist; + return *this; + } + + /** + * @brief Access element with bounds checking + * @param pos The position of the element + * @return Reference to the element at position pos + * @throws std::out_of_range if pos is not within the range of the vector + */ + reference at(size_type pos) { return vec.at(pos); } + + /** + * @brief Access element with bounds checking (const version) + * @param pos The position of the element + * @return Const reference to the element at position pos + * @throws std::out_of_range if pos is not within the range of the vector + */ + const_reference at(size_type pos) const { return vec.at(pos); } + + /** + * @brief Access element without bounds checking + * @param pos The position of the element + * @return Reference to the element at position pos + */ + reference operator[](size_type pos) { return vec[pos]; } + + /** + * @brief Access element without bounds checking (const version) + * @param pos The position of the element + * @return Const reference to the element at position pos + */ + const_reference operator[](size_type pos) const { return vec[pos]; } + + /** + * @brief Access the first element + * @return Reference to the first element + */ + reference front() { return vec.front(); } + + /** + * @brief Access the first element (const version) + * @return Const reference to the first element + */ + const_reference front() const { return vec.front(); } + + /** + * @brief Access the last element + * @return Reference to the last element + */ + reference back() { return vec.back(); } + + /** + * @brief Access the last element (const version) + * @return Const reference to the last element + */ + const_reference back() const { return vec.back(); } + + /** + * @brief Get pointer to the underlying array + * @return Pointer to the underlying array + */ + T* data() noexcept { return vec.data(); } + + /** + * @brief Get pointer to the underlying array (const version) + * @return Const pointer to the underlying array + */ + const T* data() const noexcept { return vec.data(); } + + /** + * @brief Get iterator to the beginning + * @return Iterator to the first element + */ + iterator begin() noexcept { return vec.begin(); } + + /** + * @brief Get const iterator to the beginning + * @return Const iterator to the first element + */ + const_iterator begin() const noexcept { return vec.begin(); } + + /** + * @brief Get const iterator to the beginning + * @return Const iterator to the first element + */ + const_iterator cbegin() const noexcept { return vec.cbegin(); } + + /** + * @brief Get iterator to the end + * @return Iterator to one past the last element + */ + iterator end() noexcept { return vec.end(); } + + /** + * @brief Get const iterator to the end + * @return Const iterator to one past the last element + */ + const_iterator end() const noexcept { return vec.end(); } + + /** + * @brief Get const iterator to the end + * @return Const iterator to one past the last element + */ + const_iterator cend() const noexcept { return vec.cend(); } + + /** + * @brief Get reverse iterator to the beginning + * @return Reverse iterator to the first element + */ + reverse_iterator rbegin() noexcept { return vec.rbegin(); } + + /** + * @brief Get const reverse iterator to the beginning + * @return Const reverse iterator to the first element + */ + const_reverse_iterator rbegin() const noexcept { return vec.rbegin(); } + + /** + * @brief Get const reverse iterator to the beginning + * @return Const reverse iterator to the first element + */ + const_reverse_iterator crbegin() const noexcept { return vec.crbegin(); } + + /** + * @brief Get reverse iterator to the end + * @return Reverse iterator to one past the last element + */ + reverse_iterator rend() noexcept { return vec.rend(); } + + /** + * @brief Get const reverse iterator to the end + * @return Const reverse iterator to one past the last element + */ + const_reverse_iterator rend() const noexcept { return vec.rend(); } + + /** + * @brief Get const reverse iterator to the end + * @return Const reverse iterator to one past the last element + */ + const_reverse_iterator crend() const noexcept { return vec.crend(); } + + /** + * @brief Check if the vector is empty + * @return true if the vector is empty, false otherwise + */ + bool empty() const noexcept { return vec.empty(); } + + /** + * @brief Get the number of elements + * @return The number of elements in the vector + */ + size_type size() const noexcept { return vec.size(); } + + /** + * @brief Get the maximum possible number of elements + * @return The maximum possible number of elements the vector can hold + */ + size_type max_size() const noexcept { return vec.max_size(); } + + /** + * @brief Reserve storage + * @param new_cap The new capacity of the vector + * @throws std::length_error if new_cap > max_size() + */ + void reserve(size_type new_cap) { vec.reserve(new_cap); } + + /** + * @brief Get the number of elements that can be held in current storage + * @return The capacity of the vector + */ + size_type capacity() const noexcept { return vec.capacity(); } + + /** + * @brief Reduce memory usage by freeing unused memory + */ + void shrink_to_fit() { vec.shrink_to_fit(); } + + /** + * @brief Clear the contents + */ + void clear() noexcept { vec.clear(); } + + /** + * @brief Insert an element + * @param pos Iterator to the position before which the element will be inserted + * @param value The value to insert + * @return Iterator to the inserted element + */ + iterator insert(const_iterator pos, const T& value) { return vec.insert(pos, value); } + + /** + * @brief Insert an element by moving it + * @param pos Iterator to the position before which the element will be inserted + * @param value The value to insert + * @return Iterator to the inserted element + */ + iterator insert(const_iterator pos, T&& value) { return vec.insert(pos, std::move(value)); } + + /** + * @brief Insert multiple copies of an element + * @param pos Iterator to the position before which the elements will be inserted + * @param count Number of copies to insert + * @param value The value to insert + * @return Iterator to the first inserted element + */ + iterator insert(const_iterator pos, size_type count, const T& value) { return vec.insert(pos, count, value); } + + /** + * @brief Insert elements from a range + * @tparam InputIt Input iterator type + * @param pos Iterator to the position before which the elements will be inserted + * @param first Iterator to the first element in the range + * @param last Iterator to one past the last element in the range + * @return Iterator to the first inserted element + */ + template + iterator insert(const_iterator pos, InputIt first, InputIt last) { return vec.insert(pos, first, last); } + + /** + * @brief Insert elements from an initializer list + * @param pos Iterator to the position before which the elements will be inserted + * @param ilist The initializer list to insert from + * @return Iterator to the first inserted element + */ + iterator insert(const_iterator pos, std::initializer_list ilist) { return vec.insert(pos, ilist); } + + /** + * @brief Construct an element in-place + * @tparam Args Types of arguments to forward to the constructor + * @param pos Iterator to the position before which the element will be constructed + * @param args Arguments to forward to the constructor + * @return Iterator to the inserted element + */ + template + iterator emplace(const_iterator pos, Args&&... args) { return vec.emplace(pos, std::forward(args)...); } + + /** + * @brief Erase an element + * @param pos Iterator to the element to erase + * @return Iterator to the element after the erased element + */ + iterator erase(const_iterator pos) { return vec.erase(pos); } + + /** + * @brief Erase a range of elements + * @param first Iterator to the first element to erase + * @param last Iterator to one past the last element to erase + * @return Iterator to the element after the erased range + */ + iterator erase(const_iterator first, const_iterator last) { return vec.erase(first, last); } + + /** + * @brief Add an element to the end + * @param value The value to append + */ + void push_back(const T& value) { vec.push_back(value); } + + /** + * @brief Add an element to the end by moving it + * @param value The value to append + */ + void push_back(T&& value) { vec.push_back(std::move(value)); } + + /** + * @brief Construct an element in-place at the end + * @tparam Args Types of arguments to forward to the constructor + * @param args Arguments to forward to the constructor + * @return Reference to the inserted element + */ + template + reference emplace_back(Args&&... args) { return vec.emplace_back(std::forward(args)...); } + + /** + * @brief Remove the last element + */ + void pop_back() { vec.pop_back(); } + + /** + * @brief Change the number of elements stored + * @param count The new size of the vector + */ + void resize(size_type count) { vec.resize(count); } + + /** + * @brief Change the number of elements stored + * @param count The new size of the vector + * @param value The value to initialize new elements with + */ + void resize(size_type count, const value_type& value) { vec.resize(count, value); } + + /** + * @brief Swap the contents + * @param other Vector to swap with + */ + void swap(VectorPSRAM& other) noexcept { vec.swap(other.vec); } +}; + +/** + * @brief Equality comparison operator + * @tparam T Type of elements in the vectors + * @param lhs First vector + * @param rhs Second vector + * @return true if the vectors are equal, false otherwise + */ +template +bool operator==(const VectorPSRAM& lhs, const VectorPSRAM& rhs) { + return std::equal(lhs.begin(), lhs.end(), rhs.begin(), rhs.end()); +} + +/** + * @brief Inequality comparison operator + * @tparam T Type of elements in the vectors + * @param lhs First vector + * @param rhs Second vector + * @return true if the vectors are not equal, false otherwise + */ +template +bool operator!=(const VectorPSRAM& lhs, const VectorPSRAM& rhs) { + return !(lhs == rhs); +} + +/** + * @brief Less than comparison operator + * @tparam T Type of elements in the vectors + * @param lhs First vector + * @param rhs Second vector + * @return true if lhs is lexicographically less than rhs, false otherwise + */ +template +bool operator<(const VectorPSRAM& lhs, const VectorPSRAM& rhs) { + return std::lexicographical_compare(lhs.begin(), lhs.end(), rhs.begin(), rhs.end()); +} + +/** + * @brief Less than or equal comparison operator + * @tparam T Type of elements in the vectors + * @param lhs First vector + * @param rhs Second vector + * @return true if lhs is lexicographically less than or equal to rhs, false otherwise + */ +template +bool operator<=(const VectorPSRAM& lhs, const VectorPSRAM& rhs) { + return !(rhs < lhs); +} + +/** + * @brief Greater than comparison operator + * @tparam T Type of elements in the vectors + * @param lhs First vector + * @param rhs Second vector + * @return true if lhs is lexicographically greater than rhs, false otherwise + */ +template +bool operator>(const VectorPSRAM& lhs, const VectorPSRAM& rhs) { + return rhs < lhs; +} + +/** + * @brief Greater than or equal comparison operator + * @tparam T Type of elements in the vectors + * @param lhs First vector + * @param rhs Second vector + * @return true if lhs is lexicographically greater than or equal to rhs, false otherwise + */ +template +bool operator>=(const VectorPSRAM& lhs, const VectorPSRAM& rhs) { + return !(lhs < rhs); +} + +/** + * @brief Swap the contents of two vectors + * @tparam T Type of elements in the vectors + * @param lhs First vector + * @param rhs Second vector + */ +template +void swap(VectorPSRAM& lhs, VectorPSRAM& rhs) noexcept { + lhs.swap(rhs); +} + +} // namespace esp32_psram + diff --git a/lib_standalone/esp32-psram.h b/lib_standalone/esp32-psram.h new file mode 100644 index 000000000..76c02826f --- /dev/null +++ b/lib_standalone/esp32-psram.h @@ -0,0 +1,4 @@ +#pragma once + +#define stringPSRAM std::string +#define AllocatorPSRAM std::allocator diff --git a/src/core/analogsensor.cpp b/src/core/analogsensor.cpp index 7d54b6e43..9d4919f3a 100644 --- a/src/core/analogsensor.cpp +++ b/src/core/analogsensor.cpp @@ -21,7 +21,8 @@ namespace emsesp { -uuid::log::Logger AnalogSensor::logger_{F_(analogsensor), uuid::log::Facility::DAEMON}; +uuid::log::Logger AnalogSensor::logger_{F_(analogsensor), uuid::log::Facility::DAEMON}; +std::vector AnalogSensor::exclude_types_; #ifndef EMSESP_STANDALONE portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED; @@ -30,32 +31,37 @@ unsigned long AnalogSensor::edgecnt[] = {0, 0, 0}; void IRAM_ATTR AnalogSensor::freqIrq0() { portENTER_CRITICAL_ISR(&mux); - edgecnt[0]++; - edge[0] = micros(); + if (micros() - edge[0] > 10) { // limit to 100kHz + edgecnt[0]++; + edge[0] = micros(); + } portEXIT_CRITICAL_ISR(&mux); } void IRAM_ATTR AnalogSensor::freqIrq1() { portENTER_CRITICAL_ISR(&mux); - edgecnt[1]++; - edge[1] = micros(); + if (micros() - edge[1] > 10) { // limit to 100kHz + edgecnt[1]++; + edge[1] = micros(); + } portEXIT_CRITICAL_ISR(&mux); } void IRAM_ATTR AnalogSensor::freqIrq2() { portENTER_CRITICAL_ISR(&mux); - edgecnt[2]++; - edge[2] = micros(); + if (micros() - edge[2] > 10) { // limit to 100kHz + edgecnt[2]++; + edge[2] = micros(); + } portEXIT_CRITICAL_ISR(&mux); } #endif void AnalogSensor::start(const bool factory_settings) { - // if (factory_settings && EMSESP::nvs_.getString("boot").equals("E32V2_2") && EMSESP::nvs_.getString("hwrevision").equals("3.0")) { + // if (factory_settings && EMSESP::nvs_.getString("boot").equals("E32V2_2")) { if (factory_settings && analogReadMilliVolts(39) > 700) { // core voltage > 2.6V EMSESP::webCustomizationService.update([&](WebCustomization & settings) { auto newSensor = AnalogCustomization(); - + strcpy(newSensor.name, "core_voltage"); newSensor.gpio = 39; - newSensor.name = "core_voltage"; newSensor.offset = 0; newSensor.factor = 0.00377136; // Divider 24k - 8,66k newSensor.uom = DeviceValueUOM::VOLTS; @@ -63,8 +69,8 @@ void AnalogSensor::start(const bool factory_settings) { newSensor.is_system = true; settings.analogCustomizations.push_back(newSensor); + strcpy(newSensor.name, "supply_voltage"); newSensor.gpio = 36; - newSensor.name = "supply_voltage"; newSensor.factor = 0.017; // Divider 24k - 1,5k newSensor.is_system = true; settings.analogCustomizations.push_back(newSensor); @@ -97,6 +103,7 @@ void AnalogSensor::start(const bool factory_settings) { // load settings from the customization file, sorts them and initializes the GPIOs void AnalogSensor::reload(bool get_nvs) { + exclude_types_.clear(); EMSESP::webSettingsService.read([&](WebSettings & settings) { analog_enabled_ = settings.analog_enabled; }); #if defined(EMSESP_STANDALONE) @@ -122,13 +129,13 @@ void AnalogSensor::reload(bool get_nvs) { for (const auto & sensor : settings.analogCustomizations) { // search customlist if (sensor_.gpio() == sensor.gpio) { // for output sensors set value to new start-value - if (sensor.type >= AnalogType::DIGITAL_OUT && sensor.type <= AnalogType::PWM_2 + if (((sensor.type >= AnalogType::DIGITAL_OUT && sensor.type <= AnalogType::PWM_2) || sensor.type >= AnalogType::RGB) && (sensor_.type() != sensor.type || sensor_.offset() != sensor.offset || sensor_.factor() != sensor.factor)) { sensor_.set_value(sensor.offset); } - if (sensor.type == AnalogType::COUNTER && sensor_.offset() != sensor.offset - && sensor.offset != EMSESP::nvs_.getDouble(sensor.name.c_str(), 0)) { - EMSESP::nvs_.putDouble(sensor.name.c_str(), sensor.offset); + if ((sensor.type == AnalogType::COUNTER || (sensor.type >= AnalogType::CNT_0 && sensor.type <= AnalogType::CNT_2)) + && sensor_.offset() != sensor.offset && sensor.offset != EMSESP::nvs_.getDouble(sensor.name, 0)) { + EMSESP::nvs_.putDouble(sensor.name, sensor.offset); sensor_.set_value(sensor.offset); } sensor_.set_name(sensor.name); @@ -156,11 +163,11 @@ void AnalogSensor::reload(bool get_nvs) { } if (!found) { // it's new, we assume it's valid - AnalogType type = static_cast(sensor.type); - sensors_.emplace_back(sensor.gpio, sensor.name, sensor.offset, sensor.factor, sensor.uom, type, sensor.is_system); + sensors_.emplace_back(sensor.gpio, sensor.name, sensor.offset, sensor.factor, sensor.uom, sensor.type, sensor.is_system); sensors_.back().ha_registered = false; // this will trigger recreate of the HA config - if (sensor.type == AnalogType::COUNTER || sensor.type >= AnalogType::DIGITAL_OUT) { + if (sensor.type == AnalogType::COUNTER || (sensor.type >= AnalogType::DIGITAL_OUT && sensor.type <= AnalogType::PWM_2) + || sensor.type == AnalogType::RGB || (sensor.type >= AnalogType::CNT_0 && sensor.type <= AnalogType::CNT_2)) { sensors_.back().set_value(sensor.offset); } else { sensors_.back().set_value(0); // reset value only for new sensors @@ -169,16 +176,16 @@ void AnalogSensor::reload(bool get_nvs) { // add the command to set the value of the sensor if (sensor.type == AnalogType::COUNTER || (sensor.type >= AnalogType::DIGITAL_OUT && sensor.type <= AnalogType::PWM_2) - || sensor.type == AnalogType::RGB || sensor.type == AnalogType::PULSE) { + || sensor.type == AnalogType::RGB || sensor.type == AnalogType::PULSE || (sensor.type >= AnalogType::CNT_0 && sensor.type <= AnalogType::CNT_2)) { Command::add( EMSdevice::DeviceType::ANALOGSENSOR, - sensor.name.c_str(), + sensor.name, [&](const char * value, const int8_t id) { return command_setvalue(value, sensor.gpio); }, - sensor.type == AnalogType::COUNTER ? FL_(counter) - : sensor.type == AnalogType::DIGITAL_OUT ? FL_(digital_out) - : sensor.type == AnalogType::RGB ? FL_(RGB) - : sensor.type == AnalogType::PULSE ? FL_(pulse) - : FL_(pwm), + sensor.type == AnalogType::COUNTER || (sensor.type >= AnalogType::CNT_0 && sensor.type <= AnalogType::CNT_2) ? FL_(counter) + : sensor.type == AnalogType::DIGITAL_OUT ? FL_(digital_out) + : sensor.type == AnalogType::RGB ? FL_(RGB) + : sensor.type == AnalogType::PULSE ? FL_(pulse) + : FL_(pwm), CommandFlag::ADMIN_ONLY); } } @@ -191,9 +198,20 @@ void AnalogSensor::reload(bool get_nvs) { // activate each sensor for (auto & sensor : sensors_) { sensor.ha_registered = false; // force HA configs to be re-created + if ((sensor.type() >= AnalogType::FREQ_0 && sensor.type() <= AnalogType::FREQ_2)) { + exclude_types_.push_back(sensor.type()); + exclude_types_.push_back(sensor.type() + 3); + } + if ((sensor.type() >= AnalogType::CNT_0 && sensor.type() <= AnalogType::CNT_2)) { + exclude_types_.push_back(sensor.type()); + exclude_types_.push_back(sensor.type() - 3); + } + if ((sensor.type() >= AnalogType::PWM_0 && sensor.type() <= AnalogType::PWM_2)) { + exclude_types_.push_back(sensor.type()); + } if (sensor.type() == AnalogType::COUNTER || sensor.type() == AnalogType::DIGITAL_IN || sensor.type() == AnalogType::RATE - || sensor.type() == AnalogType::TIMER) { + || sensor.type() == AnalogType::TIMER || (sensor.type() >= AnalogType::FREQ_0 && sensor.type() <= AnalogType::CNT_2)) { // pullup is mapped to DAC, so set to 3.3V #if CONFIG_IDF_TARGET_ESP32 if (sensor.gpio() == 25 || sensor.gpio() == 26) { @@ -204,6 +222,7 @@ void AnalogSensor::reload(bool get_nvs) { dacWrite(sensor.gpio(), 255); } #endif + pinMode(sensor.gpio(), INPUT_PULLUP); } if (sensor.type() == AnalogType::ADC) { LOG_DEBUG("ADC Sensor on GPIO %02d", sensor.gpio()); @@ -216,16 +235,14 @@ void AnalogSensor::reload(bool get_nvs) { sensor.set_uom(DeviceValueUOM::DEGREES); } else if (sensor.type() == AnalogType::COUNTER) { LOG_DEBUG("I/O Counter on GPIO %02d", sensor.gpio()); - pinMode(sensor.gpio(), INPUT_PULLUP); sensor.polltime_ = 0; sensor.poll_ = digitalRead(sensor.gpio()); - if (double_t val = EMSESP::nvs_.getDouble(sensor.name().c_str(), 0)) { + if (double_t val = EMSESP::nvs_.getDouble(sensor.name(), 0)) { sensor.set_value(val); } publish_sensor(sensor); } else if (sensor.type() == AnalogType::TIMER || sensor.type() == AnalogType::RATE) { LOG_DEBUG("Timer/Rate on GPIO %02d", sensor.gpio()); - pinMode(sensor.gpio(), INPUT_PULLUP); sensor.polltime_ = uuid::get_uptime(); sensor.last_polltime_ = uuid::get_uptime(); sensor.poll_ = digitalRead(sensor.gpio()); @@ -234,25 +251,33 @@ void AnalogSensor::reload(bool get_nvs) { publish_sensor(sensor); #ifndef EMSESP_STANDALONE } else if (sensor.type() >= AnalogType::FREQ_0 && sensor.type() <= AnalogType::FREQ_2) { - LOG_DEBUG("Frequency on GPIO %02d", sensor.gpio()); - pinMode(sensor.gpio(), INPUT_PULLUP); + auto index = sensor.type() - AnalogType::FREQ_0; + LOG_DEBUG("Frequency %d on GPIO %02d", index, sensor.gpio()); sensor.set_offset(0); sensor.set_value(0); publish_sensor(sensor); - auto index = sensor.type() - AnalogType::FREQ_0; attachInterrupt(sensor.gpio(), index == 0 ? freqIrq0 : index == 1 ? freqIrq1 : freqIrq2, FALLING); lastedge[index] = edge[index] = micros(); edgecnt[index] = 0; + } else if (sensor.type() >= AnalogType::CNT_0 && sensor.type() <= AnalogType::CNT_2) { + auto index = sensor.type() - AnalogType::CNT_0; + LOG_DEBUG("Counter %d on GPIO %02d", index, sensor.gpio()); + if (double_t val = EMSESP::nvs_.getDouble(sensor.name(), 0)) { + sensor.set_value(val); + } + publish_sensor(sensor); + attachInterrupt(sensor.gpio(), index == 0 ? freqIrq0 : index == 1 ? freqIrq1 : freqIrq2, FALLING); + edgecnt[index] = 0; #endif } else if (sensor.type() == AnalogType::DIGITAL_IN) { LOG_DEBUG("Digital Read on GPIO %02d", sensor.gpio()); - pinMode(sensor.gpio(), INPUT_PULLUP); sensor.set_value(digitalRead(sensor.gpio())); // initial value sensor.set_uom(0); // no uom, just for safe measures sensor.polltime_ = 0; sensor.poll_ = digitalRead(sensor.gpio()); publish_sensor(sensor); } else if (sensor.type() == AnalogType::RGB) { + sensor.set_uom(0); // no uom LOG_DEBUG("RGB on GPIO %02d", sensor.gpio()); uint32_t v = sensor.value(); uint8_t r = v / 10000; @@ -290,12 +315,15 @@ void AnalogSensor::reload(bool get_nvs) { #endif { if (sensor.uom() == 0) { // set state from NVS - if (!get_nvs || EMSESP::nvs_.getChar(sensor.name().c_str(), -1) == -1) { - EMSESP::nvs_.putChar(sensor.name().c_str(), (int8_t)sensor.offset()); + if (!get_nvs || EMSESP::nvs_.getChar(sensor.name(), -1) == -1) { + EMSESP::nvs_.putChar(sensor.name(), (int8_t)sensor.offset()); } else { - sensor.set_offset(EMSESP::nvs_.getChar(sensor.name().c_str())); + sensor.set_offset(EMSESP::nvs_.getChar(sensor.name())); } + } else if (sensor.uom() > 1) { + sensor.set_uom(2); } + sensor.set_offset(sensor.offset() > 0 ? 1 : 0); digitalWrite(sensor.gpio(), (sensor.offset() == 0) ^ (sensor.factor() > 0)); sensor.set_value(sensor.offset()); } @@ -344,10 +372,10 @@ void AnalogSensor::measure() { uint16_t a = analogReadMilliVolts(sensor.gpio()); // e.g. ADC1_CHANNEL_0_GPIO_NUM if (!sensor.analog_) { // init first time sensor.analog_ = a; - sensor.sum_ = a * 512; + sensor.sum_ = a * 128; } else { // simple moving average filter - sensor.sum_ = (sensor.sum_ * 511) / 512 + a; - sensor.analog_ = sensor.sum_ / 512; + sensor.sum_ = (sensor.sum_ * 127) / 128 + a; + sensor.analog_ = sensor.sum_ / 128; } // detect change with little hysteresis on raw mV value @@ -393,6 +421,18 @@ void AnalogSensor::measure() { changed_ = true; publish_sensor(sensor); } + } else if (sensor.type() >= AnalogType::CNT_0 && sensor.type() <= AnalogType::CNT_2) { + auto index = sensor.type() - AnalogType::CNT_0; + auto oldval = sensor.value(); + portENTER_CRITICAL_ISR(&mux); + auto c = edgecnt[index]; + edgecnt[index] = 0; + portEXIT_CRITICAL_ISR(&mux); + sensor.set_value(oldval + sensor.factor() * c); + if (sensor.value() != oldval) { + changed_ = true; + publish_sensor(sensor); + } #endif } } @@ -417,7 +457,7 @@ void AnalogSensor::measure() { } else if (!sensor.poll_) { // falling edge if (sensor.type() == AnalogType::COUNTER) { sensor.set_value(old_value + sensor.factor()); - // EMSESP::nvs_.putDouble(sensor.name().c_str(), sensor.value()); + // EMSESP::nvs_.putDouble(sensor.name(), sensor.value()); } else if (sensor.type() == AnalogType::RATE) { // default uom: Hz (1/sec) with factor 1 sensor.set_value(sensor.factor() * 1000 / (sensor.polltime_ - sensor.last_polltime_)); } else if (sensor.type() == AnalogType::TIMER) { // default seconds with factor 1 @@ -453,9 +493,9 @@ void AnalogSensor::measure() { // store counters to NVS, called every hour, on restart and update void AnalogSensor::store_counters() { for (auto & sensor : sensors_) { - if (sensor.type() == AnalogType::COUNTER) { - if (sensor.value() != EMSESP::nvs_.getDouble(sensor.name().c_str())) { - EMSESP::nvs_.putDouble(sensor.name().c_str(), sensor.value()); + if (sensor.type() == AnalogType::COUNTER || (sensor.type() >= AnalogType::CNT_0 && sensor.type() <= AnalogType::CNT_2)) { + if (sensor.value() != EMSESP::nvs_.getDouble(sensor.name())) { + EMSESP::nvs_.putDouble(sensor.name(), sensor.value()); } } } @@ -472,7 +512,13 @@ void AnalogSensor::loop() { // update analog information name, offset, factor, uom, type, deleted, is_system // a type value of -1 is used to delete the sensor // the gpio is the key -bool AnalogSensor::update(uint8_t gpio, std::string & name, double offset, double factor, uint8_t uom, int8_t type, bool deleted, bool is_system) { +bool AnalogSensor::update(uint8_t gpio, const char * org_name, double offset, double factor, uint8_t uom, int8_t type, bool deleted, bool is_system) { + char name[20]; + if (org_name[0] == '\0') { + snprintf(name, sizeof(name), "%s_%02d", FL_(list_sensortype)[type], gpio); + } else { + strlcpy(name, org_name, sizeof(name)); + } // first see if we can find the sensor in our customization list bool found_sensor = false; EMSESP::webCustomizationService.update([&](WebCustomization & settings) { @@ -480,12 +526,7 @@ bool AnalogSensor::update(uint8_t gpio, std::string & name, double offset, doubl if (AnalogCustomization.type == AnalogType::COUNTER || (AnalogCustomization.type >= AnalogType::DIGITAL_OUT && AnalogCustomization.type <= AnalogType::PWM_2) || AnalogCustomization.type == AnalogType::RGB || AnalogCustomization.type == AnalogType::PULSE) { - Command::erase_command(EMSdevice::DeviceType::ANALOGSENSOR, AnalogCustomization.name.c_str()); - } - if (name.empty()) { - char n[20]; - snprintf(n, sizeof(n), "%s_%02d", FL_(list_sensortype)[type], gpio); - name = n; + Command::erase_command(EMSdevice::DeviceType::ANALOGSENSOR, AnalogCustomization.name); } if (AnalogCustomization.gpio == gpio) { found_sensor = true; // found the record @@ -493,14 +534,14 @@ bool AnalogSensor::update(uint8_t gpio, std::string & name, double offset, doubl if (deleted) { LOG_DEBUG("Removing analog sensor GPIO %02d", gpio); EMSESP::system_.remove_gpio(gpio); // remove from used list only - EMSESP::nvs_.remove(AnalogCustomization.name.c_str()); + EMSESP::nvs_.remove(AnalogCustomization.name); settings.analogCustomizations.remove(AnalogCustomization); } else { // update existing record - if (name != AnalogCustomization.name) { - EMSESP::nvs_.remove(AnalogCustomization.name.c_str()); + if (!strcmp(name, AnalogCustomization.name)) { + EMSESP::nvs_.remove(AnalogCustomization.name); } - AnalogCustomization.name = name; + strlcpy(AnalogCustomization.name, name, sizeof(AnalogCustomization.name)); AnalogCustomization.offset = offset; AnalogCustomization.factor = factor; AnalogCustomization.uom = uom; @@ -523,9 +564,9 @@ bool AnalogSensor::update(uint8_t gpio, std::string & name, double offset, doubl if (!found_sensor) { found_sensor = true; EMSESP::webCustomizationService.update([&](WebCustomization & settings) { - auto newSensor = AnalogCustomization(); - newSensor.gpio = gpio; - newSensor.name = name; + auto newSensor = AnalogCustomization(); + newSensor.gpio = gpio; + strlcpy(newSensor.name, name, sizeof(newSensor.name)); newSensor.offset = offset; newSensor.factor = factor; newSensor.uom = uom; @@ -565,9 +606,9 @@ void AnalogSensor::publish_sensor(const Sensor & sensor) const { if (Mqtt::publish_single()) { char topic[Mqtt::MQTT_TOPIC_MAX_SIZE]; if (Mqtt::publish_single2cmd()) { - snprintf(topic, sizeof(topic), "%s/%s", F_(analogsensor), sensor.name().c_str()); + snprintf(topic, sizeof(topic), "%s/%s", F_(analogsensor), sensor.name()); } else { - snprintf(topic, sizeof(topic), "%s%s/%s", F_(analogsensor), "_data", sensor.name().c_str()); + snprintf(topic, sizeof(topic), "%s%s/%s", F_(analogsensor), "_data", sensor.name()); } char result[12]; if (sensor.type() == AnalogType::DIGITAL_IN || sensor.type() == AnalogType::DIGITAL_OUT) { @@ -578,7 +619,7 @@ void AnalogSensor::publish_sensor(const Sensor & sensor) const { Mqtt::queue_publish(topic, result); // always publish as doubles } char cmd[COMMAND_MAX_LENGTH]; - snprintf(cmd, sizeof(cmd), "%s/%s", F_(analogsensor), sensor.name().c_str()); + snprintf(cmd, sizeof(cmd), "%s/%s", F_(analogsensor), sensor.name()); EMSESP::webSchedulerService.onChange(cmd); } @@ -593,6 +634,8 @@ void AnalogSensor::remove_ha_topic(const int8_t type, const uint8_t gpio) const #if CONFIG_IDF_TARGET_ESP32 if (type == AnalogType::PULSE || (type == AnalogType::DIGITAL_OUT && gpio != 25 && gpio != 26)) { +#elif CONFIG_IDF_TARGET_ESP32S2 + if (type == AnalogType::PULSE || (type == AnalogType::DIGITAL_OUT && gpio != 17 && gpio != 18)) { #else if (type == AnalogType::PULSE || type == AnalogType::DIGITAL_OUT) { #endif @@ -637,26 +680,12 @@ void AnalogSensor::publish_values(const bool force) { #else if (sensor.type() == AnalogType::PULSE || sensor.type() == AnalogType::DIGITAL_OUT) { #endif - if (EMSESP::system_.bool_format() == BOOL_FORMAT_TRUEFALSE) { - dataSensor["value"] = sensor.value() != 0; - } else if (EMSESP::system_.bool_format() == BOOL_FORMAT_10) { - dataSensor["value"] = sensor.value() != 0 ? 1 : 0; - } else { - char result[12]; - dataSensor["value"] = Helpers::render_boolean(result, sensor.value() != 0); - } + Mqtt::add_value_bool(doc.as(), sensor.name(), sensor.value() != 0); } else { dataSensor["value"] = serialized(Helpers::render_value(s, sensor.value(), 2)); // double } } else if (sensor.type() == AnalogType::DIGITAL_IN || sensor.type() == AnalogType::DIGITAL_OUT || sensor.type() == AnalogType::PULSE) { - if (EMSESP::system_.bool_format() == BOOL_FORMAT_TRUEFALSE) { - doc[sensor.name()] = sensor.value() != 0; - } else if (EMSESP::system_.bool_format() == BOOL_FORMAT_10) { - doc[sensor.name()] = sensor.value() != 0 ? 1 : 0; - } else { - char result[12]; - doc[sensor.name()] = Helpers::render_boolean(result, sensor.value() != 0); - } + Mqtt::add_value_bool(doc.as(), sensor.name(), sensor.value() != 0); } else { char s[10]; doc[sensor.name()] = serialized(Helpers::render_value(s, sensor.value(), 2)); @@ -667,7 +696,7 @@ void AnalogSensor::publish_values(const bool force) { LOG_DEBUG("Recreating HA config for analog sensor GPIO %02d", sensor.gpio()); JsonDocument config; - config["~"] = Mqtt::base(); + config["~"] = Mqtt::base(); char stat_t[50]; snprintf(stat_t, sizeof(stat_t), "~/%s_data", F_(analogsensor)); // use base path @@ -679,7 +708,7 @@ void AnalogSensor::publish_values(const bool force) { snprintf(val_obj, sizeof(val_obj), "value_json['%02d']['value']", sensor.gpio()); snprintf(val_cond, sizeof(val_cond), "value_json['%02d'] is defined and %s is defined", sensor.gpio(), val_obj); } else { - snprintf(val_obj, sizeof(val_obj), "value_json['%s']", sensor.name().c_str()); + snprintf(val_obj, sizeof(val_obj), "value_json['%s']", sensor.name()); snprintf(val_cond, sizeof(val_cond), "%s is defined", val_obj); } char sample_val[12] = "0"; @@ -700,12 +729,9 @@ void AnalogSensor::publish_values(const bool force) { snprintf(uniq_s, sizeof(uniq_s), "%s_%02d", F_(analogsensor), sensor.gpio()); } - config["~"] = Mqtt::base(); + config["~"] = Mqtt::base(); config["uniq_id"] = uniq_s; - - char name[50]; - snprintf(name, sizeof(name), "%s", sensor.name().c_str()); - config["name"] = name; + config["name"] = sensor.name(); if (sensor.uom() != DeviceValueUOM::NONE && sensor.type() != AnalogType::DIGITAL_OUT) { config["unit_of_meas"] = EMSdevice::uom_to_string(sensor.uom()); @@ -717,16 +743,18 @@ void AnalogSensor::publish_values(const bool force) { char command_topic[Mqtt::MQTT_TOPIC_MAX_SIZE]; #if CONFIG_IDF_TARGET_ESP32 if (sensor.type() == AnalogType::PULSE || (sensor.type() == AnalogType::DIGITAL_OUT && sensor.gpio() != 25 && sensor.gpio() != 26)) { +#elif CONFIG_IDF_TARGET_ESP32S2 + if (sensor.type() == AnalogType::PULSE || (sensor.type() == AnalogType::DIGITAL_OUT && sensor.gpio() != 17 && sensor.gpio() != 18)) { #else - if (sensor.type() == AnalogType::PULSE || sensor.type() == AnalogType::DIGITAL_OUT) { + if (sensor.type() == AnalogType::PULSE || sensor.type() == AnalogType::DIGITAL_OUT) { #endif snprintf(topic, sizeof(topic), "switch/%s/%s_%02d/config", Mqtt::basename().c_str(), F_(analogsensor), sensor.gpio()); - snprintf(command_topic, sizeof(command_topic), "%s/%s/%s", Mqtt::base().c_str(), F_(analogsensor), sensor.name().c_str()); + snprintf(command_topic, sizeof(command_topic), "%s/%s/%s", Mqtt::base().c_str(), F_(analogsensor), sensor.name()); config["cmd_t"] = command_topic; Mqtt::add_ha_bool(config.as()); } else if (sensor.type() == AnalogType::DIGITAL_OUT) { // DAC snprintf(topic, sizeof(topic), "number/%s/%s_%02d/config", Mqtt::basename().c_str(), F_(analogsensor), sensor.gpio()); - snprintf(command_topic, sizeof(command_topic), "%s/%s/%s", Mqtt::base().c_str(), F_(analogsensor), sensor.name().c_str()); + snprintf(command_topic, sizeof(command_topic), "%s/%s/%s", Mqtt::base().c_str(), F_(analogsensor), sensor.name()); config["cmd_t"] = command_topic; config["min"] = 0; config["max"] = 255; @@ -734,7 +762,7 @@ void AnalogSensor::publish_values(const bool force) { config["step"] = 1; } else if (sensor.type() >= AnalogType::PWM_0 && sensor.type() <= AnalogType::PWM_2) { snprintf(topic, sizeof(topic), "number/%s/%s_%02d/config", Mqtt::basename().c_str(), F_(analogsensor), sensor.gpio()); - snprintf(command_topic, sizeof(command_topic), "%s/%s/%s", Mqtt::base().c_str(), F_(analogsensor), sensor.name().c_str()); + snprintf(command_topic, sizeof(command_topic), "%s/%s/%s", Mqtt::base().c_str(), F_(analogsensor), sensor.name()); config["cmd_t"] = command_topic; config["min"] = 0; config["max"] = 100; @@ -742,15 +770,15 @@ void AnalogSensor::publish_values(const bool force) { config["step"] = 0.1; } else if (sensor.type() == AnalogType::RGB) { snprintf(topic, sizeof(topic), "number/%s/%s_%02d/config", Mqtt::basename().c_str(), F_(analogsensor), sensor.gpio()); - snprintf(command_topic, sizeof(command_topic), "%s/%s/%s", Mqtt::base().c_str(), F_(analogsensor), sensor.name().c_str()); + snprintf(command_topic, sizeof(command_topic), "%s/%s/%s", Mqtt::base().c_str(), F_(analogsensor), sensor.name()); config["cmd_t"] = command_topic; config["min"] = 0; config["max"] = 999999; config["mode"] = "box"; // auto, slider or box config["step"] = 1; - } else if (sensor.type() == AnalogType::COUNTER) { + } else if (sensor.type() == AnalogType::COUNTER || (sensor.type() >= AnalogType::CNT_0 && sensor.type() <= AnalogType::CNT_2)) { snprintf(topic, sizeof(topic), "sensor/%s/%s_%02d/config", Mqtt::basename().c_str(), F_(analogsensor), sensor.gpio()); - snprintf(command_topic, sizeof(command_topic), "%s/%s/%s", Mqtt::base().c_str(), F_(analogsensor), sensor.name().c_str()); + snprintf(command_topic, sizeof(command_topic), "%s/%s/%s", Mqtt::base().c_str(), F_(analogsensor), sensor.name()); config["cmd_t"] = command_topic; config["stat_cla"] = "total_increasing"; // config["mode"] = "box"; // auto, slider or box @@ -835,10 +863,11 @@ void AnalogSensor::get_value_json(JsonObject output, const Sensor & sensor) { output["value"] = sensor.value(); output["readable"] = true; output["writeable"] = sensor.type() == AnalogType::COUNTER || sensor.type() == AnalogType::RGB || sensor.type() == AnalogType::PULSE - || (sensor.type() >= AnalogType::DIGITAL_OUT && sensor.type() <= AnalogType::PWM_2); + || (sensor.type() >= AnalogType::DIGITAL_OUT && sensor.type() <= AnalogType::PWM_2) + || (sensor.type() >= AnalogType::CNT_0 && sensor.type() <= AnalogType::CNT_2); output["visible"] = true; output["is_system"] = sensor.is_system(); - if (sensor.type() == AnalogType::COUNTER) { + if (sensor.type() == AnalogType::COUNTER || (sensor.type() >= AnalogType::CNT_0 && sensor.type() <= AnalogType::CNT_2)) { output["min"] = 0; output["max"] = 4000000; output["start_value"] = sensor.offset(); @@ -859,6 +888,8 @@ void AnalogSensor::get_value_json(JsonObject output, const Sensor & sensor) { output["min"] = 0; #if CONFIG_IDF_TARGET_ESP32 output["max"] = sensor.gpio() == 25 || sensor.gpio() == 26 ? 255 : 1; +#elif CONFIG_IDF_TARGET_ESP32S2 + output["max"] = sensor.gpio() == 17 || sensor.gpio() == 18 ? 255 : 1; #else output["max"] = 1; #endif @@ -870,20 +901,14 @@ void AnalogSensor::get_value_json(JsonObject output, const Sensor & sensor) { } // this creates the sensor, initializing everything -AnalogSensor::Sensor::Sensor(const uint8_t gpio, - const std::string & name, - const double offset, - const double factor, - const uint8_t uom, - const int8_t type, - const bool is_system) +AnalogSensor::Sensor::Sensor(const uint8_t gpio, const char * name, const double offset, const double factor, const uint8_t uom, const int8_t type, const bool is_system) : gpio_(gpio) - , name_(name) , offset_(offset) , factor_(factor) , uom_(uom) , type_(type) , is_system_(is_system) { + strlcpy(name_, name, sizeof(name_)); value_ = 0; // init value to 0 always } @@ -901,7 +926,7 @@ bool AnalogSensor::command_setvalue(const char * value, const int8_t gpio) { for (auto & sensor : sensors_) { if (sensor.gpio() == gpio) { double oldoffset = sensor.offset(); - if (sensor.type() == AnalogType::COUNTER) { + if (sensor.type() == AnalogType::COUNTER || (sensor.type() >= AnalogType::CNT_0 && sensor.type() <= AnalogType::CNT_2)) { if (val < 0 || value[0] == '+') { // sign corrects values // sensor.set_offset(sensor.value() + val); sensor.set_value(sensor.value() + val); @@ -910,8 +935,8 @@ bool AnalogSensor::command_setvalue(const char * value, const int8_t gpio) { sensor.set_value(val); } sensor.set_offset(sensor.value()); - if (sensor.value() != EMSESP::nvs_.getDouble(sensor.name().c_str(), 0)) { - EMSESP::nvs_.putDouble(sensor.name().c_str(), sensor.value()); + if (sensor.value() != EMSESP::nvs_.getDouble(sensor.name(), 0)) { + EMSESP::nvs_.putDouble(sensor.name(), sensor.value()); } } else if (sensor.type() == AnalogType::ADC) { sensor.set_offset(val); @@ -956,8 +981,8 @@ bool AnalogSensor::command_setvalue(const char * value, const int8_t gpio) { sensor.set_value(v); pinMode(sensor.gpio(), OUTPUT); digitalWrite(sensor.gpio(), (sensor.offset() == 0) ^ (sensor.factor() > 0)); - if (sensor.uom() == 0 && EMSESP::nvs_.getChar(sensor.name().c_str()) != (int8_t)sensor.offset()) { - EMSESP::nvs_.putChar(sensor.name().c_str(), (int8_t)sensor.offset()); + if (sensor.uom() == 0 && EMSESP::nvs_.getChar(sensor.name()) != (int8_t)sensor.offset()) { + EMSESP::nvs_.putChar(sensor.name(), (int8_t)sensor.offset()); } } } else if (sensor.type() >= AnalogType::PWM_0 && sensor.type() <= AnalogType::PWM_2) { diff --git a/src/core/analogsensor.h b/src/core/analogsensor.h index b5d28eef9..947094980 100644 --- a/src/core/analogsensor.h +++ b/src/core/analogsensor.h @@ -25,27 +25,27 @@ #include -namespace emsesp { +#include -// names, same order as AnalogType, see list_sensortype in local_common.h -// MAKE_ENUM_FIXED(AnalogTypeName, "disabled", "dig_in", "counter", "adc", "timer", "rate", "dig_out", "pwm0", "pwm1", "pwm2") +namespace emsesp { class AnalogSensor { public: class Sensor { public: - Sensor(const uint8_t gpio, const std::string & name, const double offset, const double factor, const uint8_t uom, const int8_t type, const bool is_system); + Sensor(const uint8_t gpio, const char * name, const double offset, const double factor, const uint8_t uom, const int8_t type, const bool is_system); ~Sensor() = default; void set_offset(const double offset) { offset_ = offset; } - std::string name() const { + const char * name() const { return name_; } - void set_name(const std::string & name) { - name_ = name; + + void set_name(const char * name) { + strlcpy(name_, name, sizeof(name_)); } uint8_t gpio() const { @@ -104,14 +104,14 @@ class AnalogSensor { uint32_t last_polltime_ = 0; // for timer private: - uint8_t gpio_; - std::string name_; - double offset_; - double factor_; - uint8_t uom_; - double value_; // double because of the factor is a double - int8_t type_; // one of the AnalogType enum - bool is_system_; // if true, the sensor is a system sensor + uint8_t gpio_; + char name_[20]; + double offset_; + double factor_; + uint8_t uom_; + double value_; // double because of the factor is a double + int8_t type_; // one of the AnalogType enum + bool is_system_; // if true, the sensor is a system sensor }; AnalogSensor() = default; @@ -132,7 +132,10 @@ class AnalogSensor { PULSE = 12, FREQ_0 = 13, FREQ_1 = 14, - FREQ_2 = 15 + FREQ_2 = 15, + CNT_0 = 16, + CNT_1 = 17, + CNT_2 = 18 }; void start(const bool factory_settings = false); @@ -143,7 +146,7 @@ class AnalogSensor { bool updated_values(); // return back reference to the sensor list, used by other classes - std::vector sensors() const { + std::vector> sensors() const { return sensors_; } @@ -171,9 +174,12 @@ class AnalogSensor { return sensors_.size(); } - bool update(uint8_t gpio, std::string & name, double offset, double factor, uint8_t uom, int8_t type, bool deleted, bool is_system); - bool get_value_info(JsonObject output, const char * cmd, const int8_t id = -1); - void store_counters(); + bool update(uint8_t gpio, const char * name, double offset, double factor, uint8_t uom, int8_t type, bool deleted, bool is_system); + bool get_value_info(JsonObject output, const char * cmd, const int8_t id = -1); + void store_counters(); + static std::vector exclude_types() { + return exclude_types_; + } private: static constexpr double Beta = 4260; @@ -185,14 +191,14 @@ class AnalogSensor { static constexpr uint32_t MEASURE_ANALOG_INTERVAL = 500; static uuid::log::Logger logger_; + void remove_ha_topic(const int8_t type, const uint8_t id) const; + bool command_setvalue(const char * value, const int8_t gpio); + void measure(); + void addSensorJson(JsonObject output, const Sensor & sensor); + void get_value_json(JsonObject output, const Sensor & sensor); - void remove_ha_topic(const int8_t type, const uint8_t id) const; - bool command_setvalue(const char * value, const int8_t gpio); - void measure(); - void addSensorJson(JsonObject output, const Sensor & sensor); - void get_value_json(JsonObject output, const Sensor & sensor); - - std::vector sensors_; // our list of sensors + std::vector> sensors_; // our list of sensors + static std::vector exclude_types_; bool analog_enabled_; bool changed_ = true; // this will force a publish of all sensors when initialising diff --git a/src/core/command.cpp b/src/core/command.cpp index a13468fdf..7db7dbacf 100644 --- a/src/core/command.cpp +++ b/src/core/command.cpp @@ -24,7 +24,7 @@ namespace emsesp { uuid::log::Logger Command::logger_{F_(command), uuid::log::Facility::DAEMON}; -std::vector Command::cmdfunctions_; +std::vector> Command::cmdfunctions_; // takes a URI path and a json body, parses the data and calls the command // the path is leading so if duplicate keys are in the input JSON it will be ignored diff --git a/src/core/command.h b/src/core/command.h index b4c236a9a..92832d8a0 100644 --- a/src/core/command.h +++ b/src/core/command.h @@ -22,6 +22,7 @@ #include #include "console.h" +#include using uuid::console::Shell; @@ -97,7 +98,7 @@ class Command { } }; - static std::vector commands() { + static std::vector> commands() { return cmdfunctions_; } @@ -145,7 +146,7 @@ class Command { private: static uuid::log::Logger logger_; - static std::vector cmdfunctions_; // the list of commands + static std::vector> cmdfunctions_; // the list of commands static uint8_t json_message(uint8_t error_code, const char * message, JsonObject output, const char * object = nullptr); }; diff --git a/src/core/emsdevice.cpp b/src/core/emsdevice.cpp index b5248c545..8b5ec063b 100644 --- a/src/core/emsdevice.cpp +++ b/src/core/emsdevice.cpp @@ -1637,14 +1637,7 @@ void EMSdevice::get_value_json(JsonObject json, DeviceValue & dv) { auto value_b = (bool)*(uint8_t *)(dv.value_p); json["bool"] = value_b; json["index"] = value_b ? 1 : 0; - if (EMSESP::system_.bool_format() == BOOL_FORMAT_TRUEFALSE) { - json[value] = value_b; - } else if (EMSESP::system_.bool_format() == BOOL_FORMAT_10) { - json[value] = value_b ? 1 : 0; - } else { - char s[12]; - json[value] = Helpers::render_boolean(s, value_b); - } + Mqtt::add_value_bool(json, value, value_b); } json[type] = ("boolean"); break; @@ -1953,13 +1946,8 @@ bool EMSdevice::generate_values(JsonObject output, const int8_t tag_filter, cons if (output_target == OUTPUT_TARGET::CONSOLE) { char s[12]; json[name] = Helpers::render_boolean(s, value_b, true); // console use web settings - } else if (EMSESP::system_.bool_format() == BOOL_FORMAT_TRUEFALSE) { - json[name] = value_b; - } else if (EMSESP::system_.bool_format() == BOOL_FORMAT_10) { - json[name] = value_b ? 1 : 0; } else { - char s[12]; - json[name] = Helpers::render_boolean(s, value_b); + Mqtt::add_value_bool(json, name, value_b); } } @@ -2006,12 +1994,12 @@ bool EMSdevice::generate_values(JsonObject output, const int8_t tag_filter, cons char time_s[60]; snprintf(time_s, sizeof(time_s), - "%lu %s %lu %s %lu %s", - (time_value / 1440), + "%d %s %d %s %d %s", + (int)(time_value / 1440), Helpers::translated_word(FL_(days)), - ((time_value % 1440) / 60), + (int)((time_value % 1440) / 60), Helpers::translated_word(FL_(hours)), - (time_value % 60), + (int)(time_value % 60), Helpers::translated_word(FL_(minutes))); json[name] = time_s; } else { diff --git a/src/core/emsdevice.h b/src/core/emsdevice.h index e6d904a7b..c74604d2b 100644 --- a/src/core/emsdevice.h +++ b/src/core/emsdevice.h @@ -25,6 +25,7 @@ #include "helpers.h" #include "emsdevicevalue.h" +#include #include namespace emsesp { @@ -549,13 +550,12 @@ class EMSdevice { } }; - std::vector handlers_ignored_; - + std::vector> handlers_ignored_, handlers_broadcasted_, handlers_config_; #if defined(EMSESP_STANDALONE) || defined(EMSESP_TEST) public: // so we can call it from WebCustomizationService::load_test_data() and EMSESP::dump_all_entities() #endif - std::vector telegram_functions_; // each EMS device has its own set of registered telegram types - std::vector devicevalues_; // all the device values + std::vector> telegram_functions_; // each EMS device has its own set of registered telegram types + std::vector> devicevalues_; // all the device values }; } // namespace emsesp diff --git a/src/core/emsesp.cpp b/src/core/emsesp.cpp index fd543ab69..a125689b4 100644 --- a/src/core/emsesp.cpp +++ b/src/core/emsesp.cpp @@ -29,22 +29,28 @@ static_assert(uuid::console::thread_safe, "uuid-console must be thread-safe"); namespace emsesp { // Static member definitions +#ifndef EMSESP_STANDALONE +std::vector, AllocatorPSRAM>> EMSESP::emsdevices{}; +std::vector> EMSESP::device_library_; +#else std::vector> EMSESP::emsdevices{}; std::vector EMSESP::device_library_; -uuid::log::Logger EMSESP::logger_{F_(emsesp), uuid::log::Facility::KERN}; -uint16_t EMSESP::watch_id_ = WATCH_ID_NONE; -uint8_t EMSESP::watch_ = 0; -uint16_t EMSESP::read_id_ = WATCH_ID_NONE; -bool EMSESP::read_next_ = false; -uint16_t EMSESP::publish_id_ = 0; -uint16_t EMSESP::response_id_ = 0; -bool EMSESP::tap_water_active_ = false; -uint8_t EMSESP::publish_all_idx_ = 0; -uint8_t EMSESP::unique_id_count_ = 0; -bool EMSESP::trace_raw_ = false; -uint16_t EMSESP::wait_validate_ = 0; -bool EMSESP::wait_km_ = false; -uint32_t EMSESP::last_fetch_ = 0; +#endif + +uuid::log::Logger EMSESP::logger_{F_(emsesp), uuid::log::Facility::KERN}; +uint16_t EMSESP::watch_id_ = WATCH_ID_NONE; +uint8_t EMSESP::watch_ = 0; +uint16_t EMSESP::read_id_ = WATCH_ID_NONE; +bool EMSESP::read_next_ = false; +uint16_t EMSESP::publish_id_ = 0; +uint16_t EMSESP::response_id_ = 0; +bool EMSESP::tap_water_active_ = false; +uint8_t EMSESP::publish_all_idx_ = 0; +uint8_t EMSESP::unique_id_count_ = 0; +bool EMSESP::trace_raw_ = false; +uint16_t EMSESP::wait_validate_ = 0; +bool EMSESP::wait_km_ = false; +uint32_t EMSESP::last_fetch_ = 0; AsyncWebServer webServer(80); @@ -78,10 +84,6 @@ uuid::log::Logger EMSESP::logger() { return logger_; } -#ifndef EMSESP_STANDALONE -uuid::syslog::SyslogService System::syslog_; -#endif - // The services RxService EMSESP::rxservice_; // incoming Telegram Rx handler TxService EMSESP::txservice_; // outgoing Telegram Tx handler @@ -516,19 +518,19 @@ void EMSESP::show_sensor_values(uuid::console::Shell & shell) { for (const auto & sensor : temperaturesensor_.sensors()) { if (Helpers::hasValue(sensor.temperature_c)) { shell.printfln(" %s: %s%s °%c%s (Offset: %s, ID: %s, System: %s)", - sensor.name().c_str(), + sensor.name(), COLOR_BRIGHT_GREEN, Helpers::render_value(s, sensor.temperature_c, 10, fahrenheit), (fahrenheit == 0) ? 'C' : 'F', COLOR_RESET, Helpers::render_value(s2, sensor.offset(), 10, fahrenheit), - sensor.id().c_str(), + sensor.id(), sensor.is_system() ? "Yes" : "No"); } else { shell.printfln(" %s (Offset: %s, ID: %s, System: %s)", - sensor.name().c_str(), + sensor.name(), Helpers::render_value(s, sensor.offset(), 10, fahrenheit), - sensor.id().c_str(), + sensor.id(), sensor.is_system() ? "Yes" : "No"); } } @@ -544,7 +546,7 @@ void EMSESP::show_sensor_values(uuid::console::Shell & shell) { switch (sensor.type()) { case AnalogSensor::AnalogType::ADC: shell.printfln(" %s: %s%s %s%s (Type: ADC, Factor: %s, Offset: %s, System: %s)", - sensor.name().c_str(), + sensor.name(), COLOR_BRIGHT_GREEN, Helpers::render_value(s, sensor.value(), 2), EMSdevice::uom_to_string(sensor.uom()), @@ -557,7 +559,7 @@ void EMSESP::show_sensor_values(uuid::console::Shell & shell) { // case AnalogSensor::AnalogType::DIGITAL_IN: // case AnalogSensor::AnalogType::COUNTER: shell.printfln(" %s: %s%d%s (Type: %s)", - sensor.name().c_str(), + sensor.name(), COLOR_BRIGHT_GREEN, (uint16_t)sensor.value(), // as int COLOR_RESET, diff --git a/src/core/emsesp.h b/src/core/emsesp.h index 1ec1f48a3..5fe7cfb99 100644 --- a/src/core/emsesp.h +++ b/src/core/emsesp.h @@ -72,6 +72,7 @@ #include "command.h" #include "../emsesp_version.h" +#include // Load external modules class Module {}; // forward declaration @@ -220,8 +221,7 @@ class EMSESP { static void scan_devices(); static void clear_all_devices(); - static std::vector> emsdevices; - + static std::vector, AllocatorPSRAM>> emsdevices; // services static Mqtt mqtt_; static Modbus * modbus_; @@ -266,7 +266,7 @@ class EMSESP { const char * default_name; uint8_t flags; }; - static std::vector device_library_; + static std::vector> device_library_; static uint16_t watch_id_; static uint8_t watch_; diff --git a/src/core/helpers.cpp b/src/core/helpers.cpp index 67ae61316..9e0d95fef 100644 --- a/src/core/helpers.cpp +++ b/src/core/helpers.cpp @@ -470,26 +470,25 @@ char * Helpers::utf8tolatin1(char * result, const char * c, const uint8_t len) { *p = '\0'; // terminate result return result; } + // creates string of hex values from an array of bytes std::string Helpers::data_to_hex(const uint8_t * data, const uint8_t length) { if (length == 0) { return ""; } - char str[length * 3]; - memset(str, 0, sizeof(str)); + std::string str; + str.reserve(length * 3 + 1); - char buffer[4]; - char * p = &str[0]; + char buffer[4]; for (uint8_t i = 0; i < length; i++) { - Helpers::hextoa(buffer, data[i]); - *p++ = buffer[0]; - *p++ = buffer[1]; - *p++ = ' '; // space + str.append(Helpers::hextoa(buffer, data[i])); + str.push_back(' '); } - *--p = '\0'; // null terminate just in case, loosing the trailing space - - return std::string(str); + if (!str.empty()) { + str.pop_back(); + } + return str; } // takes a hex string and convert it to an unsigned 32bit number (max 8 hex digits) diff --git a/src/core/locale_translations.h b/src/core/locale_translations.h index c3c125b0f..d253607ef 100644 --- a/src/core/locale_translations.h +++ b/src/core/locale_translations.h @@ -896,6 +896,8 @@ MAKE_TRANSLATION(cyl3BottomTemp, "cyl3bottomtemp", "third cylinder bottom temper MAKE_TRANSLATION(cylTopTemp, "cyltoptemp", "cylinder top temperature (TS10)", "Speichertemperatur oben (TS10)", "", "Cylindertemperatur Toppen (TS10)", "", "", "", "", "", "horná teplota valca (TS10)", "") // TODO translate MAKE_TRANSLATION(transferPumpMod, "transferpumpmod", "transfer pump modulation", "Transferpumpenmodulation", "", "Överföringspumpmodulering", "", "", "", "", "", "modulácia prenosového čerpadla", "") // TODO translate MAKE_TRANSLATION(transferPump, "transferpump", "transfer pump", "Transferpumpe", "", "Överföringspump", "", "", "", "", "", "prenosové čerpadlo", "") // TODO translate +MAKE_TRANSLATION(heatAssistOn, "heatassiston", "heat assistance on", "Einschaltdiff. Rücklaufanh.") +MAKE_TRANSLATION(heatAssistOff, "heatassistoff", "heat assistance off", "Ausschaltdiff. Rücklaufanh.") // solar dhw MAKE_TRANSLATION(wwColdTemp, "coldtemp", "cold water", "Kaltwasser", "", "kallvatten", "zimna woda", "", "", "", "", "studená voda", "studená voda") // TODO translate diff --git a/src/core/modbus_entity_parameters.hpp b/src/core/modbus_entity_parameters.hpp index 5f6d0f8d7..a8b1e0a77 100644 --- a/src/core/modbus_entity_parameters.hpp +++ b/src/core/modbus_entity_parameters.hpp @@ -486,6 +486,8 @@ const std::initializer_list Modbus::modbus_register_ma REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(heatCnt), 67, 1), // heatcnt REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(swapFlowTemp), 68, 1), // swapflowtemp REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(swapRetTemp), 69, 1), // swaprettemp + REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(heatAssistOn), 70, 1), // heatassiston + REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DEVICE_DATA, FL_(heatAssistOff), 71, 1), // heatassistoff REGISTER_MAPPING(dt::SOLAR, TAG_TYPE_DHW, FL_(wwMinTemp), 0, 1), // mintemp REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(airHumidity), 0, 1), // airhumidity REGISTER_MAPPING(dt::HEATPUMP, TAG_TYPE_DEVICE_DATA, FL_(dewTemperature), 1, 1), // dewtemperature diff --git a/src/core/mqtt.cpp b/src/core/mqtt.cpp index 32aa5f303..301c0c239 100644 --- a/src/core/mqtt.cpp +++ b/src/core/mqtt.cpp @@ -47,7 +47,7 @@ bool Mqtt::send_response_; bool Mqtt::publish_single_; bool Mqtt::publish_single2cmd_; -std::vector Mqtt::mqtt_subfunctions_; +std::vector> Mqtt::mqtt_subfunctions_; uint32_t Mqtt::mqtt_publish_fails_ = 0; bool Mqtt::connecting_ = false; @@ -119,7 +119,7 @@ void Mqtt::resubscribe() { } for (const auto & mqtt_subfunction : mqtt_subfunctions_) { - queue_subscribe_message(mqtt_subfunction.topic_); + queue_subscribe_message(mqtt_subfunction.topic_.c_str()); } } @@ -1511,4 +1511,16 @@ void Mqtt::add_ha_bool(JsonObject doc) { } } +// adds the bool depending on bool setting +void Mqtt::add_value_bool(JsonObject doc, const char * name, bool value) { + if (EMSESP::system_.bool_format() == BOOL_FORMAT_TRUEFALSE) { + doc[name] = value; + } else if (EMSESP::system_.bool_format() == BOOL_FORMAT_10) { + doc[name] = value ? 1 : 0; + } else { + char result[12]; + doc[name] = Helpers::render_boolean(result, value); + } +} + } // namespace emsesp \ No newline at end of file diff --git a/src/core/mqtt.h b/src/core/mqtt.h index 1dd18ca06..425919aa4 100644 --- a/src/core/mqtt.h +++ b/src/core/mqtt.h @@ -26,6 +26,7 @@ #include "console.h" #include "command.h" #include "emsdevicevalue.h" +#include using uuid::console::Shell; @@ -262,6 +263,7 @@ class Mqtt { const char * cond2 = nullptr, const char * negcond = nullptr); static void add_ha_bool(JsonObject doc); + static void add_value_bool(JsonObject doc, const char * name, bool value); private: static uuid::log::Logger logger_; @@ -279,18 +281,18 @@ class Mqtt { // function handlers for MQTT subscriptions struct MQTTSubFunction { uint8_t device_type_; // which device type, from DeviceType:: - const std::string topic_; // short topic name + const stringPSRAM topic_; // short topic name mqtt_sub_function_p mqtt_subfunction_; // can be empty // replaced &&topic with &topic in 3.7.0-dev.43, so we prevent the std:move later MQTTSubFunction(uint8_t device_type, const std::string & topic, mqtt_sub_function_p mqtt_subfunction) : device_type_(device_type) - , topic_(topic) + , topic_(topic.c_str()) , mqtt_subfunction_(mqtt_subfunction) { } }; - static std::vector mqtt_subfunctions_; // list of mqtt subscribe callbacks for all devices + static std::vector> mqtt_subfunctions_; // list of mqtt subscribe callbacks for all devices uint32_t last_publish_boiler_ = 0; uint32_t last_publish_thermostat_ = 0; diff --git a/src/core/shower.cpp b/src/core/shower.cpp index e440beab2..a1da099b0 100644 --- a/src/core/shower.cpp +++ b/src/core/shower.cpp @@ -195,9 +195,8 @@ void Shower::create_ha_discovery() { JsonDocument doc; char topic[Mqtt::MQTT_TOPIC_MAX_SIZE]; char str[70]; - char stat_t[50]; - doc["~"] = Mqtt::base(); + doc["~"] = Mqtt::base(); // shower active doc["name"] = "Shower Active"; @@ -209,11 +208,11 @@ void Shower::create_ha_discovery() { } doc["uniq_id"] = str; doc["def_ent_id"] = (std::string) "binary_sensor." + str; - doc["stat_t"] = "~/shower_active"; + doc["stat_t"] = "~/shower_active"; Mqtt::add_ha_bool(doc.as()); Mqtt::add_ha_dev_section(doc.as(), "Shower Sensor", nullptr, nullptr, nullptr, false); - Mqtt::add_ha_avail_section(doc.as(), stat_t, true); // no conditions + Mqtt::add_ha_avail_section(doc.as(), "~/shower_active", true); // no conditions snprintf(topic, sizeof(topic), "binary_sensor/%s/shower_active/config", Mqtt::basename().c_str()); ha_configdone_ = Mqtt::queue_ha(topic, doc.as()); // publish the config payload with retain flag @@ -225,8 +224,8 @@ void Shower::create_ha_discovery() { doc["uniq_id"] = str; doc["def_ent_id"] = (std::string) "sensor." + str; - doc["stat_t"] = "~/shower_data", - doc["name"] = "Shower Duration"; + doc["stat_t"] = "~/shower_data"; + doc["name"] = "Shower Duration"; // don't bother with value template conditions if using Domoticz which doesn't fully support MQTT Discovery if (Mqtt::discovery_type() == Mqtt::discoveryType::HOMEASSISTANT) { @@ -241,7 +240,7 @@ void Shower::create_ha_discovery() { // doc["ent_cat"] = "diagnostic"; Mqtt::add_ha_dev_section(doc.as(), "Shower Sensor", nullptr, nullptr, nullptr, false); - Mqtt::add_ha_avail_section(doc.as(), stat_t, false, "value_json.duration is defined"); + Mqtt::add_ha_avail_section(doc.as(), "~/shower_data", false, "value_json.duration is defined"); snprintf(topic, sizeof(topic), "sensor/%s/shower_duration/config", Mqtt::basename().c_str()); Mqtt::queue_ha(topic, doc.as()); // publish the config payload with retain flag diff --git a/src/core/system.cpp b/src/core/system.cpp index 3f092ff21..3884b859a 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -83,15 +83,19 @@ const char * const languages[] = {EMSESP_LOCALE_EN, static constexpr uint8_t NUM_LANGUAGES = sizeof(languages) / sizeof(const char *); +#ifndef EMSESP_STANDALONE +uuid::syslog::SyslogService System::syslog_; +#endif + uuid::log::Logger System::logger_{F_(system), uuid::log::Facility::KERN}; // init statics -PButton System::myPButton_; -bool System::test_set_all_active_ = false; -uint32_t System::max_alloc_mem_; -uint32_t System::heap_mem_; -std::vector System::valid_system_gpios_; -std::vector System::used_gpios_; +PButton System::myPButton_; +bool System::test_set_all_active_ = false; +uint32_t System::max_alloc_mem_; +uint32_t System::heap_mem_; +std::vector> System::valid_system_gpios_; +std::vector> System::used_gpios_; // find the index of the language // 0 = EN, 1 = DE, etc... @@ -366,21 +370,20 @@ void System::syslog_init() { #ifndef EMSESP_STANDALONE if (syslog_enabled_) { // start & configure syslog - EMSESP::logger().info("Starting Syslog service"); - syslog_.start(); - + syslog_.maximum_log_messages(10); syslog_.log_level((uuid::log::Level)syslog_level_); syslog_.mark_interval(syslog_mark_interval_); syslog_.destination(syslog_host_.c_str(), syslog_port_); - syslog_.hostname(hostname().c_str()); - + syslog_.hostname(hostname()); + EMSESP::logger().info("Starting Syslog service"); } else if (syslog_.started()) { // in case service is still running, this flushes the queue // https://github.com/emsesp/EMS-ESP/issues/496 EMSESP::logger().info("Stopping Syslog"); - syslog_.log_level((uuid::log::Level)-1); // stop server + syslog_.loop(); + syslog_.log_level(uuid::log::Level::OFF); // stop server syslog_.mark_interval(0); - syslog_.destination(""); + // syslog_.destination(""); } if (Mqtt::publish_single()) { if (Mqtt::publish_single2cmd()) { @@ -758,7 +761,9 @@ void System::network_init() { int mdio = 18; // Pin# of the I²C IO signal for the Ethernet PHY - hardcoded uint8_t phy_addr = eth_phy_addr_; // I²C-address of Ethernet PHY (0 or 1 for LAN8720, 31 for TLK110) int8_t power = eth_power_; // Pin# of the enable signal for the external crystal oscillator (-1 to disable for internal APLL source) - eth_phy_type_t type = (phy_type_ == PHY_type::PHY_TYPE_LAN8720) ? ETH_PHY_LAN8720 : ETH_PHY_TLK110; // Type of the Ethernet PHY (LAN8720 or TLK110) + eth_phy_type_t type = (phy_type_ == PHY_type::PHY_TYPE_LAN8720) ? ETH_PHY_LAN8720 + : (phy_type_ == PHY_type::PHY_TYPE_TLK110) ? ETH_PHY_TLK110 + : ETH_PHY_RTL8201; // Type of the Ethernet PHY (LAN8720 or TLK110) // clock mode: // ETH_CLOCK_GPIO0_IN = 0 RMII clock input to GPIO0 // ETH_CLOCK_GPIO0_OUT = 1 RMII clock output from GPIO0 @@ -1875,7 +1880,8 @@ bool System::command_info(const char * value, const int8_t id, JsonObject output }); // NTP status - node = output["ntp"].to(); + node = output["ntp"].to(); + node["NTPstatus"] = EMSESP::system_.ntp_connected() ? "connected" : "disconnected"; EMSESP::esp32React.getNTPSettingsService()->read([&](const NTPSettings & settings) { #ifndef EMSESP_STANDALONE node["enabled"] = settings.enabled; @@ -2507,10 +2513,10 @@ uint8_t System::systemStatus() { } // takes a string range like "6-11, 1, 23, 24-48" which has optional ranges and single values and converts to a vector of ints -std::vector System::string_range_to_vector(const std::string & range) { - std::vector gpios; - std::string::size_type pos = 0; - std::string::size_type prev = 0; +std::vector> System::string_range_to_vector(const std::string & range) { + std::vector> gpios; + std::string::size_type pos = 0; + std::string::size_type prev = 0; auto process_part = [&gpios](std::string part) { // trim whitespace @@ -2562,19 +2568,17 @@ void System::set_valid_system_gpios() { // 38 and 39 are input only // 45 and 36 are strapping pins, input only valid_system_gpios_ = string_range_to_vector("0-14, 17, 18, 21, 33-39, 45, 46"); -#elif CONFIG_IDF_TARGET_ESP32 || defined(EMSESP_STANDALONE) - // 1 and 3 are UART0 pins +#elif CONFIG_IDF_TARGET_ESP32 + // 1 and 3 are UART0 pins, but used for some eth-boards (BBQKees-E32, OlimexPOE) // 32-39 is ADC1, input only - valid_system_gpios_ = string_range_to_vector("0, 2, 4, 5, 12-19, 23, 25-27, 32-39"); -#else -#endif - - // if psram is enabled remove pins 16 and 17 from the list, if set -#if CONFIG_IDF_TARGET_ESP32 if (ESP.getPsramSize() > 0) { - valid_system_gpios_.erase(std::remove(valid_system_gpios_.begin(), valid_system_gpios_.end(), 16), valid_system_gpios_.end()); - valid_system_gpios_.erase(std::remove(valid_system_gpios_.begin(), valid_system_gpios_.end(), 17), valid_system_gpios_.end()); + // if psram is enabled remove pins 16 and 17 from the list + valid_system_gpios_ = string_range_to_vector("0-5, 12-15, 18-19, 23, 25-27, 32-39"); + } else { + valid_system_gpios_ = string_range_to_vector("0-5, 12-19, 23, 25-27, 32-39"); } +#elif defined(EMSESP_STANDALONE) + valid_system_gpios_ = string_range_to_vector("0-5, 12-19, 23, 25-27, 32-39"); #endif } diff --git a/src/core/system.h b/src/core/system.h index 31df1d0a0..a265c65c5 100644 --- a/src/core/system.h +++ b/src/core/system.h @@ -33,6 +33,7 @@ #include #endif +#include #include #include @@ -59,7 +60,7 @@ using uuid::console::Shell; namespace emsesp { -enum PHY_type : uint8_t { PHY_TYPE_NONE = 0, PHY_TYPE_LAN8720, PHY_TYPE_TLK110 }; +enum PHY_type : uint8_t { PHY_TYPE_NONE = 0, PHY_TYPE_LAN8720, PHY_TYPE_TLK110, PHY_TYPE_RTL8201 }; enum SYSTEM_STATUS : uint8_t { SYSTEM_STATUS_NORMAL = 0, @@ -395,10 +396,10 @@ class System { void led_monitor(); void system_check(); - static std::vector string_range_to_vector(const std::string & range); + static std::vector> string_range_to_vector(const std::string & range); - static std::vector valid_system_gpios_; // list of valid GPIOs for the ESP32 board that can be used - static std::vector used_gpios_; // list of GPIOs used by the application + static std::vector> valid_system_gpios_; // list of valid GPIOs for the ESP32 board that can be used + static std::vector> used_gpios_; // list of GPIOs used by the application int8_t wifi_quality(int8_t dBm); diff --git a/src/core/telegram.cpp b/src/core/telegram.cpp index c09c6e9a8..1c31f4d51 100644 --- a/src/core/telegram.cpp +++ b/src/core/telegram.cpp @@ -633,7 +633,8 @@ void TxService::retry_tx(const uint8_t operation, const uint8_t * data, const ui EMSESP::wait_validate(0); // do not wait for validation return; } - + // for the last try wait 2 sec before sending. + delayed_send_ = (retry_count_ < MAXIMUM_TX_RETRIES) ? 0 : (uuid::get_uptime() + POST_SEND_DELAY); tx_telegrams_.emplace_front(tx_telegram_id_++, std::move(telegram_last_), true, get_post_send_query()); } diff --git a/src/core/telegram.h b/src/core/telegram.h index 227c40887..b6baeae71 100644 --- a/src/core/telegram.h +++ b/src/core/telegram.h @@ -31,6 +31,7 @@ #endif #include "helpers.h" +#include #define MAX_RX_TELEGRAMS 100 // size of Rx queue #define MAX_TX_TELEGRAMS 160 // size of Tx queue @@ -287,7 +288,7 @@ class RxService : public EMSbus { } }; - std::deque queue() const { + std::deque> queue() const { return rx_telegrams_; } @@ -298,7 +299,8 @@ class RxService : public EMSbus { uint32_t telegram_count_ = 0; // # Rx received uint32_t telegram_error_count_ = 0; // # Rx CRC errors std::shared_ptr rx_telegram; // the incoming Rx telegram - std::deque rx_telegrams_; // the Rx Queue + + std::deque> rx_telegrams_; // the Rx Queue }; class TxService : public EMSbus { @@ -419,7 +421,7 @@ class TxService : public EMSbus { } }; - std::deque queue() const { + std::deque> queue() const { return tx_telegrams_; } @@ -431,7 +433,7 @@ class TxService : public EMSbus { static constexpr uint32_t POST_SEND_DELAY = 2000; private: - std::deque tx_telegrams_; // the Tx queue + std::deque> tx_telegrams_; // the Tx queue uint32_t telegram_read_count_ = 0; // # Tx successful reads uint32_t telegram_write_count_ = 0; // # Tx successful writes diff --git a/src/core/temperaturesensor.cpp b/src/core/temperaturesensor.cpp index ece7bf028..0dcf0a8db 100644 --- a/src/core/temperaturesensor.cpp +++ b/src/core/temperaturesensor.cpp @@ -108,7 +108,7 @@ void TemperatureSensor::loop() { last_activity_ = time_now; } } else if (state_ == State::READING) { - if (temperature_convert_complete() && (time_now - last_activity_ > CONVERSION_MS)) { + if (temperature_convert_complete(time_now - last_activity_)) { #ifdef EMSESP_DEBUG_SENSOR LOG_DEBUG("Scanning for temperature sensors"); #endif @@ -181,12 +181,12 @@ void TemperatureSensor::loop() { default: sensorfails_++; - LOG_ERROR("Unknown sensor %s", Sensor(addr).id().c_str()); + LOG_ERROR("Unknown sensor %s", Sensor(addr).id()); break; } } else { sensorfails_++; - LOG_ERROR("Invalid sensor %s", Sensor(addr).id().c_str()); + LOG_ERROR("Invalid sensor %s", Sensor(addr).id()); } } else { if (!parasite_) { @@ -219,13 +219,13 @@ void TemperatureSensor::loop() { s->set_name("gateway_temperature"); s->set_is_system(true); // mark as internal system temperature sensor if (!EMSESP::nvs_.isKey("intTemp")) { - EMSESP::nvs_.putString("intTemp", s->id().c_str()); + EMSESP::nvs_.putString("intTemp", s->id()); } // LOG_NOTICE("Adding system sensor for gateway temperature %s", s->id().c_str()); EMSESP::webCustomizationService.update([&](WebCustomization & settings) { - auto newSensor = SensorCustomization(); - newSensor.id = s->id(); - newSensor.name = s->name(); + auto newSensor = SensorCustomization(); + strlcpy(newSensor.id, s->id(), sizeof(newSensor.id)); + strlcpy(newSensor.name, s->name(), sizeof(newSensor.name)); newSensor.offset = 0; newSensor.is_system = s->is_system(); // always true settings.sensorCustomizations.push_back(newSensor); @@ -244,21 +244,21 @@ void TemperatureSensor::loop() { #endif } -bool TemperatureSensor::temperature_convert_complete() { +bool TemperatureSensor::temperature_convert_complete(const uint32_t time) { #ifndef EMSESP_STANDALONE if (parasite_) { - return true; // don't care, use the minimum time in loop + return time > CONVERSION_MS; // don't care, use the datasheet time } return bus_.read_bit() == 1; #else - return true; + return time > CONVERSION_MS; #endif } int16_t TemperatureSensor::get_temperature_c(const uint8_t addr[]) { #ifndef EMSESP_STANDALONE if (!bus_.reset()) { - LOG_ERROR("Bus reset failed before reading scratchpad from %s", Sensor(addr).id().c_str()); + LOG_ERROR("Bus reset failed before reading scratchpad from %s", Sensor(addr).id()); return EMS_VALUE_INT16_NOTSET; } YIELD; @@ -270,7 +270,7 @@ int16_t TemperatureSensor::get_temperature_c(const uint8_t addr[]) { YIELD; if (!bus_.reset()) { - LOG_ERROR("Bus reset failed after reading scratchpad from %s", Sensor(addr).id().c_str()); + LOG_ERROR("Bus reset failed after reading scratchpad from %s", Sensor(addr).id()); return EMS_VALUE_INT16_NOTSET; } YIELD; @@ -286,7 +286,7 @@ int16_t TemperatureSensor::get_temperature_c(const uint8_t addr[]) { scratchpad[6], scratchpad[7], scratchpad[8], - Sensor(addr).id().c_str()); + Sensor(addr).id()); return EMS_VALUE_INT16_NOTSET; } @@ -319,10 +319,10 @@ int16_t TemperatureSensor::get_temperature_c(const uint8_t addr[]) { } // update temperature sensor information name and offset -bool TemperatureSensor::update(const std::string & id, const std::string & name, int16_t offset, bool is_system) { +bool TemperatureSensor::update(const char * id, const char * name, int16_t offset, bool hide, bool is_system) { // find the sensor for (auto & sensor : sensors_) { - if (sensor.id() == id) { + if (!strcmp(id, sensor.id())) { // found a match, update the sensor object // if HA is enabled then delete the old record @@ -336,27 +336,27 @@ bool TemperatureSensor::update(const std::string & id, const std::string & name, sensor.set_is_system(is_system); // store the new name and offset in our configuration - EMSESP::webCustomizationService.update([&id, &name, &offset, &is_system, &sensor](WebCustomization & settings) { + EMSESP::webCustomizationService.update([&id, &name, &offset, &sensor, &hide, &is_system](WebCustomization & settings) { // look it up to see if it exists bool found = false; for (auto & SensorCustomization : settings.sensorCustomizations) { - if (SensorCustomization.id == id) { - SensorCustomization.name = name; + if (!strcmp(id, SensorCustomization.id)) { + strlcpy(SensorCustomization.name, name, sizeof(SensorCustomization.name)); SensorCustomization.offset = offset; SensorCustomization.is_system = is_system; found = true; - LOG_DEBUG("Customizing existing sensor ID %s", id.c_str()); + LOG_DEBUG("Customizing existing sensor ID %s", id); break; } } if (!found) { - auto newSensor = SensorCustomization(); - newSensor.id = id; - newSensor.name = name; + auto newSensor = SensorCustomization(); + strlcpy(newSensor.id, id, sizeof(newSensor.id)); + strlcpy(newSensor.name, name, sizeof(newSensor.name)); newSensor.offset = offset; newSensor.is_system = is_system; // is user defined, not system settings.sensorCustomizations.push_back(newSensor); - LOG_DEBUG("Adding new customization for sensor ID %s", id.c_str()); + LOG_DEBUG("Adding new customization for sensor ID %s", id); } sensor.ha_registered = false; // it's changed so we may need to recreate the HA config return StateUpdateResult::CHANGED; @@ -438,30 +438,27 @@ void TemperatureSensor::publish_sensor(const Sensor & sensor) { if (Mqtt::enabled() && Mqtt::publish_single()) { char topic[Mqtt::MQTT_TOPIC_MAX_SIZE]; if (Mqtt::publish_single2cmd()) { - snprintf(topic, sizeof(topic), "%s/%s", F_(temperaturesensor), sensor.name().c_str()); + snprintf(topic, sizeof(topic), "%s/%s", F_(temperaturesensor), sensor.name()); } else { - snprintf(topic, sizeof(topic), "%s%s/%s", F_(temperaturesensor), "_data", sensor.name().c_str()); + snprintf(topic, sizeof(topic), "%s%s/%s", F_(temperaturesensor), "_data", sensor.name()); } char payload[10]; Mqtt::queue_publish(topic, Helpers::render_value(payload, sensor.temperature_c, 10, EMSESP::system_.fahrenheit() ? 2 : 0)); } char cmd[COMMAND_MAX_LENGTH]; - snprintf(cmd, sizeof(cmd), "%s/%s", F_(temperaturesensor), sensor.name().c_str()); + snprintf(cmd, sizeof(cmd), "%s/%s", F_(temperaturesensor), sensor.name()); EMSESP::webSchedulerService.onChange(cmd); } // send empty config topic to remove the entry from HA -void TemperatureSensor::remove_ha_topic(const std::string & id) { +void TemperatureSensor::remove_ha_topic(const char * id) { if (!Mqtt::ha_enabled()) { return; } - LOG_DEBUG("Removing HA config for temperature sensor ID %s", id.c_str()); - // use '_' as HA doesn't like '-' in the topic name - std::string sensorid = id; - std::replace(sensorid.begin(), sensorid.end(), '-', '_'); + LOG_DEBUG("Removing HA config for temperature sensor ID %s", id); char topic[Mqtt::MQTT_TOPIC_MAX_SIZE]; - snprintf(topic, sizeof(topic), "sensor/%s/temperaturesensor_%s/config", Mqtt::basename().c_str(), sensorid.c_str()); + snprintf(topic, sizeof(topic), "sensor/%s/temperaturesensor_%s/config", Mqtt::basename().c_str(), id); Mqtt::queue_remove_topic(topic); } @@ -504,10 +501,10 @@ void TemperatureSensor::publish_values(const bool force) { remove_ha_topic(sensor.id()); sensor.ha_registered = false; } else if (!sensor.ha_registered || force) { - LOG_DEBUG("Recreating HA config for sensor ID %s", sensor.id().c_str()); + LOG_DEBUG("Recreating HA config for sensor ID %s", sensor.id()); JsonDocument config; - config["~"] = Mqtt::base(); + config["~"] = Mqtt::base(); config["dev_cla"] = "temperature"; config["stat_cla"] = "measurement"; @@ -520,10 +517,10 @@ void TemperatureSensor::publish_values(const bool force) { char val_obj[70]; char val_cond[170]; if (Mqtt::is_nested()) { - snprintf(val_obj, sizeof(val_obj), "value_json['%s']['temp']", sensor.id().c_str()); - snprintf(val_cond, sizeof(val_cond), "value_json['%s'] is defined and %s is defined", sensor.id().c_str(), val_obj); + snprintf(val_obj, sizeof(val_obj), "value_json['%s']['temp']", sensor.id()); + snprintf(val_cond, sizeof(val_cond), "value_json['%s'] is defined and %s is defined", sensor.id(), val_obj); } else { - snprintf(val_obj, sizeof(val_obj), "value_json['%s']", sensor.name().c_str()); + snprintf(val_obj, sizeof(val_obj), "value_json['%s']", sensor.name()); snprintf(val_cond, sizeof(val_cond), "%s is defined", val_obj); } @@ -537,17 +534,14 @@ void TemperatureSensor::publish_values(const bool force) { char uniq_s[70]; if (Mqtt::entity_format() == Mqtt::entityFormat::MULTI_SHORT) { - snprintf(uniq_s, sizeof(uniq_s), "%s_%s_%s", Mqtt::basename().c_str(), F_(temperaturesensor), sensor.id().c_str()); + snprintf(uniq_s, sizeof(uniq_s), "%s_%s_%s", Mqtt::basename().c_str(), F_(temperaturesensor), sensor.id()); } else { - snprintf(uniq_s, sizeof(uniq_s), "%s_%s", F_(temperaturesensor), sensor.id().c_str()); + snprintf(uniq_s, sizeof(uniq_s), "%s_%s", F_(temperaturesensor), sensor.id()); } config["uniq_id"] = uniq_s; config["def_ent_id"] = (std::string) "sensor." + uniq_s; - - char name[50]; - snprintf(name, sizeof(name), "%s", sensor.name().c_str()); - config["name"] = name; + config["name"] = sensor.name(); // see if we need to create the [devs] discovery section, as this needs only to be done once for all sensors bool is_ha_device_created = false; @@ -562,7 +556,7 @@ void TemperatureSensor::publish_values(const bool force) { Mqtt::add_ha_avail_section(config.as(), stat_t, !is_ha_device_created, val_cond); char topic[Mqtt::MQTT_TOPIC_MAX_SIZE]; - snprintf(topic, sizeof(topic), "sensor/%s/%s_%s/config", Mqtt::basename().c_str(), F_(temperaturesensor), sensor.id().c_str()); + snprintf(topic, sizeof(topic), "sensor/%s/%s_%s/config", Mqtt::basename().c_str(), F_(temperaturesensor), sensor.id()); sensor.ha_registered = Mqtt::queue_ha(topic, config.as()); } @@ -579,17 +573,15 @@ TemperatureSensor::Sensor::Sensor(const uint8_t addr[]) : internal_id_(((uint64_t)addr[0] << 48) | ((uint64_t)addr[1] << 40) | ((uint64_t)addr[2] << 32) | ((uint64_t)addr[3] << 24) | ((uint64_t)addr[4] << 16) | ((uint64_t)addr[5] << 8) | ((uint64_t)addr[6])) { // create ID string - char id_s[20]; - snprintf(id_s, - sizeof(id_s), + snprintf(id_, + sizeof(id_), "%02X_%04X_%04X_%04X", (unsigned int)(internal_id_ >> 48) & 0xFF, (unsigned int)(internal_id_ >> 32) & 0xFFFF, (unsigned int)(internal_id_ >> 16) & 0xFFFF, (unsigned int)(internal_id_) & 0xFFFF); - id_ = std::string(id_s); - name_ = std::string{}; // name (alias) is empty - offset_ = 0; // 0 degrees offset + name_[0] = '\0'; + offset_ = 0; // 0 degrees offset } uint64_t TemperatureSensor::get_id(const uint8_t addr[]) { @@ -599,8 +591,8 @@ uint64_t TemperatureSensor::get_id(const uint8_t addr[]) { // find the name from the customization service // if empty, return the ID as a string -std::string TemperatureSensor::Sensor::name() const { - if (name_.empty()) { +const char * TemperatureSensor::Sensor::name() const { + if (name_[0] == '\0') { return id_; } return name_; @@ -613,8 +605,8 @@ bool TemperatureSensor::Sensor::apply_customization() { auto const & sensors = settings.sensorCustomizations; if (!sensors.empty()) { for (const auto & sensor : sensors) { - if (id_ == sensor.id) { - LOG_DEBUG("Loading customization for temperature sensor %s", sensor.id.c_str()); + if (!strcmp(sensor.id, id_)) { + LOG_DEBUG("Loading customization for temperature sensor %s", id_); set_name(sensor.name); set_offset(sensor.offset); set_is_system(sensor.is_system); diff --git a/src/core/temperaturesensor.h b/src/core/temperaturesensor.h index 5a40954d9..bd1af5459 100644 --- a/src/core/temperaturesensor.h +++ b/src/core/temperaturesensor.h @@ -30,6 +30,7 @@ #ifndef EMSESP_STANDALONE #include #endif +#include namespace emsesp { @@ -51,7 +52,7 @@ class TemperatureSensor { is_system_ = is_system; } - std::string id() const { + const char * id() const { return id_; } @@ -62,9 +63,10 @@ class TemperatureSensor { offset_ = offset; } - std::string name() const; - void set_name(const std::string & name) { - name_ = name; + const char * name() const; + + void set_name(const char * name) { + strlcpy(name_, name, sizeof(name_)); } bool apply_customization(); @@ -75,11 +77,12 @@ class TemperatureSensor { bool ha_registered = false; private: - uint64_t internal_id_; - std::string id_; - std::string name_; - int16_t offset_; - bool is_system_; + uint64_t internal_id_; + char id_[18]; + char name_[20]; + int16_t offset_; + + bool is_system_; }; TemperatureSensor() = default; @@ -94,7 +97,7 @@ class TemperatureSensor { bool get_value_info(JsonObject output, const char * cmd, const int8_t id = -1); // return back reference to the sensor list, used by other classes - std::vector sensors() const { + std::vector> sensors() const { return sensors_; } @@ -121,7 +124,7 @@ class TemperatureSensor { return sensors_.size(); } - bool update(const std::string & id, const std::string & name, int16_t offset, bool is_system); + bool update(const char* id, const char* name, int16_t offset, bool hide = false, bool is_system = false); #if defined(EMSESP_TEST) void load_test_data(); @@ -159,13 +162,13 @@ class TemperatureSensor { static uuid::log::Logger logger_; - bool temperature_convert_complete(); + bool temperature_convert_complete(const uint32_t time); int16_t get_temperature_c(const uint8_t addr[]); uint64_t get_id(const uint8_t addr[]); void get_value_json(JsonObject output, const Sensor & sensor); - void remove_ha_topic(const std::string & id); + void remove_ha_topic(const char* id); - std::vector sensors_; // our list of active sensors + std::vector> sensors_; // our list of active sensors #ifndef EMSESP_STANDALONE OneWire bus_; diff --git a/src/devices/solar.cpp b/src/devices/solar.cpp index 6b787ccac..95f8a2861 100644 --- a/src/devices/solar.cpp +++ b/src/devices/solar.cpp @@ -417,6 +417,20 @@ Solar::Solar(uint8_t device_type, uint8_t device_id, uint8_t product_id, const c DeviceValueNumOp::DV_NUMOP_DIV10, FL_(swapRetTemp), DeviceValueUOM::DEGREES); + register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, + &heatAssistOn_, + DeviceValueType::INT8, + DeviceValueNumOp::DV_NUMOP_DIV10, + FL_(heatAssistOn), + DeviceValueUOM::K, + MAKE_CF_CB(set_solarHeatAssistOn)); + register_device_value(DeviceValueTAG::TAG_DEVICE_DATA, + &heatAssistOff_, + DeviceValueType::INT8, + DeviceValueNumOp::DV_NUMOP_DIV10, + FL_(heatAssistOff), + DeviceValueUOM::K, + MAKE_CF_CB(set_solarHeatAssistOn)); } } @@ -540,7 +554,8 @@ void Solar::process_SM100Circuit2Config(std::shared_ptr telegram // type 0x35C Heat assistance void Solar::process_SM100HeatAssist(std::shared_ptr telegram) { - has_update(telegram, solarHeatAssist_, 0); // is *10 + has_update(telegram, heatAssistOn_, 0); // is *10 + has_update(telegram, heatAssistOff_, 1); // is *10 } // type 0x361 differential control @@ -1103,4 +1118,22 @@ bool Solar::set_diffControl(const char * value, const int8_t id) { return true; } +bool Solar::set_solarHeatAssistOn(const char * value, const int8_t id) { + float t; + if (!Helpers::value2float(value, t)) { + return false; + } + write_command(0x35C, 0, (uint8_t)(t * 10), 0x35C); + return true; +} + +bool Solar::set_solarHeatAssistOff(const char * value, const int8_t id) { + float t; + if (!Helpers::value2float(value, t)) { + return false; + } + write_command(0x35C, 1, (uint8_t)(t * 10), 0x35C); + return true; +} + } // namespace emsesp diff --git a/src/devices/solar.h b/src/devices/solar.h index f6514c0ee..3dceaff31 100644 --- a/src/devices/solar.h +++ b/src/devices/solar.h @@ -105,7 +105,8 @@ class Solar : public EMSdevice { uint8_t solarPump2Mode_; // 00=off, 01=PWM, 02=10V // telegram 0x35C Heat assistance - uint8_t solarHeatAssist_; // is *10 + int8_t heatAssistOn_; // is *10 + int8_t heatAssistOff_; // is *10 // telegram 0x035F uint8_t cylPriority_; // 0 or 1 @@ -195,6 +196,8 @@ class Solar : public EMSdevice { bool set_cylPriority(const char * value, const int8_t id); bool set_heatAssist(const char * value, const int8_t id); bool set_diffControl(const char * value, const int8_t id); + bool set_solarHeatAssistOn(const char * value, const int8_t id); + bool set_solarHeatAssistOff(const char * value, const int8_t id); }; } // namespace emsesp diff --git a/src/emsesp_version.h b/src/emsesp_version.h index 16bc72b22..830281e32 100644 --- a/src/emsesp_version.h +++ b/src/emsesp_version.h @@ -1 +1 @@ -#define EMSESP_APP_VERSION "3.7.3-dev.34" +#define EMSESP_APP_VERSION "3.7.3-dev.35" diff --git a/src/uart/emsuart_esp32.cpp b/src/uart/emsuart_esp32.cpp index 66bdbdbdc..f4f0e0d17 100644 --- a/src/uart/emsuart_esp32.cpp +++ b/src/uart/emsuart_esp32.cpp @@ -106,6 +106,7 @@ void EMSuart::start(const uint8_t tx_mode, const uint8_t rx_gpio, const uint8_t xTaskCreatePinnedToCore(uart_event_task, "uart_event_task", EMSESP_UART_STACKSIZE, NULL, EMSESP_UART_PRIORITY, &xHandle, EMSESP_UART_RUNNING_CORE); #endif } else { + uart_set_pin(EMSUART_NUM, tx_gpio, rx_gpio, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); vTaskResume(xHandle); } tx_mode_ = tx_mode; diff --git a/src/web/WebAPIService.cpp b/src/web/WebAPIService.cpp index 12647383e..a6220aa49 100644 --- a/src/web/WebAPIService.cpp +++ b/src/web/WebAPIService.cpp @@ -159,7 +159,7 @@ void WebAPIService::parse(AsyncWebServerRequest * request, JsonObject input) { response->setLength(); response->setContentType("application/json; charset=utf-8"); request->send(response); - + api_count_++; // serialize JSON to string to ensure correct content-length and avoid HTTP parsing errors (issue #2752) // std::string output_str; // serializeJson(output, output_str); diff --git a/src/web/WebActivityService.cpp b/src/web/WebActivityService.cpp index 0f80cd335..40c0887bc 100644 --- a/src/web/WebActivityService.cpp +++ b/src/web/WebActivityService.cpp @@ -88,9 +88,7 @@ void WebActivityService::webActivityService(AsyncWebServerRequest * request) { statJson["id"] = 7; statJson["s"] = EMSESP::system_.syslog_count(); statJson["f"] = EMSESP::system_.syslog_fails(); - statJson["q"] = (EMSESP::system_.syslog_count() + EMSESP::system_.syslog_fails()) == 0 - ? 100 - : 100 - (uint8_t)((100 * EMSESP::system_.syslog_fails()) / (EMSESP::system_.syslog_count() + EMSESP::system_.syslog_fails())); + statJson["q"] = EMSESP::system_.syslog_count() == 0 ? 100 : 100 - (uint8_t)((100 * EMSESP::system_.syslog_fails()) / EMSESP::system_.syslog_count()); } #endif diff --git a/src/web/WebCustomEntityService.cpp b/src/web/WebCustomEntityService.cpp index e7c5260ed..3136c8b5c 100644 --- a/src/web/WebCustomEntityService.cpp +++ b/src/web/WebCustomEntityService.cpp @@ -55,7 +55,7 @@ void WebCustomEntity::read(WebCustomEntity & webEntity, JsonObject root) { ei["type_id"] = entityItem.type_id; ei["offset"] = entityItem.offset; ei["factor"] = entityItem.factor; - ei["name"] = entityItem.name; + ei["name"] = (const char *)entityItem.name; ei["uom"] = entityItem.value_type == DeviceValueType::BOOL ? 0 : entityItem.uom; ei["value_type"] = entityItem.value_type; ei["writeable"] = entityItem.writeable; @@ -91,12 +91,12 @@ StateUpdateResult WebCustomEntity::update(JsonObject root, WebCustomEntity & web entityItem.type_id = ei["type_id"]; entityItem.offset = ei["offset"]; entityItem.factor = ei["factor"]; - entityItem.name = ei["name"].as(); entityItem.uom = ei["uom"]; entityItem.value_type = ei["value_type"]; entityItem.writeable = ei["writeable"]; entityItem.hide = ei["hide"] | false; entityItem.data = ei["value"].as(); + strlcpy(entityItem.name, ei["name"].as(), sizeof(entityItem.name)); if (entityItem.ram == 1) { entityItem.device_id = 0; entityItem.type_id = 0; @@ -130,12 +130,12 @@ StateUpdateResult WebCustomEntity::update(JsonObject root, WebCustomEntity & web webCustomEntity.customEntityItems.push_back(entityItem); // add to list - if (entityItem.writeable && !entityItem.name.empty()) { + if (entityItem.writeable && entityItem.name[0] != '\0') { Command::add( EMSdevice::DeviceType::CUSTOM, - webCustomEntity.customEntityItems.back().name.c_str(), + webCustomEntity.customEntityItems.back().name, [webCustomEntity](const char * value, const int8_t id) { - return EMSESP::webCustomEntityService.command_setvalue(value, id, webCustomEntity.customEntityItems.back().name.c_str()); + return EMSESP::webCustomEntityService.command_setvalue(value, id, webCustomEntity.customEntityItems.back().name); }, FL_(entity_cmd), CommandFlag::ADMIN_ONLY); @@ -143,7 +143,7 @@ StateUpdateResult WebCustomEntity::update(JsonObject root, WebCustomEntity & web if (entityItem.ram && doc[entityItem.name].is() && doc[entityItem.name] != entityItem.value) { char cmd[COMMAND_MAX_LENGTH]; - snprintf(cmd, sizeof(cmd), "%s/%s", F_(custom), entityItem.name.c_str()); + snprintf(cmd, sizeof(cmd), "%s/%s", F_(custom), entityItem.name); EMSESP::webSchedulerService.onChange(cmd); } } @@ -196,7 +196,7 @@ bool WebCustomEntityService::command_setvalue(const char * value, const int8_t i if (!Helpers::value2float(value, f)) { return false; } - int v = f / entityItem.factor; + int v = (f / entityItem.factor + 0.5); if (entityItem.value_type == DeviceValueType::UINT8 || entityItem.value_type == DeviceValueType::INT8) { EMSESP::send_write_request(entityItem.type_id, entityItem.device_id, entityItem.offset, v, 0); } else if (entityItem.value_type == DeviceValueType::UINT16 || entityItem.value_type == DeviceValueType::INT16) { @@ -213,7 +213,7 @@ bool WebCustomEntityService::command_setvalue(const char * value, const int8_t i publish(); } char cmd[COMMAND_MAX_LENGTH]; - snprintf(cmd, sizeof(cmd), "%s/%s", F_(custom), entityItem.name.c_str()); + snprintf(cmd, sizeof(cmd), "%s/%s", F_(custom), entityItem.name); EMSESP::webSchedulerService.onChange(cmd); return true; } @@ -224,19 +224,16 @@ bool WebCustomEntityService::command_setvalue(const char * value, const int8_t i // output of a single value // if add_uom is true it will add the UOM string to the value void WebCustomEntityService::render_value(JsonObject output, CustomEntityItem const & entity, const bool useVal, const bool web, const bool add_uom) { - char payload[20]; - std::string name = useVal ? "value" : entity.name; + char payload[20]; + const char * name = useVal ? "value" : entity.name; + switch (entity.value_type) { case DeviceValueType::BOOL: if ((uint8_t)entity.value != EMS_VALUE_BOOL_NOTSET) { if (web) { output[name] = Helpers::render_boolean(payload, (uint8_t)entity.value, true); - } else if (EMSESP::system_.bool_format() == BOOL_FORMAT_TRUEFALSE) { - output[name] = (uint8_t)entity.value ? true : false; - } else if (EMSESP::system_.bool_format() == BOOL_FORMAT_10) { - output[name] = (uint8_t)entity.value ? 1 : 0; } else { - output[name] = Helpers::render_boolean(payload, (uint8_t)entity.value); + Mqtt::add_value_bool(output, name, (uint8_t)entity.value != 0); } } break; @@ -248,7 +245,7 @@ void WebCustomEntityService::render_value(JsonObject output, CustomEntityItem co break; case DeviceValueType::UINT8: if ((uint8_t)entity.value != EMS_VALUE_UINT8_NOTSET) { - std::string v = Helpers::render_value(payload, entity.factor * (uint8_t)entity.value, 2); + std::string v = Helpers::render_value(payload, entity.factor * (int8_t)entity.value, 2); output[name] = add_uom ? serialized(v + ' ' + EMSdevice::uom_to_string(entity.uom)) : serialized(v); } break; @@ -336,8 +333,8 @@ bool WebCustomEntityService::get_value_info(JsonObject output, const char * cmd) // build the json for specific entity void WebCustomEntityService::get_value_json(JsonObject output, CustomEntityItem const & entity) { - output["name"] = entity.name; - output["fullname"] = entity.name; + output["name"] = (const char *)entity.name; + output["fullname"] = (const char *)entity.name; output["storage"] = entity.ram ? "ram" : "ems"; output["type"] = entity.value_type == DeviceValueType::BOOL ? "boolean" : entity.value_type == DeviceValueType::STRING ? "string" : F_(number); output["readable"] = true; @@ -369,9 +366,9 @@ void WebCustomEntityService::publish_single(CustomEntityItem & entity) { char topic[Mqtt::MQTT_TOPIC_MAX_SIZE]; if (Mqtt::publish_single2cmd()) { - snprintf(topic, sizeof(topic), "%s/%s", F_(custom), entity.name.c_str()); + snprintf(topic, sizeof(topic), "%s/%s", F_(custom), entity.name); } else { - snprintf(topic, sizeof(topic), "%s_data/%s", F_(custom), entity.name.c_str()); + snprintf(topic, sizeof(topic), "%s_data/%s", F_(custom), entity.name); } JsonDocument doc; @@ -411,15 +408,15 @@ void WebCustomEntityService::publish(const bool force) { // create HA config if (Mqtt::ha_enabled() && !ha_registered_) { JsonDocument config; - config["~"] = Mqtt::base(); + config["~"] = Mqtt::base(); - char stat_t[50]; + char stat_t[50]; snprintf(stat_t, sizeof(stat_t), "~/%s_data", F_(custom)); config["stat_t"] = stat_t; char val_obj[50]; char val_cond[65]; - snprintf(val_obj, sizeof(val_obj), "value_json['%s']", entityItem.name.c_str()); + snprintf(val_obj, sizeof(val_obj), "value_json['%s']", entityItem.name); snprintf(val_cond, sizeof(val_cond), "%s is defined", val_obj); // don't bother with value template conditions if using Domoticz which doesn't fully support MQTT Discovery if (Mqtt::discovery_type() == Mqtt::discoveryType::HOMEASSISTANT) { @@ -429,31 +426,31 @@ void WebCustomEntityService::publish(const bool force) { } char uniq_s[70]; - snprintf(uniq_s, sizeof(uniq_s), "%s_%s", F_(custom), entityItem.name.c_str()); + snprintf(uniq_s, sizeof(uniq_s), "%s_%s", F_(custom), entityItem.name); config["uniq_id"] = uniq_s; - config["name"] = entityItem.name.c_str(); + config["name"] = (const char *)entityItem.name; char topic[Mqtt::MQTT_TOPIC_MAX_SIZE]; if (entityItem.writeable) { if (entityItem.value_type == DeviceValueType::BOOL) { - snprintf(topic, sizeof(topic), "switch/%s/%s_%s/config", Mqtt::basename().c_str(), F_(custom), entityItem.name.c_str()); + snprintf(topic, sizeof(topic), "switch/%s/%s_%s/config", Mqtt::basename().c_str(), F_(custom), entityItem.name); } else if (entityItem.value_type == DeviceValueType::STRING) { - snprintf(topic, sizeof(topic), "sensor/%s/%s_%s/config", Mqtt::basename().c_str(), F_(custom), entityItem.name.c_str()); + snprintf(topic, sizeof(topic), "sensor/%s/%s_%s/config", Mqtt::basename().c_str(), F_(custom), entityItem.name); } else if (Mqtt::discovery_type() == Mqtt::discoveryType::HOMEASSISTANT || Mqtt::discovery_type() == Mqtt::discoveryType::DOMOTICZ_LATEST) { - snprintf(topic, sizeof(topic), "number/%s/%s_%s/config", Mqtt::basename().c_str(), F_(custom), entityItem.name.c_str()); + snprintf(topic, sizeof(topic), "number/%s/%s_%s/config", Mqtt::basename().c_str(), F_(custom), entityItem.name); } else { - snprintf(topic, sizeof(topic), "sensor/%s/%s_%s/config", Mqtt::basename().c_str(), F_(custom), entityItem.name.c_str()); + snprintf(topic, sizeof(topic), "sensor/%s/%s_%s/config", Mqtt::basename().c_str(), F_(custom), entityItem.name); } char command_topic[Mqtt::MQTT_TOPIC_MAX_SIZE]; - snprintf(command_topic, sizeof(command_topic), "~/%s/%s", F_(custom), entityItem.name.c_str()); + snprintf(command_topic, sizeof(command_topic), "~/%s/%s", F_(custom), entityItem.name); config["cmd_t"] = command_topic; } else { if (entityItem.value_type == DeviceValueType::BOOL) { - snprintf(topic, sizeof(topic), "binary_sensor/%s/%s_%s/config", Mqtt::basename().c_str(), F_(custom), entityItem.name.c_str()); + snprintf(topic, sizeof(topic), "binary_sensor/%s/%s_%s/config", Mqtt::basename().c_str(), F_(custom), entityItem.name); } else { - snprintf(topic, sizeof(topic), "sensor/%s/%s_%s/config", Mqtt::basename().c_str(), F_(custom), entityItem.name.c_str()); + snprintf(topic, sizeof(topic), "sensor/%s/%s_%s/config", Mqtt::basename().c_str(), F_(custom), entityItem.name); } } @@ -523,7 +520,7 @@ void WebCustomEntityService::generate_value_web(JsonObject output, const bool is obj = root_obj; } - obj["id"] = "00" + entity.name; + obj["id"] = std::string("00") + entity.name; if (entity.value_type != DeviceValueType::BOOL) { obj["u"] = entity.uom; } @@ -648,15 +645,15 @@ bool WebCustomEntityService::get_value(std::shared_ptr telegram) if (rest > 0) { memcpy(&entity.raw[offset], message_data, rest); auto data = Helpers::data_to_hex(entity.raw, (uint8_t)length); - if (entity.data != data && length == (int)entity.factor) { - entity.data = data; + if (entity.data != data.c_str() && length == (int)entity.factor) { + entity.data = data.c_str(); if (Mqtt::publish_single()) { publish_single(entity); } else if (EMSESP::mqtt_.get_publish_onchange(0)) { has_change = true; } char cmd[COMMAND_MAX_LENGTH]; - snprintf(cmd, sizeof(cmd), "%s/%s", F_(custom), entity.name.c_str()); + snprintf(cmd, sizeof(cmd), "%s/%s", F_(custom), entity.name); EMSESP::webSchedulerService.onChange(cmd); } } @@ -678,10 +675,10 @@ bool WebCustomEntityService::get_value(std::shared_ptr telegram) has_change = true; } char cmd[COMMAND_MAX_LENGTH]; - snprintf(cmd, sizeof(cmd), "%s/%s", F_(custom), entity.name.c_str()); + snprintf(cmd, sizeof(cmd), "%s/%s", F_(custom), entity.name); EMSESP::webSchedulerService.onChange(cmd); } - // EMSESP::logger().debug("custom entity %s received with value %d", entity.name.c_str(), (int)entity.val); + // EMSESP::logger().debug("custom entity %s received with value %d", entity.name, (int)entity.val); } } @@ -709,7 +706,7 @@ void WebCustomEntityService::load_test_data() { entityItem.type_id = 24; entityItem.offset = 0; entityItem.factor = 1; - entityItem.name = "test_custom"; + strcpy(entityItem.name,"test_custom"); entityItem.uom = 1; entityItem.value_type = 1; entityItem.writeable = true; @@ -718,9 +715,9 @@ void WebCustomEntityService::load_test_data() { webCustomEntity.customEntityItems.push_back(entityItem); Command::add( EMSdevice::DeviceType::CUSTOM, - webCustomEntity.customEntityItems.back().name.c_str(), + webCustomEntity.customEntityItems.back().name, [webCustomEntity](const char * value, const int8_t id) { - return EMSESP::webCustomEntityService.command_setvalue(value, id, webCustomEntity.customEntityItems.back().name.c_str()); + return EMSESP::webCustomEntityService.command_setvalue(value, id, webCustomEntity.customEntityItems.back().name); }, FL_(entity_cmd), CommandFlag::ADMIN_ONLY); @@ -732,7 +729,7 @@ void WebCustomEntityService::load_test_data() { entityItem.type_id = 677; entityItem.offset = 3; entityItem.factor = 1; - entityItem.name = "test_read_only"; + strcpy(entityItem.name, "test_read_only"); entityItem.uom = 0; entityItem.value_type = 2; entityItem.writeable = false; @@ -746,7 +743,7 @@ void WebCustomEntityService::load_test_data() { entityItem.type_id = 0; entityItem.offset = 0; entityItem.factor = 1; - entityItem.name = "test_ram"; + strcpy(entityItem.name, "test_ram"); entityItem.uom = 0; entityItem.value_type = 8; entityItem.writeable = true; @@ -754,9 +751,9 @@ void WebCustomEntityService::load_test_data() { webCustomEntity.customEntityItems.push_back(entityItem); Command::add( EMSdevice::DeviceType::CUSTOM, - webCustomEntity.customEntityItems.back().name.c_str(), + webCustomEntity.customEntityItems.back().name, [webCustomEntity](const char * value, const int8_t id) { - return EMSESP::webCustomEntityService.command_setvalue(value, id, webCustomEntity.customEntityItems.back().name.c_str()); + return EMSESP::webCustomEntityService.command_setvalue(value, id, webCustomEntity.customEntityItems.back().name); }, FL_(entity_cmd), CommandFlag::ADMIN_ONLY); @@ -768,7 +765,7 @@ void WebCustomEntityService::load_test_data() { entityItem.type_id = 0; entityItem.offset = 0; entityItem.factor = 1; - entityItem.name = "test_seltemp"; + strcpy(entityItem.name, "test_seltemp"); entityItem.uom = 0; entityItem.value_type = 8; entityItem.writeable = true; @@ -777,9 +774,9 @@ void WebCustomEntityService::load_test_data() { webCustomEntity.customEntityItems.push_back(entityItem); Command::add( EMSdevice::DeviceType::CUSTOM, - webCustomEntity.customEntityItems.back().name.c_str(), + webCustomEntity.customEntityItems.back().name, [webCustomEntity](const char * value, const int8_t id) { - return EMSESP::webCustomEntityService.command_setvalue(value, id, webCustomEntity.customEntityItems.back().name.c_str()); + return EMSESP::webCustomEntityService.command_setvalue(value, id, webCustomEntity.customEntityItems.back().name); }, FL_(entity_cmd), CommandFlag::ADMIN_ONLY); diff --git a/src/web/WebCustomEntityService.h b/src/web/WebCustomEntityService.h index 018f37fdf..d6149d31e 100644 --- a/src/web/WebCustomEntityService.h +++ b/src/web/WebCustomEntityService.h @@ -16,6 +16,7 @@ * along with this program. If not, see . */ #include "../core/telegram.h" +#include #ifndef WebCustomEntityService_h #define WebCustomEntityService_h @@ -33,11 +34,11 @@ class CustomEntityItem { uint8_t offset; int8_t value_type; uint8_t uom; // DeviceValueUOM - std::string name; + char name[20]; double factor; bool writeable; uint32_t value; - std::string data; + stringPSRAM data; uint8_t ram; uint8_t * raw; bool hide; @@ -45,7 +46,7 @@ class CustomEntityItem { class WebCustomEntity { public: - std::list customEntityItems; + std::list> customEntityItems; static void read(WebCustomEntity & webEntity, JsonObject root); static StateUpdateResult update(JsonObject root, WebCustomEntity & webEntity); @@ -82,7 +83,7 @@ class WebCustomEntityService : public StatefulService { void getEntities(AsyncWebServerRequest * request); - std::list * customEntityItems_; // pointer to the list of entity items + std::list> * customEntityItems_; // pointer to the list of entity items bool ha_registered_ = false; }; diff --git a/src/web/WebCustomizationService.cpp b/src/web/WebCustomizationService.cpp index 35e861b1b..9490b8067 100644 --- a/src/web/WebCustomizationService.cpp +++ b/src/web/WebCustomizationService.cpp @@ -54,23 +54,23 @@ void WebCustomization::read(WebCustomization & customizations, JsonObject root) JsonArray sensorsJson = root["ts"].to(); for (const SensorCustomization & sensor : customizations.sensorCustomizations) { JsonObject sensorJson = sensorsJson.add(); - sensorJson["id"] = sensor.id; // ID of chip - sensorJson["name"] = sensor.name; // n - sensorJson["offset"] = sensor.offset; // o - sensorJson["is_system"] = sensor.is_system; // s for core_voltage, supply_voltage + sensorJson["id"] = (const char *)sensor.id; // ID of chip + sensorJson["name"] = (const char *)sensor.name; // n + sensorJson["offset"] = sensor.offset; // o + sensorJson["is_system"] = sensor.is_system; // s for core_voltage, supply_voltage } // Analog Sensor customization JsonArray analogJson = root["as"].to(); for (const AnalogCustomization & sensor : customizations.analogCustomizations) { JsonObject sensorJson = analogJson.add(); - sensorJson["gpio"] = sensor.gpio; // g - sensorJson["name"] = sensor.name; // n - sensorJson["offset"] = sensor.offset; // o - sensorJson["factor"] = sensor.factor; // f - sensorJson["uom"] = sensor.uom; // u - sensorJson["type"] = sensor.type; // t - sensorJson["is_system"] = sensor.is_system; // s for core_voltage, supply_voltage + sensorJson["gpio"] = sensor.gpio; // g + sensorJson["name"] = (const char *)sensor.name; // n + sensorJson["offset"] = sensor.offset; // o + sensorJson["factor"] = sensor.factor; // f + sensorJson["uom"] = sensor.uom; // u + sensorJson["type"] = sensor.type; // t + sensorJson["is_system"] = sensor.is_system; // s for core_voltage, supply_voltage } // Masked entities customization and custom device name (optional) @@ -83,8 +83,8 @@ void WebCustomization::read(WebCustomization & customizations, JsonObject root) // entries are in the form [optional customname] e.g "08heatingactive|heating is on" JsonArray masked_entityJson = entityJson["entity_ids"].to(); - for (const std::string & entity_id : entityCustomization.entity_ids) { - masked_entityJson.add(entity_id); + for (const auto & entity_id : entityCustomization.entity_ids) { + masked_entityJson.add(entity_id.c_str()); } } } @@ -98,16 +98,18 @@ StateUpdateResult WebCustomization::update(JsonObject root, WebCustomization & c auto sensorsJsons = root["ts"].as(); for (const JsonObject sensorJson : sensorsJsons) { // create each of the sensor, overwriting any previous settings - auto sensor = SensorCustomization(); - sensor.id = sensorJson["id"].as(); - sensor.name = sensorJson["name"].as(); - sensor.offset = sensorJson["offset"]; - if (sensor.id == sensor.name) { - sensor.name = ""; // no need to store id as name - } + auto sensor = SensorCustomization(); + strlcpy(sensor.id, sensorJson["id"].as(), sizeof(sensor.id)); + strlcpy(sensor.name, sensorJson["name"].as(), sizeof(sensor.name)); + sensor.offset = sensorJson["offset"]; sensor.is_system = sensorJson["is_system"] | false; - std::replace(sensor.id.begin(), sensor.id.end(), '-', '_'); // change old ids to v3.7 style - customizations.sensorCustomizations.push_back(sensor); // add to list + // change old ids to v3.7 style + for (char * p = sensor.id; *p != '\0'; p++) { + if (*p == '-') { + *p = '_'; + } + } + customizations.sensorCustomizations.push_back(sensor); // add to list } } @@ -121,12 +123,12 @@ StateUpdateResult WebCustomization::update(JsonObject root, WebCustomization & c if (!EMSESP::system_.add_gpio(analogJson["gpio"].as(), "Analog Sensor")) { EMSESP::logger().warning("Analog sensor: Invalid GPIO %d for %s. Skipping.", analogJson["gpio"].as(), - analogJson["name"].as().c_str()); + analogJson["name"].as()); continue; } - auto analog = AnalogCustomization(); + auto analog = AnalogCustomization(); + strlcpy(analog.name, analogJson["name"].as(), sizeof(analog.name)); analog.gpio = analogJson["gpio"]; - analog.name = analogJson["name"].as(); analog.offset = analogJson["offset"]; analog.factor = analogJson["factor"]; analog.uom = analogJson["uom"]; @@ -153,7 +155,7 @@ StateUpdateResult WebCustomization::update(JsonObject root, WebCustomization & c auto masked_entity_ids = masked_entity["entity_ids"].as(); for (const JsonVariant masked_entity_id : masked_entity_ids) { if (masked_entity_id.is()) { - emsEntity.entity_ids.push_back(masked_entity_id.as()); // add entity list + emsEntity.entity_ids.push_back(masked_entity_id.as().c_str()); // add entity list } } @@ -312,9 +314,9 @@ void WebCustomizationService::customization_entities(AsyncWebServerRequest * req read([&](WebCustomization & settings) { for (EntityCustomization entityCustomization : settings.entityCustomizations) { if (entityCustomization.device_id == device_id) { - for (const std::string & entity_id : entityCustomization.entity_ids) { + for (const auto & entity_id : entityCustomization.entity_ids) { uint8_t mask = Helpers::hextoint(entity_id.substr(0, 2).c_str()); - std::string name = DeviceValue::get_name(entity_id); + std::string name = DeviceValue::get_name(entity_id.c_str()); if (mask & 0x80) { bool is_set = false; for (const JsonVariant id : entity_ids_json) { @@ -382,23 +384,23 @@ void WebCustomizationService::load_test_data() { // Temperature sensors webCustomization.sensorCustomizations.clear(); // delete all existing sensors - auto sensor1 = SensorCustomization(); - sensor1.id = "01_0203_0405_0607"; - sensor1.name = "test_tempsensor1"; + auto sensor1 = SensorCustomization(); + strcpy(sensor1.id, "01_0203_0405_0607"); + strcpy(sensor1.name, "test_tempsensor1"); sensor1.offset = 0; sensor1.is_system = false; webCustomization.sensorCustomizations.push_back(sensor1); - auto sensor2 = SensorCustomization(); - sensor2.id = "0B_0C0D_0E0F_1011"; - sensor2.name = "test_tempsensor2"; + auto sensor2 = SensorCustomization(); + strcpy(sensor2.id, "0B_0C0D_0E0F_1011"); + strcpy(sensor2.name, "test_tempsensor2"); sensor2.offset = 4; sensor2.is_system = false; webCustomization.sensorCustomizations.push_back(sensor2); - auto sensor3 = SensorCustomization(); - sensor3.id = "28_1767_7B13_2502"; - sensor3.name = "gateway_temperature"; + auto sensor3 = SensorCustomization(); + strcpy(sensor3.id, "28_1767_7B13_2502"); + strcpy(sensor3.name, "gateway_temperature"); sensor3.offset = 0; sensor3.is_system = true; webCustomization.sensorCustomizations.push_back(sensor3); @@ -406,9 +408,9 @@ void WebCustomizationService::load_test_data() { // Analog sensors // This actually adds the sensors as we use customizations to store them webCustomization.analogCustomizations.clear(); - auto analog = AnalogCustomization(); - analog.gpio = 36; - analog.name = "test_analogsensor1"; + auto analog = AnalogCustomization(); + analog.gpio = 36; + strcpy(analog.name, "test_analogsensor1"); analog.offset = 0; analog.factor = 0.2; analog.uom = 17; @@ -416,9 +418,9 @@ void WebCustomizationService::load_test_data() { analog.is_system = false; webCustomization.analogCustomizations.push_back(analog); - analog = AnalogCustomization(); - analog.gpio = 37; - analog.name = "test_analogsensor2"; + analog = AnalogCustomization(); + analog.gpio = 37; + strcpy(analog.name, "test_analogsensor2"); analog.offset = 0; analog.factor = 1; analog.uom = 0; @@ -426,9 +428,9 @@ void WebCustomizationService::load_test_data() { analog.is_system = false; webCustomization.analogCustomizations.push_back(analog); - analog = AnalogCustomization(); - analog.gpio = 38; - analog.name = "test_analogsensor3"; + analog = AnalogCustomization(); + analog.gpio = 38; + strcpy(analog.name, "test_analogsensor3"); analog.offset = 0; analog.factor = 1; analog.uom = 0; @@ -436,9 +438,9 @@ void WebCustomizationService::load_test_data() { analog.is_system = false; webCustomization.analogCustomizations.push_back(analog); - analog = AnalogCustomization(); - analog.gpio = 33; - analog.name = "test_analogsensor4"; + analog = AnalogCustomization(); + analog.gpio = 33; + strcpy(analog.name, "test_analogsensor4"); analog.offset = 0; analog.factor = 1; analog.uom = 0; @@ -446,9 +448,9 @@ void WebCustomizationService::load_test_data() { analog.is_system = false; webCustomization.analogCustomizations.push_back(analog); - analog = AnalogCustomization(); - analog.gpio = 39; - analog.name = "test_analogsensor5"; // core_voltage + analog = AnalogCustomization(); + analog.gpio = 39; + strcpy(analog.name, "test_analogsensor5"); // core_voltage analog.offset = 0; analog.factor = 0.003771; analog.uom = 23; diff --git a/src/web/WebCustomizationService.h b/src/web/WebCustomizationService.h index 0a647b4d7..ba905b3ef 100644 --- a/src/web/WebCustomizationService.h +++ b/src/web/WebCustomizationService.h @@ -18,6 +18,7 @@ #ifndef WebCustomizationService_h #define WebCustomizationService_h +#include #define EMSESP_CUSTOMIZATION_FILE "/config/emsespCustomization.json" @@ -34,21 +35,21 @@ namespace emsesp { // Customization for temperature sensor class SensorCustomization { public: - std::string id; - std::string name; - uint16_t offset; - bool is_system; // if true, the customization is a system customization + char id[18]; + char name[20]; + uint16_t offset; + bool is_system; // if true, the customization is a system customization }; class AnalogCustomization { public: - uint8_t gpio; - std::string name; - double offset; - double factor; - uint8_t uom; // 0 is none - int8_t type; // -1 is for deletion - bool is_system; // if true, the customization is a system customization + uint8_t gpio; + char name[20]; + double offset; + double factor; + uint8_t uom; // 0 is none + int8_t type; // -1 is for deletion + bool is_system; // if true, the customization is a system customization // used for removing from a list bool operator==(const AnalogCustomization & a) const { @@ -70,11 +71,12 @@ class EntityCustomization { class WebCustomization { public: - std::list sensorCustomizations; // for sensor names and offsets - std::list analogCustomizations; // for analog sensors - std::list entityCustomizations; // for a list of entities that have a special mask set - static void read(WebCustomization & customizations, JsonObject root); - static StateUpdateResult update(JsonObject root, WebCustomization & customizations); + std::list> sensorCustomizations; // for sensor names and offsets + std::list> analogCustomizations; // for analog sensors + std::list> entityCustomizations; // for a list of entities that have a special mask set + + static void read(WebCustomization & customizations, JsonObject root); + static StateUpdateResult update(JsonObject root, WebCustomization & customizations); private: static bool _start; diff --git a/src/web/WebDataService.cpp b/src/web/WebDataService.cpp index 05af18da0..e12a77df1 100644 --- a/src/web/WebDataService.cpp +++ b/src/web/WebDataService.cpp @@ -158,6 +158,12 @@ void WebDataService::sensor_data(AsyncWebServerRequest * request) { available_gpios.add(gpio); } + // disable types that can only be used once + JsonArray exclude_types = root["exclude_types"].to(); + for (uint8_t type : EMSESP::analogsensor_.exclude_types()) { + exclude_types.add(type); + } + response->setLength(); request->send(response); } @@ -315,8 +321,8 @@ void WebDataService::write_temperature_sensor(AsyncWebServerRequest * request, J if (json.is()) { JsonObject sensor = json; - std::string id = sensor["id"]; // this is the key - std::string name = sensor["name"]; + const char * id = sensor["id"]; // this is the key + const char * name = sensor["name"]; // calculate offset. We'll convert it to an int and * 10 float offset = sensor["offset"]; @@ -325,7 +331,7 @@ void WebDataService::write_temperature_sensor(AsyncWebServerRequest * request, J offset10 = offset / 0.18; } - bool is_system = sensor["is_system"]; + bool is_system = sensor["is_system"] | false; ok = EMSESP::temperaturesensor_.update(id, name, offset10, is_system); } @@ -340,15 +346,15 @@ void WebDataService::write_analog_sensor(AsyncWebServerRequest * request, JsonVa if (json.is()) { JsonObject analog = json; - uint8_t gpio = analog["gpio"]; - std::string name = analog["name"]; - double factor = analog["factor"]; - double offset = analog["offset"]; - uint8_t uom = analog["uom"]; - int8_t type = analog["type"]; - bool deleted = analog["deleted"]; - bool is_system = analog["is_system"]; - ok = EMSESP::analogsensor_.update(gpio, name, offset, factor, uom, type, deleted, is_system); + uint8_t gpio = analog["gpio"]; + const char * name = analog["name"]; + double factor = analog["factor"]; + double offset = analog["offset"]; + uint8_t uom = analog["uom"]; + int8_t type = analog["type"]; + bool deleted = analog["deleted"]; + bool is_system = analog["is_system"] | false; + ok = EMSESP::analogsensor_.update(gpio, name, offset, factor, uom, type, deleted, is_system); } AsyncWebServerResponse * response = request->beginResponse(ok ? 200 : 400); // ok or bad request @@ -408,7 +414,7 @@ void WebDataService::dashboard_data(AsyncWebServerRequest * request) { node["id"] = (EMSdevice::DeviceTypeUniqueID::TEMPERATURESENSOR_UID * 100) + count++; JsonObject dv = node["dv"].to(); - dv["id"] = "00" + sensor.name(); + dv["id"] = std::string("00") + sensor.name(); if (EMSESP::system_.fahrenheit()) { if (Helpers::hasValue(sensor.temperature_c)) { dv["v"] = (float)sensor.temperature_c * 0.18 + 32; @@ -439,7 +445,7 @@ void WebDataService::dashboard_data(AsyncWebServerRequest * request) { node["id"] = (EMSdevice::DeviceTypeUniqueID::ANALOGSENSOR_UID * 100) + count++; JsonObject dv = node["dv"].to(); - dv["id"] = "00" + sensor.name(); + dv["id"] = std::string("00") + sensor.name(); #if CONFIG_IDF_TARGET_ESP32 if (sensor.type() == AnalogSensor::AnalogType::DIGITAL_OUT && (sensor.gpio() == 25 || sensor.gpio() == 26)) { obj["v"] = Helpers::transformNumFloat(sensor.value()); @@ -462,7 +468,8 @@ void WebDataService::dashboard_data(AsyncWebServerRequest * request) { } if (sensor.type() == AnalogSensor::AnalogType::COUNTER || (sensor.type() >= AnalogSensor::AnalogType::DIGITAL_OUT && sensor.type() <= AnalogSensor::AnalogType::PWM_2) - || sensor.type() == AnalogSensor::AnalogType::RGB || sensor.type() == AnalogSensor::AnalogType::PULSE) { + || sensor.type() == AnalogSensor::AnalogType::RGB || sensor.type() == AnalogSensor::AnalogType::PULSE + || (sensor.type() >= AnalogSensor::AnalogType::CNT_0 && sensor.type() <= AnalogSensor::AnalogType::CNT_2)) { dv["c"] = sensor.name(); } } @@ -479,12 +486,12 @@ void WebDataService::dashboard_data(AsyncWebServerRequest * request) { EMSESP::webSchedulerService.read([&](const WebScheduler & webScheduler) { for (const ScheduleItem & scheduleItem : webScheduler.scheduleItems) { // only add if we have a name - we don't need a u (UOM) for this - if (!scheduleItem.name.empty()) { + if (scheduleItem.name[0] != '\0') { JsonObject node = nodes.add(); node["id"] = (EMSdevice::DeviceTypeUniqueID::SCHEDULER_UID * 100) + count++; JsonObject dv = node["dv"].to(); - dv["id"] = "00" + scheduleItem.name; + dv["id"] = std::string("00") + scheduleItem.name; dv["c"] = scheduleItem.name; char s[12]; dv["v"] = Helpers::render_boolean(s, scheduleItem.active, true); diff --git a/src/web/WebLogService.cpp b/src/web/WebLogService.cpp index cd1b29842..15cdbc877 100644 --- a/src/web/WebLogService.cpp +++ b/src/web/WebLogService.cpp @@ -40,7 +40,8 @@ WebLogService::WebLogService(AsyncWebServer * server, SecurityManager * security // start the log service with INFO level void WebLogService::begin() { - uuid::log::Logger::register_handler(this, uuid::log::Level::INFO); + level_ = uuid::log::Level::INFO; + uuid::log::Logger::register_handler(this, level_); } // apply the user settings @@ -49,24 +50,10 @@ void WebLogService::start() { maximum_log_messages_ = settings.weblog_buffer; limit_log_messages_ = maximum_log_messages_; compact_ = settings.weblog_compact; - uuid::log::Logger::register_handler(this, (uuid::log::Level)settings.weblog_level); - if ((uuid::log::Level)settings.weblog_level == uuid::log::Level::OFF) { - log_messages_.clear(); - } + level_ = (uuid::log::Level)settings.weblog_level; }); -} - -uuid::log::Level WebLogService::log_level() const { - return uuid::log::Logger::get_log_level(this); -} - -void WebLogService::log_level(uuid::log::Level level) { - EMSESP::webSettingsService.update([&](WebSettings & settings) { - settings.weblog_level = level; - return StateUpdateResult::CHANGED; - }); - uuid::log::Logger::register_handler(this, level); - if (level == uuid::log::Level::OFF) { + uuid::log::Logger::register_handler(this, level_); + if (level_ == uuid::log::Level::OFF) { log_messages_.clear(); } } @@ -75,58 +62,32 @@ size_t WebLogService::num_log_messages() const { return log_messages_.size(); } -size_t WebLogService::maximum_log_messages() const { - return maximum_log_messages_; -} - -void WebLogService::maximum_log_messages(size_t count) { - maximum_log_messages_ = std::max((size_t)1, count); - - if (limit_log_messages_ > maximum_log_messages_) { - limit_log_messages_ = maximum_log_messages_; - } - - while (log_messages_.size() > maximum_log_messages_) { - log_messages_.pop_front(); - } - - EMSESP::webSettingsService.update([&](WebSettings & settings) { - settings.weblog_buffer = count; - return StateUpdateResult::CHANGED; - }); -} - -bool WebLogService::compact() const { - return compact_; -} - -void WebLogService::compact(bool compact) { - compact_ = compact; - EMSESP::webSettingsService.update([&](WebSettings & settings) { - settings.weblog_compact = compact; - return StateUpdateResult::CHANGED; - }); -} - -WebLogService::QueuedLogMessage::QueuedLogMessage(unsigned long id, std::shared_ptr && content) +WebLogService::QueuedLogMessage::QueuedLogMessage(unsigned long id, uint64_t uptime, uuid::log::Level level, const char * name, std::string text) : id_(id) - , content_(std::move(content)) { + , uptime_(uptime) + , level_(level) + , name_(name) + , text_(text.c_str()) { } void WebLogService::operator<<(std::shared_ptr message) { #ifndef EMSESP_STANDALONE - size_t maxAlloc = ESP.getMaxAllocHeap(); - if (limit_log_messages_ > 5 && maxAlloc < 46080) { - --limit_log_messages_; - } else if (limit_log_messages_ < maximum_log_messages_ && maxAlloc > 51200) { - ++limit_log_messages_; + if (emsesp::EMSESP::system_.PSram()) { + limit_log_messages_ = maximum_log_messages_; + } else { + uint32_t maxAlloc = ESP.getMaxAllocHeap(); + if (limit_log_messages_ > 5 && maxAlloc < (50 * 1024)) { // 50k + --limit_log_messages_; + } else if (limit_log_messages_ < maximum_log_messages_ && maxAlloc > (60 * 1024)) { // 60k + ++limit_log_messages_; + } } #endif while (log_messages_.size() >= limit_log_messages_) { log_messages_.pop_front(); } - log_messages_.emplace_back(++log_message_id_, std::move(message)); + log_messages_.emplace_back(++log_message_id_, message->uptime_ms, message->level, message->name, message->text.c_str()); } // dumps out the contents of log buffer to shell console @@ -136,30 +97,28 @@ void WebLogService::show(Shell & shell) { } shell.println(); - shell.printfln("Recent Log (level %s, max %d messages):", format_level_uppercase(log_level()), maximum_log_messages()); + shell.printfln("Recent Log (level %s, max %d messages):", uuid::log::format_level_uppercase(level_), maximum_log_messages_); shell.println(); for (const auto & message : log_messages_) { - log_message_id_tail_ = message.id_; - char time_string[26]; - shell.print(messagetime(time_string, message.content_->uptime_ms, sizeof(time_string))); - shell.printf(" %c %lu: [%s] ", uuid::log::format_level_char(message.content_->level), message.id_, message.content_->name); + shell.print(messagetime(time_string, message.uptime_, sizeof(time_string))); + shell.printf(" %c %lu: [%s] ", uuid::log::format_level_char(message.level_), message.id_, message.name_); - if ((message.content_->level == uuid::log::Level::ERR) || (message.content_->level == uuid::log::Level::WARNING)) { + if ((message.level_ == uuid::log::Level::ERR) || (message.level_ == uuid::log::Level::WARNING)) { shell.print(COLOR_RED); - shell.println(message.content_->text); + shell.println(message.text_.c_str()); shell.print(COLOR_RESET); - } else if (message.content_->level == uuid::log::Level::INFO) { + } else if (message.level_ == uuid::log::Level::INFO) { shell.print(COLOR_YELLOW); - shell.println(message.content_->text); + shell.println(message.text_.c_str()); shell.print(COLOR_RESET); - } else if (message.content_->level == uuid::log::Level::DEBUG) { + } else if (message.level_ == uuid::log::Level::DEBUG) { shell.print(COLOR_CYAN); - shell.println(message.content_->text); + shell.println(message.text_.c_str()); shell.print(COLOR_RESET); } else { - shell.println(message.content_->text); + shell.println(message.text_.c_str()); } } @@ -187,7 +146,6 @@ void WebLogService::loop() { // flush for (const auto & message : log_messages_) { if (message.id_ > log_message_id_tail_) { - log_message_id_tail_ = message.id_; transmit(message); return; } @@ -201,8 +159,8 @@ char * WebLogService::messagetime(char * out, const uint64_t t, const size_t buf strlcpy(out, uuid::log::format_timestamp_ms(t, 3).c_str(), bufsize); } else { time_t t1 = offset + (time_t)(t / 1000); - char timestr[bufsize]; - strftime(timestr, bufsize, "%FT%T", localtime(&t1)); + char timestr[25]; + strftime(timestr, 25, "%FT%T", localtime(&t1)); snprintf(out, bufsize, "%s.%03d", timestr, (uint16_t)(t % 1000)); } return out; @@ -215,17 +173,18 @@ void WebLogService::transmit(const QueuedLogMessage & message) { JsonObject logEvent = jsonDocument.to(); char time_string[25]; - logEvent["t"] = messagetime(time_string, message.content_->uptime_ms, sizeof(time_string)); - logEvent["l"] = message.content_->level; + logEvent["t"] = messagetime(time_string, message.uptime_, sizeof(time_string)); + logEvent["l"] = message.level_; // .content_->level; logEvent["i"] = message.id_; - logEvent["n"] = message.content_->name; - logEvent["m"] = message.content_->text; + logEvent["n"] = message.name_; // content_->name; + logEvent["m"] = message.text_; // content_->text; - size_t len = measureJson(jsonDocument); - char * buffer = new char[len + 1]; + size_t len = measureJson(jsonDocument) + 1; + char * buffer = new char[len]; if (buffer) { - serializeJson(jsonDocument, buffer, len + 1); + serializeJson(jsonDocument, buffer, len); events_.send(buffer, "message", message.id_); + log_message_id_tail_ = message.id_; } delete[] buffer; } @@ -236,9 +195,9 @@ void WebLogService::getSetValues(AsyncWebServerRequest * request, JsonVariant js // GET - return the values auto * response = new AsyncJsonResponse(false); JsonObject root = response->getRoot(); - root["level"] = log_level(); - root["max_messages"] = maximum_log_messages(); - root["compact"] = compact(); + root["level"] = level_; + root["max_messages"] = maximum_log_messages_; + root["compact"] = compact_; root["psram"] = (EMSESP::system_.PSram() > 0); root["developer_mode"] = EMSESP::system_.developer_mode(); @@ -250,19 +209,29 @@ void WebLogService::getSetValues(AsyncWebServerRequest * request, JsonVariant js return; } + // POST - write the settings + level_ = json["level"]; + maximum_log_messages_ = json["max_messages"]; + compact_ = json["compact"]; - // POST - set the values - auto && body = json.as(); + uuid::log::Logger::register_handler(this, level_); + if (level_ == uuid::log::Level::OFF) { + log_messages_.clear(); + } - uuid::log::Level level = body["level"]; - log_level(level); - - uint8_t max_messages = body["max_messages"]; - maximum_log_messages(max_messages); - - bool comp = body["compact"]; - compact(comp); + if (limit_log_messages_ > maximum_log_messages_) { + limit_log_messages_ = maximum_log_messages_; + while (log_messages_.size() > limit_log_messages_) { + log_messages_.pop_front(); + } + } + EMSESP::webSettingsService.update([&](WebSettings & settings) { + settings.weblog_compact = compact_; + settings.weblog_level = level_; + settings.weblog_buffer = maximum_log_messages_; + return StateUpdateResult::CHANGED; + }); request->send(200); // OK } diff --git a/src/web/WebLogService.h b/src/web/WebLogService.h index a369d5a4c..5e5c328c1 100644 --- a/src/web/WebLogService.h +++ b/src/web/WebLogService.h @@ -21,6 +21,7 @@ #define EMSESP_EVENT_SOURCE_LOG_PATH "/es/log" #define EMSESP_LOG_SETTINGS_PATH "/rest/logSettings" +#include using ::uuid::console::Shell; @@ -33,17 +34,11 @@ class WebLogService : public uuid::log::Handler { WebLogService(AsyncWebServer * server, SecurityManager * securityManager); - void begin(); - void start(); - uuid::log::Level log_level() const; - void log_level(uuid::log::Level level); - size_t maximum_log_messages() const; - size_t num_log_messages() const; - void maximum_log_messages(size_t count); - bool compact() const; - void compact(bool compact); - void loop(); - void show(Shell & shell); + void begin(); + void start(); + size_t num_log_messages() const; + void loop(); + void show(Shell & shell); virtual void operator<<(std::shared_ptr message); @@ -52,12 +47,16 @@ class WebLogService : public uuid::log::Handler { class QueuedLogMessage { public: - QueuedLogMessage(unsigned long id, std::shared_ptr && content); + QueuedLogMessage(unsigned long id, uint64_t uptime, uuid::log::Level level, const char * name, std::string text); + ~QueuedLogMessage() = default; - unsigned long id_; // Sequential identifier for this log message - struct timeval time_; // Time message was received - const std::shared_ptr content_; // Log message content + unsigned long id_; // Sequential identifier for this log message + struct timeval time_; // Time message was received + uint64_t uptime_; + uuid::log::Level level_; + const char * name_; + stringPSRAM text_; }; void transmit(const QueuedLogMessage & message); @@ -65,12 +64,13 @@ class WebLogService : public uuid::log::Handler { char * messagetime(char * out, const uint64_t t, const size_t bufsize); - size_t maximum_log_messages_ = MAX_LOG_MESSAGES; // Maximum number of log messages to buffer before they are output - size_t limit_log_messages_ = 1; // dynamic limit - unsigned long log_message_id_ = 0; // The next identifier to use for queued log messages - unsigned long log_message_id_tail_ = 0; // last event shown on the screen after fetch - std::deque log_messages_; // Queued log messages, in the order they were received - bool compact_ = true; + std::deque> log_messages_; // Queued log messages, in the order they were received + size_t maximum_log_messages_ = MAX_LOG_MESSAGES; // Maximum number of log messages to buffer before they are output + size_t limit_log_messages_ = 1; // dynamic limit + unsigned long log_message_id_ = 0; // The next identifier to use for queued log messages + unsigned long log_message_id_tail_ = 0; // last event shown on the screen after fetch + bool compact_ = true; + uuid::log::Level level_ = uuid::log::Level::INFO; }; } // namespace emsesp diff --git a/src/web/WebSchedulerService.cpp b/src/web/WebSchedulerService.cpp index e5ad1b809..0c152270e 100644 --- a/src/web/WebSchedulerService.cpp +++ b/src/web/WebSchedulerService.cpp @@ -87,19 +87,19 @@ StateUpdateResult WebScheduler::update(JsonObject root, WebScheduler & webSchedu si.time = si.flags == SCHEDULEFLAG_SCHEDULE_IMMEDIATE ? "" : schedule["time"].as(); si.cmd = schedule["cmd"].as(); si.value = schedule["value"].as(); - si.name = schedule["name"].as(); + strlcpy(si.name, schedule["name"].as(), sizeof(si.name)); // calculated elapsed minutes - si.elapsed_min = Helpers::string2minutes(si.time); + si.elapsed_min = Helpers::string2minutes(si.time.c_str()); si.retry_cnt = 0xFF; // no startup retries webScheduler.scheduleItems.push_back(si); // add to list - if (!webScheduler.scheduleItems.back().name.empty()) { + if (webScheduler.scheduleItems.back().name[0] != '\0') { Command::add( EMSdevice::DeviceType::SCHEDULER, - webScheduler.scheduleItems.back().name.c_str(), + webScheduler.scheduleItems.back().name, [webScheduler](const char * value, const int8_t id) { - return EMSESP::webSchedulerService.command_setvalue(value, id, webScheduler.scheduleItems.back().name.c_str()); + return EMSESP::webSchedulerService.command_setvalue(value, id, webScheduler.scheduleItems.back().name); }, FL_(schedule_cmd), CommandFlag::ADMIN_ONLY); @@ -146,15 +146,8 @@ bool WebSchedulerService::get_value_info(JsonObject output, const char * cmd) { if (!strlen(cmd) || !strcmp(cmd, F_(values)) || !strcmp(cmd, F_(info))) { // list all names for (const ScheduleItem & scheduleItem : *scheduleItems_) { - if (!scheduleItem.name.empty()) { - if (EMSESP::system_.bool_format() == BOOL_FORMAT_TRUEFALSE) { - output[scheduleItem.name] = scheduleItem.active; - } else if (EMSESP::system_.bool_format() == BOOL_FORMAT_10) { - output[scheduleItem.name] = scheduleItem.active ? 1 : 0; - } else { - char result[12]; - output[scheduleItem.name] = Helpers::render_boolean(result, scheduleItem.active); - } + if (scheduleItem.name[0] != '\0') { + Mqtt::add_value_bool(output, scheduleItem.name, scheduleItem.active); } } return true; @@ -162,9 +155,9 @@ bool WebSchedulerService::get_value_info(JsonObject output, const char * cmd) { if (!strcmp(cmd, F_(entities))) { uint8_t i = 0; - char name[30]; + char name[20]; for (const ScheduleItem & scheduleItem : *scheduleItems_) { - strlcpy(name, scheduleItem.name == "" ? Helpers::smallitoa(name, i++) : scheduleItem.name.c_str(), sizeof(name)); + strlcpy(name, scheduleItem.name[0] == '\0' ? Helpers::smallitoa(name, i++) : scheduleItem.name, sizeof(name)); get_value_json(output[name].to(), scheduleItem); } return true; @@ -183,17 +176,10 @@ bool WebSchedulerService::get_value_info(JsonObject output, const char * cmd) { // build the json for specific entity void WebSchedulerService::get_value_json(JsonObject output, const ScheduleItem & scheduleItem) { - output["name"] = scheduleItem.name; - output["fullname"] = scheduleItem.name; + output["name"] = (const char *)scheduleItem.name; + output["fullname"] = (const char *)scheduleItem.name; output["type"] = "boolean"; - if (EMSESP::system_.bool_format() == BOOL_FORMAT_TRUEFALSE) { - output["value"] = scheduleItem.active; - } else if (EMSESP::system_.bool_format() == BOOL_FORMAT_10) { - output["value"] = scheduleItem.active ? 1 : 0; - } else { - char result[12]; - output["value"] = Helpers::render_boolean(result, scheduleItem.active); - } + Mqtt::add_value_bool(output, "value", scheduleItem.active); if (scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_CONDITION) { output["condition"] = scheduleItem.time; } else if (scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_ONCHANGE) { @@ -205,7 +191,7 @@ void WebSchedulerService::get_value_json(JsonObject output, const ScheduleItem & } output["command"] = scheduleItem.cmd; output["cmd_data"] = scheduleItem.value; - bool hasName = scheduleItem.name != ""; + bool hasName = scheduleItem.name[0] != '\0'; output["readable"] = hasName; output["writeable"] = hasName; output["visible"] = hasName; @@ -221,7 +207,7 @@ void WebSchedulerService::publish_single(const char * name, const bool state) { if (Mqtt::publish_single2cmd()) { snprintf(topic, sizeof(topic), "%s/%s", F_(scheduler), name); } else { - snprintf(topic, sizeof(topic), "%s%s/%s", F_(scheduler), "_data", name); + snprintf(topic, sizeof(topic), "%s_data/%s", F_(scheduler), name); } char payload[12]; @@ -244,36 +230,28 @@ void WebSchedulerService::publish(const bool force) { if (Mqtt::publish_single() && force) { for (const ScheduleItem & scheduleItem : *scheduleItems_) { - publish_single(scheduleItem.name.c_str(), scheduleItem.active); + publish_single(scheduleItem.name, scheduleItem.active); } } JsonDocument doc; bool ha_created = ha_registered_; for (const ScheduleItem & scheduleItem : *scheduleItems_) { - if (!scheduleItem.name.empty() && !doc[scheduleItem.name].is()) { - if (EMSESP::system_.bool_format() == BOOL_FORMAT_TRUEFALSE) { - doc[scheduleItem.name] = scheduleItem.active; - } else if (EMSESP::system_.bool_format() == BOOL_FORMAT_10) { - doc[scheduleItem.name] = scheduleItem.active ? 1 : 0; - } else { - char result[12]; - doc[scheduleItem.name] = Helpers::render_boolean(result, scheduleItem.active); - } + if (scheduleItem.name[0] != '\0' && !doc[scheduleItem.name].is()) { + Mqtt::add_value_bool(doc.as(), scheduleItem.name, scheduleItem.active); // create HA config if (Mqtt::ha_enabled() && !ha_registered_) { - JsonDocument config; - config["~"] = Mqtt::base(); + config["~"] = Mqtt::base(); - char stat_t[50]; + char stat_t[50]; snprintf(stat_t, sizeof(stat_t), "~/%s_data", F_(scheduler)); config["stat_t"] = stat_t; char val_obj[50]; char val_cond[65]; - snprintf(val_obj, sizeof(val_obj), "value_json['%s']", scheduleItem.name.c_str()); + snprintf(val_obj, sizeof(val_obj), "value_json['%s']", scheduleItem.name); snprintf(val_cond, sizeof(val_cond), "%s is defined", val_obj); if (Mqtt::discovery_type() == Mqtt::discoveryType::HOMEASSISTANT) { @@ -283,17 +261,17 @@ void WebSchedulerService::publish(const bool force) { } char uniq_s[70]; - snprintf(uniq_s, sizeof(uniq_s), "%s_%s", F_(scheduler), scheduleItem.name.c_str()); + snprintf(uniq_s, sizeof(uniq_s), "%s_%s", F_(scheduler), scheduleItem.name); config["uniq_id"] = uniq_s; - config["name"] = scheduleItem.name.c_str(); - config["def_ent_id"] = (std::string) "switch." + uniq_s; + config["name"] = scheduleItem.name; + config["def_ent_id"] = std::string("switch.") + uniq_s; char topic[Mqtt::MQTT_TOPIC_MAX_SIZE]; char command_topic[Mqtt::MQTT_TOPIC_MAX_SIZE]; - snprintf(topic, sizeof(topic), "switch/%s/%s_%s/config", Mqtt::basename().c_str(), F_(scheduler), scheduleItem.name.c_str()); - snprintf(command_topic, sizeof(command_topic), "~/%s/%s", F_(scheduler), scheduleItem.name.c_str()); + snprintf(topic, sizeof(topic), "switch/%s/%s_%s/config", Mqtt::basename().c_str(), F_(scheduler), scheduleItem.name); + snprintf(command_topic, sizeof(command_topic), "~/%s/%s", F_(scheduler), scheduleItem.name); config["cmd_t"] = command_topic; Mqtt::add_ha_bool(config.as()); @@ -318,15 +296,13 @@ void WebSchedulerService::publish(const bool force) { uint8_t WebSchedulerService::count_entities(bool cmd_only) { uint8_t count = 0; for (const ScheduleItem & scheduleItem : *scheduleItems_) { - if (!scheduleItem.name.empty() || !cmd_only) { + if (scheduleItem.name[0] != '\0' || !cmd_only) { count++; } } return count; } - - // execute scheduled command bool WebSchedulerService::command(const char * name, const std::string & command, const std::string & data) { std::string cmd = Helpers::toLower(command); @@ -428,7 +404,7 @@ bool WebSchedulerService::command(const char * name, const std::string & command bool WebSchedulerService::onChange(const char * cmd) { for (ScheduleItem & scheduleItem : *scheduleItems_) { if (scheduleItem.active && scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_ONCHANGE - && Helpers::toLower(scheduleItem.time).find(Helpers::toLower(cmd)) != std::string::npos) { + && Helpers::toLower(scheduleItem.time.c_str()).find(Helpers::toLower(cmd)) != std::string::npos) { cmd_changed_.push_back(&scheduleItem); return true; } @@ -440,12 +416,12 @@ bool WebSchedulerService::onChange(const char * cmd) { void WebSchedulerService::condition() { for (ScheduleItem & scheduleItem : *scheduleItems_) { if (scheduleItem.active && scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_CONDITION) { - auto match = compute(scheduleItem.time); + auto match = compute(scheduleItem.time.c_str()); #ifdef EMESESP_DEBUG // EMSESP::logger().debug("condition match: %s", match.c_str()); #endif if (match.length() == 1 && match[0] == '1' && scheduleItem.retry_cnt == 0xFF) { - scheduleItem.retry_cnt = command(scheduleItem.name.c_str(), scheduleItem.cmd, compute(scheduleItem.value)) ? 1 : 0xFF; + scheduleItem.retry_cnt = command(scheduleItem.name, scheduleItem.cmd.c_str(), compute(scheduleItem.value.c_str())) ? 1 : 0xFF; } else if (match.length() == 1 && match[0] == '0' && scheduleItem.retry_cnt == 1) { scheduleItem.retry_cnt = 0xFF; } else if (match.length() != 1) { // the match is not boolean @@ -475,13 +451,13 @@ void WebSchedulerService::loop() { // check if we have onChange events while (!cmd_changed_.empty()) { ScheduleItem si = *cmd_changed_.front(); - command(si.name.c_str(), si.cmd, compute(si.value)); + command(si.name, si.cmd.c_str(), compute(si.value.c_str())); cmd_changed_.pop_front(); } for (ScheduleItem & scheduleItem : *scheduleItems_) { if (scheduleItem.active && scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_IMMEDIATE) { - command(scheduleItem.name.c_str(), scheduleItem.cmd, compute(scheduleItem.value)); + command(scheduleItem.name, scheduleItem.cmd.c_str(), compute(scheduleItem.value.c_str())); scheduleItem.active = false; } } @@ -497,7 +473,7 @@ void WebSchedulerService::loop() { if (last_tm_min == -2) { for (ScheduleItem & scheduleItem : *scheduleItems_) { if (scheduleItem.active && scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_TIMER && scheduleItem.elapsed_min == 0) { - scheduleItem.retry_cnt = command(scheduleItem.name.c_str(), scheduleItem.cmd, compute(scheduleItem.value)) ? 0xFF : 0; + scheduleItem.retry_cnt = command(scheduleItem.name, scheduleItem.cmd.c_str(), compute(scheduleItem.value.c_str())) ? 0xFF : 0; } } last_tm_min = -1; // startup done, now use for RTC @@ -510,12 +486,12 @@ void WebSchedulerService::loop() { // retry startup commands not yet executed if (scheduleItem.active && scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_TIMER && scheduleItem.elapsed_min == 0 && scheduleItem.retry_cnt < MAX_STARTUP_RETRIES) { - scheduleItem.retry_cnt = command(scheduleItem.name.c_str(), scheduleItem.cmd, scheduleItem.value) ? 0xFF : scheduleItem.retry_cnt + 1; + scheduleItem.retry_cnt = command(scheduleItem.name, scheduleItem.cmd.c_str(), scheduleItem.value.c_str()) ? 0xFF : scheduleItem.retry_cnt + 1; } // scheduled timer commands if (scheduleItem.active && scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_TIMER && scheduleItem.elapsed_min > 0 && (uptime_min % scheduleItem.elapsed_min == 0)) { - command(scheduleItem.name.c_str(), scheduleItem.cmd, compute(scheduleItem.value)); + command(scheduleItem.name, scheduleItem.cmd.c_str(), compute(scheduleItem.value.c_str())); } } last_uptime_min = uptime_min; @@ -532,7 +508,7 @@ void WebSchedulerService::loop() { for (const ScheduleItem & scheduleItem : *scheduleItems_) { uint8_t dow = scheduleItem.flags & SCHEDULEFLAG_SCHEDULE_TIMER ? 0 : scheduleItem.flags; if (scheduleItem.active && (real_dow & dow) && real_min == scheduleItem.elapsed_min) { - command(scheduleItem.name.c_str(), scheduleItem.cmd, compute(scheduleItem.value)); + command(scheduleItem.name, scheduleItem.cmd.c_str(), compute(scheduleItem.value.c_str())); } } last_tm_min = tm->tm_min; @@ -559,26 +535,26 @@ void WebSchedulerService::load_test_data() { webScheduler.scheduleItems.clear(); // delete all existing schedules // test 1 - auto si = ScheduleItem(); - si.active = true; - si.flags = 1; - si.time = "12:00"; - si.cmd = "system/fetch"; - si.value = "10"; - si.name = "test_scheduler"; + auto si = ScheduleItem(); + si.active = true; + si.flags = 1; + si.time = "12:00"; + si.cmd = "system/fetch"; + si.value = "10"; + strcpy(si.name, "test_scheduler"); si.elapsed_min = 0; si.retry_cnt = 0xFF; // no startup retries webScheduler.scheduleItems.push_back(si); // test 2 - si = ScheduleItem(); - si.active = false; - si.flags = 1; - si.time = "13:00"; - si.cmd = "system/message"; - si.value = "20"; - si.name = ""; // to make sure its excluded from Dashboard + si = ScheduleItem(); + si.active = false; + si.flags = 1; + si.time = "13:00"; + si.cmd = "system/message"; + si.value = "20"; + strcpy(si.name, ""); // to make sure its excluded from Dashboard si.elapsed_min = 0; si.retry_cnt = 0xFF; // no startup retries diff --git a/src/web/WebSchedulerService.h b/src/web/WebSchedulerService.h index 2b2f20522..6978a64b0 100644 --- a/src/web/WebSchedulerService.h +++ b/src/web/WebSchedulerService.h @@ -16,6 +16,8 @@ * along with this program. If not, see . */ +#include + #ifndef WebSchedulerService_h #define WebSchedulerService_h @@ -54,16 +56,16 @@ class ScheduleItem { boolean active; uint8_t flags; uint16_t elapsed_min; // total mins from 00:00 - std::string time; // HH:MM - std::string cmd; - std::string value; - std::string name; + stringPSRAM time; // HH:MM + stringPSRAM cmd; + stringPSRAM value; + char name[20]; uint8_t retry_cnt; }; class WebScheduler { public: - std::list scheduleItems; + std::list> scheduleItems; static void read(WebScheduler & webScheduler, JsonObject root); static StateUpdateResult update(JsonObject root, WebScheduler & webScheduler); @@ -104,10 +106,10 @@ class WebSchedulerService : public StatefulService { HttpEndpoint _httpEndpoint; FSPersistence _fsPersistence; + bool ha_registered_ = false; - std::list * scheduleItems_; // pointer to the list of schedule events - bool ha_registered_ = false; - std::deque cmd_changed_; + std::list> * scheduleItems_; // pointer to the list of schedule events + std::list> cmd_changed_; // pointer to commands in list that are triggert by change }; } // namespace emsesp diff --git a/src/web/WebSettingsService.cpp b/src/web/WebSettingsService.cpp index ea35e128d..934b62b3c 100644 --- a/src/web/WebSettingsService.cpp +++ b/src/web/WebSettingsService.cpp @@ -123,50 +123,61 @@ StateUpdateResult WebSettings::update(JsonObject root, WebSettings & settings) { add_flags(ChangeFlags::RESTART); } + check_flag(original_settings.phy_type, settings.phy_type, ChangeFlags::RESTART); + // ETH has changed, so we need to check the ethernet pins. Only if ETH is being used. + if (settings.phy_type != PHY_type::PHY_TYPE_NONE) { + check_flag(original_settings.eth_power, settings.eth_power, ChangeFlags::RESTART); + check_flag(original_settings.eth_clock_mode, settings.eth_clock_mode, ChangeFlags::RESTART); + if (settings.eth_power != -1) { // Ethernet Power -1 means disabled + EMSESP::system_.remove_gpio(settings.eth_power, true); + } + // remove the ethernet pins from valid list, regardless of whether the GPIOs are valid or not + EMSESP::system_.remove_gpio(23, true); // MDC + EMSESP::system_.remove_gpio(18, true); // MDIO + if (settings.eth_clock_mode < 2) { + EMSESP::system_.remove_gpio(0, true); // ETH.clock input + } else if (settings.eth_clock_mode == 2) { + EMSESP::system_.remove_gpio(16, true); // ETH.clock output + } else if (settings.eth_clock_mode == 3) { + EMSESP::system_.remove_gpio(17, true); // ETH.clock output + } + } +#if CONFIG_IDF_TARGET_ESP32 + // Uart0 pins not allowed for all other gpio + EMSESP::system_.remove_gpio(1, true); + EMSESP::system_.remove_gpio(3, true); +#endif // if any of the GPIOs have changed and re-validate them bool have_valid_gpios = true; - if (check_flag(original_settings.led_gpio, settings.led_gpio, ChangeFlags::LED)) { - if (settings.led_gpio != 0) { // 0 means disabled - have_valid_gpios = have_valid_gpios && EMSESP::system_.add_gpio(settings.led_gpio, "LED"); - } + // free old gpios from used list to allow remapping + EMSESP::system_.remove_gpio(original_settings.led_gpio); + EMSESP::system_.remove_gpio(original_settings.dallas_gpio); + EMSESP::system_.remove_gpio(original_settings.pbutton_gpio); + EMSESP::system_.remove_gpio(original_settings.rx_gpio); + EMSESP::system_.remove_gpio(original_settings.tx_gpio); + + // now add new gpio assignment, start with rx/tx + check_flag(original_settings.rx_gpio, settings.rx_gpio, ChangeFlags::UART); + have_valid_gpios = have_valid_gpios && EMSESP::system_.add_gpio(settings.rx_gpio, "UART Rx"); + + check_flag(original_settings.tx_gpio, settings.tx_gpio, ChangeFlags::UART); + have_valid_gpios = have_valid_gpios && EMSESP::system_.add_gpio(settings.tx_gpio, "UART Tx"); + + check_flag(original_settings.led_gpio, settings.led_gpio, ChangeFlags::LED); + if (settings.led_gpio != 0 && !EMSESP::system_.add_gpio(settings.led_gpio, "LED")) { + settings.led_gpio = 0; // 0 means disabled + have_valid_gpios = false; } - if (check_flag(original_settings.dallas_gpio, settings.dallas_gpio, ChangeFlags::TEMPERATURE_SENSOR)) { - if (settings.dallas_gpio != 0) { // 0 means disabled - have_valid_gpios = have_valid_gpios && EMSESP::system_.add_gpio(settings.dallas_gpio, "Dallas"); - } + check_flag(original_settings.dallas_gpio, settings.dallas_gpio, ChangeFlags::TEMPERATURE_SENSOR); + if (settings.dallas_gpio != 0 && !EMSESP::system_.add_gpio(settings.dallas_gpio, "Dallas")) { + settings.dallas_gpio = 0; // 0 means disabled + have_valid_gpios = false; } - if (check_flag(original_settings.rx_gpio, settings.rx_gpio, ChangeFlags::RESTART)) { - have_valid_gpios = have_valid_gpios && EMSESP::system_.add_gpio(settings.rx_gpio, "UART Rx"); - } - - if (check_flag(original_settings.tx_gpio, settings.tx_gpio, ChangeFlags::RESTART)) { - have_valid_gpios = have_valid_gpios && EMSESP::system_.add_gpio(settings.tx_gpio, "UART Tx"); - } - - if (check_flag(original_settings.pbutton_gpio, settings.pbutton_gpio, ChangeFlags::BUTTON)) { - have_valid_gpios = have_valid_gpios && EMSESP::system_.add_gpio(settings.pbutton_gpio, "Button"); - } - - if (check_flag(original_settings.phy_type, settings.phy_type, ChangeFlags::RESTART)) { - // ETH has changed, so we need to check the ethernet pins. Only if ETH is being used. - if (settings.phy_type != PHY_type::PHY_TYPE_NONE) { - if (settings.eth_power != -1) { - // Always remove Ethernet Power gpio unless disabled (-1) - EMSESP::system_.remove_gpio(settings.eth_power, true); - } - // all valid so far, now remove the ethernet pins from valid list, regardless of whether the GPIOs are valid or not - EMSESP::system_.remove_gpio(23, true); // MDC - EMSESP::system_.remove_gpio(18, true); // MDIO - EMSESP::system_.remove_gpio(0, true); // ETH.clock input - EMSESP::system_.remove_gpio(16, true); // ETH.clock output - EMSESP::system_.remove_gpio(17, true); // ETH.clock output - EMSESP::system_.remove_gpio(21, true); // I2C SDA - EMSESP::system_.remove_gpio(22, true); // I2C SCL - } - } + check_flag(original_settings.pbutton_gpio, settings.pbutton_gpio, ChangeFlags::BUTTON); + have_valid_gpios = have_valid_gpios && EMSESP::system_.add_gpio(settings.pbutton_gpio, "Button"); // check if the LED type, eth_phy_addr or eth_clock_mode have changed check_flag(original_settings.led_type, settings.led_type, ChangeFlags::LED); diff --git a/test/test_api/test_api.h b/test/test_api/test_api.h index b22125633..1e2974d63 100644 --- a/test/test_api/test_api.h +++ b/test/test_api/test_api.h @@ -164,7 +164,8 @@ void test_21() { auto expected_response = "[{\"system\":{\"version\":\"dev\",\"uptime\":\"000+00:00:00.000\",\"uptimeSec\":0,\"resetReason\":\"Unknown / " "Unknown\"},\"network\":{\"network\":\"WiFi\",\"hostname\":\"ems-esp\",\"RSSI\":-23,\"TxPowerSetting\":0,\"staticIP\":false,\"lowBandwidth\":false," - "\"disableSleep\":true,\"enableMDNS\":true,\"enableCORS\":false},\"ntp\":{\"enabled\":true,\"server\":\"pool.ntp.org\",\"tzLabel\":\"Europe/" + "\"disableSleep\":true,\"enableMDNS\":true,\"enableCORS\":false},\"ntp\":{\"NTPstatus\":\"disconnected\",\"enabled\":true,\"server\":\"pool.ntp.org\"," + "\"tzLabel\":\"Europe/" "London\",\"NTPStatus\":\"disconnected\"},\"ap\":{\"provisionMode\":\"always\",\"ssid\":\"ems-esp\"},\"mqtt\":{\"MQTTStatus\":\"disconnected\"," "\"MQTTPublishes\":0,\"MQTTQueued\":0,\"MQTTPublishFails\":0,\"MQTTReconnects\":0,\"enabled\":true,\"clientID\":\"ems-esp\",\"keepAlive\":60," "\"cleanSession\":false,\"entityFormat\":1,\"base\":\"ems-esp\",\"discoveryPrefix\":\"homeassistant\",\"discoveryType\":0,\"nestedFormat\":1," @@ -192,7 +193,8 @@ void test_22() { auto expected_response = "[{\"system\":{\"version\":\"dev\",\"uptime\":\"000+00:00:00.000\",\"uptimeSec\":0,\"resetReason\":\"Unknown / " "Unknown\"},\"network\":{\"network\":\"WiFi\",\"hostname\":\"ems-esp\",\"RSSI\":-23,\"TxPowerSetting\":0,\"staticIP\":false,\"lowBandwidth\":false," - "\"disableSleep\":true,\"enableMDNS\":true,\"enableCORS\":false},\"ntp\":{\"enabled\":true,\"server\":\"pool.ntp.org\",\"tzLabel\":\"Europe/" + "\"disableSleep\":true,\"enableMDNS\":true,\"enableCORS\":false},\"ntp\":{\"NTPstatus\":\"disconnected\",\"enabled\":true,\"server\":\"pool.ntp.org\"," + "\"tzLabel\":\"Europe/" "London\",\"NTPStatus\":\"disconnected\"},\"ap\":{\"provisionMode\":\"always\",\"ssid\":\"ems-esp\"},\"mqtt\":{\"MQTTStatus\":\"disconnected\"," "\"MQTTPublishes\":0,\"MQTTQueued\":0,\"MQTTPublishFails\":0,\"MQTTReconnects\":0,\"enabled\":true,\"clientID\":\"ems-esp\",\"keepAlive\":60," "\"cleanSession\":false,\"entityFormat\":1,\"base\":\"ems-esp\",\"discoveryPrefix\":\"homeassistant\",\"discoveryType\":0,\"nestedFormat\":1,"