Compare commits
7 Commits
3be602c6cf
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 83c27ba1bf | |||
| 6ff2249955 | |||
| ec6cfba9ba | |||
| 22b6a06b32 | |||
| 090bc11ed0 | |||
| dac9e80efd | |||
| 8fd744118f |
13
Makefile
@@ -15,7 +15,8 @@
|
||||
cleanup-check \
|
||||
cleanup-code \
|
||||
convert_video \
|
||||
convert_video_times
|
||||
convert_video_times \
|
||||
generate_asset_imports
|
||||
|
||||
default: dev
|
||||
|
||||
@@ -83,9 +84,17 @@ convert_video_times:
|
||||
-init_hw_device vaapi=va:/dev/dri/renderD128 \
|
||||
-filter_hw_device va \
|
||||
-i $(input) \
|
||||
-vf 'format=nv12,hwupload,scale_vaapi=-2:720,trim=start=$(start):end=$(end)' \
|
||||
-ss $(start) \
|
||||
-to $(end) \
|
||||
-vf 'format=nv12,hwupload,scale_vaapi=-2:720' \
|
||||
-c:v h264_vaapi \
|
||||
-rc_mode CQP \
|
||||
-qp 28 \
|
||||
-an \
|
||||
$(output)
|
||||
|
||||
generate_asset_imports:
|
||||
@for assets_path in `find "src/assets/${assets_relative_path}" -maxdepth 1 -type f -printf "%f\n"`; do \
|
||||
without_extension=$${assets_path/%.*}; \
|
||||
echo "import $${without_extension//-/_} from \"@assets/${assets_relative_path}/$$assets_path\";"; \
|
||||
done;
|
||||
|
||||
@@ -3,7 +3,9 @@ Altium
|
||||
ASSEM
|
||||
astrojs
|
||||
Atmel
|
||||
automations
|
||||
barebones
|
||||
beaglebone
|
||||
Bitwarden
|
||||
Candian
|
||||
caperren
|
||||
@@ -13,13 +15,19 @@ Concours
|
||||
CONSERV
|
||||
Corwin
|
||||
dangerousthings
|
||||
dechorionation
|
||||
Dechorionator
|
||||
dechorionators
|
||||
dockerization
|
||||
dockerizing
|
||||
drumheller
|
||||
ebox
|
||||
ELMI
|
||||
fhhs
|
||||
flowbite
|
||||
flowrate
|
||||
gcode
|
||||
gerbers
|
||||
Gitea
|
||||
HDFS
|
||||
headshot
|
||||
@@ -34,34 +42,51 @@ leconte
|
||||
Loctite
|
||||
luxon
|
||||
MGMT
|
||||
microcontroller
|
||||
microcontroller's
|
||||
Micropumps
|
||||
Millis
|
||||
modbus
|
||||
Mokai
|
||||
Multimeters
|
||||
nixos
|
||||
nvme
|
||||
offroad
|
||||
Onshape
|
||||
OSSM
|
||||
OSURC
|
||||
panelized
|
||||
Passthroughs
|
||||
pcbs
|
||||
Perren
|
||||
Perren's
|
||||
Pixhawk
|
||||
Protocase
|
||||
pubpath
|
||||
RFID
|
||||
Rito
|
||||
RSSI
|
||||
SARL
|
||||
SCARA
|
||||
showerheads
|
||||
Shuttlebox
|
||||
simplemotion
|
||||
sinnhuber
|
||||
sitemapindex
|
||||
Smartsheet
|
||||
solderable
|
||||
ssds
|
||||
Starlink
|
||||
steller
|
||||
Steller
|
||||
Tanguay
|
||||
Teamcenter
|
||||
timelapse
|
||||
touchoff
|
||||
triaging
|
||||
trivago
|
||||
Truong
|
||||
Ubiquiti
|
||||
Unstow
|
||||
uuidv
|
||||
vaapi
|
||||
|
||||
|
After Width: | Height: | Size: 4.7 MiB |
|
After Width: | Height: | Size: 4.0 MiB |
|
After Width: | Height: | Size: 3.5 MiB |
|
After Width: | Height: | Size: 4.0 MiB |
|
After Width: | Height: | Size: 3.1 MiB |
|
After Width: | Height: | Size: 3.6 MiB |
|
After Width: | Height: | Size: 4.2 MiB |
|
After Width: | Height: | Size: 3.7 MiB |
|
After Width: | Height: | Size: 7.7 MiB |
|
After Width: | Height: | Size: 7.3 MiB |
|
After Width: | Height: | Size: 8.3 MiB |
|
After Width: | Height: | Size: 7.6 MiB |
|
After Width: | Height: | Size: 4.4 MiB |
|
After Width: | Height: | Size: 3.5 MiB |
|
After Width: | Height: | Size: 3.8 MiB |
|
After Width: | Height: | Size: 4.1 MiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 239 KiB |
|
After Width: | Height: | Size: 3.8 MiB |
|
After Width: | Height: | Size: 3.9 MiB |
|
After Width: | Height: | Size: 4.6 MiB |
|
After Width: | Height: | Size: 4.2 MiB |
|
After Width: | Height: | Size: 4.2 MiB |
|
After Width: | Height: | Size: 4.3 MiB |
|
After Width: | Height: | Size: 4.4 MiB |
|
After Width: | Height: | Size: 4.4 MiB |
|
After Width: | Height: | Size: 6.4 MiB |
|
After Width: | Height: | Size: 7.9 MiB |
|
After Width: | Height: | Size: 8.9 MiB |
|
After Width: | Height: | Size: 8.7 MiB |
|
After Width: | Height: | Size: 7.8 MiB |
|
After Width: | Height: | Size: 5.9 MiB |
|
After Width: | Height: | Size: 9.2 MiB |
|
After Width: | Height: | Size: 4.2 MiB |
|
After Width: | Height: | Size: 1013 KiB |
|
After Width: | Height: | Size: 6.8 MiB |
|
After Width: | Height: | Size: 9.2 MiB |
|
After Width: | Height: | Size: 8.8 MiB |
|
After Width: | Height: | Size: 8.5 MiB |
|
After Width: | Height: | Size: 6.3 MiB |
|
After Width: | Height: | Size: 1017 KiB |
|
After Width: | Height: | Size: 878 KiB |
|
After Width: | Height: | Size: 7.1 MiB |
|
After Width: | Height: | Size: 605 KiB |
|
After Width: | Height: | Size: 826 KiB |
|
After Width: | Height: | Size: 7.1 MiB |
|
After Width: | Height: | Size: 6.1 MiB |
|
After Width: | Height: | Size: 6.9 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 8.3 MiB |
|
After Width: | Height: | Size: 7.5 MiB |
|
After Width: | Height: | Size: 7.2 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 4.6 MiB |
|
After Width: | Height: | Size: 4.6 MiB |
|
After Width: | Height: | Size: 4.4 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 167 KiB |
|
After Width: | Height: | Size: 438 KiB |
|
After Width: | Height: | Size: 224 KiB |
|
After Width: | Height: | Size: 437 KiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 2.4 MiB After Width: | Height: | Size: 2.4 MiB |
5
src/components/H4.astro
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
<h4 class="md:text-md text-xs sm:text-sm"><slot /></h4>
|
||||
@@ -1,15 +1,21 @@
|
||||
---
|
||||
import { Image } from "astro:assets";
|
||||
|
||||
import type { ComponentPropsBase } from "@interfaces/components.ts";
|
||||
import type { carouselGroup } from "@interfaces/image-carousel.ts";
|
||||
|
||||
const groupToShow: carouselGroup = Astro.props.carouselGroup;
|
||||
interface Props extends ComponentPropsBase {
|
||||
carouselGroup: carouselGroup;
|
||||
showBorder?: boolean;
|
||||
}
|
||||
|
||||
const { class: className, carouselGroup, showBorder = true } = Astro.props;
|
||||
---
|
||||
|
||||
<custom-carousel
|
||||
class="relative flex w-full flex-col"
|
||||
data-custom-carousel={groupToShow.animation}
|
||||
data-custom-carousel-interval={groupToShow.interval}
|
||||
data-custom-carousel={carouselGroup.animation ?? "slide"}
|
||||
data-custom-carousel-interval={carouselGroup.interval}
|
||||
>
|
||||
<!-- Modal for fullscreen viewing -->
|
||||
<div
|
||||
@@ -57,11 +63,18 @@ const groupToShow: carouselGroup = Astro.props.carouselGroup;
|
||||
</div>
|
||||
|
||||
<!-- Carousel wrapper -->
|
||||
<div class="relative h-56 w-full overflow-hidden md:h-120">
|
||||
{
|
||||
groupToShow.images.map((image, index) => (
|
||||
<div
|
||||
class="hidden duration-1500 ease-in-out"
|
||||
class:list={[
|
||||
"relative h-56 w-full overflow-hidden rounded-lg md:h-120",
|
||||
showBorder ? "border" : false,
|
||||
className ? className : "border-caperren-green-dark",
|
||||
]}
|
||||
>
|
||||
{
|
||||
carouselGroup.images &&
|
||||
carouselGroup.images.map((image, index) => (
|
||||
<div
|
||||
class="hidden bg-black duration-1500 ease-in-out"
|
||||
data-custom-carousel-item={index}
|
||||
>
|
||||
<Image
|
||||
@@ -78,13 +91,14 @@ const groupToShow: carouselGroup = Astro.props.carouselGroup;
|
||||
|
||||
<!-- Slider indicators -->
|
||||
{
|
||||
groupToShow.images.length > 1 && (
|
||||
carouselGroup.images && carouselGroup.images.length > 1 && (
|
||||
<div>
|
||||
<div class="absolute bottom-2 left-1/2 z-30 flex -translate-x-1/2 space-x-3 rounded-full">
|
||||
{groupToShow.images.map((_, index) => (
|
||||
{carouselGroup.images &&
|
||||
carouselGroup.images.map((_, index) => (
|
||||
<button
|
||||
type="button"
|
||||
class="hover:bg-caperren-green-light h-3 w-3 rounded-full bg-black"
|
||||
class="hover:bg-caperren-green-light h-3 w-3 rounded-full"
|
||||
aria-current={index ? "false" : "true"}
|
||||
aria-label={index.toString()}
|
||||
data-custom-carousel-slide-to={index}
|
||||
@@ -96,15 +110,16 @@ const groupToShow: carouselGroup = Astro.props.carouselGroup;
|
||||
class="group absolute start-0 top-0 z-30 flex h-full cursor-pointer items-center justify-center px-4 focus:outline-none"
|
||||
data-custom-carousel-prev
|
||||
>
|
||||
<span class="ring-caperren-green/25 inline-flex h-10 w-10 items-center justify-center rounded-full bg-black/25 ring-2 group-hover:bg-black/75">
|
||||
<span class="ring-caperren-green/35 group-hover:ring-caperren-green inline-flex h-10 w-10 items-center justify-center rounded-full bg-black/35 ring-2 group-hover:bg-black/75">
|
||||
<svg
|
||||
class="text-caperren-green group-hover:text-caperren-green-light h-4 w-4"
|
||||
class="text-caperren-green stroke-caperren-green group-hover:text-caperren-green-light h-4 w-4"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 6 10"
|
||||
viewBox="0 0 8 10"
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
fill-opacity="0%"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
@@ -119,15 +134,16 @@ const groupToShow: carouselGroup = Astro.props.carouselGroup;
|
||||
class="group absolute end-0 top-0 z-30 flex h-full cursor-pointer items-center justify-center px-4 focus:outline-none"
|
||||
data-custom-carousel-next
|
||||
>
|
||||
<span class="ring-caperren-green/25 inline-flex h-10 w-10 items-center justify-center rounded-full bg-black/25 ring-2 group-hover:bg-black/75">
|
||||
<span class="ring-caperren-green/25 group-hover:ring-caperren-green inline-flex h-10 w-10 items-center justify-center rounded-full bg-black/25 ring-2 group-hover:bg-black/75">
|
||||
<svg
|
||||
class="text-caperren-green group-hover:text-caperren-green-light h-4 w-4"
|
||||
class="text-caperren-green stroke-caperren-green group-hover:text-caperren-green-light h-4 w-4"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 6 10"
|
||||
viewBox="0 0 4 10"
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
fill-opacity="0%"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
|
||||
@@ -22,7 +22,7 @@ const {
|
||||
|
||||
<div class="mx-auto my-auto">
|
||||
<video
|
||||
class="h-auto w-full"
|
||||
class="border-caperren-green-dark h-auto w-full rounded-lg border"
|
||||
controls={controls}
|
||||
autoplay={autoPlay}
|
||||
loop={loop}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
import type { videoConfig } from "@interfaces/yt-video.ts";
|
||||
import type { videoConfig } from "@interfaces/video.ts";
|
||||
|
||||
interface Props extends videoConfig {}
|
||||
|
||||
@@ -18,7 +18,7 @@ const aspect = `${width} / ${height}`;
|
||||
<iframe
|
||||
src={videoPath}
|
||||
title={videoTitle}
|
||||
class="h-full w-full"
|
||||
class="border-caperren-green-dark h-full w-full rounded-lg border"
|
||||
allow={"accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; " +
|
||||
(autoPlay ? "autoplay;" : "")}
|
||||
referrerpolicy="strict-origin-when-cross-origin"
|
||||
|
||||
@@ -36,7 +36,8 @@ const { pathname } = Astro.url;
|
||||
data-dropdown-offset-skidding="12"
|
||||
class:list={[
|
||||
"hover:text-caperren-green-light lg:hover:text-caperren-green-light flex w-full items-center justify-between lg:p-0 lg:hover:bg-transparent",
|
||||
pathname.startsWith(getHrefPath(paths, entry))
|
||||
pathname.startsWith(getHrefPath(paths, entry)) &&
|
||||
!(entry.placeholderEntry ?? false)
|
||||
? "border-caperren-green border-b-2"
|
||||
: false,
|
||||
]}
|
||||
@@ -71,20 +72,32 @@ const { pathname } = Astro.url;
|
||||
) : (
|
||||
<div>
|
||||
<a
|
||||
href={getHrefPath(paths, entry)}
|
||||
href={
|
||||
!(entry.placeholderEntry ?? false)
|
||||
? getHrefPath(paths, entry)
|
||||
: undefined
|
||||
}
|
||||
target={
|
||||
getHrefPath(paths, entry).startsWith("/") ? "" : "_blank"
|
||||
}
|
||||
class:list={[
|
||||
"hover:text-caperren-green-light ring-caperren-green-dark block bg-transparent lg:p-0",
|
||||
"ring-caperren-green-dark block bg-transparent lg:p-0",
|
||||
pathname === getHrefPath(paths, entry)
|
||||
? "border-caperren-green border-b-2"
|
||||
: false,
|
||||
entry.isSubItem ? "ms-3" : false,
|
||||
entry.placeholderEntry
|
||||
? false
|
||||
: "hover:text-caperren-green-light",
|
||||
]}
|
||||
aria-current={
|
||||
pathname === getHrefPath(paths, entry) ? "page" : undefined
|
||||
pathname === getHrefPath(paths, entry) &&
|
||||
!(entry.placeholderEntry ?? false)
|
||||
? "page"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{entry.isSubItem && "∟ "}
|
||||
{entry.navText}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ const hasHeader = Astro.slots.has("header");
|
||||
const hasDefault = Astro.slots.has("default");
|
||||
---
|
||||
|
||||
<div class="grid grid-cols-1 gap-3">
|
||||
<div class="grid grid-cols-1 gap-0.5">
|
||||
{
|
||||
Astro.slots.has("header") && (
|
||||
<div>
|
||||
|
||||
@@ -11,6 +11,7 @@ const keys: { [key: string]: string } = {
|
||||
GUI: "Graphical user interface",
|
||||
NUC: "A small and low-power computer made by Intel",
|
||||
PCBs: "Printed circuit boards",
|
||||
SCARA: "Selective Compliance Assembly Robot Arm",
|
||||
TDD: "Test driven development",
|
||||
UPS: "Uninterruptible power supply",
|
||||
VISA: "Virtual instrument software architecture",
|
||||
|
||||
58
src/components/PrintedCircuitBoard.astro
Normal file
@@ -0,0 +1,58 @@
|
||||
---
|
||||
import Carousel from "@components/Media/CustomCarousel/CustomCarousel.astro";
|
||||
import Ul from "@components/Ul.astro";
|
||||
|
||||
import H3 from "@components/H3.astro";
|
||||
import H4 from "@components/H4.astro";
|
||||
import PageGroup from "@components/PageGroup.astro";
|
||||
import type {
|
||||
printedCircuitBoard,
|
||||
printedCircuitBoardRevision,
|
||||
} from "@interfaces/printed-circuit-board.ts";
|
||||
|
||||
interface Props {
|
||||
pcb: printedCircuitBoard;
|
||||
}
|
||||
|
||||
const { pcb } = Astro.props;
|
||||
const semanticPcbRevisionSort = (
|
||||
a: printedCircuitBoardRevision,
|
||||
b: printedCircuitBoardRevision,
|
||||
): number =>
|
||||
-((a.major - b.major) * 100 + (a.minor - b.minor) * 10 + (a.patch - b.patch));
|
||||
---
|
||||
|
||||
<PageGroup>
|
||||
<Fragment slot="header"
|
||||
><H3>{pcb.name}</H3><H4>{pcb.description}</H4></Fragment
|
||||
>
|
||||
<div class="mt-1 grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
{
|
||||
pcb.revisions?.sort(semanticPcbRevisionSort).map((revision) => (
|
||||
<div class="border-caperren-green block space-y-2 rounded-lg border bg-black py-2">
|
||||
<div class="border-caperren-green flex flex-wrap items-center justify-between rounded-none border-b px-4 pb-2">
|
||||
<div>
|
||||
<span class="font-black">Revision:</span>
|
||||
<span>
|
||||
{revision.major}.{revision.minor}.{revision.patch}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-sm italic">{revision.date.toISODate()}</div>
|
||||
</div>
|
||||
<div class="px-4">
|
||||
<Carousel
|
||||
class=""
|
||||
carouselGroup={{ images: revision.images }}
|
||||
showBorder={false}
|
||||
/>
|
||||
</div>
|
||||
{revision.notes && revision.notes.length > 0 && (
|
||||
<div class="border-caperren-green border-t px-4 pt-4 pb-2 text-sm">
|
||||
<Ul lineItems={revision.notes} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</PageGroup>
|
||||
@@ -1,7 +1,11 @@
|
||||
---
|
||||
import type { timelineEntry } from "@interfaces/timeline.ts";
|
||||
|
||||
const timeline: timelineEntry[] = Astro.props.timeline || [];
|
||||
interface Props {
|
||||
timeline: timelineEntry[];
|
||||
}
|
||||
|
||||
const { timeline = [] } = Astro.props;
|
||||
---
|
||||
|
||||
<custom-timeline>
|
||||
@@ -11,7 +15,11 @@ const timeline: timelineEntry[] = Astro.props.timeline || [];
|
||||
data-timeline
|
||||
>
|
||||
{
|
||||
timeline.map((entry, index) => (
|
||||
timeline
|
||||
.sort((a: timelineEntry, b: timelineEntry) =>
|
||||
a.date.diff(b.date).toMillis(),
|
||||
)
|
||||
.map((entry, index) => (
|
||||
<div
|
||||
class="border-caperren-green min-w-s max-w-s rounded-lg border bg-black px-2 pt-1 pb-2"
|
||||
data-timeline-node-index={index}
|
||||
@@ -19,7 +27,7 @@ const timeline: timelineEntry[] = Astro.props.timeline || [];
|
||||
<h3 class="text-lg font-bold">{entry.event}</h3>
|
||||
<h4 class="leading-none font-semibold">{entry.eventDetail}</h4>
|
||||
<time class="mt-1 mb-2 text-sm leading-none italic">
|
||||
{entry.date}
|
||||
{entry.date.toFormat("LLLL kkkk")}
|
||||
</time>
|
||||
{entry.description && (
|
||||
<p class="text-sm font-normal">{entry.description}</p>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { navLink } from "@interfaces/site-layout.ts";
|
||||
|
||||
export const siteLayout: navLink[] = [
|
||||
// Standard navbar entries
|
||||
{ navText: "About", path: "" },
|
||||
{ navText: "Education", path: "education" },
|
||||
{
|
||||
@@ -26,60 +25,65 @@ export const siteLayout: navLink[] = [
|
||||
navText: "OSU CEOAS Ocean Mixing Group",
|
||||
path: "osu-ceoas-ocean-mixing-group",
|
||||
children: [
|
||||
{
|
||||
navText: "Student Software/Electrical Engineer",
|
||||
placeholderEntry: true,
|
||||
},
|
||||
{
|
||||
navText: "Robotic Oceanographic Surface Sampler",
|
||||
isSubItem: true,
|
||||
path: "robotic-oceanographic-surface-sampler",
|
||||
},
|
||||
{
|
||||
navText: "LeConte Glacier Deployments",
|
||||
isSubItem: true,
|
||||
path: "leconte-glacier-deployments",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
enabled: false,
|
||||
navText: "OSU SARL",
|
||||
navText: "OSU Sinnhuber Aquatic Research Lab",
|
||||
path: "osu-sinnhuber-aquatic-research-laboratory",
|
||||
children: [
|
||||
{
|
||||
enabled: false,
|
||||
navText: "Team Lead",
|
||||
path: "team-lead",
|
||||
navText: "Student Automation Engineer",
|
||||
placeholderEntry: true,
|
||||
},
|
||||
{
|
||||
enabled: false,
|
||||
navText: "Zebrafish Embryo Pick and Plate",
|
||||
isSubItem: true,
|
||||
path: "zebrafish-embryo-pick-and-plate",
|
||||
},
|
||||
{
|
||||
enabled: false,
|
||||
navText: "Shuttlebox Behavior System",
|
||||
isSubItem: true,
|
||||
path: "shuttlebox-behavior-system",
|
||||
},
|
||||
{
|
||||
enabled: false,
|
||||
navText: "Dechorionator",
|
||||
isSubItem: true,
|
||||
path: "dechorionator",
|
||||
},
|
||||
{
|
||||
enabled: false,
|
||||
navText: "Denso Embryo Pick and Plate",
|
||||
isSubItem: true,
|
||||
path: "denso-embryo-pick-and-plate",
|
||||
},
|
||||
{
|
||||
enabled: false,
|
||||
navText: "ZScan Processor",
|
||||
isSubItem: true,
|
||||
path: "zscan-processor",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
enabled: false,
|
||||
navText: "OSU Robotics Club",
|
||||
path: "osu-robotics-club",
|
||||
children: [
|
||||
{
|
||||
enabled: false,
|
||||
navText: "Mars Rover Software Team Lead",
|
||||
path: "mars-rover-software-team-lead",
|
||||
},
|
||||
@@ -262,7 +266,10 @@ export const getPaths = (
|
||||
];
|
||||
} else {
|
||||
let enabled = currentEntry.enabled ?? true;
|
||||
if (disabledOnly ? !enabled : enabled) {
|
||||
if (
|
||||
(disabledOnly ? !enabled : enabled) &&
|
||||
!currentEntry.placeholderEntry
|
||||
) {
|
||||
foundPaths.push("/" + [...paths, currentEntry.path || ""].join("/"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export interface carouselGroup {
|
||||
animation: "static" | "slide";
|
||||
animation?: "static" | "slide";
|
||||
interval?: number;
|
||||
images: ImageMetadata[];
|
||||
images?: ImageMetadata[];
|
||||
}
|
||||
|
||||
32
src/interfaces/printed-circuit-board.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { ImageMetadata } from "astro";
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
import type { timelineEntry } from "@interfaces/timeline.ts";
|
||||
import type { lineItem } from "@interfaces/ul-li.ts";
|
||||
|
||||
export interface printedCircuitBoardRevision {
|
||||
major: number;
|
||||
minor: number;
|
||||
patch: number;
|
||||
|
||||
date: DateTime;
|
||||
|
||||
images?: ImageMetadata[];
|
||||
notes?: lineItem[];
|
||||
}
|
||||
|
||||
export interface printedCircuitBoard {
|
||||
name: string;
|
||||
description: string;
|
||||
|
||||
revisions: printedCircuitBoardRevision[];
|
||||
}
|
||||
|
||||
export const timelineFromPrintedCircuitBoard = (
|
||||
pcb: printedCircuitBoard,
|
||||
): timelineEntry[] =>
|
||||
pcb.revisions?.map((revision) => ({
|
||||
event: `PCB Released: ${pcb.name} `,
|
||||
eventDetail: `Revision: ${revision.major}.${revision.minor}.${revision.patch}`,
|
||||
date: revision.date,
|
||||
}));
|
||||
@@ -1,8 +1,13 @@
|
||||
export interface navLink {
|
||||
enabled?: boolean;
|
||||
hidden?: boolean;
|
||||
placeholderEntry?: boolean;
|
||||
|
||||
navText: string;
|
||||
isSubItem?: boolean; // For visual distinction only
|
||||
|
||||
path?: string;
|
||||
pubpath?: string;
|
||||
|
||||
children?: navLink[];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
export interface timelineEntry {
|
||||
event: string;
|
||||
eventDetail?: string;
|
||||
date: string;
|
||||
date: DateTime;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ const pageEnabled = pathToMetadata(Astro.url.pathname).enabled ?? true;
|
||||
class="grow overflow-x-hidden overflow-y-scroll"
|
||||
>
|
||||
<Navbar />
|
||||
<main class="mx-6 my-4">
|
||||
<main class="mx-6 my-4 space-y-4">
|
||||
{
|
||||
showTitle && pageEnabled && (
|
||||
<PageGroup>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
---
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
import BaseLayout from "@layouts/BaseLayout.astro";
|
||||
|
||||
import H2 from "@components/H2.astro";
|
||||
@@ -25,12 +27,12 @@ const timeline: timelineEntry[] = [
|
||||
{
|
||||
event: "High School Diploma",
|
||||
eventDetail: "Friday Harbor High School",
|
||||
date: "June 2011",
|
||||
date: DateTime.fromISO("2011-06-15"),
|
||||
},
|
||||
{
|
||||
event: "B.S. Computer Science",
|
||||
eventDetail: "Oregon State University",
|
||||
date: "June 2019",
|
||||
date: DateTime.fromISO("2019-06-15"),
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
import type { timelineEntry } from "@interfaces/timeline.ts";
|
||||
|
||||
export const deploymentTimeline: timelineEntry[] = [
|
||||
{
|
||||
event: "Setup & Ocean Trials",
|
||||
eventDetail: "Petersburg, AK",
|
||||
date: "April 2017",
|
||||
date: DateTime.fromISO("2017-04-01"),
|
||||
},
|
||||
{
|
||||
event: "Glacier Deployment #1",
|
||||
eventDetail: "LeConte Glacier, AK",
|
||||
date: "May 2017",
|
||||
date: DateTime.fromISO("2017-05-01"),
|
||||
},
|
||||
{
|
||||
event: "Glacier Deployment #2",
|
||||
eventDetail: "LeConte Glacier, AK",
|
||||
date: "September 2017",
|
||||
date: DateTime.fromISO("2017-09-01"),
|
||||
},
|
||||
{
|
||||
event: "Scientific Paper Published",
|
||||
eventDetail: "The Oceanographic Society",
|
||||
date: "September 2017",
|
||||
date: DateTime.fromISO("2017-09-02"),
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ import publication from "@assets/experience/osu-ceoas-ocean-mixing-group/robotic
|
||||
import ross_team from "@assets/experience/osu-ceoas-ocean-mixing-group/robotic-oceanographic-surface-sampler/ross-team.jpg";
|
||||
import ui from "@assets/experience/osu-ceoas-ocean-mixing-group/robotic-oceanographic-surface-sampler/ui.jpg";
|
||||
|
||||
import { DateTime } from "luxon";
|
||||
import {
|
||||
deploymentTimeline,
|
||||
subTitles,
|
||||
@@ -53,13 +54,13 @@ const timeline: timelineEntry[] = [
|
||||
{
|
||||
event: "Started",
|
||||
eventDetail: "Joined ROSS",
|
||||
date: "April 2016",
|
||||
date: DateTime.fromISO("2016-04-01"),
|
||||
},
|
||||
...deploymentTimeline,
|
||||
{
|
||||
event: "Finished",
|
||||
eventDetail: "Left ROSS",
|
||||
date: "May 2018",
|
||||
date: DateTime.fromISO("2018-05-01"),
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -1,26 +1,44 @@
|
||||
---
|
||||
import ExperienceLayout from "@layouts/ExperienceLayout.astro";
|
||||
|
||||
import H2 from "@components/H2.astro";
|
||||
import Carousel from "@components/Media/CustomCarousel/CustomCarousel.astro";
|
||||
import YtVideo from "@components/Media/YtVideo.astro";
|
||||
import PageGroup from "@components/PageGroup.astro";
|
||||
|
||||
import type { carouselGroup } from "@interfaces/image-carousel.ts";
|
||||
import type { videoConfig } from "@interfaces/yt-video.ts";
|
||||
import ExperienceLayout from "@layouts/ExperienceLayout.astro";
|
||||
import type { videoConfig } from "@interfaces/video.ts";
|
||||
|
||||
import { roverSubTitles } from "./osu-robotics-club.ts";
|
||||
|
||||
import H3 from "@components/H3.astro";
|
||||
import Li from "@components/Li.astro";
|
||||
|
||||
import Video from "@components/Media/Video.astro";
|
||||
import Paragraph from "@components/Paragraph.astro";
|
||||
import Paragraphs from "@components/Paragraphs.astro";
|
||||
import SkillMatrix from "@components/SkillMatrix/SkillMatrix.astro";
|
||||
import Timeline from "@components/Timeline/Timeline.astro";
|
||||
import Ul from "@components/Ul.astro";
|
||||
import type { categorySkills } from "@interfaces/skill-matrix.ts";
|
||||
import type { timelineEntry } from "@interfaces/timeline.ts";
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
import circ_champions from "@assets/experience/osu-robotics-club/mars-rover-software-lead/circ-champions.jpg";
|
||||
import corwin_at_competition from "@assets/experience/osu-robotics-club/mars-rover-software-lead/corwin-at-competition.jpg";
|
||||
import drumheller_team_photo from "@assets/experience/osu-robotics-club/mars-rover-software-lead/drumheller-team-photo.jpg";
|
||||
import final_ground_station_gui from "@assets/experience/osu-robotics-club/mars-rover-software-lead/final-ground-station-gui.png";
|
||||
import ground_station_at_competition from "@assets/experience/osu-robotics-club/mars-rover-software-lead/ground-station-at-competition.jpg";
|
||||
import iris_pcb_assembly_timelapse_converted from "@assets/experience/osu-robotics-club/mars-rover-software-lead/iris-pcb-assembly-timelapse-converted.mp4";
|
||||
import iris_pcb_assembly_timelapse_converted from "@assets/experience/osu-robotics-club/mars-rover-software-lead/iris-pcb-assembly-timelapse.mp4";
|
||||
import iris_pcb_working from "@assets/experience/osu-robotics-club/mars-rover-software-lead/iris-pcb-working.jpg";
|
||||
import rover_at_competition_from_above from "@assets/experience/osu-robotics-club/mars-rover-software-lead/rover-at-competition-from-above.jpg";
|
||||
import rover_at_competition_pickup_test from "@assets/experience/osu-robotics-club/mars-rover-software-lead/rover-at-competition-pickup-test.jpg";
|
||||
import rover_gimbal_test_converted from "@assets/experience/osu-robotics-club/mars-rover-software-lead/rover-gimbal-test-converted.mp4";
|
||||
import rover_gimbal_test_converted from "@assets/experience/osu-robotics-club/mars-rover-software-lead/rover-gimbal-test.mp4";
|
||||
import rover_pose_with_dinosaur from "@assets/experience/osu-robotics-club/mars-rover-software-lead/rover-pose-with-dinosaur.jpg";
|
||||
import rover_with_arm_pose_in_desert from "@assets/experience/osu-robotics-club/mars-rover-software-lead/rover-with-arm-pose-in-desert.jpg";
|
||||
import senior_design_fair from "@assets/experience/osu-robotics-club/mars-rover-software-lead/senior-design-fair.jpg";
|
||||
import silly_poke from "@assets/experience/osu-robotics-club/mars-rover-software-lead/silly-poke.gif";
|
||||
import Video from "@components/Media/Video.astro";
|
||||
import LinkButton from "@components/LinkButton.astro";
|
||||
|
||||
const headerCarouselGroup: carouselGroup = {
|
||||
animation: "slide",
|
||||
@@ -32,6 +50,7 @@ const headerCarouselGroup: carouselGroup = {
|
||||
corwin_at_competition,
|
||||
rover_at_competition_from_above,
|
||||
rover_at_competition_pickup_test,
|
||||
senior_design_fair,
|
||||
final_ground_station_gui,
|
||||
ground_station_at_competition,
|
||||
iris_pcb_working,
|
||||
@@ -39,17 +58,254 @@ const headerCarouselGroup: carouselGroup = {
|
||||
],
|
||||
};
|
||||
|
||||
const timeline: timelineEntry[] = [
|
||||
{
|
||||
event: "Project Started",
|
||||
eventDetail: "Design Work Begins",
|
||||
date: DateTime.fromISO("2017-09-01"),
|
||||
},
|
||||
{
|
||||
event: "Senior Design Presentation",
|
||||
eventDetail: "Senior Design Fair",
|
||||
date: DateTime.fromISO("2018-05-18"),
|
||||
},
|
||||
{
|
||||
event: "Won 1st Place!",
|
||||
eventDetail: "CIRC",
|
||||
date: DateTime.fromISO("2018-08-13"),
|
||||
},
|
||||
{
|
||||
event: "Project Finished",
|
||||
eventDetail: "Final Documentation Complete",
|
||||
date: DateTime.fromISO("2018-08-31"),
|
||||
},
|
||||
];
|
||||
|
||||
const categorizedSkills: categorySkills[] = [
|
||||
{
|
||||
category: "Software & Environments",
|
||||
skills: [
|
||||
{
|
||||
item: "Version Control",
|
||||
subItems: [{ item: "Git" }],
|
||||
},
|
||||
{
|
||||
item: "Programming",
|
||||
subItems: [
|
||||
{
|
||||
item: "Languages",
|
||||
subItems: [
|
||||
{ item: "Python 2" },
|
||||
{ item: "C++" },
|
||||
{ item: "Bash Shell Scripting" },
|
||||
{ item: "High-Level Embedded C/C++ (Arduino/Teensy)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
item: "Frameworks",
|
||||
subItems: [{ item: "OpenCV" }, { item: "Qt" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
item: "Operating Systems",
|
||||
subItems: [
|
||||
{
|
||||
item: "Linux",
|
||||
subItems: [{ item: "Ubuntu" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "Electrical",
|
||||
skills: [
|
||||
{
|
||||
item: "Schematic & PCB Design",
|
||||
subItems: [
|
||||
{
|
||||
item: "Software",
|
||||
subItems: [{ item: "Altium Designer" }],
|
||||
},
|
||||
{
|
||||
item: "PCB Assembly & Rework",
|
||||
subItems: [
|
||||
{ item: "Handheld Soldering" },
|
||||
{ item: "Handheld Hot-Air Reflow" },
|
||||
{ item: "Oven Reflow" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
item: "Electrical Diagnostics",
|
||||
subItems: [
|
||||
{ item: "Multimeters" },
|
||||
{ item: "Electronic Loads" },
|
||||
{ item: "Oscilloscopes" },
|
||||
{ item: "Logic Analyzers" },
|
||||
],
|
||||
},
|
||||
{
|
||||
item: "Harnessing Fabrication",
|
||||
subItems: [{ item: "DC Power & Signal" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const videos: videoConfig[] = [
|
||||
{ videoPath: iris_pcb_assembly_timelapse_converted },
|
||||
{ videoPath: rover_gimbal_test_converted },
|
||||
{ videoPath: "https://www.youtube-nocookie.com/embed/ZjGW-HWapVA" },
|
||||
{ videoPath: "https://www.youtube-nocookie.com/embed/sceA2ZbEV8Y0" },
|
||||
{ videoPath: "https://www.youtube-nocookie.com/embed/sceA2ZbEV8Y" },
|
||||
];
|
||||
---
|
||||
|
||||
<ExperienceLayout title="OSURC - Software Team Lead">
|
||||
<ExperienceLayout title="Software Team Lead" subTitles={roverSubTitles}>
|
||||
<Carousel carouselGroup={headerCarouselGroup} />
|
||||
|
||||
<div class="grid grid-flow-row place-content-center gap-4 md:grid-flow-col">
|
||||
<LinkButton
|
||||
href="https://github.com/OSURoboticsClub/Rover_2017_2018"
|
||||
title="Rover and Ground Station Code"
|
||||
/>
|
||||
</div>
|
||||
<PageGroup>
|
||||
<Fragment slot="header"><H2>Summary</H2></Fragment>
|
||||
<PageGroup>
|
||||
<Fragment slot="header"><H3>Timeline</H3></Fragment>
|
||||
<Timeline timeline={timeline} />
|
||||
</PageGroup>
|
||||
<PageGroup>
|
||||
<Fragment slot="header"><H3>Key Takeaways</H3></Fragment>
|
||||
<Ul>
|
||||
<Li>Won 1st place in an international competition</Li>
|
||||
<Li
|
||||
>Wrote software in Python and C++ for the Rover's onboard computer</Li
|
||||
>
|
||||
<Li
|
||||
>Wrote firmware in embedded C/C++ for the Rover's distributed
|
||||
microcontroller sub-systems</Li
|
||||
>
|
||||
<Li
|
||||
>Hand-assembled most of the custom PCBs for the Rover's distributed
|
||||
microcontroller sub-systems</Li
|
||||
>
|
||||
<Li>Hand-fabricated much of the custom harnessing on the Rover</Li>
|
||||
<Li>Piloted the Rover during some competition events</Li>
|
||||
</Ul>
|
||||
</PageGroup>
|
||||
<SkillMatrix categorizedSkills={categorizedSkills} />
|
||||
</PageGroup>
|
||||
<PageGroup>
|
||||
<Fragment slot="header"><H2>Details</H2></Fragment>
|
||||
<PageGroup>
|
||||
<Fragment slot="header"><H3>My Experience</H3></Fragment>
|
||||
<Paragraphs>
|
||||
<Paragraph>
|
||||
I had not originally planned to be software lead for this Rover year,
|
||||
as I was already working two part time jobs and going to school
|
||||
full-time, but after my best friend Nick decided to become team and
|
||||
electrical lead, and my other best friend Dylan would create the
|
||||
rover's arm for his senior design project, I just couldn't say no.
|
||||
This also came hot off the heels of me being the emergency software
|
||||
lead for Rover the previous year, writing enough code in the nine days
|
||||
before competition to at least give them a fighting chance, which they
|
||||
had, so I was even more prepped and ready. I only had one hard
|
||||
requirement, joining as the software lead, and it was that we would
|
||||
have to create a serious ground station setup. When I first joined the
|
||||
robotics club in 2011, I did so because I was enraptured by the rover,
|
||||
but especially its ground station, which was a full-screen display
|
||||
full of stats, indicators, and buttons that got the nerd in me
|
||||
excited. If I was going to go all-in, I was going to do the same, and
|
||||
ended up making the ground station software my university senior
|
||||
design project.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
The upcoming year of work turned out to be some of the most intense I
|
||||
ever had at OSU, mostly self-inflicted due to not wanting to let this
|
||||
opportunity get wasted. It's worth noting, that not only was I writing
|
||||
the ground station software, but also the software running on the
|
||||
rover's onboard Jetson TX2 computer, all the firmware for each of the
|
||||
many distributed systems, on top of also hand-assembling/debugging
|
||||
most of the rover's PCBs, and creating many of its electrical wiring
|
||||
harnesses. In total, I put in over 2000 hours of work on this project,
|
||||
while working two part-time jobs and taking classes. It was a lot,
|
||||
but, it also turned out to be my proudest achievement while at OSU, so
|
||||
I'd argue it was worth it!
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
After these countless hours, what resulted was some incredibly robust
|
||||
hardware and software, which didn't crash, and which allowed us to
|
||||
absolutely crush the competition at the Canadian International Rover
|
||||
Challenge in Drumheller, Alberta, Canada. We won first place, with a
|
||||
final score of 287 points. The second best team got 159, so it wasn't
|
||||
even close! On top of doing that well, the software was also feature
|
||||
complete by halfway through the competition, which was no small feat
|
||||
considering the amount of scope creep a project like this tends to
|
||||
encounter. I was particularly proud of the ground station by this
|
||||
point as well. Not only was it a treasure trove of information about
|
||||
the Rover's state, but included live GPS mapping, multiple switchable
|
||||
camera views with pan/tilt capabilities and quality-switching,
|
||||
artificial speed limiting, and automations to automatically complete
|
||||
some of the tasks from the competition with just a few button presses.
|
||||
Considering the robotics team hadn't placed over 3rd since before I'd
|
||||
started in 2011, finally getting a first place win was a breath of
|
||||
fresh air, and made it much more cathartic when graduating the
|
||||
following year.
|
||||
</Paragraph>
|
||||
</Paragraphs>
|
||||
</PageGroup>
|
||||
<PageGroup>
|
||||
<Fragment slot="header"><H3>Technical Overview</H3></Fragment>
|
||||
<Paragraphs>
|
||||
<Paragraph>
|
||||
The ground station itself contained an Intel NUC with a Core i5, 16GB
|
||||
ram, nvme ssd, and was connected to two 23″ 1080p monitors. I chose
|
||||
two so we would be able to see all primary Rover systems without
|
||||
having to swap between tabs or pages, which I’d had to do on some
|
||||
other software I’d written and knew wouldn’t be efficient when we were
|
||||
in the middle of a timed event. The desktop ran Ubuntu 16.04 with the
|
||||
ROS2 stack. For the UI, PyQt was used, allowing for easy tweaking
|
||||
using QtCreator, and which provided access to QtSignals, a feature
|
||||
that made simple cross-thread communication much easier in Python. The
|
||||
Python code itself was Python 2, which was a limitation of the ROS2
|
||||
stack. Control of the rover was done via USB xbox controllers, plus a
|
||||
keyboard and mouse.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
The Rover itself ran on an NVidia Jetson TX2 single-board computer
|
||||
powered from a 500Wh li-on battery. The electronics box also contained
|
||||
a fuse box, power cutoff, network switch, battery backup, composite to
|
||||
network video converter, usb hub, FrSky controller receiver, and
|
||||
custom usb->RS485 adapter board. The TX2 ran Ubuntu 16.04 with the
|
||||
ROS2 stack, just like the ground station. All remote control boards on
|
||||
the Rover were communicated to using the modbus protocol over RS485.
|
||||
Interfacing with the arm was achieved via RS485 as well, but using the
|
||||
custom simplemotion control library provided by the motor controller
|
||||
company. Using linux UDEV rules, usb->serial and cameras were
|
||||
symlinked to custom, repeatable names at /dev/rover/name so that
|
||||
devices could be accessed identically after reboots. Custom ROS nodes
|
||||
were written for each system to be controlled, and the whole Rover ROS
|
||||
package set to start automatically at boot. The Rover could be
|
||||
controlled with an FrSky X9D controller whether the ground station was
|
||||
running or not. The controller could also override all other control
|
||||
inputs on-the-fly by flipping a switch on the controller. This was a
|
||||
safety feature to ensure runaway ground station or autonomy code
|
||||
wouldn’t allow the Rover to take off on its own. Conversely, the Rover
|
||||
would also work without the controller powered on, but would
|
||||
immediately allow for this override if turned on at any time.
|
||||
Communication with the ground station was done using two Ubiquiti
|
||||
Rocket M2 long-range wifi radios, along with their 10dB
|
||||
omnidirectional antennas. This simple setup allowed for a transparent
|
||||
ethernet link to be made between the two systems, allowing for useful
|
||||
features such as remote debugging and code upload. The radios were
|
||||
tested to work at roughly a kilometer away, when mostly line-of-sight.
|
||||
</Paragraph>
|
||||
</Paragraphs>
|
||||
</PageGroup>
|
||||
</PageGroup>
|
||||
<PageGroup>
|
||||
<Fragment slot="header"><H2>Videos</H2></Fragment>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export const subTitles = ["Oregon State University", "OSU Robotics Club"];
|
||||
|
||||
export const roverSubTitles = [...subTitles, "Mars Rover Team"];
|
||||
@@ -0,0 +1,56 @@
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
import type { printedCircuitBoard } from "@interfaces/printed-circuit-board.ts";
|
||||
|
||||
import pcb_1_1_0_assembled_bottom_angle from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/pcbs/1-1-0/assembled-bottom-angle.jpg";
|
||||
import pcb_1_1_0_assembled_bottom from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/pcbs/1-1-0/assembled-bottom.jpg";
|
||||
import pcb_1_1_0_assembled_side from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/pcbs/1-1-0/assembled-side.jpg";
|
||||
import pcb_1_1_0_bare_bottom from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/pcbs/1-1-0/bare-bottom.png";
|
||||
import pcb_1_1_0_bare_top from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/pcbs/1-1-0/bare-top.png";
|
||||
import assembled_main_bottom from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/pcbs/3-0-0/assembled-main-bottom.jpg";
|
||||
import assembled_side from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/pcbs/3-0-0/assembled-side.jpg";
|
||||
import bare_main_bottom from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/pcbs/3-0-0/bare-main-bottom.jpg";
|
||||
import bare_main_top from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/pcbs/3-0-0/bare-main-top.jpg";
|
||||
import bare_surround_bottom from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/pcbs/3-0-0/bare-surround-bottom.jpg";
|
||||
import bare_surround_top from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/pcbs/3-0-0/bare-surround-top.jpg";
|
||||
import half_assembled_main_top_surround_bottom_lcd from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/pcbs/3-0-0/half-assembled-main-top-surround-bottom-lcd.jpg";
|
||||
import half_assembled_main_top_surround_bottom from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/pcbs/3-0-0/half-assembled-main-top-surround-bottom.jpg";
|
||||
|
||||
export const dechorionatorPcb: printedCircuitBoard = {
|
||||
name: "Dechorionator",
|
||||
description:
|
||||
"Control board which provides motion and water flow control, along with user control and monitoring",
|
||||
|
||||
revisions: [
|
||||
{
|
||||
major: 3,
|
||||
minor: 0,
|
||||
patch: 0,
|
||||
date: DateTime.fromISO("2015-08-30"),
|
||||
images: [
|
||||
half_assembled_main_top_surround_bottom,
|
||||
half_assembled_main_top_surround_bottom_lcd,
|
||||
assembled_main_bottom,
|
||||
assembled_side,
|
||||
bare_main_top,
|
||||
bare_main_bottom,
|
||||
bare_surround_top,
|
||||
bare_surround_bottom,
|
||||
],
|
||||
},
|
||||
{
|
||||
major: 1,
|
||||
minor: 1,
|
||||
patch: 0,
|
||||
date: DateTime.fromISO("2014-05-10"),
|
||||
images: [
|
||||
pcb_1_1_0_assembled_bottom_angle,
|
||||
pcb_1_1_0_assembled_bottom,
|
||||
pcb_1_1_0_assembled_side,
|
||||
pcb_1_1_0_bare_top,
|
||||
pcb_1_1_0_bare_bottom,
|
||||
],
|
||||
// notes: [{ item: "Effective first functional revision" }],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,5 +1,246 @@
|
||||
---
|
||||
import ExperienceLayout from "@layouts/ExperienceLayout.astro";
|
||||
|
||||
import H2 from "@components/H2.astro";
|
||||
import H3 from "@components/H3.astro";
|
||||
import InlineLink from "@components/InlineLink.astro";
|
||||
import Li from "@components/Li.astro";
|
||||
import CustomCarousel from "@components/Media/CustomCarousel/CustomCarousel.astro";
|
||||
import PageGroup from "@components/PageGroup.astro";
|
||||
import Paragraph from "@components/Paragraph.astro";
|
||||
import Paragraphs from "@components/Paragraphs.astro";
|
||||
import PopoverWordDefinition from "@components/PopoverWordDefinition.astro";
|
||||
import PrintedCircuitBoard from "@components/PrintedCircuitBoard.astro";
|
||||
import SkillMatrix from "@components/SkillMatrix/SkillMatrix.astro";
|
||||
import Timeline from "@components/Timeline/Timeline.astro";
|
||||
import Ul from "@components/Ul.astro";
|
||||
|
||||
import type { carouselGroup } from "@interfaces/image-carousel.ts";
|
||||
import { timelineFromPrintedCircuitBoard } from "@interfaces/printed-circuit-board.ts";
|
||||
import type { categorySkills } from "@interfaces/skill-matrix.ts";
|
||||
import type { timelineEntry } from "@interfaces/timeline.ts";
|
||||
|
||||
import { dechorionatorPcb } from "./dechorionator-pcbs.ts";
|
||||
import {
|
||||
subTitles,
|
||||
workingTimeline,
|
||||
} from "./osu-sinnhuber-aquatic-research-laboratory.ts";
|
||||
|
||||
import assembly_internal_overview from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/assembly-internal-overview.jpg";
|
||||
import assembly_pcb_connected from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/assembly-pcb-connected.jpg";
|
||||
import assembly_pcb_front_on from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/assembly-pcb-front-on.jpg";
|
||||
import assembly_pcb_front from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/assembly-pcb-front.jpg";
|
||||
import assembly_pcb_rear from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/assembly-pcb-rear.jpg";
|
||||
import assembly_surround_front from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/assembly-surround-front.jpg";
|
||||
import assembly_surround_rear from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/assembly-surround-rear.jpg";
|
||||
import off_front_lid_open from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/off-front-lid-open.jpg";
|
||||
import off_front from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/off-front.jpg";
|
||||
import on_front_closeup from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/on-front-closeup.jpg";
|
||||
import on_front from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/on-front.jpg";
|
||||
import rear_water_manifold from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/rear-water-manifold.jpg";
|
||||
import rear from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/rear.jpg";
|
||||
import top_drain from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/top-drain.jpg";
|
||||
import top_holder_closeup from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/top-holder-closeup.jpg";
|
||||
import top_lid_open from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/top-lid-open.jpg";
|
||||
import top_showerhead from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/top-showerhead.jpg";
|
||||
import travel_setup from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/dechorionator/travel-setup.jpg";
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
const headerCarouselGroup: carouselGroup = {
|
||||
animation: "slide",
|
||||
images: [
|
||||
off_front,
|
||||
off_front_lid_open,
|
||||
on_front,
|
||||
on_front_closeup,
|
||||
top_lid_open,
|
||||
top_showerhead,
|
||||
top_drain,
|
||||
top_holder_closeup,
|
||||
rear,
|
||||
rear_water_manifold,
|
||||
travel_setup,
|
||||
|
||||
assembly_surround_front,
|
||||
assembly_surround_rear,
|
||||
assembly_pcb_rear,
|
||||
assembly_pcb_front,
|
||||
assembly_pcb_front_on,
|
||||
assembly_pcb_connected,
|
||||
assembly_internal_overview,
|
||||
],
|
||||
};
|
||||
|
||||
const timeline: timelineEntry[] = [
|
||||
...workingTimeline,
|
||||
...timelineFromPrintedCircuitBoard(dechorionatorPcb),
|
||||
{
|
||||
event: "Project Started",
|
||||
eventDetail: "Initial Requirements Given",
|
||||
date: DateTime.fromISO("2014-05-01"),
|
||||
},
|
||||
{
|
||||
event: "Project Finished",
|
||||
eventDetail: "Delivered Units to Lab",
|
||||
date: DateTime.fromISO("2016-09-01"),
|
||||
},
|
||||
];
|
||||
|
||||
const categorizedSkills: categorySkills[] = [
|
||||
{
|
||||
category: "Electrical",
|
||||
skills: [
|
||||
{
|
||||
item: "Schematic & PCB Design",
|
||||
subItems: [
|
||||
{ item: "Mentor Graphics PADS" },
|
||||
{ item: "Altium Designer" },
|
||||
],
|
||||
},
|
||||
{
|
||||
item: "PCB Assembly & Rework",
|
||||
subItems: [
|
||||
{ item: "Handheld Soldering" },
|
||||
{ item: "Handheld Hot-Air Reflow" },
|
||||
{ item: "Oven Reflow" },
|
||||
],
|
||||
},
|
||||
{
|
||||
item: "Electrical Diagnostics",
|
||||
subItems: [{ item: "Multimeters" }, { item: "Oscilloscopes" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "Software & Environments",
|
||||
skills: [
|
||||
{ item: "Git" },
|
||||
{
|
||||
item: "Programming",
|
||||
subItems: [{ item: "Low-Level Embedded C/C++ (Atmel Studio)" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
---
|
||||
|
||||
<ExperienceLayout title="SARL - Dechorionator" />
|
||||
<ExperienceLayout title="Dechorionator" subTitles={subTitles}>
|
||||
<CustomCarousel carouselGroup={headerCarouselGroup} />
|
||||
<PageGroup>
|
||||
<Fragment slot="header"><H2>Summary</H2></Fragment>
|
||||
<PageGroup>
|
||||
<Fragment slot="header"><H3>Timeline</H3></Fragment>
|
||||
<Timeline timeline={timeline} />
|
||||
</PageGroup>
|
||||
<PageGroup>
|
||||
<Fragment slot="header"><H3>Key Takeaways</H3></Fragment>
|
||||
<Ul>
|
||||
<Li
|
||||
>Created an all-in-one tool for removing the chorions of zebrafish
|
||||
embryos in a controlled and repeatable manner</Li
|
||||
>
|
||||
<Li
|
||||
>Developed custom PCBs to handle motion, pump control, and user
|
||||
interaction</Li
|
||||
>
|
||||
<Li
|
||||
>Deployed multiple units to the lab, and one to an east-coast partner
|
||||
laboratory</Li
|
||||
>
|
||||
<Li
|
||||
>Cost reduced to roughly 1/5 that of the lab's previous dechorionation
|
||||
hardware</Li
|
||||
>
|
||||
</Ul>
|
||||
</PageGroup>
|
||||
<SkillMatrix categorizedSkills={categorizedSkills} />
|
||||
</PageGroup>
|
||||
<PageGroup>
|
||||
<Fragment slot="header"><H2>Details</H2></Fragment>
|
||||
<Paragraphs>
|
||||
<Paragraph>
|
||||
Before delving into what was built, some quick context is probably
|
||||
needed. A dechorionator is a device that removes <InlineLink
|
||||
href="https://en.wikipedia.org/wiki/Chorion">chorions</InlineLink
|
||||
> from embryos. Chorions are the outer membranes of an embryo which provide
|
||||
protection, and a permeable membrane which can allow gasses and nutrients
|
||||
to reach the developing animal inside. As SARL is a toxicology lab, and its
|
||||
experiments need to be deterministic, this protective layer can drastically
|
||||
skew tests results, and even worse, can can variances embryo to embryo, or
|
||||
egg batch to egg batch. To remove this, a special protein is added to a petri-dish
|
||||
full of embryos, and then the dish is gently swirled with jerking start and
|
||||
stop motions. The goal is to provide light agitation between the embryo, the
|
||||
dish, and their neighbors, helping the protein eat away the chorion and sluff
|
||||
off into the dish. This can, and has been done by hand, but when I joined
|
||||
SARL they already had two machines which which could automatically perform
|
||||
this task. However, they were incredibly expensive and massively overcomplicated,
|
||||
requiring a whole table's worth of custom shaker units, networked peristaltic
|
||||
pumps, and servos. The engineering team was tasked with simplifying this setup
|
||||
while reducing both their size and cost.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
We started with a <PopoverWordDefinition key="COTS" /> shaker unit from the
|
||||
company ELMI, which had a stepper-motor-based drive system, making it a perfect
|
||||
candidate for easy retrofit. After gutting the existing electronics, and taking
|
||||
some measurements, I started on a custom PCB design. Basic requirements were
|
||||
that the board needed to be able to control the stepper motor, control the
|
||||
speed of a liquid pump, provide controls to users, allow for config editing
|
||||
from those controls, and provide a screen for cycle progress and editing those
|
||||
config values. Since this was one of the first PCBs I'd ever designed and
|
||||
hand-assembled, I started with a basic proof-of-concept which was for bench
|
||||
use only (revision: 1.1.0). While I worked on the electronics, my co-worker
|
||||
and good friend <InlineLink href="https://dylanthrush.com"
|
||||
>Dylan Thrush</InlineLink
|
||||
> was busy designing a top-plate for the shaker to hold the dishes, shower
|
||||
them with water, and drain the pumped-in liquid.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
First tests showed that the overall concept was going to work, just
|
||||
needing signal conditioning for the rotary encoder to avoid ghosted or
|
||||
missing inputs. A larger problem we found was that the brushed-dc-motor
|
||||
driven peristaltic pump was not going to be able to supply the flowrate
|
||||
needed to properly shower the four dishes. We'd already chosen one of
|
||||
the highest-flowrate pumps which could fit inside the shaker housing,
|
||||
and ended up having to pivot to a much more expensive one from TCS
|
||||
Micropumps. Luckily, not only did it solve our flowrate problem, but
|
||||
also held up much better to the saltwater solution being pumped through
|
||||
it than our initial choice.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
With the proof-of-concept design functional, I began a redesign of the
|
||||
control PCB to replace the existing control panel and drive circuitry
|
||||
from the ELMI shaker (revision: 3.0.0). The existing control circuitry
|
||||
had a unique assembly design that I'd not encountered before, using a
|
||||
PCB with solderable standoffs as the front panel, and soldered copper
|
||||
strips as retention tabs. I was so fascinated by the design that I
|
||||
decided to emulate it. Check out the images at the end of the reel above
|
||||
to see how this unique assembly was put together! Around the time that
|
||||
the PCBs were ready, we'd hired a new engineer, <InlineLink
|
||||
href="https://www.linkedin.com/in/aaron-rito-2b754777/"
|
||||
>Aaron Rito</InlineLink
|
||||
>, who I tasked with writing the firmware while providing input and
|
||||
guidance.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Over the next few months, many revisions were made to the firmware, as
|
||||
well as mechanical designs for the showerheads and water manifold. Dylan
|
||||
also had a final design for the top-plate milled out that looked
|
||||
beautiful. Once those changes were complete, we provided the prototype
|
||||
unit to the researchers, along with documentation on how to use the
|
||||
tuning values. They then spent a few weeks running the new dechorionator
|
||||
alongside the old ones, while tweaking these parameters until the
|
||||
performance matched. We then built up four more units, and pre-flashed
|
||||
them with this configuration. Three of these went into the lab, where a
|
||||
total of four of our new dechorionators sat on the same table where just
|
||||
two prior-generation ones used to live. It even had additional space for
|
||||
pre and post prep work on the petri-dishes! The last one I installed in
|
||||
a partner lab on the east coast after flying there with the head of the
|
||||
lab, Robyn Tanguay and deputy director, Lisa Truong.
|
||||
</Paragraph>
|
||||
</Paragraphs>
|
||||
</PageGroup>
|
||||
<PageGroup>
|
||||
<Fragment slot="header"><H2>Printed Circuit Boards</H2></Fragment>
|
||||
<PrintedCircuitBoard pcb={dechorionatorPcb} />
|
||||
</PageGroup>
|
||||
</ExperienceLayout>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
import type { timelineEntry } from "@interfaces/timeline.ts";
|
||||
|
||||
export const subTitles = [
|
||||
"Oregon State University",
|
||||
"College of Agricultural Sciences",
|
||||
"Department of Environmental and Molecular Toxicology",
|
||||
"Sinnhuber Aquatic Research Laboratory",
|
||||
];
|
||||
|
||||
export const workingTimeline: timelineEntry[] = [
|
||||
{
|
||||
event: "Started",
|
||||
eventDetail: "Joined SARL Engineering",
|
||||
date: DateTime.fromISO("2013-09-01"),
|
||||
},
|
||||
{
|
||||
event: "Finished",
|
||||
eventDetail: "Left SARL Engineering",
|
||||
date: DateTime.fromISO("2019-08-01"),
|
||||
},
|
||||
];
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
import ExperienceLayout from "@layouts/ExperienceLayout.astro";
|
||||
---
|
||||
|
||||
<ExperienceLayout title="SARL - Team Lead" />
|
||||
@@ -0,0 +1,91 @@
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
import type { printedCircuitBoard } from "@interfaces/printed-circuit-board.ts";
|
||||
|
||||
import control_bottom_1_0_0 from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/zebrafish-embryo-pick-and-plate/pcbs/control/1-0-0/bottom.jpg";
|
||||
import control_top_1_0_0 from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/zebrafish-embryo-pick-and-plate/pcbs/control/1-0-0/top.jpg";
|
||||
import control_assembly_bottom_2_0_0 from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/zebrafish-embryo-pick-and-plate/pcbs/control/2-0-0/assembly-bottom.jpg";
|
||||
import control_assembly_top_beaglebone_2_0_0 from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/zebrafish-embryo-pick-and-plate/pcbs/control/2-0-0/assembly-top-beaglebone.jpg";
|
||||
import control_assembly_top_2_0_0 from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/zebrafish-embryo-pick-and-plate/pcbs/control/2-0-0/assembly-top.jpg";
|
||||
import control_bottom_2_0_0 from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/zebrafish-embryo-pick-and-plate/pcbs/control/2-0-0/bottom.png";
|
||||
import control_top_2_0_0 from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/zebrafish-embryo-pick-and-plate/pcbs/control/2-0-0/top.png";
|
||||
import lights_assembly_bottom_1_0_0 from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/zebrafish-embryo-pick-and-plate/pcbs/lights/1-0-0/assembly-bottom.jpg";
|
||||
import lights_assembly_top_1_0_0 from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/zebrafish-embryo-pick-and-plate/pcbs/lights/1-0-0/assembly-top.jpg";
|
||||
import lights_assembly_bottom_2_0_0 from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/zebrafish-embryo-pick-and-plate/pcbs/lights/2-0-0/assembly-bottom.jpg";
|
||||
import lights_assembly_top_2_0_0 from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/zebrafish-embryo-pick-and-plate/pcbs/lights/2-0-0/assembly-top.jpg";
|
||||
import lights_top_panelized_2_0_0 from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/zebrafish-embryo-pick-and-plate/pcbs/lights/2-0-0/top-panelized.png";
|
||||
|
||||
export const pnpLights: printedCircuitBoard = {
|
||||
name: "Lighting Board",
|
||||
description:
|
||||
"Compact, bright, 24V lighting to provide high-contrast video for the pick and plate",
|
||||
|
||||
revisions: [
|
||||
{
|
||||
major: 2,
|
||||
minor: 0,
|
||||
patch: 0,
|
||||
date: DateTime.fromISO("2014-09-23"),
|
||||
images: [
|
||||
lights_top_panelized_2_0_0,
|
||||
lights_assembly_top_2_0_0,
|
||||
lights_assembly_bottom_2_0_0,
|
||||
],
|
||||
notes: [
|
||||
{
|
||||
item: "First panelized PCB design, with built-in test features",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
major: 1,
|
||||
minor: 0,
|
||||
patch: 0,
|
||||
date: DateTime.fromISO("2014-04-07"),
|
||||
images: [lights_assembly_top_1_0_0, lights_assembly_bottom_1_0_0],
|
||||
notes: [
|
||||
{
|
||||
item: "Worked, but couldn't remove heat efficiently long-term",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
export const pnpControl: printedCircuitBoard = {
|
||||
name: "Driver Board",
|
||||
description:
|
||||
"Motion controller, and single-board-computer interface, for the embryo pick and plate machine",
|
||||
|
||||
revisions: [
|
||||
{
|
||||
major: 2,
|
||||
minor: 0,
|
||||
patch: 0,
|
||||
date: DateTime.fromISO("2014-07-11"),
|
||||
images: [
|
||||
control_top_2_0_0,
|
||||
control_bottom_2_0_0,
|
||||
control_assembly_top_2_0_0,
|
||||
control_assembly_bottom_2_0_0,
|
||||
control_assembly_top_beaglebone_2_0_0,
|
||||
],
|
||||
notes: [
|
||||
{
|
||||
item: "Functional, but ultimately scrapped due to motion control complexity",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
major: 1,
|
||||
minor: 0,
|
||||
patch: 0,
|
||||
date: DateTime.fromISO("2013-12-02"),
|
||||
images: [control_top_1_0_0, control_bottom_1_0_0],
|
||||
notes: [
|
||||
{
|
||||
item: "First printed circuit board I ever designed, which was REALLY bad",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,5 +1,316 @@
|
||||
---
|
||||
import ExperienceLayout from "@layouts/ExperienceLayout.astro";
|
||||
|
||||
import H2 from "@components/H2.astro";
|
||||
import H3 from "@components/H3.astro";
|
||||
import Li from "@components/Li.astro";
|
||||
import CustomCarousel from "@components/Media/CustomCarousel/CustomCarousel.astro";
|
||||
import Video from "@components/Media/Video.astro";
|
||||
import PageGroup from "@components/PageGroup.astro";
|
||||
import Paragraph from "@components/Paragraph.astro";
|
||||
import Paragraphs from "@components/Paragraphs.astro";
|
||||
import PopoverWordDefinition from "@components/PopoverWordDefinition.astro";
|
||||
import PrintedCircuitBoard from "@components/PrintedCircuitBoard.astro";
|
||||
import SkillMatrix from "@components/SkillMatrix/SkillMatrix.astro";
|
||||
import Timeline from "@components/Timeline/Timeline.astro";
|
||||
import Ul from "@components/Ul.astro";
|
||||
|
||||
import type { carouselGroup } from "@interfaces/image-carousel.ts";
|
||||
import { timelineFromPrintedCircuitBoard } from "@interfaces/printed-circuit-board.ts";
|
||||
import type { categorySkills } from "@interfaces/skill-matrix.ts";
|
||||
import type { timelineEntry } from "@interfaces/timeline.ts";
|
||||
|
||||
import {
|
||||
subTitles,
|
||||
workingTimeline,
|
||||
} from "./osu-sinnhuber-aquatic-research-laboratory.ts";
|
||||
|
||||
import {
|
||||
pnpControl,
|
||||
pnpLights,
|
||||
} from "./zebrafish-embryo-pick-and-plate-pcbs.ts";
|
||||
|
||||
import installation_and_tuning from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/zebrafish-embryo-pick-and-plate/installation-and-tuning.jpg";
|
||||
import interface_tuning_and_detection from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/zebrafish-embryo-pick-and-plate/interface-tuning-and-detection.png";
|
||||
import off_controls_lid_overview from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/zebrafish-embryo-pick-and-plate/off-controls-lid-overview.jpg";
|
||||
import off_controls_syringe_overview from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/zebrafish-embryo-pick-and-plate/off-controls-syringe-overview.jpg";
|
||||
import off_controls_top_overview from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/zebrafish-embryo-pick-and-plate/off-controls-top-overview.jpg";
|
||||
import off_front_working_area_overview from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/zebrafish-embryo-pick-and-plate/off-front-working-area-overview.jpg";
|
||||
import off_front from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/zebrafish-embryo-pick-and-plate/off-front.jpg";
|
||||
import off_left from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/zebrafish-embryo-pick-and-plate/off-left.jpg";
|
||||
import off_lighting from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/zebrafish-embryo-pick-and-plate/off-lighting.jpg";
|
||||
import off_rear from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/zebrafish-embryo-pick-and-plate/off-rear.jpg";
|
||||
import off_right from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/zebrafish-embryo-pick-and-plate/off-right.jpg";
|
||||
import off_touchoff from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/zebrafish-embryo-pick-and-plate/off-touchoff.jpg";
|
||||
import on_dishes_lighting_reflection from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/zebrafish-embryo-pick-and-plate/on-dishes-lighting-reflection.jpg";
|
||||
import on_front_dishes_beakers from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/zebrafish-embryo-pick-and-plate/on-front-dishes-beakers.jpg";
|
||||
import on_front_overview from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/zebrafish-embryo-pick-and-plate/on-front-overview.jpg";
|
||||
import on_front from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/zebrafish-embryo-pick-and-plate/on-front.jpg";
|
||||
import on_screen from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/zebrafish-embryo-pick-and-plate/on-screen.jpg";
|
||||
import on_touchoff_block_isometric from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/zebrafish-embryo-pick-and-plate/on-touchoff-block-isometric.jpg";
|
||||
|
||||
import pick_and_placing from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/zebrafish-embryo-pick-and-plate/pick-and-placing.mp4";
|
||||
import precision_homing from "@assets/experience/osu-sinnhuber-aquatic-research-laboratory/zebrafish-embryo-pick-and-plate/precision-homing.mp4";
|
||||
import InlineLink from "@components/InlineLink.astro";
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
const headerCarouselGroup: carouselGroup = {
|
||||
animation: "slide",
|
||||
images: [
|
||||
installation_and_tuning,
|
||||
on_front_overview,
|
||||
interface_tuning_and_detection,
|
||||
on_front,
|
||||
on_front_dishes_beakers,
|
||||
on_screen,
|
||||
off_touchoff,
|
||||
on_touchoff_block_isometric,
|
||||
off_controls_top_overview,
|
||||
off_controls_syringe_overview,
|
||||
off_controls_lid_overview,
|
||||
off_front,
|
||||
off_front_working_area_overview,
|
||||
off_left,
|
||||
off_rear,
|
||||
off_right,
|
||||
off_lighting,
|
||||
on_dishes_lighting_reflection,
|
||||
],
|
||||
};
|
||||
|
||||
const timeline: timelineEntry[] = [
|
||||
...workingTimeline,
|
||||
...timelineFromPrintedCircuitBoard(pnpLights),
|
||||
...timelineFromPrintedCircuitBoard(pnpControl),
|
||||
{
|
||||
event: "Project Started",
|
||||
eventDetail: "Initial Requirements Given",
|
||||
date: DateTime.fromISO("2013-12-01"),
|
||||
},
|
||||
{
|
||||
event: "Project Finished",
|
||||
eventDetail: "Delivered Units to Lab",
|
||||
date: DateTime.fromISO("2016-09-01"),
|
||||
},
|
||||
];
|
||||
|
||||
const categorizedSkills: categorySkills[] = [
|
||||
{
|
||||
category: "Software & Environments",
|
||||
skills: [
|
||||
{
|
||||
item: "Version Control",
|
||||
subItems: [{ item: "Git" }],
|
||||
},
|
||||
{
|
||||
item: "Programming",
|
||||
subItems: [
|
||||
{
|
||||
item: "Languages",
|
||||
subItems: [
|
||||
{ item: "Python 2" },
|
||||
{ item: "Bash Shell Scripting" },
|
||||
{ item: "Low-Level Embedded C/C++ (Atmel Studio)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
item: "Frameworks",
|
||||
subItems: [{ item: "OpenCV" }, { item: "Qt" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
item: "Operating Systems",
|
||||
subItems: [
|
||||
{
|
||||
item: "Linux",
|
||||
subItems: [{ item: "Debian" }],
|
||||
},
|
||||
{ item: "Microsoft Windows" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "Electrical",
|
||||
skills: [
|
||||
{
|
||||
item: "Schematic & PCB Design",
|
||||
subItems: [
|
||||
{
|
||||
item: "Software",
|
||||
subItems: [
|
||||
{ item: "Altium Designer" },
|
||||
{ item: "Mentor Graphics PADS" },
|
||||
],
|
||||
},
|
||||
{
|
||||
item: "Manufacturing",
|
||||
subItems: [
|
||||
{ item: "Gerber Export" },
|
||||
{ item: "BOM Management" },
|
||||
{ item: "In-House Assembly" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
item: "Electrical Diagnostics",
|
||||
subItems: [
|
||||
{ item: "Multimeters" },
|
||||
{ item: "Electronic Loads" },
|
||||
{ item: "Oscilloscopes" },
|
||||
],
|
||||
},
|
||||
{
|
||||
item: "Harnessing Fabrication",
|
||||
subItems: [{ item: "DC Low-Power & Signal" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "Mechanical",
|
||||
skills: [
|
||||
{
|
||||
item: "Fabrication",
|
||||
subItems: [{ item: "CNC" }, { item: "Hand Tools" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const videos: string[] = [pick_and_placing, precision_homing];
|
||||
---
|
||||
|
||||
<ExperienceLayout title="SARL - Zebrafish Embryo Pick and Plate" />
|
||||
<ExperienceLayout title="Zebrafish Embryo Pick and Plate" subTitles={subTitles}>
|
||||
<CustomCarousel carouselGroup={headerCarouselGroup} />
|
||||
<PageGroup>
|
||||
<Fragment slot="header"><H2>Summary</H2></Fragment>
|
||||
<PageGroup>
|
||||
<Fragment slot="header"><H3>Timeline</H3></Fragment>
|
||||
<Timeline timeline={timeline} />
|
||||
</PageGroup>
|
||||
<PageGroup>
|
||||
<Fragment slot="header"><H3>Key Takeaways</H3></Fragment>
|
||||
<Ul>
|
||||
<Li
|
||||
>Delivered a design, and multiple built units, of a custom embryo
|
||||
pick-and-plate machine</Li
|
||||
>
|
||||
<Li>Reduced cost from ~$150,000 for previous generation to ~$10,000</Li>
|
||||
<Li
|
||||
>Reduced the size from 4.5'x4.5'x8' for previous generation to
|
||||
1'x1'x1.5'</Li
|
||||
>
|
||||
</Ul>
|
||||
</PageGroup>
|
||||
<SkillMatrix categorizedSkills={categorizedSkills} />
|
||||
</PageGroup>
|
||||
<PageGroup>
|
||||
<Fragment slot="header"><H2>Details</H2></Fragment>
|
||||
<Paragraphs>
|
||||
<Paragraph>
|
||||
For some quick context on why such a machine was even needed, the
|
||||
Sinnhuber Aquatic Research Lab performs toxicology research using
|
||||
Zebrafish. This means tha the lab is also a breeding facility,
|
||||
generating a few thousand eggs a day, which need to be processed and
|
||||
isolated prior to being introduced to experimental conditions. At one
|
||||
point in time, these processes were done by hand, with all researchers
|
||||
spending a significant portion of their day simply prepping the embryos.
|
||||
Later on, they hired a contractor to design an automated solution to the
|
||||
isolation problem, and while they delivered, the units were industrial
|
||||
overkill, using massive <PopoverWordDefinition key="SCARA" /> arms and enclosures
|
||||
to pick up embryos which were roughly half a millimeter in diameter. They
|
||||
were also very expensive, costing around $150k per machine, and the lab had
|
||||
four installed. Thus, the engineering team was tasked with cost and size reducing
|
||||
them so that more could fit in the same space and throughput could be higher.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
My coworker, <InlineLink href="https://dylanthrush.com"
|
||||
>Dylan Thrush</InlineLink
|
||||
>, and I got to work. He focussed on the mechanical side, while I worked
|
||||
on electrical and software. I had recently been learning PCB design
|
||||
after becoming more familiar with it through the OSU robotics club, and
|
||||
decided that a good place to start would be to create a motion
|
||||
controller. Dylan and I had already landed on a simple stepper-motor
|
||||
based system to more than meet the needs of a task like this, and both
|
||||
of us already had experience with stepper-based CNC machines, making it
|
||||
a great jumpoff point. My first PCB was unfortunately a massive failure,
|
||||
because while I did include a quad stepper motor driver,
|
||||
microcontroller, general purpose I/O, and usb to serial interface, I
|
||||
failed to export the gerbers correctly. This resulted in a PCB with no
|
||||
drills, making it completely useless (outside of a good learning
|
||||
experience)! It was also just a poor layout overall, which isn't
|
||||
surprising considering it was my first ever PCB design. If you want to
|
||||
see this embarrassing result, check out the PCB section at the end of
|
||||
this page!
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
For the second revision, we'd made some progress in other places, but
|
||||
most importantly had decided that we would use a <InlineLink
|
||||
href="https://www.beagleboard.org/boards/beaglebone-black"
|
||||
>Beaglebone Black</InlineLink
|
||||
> single-board-computer to run the whole system. In an effort to simplify
|
||||
the assembly, I decided that the next revision would include headers to directly
|
||||
mount the Beaglebone to the unit, providing power and a serial interface over
|
||||
those pins. This version actually worked as expected, with only a few very
|
||||
minor bodges to the microcontroller's crystal, and some bulk capacitance on
|
||||
the power input. Since things were working, I began writing embedded C to
|
||||
control motion through some higher-level interfaces. Not long into this process,
|
||||
I realized I might have bitten off more than I could chew. Not only was I
|
||||
having to learn the deep ins and outs of microcontroller programming, kinematics,
|
||||
and serial interfaces, but I would still have to greatly improve my Python
|
||||
skills, learn to create graphical user interfaces, and figure out how to detect
|
||||
embryos using a camera and computer vision. While I was sad to scrap this,
|
||||
we decided to fall back on a motion controller called <InlineLink
|
||||
href="https://synthetos.com/project/tinyg">TinyG</InlineLink
|
||||
>, which would simply require gcode to be sent over serial to function.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Dylan built up a mechanical testbed, which we could mount a camera to,
|
||||
and I began to focus on embryo detection. I was relatively new to
|
||||
Python, but quickly found the Qt framework for building decent-looking
|
||||
interfaces, and OpenCV for providing generic detection capabilities via
|
||||
webcams. Over the next few years (remember, we were students doing this
|
||||
part time, and working on other projects simultaneously), I eventually
|
||||
created a fairly comprehensive user interface that would allow
|
||||
researchers to tune detection and motion parameters on-the-fly. This was
|
||||
shown on a touchscreen that the beaglebone plugged into, making it quite
|
||||
intuitive. Dylan had also created a mechanical foundation providing
|
||||
repeatable alignment for a petri dish with embryos, the 96-well plate
|
||||
for placement, and a waste container to get rid of extra water in our
|
||||
metal pipette tip that would be used to pick up the embryos. I'd also
|
||||
created some very bright lighting boards to mount to the unit's
|
||||
extrusion, illuminating the embryos from the side, something that was
|
||||
absolutely required for consistent detection with the camera. After a
|
||||
long journey, we finally delivered multiple units to the lab! Seeing
|
||||
such small devices performing the same task next to the behemoths which
|
||||
were the prior versions was quite amusing. You could easily fit a two by
|
||||
two grid of these in the working area of each of those machines.
|
||||
Overall, this was a highly ambitious project, but it developed some of
|
||||
my most successful skills I gained while at OSU while accelerating the
|
||||
research at the lab!
|
||||
</Paragraph>
|
||||
</Paragraphs>
|
||||
</PageGroup>
|
||||
<PageGroup>
|
||||
<Fragment slot="header"><H2>Videos</H2></Fragment>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
{
|
||||
videos.map((video) => (
|
||||
<Video
|
||||
videoPath={video}
|
||||
autoPlay={true}
|
||||
loop={true}
|
||||
playsInline={true}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</PageGroup>
|
||||
<PageGroup>
|
||||
<Fragment slot="header"><H2>Printed Circuit Boards</H2></Fragment>
|
||||
<PrintedCircuitBoard pcb={pnpLights} />
|
||||
<PrintedCircuitBoard pcb={pnpControl} />
|
||||
</PageGroup>
|
||||
</ExperienceLayout>
|
||||
|
||||
@@ -19,6 +19,7 @@ import swag from "@assets/experience/spacex/avionics-test-engineering-internship
|
||||
import type { carouselGroup } from "@interfaces/image-carousel.ts";
|
||||
import type { categorySkills } from "@interfaces/skill-matrix.ts";
|
||||
import type { timelineEntry } from "@interfaces/timeline.ts";
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
const headerCarouselGroup: carouselGroup = {
|
||||
animation: "slide",
|
||||
@@ -28,11 +29,11 @@ const headerCarouselGroup: carouselGroup = {
|
||||
const timeline: timelineEntry[] = [
|
||||
{
|
||||
event: "Started",
|
||||
date: "January 2019",
|
||||
date: DateTime.fromISO("2019-01-01"),
|
||||
},
|
||||
{
|
||||
event: "Finished",
|
||||
date: "March 2019",
|
||||
date: DateTime.fromISO("2019-03-01"),
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import type { timelineEntry } from "@interfaces/timeline.ts";
|
||||
import five_year_patch from "@assets/experience/spacex/hardware-test-engineer-i-ii/five-year-patch.jpg";
|
||||
import starlink_headquarters_selfie from "@assets/experience/spacex/hardware-test-engineer-i-ii/starlink-headquarters-selfie.jpg";
|
||||
import starlink_patch from "@assets/experience/spacex/hardware-test-engineer-i-ii/starlink-patch.jpg";
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
const headerCarouselGroup: carouselGroup = {
|
||||
animation: "slide",
|
||||
@@ -30,28 +31,28 @@ const timeline: timelineEntry[] = [
|
||||
{
|
||||
event: "Started",
|
||||
eventDetail: "Satellite Hardware Test Team",
|
||||
date: "September 2019",
|
||||
date: DateTime.fromISO("2019-09-01"),
|
||||
description:
|
||||
"Owned test systems for four generations of Starlink flight computers and two generations of power boards",
|
||||
},
|
||||
{
|
||||
event: "Transitioned To Remote",
|
||||
eventDetail: "Moved To Oregon",
|
||||
date: "August 2022",
|
||||
date: DateTime.fromISO("2022-08-01"),
|
||||
description:
|
||||
"Personal decision, but I was allowed to work on tools for the build reliability engineering team",
|
||||
},
|
||||
{
|
||||
event: "Changed Teams",
|
||||
eventDetail: "Components Test Infra Team",
|
||||
date: "March 2024",
|
||||
date: DateTime.fromISO("2024-03-01"),
|
||||
description:
|
||||
"Vertical move that allowed for broader application of my skills",
|
||||
},
|
||||
{
|
||||
event: "Finished",
|
||||
eventDetail: "Thanks for all the fish!",
|
||||
date: "April 2025",
|
||||
date: DateTime.fromISO("2025-04-01"),
|
||||
description:
|
||||
"Celebrated five and a half years of helping put thousands of satellites, and dozens of rockets, into orbit",
|
||||
},
|
||||
|
||||