Skip to content

Commit 0af3063

Browse files
committed
Enhancements
- edit WiFi TX power (ESP32) - keep current ledmap ID in UI - limit outputs in UI based on length - wifi.ap addition to JSON Info - relay pin init bugfix - file editor button in UI
1 parent ed0e738 commit 0af3063

File tree

11 files changed

+116
-69
lines changed

11 files changed

+116
-69
lines changed

wled00/FX_fcn.cpp

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1204,6 +1204,7 @@ void WS2812FX::finalizeInit(void) {
12041204
// for the lack of better place enumerate ledmaps here
12051205
// if we do it in json.cpp (serializeInfo()) we are getting flashes on LEDs
12061206
// unfortunately this means we do not get updates after uploads
1207+
// the other option is saving UI settings which will cause enumeration
12071208
enumerateLedmaps();
12081209

12091210
_hasWhiteChannel = _isOffRefreshRequired = false;
@@ -1247,11 +1248,12 @@ void WS2812FX::finalizeInit(void) {
12471248
unsigned busEnd = bus->getStart() + bus->getLength();
12481249
if (busEnd > _length) _length = busEnd;
12491250
#ifdef ESP8266
1250-
if ((!IS_DIGITAL(bus->getType()) || IS_2PIN(bus->getType()))) continue;
1251-
uint8_t pins[5];
1252-
if (!bus->getPins(pins)) continue;
1253-
BusDigital* bd = static_cast<BusDigital*>(bus);
1254-
if (pins[0] == 3) bd->reinit();
1251+
// why do we need to reinitialise GPIO3???
1252+
//if ((!IS_DIGITAL(bus->getType()) || IS_2PIN(bus->getType()))) continue;
1253+
//uint8_t pins[5];
1254+
//if (!bus->getPins(pins)) continue;
1255+
//BusDigital* bd = static_cast<BusDigital*>(bus);
1256+
//if (pins[0] == 3) bd->reinit();
12551257
#endif
12561258
}
12571259

@@ -1767,13 +1769,15 @@ bool WS2812FX::deserializeMap(uint8_t n) {
17671769
bool isFile = WLED_FS.exists(fileName);
17681770

17691771
customMappingSize = 0; // prevent use of mapping if anything goes wrong
1772+
currentLedmap = 0;
1773+
if (n == 0 || isFile) interfaceUpdateCallMode = CALL_MODE_WS_SEND; // schedule WS update (to inform UI)
17701774

17711775
if (!isFile && n==0 && isMatrix) {
17721776
setUpMatrix();
17731777
return false;
17741778
}
17751779

1776-
if (!isFile || !requestJSONBufferLock(7)) return false; // this will trigger setUpMatrix() when called from wled.cpp
1780+
if (!isFile || !requestJSONBufferLock(7)) return false;
17771781

17781782
if (!readObjectFromFile(fileName, nullptr, pDoc)) {
17791783
DEBUG_PRINT(F("ERROR Invalid ledmap in ")); DEBUG_PRINTLN(fileName);
@@ -1797,6 +1801,7 @@ bool WS2812FX::deserializeMap(uint8_t n) {
17971801
if (!map.isNull() && map.size()) { // not an empty map
17981802
customMappingSize = min((unsigned)map.size(), (unsigned)getLengthTotal());
17991803
for (unsigned i=0; i<customMappingSize; i++) customMappingTable[i] = (uint16_t) (map[i]<0 ? 0xFFFFU : map[i]);
1804+
currentLedmap = n;
18001805
}
18011806
} else {
18021807
DEBUG_PRINTLN(F("ERROR LED map allocation error."));

wled00/cfg.cpp

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -79,25 +79,26 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
7979
getStringFromJson(apSSID, ap[F("ssid")], 33);
8080
getStringFromJson(apPass, ap["psk"] , 65); //normally not present due to security
8181
//int ap_pskl = ap[F("pskl")];
82-
8382
CJSON(apChannel, ap[F("chan")]);
8483
if (apChannel > 13 || apChannel < 1) apChannel = 1;
85-
8684
CJSON(apHide, ap[F("hide")]);
8785
if (apHide > 1) apHide = 1;
88-
8986
CJSON(apBehavior, ap[F("behav")]);
90-
9187
/*
9288
JsonArray ap_ip = ap["ip"];
9389
for (byte i = 0; i < 4; i++) {
9490
apIP[i] = ap_ip;
9591
}
9692
*/
9793

98-
noWifiSleep = doc[F("wifi")][F("sleep")] | !noWifiSleep; // inverted
99-
noWifiSleep = !noWifiSleep;
100-
force802_3g = doc[F("wifi")][F("phy")] | force802_3g; //force phy mode g?
94+
JsonObject wifi = doc[F("wifi")];
95+
noWifiSleep = !(wifi[F("sleep")] | !noWifiSleep); // inverted
96+
//noWifiSleep = !noWifiSleep;
97+
CJSON(force802_3g, wifi[F("phy")]); //force phy mode g?
98+
#ifdef ARDUINO_ARCH_ESP32
99+
CJSON(txPower, wifi[F("txpwr")]);
100+
txPower = min(max((int)txPower, (int)WIFI_POWER_2dBm), (int)WIFI_POWER_19_5dBm);
101+
#endif
101102

102103
JsonObject hw = doc[F("hw")];
103104

@@ -774,8 +775,11 @@ void serializeConfig() {
774775
JsonObject wifi = root.createNestedObject(F("wifi"));
775776
wifi[F("sleep")] = !noWifiSleep;
776777
wifi[F("phy")] = force802_3g;
778+
#ifdef ARDUINO_ARCH_ESP32
779+
wifi[F("txpwr")] = txPower;
780+
#endif
777781

778-
#ifdef WLED_USE_ETHERNET
782+
#ifdef WLED_USE_ETHERNET
779783
JsonObject ethernet = root.createNestedObject("eth");
780784
ethernet["type"] = ethernetType;
781785
if (ethernetType != WLED_ETH_NONE && ethernetType < WLED_NUM_ETH_TYPES) {
@@ -797,7 +801,7 @@ void serializeConfig() {
797801
break;
798802
}
799803
}
800-
#endif
804+
#endif
801805

802806
JsonObject hw = root.createNestedObject(F("hw"));
803807

wled00/data/index.htm

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,9 +126,10 @@
126126
<button id="hexcnf" class="btn btn-xs" onclick="fromHex();"><i class="icons btn-icon">&#xe390;</i></button>
127127
</div>
128128
<div style="padding: 8px 0;" id="btns">
129+
<button class="btn btn-xs" title="File editor" type="button" id="edit" onclick="window.location.href=getURL('/edit')"><i class="icons btn-icon">&#xe2c6;</i></button>
129130
<button class="btn btn-xs" title="Pixel Magic Tool" type="button" id="pxmb" onclick="window.location.href=getURL('/pxmagic.htm')"><i class="icons btn-icon">&#xe410;</i></button>
130131
<button class="btn btn-xs" title="Add custom palette" type="button" onclick="window.location.href=getURL('/cpal.htm')"><i class="icons btn-icon">&#xe18a;</i></button>
131-
<button class="btn btn-xs" title="Remove custom palette" type="button" id="rmPal" onclick="palettesData=null;localStorage.removeItem('wledPalx');requestJson({rmcpal:true});setTimeout(loadPalettes,250,loadPalettesData);"><i class="icons btn-icon">&#xe037;</i></button>
132+
<button class="btn btn-xs" title="Remove last custom palette" type="button" id="rmPal" onclick="palettesData=null;localStorage.removeItem('wledPalx');requestJson({rmcpal:true});setTimeout(loadPalettes,250,loadPalettesData);"><i class="icons btn-icon">&#xe037;</i></button>
132133
</div>
133134
<p class="labels hd" id="pall"><i class="icons sel-icon" onclick="tglHex()">&#xe2b3;</i> Color palette</p>
134135
<div id="palw" class="il">

wled00/data/index.js

Lines changed: 27 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -669,18 +669,15 @@ function parseInfo(i) {
669669
//syncTglRecv = i.str;
670670
maxSeg = i.leds.maxseg;
671671
pmt = i.fs.pmt;
672+
if (pcMode && !i.wifi.ap) gId('edit').classList.remove("hide"); else gId('edit').classList.add("hide");
672673
gId('buttonNodes').style.display = lastinfo.ndc > 0 ? null:"none";
673674
// do we have a matrix set-up
674675
mw = i.leds.matrix ? i.leds.matrix.w : 0;
675676
mh = i.leds.matrix ? i.leds.matrix.h : 0;
676677
isM = mw>0 && mh>0;
677678
if (!isM) {
678-
//gId("filter0D").classList.remove('hide');
679-
//gId("filter1D").classList.add('hide');
680679
gId("filter2D").classList.add('hide');
681680
} else {
682-
//gId("filter0D").classList.add('hide');
683-
//gId("filter1D").classList.remove('hide');
684681
gId("filter2D").classList.remove('hide');
685682
}
686683
// if (i.noaudio) {
@@ -745,10 +742,10 @@ ${inforow("Environment",i.arch + " " + i.core + " (" + i.lwip + ")")}
745742
</table>`;
746743
gId('kv').innerHTML = cn;
747744
// update all sliders in Info
748-
for (let sd of (d.querySelectorAll('#kv .sliderdisplay')||[])) {
745+
d.querySelectorAll('#kv .sliderdisplay').forEach((sd,i) => {
749746
let s = sd.previousElementSibling;
750747
if (s) updateTrail(s);
751-
}
748+
});
752749
}
753750

754751
function populateSegments(s)
@@ -895,8 +892,8 @@ function populateSegments(s)
895892
gId('segutil2').style.display = (segCount > 1) ? "block":"none"; // rsbtn parent
896893

897894
if (Array.isArray(li.maps) && li.maps.length>1) {
898-
let cont = `Ledmap:&nbsp;<select class="sel-sg" onchange="requestJson({'ledmap':parseInt(this.value)})"><option value="" selected>Unchanged</option>`;
899-
for (const k of (li.maps||[])) cont += `<option value="${k.id}">${k.id==0?'Default':(k.n?k.n:'ledmap'+k.id+'.json')}</option>`;
895+
let cont = `Ledmap:&nbsp;<select class="sel-sg" onchange="requestJson({'ledmap':parseInt(this.value)})">`;
896+
for (const k of li.maps) cont += `<option ${s.ledmap===k.id?"selected":""} value="${k.id}">${k.id==0?'Default':(k.n?k.n:'ledmap'+k.id+'.json')}</option>`;
900897
cont += "</select></div>";
901898
gId("ledmap").innerHTML = cont;
902899
gId("ledmap").classList.remove('hide');
@@ -991,13 +988,12 @@ function populatePalettes()
991988

992989
function redrawPalPrev()
993990
{
994-
let palettes = d.querySelectorAll('#pallist .lstI');
995-
for (var pal of (palettes||[])) {
991+
d.querySelectorAll('#pallist .lstI').forEach((pal,i) =>{
996992
let lP = pal.querySelector('.lstIprev');
997993
if (lP) {
998994
lP.style = genPalPrevCss(pal.dataset.id);
999995
}
1000-
}
996+
});
1001997
}
1002998

1003999
function genPalPrevCss(id)
@@ -1358,10 +1354,12 @@ function updateSelectedFx()
13581354
}
13591355

13601356
// hide 2D mapping and/or sound simulation options
1361-
var segs = gId("segcont").querySelectorAll(`div[data-map="map2D"]`);
1362-
for (const seg of segs) if (selectedName.indexOf("\u25A6")<0) seg.classList.remove('hide'); else seg.classList.add('hide');
1363-
var segs = gId("segcont").querySelectorAll(`div[data-snd="si"]`);
1364-
for (const seg of segs) if (selectedName.indexOf("\u266A")<0 && selectedName.indexOf("\u266B")<0) seg.classList.add('hide'); else seg.classList.remove('hide'); // also "♫ "?
1357+
gId("segcont").querySelectorAll(`div[data-map="map2D"]`).forEach((seg)=>{
1358+
if (selectedName.indexOf("\u25A6")<0) seg.classList.remove('hide'); else seg.classList.add('hide');
1359+
});
1360+
gId("segcont").querySelectorAll(`div[data-snd="si"]`).forEach((seg)=>{
1361+
if (selectedName.indexOf("\u266A")<0 && selectedName.indexOf("\u266B")<0) seg.classList.add('hide'); else seg.classList.remove('hide'); // also "♫ "?
1362+
});
13651363
}
13661364
}
13671365

@@ -1568,8 +1566,7 @@ function setEffectParameters(idx)
15681566
var paOnOff = (effectPars.length<3 || effectPars[2]=='')?[]:effectPars[2].split(",");
15691567

15701568
// set html slider items on/off
1571-
let sliders = d.querySelectorAll("#sliders .sliderwrap");
1572-
sliders.forEach((slider, i)=>{
1569+
d.querySelectorAll("#sliders .sliderwrap").forEach((slider, i)=>{
15731570
let text = slider.getAttribute("title");
15741571
if ((!controlDefined && i<((idx<128)?2:nSliders)) || (slOnOff.length>i && slOnOff[i]!="")) {
15751572
if (slOnOff.length>i && slOnOff[i]!="!") text = slOnOff[i];
@@ -1583,8 +1580,7 @@ function setEffectParameters(idx)
15831580

15841581
if (slOnOff.length > 5) { // up to 3 checkboxes
15851582
gId('fxopt').classList.remove('fade');
1586-
let checks = d.querySelectorAll("#sliders .ochkl");
1587-
checks.forEach((check, i)=>{
1583+
d.querySelectorAll("#sliders .ochkl").forEach((check, i)=>{
15881584
let text = check.getAttribute("title");
15891585
if (5+i<slOnOff.length && slOnOff[5+i]!=='') {
15901586
if (slOnOff.length>5+i && slOnOff[5+i]!="!") text = slOnOff[5+i];
@@ -2031,7 +2027,7 @@ ${makePlSel(plJson[i].end?plJson[i].end:0, true)}
20312027
</label>`;
20322028
if (Array.isArray(lastinfo.maps) && lastinfo.maps.length>1) {
20332029
content += `<div class="lbl-l">Ledmap:&nbsp;<div class="sel-p"><select class="sel-p" id="p${i}lmp"><option value="">Unchanged</option>`;
2034-
for (const k of (lastinfo.maps||[])) content += `<option value="${k.id}"${(i>0 && pJson[i].ledmap==k.id)?" selected":""}>${k.id==0?'Default':(k.n?k.n:'ledmap'+k.id+'.json')}</option>`;
2030+
for (const k of lastinfo.maps) content += `<option value="${k.id}"${(i>0 && pJson[i].ledmap==k.id)?" selected":""}>${k.id==0?'Default':(k.n?k.n:'ledmap'+k.id+'.json')}</option>`;
20352031
content += "</select></div></div>";
20362032
}
20372033
}
@@ -2179,13 +2175,12 @@ function selGrp(g)
21792175
{
21802176
event.preventDefault();
21812177
event.stopPropagation();
2182-
var sel = gId(`segcont`).querySelectorAll(`div[data-set="${g}"]`);
21832178
var obj = {"seg":[]};
21842179
for (let i=0; i<=lSeg; i++) if (gId(`seg${i}`)) obj.seg.push({"id":i,"sel":false});
2185-
for (let s of (sel||[])) {
2180+
gId(`segcont`).querySelectorAll(`div[data-set="${g}"]`).forEach((s)=>{
21862181
let i = parseInt(s.id.substring(3));
21872182
obj.seg[i] = {"id":i,"sel":true};
2188-
}
2183+
});
21892184
if (obj.seg.length) requestJson(obj);
21902185
}
21912186

@@ -2839,15 +2834,14 @@ function search(field, listId = null) {
28392834
// do not search if filter is active
28402835
if (gId("filters").querySelectorAll("input[type=checkbox]:checked").length) return;
28412836

2842-
const listItems = gId(listId).querySelectorAll('.lstI');
28432837
// filter list items but leave (Default & Solid) always visible
2844-
for (i = (listId === 'pcont' ? 0 : 1); i < listItems.length; i++) {
2845-
const listItem = listItems[i];
2838+
gId(listId).querySelectorAll('.lstI').forEach((listItem,i)=>{
2839+
if (listId!=='pcont' && i===0) return;
28462840
const listItemName = listItem.querySelector('.lstIname').innerText.toUpperCase();
28472841
const searchIndex = listItemName.indexOf(field.value.toUpperCase());
28482842
listItem.style.display = (searchIndex < 0) ? 'none' : '';
28492843
listItem.dataset.searchIndex = searchIndex;
2850-
}
2844+
});
28512845

28522846
// sort list items by search index and name
28532847
const sortedListItems = Array.from(listItems).sort((a, b) => {
@@ -2908,14 +2902,12 @@ function filterFx() {
29082902
inputField.value = '';
29092903
inputField.focus();
29102904
clean(inputField.nextElementSibling);
2911-
const listItems = gId("fxlist").querySelectorAll('.lstI');
2912-
for (let i = 1; i < listItems.length; i++) {
2913-
const listItem = listItems[i];
2905+
gId("fxlist").querySelectorAll('.lstI').forEach((listItem,i) => {
29142906
const listItemName = listItem.querySelector('.lstIname').innerText;
29152907
let hide = false;
29162908
gId("filters").querySelectorAll("input[type=checkbox]").forEach((e) => { if (e.checked && !listItemName.includes(e.dataset.flt)) hide = true; });
29172909
listItem.style.display = hide ? 'none' : '';
2918-
}
2910+
});
29192911
}
29202912

29212913
function preventBlur(e) {
@@ -3066,6 +3058,7 @@ function size()
30663058

30673059
function togglePcMode(fromB = false)
30683060
{
3061+
let ap = (fromB && !lastinfo) || (lastinfo && lastinfo.wifi && lastinfo.witi.ap);
30693062
if (fromB) {
30703063
pcModeA = !pcModeA;
30713064
localStorage.setItem('pcm', pcModeA);
@@ -3075,6 +3068,7 @@ function togglePcMode(fromB = false)
30753068
if (!fromB && ((wW < 1024 && lastw < 1024) || (wW >= 1024 && lastw >= 1024))) return; // no change in size and called from size()
30763069
if (pcMode) openTab(0, true);
30773070
gId('buttonPcm').className = (pcMode) ? "active":"";
3071+
if (pcMode && !ap) gId('edit').classList.remove("hide"); else gId('edit').classList.add("hide");
30783072
gId('bot').style.height = (pcMode && !cfg.comp.pcmbot) ? "0":"auto";
30793073
sCol('--bh', gId('bot').clientHeight + "px");
30803074
_C.style.width = (pcMode || simplifiedUI)?'100%':'400%';
@@ -3100,8 +3094,7 @@ function mergeDeep(target, ...sources)
31003094

31013095
function tooltip(cont=null)
31023096
{
3103-
const elements = d.querySelectorAll((cont?cont+" ":"")+"[title]");
3104-
elements.forEach((element)=>{
3097+
d.querySelectorAll((cont?cont+" ":"")+"[title]").forEach((element)=>{
31053098
element.addEventListener("mouseover", ()=>{
31063099
// save title
31073100
element.setAttribute("data-title", element.getAttribute("title"));
@@ -3128,8 +3121,7 @@ function tooltip(cont=null)
31283121
});
31293122

31303123
element.addEventListener("mouseout", ()=>{
3131-
const tooltips = d.querySelectorAll('.tooltip');
3132-
tooltips.forEach((tooltip)=>{
3124+
d.querySelectorAll('.tooltip').forEach((tooltip)=>{
31333125
tooltip.classList.remove("visible");
31343126
d.body.removeChild(tooltip);
31353127
});

wled00/data/settings_leds.htm

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
<title>LED Settings</title>
77
<script>
88
var d=document,laprev=55,maxB=1,maxV=0,maxM=4000,maxPB=4096,maxL=1333,maxCO=10,maxLbquot=0; //maximum bytes for LED allocation: 4kB for 8266, 32kB for 32
9+
var oMaxB=1;
910
d.um_p = [];
1011
d.rsvd = [];
1112
d.ro_gpio = [];
@@ -58,7 +59,13 @@
5859
timeout = setTimeout(function(){ x.className = x.className.replace("show", ""); }, 2900);
5960
}
6061
function bLimits(b,v,p,m,l,o) {
61-
maxB = b; maxV = v; maxM = m; maxPB = p; maxL = l; maxCO = o;
62+
// maxB - max buses (can be changed if using ESP32 parallel I2S)
63+
// maxV - min virtual buses
64+
// maxPB - max LEDs per bus
65+
// maxM - max LED memory
66+
// maxL - max LEDs
67+
// maxCO - max Color Order mappings
68+
oMaxB = maxB = b; maxV = v; maxM = m; maxPB = p; maxL = l; maxCO = o;
6269
}
6370
function pinsOK() {
6471
var ok = true;
@@ -211,6 +218,7 @@
211218
let busMA = 0;
212219
let sLC = 0, sPC = 0, sDI = 0, maxLC = 0;
213220
const ablEN = d.Sf.ABL.checked;
221+
maxB = oMaxB; // TODO make sure we start with all possible buses
214222

215223
// enable/disable LED fields
216224
d.Sf.querySelectorAll("#mLC select[name^=LT]").forEach((s)=>{
@@ -275,6 +283,7 @@
275283
// do we have a led count field
276284
if (nm=="LC") {
277285
let c = parseInt(LC.value,10); //get LED count
286+
if (c > 300 && i < 8) maxB = min(oMaxB,10); //TODO: hard limit for buses when using ESP32 parallel I2S
278287
if (!customStarts || !startsDirty[n]) gId("ls"+n).value=sLC; //update start value
279288
gId("ls"+n).disabled = !customStarts; //enable/disable field editing
280289
if (c) {
@@ -827,7 +836,8 @@ <h3>Hardware setup</h3>
827836
Apply IR change to main segment only: <input type="checkbox" name="MSO"><br>
828837
<div id="json" style="display:none;">JSON file: <input type="file" name="data" accept=".json"><button type="button" class="sml" onclick="uploadFile('/ir.json')">Upload</button><br></div>
829838
<a href="https://kno.wled.ge/interfaces/infrared/" target="_blank">IR info</a><br>
830-
Relay GPIO: <input type="number" min="-1" max="48" name="RL" onchange="UI()" class="xs"><span style="cursor: pointer;" onclick="off('RL')">&nbsp;&#x2715;</span> Invert <input type="checkbox" name="RM"> Open drain <input type="checkbox" name="RO"><br>
839+
Relay GPIO: <input type="number" min="-1" max="48" name="RL" onchange="UI()" class="xs"><span style="cursor: pointer;" onclick="off('RL')">&nbsp;&#x2715;</span><br>
840+
Invert <input type="checkbox" name="RM"> Open drain <input type="checkbox" name="RO"><br>
831841
<hr class="sml">
832842
<h3>Defaults</h3>
833843
Turn LEDs on after power up/reset: <input type="checkbox" name="BO"><br>

wled00/data/settings_wifi.htm

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -224,8 +224,23 @@ <h3>Configure Access Point</h3>
224224
<h3>Experimental</h3>
225225
Force 802.11g mode (ESP8266 only): <input type="checkbox" name="FG"><br>
226226
Disable WiFi sleep: <input type="checkbox" name="WS"><br>
227-
<i>Can help with connectivity issues.<br>
228-
Do not enable if WiFi is working correctly, increases power consumption.</i>
227+
<i>Can help with connectivity issues and Audioreactive sync.<br>
228+
Disabling WiFi sleep increases power consumption.</i><br>
229+
<div id="tx">TX power: <select name="TX">
230+
<option value="78">19.5 dBm</option>
231+
<option value="76">19 dBm</option>
232+
<option value="74">18.5 dBm</option>
233+
<option value="68">17 dBm</option>
234+
<option value="60">15 dBm</option>
235+
<option value="52">13 dBm</option>
236+
<option value="44">11 dBm</option>
237+
<option value="34">8.5 dBm</option>
238+
<option value="28">7 dBm</option>
239+
<option value="20">5 dBm</option>
240+
<option value="8">2 dBm</option>
241+
</select><br>
242+
<i class="warn">WARNING: Modifying TX power may render device unreachable.</i>
243+
</div>
229244

230245
<h3>ESP-NOW Wireless</h3>
231246
<div id="NoESPNOW" class="hide">

0 commit comments

Comments
 (0)