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

This commit is contained in:
2025-12-12 22:48:03 -08:00
parent adcbce68c8
commit 8fd744118f
67 changed files with 586 additions and 107 deletions

View File

@@ -15,7 +15,8 @@
cleanup-check \
cleanup-code \
convert_video \
convert_video_times
convert_video_times \
generate_asset_imports
default: dev
@@ -89,3 +90,9 @@ convert_video_times:
-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

@@ -13,13 +13,17 @@ Concours
CONSERV
Corwin
dangerousthings
dechorionation
Dechorionator
dechorionators
dockerization
dockerizing
drumheller
ebox
ELMI
fhhs
flowbite
flowrate
Gitea
HDFS
headshot
@@ -34,6 +38,8 @@ leconte
Loctite
luxon
MGMT
Micropumps
Millis
Mokai
Multimeters
nixos
@@ -42,26 +48,32 @@ Onshape
OSSM
OSURC
Passthroughs
pcbs
Perren
Perren's
Pixhawk
Protocase
pubpath
RFID
Rito
RSSI
SARL
showerheads
Shuttlebox
sinnhuber
sitemapindex
Smartsheet
solderable
ssds
Starlink
steller
Steller
Tanguay
Teamcenter
timelapse
triaging
trivago
Truong
Unstow
uuidv
vaapi

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: 8.5 MiB

View File

Before

Width:  |  Height:  |  Size: 2.4 MiB

After

Width:  |  Height:  |  Size: 2.4 MiB

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,54 +63,63 @@ const groupToShow: carouselGroup = Astro.props.carouselGroup;
</div>
<!-- Carousel wrapper -->
<div class="relative h-56 w-full overflow-hidden md:h-120">
<div
class:list={[
"relative h-56 w-full overflow-hidden rounded-lg md:h-120",
showBorder ? "border" : false,
className ? className : "border-caperren-green-dark",
]}
>
{
groupToShow.images.map((image, index) => (
<div
class="hidden duration-1500 ease-in-out"
data-custom-carousel-item={index}
>
<Image
src={image}
class="absolute inset-0 m-auto h-full max-h-full w-auto max-w-full object-contain"
alt="..."
layout="constrained"
loading="eager"
/>
</div>
))
carouselGroup.images &&
carouselGroup.images.map((image, index) => (
<div
class="hidden bg-black duration-1500 ease-in-out"
data-custom-carousel-item={index}
>
<Image
src={image}
class="absolute inset-0 m-auto h-full max-h-full w-auto max-w-full object-contain"
alt="..."
layout="constrained"
loading="eager"
/>
</div>
))
}
</div>
<!-- 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) => (
<button
type="button"
class="hover:bg-caperren-green-light h-3 w-3 rounded-full bg-black"
aria-current={index ? "false" : "true"}
aria-label={index.toString()}
data-custom-carousel-slide-to={index}
/>
))}
{carouselGroup.images &&
carouselGroup.images.map((_, index) => (
<button
type="button"
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}
/>
))}
</div>
<button
type="button"
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

@@ -0,0 +1,51 @@
---
import Carousel from "@components/Media/CustomCarousel/CustomCarousel.astro";
import Ul from "@components/Ul.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));
---
<div class="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 && (
<Ul
class="border-caperren-green border-t p-4 text-sm"
lineItems={revision.notes}
/>
)}
</div>
))
}
</div>

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,21 +15,25 @@ const timeline: timelineEntry[] = Astro.props.timeline || [];
data-timeline
>
{
timeline.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}
>
<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}
</time>
{entry.description && (
<p class="text-sm font-normal">{entry.description}</p>
)}
</div>
))
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}
>
<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.toFormat("LLLL kkkk")}
</time>
{entry.description && (
<p class="text-sm font-normal">{entry.description}</p>
)}
</div>
))
}
</div>
<div

0
src/content.config.ts Normal file
View File

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,49 +25,57 @@ 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",
},
],

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,11 +1,15 @@
---
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 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";
@@ -47,7 +51,7 @@ const videos: videoConfig[] = [
];
---
<ExperienceLayout title="OSURC - Software Team Lead">
<ExperienceLayout title="Software Team Lead" subTitles={roverSubTitles}>
<Carousel carouselGroup={headerCarouselGroup} />
<PageGroup>

View File

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

View File

@@ -1,5 +1,235 @@
---
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 PageGroup from "@components/PageGroup.astro";
import Paragraph from "@components/Paragraph.astro";
import Paragraphs from "@components/Paragraphs.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.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 InlineLink from "@components/InlineLink.astro";
import PopoverWordDefinition from "@components/PopoverWordDefinition.astro";
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),
];
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,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

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

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

View File

@@ -103,16 +103,16 @@ const categorizedSkills: categorySkills[] = [
From the DevOps perspective, I created a Makefile within the repo for
local development targets to build, run, and test both in a pure
context, and within a Docker container. After pushing updates on a
branch to my local Gitea instance, and opening a pull request, the
website performs spelling checks, runs unit tests, and integration
tests. Once these pass, the website is built into a Docker container and
uploaded to the registry in my Gitea instance. The image is then
deployed to staging on my <PopoverWordDefinition key="VPS" />, allowing
for manual validation of the changes. In order to merge, the build,
test, and deploy actions must pass, and ideally I should be empirically
validating the staging deployment. After merging and closing the pull
request, another action builds, tests, and deploys the main branch in
the same way as before, but to production, and is what you see here!
branch to my local Gitea instance, and opening a pull request, an
actions runner performs spelling checks, runs unit tests, and
integration tests. Once these pass, the website is built into a Docker
container and uploaded to the registry in my Gitea instance. The image
is then deployed to staging on my <PopoverWordDefinition key="VPS" />,
allowing for manual validation of the changes. In order to merge, the
build, test, and deploy actions must pass, and I empirically validate
the deployment. Post-merge, a similar action builds, tests, and deploys
the main branch in the same way as before, but to production, and is
what you see here!
</Paragraph>
</Paragraphs>
<SkillMatrix categorizedSkills={categorizedSkills} />

View File

@@ -10,9 +10,9 @@ import Paragraphs from "@components/Paragraphs.astro";
import type { carouselGroup } from "@interfaces/image-carousel.ts";
import alaska_bike_mountain_ocean from "@assets/about/alaska-bike-mountain-ocean.jpg";
import headshot from "@assets/about/headshot.jpg";
import circ_champions from "@assets/experience/osu-robotics-club/mars-rover-software-lead/circ-champions.jpg";
import alaska_bike_mountain_ocean from "@assets/hobby/motorcycling/trips/2025-08-alaska/alaska-bike-mountain-ocean.jpg";
const headerCarouselGroup: carouselGroup = {
animation: "slide",
@@ -88,7 +88,7 @@ const headerCarouselGroup: carouselGroup = {
>rfid implant</InlineLink
> in my hand!
</Paragraph>
<Paragraph class="mt-8 flex flex-col items-center" initialTab={false}>
<Paragraph class="mt-4 flex flex-col items-center" initialTab={false}>
<div>
If you're interested in contacting me, feel free to message on <InlineLink
href="https://github.com/caperren">LinkedIn</InlineLink

View File

@@ -4,4 +4,8 @@ import ResumeLayout from "@layouts/ResumeLayout.astro";
import resume from "@assets/resume/corwin_perren_2019-07-01_hardware_test_engineer.pdf";
---
<ResumeLayout title="2019-07-01 - Hardware Test Engineer" resume={resume} />
<ResumeLayout
title="Hardware Test Engineer"
subTitles={["2019-07-01"]}
resume={resume}
/>

View File

@@ -4,4 +4,8 @@ import ResumeLayout from "@layouts/ResumeLayout.astro";
import resume from "@assets/resume/corwin_perren_2025-10-27_infrastructure_engineer.pdf";
---
<ResumeLayout title="2025-10-27 - Infrastructure Engineer" resume={resume} />
<ResumeLayout
title="Infrastructure Engineer"
subTitles={["2025-10-27"]}
resume={resume}
/>