mirror of
https://github.com/len0rd/personal-website.git
synced 2025-03-01 03:51:57 -05:00
commit
a8b32dd8dc
8
.devcontainer/devcontainer.json
Normal file
8
.devcontainer/devcontainer.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"build": {
|
||||
"context": "../",
|
||||
"dockerfile": "../Dockerfile",
|
||||
"target": "dev",
|
||||
},
|
||||
"forwardPorts": [8090]
|
||||
}
|
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -1,5 +1,5 @@
|
|||
*.DS_STORE
|
||||
node_modules
|
||||
views/partials/md/
|
||||
views/partials/generated/
|
||||
_website/
|
||||
*.mp4
|
||||
.doctrees/
|
||||
__pycache__/
|
||||
|
|
36
Dockerfile
36
Dockerfile
|
@ -1,18 +1,30 @@
|
|||
FROM node:14
|
||||
FROM python:3.11-bullseye AS dev
|
||||
|
||||
# Create app directory
|
||||
WORKDIR /usr/src/app
|
||||
ADD pip-requirements.txt /tmp/pip-requirements.txt
|
||||
RUN pip install -r /tmp/pip-requirements.txt \
|
||||
&& rm /tmp/pip-requirements.txt
|
||||
|
||||
# Install app dependencies
|
||||
# A wildcard is used to ensure both package.json AND package-lock.json are copied
|
||||
# where available (npm@5+)
|
||||
COPY package*.json ./
|
||||
# install drawio for doc diagram gen
|
||||
ENV DRAWIO_VERSION "21.1.2"
|
||||
RUN apt update \
|
||||
&& apt install -yq --no-install-recommends \
|
||||
xvfb \
|
||||
wget \
|
||||
libnotify4 \
|
||||
libgbm1 \
|
||||
libasound2 \
|
||||
libxss1 \
|
||||
libsecret-1-0 \
|
||||
&& wget https://github.com/jgraph/drawio-desktop/releases/download/v${DRAWIO_VERSION}/drawio-amd64-${DRAWIO_VERSION}.deb \
|
||||
&& apt install -y ./drawio-amd64-${DRAWIO_VERSION}.deb \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& rm -rf drawio-amd64-${DRAWIO_VERSION}.deb
|
||||
ENV XVFB_DISPLAY ":42"
|
||||
|
||||
RUN npm install
|
||||
FROM dev as prod
|
||||
|
||||
# Bundle app source
|
||||
COPY . .
|
||||
|
||||
COPY . /website
|
||||
WORKDIR /website
|
||||
EXPOSE 8090
|
||||
|
||||
CMD [ "npm", "start" ]
|
||||
ENTRYPOINT [ "/bin/bash", "/website/container_serve.sh" ]
|
||||
|
|
|
@ -21,6 +21,10 @@ Its a website! Node/npm
|
|||
1. From this directory install dependencies with `npm install`
|
||||
2. Run the server with `npm start`
|
||||
|
||||
## Build the docker image
|
||||
## Build the production docker image
|
||||
|
||||
`docker build -t leo_website:latest .`
|
||||
|
||||
## Run the production container
|
||||
|
||||
`docker run --rm -td -p 8888:8090 leo_website:prod`
|
||||
|
|
1
_templates/sitename.html
Normal file
1
_templates/sitename.html
Normal file
|
@ -0,0 +1 @@
|
|||
<a class="navbar-brand text-wrap" href="{{ pathto(master_doc) }}"><h1 class="site-logo" id="site-title">{{ html_title }}</h1></a>
|
1
assets/.gitignore
vendored
Normal file
1
assets/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
projects/
|
|
@ -1,37 +0,0 @@
|
|||
img {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-top: 2vh;
|
||||
padding-bottom: 2vh;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
iframe {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 50%;
|
||||
margin-top: 2vh;
|
||||
margin-bottom: 2vh;
|
||||
}
|
||||
|
||||
p {
|
||||
padding-left: 5%;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-left: 5%;
|
||||
}
|
||||
|
||||
ol {
|
||||
margin-left: 5%;
|
||||
}
|
||||
|
||||
li p {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.sticky-offset {
|
||||
top: 56px !important;
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
.icon-footer {
|
||||
padding: 3%;
|
||||
padding: 2vh;
|
||||
}
|
||||
|
||||
.bgimage {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background: url('../img/logo.svg');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
.bgimage h1 {
|
||||
color: white;
|
||||
text-shadow: 2px 2px #333;
|
||||
}
|
||||
|
||||
.center {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
}
|
||||
|
||||
|
||||
.cenMain {
|
||||
padding-top: 40%;
|
||||
padding-bottom: 40%;
|
||||
padding-top: 40vh;
|
||||
padding-bottom: 40vh;
|
||||
width: 60%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.container-cenMain {
|
||||
height: 100%;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.topMargin {
|
||||
padding-top: 5%;
|
||||
padding-top: 5vh;
|
||||
width: 60%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.code {
|
||||
background-color: #09003d;
|
||||
}
|
||||
|
||||
.card-soft-link {
|
||||
color: #5AB2DA;
|
||||
padding-left: 2%;
|
||||
}
|
||||
|
||||
.card-homepage {
|
||||
background-color: rgba(0, 0, 0, 0.1) !important;
|
||||
padding: 2%;
|
||||
height: 100%;
|
||||
}
|
75
assets/diagrams/hal_concept.drawio
Normal file
75
assets/diagrams/hal_concept.drawio
Normal file
|
@ -0,0 +1,75 @@
|
|||
<mxfile host="65bd71144e">
|
||||
<diagram id="SKV8PHPIui5c4Fc6kAOh" name="Page-1">
|
||||
<mxGraphModel dx="623" dy="291" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="11" style="edgeStyle=orthogonalEdgeStyle;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;endArrow=diamond;endFill=0;" parent="1" source="2" target="8" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="14" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=diamond;endFill=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" parent="1" source="2" target="12" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="460" y="100"/>
|
||||
<mxPoint x="425" y="100"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="2" value="Hardware Abstraction Layer (Interfaces of core hardware drivers)" style="html=1;align=center;verticalAlign=top;rounded=1;absoluteArcSize=1;arcSize=10;dashed=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="250" y="260" width="360" height="70" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="3" value="ISPI" style="html=1;align=center;verticalAlign=top;rounded=1;absoluteArcSize=1;arcSize=10;dashed=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="250" y="290" width="70" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="4" value="IGPIO" style="html=1;align=center;verticalAlign=top;rounded=1;absoluteArcSize=1;arcSize=10;dashed=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="320" y="290" width="70" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="5" value="IUART" style="html=1;align=center;verticalAlign=top;rounded=1;absoluteArcSize=1;arcSize=10;dashed=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="390" y="290" width="70" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="6" value="II2C" style="html=1;align=center;verticalAlign=top;rounded=1;absoluteArcSize=1;arcSize=10;dashed=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="460" y="290" width="70" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="7" value="..." style="html=1;align=center;verticalAlign=top;rounded=1;absoluteArcSize=1;arcSize=10;dashed=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="530" y="290" width="80" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="13" style="edgeStyle=orthogonalEdgeStyle;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;endArrow=diamond;endFill=0;" parent="1" source="8" target="12" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="8" value="Device Drivers <br>(decoupled from underlying<br>&nbsp;hardware driver implementation)" style="html=1;align=center;verticalAlign=top;rounded=1;absoluteArcSize=1;arcSize=10;dashed=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="250" y="120" width="180" height="100" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="9" value="IMU" style="html=1;align=center;verticalAlign=top;rounded=1;absoluteArcSize=1;arcSize=10;dashed=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="250" y="180" width="70" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="10" value="etc etc" style="html=1;align=center;verticalAlign=top;rounded=1;absoluteArcSize=1;arcSize=10;dashed=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="320" y="180" width="70" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="12" value="Libraries<br>(Business logic that uses a combination of device and<br>hardware driver interfaces)" style="html=1;align=center;verticalAlign=top;rounded=1;absoluteArcSize=1;arcSize=10;dashed=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="250" y="10" width="350" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="16" style="edgeStyle=orthogonalEdgeStyle;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;endArrow=classicThin;endFill=0;" parent="1" source="15" target="5" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="15" value="Hardware-specific implementations of interfaces" style="html=1;align=center;verticalAlign=top;rounded=1;absoluteArcSize=1;arcSize=10;dashed=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="245" y="370" width="360" height="70" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="17" value="MCU X<br>implementation" style="html=1;align=center;verticalAlign=top;rounded=1;absoluteArcSize=1;arcSize=10;dashed=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="262.5" y="410" width="117.5" height="70" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="18" value="MCU Y<br>implementation" style="html=1;align=center;verticalAlign=top;rounded=1;absoluteArcSize=1;arcSize=10;dashed=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="410" y="410" width="120" height="70" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="19" value="" style="shape=curlyBracket;whiteSpace=wrap;html=1;rounded=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="210" y="400" width="20" height="90" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="20" value="" style="shape=curlyBracket;whiteSpace=wrap;html=1;rounded=1;flipH=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="620" y="400" width="20" height="90" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="22" value="<h1><br></h1><div>Implementation is build-time selectable depending on your target environment</div>" style="text;html=1;strokeColor=none;fillColor=none;spacing=5;spacingTop=-20;whiteSpace=wrap;overflow=hidden;rounded=0;align=right;" parent="1" vertex="1">
|
||||
<mxGeometry x="20" y="370" width="190" height="110" as="geometry"/>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
112
assets/diagrams/hal_concept_with_board.drawio
Normal file
112
assets/diagrams/hal_concept_with_board.drawio
Normal file
|
@ -0,0 +1,112 @@
|
|||
<mxfile host="65bd71144e">
|
||||
<diagram id="SKV8PHPIui5c4Fc6kAOh" name="Page-1">
|
||||
<mxGraphModel dx="1739" dy="1765" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="30" value="Application ABC<br>- top-level execution (main())<br>- initializes libraries required for app using<br>interface references from Board Data ABC" style="html=1;align=center;verticalAlign=top;rounded=1;absoluteArcSize=1;arcSize=10;dashed=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="155" y="-200" width="270" height="160" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="11" style="edgeStyle=orthogonalEdgeStyle;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;endArrow=diamond;endFill=0;" parent="1" source="2" target="8" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="14" style="edgeStyle=orthogonalEdgeStyle;html=1;endArrow=diamond;endFill=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" parent="1" source="2" target="12" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="460" y="90"/>
|
||||
<mxPoint x="470" y="90"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="2" value="Hardware Abstraction Layer (Interfaces of core hardware drivers)" style="html=1;align=center;verticalAlign=top;rounded=1;absoluteArcSize=1;arcSize=10;dashed=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="250" y="260" width="360" height="70" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="3" value="ISPI" style="html=1;align=center;verticalAlign=top;rounded=1;absoluteArcSize=1;arcSize=10;dashed=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="250" y="290" width="70" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="4" value="IGPIO" style="html=1;align=center;verticalAlign=top;rounded=1;absoluteArcSize=1;arcSize=10;dashed=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="320" y="290" width="70" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="5" value="IUART" style="html=1;align=center;verticalAlign=top;rounded=1;absoluteArcSize=1;arcSize=10;dashed=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="390" y="290" width="70" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="6" value="II2C" style="html=1;align=center;verticalAlign=top;rounded=1;absoluteArcSize=1;arcSize=10;dashed=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="460" y="290" width="70" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="7" value="..." style="html=1;align=center;verticalAlign=top;rounded=1;absoluteArcSize=1;arcSize=10;dashed=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="530" y="290" width="80" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="13" style="edgeStyle=orthogonalEdgeStyle;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;endArrow=diamond;endFill=0;" parent="1" source="8" target="12" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="28" style="edgeStyle=orthogonalEdgeStyle;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;endArrow=diamond;endFill=0;" edge="1" parent="1" source="8" target="27">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="340" y="90"/>
|
||||
<mxPoint x="290" y="90"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="8" value="Device Drivers <br>(decoupled from underlying<br>&nbsp;hardware driver implementation)" style="html=1;align=center;verticalAlign=top;rounded=1;absoluteArcSize=1;arcSize=10;dashed=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="250" y="120" width="180" height="100" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="9" value="IMU" style="html=1;align=center;verticalAlign=top;rounded=1;absoluteArcSize=1;arcSize=10;dashed=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="250" y="180" width="70" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="10" value="etc etc" style="html=1;align=center;verticalAlign=top;rounded=1;absoluteArcSize=1;arcSize=10;dashed=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="320" y="180" width="70" height="40" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="31" style="edgeStyle=orthogonalEdgeStyle;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;endArrow=diamond;endFill=0;" edge="1" parent="1" source="12" target="27">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="12" value="Libraries<br>(Business logic that uses a combination of device and<br>hardware driver interfaces)" style="html=1;align=center;verticalAlign=top;rounded=1;absoluteArcSize=1;arcSize=10;dashed=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="325" y="10" width="290" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="16" style="edgeStyle=orthogonalEdgeStyle;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;endArrow=classicThin;endFill=0;" parent="1" source="15" target="5" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="26" style="edgeStyle=orthogonalEdgeStyle;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;endArrow=diamond;endFill=0;" edge="1" parent="1" source="15" target="23">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="15" value="Hardware-specific implementations of interfaces" style="html=1;align=center;verticalAlign=top;rounded=1;absoluteArcSize=1;arcSize=10;dashed=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="245" y="370" width="360" height="70" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="17" value="MCU X<br>implementation" style="html=1;align=center;verticalAlign=top;rounded=1;absoluteArcSize=1;arcSize=10;dashed=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="262.5" y="410" width="117.5" height="70" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="18" value="MCU Y<br>implementation" style="html=1;align=center;verticalAlign=top;rounded=1;absoluteArcSize=1;arcSize=10;dashed=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="410" y="410" width="120" height="70" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="19" value="" style="shape=curlyBracket;whiteSpace=wrap;html=1;rounded=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="210" y="400" width="20" height="90" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="20" value="" style="shape=curlyBracket;whiteSpace=wrap;html=1;rounded=1;flipH=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="620" y="400" width="20" height="90" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="22" value="<h1><br></h1><div>Implementation is build-time selectable depending on your target environment</div>" style="text;html=1;strokeColor=none;fillColor=none;spacing=5;spacingTop=-20;whiteSpace=wrap;overflow=hidden;rounded=0;align=right;" parent="1" vertex="1">
|
||||
<mxGeometry x="20" y="370" width="190" height="110" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="29" style="edgeStyle=orthogonalEdgeStyle;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;endArrow=diamond;endFill=0;" edge="1" parent="1" source="23" target="27">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="105" y="90"/>
|
||||
<mxPoint x="290" y="90"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="23" value="Board Configuration info:<br>- gpio configuration<br>- clock configuration<br>- any special bootup specifics" style="html=1;align=center;verticalAlign=top;rounded=1;absoluteArcSize=1;arcSize=10;dashed=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="-30" y="120" width="270" height="105" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="24" value="Board A<br>Has "MCU X"" style="html=1;align=center;verticalAlign=top;rounded=1;absoluteArcSize=1;arcSize=10;dashed=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="-20" y="200" width="120" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="25" value="Board B<br>Has "MCU Y"" style="html=1;align=center;verticalAlign=top;rounded=1;absoluteArcSize=1;arcSize=10;dashed=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="110" y="200" width="120" height="60" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="27" value="Board Data ABC<br>- Selects board<br>- initializes board-specific drivers that are<br>required by the application" style="html=1;align=center;verticalAlign=top;rounded=1;absoluteArcSize=1;arcSize=10;dashed=0;" vertex="1" parent="1">
|
||||
<mxGeometry x="155" y="-120" width="270" height="80" as="geometry"/>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
5
assets/script/dynamicscrollspy.min.js
vendored
5
assets/script/dynamicscrollspy.min.js
vendored
|
@ -1,5 +0,0 @@
|
|||
/**
|
||||
* AutoScrollspy v 0.2.0
|
||||
* https://github.com/psalmody/dynamic-scrollspy
|
||||
*/
|
||||
!function($){$.fn.DynamicScrollspy=function(opts){function encodeHTML(value){return $("<div></div>").text(value).html()}function selectAllH(){for(var st=[],i=self.options.tH;i<=self.options.bH;i++)st.push("H"+i);return $(st.join(",")).not(self.options.exclude)}function randID(){function rand(){r=Math.floor(900*Math.random())+100}var r;for(rand();self.rands.indexOf(r)>=0;)rand();return self.rands.push(r),r}function genIDs(){selectAllH().prop("id",function(){return""===$(this).prop("id")?$(this).prop("tagName")+randID():$(this).prop("id")})}function checkIDs(){var missing=0;if(selectAllH().each(function(){if(""===$(this).prop("id"))missing++;else if($('[id="'+$(this).prop("id")+'"]').length>1)throw new Error("DynamicScrollspy: Error! Duplicate id "+$(this).prop("id"))}),missing>0){var msg="DynamicScrollspy: Not all headers have ids and genIDs: false.";throw new Error(msg)}return missing}function showTesting(){selectAllH().append(function(){return" ("+$(this).prop("tagName")+", "+$(this).prop("id")+")"})}function makeTree(){var tree=self.tree;return $("H"+self.options.tH).not(self.options.exclude).each(function(){tree[$(this).prop("id")]={dstext:encodeHTML($(this).text()),jqel:$(this)}}),self.options.tH+1<=self.options.bH&&itCreateTree(tree),tree}function itCreateTree(what){for(var k in what)if(""!==k&&"dstext"!=k&&"jqel"!=k){var lvl=Number($("#"+k).prop("tagName").replace("H",""));if(lvl>=self.options.bH)return!1;$("#"+k).nextUntil("H"+lvl).filter("H"+(lvl+1)).not(self.options.exclude).each(function(){what[k][$(this).prop("id")]={dstext:encodeHTML($(this).text()),jqel:$(this)}}),lvl<self.options.bH&&itCreateTree(what[k])}}function renderTree(){var ul=$('<ul class="nav '+self.options.ulClassNames+'"></ul>');return self.append(ul),$.each(self.tree,function(k){var c=self.tree[k],li='<li id="dsli'+k+'" class="nav-item"><a href="#'+k+'" class="nav-link">'+c.dstext+"</a></li>";ul.append(li),itRenderTree(self.tree[k])}),self}function itRenderTree(what){if(Object.keys(what).length<3)return!1;var parent=$("#dsli"+what.jqel.prop("id")),ul=$("<ul class='nav child'></ul>");parent.append(ul);for(var k in what)if("dstext"!=k&&"jqel"!=k){var c=what[k];ul.append('<li id="dsli'+k+'" class="nav-item"><a href="#'+k+'" class="nav-link">'+c.dstext+"</a></li>"),itRenderTree(what[k])}}function init(){if(self.isinit===!1){if(self.options.genIDs?genIDs():checkIDs(),self.options.testing&&showTesting(),makeTree(),renderTree(),self.options.affix&&"function"==typeof self.children("ul").affix){var ul=self.children("ul");self.children("ul").affix({offset:{top:function(){var c=ul.offset().top,d=parseInt(ul.children(0).css("margin-top"),10),e=$(self).height();return this.top=c-e-d},bottom:function(){return this.bottom=$(self).outerHeight(!0)}}})}$("body").attr("data-spy","true").scrollspy({target:"#"+self.prop("id"),offset:self.options.offset}),self.isinit=!0}else makeTree(),renderTree(),$('[data-spy="scroll"]').each(function(){$(this).scrollspy("refresh")});return self}if(opts="undefined"==typeof opts?{}:opts,this.isinit="undefined"!=typeof this.isinit&&self.isinit,"destroy"==opts)return this.isinit=!1,this.empty(),this.off("activate.bs.scrollspy"),$("body").removeAttr("data-spy"),this;this.options=$.extend({},{affix:!0,tH:2,bH:6,exclude:!1,genIDs:!1,offset:100,ulClassNames:"hidden-print",activeClass:"",testing:!1},this.options,opts);var self=this;return this.tree={},this.rands=[],init()}}(jQuery);
|
54
conf.py
Normal file
54
conf.py
Normal file
|
@ -0,0 +1,54 @@
|
|||
# Sphinx docs configuration for building project documentation
|
||||
from datetime import datetime
|
||||
|
||||
project = "lenordsNet"
|
||||
author = "lenord"
|
||||
copyright = f"{datetime.now().year}, lenordsNet"
|
||||
|
||||
extensions = [
|
||||
"sphinxcontrib.youtube",
|
||||
"ablog",
|
||||
"sphinx.ext.intersphinx",
|
||||
"sphinx_design",
|
||||
"sphinxcontrib.drawio",
|
||||
]
|
||||
|
||||
templates_path = ["_templates"]
|
||||
|
||||
blog_baseurl = "https://lenord.me/"
|
||||
# A path relative to the configuration directory for posts archive pages.
|
||||
blog_path = "posts"
|
||||
# The "title" for the posts, used in active pages. Default is ``'Blog'``.
|
||||
blog_title = "lenordsNet"
|
||||
|
||||
fontawesome_included = True
|
||||
|
||||
html_baseurl = blog_baseurl
|
||||
html_title = blog_title
|
||||
|
||||
html_theme = "pydata_sphinx_theme"
|
||||
html_theme_options = {
|
||||
"search_bar_text": "search ...",
|
||||
"show_prev_next": False,
|
||||
"navbar_center": [],
|
||||
}
|
||||
html_favicon = "assets/img/favicon.ico"
|
||||
|
||||
html_sidebars = {
|
||||
"*": [
|
||||
"ablog/recentposts.html",
|
||||
"ablog/archives.html",
|
||||
],
|
||||
"posts/**": [
|
||||
"ablog/postcard.html",
|
||||
"ablog/recentposts.html",
|
||||
"ablog/archives.html",
|
||||
],
|
||||
}
|
||||
|
||||
html_context = {"html_title": html_title}
|
||||
|
||||
pygments_style = "sas"
|
||||
|
||||
drawio_headless = True
|
||||
drawio_no_sandbox = True
|
4
container_serve.sh
Executable file
4
container_serve.sh
Executable file
|
@ -0,0 +1,4 @@
|
|||
#!/bin/bash
|
||||
|
||||
ablog build
|
||||
python -m http.server -d /website/_website/ 8090
|
15
index.rst
Normal file
15
index.rst
Normal file
|
@ -0,0 +1,15 @@
|
|||
Recent Posts
|
||||
============
|
||||
|
||||
.. toctree::
|
||||
:name: mastertoc
|
||||
:caption: Contents
|
||||
:titlesonly:
|
||||
:hidden:
|
||||
|
||||
.. postlist:: 10
|
||||
:author: len0rd
|
||||
:date: %Y-%m-%d
|
||||
:format: {date} - {title}
|
||||
:list-style: none
|
||||
:excerpts:
|
1392
package-lock.json
generated
1392
package-lock.json
generated
File diff suppressed because it is too large
Load diff
30
package.json
30
package.json
|
@ -1,30 +0,0 @@
|
|||
{
|
||||
"name": "len0rd_net",
|
||||
"version": "0.0.1",
|
||||
"description": "src for lenords.net",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"prestart": "node prestart.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/len0rd/personal-website.git"
|
||||
},
|
||||
"author": "len0rd",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/len0rd/personal-website/issues"
|
||||
},
|
||||
"homepage": "https://github.com/len0rd/personal-website",
|
||||
"dependencies": {
|
||||
"dynamic-scrollspy": "^0.2.0",
|
||||
"ejs": "^3.1.8",
|
||||
"express": "^4.17.1",
|
||||
"markdown-it": "^13.0.1",
|
||||
"markdown-it-hashtag": "^0.4.0",
|
||||
"mkdirp": "^1.0.4",
|
||||
"showdown": "^2.1.0",
|
||||
"showdown-highlight": "^3.0.0"
|
||||
}
|
||||
}
|
6
pip-requirements.txt
Normal file
6
pip-requirements.txt
Normal file
|
@ -0,0 +1,6 @@
|
|||
sphinx
|
||||
ablog
|
||||
sphinx-design
|
||||
sphinxcontrib-youtube
|
||||
pydata-sphinx-theme
|
||||
sphinxcontrib-drawio
|
123
posts/darkstar.rst
Normal file
123
posts/darkstar.rst
Normal file
|
@ -0,0 +1,123 @@
|
|||
.. darkstar:
|
||||
|
||||
Darkstar Quadcopter
|
||||
===================
|
||||
|
||||
.. post:: 31, July 2018
|
||||
:tags: drone, diy
|
||||
:category: Projects
|
||||
:author: len0rd
|
||||
|
||||
The Darkstar is a RC quadcopter with the ability to fly autonomously through pre-designated waypoints, using advanced estimation techniques and object avoidance.
|
||||
|
||||
At least that is the end-goal. Getting there however requires resources I simply do not have as a college student (read: money). Given such constraints, building an advance copter on the cheap sounded like a good challenge. Perhaps you can learn from my mistakes if you're interested in such a venture.
|
||||
|
||||
Having landed a gig at an autonomous flight research center for the summer, I figured now was as good a time as any to dive into the exciting world of quadrotors - a world I've always been interested in, but never had the time to sit down and learn.
|
||||
|
||||
Part 1: Part Selection
|
||||
----------------------
|
||||
|
||||
As is the case with most hobbies, much of my money was spent on initial startup costs: an RC transmitter and battery charger, while essential, would only need to be purchased once. If I want to build more rc-craft in the future, this will make it a lot easier.
|
||||
|
||||
.. image:: ../assets/img/writeup/darkstar/frsky-controller.jpg
|
||||
|
||||
**Transmitter:** I went with a `FrSky Taranis QX7 <https://www.amazon.com/dp/B06XC4C4WH>`_ for a few reasons:
|
||||
|
||||
1. It's "cheap": This is a pretty feature-rich transmitter for the price. Amazon has it priced fairly high, but I managed to pick mine up from `ProgressiveRC <https://www.progressiverc.com/>`_ for $105, shipping was free and fast.
|
||||
|
||||
2. Experience: I've been using/fiddling around with this transmitter at work while building some vehicles, so I had at least an introduction to the system, leaving me with one less thing to learn.
|
||||
|
||||
3. Aesthetic: From what I've seen, there are few transmitters out there that have any semblance of competent industrial design. This happens to be one of them. It's well-built with decent ergonomics and doesn't look overly tacky or intimidating. Plus it comes in white, which I really have been enjoying lately for some reason.
|
||||
|
||||
**Frame:** Really nothing special here. I got a `Reptile 500 <https://www.ebay.com/sch/i.html?_nkw=reptile500+v3+quadcopter+frame>`_ frame... or something. Honestly not sure on the name here. Basically my strategy was to browse `hobbyking <https://hobbyking.com>`_ til I found something I liked, then I headed over to ebay, to buy something similar. You can get frames for pretty cheap off there if you're willing to wait for it to ship from China. I wasn't, so I paid an extra $10 to get it from a US seller.
|
||||
|
||||
As Shipped:
|
||||
.. image:: ../assets/img/writeup/darkstar/frame-boxed-sm.jpg
|
||||
|
||||
(Mostly) Assembled:
|
||||
.. image:: ../assets/img/writeup/darkstar/frame-assembled-sm.jpg
|
||||
|
||||
Some of the arms were a pain to secure:
|
||||
.. image:: ../assets/img/writeup/darkstar/frame-armtrouble-sm.jpg
|
||||
|
||||
Overall I'm pretty happy with the frame. It's simple and it was cheap. At first I thought 500mm between motors would be huge, but I've grown to like it and how much space it gives me. I have plenty of room to jam all of my various gizmos throughout. Being cheap and from ebay, it was a bit of an effort to secure all the arms to the frame; aligning the holes was more difficult than anticipated. But once mounted they're pretty solid, and have already survived a few crashes with ease.
|
||||
|
||||
**Motors:** `LHI 2212 920KV <https://www.amazon.com/dp/B00XQYTZQ2>`_ motors. Again, cheap and functional. The product I ordered from Amazon came with ESC's, which I thought was great, but I eventually had to swap out those ESC's, so overall, not worth the 'savings' I thought I was getting. I didn't have any idea as to what speed or motor rating I wanted, and initially I was worried that 920KV wouldn't be fast enough. However, seeing that 920 is used by the phantom reassured me and they work great. The copter isn't too acrobatic, but still has some 'umph'. Another great thing about these motors is they're built as DJI replacements, which means they also work with DJI's self-tightening props. Thank goodness! That makes portability/replacement so much easier.
|
||||
|
||||
I was stupid and tried to screw the motor in through all 4 holes on the arm with some aggressive dremeling, before realizing there were two holes for my motor size, and two for a different size.
|
||||
|
||||
.. image:: ../assets/img/writeup/darkstar/motor-attached-sm.jpg
|
||||
|
||||
DJI props! So much easier than the other nightmares I've worked with in the past.
|
||||
|
||||
.. image:: ../assets/img/writeup/darkstar/motor-djiprops-sm.jpg
|
||||
|
||||
**Receiver:** `FrSky D4R-II <https://www.amazon.com/gp/product/B00SWHWFWO/>`_ Cheap, compatible, capable. I would be comfortable with any FrSky CPPM receiver here.
|
||||
|
||||
**ESC:** `Makerfire 20A <https://www.amazon.com/gp/product/B01DEN46I6>`_ As I mentioned above, the ESC's that came with my motors had some weird issues... Actually come to think of it, it was likely my own stupidity that was the issue. It's okay though, the makerfire esc's get the job done, and are a factor of magnitude smaller/lighter than my original esc's, so I'll consider that a win.
|
||||
|
||||
**Flight Controller:** Flip32+ This is one of those parts that I didn't want to skimp out on or mess around with. This is the board that we use fairly exclusively at work, so I'm familiar with it, and it's a reasonable price. The cheaper Chinese versions of these have been known to have some IMU/Gyro issues, so we only buy these from `readytoflyquads <http://www.readytoflyquads.com/the-flip32-187>`_
|
||||
|
||||
**Battery:** Currently I'm using a 2200mAh 3S LiPo battery, but as of writing this, I'm looking at stepping up to a 4 or 5000mAh. 2200 is adequate in terms of flight time, but as I throw more gear on this thing, it'd be good to have something a bit larger.
|
||||
|
||||
Assembly
|
||||
^^^^^^^^
|
||||
|
||||
Mostly I just added things on here and there as I got them in the mail. I had most of the frame pieces setup and ready to go by the time I had my big 'assembly party'... alone... on a Friday night.... Help me:
|
||||
|
||||
Receiver mounted on top with a twist tie, ESC's secured on the arm with some good velcro/zip ties:
|
||||
.. image:: ../assets/img/writeup/darkstar/assembly-1-sm.jpg
|
||||
|
||||
This plate+anti-shock mount combo was intended to go in the front of the drone for fpv. While that is something I would like to eventually add, this plate also happened to be the perfect size for the Flip32. So I drilled a few holes, allowing me to mount the naze as close to the center of gravity as was reasonable:
|
||||
.. image:: ../assets/img/writeup/darkstar/assembly-2-sm.jpg
|
||||
|
||||
Assembly can get messy:
|
||||
.. image:: ../assets/img/writeup/darkstar/assembly-3-sm.jpg
|
||||
|
||||
This is the handiest edition I think I've made. This allows me to plug in the battery with the confidence that the motors aren't going to immediately attack me. Currently I only use one side of the switch, but in the future I plan on having one side turn everything on, while the other side only turns on the small electronics(and not the finger-slicing motors):
|
||||
.. image:: ../assets/img/writeup/darkstar/assembly-4-sm.jpg
|
||||
|
||||
Power distribution soldered and mounted! The velcroed piece at the top is my 5V BEC:
|
||||
.. image:: ../assets/img/writeup/darkstar/assembly-5-sm.jpg
|
||||
|
||||
Naze (aka Flip32) mounted! This is all a bit tighter than anticipated:
|
||||
.. image:: ../assets/img/writeup/darkstar/assembly-6-sm.jpg
|
||||
|
||||
Todo: cable management
|
||||
.. image:: ../assets/img/writeup/darkstar/assembly-7-sm.jpg
|
||||
|
||||
Also Todo: Secure the battery in a non-terrible way
|
||||
.. image:: ../assets/img/writeup/darkstar/assembly-8-sm.jpg
|
||||
|
||||
Part 2: Fixes and Tweaks
|
||||
------------------------
|
||||
|
||||
Its been a few weeks since writing part 1 and a lot has changed. For starters the thing actually flies now. As I mentioned in part 1 I had some troubles getting my first set of esc's to work correctly. So I replaced them with new, smaller ones, and still had trouble with them. That is until I finally sat down and figured out how to calibrate them (protip: read the instructions that come with your products!). With that squared away, this hunk of junk finally became a flyable drone as opposed to a 180deg flipping machine, as shown in the video below.
|
||||
|
||||
.. youtube:: TKvzu6X0z1E
|
||||
|
||||
Unfortunately I dont have any footage of the first successful flights(I was out by myself, as usual), but just know it was legendary. Flight 1 went well until I accidentally crashed into a pine tree, which provided a surprisingly cushy landing. Flight 2 was absolutely beautiful until I somehow managed to clothesline my drone on literally the only power line in the immediate vicinity. The rest of the initial flights followed a similar pattern. All-in-all, I ended up buying another 4 sets (with 2/set) of props, after breaking 4 of them. Seeing as this was my first real drone-flying experience, and I was still tweaking some of the settings, I chalk it up to the cost of learning.
|
||||
|
||||
.. image:: ../assets/img/writeup/darkstar/tweaks-1-sm.jpg
|
||||
|
||||
Sim Practice
|
||||
^^^^^^^^^^^^
|
||||
|
||||
Around this time I also discovered a nifty, cheap piece of software: `fpv-freedrider <https://fpv-freerider.itch.io/fpv-freerider>`_ . This is simple but functional simulator that is handy to practice on. I simply plugged my transmitter via usb into my machine and it recognized it straight away. All I had to do was calibrate once in the software and I was off to the sim. This definitely helps with mastering some basic flight skills, but naturally its not as difficult as the real thing. And I've found the best way to learn to fly is having the constant threat of a catastrophic crash looming over you and every decision you make (while flying that is).
|
||||
|
||||
LED Upgrade
|
||||
^^^^^^^^^^^
|
||||
|
||||
These days its basically an undeniable fact that RGB LEDs make everything in this world better. And you'd be a fool to think I wasn't planning on adding them from the beginning. With the quad finally getting up in the air, I needed something to make my crashes look cooler. and boy-oh-boy did the LEDs fit the bill. The LEDs are programmed through an Arduino and change state/pattern based on the CPPM input coming from the rc receiver!
|
||||
|
||||
I've been planning for some time to integrate an Arduino nano into the copter because of the rapid prototyping I'll be able to do with various sensors that work with it. LED control was the perfect first-step to get the Arduino project online. This was also my first time working with the Arduino and LED control is the simple (and traditional!) place to start. Here's an overview of how the LED control works as of this writing:
|
||||
|
||||
- When the craft is disarmed, the LEDs fade in and out
|
||||
|
||||
- When armed, the LEDs switch to solid illumination
|
||||
|
||||
- If armed, and not in autopilot mode, if the incoming CPPM command does change enough (ie the current command hasn't deviated by some delta compared to multiple past commands), then the LEDs alternate from solid to blinking about one every 1.5 seconds, until a new unique command is read.
|
||||
|
||||
The backbone of this project is an absolutely killer `CPPM library <https://github.com/jmparatte/CPPM>`_ developed by Jean-Marc Paratte. The library is very simple to use, and the examples are self-explanatory. It was pretty accurate with my 8-channel FrSky receiver. With that library in place, the rest was just some good 'n simple state machine logic. If you're interested in taking a peek, here's `the repository <https://github.com/len0rd/darkstar_copter>`_ . Release 0.1 has the basic LED state machine using CPPM. After that release I've added some more features, making it a bit more complex (I'll write about those later, when I know they all work properly).
|
||||
|
||||
Assembly was easy. For now, I'm running all 4 LEDs off one MOSFET and 1 pin on the Arduino. This is so I have more pins available for other sensors in the future, but it would also be cool to have each arm individually controlled. `Here <https://www.amazon.com/gp/product/B017X92K9Y>`_ are the LEDs I used. `This guy <https://www.youtube.com/watch?v=sVyi7yWuXxs>`_ is pretty helpful if you need help figuring out how to use a MOSFET + Arduino to control 12V LEDs. These LEDs are actually a really good reason to use a 3S LiPo battery, since its standard voltage is ~12V ish.
|
123
posts/embedded_dev_primer.rst
Normal file
123
posts/embedded_dev_primer.rst
Normal file
|
@ -0,0 +1,123 @@
|
|||
.. embedded_dev_primer:
|
||||
|
||||
Embedded Development Primer
|
||||
===========================
|
||||
|
||||
.. post:: 05, January 2023
|
||||
:tags: diy, docker, embedded, development, toolchain, advice
|
||||
:category: Projects
|
||||
:author: len0rd
|
||||
|
||||
This post is meant to be an overview of some principles and practices I have found helpful for professional baremetal embedded development. This is meant for people who want to have total control over their embedded development environment and codebase.
|
||||
|
||||
Tools like `platformio <https://platformio.org/>`_ are excellent and can help us get projects off the ground but are perhaps a little opaque when it comes to toolchain and dependency management. In a professional setting, I often find I need total control over toolchains and dependencies down to the lowest-level. Learning how to intelligently manage these tools in a modern way has been a major effort in my career so far. This post will serve as a collection of my thoughts and lessons-learned from building multiple embedded projects from the ground up.
|
||||
|
||||
Lesson 1: Docker for toolchain management
|
||||
-----------------------------------------
|
||||
|
||||
Step in my shoes for a minute: You start a new job. As usual your first task is setting up a development machine for the project. How do you setup the machine? Ah of course, the project has a 10,000 word README with detailed instructions of how to set everything up. It requires you use a very specific linux distro/kernel, install packages in the 'correct' order, and adhere to a specific directory structure. You follow the steps. The project fails to build. You now spend a week combing through the README, comparing your machine to other developers, maybe even reinstalling and starting from scratch.
|
||||
|
||||
Does this scenario sound familiar? Its something I have encountered multiple times in my career. Obviously there are a lot of issues with the "installation README" approach. I've also worked projects where a monolithic bash script handles dependency setup. While slightly better, this often encounters the same problems as a README. The root of the issue is that existing developers do not have to consistently use the installation README/script. Therefore these helper utilities are not maintained and eventually fall out of date. They are only touched when a new developer is required to struggle through them. They also become a nightmare when larger upgrades (like a distro bump) become necessary.
|
||||
|
||||
These dependency-management issues extend far beyond just setting up a new environment. They also contribute to the core "it works on my machine" problems we so often encounter. In embedded development such problems are amplified since our dependencies (toolchains, debug packages) seemingly cant be neatly container-ized like other languages (python). `Docker <https://docs.docker.com/>`_ is the solution to all of these problems.
|
||||
|
||||
|
||||
What does docker provide?
|
||||
|
||||
- **A documented, repeatable dependency installation method**
|
||||
|
||||
- **Dependency management that can be version-controlled with the project.** Your dockerfile and the dependencies it captures evolve with the project!
|
||||
|
||||
- **A consistent environment between developers and CI servers.** Greatly reducing the "it works on my machine" problem
|
||||
|
||||
- **Development environment freedom.** Developers can run any parent distro or IDE they want as long as they can use docker
|
||||
|
||||
- **Easy to experiment with and revert dependency changes.** This is huge when looking at bumping toolchains
|
||||
|
||||
- **Project environment isolation.** One project doesn't mess things up for another project
|
||||
|
||||
Truly I can not sing enough praises to how docker has streamlined embedded development in my career. Today, many IDEs support developing from within a docker container. For instance VSCodes `devcontainer extension <https://code.visualstudio.com/docs/devcontainers/create-dev-container>`_. But since docker is an independent tool, your free to use it with any ide/text-editor you want.
|
||||
|
||||
Some common questions:
|
||||
|
||||
What about debugging?
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
If my toolchain (gcc, gdb) is all within a docker container, how do I flash or debug my target hardware? Easy, just mount your devices into the container. When you run a container in privileged mode (``--privileged``) you can bind mount pretty much anything. So I will bind-mount ``/dev`` which means all USB devices attached to my host are also accessible within the container:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker run --mount source=/dev,target=/dev,type=bind --privileged --mount<BIND MOUNT PROJECT DIR> <IMAGE_NAME>
|
||||
|
||||
As you would expect, your container will also need udev rules installed if you want to access devices as a non-root user. This configuration step can be easily captured in your dockerfile!
|
||||
|
||||
Sharing a development dockerfile with a CI server?
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Dependencies/requirements for development are not the same as CI/CD requirements. Typically development requirements are a super-set of CI/CD requirements. ie: developers need additional debug/helper tools in addition to the core toolchain dependencies. This is where `multi-stage docker builds <https://docs.docker.com/build/building/multi-stage/>`_ come in. They allow you to define separate stages in a dockerfile. Thus allowing you to avoid unnecessary installations on CI. For example:
|
||||
|
||||
.. code-block:: dockerfile
|
||||
|
||||
FROM ubuntu:latest AS project_builder_base
|
||||
|
||||
RUN apt update \
|
||||
&& apt install <toolchain>
|
||||
&& ...
|
||||
|
||||
FROM project_builder_base AS project_development
|
||||
|
||||
RUN apt update \
|
||||
&& apt install openocd <debugTool2> ...
|
||||
|
||||
Then in CI you only build the ``project_builder_base`` stage with:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker build --target project_builder_base -t projectTag:latest
|
||||
|
||||
Multi-stage builds like this require using dockers new build backend `"buildkit" <https://docs.docker.com/build/buildkit/>`_
|
||||
|
||||
Lesson 2: Abstract ASAP
|
||||
-----------------------
|
||||
|
||||
Making unit-testable code in embedded development can be challenging. There is a good chunk of code that can only be run on your target and would be complex to either mock out or build an automated test-rig for. I'm talking about the low-level drivers that interact directly with a MCU's hardware peripherals. For this reason, I think its critical to create a decoupling abstraction layer as-soon-as-possible in modern embedded development.
|
||||
|
||||
What does that look like? Something like this:
|
||||
|
||||
.. drawio-image:: ../assets/diagrams/hal_concept.drawio
|
||||
|
||||
Notice all SOC/MCU-specific code is encapsulated down in a single library which we abstract out with an interface layer immediately.
|
||||
|
||||
Benefits:
|
||||
|
||||
- **Easy to build a project for multiple targets** This benefit is huge. Imagine being able to build the same application for both linux (to unit test/simulate) and for your embedded target. You will catch bugs sooner and easier
|
||||
|
||||
- **All layers above the Hardware Abstraction Layer can be unit tested.** This includes device drivers (ie: sensor or actuator drivers) which in old-style development are often coupled to a specific MCU peripheral.
|
||||
|
||||
- **Component modularity.** At a system level, it is now very simple to move drivers and libraries from one MCU to another. As long as you have a Hardware Abstraction Layer (HAL) implementation for the new MCU you can mix and match things around. This can be critical in the prototyping stage of product development
|
||||
|
||||
I plan to dive into these concepts further in a future post.
|
||||
|
||||
Lesson 3: Separate the concepts of MCU and board
|
||||
------------------------------------------------
|
||||
|
||||
This concept is going to add yet-another piece to the Hardware Abstraction Layer puzzle. Separating your HAL implementation from board-specifics is an important step to having a fully modular embedded development project.
|
||||
|
||||
What are "board-specifics"? This is the term I use to describe the configuration information of a MCU that is specific to a real piece of hardware. For instance: MCU's will have a set of GPIOs. These GPIOs can be used as basic GPIOs or they can be mapped to SPI clocks, UART data lines, etc etc. The functional mapping of these pins is a detail that is specific to the overall hardware (aka 'board') you run your application on. It makes sense that information like this that is specific to a PCB board layout of a particular MCU should be encapsulated in its own 'library'. This library is separate from your HAL implementation
|
||||
|
||||
Here's an extension of the earlier diagram with these concepts added:
|
||||
|
||||
.. drawio-image:: ../assets/diagrams/hal_concept_with_board.drawio
|
||||
|
||||
Benefits:
|
||||
|
||||
- **Easy to track hardware revisions.** Structuring your project this way allows you to simultaneously support multiple revisions of a single board, which can be valuable if hardware availability is limited
|
||||
|
||||
- **Hardware Abstraction Layer doesnt change over board revisions.** With MCU peripheral drivers decoupled from board hardware - they dont need to change with each board change
|
||||
|
||||
- **Applications only instantiate drivers/resources they need.** Having a abstracted board data at the application-level allows you to only instantiate the drivers/peripherals that are required by the application. This will save memory and compute.
|
||||
|
||||
Conclusion
|
||||
----------
|
||||
|
||||
This was a very brief overview of some of my most valuable baremetal development lessons-learned. Following these lessons allows truly agile hardware and software development in the embedded space, which I dont think was possible with older development paradigms. I plan to expand on each of these topics in future posts.
|
|
@ -1,31 +1,39 @@
|
|||
# LS-1: Modular Synth
|
||||
.. _ls1synth:
|
||||
|
||||
LS-1: Modular Synth
|
||||
===================
|
||||
|
||||
.. post:: 04, September 2018
|
||||
:tags: audio, synthesizer, diy, breadboard
|
||||
:category: Projects
|
||||
:author: len0rd
|
||||
|
||||
The LS-1 is a modular oscillator and sequencer, and includes 2 LFO’s (low-frequency oscillators), one external oscillator, and the oscillator attached to the sequencer. The sequencer itself is made up of a counter which acts as a LFO/clock divider, and dual muxes to select the feedback resistance and led to display. The counter outputs and mux select inputs have ports on the front-panel allowing the user to mix and match LFO divisions with mux selects, thus creating custom sequences.
|
||||
|
||||
This build included a lot of firsts for me. This is the first time I’ve used Eagle to create a PCB/schematic (which should honestly be considered an atrocity given I am a Computer Engineering student), my first in creating a metal case using a water jet, and in general this is my first large-scale hobby project.
|
||||
|
||||
## Case
|
||||
Case
|
||||
----
|
||||
|
||||
### Design
|
||||
Design
|
||||
^^^^^^
|
||||
|
||||
Originally I was hoping to pack everything into a 1U 19″ package, using the case of an old network switch I had laying around, but I soon realized to include all the I/O I wanted I would have to increase the size, so I made the logical step up to 2U. Even with the increased size, its a pretty cozy fit for the front panel. I did some prototype configurations for different control sections using cardboard and the components I was planning on using, testing which layout I found to be the most natural.
|
||||
|
||||

|
||||
.. image:: ../assets/img/writeup/ls1synth/case-1-sm.jpg
|
||||
|
||||
Following this, I designed the case in Autodesk Inventor (since it’s free to students, I’m more of a Solidworks guy personally). It had been a few years since I had needed to touch Inventor, so it was a little rough, but I got the job done. Going in I knew I was planning on cutting this on my university’s water jet, so I built it all off a single sketch, taking into account how the faces would link together. I also decided to make a timelapse of the process, mostly for my own enjoyment, but feel free to watch and mock my terrible CAD skills:
|
||||
Following this, I designed the case in Autodesk Inventor (since its free to students, I’m more of a Solidworks guy personally). It had been a few years since I had needed to touch Inventor, so it was a little rough, but I got the job done. Going in I knew I was planning on cutting this on my university’s water jet, so I built it all off a single sketch, taking into account how the faces would link together. I also decided to make a timelapse of the process, mostly for my own enjoyment, but feel free to watch and mock my terrible CAD skills:
|
||||
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/iJbLcks4f_g?rel=0" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>
|
||||
.. youtube:: iJbLcks4f_g
|
||||
|
||||
This timelapse shows the majority of the design, but not all of it. Following this I consulted with my ME friend about how to build a proper metal case. He suggested cutting small circles where corners would fold to make sure excess material didn't get in the way. Before the water jet, I also made some slight modifications the the front panel layout - mostly related to spacing between components.
|
||||
|
||||
### Build
|
||||
Build
|
||||
^^^^^
|
||||
|
||||
With the panel designed, I simply needed to export the sketch face to a format the water jet would understand. I purchased a decently high gauge sheet of steel (maybe 12? I cannot remember) from my university and they cut it to roughly the dimensions I would need. Despite being a student, many of the resources I used for this part of the project are open to the public. If you're looking to do something similar, it never hurts to check with your local university's engineering/technology department to see what resources they have available.
|
||||
|
||||

|
||||
.. image:: ../assets/img/writeup/ls1synth/case-2-sm.jpg
|
||||
|
||||
With the metal sheet cut, I was ready to cut out the case with the water jet. My university charges for how much time you use a product, and since the case is not too large, this only cost me around $20. Putting the case portion of the project at ~$30 total! This was my first time using a water jet, so it was a very big deal for me. Computer Engineering is fun and all, but you don't get enough chances to play with big toys like other disciplines do.
|
||||
|
||||
### TODO: video embed
|
||||

|
||||
|
58
posts/myWebsite.rst
Normal file
58
posts/myWebsite.rst
Normal file
|
@ -0,0 +1,58 @@
|
|||
.. myWebsite:
|
||||
|
||||
My Website
|
||||
==========
|
||||
|
||||
.. post:: 31, July 2018
|
||||
:tags: coding, diy, old
|
||||
:category: Projects
|
||||
:author: len0rd
|
||||
|
||||
Starting out with this website, I had essentially no knowledge of modern web technologies. I knew that I wanted something modern but also easy to maintain that I could use well into the future.
|
||||
|
||||
The end result is the site you see here. By no means perfect or beautiful, but functional and a place where I can store guides mainly for my benefit. But maybe for your benefit too? I certainly dont know who's reading this ¯\\\_(ツ)_/¯
|
||||
|
||||
As I'm writing this page after the fact (about a year since originally making this site), I'll likely glaze over a lot of the details.
|
||||
|
||||
Technologies Used
|
||||
-----------------
|
||||
|
||||
- `Node <https://nodejs.org/>`_ for quick 'n easy webserver creation
|
||||
|
||||
- `NPM <https://www.npmjs.com>`_ for a bunch of support packages
|
||||
|
||||
- `ExpressJS <https://expressjs.com>`_ with `ejs <https://ejs.co/>`_ for amped up static pages. Express is great for beginners that want a simple framework for a static website with no code duplication
|
||||
|
||||
- `Bootstrap <https://getbootstrap.com>`_ to make it all pretty
|
||||
|
||||
- Other stuff
|
||||
|
||||
Express
|
||||
-------
|
||||
|
||||
Originally I started this site as a pure html/bootstrap affair. This worked for all of 2 days until I got sick of copying and pasting code all over the place. While I had no desire to maintain duplicate copies of code, I was even less interested in using some massive overkill framework (as an embedded dev, I have a need for speed). Low and behold: ExpressJS! The perfect minimal framework solution for my problem. Express has a concept of 'pages' and 'partials'. A page defines the overall structure of a static webpage (say my home page). Partials define chunks/components of that page that are shared in other locations. So for example, all the html for my navigation/ header bar has its own partial, as does the footer. Then in a page, to use this content you can simply add a ``<% include`` as if you were writing a C program! Express was speaking my language.
|
||||
|
||||
Static Project Pages [old]
|
||||
--------------------------
|
||||
|
||||
.. note::
|
||||
|
||||
I've replaced this implementation with Sphinx docs
|
||||
|
||||
The bulk of the effort for me was sunk into generating the project writeup pages (like the page you're reading this off of right now!). I wanted them to be simple static text, images and video. But I didn't want the complexity of using a whole framework like wordpress, and I definitely wasn't into the idea of writting everything in html. I wanted my writeups to be in a portable format I could easily migrate or use in other places in the future.
|
||||
|
||||
Given these requirements I thought it best to write about all of my projects in markdown. I've used markdown for years and like its readability and easy syntax. To convert my markdown to HTML, I grabbed `showdown <https://github.com/showdownjs/showdown>`_ . Showdown does it's job well and has some hooks (called 'extensions') that made it easier for me to get the formatting jusssst right. At present the only extension I've created helps make the title/H1 of each writeup nice and big (ie: look at those big 'My Website' letters up top). All the showdown generator stuff lives in ``prestart.js`` which is run before the server is started so the markdown is generated once and can then be served statically for all time.
|
||||
|
||||
I saved showdown's resulting files as ExpressJS partials. These partials are linked to a template page which adds the header, footer and table of contents you see here. Then, any requests that contain ``/projects`` actually load the ``project_template`` page with the requested project-name partial. Express makes this all surprisingly simple (I say after struggling with it for hours):
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
if (pathname.includes('projects') && page !== 'index') {
|
||||
// projects has a custom template that is used for all projects
|
||||
// so we need to change the pathname that the renderer is using
|
||||
// that template:
|
||||
pathname = pathname.substr(0, pathname.lastIndexOf(page));
|
||||
pathname += 'project_template'
|
||||
// provide the pagename for project_template to use for main content
|
||||
page = 'partials/md/' + page;
|
||||
}
|
300
project_writeups/palletDesk.md → posts/palletDesk.rst
Executable file → Normal file
300
project_writeups/palletDesk.md → posts/palletDesk.rst
Executable file → Normal file
|
@ -1,141 +1,159 @@
|
|||
# Pallet Desk
|
||||
|
||||
As I finished up my sophomore year at university, I realized that I needed my own desk. At that point I had lived in three different apartments. All 3 of them had desks provided, and all of those desks were mediocre at best, downright broken at worst. What's worse is at the time I worked from home, which meant I *needed* a reliable desk. With the summer about to hit, and plans to work full-time from home, I knew I needed something fast. Here's the end result:
|
||||
|
||||

|
||||
|
||||
Before we get into this build, I will note that I'm writing this guide > 2 years later. This means I have forgotten some details of the build process, but have also have the gift of hindsight thanks to this desk being my main piece of furniture for the last 2.5 years. Like seriously, I eat, study and work at this thing everyday.
|
||||
|
||||
## Plans
|
||||
|
||||
### Requirements
|
||||
|
||||
Before I started building I had to think through what I wanted. I came up with some basic ideas:
|
||||
|
||||
- I didn't need storage. Drawers would be difficult and expensive to integrate, and I don't think they look that good anyways.
|
||||
|
||||
- It needed to be cheap. I had a job as a software engineer at the time, but it did not pay much at all. The desk couldn't eat too much of my budget.
|
||||
|
||||
- Durability. I wanted a desk that could last me a long time. Like 5+ years. I'm unsure if I'll actually use it for that long, but I'm confident it could last.
|
||||
|
||||
- Portability. During the planning stages I had already moved twice and was about to move a third time. I needed a desk that wouldn't be a hassle to move. I designed the desk so that it could be easily torn down by one person, and fit inside my Toyota Camry. Ironically, I haven't moved since I finished building the desk, but I have still had occasion to tear it down and move it, which is surprisingly convenient.
|
||||
|
||||
- Sitting/Standing. I'm a decently tall person. Measuring in at around ~6'5" (2m), I'm not a huge fan of short things. I like sitting in high chairs where my legs can dangle. I wanted something similar in a desk that would allow me to stand or sit depending on what I was feeling.
|
||||
|
||||
### Design
|
||||
|
||||
With the requirements thought out, I started planning how I was going to build everything. Between planning/designing I probably spent a month or two just *thinking* about how I would build the desk. I spent a lot of time online looking at what other people have done. Seeing other peoples designs is the one reason I currently have a Pinterest account.
|
||||
|
||||
There were some really cool designs online, but I needed something simple. This was my first woodworking project, and I knew I *would* (hehe) be making a lot of mistakes. All of these thoughts culminated into a single whiteboard sketch:
|
||||
|
||||

|
||||
|
||||
Yep. That's it. It was all a lot clearer in my head. Essentially the red 'rectangles' are 2x4 cross beams that would support the desktop and create a structure to build onto. The measurements were mainly based on what would fit in my Camry. Note: this only fits in the car when I put the back seats down so the desk can go through the trunk and into the back of the car. pics further down. I also measured a few desks nearby to see what an appropriate depth for a monitor + keyboard would be.
|
||||
|
||||
## Build
|
||||
|
||||
### Pallet Acquisition
|
||||
|
||||
There are a lot of guides on how to get pallets online. If you're in the US, check the free section of Craigslist or your local classifieds. You can also just walk into local places and ask if they have any pallets that you can use. Alternatively, just drive behind businesses and see if they have pallets stacked near the dumpster.
|
||||
|
||||
If you have a smaller car, be prepared to potentially tear down the pallet on site. Pallets were a bit larger than I expected and some of them would not fit in my car! Because of this, I had to pass up on some of the nicer pallets out there. In all, I think this project took 2-3 pallets. It could be done with less, but my teardown methods were less than ideal.
|
||||
|
||||
The problem with pallet wood is that it's a bit of a mixed bag. Sometimes you get a really new pallet that is made out of decent wood, sometimes you get a pallet that's a few years old and on the verge of fallng apart. Sourcing newer pallets should be your priority. I didn't bother doing so and paid the price through many stages of the build.
|
||||
|
||||
### Pallet Teardown
|
||||
|
||||
The best way to get usable wood from a pallet is to use a jig saw. Period. If you don't have one, buy one online or borrow a friends. It's worth it especially if you want to do a lot of projects like this. With a jig saw, it's very easy to align the blade between the pallet planks and cut through the nails holding them in place. If you have room to spare, you could also use a table saw and just cut off the pallet planks before they're nailed into the base.
|
||||
|
||||
If you're a poor college student as I was, the other option is a crowbar, hammer and *a lot* of patience. With this method you need to jimmy the crow bar in between the pallet plank and base and slowely wedge out the nails holding the plank in place. Making sure you dont strain one end of the plank too much and crack it. This is a delicate process that takes a long time and often ends in breaking the pallet planks much to the despair of the builder.
|
||||
|
||||
Because of this expense of learning, Perhaps only 50% of the 3 pallets I had turned out usable. Unfortunately I don't have any images of this stage of the process likely due to the rage-inducing nature of it all. I worked on this portion of the project at night after getting home from school across a couple of days.
|
||||
|
||||
### Desktop
|
||||
|
||||
Finally, with all the pallets torn down, I had wood I could start working with. I started out by building the basic frame of the desktop, cutting pallet base 2x4's to size and joining them at 45deg on the corners. This was stupid. Getting perfect 45deg angles to come together and hold well is not easy, especially if you don't know what you're doing. It looks okay in the end product, but if I were todo it again, I would have done simpler, more reliable corners.
|
||||
|
||||

|
||||
|
||||
As I was starting, a friendly neighbor came over to see what I was working on and lent me these corner clamps. Regardless of the join you're doing, I think these could be pretty helpful
|
||||
|
||||

|
||||
|
||||
Fortunately, the crappy corners are mitigated by the cross beams that run the width of the desktop. For these I decided to go with new 2x4's. I realize this is against the spirit of the whole project, but at this point I was *not* about to teardown another pallet for 2x4s. Additionally, the rest of the entire desktop would be secured into these, so they needed to be non-horrible.
|
||||
|
||||
Basically every piece of this project is secured by a combination of wood glue and screws.
|
||||
|
||||
Also, for this entire project, wood was cut to size using my friends chop saw. Not sure what I would've done without it. (I mean everything *could* be done with a hand saw... but).
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
With the basic frame in place, it was time to start building the visible part of the desktop. I started by bordering the exterior with pallet plants to make something like a picture frame for the middle. The chop saw came in handy, helping make angles that match.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
From this angle it looks like the border is slightly, missaligned, but I dont ever notice this and it seems to be fine. Notice that there is a ~1" lip off the edge of the frame. The main reason for this was to keep some of the frame exposed so the middle of the destop could be secured to it. But also because it looks cool.
|
||||
|
||||
With the border completed, I could move on to filling in the middle of the desktop. Before doing this, glued and stapled down some crate paper, in the hopes that it would help retain any epoxy that slipped through once it was time for that.
|
||||
|
||||
Once the crate paper was secured I started cutting and fitting pallet planks. I would cut one at a time to test the fit and make sure it was tight. Then I would line the edges and bottom with glue, push it into its general location and use a mallet to nudge it as far to the right as possible. Below is the partially completed top (note the exposed crate paper on the left side):
|
||||
|
||||

|
||||
|
||||
Once a single board was pushed to the far right side, It would be secured on the 3 2x4 frame pieces with screws and glue. Once all the pieces were secured I went back and started sanding. The desktop was made from a combination of three different pallets, so the wood was vastly different in quality and thickness.
|
||||
|
||||
My **biggest** regret in this project was not spending enough time sanding the top. Especially the middle of the desk, I should've spend multiple hours sanding it, with multiple passes at different levels of courseness to make sure it was smooth and flat. A planar and/or a really good electric sander should be a must for this portion of the project. A single board in the middle of my destop is slightly thicker than the others, which means my keyboard can not lie flat without wobbling.
|
||||
|
||||
If you plan on covering the top in epoxy as I did, this is also a good time to make *sure* any and all holes are filled with wood filler (I made wood filler by mixing wood glue with some sawdust). You dont want anywhere for the epoxy to escape so make sure all the old nail holes on the pallet boards are properly filled.
|
||||
|
||||
### Legs
|
||||
|
||||
With the desktop done it was time to move on to the legs. There are lots of different options on what to do here. I decided to go with some basic 4x4's. I measured a few bar counters/stools to decide what would be a good height for the legs. I finally decided on ~4ft. This allows the collapsed legs to fit within the desktops footprint for easy portability.
|
||||
|
||||
As always transport with the Camry was enjoyable:
|
||||
|
||||

|
||||
|
||||
Nothing magic here, just measure and cut. There are definitely better ways to secure the cross beams to the legs, but the best I came up with was using metal plates. The long cross bar is removable, meaning the legs can be broken down into 3 pieces: the left 2 legs, the right 2 legs, and the long cross bar.
|
||||
|
||||
Securing the leg assembley to the desktop is done with bolts. Two massive bolts per leg are inserted in perpendicular directions through the desktop and leg. The 3/8" bolts are then secured with washer, split-locking washer and nut.
|
||||
|
||||
Once again, my impatience got the better of me, and I failed to sand the legs as much as I should have. Don't skimp on sanding!
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### Stain and Epoxy
|
||||
|
||||
With the desk fully constructed, it was time to add the finishing touches to make it a usable piece of furniture. First up was staining. I went with a homemade solution for the stain. I let a gallon of vinegar, steel wool and hydrogen peroxide... ferment(?)... soak for a week. There are a lot of videos online about this type of homemade stain. The desired outcome of this stain is a rustic/aged look. The hydrogen peroxide helps add a red tint to the stain.
|
||||
|
||||
The results for me were a bit mixed. Some board took the stain beautifully; producing a slightly dark stain with a perfect hint of red. Others ate it right up and produced an incredibly dark wood with 0 red. Especially the legs, which the stain essentially just made dark brown. In hindsight, I should have tested the stain more on some scrap wood before applying it.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
That 5 gallon bucket next to the desk? That's the stain... I may have made a little too much.
|
||||
|
||||
With everything stained I was ready to finish up by applying epoxy to the desktop. The idea here is that epoxy is fairly self-leveling, which will help reduce any error in my build. With using old beatup pallet wood, I also thought it best to use epoxy to avoid any latent splinters. The epoxy provides a smooth glass-like surface which has looked great since it was applied.
|
||||
|
||||
I got a gallon kit of the stuff off Amazon. It came with instructions, and I made sure to watch *many* youtube videos about epoxy application before attempting. As previously stated, I wish I had spent more time sanding the top, and making sure all the holes many, many nail holes were filled in better. Despite these errors, the epoxy took fairly well. I started with a thin coat to try and help 'seal' the top. This likely helped with some of the smaller holes, but there was nothing to be done for the larger ones. After that, I applied the whole thing. Pouring right in the middle and letting it spread itself out. The end result is a nice glossy surface that really brings out the subtle stain in some of the boards.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
And, in case you doubted, here's the desk broken down and in the trunk of my Camry:
|
||||
|
||||

|
||||
|
||||
## tl;dr
|
||||
|
||||
The desk works great, and overall the design is very forgiving to rookie mistakes. Make sure you have the proper tools (Jigsaw) for pallet dissassembly. Dont skimp on sanding the desktop, and if you're going to use epoxy, make *absolutely* sure that you have adequately filled in all the nail holes in the pallet wood before application.
|
||||
|
||||

|
||||
.. palletDesk:
|
||||
|
||||
Pallet Desk
|
||||
===========
|
||||
|
||||
.. post:: 02, September 2018
|
||||
:tags: furniture, diy, amateur, pallets
|
||||
:category: Projects
|
||||
:author: len0rd
|
||||
|
||||
As I finished up my sophomore year at university, I realized that I needed my own desk. At that point I had lived in three different apartments. All 3 of them had desks provided, and all of those desks were mediocre at best, downright broken at worst. What's worse is at the time I worked from home, which meant I *needed* a reliable desk. With the summer about to hit, and plans to work full-time from home, I knew I needed something fast. Here's the end result:
|
||||
|
||||
.. image:: ../assets/img/writeup/palletDesk/finished-1-sm.jpg
|
||||
|
||||
Before we get into this build, I will note that I'm writing this guide > 2 years later. This means I have forgotten some details of the build process, but have also have the gift of hindsight thanks to this desk being my main piece of furniture for the last 2.5 years. Like seriously, I eat, study and work at this thing everyday.
|
||||
|
||||
Plans
|
||||
-----
|
||||
|
||||
Requirements
|
||||
^^^^^^^^^^^^
|
||||
|
||||
Before I started building I had to think through what I wanted. I came up with some basic ideas:
|
||||
|
||||
- I didn't need storage. Drawers would be difficult and expensive to integrate, and I don't think they look that good anyways.
|
||||
|
||||
- It needed to be cheap. I had a job as a software engineer at the time, but it did not pay much at all. The desk couldn't eat too much of my budget.
|
||||
|
||||
- Durability. I wanted a desk that could last me a long time. Like 5+ years. I'm unsure if I'll actually use it for that long, but I'm confident it could last.
|
||||
|
||||
- Portability. During the planning stages I had already moved twice and was about to move a third time. I needed a desk that wouldn't be a hassle to move. I designed the desk so that it could be easily torn down by one person, and fit inside my Toyota Camry. Ironically, I haven't moved since I finished building the desk, but I have still had occasion to tear it down and move it, which is surprisingly convenient.
|
||||
|
||||
- Sitting/Standing. I'm a decently tall person. Measuring in at around ~6'5" (2m), I'm not a huge fan of short things. I like sitting in high chairs where my legs can dangle. I wanted something similar in a desk that would allow me to stand or sit depending on what I was feeling.
|
||||
|
||||
Design
|
||||
^^^^^^
|
||||
|
||||
With the requirements thought out, I started planning how I was going to build everything. Between planning/designing I probably spent a month or two just *thinking* about how I would build the desk. I spent a lot of time online looking at what other people have done. Seeing other peoples designs is the one reason I currently have a Pinterest account.
|
||||
|
||||
There were some really cool designs online, but I needed something simple. This was my first woodworking project, and I knew I *would* (hehe) be making a lot of mistakes. All of these thoughts culminated into a single whiteboard sketch:
|
||||
|
||||
.. image:: ../assets/img/writeup/palletDesk/plans-1-sm.jpg
|
||||
|
||||
Yep. That's it. It was all a lot clearer in my head. Essentially the red 'rectangles' are 2x4 cross beams that would support the desktop and create a structure to build onto. The measurements were mainly based on what would fit in my Camry. Note: this only fits in the car when I put the back seats down so the desk can go through the trunk and into the back of the car. pics further down. I also measured a few desks nearby to see what an appropriate depth for a monitor + keyboard would be.
|
||||
|
||||
Build
|
||||
-----
|
||||
|
||||
Pallet Acquisition
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
There are a lot of guides on how to get pallets online. If you're in the US, check the free section of Craigslist or your local classifieds. You can also just walk into local places and ask if they have any pallets that you can use. Alternatively, just drive behind businesses and see if they have pallets stacked near the dumpster.
|
||||
|
||||
If you have a smaller car, be prepared to potentially tear down the pallet on site. Pallets were a bit larger than I expected and some of them would not fit in my car! Because of this, I had to pass up on some of the nicer pallets out there. In all, I think this project took 2-3 pallets. It could be done with less, but my teardown methods were less than ideal.
|
||||
|
||||
The problem with pallet wood is that it's a bit of a mixed bag. Sometimes you get a really new pallet that is made out of decent wood, sometimes you get a pallet that's a few years old and on the verge of falling apart. Sourcing newer pallets should be your priority. I didn't bother doing so and paid the price through many stages of the build.
|
||||
|
||||
Pallet Teardown
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
The best way to get usable wood from a pallet is to use a jig saw. Period. If you don't have one, buy one online or borrow a friends. It's worth it especially if you want to do a lot of projects like this. With a jig saw, it's very easy to align the blade between the pallet planks and cut through the nails holding them in place. If you have room to spare, you could also use a table saw and just cut off the pallet planks before they're nailed into the base.
|
||||
|
||||
If you're a poor college student as I was, the other option is a crowbar, hammer and *a lot* of patience. With this method you need to jimmy the crow bar in between the pallet plank and base and slowely wedge out the nails holding the plank in place. Making sure you dont strain one end of the plank too much and crack it. This is a delicate process that takes a long time and often ends in breaking the pallet planks much to the despair of the builder.
|
||||
|
||||
Because of this expense of learning, Perhaps only 50% of the 3 pallets I had turned out usable. Unfortunately I don't have any images of this stage of the process likely due to the rage-inducing nature of it all. I worked on this portion of the project at night after getting home from school across a couple of days.
|
||||
|
||||
Desktop
|
||||
^^^^^^^
|
||||
|
||||
Finally, with all the pallets torn down, I had wood I could start working with. I started out by building the basic frame of the desktop, cutting pallet base 2x4's to size and joining them at 45deg on the corners. This was stupid. Getting perfect 45deg angles to come together and hold well is not easy, especially if you don't know what you're doing. It looks okay in the end product, but if I were todo it again, I would have done simpler, more reliable corners.
|
||||
|
||||
.. image:: ../assets/img/writeup/palletDesk/build-1-sm.jpg
|
||||
|
||||
As I was starting, a friendly neighbor came over to see what I was working on and lent me these corner clamps. Regardless of the join you're doing, I think these could be pretty helpful
|
||||
|
||||
.. image:: ../assets/img/writeup/palletDesk/build-2-sm.jpg
|
||||
|
||||
Fortunately, the crappy corners are mitigated by the cross beams that run the width of the desktop. For these I decided to go with new 2x4's. I realize this is against the spirit of the whole project, but at this point I was *not* about to teardown another pallet for 2x4s. Additionally, the rest of the entire desktop would be secured into these, so they needed to be non-horrible.
|
||||
|
||||
Basically every piece of this project is secured by a combination of wood glue and screws.
|
||||
|
||||
Also, for this entire project, wood was cut to size using my friends chop saw. Not sure what I would've done without it. (I mean everything *could* be done with a hand saw... but).
|
||||
|
||||
.. image:: ../assets/img/writeup/palletDesk/build-3-sm.jpg
|
||||
|
||||
.. image:: ../assets/img/writeup/palletDesk/build-4-sm.jpg
|
||||
|
||||
With the basic frame in place, it was time to start building the visible part of the desktop. I started by bordering the exterior with pallet plants to make something like a picture frame for the middle. The chop saw came in handy, helping make angles that match.
|
||||
|
||||
.. image:: ../assets/img/writeup/palletDesk/build-5-sm.jpg
|
||||
|
||||
.. image:: ../assets/img/writeup/palletDesk/build-6-sm.jpg
|
||||
|
||||
From this angle it looks like the border is slightly, missaligned, but I dont ever notice this and it seems to be fine. Notice that there is a ~1" lip off the edge of the frame. The main reason for this was to keep some of the frame exposed so the middle of the destop could be secured to it. But also because it looks cool.
|
||||
|
||||
With the border completed, I could move on to filling in the middle of the desktop. Before doing this, glued and stapled down some crate paper, in the hopes that it would help retain any epoxy that slipped through once it was time for that.
|
||||
|
||||
Once the crate paper was secured I started cutting and fitting pallet planks. I would cut one at a time to test the fit and make sure it was tight. Then I would line the edges and bottom with glue, push it into its general location and use a mallet to nudge it as far to the right as possible. Below is the partially completed top (note the exposed crate paper on the left side):
|
||||
|
||||
.. image:: ../assets/img/writeup/palletDesk/build-7-sm.jpg
|
||||
|
||||
Once a single board was pushed to the far right side, It would be secured on the 3 2x4 frame pieces with screws and glue. Once all the pieces were secured I went back and started sanding. The desktop was made from a combination of three different pallets, so the wood was vastly different in quality and thickness.
|
||||
|
||||
My **biggest** regret in this project was not spending enough time sanding the top. Especially the middle of the desk, I should've spend multiple hours sanding it, with multiple passes at different levels of courseness to make sure it was smooth and flat. A planar and/or a really good electric sander should be a must for this portion of the project. A single board in the middle of my destop is slightly thicker than the others, which means my keyboard can not lie flat without wobbling.
|
||||
|
||||
If you plan on covering the top in epoxy as I did, this is also a good time to make *sure* any and all holes are filled with wood filler (I made wood filler by mixing wood glue with some sawdust). You dont want anywhere for the epoxy to escape so make sure all the old nail holes on the pallet boards are properly filled.
|
||||
|
||||
Legs
|
||||
^^^^
|
||||
|
||||
With the desktop done it was time to move on to the legs. There are lots of different options on what to do here. I decided to go with some basic 4x4's. I measured a few bar counters/stools to decide what would be a good height for the legs. I finally decided on ~4ft. This allows the collapsed legs to fit within the desktops footprint for easy portability.
|
||||
|
||||
As always transport with the Camry was enjoyable:
|
||||
|
||||
.. image:: ../assets/img/writeup/palletDesk/build-8-sm.png
|
||||
|
||||
Nothing magic here, just measure and cut. There are definitely better ways to secure the cross beams to the legs, but the best I came up with was using metal plates. The long cross bar is removable, meaning the legs can be broken down into 3 pieces: the left 2 legs, the right 2 legs, and the long cross bar.
|
||||
|
||||
Securing the leg assembly to the desktop is done with bolts. Two massive bolts per leg are inserted in perpendicular directions through the desktop and leg. The 3/8" bolts are then secured with washer, split-locking washer and nut.
|
||||
|
||||
Once again, my impatience got the better of me, and I failed to sand the legs as much as I should have. Don't skimp on sanding!
|
||||
|
||||
.. image:: ../assets/img/writeup/palletDesk/build-9-sm.jpg
|
||||
|
||||
.. image:: ../assets/img/writeup/palletDesk/build-10-sm.jpg
|
||||
|
||||
Stain and Epoxy
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
With the desk fully constructed, it was time to add the finishing touches to make it a usable piece of furniture. First up was staining. I went with a homemade solution for the stain. I let a gallon of vinegar, steel wool and hydrogen peroxide... ferment(?)... soak for a week. There are a lot of videos online about this type of homemade stain. The desired outcome of this stain is a rustic/aged look. The hydrogen peroxide helps add a red tint to the stain.
|
||||
|
||||
The results for me were a bit mixed. Some board took the stain beautifully; producing a slightly dark stain with a perfect hint of red. Others ate it right up and produced an incredibly dark wood with 0 red. Especially the legs, which the stain essentially just made dark brown. In hindsight, I should have tested the stain more on some scrap wood before applying it.
|
||||
|
||||
.. image:: ../assets/img/writeup/palletDesk/build-11-sm.jpg
|
||||
|
||||
.. image:: ../assets/img/writeup/palletDesk/build-12-sm.jpg
|
||||
|
||||
That 5 gallon bucket next to the desk? That's the stain... I may have made a little too much.
|
||||
|
||||
With everything stained I was ready to finish up by applying epoxy to the desktop. The idea here is that epoxy is fairly self-leveling, which will help reduce any error in my build. With using old beatup pallet wood, I also thought it best to use epoxy to avoid any latent splinters. The epoxy provides a smooth glass-like surface which has looked great since it was applied.
|
||||
|
||||
I got a gallon kit of the stuff off Amazon. It came with instructions, and I made sure to watch *many* youtube videos about epoxy application before attempting. As previously stated, I wish I had spent more time sanding the top, and making sure all the holes many, many nail holes were filled in better. Despite these errors, the epoxy took fairly well. I started with a thin coat to try and help 'seal' the top. This likely helped with some of the smaller holes, but there was nothing to be done for the larger ones. After that, I applied the whole thing. Pouring right in the middle and letting it spread itself out. The end result is a nice glossy surface that really brings out the subtle stain in some of the boards.
|
||||
|
||||
.. image:: ../assets/img/writeup/palletDesk/build-13-sm.jpg
|
||||
|
||||
.. image:: ../assets/img/writeup/palletDesk/build-14-sm.jpg
|
||||
|
||||
.. image:: ../assets/img/writeup/palletDesk/build-15-sm.jpg
|
||||
|
||||
And, in case you doubted, here's the desk broken down and in the trunk of my Camry:
|
||||
|
||||
.. image:: ../assets/img/writeup/palletDesk/incar-1-sm.jpg
|
||||
|
||||
tl;dr
|
||||
-----
|
||||
|
||||
The desk works great, and overall the design is very forgiving to rookie mistakes. Make sure you have the proper tools (Jigsaw) for pallet disassembly. Dont skimp on sanding the desktop, and if you're going to use epoxy, make *absolutely* sure that you have adequately filled in all the nail holes in the pallet wood before application.
|
||||
|
||||
.. image:: ../assets/img/writeup/palletDesk/finished-1-lg.jpg
|
262
prestart.js
262
prestart.js
|
@ -1,262 +0,0 @@
|
|||
// convert our markdown documentation files
|
||||
// to 'static' html/ejs partials:
|
||||
// while this is a bit inconvenient (you need to restart
|
||||
// the server everytime you want to see
|
||||
// md changes), it is more efficient in
|
||||
// that we aren't converting MD -> ejs
|
||||
// on EVERY request
|
||||
const showdown = require('showdown'),
|
||||
showdownHighlight = require("showdown-highlight"),
|
||||
fs = require('fs'),
|
||||
mkdirp = require('mkdirp'),
|
||||
projectInputDir = './project_writeups/',
|
||||
projectOutputDir = './views/partials/md/projects/',
|
||||
recipeInputDir = './recipes/',
|
||||
recipeOutputDir = './views/partials/md/recipes/',
|
||||
recipeListGeneratedOutputDir = './views/partials/generated/',
|
||||
projectClassMap = {
|
||||
h1: 'display-1' //tag type : class to add to all tags of that type (class="display-1" added to all <h1>)
|
||||
};
|
||||
const { assert } = require('console');
|
||||
|
||||
|
||||
function addClassToTag(text, classMap) {
|
||||
var modifiedText = text;
|
||||
Object.keys(classMap).forEach(function (key) {
|
||||
var regex = new RegExp(`<(${key})(.*?)>`, 'g');
|
||||
matcher = regex.exec(modifiedText);
|
||||
|
||||
// only proceed if we found a match, and the class we add isn't already on the tag somehow
|
||||
while (matcher != null && !matcher[2].includes(classMap[key])) {
|
||||
// add the class content WHILE preserving any other properties already in the tag!
|
||||
console.log("adding class content in: " + matcher[0]);
|
||||
|
||||
var restOfTag = matcher[2];
|
||||
modifiedText = modifiedText.replace(matcher[0], `<${key} class="${classMap[key]}" ${restOfTag}>`);
|
||||
|
||||
matcher = regex.exec(modifiedText);
|
||||
}
|
||||
});
|
||||
return modifiedText;
|
||||
}
|
||||
|
||||
// handles adding classes to specific
|
||||
// tag types automatically in project writeups
|
||||
const projectsAddHeaderClass = {
|
||||
type: 'output', // when it's triggered -> output is at the very end when text is html
|
||||
filter: text => { return addClassToTag(text, projectClassMap); }
|
||||
};
|
||||
|
||||
// create Showdown converters
|
||||
const projectsConverter = new showdown.Converter({
|
||||
extensions: [projectsAddHeaderClass, showdownHighlight],
|
||||
tables: true
|
||||
});
|
||||
|
||||
function convertMarkdownInDirWithShowdown(inputDir, outputDir, converter) {
|
||||
// make the directory for the html output if necessary
|
||||
mkdirp.sync(outputDir);
|
||||
fs.readdir(inputDir, (err, files) => {
|
||||
files.forEach(file => {
|
||||
if (file.endsWith('.md')) {
|
||||
let fileNameNoExtension = file.slice(0, -3);
|
||||
console.log('converting: ' + fileNameNoExtension);
|
||||
fs.readFile(inputDir + file, 'utf8', (err, data) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
} else {
|
||||
let html = converter.makeHtml(data); // where the magic happens
|
||||
fs.writeFile(outputDir + fileNameNoExtension + '.ejs', html, 'utf8', (err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function convertRecipeMarkdown(inputDir, outputDir) {
|
||||
var md = require('markdown-it')()
|
||||
.use(require('markdown-it-hashtag'));
|
||||
|
||||
md.renderer.rules.hashtag_open = function (tokens, idx) {
|
||||
var tagName = tokens[idx].content.toLowerCase();
|
||||
return '<a href="/recipe_navigator?tag=' + tagName + '"><span class="badge bg-secondary">';
|
||||
}
|
||||
|
||||
md.renderer.rules.hashtag_close = function () { return '</span></a>'; }
|
||||
|
||||
// This is a hardcoded markdown header section number to html file name
|
||||
//
|
||||
// Example.md:
|
||||
// """
|
||||
// ... maybe some other header info here -| - exported as filename-title.ejs
|
||||
// # Delicious Recipe Name -|
|
||||
// Catch phrase or yield -|
|
||||
// | - exported as filename-subtitle
|
||||
// image of the food |
|
||||
// -|
|
||||
// ## Ingredients -|
|
||||
// ... ingredients table, etc | - exported as filename-ingredients.ejs
|
||||
// -|
|
||||
// ## Instructions
|
||||
// """
|
||||
//
|
||||
// NOTE: these titles are HARDCODED in recipe_template.ejs!
|
||||
const mdSectionHtmlTitles = [
|
||||
'title',
|
||||
// 'subtitle',
|
||||
'ingredients',
|
||||
'instructions',
|
||||
]
|
||||
|
||||
mkdirp.sync(outputDir);
|
||||
fs.readdir(inputDir, (err, files) => {
|
||||
files.forEach(file => {
|
||||
if (!file.endsWith('.md')) {
|
||||
return;
|
||||
}
|
||||
let fileNameNoExtension = file.slice(0, -3);
|
||||
console.log('converting: ' + fileNameNoExtension);
|
||||
fs.readFile(inputDir + file, 'utf8', (err, data) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
|
||||
var ingredientTableRegex = new RegExp(`^(\\|?.*?)\\|(.*?)(\\|.*?\\|.*?)\n`, `gm`);
|
||||
var ingredientDashCheck = new RegExp("^\-+$");
|
||||
ingredientTableMatcher = ingredientTableRegex.exec(data);
|
||||
while (ingredientTableMatcher != null) {
|
||||
meas = ingredientTableMatcher[1];
|
||||
let unit = ingredientTableMatcher[2].toLowerCase().trim();
|
||||
if (unit != "unit" && unit && !ingredientDashCheck.test(unit)) {
|
||||
meas += unit + " ";
|
||||
}
|
||||
data = data.replace(ingredientTableMatcher[0], `${meas}${ingredientTableMatcher[3]}\n`);
|
||||
ingredientTableMatcher = ingredientTableRegex.exec(data);
|
||||
}
|
||||
ingredientTableMatcher = ingredientTableRegex.exec(data);
|
||||
while (ingredientTableMatcher != null) {
|
||||
meas = ingredientTableMatcher[1];
|
||||
let unit = ingredientTableMatcher[2].toLowerCase().trim();
|
||||
if (unit != "unit" && unit && !ingredientDashCheck.test(unit)) {
|
||||
meas += unit + " ";
|
||||
}
|
||||
data = data.replace(ingredientTableMatcher[0], `${meas}${ingredientTableMatcher[3]}\n`);
|
||||
ingredientTableMatcher = ingredientTableRegex.exec(data);
|
||||
}
|
||||
|
||||
let tokens = md.parse(data)
|
||||
|
||||
let sections = []
|
||||
sections.push([]); // start off the array and put everything before and including the first header in title
|
||||
let numSections = 0;
|
||||
for (const token of tokens) {
|
||||
if (token.type === 'heading_open') {
|
||||
if (numSections == 0) {
|
||||
numSections++;
|
||||
}
|
||||
else if (numSections < mdSectionHtmlTitles.length) {
|
||||
numSections++;
|
||||
sections.push([]);
|
||||
}
|
||||
}
|
||||
sections[sections.length - 1].push(token)
|
||||
}
|
||||
|
||||
assert(sections.length <= mdSectionHtmlTitles.length);
|
||||
|
||||
// hardcode bootstrap class attribute to add to <table> tag in ingredients
|
||||
for (let ii = 0; ii < sections[1].length; ii++) {
|
||||
if (sections[1][ii].type == 'table_open') {
|
||||
sections[1][ii].attrs = [["class", "table table-striped table-sm table-hover"]];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (let ii = 0; ii < sections.length; ii++) {
|
||||
let html = md.renderer.render(sections[ii], md.options);
|
||||
// hardcode making images in the title section larger
|
||||
if (ii == 0) {
|
||||
var regex = new RegExp(`<img (.*?)>`, `g`);
|
||||
matcher = regex.exec(html);
|
||||
while (matcher != null && !matcher[1].includes("w-100")) {
|
||||
var restOfTag = matcher[1];
|
||||
html = html.replace(matcher[0], `<img class="w-100" ${restOfTag}>`);
|
||||
matcher = regex.exec(html);
|
||||
}
|
||||
}
|
||||
fs.writeFileSync(outputDir + fileNameNoExtension + '-' + mdSectionHtmlTitles[ii] + '.ejs', html, 'utf8');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function generateRecipeNavigatorList(recipeSrcDir, generatedOutputDir) {
|
||||
// generate a list of recipe links. While doing so generate an array
|
||||
// of unique hashtags found in all recipes
|
||||
mkdirp.sync(generatedOutputDir);
|
||||
let recipeListPartialOut = "";
|
||||
let allRecipeHashtags = [];
|
||||
fs.readdir(recipeSrcDir, (err, files) => {
|
||||
files.sort().forEach(file => {
|
||||
if (!file.endsWith('.md')) {
|
||||
return;
|
||||
}
|
||||
let fileNameNoExtension = file.slice(0, -3);
|
||||
|
||||
const data = fs.readFileSync(recipeSrcDir + file, { encoding: 'utf8', flag: 'r' });
|
||||
|
||||
// find all hashtags in the file
|
||||
var hashtagRegex = new RegExp(`#(\\w+)`, `g`);
|
||||
hashtagMatcher = hashtagRegex.exec(data);
|
||||
var recipeTags = []; // hashtags of the current recipe only
|
||||
while (hashtagMatcher != null) {
|
||||
var hashtag = hashtagMatcher[1].toLowerCase();
|
||||
if (!allRecipeHashtags.includes(hashtag)) {
|
||||
allRecipeHashtags.push(hashtag);
|
||||
}
|
||||
if (!recipeTags.includes(hashtag)) {
|
||||
recipeTags.push(hashtag);
|
||||
}
|
||||
hashtagMatcher = hashtagRegex.exec(data);
|
||||
}
|
||||
|
||||
let combinedRecipeTags = "";
|
||||
if (recipeTags.length > 0) {
|
||||
combinedRecipeTags = recipeTags.join(",");
|
||||
}
|
||||
|
||||
// get first recipe title from document
|
||||
var titleRegex = new RegExp(`#\\s+(.+)\\n`, `g`);
|
||||
titleMatcher = titleRegex.exec(data);
|
||||
var recipeTitle = fileNameNoExtension;
|
||||
if (titleMatcher != null) {
|
||||
recipeTitle = titleMatcher[1];
|
||||
}
|
||||
|
||||
recipeListPartialOut += `<a href="recipes/${fileNameNoExtension}" class="list-group-item list-group-item-action" tags="${combinedRecipeTags}">${recipeTitle}</a>\n`;
|
||||
});
|
||||
|
||||
// writeout the link list partial
|
||||
fs.writeFileSync(generatedOutputDir + "recipe-links.ejs", recipeListPartialOut, "utf-8");
|
||||
|
||||
// now generate the hashtag button list partial
|
||||
// TODO: in the future sort the list by number of hashtag hits (most -> least common)
|
||||
// instead of alphabetically
|
||||
let tagListPartialOut = "";
|
||||
allRecipeHashtags.sort().forEach(hashtag => {
|
||||
tagListPartialOut += `<button type="button" class="btn btn-light btn-sm">${hashtag}</button>\n`;
|
||||
});
|
||||
fs.writeFileSync(generatedOutputDir + "recipe-tags.ejs", tagListPartialOut, "utf-8");
|
||||
});
|
||||
}
|
||||
|
||||
convertMarkdownInDirWithShowdown(projectInputDir, projectOutputDir, projectsConverter);
|
||||
convertRecipeMarkdown(recipeInputDir, recipeOutputDir);
|
||||
generateRecipeNavigatorList(recipeInputDir, recipeListGeneratedOutputDir);
|
|
@ -1,108 +0,0 @@
|
|||
# Darkstar Quadcopter
|
||||
|
||||
The Darkstar is a RC quadcopter with the ability to fly autonomously through pre-designated waypoints, using advanced estimation techniques and object avoidance.
|
||||
|
||||
..At least that is the end-goal. Getting there however requires resources I simply do not have as a college student (read: money). Given such constraints, building an advance copter on the cheap sounded like a good challenge. Perhaps you can learn from my mistakes if you're interested in such a venture.
|
||||
|
||||
Having landed a gig at an autonomous flight research center for the summer, I figured now was as good a time as any to dive into the exciting world of quadrotors - a world I've always been interested in, but never had the time to sit down and learn.
|
||||
|
||||
## Part 1: Part Selection
|
||||
|
||||
As is the case with most hobbies, much of my money was spent on initial startup costs: an RC transmitter and battery charger, while essential, would only need to be purchased once. If I want to build more rc-craft in the future, this will make it a lot easier.
|
||||
|
||||

|
||||
|
||||
**Transmitter:** I went with a [FrSky Taranis QX7](https://www.amazon.com/dp/B06XC4C4WH) for a few reasons:
|
||||
|
||||
1. It's "cheap": This is a pretty feature-rich transmitter for the price. Amazon has it priced fairly high, but I managed to pick mine up from [ProgressiveRC](https://www.progressiverc.com/) for $105, shipping was free and fast.
|
||||
|
||||
2. Experience: I've been using/fiddling around with this transmitter at work while building some vehicles, so I had at least an introduction to the system, leaving me with one less thing to learn.
|
||||
|
||||
3. Aesthetic: From what I've seen, there are few transmitters out there that have any semblance of competent industrial design. This happens to be one of them. It's well-built with decent ergonomics and doesn't look overly tacky or intimidating. Plus it comes in white, which I really have been enjoying lately for some reason.
|
||||
|
||||
**Frame:** Really nothing special here. I got a [Reptile 500](https://www.ebay.com/sch/i.html?_nkw=reptile500+v3+quadcopter+frame) frame... or something. Honestly not sure on the name here. Basically my strategy was to browse [hobbyking](https://hobbyking.com) til I found something I liked, then I headed over to ebay, to buy something similar. You can get frames for pretty cheap off there if you're willing to wait for it to ship from China. I wasn't, so I paid an extra $10 to get it from a US seller.
|
||||
|
||||
As Shipped:
|
||||

|
||||
|
||||
(Mostly) Assembled:
|
||||

|
||||
|
||||
Some of the arms were a pain to secure:
|
||||

|
||||
|
||||
Overall I'm pretty happy with the frame. It's simple and it was cheap. At first I thought 500mm between motors would be huge, but I've grown to like it and how much space it gives me. I have plenty of room to jam all of my various gizmos throughout. Being cheap and from ebay, it was a bit of an effort to secure all the arms to the frame; aligning the holes was more difficult than anticipated. But once mounted they're pretty solid, and have already survived a few crashes with ease.
|
||||
|
||||
**Motors:** [LHI 2212 920KV](https://www.amazon.com/dp/B00XQYTZQ2) motors. Again, cheap and functional. The product I ordered from Amazon came with ESC's, which I thought was great, but I eventually had to swap out those ESC's, so overall, not worth the 'savings' I thought I was getting. I didn't have any idea as to what speed or motor rating I wanted, and initially I was worried that 920KV wouldn't be fast enough. However, seeing that 920 is used by the phantom reassured me and they work great. The copter isn't too acrobatic, but still has some 'umph'. Another great thing about these motors is they're built as DJI replacements, which means they also work with DJI's self-tightening props. Thank goodness! That makes portability/replacement so much easier.
|
||||
|
||||
I was stupid and tried to screw the motor in through all 4 holes on the arm with some aggressive dremeling, before realizing there were two holes for my motor size, and two for a different size.
|
||||
|
||||

|
||||
|
||||
DJI props! So much easier than the other nightmares I've worked with in the past.
|
||||
|
||||

|
||||
|
||||
**Receiver:** [FrSky D4R-II](https://www.amazon.com/gp/product/B00SWHWFWO/) Cheap, compatible, capable. I would be comfortable with any FrSky CPPM receiver here.
|
||||
|
||||
**ESC:** [Makerfire 20A](https://www.amazon.com/gp/product/B01DEN46I6) As I mentioned above, the ESC's that came with my motors had some weird issues... Actually come to think of it, it was likely my own stupidity that was the issue. It's okay though, the makerfire esc's get the job done, and are a factor of magnitude smaller/lighter than my original esc's, so I'll consider that a win.
|
||||
|
||||
**Flight Controller:** Flip32+ This is one of those parts that I didn't want to skimp out on or mess around with. This is the board that we use fairly exclusively at work, so I'm familiar with it, and it's a reasonable price. The cheaper Chinese versions of these have been known to have some IMU/Gyro issues, so we only buy these from [readytoflyquads](http://www.readytoflyquads.com/the-flip32-187)
|
||||
|
||||
**Battery:**Currently I'm using a 2200mAh 3S LiPo battery, but as of writing this, I'm looking at stepping up to a 4 or 5000mAh. 2200 is adequate in terms of flight time, but as I throw more gear on this thing, it'd be good to have something a bit larger.
|
||||
|
||||
### Assembly
|
||||
Mostly I just added things on here and there as I got them in the mail. I had most of the frame pieces setup and ready to go by the time I had my big 'assembly party'... alone... on a Friday night.... Help me:
|
||||
|
||||
Receiver mounted on top with a twist tie, ESC's secured on the arm with some good velcro/zip ties:
|
||||

|
||||
|
||||
This plate+anti-shock mount combo was intended to go in the front of the drone for fpv. While that is something I would like to eventually add, this plate also happened to be the perfect size for the Flip32. So I drilled a few holes, allowing me to mount the naze as close to the center of gravity as was reasonable:
|
||||

|
||||
|
||||
Assembly can get messy:
|
||||

|
||||
|
||||
This is the handiest edition I think I've made. This allows me to plug in the battery with the confidence that the motors aren't going to immediately attack me. Currently I only use one side of the switch, but in the future I plan on having one side turn everything on, while the other side only turns on the small electronics(and not the finger-slicing motors):
|
||||

|
||||
|
||||
Power distribution soldered and mounted! The velcroed piece at the top is my 5V BEC:
|
||||

|
||||
|
||||
Naze (aka Flip32) mounted! This is all a bit tighter than anticipated:
|
||||

|
||||
|
||||
Todo: cable management
|
||||

|
||||
|
||||
Also Todo: Secure the battery in a non-terrible way
|
||||

|
||||
|
||||
## Part 2: Fixes and Tweaks
|
||||
Its been a few weeks since writing part 1 and a lot has changed. For starters the thing actually flies now. As I mentioned in part 1 I had some troubles getting my first set of esc's to work correctly. So I replaced them with new, smaller ones, and *still* had trouble with them. That is until I finally sat down and figured out how to calibrate them (protip: read the instructions that come with your products!). With that squared away, this hunk of junk finally became a flyable drone as opposed to a 180deg flipping machine, as shown in the video below.
|
||||
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/TKvzu6X0z1E" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>
|
||||
|
||||
Unfortunately I dont have any footage of the first successful flights(I was out by myself, as usual), but just know it was legendary. Flight 1 went well until I accidentally crashed into a pine tree, which provided a surprisingly cushy landing. Flight 2 was absolutely beautiful until I somehow managed to clothesline my drone on literally the only power line in the immediate vicinity. The rest of the initial flights followed a similar pattern. All-in-all, I ended up buying another 4 sets (with 2/set) of props, after breaking 4 of them. Seeing as this was my first real drone-flying experience, and I was still tweaking some of the settings, I chalk it up to the cost of learning.
|
||||
|
||||

|
||||
|
||||
### Sim Practice
|
||||
|
||||
Around this time I also discovered a nifty, cheap piece of software: [fpv-freedrider](https://fpv-freerider.itch.io/fpv-freerider). This is simple but functional simulator that is handy to practice on. I simply plugged my transmitter via usb into my machine and it recognized it straight away. All I had to do was calibrate once in the software and I was off to the sim. This definitely helps with mastering some basic flight skills, but naturally its not as difficult as the real thing. And I've found the best way to learn to fly is having the constant threat of a catastrophic crash looming over you and every decision you make (while flying that is).
|
||||
|
||||
### LED Upgrade
|
||||
|
||||
These days its basically an undeniable fact that RGB LEDs make everything in this world better. And you'd be a fool to think I wasn't planning on adding them from the beginning. With the quad finally getting up in the air, I needed something to make my crashes look *cooler*. and boy-oh-boy did the LEDs fit the bill. The LEDs are programmed through an Arduino and change state/pattern based on the CPPM input coming from the rc receiver!
|
||||
|
||||
I've been planning for some time to integrate an Arduino nano into the copter because of the rapid prototyping I'll be able to do with various sensors that work with it. LED control was the perfect first-step to get the Arduino project online. This was also my first time working with the Arduino and LED control is the simple (and traditional!) place to start. Here's an overview of how the LED control works as of this writing:
|
||||
|
||||
- When the craft is disarmed, the LEDs fade in and out
|
||||
|
||||
- When armed, the LEDs switch to solid illumination
|
||||
|
||||
- If armed, and not in autopilot mode, if the incoming CPPM command does change *enough* (ie the current command hasn't deviated by some delta compared to multiple past commands), then the LEDs alternate from solid to blinking about one every 1.5 seconds, until a new unique command is read.
|
||||
|
||||
The backbone of this project is an absolutely killer [CPPM library](https://github.com/jmparatte/CPPM) developed by Jean-Marc Paratte. The library is very simple to use, and the examples are self-explanatory. It was pretty accurate with my 8-channel FrSky receiver. With that library in place, the rest was just some good 'n simple state machine logic. If you're interested in taking a peek, here's [the repository](https://github.com/len0rd/darkstar_copter). Release 0.1 has the basic LED state machine using CPPM. After that release I've added some more features, making it a bit more complex (I'll write about those later, when I know they all work properly).
|
||||
|
||||
Assembly was easy. For now, I'm running all 4 LEDs off one MOSFET and 1 pin on the Arduino. This is so I have more pins available for other sensors in the future, but it would also be cool to have each arm individually controlled. [Here](https://www.amazon.com/gp/product/B017X92K9Y) are the LEDs I used. [This guy](https://www.youtube.com/watch?v=sVyi7yWuXxs) is pretty helpful if you need help figuring out how to use a MOSFET + Arduino to control 12V LEDs. These LEDs are actually a really good reason to use a 3S LiPo battery, since its standard voltage is ~12V ish.
|
|
@ -1,43 +0,0 @@
|
|||
# My Website
|
||||
|
||||
Starting out with this website, I had essentially no knowledge of modern web technologies. I knew that I wanted something modern but also easy to maintain that I could use well into the future.
|
||||
|
||||
The end result is the site you see here. By no means perfect or beautiful, but functional and a place where I can store guides mainly for my benefit. But maybe for your benefit too? I certainly dont know who's reading this ¯\\\_(ツ)_/¯
|
||||
|
||||
As I'm writing this page after the fact (about a year since originally making this site), I'll likely glaze over a lot of the details.
|
||||
|
||||
## Technologies Used
|
||||
|
||||
- [Node](https://nodejs.org/) for quick 'n easy webserver creation
|
||||
|
||||
- [NPM](https://www.npmjs.com) for a bunch of support packages
|
||||
|
||||
- [ExpressJS](https://expressjs.com) with [ejs](https://ejs.co/) for amped up static pages. Express is great for beginners that want a simple framework for a static website with no code duplication
|
||||
|
||||
- [Bootstrap](https://getbootstrap.com) to make it all pretty
|
||||
|
||||
- Other stuff
|
||||
|
||||
## Express
|
||||
|
||||
Originally I started this site as a pure html/bootstrap affair. This worked for all of 2 days until I got sick of copying and pasting code all over the place. While I had no desire to maintain duplicate copies of code, I was even less interested in using some massive overkill framework (as an embedded dev, I have a need for speed). Low and behold: ExpressJS! The perfect minimal framework solution for my problem. Express has a concept of 'pages' and 'partials'. A page defines the overall structure of a static webpage (say my home page). Partials define chunks/components of that page that are shared in other locations. So for example, all the html for my navigation/ header bar has its own partial, as does the footer. Then in a page, to use this content you can simply add a `<% include` as if you were writing a C program! Express was speaking my language.
|
||||
|
||||
## Static Project Pages
|
||||
|
||||
The bulk of the effort for me was sunk into generating the project writeup pages (like the page you're reading this off of _right now_!). I wanted them to be simple static text, images and video. But I didn't want the complexity of using a whole framework like wordpress, and I definitely wasn't into the idea of writting everything in html. I wanted my writeups to be in a portable format I could easily migrate or use in other places in the future.
|
||||
|
||||
Given these requirements I thought it best to write about all of my projects in markdown. I've used markdown for years and like its readability and easy syntax. To convert my markdown to HTML, I grabbed [showdown](https://github.com/showdownjs/showdown). Showdown does it's job well and has some hooks (called 'extensions') that made it easier for me to get the formatting jusssst right. At present the only extension I've created helps make the title/H1 of each writeup nice and big (ie: look at those big 'My Website' letters up top). All the showdown generator stuff lives in `prestart.js` which is run before the server is started so the markdown is generated once and can then be served statically for all time.
|
||||
|
||||
I saved showdown's resulting files as ExpressJS partials. These partials are linked to a template page which adds the header, footer and table of contents you see here. Then, any requests that contain `/projects` actually load the `project_template` page with the requested project-name partial. Express makes this all surprisingly simple (I say after struggling with it for hours):
|
||||
|
||||
```js
|
||||
if (pathname.includes('projects') && page !== 'index') {
|
||||
// projects has a custom template that is used for all projects
|
||||
// so we need to change the pathname that the renderer is using
|
||||
// that template:
|
||||
pathname = pathname.substr(0, pathname.lastIndexOf(page));
|
||||
pathname += 'project_template'
|
||||
// provide the pagename for project_template to use for main content
|
||||
page = 'partials/md/' + page;
|
||||
}
|
||||
```
|
43
server.js
43
server.js
|
@ -1,43 +0,0 @@
|
|||
const PORT = 8090;
|
||||
var express = require('express');
|
||||
var app = express();
|
||||
|
||||
console.log('Starting express server on port ' + PORT);
|
||||
|
||||
// set the view engine to ejs
|
||||
app.set('view engine', 'ejs');
|
||||
|
||||
// add folder for static content:
|
||||
app.use(express.static(__dirname + '/assets'));
|
||||
|
||||
app.get(/\/.*/, function (req, res) {
|
||||
let pathname = 'pages' + req.path;
|
||||
let page = pathname.substr(pathname.lastIndexOf('/') + 1);
|
||||
|
||||
if (pathname !== null && pathname !== undefined) {
|
||||
if ((pathname)[pathname.length - 1] === '/') {
|
||||
pathname += 'index';
|
||||
page = 'index';
|
||||
}
|
||||
if (pathname.includes('projects') && page !== 'index') {
|
||||
// projects has a custom template that is used for all projects
|
||||
// so we need to change the pathname that the renderer is using
|
||||
// that template:
|
||||
pathname = pathname.substr(0, pathname.lastIndexOf(page));
|
||||
pathname += 'project_template'
|
||||
// provide the pagename for project_template to use for main content
|
||||
page = 'partials/md/projects/' + page;
|
||||
}
|
||||
else if (pathname.includes('recipes') && page !== 'index') {
|
||||
pathname = pathname.substr(0, pathname.lastIndexOf(page));
|
||||
pathname += 'recipe_template'
|
||||
// provide the pagename for project_template to use for main content
|
||||
page = 'partials/md/recipes/' + page;
|
||||
}
|
||||
}
|
||||
console.log('request for path: ' + pathname + ', and page: ' + page);
|
||||
|
||||
res.render(pathname, { "page": page });
|
||||
});
|
||||
|
||||
app.listen(PORT);
|
|
@ -1,23 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<%- include('../partials/include') %>
|
||||
</head>
|
||||
|
||||
<body class="d-flex flex-column min-vh-100">
|
||||
<header>
|
||||
<%- include('../partials/nav') %>
|
||||
</header>
|
||||
|
||||
<div class="container mt-5 topMargin">
|
||||
<h1>Contact me</h1>
|
||||
<p>Questions? Comments? Spam? Please email me with the below address:</p>
|
||||
<h4>len0rd"AT"fastmail.co.uk</h4>
|
||||
</div>
|
||||
|
||||
<%- include('../partials/footer') %>
|
||||
<%- include('../partials/post_html_include') %>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,142 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<%- include('../partials/include') %>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<%- include('../partials/nav') %>
|
||||
</header>
|
||||
|
||||
<main role="main">
|
||||
|
||||
<section class="bgimage">
|
||||
<div class="container center">
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<h1 class="display-1">welcome to lenordsNet</h1>
|
||||
<h3 class="text-white">enjoy my ramblings</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="container-flex code">
|
||||
<div class="container">
|
||||
<div class="row text-white pb-5 pt-5">
|
||||
<div class="col-4 pr-5 d-flex">
|
||||
<samp class="display-1 ml-auto">></samp>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<samp>I'm an American embedded software engineer with diverse industry experience. From large
|
||||
commercial Java projects to low-level C firmware development, I've done a little bit of
|
||||
everything (except web development as will be evident with this website). Such experience
|
||||
allows me to understand and solve new problems quickly. My hobbies are just as eclectic as
|
||||
my professional experience. This website showcases many of my personal projects.</samp>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row row-cols-1 row-cols-md-3 g-2 p-5">
|
||||
<div class="col">
|
||||
<div class="card bg-dark card-homepage border-light text-white">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Yama Crawler</h5>
|
||||
<h6 class="card-subtitle mb-2 text-muted">Selenium-Based Web Crawler</h6>
|
||||
<p class="card-text">Yama is a powerful web crawler/scraper based on Selenium and Java.
|
||||
Built for and used in a commercial application by yours truly</p>
|
||||
</div>
|
||||
<div class="card-footer pb-3">
|
||||
<a href="https://github.com/len0rd/YamaCrawler" class="card-link card-soft-link">See
|
||||
Code</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="card bg-dark card-homepage border-light text-white">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Mavlib Gen</h5>
|
||||
<h6 class="card-subtitle mb-2 text-muted">Modern Mavlink C generator</h6>
|
||||
<p class="card-text">This is what happens when I get fed up with a protocols reference
|
||||
implementation (after using it for over 4 years)</p>
|
||||
</div>
|
||||
<div class="card-footer pb-3">
|
||||
<a href="https://github.com/len0rd/mavlib_gen" class="card-link card-soft-link">See Code</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="card bg-dark card-homepage border-light text-white">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Darkstar</h5>
|
||||
<h6 class="card-subtitle mb-2 text-muted">Why buy a quad when you can build it</h6>
|
||||
<p class="card-text">Darkstar was my first foray into building my own drone. I used a
|
||||
mixture
|
||||
of open source software and some of my own stuff to get the hunk of junk into the air
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-footer pb-3">
|
||||
<a href="projects/darkstar" class="btn btn-outline-light">Read More</a>
|
||||
<a href="https://github.com/len0rd/darkstar_copter" class="card-link card-soft-link">See
|
||||
Code</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="card bg-dark card-homepage border-light text-white">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Pallet Desk</h5>
|
||||
<h6 class="card-subtitle mb-2 text-muted">Reliable and cheap desk</h6>
|
||||
<p class="card-text">Building my dream desk on a poor-college-student budget. The end
|
||||
result: a portable and rock solid desk with a couple of flaws</p>
|
||||
</div>
|
||||
<div class="card-footer pb-3">
|
||||
<a href="projects/palletDesk" class="btn btn-outline-light">Read More</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="card bg-dark card-homepage border-light text-white">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">My Website</h5>
|
||||
<h6 class="card-subtitle mb-2 text-muted">Is this meta</h6>
|
||||
<p class="card-text">A post on my website about how I built my website? Yep. Turns out
|
||||
things aren't quite plug 'n play when you're a stubborn engineer who wants something to
|
||||
function in a very specific way</p>
|
||||
</div>
|
||||
<div class="card-footer pb-3">
|
||||
<a href="projects/myWebsite" class="btn btn-outline-light">Read More</a>
|
||||
<a href="https://github.com/len0rd/personal-website" class="card-link card-soft-link">See
|
||||
Code</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="card bg-dark card-homepage border-light text-white">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">LS-1 Synth</h5>
|
||||
<h6 class="card-subtitle mb-2 text-muted">Music to my ears</h6>
|
||||
<p class="card-text">I find that I learn best by doing. So when I starting considering
|
||||
getting into the <i>extremely</i> expensive hobby of analog modular synths, I figured
|
||||
the best way to start would be to build my own. The results aren't terribly impressive,
|
||||
but helped me learn a whole ton along the way</p>
|
||||
</div>
|
||||
<div class="card-footer pb-3">
|
||||
<a href="projects/ls1synth" class="btn btn-outline-light">Read More</a>
|
||||
<a href="https://github.com/len0rd/LS1-sequencer" class="card-link card-soft-link">See
|
||||
Code</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</main>
|
||||
|
||||
<%- include('../partials/footer') %>
|
||||
<%- include('../partials/post_html_include') %>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,36 +0,0 @@
|
|||
<% var rootPath = '../../'; %>
|
||||
<% var navExt = '-nav' %>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<%- include(rootPath + 'partials/include') %>
|
||||
<link rel="stylesheet" type="text/css" href="/css/projects.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<%- include(rootPath + 'partials/nav') %>
|
||||
</header>
|
||||
|
||||
<div class="container mt-5">
|
||||
<div class="row">
|
||||
<div class="col-3 mt-5" id="scrollBar">
|
||||
|
||||
</div>
|
||||
<div data-spy="scroll" data-target="#scrollBar" data-offset="0" class="col-9 mt-5 pt-5 pb-5">
|
||||
<%- include(rootPath + page) %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%- include(rootPath + 'partials/footer') %>
|
||||
<%- include(rootPath + 'partials/post_html_include') %>
|
||||
<script type="text/javascript" src="/script/dynamicscrollspy.min.js"></script>
|
||||
<script>
|
||||
$('#scrollBar').DynamicScrollspy({
|
||||
ulClassNames: 'navbar navbar-light bg-light sticky-top sticky-offset'
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,103 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<%- include('../partials/include' ) %>
|
||||
<link rel="stylesheet" type="text/css" href="/css/projects.css">
|
||||
</head>
|
||||
|
||||
<body class="d-flex flex-column min-vh-100">
|
||||
<header>
|
||||
<%- include('../partials/nav') %>
|
||||
</header>
|
||||
|
||||
<div class="container mt-5 topMargin">
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<h1 class="display-1">Recipes</h1>
|
||||
</div>
|
||||
<div class="col-4 align-self-end">
|
||||
<input class="form-control" id="recipeSearch" type="text" placeholder="search..">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col btn-container">
|
||||
<%- include('../partials/generated/recipe-tags') %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="list-group list-group-flush" id="recipe-link-container">
|
||||
<%- include('../partials/generated/recipe-links') %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%- include('../partials/footer') %>
|
||||
<%- include('../partials/post_html_include') %>
|
||||
<script>
|
||||
const TAG_ACTIVE_CLASS = "btn-primary";
|
||||
|
||||
/// Returns a list of the current recipe hashtags that are enabled (enabled filters)
|
||||
function getActiveTags() {
|
||||
var activeTags = [];
|
||||
// create a list of currently active tags
|
||||
$(".btn-container button").filter(function() {
|
||||
if ($(this).hasClass(TAG_ACTIVE_CLASS)){
|
||||
activeTags.push($(this).text())
|
||||
}
|
||||
});
|
||||
return activeTags;
|
||||
}
|
||||
|
||||
/// check if the given recipe item all the currently selected hashtag filters (an 'AND' search)
|
||||
function recipeHasAllActiveTags(activeTags, recipe) {
|
||||
const recipeTags = recipe.attr("tags").toLowerCase().split(",");
|
||||
return activeTags.every(elem => recipeTags.includes(elem));
|
||||
}
|
||||
|
||||
function recipeHasCurrentSearchPhrase(searchValue, recipe) {
|
||||
return !searchValue || recipe.text().toLowerCase().indexOf(searchValue) > -1;
|
||||
}
|
||||
|
||||
function setActiveRecipes() {
|
||||
const activeTags = getActiveTags();
|
||||
const searchValue = $("#recipeSearch").val().toLowerCase();
|
||||
|
||||
$("#recipe-link-container a").filter(function() {
|
||||
$(this).toggle(recipeHasCurrentSearchPhrase(searchValue, $(this)) && recipeHasAllActiveTags(activeTags, $(this)));
|
||||
});
|
||||
}
|
||||
|
||||
/// This script is responsible for filtering the recipe list
|
||||
/// based on enabled/disabled hashtags. I'm certain theres more
|
||||
/// efficient ways to do this but /shrug
|
||||
$(".btn-container").on("click", "button", function() {
|
||||
$(this).toggleClass("btn-light btn-primary");
|
||||
setActiveRecipes();
|
||||
});
|
||||
|
||||
/// This method is responsible for filtering the recipe list based on the search bar
|
||||
$(document).ready(function() {
|
||||
$("#recipeSearch").on("keyup", function() {
|
||||
setActiveRecipes();
|
||||
});
|
||||
});
|
||||
|
||||
// apply a tag on page load if specified in the url
|
||||
$(document).ready(function() {
|
||||
let searchParams = new URLSearchParams(window.location.search);
|
||||
if (searchParams.has("tag")) {
|
||||
const tagParam = searchParams.get("tag").toLowerCase().trim();
|
||||
$(".btn-container button").filter(function() {
|
||||
if (tagParam == $(this).text().toLowerCase().trim()) {
|
||||
$(this).toggleClass("btn-light btn-primary");
|
||||
setActiveRecipes();
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,34 +0,0 @@
|
|||
<% var rootPath = '../../'; %>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<%- include(rootPath + 'partials/include' ) %>
|
||||
<link rel="stylesheet" type="text/css" href="/css/projects.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<%- include(rootPath + 'partials/nav') %>
|
||||
</header>
|
||||
|
||||
<div class="container mt-5 topMargin">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<%- include(rootPath + page + '-title') %>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<%- include(rootPath + page + '-ingredients') %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%- include(rootPath + page + '-instructions') %>
|
||||
</div>
|
||||
|
||||
<%- include(rootPath + 'partials/post_html_include') %>
|
||||
</body>
|
||||
</html>
|
|
@ -1,26 +0,0 @@
|
|||
<!-- you may need to remove 'fixed-bottom' if the footer is persisting on scroll-->
|
||||
<footer class="footer bg-black">
|
||||
<div class="container-fluid text-center">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="flex-center py-1">
|
||||
<a class="li-ic icon-footer" style="color:black" href="https://github.com/len0rd">
|
||||
<i class="fab fa-github fa-lg" style="color:white"></i>
|
||||
</a>
|
||||
<a class="li-ic icon-footer" style="color:black" href="https://www.youtube.com/channel/UCwGDckTCkqqW5W8JcRrxELA">
|
||||
<i class="fab fa-youtube fa-lg" style="color:white"></i>
|
||||
</a>
|
||||
<a class="li-ic icon-footer" href="/contact">
|
||||
<i class="fas fa-at fa-lg" style="color:white"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12 mb-2">
|
||||
<div class="footer-copyright text-center text-white">© lenordsNet <%= new Date().getFullYear();%></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</footer>
|
|
@ -1,9 +0,0 @@
|
|||
<meta charset="UTF-8">
|
||||
|
||||
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/animate.css@3.5.2/animate.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="/css/site.css">
|
||||
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.2.0/css/all.css" integrity="sha384-hWVjflwFxL6sNzntih27bfxkr27PmbbK/iSvJ+a4+0owXq79v+lsFkW54bOGbiDQ" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="//cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.16.2/build/styles/default.min.css">
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
|
|
@ -1,21 +0,0 @@
|
|||
<nav class="navbar navbar-expand-sm navbar-dark bg-black fixed-top">
|
||||
<div class="container-fluid">
|
||||
|
||||
<a class="navbar-brand" href="/#">lenordsNet</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarText"
|
||||
aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarText">
|
||||
<ul class="navbar-nav me-auto my-2 my-lg-0 navbar-nav-scroll" style="--bs-scroll-height: 100px;">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="https://home.lenord.me/">Home Assistant</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/recipe_navigator">Recipes</a>
|
||||
</li>
|
||||
</ul>
|
||||
<a href="/contact" class="btn btn-outline-light">Contact</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
|
@ -1,2 +0,0 @@
|
|||
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
|
Loading…
Reference in a new issue