[{"data":1,"prerenderedAt":2133},["ShallowReactive",2],{"\u002Fprojects":3},[4,1469,1847],{"id":5,"title":6,"body":7,"date":1457,"description":1458,"extension":1459,"image":1460,"meta":1461,"navigation":1462,"path":1463,"repository":1464,"seo":1466,"stem":1467,"tags":1460,"__hash__":1468},"projects\u002Fprojects\u002Fmulti-user-docker-lab-management.md","Multi-User Docker-based Server Management",{"type":8,"value":9,"toc":1437},"minimark",[10,19,27,60,68,74,99,105,108,129,134,140,147,159,162,187,193,196,218,224,230,267,273,280,311,317,320,369,373,378,385,387,726,731,879,884,889,900,1369,1375,1417,1421,1424,1430,1433],[11,12,15],"card",{"icon":13,"title":14},"i-lucide-info","Project Overview",[16,17,18],"p",{},"This project provisions isolated, containerized Linux environments (Ubuntu 20.04) for multiple users on a shared host server (CentOS 7). It dynamically maps host users to container users using UID\u002FGID matching, ensuring smooth read\u002Fwrite permissions for shared volumes. Each user gets their own dedicated SSH access, a pre-configured Miniconda environment, and a mapped workspace.",[20,21,23],"h2",{"id":22},"key-features",[24,25,26],"strong",{},"Key Features",[28,29,30,37,43,54],"ul",{},[31,32,33,36],"li",{},[24,34,35],{},"Dynamic User Mapping:"," Bypasses Docker's root-ownership problem. The container mimics the host system's user ID and group ID on startup.",[31,38,39,42],{},[24,40,41],{},"Isolated Environments:"," Users access their specific containers via unique SSH ports.",[31,44,45,48,49,53],{},[24,46,47],{},"Persistent Storage:"," The host user's home directory is mapped to ",[50,51,52],"code",{},"\u002Fworkspace"," inside the container.",[31,55,56,59],{},[24,57,58],{},"Legacy Kernel Compatibility:"," Configured to build successfully on CentOS 7 hosts using unauthenticated APT mirrors (Tsinghua) to bypass outdated certificate issues.",[11,61,65],{"icon":62,"title":63,"color":64},"i-lucide-circle-question-mark","What we attempted to solve","warning",[16,66,67],{},"Given the out-dated nature of CentOS 7, we were unable to install many softwares, including conda, onto the server. We also faced issues where multiple users had to use similar softwares, but there was no user management systems in place. Our solution was to set up a docker base image that we could then mirror for each person and set up an individual docker container of a Ubuntu system for each user, to which they can directly access via SSH.",[20,69,71],{"id":70},"prerequisites",[24,72,73],{},"Prerequisites",[28,75,76,82,85,92],{},[31,77,78,81],{},[24,79,80],{},"Host OS:"," CentOS 7 (or similar Linux distribution)",[31,83,84],{},"Docker installed and running",[31,86,87,88,91],{},"Root (",[50,89,90],{},"sudo",") privileges on the host",[31,93,94,95,98],{},"Host users must already exist (e.g., ",[50,96,97],{},"sudo useradd -m username",")",[20,100,102],{"id":101},"architecture-file-structure",[24,103,104],{},"Architecture & File Structure",[16,106,107],{},"The project relies on three core files:",[109,110,111,117,123],"ol",{},[31,112,113,116],{},[50,114,115],{},"Dockerfile",": Defines the base image (Ubuntu 20.04), installs core dependencies, configures Miniconda, and sets up SSH.",[31,118,119,122],{},[50,120,121],{},"entrypoint.sh",": Injected into the container. It runs on startup to create the user, map UID\u002FGID, fix Conda permissions, and start the SSH daemon as Process 1.",[31,124,125,128],{},[50,126,127],{},"start_lab.sh",": The host-side deployment script used by the administrator to spin up new environments.",[64,130,131],{},[16,132,133],{},"Ubuntu 20.04 was used here because 22.04 was incompatible with CentOS 7.",[20,135,137],{"id":136},"deployment-workflow",[24,138,139],{},"Deployment Workflow",[141,142,144],"h3",{"id":143},"_1-prepare-the-build-directory",[24,145,146],{},"1. Prepare the Build Directory",[16,148,149,150,152,153,155,156,158],{},"Place ",[50,151,115],{},", ",[50,154,121],{},", and ",[50,157,127],{}," in the same directory on the host server.",[16,160,161],{},"Ensure the scripts are executable:",[163,164,169],"pre",{"className":165,"code":166,"language":167,"meta":168,"style":168},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","chmod +x start_lab.sh\n","bash","",[50,170,171],{"__ignoreMap":168},[172,173,176,180,184],"span",{"class":174,"line":175},"line",1,[172,177,179],{"class":178},"sBMFI","chmod",[172,181,183],{"class":182},"sfazB"," +x",[172,185,186],{"class":182}," start_lab.sh\n",[141,188,190],{"id":189},"_2-build-the-base-image",[24,191,192],{},"2. Build the Base Image",[16,194,195],{},"Run this command from the directory containing your Dockerfile:",[163,197,199],{"className":165,"code":198,"language":167,"meta":168,"style":168},"docker build -t lab_base_image .\n",[50,200,201],{"__ignoreMap":168},[172,202,203,206,209,212,215],{"class":174,"line":175},[172,204,205],{"class":178},"docker",[172,207,208],{"class":182}," build",[172,210,211],{"class":182}," -t",[172,213,214],{"class":182}," lab_base_image",[172,216,217],{"class":182}," .\n",[141,219,221],{"id":220},"_3-deploy-a-user-container",[24,222,223],{},"3. Deploy a User Container",[16,225,226,227,229],{},"Use the ",[50,228,127],{}," script to spawn a container for an existing host user. Provide the username and the designated SSH port.",[163,231,233],{"className":165,"code":232,"language":167,"meta":168,"style":168},"sudo .\u002Fstart_lab.sh \u003Cusername> \u003Cport>\n",[50,234,235],{"__ignoreMap":168},[172,236,237,239,242,246,249,253,256,258,261,264],{"class":174,"line":175},[172,238,90],{"class":178},[172,240,241],{"class":182}," .\u002Fstart_lab.sh",[172,243,245],{"class":244},"sMK4o"," \u003C",[172,247,248],{"class":182},"usernam",[172,250,252],{"class":251},"sTEyZ","e",[172,254,255],{"class":244},">",[172,257,245],{"class":244},[172,259,260],{"class":182},"por",[172,262,263],{"class":251},"t",[172,265,266],{"class":244},">\n",[141,268,270],{"id":269},"_4-configure-the-host-firewall",[24,271,272],{},"4. Configure the Host Firewall",[16,274,275,276,279],{},"CentOS uses ",[50,277,278],{},"firewalld"," by default. You must open the assigned port so the user can connect:",[163,281,283],{"className":165,"code":282,"language":167,"meta":168,"style":168},"sudo firewall-cmd --zone=public --add-port=2001\u002Ftcp --permanent\nsudo firewall-cmd --reload\n",[50,284,285,301],{"__ignoreMap":168},[172,286,287,289,292,295,298],{"class":174,"line":175},[172,288,90],{"class":178},[172,290,291],{"class":182}," firewall-cmd",[172,293,294],{"class":182}," --zone=public",[172,296,297],{"class":182}," --add-port=2001\u002Ftcp",[172,299,300],{"class":182}," --permanent\n",[172,302,304,306,308],{"class":174,"line":303},2,[172,305,90],{"class":178},[172,307,291],{"class":182},[172,309,310],{"class":182}," --reload\n",[141,312,314],{"id":313},"_5-access-the-lab",[24,315,316],{},"5. Access the Lab",[16,318,319],{},"The user can now SSH into their isolated environment from their local machine:",[163,321,323],{"className":165,"code":322,"language":167,"meta":168,"style":168},"ssh \u003Cusername>@\u003Chost_ip_address> -p \u003Cport>\n# Password defaults to: password123 (forces change\u002Fsetup later)\n",[50,324,325,363],{"__ignoreMap":168},[172,326,327,330,332,334,336,338,341,344,347,350,352,355,357,359,361],{"class":174,"line":175},[172,328,329],{"class":178},"ssh",[172,331,245],{"class":244},[172,333,248],{"class":182},[172,335,252],{"class":251},[172,337,255],{"class":244},[172,339,340],{"class":182},"@",[172,342,343],{"class":244},"\u003C",[172,345,346],{"class":182},"host_ip_addres",[172,348,349],{"class":251},"s",[172,351,255],{"class":244},[172,353,354],{"class":182}," -p",[172,356,245],{"class":244},[172,358,260],{"class":182},[172,360,263],{"class":251},[172,362,266],{"class":244},[172,364,365],{"class":174,"line":303},[172,366,368],{"class":367},"sHwdD","# Password defaults to: password123 (forces change\u002Fsetup later)\n",[20,370,372],{"id":371},"files","Files",[141,374,376],{"id":375},"entrypointsh",[50,377,121],{},[16,379,380,381,384],{},"This file is used to conduct the UID\u002FGID matching to ensure that the user can access the files inside the container. Note that the default password is set to ",[50,382,383],{},"password123"," .",[141,386],{"id":168},[163,388,390],{"className":165,"code":389,"filename":121,"language":167,"meta":168,"style":168},"#!\u002Fbin\u002Fbash\n\n# Default values if variables aren't passed\nUSER_UID=${HOST_UID:-1000}\nUSER_GID=${HOST_GID:-1000}\nUSER_NAME=${USERNAME:-labuser}\n\necho \"Configuring container for $USER_NAME (UID: $USER_UID, GID: $USER_GID)\"\n\n# 1. Create the group and user to match the host\ngroupadd -g $USER_GID $USER_NAME 2>\u002Fdev\u002Fnull || echo \"Group exists\"\nuseradd -m -u $USER_UID -g $USER_GID -s \u002Fbin\u002Fbash $USER_NAME 2>\u002Fdev\u002Fnull || echo \"User exists\"\n\n# 2. Set a default password and give sudo rights\necho \"$USER_NAME:password123\" | chpasswd\necho \"$USER_NAME ALL=(ALL) NOPASSWD:ALL\" >> \u002Fetc\u002Fsudoers\n\n# 3. Fix permissions for Conda so the user can manage their own environments\nchown -R $USER_NAME:$USER_GID \u002Fopt\u002Fconda\n\n# 4. Initialize Conda for the new user\nsudo -u $USER_NAME \u002Fopt\u002Fconda\u002Fbin\u002Fconda init bash\n\n# 5. Start the SSH service in the foreground\nexec \u002Fusr\u002Fsbin\u002Fsshd -D\n",[50,391,392,397,403,409,430,447,465,470,503,508,514,545,590,595,601,622,642,647,653,674,679,685,703,708,714],{"__ignoreMap":168},[172,393,394],{"class":174,"line":175},[172,395,396],{"class":367},"#!\u002Fbin\u002Fbash\n",[172,398,399],{"class":174,"line":303},[172,400,402],{"emptyLinePlaceholder":401},true,"\n",[172,404,406],{"class":174,"line":405},3,[172,407,408],{"class":367},"# Default values if variables aren't passed\n",[172,410,412,415,418,421,424,427],{"class":174,"line":411},4,[172,413,414],{"class":251},"USER_UID",[172,416,417],{"class":244},"=${",[172,419,420],{"class":251},"HOST_UID",[172,422,423],{"class":244},":-",[172,425,426],{"class":251},"1000",[172,428,429],{"class":244},"}\n",[172,431,433,436,438,441,443,445],{"class":174,"line":432},5,[172,434,435],{"class":251},"USER_GID",[172,437,417],{"class":244},[172,439,440],{"class":251},"HOST_GID",[172,442,423],{"class":244},[172,444,426],{"class":251},[172,446,429],{"class":244},[172,448,450,453,455,458,460,463],{"class":174,"line":449},6,[172,451,452],{"class":251},"USER_NAME",[172,454,417],{"class":244},[172,456,457],{"class":251},"USERNAME",[172,459,423],{"class":244},[172,461,462],{"class":251},"labuser",[172,464,429],{"class":244},[172,466,468],{"class":174,"line":467},7,[172,469,402],{"emptyLinePlaceholder":401},[172,471,473,477,480,483,486,489,492,495,498,500],{"class":174,"line":472},8,[172,474,476],{"class":475},"s2Zo4","echo",[172,478,479],{"class":244}," \"",[172,481,482],{"class":182},"Configuring container for ",[172,484,485],{"class":251},"$USER_NAME",[172,487,488],{"class":182}," (UID: ",[172,490,491],{"class":251},"$USER_UID",[172,493,494],{"class":182},", GID: ",[172,496,497],{"class":251},"$USER_GID",[172,499,98],{"class":182},[172,501,502],{"class":244},"\"\n",[172,504,506],{"class":174,"line":505},9,[172,507,402],{"emptyLinePlaceholder":401},[172,509,511],{"class":174,"line":510},10,[172,512,513],{"class":367},"# 1. Create the group and user to match the host\n",[172,515,517,520,523,526,529,532,535,538,540,543],{"class":174,"line":516},11,[172,518,519],{"class":178},"groupadd",[172,521,522],{"class":182}," -g",[172,524,525],{"class":251}," $USER_GID $USER_NAME ",[172,527,528],{"class":244},"2>",[172,530,531],{"class":182},"\u002Fdev\u002Fnull",[172,533,534],{"class":244}," ||",[172,536,537],{"class":475}," echo",[172,539,479],{"class":244},[172,541,542],{"class":182},"Group exists",[172,544,502],{"class":244},[172,546,548,551,554,557,560,563,566,569,572,575,577,579,581,583,585,588],{"class":174,"line":547},12,[172,549,550],{"class":178},"useradd",[172,552,553],{"class":182}," -m",[172,555,556],{"class":182}," -u",[172,558,559],{"class":251}," $USER_UID ",[172,561,562],{"class":182},"-g",[172,564,565],{"class":251}," $USER_GID ",[172,567,568],{"class":182},"-s",[172,570,571],{"class":182}," \u002Fbin\u002Fbash",[172,573,574],{"class":251}," $USER_NAME ",[172,576,528],{"class":244},[172,578,531],{"class":182},[172,580,534],{"class":244},[172,582,537],{"class":475},[172,584,479],{"class":244},[172,586,587],{"class":182},"User exists",[172,589,502],{"class":244},[172,591,593],{"class":174,"line":592},13,[172,594,402],{"emptyLinePlaceholder":401},[172,596,598],{"class":174,"line":597},14,[172,599,600],{"class":367},"# 2. Set a default password and give sudo rights\n",[172,602,604,606,608,610,613,616,619],{"class":174,"line":603},15,[172,605,476],{"class":475},[172,607,479],{"class":244},[172,609,485],{"class":251},[172,611,612],{"class":182},":password123",[172,614,615],{"class":244},"\"",[172,617,618],{"class":244}," |",[172,620,621],{"class":178}," chpasswd\n",[172,623,625,627,629,631,634,636,639],{"class":174,"line":624},16,[172,626,476],{"class":475},[172,628,479],{"class":244},[172,630,485],{"class":251},[172,632,633],{"class":182}," ALL=(ALL) NOPASSWD:ALL",[172,635,615],{"class":244},[172,637,638],{"class":244}," >>",[172,640,641],{"class":182}," \u002Fetc\u002Fsudoers\n",[172,643,645],{"class":174,"line":644},17,[172,646,402],{"emptyLinePlaceholder":401},[172,648,650],{"class":174,"line":649},18,[172,651,652],{"class":367},"# 3. Fix permissions for Conda so the user can manage their own environments\n",[172,654,656,659,662,665,668,671],{"class":174,"line":655},19,[172,657,658],{"class":178},"chown",[172,660,661],{"class":182}," -R",[172,663,664],{"class":251}," $USER_NAME",[172,666,667],{"class":182},":",[172,669,670],{"class":251},"$USER_GID ",[172,672,673],{"class":182},"\u002Fopt\u002Fconda\n",[172,675,677],{"class":174,"line":676},20,[172,678,402],{"emptyLinePlaceholder":401},[172,680,682],{"class":174,"line":681},21,[172,683,684],{"class":367},"# 4. Initialize Conda for the new user\n",[172,686,688,690,692,694,697,700],{"class":174,"line":687},22,[172,689,90],{"class":178},[172,691,556],{"class":182},[172,693,574],{"class":251},[172,695,696],{"class":182},"\u002Fopt\u002Fconda\u002Fbin\u002Fconda",[172,698,699],{"class":182}," init",[172,701,702],{"class":182}," bash\n",[172,704,706],{"class":174,"line":705},23,[172,707,402],{"emptyLinePlaceholder":401},[172,709,711],{"class":174,"line":710},24,[172,712,713],{"class":367},"# 5. Start the SSH service in the foreground\n",[172,715,717,720,723],{"class":174,"line":716},25,[172,718,719],{"class":475},"exec",[172,721,722],{"class":182}," \u002Fusr\u002Fsbin\u002Fsshd",[172,724,725],{"class":182}," -D\n",[141,727,729],{"id":728},"dockerfile",[50,730,115],{},[163,732,735],{"className":733,"code":734,"filename":115,"language":728,"meta":168,"style":168},"language-dockerfile shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","FROM ubuntu:20.04\n\n# Setup APT and Mirrors\nRUN rm -rf \u002Fetc\u002Fapt\u002Fapt.conf.d\u002F* && \\\n    echo 'APT::Get::AllowUnauthenticated \"true\";' > \u002Fetc\u002Fapt\u002Fapt.conf.d\u002F99force-insecure\nRUN echo \"deb [trusted=yes] http:\u002F\u002Fmirrors.tuna.tsinghua.edu.cn\u002Fubuntu\u002F focal main restricted universe multiverse\" > \u002Fetc\u002Fapt\u002Fsources.list\n\n# Install Core Tools and SSH\nRUN apt-get update && \\\n    DEBIAN_FRONTEND=noninteractive apt-get install -y --allow-unauthenticated \\\n    wget ca-certificates bzip2 openssh-server sudo && \\\n    mkdir \u002Fvar\u002Frun\u002Fsshd && \\\n    apt-get clean\n\n# Install Miniconda\nRUN wget --no-check-certificate https:\u002F\u002Fmirrors.tuna.tsinghua.edu.cn\u002Fanaconda\u002Fminiconda\u002FMiniconda3-py39_4.12.0-Linux-x86_64.sh -O \u002Ftmp\u002Fminiconda.sh && \\\n    bash \u002Ftmp\u002Fminiconda.sh -b -p \u002Fopt\u002Fconda && \\\n    rm \u002Ftmp\u002Fminiconda.sh\n\nENV PATH=\"\u002Fopt\u002Fconda\u002Fbin:$PATH\"\n\n# Copy the entrypoint script from your host into the image\nCOPY entrypoint.sh \u002Fusr\u002Flocal\u002Fbin\u002Fentrypoint.sh\nRUN chmod +x \u002Fusr\u002Flocal\u002Fbin\u002Fentrypoint.sh\n\nEXPOSE 22\n\n# Tell Docker to run our script on startup\nENTRYPOINT [\"\u002Fusr\u002Flocal\u002Fbin\u002Fentrypoint.sh\"]\n",[50,736,737,742,746,751,756,761,766,770,775,780,785,790,795,800,804,809,814,819,824,828,833,837,842,847,852,856,862,867,873],{"__ignoreMap":168},[172,738,739],{"class":174,"line":175},[172,740,741],{},"FROM ubuntu:20.04\n",[172,743,744],{"class":174,"line":303},[172,745,402],{"emptyLinePlaceholder":401},[172,747,748],{"class":174,"line":405},[172,749,750],{},"# Setup APT and Mirrors\n",[172,752,753],{"class":174,"line":411},[172,754,755],{},"RUN rm -rf \u002Fetc\u002Fapt\u002Fapt.conf.d\u002F* && \\\n",[172,757,758],{"class":174,"line":432},[172,759,760],{},"    echo 'APT::Get::AllowUnauthenticated \"true\";' > \u002Fetc\u002Fapt\u002Fapt.conf.d\u002F99force-insecure\n",[172,762,763],{"class":174,"line":449},[172,764,765],{},"RUN echo \"deb [trusted=yes] http:\u002F\u002Fmirrors.tuna.tsinghua.edu.cn\u002Fubuntu\u002F focal main restricted universe multiverse\" > \u002Fetc\u002Fapt\u002Fsources.list\n",[172,767,768],{"class":174,"line":467},[172,769,402],{"emptyLinePlaceholder":401},[172,771,772],{"class":174,"line":472},[172,773,774],{},"# Install Core Tools and SSH\n",[172,776,777],{"class":174,"line":505},[172,778,779],{},"RUN apt-get update && \\\n",[172,781,782],{"class":174,"line":510},[172,783,784],{},"    DEBIAN_FRONTEND=noninteractive apt-get install -y --allow-unauthenticated \\\n",[172,786,787],{"class":174,"line":516},[172,788,789],{},"    wget ca-certificates bzip2 openssh-server sudo && \\\n",[172,791,792],{"class":174,"line":547},[172,793,794],{},"    mkdir \u002Fvar\u002Frun\u002Fsshd && \\\n",[172,796,797],{"class":174,"line":592},[172,798,799],{},"    apt-get clean\n",[172,801,802],{"class":174,"line":597},[172,803,402],{"emptyLinePlaceholder":401},[172,805,806],{"class":174,"line":603},[172,807,808],{},"# Install Miniconda\n",[172,810,811],{"class":174,"line":624},[172,812,813],{},"RUN wget --no-check-certificate https:\u002F\u002Fmirrors.tuna.tsinghua.edu.cn\u002Fanaconda\u002Fminiconda\u002FMiniconda3-py39_4.12.0-Linux-x86_64.sh -O \u002Ftmp\u002Fminiconda.sh && \\\n",[172,815,816],{"class":174,"line":644},[172,817,818],{},"    bash \u002Ftmp\u002Fminiconda.sh -b -p \u002Fopt\u002Fconda && \\\n",[172,820,821],{"class":174,"line":649},[172,822,823],{},"    rm \u002Ftmp\u002Fminiconda.sh\n",[172,825,826],{"class":174,"line":655},[172,827,402],{"emptyLinePlaceholder":401},[172,829,830],{"class":174,"line":676},[172,831,832],{},"ENV PATH=\"\u002Fopt\u002Fconda\u002Fbin:$PATH\"\n",[172,834,835],{"class":174,"line":681},[172,836,402],{"emptyLinePlaceholder":401},[172,838,839],{"class":174,"line":687},[172,840,841],{},"# Copy the entrypoint script from your host into the image\n",[172,843,844],{"class":174,"line":705},[172,845,846],{},"COPY entrypoint.sh \u002Fusr\u002Flocal\u002Fbin\u002Fentrypoint.sh\n",[172,848,849],{"class":174,"line":710},[172,850,851],{},"RUN chmod +x \u002Fusr\u002Flocal\u002Fbin\u002Fentrypoint.sh\n",[172,853,854],{"class":174,"line":716},[172,855,402],{"emptyLinePlaceholder":401},[172,857,859],{"class":174,"line":858},26,[172,860,861],{},"EXPOSE 22\n",[172,863,865],{"class":174,"line":864},27,[172,866,402],{"emptyLinePlaceholder":401},[172,868,870],{"class":174,"line":869},28,[172,871,872],{},"# Tell Docker to run our script on startup\n",[172,874,876],{"class":174,"line":875},29,[172,877,878],{},"ENTRYPOINT [\"\u002Fusr\u002Flocal\u002Fbin\u002Fentrypoint.sh\"]\n",[64,880,881],{},[16,882,883],{},"Note that the Tsinghua mirror is used.",[141,885,887],{"id":886},"start_labsh",[50,888,127],{},[16,890,891,892,895,896,899],{},"Note that the host ",[50,893,894],{},"UID"," and ",[50,897,898],{},"GID"," are fetched so that matching can be made. ",[163,901,903],{"className":165,"code":902,"filename":127,"language":167,"meta":168,"style":168},"#!\u002Fbin\u002Fbash\n\n# Usage: .\u002Fstart_lab.sh \u003Chost_username> \u003Cexternal_port>\nTARGET_USER=$1\nPORT=$2\n\nif [ -z \"$TARGET_USER\" ] || [ -z \"$PORT\" ]; then\n    echo \"Usage: .\u002Fstart_lab.sh \u003Cusername> \u003Cport>\"\n    exit 1\nfi\n\n# Get IDs from the host system\nU_ID=$(id -u $TARGET_USER)\nG_ID=$(id -g $TARGET_USER)\nU_HOME=\"\u002Fhome\u002F$TARGET_USER\"\n\n# Ensure the host directory exists\nmkdir -p \"$U_HOME\"\n\n# Change ownership to the target user \n# (This assumes the user exists on the CentOS host with the same name)\nchown -R \"$TARGET_USER:$TARGET_USER\" \"$U_HOME\"\n\n# Build\u002FRefresh the image (optional, but ensures entrypoint updates are live)\n# docker build -t lab_base_image .\n\ndocker run -d \\\n  --name \"lab_$TARGET_USER\" \\\n  --restart unless-stopped \\\n  -e HOST_UID=$U_ID \\\n  -e HOST_GID=$G_ID \\\n  -e USERNAME=$TARGET_USER \\\n  -p \"$PORT:22\" \\\n  -v \"$U_HOME:\u002Fhome\u002F$TARGET_USER:z\" \\\n  lab_base_image\n\necho \"----------------------------------------------------\"\necho \"Container for $TARGET_USER is now LIVE.\"\necho \"Access via: ssh $TARGET_USER@$(hostname -I | awk '{print $1}') -p $PORT\"\necho \"Work directory mapped to: $U_HOME\"\necho \"----------------------------------------------------\"\n",[50,904,905,909,913,918,930,940,944,985,997,1006,1011,1015,1020,1039,1054,1070,1074,1079,1093,1097,1102,1107,1129,1133,1138,1143,1147,1160,1176,1186,1198,1209,1220,1237,1259,1265,1270,1282,1299,1344,1358],{"__ignoreMap":168},[172,906,907],{"class":174,"line":175},[172,908,396],{"class":367},[172,910,911],{"class":174,"line":303},[172,912,402],{"emptyLinePlaceholder":401},[172,914,915],{"class":174,"line":405},[172,916,917],{"class":367},"# Usage: .\u002Fstart_lab.sh \u003Chost_username> \u003Cexternal_port>\n",[172,919,920,923,926],{"class":174,"line":411},[172,921,922],{"class":251},"TARGET_USER",[172,924,925],{"class":244},"=",[172,927,929],{"class":928},"sHdIc","$1\n",[172,931,932,935,937],{"class":174,"line":432},[172,933,934],{"class":251},"PORT",[172,936,925],{"class":244},[172,938,939],{"class":928},"$2\n",[172,941,942],{"class":174,"line":449},[172,943,402],{"emptyLinePlaceholder":401},[172,945,946,950,953,956,958,961,963,966,968,970,972,974,977,979,982],{"class":174,"line":467},[172,947,949],{"class":948},"s7zQu","if",[172,951,952],{"class":244}," [",[172,954,955],{"class":244}," -z",[172,957,479],{"class":244},[172,959,960],{"class":251},"$TARGET_USER",[172,962,615],{"class":244},[172,964,965],{"class":244}," ]",[172,967,534],{"class":244},[172,969,952],{"class":244},[172,971,955],{"class":244},[172,973,479],{"class":244},[172,975,976],{"class":251},"$PORT",[172,978,615],{"class":244},[172,980,981],{"class":244}," ];",[172,983,984],{"class":948}," then\n",[172,986,987,990,992,995],{"class":174,"line":472},[172,988,989],{"class":475},"    echo",[172,991,479],{"class":244},[172,993,994],{"class":182},"Usage: .\u002Fstart_lab.sh \u003Cusername> \u003Cport>",[172,996,502],{"class":244},[172,998,999,1002],{"class":174,"line":505},[172,1000,1001],{"class":475},"    exit",[172,1003,1005],{"class":1004},"sbssI"," 1\n",[172,1007,1008],{"class":174,"line":510},[172,1009,1010],{"class":948},"fi\n",[172,1012,1013],{"class":174,"line":516},[172,1014,402],{"emptyLinePlaceholder":401},[172,1016,1017],{"class":174,"line":547},[172,1018,1019],{"class":367},"# Get IDs from the host system\n",[172,1021,1022,1025,1028,1031,1033,1036],{"class":174,"line":592},[172,1023,1024],{"class":251},"U_ID",[172,1026,1027],{"class":244},"=$(",[172,1029,1030],{"class":178},"id",[172,1032,556],{"class":182},[172,1034,1035],{"class":251}," $TARGET_USER",[172,1037,1038],{"class":244},")\n",[172,1040,1041,1044,1046,1048,1050,1052],{"class":174,"line":597},[172,1042,1043],{"class":251},"G_ID",[172,1045,1027],{"class":244},[172,1047,1030],{"class":178},[172,1049,522],{"class":182},[172,1051,1035],{"class":251},[172,1053,1038],{"class":244},[172,1055,1056,1059,1061,1063,1066,1068],{"class":174,"line":603},[172,1057,1058],{"class":251},"U_HOME",[172,1060,925],{"class":244},[172,1062,615],{"class":244},[172,1064,1065],{"class":182},"\u002Fhome\u002F",[172,1067,960],{"class":251},[172,1069,502],{"class":244},[172,1071,1072],{"class":174,"line":624},[172,1073,402],{"emptyLinePlaceholder":401},[172,1075,1076],{"class":174,"line":644},[172,1077,1078],{"class":367},"# Ensure the host directory exists\n",[172,1080,1081,1084,1086,1088,1091],{"class":174,"line":649},[172,1082,1083],{"class":178},"mkdir",[172,1085,354],{"class":182},[172,1087,479],{"class":244},[172,1089,1090],{"class":251},"$U_HOME",[172,1092,502],{"class":244},[172,1094,1095],{"class":174,"line":655},[172,1096,402],{"emptyLinePlaceholder":401},[172,1098,1099],{"class":174,"line":676},[172,1100,1101],{"class":367},"# Change ownership to the target user \n",[172,1103,1104],{"class":174,"line":681},[172,1105,1106],{"class":367},"# (This assumes the user exists on the CentOS host with the same name)\n",[172,1108,1109,1111,1113,1115,1117,1119,1121,1123,1125,1127],{"class":174,"line":687},[172,1110,658],{"class":178},[172,1112,661],{"class":182},[172,1114,479],{"class":244},[172,1116,960],{"class":251},[172,1118,667],{"class":182},[172,1120,960],{"class":251},[172,1122,615],{"class":244},[172,1124,479],{"class":244},[172,1126,1090],{"class":251},[172,1128,502],{"class":244},[172,1130,1131],{"class":174,"line":705},[172,1132,402],{"emptyLinePlaceholder":401},[172,1134,1135],{"class":174,"line":710},[172,1136,1137],{"class":367},"# Build\u002FRefresh the image (optional, but ensures entrypoint updates are live)\n",[172,1139,1140],{"class":174,"line":716},[172,1141,1142],{"class":367},"# docker build -t lab_base_image .\n",[172,1144,1145],{"class":174,"line":858},[172,1146,402],{"emptyLinePlaceholder":401},[172,1148,1149,1151,1154,1157],{"class":174,"line":864},[172,1150,205],{"class":178},[172,1152,1153],{"class":182}," run",[172,1155,1156],{"class":182}," -d",[172,1158,1159],{"class":251}," \\\n",[172,1161,1162,1165,1167,1170,1172,1174],{"class":174,"line":869},[172,1163,1164],{"class":182},"  --name",[172,1166,479],{"class":244},[172,1168,1169],{"class":182},"lab_",[172,1171,960],{"class":251},[172,1173,615],{"class":244},[172,1175,1159],{"class":251},[172,1177,1178,1181,1184],{"class":174,"line":875},[172,1179,1180],{"class":182},"  --restart",[172,1182,1183],{"class":182}," unless-stopped",[172,1185,1159],{"class":251},[172,1187,1189,1192,1195],{"class":174,"line":1188},30,[172,1190,1191],{"class":182},"  -e",[172,1193,1194],{"class":182}," HOST_UID=",[172,1196,1197],{"class":251},"$U_ID \\\n",[172,1199,1201,1203,1206],{"class":174,"line":1200},31,[172,1202,1191],{"class":182},[172,1204,1205],{"class":182}," HOST_GID=",[172,1207,1208],{"class":251},"$G_ID \\\n",[172,1210,1212,1214,1217],{"class":174,"line":1211},32,[172,1213,1191],{"class":182},[172,1215,1216],{"class":182}," USERNAME=",[172,1218,1219],{"class":251},"$TARGET_USER \\\n",[172,1221,1223,1226,1228,1230,1233,1235],{"class":174,"line":1222},33,[172,1224,1225],{"class":182},"  -p",[172,1227,479],{"class":244},[172,1229,976],{"class":251},[172,1231,1232],{"class":182},":22",[172,1234,615],{"class":244},[172,1236,1159],{"class":251},[172,1238,1240,1243,1245,1247,1250,1252,1255,1257],{"class":174,"line":1239},34,[172,1241,1242],{"class":182},"  -v",[172,1244,479],{"class":244},[172,1246,1090],{"class":251},[172,1248,1249],{"class":182},":\u002Fhome\u002F",[172,1251,960],{"class":251},[172,1253,1254],{"class":182},":z",[172,1256,615],{"class":244},[172,1258,1159],{"class":251},[172,1260,1262],{"class":174,"line":1261},35,[172,1263,1264],{"class":182},"  lab_base_image\n",[172,1266,1268],{"class":174,"line":1267},36,[172,1269,402],{"emptyLinePlaceholder":401},[172,1271,1273,1275,1277,1280],{"class":174,"line":1272},37,[172,1274,476],{"class":475},[172,1276,479],{"class":244},[172,1278,1279],{"class":182},"----------------------------------------------------",[172,1281,502],{"class":244},[172,1283,1285,1287,1289,1292,1294,1297],{"class":174,"line":1284},38,[172,1286,476],{"class":475},[172,1288,479],{"class":244},[172,1290,1291],{"class":182},"Container for ",[172,1293,960],{"class":251},[172,1295,1296],{"class":182}," is now LIVE.",[172,1298,502],{"class":244},[172,1300,1302,1304,1306,1309,1311,1313,1316,1319,1322,1325,1328,1331,1334,1337,1340,1342],{"class":174,"line":1301},39,[172,1303,476],{"class":475},[172,1305,479],{"class":244},[172,1307,1308],{"class":182},"Access via: ssh ",[172,1310,960],{"class":251},[172,1312,340],{"class":182},[172,1314,1315],{"class":244},"$(",[172,1317,1318],{"class":178},"hostname",[172,1320,1321],{"class":182}," -I ",[172,1323,1324],{"class":244},"|",[172,1326,1327],{"class":178}," awk",[172,1329,1330],{"class":244}," '",[172,1332,1333],{"class":182},"{print $1}",[172,1335,1336],{"class":244},"')",[172,1338,1339],{"class":182}," -p ",[172,1341,976],{"class":251},[172,1343,502],{"class":244},[172,1345,1347,1349,1351,1354,1356],{"class":174,"line":1346},40,[172,1348,476],{"class":475},[172,1350,479],{"class":244},[172,1352,1353],{"class":182},"Work directory mapped to: ",[172,1355,1090],{"class":251},[172,1357,502],{"class":244},[172,1359,1361,1363,1365,1367],{"class":174,"line":1360},41,[172,1362,476],{"class":475},[172,1364,479],{"class":244},[172,1366,1279],{"class":182},[172,1368,502],{"class":244},[20,1370,1372],{"id":1371},"troubleshooting",[24,1373,1374],{},"Troubleshooting",[28,1376,1377,1395],{},[31,1378,1379,1382,1383,1386,1387,1390,1391,1394],{},[24,1380,1381],{},"\"Command Not Found\" when running scripts:"," Ensure you are using ",[50,1384,1385],{},".\u002F"," to execute scripts in the current directory (e.g., ",[50,1388,1389],{},".\u002Fstart_lab.sh",") and that the file has execution permissions (",[50,1392,1393],{},"chmod +x",").",[31,1396,1397,1400],{},[24,1398,1399],{},"Connection Refused on SSH:",[109,1401,1402,1408,1414],{},[31,1403,1404,1405],{},"Verify the container is running: ",[50,1406,1407],{},"docker ps",[31,1409,1410,1411],{},"Verify the SSH service is active inside the container: ",[50,1412,1413],{},"docker exec \u003Ccontainer_name> netstat -tulpn | grep 22",[31,1415,1416],{},"Ensure the host firewall has the specific port opened (see step 4 above).",[20,1418,1420],{"id":1419},"disclaimer","Disclaimer",[16,1422,1423],{},"This project was written under AI assistance.",[16,1425,1426,1429],{},[24,1427,1428],{},"This code is provided \"as-is\" without any warranty."," Users are solely responsible for validating results for their specific applications. The author(s) are not liable for any errors, inaccuracies, or damages arising from the use of this software.",[16,1431,1432],{},"For critical research applications, please independently verify all measurements and consult established methodologies in the field.",[1434,1435,1436],"style",{},"html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}",{"title":168,"searchDepth":303,"depth":303,"links":1438},[1439,1440,1441,1442,1449,1455,1456],{"id":22,"depth":303,"text":26},{"id":70,"depth":303,"text":73},{"id":101,"depth":303,"text":104},{"id":136,"depth":303,"text":139,"children":1443},[1444,1445,1446,1447,1448],{"id":143,"depth":405,"text":146},{"id":189,"depth":405,"text":192},{"id":220,"depth":405,"text":223},{"id":269,"depth":405,"text":272},{"id":313,"depth":405,"text":316},{"id":371,"depth":303,"text":372,"children":1450},[1451,1452,1453,1454],{"id":375,"depth":405,"text":121},{"id":168,"depth":405,"text":168},{"id":728,"depth":405,"text":115},{"id":886,"depth":405,"text":127},{"id":1371,"depth":303,"text":1374},{"id":1419,"depth":303,"text":1420},"2026-04-22","A streamlined procedure for managing user environments as docker images in a small team-based environment on a shared server. ","md",null,{},{"title":168},"\u002Fprojects\u002Fmulti-user-docker-lab-management",{"showWiki":1465,"customWikiLink":168},false,{"title":6,"description":1458},"projects\u002Fmulti-user-docker-lab-management","rE3TeNl3W1_5kUMfAE_ueqoWY59raPfEY77HbYTk3Cc",{"id":1470,"title":1471,"body":1472,"date":1834,"description":1835,"extension":1459,"image":1460,"meta":1836,"navigation":401,"path":1837,"repository":1838,"seo":1841,"stem":1842,"tags":1843,"__hash__":1846},"projects\u002Fprojects\u002Fwireless-motion-controlled-slide-show-clicker.md","Wireless Motion Controlled Side Show Clicker",{"type":8,"value":1473,"toc":1826},[1474,1478,1484,1491,1497,1517,1522,1526,1545,1548,1559,1562,1565,1571,1575,1599,1603,1610,1613,1793,1797,1808,1811,1813,1816,1821,1823],[20,1475,1477],{"id":1476},"vibrating-structure-gyroscopes","💞 Vibrating Structure Gyroscopes",[1479,1480,1481],"blockquote",{},[16,1482,1483],{},"We spun heart-to-heart, dancing to the tunes of our forever-long story. ",[16,1485,1486,1487,1490],{},"Fundamentally, vibrating structure gyroscopes are a type of ",[24,1488,1489],{},"Micro-Electrical-Mechanical-Systems (MEMS)",". MEMS serve an especially unique role in that at their small size, surface area becomes much more dominant than volume. Hence, electrostatic forces, surface tension, Van der Waals forces, viscous drag, and elastic forces become much stronger than gravity. ",[16,1492,1493,1496],{},[24,1494,1495],{},"Accelerometers"," function by detecting changes in capacitance. They are generally composed of fixed plates of capacitance, alongside a moving mass held in place my springs. As the object moves, the moving mass changes position, altering the electric field. This could then be used to determine the direction and magnitude of the movement. ",[16,1498,1499,1502,1503,1510,1511,1516],{},[24,1500,1501],{},"Gyroscopes",", function similarly, but instead utilizes ",[1504,1505,1509],"a",{"href":1506,"rel":1507},"https:\u002F\u002Fwww.vectornav.com\u002Fresources\u002Finertial-navigation-primer\u002Ftheory-of-operation\u002Ftheory-mems#:~:text=Typically%2C%20MEMS%20gyroscopes%20use%20a,proportional%20to%20the%20angular%20velocity.",[1508],"nofollow","Coriolis force",". More information about gyroscopes can be found ",[1504,1512,1515],{"href":1513,"rel":1514},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FVibrating_structure_gyroscope",[1508],"here",". ",[64,1518,1519],{},[16,1520,1521],{},"Note that the above sections gives simplified explanations that aim to provide the bigger picture necessary to understanding the Arduino device. It is by no means \"perfectly-accurate\".",[20,1523,1525],{"id":1524},"hardware-components","Hardware Components",[11,1527,1528,1539],{},[109,1529,1530,1533,1536],{},[31,1531,1532],{},"MPU-9250 9-axes Gyroscope",[31,1534,1535],{},"ESP32 Microchip",[31,1537,1538],{},"9V Battery",[1540,1541,1542],"template",{"v-slot:title":168},[16,1543,1544],{},"An Overview of the Components",[16,1546,1547],{},"The MPU-9250 9-axes gyroscope is capable of detecting: ",[109,1549,1550,1553,1556],{},[31,1551,1552],{},"acceleration in three directions - x, y, and z",[31,1554,1555],{},"angular Acceleration in three directions: raw, pitch, and yaw​",[31,1557,1558],{},"magnetic field via its magnetometer",[16,1560,1561],{},"The ESP32 microchip was chosen for its bluetooth functions, which is essential for remote control. ",[16,1563,1564],{},"The 9V battery was used to power this system. ",[1566,1567,1568],"note",{},[16,1569,1570],{},"In the actual process, we found out that 9V might be a bit too much because the entire system began overheating and some components were burned. Be careful!",[20,1572,1574],{"id":1573},"circuit-design-routing","Circuit Design & Routing",[16,1576,1577,1578,1581,1582,1585,1586,1589,1590,1593,1594,895,1596,1598],{},"Since the entire system is essentially a Inte-Integrated Circuit (I2C), we connected the input of the gyroscope to ",[50,1579,1580],{},"PIN 21"," as ",[50,1583,1584],{},"SDA"," (Serial Data) and ",[50,1587,1588],{},"PIN 22","  as ",[50,1591,1592],{},"SCL"," (Serial Clock). ",[50,1595,1584],{},[50,1597,1592],{}," provide all the necessary information to know the directional data provided by the gyroscope that is accompanied by a time stamp. ",[20,1600,1602],{"id":1601},"overview-of-code","Overview of Code",[16,1604,1605,1606,1609],{},"The ",[50,1607,1608],{},"bleMouse"," package was used to control the mouse on the connected laptop. ",[16,1611,1612],{},"The main logic of the motion detection is quite self-explanatory. ",[163,1614,1619],{"className":1615,"code":1616,"filename":1617,"language":1618,"meta":168,"style":168},"language-C++ shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","void loop() {\n  if (!bleMouse.isConnected()) return;\n\n  mySensor.accelUpdate();\n  mySensor.gyroUpdate();\n  Serial.print(String(millis()) + \" \"+ \"-->\"+\" \");\n\n  float aX = mySensor.accelX();\n  float aY = mySensor.accelY();\n  float aZ = mySensor.accelZ();\n  float gZ = mySensor.gyroZ();\n  Serial.print(String(aX)+\" \");\n  Serial.print(String(aY)+\" \");\n  Serial.print(String(aZ)+\" \");\n  Serial.print(String(gZ) + \"\");\n  \u002F\u002FSerial.print(String(gZ)+\" \");\n  if (abs(aX) > 0.4 || abs(aY) > 0.4) {\n    int Cx = constrain(aX * 15, -10, 10);\n    int Cy = constrain(-aY * 15, -10, 10);\n    bleMouse.move(Cx, Cy);\n    delay(20);\n  }\n\n  if (aZ \u003C -1.5) {\n    bleMouse.press(MOUSE_LEFT);\n    delay(100);\n    bleMouse.release(MOUSE_LEFT);\n  }\n  if (gZ > 100) {\n    bleMouse.press(MOUSE_RIGHT);\n    delay(100);\n    bleMouse.release(MOUSE_RIGHT);\n  }\n\n Serial.println(\"\\n\");\n}\n","wireless_mouse_clicker.ino","C++",[50,1620,1621,1626,1631,1635,1640,1645,1650,1654,1659,1664,1669,1674,1679,1684,1689,1694,1699,1704,1709,1714,1719,1724,1729,1733,1738,1743,1748,1753,1757,1762,1767,1771,1776,1780,1784,1789],{"__ignoreMap":168},[172,1622,1623],{"class":174,"line":175},[172,1624,1625],{},"void loop() {\n",[172,1627,1628],{"class":174,"line":303},[172,1629,1630],{},"  if (!bleMouse.isConnected()) return;\n",[172,1632,1633],{"class":174,"line":405},[172,1634,402],{"emptyLinePlaceholder":401},[172,1636,1637],{"class":174,"line":411},[172,1638,1639],{},"  mySensor.accelUpdate();\n",[172,1641,1642],{"class":174,"line":432},[172,1643,1644],{},"  mySensor.gyroUpdate();\n",[172,1646,1647],{"class":174,"line":449},[172,1648,1649],{},"  Serial.print(String(millis()) + \" \"+ \"-->\"+\" \");\n",[172,1651,1652],{"class":174,"line":467},[172,1653,402],{"emptyLinePlaceholder":401},[172,1655,1656],{"class":174,"line":472},[172,1657,1658],{},"  float aX = mySensor.accelX();\n",[172,1660,1661],{"class":174,"line":505},[172,1662,1663],{},"  float aY = mySensor.accelY();\n",[172,1665,1666],{"class":174,"line":510},[172,1667,1668],{},"  float aZ = mySensor.accelZ();\n",[172,1670,1671],{"class":174,"line":516},[172,1672,1673],{},"  float gZ = mySensor.gyroZ();\n",[172,1675,1676],{"class":174,"line":547},[172,1677,1678],{},"  Serial.print(String(aX)+\" \");\n",[172,1680,1681],{"class":174,"line":592},[172,1682,1683],{},"  Serial.print(String(aY)+\" \");\n",[172,1685,1686],{"class":174,"line":597},[172,1687,1688],{},"  Serial.print(String(aZ)+\" \");\n",[172,1690,1691],{"class":174,"line":603},[172,1692,1693],{},"  Serial.print(String(gZ) + \"\");\n",[172,1695,1696],{"class":174,"line":624},[172,1697,1698],{},"  \u002F\u002FSerial.print(String(gZ)+\" \");\n",[172,1700,1701],{"class":174,"line":644},[172,1702,1703],{},"  if (abs(aX) > 0.4 || abs(aY) > 0.4) {\n",[172,1705,1706],{"class":174,"line":649},[172,1707,1708],{},"    int Cx = constrain(aX * 15, -10, 10);\n",[172,1710,1711],{"class":174,"line":655},[172,1712,1713],{},"    int Cy = constrain(-aY * 15, -10, 10);\n",[172,1715,1716],{"class":174,"line":676},[172,1717,1718],{},"    bleMouse.move(Cx, Cy);\n",[172,1720,1721],{"class":174,"line":681},[172,1722,1723],{},"    delay(20);\n",[172,1725,1726],{"class":174,"line":687},[172,1727,1728],{},"  }\n",[172,1730,1731],{"class":174,"line":705},[172,1732,402],{"emptyLinePlaceholder":401},[172,1734,1735],{"class":174,"line":710},[172,1736,1737],{},"  if (aZ \u003C -1.5) {\n",[172,1739,1740],{"class":174,"line":716},[172,1741,1742],{},"    bleMouse.press(MOUSE_LEFT);\n",[172,1744,1745],{"class":174,"line":858},[172,1746,1747],{},"    delay(100);\n",[172,1749,1750],{"class":174,"line":864},[172,1751,1752],{},"    bleMouse.release(MOUSE_LEFT);\n",[172,1754,1755],{"class":174,"line":869},[172,1756,1728],{},[172,1758,1759],{"class":174,"line":875},[172,1760,1761],{},"  if (gZ > 100) {\n",[172,1763,1764],{"class":174,"line":1188},[172,1765,1766],{},"    bleMouse.press(MOUSE_RIGHT);\n",[172,1768,1769],{"class":174,"line":1200},[172,1770,1747],{},[172,1772,1773],{"class":174,"line":1211},[172,1774,1775],{},"    bleMouse.release(MOUSE_RIGHT);\n",[172,1777,1778],{"class":174,"line":1222},[172,1779,1728],{},[172,1781,1782],{"class":174,"line":1239},[172,1783,402],{"emptyLinePlaceholder":401},[172,1785,1786],{"class":174,"line":1261},[172,1787,1788],{}," Serial.println(\"\\n\");\n",[172,1790,1791],{"class":174,"line":1267},[172,1792,429],{},[20,1794,1796],{"id":1795},"additional-information","Additional Information",[16,1798,1799,1800,1803,1804,1807],{},"This project was developed for the ",[24,1801,1802],{},"Nanotechnology"," course at the ",[24,1805,1806],{},"Engineering Summer Academy at Penn"," summer program that I took during the summer of 2025.",[16,1809,1810],{},"More pictures and information will be added to this page as I slowly organize them. ",[20,1812,1420],{"id":1419},[16,1814,1815],{},"This device was developed for educational purposes as part of the Engineering Summer Academy at Penn (summer 2025). Both methods are experimental and have known limitations.",[16,1817,1818,1429],{},[24,1819,1820],{},"The code and explanations are provided \"as-is\" without any warranty.",[16,1822,1432],{},[1434,1824,1825],{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":168,"searchDepth":303,"depth":303,"links":1827},[1828,1829,1830,1831,1832,1833],{"id":1476,"depth":303,"text":1477},{"id":1524,"depth":303,"text":1525},{"id":1573,"depth":303,"text":1574},{"id":1601,"depth":303,"text":1602},{"id":1795,"depth":303,"text":1796},{"id":1419,"depth":303,"text":1420},"2025-10-31","An Arduino-based device built on the ESP32 microchip that  remotely controls any Slide Show via hand gesture detection. ",{},"\u002Fprojects\u002Fwireless-motion-controlled-slide-show-clicker",{"repoUsername":1839,"repoName":1840},"AnsonZhang2009","wireless-motion-controlled-slide-show-clicker",{"title":1471,"description":1835},"projects\u002Fwireless-motion-controlled-slide-show-clicker",[1844,1845],"ESAP","Engineering","GGZ7VXcqrSyoCI1nKlNx_PBebW4Hl9Am1l5HisuhRkY",{"id":1848,"title":1849,"body":1850,"date":2120,"description":2121,"extension":1459,"image":2122,"meta":2123,"navigation":401,"path":2124,"repository":2125,"seo":2128,"stem":2129,"tags":2130,"__hash__":2132},"projects\u002Fprojects\u002Fmicrofluidics-gradient-analyzer.md","Microfluidics Gradient Analyzer",{"type":8,"value":1851,"toc":2106},[1852,1878,1882,1895,1903,1908,1914,1959,1962,1992,1995,2004,2010,2019,2023,2026,2029,2032,2039,2043,2046,2049,2056,2067,2071,2074,2078,2081,2083,2089,2091,2093,2096,2101,2103],[11,1853,1855,1858],{"icon":13,"title":1854},"TL;DR",[16,1856,1857],{},"Microfluidics Gradient Analyzer is a comprehensive pipeline written in R that includes two separate methods to quantify laminar flow in microfluidic devices under different flow rates.",[16,1859,1860,1861,1864,1865,1868,1871,1872,1877],{},"The first method includes using the ",[50,1862,1863],{},"magick:quantize"," function that the author utilized. The second method applies a ",[172,1866],{"style":1867},"font-size: 14px; background-color: oklch(0.269 0 0);",[50,1869,1870],{},"Sobel-Kernel"," filter (commonly used in Edge-Detection systems) to quantify gradient, inspired by ",[1504,1873,1876],{"href":1874,"rel":1875},"https:\u002F\u002Fdoi.org\u002F10.3390\u002Fjimaging4060074",[1508],"this paper",".",[20,1879,1881],{"id":1880},"laminar-flow","💦 Laminar Flow",[16,1883,1884,1885,1888,1889,1894],{},"Typically, at ",[50,1886,1887],{},"R \u003C 3000"," (Reynold's number), fluids exhibit ",[1504,1890,1893],{"href":1891,"rel":1892},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FLaminar_flow",[1508],"laminar flow",". Essentially, this means that instead of the fluid flowing in a chaotic, circular motion in turbulent flow, the liquids \"follow smooth paths in layers, with each layer moving smoothly past the adjacent layers with little or no mixing\" (Wikipedia). ",[20,1896,1898,1899,1902],{"id":1897},"method-i-quantizedetection-system","Method I - ",[50,1900,1901],{},"Quantize"," Detection System",[1566,1904,1905],{},[16,1906,1907],{},"This algorithm was only developed by me as a fun test. It simplifies a lot of the aspects related to gradient analysis and is by no means comprehensive. However, it is still a fun experiment on data analysis that I did that could give some inspiration.",[16,1909,1910,1911,1913],{},"This system utilizes the ",[50,1912,1863],{}," function to select the top 50 most prominent colors in any particular section of the microfluidics device where the gradient is most prominent. ",[163,1915,1919],{"className":1916,"code":1917,"language":1918,"meta":168,"style":168},"language-vue shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","palette \u003C- image_quantize(img, max = 50, colorspace = \"RGB\")\n","vue",[50,1920,1921],{"__ignoreMap":168},[172,1922,1923,1926,1928,1932,1936,1939,1942,1945,1948,1950,1952,1955,1957],{"class":174,"line":175},[172,1924,1925],{"class":251},"palette ",[172,1927,343],{"class":244},[172,1929,1931],{"class":1930},"swJcz","-",[172,1933,1935],{"class":1934},"spNyl"," image_quantize(img,",[172,1937,1938],{"class":1934}," max",[172,1940,1941],{"class":244}," =",[172,1943,1944],{"class":182}," 50,",[172,1946,1947],{"class":1934}," colorspace",[172,1949,1941],{"class":244},[172,1951,479],{"class":244},[172,1953,1954],{"class":182},"RGB",[172,1956,615],{"class":244},[172,1958,1038],{"class":1934},[16,1960,1961],{},"The pixels are then extracted. ",[163,1963,1965],{"className":1916,"code":1964,"language":1918,"meta":168,"style":168},"data \u003C- image_data(palette, channels = \"RGB\")\n",[50,1966,1967],{"__ignoreMap":168},[172,1968,1969,1972,1974,1976,1979,1982,1984,1986,1988,1990],{"class":174,"line":175},[172,1970,1971],{"class":251},"data ",[172,1973,343],{"class":244},[172,1975,1931],{"class":1930},[172,1977,1978],{"class":1934}," image_data(palette,",[172,1980,1981],{"class":1934}," channels",[172,1983,1941],{"class":244},[172,1985,479],{"class":244},[172,1987,1954],{"class":182},[172,1989,615],{"class":244},[172,1991,1038],{"class":1934},[16,1993,1994],{},"Then the number of unique pixels are counted. The higher the number of unique pixels, the more gradual the gradient, which reflects a higher flow rate. If the pixels are very similar, it would mean that the gradient was very abrupt, indicating a higher flow rate. ",[64,1996,1997],{},[16,1998,1999,2000,1877],{},"As you can see, there are key flaws within this algorithm. It doesn't take into account that very gradual gradients might have very similar pixels, resulting in a decrease in the number of unique pixels, ",[2001,2002,2003],"em",{},"even after the preprocessing",[20,2005,2007,2008,1902],{"id":2006},"method-ii-sobel-kerneldetection-system","Method II - ",[50,2009,1870],{},[16,2011,2012,2013,2015,2016,1516],{},"This algorithm is a lot more comprehensive than the ",[50,2014,1901],{}," detection system. It includes steps for normalization, Sobel convolution, gradient calculation, non-maximum suppression, and graphing with quantitative metrics. This was modified based on this paper ",[1504,2017,1515],{"href":1874,"rel":2018},[1508],[141,2020,2022],{"id":2021},"step-1-image-normalization","Step 1: Image Normalization",[16,2024,2025],{},"In this step, the images were normalized in a three-part process.",[16,2027,2028],{},"The first part includes image scaling. Since the input images may have different x-values and y-values, they were all scaled to the same height (since height remains a more important factor of measure where vertical gradients are important). ",[16,2030,2031],{},"The second part includes grey-scaling. By grey-scaling the image, we can better observe this gradient change from green to red. This also allows the third step to be conducted more easily. ",[16,2033,2034,2035,2038],{},"The third part includes changing normalizing the illumination of the images, achieved through a function in the ",[50,2036,2037],{},"magick"," package. ",[141,2040,2042],{"id":2041},"step-2-sobel-convolution","Step 2: Sobel Convolution",[16,2044,2045],{},"The Sobel operator is a convolution filter commonly used in edge-detection systems.",[16,2047,2048],{},"Edge-detection systems usually work by taking the partial derivatives of the change in color intensity in the x- and y-direction to find the local maxima or minima, and defines that as an edge. Convolution filters allow the partial derivatives to be approximated through the intensity of neighboring pixels. The tangent of the y-partial derivative over the x-partial derivative could also be calculated to determine the gradient direction. ",[16,2050,2051,2052,2055],{},"In our case, the",[50,2053,2054],{},"3x3 Sobel convolution filter","was chosen.",[1566,2057,2058],{},[16,2059,2060,2061,2066],{},"More information on convolution filters can be found in ",[1504,2062,2065],{"href":2063,"rel":2064},"https:\u002F\u002Fyoutu.be\u002FlOEBsQodtEQ?si=cIia9N8H189Hah-v",[1508],"this video",", which helped me out a lot during my process of learning. ",[141,2068,2070],{"id":2069},"step-3-non-maximum-suppression","Step 3: Non-maximum Suppression ",[16,2072,2073],{},"Since the Sobel operator is applied on every pixel on the image, we would need to extract prominent \"edges\" that can be used to determine the gradient. Hence, we refined our edge (gradient) detection system by only keeping local maxima. ",[141,2075,2077],{"id":2076},"step-4-quantitative-metrics-graphing","Step 4: Quantitative Metrics & Graphing",[16,2079,2080],{},"We quantified the gradient by calculating the number of edge pixels, which reflected the intensity of the gradient. A gradual gradient would have a higher number of pixels since there would be more transitions. A shaper gradient would only have a few number of edge pixels since there would only be one sharp transition. Graphing was done by comparing flow-rate against the number of edge pixels. ",[20,2082,1796],{"id":1795},[16,2084,1799,2085,1803,2087,1807],{},[24,2086,1802],{},[24,2088,1806],{},[16,2090,1810],{},[20,2092,1420],{"id":1419},[16,2094,2095],{},"This pipeline was developed for educational purposes as part of the Engineering Summer Academy at Penn (summer 2025). Both methods are experimental and have known limitations.",[16,2097,2098,1429],{},[24,2099,2100],{},"This software is provided \"as-is\" without any warranty.",[16,2102,1432],{},[1434,2104,2105],{},"html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":168,"searchDepth":303,"depth":303,"links":2107},[2108,2109,2111,2118,2119],{"id":1880,"depth":303,"text":1881},{"id":1897,"depth":303,"text":2110},"Method I - Quantize Detection System",{"id":2006,"depth":303,"text":2112,"children":2113},"Method II - Sobel-Kernel Detection System",[2114,2115,2116,2117],{"id":2021,"depth":405,"text":2022},{"id":2041,"depth":405,"text":2042},{"id":2069,"depth":405,"text":2070},{"id":2076,"depth":405,"text":2077},{"id":1795,"depth":303,"text":1796},{"id":1419,"depth":303,"text":1420},"2025-10-23","A simple microfluidics gradient analyzer I wrote in R to analyze gradients in microfluidic devices under different flow rates. ","https:\u002F\u002Fcdn.the-scientist.com\u002Fassets\u002FarticleNo\u002F71667\u002FhImg\u002F52223\u002Fmicrofluidics-biology-s-liquid-revolution-1800x720-x.webp",{},"\u002Fprojects\u002Fmicrofluidics-gradient-analyzer",{"repoUsername":2126,"repoName":2127},"ansonzhang2009","MicrofluidicsGradientAnalysis",{"title":1849,"description":2121},"projects\u002Fmicrofluidics-gradient-analyzer",[1844,2131],"Analysis","iuHN04Dexyl8XsuCtO4Fo3w5eS7jSX9gznh7Z80l2MA",1777421405535]