From 6b6dc4b14e861e53aef4b3f40f846301ff0c4f9f Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Thu, 17 Nov 2022 18:17:37 +0100 Subject: [PATCH 1/6] bthrm - Show internal values if replacing HRM --- apps/bthrm/bthrm.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/bthrm/bthrm.js b/apps/bthrm/bthrm.js index fadf2a5d8..84fce927c 100644 --- a/apps/bthrm/bthrm.js +++ b/apps/bthrm/bthrm.js @@ -91,7 +91,12 @@ function onHrm(e) { var settings = require('Storage').readJSON("bthrm.json", true) || {}; Bangle.on('BTHRM', onBtHrm); -Bangle.on('HRM', onHrm); + +if (settings.replace){ + Bangle.on('HRM_int', onHrm); +} else { + Bangle.on('HRM', onHrm); +} Bangle.setHRMPower(1,'bthrm'); if (!(settings.startWithHrm)){ From 1530ed39154de83e0e64e2fee49bd255f300f74e Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Thu, 1 Dec 2022 22:49:02 +0100 Subject: [PATCH 2/6] bthrm - Reimplement viewer app with layout --- apps/bthrm/bthrm.js | 191 ++++++++++++++++++++++++++++++++------------ 1 file changed, 139 insertions(+), 52 deletions(-) diff --git a/apps/bthrm/bthrm.js b/apps/bthrm/bthrm.js index 84fce927c..05bf323a5 100644 --- a/apps/bthrm/bthrm.js +++ b/apps/bthrm/bthrm.js @@ -1,5 +1,9 @@ var intervalInt; var intervalBt; +var intervalAgg; + +const BPM_FONT_SIZE="19%"; +const VALUE_TIMEOUT=3000; var BODY_LOCS = { 0: 'Other', @@ -7,40 +11,123 @@ var BODY_LOCS = { 2: 'Wrist', 3: 'Finger', 4: 'Hand', - 5: 'Ear Lobe', + 5: 'Earlobe', 6: 'Foot', +}; + +var Layout = require("Layout"); + +function border(l,c) { + g.setColor(c).drawLine(l.x+l.w*0.05, l.y-4, l.x+l.w*0.95, l.y-4); + c++; } -function clear(y){ - g.reset(); - g.clearRect(0,y,g.getWidth(),y+75); -} - -function draw(y, type, event) { - clear(y); - var px = g.getWidth()/2; - var str = event.bpm + ""; - g.reset(); - g.setFontAlign(0,0); - g.setFontVector(40).drawString(str,px,y+20); - str = "Event: " + type; - if (type === "HRM") { - str += " Confidence: " + event.confidence; - g.setFontVector(12).drawString(str,px,y+40); - str = " Source: " + (event.src ? event.src : "internal"); - g.setFontVector(12).drawString(str,px,y+50); +function getRow(id, text, additionalInfo){ + let additional = []; + let l = { + type:"h", c: [ + { + type:"v", + width: g.getWidth()*0.4, + c: [ + {type:"txt", halign:1, font:"8%", label:text, id:id+"text" }, + {type:"txt", halign:1, font:BPM_FONT_SIZE, label:"--", id:id, bgCol: g.theme.bg } + ] + },{ + type:undefined, fillx:1 + },{ + type:"v", + valign: -1, + width: g.getWidth()*0.45, + c: additional + },{ + type:undefined, width:g.getWidth()*0.05 + } + ] + }; + for (let i of additionalInfo){ + let label = {type:"txt", font:"6x8", label:i + ":" }; + let value = {type:"txt", font:"6x8", label:"--", id:id + i }; + additional.push({type:"h", halign:-1, c:[ label, {type:undefined, fillx:1}, value ]}); } - if (type === "BTHRM"){ - if (event.battery) str += " Bat: " + (event.battery ? event.battery : ""); - g.setFontVector(12).drawString(str,px,y+40); - str= ""; - if (event.location) str += "Loc: " + BODY_LOCS[event.location]; - if (event.rr && event.rr.length > 0) str += " RR: " + event.rr.join(","); - g.setFontVector(12).drawString(str,px,y+50); - str= ""; - if (event.contact) str += " Contact: " + event.contact; - if (event.energy) str += " kJoule: " + event.energy.toFixed(0); - g.setFontVector(12).drawString(str,px,y+60); + + return l; +} + +var layout = new Layout( { + type:"v", c: [ + getRow("int", "INT", ["Confidence"]), + getRow("agg", "HRM", ["Confidence", "Source"]), + getRow("bt", "BT", ["Battery","Location","Contact", "RR", "Energy"]), + { type:undefined, height:8 } //dummy to protect debug output + ] +}, { + lazy:true +}); + +var int,agg,bt; +var firstEvent = true; + +var drawTimeout; +function draw(){ + + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 1000); + + if (!(int || agg || bt)) return; + + if (firstEvent) { + g.clearRect(Bangle.appRect); + firstEvent = false; + } + + if (int){ + layout.int.label = int.bpm; + if (!isNaN(int.confidence)) layout.intConfidence.label = int.confidence; + } else { + layout.int.label = "--"; + layout.intConfidence.label = "--"; + } + + if (agg){ + layout.agg.label = agg.bpm; + if (!isNaN(agg.confidence)) layout.aggConfidence.label = agg.confidence; + if (agg.src) layout.aggSource.label = agg.src; + } else { + layout.agg.label = "--"; + layout.aggConfidence.label = "--"; + layout.aggSource.label = "--"; + } + + if (bt) { + layout.bt.label = bt.bpm; + if (bt.battery) layout.btBattery.label = bt.battery; + if (bt.rr) layout.btRR.label = bt.rr.join(","); + if (bt.location) layout.btLocation.label = BODY_LOCS[bt.location]; + if (bt.contact !== undefined) layout.btContact.label = bt.contact ? "Yes":"No"; + if (bt.energy) layout.btEnergy.label = bt.energy.toFixed(0) + "kJ"; + } else { + layout.bt.label = "--"; + layout.btBattery.label = "--"; + layout.btRR.label = "--"; + layout.btLocation.label = "--"; + layout.btContact.label = "--"; + layout.btEnergy.label = "--"; + } + + layout.update(); + layout.render(); + let first = true; + for (let c of layout.l.c){ + if (first) { + first = false; + continue; + } + if (c.type && c.type == "h") + border(c,g.theme.fg); } } @@ -57,11 +144,7 @@ function showStatusInfo(txt) { } function onBtHrm(e) { - if (firstEventBt){ - clear(24); - firstEventBt = false; - } - draw(100, "BTHRM", e); + bt = e; if (e.bpm === 0){ Bangle.buzz(100,0.2); } @@ -69,34 +152,37 @@ function onBtHrm(e) { clearInterval(intervalBt); } intervalBt = setInterval(()=>{ - clear(100); - }, 2000); + bt = undefined; + }, VALUE_TIMEOUT); } -function onHrm(e) { - if (firstEventInt){ - clear(24); - firstEventInt = false; - } - draw(24, "HRM", e); +function onInt(e) { + int = e; if (intervalInt){ clearInterval(intervalInt); } intervalInt = setInterval(()=>{ - clear(24); - }, 2000); + int = undefined; + }, VALUE_TIMEOUT); +} + +function onAgg(e) { + agg = e; + if (intervalAgg){ + clearInterval(intervalAgg); + } + intervalAgg = setInterval(()=>{ + agg = undefined; + }, VALUE_TIMEOUT); } var settings = require('Storage').readJSON("bthrm.json", true) || {}; Bangle.on('BTHRM', onBtHrm); +Bangle.on('HRM_int', onInt); +Bangle.on('HRM', onAgg); -if (settings.replace){ - Bangle.on('HRM_int', onHrm); -} else { - Bangle.on('HRM', onHrm); -} Bangle.setHRMPower(1,'bthrm'); if (!(settings.startWithHrm)){ @@ -108,10 +194,11 @@ Bangle.loadWidgets(); Bangle.drawWidgets(); if (Bangle.setBTHRMPower){ g.reset().setFont("6x8",2).setFontAlign(0,0); - g.drawString("Please wait...",g.getWidth()/2,g.getHeight()/2 - 24); + g.drawString("Please wait...",g.getWidth()/2,g.getHeight()/2); + draw(); } else { g.reset().setFont("6x8",2).setFontAlign(0,0); - g.drawString("BTHRM disabled",g.getWidth()/2,g.getHeight()/2 + 32); + g.drawString("BTHRM disabled",g.getWidth()/2,g.getHeight()/2); } E.on('kill', ()=>Bangle.setBTHRMPower(0,'bthrm')); From e77a5f973c535bbfc2a755ceebdd4dc13c3f82e0 Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Fri, 2 Dec 2022 19:10:26 +0100 Subject: [PATCH 3/6] bthrm - Emit copies of events when changing --- apps/bthrm/lib.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/apps/bthrm/lib.js b/apps/bthrm/lib.js index f5e0e1e5b..a792167ca 100644 --- a/apps/bthrm/lib.js +++ b/apps/bthrm/lib.js @@ -553,14 +553,15 @@ exports.enable = () => { if (settings.replace){ // register a listener for original HRM events and emit as HRM_int - Bangle.on("HRM", (e) => { - e.modified = true; + Bangle.on("HRM", (o) => { + let e = Object.assign({},o); log("Emitting HRM_int", e); Bangle.emit("HRM_int", e); if (fallbackActive){ // if fallback to internal HRM is active, emit as HRM_R to which everyone listens - log("Emitting HRM_R(int)", e); - Bangle.emit("HRM_R", e); + o.src = "int"; + log("Emitting HRM_R(int)", o); + Bangle.emit("HRM_R", o); } }); @@ -576,6 +577,13 @@ exports.enable = () => { if (name == "HRM") o("HRM_R", cb); else o(name, cb); })(Bangle.removeListener); + } else { + Bangle.on("HRM", (o)=>{ + o.src = "int"; + let e = Object.assign({},o); + log("Emitting HRM_int", e); + Bangle.emit("HRM_int", e); + }); } Bangle.origSetHRMPower = Bangle.setHRMPower; From 85c2f92e0aa816ce8c47f46ea077ae82548b7d90 Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Fri, 2 Dec 2022 19:15:03 +0100 Subject: [PATCH 4/6] bthrm - Bump version --- apps/bthrm/ChangeLog | 1 + apps/bthrm/metadata.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/bthrm/ChangeLog b/apps/bthrm/ChangeLog index 99cf0c670..000c5e3f8 100644 --- a/apps/bthrm/ChangeLog +++ b/apps/bthrm/ChangeLog @@ -40,3 +40,4 @@ 0.16: Set powerdownRequested correctly on BTHRM power on Additional logging on errors Add debug option for disabling active scanning +0.17: New GUI based on layout library diff --git a/apps/bthrm/metadata.json b/apps/bthrm/metadata.json index 18c34ea33..0977fd755 100644 --- a/apps/bthrm/metadata.json +++ b/apps/bthrm/metadata.json @@ -2,7 +2,7 @@ "id": "bthrm", "name": "Bluetooth Heart Rate Monitor", "shortName": "BT HRM", - "version": "0.16", + "version": "0.17", "description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.", "icon": "app.png", "type": "app", From ddbbef2bf814eb254d9fd3969f5c05ddb4e70da1 Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Fri, 2 Dec 2022 23:50:14 +0100 Subject: [PATCH 5/6] bthrm - Change from timeouts to timestamps for resetting values --- apps/bthrm/bthrm.js | 55 +++++++++------------------------------------ 1 file changed, 11 insertions(+), 44 deletions(-) diff --git a/apps/bthrm/bthrm.js b/apps/bthrm/bthrm.js index 05bf323a5..e163dd8b7 100644 --- a/apps/bthrm/bthrm.js +++ b/apps/bthrm/bthrm.js @@ -1,7 +1,3 @@ -var intervalInt; -var intervalBt; -var intervalAgg; - const BPM_FONT_SIZE="19%"; const VALUE_TIMEOUT=3000; @@ -19,7 +15,6 @@ var Layout = require("Layout"); function border(l,c) { g.setColor(c).drawLine(l.x+l.w*0.05, l.y-4, l.x+l.w*0.95, l.y-4); - c++; } function getRow(id, text, additionalInfo){ @@ -68,23 +63,17 @@ var layout = new Layout( { var int,agg,bt; var firstEvent = true; -var drawTimeout; function draw(){ - - if (drawTimeout) clearTimeout(drawTimeout); - drawTimeout = setTimeout(function() { - drawTimeout = undefined; - draw(); - }, 1000); - if (!(int || agg || bt)) return; - + if (firstEvent) { g.clearRect(Bangle.appRect); firstEvent = false; } - - if (int){ + + let now = Date.now(); + + if (int && int.time > (now - VALUE_TIMEOUT)){ layout.int.label = int.bpm; if (!isNaN(int.confidence)) layout.intConfidence.label = int.confidence; } else { @@ -92,7 +81,7 @@ function draw(){ layout.intConfidence.label = "--"; } - if (agg){ + if (agg && agg.time > (now - VALUE_TIMEOUT)){ layout.agg.label = agg.bpm; if (!isNaN(agg.confidence)) layout.aggConfidence.label = agg.confidence; if (agg.src) layout.aggSource.label = agg.src; @@ -102,7 +91,7 @@ function draw(){ layout.aggSource.label = "--"; } - if (bt) { + if (bt && bt.time > (now - VALUE_TIMEOUT)) { layout.bt.label = bt.bpm; if (bt.battery) layout.btBattery.label = bt.battery; if (bt.rr) layout.btRR.label = bt.rr.join(","); @@ -131,9 +120,6 @@ function draw(){ } } -var firstEventBt = true; -var firstEventInt = true; - // This can get called for the boot code to show what's happening function showStatusInfo(txt) { @@ -145,38 +131,19 @@ function showStatusInfo(txt) { function onBtHrm(e) { bt = e; - if (e.bpm === 0){ - Bangle.buzz(100,0.2); - } - if (intervalBt){ - clearInterval(intervalBt); - } - intervalBt = setInterval(()=>{ - bt = undefined; - }, VALUE_TIMEOUT); + bt.time = Date.now(); } function onInt(e) { int = e; - if (intervalInt){ - clearInterval(intervalInt); - } - intervalInt = setInterval(()=>{ - int = undefined; - }, VALUE_TIMEOUT); + int.time = Date.now(); } function onAgg(e) { agg = e; - if (intervalAgg){ - clearInterval(intervalAgg); - } - intervalAgg = setInterval(()=>{ - agg = undefined; - }, VALUE_TIMEOUT); + agg.time = Date.now(); } - var settings = require('Storage').readJSON("bthrm.json", true) || {}; Bangle.on('BTHRM', onBtHrm); @@ -195,7 +162,7 @@ Bangle.drawWidgets(); if (Bangle.setBTHRMPower){ g.reset().setFont("6x8",2).setFontAlign(0,0); g.drawString("Please wait...",g.getWidth()/2,g.getHeight()/2); - draw(); + setInterval(draw, 1000); } else { g.reset().setFont("6x8",2).setFontAlign(0,0); g.drawString("BTHRM disabled",g.getWidth()/2,g.getHeight()/2); From 8016e8ec5333ec8fe5ef4b8ea95b4a7428513bf6 Mon Sep 17 00:00:00 2001 From: Martin Boonk Date: Fri, 2 Dec 2022 23:53:32 +0100 Subject: [PATCH 6/6] bthrm - Correctly handle 0 values --- apps/bthrm/bthrm.js | 6 +++--- apps/bthrm/metadata.json | 1 + apps/bthrm/screen.png | Bin 0 -> 3702 bytes 3 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 apps/bthrm/screen.png diff --git a/apps/bthrm/bthrm.js b/apps/bthrm/bthrm.js index e163dd8b7..b07e7bd37 100644 --- a/apps/bthrm/bthrm.js +++ b/apps/bthrm/bthrm.js @@ -93,11 +93,11 @@ function draw(){ if (bt && bt.time > (now - VALUE_TIMEOUT)) { layout.bt.label = bt.bpm; - if (bt.battery) layout.btBattery.label = bt.battery; + if (!isNaN(bt.battery)) layout.btBattery.label = bt.battery + "%"; if (bt.rr) layout.btRR.label = bt.rr.join(","); - if (bt.location) layout.btLocation.label = BODY_LOCS[bt.location]; + if (!isNaN(bt.location)) layout.btLocation.label = BODY_LOCS[bt.location]; if (bt.contact !== undefined) layout.btContact.label = bt.contact ? "Yes":"No"; - if (bt.energy) layout.btEnergy.label = bt.energy.toFixed(0) + "kJ"; + if (!isNaN(bt.energy)) layout.btEnergy.label = bt.energy.toFixed(0) + "kJ"; } else { layout.bt.label = "--"; layout.btBattery.label = "--"; diff --git a/apps/bthrm/metadata.json b/apps/bthrm/metadata.json index 0977fd755..fea274ff3 100644 --- a/apps/bthrm/metadata.json +++ b/apps/bthrm/metadata.json @@ -5,6 +5,7 @@ "version": "0.17", "description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.", "icon": "app.png", + "screenshots": [{"url":"screen.png"}], "type": "app", "tags": "health,bluetooth,hrm,bthrm", "supports": ["BANGLEJS","BANGLEJS2"], diff --git a/apps/bthrm/screen.png b/apps/bthrm/screen.png new file mode 100644 index 0000000000000000000000000000000000000000..6b6b8522758de6d8a330fbe6184b59517f74360a GIT binary patch literal 3702 zcmaJ^c{CJy^#9IgBunGj-=L64+En%>&0;BQcE+^YvJRpNgJhZTo`?uBNn-HBXw+cH zDi~!pWhNn64Jr}WRpn>5n=iJ+@7y?RVNrK{b!)V86+eNzp7PKEf%`Y~1L@)h)H7=N z>OKc{opC=@zTJ|`Yf|T2ka*LrcX9(p+{r^__>}86jg((F`i_1zH^AqHLaktG_hrbP z)^Dhvu`mYNOc108D52RMK0rY6q!w=qCA`#=5-&fNWzokLV=w}&!*!CO0Z3Bq&5Zjk z+XxQHL>vn}3Fmd@<3LBNg}i$rNt66*Sf9GCWG= zE3%#AXuC)$Z|*Su8vtMWb6O@Y9O*4C!o{61Msr z1@NvJf#j?gS6)Z%4mNg;0@wGpxgR@YzsDUb(D`dv1dP%hn>h$@$h61nS4E6p$rZWY zKsbV}9c(P64H56zf-TaGfFnHVGkan8 zfWNj+d_=T&!VQN*v^`M#haZ253c#lK-tP6bynD^45bcKXMFs(*VwAK}Xm(_URl3OOib0p>?HXF#LOB|4nYFt7A${^KGUbvxUQ8m{=t@>3LReD zwAlFTn)xcz{~Cg_;6Fl5+;ls6IQE8ry#9~;+i&Na?AIo}jdJ14Ox@%;NbltBJjStUp3&oX5*ic6g-?XAPHBMEuoIq)Kw4>X5*Orh5q#DdeI51r{qr{?m7 zmm{a1h3Ovz8;3kTx};8HcDU$p`rD940j=bn8rugZ9 zhIXqWn2wfa#-GL6WvMy&){H@VBw4_UZ;r8;Jy%%S77)gZ-0lc%HG%q|R5+`;fMUrI z)r%6sx@-uD9E9h@^xO#~rJXq%vT8$Nc-JoXJ<_Y)wKDmWKvmnZME;O=cgsX6F;S4v zEH!^JIZ9$Tw2-tzGD`n znH7jiFfvzALo_P#2uhU2t_NWy7l2ywYSd0pzq9&flMs+^%}+QT)=Y}Wlh_Jx$+^QEDb0j+wiV_ z3W7SFy9Ia5ic^q(ZtbT)zNXsBpFXrBLM$+@<+>WnKV~D#Eu&yARYZZw94V7o83r)k z3@o_+KAK%yc}T$UO?t8N35YQRD08J)T}psVIEAwyB5@1EjIQ-sSN)4V+-_hjCU`$9 zl$R6+>pE?x&;JDVi1C9VIY4RYOLrB|XfG&3L@Yl!Qfyk;7He+U!Kusx_+%&>{B%bg z{m$>bQMprl#E?6^KY|P|OIyuDqKp*tUbpk5BQ8Y}eG;~pnV{ZfjfMOncrR3*Kb5SE zsQZz*W9f~6sVH%A`@A&EHFseZB@K}ddTalD{@W@_jvc*TbvW4BK-PILeq?ZBaAZbG zOL!Dd?PO3rKZ&j}Mo&&;vz&&}6So^e;@euC)F2T!`4pHIv~l0s{WBd< zJmfSjL-G1T$92Pdrq;)T$$bHQ=ifinr?#xgay^NrBm4hwPrN+U6&+%x_}tup%1$(6 zOP#N68GrVo#5&O0zGjC<*}S8Gn|!+j7CP8}RVMbk4)U+*g^X(xw^X;l2Lg$D~(v|F}`pEQn||++4Ag4PwY? zKpSIwl_9%*j)HsiCE`|@2|fQ=YfD#&{o7+SL*wrvuHbBXIQ1TK?O5jY%VJ1XU*PlC z|GwbpG^!HU9_)ME^29+W`%}ARup*aFV@CqAqPSHDk~v2Ek7oqkbYK00YkeL9B??|3 zl)?XUxo|@g(F#MC-Vnu}u5xdn9|a;m1_+r3N)CoAUwOGsrON6HZYXAlVx5A zjT0@2#p!9>32_ag(9#c_Rf#%R^KShJ7{q1V(VRo7}$^ zOX2I8U+%Dm0G2g&IkE(*&2FCf5o++9jn^TTI-aKFIzet5xkJG9x)A=6BVa??Qr*-Z zuwF4ZUe;@_9`0pNUiJ8-2L;eK^#;wJqG64zZgzteqq1QSVt?Z(UKVKk8 zF-H}pW}B3gik4%`uA(q|mY~W(GanFURNp^WjVN;$8PP;-C<%l({OIQ3-DMUN@~h&f zN>T4htRnz^Bg%}OAorOvRBx~6Z@-7dVV0)gu1GvEY`RAC*Q(5EnQ{@=>s)=1Xc=v6 zjBM@6s9Q!Mzp(Hrm#U?%-iq;sPfH_Q^68R|3ZdzkR}f7c<+{x_{N4;J(bE$zBS?8Wbe+cc+*tCHQgp4GY9f6 zOHHqVtS3P)HEfErw;SJcMuqd#@oix2fSNwbL^VFcXUC&4PMtwj#s~ak8|gYMzJ?Im z|2IMXu7NO>3^fi>*N7Os?eYG_uk$(bF)bNPn82oodQdi;&+|j-_JBv+Jz-i;BihcF z%iu^@M}0491fjS1L)j@bB5%N}q#;^+$TwY_wd+bW!#`OF-pVVLMm@NAfi9YTr!1s7 zJ59)Ia4)`@%CX3WdwOf>slExV)|57r7u?OlV_tp&;~E?LDfH(aB+3de{Mc4O6egtF zi(#Wd#2#09mDD*P$*C|o|L)2}om}_Za0T}7Jpp4r!R-U?$7#m?WMXsh7t|IhmRzgM zi)@809=~P!YsB^GC{E==knS!hF{)5+srJRJ?J z!%kxOmT&&_bSz%toY3x$4C_RpSdslsrp|uVM|G1!vd+)GDBs>ZSTa4j$ci8P^2gXM zm^0UJt)pFJAt4CNDclh;`gdSCjg12KBS3%-+`rL3|91#JVT_vPsgI1_P4I6VcNO2qyx&y?dgVnk#|8ZhWParx0vHv zAA(}|QOiPRC)FWKfupbCMr-Vkx{3L^NatWXJ1C7cs#tp#{xxAwpKS z!8w9nX#qCd!@!c&fnkkC_)-Iu^}Tu-UyFvhzE7m)CuTpqv=^uyC%lfK5i+N_n!vsx zIoPq1x$(iywUU$>>_$(prO+G%3Raj|`A-brJ5=^DAv1BB8!{$02(^+%e_QKrZEl+n zqbNG~EoPZ62mM-=F4>NI{V)*jVnuempU#0+n%@+$bZA(i zj!oWag~dsumWEBluh?vyV0=*Z*_&WY4wdbQ^t@J*?Q>-~?UUC~Eu`;@3^48d5vm0> zoMj$MBh?{_RI(1wWovx2P#rIWAHQRsXN^fpv!yQGy?<%t vw+*Gar+3!F$4L28JKYg%oXAx_!QTRYp6A|59miX4|Bk>G>wu}ZCZ_x^V3^&| literal 0 HcmV?d00001