Compare commits

7 Commits

Author SHA1 Message Date
83c27ba1bf Merge pull request 'mars rover and embryo pick and plate content' (#19) from website-content-updates into main
All checks were successful
Build and Test - Production / test (push) Successful in 6m31s
Build and Test - Production / build_and_push (push) Successful in 5m27s
Build and Test - Production / deploy_production (push) Successful in 2s
Reviewed-on: #19
2025-12-18 04:24:01 +00:00
6ff2249955 Added won 1st to mars rover
All checks were successful
Build and Test - Staging / test (pull_request) Successful in 6m36s
Build and Test - Staging / build_and_push (pull_request) Successful in 5m14s
Build and Test - Staging / deploy_staging (pull_request) Successful in 2s
2025-12-17 20:23:44 -08:00
ec6cfba9ba Content for mars rover software lead and embryo pick and plate, small padding tweak to printed circuit board notes
All checks were successful
Build and Test - Staging / test (pull_request) Successful in 6m24s
Build and Test - Staging / build_and_push (pull_request) Successful in 5m23s
Build and Test - Staging / deploy_staging (pull_request) Successful in 2s
2025-12-17 20:05:12 -08:00
22b6a06b32 Added media for embryo pick and plate, including pcbs, made tweaks to pcb astro component for title and description, new H4 component, enabled zebrafish pnp and mars rover software team pages 2025-12-17 17:37:44 -08:00
090bc11ed0 Merge pull request 'Component for PCBs, many visual tweaks, finished dechorionator content, added many many photos, started work on mars rover software lead, timeline to luxon and automatic date-based ordering' (#18) from website-content-updates into main
All checks were successful
Build and Test - Production / test (push) Successful in 5m12s
Build and Test - Production / build_and_push (push) Successful in 4m24s
Build and Test - Production / deploy_production (push) Successful in 2s
Reviewed-on: #18
2025-12-13 07:23:40 +00:00
dac9e80efd Fix unit tests for placeholder site-layout entries
All checks were successful
Build and Test - Staging / test (pull_request) Successful in 5m26s
Build and Test - Staging / build_and_push (pull_request) Successful in 4m16s
Build and Test - Staging / deploy_staging (pull_request) Successful in 3s
2025-12-12 23:05:47 -08:00
8fd744118f Component for PCBs, many visual tweaks, finished dechorionator content, added many many photos, started work on mars rover software lead, timeline to luxon and automatic date-based ordering
Some checks failed
Build and Test - Staging / test (pull_request) Failing after 4m56s
Build and Test - Staging / build_and_push (pull_request) Has been skipped
Build and Test - Staging / deploy_staging (pull_request) Has been skipped
2025-12-12 22:48:03 -08:00
105 changed files with 1296 additions and 121 deletions

View File

@@ -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;

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1017 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 878 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 826 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 MiB

View File

Before

Width:  |  Height:  |  Size: 2.4 MiB

After

Width:  |  Height:  |  Size: 2.4 MiB

5
src/components/H4.astro Normal file
View File

@@ -0,0 +1,5 @@
---
---
<h4 class="md:text-md text-xs sm:text-sm"><slot /></h4>

View File

@@ -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"

View File

@@ -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}

View File

@@ -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"

View File

@@ -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>

View File

@@ -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>

View File

@@ -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",

View 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>

View File

@@ -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>

View File

@@ -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("/"));
}
}

View File

@@ -1,5 +1,5 @@
export interface carouselGroup {
animation: "static" | "slide";
animation?: "static" | "slide";
interval?: number;
images: ImageMetadata[];
images?: ImageMetadata[];
}

View 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,
}));

View File

@@ -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[];
}

View File

@@ -1,6 +1,8 @@
import { DateTime } from "luxon";
export interface timelineEntry {
event: string;
eventDetail?: string;
date: string;
date: DateTime;
description?: string;
}

View File

@@ -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>

View File

@@ -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"),
},
];

View File

@@ -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"),
},
];

View File

@@ -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"),
},
];

View File

@@ -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 Id had to do on some
other software Id written and knew wouldnt 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
wouldnt 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">

View File

@@ -0,0 +1,3 @@
export const subTitles = ["Oregon State University", "OSU Robotics Club"];
export const roverSubTitles = [...subTitles, "Mars Rover Team"];

View File

@@ -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" }],
},
],
};

View File

@@ -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>

View File

@@ -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"),
},
];

View File

@@ -1,5 +0,0 @@
---
import ExperienceLayout from "@layouts/ExperienceLayout.astro";
---
<ExperienceLayout title="SARL - Team Lead" />

View File

@@ -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",
},
],
},
],
};

View File

@@ -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>

View File

@@ -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"),
},
];

View File

@@ -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",
},

Some files were not shown because too many files have changed in this diff Show More