[{"data":1,"prerenderedAt":5398},["ShallowReactive",2],{"writing-\u002Fwriting\u002Fbuilding-my-own-smart-roller-shades":3},{"id":4,"title":5,"body":6,"date":5389,"description":12,"excerpt":5390,"extension":5391,"image":5392,"meta":5393,"navigation":37,"path":5394,"readTime":231,"seo":5395,"stem":5396,"__hash__":5397},"writing\u002Fwriting\u002Fbuilding-my-own-smart-roller-shades.md","Building My Own Smart Roller Shades",{"type":7,"value":8,"toc":5356},"minimark",[9,13,16,29,32,54,64,69,72,94,98,101,104,426,429,444,448,451,458,630,733,742,871,1589,1592,2132,2135,2956,2972,2981,2984,3204,3207,3211,3232,3247,3252,3255,3269,3272,3285,3294,3300,3303,3309,3313,3316,3325,3449,3453,3474,3483,3487,3502,3505,3514,3518,3521,3530,3539,3543,3546,3608,3611,3626,3630,3645,3660,3675,3685,3689,3698,3704,3707,3713,3717,3720,3729,3733,3748,3762,3766,3769,3772,3776,3779,3782,3787,3790,3794,3797,3806,3810,3813,3999,4003,4018,4045,4049,4052,4065,4076,4238,4244,4248,4257,4266,4270,4281,4438,4441,4445,4454,4700,4703,4733,4736,4740,4743,4762,4771,4782,4831,5057,5069,5078,5082,5091,5102,5106,5119,5236,5245,5249,5252,5262,5265,5297,5300,5319,5323,5326,5337,5340,5344,5352],[10,11,12],"p",{},"A few weeks after moving into a new apartment, I started getting annoyed by a minor yet recurring problem: having to raise and lower my shades every morning and night.\nWouldn't it be great if they did that on their own?",[10,14,15],{},"Around the same time, I came across the AXIS Gear.\nFor $250 a pop, it promised to retrofit your existing dumb shades and bring them to the world of IoT.\nOutfitting the two shades in my room would cost $500, and retrofitting the whole apartment would approach $3,000, so going down the DIY route started to make sense.\nIt also sounded much more fun and rewarding.",[17,18,19,20,19,25],"figure",{},"\n  ",[21,22],"img",{"src":23,"alt":24},"\u002Fimages\u002Fwriting\u002Fbuilding-my-own-smart-roller-shades\u002Fwindow.jpg","Floor-to-ceiling roller shades in the apartment window",[26,27,28],"figcaption",{},"My window with the shades",[10,30,31],{},"After several months of designing circuits, 3D-printing mechanical parts, writing firmware, and integrating with Google Assistant, I had a working system.\nI called it Shado.",[17,33,19,34,19,46],{},[35,36,40,41,19],"video",{"controls":37,"playsInline":37,"preload":38,"poster":39},true,"metadata","\u002Fimages\u002Fwriting\u002Fbuilding-my-own-smart-roller-shades\u002Fshado_poster.jpg","\n    ",[42,43],"source",{"src":44,"type":45},"\u002Fimages\u002Fwriting\u002Fbuilding-my-own-smart-roller-shades\u002Fshado.mp4","video\u002Fmp4",[26,47,48,49,53],{},"\"OK Google, open the ",[50,51,52],"s",{},"shades"," blinds.\"",[55,56,57],"blockquote",{},[10,58,59,63],{},[60,61,62],"strong",{},"A note on timing:"," I originally built Shado between 2019 and 2020 and revisited it intermittently afterward. It worked well enough to control the shades, though I never completed the final mechanical revision needed for long-term reliability before moving out of the apartment.",[65,66,68],"h2",{"id":67},"what-i-wanted-to-build","What I wanted to build",[10,70,71],{},"The core requirement was simple: build a contraption to pull on the existing beaded chain of my roller shade without modifying the shade itself.\nFor it to be truly useful, it also needed to:",[73,74,75,79,82,85,88,91],"ul",{},[76,77,78],"li",{},"Lift the largest shade in my room without stalling and in under 30 seconds.",[76,80,81],{},"Include physical controls so anyone could operate it.",[76,83,84],{},"Connect to Wi-Fi without hardcoded credentials.",[76,86,87],{},"Support control from a browser and Google Assistant.",[76,89,90],{},"Fit into a wall-mounted enclosure I could print at home.",[76,92,93],{},"Be adaptable to the other, slightly different shades in the apartment.",[65,95,97],{"id":96},"how-shado-works","How Shado works",[10,99,100],{},"At a high level, each shade is driven by a geared DC motor connected to a custom ESP8266-based controller.\nA 3D-printed drive wheel pulls the shade's existing beaded chain, with the motor and PCB held against the wall by a custom printed enclosure.",[10,102,103],{},"The controller accepts input from two physical buttons or receives a command over Wi-Fi via MQTT.\nA cloud service provides browser-based controls and integrates with Google Assistant, allowing a voice command to travel from Google to the service, down to the device, and ultimately to the motor.",[105,106,111],"pre",{"className":107,"code":108,"language":109,"meta":110,"style":110},"language-mermaid shiki shiki-themes github-dark","---\nconfig:\n  layout: dagre\n  flowchart:\n    curve: linear\n    nodeSpacing: 30\n    rankSpacing: 65\n---\nflowchart TD\n    subgraph UX[\"User Interfaces\"]\n        direction TB\n        google[\"Google Assistant\"]\n        web[\"Web Dashboard\"]\n    end\n\n    subgraph CLOUD[\"AWS Cloud\"]\n        direction LR\n        api[\"API Gateway + Lambda\"]\n        iot[\"AWS IoT Core\u003Cbr\u002F>Device Shadow + MQTT\"]\n\n        api --> iot\n    end\n\n    subgraph CONTROL[\"Shado Controller\"]\n        direction LR\n        buttons[\"Open \u002F Close Buttons\"]\n        esp[\"ESP8266 Controller\"]\n        driver[\"Motor Driver\"]\n\n        buttons --> esp --> driver\n    end\n\n    subgraph MECH[\"Shade Mechanism\"]\n        direction LR\n        motor[\"Geared DC Motor\"]\n        wheel[\"3D-Printed Drive Wheel\"]\n        shades[\"Roller Shades\"]\n\n        motor --> wheel --> shades\n    end\n\n    UX -->|\"HTTPS \u002F webhook\"| CLOUD\n    CLOUD -->|\"MQTT over TLS\"| CONTROL\n    CONTROL --> MECH\n\n    classDef ux fill:#101a25,stroke:#4ea3ff,color:#eef6ff,stroke-width:1.5px;\n    classDef cloud fill:#141322,stroke:#7a6ee6,color:#eae7fa,stroke-width:1.5px;\n    classDef control fill:#10201d,stroke:#39c7a5,color:#edfffb,stroke-width:1.5px;\n    classDef mech fill:#1d1711,stroke:#be9342,color:#f5eedf,stroke-width:1.5px;\n\n    class google,web ux;\n    class api,iot cloud;\n    class buttons,esp,driver control;\n    class motor,wheel,shades mech;\n","mermaid","",[112,113,114,122,128,134,140,146,152,158,163,169,175,181,187,193,199,205,211,217,223,229,234,240,245,250,256,261,267,273,279,284,290,295,300,306,311,317,323,329,334,340,345,350,356,362,368,373,379,385,391,397,402,408,414,420],"code",{"__ignoreMap":110},[115,116,119],"span",{"class":117,"line":118},"line",1,[115,120,121],{},"---\n",[115,123,125],{"class":117,"line":124},2,[115,126,127],{},"config:\n",[115,129,131],{"class":117,"line":130},3,[115,132,133],{},"  layout: dagre\n",[115,135,137],{"class":117,"line":136},4,[115,138,139],{},"  flowchart:\n",[115,141,143],{"class":117,"line":142},5,[115,144,145],{},"    curve: linear\n",[115,147,149],{"class":117,"line":148},6,[115,150,151],{},"    nodeSpacing: 30\n",[115,153,155],{"class":117,"line":154},7,[115,156,157],{},"    rankSpacing: 65\n",[115,159,161],{"class":117,"line":160},8,[115,162,121],{},[115,164,166],{"class":117,"line":165},9,[115,167,168],{},"flowchart TD\n",[115,170,172],{"class":117,"line":171},10,[115,173,174],{},"    subgraph UX[\"User Interfaces\"]\n",[115,176,178],{"class":117,"line":177},11,[115,179,180],{},"        direction TB\n",[115,182,184],{"class":117,"line":183},12,[115,185,186],{},"        google[\"Google Assistant\"]\n",[115,188,190],{"class":117,"line":189},13,[115,191,192],{},"        web[\"Web Dashboard\"]\n",[115,194,196],{"class":117,"line":195},14,[115,197,198],{},"    end\n",[115,200,202],{"class":117,"line":201},15,[115,203,204],{"emptyLinePlaceholder":37},"\n",[115,206,208],{"class":117,"line":207},16,[115,209,210],{},"    subgraph CLOUD[\"AWS Cloud\"]\n",[115,212,214],{"class":117,"line":213},17,[115,215,216],{},"        direction LR\n",[115,218,220],{"class":117,"line":219},18,[115,221,222],{},"        api[\"API Gateway + Lambda\"]\n",[115,224,226],{"class":117,"line":225},19,[115,227,228],{},"        iot[\"AWS IoT Core\u003Cbr\u002F>Device Shadow + MQTT\"]\n",[115,230,232],{"class":117,"line":231},20,[115,233,204],{"emptyLinePlaceholder":37},[115,235,237],{"class":117,"line":236},21,[115,238,239],{},"        api --> iot\n",[115,241,243],{"class":117,"line":242},22,[115,244,198],{},[115,246,248],{"class":117,"line":247},23,[115,249,204],{"emptyLinePlaceholder":37},[115,251,253],{"class":117,"line":252},24,[115,254,255],{},"    subgraph CONTROL[\"Shado Controller\"]\n",[115,257,259],{"class":117,"line":258},25,[115,260,216],{},[115,262,264],{"class":117,"line":263},26,[115,265,266],{},"        buttons[\"Open \u002F Close Buttons\"]\n",[115,268,270],{"class":117,"line":269},27,[115,271,272],{},"        esp[\"ESP8266 Controller\"]\n",[115,274,276],{"class":117,"line":275},28,[115,277,278],{},"        driver[\"Motor Driver\"]\n",[115,280,282],{"class":117,"line":281},29,[115,283,204],{"emptyLinePlaceholder":37},[115,285,287],{"class":117,"line":286},30,[115,288,289],{},"        buttons --> esp --> driver\n",[115,291,293],{"class":117,"line":292},31,[115,294,198],{},[115,296,298],{"class":117,"line":297},32,[115,299,204],{"emptyLinePlaceholder":37},[115,301,303],{"class":117,"line":302},33,[115,304,305],{},"    subgraph MECH[\"Shade Mechanism\"]\n",[115,307,309],{"class":117,"line":308},34,[115,310,216],{},[115,312,314],{"class":117,"line":313},35,[115,315,316],{},"        motor[\"Geared DC Motor\"]\n",[115,318,320],{"class":117,"line":319},36,[115,321,322],{},"        wheel[\"3D-Printed Drive Wheel\"]\n",[115,324,326],{"class":117,"line":325},37,[115,327,328],{},"        shades[\"Roller Shades\"]\n",[115,330,332],{"class":117,"line":331},38,[115,333,204],{"emptyLinePlaceholder":37},[115,335,337],{"class":117,"line":336},39,[115,338,339],{},"        motor --> wheel --> shades\n",[115,341,343],{"class":117,"line":342},40,[115,344,198],{},[115,346,348],{"class":117,"line":347},41,[115,349,204],{"emptyLinePlaceholder":37},[115,351,353],{"class":117,"line":352},42,[115,354,355],{},"    UX -->|\"HTTPS \u002F webhook\"| CLOUD\n",[115,357,359],{"class":117,"line":358},43,[115,360,361],{},"    CLOUD -->|\"MQTT over TLS\"| CONTROL\n",[115,363,365],{"class":117,"line":364},44,[115,366,367],{},"    CONTROL --> MECH\n",[115,369,371],{"class":117,"line":370},45,[115,372,204],{"emptyLinePlaceholder":37},[115,374,376],{"class":117,"line":375},46,[115,377,378],{},"    classDef ux fill:#101a25,stroke:#4ea3ff,color:#eef6ff,stroke-width:1.5px;\n",[115,380,382],{"class":117,"line":381},47,[115,383,384],{},"    classDef cloud fill:#141322,stroke:#7a6ee6,color:#eae7fa,stroke-width:1.5px;\n",[115,386,388],{"class":117,"line":387},48,[115,389,390],{},"    classDef control fill:#10201d,stroke:#39c7a5,color:#edfffb,stroke-width:1.5px;\n",[115,392,394],{"class":117,"line":393},49,[115,395,396],{},"    classDef mech fill:#1d1711,stroke:#be9342,color:#f5eedf,stroke-width:1.5px;\n",[115,398,400],{"class":117,"line":399},50,[115,401,204],{"emptyLinePlaceholder":37},[115,403,405],{"class":117,"line":404},51,[115,406,407],{},"    class google,web ux;\n",[115,409,411],{"class":117,"line":410},52,[115,412,413],{},"    class api,iot cloud;\n",[115,415,417],{"class":117,"line":416},53,[115,418,419],{},"    class buttons,esp,driver control;\n",[115,421,423],{"class":117,"line":422},54,[115,424,425],{},"    class motor,wheel,shades mech;\n",[10,427,428],{},"The project thus broke down into four major pieces:",[430,431,432,435,438,441],"ol",{},[76,433,434],{},"A motor and drive-wheel assembly strong enough to move the shade.",[76,436,437],{},"A custom circuit board to control it.",[76,439,440],{},"Firmware for motor control, Wi-Fi provisioning, and communication.",[76,442,443],{},"A web service that controls the device and connects it to Google.",[65,445,447],{"id":446},"choosing-the-motor","Choosing the motor",[10,449,450],{},"Choosing the right motor required estimating both the force needed to move the shades and the desired speed.\nA rough estimate would be good enough — I did not have a spring scale, so I tied a pair of cheap earbuds around the chain and hung plates from some dumbbells I had lying around.",[17,452,19,454],{"style":453},"max-width: 75%; margin: 24px auto;",[21,455],{"src":456,"alt":457},"\u002Fimages\u002Fwriting\u002Fbuilding-my-own-smart-roller-shades\u002Fweights.webp","Testing the resistance needed",[10,459,460,461,533,534,586,587,629],{},"By attaching progressively heavier weights, I could get a rough sense of the force needed to open the shade.\nEach plate weighed in at around ",[115,462,465,502],{"className":463},[464],"katex",[115,466,469],{"className":467},[468],"katex-mathml",[470,471,473],"math",{"xmlns":472},"http:\u002F\u002Fwww.w3.org\u002F1998\u002FMath\u002FMathML",[474,475,476,497],"semantics",{},[477,478,479,483,487],"mrow",{},[480,481,482],"mn",{},"2.5",[484,485,486],"mtext",{}," ",[477,488,489,494],{},[490,491,493],"mi",{"mathvariant":492},"normal","l",[490,495,496],{"mathvariant":492},"b",[498,499,501],"annotation",{"encoding":500},"application\u002Fx-tex","\\qty{2.5}{lb}",[115,503,507],{"className":504,"ariaHidden":506},[505],"katex-html","true",[115,508,511,516,520,525],{"className":509},[510],"base",[115,512],{"className":513,"style":515},[514],"strut","height:0.6944em;",[115,517,482],{"className":518},[519],"mord",[115,521],{"className":522,"style":524},[523],"mspace","margin-right:0.1667em;",[115,526,528],{"className":527},[519],[115,529,532],{"className":530},[519,531],"mathrm","lb",", and it turned out that I needed 1–2 of them to start lifting the smaller shade, and 2–3 for the larger one.\nConverting to metric, the heaviest case was equivalent to the weight of roughly ",[115,535,537,562],{"className":536},[464],[115,538,540],{"className":539},[468],[470,541,542],{"xmlns":472},[474,543,544,559],{},[477,545,546,549,551],{},[480,547,548],{},"3.5",[484,550,486],{},[477,552,553,556],{},[490,554,555],{"mathvariant":492},"k",[490,557,558],{"mathvariant":492},"g",[498,560,561],{"encoding":500},"\\qty{3.5}{\\kilogram}",[115,563,565],{"className":564,"ariaHidden":506},[505],[115,566,568,572,575,578],{"className":567},[510],[115,569],{"className":570,"style":571},[514],"height:0.8889em;vertical-align:-0.1944em;",[115,573,548],{"className":574},[519],[115,576],{"className":577,"style":524},[523],[115,579,581],{"className":580},[519],[115,582,585],{"className":583,"style":584},[519,531],"margin-right:0.0139em;","kg",", or about ",[115,588,590,610],{"className":589},[464],[115,591,593],{"className":592},[468],[470,594,595],{"xmlns":472},[474,596,597,607],{},[477,598,599,602,604],{},[480,600,601],{},"34",[484,603,486],{},[490,605,606],{"mathvariant":492},"N",[498,608,609],{"encoding":500},"\\qty{34}{\\newton}",[115,611,613],{"className":612,"ariaHidden":506},[505],[115,614,616,620,623,626],{"className":615},[510],[115,617],{"className":618,"style":619},[514],"height:0.6833em;",[115,621,601],{"className":622},[519],[115,624],{"className":625,"style":524},[523],[115,627,606],{"className":628},[519,531],". I treated that as the practical lower bound for choosing the motor.",[10,631,632,633,684,685,732],{},"For the drive mechanism, I estimated an effective wheel diameter of roughly ",[115,634,636,661],{"className":635},[464],[115,637,639],{"className":638},[468],[470,640,641],{"xmlns":472},[474,642,643,658],{},[477,644,645,648,650],{},[480,646,647],{},"3",[484,649,486],{},[477,651,652,655],{},[490,653,654],{"mathvariant":492},"c",[490,656,657],{"mathvariant":492},"m",[498,659,660],{"encoding":500},"\\qty{3}{\\centimeter}",[115,662,664],{"className":663,"ariaHidden":506},[505],[115,665,667,671,674,677],{"className":666},[510],[115,668],{"className":669,"style":670},[514],"height:0.6444em;",[115,672,647],{"className":673},[519],[115,675],{"className":676,"style":524},[523],[115,678,680],{"className":679},[519],[115,681,683],{"className":682},[519,531],"cm"," and about ",[115,686,688,711],{"className":687},[464],[115,689,691],{"className":690},[468],[470,692,693],{"xmlns":472},[474,694,695,708],{},[477,696,697,700,702],{},[480,698,699],{},"250",[484,701,486],{},[477,703,704,706],{},[490,705,654],{"mathvariant":492},[490,707,657],{"mathvariant":492},[498,709,710],{"encoding":500},"\\qty{250}{\\centimeter}",[115,712,714],{"className":713,"ariaHidden":506},[505],[115,715,717,720,723,726],{"className":716},[510],[115,718],{"className":719,"style":670},[514],[115,721,699],{"className":722},[519],[115,724],{"className":725,"style":524},[523],[115,727,729],{"className":728},[519],[115,730,683],{"className":731},[519,531]," of chain travel from fully closed to fully open.",[17,734,19,735,19,739],{"style":453},[21,736],{"src":737,"alt":738},"\u002Fimages\u002Fwriting\u002Fbuilding-my-own-smart-roller-shades\u002Fbeads.jpg","Beaded shade chain wrapped around a contact-lens cap for scale",[26,740,741],{},"Contact lens cap for scale.",[10,743,744,745,775,776,841,842,870],{},"While half a minute to pull the shades up was acceptable, I wanted to set a more aggressive design target of 15 seconds.\nGiven a drive wheel of diameter ",[115,746,748,762],{"className":747},[464],[115,749,751],{"className":750},[468],[470,752,753],{"xmlns":472},[474,754,755,760],{},[477,756,757],{},[490,758,759],{},"d",[498,761,759],{"encoding":500},[115,763,765],{"className":764,"ariaHidden":506},[505],[115,766,768,771],{"className":767},[510],[115,769],{"className":770,"style":515},[514],[115,772,759],{"className":773},[519,774],"mathnormal",", the motor must complete ",[115,777,779,809],{"className":778},[464],[115,780,782],{"className":781},[468],[470,783,784],{"xmlns":472},[474,785,786,806],{},[477,787,788,790,793,798,801,803],{},[490,789,493],{},[490,791,792],{"mathvariant":492},"\u002F",[794,795,797],"mo",{"stretchy":796},"false","(",[490,799,800],{},"π",[490,802,759],{},[794,804,805],{"stretchy":796},")",[498,807,808],{"encoding":500},"l\u002F(\\pi d)",[115,810,812],{"className":811,"ariaHidden":506},[505],[115,813,815,819,823,826,830,834,837],{"className":814},[510],[115,816],{"className":817,"style":818},[514],"height:1em;vertical-align:-0.25em;",[115,820,493],{"className":821,"style":822},[519,774],"margin-right:0.0197em;",[115,824,792],{"className":825},[519],[115,827,797],{"className":828},[829],"mopen",[115,831,800],{"className":832,"style":833},[519,774],"margin-right:0.0359em;",[115,835,759],{"className":836},[519,774],[115,838,805],{"className":839},[840],"mclose"," revolutions to pull ",[115,843,845,858],{"className":844},[464],[115,846,848],{"className":847},[468],[470,849,850],{"xmlns":472},[474,851,852,856],{},[477,853,854],{},[490,855,493],{},[498,857,493],{"encoding":500},[115,859,861],{"className":860,"ariaHidden":506},[505],[115,862,864,867],{"className":863},[510],[115,865],{"className":866,"style":515},[514],[115,868,493],{"className":869,"style":822},[519,774]," centimeters of chain. Therefore, its required speed is:",[115,872,875],{"className":873},[874],"katex-display",[115,876,878,1037],{"className":877},[464],[115,879,881],{"className":880},[468],[470,882,884],{"xmlns":472,"display":883},"block",[474,885,886,1034],{},[477,887,888,903,906,920,923,943,945,984,986,1004,1007],{},[889,890,891,894],"msub",{},[490,892,893],{},"n",[477,895,896,899,901],{},[490,897,898],{"mathvariant":492},"r",[490,900,10],{"mathvariant":492},[490,902,657],{"mathvariant":492},[794,904,905],{},"=",[907,908,909,911],"mfrac",{},[490,910,493],{},[477,912,913,915,917],{},[490,914,800],{},[490,916,759],{},[490,918,919],{},"t",[794,921,922],{},"⋅",[907,924,925,934],{},[477,926,927,930,932],{},[480,928,929],{},"60",[484,931,486],{},[490,933,50],{"mathvariant":492},[477,935,936,938,941],{},[490,937,657],{"mathvariant":492},[490,939,940],{"mathvariant":492},"i",[490,942,893],{"mathvariant":492},[794,944,905],{},[907,946,947,959],{},[477,948,949,951,953],{},[480,950,699],{},[484,952,486],{},[477,954,955,957],{},[490,956,654],{"mathvariant":492},[490,958,657],{"mathvariant":492},[477,960,961,963,965,967,969,975,977,980,982],{},[490,962,800],{},[794,964,922],{},[480,966,647],{},[484,968,486],{},[477,970,971,973],{},[490,972,654],{"mathvariant":492},[490,974,657],{"mathvariant":492},[794,976,922],{},[480,978,979],{},"15",[484,981,486],{},[490,983,50],{"mathvariant":492},[794,985,922],{},[907,987,988,996],{},[477,989,990,992,994],{},[480,991,929],{},[484,993,486],{},[490,995,50],{"mathvariant":492},[477,997,998,1000,1002],{},[490,999,657],{"mathvariant":492},[490,1001,940],{"mathvariant":492},[490,1003,893],{"mathvariant":492},[794,1005,1006],{},"≈",[1008,1009,1011],"menclose",{"notation":1010},"box",[1012,1013,1015],"mstyle",{"scriptlevel":1014,"displaystyle":796},"0",[1012,1016,1017],{"scriptlevel":1014,"displaystyle":796},[1012,1018,1019],{"scriptlevel":1014,"displaystyle":506},[477,1020,1021,1024,1026],{},[480,1022,1023],{},"106",[484,1025,486],{},[477,1027,1028,1030,1032],{},[490,1029,898],{"mathvariant":492},[490,1031,10],{"mathvariant":492},[490,1033,657],{"mathvariant":492},[498,1035,1036],{"encoding":500},"n_{\\mathrm{rpm}}\n= \\frac{l}{\\pi d t} \\cdot \\frac{\\qty{60}{\\s}}{\\unit{\\min}}\n= \\frac{\\qty{250}{\\cm}}{\\pi \\cdot \\qty{3}{\\cm} \\cdot \\qty{15}{\\s}} \\cdot \\frac{\\qty{60}{\\s}}{\\unit{\\min}}\n\\approx \\boxed{\\qty{106}{rpm}}",[115,1038,1040,1121,1216,1305,1430,1516],{"className":1039,"ariaHidden":506},[505],[115,1041,1043,1047,1110,1114,1118],{"className":1042},[510],[115,1044],{"className":1045,"style":1046},[514],"height:0.7167em;vertical-align:-0.2861em;",[115,1048,1050,1053],{"className":1049},[519],[115,1051,893],{"className":1052},[519,774],[115,1054,1057],{"className":1055},[1056],"msupsub",[115,1058,1062,1101],{"className":1059},[1060,1061],"vlist-t","vlist-t2",[115,1063,1066,1096],{"className":1064},[1065],"vlist-r",[115,1067,1071],{"className":1068,"style":1070},[1069],"vlist","height:0.1514em;",[115,1072,1074,1079],{"style":1073},"top:-2.55em;margin-left:0em;margin-right:0.05em;",[115,1075],{"className":1076,"style":1078},[1077],"pstrut","height:2.7em;",[115,1080,1086],{"className":1081},[1082,1083,1084,1085],"sizing","reset-size6","size3","mtight",[115,1087,1089],{"className":1088},[519,1085],[115,1090,1092],{"className":1091},[519,1085],[115,1093,1095],{"className":1094},[519,531,1085],"rpm",[115,1097,1100],{"className":1098},[1099],"vlist-s","​",[115,1102,1104],{"className":1103},[1065],[115,1105,1108],{"className":1106,"style":1107},[1069],"height:0.2861em;",[115,1109],{},[115,1111],{"className":1112,"style":1113},[523],"margin-right:0.2778em;",[115,1115,905],{"className":1116},[1117],"mrel",[115,1119],{"className":1120,"style":1113},[523],[115,1122,1124,1128,1205,1209,1213],{"className":1123},[510],[115,1125],{"className":1126,"style":1127},[514],"height:2.0574em;vertical-align:-0.686em;",[115,1129,1131,1135,1202],{"className":1130},[519],[115,1132],{"className":1133},[829,1134],"nulldelimiter",[115,1136,1138],{"className":1137},[907],[115,1139,1141,1193],{"className":1140},[1060,1061],[115,1142,1144,1190],{"className":1143},[1065],[115,1145,1148,1167,1178],{"className":1146,"style":1147},[1069],"height:1.3714em;",[115,1149,1151,1155],{"style":1150},"top:-2.314em;",[115,1152],{"className":1153,"style":1154},[1077],"height:3em;",[115,1156,1158,1161,1164],{"className":1157},[519],[115,1159,800],{"className":1160,"style":833},[519,774],[115,1162,759],{"className":1163},[519,774],[115,1165,919],{"className":1166},[519,774],[115,1168,1170,1173],{"style":1169},"top:-3.23em;",[115,1171],{"className":1172,"style":1154},[1077],[115,1174],{"className":1175,"style":1177},[1176],"frac-line","border-bottom-width:0.04em;",[115,1179,1181,1184],{"style":1180},"top:-3.677em;",[115,1182],{"className":1183,"style":1154},[1077],[115,1185,1187],{"className":1186},[519],[115,1188,493],{"className":1189,"style":822},[519,774],[115,1191,1100],{"className":1192},[1099],[115,1194,1196],{"className":1195},[1065],[115,1197,1200],{"className":1198,"style":1199},[1069],"height:0.686em;",[115,1201],{},[115,1203],{"className":1204},[840,1134],[115,1206],{"className":1207,"style":1208},[523],"margin-right:0.2222em;",[115,1210,922],{"className":1211},[1212],"mbin",[115,1214],{"className":1215,"style":1208},[523],[115,1217,1219,1223,1296,1299,1302],{"className":1218},[510],[115,1220],{"className":1221,"style":1222},[514],"height:2.0074em;vertical-align:-0.686em;",[115,1224,1226,1229,1293],{"className":1225},[519],[115,1227],{"className":1228},[829,1134],[115,1230,1232],{"className":1231},[907],[115,1233,1235,1285],{"className":1234},[1060,1061],[115,1236,1238,1282],{"className":1237},[1065],[115,1239,1242,1257,1265],{"className":1240,"style":1241},[1069],"height:1.3214em;",[115,1243,1244,1247],{"style":1150},[115,1245],{"className":1246,"style":1154},[1077],[115,1248,1250],{"className":1249},[519],[115,1251,1253],{"className":1252},[519],[115,1254,1256],{"className":1255},[519,531],"min",[115,1258,1259,1262],{"style":1169},[115,1260],{"className":1261,"style":1154},[1077],[115,1263],{"className":1264,"style":1177},[1176],[115,1266,1267,1270],{"style":1180},[115,1268],{"className":1269,"style":1154},[1077],[115,1271,1273,1276,1279],{"className":1272},[519],[115,1274,929],{"className":1275},[519],[115,1277],{"className":1278,"style":524},[523],[115,1280,50],{"className":1281},[519,531],[115,1283,1100],{"className":1284},[1099],[115,1286,1288],{"className":1287},[1065],[115,1289,1291],{"className":1290,"style":1199},[1069],[115,1292],{},[115,1294],{"className":1295},[840,1134],[115,1297],{"className":1298,"style":1113},[523],[115,1300,905],{"className":1301},[1117],[115,1303],{"className":1304,"style":1113},[523],[115,1306,1308,1311,1421,1424,1427],{"className":1307},[510],[115,1309],{"className":1310,"style":1222},[514],[115,1312,1314,1317,1418],{"className":1313},[519],[115,1315],{"className":1316},[829,1134],[115,1318,1320],{"className":1319},[907],[115,1321,1323,1410],{"className":1322},[1060,1061],[115,1324,1326,1407],{"className":1325},[1065],[115,1327,1329,1379,1387],{"className":1328,"style":1241},[1069],[115,1330,1331,1334],{"style":1150},[115,1332],{"className":1333,"style":1154},[1077],[115,1335,1337,1340,1343,1346,1349,1352,1355,1361,1364,1367,1370,1373,1376],{"className":1336},[519],[115,1338,800],{"className":1339,"style":833},[519,774],[115,1341],{"className":1342,"style":1208},[523],[115,1344,922],{"className":1345},[1212],[115,1347],{"className":1348,"style":1208},[523],[115,1350,647],{"className":1351},[519],[115,1353],{"className":1354,"style":524},[523],[115,1356,1358],{"className":1357},[519],[115,1359,683],{"className":1360},[519,531],[115,1362],{"className":1363,"style":1208},[523],[115,1365,922],{"className":1366},[1212],[115,1368],{"className":1369,"style":1208},[523],[115,1371,979],{"className":1372},[519],[115,1374],{"className":1375,"style":524},[523],[115,1377,50],{"className":1378},[519,531],[115,1380,1381,1384],{"style":1169},[115,1382],{"className":1383,"style":1154},[1077],[115,1385],{"className":1386,"style":1177},[1176],[115,1388,1389,1392],{"style":1180},[115,1390],{"className":1391,"style":1154},[1077],[115,1393,1395,1398,1401],{"className":1394},[519],[115,1396,699],{"className":1397},[519],[115,1399],{"className":1400,"style":524},[523],[115,1402,1404],{"className":1403},[519],[115,1405,683],{"className":1406},[519,531],[115,1408,1100],{"className":1409},[1099],[115,1411,1413],{"className":1412},[1065],[115,1414,1416],{"className":1415,"style":1199},[1069],[115,1417],{},[115,1419],{"className":1420},[840,1134],[115,1422],{"className":1423,"style":1208},[523],[115,1425,922],{"className":1426},[1212],[115,1428],{"className":1429,"style":1208},[523],[115,1431,1433,1436,1507,1510,1513],{"className":1432},[510],[115,1434],{"className":1435,"style":1222},[514],[115,1437,1439,1442,1504],{"className":1438},[519],[115,1440],{"className":1441},[829,1134],[115,1443,1445],{"className":1444},[907],[115,1446,1448,1496],{"className":1447},[1060,1061],[115,1449,1451,1493],{"className":1450},[1065],[115,1452,1454,1468,1476],{"className":1453,"style":1241},[1069],[115,1455,1456,1459],{"style":1150},[115,1457],{"className":1458,"style":1154},[1077],[115,1460,1462],{"className":1461},[519],[115,1463,1465],{"className":1464},[519],[115,1466,1256],{"className":1467},[519,531],[115,1469,1470,1473],{"style":1169},[115,1471],{"className":1472,"style":1154},[1077],[115,1474],{"className":1475,"style":1177},[1176],[115,1477,1478,1481],{"style":1180},[115,1479],{"className":1480,"style":1154},[1077],[115,1482,1484,1487,1490],{"className":1483},[519],[115,1485,929],{"className":1486},[519],[115,1488],{"className":1489,"style":524},[523],[115,1491,50],{"className":1492},[519,531],[115,1494,1100],{"className":1495},[1099],[115,1497,1499],{"className":1498},[1065],[115,1500,1502],{"className":1501,"style":1199},[1069],[115,1503],{},[115,1505],{"className":1506},[840,1134],[115,1508],{"className":1509,"style":1113},[523],[115,1511,1006],{"className":1512},[1117],[115,1514],{"className":1515,"style":1113},[523],[115,1517,1519,1523],{"className":1518},[510],[115,1520],{"className":1521,"style":1522},[514],"height:1.5189em;vertical-align:-0.5344em;",[115,1524,1526],{"className":1525},[519],[115,1527,1529,1580],{"className":1528},[1060,1061],[115,1530,1532,1577],{"className":1531},[1065],[115,1533,1536,1565],{"className":1534,"style":1535},[1069],"height:0.9844em;",[115,1537,1539,1543],{"style":1538},"top:-3.5189em;",[115,1540],{"className":1541,"style":1542},[1077],"height:3.5189em;",[115,1544,1547],{"className":1545},[1546],"boxpad",[115,1548,1550],{"className":1549},[519],[115,1551,1553,1556,1559],{"className":1552},[519],[115,1554,1023],{"className":1555},[519],[115,1557],{"className":1558,"style":524},[523],[115,1560,1562],{"className":1561},[519],[115,1563,1095],{"className":1564},[519,531],[115,1566,1568,1571],{"style":1567},"top:-2.9844em;",[115,1569],{"className":1570,"style":1542},[1077],[115,1572],{"className":1573,"style":1576},[1574,1575],"stretchy","fbox","height:1.5189em;border-style:solid;border-width:0.04em;",[115,1578,1100],{"className":1579},[1099],[115,1581,1583],{"className":1582},[1065],[115,1584,1587],{"className":1585,"style":1586},[1069],"height:0.5344em;",[115,1588],{},[10,1590,1591],{},"As for the torque:",[115,1593,1595],{"className":1594},[874],[115,1596,1598,1767],{"className":1597},[464],[115,1599,1601],{"className":1600},[468],[470,1602,1603],{"xmlns":472,"display":883},[474,1604,1605,1764],{},[1606,1607,1611,1645,1703],"mtable",{"rowspacing":1608,"columnalign":1609,"columnspacing":1610},"0.25em","right left","0em",[1612,1613,1614,1622],"mtr",{},[1615,1616,1617],"mtd",{},[1012,1618,1619],{"scriptlevel":1014,"displaystyle":506},[490,1620,1621],{},"τ",[1615,1623,1624],{},[1012,1625,1626],{"scriptlevel":1014,"displaystyle":506},[477,1627,1628,1630,1632,1635,1637,1639,1641,1643],{},[477,1629],{},[794,1631,905],{},[490,1633,1634],{},"F",[490,1636,898],{},[794,1638,905],{},[490,1640,657],{},[490,1642,558],{},[490,1644,898],{},[1612,1646,1647,1653],{},[1615,1648,1649],{},[1012,1650,1651],{"scriptlevel":1014,"displaystyle":506},[477,1652],{},[1615,1654,1655],{},[1012,1656,1657],{"scriptlevel":1014,"displaystyle":506},[477,1658,1659,1661,1663,1665,1667,1673,1675,1678,1680,1694,1696,1699,1701],{},[477,1660],{},[794,1662,905],{},[480,1664,548],{},[484,1666,486],{},[477,1668,1669,1671],{},[490,1670,555],{"mathvariant":492},[490,1672,558],{"mathvariant":492},[794,1674,922],{},[480,1676,1677],{},"9.80665",[484,1679,486],{},[477,1681,1682,1684,1686],{},[490,1683,657],{"mathvariant":492},[490,1685,792],{"mathvariant":492},[1687,1688,1689,1691],"msup",{},[490,1690,50],{"mathvariant":492},[480,1692,1693],{},"2",[794,1695,922],{},[480,1697,1698],{},"0.015",[484,1700,486],{},[490,1702,657],{"mathvariant":492},[1612,1704,1705,1711],{},[1615,1706,1707],{},[1012,1708,1709],{"scriptlevel":1014,"displaystyle":506},[477,1710],{},[1615,1712,1713],{},[1012,1714,1715],{"scriptlevel":1014,"displaystyle":506},[477,1716,1717,1719,1721,1724,1726,1732,1734],{},[477,1718],{},[794,1720,1006],{},[480,1722,1723],{},"0.515",[484,1725,486],{},[477,1727,1728,1730],{},[490,1729,606],{"mathvariant":492},[490,1731,657],{"mathvariant":492},[794,1733,1006],{},[1008,1735,1736],{"notation":1010},[1012,1737,1738],{"scriptlevel":1014,"displaystyle":796},[1012,1739,1740],{"scriptlevel":1014,"displaystyle":796},[1012,1741,1742],{"scriptlevel":1014,"displaystyle":506},[477,1743,1744,1747,1749],{},[480,1745,1746],{},"5.25",[484,1748,486],{},[477,1750,1751,1753,1755,1758,1760,1762],{},[490,1752,555],{"mathvariant":492},[490,1754,558],{"mathvariant":492},[490,1756,1757],{"mathvariant":492},"f",[794,1759,922],{},[490,1761,654],{"mathvariant":492},[490,1763,657],{"mathvariant":492},[498,1765,1766],{"encoding":500},"\\begin{aligned}\n\\tau\n  &= Fr = mgr \\\\\n  &= \\qty{3.5}{\\kg} \\cdot \\qty{9.80665}{\\m\u002F\\s^2} \\cdot \\qty{0.015}{\\m} \\\\\n  &\\approx \\qty{0.515}{\\newton\\meter}\n  \\approx \\boxed{\\qty{5.25}{kgf\\cdot cm}}\n\\end{aligned}",[115,1768,1770],{"className":1769,"ariaHidden":506},[505],[115,1771,1773,1777],{"className":1772},[510],[115,1774],{"className":1775,"style":1776},[514],"height:4.593em;vertical-align:-2.0465em;",[115,1778,1780],{"className":1779},[519],[115,1781,1783,1841],{"className":1782},[1606],[115,1784,1787],{"className":1785},[1786],"col-align-r",[115,1788,1790,1832],{"className":1789},[1060,1061],[115,1791,1793,1829],{"className":1792},[1065],[115,1794,1797,1811,1820],{"className":1795,"style":1796},[1069],"height:2.5465em;",[115,1798,1800,1804],{"style":1799},"top:-4.7409em;",[115,1801],{"className":1802,"style":1803},[1077],"height:3.0344em;",[115,1805,1807],{"className":1806},[519],[115,1808,1621],{"className":1809,"style":1810},[519,774],"margin-right:0.1132em;",[115,1812,1814,1817],{"style":1813},"top:-3.2168em;",[115,1815],{"className":1816,"style":1803},[1077],[115,1818],{"className":1819},[519],[115,1821,1823,1826],{"style":1822},"top:-1.5224em;",[115,1824],{"className":1825,"style":1803},[1077],[115,1827],{"className":1828},[519],[115,1830,1100],{"className":1831},[1099],[115,1833,1835],{"className":1834},[1065],[115,1836,1839],{"className":1837,"style":1838},[1069],"height:2.0465em;",[115,1840],{},[115,1842,1845],{"className":1843},[1844],"col-align-l",[115,1846,1848,2124],{"className":1847},[1060,1061],[115,1849,1851,2121],{"className":1850},[1065],[115,1852,1854,1900,2003],{"className":1853,"style":1796},[1069],[115,1855,1856,1859],{"style":1799},[115,1857],{"className":1858,"style":1803},[1077],[115,1860,1862,1865,1868,1871,1874,1878,1882,1885,1888,1891,1894,1897],{"className":1861},[519],[115,1863],{"className":1864},[519],[115,1866],{"className":1867,"style":1113},[523],[115,1869,905],{"className":1870},[1117],[115,1872],{"className":1873,"style":1113},[523],[115,1875,1634],{"className":1876,"style":1877},[519,774],"margin-right:0.1389em;",[115,1879,898],{"className":1880,"style":1881},[519,774],"margin-right:0.0278em;",[115,1883],{"className":1884,"style":1113},[523],[115,1886,905],{"className":1887},[1117],[115,1889],{"className":1890,"style":1113},[523],[115,1892,657],{"className":1893},[519,774],[115,1895,558],{"className":1896,"style":833},[519,774],[115,1898,898],{"className":1899,"style":1881},[519,774],[115,1901,1902,1905],{"style":1813},[115,1903],{"className":1904,"style":1803},[1077],[115,1906,1908,1911,1914,1917,1920,1923,1926,1932,1935,1938,1941,1944,1947,1985,1988,1991,1994,1997,2000],{"className":1907},[519],[115,1909],{"className":1910},[519],[115,1912],{"className":1913,"style":1113},[523],[115,1915,905],{"className":1916},[1117],[115,1918],{"className":1919,"style":1113},[523],[115,1921,548],{"className":1922},[519],[115,1924],{"className":1925,"style":524},[523],[115,1927,1929],{"className":1928},[519],[115,1930,585],{"className":1931,"style":584},[519,531],[115,1933],{"className":1934,"style":1208},[523],[115,1936,922],{"className":1937},[1212],[115,1939],{"className":1940,"style":1208},[523],[115,1942,1677],{"className":1943},[519],[115,1945],{"className":1946,"style":524},[523],[115,1948,1950,1954],{"className":1949},[519],[115,1951,1953],{"className":1952},[519,531],"m\u002F",[115,1955,1957,1960],{"className":1956},[519],[115,1958,50],{"className":1959},[519,531],[115,1961,1963],{"className":1962},[1056],[115,1964,1966],{"className":1965},[1060],[115,1967,1969],{"className":1968},[1065],[115,1970,1973],{"className":1971,"style":1972},[1069],"height:0.8641em;",[115,1974,1976,1979],{"style":1975},"top:-3.113em;margin-right:0.05em;",[115,1977],{"className":1978,"style":1078},[1077],[115,1980,1982],{"className":1981},[1082,1083,1084,1085],[115,1983,1693],{"className":1984},[519,531,1085],[115,1986],{"className":1987,"style":1208},[523],[115,1989,922],{"className":1990},[1212],[115,1992],{"className":1993,"style":1208},[523],[115,1995,1698],{"className":1996},[519],[115,1998],{"className":1999,"style":524},[523],[115,2001,657],{"className":2002},[519,531],[115,2004,2005,2008],{"style":1822},[115,2006],{"className":2007,"style":1803},[1077],[115,2009,2011,2014,2017,2020,2023,2026,2029,2036,2039,2042,2045],{"className":2010},[519],[115,2012],{"className":2013},[519],[115,2015],{"className":2016,"style":1113},[523],[115,2018,1006],{"className":2019},[1117],[115,2021],{"className":2022,"style":1113},[523],[115,2024,1723],{"className":2025},[519],[115,2027],{"className":2028,"style":524},[523],[115,2030,2032],{"className":2031},[519],[115,2033,2035],{"className":2034},[519,531],"Nm",[115,2037],{"className":2038,"style":1113},[523],[115,2040,1006],{"className":2041},[1117],[115,2043],{"className":2044,"style":1113},[523],[115,2046,2048],{"className":2047},[519],[115,2049,2051,2113],{"className":2050},[1060,1061],[115,2052,2054,2110],{"className":2053},[1065],[115,2055,2058,2100],{"className":2056,"style":2057},[1069],"height:1.0344em;",[115,2059,2061,2065],{"style":2060},"top:-3.5689em;",[115,2062],{"className":2063,"style":2064},[1077],"height:3.5689em;",[115,2066,2068],{"className":2067},[1546],[115,2069,2071],{"className":2070},[519],[115,2072,2074,2077,2080],{"className":2073},[519],[115,2075,1746],{"className":2076},[519],[115,2078],{"className":2079,"style":524},[523],[115,2081,2083,2088,2091,2094,2097],{"className":2082},[519],[115,2084,2087],{"className":2085,"style":2086},[519,531],"margin-right:0.0778em;","kgf",[115,2089],{"className":2090,"style":1208},[523],[115,2092,922],{"className":2093},[1212],[115,2095],{"className":2096,"style":1208},[523],[115,2098,683],{"className":2099},[519,531],[115,2101,2103,2106],{"style":2102},"top:-3.0344em;",[115,2104],{"className":2105,"style":2064},[1077],[115,2107],{"className":2108,"style":2109},[1574,1575],"height:1.5689em;border-style:solid;border-width:0.04em;",[115,2111,1100],{"className":2112},[1099],[115,2114,2116],{"className":2115},[1065],[115,2117,2119],{"className":2118,"style":1586},[1069],[115,2120],{},[115,2122,1100],{"className":2123},[1099],[115,2125,2127],{"className":2126},[1065],[115,2128,2130],{"className":2129,"style":1838},[1069],[115,2131],{},[10,2133,2134],{},"Plugging that in, I could compute the required mechanical output power to be:",[115,2136,2138],{"className":2137},[874],[115,2139,2141,2359],{"className":2140},[464],[115,2142,2144],{"className":2143},[468],[470,2145,2146],{"xmlns":472,"display":883},[474,2147,2148,2356],{},[1606,2149,2150,2174,2232,2320],{"rowspacing":1608,"columnalign":1609,"columnspacing":1610},[1612,2151,2152,2159],{},[1615,2153,2154],{},[1012,2155,2156],{"scriptlevel":1014,"displaystyle":506},[490,2157,2158],{},"P",[1615,2160,2161],{},[1012,2162,2163],{"scriptlevel":1014,"displaystyle":506},[477,2164,2165,2167,2169,2171],{},[477,2166],{},[794,2168,905],{},[490,2170,1621],{},[490,2172,2173],{},"ω",[1612,2175,2176,2182],{},[1615,2177,2178],{},[1012,2179,2180],{"scriptlevel":1014,"displaystyle":506},[477,2181],{},[1615,2183,2184],{},[1012,2185,2186],{"scriptlevel":1014,"displaystyle":506},[477,2187,2188,2190,2192,2194,2196,2198,2200,2202],{},[477,2189],{},[794,2191,905],{},[794,2193,797],{"stretchy":796},[490,2195,657],{},[490,2197,558],{},[490,2199,898],{},[794,2201,805],{"stretchy":796},[477,2203,2204,2206,2218,2220,2230],{},[794,2205,797],{"fence":506},[889,2207,2208,2210],{},[490,2209,893],{},[477,2211,2212,2214,2216],{},[490,2213,898],{"mathvariant":492},[490,2215,10],{"mathvariant":492},[490,2217,657],{"mathvariant":492},[794,2219,922],{},[907,2221,2222,2228],{},[477,2223,2224,2226],{},[480,2225,1693],{},[490,2227,800],{},[480,2229,929],{},[794,2231,805],{"fence":506},[1612,2233,2234,2240],{},[1615,2235,2236],{},[1012,2237,2238],{"scriptlevel":1014,"displaystyle":506},[477,2239],{},[1615,2241,2242],{},[1012,2243,2244],{"scriptlevel":1014,"displaystyle":506},[477,2245,2246,2248,2250,2252,2254,2256,2262,2264,2266,2268,2280,2282,2284,2286,2288,2290],{},[477,2247],{},[794,2249,905],{},[794,2251,797],{"stretchy":796},[480,2253,548],{},[484,2255,486],{},[477,2257,2258,2260],{},[490,2259,555],{"mathvariant":492},[490,2261,558],{"mathvariant":492},[794,2263,922],{},[480,2265,1677],{},[484,2267,486],{},[477,2269,2270,2272,2274],{},[490,2271,657],{"mathvariant":492},[490,2273,792],{"mathvariant":492},[1687,2275,2276,2278],{},[490,2277,50],{"mathvariant":492},[480,2279,1693],{},[794,2281,922],{},[480,2283,1698],{},[484,2285,486],{},[490,2287,657],{"mathvariant":492},[794,2289,805],{"stretchy":796},[477,2291,2292,2294,2296,2298,2306,2308,2318],{},[794,2293,797],{"fence":506},[480,2295,1023],{},[484,2297,486],{},[477,2299,2300,2302,2304],{},[490,2301,898],{"mathvariant":492},[490,2303,10],{"mathvariant":492},[490,2305,657],{"mathvariant":492},[794,2307,922],{},[907,2309,2310,2316],{},[477,2311,2312,2314],{},[480,2313,1693],{},[490,2315,800],{},[480,2317,929],{},[794,2319,805],{"fence":506},[1612,2321,2322,2328],{},[1615,2323,2324],{},[1012,2325,2326],{"scriptlevel":1014,"displaystyle":506},[477,2327],{},[1615,2329,2330],{},[1012,2331,2332],{"scriptlevel":1014,"displaystyle":506},[477,2333,2334,2336,2338],{},[477,2335],{},[794,2337,1006],{},[1008,2339,2340],{"notation":1010},[1012,2341,2342],{"scriptlevel":1014,"displaystyle":796},[1012,2343,2344],{"scriptlevel":1014,"displaystyle":796},[1012,2345,2346],{"scriptlevel":1014,"displaystyle":506},[477,2347,2348,2351,2353],{},[480,2349,2350],{},"5.7",[484,2352,486],{},[490,2354,2355],{"mathvariant":492},"W",[498,2357,2358],{"encoding":500},"\\begin{aligned}\nP\n  &= \\tau\\omega \\\\\n  &= (mgr)\\left(n_{\\mathrm{rpm}} \\cdot \\frac{2\\pi}{60}\\right) \\\\\n  &= (\\qty{3.5}{\\kg} \\cdot \\qty{9.80665}{\\m\u002F\\s^2} \\cdot \\qty{0.015}{\\m})\n     \\left(\\qty{106}{rpm} \\cdot \\frac{2\\pi}{60}\\right) \\\\\n  &\\approx \\boxed{\\qty{5.7}{W}}\n\\end{aligned}",[115,2360,2362],{"className":2361,"ariaHidden":506},[505],[115,2363,2365,2369],{"className":2364},[510],[115,2366],{"className":2367,"style":2368},[514],"height:8.2834em;vertical-align:-3.8917em;",[115,2370,2372],{"className":2371},[519],[115,2373,2375,2440],{"className":2374},[1606],[115,2376,2378],{"className":2377},[1786],[115,2379,2381,2431],{"className":2380},[1060,1061],[115,2382,2384,2428],{"className":2383},[1065],[115,2385,2388,2401,2410,2419],{"className":2386,"style":2387},[1069],"height:4.3917em;",[115,2389,2391,2395],{"style":2390},"top:-7.0017em;",[115,2392],{"className":2393,"style":2394},[1077],"height:3.45em;",[115,2396,2398],{"className":2397},[519],[115,2399,2158],{"className":2400,"style":1877},[519,774],[115,2402,2404,2407],{"style":2403},"top:-4.8917em;",[115,2405],{"className":2406,"style":2394},[1077],[115,2408],{"className":2409},[519],[115,2411,2413,2416],{"style":2412},"top:-2.1917em;",[115,2414],{"className":2415,"style":2394},[1077],[115,2417],{"className":2418},[519],[115,2420,2422,2425],{"style":2421},"top:0.0817em;",[115,2423],{"className":2424,"style":2394},[1077],[115,2426],{"className":2427},[519],[115,2429,1100],{"className":2430},[1099],[115,2432,2434],{"className":2433},[1065],[115,2435,2438],{"className":2436,"style":2437},[1069],"height:3.8917em;",[115,2439],{},[115,2441,2443],{"className":2442},[1844],[115,2444,2446,2948],{"className":2445},[1060,1061],[115,2447,2449,2945],{"className":2448},[1065],[115,2450,2452,2478,2655,2865],{"className":2451,"style":2387},[1069],[115,2453,2454,2457],{"style":2390},[115,2455],{"className":2456,"style":2394},[1077],[115,2458,2460,2463,2466,2469,2472,2475],{"className":2459},[519],[115,2461],{"className":2462},[519],[115,2464],{"className":2465,"style":1113},[523],[115,2467,905],{"className":2468},[1117],[115,2470],{"className":2471,"style":1113},[523],[115,2473,1621],{"className":2474,"style":1810},[519,774],[115,2476,2173],{"className":2477,"style":833},[519,774],[115,2479,2480,2483],{"style":2403},[115,2481],{"className":2482,"style":2394},[1077],[115,2484,2486,2489,2492,2495,2498,2501,2504,2507,2510,2513,2516],{"className":2485},[519],[115,2487],{"className":2488},[519],[115,2490],{"className":2491,"style":1113},[523],[115,2493,905],{"className":2494},[1117],[115,2496],{"className":2497,"style":1113},[523],[115,2499,797],{"className":2500},[829],[115,2502,657],{"className":2503},[519,774],[115,2505,558],{"className":2506,"style":833},[519,774],[115,2508,898],{"className":2509,"style":1881},[519,774],[115,2511,805],{"className":2512},[840],[115,2514],{"className":2515,"style":524},[523],[115,2517,2520,2529,2575,2578,2581,2584,2649],{"className":2518},[2519],"minner",[115,2521,2525],{"className":2522,"style":2524},[829,2523],"delimcenter","top:0em;",[115,2526,797],{"className":2527},[2528,1084],"delimsizing",[115,2530,2532,2535],{"className":2531},[519],[115,2533,893],{"className":2534},[519,774],[115,2536,2538],{"className":2537},[1056],[115,2539,2541,2567],{"className":2540},[1060,1061],[115,2542,2544,2564],{"className":2543},[1065],[115,2545,2547],{"className":2546,"style":1070},[1069],[115,2548,2549,2552],{"style":1073},[115,2550],{"className":2551,"style":1078},[1077],[115,2553,2555],{"className":2554},[1082,1083,1084,1085],[115,2556,2558],{"className":2557},[519,1085],[115,2559,2561],{"className":2560},[519,1085],[115,2562,1095],{"className":2563},[519,531,1085],[115,2565,1100],{"className":2566},[1099],[115,2568,2570],{"className":2569},[1065],[115,2571,2573],{"className":2572,"style":1107},[1069],[115,2574],{},[115,2576],{"className":2577,"style":1208},[523],[115,2579,922],{"className":2580},[1212],[115,2582],{"className":2583,"style":1208},[523],[115,2585,2587,2590,2646],{"className":2586},[519],[115,2588],{"className":2589},[829,1134],[115,2591,2593],{"className":2592},[907],[115,2594,2596,2638],{"className":2595},[1060,1061],[115,2597,2599,2635],{"className":2598},[1065],[115,2600,2602,2613,2621],{"className":2601,"style":1241},[1069],[115,2603,2604,2607],{"style":1150},[115,2605],{"className":2606,"style":1154},[1077],[115,2608,2610],{"className":2609},[519],[115,2611,929],{"className":2612},[519],[115,2614,2615,2618],{"style":1169},[115,2616],{"className":2617,"style":1154},[1077],[115,2619],{"className":2620,"style":1177},[1176],[115,2622,2623,2626],{"style":1180},[115,2624],{"className":2625,"style":1154},[1077],[115,2627,2629,2632],{"className":2628},[519],[115,2630,1693],{"className":2631},[519],[115,2633,800],{"className":2634,"style":833},[519,774],[115,2636,1100],{"className":2637},[1099],[115,2639,2641],{"className":2640},[1065],[115,2642,2644],{"className":2643,"style":1199},[1069],[115,2645],{},[115,2647],{"className":2648},[840,1134],[115,2650,2652],{"className":2651,"style":2524},[840,2523],[115,2653,805],{"className":2654},[2528,1084],[115,2656,2657,2660],{"style":2412},[115,2658],{"className":2659,"style":2394},[1077],[115,2661,2663,2666,2669,2672,2675,2678,2681,2684,2690,2693,2696,2699,2702,2705,2740,2743,2746,2749,2752,2755,2758,2761,2764],{"className":2662},[519],[115,2664],{"className":2665},[519],[115,2667],{"className":2668,"style":1113},[523],[115,2670,905],{"className":2671},[1117],[115,2673],{"className":2674,"style":1113},[523],[115,2676,797],{"className":2677},[829],[115,2679,548],{"className":2680},[519],[115,2682],{"className":2683,"style":524},[523],[115,2685,2687],{"className":2686},[519],[115,2688,585],{"className":2689,"style":584},[519,531],[115,2691],{"className":2692,"style":1208},[523],[115,2694,922],{"className":2695},[1212],[115,2697],{"className":2698,"style":1208},[523],[115,2700,1677],{"className":2701},[519],[115,2703],{"className":2704,"style":524},[523],[115,2706,2708,2711],{"className":2707},[519],[115,2709,1953],{"className":2710},[519,531],[115,2712,2714,2717],{"className":2713},[519],[115,2715,50],{"className":2716},[519,531],[115,2718,2720],{"className":2719},[1056],[115,2721,2723],{"className":2722},[1060],[115,2724,2726],{"className":2725},[1065],[115,2727,2729],{"className":2728,"style":1972},[1069],[115,2730,2731,2734],{"style":1975},[115,2732],{"className":2733,"style":1078},[1077],[115,2735,2737],{"className":2736},[1082,1083,1084,1085],[115,2738,1693],{"className":2739},[519,531,1085],[115,2741],{"className":2742,"style":1208},[523],[115,2744,922],{"className":2745},[1212],[115,2747],{"className":2748,"style":1208},[523],[115,2750,1698],{"className":2751},[519],[115,2753],{"className":2754,"style":524},[523],[115,2756,657],{"className":2757},[519,531],[115,2759,805],{"className":2760},[840],[115,2762],{"className":2763,"style":524},[523],[115,2765,2767,2773,2776,2779,2785,2788,2791,2794,2859],{"className":2766},[2519],[115,2768,2770],{"className":2769,"style":2524},[829,2523],[115,2771,797],{"className":2772},[2528,1084],[115,2774,1023],{"className":2775},[519],[115,2777],{"className":2778,"style":524},[523],[115,2780,2782],{"className":2781},[519],[115,2783,1095],{"className":2784},[519,531],[115,2786],{"className":2787,"style":1208},[523],[115,2789,922],{"className":2790},[1212],[115,2792],{"className":2793,"style":1208},[523],[115,2795,2797,2800,2856],{"className":2796},[519],[115,2798],{"className":2799},[829,1134],[115,2801,2803],{"className":2802},[907],[115,2804,2806,2848],{"className":2805},[1060,1061],[115,2807,2809,2845],{"className":2808},[1065],[115,2810,2812,2823,2831],{"className":2811,"style":1241},[1069],[115,2813,2814,2817],{"style":1150},[115,2815],{"className":2816,"style":1154},[1077],[115,2818,2820],{"className":2819},[519],[115,2821,929],{"className":2822},[519],[115,2824,2825,2828],{"style":1169},[115,2826],{"className":2827,"style":1154},[1077],[115,2829],{"className":2830,"style":1177},[1176],[115,2832,2833,2836],{"style":1180},[115,2834],{"className":2835,"style":1154},[1077],[115,2837,2839,2842],{"className":2838},[519],[115,2840,1693],{"className":2841},[519],[115,2843,800],{"className":2844,"style":833},[519,774],[115,2846,1100],{"className":2847},[1099],[115,2849,2851],{"className":2850},[1065],[115,2852,2854],{"className":2853,"style":1199},[1069],[115,2855],{},[115,2857],{"className":2858},[840,1134],[115,2860,2862],{"className":2861,"style":2524},[840,2523],[115,2863,805],{"className":2864},[2528,1084],[115,2866,2867,2870],{"style":2421},[115,2868],{"className":2869,"style":2394},[1077],[115,2871,2873,2876,2879,2882,2885],{"className":2872},[519],[115,2874],{"className":2875},[519],[115,2877],{"className":2878,"style":1113},[523],[115,2880,1006],{"className":2881},[1117],[115,2883],{"className":2884,"style":1113},[523],[115,2886,2888],{"className":2887},[519],[115,2889,2891,2936],{"className":2890},[1060,1061],[115,2892,2894,2933],{"className":2893},[1065],[115,2895,2898,2923],{"className":2896,"style":2897},[1069],"height:1.0233em;",[115,2899,2901,2905],{"style":2900},"top:-3.3633em;",[115,2902],{"className":2903,"style":2904},[1077],"height:3.3633em;",[115,2906,2908],{"className":2907},[1546],[115,2909,2911],{"className":2910},[519],[115,2912,2914,2917,2920],{"className":2913},[519],[115,2915,2350],{"className":2916},[519],[115,2918],{"className":2919,"style":524},[523],[115,2921,2355],{"className":2922,"style":584},[519,531],[115,2924,2926,2929],{"style":2925},"top:-3.0233em;",[115,2927],{"className":2928,"style":2904},[1077],[115,2930],{"className":2931,"style":2932},[1574,1575],"height:1.3633em;border-style:solid;border-width:0.04em;",[115,2934,1100],{"className":2935},[1099],[115,2937,2939],{"className":2938},[1065],[115,2940,2943],{"className":2941,"style":2942},[1069],"height:0.34em;",[115,2944],{},[115,2946,1100],{"className":2947},[1099],[115,2949,2951],{"className":2950},[1065],[115,2952,2954],{"className":2953,"style":2437},[1069],[115,2955],{},[10,2957,2958,2959,2966,2967,2971],{},"These numbers were theoretical, with additional margin needed for gearbox and motor losses, starting friction, and variations in shade resistance. Still, they gave me a useful starting point for choosing a motor and power supply.\nAfter spelunking on Amazon, I found a promising candidate: a ",[2960,2961,2965],"a",{"href":2962,"rel":2963},"https:\u002F\u002Fwww.amazon.com\u002Fdp\u002FB072B83JY4",[2964],"nofollow","uxcell worm gear motor"," (",[2960,2968,2970],{"href":2969},"\u002Fimages\u002Fwriting\u002Fbuilding-my-own-smart-roller-shades\u002Fuxcell_motor_product_page.png","screenshot",") for ~$20.",[17,2973,19,2974,19,2978],{"style":453},[21,2975],{"src":2976,"alt":2977},"\u002Fimages\u002Fwriting\u002Fbuilding-my-own-smart-roller-shades\u002Fuxcell_motor.jpg","Uxcell 12 V geared DC motor with a right-angle gearbox",[26,2979,2980],{},"Seems to look robust enough—will it be?",[10,2982,2983],{},"On paper, it had these specs:",[73,2985,2986,3039,3109,3153],{},[76,2987,2988,3038],{},[115,2989,2991,3016],{"className":2990},[464],[115,2992,2994],{"className":2993},[468],[470,2995,2996],{"xmlns":472},[474,2997,2998,3013],{},[477,2999,3000,3003,3005],{},[480,3001,3002],{},"110",[484,3004,486],{},[477,3006,3007,3009,3011],{},[490,3008,898],{"mathvariant":492},[490,3010,10],{"mathvariant":492},[490,3012,657],{"mathvariant":492},[498,3014,3015],{"encoding":500},"\\qty{110}{rpm}",[115,3017,3019],{"className":3018,"ariaHidden":506},[505],[115,3020,3022,3026,3029,3032],{"className":3021},[510],[115,3023],{"className":3024,"style":3025},[514],"height:0.8389em;vertical-align:-0.1944em;",[115,3027,3002],{"className":3028},[519],[115,3030],{"className":3031,"style":524},[523],[115,3033,3035],{"className":3034},[519],[115,3036,1095],{"className":3037},[519,531]," — though this was the no-load speed, so the actual speed would be lower.",[76,3040,3041,3108],{},[115,3042,3044,3075],{"className":3043},[464],[115,3045,3047],{"className":3046},[468],[470,3048,3049],{"xmlns":472},[474,3050,3051,3072],{},[477,3052,3053,3056,3058],{},[480,3054,3055],{},"8",[484,3057,486],{},[477,3059,3060,3062,3064,3066,3068,3070],{},[490,3061,555],{"mathvariant":492},[490,3063,558],{"mathvariant":492},[490,3065,1757],{"mathvariant":492},[794,3067,922],{},[490,3069,654],{"mathvariant":492},[490,3071,657],{"mathvariant":492},[498,3073,3074],{"encoding":500},"\\qty{8}{kgf\\cdot cm}",[115,3076,3078],{"className":3077,"ariaHidden":506},[505],[115,3079,3081,3084,3087,3090],{"className":3080},[510],[115,3082],{"className":3083,"style":571},[514],[115,3085,3055],{"className":3086},[519],[115,3088],{"className":3089,"style":524},[523],[115,3091,3093,3096,3099,3102,3105],{"className":3092},[519],[115,3094,2087],{"className":3095,"style":2086},[519,531],[115,3097],{"className":3098,"style":1208},[523],[115,3100,922],{"className":3101},[1212],[115,3103],{"className":3104,"style":1208},[523],[115,3106,683],{"className":3107},[519,531]," of stall torque — about 50% above my estimate, although stall torque is not directly comparable to the torque available at speed.",[76,3110,3111,3152],{},[115,3112,3114,3134],{"className":3113},[464],[115,3115,3117],{"className":3116},[468],[470,3118,3119],{"xmlns":472},[474,3120,3121,3131],{},[477,3122,3123,3126,3128],{},[480,3124,3125],{},"12",[484,3127,486],{},[490,3129,3130],{"mathvariant":492},"V",[498,3132,3133],{"encoding":500},"\\qty{12}{V}",[115,3135,3137],{"className":3136,"ariaHidden":506},[505],[115,3138,3140,3143,3146,3149],{"className":3139},[510],[115,3141],{"className":3142,"style":619},[514],[115,3144,3125],{"className":3145},[519],[115,3147],{"className":3148,"style":524},[523],[115,3150,3130],{"className":3151,"style":584},[519,531]," DC, which meant I would need a buck converter for the ESP8266.",[76,3154,3155,3156,3203],{},"Reasonable dimensions for the gearbox, and an ",[115,3157,3159,3181],{"className":3158},[464],[115,3160,3162],{"className":3161},[468],[470,3163,3164],{"xmlns":472},[474,3165,3166,3178],{},[477,3167,3168,3170,3172],{},[480,3169,3055],{},[484,3171,486],{},[477,3173,3174,3176],{},[490,3175,657],{"mathvariant":492},[490,3177,657],{"mathvariant":492},[498,3179,3180],{"encoding":500},"\\qty{8}{mm}",[115,3182,3184],{"className":3183,"ariaHidden":506},[505],[115,3185,3187,3190,3193,3196],{"className":3186},[510],[115,3188],{"className":3189,"style":670},[514],[115,3191,3055],{"className":3192},[519],[115,3194],{"className":3195,"style":524},[523],[115,3197,3199],{"className":3198},[519],[115,3200,3202],{"className":3201},[519,531],"mm"," shaft that the drive wheel could fit onto.",[10,3205,3206],{},"With a suitable motor in hand, I could now turn my attention to the mechanical assembly.",[65,3208,3210],{"id":3209},"building-the-drive-mechanism","Building the drive mechanism",[10,3212,3213,3214,3219,3220,3225,3226,3231],{},"A few months ago, I came across a 3D printer on Marketplace for just shy of $100: the ",[2960,3215,3218],{"href":3216,"rel":3217},"https:\u002F\u002Fwww.biline.ca\u002F3dmod-t.htm",[2964],"New Matter MOD-t",".\nI had fun making shot glasses and 3D-printed figures, but it was time to start putting it to something useful.\nThe filament it came with was PLA, which was a bit too malleable for the parts that were supposed to handle torque.\nABS might have been a better mechanical choice, but the toxic fumes didn't seem that appealing for home printing—and the MOD-t didn't support ABS or PETG anyway.\nLooking for the toughest PLA available, I came across ",[2960,3221,3224],{"href":3222,"rel":3223},"https:\u002F\u002Fshop.polymaker.com\u002Fproducts\u002Fpolymax-pla",[2964],"PolyMax PLA",", which I figured I could use for the drive wheel.\nThen, for the enclosure, I ordered some cheaper ",[2960,3227,3230],{"href":3228,"rel":3229},"https:\u002F\u002Fwww.amazon.com\u002Fdp\u002FB07H9KW5P9",[2964],"Amazon Basics filament",".",[10,3233,3234,3235,3240,3241,3246],{},"It was then time to brush up on my 3D modeling skills.\nAlthough I had some experience with SolidWorks and AutoCAD from first-year engineering courses, I had not touched 3D-modeling software since.\nAfter seeing the benefits of infrastructure as code, I knew I would enjoy a tool that let me generate and refine 3D models programmatically.\n",[2960,3236,3239],{"href":3237,"rel":3238},"https:\u002F\u002Fopenscad.org",[2964],"OpenSCAD"," seemed perfect.\nI could write and iterate on my designs in code, generate an STL file, open it in ",[2960,3242,3245],{"href":3243,"rel":3244},"https:\u002F\u002Fultimaker.com\u002Fsoftware\u002Fultimaker-cura\u002F",[2964],"UltiMaker Cura",", and then export G-code to the printer.",[3248,3249,3251],"h3",{"id":3250},"designing-the-drive-wheel","Designing the drive wheel",[10,3253,3254],{},"The drive wheel was the first part I started with, and I knew I'd need to go through several iterations.\nMy first design was a sprocket of sorts that had just the right grooves to grip the metal beads.",[17,3256,19,3257,19,3261],{},[21,3258],{"src":3259,"alt":3260},"\u002Fimages\u002Fwriting\u002Fbuilding-my-own-smart-roller-shades\u002Fopenscad.png","OpenSCAD rendering and source code for the first drive-wheel design",[26,3262,3263,3264,3268],{},"My ",[2960,3265,3267],{"href":3266},"https:\u002F\u002Fgithub.com\u002Fvaskevich\u002Fshado\u002Fblob\u002F4b31024e89965597d087d73bd5d97913a447dda3\u002Fhardware\u002Fgear.scad","first attempt"," at 3D modeling a drive wheel.",[10,3270,3271],{},"And thanks to WebGL, here's that same interactive model:",[3273,3274],"model-viewer",{"src":3275,"camera-controls":110,"auto-rotate":110,"rotation-per-second":3276,"auto-rotate-delay":1014,"camera-orbit":3277,"orientation":3278,"shadow-intensity":3279,"shadow-softness":3280,"exposure":3281,"environment-image":3282,"touch-action":3283,"style":3284},"\u002Fimages\u002Fwriting\u002Fbuilding-my-own-smart-roller-shades\u002Fmodels\u002Fgear1.glb?v=6","60deg","45deg 65deg 100%","0deg 90deg 0deg","1.5","1","0.75","neutral","pan-y","width: 100%; height: 500px; background-color: #1a1a1a;",[10,3286,3287,3288,3293],{},"My 3D printer was essentially unsupported, but thanks to a ",[2960,3289,3292],{"href":3290,"rel":3291},"https:\u002F\u002Fwww.reddit.com\u002Fr\u002Fnewmatter\u002F",[2964],"Reddit community",", and a bit of experimentation, I was able to get it up and running.",[17,3295,19,3296],{},[21,3297],{"src":3298,"alt":3299},"\u002Fimages\u002Fwriting\u002Fbuilding-my-own-smart-roller-shades\u002Fprinting_gear.webp","New Matter MOD-t 3D printer producing a drive-wheel prototype",[10,3301,3302],{},"Suffice it to say, it took a few tries to find the right printer settings, notch spacing, and tolerance to get a drive wheel that gripped both the shaft and chain reliably enough for testing. Eventually, I had something workable.",[10,3304,3305],{},[21,3306],{"alt":3307,"src":3308},"First drive-wheel prototype","\u002Fimages\u002Fwriting\u002Fbuilding-my-own-smart-roller-shades\u002Fprinted_gear.jpg",[65,3310,3312],{"id":3311},"designing-the-electronics","Designing the electronics",[10,3314,3315],{},"With the motor and drive wheel selected, I needed a control system that could do three things: run the motor in either direction, accept commands over the internet, and support a pair of physical open and close buttons.",[10,3317,3318,3319,3324],{},"For the controller, I decided to use an ESP8266, a small, Wi-Fi-enabled chip often available for under $2 online.\nTo make it easier on myself, I went with a ",[2960,3320,3323],{"href":3321,"rel":3322},"https:\u002F\u002Fwww.wemos.cc\u002Fen\u002Flatest\u002Fd1\u002Fd1_mini_3.1.0.html",[2964],"Wemos D1 mini"," development board, which exposed 11 digital I\u002FO pins and one analog input, and included a Micro-USB connector for flashing it.",[10,3326,3327,3328,3367,3368,3408,3409,3448],{},"And with that, I had my first electrical challenge.\nThe motor ran at ",[115,3329,3331,3349],{"className":3330},[464],[115,3332,3334],{"className":3333},[468],[470,3335,3336],{"xmlns":472},[474,3337,3338,3346],{},[477,3339,3340,3342,3344],{},[480,3341,3125],{},[484,3343,486],{},[490,3345,3130],{"mathvariant":492},[498,3347,3348],{"encoding":500},"\\qty{12}{\\volt}",[115,3350,3352],{"className":3351,"ariaHidden":506},[505],[115,3353,3355,3358,3361,3364],{"className":3354},[510],[115,3356],{"className":3357,"style":619},[514],[115,3359,3125],{"className":3360},[519],[115,3362],{"className":3363,"style":524},[523],[115,3365,3130],{"className":3366,"style":584},[519,531]," and required significant current, while the ESP8266 operated at ",[115,3369,3371,3390],{"className":3370},[464],[115,3372,3374],{"className":3373},[468],[470,3375,3376],{"xmlns":472},[474,3377,3378,3387],{},[477,3379,3380,3383,3385],{},[480,3381,3382],{},"3.3",[484,3384,486],{},[490,3386,3130],{"mathvariant":492},[498,3388,3389],{"encoding":500},"\\qty{3.3}{\\volt}",[115,3391,3393],{"className":3392,"ariaHidden":506},[505],[115,3394,3396,3399,3402,3405],{"className":3395},[510],[115,3397],{"className":3398,"style":619},[514],[115,3400,3382],{"className":3401},[519],[115,3403],{"className":3404,"style":524},[523],[115,3406,3130],{"className":3407,"style":584},[519,531],".\nI wanted the whole device to use a single power adapter, so I needed a way to efficiently produce a stable ",[115,3410,3412,3430],{"className":3411},[464],[115,3413,3415],{"className":3414},[468],[470,3416,3417],{"xmlns":472},[474,3418,3419,3427],{},[477,3420,3421,3423,3425],{},[480,3422,3382],{},[484,3424,486],{},[490,3426,3130],{"mathvariant":492},[498,3428,3429],{"encoding":500},"\\qty{3.3}{V}",[115,3431,3433],{"className":3432,"ariaHidden":506},[505],[115,3434,3436,3439,3442,3445],{"className":3435},[510],[115,3437],{"className":3438,"style":619},[514],[115,3440,3382],{"className":3441},[519],[115,3443],{"className":3444,"style":524},[523],[115,3446,3130],{"className":3447,"style":584},[519,531]," supply from the motor's input.",[3248,3450,3452],{"id":3451},"powering-the-controller","Powering the controller",[10,3454,3455,3456,3461,3462,3467,3468,3473],{},"I initially explored the ",[2960,3457,3460],{"href":3458,"rel":3459},"https:\u002F\u002Fwww.ti.com\u002Flit\u002Fds\u002Fsymlink\u002Flm1117.pdf",[2964],"LM1117"," linear voltage regulator, but it became quite hot while dissipating the excess voltage as heat.\nA ",[2960,3463,3466],{"href":3464,"rel":3465},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FBuck_converter",[2964],"buck converter"," was the better choice, and so I got one working with an ",[2960,3469,3472],{"href":3470,"rel":3471},"https:\u002F\u002Fwww.ti.com\u002Flit\u002Fds\u002Fsymlink\u002Ftl2575-33.pdf",[2964],"TL2575-33IN"," and an inductor, diode, and capacitors to step the voltage down more efficiently.",[17,3475,19,3476,19,3480],{},[21,3477],{"src":3478,"alt":3479},"\u002Fimages\u002Fwriting\u002Fbuilding-my-own-smart-roller-shades\u002Fbuck_converter.jpeg","Buck-converter circuit assembled on a breadboard",[26,3481,3482],{},"Testing the 3.3 V buck converter on a breadboard before attaching the microcontroller.",[3248,3484,3486],{"id":3485},"reversing-the-motor","Reversing the motor",[10,3488,3489,3490,3495,3496,3501],{},"The next problem was controlling the motor and its direction.\nThe ESP8266 could not drive it directly.\nIts GPIO pins provide only low-power control signals, while the motor required substantially more voltage and current.\nFor that, I needed an ",[2960,3491,3494],{"href":3492,"rel":3493},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FH-bridge",[2964],"H-bridge",".\nI began by experimenting with one built from N- and P-channel MOSFETs, but after blowing a few out while running the motor, I caved and bought the ",[2960,3497,3500],{"href":3498,"rel":3499},"https:\u002F\u002Fwww.st.com\u002Fen\u002Fmotor-drivers\u002Fl6203.html",[2964],"L6203"," integrated full-bridge driver.",[10,3503,3504],{},"I combined the regulator and motor driver on perfboard and powered them with a 12 V, 5 A AC-to-DC adapter connected through a barrel jack. At last, I had an initial prototype:",[17,3506,19,3507,19,3511],{},[21,3508],{"src":3509,"alt":3510},"\u002Fimages\u002Fwriting\u002Fbuilding-my-own-smart-roller-shades\u002Fshado_prototype.jpeg","Hand-wired Shado electronics prototype assembled on perfboard",[26,3512,3513],{},"Not pictured: the underside with amateur-level soldering.",[3248,3515,3517],{"id":3516},"tracking-the-shade-position","Tracking the shade position",[10,3519,3520],{},"The prototype worked, and a basic program could now run the motor in either direction.\nHolding it against the beaded chain, I noticed that the motor slowed down a fair bit under load. The controller also needed a way to know how far the shade had moved, both to report its position and to know when to stop.",[10,3522,3523,3524,3529],{},"To add this position tracking, I integrated a small magnet into the drive wheel.\nWith a ",[2960,3525,3528],{"href":3526,"rel":3527},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FHall_effect_sensor",[2964],"Hall-effect sensor"," taped to the base of the motor, the controller could detect each revolution of the drive wheel.",[17,3531,19,3532,19,3536],{"style":453},[21,3533],{"src":3534,"alt":3535},"\u002Fimages\u002Fwriting\u002Fbuilding-my-own-smart-roller-shades\u002Fhall_effect.jpeg","Hall-effect sensor mounted near the motor and drive wheel",[26,3537,3538],{},"Turns out the analog input on the Wemos D1 mini would come in handy after all!",[3248,3540,3542],{"id":3541},"from-hand-wired-prototype-to-pcb","From hand-wired prototype to PCB",[10,3544,3545],{},"After a few trips to Jameco Electronics near San Mateo, I had what seemed like a viable design.\nPutting it all together, the control circuit looked something like this:",[105,3547,3549],{"className":107,"code":3548,"language":109,"meta":110,"style":110},"flowchart LR\n    Input[\"12 V Input\"]\n\n    Input --> Buck[\"TL2575\u003Cbr\u002F>Buck Converter\"]\n    Buck -->|\"3.3 V\"| ESP[\"ESP8266\u003Cbr\u002F>Controller\"]\n\n    Buttons[\"Buttons\"] --> ESP\n    Hall[\"Hall Sensor\"] --> ESP\n\n    Input -->|\"12 V\"| Driver[\"L6203\u003Cbr\u002F>Motor Driver\"]\n    ESP -->|\"Direction\u003Cbr\u002F>+ enable\"| Driver\n    Driver --> Motor[\"Motor\"]\n",[112,3550,3551,3556,3561,3565,3570,3575,3579,3584,3589,3593,3598,3603],{"__ignoreMap":110},[115,3552,3553],{"class":117,"line":118},[115,3554,3555],{},"flowchart LR\n",[115,3557,3558],{"class":117,"line":124},[115,3559,3560],{},"    Input[\"12 V Input\"]\n",[115,3562,3563],{"class":117,"line":130},[115,3564,204],{"emptyLinePlaceholder":37},[115,3566,3567],{"class":117,"line":136},[115,3568,3569],{},"    Input --> Buck[\"TL2575\u003Cbr\u002F>Buck Converter\"]\n",[115,3571,3572],{"class":117,"line":142},[115,3573,3574],{},"    Buck -->|\"3.3 V\"| ESP[\"ESP8266\u003Cbr\u002F>Controller\"]\n",[115,3576,3577],{"class":117,"line":148},[115,3578,204],{"emptyLinePlaceholder":37},[115,3580,3581],{"class":117,"line":154},[115,3582,3583],{},"    Buttons[\"Buttons\"] --> ESP\n",[115,3585,3586],{"class":117,"line":160},[115,3587,3588],{},"    Hall[\"Hall Sensor\"] --> ESP\n",[115,3590,3591],{"class":117,"line":165},[115,3592,204],{"emptyLinePlaceholder":37},[115,3594,3595],{"class":117,"line":171},[115,3596,3597],{},"    Input -->|\"12 V\"| Driver[\"L6203\u003Cbr\u002F>Motor Driver\"]\n",[115,3599,3600],{"class":117,"line":177},[115,3601,3602],{},"    ESP -->|\"Direction\u003Cbr\u002F>+ enable\"| Driver\n",[115,3604,3605],{"class":117,"line":183},[115,3606,3607],{},"    Driver --> Motor[\"Motor\"]\n",[10,3609,3610],{},"My perfboard prototype worked, but it was bulky, difficult to reproduce if I wanted more than one, and frankly a bit unsightly.\nIt was time to take things to the next level.",[10,3612,3613,3614,3619,3620,3625],{},"I had always pictured custom PCBs as being out of reach for all but the most well-funded projects.\nSo when I discovered that I could have five boards fabricated and delivered within a few days for $20, I jumped at the opportunity.\nThere are several PCB manufacturers on the market, but I went with ",[2960,3615,3618],{"href":3616,"rel":3617},"https:\u002F\u002Fwww.pcbway.com",[2964],"PCBWay",".\n(If you haven't seen it already, check out ",[2960,3621,3624],{"href":3622,"rel":3623},"https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=lVyxlw3rixI",[2964],"this tour"," of their Shenzhen factory.)",[3248,3627,3629],{"id":3628},"schematic-and-board-layout","Schematic and board layout",[10,3631,3632,3633,3638,3639,3644],{},"The first step in creating a custom PCB was designing the schematic with the right tool.\nAt the time, I got started with Autodesk EAGLE (now Fusion 360), but the newer versions of ",[2960,3634,3637],{"href":3635,"rel":3636},"https:\u002F\u002Fwww.kicad.org\u002F",[2964],"KiCad"," would have also worked.\nAfter a lot of pointing and clicking, and with the help of ",[2960,3640,3643],{"href":3641,"rel":3642},"https:\u002F\u002Fgithub.com\u002Fsparkfun\u002FSparkFun-Eagle-Libraries",[2964],"SparkFun's libraries",", I had a presentable design.",[17,3646,19,3649,19,3653],{"className":3647},[3648],"breakout",[21,3650],{"src":3651,"alt":3652},"\u002Fimages\u002Fwriting\u002Fbuilding-my-own-smart-roller-shades\u002Fschematic.svg","Shado schematic",[26,3654,3655,3656,3231],{},"At the top left, the controller is connected to two buttons, the Hall effect sensor, and the motor driver, with an LED and a couple pull-up resistors. The motor driver below, based on the L6203, uses two bootstrap capacitors for its charge pump, alongside a sense resistor for current feedback and a decoupling capacitor to stabilize the reference voltage. It's powered by the 12 V input to the right, which also feeds into the buck converter above it. The buck converter, centered around the TL2575-33IN, steps the 12 V power supply down to 3.3 V using an input filter capacitor, an inductor, a Schottky catch diode for energy storage and freewheeling current during switching, along with an output smoothing capacitor. View the full schematic ",[2960,3657,3659],{"href":3658},"\u002Fimages\u002Fwriting\u002Fbuilding-my-own-smart-roller-shades\u002Fschematic.pdf","here",[10,3661,3662,3663,3668,3669,3674],{},"The next part was the board layout.\nFollowing some ",[2960,3664,3667],{"href":3665,"rel":3666},"https:\u002F\u002Flearn.sparkfun.com\u002Ftutorials\u002Fusing-eagle-board-layout\u002F",[2964],"tutorials"," and ",[2960,3670,3673],{"href":3671,"rel":3672},"https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=5nLONfdh7vw",[2964],"videos"," online, I arranged all the parts, routed them together with traces and vias (it's like solving a puzzle!), and validated the board for errors.",[17,3676,19,3678,19,3682],{"className":3677},[3648],[21,3679],{"src":3680,"alt":3681},"\u002Fimages\u002Fwriting\u002Fbuilding-my-own-smart-roller-shades\u002Fboard_layout.png","Board layout",[26,3683,3684],{},"Autodesk Fusion showing the top layer of the 2-layer design for the circuit. If you look closely, you can see all the components laid out and connected. A copper pour gives it a red finish here.",[3248,3686,3688],{"id":3687},"sending-it-to-the-fab","Sending it to the fab",[10,3690,3691,3692,3697],{},"With the design ready, it was time to have the boards fabbed.\nExporting the ",[2960,3693,3696],{"href":3694,"rel":3695},"https:\u002F\u002Fwww.autodesk.com\u002Fproducts\u002Ffusion-360\u002Fblog\u002Fa-history-of-gerber-files-and-the-man-who-made-them\u002F",[2964],"Gerber files"," from Fusion 360, I sent them to PCBWay.\nSomeone reviewed them, and just shy of 24 hours later I was notified that the boards I requested were ready to be shipped.\nThe whole process was astoundingly fast.",[17,3699,19,3700],{"style":453},[21,3701],{"src":3702,"alt":3703},"\u002Fimages\u002Fwriting\u002Fbuilding-my-own-smart-roller-shades\u002Fpcbway.png","PCBWay status",[10,3705,3706],{},"It also didn't take long to get the boards delivered, and they looked phenomenal.",[17,3708,19,3709],{"style":453},[21,3710],{"src":3711,"alt":3712},"\u002Fimages\u002Fwriting\u002Fbuilding-my-own-smart-roller-shades\u002Fpcb.jpeg","Fabricated Shado printed circuit boards",[3248,3714,3716],{"id":3715},"assembling-the-board","Assembling the board",[10,3718,3719],{},"Compared with the perfboard, the PCB was much easier to work with.\nThe parts fit onto the board like a glove, and I just had to carefully solder them in place and clip off excess leads.",[17,3721,19,3722,19,3726],{},[21,3723],{"src":3724,"alt":3725},"\u002Fimages\u002Fwriting\u002Fbuilding-my-own-smart-roller-shades\u002Fshado_assembled.jpeg","Fully assembled Shado printed circuit board",[26,3727,3728],{},"The final assembly of the PCB.",[3248,3730,3732],{"id":3731},"wiring-the-remaining-components","Wiring the remaining components",[10,3734,3735,3736,3741,3742,3747],{},"The final step was connecting the board to the remaining components: the motor, Hall effect sensor, and physical buttons.\nSince I wanted to adjust those more easily, I used a ",[2960,3737,3740],{"href":3738,"rel":3739},"https:\u002F\u002Fwww.amazon.com\u002Fdp\u002FB078RRPRQZ",[2964],"Dupont connector kit"," with a ",[2960,3743,3746],{"href":3744,"rel":3745},"https:\u002F\u002Fwww.amazon.com\u002Fdp\u002FB00OMM4YUY",[2964],"crimping tool"," to set up headers and removable wires.",[17,3749,19,3750,19,3754],{},[21,3751],{"src":3752,"alt":3753},"\u002Fimages\u002Fwriting\u002Fbuilding-my-own-smart-roller-shades\u002Fcrimping.jpeg","Crimping tool, wire stripper, and Dupont connector components",[26,3755,3756,3757,3761],{},"Orange crimping tool for ",[2960,3758,3760],{"href":3759},"https:\u002F\u002Fwww.instructables.com\u002FDupont-Crimp-Tool-Tutorial","solder-free connectors",". The blue wire stripper also came in useful.",[65,3763,3765],{"id":3764},"designing-the-enclosure","Designing the enclosure",[10,3767,3768],{},"With the drive mechanism working and the PCB dimensions finalized, I could finally design the enclosure that would house everything in a neat package.\nThis was not just for aesthetics: I also needed a way to mount the assembly on the wall while keeping the beaded chain under the right amount of tension.",[10,3770,3771],{},"Firing up OpenSCAD, I started by figuring out how to get the motor's unwieldy shape into a chassis that could hold it.\nI also needed a place for the PCB, so I built a small platform with grooves that it could slide into.",[3273,3773],{"src":3774,"camera-controls":110,"auto-rotate":110,"orientation":3775,"shadow-intensity":3279,"shadow-softness":3280,"exposure":3281,"environment-image":3282,"touch-action":3283,"style":3284},"\u002Fimages\u002Fwriting\u002Fbuilding-my-own-smart-roller-shades\u002Fmodels\u002Fmotor_chassis.glb?v=6","-45deg -90deg 180deg",[10,3777,3778],{},"To print it, I split the chassis into smaller parts and superglued them together. This avoided supports, which were error-prone and increased both print time and filament usage.",[10,3780,3781],{},"For the wall mount, I designed a base plate with a dovetail groove. A matching insert on the underside of the chassis allowed it to slide into place—try rotating the model above to see it.\nAfter mounting the base plate, I could still adjust the chain tension by placing small trapezoidal spacers in the groove to change the chassis offset.",[3273,3783],{"src":3784,"camera-controls":110,"auto-rotate":110,"camera-orbit":3785,"orientation":3786,"shadow-intensity":3279,"shadow-softness":3280,"exposure":3281,"environment-image":3282,"touch-action":3283,"style":3284},"\u002Fimages\u002Fwriting\u002Fbuilding-my-own-smart-roller-shades\u002Fmodels\u002Fmounting_plate.glb?v=8","45deg 60deg 105%","0deg -90deg 90deg",[10,3788,3789],{},"To finish up the enclosure, I added a chamfered removable shell with integrated button caps (and an interior mount for the switches), an opening for an LED, and an inset logo.",[3273,3791],{"src":3792,"camera-controls":110,"autoPlay":37,"auto-rotate":110,"toggle-cover":110,"orientation":3793,"shadow-intensity":3279,"shadow-softness":3280,"exposure":3281,"environment-image":3282,"touch-action":3283,"style":3284},"\u002Fimages\u002Fwriting\u002Fbuilding-my-own-smart-roller-shades\u002Fmodels\u002Ffull_assembly.glb?v=17","20deg -90deg 180deg",[10,3795,3796],{},"It was time to get everything printed and glued.\nAs always, it took a few attempts (and misprints) to get it right, but I could now move on to writing some code.",[17,3798,19,3799,19,3803],{},[21,3800],{"src":3801,"alt":3802},"\u002Fimages\u002Fwriting\u002Fbuilding-my-own-smart-roller-shades\u002Fcover.jpeg","Two printed pieces of the Shado enclosure shell",[26,3804,3805],{},"To print out the shell, I had to break it up into two pieces. Perhaps modern 3D printers would fare better, but I had more trouble with misprints when relying on supports.",[65,3807,3809],{"id":3808},"writing-the-firmware","Writing the firmware",[10,3811,3812],{},"Compared with the mechanical and electrical work, writing code was much more familiar territory, though previous embedded systems projects had taught me that it would have its own challenges.\nDeveloping the firmware was largely iterative, but the final architecture looked like this:",[105,3814,3817],{"className":107,"code":3815,"language":109,"meta":3816,"style":110},"graph LR\n    subgraph External [\"Inputs & Cloud\"]\n        Cloud[\"AWS IoT Core\u003Cbr\u002F>(Shadow & MQTT Commands)\"]\n        WebUI[\"Captive Portal\u003Cbr\u002F>(Wi-Fi Onboarding)\"]\n        Buttons[\"Physical Buttons\u003Cbr\u002F>(GPIO Interrupts)\"]\n    end\n\n    subgraph ESP [\"ESP8266 Firmware\"]\n        Scheduler[\"TaskScheduler\u003Cbr\u002F>(Cooperative Event Loop)\"]\n\n        subgraph Subsystems [\"Core Modules\"]\n            Conn[\"Connectivity\u003Cbr\u002F>(TLS \u002F NTP \u002F AWS)\"]\n            Cmd[\"Commander\u003Cbr\u002F>(Input Handler)\"]\n            Motor[\"Motor Control\u003Cbr\u002F>(State Machine)\"]\n            Settings[\"Settings Store\u003Cbr\u002F>(Dual-Sector EEPROM)\"]\n        end\n    end\n\n    subgraph Actuators [\"Hardware\"]\n        Drive[\"H-Bridge Driver\u003Cbr\u002F>& Geared DC Motor\"]\n        Sensor[\"Hall Effect Sensor\u003Cbr\u002F>(Rotation Counter)\"]\n    end\n\n    %% Flow Connections\n    Cloud \u003C-->|MQTT| Conn\n    WebUI -->|Config| Settings\n    Buttons -->|Interrupts| Cmd\n\n    Scheduler --> Conn\n    Scheduler --> Cmd\n    Scheduler --> Motor\n\n    Cmd --> Motor\n    Conn \u003C--> Motor\n\n    Motor -->|PWM Acceleration| Drive\n    Sensor -->|Analog Feedback| Motor\n    Motor -->|Save Position| Settings\n","zoom",[112,3818,3819,3824,3829,3834,3839,3844,3848,3852,3857,3862,3866,3871,3876,3881,3886,3891,3896,3900,3904,3909,3914,3919,3923,3927,3932,3937,3942,3947,3951,3956,3961,3966,3970,3975,3980,3984,3989,3994],{"__ignoreMap":110},[115,3820,3821],{"class":117,"line":118},[115,3822,3823],{},"graph LR\n",[115,3825,3826],{"class":117,"line":124},[115,3827,3828],{},"    subgraph External [\"Inputs & Cloud\"]\n",[115,3830,3831],{"class":117,"line":130},[115,3832,3833],{},"        Cloud[\"AWS IoT Core\u003Cbr\u002F>(Shadow & MQTT Commands)\"]\n",[115,3835,3836],{"class":117,"line":136},[115,3837,3838],{},"        WebUI[\"Captive Portal\u003Cbr\u002F>(Wi-Fi Onboarding)\"]\n",[115,3840,3841],{"class":117,"line":142},[115,3842,3843],{},"        Buttons[\"Physical Buttons\u003Cbr\u002F>(GPIO Interrupts)\"]\n",[115,3845,3846],{"class":117,"line":148},[115,3847,198],{},[115,3849,3850],{"class":117,"line":154},[115,3851,204],{"emptyLinePlaceholder":37},[115,3853,3854],{"class":117,"line":160},[115,3855,3856],{},"    subgraph ESP [\"ESP8266 Firmware\"]\n",[115,3858,3859],{"class":117,"line":165},[115,3860,3861],{},"        Scheduler[\"TaskScheduler\u003Cbr\u002F>(Cooperative Event Loop)\"]\n",[115,3863,3864],{"class":117,"line":171},[115,3865,204],{"emptyLinePlaceholder":37},[115,3867,3868],{"class":117,"line":177},[115,3869,3870],{},"        subgraph Subsystems [\"Core Modules\"]\n",[115,3872,3873],{"class":117,"line":183},[115,3874,3875],{},"            Conn[\"Connectivity\u003Cbr\u002F>(TLS \u002F NTP \u002F AWS)\"]\n",[115,3877,3878],{"class":117,"line":189},[115,3879,3880],{},"            Cmd[\"Commander\u003Cbr\u002F>(Input Handler)\"]\n",[115,3882,3883],{"class":117,"line":195},[115,3884,3885],{},"            Motor[\"Motor Control\u003Cbr\u002F>(State Machine)\"]\n",[115,3887,3888],{"class":117,"line":201},[115,3889,3890],{},"            Settings[\"Settings Store\u003Cbr\u002F>(Dual-Sector EEPROM)\"]\n",[115,3892,3893],{"class":117,"line":207},[115,3894,3895],{},"        end\n",[115,3897,3898],{"class":117,"line":213},[115,3899,198],{},[115,3901,3902],{"class":117,"line":219},[115,3903,204],{"emptyLinePlaceholder":37},[115,3905,3906],{"class":117,"line":225},[115,3907,3908],{},"    subgraph Actuators [\"Hardware\"]\n",[115,3910,3911],{"class":117,"line":231},[115,3912,3913],{},"        Drive[\"H-Bridge Driver\u003Cbr\u002F>& Geared DC Motor\"]\n",[115,3915,3916],{"class":117,"line":236},[115,3917,3918],{},"        Sensor[\"Hall Effect Sensor\u003Cbr\u002F>(Rotation Counter)\"]\n",[115,3920,3921],{"class":117,"line":242},[115,3922,198],{},[115,3924,3925],{"class":117,"line":247},[115,3926,204],{"emptyLinePlaceholder":37},[115,3928,3929],{"class":117,"line":252},[115,3930,3931],{},"    %% Flow Connections\n",[115,3933,3934],{"class":117,"line":258},[115,3935,3936],{},"    Cloud \u003C-->|MQTT| Conn\n",[115,3938,3939],{"class":117,"line":263},[115,3940,3941],{},"    WebUI -->|Config| Settings\n",[115,3943,3944],{"class":117,"line":269},[115,3945,3946],{},"    Buttons -->|Interrupts| Cmd\n",[115,3948,3949],{"class":117,"line":275},[115,3950,204],{"emptyLinePlaceholder":37},[115,3952,3953],{"class":117,"line":281},[115,3954,3955],{},"    Scheduler --> Conn\n",[115,3957,3958],{"class":117,"line":286},[115,3959,3960],{},"    Scheduler --> Cmd\n",[115,3962,3963],{"class":117,"line":292},[115,3964,3965],{},"    Scheduler --> Motor\n",[115,3967,3968],{"class":117,"line":297},[115,3969,204],{"emptyLinePlaceholder":37},[115,3971,3972],{"class":117,"line":302},[115,3973,3974],{},"    Cmd --> Motor\n",[115,3976,3977],{"class":117,"line":308},[115,3978,3979],{},"    Conn \u003C--> Motor\n",[115,3981,3982],{"class":117,"line":313},[115,3983,204],{"emptyLinePlaceholder":37},[115,3985,3986],{"class":117,"line":319},[115,3987,3988],{},"    Motor -->|PWM Acceleration| Drive\n",[115,3990,3991],{"class":117,"line":325},[115,3992,3993],{},"    Sensor -->|Analog Feedback| Motor\n",[115,3995,3996],{"class":117,"line":331},[115,3997,3998],{},"    Motor -->|Save Position| Settings\n",[3248,4000,4002],{"id":4001},"adding-cooperative-multitasking","Adding cooperative multitasking",[10,4004,4005,4006,4011,4012,4017],{},"The ESP8266 runs on a single 80 MHz core with limited resources (~80 KB of available RAM).\nDespite that, it had to manage the motor, maintain its cloud connection, and remain responsive to physical controls.\nA single monolithic loop would quickly become difficult to manage, and integrating the ",[2960,4007,4010],{"href":4008,"rel":4009},"https:\u002F\u002Fgithub.com\u002Farkhipenko\u002FTaskScheduler",[2964],"TaskScheduler library"," kept the different tasks responsive while leaving the main loop ",[2960,4013,4016],{"href":4014,"rel":4015},"https:\u002F\u002Fgithub.com\u002Fvaskevich\u002Fshado\u002Fblob\u002Fb0f8df77ad48371fb17153f9f9d06d4fd0e58b42\u002Ffirmware\u002Ffirmware.ino#L51",[2964],"dead simple",":",[105,4019,4023],{"className":4020,"code":4021,"language":4022,"meta":110,"style":110},"language-cpp shiki shiki-themes github-dark","void loop() { runner.execute(); }\n","cpp",[112,4024,4025],{"__ignoreMap":110},[115,4026,4027,4031,4035,4039,4042],{"class":117,"line":118},[115,4028,4030],{"class":4029},"snl16","void",[115,4032,4034],{"class":4033},"svObZ"," loop",[115,4036,4038],{"class":4037},"s95oV","() { runner.",[115,4040,4041],{"class":4033},"execute",[115,4043,4044],{"class":4037},"(); }\n",[3248,4046,4048],{"id":4047},"controlling-and-tracking-the-shades","Controlling and tracking the shades",[10,4050,4051],{},"At the end of the day, the controller's primary function was to operate the motor, so getting that working was my first order of business.",[10,4053,4054,4055,4058,4059,4064],{},"While the Hall-effect sensor provided the raw signal, it wasn't enough to just read the ",[112,4056,4057],{},"A0"," analog pin and compare it against a fixed threshold.\nIn practice, I found the magnetic field strength to be quite sensitive, and to make the encoder more robust I implemented ",[2960,4060,4063],{"href":4061,"rel":4062},"https:\u002F\u002Fgithub.com\u002Fvaskevich\u002Fshado\u002Fblob\u002Fb0f8df77ad48371fb17153f9f9d06d4fd0e58b42\u002Ffirmware\u002Fmotor.cpp#L78-L91",[2964],"self-calibration",". Here's how it worked on initial setup:",[430,4066,4067,4070,4073],{},[76,4068,4069],{},"The firmware runs the motor for 60 steps while recording the minimum and maximum 10-bit ADC values from the sensor.",[76,4071,4072],{},"To calculate hysteresis, it sets the detection threshold to 1\u002F3rd of the way between those minimum and maximum values.",[76,4074,4075],{},"To complete alignment, the motor reverses to the baseline position, and the firmware saves the calibrated threshold to EEPROM.",[105,4077,4079],{"className":4020,"code":4078,"language":4022,"meta":110,"style":110},"void Motor::CalibrateFindSensorRange() {\n  if (calibration_counter_++ \u003C kCalibrationSteps) {\n    unsigned int encoder_value = analogRead(HALL_EFFECT_SENSOR_PIN);\n    calibration_min_ = std::min(calibration_min_, encoder_value);\n    calibration_max_ = std::max(calibration_max_, encoder_value);\n    return;\n  }\n\n  \u002F\u002F Calculate dynamic threshold\n  encoder_threshold_ = calibration_min_ + (calibration_max_ - calibration_min_) \u002F 3;\n  state_ = MotorState::kCalibratingAligningMotor;\n}\n",[112,4080,4081,4097,4114,4133,4150,4167,4175,4180,4184,4190,4220,4233],{"__ignoreMap":110},[115,4082,4083,4085,4088,4091,4094],{"class":117,"line":118},[115,4084,4030],{"class":4029},[115,4086,4087],{"class":4033}," Motor",[115,4089,4090],{"class":4037},"::",[115,4092,4093],{"class":4033},"CalibrateFindSensorRange",[115,4095,4096],{"class":4037},"() {\n",[115,4098,4099,4102,4105,4108,4111],{"class":117,"line":124},[115,4100,4101],{"class":4029},"  if",[115,4103,4104],{"class":4037}," (calibration_counter_",[115,4106,4107],{"class":4029},"++",[115,4109,4110],{"class":4029}," \u003C",[115,4112,4113],{"class":4037}," kCalibrationSteps) {\n",[115,4115,4116,4119,4122,4125,4127,4130],{"class":117,"line":130},[115,4117,4118],{"class":4029},"    unsigned",[115,4120,4121],{"class":4029}," int",[115,4123,4124],{"class":4037}," encoder_value ",[115,4126,905],{"class":4029},[115,4128,4129],{"class":4033}," analogRead",[115,4131,4132],{"class":4037},"(HALL_EFFECT_SENSOR_PIN);\n",[115,4134,4135,4138,4140,4143,4145,4147],{"class":117,"line":136},[115,4136,4137],{"class":4037},"    calibration_min_ ",[115,4139,905],{"class":4029},[115,4141,4142],{"class":4033}," std",[115,4144,4090],{"class":4037},[115,4146,1256],{"class":4033},[115,4148,4149],{"class":4037},"(calibration_min_, encoder_value);\n",[115,4151,4152,4155,4157,4159,4161,4164],{"class":117,"line":142},[115,4153,4154],{"class":4037},"    calibration_max_ ",[115,4156,905],{"class":4029},[115,4158,4142],{"class":4033},[115,4160,4090],{"class":4037},[115,4162,4163],{"class":4033},"max",[115,4165,4166],{"class":4037},"(calibration_max_, encoder_value);\n",[115,4168,4169,4172],{"class":117,"line":148},[115,4170,4171],{"class":4029},"    return",[115,4173,4174],{"class":4037},";\n",[115,4176,4177],{"class":117,"line":154},[115,4178,4179],{"class":4037},"  }\n",[115,4181,4182],{"class":117,"line":160},[115,4183,204],{"emptyLinePlaceholder":37},[115,4185,4186],{"class":117,"line":165},[115,4187,4189],{"class":4188},"sAwPA","  \u002F\u002F Calculate dynamic threshold\n",[115,4191,4192,4195,4197,4200,4203,4206,4209,4212,4214,4218],{"class":117,"line":171},[115,4193,4194],{"class":4037},"  encoder_threshold_ ",[115,4196,905],{"class":4029},[115,4198,4199],{"class":4037}," calibration_min_ ",[115,4201,4202],{"class":4029},"+",[115,4204,4205],{"class":4037}," (calibration_max_ ",[115,4207,4208],{"class":4029},"-",[115,4210,4211],{"class":4037}," calibration_min_) ",[115,4213,792],{"class":4029},[115,4215,4217],{"class":4216},"sDLfK"," 3",[115,4219,4174],{"class":4037},[115,4221,4222,4225,4227,4230],{"class":117,"line":177},[115,4223,4224],{"class":4037},"  state_ ",[115,4226,905],{"class":4029},[115,4228,4229],{"class":4033}," MotorState",[115,4231,4232],{"class":4037},"::kCalibratingAligningMotor;\n",[115,4234,4235],{"class":117,"line":183},[115,4236,4237],{"class":4037},"}\n",[10,4239,4240,4241,4243],{},"During operation, the motor task polls ",[112,4242,4057],{}," every 50 ms, tracking rising threshold crossings and combining them with the motor direction to estimate the shade's current position.",[3248,4245,4247],{"id":4246},"refinements","Refinements",[10,4249,4250,4251,4256],{},"One practical refinement I had to make was handling motor acceleration. Starting the heavy worm gear motor at 100% duty cycle produced sudden torque spikes that wore down the drive wheel, so I updated the firmware to ",[2960,4252,4255],{"href":4253,"rel":4254},"https:\u002F\u002Fgithub.com\u002Fvaskevich\u002Fshado\u002Fblob\u002Fb0f8df77ad48371fb17153f9f9d06d4fd0e58b42\u002Ffirmware\u002Fmotor.cpp#L182-L188",[2964],"ramp up"," the PWM duty cycle gradually, reducing the mechanical shock at startup.",[10,4258,4259,4260,4265],{},"Another refinement was reducing flash wear. Microcontroller flash has finite write endurance, and an erase operation affects an entire 4 KB sector, so frequently updated values need to be stored carefully. The Wi-Fi X.509 certificates and private keys for AWS are large and seldom changed compared with values such as motor position and calibration data. I therefore stored the latter ",[2960,4261,4264],{"href":4262,"rel":4263},"https:\u002F\u002Fgithub.com\u002Fvaskevich\u002Fshado\u002Fblob\u002Fb0f8df77ad48371fb17153f9f9d06d4fd0e58b42\u002Ffirmware\u002Fsettings.h#L58-L62",[2964],"separately"," with rotation-based partitioning to reduce writes.",[3248,4267,4269],{"id":4268},"provisioning-wi-fi-credentials","Provisioning Wi-Fi credentials",[10,4271,4272,4273,4280],{},"Connecting the ESP8266 to a hardcoded Wi-Fi network is quite easy, but I set out to make a complete end-to-end onboarding flow.\nThe idea was that an end user would visit a website (and perhaps eventually use an app) that would guide them through connecting the device to Wi-Fi and pairing it with their account.\nTo support that flow, the device started in access-point mode and hosted a small web server with a few endpoints (see ",[2960,4274,4277],{"href":4275,"rel":4276},"https:\u002F\u002Fgithub.com\u002Fvaskevich\u002Fshado\u002Fblob\u002Fmain\u002Ffirmware\u002Fconfig_portal.cpp",[2964],[112,4278,4279],{},"ConfigPortal",").",[4282,4283,4284,4304],"table",{},[4285,4286,4287],"thead",{},[4288,4289,4290,4295,4298,4301],"tr",{},[4291,4292,4294],"th",{"align":4293},"left","Method",[4291,4296,4297],{"align":4293},"Endpoint",[4291,4299,4300],{"align":4293},"Description",[4291,4302,4303],{"align":4293},"Payload \u002F Response",[4305,4306,4307,4332,4353,4389,4415],"tbody",{},[4288,4308,4309,4315,4319,4322],{},[4310,4311,4312],"td",{"align":4293},[112,4313,4314],{},"GET",[4310,4316,4317],{"align":4293},[112,4318,792],{},[4310,4320,4321],{"align":4293},"Captive Portal Landing Page",[4310,4323,4324,4325,4328,4329,3231],{"align":4293},"Returns the captive HTML page (",[112,4326,4327],{},"kIndexContent","). Redirects foreign host requests to ",[112,4330,4331],{},"http:\u002F\u002Fshado.local",[4288,4333,4334,4338,4343,4346],{},[4310,4335,4336],{"align":4293},[112,4337,4314],{},[4310,4339,4340],{"align":4293},[112,4341,4342],{},"\u002Fshado_info",[4310,4344,4345],{"align":4293},"Device & Wi-Fi Scanner",[4310,4347,4348,4349,4352],{"align":4293},"Triggers ",[112,4350,4351],{},"WiFi.scanNetworks()"," and returns JSON containing the device MAC address and nearby SSIDs.",[4288,4354,4355,4360,4365,4368],{},[4310,4356,4357],{"align":4293},[112,4358,4359],{},"POST",[4310,4361,4362],{"align":4293},[112,4363,4364],{},"\u002Fshado_config",[4310,4366,4367],{"align":4293},"Submit Credentials",[4310,4369,4370,4371,4374,4375,4378,4379,4374,4382,4374,4385,4388],{"align":4293},"Accepts Wi-Fi credentials (",[112,4372,4373],{},"ssid",", ",[112,4376,4377],{},"pw",") and AWS IoT parameters (",[112,4380,4381],{},"ep",[112,4383,4384],{},"cert_crt",[112,4386,4387],{},"cert_pk","), saves them to EEPROM, and shuts down AP mode.",[4288,4390,4391,4396,4401,4404],{},[4310,4392,4393],{"align":4293},[112,4394,4395],{},"OPTIONS",[4310,4397,4398],{"align":4293},[112,4399,4400],{},"*",[4310,4402,4403],{"align":4293},"CORS Preflight",[4310,4405,4406,4407,4410,4411,4414],{"align":4293},"Handles cross-origin requests from web-based setup apps (e.g., ",[112,4408,4409],{},"shado.osv.im","), returning ",[112,4412,4413],{},"204 No Content"," with CORS headers.",[4288,4416,4417,4421,4426,4429],{},[4310,4418,4419],{"align":4293},[112,4420,4400],{},[4310,4422,4423],{"align":4293},[112,4424,4425],{},"NotFound",[4310,4427,4428],{"align":4293},"Captive Redirect \u002F 404",[4310,4430,4431,4432,4435,4436,3231],{"align":4293},"Intercepts operating system captive-portal probe URLs and redirects (",[112,4433,4434],{},"302",") to ",[112,4437,4331],{},[10,4439,4440],{},"With the controller connected to Wi-Fi, it was time to tackle the next part of the puzzle: the Internet of Things.",[65,4442,4444],{"id":4443},"connecting-it-to-the-cloud","Connecting it to the cloud",[10,4446,4447,4448,4453],{},"Going into this project, I wanted more exposure to AWS. Building a web service that could manage accounts, devices, commands, and state ultimately required a fair amount of infrastructure.\nBy the time I was done with the ",[2960,4449,4452],{"href":4450,"rel":4451},"https:\u002F\u002Fgithub.com\u002Fvaskevich\u002Fshado\u002Ftree\u002Fmain\u002Fwebservice\u002Finfra",[2964],"Terraform config",", it looked something like this:",[105,4455,4457],{"className":107,"code":4456,"language":109,"meta":110,"style":110},"flowchart TD\n    subgraph UX [\"1. User Interfaces & Integrations\"]\n        direction LR\n        WEB[\"Web Dashboard\u003Cbr\u002F>(React SPA on S3 + CloudFront)\"]\n        GOOGLE[\"Google Assistant\u003Cbr\u002F>(Google Home Actions)\"]\n    end\n\n    subgraph AUTH [\"2. Authentication\"]\n        COG[\"AWS Cognito\u003Cbr\u002F>(Google OAuth 2.0)\"]\n    end\n\n    subgraph BACKEND [\"3. Serverless API\"]\n        APIGW[\"AWS API Gateway v2\"]\n        LAMBDA[\"AWS Lambda Backend\u003Cbr\u002F>(Node.js Handler)\"]\n\n        APIGW --> LAMBDA\n    end\n\n    subgraph DATA [\"4. Persistence & IoT Messaging\"]\n        DYNAMO[(\"Amazon DynamoDB\u003Cbr\u002F>Devices Table\")]\n        IOT[\"AWS IoT Core\u003Cbr\u002F>(MQTT Broker & Device Shadow)\"]\n    end\n\n    subgraph HW [\"5. Hardware\"]\n        ESP[\"ESP8266 Controller\u003Cbr\u002F>(Shado Firmware)\"]\n    end\n\n    %% Flow Connections\n    WEB -->|\"Authenticate\"| COG\n    GOOGLE -->|\"OAuth Link\"| COG\n\n    WEB -->|\"HTTPS API Requests\"| APIGW\n    GOOGLE -->|\"Smart Home Intents\"| APIGW\n    APIGW -.->|\"Validate JWT\"| COG\n\n    LAMBDA \u003C-->|\"Read \u002F Write Mappings\"| DYNAMO\n    LAMBDA \u003C-->|\"Publish MQTT & Update Shadow\"| IOT\n\n    ESP \u003C-->|\"MQTT over TLS (Port 8883)\"| IOT\n\n    %% Color Styling\n    classDef uxStyle fill:#0d2538,stroke:#38bdf8,color:#f0f9ff,stroke-width:2px;\n    classDef authStyle fill:#451a03,stroke:#fb923c,color:#fff7ed,stroke-width:2px;\n    classDef apiStyle fill:#3b0764,stroke:#c084fc,color:#faf5ff,stroke-width:2px;\n    classDef dataStyle fill:#064e3b,stroke:#34d399,color:#ecfdf5,stroke-width:2px;\n    classDef hwStyle fill:#1c1917,stroke:#a8a29e,color:#f5f5f4,stroke-width:2px;\n\n    class WEB,GOOGLE uxStyle;\n    class COG authStyle;\n    class APIGW,LAMBDA apiStyle;\n    class DYNAMO,IOT dataStyle;\n    class ESP hwStyle;\n",[112,4458,4459,4463,4468,4472,4477,4482,4486,4490,4495,4500,4504,4508,4513,4518,4523,4527,4532,4536,4540,4545,4550,4555,4559,4563,4568,4573,4577,4581,4585,4590,4595,4599,4604,4609,4614,4618,4623,4628,4632,4637,4641,4646,4651,4656,4661,4666,4671,4675,4680,4685,4690,4695],{"__ignoreMap":110},[115,4460,4461],{"class":117,"line":118},[115,4462,168],{},[115,4464,4465],{"class":117,"line":124},[115,4466,4467],{},"    subgraph UX [\"1. User Interfaces & Integrations\"]\n",[115,4469,4470],{"class":117,"line":130},[115,4471,216],{},[115,4473,4474],{"class":117,"line":136},[115,4475,4476],{},"        WEB[\"Web Dashboard\u003Cbr\u002F>(React SPA on S3 + CloudFront)\"]\n",[115,4478,4479],{"class":117,"line":142},[115,4480,4481],{},"        GOOGLE[\"Google Assistant\u003Cbr\u002F>(Google Home Actions)\"]\n",[115,4483,4484],{"class":117,"line":148},[115,4485,198],{},[115,4487,4488],{"class":117,"line":154},[115,4489,204],{"emptyLinePlaceholder":37},[115,4491,4492],{"class":117,"line":160},[115,4493,4494],{},"    subgraph AUTH [\"2. Authentication\"]\n",[115,4496,4497],{"class":117,"line":165},[115,4498,4499],{},"        COG[\"AWS Cognito\u003Cbr\u002F>(Google OAuth 2.0)\"]\n",[115,4501,4502],{"class":117,"line":171},[115,4503,198],{},[115,4505,4506],{"class":117,"line":177},[115,4507,204],{"emptyLinePlaceholder":37},[115,4509,4510],{"class":117,"line":183},[115,4511,4512],{},"    subgraph BACKEND [\"3. Serverless API\"]\n",[115,4514,4515],{"class":117,"line":189},[115,4516,4517],{},"        APIGW[\"AWS API Gateway v2\"]\n",[115,4519,4520],{"class":117,"line":195},[115,4521,4522],{},"        LAMBDA[\"AWS Lambda Backend\u003Cbr\u002F>(Node.js Handler)\"]\n",[115,4524,4525],{"class":117,"line":201},[115,4526,204],{"emptyLinePlaceholder":37},[115,4528,4529],{"class":117,"line":207},[115,4530,4531],{},"        APIGW --> LAMBDA\n",[115,4533,4534],{"class":117,"line":213},[115,4535,198],{},[115,4537,4538],{"class":117,"line":219},[115,4539,204],{"emptyLinePlaceholder":37},[115,4541,4542],{"class":117,"line":225},[115,4543,4544],{},"    subgraph DATA [\"4. Persistence & IoT Messaging\"]\n",[115,4546,4547],{"class":117,"line":231},[115,4548,4549],{},"        DYNAMO[(\"Amazon DynamoDB\u003Cbr\u002F>Devices Table\")]\n",[115,4551,4552],{"class":117,"line":236},[115,4553,4554],{},"        IOT[\"AWS IoT Core\u003Cbr\u002F>(MQTT Broker & Device Shadow)\"]\n",[115,4556,4557],{"class":117,"line":242},[115,4558,198],{},[115,4560,4561],{"class":117,"line":247},[115,4562,204],{"emptyLinePlaceholder":37},[115,4564,4565],{"class":117,"line":252},[115,4566,4567],{},"    subgraph HW [\"5. Hardware\"]\n",[115,4569,4570],{"class":117,"line":258},[115,4571,4572],{},"        ESP[\"ESP8266 Controller\u003Cbr\u002F>(Shado Firmware)\"]\n",[115,4574,4575],{"class":117,"line":263},[115,4576,198],{},[115,4578,4579],{"class":117,"line":269},[115,4580,204],{"emptyLinePlaceholder":37},[115,4582,4583],{"class":117,"line":275},[115,4584,3931],{},[115,4586,4587],{"class":117,"line":281},[115,4588,4589],{},"    WEB -->|\"Authenticate\"| COG\n",[115,4591,4592],{"class":117,"line":286},[115,4593,4594],{},"    GOOGLE -->|\"OAuth Link\"| COG\n",[115,4596,4597],{"class":117,"line":292},[115,4598,204],{"emptyLinePlaceholder":37},[115,4600,4601],{"class":117,"line":297},[115,4602,4603],{},"    WEB -->|\"HTTPS API Requests\"| APIGW\n",[115,4605,4606],{"class":117,"line":302},[115,4607,4608],{},"    GOOGLE -->|\"Smart Home Intents\"| APIGW\n",[115,4610,4611],{"class":117,"line":308},[115,4612,4613],{},"    APIGW -.->|\"Validate JWT\"| COG\n",[115,4615,4616],{"class":117,"line":313},[115,4617,204],{"emptyLinePlaceholder":37},[115,4619,4620],{"class":117,"line":319},[115,4621,4622],{},"    LAMBDA \u003C-->|\"Read \u002F Write Mappings\"| DYNAMO\n",[115,4624,4625],{"class":117,"line":325},[115,4626,4627],{},"    LAMBDA \u003C-->|\"Publish MQTT & Update Shadow\"| IOT\n",[115,4629,4630],{"class":117,"line":331},[115,4631,204],{"emptyLinePlaceholder":37},[115,4633,4634],{"class":117,"line":336},[115,4635,4636],{},"    ESP \u003C-->|\"MQTT over TLS (Port 8883)\"| IOT\n",[115,4638,4639],{"class":117,"line":342},[115,4640,204],{"emptyLinePlaceholder":37},[115,4642,4643],{"class":117,"line":347},[115,4644,4645],{},"    %% Color Styling\n",[115,4647,4648],{"class":117,"line":352},[115,4649,4650],{},"    classDef uxStyle fill:#0d2538,stroke:#38bdf8,color:#f0f9ff,stroke-width:2px;\n",[115,4652,4653],{"class":117,"line":358},[115,4654,4655],{},"    classDef authStyle fill:#451a03,stroke:#fb923c,color:#fff7ed,stroke-width:2px;\n",[115,4657,4658],{"class":117,"line":364},[115,4659,4660],{},"    classDef apiStyle fill:#3b0764,stroke:#c084fc,color:#faf5ff,stroke-width:2px;\n",[115,4662,4663],{"class":117,"line":370},[115,4664,4665],{},"    classDef dataStyle fill:#064e3b,stroke:#34d399,color:#ecfdf5,stroke-width:2px;\n",[115,4667,4668],{"class":117,"line":375},[115,4669,4670],{},"    classDef hwStyle fill:#1c1917,stroke:#a8a29e,color:#f5f5f4,stroke-width:2px;\n",[115,4672,4673],{"class":117,"line":381},[115,4674,204],{"emptyLinePlaceholder":37},[115,4676,4677],{"class":117,"line":387},[115,4678,4679],{},"    class WEB,GOOGLE uxStyle;\n",[115,4681,4682],{"class":117,"line":393},[115,4683,4684],{},"    class COG authStyle;\n",[115,4686,4687],{"class":117,"line":399},[115,4688,4689],{},"    class APIGW,LAMBDA apiStyle;\n",[115,4691,4692],{"class":117,"line":404},[115,4693,4694],{},"    class DYNAMO,IOT dataStyle;\n",[115,4696,4697],{"class":117,"line":410},[115,4698,4699],{},"    class ESP hwStyle;\n",[10,4701,4702],{},"To share a few details on infrastructure choices:",[73,4704,4705,4711,4721,4727],{},[76,4706,4707,4710],{},[60,4708,4709],{},"100% serverless:"," At least initially, I expected low utilization: each shade would open or close only a few times per day. Using Lambda, API Gateway, and CloudFront kept the monthly infrastructure bill close to $0.",[76,4712,4713,4716,4717,4720],{},[60,4714,4715],{},"NoSQL:"," DynamoDB's ",[112,4718,4719],{},"PAY_PER_REQUEST"," model fit the sparse device-to-user mapping workload and required no database maintenance.",[76,4722,4723,4726],{},[60,4724,4725],{},"AWS IoT Core:"," Persistent MQTT avoided polling, while Device Shadows separated the requested state from the state eventually reported by the device.",[76,4728,4729,4732],{},[60,4730,4731],{},"AWS Cognito:"," Although many libraries make it possible to build authentication yourself, Cognito handled session management and provided an OAuth flow for Google Home account linking.",[10,4734,4735],{},"I wouldn't necessarily make all the same choices for a long-lived product, but they let me explore AWS while keeping both development time and operating costs low.",[3248,4737,4739],{"id":4738},"registering-a-device","Registering a device",[10,4741,4742],{},"With the infrastructure in place, it was time to get the entire onboarding flow working end-to-end.\nEarlier on, I decided not to take shortcuts by hardcoding Wi-Fi credentials and, perhaps to my own detriment, I knew I wasn't about to do the same with AWS IoT Core.",[10,4744,4745,4746,4751,4752,4757,4758,4761],{},"AWS IoT Core handles authentication using client certificates, and a common pattern is for devices to come preinstalled with a certificate.\nAn alternative is to leverage ",[2960,4747,4750],{"href":4748,"rel":4749},"https:\u002F\u002Fdocs.aws.amazon.com\u002Fiot\u002Flatest\u002Fdeveloperguide\u002Fprovision-wo-cert.html",[2964],"AWS IoT fleet provisioning",", which enables just-in-time registration.\nThis was a new pattern at the time, and as part of this, I ",[2960,4753,4756],{"href":4754,"rel":4755},"https:\u002F\u002Fgithub.com\u002Fhashicorp\u002Fterraform-provider-aws\u002Fpull\u002F12108",[2964],"contributed"," the ",[112,4759,4760],{},"aws_iot_provisioning_template"," resource to the AWS Terraform provider.",[17,4763,19,4764,19,4768],{},[21,4765],{"src":4766,"alt":4767},"\u002Fimages\u002Fwriting\u002Fbuilding-my-own-smart-roller-shades\u002Fwebservice_onboarding.png","Shado web onboarding screen prompting the user to connect to the device",[26,4769,4770],{},"After signing in via Google, the onboarding flow starts by asking the user to connect to the device.",[10,4772,4773,4774,4781],{},"The details are in ",[2960,4775,4778],{"href":4776,"rel":4777},"https:\u002F\u002Fgithub.com\u002Fvaskevich\u002Fshado\u002Fblob\u002Fmain\u002Ffirmware\u002Fiot_client.cpp",[2964],[112,4779,4780],{},"iot_client.cpp",", but at a high level:",[430,4783,4784,4787,4794,4802,4809,4824],{},[76,4785,4786],{},"The user signs up through the web portal and enters their Wi-Fi credentials.",[76,4788,4789,4790,4793],{},"The web app calls ",[112,4791,4792],{},"POST \u002Fprovision_device"," through API Gateway, which creates a short-lived provisioning claim certificate.",[76,4795,4796,4797,4799,4800,3231],{},"The web app asks the user to connect to the device's access-point network, then ",[112,4798,4359],{},"s the credentials and claim to ",[112,4801,4364],{},[76,4803,4804,4805,4808],{},"The device restarts, connects to Wi-Fi, and synchronizes its clock through NTP. It then connects to AWS IoT Core over MQTT and exchanges the claim for a permanent certificate through the ",[112,4806,4807],{},"$aws\u002Fcertificates\u002Fcreate\u002Fjson"," topic.",[76,4810,4811,4812,4815,4816,4819,4820,4823],{},"The device publishes its serial number to ",[112,4813,4814],{},"$aws\u002Fprovisioning-templates\u002Fshado_fleet\u002Fprovision\u002Fjson",". AWS IoT Core then executes the ",[112,4817,4818],{},"shado_fleet"," template, registers an IoT Thing, attaches the ",[112,4821,4822],{},"ShadoDevicePolicy",", and links it to the Cognito principal.",[76,4825,4826,4827,4830],{},"On subsequent boots, the device connects using its permanent certificate, updates its shadow to ",[112,4828,4829],{},"online: true",", and listens for commands.",[4832,4833,4834,4838],"details",{},[4835,4836,4837],"summary",{},"Here's the full sequence diagram",[105,4839,4841],{"className":107,"code":4840,"language":109,"meta":3816,"style":110},"sequenceDiagram\n    autonumber\n    actor User as User & Web App (shado.osv.im)\n    participant HW as ESP8266 Firmware (SoftAP)\n    participant API as AWS Lambda Backend\n    participant AWS as AWS IoT Core (Fleet Provisioning)\n\n    rect rgb(20, 30, 45)\n        note over User, HW: Phase 1: Local Discovery\n        User->>HW: Connect to Wi-Fi \"Shado-XXXX\"\n        User->>HW: GET http:\u002F\u002F192.168.4.1\u002Fshado_info\n        HW-->>User: Discovered Wi-Fi SSIDs & Serial Number (Chip ID)\n    end\n\n    rect rgb(35, 20, 45)\n        note over User, AWS: Phase 2: Claim Request\n        User->>API: POST api.shado.osv.im\u002Fprovision_device (JWT Auth)\n        API->>AWS: iot.createProvisioningClaim(shado_fleet)\n        AWS-->>API: Temporary Claim Cert, Key & Endpoint (5 min expiry)\n        API-->>User: Return Claim Credentials Payload\n    end\n\n    rect rgb(20, 45, 30)\n        note over User, HW: Phase 3: Push Credentials to Hardware\n        User->>HW: POST http:\u002F\u002F192.168.4.1\u002Fshado_config (Wi-Fi + AWS Claim Cert)\n        HW->>HW: Save credentials to EEPROM & Shutdown SoftAP\n    end\n\n    rect rgb(45, 35, 20)\n        note over HW, AWS: Phase 4: Dynamic Certificate & Thing Creation\n        HW->>HW: Connect to Home Wi-Fi STA & Sync NTP Clock\n        HW->>AWS: MQTT Connect via Claim Cert ➔ Publish $aws\u002Fcertificates\u002Fcreate\u002Fjson\n        AWS-->>HW: Issue New Permanent X.509 Cert & Private Key\n        HW->>AWS: Publish $aws\u002Fprovisioning-templates\u002Fshado_fleet\u002Fprovision\u002Fjson\n        AWS->>AWS: Create Thing \"ShadoController_XXXX\", Attach Policy & User Identity\n        AWS-->>HW: Return permanent thingName\n        HW->>HW: Save thingName, Mark Provisioned, & ESP.restart()\n    end\n\n    rect rgb(20, 30, 45)\n        note over HW, User: Phase 5: Normal Operational State\n        HW->>AWS: MQTT Connect via Permanent Cert ➔ Publish shadow state (online: true)\n        User->>API: GET \u002Fdevices\n        API-->>User: List device \"ShadoController_XXXX\" (Ready for control!)\n    end\n",[112,4842,4843,4848,4853,4858,4863,4868,4873,4877,4882,4887,4892,4897,4902,4906,4910,4915,4920,4925,4930,4935,4940,4944,4948,4953,4958,4963,4968,4972,4976,4981,4986,4991,4996,5001,5006,5011,5016,5021,5025,5029,5033,5038,5043,5048,5053],{"__ignoreMap":110},[115,4844,4845],{"class":117,"line":118},[115,4846,4847],{},"sequenceDiagram\n",[115,4849,4850],{"class":117,"line":124},[115,4851,4852],{},"    autonumber\n",[115,4854,4855],{"class":117,"line":130},[115,4856,4857],{},"    actor User as User & Web App (shado.osv.im)\n",[115,4859,4860],{"class":117,"line":136},[115,4861,4862],{},"    participant HW as ESP8266 Firmware (SoftAP)\n",[115,4864,4865],{"class":117,"line":142},[115,4866,4867],{},"    participant API as AWS Lambda Backend\n",[115,4869,4870],{"class":117,"line":148},[115,4871,4872],{},"    participant AWS as AWS IoT Core (Fleet Provisioning)\n",[115,4874,4875],{"class":117,"line":154},[115,4876,204],{"emptyLinePlaceholder":37},[115,4878,4879],{"class":117,"line":160},[115,4880,4881],{},"    rect rgb(20, 30, 45)\n",[115,4883,4884],{"class":117,"line":165},[115,4885,4886],{},"        note over User, HW: Phase 1: Local Discovery\n",[115,4888,4889],{"class":117,"line":171},[115,4890,4891],{},"        User->>HW: Connect to Wi-Fi \"Shado-XXXX\"\n",[115,4893,4894],{"class":117,"line":177},[115,4895,4896],{},"        User->>HW: GET http:\u002F\u002F192.168.4.1\u002Fshado_info\n",[115,4898,4899],{"class":117,"line":183},[115,4900,4901],{},"        HW-->>User: Discovered Wi-Fi SSIDs & Serial Number (Chip ID)\n",[115,4903,4904],{"class":117,"line":189},[115,4905,198],{},[115,4907,4908],{"class":117,"line":195},[115,4909,204],{"emptyLinePlaceholder":37},[115,4911,4912],{"class":117,"line":201},[115,4913,4914],{},"    rect rgb(35, 20, 45)\n",[115,4916,4917],{"class":117,"line":207},[115,4918,4919],{},"        note over User, AWS: Phase 2: Claim Request\n",[115,4921,4922],{"class":117,"line":213},[115,4923,4924],{},"        User->>API: POST api.shado.osv.im\u002Fprovision_device (JWT Auth)\n",[115,4926,4927],{"class":117,"line":219},[115,4928,4929],{},"        API->>AWS: iot.createProvisioningClaim(shado_fleet)\n",[115,4931,4932],{"class":117,"line":225},[115,4933,4934],{},"        AWS-->>API: Temporary Claim Cert, Key & Endpoint (5 min expiry)\n",[115,4936,4937],{"class":117,"line":231},[115,4938,4939],{},"        API-->>User: Return Claim Credentials Payload\n",[115,4941,4942],{"class":117,"line":236},[115,4943,198],{},[115,4945,4946],{"class":117,"line":242},[115,4947,204],{"emptyLinePlaceholder":37},[115,4949,4950],{"class":117,"line":247},[115,4951,4952],{},"    rect rgb(20, 45, 30)\n",[115,4954,4955],{"class":117,"line":252},[115,4956,4957],{},"        note over User, HW: Phase 3: Push Credentials to Hardware\n",[115,4959,4960],{"class":117,"line":258},[115,4961,4962],{},"        User->>HW: POST http:\u002F\u002F192.168.4.1\u002Fshado_config (Wi-Fi + AWS Claim Cert)\n",[115,4964,4965],{"class":117,"line":263},[115,4966,4967],{},"        HW->>HW: Save credentials to EEPROM & Shutdown SoftAP\n",[115,4969,4970],{"class":117,"line":269},[115,4971,198],{},[115,4973,4974],{"class":117,"line":275},[115,4975,204],{"emptyLinePlaceholder":37},[115,4977,4978],{"class":117,"line":281},[115,4979,4980],{},"    rect rgb(45, 35, 20)\n",[115,4982,4983],{"class":117,"line":286},[115,4984,4985],{},"        note over HW, AWS: Phase 4: Dynamic Certificate & Thing Creation\n",[115,4987,4988],{"class":117,"line":292},[115,4989,4990],{},"        HW->>HW: Connect to Home Wi-Fi STA & Sync NTP Clock\n",[115,4992,4993],{"class":117,"line":297},[115,4994,4995],{},"        HW->>AWS: MQTT Connect via Claim Cert ➔ Publish $aws\u002Fcertificates\u002Fcreate\u002Fjson\n",[115,4997,4998],{"class":117,"line":302},[115,4999,5000],{},"        AWS-->>HW: Issue New Permanent X.509 Cert & Private Key\n",[115,5002,5003],{"class":117,"line":308},[115,5004,5005],{},"        HW->>AWS: Publish $aws\u002Fprovisioning-templates\u002Fshado_fleet\u002Fprovision\u002Fjson\n",[115,5007,5008],{"class":117,"line":313},[115,5009,5010],{},"        AWS->>AWS: Create Thing \"ShadoController_XXXX\", Attach Policy & User Identity\n",[115,5012,5013],{"class":117,"line":319},[115,5014,5015],{},"        AWS-->>HW: Return permanent thingName\n",[115,5017,5018],{"class":117,"line":325},[115,5019,5020],{},"        HW->>HW: Save thingName, Mark Provisioned, & ESP.restart()\n",[115,5022,5023],{"class":117,"line":331},[115,5024,198],{},[115,5026,5027],{"class":117,"line":336},[115,5028,204],{"emptyLinePlaceholder":37},[115,5030,5031],{"class":117,"line":342},[115,5032,4881],{},[115,5034,5035],{"class":117,"line":347},[115,5036,5037],{},"        note over HW, User: Phase 5: Normal Operational State\n",[115,5039,5040],{"class":117,"line":352},[115,5041,5042],{},"        HW->>AWS: MQTT Connect via Permanent Cert ➔ Publish shadow state (online: true)\n",[115,5044,5045],{"class":117,"line":358},[115,5046,5047],{},"        User->>API: GET \u002Fdevices\n",[115,5049,5050],{"class":117,"line":364},[115,5051,5052],{},"        API-->>User: List device \"ShadoController_XXXX\" (Ready for control!)\n",[115,5054,5055],{"class":117,"line":370},[115,5056,198],{},[10,5058,5059,5060,5065,5066,3231],{},"To detect abrupt disconnects, the device publishes an MQTT ",[2960,5061,5064],{"href":5062,"rel":5063},"https:\u002F\u002Faws.amazon.com\u002Fblogs\u002Fiot\u002Fmonitor-aws-iot-connections-in-near-real-time-using-mqtt-lwt\u002F",[2964],"Last Will and Testament"," message. An AWS IoT rule consumes that message and updates the Device Shadow to ",[112,5067,5068],{},"online: false",[17,5070,19,5071,19,5075],{},[21,5072],{"src":5073,"alt":5074},"\u002Fimages\u002Fwriting\u002Fbuilding-my-own-smart-roller-shades\u002Fwebservice.png","Shado web control panel showing a registered device, controls, status, and position",[26,5076,5077],{},"The basic operational control panel used to manage devices, whose state is populated from AWS IoT Core. Note that the controls here route directly to MQTT, bypassing the Device Shadow for lower latency.",[3248,5079,5081],{"id":5080},"connecting-google-home","Connecting Google Home",[10,5083,5084,5085,5090],{},"The final milestone was integrating with Google Home and Assistant via ",[2960,5086,5089],{"href":5087,"rel":5088},"https:\u002F\u002Fdevelopers.home.google.com\u002Fcloud-to-cloud\u002Fget-started",[2964],"Cloud-to-cloud",".\nAfter going through a brief developer registration process, Shado could now appear under \"Works with Google\" in the Google Home app.\nSelecting it started the OAuth 2.0 Authorization Code flow, with Cognito handling most of the work.",[17,5092,19,5093,19,5099],{},[35,5094,40,5096,19],{"controls":37,"playsInline":37,"preload":38,"poster":5095},"\u002Fimages\u002Fwriting\u002Fbuilding-my-own-smart-roller-shades\u002Fgoogle_home_flow_poster.jpg",[42,5097],{"src":5098,"type":45},"\u002Fimages\u002Fwriting\u002Fbuilding-my-own-smart-roller-shades\u002Fgoogle_home_flow.mp4",[26,5100,5101],{},"Not quite Google Home certified, but getting there!",[3248,5103,5105],{"id":5104},"following-a-voice-command-end-to-end","Following a voice command end to end",[10,5107,5108,5109,5112,5113,5118],{},"With the account linked, the web service now had to react to events from Google Home.\nThe interface is elegant: Google Home calls a single ",[112,5110,5111],{},"POST \u002Fsmarthome"," endpoint ",[2960,5114,5117],{"href":5115,"rel":5116},"https:\u002F\u002Fgithub.com\u002Fvaskevich\u002Fshado\u002Fblob\u002Fmain\u002Fwebservice\u002Fapi\u002Fhandlers\u002Fsmarthome\u002Fsmarthome_handler.ts",[2964],"implemented here",", and the backend implements a few intents:",[4282,5120,5121,5134],{},[4285,5122,5123],{},[4288,5124,5125,5128,5131],{},[4291,5126,5127],{"align":4293},"Intent Name",[4291,5129,5130],{"align":4293},"How It's Initiated",[4291,5132,5133],{"align":4293},"What Happens",[4305,5135,5136,5169,5204],{},[4288,5137,5138,5152,5158],{},[4310,5139,5140,5145,5148],{"align":4293},[60,5141,5142],{},[112,5143,5144],{},"action.devices.INTENT.SYNC",[5146,5147],"br",{},[5149,5150,5151],"em",{},"(Discovery)",[4310,5153,5154,5155],{"align":4293},"User links their Shado account in the Google Home app or says ",[5149,5156,5157],{},"\"OK Google, sync my devices.\"",[4310,5159,5160,5161,5164,5165,5168],{"align":4293},"The backend queries the user's registered devices and returns device metadata (",[112,5162,5163],{},"BLINDS"," type with ",[112,5166,5167],{},"OpenClose"," trait), adding the shades to the Google Home app.",[4288,5170,5171,5183,5193],{},[4310,5172,5173,5178,5180],{"align":4293},[60,5174,5175],{},[112,5176,5177],{},"action.devices.INTENT.EXECUTE",[5146,5179],{},[5149,5181,5182],{},"(Control)",[4310,5184,5185,5186,4374,5189,5192],{"align":4293},"User gives a voice command (",[5149,5187,5188],{},"\"OK Google, open the blinds\"",[5149,5190,5191],{},"\"Set shades to 75%\"",") or taps controls in Google Home.",[4310,5194,5195,5196,5199,5200,5203],{"align":4293},"The backend parses the target ",[112,5197,5198],{},"openPercent"," and updates the AWS Device Shadow (",[112,5201,5202],{},"desired"," state). An MQTT delta message triggers the ESP8266 to spin the motor to the target position.",[4288,5205,5206,5218,5225],{},[4310,5207,5208,5213,5215],{"align":4293},[60,5209,5210],{},[112,5211,5212],{},"action.devices.INTENT.QUERY",[5146,5214],{},[5149,5216,5217],{},"(Status Check)",[4310,5219,5220,5221,5224],{"align":4293},"User asks a status question (",[5149,5222,5223],{},"\"OK Google, are the blinds open?\"",") or opens the device card in the Google Home app.",[4310,5226,5227,5228,5231,5232,5235],{"align":4293},"The backend reads ",[112,5229,5230],{},"getThingShadow()"," from AWS IoT Core and returns the current ",[112,5233,5234],{},"reported.openPercent"," position and online status to Google Assistant.",[10,5237,5238,5239,5244],{},"Getting this working wasn't difficult, and ",[2960,5240,5243],{"href":5241,"rel":5242},"https:\u002F\u002Fdevelopers.home.google.com\u002Fcodelabs\u002Fsmarthome-washer",[2964],"this codelab"," was a great reference.",[65,5246,5248],{"id":5247},"how-it-turned-out","How it turned out",[10,5250,5251],{},"This is a long write-up, so here is the video again:",[17,5253,19,5254,19,5258],{},[35,5255,40,5256,19],{"controls":37,"playsInline":37,"preload":38,"poster":39},[42,5257],{"src":44,"type":45},[26,5259,48,5260,53],{},[50,5261,52],{},[10,5263,5264],{},"By the end, I had a working end-to-end system. Looking back, it was a rewarding project that touched nearly every layer of the engineering stack:",[73,5266,5267,5273,5279,5285,5291],{},[76,5268,5269,5272],{},[60,5270,5271],{},"3D CAD & mechanics",": OpenSCAD modeling, drive-wheel design, and a wall-mounted enclosure",[76,5274,5275,5278],{},[60,5276,5277],{},"Electrical engineering",": custom PCB, L6203 motor driver, and Hall-effect rotation sensing",[76,5280,5281,5284],{},[60,5282,5283],{},"Embedded firmware",": cooperative C++ task scheduling, dynamic sensor calibration, access-point onboarding, and MQTT\u002FIoT",[76,5286,5287,5290],{},[60,5288,5289],{},"Cloud infrastructure",": Terraform IaC, API Gateway v2, Lambda, and IoT Core",[76,5292,5293,5296],{},[60,5294,5295],{},"Application and integrations",": AWS Cognito, a React dashboard, and Google Home",[10,5298,5299],{},"At the same time, I had a few setbacks on the way:",[73,5301,5302,5305,5308,5316],{},[76,5303,5304],{},"My 3D printer produced plenty of failed prints, and some of the larger parts took hours to reprint.",[76,5306,5307],{},"During IoT fleet provisioning, serializing a 4 KB certificate payload into a JSON object wreaked havoc on the ESP8266's limited heap and occasionally caused crashes.",[76,5309,5310,5311,3231],{},"My MQTT client also kept disconnecting — turns out the receive buffer was ",[2960,5312,5315],{"href":5313,"rel":5314},"https:\u002F\u002Fstackoverflow.com\u002Fquestions\u002F63448172\u002Fesp8266-failing-to-receive-message-over-mqtt-via-aws-iot-core",[2964],"too small",[76,5317,5318],{},"AWS Cognito didn't provide a server-side way to resolve an Identity Pool ID from a User Pool profile. As a result, Google Assistant requests could not look up a user's devices until I cached the Identity ID in a custom user attribute during web login.",[65,5320,5322],{"id":5321},"what-i-would-change","What I would change",[10,5324,5325],{},"While a working prototype was rewarding, there were a few notable issues:",[73,5327,5328,5331,5334],{},[76,5329,5330],{},"The enclosure did not keep the beaded chain perfectly aligned with the drive wheel, so over time, the lateral force gradually pulled the wheel along the shaft until it slipped off. A revised enclosure or a shaft collar could have kept it in place.",[76,5332,5333],{},"Although it concealed all the components, the enclosure was bulky. A redesigned PCB or a lower-profile motor could have reduced its footprint.",[76,5335,5336],{},"The motor was quite loud. A slower, quieter mode would have been useful for morning wakeups.",[10,5338,5339],{},"These days, I’d probably build around Matter, integrate with Home Assistant, and experiment with OpenClaw for natural-language control.",[65,5341,5343],{"id":5342},"source-code-and-project-files","Source code and project files",[10,5345,5346,5347,3231],{},"The full source code, PCB files, OpenSCAD models, firmware, and Terraform configuration are available in the ",[2960,5348,5351],{"href":5349,"rel":5350},"https:\u002F\u002Fgithub.com\u002Fvaskevich\u002Fshado",[2964],"Shado repository on GitHub",[5353,5354,5355],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}",{"title":110,"searchDepth":124,"depth":124,"links":5357},[5358,5359,5360,5361,5364,5374,5375,5381,5386,5387,5388],{"id":67,"depth":124,"text":68},{"id":96,"depth":124,"text":97},{"id":446,"depth":124,"text":447},{"id":3209,"depth":124,"text":3210,"children":5362},[5363],{"id":3250,"depth":130,"text":3251},{"id":3311,"depth":124,"text":3312,"children":5365},[5366,5367,5368,5369,5370,5371,5372,5373],{"id":3451,"depth":130,"text":3452},{"id":3485,"depth":130,"text":3486},{"id":3516,"depth":130,"text":3517},{"id":3541,"depth":130,"text":3542},{"id":3628,"depth":130,"text":3629},{"id":3687,"depth":130,"text":3688},{"id":3715,"depth":130,"text":3716},{"id":3731,"depth":130,"text":3732},{"id":3764,"depth":124,"text":3765},{"id":3808,"depth":124,"text":3809,"children":5376},[5377,5378,5379,5380],{"id":4001,"depth":130,"text":4002},{"id":4047,"depth":130,"text":4048},{"id":4246,"depth":130,"text":4247},{"id":4268,"depth":130,"text":4269},{"id":4443,"depth":124,"text":4444,"children":5382},[5383,5384,5385],{"id":4738,"depth":130,"text":4739},{"id":5080,"depth":130,"text":5081},{"id":5104,"depth":130,"text":5105},{"id":5247,"depth":124,"text":5248},{"id":5321,"depth":124,"text":5322},{"id":5342,"depth":124,"text":5343},"2026-08-06",null,"md","\u002Fimages\u002Fwriting\u002Fbuilding-my-own-smart-roller-shades\u002Fshado.jpg",{},"\u002Fwriting\u002Fbuilding-my-own-smart-roller-shades",{"title":5,"description":12},"writing\u002Fbuilding-my-own-smart-roller-shades","-YfW4YJYUa_noxfY3Z4g-vrQ9AHbiU-f_cJVqcHOsG0",1786027312468]