diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 00000000..e69de29b diff --git a/404.html b/404.html new file mode 100644 index 00000000..7e657bb3 --- /dev/null +++ b/404.html @@ -0,0 +1,515 @@ + + + + + + + + + + + + + + + + + + + + + + + Teddy Warner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + +
+ +
+ + +
+ + + + + + + + + +
+
+ + + +
+
+
+ + + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+ +
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CNAME b/CNAME new file mode 100644 index 00000000..db987e67 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +teddywarner.org diff --git a/Projects/8-bit/index.html b/Projects/8-bit/index.html new file mode 100644 index 00000000..155740e1 --- /dev/null +++ b/Projects/8-bit/index.html @@ -0,0 +1,1255 @@ + + + + + + + + + + + + + + + + + + + + + + + + + 8-bit Breadboard Computer - Teddy Warner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + +
+ +
+ + +
+ + + + + + + + + +
+
+ + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

+ +

+ +

+ + + +

+ +

+ + + + + + +

+ +

+ + + +

+ + +

+ + + + + +

+ + +
+ + + + + + +

8-bit Breadboard Computer - Build Log

+
+ Teddy Warner's GitHub profile picture Teddy Warner| 2021-2022 | X-X minutes + +
+ +
+

Progress

+

I’ve used microcontrollers in most of my past projects. IC data sheets are super helpful in board design with their pinouts, but I’ve noticed much of the contents have been pretty foreign to me. Every once in a while, the youtube algorithm recommended one of Ben Eater’s computer concept videos to me, which provide a great explanation of the simplistic logic and proofs in computers. I ended up watching his entire 8-bit Breadboard Computer series (embedded below) over the fall of 2021, to try to get a bit of a better idea of the workings of a microcontroller. The computer is an “as simple as possible”1 take on an 8-bit computer, using only simple logic to create an 8-bit computer on breadboards. The computer is built-in sectioned modules and then interfaced together, making each section a bit easier to swallow.

+
+

Documentation Style

+

This page is a bit different than my other project pages, as it is a build log for my take on this 8-bit Breadboard Computer. Instead of outlining project development (as Mr. Eaters’ videos do an amazing job with that), this page will be a collection of media behind each of my computer modules, as well as some final programs ran on the computer.

+
+

+ +

+

+

Bill of Materials

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Qty.DescriptionCostURL
14Breadboard$112link
101kΩ resistor
910kΩ resistor
1100kΩ resistor
24470Ω resistor
11MΩ resistor$16.99link
11MΩ potentiometer$1.39link
60.01µF capacitor$1.20link
160.1µF capacitor$3.00link
11µF capacitor$0.15link
4555 timer IC$0.63link
274LS00 (Quad NAND gate)$0.76link
174LS02 (Quad NOR gate)$0.76link
574LS04 (Hex inverter)$0.87link
374LS08 (Quad AND gate)$0.72link
174LS32 (Quad OR gate)$0.89link
174LS107 (Dual JK flip-flop)
Note: The videos use the 74LS76 which is extremely difficult to find. The 74LS107 is functionally equivalent but be careful: it has a different pinout.
$1.58link
274LS86 (Quad XOR gate)$1.11link
174LS138 (3-to-8 line decoder)$0.80link
174LS139 (Dual 2-line to 4-line decoder)$1.55link
474LS157 (Quad 2-to-1 line data selector)$1.58link
274LS161 (4-bit synchronous binary counter)$1.95link
874LS173 (4-bit D-type register)$1.45link
274189 (64-bit random access memory)$9.90link
674LS245 (Octal bus transceiver)$1.26link
174LS273 (Octal D flip-flop)$1.99link
274LS283 (4-bit binary full adder)$1.38link
328C16 EEPROM$11.85link
3Double-throw toggle switch$2.97link
3Momentary 6mm tact switch$1.05link
18-position DIP switch$0.79link
14-position DIP switch$0.79link
44Red LED
8Yellow LED
12Green LED
21Blue LED$12.99link
4Common cathode 7-segment display$4.36link
122 AWG Solid Tinned-Copper Hook-Up Wire$29.95link
+

— Total Cost - $228.66 — +

+

Clock Module

+

+ +

+

+

Setup of astable multivibrator circuit

+

+ + +

+

Oscilloscope display showing clock vs capacitor waveforms +
+

Clock vs Capacitor
+

+

+

+

Variable astable multivibrator circuit setup

+

+ + +

+

Monostable multivibrator circuit setup

+

+ + +

+ +

+

+ +

+

+ +

+

+

Completed clock module assembly

+

+ + +

Registers

+

+ +

+

+ +

+

+

8-bit register module assembly

+

+ + + + +

Arithmetic Logic Unit (ALU)

+

+ +

+

+ +

+

+ +

+

Random Access Memory Module (RAM)

+

+ +

+

+ +

+

+ +

+

Program Counter

+

+ +

+

Output Register

+

+ +

+

+ +

+

+ +

+

+ +

+

Module Meshing

+

CPU Control Logic

+

Fibonacci Sequence

+

+ +

+

+

Visual representation of Fibonacci sequence spiral

+

+

+

Mathematical equation for Fibonacci sequence

+

+

+

First 12 Numberes in the Fibonacci Sequence

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
nFibonacci Number
00
11
21
32
43
55
68
713
821
934
1055
1189
+

+
+
+
    +
  1. +

    https://eater.net/8bit 

    +
  2. +
+
+ + + + + + + + + + + + + + + + + + +

Comments

+ + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Projects/AdaptableAquaponics/index.html b/Projects/AdaptableAquaponics/index.html new file mode 100644 index 00000000..8483c9fb --- /dev/null +++ b/Projects/AdaptableAquaponics/index.html @@ -0,0 +1,996 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Adaptable Aquaponics - Teddy Warner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + +
+ +
+ + +
+ + + + + + + + + +
+
+ + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

+ +

+ +

+ + + +

+ +

+ + + + + + +

+ +

+ + + +

+ + +

+ + + + + +

+ + +
+ + + + + + +

Adaptable Aquaponics

+
+ Teddy Warner's GitHub profile picture Teddy Warner| Summer, 2022 | 18-23 minutes + +
+ +
+

+
+ +
+ +

A Theoretical Confrontation to Contemporary Farming Challenges.

+

+

Paralleling the growth of the global human population, the increased strain is placed on ever-expanding food production. Following current population growth trends, the United Nation’s Food and Agricultural Organization (FAO) predicts a required 70% growth in food production by 2050 to adequately match global needs.1 As of 2020, 9.9% of the present human population faced undernourishment with an estimated 720 and 811 million humans going hungry. Continued reliance on current insufficient North American Agriculture practices will bolster inadequate access to nutrition. Seedstock.com divides problems with contemporary agricultural practices into categories including …

+
    +
  • Resource Depletion - The Costs of Industrial Agriculture
  • +
  • Land Management - Degrading and Undervaluing Farmland
  • +
  • Food Waste - Compromising Food Security
  • +
  • Demographic Changes - A Disconnected Public
  • +
+

Addressing the developing demand for food requires a direct confrontation with each of these four challenges and opens the door for sustainable alternatives to classic agricultural practices - massive potential for entrepreneurial pursuit. Aquaponics offers the potential to address the problems of contemporary agricultural practices whilst demonstrating scientific principles and modeling biological cycles for the education of a local community. “Aquaponics is a form of agriculture that combines raising fish in tanks (recirculating aquaculture) with soilless plant culture (hydroponics). In aquaponics, the nutrient-rich water from raising fish provides a natural fertilizer for the plants and the plants help to purify the water for the fish”, and this is a sustainable method to both raise fish and grow crops.2

+

+

Basic aquaponics system diagram showing water flow +Detailed aquaponics system diagram with component labels +
+

Aquaponics System Diagrams
+

+

+

Adaptable Aquaponics serves as a theoretical successor to my Assistive Aquaponics Fish Tank, a small-scale, self-sustaining ecosystem, with the hopes ofexpanding the accessibility of successful & thriving aquaponics ecosystems to individuals. While the Assistive Aquaponics Fish Tank is equipped to enable access to aquaponics at an individual level, economic optimization of the project came second to maintaining a sleek design hoping to catalyze in-house acceptance of the product. Adaptable Aquaponics offers some contrasting design choices with the goal of addressing all four categories of contemporary agricultural challenges and addressing the current bottlenecks of global nutrition.

+

+ + + +

+

Both the Fusion 360 model and the prototyping mesh & SVG files for this project can be accessed via the button below. These files can be used and altered with attribution, as stated and enforced by my project license. In addition to the files included in this downloads folder, please note that Fusion 360 source files can be accessed with the Open in Fusion 360 button in the bottom right-hand corner of each of the embedded designs on this page. The renders of my Adaptable Aquaponics model utilize the Ulmer Muenster HDRI from polyhaven.com in their environment. Additionally, this model’s Solar Panels, Water Pump & Casters are altered files from CAD libraries.

+

+

Adaptable Aquaponics Files

+

+
+

Make it Modular

+

Autodesk Construction Solutions Scholarship Award badge

+

Adaptive Aquaponics is the scaled successor to my Assistive Aquaponics Fish Tank & my winning entry into Autodesk Instructable’s Make It Modular student design challenge receiving the Autodesk Construction Solutions Scholarship Award. Be sure to check Adaptable Aquaponics out on instructables!

+
+

CAD - Fusion 360

+

Being the theoretical successor to my Assistive Aquaponics Fish Tank, the CAD for Adaptable Aquaponics utilizes the same “skeleton” of a closed-loop water circulation system, incorporating a remixed version of my Assistive Aquaponics Fish Tank’s plant box. Each “Aquaponics Cell” in Adaptable Aquaponics (I’ll touch on the distinction of these mentioned modules later in this section) contains eight individual plant boxes, each with a volume over two times as great as the smaller plant box of my Assistive Aquaponics Fish Tank. These up-scaled cells have been altered to ensure proper water flow at a larger scale, and are linked in series via several pipes to a Biofilter housed on the “Aquaponics Rack”. Unlike in a small-scale ecosystem such as my Assistive Aquaponics Fish Tank, a biofilter is an essential component of a large-scale aquaponics setup. The filter expands the surface area of your system, providing ample space for the nitrogen cycle to occur.

+

Following the Assistive Aquaponics Fish Tank once more, Adaptable Aquaponics is a cross between a Nutrient Film & Media Bed aquaponics system. As water is pumped into each plant box from the biofilter, it continuously flows along the sloping profile of the plant box’s bed, passing through the plant roots, before exiting the plant box through a waterfall-esk exit slit & draining into the fish tank below. Plant beds & fish tanks are placed in an eight-to-one ratio in each “Aquaponics Cell”, a stack in contrast to the small tank and one-to-one ratio of the Assistive Aquaponics Fish Tank. Opting for a larger fish tank increases the output efficiency of an aquaponics farm. Instead of being restrained to small-scale aquaculture (such as goldfish in my Assistive Aquaponics Fish Tank), hardier fish such as Tilapia & Catfish can be farmed - making the food output of the farms even more efficient. Water is pumped out through a drain pipe at the bottom of each tank, flowing through a sediment tank located on the “Aquaponics Rack” before into the water pump & then back into the biofilter. Parraling the system’s inclusion of a larger tank for larger fish, a sediment tank is a must to free the system of excessive fish waste, allowing for maintained water clarity. Heavy waste carried in by the stream of water in the system will sink to the bottom of the sediment tank under conditions of low flow.

+

The principle behind my design work on Adaptable Aquaponics parallels the “circles of progress” model, where you begin by achieving the core minimum viable product, and expanding the diameter of your “circle”, inflating your project with more layers of progress. Though this isn’t my suggested approach to CAD, I found using the “circles of progress” mindset useful while thinking of the modularity scales in my file.

+

+ +
Aquaponics Cell
+ + +
Adaptable Aquaponics System
+ +

+

Adaptable Aquaponics is constructed with layered modularity, a distinction highlighted by the component layering in my Fusion browser.

+

+

Fusion 360 browser showing component layering

+

+

Eight Individual plant boxes make up a single aquaponics cell, two aquaponics cells make up a single aquaponics rack, and two aquaponics racks are housed inside of a single shipping container. The scaling of modularity allows for application scaling to be applied to Adaptable Aquaponics. A single plant box could be used in the form factor of my Assistive Aquaponics Fish Tank, or multiple Adaptable Aquaponic Shipping Containers could be housed in parallel to allow for large-scale farming. Modularity in the case of Adaptable Aquaponics not only enables future iteration & reconfiguration but also the scaling of the ecosystem itself - meeting the needs of any potential consumers - ranging from individual to community.

+

Computational Design

+

Shifting to an analysis of the CAD work behind Adaptable Aquaponics, I was super stoked to get to experiment with Fusion 360’s new Automated Modeling tool! I used automated modeling to generate an optimized vertical support structure for my systems aquaponics rack, as well as for the angled mounts for adaptable aquaponics solar panels. The tool can be found under the Automate menu in the toolbar.

+

+

Fusion 360 automated modeling toolbar interface

+

+

Automated modeling is about as intuitive as you can get, requiring only three inputs to generate a body - Faces to Connect, Bodies to Avoid & Operation type. I began the computational design work of my aquaponics rack outside of the automated modeling tool itself, but instead by creating both the top and bottom plates of my Aquaponics rack & attaching 6 mounting points to each sheet. Then, working around the prepositioned Aquaponics Cells, biofilter & sediment tank, I created shelves to hold the content of the racks, unattached to support. I then opened the automated modeling tool and selected the previously created mounting points on both the top and bottom plates as well as each shelf with the Faces to Connect selection tool. Note, that there are three individual vertical supports on each Aquaponics rack, so I selected a corresponding set of 1/3 of the total created faces for each automated operation. I moved on to the Bodies to Avoid selection tool, highlighting the components of each Aquaponics Cell, the biofilter & the sediment tank closest to the selected points of connection. I was careful here to only select crucial bodies to avoid, not all in the model - the fewer input selections, the faster computation time. With these selected inputs, I generated a design with a New Body operation.

+

+

Automated design workspace showing bodies to avoid in red and faces to connect in blue +
+

Bodies to Avoid are highlighted red, while Faces to Connect are highlighted in blue
+

+

+

After ~10 Mins, the Automated Modeling menu presented me with 6 generated bodies under the Alternatives menu. You can click through these generated bodies to see them in the workspace, before finalizing your selection with the Ok button. I repeated this generation for the two other vertical supports on the Aquaponics Rack, choosing a visually similar generation between all three to keep consistency to the rack. Computationally designed bodies can also be seen in the solar panel mounts located on the roof of Adaptable Aquaponics. These bodies followed a similar workflow, where I connected the back face of each solar panel to pre-created mounting points.

+

3D Plumbing

+

The complex spatial routing of Adaptable Aquaponics plumbing required the use of Fusion 360’s ability to sketch non-planar line sand splines. To enable this feature, click on your user icon in the top right of the window & navigate to …

+
Preferences < Design < Allow 3D sketching of lines and splines
+
+

… and toggle this option on. Be sure to click Apply in the bottom right-hand corner of the Preferences menu before exiting.

+

+

Fusion 360 preferences menu showing 3D sketching option

+

+

Fusion 360’s non-planar sketching allows spline points to be removed from their initial sketch plane and routed to other planes. Once a point is placed on a 3D spline, Fusion allows for the alteration of direction in three dimensions before you place the next point.

+

+

3D spline routing for plumbing system

+

+

Beginning from the intake valve of each plant box, I routed a spline to a t-connector before using the Pipe tool with each routed spline as a reference - leaving me with all of the plant boxes in a given Aquaponics Cell routed in series. Additionally, 3D splines were used to route the plumbing from each fish tank to the sediment tank, from the sediment tank to the water pump, from the water pump to the biofilter, and from the biofilter back to the plant boxes.

+

+ + +

+

Rendering - Fusion 360

+

Once Adaptable Aquaponics was finalized - the 95th iteration … phew - I continued to Fusion 360’s rendering workspace.

+

+

Fusion 360 render workspace toolbar

+

+

I began by setting up the renders scene by navigating to …

+
Setup < Scene Settings < Environment Library
+
+

… where I chose to upload a custom HDRI backdrop.

+

+

Render workspace setup with HDRI environment +Ulmer Muenster HDRI background preview

+

+

As mentioned earlier on this page, I chose the Ulmer Muenster HDRI from polyhaven.com as my backdrop, as the scene offers a compromise of beauty and urban (not that those are mutually exclusive) - preserving the largest of my envisioned use cases for Adaptable Aquaponics, urban farming (stay tuned for the next section!). Once my HDRI backdrop was uploaded, I navigated back to …

+
Setup < Scene Settings
+
+

… where I took a trial and error approach to tune the renders Environment, Ground & Camera settings until I was left with some nice looking renders! Check them out below!

+

+

Final render of Adaptable Aquaponics system - perspective view +Final render of Adaptable Aquaponics system - side view +Final render of Adaptable Aquaponics system - full setup

+

+

Addressing the Current Bottlenecks of Global Nutrition

+

Resource Depletion - The Costs of Industrial Agriculture

+

Contemporary agriculture is completely dependent on non-renewable materials. Dependence on fossil fuels is not only not a sustainable practice that is damaging our planet but jeopardizes the future of our agricultural stability. With rigid dependence, how do we expect to handle the extinction of a non-renewable? Adaptable Aquaponics offers a solar-powered solution, completely independent of any non-renewables & costly electric bills that hinder access in communities facing a scarcity of access. Though the elimination of dependencies is crucial to making Adaptable Aquaponics a sustainable solution to contemporary farming challenges, the vertical integration of the system offers a Carnegie-esk entrepreneurial venture, ensuring freedom from the potentially binding agreement for resources.

+

Data from the US Geological Society shows rainfall patterns “may become less predictable and dependable”, while the amount of groundwater “drawn for use in irrigation has tripled since the 1950s”.3 Though, unlike fossil fuels, water is a renewable resource, freshwater has its limits. Considering nearly 70% of all freshwater used by humans is used in agriculture, addressing water usage is a warranted concern. When compared to standard agricultural practices, Aquaponics requires 90 to 99% less water.4 Similar to the potential lack of electricity access, Adaptive Aquaponics offers communities with a shortage or imposition in access to water a sustainable agricultural alternative, while also offering entrepreneurs another opportunity to continue to vertically integrate.

+

Land Management - Degrading and Undervaluing Farmland

+

Faulty land management techniques have stripped soil from its nutrients, requiring fertilizers to aid plant growth. The nature of aquaponics replaces any dependency on soil with nutrients from the nitrogen cycle - a naturally occurring ecosystem that can run independently from fertilizers. Adaptive Aquaponics’ cross between a Nutrient Film & Media Bed aquaponics system preserves the dirt-free nature of hydroponics, whilst maintaining a small, modular structure of its plant boxes. The modular nature of adaptive aquaponics allows the system to potentially thrive regardless of scale and location, opening the door for urban farming!

+

Food Waste & Demographic Changes

+

The UN estimates that 1/3 “of the world’s food goes to waste, either during agricultural production, post-harvest handling, and storage, processing, distribution, or consumption”.3 The modular nature of Adaptive Aquaponics ensures adequate scaling of agriculture in a given population. The compacted form factor and potential for urban or harsh environment farming enable the growth of food locally to a population. Instead of depending on agricultural preservation, processing, storage, and distribution, providing a community with Adaptable Aquaponics enables full control over food production, ensuring neither over nor under production. Interaction with agriculture at a community scale may remove the barrier between farming communities and consumers and hopefully, will catalyze a shift from to favor “conservation over convenience” thus regulating agriculture to ensure adequate food is provided.3

+

Scale Prototyping

+

As has seemingly become a trend across my work, I attempted to use a plethora of digital fabrication in the creation of my scale model. My model is a 2.3% scale prototype of Adaptable Aquaponics, fabricated with both additive and subtractive processes. Shout out to @taimakestuff who was my lab buddy @makerspaceCharlotte while I ground out this project’s post-processing :)

+

Computationally designed elements utilize organic surveys to ensure optimal geometry for both strength and efficiency, thus the perfect partner for additive manufacturing. I opted to 3D print all elements of my scale model that utilized Fusion 360’s Automated Modeling, including the shipping container with its solar panel mounts & Aquaponics Rack with its vertical supports. I also printed the contents of each aquaponics rack to achieve the highest accuracy to the draft angles and plumbing locations on the bodies. All parts for this project were printed on my Prusa I3 MK3S+ with a 0.4mm nozzle at a 0.3mm layer height. I chose a white PETG, along with a pretty aggressive layer height to allow for ease of post-processing & painting of the model.

+

+

+ + +

+

+

PETG is my go-to generic 3D printing material, however, it really shines when faced with post-processing. PETG takes well to sanding and is overall rather easy to clean up. One of my favorite tricks is taking hot air from a hair-dryer to a PETG printed part. The hot air shrivels any stringing or surface defects into masses that fall off after rubbing your finger over the surface.

+

+ + +

+

+

I was lucky enough to stumble across some gloss black acrylic in the scrap bin @makerspaceCharlotte, which I used to create Adaptable Aquaponic’s solar panels. This was my first time using the lasers @makerspaceCharlotte, and the machine that I was on ran a different wattage than the lasers at the Charlotte Latin Fab Lab (my school’s lab & my workplace) so I began with a bit of research into optimal cut settings. After altering the laser focus to achieve the desired (slightly thickened beam) effect, I began batch cutting the model solar panels.

+ + +

Note the other test cuts on the stock material above. Though for the sake of focusing on Adaptable Aquaponics and not the prototyping process I’ve chosen to not mention much of the prototyping behind this physical model. I usually include ample prototyping content in my articles, so it felt necessary to call attention to the test cuts here :)

+

I cleaned up the final cut parts with some soap and water, before continuing to attach each solar panel to its 3D printed mount & then to the roof of the Adaptable Aquaponics shipping container.

+

+

+

+ + +

+

+

I followed up my acrylic cutting with the wooden base and top to each Aquaponics Rack. Unfortunately, I didn’t have access to any 1/8” ply, so I had to do some prep work with a piece of 1/4” plywood stock. I planned the surface of my stock material on a drum-sander until down to just below 1/8”. I then focused the laser to this stock & cut all four planks.

+

The glue up of these shelves was a bit finicky, as my fingers just seem to be a super glue magnet - but eventually, I got both Aquaponics Racks complete with their plants, vertical support, shelves, Aquaponics Cells, biofilter & sediment tank, and then installed them inside the shipping container.

+

+ + +

+ +

+

+

As a finishing touch, I used a Cricut Explore Air 2 to cut the sign attached to the side of Adaptable Aquaponics from vinyl. Though I’ve made dozens upon dozens of stickers with the same workflow, the scale of this logo proved to be a bit of a challenge. I not only had difficulties weeding the cut vinyl at such a small scale, but the machine itself was ripping up parts of the design upon retraction. I ended up altering the cut settings to a custom array as suggested by sofontsy.com, using the Washi Sheet default with a pressure set to More. With these settings, I was able to get a clean enough cut that I could weed the sticker, layer the two colors, and then apply to the side of the Adaptable Aquaponics model.

+

+ + +

+

+

Glory Shots

+

+

+ +

+

+

With the design and prototyping completed, I headed out to my local strip mall for a photo op, only getting a couple of strange looks from shoppers wondering what the boy in the old-navy pajama pants was doing taking pictures of a miniature in the middle of the parking lot. But it was worth it! As opposed to settling for a diorama background, or just pictures of my model on my workbench, I highlighted a legitimate potential environment for such a product, and I promise you, when Adaptable Aquaponics hits full scale in a parking lot, the shoppers won’t be blown away by my magnificent fashion choices, but instead by the wonders of Aquaponics!

+
+
+
    +
  1. +

    https://www.un.org/en/global-issues/food 

    +
  2. +
  3. +

    https://aquaponics.com/ 

    +
  4. +
  5. +

    http://seedstock.com/2012/04/18/five-major-challenges-facing-north-american-agriculture/ 

    +
  6. +
  7. +

    https://earth.org/data_visualization/aquaponics-a-solution-to-food-insecurity/ 

    +
  8. +
+
+ + + + + + + + + + + + + + + + + + +

Comments

+ + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Projects/AssistiveAquaponics/index.html b/Projects/AssistiveAquaponics/index.html new file mode 100644 index 00000000..c68323a1 --- /dev/null +++ b/Projects/AssistiveAquaponics/index.html @@ -0,0 +1,2294 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Assistive Aquaponics Fish Tank - Teddy Warner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + +
+ +
+ + +
+ + + + + + + + + +
+
+ + + + + + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + +

+ +

+ +

+ + + +

+ +

+ + + + + + +

+ +

+ + + +

+ + +

+ + + + + +

+ + +
+ + + + + + +

Assistive Aquaponics Fish Tank

+
+ Teddy Warner's GitHub profile picture Teddy Warner| Winter-Spring, 2021 | 83–105 minutes + +
+ +
+

+ + +

+

Final Project Concept

+

I’ve always had an interest in having my own home garden capable of growing plants and herbs, as I enjoy the access to fresh foods, however, my past attempts at gardening always seemed to be relatively fruitless. Due to my structured high school student schedule, I am usually not around to look after and take care of a garden, and thus, this want for fresh foods hasn’t become a reality. My interest in my Fab Academy final project came from this interest in having a home garden that could morph to my tight high school schedule. I started brainstorming this concept and found many projects and articles that were helpful or influential to my idea, some of them being Gardening + Arduino, Garduino, DIY Home Assistant Garden Automation System, Raspberry Pi Powered IOT Garden, and the Advanced Irrigation System. Although looking at projects like these gave me some inspiration, I have many personal design factors that have also helped me form my idea. I started the Fab Academy course with the idea of an Automated Windowsill Garden for my final project, talked about on my Orginal Project Concept page, however as I began work through the class, I expanded on the idea. After talking with Professor Neil Gershenfeld about my Final Project concept in the Computer-Aided Design week lecture, I began to alter my idea from this windowsill box garden, into a garden in a different form factor. While discussing my windowsill box concept, Neil suggested some research into the world of ‘ponics’, the techniques of growing plantlife without soil, where plant roots are placed in a water-mineral solution and fed with liquid fertilizers. After some research, I found the practice of Aquaponics especially appealing to me. “Aquaponics is a system of producing food that combines aquaculture (raising fish, snails, or prawns) and hydroponics (growing plants in water). It can be an elegant closed system whereby the fish produce waste which, in turn, circulates to feed the plants which, in turn, purify the water for the next school of aquatic generations” (The Evolution of “Ponics” in Organic). The second thought that also leads me to this change was the form factor of the projects themselves. I attend Fab Academy classes on my Highschool campus after I finished my school day, and carrying in a windowsill box like a coffin to and from our lab every day would be impractical. With this form factor issue comes my third motivation for this change, my family may be moving houses in the upcoming months, and I would not want to build a windowsill box to fit a windowsill on a house that I may not even live in by the end of this course. With all of these thoughts in mind, I was still committed to the idea of a Garden in general, and settled on an aquaponic garden in a fish tank-like form factor, with the plants growing on top of the tank itself. This not only solves my transportation problem, but a fish tank-style aquaponic garden could be placed inside, and then be moved from house to house.

+

Reasearch, Planning & Sketches

+

Before starting work on designing, manufacturing, and running an aquaponics system, I set off to learn about the practice of aquaponics more in-depth. As stated previously, “Aquaponics is a system of producing food that combines aquaculture (raising fish, snails, or prawns) and hydroponics (growing plants in water). It can be an elegant closed system whereby the fish produce waste which, in turn, circulates to feed the plants which, in turn, purify the water for the next school of aquatic generations” (The Evolution of “Ponics” in Organic). An Aquaponics system is thus an ecosystem shaped like a closed circle, where each step leads to the next until returned to where the system started. I jotted down this “Circle” concept in the diagram shown below, listing out each step taken in the system to return to the starting place.

+

+
+

Aquaponics Circle Diagram - System of Recirulation
+

+

+
+

Aquaponics System Diagram - A more Colorful System Depiction
+

+

When in balance, six steps create a successful aquaponics ecosystem, where all parts of the system rely on the others, a process known as recirculation. The aquatic life of the tank contributes poop and waste food into the ecosystem, creating ammonia, a toxin for the fish, in the water in the process. This generated ammonia is then converted into helpful Nitrites, and eventually, Nitrates by bacteria in the system through a natural process called The Nitrification Cycle, after which, the system’s plants absorb these Nitades as plant food, filtering the water in the process, and therefore the eliminating any harmful ammonia in the system’s water, returning the system to its starting place. The six major players in the system, Fish, Plants, Bacteria, Ammonia, Nitrites, and Nitrates, rotate around this six-step ecosystem repeatedly, and when kept intolerance with each other, create the thriving aquaponics ecosystem I’m looking for in creating my aquaponics fish tank. In The Effectiveness of Aquaponic Compared to Modified Conventional Aquaculture for Improved of Ammonia, Nitrite, and Nitrate, an academy journal article written by Deswati, Amelliza Deviona, Ella Intan Sari, Yulizar Yusuf, and Hilfi Pardi for the Rasayan Journal of Chemistry, elements of the aquaponic system are analyzed, and some ideal thresholds are given for a balanced system between Fish, Plants, Bacteria, Ammonia, Nitrites, and Nitrates, all with included real-life large scale aquaponics systems. I found this journal super helpful with providing application to aquaponics and also expanding my understanding of the ecosystem components through the journal’s experiments.

+ + +

Deswati, et al. “The Effectiveness of Aquaponic Compared to Modified Conventional Aquaculture for Improved of Ammonia, Nitrite, and Nitrate.” Rasayan Journal of Chemistry, vol. 13, no. 1, Jan. 2020, p. 1. EBSCOhost, search.ebscohost.com/login.aspx?direct=true&db=edb&AN=142699175&site=eds-live.

+

With the workings of this system in mind, I began to sketch out a design for my aquaponics tank, where a garden box is located above a fish tank, to allow for optimal water paths. I planned on making this structure from plywood and then adding additional internal sections for both the fish and plants, connecting these two to the plywood structure, which will also house electronics. Below are some of these concept sketches, showing the brainstorming of the tank’s plywood structure.

+

+

+

+
Some Notes and Sketches of the Aquaponics Fish Tank
+ +

+

+

Project License

+

Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.

+

+

+

Bill of Materials

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
QtyDescriptionPriceLinkNotes
1Power Supply$26.95LinkAC to 12v DC - Low Height Profile
1AC Power Adapter$8.99LinkRocker Switch for Power Control - Inline Fuse
1Water Pump$24.99Link12v - 9mm outlet - 130GPH
2m / 6.6ft9mm Silicon Tubing$10.49LinkN/A
5m / 16.4ftGrow Light Strips$13.99Link4 Red : 1 Blue
5m / 16.4ftWhite Light Strips$11.99LinkIn Lab
1pH Sensor$66.99LinkN/A
1Electrical Isolation Board$16.99LinkN/A
120pFemale to Female Jumper Cables$13.99LinkIn Lab
1 SpoolSpeaker Wire$9.95LinkIn Lab
1 Pack18 Gauge Wire$15.99LinkIn Lab
1.75mm - 1kgWhite PETG$23.99LinkN/A
2lbAquaponics Beads$11.95LinkN/A
5oz TubeWeldOn 16$17.90LinkN/A
10.1oz Caulk TubeAll Purpose Clear Silicone$5.38LinkN/A
6 - 1206 FootprintSMD LED$3.00LinkIn Lab
1 - 1206 Footprint100nf Capacitor$0.18LinkIn Lab
2 - 1206 Footprint22pf Capacitor$0.10LinkIn Lab
7 - 1206 Footprint10uf Capacitor$1.40LinkIn Lab
1 - 1206 Footprint1uf Capacitor$0.13LinkIn Lab
116Mhz SMD Crystal$0.27LinkIn Lab
8 - 1206 Footprint330 Resistor$3.20LinkIn Lab
3 - 1206 Footprint10k Resistor$0.30LinkIn Lab
11 - 1206 Footprint0 Resistor$0.26LinkIn Lab
3SMD 12v-5v Fixed Voltage Regulator$4.80LinkIn Lab
110k Potentiometer$0.87LinkIn Lab
3 - 1206 Footprint4.9k Resistor$1.50LinkIn Lab
3SMD Schottky Diode$0.36LinkIn Lab
1SMD MOSFET$0.37LinkIn Lab
1ATTiny 1614$0.91LinkIn Lab
2ATTiny 412$1.40LinkIn Lab
1ATMega 328p$2.43LinkIn Lab
1 PackThrough Hole Male Headers$7.49LinkIn Lab
5SMD Male Headers$4.75LinkIn Lab
3 - Single SidedCopper PCB Blanks$9.35LinkIn Lab
+

Total Cost: $323.60

+

+

Downloads

+
    +
  • Click Here to access and download all files, including CAD, Code, and Schematics, for this project. These files can be used and altered with attribution, as stated and enforced by my project license. In addition to the files included in this downloads folder, please note that Fusion 360 source files can be accessed with the Open in Fusion 360 button in the bottom right-hand corner of each of the embedded designs on this page.
  • +
+

Assignments Devoted Towards my Final Project

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
WeekTool/PracticeProductLink to Compleation
6Electronics DesignTank Main Board✔️ First Board Iteration
7CNC MillingAquaponics Tank Plywood Structure✔️ Plywood Tank Structure
11InputsTemp/Humidity Sensor✔️ Temp/Humidity Board
13OutputsLCD✔️ LCD Breakout Board
14Networking & CommunicationLCD Interface & Tank System Communication✔️ LCD Interface & Communications
+

Tank Render - Fusion 360

+ + +

Electronics System

+

Main Board Eagle Prototyping and Design

+

I started this final project board design process following the same steps I used while making any other board in the previous weeks of this class, beginning the process by creating a schematic of my board. Throughout the schematic creating process, I referenced a prior Fab Academy final project, the Sashakit “an Arduino compatible, fabbable board, and also an improved version of Fabkit.” (Quote from the Sashakit, as well as one of my professors, Dr. Adam Harris, In-Circuit programmer board I made in my week 4 class. I found that looking at the labeled schematics and board designs for these boards give me the information I needed to dive deeper into the workings of a microcontroller. Before starting any work in Autodesk EAGLE, I took some time to write out my needs on a final project board and was able to use all of this pre-work to start creating my final project board schematic. I worked through this schematic system by system, adding components as I went until I had a finished schematic. I found this approach to be more assistive in my understandings of how the components interact with each other and also allowed me to do some additional research on the different needed sections while working on them. I finished all major sections of my schematic following this approach, and the only editing that I had to go in to add later were 0-ohm jumper resistors, a component I used to connect traces at intersections while designing my final project board.

+

+

With this schematic done, I moved onto generating my final board file, however, I wasn’t entirely happy with the default generation for my final project board, as I wanted it to have a little more flair, so I set off to add a custom board shape, and some icons to my board design before routing any traces on the board. After doing some research on this process, I found this good article on Importing Custom Images into Eagle, a process easier said than done. Although this process of importing images and shapes into EAGLE is entirely possible, the software cannot handle the importing of any closed polygons or shapes with complex curves of any kind, and therefore any images/icons you want to be imported require a good bit of prep work. I completed this prep work in Inkscape on my desired board shape, a fishbowl, as well as on a little fish icon, and a TW logo for some more personalization. The prep work process for all these images was the same in Inkscape, starting with tracing the bitmaps into SVGs of these images after opening them in Inkscape. This step was to dive by right-clicking on the imported bitmap and selecting the Trace from Bitmap tool. This process generated a scalable vector graphic of the imported bitmap, that I could then start to alter so it could be imported into EAGLE. My workflow of altering this graphic started by resizing the Inkscape page by going to

+
File > Document Properties
+
+

and selecting the Resize page to drawing or selection button under the Custom Size window. After this, I converted the graphic into a path by selecting Object to Path under the Path tab and the top of the Inkscape GUI, and ungrouped all parts of this path by selecting everything in the design, right-clicking, and then selecting Ungroup. Nextly, I navigated to

+
Extensions > Modify Path
+
+

Where I used the Add Nodes tool to add excess nodes to my design. Under this same menu, I removed any complex curves that EAGLE would not be able to handle by using the Flatten Beziers tool with a flatness value of 8. Finally, to address EAGLE’s inability to import closed polygons, I used the Division tool under the Path menu to cut any closed polygons in half, and then separated all of these nodes by using the Break Apart tool under the same menu. This process adds some unwanted dividing lines, that can be removed once imported into EAGLE, but these lines allow for the successful importing of these shapes in the first place, so are required.

+

+ +

+

Once all this pre-work in Inkscape was complete, I saved each of these altered files as a .dxf file to my computer from Inkscape. To import these .dxf files into EAGLE, I found making use of the import-dxf plugin for EAGLE worked the best. After installing this ULP into EAGLE following the instructions provided by the import-dxf Github, I opened my final project generated board file in EAGLE and ran the command

+
run import-dxf
+
+

This line opens the import-dxf interface, where the desired .dxf file can be selected, and then imported into a specific layer of a board design. I imported my fishbowl .dxf into layer 20 of my design, the Dimensions layer, to replace the generic rectangle board profile, and then ran the same command to import both the fish icon, and the TW logo, but to the Top layer over the Dimensions. Because these shapes are on the Top layer of my board design, they will both be milled as traces instead of being included with the outline shape of the board. With all of this importing work done, I had a template to start routing the traces of my board around. I opted for the hand routing process for my final project board, as after attempting to autoroute a couple of times, I just wasn’t happy with the results. This also gave me the ability to utilize parts like 0-ohm resistors as jumpers over weird interceptions of the board, a technique I took advantage of while routing the traces on this board. This hand routing process was relatively tedious and pretty time-consuming, but I think the results look great and work much better than the autorouter generated traces with the weird constraints put in place by the odd board shape.

+

+
+

Final Project Board With All Trace Routed, and Icons Imported
+

+

Unfortunately, this first iteration of my final project board didn’t work on the first attempt, and I seemed to have an excess of noize across it, probably due to the large quantity of 0-ohm resistors in line with the traces.

+

I kept iterating on this board, working towards the removal of the excess 0 ohms with better trace routing, an aspect of my board design that only improved over the iterations. On the sixth iteration of this board, shown in the schematic and board file images below, I had reduced the number of 0 ohms’s on my board by a good bit, but also worked on the smoothing capacitors routing.

+

+

+
+

Sixth Itteration Fish Bowl Board Schematic
+

+

+
+

Sixth Itteration Fish Bowl Board File
+

+

+

Just like its prior iterations though, this sixth iteration was still unsuccessful, and so far I had been unable to get a version working. I began to make some larger strides on this board during my week 13 class and started another iteration of this board by working back through all systems in the board’s schematic, familiarizing myself with the purpose of each system/component. From here, I started work on adding in a 12v to 5v voltage regulator on the fishbowl board, as my fish tank will be run on a 12v DC supply, and this regulator will allow for this 12v system. I took advantage of a 12v to 5v voltage regulator in this regulator system, opposed to a variable voltage regulator, as the components on my fishbowl board will always be 5v, and thus the voltage can be set by the regulator itself. Along with this regulator, I included two 10uf capacitors between GND and VCC on the voltage regulators’ input and output, reducing portion noise on my board.

+

+
+

Seventh Itteration of the Fish Bowl Main Board Schematic
+

+

With the addition of these new voltage regulator systems, a rewiring of the fishbowl board was needed, and during this time around, I took some extra time to reduce the needed components on the board, and allow for easier soldering. After messing around in the board file for a bit, I found I could use one of my “Fish Bowl” shape decorative traces as a GND line that surrounded the entirety of my board. This line implemented a universal ground way more accessible from everywhere on the board and offered another solution to a problem I had previously addressed by using 0-ohm jumpers resistors to solve, routing GND lines over other traces with these “jumpers”. This surrounding ground line reduced the crazy amount of 0-ohm resistors on my board and made the board’s soldering/stuffing process simpler and less time-consuming.

+

+
+

Seventh First Itteration of the Fish Bowl Main Board
+

+

I milled this board following the same steps I used while milling boards previously in week 4, and other electronics production assignments in my previous weeks, starting with the board traces operation with a PCB engraving bit. While iterating on this board since week 6, I’ve milled a handful of tests, some yielding successes in some systems, but always including another issue/broken system to flatten out. I next moved on to milling the board’s holes and outline, using a 1/32” bit for these two operations, before moving onto board soldering.

+

The part reduction steps I had taken earlier while encluding the wrap-around GND trace on my board paid off here, and the boards soldering and stuffing time had been greatly reduced. One thing that I’ve picked up on while soldering prior test fishbowl boards is a “drag” technique while soldering the ATMega to the board. I used this technique on these boards as well, first aligning the chip to its corresponding pins (ensuring correct orientation), before tacking down one of the pins of the chip to the board, and then dragging the tinned iron across one of the rows of pads, allowing solder to flow and connect between each of the chips pins and the board’s pads, and leaving me with a clean set of joints. With the chip soldered, I wrapped up the board by soldering all of the other components, finishing with the three largest components, the crystal, voltage regulator, and reset button, leaving me with the boards show below.

+

+
+

Seventh Itteration of the Fish Bowl Main Board Stuffed & Soldered
+

+

As the first test for this new fishbowl board, after soldering, I hooked up the board to a 12v DC power supply, and then probed the new voltage regulator output to test the output voltage. To my luck, the regulator yielded a 5v output and remained at a constant voltage throughout my probing. I next moved onto attempting to burn a bootloader to the fishbowl board. This process is similar to the one taken while burning the bootloader on my Sashakit made in our Student Bootcamp class. The bootloader is burned to my board through an Arduino acting as an ISP programmer, so the first step while attempting to burn the bootloader was to set up the Arduino. I uploaded the Arduino example skets, Arduino as ISP, to an Arduino board, and referencing the Arduino as ISP sketch, and my fishbowl board eagle file, I attached the two with some jumpers using the pins called on by the Arduino as ISP sketch. From here, I opened the Arduino IDE, selected the port my Arduino acting as a programmer was connected to, and then used the IDE’s Burn Bootloader tool, and … it failed. Unfortunately, even after triple-checking my wiring, switching my Arduino board, reuploading the Arduino as an ISP sketch, and ensuring my fishbowl board was receiving enough power from my power supply, I still received an error message upon any attempt to burn a bootloader. I tried this process a couple of times and received two different error codes, one of which is shown below.

+

+
+

One Error Message Recieved Upon a Bootloader Burning Attempt
+

+

A bit of research into the codes I was receiving helped me break down the problem a little bit, but not a ton. I found that there were three major potential flaws, however, a board short / VCC error, a “bricked” chip, or a non-oscillating clock, and decided to look into these three more. Being the easiest resolved I began looking into the VCC error a little more, starting by probing each of my board’s traces for continuity between places that should not be connected, and also probed the voltages of the GND and VCC line of the board when supplied power, however, all of these results looked normal. After a little more research of the matter online, as well as into the ATMega 328p, I noticed that it recommended to included a smoothing 100nf capacitor in between the chips AREF line and its nearest GND pin, so back in the board’s eagle file, I added this smoothing capacitor along with the previously existing 100nf smoothing capacitor running between two GND and VCC pins on my chip.

+

+
+

Eight Itteration of the Fish Bowl Main Board With Smoothing Cap
+

+

From here, I also explored the other two potential causes of my bootloader burning issue, however, I found no easy fix to a “bricked” chip, and therefore moved on researching a non-oscillating clock a little more. I found that similar to the “bricked” chip, a non-oscillating clock is not easily fixed, but can be avoided, as the clock will not turn to its non-oscillating state unless the fuses of the ISP programmer are incorrectly set. Confirming in my ISP programmer, I had the correct fuses set, and eliminated this as being one of my potential issues.

+

As a workaround to my inability to “Un-Brick” a chip, I followed the advice of one of my instructors, Dr. Adam Harris, to test the working of my chip before using it aboard. To do this, I soldered up a Sashakit board, just like I had in my Student Bootcamp class, however, I left the chip unsoldered to this board. I was then able to connect this Sashakit board to an ArduinoISP programmer, and then use it to try out the bootloader on multiple chips. I aligned a chip with its corresponding pads of the sashakit, heald it down in the center with a pair of tweezers, and used the Arduino IDE to burn a bootloader to my board. All this worked! and the Arduino let me burn a bootloader to a couple of different chips, however, I did find some other faulty “Bricked” chips along the way. Despite that, I was left with a couple of chips with bootloaders burned, that I could be sure were working, and was able to eliminate another potential issue on my board.

+

+
+

Sashakit ATMega 328p Bootloader Burning Setup
+

+

With all this troubleshooting into my issues at a stopping point, I was ready to manufacture another board iteration, going first milling the PCB,

+

+

then soldering and stuffing, leaving me with the board shown below.

+

+

Due to my previous failures with this board, I was pretty nervous to test this iteration and started my first just powering up the board through a power supply. Unfortunately, this simple startup produced a cloud of smoke from the top of my board, and the board’s voltage regulator exploded. This error sucked, and ended up stemming from the misplacement of a strip of 5v to 3.3v regulators in a bin meant for 12v to 5v (the ones needed by my board), and the false component use ended up frying one of my boot loaded chips. I unsoldered the remains of the busted voltage regulator, as well as the fired chip, and replaced them with working correct components. I tested the board with power again, this time fortunately without explosion, and was ready to attempt to program the board again. I wired the board to an Arduino with the Arduino as ISP sketch flashed to it, and tried to upload a blink code … no luck. Upon this uploading attempt, the uploading process began, and then returned error 0x0000000, a GND or Reset error.

+

Here I went back to my schematic, and went through the system by system, and referencing the datasheets for major components. I wanted to make sure to include all recommended/required components for components on each of their pins. In addition to what I already had, this included adding a larger smoothing capacitor between GND and VCC, another capacitor between AREF and its closest GND pin, as well as an additional RST pin, without the 100nf inline capacitor. After a board works though with Dr. Adam Harris, I also moved these additional smoothing capacitors closer to my board’s ATMega 328 to reduce any resistance build upon the traces. All of these changes left me with yet another board iteration, shown below.

+

+

Again, I worked through the manufacturing process for this board, first milling, and the soldering and stuffing of the board. All of these board iterations have sped up their production time, as I know have the component layout of the board memorized, and have increased my soldering speed with each iteration.

+

+
+

Working Iterration of the ATMega 328 Fish Bowl Board
+

+

Just like the prior iterations, I wired up this board to an Arduino with the Arduino as ISP example sketch flashed to it, and attempted a bootloader burn, this time, yielding successful results!!

+

Main Board Testing

+

After all of the troubleshooting that went into my fishbowl board, it was super nice to finally have to worry about code. I began this fishbowl board programming process with a simple blink sketch, and uploaded the Arduino IDE Blink example sketch to my fishbowl board via an Arduino as ISP programmer, yielding a successful upload shown below.

+

+

This simple sketch proved the working of my board and flashed my onboard pin 13 LED on my fishbowl board.

+

+
+

Blinky Example Sketch Running on my Fish Bowl Board
+

+

+
+

All Fish Bowl Board Attempts Up to First Working Itteration
+

+

Inputs & Outputs

+

To interface with my Fish Bowl mainboard, I’m planning on reading a temp/humidity sensor as well as a pH probe placed in my tank. The readings of both temperature and humidity are crucial measurements when calculating the toxicity of ammonia in an aquaponics system. Keeping the nitrogen cycle in check is needed for the success of any aquaponics system, and this is why I’ve chosen these inputs to read.

+

PH Sensor

+

A quick beginning note - here’s the pH scale I referenced during the pH work of my tank. Goldfish, the fish that will be using in my ecosystem, thrive at a neutral pH, something between 7 & 8.

+

+

+

+

I settled on using an Atlas Scientific Gravity pH Probe in my tank. This sensor, in addition to coming from a reputable company, allows the probe to be underwater up to its connector indefinitely, an important aspect of any tank probe, as well as for a connection between the probe and its interfacing board constantly, without a need for the probe to be detached when not in use. Before beginning the work on my board to interface with this pH probe and board, I did a bit of research into the operating principle of the sensor, a subject well covered in the Probe’s Datasheet, with some highlights covered below.

+

“A pH (potential of Hydrogen) probe measures the hydrogen ion activity in a liquid. +At the tip of a pH probe is a glass membrane. This glass membrane permits hydrogen +ions from the liquid being measured to defuse into the outer layer of the glass, while +larger ions remain in the solution. The difference in the concentration of hydrogen ions +(outside the probe vs. inside the probe) creates a VERY small current. This current +is proportional to the concentration of hydrogen ions in the liquid being measured.” (Probe Datasheet)

+

+

+

+

“A pH electrode is a passive device that detects a current generated from hydrogen ion +activity. This current (which can be positive or negative) is very weak and cannot be +detected with a multimeter, or an analog to digital converter. This weak electrical signal +can easily be disrupted and care should be taken to only use proper connectors and cables … The current that is generated from the hydrogen ion activity is the reciprocal of that +activity and can be predicted using [the equation pictured below]” (Probe Datasheet)

+
\[E = E^0 + \frac{RT}{F}\ln(\alpha_{H+}) = E^0 - \frac{2.303RT}{F}pH\]
+
    +
  • Where R is the ideal gas constant
  • +
  • T is the temperature in Kelvin
  • +
  • F is the Faraday constant
  • +
+

+

+

+

To calibrate the pH probe to work against these voltage reading thresholds, a calibration is required. Included along with the Atlas Scientific pH probe I ordered were three packs of pH calibration liquids, a 4, 7, and 10 level liquid. The concept of the calibration reflects measurements of the probe at each of these points, and the probe can be calibrated by being placed in each one of these liquids and then set to the liquid’s corresponding pH value, leaving a scale like that shown below.

+

+

To achieve probe calibration and eventual readings from the probe, I created a simple analog sensor interfacing board with an ATTiny 1614 microcontroller. This board includes data pins to connect to the pH probe board array, along with transmitting and receiving pins that will be used for the eventual calibration of the probe.

+

+ +
+

ATTiny 1614 pH Probe Interfacing Board Eagle Files
+

+

This board is designed to reflect the exact board footprint of the pH sensing board, as well as its electrical isolation board used between the connection. Speaking of which, this electrical isolation board is a pH probe board accessory from Atlas Scientific that connects between the Atlas Scientific pH board and my analog interfacing board, allowing for complete isolation of analog signal from the pH sensor board, and its conversion into a PWM signal. This isolation board removes the issue of false readings that could occur from excess current in the water released by my tank’s pump and leaves me with accurate pH readings. After the completion of my analog interfacing board, I milled and stuffed the board with its four components, leaving me with the board shown below.

+

+

With this board manufactured and ready to program, I again referenced the pH Probe’s Datasheet, to find the Atlas Scientific Gravity pH Library & Sample Code. This library adds a ton of helpful calibration and probe reading functions to Arduino. I ended up modifying the pH calibration example sketch included in this library for my final pH reading sketch, due to the inclusion of the entire calibration via serial feature as well as integration with my electrical isolation board. After this calibration function, the pH itself is read, and then written to a string and printed to serial. In front of this pH value, the line 3pH = is printed, to interface with my tanks LCD parsing statements, a function of code discussed later on this page, leaving me with the pH reading and printing loop function below …

+
void loop() {
+  if (Serial.available() > 0) {                                                      
+    user_bytes_received = Serial.readBytesUntil(13, user_data, sizeof(user_data));   
+  }
+
+  if (user_bytes_received) {                                                      
+    parse_cmd(user_data);                                                          
+    user_bytes_received = 0;                                                        
+    memset(user_data, 0, sizeof(user_data));                                         
+  }
+
+  Serial.println("3pH = " + String(pH.read_ph(), 2));                                                      
+  delay(20000);
+}
+
+

This loop with my pH reading string was then incorporated back into the other functions of my pH code, and I was left with the final code below.

+
#define USE_GRAV_ISOLATOR
+#ifdef USE_GRAV_ISOLATOR
+  #include "ph_iso_grav.h"       
+  Gravity_pH_Isolated pH = Gravity_pH_Isolated(10);         
+#else
+  #include "ph_grav.h"             
+  Gravity_pH pH = Gravity_pH(10);   
+#endif
+
+uint8_t user_bytes_received = 0;                
+const uint8_t bufferlen = 32;                   
+char user_data[bufferlen];                     
+
+void parse_cmd(char* string) {                   
+  strupr(string);                                
+  if (strcmp(string, "CAL,7") == 0) {       
+    pH.cal_mid();                                
+    Serial.println("MID CALIBRATED");
+  }
+  else if (strcmp(string, "CAL,4") == 0) {            
+    pH.cal_low();                                
+    Serial.println("LOW CALIBRATED");
+  }
+  else if (strcmp(string, "CAL,10") == 0) {      
+    pH.cal_high();                               
+    Serial.println("HIGH CALIBRATED");
+  }
+  else if (strcmp(string, "CAL,CLEAR") == 0) { 
+    pH.cal_clear();                              
+    Serial.println("CALIBRATION CLEARED");
+  }
+}
+
+void setup() {
+  Serial.begin(9600);                            
+  delay(200);
+  Serial.println(F("Use commands \"CAL,7\", \"CAL,4\", and \"CAL,10\" to calibrate the circuit to those respective values"));
+  Serial.println(F("Use command \"CAL,CLEAR\" to clear the calibration"));
+  if (pH.begin()) {                                     
+    Serial.println("Loaded EEPROM");
+  }
+}
+
+void loop() {
+  if (Serial.available() > 0) {                                                      
+    user_bytes_received = Serial.readBytesUntil(13, user_data, sizeof(user_data));   
+  }
+
+  if (user_bytes_received) {                                                      
+    parse_cmd(user_data);                                                          
+    user_bytes_received = 0;                                                        
+    memset(user_data, 0, sizeof(user_data));                                         
+  }
+
+  Serial.println("3pH = " + String(pH.read_ph(), 2));                                                      
+  delay(20000);
+}
+
+

I uploaded this code to my analog pH interfacing board, and connected the board’s communication and power headers to their corresponding pins on an FTDI chip, and then via USB to my laptop.

+

+
+

pH probe wired to all three pH interfacing boards
+

+

Following the calibration steps discussed a bit above, I completed the pH probe’s first calibration starting with the 7 mid calibration liquid. I placed the end of the probe in the liquid, and set the command CAL,7 via serial to my analog pH interfacing board. After this, I repeated this step with the 4 low calibration liquid with the line CAL,4, and for a final time with the 10 high calibration liquid, and the line CAL,10.

+ + +

Temp/Humidity Sensor

+

Following my system’s PH sensor, the next sensor in my system is a DHT11 Temperature and Humidity sensor I manufactured in my week 11 inputs class. For space reasons, this page just covers the highlights of the working temperature/humidity monitoring board, and the troubleshooting behind it can be read about on my week 11 inputs class page.

+

I began this temperature monitoring board manufacturing process by reading up on the DHT11 temp and humidity sensor, finding this link on its set up with an Arduino helpful, giving me some useful wiring diagrams. I planned on using this DHT11 Temperature and Humidity sensor on my temp and humidity monitoring board, reading the sensor through an onboard ATtiny 412, and writing the read values to another microcontroller through RX and TX pins. I started in EAGLE, creating a small ATtiny 412 circuit, incorporating a DHT11 Temperature and Humidity sensor, as well as some headers for programming and communication. First, I started a new schematic for the board, keeping in mind the required components while adding them to the schematic. The DHT11 sensor is a four-pin sensor with a VCC, GND, and two data pins. In my case, I communicated with the sensor via one data pin, keeping in mind this pin not only needed to be connected to my ATtiny 412 but also ran to VCC with a 10k ohms resistor. Along with its connection to this sensor, my board’s ATtiny 412 also runs to two separate headers, one for programming via the board’s UPDI pin, and another supplying the board’s power, and RX and TX pins, for future communication between boards. A final little touch to this schematic was a power indicating LED, and this left me with the schematic shown below.

+

+

The next step in this board design process was the conversion of my schematic into my temperature and humidity reading board. Keeping with the aquarium theme, I decided to shape this board like a fish, following the same steps I used creating a custom board shape in eagle during week 6. For this board outline, I used the same fish icon I had created in that week’s assignment, importing the shape into the dimensions layer of my sensor board file to create the shape. From here I began the process of laying out the board’s components, in eagle, starting with the mounting position of the DHT11 sensor and ATtiny 412, and positioning around those two components. One thing I had to keep in mind during this layout is the size of the DHT11, as its connection to my board using a 4 pin header, but the body of the sensor itself was much larger than this. I began by positioning the header where I thought the rest of the sensor would fit, and checked this positioning by drawing a square from the corner of the header, so its dimensions were the size of the DHT11. From here I laid out all other components on my board and routed the board’s traces. Due to the small size of this board, the trace routing was a relatively simple task and left me with a nice-looking temperature and humidity monitoring fish board.

+

+
+

Final Tempeture and Humitidy Monetering Fish Board
+

+

+ +
+

Fish Tempature and Humidity Sensor Board
+

+

Similarly to before starting my board’s schematic, before programming the board, I read up on the DHT11 sensor, referencing the same site as before, found here.

+

“The DHT11 detects water vapor by measuring the electrical resistance between two electrodes. The humidity sensing component is a moisture-holding substrate with electrodes applied to the surface. When water vapor is absorbed by the substrate, ions are released by the substrate which increases the conductivity between the electrodes. The change in resistance between the two electrodes is proportional to the relative humidity. Higher relative humidity decreases the resistance between the electrodes, while lower relative humidity increases the resistance between the electrodes. The DHT11 measures temperature with a surface mounted NTC temperature sensor (thermistor) built into the unit.” (Circuit Basics)

+

The sensor relies on the DHTLib Arduino library and can read the sensors data pin into a readable humidity and temperature output. The libraries follow the equation below to derive the relative humidity from the sensor.

+
\[RH = (\frac{\rho_w}{\rho_s}) \times 100\%\]
+

Where: +- \(RH\): Relative Humidity +- \(\rho_w\): Density of water vapor +- \(\rho_s\): Density of water vapor at saturation

+

With knowledge of the DHT11’s workings, I began programming the code to read temp and humidity through the DHT11 and print the found values through TX to a serial monitor. I used the sensors reference code found on the Circuit Basics code as a reference and began the coding process. The code starts by including the needed DHTLib library and setting up the DHT11’s connections to my boards ATtiny 412. Next, the code begins serial and then prints the found values to it, with the prefixes “Temp” & “Humidity” in front of their corresponding values. This left me with the sensor reading code below.

+
#include <Arduino.h>
+#include <dht.h>
+
+dht DHT;
+
+#define DHT11_PIN 4
+
+void setup(){
+  Serial.begin(9600);
+}
+
+void loop(){
+  int chk = DHT.read11(DHT11_PIN);
+  Serial.print("Temp = ");
+  Serial.println(DHT.temperature);
+  Serial.print("Humidity = ");
+  Serial.println(DHT.humidity);
+  delay(5000);
+}
+
+

At the end of this sensor reading loop, I included a 5-second delay, as the DHT11 can only be read every 2 seconds, so this gives the sensor plenty of wiggle room.

+

With this code flattened out, I wired my fish sensor board to my in-circuit programmed, made in week 4’s electronic production class, connecting my boards GND and VCC to the programmers corresponding pins, and my sensor boards UPDI to the UPDI pin of my in-circuit programmer. I followed the same programing steps I’ve used for all of my other ATtiny 412 containing boards, like that in week 4, uploading my sensor reading code through my in-circuit programmer using the Arduino IDE’s Upload Using Programmer tool. This code was uploaded successfully to my board, and I was ready to move on to testing and reading my sensor.

+

I attached this fish board to an FDTI chip, RX to TX, TX to RX, and GND to GND, and then attempted to read the sensors values again, and this time, it worked! Every five seconds the serial monitor is updated with reading temperature and humidity values from my DHT11, and output the values in a serial monitor through serial, shown below.

+

+

The final step in this sensor board code was a little bit of alteration to the serial prints I did in my week 14 class. These alterations allow for the communication required to print the transmitted values to a specific line on my system’s LCD, a feature discussed later on this page. Beginning this process, I had to figure out how to print a specific line of incoming serial to a controlled position on an LCD. After a bit of research online, It seemed like the best way about doing this would be to include an indicator between different lines being sent over serial, and then read that indicator to determine the position of the text in the interface. With this idea in mind, I altered my input board’s code to include a 1 before the transmitted temperature line, and a 2 before the transmitted humidity.

+
//(c) Teddy Warner 28 May 2021
+
+//This work may be reproduced, modified, distributed, performed, and displayed 
+//for any purpose, but must acknowledge the Assistive Aquaponics Fish Tank. 
+//Copyright is retained and must be preserved. The work is provided as is; 
+//no warranty is provided, and users accept all liability.
+
+#include <Arduino.h>
+#include <dht.h>
+
+dht DHT;
+
+#define DHT11_PIN 4
+
+void setup(){
+  Serial.begin(9600);
+}
+
+void loop(){
+  int chk = DHT.read11(DHT11_PIN);
+  Serial.print("1Temp = ");
+  Serial.println(DHT.temperature);
+  delay(5000);
+  Serial.print("2Humidity = ");
+  Serial.println(DHT.humidity);
+  delay(5000);
+ }
+}
+
+

LCD

+

I spent the better half of an afternoon during my week 13 output class searching for the perfect LCD for my tank, as the space requirements of the mounting area, along with the need for a 20 x 4 LCD minimum to display all necessary info in a readable format made this search more difficult. Eventually, I stumbled on the C204C-FTW-LW65 from Focus LCDs, a vertical compact 20 x 4 LCD, that works great for my use case. I originally began work on my systems LCD during my week 13 output class, as mentioned above, and just like the previous temperature and humidity monitoring board, this page will just touch on the working highlights of this feature, and my week 13 page goes into more depth on the troubleshooting behind it.

+

This LCD met all of my space and screen size requirements, and I began in Eagle creating a breakout board to the screen. This board will mount to the LCD, and break out the screen pins to the headers that will need to connect to a microcontroller. The premise of the breakout board was pretty simple, and I referenced the C204C-FTW-LW65’s datasheet and attached the needed data lines from the LCD, DB4-DB7, to a set of breakout pins on the board. I next hooked up the LCD’s VCC and GND, as well as its backlight VCC and GND, to their corresponding power headers on the board. A 330 ohms resistor is included on the LCD backlight’s VCC line, to dim the light so the screen’s content is readable. I found the 330 resistance value worked better than the standard 220 for my C204C-FTW-LW65 greyscale style screen and allowed for better readability. The final portion of this board was a contrast adjusting 10k ohms potentiometer. This potentiometer provides a voltage to the LCD’s contrast adjusting pin, allowing the LCD character brightness to be altered, leaving me with the final schematic shown below.

+

+

With this schematic flatted out, I moved onto converting this schematic into my LCD breakout board. Here I began again by referencing the datasheet, positioning the LCD mounting headers to the correct dimensions on my board to line up with the C204C-FTW-LW65 LCD. From here, I positioned the contrast adjusting potentiometer to mount on the backside of the board, allowing the LCD to mount directly over the breakout board, in the most space-efficient manner. From here, I also positioned the board’s resistor and jumper resistors, and then positioned the breakout headers towards the bottom of the board, before running all of the board’s traces. Due to the mounting position of this breaker board, the shape had to fit in as compact of a space behind the LCD as possible, so I dragged the boards outline down to fit around the boards content, and used Eagles miter tool to round over the board’s corners, leaving me with the boards shown below.

+

+

Due to its single 330 ohms resistor component and three-pin potentiometer, the milling and stuffing of this LCD breakout board weren’t super time-consuming, and I worked through it with relative ease. I first attached the breakout board to the backside of the LCD board, soldering the pins of the LCD to their propper pads on the breakout board. From here I tracked down the 330 ohms resistor, and also soldered the potentiometer in place. Finally, I attached the breakout board headers, that would allow for connections between the LCD and the board, leaving me with the LCD and breakout system below.

+

+ +
+

Compleated LCD and Breakout Board Assembly
+

+

After the completion of my LCD breakout, I began the interface coding process in the second half of my week 13, and then later expanded on the interface in my week 14 networking and communication assignment. The LCD interface code utilizes the Liquid Crystal Library included by default in the Arduino library for LCD control. Breaking down the code, there are two sections of my LCD interface, a boot screen, and a status display screen that will in the future, provide information read from my Aquaponics fish tank’s sensors. I started the interface work on my tank’s boot screen. The boot screen code is a simple display of characters spanning the width of the LCD, with the top and bottom lines being filled with -, and the middle lines displaying the text Aquaponics Fish Tank and … In Development. The inclusion of this boot screen in the Void Setup function of my code shows this boot screen upon bootup of the system, and the function includes a short delay while displaying the screen, allowing all of the input sensors to get started up and begin future communications with the mainboard.

+

+
+

Aquaponics Fish Tank LCD Bootscreen Code
+

+

+
+

Aquaponics Fish Tank LCD Bootscreen Implemented in Void Setup
+

+

From this boot screen, the interface next moves into its status screen display. It was here where I planned on taking full advantage of the 20 x 4 display size of my LCD, and decided to create some custom characters to include a fish icon on my status screen, as well as include an Aquaponics Fish Tank title. I began this status screen creation with some research on the creation of custom characters, where I found this Hackster.io article to give a good description of the topic. The article is linked to an online LCD Character Creator, where a GUI is provided that converts a drawn character into the hex codes that can be written to an LCD in code. I used this generator online to create a 2 x 3 character fish and was able to copy the generated characters generated into my LCD code.

+

+

I began the status screen code in the Void Loop function of my code, where I started by including an Aquaponics Fish Tank title printed at the top of the display. Following this, I called upon each of my 6 custom characters established earlier in the code, positioning each of them to create a 2 x 3 fish icon on the left side of the screen. The final touch to this first iteration of the status screen would be indicators for Temp, Humidity, and PH, each receiving their line in the interface. In this instance of the status screen, these indicators are just text displayed on a set line of the interface, to the right of the fish icon. All of this left me with the first iteration of the status screen, shown below.

+

+
+

First Itteration of the Aquaponics Fish Tank LCD Statusscreen
+

+

I began working from this interface in my week 14 class to include the sensor readings on my interface’s status screen, altering the loop to read transmitting nodes via the software serial library, and print them to their proper interface positions, removing the setup Temp =, Humidity =, and PH = place holders in the process.

+

The bulk of this communication setup work is discussed on my week 14 page, but I’ll touch on the highlights on this function here. As mentioned above, this LCD networking feature in my system uses the software serial library. This library allows serial communications on any of a microcontroller’s digital pins, and allows me to configure two receiving and transmitting pins on my mainboard node, and communicate with bus addresses to my two input nodes. My first step while implementing this library was to set up two communication ports in my mainboard code, one for communication with my systems temperature and humidity input node, and another for the PH input node. Through this setup, I’m able to communicate across all three nodes, passing through these two set communication ports, and will be able to call on specific nodes with their defined addresses, portDHT and portPH.

+

+

+

+

I call upon each of these different nodes throughout my mainboard interface code. Each port is started with the line …

+
 portDHT.listen();
+
+

as in the software serial library can only listen to one transmitting node through one port at a time, and then this data is saved to an incoming string. From here, the data is run through two of my code’s parsing If statements, to determine whether the board is receiving temperature or humidity data, and the base of this, just as before, will print the read values to my LCD.

+

+

This same process is repeated in the Void Loop function, after the listing of portDHT, with portPH. This, just like the previous port, saves read data to a string, and then prints the read data to the proper line of the LCD interface, this time without any If statement due to the singular reading that will be coming in over this address.

+

+

All of these interface features and functions together left me with an LCD interface code capable of reading from transmitting sensor nodes and printing to the LCD, also including a startup routine with an included interface boot screen, as well as the main status screen with a custom fish icon. This final interface code, with all of these features implemented, is included below.

+
//(c) Teddy Warner 28 May 2021
+
+//This work may be reproduced, modified, distributed, performed, and displayed 
+//for any purpose, but must acknowledge the Assistive Aquaponics Fish Tank. 
+//Copyright is retained and must be preserved. The work is provided as is; 
+//no warranty is provided, and users accept all liability.
+
+#include <LiquidCrystal.h>
+#include <SoftwareSerial.h>    
+
+SoftwareSerial portDHT(2, 3);
+SoftwareSerial portPH(4, 5);
+
+const int rs = 12, en = 11, d4 = 10, d5 = 9, d6 = 8, d7 = 7;
+LiquidCrystal lcd(rs, en, d4, d5, d6, d7);
+
+String Temp;
+String Humidity;
+String pH;
+
+byte fishbl[] = {
+  B00011,
+  B00111,
+  B01111,
+  B01111,
+  B00110,
+  B00000,
+  B00000,
+  B00000
+};
+
+byte fishbr[] = {
+  B11000,
+  B11100,
+  B11110,
+  B11110,
+  B01100,
+  B00000,
+  B00000,
+  B00000
+};
+
+byte fishml[] = {
+  B11111,
+  B11111,
+  B11111,
+  B01111,
+  B01111,
+  B00111,
+  B00111,
+  B00011
+};
+
+byte fishmr[] = {
+  B11111,
+  B11111,
+  B11111,
+  B11110,
+  B11110,
+  B11100,
+  B11100,
+  B11000
+
+};
+
+byte fishtl[] = {
+  B00000,
+  B00000,
+  B00000,
+  B00011,
+  B00111,
+  B01111,
+  B01111,
+  B11111
+};
+
+byte fishtr[] = {
+  B00000,
+  B00000,
+  B00000,
+  B11000,
+  B11100,
+  B11110,
+  B11110,
+  B11111
+};
+
+void setup() {
+
+  Serial.begin(9600);
+  portDHT.begin(9600);
+  portPH.begin(9600);
+
+  lcd.begin(20, 4);
+
+  lcd.setCursor(0,0);
+    lcd.print("--------------------");
+  lcd.setCursor(0,1);
+    lcd.print("Aquaponics Fish Tank");
+  lcd.setCursor(0,2);
+    lcd.print("... In Development");
+  lcd.setCursor(0,3);
+    lcd.print("--------------------");
+
+  delay(5000);
+  lcd.clear();
+
+}
+
+void loop() {
+
+ lcd.createChar(1,fishtl);
+ lcd.createChar(2,fishtr);
+ lcd.createChar(3,fishml);
+ lcd.createChar(4,fishmr);
+ lcd.createChar(5,fishbl);
+ lcd.createChar(6,fishbr);
+
+ lcd.setCursor(0,0); 
+ lcd.print("Aquaponics Fish Tank");
+
+  lcd.setCursor(0,1);  
+  lcd.write(1);
+  lcd.setCursor(1,1);
+  lcd.write(2);
+  lcd.setCursor(0,2);
+  lcd.write(3);
+  lcd.setCursor(1,2);
+  lcd.write(4);
+  lcd.setCursor(0,3);
+  lcd.write(5);
+  lcd.setCursor(1,3);
+  lcd.write(6);
+
+ String IN;
+ String readString;
+
+ portDHT.listen();
+ portDHT.print(F("AT\r\n"));     
+
+ delay(100);
+
+while(portDHT.available()) {
+  delay(100);
+  if (portDHT.available() > 0) { 
+    char c = portDHT.read();
+    readString += c;
+  }
+ }
+
+ IN = readString;
+
+ if(IN.substring(0,1)=="1"){
+  Temp = IN.substring(1);
+  lcd.setCursor(3,1);
+  lcd.print(Temp);
+ }
+
+  lcd.setCursor(15,1);
+  lcd.print(" ");
+  lcd.setCursor(16,1);
+  lcd.print(" ");
+
+ if(IN.substring(0,1)=="2"){
+  Humidity = IN.substring(1);
+  lcd.setCursor(3,2);
+  lcd.print(Humidity);
+ }
+
+  lcd.setCursor(19,2);
+  lcd.print(" ");
+
+ portPH.listen();
+ portPH.print(F("AT\r\n"));     
+
+ delay(100);
+
+while(portPH.available()) {
+  delay(100);
+  if (portPH.available() > 0) { 
+    char b = portPH.read();
+  readString += b;
+  }
+ }
+
+ IN = readString;
+
+ if(IN.substring(0,1)=="3"){
+  pH = IN.substring(1);
+  lcd.setCursor(3,3);
+  lcd.print(pH);
+ }
+
+  lcd.setCursor(13,3);
+  lcd.print(" ");
+  lcd.setCursor(14,3);
+  lcd.print(" ");
+
+}
+
+

I updated my Fish Bowl mainboard with this final code, and tested the system with my Temp/Humidity sensor board, (shown in the image below), and … It Worked!

+

+
+

Updated Software Serial Code Running on my System
+

+

Power Supply & Breakout

+

To power, all of these parts of my electronics system & the tank as a whole, I’ll be using a 12v DC power supply with a connection to an AC mains adapter to take wall power. Before doing any powering from this supply, I dialed the output voltage to exactly 12v DC, shown in the image below.

+

+

Water Pump, Tank & Plant Lights

+

All three outputs from this DC power supply run to a power breakout board allowing the four power drawing components to receive power, the Fish Bowl Main Board, Tank Lights, Grow Lights, and Water Pump. This power breakout board directs two of the three 12v DC inputs to its tank lights and mainboard output terminals and steps down the third input to a 5v current to be used in an ATTiny 412 timing circuit. This third line powers the tank’s pump at 5v, as well as the timing circuit, which toggles a MOSFET on and off every 12 hours to allow 12v DC to reach the tank’s plant lights.

+

+
+

Power Breakout Board Eagle Schematic
+

+

+
+

Power Breakout Board Eagle Board File
+

+

I manufactured this board following the same steps as all PCBs previously made for my project and then was ready to begin the programming process.

+

After a bit of research online, indoor plants grown under grow lights thrive under 10 to 14 hours of grow lights, and I took the middle of this for my timer and toggled every 12 hours. The time sketch is super simple, just writing a MOSFET on pin 4 to high for 12 hours, and then low for another 12. The final grow light timer code is included below.

+
//(c) Teddy Warner 28 May 2021
+
+//This work may be reproduced, modified, distributed, performed, and displayed 
+//for any purpose, but must acknowledge the Assistive Aquaponics Fish Tank. 
+//Copyright is retained and must be preserved. The work is provided as is; 
+//no warranty is provided, and users accept all liability.
+
+int Mosfet = 4;
+
+void setup(){
+pinMode(Mosfet,OUTPUT);
+}
+void loop(){
+  digitalWrite(Mosfet,HIGH);
+  delay(43200000);                  // 12hr
+  digitalWrite(Mosfet,LOW);
+  delay(43200000);                  // 12hr
+}
+
+

To test this sketch and board, however, I didn’t want to wait the 12-hour alternating period, so I shortened the time frame of my sketch to only 5 seconds by dropping the delay from 12 hours …

+
  delay(43200000);                  // 12hr
+
+

… to 5 seconds…

+
  delay(5000);                  // 5sec
+
+

I uploaded this test code to my power breakout board, connected it to a voltage input, and probed the output terminal block ports with a multimeter, yielding the 5 seconds on 5 seconds off results shown below.

+ + +

After this successful test, I uploaded the final 12-hour iteration of the code and was then ready to test the breakout in my system, the step where everything when wrong. Although the timing circuit of my power breakout board worked great, the board itself did not. Unfortunately during the design process, I forgot to consider my usage of an inductive load, my water pump. Upon the first bootup of my system with this power breakout board, the circuit worked great, however, after cutting power to the system, my inductive water pump sent a current back into my breakout, destroying the timing circuit of the board. This board design left me facing the problem of always off grow lights after powering the water pump since when power is cut to a motor, like my water pump, the motion doesn’t stop, and the excess current generated needs to go somewhere, in my case, my timing circuit. The fix for this use of an inductive load revolves around the use of a flyback diode, a feature I’m familiar with from use on 3D Printers.

+

With this solution in mind, I began the process of setting up a diode flyback protection circuit around my water pump terminals, starting with some research yielding these two articles, Simple Over-Voltage Protection Circuit using Zener Diodes & How To Prevent an Inductive Load from Damaging Your Power Supply. Based off of the information in the first Simple Over-Voltage Protection Circuit using Zener Diodes article, I began work on modifying my power breakout schematic to include a Zener diode in parallel with the water pumps two terminals, orientated to direct any returning current into ground, and creating a new part of least resistance for the returning current. Due to the spec of the SMD Zener diode that was accessible to me, however, another component was needed, a current liming resistor. This resistor keeps the Zener diode from exploding on my board and reduces the current down to one the diode could handle, in my case I used a 100 ohms resistor. Unfortunately, this implementation only leads to more troubleshooting, as the current draw of the pump during its startup couldn’t be matched with this Zener diode & limiting resistor setup, so after a couple more iterations, I decided to take another approach, based more off of the information in the second article, How To Prevent an Inductive Load from Damaging Your Power Supply. This lead to the implementation of a second flyback diode setup, one where a Schottky diode is run in parallel with the water pumps terminal headers, along with a second in line with the input VCC to the system. In addition to this diode setup, I also completely isolated the voltage regulation setup for my pump, eliminating any connection to the timing circuit, and allowing for complete isolation of my inductive water pump load. In this instance, like the prior, the two diodes prevent a returning current from the water pump from reaching my voltage regulation circuitry, by creating a new path of least resistance to a GND line. This update left me with the board schematic and board file shown below.

+

+
+

Final Power Breakout Board Eagle Schematic
+

+

+
+

Final Power Breakout Board Eagle Board File
+

+

Again, I fabricated this power breakout board, and uploaded my timing sketch to it, as done prior.

+

+
+

Final Fabricated Power Breakout Board
+

+

I then implemented the power board in my tank’s electronics system and booted up the PSU, yielding the results in the video below. All aspects of the electronics system are powered from the breakout, and bootup after multiple power cuts.

+ + +

Tank Structure - CNC Milling & Laser Cutting

+

The wooden tank structure was my first large physical step towards my final project that I completed in my week 7 computer-controlled machining assignment. The structure is documented in-depth on my week 7 page, and again, for space concerns, this page will just tough on the important bits and highlights.

+

Although I created a model of this wooden structure in Fusion 360 while planning the project, I began the structure creation process by starting to put some thought into the manufacturing processes I would need to take to produce this structure in real life. I settled on milling 6 different pieces to assemble the structure, one back wall, two sides, and 3 stacked pieces to make the base of the structure. I planned on creating these parts based on their corresponding components in my Fusion 360 design. For each of these pieces, I created a new sketch on the face of the profile and used Fusion 360’s Project tool found under the Sketch menu to project all desired lines for each part into one single sketch in the Fusion 360 browser. With all of these lines in one single sketch for each of the 6 different components, I could export each sketch from the Fusion 360 browser onto my computer as a .dxf file by right-clicking on the sketch, and then selecting export as .dxf.

+

+
+

Fusion Model with Projected Sketches for Each Component
+

+

Aspire Work & Structure Milling

+

To generated the toolpaths I would later run on a CNC machine from my altered .svg files, I used Vectric CNC’s Aspire CAM software. I firstly created a new file in Aspire, where I was prompted to set a workplane size. The workplane of the ShopBot PRSalpha, the CNC I would be milling on, was 96” x 48”, and my material height was 0.776”, so I entered these value in the prompt accordingly. I then imported each of my .svg files into Aspire, nesting the files as I imported to reduce waste stock material. I spent a little more time with all of my files imported messing with the part orientation to ensure that there would be little stock material waste between my parts. In the assembly of my design, I planned to use milled tabs to align the back piece to the structure’s two sidewalls and created this feature in my design. I then switched over to Aspires 3D View by clicking on the tab labeled 3D View at the top of the interface’s workspace. Here I could start working on generating the different toolpaths for my parts. Throughout this process, I referenced my original Fusion 360 design to determine which sections of each of my files would require different toolpath operations. The first toolpath operation I generated was a Boreing operation that would boreholes for alignment dowels in the base of my structure. This operation was especially needed in my operation as I was not planning on switching to a drill bit anytime throughout the milling process, and this boreing operation allows me to mill these holes with an endmill instead. Nextly I generated Profile toolpaths for all of my parts, outlining each of my files, and cutting them out. I selected all intended profile cuts for each of my files and set the depth of each of these profile cuts to 0.776”, the thickness of my stock material. This cut depth would ensure all of my profile cuts would go all the way through the material. Nextly I selected a .25 Endmill bit for both my profile and boreing operations, as this was the endmill I was planning on using. To prevent the movement of each of these parts after the profile cut completes, I added tabs to the toolpath under the Edit Tabs section of the toolpaths settings. These “Tabs” leave a small bit of stock material connecting the profile to the rest of the stock and holding their position. I found adding 3 to 4 tabs per profile, depending on size and shape, worked best to hold my parts in place. The final tool paths I generated were pocket cuts. These toolpaths remove material to add depth to some of my parts. These pockets were relatively straightforward, as they didn’t require any added tabs because they cont cut through the material, however, I used a different, larger, bit size for these pockets to speed up milling time, so I was required to change that. Finally, before exporting any of these toolpaths, I went back through and turned on “Ramping” for each operation. This setting diagonally moves the Endmill bit, as opposed to vertical plunges and then horizontal movement, allowing the side of the endmill to cut material as intended. I saved all of these toolpaths from Aspire through the Save Toolpath menu. Each of these toolpaths was saved under the Shopbot format, .sbp, and grouped based on the bit used. This left me with 3 different .sbp files for each a 1/8”, 1/4”, and 1/2” bit.

+

+

+

I began by prepping my machine with the proper bit and mounted my stock to the machine’s bed. My classmate Graham Smith and I did all of our millings this week together as the operation of a machine on this scale can be unsafe if the right precautions are not taken, or if operated alone. I loaded my first tool path, my 1/2” pocket operation, into the Shopbot Command Console through the Load File menu under the File tab. I selected my .sbp toolpath, and under the pop-up menu, selected 3D Offset as the operations offset, and then proceeded to click enter to start the cut. This “3D Offset” allows for me to complete an “Air Cut” of my file, where the tool doesn’t engage with my stock material but just runs above it. I ran this Air Cut to ensure my part placement fit with the side of the machine’s workspace limits, and also didn’t run into any screws. This test of cutting air was successful, so I was ready to move onto the real milling operation. I loaded the file into Shopbot Command Console following the same steps taken previously, but this time I left the offset dropdown set to the default No Offset as opposed to 3D offset, so the machine would run another air cut. Before running this operation, I ensured the dust collection was turned on, and then clicked the enter key to start the operation.

+

The assembly and post-processing work on this wooden tank structure is well documented on my week 7 computer-controlled machining class page, and that work will not be documented in-depth again on this page for space reasons, however, the final assembled and post-processed beauty shots are included below.

+

+

+

+
Final Shots of Sanded Fish Tank Structure
+ +

+

After the completion of my tanks structure in my week 7 class, a couple of additions had to be made to allow for the mounting of a mains AC input, my front panel electronics, as well as a mount for my tanks grow lights. Beginning with the mount for my mains AC adapter, I used a paddle bit on my drill to take out some material in the shape of the adapter on the back of the tank, before finishing up the rectangle with a chisel, using the adapter as a reference until reaching a press-fit install. I also drilled two M4 mounting holes with this adapter as a reference, allowing for a secure attachment of the adapter to the frame.

+

+
+

Mains AC Adapter and Switch Box Mounting Hole
+

+

The cables from this adapter are routed through this pocket on the back of my tank, and into the underside electronics enclosure before the adapter is mounted to the frame with M4 bolts, as shown in the images below. Note these M4 screws were just a place holder and were replaced with some nice matching matte black M4 bolts, allowing for a cleaner adapter look.

+

+

+

The next addition to my plywood tank structure is the grow light mounting bard, which ran across between the two highest points of my structure. This bar is made out of some scrap plywood from my tanks CNC job, and I ripped it down to size on a table saw, before tweaking the height to match that of its mounts on a bandsaw. I used the same tactics sued to assemble the tank structure to mount this light bar, with something dowels for alignment, with wood glue to hold. Underneath this bar, on the side that will be facing the eventual plant box, I mounted three rows of Grow Light Strips, a strip of special neo pixels with light frequencies meant to help plant growth. In my particular case, I used 4 red : 1 blue strip, and alternated the mounting of the three rows to offset this pattern.

+

+

The final modification needed on my plywood tank structure was a pocket allowing for the mounting of my front panel’s electronics, a switch, and my tank’s LCD. I spent a while pinpointing the location and sizing of this box, as to not interfere with the lip holding the acrylic panel over my electronics enclosure, a feature discussed later on this page. I used a speed square and a Metric/Imperial ruler to draw out a pocket large enough to fit the two needed components, with some additional space for wiring, considering the restrictions of the area throughout.

+

+
+

Front Panle Electronics Clearence Hole Layout
+

+

I then followed the same approach taken to remove material for my mains AC adapter mount, starting by removing material with paddle bits and my drill, before cleaning up with a chisel. This hole will eventually be completely covered with the front panel of my tank, so I wasn’t too concerned with the appearance of this one. After all the material was removed, I was left with the clearance hole, shown below.

+

+

Tank Front Plate

+

To cover up this newly carved electronics clearance hole, and also complete the side symmetry of my plywood structure, my next step was to laser cut my tank structure’s front plate. I began this plate in Fusion 360, as even in 2d design, I find myself most conferrable when using Fusion 360’s constraints and sketch interface. I mocked this plate up to hold my system’s LCD and a light power switch over the physical cut clearance hole and then added in some icons to the plate, including a TW logo, as well as a light on and light off icon.

+ + +

I saved this Fusion 360 sketch as a .dxf and imported the file into CorelDRAW, where I could then, following the same process I took in week 3, could run this file on our lab’s Fusion Pro 48 laser cutter. I ran this plate of a sheet of 1/4” plywood, a sheet with matching veneer to that of my plywood structure, allowing the front plate to blend in with the rest of the structure. I wanted to reduce as much laser residue and char on this piece as possible, so I weighted this piece down to the bed of the laser before focusing on the laser, and starting the job. To get a minimal amount of char took a couple of different attempts, but eventually, I was left with the product below …

+

+

… and after a bit of touching up, fit perfectly to the front of my tank, aligning with the electronics clearance hole, and when mounted with some brass furniture screws, left me with the tank front below.

+

+

Electronics Compartment Cover

+

To finish up the fish tank structure that will be the bones of my final project, I laser cut a press-fit acrylic bottom panel for the base of the tank that will cover the electronic housing cutout located on the bottom of the tank structure. I followed the same steps take earlier this week to export the bottom panel as a .dxf file from Fusion, but instead of worrying about toolpaths for this file, I imported the .dxf into Inkscape, where I added in some air holes over the PSU mounting position, as well as a mount for a 40x40 fan for cooling. As a final touch, I included a QR code that links to this project page your reading currently, along with the project name and copyright.

+

+

Next, following the same process I took in week 3, I laser cut this file from 1/4” acrylic on our lab’s Fusion Pro 48 laser cutter.

+

+

Acrylic Fish Tank

+

Being the real centerpiece of my Aquaponics Fish Tank project, the fish tank itself was a special piece to work on, that I spent a good bit of time planning out before its creation. The tank, in its simplest form, is 5 acrylic panels cut on a laser cutter, and bonded together to form the tank itself with WeldOn 16. However just a plain acrylic box like this is a little too basic, and taking inspiration from an assignment completed by 7th-grade engineering classes in our lab, I decided to etch something on the back wall of my tank. This assignment I took inspiration from is a pretty simple project completed by our lab’s seventh graders as an intro to the laser cutter. The gist of the assignment involves laser-cutting and etching an acrylic sign, and then shining a neopixel strip down on the acrylic, lighting up the etched portion of the assignment. My fish tank will include white neopixel light strips on the inside of the tank as accent lighting, and an etched back wall of my fishtank would complement well with these lights, just like the seventh-grade sign project. Although I didn’t know for sure what I would be etched into this back panel yet, I knew I wanted this design to align with the four cutouts on the back wall of my wooden tank structure, so I began this tank design work referencing my physical wooden tank structure. I used Inkscape to create the tank’s basic 5 rectangles, so four outer walls would all sit on a larger tank baseplate. From here, referencing the wooden structure again, I projected the cutouts of my structure’s back wall to the back panel, giving me a workspace to include my future design. During all this Inkscape work, I remembered a conversation between one of my instructors, Mr. Tom Ducick and our Fab group, where Mr. Dubick brought up the inclusion of our Eagle designs outside of circuit boards. For the back panel of my fish tank, I decided on embedding an SVG exported from my first mainboard attempt made all the back in week 6. I used Inkscape’s Intercect tool to crop this SVG down into the back walls four design slots. My final bit of work on this tank design fell on this back panel design again, as after meeting with another one of my instructors Dr. Terence Fagan, the point was brought up that as sweet as the etched original circuit board would look on the back panel of the tank, my final project is a fish tank, something that is more organic and doesn’t make you think of something as modern looking as the sharpe traces of a circuit board. To combat this, I increased the nodes of this back panel design in Inkscape, and then also increased the designs beziers, until left with something still legible as my original circuit board, but less sharp and with more organic curves, leaving me with the finished fish tank design in Inkscape, shown below.

+

+

I exported this Inkscape design as an SVG file, and then opened the file into CorelDRAW, and following the workflow for laser cutting I used in my week 3 class, I ran this tank file on a sheet of 1/4” clear acrylic on our lab’s Fusion Pro 48 laser cutter.

+ + +

This job left me with the 5 panels of my tank, ready to assemble

+

+

As mentioned earlier, for the panel assembly of my tank, I’m using WeldOn 16 to bond all of my acrylic panels together. This bonding process was super time-consuming, as each run with Weldon 16 required a 24 hour set time to completely bond. Despite this long total set time, the work time of WeldOn 16 is only a little over a minute, so all of my application had to be clean and quick. I used a set of two 90 degrees angle clamps to hold my tank’s panels in place while using WeldOn 16 and left the clamps on through the 24 hours set time for each individual corner to allow for a square tank. Below is an image of the last bonding run for the tank’s sidewalls.

+

+

The longes part of the tank assembly was all 4 of the sidewalls of the tank, and after this 4-day process was complete, I followed the same application process for the bottom of the tank, this time holding the pieces in place with some painters tape along all edges, as well as across both pieces, shown below.

+

+

After this final bit of bonding was entirely set, I went to test out by tank, setting the tank up on a sheet of cardboard to see if any water leaked out, and unfortunately, it did … a lot.

+

+

I found that although WeldOn 16 worked great for the structure of my tank, and allowed for a secure bond between all of the tanks panels, it didn’t leave a watertight seal. This problem was simply fixed, however, with the application of silicon to the interior edges of my tank. During this step finding an Aquarium-Safe Silicone is super important, as to not kill any future fish. I found this article on Aquarium-Safe Silicones to be a super helpful reference while picking one of these, and with the article as a reference, was able to pick a tank safe silicone out from my local hardware store.

+

The application process for this silicone was worlds easier than the previous WeldOn 16 work, due to the large working window with the product, and ability to do all of my tanks edges at once. I worked through starting with the four bottom edges of my tank, before tackling the four vertices, and then taking out any high spots on these lines with a popsicle stick.

+

+

The set time for this silicone was only 15 hours, but 24 was recommended on my brand’s datasheet before exposer to water, so after this time, another watertight test of my tank was in order. I set up my silicone sealed tank of some sheets of dry computer paper, to reveal any leaks right away. I stress tested the sealed tank, filling it up to the brim with water, and then allowing the tank to sit for 3 hours, checking back on it around every 30 minutes. This test yielded successful results, with no water on the computer paper, and dry external semes on the tank, leaving with finally, with a fish ready tank!

+

+
+

Water-Tight Test on Sealed Tank - Three Hour Mark
+

+

+
+

BTT Rubber Duckies Enjoying the Water-Tight Tank
+

+

Plant Box - 3D Printing

+

Following the acrylic fish tank, the aqua part of the aquaponics system, it was time to move onto my tank’s plant box, covering the system ponics side. The fabrication of this plant box is one of the additive processes used on my tank, allowing me to incorporate otherwise impossible features, such as embedded water pipes, and nice cable management. Around the time of designing this plant box a nice article from Prusa Research came out on How to Make Prints Waterproof, and its contents gave this additive plant box manufacturing some more light. With the contents of that article, as well as this article on Waterproof Printing from All3D as a reference, I settled on some basic goals for this plant box manufacturing, The box itself will be printed from white PETG. PETG is not only one of my favorite filaments to print in, but also considered a watertight material, and when not colored and left white, is also fish tank safe.

+

With the manufacturing process in mind, I began work on the plant box’s CAD in Fusion 360. The CAD for this part is pretty complex, including not just water routing from a connection port on the bottom of the part, but also a water directing reflow system, and cable management with board mounts incorporated in the design. The part is split into four separate sections, to allow for optimal print orientation on my printer’s bed. These four pieces align with three mounting alignment holes for embedded M3 nuts and bolts, and these same alignment screws are used to secure the four sections together.

+ + +
Final Plant Box Itteration in Fusion 360
+ +

After the completion of all design work for my tank’s plant box, the tedious process of manufacturing was ready to begin. In total, all four parts of the plant box took just over 72 hours to print. Each of the printed parts went through a couple of extra steps in the slicing process, where the plant box’s water tightness would be ensured. The basic premise of a watertight component relates very closely to the density of that component. With this in mind, each of the four plant box parts are sliced with four perimeters, and with a 10% gyroid infill, an infill type with super high strength along with a low weight (less filament usage). Each of these parts runs at a 0.20mm layer height, allowing for a relatively high part density while keeping a lower number of layer lines, places where water could seep into the print. After all of this, I used Prusa SLicer’s ( my slicer of choice) Paint on Supports tool, to add supports to the aspects of my print where they were needed. This step, as opposed to auto-generating the supports allowed me to keep the internal water pipes of my part intact and usable without supports clogging them, shown in the inspected slicing image below.

+

+

This slicing setup process was used across all four parts of my tank plant box, and after a ton of print time, I was left with the task of assembly. As mentioned above, my CAD design for this part includes alignment pins for all four of the plant box’s sections. After cleaning up the supports from each of these sections, I inserted the embedded M3 nuts used in this alignment setup and dry-fit the four parts together with the alignment M3 bolts. After this, I repeated the same assembly process, this time using the same silicone used to seal my tank on all of the inside surfaces on my plant box sections, leaving me with the assembled part below…

+

+

Electronics Mounting & Cable Managment- 3D Printing

+

Following the plant box system of my assistive aquaponics fish tank, a plethora of other small bits and pieces helping with the system integration of my part were also made with the 3d printing workflow. Due to a large number of these small bits, and their relative simpleness, instead of taking a deep dive into the concept behind each design and the manufacturing of the part, I’ll just hit some design concept highlights in this paragraph, and include the CAD and manufacture a part for each piece below. All of these parts are mounts of one kind or another, either press fit for boards, or bolt down for cables/sensors. A lot of these pieces serve as a visual benefit just as much as their purpose of mounting and cable management. With this in mind, many of the pieces are designed with a framing drafted banding around the edge. Inspiration from this angled framing came from my time in my 8th-grade art class, where I was taught the practice of directing a viewer’s vision. The angled frame of a lot of these mounts serves to direct your vision inward, away from the frame itself. In addition to the angled nature of these mounts, any part I’m wishing to achieve this effect with is printed from a black PETG, assisting in the directing of vision away from them, as they don’t ‘pop’. A good example of this effect is seen in the Fish Bowl Board Mount part included below. This mount, for my fishbowl mainboard, uses the framing draft angle technique, along with the black filament color to direct one’s eyes towards the center of the board, where the microchip is located, upon first glance.

+

Fish Bowl Board Mount

+

+ + +

+ +
+

Board Prototype Fitted in Fish Bowl Board Mount
+

+

+

Jumper Cable Mounts

+

+ + +

+
+

12x Jumper Mount On Jumper Cables
+

+

+

PSU Mounting Brackets

+

+ + +

+
+

Power Supply Mounting Brackets After Cleanup
+

+

+
+

Power Supply Mounts Attached to PSU
+

+

+

Wire Grommet

+

+ + +

+
+

Back Wire Grommet After Cleanup
+

+

+
+

Back Wire Grommet Fitted to Wire Passthrough Hole
+

+

+

PSU Terminal Cover

+

+ + +

+
+

PSU Terminal Cover Plate After Cleanup
+

+

+
+

PSU Terminal Cover Mounted to PSU Terminals
+

+

+

Power Breakout Board Mount

+

+ + +

+ +
+

Power Breakout Board Fitted in Board Mount
+

+

+

pH Boards Mounts

+

+ + +

+
+

pH electrical Isolation Board Mounted on Board Mount
+

+

+

pH Probe Mount

+

+ + +

+
+

Assembled pH Probe Mount After Cleanup
+

+

+

Cable Bundle Mounting Clips

+

+ + +

+
+

Cable Bundle Clip Mounted to Back of Tank Structure
+

+

+

System Assembly & Testing

+

Due to the countless design hours spent planning the assembly of my entire tank, this process had plenty of digital references, making the most time-consuming process of the assembly the one, not digitally planned aspect, cable management. I began the assembly process on this task, beginning with the connection of my power supply to mains via the inlaid AC adapter in my tank structure. Due to the dangerously high voltages in these terminals, I followed best practices for the connecting of these wires, using crimp-on connectors for each wire connected to these terminals, as well as grounding the whole system, and including an AC adapter with a 250v fuze.

+

+

+
+

Power Supply Terminal Wireing
+

+

+

As included above, these PSU terminals have a 3d printed terminal cover plate, to provide a little extra protection from any accidental bumps into the terminals. After completing the terminal wiring of the PSU, I installed this terminal cover plate, screwing it down to the wooden plate with M3 screws. I then, with the installed PSU as a reference, found the center of the remaining chunk of the electronics compartment and installed my printed fishbowl main board mount.

+

+

+
+

Begining of System Integraiton - PSU and Main Board
+

+

+

Taking a break from cable management for a bit, It was time to begin the incorporation of my tank’s subsystems into my plywood tank structure, beginning with my acrylic fish tank. The tank was designed to be an exact fit in this plywood structure, and after real-world fabrication, fit in the structure tight enough to stay put without adhesives of fasteners, even when upsidedown. For the installation of the acrylic tank into the plywood structure, I used four wood clamps long enough to span the width/length of both pieces, and then used the clamps to slowly and evenly press the tank into the structure so it sat flush on all four connecting walls. Following this, I then reattached the structure’s laser-cut front panel with some brass furniture screws.

+

+

+
+

Acrylic Fish Tank Installed in Plywood Structure
+

+

+

Next, I began preparing the next subsystem of my tank, the plant box. Due to the predesigned cable management features, the process of including the necessary components here was pretty straightforward. I first routed the tank light strips along their channel, using the zip tie cable mount and cable passthrough to route the cables out the back grommet hole of the plant box. From here I routed a wire that would later be connected to the tank’s water pump and finally moved onto the inlaying of the fishbowl board. Behind this board inlay is a little channel meant for the jumpers to run through while attached to the backside of the board. I connected these jumpers to their pins on my fish board, super glued them in place, and took a picture for reference later, as the wires will next be completely hidden. Then, I pressed the board into its corresponding slot, followed by its press-fit laser cut acrylic cover, and sealed around any gaps with silicon. This left me, as shown below, with a mounted sensor board with only the sensor itself exposed.

+

+

+
+

Bottom of Plant Box After Electronics & Cable Installation
+

+

+

I was then ready to press this plant box into place, with all of its mounted electronics. The plant box follows the same tight press fit mounting principle as the previous acrylic tank, however instead of using clamps, I slowly worked the assembled plant box back into position, until aligned with the acrylic fish tank and plywood structure, shown below.

+

+

+ +
+

Plant Box Installed in Tank Structure Above Acrylic Tank
+

+

+

While pressing the plant box into position, I was sure to route and keep the structures grow light wires in their cable passthrough on the side of the plant box, yielding some nice clean cable routing across the components of the whole system.

+

+

+
+

Grow Light Cables Routed Through Plant Box Cable Passthrough
+

+

+

After the installation of all this, a test of the newly installed lights was in order, and I hooked up both my tank’s grow & tank lights to a power supply set to 12v and turned them on. +

+

+
+

System Grow & Tank Lights Testing
+

+

+

With the bulk of the structure assembly done, I next began the mounting and integration process, starting with the internal tank components like the pH probe. I attached both printed probe clamps to my pH probe and then used some silicone to stick it in the back left corner of my tank, where the probe cable routed up through the plant box, hidden away.

+

+

+
+

pH Probe Mounted in Back Left Tank Corner
+

+

+

Following the pH probe, next up in the tank itself was the water pump. I first connected the pump to the plant box via both a bit of silicone tubing cut to length and then hose clamped and siliconed in place, as well as with connection between the pump’s cable and the router pump wires installed previously. I then used the pump’s two included mounts to stick the pump down to the tank floor with silicone.

+

+

+
+

Water Pump Connected & Installed in Tank
+

+

+

Once all contents of the tank/plant box systems were embedded, I could move on routing their cables down to my structure’s main electronics compartment. I used the 3D-printed cable clips printed earlier to route these cables down the back of my tank, between the two included wire grommets. I wrapped all of these cables with some cable wrap I had laying around from and Ender 3 mod I had done.

+

+

+
+

Cable Routing From Plant Box to Electronics Compartment
+

+

+

These routed cables run through the cable passthrough grommet on the backside of the tank and then slot through their cable route in the PSU terminal cover plate, until out the other side. From here the bulk of cable management began, starting with the power breakout system. I mounted the power breakout board on its 3D printed mount right below the fishbowl mainboard, and then connected the correct incoming and outgoing wires to their connected headers while referencing my eagle power breakout board file. Each of these cables was screwed into their terminal headers before the board was mounted down to the wooden base. From here I began the mounting of the 3 remaining pH boards, two of which sit to the left of my mainboard, and one just off-centered above. These three boards mount via press-fit mounts & their mounting holes, two boards with 3mm holes, and one with 4mm. I then connected the incoming pH probe cable to its terminal, and wired the three pH boards together, before moving onto the main data wiring. Using all of my testing media as a reference, I made the jumper connections for all of the lines in my electronics enclosure, coming from the pH board, temp/humidity sensor, LCD, and light switch, and with that had an integrated system.

+

+

+
+

All Board Mounted Electronics Compartment
+

+

+

Although I had wired all of my connections with mounted boards in my electronics enclosure, I finished the system integration process with some additional cable management via jumper cable mounts.

+

+

+
+

Final Cable Managed Electronics Compartment
+

+

+

… and, with all this assembly & integration work done, it was time for final testing. Below is a successful demonstration of my tank’s electronics system. Just a note, in this video, my tank’s pump is disconnected, because it’s never good to run a water pump outside of water. Instead of leaving this pump connected during the testing of the electronics system, I tested the voltage and current from the pump cables run under the plant box.

+ + +

Finally, came the water test. Despite the number of water tests I had done with the subsystems before, and the proof of their watertight features, this test was damn stressful leading up to it, due to the inclusion of all my electronics in my system on this test. I prepared a spot outside where I could test my tank, and then began preparing the tank, first by filling the plant box with my LECA grow medium …

+

+

… and next, by filling the tank itself. I did this with the help of a couple of my fab groupmates and was able to use a spare water pump and some excess silicone tubing to pump water into my fish tank, without concern of spilling around the base of the tank, where its electronics are located.

+ + +

Unsurprisingly, but to my relief, the filling of the tank went smoothly, and there was absolutely no spillage/leakes anywhere in the whole system, leaving me with the last remaining tank … turning it on.

+ + +

it worked!, upon bootup, water begins flowing through the system, and draining out through the plant boxes draining feature, at the same time as the electronics system is taking measurements from the system and displaying it all of the tanks embedded front LCD.

+ + + + + + + + + + + + + + + + + + +

Comments

+ + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Projects/CornerDesk/index.html b/Projects/CornerDesk/index.html new file mode 100644 index 00000000..32f98d96 --- /dev/null +++ b/Projects/CornerDesk/index.html @@ -0,0 +1,812 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Cable Managed Corner Desk - Teddy Warner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + +
+ +
+ + +
+ + + + + + + + + +
+
+ + + + + + + + + + + +
+
+ +
+
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + +

+ +

+ +

+ + + +

+ +

+ + + + + + +

+ +

+ + + +

+ + +

+ + + + + +

+ + +
+ + + + + + +

Cable Managed Corner Desk

+
+ Teddy Warner's GitHub profile picture Teddy Warner| Spring, 2020 | 6-8 minutes + +
+ +
+

This start of the Pandemic project stemmed from my freshman year engineering capstone project. At the start of 8th grade I had an issue, I was a couple of months into learning Fusion360 and I decided to upgrade my setup from an old janky pc to a brand new laptop and some 20-inch monitors to go along with it, which was great and all but gave me a whole tone of issues to sort out. I had this great desk in the corner of my room, that my great grandfather built and that had been passed down to me, however, there was one huge flaw using that desk with my brand new setup and that was cable management. Below are some plans draw out for the desk.

+

+

Initial design sketches and measurements for corner desk +Additional design notes and calculations for desk

+

+

CAD

+

So I set off in Fusion to design a desk that fit all of my needs. My goals were to have a desk measured to fit perfectly in the corner of my room, with plenty of legroom so I would never hit my knees on it, and the best cable management imaginable. And after a couple of different iterations, I came up with this Model.

+ + + + +

Lowes Trip and New Tool

+

However shortly after finishing up the CAD aspect of this project, Covid-19 caused our school to resort to online learning, cutting me off from our school’s lab equipment, however, I happen to have a lot of the tools we have in our lab in my little workshop at my house, and I was pretty determined to finish the desk, so just before everything closed down I took a trip to lowes to pick up some lumber and supplies. I settled on a frame and cabinet of 2x4s and some plywood that was painted white pretty quickly, but the top took some more thought. Eventually, I decided to make the top out of cedar planks that were stained with Provincial to match the stain of the floor in my room. Also at lowes, I picked up a Kreg-Jig to drill pocket holes for the desk, as I didn’t like the idea of screws being visible. It was my first time using a Kreg-Jig but it went super well as it’s a relatively simple jig to use.

+

+

Kreg Jig setup for pocket hole joinery

+

+

Building Process

+

A couple of days after getting the wood, I finally started assembling the desk, starting by laying down the frame, making sure the front 2x4 was flat and the back was upright (to give more leg space) and I was pretty excited to get the whole thing together. Maybe a little too excited because even though you can’t tell from the pictures above, I built the whole desk wrong, putting the corner on the wrong side. This was a huge setback to fix as it meant that I would have to take the entire desk apart, use wood filler on all of the newly exposed holes, and build it back up the right way. I was even considering just leave the desk as is, and rearrange my room to match the flaw, however, I wanted my desk to be perfect and that meant fixing it according to my CAD model. So I did the laborious tasks listed above and finished the frame of the desk the right way late that night. The next day I was ready to do some more on of the desk, so I decided to start throwing together the top. I spent that whole day sawing, planning, and sanding the cedar before I put the top together using the Kreg-Jig the next morning. And this time I didn’t make a single mistake, and I’m really happy with the result.

+

+

Initial layout of desk frame components +Assembled base frame of the desk +Completed wood assembly before finishing

+

+

Painting and Staining

+

I spent the next two days painting the frame with three coats of white paint and staining the cedar with Provincial.

+ + + + +

+

Cedar desktop after staining with Provincial finish +
+

Stained Desktop
+

+

+

Assembling the Desk

+

As I said earlier the choice of wood for the surface of the desk took some thought, but I’m super glad I went the cedar route. Originally I was planning on using oak, but the cost scared me away to a much better smelling and looking cedar. My only concern with this type of wood was how much it expands and contracts. I was concerned that screwing a cedar top to my frame would just wiggle itself loose over time and cause issues. So I came up with a workaround. Instead of attaching the Cedar top directly to the frame, it’s held by 3d Printed mounts that allow the top to expand and contract a whole lot more without becoming loose. I’m super happy I took the time to figure this one out as it’s going to save me trouble with using cedar in the long run.

+

+

Installed laptop mounting arm with cable management

+

+

Cable Management

+

This is a huge part of the project as it’s one of the reasons I decided to even build a new desk in the first place. After attaching the top of the desk to the frame, I threw on this great air spring laptop mount that routs all of the cables from my laptop through a grommet in the top on the table, down to the corner cabinet. The cabinet is designed with a false top that hides all of the cables (except for two going to the subwoofer on a lower shelf) from inside the cabinet. Then the cables are routed through a hole in the back and ran through 3d printed cable clips on the back of the desk. The cable clips are exactly as long as the trim of our floor is, so they don’t push the desk off the wall any more than it needs to be. All of the cables are then ran to where their needed (i.e monitors, speakers, keyboard, google home, etc.). Finally, all of the power cables are ran back into the top of that corner cabinet and plugged into a built-in power strip, which plugs into a wall socket directly to the left of the cabinet, hiding all the cables from view.

+

+

3D printed cable management clips on desk back

+

+

Final Thoughts

+

I love how my desk came out, It solves all of my annoying cable issues and fits perfectly in the right corner of my room. Eventually, I’m planning on wall mounting my monitors to give me even more desk space, however, the little corner piece gives me a perfect location to use a pen and some paper and write things out. Putting the desk in its place, I realized that there was way too much space above it and after some browsing, I found some shelves on amazon that use the same stain and match perfectly, so I ordered and installed those over the desk. This project we surprisingly smoothly in the face of the current Pandemic, and even with my building setbacks came out well.

+

+

Final beauty shot of completed corner desk installation +
+

Final Desk Beauty Shot
+

+

+ + + + + + + + + + + + + + + + + + +

Comments

+ + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Projects/Impetus/index.html b/Projects/Impetus/index.html new file mode 100644 index 00000000..2374d801 --- /dev/null +++ b/Projects/Impetus/index.html @@ -0,0 +1,1544 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Impetus - Teddy Warner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + +
+ +
+ + +
+ + + + + + + + + +
+
+ + + + + + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + +

+ +

+ +

+ + + +

+ +

+ + + + + + +

+ +

+ + + +

+ + +

+ + + + + + +

+ + +
+ + + + + + +

Impetus

+
+ Teddy Warner's GitHub profile picture Teddy Warner| 2024-2025 | X-X minutes +
+ +
+

Giving agents a temporal understanding through human physiological data.

+
+ +

Introduction

+

There’s a concept in AI research called the World Model, which aims to create neural networks capable of understanding and simulating cause and effect within a temporal and spatial context. Unlike current Large Language Models (LLMs) that primarily predict outputs based on input patterns, world models aspire to simulate both causes and effects based on a deeper understanding of time and space.

+

The key to a world model is its ability to grasp cause and effect, which fundamentally requires a temporal understanding. As it turns out, giving a neural network a temporal understanding is quite challenging. While we can instruct an LLM to output current timestamps or locations, it lacks the ability to truly associate actions and experiences within a relative dimension of time and space as humans do.

+

Current approaches to building world models include aim to provide AI systems with a sense of time and space, but they may be missing a crucial element of human temporal understanding:

+

Imagine a humaniod and a human are sitting together at a table. Suddenly, the human stands up, screams, and hurles a chair across the room. The robot, relying solely on visual input, might respond by moving away. In this case:

+
\[\begin{align*} +x(t) &= \text{scream, chair thrown} \\ +a(t) &= \text{move away} \\ +\end{align*}\]
+

\(x(t)\) is an observation in a given instant and \(a(t)\) is the resulting action in that instant.

+

Now, consider the same scenario, but this time two humans sit together at a table. One human stands, screams, and hurles a chair across the room. The other human’s response is more nuanced, responding first physiologically: they exhibit a change in internal state, then physically: they move backward. In this case:

+
\[\begin{align*} +x(t) &= \text{scream, chair thrown} \\ +a(t) &= \text{physiological state change} \\ +a(t+1) &= \text{move away} +\end{align*}\]
+

Thus a contemporary humanoid (one constrained to vision alone) goes from A -> C, while a human goes from A -> B -> C.

+

Hypothesis

+

World models cannot truly gain a comprehensive temporal understanding based solely on data collected from robots or purely external observations.

+

Temporal understanding cannot be trained from data that goes from A -> C. World models must be trained on data that goes from A -> B -> C.

+

The collection of physiological state data may provide insight into man’s temporal understanding, and thus how to train a neural network to understand cause and effect.

+

Current efforts in world model development often rely heavily on data collected from robotic systems or external observations that we humans can describe (i.e. see chair thrown -> move backward). These observations forgo the subconscious response integral to a human’s actions (i.e. see chair thrown -> physiological state change -> move backward).

+

If we acknowledge that current AI systems lack a subjective understanding of time, why do we primarily use data from these systems to try to instill temporal understanding?

+

Data collected from human subjects could provide a window into how humans subjectively experience time, potentially leading to more sophisticated and human-like temporal reasoning in AI systems.

+

Questions

+
Do physiological states relate to man's perception of time and space?
+ +

Yes, physiological states may be able to provide a window into man’s subjective experience of time. But what physiological states?

+
    +
  • Facial muscle activity: The dynamic electromyographic activity in the corrugator-supercilii muscle over time reflects objective time and predicts subjective judgments of duration 36. This suggests that subjective duration could be an embodiment process based on motor information changing over time and associated feelings.
  • +
  • Heart rate: Pulse rate has been found to correlate with subjective well-being metrics and could potentially offer insights into time perception 37. Additionally, heart rate variability, measured through the Lorenz plot area, has been used in stress estimation models, which could indirectly relate to time perception 38.
  • +
  • Electrodermal activity: Galvanic skin response (GSR) has been used alongside other physiological state measures to assess engagement in activities, which could influence subjective time perception 39.
  • +
  • Brain activity: Neuroimaging studies have explored the neural correlates of subjective time perception, suggesting that specific brain regions are involved in our consciousness of time40.
  • +
+

It’s important to note that the relationship between physiological states and time perception is complex and influenced by various factors, including attention, emotion, and cognitive load 36 41.

+
Why has no one done this?
+ +

When my hypothesis first came to me, I believed I had to be naive to some particular reason our pipeline is robot -> simulator -> robot and not human -> simulator -> robot. It was simultaneously a relief and a pain in the ass to not find a specific reason on the first page of Google.

+

I’ve got a few thoughts as to why no one has done this yet:

+
    +
  1. It’s hard to collect data from humans.
  2. +
  3. The use of physiological state data on mass raises important ethical and privacy considerations.
  4. +
  5. Only recently have advances in transformer architecture made such analysis feasible at scale.
  6. +
  7. Traditional approaches to world models have relied upon absolute temporal markers. Shifting to a subjective, biologically inspired approach would require significant effort.
  8. +
+

Approach

+

So what would such a venture look like? Well, there are a few assumptions for us to start from:

+
    +
  1. +

    As our context window approaches the total size of the internet, the bottleneck will no longer be compute, but rather data. At the last NeurIPS, Ilya Sutskever notes that data is “the fossil fuel of AI”.

    +

    +
    + + +
    +

    +
  2. +
  3. +

    World models are expanding in prominence amongst academics and top research labs.

    +
    +
    +
    +

    Plot of arXiv trends world models over time +Plot of arXiv trends world models over time

    +
    +
    +

    Plot of google trends world models over time +Plot of google trends world models over time

    +
    +
    +
    +
  4. +
+

It is prime time to develop foundational world models, and we currently don’t have enough data, much less granular enough data, to train them. While reviewing the initial draft of this article, Dr. Alexander Titus offered me this thought: “Think of it like filling a mesh. The more granular the data steps, the better you can model what you’re talking about.” If this translates to world models, the A -> B -> C hypothesis will hold.

+

Thus I am building a data acquisition venture focused specifically on physiological data acquisition from humans at scale, in hopes of giving agents a better understanding (human-like that is) of time and space.

+

Collecting physiological data from humans is hard, but at scale, it is even harder.

+
    +
  • Need some minimal physiological state collection hardware
  • +
  • Then need to make a bunch of these devices
  • +
  • Then need to pay people to collect data with said devices for a select window of time
  • +
+

INSERT VIDEO

+

Methods

+

Physiological changes should simultaneously govern rewards and be given to the agent as a state. Correlation may then be drawn between an agent’s actions and physiological state changes.

+

+

Diagram showing RL setup in light theme + Diagram showing RL setup in dark theme

+

+

This calls for a few things …

+
    +
  1. +

    An “Interoception Model”, that is, a neural netowrk which predicts a physlogical change given an external input.

    +
    \[\begin{align*} +\text{Input: } \sigma_t &= \text{state at time } t \\ +\text{Output: } \iota_t &= \text{internal state at time } t \\ +\end{align*}\]
    +
  2. +
  3. +

    A “Temporal Model”, a model to adjust an agent’s “perception of time” (that is with a scalar), in accordance with \(\iota_t\).

    +
    \[\begin{align*} +\text{Input: } &\begin{cases} +\iota_t &= \text{internal state at time } t \\ +\tau_{t-1} &= \text{previous temporal scaling} +\end{cases} \\ +\text{Output: } &\tau_t = \text{temporal scaling at time } t +\end{align*}\]
    +
  4. +
  5. +

    A “Reinforcement Gymnasium”. An agent seeks to maximize rewards. Their actions should be a product of their internal and external states, as well as previous actions/rewards.

    +
    \[\begin{align*} +\text{Input: } &\begin{cases} +\iota_t &= \text{interoception at time } t \\ +s_t &= \text{state at time } t \\ +a_{t-1} &= \text{previous action} \\ +\tau_t &= \text{temporal scaling at time } t +\end{cases} \\ +\text{Output: } &a_t = \text{action at time } t +\end{align*}\]
    +
  6. +
+

Timeline

+
    +
  • Utlize publically available physiological data to asses such data’s ability to provide a window into temporal understanding
  • +
  • Write and train an Interoception Model with the data processed in step 1
  • +
  • Construct a benchmark RL task and set Reinforcement Gymnasium
  • +
  • Write and train an Temporal Model to interpret the Interoception Model’s \(\iota_t\) and provide \(\tau_t\) for an agent
  • +
  • Integrate \(\iota_t\) and \(\tau_t\) into the agents within the Reinforcement Gymnasium
  • +
  • Compare new results (with the interoception and temporal models) to the RL benchmark
  • +
  • Develop minimal, cheap, mass-producible hardware to collect physiological data
  • +
  • Collect data from people at scale
  • +
  • Retrain the Interoception Model with more, and more granular data
  • +
  • Retrain the agents with this superior \(\iota_t\)
  • +
+

Interoception Model

+

I need to gather some resources before I can get started with physiological state data collection at a large enough scale to train a foundational model, I started by toying around with Vision Transformers and some fMRI datasets I found on the internet.

+

The rest of this Interoception Model section is a preprint I wrote while attempting Timeline step one: “Utilize publically available physiological data to asses such data’s ability to provide a window into temporal understanding”.

+

Abstract

+

This research presents a novel approach to understanding temporal cognition through the application of Vision Transformers to functional Magnetic Resonance Imaging (fMRI) data analysis. While current artificial intelligence approaches to world modeling rely heavily on absolute temporal markers and timestamps, human perception of time operates as a fundamentally subjective experience that adapts with cognitive state and learning progress. We look to demonstrate that neural activation patterns captured during learning through fMRI contain rich temporal information that can inform more nuanced approaches to temporal processing in AI systems.

+

By analyzing temporal patterns across multiple learning stages and tasks, we attempt to demonstrate the feasibility of developing AI systems capable of processing time as a relative rather than absolute construct. This work represents an important step toward artificial intelligence systems that can reason about time in ways that more closely mirror human cognitive processes.

+

Background

+

This work implements a Vision Transformer architecture 18 19 optimized for learning stage classification from fMRI data. While fMRI presents known limitations in its reliance on blood-oxygen-level-dependent (BOLD) signals 20, deep learning architectures may be able to extract temporal patterns that traditional analysis methods miss.

+

Neural Bases of Learning Stages

+ +

Human learning progresses through distinct stages characterized by shifting patterns of neural activation 15. These transitions are particularly evident in the striatum and medial temporal lobe regions 16. Our architecture’s design mirrors these biological principles through its progressive processing stages and attention mechanisms.

+

fMRI captures these learning stages through blood-oxygen-level-dependent (BOLD) signals, providing an indirect but reliable measure of neural activity 20. While this indirect measurement presents certain limitations, research has demonstrated correlations between BOLD signal temporal patterns and learning progression 22. The robust test-retest reliability of fMRI in classification learning tasks 14 provides a stable foundation for extracting temporal patterns relevant to learning stages.

+

Deep Learning Approaches to Temporal Understanding

+ +

Recent advances in transformer architectures have revolutionized sequence processing capabilities 18. Our implementation builds on the Vision Transformer architecture 19, but with significant modifications designed specifically for fMRI data processing. These modifications include:

+
    +
  • Custom channel reduction networks that efficiently handle high-dimensional fMRI volumes
  • +
  • Temporal attention mechanisms that incorporate hemodynamic response characteristics
  • +
  • Progressive dropout strategies that maintain signal fidelity while preventing overfitting
  • +
+

Methods

+

Our implementation addresses two core challenges: extracting meaningful patterns from complex fMRI data 22 and developing architectures capable of learning from these patterns 14. This section outlines our approach in three parts: data preprocessing implementation, fMRI-specific augmentation strategies, and temporal-aware transformer architecture design 18 19.

+

Data Collection and Processing

+ +
Dataset Characteristics
+ +

The implementation utilizes four complementary classification learning datasets from OpenFMRI. Each dataset provides specific insights into temporal learning aspects 15. The primary dataset (ds000002) contains data from 17 right-handed subjects performing probabilistic and deterministic classification tasks 16. Task structure includes:

+
    +
  • Pure blocks: 10 cycles of 5 classification trials followed by 3 baseline trials
  • +
  • Mixed blocks: 100 stimuli split equally between probabilistic and deterministic trials
  • +
+

Data acquisition specifications:

+
    +
  • Scanner: 3T Siemens Allegra MRI
  • +
  • Parameters: TR = 2s, 180 functional T2*-weighted echoplanar images per session
  • +
  • Resolution: 2mm slice thickness, 2x2mm in-plane resolution
  • +
  • Enhancement: Multiband acceleration factor of 4
  • +
+

Three additional datasets complement the primary collection:

+
    +
  • ds000011: 14 subjects, single/dual-task classification for attention-modulated learning analysis 16
  • +
  • ds000017: 8 subjects, classification with stop-signal tasks for inhibitory control examination 14
  • +
  • ds000052: Classification with reward contingency reversal for adaptive learning mechanism investigation 15
  • +
+

Preprocessing Pipeline

+ +

Our implementation uses a three-stage preprocessing approach based on established neuroimaging practices 22 with optimizations for temporal pattern preservation. The pipeline integrates spatial normalization and temporal alignment to maintain both anatomical accuracy and temporal fidelity. The complete preprocessing pipeline follows:

+
\[\begin{equation} + x_{\text{processed}} = \mathcal{N}(\mathcal{R}(\mathcal{V}(x))) +\end{equation}\]
+

Where:

+
    +
  • \(\mathcal{V}\) performs dimension validation
  • +
  • \(\mathcal{R}\) applies spatial resizing
  • +
  • \(\mathcal{N}\) implements intensity normalization
  • +
+
Dimension Validation
+ +

fMRI acquisitions vary in dimensionality 22. Our validation ensures consistent dimensionality while preserving temporal information:

+
\[\begin{equation} + \mathcal{V}(x) = \begin{cases} + x & \text{if } x \in \mathbb{R}^{H \times W \times D \times T} \\ + x[..., \text{newaxis}] & \text{if } x \in \mathbb{R}^{H \times W \times D} \\ + \text{undefined} & \text{otherwise} + \end{cases} +\end{equation}\]
+

This validation maintains spatial integrity while ensuring proper temporal dimension handling 20. Single-volume inputs receive an added temporal dimension for consistent processing.

+
Spatial Resizing
+ +

The implementation standardizes spatial dimensions while maintaining anatomical proportions 22 through trilinear interpolation:

+
\[\begin{equation} + \mathcal{R}(x) = \text{zoom}(x, [\frac{H_t}{H}, \frac{W_t}{W}, \frac{D_t}{D}, 1]) +\end{equation}\]
+

Target dimensions \((H_t, W_t, D_t) = (64, 64, 30)\) balance spatial resolution and computational efficiency 14. The temporal dimension scaling factor of 1 preserves original temporal resolution.

+
Intensity Normalization
+ +

Following fMRI preprocessing protocols 22, we implement temporal-aware normalization accounting for BOLD signal dynamics:

+
\[\begin{equation} + \mathcal{N}(x_t) = \frac{x_t - \mu_t}{\sigma_t + \epsilon} \;\; \forall t \in T +\end{equation}\]
+

Where:

+
    +
  • \(\mu_t\) and \(\sigma_t\) represent mean and standard deviation at timepoint \(t\)
  • +
  • \(\epsilon = 1e\text{-}6\) prevents division by zero
  • +
+

This normalization preserves temporal dynamics while standardizing signal intensity across sessions and subjects 20. Independent timepoint normalization maintains relative temporal patterns crucial for learning stage classification.

+

Data Augmentation Strategies

+ +

Our implementation includes a comprehensive suite of domain-specific augmentation techniques designed to enhance model robustness while respecting the unique characteristics of fMRI data. These techniques are validated through neuroimaging research and carefully adapted for deep learning applications:

+
Temporal Masking
+ +

We implement an adaptive temporal dropout mechanism that helps the model learn robust temporal features despite potential signal interruptions or artifacts. The masking strategy:

+
    +
  • Applies random-length masks (1-5 timepoints) to simulate temporal dropouts
  • +
  • Maintains temporal coherence through continuous masking windows
  • +
  • Varies mask duration to ensure robustness to different types of signal interruptions
  • +
+
Spatial Masking
+ +

The implementation incorporates structured dropout in the spatial domain to handle regional signal variations and encourage learning from distributed patterns. Key features include:

+
    +
  • Probability-based masking with empirically optimized threshold values
  • +
  • Preservation of anatomical structure through contiguous region masking
  • +
  • Balance between feature preservation and augmentation strength
  • +
+
Elastic Deformation
+ +

To account for natural variations in brain structure and registration, we apply anatomically-constrained elastic deformations that:

+
    +
  • Preserve biological plausibility through controlled deformation magnitude
  • +
  • Maintain spatial relationships while introducing realistic variability
  • +
  • Apply smooth transformations through Gaussian filtering
  • +
+

Model Architecture

+ +

Our architecture combines Vision Transformer principles with specific adaptations for fMRI data processing. The implementation consists of three primary components, each optimized for the unique characteristics of neuroimaging data:

+
Channel Reduction Network
+ +

The channel reduction component efficiently processes high-dimensional fMRI input through a dual-stage approach:

+
    +
  • Initial dimensionality reduction from 30 to 16 channels
  • +
  • Batch normalization and GELU activation for stable training
  • +
  • Progressive dropout for regularization
  • +
  • Careful preservation of spatial relationships
  • +
+
Temporal Processing
+ +

Our temporal processing incorporates hemodynamic response function (HRF) characteristics 20 through causal attention masking:

+
\[\begin{equation} + M_{ij} = \begin{cases} + -\infty & \text{if } j < i + 3 \\ + 0 & \text{otherwise} + \end{cases} +\end{equation}\]
+

This enforces a 6-second BOLD delay constraint, reflecting established HRF parameters 22 while maintaining temporal causality in BOLD response learning.

+
Progressive Dropout
+ +

We implement a depth-dependent dropout strategy that provides stronger regularization in deeper layers while maintaining high information flow in early layers.

+
\[\begin{equation} + p_i = 0.1 \cdot \frac{i + 1}{12} \;\; \text{for layer } i +\end{equation}\]
+

This strategy:

+
    +
  • Increases dropout probability with network depth
  • +
  • Maintains high information flow in early layers
  • +
  • Improves generalization while preserving low-level features
  • +
+

Training Protocol

+ +
Mixed Precision Training
+ +

We implement dynamic loss scaling for numerical stability:

+
\[\begin{equation} + \text{scale}_t = \begin{cases} + 2 \cdot \text{scale}_{t-1} & \text{if no overflow for } 2000 \text{ steps}\\ + \frac{\text{scale}_{t-1}}{2} & \text{if overflow detected} + \end{cases} +\end{equation}\]
+

This adaptive scaling ensures stable training while maintaining computational efficiency.

+
Optimization Strategy
+ +

The implementation uses AdamW optimizer with fMRI-validated parameters 19:

+
    +
  • Learning rate: \(1e\text{-}4\)
  • +
  • Weight decay: 0.05
  • +
  • Beta parameters: \(\beta_1 = 0.9\), \(\beta_2 = 0.999\)
  • +
+
Learning Rate Schedule
+ +

We implement a custom warmup-decay schedule optimized for fMRI data processing:

+
\[\begin{equation} + \eta_t = \begin{cases} + \eta_{\text{base}} \cdot \frac{t}{t_w} & \text{if } t < t_w\\ + \eta_{\text{min}} + \frac{\eta_{\text{base}}-\eta_{\text{min}}}{2}(1 + \cos(\pi\frac{t-t_w}{T-t_w})) & \text{otherwise} + \end{cases} +\end{equation}\]
+

Schedule parameters:

+
    +
  • Base learning rate \(\eta_{\text{base}} = 1e\text{-}4\)
  • +
  • Minimum learning rate \(\eta_{\text{min}} = 1e\text{-}6\)
  • +
  • Warmup period \(t_w = 0.1T\)
  • +
+

This provides stable initial training followed by gradual learning rate decay for optimal parameter convergence.

+
Regularization and Early Stopping
+ +

We implement comprehensive regularization following established practices 18:

+
    +
  • Label smoothing: \(\alpha = 0.1\)
  • +
  • L2 regularization: \(\lambda = 1e\text{-}4\)
  • +
  • Gradient clipping: norm 5.0
  • +
+

Early stopping criteria definition:

+
\[\begin{equation} + \text{stop} = \begin{cases} + \text{True} & \text{if } \text{val\_loss}_t > \text{best\_loss} - \delta \text{ for } p \text{ epochs}\\ + \text{False} & \text{otherwise} + \end{cases} +\end{equation}\]
+

Parameters:

+
    +
  • Improvement threshold \(\delta = 1e\text{-}4\)
  • +
  • Patience period \(p = 7\)
  • +
+

Results

+

Our implementation demonstrated patterns in learning stage classification from fMRI data, with performance characteristics varying significantly across learning stages. The complete analysis reveals both potential capabilities and areas requiring further refinement.

+

Overall Model Performance

+ +

The model achieved an overall accuracy of 35.6% across four learning stages, with a balanced accuracy of 42.8% and a macro F1 score of 0.407.

+

While exceeding random chance performance (25% for four classes), these metrics highlight the inherent complexity of learning stage classification from neuroimaging data.

+

fMRI data from varaying datasets/test conditions is quite volatile to work with. This research would benefit signficantly from custom/standardized means of physiological state data collection at scale.

+

The Cohen’s Kappa score of 0.093 indicates performance above chance but demonstrates the substantial challenge in achieving consistent classification across all learning stages.

+

+

Training Results plots

+

+Figure 1: Comprehensive model performance analysis showing (A) Normalized confusion matrix demonstrating classification patterns across learning stages, (B) ROC curves indicating increasing reliability from early to mastery stages, (C) Per-class performance metrics highlighting strongest performance in mastery classification, and (D) Prediction confidence distributions revealing distinct patterns for each learning stage.

+

Stage-Specific Classification Performance

+ +

Performance varied substantially across learning stages, revealing distinct patterns in the model’s classification capabilities. The model demonstrated strongest performance in identifying the mastery stage, achieving a precision of 0.600 and recall of 0.750 (F1 = 0.667). The ROC curve for mastery classification shows an impressive AUC of 0.945, suggesting highly distinctive neural activation patterns associated with mastery-level learning.

+

The middle learning stage showed moderate classification success (precision = 0.353, recall = 0.429, F1 = 0.387), while early and late stages proved more challenging to classify (F1 scores of 0.258 and 0.316 respectively). The confusion matrix reveals a tendency to misclassify early learning stages as middle stages (47.1% of cases), suggesting a gradual transition in neural activation patterns during learning progression.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Learning StagePrecisionRecallF1SupportROC AUCMean Conf.Error Rate
Early0.2860.2350.258170.3680.4370.765
Middle0.3530.4290.387140.5560.4120.571
Late0.3330.3000.316100.7400.3890.700
Mastery0.6000.7500.66740.9450.5280.250
Overall0.4070.4280.347450.6520.4370.644
+

Neural Activation Patterns

+ +

Analysis of fMRI activation patterns, as exemplified in Figure 2, reveals characteristic spatial distributions associated with different learning stages. The sample brain slice visualization demonstrates the complex nature of the neural activation patterns the model must interpret, with varying intensity values representing normalized BOLD signal strength across different brain regions.

+

+

Example f MRI slice

+

+Figure 2: Representative brain slice visualization from early learning stage (z=15, t=118) demonstrating characteristic activation patterns. Intensity values represent normalized BOLD signal strength.

+

Classification Reliability Analysis

+ +

The model’s reliability metrics provide crucial insight into its decision-making characteristics. The mean confidence of 0.437 with an overconfidence measure of 0.088 indicates relatively calibrated predictions, though the expected calibration error of 0.491 suggests room for improvement in uncertainty estimation. As shown in Figure 1, the confidence distribution shows distinct patterns for each learning stage, with mastery predictions showing a broader, right-skewed distribution compared to the more concentrated distributions of earlier stages.

+

The ROC curves reveal a clear progression in classification reliability across learning stages: early (AUC = 0.368), middle (AUC = 0.556), late (AUC = 0.740), and mastery (AUC = 0.945). This progression suggests that distinctive neural patterns become increasingly detectable as learning progresses, with mastery showing particularly clear neural signatures.

+

The mean loss of 1.082 (±0.257) suggests stable model training despite the classification challenges, with the relatively small standard deviation indicating consistent performance across validation folds. These results demonstrate both the promise and limitations of our approach, suggesting that while neural activation patterns contain meaningful information about learning stages, additional architectural innovations may be needed to fully capture the complexity of temporal learning progression in fMRI data.

+

Usage

+
+

Github

+

All the code and full replication instructions may be found in the Interoception Model repository: https://github.com/Twarner491/learnedSpectrum

*Please note that this repo is in devolopment and may contain faulty code for the time being. Feel free to reach out over email or drop a comment at the bottom of this page and I’d be happy to assemble the last working repo for you.

+
+
Prerequisites
+ +
    +
  • Python 3.8+
  • +
  • CUDA-capable GPU
  • +
  • Git
  • +
+

Installation

+ +
    +
  1. +

    Clone the repository: +

    git clone https://github.com/Twarner491/learnedSpectrum.git
    +cd learnedSpectrum
    +

    +
  2. +
  3. +

    Create and activate a virtual environment: +

    python -m venv venv
    +source venv/bin/activate  # On Windows, use: venv\Scripts\activate
    +

    +
  4. +
  5. +

    Install the package in editable mode with all dependencies: +

    pip install -e .
    +

    +
  6. +
+

Data Preparation

+ +
    +
  1. Create necessary directories and download datasets (automated): +
    python scripts/data/download.py
    +
  2. +
+

The resulting directory structure will be: +

data/
+├── raw/
+│   ├── ds000002/
+│   ├── ds000011/
+│   ├── ds000017/
+│   └── ds000052/
+├── processed/
+└── cleaned/
+

+

Running the Analysis

+ +
    +
  1. Start Jupyter: +
    jupyter notebook
    +
  2. +
  3. Navigate to notebooks/learnedSpectrum.ipynb
  4. +
  5. The notebook is organized into clear sections:
      +
    • Setup and Imports
    • +
    • Configuration
    • +
    • Data Preparation
    • +
    • Model Training
    • +
    • Evaluation
    • +
    • Results Visualization
    • +
    +
  6. +
  7. Execute cells sequentially to:
      +
    • Process the fMRI data
    • +
    • Train the Vision Transformer model
    • +
    • Visualize results and attention maps
    • +
    • Generate performance metrics
    • +
    +
  8. +
+

Experiment Tracking

+ +
    +
  1. Set up Weights & Biases (Optional): +
    wandb login
    +
  2. +
  3. View training progress:
      +
    • Real-time metrics at wandb.ai
    • +
    • Local visualizations in visualizations/
    • +
    • Training logs in models/checkpoints/
    • +
    +
  4. +
+

Discussion

+

Implications for World Models

+ +

Our findings reveal critical implications for AI world model development. The successful extraction of learning stage patterns from fMRI data demonstrates that neural networks can capture subjective temporal experience aspects typically overlooked in current world models 17. This capability suggests that integrating physiological state data into world model training provides essential insights into biological temporal information processing.

+
Temporal Relativity in World Models
+ +

The correlation between neural activation patterns and learning stages demonstrates systematic variation in temporal perception with cognitive state 16. This finding challenges conventional world model time representation, which treats time as an absolute dimension 12. Effective world models require mechanisms for relative temporal processing that adapt to learning contexts and cognitive states.

+
Physiological State Integration Pathways
+ +

Our temporal processing architecture’s success in capturing learning stage transitions suggests three integration pathways for physiological state insights:

+
    +
  • +

    Direct Signal Integration: Train world models directly on physiological state data for nuanced temporal understanding

    +
  • +
  • +

    Architectural Inspiration: Adapt temporal processing mechanisms based on fMRI characteristics for general world model architectures

    +
  • +
  • +

    Hybrid Learning Approaches: Combine physiological state data with traditional training signals for comprehensive temporal understanding

    +
  • +
+
Causal Understanding Enhancement
+ +

The correlation between learning stage transitions and temporal processing patterns indicates that subjective time perception plays a critical role in biological causal learning 15. Current world model approaches to causal learning, relying on objective temporal sequences, may face fundamental limitations.

+

Current Limitations

+ +

The implementation faces several important constraints:

+
    +
  1. +

    Temporal Resolution

    +
      +
    • Limited by fMRI BOLD signal characteristics
    • +
    • Fixed sampling rate constraints
    • +
    • Hemodynamic response delay
    • +
    +
  2. +
  3. +

    Computational Requirements

    +
      +
    • High memory demands for 4D volumes
    • +
    • Complex attention computation
    • +
    • Large model parameter space
    • +
    +
  4. +
  5. +

    Scaling Challenges

    +
      +
    • Dataset size limitations
    • +
    • Training time requirements
    • +
    • Hardware constraints
    • +
    +
  6. +
+

Conclusion

+ +

The exploration of fMRI data for temporal pattern extraction represents a first step toward validating our hypothesis that world models require physiological state-informed training data to develop genuine temporal understanding. Our implementation demonstrates that neural architectures can successfully extract and utilize these biological temporal patterns, suggesting a viable path toward bridging the A → B → C gap in current world model approaches.

+

Future work should expand beyond fMRI to incorporate the full spectrum of physiological state signals identified in our introduction (facial EMG, heart rate variability, electrodermal activity). This multi-modal physiological state approach, combined with advanced transformer architectures, could enable world models to develop temporal understanding that more closely mirrors human cognitive processes, particularly in causal reasoning and state transitions. The technical foundation established here provides a framework for this broader integration while addressing the practical challenges of physiological state data collection and processing at scale.

+

Temporal Model

+

Static Badge

+

Reinforcement Gymnasium

+

Static Badge

+
+

Github

+

All the code and full replication instructions may be found in the Reinforcement Gymnasium repository: https://github.com/Twarner491/Impetus

*Please note that this repo is in devolopment and may contain faulty code for the time being. Feel free to reach out over email or drop a comment at the bottom of this page and I’d be happy to assemble the last working repo for you.

+
+
+
+
    +
  1. +

    https://runwayml.com/research/introducing-general-world-models 

    +
  2. +
  3. +

    https://worldmodels.github.io/ 

    +
  4. +
  5. +

    https://x.com/yugu_nlp/status/1859424088927940831 

    +
  6. +
  7. +

    https://x.com/hongyangzh/status/1859288829825515810 

    +
  8. +
  9. +

    https://x.com/WayneINR/status/1859448167827186168 

    +
  10. +
  11. +

    https://www.forrester.com/blogs/llms-make-room-for-world-models/ 

    +
  12. +
  13. +

    https://openreview.net/pdf?id=BZ5a1r-kVsf 

    +
  14. +
  15. +

    https://en.wikipedia.org/wiki/Human_Compatible 

    +
  16. +
  17. +

    https://en.wikipedia.org/wiki/Model_predictive_control 

    +
  18. +
  19. +

    https://www.linkedin.com/posts/yannlecun_lots-of-confusion-about-what-a-world-model-activity-7165738293223931904-vdgR/# 

    +
  20. +
  21. +

    https://techterrain.substack.com/p/world-models-vs-kalman-filter 

    +
  22. +
  23. +

    https://arxiv.org/abs/2302.10035 

    +
  24. +
  25. +

    https://www.1x.tech/discover/1x-world-model 

    +
  26. +
  27. +

    https://pubmed.ncbi.nlm.nih.gov/16139527/ 

    +
  28. +
  29. +

    https://www.jstor.org/stable/2891421 

    +
  30. +
  31. +

    https://www.nature.com/articles/35107080 

    +
  32. +
  33. +

    https://arxiv.org/abs/1803.10122 

    +
  34. +
  35. +

    https://arxiv.org/abs/1706.03762 

    +
  36. +
  37. +

    https://arxiv.org/abs/2010.11929 

    +
  38. +
  39. +

    https://www.nature.com/articles/nature06976 

    +
  40. +
  41. +

    https://openreview.net/pdf?id=BZ5a1r-kVsf 

    +
  42. +
  43. +

    https://doi.org/10.1017/CBO9780511895029 

    +
  44. +
  45. +

    https://arxiv.org/abs/1704.03924 

    +
  46. +
  47. +

    https://arxiv.org/abs/2404.16078 

    +
  48. +
  49. +

    https://neilsahota.com/causal-ai-bridging-the-gap-between-correlation-and-causation/ 

    +
  50. +
  51. +

    https://www.ijcai.org/Proceedings/83-2/Papers/036.pdf 

    +
  52. +
  53. +

    https://betterworld.mit.edu/spectrum/issues/2024-spring/biometrics-in-the-age-of-artificial-intelligence/ 

    +
  54. +
  55. +

    https://pubmed.ncbi.nlm.nih.gov/29684246/ 

    +
  56. +
  57. +

    https://pmc.ncbi.nlm.nih.gov/articles/PMC7875380/ 

    +
  58. +
  59. +

    https://pmc.ncbi.nlm.nih.gov/articles/PMC1866291/ 

    +
  60. +
  61. +

    https://www.science.org/doi/10.1126/sciadv.abj0751 

    +
  62. +
  63. +

    https://pmc.ncbi.nlm.nih.gov/articles/PMC9320620/ 

    +
  64. +
  65. +

    https://www.frontiersin.org/journals/psychology/articles/10.3389/fpsyg.2020.01842/full 

    +
  66. +
  67. +

    https://www.frontiersin.org/journals/neuroscience/articles/10.3389/fnins.2024.1387641/full 

    +
  68. +
  69. +

    https://cacm.acm.org/research/challenges-and-constraints-to-the-diffusion-of-biometrics-in-information-systems/ 

    +
  70. +
  71. +

    https://pmc.ncbi.nlm.nih.gov/articles/PMC6904682/ 

    +
  72. +
  73. +

    https://www.nber.org/system/files/working_papers/w29587/w29587.pdf 

    +
  74. +
  75. +

    https://pmc.ncbi.nlm.nih.gov/articles/PMC9824521/ 

    +
  76. +
  77. +

    https://www.biorxiv.org/content/10.1101/2022.02.28.482337v2.full 

    +
  78. +
  79. +

    https://www.pnas.org/doi/10.1073/pnas.1016823108 

    +
  80. +
  81. +

    https://pmc.ncbi.nlm.nih.gov/articles/PMC10960227/ 

    +
  82. +
+
+ + + + + + + + + + + + + + + + + + +

Comments

+ + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Projects/KlipperEnder3/index.html b/Projects/KlipperEnder3/index.html new file mode 100644 index 00000000..a36a962f --- /dev/null +++ b/Projects/KlipperEnder3/index.html @@ -0,0 +1,1514 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Klipper Enabled Ender 3 - Teddy Warner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + +
+ +
+ + +
+ + + + + + + + + +
+
+ + + + + + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + +

+ +

+ +

+ + + +

+ +

+ + + + + + +

+ +

+ + + +

+ + +

+ + + + + +

+ + +
+ + + + + + +

Klipper Enabled Ender 3

+
+ Teddy Warner's GitHub profile picture Teddy Warner| Spring & Summer, 2021 | 10-12 minutes + +
+ +
+

Due to uncertainty surrounding lab access during the Covid-19 pandemic, I received an Ender 3 3D-printer at the start of my cycle in Fab Academy. During my second week in the course, I built and began to mod this machine as a little side project to the course work. However as the cycle progressed, I quickly ran out of time to keep working on this machine’s mods and moved the machine to my closet, where it sat up until the Klipper firmware piqued my interest. After modding the printer during Fab Academy1, I was running the machine on Marlin linked to Octoprint2, a setup I use on my other machines. This setup worked great with my machine mods, allowing me to print over a network through Octoprint as I would on any of my other Octoprint enabled machines. In this configuration, a Raspberry Pi running Octorpint hosts a local server that .gcode flies can be uploaded to and ran from. The Pi sends this uploaded .gcode to the machine’s mainboard, where the code is processed, and movements/operations are determined.

+
+

Klipper Opperation Principal

+

Klipper on the other hand operates on a different principle. The firmware runs across both the mainboard and the Raspberry Pi, allowing for the same type of hosted server to exist (I used FluiddPi as my servers GUI), but instead of relying on the machines mainboard to compute .gcode, the code parsing is done by the Raspberry Pi - the more powerful computer- leaving the machine mainboard to only worry about stepper movements and operations. This setup uses the given hardware of a setup more optimally, allowing for faster calculations and more precise movements.

+
+

After learning about the benefits of Klipper I knew I had to try it out and thus, the Ender 3 modding project was revied and pulled from the closet.

+

Ender 3 Modding

+

Before the installation of Klipper, here are all the hardware mods done to the machine. I upgraded the main bored from the original factory board to a SKR Mini E3 V2, and the original factory screen I switched out to a TFT35 Touchscreen. Following that I added a Bltouch for auto bed leveling, and switched the machine from Bowen to direct drive, with this Thingiverse design. I spent some time adding some customization to the machine as well, giving the direct drive system a little nameplate.

+

+

Direct drive system with custom nameplate on Ender 3

+

+

Finally, I moved all of the electronics of the machine to the rear of the machine using this Thingiverse design. This mod gave the space to add a raspberry pi and a means of power for it in addition to the mainboard, a perfect case for a Klipper-enabled machine.

+

+

Electronics enclosure at the rear of the Ender 3 +Installed electronics box with Raspberry Pi and mainboard

+

+

I used this Thingiverse design to mount a raspberry pi camera to my printer’s x-axis as shown in one of the pictures below. All of these modifications were done with parts printed on my Prusa I3 Mk3S+, and electronics I had laying around, making these printer modifications only take around three days to complete, including the time it took to print the parts.

+

+

Raspberry Pi camera mounted on Ender 3's x-axis

+

+

Full view of the modified Ender 3 printer

+

Klipper Installation

+

As mentioned in the introduction paragraph of this page, I settled on using FluiddPi as the GUI for this Klipper setup. As opposed to a more broadly know GUI like Octoprint, FluiddPi offers some features that work directly with Klipper that make the entire experience pretty seamless. FluiddPi in addition to containing the servers GUI also packages Klipper & Moonraker into the software image, making the install of Klipper super straightforward.

+

+

Download the Latest FluiddPi Image

+

+

Beginning the installation of Klipper, the first step is to download and flash the latest FluiddPi image (linked above). This Pi image can be flashed as you would with any other Raspberry Pi project - I use balenaEtcher for flashing, its simple 3-step interface makes the process super straightforward. After a successful flash of this image on your Pi’s SD card configure your WiFi information in the fluiddpi-wpa-supplicant.txt located on the flashed SD card (If you’re using wired internet, you can skip this setup). Then, you can install your flashed SD in your Pi and boot.

+

Next, some basic Pi config. Make sure you are on the same WiFi network you set up your Pi on, and then SSH into your booted Pi at the IP fluiddpi.local with the Pi’s default credentials (Username - pi / Password - raspberry). I use Putty as my SSH client as again, it’s pretty straightforward to use. Once connected to your Pi, run the command …

+
sudo raspi-config
+
+

to open the configuration GUI. Here you can change your Pi’s credentials (highly recommended), set up your local timezone (to allow for accurate machine ETAs), and change your machine’s hostname (thus changing the URL from the default fluiddpi.local to YOURHOSTNAME.local). After all, changes, reboot your Pi and then confirm all software is up to date with the lines

+
1
+2
sudo apt-get update
+sudo apt-get upgrade
+
+
+

Congrats!

+

You have now successfully set up FluiddPi on your Raspberry Pi, and can now connect to its web server with the URL http://fluiddpi.local ( or if you changed your hostname, with the URL http://YOURHOSTNAME.local).

+
+

+

FluiddPi web server interface for Klipper +
+

The FluiddPi Web Server Interface
+

+

+

Upon connection, the webserver will throw an error with a lack of a printer.cfg file. This file is your machine config file. Notably different from Marlin, Klipper does not require a machine to be specified before the flash but instead looks to this printer.cfg file for machine configuration. One of the benefits of using FluiddPi for Klipper is the ability to alter this printer.cfg file and reboot the firmware right from the server’s GUI. The machine configuration for my Klipper Enabled Ender 3 is covered later on this page under the Klipper Configuration section, but for any other machines, preconfigured printer.cfg files can be found online.

+

Next up, building Klipper to for machines mainboard. SSH back into your Raspberry Pi (making sure to change your IP to match your altered hostname if you changed it) and run the commands …

+
1
+2
cd ~/klipper/
+make menuconfig
+
+

to open the mainboard configuration GUI. Work through this interface with your mainboards information before running the command …

+
make
+
+

to build your set configurations. Now, connect your Pi and machine mainboard via USB and determine the connected serial port with the line

+
ls /dev/serial/by-id/*
+
+

the report this line will yield provides the serial port that connects your mainboard to your Pi, take note of this.

+

Now, to begin the flashing, run the lines…

+
1
+2
+3
sudo service klipper stop
+make flash FLASH_DEVICE=YOUR-SERIAL-PORT-INFORMATION-HERE
+sudo service klipper start
+
+

making sure to paste in the reported serial port where it says YOUR-SERIAL-PORT-INFORMATION-HERE. This will flash the built Klipper instance to your machine’s mainboard, and then begin communication between the two parallel boards.

+
+

Good Work!

+

Klipper is now installed.

+
+

Klipper Configuration

+

As mentioned prior, unlike the Marlin firmware, Klipper is not compiled for a specific machine. Instead, the firmware looks to a machine configuration file - printer.cfg. This file can be altered without the reflashing of the firmware, just a simple reboot, thus making machine tinkering and hardware swapping significantly simpler. This printer.cfg file can be altered in the FluiddPi web server interface, simplifying the configuration process even more.

+

+

FluiddPi configuration menu for printer.cfg file +
+

FluiddPi's Configuration Menu in Web Server Interface
+

+

+

As shown above, your printer.cfg file can be accessed in FluiddPi’s configuration menu. Due to Klipper’s ability to experiment with your machine config, my printer’s configuration is always subject to change, but included below is the latest machine config running on my Klipper Enabled Ender 3.

+

+

printer.cfg

+

+
+ +
printer.cfg
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
+178
+179
+180
+181
+182
+183
+184
+185
+186
+187
+188
+189
+190
+191
+192
+193
+194
+195
+196
+197
+198
+199
+200
+201
+202
+203
+204
+205
+206
+207
+208
+209
+210
+211
+212
+213
+214
+215
+216
+217
+218
+219
+220
+221
+222
+223
+224
+225
+226
+227
+228
+229
+230
+231
+232
+233
+234
+235
+236
+237
+238
+239
+240
+241
+242
+243
+244
+245
+246
+247
+248
+249
+250
+251
+252
+253
+254
+255
+256
+257
+258
+259
+260
+261
+262
+263
+264
+265
+266
+267
+268
+269
+270
+271
+272
+273
+274
+275
+276
+277
+278
+279
+280
+281
+282
+283
+284
+285
+286
+287
+288
+289
+290
+291
+292
+293
+294
+295
+296
+297
+298
+299
+300
+301
+302
+303
+304
+305
+306
+307
+308
+309
+310
+311
+312
+313
+314
+315
+316
+317
+318
+319
+320
+321
+322
+323
+324
+325
+326
+327
+328
+329
# See docs/Config_Reference.md for a description of parameters.
+
+[virtual_sdcard]
+path: ~/gcode_files
+
+[display_status]
+
+[pause_resume]
+
+[stepper_x]
+step_pin: PB13
+dir_pin: !PB12
+enable_pin: !PB14
+microsteps: 16
+rotation_distance: 40
+endstop_pin: ^PC0
+position_endstop: 0
+position_max: 235
+homing_speed: 50
+
+[tmc2209 stepper_x]
+uart_pin: PC11
+tx_pin: PC10
+uart_address: 0
+run_current: 0.580
+hold_current: 0.500
+stealthchop_threshold: 999999
+
+[stepper_y]
+step_pin: PB10
+dir_pin: !PB2
+enable_pin: !PB11
+microsteps: 16
+rotation_distance: 40
+endstop_pin: ^PC1
+position_endstop: 0
+position_max: 235
+homing_speed: 50
+
+[tmc2209 stepper_y]
+uart_pin: PC11
+tx_pin: PC10
+uart_address: 2
+run_current: 0.580
+hold_current: 0.500
+stealthchop_threshold: 999999
+
+[stepper_z]
+step_pin: PB0
+dir_pin: PC5
+enable_pin: !PB1
+microsteps: 16
+rotation_distance: 8
+endstop_pin: probe:z_virtual_endstop
+#position_endstop: 0.0
+position_min: -5
+position_max: 250
+
+[tmc2209 stepper_z]
+uart_pin: PC11
+tx_pin: PC10
+uart_address: 1
+run_current: 0.580
+hold_current: 0.500
+stealthchop_threshold: 999999
+
+[extruder]
+step_pin: PB3
+dir_pin: !PB4
+enable_pin: !PD2
+microsteps: 16
+rotation_distance: 7.61
+pressure_advance = 0.235
+nozzle_diameter: 0.400
+filament_diameter: 1.750
+heater_pin: PC8
+sensor_type: EPCOS 100K B57560G104F
+sensor_pin: PA0
+#control: pid
+#pid_Kp: 21.527
+#pid_Ki: 1.063
+#pid_Kd: 108.982
+min_temp: 0
+max_temp: 250
+
+[tmc2209 extruder]
+uart_pin: PC11
+tx_pin: PC10
+uart_address: 3
+run_current: 0.650
+hold_current: 0.500
+stealthchop_threshold: 999999
+
+[heater_bed]
+heater_pin: PC9
+sensor_type: ATC Semitec 104GT-2
+sensor_pin: PC3
+#control: pid
+#pid_Kp: 54.027
+#pid_Ki: 0.770
+#pid_Kd: 948.182
+min_temp: 0
+max_temp: 130
+
+[heater_fan nozzle_cooling_fan]
+pin: PC7
+
+[fan]
+pin: PC6
+
+[mcu]
+serial: /dev/serial/by-id/usb-Klipper_stm32f103xe_36FFDB05424E313841781457-if00
+
+[printer]
+kinematics: cartesian
+max_velocity: 300
+max_accel: 3000
+max_z_velocity: 5
+max_z_accel: 100
+
+[static_digital_output usb_pullup_enable]
+pins: !PA14
+
+[board_pins]
+aliases:
+    # EXP1 header
+    EXP1_1=PB5,  EXP1_3=PA9,   EXP1_5=PA10, EXP1_7=PB8,  EXP1_9=<GND>,
+    EXP1_2=PA15, EXP1_4=<RST>, EXP1_6=PB9,  EXP1_8=PB15, EXP1_10=<5V>
+
+[display]
+lcd_type: st7920
+cs_pin: EXP1_7
+sclk_pin: EXP1_6
+sid_pin: EXP1_8
+encoder_pins: ^EXP1_5, ^EXP1_3
+click_pin: ^!EXP1_2
+
+[bltouch]
+sensor_pin: ^PC2
+control_pin: PA1
+x_offset: 32
+y_offset: -2
+#z_offset = 3.050
+
+[safe_z_home]
+home_xy_position: 85.5,119.5
+speed: 75
+z_hop: 10                 
+z_hop_speed: 5
+
+[bed_mesh]
+speed: 120
+horizontal_move_z: 5
+mesh_min: 32,44
+mesh_max: 202, 220
+probe_count: 3,3
+
+[screws_tilt_adjust]
+screw1: 0,45
+screw1_name: front left screw
+screw2: 164,45
+screw2_name: front right screw
+screw3: 164,220
+screw3_name: rear right screw
+screw4: 0,220
+screw4_name: rear left screw
+horizontal_move_z: 10.
+speed: 50.
+screw_thread: CW-M4
+
+[gcode_macro PAUSE]
+description: Pause the actual running print
+rename_existing: PAUSE_BASE
+# change this if you need more or less extrusion
+variable_extrude: 1.0
+gcode:
+  ##### read E from pause macro #####
+  {% set E = printer["gcode_macro PAUSE"].extrude|float %}
+  ##### set park positon for x and y #####
+  # default is your max posion from your printer.cfg
+  {% set x_park = printer.toolhead.axis_maximum.x|float - 5.0 %}
+  {% set y_park = printer.toolhead.axis_maximum.y|float - 5.0 %}
+  ##### calculate save lift position #####
+  {% set max_z = printer.toolhead.axis_maximum.z|float %}
+  {% set act_z = printer.toolhead.position.z|float %}
+  {% if act_z < (max_z - 2.0) %}
+      {% set z_safe = 2.0 %}
+  {% else %}
+      {% set z_safe = max_z - act_z %}
+  {% endif %}
+  ##### end of definitions #####
+  PAUSE_BASE
+  G91
+  {% if printer.extruder.can_extrude|lower == 'true' %}
+    G1 E-{E} F2100
+  {% else %}
+    {action_respond_info("Extruder not hot enough")}
+  {% endif %}
+  {% if "xyz" in printer.toolhead.homed_axes %}
+    G1 Z{z_safe} F900
+    G90
+    G1 X{x_park} Y{y_park} F6000
+  {% else %}
+    {action_respond_info("Printer not homed")}
+  {% endif %} 
+
+[gcode_macro RESUME]
+description: Resume the actual running print
+rename_existing: RESUME_BASE
+gcode:
+  ##### read E from pause macro #####
+  {% set E = printer["gcode_macro PAUSE"].extrude|float %}
+  #### get VELOCITY parameter if specified ####
+  {% if 'VELOCITY' in params|upper %}
+    {% set get_params = ('VELOCITY=' + params.VELOCITY)  %}
+  {%else %}
+    {% set get_params = "" %}
+  {% endif %}
+  ##### end of definitions #####
+  {% if printer.extruder.can_extrude|lower == 'true' %}
+    G91
+    G1 E{E} F2100
+  {% else %}
+    {action_respond_info("Extruder not hot enough")}
+  {% endif %}  
+  RESUME_BASE {get_params}
+
+[gcode_macro CANCEL_PRINT]
+description: Cancel the actual running print
+rename_existing: CANCEL_PRINT_BASE
+gcode:
+  TURN_OFF_HEATERS
+  CANCEL_PRINT_BASE
+
+[gcode_macro START_PRINT]
+gcode:
+    {% set BED_TEMP = params.BED_TEMP|default(60)|float %}
+    {% set EXTRUDER_TEMP = params.EXTRUDER_TEMP|default(190)|float %}
+    # Start bed heating
+    M140 S{BED_TEMP}
+    # Use absolute coordinates
+    G90
+    # Reset the G-Code Z offset (adjust Z offset if needed)
+    SET_GCODE_OFFSET Z=0.395
+    # Home the printer
+    G28
+    # Move the nozzle
+    G1 Z10 F3000
+    G0 X10 Y0
+    # Wait for bed to reach temperature
+    M190 S{BED_TEMP}
+    # Set and wait for nozzle to reach temperature
+    M109 S{EXTRUDER_TEMP}
+
+    G92 E0 ; reset extruder G1 Z1.0 F3000 ; move z up little to prevent scratching of surface
+    G1 X0.1 Y20 Z0.395 F5000.0 ; move to start-line position
+    G1 X0.1 Y200.0 Z0.395 F1500.0 E15 ; draw 1st line
+    G1 X0.4 Y200.0 Z0.395 F5000.0 ; move to side a little
+    G1 X0.4 Y20 Z0.395 F1500.0 E30 ; draw 2nd line
+    G92 E0 ; reset extruder
+
+[gcode_macro END_PRINT]
+gcode:
+    # Turn off bed, extruder, and fan
+    M140 S0
+    M104 S0
+    M106 S0
+    # Move nozzle away from print while retracting
+    G91
+    G1 X-2 Y-2 E-3 F300
+    # Raise nozzle by 15mm
+    G1 Z15 F3000
+    # Present Print
+    G90
+    G1 X0 Y190 F6000
+    G90
+    # Disable steppers
+    M84
+
+[gcode_macro M600]
+gcode:
+    {% set X = params.X|default(50)|float %}
+    {% set Y = params.Y|default(0)|float %}
+    {% set Z = params.Z|default(10)|float %}
+    SAVE_GCODE_STATE NAME=M600_state
+    PAUSE
+    G91
+    G1 E-.8 F2700
+    G1 Z{Z}
+    G90
+    G1 X{X} Y{Y} F3000
+    G91
+    G1 E-50 F1000
+    RESTORE_GCODE_STATE NAME=M600_state
+
+#*# <---------------------- SAVE_CONFIG ---------------------->
+#*# DO NOT EDIT THIS BLOCK OR BELOW. The contents are auto-generated.
+#*#
+#*# [bed_mesh default]
+#*# version = 1
+#*# points =
+#*#     -0.160000, -0.305000, -0.365000
+#*#     0.107500, -0.015000, -0.337500
+#*#     0.097500, -0.020000, -0.162500
+#*# tension = 0.2
+#*# min_x = 32.0
+#*# algo = lagrange
+#*# y_count = 3
+#*# mesh_y_pps = 2
+#*# min_y = 44.0
+#*# x_count = 3
+#*# max_y = 220.0
+#*# mesh_x_pps = 2
+#*# max_x = 202.0
+#*#
+#*# [heater_bed]
+#*# control = pid
+#*# pid_kp = 63.826
+#*# pid_ki = 1.462
+#*# pid_kd = 696.505
+#*#
+#*# [extruder]
+#*# control = pid
+#*# pid_kp = 25.693
+#*# pid_ki = 1.338
+#*# pid_kd = 123.326
+#*#
+#*# [bltouch]
+#*# z_offset = 2.900
+
+ +
+ +

Slicer Configuration

+

Check out my Klipper Enabled Ender 3 machine profile page, linked below, for information regarding the slicer setup & config, as well as my profile download.

+

+

Klipper Enabled Ender 3 Profile

+

+
+
+
    +
  1. +

    http://fabacademy.org/2021/labs/charlotte/students/theodore-warner/Assignments/week02/ 

    +
  2. +
  3. +

    https://teddywarner.org/Projects/Octoprint/ 

    +
  4. +
+
+ + + + + + + + + + + + + + + + + + +

Comments

+ + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Projects/LithophaneExperiments/index.html b/Projects/LithophaneExperiments/index.html new file mode 100644 index 00000000..cdd2c45e --- /dev/null +++ b/Projects/LithophaneExperiments/index.html @@ -0,0 +1,753 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Lithophane Experiments - Teddy Warner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + +
+ +
+ + +
+ + + + + + + + + +
+
+ + + + + + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + +

+ +

+ +

+ + + +

+ +

+ + + + + + +

+ +

+ + + +

+ + +

+ + + + + +

+ + +
+ + + + + + +

Lithophane Experiments

+
+ Teddy Warner's GitHub profile picture Teddy Warner| 2019-2022 | 1-2 minutes + +
+ +
+

A Lithophane is a piece of art made out of a thin translucent material designed to show an image in a “grisaille” color format when held up to light.1 3D printers can be utilized to create some nice-looking lithophanes, a practice made easy with 3dp.rocks lithophane generator, linked below. Due to their easy creation, I find lithophanes to be a nice medium to test slicer changes with, allowing for your time to be spent with the slicer settings, and not the file prep.

+
+

Note

+

This page is not documentation on the lithophane making process, but more of a portfolio of some notable lithophanes I’ve printed since taking up this medium.

+
+

+

3dp.rocks’s Lithophane Generator

+ + +

+
+

People

+

+

Original photo of friends group +Lithophane print of friends group photo

+

Original family photo +Lithophane print of family photo

+

Original photo of MC +Lithophane print of MC photo

+

Original photo of Nina +Lithophane print of Nina photo

+

Original photo of Zach +Lithophane print of Zach photo

+

Original photo of Maeko +Lithophane print of Maeko photo

+

Original photo of Alex +Lithophane print of Alex photo

+

+

Art

+

+

Original Keith Haring artwork +Lithophane print of Keith Haring artwork

+

Original painting artwork +Lithophane print of painting

+

Original Georgia O'Keeffe artwork +Lithophane print of Georgia O'Keeffe artwork

+

+
+
+
    +
  1. +

    https://en.wikipedia.org/wiki/Lithophane 

    +
  2. +
+
+ + + + + + + + + + + + + + + + + + +

Comments

+ + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Projects/MillingWorflow/index.html b/Projects/MillingWorflow/index.html new file mode 100644 index 00000000..d56b977c --- /dev/null +++ b/Projects/MillingWorflow/index.html @@ -0,0 +1,1687 @@ + + + + + + + + + + + + + + + + + + + + + + + + + CNC Milling Workflows - Teddy Warner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + +
+ +
+ + +
+ + + + + + + + + +
+
+ + + + + + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + +

+ +

+ +

+ + + +

+ +

+ + + + + + +

+ +

+ + + +

+ + + + +

+ + + + + + +

+ + +
+ + + + + + +

CNC Milling Workflows

+
+ Profile Picture Teddy Warner| 2022-2023 | 12-15 minutes + +
+ +
+

Subtractive manufacturing is a process I’ve always found mesmerizing. Although additive processes such as 3D printing allow for the creation of something from seemingly nothing, I believe it’s even cooler to watch the morphing of existing stock into a different object. Despite this interest, I feel as though I’ve barely scratched the surface of CNC - primarily focusing on PCB milling in my subtractive manufacturing projects. I made a first attempt to dive into more subtractive manufacturing work a couple of years ago now when I built V1 Engineering’s original MPCNC.

+

+

+

+
My Mostly Printed CNC Machine
+ +

+

I absolutely loved building this machine and recommend it to all interested in understanding the workings of CNC, yet I don’t mill on mine all that often, I find more use in using the machine as a GCode dev platform for some of my other projects.

+

Since my initial attempted steps with the MPCNC, I’ve become more versed with CNC milling due to exposure during my Fab Academy Week 7: Computer Controlled Machining assignment. For this assignment, I used the CLS Fab Lab’s ShopBot PRSalpha to mill the plywood fish tank housing for my Fab Academy Final Project. I continued my milling work over the summer, running the ShopBot almost daily over July for Young Engineers of Today’s Summer Camps. Last fall, our lab picked up a Bantam Tools Desktop CNC Milling Machine, Bantam’s flagship machine, capable of super-rapid prototyping when partnered with their Milling Machine Software.

+

+

+

+

+

This article attempts to serve as a complete guide for CNC milling, covering machinist jargon, material standards, feeds and speeds, CAM workflows with Autodesk Fusion 360 & Vectric Aspire, and milling workflows for Shopbot & Bantam Tools machines - all through a collection of milling micro-projects ranging from a 1 to 1.9 Million scale mill of Mars’s Gale Crater to brass wax seals 😄.

+
+

Milling Micro-Projects

+

This page strays from my normal single-project documentation style, opting to cover different CAM & milling workflows via a collection of micro-projects. In a similar approach to the “Constantly Updating” status of my Lithophane Experiments article, I plan to continually expand the content of this article with any additional milling micro-project I complete. Initally, the page will just host material reference & a feeds and speed calculator. That being said, be sure to stay tuned for future updates!

+
+

Feeds & Speeds

+
+ +
+ +

Creating a successful subtractive manufacturing toolpath can be a bit more intimidating than slicing for additive manufacturing. Not only can you jeopardize your stock material, but incorrect feeds and speeds can also yield a hazardous environment & potential machine damage. There is no streamlined answer to find the right feeds and speeds, ideal values depend on numerous variables & feeds and speeds may not remain constant from job to job. This section provides a starting place for feeds and speeds, with general values provided as well as a calculator for the required inputs in your toolpath. This is by no means a universal key and necessary precautions & alterations should be made to provide values to mesh with your manufacturing workflow. Often the manufacturer of your tool & your machine will provide generic feeds and speeds for your specific equipment. Operator input is required to achieve successful & safe cuts, often you’ll be able to hear machine strain - a telltale sign of poor cutting. Trial and error adjusting is necessary to achieve the sweet spot for your feeds and speeds.

+
+Feeds & Speeds Terminology +
+
Tool Diameter
+
+

The diameter of the endmill. When Profile milling or Through Cutting stock, it is best to keep the Tool Diameter ≥ 1/6 Stock Thickness, preserving the integrity of the endmill while allowing for full passes. Larger tool diameters yield shorter cut times when removing a lot of material, while smaller diameters allow for higher detail to be reached. It is typically best to utilize both larger and smaller diameter endmills, using the larger to clear the bulk of stock material, while finishing the mill with a smaller diameter tool to achieve the best detail.

+

+

+

+

+
+
Flute Count
+
+

The number of individual flutes an endmill has. Flutes are the upward running cutting edges on an endmill. Lower flute counts allow for better chip & heat clearance from the cutting edge, while higher flute counts provided a smoother finish to a part. Stock materials that require a high surface speed also require higher flute bits, and vice versa with lower surface speed requirements. One to Two flute bits will suffice for prototyping with soft plastics & waxes. Two or more flutes will typically be needed for harder plastics (such as HDPE), woods, and metals.

+

+

+

+

+
+
Surface Speed
+
+

The speed at which the tool’s cutting edge travels through the stock. Surface Speed is dependent on both the stock & endmill materials. Maximum surface speeds are typically published by your endmill’s manufacturer and do not need to be derived experimentally. Staring jobs at ≤ 50% of the maximum provided value is the best practice to allow for ample time to ensure the successful operation of your machine.

+

+

+

+

+
+
Chip Load
+
+

The thickness of offcut ‘chips’ removed with each flute per revolution of the endmill. Maximum chip load is typically provided by the endmill’s manufacturer, based upon the tool’s characteristics. Finding a balance between chip load extremes is crucial for the safety of you and your machine. While larger chip loads yield shorter machine time, they also put greater forces on your endmill, potentially pushing the bit towards its point of rupture. On the other hand, smaller chip loads increase machine time, while expelling waste and heat from the tool at a slower pace, thus risking overheating and potential fire. Safe chip load values usually fall between 0.001” & 0.010”0.0254mm & 0.254mm.

+

+

+

+

+
+
Spindle Speed
+
+

The number of revolutions made by the endmill in a unit of time (RPM is standard). A Lower RPM yields a higher quality surface finish & more cutting power, while a higher RPM decreases machine time. For optimal milling operations, use a higher RPM for roughing passes & a lower for finishing.

+

+

+

+

+
+
\[ +Spindle Speed (RPM) = {Surface Speed (ft/min) \over π ∗ {1 \over 12} ∗ Tool Diameter (in)} +\]
+

+

+
\[ +Spindle Speed (RPM) = {Surface Speed (M/min) \over π ∗ {1 \over 1000} ∗ Tool Diameter (mm)} +\]
+
+
+
Feed Rate
+
+

The speed at which the machine moves the tool through the stock. Finding the right feed rate is crucial for the safety of you and your machine. Excessively high feed rates cause excessive load on the cutter, leading to cataclysmic failure of the endmill, spindle, and machine. Feed rates that are too low produce unnecessary vibration on the machine, leading to poor surface finishes & potential cutter failure.

+

+

+

+

+
+
\[ +Feed Rate (in/min) = Spindle Speed (RPM) ∗ Flute Count ∗ Chip Load (in) +\]
+

+

+
\[ +Feed Rate (mm/min) = Spindle Speed (RPM) ∗ Flute Count ∗ Chip Load (mm) +\]
+
+
+
Plunge Rate
+
+

The speed at which the endmill is driven down into the stock. The vertically running flutes on an endmill allow the tool to cut horizontally, while vertical plunges are more demanding on the tool. Lower plunge rates prevent tool damage & maintain lower temperatures while boring holes. All plunging cuts should be ramped, as gradual plunging while traveling across the stock will reduce tool stress.

+

+

+

+

+
+
\[ +Plunge Rate(in/min) = Feed Rate (in/min) * {1 \over 2} +\]
+

+

+
\[ +Plunge Rate(mm/min) = Feed Rate (mm/min) * {1 \over 2} +\]
+
+
+
Stepdown
+
+

The vertical depth of each pass of the tool into the stock. Best practice is to maintain a step down of ≤ 50% of the tool diameter, however, may be increased while milling softer materials. The step down should always remain less than the tool diameter.

+

+

+

+

+
+
\[ +Stepover(in) = Tool Diameter (in) * {3 \over 5} +\]
+

+

+
\[ +Stepover(mm) = Tool Diameter (mm) * {3 \over 5} +\]
+
+
+
Stepover
+
+

The space between passes of the tool into the stock. While pocketing, a maximum stepover of 50% can be used, however lower stepover values will leave a better surface finish.

+

+

+

+

+
+
\[ +Stepover(in) = Tool Diameter (in) * {9 \over 20} +\]
+

+

+
\[ +Stepover(mm) = Tool Diameter (mm) * {9 \over 20} +\]
+
+
+
+
+

Material Reference

+

Feeds and speeds are equally dependent on material properties and the specific of your machine & tool. Below I’ve compiled a table of commonly milled stock materials, ranging from woods to plastics to metals. Each material offers a brief description of the stock, as well as average surface speed & chip loads. Chiploads are tool-dependent just as much as they are material-dependent, so be sure to select an adequate load for your tool’s diameter. More conservative chip loads are given on the left, while aggressive loads are given on the right. Conservative values will extend the life of your tool, while the higher loads will catalyze your job time - select a chipload within the provided range based on your needs. As mentioned above, This is by no means a universal key and necessary precautions & alterations should be made to provide values to mesh with your manufacturing workflow. Often the manufacturer of your tool & your machine will provide generic feeds and speeds for your specific equipment. Operator input is required to achieve successful & safe cuts, often you’ll be able to hear machine strain - a telltale sign of poor cutting.

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MaterialDescription
WaxA easily milled medium, commonly used for casting cores, molds, and CNC program proofs.
Soft WoodA accessible & stylish medium, not to tricky to mill, often used for large scale parts - Pine, Spruce, Cedar, etc.
Hard WoodSimilar benefits to soft wood, can be a bit more costly - Maple, Walnut, Cherry, Ash, etc.
Medium Density Fiberboard MDFAn easily machined & finished material, relatively cheap, commonly used for jigs, fixtures, vacuum molds & engraving.
Oriented Strand Board OSBA strong, light weight, cost-efficient material, a great choice for large scale stiff parts.
High-density Polyethylene HDPEA soft-ish plastic, great for prototyping but a tricky material to mill.
Polyetheretherketone PEEKA high-performance thermoplastic, great for structural applications with resistance to fatigue and stress-cracking.
Polyetherimide PEIA high-performance thermoplastic, with great heat, solvent & flame resistant. Often used in applications with long term steam exposure.
Polyvinyl Chloride PVCAn cost efficient & accessible plastic, typically used across industry but offers a cheap & lightweight medium.
Polyoxymethylene POMA general purpose, good all around thermoplastic - also know as Acetal or Delrin.
Acrylonitrile Butadiene Styrene ABSEasily accessed & low cost thermoplastic, commonly used for molding applications.
Polycarbonate PCAn incredibly strong and possibly transparent thermoplastic, known for maintained temperature resistance.
NylonA cost-effective & long-lasting thermoplastic, used in cases of required mechanical dampening or electrical insulation.
AcrylicA lightweight insulating thermoplastic, serves as an effective replacement for glass.
Carbon FiberA strong & lightweight composite of carbon based fibers, desirable for various industrial applications due to its heat & chemical resistance.
AluminiumAn accessible, easy to mill metal, seen across thermal & electrical conducting components.
BrassA easy to machine & highly conductive metal, ideal for low friction & intricate parts.
CopperA malleable metal, typically used for its high electrical & thermal conductivity.
SteelAn iron alloy with high strength & fracture resistance, typically used in situations requiring its high tensile strength, such as tools & machines.
Insulation FoamA low cost, accessible & easy to mill material, commonly used for large molds & as cores for lightweight parts.
+ +

+

Feeds & Speeds Calculator

+
+ +
+ +
+Instructions for Use +

Work through the calcuator from top to bottom. First select your coolant prefrence, then input tool diamater and flute count. You may pull reccomended material settings from the table above via the “Material Library” dropdown, or you may under your own. Please note the prior warnings on calcuator dependence: This is by no means a universal key and necessary precautions & alterations should be made to provide values to mesh with your manufacturing workflow. Often the manufacturer of your tool & your machine will provide generic feeds and speeds for your specific equipment. Operator input is required to achieve successful & safe cuts, often you’ll be able to hear machine strain - a telltale sign of poor cutting.

+
+
+ +
+ +
+ +
+
+ +

+
+



+
+ +
+ +
+
+
+

+
+

+
+

+
+

+
+
+
+
+
+ +
+ + + +
+
+ +

Microprojects

+

An addendum - Jan 2025.

+

When I set out to write this piece a few years ago, I had the somewhat lofty intention of doccumenting as many different CAD/CAM softwares as I could, providing comprehensive walkthroughs for their usage. By the time I finished assembling and testing the above Material Reference section (believe me when I say this was a lift), I was quite burnt out of this endevour, and so this article just sat, unfinished on this site for a few years.

+

In hopes of providing a slightly better “conclusion” to this piece, I’ve appended a gallary of project media below, all of which was machined with settings from the above Feeds & Speeds Calcuator. Enjoy!

+

Gale Crater Mill

+

+
+

+
Scanning the profile of my cedar stock
+

+

+
+

+
and processing this scan on Inkscape
+

+

+
+

+
Gale Crater model on Fusion 360
+

+

+ +
+ +

+
Milling on the Shopbot Desktop MAX
+

+

+

+
+

... and the results
+

+

+

+

+ +
+

Final Gale Crater Mill after finishing
+

+

+

Topography Paper Weight

+

+

+
+

Using Terrain2STL to generate the topography
+

+

+

+
+

+
Milling on the Bantam Tools Desktop CNC Milling Machine
+

+

+

+ + +
+

Some more milling media
+

+

+

+ +
+ +

+
and some more milling media
+

+

+

+ +
+

... and the results
+

+

+

Flat-Pack Stool

+

+
+

+
Flat-Pack Stool model in Fusion 360
+

+

+

+ +
+

Milling in progress
+

+

+

+

+ + +
+

... and the results
+

+

+
+
+
    +
  1. +

    https://www.cnccookbook.com/machining-carbon-fiber-composites-drilling-cnc-tools/ 

    +
  2. +
  3. +

    https://dragonplate.com/how-to-cut-carbon-fiber 

    +
  4. +
  5. +

    https://www.gutenberg.org/files/12299/12299-h/12299-h.htm 

    +
  6. +
  7. +

    https://www.practicalmachinist.com/vb/general-archive/face-milling-pvc-152424/ 

    +
  8. +
  9. +

    https://www.precisebits.com/tutorials/spindle-rpm 

    +
  10. +
  11. +

    https://pub.pages.cba.mit.edu/feed_speeds/ 

    +
  12. +
  13. +

    https://www.protolabs.com/resources/design-tips/cnc-machining-materials/ 

    +
  14. +
  15. +

    https://www.3erp.com/blog/cnc-milling-materials-choose-application/ 

    +
  16. +
  17. +

    https://www.hubs.com/knowledge-base/selecting-right-cnc-material/ 

    +
  18. +
+
+ + + + + + + + + + + + + + + + + + +

Comments

+ + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Projects/Octoprint/index.html b/Projects/Octoprint/index.html new file mode 100644 index 00000000..3d9af424 --- /dev/null +++ b/Projects/Octoprint/index.html @@ -0,0 +1,984 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Octoprint Setup - Teddy Warner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + +
+ +
+ + +
+ + + + + + + + + +
+
+ + + + + + + + + + + +
+
+ +
+
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + +

+ +

+ +

+ + + +

+ +

+ + + + + + +

+ +

+ + + +

+ + +

+ + + + + +

+ + +
+ + + + + + +

Octoprint Setup Documentation

+
+ Teddy Warner's GitHub profile picture Teddy Warner| Fall, 2021 | 9-11 minutes + +
+ +
+

+

Octoprint logo and interface overview

+

+

Octoprint takes 3D printing to new heights, creating a more concise workflow with more accessible machine control. The opensource Octoprint project was created and maintained by Gina Häußge. The software enables a machine with a web interface with full machine controls and a world of community plugins - all of which run on a Raspberry Pi made Server with an octoprint image. I use Octoprint on all of my personal printers, and a special CNC-focused fork of the software for my MPCNC. As mentioned above, web interfacing a machine creates a more concise workflow and thus is great for personal machines. However, an Octoprint setup shines even more in a print farm instance. During the winter months of my sophomore year (2020) I set up secure Octoprint instances on each of my lab’s array of 20 FDM printers. Each of my setup instances can be read seen under the My Octoprint Instances section of this page.

+

Octoprint Installation

+
+

Octoprint Installation Requirements

+

You’ll need a Raspberry Pi (Recommended hardware: Raspberry Pi 3B, 3B+ or 4B), a sufficient power source for the Pi, as well as a micro SD card (4GB or larger) to flash the Octorpint image to.

+
+

+

Download the Latest Octopi Image

+

+

First, download and flash the latest Octoprint image (linked above). This Pi image can be flashed as you would with any other Raspberry Pi project - I use balenaEtcher for flashing, its simple 3-step interface makes the process super straightforward. After a successful flash of this image on your Pi’s SD card configure your WiFi information in the octopi-wpa-supplicant.txt located on the flashed SD card (If you’re using wired internet, you can skip this setup). To update the octopi-wpa-supplicant.txt file with your network, open the file in a code editor of your choice.

+
+

Do not use WordPad (Windows) or TextEdit (MacOS X)

+

These editors will mess with the file layout, and cause problems with your setup.

+
+

Next, locate the type of network you’ll be connecting to - for this example, I’ll be using the WPA/WPA2 Secured standard,

+
## WPA/WPA2 secured
+#network={
+#  ssid="put SSID here"
+#  psk="put password here"
+#}
+
+

for any type of connection you choose, you can enable it by uncommenting the sections lines with a single hashtag, leaving you with a block like below.

+
## WPA/WPA2 secured
+network={
+  ssid="put SSID here" // (1)
+  psk="put password here" // (2)
+}
+
+
    +
  1. Replace “put SSID here” with your network’s SSID.
  2. +
  3. Replace “put password here” with your network’s password.
  4. +
+

Next, scroll down to the bottom of the document to the section starting ”# Uncomment the country your Pi is in…”. Here, remove the hashtag in front of your network’s country, and add a hashtag in front of all other (non-selected) countries.

+

Then, after saving your file and ejecting your SD, you can install your flashed SD in your Pi and boot.

+

Next, some basic Pi config. Make sure you are on the same WiFi network you set up your Pi on, and then SSH into your booted Pi at the IP octopi.local with the Pi’s default credentials (Username - pi / Password - raspberry). I use Putty as my SSH client as again, it’s pretty straightforward to use. Once connected to your Pi, run the command …

+
sudo raspi-config
+
+

to open the configuration GUI. Here you can change your Pi’s credentials (highly recommended), set up your local timezone (to allow for accurate machine ETAs), and change your machine’s hostname (thus changing the URL from the default octopi.local to YOURHOSTNAME.local). After all, changes, be sure to reboot your Pi.

+
+

Congrats!

+

You have now installed Octoprint on your Raspberry Pi, and can now connect to its web server with the URL http://octopi.local ( or if you changed your hostname, with the URL http://YOURHOSTNAME.local).

+
+

Octoprint Configuration

+

With an Octoprint instance up and running, the difficult part of the setup is complete. Access to the web interface allows for easy machine and interface setup, in addition to some more customizations in the form of community plug-ins.

+

Interface Config

+

Beginning with the configuration of the interface itself, basic settings can be found in the web interface by clicking on the wrench icon on the right side of the navigation bar located at the top of the interface. Scrolling through these menus allows for total customization of the interface. A good starting place is under the Octoprint section in the Appearance menu. Here (as shown below) you can customize the interface title - displayed in the interface’s navigation bar, as well as the default interface highlight color.

+

+

Octoprint appearance settings menu

+

+

Printer Setup

+

To enable Octoprint’s main purpose, a machine must be connected to the hardware running Octoprint via serial. This connection can come in the form of a USB cable in the simplest setup, or GPIO pins can be used for communication as well.

+
+

Personal Setup

+

In the case of my Prusa I3 MK3S+, I took advantage of this GPIO communication setup, to allow for the mounting of my Raspberry Pi directly under my printer’s mainboard (via this Raspberry Pi 4 Case) without a bulky USB cable connecting them … +

+Raspberry Pi mounted under printer mainboard using 3030 mount +

+
+

Your serial connection must be set up software side in the Serial Connection menu found under the Printer section of the interface settings. Octoprint can automatically detect your Serial Port and Baudrate when AUTO is selected in these dropdowns, however, you may also specify specifics for each field.

+

+

Serial connection settings for printer configuration

+

+

Next, a machine profile must be created for your connected machine to ensure safe machine control with regard to hardware limits. A profile can be created in the Printer Profiles menu under the Printer section of the interface’s settings. Create a new profile by clicking the Add Profile… button in the lower right corner of the menu, and fill out the profile with your machine’s information.

+

+

Printer profiles configuration menu

+

+

One key feature of Octoprint is its webcam machine stream, allowing for remote supervision of your machine. Anything from USB webcams to Raspberry Pi ribbon cable cameras can be used for this stream & Octoprint will automatically find this attached webcam and will use it to stream your machine (some more exotic cameras or mounting styles may require an additional configuration - discussed below).

+
+

Personal Setup

+

On my printer, I’ve mounted a Raspberry Pi camera to the X-Axis stepper of my machine (via this Raspberry Pi camera mount), yielding a pretty nice side view of prints, shown below. +

+Raspberry Pi camera mounted to X-axis stepper motor +Octoprint webcam feed showing side view of print +

+
+

Due to my printers camera mount, my Raspberry Pi camera is heald in an upsidedown orientation and thus needs to be compensated for on the software side. Luckily, in the Webcam & Timelapse menu under the Features section of the Octorprint interface settings, webcam orientation can be changed. In addition, more advanced webcam options can be found, allowing for the use of almost all cameras connected to the hardware running Octorpint.

+

+

Webcam and timelapse settings configuration

+

+

Plugins

+

One of the greatest features of Octorpint stems from its open-source nature … its community plugins. Under the Plugin Manager menu in the Octoprint section of the interface’s settings, plugins can be added to the octoprint instance, allowing for entire customization of the instance from the interface to machine interaction. There are tons upon tons of plugins that exist in the Octorprint community, and you may even develop your own, however below is a list of plugins I find to be amazing additions to an instance

+
+

Note

+

Some of these shown plugins are machine specific to my Prusa I3 MK3S+

+
+

+

Plugin manager showing installed plugins

+

+
    +
  • Access Anywhere - The Spaghetti Detective1 - AI-powered failure detection & Remote Octoprint Access
  • +
+

+

The Spaghetti Detective home interface + The Spaghetti Detective mobile app interface

+

The Spaghetti Detective printer control interface

+

+
    +
  • Bed Visualizer2 - Uses Plotly js library to render a 3D surface of the bed’s reported mesh
  • +
+

+

Bed visualizer showing mesh leveling data in 3D

+

+
    +
  • Custom Background3 - Change the background image on the temperature graph +
  • +
+

Custom background image on temperature graph

+

+
    +
  • Exclude Region4 - Adds the ability to prevent printing within rectangular or circular regions of the currently active gcode file
  • +
+

+

Exclude region configuration menu

+

+
    +
  • Floating Navbar5 - Make the navbar stick to the top of the page while scrolling
  • +
  • GcodeEditor6 - Edit gcode that’s been uploaded to OctoPrint
  • +
+

+

Gcode editor interface + Gcode editor interface

+

+
    +
  • Heater Timeout7 - Automatically shut off heaters if no print has been started
  • +
  • Navbar Temp8 - Display temperatures on the navbar
  • +
+

+

Navbar temperature display

+

+
    +
  • Octoprint Display ETA9 - Show finish time (ETA) for current print
  • +
+

+

Octoprint display ETA

+

+
    +
  • PrettyGCode10 - adds a 3D GCode visualizer tab in Octoprint
  • +
+

+

PrettyGCode 3D GCode visualizer tab

+

+
    +
  • Themeify11 - Beautiful themes for octoprint
  • +
  • ipOnConnect12 - Display the ip address of the connected OctoPrint instance on the control panel ⚠ MK3S+ specific
  • +
+

+

IP address display on control panel

+

+

Octoprint Interface Workflow

+

Although the Octoprint web interface is rather intuitive, the number of stock features (not to mention plugin-enabled ones) can be a bit overwhelming without a bit of exploring. Below is a short PowerPoint I put together walking through the basics of the interface for students using my lab print farm, allowing for a bit of knowledge before exploration.

+ + +

My Octoprint Instances

+

+

Personal Octoprint enabled Prusa I3 MK3S+ +
+

My Personal Octoprint Enabled Prusa I3 MK3S+
+

+

Lab's Octoprint enabled printer farm +
+

My Lab's Octoprint Enabled Printer Farm
+

+

+
+
+
    +
  1. +

    https://plugins.octoprint.org/plugins/thespaghettidetective/ 

    +
  2. +
  3. +

    https://plugins.octoprint.org/plugins/bedlevelvisualizer/ 

    +
  4. +
  5. +

    https://plugins.octoprint.org/plugins/custombackground/ 

    +
  6. +
  7. +

    https://plugins.octoprint.org/plugins/excluderegion/ 

    +
  8. +
  9. +

    https://plugins.octoprint.org/plugins/floatingnavbar/ 

    +
  10. +
  11. +

    https://plugins.octoprint.org/plugins/GcodeEditor/ 

    +
  12. +
  13. +

    https://plugins.octoprint.org/plugins/HeaterTimeout/ 

    +
  14. +
  15. +

    https://plugins.octoprint.org/plugins/navbartemp/ 

    +
  16. +
  17. +

    https://plugins.octoprint.org/plugins/display_eta/ 

    +
  18. +
  19. +

    https://plugins.octoprint.org/plugins/prettygcode/ 

    +
  20. +
  21. +

    https://plugins.octoprint.org/plugins/themeify/ 

    +
  22. +
  23. +

    https://plugins.octoprint.org/plugins/ipOnConnect/ 

    +
  24. +
+
+ + + + + + + + + + + + + + + + + + +

Comments

+ + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Projects/ParametricGenerator/index.html b/Projects/ParametricGenerator/index.html new file mode 100644 index 00000000..bbf0845b --- /dev/null +++ b/Projects/ParametricGenerator/index.html @@ -0,0 +1,907 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Parametric Bottle Cap Generator - Teddy Warner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + +
+ +
+ + +
+ + + + + + + + + +
+
+ + + + + + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + +

+ +

+ +

+ + + +

+ +

+ + + + + + +

+ +

+ + + +

+ + +

+ + + + + +

+ + +
+ + + + + + +

Parametric Bottle Cap Generator

+
+ Teddy Warner's GitHub profile picture Teddy Warner| Fall, 2021 | 6-7 minutes + +
+ +
+

+

Lost a lid or just want a more functional cap? Generate and print your own, compatible with any existing threads!

+

Beauty shot of various generated bottle caps

+

Be Sure to check out this projects page on PrusaPrinters, and its Instructable!

+

Parametric Bottle Cap Generator Files

+

+

CAD & Testing

+

This model was designed in Fusion 360 and uses 3 required input parameters and standard Metric thread profile equations to generate a cap that will perfectly fit any of your threaded containers. Input Parameters can be found from an existing threaded connector following the documentation below and their values can be written in their corresponding Expression boxes in the Parameters spreadsheet of the attached Parametric Bottle Cap Fusion 360 file (shown below)

+

+

Fusion 360 parameters spreadsheet for cap generation

+

+
+

Project Origins

+

I originally set out to make this generator due to a need for a lower profile cap for the isopropyl alcohol bottle I keep next to my printer. The thread profile of this bottle is rather abnormal, and thus I found myself finding thread component values with standard Metric thread profile equations, the same used by the generator.

+
+

Following the standard Metric thread profile1 (displayed in the diagram below)…

+

+

+

+

The model derives all necessary values from three required input parameters, all of which are fed into Fusion 360’s coil tool, creating entirely parametrically generated threads. The calculations for user parameters derived from the three required input parameters are as followed …

+
    +
  • Thread Height - 0.8660254037844386 * ThreadPitch
  • +
  • Hole Size - ConnectDiamater + ThreadHight
  • +
  • ThreadDmax - ConnectDiamater
  • +
  • ThreadDmin - ThreadDmax - 2 * 5 / 8 * ThreadHight
  • +
  • Cap Diamater - HoleSize + ThreadHight
  • +
  • Cap Height - ConnectLegnth + ThreadPitch / 2 + 1.5 mm
  • +
+

The implementation of these standard Metric thread profile equations in a Fusion model parametrically was the real kicker of this design. Unfortunately, Fusion’s native thread tool is incompatible with user parameters and thus was unusable in the case of this generator. In its place, I utilized Fusion’s coil tool, manipulating the values found in the generator’s user parameters to create the caps inner threads. The final working coil tool calculations are as followed …

+
    +
  • Diameter - ThreadDmax + ThreadHight / 4 * 2
  • +
  • Height - ConnectLegnth + ThreadPitch / 2
  • +
  • Pitch - ThreadPitch
  • +
  • Angle - 0.0 deg
  • +
  • Selection Size - ThreadHeight
  • +
+

all of which are included, shown below, to generate the cap’s threads.

+

+

Fusion 360 coil tool settings for thread generation

+

+

Following the Generation of the caps thread, an inner contour is added defined by the ISO 965-1 standard2 - shown in the diagram below.

+

+

+

+

This standard calls for radius value ThreadPitch / 4, and thus the following values are used in the inner contour …

+
    +
  • Radius - ThreadPitch / 4
  • +
  • Radius Type - Constant
  • +
+

The contour is created with Fusion’s Fillet tool and the prior mentioned values, shown below.

+

+

Fusion 360 fillet tool settings for thread contour

+

+

All this yields the successful basic generator, embedded below …

+

+ + +

+

… however, what fun would a custom cap generator be without a little customization. The generator includes four different body styles,

+
    +
  1. Plain
  2. +
  3. Single-Hole
  4. +
  5. Salt-Shaker
  6. +
  7. Lanyard
  8. +
+

… allowing for total cap customization. These styles can be changed, along with two other customization factors, discussed in the Cap Generation section below.

+

Cap Generation -

+

For documentation purposes, I created a new cap for my Nalgene water bottle …

+

Required Measurements

+

There are three measurements required to generate your cap, all of which can be taken from the existing threaded connector …

+
    +
  1. +

    Connector Diameter - +Measure the diameter (in MM) of your existing connector, from the very farthest point (i.e. the point of the thread) on either side. +

    +Measuring connector diameter with calipers +
    +Then, update the Expression value in the ConnectDiameter row (the box highlighted yellow below) with this found value. +
    +ConnectDiameter parameter input in Fusion 360 +

    +
  2. +
  3. +

    Connector Length - +Measure the height (in MM) of your existing connector, from the top lip to underneath the threads. +

    +Measuring connector length with calipers +
    +Then, update the Expression value in the ConnectLegnth row (the box highlighted yellow below) with this found value. +
    +ConnectLength parameter input in Fusion 360 +

    +
  4. +
  5. +

    Thread Pitch - +Measure the thread pitch of your existing connector, the distance in MM between the points of two sequential threads. +

    +Measuring thread pitch with calipers +
    +Then, update the Expression value in the ThreadPitch row (the box highlighted yellow below) with this found value. +
    +ThreadPitch parameter input in Fusion 360 +

    +
  6. +
+

Optional Customization

+

To offer a bit more customization to each generated cap, there are a couple of different preferences allowing for different functions.

+
    +
  1. +

    Number of Grips - +The number of grips lining the edge of the cap can be changed in the Expression value of the NumofGrips row. I find values between 40 through 55 work best, but if your experimenting, going below 11 will stop the generation of grip chamfered. +

    +NumofGrips parameter input in Fusion 360 +

    +
  2. +
  3. +

    Grip Depth - +The depths of these grips can be altered, determining how grippy your grips are. I’ve found a value around 0.3 or 0.4 offers a good texture around the edge. +

    +GripDepth parameter input in Fusion 360 +

    +
  4. +
  5. +

    Lid Style - +The lid style of your cap can be toggled between 4 presets in the Fusion Parametric Bottle Cap file by navigating to

    +
  6. +
+
Parametric-Bottle_Cap > Bodies > Styles
+
+

in the Fusion browser. The lid styles can be toggled between via the eye icon to the left of each style. The four styles are included below, with each of the toggles highlighted.

+
    +
  1. Plain - +
    +Plain cap style selection in Fusion 360 +
  2. +
  3. Single Hole - +
    +Single hole cap style selection in Fusion 360 +
  4. +
  5. Salt Shaker - +
    +Salt shaker cap style selection in Fusion 360 +
  6. +
  7. Lanyard - +
    +Lanyard cap style selection in Fusion 360 +
  8. +
+
+

Congrats!

+

You’ve successfully generated your own bottle cap!

+
+
+
+
    +
  1. +

    https://amesweb.info/Screws/metric-thread-profile-form-formula.aspx 

    +
  2. +
  3. +

    https://www.iso.org/standard/57778.html 

    +
  4. +
+
+ + + + + + + + + + + + + + + + + + +

Comments

+ + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Projects/ProjectPortfolioSite/index.html b/Projects/ProjectPortfolioSite/index.html new file mode 100644 index 00000000..94fba96d --- /dev/null +++ b/Projects/ProjectPortfolioSite/index.html @@ -0,0 +1,1547 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Project Portfolio Site - Teddy Warner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + +
+ +
+ + +
+ + + + + + + + + +
+
+ + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

+ +

+ +

+ + + +

+ +

+ + + + + + +

+ +

+ + + +

+ + +

+ + + + + +

+ + +
+ + + + + + +

Project Portfolio Site

+
+ Teddy Warner's GitHub profile picture Teddy Warner| Summer, 2021 | 6-7 minutes + +
+ +
+

The behind the scenes of the site you are on right now :)

+

This site relies on MKdocs, a static site generator is written in python to create the content of the site, as well as the MKdocs Material Theme for customization. I used this same static site generator to create my Fab Academy Documentation Site, however, that site runs strictly on the MKdocs Material Theme without custom alterations. This site relies on the same structure as the prior Fab Academy Documentation Site but includes experiments with entirely custom components, and an altered MKdocs Material Theme. In addition to all that, it’s also built and hosted through my personal GitHub Pages repo, under my personal domain.

+

Repo Setup and Cloning

+

To start the site I created a new GitHub Pages repo on my GitHub and cloned in some of my site files from my prior Fab Academy Documentation Site (the cloned files were just so site setup files, as well as a bit of content on my Fab Academy final project to be displayed on this site). I then pulled this repo to my local system folder with the command

+
git clone REPO-HERE # (1)
+
+
    +
  1. Replace the text REPO-HERE with your site repo’s Clone with SSH link. This can be found under the Clone button in most Git web interfaces (i.e. Gitlab, Github, etc.).
  2. +
+
+Do I Need to Clone my Repo Locally? +

Modern Hosting Platforms such as GitHub or GitLab allow for Repo editing to take place directly in the browser, and thus technically speaking, one does not need to pull their site Repo locally. However, browser editing eliminates the possibility of locally hosting, and thus one cannot view changes before pushing publically. Despite this, If you’d still like to work in a Browser-based editor, you may skip the SSH Setup and Site Publishing sections, and follow an editing and pushing workflow for your specific browser-based editor.

+
+

SSH setup

+

To clone a Git repository locally, an SSH key must be generated on your computer and then attached to your GitHub account. GitHub has really good documentation on creating a new SSH key and attaching it to your Git Web Interface (Github, Gitlab, etc.).

+

The process for generating and attaching a new SSH key to your Git Web Interface is relatively pain-free when following this documentation and using the command written above. My process involved running these commands …

+
ssh-keygen -t ed25519 -C "<Your-SSH-Name-Here>" # (1)
+
+Generating public/private ed25519 key pair.
+Enter file in which to save the key (/home/user/.ssh/id_ed25519): # (2)
+
+Enter passphrase (empty for no passphrase): # (3)
+Enter the same passphrase again:
+
+cat ~/.ssh/id_ed25519.pub | clip # (4)
+
+
    +
  1. +

    This generates a new SSH key on your computer.

    +
  2. +
  3. +

    Click enter to confirm location.

    +
  4. +
  5. +

    Enter a passphrase for your SSH key, you will need this password whenever you attempt to push your site.

    +
  6. +
  7. +

    This copies your new SSH key to clipboard. Replace id_ed25519.pub with your filename. For example, use id_rsa.pub for RSA.

    +
  8. +
+

Once copied to the clipboard, you can paste the newly generated SSH key in your SSH key settings in your Git Web Interface, to link your computer and your Git Web Interface. Now you can finally clone your site Repository locally as discussed above.

+

Mkdocs Material Install & Site Building

+
+

Requirements

+
    +
  • Latest Version of Python ⚠ Be Sure Your Python Installation is Installed to PATH in the setup wizard
  • +
  • Latest Version of Git
  • +
+
+

Open your cloned GitHub Pages repo in your code editor of choice, my personal favorite source-code editor is Visual Studio Code, so naturally, I used it. In a new terminal in the clone site folder, run the command

+

pip install mkdocs-material
+
+This will install all dependencies needed to run a static site with MKdocs, as well as the MKdocs Material Theme.

+
+Note for Fab Academy Students +

The MKdocs Material theme is paired with the mkdocs-git-revision-date-localized-plugin by default in your Fab Academy student repo. This plugin enables displaying the date of the last git modification of a page at the bottom of each page. To build your static site, this plugin must be installed in your local site folder with the line below …

+
pip3 install mkdocs-git-revision-date-localized-plugin
+
+

However, one may opt to not use it via the removal of the line

+
- git-revision-date-localized
+
+

found under the plugins section of your sites mkdocs.yml file.

+
+

Following this setup, I build my static site locally with the command in my site folders terminal

+
mkdocs serve
+
+

and followed the link given in my computer’s web browser. You should get an output like this after the mkdocs serve command …

+
INFO     -  Documentation built in X.XX seconds
+INFO     -  [XX:XX:XX] Serving on http://YOUR-LOCAL-SITE/
+
+

Auto Site Building

+
+

GitHub Actions

+

The easiest way for your static site to be built in GitHub pages upon a push is through GitHub Actions allowing for an automated workflow to run to build and pipeline your static site publicly.

+
+

At the root of your repository, create a new GitHub Actions +workflow, e.g. .github/workflows/ci.yml, and copy and paste the following +contents:

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
name: ci # (1)
+on:
+  push:
+     branches: # (2)
+      - master
+       - main
+jobs:
+  deploy:
+    runs-on: ubuntu-latest
+    steps:
+       - uses: actions/checkout@v2
+       - uses: actions/setup-python@v2
+         with:
+          python-version: 3.x
+       - run: pip install mkdocs-material # (3)
+       - run: mkdocs gh-deploy --force
+
+
    +
  1. +

    You can change the name to your liking.

    +
  2. +
  3. +

    At some point, GitHub renamed master to main. If your default branch is named master, you can safely remove main, vice versa.

    +
  4. +
  5. +

    This is the place to install further MkDocs plugins or Markdown extensions with pip to be used during the build.

    +
  6. +
+

For my site setup, I added the line …

+
- run: pip install mkdocs-git-revision-date-localized-plugin
+
+

… to install the Git Revision date plugin to my site, allowing for the date each page was updated to be displayed under all of the contents of the page.

+

Site Customisation

+

The basic setup and customization of your static site are covered wonderfully on the Mkdocs Material Theme Setup Documentation. However, following this basic theme setup, your site can be altered farther by extending the Material Theme - Witch can be read about here. All of my overrides can be found in this site’s GitHub repo.

+
+Git Pull +

When working with other developers in a Git Repo, the Git Pull command is necessary to keep your local branch up to date with others’ work. Running the line …

+
git pull
+
+

will pull in any pushed changes to your repository, allowing you to merge work/changes with work from other branches.

+
+

After working on teddywarner.org for a couple of months, I began to develop a project template page that accumulates different front-end HTML, CSS & Markdown aspects I use on my pages, which can be found via the button below.

+

+

Project Page Template

+

+

Here I’ve also compiled a bit of a mkdocs markdown cheat sheet with 10 sections for customization, formatting, or additional flair. Again, the Mkdocs Material Theme Setup Documentation covers the theme-specific markdown at length and is a nice resource while setting up specific theme-specific bits. This section compiles theme-specific markdown with overall markdown features & all of the markdowns behind it can be found in the following sections, or on this page source - linked below.

+

+

Project Portfolio Site Source Code

+

+

1. Markdown Meta Data

+
markdown_extensions:
+  - meta
+
+
1
+2
+3
+4
+5
+6
+7
+8
---
+title: PROJECT-TITLE
+description: Nullam urna elit, malesuada eget finibus ut, ac tortor.
+template: custom.html
+hide:
+  - navigation
+  - toc
+---
+
+
+

2. Basic Syntax

+
+
+
+

![alt text](imageURL.jpg){: align=right/left width="100%"}
+  <figcaption>Caption</figcaption>
+
+
+ Terminal output showing successful mkdocs serve command +
Caption Example
+

+
+
+
# Heading 1
+## Heading 2
+### Heading 3
+#### Heading 4
+##### Heading 5
+###### Heading 6
+
+

+

Heading 1

+

Heading 2

+

Heading 3

+

Heading 4

+
Heading 5
+
Heading 6
+

+
+
+

[Hyperlink Example](URl)
+
+
+ Hyperlink Example +

+
+
+
> blockquote
+
+
+

Vivamus id mi enim. Integer id turpis sapien. Ut condimentum lobortis. Aliquam purus tellus, faucibus eget urna at, iaculis venenatis nulla. Vivamus a pharetra leo.

+
+
+
+
+
+

3. Text Formatting

+
markdown_extensions:
+  - pymdownx.critic
+  - pymdownx.caret
+  - pymdownx.keys
+  - pymdownx.mark
+  - pymdownx.tilde
+
+
    +
  • +

    Bold Text - **bold text**

    +
  • +
  • +

    Italicized Text - *italicized text*

    +
  • +
  • +

    Strikethrough - ~~Strikethrough~~

    +
  • +
  • +

    Subscript - H2O - H~2~O

    +
  • +
  • +

    Superscript - X2 - X^2^

    +
  • +
+
+

4. Buttons

+
markdown_extensions:
+  - attr_list
+
+
1
+2
[Solid Button :fontawesome-solid-download:](URL){ .md-button .md-button--primary}
+[Outline Button](URL){ .md-button}
+
+

+

Solid Button + Outline Button

+

+
+

5. Different List Styles

+
markdown_extensions:
+  - def_list
+  - pymdownx.tasklist:
+      custom_checkbox: true
+
+
+
+
+

“Unordered lists can be written by prefixing a line with a -, * or + list marker, all of which can be used interchangeably. Furthermore, all flavors of lists can be nested inside each other:” 1

+
1
+2
+3
+4
+5
+6
+7
- Duis mollis est eget nibh volutpat, fermentum aliquet dui mollis.
+    * Nullam dignissim ultrices urna non auctor.
+- Nam vulputate tincidunt fringilla.
+    * Nam vulputate tincidunt fringilla.
+    * Nullam dignissim ultrices urna non auctor.
+- Nullam dignissim ultrices urna non auctor.
+    * Duis mollis est eget nibh volutpat, fermentum aliquet dui mollis.
+
+

Result -

+
    +
  • Duis mollis est eget nibh volutpat, fermentum aliquet dui mollis.
      +
    • Nullam dignissim ultrices urna non auctor.
    • +
    +
  • +
  • Nam vulputate tincidunt fringilla.
      +
    • Nam vulputate tincidunt fringilla.
    • +
    • Nullam dignissim ultrices urna non auctor.
    • +
    +
  • +
  • Nullam dignissim ultrices urna non auctor.
      +
    • Duis mollis est eget nibh volutpat, fermentum aliquet dui mollis.
    • +
    +
  • +
+
+
+

“Ordered lists must start with a number immediately followed by a dot. The numbers do not need to be consecutive and can be all set to 1., as they will be re-numbered when rendered:” 2

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
  1.  Vivamus id mi enim. Integer id turpis sapien. Ut condimentum lobortis
+      sagittis. Aliquam purus tellus, faucibus eget urna at, iaculis venenatis
+      nulla. Vivamus a pharetra leo.
+
+      1.  Vivamus venenatis porttitor tortor sit amet rutrum. Pellentesque aliquet
+          quam enim, eu volutpat urna rutrum a. Nam vehicula nunc mauris, a
+          ultricies libero efficitur sed.
+
+      2.  Morbi eget dapibus felis. Vivamus venenatis porttitor tortor sit amet
+          rutrum. Pellentesque aliquet quam enim, eu volutpat urna rutrum a.
+
+          1.  Mauris dictum mi lacus
+          2.  Ut sit amet placerat ante
+          3.  Suspendisse ac eros arcu
+
+

Result -

+
    +
  1. +

    Vivamus id mi enim. Integer id turpis sapien. Ut condimentum lobortis + sagittis. Aliquam purus tellus, faucibus eget urna at, iaculis venenatis + nulla. Vivamus a pharetra leo.

    +
      +
    1. +

      Vivamus venenatis porttitor tortor sit amet rutrum. Pellentesque aliquet + quam enim, eu volutpat urna rutrum a. Nam vehicula nunc mauris, a + ultricies libero efficitur sed.

      +
    2. +
    3. +

      Morbi eget dapibus felis. Vivamus venenatis porttitor tortor sit amet + rutrum. Pellentesque aliquet quam enim, eu volutpat urna rutrum a.

      +
        +
      1. Mauris dictum mi lacus
      2. +
      3. Ut sit amet placerat ante
      4. +
      5. Suspendisse ac eros arcu
      6. +
      +
    4. +
    +
  2. +
+
+
+
1
+2
+3
+4
+5
+6
- [x] Lorem ipsum dolor sit amet, consectetur adipiscing elit
+- [ ] Vestibulum convallis sit amet nisi a tincidunt
+    * [x] In hac habitasse platea dictumst
+    * [x] In scelerisque nibh non dolor mollis congue sed et metus
+    * [ ] Praesent sed risus massa
+- [ ] Aenean pretium efficitur erat, donec pharetra, ligula non scelerisque
+
+

Result -

+
    +
  • Lorem ipsum dolor sit amet, consectetur adipiscing elit
  • +
  • Vestibulum convallis sit amet nisi a tincidunt
      +
    • In hac habitasse platea dictumst
    • +
    • In scelerisque nibh non dolor mollis congue sed et metus
    • +
    • Praesent sed risus massa
    • +
    +
  • +
  • Aenean pretium efficitur erat, donec pharetra, ligula non scelerisque
  • +
+
+
+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
`Lorem ipsum dolor sit amet`
+
+:   Sed sagittis eleifend rutrum. Donec vitae suscipit est. Nullam tempus
+    tellus non sem sollicitudin, quis rutrum leo facilisis.
+
+`Cras arcu libero`
+
+:   Aliquam metus eros, pretium sed nulla venenatis, faucibus auctor ex. Proin
+    ut eros sed sapien ullamcorper consequat. Nunc ligula ante.
+
+    Duis mollis est eget nibh volutpat, fermentum aliquet dui mollis.
+    Nam vulputate tincidunt fringilla.
+    Nullam dignissim ultrices urna non auctor.
+
+

Result -

+
+
Lorem ipsum dolor sit amet
+
+

Sed sagittis eleifend rutrum. Donec vitae suscipit est. Nullam tempus +tellus non sem sollicitudin, quis rutrum leo facilisis.

+
+
Cras arcu libero
+
+

Aliquam metus eros, pretium sed nulla venenatis, faucibus auctor ex. Proin +ut eros sed sapien ullamcorper consequat. Nunc ligula ante.

+

Duis mollis est eget nibh volutpat, fermentum aliquet dui mollis. +Nam vulputate tincidunt fringilla. +Nullam dignissim ultrices urna non auctor.

+
+
+
+
+
+
+

6. Content Tabs, Code Styling & Annotations

+
markdown_extensions:
+  - attr_list
+  - md_in_html
+  - pymdownx.superfences
+  - pymdownx.highlight:
+      anchor_linenums: true
+  - pymdownx.inlinehilite
+  - pymdownx.snippets
+  - pymdownx.superfences
+  - pymdownx.tabbed:
+      alternate_style: true 
+
+
theme:
+  features:
+    - content.code.annotate 
+
+
+
+
+
YAML EXAMPLE
1
+2
+3
theme:
+  features:
+    - content.code.annotate # (1)
+
+
    +
  1. 🙋‍♂️ I’m a code annotation! I can contain code, formatted + text, assets/images, … basically anything that can be expressed in Markdown.
  2. +
+
+
+
C++ EXAMPLE
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
void setup() {
+  pinMode(0, OUTPUT);
+}
+
+void loop() {
+  digitalWrite(0, HIGH); 
+  delay(500);
+  digitalWrite(0, LOW); 
+  delay(500); 
+}
+
+
+
+
PYTHON EXAMPLE
import tensorflow as tf
+
+
+
+
+
+

7. Footnotes

+
markdown_extensions:
+  - footnotes
+
+
1
+2
+3
Here's a sentence with a footnote. [^3]
+
+[^3]: This is the footnote. 
+
+

Here’s a sentence with a footnote. 3

+
+

8. Formatted Tables

+
markdown_extensions:
+  - tables
+
+
1
+2
+3
+4
+5
| Method      | Description                          |
+| ----------- | ------------------------------------ |
+| `GET`       | :material-check:     Fetch resource  |
+| `PUT`       | :material-check-all: Update resource |
+| `DELETE`    | :material-close:     Delete resource |
+
+

+ + + + + + + + + + + + + + + + + + + + + +
MethodDescription
GET Fetch resource
PUT Update resource
DELETE Delete resource
+

+
+

9. Admonitions

+
markdown_extensions:
+  - admonition
+  - pymdownx.details
+  - pymdownx.superfences
+
+
+

NOTE EXAMPLE

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod +nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor +massa, nec semper lorem quam in massa.

+
+
+

INFO EXAMPLE

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod +nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor +massa, nec semper lorem quam in massa.

+
+
+

ABSTRACT EXAMPLE

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod +nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor +massa, nec semper lorem quam in massa.

+
+
+TIP EXAMPLE +

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod +nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor +massa, nec semper lorem quam in massa.

+
+
+SUCCESS EXAMPLE +

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod +nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor +massa, nec semper lorem quam in massa.

+
+
+QUESTION EXAMPLE +

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod +nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor +massa, nec semper lorem quam in massa.

+
+
+WARNING EXAMPLE +

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod +nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor +massa, nec semper lorem quam in massa.

+
+
+FAILURE EXAMPLE +

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod +nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor +massa, nec semper lorem quam in massa.

+
+
+DANGER EXAMPLE +

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod +nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor +massa, nec semper lorem quam in massa.

+
+
+BUG EXAMPLE +

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod +nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor +massa, nec semper lorem quam in massa.

+
+
+EXAMPLE +

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod +nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor +massa, nec semper lorem quam in massa.

+
+
+QUOTE EXAMPLE +

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod +nulla. Curabitur feugiat, tortor non consequat finibus, justo purus auctor +massa, nec semper lorem quam in massa.

+
+
+

10. Icons & Emojis

+
markdown_extensions:
+  - pymdownx.emoji:
+      emoji_index: !!python/name:materialx.emoji.twemoji
+      emoji_generator: !!python/name:materialx.emoji.to_svg
+
+

+

Icon & Emoji Search Engine

+

+
:fontawesome-regular-laugh-wink:
+
+

:fontawesome-regular-laugh-wink:

+

:smile: 
+
+😄

+
+

Site Publishing

+

Finally, after any changes are made to your site, you can merge your local changes back to your GitHub Pages repo to be built and published with the commands …

+
git add . # (1)
+git commit -m "Updated Site" # (2)
+git push # (3)
+
+
    +
  1. +

    Stages all new changes made to your site.

    +
  2. +
  3. +

    Gives your push a title that will be visible on GitHub

    +
  4. +
  5. +

    Pushes changes to your GitHub - when prompted, enter your SSH password.

    +
  6. +
+
+

Congrats!

+

You now have built your very own static site with MKdocs and its Material Theme.

+
+

Wooo 🥳 🥳 as of 12/01/2021, teddywarner.org is now 100 Commits old!! +

+

200 Commits 🙌 - 05/02/2022! +

+

… and 300 Commits! 🤯 - 06/28/2023! +

+

400 Commits ⁉ - 09/18/2024! +

+
+
+
    +
  1. +

    https://squidfunk.github.io/mkdocs-material/reference/lists/#configuration 

    +
  2. +
  3. +

    https://squidfunk.github.io/mkdocs-material/reference/lists/#configuration 

    +
  4. +
  5. +

    This is the footnote. 

    +
  6. +
+
+ + + + + + + + + + + + + + + + + + +

Comments

+ + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Projects/SerialUPDI/index.html b/Projects/SerialUPDI/index.html new file mode 100644 index 00000000..296c388d --- /dev/null +++ b/Projects/SerialUPDI/index.html @@ -0,0 +1,1493 @@ + + + + + + + + + + + + + + + + + + + + + + + + + UPDI Serial Programming - Teddy Warner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + +
+ +
+ + +
+ + + + + + + + + +
+
+ + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

+ +

+ +

+ + + +

+ +

+ + + + + + +

+ +

+ + + +

+ + +

+ + + + + +

+ + +
+ + + + + + +

UPDI Serial Programming

+
+ Teddy Warner's GitHub profile picture Teddy Warner| Summer & Winter, 2021 | 24-31 minutes + +
+ +
+

Many modern small chipsets rely on the Unified Program and Debug Interface (UPDI), a one-wire interface allowing for the changing of fuzes, burning of bootloaders, or uploading of sketches to any AVR Dx-series parts or any modern AVR Microcontrollers. Due to their reliance on the Unified Program and Debug Interface, these parts cannot be programmed with classic ISP style programmers, and thus a UPDI enabled programmer is required. Although UPDI is the only programming option in the case of these parts, the one-wire interface is superior to the four-wire of a classic ISP programmer and allows for quicker programming setup and less cluttered boards due to fewer programming pins. This USB-C UPDI programmer is built with a Ft232rl chip allowing for serial interfacing, and runs alongside the megaTinyCore library witch equips your IDE for serial UPDI programming via a portable python implementation.

+

megaTinyCore Documentation

+

In short, the megaTinyCore library is “An Arduino core for the tinyAVR 0-series, 1-series, and now the 2-series”1. The megaTinyCore project was created and maintained by SpenceKonde. As mentioned above, the library equips your IDE for UPDI programming via a portable python installation and can be used alongside a handful of different programmer types. The USB-C UPDI Serial programmer documented alter on this page is currently the optimal choice out of these options due to its low component requirement and faster speeds than other programmers. In addition to serial programmers, One can use a 328p based board as a programmer (i.e. an Arduino or any of its clones) via megaTinyCore’s jtag2updi sketch, or a designated microchip programming board or and UPDI programming tool that mimicks any listed above. Dr. Adam Harris, a mentor of mine during my cycle of Fab Academy has a Simple Fab-jtag2UPDI Board project where a 328p based board is used alongside the jtag2UPDI sketch and is a great option when creating an In-Circuit programmer though the library. I manufactured this board during my Electronics Production and continued to use it for the next 16 weeks of the course.

+

Package Installation

+
+

Software Requirement

+

Arduino 1.8.13 is recommended for use with the megaTinyCore library

+
+

Recent updates to the megaTinyCore library have simplified the installation process significantly. The library relies upon a board package that can be installed via the board manager witch can be installed with 3 simple steps included below2.

+
http://drazzy.com/package_drazzy.com_index.json
+
+
    +
  1. File -> Preferences, enter the above URL in Additional Boards Manager URLs
  2. +
  3. Tools -> Boards -> Boards Manager…
  4. +
  5. Select megaTinyCore by Spence Konde and click “Install”. For best results, choose the most recent version.
  6. +
+

jtag2UPDI Programming

+

Promotion

+

As mentioned prior, jtag2updi is a sketch from the megaTinyCore library, allowing for the use of a 328p-based board (i.e. Arduino or a clone) as an In-Circuit programmer. To begin with, this programming approach, download the latest version of the jtag2udpi sketch via the button below (Code -> Download Zip).

+

+

Download the Latest jtag2udpi Sketch

+

+

Extract the contents of this downloaded sketch folder by first unzipping the downloaded folder and next moving the contained sketch folder in the unzipped folder to a separate location. Finally, Rename this sketch folder to jtag2updi, leaving you with a folder similar to the one circled in red in the image below.

+

+

File explorer showing renamed jtag2updi folder structure

+

+

Inside of this jtag2updi folder, navigate to the jtag2UPDI.ino project - circled and highlighted below - and open it in your IDE.

+

+

+

+

This will open the jtag2updi project file system, leaving you with a blank jtag2updi sketch followed by multiple file tabs in your IDE (like shown in the picture below).

+

+

+

+

You can next upload this sketch to your 328p-based board of choice as you would any other sketch.

+
+

Tip

+

As a first test to confirm the successful upload of this sketch to your board, hook up an LED to pin 6 of the board. If the board is successfully programmed, the LED will be dimly lit as shown below. This simple test eliminates a potential error while troubleshooting a project, and confirms the programmer’s working condition. +

+LED test showing successful jtag2updi programming +

+
+

Once a board is programmed with the jtag2updi project, it will act as a programmer until the next sketch is uploaded, unless the board’s reset line is cut.

+

jtag2UPDI Usage

+

To make use of a jtag2updi enabled In-Circuit programmer board, prepare a sketch in your IDE to upload to your desired board. For documentation purposes, I’ll be uploading the simple blink sketch included below to a super simple ATtiny 412 based LED board I made in week 4 of my Fab Academy cycle and documenting it for use with the Arduino IDE.

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
void setup() {
+  pinMode(0, OUTPUT);
+}
+
+void loop() {
+  digitalWrite(0, HIGH); 
+  delay(500);
+  digitalWrite(0, LOW); 
+  delay(500); 
+}
+
+

To upload code to your board via your jtag2updi enabled In-Circuit programmer board …

+
    +
  1. Wire your board to your jtag2updi In-Circuit Programmer board - UPDI pin of the board to pin 6 of jtag2updi In-Circuit Programmer board with an inline 100-470 ohm resistor running between the UPDI pin and the In-Circuit Programmer, VCC to VCC, and GND to GND
  2. +
  3. Under the Tools menu in the Arduino IDE, change your board to your board’s chipset by navigating to the megaTinyCore board menu via Tools -> Board -> megaTinyCore and then selecting your chip group from the list.
  4. +
  5. Next, specify your chip type under the Chip menu via Tools -> Chip.
  6. +
  7. For use with you jtag2updi In-Circuit Programmer board, change the programmer option to Tools–>Programmer –> jtag2updi
  8. +
  9. Change your COM port to connect via serial to your In-Circuit Programmer board if not already connected
  10. +
  11. Finally, upload your sketch via the Upload Using Programmer button found under the IDE’s Sketch menu - Not with the normal upload button (this will override your programmers jtag2updi sketch if done).
  12. +
+
+

Example

+

Following these steps on my simple ATtiny 412 board yielded the 1/2 second blink example below. +

+ +

+
+
+

Congrats!

+

You can now program any modern AVR Microcontrollers via UPDI and a jtag2updi enabled 328p-based board.

+
+

Serial Programming

+

Promotion

+
+

Serial UPDI Programming with megaTinyCore

+

Version 2.2.0 of the megaTinyCore library brings the implementation of a portable python implementation. This installation, based on pymcuprog, allows for a programming speed increase by a factor of 20 when compared to the prior jtag2udpi programming style. With this performance increase in addition to a smaller & cheaper hardware list, Serial UDPI programming with the megaTinyCore library is the most optimal form of programming I’ve used on my boards to date.

+
+

The serial programming style relies on this prior mentioned portable python installation alongside a USB-Serial adapter acting as the programmer in this case. Though this section of the page will document this programming style via a pre-made USB-Serial adapter (in this case an FTDI chip) and some other hardware bits, please note the USB-C UPDI Serial Programmer board documented later on this page - this board is a set hardware piece for this Serial UPDI programming style, and provides a permanent hardware programming board. To engage the Serial UPDI programming method without a board like this you’ll need the required hardware…

+
    +
  1. A USB-Serial Adapter - I’ll be using an FTDI FT232 for this documentation, but boards based on the CH340G or the CP210 chips work great as well
  2. +
  3. Some Jumper Wires
  4. +
  5. A Schottky Diode - I tacked some leads to an SMD package (as shown below), but if you can get your hands on a through-hole package, that works great as well. +
    + +
  6. +
  7. A 470-ohm Resistor - If creating a UPDI programmable board, incorporate this resistor into your schematic using the pinout below, running the 470-ohm resistor inline to the UPDI pin. Any value between 100 and 1k ohms will do here, but 470 is the optimal value.
  8. +
+
Target Board Programming Headers
      __________________
+-----| UPDI---\/\/------>
+-----| Gnd    470 ohms (100 ~ 1k)
+-----| Vcc
+     |__________________
+
+

If not creating your own board, and there’s no inline UPDI resistor, you’ll need to include this resistor externally. With that said, wiring between your serial adapter and the target device is diagrammed in the schematics below.

+
    +
  • VCC of Adapter to VCC of Target
  • +
  • GND of Adapter to GND of Target
  • +
  • Schottky Diode between the Rx & Tx of Serial Adapter (Cathode to Tx)
  • +
  • Rx of Adapter to UPDI of Target - Here include the 470 ohms resistor as needed
  • +
+
Ideal - internal resistor in the adapter - not more than 1k
--------------------                                 To Target device
+                DTR|                                  __________________
+    internal    Rx |--------------,------------------| UPDI---\/\/------>
+  Tx---/\/\/\---Tx |-------|<|---'          .--------| Gnd    470 ohms (100 ~ 1k)
+    resistor    Vcc|---------------------------------| Vcc
+  typ. 1k       CTS|                     .`          |__________________
+                Gnd|--------------------'             If you make a 3-pin connector, use this pinout
+--------------------
+
+or
+
+--------------------                                 To Target device
+                DTR|     External 470 ohms (100 ~ 1k) __________________
+    internal    Rx |--------------,--\/\/------------| UPDI------------->
+  Tx---/\/\/\---Tx |-------|<|---'          .--------| Gnd
+    resistor    Vcc|---------------------------------| Vcc
+  typ 1k        CTS|                     .`          |__________________
+                Gnd|--------------------'
+--------------------
+
+
Or with no internal resistor on the adapter - as long as the target has one
--------------------                                 To Target device
+                DTR|                                  __________________
+       no       Rx |--------------,------------------| UPDI---\/\/------>
+    internal    Tx |-------|<|---'          .--------| Gnd    470 ohms (100 ~ 1k)
+    resistor    Vcc|---------------------------------| Vcc
+                CTS|                     .`          |__________________
+                Gnd|--------------------'
+--------------------
+
+

I rigged up the second included schematic on a breadboard as unfortunately the blinky board I’m programming doesn’t have a valid inline resistor I can use.

+

+

+
+

Serial Programmer Setup on a Breadboard
+

+

+

Serial Programmer Usage

+

To program via your USB-Serial setup …

+
    +
  1. Navigate to the Tools -> Programmer menu and select an iteration of the Serial-UPDI options (Based on upload speeds, the tinyAVR core offers 57600 baud, 230400 baud, and 460800 baud options - thus the different speed settings)
  2. +
  3. Then, under Tools -> Port select the port connected to your USB-Serial adapter, if not already selected
  4. +
  5. Finally, upload your sketch via the Upload button and watch as your board is programmed at lightning speeds!
  6. +
+
+

Note - this serial programmer setup does not give you a serial monitor

+

You’ll need to connect a serial adapter the normal way for that. The later documented USB-C UPDI Serial Programmer & ftdi2updi boards provide a switching feature between a serial programming and serial monitoring mode, allowing for all programming and monitoring work to be done without rework of connections. Be sure to check out the specific USB-C UPDI Serial Programmer usage section for use of this feature.

+
+
+

Example

+

For this example, I modified my prior used blink sketch to run a 5-second delay as opposed to the prior 0.5 seconds…

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
void setup() {
+  pinMode(0, OUTPUT);
+}
+
+void loop() {
+  digitalWrite(0, HIGH); 
+  delay(5000);
+  digitalWrite(0, LOW); 
+  delay(5000); 
+}
+
+

I uploaded this via the three-step process listed above, yielding the successful results below.

+

+ +

+
+
+

Congrats!

+

You can now program any modern AVR Microcontrollers via UPDI with a simple USB-Serial adapter with a speed increase by a factor of twenty when compared with the prior jtag2updi method.

+
+

UPDI Serial Programmer Manufacturing

+

I made countless PCBs during my cycle of the Fab Academy, one of the first being a tool to program the rest, an In-Circuit Programmer. Being one of the first boards I fabricated myself, from the PCB milling to soldering & stuffing, I hadn’t yet taken up the practice of designing my boards, and for this assignment, fabricated the In-Circuit Programmer design of one of my mentors, Dr. Adam Harris. This board is a specialized Arduino-like board, running on an ATMega 328p with a programmer sketch provided by the megaTinyCore library.

+
+

Project Origins

+

The In-Circuit Programmer I fabricated in Fab Academy’s operation is great, but due to new updates in the megaTinyCore library, the process can be optimized for faster speeds, and the board size and component requirements can be reduced. This board marks my first steps into the world of multi-layered PCB design and fabrication, a process I’m super pumped to pick up and apply to future projects.

+
+

The USB-C UPDI Programmer is a specialized piece of hardware for use with the prior discussed serial programming method and has a couple of advantages over rigging a programmer up on a breadboard. A permanent piece of the hardware enables the serial programming method to require a bit less setup, once the board has been made. Following that, this board includes “modes”, allowing switching between serial programming and monitoring functions. When rigging a serial programmer up via a USB-serial adapter as documented prior, you cannot monitor serial from the board at the same time, but instead are required to require the board to the USB-serial adapter. The switching of “modes” on this programmer eliminates this extra hassle, handling all the required connections at the flip of a switch located on the board’s underside.

+

USB-C UPDI Design

+

It was this elimination of hassle that pushed me to create this piece of hardware, yielding a simple workflow when serial programming. The USB-C UPDI Serial Programmer is based on the FT232RL3 IC, handling the USB protocol and USB to serial data transfer on the programmer.

+ + +

The FT232RL is broken out following the same wiring principals diagrammed in the prior serial programming documentation, converting the IC’s Rx and Tx lines to a UPDI line, routed to a programming pin. Just before this Rx and Tx junction, a Double-Pole Double-Through switch allows for the switching of this conversion to UPDI to straightforward Rx and Tx lines, routed each to their corresponding pin. Thus, the programmer board includes a UPDI, Rx, and Tx output pin, in addition to a VCC and GND line. This 5 pin programming pinout can be mimicked on any board containing a AVR Microcontroller, allowing for programming and monitoring functions to be switched without reworking connections between the programmer and board.

+

+

+
+

USB-C UPDI Programmer Eagle Schematic
+

+

+

Then came the nesting process. Being my first double-sided board in Eagle, it took some work to get used to the use of vias between sides, but eventually, I worked out the process of changing Via’s drill class through Eagle’s Change -> Drill tool. I ended up using 0.6mm rivets for this board’s vias, using 8 on the board in total. I nested bards on the board, including both the FT232RL and the USB-C female adapter on the top side, along with some smoothing capacitors, and on the bottom, the UPDI programming component array, as well as the DPDT switch.

+

+

+ + +
+

USB-C UPDI Programmer Eagle Board
+

+

+

The nesting of the two highest pinout parts on the board’s topside inspired my next experiment with this board, Solder Paste Stencil making. Although I feel pretty confident in my SMD soldering skills, soldering pads this small wouldn’t only be a hassle, it would also be tedious time waste, and thus I settled on a solder paste stencil approach for the boards topside, while hand soldering the bottom half. I exported the Tcream layer of my programmer board from Eagle as a .pdf for cutting of the stencil from cardstock on a Epliog Fusion Pro 48 laser.

+

+

+
+

Tcream board layer in Eagle
+

+

Bill of Matierals - USB-C UPDI

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Qty.PartCostURL
x1SMD DPDT Switch$1.50link
x1CDBM1100-G$0.39link
x1470 ohm SMD Resistor$0.10link
x11.5k ohm SMD Resistor$0.10link
x1100nf SMD Cap$0.18link
x11uf SMD Cap$0.10link
x1DX07S016JA1R1500$1.59link
x1FT232RL$4.19link
x11Header Pins$0.14link
x1Copper PCB Blank$0.70link
+

Total Cost- $8.99

+

+

ftdi2updi Design

+

In addition to the prior USB-C UPDI Serial Programmer board, I created an alternative hardware programmer that makes use of a pre-existing FTDI chip, enabling it with the same switching functions feature as the prior board. This provided a workaround to 2021’s silicone shortage, as due to backed-up supply chains, I wasn’t able to get my hands on the FT232RL right away.

+

The board’s schematic is a cloned iteration of the prior programmer, but with the USB-C & FT232RL components removed, and replaced with headers for connection to an FTDI adapter, yielding the schematic below.

+

+

+
+

First Iteration ftdi2updi Programmer Eagle Schematic
+

+

+

I was compelled to expand on the original shape of an FTDI serial adapter for this board, as it expands on the functionality of the adapter itself. I used Inkscape to create the board shape, allowing for meshing with an FTDI serial adapter, with the board aligning two of the sides of the adapter and then imported this board shape into Eagle via an ulp as documented on my Fab Academy Final Project page. I then nested the components on both sides of the board, aligning the FTDI connection pins allowing for proper seating of the FTDI serial adapter with the board.

+

+

+ + +
+

First Iteration ftdi2updi Programmer Eagle Board
+

+

Bill of Matierals - ftdi2updi

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Qty.PartCostURL
x1SMD DPDT Switch$1.50link
x1CDBM1100-G$0.39link
x1470 ohm SMD Resistor$0.10link
x11Header Pins$0.14link
x1Copper PCB Blank$0.70link
x1FTDI Serial Adapter$14.95link
+

Total Cost (W/out FTDI)- $2.83

+

Final Cost- $17.78

+

+

Fabrication & Testing

+

ftdi2updi

+

I milled my boards with a Bantam PCB mill and its Bantam Tools Desktop Milling Machine Software. The software handles all toolpath generation from my Eagle board file, and conveniently, when paired with Bantam’s PCB placement bracket, also handles the double-sided board milling process via a toggle switch in the interface. I followed the same milling process I have documented on my week 4 Fab Academy page, however after the top-side traces and holes were milled, I flipped the copper stock, aligning to the right-hand corner of the PCB alignment bracket as opposed to the left, and toggled the board to its bottom side in the Bantam Tools Desktop Milling Machine Software. With the stock realigned, I repeated the same milling process for the bottom side of the board. This whole process is showcased in this view from Bantam.

+

I used a 0.005” PCB engraving bit for the ftdi2updi’s traces, and a 1/32” for its holes and outlines, yielding the milled results below.

+

+

+

+

+

After the milling, I washed off my board with some soap and water. The Bantam Tools Desktop Milling Machine Software makes the milling of double-sided boards super straightforward, not much harder than a single-sided PCB, however, the boards require the extra step of riveting vias between both sides of the board before soldering. This article on PCB Rivets from the Fab Docs walks through the process down into a couple of steps.

+

First the via has to be inserted into its corresponding hole, I used 0.6mm vias here. Then I used a V-Shaped nail to chamfer the top of the via and finally pressed it flush with a flat press, each step shown in the three assets/images below.

+

+

+ +

+

+

Finally, to ensure a proper connection, I soldered over each side of the via and repeated this process for all four of the board’s vias.

+

+

+ +
+

Boards with Vias
+

+

+

I finished the stuffing and soldering of this board, a simple enough process due to the small component size.

+
+

Note

+

The board that is being documented here is an early iteration of the ftdi2updi and contains an extra resistor, and thus your final board may not look exactly like the ones on this page until the Programmer Usage section.

+
+

+

+

+

+

I ran two tests on the ftdi2updi, one proving each of the board’s functions.

+
+

Programming Test

+

Beginning with its programming function, I uploaded a modified version of the same blink test sketch used above to my simple LED board …

+
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
void setup() {
+  pinMode(0, OUTPUT);
+}
+
+void loop() {
+  digitalWrite(0, HIGH); 
+  Serial.println('ON')
+  delay(5000);
+  digitalWrite(0, LOW); 
+  Serial.println('OFF')
+  delay(5000); 
+}
+
+

I uploaded this via the Programmer Usage process documented in the corresponding section below. The first upload was successful at a medium (230400) baudrate. Then I uploaded the test, and repeated it at the highest (TURBO 4.5v + 460800) baudrate, yielding the successful results below.

+

+ +

+
+
+

Note

+

The ftdi2updi serial adapter meshing headers were reversed in this iteration, and thus I attached the serial adapter upsidedown for this test, the proper orientation as outlined in the following Programmer Usage section.

+
+
+

Serial Monitoring Test

+

Next, I switched the programmer to its monitoring mode and hooked it up to Arduino RX & TX pins, shown below. Unfortunately, my simple LED board doesn’t have serial communication pins, and thus this Arduino was my next best testing option.

+

+

+

+

I uploaded this super simple serial print sketch to the Arduino …

+
1
+2
+3
+4
+5
+6
+7
+8
void setup(){
+  Serial.begin(9600);
+}
+
+void loop(){
+  Serial.println("Hello World");
+  delay(5000);
+}
+
+

and then opened the Arduino IDE’s serial monitor on my ftdi2updi port, which successfully read the serial output printed, shown below - and also was able to transmit via serial, shown by the Arduinos receiving LED.

+

+ +

+
+

Although this first iteration of the ftdi2updi worked, there were a couple of design flaws (Noted above) I’ve flatted out in later versions including - a reversed FTDI header, oversized vias, an unnecessary 1k resistor, and improper meshing with an existing FTDI serial adapter. The final version of this programmer (and its CAM files) can be accessed in the repo linked by the download button at the bottom of the Fabrication & Testing section. Use this version as refrence whilst fabricating your own board.

+

I ordered a couple of these final iteration boards from a PCB fab …

+

+

+

+

+

Which turned out incredible. The Gerber files for these boards are included in the Repo linked via the download button at the bottom of the Fabrication & Testing section, along with an Eagle board file with silkscreen decals instead of milled ones. I soldered the components to this board, a process made even easier by the already plated vias, and lack of need to insert my own.

+

+

+ +
+

ftdi2updi Front and Back
+

+

Download the FTDI2UPDI Files

+

+

USB-C UPDI

+
+

Pending Compleation

+

As mentioned prior, due to 2021’s supply chain limitations, I cannot currently get my hands on an FT232RL, and thus, for now, the fabrication & testing sections will only include my ftdi2upid board. This section will be updated by future me once I can get an FT232RL.

+
+
+

This USB-C UPDI Programmer Board is not yet complete

+

Although you can download its files, the board is still in development and may not function properly. Again, this section will be updated upon its completion.

+
+

+

+

+

+
    +
  • Process Type - Vector
  • +
  • Speed - 100.0%
  • +
  • Power - 19.0%
  • +
  • Frequency - 6.0%
  • +
+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

Download the USB-C UPDI Files

+

+

Programmer Usage

+

Serial programming with the megaTinyCore library is incredibly straightforward, and when pair with the switching functionality of both of my hardware programmers, the process is streamlined. I’ll be using a final iteration ftdi2updi board for this Programmer Usage documentation, however, these steps will work for either programmer style included on this page, regardless of fabrication technique.

+

Both Serial UPDI programmers rely on the prior documented megaTinyCore library, which must be installed for programmer usage. This process is outlined in the Package Installation section of this page.

+

Programming

+

To use the UPDI programming function, toggle the programmers switch to UPDI as shown below, and then connect UPDI and GND lines to their corresponding pins. Here you can also run a VCC line to your target board, which provides 5v, shown in the wiring below.

+
+

Note

+

Connecting serial data lines here is totally ok, as the switch disconnects them from communication when in programming mode. The wiring shown here is just the required connections for UPDI programming.

+
+

+

+
+

Switched to Progamming Mode

+

+
+

Minimum Wiring for UPDI Programming
+

+

+

Once connections are made between the programmer and the target board, you must configure your target board in your IDE. For this documentation, I’ll continue using the Arduino IDE, however other IDEs with the megaTinyCore installed can be used for this (I may do some tests with PlatformIO in the future, and will update this page accordingly).

+

First, you must select your target board’s microcontroller by navigating to …

+
Tools > Board > megaTinyCore
+
+

and then selecting your microcontroller from the list, shown in the image below.

+

+

+

+

Next, connect your programmer to your computer via a serial cable and, select your serial port by navigating to …

+
Tools > Port
+
+

and then select the serial port your programmer board is connected to (In my case this port was COM11 - shown below). If you’re on windows, you can find this connection on Device Manager before your selection in the IDE.

+

+

+

+

Finally, select your programming style. For all programmers documented on this page, the Serial UPDI w/ 4.7k resistor or diode series of styles are used, and you can select between three baudrate/speed options (highlighted in the image below) …

+
    +
  • SLOW (57600 baud)
  • +
  • (230400 baud)
  • +
  • (TURBO 4.5v + 460800 baud)
  • +
+

+

+

+

Monitoring

+

To use the serial monitoring function, toggle the programmers switch to Serial as shown below, and then connect Transmitting and Receiving Data lines as well as GND to their corresponding pins. Keep in Mind RX and TX lines must be switched between the programmer board and the target, with Transmitting of one going to Receiving of the other and vise versa. Here, as prior, you can also run a VCC line to your target board, which provides 5v.

+
+

Note

+

Connecting the UPDI line here is totally ok, as the switch disconnects it from communication when in monitoring mode. The wiring shown here is just the required connections for Serial monitoring.

+
+

+

+
+

Switched to Monitoring Mode

+

+
+

Minimum Wiring for Serial Monitoring
+

+

+

To begin serial monitoring through the programmer in the Arduino IDE, first, ensure your programmer is connected to your computer via a serial cable, and the proper boart is selected in the IDE (as done in the prior programming section - and highlighted below). Then navigate to …

+
Tools > Serial Monitor 
+
+

as selected in the picture below

+

+

+

+

This will open the IDE’s serial terminal, where incoming serial data is printed, and outgoing data can be transmitted via the input at the top of the window. I wired a programmer to a simple “Hello World” printing Arduino, yielding the received results below.

+

+

+

+
+

Congrats!

+

You now know the methods to UPDI programming with the megaTinyCore library, and may have a cool hardware programmer to show for it 😁.

+
+
+
+
    +
  1. +

    https://github.com/SpenceKonde/megaTinyCore 

    +
  2. +
  3. +

    https://github.com/SpenceKonde/megaTinyCore/blob/master/Installation.md 

    +
  4. +
  5. +

    https://drive.google.com/file/d/1sXxm9A5GLxVPGFNDkNBzLakp-TWlVKT2/view 

    +
  6. +
+
+ + + + + + + + + + + + + + + + + + +

Comments

+ + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Projects/VonNiemannProbe/index.html b/Projects/VonNiemannProbe/index.html new file mode 100644 index 00000000..fce36d92 --- /dev/null +++ b/Projects/VonNiemannProbe/index.html @@ -0,0 +1,2315 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Von Niemann Probe - Teddy Warner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ +
+ + +
+ +
+ + +
+ + + + + + + + + +
+
+ + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + +

+ +

+ +

+ + + +

+ +

+ + + + + + +

+ +

+ + + +

+ + + +

+ + + + + + +

+ + +
+ + + + + + +

Von Niemann Probe

+
+ Teddy Warner's GitHub profile picture Teddy Warner| Winter, 2023 | 46–58 minutes + +
+ +
+

+
+
+ +
+

+

An AVR-based Bluetooth telegraphing shoe insole that lets you play chess in the way Hans Niemann has been accused!

+

+
+

Project Sponsor

+

PCBWay logo +

The development of the Von Niemann Probe was made possible by PCBWay, who provided the fabrication of the probe’s main board and nylon insole. Be sure to check out PCBWay.com for all your PCB prototyping, assembly, design, CNC, and 3D printing needs! Thank you to the wonderful folks at PCBWay for sponsoring this project!

+
+

On September 4, 2022, 19-year-old rising chess superstar, Grandmaster Hans Moke Niemann, shocked the chess world when he ended the 8-time World Champion, and arguably the greatest player in world history, Magnus Carlsen’s unprecedented 53-game win streak at the Sinquefield Cup in St. Louis, Missouri. Niemann added insult to injury by accomplishing this as the lowest-ranked player in the tournament while playing with the black pieces, which possess a slight, but crucially important, disadvantage at the highest levels of competition.

+

Soon after his loss, Carlsen withdrew from the Sinquefield Cup without explanation, a bizarre occurrence due to the remaining six rounds in the tournament. While chess enthusiasts on the internet grappled with possible reasons for Carlsen’s withdrawal, Carlsen posted the tweet below, depicting Portuguese football manager José Mourinho saying “If I speak I am in big trouble”, a brief innuendo to the accusations to come.

+

+ + +

+

Carlsen’s message drove many chess fans around the globe quickly began to lob accusations of foul play at Niemann, who has had a history of cheating in online games, and speculate on how he could have cheated in an over-the-board match. A theory that Niemann used a wearable device to relay computer-generated moves was quickly popularized and became incredibly popular amongst chess fans and the broader internet. Despite the internet’s suspicions, no clear accusations were placed, nor did any solid evidence surface. Yet, on September 8th, 2022, Chess.com released the tweet seen below, confirming it had removed Hans Moke Niemann from its user base for cheating.

+

+ + +

+

These direct allegations from Chess.com spurred further allegations against Niemann, as well as his confession to cheating on two separate occasions, both in online games, once at age 12 and another at 16. Yet, Niemann denied ever cheating over the board, even offering to “strip fully naked”16 or “to play in a closed box with zero electronic transmission”16 to prove his innocence.

+

To only further the drama, on Monday, September 26, 2022, the pair were re-matched in an online tournament in which Carlsen resigned after playing a single move. Later that night, Carlsen released his open accusations against Niemann claiming he believes that “Niemann has cheated more — and more recently — than he has publicly admitted.”18 Carlsen claims he derived his initial suspicions form Niemann’s rapid rise through the chess ranks. For reference, Niemann surged from being approximately 800 in the world, to within the top 50 over the 20 months leading up to the Sinquefield Cup. GM Hikaru Nakamura describes this rise as “unprecedented”14 and Chess.com claims that Niemann is the “fastest rising top player … in modern recorded history”13, yet others find it comparable to top junior ranked players. Carlsen outline in his formal accusation (found in the tweet below) that Niemann “wasn’t tense or even fully concentrating”18 while outplaying him using the black pieces “in a way I think only a handful of players can do”18 during the tournament.

+

+ + +

+

The implications of such an unprecedented accusation against a player at the GM (Grandmaster) level are highly slanderous. Grandmaster Nigel Short notes that cheating at this level of chess is exceptionally rare, as any proven allegations would end a player’s career. GM Short told the BBC “I think in the absence of any evidence, statement or anything, then this is a very unfortunate way to go about things. It’s death by innuendo.”14 Furthering GM Short’s skepticism, Professor Kenneth Regan, widely considered to be the world’s leading expert on chess cheating, took it upon himself to analyze Niemann’s games and found no evidence of cheating.

+

Naturally, Niemann isn’t taking kindly to these slanderous accusations, and has filed a federal lawsuit accusing Carlsen of “maliciously colluding with others to defame [him] and ruin his career.”1 Niemann is after at least $100 Million in damages from defendants Carlsen, his company Play Magnus Group, the online platform Chess.com and its leader, Danny Rensch, as well as grandmaster Hikaru Nakamura. The court filing, found below, accuses GM Nakamura and Chess.com of bolstering Carlsen’s “egregious” claims through their platforms.

+

Niemann’s counterattack hasn’t ended in his pursuit of reparations, Niemann claims Carlsen couldn’t handle losing to him, implying through his court filing that Carlsen’s actions further support his notorious “inability to cope with defeat.”20 The court filing states that Carlsen’s actions were taken in an attempt to solidify his status as “king of chess”20 so he could complete his company’s buyout by Chess.com - a $82.9 million acquisition.21

+

Niemann’s victory over the world champion, “should have propelled Niemann’s career to the next level and allowed him to continue realizing his enormous potential as the next great American chess player.”20 Yet, “Unbeknownst to Niemann at the time, Defendants would do whatever it took to ensure that this would never happen.”20

+

+
+

+

+

It’s worth mentioning again that Niemann has publically admitted to cheating with aid from electronic devices in online matches, once at age 12 and again at age 16. Niemann referred to one of these instances as “an absolutely ridiculous mistake”22, while maintaining that he had never cheated in a tournament with prize money.

+

A couple of weeks after their initial accusations, Chess.com released a formal, 72-page report (found below) outlining an investigation into Niemann’s play, and alleging that Niemann has likely cheated with electronic aid in more than 100 games, and as recently as 2020, including in matches where money was on the line. The magnitude of Niemann’s alleged cheating scandal, as presented in Chess.com’s report, proves to be much larger and longer-lasting than Niemann publically admitted. Chess.com’s investigation relied on a variety of analytical tools to compare the moves of Niemann to those recommended by chess engines, and noted “many remarkable signals and unusual patterns in Hans’ path as a player.”13 The site claims that “while [they] don’t doubt that Hans is a talented player, we note that his results are statistically extraordinary”13, and their investigation into Niemann’s gameplay has uncovered “blatant cheating”.13 The report states that Niemann privately confessed to these allegations and that his Chess.com account was subsequently closed in 2020.

+

In a private letter to Niemann, Chess.com’s Chief Chess Officer, Danny Rechsch writes “We are prepared to present strong statistical evidence that confirms each of those cases above, as well as clear ‘toggling’ vs ‘non-toggling’ evidence, where you perform much better while toggling to a different screen during your moves”.13 While the report outlines that Chess.com has historically treated bans as a private matter, the ongoing accusations and Niemann’s ban from the site’s Global Championship spurred the platform to provide the investigation that provided justification for their decisions.

+

+
+

+

+

While this evidence against Niemann appears to be overwhelming, it’s important to remember that the Sinquefield Cup is an over-the-board tournament. Cheating online and over the board are two entirely separate entities, no amount of online cheating in the world provides sufficient reason to accuse Hans of over-the-board cheating, as the act itself is a different task that would require extreme preparation to achieve. Thus, in face of a lack of evidence to support over-the-board cheating allegations against Niemann, the Von Niemann Probe was born. In an attempt to further the investigation into this scandal, we developed an AVR-based Bluetooth telegraphing shoe insole to test the feasibility of creating and using an over-the-board chess cheating aid.

+
+

Cheating in Chess

+

Cheating in chess, whether online or over the board, is wrong and ruins the game for everyone. This project has been published to showcase the possibility of such a device’s existence/use, and should not be recreated with the intent of using it for genuine foul play.

+
+
+

Name Orgins

+

The Von Niemann Probe is named after a theoretical self-replicating space probe known as the Von Neumann Probe, named after legendary 20th century Hungarian-American mathematician John Von Neumann. The Von Neumann Probe is regarded as the optimal way to explore the cosmos, as it can mine asteroids and leverage the exponential growth of its progeny to more efficiently visit star systems. And, coincidentally, probes are also medical devices that can be used to explore one’s bowels. Thus, we settled on naming our device the Von Niemann Probe.

+
+

+

Von Niemann Probe Repository

+

+

Telegraph Design

+

PCB Design & Integration

+

At the heart of the Von Niemann Probe lies an AVR ATtiny 412-based Bluetooth telegraph. This PCB is capable of interpreting Bluetooth serial data from our chess engine (see Step 3), and translating the signal to pulses of a vibration motor. Before starting any PCB design work in Autodesk EAGLE, I took some time to write out these expectations for the systems operating principle and then began the PCB schematic. The VNP mainboard is a barebones ATtiny 412 setup, including voltage regulation and smoothing capacitors, as well as headers for connection to a Bluetooth module, vibration motor, and battery power.

+

The schematic includes headers for an HC-06, with serial connections to the ATTiny 412, and a shared power line with both the IC and Vibration Motor. This vibration motor is wired both to common GND on the PCB, as well as to Digital Pin 2, allowing the IC to pulse the motor to convey moves in morse code.

+

+

+

+

With this schematic done, I moved on to generating my final board file. The PCB is built around the rectangular nature of the HC-06 Bluetooth module - one of these modules is placed in the center of the PCB, allowing for an as-compact-as-possible footprint ideal for fitting in the VNP’s discrete insole body (see Step 2). Just above the HC-06 module lies the ATtiny 412 circuit and UPDI programming pins, as well as a cutout and mounting points for the telegraph’s vibration motor. The entire PCB is designed to be mounted inside the VNP insole with M3 screws and has been designed to maintain a large, shared surface area with the 3D-printed insole to ensure the transfer of vibration from the telegraph.

+

+

+

+

+ +

+

Parametric Shoe Insole

+

To prove the feasibility of the accusations placed against Hans Niemann, the Von Niemann Probe (VNP) must be robust enough to see consistent use, while remaining discrete enough to not be detected. With this in mind, we selected to place our Bluetooth telegraph inside the body of a parametric shoe insole. The VNP was designed in Autodesk’s Fusion 360, and makes heavy use of the software’s Parametric Engine, allowing a custom insole to be generated for all foot sizes. Using the shoe size standards found in the table below, an insole may be generated to fit your exact foot dimensions. Note - the sizes included in the table below are Mens, as that is the style of shoe both Jack and I wear - be sure to convert your shoe size accordingly.

+

+

Diagram of foot dimensions for insole design

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
US SizeUK SizeEuropean SizeJapanese SizeLength (SL)Width (SMW)
65.5392523.5cm8.9cm
6.5639/4025.524.1cm9cm
76.5402624.4cm9.2cm
7.5740/4126.524.8cm9.4cm
87.5412725.4cm9.5cm
8.5841/4227.525.7cm9.7cm
98.5422826cm9.8cm
9.5942/4328.526.7cm10cm
109.5432927cm10.2cm
10.51043/4429.527.3cm10.3cm
1110.5443028.9cm10.5cm
11.51144/453128.3cm10.6cm
1211.5453228.6cm10.8cm
12.51245/4629.2cm11cm
1312.54629.5cm11.1cm
13.51346/4730.2cm11.3cm
1413.54731cm11.4cm
14.51447/4831cm11.6cm
1514.54831.4cm11.7cm
+

+

+

A custom insole will be generated after updating the model with your foot Length (SL) & Width (SMW) via Fusion’s Change Parameters tool. Paste in your foot length (SL) in the InsoleLegnth expression input, and repeat with your foot width (SMW) and the InsoleWidth expression input.

+

+

+

+

After establishing the model’s parameters and extruding a basic shape, I drafted the insole’s outer edge to allow the insole to fit the angled inner walls of shoes, before using Fusion 360’s Surface workspace to ad a 3D profile to the top of the insole.

+

+

+

+

+

The insole consists of two main parts, the body which is the bulk of the insole itself, and the lid, which screws to the bottom of the body, covering the electronics compartment. The body of the insole consists of mounting points for the main PCB, as well as a 1000mah LiPo battery, a USB-C LiPo charging board, a power switch, and a 3.3v to 5v boost converter. The lid not only hides the VNP’s electronics system from sight but also redistributes the load placed upon the insole when a user is standing on it.

+

+

+

+

+
+

+

+

Chess Engine

+

The Von Niemann Probe is useless without a competent artificial intelligence running the show behind the scenes, obviously requiring a hefty amount of code to get it to work properly. I decided to build the brains around Stockfish, a famous chess engine that already has a functioning Python integration and also happens to be the highest-rated engine at the time of writing. For reference, the engine plays at a 4000 level, while the current highest-rated player, Magnus Carlsen, is currently sitting at about 3200. Safe to say, Stockfish is more than sufficient for the players our insole will encounter who are obviously considerably worse than Carlsen.

+
+

Python Dependencies

+

Various Python dependencies are needed to run the chess engine script, but overall the installation is pretty lightweight. Install commands for all Python dependencies are below, any release past Python 3.7 will work.

+
pip install regex
+pip install python-chess
+
+
+

Downloading & Configuring Stockfish

+

First, I downloaded Stockfish and extracted it locally on my Windows PC. I used Version 15 in this project. A more current version can be downloaded from stockfishchess.org or use this permanent download link of the version that I used during development.

+

Next, I researched the Stockfish Python module and integrated it into a continuous loop in Python that would allow it to play games repeatedly without requiring the relaunching of the code. The module is pretty simple to instantiate, but it does feature a few optional parameters that can regulate its strength and how much hardware it occupies locally.

+
stockfishPath = "C:/…" #replace with the path to your Stockfish exe. 
+#Note - the path may only contain forward slashes, no backslashes.
+
+

I began by adding a path variable to my Python program that stores the local path of the Stockfish installation. The path can be found by navigating to the folder that stores Stockfish and finding the .exe file inside of the folder. I then placed this path into the variable, making sure to replace the backslashes that windows copies with for forward slashes. The code will not run with backslashes.

+
fish = Stockfish(r"{0}".format(stockfishPath),
+depth=18, parameters={"Threads": 4, "Hash": 256, "UCI_LimitStrength": "false"}) #stockfish object declaration, can regulate strength
+
+print("WDL Accepted " + str(fish.does_current_engine_version_have_wdl_option()))
+
+

I then configured Stockfish inside of the program. The above snippet instantiates stockfish, creating an object called “fish” that can be called using the Python Stockfish module.

+
print("Board State " + fish.get_board_visual()) #prints unicode image of current board state
+
+

The Python Stockfish module also features a function that prints a Unicode display of the current board state! This proved very useful during debugging to follow along with games that are not being played physically.

+
while True:
+    isMate = False
+    print("side of stockfish:")
+    fishSide = input()
+    playGame(fishSide)
+
+

I then created an infinite loop that takes the side of the player wearing the insole that launches one of two loops that allow games to be played inside of the playGame() function.

+
def playGame(side):
+    global legal; global legalMoves
+    global board; global isMate; global morseMove; 
+    mate = False
+    turns = 0; i = 0
+    board = chess.Board()
+    print("fish playing as " + side)
+
+

Several declarations occur at the top of the playGame() function regardless of what side Stockfish is on. These lines help keep track of legality and reset the state of the board in the two modules that track it. The importance of these modules is detailed later on in this section.

+
if side == "white":
+        while mate == False:
+            bestMove = fish.get_best_move(1000)
+            fish.make_moves_from_current_position([bestMove]);
+            chessMove = chess.Move.from_uci(bestMove) #create chessMove 
+            print("whitefish plays " + bestMove)
+
+

I decided to start by writing code for games for when the engine is generating moves for the white pieces. Stockfish will immediately generate a move in this case so this made early development a bit easier. The above snippet generates the optimal move from the starting position and plays it.

+
print("black move:") #request player move
+move = input()
+#The above lines request the move of the engine's opponent that is playing on the black pieces.
+
+

After black’s move is input, one turn has passed, and now it is time for the engine to respond to the human player’s move. But before that can happen, the program needs to check the legality of the Stockfish move. If the Stockfish module is passed an illegal move that cannot be played given a current board position, it crashes. So I needed to create a function to test the legality of moves before passing them to the Stockfish module to avoid this potential point of failure.

+

Checking Move Legality

+

While the Python Stockfish module is robust and does have many useful features, it does not natively feature a function to check the legality of a move. As such, I decided to add the python-chess module to run a legality check on each move before passing it to Stockfish, prompting the user to enter another move if a move is not legal, preventing crashes due to misinput.

+
board = chess.Board(); #create chess board object
+
+

The above function call creates a Board object that allows for the calling of the many functions included in the python-chess module.

+

The python-chess module features a board.is_legal() function that checks the legality of a formatted move object given the current state of a board object. It returns a boolean that reflects the legality of a move. However, it needs to be passed a chess.Move object which must be created using letters and numbers that can appear in chess moves. As such, any input that is incorrect, say “n9z0” would not create a move object and would trigger a crash. Therefore, it is necessary to verify that input only contains properly formatted characters that can actually exist in a chess move before trying to use the is_legal() function.

+
badChars = ["i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
+"9", "0"] #chars that can't be in a legal chess move
+
+

I started by creating a dict that contains all of the characters that cannot appear in a legal chess move.

+
for char in move: #check if badChars exist in move
+        i = 0 #reset iterating var
+        while i < len(badChars):
+            badChar = badChars[i]
+            if char == badChar:
+                print(char + ' is ILLEGAL')
+                noBadChars = False #set bool to reflect bad char
+            if char.isalpha() == True:
+                charCount += 1 #reflect that at least 1 char in string
+            if char.isdigit() == True:
+                numCount += 1 #reflect that at least one number in string
+            i += 1
+            #print(char + "is legal") #debug bad chars function
+
+

I then wrote some code to iterate through each character in a move received as input and verify that it contains only legal characters.

+
if len(move) != 4 and len(move) != 5: #solve edge case where small string doesn't break other rules
+    noBadChars = False;
+
+

I also added a quick check to verify that a move actually contains 4 characters. A move object cannot be created by any other number of characters, so this check is useful in preventing crashes due to misinput.

+
if noBadChars == True: #only creative move objects if correct formatting
+        myMove = chess.Move.from_uci(move)
+        legal = board.is_legal(myMove)
+        print("Legal? " + str(legal))
+
+

If the user input does not contain any illegal characters and is 4 characters in length, then it can be made into a chess.Move object in the python-chess module and passed to the is_legal() function. The above snippet does precisely that.

+
if legal and noBadChars: #move can only happen if legal and doesn't contain illegal chars
+        fish.make_moves_from_current_position([move])
+        chessMove = chess.Move.from_uci(move)
+        board.push_san(move)
+        return #back to game loop
+
+

If the player’s move is legal, then it is added to the games being tracked by python-chess and Stockfish. After the move is played, the program returns to the playGame() parent function and the game will continue with a move from Stockfish.

+
else:
+    print("illegal move, input new move:")
+    newMove = input()
+    getPlayerMove(newMove)
+    return #back to game loop
+
+

If the move is not legal, the function getPlayerMove() function will recursively call back to itself until a legal move is received, at which point it will return to the playGame() parent function and the game will continue.

+

Morse Code Translation

+

After getting the legality function to work, I began focusing on solving the core challenge of this project: sending the moves generated by Stockfish to a shoe insole. I initially wrote code to translate the moves into Morse Code, but quickly discovered that due to the short nature of chess moves I could save a lot of time by just pulsing a vibrating motor for each additional index in the alphabet or number line a certain character occupied.

+

For example, a C in morse code is --, which would take 10 seconds to buzz assuming a dot length of 1 second where a 1:3 dot:dash ratio is established with a wait time of 1 dot between each character. This is standard. However, a simple *** of 0.1 second pulses with 0.05 second pauses in between accomplishes the same thing in much less time and is easily understood to be a C, because chess moves will always follow a character:number:character:number format. As such, I decided that this way would be faster and would not require me to learn 16 characters of Morse Code. Both encoding versions are in our Github repository for the project if for some reason you prefer Morse Code. For the sake of concision I am only going to include the code for the shorter encoding method that I described which is also the one that is used in all of our testing videos.

+
morseDict = { 'a':'.', 'b':'..',
+   'c':'...', 'd':'....', 'e':'.....',
+   'f':'......', 'g':'.......', 'h':'........',
+   '1':'.', '2':'..', '3':'...',
+   '4':'....', '5':'.....', '6':'......',
+   '7':'.......', '8':'........'}
+
+

I began by creating a dict of all of the dot values for each letter and number.

+
def toMorse(move): #convert move to morse code
+    ret = "" #empty morse conversion
+    newConvert = "" #empty mid-conversion string
+    for char in move:
+        #print("converting " + char) #debug print
+        newConvert = morseDict[char] #take key of char index in morse dict
+        ret += newConvert + ' ' #add morse for new char to morse string
+    print("Morse-Converted Move: " + ret)
+    return ret
+
+

The above function takes a move played by Stockfish and converts it to a format that is almost ready to be sent via Bluetooth to the insole.

+

At this point the move “e2e4” would look like ”..... .. ..... ....” meaning it is ready to be pulsed to the wearer of the device once it is sent to and received by the insole.

+

Sending Moves Via Bluetooth

+

The job of the Python program each move ends with the sending of the optimal move to the user of the insole. This is accomplished by connecting the machine that the program is running on to the HC-05 or HC-06 Bluetooth module located in the device and sending the moves via a COM port.

+

Whenever you connect to the Bluetooth module, it will take on a different COM port. This address can be found in Windows settings by searching “Device Manager” or pressing ⊞+x and navigating to Device Manager. Once in this interface, navigate to “Bluetooth” and find the Bluetooth module.

+
port = "COM10" #set bluetooth port
+
+

Once you find the port, change the value of the port string to whatever value your computer reflects. The Bluetooth module will have a couple of values that it takes on on your machine, so you might not have to change it every time you reconnect, but it is certainly best practice to check just to make sure. These modules can be pretty buggy sometimes, but usually just forgetting the device and reconnecting solves the problem!

+
ser = serial.Serial(port, 9600, timeout = 1)
+print("serial opened")
+
+

The above snippet establishes a connection to the Bluetooth module at 9600 baud using the pre-specified port and prints a confirmation. After this runs successfully, a game can be started and the moves will be sent to the shoe insoel automatically.

+
def sendMove(morse):
+    print(morse) #print morse move with spaces replaced with 9 - easier to parse on arduino side as empty bytes hard to work with
+    for char in morse:
+        tempChar = char.encode() #temporary placeholder set to current char in morse move
+        ser.write(tempChar) #send individual character of final morse message encoded in utf-8
+    print("sent move")
+    return
+
+

The above function is responsible for sending the moves. During testing, I found that sending one byte at a time is easier to parse on the insole, so this function simply iterates through the converted string and sends each byte one by one. After all of the bytes are sent, it returns to the parent function and the game continues.

+
morse = re.sub("[ ]", "9", morse)
+
+

I realized that spaces can behave weirdly when sent over Bluetooth using UTF-8, so instead of further researching the problem I simply set all of the spaces to a value that cannot appear in a chess move before sending them to the insole. 9’s are simply treated as a constant delay in the C++ code that runs on the insole.

+

At this point, the work of the Python function is complete. A player move has been input, checked for legality, and an engine move has been generated, converted, and sent to the insole. Once the player wearing the device makes the move on the board and the human player responds, this process will continue until Stockfish triumphs!

+
+ +
Bluetooth Interfacing Chess Engine
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
+154
+155
+156
+157
+158
+159
+160
+161
+162
+163
+164
+165
+166
+167
+168
+169
+170
+171
+172
+173
+174
+175
+176
+177
    #(c) Teddy Warner & Jack Hollingsworth - 2022
+
+    #This work may be reproduced, modified, distributed, performed, and displayed
+    #for any purpose, but must acknowledge Teddy Warner  & Jack Hollingsworth.
+    #Copyright is retained and must be preserved. The work is provided as is;
+    #no warranty is provided, and users accept all liability.
+
+    #download stockfish from https://stockfishchess.org/files/stockfish_15_win_x64_avx2.zip
+
+    from stockfish import Stockfish #pip install stockfish
+    import chess #pip install python-chess
+    import time
+    import serial
+    import re #pip install regex
+
+    global board; global fish
+    global legal; global legalMoves
+    global moveAdjusted; global badChars
+    global isMate; global morseDict
+    global morseMove; global port 
+    global stockfishPath
+
+    port = "COM10" #set bluetooth port
+
+    stockfishPath = "C:/Users/jackh/Downloads/stockfish_15_win_x64_avx2/stockfish_15_win_x64_avx2/stockfish_15_x64_avx2.exe" #replace with the path to your Stockfish exe. Note - the path may only contain forward slashes, no backslashes.
+
+    morseDict = { 'a':'.-', 'b':'-...',
+      'c':'-.-.', 'd':'-..', 'e':'.',
+      'f':'..-.', 'g':'--.', 'h':'....',
+      '1':'.----', '2':'..---', '3':'...--',
+      '4':'....-', '5':'.....', '6':'-....',
+      '7':'--...', '8':'---..'} #morse dict for all chars in chess moves, other characters not needed here
+
+    board = chess.Board(); #create chess board object
+
+    badChars = ["i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
+    "9", "0"] #chars that can't be in a legal chess move
+
+    morseMove = "" #placeholder for morse code move, filled later
+
+    fish = Stockfish(r"{0}".format(stockfishPath), 
+    depth=18, parameters={"Threads": 4, "Hash": 256, "UCI_LimitStrength": "false"}) #stockfish object declaration, can regulate strength
+    print("WDL Accepted " + str(fish.does_current_engine_version_have_wdl_option()))
+    print("Board State " + fish.get_board_visual()) #prints unicode image of current board state
+
+    ser = serial.Serial(port, 9600, timeout = 1) #open com port of hc-06 receiving, set to 9600 baud
+    print("serial opened")
+
+    def playGame(side):
+        global legal; global legalMoves
+        global board; global isMate; global morseMove; 
+        mate = False #checkmate state declaration
+        turns = 0; i = 0 #these could be the same, but easier to keep sep = SPAGHETTIOLI CODE MOMENT
+        board = chess.Board(); #declare chess board object in pychess module, for checking legality of moves
+        print("fish playing as " + side)
+        if side == "white":
+            while mate == False: #if game not over, play continues
+                bestMove = fish.get_best_move(1000) #stockfish get best current move
+                fish.make_moves_from_current_position([bestMove]); 
+                chessMove = chess.Move.from_uci(bestMove) #create chessMove object in pychess, push it to board on next line
+                board.push_san(bestMove) #make move in pychess to keep up with stockfish game
+                print("whitefish plays " + bestMove)
+                morseMove = toMorse(bestMove) #call morse conversion of fish move
+                sendMove(morseMove) #call to move sending function, arg is stockfish best move
+                mate = board.is_checkmate() #returns state of checkmate
+                print("Checkmate: " + str(mate)) #prints state of checkmate after every fish move
+                if mate == True:
+                    print("checkmate, stockfish victory!")
+                    return
+                turns += 1
+                legalMoves = str(board.legal_moves) #convert list to string
+                print("black move:") #request player move
+                move = input()
+                getPlayerMove(move)
+                mate = board.is_checkmate() #check if player has won
+                if mate == True:
+                  print("checkmate, player victory")
+                  return
+                turns += 1
+                print("board after " + str(turns) + " moves:")
+                print(fish.get_board_visual(False)) #shows board from player perspective (white)
+            print("")
+            return #return to infinite loop
+        if side == "black":
+            legalMoves = str(board.legal_moves) #convert list to string
+            while mate == False:
+                print("white move:")
+                move = input()
+                getPlayerMove(move)
+                mate = board.is_checkmate() #check if player has won
+                if mate == True:
+                  print("checkmate, player victory")
+                  return
+                bestMove = fish.get_best_move(1000) #stockfish get best current move
+                fish.make_moves_from_current_position([bestMove]); 
+                chessMove = chess.Move.from_uci(bestMove)
+                board.push_san(bestMove)
+                print("blackfish plays " + bestMove)
+                morseMove = toMorse(bestMove) #get string of morse-converted move, not in use
+                sendMove(morseMove) #send morse move via bluetooth
+                mate = board.is_checkmate() #returns state of checkmate
+                print("Checkmate: " + str(mate)) #prints state of checkmate after every fish move
+                if mate == True:
+                    print("checkmate, stockfish victory!")
+                    return
+                turns += 1
+                print("board after " + str(turns) + " moves:")
+                print(fish.get_board_visual()) #shows board from player perspective (black)
+            print("")
+            return #return to infinite loop
+
+    def getPlayerMove(move):
+        global board; global moveAdjusted; global badChars; global morseMove
+        noBadChars = True #reset no bad chars bool
+        legal = False #reset illegal bool
+        charCount = 0; numCount = 0
+        for char in move: #check if badChars exist in move
+            i = 0 #reset iterating var
+            while i < len(badChars):
+                badChar = badChars[i]
+                if char == badChar:
+                    print(char + ' is ILLEGAL')
+                    noBadChars = False #set bool to reflect bad char
+                if char.isalpha() == True:
+                    charCount += 1 #reflect that at least 1 char in string
+                if char.isdigit() == True:
+                    numCount += 1 #reflect that at least one number in string
+                i += 1
+                #print(char + "is legal") #debug bad chars function
+        if len(move) != 4 and len(move) != 5: #solve edge case where small string doesn't break other rules
+            noBadChars = False;
+        if charCount == 0 or numCount == 0:
+            print("numbers: " + str(numCount))
+            print("chars " + str(charCount))
+            noBadChars = False #change bool to reflect bad formatting
+        if noBadChars == True: #only creative move objects if correct formatting
+            print(move + " is good format")
+            myMove = chess.Move.from_uci(move) #create move object frmo current player move, used to check legality
+            legal = board.is_legal(myMove) #checks legality of desired move
+            print("Legal? " + str(legal))
+        if legal and noBadChars: #move can only happen if legal and doesn't contain illegal chars
+            fish.make_moves_from_current_position([move])#plays legal player move in STOCKFISH
+            chessMove = chess.Move.from_uci(move) #load legal player move
+            board.push_san(move) #send player move to board tracker
+            print("player plays " + move)
+            return #back to game loop
+        else:
+            print("illegal move, input new move:")
+            newMove = input()
+            getPlayerMove(newMove)
+            return #back to game loop
+
+    def toMorse(move): #convert move to morse code
+        ret = "" #empty morse conversion
+        newConvert = "" #empty mid-conversion string
+        for char in move:
+            #print("converting " + char) #debug print
+            newConvert = morseDict[char] #take key of char index in morse dict
+            ret += newConvert + ' ' #add morse for new char to morse string
+        print("Morse-Converted Move: " + ret)
+        return ret
+
+    def sendMove(morse):
+        morse = re.sub("[ ]", "9", morse)
+        print(morse) #print morse move with spaces replaced with 9 - easier to parse on arduino side as empty bytes hard to work with
+        for char in morse:
+            tempChar = char.encode() #temporary placeholder set to current char in morse move
+            ser.write(tempChar) #send individual character of final morse message encoded in utf-8
+        print("sent move")
+        return
+
+    while True: #enables playing of inifinite games, playGame() returns to here after checkmate, resets all local values
+        isMate = False
+        print("good chess speaks for itself, welcome to Von Niemann Probe")
+        print("side of stockfish:") #request side of stockfish player
+        fishSide = input() #which side is hans niemann on?
+        playGame(fishSide) #initiate game with advantage player's side receiving hints based on other player's moves
+
+ +
+ +
+Bluetoothless Implemetation +
+

Chess Engine
  1
+  2
+  3
+  4
+  5
+  6
+  7
+  8
+  9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51
+ 52
+ 53
+ 54
+ 55
+ 56
+ 57
+ 58
+ 59
+ 60
+ 61
+ 62
+ 63
+ 64
+ 65
+ 66
+ 67
+ 68
+ 69
+ 70
+ 71
+ 72
+ 73
+ 74
+ 75
+ 76
+ 77
+ 78
+ 79
+ 80
+ 81
+ 82
+ 83
+ 84
+ 85
+ 86
+ 87
+ 88
+ 89
+ 90
+ 91
+ 92
+ 93
+ 94
+ 95
+ 96
+ 97
+ 98
+ 99
+100
+101
+102
+103
+104
+105
+106
+107
+108
+109
+110
+111
+112
+113
+114
+115
+116
+117
+118
+119
+120
+121
+122
+123
+124
+125
+126
+127
+128
+129
+130
+131
+132
+133
+134
+135
+136
+137
+138
+139
+140
+141
+142
+143
+144
+145
+146
+147
+148
+149
+150
+151
+152
+153
#(c) Teddy Warner & Jack Hollingsworth - 2022
+
+#This work may be reproduced, modified, distributed, performed, and displayed
+#for any purpose, but must acknowledge Teddy Warner  & Jack Hollingsworth.
+#Copyright is retained and must be preserved. The work is provided as is;
+#no warranty is provided, and users accept all liability.
+
+#complete implementation without bluetooth sending, everything else works though so can easily be iterated upon
+
+#download stockfish from https://stockfishchess.org/files/stockfish_15_win_x64_avx2.zip
+
+from stockfish import Stockfish
+import chess
+import time
+
+global board; global fish
+global legal; global legalMoves
+global moveAdjusted; global badChars
+global isMate; global morseDict
+global morseMove; global stockfishPath
+
+stockfishPath = "C:/Users/jackh/Downloads/stockfish_15_win_x64_avx2/stockfish_15_win_x64_avx2/stockfish_15_x64_avx2.exe" #replace URL with the path to your Stockfish exe. Note - the path may only contain forward slashes, no backslashes.
+
+morseDict = { 'a':'.-', 'b':'-...',
+  'c':'-.-.', 'd':'-..', 'e':'.',
+  'f':'..-.', 'g':'--.', 'h':'....',
+  '1':'.----', '2':'..---', '3':'...--',
+  '4':'....-', '5':'.....', '6':'-....',
+  '7':'--...', '8':'---..'} #morse dict for all chars in chess moves
+
+board = chess.Board(); #create chess board object
+
+badChars = ["i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
+"9", "0"] #chars that can't be in a legal chess move
+
+morseMove = "" #placeholder for morse code move, filled later
+
+fish = Stockfish(r"{0}".format(stockfishPath), 
+depth=18, parameters={"Threads": 4, "Hash": 256, "UCI_LimitStrength": "false"}) #stockfish object declaration, can regulate strength
+print("WDL Accepted " + str(fish.does_current_engine_version_have_wdl_option()))
+print("Board State " + fish.get_board_visual())
+
+def playGame(side):
+    global legal; global legalMoves
+    global board; global isMate; global morseMove; 
+    mate = False #checkmate state declaration
+    turns = 0; i = 0 #these could be the same, but easier to keep sep = SPAGHETTIOLI CODE MOMENT
+    board = chess.Board(); #declare chess board object in chess module
+    print("fish playing as " + side)
+    if side == "white":
+        while mate == False: #if game not over, play continues
+            bestMove = fish.get_best_move(1000) #stockfish get best current move
+            fish.make_moves_from_current_position([bestMove]); 
+            chessMove = chess.Move.from_uci(bestMove)
+            board.push_san(bestMove)
+            print("whitefish plays " + bestMove)
+            morseMove = toMorse(bestMove) #call morse conversion of fish move
+            mate = board.is_checkmate() #returns state of checkmate
+            print("Checkmate: " + str(mate)) #prints state of checkmate after every fish move
+            if mate == True:
+                print("checkmate, stockfish victory!")
+                return
+            turns += 1
+            legalMoves = str(board.legal_moves) #convert list to string
+            print("black move:") #request player move
+            move = input()
+            getPlayerMove(move)
+            turns += 1
+            print("board after " + str(turns) + " moves:")
+            print(fish.get_board_visual(False)) #shows board from player perspective (white)
+        print("checkmate, stockfish victory")
+        return #return to infinite loop
+    if side == "black":
+        legalMoves = str(board.legal_moves) #convert list to string
+        while mate == False:
+            print("white move:")
+            move = input()
+            getPlayerMove(move)
+            bestMove = fish.get_best_move(1000) #stockfish get best current move
+            fish.make_moves_from_current_position([bestMove]); 
+            chessMove = chess.Move.from_uci(bestMove)
+            board.push_san(bestMove)
+            print("blackfish plays " + bestMove)
+            morseMove = toMorse(bestMove) #get string of morse-converted move, not in use
+            mate = board.is_checkmate() #returns state of checkmate
+            print("Checkmate: " + str(mate)) #prints state of checkmate after every fish move
+            if mate == True:
+                print("checkmate, stockfish victory!")
+                return
+            turns += 1
+            print("board after " + str(turns) + " moves:")
+            print(fish.get_board_visual()) #shows board from player perspective (black)
+        print("checkmate, stockfish victory!")
+        print("")
+        return #return to infinite loop
+
+def getPlayerMove(move):
+    global board; global moveAdjusted; global badChars; global morseMove
+    noBadChars = True #reset no bad chars bool
+    legal = False #reset illegal bool
+    charCount = 0; numCount = 0
+    for char in move: #check if badChars exist in move
+        i = 0 #reset iterating var
+        while i < len(badChars):
+            badChar = badChars[i]
+            if char == badChar:
+                print(char + ' is ILLEGAL')
+                noBadChars = False #set bool to reflect bad char
+            if char.isalpha() == True:
+                charCount += 1 #reflect that at least 1 char in string
+            if char.isdigit() == True:
+                numCount += 1 #reflect that at least one number in string
+            i += 1
+            #print(char + "is legal") #debug bad chars function
+    if len(move) != 4 and len(move) != 5: #solve edge case where small string doesn't break other rules
+        noBadChars = False;
+    if charCount == 0 or numCount == 0:
+        print("numbers: " + str(numCount))
+        print("chars " + str(charCount))
+        noBadChars = False #change bool to reflect bad formatting
+    if noBadChars == True: #only creative move objects if correct formatting
+        print(move + " is good format")
+        myMove = chess.Move.from_uci(move) #create move object frmo current player move, used to check legality
+        legal = board.is_legal(myMove) #checks legality of desired move
+        print("Legal? " + str(legal))
+    if legal and noBadChars: #move can only happen if legal and doesn't contain illegal chars
+        fish.make_moves_from_current_position([move])#plays legal player move in STOCKFISH
+        chessMove = chess.Move.from_uci(move) #load legal player move
+        board.push_san(move) #send player move to board tracker
+        print("player plays " + move)
+        return #back to game loop
+    else:
+        print("illegal move, input new move:")
+        newMove = input()
+        getPlayerMove(newMove)
+        return #back to game loop
+
+def toMorse(move): #convert move to morse code
+    ret = "" #empty morse conversion
+    newConvert = "" #empty mid-conversion string
+    for char in move:
+        #print("converting " + char) #debug print
+        newConvert = morseDict[char] #take key of char index in morse dict
+        ret += newConvert + ' ' #add morse for new char to morse string
+    print("Morse-Converted Move: " + ret)
+    return ret
+
+while True: #enables playing of inifinite games, playGame() returns to here after checkmate
+    isMate = False
+    print("good chess speaks for itself, welcome to Von Niemann Probe")
+    print("side of stockfish:") #request side of stockfish player
+    fishSide = input() #laptop user input
+    playGame(fishSide)
+
+

+
+

Telegraph Code

+

Once a formatted move is sent to the shoe insole, it needs to use its vibrating motor to discretely tell its wearer what the best move is. As all of the major processing and translation is done in Python, all the ATTiny412 microcontroller in the insole does is receive a move via Bluetooth, parse it, and buzz it to the user, making its job considerably simpler.

+

C++ Setup & Initialization

+
#include <SoftwareSerial.h> //software serial library, native in base installation of ide
+SoftwareSerial HC06(0, 1); //HC06-TX Pin 10, HC06-RX to Arduino Pin 11
+
+

The HC-06 cannot use the normal serial pins, so the SoftwareSerial library is required. The above snippet imports the library and establishes a transmitting line on pin 0 and a receiving line on pin 1.

+
int buzzerPin = 2; //pin of buzzer/vibrating motor
+int dotLength = 200; //establish length of 1 dot
+
+

These lines establish the pin of the buzzer and specify the length of one dot. Because of the encoding method used here, it is not necessary to specify the length of a dash. The length of a dot is also used for pauses here, but another time could be added to further optimize the buzzing of moves.

+
void setup() {
+  HC06.begin(9600); //Baudrate 9600 , Choose your own baudrate
+  pinMode(buzzerPin, OUTPUT);
+}
+
+

These lines establish a connection between the Bluetooth module and the machine that is running the Python program. It also initializes the buzzer as an output.

+

Receiving & Transmitting Moves

+

Once the Bluetooth module is connected to the Python program and the buzzer is setup, the insole is ready to receive moves and buzz them on the internal vibrating motor.

+
void loop(){
+  if(HC06.available() > 0)
+  {
+    char receive = HC06.read();
+    if(receive =='.'){
+      digitalWrite(buzzerPin, HIGH);
+      delay(dotLength); 
+      digitalWrite(buzzerPin, LOW);
+      delay(dotLength);
+    }
+    if(receive =='9'){
+      digitalWrite(buzzerPin, LOW);
+      delay(dotLength * 2);
+    }
+    else {
+      digitalWrite(buzzerPin, LOW);
+      delay(5);
+    }
+  }
+}
+
+

The Python program sends bytes one at a time and they simply pile up in the stack on the ATTiny412. This allows us to interact with each byte one at a time and not touch the other ones until they are ready to be buzzed. This prevents the need for any local strings or storage for the moves, as each byte in a move is no longer necessary after it is buzzed once, which means that accessing these bytes just once is sufficient.

+

The local char, receive, is set to each byte contained in the move, in the order that they are received. With our encoding method, each byte is either a “.” or a “9” or empty, so these are the only conditions necessary that will result in a buzz. Sometimes junk data is sent, so it is better to leave this final condition open-ended as an else statement instead of including another conditional checking for null bytes, as junk data would result in a crash in this case.

+
+ +
Telegraph Reciving
 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
+21
+22
+23
+24
+25
+26
+27
+28
+29
+30
+31
+32
+33
+34
+35
+36
+37
+38
+39
+40
+41
+42
+43
+44
+45
+46
+47
+48
+49
+50
+51
    //(c) Teddy Warner & Jack Hollingsworth - 2022
+
+    //This work may be reproduced, modified, distributed, performed, and displayed
+    //for any purpose, but must acknowledge Teddy Warner  & Jack Hollingsworth.
+    //Copyright is retained and must be preserved. The work is provided as is;
+    //no warranty is provided, and users accept all liability.
+
+    #include <Arduino.h> //Arduino Parent Lib
+    #include <SoftwareSerial.h> //software serial library, native in base installation of ide
+
+    SoftwareSerial HC06(0, 1); //HC06-TX Pin 10, HC06-RX to Arduino Pin 11
+
+    int buzzerPin = 2; //pin of buzzer/vibrator
+    String fullString = ""; // 
+    int dotLength = 1000; //establish length of 1 dot
+    int dashLength = 3 * dotLength; //establish proportion of dots to dashes
+
+    void setup() {
+      HC06.begin(9600); //Baudrate 9600 , Choose your own baudrate 
+      pinMode(buzzerPin, OUTPUT);
+    }
+
+    void loop(){
+
+      if(HC06.available() > 0) //When HC06 receive something
+      {
+        char receive = HC06.read(); //Read from Serial Communication
+        if(receive =='.'){
+          digitalWrite(buzzerPin, HIGH);
+          delay(dotLength); //delay for dotlength
+          digitalWrite(buzzerPin, LOW);
+          delay(dotLength); //dotlength delay between next character
+        }
+        if(receive == '-')
+        {
+        digitalWrite(buzzerPin, HIGH);
+        delay(dashLength);
+        digitalWrite(buzzerPin, LOW);
+        delay(dotLength); //dot length delay between next character
+        }
+        if(receive =='9'){
+          digitalWrite(buzzerPin, LOW);
+          delay(dotLength * 2); //delay for another 2 seconds if space, gives 3 seconds total division between letters - make proportional to actual thing
+        }
+        else {
+          digitalWrite(buzzerPin, LOW);
+          delay(5);
+        }
+      }
+
+    }
+
+ +
+ +

Fabrication & Testing

+

I began the fabrication of the Von Niemann Probe through some prototype PCB milling on the Bantam Tools Desktop CNC Milling Machine

+

+
+

+

+

Leveraging these machines would allow me to test early designs before ordering final boards from PCBWay, allowing me to ensure their functionality early on in the design process. This process also makes it considerably easier to make changes, as I can test multiple iterations within several hours of each other, including the time to solder each board!

+

+

+
+

Von Niemann Probe Mainboard
+

+

+

Next up, 3D Printing … and a lot of it. All prototype prints for the Von Niemann Probe were done on a Prusa I3 MK3S+ equipped with an 0.4mm E3D Nozzle X from White PETG.

+ + + + + +

Similar to the PCB prototyping, I fabricated several prints while iterating on the insole’s CAD, before ordering the final iteration from PCBWay. Each print required over 14 hours of print time and numerous hours to complete the necessary changes to the design.

+

+

+
+

All Iterations of the Von Niemann Probe Insole
+

+

+

After finally arriving at a version of the insole that I was satisfied with, I was ready to assemble the device …

+

+
+

+

+

Leaving me with the final product below! 😄

+

+

+ +
+

Von Niemann Probe Electronics Enclosure & Cover
+

+

+
+
+
    +
  1. +

    https://www.npr.org/2022/10/21/1130442319/hans-niemann-sues-magnus-carlsen-for-100-million-accusing-him-of-defamation 

    +
  2. +
  3. +

    https://youtu.be/eg5G1A_mTFg 

    +
  4. +
  5. +

    https://twitter.com/hansmokeniemann 

    +
  6. +
  7. +

    https://futurism.com/von-neumann-probe 

    +
  8. +
  9. +

    https://ww1.microchip.com/downloads/en/DeviceDoc/ATtiny212-412-DataSheet-DS40001911B.pdf 

    +
  10. +
  11. +

    https://components101.com/sites/default/files/component_datasheet/HC06%20Bluetooth%20Module%20Datasheet.pdf 

    +
  12. +
  13. +

    https://components101.com/wireless/hc-06-bluetooth-module-pinout-datasheet 

    +
  14. +
  15. +

    http://www.sizecharter.com/clothing-fit-and-measurement/understanding-shoe-sizing 

    +
  16. +
  17. +

    https://www.researchgate.net/figure/The-Mean-of-Absolute-in-mm-and-Relative-in-FL-Foot-Dimensions-for-Both-Genders-a_tbl2_327021689 

    +
  18. +
  19. +

    https://www.researchgate.net/figure/Measurements-of-the-foot-and-the-inner-shoe-dimensions-a-Foot-Length-FL-and-Foot_fig5_280976288 

    +
  20. +
  21. +

    https://www.protalus.com/blogs/news/how-to-measure-feet 

    +
  22. +
  23. +

    https://www.magnuscarlsen.com/en 

    +
  24. +
  25. +

    https://www.chess.com/blog/CHESScom/hans-niemann-report 

    +
  26. +
  27. +

    https://www.bbc.com/news/world-63010107 

    +
  28. +
  29. +

    https://www.bbc.com/news/world-63043023 

    +
  30. +
  31. +

    https://www.youtube.com/watch?v=CJZuT-_kij0&t=593s 

    +
  32. +
  33. +

    https://cse.buffalo.edu/~regan/chess/fidelity/data/Niemann/ 

    +
  34. +
  35. +

    https://twitter.com/MagnusCarlsen/status/1574482694406565888?s=20 

    +
  36. +
  37. +

    https://www.wsj.com/articles/chess-cheating-hans-niemann-report-magnus-carlsen-11664911524 

    +
  38. +
  39. +

    https://storage.courtlistener.com/recap/gov.uscourts.moed.198608/gov.uscourts.moed.198608.1.0.pdf 

    +
  40. +
  41. +

    https://frontofficesports.com/top-chess-player-platform-join-forces-in-82-9m-deal/ 

    +
  42. +
  43. +

    https://www.washingtonpost.com/sports/2022/10/05/chess-com-investigation-cheating-hans-niemann-magnus-carlsen/ 

    +
  44. +
  45. +

    https://www.youtube.com/watch?v=5uDM3fPeNFM&t=224s 

    +
  46. +
  47. +

    https://www.youtube.com/watch?v=QNuu8KTUEwU&t=390s 

    +
  48. +
  49. +

    https://www.reddit.com/r/chess/comments/x6ixud/during_magnus_carlsens_current_unbroken_streak_as/ 

    +
  50. +
  51. +

    https://pawnalyze.com/chess-drama/2022/09/05/Analyzing-Allegations-Niemann-Cheating-Scandal.html 

    +
  52. +
  53. +

    https://www.chess.com/news/view/hans-niemann-us-junior-championship-annie-wang 

    +
  54. +
  55. +

    https://www.mid-day.com/sports/other-sports/article/world-champion-magnus-carlsen-quits-game-amid-cheating-allegations-23246759 

    +
  56. +
  57. +

    https://www.fide.com/news/1968 

    +
  58. +
  59. +

    https://www.youtube.com/watch?v=TkUkvLqHfZM 

    +
  60. +
  61. +

    https://www.youtube.com/watch?v=9wtvXoXh0VU 

    +
  62. +
  63. +

    https://www.chess.com/events/2022-sinquefield-cup/03/Carlsen_Magnus-Niemann_Hans_Moke 

    +
  64. +
  65. +

    https://www.youtube.com/watch?v=CJZuT-_kij0 

    +
  66. +
  67. +

    https://www.youtube.com/watch?v=VptbNKbHQiM&t=5s 

    +
  68. +
  69. +

    https://www.youtube.com/watch?v=O6ML2b7IdD4&t=2s 

    +
  70. +
  71. +

    https://www.youtube.com/watch?v=qjtbXxA8Fcc 

    +
  72. +
  73. +

    https://www.youtube.com/watch?v=uCzwLk6fXXs 

    +
  74. +
  75. +

    https://www.youtube.com/watch?v=LkXDhw-TuGw 

    +
  76. +
  77. +

    https://www.nationalworld.com/news/offbeat/how-do-you-cheat-at-chess-cheating-player-hans-niemann-scandal-play-online-board-3870066 

    +
  78. +
  79. +

    https://www.nytimes.com/2022/12/04/business/chess-cheating-scandal-magnus-carlsen-hans-niemann.html 

    +
  80. +
  81. +

    https://www.wsj.com/articles/chess-cheating-magnus-carlsen-hans-niemann-11663751262?mod=article_inline 

    +
  82. +
  83. +

    https://www.buzzfeednews.com/article/kelseyweekman/chess-cheating-anal-beads-conspiracy-hans-niemann 

    +
  84. +
  85. +

    https://www.youtube.com/watch?v=lpfWGyvO80o 

    +
  86. +
  87. +

    https://www.youtube.com/watch?v=GLwyhmmEoAQ 

    +
  88. +
  89. +

    https://www.youtube.com/watch?v=umaHIQJY6Tw 

    +
  90. +
  91. +

    https://www.youtube.com/watch?v=aDUmS_MJceU 

    +
  92. +
  93. +

    https://www.republicworld.com/world-news/rest-of-the-world-news/elon-musk-claims-niemann-may-have-used-anal-beads-to-beat-carlsen-in-chess-championship-articleshow.html 

    +
  94. +
  95. +

    https://www.newsweek.com/hans-niemann-magnus-carlsen-chess-sex-toy-scandal-1749903 

    +
  96. +
  97. +

    https://www.youtube.com/watch?v=HdHWAuQRG7E 

    +
  98. +
  99. +

    https://www.youtube.com/watch?v=J4_e12Md4Fg 

    +
  100. +
  101. +

    https://www.youtube.com/watch?v=XjupJslRj5E 

    +
  102. +
  103. +

    https://twitter.com/nigelshortchess/status/1573434084239593481 

    +
  104. +
  105. +

    https://twitter.com/hansmokeniemann/status/1583164606029365248 

    +
  106. +
+
+ + + + + + + + + + + + + + + + + + +

Comments

+ + + + + +
+
+ + + + + +
+ + + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/css/404.css b/assets/css/404.css new file mode 100644 index 00000000..a41deff7 --- /dev/null +++ b/assets/css/404.css @@ -0,0 +1,111 @@ +:root { + --duration: 1s; + --nav-duration: calc(var(--duration) / 4); + --ease: cubic-bezier(0.215, 0.61, 0.355, 1); +} + +.md-header { + background: none; + box-shadow: none; +} + +.error-container { + height: 100vh; + width: 100%; + display: flex; + align-items: center; + justify-content: center; + text-align: center; + padding: 2rem; + position: fixed; + top: 0; + left: 0; + overflow: hidden; + z-index: 2; +} + +body { + overflow: hidden; + position: fixed; + width: 100%; + height: 100%; + margin: 0; +} + +html { + overflow: hidden; +} + +.error-content { + max-width: 600px; + animation: fadeIn 0.8s ease-out; + margin: 0 auto; + padding: 0 1rem; +} + +.error-content h1 { + font-family: "Crimson Pro"; + font-size: 12vw; + margin: 0; + color: var(--md-default-fg-color); + line-height: 1; +} + +.error-content h2 { + font-family: "Crimson Pro"; + font-size: 3vw; + margin: 0.5em 0; + color: var(--md-default-fg-color); + font-weight: 400; +} + +.error-content p { + font-family: "JetBrains Mono"; + font-size: 1em; + margin: 1.5em 0; + color: var(--md-default-fg-color--light); +} + +.home-button { + display: inline-flex; + align-items: center; + gap: 0.5em; + padding: 0.8em 1.6em; + font-family: "JetBrains Mono"; + font-size: 0.9em; + color: var(--md-default-bg-color) !important; + background-color: var(--md-default-fg-color); + border-radius: 50px; + text-decoration: none; + transition: transform 0.2s ease; +} + +.home-button:hover { + transform: scale(0.97); + color: var(--md-default-bg-color) !important; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@media (max-width: 768px) { + .error-content h1 { + font-size: 20vw; + } + + .error-content h2 { + font-size: 6vw; + } + + .error-content p { + font-size: 0.9em; + } +} diff --git a/assets/css/extra.css b/assets/css/extra.css new file mode 100644 index 00000000..00c66f1b --- /dev/null +++ b/assets/css/extra.css @@ -0,0 +1,6 @@ +[data-md-color-scheme="slate"] { + --md-default-bg-color: hsla(228, 7%, 14%, 1); + --md-footer-bg-color--dark: hsla(228, 8%, 7%, 0.58); + --md-footer-bg-color: hsla(220, 7%, 7%, 1); + --md-code-bg-color: hsla(228, 7%, 12%, 1); + } \ No newline at end of file diff --git a/assets/css/header.css b/assets/css/header.css new file mode 100644 index 00000000..95a21a5f --- /dev/null +++ b/assets/css/header.css @@ -0,0 +1,75 @@ +:root { + --scroll-position: 0px; +} + +header { + height: 192px; + padding-top: 10em; + padding-left: 4em; + padding-right: 4em; + transform: translateY(0); + transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.scrolled header { + transform: translateY(-5.5em); +} + +@media screen and (min-width: 2000px) { + .scrolled header { + transform: translateY(-6em); + } +} + +@media (max-width: 1200px) { + .scrolled header { + transform: translateY(-3em); + } + header { + padding-top: 5em; + padding-left: 1em; + padding-right: 1em; + } +} + +@media (max-width: 860px) { + .scrolled header { + transform: translateY(-2.5em); + } +} + +@media (max-width: 695px) { + header { + position: fixed; + top: 0; + width: 100%; + transition: transform 0.3s ease; + } + + .scrollDown header { + transform: translateY(-100%); + } + + .scrollUp header { + transform: translateY(0); + } +} + +.logo { + content: var(--avatar); + opacity: 1; + transition: opacity 0.15s ease, transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); + transition-delay: 0.15s, 0s; + transform: scale(1); + margin-top: -1em; +} + +.toggle .logo { + opacity: 0; + transition: opacity 0.05s ease; + transition-delay: 0s; +} + +.scrolled .logo { + transform: scale(0.8); +} \ No newline at end of file diff --git a/assets/css/index.css b/assets/css/index.css new file mode 100644 index 00000000..cfc9fc08 --- /dev/null +++ b/assets/css/index.css @@ -0,0 +1,1350 @@ +:root { + --duration: 1s; + --nav-duration: calc(var(--duration) / 4); + --ease: cubic-bezier(0.215, 0.61, 0.355, 1); + --space: 1rem; + --font-size: 1em; + --line-height: 1.5; + --inputicon: hsla(0, 0%, 0%, 0.87); + --toggledinputicon: white; + --formbg: hsla(0, 0%, 0%, 0.25); + --formhover: hsla(0, 0%, 0%, 0.32); + --toggledform: hsla(0, 0%, 100%, 0.12); + --toggledformhover: hsla(0, 0%, 100%, 0.3); + --avatar: url("../../assets/images/index/avatardark.svg"); + --articlebg: hsla(0, 0%, 0%, 0.04); + --content1-height: 190px; + --content2-height: 760px; + --content3-height: 356px; + --content4-height: 192px; + --content5-height: 284px; + --content6-height: 383px; + --content7-height: 93px; + --tone-url-light: "assets/images/index/tonelight.png"; + --tone-url-slate: "assets/images/index/tonedark.png"; + --scroll-position: 0px; +} + +[data-md-color-scheme="slate"] { + --inputicon: white; + --toggledinputicon: hsla(0, 0%, 0%, 0.87); + --formhover: hsla(0, 0%, 100%, 0.3); + --toggledform: hsla(0, 0%, 0%, 0.2); + --toggledformhover: hsla(0, 0%, 0%, 0.32); + --avatar: url("../../assets/images/index/avatarlight.svg"); + --articlebg: hsla(234, 76%, 95%, 0.04); +} + +.logo { + content: var(--avatar); +} + +@media screen and (min-width: 76.25em) { + .md-sidebar--primary { + display: none; + } +} + +.md-sidebar { + width: 0px; +} + +.md-main__inner { + margin: 0; + height: calc(var(--content1-height) + 24em + var(--content2-height) + var(--content3-height) + var(--content4-height) + var(--content5-height) + var(--content6-height) + var(--content7-height)); +} + +.md-source { + color: var(--md-default-fg-color); +} + +.toggle .socialsparent { + display: none; +} + +.close { + display: none; +} + +.toggle .close { + display: flex; +} + +.toggle .menu { + display: none; +} + +.md-header__button.md-icon[for="__search"] { + color: var(--md-default-fg-color); +} + +.toggle .md-header__button.md-icon[for="__search"] { + color: var(--md-default-bg-color); +} + +.md-search__input + .md-search__icon { + color: var(--inputicon); +} + +.toggle .md-search__input + .md-search__icon { + color: var(--toggledinputicon); +} + +.md-search__form { + background-color: var(--formbg); +} + +.md-search__form:hover { + background-color: var(--formhover); +} + +.toggle .md-search__form { + background-color: var(--toggledform); +} + +.toggle .md-search__form:hover { + background-color: var(--toggledformhover); +} + +header { + height: 192px; + padding-top: 10em; + padding-left: 4em; + padding-right: 4em; + transform: translateY(0); + transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.toggle header { + padding-top: 12.174em; +} + +.menu { + color: var(--md-default-fg-color); +} + +.close { + color: var(--md-default-bg-color); +} + +.md-header__option { + color: var(--md-default-fg-color); +} + +.toggle .md-header__option { + color: var(--md-default-bg-color); +} + +.md-header[data-md-state=shadow] { + box-shadow: 0 0 0 rgba(0, 0, 0, 0), 0 0 0 rgba(0, 0, 0, 0); +} + +.md-header { + background: none; + box-shadow: 0 0 0 transparent, 0 0 0 transparent; +} + +.md-header p { + display: none; +} + +.md-tabs { + display: none; +} + +.logo { + padding-left: 0; +} + +.abtbtn { + width: 92px; + height: 92px; +} + +.abtbtn:hover { + cursor: pointer; +} + +@keyframes rotate { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +@keyframes reverse { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(-360deg); + } +} + +.circleGroup { + --rotation-direction: normal; + transform-origin: 1137px 797px; + animation: rotate 7s linear infinite var(--rotation-direction); +} + +.circleLink { + transition: all 0.3s ease; + transform-origin: 1137px 797px; +} + +.circleLink:hover { + transform: scale(0.97); +} + +.main-navigation { + position: fixed; + top: 0; + left: 0; + display: flex; + align-items: center; + width: 100%; + height: 100%; + transform: translateX(-100%); + transition: transform var(--nav-duration); + background-color: var(--md-default-fg-color); + z-index: 3; +} + +.main-navigation:after { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: inherit; + transform-origin: 0 50%; + z-index: 3; +} + +.main-navigation ul { + font-size: 9vmin; + width: 100%; + height: auto; + z-index: 4; +} + +.main-navigation li { + display: flex; + align-items: center; + position: relative; + overflow: hidden; + z-index: 4; + margin-top: -0.3em; + margin-bottom: -0.3em; +} + +.main-navigation li:after { + content: ""; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: auto; + background-color: inherit; + transform-origin: 0 50%; + transform: translateX(-100%) skew(15deg); + z-index: 4; +} + +.main-navigation a { + display: inline-block; + width: 100%; + max-width: 1200px; + margin: 0 auto; + color: var(--md-default-bg-color); + font-family: "Crimson Pro"; + font-size: 1.2em; + line-height: 1.2; + text-decoration: none; + user-select: none; + padding: auto; + transform: translateY(100%); + z-index: 4; +} + +.main-navigation a:hover { + cursor: pointer; + color: var(--md-default-bg-color); +} + +.main-content { + position: relative; + will-change: filter; +} + +.home, .proj, .writ { + transition: all 0.3s ease; +} + +.home:hover, .proj:hover, .writ:hover { + font-style: italic; +} + +.navnum { + font-size: 0.35em; + font-weight: lighter; + font-style: italic; +} + +.blur-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + height: 100px; + pointer-events: none; + z-index: 2; + backdrop-filter: blur(48px); + -webkit-backdrop-filter: blur(48px); + mask-image: linear-gradient( + to bottom, + black 0%, + rgba(0, 0, 0, 0.9) 20%, + rgba(0, 0, 0, 0.6) 45%, + transparent 100% + ); + -webkit-mask-image: linear-gradient( + to bottom, + black 0%, + rgba(0, 0, 0, 0.9) 20%, + rgba(0, 0, 0, 0.6) 45%, + transparent 100% + ); +} + +.content1 { + position: absolute; + width: 100%; + max-width: 1350px; + height: 100px; + top: 24em; + left: 50%; + transform: translateX(-50%); + margin-top: -100px; + margin-bottom: 0; + padding-left: 7em; + padding-right: 7em; +} + +.introabt { + margin-top: -5em; +} + +.introabt h2 { + color: var(--md-default-fg-color); + font-family: "Crimson Pro"; + font-size: 2.8125em; + font-style: normal; + font-weight: 400; + line-height: normal; + margin-bottom: 0em; +} + +.introabt h2 a{ + color: var(--md-default-fg-color); +} + +.introabt h2 a:hover { + color: var(--md-default-fg-color); +} + +.introabt h2 span { + font-style: italic; +} + +.introabt h3 { + color: var(--md-default-fg-color); + font-family: "JetBrains Mono"; + font-size: 1.1875em; + font-style: normal; + font-weight: 400; + line-height: 109.588%; +} + +.intro-section { + margin-top: 14em; +} + +.featured-projects { + margin-top: 14em; +} + +.experience { + margin-top: -14em; +} + +.content2 { + position: absolute; + width: 100%; + height: auto; + left: 50%; + max-width: 1350px; + transform: translateX(-50%); + margin-top: -100px; + margin-bottom: 0; + padding-left: 7em; + padding-right: 7em; + top: calc(var(--content1-height) + 24em + 20px); +} + +.project { + transition: all 0.3s ease; + transform-origin: center; +} + +.project.shadowbox { + box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1); +} + +.project:hover { + transform: scale(0.98); + cursor: pointer; +} + +.project:hover .shadowbox { + background: rgba(255, 255, 255, 0.2); + backdrop-filter: blur(9px); +} + +.projparent { + position: relative; + width: 100%; + max-width: 1600px; + height: auto; + margin: 0 auto; + overflow: hidden; +} + +.projscale svg { + width: 100%; + scale: 1.24; + height: auto; + display: block; + margin: 0; + padding: 0; +} + +#reading-card image { + transition: transform 0.3s ease; +} + +#reading-card:hover image { + transform: rotate(0deg) translate(-5.9%, -6.4%); + filter: none; +} + +#reading-card:hover .reading-content { + filter: brightness(94%); +} + +.reading-content { + transition: filter 0.3s ease; +} + +.content3 { + position: absolute; + width: 100%; + height: auto; + left: 50%; + max-width: 1350px; + transform: translateX(-50%); + padding-left: 7em; + padding-right: 7em; + top: calc(var(--content1-height) + 24em + 10px + var(--content2-height)); +} + +.content3 h2 { + margin-top: 0em; + color: var(--md-default-fg-color); + opacity: 0.90; + text-align: right; + font-family: "Crimson Pro"; + font-size: 2em; + font-style: normal; + font-weight: 300; + line-height: 109.588%; +} + +.pfp { + display: inline-block; + width: 38px; + height: 38px; + border-radius: 5px; + margin-right: 10px; + margin-left: 1px; + margin-bottom: -0.1em; + flex-shrink: 0; +} + +.inlineImg { + display: inline-block; + width: 20px; + height: 20px; + border-radius: 4px; + margin-right: 2px; + margin-left: -2px; + margin-bottom: -0.3em; + flex-shrink: 0; +} + +.companylogo { + display: inline-block; + width: 28px; + height: 28px; + margin-right: 10px; + margin-bottom: -0.5em; + flex-shrink: 0; +} + +.company { + display: inline-block; + margin-right: 28px; + flex-direction: column; + justify-content: center; + flex-shrink: 0; + color: var(--md-default-fg-color); + opacity: 0.75; + font-family: "JetBrains Mono"; + font-size: 1.125em; + font-style: normal; + font-weight: 300; +} + +.role{ + display: inline-block; + flex-direction: column; + justify-content: center; + flex-shrink: 0; + color: var(--md-default-fg-color); + opacity: 0.55; + font-family: "JetBrains Mono"; + font-size: 1.125em; + font-style: normal; + font-weight: 300; +} + +.year { + position: absolute; + display: inline-block; + right: 6.2em; + flex-direction: column; + justify-content: center; + flex-shrink: 0; + color: var(--md-default-fg-color); + opacity: 0.75; + text-align: right; + font-family: "JetBrains Mono"; + font-size: 1.125em; + font-style: normal; + font-weight: 300; +} + +.mobileyear { + display: inline; +} + +.content3 hr { + margin-top: -0.5em; + margin-bottom: -0.5em; +} + +.content4 { + position: absolute; + width: 100%; + height: auto; + left: 50%; + max-width: 1350px; + transform: translateX(-50%); + padding-left: 7em; + padding-right: 7em; + top: calc(var(--content1-height) + 24em + 20px + var(--content2-height) + 10px + var(--content3-height) + 40px); +} + +.content4 h2, .content5 h2, .content6 h2, .content7 h2 { + color: var(--md-default-fg-color); + opacity: 0.90; + font-family: "Crimson Pro"; + font-size: 32px; + font-style: normal; + font-weight: 400; + line-height: normal; +} + +.abt1, .abt2, .abt3 { + width: calc(100% / 3 - 20px); + height: 130px; + display: inline-block; +} + +.abt1{ + margin-right: 10px; +} + +.abt2{ + margin: 10px; +} + +.abt3{ + margin-left: 10px; +} + +.content4 p, .content7 p { + color: var(--md-default-fg-color); + opacity: 0.90; + text-align: justify; + font-family: "JetBrains Mono"; + font-size: 1em; + font-style: normal; + font-weight: 400; + line-height: normal; +} + +.content4 a, .content7 a { + color: var(--md-default-fg-color); + opacity: 0.90; + font-family: "JetBrains Mono"; + font-size: 1em; + font-style: normal; + font-weight: 400; + line-height: normal; + text-decoration-line: underline; +} + +.content4 a:hover, .content7 a:hover { + color: var(--md-default-fg-color); + opacity: 0.90; +} + +.content5 { + position: absolute; + width: 100%; + height: auto; + left: 50%; + max-width: 1350px; + transform: translateX(-50%); + padding-left: 7em; + padding-right: 7em; + top: calc(var(--content1-height) + 24em + 20px + var(--content2-height) + 20px + var(--content3-height) + 40px + var(--content4-height) + 80px); +} + +.content5 .grid-container { + margin-top: 2.75em; + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 10px; + width: 100%; +} + +.content5 .grid-container a { + height: 101px; +} + +.content5 .grid-item { + height: 101px; + border-radius: 28px; + border: 1.2px solid var(--md-default-fg-color--lightest); + background: var(--md-default-bg-color); +} + +.content5 .grid-item { + transition: all 0.3s ease; + transform-origin: center; +} + +.content5 .grid-item:hover { + transform: scale(0.98); + cursor: pointer; +} + +.projtitle { + position: relative; + color: var(--md-default-fg-color); + opacity: 0.75; + font-family: "JetBrains Mono"; + font-size: 1.125em; + font-style: normal; + font-weight: 300; + top: -0.3em; + left: 1em; +} + +.projdescription { + position: relative; + color: var(--md-default-fg-color); + opacity: 0.55; + font-family: "JetBrains Mono"; + font-size: 1em; + font-style: normal; + font-weight: 300; + top: -1.7em; + left: 1.3em; +} + +.content5 .projtitle { + top: 0.3em; + left: 1em; +} + +.content5 .projdescription { + top: -1.1em; + left: 1.3em; +} + +.fa-arrow-up-right-from-square { + position: relative; + width: 15px; + height: 15px; + flex-shrink: 0; + color: var(--md-default-fg-color); + opacity: 0.12; + float: right; + margin-right: 1.2em; + top: -5.4em; +} + +.content6 { + position: absolute; + width: 100%; + height: auto; + left: 50%; + max-width: 1350px; + transform: translateX(-50%); + padding-left: 7em; + padding-right: 7em; + top: calc(var(--content1-height) + 24em + 20px + var(--content2-height) + 20px + var(--content3-height) + 40px + var(--content4-height) + 80px + var(--content5-height) + 130px); +} + +.content6 h2 { + margin-bottom: 1.5em; +} + +.imgparent { + display: flex; + width: 151px; + height: 87px; + padding: 5px; + justify-content: center; + align-items: center; + flex-shrink: 0; + border-radius: 28px; + border: 1px solid var(--md-default-fg-color--lightest); +} + +.imgparent img { + object-fit: cover; + display: flex; + width: 141px; + height: 77px; + justify-content: center; + align-items: center; + flex-shrink: 0; + border-radius: 25px; + border: 1px solid var(--md-default-fg-color--lightest); +} + +.content6 .projtitle { + position: relative; + top: -4.9em; + left: 9.5em; +} + +.content6 .projdescription { + position: relative; + top: -6.9em; + left: 10.7em; +} + +.writeyear { + position: relative; + color: var(--md-default-fg-color); + opacity: 0.38; + text-align: right; + font-family: "JetBrains Mono"; + font-size: 1em; + font-style: normal; + font-weight: 300; + top: -10.9em; +} + +.readtime { + position: relative; + color: var(--md-default-fg-color); + opacity: 0.28; + text-align: right; + font-family: "JetBrains Mono"; + font-size: 1em; + font-style: normal; + font-weight: 300; + top: -12em; +} + +.writparent { + margin-top: -8px; + margin-bottom: -8px; + height: 87px; + width: 100%; + transition: all 0.3s ease; + transform-origin: center; + overflow: hidden; +} + +.writparent:hover { + transform: scale(0.98); + cursor: pointer; +} + +.content7 { + position: absolute; + width: 100%; + height: auto; + left: 50%; + max-width: 1350px; + transform: translateX(-50%); + padding-left: 7em; + padding-right: 7em; + top: calc(var(--content1-height) + 24em + var(--content2-height) + var(--content3-height) + 40px + var(--content4-height) + 80px + var(--content5-height) + 130px + var(--content6-height) + 140px); +} + +.hiddentext { + color: var(--md-default-bg-color); + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.content8 { + position: absolute; + width: 100%; + height: auto; + left: 50%; + max-width: 1350px; + transform: translateX(-50%); + padding-left: 7em; + padding-right: 7em; + top: calc(var(--content1-height) + 24em + 20px + var(--content2-height) + 20px + var(--content3-height) + 40px + var(--content4-height) + 80px + var(--content5-height) + 130px + var(--content6-height) + 140px + var(--content7-height) + 150px); +} + +.socialpar { + display: inline-block; + margin-left: -7.5px; + width: 32px; + height: 32px; + padding: 8.39px 8.25px 7.61px 8.25px; + justify-content: center; + align-items: center; + flex-shrink: 0; +} + +.fa-github, .fa-twitter, .fa-paper-plane { + width: 15.5px; + height: 16px; + flex-shrink: 0; + color: var(--md-default-fg-color); + opacity: 0.9; + transition: all 0.3s ease; +} + +.fa-github:hover, .fa-twitter:hover, .fa-paper-plane:hover { + scale: 0.92; + opacity: 0.8; +} + +.copyright { + margin-top: 0.5em; + margin-bottom: 10em; + color: var(--md-default-fg-color); + opacity: 0.9; + font-family: "JetBrains Mono"; + font-size: 0.8em; + font-style: normal; + font-weight: 400; + line-height: 20.48px; +} + +.toggle .main-navigation { + transition-duration: 0.35; + transform: translateX(0); +} + +.toggle .main-navigation a { + animation: link-appear calc(var(--duration) * 1.5) var(--ease) forwards; +} + +.mobileshow { + display: none; +} + +@keyframes link-appear { + 0%, 25% { + transform: translateY(100%); + } + 50%, 100% { + transform: translateY(0); + } +} + +@media (max-width: 59.892857142857146em) { + .toggle .md-search__icon.md-icon:first-of-type { + color: var(--md-default-fg-color); + } + +} + +@media screen and (min-width: 2000px) { /* Desktop */ + .toggle header { + padding-top: 10em; + } + :root { + --font-size: 1em; + } + .introabt h2 { + font-size: 2.5em; + } + .introabt h3 { + font-size: 0.98em; + } + .content3 h2 { + font-size: 1.9em; + } + .company { + font-size: 1em; + } + .role{ + font-size: 1em; + } + .year { + font-size: 1em; + } + .content4 h2, .content5 h2, .content6 h2, .content7 h2 { + font-size: 32px; + } + .content4 p, .content7 p { + font-size: 0.9em; + } + .content4 a, .content7 a { + font-size: 1em; + } + .projtitle { + font-size: 1em; + } + .projdescription { + font-size: 0.9em; + } + .writeyear { + font-size: 0.85em; + } + .readtime { + font-size: 0.85em; + } + .copyright { + font-size: 0.8em; + } +} + +@media screen and (max-width: 1600px) { + .toggle header { + padding-top: 10em; + } +} + +@media (max-width: 1200px) { /* Laptop */ + header { + padding-top: 5em; + padding-left: 1em; + padding-right: 1em; + } + .toggle header { + padding-top: 5em; + } + .main-navigation a { + font-size: 1.1em; + } + body { + font-size: 0.9em; + } + .content1 { + padding-left: 1em; + padding-right: 1em; + } + .content2, .content3, .content4, .content5, .content6, .content7, .content8 { + padding-left: 1em; + padding-right: 1em; + } + .introabt h2 { + font-size: 2.5em; + } + .introabt h3 { + font-size: 1em; + } + .content3 h2, .content4 h2, .content5 h2, .content6 h2, .content7 h2 { + font-size: 1.7em; + } + .companylogo { + width: 23px; + height: 23px; + margin-right: 9px; + margin-bottom: -0.4em; + } + .pfp { + width: 30px; + height: 30px; + margin-right: 9px; + margin-left: 2px; + margin-bottom: -0.05em; + } + .company { + font-size: 0.95em; + } + .role { + font-size: 0.95em; + } + .year { + font-size: 0.95em; + right: 1em; + } + .content4 p, .content7 p { + font-size: 0.95em; + } + .content4 { + top: calc(var(--content1-height) + 24em + var(--content2-height) + var(--content3-height)); + } + .content5 { + top: calc(var(--content1-height) + 24em + 97px + var(--content2-height) + var(--content3-height) + var(--content4-height)); + } + .fa-arrow-up-right-from-square { + scale: 0.8; + margin-right: 0.9em; + top: -4.5em; + } + .content5 .grid-container { + margin-top: 2em; + } + .content5 .grid-container a { + height: 81px; + } + .content5 .grid-item { + height: 81px; + } + .projtitle { + font-size: 0.95em; + } + .projdescription { + font-size: 0.825em; + } + .content5 .projtitle { + top: 0.1em; + left: 1em; + } + .content5 .projdescription { + top: -1.3em; + left: 1.3em; + } + .writparent { + margin-top: -12px; + margin-bottom: -12px; + height: 58px; + } + .imgparent { + display: flex; + width: 98px; + height: 58px; + padding: 2px; + border-radius: 18px; + } + .imgparent img { + width: 94px; + height: 51.34px; + border-radius: 15px; + } + .writeyear, .readtime { + font-size: 0.825em; + } + .content6 .projtitle { + position: relative; + top: -4.2em; + left: 7.2em; + } + .content6 .projdescription { + position: relative; + top: -6.2em; + left: 8.3em; + } + .content6 { + top: calc(var(--content1-height) + 24em + 97px + var(--content2-height) + 40px + var(--content3-height) + var(--content4-height) + var(--content5-height)); + } + .content7 { + top: calc(var(--content1-height) + 24em + 157px + var(--content2-height) + 40px + var(--content3-height) + var(--content4-height) + var(--content5-height) + var(--content6-height)); + } + .content8 { + top: calc(var(--content1-height) + 24em + 147px + var(--content2-height) + 160px + var(--content3-height) + var(--content4-height) + var(--content5-height) + var(--content6-height) + var(--content7-height)); + } +} + +@media (max-width: 860px) { /* Tablet */ + .content1 { + padding-left: 1em; + padding-right: 1em; + } + .content2, .content3, .content4, .content5, .content6, .content7, .content8 { + padding-left: 1em; + padding-right: 1em; + } + .introabt h2 { + font-size: 2.5em; + } + .introabt h3 { + font-size: 1em; + } + .content3 h2, .content4 h2, .content5 h2, .content6 h2, .content7 h2 { + font-size: 1.5em; + } + .companylogo { + width: 20px; + height: 20px; + margin-right: 8px; + margin-bottom: -0.3em; + } + .inlineImg { + display: inline-block; + width: 16px; + height: 16px; + border-radius: 4px; + margin-right: 0; + margin-left: -3px; + margin-bottom: -0.3em; + flex-shrink: 0; + } + .company { + font-size: 0.8em; + } + .role { + font-size: 0.8em; + } + .year { + font-size: 0.8em; + } + .content4 p, .content7 p { + font-size: 0.8em; + } + .fa-arrow-up-right-from-square { + scale: 0.8; + margin-right: 0.9em; + top: -3.75em; + } + .projtitle { + font-size: 0.8em; + } + .projdescription { + font-size: 0.675em; + } + .content5 .projtitle { + top: 0.6em; + left: 1em; + } + .content5 .projdescription { + top: -0.8em; + left: 1.3em; + } + .writeyear, .readtime { + font-size: 0.675em; + } + .content6 .projtitle { + position: relative; + top: -4.9em; + left: 9em; + } + .content6 .projdescription { + position: relative; + top: -6.9em; + left: 10.7em; + } +} + +@media (max-width: 695px) { /* Mobile */ + :root{ + --content6-height: 700px; + } + .main-navigation a { + font-size: 1.5em; + } + body { + font-size: 0.8em; + } + .content1 { + padding-left: 1em; + padding-right: 1em; + } + .content2 { + display: none; + } + .content3, .content4, .content5, .content6, .content7, .content8 { + padding-left: 1em; + padding-right: 1em; + } + .introabt h2 { + font-size: 1.9em; + } + .introabt h3 { + font-size: 0.8em; + } + .content3 h2, .content4 h2, .content5 h2, .content6 h2, .content7 h2 { + font-size: 1.5em; + } + .content3 h2 { + text-align: left; + } + .content3 { + top: calc(var(--content1-height) + 13em); + } + .companylogo { + width: 20px; + height: 20px; + margin-right: 8px; + margin-bottom: -0.3em; + } + .pfp { + width: 24px; + height: 24px; + margin-right: 9px; + margin-left: 2px; + margin-bottom: -0.07em; + } + .inlineImg { + display: inline-block; + width: 16px; + height: 16px; + border-radius: 4px; + margin-right: 0; + margin-left: -5px; + margin-bottom: -0.3em; + flex-shrink: 0; + } + .company { + font-size: 0.8em; + margin-right: 18px; + } + .role { + font-size: 0.8em; + } + .year { + font-size: 0.8em; + right: 1em; + } + .mobileyear { + display: none; + } + .mobileshow { + display: inline; + } + .content4 { + top: calc(var(--content1-height) + 8em + var(--content3-height)); + } + .content4 p, .content7 p { + font-size: 0.85em; + } + .abt1, .abt2, .abt3 { + width: 100%; + height: auto; + display: inline-block; + } + .abt1{ + margin-right: 0px; + } + .abt2{ + margin: 0px; + } + .abt3{ + margin-left: 0px; + } + .content5 { + top: calc(var(--content1-height) + 19em + var(--content3-height) + var(--content4-height)); + } + .fa-arrow-up-right-from-square { + scale: 0.7; + margin-right: 0.9em; + top: -4em; + } + .content5 .grid-container { + margin-top: 2em; + grid-template-columns: 1fr; + } + .content5 .grid-container a { + height: 65px; + } + .content5 .grid-item { + height: 65px; + border-radius: 18px; + } + .projtitle { + font-size: 0.85em; + } + .projdescription { + font-size: 0.725em; + } + .content5 .projtitle { + top: 0.05em; + left: 1em; + } + .content5 .projdescription { + top: -1.45em; + left: 1.3em; + } + .writparent { + margin-top: -10px; + margin-bottom: -78px; + height: auto; + } + .imgparent { + display: flex; + width: 100%; + height: auto; + padding: 2px; + border-radius: 18px; + } + .imgparent img { + width: 100%; + height: auto; + border-radius: 15px; + } + .writeyear{ + font-size: 0.625em; + top: -7.24em; + right: 1em; + } + .content6 { + top: calc(var(--content1-height) + 33em + var(--content3-height) + var(--content4-height) + var(--content5-height)); + } + .content6 .projtitle { + position: relative; + top: -0.4em; + left: 1em; + } + .content6 .projdescription { + position: relative; + top: -2.2em; + left: 1.2em; + } + .content7 { + top: calc(var(--content1-height) + 37em + var(--content3-height) + var(--content4-height) + var(--content5-height) + var(--content6-height)); + } + .content8 { + top: calc(var(--content1-height) + 44em + var(--content3-height) + var(--content4-height) + var(--content5-height) + var(--content6-height) + var(--content7-height)); + } + .copyright { + font-size: 0.6em; + } +} + +@media (max-width: 400px) { /* Mobile Small */ + .pfp { + display: none; + } +} diff --git a/assets/css/proj.css b/assets/css/proj.css new file mode 100644 index 00000000..eb275b45 --- /dev/null +++ b/assets/css/proj.css @@ -0,0 +1,777 @@ +:root { + --duration: 1s; + --nav-duration: calc(var(--duration) / 4); + --ease: cubic-bezier(0.215, 0.61, 0.355, 1); + --space: 1rem; + --font-size: 1em; + --line-height: 1.5; + --inputicon: hsla(0, 0%, 0%, 0.87); + --toggledinputicon: white; + --formbg: hsla(0, 0%, 0%, 0.25); + --formhover: hsla(0, 0%, 0%, 0.32); + --toggledform: hsla(0, 0%, 100%, 0.12); + --toggledformhover: hsla(0, 0%, 100%, 0.3); + --avatar: url("../../assets/images/index/avatardark.svg"); + --articlebg: hsla(0, 0%, 0%, 0.04); + --content1-height: 190px; + --content6-height: 383px; + --tone-url-light: "assets/images/index/tonelight.png"; + --tone-url-slate: "assets/images/index/tonedark.png"; +} + +[data-md-color-scheme="slate"] { + --inputicon: white; + --toggledinputicon: hsla(0, 0%, 0%, 0.87); + --formhover: hsla(0, 0%, 100%, 0.3); + --toggledform: hsla(0, 0%, 0%, 0.2); + --toggledformhover: hsla(0, 0%, 0%, 0.32); + --avatar: url("../../assets/images/index/avatarlight.svg"); + --articlebg: hsla(234, 76%, 95%, 0.04); +} + +.logo { + content: var(--avatar); +} + +@media screen and (min-width: 76.25em) { + .md-sidebar--primary { + display: none; + } +} + +.md-sidebar { + width: 0px; +} + +.md-main__inner { + margin: 0; + height: calc(var(--content1-height) + var(--content6-height)); +} + +.md-source { + color: var(--md-default-fg-color); +} + +.toggle .socialsparent { + display: none; +} + +.close { + display: none; +} + +.toggle .close { + display: flex; +} + +.toggle .menu { + display: none; +} + +.md-header__button.md-icon[for="__search"] { + color: var(--md-default-fg-color); +} + +.toggle .md-header__button.md-icon[for="__search"] { + color: var(--md-default-bg-color); +} + +.md-search__input + .md-search__icon { + color: var(--inputicon); +} + +.toggle .md-search__input + .md-search__icon { + color: var(--toggledinputicon); +} + +.md-search__form { + background-color: var(--formbg); +} + +.md-search__form:hover { + background-color: var(--formhover); +} + +.toggle .md-search__form { + background-color: var(--toggledform); +} + +.toggle .md-search__form:hover { + background-color: var(--toggledformhover); +} + +.menu { + color: var(--md-default-fg-color); +} + +.close { + color: var(--md-default-bg-color); +} + +.md-header__option { + color: var(--md-default-fg-color); +} + +.toggle .md-header__option { + color: var(--md-default-bg-color); +} + +.md-header[data-md-state=shadow] { + box-shadow: 0 0 0 rgba(0, 0, 0, 0), 0 0 0 rgba(0, 0, 0, 0); +} + +.md-header { + background: none; + box-shadow: 0 0 0 transparent, 0 0 0 transparent; +} + +.md-header p { + display: none; +} + +.md-tabs { + display: none; +} + +.logo { + padding-left: 0; +} + +.blur-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + height: 100px; + pointer-events: none; + z-index: 2; + backdrop-filter: blur(48px); + -webkit-backdrop-filter: blur(48px); + mask-image: linear-gradient( + to bottom, + black 0%, + rgba(0, 0, 0, 0.9) 20%, + rgba(0, 0, 0, 0.6) 45%, + transparent 100% + ); + -webkit-mask-image: linear-gradient( + to bottom, + black 0%, + rgba(0, 0, 0, 0.9) 20%, + rgba(0, 0, 0, 0.6) 45%, + transparent 100% + ); +} + +.abtbtn { + width: 92px; + height: 92px; +} + +.abtbtn:hover { + cursor: pointer; +} + +@keyframes rotate { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +@keyframes reverse { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(-360deg); + } +} + +.circleGroup { + --rotation-direction: normal; + transform-origin: 1137px 797px; + animation: rotate 7s linear infinite var(--rotation-direction); +} + +.circleLink { + transition: all 0.3s ease; + transform-origin: 1137px 797px; +} + +.circleLink:hover { + transform: scale(0.97); +} + +.main-navigation { + position: fixed; + top: 0; + left: 0; + display: flex; + align-items: center; + width: 100%; + height: 100%; + transform: translateX(-100%); + transition: transform var(--nav-duration); + background-color: var(--md-default-fg-color); + z-index: 3; +} + +.main-navigation:after { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: inherit; + transform-origin: 0 50%; + z-index: 3; +} + +.main-navigation ul { + font-size: 9vmin; + width: 100%; + height: auto; + z-index: 4; +} + +.main-navigation li { + display: flex; + align-items: center; + position: relative; + overflow: hidden; + z-index: 4; + margin-top: -0.3em; + margin-bottom: -0.3em; +} + +.main-navigation li:after { + content: ""; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: auto; + background-color: inherit; + transform-origin: 0 50%; + transform: translateX(-100%) skew(15deg); + z-index: 4; +} + +.main-navigation a { + display: inline-block; + width: 100%; + max-width: 1200px; + margin: 0 auto; + color: var(--md-default-bg-color); + font-family: "Crimson Pro"; + font-size: 1.2em; + line-height: 1.2; + text-decoration: none; + user-select: none; + padding: auto; + transform: translateY(100%); + z-index: 4; +} + +.main-navigation a:hover { + cursor: pointer; + color: var(--md-default-bg-color); +} + +.main-content { + z-index: 1; +} + +.home, .proj, .writ { + transition: all 0.3s ease; +} + +.home:hover, .proj:hover, .writ:hover { + font-style: italic; +} + +.navnum { + font-size: 0.35em; + font-weight: lighter; + font-style: italic; +} + +.content1 { + position: absolute; + width: 100%; + max-width: 1350px; + height: 100px; + top: 24em; + left: 50%; + transform: translateX(-50%); + margin-top: -100px; + margin-bottom: 0; + padding-left: 7em; + padding-right: 7em; +} + +.introabt { + margin-top: -5em; +} + +.introabt h2 { + color: var(--md-default-fg-color); + font-family: "Crimson Pro"; + font-size: 2.8125em; + font-style: normal; + font-weight: 400; + line-height: normal; + margin-bottom: 0em; +} + +.introabt h2 a{ + color: var(--md-default-fg-color); +} + +.introabt h2 a:hover { + color: var(--md-default-fg-color); +} + +.introabt h2 span { + font-style: italic; +} + +.introabt h3 { + color: var(--md-default-fg-color); + font-family: "JetBrains Mono"; + font-size: 1.1875em; + font-style: normal; + font-weight: 400; + line-height: 109.588%; +} + +.intro-section { + margin-top: 14em; +} + +.projtitle { + position: relative; + color: var(--md-default-fg-color); + opacity: 0.75; + font-family: "JetBrains Mono"; + font-size: 1.2em; + font-style: normal; + font-weight: 300; + top: -0.3em; + left: 1em; +} + +.projdescription { + position: relative; + width: 45%; + color: var(--md-default-fg-color); + opacity: 0.55; + font-family: "JetBrains Mono"; + font-size: 1em; + font-style: normal; + font-weight: 300; + top: -1.7em; + left: 1em; +} + +.content6 { + position: absolute; + width: 100%; + height: auto; + left: 50%; + max-width: 1350px; + transform: translateX(-50%); + padding-left: 7em; + padding-right: 7em; + top: calc(var(--content1-height) + 18em + 20px); +} + +.content6 h2 { + margin-bottom: 1.5em; +} + +.imgparent { + display: flex; + width: 378px; + height: 248px; + padding: 5px; + justify-content: center; + align-items: center; + flex-shrink: 0; + border-radius: 28px; + border: 1px solid var(--md-default-fg-color--lightest); +} + +.imgparent img { + object-fit: cover; + display: flex; + width: 360px; + height: 230px; + justify-content: center; + align-items: center; + flex-shrink: 0; + border-radius: 22px; + border: 1px solid var(--md-default-fg-color--lightest); +} + +.content6 .projtitle { + position: relative; + top: -13em; + left: 20.75em; +} + +.content6 .projdescription { + position: relative; + top: -22em; + left: 25em; +} + +.writeyear { + position: relative; + color: var(--md-default-fg-color); + opacity: 0.38; + top: -18.65em; + text-align: right; + font-family: "JetBrains Mono"; + font-size: 1em; + font-style: normal; + font-weight: 300; +} + +.readtime { + position: relative; + color: var(--md-default-fg-color); + opacity: 0.28; + top: -19.9em; + text-align: right; + font-family: "JetBrains Mono"; + font-size: 1em; + font-style: normal; + font-weight: 300; +} + +.writparent { + margin-top: -8px; + margin-bottom: -8px; + height: 260px; + width: 100%; + transition: all 0.3s ease; + transform-origin: center; + overflow: hidden; +} + +.writparent:hover { + transform: scale(0.98); + cursor: pointer; +} + +.content8 { + position: absolute; + width: 100%; + height: auto; + left: 50%; + max-width: 1350px; + transform: translateX(-50%); + padding-left: 7em; + padding-right: 7em; + top: calc(var(--content1-height) + var(--content6-height) + 25em); +} + +.socialpar { + display: inline-block; + margin-left: -7.5px; + width: 32px; + height: 32px; + padding: 8.39px 8.25px 7.61px 8.25px; + justify-content: center; + align-items: center; + flex-shrink: 0; +} + +.fa-github, .fa-twitter, .fa-paper-plane { + width: 15.5px; + height: 16px; + flex-shrink: 0; + color: var(--md-default-fg-color); + opacity: 0.9; + transition: all 0.3s ease; +} + +.fa-github:hover, .fa-twitter:hover, .fa-paper-plane:hover { + scale: 0.92; + opacity: 0.8; +} + +.copyright { + margin-top: 0.5em; + margin-bottom: 10em; + color: var(--md-default-fg-color); + opacity: 0.9; + font-family: "JetBrains Mono"; + font-size: 0.8em; + font-style: normal; + font-weight: 400; + line-height: 20.48px; +} + +.toggle .main-navigation { + transition-duration: 0.35; + transform: translateX(0); +} + +.toggle .main-navigation a { + animation: link-appear calc(var(--duration) * 1.5) var(--ease) forwards; +} + +.mobileshow { + display: none; +} + +.laphide { + display: inline; +} + +@keyframes link-appear { + 0%, 25% { + transform: translateY(100%); + } + 50%, 100% { + transform: translateY(0); + } +} + +@media (max-width: 59.892857142857146em) { + .toggle .md-search__icon.md-icon:first-of-type { + color: var(--md-default-fg-color); + } + +} + +@media screen and (min-width: 2000px) { /* Desktop */ + :root { + --font-size: 1em; + } + .introabt h2 { + font-size: 2.5em; + } + .introabt h3 { + font-size: 0.98em; + } + .projtitle { + font-size: 1em; + } + .projdescription { + font-size: 0.9em; + } + .content6 .projtitle { + position: relative; + top: -13em; + left: 20.75em; + } + + .content6 .projdescription { + position: relative; + top: -20.65em; + left: 23.1em; + } + .writeyear { + font-size: 0.85em; + } + .readtime { + font-size: 0.85em; + } + .copyright { + font-size: 0.8em; + } + .laphide { + display: none; + } +} + +@media (max-width: 1200px) { /* Laptop */ + .main-navigation a { + font-size: 1.1em; + } + body { + font-size: 0.9em; + } + .content1 { + padding-left: 1em; + padding-right: 1em; + } + .content6, .content8 { + padding-left: 1em; + padding-right: 1em; + } + .introabt h2 { + font-size: 2.5em; + } + .introabt h3 { + font-size: 1em; + } + .content6 h2 { + font-size: 1.7em; + } + .projtitle { + font-size: 0.95em; + } + .projdescription { + font-size: 0.825em; + } + .content5 .projtitle { + top: 0.1em; + left: 1em; + } + .content5 .projdescription { + top: -1.3em; + left: 1.3em; + } + .writparent { + margin-top: -12px; + margin-bottom: -12px; + height: 150px; + } + .imgparent { + display: flex; + width: 220px; + height: 150px; + padding: 2px; + border-radius: 18px; + } + .imgparent img { + width: 205px; + height: 135px; + border-radius: 12px; + } + .laphide { + display: none; + } + .writeyear, .readtime { + font-size: 0.675em; + } + .content6 .projtitle { + position: relative; + top: -10.2em; + left: 15.2em; + } + .content6 .projdescription { + position: relative; + top: -17.2em; + left: 17.7em; + } + .content6 { + top: calc(var(--content1-height) + 16em + 20px); + } + .content8 { + top: calc(var(--content1-height) + var(--content6-height) + 25em); + } +} + +@media (max-width: 860px) { /* Tablet */ + .content1 { + padding-left: 1em; + padding-right: 1em; + } + .content6, .content8 { + padding-left: 1em; + padding-right: 1em; + } + .introabt h2 { + font-size: 2.5em; + } + .introabt h3 { + font-size: 1em; + } + .content6 h2 { + font-size: 1.5em; + } +} + +@media (max-width: 695px) { /* Mobile */ + :root{ + --content6-height: 700px; + } + .main-navigation a { + font-size: 1.5em; + } + body { + font-size: 0.8em; + } + .content1 { + padding-left: 1em; + padding-right: 1em; + } + .content6, .content8 { + padding-left: 1em; + padding-right: 1em; + } + .introabt h2 { + font-size: 1.9em; + } + .introabt h3 { + font-size: 0.8em; + } + .content6 h2{ + font-size: 1.5em; + } + .mobileshow { + display: inline; + } + .projtitle { + font-size: 0.85em; + } + .projdescription { + font-size: 0.725em; + } + .writparent { + margin-top: -10px; + margin-bottom: -98px; + height: auto; + } + .imgparent { + display: flex; + width: 100%; + height: 375px; + padding: 5px; + border-radius: 18px; + } + .imgparent img { + width: 100%; + height: 360px; + border-radius: 12px; + } + .readtime{ + font-size: 0.625em; + top: -5.2em; + right: 1em; + } + .writeyear{ + font-size: 0.625em; + top: -4em; + right: 1em; + } + .content6 { + top: calc(var(--content1-height) + 15em); + } + .content6 .projtitle { + position: relative; + top: -0.4em; + left: 1em; + } + .content6 .projdescription { + position: relative; + top: -6.2em; + left: 1.2em; + width: 70%; + } + .content8 { + top: calc(var(--content1-height) + var(--content6-height) + 20em); + } + .copyright { + font-size: 0.6em; + } +} diff --git a/assets/css/projects/milling.css b/assets/css/projects/milling.css new file mode 100644 index 00000000..afa7b2ae --- /dev/null +++ b/assets/css/projects/milling.css @@ -0,0 +1,731 @@ +:root { + --toggleboxshadow: 0 1px 3px rgba(0, 0, 0, 0.6); + --calcplaceholder: rgba(0, 0, 0, 0.75); + } + + [data-md-color-scheme="slate"] { + --toggleboxshadow: 0 1px 3px rgba(75, 75, 75, 0.6); + --calcplaceholder: rgba(255, 255, 255, 0.75); + } + + .toggleswitch { + width: 100%; + height: 20px; + margin-bottom: -2.5em; + } + + .cl-switch { + position: relative; + top: -2.82em; + left: 80%; + padding-right: 2em; + } + + .cl-switch input[type="checkbox"] { + display: none; + visibility: hidden; + } + + .cl-switch .switcher { + display: inline-block; + border-radius: 100px; + width: 35px; + height: 15px; + background-color: var(--md-default-fg-color--lighter); + position: relative; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + vertical-align: middle; + cursor: pointer; + } + .cl-switch .switcher:before { + content: ""; + display: block; + width: 20px; + height: 20px; + background-color: #fff; + box-shadow: var(--toggleboxshadow); + border-radius: 50%; + margin-top: -2.5px; + position: absolute; + top: 0; + left: 0; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + margin-right: 0; + -webkit-transition: all 0.2s; + -moz-transition: all 0.2s; + -ms-transition: all 0.2s; + -o-transition: all 0.2s; + transition: all 0.2s; + } + .cl-switch .switcher:active:before { + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6), 0 0 0 10px rgba(63, 81, 181, 0.3); + transition: all, 0.1s; + } + + .cl-switch .label { + cursor: pointer; + vertical-align: middle; + margin: 0 5px; + } + + .cl-switch input[type="checkbox"]:checked + .switcher { + background-color: #5466CE; + } + .cl-switch input[type="checkbox"]:checked + .switcher:before { + left: 100%; + margin-left: -20px; + background-color: #3f51b5; + } + .cl-switch [disabled]:not([disabled="false"]) + .switcher { + background: #000 !important; + } + .cl-switch [disabled]:not([disabled="false"]) + .switcher:active:before { + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.2) !important; + } + .cl-switch [disabled]:not([disabled="false"]) + .switcher:before { + background-color: #e2e2e2 !important; + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.2) !important; + } + + #matlib { + position: relative; + top: 0.1em; + left: 80%; + padding-right: 2em; + color: var(--md-default-fg-color); + height: 31.5px; + width: 180px; + outline: 1px solid rgba(0, 0, 0, 0.08); + border-radius: 5px; + border: none; + background-color: var(--md-default-bg-color); + padding-left: 20px; + font-size: 1em; + } + + #matlib option { + color: var(--calcplaceholder); + text-align: center; + font-size: 1em; + pointer-events: none; + z-index: 2; + } + + #diameterparent{ + position: relative; + left: 1em; + } + + .FluteCountParent { + position: relative; + top: -1.9em; + padding-left: 14em; + } + + #surfacepar { + position: relative; + top: -3em; + left: 1em; + margin-bottom:-5em; + } + + #chippar { + position: relative; + top: -5em; + left: 14em; + margin-bottom:-5em; + } + + @media (max-width: 59.892857142857146em) { + .cl-switch { + position: relative; + top: -2.82em; + left: 0; + float: right; + margin-right: -1.6em; + margin-bottom: -5em; + } + #matlib { + position: relative; + top: 0em; + left: 0; + float: right; + margin-right: 0; + margin-bottom: -5em; + } + #surfacepar { + position: relative; + top: 0em; + margin-bottom: -3.5em; + } + #chippar { + position: relative; + top: -3.7em; + margin-bottom: -3.5em; + } + } + + .outlined-input { + position: relative; + height: 31.5px; + width: 180px; + display: flex; + align-items: center; + } + + .outlined-input input { + color: var(--md-default-fg-color); + height: 31.5px; + width: 180px; + outline: 1px solid rgba(0, 0, 0, 0.08); + border-radius: 5px; + border: none; + background-color: var(--md-default-bg-color); + padding-left: 20px; + font-size: 1em; + transition: .1s cubic-bezier(.65,.05,.36,1); + } + + .outlined-input label { + position: absolute; + top: 50%; + width: 180px; + transform: translateY(-50%); + color: var(--calcplaceholder); + text-align: center; + -webkit-transition: all 0.2s cubic-bezier(0.79, 0.33, 0.2, 0.59); + transition: all 0.2s cubic-bezier(0.79, 0.33, 0.2, 0.59); + font-size: 1em; + pointer-events: none; + z-index: 2; + } + + .outlined-input input:not(:placeholder-shown) ~ label, + .outlined-input input:focus ~ label { + top: 0px; + left: 15px; + width: max-content; + font-size: 0.7em; + background-color: var(--md-default-bg-color); + } + + .outlined-input input:focus { + outline: 1px solid rgba(84, 102, 206, 0.6); !important; + } + .outlined-input input:hover { + outline: 1px solid rgba(84, 102, 206, 0.6); + } + .outlined-input input:focus ~ label { + color: #5466CE; !important; + } + + .calcbutton { + position: relative; + left: 1em; + color: var(--md-default-fg-color); + height: 31.5px; + outline: 1px solid rgba(0, 0, 0, 0.08); + border-radius: 5px; + border: none; + background-color: var(--md-default-bg-color); + padding: 3px; + font-size: 1em; + margin-right: 0.5em; + } + + .flutecountparent { + display: inline; + justify-content: center; + align-items: center; + font-size: 1rem; + } + + .quantity { + position: relative; + } + + input[type=number]::-webkit-inner-spin-button, + input[type=number]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; + } + + input[type=number] { + -moz-appearance: textfield; + } + + .quantity input { + width: 51px; + height: 31.5px; + line-height: 1.65; + float: left; + display: block; + padding: 0; + margin: 0; + padding-left: 15px; + border: none; + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.08); + font-size: 1.1em; + border-radius: 4px; + } + + .quantity input:focus { + outline: 0; + } + + .quantity label { + font-size: 0.85em; + opacity: 0.8; + position: absolute; + top: -15px; + left: -6px; + } + + .quantity-nav { + float: left; + position: relative; + height: 31.5px; + } + + .quantity-button { + position: relative; + cursor: pointer; + border: none; + border-left: 1px solid rgba(0, 0, 0, 0.08); + width: 15.75px; + text-align: center; + color: #333; + font-size: 13px; + font-family: "FontAwesome" !important; + line-height: 1.5; + padding: 0; + background: #FAFAFA; + -webkit-transform: translateX(-100%); + transform: translateX(-100%); + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + -o-user-select: none; + user-select: none; + } + + .quantity-button:active { + background: #EAEAEA; + } + + .quantity-button.quantity-up { + position: absolute; + height: 50%; + top: 0; + border-bottom: 1px solid rgba(0, 0, 0, 0.08); + font-family: "FontAwesome"; + border-radius: 0 4px 0 0; + line-height: 1.6 + } + + .quantity-button.quantity-down { + position: absolute; + bottom: 0; + height: 50%; + font-family: "FontAwesome"; + border-radius: 0 0 4px 0; + } + + .mm { + display: none; + } + + .mmmin { + display: none; + } + + .mmin { + display: none; + } + + .in { + display: inline; + } + + .inmin { + display: inline; + } + + .ftmin { + display: inline; + } + + .metric .mm { + display: inline; + } + + .metric .mmmin { + display: inline; + } + + .metric .mmin { + display: inline; + } + + .metric .in { + display: none; + } + + .metric .inmin { + display: none; + } + + .metric .ftmin { + display: none; + } + + .tablehover:hover { + cursor: pointer; + } + + #right1 { + display: inline; + } + + #down1 { + display: none; + } + + #right2 { + display: inline; + } + + #down2 { + display: none; + } + + #right3 { + display: inline; + } + + #down3 { + display: none; + } + + #right4 { + display: inline; + } + + #down4 { + display: none; + } + + #right5 { + display: inline; + } + + #down5 { + display: none; + } + + #right6 { + display: inline; + } + + #down6 { + display: none; + } + + #right7 { + display: inline; + } + + #down7 { + display: none; + } + + #right8 { + display: inline; + } + + #down8 { + display: none; + } + + #right9 { + display: inline; + } + + #down9 { + display: none; + } + + #right10 { + display: inline; + } + + #down10 { + display: none; + } + + #right11 { + display: inline; + } + + #down11 { + display: none; + } + + #right12 { + display: inline; + } + + #down12 { + display: none; + } + + #right13 { + display: inline; + } + + #down13 { + display: none; + } + + #right14 { + display: inline; + } + + #down14 { + display: none; + } + + #right15 { + display: inline; + } + + #down15 { + display: none; + } + + #right16 { + display: inline; + } + + #down16 { + display: none; + } + + #right17 { + display: inline; + } + + #down17 { + display: none; + } + + #right18 { + display: inline; + } + + #down18 { + display: none; + } + + #right19 { + display: inline; + } + + #down19 { + display: none; + } + + #right20 { + display: inline; + } + + #down20 { + display: none; + } + + #right21 { + display: inline; + } + + #down21 { + display: none; + } + + + .hidden_row1 #right1 { + display: none; + } + + .hidden_row1 #down1 { + display: inline; + } + + .hidden_row2 #right2 { + display: none; + } + + .hidden_row2 #down2 { + display: inline; + } + + .hidden_row3 #right3 { + display: none; + } + + .hidden_row3 #down3 { + display: inline; + } + + .hidden_row4 #right4 { + display: none; + } + + .hidden_row4 #down4 { + display: inline; + } + + .hidden_row5 #right5 { + display: none; + } + + .hidden_row5 #down5 { + display: inline; + } + + .hidden_row6 #right6 { + display: none; + } + + .hidden_row6 #down6 { + display: inline; + } + + .hidden_row7 #right7 { + display: none; + } + + .hidden_row7 #down7 { + display: inline; + } + + .hidden_row8 #right8 { + display: none; + } + + .hidden_row8 #down8 { + display: inline; + } + + .hidden_row9 #right9 { + display: none; + } + + .hidden_row9 #down9 { + display: inline; + } + + .hidden_row10 #right10 { + display: none; + } + + .hidden_row10 #down10 { + display: inline; + } + + .hidden_row11 #right11 { + display: none; + } + + .hidden_row11 #down11 { + display: inline; + } + + .hidden_row12 #right12 { + display: none; + } + + .hidden_row12 #down12 { + display: inline; + } + + .hidden_row13 #right13 { + display: none; + } + + .hidden_row13 #down13 { + display: inline; + } + + .hidden_row14 #right14 { + display: none; + } + + .hidden_row14 #down14 { + display: inline; + } + + .hidden_row15 #right15 { + display: none; + } + + .hidden_row15 #down15 { + display: inline; + } + + .hidden_row16 #right16 { + display: none; + } + + .hidden_row16 #down16 { + display: inline; + } + + .hidden_row17 #right17 { + display: none; + } + + .hidden_row17 #down17 { + display: inline; + } + + .hidden_row18 #right18 { + display: none; + } + + .hidden_row18 #down18 { + display: inline; + } + + .hidden_row19 #right19 { + display: none; + } + + .hidden_row19 #down19 { + display: inline; + } + + .hidden_row20 #right20 { + display: none; + } + + .hidden_row20 #down20 { + display: inline; + } + + .hidden_row21 #right21 { + display: none; + } + + .hidden_row21 #down21 { + display: inline; + } + + @media (max-width: 40rem) { + input{ + width: 70vw; + } + } \ No newline at end of file diff --git a/assets/css/projects/project.css b/assets/css/projects/project.css new file mode 100644 index 00000000..b4e70c53 --- /dev/null +++ b/assets/css/projects/project.css @@ -0,0 +1,627 @@ +:root { + --duration: 1s; + --nav-duration: calc(var(--duration) / 4); + --ease: cubic-bezier(0.215, 0.61, 0.355, 1); + --space: 1rem; + --font-size: 1em; + --line-height: 1.5; + --inputicon: hsla(0, 0%, 0%, 0.87); + --toggledinputicon: white; + --formbg: hsla(0, 0%, 0%, 0.25); + --formhover: hsla(0, 0%, 0%, 0.32); + --toggledform: hsla(0, 0%, 100%, 0.12); + --toggledformhover: hsla(0, 0%, 100%, 0.3); + --avatar: url("../../../assets/images/index/avatardark.svg"); + --articlebg: hsla(0, 0%, 0%, 0.04); +} + +[data-md-color-scheme="slate"] { + --inputicon: white; + --toggledinputicon: hsla(0, 0%, 0%, 0.87); + --formhover: hsla(0, 0%, 100%, 0.3); + --toggledform: hsla(0, 0%, 0%, 0.2); + --toggledformhover: hsla(0, 0%, 0%, 0.32); + --avatar: url("../../../assets/images/index/avatarlight.svg"); + --articlebg: hsla(234, 76%, 95%, 0.04); +} + +.logo { + content: var(--avatar); +} + + +.md-source { + color: var(--md-default-fg-color); +} + +.toggle .socialsparent { + display: none; +} + +.close { + display: none; +} + +.toggle .close { + display: flex; +} + +.toggle .menu { + display: none; +} + +.md-header__button.md-icon[for="__search"] { + color: var(--md-default-fg-color); +} + +.toggle .md-header__button.md-icon[for="__search"] { + color: var(--md-default-bg-color); +} + +.md-search__input + .md-search__icon { + color: var(--inputicon); +} + +.toggle .md-search__input + .md-search__icon { + color: var(--toggledinputicon); +} + +.md-search__form { + background-color: var(--formbg); +} + +.md-search__form:hover { + background-color: var(--formhover); +} + +.toggle .md-search__form { + background-color: var(--toggledform); +} + +.toggle .md-search__form:hover { + background-color: var(--toggledformhover); +} + +.menu { + color: var(--md-default-fg-color); +} + +.close { + color: var(--md-default-bg-color); +} + +.md-header__option { + color: var(--md-default-fg-color); +} + +.toggle .md-header__option { + color: var(--md-default-bg-color); +} + +.md-header[data-md-state=shadow] { + box-shadow: 0 0 0 rgba(0, 0, 0, 0), 0 0 0 rgba(0, 0, 0, 0); +} + +.md-header { + background: none; + box-shadow: 0 0 0 transparent, 0 0 0 transparent; +} + +.md-header p { + display: none; +} + +.md-tabs { + display: none; +} + +.logo { + padding-left: 0; +} + +.main-navigation { + position: fixed; + top: 0; + left: 0; + display: flex; + align-items: center; + width: 100%; + height: 100%; + transform: translateX(-100%); + transition: transform var(--nav-duration); + background-color: var(--md-default-fg-color); + z-index: 3; +} + +.main-navigation:after { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: inherit; + transform-origin: 0 50%; + z-index: 3; +} + +.main-navigation ul { + font-size: 9vmin; + width: 100%; + height: auto; + z-index: 4; +} + +.main-navigation li { + display: flex; + align-items: center; + position: relative; + overflow: hidden; + z-index: 4; + margin-top: -0.3em; + margin-bottom: -0.3em; +} + +.main-navigation li:after { + content: ""; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: auto; + background-color: inherit; + transform-origin: 0 50%; + transform: translateX(-100%) skew(15deg); + z-index: 4; +} + +.main-navigation a { + display: inline-block; + width: 100%; + max-width: 1200px; + margin: 0 auto; + color: var(--md-default-bg-color); + font-family: "Crimson Pro"; + font-size: 1.2em; + line-height: 1.2; + text-decoration: none; + user-select: none; + padding: auto; + transform: translateY(100%); + z-index: 4; +} + +.main-navigation a:hover { + cursor: pointer; + color: var(--md-default-bg-color); +} + +.main-content { + z-index: 1; +} + +.home, .proj, .writ { + transition: all 0.3s ease; +} + +.home:hover, .proj:hover, .writ:hover { + font-style: italic; +} + +.navnum { + font-size: 0.35em; + font-weight: lighter; + font-style: italic; +} + +.blur-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + height: 100px; + pointer-events: none; + z-index: 2; + backdrop-filter: blur(48px); + -webkit-backdrop-filter: blur(48px); + mask-image: linear-gradient( + to bottom, + black 0%, + rgba(0, 0, 0, 0.9) 20%, + rgba(0, 0, 0, 0.6) 45%, + transparent 100% + ); + -webkit-mask-image: linear-gradient( + to bottom, + black 0%, + rgba(0, 0, 0, 0.9) 20%, + rgba(0, 0, 0, 0.6) 45%, + transparent 100% + ); +} + +.return2feed { + padding-bottom: 0.9em; +} + +.return2feed a { + color: var(--md-default-fg-color); + font-family: "JetBrains Mono"; + letter-spacing: -.01em; + font-size: 0.8em; + opacity: 0.6; + transition: all 0.3s ease; +} + +.return2feed a:hover { + color: var(--md-default-fg-color); + opacity: 0.4; + cursor: pointer; +} + +.md-typeset h1 { + color: var(--md-default-fg-color); + opacity: 0.9; + font-family: "Crimson Pro"; + font-size: 2.5125em; + font-style: normal; + font-weight: 400; + line-height: 1.3; + margin: 0 0 0.9em; +} + +.md-typeset h2 { + color: var(--md-default-fg-color); + opacity: 0.9; + font-family: "Crimson Pro"; + font-size: 2.0125em; + font-style: normal; + font-weight: 400; + line-height: 1.4; + margin: 1.6em 0 0.64em; +} + +.md-typeset h3 { + color: var(--md-default-fg-color); + opacity: 0.9; + font-family: "Crimson Pro"; + font-size: 1.7125em; + font-style: normal; + font-weight: 400; + line-height: 1.5; + margin: 1.6em 0 0.8em; +} + +.md-typeset h4 { + color: var(--md-default-fg-color); + opacity: 0.9; + font-family: "Crimson Pro"; + font-size: 1.4125em; + font-style: normal; + font-weight: 400; + line-height: 1.5; + margin: 1em 0; +} + +.md-typeset h5 { + color: var(--md-default-fg-color); + opacity: 0.9; + font-family: "Crimson Pro"; + font-size: 1.1125em; + font-style: normal; + font-weight: 400; + line-height: 1.5; + margin: 1.25em 0; +} + +.md-typeset h6 { + color: var(--md-default-fg-color); + opacity: 0.9; + font-family: "Crimson Pro"; + font-size: 1em; + font-style: normal; + font-weight: 400; + line-height: 1.5; + margin: 1.25em 0; +} + +.profilepic { + display: inline-block; + vertical-align:middle; + margin-top: -2px; + margin-right: -4px; + width: 30px; + height: 29px; + border-radius: 50%; +} + +.profilepic2 { + display: inline-block; + vertical-align:middle; + margin-top: -2px; + margin-right: -4px; + width: 30px; + height: 29px; + border-radius: 50%; +} + +#name { + color: var(--md-default-fg-color); + opacity: 0.9; + font-weight: 300; + font-family: "JetBrains Mono"; + font-size: 0.9em; + padding-left: 7px; + transition: all 0.3s ease; +} + +#name:hover { + opacity: 0.7; +} + +.year, .readTime { + color: var(--md-default-fg-color); + opacity: 0.9; + font-weight: 300; + padding-left: 6px; + font-family: "JetBrains Mono"; + font-size: 0.9em; +} + +.md-typeset .md-button { + border: .1rem solid; + border-radius: .5rem; + color: var(--md-primary-fg-color); + cursor: pointer; + display: inline-block; + font-weight: 700; + padding: .625em 2em; + transition: color 125ms,background-color 125ms,border-color 125ms; +} + +.md-typeset .md-button--primary { + background-color: var(--md-primary-fg-color); + border-color: var(--md-primary-fg-color); + color: var(--md-primary-bg-color); + transition: all 0.3s ease; + opacity: 0.9; +} + +.md-typeset .md-button:focus, +.md-typeset .md-button:hover { + background-color: var(--md-primary-fg-color); + border-color: var(--md-primary-fg-color); + color: var(--md-primary-bg-color); + transform: scale(0.99); + opacity: 0.7; +} + +.abtlinks a { + color: var(--md-default-fg-color); +} + +.share { + display: inline; + float:right; + margin-top: 0.225em; + font-size: 0.9em; +} + +.share a { + color: var(--md-default-fg-color); + opacity: 0.45; + transition: all 0.3s ease; +} + +.share a:hover { + color: var(--md-default-fg-color); + opacity: 0.3; + cursor: pointer; +} + +.twitter { + padding-left: 0.2em; + padding-right: 0.2em; +} + +.fb { + padding-left: 0.2em; + padding-right: 0.2em; +} + +.pin { + padding-left: 0.2em; + padding-right: 0.2em; +} + +.ln { + padding-left: 0.2em; + padding-right: 0.2em; +} + +.email { + padding-left: 0.2em; + padding-right: 1.5em; +} + +.md-footer { + background-color: var(--md-default-bg-color); + color: var(--md-default-fg-color); +} + +.md-footer-meta { + background-color: var(--md-default-bg-color); + padding-top: 2em; +} + +.md-footer__inner, +.md-footer-meta__inner { + display: block; + padding: 0; + max-width: none; +} + +.md-footer__link, +.md-footer__title, +.md-footer__direction, +.md-footer-nav, +.md-footer-meta__inner .md-social, +.md-footer-meta__inner .md-copyright:not(.copyright) { + display: none; +} + +.footer { + width: 100%; + margin-top: 5em; + max-width: 61rem; + margin-left: auto; + margin-right: auto; + padding-left: 1rem; + padding-right: 1rem; + box-sizing: border-box; + text-align: left; +} + +.socials { + margin-left: 0.1em; +} + +.socialpar { + display: inline-block; + margin-left: -7.5px; + width: 32px; + height: 32px; + font-size: 1.4em; + padding: 8.39px 8.25px 7.61px 8.25px; + justify-content: center; + align-items: center; +} + +.fa-github, .fa-twitter, .fa-paper-plane { + width: 15.5px; + height: 16px; + color: var(--md-default-fg-color); + opacity: 0.9; + transition: all 0.3s ease; +} + +.fa-github:hover, .fa-twitter:hover, .fa-paper-plane:hover { + transform: scale(0.92); + opacity: 0.8; +} + +.copyright { + margin-top: 0.5em; + margin-bottom: 10em; + color: var(--md-default-fg-color); + opacity: 0.9; + font-family: "JetBrains Mono", monospace; + font-size: 1.2em; + font-style: normal; + font-weight: 400; + line-height: 20.48px; +} + +.toggle .main-navigation { + transition-duration: 0.35s; + transform: translateX(0); +} + +.toggle .main-navigation a { + animation: link-appear calc(var(--duration) * 1.5) var(--ease) forwards; +} + +@keyframes link-appear { + 0%, 25% { + transform: translateY(100%); + } + 50%, 100% { + transform: translateY(0); + } +} + +@media (max-width: 59.892857142857146em) { + .toggle .md-search__icon.md-icon:first-of-type { + color: var(--md-default-fg-color); + } +} + +@media (max-width: 1200px) { /* Laptop */ + .main-navigation a { + font-size: 1.1em; + } + body { + font-size: 0.9em; + } + .socialpar { + font-size: 0.8em; + } + .copyright { + font-size: 0.7em; + } +} + +@media (max-width: 695px) { /* Mobile */ + .main-navigation a { + font-size: 1.5em; + } + body { + font-size: 0.8em; + } + .share { + display:none; + } + #name { + padding-right: 7px; + } +} + +@media (max-width: 35em) { + .year { + display: none; + } +} + +@media (max-width: 28em) { + .profilepic { + margin-top: -4.5px; + width: 30px; + height: 30px; + } + .abtlinks { + font-size: 0.9em; + } + .year { + display: none; + } +} + +/* Add transition to sidebar while preserving MkDocs Material defaults */ +.md-sidebar__scrollwrap { + transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); + transform: translateY(0); +} + +/* When scrolled, move the scrollwrap up */ +.scrolled .md-sidebar__scrollwrap { + transform: translateY(-5.5em); +} + +/* Match header.css media queries */ +@media screen and (min-width: 2000px) { + .scrolled .md-sidebar__scrollwrap { + transform: translateY(-6em); + } +} + +@media (max-width: 1200px) { + .scrolled .md-sidebar__scrollwrap { + transform: translateY(-3em); + } +} + +@media (max-width: 860px) { + .scrolled .md-sidebar__scrollwrap { + transform: translateY(-2.5em); + } +} \ No newline at end of file diff --git a/assets/css/projects/vnp.css b/assets/css/projects/vnp.css new file mode 100644 index 00000000..fba5ee59 --- /dev/null +++ b/assets/css/projects/vnp.css @@ -0,0 +1,17 @@ +:root { + --lightdisplaystyle: inline; + --darkdisplaystyle: none; +} + +[data-md-color-scheme="slate"] { + --lightdisplaystyle: none; + --darkdisplaystyle: inline; +} + +.lighttweet { + display: var(--lightdisplaystyle); +} + +.darktweet { + display: var(--darkdisplaystyle); +} \ No newline at end of file diff --git a/assets/images/8-bitComputer/555wave.jpg b/assets/images/8-bitComputer/555wave.jpg new file mode 100644 index 00000000..28e16e3c Binary files /dev/null and b/assets/images/8-bitComputer/555wave.jpg differ diff --git a/assets/images/8-bitComputer/8-BitRegister.jpg b/assets/images/8-bitComputer/8-BitRegister.jpg new file mode 100644 index 00000000..71bdb79b Binary files /dev/null and b/assets/images/8-bitComputer/8-BitRegister.jpg differ diff --git a/assets/images/8-bitComputer/FibonacciEquasion.png b/assets/images/8-bitComputer/FibonacciEquasion.png new file mode 100644 index 00000000..57395b22 Binary files /dev/null and b/assets/images/8-bitComputer/FibonacciEquasion.png differ diff --git a/assets/images/8-bitComputer/Fibonaccisequencespiral.png b/assets/images/8-bitComputer/Fibonaccisequencespiral.png new file mode 100644 index 00000000..396634d8 Binary files /dev/null and b/assets/images/8-bitComputer/Fibonaccisequencespiral.png differ diff --git a/assets/images/8-bitComputer/clockmodule.jpg b/assets/images/8-bitComputer/clockmodule.jpg new file mode 100644 index 00000000..29ad3ab1 Binary files /dev/null and b/assets/images/8-bitComputer/clockmodule.jpg differ diff --git a/assets/images/8-bitComputer/monostable.jpg b/assets/images/8-bitComputer/monostable.jpg new file mode 100644 index 00000000..bfe28526 Binary files /dev/null and b/assets/images/8-bitComputer/monostable.jpg differ diff --git a/assets/images/8-bitComputer/setastable.jpg b/assets/images/8-bitComputer/setastable.jpg new file mode 100644 index 00000000..e59474e0 Binary files /dev/null and b/assets/images/8-bitComputer/setastable.jpg differ diff --git a/assets/images/8-bitComputer/variableastable.jpg b/assets/images/8-bitComputer/variableastable.jpg new file mode 100644 index 00000000..17de996e Binary files /dev/null and b/assets/images/8-bitComputer/variableastable.jpg differ diff --git a/assets/images/AdaptableAquaponics/AutomatedModelingToolbar.jpg b/assets/images/AdaptableAquaponics/AutomatedModelingToolbar.jpg new file mode 100644 index 00000000..ba299ce8 Binary files /dev/null and b/assets/images/AdaptableAquaponics/AutomatedModelingToolbar.jpg differ diff --git a/assets/images/AdaptableAquaponics/Render1.png b/assets/images/AdaptableAquaponics/Render1.png new file mode 100644 index 00000000..806557ed Binary files /dev/null and b/assets/images/AdaptableAquaponics/Render1.png differ diff --git a/assets/images/AdaptableAquaponics/RenderWorkspace.jpg b/assets/images/AdaptableAquaponics/RenderWorkspace.jpg new file mode 100644 index 00000000..ee60cd15 Binary files /dev/null and b/assets/images/AdaptableAquaponics/RenderWorkspace.jpg differ diff --git a/assets/images/AdaptableAquaponics/Sticker!.jpg b/assets/images/AdaptableAquaponics/Sticker!.jpg new file mode 100644 index 00000000..87db99f0 Binary files /dev/null and b/assets/images/AdaptableAquaponics/Sticker!.jpg differ diff --git a/assets/images/AdaptableAquaponics/assembledmodel.jpg b/assets/images/AdaptableAquaponics/assembledmodel.jpg new file mode 100644 index 00000000..0fab3f9f Binary files /dev/null and b/assets/images/AdaptableAquaponics/assembledmodel.jpg differ diff --git a/assets/images/AdaptableAquaponics/assembledshelf.jpg b/assets/images/AdaptableAquaponics/assembledshelf.jpg new file mode 100644 index 00000000..970bd2c1 Binary files /dev/null and b/assets/images/AdaptableAquaponics/assembledshelf.jpg differ diff --git a/assets/images/AdaptableAquaponics/assembledshippingcont.jpg b/assets/images/AdaptableAquaponics/assembledshippingcont.jpg new file mode 100644 index 00000000..9bcb1441 Binary files /dev/null and b/assets/images/AdaptableAquaponics/assembledshippingcont.jpg differ diff --git a/assets/images/AdaptableAquaponics/assembledsolar.jpg b/assets/images/AdaptableAquaponics/assembledsolar.jpg new file mode 100644 index 00000000..fc556215 Binary files /dev/null and b/assets/images/AdaptableAquaponics/assembledsolar.jpg differ diff --git a/assets/images/AdaptableAquaponics/automateddesign.jpg b/assets/images/AdaptableAquaponics/automateddesign.jpg new file mode 100644 index 00000000..efad5c1b Binary files /dev/null and b/assets/images/AdaptableAquaponics/automateddesign.jpg differ diff --git a/assets/images/AdaptableAquaponics/cover.jpg b/assets/images/AdaptableAquaponics/cover.jpg new file mode 100644 index 00000000..8e3d6e24 Binary files /dev/null and b/assets/images/AdaptableAquaponics/cover.jpg differ diff --git a/assets/images/AdaptableAquaponics/glory1.jpg b/assets/images/AdaptableAquaponics/glory1.jpg new file mode 100644 index 00000000..81355f93 Binary files /dev/null and b/assets/images/AdaptableAquaponics/glory1.jpg differ diff --git a/assets/images/AdaptableAquaponics/glory2.jpg b/assets/images/AdaptableAquaponics/glory2.jpg new file mode 100644 index 00000000..72d1b188 Binary files /dev/null and b/assets/images/AdaptableAquaponics/glory2.jpg differ diff --git a/assets/images/AdaptableAquaponics/glory3.jpg b/assets/images/AdaptableAquaponics/glory3.jpg new file mode 100644 index 00000000..4787785e Binary files /dev/null and b/assets/images/AdaptableAquaponics/glory3.jpg differ diff --git a/assets/images/AdaptableAquaponics/hdirbg.jpg b/assets/images/AdaptableAquaponics/hdirbg.jpg new file mode 100644 index 00000000..89745838 Binary files /dev/null and b/assets/images/AdaptableAquaponics/hdirbg.jpg differ diff --git a/assets/images/AdaptableAquaponics/header.jpg b/assets/images/AdaptableAquaponics/header.jpg new file mode 100644 index 00000000..a5c48843 Binary files /dev/null and b/assets/images/AdaptableAquaponics/header.jpg differ diff --git a/assets/images/AdaptableAquaponics/hero.png b/assets/images/AdaptableAquaponics/hero.png new file mode 100644 index 00000000..413cbabc Binary files /dev/null and b/assets/images/AdaptableAquaponics/hero.png differ diff --git a/assets/images/AdaptableAquaponics/installedshelf.jpg b/assets/images/AdaptableAquaponics/installedshelf.jpg new file mode 100644 index 00000000..d9a23132 Binary files /dev/null and b/assets/images/AdaptableAquaponics/installedshelf.jpg differ diff --git a/assets/images/AdaptableAquaponics/modelinfucion.jpg b/assets/images/AdaptableAquaponics/modelinfucion.jpg new file mode 100644 index 00000000..f073403d Binary files /dev/null and b/assets/images/AdaptableAquaponics/modelinfucion.jpg differ diff --git a/assets/images/AdaptableAquaponics/note1.png b/assets/images/AdaptableAquaponics/note1.png new file mode 100644 index 00000000..6de2239b Binary files /dev/null and b/assets/images/AdaptableAquaponics/note1.png differ diff --git a/assets/images/AdaptableAquaponics/note2.png b/assets/images/AdaptableAquaponics/note2.png new file mode 100644 index 00000000..1e7c61c3 Binary files /dev/null and b/assets/images/AdaptableAquaponics/note2.png differ diff --git a/assets/images/AdaptableAquaponics/post-processedprint.jpg b/assets/images/AdaptableAquaponics/post-processedprint.jpg new file mode 100644 index 00000000..cd475332 Binary files /dev/null and b/assets/images/AdaptableAquaponics/post-processedprint.jpg differ diff --git a/assets/images/AdaptableAquaponics/preferences.jpg b/assets/images/AdaptableAquaponics/preferences.jpg new file mode 100644 index 00000000..6e1d4793 Binary files /dev/null and b/assets/images/AdaptableAquaponics/preferences.jpg differ diff --git a/assets/images/AdaptableAquaponics/print1.jpg b/assets/images/AdaptableAquaponics/print1.jpg new file mode 100644 index 00000000..4f169a84 Binary files /dev/null and b/assets/images/AdaptableAquaponics/print1.jpg differ diff --git a/assets/images/AdaptableAquaponics/print2.jpg b/assets/images/AdaptableAquaponics/print2.jpg new file mode 100644 index 00000000..9e5f008c Binary files /dev/null and b/assets/images/AdaptableAquaponics/print2.jpg differ diff --git a/assets/images/AdaptableAquaponics/print3.jpg b/assets/images/AdaptableAquaponics/print3.jpg new file mode 100644 index 00000000..4fd3f726 Binary files /dev/null and b/assets/images/AdaptableAquaponics/print3.jpg differ diff --git a/assets/images/AdaptableAquaponics/prusaslicer.jpg b/assets/images/AdaptableAquaponics/prusaslicer.jpg new file mode 100644 index 00000000..e17f4d48 Binary files /dev/null and b/assets/images/AdaptableAquaponics/prusaslicer.jpg differ diff --git a/assets/images/AdaptableAquaponics/render2.png b/assets/images/AdaptableAquaponics/render2.png new file mode 100644 index 00000000..9d88c08f Binary files /dev/null and b/assets/images/AdaptableAquaponics/render2.png differ diff --git a/assets/images/AdaptableAquaponics/render3.png b/assets/images/AdaptableAquaponics/render3.png new file mode 100644 index 00000000..d5a91153 Binary files /dev/null and b/assets/images/AdaptableAquaponics/render3.png differ diff --git a/assets/images/AdaptableAquaponics/rendertoolbar.jpg b/assets/images/AdaptableAquaponics/rendertoolbar.jpg new file mode 100644 index 00000000..e07b5c24 Binary files /dev/null and b/assets/images/AdaptableAquaponics/rendertoolbar.jpg differ diff --git a/assets/images/AdaptableAquaponics/scholarship.jpg b/assets/images/AdaptableAquaponics/scholarship.jpg new file mode 100644 index 00000000..67ffd02d Binary files /dev/null and b/assets/images/AdaptableAquaponics/scholarship.jpg differ diff --git a/assets/images/AdaptableAquaponics/solarparts.jpg b/assets/images/AdaptableAquaponics/solarparts.jpg new file mode 100644 index 00000000..2c550ac9 Binary files /dev/null and b/assets/images/AdaptableAquaponics/solarparts.jpg differ diff --git a/assets/images/AdaptableAquaponics/spatialplumbing.jpg b/assets/images/AdaptableAquaponics/spatialplumbing.jpg new file mode 100644 index 00000000..589581ee Binary files /dev/null and b/assets/images/AdaptableAquaponics/spatialplumbing.jpg differ diff --git a/assets/images/AssistiveAquaponics/4permiterslicing.jpg b/assets/images/AssistiveAquaponics/4permiterslicing.jpg new file mode 100644 index 00000000..0ac642a6 Binary files /dev/null and b/assets/images/AssistiveAquaponics/4permiterslicing.jpg differ diff --git a/assets/images/AssistiveAquaponics/AquaponicsCircle.png b/assets/images/AssistiveAquaponics/AquaponicsCircle.png new file mode 100644 index 00000000..25931f2f Binary files /dev/null and b/assets/images/AssistiveAquaponics/AquaponicsCircle.png differ diff --git a/assets/images/AssistiveAquaponics/AquaponicsSystemDiagram.png b/assets/images/AssistiveAquaponics/AquaponicsSystemDiagram.png new file mode 100644 index 00000000..81c649e9 Binary files /dev/null and b/assets/images/AssistiveAquaponics/AquaponicsSystemDiagram.png differ diff --git a/assets/images/AssistiveAquaponics/Aspire.png b/assets/images/AssistiveAquaponics/Aspire.png new file mode 100644 index 00000000..5fc337c4 Binary files /dev/null and b/assets/images/AssistiveAquaponics/Aspire.png differ diff --git a/assets/images/AssistiveAquaponics/BoardMountSide.jpg b/assets/images/AssistiveAquaponics/BoardMountSide.jpg new file mode 100644 index 00000000..34cf7127 Binary files /dev/null and b/assets/images/AssistiveAquaponics/BoardMountSide.jpg differ diff --git a/assets/images/AssistiveAquaponics/BoardMountTop.jpg b/assets/images/AssistiveAquaponics/BoardMountTop.jpg new file mode 100644 index 00000000..3efd0df6 Binary files /dev/null and b/assets/images/AssistiveAquaponics/BoardMountTop.jpg differ diff --git a/assets/images/AssistiveAquaponics/CableClip.jpg b/assets/images/AssistiveAquaponics/CableClip.jpg new file mode 100644 index 00000000..212aa404 Binary files /dev/null and b/assets/images/AssistiveAquaponics/CableClip.jpg differ diff --git a/assets/images/AssistiveAquaponics/Error.png b/assets/images/AssistiveAquaponics/Error.png new file mode 100644 index 00000000..1367f63f Binary files /dev/null and b/assets/images/AssistiveAquaponics/Error.png differ diff --git a/assets/images/AssistiveAquaponics/Final1.jpg b/assets/images/AssistiveAquaponics/Final1.jpg new file mode 100644 index 00000000..a589ed28 Binary files /dev/null and b/assets/images/AssistiveAquaponics/Final1.jpg differ diff --git a/assets/images/AssistiveAquaponics/Final2.jpg b/assets/images/AssistiveAquaponics/Final2.jpg new file mode 100644 index 00000000..651b0518 Binary files /dev/null and b/assets/images/AssistiveAquaponics/Final2.jpg differ diff --git a/assets/images/AssistiveAquaponics/FishBoard.jpg b/assets/images/AssistiveAquaponics/FishBoard.jpg new file mode 100644 index 00000000..495ec12b Binary files /dev/null and b/assets/images/AssistiveAquaponics/FishBoard.jpg differ diff --git a/assets/images/AssistiveAquaponics/FishBowlBoardv4.png b/assets/images/AssistiveAquaponics/FishBowlBoardv4.png new file mode 100644 index 00000000..b600aaa4 Binary files /dev/null and b/assets/images/AssistiveAquaponics/FishBowlBoardv4.png differ diff --git a/assets/images/AssistiveAquaponics/FishBowlSchematicv4.png b/assets/images/AssistiveAquaponics/FishBowlSchematicv4.png new file mode 100644 index 00000000..63d15edd Binary files /dev/null and b/assets/images/AssistiveAquaponics/FishBowlSchematicv4.png differ diff --git a/assets/images/AssistiveAquaponics/FishbowlV1.png b/assets/images/AssistiveAquaponics/FishbowlV1.png new file mode 100644 index 00000000..36702045 Binary files /dev/null and b/assets/images/AssistiveAquaponics/FishbowlV1.png differ diff --git a/assets/images/AssistiveAquaponics/Fishbowlblinlk.png b/assets/images/AssistiveAquaponics/Fishbowlblinlk.png new file mode 100644 index 00000000..899246c6 Binary files /dev/null and b/assets/images/AssistiveAquaponics/Fishbowlblinlk.png differ diff --git a/assets/images/AssistiveAquaponics/Fishbowlboard.png b/assets/images/AssistiveAquaponics/Fishbowlboard.png new file mode 100644 index 00000000..be29a6b9 Binary files /dev/null and b/assets/images/AssistiveAquaponics/Fishbowlboard.png differ diff --git a/assets/images/AssistiveAquaponics/HoleLayout.jpg b/assets/images/AssistiveAquaponics/HoleLayout.jpg new file mode 100644 index 00000000..8a82b0e3 Binary files /dev/null and b/assets/images/AssistiveAquaponics/HoleLayout.jpg differ diff --git a/assets/images/AssistiveAquaponics/InkscapeBowl.png b/assets/images/AssistiveAquaponics/InkscapeBowl.png new file mode 100644 index 00000000..7f84ad30 Binary files /dev/null and b/assets/images/AssistiveAquaponics/InkscapeBowl.png differ diff --git a/assets/images/AssistiveAquaponics/InkscapeFish.png b/assets/images/AssistiveAquaponics/InkscapeFish.png new file mode 100644 index 00000000..9c440454 Binary files /dev/null and b/assets/images/AssistiveAquaponics/InkscapeFish.png differ diff --git a/assets/images/AssistiveAquaponics/InkscapeTW.png b/assets/images/AssistiveAquaponics/InkscapeTW.png new file mode 100644 index 00000000..5f1d7a44 Binary files /dev/null and b/assets/images/AssistiveAquaponics/InkscapeTW.png differ diff --git a/assets/images/AssistiveAquaponics/LCDSchematic2.png b/assets/images/AssistiveAquaponics/LCDSchematic2.png new file mode 100644 index 00000000..ae7507e2 Binary files /dev/null and b/assets/images/AssistiveAquaponics/LCDSchematic2.png differ diff --git a/assets/images/AssistiveAquaponics/LCDboard2.png b/assets/images/AssistiveAquaponics/LCDboard2.png new file mode 100644 index 00000000..cc31db86 Binary files /dev/null and b/assets/images/AssistiveAquaponics/LCDboard2.png differ diff --git a/assets/images/AssistiveAquaponics/LCDcode.png b/assets/images/AssistiveAquaponics/LCDcode.png new file mode 100644 index 00000000..c7dca884 Binary files /dev/null and b/assets/images/AssistiveAquaponics/LCDcode.png differ diff --git a/assets/images/AssistiveAquaponics/LCDhole.jpg b/assets/images/AssistiveAquaponics/LCDhole.jpg new file mode 100644 index 00000000..35b2c273 Binary files /dev/null and b/assets/images/AssistiveAquaponics/LCDhole.jpg differ diff --git a/assets/images/AssistiveAquaponics/PHoperatingprincipal.png b/assets/images/AssistiveAquaponics/PHoperatingprincipal.png new file mode 100644 index 00000000..38635453 Binary files /dev/null and b/assets/images/AssistiveAquaponics/PHoperatingprincipal.png differ diff --git a/assets/images/AssistiveAquaponics/PHtoV.jpg b/assets/images/AssistiveAquaponics/PHtoV.jpg new file mode 100644 index 00000000..2db6da09 Binary files /dev/null and b/assets/images/AssistiveAquaponics/PHtoV.jpg differ diff --git a/assets/images/AssistiveAquaponics/PSUMounts.jpg b/assets/images/AssistiveAquaponics/PSUMounts.jpg new file mode 100644 index 00000000..9bb2b669 Binary files /dev/null and b/assets/images/AssistiveAquaponics/PSUMounts.jpg differ diff --git a/assets/images/AssistiveAquaponics/PSUcover.jpg b/assets/images/AssistiveAquaponics/PSUcover.jpg new file mode 100644 index 00000000..c550ef29 Binary files /dev/null and b/assets/images/AssistiveAquaponics/PSUcover.jpg differ diff --git a/assets/images/AssistiveAquaponics/PSUonWood.jpg b/assets/images/AssistiveAquaponics/PSUonWood.jpg new file mode 100644 index 00000000..a43bb84b Binary files /dev/null and b/assets/images/AssistiveAquaponics/PSUonWood.jpg differ diff --git a/assets/images/AssistiveAquaponics/PSUwMoutns.jpg b/assets/images/AssistiveAquaponics/PSUwMoutns.jpg new file mode 100644 index 00000000..f798eb2d Binary files /dev/null and b/assets/images/AssistiveAquaponics/PSUwMoutns.jpg differ diff --git a/assets/images/AssistiveAquaponics/PSUwireing.jpg b/assets/images/AssistiveAquaponics/PSUwireing.jpg new file mode 100644 index 00000000..83ab138c Binary files /dev/null and b/assets/images/AssistiveAquaponics/PSUwireing.jpg differ diff --git a/assets/images/AssistiveAquaponics/PhCalabration.jpg b/assets/images/AssistiveAquaponics/PhCalabration.jpg new file mode 100644 index 00000000..ef18588f Binary files /dev/null and b/assets/images/AssistiveAquaponics/PhCalabration.jpg differ diff --git a/assets/images/AssistiveAquaponics/ProjectedSketches.jpg b/assets/images/AssistiveAquaponics/ProjectedSketches.jpg new file mode 100644 index 00000000..b778144c Binary files /dev/null and b/assets/images/AssistiveAquaponics/ProjectedSketches.jpg differ diff --git a/assets/images/AssistiveAquaponics/Schem1.png b/assets/images/AssistiveAquaponics/Schem1.png new file mode 100644 index 00000000..b52520a7 Binary files /dev/null and b/assets/images/AssistiveAquaponics/Schem1.png differ diff --git a/assets/images/AssistiveAquaponics/Siliconetank.jpg b/assets/images/AssistiveAquaponics/Siliconetank.jpg new file mode 100644 index 00000000..d4b6eab5 Binary files /dev/null and b/assets/images/AssistiveAquaponics/Siliconetank.jpg differ diff --git a/assets/images/AssistiveAquaponics/Sketch1.png b/assets/images/AssistiveAquaponics/Sketch1.png new file mode 100644 index 00000000..a613af2f Binary files /dev/null and b/assets/images/AssistiveAquaponics/Sketch1.png differ diff --git a/assets/images/AssistiveAquaponics/Sketch2.png b/assets/images/AssistiveAquaponics/Sketch2.png new file mode 100644 index 00000000..60220eef Binary files /dev/null and b/assets/images/AssistiveAquaponics/Sketch2.png differ diff --git a/assets/images/AssistiveAquaponics/TankInkscape.jpg b/assets/images/AssistiveAquaponics/TankInkscape.jpg new file mode 100644 index 00000000..33c4c891 Binary files /dev/null and b/assets/images/AssistiveAquaponics/TankInkscape.jpg differ diff --git a/assets/images/AssistiveAquaponics/UpdatedElectronicsCoverInkscape.jpg b/assets/images/AssistiveAquaponics/UpdatedElectronicsCoverInkscape.jpg new file mode 100644 index 00000000..9fbf3fc4 Binary files /dev/null and b/assets/images/AssistiveAquaponics/UpdatedElectronicsCoverInkscape.jpg differ diff --git a/assets/images/AssistiveAquaponics/UsingShopbot.jpg b/assets/images/AssistiveAquaponics/UsingShopbot.jpg new file mode 100644 index 00000000..54a28bdd Binary files /dev/null and b/assets/images/AssistiveAquaponics/UsingShopbot.jpg differ diff --git a/assets/images/AssistiveAquaponics/WorkingTempHumidity.jpg b/assets/images/AssistiveAquaponics/WorkingTempHumidity.jpg new file mode 100644 index 00000000..e8196db4 Binary files /dev/null and b/assets/images/AssistiveAquaponics/WorkingTempHumidity.jpg differ diff --git a/assets/images/AssistiveAquaponics/allboards.jpg b/assets/images/AssistiveAquaponics/allboards.jpg new file mode 100644 index 00000000..e5a36b12 Binary files /dev/null and b/assets/images/AssistiveAquaponics/allboards.jpg differ diff --git a/assets/images/AssistiveAquaponics/backgrommet.jpg b/assets/images/AssistiveAquaponics/backgrommet.jpg new file mode 100644 index 00000000..1a59d3f6 Binary files /dev/null and b/assets/images/AssistiveAquaponics/backgrommet.jpg differ diff --git a/assets/images/AssistiveAquaponics/blinkybowl.jpg b/assets/images/AssistiveAquaponics/blinkybowl.jpg new file mode 100644 index 00000000..f5e76957 Binary files /dev/null and b/assets/images/AssistiveAquaponics/blinkybowl.jpg differ diff --git a/assets/images/AssistiveAquaponics/bootloadingchips.jpg b/assets/images/AssistiveAquaponics/bootloadingchips.jpg new file mode 100644 index 00000000..798d9ae5 Binary files /dev/null and b/assets/images/AssistiveAquaponics/bootloadingchips.jpg differ diff --git a/assets/images/AssistiveAquaponics/bootscreen.jpg b/assets/images/AssistiveAquaponics/bootscreen.jpg new file mode 100644 index 00000000..a9a333b1 Binary files /dev/null and b/assets/images/AssistiveAquaponics/bootscreen.jpg differ diff --git a/assets/images/AssistiveAquaponics/cableclipmounted.jpg b/assets/images/AssistiveAquaponics/cableclipmounted.jpg new file mode 100644 index 00000000..4e0b8857 Binary files /dev/null and b/assets/images/AssistiveAquaponics/cableclipmounted.jpg differ diff --git a/assets/images/AssistiveAquaponics/clampedtankglueup.jpg b/assets/images/AssistiveAquaponics/clampedtankglueup.jpg new file mode 100644 index 00000000..ed0ac9eb Binary files /dev/null and b/assets/images/AssistiveAquaponics/clampedtankglueup.jpg differ diff --git a/assets/images/AssistiveAquaponics/coverinstalledv2.jpg b/assets/images/AssistiveAquaponics/coverinstalledv2.jpg new file mode 100644 index 00000000..596adf47 Binary files /dev/null and b/assets/images/AssistiveAquaponics/coverinstalledv2.jpg differ diff --git a/assets/images/AssistiveAquaponics/datascreentest.jpg b/assets/images/AssistiveAquaponics/datascreentest.jpg new file mode 100644 index 00000000..36bda91a Binary files /dev/null and b/assets/images/AssistiveAquaponics/datascreentest.jpg differ diff --git a/assets/images/AssistiveAquaponics/dht11in.png b/assets/images/AssistiveAquaponics/dht11in.png new file mode 100644 index 00000000..dca8ce75 Binary files /dev/null and b/assets/images/AssistiveAquaponics/dht11in.png differ diff --git a/assets/images/AssistiveAquaponics/finalcablerouting.jpg b/assets/images/AssistiveAquaponics/finalcablerouting.jpg new file mode 100644 index 00000000..f185ecdf Binary files /dev/null and b/assets/images/AssistiveAquaponics/finalcablerouting.jpg differ diff --git a/assets/images/AssistiveAquaponics/finalpowerbreakout.jpg b/assets/images/AssistiveAquaponics/finalpowerbreakout.jpg new file mode 100644 index 00000000..9210cf69 Binary files /dev/null and b/assets/images/AssistiveAquaponics/finalpowerbreakout.jpg differ diff --git a/assets/images/AssistiveAquaponics/fisahbowl.png b/assets/images/AssistiveAquaponics/fisahbowl.png new file mode 100644 index 00000000..fe0af755 Binary files /dev/null and b/assets/images/AssistiveAquaponics/fisahbowl.png differ diff --git a/assets/images/AssistiveAquaponics/fishboardsoftwareserial.jpg b/assets/images/AssistiveAquaponics/fishboardsoftwareserial.jpg new file mode 100644 index 00000000..99ae8fe0 Binary files /dev/null and b/assets/images/AssistiveAquaponics/fishboardsoftwareserial.jpg differ diff --git a/assets/images/AssistiveAquaponics/fishbowl1.jpg b/assets/images/AssistiveAquaponics/fishbowl1.jpg new file mode 100644 index 00000000..238d6250 Binary files /dev/null and b/assets/images/AssistiveAquaponics/fishbowl1.jpg differ diff --git a/assets/images/AssistiveAquaponics/fishbowlboard2.png b/assets/images/AssistiveAquaponics/fishbowlboard2.png new file mode 100644 index 00000000..9d555143 Binary files /dev/null and b/assets/images/AssistiveAquaponics/fishbowlboard2.png differ diff --git a/assets/images/AssistiveAquaponics/fishbowlschematic.png b/assets/images/AssistiveAquaponics/fishbowlschematic.png new file mode 100644 index 00000000..a2031a6a Binary files /dev/null and b/assets/images/AssistiveAquaponics/fishbowlschematic.png differ diff --git a/assets/images/AssistiveAquaponics/fishschematic.png b/assets/images/AssistiveAquaponics/fishschematic.png new file mode 100644 index 00000000..95d2c403 Binary files /dev/null and b/assets/images/AssistiveAquaponics/fishschematic.png differ diff --git a/assets/images/AssistiveAquaponics/frontplateinstalled.jpg b/assets/images/AssistiveAquaponics/frontplateinstalled.jpg new file mode 100644 index 00000000..1aa368f8 Binary files /dev/null and b/assets/images/AssistiveAquaponics/frontplateinstalled.jpg differ diff --git a/assets/images/AssistiveAquaponics/frontplatelaser.jpg b/assets/images/AssistiveAquaponics/frontplatelaser.jpg new file mode 100644 index 00000000..47800d0b Binary files /dev/null and b/assets/images/AssistiveAquaponics/frontplatelaser.jpg differ diff --git a/assets/images/AssistiveAquaponics/generator.png b/assets/images/AssistiveAquaponics/generator.png new file mode 100644 index 00000000..061b8b33 Binary files /dev/null and b/assets/images/AssistiveAquaponics/generator.png differ diff --git a/assets/images/AssistiveAquaponics/growmedium.jpg b/assets/images/AssistiveAquaponics/growmedium.jpg new file mode 100644 index 00000000..00585f7d Binary files /dev/null and b/assets/images/AssistiveAquaponics/growmedium.jpg differ diff --git a/assets/images/AssistiveAquaponics/hero.png b/assets/images/AssistiveAquaponics/hero.png new file mode 100644 index 00000000..275817b6 Binary files /dev/null and b/assets/images/AssistiveAquaponics/hero.png differ diff --git a/assets/images/AssistiveAquaponics/installedbackgrommet.jpg b/assets/images/AssistiveAquaponics/installedbackgrommet.jpg new file mode 100644 index 00000000..1eb0de3f Binary files /dev/null and b/assets/images/AssistiveAquaponics/installedbackgrommet.jpg differ diff --git a/assets/images/AssistiveAquaponics/installedtankback.jpg b/assets/images/AssistiveAquaponics/installedtankback.jpg new file mode 100644 index 00000000..6a1fbc93 Binary files /dev/null and b/assets/images/AssistiveAquaponics/installedtankback.jpg differ diff --git a/assets/images/AssistiveAquaponics/lcdboardsoldered1.jpg b/assets/images/AssistiveAquaponics/lcdboardsoldered1.jpg new file mode 100644 index 00000000..dcd99643 Binary files /dev/null and b/assets/images/AssistiveAquaponics/lcdboardsoldered1.jpg differ diff --git a/assets/images/AssistiveAquaponics/lcdboardsoldered2.jpg b/assets/images/AssistiveAquaponics/lcdboardsoldered2.jpg new file mode 100644 index 00000000..767a3d71 Binary files /dev/null and b/assets/images/AssistiveAquaponics/lcdboardsoldered2.jpg differ diff --git a/assets/images/AssistiveAquaponics/leakytank.jpg b/assets/images/AssistiveAquaponics/leakytank.jpg new file mode 100644 index 00000000..8ea45706 Binary files /dev/null and b/assets/images/AssistiveAquaponics/leakytank.jpg differ diff --git a/assets/images/AssistiveAquaponics/lightbar.jpg b/assets/images/AssistiveAquaponics/lightbar.jpg new file mode 100644 index 00000000..ee1b3a47 Binary files /dev/null and b/assets/images/AssistiveAquaponics/lightbar.jpg differ diff --git a/assets/images/AssistiveAquaponics/milledfish.jpg b/assets/images/AssistiveAquaponics/milledfish.jpg new file mode 100644 index 00000000..c4a9c383 Binary files /dev/null and b/assets/images/AssistiveAquaponics/milledfish.jpg differ diff --git a/assets/images/AssistiveAquaponics/mountinginprogress.jpg b/assets/images/AssistiveAquaponics/mountinginprogress.jpg new file mode 100644 index 00000000..6b532604 Binary files /dev/null and b/assets/images/AssistiveAquaponics/mountinginprogress.jpg differ diff --git a/assets/images/AssistiveAquaponics/pHboard.jpg b/assets/images/AssistiveAquaponics/pHboard.jpg new file mode 100644 index 00000000..a81d1970 Binary files /dev/null and b/assets/images/AssistiveAquaponics/pHboard.jpg differ diff --git a/assets/images/AssistiveAquaponics/pHboardfile.png b/assets/images/AssistiveAquaponics/pHboardfile.png new file mode 100644 index 00000000..8d3d82de Binary files /dev/null and b/assets/images/AssistiveAquaponics/pHboardfile.png differ diff --git a/assets/images/AssistiveAquaponics/pHboardmount.jpg b/assets/images/AssistiveAquaponics/pHboardmount.jpg new file mode 100644 index 00000000..f2b8d8e8 Binary files /dev/null and b/assets/images/AssistiveAquaponics/pHboardmount.jpg differ diff --git a/assets/images/AssistiveAquaponics/pHbracket.jpg b/assets/images/AssistiveAquaponics/pHbracket.jpg new file mode 100644 index 00000000..647888d9 Binary files /dev/null and b/assets/images/AssistiveAquaponics/pHbracket.jpg differ diff --git a/assets/images/AssistiveAquaponics/pHcalabrationtest.jpg b/assets/images/AssistiveAquaponics/pHcalabrationtest.jpg new file mode 100644 index 00000000..2bd93055 Binary files /dev/null and b/assets/images/AssistiveAquaponics/pHcalabrationtest.jpg differ diff --git a/assets/images/AssistiveAquaponics/pHmounted.jpg b/assets/images/AssistiveAquaponics/pHmounted.jpg new file mode 100644 index 00000000..1ffb4d15 Binary files /dev/null and b/assets/images/AssistiveAquaponics/pHmounted.jpg differ diff --git a/assets/images/AssistiveAquaponics/pHschem.png b/assets/images/AssistiveAquaponics/pHschem.png new file mode 100644 index 00000000..ef339ec5 Binary files /dev/null and b/assets/images/AssistiveAquaponics/pHschem.png differ diff --git a/assets/images/AssistiveAquaponics/ph-scale_orig.png b/assets/images/AssistiveAquaponics/ph-scale_orig.png new file mode 100644 index 00000000..28870f2d Binary files /dev/null and b/assets/images/AssistiveAquaponics/ph-scale_orig.png differ diff --git a/assets/images/AssistiveAquaponics/phin.png b/assets/images/AssistiveAquaponics/phin.png new file mode 100644 index 00000000..428e412f Binary files /dev/null and b/assets/images/AssistiveAquaponics/phin.png differ diff --git a/assets/images/AssistiveAquaponics/plantboxassembly.jpg b/assets/images/AssistiveAquaponics/plantboxassembly.jpg new file mode 100644 index 00000000..91233edd Binary files /dev/null and b/assets/images/AssistiveAquaponics/plantboxassembly.jpg differ diff --git a/assets/images/AssistiveAquaponics/plantboxback.jpg b/assets/images/AssistiveAquaponics/plantboxback.jpg new file mode 100644 index 00000000..83031b7f Binary files /dev/null and b/assets/images/AssistiveAquaponics/plantboxback.jpg differ diff --git a/assets/images/AssistiveAquaponics/plantboxbottom.jpg b/assets/images/AssistiveAquaponics/plantboxbottom.jpg new file mode 100644 index 00000000..5ebef68b Binary files /dev/null and b/assets/images/AssistiveAquaponics/plantboxbottom.jpg differ diff --git a/assets/images/AssistiveAquaponics/plantboxfront.jpg b/assets/images/AssistiveAquaponics/plantboxfront.jpg new file mode 100644 index 00000000..01f1493f Binary files /dev/null and b/assets/images/AssistiveAquaponics/plantboxfront.jpg differ diff --git a/assets/images/AssistiveAquaponics/plantboxgrowwires.jpg b/assets/images/AssistiveAquaponics/plantboxgrowwires.jpg new file mode 100644 index 00000000..3f9a9595 Binary files /dev/null and b/assets/images/AssistiveAquaponics/plantboxgrowwires.jpg differ diff --git a/assets/images/AssistiveAquaponics/ports.png b/assets/images/AssistiveAquaponics/ports.png new file mode 100644 index 00000000..c3ad9841 Binary files /dev/null and b/assets/images/AssistiveAquaponics/ports.png differ diff --git a/assets/images/AssistiveAquaponics/powerboardboardfinal.png b/assets/images/AssistiveAquaponics/powerboardboardfinal.png new file mode 100644 index 00000000..e116d4a8 Binary files /dev/null and b/assets/images/AssistiveAquaponics/powerboardboardfinal.png differ diff --git a/assets/images/AssistiveAquaponics/powerboardfinal.png b/assets/images/AssistiveAquaponics/powerboardfinal.png new file mode 100644 index 00000000..38207a5c Binary files /dev/null and b/assets/images/AssistiveAquaponics/powerboardfinal.png differ diff --git a/assets/images/AssistiveAquaponics/powerbreakout1.jpg b/assets/images/AssistiveAquaponics/powerbreakout1.jpg new file mode 100644 index 00000000..c1642e29 Binary files /dev/null and b/assets/images/AssistiveAquaponics/powerbreakout1.jpg differ diff --git a/assets/images/AssistiveAquaponics/powerbreakout2.jpg b/assets/images/AssistiveAquaponics/powerbreakout2.jpg new file mode 100644 index 00000000..5eed1e82 Binary files /dev/null and b/assets/images/AssistiveAquaponics/powerbreakout2.jpg differ diff --git a/assets/images/AssistiveAquaponics/powerbreakoutschemv2.png b/assets/images/AssistiveAquaponics/powerbreakoutschemv2.png new file mode 100644 index 00000000..c83b9d43 Binary files /dev/null and b/assets/images/AssistiveAquaponics/powerbreakoutschemv2.png differ diff --git a/assets/images/AssistiveAquaponics/powerbreakoutside.jpg b/assets/images/AssistiveAquaponics/powerbreakoutside.jpg new file mode 100644 index 00000000..e59bc117 Binary files /dev/null and b/assets/images/AssistiveAquaponics/powerbreakoutside.jpg differ diff --git a/assets/images/AssistiveAquaponics/powerbreakouttop.jpg b/assets/images/AssistiveAquaponics/powerbreakouttop.jpg new file mode 100644 index 00000000..f4b459bf Binary files /dev/null and b/assets/images/AssistiveAquaponics/powerbreakouttop.jpg differ diff --git a/assets/images/AssistiveAquaponics/powerbreakoutv2.png b/assets/images/AssistiveAquaponics/powerbreakoutv2.png new file mode 100644 index 00000000..6ab0648c Binary files /dev/null and b/assets/images/AssistiveAquaponics/powerbreakoutv2.png differ diff --git a/assets/images/AssistiveAquaponics/powerinput.jpg b/assets/images/AssistiveAquaponics/powerinput.jpg new file mode 100644 index 00000000..8ac66e2a Binary files /dev/null and b/assets/images/AssistiveAquaponics/powerinput.jpg differ diff --git a/assets/images/AssistiveAquaponics/powerinput2.jpg b/assets/images/AssistiveAquaponics/powerinput2.jpg new file mode 100644 index 00000000..cf8f1e51 Binary files /dev/null and b/assets/images/AssistiveAquaponics/powerinput2.jpg differ diff --git a/assets/images/AssistiveAquaponics/powerinputmountinghole.jpg b/assets/images/AssistiveAquaponics/powerinputmountinghole.jpg new file mode 100644 index 00000000..cbc9c853 Binary files /dev/null and b/assets/images/AssistiveAquaponics/powerinputmountinghole.jpg differ diff --git a/assets/images/AssistiveAquaponics/powerschemeagle.png b/assets/images/AssistiveAquaponics/powerschemeagle.png new file mode 100644 index 00000000..ac941a4e Binary files /dev/null and b/assets/images/AssistiveAquaponics/powerschemeagle.png differ diff --git a/assets/images/AssistiveAquaponics/powersupplytoAC.jpg b/assets/images/AssistiveAquaponics/powersupplytoAC.jpg new file mode 100644 index 00000000..27aca8b3 Binary files /dev/null and b/assets/images/AssistiveAquaponics/powersupplytoAC.jpg differ diff --git a/assets/images/AssistiveAquaponics/powrboardeagle.png b/assets/images/AssistiveAquaponics/powrboardeagle.png new file mode 100644 index 00000000..e52f4520 Binary files /dev/null and b/assets/images/AssistiveAquaponics/powrboardeagle.png differ diff --git a/assets/images/AssistiveAquaponics/presentation.png b/assets/images/AssistiveAquaponics/presentation.png new file mode 100644 index 00000000..a600e98d Binary files /dev/null and b/assets/images/AssistiveAquaponics/presentation.png differ diff --git a/assets/images/AssistiveAquaponics/pumpinstalled.jpg b/assets/images/AssistiveAquaponics/pumpinstalled.jpg new file mode 100644 index 00000000..1d4dd57c Binary files /dev/null and b/assets/images/AssistiveAquaponics/pumpinstalled.jpg differ diff --git a/assets/images/AssistiveAquaponics/routedbackcable.jpg b/assets/images/AssistiveAquaponics/routedbackcable.jpg new file mode 100644 index 00000000..419b3363 Binary files /dev/null and b/assets/images/AssistiveAquaponics/routedbackcable.jpg differ diff --git a/assets/images/AssistiveAquaponics/rubberducks.jpg b/assets/images/AssistiveAquaponics/rubberducks.jpg new file mode 100644 index 00000000..bae6ccbd Binary files /dev/null and b/assets/images/AssistiveAquaponics/rubberducks.jpg differ diff --git a/assets/images/AssistiveAquaponics/solderedfish.jpg b/assets/images/AssistiveAquaponics/solderedfish.jpg new file mode 100644 index 00000000..4ae88d0e Binary files /dev/null and b/assets/images/AssistiveAquaponics/solderedfish.jpg differ diff --git a/assets/images/AssistiveAquaponics/solderedfishbowl2.jpg b/assets/images/AssistiveAquaponics/solderedfishbowl2.jpg new file mode 100644 index 00000000..6efae3dd Binary files /dev/null and b/assets/images/AssistiveAquaponics/solderedfishbowl2.jpg differ diff --git a/assets/images/AssistiveAquaponics/successfulfileldtank.jpg b/assets/images/AssistiveAquaponics/successfulfileldtank.jpg new file mode 100644 index 00000000..f77f531f Binary files /dev/null and b/assets/images/AssistiveAquaponics/successfulfileldtank.jpg differ diff --git a/assets/images/AssistiveAquaponics/tankcutparts.jpg b/assets/images/AssistiveAquaponics/tankcutparts.jpg new file mode 100644 index 00000000..2fb4b5a6 Binary files /dev/null and b/assets/images/AssistiveAquaponics/tankcutparts.jpg differ diff --git a/assets/images/AssistiveAquaponics/tankhighlight.jpg b/assets/images/AssistiveAquaponics/tankhighlight.jpg new file mode 100644 index 00000000..ae09f7ea Binary files /dev/null and b/assets/images/AssistiveAquaponics/tankhighlight.jpg differ diff --git a/assets/images/AssistiveAquaponics/tapedtankgliueup.jpg b/assets/images/AssistiveAquaponics/tapedtankgliueup.jpg new file mode 100644 index 00000000..62be6787 Binary files /dev/null and b/assets/images/AssistiveAquaponics/tapedtankgliueup.jpg differ diff --git a/assets/images/AssistiveAquaponics/workingboardfile.png b/assets/images/AssistiveAquaponics/workingboardfile.png new file mode 100644 index 00000000..99414cbc Binary files /dev/null and b/assets/images/AssistiveAquaponics/workingboardfile.png differ diff --git a/assets/images/AssistiveAquaponics/workinglights.jpg b/assets/images/AssistiveAquaponics/workinglights.jpg new file mode 100644 index 00000000..a31a2830 Binary files /dev/null and b/assets/images/AssistiveAquaponics/workinglights.jpg differ diff --git a/assets/images/AssistiveAquaponics/workingmainschem.png b/assets/images/AssistiveAquaponics/workingmainschem.png new file mode 100644 index 00000000..e5e9c8c9 Binary files /dev/null and b/assets/images/AssistiveAquaponics/workingmainschem.png differ diff --git a/assets/images/AssistiveAquaponics/workingsolderedboard.jpg b/assets/images/AssistiveAquaponics/workingsolderedboard.jpg new file mode 100644 index 00000000..d01ca9e1 Binary files /dev/null and b/assets/images/AssistiveAquaponics/workingsolderedboard.jpg differ diff --git a/assets/images/AssistiveAquaponics/workingwunrouted.jpg b/assets/images/AssistiveAquaponics/workingwunrouted.jpg new file mode 100644 index 00000000..38f3a6ca Binary files /dev/null and b/assets/images/AssistiveAquaponics/workingwunrouted.jpg differ diff --git a/assets/images/CornerDesk/baseframe.jpg b/assets/images/CornerDesk/baseframe.jpg new file mode 100644 index 00000000..21639a12 Binary files /dev/null and b/assets/images/CornerDesk/baseframe.jpg differ diff --git a/assets/images/CornerDesk/cableclips.jpg b/assets/images/CornerDesk/cableclips.jpg new file mode 100644 index 00000000..df71f924 Binary files /dev/null and b/assets/images/CornerDesk/cableclips.jpg differ diff --git a/assets/images/CornerDesk/deskbeautyshot.jpg b/assets/images/CornerDesk/deskbeautyshot.jpg new file mode 100644 index 00000000..ef7803d2 Binary files /dev/null and b/assets/images/CornerDesk/deskbeautyshot.jpg differ diff --git a/assets/images/CornerDesk/desknotes1.jpg b/assets/images/CornerDesk/desknotes1.jpg new file mode 100644 index 00000000..24e59c0c Binary files /dev/null and b/assets/images/CornerDesk/desknotes1.jpg differ diff --git a/assets/images/CornerDesk/desknotes2.jpg b/assets/images/CornerDesk/desknotes2.jpg new file mode 100644 index 00000000..8ee4658b Binary files /dev/null and b/assets/images/CornerDesk/desknotes2.jpg differ diff --git a/assets/images/CornerDesk/finishedwoodassembly.jpg b/assets/images/CornerDesk/finishedwoodassembly.jpg new file mode 100644 index 00000000..a16de2a2 Binary files /dev/null and b/assets/images/CornerDesk/finishedwoodassembly.jpg differ diff --git a/assets/images/CornerDesk/framelayout.jpg b/assets/images/CornerDesk/framelayout.jpg new file mode 100644 index 00000000..31717e97 Binary files /dev/null and b/assets/images/CornerDesk/framelayout.jpg differ diff --git a/assets/images/CornerDesk/hero.png b/assets/images/CornerDesk/hero.png new file mode 100644 index 00000000..ce283352 Binary files /dev/null and b/assets/images/CornerDesk/hero.png differ diff --git a/assets/images/CornerDesk/kregjig.jpg b/assets/images/CornerDesk/kregjig.jpg new file mode 100644 index 00000000..68dddc44 Binary files /dev/null and b/assets/images/CornerDesk/kregjig.jpg differ diff --git a/assets/images/CornerDesk/laptoparm.jpg b/assets/images/CornerDesk/laptoparm.jpg new file mode 100644 index 00000000..fd7dd7dd Binary files /dev/null and b/assets/images/CornerDesk/laptoparm.jpg differ diff --git a/assets/images/CornerDesk/stainedtop.jpg b/assets/images/CornerDesk/stainedtop.jpg new file mode 100644 index 00000000..3731e2a2 Binary files /dev/null and b/assets/images/CornerDesk/stainedtop.jpg differ diff --git a/assets/images/KlipperEnder3/directdrive.jpg b/assets/images/KlipperEnder3/directdrive.jpg new file mode 100644 index 00000000..518f0699 Binary files /dev/null and b/assets/images/KlipperEnder3/directdrive.jpg differ diff --git a/assets/images/KlipperEnder3/electronicsenclouser.jpg b/assets/images/KlipperEnder3/electronicsenclouser.jpg new file mode 100644 index 00000000..5bc8e000 Binary files /dev/null and b/assets/images/KlipperEnder3/electronicsenclouser.jpg differ diff --git a/assets/images/KlipperEnder3/ender3cam.jpg b/assets/images/KlipperEnder3/ender3cam.jpg new file mode 100644 index 00000000..2bb0c9bb Binary files /dev/null and b/assets/images/KlipperEnder3/ender3cam.jpg differ diff --git a/assets/images/KlipperEnder3/fluiddconfig.png b/assets/images/KlipperEnder3/fluiddconfig.png new file mode 100644 index 00000000..2bedc59a Binary files /dev/null and b/assets/images/KlipperEnder3/fluiddconfig.png differ diff --git a/assets/images/KlipperEnder3/fluiddinterface.png b/assets/images/KlipperEnder3/fluiddinterface.png new file mode 100644 index 00000000..b613aaa8 Binary files /dev/null and b/assets/images/KlipperEnder3/fluiddinterface.png differ diff --git a/assets/images/KlipperEnder3/hero.png b/assets/images/KlipperEnder3/hero.png new file mode 100644 index 00000000..54dfe360 Binary files /dev/null and b/assets/images/KlipperEnder3/hero.png differ diff --git a/assets/images/KlipperEnder3/installedelebox.jpg b/assets/images/KlipperEnder3/installedelebox.jpg new file mode 100644 index 00000000..98297466 Binary files /dev/null and b/assets/images/KlipperEnder3/installedelebox.jpg differ diff --git a/assets/images/KlipperEnder3/wholeprinter.jpg b/assets/images/KlipperEnder3/wholeprinter.jpg new file mode 100644 index 00000000..4b8221af Binary files /dev/null and b/assets/images/KlipperEnder3/wholeprinter.jpg differ diff --git a/assets/images/LithophaneExperiments/MC.jpg b/assets/images/LithophaneExperiments/MC.jpg new file mode 100644 index 00000000..a6f04286 Binary files /dev/null and b/assets/images/LithophaneExperiments/MC.jpg differ diff --git a/assets/images/LithophaneExperiments/MClithophane.jpg b/assets/images/LithophaneExperiments/MClithophane.jpg new file mode 100644 index 00000000..a466fd10 Binary files /dev/null and b/assets/images/LithophaneExperiments/MClithophane.jpg differ diff --git a/assets/images/LithophaneExperiments/alex.jpg b/assets/images/LithophaneExperiments/alex.jpg new file mode 100644 index 00000000..19bb3269 Binary files /dev/null and b/assets/images/LithophaneExperiments/alex.jpg differ diff --git a/assets/images/LithophaneExperiments/alexlithophane.jpg b/assets/images/LithophaneExperiments/alexlithophane.jpg new file mode 100644 index 00000000..57eea88e Binary files /dev/null and b/assets/images/LithophaneExperiments/alexlithophane.jpg differ diff --git a/assets/images/LithophaneExperiments/fam.jpg b/assets/images/LithophaneExperiments/fam.jpg new file mode 100644 index 00000000..792e2f29 Binary files /dev/null and b/assets/images/LithophaneExperiments/fam.jpg differ diff --git a/assets/images/LithophaneExperiments/famlithophane.jpg b/assets/images/LithophaneExperiments/famlithophane.jpg new file mode 100644 index 00000000..98094d53 Binary files /dev/null and b/assets/images/LithophaneExperiments/famlithophane.jpg differ diff --git a/assets/images/LithophaneExperiments/friends.jpg b/assets/images/LithophaneExperiments/friends.jpg new file mode 100644 index 00000000..72750cb4 Binary files /dev/null and b/assets/images/LithophaneExperiments/friends.jpg differ diff --git a/assets/images/LithophaneExperiments/friendslithophane.jpg b/assets/images/LithophaneExperiments/friendslithophane.jpg new file mode 100644 index 00000000..6b7fe396 Binary files /dev/null and b/assets/images/LithophaneExperiments/friendslithophane.jpg differ diff --git a/assets/images/LithophaneExperiments/gerogiaokeef.jpg b/assets/images/LithophaneExperiments/gerogiaokeef.jpg new file mode 100644 index 00000000..207f0af7 Binary files /dev/null and b/assets/images/LithophaneExperiments/gerogiaokeef.jpg differ diff --git a/assets/images/LithophaneExperiments/gerogiaokeeflithophane.jpg b/assets/images/LithophaneExperiments/gerogiaokeeflithophane.jpg new file mode 100644 index 00000000..01b9ec58 Binary files /dev/null and b/assets/images/LithophaneExperiments/gerogiaokeeflithophane.jpg differ diff --git a/assets/images/LithophaneExperiments/hero.png b/assets/images/LithophaneExperiments/hero.png new file mode 100644 index 00000000..f071ab64 Binary files /dev/null and b/assets/images/LithophaneExperiments/hero.png differ diff --git a/assets/images/LithophaneExperiments/keithharring.jpg b/assets/images/LithophaneExperiments/keithharring.jpg new file mode 100644 index 00000000..5033525e Binary files /dev/null and b/assets/images/LithophaneExperiments/keithharring.jpg differ diff --git a/assets/images/LithophaneExperiments/keithharringlithophane.jpg b/assets/images/LithophaneExperiments/keithharringlithophane.jpg new file mode 100644 index 00000000..6b254d83 Binary files /dev/null and b/assets/images/LithophaneExperiments/keithharringlithophane.jpg differ diff --git a/assets/images/LithophaneExperiments/lithophanemakeo.jpg b/assets/images/LithophaneExperiments/lithophanemakeo.jpg new file mode 100644 index 00000000..f4efb018 Binary files /dev/null and b/assets/images/LithophaneExperiments/lithophanemakeo.jpg differ diff --git a/assets/images/LithophaneExperiments/maeko.jpg b/assets/images/LithophaneExperiments/maeko.jpg new file mode 100644 index 00000000..3a92ca37 Binary files /dev/null and b/assets/images/LithophaneExperiments/maeko.jpg differ diff --git a/assets/images/LithophaneExperiments/nina.jpg b/assets/images/LithophaneExperiments/nina.jpg new file mode 100644 index 00000000..ff58064c Binary files /dev/null and b/assets/images/LithophaneExperiments/nina.jpg differ diff --git a/assets/images/LithophaneExperiments/ninalithophane.jpg b/assets/images/LithophaneExperiments/ninalithophane.jpg new file mode 100644 index 00000000..866d2f07 Binary files /dev/null and b/assets/images/LithophaneExperiments/ninalithophane.jpg differ diff --git a/assets/images/LithophaneExperiments/painting.jpg b/assets/images/LithophaneExperiments/painting.jpg new file mode 100644 index 00000000..7d94e5f0 Binary files /dev/null and b/assets/images/LithophaneExperiments/painting.jpg differ diff --git a/assets/images/LithophaneExperiments/paintinglithophane.jpg b/assets/images/LithophaneExperiments/paintinglithophane.jpg new file mode 100644 index 00000000..8602b525 Binary files /dev/null and b/assets/images/LithophaneExperiments/paintinglithophane.jpg differ diff --git a/assets/images/LithophaneExperiments/zach.jpg b/assets/images/LithophaneExperiments/zach.jpg new file mode 100644 index 00000000..db414633 Binary files /dev/null and b/assets/images/LithophaneExperiments/zach.jpg differ diff --git a/assets/images/LithophaneExperiments/zachlithophane.jpg b/assets/images/LithophaneExperiments/zachlithophane.jpg new file mode 100644 index 00000000..d56ed442 Binary files /dev/null and b/assets/images/LithophaneExperiments/zachlithophane.jpg differ diff --git a/assets/images/MillingWorkflow/AspireBanner.png b/assets/images/MillingWorkflow/AspireBanner.png new file mode 100644 index 00000000..6cd65a01 Binary files /dev/null and b/assets/images/MillingWorkflow/AspireBanner.png differ diff --git a/assets/images/MillingWorkflow/BantamBanner.png b/assets/images/MillingWorkflow/BantamBanner.png new file mode 100644 index 00000000..c40c3919 Binary files /dev/null and b/assets/images/MillingWorkflow/BantamBanner.png differ diff --git a/assets/images/MillingWorkflow/EagleBanner.png b/assets/images/MillingWorkflow/EagleBanner.png new file mode 100644 index 00000000..a43e0b6d Binary files /dev/null and b/assets/images/MillingWorkflow/EagleBanner.png differ diff --git a/assets/images/MillingWorkflow/FusionBanner.jpg b/assets/images/MillingWorkflow/FusionBanner.jpg new file mode 100644 index 00000000..c2b8f5d7 Binary files /dev/null and b/assets/images/MillingWorkflow/FusionBanner.jpg differ diff --git a/assets/images/MillingWorkflow/MPCNC1.jpg b/assets/images/MillingWorkflow/MPCNC1.jpg new file mode 100644 index 00000000..5de6b5bf Binary files /dev/null and b/assets/images/MillingWorkflow/MPCNC1.jpg differ diff --git a/assets/images/MillingWorkflow/MPCNC2.jpg b/assets/images/MillingWorkflow/MPCNC2.jpg new file mode 100644 index 00000000..0e25f3b4 Binary files /dev/null and b/assets/images/MillingWorkflow/MPCNC2.jpg differ diff --git a/assets/images/MillingWorkflow/ShopbotBanner.png b/assets/images/MillingWorkflow/ShopbotBanner.png new file mode 100644 index 00000000..281ce07c Binary files /dev/null and b/assets/images/MillingWorkflow/ShopbotBanner.png differ diff --git a/assets/images/MillingWorkflow/Topographyselection.jpg b/assets/images/MillingWorkflow/Topographyselection.jpg new file mode 100644 index 00000000..fa5eb3b0 Binary files /dev/null and b/assets/images/MillingWorkflow/Topographyselection.jpg differ diff --git a/assets/images/MillingWorkflow/cncnostool.jpg b/assets/images/MillingWorkflow/cncnostool.jpg new file mode 100644 index 00000000..4328bcaa Binary files /dev/null and b/assets/images/MillingWorkflow/cncnostool.jpg differ diff --git a/assets/images/MillingWorkflow/cncyesstool.jpg b/assets/images/MillingWorkflow/cncyesstool.jpg new file mode 100644 index 00000000..27e63b23 Binary files /dev/null and b/assets/images/MillingWorkflow/cncyesstool.jpg differ diff --git a/assets/images/MillingWorkflow/cover.jpg b/assets/images/MillingWorkflow/cover.jpg new file mode 100644 index 00000000..69c556d3 Binary files /dev/null and b/assets/images/MillingWorkflow/cover.jpg differ diff --git a/assets/images/MillingWorkflow/darkchip.png b/assets/images/MillingWorkflow/darkchip.png new file mode 100644 index 00000000..319a85c1 Binary files /dev/null and b/assets/images/MillingWorkflow/darkchip.png differ diff --git a/assets/images/MillingWorkflow/darkdiameter.png b/assets/images/MillingWorkflow/darkdiameter.png new file mode 100644 index 00000000..2b869893 Binary files /dev/null and b/assets/images/MillingWorkflow/darkdiameter.png differ diff --git a/assets/images/MillingWorkflow/darkfeed.png b/assets/images/MillingWorkflow/darkfeed.png new file mode 100644 index 00000000..e3fb2b8d Binary files /dev/null and b/assets/images/MillingWorkflow/darkfeed.png differ diff --git a/assets/images/MillingWorkflow/darkflute.png b/assets/images/MillingWorkflow/darkflute.png new file mode 100644 index 00000000..9fb2fb78 Binary files /dev/null and b/assets/images/MillingWorkflow/darkflute.png differ diff --git a/assets/images/MillingWorkflow/darkplunge.png b/assets/images/MillingWorkflow/darkplunge.png new file mode 100644 index 00000000..30528161 Binary files /dev/null and b/assets/images/MillingWorkflow/darkplunge.png differ diff --git a/assets/images/MillingWorkflow/darkspindal.png b/assets/images/MillingWorkflow/darkspindal.png new file mode 100644 index 00000000..c5e768e5 Binary files /dev/null and b/assets/images/MillingWorkflow/darkspindal.png differ diff --git a/assets/images/MillingWorkflow/darkstepdown.png b/assets/images/MillingWorkflow/darkstepdown.png new file mode 100644 index 00000000..14f2c581 Binary files /dev/null and b/assets/images/MillingWorkflow/darkstepdown.png differ diff --git a/assets/images/MillingWorkflow/darkstepover.png b/assets/images/MillingWorkflow/darkstepover.png new file mode 100644 index 00000000..86d12015 Binary files /dev/null and b/assets/images/MillingWorkflow/darkstepover.png differ diff --git a/assets/images/MillingWorkflow/darksurface.png b/assets/images/MillingWorkflow/darksurface.png new file mode 100644 index 00000000..17560aa7 Binary files /dev/null and b/assets/images/MillingWorkflow/darksurface.png differ diff --git a/assets/images/MillingWorkflow/galeglory1.jpg b/assets/images/MillingWorkflow/galeglory1.jpg new file mode 100644 index 00000000..997e7f89 Binary files /dev/null and b/assets/images/MillingWorkflow/galeglory1.jpg differ diff --git a/assets/images/MillingWorkflow/galeglory2.jpg b/assets/images/MillingWorkflow/galeglory2.jpg new file mode 100644 index 00000000..6c54c3dc Binary files /dev/null and b/assets/images/MillingWorkflow/galeglory2.jpg differ diff --git a/assets/images/MillingWorkflow/galeraw.jpg b/assets/images/MillingWorkflow/galeraw.jpg new file mode 100644 index 00000000..b2c4f0b6 Binary files /dev/null and b/assets/images/MillingWorkflow/galeraw.jpg differ diff --git a/assets/images/MillingWorkflow/hero.png b/assets/images/MillingWorkflow/hero.png new file mode 100644 index 00000000..1438cc5a Binary files /dev/null and b/assets/images/MillingWorkflow/hero.png differ diff --git a/assets/images/MillingWorkflow/lightchip.png b/assets/images/MillingWorkflow/lightchip.png new file mode 100644 index 00000000..94791e52 Binary files /dev/null and b/assets/images/MillingWorkflow/lightchip.png differ diff --git a/assets/images/MillingWorkflow/lightdiameter.png b/assets/images/MillingWorkflow/lightdiameter.png new file mode 100644 index 00000000..c481ba82 Binary files /dev/null and b/assets/images/MillingWorkflow/lightdiameter.png differ diff --git a/assets/images/MillingWorkflow/lightfeed.png b/assets/images/MillingWorkflow/lightfeed.png new file mode 100644 index 00000000..d72db1ba Binary files /dev/null and b/assets/images/MillingWorkflow/lightfeed.png differ diff --git a/assets/images/MillingWorkflow/lightflute.png b/assets/images/MillingWorkflow/lightflute.png new file mode 100644 index 00000000..61a251ca Binary files /dev/null and b/assets/images/MillingWorkflow/lightflute.png differ diff --git a/assets/images/MillingWorkflow/lightplunge.png b/assets/images/MillingWorkflow/lightplunge.png new file mode 100644 index 00000000..f50d22d9 Binary files /dev/null and b/assets/images/MillingWorkflow/lightplunge.png differ diff --git a/assets/images/MillingWorkflow/lightspindal.png b/assets/images/MillingWorkflow/lightspindal.png new file mode 100644 index 00000000..a6746344 Binary files /dev/null and b/assets/images/MillingWorkflow/lightspindal.png differ diff --git a/assets/images/MillingWorkflow/lightstepdown.png b/assets/images/MillingWorkflow/lightstepdown.png new file mode 100644 index 00000000..90206d06 Binary files /dev/null and b/assets/images/MillingWorkflow/lightstepdown.png differ diff --git a/assets/images/MillingWorkflow/lightstepover.png b/assets/images/MillingWorkflow/lightstepover.png new file mode 100644 index 00000000..8946ce30 Binary files /dev/null and b/assets/images/MillingWorkflow/lightstepover.png differ diff --git a/assets/images/MillingWorkflow/lightsurface.png b/assets/images/MillingWorkflow/lightsurface.png new file mode 100644 index 00000000..6b23a85d Binary files /dev/null and b/assets/images/MillingWorkflow/lightsurface.png differ diff --git a/assets/images/MillingWorkflow/materials/abs.png b/assets/images/MillingWorkflow/materials/abs.png new file mode 100644 index 00000000..e94b4745 Binary files /dev/null and b/assets/images/MillingWorkflow/materials/abs.png differ diff --git a/assets/images/MillingWorkflow/materials/acrylic.png b/assets/images/MillingWorkflow/materials/acrylic.png new file mode 100644 index 00000000..d3cee6bc Binary files /dev/null and b/assets/images/MillingWorkflow/materials/acrylic.png differ diff --git a/assets/images/MillingWorkflow/materials/aluminum.png b/assets/images/MillingWorkflow/materials/aluminum.png new file mode 100644 index 00000000..eced3279 Binary files /dev/null and b/assets/images/MillingWorkflow/materials/aluminum.png differ diff --git a/assets/images/MillingWorkflow/materials/brass.png b/assets/images/MillingWorkflow/materials/brass.png new file mode 100644 index 00000000..c6aa46a4 Binary files /dev/null and b/assets/images/MillingWorkflow/materials/brass.png differ diff --git a/assets/images/MillingWorkflow/materials/cf.png b/assets/images/MillingWorkflow/materials/cf.png new file mode 100644 index 00000000..ab9fee8a Binary files /dev/null and b/assets/images/MillingWorkflow/materials/cf.png differ diff --git a/assets/images/MillingWorkflow/materials/copper.png b/assets/images/MillingWorkflow/materials/copper.png new file mode 100644 index 00000000..67a51d40 Binary files /dev/null and b/assets/images/MillingWorkflow/materials/copper.png differ diff --git a/assets/images/MillingWorkflow/materials/foam.png b/assets/images/MillingWorkflow/materials/foam.png new file mode 100644 index 00000000..7e8970bf Binary files /dev/null and b/assets/images/MillingWorkflow/materials/foam.png differ diff --git a/assets/images/MillingWorkflow/materials/hdpe.png b/assets/images/MillingWorkflow/materials/hdpe.png new file mode 100644 index 00000000..df7b0a4b Binary files /dev/null and b/assets/images/MillingWorkflow/materials/hdpe.png differ diff --git a/assets/images/MillingWorkflow/materials/mdf.png b/assets/images/MillingWorkflow/materials/mdf.png new file mode 100644 index 00000000..6bdac578 Binary files /dev/null and b/assets/images/MillingWorkflow/materials/mdf.png differ diff --git a/assets/images/MillingWorkflow/materials/nylon.png b/assets/images/MillingWorkflow/materials/nylon.png new file mode 100644 index 00000000..9f413928 Binary files /dev/null and b/assets/images/MillingWorkflow/materials/nylon.png differ diff --git a/assets/images/MillingWorkflow/materials/obs.png b/assets/images/MillingWorkflow/materials/obs.png new file mode 100644 index 00000000..42184828 Binary files /dev/null and b/assets/images/MillingWorkflow/materials/obs.png differ diff --git a/assets/images/MillingWorkflow/materials/pc.png b/assets/images/MillingWorkflow/materials/pc.png new file mode 100644 index 00000000..6adc09d2 Binary files /dev/null and b/assets/images/MillingWorkflow/materials/pc.png differ diff --git a/assets/images/MillingWorkflow/materials/peek.png b/assets/images/MillingWorkflow/materials/peek.png new file mode 100644 index 00000000..864b4e4c Binary files /dev/null and b/assets/images/MillingWorkflow/materials/peek.png differ diff --git a/assets/images/MillingWorkflow/materials/pei.png b/assets/images/MillingWorkflow/materials/pei.png new file mode 100644 index 00000000..3fcde96c Binary files /dev/null and b/assets/images/MillingWorkflow/materials/pei.png differ diff --git a/assets/images/MillingWorkflow/materials/pom.png b/assets/images/MillingWorkflow/materials/pom.png new file mode 100644 index 00000000..deb2f0ab Binary files /dev/null and b/assets/images/MillingWorkflow/materials/pom.png differ diff --git a/assets/images/MillingWorkflow/materials/pvc.png b/assets/images/MillingWorkflow/materials/pvc.png new file mode 100644 index 00000000..e74aa8fb Binary files /dev/null and b/assets/images/MillingWorkflow/materials/pvc.png differ diff --git a/assets/images/MillingWorkflow/materials/softwood.png b/assets/images/MillingWorkflow/materials/softwood.png new file mode 100644 index 00000000..737da1d1 Binary files /dev/null and b/assets/images/MillingWorkflow/materials/softwood.png differ diff --git a/assets/images/MillingWorkflow/materials/steel.png b/assets/images/MillingWorkflow/materials/steel.png new file mode 100644 index 00000000..32b652c6 Binary files /dev/null and b/assets/images/MillingWorkflow/materials/steel.png differ diff --git a/assets/images/MillingWorkflow/materials/wax.png b/assets/images/MillingWorkflow/materials/wax.png new file mode 100644 index 00000000..1c7e99e5 Binary files /dev/null and b/assets/images/MillingWorkflow/materials/wax.png differ diff --git a/assets/images/MillingWorkflow/materials/wood.png b/assets/images/MillingWorkflow/materials/wood.png new file mode 100644 index 00000000..b0473e07 Binary files /dev/null and b/assets/images/MillingWorkflow/materials/wood.png differ diff --git a/assets/images/MillingWorkflow/stool1.jpg b/assets/images/MillingWorkflow/stool1.jpg new file mode 100644 index 00000000..713774a4 Binary files /dev/null and b/assets/images/MillingWorkflow/stool1.jpg differ diff --git a/assets/images/MillingWorkflow/stool2.jpg b/assets/images/MillingWorkflow/stool2.jpg new file mode 100644 index 00000000..7302a754 Binary files /dev/null and b/assets/images/MillingWorkflow/stool2.jpg differ diff --git a/assets/images/MillingWorkflow/stool3.jpg b/assets/images/MillingWorkflow/stool3.jpg new file mode 100644 index 00000000..7e57b127 Binary files /dev/null and b/assets/images/MillingWorkflow/stool3.jpg differ diff --git a/assets/images/MillingWorkflow/tahoefinishingpass.jpg b/assets/images/MillingWorkflow/tahoefinishingpass.jpg new file mode 100644 index 00000000..74c01fb2 Binary files /dev/null and b/assets/images/MillingWorkflow/tahoefinishingpass.jpg differ diff --git a/assets/images/MillingWorkflow/tahoeglory1.jpg b/assets/images/MillingWorkflow/tahoeglory1.jpg new file mode 100644 index 00000000..8e819f39 Binary files /dev/null and b/assets/images/MillingWorkflow/tahoeglory1.jpg differ diff --git a/assets/images/MillingWorkflow/tahoeglory2.jpg b/assets/images/MillingWorkflow/tahoeglory2.jpg new file mode 100644 index 00000000..3d4d855e Binary files /dev/null and b/assets/images/MillingWorkflow/tahoeglory2.jpg differ diff --git a/assets/images/MillingWorkflow/tahoeroughingpass.jpg b/assets/images/MillingWorkflow/tahoeroughingpass.jpg new file mode 100644 index 00000000..bf80d9d2 Binary files /dev/null and b/assets/images/MillingWorkflow/tahoeroughingpass.jpg differ diff --git a/assets/images/MillingWorkflow/tahoestock.jpg b/assets/images/MillingWorkflow/tahoestock.jpg new file mode 100644 index 00000000..65e6bc30 Binary files /dev/null and b/assets/images/MillingWorkflow/tahoestock.jpg differ diff --git a/assets/images/MillingWorkflow/topographylocation.jpg b/assets/images/MillingWorkflow/topographylocation.jpg new file mode 100644 index 00000000..658e1f8e Binary files /dev/null and b/assets/images/MillingWorkflow/topographylocation.jpg differ diff --git a/assets/images/MillingWorkflow/topographywaterbase.jpg b/assets/images/MillingWorkflow/topographywaterbase.jpg new file mode 100644 index 00000000..f5fe9497 Binary files /dev/null and b/assets/images/MillingWorkflow/topographywaterbase.jpg differ diff --git a/assets/images/Octoprint/AppearanceSettings.png b/assets/images/Octoprint/AppearanceSettings.png new file mode 100644 index 00000000..84b3e716 Binary files /dev/null and b/assets/images/Octoprint/AppearanceSettings.png differ diff --git a/assets/images/Octoprint/CameraWindow.png b/assets/images/Octoprint/CameraWindow.png new file mode 100644 index 00000000..47571fe8 Binary files /dev/null and b/assets/images/Octoprint/CameraWindow.png differ diff --git a/assets/images/Octoprint/Octoprint.png b/assets/images/Octoprint/Octoprint.png new file mode 100644 index 00000000..4e7cdd5e Binary files /dev/null and b/assets/images/Octoprint/Octoprint.png differ diff --git a/assets/images/Octoprint/PrettyGcode-Screen3.jpg b/assets/images/Octoprint/PrettyGcode-Screen3.jpg new file mode 100644 index 00000000..ca84ba9d Binary files /dev/null and b/assets/images/Octoprint/PrettyGcode-Screen3.jpg differ diff --git a/assets/images/Octoprint/bedlevlevisualizer.png b/assets/images/Octoprint/bedlevlevisualizer.png new file mode 100644 index 00000000..f8652f4b Binary files /dev/null and b/assets/images/Octoprint/bedlevlevisualizer.png differ diff --git a/assets/images/Octoprint/cameramount.png b/assets/images/Octoprint/cameramount.png new file mode 100644 index 00000000..ab9a9d86 Binary files /dev/null and b/assets/images/Octoprint/cameramount.png differ diff --git a/assets/images/Octoprint/custombackground.png b/assets/images/Octoprint/custombackground.png new file mode 100644 index 00000000..a0dda2c8 Binary files /dev/null and b/assets/images/Octoprint/custombackground.png differ diff --git a/assets/images/Octoprint/edit_gcode.png b/assets/images/Octoprint/edit_gcode.png new file mode 100644 index 00000000..3fd891ce Binary files /dev/null and b/assets/images/Octoprint/edit_gcode.png differ diff --git a/assets/images/Octoprint/edit_gcode2.png b/assets/images/Octoprint/edit_gcode2.png new file mode 100644 index 00000000..46b201bb Binary files /dev/null and b/assets/images/Octoprint/edit_gcode2.png differ diff --git a/assets/images/Octoprint/eta.png b/assets/images/Octoprint/eta.png new file mode 100644 index 00000000..ab045516 Binary files /dev/null and b/assets/images/Octoprint/eta.png differ diff --git a/assets/images/Octoprint/excluderegions.png b/assets/images/Octoprint/excluderegions.png new file mode 100644 index 00000000..bccb247c Binary files /dev/null and b/assets/images/Octoprint/excluderegions.png differ diff --git a/assets/images/Octoprint/hero.png b/assets/images/Octoprint/hero.png new file mode 100644 index 00000000..3fddbae5 Binary files /dev/null and b/assets/images/Octoprint/hero.png differ diff --git a/assets/images/Octoprint/homescreen.jpg b/assets/images/Octoprint/homescreen.jpg new file mode 100644 index 00000000..6ef9c61f Binary files /dev/null and b/assets/images/Octoprint/homescreen.jpg differ diff --git a/assets/images/Octoprint/ipdisplay.jpg b/assets/images/Octoprint/ipdisplay.jpg new file mode 100644 index 00000000..cc618f4e Binary files /dev/null and b/assets/images/Octoprint/ipdisplay.jpg differ diff --git a/assets/images/Octoprint/laboctoprint.jpg b/assets/images/Octoprint/laboctoprint.jpg new file mode 100644 index 00000000..327a10fe Binary files /dev/null and b/assets/images/Octoprint/laboctoprint.jpg differ diff --git a/assets/images/Octoprint/mountedelectronics.jpg b/assets/images/Octoprint/mountedelectronics.jpg new file mode 100644 index 00000000..82e6ec07 Binary files /dev/null and b/assets/images/Octoprint/mountedelectronics.jpg differ diff --git a/assets/images/Octoprint/navbar.png b/assets/images/Octoprint/navbar.png new file mode 100644 index 00000000..19763898 Binary files /dev/null and b/assets/images/Octoprint/navbar.png differ diff --git a/assets/images/Octoprint/personaloctoprint.jpg b/assets/images/Octoprint/personaloctoprint.jpg new file mode 100644 index 00000000..57acb5b9 Binary files /dev/null and b/assets/images/Octoprint/personaloctoprint.jpg differ diff --git a/assets/images/Octoprint/pluginsettings.png b/assets/images/Octoprint/pluginsettings.png new file mode 100644 index 00000000..ba134b90 Binary files /dev/null and b/assets/images/Octoprint/pluginsettings.png differ diff --git a/assets/images/Octoprint/printerpofiles.png b/assets/images/Octoprint/printerpofiles.png new file mode 100644 index 00000000..3f9094f6 Binary files /dev/null and b/assets/images/Octoprint/printerpofiles.png differ diff --git a/assets/images/Octoprint/serialconnection.png b/assets/images/Octoprint/serialconnection.png new file mode 100644 index 00000000..da28d0ea Binary files /dev/null and b/assets/images/Octoprint/serialconnection.png differ diff --git a/assets/images/Octoprint/tsdapp.jpg b/assets/images/Octoprint/tsdapp.jpg new file mode 100644 index 00000000..8120ec4f Binary files /dev/null and b/assets/images/Octoprint/tsdapp.jpg differ diff --git a/assets/images/Octoprint/tsdcontrol.jpg b/assets/images/Octoprint/tsdcontrol.jpg new file mode 100644 index 00000000..bf94a167 Binary files /dev/null and b/assets/images/Octoprint/tsdcontrol.jpg differ diff --git a/assets/images/Octoprint/webcamsettings.png b/assets/images/Octoprint/webcamsettings.png new file mode 100644 index 00000000..6dc28b38 Binary files /dev/null and b/assets/images/Octoprint/webcamsettings.png differ diff --git a/assets/images/ParametricGenerator/ISOExternalThreadRootContour.jpg b/assets/images/ParametricGenerator/ISOExternalThreadRootContour.jpg new file mode 100644 index 00000000..0b3d150a Binary files /dev/null and b/assets/images/ParametricGenerator/ISOExternalThreadRootContour.jpg differ diff --git a/assets/images/ParametricGenerator/ISOThreadForm1.jpg b/assets/images/ParametricGenerator/ISOThreadForm1.jpg new file mode 100644 index 00000000..e1eea33f Binary files /dev/null and b/assets/images/ParametricGenerator/ISOThreadForm1.jpg differ diff --git a/assets/images/ParametricGenerator/ThreadCap.png b/assets/images/ParametricGenerator/ThreadCap.png new file mode 100644 index 00000000..de778ae7 Binary files /dev/null and b/assets/images/ParametricGenerator/ThreadCap.png differ diff --git a/assets/images/ParametricGenerator/beautyshot.jpg b/assets/images/ParametricGenerator/beautyshot.jpg new file mode 100644 index 00000000..55b01af4 Binary files /dev/null and b/assets/images/ParametricGenerator/beautyshot.jpg differ diff --git a/assets/images/ParametricGenerator/beautyshot1.jpg b/assets/images/ParametricGenerator/beautyshot1.jpg new file mode 100644 index 00000000..d3062487 Binary files /dev/null and b/assets/images/ParametricGenerator/beautyshot1.jpg differ diff --git a/assets/images/ParametricGenerator/capparameters.png b/assets/images/ParametricGenerator/capparameters.png new file mode 100644 index 00000000..e5cb042d Binary files /dev/null and b/assets/images/ParametricGenerator/capparameters.png differ diff --git a/assets/images/ParametricGenerator/connectdiameter.png b/assets/images/ParametricGenerator/connectdiameter.png new file mode 100644 index 00000000..1c313739 Binary files /dev/null and b/assets/images/ParametricGenerator/connectdiameter.png differ diff --git a/assets/images/ParametricGenerator/connectlegnth.png b/assets/images/ParametricGenerator/connectlegnth.png new file mode 100644 index 00000000..c52759fa Binary files /dev/null and b/assets/images/ParametricGenerator/connectlegnth.png differ diff --git a/assets/images/ParametricGenerator/diametermeasurment.jpg b/assets/images/ParametricGenerator/diametermeasurment.jpg new file mode 100644 index 00000000..0e650535 Binary files /dev/null and b/assets/images/ParametricGenerator/diametermeasurment.jpg differ diff --git a/assets/images/ParametricGenerator/gripdepth.png b/assets/images/ParametricGenerator/gripdepth.png new file mode 100644 index 00000000..dc485a8d Binary files /dev/null and b/assets/images/ParametricGenerator/gripdepth.png differ diff --git a/assets/images/ParametricGenerator/hero.png b/assets/images/ParametricGenerator/hero.png new file mode 100644 index 00000000..a81e47f2 Binary files /dev/null and b/assets/images/ParametricGenerator/hero.png differ diff --git a/assets/images/ParametricGenerator/lanyard.png b/assets/images/ParametricGenerator/lanyard.png new file mode 100644 index 00000000..e8ddd49e Binary files /dev/null and b/assets/images/ParametricGenerator/lanyard.png differ diff --git a/assets/images/ParametricGenerator/legnthmeasurement.jpg b/assets/images/ParametricGenerator/legnthmeasurement.jpg new file mode 100644 index 00000000..49d04c12 Binary files /dev/null and b/assets/images/ParametricGenerator/legnthmeasurement.jpg differ diff --git a/assets/images/ParametricGenerator/numofgrips.png b/assets/images/ParametricGenerator/numofgrips.png new file mode 100644 index 00000000..67772f57 Binary files /dev/null and b/assets/images/ParametricGenerator/numofgrips.png differ diff --git a/assets/images/ParametricGenerator/pitchmeasurment.jpg b/assets/images/ParametricGenerator/pitchmeasurment.jpg new file mode 100644 index 00000000..c3c1e9f0 Binary files /dev/null and b/assets/images/ParametricGenerator/pitchmeasurment.jpg differ diff --git a/assets/images/ParametricGenerator/plain.png b/assets/images/ParametricGenerator/plain.png new file mode 100644 index 00000000..a0e0d809 Binary files /dev/null and b/assets/images/ParametricGenerator/plain.png differ diff --git a/assets/images/ParametricGenerator/saltshaker.png b/assets/images/ParametricGenerator/saltshaker.png new file mode 100644 index 00000000..f77c04f1 Binary files /dev/null and b/assets/images/ParametricGenerator/saltshaker.png differ diff --git a/assets/images/ParametricGenerator/singlehole.png b/assets/images/ParametricGenerator/singlehole.png new file mode 100644 index 00000000..e34e864b Binary files /dev/null and b/assets/images/ParametricGenerator/singlehole.png differ diff --git a/assets/images/ParametricGenerator/threadfillet.png b/assets/images/ParametricGenerator/threadfillet.png new file mode 100644 index 00000000..b05d9b13 Binary files /dev/null and b/assets/images/ParametricGenerator/threadfillet.png differ diff --git a/assets/images/ParametricGenerator/threadpitch.png b/assets/images/ParametricGenerator/threadpitch.png new file mode 100644 index 00000000..0af9598d Binary files /dev/null and b/assets/images/ParametricGenerator/threadpitch.png differ diff --git a/assets/images/PortfolioSite/100commit.jpg b/assets/images/PortfolioSite/100commit.jpg new file mode 100644 index 00000000..683b74d7 Binary files /dev/null and b/assets/images/PortfolioSite/100commit.jpg differ diff --git a/assets/images/PortfolioSite/200commit.jpg b/assets/images/PortfolioSite/200commit.jpg new file mode 100644 index 00000000..e889928e Binary files /dev/null and b/assets/images/PortfolioSite/200commit.jpg differ diff --git a/assets/images/PortfolioSite/300commit.png b/assets/images/PortfolioSite/300commit.png new file mode 100644 index 00000000..6efd2fb1 Binary files /dev/null and b/assets/images/PortfolioSite/300commit.png differ diff --git a/assets/images/PortfolioSite/400commit.png b/assets/images/PortfolioSite/400commit.png new file mode 100644 index 00000000..f2b147e4 Binary files /dev/null and b/assets/images/PortfolioSite/400commit.png differ diff --git a/assets/images/PortfolioSite/highlightcode.jpg b/assets/images/PortfolioSite/highlightcode.jpg new file mode 100644 index 00000000..90643cc7 Binary files /dev/null and b/assets/images/PortfolioSite/highlightcode.jpg differ diff --git a/assets/images/PortfolioSite/portfolioSiteHero.png b/assets/images/PortfolioSite/portfolioSiteHero.png new file mode 100644 index 00000000..829ebca0 Binary files /dev/null and b/assets/images/PortfolioSite/portfolioSiteHero.png differ diff --git a/assets/images/SerialUPDI/2ndgenmileldtop.jpg b/assets/images/SerialUPDI/2ndgenmileldtop.jpg new file mode 100644 index 00000000..9036b5c3 Binary files /dev/null and b/assets/images/SerialUPDI/2ndgenmileldtop.jpg differ diff --git a/assets/images/SerialUPDI/2ndgenmilledbottom.jpg b/assets/images/SerialUPDI/2ndgenmilledbottom.jpg new file mode 100644 index 00000000..c2a13d6d Binary files /dev/null and b/assets/images/SerialUPDI/2ndgenmilledbottom.jpg differ diff --git a/assets/images/SerialUPDI/2ndgensolderdtop.jpg b/assets/images/SerialUPDI/2ndgensolderdtop.jpg new file mode 100644 index 00000000..4f67b003 Binary files /dev/null and b/assets/images/SerialUPDI/2ndgensolderdtop.jpg differ diff --git a/assets/images/SerialUPDI/2ndgensolderedbottom.jpg b/assets/images/SerialUPDI/2ndgensolderedbottom.jpg new file mode 100644 index 00000000..fcdd8b97 Binary files /dev/null and b/assets/images/SerialUPDI/2ndgensolderedbottom.jpg differ diff --git a/assets/images/SerialUPDI/3genmilledtop.jpg b/assets/images/SerialUPDI/3genmilledtop.jpg new file mode 100644 index 00000000..5cab8c06 Binary files /dev/null and b/assets/images/SerialUPDI/3genmilledtop.jpg differ diff --git a/assets/images/SerialUPDI/3rdgenmilledback.jpg b/assets/images/SerialUPDI/3rdgenmilledback.jpg new file mode 100644 index 00000000..e7e0830b Binary files /dev/null and b/assets/images/SerialUPDI/3rdgenmilledback.jpg differ diff --git a/assets/images/SerialUPDI/3rdgensolderedback.jpg b/assets/images/SerialUPDI/3rdgensolderedback.jpg new file mode 100644 index 00000000..790f2f48 Binary files /dev/null and b/assets/images/SerialUPDI/3rdgensolderedback.jpg differ diff --git a/assets/images/SerialUPDI/3rdgensolderedtop.jpg b/assets/images/SerialUPDI/3rdgensolderedtop.jpg new file mode 100644 index 00000000..0c425f44 Binary files /dev/null and b/assets/images/SerialUPDI/3rdgensolderedtop.jpg differ diff --git a/assets/images/SerialUPDI/ICPBottom1.png b/assets/images/SerialUPDI/ICPBottom1.png new file mode 100644 index 00000000..412f8473 Binary files /dev/null and b/assets/images/SerialUPDI/ICPBottom1.png differ diff --git a/assets/images/SerialUPDI/ICPLayered1.png b/assets/images/SerialUPDI/ICPLayered1.png new file mode 100644 index 00000000..0223297a Binary files /dev/null and b/assets/images/SerialUPDI/ICPLayered1.png differ diff --git a/assets/images/SerialUPDI/ICPSchem1.png b/assets/images/SerialUPDI/ICPSchem1.png new file mode 100644 index 00000000..d79ad0e2 Binary files /dev/null and b/assets/images/SerialUPDI/ICPSchem1.png differ diff --git a/assets/images/SerialUPDI/ICPTop1.png b/assets/images/SerialUPDI/ICPTop1.png new file mode 100644 index 00000000..14717e9f Binary files /dev/null and b/assets/images/SerialUPDI/ICPTop1.png differ diff --git a/assets/images/SerialUPDI/SerialBreadboardWired.jpg b/assets/images/SerialUPDI/SerialBreadboardWired.jpg new file mode 100644 index 00000000..27a1c4bd Binary files /dev/null and b/assets/images/SerialUPDI/SerialBreadboardWired.jpg differ diff --git a/assets/images/SerialUPDI/blankjtagsketch.png b/assets/images/SerialUPDI/blankjtagsketch.png new file mode 100644 index 00000000..db03e9ac Binary files /dev/null and b/assets/images/SerialUPDI/blankjtagsketch.png differ diff --git a/assets/images/SerialUPDI/boardselection.jpg b/assets/images/SerialUPDI/boardselection.jpg new file mode 100644 index 00000000..dd1c01c5 Binary files /dev/null and b/assets/images/SerialUPDI/boardselection.jpg differ diff --git a/assets/images/SerialUPDI/diodeleads.jpg b/assets/images/SerialUPDI/diodeleads.jpg new file mode 100644 index 00000000..0d7b5642 Binary files /dev/null and b/assets/images/SerialUPDI/diodeleads.jpg differ diff --git a/assets/images/SerialUPDI/foldernaming.png b/assets/images/SerialUPDI/foldernaming.png new file mode 100644 index 00000000..fffda5e0 Binary files /dev/null and b/assets/images/SerialUPDI/foldernaming.png differ diff --git a/assets/images/SerialUPDI/ftdi2updiboard.png b/assets/images/SerialUPDI/ftdi2updiboard.png new file mode 100644 index 00000000..afa9bdc9 Binary files /dev/null and b/assets/images/SerialUPDI/ftdi2updiboard.png differ diff --git a/assets/images/SerialUPDI/ftdi2updibottom.png b/assets/images/SerialUPDI/ftdi2updibottom.png new file mode 100644 index 00000000..4a90f16d Binary files /dev/null and b/assets/images/SerialUPDI/ftdi2updibottom.png differ diff --git a/assets/images/SerialUPDI/ftdi2updiirlbottom.jpg b/assets/images/SerialUPDI/ftdi2updiirlbottom.jpg new file mode 100644 index 00000000..704db2c6 Binary files /dev/null and b/assets/images/SerialUPDI/ftdi2updiirlbottom.jpg differ diff --git a/assets/images/SerialUPDI/ftdi2updiirltop.jpg b/assets/images/SerialUPDI/ftdi2updiirltop.jpg new file mode 100644 index 00000000..4aefa2aa Binary files /dev/null and b/assets/images/SerialUPDI/ftdi2updiirltop.jpg differ diff --git a/assets/images/SerialUPDI/ftdi2updischem.png b/assets/images/SerialUPDI/ftdi2updischem.png new file mode 100644 index 00000000..0647c831 Binary files /dev/null and b/assets/images/SerialUPDI/ftdi2updischem.png differ diff --git a/assets/images/SerialUPDI/ftdi2updisolderedbottom.jpg b/assets/images/SerialUPDI/ftdi2updisolderedbottom.jpg new file mode 100644 index 00000000..840edcd8 Binary files /dev/null and b/assets/images/SerialUPDI/ftdi2updisolderedbottom.jpg differ diff --git a/assets/images/SerialUPDI/ftdi2updisolderedtop.jpg b/assets/images/SerialUPDI/ftdi2updisolderedtop.jpg new file mode 100644 index 00000000..ed576fb8 Binary files /dev/null and b/assets/images/SerialUPDI/ftdi2updisolderedtop.jpg differ diff --git a/assets/images/SerialUPDI/ftdi2upditop.png b/assets/images/SerialUPDI/ftdi2upditop.png new file mode 100644 index 00000000..b1a76114 Binary files /dev/null and b/assets/images/SerialUPDI/ftdi2upditop.png differ diff --git a/assets/images/SerialUPDI/jtagtest.jpg b/assets/images/SerialUPDI/jtagtest.jpg new file mode 100644 index 00000000..8d53dcbe Binary files /dev/null and b/assets/images/SerialUPDI/jtagtest.jpg differ diff --git a/assets/images/SerialUPDI/orderedpcbback.jpg b/assets/images/SerialUPDI/orderedpcbback.jpg new file mode 100644 index 00000000..e7cc38a7 Binary files /dev/null and b/assets/images/SerialUPDI/orderedpcbback.jpg differ diff --git a/assets/images/SerialUPDI/orderedpcbtop.jpg b/assets/images/SerialUPDI/orderedpcbtop.jpg new file mode 100644 index 00000000..3647d48c Binary files /dev/null and b/assets/images/SerialUPDI/orderedpcbtop.jpg differ diff --git a/assets/images/SerialUPDI/orderedsolderedpcbbottom.jpg b/assets/images/SerialUPDI/orderedsolderedpcbbottom.jpg new file mode 100644 index 00000000..2fa7a9b4 Binary files /dev/null and b/assets/images/SerialUPDI/orderedsolderedpcbbottom.jpg differ diff --git a/assets/images/SerialUPDI/orderedsolderedpcbtop.jpg b/assets/images/SerialUPDI/orderedsolderedpcbtop.jpg new file mode 100644 index 00000000..678cbdc3 Binary files /dev/null and b/assets/images/SerialUPDI/orderedsolderedpcbtop.jpg differ diff --git a/assets/images/SerialUPDI/pasteapplication.jpg b/assets/images/SerialUPDI/pasteapplication.jpg new file mode 100644 index 00000000..33ba1ed8 Binary files /dev/null and b/assets/images/SerialUPDI/pasteapplication.jpg differ diff --git a/assets/images/SerialUPDI/pastestencil.jpg b/assets/images/SerialUPDI/pastestencil.jpg new file mode 100644 index 00000000..b38cf28c Binary files /dev/null and b/assets/images/SerialUPDI/pastestencil.jpg differ diff --git a/assets/images/SerialUPDI/portselection.jpg b/assets/images/SerialUPDI/portselection.jpg new file mode 100644 index 00000000..febcd483 Binary files /dev/null and b/assets/images/SerialUPDI/portselection.jpg differ diff --git a/assets/images/SerialUPDI/programmerselection.jpg b/assets/images/SerialUPDI/programmerselection.jpg new file mode 100644 index 00000000..82e3249b Binary files /dev/null and b/assets/images/SerialUPDI/programmerselection.jpg differ diff --git a/assets/images/SerialUPDI/projectopen.png b/assets/images/SerialUPDI/projectopen.png new file mode 100644 index 00000000..53b239a3 Binary files /dev/null and b/assets/images/SerialUPDI/projectopen.png differ diff --git a/assets/images/SerialUPDI/serialmonitor.jpg b/assets/images/SerialUPDI/serialmonitor.jpg new file mode 100644 index 00000000..f22b1d8b Binary files /dev/null and b/assets/images/SerialUPDI/serialmonitor.jpg differ diff --git a/assets/images/SerialUPDI/serialmonitroselection.jpg b/assets/images/SerialUPDI/serialmonitroselection.jpg new file mode 100644 index 00000000..e7335dc3 Binary files /dev/null and b/assets/images/SerialUPDI/serialmonitroselection.jpg differ diff --git a/assets/images/SerialUPDI/serialswitch.jpg b/assets/images/SerialUPDI/serialswitch.jpg new file mode 100644 index 00000000..a9928ae1 Binary files /dev/null and b/assets/images/SerialUPDI/serialswitch.jpg differ diff --git a/assets/images/SerialUPDI/serialtestwireing.jpg b/assets/images/SerialUPDI/serialtestwireing.jpg new file mode 100644 index 00000000..f0950c86 Binary files /dev/null and b/assets/images/SerialUPDI/serialtestwireing.jpg differ diff --git a/assets/images/SerialUPDI/serialwireing.jpg b/assets/images/SerialUPDI/serialwireing.jpg new file mode 100644 index 00000000..289ee358 Binary files /dev/null and b/assets/images/SerialUPDI/serialwireing.jpg differ diff --git a/assets/images/SerialUPDI/solderedviasbottom.jpg b/assets/images/SerialUPDI/solderedviasbottom.jpg new file mode 100644 index 00000000..ae621e4d Binary files /dev/null and b/assets/images/SerialUPDI/solderedviasbottom.jpg differ diff --git a/assets/images/SerialUPDI/solderedviastop.jpg b/assets/images/SerialUPDI/solderedviastop.jpg new file mode 100644 index 00000000..d357d481 Binary files /dev/null and b/assets/images/SerialUPDI/solderedviastop.jpg differ diff --git a/assets/images/SerialUPDI/topstencil.png b/assets/images/SerialUPDI/topstencil.png new file mode 100644 index 00000000..4b18dd49 Binary files /dev/null and b/assets/images/SerialUPDI/topstencil.png differ diff --git a/assets/images/SerialUPDI/updiHero.png b/assets/images/SerialUPDI/updiHero.png new file mode 100644 index 00000000..bbbd1a73 Binary files /dev/null and b/assets/images/SerialUPDI/updiHero.png differ diff --git a/assets/images/SerialUPDI/updiswitch.jpg b/assets/images/SerialUPDI/updiswitch.jpg new file mode 100644 index 00000000..734379fb Binary files /dev/null and b/assets/images/SerialUPDI/updiswitch.jpg differ diff --git a/assets/images/SerialUPDI/updiwireing.jpg b/assets/images/SerialUPDI/updiwireing.jpg new file mode 100644 index 00000000..b89ea4d0 Binary files /dev/null and b/assets/images/SerialUPDI/updiwireing.jpg differ diff --git a/assets/images/SerialUPDI/viasstep1.jpg b/assets/images/SerialUPDI/viasstep1.jpg new file mode 100644 index 00000000..718d7a35 Binary files /dev/null and b/assets/images/SerialUPDI/viasstep1.jpg differ diff --git a/assets/images/SerialUPDI/viasstep2.jpg b/assets/images/SerialUPDI/viasstep2.jpg new file mode 100644 index 00000000..be5bbbde Binary files /dev/null and b/assets/images/SerialUPDI/viasstep2.jpg differ diff --git a/assets/images/SerialUPDI/viasstep3.jpg b/assets/images/SerialUPDI/viasstep3.jpg new file mode 100644 index 00000000..bceaec30 Binary files /dev/null and b/assets/images/SerialUPDI/viasstep3.jpg differ diff --git a/assets/images/TeddyWarner.svg b/assets/images/TeddyWarner.svg new file mode 100644 index 00000000..267a0922 --- /dev/null +++ b/assets/images/TeddyWarner.svg @@ -0,0 +1,80 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/assets/images/VonNiemannProbe/FinalElectronics.jpg b/assets/images/VonNiemannProbe/FinalElectronics.jpg new file mode 100644 index 00000000..18f3df57 Binary files /dev/null and b/assets/images/VonNiemannProbe/FinalElectronics.jpg differ diff --git a/assets/images/VonNiemannProbe/FusionParameters.png b/assets/images/VonNiemannProbe/FusionParameters.png new file mode 100644 index 00000000..645f0318 Binary files /dev/null and b/assets/images/VonNiemannProbe/FusionParameters.png differ diff --git a/assets/images/VonNiemannProbe/PCBWay.png b/assets/images/VonNiemannProbe/PCBWay.png new file mode 100644 index 00000000..e51516ca Binary files /dev/null and b/assets/images/VonNiemannProbe/PCBWay.png differ diff --git a/assets/images/VonNiemannProbe/VNPBoard.jpg b/assets/images/VonNiemannProbe/VNPBoard.jpg new file mode 100644 index 00000000..bdc6c148 Binary files /dev/null and b/assets/images/VonNiemannProbe/VNPBoard.jpg differ diff --git a/assets/images/VonNiemannProbe/VNPCADHome.jpg b/assets/images/VonNiemannProbe/VNPCADHome.jpg new file mode 100644 index 00000000..3ab18840 Binary files /dev/null and b/assets/images/VonNiemannProbe/VNPCADHome.jpg differ diff --git a/assets/images/VonNiemannProbe/VNPSchematic.jpg b/assets/images/VonNiemannProbe/VNPSchematic.jpg new file mode 100644 index 00000000..26b66e0b Binary files /dev/null and b/assets/images/VonNiemannProbe/VNPSchematic.jpg differ diff --git a/assets/images/VonNiemannProbe/VNPTWxJHH.jpg b/assets/images/VonNiemannProbe/VNPTWxJHH.jpg new file mode 100644 index 00000000..3daeda24 Binary files /dev/null and b/assets/images/VonNiemannProbe/VNPTWxJHH.jpg differ diff --git a/assets/images/VonNiemannProbe/allprints.jpg b/assets/images/VonNiemannProbe/allprints.jpg new file mode 100644 index 00000000..dad5dc9f Binary files /dev/null and b/assets/images/VonNiemannProbe/allprints.jpg differ diff --git a/assets/images/VonNiemannProbe/bottomview.jpg b/assets/images/VonNiemannProbe/bottomview.jpg new file mode 100644 index 00000000..f5158e78 Binary files /dev/null and b/assets/images/VonNiemannProbe/bottomview.jpg differ diff --git a/assets/images/VonNiemannProbe/cover.jpg b/assets/images/VonNiemannProbe/cover.jpg new file mode 100644 index 00000000..3cdff745 Binary files /dev/null and b/assets/images/VonNiemannProbe/cover.jpg differ diff --git a/assets/images/VonNiemannProbe/coverR.jpg b/assets/images/VonNiemannProbe/coverR.jpg new file mode 100644 index 00000000..15f5d4f3 Binary files /dev/null and b/assets/images/VonNiemannProbe/coverR.jpg differ diff --git a/assets/images/VonNiemannProbe/electronicsVNP.png b/assets/images/VonNiemannProbe/electronicsVNP.png new file mode 100644 index 00000000..d7470ee1 Binary files /dev/null and b/assets/images/VonNiemannProbe/electronicsVNP.png differ diff --git a/assets/images/VonNiemannProbe/extrudeFusion.png b/assets/images/VonNiemannProbe/extrudeFusion.png new file mode 100644 index 00000000..b434d90e Binary files /dev/null and b/assets/images/VonNiemannProbe/extrudeFusion.png differ diff --git a/assets/images/VonNiemannProbe/footdi.png b/assets/images/VonNiemannProbe/footdi.png new file mode 100644 index 00000000..9222adb8 Binary files /dev/null and b/assets/images/VonNiemannProbe/footdi.png differ diff --git a/assets/images/VonNiemannProbe/footgraph.png b/assets/images/VonNiemannProbe/footgraph.png new file mode 100644 index 00000000..73e80cde Binary files /dev/null and b/assets/images/VonNiemannProbe/footgraph.png differ diff --git a/assets/images/VonNiemannProbe/formedFusion.png b/assets/images/VonNiemannProbe/formedFusion.png new file mode 100644 index 00000000..8085a2d9 Binary files /dev/null and b/assets/images/VonNiemannProbe/formedFusion.png differ diff --git a/assets/images/VonNiemannProbe/inshoe.jpg b/assets/images/VonNiemannProbe/inshoe.jpg new file mode 100644 index 00000000..7189e4aa Binary files /dev/null and b/assets/images/VonNiemannProbe/inshoe.jpg differ diff --git a/assets/images/VonNiemannProbe/insoleirl.jpg b/assets/images/VonNiemannProbe/insoleirl.jpg new file mode 100644 index 00000000..b8560735 Binary files /dev/null and b/assets/images/VonNiemannProbe/insoleirl.jpg differ diff --git a/assets/images/VonNiemannProbe/mainboard.png b/assets/images/VonNiemannProbe/mainboard.png new file mode 100644 index 00000000..5bcefb27 Binary files /dev/null and b/assets/images/VonNiemannProbe/mainboard.png differ diff --git a/assets/images/VonNiemannProbe/vonNiemannHero.png b/assets/images/VonNiemannProbe/vonNiemannHero.png new file mode 100644 index 00000000..86d4fc82 Binary files /dev/null and b/assets/images/VonNiemannProbe/vonNiemannHero.png differ diff --git a/assets/images/efd/efd.png b/assets/images/efd/efd.png new file mode 100644 index 00000000..aaa733ee Binary files /dev/null and b/assets/images/efd/efd.png differ diff --git a/assets/images/efd/midlight.png b/assets/images/efd/midlight.png new file mode 100644 index 00000000..e0cbfd07 Binary files /dev/null and b/assets/images/efd/midlight.png differ diff --git a/assets/images/efd/sdtdark.png b/assets/images/efd/sdtdark.png new file mode 100644 index 00000000..14f85a72 Binary files /dev/null and b/assets/images/efd/sdtdark.png differ diff --git a/assets/images/efd/sdtlight.png b/assets/images/efd/sdtlight.png new file mode 100644 index 00000000..a5eb5465 Binary files /dev/null and b/assets/images/efd/sdtlight.png differ diff --git a/assets/images/efd/stressdark.png b/assets/images/efd/stressdark.png new file mode 100644 index 00000000..d415ec73 Binary files /dev/null and b/assets/images/efd/stressdark.png differ diff --git a/assets/images/efd/stresslight.png b/assets/images/efd/stresslight.png new file mode 100644 index 00000000..faedb001 Binary files /dev/null and b/assets/images/efd/stresslight.png differ diff --git a/assets/images/favicon.png b/assets/images/favicon.png new file mode 100644 index 00000000..5417e169 Binary files /dev/null and b/assets/images/favicon.png differ diff --git a/assets/images/force/dark.png b/assets/images/force/dark.png new file mode 100644 index 00000000..72fdfd76 Binary files /dev/null and b/assets/images/force/dark.png differ diff --git a/assets/images/force/force.png b/assets/images/force/force.png new file mode 100644 index 00000000..0ed22d69 Binary files /dev/null and b/assets/images/force/force.png differ diff --git a/assets/images/force/light.png b/assets/images/force/light.png new file mode 100644 index 00000000..83c5fa1c Binary files /dev/null and b/assets/images/force/light.png differ diff --git a/assets/images/index/aaft.png b/assets/images/index/aaft.png new file mode 100644 index 00000000..4fff509a Binary files /dev/null and b/assets/images/index/aaft.png differ diff --git a/assets/images/index/avatardark.svg b/assets/images/index/avatardark.svg new file mode 100644 index 00000000..a14d3c07 --- /dev/null +++ b/assets/images/index/avatardark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/index/avatarlight.svg b/assets/images/index/avatarlight.svg new file mode 100644 index 00000000..fba8951d --- /dev/null +++ b/assets/images/index/avatarlight.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/index/flowers.jpg b/assets/images/index/flowers.jpg new file mode 100644 index 00000000..8b070235 Binary files /dev/null and b/assets/images/index/flowers.jpg differ diff --git a/assets/images/index/logos/autodesk.png b/assets/images/index/logos/autodesk.png new file mode 100644 index 00000000..46aaf92c Binary files /dev/null and b/assets/images/index/logos/autodesk.png differ diff --git a/assets/images/index/logos/fab.png b/assets/images/index/logos/fab.png new file mode 100644 index 00000000..22f1e712 Binary files /dev/null and b/assets/images/index/logos/fab.png differ diff --git a/assets/images/index/logos/mj.png b/assets/images/index/logos/mj.png new file mode 100644 index 00000000..2c5f2645 Binary files /dev/null and b/assets/images/index/logos/mj.png differ diff --git a/assets/images/index/logos/ri.png b/assets/images/index/logos/ri.png new file mode 100644 index 00000000..0fa5f765 Binary files /dev/null and b/assets/images/index/logos/ri.png differ diff --git a/assets/images/index/logos/tone.png b/assets/images/index/logos/tone.png new file mode 100644 index 00000000..497d0952 Binary files /dev/null and b/assets/images/index/logos/tone.png differ diff --git a/assets/images/index/logos/yeot.png b/assets/images/index/logos/yeot.png new file mode 100644 index 00000000..098e0bc1 Binary files /dev/null and b/assets/images/index/logos/yeot.png differ diff --git a/assets/images/index/onb.png b/assets/images/index/onb.png new file mode 100644 index 00000000..a378e247 Binary files /dev/null and b/assets/images/index/onb.png differ diff --git a/assets/images/index/orange.png b/assets/images/index/orange.png new file mode 100644 index 00000000..8ab40e56 Binary files /dev/null and b/assets/images/index/orange.png differ diff --git a/assets/images/index/snake.png b/assets/images/index/snake.png new file mode 100644 index 00000000..df9f2b87 Binary files /dev/null and b/assets/images/index/snake.png differ diff --git a/assets/images/index/toneSmall.png b/assets/images/index/toneSmall.png new file mode 100644 index 00000000..928223ec Binary files /dev/null and b/assets/images/index/toneSmall.png differ diff --git a/assets/images/index/tonedark.png b/assets/images/index/tonedark.png new file mode 100644 index 00000000..29cf2247 Binary files /dev/null and b/assets/images/index/tonedark.png differ diff --git a/assets/images/index/tonelight.png b/assets/images/index/tonelight.png new file mode 100644 index 00000000..62f28af0 Binary files /dev/null and b/assets/images/index/tonelight.png differ diff --git a/assets/images/index/venuedb.png b/assets/images/index/venuedb.png new file mode 100644 index 00000000..f55de34c Binary files /dev/null and b/assets/images/index/venuedb.png differ diff --git a/assets/images/index/vnp.gif b/assets/images/index/vnp.gif new file mode 100644 index 00000000..f9c519e9 Binary files /dev/null and b/assets/images/index/vnp.gif differ diff --git a/assets/images/juggle/color.png b/assets/images/juggle/color.png new file mode 100644 index 00000000..e9c7c6d7 Binary files /dev/null and b/assets/images/juggle/color.png differ diff --git a/assets/images/juggle/dark.png b/assets/images/juggle/dark.png new file mode 100644 index 00000000..aa08b560 Binary files /dev/null and b/assets/images/juggle/dark.png differ diff --git a/assets/images/juggle/grey.png b/assets/images/juggle/grey.png new file mode 100644 index 00000000..d0b6f31f Binary files /dev/null and b/assets/images/juggle/grey.png differ diff --git a/assets/images/juggle/juggle.png b/assets/images/juggle/juggle.png new file mode 100644 index 00000000..3b04118a Binary files /dev/null and b/assets/images/juggle/juggle.png differ diff --git a/assets/images/juggle/light.png b/assets/images/juggle/light.png new file mode 100644 index 00000000..34be6b94 Binary files /dev/null and b/assets/images/juggle/light.png differ diff --git a/assets/images/learnedspec/arxiv_world_model_dark.png b/assets/images/learnedspec/arxiv_world_model_dark.png new file mode 100644 index 00000000..85842198 Binary files /dev/null and b/assets/images/learnedspec/arxiv_world_model_dark.png differ diff --git a/assets/images/learnedspec/arxiv_world_model_light.png b/assets/images/learnedspec/arxiv_world_model_light.png new file mode 100644 index 00000000..3eec3f36 Binary files /dev/null and b/assets/images/learnedspec/arxiv_world_model_light.png differ diff --git a/assets/images/learnedspec/fig1.png b/assets/images/learnedspec/fig1.png new file mode 100644 index 00000000..59537562 Binary files /dev/null and b/assets/images/learnedspec/fig1.png differ diff --git a/assets/images/learnedspec/fig2.png b/assets/images/learnedspec/fig2.png new file mode 100644 index 00000000..7f2c6a29 Binary files /dev/null and b/assets/images/learnedspec/fig2.png differ diff --git a/assets/images/learnedspec/hero.png b/assets/images/learnedspec/hero.png new file mode 100644 index 00000000..a3af569d Binary files /dev/null and b/assets/images/learnedspec/hero.png differ diff --git a/assets/images/learnedspec/rlddark.png b/assets/images/learnedspec/rlddark.png new file mode 100644 index 00000000..1621a754 Binary files /dev/null and b/assets/images/learnedspec/rlddark.png differ diff --git a/assets/images/learnedspec/rldlight.png b/assets/images/learnedspec/rldlight.png new file mode 100644 index 00000000..1f58f9eb Binary files /dev/null and b/assets/images/learnedspec/rldlight.png differ diff --git a/assets/images/learnedspec/world_models_trends_dark.png b/assets/images/learnedspec/world_models_trends_dark.png new file mode 100644 index 00000000..52457a46 Binary files /dev/null and b/assets/images/learnedspec/world_models_trends_dark.png differ diff --git a/assets/images/learnedspec/world_models_trends_light.png b/assets/images/learnedspec/world_models_trends_light.png new file mode 100644 index 00000000..b7c70db1 Binary files /dev/null and b/assets/images/learnedspec/world_models_trends_light.png differ diff --git a/assets/images/outlinedTeddyWarner.png b/assets/images/outlinedTeddyWarner.png new file mode 100644 index 00000000..87a62a14 Binary files /dev/null and b/assets/images/outlinedTeddyWarner.png differ diff --git a/assets/images/outlinedTeddyWarner.svg b/assets/images/outlinedTeddyWarner.svg new file mode 100644 index 00000000..a878552b --- /dev/null +++ b/assets/images/outlinedTeddyWarner.svg @@ -0,0 +1,98 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/thumb.png b/assets/images/thumb.png new file mode 100644 index 00000000..d98f79f6 Binary files /dev/null and b/assets/images/thumb.png differ diff --git a/assets/images/weather-night.svg b/assets/images/weather-night.svg new file mode 100644 index 00000000..d880912b --- /dev/null +++ b/assets/images/weather-night.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/weather-sunny.svg b/assets/images/weather-sunny.svg new file mode 100644 index 00000000..8f973304 --- /dev/null +++ b/assets/images/weather-sunny.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/javascripts/bundle.88dd0f4e.min.js b/assets/javascripts/bundle.88dd0f4e.min.js new file mode 100644 index 00000000..fb8f3109 --- /dev/null +++ b/assets/javascripts/bundle.88dd0f4e.min.js @@ -0,0 +1,16 @@ +"use strict";(()=>{var Wi=Object.create;var gr=Object.defineProperty;var Di=Object.getOwnPropertyDescriptor;var Vi=Object.getOwnPropertyNames,Vt=Object.getOwnPropertySymbols,Ni=Object.getPrototypeOf,yr=Object.prototype.hasOwnProperty,ao=Object.prototype.propertyIsEnumerable;var io=(e,t,r)=>t in e?gr(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,$=(e,t)=>{for(var r in t||(t={}))yr.call(t,r)&&io(e,r,t[r]);if(Vt)for(var r of Vt(t))ao.call(t,r)&&io(e,r,t[r]);return e};var so=(e,t)=>{var r={};for(var o in e)yr.call(e,o)&&t.indexOf(o)<0&&(r[o]=e[o]);if(e!=null&&Vt)for(var o of Vt(e))t.indexOf(o)<0&&ao.call(e,o)&&(r[o]=e[o]);return r};var xr=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var zi=(e,t,r,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of Vi(t))!yr.call(e,n)&&n!==r&&gr(e,n,{get:()=>t[n],enumerable:!(o=Di(t,n))||o.enumerable});return e};var Mt=(e,t,r)=>(r=e!=null?Wi(Ni(e)):{},zi(t||!e||!e.__esModule?gr(r,"default",{value:e,enumerable:!0}):r,e));var co=(e,t,r)=>new Promise((o,n)=>{var i=p=>{try{s(r.next(p))}catch(c){n(c)}},a=p=>{try{s(r.throw(p))}catch(c){n(c)}},s=p=>p.done?o(p.value):Promise.resolve(p.value).then(i,a);s((r=r.apply(e,t)).next())});var lo=xr((Er,po)=>{(function(e,t){typeof Er=="object"&&typeof po!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(Er,function(){"use strict";function e(r){var o=!0,n=!1,i=null,a={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function s(k){return!!(k&&k!==document&&k.nodeName!=="HTML"&&k.nodeName!=="BODY"&&"classList"in k&&"contains"in k.classList)}function p(k){var ft=k.type,qe=k.tagName;return!!(qe==="INPUT"&&a[ft]&&!k.readOnly||qe==="TEXTAREA"&&!k.readOnly||k.isContentEditable)}function c(k){k.classList.contains("focus-visible")||(k.classList.add("focus-visible"),k.setAttribute("data-focus-visible-added",""))}function l(k){k.hasAttribute("data-focus-visible-added")&&(k.classList.remove("focus-visible"),k.removeAttribute("data-focus-visible-added"))}function f(k){k.metaKey||k.altKey||k.ctrlKey||(s(r.activeElement)&&c(r.activeElement),o=!0)}function u(k){o=!1}function d(k){s(k.target)&&(o||p(k.target))&&c(k.target)}function y(k){s(k.target)&&(k.target.classList.contains("focus-visible")||k.target.hasAttribute("data-focus-visible-added"))&&(n=!0,window.clearTimeout(i),i=window.setTimeout(function(){n=!1},100),l(k.target))}function L(k){document.visibilityState==="hidden"&&(n&&(o=!0),X())}function X(){document.addEventListener("mousemove",J),document.addEventListener("mousedown",J),document.addEventListener("mouseup",J),document.addEventListener("pointermove",J),document.addEventListener("pointerdown",J),document.addEventListener("pointerup",J),document.addEventListener("touchmove",J),document.addEventListener("touchstart",J),document.addEventListener("touchend",J)}function te(){document.removeEventListener("mousemove",J),document.removeEventListener("mousedown",J),document.removeEventListener("mouseup",J),document.removeEventListener("pointermove",J),document.removeEventListener("pointerdown",J),document.removeEventListener("pointerup",J),document.removeEventListener("touchmove",J),document.removeEventListener("touchstart",J),document.removeEventListener("touchend",J)}function J(k){k.target.nodeName&&k.target.nodeName.toLowerCase()==="html"||(o=!1,te())}document.addEventListener("keydown",f,!0),document.addEventListener("mousedown",u,!0),document.addEventListener("pointerdown",u,!0),document.addEventListener("touchstart",u,!0),document.addEventListener("visibilitychange",L,!0),X(),r.addEventListener("focus",d,!0),r.addEventListener("blur",y,!0),r.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&r.host?r.host.setAttribute("data-js-focus-visible",""):r.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window!="undefined"&&typeof document!="undefined"){window.applyFocusVisiblePolyfill=e;var t;try{t=new CustomEvent("focus-visible-polyfill-ready")}catch(r){t=document.createEvent("CustomEvent"),t.initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(t)}typeof document!="undefined"&&e(document)})});var qr=xr((hy,On)=>{"use strict";/*! + * escape-html + * Copyright(c) 2012-2013 TJ Holowaychuk + * Copyright(c) 2015 Andreas Lubbe + * Copyright(c) 2015 Tiancheng "Timothy" Gu + * MIT Licensed + */var $a=/["'&<>]/;On.exports=Pa;function Pa(e){var t=""+e,r=$a.exec(t);if(!r)return t;var o,n="",i=0,a=0;for(i=r.index;i{/*! + * clipboard.js v2.0.11 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */(function(t,r){typeof It=="object"&&typeof Yr=="object"?Yr.exports=r():typeof define=="function"&&define.amd?define([],r):typeof It=="object"?It.ClipboardJS=r():t.ClipboardJS=r()})(It,function(){return function(){var e={686:function(o,n,i){"use strict";i.d(n,{default:function(){return Ui}});var a=i(279),s=i.n(a),p=i(370),c=i.n(p),l=i(817),f=i.n(l);function u(V){try{return document.execCommand(V)}catch(A){return!1}}var d=function(A){var M=f()(A);return u("cut"),M},y=d;function L(V){var A=document.documentElement.getAttribute("dir")==="rtl",M=document.createElement("textarea");M.style.fontSize="12pt",M.style.border="0",M.style.padding="0",M.style.margin="0",M.style.position="absolute",M.style[A?"right":"left"]="-9999px";var F=window.pageYOffset||document.documentElement.scrollTop;return M.style.top="".concat(F,"px"),M.setAttribute("readonly",""),M.value=V,M}var X=function(A,M){var F=L(A);M.container.appendChild(F);var D=f()(F);return u("copy"),F.remove(),D},te=function(A){var M=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},F="";return typeof A=="string"?F=X(A,M):A instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(A==null?void 0:A.type)?F=X(A.value,M):(F=f()(A),u("copy")),F},J=te;function k(V){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?k=function(M){return typeof M}:k=function(M){return M&&typeof Symbol=="function"&&M.constructor===Symbol&&M!==Symbol.prototype?"symbol":typeof M},k(V)}var ft=function(){var A=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},M=A.action,F=M===void 0?"copy":M,D=A.container,Y=A.target,$e=A.text;if(F!=="copy"&&F!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(Y!==void 0)if(Y&&k(Y)==="object"&&Y.nodeType===1){if(F==="copy"&&Y.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(F==="cut"&&(Y.hasAttribute("readonly")||Y.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if($e)return J($e,{container:D});if(Y)return F==="cut"?y(Y):J(Y,{container:D})},qe=ft;function Fe(V){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?Fe=function(M){return typeof M}:Fe=function(M){return M&&typeof Symbol=="function"&&M.constructor===Symbol&&M!==Symbol.prototype?"symbol":typeof M},Fe(V)}function ki(V,A){if(!(V instanceof A))throw new TypeError("Cannot call a class as a function")}function no(V,A){for(var M=0;M0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof D.action=="function"?D.action:this.defaultAction,this.target=typeof D.target=="function"?D.target:this.defaultTarget,this.text=typeof D.text=="function"?D.text:this.defaultText,this.container=Fe(D.container)==="object"?D.container:document.body}},{key:"listenClick",value:function(D){var Y=this;this.listener=c()(D,"click",function($e){return Y.onClick($e)})}},{key:"onClick",value:function(D){var Y=D.delegateTarget||D.currentTarget,$e=this.action(Y)||"copy",Dt=qe({action:$e,container:this.container,target:this.target(Y),text:this.text(Y)});this.emit(Dt?"success":"error",{action:$e,text:Dt,trigger:Y,clearSelection:function(){Y&&Y.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(D){return vr("action",D)}},{key:"defaultTarget",value:function(D){var Y=vr("target",D);if(Y)return document.querySelector(Y)}},{key:"defaultText",value:function(D){return vr("text",D)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(D){var Y=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return J(D,Y)}},{key:"cut",value:function(D){return y(D)}},{key:"isSupported",value:function(){var D=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],Y=typeof D=="string"?[D]:D,$e=!!document.queryCommandSupported;return Y.forEach(function(Dt){$e=$e&&!!document.queryCommandSupported(Dt)}),$e}}]),M}(s()),Ui=Fi},828:function(o){var n=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}function a(s,p){for(;s&&s.nodeType!==n;){if(typeof s.matches=="function"&&s.matches(p))return s;s=s.parentNode}}o.exports=a},438:function(o,n,i){var a=i(828);function s(l,f,u,d,y){var L=c.apply(this,arguments);return l.addEventListener(u,L,y),{destroy:function(){l.removeEventListener(u,L,y)}}}function p(l,f,u,d,y){return typeof l.addEventListener=="function"?s.apply(null,arguments):typeof u=="function"?s.bind(null,document).apply(null,arguments):(typeof l=="string"&&(l=document.querySelectorAll(l)),Array.prototype.map.call(l,function(L){return s(L,f,u,d,y)}))}function c(l,f,u,d){return function(y){y.delegateTarget=a(y.target,f),y.delegateTarget&&d.call(l,y)}}o.exports=p},879:function(o,n){n.node=function(i){return i!==void 0&&i instanceof HTMLElement&&i.nodeType===1},n.nodeList=function(i){var a=Object.prototype.toString.call(i);return i!==void 0&&(a==="[object NodeList]"||a==="[object HTMLCollection]")&&"length"in i&&(i.length===0||n.node(i[0]))},n.string=function(i){return typeof i=="string"||i instanceof String},n.fn=function(i){var a=Object.prototype.toString.call(i);return a==="[object Function]"}},370:function(o,n,i){var a=i(879),s=i(438);function p(u,d,y){if(!u&&!d&&!y)throw new Error("Missing required arguments");if(!a.string(d))throw new TypeError("Second argument must be a String");if(!a.fn(y))throw new TypeError("Third argument must be a Function");if(a.node(u))return c(u,d,y);if(a.nodeList(u))return l(u,d,y);if(a.string(u))return f(u,d,y);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function c(u,d,y){return u.addEventListener(d,y),{destroy:function(){u.removeEventListener(d,y)}}}function l(u,d,y){return Array.prototype.forEach.call(u,function(L){L.addEventListener(d,y)}),{destroy:function(){Array.prototype.forEach.call(u,function(L){L.removeEventListener(d,y)})}}}function f(u,d,y){return s(document.body,u,d,y)}o.exports=p},817:function(o){function n(i){var a;if(i.nodeName==="SELECT")i.focus(),a=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var s=i.hasAttribute("readonly");s||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),s||i.removeAttribute("readonly"),a=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var p=window.getSelection(),c=document.createRange();c.selectNodeContents(i),p.removeAllRanges(),p.addRange(c),a=p.toString()}return a}o.exports=n},279:function(o){function n(){}n.prototype={on:function(i,a,s){var p=this.e||(this.e={});return(p[i]||(p[i]=[])).push({fn:a,ctx:s}),this},once:function(i,a,s){var p=this;function c(){p.off(i,c),a.apply(s,arguments)}return c._=a,this.on(i,c,s)},emit:function(i){var a=[].slice.call(arguments,1),s=((this.e||(this.e={}))[i]||[]).slice(),p=0,c=s.length;for(p;p0&&i[i.length-1])&&(c[0]===6||c[0]===2)){r=0;continue}if(c[0]===3&&(!i||c[1]>i[0]&&c[1]=e.length&&(e=void 0),{value:e&&e[o++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function N(e,t){var r=typeof Symbol=="function"&&e[Symbol.iterator];if(!r)return e;var o=r.call(e),n,i=[],a;try{for(;(t===void 0||t-- >0)&&!(n=o.next()).done;)i.push(n.value)}catch(s){a={error:s}}finally{try{n&&!n.done&&(r=o.return)&&r.call(o)}finally{if(a)throw a.error}}return i}function q(e,t,r){if(r||arguments.length===2)for(var o=0,n=t.length,i;o1||p(d,L)})},y&&(n[d]=y(n[d])))}function p(d,y){try{c(o[d](y))}catch(L){u(i[0][3],L)}}function c(d){d.value instanceof nt?Promise.resolve(d.value.v).then(l,f):u(i[0][2],d)}function l(d){p("next",d)}function f(d){p("throw",d)}function u(d,y){d(y),i.shift(),i.length&&p(i[0][0],i[0][1])}}function uo(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t=e[Symbol.asyncIterator],r;return t?t.call(e):(e=typeof he=="function"?he(e):e[Symbol.iterator](),r={},o("next"),o("throw"),o("return"),r[Symbol.asyncIterator]=function(){return this},r);function o(i){r[i]=e[i]&&function(a){return new Promise(function(s,p){a=e[i](a),n(s,p,a.done,a.value)})}}function n(i,a,s,p){Promise.resolve(p).then(function(c){i({value:c,done:s})},a)}}function H(e){return typeof e=="function"}function ut(e){var t=function(o){Error.call(o),o.stack=new Error().stack},r=e(t);return r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r}var zt=ut(function(e){return function(r){e(this),this.message=r?r.length+` errors occurred during unsubscription: +`+r.map(function(o,n){return n+1+") "+o.toString()}).join(` + `):"",this.name="UnsubscriptionError",this.errors=r}});function Qe(e,t){if(e){var r=e.indexOf(t);0<=r&&e.splice(r,1)}}var Ue=function(){function e(t){this.initialTeardown=t,this.closed=!1,this._parentage=null,this._finalizers=null}return e.prototype.unsubscribe=function(){var t,r,o,n,i;if(!this.closed){this.closed=!0;var a=this._parentage;if(a)if(this._parentage=null,Array.isArray(a))try{for(var s=he(a),p=s.next();!p.done;p=s.next()){var c=p.value;c.remove(this)}}catch(L){t={error:L}}finally{try{p&&!p.done&&(r=s.return)&&r.call(s)}finally{if(t)throw t.error}}else a.remove(this);var l=this.initialTeardown;if(H(l))try{l()}catch(L){i=L instanceof zt?L.errors:[L]}var f=this._finalizers;if(f){this._finalizers=null;try{for(var u=he(f),d=u.next();!d.done;d=u.next()){var y=d.value;try{ho(y)}catch(L){i=i!=null?i:[],L instanceof zt?i=q(q([],N(i)),N(L.errors)):i.push(L)}}}catch(L){o={error:L}}finally{try{d&&!d.done&&(n=u.return)&&n.call(u)}finally{if(o)throw o.error}}}if(i)throw new zt(i)}},e.prototype.add=function(t){var r;if(t&&t!==this)if(this.closed)ho(t);else{if(t instanceof e){if(t.closed||t._hasParent(this))return;t._addParent(this)}(this._finalizers=(r=this._finalizers)!==null&&r!==void 0?r:[]).push(t)}},e.prototype._hasParent=function(t){var r=this._parentage;return r===t||Array.isArray(r)&&r.includes(t)},e.prototype._addParent=function(t){var r=this._parentage;this._parentage=Array.isArray(r)?(r.push(t),r):r?[r,t]:t},e.prototype._removeParent=function(t){var r=this._parentage;r===t?this._parentage=null:Array.isArray(r)&&Qe(r,t)},e.prototype.remove=function(t){var r=this._finalizers;r&&Qe(r,t),t instanceof e&&t._removeParent(this)},e.EMPTY=function(){var t=new e;return t.closed=!0,t}(),e}();var Tr=Ue.EMPTY;function qt(e){return e instanceof Ue||e&&"closed"in e&&H(e.remove)&&H(e.add)&&H(e.unsubscribe)}function ho(e){H(e)?e():e.unsubscribe()}var Pe={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var dt={setTimeout:function(e,t){for(var r=[],o=2;o0},enumerable:!1,configurable:!0}),t.prototype._trySubscribe=function(r){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,r)},t.prototype._subscribe=function(r){return this._throwIfClosed(),this._checkFinalizedStatuses(r),this._innerSubscribe(r)},t.prototype._innerSubscribe=function(r){var o=this,n=this,i=n.hasError,a=n.isStopped,s=n.observers;return i||a?Tr:(this.currentObservers=null,s.push(r),new Ue(function(){o.currentObservers=null,Qe(s,r)}))},t.prototype._checkFinalizedStatuses=function(r){var o=this,n=o.hasError,i=o.thrownError,a=o.isStopped;n?r.error(i):a&&r.complete()},t.prototype.asObservable=function(){var r=new j;return r.source=this,r},t.create=function(r,o){return new To(r,o)},t}(j);var To=function(e){oe(t,e);function t(r,o){var n=e.call(this)||this;return n.destination=r,n.source=o,n}return t.prototype.next=function(r){var o,n;(n=(o=this.destination)===null||o===void 0?void 0:o.next)===null||n===void 0||n.call(o,r)},t.prototype.error=function(r){var o,n;(n=(o=this.destination)===null||o===void 0?void 0:o.error)===null||n===void 0||n.call(o,r)},t.prototype.complete=function(){var r,o;(o=(r=this.destination)===null||r===void 0?void 0:r.complete)===null||o===void 0||o.call(r)},t.prototype._subscribe=function(r){var o,n;return(n=(o=this.source)===null||o===void 0?void 0:o.subscribe(r))!==null&&n!==void 0?n:Tr},t}(g);var _r=function(e){oe(t,e);function t(r){var o=e.call(this)||this;return o._value=r,o}return Object.defineProperty(t.prototype,"value",{get:function(){return this.getValue()},enumerable:!1,configurable:!0}),t.prototype._subscribe=function(r){var o=e.prototype._subscribe.call(this,r);return!o.closed&&r.next(this._value),o},t.prototype.getValue=function(){var r=this,o=r.hasError,n=r.thrownError,i=r._value;if(o)throw n;return this._throwIfClosed(),i},t.prototype.next=function(r){e.prototype.next.call(this,this._value=r)},t}(g);var At={now:function(){return(At.delegate||Date).now()},delegate:void 0};var Ct=function(e){oe(t,e);function t(r,o,n){r===void 0&&(r=1/0),o===void 0&&(o=1/0),n===void 0&&(n=At);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=o,i._timestampProvider=n,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=o===1/0,i._bufferSize=Math.max(1,r),i._windowTime=Math.max(1,o),i}return t.prototype.next=function(r){var o=this,n=o.isStopped,i=o._buffer,a=o._infiniteTimeWindow,s=o._timestampProvider,p=o._windowTime;n||(i.push(r),!a&&i.push(s.now()+p)),this._trimBuffer(),e.prototype.next.call(this,r)},t.prototype._subscribe=function(r){this._throwIfClosed(),this._trimBuffer();for(var o=this._innerSubscribe(r),n=this,i=n._infiniteTimeWindow,a=n._buffer,s=a.slice(),p=0;p0?e.prototype.schedule.call(this,r,o):(this.delay=o,this.state=r,this.scheduler.flush(this),this)},t.prototype.execute=function(r,o){return o>0||this.closed?e.prototype.execute.call(this,r,o):this._execute(r,o)},t.prototype.requestAsyncId=function(r,o,n){return n===void 0&&(n=0),n!=null&&n>0||n==null&&this.delay>0?e.prototype.requestAsyncId.call(this,r,o,n):(r.flush(this),0)},t}(gt);var Lo=function(e){oe(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t}(yt);var kr=new Lo(Oo);var Mo=function(e){oe(t,e);function t(r,o){var n=e.call(this,r,o)||this;return n.scheduler=r,n.work=o,n}return t.prototype.requestAsyncId=function(r,o,n){return n===void 0&&(n=0),n!==null&&n>0?e.prototype.requestAsyncId.call(this,r,o,n):(r.actions.push(this),r._scheduled||(r._scheduled=vt.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,o,n){var i;if(n===void 0&&(n=0),n!=null?n>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,r,o,n);var a=r.actions;o!=null&&((i=a[a.length-1])===null||i===void 0?void 0:i.id)!==o&&(vt.cancelAnimationFrame(o),r._scheduled=void 0)},t}(gt);var _o=function(e){oe(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var o=this._scheduled;this._scheduled=void 0;var n=this.actions,i;r=r||n.shift();do if(i=r.execute(r.state,r.delay))break;while((r=n[0])&&r.id===o&&n.shift());if(this._active=!1,i){for(;(r=n[0])&&r.id===o&&n.shift();)r.unsubscribe();throw i}},t}(yt);var me=new _o(Mo);var S=new j(function(e){return e.complete()});function Yt(e){return e&&H(e.schedule)}function Hr(e){return e[e.length-1]}function Xe(e){return H(Hr(e))?e.pop():void 0}function ke(e){return Yt(Hr(e))?e.pop():void 0}function Bt(e,t){return typeof Hr(e)=="number"?e.pop():t}var xt=function(e){return e&&typeof e.length=="number"&&typeof e!="function"};function Gt(e){return H(e==null?void 0:e.then)}function Jt(e){return H(e[bt])}function Xt(e){return Symbol.asyncIterator&&H(e==null?void 0:e[Symbol.asyncIterator])}function Zt(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function Zi(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var er=Zi();function tr(e){return H(e==null?void 0:e[er])}function rr(e){return fo(this,arguments,function(){var r,o,n,i;return Nt(this,function(a){switch(a.label){case 0:r=e.getReader(),a.label=1;case 1:a.trys.push([1,,9,10]),a.label=2;case 2:return[4,nt(r.read())];case 3:return o=a.sent(),n=o.value,i=o.done,i?[4,nt(void 0)]:[3,5];case 4:return[2,a.sent()];case 5:return[4,nt(n)];case 6:return[4,a.sent()];case 7:return a.sent(),[3,2];case 8:return[3,10];case 9:return r.releaseLock(),[7];case 10:return[2]}})})}function or(e){return H(e==null?void 0:e.getReader)}function U(e){if(e instanceof j)return e;if(e!=null){if(Jt(e))return ea(e);if(xt(e))return ta(e);if(Gt(e))return ra(e);if(Xt(e))return Ao(e);if(tr(e))return oa(e);if(or(e))return na(e)}throw Zt(e)}function ea(e){return new j(function(t){var r=e[bt]();if(H(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function ta(e){return new j(function(t){for(var r=0;r=2;return function(o){return o.pipe(e?b(function(n,i){return e(n,i,o)}):le,Te(1),r?De(t):Qo(function(){return new ir}))}}function jr(e){return e<=0?function(){return S}:E(function(t,r){var o=[];t.subscribe(T(r,function(n){o.push(n),e=2,!0))}function pe(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new g}:t,o=e.resetOnError,n=o===void 0?!0:o,i=e.resetOnComplete,a=i===void 0?!0:i,s=e.resetOnRefCountZero,p=s===void 0?!0:s;return function(c){var l,f,u,d=0,y=!1,L=!1,X=function(){f==null||f.unsubscribe(),f=void 0},te=function(){X(),l=u=void 0,y=L=!1},J=function(){var k=l;te(),k==null||k.unsubscribe()};return E(function(k,ft){d++,!L&&!y&&X();var qe=u=u!=null?u:r();ft.add(function(){d--,d===0&&!L&&!y&&(f=Ur(J,p))}),qe.subscribe(ft),!l&&d>0&&(l=new at({next:function(Fe){return qe.next(Fe)},error:function(Fe){L=!0,X(),f=Ur(te,n,Fe),qe.error(Fe)},complete:function(){y=!0,X(),f=Ur(te,a),qe.complete()}}),U(k).subscribe(l))})(c)}}function Ur(e,t){for(var r=[],o=2;oe.next(document)),e}function P(e,t=document){return Array.from(t.querySelectorAll(e))}function R(e,t=document){let r=fe(e,t);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${e}" to be present`);return r}function fe(e,t=document){return t.querySelector(e)||void 0}function Ie(){var e,t,r,o;return(o=(r=(t=(e=document.activeElement)==null?void 0:e.shadowRoot)==null?void 0:t.activeElement)!=null?r:document.activeElement)!=null?o:void 0}var wa=O(h(document.body,"focusin"),h(document.body,"focusout")).pipe(_e(1),Q(void 0),m(()=>Ie()||document.body),G(1));function et(e){return wa.pipe(m(t=>e.contains(t)),K())}function $t(e,t){return C(()=>O(h(e,"mouseenter").pipe(m(()=>!0)),h(e,"mouseleave").pipe(m(()=>!1))).pipe(t?Ht(r=>Le(+!r*t)):le,Q(e.matches(":hover"))))}function Jo(e,t){if(typeof t=="string"||typeof t=="number")e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(let r of t)Jo(e,r)}function x(e,t,...r){let o=document.createElement(e);if(t)for(let n of Object.keys(t))typeof t[n]!="undefined"&&(typeof t[n]!="boolean"?o.setAttribute(n,t[n]):o.setAttribute(n,""));for(let n of r)Jo(o,n);return o}function sr(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function Tt(e){let t=x("script",{src:e});return C(()=>(document.head.appendChild(t),O(h(t,"load"),h(t,"error").pipe(v(()=>$r(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(m(()=>{}),_(()=>document.head.removeChild(t)),Te(1))))}var Xo=new g,Ta=C(()=>typeof ResizeObserver=="undefined"?Tt("https://unpkg.com/resize-observer-polyfill"):I(void 0)).pipe(m(()=>new ResizeObserver(e=>e.forEach(t=>Xo.next(t)))),v(e=>O(Ye,I(e)).pipe(_(()=>e.disconnect()))),G(1));function ce(e){return{width:e.offsetWidth,height:e.offsetHeight}}function ge(e){let t=e;for(;t.clientWidth===0&&t.parentElement;)t=t.parentElement;return Ta.pipe(w(r=>r.observe(t)),v(r=>Xo.pipe(b(o=>o.target===t),_(()=>r.unobserve(t)))),m(()=>ce(e)),Q(ce(e)))}function St(e){return{width:e.scrollWidth,height:e.scrollHeight}}function cr(e){let t=e.parentElement;for(;t&&(e.scrollWidth<=t.scrollWidth&&e.scrollHeight<=t.scrollHeight);)t=(e=t).parentElement;return t?e:void 0}function Zo(e){let t=[],r=e.parentElement;for(;r;)(e.clientWidth>r.clientWidth||e.clientHeight>r.clientHeight)&&t.push(r),r=(e=r).parentElement;return t.length===0&&t.push(document.documentElement),t}function Ve(e){return{x:e.offsetLeft,y:e.offsetTop}}function en(e){let t=e.getBoundingClientRect();return{x:t.x+window.scrollX,y:t.y+window.scrollY}}function tn(e){return O(h(window,"load"),h(window,"resize")).pipe(Me(0,me),m(()=>Ve(e)),Q(Ve(e)))}function pr(e){return{x:e.scrollLeft,y:e.scrollTop}}function Ne(e){return O(h(e,"scroll"),h(window,"scroll"),h(window,"resize")).pipe(Me(0,me),m(()=>pr(e)),Q(pr(e)))}var rn=new g,Sa=C(()=>I(new IntersectionObserver(e=>{for(let t of e)rn.next(t)},{threshold:0}))).pipe(v(e=>O(Ye,I(e)).pipe(_(()=>e.disconnect()))),G(1));function tt(e){return Sa.pipe(w(t=>t.observe(e)),v(t=>rn.pipe(b(({target:r})=>r===e),_(()=>t.unobserve(e)),m(({isIntersecting:r})=>r))))}function on(e,t=16){return Ne(e).pipe(m(({y:r})=>{let o=ce(e),n=St(e);return r>=n.height-o.height-t}),K())}var lr={drawer:R("[data-md-toggle=drawer]"),search:R("[data-md-toggle=search]")};function nn(e){return lr[e].checked}function Je(e,t){lr[e].checked!==t&&lr[e].click()}function ze(e){let t=lr[e];return h(t,"change").pipe(m(()=>t.checked),Q(t.checked))}function Oa(e,t){switch(e.constructor){case HTMLInputElement:return e.type==="radio"?/^Arrow/.test(t):!0;case HTMLSelectElement:case HTMLTextAreaElement:return!0;default:return e.isContentEditable}}function La(){return O(h(window,"compositionstart").pipe(m(()=>!0)),h(window,"compositionend").pipe(m(()=>!1))).pipe(Q(!1))}function an(){let e=h(window,"keydown").pipe(b(t=>!(t.metaKey||t.ctrlKey)),m(t=>({mode:nn("search")?"search":"global",type:t.key,claim(){t.preventDefault(),t.stopPropagation()}})),b(({mode:t,type:r})=>{if(t==="global"){let o=Ie();if(typeof o!="undefined")return!Oa(o,r)}return!0}),pe());return La().pipe(v(t=>t?S:e))}function ye(){return new URL(location.href)}function lt(e,t=!1){if(B("navigation.instant")&&!t){let r=x("a",{href:e.href});document.body.appendChild(r),r.click(),r.remove()}else location.href=e.href}function sn(){return new g}function cn(){return location.hash.slice(1)}function pn(e){let t=x("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function Ma(e){return O(h(window,"hashchange"),e).pipe(m(cn),Q(cn()),b(t=>t.length>0),G(1))}function ln(e){return Ma(e).pipe(m(t=>fe(`[id="${t}"]`)),b(t=>typeof t!="undefined"))}function Pt(e){let t=matchMedia(e);return ar(r=>t.addListener(()=>r(t.matches))).pipe(Q(t.matches))}function mn(){let e=matchMedia("print");return O(h(window,"beforeprint").pipe(m(()=>!0)),h(window,"afterprint").pipe(m(()=>!1))).pipe(Q(e.matches))}function Nr(e,t){return e.pipe(v(r=>r?t():S))}function zr(e,t){return new j(r=>{let o=new XMLHttpRequest;return o.open("GET",`${e}`),o.responseType="blob",o.addEventListener("load",()=>{o.status>=200&&o.status<300?(r.next(o.response),r.complete()):r.error(new Error(o.statusText))}),o.addEventListener("error",()=>{r.error(new Error("Network error"))}),o.addEventListener("abort",()=>{r.complete()}),typeof(t==null?void 0:t.progress$)!="undefined"&&(o.addEventListener("progress",n=>{var i;if(n.lengthComputable)t.progress$.next(n.loaded/n.total*100);else{let a=(i=o.getResponseHeader("Content-Length"))!=null?i:0;t.progress$.next(n.loaded/+a*100)}}),t.progress$.next(5)),o.send(),()=>o.abort()})}function je(e,t){return zr(e,t).pipe(v(r=>r.text()),m(r=>JSON.parse(r)),G(1))}function fn(e,t){let r=new DOMParser;return zr(e,t).pipe(v(o=>o.text()),m(o=>r.parseFromString(o,"text/html")),G(1))}function un(e,t){let r=new DOMParser;return zr(e,t).pipe(v(o=>o.text()),m(o=>r.parseFromString(o,"text/xml")),G(1))}function dn(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function hn(){return O(h(window,"scroll",{passive:!0}),h(window,"resize",{passive:!0})).pipe(m(dn),Q(dn()))}function bn(){return{width:innerWidth,height:innerHeight}}function vn(){return h(window,"resize",{passive:!0}).pipe(m(bn),Q(bn()))}function gn(){return z([hn(),vn()]).pipe(m(([e,t])=>({offset:e,size:t})),G(1))}function mr(e,{viewport$:t,header$:r}){let o=t.pipe(ee("size")),n=z([o,r]).pipe(m(()=>Ve(e)));return z([r,t,n]).pipe(m(([{height:i},{offset:a,size:s},{x:p,y:c}])=>({offset:{x:a.x-p,y:a.y-c+i},size:s})))}function _a(e){return h(e,"message",t=>t.data)}function Aa(e){let t=new g;return t.subscribe(r=>e.postMessage(r)),t}function yn(e,t=new Worker(e)){let r=_a(t),o=Aa(t),n=new g;n.subscribe(o);let i=o.pipe(Z(),ie(!0));return n.pipe(Z(),Re(r.pipe(W(i))),pe())}var Ca=R("#__config"),Ot=JSON.parse(Ca.textContent);Ot.base=`${new URL(Ot.base,ye())}`;function xe(){return Ot}function B(e){return Ot.features.includes(e)}function Ee(e,t){return typeof t!="undefined"?Ot.translations[e].replace("#",t.toString()):Ot.translations[e]}function Se(e,t=document){return R(`[data-md-component=${e}]`,t)}function ae(e,t=document){return P(`[data-md-component=${e}]`,t)}function ka(e){let t=R(".md-typeset > :first-child",e);return h(t,"click",{once:!0}).pipe(m(()=>R(".md-typeset",e)),m(r=>({hash:__md_hash(r.innerHTML)})))}function xn(e){if(!B("announce.dismiss")||!e.childElementCount)return S;if(!e.hidden){let t=R(".md-typeset",e);__md_hash(t.innerHTML)===__md_get("__announce")&&(e.hidden=!0)}return C(()=>{let t=new g;return t.subscribe(({hash:r})=>{e.hidden=!0,__md_set("__announce",r)}),ka(e).pipe(w(r=>t.next(r)),_(()=>t.complete()),m(r=>$({ref:e},r)))})}function Ha(e,{target$:t}){return t.pipe(m(r=>({hidden:r!==e})))}function En(e,t){let r=new g;return r.subscribe(({hidden:o})=>{e.hidden=o}),Ha(e,t).pipe(w(o=>r.next(o)),_(()=>r.complete()),m(o=>$({ref:e},o)))}function Rt(e,t){return t==="inline"?x("div",{class:"md-tooltip md-tooltip--inline",id:e,role:"tooltip"},x("div",{class:"md-tooltip__inner md-typeset"})):x("div",{class:"md-tooltip",id:e,role:"tooltip"},x("div",{class:"md-tooltip__inner md-typeset"}))}function wn(...e){return x("div",{class:"md-tooltip2",role:"tooltip"},x("div",{class:"md-tooltip2__inner md-typeset"},e))}function Tn(e,t){if(t=t?`${t}_annotation_${e}`:void 0,t){let r=t?`#${t}`:void 0;return x("aside",{class:"md-annotation",tabIndex:0},Rt(t),x("a",{href:r,class:"md-annotation__index",tabIndex:-1},x("span",{"data-md-annotation-id":e})))}else return x("aside",{class:"md-annotation",tabIndex:0},Rt(t),x("span",{class:"md-annotation__index",tabIndex:-1},x("span",{"data-md-annotation-id":e})))}function Sn(e){return x("button",{class:"md-clipboard md-icon",title:Ee("clipboard.copy"),"data-clipboard-target":`#${e} > code`})}var Ln=Mt(qr());function Qr(e,t){let r=t&2,o=t&1,n=Object.keys(e.terms).filter(p=>!e.terms[p]).reduce((p,c)=>[...p,x("del",null,(0,Ln.default)(c))," "],[]).slice(0,-1),i=xe(),a=new URL(e.location,i.base);B("search.highlight")&&a.searchParams.set("h",Object.entries(e.terms).filter(([,p])=>p).reduce((p,[c])=>`${p} ${c}`.trim(),""));let{tags:s}=xe();return x("a",{href:`${a}`,class:"md-search-result__link",tabIndex:-1},x("article",{class:"md-search-result__article md-typeset","data-md-score":e.score.toFixed(2)},r>0&&x("div",{class:"md-search-result__icon md-icon"}),r>0&&x("h1",null,e.title),r<=0&&x("h2",null,e.title),o>0&&e.text.length>0&&e.text,e.tags&&x("nav",{class:"md-tags"},e.tags.map(p=>{let c=s?p in s?`md-tag-icon md-tag--${s[p]}`:"md-tag-icon":"";return x("span",{class:`md-tag ${c}`},p)})),o>0&&n.length>0&&x("p",{class:"md-search-result__terms"},Ee("search.result.term.missing"),": ",...n)))}function Mn(e){let t=e[0].score,r=[...e],o=xe(),n=r.findIndex(l=>!`${new URL(l.location,o.base)}`.includes("#")),[i]=r.splice(n,1),a=r.findIndex(l=>l.scoreQr(l,1)),...p.length?[x("details",{class:"md-search-result__more"},x("summary",{tabIndex:-1},x("div",null,p.length>0&&p.length===1?Ee("search.result.more.one"):Ee("search.result.more.other",p.length))),...p.map(l=>Qr(l,1)))]:[]];return x("li",{class:"md-search-result__item"},c)}function _n(e){return x("ul",{class:"md-source__facts"},Object.entries(e).map(([t,r])=>x("li",{class:`md-source__fact md-source__fact--${t}`},typeof r=="number"?sr(r):r)))}function Kr(e){let t=`tabbed-control tabbed-control--${e}`;return x("div",{class:t,hidden:!0},x("button",{class:"tabbed-button",tabIndex:-1,"aria-hidden":"true"}))}function An(e){return x("div",{class:"md-typeset__scrollwrap"},x("div",{class:"md-typeset__table"},e))}function Ra(e){var o;let t=xe(),r=new URL(`../${e.version}/`,t.base);return x("li",{class:"md-version__item"},x("a",{href:`${r}`,class:"md-version__link"},e.title,((o=t.version)==null?void 0:o.alias)&&e.aliases.length>0&&x("span",{class:"md-version__alias"},e.aliases[0])))}function Cn(e,t){var o;let r=xe();return e=e.filter(n=>{var i;return!((i=n.properties)!=null&&i.hidden)}),x("div",{class:"md-version"},x("button",{class:"md-version__current","aria-label":Ee("select.version")},t.title,((o=r.version)==null?void 0:o.alias)&&t.aliases.length>0&&x("span",{class:"md-version__alias"},t.aliases[0])),x("ul",{class:"md-version__list"},e.map(Ra)))}var Ia=0;function ja(e){let t=z([et(e),$t(e)]).pipe(m(([o,n])=>o||n),K()),r=C(()=>Zo(e)).pipe(ne(Ne),pt(1),He(t),m(()=>en(e)));return t.pipe(Ae(o=>o),v(()=>z([t,r])),m(([o,n])=>({active:o,offset:n})),pe())}function Fa(e,t){let{content$:r,viewport$:o}=t,n=`__tooltip2_${Ia++}`;return C(()=>{let i=new g,a=new _r(!1);i.pipe(Z(),ie(!1)).subscribe(a);let s=a.pipe(Ht(c=>Le(+!c*250,kr)),K(),v(c=>c?r:S),w(c=>c.id=n),pe());z([i.pipe(m(({active:c})=>c)),s.pipe(v(c=>$t(c,250)),Q(!1))]).pipe(m(c=>c.some(l=>l))).subscribe(a);let p=a.pipe(b(c=>c),re(s,o),m(([c,l,{size:f}])=>{let u=e.getBoundingClientRect(),d=u.width/2;if(l.role==="tooltip")return{x:d,y:8+u.height};if(u.y>=f.height/2){let{height:y}=ce(l);return{x:d,y:-16-y}}else return{x:d,y:16+u.height}}));return z([s,i,p]).subscribe(([c,{offset:l},f])=>{c.style.setProperty("--md-tooltip-host-x",`${l.x}px`),c.style.setProperty("--md-tooltip-host-y",`${l.y}px`),c.style.setProperty("--md-tooltip-x",`${f.x}px`),c.style.setProperty("--md-tooltip-y",`${f.y}px`),c.classList.toggle("md-tooltip2--top",f.y<0),c.classList.toggle("md-tooltip2--bottom",f.y>=0)}),a.pipe(b(c=>c),re(s,(c,l)=>l),b(c=>c.role==="tooltip")).subscribe(c=>{let l=ce(R(":scope > *",c));c.style.setProperty("--md-tooltip-width",`${l.width}px`),c.style.setProperty("--md-tooltip-tail","0px")}),a.pipe(K(),ve(me),re(s)).subscribe(([c,l])=>{l.classList.toggle("md-tooltip2--active",c)}),z([a.pipe(b(c=>c)),s]).subscribe(([c,l])=>{l.role==="dialog"?(e.setAttribute("aria-controls",n),e.setAttribute("aria-haspopup","dialog")):e.setAttribute("aria-describedby",n)}),a.pipe(b(c=>!c)).subscribe(()=>{e.removeAttribute("aria-controls"),e.removeAttribute("aria-describedby"),e.removeAttribute("aria-haspopup")}),ja(e).pipe(w(c=>i.next(c)),_(()=>i.complete()),m(c=>$({ref:e},c)))})}function mt(e,{viewport$:t},r=document.body){return Fa(e,{content$:new j(o=>{let n=e.title,i=wn(n);return o.next(i),e.removeAttribute("title"),r.append(i),()=>{i.remove(),e.setAttribute("title",n)}}),viewport$:t})}function Ua(e,t){let r=C(()=>z([tn(e),Ne(t)])).pipe(m(([{x:o,y:n},i])=>{let{width:a,height:s}=ce(e);return{x:o-i.x+a/2,y:n-i.y+s/2}}));return et(e).pipe(v(o=>r.pipe(m(n=>({active:o,offset:n})),Te(+!o||1/0))))}function kn(e,t,{target$:r}){let[o,n]=Array.from(e.children);return C(()=>{let i=new g,a=i.pipe(Z(),ie(!0));return i.subscribe({next({offset:s}){e.style.setProperty("--md-tooltip-x",`${s.x}px`),e.style.setProperty("--md-tooltip-y",`${s.y}px`)},complete(){e.style.removeProperty("--md-tooltip-x"),e.style.removeProperty("--md-tooltip-y")}}),tt(e).pipe(W(a)).subscribe(s=>{e.toggleAttribute("data-md-visible",s)}),O(i.pipe(b(({active:s})=>s)),i.pipe(_e(250),b(({active:s})=>!s))).subscribe({next({active:s}){s?e.prepend(o):o.remove()},complete(){e.prepend(o)}}),i.pipe(Me(16,me)).subscribe(({active:s})=>{o.classList.toggle("md-tooltip--active",s)}),i.pipe(pt(125,me),b(()=>!!e.offsetParent),m(()=>e.offsetParent.getBoundingClientRect()),m(({x:s})=>s)).subscribe({next(s){s?e.style.setProperty("--md-tooltip-0",`${-s}px`):e.style.removeProperty("--md-tooltip-0")},complete(){e.style.removeProperty("--md-tooltip-0")}}),h(n,"click").pipe(W(a),b(s=>!(s.metaKey||s.ctrlKey))).subscribe(s=>{s.stopPropagation(),s.preventDefault()}),h(n,"mousedown").pipe(W(a),re(i)).subscribe(([s,{active:p}])=>{var c;if(s.button!==0||s.metaKey||s.ctrlKey)s.preventDefault();else if(p){s.preventDefault();let l=e.parentElement.closest(".md-annotation");l instanceof HTMLElement?l.focus():(c=Ie())==null||c.blur()}}),r.pipe(W(a),b(s=>s===o),Ge(125)).subscribe(()=>e.focus()),Ua(e,t).pipe(w(s=>i.next(s)),_(()=>i.complete()),m(s=>$({ref:e},s)))})}function Wa(e){return e.tagName==="CODE"?P(".c, .c1, .cm",e):[e]}function Da(e){let t=[];for(let r of Wa(e)){let o=[],n=document.createNodeIterator(r,NodeFilter.SHOW_TEXT);for(let i=n.nextNode();i;i=n.nextNode())o.push(i);for(let i of o){let a;for(;a=/(\(\d+\))(!)?/.exec(i.textContent);){let[,s,p]=a;if(typeof p=="undefined"){let c=i.splitText(a.index);i=c.splitText(s.length),t.push(c)}else{i.textContent=s,t.push(i);break}}}}return t}function Hn(e,t){t.append(...Array.from(e.childNodes))}function fr(e,t,{target$:r,print$:o}){let n=t.closest("[id]"),i=n==null?void 0:n.id,a=new Map;for(let s of Da(t)){let[,p]=s.textContent.match(/\((\d+)\)/);fe(`:scope > li:nth-child(${p})`,e)&&(a.set(p,Tn(p,i)),s.replaceWith(a.get(p)))}return a.size===0?S:C(()=>{let s=new g,p=s.pipe(Z(),ie(!0)),c=[];for(let[l,f]of a)c.push([R(".md-typeset",f),R(`:scope > li:nth-child(${l})`,e)]);return o.pipe(W(p)).subscribe(l=>{e.hidden=!l,e.classList.toggle("md-annotation-list",l);for(let[f,u]of c)l?Hn(f,u):Hn(u,f)}),O(...[...a].map(([,l])=>kn(l,t,{target$:r}))).pipe(_(()=>s.complete()),pe())})}function $n(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return $n(t)}}function Pn(e,t){return C(()=>{let r=$n(e);return typeof r!="undefined"?fr(r,e,t):S})}var Rn=Mt(Br());var Va=0;function In(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return In(t)}}function Na(e){return ge(e).pipe(m(({width:t})=>({scrollable:St(e).width>t})),ee("scrollable"))}function jn(e,t){let{matches:r}=matchMedia("(hover)"),o=C(()=>{let n=new g,i=n.pipe(jr(1));n.subscribe(({scrollable:c})=>{c&&r?e.setAttribute("tabindex","0"):e.removeAttribute("tabindex")});let a=[];if(Rn.default.isSupported()&&(e.closest(".copy")||B("content.code.copy")&&!e.closest(".no-copy"))){let c=e.closest("pre");c.id=`__code_${Va++}`;let l=Sn(c.id);c.insertBefore(l,e),B("content.tooltips")&&a.push(mt(l,{viewport$}))}let s=e.closest(".highlight");if(s instanceof HTMLElement){let c=In(s);if(typeof c!="undefined"&&(s.classList.contains("annotate")||B("content.code.annotate"))){let l=fr(c,e,t);a.push(ge(s).pipe(W(i),m(({width:f,height:u})=>f&&u),K(),v(f=>f?l:S)))}}return P(":scope > span[id]",e).length&&e.classList.add("md-code__content"),Na(e).pipe(w(c=>n.next(c)),_(()=>n.complete()),m(c=>$({ref:e},c)),Re(...a))});return B("content.lazy")?tt(e).pipe(b(n=>n),Te(1),v(()=>o)):o}function za(e,{target$:t,print$:r}){let o=!0;return O(t.pipe(m(n=>n.closest("details:not([open])")),b(n=>e===n),m(()=>({action:"open",reveal:!0}))),r.pipe(b(n=>n||!o),w(()=>o=e.open),m(n=>({action:n?"open":"close"}))))}function Fn(e,t){return C(()=>{let r=new g;return r.subscribe(({action:o,reveal:n})=>{e.toggleAttribute("open",o==="open"),n&&e.scrollIntoView()}),za(e,t).pipe(w(o=>r.next(o)),_(()=>r.complete()),m(o=>$({ref:e},o)))})}var Un=".node circle,.node ellipse,.node path,.node polygon,.node rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}marker{fill:var(--md-mermaid-edge-color)!important}.edgeLabel .label rect{fill:#0000}.flowchartTitleText{fill:var(--md-mermaid-label-fg-color)}.label{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.label foreignObject{line-height:normal;overflow:visible}.label div .edgeLabel{color:var(--md-mermaid-label-fg-color)}.edgeLabel,.edgeLabel p,.label div .edgeLabel{background-color:var(--md-mermaid-label-bg-color)}.edgeLabel,.edgeLabel p{fill:var(--md-mermaid-label-bg-color);color:var(--md-mermaid-edge-color)}.edgePath .path,.flowchart-link{stroke:var(--md-mermaid-edge-color);stroke-width:.05rem}.edgePath .arrowheadPath{fill:var(--md-mermaid-edge-color);stroke:none}.cluster rect{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}.cluster span{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}g #flowchart-circleEnd,g #flowchart-circleStart,g #flowchart-crossEnd,g #flowchart-crossStart,g #flowchart-pointEnd,g #flowchart-pointStart{stroke:none}.classDiagramTitleText{fill:var(--md-mermaid-label-fg-color)}g.classGroup line,g.classGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.classGroup text{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.classLabel .box{fill:var(--md-mermaid-label-bg-color);background-color:var(--md-mermaid-label-bg-color);opacity:1}.classLabel .label{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.node .divider{stroke:var(--md-mermaid-node-fg-color)}.relation{stroke:var(--md-mermaid-edge-color)}.cardinality{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.cardinality text{fill:inherit!important}defs #classDiagram-compositionEnd,defs #classDiagram-compositionStart,defs #classDiagram-dependencyEnd,defs #classDiagram-dependencyStart,defs #classDiagram-extensionEnd,defs #classDiagram-extensionStart{fill:var(--md-mermaid-edge-color)!important;stroke:var(--md-mermaid-edge-color)!important}defs #classDiagram-aggregationEnd,defs #classDiagram-aggregationStart{fill:var(--md-mermaid-label-bg-color)!important;stroke:var(--md-mermaid-edge-color)!important}.statediagramTitleText{fill:var(--md-mermaid-label-fg-color)}g.stateGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.stateGroup .state-title{fill:var(--md-mermaid-label-fg-color)!important;font-family:var(--md-mermaid-font-family)}g.stateGroup .composit{fill:var(--md-mermaid-label-bg-color)}.nodeLabel,.nodeLabel p{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}a .nodeLabel{text-decoration:underline}.node circle.state-end,.node circle.state-start,.start-state{fill:var(--md-mermaid-edge-color);stroke:none}.end-state-inner,.end-state-outer{fill:var(--md-mermaid-edge-color)}.end-state-inner,.node circle.state-end{stroke:var(--md-mermaid-label-bg-color)}.transition{stroke:var(--md-mermaid-edge-color)}[id^=state-fork] rect,[id^=state-join] rect{fill:var(--md-mermaid-edge-color)!important;stroke:none!important}.statediagram-cluster.statediagram-cluster .inner{fill:var(--md-default-bg-color)}.statediagram-cluster rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}.statediagram-state rect.divider{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}defs #statediagram-barbEnd{stroke:var(--md-mermaid-edge-color)}.entityTitleText{fill:var(--md-mermaid-label-fg-color)}.attributeBoxEven,.attributeBoxOdd{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}.entityBox{fill:var(--md-mermaid-label-bg-color);stroke:var(--md-mermaid-node-fg-color)}.entityLabel{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.relationshipLabelBox{fill:var(--md-mermaid-label-bg-color);fill-opacity:1;background-color:var(--md-mermaid-label-bg-color);opacity:1}.relationshipLabel{fill:var(--md-mermaid-label-fg-color)}.relationshipLine{stroke:var(--md-mermaid-edge-color)}defs #ONE_OR_MORE_END *,defs #ONE_OR_MORE_START *,defs #ONLY_ONE_END *,defs #ONLY_ONE_START *,defs #ZERO_OR_MORE_END *,defs #ZERO_OR_MORE_START *,defs #ZERO_OR_ONE_END *,defs #ZERO_OR_ONE_START *{stroke:var(--md-mermaid-edge-color)!important}defs #ZERO_OR_MORE_END circle,defs #ZERO_OR_MORE_START circle{fill:var(--md-mermaid-label-bg-color)}text:not([class]):last-child{fill:var(--md-mermaid-label-fg-color)}.actor{fill:var(--md-mermaid-sequence-actor-bg-color);stroke:var(--md-mermaid-sequence-actor-border-color)}text.actor>tspan{fill:var(--md-mermaid-sequence-actor-fg-color);font-family:var(--md-mermaid-font-family)}line{stroke:var(--md-mermaid-sequence-actor-line-color)}.actor-man circle,.actor-man line{fill:var(--md-mermaid-sequence-actorman-bg-color);stroke:var(--md-mermaid-sequence-actorman-line-color)}.messageLine0,.messageLine1{stroke:var(--md-mermaid-sequence-message-line-color)}.note{fill:var(--md-mermaid-sequence-note-bg-color);stroke:var(--md-mermaid-sequence-note-border-color)}.loopText,.loopText>tspan,.messageText,.noteText>tspan{stroke:none;font-family:var(--md-mermaid-font-family)!important}.messageText{fill:var(--md-mermaid-sequence-message-fg-color)}.loopText,.loopText>tspan{fill:var(--md-mermaid-sequence-loop-fg-color)}.noteText>tspan{fill:var(--md-mermaid-sequence-note-fg-color)}#arrowhead path{fill:var(--md-mermaid-sequence-message-line-color);stroke:none}.loopLine{fill:var(--md-mermaid-sequence-loop-bg-color);stroke:var(--md-mermaid-sequence-loop-border-color)}.labelBox{fill:var(--md-mermaid-sequence-label-bg-color);stroke:none}.labelText,.labelText>span{fill:var(--md-mermaid-sequence-label-fg-color);font-family:var(--md-mermaid-font-family)}.sequenceNumber{fill:var(--md-mermaid-sequence-number-fg-color)}rect.rect{fill:var(--md-mermaid-sequence-box-bg-color);stroke:none}rect.rect+text.text{fill:var(--md-mermaid-sequence-box-fg-color)}defs #sequencenumber{fill:var(--md-mermaid-sequence-number-bg-color)!important}";var Gr,Qa=0;function Ka(){return typeof mermaid=="undefined"||mermaid instanceof Element?Tt("https://unpkg.com/mermaid@11/dist/mermaid.min.js"):I(void 0)}function Wn(e){return e.classList.remove("mermaid"),Gr||(Gr=Ka().pipe(w(()=>mermaid.initialize({startOnLoad:!1,themeCSS:Un,sequence:{actorFontSize:"16px",messageFontSize:"16px",noteFontSize:"16px"}})),m(()=>{}),G(1))),Gr.subscribe(()=>co(this,null,function*(){e.classList.add("mermaid");let t=`__mermaid_${Qa++}`,r=x("div",{class:"mermaid"}),o=e.textContent,{svg:n,fn:i}=yield mermaid.render(t,o),a=r.attachShadow({mode:"closed"});a.innerHTML=n,e.replaceWith(r),i==null||i(a)})),Gr.pipe(m(()=>({ref:e})))}var Dn=x("table");function Vn(e){return e.replaceWith(Dn),Dn.replaceWith(An(e)),I({ref:e})}function Ya(e){let t=e.find(r=>r.checked)||e[0];return O(...e.map(r=>h(r,"change").pipe(m(()=>R(`label[for="${r.id}"]`))))).pipe(Q(R(`label[for="${t.id}"]`)),m(r=>({active:r})))}function Nn(e,{viewport$:t,target$:r}){let o=R(".tabbed-labels",e),n=P(":scope > input",e),i=Kr("prev");e.append(i);let a=Kr("next");return e.append(a),C(()=>{let s=new g,p=s.pipe(Z(),ie(!0));z([s,ge(e),tt(e)]).pipe(W(p),Me(1,me)).subscribe({next([{active:c},l]){let f=Ve(c),{width:u}=ce(c);e.style.setProperty("--md-indicator-x",`${f.x}px`),e.style.setProperty("--md-indicator-width",`${u}px`);let d=pr(o);(f.xd.x+l.width)&&o.scrollTo({left:Math.max(0,f.x-16),behavior:"smooth"})},complete(){e.style.removeProperty("--md-indicator-x"),e.style.removeProperty("--md-indicator-width")}}),z([Ne(o),ge(o)]).pipe(W(p)).subscribe(([c,l])=>{let f=St(o);i.hidden=c.x<16,a.hidden=c.x>f.width-l.width-16}),O(h(i,"click").pipe(m(()=>-1)),h(a,"click").pipe(m(()=>1))).pipe(W(p)).subscribe(c=>{let{width:l}=ce(o);o.scrollBy({left:l*c,behavior:"smooth"})}),r.pipe(W(p),b(c=>n.includes(c))).subscribe(c=>c.click()),o.classList.add("tabbed-labels--linked");for(let c of n){let l=R(`label[for="${c.id}"]`);l.replaceChildren(x("a",{href:`#${l.htmlFor}`,tabIndex:-1},...Array.from(l.childNodes))),h(l.firstElementChild,"click").pipe(W(p),b(f=>!(f.metaKey||f.ctrlKey)),w(f=>{f.preventDefault(),f.stopPropagation()})).subscribe(()=>{history.replaceState({},"",`#${l.htmlFor}`),l.click()})}return B("content.tabs.link")&&s.pipe(Ce(1),re(t)).subscribe(([{active:c},{offset:l}])=>{let f=c.innerText.trim();if(c.hasAttribute("data-md-switching"))c.removeAttribute("data-md-switching");else{let u=e.offsetTop-l.y;for(let y of P("[data-tabs]"))for(let L of P(":scope > input",y)){let X=R(`label[for="${L.id}"]`);if(X!==c&&X.innerText.trim()===f){X.setAttribute("data-md-switching",""),L.click();break}}window.scrollTo({top:e.offsetTop-u});let d=__md_get("__tabs")||[];__md_set("__tabs",[...new Set([f,...d])])}}),s.pipe(W(p)).subscribe(()=>{for(let c of P("audio, video",e))c.pause()}),Ya(n).pipe(w(c=>s.next(c)),_(()=>s.complete()),m(c=>$({ref:e},c)))}).pipe(Ke(se))}function zn(e,{viewport$:t,target$:r,print$:o}){return O(...P(".annotate:not(.highlight)",e).map(n=>Pn(n,{target$:r,print$:o})),...P("pre:not(.mermaid) > code",e).map(n=>jn(n,{target$:r,print$:o})),...P("pre.mermaid",e).map(n=>Wn(n)),...P("table:not([class])",e).map(n=>Vn(n)),...P("details",e).map(n=>Fn(n,{target$:r,print$:o})),...P("[data-tabs]",e).map(n=>Nn(n,{viewport$:t,target$:r})),...P("[title]",e).filter(()=>B("content.tooltips")).map(n=>mt(n,{viewport$:t})))}function Ba(e,{alert$:t}){return t.pipe(v(r=>O(I(!0),I(!1).pipe(Ge(2e3))).pipe(m(o=>({message:r,active:o})))))}function qn(e,t){let r=R(".md-typeset",e);return C(()=>{let o=new g;return o.subscribe(({message:n,active:i})=>{e.classList.toggle("md-dialog--active",i),r.textContent=n}),Ba(e,t).pipe(w(n=>o.next(n)),_(()=>o.complete()),m(n=>$({ref:e},n)))})}var Ga=0;function Ja(e,t){document.body.append(e);let{width:r}=ce(e);e.style.setProperty("--md-tooltip-width",`${r}px`),e.remove();let o=cr(t),n=typeof o!="undefined"?Ne(o):I({x:0,y:0}),i=O(et(t),$t(t)).pipe(K());return z([i,n]).pipe(m(([a,s])=>{let{x:p,y:c}=Ve(t),l=ce(t),f=t.closest("table");return f&&t.parentElement&&(p+=f.offsetLeft+t.parentElement.offsetLeft,c+=f.offsetTop+t.parentElement.offsetTop),{active:a,offset:{x:p-s.x+l.width/2-r/2,y:c-s.y+l.height+8}}}))}function Qn(e){let t=e.title;if(!t.length)return S;let r=`__tooltip_${Ga++}`,o=Rt(r,"inline"),n=R(".md-typeset",o);return n.innerHTML=t,C(()=>{let i=new g;return i.subscribe({next({offset:a}){o.style.setProperty("--md-tooltip-x",`${a.x}px`),o.style.setProperty("--md-tooltip-y",`${a.y}px`)},complete(){o.style.removeProperty("--md-tooltip-x"),o.style.removeProperty("--md-tooltip-y")}}),O(i.pipe(b(({active:a})=>a)),i.pipe(_e(250),b(({active:a})=>!a))).subscribe({next({active:a}){a?(e.insertAdjacentElement("afterend",o),e.setAttribute("aria-describedby",r),e.removeAttribute("title")):(o.remove(),e.removeAttribute("aria-describedby"),e.setAttribute("title",t))},complete(){o.remove(),e.removeAttribute("aria-describedby"),e.setAttribute("title",t)}}),i.pipe(Me(16,me)).subscribe(({active:a})=>{o.classList.toggle("md-tooltip--active",a)}),i.pipe(pt(125,me),b(()=>!!e.offsetParent),m(()=>e.offsetParent.getBoundingClientRect()),m(({x:a})=>a)).subscribe({next(a){a?o.style.setProperty("--md-tooltip-0",`${-a}px`):o.style.removeProperty("--md-tooltip-0")},complete(){o.style.removeProperty("--md-tooltip-0")}}),Ja(o,e).pipe(w(a=>i.next(a)),_(()=>i.complete()),m(a=>$({ref:e},a)))}).pipe(Ke(se))}function Xa({viewport$:e}){if(!B("header.autohide"))return I(!1);let t=e.pipe(m(({offset:{y:n}})=>n),Be(2,1),m(([n,i])=>[nMath.abs(i-n.y)>100),m(([,[n]])=>n),K()),o=ze("search");return z([e,o]).pipe(m(([{offset:n},i])=>n.y>400&&!i),K(),v(n=>n?r:I(!1)),Q(!1))}function Kn(e,t){return C(()=>z([ge(e),Xa(t)])).pipe(m(([{height:r},o])=>({height:r,hidden:o})),K((r,o)=>r.height===o.height&&r.hidden===o.hidden),G(1))}function Yn(e,{header$:t,main$:r}){return C(()=>{let o=new g,n=o.pipe(Z(),ie(!0));o.pipe(ee("active"),He(t)).subscribe(([{active:a},{hidden:s}])=>{e.classList.toggle("md-header--shadow",a&&!s),e.hidden=s});let i=ue(P("[title]",e)).pipe(b(()=>B("content.tooltips")),ne(a=>Qn(a)));return r.subscribe(o),t.pipe(W(n),m(a=>$({ref:e},a)),Re(i.pipe(W(n))))})}function Za(e,{viewport$:t,header$:r}){return mr(e,{viewport$:t,header$:r}).pipe(m(({offset:{y:o}})=>{let{height:n}=ce(e);return{active:o>=n}}),ee("active"))}function Bn(e,t){return C(()=>{let r=new g;r.subscribe({next({active:n}){e.classList.toggle("md-header__title--active",n)},complete(){e.classList.remove("md-header__title--active")}});let o=fe(".md-content h1");return typeof o=="undefined"?S:Za(o,t).pipe(w(n=>r.next(n)),_(()=>r.complete()),m(n=>$({ref:e},n)))})}function Gn(e,{viewport$:t,header$:r}){let o=r.pipe(m(({height:i})=>i),K()),n=o.pipe(v(()=>ge(e).pipe(m(({height:i})=>({top:e.offsetTop,bottom:e.offsetTop+i})),ee("bottom"))));return z([o,n,t]).pipe(m(([i,{top:a,bottom:s},{offset:{y:p},size:{height:c}}])=>(c=Math.max(0,c-Math.max(0,a-p,i)-Math.max(0,c+p-s)),{offset:a-i,height:c,active:a-i<=p})),K((i,a)=>i.offset===a.offset&&i.height===a.height&&i.active===a.active))}function es(e){let t=__md_get("__palette")||{index:e.findIndex(o=>matchMedia(o.getAttribute("data-md-color-media")).matches)},r=Math.max(0,Math.min(t.index,e.length-1));return I(...e).pipe(ne(o=>h(o,"change").pipe(m(()=>o))),Q(e[r]),m(o=>({index:e.indexOf(o),color:{media:o.getAttribute("data-md-color-media"),scheme:o.getAttribute("data-md-color-scheme"),primary:o.getAttribute("data-md-color-primary"),accent:o.getAttribute("data-md-color-accent")}})),G(1))}function Jn(e){let t=P("input",e),r=x("meta",{name:"theme-color"});document.head.appendChild(r);let o=x("meta",{name:"color-scheme"});document.head.appendChild(o);let n=Pt("(prefers-color-scheme: light)");return C(()=>{let i=new g;return i.subscribe(a=>{if(document.body.setAttribute("data-md-color-switching",""),a.color.media==="(prefers-color-scheme)"){let s=matchMedia("(prefers-color-scheme: light)"),p=document.querySelector(s.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");a.color.scheme=p.getAttribute("data-md-color-scheme"),a.color.primary=p.getAttribute("data-md-color-primary"),a.color.accent=p.getAttribute("data-md-color-accent")}for(let[s,p]of Object.entries(a.color))document.body.setAttribute(`data-md-color-${s}`,p);for(let s=0;sa.key==="Enter"),re(i,(a,s)=>s)).subscribe(({index:a})=>{a=(a+1)%t.length,t[a].click(),t[a].focus()}),i.pipe(m(()=>{let a=Se("header"),s=window.getComputedStyle(a);return o.content=s.colorScheme,s.backgroundColor.match(/\d+/g).map(p=>(+p).toString(16).padStart(2,"0")).join("")})).subscribe(a=>r.content=`#${a}`),i.pipe(ve(se)).subscribe(()=>{document.body.removeAttribute("data-md-color-switching")}),es(t).pipe(W(n.pipe(Ce(1))),ct(),w(a=>i.next(a)),_(()=>i.complete()),m(a=>$({ref:e},a)))})}function Xn(e,{progress$:t}){return C(()=>{let r=new g;return r.subscribe(({value:o})=>{e.style.setProperty("--md-progress-value",`${o}`)}),t.pipe(w(o=>r.next({value:o})),_(()=>r.complete()),m(o=>({ref:e,value:o})))})}var Jr=Mt(Br());function ts(e){e.setAttribute("data-md-copying","");let t=e.closest("[data-copy]"),r=t?t.getAttribute("data-copy"):e.innerText;return e.removeAttribute("data-md-copying"),r.trimEnd()}function Zn({alert$:e}){Jr.default.isSupported()&&new j(t=>{new Jr.default("[data-clipboard-target], [data-clipboard-text]",{text:r=>r.getAttribute("data-clipboard-text")||ts(R(r.getAttribute("data-clipboard-target")))}).on("success",r=>t.next(r))}).pipe(w(t=>{t.trigger.focus()}),m(()=>Ee("clipboard.copied"))).subscribe(e)}function ei(e,t){return e.protocol=t.protocol,e.hostname=t.hostname,e}function rs(e,t){let r=new Map;for(let o of P("url",e)){let n=R("loc",o),i=[ei(new URL(n.textContent),t)];r.set(`${i[0]}`,i);for(let a of P("[rel=alternate]",o)){let s=a.getAttribute("href");s!=null&&i.push(ei(new URL(s),t))}}return r}function ur(e){return un(new URL("sitemap.xml",e)).pipe(m(t=>rs(t,new URL(e))),de(()=>I(new Map)))}function os(e,t){if(!(e.target instanceof Element))return S;let r=e.target.closest("a");if(r===null)return S;if(r.target||e.metaKey||e.ctrlKey)return S;let o=new URL(r.href);return o.search=o.hash="",t.has(`${o}`)?(e.preventDefault(),I(new URL(r.href))):S}function ti(e){let t=new Map;for(let r of P(":scope > *",e.head))t.set(r.outerHTML,r);return t}function ri(e){for(let t of P("[href], [src]",e))for(let r of["href","src"]){let o=t.getAttribute(r);if(o&&!/^(?:[a-z]+:)?\/\//i.test(o)){t[r]=t[r];break}}return I(e)}function ns(e){for(let o of["[data-md-component=announce]","[data-md-component=container]","[data-md-component=header-topic]","[data-md-component=outdated]","[data-md-component=logo]","[data-md-component=skip]",...B("navigation.tabs.sticky")?["[data-md-component=tabs]"]:[]]){let n=fe(o),i=fe(o,e);typeof n!="undefined"&&typeof i!="undefined"&&n.replaceWith(i)}let t=ti(document);for(let[o,n]of ti(e))t.has(o)?t.delete(o):document.head.appendChild(n);for(let o of t.values()){let n=o.getAttribute("name");n!=="theme-color"&&n!=="color-scheme"&&o.remove()}let r=Se("container");return We(P("script",r)).pipe(v(o=>{let n=e.createElement("script");if(o.src){for(let i of o.getAttributeNames())n.setAttribute(i,o.getAttribute(i));return o.replaceWith(n),new j(i=>{n.onload=()=>i.complete()})}else return n.textContent=o.textContent,o.replaceWith(n),S}),Z(),ie(document))}function oi({location$:e,viewport$:t,progress$:r}){let o=xe();if(location.protocol==="file:")return S;let n=ur(o.base);I(document).subscribe(ri);let i=h(document.body,"click").pipe(He(n),v(([p,c])=>os(p,c)),pe()),a=h(window,"popstate").pipe(m(ye),pe());i.pipe(re(t)).subscribe(([p,{offset:c}])=>{history.replaceState(c,""),history.pushState(null,"",p)}),O(i,a).subscribe(e);let s=e.pipe(ee("pathname"),v(p=>fn(p,{progress$:r}).pipe(de(()=>(lt(p,!0),S)))),v(ri),v(ns),pe());return O(s.pipe(re(e,(p,c)=>c)),s.pipe(v(()=>e),ee("pathname"),v(()=>e),ee("hash")),e.pipe(K((p,c)=>p.pathname===c.pathname&&p.hash===c.hash),v(()=>i),w(()=>history.back()))).subscribe(p=>{var c,l;history.state!==null||!p.hash?window.scrollTo(0,(l=(c=history.state)==null?void 0:c.y)!=null?l:0):(history.scrollRestoration="auto",pn(p.hash),history.scrollRestoration="manual")}),e.subscribe(()=>{history.scrollRestoration="manual"}),h(window,"beforeunload").subscribe(()=>{history.scrollRestoration="auto"}),t.pipe(ee("offset"),_e(100)).subscribe(({offset:p})=>{history.replaceState(p,"")}),s}var ni=Mt(qr());function ii(e){let t=e.separator.split("|").map(n=>n.replace(/(\(\?[!=<][^)]+\))/g,"").length===0?"\uFFFD":n).join("|"),r=new RegExp(t,"img"),o=(n,i,a)=>`${i}${a}`;return n=>{n=n.replace(/[\s*+\-:~^]+/g," ").trim();let i=new RegExp(`(^|${e.separator}|)(${n.replace(/[|\\{}()[\]^$+*?.-]/g,"\\$&").replace(r,"|")})`,"img");return a=>(0,ni.default)(a).replace(i,o).replace(/<\/mark>(\s+)]*>/img,"$1")}}function jt(e){return e.type===1}function dr(e){return e.type===3}function ai(e,t){let r=yn(e);return O(I(location.protocol!=="file:"),ze("search")).pipe(Ae(o=>o),v(()=>t)).subscribe(({config:o,docs:n})=>r.next({type:0,data:{config:o,docs:n,options:{suggest:B("search.suggest")}}})),r}function si(e){var l;let{selectedVersionSitemap:t,selectedVersionBaseURL:r,currentLocation:o,currentBaseURL:n}=e,i=(l=Xr(n))==null?void 0:l.pathname;if(i===void 0)return;let a=ss(o.pathname,i);if(a===void 0)return;let s=ps(t.keys());if(!t.has(s))return;let p=Xr(a,s);if(!p||!t.has(p.href))return;let c=Xr(a,r);if(c)return c.hash=o.hash,c.search=o.search,c}function Xr(e,t){try{return new URL(e,t)}catch(r){return}}function ss(e,t){if(e.startsWith(t))return e.slice(t.length)}function cs(e,t){let r=Math.min(e.length,t.length),o;for(o=0;oS)),o=r.pipe(m(n=>{let[,i]=t.base.match(/([^/]+)\/?$/);return n.find(({version:a,aliases:s})=>a===i||s.includes(i))||n[0]}));r.pipe(m(n=>new Map(n.map(i=>[`${new URL(`../${i.version}/`,t.base)}`,i]))),v(n=>h(document.body,"click").pipe(b(i=>!i.metaKey&&!i.ctrlKey),re(o),v(([i,a])=>{if(i.target instanceof Element){let s=i.target.closest("a");if(s&&!s.target&&n.has(s.href)){let p=s.href;return!i.target.closest(".md-version")&&n.get(p)===a?S:(i.preventDefault(),I(new URL(p)))}}return S}),v(i=>ur(i).pipe(m(a=>{var s;return(s=si({selectedVersionSitemap:a,selectedVersionBaseURL:i,currentLocation:ye(),currentBaseURL:t.base}))!=null?s:i})))))).subscribe(n=>lt(n,!0)),z([r,o]).subscribe(([n,i])=>{R(".md-header__topic").appendChild(Cn(n,i))}),e.pipe(v(()=>o)).subscribe(n=>{var a;let i=__md_get("__outdated",sessionStorage);if(i===null){i=!0;let s=((a=t.version)==null?void 0:a.default)||"latest";Array.isArray(s)||(s=[s]);e:for(let p of s)for(let c of n.aliases.concat(n.version))if(new RegExp(p,"i").test(c)){i=!1;break e}__md_set("__outdated",i,sessionStorage)}if(i)for(let s of ae("outdated"))s.hidden=!1})}function ls(e,{worker$:t}){let{searchParams:r}=ye();r.has("q")&&(Je("search",!0),e.value=r.get("q"),e.focus(),ze("search").pipe(Ae(i=>!i)).subscribe(()=>{let i=ye();i.searchParams.delete("q"),history.replaceState({},"",`${i}`)}));let o=et(e),n=O(t.pipe(Ae(jt)),h(e,"keyup"),o).pipe(m(()=>e.value),K());return z([n,o]).pipe(m(([i,a])=>({value:i,focus:a})),G(1))}function pi(e,{worker$:t}){let r=new g,o=r.pipe(Z(),ie(!0));z([t.pipe(Ae(jt)),r],(i,a)=>a).pipe(ee("value")).subscribe(({value:i})=>t.next({type:2,data:i})),r.pipe(ee("focus")).subscribe(({focus:i})=>{i&&Je("search",i)}),h(e.form,"reset").pipe(W(o)).subscribe(()=>e.focus());let n=R("header [for=__search]");return h(n,"click").subscribe(()=>e.focus()),ls(e,{worker$:t}).pipe(w(i=>r.next(i)),_(()=>r.complete()),m(i=>$({ref:e},i)),G(1))}function li(e,{worker$:t,query$:r}){let o=new g,n=on(e.parentElement).pipe(b(Boolean)),i=e.parentElement,a=R(":scope > :first-child",e),s=R(":scope > :last-child",e);ze("search").subscribe(l=>s.setAttribute("role",l?"list":"presentation")),o.pipe(re(r),Wr(t.pipe(Ae(jt)))).subscribe(([{items:l},{value:f}])=>{switch(l.length){case 0:a.textContent=f.length?Ee("search.result.none"):Ee("search.result.placeholder");break;case 1:a.textContent=Ee("search.result.one");break;default:let u=sr(l.length);a.textContent=Ee("search.result.other",u)}});let p=o.pipe(w(()=>s.innerHTML=""),v(({items:l})=>O(I(...l.slice(0,10)),I(...l.slice(10)).pipe(Be(4),Vr(n),v(([f])=>f)))),m(Mn),pe());return p.subscribe(l=>s.appendChild(l)),p.pipe(ne(l=>{let f=fe("details",l);return typeof f=="undefined"?S:h(f,"toggle").pipe(W(o),m(()=>f))})).subscribe(l=>{l.open===!1&&l.offsetTop<=i.scrollTop&&i.scrollTo({top:l.offsetTop})}),t.pipe(b(dr),m(({data:l})=>l)).pipe(w(l=>o.next(l)),_(()=>o.complete()),m(l=>$({ref:e},l)))}function ms(e,{query$:t}){return t.pipe(m(({value:r})=>{let o=ye();return o.hash="",r=r.replace(/\s+/g,"+").replace(/&/g,"%26").replace(/=/g,"%3D"),o.search=`q=${r}`,{url:o}}))}function mi(e,t){let r=new g,o=r.pipe(Z(),ie(!0));return r.subscribe(({url:n})=>{e.setAttribute("data-clipboard-text",e.href),e.href=`${n}`}),h(e,"click").pipe(W(o)).subscribe(n=>n.preventDefault()),ms(e,t).pipe(w(n=>r.next(n)),_(()=>r.complete()),m(n=>$({ref:e},n)))}function fi(e,{worker$:t,keyboard$:r}){let o=new g,n=Se("search-query"),i=O(h(n,"keydown"),h(n,"focus")).pipe(ve(se),m(()=>n.value),K());return o.pipe(He(i),m(([{suggest:s},p])=>{let c=p.split(/([\s-]+)/);if(s!=null&&s.length&&c[c.length-1]){let l=s[s.length-1];l.startsWith(c[c.length-1])&&(c[c.length-1]=l)}else c.length=0;return c})).subscribe(s=>e.innerHTML=s.join("").replace(/\s/g," ")),r.pipe(b(({mode:s})=>s==="search")).subscribe(s=>{switch(s.type){case"ArrowRight":e.innerText.length&&n.selectionStart===n.value.length&&(n.value=e.innerText);break}}),t.pipe(b(dr),m(({data:s})=>s)).pipe(w(s=>o.next(s)),_(()=>o.complete()),m(()=>({ref:e})))}function ui(e,{index$:t,keyboard$:r}){let o=xe();try{let n=ai(o.search,t),i=Se("search-query",e),a=Se("search-result",e);h(e,"click").pipe(b(({target:p})=>p instanceof Element&&!!p.closest("a"))).subscribe(()=>Je("search",!1)),r.pipe(b(({mode:p})=>p==="search")).subscribe(p=>{let c=Ie();switch(p.type){case"Enter":if(c===i){let l=new Map;for(let f of P(":first-child [href]",a)){let u=f.firstElementChild;l.set(f,parseFloat(u.getAttribute("data-md-score")))}if(l.size){let[[f]]=[...l].sort(([,u],[,d])=>d-u);f.click()}p.claim()}break;case"Escape":case"Tab":Je("search",!1),i.blur();break;case"ArrowUp":case"ArrowDown":if(typeof c=="undefined")i.focus();else{let l=[i,...P(":not(details) > [href], summary, details[open] [href]",a)],f=Math.max(0,(Math.max(0,l.indexOf(c))+l.length+(p.type==="ArrowUp"?-1:1))%l.length);l[f].focus()}p.claim();break;default:i!==Ie()&&i.focus()}}),r.pipe(b(({mode:p})=>p==="global")).subscribe(p=>{switch(p.type){case"f":case"s":case"/":i.focus(),i.select(),p.claim();break}});let s=pi(i,{worker$:n});return O(s,li(a,{worker$:n,query$:s})).pipe(Re(...ae("search-share",e).map(p=>mi(p,{query$:s})),...ae("search-suggest",e).map(p=>fi(p,{worker$:n,keyboard$:r}))))}catch(n){return e.hidden=!0,Ye}}function di(e,{index$:t,location$:r}){return z([t,r.pipe(Q(ye()),b(o=>!!o.searchParams.get("h")))]).pipe(m(([o,n])=>ii(o.config)(n.searchParams.get("h"))),m(o=>{var a;let n=new Map,i=document.createNodeIterator(e,NodeFilter.SHOW_TEXT);for(let s=i.nextNode();s;s=i.nextNode())if((a=s.parentElement)!=null&&a.offsetHeight){let p=s.textContent,c=o(p);c.length>p.length&&n.set(s,c)}for(let[s,p]of n){let{childNodes:c}=x("span",null,p);s.replaceWith(...Array.from(c))}return{ref:e,nodes:n}}))}function fs(e,{viewport$:t,main$:r}){let o=e.closest(".md-grid"),n=o.offsetTop-o.parentElement.offsetTop;return z([r,t]).pipe(m(([{offset:i,height:a},{offset:{y:s}}])=>(a=a+Math.min(n,Math.max(0,s-i))-n,{height:a,locked:s>=i+n})),K((i,a)=>i.height===a.height&&i.locked===a.locked))}function Zr(e,o){var n=o,{header$:t}=n,r=so(n,["header$"]);let i=R(".md-sidebar__scrollwrap",e),{y:a}=Ve(i);return C(()=>{let s=new g,p=s.pipe(Z(),ie(!0)),c=s.pipe(Me(0,me));return c.pipe(re(t)).subscribe({next([{height:l},{height:f}]){i.style.height=`${l-2*a}px`,e.style.top=`${f}px`},complete(){i.style.height="",e.style.top=""}}),c.pipe(Ae()).subscribe(()=>{for(let l of P(".md-nav__link--active[href]",e)){if(!l.clientHeight)continue;let f=l.closest(".md-sidebar__scrollwrap");if(typeof f!="undefined"){let u=l.offsetTop-f.offsetTop,{height:d}=ce(f);f.scrollTo({top:u-d/2})}}}),ue(P("label[tabindex]",e)).pipe(ne(l=>h(l,"click").pipe(ve(se),m(()=>l),W(p)))).subscribe(l=>{let f=R(`[id="${l.htmlFor}"]`);R(`[aria-labelledby="${l.id}"]`).setAttribute("aria-expanded",`${f.checked}`)}),fs(e,r).pipe(w(l=>s.next(l)),_(()=>s.complete()),m(l=>$({ref:e},l)))})}function hi(e,t){if(typeof t!="undefined"){let r=`https://api.github.com/repos/${e}/${t}`;return st(je(`${r}/releases/latest`).pipe(de(()=>S),m(o=>({version:o.tag_name})),De({})),je(r).pipe(de(()=>S),m(o=>({stars:o.stargazers_count,forks:o.forks_count})),De({}))).pipe(m(([o,n])=>$($({},o),n)))}else{let r=`https://api.github.com/users/${e}`;return je(r).pipe(m(o=>({repositories:o.public_repos})),De({}))}}function bi(e,t){let r=`https://${e}/api/v4/projects/${encodeURIComponent(t)}`;return st(je(`${r}/releases/permalink/latest`).pipe(de(()=>S),m(({tag_name:o})=>({version:o})),De({})),je(r).pipe(de(()=>S),m(({star_count:o,forks_count:n})=>({stars:o,forks:n})),De({}))).pipe(m(([o,n])=>$($({},o),n)))}function vi(e){let t=e.match(/^.+github\.com\/([^/]+)\/?([^/]+)?/i);if(t){let[,r,o]=t;return hi(r,o)}if(t=e.match(/^.+?([^/]*gitlab[^/]+)\/(.+?)\/?$/i),t){let[,r,o]=t;return bi(r,o)}return S}var us;function ds(e){return us||(us=C(()=>{let t=__md_get("__source",sessionStorage);if(t)return I(t);if(ae("consent").length){let o=__md_get("__consent");if(!(o&&o.github))return S}return vi(e.href).pipe(w(o=>__md_set("__source",o,sessionStorage)))}).pipe(de(()=>S),b(t=>Object.keys(t).length>0),m(t=>({facts:t})),G(1)))}function gi(e){let t=R(":scope > :last-child",e);return C(()=>{let r=new g;return r.subscribe(({facts:o})=>{t.appendChild(_n(o)),t.classList.add("md-source__repository--active")}),ds(e).pipe(w(o=>r.next(o)),_(()=>r.complete()),m(o=>$({ref:e},o)))})}function hs(e,{viewport$:t,header$:r}){return ge(document.body).pipe(v(()=>mr(e,{header$:r,viewport$:t})),m(({offset:{y:o}})=>({hidden:o>=10})),ee("hidden"))}function yi(e,t){return C(()=>{let r=new g;return r.subscribe({next({hidden:o}){e.hidden=o},complete(){e.hidden=!1}}),(B("navigation.tabs.sticky")?I({hidden:!1}):hs(e,t)).pipe(w(o=>r.next(o)),_(()=>r.complete()),m(o=>$({ref:e},o)))})}function bs(e,{viewport$:t,header$:r}){let o=new Map,n=P(".md-nav__link",e);for(let s of n){let p=decodeURIComponent(s.hash.substring(1)),c=fe(`[id="${p}"]`);typeof c!="undefined"&&o.set(s,c)}let i=r.pipe(ee("height"),m(({height:s})=>{let p=Se("main"),c=R(":scope > :first-child",p);return s+.8*(c.offsetTop-p.offsetTop)}),pe());return ge(document.body).pipe(ee("height"),v(s=>C(()=>{let p=[];return I([...o].reduce((c,[l,f])=>{for(;p.length&&o.get(p[p.length-1]).tagName>=f.tagName;)p.pop();let u=f.offsetTop;for(;!u&&f.parentElement;)f=f.parentElement,u=f.offsetTop;let d=f.offsetParent;for(;d;d=d.offsetParent)u+=d.offsetTop;return c.set([...p=[...p,l]].reverse(),u)},new Map))}).pipe(m(p=>new Map([...p].sort(([,c],[,l])=>c-l))),He(i),v(([p,c])=>t.pipe(Fr(([l,f],{offset:{y:u},size:d})=>{let y=u+d.height>=Math.floor(s.height);for(;f.length;){let[,L]=f[0];if(L-c=u&&!y)f=[l.pop(),...f];else break}return[l,f]},[[],[...p]]),K((l,f)=>l[0]===f[0]&&l[1]===f[1])))))).pipe(m(([s,p])=>({prev:s.map(([c])=>c),next:p.map(([c])=>c)})),Q({prev:[],next:[]}),Be(2,1),m(([s,p])=>s.prev.length{let i=new g,a=i.pipe(Z(),ie(!0));if(i.subscribe(({prev:s,next:p})=>{for(let[c]of p)c.classList.remove("md-nav__link--passed"),c.classList.remove("md-nav__link--active");for(let[c,[l]]of s.entries())l.classList.add("md-nav__link--passed"),l.classList.toggle("md-nav__link--active",c===s.length-1)}),B("toc.follow")){let s=O(t.pipe(_e(1),m(()=>{})),t.pipe(_e(250),m(()=>"smooth")));i.pipe(b(({prev:p})=>p.length>0),He(o.pipe(ve(se))),re(s)).subscribe(([[{prev:p}],c])=>{let[l]=p[p.length-1];if(l.offsetHeight){let f=cr(l);if(typeof f!="undefined"){let u=l.offsetTop-f.offsetTop,{height:d}=ce(f);f.scrollTo({top:u-d/2,behavior:c})}}})}return B("navigation.tracking")&&t.pipe(W(a),ee("offset"),_e(250),Ce(1),W(n.pipe(Ce(1))),ct({delay:250}),re(i)).subscribe(([,{prev:s}])=>{let p=ye(),c=s[s.length-1];if(c&&c.length){let[l]=c,{hash:f}=new URL(l.href);p.hash!==f&&(p.hash=f,history.replaceState({},"",`${p}`))}else p.hash="",history.replaceState({},"",`${p}`)}),bs(e,{viewport$:t,header$:r}).pipe(w(s=>i.next(s)),_(()=>i.complete()),m(s=>$({ref:e},s)))})}function vs(e,{viewport$:t,main$:r,target$:o}){let n=t.pipe(m(({offset:{y:a}})=>a),Be(2,1),m(([a,s])=>a>s&&s>0),K()),i=r.pipe(m(({active:a})=>a));return z([i,n]).pipe(m(([a,s])=>!(a&&s)),K(),W(o.pipe(Ce(1))),ie(!0),ct({delay:250}),m(a=>({hidden:a})))}function Ei(e,{viewport$:t,header$:r,main$:o,target$:n}){let i=new g,a=i.pipe(Z(),ie(!0));return i.subscribe({next({hidden:s}){e.hidden=s,s?(e.setAttribute("tabindex","-1"),e.blur()):e.removeAttribute("tabindex")},complete(){e.style.top="",e.hidden=!0,e.removeAttribute("tabindex")}}),r.pipe(W(a),ee("height")).subscribe(({height:s})=>{e.style.top=`${s+16}px`}),h(e,"click").subscribe(s=>{s.preventDefault(),window.scrollTo({top:0})}),vs(e,{viewport$:t,main$:o,target$:n}).pipe(w(s=>i.next(s)),_(()=>i.complete()),m(s=>$({ref:e},s)))}function wi({document$:e,viewport$:t}){e.pipe(v(()=>P(".md-ellipsis")),ne(r=>tt(r).pipe(W(e.pipe(Ce(1))),b(o=>o),m(()=>r),Te(1))),b(r=>r.offsetWidth{let o=r.innerText,n=r.closest("a")||r;return n.title=o,B("content.tooltips")?mt(n,{viewport$:t}).pipe(W(e.pipe(Ce(1))),_(()=>n.removeAttribute("title"))):S})).subscribe(),B("content.tooltips")&&e.pipe(v(()=>P(".md-status")),ne(r=>mt(r,{viewport$:t}))).subscribe()}function Ti({document$:e,tablet$:t}){e.pipe(v(()=>P(".md-toggle--indeterminate")),w(r=>{r.indeterminate=!0,r.checked=!1}),ne(r=>h(r,"change").pipe(Dr(()=>r.classList.contains("md-toggle--indeterminate")),m(()=>r))),re(t)).subscribe(([r,o])=>{r.classList.remove("md-toggle--indeterminate"),o&&(r.checked=!1)})}function gs(){return/(iPad|iPhone|iPod)/.test(navigator.userAgent)}function Si({document$:e}){e.pipe(v(()=>P("[data-md-scrollfix]")),w(t=>t.removeAttribute("data-md-scrollfix")),b(gs),ne(t=>h(t,"touchstart").pipe(m(()=>t)))).subscribe(t=>{let r=t.scrollTop;r===0?t.scrollTop=1:r+t.offsetHeight===t.scrollHeight&&(t.scrollTop=r-1)})}function Oi({viewport$:e,tablet$:t}){z([ze("search"),t]).pipe(m(([r,o])=>r&&!o),v(r=>I(r).pipe(Ge(r?400:100))),re(e)).subscribe(([r,{offset:{y:o}}])=>{if(r)document.body.setAttribute("data-md-scrolllock",""),document.body.style.top=`-${o}px`;else{let n=-1*parseInt(document.body.style.top,10);document.body.removeAttribute("data-md-scrolllock"),document.body.style.top="",n&&window.scrollTo(0,n)}})}Object.entries||(Object.entries=function(e){let t=[];for(let r of Object.keys(e))t.push([r,e[r]]);return t});Object.values||(Object.values=function(e){let t=[];for(let r of Object.keys(e))t.push(e[r]);return t});typeof Element!="undefined"&&(Element.prototype.scrollTo||(Element.prototype.scrollTo=function(e,t){typeof e=="object"?(this.scrollLeft=e.left,this.scrollTop=e.top):(this.scrollLeft=e,this.scrollTop=t)}),Element.prototype.replaceWith||(Element.prototype.replaceWith=function(...e){let t=this.parentNode;if(t){e.length===0&&t.removeChild(this);for(let r=e.length-1;r>=0;r--){let o=e[r];typeof o=="string"?o=document.createTextNode(o):o.parentNode&&o.parentNode.removeChild(o),r?t.insertBefore(this.previousSibling,o):t.replaceChild(o,this)}}}));function ys(){return location.protocol==="file:"?Tt(`${new URL("search/search_index.js",eo.base)}`).pipe(m(()=>__index),G(1)):je(new URL("search/search_index.json",eo.base))}document.documentElement.classList.remove("no-js");document.documentElement.classList.add("js");var ot=Go(),Ut=sn(),Lt=ln(Ut),to=an(),Oe=gn(),hr=Pt("(min-width: 960px)"),Mi=Pt("(min-width: 1220px)"),_i=mn(),eo=xe(),Ai=document.forms.namedItem("search")?ys():Ye,ro=new g;Zn({alert$:ro});var oo=new g;B("navigation.instant")&&oi({location$:Ut,viewport$:Oe,progress$:oo}).subscribe(ot);var Li;((Li=eo.version)==null?void 0:Li.provider)==="mike"&&ci({document$:ot});O(Ut,Lt).pipe(Ge(125)).subscribe(()=>{Je("drawer",!1),Je("search",!1)});to.pipe(b(({mode:e})=>e==="global")).subscribe(e=>{switch(e.type){case"p":case",":let t=fe("link[rel=prev]");typeof t!="undefined"&<(t);break;case"n":case".":let r=fe("link[rel=next]");typeof r!="undefined"&<(r);break;case"Enter":let o=Ie();o instanceof HTMLLabelElement&&o.click()}});wi({viewport$:Oe,document$:ot});Ti({document$:ot,tablet$:hr});Si({document$:ot});Oi({viewport$:Oe,tablet$:hr});var rt=Kn(Se("header"),{viewport$:Oe}),Ft=ot.pipe(m(()=>Se("main")),v(e=>Gn(e,{viewport$:Oe,header$:rt})),G(1)),xs=O(...ae("consent").map(e=>En(e,{target$:Lt})),...ae("dialog").map(e=>qn(e,{alert$:ro})),...ae("palette").map(e=>Jn(e)),...ae("progress").map(e=>Xn(e,{progress$:oo})),...ae("search").map(e=>ui(e,{index$:Ai,keyboard$:to})),...ae("source").map(e=>gi(e))),Es=C(()=>O(...ae("announce").map(e=>xn(e)),...ae("content").map(e=>zn(e,{viewport$:Oe,target$:Lt,print$:_i})),...ae("content").map(e=>B("search.highlight")?di(e,{index$:Ai,location$:Ut}):S),...ae("header").map(e=>Yn(e,{viewport$:Oe,header$:rt,main$:Ft})),...ae("header-title").map(e=>Bn(e,{viewport$:Oe,header$:rt})),...ae("sidebar").map(e=>e.getAttribute("data-md-type")==="navigation"?Nr(Mi,()=>Zr(e,{viewport$:Oe,header$:rt,main$:Ft})):Nr(hr,()=>Zr(e,{viewport$:Oe,header$:rt,main$:Ft}))),...ae("tabs").map(e=>yi(e,{viewport$:Oe,header$:rt})),...ae("toc").map(e=>xi(e,{viewport$:Oe,header$:rt,main$:Ft,target$:Lt})),...ae("top").map(e=>Ei(e,{viewport$:Oe,header$:rt,main$:Ft,target$:Lt})))),Ci=ot.pipe(v(()=>Es),Re(xs),G(1));Ci.subscribe();window.document$=ot;window.location$=Ut;window.target$=Lt;window.keyboard$=to;window.viewport$=Oe;window.tablet$=hr;window.screen$=Mi;window.print$=_i;window.alert$=ro;window.progress$=oo;window.component$=Ci;})(); +//# sourceMappingURL=bundle.88dd0f4e.min.js.map + diff --git a/assets/javascripts/bundle.88dd0f4e.min.js.map b/assets/javascripts/bundle.88dd0f4e.min.js.map new file mode 100644 index 00000000..dab2a875 --- /dev/null +++ b/assets/javascripts/bundle.88dd0f4e.min.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["node_modules/focus-visible/dist/focus-visible.js", "node_modules/escape-html/index.js", "node_modules/clipboard/dist/clipboard.js", "src/templates/assets/javascripts/bundle.ts", "node_modules/tslib/tslib.es6.mjs", "node_modules/rxjs/src/internal/util/isFunction.ts", "node_modules/rxjs/src/internal/util/createErrorClass.ts", "node_modules/rxjs/src/internal/util/UnsubscriptionError.ts", "node_modules/rxjs/src/internal/util/arrRemove.ts", "node_modules/rxjs/src/internal/Subscription.ts", "node_modules/rxjs/src/internal/config.ts", "node_modules/rxjs/src/internal/scheduler/timeoutProvider.ts", "node_modules/rxjs/src/internal/util/reportUnhandledError.ts", "node_modules/rxjs/src/internal/util/noop.ts", "node_modules/rxjs/src/internal/NotificationFactories.ts", "node_modules/rxjs/src/internal/util/errorContext.ts", "node_modules/rxjs/src/internal/Subscriber.ts", "node_modules/rxjs/src/internal/symbol/observable.ts", "node_modules/rxjs/src/internal/util/identity.ts", "node_modules/rxjs/src/internal/util/pipe.ts", "node_modules/rxjs/src/internal/Observable.ts", "node_modules/rxjs/src/internal/util/lift.ts", "node_modules/rxjs/src/internal/operators/OperatorSubscriber.ts", "node_modules/rxjs/src/internal/scheduler/animationFrameProvider.ts", "node_modules/rxjs/src/internal/util/ObjectUnsubscribedError.ts", "node_modules/rxjs/src/internal/Subject.ts", "node_modules/rxjs/src/internal/BehaviorSubject.ts", "node_modules/rxjs/src/internal/scheduler/dateTimestampProvider.ts", "node_modules/rxjs/src/internal/ReplaySubject.ts", "node_modules/rxjs/src/internal/scheduler/Action.ts", "node_modules/rxjs/src/internal/scheduler/intervalProvider.ts", "node_modules/rxjs/src/internal/scheduler/AsyncAction.ts", "node_modules/rxjs/src/internal/Scheduler.ts", "node_modules/rxjs/src/internal/scheduler/AsyncScheduler.ts", "node_modules/rxjs/src/internal/scheduler/async.ts", "node_modules/rxjs/src/internal/scheduler/QueueAction.ts", "node_modules/rxjs/src/internal/scheduler/QueueScheduler.ts", "node_modules/rxjs/src/internal/scheduler/queue.ts", "node_modules/rxjs/src/internal/scheduler/AnimationFrameAction.ts", "node_modules/rxjs/src/internal/scheduler/AnimationFrameScheduler.ts", "node_modules/rxjs/src/internal/scheduler/animationFrame.ts", "node_modules/rxjs/src/internal/observable/empty.ts", "node_modules/rxjs/src/internal/util/isScheduler.ts", "node_modules/rxjs/src/internal/util/args.ts", "node_modules/rxjs/src/internal/util/isArrayLike.ts", "node_modules/rxjs/src/internal/util/isPromise.ts", "node_modules/rxjs/src/internal/util/isInteropObservable.ts", "node_modules/rxjs/src/internal/util/isAsyncIterable.ts", "node_modules/rxjs/src/internal/util/throwUnobservableError.ts", "node_modules/rxjs/src/internal/symbol/iterator.ts", "node_modules/rxjs/src/internal/util/isIterable.ts", "node_modules/rxjs/src/internal/util/isReadableStreamLike.ts", "node_modules/rxjs/src/internal/observable/innerFrom.ts", "node_modules/rxjs/src/internal/util/executeSchedule.ts", "node_modules/rxjs/src/internal/operators/observeOn.ts", "node_modules/rxjs/src/internal/operators/subscribeOn.ts", "node_modules/rxjs/src/internal/scheduled/scheduleObservable.ts", "node_modules/rxjs/src/internal/scheduled/schedulePromise.ts", "node_modules/rxjs/src/internal/scheduled/scheduleArray.ts", "node_modules/rxjs/src/internal/scheduled/scheduleIterable.ts", "node_modules/rxjs/src/internal/scheduled/scheduleAsyncIterable.ts", "node_modules/rxjs/src/internal/scheduled/scheduleReadableStreamLike.ts", "node_modules/rxjs/src/internal/scheduled/scheduled.ts", "node_modules/rxjs/src/internal/observable/from.ts", "node_modules/rxjs/src/internal/observable/of.ts", "node_modules/rxjs/src/internal/observable/throwError.ts", "node_modules/rxjs/src/internal/util/EmptyError.ts", "node_modules/rxjs/src/internal/util/isDate.ts", "node_modules/rxjs/src/internal/operators/map.ts", "node_modules/rxjs/src/internal/util/mapOneOrManyArgs.ts", "node_modules/rxjs/src/internal/util/argsArgArrayOrObject.ts", "node_modules/rxjs/src/internal/util/createObject.ts", "node_modules/rxjs/src/internal/observable/combineLatest.ts", "node_modules/rxjs/src/internal/operators/mergeInternals.ts", "node_modules/rxjs/src/internal/operators/mergeMap.ts", "node_modules/rxjs/src/internal/operators/mergeAll.ts", "node_modules/rxjs/src/internal/operators/concatAll.ts", "node_modules/rxjs/src/internal/observable/concat.ts", "node_modules/rxjs/src/internal/observable/defer.ts", "node_modules/rxjs/src/internal/observable/fromEvent.ts", "node_modules/rxjs/src/internal/observable/fromEventPattern.ts", "node_modules/rxjs/src/internal/observable/timer.ts", "node_modules/rxjs/src/internal/observable/merge.ts", "node_modules/rxjs/src/internal/observable/never.ts", "node_modules/rxjs/src/internal/util/argsOrArgArray.ts", "node_modules/rxjs/src/internal/operators/filter.ts", "node_modules/rxjs/src/internal/observable/zip.ts", "node_modules/rxjs/src/internal/operators/audit.ts", "node_modules/rxjs/src/internal/operators/auditTime.ts", "node_modules/rxjs/src/internal/operators/bufferCount.ts", "node_modules/rxjs/src/internal/operators/catchError.ts", "node_modules/rxjs/src/internal/operators/scanInternals.ts", "node_modules/rxjs/src/internal/operators/combineLatest.ts", "node_modules/rxjs/src/internal/operators/combineLatestWith.ts", "node_modules/rxjs/src/internal/operators/debounce.ts", "node_modules/rxjs/src/internal/operators/debounceTime.ts", "node_modules/rxjs/src/internal/operators/defaultIfEmpty.ts", "node_modules/rxjs/src/internal/operators/take.ts", "node_modules/rxjs/src/internal/operators/ignoreElements.ts", "node_modules/rxjs/src/internal/operators/mapTo.ts", "node_modules/rxjs/src/internal/operators/delayWhen.ts", "node_modules/rxjs/src/internal/operators/delay.ts", "node_modules/rxjs/src/internal/operators/distinctUntilChanged.ts", "node_modules/rxjs/src/internal/operators/distinctUntilKeyChanged.ts", "node_modules/rxjs/src/internal/operators/throwIfEmpty.ts", "node_modules/rxjs/src/internal/operators/endWith.ts", "node_modules/rxjs/src/internal/operators/finalize.ts", "node_modules/rxjs/src/internal/operators/first.ts", "node_modules/rxjs/src/internal/operators/takeLast.ts", "node_modules/rxjs/src/internal/operators/merge.ts", "node_modules/rxjs/src/internal/operators/mergeWith.ts", "node_modules/rxjs/src/internal/operators/repeat.ts", "node_modules/rxjs/src/internal/operators/scan.ts", "node_modules/rxjs/src/internal/operators/share.ts", "node_modules/rxjs/src/internal/operators/shareReplay.ts", "node_modules/rxjs/src/internal/operators/skip.ts", "node_modules/rxjs/src/internal/operators/skipUntil.ts", "node_modules/rxjs/src/internal/operators/startWith.ts", "node_modules/rxjs/src/internal/operators/switchMap.ts", "node_modules/rxjs/src/internal/operators/takeUntil.ts", "node_modules/rxjs/src/internal/operators/takeWhile.ts", "node_modules/rxjs/src/internal/operators/tap.ts", "node_modules/rxjs/src/internal/operators/throttle.ts", "node_modules/rxjs/src/internal/operators/throttleTime.ts", "node_modules/rxjs/src/internal/operators/withLatestFrom.ts", "node_modules/rxjs/src/internal/operators/zip.ts", "node_modules/rxjs/src/internal/operators/zipWith.ts", "src/templates/assets/javascripts/browser/document/index.ts", "src/templates/assets/javascripts/browser/element/_/index.ts", "src/templates/assets/javascripts/browser/element/focus/index.ts", "src/templates/assets/javascripts/browser/element/hover/index.ts", "src/templates/assets/javascripts/utilities/h/index.ts", "src/templates/assets/javascripts/utilities/round/index.ts", "src/templates/assets/javascripts/browser/script/index.ts", "src/templates/assets/javascripts/browser/element/size/_/index.ts", "src/templates/assets/javascripts/browser/element/size/content/index.ts", "src/templates/assets/javascripts/browser/element/offset/_/index.ts", "src/templates/assets/javascripts/browser/element/offset/content/index.ts", "src/templates/assets/javascripts/browser/element/visibility/index.ts", "src/templates/assets/javascripts/browser/toggle/index.ts", "src/templates/assets/javascripts/browser/keyboard/index.ts", "src/templates/assets/javascripts/browser/location/_/index.ts", "src/templates/assets/javascripts/browser/location/hash/index.ts", "src/templates/assets/javascripts/browser/media/index.ts", "src/templates/assets/javascripts/browser/request/index.ts", "src/templates/assets/javascripts/browser/viewport/offset/index.ts", "src/templates/assets/javascripts/browser/viewport/size/index.ts", "src/templates/assets/javascripts/browser/viewport/_/index.ts", "src/templates/assets/javascripts/browser/viewport/at/index.ts", "src/templates/assets/javascripts/browser/worker/index.ts", "src/templates/assets/javascripts/_/index.ts", "src/templates/assets/javascripts/components/_/index.ts", "src/templates/assets/javascripts/components/announce/index.ts", "src/templates/assets/javascripts/components/consent/index.ts", "src/templates/assets/javascripts/templates/tooltip/index.tsx", "src/templates/assets/javascripts/templates/annotation/index.tsx", "src/templates/assets/javascripts/templates/clipboard/index.tsx", "src/templates/assets/javascripts/templates/search/index.tsx", "src/templates/assets/javascripts/templates/source/index.tsx", "src/templates/assets/javascripts/templates/tabbed/index.tsx", "src/templates/assets/javascripts/templates/table/index.tsx", "src/templates/assets/javascripts/templates/version/index.tsx", "src/templates/assets/javascripts/components/tooltip2/index.ts", "src/templates/assets/javascripts/components/content/annotation/_/index.ts", "src/templates/assets/javascripts/components/content/annotation/list/index.ts", "src/templates/assets/javascripts/components/content/annotation/block/index.ts", "src/templates/assets/javascripts/components/content/code/_/index.ts", "src/templates/assets/javascripts/components/content/details/index.ts", "src/templates/assets/javascripts/components/content/mermaid/index.css", "src/templates/assets/javascripts/components/content/mermaid/index.ts", "src/templates/assets/javascripts/components/content/table/index.ts", "src/templates/assets/javascripts/components/content/tabs/index.ts", "src/templates/assets/javascripts/components/content/_/index.ts", "src/templates/assets/javascripts/components/dialog/index.ts", "src/templates/assets/javascripts/components/tooltip/index.ts", "src/templates/assets/javascripts/components/header/_/index.ts", "src/templates/assets/javascripts/components/header/title/index.ts", "src/templates/assets/javascripts/components/main/index.ts", "src/templates/assets/javascripts/components/palette/index.ts", "src/templates/assets/javascripts/components/progress/index.ts", "src/templates/assets/javascripts/integrations/clipboard/index.ts", "src/templates/assets/javascripts/integrations/sitemap/index.ts", "src/templates/assets/javascripts/integrations/instant/index.ts", "src/templates/assets/javascripts/integrations/search/highlighter/index.ts", "src/templates/assets/javascripts/integrations/search/worker/message/index.ts", "src/templates/assets/javascripts/integrations/search/worker/_/index.ts", "src/templates/assets/javascripts/integrations/version/findurl/index.ts", "src/templates/assets/javascripts/integrations/version/index.ts", "src/templates/assets/javascripts/components/search/query/index.ts", "src/templates/assets/javascripts/components/search/result/index.ts", "src/templates/assets/javascripts/components/search/share/index.ts", "src/templates/assets/javascripts/components/search/suggest/index.ts", "src/templates/assets/javascripts/components/search/_/index.ts", "src/templates/assets/javascripts/components/search/highlight/index.ts", "src/templates/assets/javascripts/components/sidebar/index.ts", "src/templates/assets/javascripts/components/source/facts/github/index.ts", "src/templates/assets/javascripts/components/source/facts/gitlab/index.ts", "src/templates/assets/javascripts/components/source/facts/_/index.ts", "src/templates/assets/javascripts/components/source/_/index.ts", "src/templates/assets/javascripts/components/tabs/index.ts", "src/templates/assets/javascripts/components/toc/index.ts", "src/templates/assets/javascripts/components/top/index.ts", "src/templates/assets/javascripts/patches/ellipsis/index.ts", "src/templates/assets/javascripts/patches/indeterminate/index.ts", "src/templates/assets/javascripts/patches/scrollfix/index.ts", "src/templates/assets/javascripts/patches/scrolllock/index.ts", "src/templates/assets/javascripts/polyfills/index.ts"], + "sourcesContent": ["(function (global, factory) {\n typeof exports === 'object' && typeof module !== 'undefined' ? factory() :\n typeof define === 'function' && define.amd ? define(factory) :\n (factory());\n}(this, (function () { 'use strict';\n\n /**\n * Applies the :focus-visible polyfill at the given scope.\n * A scope in this case is either the top-level Document or a Shadow Root.\n *\n * @param {(Document|ShadowRoot)} scope\n * @see https://github.com/WICG/focus-visible\n */\n function applyFocusVisiblePolyfill(scope) {\n var hadKeyboardEvent = true;\n var hadFocusVisibleRecently = false;\n var hadFocusVisibleRecentlyTimeout = null;\n\n var inputTypesAllowlist = {\n text: true,\n search: true,\n url: true,\n tel: true,\n email: true,\n password: true,\n number: true,\n date: true,\n month: true,\n week: true,\n time: true,\n datetime: true,\n 'datetime-local': true\n };\n\n /**\n * Helper function for legacy browsers and iframes which sometimes focus\n * elements like document, body, and non-interactive SVG.\n * @param {Element} el\n */\n function isValidFocusTarget(el) {\n if (\n el &&\n el !== document &&\n el.nodeName !== 'HTML' &&\n el.nodeName !== 'BODY' &&\n 'classList' in el &&\n 'contains' in el.classList\n ) {\n return true;\n }\n return false;\n }\n\n /**\n * Computes whether the given element should automatically trigger the\n * `focus-visible` class being added, i.e. whether it should always match\n * `:focus-visible` when focused.\n * @param {Element} el\n * @return {boolean}\n */\n function focusTriggersKeyboardModality(el) {\n var type = el.type;\n var tagName = el.tagName;\n\n if (tagName === 'INPUT' && inputTypesAllowlist[type] && !el.readOnly) {\n return true;\n }\n\n if (tagName === 'TEXTAREA' && !el.readOnly) {\n return true;\n }\n\n if (el.isContentEditable) {\n return true;\n }\n\n return false;\n }\n\n /**\n * Add the `focus-visible` class to the given element if it was not added by\n * the author.\n * @param {Element} el\n */\n function addFocusVisibleClass(el) {\n if (el.classList.contains('focus-visible')) {\n return;\n }\n el.classList.add('focus-visible');\n el.setAttribute('data-focus-visible-added', '');\n }\n\n /**\n * Remove the `focus-visible` class from the given element if it was not\n * originally added by the author.\n * @param {Element} el\n */\n function removeFocusVisibleClass(el) {\n if (!el.hasAttribute('data-focus-visible-added')) {\n return;\n }\n el.classList.remove('focus-visible');\n el.removeAttribute('data-focus-visible-added');\n }\n\n /**\n * If the most recent user interaction was via the keyboard;\n * and the key press did not include a meta, alt/option, or control key;\n * then the modality is keyboard. Otherwise, the modality is not keyboard.\n * Apply `focus-visible` to any current active element and keep track\n * of our keyboard modality state with `hadKeyboardEvent`.\n * @param {KeyboardEvent} e\n */\n function onKeyDown(e) {\n if (e.metaKey || e.altKey || e.ctrlKey) {\n return;\n }\n\n if (isValidFocusTarget(scope.activeElement)) {\n addFocusVisibleClass(scope.activeElement);\n }\n\n hadKeyboardEvent = true;\n }\n\n /**\n * If at any point a user clicks with a pointing device, ensure that we change\n * the modality away from keyboard.\n * This avoids the situation where a user presses a key on an already focused\n * element, and then clicks on a different element, focusing it with a\n * pointing device, while we still think we're in keyboard modality.\n * @param {Event} e\n */\n function onPointerDown(e) {\n hadKeyboardEvent = false;\n }\n\n /**\n * On `focus`, add the `focus-visible` class to the target if:\n * - the target received focus as a result of keyboard navigation, or\n * - the event target is an element that will likely require interaction\n * via the keyboard (e.g. a text box)\n * @param {Event} e\n */\n function onFocus(e) {\n // Prevent IE from focusing the document or HTML element.\n if (!isValidFocusTarget(e.target)) {\n return;\n }\n\n if (hadKeyboardEvent || focusTriggersKeyboardModality(e.target)) {\n addFocusVisibleClass(e.target);\n }\n }\n\n /**\n * On `blur`, remove the `focus-visible` class from the target.\n * @param {Event} e\n */\n function onBlur(e) {\n if (!isValidFocusTarget(e.target)) {\n return;\n }\n\n if (\n e.target.classList.contains('focus-visible') ||\n e.target.hasAttribute('data-focus-visible-added')\n ) {\n // To detect a tab/window switch, we look for a blur event followed\n // rapidly by a visibility change.\n // If we don't see a visibility change within 100ms, it's probably a\n // regular focus change.\n hadFocusVisibleRecently = true;\n window.clearTimeout(hadFocusVisibleRecentlyTimeout);\n hadFocusVisibleRecentlyTimeout = window.setTimeout(function() {\n hadFocusVisibleRecently = false;\n }, 100);\n removeFocusVisibleClass(e.target);\n }\n }\n\n /**\n * If the user changes tabs, keep track of whether or not the previously\n * focused element had .focus-visible.\n * @param {Event} e\n */\n function onVisibilityChange(e) {\n if (document.visibilityState === 'hidden') {\n // If the tab becomes active again, the browser will handle calling focus\n // on the element (Safari actually calls it twice).\n // If this tab change caused a blur on an element with focus-visible,\n // re-apply the class when the user switches back to the tab.\n if (hadFocusVisibleRecently) {\n hadKeyboardEvent = true;\n }\n addInitialPointerMoveListeners();\n }\n }\n\n /**\n * Add a group of listeners to detect usage of any pointing devices.\n * These listeners will be added when the polyfill first loads, and anytime\n * the window is blurred, so that they are active when the window regains\n * focus.\n */\n function addInitialPointerMoveListeners() {\n document.addEventListener('mousemove', onInitialPointerMove);\n document.addEventListener('mousedown', onInitialPointerMove);\n document.addEventListener('mouseup', onInitialPointerMove);\n document.addEventListener('pointermove', onInitialPointerMove);\n document.addEventListener('pointerdown', onInitialPointerMove);\n document.addEventListener('pointerup', onInitialPointerMove);\n document.addEventListener('touchmove', onInitialPointerMove);\n document.addEventListener('touchstart', onInitialPointerMove);\n document.addEventListener('touchend', onInitialPointerMove);\n }\n\n function removeInitialPointerMoveListeners() {\n document.removeEventListener('mousemove', onInitialPointerMove);\n document.removeEventListener('mousedown', onInitialPointerMove);\n document.removeEventListener('mouseup', onInitialPointerMove);\n document.removeEventListener('pointermove', onInitialPointerMove);\n document.removeEventListener('pointerdown', onInitialPointerMove);\n document.removeEventListener('pointerup', onInitialPointerMove);\n document.removeEventListener('touchmove', onInitialPointerMove);\n document.removeEventListener('touchstart', onInitialPointerMove);\n document.removeEventListener('touchend', onInitialPointerMove);\n }\n\n /**\n * When the polfyill first loads, assume the user is in keyboard modality.\n * If any event is received from a pointing device (e.g. mouse, pointer,\n * touch), turn off keyboard modality.\n * This accounts for situations where focus enters the page from the URL bar.\n * @param {Event} e\n */\n function onInitialPointerMove(e) {\n // Work around a Safari quirk that fires a mousemove on whenever the\n // window blurs, even if you're tabbing out of the page. \u00AF\\_(\u30C4)_/\u00AF\n if (e.target.nodeName && e.target.nodeName.toLowerCase() === 'html') {\n return;\n }\n\n hadKeyboardEvent = false;\n removeInitialPointerMoveListeners();\n }\n\n // For some kinds of state, we are interested in changes at the global scope\n // only. For example, global pointer input, global key presses and global\n // visibility change should affect the state at every scope:\n document.addEventListener('keydown', onKeyDown, true);\n document.addEventListener('mousedown', onPointerDown, true);\n document.addEventListener('pointerdown', onPointerDown, true);\n document.addEventListener('touchstart', onPointerDown, true);\n document.addEventListener('visibilitychange', onVisibilityChange, true);\n\n addInitialPointerMoveListeners();\n\n // For focus and blur, we specifically care about state changes in the local\n // scope. This is because focus / blur events that originate from within a\n // shadow root are not re-dispatched from the host element if it was already\n // the active element in its own scope:\n scope.addEventListener('focus', onFocus, true);\n scope.addEventListener('blur', onBlur, true);\n\n // We detect that a node is a ShadowRoot by ensuring that it is a\n // DocumentFragment and also has a host property. This check covers native\n // implementation and polyfill implementation transparently. If we only cared\n // about the native implementation, we could just check if the scope was\n // an instance of a ShadowRoot.\n if (scope.nodeType === Node.DOCUMENT_FRAGMENT_NODE && scope.host) {\n // Since a ShadowRoot is a special kind of DocumentFragment, it does not\n // have a root element to add a class to. So, we add this attribute to the\n // host element instead:\n scope.host.setAttribute('data-js-focus-visible', '');\n } else if (scope.nodeType === Node.DOCUMENT_NODE) {\n document.documentElement.classList.add('js-focus-visible');\n document.documentElement.setAttribute('data-js-focus-visible', '');\n }\n }\n\n // It is important to wrap all references to global window and document in\n // these checks to support server-side rendering use cases\n // @see https://github.com/WICG/focus-visible/issues/199\n if (typeof window !== 'undefined' && typeof document !== 'undefined') {\n // Make the polyfill helper globally available. This can be used as a signal\n // to interested libraries that wish to coordinate with the polyfill for e.g.,\n // applying the polyfill to a shadow root:\n window.applyFocusVisiblePolyfill = applyFocusVisiblePolyfill;\n\n // Notify interested libraries of the polyfill's presence, in case the\n // polyfill was loaded lazily:\n var event;\n\n try {\n event = new CustomEvent('focus-visible-polyfill-ready');\n } catch (error) {\n // IE11 does not support using CustomEvent as a constructor directly:\n event = document.createEvent('CustomEvent');\n event.initCustomEvent('focus-visible-polyfill-ready', false, false, {});\n }\n\n window.dispatchEvent(event);\n }\n\n if (typeof document !== 'undefined') {\n // Apply the polyfill to the global document, so that no JavaScript\n // coordination is required to use the polyfill in the top-level document:\n applyFocusVisiblePolyfill(document);\n }\n\n})));\n", "/*!\n * escape-html\n * Copyright(c) 2012-2013 TJ Holowaychuk\n * Copyright(c) 2015 Andreas Lubbe\n * Copyright(c) 2015 Tiancheng \"Timothy\" Gu\n * MIT Licensed\n */\n\n'use strict';\n\n/**\n * Module variables.\n * @private\n */\n\nvar matchHtmlRegExp = /[\"'&<>]/;\n\n/**\n * Module exports.\n * @public\n */\n\nmodule.exports = escapeHtml;\n\n/**\n * Escape special characters in the given string of html.\n *\n * @param {string} string The string to escape for inserting into HTML\n * @return {string}\n * @public\n */\n\nfunction escapeHtml(string) {\n var str = '' + string;\n var match = matchHtmlRegExp.exec(str);\n\n if (!match) {\n return str;\n }\n\n var escape;\n var html = '';\n var index = 0;\n var lastIndex = 0;\n\n for (index = match.index; index < str.length; index++) {\n switch (str.charCodeAt(index)) {\n case 34: // \"\n escape = '"';\n break;\n case 38: // &\n escape = '&';\n break;\n case 39: // '\n escape = ''';\n break;\n case 60: // <\n escape = '<';\n break;\n case 62: // >\n escape = '>';\n break;\n default:\n continue;\n }\n\n if (lastIndex !== index) {\n html += str.substring(lastIndex, index);\n }\n\n lastIndex = index + 1;\n html += escape;\n }\n\n return lastIndex !== index\n ? html + str.substring(lastIndex, index)\n : html;\n}\n", "/*!\n * clipboard.js v2.0.11\n * https://clipboardjs.com/\n *\n * Licensed MIT \u00A9 Zeno Rocha\n */\n(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"ClipboardJS\"] = factory();\n\telse\n\t\troot[\"ClipboardJS\"] = factory();\n})(this, function() {\nreturn /******/ (function() { // webpackBootstrap\n/******/ \tvar __webpack_modules__ = ({\n\n/***/ 686:\n/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {\n\n\"use strict\";\n\n// EXPORTS\n__webpack_require__.d(__webpack_exports__, {\n \"default\": function() { return /* binding */ clipboard; }\n});\n\n// EXTERNAL MODULE: ./node_modules/tiny-emitter/index.js\nvar tiny_emitter = __webpack_require__(279);\nvar tiny_emitter_default = /*#__PURE__*/__webpack_require__.n(tiny_emitter);\n// EXTERNAL MODULE: ./node_modules/good-listener/src/listen.js\nvar listen = __webpack_require__(370);\nvar listen_default = /*#__PURE__*/__webpack_require__.n(listen);\n// EXTERNAL MODULE: ./node_modules/select/src/select.js\nvar src_select = __webpack_require__(817);\nvar select_default = /*#__PURE__*/__webpack_require__.n(src_select);\n;// CONCATENATED MODULE: ./src/common/command.js\n/**\n * Executes a given operation type.\n * @param {String} type\n * @return {Boolean}\n */\nfunction command(type) {\n try {\n return document.execCommand(type);\n } catch (err) {\n return false;\n }\n}\n;// CONCATENATED MODULE: ./src/actions/cut.js\n\n\n/**\n * Cut action wrapper.\n * @param {String|HTMLElement} target\n * @return {String}\n */\n\nvar ClipboardActionCut = function ClipboardActionCut(target) {\n var selectedText = select_default()(target);\n command('cut');\n return selectedText;\n};\n\n/* harmony default export */ var actions_cut = (ClipboardActionCut);\n;// CONCATENATED MODULE: ./src/common/create-fake-element.js\n/**\n * Creates a fake textarea element with a value.\n * @param {String} value\n * @return {HTMLElement}\n */\nfunction createFakeElement(value) {\n var isRTL = document.documentElement.getAttribute('dir') === 'rtl';\n var fakeElement = document.createElement('textarea'); // Prevent zooming on iOS\n\n fakeElement.style.fontSize = '12pt'; // Reset box model\n\n fakeElement.style.border = '0';\n fakeElement.style.padding = '0';\n fakeElement.style.margin = '0'; // Move element out of screen horizontally\n\n fakeElement.style.position = 'absolute';\n fakeElement.style[isRTL ? 'right' : 'left'] = '-9999px'; // Move element to the same position vertically\n\n var yPosition = window.pageYOffset || document.documentElement.scrollTop;\n fakeElement.style.top = \"\".concat(yPosition, \"px\");\n fakeElement.setAttribute('readonly', '');\n fakeElement.value = value;\n return fakeElement;\n}\n;// CONCATENATED MODULE: ./src/actions/copy.js\n\n\n\n/**\n * Create fake copy action wrapper using a fake element.\n * @param {String} target\n * @param {Object} options\n * @return {String}\n */\n\nvar fakeCopyAction = function fakeCopyAction(value, options) {\n var fakeElement = createFakeElement(value);\n options.container.appendChild(fakeElement);\n var selectedText = select_default()(fakeElement);\n command('copy');\n fakeElement.remove();\n return selectedText;\n};\n/**\n * Copy action wrapper.\n * @param {String|HTMLElement} target\n * @param {Object} options\n * @return {String}\n */\n\n\nvar ClipboardActionCopy = function ClipboardActionCopy(target) {\n var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {\n container: document.body\n };\n var selectedText = '';\n\n if (typeof target === 'string') {\n selectedText = fakeCopyAction(target, options);\n } else if (target instanceof HTMLInputElement && !['text', 'search', 'url', 'tel', 'password'].includes(target === null || target === void 0 ? void 0 : target.type)) {\n // If input type doesn't support `setSelectionRange`. Simulate it. https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/setSelectionRange\n selectedText = fakeCopyAction(target.value, options);\n } else {\n selectedText = select_default()(target);\n command('copy');\n }\n\n return selectedText;\n};\n\n/* harmony default export */ var actions_copy = (ClipboardActionCopy);\n;// CONCATENATED MODULE: ./src/actions/default.js\nfunction _typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return _typeof(obj); }\n\n\n\n/**\n * Inner function which performs selection from either `text` or `target`\n * properties and then executes copy or cut operations.\n * @param {Object} options\n */\n\nvar ClipboardActionDefault = function ClipboardActionDefault() {\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n // Defines base properties passed from constructor.\n var _options$action = options.action,\n action = _options$action === void 0 ? 'copy' : _options$action,\n container = options.container,\n target = options.target,\n text = options.text; // Sets the `action` to be performed which can be either 'copy' or 'cut'.\n\n if (action !== 'copy' && action !== 'cut') {\n throw new Error('Invalid \"action\" value, use either \"copy\" or \"cut\"');\n } // Sets the `target` property using an element that will be have its content copied.\n\n\n if (target !== undefined) {\n if (target && _typeof(target) === 'object' && target.nodeType === 1) {\n if (action === 'copy' && target.hasAttribute('disabled')) {\n throw new Error('Invalid \"target\" attribute. Please use \"readonly\" instead of \"disabled\" attribute');\n }\n\n if (action === 'cut' && (target.hasAttribute('readonly') || target.hasAttribute('disabled'))) {\n throw new Error('Invalid \"target\" attribute. You can\\'t cut text from elements with \"readonly\" or \"disabled\" attributes');\n }\n } else {\n throw new Error('Invalid \"target\" value, use a valid Element');\n }\n } // Define selection strategy based on `text` property.\n\n\n if (text) {\n return actions_copy(text, {\n container: container\n });\n } // Defines which selection strategy based on `target` property.\n\n\n if (target) {\n return action === 'cut' ? actions_cut(target) : actions_copy(target, {\n container: container\n });\n }\n};\n\n/* harmony default export */ var actions_default = (ClipboardActionDefault);\n;// CONCATENATED MODULE: ./src/clipboard.js\nfunction clipboard_typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { clipboard_typeof = function _typeof(obj) { return typeof obj; }; } else { clipboard_typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return clipboard_typeof(obj); }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }\n\nfunction _inherits(subClass, superClass) { if (typeof superClass !== \"function\" && superClass !== null) { throw new TypeError(\"Super expression must either be null or a function\"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }\n\nfunction _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }\n\nfunction _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }\n\nfunction _possibleConstructorReturn(self, call) { if (call && (clipboard_typeof(call) === \"object\" || typeof call === \"function\")) { return call; } return _assertThisInitialized(self); }\n\nfunction _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\"); } return self; }\n\nfunction _isNativeReflectConstruct() { if (typeof Reflect === \"undefined\" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === \"function\") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } }\n\nfunction _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }\n\n\n\n\n\n\n/**\n * Helper function to retrieve attribute value.\n * @param {String} suffix\n * @param {Element} element\n */\n\nfunction getAttributeValue(suffix, element) {\n var attribute = \"data-clipboard-\".concat(suffix);\n\n if (!element.hasAttribute(attribute)) {\n return;\n }\n\n return element.getAttribute(attribute);\n}\n/**\n * Base class which takes one or more elements, adds event listeners to them,\n * and instantiates a new `ClipboardAction` on each click.\n */\n\n\nvar Clipboard = /*#__PURE__*/function (_Emitter) {\n _inherits(Clipboard, _Emitter);\n\n var _super = _createSuper(Clipboard);\n\n /**\n * @param {String|HTMLElement|HTMLCollection|NodeList} trigger\n * @param {Object} options\n */\n function Clipboard(trigger, options) {\n var _this;\n\n _classCallCheck(this, Clipboard);\n\n _this = _super.call(this);\n\n _this.resolveOptions(options);\n\n _this.listenClick(trigger);\n\n return _this;\n }\n /**\n * Defines if attributes would be resolved using internal setter functions\n * or custom functions that were passed in the constructor.\n * @param {Object} options\n */\n\n\n _createClass(Clipboard, [{\n key: \"resolveOptions\",\n value: function resolveOptions() {\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n this.action = typeof options.action === 'function' ? options.action : this.defaultAction;\n this.target = typeof options.target === 'function' ? options.target : this.defaultTarget;\n this.text = typeof options.text === 'function' ? options.text : this.defaultText;\n this.container = clipboard_typeof(options.container) === 'object' ? options.container : document.body;\n }\n /**\n * Adds a click event listener to the passed trigger.\n * @param {String|HTMLElement|HTMLCollection|NodeList} trigger\n */\n\n }, {\n key: \"listenClick\",\n value: function listenClick(trigger) {\n var _this2 = this;\n\n this.listener = listen_default()(trigger, 'click', function (e) {\n return _this2.onClick(e);\n });\n }\n /**\n * Defines a new `ClipboardAction` on each click event.\n * @param {Event} e\n */\n\n }, {\n key: \"onClick\",\n value: function onClick(e) {\n var trigger = e.delegateTarget || e.currentTarget;\n var action = this.action(trigger) || 'copy';\n var text = actions_default({\n action: action,\n container: this.container,\n target: this.target(trigger),\n text: this.text(trigger)\n }); // Fires an event based on the copy operation result.\n\n this.emit(text ? 'success' : 'error', {\n action: action,\n text: text,\n trigger: trigger,\n clearSelection: function clearSelection() {\n if (trigger) {\n trigger.focus();\n }\n\n window.getSelection().removeAllRanges();\n }\n });\n }\n /**\n * Default `action` lookup function.\n * @param {Element} trigger\n */\n\n }, {\n key: \"defaultAction\",\n value: function defaultAction(trigger) {\n return getAttributeValue('action', trigger);\n }\n /**\n * Default `target` lookup function.\n * @param {Element} trigger\n */\n\n }, {\n key: \"defaultTarget\",\n value: function defaultTarget(trigger) {\n var selector = getAttributeValue('target', trigger);\n\n if (selector) {\n return document.querySelector(selector);\n }\n }\n /**\n * Allow fire programmatically a copy action\n * @param {String|HTMLElement} target\n * @param {Object} options\n * @returns Text copied.\n */\n\n }, {\n key: \"defaultText\",\n\n /**\n * Default `text` lookup function.\n * @param {Element} trigger\n */\n value: function defaultText(trigger) {\n return getAttributeValue('text', trigger);\n }\n /**\n * Destroy lifecycle.\n */\n\n }, {\n key: \"destroy\",\n value: function destroy() {\n this.listener.destroy();\n }\n }], [{\n key: \"copy\",\n value: function copy(target) {\n var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {\n container: document.body\n };\n return actions_copy(target, options);\n }\n /**\n * Allow fire programmatically a cut action\n * @param {String|HTMLElement} target\n * @returns Text cutted.\n */\n\n }, {\n key: \"cut\",\n value: function cut(target) {\n return actions_cut(target);\n }\n /**\n * Returns the support of the given action, or all actions if no action is\n * given.\n * @param {String} [action]\n */\n\n }, {\n key: \"isSupported\",\n value: function isSupported() {\n var action = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ['copy', 'cut'];\n var actions = typeof action === 'string' ? [action] : action;\n var support = !!document.queryCommandSupported;\n actions.forEach(function (action) {\n support = support && !!document.queryCommandSupported(action);\n });\n return support;\n }\n }]);\n\n return Clipboard;\n}((tiny_emitter_default()));\n\n/* harmony default export */ var clipboard = (Clipboard);\n\n/***/ }),\n\n/***/ 828:\n/***/ (function(module) {\n\nvar DOCUMENT_NODE_TYPE = 9;\n\n/**\n * A polyfill for Element.matches()\n */\nif (typeof Element !== 'undefined' && !Element.prototype.matches) {\n var proto = Element.prototype;\n\n proto.matches = proto.matchesSelector ||\n proto.mozMatchesSelector ||\n proto.msMatchesSelector ||\n proto.oMatchesSelector ||\n proto.webkitMatchesSelector;\n}\n\n/**\n * Finds the closest parent that matches a selector.\n *\n * @param {Element} element\n * @param {String} selector\n * @return {Function}\n */\nfunction closest (element, selector) {\n while (element && element.nodeType !== DOCUMENT_NODE_TYPE) {\n if (typeof element.matches === 'function' &&\n element.matches(selector)) {\n return element;\n }\n element = element.parentNode;\n }\n}\n\nmodule.exports = closest;\n\n\n/***/ }),\n\n/***/ 438:\n/***/ (function(module, __unused_webpack_exports, __webpack_require__) {\n\nvar closest = __webpack_require__(828);\n\n/**\n * Delegates event to a selector.\n *\n * @param {Element} element\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @param {Boolean} useCapture\n * @return {Object}\n */\nfunction _delegate(element, selector, type, callback, useCapture) {\n var listenerFn = listener.apply(this, arguments);\n\n element.addEventListener(type, listenerFn, useCapture);\n\n return {\n destroy: function() {\n element.removeEventListener(type, listenerFn, useCapture);\n }\n }\n}\n\n/**\n * Delegates event to a selector.\n *\n * @param {Element|String|Array} [elements]\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @param {Boolean} useCapture\n * @return {Object}\n */\nfunction delegate(elements, selector, type, callback, useCapture) {\n // Handle the regular Element usage\n if (typeof elements.addEventListener === 'function') {\n return _delegate.apply(null, arguments);\n }\n\n // Handle Element-less usage, it defaults to global delegation\n if (typeof type === 'function') {\n // Use `document` as the first parameter, then apply arguments\n // This is a short way to .unshift `arguments` without running into deoptimizations\n return _delegate.bind(null, document).apply(null, arguments);\n }\n\n // Handle Selector-based usage\n if (typeof elements === 'string') {\n elements = document.querySelectorAll(elements);\n }\n\n // Handle Array-like based usage\n return Array.prototype.map.call(elements, function (element) {\n return _delegate(element, selector, type, callback, useCapture);\n });\n}\n\n/**\n * Finds closest match and invokes callback.\n *\n * @param {Element} element\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @return {Function}\n */\nfunction listener(element, selector, type, callback) {\n return function(e) {\n e.delegateTarget = closest(e.target, selector);\n\n if (e.delegateTarget) {\n callback.call(element, e);\n }\n }\n}\n\nmodule.exports = delegate;\n\n\n/***/ }),\n\n/***/ 879:\n/***/ (function(__unused_webpack_module, exports) {\n\n/**\n * Check if argument is a HTML element.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.node = function(value) {\n return value !== undefined\n && value instanceof HTMLElement\n && value.nodeType === 1;\n};\n\n/**\n * Check if argument is a list of HTML elements.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.nodeList = function(value) {\n var type = Object.prototype.toString.call(value);\n\n return value !== undefined\n && (type === '[object NodeList]' || type === '[object HTMLCollection]')\n && ('length' in value)\n && (value.length === 0 || exports.node(value[0]));\n};\n\n/**\n * Check if argument is a string.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.string = function(value) {\n return typeof value === 'string'\n || value instanceof String;\n};\n\n/**\n * Check if argument is a function.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.fn = function(value) {\n var type = Object.prototype.toString.call(value);\n\n return type === '[object Function]';\n};\n\n\n/***/ }),\n\n/***/ 370:\n/***/ (function(module, __unused_webpack_exports, __webpack_require__) {\n\nvar is = __webpack_require__(879);\nvar delegate = __webpack_require__(438);\n\n/**\n * Validates all params and calls the right\n * listener function based on its target type.\n *\n * @param {String|HTMLElement|HTMLCollection|NodeList} target\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listen(target, type, callback) {\n if (!target && !type && !callback) {\n throw new Error('Missing required arguments');\n }\n\n if (!is.string(type)) {\n throw new TypeError('Second argument must be a String');\n }\n\n if (!is.fn(callback)) {\n throw new TypeError('Third argument must be a Function');\n }\n\n if (is.node(target)) {\n return listenNode(target, type, callback);\n }\n else if (is.nodeList(target)) {\n return listenNodeList(target, type, callback);\n }\n else if (is.string(target)) {\n return listenSelector(target, type, callback);\n }\n else {\n throw new TypeError('First argument must be a String, HTMLElement, HTMLCollection, or NodeList');\n }\n}\n\n/**\n * Adds an event listener to a HTML element\n * and returns a remove listener function.\n *\n * @param {HTMLElement} node\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenNode(node, type, callback) {\n node.addEventListener(type, callback);\n\n return {\n destroy: function() {\n node.removeEventListener(type, callback);\n }\n }\n}\n\n/**\n * Add an event listener to a list of HTML elements\n * and returns a remove listener function.\n *\n * @param {NodeList|HTMLCollection} nodeList\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenNodeList(nodeList, type, callback) {\n Array.prototype.forEach.call(nodeList, function(node) {\n node.addEventListener(type, callback);\n });\n\n return {\n destroy: function() {\n Array.prototype.forEach.call(nodeList, function(node) {\n node.removeEventListener(type, callback);\n });\n }\n }\n}\n\n/**\n * Add an event listener to a selector\n * and returns a remove listener function.\n *\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenSelector(selector, type, callback) {\n return delegate(document.body, selector, type, callback);\n}\n\nmodule.exports = listen;\n\n\n/***/ }),\n\n/***/ 817:\n/***/ (function(module) {\n\nfunction select(element) {\n var selectedText;\n\n if (element.nodeName === 'SELECT') {\n element.focus();\n\n selectedText = element.value;\n }\n else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {\n var isReadOnly = element.hasAttribute('readonly');\n\n if (!isReadOnly) {\n element.setAttribute('readonly', '');\n }\n\n element.select();\n element.setSelectionRange(0, element.value.length);\n\n if (!isReadOnly) {\n element.removeAttribute('readonly');\n }\n\n selectedText = element.value;\n }\n else {\n if (element.hasAttribute('contenteditable')) {\n element.focus();\n }\n\n var selection = window.getSelection();\n var range = document.createRange();\n\n range.selectNodeContents(element);\n selection.removeAllRanges();\n selection.addRange(range);\n\n selectedText = selection.toString();\n }\n\n return selectedText;\n}\n\nmodule.exports = select;\n\n\n/***/ }),\n\n/***/ 279:\n/***/ (function(module) {\n\nfunction E () {\n // Keep this empty so it's easier to inherit from\n // (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3)\n}\n\nE.prototype = {\n on: function (name, callback, ctx) {\n var e = this.e || (this.e = {});\n\n (e[name] || (e[name] = [])).push({\n fn: callback,\n ctx: ctx\n });\n\n return this;\n },\n\n once: function (name, callback, ctx) {\n var self = this;\n function listener () {\n self.off(name, listener);\n callback.apply(ctx, arguments);\n };\n\n listener._ = callback\n return this.on(name, listener, ctx);\n },\n\n emit: function (name) {\n var data = [].slice.call(arguments, 1);\n var evtArr = ((this.e || (this.e = {}))[name] || []).slice();\n var i = 0;\n var len = evtArr.length;\n\n for (i; i < len; i++) {\n evtArr[i].fn.apply(evtArr[i].ctx, data);\n }\n\n return this;\n },\n\n off: function (name, callback) {\n var e = this.e || (this.e = {});\n var evts = e[name];\n var liveEvents = [];\n\n if (evts && callback) {\n for (var i = 0, len = evts.length; i < len; i++) {\n if (evts[i].fn !== callback && evts[i].fn._ !== callback)\n liveEvents.push(evts[i]);\n }\n }\n\n // Remove event from queue to prevent memory leak\n // Suggested by https://github.com/lazd\n // Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910\n\n (liveEvents.length)\n ? e[name] = liveEvents\n : delete e[name];\n\n return this;\n }\n};\n\nmodule.exports = E;\nmodule.exports.TinyEmitter = E;\n\n\n/***/ })\n\n/******/ \t});\n/************************************************************************/\n/******/ \t// The module cache\n/******/ \tvar __webpack_module_cache__ = {};\n/******/ \t\n/******/ \t// The require function\n/******/ \tfunction __webpack_require__(moduleId) {\n/******/ \t\t// Check if module is in cache\n/******/ \t\tif(__webpack_module_cache__[moduleId]) {\n/******/ \t\t\treturn __webpack_module_cache__[moduleId].exports;\n/******/ \t\t}\n/******/ \t\t// Create a new module (and put it into the cache)\n/******/ \t\tvar module = __webpack_module_cache__[moduleId] = {\n/******/ \t\t\t// no module.id needed\n/******/ \t\t\t// no module.loaded needed\n/******/ \t\t\texports: {}\n/******/ \t\t};\n/******/ \t\n/******/ \t\t// Execute the module function\n/******/ \t\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n/******/ \t\n/******/ \t\t// Return the exports of the module\n/******/ \t\treturn module.exports;\n/******/ \t}\n/******/ \t\n/************************************************************************/\n/******/ \t/* webpack/runtime/compat get default export */\n/******/ \t!function() {\n/******/ \t\t// getDefaultExport function for compatibility with non-harmony modules\n/******/ \t\t__webpack_require__.n = function(module) {\n/******/ \t\t\tvar getter = module && module.__esModule ?\n/******/ \t\t\t\tfunction() { return module['default']; } :\n/******/ \t\t\t\tfunction() { return module; };\n/******/ \t\t\t__webpack_require__.d(getter, { a: getter });\n/******/ \t\t\treturn getter;\n/******/ \t\t};\n/******/ \t}();\n/******/ \t\n/******/ \t/* webpack/runtime/define property getters */\n/******/ \t!function() {\n/******/ \t\t// define getter functions for harmony exports\n/******/ \t\t__webpack_require__.d = function(exports, definition) {\n/******/ \t\t\tfor(var key in definition) {\n/******/ \t\t\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n/******/ \t\t\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n/******/ \t\t\t\t}\n/******/ \t\t\t}\n/******/ \t\t};\n/******/ \t}();\n/******/ \t\n/******/ \t/* webpack/runtime/hasOwnProperty shorthand */\n/******/ \t!function() {\n/******/ \t\t__webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }\n/******/ \t}();\n/******/ \t\n/************************************************************************/\n/******/ \t// module exports must be returned from runtime so entry inlining is disabled\n/******/ \t// startup\n/******/ \t// Load entry module and return exports\n/******/ \treturn __webpack_require__(686);\n/******/ })()\n.default;\n});", "/*\n * Copyright (c) 2016-2024 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport \"focus-visible\"\n\nimport {\n EMPTY,\n NEVER,\n Observable,\n Subject,\n defer,\n delay,\n filter,\n map,\n merge,\n mergeWith,\n shareReplay,\n switchMap\n} from \"rxjs\"\n\nimport { configuration, feature } from \"./_\"\nimport {\n at,\n getActiveElement,\n getOptionalElement,\n requestJSON,\n setLocation,\n setToggle,\n watchDocument,\n watchKeyboard,\n watchLocation,\n watchLocationTarget,\n watchMedia,\n watchPrint,\n watchScript,\n watchViewport\n} from \"./browser\"\nimport {\n getComponentElement,\n getComponentElements,\n mountAnnounce,\n mountBackToTop,\n mountConsent,\n mountContent,\n mountDialog,\n mountHeader,\n mountHeaderTitle,\n mountPalette,\n mountProgress,\n mountSearch,\n mountSearchHiglight,\n mountSidebar,\n mountSource,\n mountTableOfContents,\n mountTabs,\n watchHeader,\n watchMain\n} from \"./components\"\nimport {\n SearchIndex,\n setupClipboardJS,\n setupInstantNavigation,\n setupVersionSelector\n} from \"./integrations\"\nimport {\n patchEllipsis,\n patchIndeterminate,\n patchScrollfix,\n patchScrolllock\n} from \"./patches\"\nimport \"./polyfills\"\n\n/* ----------------------------------------------------------------------------\n * Functions - @todo refactor\n * ------------------------------------------------------------------------- */\n\n/**\n * Fetch search index\n *\n * @returns Search index observable\n */\nfunction fetchSearchIndex(): Observable {\n if (location.protocol === \"file:\") {\n return watchScript(\n `${new URL(\"search/search_index.js\", config.base)}`\n )\n .pipe(\n // @ts-ignore - @todo fix typings\n map(() => __index),\n shareReplay(1)\n )\n } else {\n return requestJSON(\n new URL(\"search/search_index.json\", config.base)\n )\n }\n}\n\n/* ----------------------------------------------------------------------------\n * Application\n * ------------------------------------------------------------------------- */\n\n/* Yay, JavaScript is available */\ndocument.documentElement.classList.remove(\"no-js\")\ndocument.documentElement.classList.add(\"js\")\n\n/* Set up navigation observables and subjects */\nconst document$ = watchDocument()\nconst location$ = watchLocation()\nconst target$ = watchLocationTarget(location$)\nconst keyboard$ = watchKeyboard()\n\n/* Set up media observables */\nconst viewport$ = watchViewport()\nconst tablet$ = watchMedia(\"(min-width: 960px)\")\nconst screen$ = watchMedia(\"(min-width: 1220px)\")\nconst print$ = watchPrint()\n\n/* Retrieve search index, if search is enabled */\nconst config = configuration()\nconst index$ = document.forms.namedItem(\"search\")\n ? fetchSearchIndex()\n : NEVER\n\n/* Set up Clipboard.js integration */\nconst alert$ = new Subject()\nsetupClipboardJS({ alert$ })\n\n/* Set up progress indicator */\nconst progress$ = new Subject()\n\n/* Set up instant navigation, if enabled */\nif (feature(\"navigation.instant\"))\n setupInstantNavigation({ location$, viewport$, progress$ })\n .subscribe(document$)\n\n/* Set up version selector */\nif (config.version?.provider === \"mike\")\n setupVersionSelector({ document$ })\n\n/* Always close drawer and search on navigation */\nmerge(location$, target$)\n .pipe(\n delay(125)\n )\n .subscribe(() => {\n setToggle(\"drawer\", false)\n setToggle(\"search\", false)\n })\n\n/* Set up global keyboard handlers */\nkeyboard$\n .pipe(\n filter(({ mode }) => mode === \"global\")\n )\n .subscribe(key => {\n switch (key.type) {\n\n /* Go to previous page */\n case \"p\":\n case \",\":\n const prev = getOptionalElement(\"link[rel=prev]\")\n if (typeof prev !== \"undefined\")\n setLocation(prev)\n break\n\n /* Go to next page */\n case \"n\":\n case \".\":\n const next = getOptionalElement(\"link[rel=next]\")\n if (typeof next !== \"undefined\")\n setLocation(next)\n break\n\n /* Expand navigation, see https://bit.ly/3ZjG5io */\n case \"Enter\":\n const active = getActiveElement()\n if (active instanceof HTMLLabelElement)\n active.click()\n }\n })\n\n/* Set up patches */\npatchEllipsis({ viewport$, document$ })\npatchIndeterminate({ document$, tablet$ })\npatchScrollfix({ document$ })\npatchScrolllock({ viewport$, tablet$ })\n\n/* Set up header and main area observable */\nconst header$ = watchHeader(getComponentElement(\"header\"), { viewport$ })\nconst main$ = document$\n .pipe(\n map(() => getComponentElement(\"main\")),\n switchMap(el => watchMain(el, { viewport$, header$ })),\n shareReplay(1)\n )\n\n/* Set up control component observables */\nconst control$ = merge(\n\n /* Consent */\n ...getComponentElements(\"consent\")\n .map(el => mountConsent(el, { target$ })),\n\n /* Dialog */\n ...getComponentElements(\"dialog\")\n .map(el => mountDialog(el, { alert$ })),\n\n /* Color palette */\n ...getComponentElements(\"palette\")\n .map(el => mountPalette(el)),\n\n /* Progress bar */\n ...getComponentElements(\"progress\")\n .map(el => mountProgress(el, { progress$ })),\n\n /* Search */\n ...getComponentElements(\"search\")\n .map(el => mountSearch(el, { index$, keyboard$ })),\n\n /* Repository information */\n ...getComponentElements(\"source\")\n .map(el => mountSource(el))\n)\n\n/* Set up content component observables */\nconst content$ = defer(() => merge(\n\n /* Announcement bar */\n ...getComponentElements(\"announce\")\n .map(el => mountAnnounce(el)),\n\n /* Content */\n ...getComponentElements(\"content\")\n .map(el => mountContent(el, { viewport$, target$, print$ })),\n\n /* Search highlighting */\n ...getComponentElements(\"content\")\n .map(el => feature(\"search.highlight\")\n ? mountSearchHiglight(el, { index$, location$ })\n : EMPTY\n ),\n\n /* Header */\n ...getComponentElements(\"header\")\n .map(el => mountHeader(el, { viewport$, header$, main$ })),\n\n /* Header title */\n ...getComponentElements(\"header-title\")\n .map(el => mountHeaderTitle(el, { viewport$, header$ })),\n\n /* Sidebar */\n ...getComponentElements(\"sidebar\")\n .map(el => el.getAttribute(\"data-md-type\") === \"navigation\"\n ? at(screen$, () => mountSidebar(el, { viewport$, header$, main$ }))\n : at(tablet$, () => mountSidebar(el, { viewport$, header$, main$ }))\n ),\n\n /* Navigation tabs */\n ...getComponentElements(\"tabs\")\n .map(el => mountTabs(el, { viewport$, header$ })),\n\n /* Table of contents */\n ...getComponentElements(\"toc\")\n .map(el => mountTableOfContents(el, {\n viewport$, header$, main$, target$\n })),\n\n /* Back-to-top button */\n ...getComponentElements(\"top\")\n .map(el => mountBackToTop(el, { viewport$, header$, main$, target$ }))\n))\n\n/* Set up component observables */\nconst component$ = document$\n .pipe(\n switchMap(() => content$),\n mergeWith(control$),\n shareReplay(1)\n )\n\n/* Subscribe to all components */\ncomponent$.subscribe()\n\n/* ----------------------------------------------------------------------------\n * Exports\n * ------------------------------------------------------------------------- */\n\nwindow.document$ = document$ /* Document observable */\nwindow.location$ = location$ /* Location subject */\nwindow.target$ = target$ /* Location target observable */\nwindow.keyboard$ = keyboard$ /* Keyboard observable */\nwindow.viewport$ = viewport$ /* Viewport observable */\nwindow.tablet$ = tablet$ /* Media tablet observable */\nwindow.screen$ = screen$ /* Media screen observable */\nwindow.print$ = print$ /* Media print observable */\nwindow.alert$ = alert$ /* Alert subject */\nwindow.progress$ = progress$ /* Progress indicator subject */\nwindow.component$ = component$ /* Component observable */\n", "/******************************************************************************\nCopyright (c) Microsoft Corporation.\n\nPermission to use, copy, modify, and/or distribute this software for any\npurpose with or without fee is hereby granted.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\nREGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY\nAND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\nINDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM\nLOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR\nOTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\nPERFORMANCE OF THIS SOFTWARE.\n***************************************************************************** */\n/* global Reflect, Promise, SuppressedError, Symbol, Iterator */\n\nvar extendStatics = function(d, b) {\n extendStatics = Object.setPrototypeOf ||\n ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||\n function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };\n return extendStatics(d, b);\n};\n\nexport function __extends(d, b) {\n if (typeof b !== \"function\" && b !== null)\n throw new TypeError(\"Class extends value \" + String(b) + \" is not a constructor or null\");\n extendStatics(d, b);\n function __() { this.constructor = d; }\n d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());\n}\n\nexport var __assign = function() {\n __assign = Object.assign || function __assign(t) {\n for (var s, i = 1, n = arguments.length; i < n; i++) {\n s = arguments[i];\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];\n }\n return t;\n }\n return __assign.apply(this, arguments);\n}\n\nexport function __rest(s, e) {\n var t = {};\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)\n t[p] = s[p];\n if (s != null && typeof Object.getOwnPropertySymbols === \"function\")\n for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {\n if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))\n t[p[i]] = s[p[i]];\n }\n return t;\n}\n\nexport function __decorate(decorators, target, key, desc) {\n var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;\n if (typeof Reflect === \"object\" && typeof Reflect.decorate === \"function\") r = Reflect.decorate(decorators, target, key, desc);\n else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;\n return c > 3 && r && Object.defineProperty(target, key, r), r;\n}\n\nexport function __param(paramIndex, decorator) {\n return function (target, key) { decorator(target, key, paramIndex); }\n}\n\nexport function __esDecorate(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {\n function accept(f) { if (f !== void 0 && typeof f !== \"function\") throw new TypeError(\"Function expected\"); return f; }\n var kind = contextIn.kind, key = kind === \"getter\" ? \"get\" : kind === \"setter\" ? \"set\" : \"value\";\n var target = !descriptorIn && ctor ? contextIn[\"static\"] ? ctor : ctor.prototype : null;\n var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});\n var _, done = false;\n for (var i = decorators.length - 1; i >= 0; i--) {\n var context = {};\n for (var p in contextIn) context[p] = p === \"access\" ? {} : contextIn[p];\n for (var p in contextIn.access) context.access[p] = contextIn.access[p];\n context.addInitializer = function (f) { if (done) throw new TypeError(\"Cannot add initializers after decoration has completed\"); extraInitializers.push(accept(f || null)); };\n var result = (0, decorators[i])(kind === \"accessor\" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);\n if (kind === \"accessor\") {\n if (result === void 0) continue;\n if (result === null || typeof result !== \"object\") throw new TypeError(\"Object expected\");\n if (_ = accept(result.get)) descriptor.get = _;\n if (_ = accept(result.set)) descriptor.set = _;\n if (_ = accept(result.init)) initializers.unshift(_);\n }\n else if (_ = accept(result)) {\n if (kind === \"field\") initializers.unshift(_);\n else descriptor[key] = _;\n }\n }\n if (target) Object.defineProperty(target, contextIn.name, descriptor);\n done = true;\n};\n\nexport function __runInitializers(thisArg, initializers, value) {\n var useValue = arguments.length > 2;\n for (var i = 0; i < initializers.length; i++) {\n value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);\n }\n return useValue ? value : void 0;\n};\n\nexport function __propKey(x) {\n return typeof x === \"symbol\" ? x : \"\".concat(x);\n};\n\nexport function __setFunctionName(f, name, prefix) {\n if (typeof name === \"symbol\") name = name.description ? \"[\".concat(name.description, \"]\") : \"\";\n return Object.defineProperty(f, \"name\", { configurable: true, value: prefix ? \"\".concat(prefix, \" \", name) : name });\n};\n\nexport function __metadata(metadataKey, metadataValue) {\n if (typeof Reflect === \"object\" && typeof Reflect.metadata === \"function\") return Reflect.metadata(metadataKey, metadataValue);\n}\n\nexport function __awaiter(thisArg, _arguments, P, generator) {\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\n return new (P || (P = Promise))(function (resolve, reject) {\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\n step((generator = generator.apply(thisArg, _arguments || [])).next());\n });\n}\n\nexport function __generator(thisArg, body) {\n var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === \"function\" ? Iterator : Object).prototype);\n return g.next = verb(0), g[\"throw\"] = verb(1), g[\"return\"] = verb(2), typeof Symbol === \"function\" && (g[Symbol.iterator] = function() { return this; }), g;\n function verb(n) { return function (v) { return step([n, v]); }; }\n function step(op) {\n if (f) throw new TypeError(\"Generator is already executing.\");\n while (g && (g = 0, op[0] && (_ = 0)), _) try {\n if (f = 1, y && (t = op[0] & 2 ? y[\"return\"] : op[0] ? y[\"throw\"] || ((t = y[\"return\"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;\n if (y = 0, t) op = [op[0] & 2, t.value];\n switch (op[0]) {\n case 0: case 1: t = op; break;\n case 4: _.label++; return { value: op[1], done: false };\n case 5: _.label++; y = op[1]; op = [0]; continue;\n case 7: op = _.ops.pop(); _.trys.pop(); continue;\n default:\n if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }\n if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }\n if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }\n if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }\n if (t[2]) _.ops.pop();\n _.trys.pop(); continue;\n }\n op = body.call(thisArg, _);\n } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }\n if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };\n }\n}\n\nexport var __createBinding = Object.create ? (function(o, m, k, k2) {\n if (k2 === undefined) k2 = k;\n var desc = Object.getOwnPropertyDescriptor(m, k);\n if (!desc || (\"get\" in desc ? !m.__esModule : desc.writable || desc.configurable)) {\n desc = { enumerable: true, get: function() { return m[k]; } };\n }\n Object.defineProperty(o, k2, desc);\n}) : (function(o, m, k, k2) {\n if (k2 === undefined) k2 = k;\n o[k2] = m[k];\n});\n\nexport function __exportStar(m, o) {\n for (var p in m) if (p !== \"default\" && !Object.prototype.hasOwnProperty.call(o, p)) __createBinding(o, m, p);\n}\n\nexport function __values(o) {\n var s = typeof Symbol === \"function\" && Symbol.iterator, m = s && o[s], i = 0;\n if (m) return m.call(o);\n if (o && typeof o.length === \"number\") return {\n next: function () {\n if (o && i >= o.length) o = void 0;\n return { value: o && o[i++], done: !o };\n }\n };\n throw new TypeError(s ? \"Object is not iterable.\" : \"Symbol.iterator is not defined.\");\n}\n\nexport function __read(o, n) {\n var m = typeof Symbol === \"function\" && o[Symbol.iterator];\n if (!m) return o;\n var i = m.call(o), r, ar = [], e;\n try {\n while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);\n }\n catch (error) { e = { error: error }; }\n finally {\n try {\n if (r && !r.done && (m = i[\"return\"])) m.call(i);\n }\n finally { if (e) throw e.error; }\n }\n return ar;\n}\n\n/** @deprecated */\nexport function __spread() {\n for (var ar = [], i = 0; i < arguments.length; i++)\n ar = ar.concat(__read(arguments[i]));\n return ar;\n}\n\n/** @deprecated */\nexport function __spreadArrays() {\n for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;\n for (var r = Array(s), k = 0, i = 0; i < il; i++)\n for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)\n r[k] = a[j];\n return r;\n}\n\nexport function __spreadArray(to, from, pack) {\n if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {\n if (ar || !(i in from)) {\n if (!ar) ar = Array.prototype.slice.call(from, 0, i);\n ar[i] = from[i];\n }\n }\n return to.concat(ar || Array.prototype.slice.call(from));\n}\n\nexport function __await(v) {\n return this instanceof __await ? (this.v = v, this) : new __await(v);\n}\n\nexport function __asyncGenerator(thisArg, _arguments, generator) {\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\n var g = generator.apply(thisArg, _arguments || []), i, q = [];\n return i = Object.create((typeof AsyncIterator === \"function\" ? AsyncIterator : Object).prototype), verb(\"next\"), verb(\"throw\"), verb(\"return\", awaitReturn), i[Symbol.asyncIterator] = function () { return this; }, i;\n function awaitReturn(f) { return function (v) { return Promise.resolve(v).then(f, reject); }; }\n function verb(n, f) { if (g[n]) { i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; if (f) i[n] = f(i[n]); } }\n function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }\n function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }\n function fulfill(value) { resume(\"next\", value); }\n function reject(value) { resume(\"throw\", value); }\n function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }\n}\n\nexport function __asyncDelegator(o) {\n var i, p;\n return i = {}, verb(\"next\"), verb(\"throw\", function (e) { throw e; }), verb(\"return\"), i[Symbol.iterator] = function () { return this; }, i;\n function verb(n, f) { i[n] = o[n] ? function (v) { return (p = !p) ? { value: __await(o[n](v)), done: false } : f ? f(v) : v; } : f; }\n}\n\nexport function __asyncValues(o) {\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\n var m = o[Symbol.asyncIterator], i;\n return m ? m.call(o) : (o = typeof __values === \"function\" ? __values(o) : o[Symbol.iterator](), i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i);\n function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }\n function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }\n}\n\nexport function __makeTemplateObject(cooked, raw) {\n if (Object.defineProperty) { Object.defineProperty(cooked, \"raw\", { value: raw }); } else { cooked.raw = raw; }\n return cooked;\n};\n\nvar __setModuleDefault = Object.create ? (function(o, v) {\n Object.defineProperty(o, \"default\", { enumerable: true, value: v });\n}) : function(o, v) {\n o[\"default\"] = v;\n};\n\nexport function __importStar(mod) {\n if (mod && mod.__esModule) return mod;\n var result = {};\n if (mod != null) for (var k in mod) if (k !== \"default\" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);\n __setModuleDefault(result, mod);\n return result;\n}\n\nexport function __importDefault(mod) {\n return (mod && mod.__esModule) ? mod : { default: mod };\n}\n\nexport function __classPrivateFieldGet(receiver, state, kind, f) {\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a getter\");\n if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot read private member from an object whose class did not declare it\");\n return kind === \"m\" ? f : kind === \"a\" ? f.call(receiver) : f ? f.value : state.get(receiver);\n}\n\nexport function __classPrivateFieldSet(receiver, state, value, kind, f) {\n if (kind === \"m\") throw new TypeError(\"Private method is not writable\");\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a setter\");\n if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot write private member to an object whose class did not declare it\");\n return (kind === \"a\" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;\n}\n\nexport function __classPrivateFieldIn(state, receiver) {\n if (receiver === null || (typeof receiver !== \"object\" && typeof receiver !== \"function\")) throw new TypeError(\"Cannot use 'in' operator on non-object\");\n return typeof state === \"function\" ? receiver === state : state.has(receiver);\n}\n\nexport function __addDisposableResource(env, value, async) {\n if (value !== null && value !== void 0) {\n if (typeof value !== \"object\" && typeof value !== \"function\") throw new TypeError(\"Object expected.\");\n var dispose, inner;\n if (async) {\n if (!Symbol.asyncDispose) throw new TypeError(\"Symbol.asyncDispose is not defined.\");\n dispose = value[Symbol.asyncDispose];\n }\n if (dispose === void 0) {\n if (!Symbol.dispose) throw new TypeError(\"Symbol.dispose is not defined.\");\n dispose = value[Symbol.dispose];\n if (async) inner = dispose;\n }\n if (typeof dispose !== \"function\") throw new TypeError(\"Object not disposable.\");\n if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };\n env.stack.push({ value: value, dispose: dispose, async: async });\n }\n else if (async) {\n env.stack.push({ async: true });\n }\n return value;\n}\n\nvar _SuppressedError = typeof SuppressedError === \"function\" ? SuppressedError : function (error, suppressed, message) {\n var e = new Error(message);\n return e.name = \"SuppressedError\", e.error = error, e.suppressed = suppressed, e;\n};\n\nexport function __disposeResources(env) {\n function fail(e) {\n env.error = env.hasError ? new _SuppressedError(e, env.error, \"An error was suppressed during disposal.\") : e;\n env.hasError = true;\n }\n var r, s = 0;\n function next() {\n while (r = env.stack.pop()) {\n try {\n if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);\n if (r.dispose) {\n var result = r.dispose.call(r.value);\n if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });\n }\n else s |= 1;\n }\n catch (e) {\n fail(e);\n }\n }\n if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();\n if (env.hasError) throw env.error;\n }\n return next();\n}\n\nexport default {\n __extends,\n __assign,\n __rest,\n __decorate,\n __param,\n __metadata,\n __awaiter,\n __generator,\n __createBinding,\n __exportStar,\n __values,\n __read,\n __spread,\n __spreadArrays,\n __spreadArray,\n __await,\n __asyncGenerator,\n __asyncDelegator,\n __asyncValues,\n __makeTemplateObject,\n __importStar,\n __importDefault,\n __classPrivateFieldGet,\n __classPrivateFieldSet,\n __classPrivateFieldIn,\n __addDisposableResource,\n __disposeResources,\n};\n", "/**\n * Returns true if the object is a function.\n * @param value The value to check\n */\nexport function isFunction(value: any): value is (...args: any[]) => any {\n return typeof value === 'function';\n}\n", "/**\n * Used to create Error subclasses until the community moves away from ES5.\n *\n * This is because compiling from TypeScript down to ES5 has issues with subclassing Errors\n * as well as other built-in types: https://github.com/Microsoft/TypeScript/issues/12123\n *\n * @param createImpl A factory function to create the actual constructor implementation. The returned\n * function should be a named function that calls `_super` internally.\n */\nexport function createErrorClass(createImpl: (_super: any) => any): T {\n const _super = (instance: any) => {\n Error.call(instance);\n instance.stack = new Error().stack;\n };\n\n const ctorFunc = createImpl(_super);\n ctorFunc.prototype = Object.create(Error.prototype);\n ctorFunc.prototype.constructor = ctorFunc;\n return ctorFunc;\n}\n", "import { createErrorClass } from './createErrorClass';\n\nexport interface UnsubscriptionError extends Error {\n readonly errors: any[];\n}\n\nexport interface UnsubscriptionErrorCtor {\n /**\n * @deprecated Internal implementation detail. Do not construct error instances.\n * Cannot be tagged as internal: https://github.com/ReactiveX/rxjs/issues/6269\n */\n new (errors: any[]): UnsubscriptionError;\n}\n\n/**\n * An error thrown when one or more errors have occurred during the\n * `unsubscribe` of a {@link Subscription}.\n */\nexport const UnsubscriptionError: UnsubscriptionErrorCtor = createErrorClass(\n (_super) =>\n function UnsubscriptionErrorImpl(this: any, errors: (Error | string)[]) {\n _super(this);\n this.message = errors\n ? `${errors.length} errors occurred during unsubscription:\n${errors.map((err, i) => `${i + 1}) ${err.toString()}`).join('\\n ')}`\n : '';\n this.name = 'UnsubscriptionError';\n this.errors = errors;\n }\n);\n", "/**\n * Removes an item from an array, mutating it.\n * @param arr The array to remove the item from\n * @param item The item to remove\n */\nexport function arrRemove(arr: T[] | undefined | null, item: T) {\n if (arr) {\n const index = arr.indexOf(item);\n 0 <= index && arr.splice(index, 1);\n }\n}\n", "import { isFunction } from './util/isFunction';\nimport { UnsubscriptionError } from './util/UnsubscriptionError';\nimport { SubscriptionLike, TeardownLogic, Unsubscribable } from './types';\nimport { arrRemove } from './util/arrRemove';\n\n/**\n * Represents a disposable resource, such as the execution of an Observable. A\n * Subscription has one important method, `unsubscribe`, that takes no argument\n * and just disposes the resource held by the subscription.\n *\n * Additionally, subscriptions may be grouped together through the `add()`\n * method, which will attach a child Subscription to the current Subscription.\n * When a Subscription is unsubscribed, all its children (and its grandchildren)\n * will be unsubscribed as well.\n *\n * @class Subscription\n */\nexport class Subscription implements SubscriptionLike {\n /** @nocollapse */\n public static EMPTY = (() => {\n const empty = new Subscription();\n empty.closed = true;\n return empty;\n })();\n\n /**\n * A flag to indicate whether this Subscription has already been unsubscribed.\n */\n public closed = false;\n\n private _parentage: Subscription[] | Subscription | null = null;\n\n /**\n * The list of registered finalizers to execute upon unsubscription. Adding and removing from this\n * list occurs in the {@link #add} and {@link #remove} methods.\n */\n private _finalizers: Exclude[] | null = null;\n\n /**\n * @param initialTeardown A function executed first as part of the finalization\n * process that is kicked off when {@link #unsubscribe} is called.\n */\n constructor(private initialTeardown?: () => void) {}\n\n /**\n * Disposes the resources held by the subscription. May, for instance, cancel\n * an ongoing Observable execution or cancel any other type of work that\n * started when the Subscription was created.\n * @return {void}\n */\n unsubscribe(): void {\n let errors: any[] | undefined;\n\n if (!this.closed) {\n this.closed = true;\n\n // Remove this from it's parents.\n const { _parentage } = this;\n if (_parentage) {\n this._parentage = null;\n if (Array.isArray(_parentage)) {\n for (const parent of _parentage) {\n parent.remove(this);\n }\n } else {\n _parentage.remove(this);\n }\n }\n\n const { initialTeardown: initialFinalizer } = this;\n if (isFunction(initialFinalizer)) {\n try {\n initialFinalizer();\n } catch (e) {\n errors = e instanceof UnsubscriptionError ? e.errors : [e];\n }\n }\n\n const { _finalizers } = this;\n if (_finalizers) {\n this._finalizers = null;\n for (const finalizer of _finalizers) {\n try {\n execFinalizer(finalizer);\n } catch (err) {\n errors = errors ?? [];\n if (err instanceof UnsubscriptionError) {\n errors = [...errors, ...err.errors];\n } else {\n errors.push(err);\n }\n }\n }\n }\n\n if (errors) {\n throw new UnsubscriptionError(errors);\n }\n }\n }\n\n /**\n * Adds a finalizer to this subscription, so that finalization will be unsubscribed/called\n * when this subscription is unsubscribed. If this subscription is already {@link #closed},\n * because it has already been unsubscribed, then whatever finalizer is passed to it\n * will automatically be executed (unless the finalizer itself is also a closed subscription).\n *\n * Closed Subscriptions cannot be added as finalizers to any subscription. Adding a closed\n * subscription to a any subscription will result in no operation. (A noop).\n *\n * Adding a subscription to itself, or adding `null` or `undefined` will not perform any\n * operation at all. (A noop).\n *\n * `Subscription` instances that are added to this instance will automatically remove themselves\n * if they are unsubscribed. Functions and {@link Unsubscribable} objects that you wish to remove\n * will need to be removed manually with {@link #remove}\n *\n * @param teardown The finalization logic to add to this subscription.\n */\n add(teardown: TeardownLogic): void {\n // Only add the finalizer if it's not undefined\n // and don't add a subscription to itself.\n if (teardown && teardown !== this) {\n if (this.closed) {\n // If this subscription is already closed,\n // execute whatever finalizer is handed to it automatically.\n execFinalizer(teardown);\n } else {\n if (teardown instanceof Subscription) {\n // We don't add closed subscriptions, and we don't add the same subscription\n // twice. Subscription unsubscribe is idempotent.\n if (teardown.closed || teardown._hasParent(this)) {\n return;\n }\n teardown._addParent(this);\n }\n (this._finalizers = this._finalizers ?? []).push(teardown);\n }\n }\n }\n\n /**\n * Checks to see if a this subscription already has a particular parent.\n * This will signal that this subscription has already been added to the parent in question.\n * @param parent the parent to check for\n */\n private _hasParent(parent: Subscription) {\n const { _parentage } = this;\n return _parentage === parent || (Array.isArray(_parentage) && _parentage.includes(parent));\n }\n\n /**\n * Adds a parent to this subscription so it can be removed from the parent if it\n * unsubscribes on it's own.\n *\n * NOTE: THIS ASSUMES THAT {@link _hasParent} HAS ALREADY BEEN CHECKED.\n * @param parent The parent subscription to add\n */\n private _addParent(parent: Subscription) {\n const { _parentage } = this;\n this._parentage = Array.isArray(_parentage) ? (_parentage.push(parent), _parentage) : _parentage ? [_parentage, parent] : parent;\n }\n\n /**\n * Called on a child when it is removed via {@link #remove}.\n * @param parent The parent to remove\n */\n private _removeParent(parent: Subscription) {\n const { _parentage } = this;\n if (_parentage === parent) {\n this._parentage = null;\n } else if (Array.isArray(_parentage)) {\n arrRemove(_parentage, parent);\n }\n }\n\n /**\n * Removes a finalizer from this subscription that was previously added with the {@link #add} method.\n *\n * Note that `Subscription` instances, when unsubscribed, will automatically remove themselves\n * from every other `Subscription` they have been added to. This means that using the `remove` method\n * is not a common thing and should be used thoughtfully.\n *\n * If you add the same finalizer instance of a function or an unsubscribable object to a `Subscription` instance\n * more than once, you will need to call `remove` the same number of times to remove all instances.\n *\n * All finalizer instances are removed to free up memory upon unsubscription.\n *\n * @param teardown The finalizer to remove from this subscription\n */\n remove(teardown: Exclude): void {\n const { _finalizers } = this;\n _finalizers && arrRemove(_finalizers, teardown);\n\n if (teardown instanceof Subscription) {\n teardown._removeParent(this);\n }\n }\n}\n\nexport const EMPTY_SUBSCRIPTION = Subscription.EMPTY;\n\nexport function isSubscription(value: any): value is Subscription {\n return (\n value instanceof Subscription ||\n (value && 'closed' in value && isFunction(value.remove) && isFunction(value.add) && isFunction(value.unsubscribe))\n );\n}\n\nfunction execFinalizer(finalizer: Unsubscribable | (() => void)) {\n if (isFunction(finalizer)) {\n finalizer();\n } else {\n finalizer.unsubscribe();\n }\n}\n", "import { Subscriber } from './Subscriber';\nimport { ObservableNotification } from './types';\n\n/**\n * The {@link GlobalConfig} object for RxJS. It is used to configure things\n * like how to react on unhandled errors.\n */\nexport const config: GlobalConfig = {\n onUnhandledError: null,\n onStoppedNotification: null,\n Promise: undefined,\n useDeprecatedSynchronousErrorHandling: false,\n useDeprecatedNextContext: false,\n};\n\n/**\n * The global configuration object for RxJS, used to configure things\n * like how to react on unhandled errors. Accessible via {@link config}\n * object.\n */\nexport interface GlobalConfig {\n /**\n * A registration point for unhandled errors from RxJS. These are errors that\n * cannot were not handled by consuming code in the usual subscription path. For\n * example, if you have this configured, and you subscribe to an observable without\n * providing an error handler, errors from that subscription will end up here. This\n * will _always_ be called asynchronously on another job in the runtime. This is because\n * we do not want errors thrown in this user-configured handler to interfere with the\n * behavior of the library.\n */\n onUnhandledError: ((err: any) => void) | null;\n\n /**\n * A registration point for notifications that cannot be sent to subscribers because they\n * have completed, errored or have been explicitly unsubscribed. By default, next, complete\n * and error notifications sent to stopped subscribers are noops. However, sometimes callers\n * might want a different behavior. For example, with sources that attempt to report errors\n * to stopped subscribers, a caller can configure RxJS to throw an unhandled error instead.\n * This will _always_ be called asynchronously on another job in the runtime. This is because\n * we do not want errors thrown in this user-configured handler to interfere with the\n * behavior of the library.\n */\n onStoppedNotification: ((notification: ObservableNotification, subscriber: Subscriber) => void) | null;\n\n /**\n * The promise constructor used by default for {@link Observable#toPromise toPromise} and {@link Observable#forEach forEach}\n * methods.\n *\n * @deprecated As of version 8, RxJS will no longer support this sort of injection of a\n * Promise constructor. If you need a Promise implementation other than native promises,\n * please polyfill/patch Promise as you see appropriate. Will be removed in v8.\n */\n Promise?: PromiseConstructorLike;\n\n /**\n * If true, turns on synchronous error rethrowing, which is a deprecated behavior\n * in v6 and higher. This behavior enables bad patterns like wrapping a subscribe\n * call in a try/catch block. It also enables producer interference, a nasty bug\n * where a multicast can be broken for all observers by a downstream consumer with\n * an unhandled error. DO NOT USE THIS FLAG UNLESS IT'S NEEDED TO BUY TIME\n * FOR MIGRATION REASONS.\n *\n * @deprecated As of version 8, RxJS will no longer support synchronous throwing\n * of unhandled errors. All errors will be thrown on a separate call stack to prevent bad\n * behaviors described above. Will be removed in v8.\n */\n useDeprecatedSynchronousErrorHandling: boolean;\n\n /**\n * If true, enables an as-of-yet undocumented feature from v5: The ability to access\n * `unsubscribe()` via `this` context in `next` functions created in observers passed\n * to `subscribe`.\n *\n * This is being removed because the performance was severely problematic, and it could also cause\n * issues when types other than POJOs are passed to subscribe as subscribers, as they will likely have\n * their `this` context overwritten.\n *\n * @deprecated As of version 8, RxJS will no longer support altering the\n * context of next functions provided as part of an observer to Subscribe. Instead,\n * you will have access to a subscription or a signal or token that will allow you to do things like\n * unsubscribe and test closed status. Will be removed in v8.\n */\n useDeprecatedNextContext: boolean;\n}\n", "import type { TimerHandle } from './timerHandle';\ntype SetTimeoutFunction = (handler: () => void, timeout?: number, ...args: any[]) => TimerHandle;\ntype ClearTimeoutFunction = (handle: TimerHandle) => void;\n\ninterface TimeoutProvider {\n setTimeout: SetTimeoutFunction;\n clearTimeout: ClearTimeoutFunction;\n delegate:\n | {\n setTimeout: SetTimeoutFunction;\n clearTimeout: ClearTimeoutFunction;\n }\n | undefined;\n}\n\nexport const timeoutProvider: TimeoutProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n setTimeout(handler: () => void, timeout?: number, ...args) {\n const { delegate } = timeoutProvider;\n if (delegate?.setTimeout) {\n return delegate.setTimeout(handler, timeout, ...args);\n }\n return setTimeout(handler, timeout, ...args);\n },\n clearTimeout(handle) {\n const { delegate } = timeoutProvider;\n return (delegate?.clearTimeout || clearTimeout)(handle as any);\n },\n delegate: undefined,\n};\n", "import { config } from '../config';\nimport { timeoutProvider } from '../scheduler/timeoutProvider';\n\n/**\n * Handles an error on another job either with the user-configured {@link onUnhandledError},\n * or by throwing it on that new job so it can be picked up by `window.onerror`, `process.on('error')`, etc.\n *\n * This should be called whenever there is an error that is out-of-band with the subscription\n * or when an error hits a terminal boundary of the subscription and no error handler was provided.\n *\n * @param err the error to report\n */\nexport function reportUnhandledError(err: any) {\n timeoutProvider.setTimeout(() => {\n const { onUnhandledError } = config;\n if (onUnhandledError) {\n // Execute the user-configured error handler.\n onUnhandledError(err);\n } else {\n // Throw so it is picked up by the runtime's uncaught error mechanism.\n throw err;\n }\n });\n}\n", "/* tslint:disable:no-empty */\nexport function noop() { }\n", "import { CompleteNotification, NextNotification, ErrorNotification } from './types';\n\n/**\n * A completion object optimized for memory use and created to be the\n * same \"shape\" as other notifications in v8.\n * @internal\n */\nexport const COMPLETE_NOTIFICATION = (() => createNotification('C', undefined, undefined) as CompleteNotification)();\n\n/**\n * Internal use only. Creates an optimized error notification that is the same \"shape\"\n * as other notifications.\n * @internal\n */\nexport function errorNotification(error: any): ErrorNotification {\n return createNotification('E', undefined, error) as any;\n}\n\n/**\n * Internal use only. Creates an optimized next notification that is the same \"shape\"\n * as other notifications.\n * @internal\n */\nexport function nextNotification(value: T) {\n return createNotification('N', value, undefined) as NextNotification;\n}\n\n/**\n * Ensures that all notifications created internally have the same \"shape\" in v8.\n *\n * TODO: This is only exported to support a crazy legacy test in `groupBy`.\n * @internal\n */\nexport function createNotification(kind: 'N' | 'E' | 'C', value: any, error: any) {\n return {\n kind,\n value,\n error,\n };\n}\n", "import { config } from '../config';\n\nlet context: { errorThrown: boolean; error: any } | null = null;\n\n/**\n * Handles dealing with errors for super-gross mode. Creates a context, in which\n * any synchronously thrown errors will be passed to {@link captureError}. Which\n * will record the error such that it will be rethrown after the call back is complete.\n * TODO: Remove in v8\n * @param cb An immediately executed function.\n */\nexport function errorContext(cb: () => void) {\n if (config.useDeprecatedSynchronousErrorHandling) {\n const isRoot = !context;\n if (isRoot) {\n context = { errorThrown: false, error: null };\n }\n cb();\n if (isRoot) {\n const { errorThrown, error } = context!;\n context = null;\n if (errorThrown) {\n throw error;\n }\n }\n } else {\n // This is the general non-deprecated path for everyone that\n // isn't crazy enough to use super-gross mode (useDeprecatedSynchronousErrorHandling)\n cb();\n }\n}\n\n/**\n * Captures errors only in super-gross mode.\n * @param err the error to capture\n */\nexport function captureError(err: any) {\n if (config.useDeprecatedSynchronousErrorHandling && context) {\n context.errorThrown = true;\n context.error = err;\n }\n}\n", "import { isFunction } from './util/isFunction';\nimport { Observer, ObservableNotification } from './types';\nimport { isSubscription, Subscription } from './Subscription';\nimport { config } from './config';\nimport { reportUnhandledError } from './util/reportUnhandledError';\nimport { noop } from './util/noop';\nimport { nextNotification, errorNotification, COMPLETE_NOTIFICATION } from './NotificationFactories';\nimport { timeoutProvider } from './scheduler/timeoutProvider';\nimport { captureError } from './util/errorContext';\n\n/**\n * Implements the {@link Observer} interface and extends the\n * {@link Subscription} class. While the {@link Observer} is the public API for\n * consuming the values of an {@link Observable}, all Observers get converted to\n * a Subscriber, in order to provide Subscription-like capabilities such as\n * `unsubscribe`. Subscriber is a common type in RxJS, and crucial for\n * implementing operators, but it is rarely used as a public API.\n *\n * @class Subscriber\n */\nexport class Subscriber extends Subscription implements Observer {\n /**\n * A static factory for a Subscriber, given a (potentially partial) definition\n * of an Observer.\n * @param next The `next` callback of an Observer.\n * @param error The `error` callback of an\n * Observer.\n * @param complete The `complete` callback of an\n * Observer.\n * @return A Subscriber wrapping the (partially defined)\n * Observer represented by the given arguments.\n * @nocollapse\n * @deprecated Do not use. Will be removed in v8. There is no replacement for this\n * method, and there is no reason to be creating instances of `Subscriber` directly.\n * If you have a specific use case, please file an issue.\n */\n static create(next?: (x?: T) => void, error?: (e?: any) => void, complete?: () => void): Subscriber {\n return new SafeSubscriber(next, error, complete);\n }\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n protected isStopped: boolean = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n protected destination: Subscriber | Observer; // this `any` is the escape hatch to erase extra type param (e.g. R)\n\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n * There is no reason to directly create an instance of Subscriber. This type is exported for typings reasons.\n */\n constructor(destination?: Subscriber | Observer) {\n super();\n if (destination) {\n this.destination = destination;\n // Automatically chain subscriptions together here.\n // if destination is a Subscription, then it is a Subscriber.\n if (isSubscription(destination)) {\n destination.add(this);\n }\n } else {\n this.destination = EMPTY_OBSERVER;\n }\n }\n\n /**\n * The {@link Observer} callback to receive notifications of type `next` from\n * the Observable, with a value. The Observable may call this method 0 or more\n * times.\n * @param {T} [value] The `next` value.\n * @return {void}\n */\n next(value?: T): void {\n if (this.isStopped) {\n handleStoppedNotification(nextNotification(value), this);\n } else {\n this._next(value!);\n }\n }\n\n /**\n * The {@link Observer} callback to receive notifications of type `error` from\n * the Observable, with an attached `Error`. Notifies the Observer that\n * the Observable has experienced an error condition.\n * @param {any} [err] The `error` exception.\n * @return {void}\n */\n error(err?: any): void {\n if (this.isStopped) {\n handleStoppedNotification(errorNotification(err), this);\n } else {\n this.isStopped = true;\n this._error(err);\n }\n }\n\n /**\n * The {@link Observer} callback to receive a valueless notification of type\n * `complete` from the Observable. Notifies the Observer that the Observable\n * has finished sending push-based notifications.\n * @return {void}\n */\n complete(): void {\n if (this.isStopped) {\n handleStoppedNotification(COMPLETE_NOTIFICATION, this);\n } else {\n this.isStopped = true;\n this._complete();\n }\n }\n\n unsubscribe(): void {\n if (!this.closed) {\n this.isStopped = true;\n super.unsubscribe();\n this.destination = null!;\n }\n }\n\n protected _next(value: T): void {\n this.destination.next(value);\n }\n\n protected _error(err: any): void {\n try {\n this.destination.error(err);\n } finally {\n this.unsubscribe();\n }\n }\n\n protected _complete(): void {\n try {\n this.destination.complete();\n } finally {\n this.unsubscribe();\n }\n }\n}\n\n/**\n * This bind is captured here because we want to be able to have\n * compatibility with monoid libraries that tend to use a method named\n * `bind`. In particular, a library called Monio requires this.\n */\nconst _bind = Function.prototype.bind;\n\nfunction bind any>(fn: Fn, thisArg: any): Fn {\n return _bind.call(fn, thisArg);\n}\n\n/**\n * Internal optimization only, DO NOT EXPOSE.\n * @internal\n */\nclass ConsumerObserver implements Observer {\n constructor(private partialObserver: Partial>) {}\n\n next(value: T): void {\n const { partialObserver } = this;\n if (partialObserver.next) {\n try {\n partialObserver.next(value);\n } catch (error) {\n handleUnhandledError(error);\n }\n }\n }\n\n error(err: any): void {\n const { partialObserver } = this;\n if (partialObserver.error) {\n try {\n partialObserver.error(err);\n } catch (error) {\n handleUnhandledError(error);\n }\n } else {\n handleUnhandledError(err);\n }\n }\n\n complete(): void {\n const { partialObserver } = this;\n if (partialObserver.complete) {\n try {\n partialObserver.complete();\n } catch (error) {\n handleUnhandledError(error);\n }\n }\n }\n}\n\nexport class SafeSubscriber extends Subscriber {\n constructor(\n observerOrNext?: Partial> | ((value: T) => void) | null,\n error?: ((e?: any) => void) | null,\n complete?: (() => void) | null\n ) {\n super();\n\n let partialObserver: Partial>;\n if (isFunction(observerOrNext) || !observerOrNext) {\n // The first argument is a function, not an observer. The next\n // two arguments *could* be observers, or they could be empty.\n partialObserver = {\n next: (observerOrNext ?? undefined) as (((value: T) => void) | undefined),\n error: error ?? undefined,\n complete: complete ?? undefined,\n };\n } else {\n // The first argument is a partial observer.\n let context: any;\n if (this && config.useDeprecatedNextContext) {\n // This is a deprecated path that made `this.unsubscribe()` available in\n // next handler functions passed to subscribe. This only exists behind a flag\n // now, as it is *very* slow.\n context = Object.create(observerOrNext);\n context.unsubscribe = () => this.unsubscribe();\n partialObserver = {\n next: observerOrNext.next && bind(observerOrNext.next, context),\n error: observerOrNext.error && bind(observerOrNext.error, context),\n complete: observerOrNext.complete && bind(observerOrNext.complete, context),\n };\n } else {\n // The \"normal\" path. Just use the partial observer directly.\n partialObserver = observerOrNext;\n }\n }\n\n // Wrap the partial observer to ensure it's a full observer, and\n // make sure proper error handling is accounted for.\n this.destination = new ConsumerObserver(partialObserver);\n }\n}\n\nfunction handleUnhandledError(error: any) {\n if (config.useDeprecatedSynchronousErrorHandling) {\n captureError(error);\n } else {\n // Ideal path, we report this as an unhandled error,\n // which is thrown on a new call stack.\n reportUnhandledError(error);\n }\n}\n\n/**\n * An error handler used when no error handler was supplied\n * to the SafeSubscriber -- meaning no error handler was supplied\n * do the `subscribe` call on our observable.\n * @param err The error to handle\n */\nfunction defaultErrorHandler(err: any) {\n throw err;\n}\n\n/**\n * A handler for notifications that cannot be sent to a stopped subscriber.\n * @param notification The notification being sent\n * @param subscriber The stopped subscriber\n */\nfunction handleStoppedNotification(notification: ObservableNotification, subscriber: Subscriber) {\n const { onStoppedNotification } = config;\n onStoppedNotification && timeoutProvider.setTimeout(() => onStoppedNotification(notification, subscriber));\n}\n\n/**\n * The observer used as a stub for subscriptions where the user did not\n * pass any arguments to `subscribe`. Comes with the default error handling\n * behavior.\n */\nexport const EMPTY_OBSERVER: Readonly> & { closed: true } = {\n closed: true,\n next: noop,\n error: defaultErrorHandler,\n complete: noop,\n};\n", "/**\n * Symbol.observable or a string \"@@observable\". Used for interop\n *\n * @deprecated We will no longer be exporting this symbol in upcoming versions of RxJS.\n * Instead polyfill and use Symbol.observable directly *or* use https://www.npmjs.com/package/symbol-observable\n */\nexport const observable: string | symbol = (() => (typeof Symbol === 'function' && Symbol.observable) || '@@observable')();\n", "/**\n * This function takes one parameter and just returns it. Simply put,\n * this is like `(x: T): T => x`.\n *\n * ## Examples\n *\n * This is useful in some cases when using things like `mergeMap`\n *\n * ```ts\n * import { interval, take, map, range, mergeMap, identity } from 'rxjs';\n *\n * const source$ = interval(1000).pipe(take(5));\n *\n * const result$ = source$.pipe(\n * map(i => range(i)),\n * mergeMap(identity) // same as mergeMap(x => x)\n * );\n *\n * result$.subscribe({\n * next: console.log\n * });\n * ```\n *\n * Or when you want to selectively apply an operator\n *\n * ```ts\n * import { interval, take, identity } from 'rxjs';\n *\n * const shouldLimit = () => Math.random() < 0.5;\n *\n * const source$ = interval(1000);\n *\n * const result$ = source$.pipe(shouldLimit() ? take(5) : identity);\n *\n * result$.subscribe({\n * next: console.log\n * });\n * ```\n *\n * @param x Any value that is returned by this function\n * @returns The value passed as the first parameter to this function\n */\nexport function identity(x: T): T {\n return x;\n}\n", "import { identity } from './identity';\nimport { UnaryFunction } from '../types';\n\nexport function pipe(): typeof identity;\nexport function pipe(fn1: UnaryFunction): UnaryFunction;\nexport function pipe(fn1: UnaryFunction, fn2: UnaryFunction): UnaryFunction;\nexport function pipe(fn1: UnaryFunction, fn2: UnaryFunction, fn3: UnaryFunction): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction,\n fn9: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction,\n fn9: UnaryFunction,\n ...fns: UnaryFunction[]\n): UnaryFunction;\n\n/**\n * pipe() can be called on one or more functions, each of which can take one argument (\"UnaryFunction\")\n * and uses it to return a value.\n * It returns a function that takes one argument, passes it to the first UnaryFunction, and then\n * passes the result to the next one, passes that result to the next one, and so on. \n */\nexport function pipe(...fns: Array>): UnaryFunction {\n return pipeFromArray(fns);\n}\n\n/** @internal */\nexport function pipeFromArray(fns: Array>): UnaryFunction {\n if (fns.length === 0) {\n return identity as UnaryFunction;\n }\n\n if (fns.length === 1) {\n return fns[0];\n }\n\n return function piped(input: T): R {\n return fns.reduce((prev: any, fn: UnaryFunction) => fn(prev), input as any);\n };\n}\n", "import { Operator } from './Operator';\nimport { SafeSubscriber, Subscriber } from './Subscriber';\nimport { isSubscription, Subscription } from './Subscription';\nimport { TeardownLogic, OperatorFunction, Subscribable, Observer } from './types';\nimport { observable as Symbol_observable } from './symbol/observable';\nimport { pipeFromArray } from './util/pipe';\nimport { config } from './config';\nimport { isFunction } from './util/isFunction';\nimport { errorContext } from './util/errorContext';\n\n/**\n * A representation of any set of values over any amount of time. This is the most basic building block\n * of RxJS.\n *\n * @class Observable\n */\nexport class Observable implements Subscribable {\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n */\n source: Observable | undefined;\n\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n */\n operator: Operator | undefined;\n\n /**\n * @constructor\n * @param {Function} subscribe the function that is called when the Observable is\n * initially subscribed to. This function is given a Subscriber, to which new values\n * can be `next`ed, or an `error` method can be called to raise an error, or\n * `complete` can be called to notify of a successful completion.\n */\n constructor(subscribe?: (this: Observable, subscriber: Subscriber) => TeardownLogic) {\n if (subscribe) {\n this._subscribe = subscribe;\n }\n }\n\n // HACK: Since TypeScript inherits static properties too, we have to\n // fight against TypeScript here so Subject can have a different static create signature\n /**\n * Creates a new Observable by calling the Observable constructor\n * @owner Observable\n * @method create\n * @param {Function} subscribe? the subscriber function to be passed to the Observable constructor\n * @return {Observable} a new observable\n * @nocollapse\n * @deprecated Use `new Observable()` instead. Will be removed in v8.\n */\n static create: (...args: any[]) => any = (subscribe?: (subscriber: Subscriber) => TeardownLogic) => {\n return new Observable(subscribe);\n };\n\n /**\n * Creates a new Observable, with this Observable instance as the source, and the passed\n * operator defined as the new observable's operator.\n * @method lift\n * @param operator the operator defining the operation to take on the observable\n * @return a new observable with the Operator applied\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n * If you have implemented an operator using `lift`, it is recommended that you create an\n * operator by simply returning `new Observable()` directly. See \"Creating new operators from\n * scratch\" section here: https://rxjs.dev/guide/operators\n */\n lift(operator?: Operator): Observable {\n const observable = new Observable();\n observable.source = this;\n observable.operator = operator;\n return observable;\n }\n\n subscribe(observerOrNext?: Partial> | ((value: T) => void)): Subscription;\n /** @deprecated Instead of passing separate callback arguments, use an observer argument. Signatures taking separate callback arguments will be removed in v8. Details: https://rxjs.dev/deprecations/subscribe-arguments */\n subscribe(next?: ((value: T) => void) | null, error?: ((error: any) => void) | null, complete?: (() => void) | null): Subscription;\n /**\n * Invokes an execution of an Observable and registers Observer handlers for notifications it will emit.\n *\n * Use it when you have all these Observables, but still nothing is happening.\n *\n * `subscribe` is not a regular operator, but a method that calls Observable's internal `subscribe` function. It\n * might be for example a function that you passed to Observable's constructor, but most of the time it is\n * a library implementation, which defines what will be emitted by an Observable, and when it be will emitted. This means\n * that calling `subscribe` is actually the moment when Observable starts its work, not when it is created, as it is often\n * the thought.\n *\n * Apart from starting the execution of an Observable, this method allows you to listen for values\n * that an Observable emits, as well as for when it completes or errors. You can achieve this in two\n * of the following ways.\n *\n * The first way is creating an object that implements {@link Observer} interface. It should have methods\n * defined by that interface, but note that it should be just a regular JavaScript object, which you can create\n * yourself in any way you want (ES6 class, classic function constructor, object literal etc.). In particular, do\n * not attempt to use any RxJS implementation details to create Observers - you don't need them. Remember also\n * that your object does not have to implement all methods. If you find yourself creating a method that doesn't\n * do anything, you can simply omit it. Note however, if the `error` method is not provided and an error happens,\n * it will be thrown asynchronously. Errors thrown asynchronously cannot be caught using `try`/`catch`. Instead,\n * use the {@link onUnhandledError} configuration option or use a runtime handler (like `window.onerror` or\n * `process.on('error)`) to be notified of unhandled errors. Because of this, it's recommended that you provide\n * an `error` method to avoid missing thrown errors.\n *\n * The second way is to give up on Observer object altogether and simply provide callback functions in place of its methods.\n * This means you can provide three functions as arguments to `subscribe`, where the first function is equivalent\n * of a `next` method, the second of an `error` method and the third of a `complete` method. Just as in case of an Observer,\n * if you do not need to listen for something, you can omit a function by passing `undefined` or `null`,\n * since `subscribe` recognizes these functions by where they were placed in function call. When it comes\n * to the `error` function, as with an Observer, if not provided, errors emitted by an Observable will be thrown asynchronously.\n *\n * You can, however, subscribe with no parameters at all. This may be the case where you're not interested in terminal events\n * and you also handled emissions internally by using operators (e.g. using `tap`).\n *\n * Whichever style of calling `subscribe` you use, in both cases it returns a Subscription object.\n * This object allows you to call `unsubscribe` on it, which in turn will stop the work that an Observable does and will clean\n * up all resources that an Observable used. Note that cancelling a subscription will not call `complete` callback\n * provided to `subscribe` function, which is reserved for a regular completion signal that comes from an Observable.\n *\n * Remember that callbacks provided to `subscribe` are not guaranteed to be called asynchronously.\n * It is an Observable itself that decides when these functions will be called. For example {@link of}\n * by default emits all its values synchronously. Always check documentation for how given Observable\n * will behave when subscribed and if its default behavior can be modified with a `scheduler`.\n *\n * #### Examples\n *\n * Subscribe with an {@link guide/observer Observer}\n *\n * ```ts\n * import { of } from 'rxjs';\n *\n * const sumObserver = {\n * sum: 0,\n * next(value) {\n * console.log('Adding: ' + value);\n * this.sum = this.sum + value;\n * },\n * error() {\n * // We actually could just remove this method,\n * // since we do not really care about errors right now.\n * },\n * complete() {\n * console.log('Sum equals: ' + this.sum);\n * }\n * };\n *\n * of(1, 2, 3) // Synchronously emits 1, 2, 3 and then completes.\n * .subscribe(sumObserver);\n *\n * // Logs:\n * // 'Adding: 1'\n * // 'Adding: 2'\n * // 'Adding: 3'\n * // 'Sum equals: 6'\n * ```\n *\n * Subscribe with functions ({@link deprecations/subscribe-arguments deprecated})\n *\n * ```ts\n * import { of } from 'rxjs'\n *\n * let sum = 0;\n *\n * of(1, 2, 3).subscribe(\n * value => {\n * console.log('Adding: ' + value);\n * sum = sum + value;\n * },\n * undefined,\n * () => console.log('Sum equals: ' + sum)\n * );\n *\n * // Logs:\n * // 'Adding: 1'\n * // 'Adding: 2'\n * // 'Adding: 3'\n * // 'Sum equals: 6'\n * ```\n *\n * Cancel a subscription\n *\n * ```ts\n * import { interval } from 'rxjs';\n *\n * const subscription = interval(1000).subscribe({\n * next(num) {\n * console.log(num)\n * },\n * complete() {\n * // Will not be called, even when cancelling subscription.\n * console.log('completed!');\n * }\n * });\n *\n * setTimeout(() => {\n * subscription.unsubscribe();\n * console.log('unsubscribed!');\n * }, 2500);\n *\n * // Logs:\n * // 0 after 1s\n * // 1 after 2s\n * // 'unsubscribed!' after 2.5s\n * ```\n *\n * @param {Observer|Function} observerOrNext (optional) Either an observer with methods to be called,\n * or the first of three possible handlers, which is the handler for each value emitted from the subscribed\n * Observable.\n * @param {Function} error (optional) A handler for a terminal event resulting from an error. If no error handler is provided,\n * the error will be thrown asynchronously as unhandled.\n * @param {Function} complete (optional) A handler for a terminal event resulting from successful completion.\n * @return {Subscription} a subscription reference to the registered handlers\n * @method subscribe\n */\n subscribe(\n observerOrNext?: Partial> | ((value: T) => void) | null,\n error?: ((error: any) => void) | null,\n complete?: (() => void) | null\n ): Subscription {\n const subscriber = isSubscriber(observerOrNext) ? observerOrNext : new SafeSubscriber(observerOrNext, error, complete);\n\n errorContext(() => {\n const { operator, source } = this;\n subscriber.add(\n operator\n ? // We're dealing with a subscription in the\n // operator chain to one of our lifted operators.\n operator.call(subscriber, source)\n : source\n ? // If `source` has a value, but `operator` does not, something that\n // had intimate knowledge of our API, like our `Subject`, must have\n // set it. We're going to just call `_subscribe` directly.\n this._subscribe(subscriber)\n : // In all other cases, we're likely wrapping a user-provided initializer\n // function, so we need to catch errors and handle them appropriately.\n this._trySubscribe(subscriber)\n );\n });\n\n return subscriber;\n }\n\n /** @internal */\n protected _trySubscribe(sink: Subscriber): TeardownLogic {\n try {\n return this._subscribe(sink);\n } catch (err) {\n // We don't need to return anything in this case,\n // because it's just going to try to `add()` to a subscription\n // above.\n sink.error(err);\n }\n }\n\n /**\n * Used as a NON-CANCELLABLE means of subscribing to an observable, for use with\n * APIs that expect promises, like `async/await`. You cannot unsubscribe from this.\n *\n * **WARNING**: Only use this with observables you *know* will complete. If the source\n * observable does not complete, you will end up with a promise that is hung up, and\n * potentially all of the state of an async function hanging out in memory. To avoid\n * this situation, look into adding something like {@link timeout}, {@link take},\n * {@link takeWhile}, or {@link takeUntil} amongst others.\n *\n * #### Example\n *\n * ```ts\n * import { interval, take } from 'rxjs';\n *\n * const source$ = interval(1000).pipe(take(4));\n *\n * async function getTotal() {\n * let total = 0;\n *\n * await source$.forEach(value => {\n * total += value;\n * console.log('observable -> ' + value);\n * });\n *\n * return total;\n * }\n *\n * getTotal().then(\n * total => console.log('Total: ' + total)\n * );\n *\n * // Expected:\n * // 'observable -> 0'\n * // 'observable -> 1'\n * // 'observable -> 2'\n * // 'observable -> 3'\n * // 'Total: 6'\n * ```\n *\n * @param next a handler for each value emitted by the observable\n * @return a promise that either resolves on observable completion or\n * rejects with the handled error\n */\n forEach(next: (value: T) => void): Promise;\n\n /**\n * @param next a handler for each value emitted by the observable\n * @param promiseCtor a constructor function used to instantiate the Promise\n * @return a promise that either resolves on observable completion or\n * rejects with the handled error\n * @deprecated Passing a Promise constructor will no longer be available\n * in upcoming versions of RxJS. This is because it adds weight to the library, for very\n * little benefit. If you need this functionality, it is recommended that you either\n * polyfill Promise, or you create an adapter to convert the returned native promise\n * to whatever promise implementation you wanted. Will be removed in v8.\n */\n forEach(next: (value: T) => void, promiseCtor: PromiseConstructorLike): Promise;\n\n forEach(next: (value: T) => void, promiseCtor?: PromiseConstructorLike): Promise {\n promiseCtor = getPromiseCtor(promiseCtor);\n\n return new promiseCtor((resolve, reject) => {\n const subscriber = new SafeSubscriber({\n next: (value) => {\n try {\n next(value);\n } catch (err) {\n reject(err);\n subscriber.unsubscribe();\n }\n },\n error: reject,\n complete: resolve,\n });\n this.subscribe(subscriber);\n }) as Promise;\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): TeardownLogic {\n return this.source?.subscribe(subscriber);\n }\n\n /**\n * An interop point defined by the es7-observable spec https://github.com/zenparsing/es-observable\n * @method Symbol.observable\n * @return {Observable} this instance of the observable\n */\n [Symbol_observable]() {\n return this;\n }\n\n /* tslint:disable:max-line-length */\n pipe(): Observable;\n pipe(op1: OperatorFunction): Observable;\n pipe(op1: OperatorFunction, op2: OperatorFunction): Observable;\n pipe(op1: OperatorFunction, op2: OperatorFunction, op3: OperatorFunction): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction,\n op9: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction,\n op9: OperatorFunction,\n ...operations: OperatorFunction[]\n ): Observable;\n /* tslint:enable:max-line-length */\n\n /**\n * Used to stitch together functional operators into a chain.\n * @method pipe\n * @return {Observable} the Observable result of all of the operators having\n * been called in the order they were passed in.\n *\n * ## Example\n *\n * ```ts\n * import { interval, filter, map, scan } from 'rxjs';\n *\n * interval(1000)\n * .pipe(\n * filter(x => x % 2 === 0),\n * map(x => x + x),\n * scan((acc, x) => acc + x)\n * )\n * .subscribe(x => console.log(x));\n * ```\n */\n pipe(...operations: OperatorFunction[]): Observable {\n return pipeFromArray(operations)(this);\n }\n\n /* tslint:disable:max-line-length */\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(): Promise;\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(PromiseCtor: typeof Promise): Promise;\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(PromiseCtor: PromiseConstructorLike): Promise;\n /* tslint:enable:max-line-length */\n\n /**\n * Subscribe to this Observable and get a Promise resolving on\n * `complete` with the last emission (if any).\n *\n * **WARNING**: Only use this with observables you *know* will complete. If the source\n * observable does not complete, you will end up with a promise that is hung up, and\n * potentially all of the state of an async function hanging out in memory. To avoid\n * this situation, look into adding something like {@link timeout}, {@link take},\n * {@link takeWhile}, or {@link takeUntil} amongst others.\n *\n * @method toPromise\n * @param [promiseCtor] a constructor function used to instantiate\n * the Promise\n * @return A Promise that resolves with the last value emit, or\n * rejects on an error. If there were no emissions, Promise\n * resolves with undefined.\n * @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise\n */\n toPromise(promiseCtor?: PromiseConstructorLike): Promise {\n promiseCtor = getPromiseCtor(promiseCtor);\n\n return new promiseCtor((resolve, reject) => {\n let value: T | undefined;\n this.subscribe(\n (x: T) => (value = x),\n (err: any) => reject(err),\n () => resolve(value)\n );\n }) as Promise;\n }\n}\n\n/**\n * Decides between a passed promise constructor from consuming code,\n * A default configured promise constructor, and the native promise\n * constructor and returns it. If nothing can be found, it will throw\n * an error.\n * @param promiseCtor The optional promise constructor to passed by consuming code\n */\nfunction getPromiseCtor(promiseCtor: PromiseConstructorLike | undefined) {\n return promiseCtor ?? config.Promise ?? Promise;\n}\n\nfunction isObserver(value: any): value is Observer {\n return value && isFunction(value.next) && isFunction(value.error) && isFunction(value.complete);\n}\n\nfunction isSubscriber(value: any): value is Subscriber {\n return (value && value instanceof Subscriber) || (isObserver(value) && isSubscription(value));\n}\n", "import { Observable } from '../Observable';\nimport { Subscriber } from '../Subscriber';\nimport { OperatorFunction } from '../types';\nimport { isFunction } from './isFunction';\n\n/**\n * Used to determine if an object is an Observable with a lift function.\n */\nexport function hasLift(source: any): source is { lift: InstanceType['lift'] } {\n return isFunction(source?.lift);\n}\n\n/**\n * Creates an `OperatorFunction`. Used to define operators throughout the library in a concise way.\n * @param init The logic to connect the liftedSource to the subscriber at the moment of subscription.\n */\nexport function operate(\n init: (liftedSource: Observable, subscriber: Subscriber) => (() => void) | void\n): OperatorFunction {\n return (source: Observable) => {\n if (hasLift(source)) {\n return source.lift(function (this: Subscriber, liftedSource: Observable) {\n try {\n return init(liftedSource, this);\n } catch (err) {\n this.error(err);\n }\n });\n }\n throw new TypeError('Unable to lift unknown Observable type');\n };\n}\n", "import { Subscriber } from '../Subscriber';\n\n/**\n * Creates an instance of an `OperatorSubscriber`.\n * @param destination The downstream subscriber.\n * @param onNext Handles next values, only called if this subscriber is not stopped or closed. Any\n * error that occurs in this function is caught and sent to the `error` method of this subscriber.\n * @param onError Handles errors from the subscription, any errors that occur in this handler are caught\n * and send to the `destination` error handler.\n * @param onComplete Handles completion notification from the subscription. Any errors that occur in\n * this handler are sent to the `destination` error handler.\n * @param onFinalize Additional teardown logic here. This will only be called on teardown if the\n * subscriber itself is not already closed. This is called after all other teardown logic is executed.\n */\nexport function createOperatorSubscriber(\n destination: Subscriber,\n onNext?: (value: T) => void,\n onComplete?: () => void,\n onError?: (err: any) => void,\n onFinalize?: () => void\n): Subscriber {\n return new OperatorSubscriber(destination, onNext, onComplete, onError, onFinalize);\n}\n\n/**\n * A generic helper for allowing operators to be created with a Subscriber and\n * use closures to capture necessary state from the operator function itself.\n */\nexport class OperatorSubscriber extends Subscriber {\n /**\n * Creates an instance of an `OperatorSubscriber`.\n * @param destination The downstream subscriber.\n * @param onNext Handles next values, only called if this subscriber is not stopped or closed. Any\n * error that occurs in this function is caught and sent to the `error` method of this subscriber.\n * @param onError Handles errors from the subscription, any errors that occur in this handler are caught\n * and send to the `destination` error handler.\n * @param onComplete Handles completion notification from the subscription. Any errors that occur in\n * this handler are sent to the `destination` error handler.\n * @param onFinalize Additional finalization logic here. This will only be called on finalization if the\n * subscriber itself is not already closed. This is called after all other finalization logic is executed.\n * @param shouldUnsubscribe An optional check to see if an unsubscribe call should truly unsubscribe.\n * NOTE: This currently **ONLY** exists to support the strange behavior of {@link groupBy}, where unsubscription\n * to the resulting observable does not actually disconnect from the source if there are active subscriptions\n * to any grouped observable. (DO NOT EXPOSE OR USE EXTERNALLY!!!)\n */\n constructor(\n destination: Subscriber,\n onNext?: (value: T) => void,\n onComplete?: () => void,\n onError?: (err: any) => void,\n private onFinalize?: () => void,\n private shouldUnsubscribe?: () => boolean\n ) {\n // It's important - for performance reasons - that all of this class's\n // members are initialized and that they are always initialized in the same\n // order. This will ensure that all OperatorSubscriber instances have the\n // same hidden class in V8. This, in turn, will help keep the number of\n // hidden classes involved in property accesses within the base class as\n // low as possible. If the number of hidden classes involved exceeds four,\n // the property accesses will become megamorphic and performance penalties\n // will be incurred - i.e. inline caches won't be used.\n //\n // The reasons for ensuring all instances have the same hidden class are\n // further discussed in this blog post from Benedikt Meurer:\n // https://benediktmeurer.de/2018/03/23/impact-of-polymorphism-on-component-based-frameworks-like-react/\n super(destination);\n this._next = onNext\n ? function (this: OperatorSubscriber, value: T) {\n try {\n onNext(value);\n } catch (err) {\n destination.error(err);\n }\n }\n : super._next;\n this._error = onError\n ? function (this: OperatorSubscriber, err: any) {\n try {\n onError(err);\n } catch (err) {\n // Send any errors that occur down stream.\n destination.error(err);\n } finally {\n // Ensure finalization.\n this.unsubscribe();\n }\n }\n : super._error;\n this._complete = onComplete\n ? function (this: OperatorSubscriber) {\n try {\n onComplete();\n } catch (err) {\n // Send any errors that occur down stream.\n destination.error(err);\n } finally {\n // Ensure finalization.\n this.unsubscribe();\n }\n }\n : super._complete;\n }\n\n unsubscribe() {\n if (!this.shouldUnsubscribe || this.shouldUnsubscribe()) {\n const { closed } = this;\n super.unsubscribe();\n // Execute additional teardown if we have any and we didn't already do so.\n !closed && this.onFinalize?.();\n }\n }\n}\n", "import { Subscription } from '../Subscription';\n\ninterface AnimationFrameProvider {\n schedule(callback: FrameRequestCallback): Subscription;\n requestAnimationFrame: typeof requestAnimationFrame;\n cancelAnimationFrame: typeof cancelAnimationFrame;\n delegate:\n | {\n requestAnimationFrame: typeof requestAnimationFrame;\n cancelAnimationFrame: typeof cancelAnimationFrame;\n }\n | undefined;\n}\n\nexport const animationFrameProvider: AnimationFrameProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n schedule(callback) {\n let request = requestAnimationFrame;\n let cancel: typeof cancelAnimationFrame | undefined = cancelAnimationFrame;\n const { delegate } = animationFrameProvider;\n if (delegate) {\n request = delegate.requestAnimationFrame;\n cancel = delegate.cancelAnimationFrame;\n }\n const handle = request((timestamp) => {\n // Clear the cancel function. The request has been fulfilled, so\n // attempting to cancel the request upon unsubscription would be\n // pointless.\n cancel = undefined;\n callback(timestamp);\n });\n return new Subscription(() => cancel?.(handle));\n },\n requestAnimationFrame(...args) {\n const { delegate } = animationFrameProvider;\n return (delegate?.requestAnimationFrame || requestAnimationFrame)(...args);\n },\n cancelAnimationFrame(...args) {\n const { delegate } = animationFrameProvider;\n return (delegate?.cancelAnimationFrame || cancelAnimationFrame)(...args);\n },\n delegate: undefined,\n};\n", "import { createErrorClass } from './createErrorClass';\n\nexport interface ObjectUnsubscribedError extends Error {}\n\nexport interface ObjectUnsubscribedErrorCtor {\n /**\n * @deprecated Internal implementation detail. Do not construct error instances.\n * Cannot be tagged as internal: https://github.com/ReactiveX/rxjs/issues/6269\n */\n new (): ObjectUnsubscribedError;\n}\n\n/**\n * An error thrown when an action is invalid because the object has been\n * unsubscribed.\n *\n * @see {@link Subject}\n * @see {@link BehaviorSubject}\n *\n * @class ObjectUnsubscribedError\n */\nexport const ObjectUnsubscribedError: ObjectUnsubscribedErrorCtor = createErrorClass(\n (_super) =>\n function ObjectUnsubscribedErrorImpl(this: any) {\n _super(this);\n this.name = 'ObjectUnsubscribedError';\n this.message = 'object unsubscribed';\n }\n);\n", "import { Operator } from './Operator';\nimport { Observable } from './Observable';\nimport { Subscriber } from './Subscriber';\nimport { Subscription, EMPTY_SUBSCRIPTION } from './Subscription';\nimport { Observer, SubscriptionLike, TeardownLogic } from './types';\nimport { ObjectUnsubscribedError } from './util/ObjectUnsubscribedError';\nimport { arrRemove } from './util/arrRemove';\nimport { errorContext } from './util/errorContext';\n\n/**\n * A Subject is a special type of Observable that allows values to be\n * multicasted to many Observers. Subjects are like EventEmitters.\n *\n * Every Subject is an Observable and an Observer. You can subscribe to a\n * Subject, and you can call next to feed values as well as error and complete.\n */\nexport class Subject extends Observable implements SubscriptionLike {\n closed = false;\n\n private currentObservers: Observer[] | null = null;\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n observers: Observer[] = [];\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n isStopped = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n hasError = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n thrownError: any = null;\n\n /**\n * Creates a \"subject\" by basically gluing an observer to an observable.\n *\n * @nocollapse\n * @deprecated Recommended you do not use. Will be removed at some point in the future. Plans for replacement still under discussion.\n */\n static create: (...args: any[]) => any = (destination: Observer, source: Observable): AnonymousSubject => {\n return new AnonymousSubject(destination, source);\n };\n\n constructor() {\n // NOTE: This must be here to obscure Observable's constructor.\n super();\n }\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n lift(operator: Operator): Observable {\n const subject = new AnonymousSubject(this, this);\n subject.operator = operator as any;\n return subject as any;\n }\n\n /** @internal */\n protected _throwIfClosed() {\n if (this.closed) {\n throw new ObjectUnsubscribedError();\n }\n }\n\n next(value: T) {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n if (!this.currentObservers) {\n this.currentObservers = Array.from(this.observers);\n }\n for (const observer of this.currentObservers) {\n observer.next(value);\n }\n }\n });\n }\n\n error(err: any) {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n this.hasError = this.isStopped = true;\n this.thrownError = err;\n const { observers } = this;\n while (observers.length) {\n observers.shift()!.error(err);\n }\n }\n });\n }\n\n complete() {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n this.isStopped = true;\n const { observers } = this;\n while (observers.length) {\n observers.shift()!.complete();\n }\n }\n });\n }\n\n unsubscribe() {\n this.isStopped = this.closed = true;\n this.observers = this.currentObservers = null!;\n }\n\n get observed() {\n return this.observers?.length > 0;\n }\n\n /** @internal */\n protected _trySubscribe(subscriber: Subscriber): TeardownLogic {\n this._throwIfClosed();\n return super._trySubscribe(subscriber);\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n this._throwIfClosed();\n this._checkFinalizedStatuses(subscriber);\n return this._innerSubscribe(subscriber);\n }\n\n /** @internal */\n protected _innerSubscribe(subscriber: Subscriber) {\n const { hasError, isStopped, observers } = this;\n if (hasError || isStopped) {\n return EMPTY_SUBSCRIPTION;\n }\n this.currentObservers = null;\n observers.push(subscriber);\n return new Subscription(() => {\n this.currentObservers = null;\n arrRemove(observers, subscriber);\n });\n }\n\n /** @internal */\n protected _checkFinalizedStatuses(subscriber: Subscriber) {\n const { hasError, thrownError, isStopped } = this;\n if (hasError) {\n subscriber.error(thrownError);\n } else if (isStopped) {\n subscriber.complete();\n }\n }\n\n /**\n * Creates a new Observable with this Subject as the source. You can do this\n * to create custom Observer-side logic of the Subject and conceal it from\n * code that uses the Observable.\n * @return {Observable} Observable that the Subject casts to\n */\n asObservable(): Observable {\n const observable: any = new Observable();\n observable.source = this;\n return observable;\n }\n}\n\n/**\n * @class AnonymousSubject\n */\nexport class AnonymousSubject extends Subject {\n constructor(\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n public destination?: Observer,\n source?: Observable\n ) {\n super();\n this.source = source;\n }\n\n next(value: T) {\n this.destination?.next?.(value);\n }\n\n error(err: any) {\n this.destination?.error?.(err);\n }\n\n complete() {\n this.destination?.complete?.();\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n return this.source?.subscribe(subscriber) ?? EMPTY_SUBSCRIPTION;\n }\n}\n", "import { Subject } from './Subject';\nimport { Subscriber } from './Subscriber';\nimport { Subscription } from './Subscription';\n\n/**\n * A variant of Subject that requires an initial value and emits its current\n * value whenever it is subscribed to.\n *\n * @class BehaviorSubject\n */\nexport class BehaviorSubject extends Subject {\n constructor(private _value: T) {\n super();\n }\n\n get value(): T {\n return this.getValue();\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n const subscription = super._subscribe(subscriber);\n !subscription.closed && subscriber.next(this._value);\n return subscription;\n }\n\n getValue(): T {\n const { hasError, thrownError, _value } = this;\n if (hasError) {\n throw thrownError;\n }\n this._throwIfClosed();\n return _value;\n }\n\n next(value: T): void {\n super.next((this._value = value));\n }\n}\n", "import { TimestampProvider } from '../types';\n\ninterface DateTimestampProvider extends TimestampProvider {\n delegate: TimestampProvider | undefined;\n}\n\nexport const dateTimestampProvider: DateTimestampProvider = {\n now() {\n // Use the variable rather than `this` so that the function can be called\n // without being bound to the provider.\n return (dateTimestampProvider.delegate || Date).now();\n },\n delegate: undefined,\n};\n", "import { Subject } from './Subject';\nimport { TimestampProvider } from './types';\nimport { Subscriber } from './Subscriber';\nimport { Subscription } from './Subscription';\nimport { dateTimestampProvider } from './scheduler/dateTimestampProvider';\n\n/**\n * A variant of {@link Subject} that \"replays\" old values to new subscribers by emitting them when they first subscribe.\n *\n * `ReplaySubject` has an internal buffer that will store a specified number of values that it has observed. Like `Subject`,\n * `ReplaySubject` \"observes\" values by having them passed to its `next` method. When it observes a value, it will store that\n * value for a time determined by the configuration of the `ReplaySubject`, as passed to its constructor.\n *\n * When a new subscriber subscribes to the `ReplaySubject` instance, it will synchronously emit all values in its buffer in\n * a First-In-First-Out (FIFO) manner. The `ReplaySubject` will also complete, if it has observed completion; and it will\n * error if it has observed an error.\n *\n * There are two main configuration items to be concerned with:\n *\n * 1. `bufferSize` - This will determine how many items are stored in the buffer, defaults to infinite.\n * 2. `windowTime` - The amount of time to hold a value in the buffer before removing it from the buffer.\n *\n * Both configurations may exist simultaneously. So if you would like to buffer a maximum of 3 values, as long as the values\n * are less than 2 seconds old, you could do so with a `new ReplaySubject(3, 2000)`.\n *\n * ### Differences with BehaviorSubject\n *\n * `BehaviorSubject` is similar to `new ReplaySubject(1)`, with a couple of exceptions:\n *\n * 1. `BehaviorSubject` comes \"primed\" with a single value upon construction.\n * 2. `ReplaySubject` will replay values, even after observing an error, where `BehaviorSubject` will not.\n *\n * @see {@link Subject}\n * @see {@link BehaviorSubject}\n * @see {@link shareReplay}\n */\nexport class ReplaySubject extends Subject {\n private _buffer: (T | number)[] = [];\n private _infiniteTimeWindow = true;\n\n /**\n * @param bufferSize The size of the buffer to replay on subscription\n * @param windowTime The amount of time the buffered items will stay buffered\n * @param timestampProvider An object with a `now()` method that provides the current timestamp. This is used to\n * calculate the amount of time something has been buffered.\n */\n constructor(\n private _bufferSize = Infinity,\n private _windowTime = Infinity,\n private _timestampProvider: TimestampProvider = dateTimestampProvider\n ) {\n super();\n this._infiniteTimeWindow = _windowTime === Infinity;\n this._bufferSize = Math.max(1, _bufferSize);\n this._windowTime = Math.max(1, _windowTime);\n }\n\n next(value: T): void {\n const { isStopped, _buffer, _infiniteTimeWindow, _timestampProvider, _windowTime } = this;\n if (!isStopped) {\n _buffer.push(value);\n !_infiniteTimeWindow && _buffer.push(_timestampProvider.now() + _windowTime);\n }\n this._trimBuffer();\n super.next(value);\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n this._throwIfClosed();\n this._trimBuffer();\n\n const subscription = this._innerSubscribe(subscriber);\n\n const { _infiniteTimeWindow, _buffer } = this;\n // We use a copy here, so reentrant code does not mutate our array while we're\n // emitting it to a new subscriber.\n const copy = _buffer.slice();\n for (let i = 0; i < copy.length && !subscriber.closed; i += _infiniteTimeWindow ? 1 : 2) {\n subscriber.next(copy[i] as T);\n }\n\n this._checkFinalizedStatuses(subscriber);\n\n return subscription;\n }\n\n private _trimBuffer() {\n const { _bufferSize, _timestampProvider, _buffer, _infiniteTimeWindow } = this;\n // If we don't have an infinite buffer size, and we're over the length,\n // use splice to truncate the old buffer values off. Note that we have to\n // double the size for instances where we're not using an infinite time window\n // because we're storing the values and the timestamps in the same array.\n const adjustedBufferSize = (_infiniteTimeWindow ? 1 : 2) * _bufferSize;\n _bufferSize < Infinity && adjustedBufferSize < _buffer.length && _buffer.splice(0, _buffer.length - adjustedBufferSize);\n\n // Now, if we're not in an infinite time window, remove all values where the time is\n // older than what is allowed.\n if (!_infiniteTimeWindow) {\n const now = _timestampProvider.now();\n let last = 0;\n // Search the array for the first timestamp that isn't expired and\n // truncate the buffer up to that point.\n for (let i = 1; i < _buffer.length && (_buffer[i] as number) <= now; i += 2) {\n last = i;\n }\n last && _buffer.splice(0, last + 1);\n }\n }\n}\n", "import { Scheduler } from '../Scheduler';\nimport { Subscription } from '../Subscription';\nimport { SchedulerAction } from '../types';\n\n/**\n * A unit of work to be executed in a `scheduler`. An action is typically\n * created from within a {@link SchedulerLike} and an RxJS user does not need to concern\n * themselves about creating and manipulating an Action.\n *\n * ```ts\n * class Action extends Subscription {\n * new (scheduler: Scheduler, work: (state?: T) => void);\n * schedule(state?: T, delay: number = 0): Subscription;\n * }\n * ```\n *\n * @class Action\n */\nexport class Action extends Subscription {\n constructor(scheduler: Scheduler, work: (this: SchedulerAction, state?: T) => void) {\n super();\n }\n /**\n * Schedules this action on its parent {@link SchedulerLike} for execution. May be passed\n * some context object, `state`. May happen at some point in the future,\n * according to the `delay` parameter, if specified.\n * @param {T} [state] Some contextual data that the `work` function uses when\n * called by the Scheduler.\n * @param {number} [delay] Time to wait before executing the work, where the\n * time unit is implicit and defined by the Scheduler.\n * @return {void}\n */\n public schedule(state?: T, delay: number = 0): Subscription {\n return this;\n }\n}\n", "import type { TimerHandle } from './timerHandle';\ntype SetIntervalFunction = (handler: () => void, timeout?: number, ...args: any[]) => TimerHandle;\ntype ClearIntervalFunction = (handle: TimerHandle) => void;\n\ninterface IntervalProvider {\n setInterval: SetIntervalFunction;\n clearInterval: ClearIntervalFunction;\n delegate:\n | {\n setInterval: SetIntervalFunction;\n clearInterval: ClearIntervalFunction;\n }\n | undefined;\n}\n\nexport const intervalProvider: IntervalProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n setInterval(handler: () => void, timeout?: number, ...args) {\n const { delegate } = intervalProvider;\n if (delegate?.setInterval) {\n return delegate.setInterval(handler, timeout, ...args);\n }\n return setInterval(handler, timeout, ...args);\n },\n clearInterval(handle) {\n const { delegate } = intervalProvider;\n return (delegate?.clearInterval || clearInterval)(handle as any);\n },\n delegate: undefined,\n};\n", "import { Action } from './Action';\nimport { SchedulerAction } from '../types';\nimport { Subscription } from '../Subscription';\nimport { AsyncScheduler } from './AsyncScheduler';\nimport { intervalProvider } from './intervalProvider';\nimport { arrRemove } from '../util/arrRemove';\nimport { TimerHandle } from './timerHandle';\n\nexport class AsyncAction extends Action {\n public id: TimerHandle | undefined;\n public state?: T;\n // @ts-ignore: Property has no initializer and is not definitely assigned\n public delay: number;\n protected pending: boolean = false;\n\n constructor(protected scheduler: AsyncScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n public schedule(state?: T, delay: number = 0): Subscription {\n if (this.closed) {\n return this;\n }\n\n // Always replace the current state with the new state.\n this.state = state;\n\n const id = this.id;\n const scheduler = this.scheduler;\n\n //\n // Important implementation note:\n //\n // Actions only execute once by default, unless rescheduled from within the\n // scheduled callback. This allows us to implement single and repeat\n // actions via the same code path, without adding API surface area, as well\n // as mimic traditional recursion but across asynchronous boundaries.\n //\n // However, JS runtimes and timers distinguish between intervals achieved by\n // serial `setTimeout` calls vs. a single `setInterval` call. An interval of\n // serial `setTimeout` calls can be individually delayed, which delays\n // scheduling the next `setTimeout`, and so on. `setInterval` attempts to\n // guarantee the interval callback will be invoked more precisely to the\n // interval period, regardless of load.\n //\n // Therefore, we use `setInterval` to schedule single and repeat actions.\n // If the action reschedules itself with the same delay, the interval is not\n // canceled. If the action doesn't reschedule, or reschedules with a\n // different delay, the interval will be canceled after scheduled callback\n // execution.\n //\n if (id != null) {\n this.id = this.recycleAsyncId(scheduler, id, delay);\n }\n\n // Set the pending flag indicating that this action has been scheduled, or\n // has recursively rescheduled itself.\n this.pending = true;\n\n this.delay = delay;\n // If this action has already an async Id, don't request a new one.\n this.id = this.id ?? this.requestAsyncId(scheduler, this.id, delay);\n\n return this;\n }\n\n protected requestAsyncId(scheduler: AsyncScheduler, _id?: TimerHandle, delay: number = 0): TimerHandle {\n return intervalProvider.setInterval(scheduler.flush.bind(scheduler, this), delay);\n }\n\n protected recycleAsyncId(_scheduler: AsyncScheduler, id?: TimerHandle, delay: number | null = 0): TimerHandle | undefined {\n // If this action is rescheduled with the same delay time, don't clear the interval id.\n if (delay != null && this.delay === delay && this.pending === false) {\n return id;\n }\n // Otherwise, if the action's delay time is different from the current delay,\n // or the action has been rescheduled before it's executed, clear the interval id\n if (id != null) {\n intervalProvider.clearInterval(id);\n }\n\n return undefined;\n }\n\n /**\n * Immediately executes this action and the `work` it contains.\n * @return {any}\n */\n public execute(state: T, delay: number): any {\n if (this.closed) {\n return new Error('executing a cancelled action');\n }\n\n this.pending = false;\n const error = this._execute(state, delay);\n if (error) {\n return error;\n } else if (this.pending === false && this.id != null) {\n // Dequeue if the action didn't reschedule itself. Don't call\n // unsubscribe(), because the action could reschedule later.\n // For example:\n // ```\n // scheduler.schedule(function doWork(counter) {\n // /* ... I'm a busy worker bee ... */\n // var originalAction = this;\n // /* wait 100ms before rescheduling the action */\n // setTimeout(function () {\n // originalAction.schedule(counter + 1);\n // }, 100);\n // }, 1000);\n // ```\n this.id = this.recycleAsyncId(this.scheduler, this.id, null);\n }\n }\n\n protected _execute(state: T, _delay: number): any {\n let errored: boolean = false;\n let errorValue: any;\n try {\n this.work(state);\n } catch (e) {\n errored = true;\n // HACK: Since code elsewhere is relying on the \"truthiness\" of the\n // return here, we can't have it return \"\" or 0 or false.\n // TODO: Clean this up when we refactor schedulers mid-version-8 or so.\n errorValue = e ? e : new Error('Scheduled action threw falsy error');\n }\n if (errored) {\n this.unsubscribe();\n return errorValue;\n }\n }\n\n unsubscribe() {\n if (!this.closed) {\n const { id, scheduler } = this;\n const { actions } = scheduler;\n\n this.work = this.state = this.scheduler = null!;\n this.pending = false;\n\n arrRemove(actions, this);\n if (id != null) {\n this.id = this.recycleAsyncId(scheduler, id, null);\n }\n\n this.delay = null!;\n super.unsubscribe();\n }\n }\n}\n", "import { Action } from './scheduler/Action';\nimport { Subscription } from './Subscription';\nimport { SchedulerLike, SchedulerAction } from './types';\nimport { dateTimestampProvider } from './scheduler/dateTimestampProvider';\n\n/**\n * An execution context and a data structure to order tasks and schedule their\n * execution. Provides a notion of (potentially virtual) time, through the\n * `now()` getter method.\n *\n * Each unit of work in a Scheduler is called an `Action`.\n *\n * ```ts\n * class Scheduler {\n * now(): number;\n * schedule(work, delay?, state?): Subscription;\n * }\n * ```\n *\n * @class Scheduler\n * @deprecated Scheduler is an internal implementation detail of RxJS, and\n * should not be used directly. Rather, create your own class and implement\n * {@link SchedulerLike}. Will be made internal in v8.\n */\nexport class Scheduler implements SchedulerLike {\n public static now: () => number = dateTimestampProvider.now;\n\n constructor(private schedulerActionCtor: typeof Action, now: () => number = Scheduler.now) {\n this.now = now;\n }\n\n /**\n * A getter method that returns a number representing the current time\n * (at the time this function was called) according to the scheduler's own\n * internal clock.\n * @return {number} A number that represents the current time. May or may not\n * have a relation to wall-clock time. May or may not refer to a time unit\n * (e.g. milliseconds).\n */\n public now: () => number;\n\n /**\n * Schedules a function, `work`, for execution. May happen at some point in\n * the future, according to the `delay` parameter, if specified. May be passed\n * some context object, `state`, which will be passed to the `work` function.\n *\n * The given arguments will be processed an stored as an Action object in a\n * queue of actions.\n *\n * @param {function(state: ?T): ?Subscription} work A function representing a\n * task, or some unit of work to be executed by the Scheduler.\n * @param {number} [delay] Time to wait before executing the work, where the\n * time unit is implicit and defined by the Scheduler itself.\n * @param {T} [state] Some contextual data that the `work` function uses when\n * called by the Scheduler.\n * @return {Subscription} A subscription in order to be able to unsubscribe\n * the scheduled work.\n */\n public schedule(work: (this: SchedulerAction, state?: T) => void, delay: number = 0, state?: T): Subscription {\n return new this.schedulerActionCtor(this, work).schedule(state, delay);\n }\n}\n", "import { Scheduler } from '../Scheduler';\nimport { Action } from './Action';\nimport { AsyncAction } from './AsyncAction';\nimport { TimerHandle } from './timerHandle';\n\nexport class AsyncScheduler extends Scheduler {\n public actions: Array> = [];\n /**\n * A flag to indicate whether the Scheduler is currently executing a batch of\n * queued actions.\n * @type {boolean}\n * @internal\n */\n public _active: boolean = false;\n /**\n * An internal ID used to track the latest asynchronous task such as those\n * coming from `setTimeout`, `setInterval`, `requestAnimationFrame`, and\n * others.\n * @type {any}\n * @internal\n */\n public _scheduled: TimerHandle | undefined;\n\n constructor(SchedulerAction: typeof Action, now: () => number = Scheduler.now) {\n super(SchedulerAction, now);\n }\n\n public flush(action: AsyncAction): void {\n const { actions } = this;\n\n if (this._active) {\n actions.push(action);\n return;\n }\n\n let error: any;\n this._active = true;\n\n do {\n if ((error = action.execute(action.state, action.delay))) {\n break;\n }\n } while ((action = actions.shift()!)); // exhaust the scheduler queue\n\n this._active = false;\n\n if (error) {\n while ((action = actions.shift()!)) {\n action.unsubscribe();\n }\n throw error;\n }\n }\n}\n", "import { AsyncAction } from './AsyncAction';\nimport { AsyncScheduler } from './AsyncScheduler';\n\n/**\n *\n * Async Scheduler\n *\n * Schedule task as if you used setTimeout(task, duration)\n *\n * `async` scheduler schedules tasks asynchronously, by putting them on the JavaScript\n * event loop queue. It is best used to delay tasks in time or to schedule tasks repeating\n * in intervals.\n *\n * If you just want to \"defer\" task, that is to perform it right after currently\n * executing synchronous code ends (commonly achieved by `setTimeout(deferredTask, 0)`),\n * better choice will be the {@link asapScheduler} scheduler.\n *\n * ## Examples\n * Use async scheduler to delay task\n * ```ts\n * import { asyncScheduler } from 'rxjs';\n *\n * const task = () => console.log('it works!');\n *\n * asyncScheduler.schedule(task, 2000);\n *\n * // After 2 seconds logs:\n * // \"it works!\"\n * ```\n *\n * Use async scheduler to repeat task in intervals\n * ```ts\n * import { asyncScheduler } from 'rxjs';\n *\n * function task(state) {\n * console.log(state);\n * this.schedule(state + 1, 1000); // `this` references currently executing Action,\n * // which we reschedule with new state and delay\n * }\n *\n * asyncScheduler.schedule(task, 3000, 0);\n *\n * // Logs:\n * // 0 after 3s\n * // 1 after 4s\n * // 2 after 5s\n * // 3 after 6s\n * ```\n */\n\nexport const asyncScheduler = new AsyncScheduler(AsyncAction);\n\n/**\n * @deprecated Renamed to {@link asyncScheduler}. Will be removed in v8.\n */\nexport const async = asyncScheduler;\n", "import { AsyncAction } from './AsyncAction';\nimport { Subscription } from '../Subscription';\nimport { QueueScheduler } from './QueueScheduler';\nimport { SchedulerAction } from '../types';\nimport { TimerHandle } from './timerHandle';\n\nexport class QueueAction extends AsyncAction {\n constructor(protected scheduler: QueueScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n public schedule(state?: T, delay: number = 0): Subscription {\n if (delay > 0) {\n return super.schedule(state, delay);\n }\n this.delay = delay;\n this.state = state;\n this.scheduler.flush(this);\n return this;\n }\n\n public execute(state: T, delay: number): any {\n return delay > 0 || this.closed ? super.execute(state, delay) : this._execute(state, delay);\n }\n\n protected requestAsyncId(scheduler: QueueScheduler, id?: TimerHandle, delay: number = 0): TimerHandle {\n // If delay exists and is greater than 0, or if the delay is null (the\n // action wasn't rescheduled) but was originally scheduled as an async\n // action, then recycle as an async action.\n\n if ((delay != null && delay > 0) || (delay == null && this.delay > 0)) {\n return super.requestAsyncId(scheduler, id, delay);\n }\n\n // Otherwise flush the scheduler starting with this action.\n scheduler.flush(this);\n\n // HACK: In the past, this was returning `void`. However, `void` isn't a valid\n // `TimerHandle`, and generally the return value here isn't really used. So the\n // compromise is to return `0` which is both \"falsy\" and a valid `TimerHandle`,\n // as opposed to refactoring every other instanceo of `requestAsyncId`.\n return 0;\n }\n}\n", "import { AsyncScheduler } from './AsyncScheduler';\n\nexport class QueueScheduler extends AsyncScheduler {\n}\n", "import { QueueAction } from './QueueAction';\nimport { QueueScheduler } from './QueueScheduler';\n\n/**\n *\n * Queue Scheduler\n *\n * Put every next task on a queue, instead of executing it immediately\n *\n * `queue` scheduler, when used with delay, behaves the same as {@link asyncScheduler} scheduler.\n *\n * When used without delay, it schedules given task synchronously - executes it right when\n * it is scheduled. However when called recursively, that is when inside the scheduled task,\n * another task is scheduled with queue scheduler, instead of executing immediately as well,\n * that task will be put on a queue and wait for current one to finish.\n *\n * This means that when you execute task with `queue` scheduler, you are sure it will end\n * before any other task scheduled with that scheduler will start.\n *\n * ## Examples\n * Schedule recursively first, then do something\n * ```ts\n * import { queueScheduler } from 'rxjs';\n *\n * queueScheduler.schedule(() => {\n * queueScheduler.schedule(() => console.log('second')); // will not happen now, but will be put on a queue\n *\n * console.log('first');\n * });\n *\n * // Logs:\n * // \"first\"\n * // \"second\"\n * ```\n *\n * Reschedule itself recursively\n * ```ts\n * import { queueScheduler } from 'rxjs';\n *\n * queueScheduler.schedule(function(state) {\n * if (state !== 0) {\n * console.log('before', state);\n * this.schedule(state - 1); // `this` references currently executing Action,\n * // which we reschedule with new state\n * console.log('after', state);\n * }\n * }, 0, 3);\n *\n * // In scheduler that runs recursively, you would expect:\n * // \"before\", 3\n * // \"before\", 2\n * // \"before\", 1\n * // \"after\", 1\n * // \"after\", 2\n * // \"after\", 3\n *\n * // But with queue it logs:\n * // \"before\", 3\n * // \"after\", 3\n * // \"before\", 2\n * // \"after\", 2\n * // \"before\", 1\n * // \"after\", 1\n * ```\n */\n\nexport const queueScheduler = new QueueScheduler(QueueAction);\n\n/**\n * @deprecated Renamed to {@link queueScheduler}. Will be removed in v8.\n */\nexport const queue = queueScheduler;\n", "import { AsyncAction } from './AsyncAction';\nimport { AnimationFrameScheduler } from './AnimationFrameScheduler';\nimport { SchedulerAction } from '../types';\nimport { animationFrameProvider } from './animationFrameProvider';\nimport { TimerHandle } from './timerHandle';\n\nexport class AnimationFrameAction extends AsyncAction {\n constructor(protected scheduler: AnimationFrameScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n protected requestAsyncId(scheduler: AnimationFrameScheduler, id?: TimerHandle, delay: number = 0): TimerHandle {\n // If delay is greater than 0, request as an async action.\n if (delay !== null && delay > 0) {\n return super.requestAsyncId(scheduler, id, delay);\n }\n // Push the action to the end of the scheduler queue.\n scheduler.actions.push(this);\n // If an animation frame has already been requested, don't request another\n // one. If an animation frame hasn't been requested yet, request one. Return\n // the current animation frame request id.\n return scheduler._scheduled || (scheduler._scheduled = animationFrameProvider.requestAnimationFrame(() => scheduler.flush(undefined)));\n }\n\n protected recycleAsyncId(scheduler: AnimationFrameScheduler, id?: TimerHandle, delay: number = 0): TimerHandle | undefined {\n // If delay exists and is greater than 0, or if the delay is null (the\n // action wasn't rescheduled) but was originally scheduled as an async\n // action, then recycle as an async action.\n if (delay != null ? delay > 0 : this.delay > 0) {\n return super.recycleAsyncId(scheduler, id, delay);\n }\n // If the scheduler queue has no remaining actions with the same async id,\n // cancel the requested animation frame and set the scheduled flag to\n // undefined so the next AnimationFrameAction will request its own.\n const { actions } = scheduler;\n if (id != null && actions[actions.length - 1]?.id !== id) {\n animationFrameProvider.cancelAnimationFrame(id as number);\n scheduler._scheduled = undefined;\n }\n // Return undefined so the action knows to request a new async id if it's rescheduled.\n return undefined;\n }\n}\n", "import { AsyncAction } from './AsyncAction';\nimport { AsyncScheduler } from './AsyncScheduler';\n\nexport class AnimationFrameScheduler extends AsyncScheduler {\n public flush(action?: AsyncAction): void {\n this._active = true;\n // The async id that effects a call to flush is stored in _scheduled.\n // Before executing an action, it's necessary to check the action's async\n // id to determine whether it's supposed to be executed in the current\n // flush.\n // Previous implementations of this method used a count to determine this,\n // but that was unsound, as actions that are unsubscribed - i.e. cancelled -\n // are removed from the actions array and that can shift actions that are\n // scheduled to be executed in a subsequent flush into positions at which\n // they are executed within the current flush.\n const flushId = this._scheduled;\n this._scheduled = undefined;\n\n const { actions } = this;\n let error: any;\n action = action || actions.shift()!;\n\n do {\n if ((error = action.execute(action.state, action.delay))) {\n break;\n }\n } while ((action = actions[0]) && action.id === flushId && actions.shift());\n\n this._active = false;\n\n if (error) {\n while ((action = actions[0]) && action.id === flushId && actions.shift()) {\n action.unsubscribe();\n }\n throw error;\n }\n }\n}\n", "import { AnimationFrameAction } from './AnimationFrameAction';\nimport { AnimationFrameScheduler } from './AnimationFrameScheduler';\n\n/**\n *\n * Animation Frame Scheduler\n *\n * Perform task when `window.requestAnimationFrame` would fire\n *\n * When `animationFrame` scheduler is used with delay, it will fall back to {@link asyncScheduler} scheduler\n * behaviour.\n *\n * Without delay, `animationFrame` scheduler can be used to create smooth browser animations.\n * It makes sure scheduled task will happen just before next browser content repaint,\n * thus performing animations as efficiently as possible.\n *\n * ## Example\n * Schedule div height animation\n * ```ts\n * // html:
\n * import { animationFrameScheduler } from 'rxjs';\n *\n * const div = document.querySelector('div');\n *\n * animationFrameScheduler.schedule(function(height) {\n * div.style.height = height + \"px\";\n *\n * this.schedule(height + 1); // `this` references currently executing Action,\n * // which we reschedule with new state\n * }, 0, 0);\n *\n * // You will see a div element growing in height\n * ```\n */\n\nexport const animationFrameScheduler = new AnimationFrameScheduler(AnimationFrameAction);\n\n/**\n * @deprecated Renamed to {@link animationFrameScheduler}. Will be removed in v8.\n */\nexport const animationFrame = animationFrameScheduler;\n", "import { Observable } from '../Observable';\nimport { SchedulerLike } from '../types';\n\n/**\n * A simple Observable that emits no items to the Observer and immediately\n * emits a complete notification.\n *\n * Just emits 'complete', and nothing else.\n *\n * ![](empty.png)\n *\n * A simple Observable that only emits the complete notification. It can be used\n * for composing with other Observables, such as in a {@link mergeMap}.\n *\n * ## Examples\n *\n * Log complete notification\n *\n * ```ts\n * import { EMPTY } from 'rxjs';\n *\n * EMPTY.subscribe({\n * next: () => console.log('Next'),\n * complete: () => console.log('Complete!')\n * });\n *\n * // Outputs\n * // Complete!\n * ```\n *\n * Emit the number 7, then complete\n *\n * ```ts\n * import { EMPTY, startWith } from 'rxjs';\n *\n * const result = EMPTY.pipe(startWith(7));\n * result.subscribe(x => console.log(x));\n *\n * // Outputs\n * // 7\n * ```\n *\n * Map and flatten only odd numbers to the sequence `'a'`, `'b'`, `'c'`\n *\n * ```ts\n * import { interval, mergeMap, of, EMPTY } from 'rxjs';\n *\n * const interval$ = interval(1000);\n * const result = interval$.pipe(\n * mergeMap(x => x % 2 === 1 ? of('a', 'b', 'c') : EMPTY),\n * );\n * result.subscribe(x => console.log(x));\n *\n * // Results in the following to the console:\n * // x is equal to the count on the interval, e.g. (0, 1, 2, 3, ...)\n * // x will occur every 1000ms\n * // if x % 2 is equal to 1, print a, b, c (each on its own)\n * // if x % 2 is not equal to 1, nothing will be output\n * ```\n *\n * @see {@link Observable}\n * @see {@link NEVER}\n * @see {@link of}\n * @see {@link throwError}\n */\nexport const EMPTY = new Observable((subscriber) => subscriber.complete());\n\n/**\n * @param scheduler A {@link SchedulerLike} to use for scheduling\n * the emission of the complete notification.\n * @deprecated Replaced with the {@link EMPTY} constant or {@link scheduled} (e.g. `scheduled([], scheduler)`). Will be removed in v8.\n */\nexport function empty(scheduler?: SchedulerLike) {\n return scheduler ? emptyScheduled(scheduler) : EMPTY;\n}\n\nfunction emptyScheduled(scheduler: SchedulerLike) {\n return new Observable((subscriber) => scheduler.schedule(() => subscriber.complete()));\n}\n", "import { SchedulerLike } from '../types';\nimport { isFunction } from './isFunction';\n\nexport function isScheduler(value: any): value is SchedulerLike {\n return value && isFunction(value.schedule);\n}\n", "import { SchedulerLike } from '../types';\nimport { isFunction } from './isFunction';\nimport { isScheduler } from './isScheduler';\n\nfunction last(arr: T[]): T | undefined {\n return arr[arr.length - 1];\n}\n\nexport function popResultSelector(args: any[]): ((...args: unknown[]) => unknown) | undefined {\n return isFunction(last(args)) ? args.pop() : undefined;\n}\n\nexport function popScheduler(args: any[]): SchedulerLike | undefined {\n return isScheduler(last(args)) ? args.pop() : undefined;\n}\n\nexport function popNumber(args: any[], defaultValue: number): number {\n return typeof last(args) === 'number' ? args.pop()! : defaultValue;\n}\n", "export const isArrayLike = ((x: any): x is ArrayLike => x && typeof x.length === 'number' && typeof x !== 'function');", "import { isFunction } from \"./isFunction\";\n\n/**\n * Tests to see if the object is \"thennable\".\n * @param value the object to test\n */\nexport function isPromise(value: any): value is PromiseLike {\n return isFunction(value?.then);\n}\n", "import { InteropObservable } from '../types';\nimport { observable as Symbol_observable } from '../symbol/observable';\nimport { isFunction } from './isFunction';\n\n/** Identifies an input as being Observable (but not necessary an Rx Observable) */\nexport function isInteropObservable(input: any): input is InteropObservable {\n return isFunction(input[Symbol_observable]);\n}\n", "import { isFunction } from './isFunction';\n\nexport function isAsyncIterable(obj: any): obj is AsyncIterable {\n return Symbol.asyncIterator && isFunction(obj?.[Symbol.asyncIterator]);\n}\n", "/**\n * Creates the TypeError to throw if an invalid object is passed to `from` or `scheduled`.\n * @param input The object that was passed.\n */\nexport function createInvalidObservableTypeError(input: any) {\n // TODO: We should create error codes that can be looked up, so this can be less verbose.\n return new TypeError(\n `You provided ${\n input !== null && typeof input === 'object' ? 'an invalid object' : `'${input}'`\n } where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.`\n );\n}\n", "export function getSymbolIterator(): symbol {\n if (typeof Symbol !== 'function' || !Symbol.iterator) {\n return '@@iterator' as any;\n }\n\n return Symbol.iterator;\n}\n\nexport const iterator = getSymbolIterator();\n", "import { iterator as Symbol_iterator } from '../symbol/iterator';\nimport { isFunction } from './isFunction';\n\n/** Identifies an input as being an Iterable */\nexport function isIterable(input: any): input is Iterable {\n return isFunction(input?.[Symbol_iterator]);\n}\n", "import { ReadableStreamLike } from '../types';\nimport { isFunction } from './isFunction';\n\nexport async function* readableStreamLikeToAsyncGenerator(readableStream: ReadableStreamLike): AsyncGenerator {\n const reader = readableStream.getReader();\n try {\n while (true) {\n const { value, done } = await reader.read();\n if (done) {\n return;\n }\n yield value!;\n }\n } finally {\n reader.releaseLock();\n }\n}\n\nexport function isReadableStreamLike(obj: any): obj is ReadableStreamLike {\n // We don't want to use instanceof checks because they would return\n // false for instances from another Realm, like an