Feature: Added ability to change the brightness of the LEDs

Based on the idea of @moritzlerch with several modifications like pwmTable and structure
This commit is contained in:
Thomas Basler 2023-12-07 12:46:38 +01:00
parent 3b6e9343d4
commit 9ae791edd4
11 changed files with 171 additions and 30 deletions

View File

@ -1,6 +1,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#pragma once #pragma once
#include "PinMapping.h"
#include <cstdint> #include <cstdint>
#define CONFIG_FILENAME "/config.json" #define CONFIG_FILENAME "/config.json"
@ -144,6 +145,10 @@ struct CONFIG_T {
uint8_t Language; uint8_t Language;
} Display; } Display;
struct {
uint8_t Brightness;
} Led_Single[PINMAPPING_LED_COUNT];
INVERTER_CONFIG_T Inverter[INV_MAX_COUNT]; INVERTER_CONFIG_T Inverter[INV_MAX_COUNT];
char Dev_PinMapping[DEV_MAX_MAPPING_NAME_STRLEN + 1]; char Dev_PinMapping[DEV_MAX_MAPPING_NAME_STRLEN + 1];
}; };

View File

@ -19,6 +19,8 @@ private:
void setLoop(); void setLoop();
void outputLoop(); void outputLoop();
void setLed(uint8_t ledNo, bool ledState);
Task _setTask; Task _setTask;
Task _outputTask; Task _outputTask;
@ -28,8 +30,9 @@ private:
Blink, Blink,
}; };
LedState_t _ledState[PINMAPPING_LED_COUNT]; LedState_t _ledMode[PINMAPPING_LED_COUNT];
LedState_t _allState; LedState_t _allMode;
bool _ledStateCurrent[PINMAPPING_LED_COUNT];
TimeoutHelper _blinkTimeout; TimeoutHelper _blinkTimeout;
}; };

View File

@ -100,3 +100,5 @@
#define DISPLAY_LANGUAGE 0U #define DISPLAY_LANGUAGE 0U
#define REACHABLE_THRESHOLD 2U #define REACHABLE_THRESHOLD 2U
#define LED_BRIGHTNESS 100U

View File

@ -104,6 +104,12 @@ bool ConfigurationClass::write()
display["contrast"] = config.Display.Contrast; display["contrast"] = config.Display.Contrast;
display["language"] = config.Display.Language; display["language"] = config.Display.Language;
JsonArray leds = device.createNestedArray("led");
for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) {
JsonObject led = leds.createNestedObject();
led["brightness"] = config.Led_Single[i].Brightness;
}
JsonArray inverters = doc.createNestedArray("inverters"); JsonArray inverters = doc.createNestedArray("inverters");
for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { for (uint8_t i = 0; i < INV_MAX_COUNT; i++) {
JsonObject inv = inverters.createNestedObject(); JsonObject inv = inverters.createNestedObject();
@ -259,6 +265,12 @@ bool ConfigurationClass::read()
config.Display.Contrast = display["contrast"] | DISPLAY_CONTRAST; config.Display.Contrast = display["contrast"] | DISPLAY_CONTRAST;
config.Display.Language = display["language"] | DISPLAY_LANGUAGE; config.Display.Language = display["language"] | DISPLAY_LANGUAGE;
JsonArray leds = device["led"];
for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) {
JsonObject led = leds[i].as<JsonObject>();
config.Led_Single[i].Brightness = led["brightness"] | LED_BRIGHTNESS;
}
JsonArray inverters = doc["inverters"]; JsonArray inverters = doc["inverters"];
for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { for (uint8_t i = 0; i < INV_MAX_COUNT; i++) {
JsonObject inv = inverters[i].as<JsonObject>(); JsonObject inv = inverters[i].as<JsonObject>();

View File

@ -12,6 +12,31 @@
LedSingleClass LedSingle; LedSingleClass LedSingle;
/*
The table is calculated using the following formula
(See https://www.mikrocontroller.net/articles/LED-Fading)
a = Step count: 101 --> 0 - 100
b = PWM resolution: 256: 0 - 255
y = Calculated value of index x:
y = 0 if x = 0
y = pow(2, log2(b-1) * (x+1) / a) if x > 0
*/
const uint8_t pwmTable[] = {
0,
1, 1, 1, 1, 1, 1, 2, 2, 2, 2,
2, 2, 2, 2, 2, 3, 3, 3, 3, 3,
3, 4, 4, 4, 4, 4, 5, 5, 5, 5,
6, 6, 6, 7, 7, 8, 8, 8, 9, 9,
10, 11, 11, 12, 12, 13, 14, 15, 16, 16,
17, 18, 19, 20, 22, 23, 24, 25, 27, 28,
30, 32, 33, 35, 37, 39, 42, 44, 47, 49,
52, 55, 58, 61, 65, 68, 72, 76, 81, 85,
90, 95, 100, 106, 112, 118, 125, 132, 139, 147,
156, 164, 174, 183, 194, 205, 216, 228, 241, 255
};
#define LED_OFF 0
LedSingleClass::LedSingleClass() LedSingleClass::LedSingleClass()
{ {
} }
@ -28,11 +53,11 @@ void LedSingleClass::init(Scheduler* scheduler)
if (pin.led[i] >= 0) { if (pin.led[i] >= 0) {
pinMode(pin.led[i], OUTPUT); pinMode(pin.led[i], OUTPUT);
digitalWrite(pin.led[i], LOW); setLed(i, false);
ledActive = true; ledActive = true;
} }
_ledState[i] = LedState_t::Off; _ledMode[i] = LedState_t::Off;
} }
if (ledActive) { if (ledActive) {
@ -51,58 +76,52 @@ void LedSingleClass::init(Scheduler* scheduler)
void LedSingleClass::setLoop() void LedSingleClass::setLoop()
{ {
if (_allState == LedState_t::On) { if (_allMode == LedState_t::On) {
const CONFIG_T& config = Configuration.get(); const CONFIG_T& config = Configuration.get();
// Update network status // Update network status
_ledState[0] = LedState_t::Off; _ledMode[0] = LedState_t::Off;
if (NetworkSettings.isConnected()) { if (NetworkSettings.isConnected()) {
_ledState[0] = LedState_t::Blink; _ledMode[0] = LedState_t::Blink;
} }
struct tm timeinfo; struct tm timeinfo;
if (getLocalTime(&timeinfo, 5) && (!config.Mqtt.Enabled || (config.Mqtt.Enabled && MqttSettings.getConnected()))) { if (getLocalTime(&timeinfo, 5) && (!config.Mqtt.Enabled || (config.Mqtt.Enabled && MqttSettings.getConnected()))) {
_ledState[0] = LedState_t::On; _ledMode[0] = LedState_t::On;
} }
// Update inverter status // Update inverter status
_ledState[1] = LedState_t::Off; _ledMode[1] = LedState_t::Off;
if (Hoymiles.getNumInverters() && Datastore.getIsAtLeastOnePollEnabled()) { if (Hoymiles.getNumInverters() && Datastore.getIsAtLeastOnePollEnabled()) {
// set LED status // set LED status
if (Datastore.getIsAllEnabledReachable() && Datastore.getIsAllEnabledProducing()) { if (Datastore.getIsAllEnabledReachable() && Datastore.getIsAllEnabledProducing()) {
_ledState[1] = LedState_t::On; _ledMode[1] = LedState_t::On;
} }
if (Datastore.getIsAllEnabledReachable() && !Datastore.getIsAllEnabledProducing()) { if (Datastore.getIsAllEnabledReachable() && !Datastore.getIsAllEnabledProducing()) {
_ledState[1] = LedState_t::Blink; _ledMode[1] = LedState_t::Blink;
} }
} }
} else if (_allState == LedState_t::Off) { } else if (_allMode == LedState_t::Off) {
_ledState[0] = LedState_t::Off; _ledMode[0] = LedState_t::Off;
_ledState[1] = LedState_t::Off; _ledMode[1] = LedState_t::Off;
} }
} }
void LedSingleClass::outputLoop() void LedSingleClass::outputLoop()
{ {
auto& pin = PinMapping.get();
for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) { for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) {
switch (_ledMode[i]) {
if (pin.led[i] < 0) {
continue;
}
switch (_ledState[i]) {
case LedState_t::Off: case LedState_t::Off:
digitalWrite(pin.led[i], LOW); setLed(i, false);
break; break;
case LedState_t::On: case LedState_t::On:
digitalWrite(pin.led[i], HIGH); setLed(i, true);
break; break;
case LedState_t::Blink: case LedState_t::Blink:
if (_blinkTimeout.occured()) { if (_blinkTimeout.occured()) {
digitalWrite(pin.led[i], !digitalRead(pin.led[i])); setLed(i, !_ledStateCurrent[i]);
_blinkTimeout.reset(); _blinkTimeout.reset();
} }
break; break;
@ -110,12 +129,32 @@ void LedSingleClass::outputLoop()
} }
} }
void LedSingleClass::setLed(uint8_t ledNo, bool ledState)
{
const auto& pin = PinMapping.get();
const auto& config = Configuration.get();
if (pin.led[ledNo] < 0) {
return;
}
const uint32_t currentPWM = ledcRead(analogGetChannel(pin.led[ledNo]));
const uint32_t targetPWM = ledState ? pwmTable[config.Led_Single[ledNo].Brightness] : LED_OFF;
if (currentPWM == targetPWM) {
return;
}
analogWrite(pin.led[ledNo], targetPWM);
_ledStateCurrent[ledNo] = ledState;
}
void LedSingleClass::turnAllOff() void LedSingleClass::turnAllOff()
{ {
_allState = LedState_t::Off; _allMode = LedState_t::Off;
} }
void LedSingleClass::turnAllOn() void LedSingleClass::turnAllOn()
{ {
_allState = LedState_t::On; _allMode = LedState_t::On;
} }

View File

@ -84,6 +84,12 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request)
display["contrast"] = config.Display.Contrast; display["contrast"] = config.Display.Contrast;
display["language"] = config.Display.Language; display["language"] = config.Display.Language;
JsonArray leds = root.createNestedArray("led");
for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) {
JsonObject led = leds.createNestedObject();
led["brightness"] = config.Led_Single[i].Brightness;
}
response->setLength(); response->setLength();
request->send(response); request->send(response);
} }
@ -155,6 +161,12 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request)
config.Display.Contrast = root["display"]["contrast"].as<uint8_t>(); config.Display.Contrast = root["display"]["contrast"].as<uint8_t>();
config.Display.Language = root["display"]["language"].as<uint8_t>(); config.Display.Language = root["display"]["language"].as<uint8_t>();
for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) {
Serial.println(root["led"][i]["brightness"].as<uint8_t>());
config.Led_Single[i].Brightness = root["led"][i]["brightness"].as<uint8_t>();
config.Led_Single[i].Brightness = min<uint8_t>(100, config.Led_Single[i].Brightness);
}
Display.setOrientation(config.Display.Rotation); Display.setOrientation(config.Display.Rotation);
Display.enablePowerSafe = config.Display.PowerSafe; Display.enablePowerSafe = config.Display.PowerSafe;
Display.enableScreensaver = config.Display.ScreenSaver; Display.enableScreensaver = config.Display.ScreenSaver;

View File

@ -574,12 +574,15 @@
"Rotation": "Rotation:", "Rotation": "Rotation:",
"rot0": "Keine Rotation", "rot0": "Keine Rotation",
"rot90": "90 Grad Drehung", "rot90": "90 Grad Drehung",
"rot180": "180 Grad Drehung",
"rot270": "270 Grad Drehung",
"DisplayLanguage": "Displaysprache:", "DisplayLanguage": "Displaysprache:",
"en": "Englisch", "en": "Englisch",
"de": "Deutsch", "de": "Deutsch",
"fr": "Französisch", "fr": "Französisch",
"rot180": "180 Grad Drehung", "Leds": "LEDs",
"rot270": "270 Grad Drehung", "EqualBrightness": "Gleiche Helligkeit:",
"LedBrightness": "LED {led} Helligkeit ({brightness}):",
"Save": "@:dtuadmin.Save" "Save": "@:dtuadmin.Save"
}, },
"pininfo": { "pininfo": {

View File

@ -580,6 +580,9 @@
"en": "English", "en": "English",
"de": "German", "de": "German",
"fr": "French", "fr": "French",
"Leds": "LEDs",
"EqualBrightness": "Equal brightness:",
"LedBrightness": "LED {led} brightness ({brightness}):",
"Save": "@:dtuadmin.Save" "Save": "@:dtuadmin.Save"
}, },
"pininfo": { "pininfo": {

View File

@ -580,6 +580,9 @@
"en": "Anglais", "en": "Anglais",
"de": "Allemand", "de": "Allemand",
"fr": "Français", "fr": "Français",
"Leds": "LEDs",
"EqualBrightness": "Même luminosité:",
"LedBrightness": "LED {led} luminosité ({brightness}):",
"Save": "@:dtuadmin.Save" "Save": "@:dtuadmin.Save"
}, },
"pininfo": { "pininfo": {

View File

@ -8,7 +8,12 @@ export interface Display {
language: number; language: number;
} }
export interface Led {
brightness: number;
}
export interface DeviceConfig { export interface DeviceConfig {
curPin: Device; curPin: Device;
display: Display; display: Display;
led: Array<Led>;
} }

View File

@ -13,6 +13,8 @@
}}</button> }}</button>
<button class="nav-link" id="nav-display-tab" data-bs-toggle="tab" data-bs-target="#nav-display" <button class="nav-link" id="nav-display-tab" data-bs-toggle="tab" data-bs-target="#nav-display"
type="button" role="tab" aria-controls="nav-display">{{ $t('deviceadmin.Display') }}</button> type="button" role="tab" aria-controls="nav-display">{{ $t('deviceadmin.Display') }}</button>
<button class="nav-link" id="nav-leds-tab" data-bs-toggle="tab" data-bs-target="#nav-leds"
type="button" role="tab" aria-controls="nav-leds">{{ $t('deviceadmin.Leds') }}</button>
</div> </div>
</nav> </nav>
<div class="tab-content" id="nav-tabContent"> <div class="tab-content" id="nav-tabContent">
@ -96,6 +98,29 @@
</div> </div>
</div> </div>
</div> </div>
<div class="tab-pane fade show" id="nav-leds" role="tabpanel" aria-labelledby="nav-leds-tab" tabindex="0">
<div class="card">
<div class="card-body">
<InputElement :label="$t('deviceadmin.EqualBrightness')"
v-model="equalBrightnessCheckVal" type="checkbox" />
<div class="row mb-3" v-for="(ledSetting, index) in deviceConfigList.led">
<label :for="getLedIdFromNumber(index)" class="col-sm-2 col-form-label">{{
$t('deviceadmin.LedBrightness', {
led: index,
brightness: $n(ledSetting.brightness / 100,
'percent')
})
}}</label>
<div class="col-sm-10">
<input type="range" class="form-range" min="0" max="100" :id="getLedIdFromNumber(index)"
v-model="ledSetting.brightness" @change="syncSliders" />
</div>
</div>
</div>
</div>
</div>
</div> </div>
<button type="submit" class="btn btn-primary mb-3">{{ $t('deviceadmin.Save') }}</button> <button type="submit" class="btn btn-primary mb-3">{{ $t('deviceadmin.Save') }}</button>
@ -109,7 +134,7 @@ import BasePage from '@/components/BasePage.vue';
import BootstrapAlert from "@/components/BootstrapAlert.vue"; import BootstrapAlert from "@/components/BootstrapAlert.vue";
import InputElement from '@/components/InputElement.vue'; import InputElement from '@/components/InputElement.vue';
import PinInfo from '@/components/PinInfo.vue'; import PinInfo from '@/components/PinInfo.vue';
import type { DeviceConfig } from "@/types/DeviceConfig"; import type { DeviceConfig, Led } from "@/types/DeviceConfig";
import type { PinMapping, Device } from "@/types/PinMapping"; import type { PinMapping, Device } from "@/types/PinMapping";
import { authHeader, handleResponse } from '@/utils/authentication'; import { authHeader, handleResponse } from '@/utils/authentication';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
@ -130,6 +155,7 @@ export default defineComponent({
alertMessage: "", alertMessage: "",
alertType: "info", alertType: "info",
showAlert: false, showAlert: false,
equalBrightnessCheckVal: false,
displayRotationList: [ displayRotationList: [
{ key: 0, value: 'rot0' }, { key: 0, value: 'rot0' },
{ key: 1, value: 'rot90' }, { key: 1, value: 'rot90' },
@ -147,6 +173,14 @@ export default defineComponent({
this.getDeviceConfig(); this.getDeviceConfig();
this.getPinMappingList(); this.getPinMappingList();
}, },
watch: {
equalBrightnessCheckVal: function(val) {
if (!val) {
return;
}
this.deviceConfigList.led.every(v => v.brightness = this.deviceConfigList.led[0].brightness);
}
},
methods: { methods: {
getPinMappingList() { getPinMappingList() {
this.pinMappingLoading = true; this.pinMappingLoading = true;
@ -180,7 +214,10 @@ export default defineComponent({
this.deviceConfigList = data; this.deviceConfigList = data;
this.dataLoading = false; this.dataLoading = false;
} }
); )
.then(() => {
this.equalBrightnessCheckVal = this.isEqualBrightness();
});
}, },
savePinConfig(e: Event) { savePinConfig(e: Event) {
e.preventDefault(); e.preventDefault();
@ -202,6 +239,23 @@ export default defineComponent({
} }
); );
}, },
getLedIdFromNumber(ledNo: number) : string {
return 'inputLED' + ledNo + 'Brightness';
},
getNumberFromLedId(id: string): number {
return parseInt(id.replace("inputLED", "").replace("Brightness", ""));
},
isEqualBrightness(): boolean {
const allEqual = (arr : Led[]) => arr.every(v => v.brightness === arr[0].brightness);
return allEqual(this.deviceConfigList.led);
},
syncSliders(event: Event) {
if (!this.equalBrightnessCheckVal) {
return;
}
const srcId = this.getNumberFromLedId((event.target as Element).id);
this.deviceConfigList.led.every(v => v.brightness = this.deviceConfigList.led[srcId].brightness);
}
}, },
}); });
</script> </script>