Skip to content

Commit

Permalink
Fixed incorrect over-memory indication in LED settings on ESP32
Browse files Browse the repository at this point in the history
  • Loading branch information
Aircoookie committed Mar 6, 2021
1 parent 05521bf commit 71edc3a
Show file tree
Hide file tree
Showing 8 changed files with 26 additions and 19 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

### Development versions after 0.11.1 release

#### Build 2103050

- Fixed incorrect over-memory indication in LED settings on ESP32

#### Build 2103041

- Added destructor for BusPwm (fixes #1789)
Expand Down
1 change: 0 additions & 1 deletion wled00/bus_manager.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ class Bus {
virtual void cleanup() {};

virtual ~Bus() { //throw the bus under the bus
//Serial.println("Destructor!");
}

virtual uint8_t getPins(uint8_t* pinArray) { return 0; }
Expand Down
24 changes: 14 additions & 10 deletions wled00/data/settings_leds.htm
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<meta name="viewport" content="width=500">
<title>LED Settings</title>
<script>
var d=document,laprev=55,maxST=1,bmax=5000,bquot=0; //maximum bytes for LED allocation: 5kB for 8266, 32kB for 32
var d=document,laprev=55,maxB=1,maxM=5000,bquot=0; //maximum bytes for LED allocation: 5kB for 8266, 32kB for 32
function H()
{
window.open("https://github.com/Aircoookie/WLED/wiki/Settings#led-settings");
Expand All @@ -14,6 +14,9 @@
{
window.open("/settings","_self");
}
function bLimits(b,m) {
maxB = b; maxM = m;
}
function trySubmit() {
var LCs = d.getElementsByTagName("input");
for (i=0; i<LCs.length; i++) {
Expand All @@ -32,10 +35,10 @@
}
}
}
if (bquot > 100) {var msg = "Too many LEDs for me to handle!"; if (bmax < 10000) msg += " Consider using an ESP32."; alert(msg); return;}
if (bquot > 100) {var msg = "Too many LEDs for me to handle!"; if (maxM < 10000) msg += " Consider using an ESP32."; alert(msg); return;}
if (d.Sf.reportValidity()) d.Sf.submit();
}
function S(){GetV();setABL(); if (maxST>4) bmax=64000; d.getElementById('m1').innerHTML = bmax;}
function S(){GetV();setABL();}
function enABL()
{
var en = d.getElementById('able').checked;
Expand Down Expand Up @@ -63,16 +66,17 @@
case 255: d.Sf.LAsel.value = 255; break;
default: d.getElementById('LAdis').style.display = 'inline';
}
d.getElementById('m1').innerHTML = maxM;
UI();
}
//returns mem usage
function getMem(type, len, p0) {
//len = parseInt(len);
if (type < 32) {
if (bmax < 10000 && p0 ==3) { //8266 DMA uses 5x the mem
if (maxM < 10000 && p0 ==3) { //8266 DMA uses 5x the mem
if (type > 29) return len*20; //RGBW
return len*15;
} else if (bmax > 10000) //ESP32 RMT uses double buffer?
} else if (maxM >= 10000) //ESP32 RMT uses double buffer?
{
if (type > 29) return len*8; //RGBW
return len*6;
Expand Down Expand Up @@ -117,7 +121,7 @@
LK.value="";
}
}
if (type == 30 || type == 31 || type == 44 || type == 45) isRGBW = true;
if (type == 30 || type == 31 || (type > 40 && type < 46 && type != 43)) isRGBW = true;
d.getElementById("dig"+n).style.display = (type > 31 && type < 48) ? "none":"inline";
d.getElementById("psd"+n).innerHTML = (type > 31 && type < 48) ? "Index:":"Start:";
}
Expand All @@ -143,7 +147,7 @@
}

d.getElementById('m0').innerHTML = memu;
bquot = memu / bmax * 100;
bquot = memu / maxM * 100;
d.getElementById('dbar').style.background = `linear-gradient(90deg, ${bquot > 60 ? bquot > 90 ? "red":"orange":"#ccc"} 0 ${bquot}%%, #444 ${bquot}%% 100%%)`;
d.getElementById('ledwarning').style.display = (maxLC > 800 || bquot > 80) ? 'inline':'none';
//TODO add warning "Recommended pins on ESP8266 are 1 and 2 (3 only with low LED count)"
Expand Down Expand Up @@ -176,12 +180,12 @@
}
function addLEDs(n)
{
if (n>1) {maxST=n; d.getElementById("+").style.display="inline"; return;}
if (n>1) {maxB=n; d.getElementById("+").style.display="inline"; return;}

var o = d.getElementsByClassName("iST");
var i = o.length;

if ((n==1 && i>=maxST) || (n==-1 && i==0)) return;
if ((n==1 && i>=maxB) || (n==-1 && i==0)) return;

var f = d.getElementById("mLC");
if (n==1) {
Expand Down Expand Up @@ -229,7 +233,7 @@
o[--i].remove();--i;
}

d.getElementById("+").style.display = (i<maxST-1) ? "inline":"none";
d.getElementById("+").style.display = (i<maxB-1) ? "inline":"none";
d.getElementById("-").style.display = (i>0) ? "inline":"none";

UI();
Expand Down
2 changes: 1 addition & 1 deletion wled00/html_settings.h
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ Do not enable if WiFi is working correctly, increases power consumption.</i><div
// Autogenerated from wled00/data/settings_leds.htm, do not edit!!
const char PAGE_settings_leds[] PROGMEM = R"=====(<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta
name="viewport" content="width=500"><title>LED Settings</title><script>
var d=document,laprev=55,maxST=1,bmax=5e3,bquot=0;function H(){window.open("https://github.com/Aircoookie/WLED/wiki/Settings#led-settings")}function B(){window.open("/settings","_self")}function trySubmit(){var e=d.getElementsByTagName("input");for(i=0;i<e.length;i++){var n=e[i].name.substring(0,2);if(("L0"==n||"L1"==n||"RL"==n||"BT"==n||"IR"==n||"AX"==n)&&""!=e[i].value&&"-1"!=e[i].value){if(e[i].value>5&&e[i].value<12)return alert("Sorry, pins 6-11 can not be used."),void e[i].focus();if(d.um_p&&d.um_p.some(n=>n==parseInt(e[i].value,10)))return alert("Usermod pin conflict!"),void e[i].focus();for(j=i+1;j<e.length;j++){var t=e[j].name.substring(0,2);if(("L0"==t||"L1"==t||"RL"==t||"BT"==t||"IR"==t||"AX"==t)&&""!=e[j].value&&e[i].value==e[j].value)return alert("Pin conflict!"),void e[i].focus()}}}if(bquot>100){var a="Too many LEDs for me to handle!";return bmax<1e4&&(a+=" Consider using an ESP32."),void alert(a)}d.Sf.reportValidity()&&d.Sf.submit()}function S(){GetV(),setABL(),maxST>4&&(bmax=64e3),d.getElementById("m1").innerHTML=bmax}function enABL(){var e=d.getElementById("able").checked;d.Sf.LA.value=e?laprev:0,d.getElementById("abl").style.display=e?"inline":"none",d.getElementById("psu2").style.display=e?"inline":"none",d.Sf.LA.value>0&&setABL()}function enLA(){var e=d.Sf.LAsel.value;d.Sf.LA.value=e,d.getElementById("LAdis").style.display=50==e?"inline":"none",UI()}function setABL(){switch(d.getElementById("able").checked=!0,d.Sf.LAsel.value=50,parseInt(d.Sf.LA.value)){case 0:d.getElementById("able").checked=!1,enABL();break;case 30:d.Sf.LAsel.value=30;break;case 35:d.Sf.LAsel.value=35;break;case 55:d.Sf.LAsel.value=55;break;case 255:d.Sf.LAsel.value=255;break;default:d.getElementById("LAdis").style.display="inline"}UI()}function getMem(e,n,t){return e<32?bmax<1e4&&3==t?e>29?20*n:15*n:bmax>1e4?e>29?8*n:6*n:e>29?4*n:3*n:e>31&&e<48?5:44==e||45==e?4*n:3*n}function UI(){var e=!1,t=0;d.getElementById("ampwarning").style.display=d.Sf.MA.value>7200?"inline":"none",255==d.Sf.LA.value?laprev=12:d.Sf.LA.value>0&&(laprev=d.Sf.LA.value);var i=d.getElementsByTagName("select");for(u=0;u<i.length;u++)if("LT"==i[u].name.substring(0,2)){n=i[u].name.substring(2);var a=i[u].value;d.getElementById("p0d"+n).innerHTML=a>49?"Data pin:":a>41?"Pins:":"Pin:",d.getElementById("p1d"+n).innerHTML=a>49?"Clk:":"";var l=d.getElementsByName("L1"+n)[0];for(t+=getMem(a,d.getElementsByName("LC"+n)[0].value,d.getElementsByName("L0"+n)[0].value),p=1;p<5;p++){(l=d.getElementsByName("L"+p+n)[0])&&(a>49&&1==p||a>41&&a<50&&p+40<a?(l.style.display="inline",l.required=!0):(l.style.display="none",l.required=!1,l.value=""))}30!=a&&31!=a&&44!=a&&45!=a||(e=!0),d.getElementById("dig"+n).style.display=a>31&&a<48?"none":"inline",d.getElementById("psd"+n).innerHTML=a>31&&a<48?"Index:":"Start:"}var o=d.querySelectorAll(".wc"),s=o.length;for(u=0;u<s;u++)o[u].style.display=e?"inline":"none";if(d.activeElement==d.getElementsByName("LC")[0]){var u=d.getElementsByClassName("iST").length;1==u&&(d.getElementsByName("LC0")[0].value=d.getElementsByName("LC")[0].value)}var r=d.getElementsByTagName("input"),m=0,v=0;for(u=0;u<r.length;u++){if("LC"!=r[u].name.substring(0,2)||"LC"==r[u].name);else{var y=parseInt(r[u].value,10);y&&(m+=y,y>v&&(v=y))}}d.getElementById("m0").innerHTML=t,bquot=t/bmax*100,d.getElementById("dbar").style.background=`linear-gradient(90deg, ${bquot>60?bquot>90?"red":"orange":"#ccc"} 0 ${bquot}%%, #444 ${bquot}%% 100%%)`,d.getElementById("ledwarning").style.display=v>800||bquot>80?"inline":"none",d.getElementById("wreason").innerHTML=bquot>80?"than 60%% of max. LED memory":"800 LEDs per pin";var g=Math.ceil((100+m*laprev)/500)/2;g=g>5?Math.ceil(g):g;i="";var L=30==d.Sf.LAsel.value,c=255==d.Sf.LAsel.value;g<1.02&&!L&&!c?i="ESP 5V pin with 1A USB supply":(i+=L?"12V ":c?"WS2815 12V ":"5V ",i+=g,i+="A supply connected to LEDs");var f=Math.ceil((100+m*laprev)/1500)/2,B="(for most effects, ~";B+=f=f>5?Math.ceil(f):f,B+="A is enough)<br>",d.getElementById("psu").innerHTML=i,d.getElementById("psu2").innerHTML=c?"":B}function addLEDs(e){if(e>1)return maxST=e,void(d.getElementById("+").style.display="inline");var n=d.getElementsByClassName("iST"),t=n.length;if(!(1==e&&t>=maxST||-1==e&&0==t)){var i=d.getElementById("mLC");if(1==e){var a=`<div class="iST">\n ${t>0?'<hr style="width:260px">':""}\n ${t+1}:\n <select name="LT${t}" onchange="UI()">\n <option value="22">WS281x</option>\n <option value="30">SK6812 RGBW</option>\n <option value="31">TM1814</option>\n <option value="24">400kHz</option>\n <option value="50">WS2801</option>\n <option value="51">APA102</option>\n <option value="52">LPD8806</option>\n <option value="53">P9813</option>\n <option value="41">PWM White</option>\n <option value="42">PWM WWCW</option>\n <option value="43">PWM RGB</option>\n <option value="44">PWM RGBW</option>\n <option value="45">PWM RGBWC</option>\n </select>&nbsp;\n Color Order:\n <select name="CO${t}">\n <option value="0">GRB</option>\n <option value="1">RGB</option>\n <option value="2">BRG</option>\n <option value="3">RBG</option>\n <option value="4">BGR</option>\n <option value="5">GBR</option>\n </select><br>\n <span id="p0d${t}">Pin:</span> <input type="number" name="L0${t}" min="0" max="40" required style="width:35px" oninput="UI()"/>\n <span id="p1d${t}">Clock:</span> <input type="number" name="L1${t}" min="0" max="40" style="width:35px"/>\n <span id="p2d${t}"></span><input type="number" name="L2${t}" min="0" max="40" style="width:35px"/>\n <span id="p3d${t}"></span><input type="number" name="L3${t}" min="0" max="40" style="width:35px"/>\n <span id="p4d${t}"></span><input type="number" name="L4${t}" min="0" max="40" style="width:35px"/>\n <br>\n <span id="psd${t}">Start:</span> <input type="number" name="LS${t}" min="0" max="8191" value="0" required />&nbsp;\n <div id="dig${t}" style="display:inline">\n Count: <input type="number" name="LC${t}" min="0" max="2048" value="1" required oninput="UI()" /><br>\n Reverse: <input type="checkbox" name="CV${t}"></div><br>\n </div>`;i.insertAdjacentHTML("beforeend",a)}-1==e&&(n[--t].remove(),--t),d.getElementById("+").style.display=t<maxST-1?"inline":"none",d.getElementById("-").style.display=t>0?"inline":"none",UI()}}function GetV() {var d=document;
var d=document,laprev=55,maxB=1,maxM=5e3,bquot=0;function H(){window.open("https://github.com/Aircoookie/WLED/wiki/Settings#led-settings")}function B(){window.open("/settings","_self")}function bLimits(e,n){maxB=e,maxM=n}function trySubmit(){var e=d.getElementsByTagName("input");for(i=0;i<e.length;i++){var n=e[i].name.substring(0,2);if(("L0"==n||"L1"==n||"RL"==n||"BT"==n||"IR"==n||"AX"==n)&&""!=e[i].value&&"-1"!=e[i].value){if(e[i].value>5&&e[i].value<12)return alert("Sorry, pins 6-11 can not be used."),void e[i].focus();if(d.um_p&&d.um_p.some(n=>n==parseInt(e[i].value,10)))return alert("Usermod pin conflict!"),void e[i].focus();for(j=i+1;j<e.length;j++){var t=e[j].name.substring(0,2);if(("L0"==t||"L1"==t||"RL"==t||"BT"==t||"IR"==t||"AX"==t)&&""!=e[j].value&&e[i].value==e[j].value)return alert("Pin conflict!"),void e[i].focus()}}}if(bquot>100){var a="Too many LEDs for me to handle!";return maxM<1e4&&(a+=" Consider using an ESP32."),void alert(a)}d.Sf.reportValidity()&&d.Sf.submit()}function S(){GetV(),setABL()}function enABL(){var e=d.getElementById("able").checked;d.Sf.LA.value=e?laprev:0,d.getElementById("abl").style.display=e?"inline":"none",d.getElementById("psu2").style.display=e?"inline":"none",d.Sf.LA.value>0&&setABL()}function enLA(){var e=d.Sf.LAsel.value;d.Sf.LA.value=e,d.getElementById("LAdis").style.display=50==e?"inline":"none",UI()}function setABL(){switch(d.getElementById("able").checked=!0,d.Sf.LAsel.value=50,parseInt(d.Sf.LA.value)){case 0:d.getElementById("able").checked=!1,enABL();break;case 30:d.Sf.LAsel.value=30;break;case 35:d.Sf.LAsel.value=35;break;case 55:d.Sf.LAsel.value=55;break;case 255:d.Sf.LAsel.value=255;break;default:d.getElementById("LAdis").style.display="inline"}d.getElementById("m1").innerHTML=maxM,UI()}function getMem(e,n,t){return e<32?maxM<1e4&&3==t?e>29?20*n:15*n:maxM>=1e4?e>29?8*n:6*n:e>29?4*n:3*n:e>31&&e<48?5:44==e||45==e?4*n:3*n}function UI(){var e=!1,t=0;d.getElementById("ampwarning").style.display=d.Sf.MA.value>7200?"inline":"none",255==d.Sf.LA.value?laprev=12:d.Sf.LA.value>0&&(laprev=d.Sf.LA.value);var i=d.getElementsByTagName("select");for(u=0;u<i.length;u++)if("LT"==i[u].name.substring(0,2)){n=i[u].name.substring(2);var a=i[u].value;d.getElementById("p0d"+n).innerHTML=a>49?"Data pin:":a>41?"Pins:":"Pin:",d.getElementById("p1d"+n).innerHTML=a>49?"Clk:":"";var l=d.getElementsByName("L1"+n)[0];for(t+=getMem(a,d.getElementsByName("LC"+n)[0].value,d.getElementsByName("L0"+n)[0].value),p=1;p<5;p++){(l=d.getElementsByName("L"+p+n)[0])&&(a>49&&1==p||a>41&&a<50&&p+40<a?(l.style.display="inline",l.required=!0):(l.style.display="none",l.required=!1,l.value=""))}(30==a||31==a||a>40&&a<46&&43!=a)&&(e=!0),d.getElementById("dig"+n).style.display=a>31&&a<48?"none":"inline",d.getElementById("psd"+n).innerHTML=a>31&&a<48?"Index:":"Start:"}var o=d.querySelectorAll(".wc"),s=o.length;for(u=0;u<s;u++)o[u].style.display=e?"inline":"none";if(d.activeElement==d.getElementsByName("LC")[0]){var u=d.getElementsByClassName("iST").length;1==u&&(d.getElementsByName("LC0")[0].value=d.getElementsByName("LC")[0].value)}var r=d.getElementsByTagName("input"),m=0,v=0;for(u=0;u<r.length;u++){if("LC"!=r[u].name.substring(0,2)||"LC"==r[u].name);else{var y=parseInt(r[u].value,10);y&&(m+=y,y>v&&(v=y))}}d.getElementById("m0").innerHTML=t,bquot=t/maxM*100,d.getElementById("dbar").style.background=`linear-gradient(90deg, ${bquot>60?bquot>90?"red":"orange":"#ccc"} 0 ${bquot}%%, #444 ${bquot}%% 100%%)`,d.getElementById("ledwarning").style.display=v>800||bquot>80?"inline":"none",d.getElementById("wreason").innerHTML=bquot>80?"than 60%% of max. LED memory":"800 LEDs per pin";var g=Math.ceil((100+m*laprev)/500)/2;g=g>5?Math.ceil(g):g;i="";var L=30==d.Sf.LAsel.value,c=255==d.Sf.LAsel.value;g<1.02&&!L&&!c?i="ESP 5V pin with 1A USB supply":(i+=L?"12V ":c?"WS2815 12V ":"5V ",i+=g,i+="A supply connected to LEDs");var f=Math.ceil((100+m*laprev)/1500)/2,B="(for most effects, ~";B+=f=f>5?Math.ceil(f):f,B+="A is enough)<br>",d.getElementById("psu").innerHTML=i,d.getElementById("psu2").innerHTML=c?"":B}function addLEDs(e){if(e>1)return maxB=e,void(d.getElementById("+").style.display="inline");var n=d.getElementsByClassName("iST"),t=n.length;if(!(1==e&&t>=maxB||-1==e&&0==t)){var i=d.getElementById("mLC");if(1==e){var a=`<div class="iST">\n ${t>0?'<hr style="width:260px">':""}\n ${t+1}:\n <select name="LT${t}" onchange="UI()">\n <option value="22">WS281x</option>\n <option value="30">SK6812 RGBW</option>\n <option value="31">TM1814</option>\n <option value="24">400kHz</option>\n <option value="50">WS2801</option>\n <option value="51">APA102</option>\n <option value="52">LPD8806</option>\n <option value="53">P9813</option>\n <option value="41">PWM White</option>\n <option value="42">PWM WWCW</option>\n <option value="43">PWM RGB</option>\n <option value="44">PWM RGBW</option>\n <option value="45">PWM RGBWC</option>\n </select>&nbsp;\n Color Order:\n <select name="CO${t}">\n <option value="0">GRB</option>\n <option value="1">RGB</option>\n <option value="2">BRG</option>\n <option value="3">RBG</option>\n <option value="4">BGR</option>\n <option value="5">GBR</option>\n </select><br>\n <span id="p0d${t}">Pin:</span> <input type="number" name="L0${t}" min="0" max="40" required style="width:35px" oninput="UI()"/>\n <span id="p1d${t}">Clock:</span> <input type="number" name="L1${t}" min="0" max="40" style="width:35px"/>\n <span id="p2d${t}"></span><input type="number" name="L2${t}" min="0" max="40" style="width:35px"/>\n <span id="p3d${t}"></span><input type="number" name="L3${t}" min="0" max="40" style="width:35px"/>\n <span id="p4d${t}"></span><input type="number" name="L4${t}" min="0" max="40" style="width:35px"/>\n <br>\n <span id="psd${t}">Start:</span> <input type="number" name="LS${t}" min="0" max="8191" value="0" required />&nbsp;\n <div id="dig${t}" style="display:inline">\n Count: <input type="number" name="LC${t}" min="0" max="2048" value="1" required oninput="UI()" /><br>\n Reverse: <input type="checkbox" name="CV${t}"></div><br>\n </div>`;i.insertAdjacentHTML("beforeend",a)}-1==e&&(n[--t].remove(),--t),d.getElementById("+").style.display=t<maxB-1?"inline":"none",d.getElementById("-").style.display=t>0?"inline":"none",UI()}}function GetV() {var d=document;
%CSS%%SCSS%</head><body onload="S()"><form
id="form_s" name="Sf" method="post"><div class="helpB"><button type="button"
onclick="H()">?</button></div><button type="button" onclick="B()">Back</button>
Expand Down
1 change: 0 additions & 1 deletion wled00/set.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,6 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)

ledCount = request->arg(F("LC")).toInt();
if (t > 0 && t <= MAX_LEDS) ledCount = t;
//DMA method uses too much ram, TODO: limit!

// upate other pins
#ifndef WLED_DISABLE_INFRARED
Expand Down
5 changes: 2 additions & 3 deletions wled00/wled.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ ethernet_settings ethernetBoards[] = {
5, // eth_power,
23, // eth_mdc,
18, // eth_mdio,
ETH_PHY_LAN8720, // eth_type, (confirm this is right?)
ETH_PHY_LAN8720, // eth_type,
ETH_CLOCK_GPIO17_OUT // eth_clk_mode
}
};
Expand Down Expand Up @@ -338,8 +338,7 @@ void WLED::setup()
WiFi.persistent(false);
WiFi.onEvent(WiFiEvent);

// Serial.println(F("Ada"));
DEBUG_PRINTLN(F("Ada"));
Serial.println(F("Ada"));

// generate module IDs
escapedMac = WiFi.macAddress();
Expand Down
2 changes: 1 addition & 1 deletion wled00/wled.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
*/

// version code in format yymmddb (b = daily build)
#define VERSION 2103041
#define VERSION 2103050

//uncomment this if you have a "my_config.h" file you'd like to use
//#define WLED_USE_MY_CONFIG
Expand Down
6 changes: 4 additions & 2 deletions wled00/xml.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ void getSettingsJS(byte subPage, char* dest)
}

if (subPage == 2) {
char nS[3];
char nS[8];

// add usermod pins as d.um_p array (TODO: usermod config shouldn't use state. instead we should load "um" object from cfg.json)
/*DynamicJsonDocument doc(JSON_BUFFER_SIZE);
Expand All @@ -273,8 +273,10 @@ void getSettingsJS(byte subPage, char* dest)
}*/

#if defined(WLED_MAX_BUSSES) && WLED_MAX_BUSSES>1
oappend(SET_F("addLEDs("));
oappend(SET_F("bLimits("));
oappend(itoa(WLED_MAX_BUSSES,nS,10));
oappend(",");
oappend(itoa(MAX_LED_MEMORY,nS,10));
oappend(SET_F(");"));
#endif

Expand Down

0 comments on commit 71edc3a

Please sign in to comment.