summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJakob L. Kreuze <zerodaysfordays@sdf.org>2026-02-13 16:36:33 -0500
committerJakob L. Kreuze <zerodaysfordays@sdf.org>2026-02-13 16:36:33 -0500
commit8fa54d5ac98d95016f0c3eaa9fbc4f042294705d (patch)
treec8bfac4a9c3321f9b01acbb339dfeacac28e42e8
parent9df8cb9cfa56f694ae10cfc0b9e9fd4427cc3c72 (diff)
Various site updates
-rw-r--r--api.scm2
-rw-r--r--cookbook/recipes/bbq-seasoning.org.ignore13
-rw-r--r--cookbook/recipes/fried-chicken.org74
-rw-r--r--cookbook/recipes/pasta.org.ignore11
-rw-r--r--cookbook/recipes/roast-chicken.org5
-rw-r--r--cookbook/recipes/waakye-stew.org93
-rw-r--r--data/blogroll.scm9
-rw-r--r--data/bookmarks.scm8
-rw-r--r--jakob/builder/blog.scm2
-rw-r--r--jakob/builder/cookbook.scm10
-rw-r--r--jakob/reader/org-mode-prime.scm5
-rw-r--r--jakob/utils/features.scm24
-rw-r--r--jakob/utils/sxml.scm40
-rw-r--r--pages/about-complete.org2
-rw-r--r--pages/about.org2
-rw-r--r--pages/changelog.org18
-rw-r--r--posts-assets/usb-pd-is-pretty-cool-food-warmer-circuit.svg184
-rw-r--r--posts/replacing-anki-with-org-drill.org63
-rw-r--r--posts/spaced-repetition-anywhere.org164
-rw-r--r--posts/usb-pd-is-pretty-cool.org235
-rw-r--r--static/css/style.css17
21 files changed, 840 insertions, 141 deletions
diff --git a/api.scm b/api.scm
index 43660eb..1bb69cf 100644
--- a/api.scm
+++ b/api.scm
@@ -61,7 +61,7 @@
(handler request body))))
(define (dump-error request body endpoint)
- (define file-name (date->string (current-date) "jakob-api-crash-report-~4.txt"))
+ (define file-name (date->string (current-date) "/tmp/jakob-api-crash-report-~4.txt"))
(log-append! 'error (format #f "Unhandled error! Crash report written to ~a" file-name))
(call-with-output-file file-name
(lambda (port)
diff --git a/cookbook/recipes/bbq-seasoning.org.ignore b/cookbook/recipes/bbq-seasoning.org.ignore
new file mode 100644
index 0000000..3faab7b
--- /dev/null
+++ b/cookbook/recipes/bbq-seasoning.org.ignore
@@ -0,0 +1,13 @@
+
+
+ ½ cup light brown sugar
+
+ 1 tablespoon smoked paprika
+
+ 2 teaspoons chili powder
+
+ 1 tablespoon garlic powder
+
+ ½ teaspoon pepper
+
+ 2 tablespoons kosher salt
diff --git a/cookbook/recipes/fried-chicken.org b/cookbook/recipes/fried-chicken.org
new file mode 100644
index 0000000..88cd830
--- /dev/null
+++ b/cookbook/recipes/fried-chicken.org
@@ -0,0 +1,74 @@
+#+TITLE: Fried Chicken
+#+DATE: <2025-06-12 Thu 01:46>
+#+TAGS: difficulty:medium cuisine:american flavor-profile:savory dish-type:protein dish-type:main
+#+COOKBOOK_THUMBNAIL: fried-chicken-thumb.jpg
+#+COOKBOOK_PREP_TIME: 20 min
+#+COOKBOOK_COOK_TIME: 60 min
+
+This fried chicken is among my arsenal of crowd-pleasers. In my experience, deep frying is a great technique when you're cooking for a lot of people, since it's probably the fastest way to cook something.
+
+#+CAPTION: A basket of finger lickin' good.
+[[file:fried-chicken.jpg]]
+
+* Credit, Where Credit is Due
+
+This is Joshua Weissman's [[https://www.joshuaweissman.com/post/korean-fried-chicken-vs-american-fried-chicken]["American Fried Chicken" recipe]] with a few minor tweaks.
+
+* Ingredients
+
+1. Fryer chicken; 1 whole, approximately 4 lbs.
+2. Corn oil; 128 fl. oz.
+
+** Marinade
+
+1. Buttermilk; 2 cups, or enough to cover the chicken.
+2. Monosodium Glutamate (MSG); 1 tbsp
+3. Salt; 2 tbsp
+4. White pepper; 2 tbsp
+5. Garlic powder; 2 tbsp
+6. Smoked paprika; 2 tbsp
+
+** Dredge
+
+1. All-purpose flour; 2.5 cups
+2. Cornstarch; 0.5 cup
+3. Dried thyme; 4 tsp
+4. Garlic powder; 2 tbsp
+5. Onion powder; 2 tsp
+6. Old Bay; 2 tsp
+7. Smoked paprika; 2 tbsp
+8. MSG; 2 tsp
+9. Salt; 2 tbsp
+
+* Equipment
+
+- Two mixing bowls
+- A wide but shallow pot
+- Thermometer
+- A spider or fryer basket helps, but you can get by with just tongs
+- Wire rack
+
+* Steps
+
+1. Cut the chicken into 10 pieces.
+ 1. I tend to start by removing the wings, then the leg quarters, then separating the breast bone (sternum) from the rest of the carcass. I cut the wing tips off of the wings and separate the thighs from the drumsticks. If you're inexperienced, you can bend the leg to get an idea of where the joint is. You want to be cutting the cartilage, not the bone. Then I slice directly down the sternum to separate the breasts, and then cut each breast into two. If you don't want to cut through bone, you can definitely remove the breast from the bone cut each de-boned breast into two, but my preference is to fry the white meat with the bone-in.
+2. Prepare the marinade by placing the chicken into a mixing bowl, adding enough buttermilk to cover, and then adding the dry spices listed under the "Marinade" section of the ingredients list. Let marinade in the refrigerator overnight.
+3. Fill the pot with the oil and heat to 350°F.
+4. Prepare the dredge by mixing all the remaining ingredients in a mixing bowl.
+ 1. Weissman recommends dripping some of the marinade into the seasoned flour mixture and mixing it in with your hands. This produces a coating for the chicken with a more heterogeneous texture.
+5. Take each piece of chicken and coat it in the seasoned flour. Mix it around, and squeeze the chicken pieces as you do this. You want to ensure that the chicken is completely coated in flour.
+ 1. It helps to treat one hand as the "dry hand" that touches the flour and chicken that's been coated in the flour, and one hand as the "wet hand" that touches the uncoated chicken.
+6. Fry the chicken in batches, ensuring the oil returns to a temperature of 350°F before starting the next batch.
+ 1. If you throw all the chicken in the oil at once, the temperature of the oil is going to drop extremely quickly and prevent the browning you're going for.
+ 2. The chicken should be a deep golden brown after about 7-8 minutes. I would definitely recommend probing the white meat as it comes out to ensure it's cooked to 165°F internal. If your temperature control is off, you end up with whit meat that looks done on the outside but is still undercooked on the inside.
+ 3. Cool the chicken on a wire rack to ensure that the coating is still crisp by mealtime.
+
+* Notes
+
+This can be made without MSG. But if you're omitting it because you don't expect to be able to find it: you can almost certainly find it locally, assuming you live in the United States. The brand you'll find in ordinary American supermarkets is /Ac'cent Flavor Enhancer/. It's going to be in the aisle that seasonings are in, probably not far from the salt. If you have an Asian grocery store nearby like an H-Mart, I would recommend stopping there and buying a giant bag of Ajinomoto because it's much more economical.
+
+I hate wasting the buttermilk, so I usually re-purpose it for whatever side dish I'm serving alongside the fried chicken. I've successfully used it as the liquid for a baked mac and cheese, for grits, and for cornbread. That's what I do, because of my aversion to wasting dairy or meat. My younger brother apparently freezes his buttermilk marinade and re-uses it for later batches of fried chicken to great success, so that's also an option. You can also just dump it.
+
+This reheats perfectly in an air fryer.
+
+You can save the oil and re-use it for another batch or two of fried chicken, or some other fried dish. Discard when it starts to smell off. If you're re-using oil, be mindful of what's been cooked in it beforehand. I keep a separate jugs of recycled oil for fish, and things besides fish. Chicken fried in oil that was previously used for fish is nasty.
diff --git a/cookbook/recipes/pasta.org.ignore b/cookbook/recipes/pasta.org.ignore
new file mode 100644
index 0000000..9c3c828
--- /dev/null
+++ b/cookbook/recipes/pasta.org.ignore
@@ -0,0 +1,11 @@
+#+TITLE: High-Fiber Pasta
+#+DATE: <2025-06-12 Thu 02:56>
+#+TAGS: difficulty:hard cuisine:american flavor-profile:savory dish-type:base
+#+COOKBOOK_THUMBNAIL: pasta-thumb.jpg
+#+COOKBOOK_PREP_TIME: 20 min
+#+COOKBOOK_COOK_TIME: 60 min
+
+- 500g chickpea or red lentil flour
+- 160g water
+- 1 tbsp xanthan gum
+- setting 2
diff --git a/cookbook/recipes/roast-chicken.org b/cookbook/recipes/roast-chicken.org
index 2bfe52a..e9f2c38 100644
--- a/cookbook/recipes/roast-chicken.org
+++ b/cookbook/recipes/roast-chicken.org
@@ -1,6 +1,6 @@
#+TITLE: Stupid Simple Roast Chicken Parts
#+DATE: <2025-05-31 Sat 14:15>
-#+TAGS: difficulty:easy cuisine:american flavor-profile:savory dish-type:meat dish-type:main
+#+TAGS: difficulty:easy cuisine:american flavor-profile:savory dish-type:protein dish-type:main
#+COOKBOOK_THUMBNAIL: roast-chicken-thumb.jpg
#+COOKBOOK_PREP_TIME: 20 min
#+COOKBOOK_COOK_TIME: 120 min
@@ -30,7 +30,8 @@ This becomes incredibly economical when you use the bones for stock and use the
1. Separate the thigh and drumstick for each leg quarter. If you're inexperienced, you can bend the leg to get an idea of where the joint is. You want to be cutting the cartilage, not the bone.
2. Place the wire rack over the baking sheet. You may need more than one baking sheet or wire rack to cook all 10 lbs of the chicken.
-3. Combine the flour, corn starch, and seasoning in a large bowl, and dredge each chicken piece thoroughly in the mixture. It helps to treat one hand as the "dry hand" that touches the flour and chicken that's been coated in the flour, and one hand as the "wet hand" that touches the uncoated chicken.
+3. Combine the flour, corn starch, and seasoning in a large bowl, and dredge each chicken piece thoroughly in the mixture.
+ 1. It helps to treat one hand as the "dry hand" that touches the flour and chicken that's been coated in the flour, and one hand as the "wet hand" that touches the uncoated chicken.
4. Lay each piece on the wire rack and bake for 2 hours at 275°F.
5. When you're ready to serve, portion off the amount you want to eat and air fry at 350°F for 15 minutes. Let cool in the basket for at least 2 minutes.
diff --git a/cookbook/recipes/waakye-stew.org b/cookbook/recipes/waakye-stew.org
new file mode 100644
index 0000000..803edb9
--- /dev/null
+++ b/cookbook/recipes/waakye-stew.org
@@ -0,0 +1,93 @@
+#+TITLE: Waakye and Stew
+#+DATE: <2025-06-09 Mon 02:27>
+#+TAGS: difficulty:hard cuisine:ghanaian flavor-profile:savory flavor-profile:spicy dish-type:stew dish-type:starch dish-type:main
+#+COOKBOOK_THUMBNAIL: waakye-stew-thumb.jpg
+#+COOKBOOK_PREP_TIME: 90 min
+#+COOKBOOK_COOK_TIME: 400 min
+
+I'm not going to pretend to know much about Ghanaian cuisine, or even enough about it to confidently say that this truly is a Ghanaian dish. The story of this recipe is that I was on temporary duty to [[https://en.wikipedia.org/wiki/Fort_Belvoir][Fort Belvoir]] and found a Ghanaian restaurant near where I was staying ([[https://order.rahamarestaurants.com/#most-popular][Rahama]], right off [[https://en.wikipedia.org/wiki/U.S._Route_1][U.S. Route 1]]) and ordered their Waakye, which came with a seemingly tomato-based stew. It was probably the best take-out I've had in my life, so I did some research and landed on this recreation.
+
+This is also quite spicy. I once offered a bowl to my partner and they were unable to finish more than a bite.
+
+#+CAPTION: Piping hot bowl of waakye (left) and the accompanying stew (right) with a hard-boiled egg. I actually dropped the ball on this batch; I prefer my waakye to have a much deeper mahogany color. In this case, I skimped on the waakye leaves and paid for it. I also didn't caramelize the tomato as much as I typically like to.
+[[file:waakye-stew.jpg]]
+
+* Ingredients
+
+** Waakye
+
+1. Sorghum stems; 6.
+2. black-eyed peas, dry; 2 cups.
+3. Brown sweet rice[fn:1], dry; 2 cups.
+4. Salt; 1 tbsp.
+5. Baking soda; 1 tsp.
+
+** Stew
+
+1. Fennel seed; 3 tbsp.
+2. Coriander seed; 3 tbsp.
+3. Cubeb tail pepper; 1 tbsp;
+4. Cloves; 6.
+5. Calabash nutmeg; 3.
+6. Corn oil.
+7. Eggs; 8.
+8. Stewing beef; 6 lbs.
+9. Red onion; 3.
+10. Garlic; 6 cloves.
+11. Ginger; 1".
+12. Habanero[fn:2] pepper; 6.
+13. Tomato paste; 24 oz.
+14. Tomato puree; 28 oz.
+15. White miso; 2 tbsp.
+16. Salt; 1 tbsp.
+17. Smoked shrimp or crayfish powder; 2 tbsp.
+
+* Equipment
+
+** Waakye
+
+- Large pot.
+
+** Stew
+
+- Cutting board and knife.
+- Spice grinder.
+- Vessel for toasting spices (pan or microwave-safe bowl).
+- Small blender.
+- Dutch oven or similarly-sized pot.
+- Small pot for boiling eggs.
+- At least one bowl for holding intermediaries, but it's nice to have a couple handy.
+- A spider or a slotted spoon is helpful when frying the beef.
+- An oven is preferable, but you definitely can do this on the stovetop.
+
+* Steps
+
+** Waakye
+
+1. Add about 6 cups of water to a pot with the baking soda, salt, and sorghum stems. Boil for an hour.
+2. Remove the stems, and add the black-eyed peas. Cook for about 15 minutes to give them a head start.
+3. Add the rice, cover, and reduce to a simmer.
+
+** Stew
+
+1. Toast the dry spices (fennel, coriander, pepper, cloves, nutmeg) and grind into a powder. Set aside.
+2. Cut the beef into cubes, maybe an inch or so in size. Set aside.
+3. Slice two of the onions lengthwise into slivers. Set aside.
+4. Blend the remaining onion, the garlic, the ginger, and the habanero peppers. Set aside
+5. Add about a quarter inch of corn oil to the dutch oven and fry the beef in batches. Remove from the pot and set aside.
+6. Ensure there's enough oil left, and then fry the sliced onion. When it's sufficiently softened, add the spices, then the tomato paste, and the salt. Fry the tomato paste until it's deeply caramelized, but take care not to burn it. Then add the tomato puree, and ensure it has a chance to fry as well.
+7. Add the beef and the miso. If there isn't enough substance to cover everything, add some water. Stew low and slow. I've settled on 6 hours at 275°F in the oven, uncovered, but it isn't an exact science. You want the beef to be tender and the gravy to be nice and deeply browned.
+8. Within the last hours of cooking, hard-boil some eggs, peel them, and add them to the stew, along with the shrimp or crayfish powder. You want them to absorb some of the flavor from the gravy while not completely overcooking them.
+ #+CAPTION: Cooking without a lid encourages browning.
+ [[file:waakye-stew-done.jpg]]
+
+* Notes
+
+Cubeb tail pepper and calabash nutmeg will probably be hard to find if you don't have a local African grocer. Before I moved to NoVA, I'd substitute regular black peppercorns for the cubeb tail pepper, and regular nutmeg for the calabash nutmeg. You can also skip the smoked shrimp/crayfish powder if you're having trouble finding that.
+
+The nice thing about black-eyed peas is that they cook relatively quickly without soaking, so I just cook them straight from dry in this recipe.
+
+This recipe probably could be done in a slow cooker or similar appliance, just ensure you get enough browning on the tomatoes during the initial frying stage.
+
+[fn:1]: Short-grain Japonica rice. A.K.A. Glutinous rice. Oryza sativa var. glutinosa.
+[fn:2]: I've also made this with scotch bonnet peppers but I actually prefer the habaneros.
diff --git a/data/blogroll.scm b/data/blogroll.scm
index 7aefb9d..ab46fb8 100644
--- a/data/blogroll.scm
+++ b/data/blogroll.scm
@@ -198,6 +198,9 @@
"math"
"programming"
"video-games"))
+ ("Filippo Valsorda"
+ "https://words.filippo.io/"
+ ("crypto" "programming"))
("Improved Means for Achieving Deteriorated Ends"
"https://blog.kingcons.io/"
("common-lisp"
@@ -220,6 +223,9 @@
("Cryogenix"
"https://cryogenix.net/"
("bsd" "retro-tech" "reversing" "x86"))
+ ("Makefile.feld"
+ "https://blog.feld.me/"
+ ("bsd"))
("Fedor Indutnys Blog"
"https://darksi.de/"
("binary-exploitation"
@@ -666,6 +672,9 @@
("Shell is coming..."
"https://www.shelliscoming.com/"
("binary-exploitation" "reversing" "security" "windows" "x86"))
+ ("0xca7"
+ "https://0xca7.github.io/"
+ ("reversing" "malware" "rust"))
("Mildly entertainingᵝ"
"https://beepb00p.xyz/"
("emacs" "programming" "python"))
diff --git a/data/bookmarks.scm b/data/bookmarks.scm
index 3da01cc..a76c750 100644
--- a/data/bookmarks.scm
+++ b/data/bookmarks.scm
@@ -357,4 +357,10 @@ are interested in this sort of thing.")))
("bitcoin" "politics" "sustainability"))
("Consider SQLite"
"https://blog.wesleyac.com/posts/consider-sqlite"
- ("databases")))
+ ("databases"))
+ ("Noam Chomsky Speaks on What ChatGPT Is Really Good For"
+ "https://chomsky.info/20230503-2/"
+ ("ai" "llm"))
+ ("LLMs are still surprisingly bad at some simple tasks"
+ "https://shkspr.mobi/blog/2025/09/llms-are-still-surprisingly-bad-at-simple-tasks/"
+ ("ai" "llm")))
diff --git a/jakob/builder/blog.scm b/jakob/builder/blog.scm
index e03c3e2..003f96c 100644
--- a/jakob/builder/blog.scm
+++ b/jakob/builder/blog.scm
@@ -49,7 +49,7 @@
;;;
(define (build-comment-url post)
- (format #f "/api/comment-form/~a" (post-slug post)))
+ (format #f "/apps/comment-form/~a" (post-slug post)))
(define (render-article post)
"Return the SHTML for POST's contents."
diff --git a/jakob/builder/cookbook.scm b/jakob/builder/cookbook.scm
index a0be836..f221107 100644
--- a/jakob/builder/cookbook.scm
+++ b/jakob/builder/cookbook.scm
@@ -32,9 +32,11 @@
#:use-module (srfi srfi-26)
#:export (cookbook))
+
+
;;; Commentary:
;;;
-;;; A builder for a cookbook, based on Haunt's default 'blog' builder.
+;;; A custom cookbook builder.
;;;
;;; Code:
@@ -50,7 +52,7 @@
;;; Rendering.
;;;
-(define (tags-to-spans tags)
+(define (tags->spans tags)
(define (tag->class tag)
(string-map (lambda (c) (if (char=? c #\:) #\- c)) tag))
(define (make-class tag)
@@ -62,7 +64,7 @@
(($ <cookbook-page> path metadata body)
(let* ((title (or (assq-ref metadata 'title) "Untitled"))
(content `((h1 ,title)
- (p ,(tags-to-spans (assq-ref metadata 'tags)))
+ (p ,(tags->spans (assq-ref metadata 'tags)))
,@body
(p ,(hyperlink "/cookbook/" "Back to Cookbook"))))
(content (theme #:title title #:content content)))
@@ -78,7 +80,7 @@
(format #f "/cookbook/recipes/~a" thumb)
"/cookbook/recipes/default-thumb.jpg")))))
(div (a (@ (href ,(format #f "/~a" path))) (h3 ,title))
- (p ,(tags-to-spans (assq-ref metadata 'tags)))))))))
+ (p ,(tags->spans (assq-ref metadata 'tags)))))))))
;;;
diff --git a/jakob/reader/org-mode-prime.scm b/jakob/reader/org-mode-prime.scm
index 118f1a6..85c4296 100644
--- a/jakob/reader/org-mode-prime.scm
+++ b/jakob/reader/org-mode-prime.scm
@@ -32,5 +32,6 @@
(make-reader (reader-matcher org-mode-reader)
(lambda args
(let-values (((metadata sxml) (apply (reader-proc org-mode-reader) args)))
- (values metadata
- (rewrite-absolute-urls-as-relative sxml))))))
+ (let* ((sxml (rewrite-absolute-urls-as-relative sxml))
+ (sxml (flatten-section-headings sxml)))
+ (values metadata sxml))))))
diff --git a/jakob/utils/features.scm b/jakob/utils/features.scm
new file mode 100644
index 0000000..4dc66db
--- /dev/null
+++ b/jakob/utils/features.scm
@@ -0,0 +1,24 @@
+;;; Copyright © 2019 - 2025 Jakob L. Kreuze <zerodaysfordays@sdf.org>
+;;;
+;;; This program is free software; you can redistribute it and/or
+;;; modify it under the terms of the GNU General Public License as
+;;; published by the Free Software Foundation; either version 3 of the
+;;; License, or (at your option) any later version.
+;;;
+;;; This program is distributed in the hope that it will be useful,
+;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+;;; General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU General Public License
+;;; along with this program. If not, see
+;;; <http://www.gnu.org/licenses/>.
+
+(define-module (jakob utils features)
+ #:export (feature-enabled?))
+
+(define %feature-condition-alist
+ `((food-gallery . ,(lambda () (getenv "HAUNT_INCLUDE_FOOD_GALLERY")))))
+
+(define (feature-enabled? key)
+ ((assq-ref %feature-condition-alist key)))
diff --git a/jakob/utils/sxml.scm b/jakob/utils/sxml.scm
index 0fc34d2..5ab08a9 100644
--- a/jakob/utils/sxml.scm
+++ b/jakob/utils/sxml.scm
@@ -23,7 +23,8 @@
script
sanitize-subtree
- rewrite-absolute-urls-as-relative))
+ rewrite-absolute-urls-as-relative
+ flatten-section-headings))
;;;
@@ -89,3 +90,40 @@ sanitize the resultant subtree."
((xs ...)
(map rewrite-absolute-urls-as-relative xs))
(elem elem)))
+
+(define (flatten-section-headings tree)
+ """Flattens extraneous sections output by Org-mode
+
+This function traverses a SXML tree and removes the outermost `<div>`
+elements that represent Org-mode section headings.
+
+Args:
+ tree: A SXML tree.
+
+Returns:
+ A SXML tree with the outermost Org section heading `<div>` elements
+removed and their contents flattened.
+ """
+ (define (flatten-section-headings-inner tree)
+ (define (is-org-section form)
+ (match form
+ ((or ('div ('@ ('id id) ('class _))
+ rest ...)
+ ('div ('@ ('id id))
+ rest ...)
+ ('div ('@ ('role "doc-footnote") ('class id))
+ rest ...))
+ (or (string=? "footpara" id)
+ (string-prefix? "text-footnotes" id)
+ (string-prefix? "outline-container-org" id)
+ (string-prefix? "text-org" id)))
+ (_ #f)))
+ (match tree
+ ((? is-org-section
+ ('div ('@ _ ...)
+ rest ...))
+ (append-map flatten-section-headings-inner rest))
+ ((xs ...)
+ (list (append-map flatten-section-headings-inner xs)))
+ (elem (list elem))))
+ (first (flatten-section-headings-inner tree)))
diff --git a/pages/about-complete.org b/pages/about-complete.org
index a1832ae..3e59566 100644
--- a/pages/about-complete.org
+++ b/pages/about-complete.org
@@ -14,7 +14,7 @@ img {
If you're interested in game engine tech, my younger brother is working on a game engine and graphics engine of his own. You can read about it on [[https://p0ly.com/][his website]]. I like to think of him as my protégé, but he's mostly self-taught.
-My significant other, Oli, also has a [[https://hewwo.ooo/][website built with Haunt]].
+My fiancée, Oli, also has a [[https://hewwo.ooo/][website built with Haunt]].
* Curriculum Vitae
diff --git a/pages/about.org b/pages/about.org
index 1f4a6b8..0ead4e1 100644
--- a/pages/about.org
+++ b/pages/about.org
@@ -46,7 +46,7 @@ The website is built using the [[https://dthompson.us/projects/haunt.html][Haunt
I am not currently aware of any attempts to censor this website. Nonetheless, I maintain mirrors on [[http://jakobyallfrbd3herebyee7hug3hxs2ma6aflmmllk35d7dyiwp3adid.onion/][Tor]] and [[http://mtgqjdrqqpovyhx7tortulrnvakbptsi3w5yintmo6lqapdd3hnq.b32.i2p][I2P]] should you have difficulty accessing this website in your locality.
-No part of this website was generated by a [[https://en.wikipedia.org/wiki/Large_language_model][large language model]] (LLM), and the use of other forms of [[https://en.wikipedia.org/wiki/Generative_artificial_intelligence][generative artificial intelligence]] are explicitly highlighted.
+I aim to be as transparent as possible about the use of [[https://en.wikipedia.org/wiki/Generative_artificial_intelligence][generative artificial intelligence]] on this website. Unless stated otherwise, all text and source code is written by myself.
** Symbology
diff --git a/pages/changelog.org b/pages/changelog.org
index 9e9049f..68a4e90 100644
--- a/pages/changelog.org
+++ b/pages/changelog.org
@@ -4,6 +4,24 @@
A record of any notable user-facing changes made to this website. For more detail, refer to the log of the [[https://git.sr.ht/~jakob/blog][Git repository]].
+** Wednesday, August 20, 2025
+
+- Added a historical article, [[http://jakob.space/blog/replacing-anki-with-org-drill.html][Replacing Anki with org-drill]], originally published in 2018 that was lost when I moved from Flask to a static site generator.
+
+** Sunday, August 17, 2025
+
+- Published a new article: [[http://jakob.space/blog/usb-pd-is-pretty-cool.html][USB-PD is Pretty Cool]].
+
+** Sunday, July 20, 2025
+
+- Published a new article: [[https://jakob.space/blog/spaced-repetition-anywhere.html][Spaced Repetition Anywhere]].
+
+** Sunday, June 29, 2025
+
+- Added two recipes to the cookbook:
+ - Fried Chicken
+ - Waakye and Stew
+
** Saturday, May 31, 2025
- Added a cookbook containing the following recipes:
diff --git a/posts-assets/usb-pd-is-pretty-cool-food-warmer-circuit.svg b/posts-assets/usb-pd-is-pretty-cool-food-warmer-circuit.svg
new file mode 100644
index 0000000..df5af55
--- /dev/null
+++ b/posts-assets/usb-pd-is-pretty-cool-food-warmer-circuit.svg
@@ -0,0 +1,184 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ width="63.569756mm"
+ height="54.93486mm"
+ viewBox="0 0 63.569756 54.93486"
+ version="1.1"
+ id="svg1"
+ xml:space="preserve"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg"><defs
+ id="defs1" /><g
+ id="g5"
+ transform="matrix(2.5332666,0,0,2.5332666,-104.41006,-329.82109)"><ellipse
+ style="fill:none;stroke:#000000;stroke-width:0.244748;stroke-opacity:1"
+ id="path2"
+ cx="44.035557"
+ cy="146.09866"
+ rx="2.2002535"
+ ry="2.2656367" /><path
+ style="fill:none;stroke:#000000;stroke-width:0.276525;stroke-opacity:1"
+ d="m 44.025256,144.46618 v 0.82064"
+ id="path3" /><path
+ style="fill:none;stroke:#000000;stroke-width:0.249451;stroke-opacity:1"
+ d="m 43.643835,144.88228 h 0.739726"
+ id="path4" /><path
+ style="fill:none;stroke:#000000;stroke-width:0.264583;stroke-opacity:1"
+ d="M 43.967466,147.54067 H 44.39512"
+ id="path5" /></g><g
+ id="g8"
+ transform="translate(-64.422372,-135.82709)"><path
+ style="fill:#000000"
+ d="m 64.422372,149.42157 v -7.14375 h 7.143749 7.14375 v 7.14375 7.14375 h -7.14375 -7.143749 z m 13.758333,0.20051 V 143.208 l -6.35,6.34586 c -3.4925,3.49023 -6.35,6.37656 -6.35,6.41408 0,0.0375 2.8575,0.0682 6.35,0.0682 h 6.35 z m -0.529167,-6.74688 c 0,-0.0375 -2.8575,-0.0682 -6.35,-0.0682 h -6.35 v 6.41408 6.41408 l 6.35,-6.34587 c 3.4925,-3.49023 6.35,-6.37656 6.35,-6.41408 z"
+ id="path1" /><path
+ style="fill:none;stroke:#000000;stroke-width:0.264583;stroke-opacity:1"
+ d="m 67.132159,144.85639 c 3.285504,0.0163 3.285504,0.0163 3.285504,0.0163"
+ id="path6" /><path
+ style="fill:none;stroke:#000000;stroke-width:0.264583;stroke-opacity:1"
+ d="m 67.295619,145.57561 c 0.997093,0 0.997093,0 0.997093,0"
+ id="path7" /><path
+ style="fill:none;stroke:#000000;stroke-width:0.264583;stroke-opacity:1"
+ d="m 69.289805,145.59195 c 0.948055,-0.0163 0.948055,-0.0163 0.948055,-0.0163"
+ id="path8" /><path
+ style="fill:none;stroke:#000000;stroke-width:0.264583;stroke-opacity:1"
+ d="m 73.583597,153.76978 c 3.285504,0.0163 3.285504,0.0163 3.285504,0.0163"
+ id="path6-0" /><path
+ style="fill:none;stroke:#000000;stroke-width:0.264583;stroke-opacity:1"
+ d="m 73.747057,154.489 c 0.997093,0 0.997093,0 0.997093,0"
+ id="path7-9" /><path
+ style="fill:none;stroke:#000000;stroke-width:0.264583;stroke-opacity:1"
+ d="m 75.741243,154.50534 c 0.948055,-0.0163 0.948055,-0.0163 0.948055,-0.0163"
+ id="path8-3" /></g><g
+ id="g14"
+ transform="translate(-40.738267,-109.47435)"><circle
+ style="fill:none;stroke:#000000;stroke-width:0.264583;stroke-opacity:1"
+ id="path13"
+ cx="86.409241"
+ cy="122.28596"
+ r="4.9469175" /><text
+ xml:space="preserve"
+ style="font-size:6.22996px;text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.519163;stroke-opacity:1"
+ x="84.760262"
+ y="124.67165"
+ id="text13"><tspan
+ id="tspan13"
+ style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.519163"
+ x="84.760262"
+ y="124.67165">θ</tspan></text></g><path
+ style="fill:none;stroke:#000000;stroke-width:0.565;stroke-dasharray:none;stroke-opacity:1"
+ d="M 7.053655,34.49483 V 20.44003"
+ id="path14" /><path
+ style="fill:none;stroke:#000000;stroke-width:0.565;stroke-dasharray:none;stroke-opacity:1"
+ d="M 7.146122,6.84757 V 0.2825 h 38.465753 v 7.67466"
+ id="path15" /><path
+ style="fill:none;stroke:#000000;stroke-width:0.565;stroke-dasharray:none;stroke-opacity:1"
+ d="m 45.715897,17.75852 v 7.85959 H 31.846036 v 3.7911"
+ id="path16" /><path
+ style="fill:none;stroke:#000000;stroke-width:0.565;stroke-dasharray:none;stroke-opacity:1"
+ d="m 40.93442,25.63959 v 4.06849"
+ id="path17" /><path
+ style="fill:none;stroke:#000000;stroke-width:0.565;stroke-dasharray:none;stroke-opacity:1"
+ d="m 45.111244,25.62933 h 4.900686 v 4.016642"
+ id="path18" /><path
+ style="fill:none;stroke:#000000;stroke-width:0.565;stroke-dasharray:none;stroke-opacity:1"
+ d="m 50.085485,25.6319 h 8.969177 v 4.34589"
+ id="path19" /><path
+ style="fill:none;stroke:#000000;stroke-width:0.564999;stroke-dasharray:none;stroke-opacity:1"
+ d="m 31.824168,46.7914 v 1.69996 h 27.330163"
+ id="path20" /><path
+ style="fill:none;stroke:#000000;stroke-width:0.60363;stroke-dasharray:none;stroke-opacity:1"
+ d="M 59.092912,48.76876 V 46.52987"
+ id="path21" /><path
+ style="fill:none;stroke:#000000;stroke-width:0.564999;stroke-dasharray:none;stroke-opacity:1"
+ d="m 49.935305,46.13757 v 2.35379"
+ id="path22" /><path
+ style="fill:none;stroke:#000000;stroke-width:0.564999;stroke-dasharray:none;stroke-opacity:1"
+ d="m 40.912429,46.46449 v 2.02687"
+ id="path23" /><path
+ style="fill:none;stroke:#000000;stroke-width:0.564999;stroke-dasharray:none;stroke-opacity:1"
+ d="m 46.259134,48.73455 c 0,6.01028 0,5.91781 0,5.91781 H 7.146121 v -8.32192"
+ id="path24" /><text
+ xml:space="preserve"
+ style="font-size:3.175px;text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.265;stroke-dasharray:none;stroke-opacity:1"
+ x="9.495821"
+ y="34.564758"
+ id="text24"><tspan
+ id="tspan24"
+ style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.265;stroke-dasharray:none"
+ x="9.495821"
+ y="34.564758">20V</tspan></text><text
+ xml:space="preserve"
+ style="font-size:3.175px;text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.265;stroke-dasharray:none;stroke-opacity:1"
+ x="9.495821"
+ y="5.9923172"
+ id="text25"><tspan
+ id="tspan25"
+ style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.265;stroke-dasharray:none"
+ x="9.495821"
+ y="5.9923172">12V</tspan></text><text
+ xml:space="preserve"
+ style="font-size:1.41111px;text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264999;stroke-dasharray:none;stroke-opacity:1"
+ x="33.513611"
+ y="37.845104"
+ id="text26"><tspan
+ id="tspan26"
+ style="font-size:1.41111px;stroke-width:0.265"
+ x="33.513611"
+ y="37.845104">7.2Ω</tspan></text><text
+ xml:space="preserve"
+ style="font-size:1.41111px;text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264999;stroke-dasharray:none;stroke-opacity:1"
+ x="42.506844"
+ y="37.845104"
+ id="text27"><tspan
+ id="tspan27"
+ style="font-size:1.41111px;stroke-width:0.265"
+ x="42.506844"
+ y="37.845104">7.2Ω</tspan></text><text
+ xml:space="preserve"
+ style="font-size:1.41111px;text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264999;stroke-dasharray:none;stroke-opacity:1"
+ x="51.884621"
+ y="37.845104"
+ id="use28"><tspan
+ id="tspan11"
+ style="font-size:1.41111px;stroke-width:0.265"
+ x="51.884621"
+ y="37.845104">7.2Ω</tspan></text><text
+ xml:space="preserve"
+ style="font-size:1.41111px;text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264999;stroke-dasharray:none;stroke-opacity:1"
+ x="60.450825"
+ y="37.845104"
+ id="use29"><tspan
+ id="tspan12"
+ style="font-size:1.41111px;stroke-width:0.265"
+ x="60.450825"
+ y="37.845104">7.2Ω</tspan></text><path
+ d="m 31.817802,29.393761 v 4.380882 l 1.327187,0.73015 -2.654373,1.460297 2.654373,1.460288 -2.654373,1.460297 2.654373,1.460287 -2.654373,1.460298 1.327186,0.730148 v 4.380884"
+ stroke="#000000"
+ stroke-width="1"
+ stroke-linejoin="bevel"
+ fill="none"
+ id="path1-6"
+ style="stroke-width:0.594567;stroke-dasharray:none" /><path
+ d="m 40.916602,29.393761 v 4.380882 l 1.327187,0.73015 -2.654373,1.460297 2.654373,1.460288 -2.654373,1.460297 2.654373,1.460287 -2.654373,1.460298 1.327186,0.730148 v 4.380884"
+ stroke="#000000"
+ stroke-width="1"
+ stroke-linejoin="bevel"
+ fill="none"
+ id="path9"
+ style="stroke-width:0.594567;stroke-dasharray:none" /><path
+ d="m 50.015402,29.393761 v 4.380882 l 1.327187,0.73015 -2.654373,1.460297 2.654373,1.460288 -2.654373,1.460297 2.654373,1.460287 -2.654373,1.460298 1.327186,0.730148 v 4.380884"
+ stroke="#000000"
+ stroke-width="1"
+ stroke-linejoin="bevel"
+ fill="none"
+ id="path10"
+ style="stroke-width:0.594567;stroke-dasharray:none" /><path
+ d="m 59.061454,29.393761 v 4.380882 l 1.327187,0.73015 -2.654373,1.460297 2.654373,1.460288 -2.654373,1.460297 2.654373,1.460287 -2.654373,1.460298 1.327186,0.730148 v 4.380884"
+ stroke="#000000"
+ stroke-width="1"
+ stroke-linejoin="bevel"
+ fill="none"
+ id="path11"
+ style="stroke-width:0.594567;stroke-dasharray:none" /></svg>
diff --git a/posts/replacing-anki-with-org-drill.org b/posts/replacing-anki-with-org-drill.org
new file mode 100644
index 0000000..f2f5f53
--- /dev/null
+++ b/posts/replacing-anki-with-org-drill.org
@@ -0,0 +1,63 @@
+#+TITLE: Replacing Anki With org-drill
+#+DATE: <2018-07-13 Fri 09:53>
+#+TAGS: writeup emacs practices lisp math
+
+Recently, I read Michael Nielsen's essay, [[https://augmentingcognition.com/ltm.html]["Augmenting Cognition"]]. It talks about some very interesting use cases for the spaced repetition software "Anki" that made me want to try it out again. I'm familiar with Anki, as I used it extensively throughout my last year of high school to study for AP exams. At the time, Anki's "killer feature" for me over similar software was being able to typeset mathematical notation in LaTeX (the exams were Chemistry and Calculus, so almost all of the material to memorize was mathematical notation). It's a great piece of software; I've been using it with the brother I'm helping through summer school. But ever since I began using Gentoo, I've been trying to avoid packages like QtWebView, which has deterred me from installing Anki on my machine. With a little bit of searching, however, I found that there was an Emacs package for spaced repetition named 'org-drill', so I decided to check it out.
+
+org-drill is included in org by default (which happens to be included in Emacs by default), but it does need to be enabled. The steps to do so are outlined on the corresponding [[https://orgmode.org/worg/org-contrib/org-drill.html][worg page]]. So far, I've used it to study German vocabulary and the material for my ham radio license exams, and I'm very happy with it. It has all of the features you might want from Anki, like Cloze deletion and double-sided cards, but I find that card creation is even more intuitive in org markup. Clozes are as simple as enclosing the answers in square brackets, and multi-sided cards just entail making multiple headings and setting the ":DRILL_CARD_TYPE:". You can even write your own card types in elisp. Another benefit of using org markup as the source for cards is that I can easily transform a plain text file into a deck using emacs macros.
+
+Unlike Anki, however, org-drill has support for the SM5 and SM8 scheduling algorithms. [[https://apps.ankiweb.net/docs/manual.html#what-algorithm][Anki is quite outspoken about the benefits of SM2 over the later renditions]], but I appreciate that I at least have the option to use these schedulers if I want to. The algorithms' parameters can also be finely tuned; the one I've found most useful is 'org-drill-learn-fraction', which I can use to decrease the amount of time before I see a card again.
+
+As I mentioned earlier, the feature that brought me to Anki was its support for typesetting math with LaTeX. Emacs certainly has support for rendering LaTeX, but I have a pretty wonky setup where I'm running Emacs in a terminal emulator, so what I opted for instead was a typesetting language that renders to unicode text. There are quite a few of these, but the one I was most impressed with is [[https://arthursonzogni.com/Diagon/][Diagon]]. It's meant to be run in the browser, but the backend is written in C++ and can be compiled to run natively. Be warned, however, that the build system does require Java.
+
+First, I replace 'src/main.cpp' with the following. The version in VCS will unconditionally run the SequenceTranslator, but this modification enables us to select which translator to use from a command-line argument.
+
+#+BEGIN_SRC cpp
+#include "translator/Translator.h"
+#include <iostream>
+
+int main(int argc, const char **argv) {
+ if (argc != 2) {
+ std::cerr << "usage: " << argv[0] << " [translator]" << std::endl;
+ return 1;
+ }
+
+ std::string input;
+ for (std::string line; std::getline(std::cin, line);) {
+ input += line + "\n";
+ }
+
+ auto translator = TranslatorFromName(argv[1]);
+ std::cout << (*translator)(input, "") << std::endl;
+
+ return 0;
+}
+#+END_SRC
+
+Then, compiling is as easy as
+
+#+BEGIN_SRC sh
+cd tools/antlr/
+./download_and_patch.sh
+cd ../../
+mkdir build
+cd build
+cmake ..
+make
+#+END_SRC
+
+And for Emacs integration, I've added the following to my '.emacs'
+
+#+BEGIN_SRC elisp
+;; Applies Diagon's "Math" formatter to the current region, replacing
+;; the contents of the region with the formatted output.
+(defun format-math-at-region ()
+ (interactive)
+ (let* ((math-to-format (buffer-substring (region-beginning) (region-end)))
+ (command (format "echo \"%s\" | diagon Math" math-to-format))) ;; Bad and hacky. I'm aware.
+ (kill-region (region-beginning) (region-end))
+ (insert (string-trim-right (shell-command-to-string command)))))
+#+END_SRC
+
+It's not as powerful as LaTeX, but it certainly suits my needs.
+
diff --git a/posts/spaced-repetition-anywhere.org b/posts/spaced-repetition-anywhere.org
new file mode 100644
index 0000000..3ae66d3
--- /dev/null
+++ b/posts/spaced-repetition-anywhere.org
@@ -0,0 +1,164 @@
+#+TITLE: Spaced Repetition Anywhere
+#+DATE: <2025-07-20 Sun 13:57>
+#+TAGS: writeup programming webdev javascript artifical-intelligence
+#+META-TAGS: (("twitter:card" . "summary") ("twitter:site" . "@0daysfordays") ("twitter:creator" . "@0daysfordays") ("og:description" . "A short article on my experience using an LLM model running locally to put together a flash card program.") ("og:image" . "https://jakob.space/static/image/opengraph-icon-spaced-repetition-anywhere.png"))
+
+Spaced repetition[fn:1] is an effective habit for memorizing small fragments of information for, effectively, an indefinite amount of time. The idea is to repeatedly challenge your recall of a piece of information, typically with a sort of [[https://en.wikipedia.org/wiki/Flashcard][flashcard]], doing so at a frequency determined by your previous recall performance: new information is challenged frequently until you remember it, and previously learned information is challenged less frequently. The concept is simple enough to implement with [[https://en.wikipedia.org/wiki/Leitner_system][a few index cards and a shoebox]]. Despite the simplicity, the landscape of software implementations of spaced repetition is relatively small. The venerable [[https://en.wikipedia.org/wiki/Anki_(software)][Anki]] is the best-known spaced repetition application, and I've used [[https://orgmode.org/worg/org-contrib/org-drill.html][org-drill]] and [[https://github.com/l3kn/org-fc][org-fc]] in the past, but other than those three, I'm not aware of many other programs for it. These existing systems are fine, but I have a fairly specific use-case that none are appropriate for: I might want to do my reviews on a system where I'm unable to install Anki or GNU Emacs, and I don't want to have to trust a third-party with my cards. This rules out services like Quizlet and AnkiWeb. It's a simple enough concept, so I wanted to see if I could write a flash card application contained within a single HTML file. All we really need is a spaced repetition scheduler algorithm, a way of implementing the "challenge" piece in the browser, and a way of storing cards and their scheduling parameters across reviews.
+
+* The Algorithm
+
+The best-known algorithm for spaced repetition is [[https://www.super-memory.com/english/ol/sm2.htm][SuperMemo 2]] (SM-2), developed by [[https://en.wikipedia.org/wiki/Piotr_Wo%C5%BAniak_(researcher)][Piotr Woźniak]] in 1990 for SuperMemo and used in Anki. SM-2 works well, but there are better scheduling algorithms available these days. My current favorite is the [[https://github.com/open-spaced-repetition/free-spaced-repetition-scheduler][Free Spaced Repetition Scheduling]] (FSRS) algorithm because it [[https://github.com/open-spaced-repetition/srs-benchmark/blob/main/README.md#superiority][empirically outperforms the others]] by a significant margin. For the theory behind it, I would recommend Domenic Denicola's article [[https://domenic.me/fsrs/][Spaced Repetition Systems Have Gotten Way Better]]. For our purposes, we can just treat the scheduling algorithm as a black box. Given some parameters on the card (in the case of FSRS: retrievability, stability, and difficulty) and a grade based on how well we recalled some information, we generate new parameters and a time in the future when we will next review the card.
+
+Treating the algorithm as a black box, I took Fernando Borretti's implementation in his article [[https://borretti.me/article/implementing-fsrs-in-100-lines][Implementing FSRS in 100 Lines]], dumped it into =Qwen3-30B-A3B-UD-Q3_K_XL=[fn:2], and asked it to translate the code to JavaScript. It made a few mistakes around the use of enums, but I was able to get it working with a handful of changes.
+
+#+BEGIN_EXPORT html
+<div class="section-fold" data-name="JavaScript implementation of FSRS5.">
+#+END_EXPORT
+
+#+BEGIN_SRC javascript
+/***********************************************************************
+ ,* Actual FSRS5 algorithm *
+ ,***********************************************************************/
+
+const W = [
+ 0.40255, 1.18385, 3.173, 15.69105, 7.1949, 0.5345, 1.4604, 0.0046, 1.54575, 0.1192, 1.01925,
+ 1.9395, 0.11, 0.29605, 2.2698, 0.2315, 2.9898, 0.51655, 0.6621,
+];
+
+const F = 19.0 / 81.0;
+const C = -0.5;
+
+const Grade = {
+ forgot: 1.0,
+ hard: 2.0,
+ good: 3.0,
+ easy: 4.0
+};
+
+function retrievability(t, s) {
+ return Math.pow(1.0 + F * (t / s), C);
+}
+
+function interval(r_d, s) {
+ return (s / F) * (Math.pow(r_d, 1.0 / C) - 1.0);
+}
+
+function s_0(g) {
+ switch (g) {
+ case Grade.forgot: return W[0];
+ case Grade.hard: return W[1];
+ case Grade.good: return W[2];
+ case Grade.easy: return W[3];
+ }
+}
+
+function s_success(d, s, r, g) {
+ const t_d = 11.0 - d;
+ const t_s = Math.pow(s, -W[9]);
+ const t_r = Math.exp(W[10] * (1.0 - r)) - 1.0;
+ const h = g === Grade.hard ? W[15] : 1.0;
+ const b = g === Grade.easy ? W[16] : 1.0;
+ const c = Math.exp(W[8]);
+ const alpha = 1.0 + t_d * t_s * t_r * h * b * c;
+ return s * alpha;
+}
+
+function s_fail(d, s, r) {
+ const d_f = Math.pow(d, -W[12]);
+ const s_f = Math.pow(s + 1.0, W[13]) - 1.0;
+ const r_f = Math.exp(W[14] * (1.0 - r));
+ const c_f = W[11];
+ const result = d_f * s_f * r_f * c_f;
+ return Math.min(result, s);
+}
+
+function stability(d, s, r, g) {
+ return g === Grade.forgot ? s_fail(d, s, r) : s_success(d, s, r, g);
+}
+
+function clamp_d(d) {
+ return Math.min(10.0, Math.max(1.0, d));
+}
+
+function d_0(g) {
+ return clamp_d(W[4] - Math.exp(W[5] * (g - 1.0)) + 1.0);
+}
+
+function difficulty(d, g) {
+ return clamp_d(W[7] * d_0(Grade.easy) + (1.0 - W[7]) * dp(d, g));
+}
+
+function dp(d, g) {
+ return d + delta_d(g) * ((10.0 - d) / 9.0);
+}
+
+function delta_d(g) {
+ return -W[6] * (g - 3.0);
+}
+#+END_SRC
+
+#+BEGIN_EXPORT html
+</div>
+#+END_EXPORT
+
+Borretti's article provided a simulator which was helpful for testing whether the translated code was actually correct.
+
+* The Boring Part: A User Interface
+
+Even though I spend a lot of time on building my personal website, I find many aspects of web development to be uninteresting. The idea for this self-contained spaced repetition software is one I've had for over a year now, but I've consistently put it off because I couldn't motivate myself to spend my free time tackling the step of "create a user interface in HTML, CSS, and JavaScript". Not because it's especially hard, but because I'd just rather do something else. Fortunately, resources on web development are prevalent online, I'm building something from fragments that almost certainly appear verbatim in the training data of large language models, and I just want the bare minimum that works, so this is a perfect use-case for [[https://en.wikipedia.org/wiki/Vibe_coding][vibe coding]]. Qwen did a decent job at translating the FSRS algorithm into JavaScript, and it did quite well building up the UI for me. I ended up completely scrapping the actual flashcard piece it generated, but I kept the other components of the UI with relatively minor changes. Overall, I'm happy with the results.
+
+#+CAPTION: SRS Anywhere in LibreWolf showing the card for 难以置信 from my HSK7-9 deck.
+[[./spaced-repetition-anywhere-browser-screenshot.png]]
+
+With some minor changes, I got it to work on mobile, too.
+
+#+CAPTION: SRS Anywhere in Fennec showing the card for 就 from my HSK1 deck.
+[[./spaced-repetition-anywhere-mobile-screenshot.png]]
+
+* Data Model
+
+I'm familiar with IndexedDB and other in-browser storage mechanisms, but I wanted the cards and their scheduling metadata to be portable, and to persist through a browser refresh, so I went with storing cards and metadata in a JSON file. The program loads in the cards initially from a JSON file, conducts the review, and then prompts the user to download a JSON file containing all of the cards and their updated scheduling metadata. I wrote the parsing and review logic by hand, but asked Qwen to come up with this snippet for the final piece of giving the updated cards back to the user. It saved me from having to figure out that the Blob API is what I wanted here, and then reading through the MDN article on figuring out how to use it.
+
+#+BEGIN_SRC javascript
+function downloadJSON(obj) {
+ // Convert the object to a JSON string (with default formatting)
+ const jsonString = JSON.stringify(obj, null, 2); // 'null, 2' adds indentation for readability
+
+ // Create a Blob with the JSON content
+ const blob = new Blob([jsonString], { type: 'application/json' });
+
+ // Generate a URL for the Blob
+ const url = URL.createObjectURL(blob);
+
+ // Create an anchor element
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = 'data.json'; // Default filename
+
+ // Append the anchor to the body (required for some browsers)
+ document.body.appendChild(a);
+
+ // Simulate a click on the anchor to trigger the download
+ a.click();
+
+ // Clean up
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+}
+#+END_SRC
+
+The =cards.json= file is an array of objects containing at least the keys =id=, =front=, and =back=. The only requirement for =id= is that it's unique across the deck: it could be a UUID, but it could also be something more descriptive. =front= and =back= are HTML fragments that are injected into the page when the card is shown.[fn:3]
+
+* Conclusions
+
+It felt empowering to do all of the code generation on my Intel Arc A770 using [[https://github.com/ggml-org/llama.cpp][free software]]. The experience alleviated some of my skepticism around the usefulness of LLM's for programming use-cases, even if it required some corralling. I'll probably continue to use it for these inconsequential projects where my own activation energy is the main roadblock.
+
+You can find the repository on SourceHut: [[https://git.sr.ht/~jakob/srs-anywhere][~jakob/srs-anywhere]].
+
+---
+
+[fn:1] For more on the concept, I would highly recommend Michael Nielsen's article [[https://augmentingcognition.com/ltm.html][Augmenting Long-term Memory]] (2018) and Nicky Case's [[https://ncase.me/remember/][How To Remember Anything Forever-ish]] (2018).
+
+[fn:2] Regrettably, I suppose. I'm drawn to Qwen because it's Apache-licensed and actually quite good. I'm hopeful that we'll have more decent open-weight models from friendlier nations in the near future, but for now it's hard to find something comparable to DeepSeek or Qwen.
+
+[fn:3] They aren't sanitized because I can't think of a reason where you would care about XSS or other forms of content injection in a single-page application that you've loaded from the local filesystem -- other than vulnerabilities that would be exploitable from any untrusted website. If you can come up with a way to exploit this in a legitimately useful way, please write me and let me know.
diff --git a/posts/usb-pd-is-pretty-cool.org b/posts/usb-pd-is-pretty-cool.org
index e5ffb2f..6315778 100644
--- a/posts/usb-pd-is-pretty-cool.org
+++ b/posts/usb-pd-is-pretty-cool.org
@@ -1,173 +1,158 @@
-#+TITLE: USB-PD is Pretty Cool
-#+DATE:
-#+TAGS: writeup hardware diy 3d-printing
-#+META-TAGS: (("twitter:card" . "summary") ("twitter:site" . "@0daysfordays") ("twitter:creator" . "@0daysfordays") ("og:description" . "An attempt to document my experiences and rationale for wanting to use a PinePhone, as well as my thoughts on mobile Linux in general.") ("og:image" . "https://jakob.space/static/image/pinephone-1.jpg"))
+#+TITLE: USB-PD Is Pretty Cool
+#+DATE: <2025-08-17 Sun 19:19>
+#+TAGS: writeup 3d-printing electronics hardware
+#+META-TAGS: (("twitter:card" . "summary") ("twitter:site" . "@0daysfordays") ("twitter:creator" . "@0daysfordays") ("og:description" . "Using a USB-PD trigger module and some other off-the-shelf components for warming up a brown bag lunch.") ("og:image" . "https://jakob.space/blog/usb-pd-is-pretty-cool-opengraph.jpg"))
-Alternatively, "Putting Too Much Thought Into Lunch," or "Often, High School Physics is All You Need." This is a device I built for warming a meal away from home. A relatively simple solution to the problem of needing to feed yourself
+Packed lunches are a great way to save money, but being limited to foods palatable at room temperature (or colder) can be a little discouraging. In most American workplaces, a microwave oven is readily available in the breakroom, making it easy to reheat a home-cooked meal. However, after leaving a desk-bound white-collar job for one that often takes me into the field, that's only occasionally an option. So I DIY'd a device for warming up food that's portable and usable in almost any situation.
-just pack a sandwich or something that doesn't need to be warmed up, but especially during the cold winters of D&C, sometimes you just want a whole bowl of soup.
+As any good engineer knows, it's important to evaluate existing solutions before building your own. Let's go through a few typical situations I might be in during a mealtime:
-If I'm going to be away from home during a mealtime, I'll bring my food with me so I can avoid having to buy awful takeout or an overpriced restaurant experience. The problem is that, unless you're bringing something like a sandwich that's meant to be consumed cold, you need some way to warm the food you've taken along with you. During a typical workday, this is no problem, since there's a microwave in the breakroom. Anywhere else, though, and I'm probably out of luck. Or, I was, until I built my own solution, which I'd like to share with you today.
+1. It's a workday for me and I'm not at my typical worksite: likely, I'm in the field.
+2. I'm out exploring the city, or meeting up at a venue with friends.
+3. I've taken a break to charge my car while driving across the country.
+4. I'm waiting for my train at an Amtrak station.
+5. I am currently riding on a train.
+6. I'm waiting for my flight at an airport.
+7. I am currently on an airplane.
+What I've been doing is using what's marketed as a "portable oven." Mine is the (now-discontinued) SabotHeat. It's the heating element of a slow cooker, placed in an insulated lunchbox, with a standard [[https://en.wikipedia.org/wiki/NEMA_connector][NEMA 1-15P connector]] to be plugged into a wall outlet. It works well, but it can take as long as two hours to warm a portion of food to my preferred temperature. In the case of (1.), (2.), (4.), and (6.), I'm moving around enough that I can't sit by an outlet for that long. In the case of (3.), and (7.), there likely isn't even an outlet available. (5.) is the sweet spot: a train ride from Washington to Boston usually exceeds 7 hours and there's almost always a working 120V outlet available.
+#+CAPTION: The SabotHeat: an insulated bag with a heating element and a wall plug.
+[[./usb-pd-is-pretty-cool-sabotheat.jpg]]
-Coming of age during the COVID- 19 pandemic meant that I was accustomed or comfortable with the idea of not doing any traveling in the foreseeable future. Unfortunately, with my new job, the reality is that I have to travel a lot. So may as well come up with a solution.
+Another approach, albeit far more unhinged, is bringing a pot and a [[https://en.wikipedia.org/wiki/Hot_plate][ceramic hot plate]] (the kind that you cook on). I have actually done this more than once, dehydrating a portion of soup and, while waiting at an airport, bringing a pot of water to a boil to re-constitute the dehydrated soup. It produces hot food in a much shorter amount of time, but then you have to deal with a hot plate that needs to cool down before you can put it back in your bag. And you still need an outlet. It's impractical, even if somewhat entertaining due to how absurd it is to be cooking up a small pot of soup at the airport.
-As you should do, or as any good engineer should do, you need to compare existing solutions to see whether or not they solve your problem. Because sometimes it makes more sense to buy than to build. See, not invented here syndrome. As you can probably predict from the length of this article, I decided not to, or I did not find any of the existing solutions to be suitable. I'll go through each one that I considered and explain why they did not suit my need, because that is what drives the requirements for this project.
+Despite the impracticality, there is an upside to this approach: dehydrated soup isn't a liquid, so it goes through security just fine.
-One thing with the heating pad is that you have to consider the thermal conductivity of your dish. I started with Pyrex Glass, which has a okay thermal conductivity, but compared to something like stainless steel, it's actually more of an insulator, so it takes longer for your food to warm up, but when it is warmed, you eat all stay warm. The stainless steel, it warms up much faster, however, when you take the dish out of the bag, it tends to cool off. Fortunately, this is something that you can work around with, work around fairly easily, by just not taking the dish out of the bag and instead eating it out of the bag
-Another consideration relative to thermal conductivity is the thermal conductivity of your food. Something that is homogenous like a soup is going to warm very well, but if you have a dish that's got rice, some refried beans, some vegetables, some chicken in it. It's actually going to, and you just put it on the heating element, it's not going to warm up that well. However, this is another thing that's relatively easy to work around, is you can add a little bit of water to the bottom of the dish, and that water is going to turn into steam, which will warm the food.
+What I'd like, then, is something between the SabotHeat and the hot plate that can be powered by a battery. There are portable batteries with a built-in inverter and NEMA 5-15 socket, but they're heavy and bulky. What I really want is a warming device that can be powered by the readily-available slim USB-PD batteries you can buy to charge your laptop.
-I didn't have any electronics experience, but I basically knew what I needed to do. I wanted something that generated heat, so a heating element, and I needed some way to power it. I started by finding a suitable thermal element or heating element. I started by looking at PTC or positive thermal coefficient here, but ended up with a standard resistive heater. Why, you might ask? I don't know. It just seemed to make sense. I'm going off of vibes here because I don't have a background electronic design. Once you have the heating element, all you gotta do is run some power through it. Power is made up of voltage and current, so I looked at the specs for the heating element I just bought. 12 volts. Okay, 20 watts. Okay, so I need to run about 1.7 amps through this. At 12 volts. And I know that I want to be able to power this off of USB-C because I can get battery banks that have USB-C out. So, I'll use a USB-C power delivery trigger module as my power source. And I'll run that through the heating element. Okay, simple enough, except if I want 100 watts, which I do because I'm going to have four of these heating elements. Then I need 20 volts power delivery module because that's just how the standard works. But I know that I can convert 20 volts to 12 volts using a buck converter. In retrospect, a different, a regulated component to do look up what the name of that is.
-The buck converter is going to give me pretty good efficiency though, so the drop in voltage is going to result in an increase in current that I can use. I'm going to wire all of the heating elements in parallel, because for my understanding of parallel circuits, the current is equally distributed across all the elements that are in parallel. So if I drive 80 watts, I should be driving 20 watts through each of the heating elements, which is desirable. And then in addition to the difference in voltage, I'll throw in a thermometer or a thermal regulator so that I can configure the temperature. I'm probably going to want to be running this at the maximum temperature, but you never know, and it gives me some data on whether or not the device is actually working and getting my food hot. Thanks for watching. Thank you.
+This post will probably make you cringe if you're moderately experienced in circuit design or 3D printing.
+* The Simplest Circuit
-The Sabbath heat is something I bought a couple of years ago and used when I went to a conference, but otherwise I haven't really used until recently. It's pretty good, but it requires an outlet, which is a problem, because it takes about an hour and a half to warm up a reasonably sized meal to the temperature that I like. I am probably moving around quite a bit to the point that I can't sit by an outlet for an hour and a half. So a solution that I've tried is getting these relatively large batteries that have an inverter built in, which basically gives me a portable wall outlet. The problem is these batteries are heavy and massive. So carrying them around during travel is just shitty.
-I would much rather have that space in my backpack available for something like a steam deck or maybe my Oculus Quest.
+The extent of my electronics experience is high school physics, but I basically know what I want. I need a heating element, and some way of driving current through it. For my first prototype, I used four resistive heating elements (12V, 20W) in parallel because my understanding of parallel circuits is that the current is equally distributed across all the elements. I.e., if my input is about 6.7A, I'll be driving about 1.7A through each of the heating elements, equivalent to 20W, maxing them out. For inducing a current through them, I used a [[https://en.wikipedia.org/wiki/USB_hardware#USB_Power_Delivery][USB-C Power Delivery (USB-PD)]] trigger module.
-Of all the solutions I've outlined, this is the one that's worked best, but that issue about the space and the weight is one that I wanted to address. So I decided to build, essentially, the same thing, except instead of being powered by 120 volt AC outlet to do, look up the NEMA code for that, I'll build one that's powered by USB-C. Why USB-C, because the standard has USB-C power delivery, which allows me to drive extremely high-power loads. If you've used an electric stove, an induction cooktop, an electric dryer, or anything of the like, you know that it takes a lot of power to warm something up, so we need high power. In the case of USB-C power delivery, we can actually drive hundreds of watts through our load. Ultimately, it comes down to the power source on what wattage can support. I have chargers that support 100 watt output, and I was able to find a couple of batteries that support 100 watt output as well. I bought three of them combined, weighs less, and is less compact than any of the single batteries with the built-in inverter. The other benefit is that their capacity is much less specifically less than the travel safety administration's arbitrary limit of 100 lot hours, which means I should have no issues taking this past airport security. I also don't necessarily have to run it off of a battery because USB-C can be plugged into a wall with a wall wart. As I mentioned I have chargers that output 100 watts so if I do have an outlet available I can just plug it into the wall. I always have my 100 watt wall warts with me. I don't know what I'm going to do. I don't know what I'm going to do.p to do, mention that the cable has to support 101 output as well.
+#+CAPTION: The USB-PD trigger module I used for this project.
+[[./usb-pd-is-pretty-cool-module.jpg]]
-#+CAPTION: The diagram is almost too simple to warrant drawing out.
-[[./food-warmer-circuit.svg]]
-
-* Solution 0: Use a Microwave at a Wawa or 7/11
-
-You basically have no idea if a convenience store is actually going to have one that works and lets you use it to warm things besides food you buy from the convenience store.
-
-I had the idea for a crowdsourced map of microwave ovens along popular travel corridors but I'm sure I'm the only person who would use it.
-
-Doesn't work for rail or air travel.
-
-* Solution 1: Bring a Microwave With You
-
-This is an absurd solution, but something I've done in the past, notably when I drove home during Thanksgiving and Christmas of 2024. The problem is that you have to find an outlet, which are ubiquitous at rest areas but nowhere else. And along the I-95 corridor, there are some states where rest areas have plentiful CCS charging stations, and some where they do not. Notably New Jersey.[fn:1]
-
-One thing you can do is get a battery and use that to power the microwave. Great, an extra thing that's incredibly heavy. I actually haven't tried this, I just went onto the next solution
+If you aren't familiar with USB-PD, it's a standard for delivering >7.5W over USB. It's why modern laptops and phones are able to charge so quickly over a USB-C connection. Both devices (charger and device being charged) need to support it, but if they do, they can negotiate a higher voltage/current over the data line based on what each support and what the cable is capable of carrying. PD 1.0 allows the following voltage/current pairs. (From [[https://en.wikipedia.org/wiki/USB_hardware#USB_Power_Delivery][Wikipedia]], the free encyclopedia.)
-Doesn't work for rail or air travel.
-
-* Solution 3: Dehydrate Food, Rehydrate on Hotplate
-
-Severely limits what you can bring with you. Probably the strangest looking of all the solutions.
+#+BEGIN_EXPORT html
+<table style="text-align:right">
+ <caption>USB PD Rev. 1.0 source profiles</caption>
+ <tbody>
+ <tr>
+ <th>Profile</th>
+ <th>+5 V</th>
+ <th>+12 V</th>
+ <th>+20 V</th>
+ </tr>
+ <tr>
+ <th>0</th>
+ <td colspan="3" data-sort-value="" style="background: var(--background-color-interactive, #ececec); color: var(--color-base, inherit); vertical-align: middle; text-align: center;" class="table-na">Reserved</td>
+ </tr>
+ <tr>
+ <th>1</th>
+ <td rowspan="5" valign="top">3.0 A, 15 W</td>
+ <td data-sort-value="" style="background: var(--background-color-interactive, #ececec); color: var(--color-base, inherit); vertical-align: middle; text-align: center;" class="table-na">—</td>
+ <td rowspan="3" data-sort-value="" style="background: var(--background-color-interactive, #ececec); color: var(--color-base, inherit); vertical-align: middle; text-align: center;" class="table-na">—</td>
+ </tr>
+ <tr>
+ <th>2</th>
+ <td>1.5 A, 18 W</td>
+ </tr>
+ <tr>
+ <th>3</th>
+ <td rowspan="2" valign="top">3.0 A, 36 W</td>
+ </tr>
+ <tr>
+ <th>4</th>
+ <td>3.0 A, 60 W</td>
+ </tr>
+ <tr>
+ <th>5</th>
+ <td>5.0 A, 60 W</td>
+ <td>5.0 A, 100 W</td>
+ </tr>
+ </tbody>
+</table>
+#+END_EXPORT
-* Solution 2: Bring a Heating Element
+What the trigger module does is handle the negotiation and give you, effectively, a constant voltage source. Connect it to a load, and you induce a current through your circuit. Perfect. Warming something up takes a lot of energy, so we'll opt for 20V, being the only voltage in the standard capable of carrying 100W. The problem is that the resistive heating elements are 12V. Fortunately we can use a [[https://en.wikipedia.org/wiki/Buck_converter][buck converter]] to efficiently convert the voltage. In retrospect, a [[https://en.wikipedia.org/wiki/Linear_regulator][linear regulator]] would have been fine because we would could have recycled the waste heat for warming the food, but the benefit of the buck converter is that you can just buy one and it's extremely simple to use. I was able to treat it as a black box that takes in 20V and puts out 12V.
-SabotHeat
+The reason I think these trigger modules are so great for the average tinkerer is that they're an incredibly inexpensive (< $0.50) and compact power source for just about any project you might want to make[fn:1], and USB-C is incredibly ubiquitous these days. You're probably reading this on a USB-C power source.
-Even if I'm trying to build a different solution, this was money well spent since the battery gets plenty of use otherwise for solar experiments
+The heating element and power source are all we really need, but I also threw in a temperature controller module for fun. It definitely wasn't necessary; due to the relatively low current draw, I wasn't expecting to have any desire to cap the temperature for the resistive heating elements, but you never know. Plus, it has a nice display to tell me whether or not the device is actually working and making my food hot. It's another simple black box, effectively a switch (passing through the current) that's controlled by a temperature probe.
-Works fairly well for rail travel. On Amtrak, the outlet next to the seat is powerful enough to for the SabotHeat. Presumably works for air travel because the batteries are within the range of "two spare larger lithium ion batteries acceptable with airline approval", but I haven't tried that yet.
+#+CAPTION: Temperature controller module, left, and buck converter, right.
+[[./usb-pd-is-pretty-cool-buck-converter-temperature-module.jpg]]
-The batteries are extremely bulky and heavy, though. So let's improve.
+With all that, here's the circuit.
-* New Solution: USB-C Heating Element
+#+CAPTION: The diagram is almost too simple to warrant drawing out.
+[[./usb-pd-is-pretty-cool-food-warmer-circuit.svg]]
-Can be powered on typical battery banks (saves [TODO] lbs, [TODO] cu in), more efficient since there's no intermediate conversion to AC necessary, can theoretically get hotter temperatures.
+One tool I purchased specifically for this project is a USB-C ammeter. For about $10, I get a real-time display on the voltage, current, and power draw passing through it. Very useful for this project, particularly when I accidentally plugged one of the prototypes into a port that only supported 12V. Much easier than holding multimeter test leads onto relatively small solder pads.
----
+#+CAPTION: USB-C ammeter charging a Flipper Zero.
+[[./usb-pd-is-pretty-cool-ammeter.jpg]]
-This should work in theory, but as the title suggests, I have no experience, so let's check myself.
+* First Prototype
-* Experiments
+I had an (irrational) fear of the heating elements getting so hot that they would melt or burn the containing bag, so I wanted to put something down as an insulator. I had a silicone baking sheet that I received as a "free gift" when I ordered some artificial flavorings a couple months ago. It's been sitting on a shelf ever since, so it will probably get more use being repurposed for this project than its intended purpose. If it's designed to withstand oven heats, it should do fine in direct contact with some heating elements. So I folded it over, put it in the lunch bag that came with the SabotHeat, and wired everything up.
-Started with one thermistor.
+#+CAPTION: First prototype warming a portion of turkey chili.
+[[./usb-pd-is-pretty-cool-chili.jpg]]
-Thermistors aren't polarized so I can connect them in any direction. Connect them in parallel
+The dish here is stainless steel. One thing I'd learned with the stock SabotHeat element is that you have to consider the thermal conductivity (watt per meter-kelvin) of your dish. Effectively: how good your dish is at conducting heat. I started with Pyrex, which has a okay thermal conductivity, but stainless is much better. Another consideration is the thermal conductivity of your food. Chili (especially when it's a bit thin) is a great test bed because it's liquid, and heat generally transfers through that well. A dish that's made up of a couple solid components, like a cup of rice, some refried beans, some vegetables, and some chicken won't warm up as well if you just put the dish on the heating element. That's relatively easy to work around by adding a little bit of water to the bottom of the dish, because the water turns into steam, which is a good medium for transferring heat to food (as evidenced by steaming being a common method for cooking vegetables). Anyway, that's a massive aside to say that chili in a stainless dish was a good representation of the "best case scenario".
-#+BEGIN_EXPORT html
-<table class="wikitable floatright" style="text-align:right">
- <caption>USB PD Rev. 1.0 source profiles<sup id="cite_ref-PD1.0_91-0" class="reference"><a href="#cite_note-PD1.0-91"><span class="cite-bracket">[</span>69<span class="cite-bracket">]</span></a></sup>
- </caption>
- <tbody><tr>
- <th>Profile
- </th>
- <th>+5&nbsp;V
- </th>
- <th>+12&nbsp;V
- </th>
- <th>+20&nbsp;V
- </th></tr>
- <tr>
- <th>0
- </th>
- <td colspan="3" data-sort-value="" style="background: var(--background-color-interactive, #ececec); color: var(--color-base, inherit); vertical-align: middle; text-align: center;" class="table-na">Reserved
- </td></tr>
- <tr>
- <th>1
- </th>
- <td rowspan="5" valign="top">3.0&nbsp;A, 15&nbsp;W<sup id="cite_ref-default-profile_92-0" class="reference"><a href="#cite_note-default-profile-92"><span class="cite-bracket">[</span>a<span class="cite-bracket">]</span></a></sup>
- </td>
- <td data-sort-value="" style="background: var(--background-color-interactive, #ececec); color: var(--color-base, inherit); vertical-align: middle; text-align: center;" class="table-na">—
- </td>
- <td rowspan="3" data-sort-value="" style="background: var(--background-color-interactive, #ececec); color: var(--color-base, inherit); vertical-align: middle; text-align: center;" class="table-na">—
- </td></tr>
- <tr>
- <th>2
- </th>
- <td>1.5&nbsp;A, 18&nbsp;W
- </td></tr>
- <tr>
- <th>3
- </th>
- <td rowspan="2" valign="top">3.0&nbsp;A, 36&nbsp;W
- </td></tr>
- <tr>
- <th>4
- </th>
- <td>3.0&nbsp;A, 60&nbsp;W
- </td></tr>
- <tr>
- <th>5
- </th>
- <td>5.0&nbsp;A, 60&nbsp;W
- </td>
- <td>5.0&nbsp;A, 100&nbsp;W
- </td></tr>
- <tr>
- <td colspan="4"><link rel="mw-deduplicated-inline-style" href="mw-data:TemplateStyles:r1239543626"><div class="reflist reflist-lower-alpha">
- <div class="mw-references-wrap"><ol class="references">
- <li id="cite_note-default-profile-92"><span class="mw-cite-backlink"><b><a href="#cite_ref-default-profile_92-0" aria-label="Jump up" title="Jump up">^</a></b></span> <span class="reference-text">Default start-up profile</span>
- </li>
- </ol></div></div>
-</td></tr></tbody></table>
-#+END_EXPORT
+And it passed with flying colors. There was steam coming off of it when I pulled it. I threw some fries and cheese on top and it made for a fulfilling supper. That said, the device ran for nearly 2 hours at 80W, and the temperature rose very slowly. It started at 18°C and ended at about 70°C. So I started wondering if I could do better.
-** First Challenge: Why Am I Getting 12 Volts?
+* Second Prototype
-On my desk is a 175 watt power supply with a handful of USB ports, a few of which are USB-C PD. Three are labeled 100W, so I grabbed one and plugged the ammeter and trigger module into the unoccupied one. The ammeter read 12 volts.
+The resistive heating elements worked good enough, but I did have some positive-temperature-coefficient (PTC) heating elements handy of various voltages and temperatures, and figured it would be worth experimenting. I had two that were built into aluminum plates, so I figured I would give it a shot with those instead. They were /significantly/ better than the resistive heating elements. The surface temperature of the heating elements rose from 25°C to 70°C in 15 minutes.
-Well, I suppose that gives me a convenient way to experiment without the buck converter, since the heating elements are rated for 12 volts anyway. I connected one heating element across the terminals, which drew a touch over 20W and the heating element got warm. I connected two in parallel over the terminals and... WTF? The voltage drops to [TODO] volts and the current increases.
+Of course, I don't want to burn my food, and if I rely on the temperature controller module alone it's going to be constantly cycling on and off. Fortunately, I had a LED dimmer module handy that I could use to drop the voltage across the heating elements. Yet another black box which, in a massively oversimplified mental model of the circuit (that's probably incorrect) acts as a variable [[https://en.wikipedia.org/wiki/Voltage_divider][voltage divider]], but in practice probably does more under the hood than a plain old potentiometer.
-Through some experimentation, I came to learn that the reason I was getting 12V across the terminals is because my power supply is mis-labeled. Three of the ports are labeled "100W", but in reality, only two of them support that much power. The one I was plugging the trigger module into only supported 20V. Alternative explanation: two of them support 20V output and the trigger module only supports 100W when the voltage across the terminals is 20V. I don't have a datasheet so I'm flying blind, but when I do plug the module into the other 100W ports I do get 20V and I am able to connect all four elements in parallel.
+#+CAPTION: PTC heating elements, left, and LED dimmer module, right.
+[[./usb-pd-is-pretty-cool-dimmer-ptc.jpg]]
-After learning this, I checked the battery banks and... yes! I do get 20V when I plug into the more powerful USB-C port, so we should be good to go for warming food.
+A few days ago I had a medical evaluation for work early in the morning in Washington, plans to spend the rest of the day with some friends downtown, and one serving of chili left. A perfect opportunity to test out the device with the PTC elements. It worked great until the one battery I brought with me died about 40 minutes in.
-** Whoops, Over-Volted the
+Did I forget to charge it? My batteries are labeled 20,000 mAh, so my thinking was that if I'm drawing 4 amps for the heating elements, I should to be able to run the device for 5 hours. After doing a little research, though, I learned that the mAh rating for these sorts of power banks is done at the nominal voltage of a single lithium ion cell (3.7v), not at the output voltage. The watt-hour (Wh) is a more useful measurement, which for this battery, is 74 Wh. Operating at 20V, (74 wH * 1000) / 20V gives us 3,700 mAh, meaning I can run 4 amps for a touch under an hour. So I'll need a second battery in practice, which is inconvenient, but two is just a touch over 1.5 lbs and it's easy to find space for them in my backpack, so it's not terrible.
-My amnesiac ass forgot that the temperature controller was rated for 12V and put it before the buck converter. Whoops.
+* You're Really Going to Travel With That?
-* Making Sure I Don't Melt The Bag
+If the American [[https://en.wikipedia.org/wiki/Transportation_Security_Administration][Transportation Security Administration]] (TSA) is willing to peculate my lawfully-owned lock picks under the guise that sharp things, however small, could be dangerous, they'll almost certainly blow a gasket seeing me try to travel with a jumble of wires and a LCD display reminiscent of al-Qaeda's [[https://en.wikipedia.org/wiki/Casio_F-91W#Usage_in_terrorism][watch of choice]]. So let's build a nice enclosure for all of the electronic components. I'll gladly take the excuse to break out the 3D printer I haven't used in ages.
-I had a (probably irrational fear) of the heating elements getting so hot that they melt or burn the containing bag, so I wanted to put something down to insulate the bag and prevent that, akin to how the SabotHeat element has one hot surface and the rest is plastic.
+I'm a CAD neophyte, but I just needed a box to put the components in, which wasn't too hard to put together in [[https://en.wikipedia.org/wiki/FreeCAD][FreeCAD]]. Measurements for my first attempt were way off, so for the second version of the design I drew the model out on paper and placed the components on it to make sure they would fit, which worked well. I had some M3 screws handy so I added holes to join the top with the rest of the box.
-I had a baking sheet I received as a free gift when I ordered some artificial flavorings from a vendor a couple months ago. I hadn't used it, and I usually don't use any sort of parchment paper on my baking sheet, so I think it would get more use being the insulator for this application. If it's designed to withstand oven heats, I'm imagining it should do fine in direct contact with some regulated heating elements.
+#+CAPTION: An extremely simple CAD design of a box.
+[[./usb-pd-is-pretty-cool-food-warmer-cad.jpg]]
-* You're Really Going to Travel With That?
+* End Product
-If the American [[https://en.wikipedia.org/wiki/Transportation_Security_Administration][Transportation Security Administration]] is willing to peculate my lawfully-owned lock picks under the guise that sharp things, however small, could be dangerous, they would almost certainly blow a gasket seeing me try to travel with a jumble of wires and a LCD display reminiscent of al-Qaeda's [[https://en.wikipedia.org/wiki/Casio_F-91W#Usage_in_terrorism][watch of choice]]. So let's build a nice enclosure for all of the electronic components.
+The Ender 3 that's been sitting in storage for over a year now managed to build it just fine, at least after some maintenance to replace one of the pneumatic couplers after it catastrophically failed. I should probably replace the springs, too, since the bed isn't staying level, but that's a project for another day.
-I'll gladly take the excuse to break out the 3D printer I haven't used in ages.
+#+CAPTION: A crude but otherwise unremarkable white cube with three things of note protruding from it: a USB-C cable, the display for the temperature control module, and the LED dimmer moduler.
+[[./usb-pd-is-pretty-cool-final-product.jpg]]
-Unfortunately the FreeCAD package in Gentoo is broken so I had to use the AppImage.
+Great, that looks even more like a bomb than my bundle of wires. At least it's easier to manage, so I can keep it out of sight.
-* Conclusions
+** Bill of Materials
-What's that? "Productize?" Probably not. First, my job keeps me far too busy to meaningfully engage in any sort of side-hustle. Second, I'm certain this isn't the most sound solution. As I stated in the introduction, I have a very limited background when it comes to electronics. If I were going to turn this into a product to sell, I would probably recruit my electrical engineer brother to look over the design and manufacturing process.
+I bought some of these components before increased tariffs, so this would probably be a few dollars more expensive if you bought everything today.
-For that reason, consider the design open hardware. This post is CC-BY-SA 4.0. The design is certainly too simple to protect under copyright or any sort of open hardware license, so if you want to turn this into a product, go wild. Just let me know if you do, so I can lose sleep at night over my decision not to commercialize my silly electronics project.
+| 2x PTC Heater Element | $8.54 |
+| 30A DC 12V-24V LED Brightness Regulator | $7.41 |
+| 10A 120W Step Down to DC 12V Buck Converter | $14.99 |
+| PDC004-PD 20V PD Decoy Module | $0.90 |
+| Temperature Controller Module | $4.99 |
+|---------------------------------------------+--------|
+| Total | $36.83 |
-That said, if you want one but lack the skill to construct it yourself, send me an email and I would be happy to build one for the cost of materials and labor.
+I was able to obtain a few 100W batteries for $35 a piece. If you count those, with the cost of the dish, the heat shrink tubing, the solder, etc. the effective price probably comes to much more than $37. But I had fun, I suppose, which I think is the part that matters.
---
-[fn:1] At the time, I didn't have Ford's NACS adapter. These days, I have one but avoid the Tesla Supercharger network on personal principle.
+[fn:1] Random anecdote in support of my argument: I replaced the DC barrel jack in a Baofeng charger with one for soldering practice, and now it's easier to bring it with me when I'm traveling since I no longer need an additional wall-wart. I just needed to connect to two solder pads and I had an appropriate voltage source for the charging circuit.
diff --git a/static/css/style.css b/static/css/style.css
index 55b9080..9fd302b 100644
--- a/static/css/style.css
+++ b/static/css/style.css
@@ -58,10 +58,13 @@ body > header > a > img {
body > header > nav ul {
list-style-type: none;
- padding-left: 0;
}
@media screen and (min-width: 480px) {
+ body > header > nav ul {
+ padding-left: 0;
+ }
+
body > header > nav > ul > li {
display: inline-block;
}
@@ -93,6 +96,10 @@ body > header > nav ul {
}
@media screen and (max-width: 480px) {
+ body > header > nav ul {
+ padding-left: 2em;
+ }
+
body > header > nav > ul .drop-menu {
list-style-type: circle;
padding-left: 1em;
@@ -616,12 +623,18 @@ span.cookbook-tag-difficulty-easy { background-color: green; color:
span.cookbook-tag-difficulty-easy:before { content: "Difficulty: Easy"; }
span.cookbook-tag-difficulty-medium { background-color: yellow; }
span.cookbook-tag-difficulty-medium:before { content: "Difficulty: Medium"; }
+span.cookbook-tag-difficulty-hard { background-color: maroon; color: white; }
+span.cookbook-tag-difficulty-hard:before { content: "Difficulty: Hard"; }
span.cookbook-tag-cuisine-american:before { content: "Cuisine: 🇺🇸 American"; }
span.cookbook-tag-cuisine-texmex:before { content: "Cuisine: 🤠 Tex-Mex"; }
+span.cookbook-tag-cuisine-ghanaian:before { content: "Cuisine: 🇬🇭 Ghanaian"; }
span.cookbook-tag-flavor-profile-savory:before { content: "Flavor Profile: 🧂 Savory"; }
+span.cookbook-tag-flavor-profile-spicy:before { content: "Flavor Profile: 🌶️ Spicy"; }
span.cookbook-tag-dish-type-sauce:before { content: "Type: 🥫 Sauce"; }
span.cookbook-tag-dish-type-soup:before { content: "Type: 🥣 Soup"; }
-span.cookbook-tag-dish-type-meat:before { content: "Type: 🍖 Meat"; }
+span.cookbook-tag-dish-type-stew:before { content: "Type: 🍲 Stew"; }
+span.cookbook-tag-dish-type-starch:before { content: "Type: 🍚 Starch"; }
+span.cookbook-tag-dish-type-protein:before { content: "Type: 🍖 Protein"; }
span.cookbook-tag-dish-type-main:before { content: "Type: 🍽️ Main Course"; }
span.cookbook-tag-dish-type-side:before { content: "Type: 🍴 Side Dish"; }
span.cookbook-tag-dish-type-baked-good:before { content: "Type: 🍞 Baked Good"; }