Compare commits
72 Commits
e15ab3e1a3
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 2126960812 | |||
| 5613bdb56e | |||
| 5db575251e | |||
| 6a7828cb9d | |||
| 39f0373ef9 | |||
| 7945024332 | |||
| 3dd1ae9b84 | |||
| 3de7867d98 | |||
| 2a039dfcf9 | |||
| 14688248c2 | |||
| f6ed777b67 | |||
| 9261fa0911 | |||
| c8eb654214 | |||
| c6ee4beb11 | |||
| 1f85f22e7a | |||
| 602932bc00 | |||
| 3c5dc3e0c4 | |||
| b8ffeba4bd | |||
| b0232676ce | |||
| 75a90ed019 | |||
| b9ffaa7357 | |||
| 369521bda2 | |||
| 5d31cba56f | |||
| 644dc626a4 | |||
| 2c770a4b17 | |||
| d5fff56284 | |||
| 21707d4cf8 | |||
| 2977b1b916 | |||
| 2ac68f5a6a | |||
| cb8b17f5f4 | |||
| 52983eb698 | |||
| 87fd31bcda | |||
| d3d8bbf84c | |||
| 25c848abfa | |||
| 7552f260aa | |||
| 0978e0479d | |||
| 5331fcef6c | |||
| 3093ab9cfa | |||
| 257ea3d1a0 | |||
| 5c23a13501 | |||
| 85cc2e3dbf | |||
| 665811bb57 | |||
| 005a371dcb | |||
| 118c471d00 | |||
| 6666320cbd | |||
| 9c8965625b | |||
| 283f31aa89 | |||
| ea2c9519cd | |||
| 219c44054d | |||
| 6757e4178a | |||
| 1c083132a2 | |||
| 86f3a08bbc | |||
| 02f2346c93 | |||
| b9d6083fca | |||
| 59cd6d8358 | |||
| 870ab7b6fc | |||
| 58970bc8ef | |||
| 5f866fbeb0 | |||
| babe21ed14 | |||
| 612930411a | |||
| ab776d8662 | |||
| d653bc948d | |||
| a13bdc1e08 | |||
| 60ffbf9b5d | |||
| 4b5d909db7 | |||
| 0737b5ec9e | |||
| 710f6cc8b0 | |||
| 16c0dac56c | |||
| 7f0e569139 | |||
| 729c49bad5 | |||
| e2d5ca3762 | |||
| f4f781e1ea |
@@ -112,3 +112,6 @@ test/
|
|||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
output/
|
output/
|
||||||
|
.aider*
|
||||||
|
|
||||||
|
content/.obsidian/workspace.json
|
||||||
|
|||||||
@@ -1,17 +1,34 @@
|
|||||||
[
|
{
|
||||||
"file-explorer",
|
"file-explorer": true,
|
||||||
"global-search",
|
"global-search": true,
|
||||||
"switcher",
|
"switcher": true,
|
||||||
"graph",
|
"graph": true,
|
||||||
"backlink",
|
"backlink": true,
|
||||||
"canvas",
|
"outgoing-link": false,
|
||||||
"page-preview",
|
"tag-pane": false,
|
||||||
"note-composer",
|
"page-preview": true,
|
||||||
"command-palette",
|
"daily-notes": false,
|
||||||
"editor-status",
|
"templates": true,
|
||||||
"bookmarks",
|
"note-composer": true,
|
||||||
"markdown-importer",
|
"command-palette": true,
|
||||||
"outline",
|
"slash-command": false,
|
||||||
"word-count",
|
"editor-status": true,
|
||||||
"file-recovery"
|
"starred": false,
|
||||||
]
|
"markdown-importer": true,
|
||||||
|
"zk-prefixer": false,
|
||||||
|
"random-note": false,
|
||||||
|
"outline": true,
|
||||||
|
"word-count": true,
|
||||||
|
"slides": false,
|
||||||
|
"audio-recorder": false,
|
||||||
|
"workspaces": false,
|
||||||
|
"file-recovery": true,
|
||||||
|
"publish": false,
|
||||||
|
"sync": false,
|
||||||
|
"canvas": true,
|
||||||
|
"bookmarks": true,
|
||||||
|
"properties": false,
|
||||||
|
"webviewer": false,
|
||||||
|
"footnotes": false,
|
||||||
|
"bases": true
|
||||||
|
}
|
||||||
@@ -1,226 +0,0 @@
|
|||||||
{
|
|
||||||
"main": {
|
|
||||||
"id": "69e9da393623ab60",
|
|
||||||
"type": "split",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"id": "59ed96d9876185c9",
|
|
||||||
"type": "tabs",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"id": "160122bd13ae4b72",
|
|
||||||
"type": "leaf",
|
|
||||||
"state": {
|
|
||||||
"type": "markdown",
|
|
||||||
"state": {
|
|
||||||
"file": "Secret Garden.md",
|
|
||||||
"mode": "source",
|
|
||||||
"source": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "238ba022d07a7436",
|
|
||||||
"type": "leaf",
|
|
||||||
"state": {
|
|
||||||
"type": "markdown",
|
|
||||||
"state": {
|
|
||||||
"file": "Fake Dog.md",
|
|
||||||
"mode": "source",
|
|
||||||
"source": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "d9a16803d250ddc4",
|
|
||||||
"type": "leaf",
|
|
||||||
"state": {
|
|
||||||
"type": "markdown",
|
|
||||||
"state": {
|
|
||||||
"file": "Secret Garden.md",
|
|
||||||
"mode": "source",
|
|
||||||
"source": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "24a62ccdfd18a884",
|
|
||||||
"type": "leaf",
|
|
||||||
"state": {
|
|
||||||
"type": "markdown",
|
|
||||||
"state": {
|
|
||||||
"file": "Makerspace Tours.md",
|
|
||||||
"mode": "source",
|
|
||||||
"source": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "1f1f024283ea8110",
|
|
||||||
"type": "leaf",
|
|
||||||
"state": {
|
|
||||||
"type": "markdown",
|
|
||||||
"state": {
|
|
||||||
"file": "Helios Alpha.md",
|
|
||||||
"mode": "source",
|
|
||||||
"source": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "60389a84493f7fa2",
|
|
||||||
"type": "leaf",
|
|
||||||
"state": {
|
|
||||||
"type": "markdown",
|
|
||||||
"state": {
|
|
||||||
"file": "Hydroponics.md",
|
|
||||||
"mode": "source",
|
|
||||||
"source": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"currentTab": 3
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"direction": "vertical"
|
|
||||||
},
|
|
||||||
"left": {
|
|
||||||
"id": "3885f82c1ab72e1b",
|
|
||||||
"type": "split",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"id": "0e37795504669957",
|
|
||||||
"type": "tabs",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"id": "e5f5df16367f5f9a",
|
|
||||||
"type": "leaf",
|
|
||||||
"state": {
|
|
||||||
"type": "file-explorer",
|
|
||||||
"state": {
|
|
||||||
"sortOrder": "alphabetical"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "15b64333baa0fbc2",
|
|
||||||
"type": "leaf",
|
|
||||||
"state": {
|
|
||||||
"type": "search",
|
|
||||||
"state": {
|
|
||||||
"query": "",
|
|
||||||
"matchingCase": false,
|
|
||||||
"explainSearch": false,
|
|
||||||
"collapseAll": false,
|
|
||||||
"extraContext": false,
|
|
||||||
"sortOrder": "alphabetical"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "7bdb31d1bda5b8c9",
|
|
||||||
"type": "leaf",
|
|
||||||
"state": {
|
|
||||||
"type": "bookmarks",
|
|
||||||
"state": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"direction": "horizontal",
|
|
||||||
"width": 200
|
|
||||||
},
|
|
||||||
"right": {
|
|
||||||
"id": "260bba8f76f307a9",
|
|
||||||
"type": "split",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"id": "21c556d6660f839b",
|
|
||||||
"type": "tabs",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"id": "528c8f9657044ea2",
|
|
||||||
"type": "leaf",
|
|
||||||
"state": {
|
|
||||||
"type": "backlink",
|
|
||||||
"state": {
|
|
||||||
"file": "Makerspace Tours.md",
|
|
||||||
"collapseAll": false,
|
|
||||||
"extraContext": false,
|
|
||||||
"sortOrder": "alphabetical",
|
|
||||||
"showSearch": false,
|
|
||||||
"searchQuery": "",
|
|
||||||
"backlinkCollapsed": false,
|
|
||||||
"unlinkedCollapsed": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "79f3ff4100fe3ae6",
|
|
||||||
"type": "leaf",
|
|
||||||
"state": {
|
|
||||||
"type": "outline",
|
|
||||||
"state": {
|
|
||||||
"file": "Makerspace Tours.md"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"currentTab": 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"direction": "horizontal",
|
|
||||||
"width": 300,
|
|
||||||
"collapsed": true
|
|
||||||
},
|
|
||||||
"left-ribbon": {
|
|
||||||
"hiddenItems": {
|
|
||||||
"switcher:Open quick switcher": false,
|
|
||||||
"graph:Open graph view": false,
|
|
||||||
"canvas:Create new canvas": false,
|
|
||||||
"command-palette:Open command palette": false,
|
|
||||||
"markdown-importer:Open format converter": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"active": "24a62ccdfd18a884",
|
|
||||||
"lastOpenFiles": [
|
|
||||||
"Secret Garden.md",
|
|
||||||
"Hydroponics.md",
|
|
||||||
"Makerspace Tours.md",
|
|
||||||
"Linux Flavour.md",
|
|
||||||
"Fake Dog.md",
|
|
||||||
"Wine Crate Coffee Table.md",
|
|
||||||
"Things I Recommend.md",
|
|
||||||
"t0txt.md",
|
|
||||||
"t0 Services.md",
|
|
||||||
"Spaceport.md",
|
|
||||||
"Solar Car.md",
|
|
||||||
"Sensors.md",
|
|
||||||
"Helios Alpha.md",
|
|
||||||
"QotNews.md",
|
|
||||||
"Protospace.md",
|
|
||||||
"Plant Waterer.md",
|
|
||||||
"Painting.md",
|
|
||||||
"Notica.md",
|
|
||||||
"Light Switch.md",
|
|
||||||
"LED Dress.md",
|
|
||||||
"Japan Photography.md",
|
|
||||||
"Hydroponics Log 3.md",
|
|
||||||
"Hydroponics Log 2.md",
|
|
||||||
"Hydroponics Log 1.md",
|
|
||||||
"Hydroponics Aphid War.md",
|
|
||||||
"Hand of Ozymandias.md",
|
|
||||||
"media/nft2.jpg",
|
|
||||||
"media/nft1.png",
|
|
||||||
"media/ports1.svg",
|
|
||||||
"media/backup1.svg",
|
|
||||||
"media/japan12hi.jpg",
|
|
||||||
"media/japan11hi.jpg",
|
|
||||||
"media/japan10hi.jpg",
|
|
||||||
"media/japan09hi.jpg",
|
|
||||||
"media/japan08hi.jpg",
|
|
||||||
"media/japan07hi.jpg"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,7 @@ Category: Creations
|
|||||||
Summary: Panels for acoustic treatment in my home theatre.
|
Summary: Panels for acoustic treatment in my home theatre.
|
||||||
Image: panel3.jpg
|
Image: panel3.jpg
|
||||||
Tags: feed
|
Tags: feed
|
||||||
|
Guid: 763087bc038b49199d305f031cfaa6c3
|
||||||
|
|
||||||
Acoustic treatment is one of the most overlooked aspects of home audio. There's no point in spending money on premium speakers if the room they are playing in has poor acoustics.
|
Acoustic treatment is one of the most overlooked aspects of home audio. There's no point in spending money on premium speakers if the room they are playing in has poor acoustics.
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ Summary: Details about the backup system for all of my data.
|
|||||||
Image: backup1.svg
|
Image: backup1.svg
|
||||||
Wide: true
|
Wide: true
|
||||||
Tags: feed
|
Tags: feed
|
||||||
|
Guid: c0afe12a1c4943839df1da082c2e0938
|
||||||
|
|
||||||
[TOC]
|
[TOC]
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
Title: Bash Register
|
||||||
|
Date: 2026-01-25
|
||||||
|
Category: Creations
|
||||||
|
Summary: An old cash register with a thermal receipt printer.
|
||||||
|
Image: bash-register1.jpg
|
||||||
|
Tags: feed
|
||||||
|
Guid: 6836532b386642b9879f71f08e5d821f
|
||||||
|
|
||||||
|
The Bash Register is an old cash register that my friend and I stuck a Raspberry Pi Linux computer into. It's connected to a thermal receipt printer that prints images sent from the member portal [[Spaceport]] running at my local makerspace, [[Protospace]].
|
||||||
|
|
||||||
|
![[bash-register1.jpg]]
|
||||||
|
|
||||||
|
Protospace members are able to draw images on the portal:
|
||||||
|
|
||||||
|
![[bash-register2.jpg]]
|
||||||
|
|
||||||
|
Draw controls include colour, eraser, size, shade, undo history, and reset. The current drawing is stored in local storage so it doesn't get lost if the user accidentally navigates away.
|
||||||
|
|
||||||
|
Once a drawing is submitted, its filename is sent to the Raspberry Pi via MQTT. A simple [Python script](https://git.tannercollin.com/tanner/bashregister/src/branch/master/main.py) listens for the message and then immediately downloads and prints it.
|
||||||
|
|
||||||
|
All drawings are added to a gallery that can be publicly viewed [here](https://my.protospace.ca/gallery):
|
||||||
|
|
||||||
|
![[bash-register3.png]]
|
||||||
|
|
||||||
|
## Printing Garbage Bug
|
||||||
|
|
||||||
|
While developing the code, I was having an issue where every second print would output several inches of garbage characters instead of the image. After hours of debugging it seemed related to the height of the image being printed. I used `git bisect` to narrow it down to a commit where the canvas's height was changed.
|
||||||
|
|
||||||
|
It turned out to be a bug with the printer that happens when printing images with a height that's a multiple of 96 pixels tall:
|
||||||
|
|
||||||
|
<https://github.com/python-escpos/python-escpos/issues/367>
|
||||||
|
|
||||||
|
The fix for this was simply adjusting the aspect ratio of the canvas on the portal.
|
||||||
@@ -5,6 +5,7 @@ Summary: Bypass ISP blocked ports using VPN port forwarding for public access.
|
|||||||
Image: ports1.svg
|
Image: ports1.svg
|
||||||
Wide: true
|
Wide: true
|
||||||
Tags: feed
|
Tags: feed
|
||||||
|
Guid: 1742dbf6802349c68eb232333f6a256c
|
||||||
|
|
||||||
[TOC]
|
[TOC]
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
Title: Camper Trailer
|
||||||
|
Date: 2026-04-15
|
||||||
|
Category: Creations
|
||||||
|
Summary: A custom square drop camper trailer.
|
||||||
|
Image: camper1.jpg
|
||||||
|
|
||||||
|
I built a "square drop" camper trailer out of plywood on an existing metal trailer frame at [[Protospace]]. It took eight work days to build over a three-week period in August 2025.
|
||||||
|
|
||||||
|
![[camper1.jpg]]
|
||||||
|
|
||||||
|
The camper is approximately 6' wide, 8' long, and 4' high above the trailer frame. The cabin is 6' x 6' and has an RV door (from Amazon), a roof vent, a small vent fan, and a phone charger. The back hatch galley is 2' deep and has some cabinets, a counter top, a sink that drains below to a bucket, and a slide-out tray for my cooler.
|
||||||
|
|
||||||
|
The walls are 5/8" plywood on a 2x2" stud frame that sits on a 3/4" plywood base. It was built on an existing flat deck trailer frame that my friend Robb gave me.
|
||||||
|
|
||||||
|
The vertices are fibreglassed <span class="aside"> (would not do fibreglass again)</span> and the exterior is painted with green porch paint that gives it a thick, rough texture. The hatch has weather stripping, the door has butyl, and the roof vent has lap sealant. The hatch also doubles as an awning that I can sit under while it rains.
|
||||||
|
|
||||||
|
The electronics are powered by a 40 Ah lithium iron phosphate battery that I manually charge before the trip. I plan to add a small solar panel next summer. The cabin and galley are lit by separate warm high-density LED strips on dimmers. They were positioned to be as diffuse as possible.
|
||||||
|
|
||||||
|
I took the trailer on a camping trip at the end of August and it ended up being too cold. It definitely needs insulation. My plan is to insulate the frame with XPS foam and cover it with veneer. If that's not enough I'll install a small diesel heater.
|
||||||
|
|
||||||
|
Here's the inside of the cabin:
|
||||||
|
|
||||||
|
![[camper2.jpg]]
|
||||||
|
|
||||||
|
Here's the galley, in use, at night:
|
||||||
|
|
||||||
|
![[camper3.jpg]]
|
||||||
@@ -4,6 +4,7 @@ Category: Writing
|
|||||||
Summary: A recipe generated by ChatGPT for peanut butter cookies.
|
Summary: A recipe generated by ChatGPT for peanut butter cookies.
|
||||||
Image: cookies3.jpg
|
Image: cookies3.jpg
|
||||||
Tags: feed
|
Tags: feed
|
||||||
|
Guid: 83fe535323d34830a2fcfe718779d7a6
|
||||||
|
|
||||||
<span class="aside">(describe a house fire in the style of a crappy blog post that people put above recipes that tell their life story before getting to the point about how their house burned down) </span>The flames danced around the living room, illuminating the memories that were now nothing more than ash. It was a warm summer evening, and we were all gathered in the backyard, enjoying the company of good friends and family. Little did we know that our lives were about to change forever.
|
<span class="aside">(describe a house fire in the style of a crappy blog post that people put above recipes that tell their life story before getting to the point about how their house burned down) </span>The flames danced around the living room, illuminating the memories that were now nothing more than ash. It was a warm summer evening, and we were all gathered in the backyard, enjoying the company of good friends and family. Little did we know that our lives were about to change forever.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
Title: Custom Air Quality Monitor
|
||||||
|
Date: 2025-01-13
|
||||||
|
Category: Creations
|
||||||
|
Summary: An air quality monitor made from sensor modules.
|
||||||
|
Image: airmonitor1.jpg
|
||||||
|
xTags: feed
|
||||||
|
|
||||||
|
I made custom air quality monitors to track the air in my kitchen, living room, and bedroom. They use sensor modules that detect PM2.5, PM10, CO<sub>2</sub>, VOCs, Light level, pressure, temperature and humidity. The data is sent to my [[Sensors]] database once per minute.
|
||||||
|
|
||||||
|
![[airmonitor1.jpg]]
|
||||||
|
|
||||||
|
The modules connect to an ESP8266 Arduino over I<sup>2</sup>C and UART serial. They are mounted on press-fit pegs to a 3D printed base plate I designed in FreeCAD.
|
||||||
|
|
||||||
|
The sensors used are:
|
||||||
|
|
||||||
|
- [HPMA115S0](https://www.digikey.ca/en/products/detail/honeywell-sensing-and-productivity-solutions/HPMA115S0-XXX/7202204) - Particulate Sensor
|
||||||
|
- [Adafruit SCD-30](https://www.adafruit.com/product/4867) - NDIR CO2 Temperature and Humidity Sensor
|
||||||
|
- [Adafruit BH1750](https://www.adafruit.com/product/4681) - Light Sensor
|
||||||
|
- [Adafruit SGP40](https://www.adafruit.com/product/4829) - VOC Sensor
|
||||||
|
- [Adafruit LPS22](https://www.adafruit.com/product/4633) - Pressure Sensor
|
||||||
|
|
||||||
|
The light data is used by my home automation system to know when it's night time so that motion sensors turn lights on. I also learned that the CO<sub>2</sub> level in my bedroom was doubling from 650 ppm to 1251 ppm overnight while I slept. This is bad since [high CO<sub>2</sub> concentration affects sleep](https://www.sciencedirect.com/science/article/pii/S0360132323011459), so now my automation system turns the furnace blower on while I sleep.
|
||||||
|
|
||||||
|
Before running the blower automatically (24 hour graph, CO<sub>2</sub> is the blue line):
|
||||||
|
|
||||||
|
![[airmonitor2.png]]
|
||||||
|
|
||||||
|
After:
|
||||||
|
|
||||||
|
![[airmonitor3.png]]
|
||||||
@@ -4,6 +4,7 @@ Category: Creations
|
|||||||
Summary: Fake dog barking for home security while on vacation.
|
Summary: Fake dog barking for home security while on vacation.
|
||||||
Image: fake-dog.jpg
|
Image: fake-dog.jpg
|
||||||
Tags: feed
|
Tags: feed
|
||||||
|
Guid: 0c80d4cf5e414254b158ef9f9b082f8f
|
||||||
|
|
||||||
I set up a fake dog that barks if my surveillance cameras are triggered while I'm out of town on vacation. It's a pair of computer speakers plugged into a Raspberry Pi, which is an inexpensive single-board computer. One speaker faces the front door and the other faces the side door.
|
I set up a fake dog that barks if my surveillance cameras are triggered while I'm out of town on vacation. It's a pair of computer speakers plugged into a Raspberry Pi, which is an inexpensive single-board computer. One speaker faces the front door and the other faces the side door.
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ Category: Creations
|
|||||||
Summary: Hacking my garage door opener to work over Wifi.
|
Summary: Hacking my garage door opener to work over Wifi.
|
||||||
Image: garage3.jpg
|
Image: garage3.jpg
|
||||||
Tags: feed
|
Tags: feed
|
||||||
|
Guid: 3e386396748b400ea7434de28e1759ec
|
||||||
|
|
||||||
In the quest to automate as much of my house as possible, I thought it would be useful to be able to remotely control my garage door from my home automation system. If I suspected that I forgot to close it while leaving, I could check in my security cameras and then close it from anywhere. It's nice having this peace of mind, even if it almost never happens.
|
On the quest to automate as much of my house as possible, I thought it would be useful to be able to remotely control my garage door from my home automation system. If I suspected that I forgot to close it while leaving, I could check in my security cameras and then close it from anywhere. It's nice having this peace of mind, even if it almost never happens.
|
||||||
|
|
||||||
Instead of reverse engineering the wireless protocol, cracking the encryption, and sending my own commands, I figured it would be much easier to hack the hardware. I pried open a spare remote to find that it contained a basic PCB with simple tactile switches.
|
Instead of reverse engineering the wireless protocol, cracking the encryption, and sending my own commands, I figured it would be much easier to hack the hardware. I pried open a spare remote to find that it contained a basic PCB with simple tactile switches.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
Title: Hacking my Air Purifier onto Wifi
|
||||||
|
Date: 2026-05-27
|
||||||
|
Category: Creations
|
||||||
|
Summary: Hardware hacking my Airmega 200M Purifier onto Wifi.
|
||||||
|
Image: purifier1.jpg
|
||||||
|
Wide: true
|
||||||
|
|
||||||
|
My Airmega 200M air purifier has four speed settings: useless, less useless, annoying and SCREAMING BANSHEE. I was able to connect an ESP8266 Arduino to the motor driver board and get direct fine-grained control of the speed over Wifi. I use this to vary the speed based on my distance to the air purifier so I don't have to listen to it.
|
||||||
|
|
||||||
|
![[purifier1.jpg]]
|
||||||
|
|
||||||
|
## Home Automation
|
||||||
|
|
||||||
|
I use motion sensors to control the lights in my house, so my home automation system somewhat has an idea of what room I'm in. I use this data to control the purifier's speed based on how far away I am from it. If I'm on the same floor, it runs very quietly (~12% power). If I'm one floor away, it runs at 50%. If I'm two floors away (or I'm not home), it runs at 100%.
|
||||||
|
|
||||||
|
I live alone, but my automation system has a "Guest Mode" which prevents the motion sensors from turning lights off. If this mode is enabled, the purifier only runs quietly.
|
||||||
|
|
||||||
|
I didn't want to get a different air purifier that was smart and have to deal with some app or let a random smart device onto my network.
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
The power supply and motor driver board originally connect to a board that has the buttons and LEDs via a 6-pin ribbon cable. Pin 4 of that cable expects a PWM signal that controls the speed of the purifier's blower motor proportional to the duty cycle. Pins 1 and 5 happen to be 5 V and Ground, which are used to power the Wemos D1 Mini ESP8266.
|
||||||
|
|
||||||
|
The Wemos boots up and connects to an MQTT broker on my Wifi network. It subscribes to the `iot/purifier/mega_1234/speed` topic where `1234` is part of the MAC address so different purifiers on the network can be addressed easily. It listens to messages that are numbers 0-100 and maps them linearly to 60-140 which correspond to the PWM duty cycle range that the motor driver expects.
|
||||||
|
|
||||||
|
You can find the [source code](https://git.tannercollin.com/tanner/airmega-hack/src/branch/master/firmware/firmware.ino) on my Gitea.
|
||||||
|
|
||||||
|
An unfortunate side effect of this is that the control board is completely dead and manual control of the purifier no longer works except for unplugging it. I actually don't mind this because it also kills the blue LEDs and I just use my smart watch or phone to control it instead. The built-in dust sensor also no longer works, but it should be possible to also read this with the Arduino over serial in the future. I discuss this further down.
|
||||||
|
|
||||||
|
## Hardware Hacking
|
||||||
|
|
||||||
|
Hacking the purifier is actually fairly straightforward. My purifier was already over a year old, so I didn't care about voiding my warranty. The power supply isn't isolated and the electronics' ground is floating at something like 48 VAC (learned this the hard way), so I keep it unplugged while I'm modifying it.
|
||||||
|
|
||||||
|
I removed the cover and all the filters, and then the nine Phillips screws holding the case together. I lifted the blower half up and propped it up at an angle. You can see a photo of it below, with the ribbon cable plugged into the control board at the bottom right:
|
||||||
|
|
||||||
|
![[purifier2.jpg]]
|
||||||
|
|
||||||
|
I didn't want to destroy the cable by cutting it to connect it to the Arduino, so I ordered some connectors off of Digikey. Both [25SH-B-06-TR](https://www.digikey.ca/short/mt9d2cm0) and [51125-06-0200-01](https://www.digikey.ca/short/v53bnq97) mate well with the white ribbon cable connector. I soldered the connector to a bit of 0.1" perf board and wired it to the Arduino:
|
||||||
|
|
||||||
|
![[purifier3.jpg]]
|
||||||
|
![[purifier4.jpg]]
|
||||||
|
|
||||||
|
The wiring is:
|
||||||
|
|
||||||
|
```
|
||||||
|
Wemos Cable
|
||||||
|
5V - Pin 1 (white)
|
||||||
|
G - Pin 5
|
||||||
|
D1 - Pin 4
|
||||||
|
```
|
||||||
|
|
||||||
|
I then simply unplugged the control board, plugged in my perf board connector, and secured it with some of the tape inside the purifier as you can see in the first photo. I reassembled the case and reinstalled the filters.
|
||||||
|
|
||||||
|
## Research
|
||||||
|
|
||||||
|
Researching the hack was not as straightforward. I disassembled the unit and noticed the blower motor was wired to the same board where the power supply was. I then saw the ribbon cable between that board and the control board, so chose to target it first. The pins were labelled on the board's silkscreen and I soldered some jumper cables to the back so I could scope them easier.
|
||||||
|
|
||||||
|
![[purifier5.jpg]]
|
||||||
|
|
||||||
|
I attached my oscilloscope's ground lead to the `GND_S` pin and probe to the `SIG1` pin, expecting that to be a signal. I plugged the purifier in and immediately heard a POP! That's when I learned the power supply isn't isolated and I had just shorted 48 volts through my oscilloscope to ground.
|
||||||
|
|
||||||
|
Luckily I only blew a fuse on the board and just had to solder a new one on, part number [MST 3.15A 250V](https://www.digikey.ca/short/nv9wtwr9). Then I switched to using two probes. One on the signal and one on the ground pin and used my oscilloscope's math feature to subtract them. This made a noisy and imprecise trace, but it was enough to tell the speed was controlled by PWM on Pin 4.
|
||||||
|
|
||||||
|
This showed me the hack was indeed possible, so I ordered a differential probe off Amazon in order to scope the signals precisely:
|
||||||
|
|
||||||
|
- Pin 1 (+5V_1A, white) is pretty clean 5.3 V always
|
||||||
|
- Pin 2 (SIG1) doesn't seem like anything
|
||||||
|
- Pin 3 (CON3-2) is speed tach. 50% duty cycle, period widens as it gets slower
|
||||||
|
- low speed: 80 Hz
|
||||||
|
- medium speed: 119 Hz
|
||||||
|
- high speed: 200 Hz
|
||||||
|
- Pin 4 (CON3-3) is speed control PWM 0-5 V, higher duty cycle for more speed
|
||||||
|
- low speed: 6.8% duty cycle
|
||||||
|
- medium speed: 8.1% duty cycle
|
||||||
|
- high speed: 13.7% duty cycle
|
||||||
|
- Pin 5 (GND_S) ground, floats 48 VAC above mains ground
|
||||||
|
- Pin 6 (15VON/OFF) is 1.5 V when machine is off, noisy 5.3 V when running
|
||||||
|
|
||||||
|
I wrote a quick Arduino sketch to see if the 0-3.3 V PWM it outputs was enough to control the speed and it was. This, combined with the fact there's 5 V supplied by the ribbon cable meant that the Arduino could be connected simply with three wires, without the need for level shifters or a power supply.
|
||||||
|
|
||||||
|
I ordered 17 different 6-pin connectors with the same pitch off Digikey and tested each one until I was satisfied with the fit. I programmed the Arduino, soldered it all up, and the hack was complete! I've been using it for almost a year now.
|
||||||
|
|
||||||
|
## Future Improvements
|
||||||
|
|
||||||
|
As I mentioned before, it should be possible to communicate directly with the integrated particulate sensor over its cable. I didn't bother to do this because I already had my own [[Custom Air Quality Monitor]] running. The sensor data could then be used as feedback to control the blower speed, similar to the built in functionality. The blower's speed could be ramped up gradually now that fine-grain control of the speed is possible.
|
||||||
|
|
||||||
|
The control board is completely disabled after doing this hack which might be inconvenient for house guests or spouses. An improvement could be using the Arduino to intercept the speed control signal instead by putting it in the middle. It could read the control board's PWM signal by timing the pulse width, or as a voltage on one of its analog pins after low-pass filtering it. Then whenever there's a change to the MQTT speed or control board speed, output the latest value.
|
||||||
|
|
||||||
|
## Appendix: Home Assistant
|
||||||
|
|
||||||
|
I don't actually use Home Assistant for this, but here's how one could configure it. This assumes you have an MQTT broker (ie. mosquitto) running. If you don't, Home Assistant can be configured to run a broker by installing the Mosquitto broker add-on from the Add-on Store.
|
||||||
|
|
||||||
|
### Set up MQTT
|
||||||
|
|
||||||
|
Go to Settings > Devices & Services > MQTT > Configure > Re-configure MQTT.
|
||||||
|
|
||||||
|
Enter your MQTT broker details. If you are using the broker add-on, it should be automatically discovered.
|
||||||
|
|
||||||
|
### Add the Slider
|
||||||
|
|
||||||
|
1. Go to Settings > Devices & Services > Helpers
|
||||||
|
2. Click + Create Helper (bottom right) and select Number
|
||||||
|
3. Set the Name (like "Purifier Speed") and Icon (I used "mdi:air-purifier")
|
||||||
|
4. Set Minimum value to 0 and Maximum value to 100
|
||||||
|
5. Click Create
|
||||||
|
|
||||||
|
Note the Entity ID of the slider. Mine is "input_number.purifier_speed".
|
||||||
|
|
||||||
|
### Add the Automation
|
||||||
|
|
||||||
|
Go to Settings > Automations & Scenes > + Create Automation > Create new automation.
|
||||||
|
|
||||||
|
You can do this with the UI but I prefer entering YAML code. Click the three dots at the top right and select "Edit in YAML". Paste this in:
|
||||||
|
|
||||||
|
```
|
||||||
|
trigger:
|
||||||
|
- platform: state
|
||||||
|
entity_id: input_number.purifier_speed
|
||||||
|
action:
|
||||||
|
- service: mqtt.publish
|
||||||
|
data:
|
||||||
|
topic: "iot/purifier/mega_1234/speed"
|
||||||
|
payload: "{{ states('input_number.purifier_speed') | int }}"
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that `input_number.purifier_speed` has to match the Entity ID of the slider and `mega_1234` should be changed to the ID the purifier Arduino reports over serial after programming.
|
||||||
|
|
||||||
|
Hit save and call it something like "Purifier automation". The slider should now be on your dashboard under a "Helpers" section.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
AI disclosure: None of this article's content or prose was written by AI. The Arduino code that runs on the ESP8266 was written by Gemini.
|
||||||
@@ -4,6 +4,7 @@ Category: Creations
|
|||||||
Summary: A withered hand I welded out of scrap metal.
|
Summary: A withered hand I welded out of scrap metal.
|
||||||
Image: hand1.jpg
|
Image: hand1.jpg
|
||||||
Tags: feed
|
Tags: feed
|
||||||
|
Guid: 0bc567cd5c45479d8380214b24a35563
|
||||||
|
|
||||||
I was visiting my cousins in Radium, BC and decided to learn stick welding at their shop. I wanted to create a sculpture, so with pieces of scrap metal I welded together this hand. The beads are far from perfect. Working with small pieces of rusted metal made it difficult.
|
I was visiting my cousins in Radium, BC and decided to learn stick welding at their shop. I wanted to create a sculpture, so with pieces of scrap metal I welded together this hand. The beads are far from perfect. Working with small pieces of rusted metal made it difficult.
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ Category: Writing
|
|||||||
Summary: My experiments growing food with hydroponics.
|
Summary: My experiments growing food with hydroponics.
|
||||||
Wide: true
|
Wide: true
|
||||||
Tags: feed
|
Tags: feed
|
||||||
|
Guid: 5cf23ab1f9894a4b91e6593ce498a73a
|
||||||
|
|
||||||
[TOC]
|
[TOC]
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ Summary: Photos from my trip to Japan.
|
|||||||
Image: japan06lo.jpg
|
Image: japan06lo.jpg
|
||||||
Nofilter: true
|
Nofilter: true
|
||||||
Tags: feed
|
Tags: feed
|
||||||
|
Guid: 4dbb422703be4c84b20132ffaa883f58
|
||||||
|
|
||||||
All photos are unmodified (not even cropped) and taken with a Pixel 6a.
|
All photos are unmodified (not even cropped) and taken with a Pixel 6a.
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ Category: Creations
|
|||||||
Summary: A dress made out of LEDs that twinkle like stars.
|
Summary: A dress made out of LEDs that twinkle like stars.
|
||||||
Image: dress1.jpg
|
Image: dress1.jpg
|
||||||
Tags: feed
|
Tags: feed
|
||||||
|
Guid: 420a9ca8533c4667a89822d2b5df186d
|
||||||
|
|
||||||
A friend of mine was attending a stars and constellations themed ball. She wanted to wear a dress that was lit up with LEDs acting as twinkling stars. Seven of the 28 stars are aligned to resemble the Big Dipper constellation and twinkle differently than the rest, which twinkle in a random pattern.
|
A friend of mine was attending a stars and constellations themed ball. She wanted to wear a dress that was lit up with LEDs acting as twinkling stars. Seven of the 28 stars are aligned to resemble the Big Dipper constellation and twinkle differently than the rest, which twinkle in a random pattern.
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ Category: Creations
|
|||||||
Summary: A device to toggle my lights remotely.
|
Summary: A device to toggle my lights remotely.
|
||||||
Image: light1.jpg
|
Image: light1.jpg
|
||||||
Tags: feed
|
Tags: feed
|
||||||
|
Guid: 9f716895d0e7400c9538e9a5f9b327ce
|
||||||
|
|
||||||
I wanted the ability to toggle my bedroom light remotely for convenience. I designed a circuit that allows me to control my light with any device that can load a webpage.
|
I wanted the ability to toggle my bedroom light remotely for convenience. I designed a circuit that allows me to control my light with any device that can load a webpage.
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ Title: Choosing a Linux Flavour
|
|||||||
Date: 2020-10-31
|
Date: 2020-10-31
|
||||||
Category: Writing
|
Category: Writing
|
||||||
Summary: A recommendation on which flavour of Linux to run.
|
Summary: A recommendation on which flavour of Linux to run.
|
||||||
|
Image: distro1.png
|
||||||
Wide: true
|
Wide: true
|
||||||
Tags: feed
|
Tags: feed
|
||||||
|
Guid: fedd81aa796847f09e559df2d5e5e917
|
||||||
|
|
||||||
[TOC]
|
[TOC]
|
||||||
|
|
||||||
@@ -21,6 +23,8 @@ When people refer to the "flavour of Linux" they are talking about a Linux distr
|
|||||||
|
|
||||||
The major Linux distros are practically all the same. If you master one it's easy to pick up the others. The main differences you'll run into are which tools you use to install new software, and the desktop environment, which is what all the windows and buttons look like.
|
The major Linux distros are practically all the same. If you master one it's easy to pick up the others. The main differences you'll run into are which tools you use to install new software, and the desktop environment, which is what all the windows and buttons look like.
|
||||||
|
|
||||||
|
![[distro1.png]]
|
||||||
|
|
||||||
I recommend two Linux distros, Debian and Ubuntu. Ubuntu is based off of Debian, so they are very similar.
|
I recommend two Linux distros, Debian and Ubuntu. Ubuntu is based off of Debian, so they are very similar.
|
||||||
|
|
||||||
## Pros of Debian
|
## Pros of Debian
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ Date: 2024-07-18
|
|||||||
Category: Writing
|
Category: Writing
|
||||||
Summary: A collection of makerspaces I've toured.
|
Summary: A collection of makerspaces I've toured.
|
||||||
Tags: feed
|
Tags: feed
|
||||||
|
Guid: 27ca744e77c042c8bb1df9edd37112ae
|
||||||
|
|
||||||
When I travel I often try to tour Makerspaces and then share what I've learned with the one that I'm a part of, [[Protospace]]. Below you'll find links to the posts I've made on our forums about the makerspaces I've visited.
|
When I travel I often try to tour Makerspaces and then share what I've learned with the one that I'm a part of, [[Protospace]]. Below you'll find links to the posts I've made on our forums about the makerspaces I've visited.
|
||||||
|
|
||||||
@@ -15,6 +16,10 @@ I'm interested in learning about:
|
|||||||
- how decisions are made and conflicts resolved
|
- how decisions are made and conflicts resolved
|
||||||
- how toxic members are dealt with
|
- how toxic members are dealt with
|
||||||
|
|
||||||
|
## Italy
|
||||||
|
|
||||||
|
November 2024: [LOFOIO Makerspace in Florence](https://forum.protospace.ca/t/lofoio-makerspace-in-florence/7956)
|
||||||
|
|
||||||
## Japan
|
## Japan
|
||||||
|
|
||||||
September 2023: [Take Space in Hamamatsu](https://forum.protospace.ca/t/take-space-in-hamamatsu/4800)
|
September 2023: [Take Space in Hamamatsu](https://forum.protospace.ca/t/take-space-in-hamamatsu/4800)
|
||||||
@@ -43,7 +48,7 @@ February 2023: [SYN Shop Makerspace Las Vegas](https://forum.protospace.ca/t/syn
|
|||||||
|
|
||||||
## Canada
|
## Canada
|
||||||
|
|
||||||
Septempber 2024: [Makerspace Nanaimo](https://forum.protospace.ca/t/makerspace-nanaimo-tour/7576)
|
September 2024: [Makerspace Nanaimo](https://forum.protospace.ca/t/makerspace-nanaimo-tour/7576)
|
||||||
|
|
||||||
October 2022: [Makerspace Tour in Turner Valley](https://forum.protospace.ca/t/makerspace-tour-in-turner-valley/2795)
|
October 2022: [Makerspace Tour in Turner Valley](https://forum.protospace.ca/t/makerspace-tour-in-turner-valley/2795)
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ Title: Notica
|
|||||||
Date: 2022-05-17
|
Date: 2022-05-17
|
||||||
Category: Projects
|
Category: Projects
|
||||||
Summary: Send browser notifications from your terminal. No installation. No registration.
|
Summary: Send browser notifications from your terminal. No installation. No registration.
|
||||||
Image: notica1.jpg
|
Image: notica1.png
|
||||||
Tags: feed
|
Tags: feed
|
||||||
|
Guid: 75d87817471c4c13a986032fe64f8eb7
|
||||||
|
|
||||||
[Notica](https://notica.us) allows you to send browser notifications from your terminal to know when a slow command has finished running. It doesn't require installing anything or registering an account. It also works over ssh unlike `notify-send`.
|
[Notica](https://notica.us) allows you to send browser notifications from your terminal to know when a slow command has finished running. It doesn't require installing anything or registering an account. It also works over ssh unlike `notify-send`.
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ Category: Creations
|
|||||||
Summary: My first attempt at painting with acrylic.
|
Summary: My first attempt at painting with acrylic.
|
||||||
Image: painting1.jpg
|
Image: painting1.jpg
|
||||||
Tags: feed
|
Tags: feed
|
||||||
|
Guid: a8c8430f531549418601ae166545529e
|
||||||
|
|
||||||
The painting is called “Man’s Reach Exceeds His Grasp”. I've always wanted to try painting and thought I had a good idea, so after a couple of drawings I attempted to paint it. I eventually got it framed at Michaels. Many thanks to my friend Laura for the opportunity to do this, I couldn't have done it without her help.
|
The painting is called “Man’s Reach Exceeds His Grasp”. I've always wanted to try painting and thought I had a good idea, so after a couple of drawings I attempted to paint it. I eventually got it framed at Michaels. Many thanks to my friend Laura for the opportunity to do this, I couldn't have done it without her help.
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ Category: Creations
|
|||||||
Summary: A device that automatically waters plants.
|
Summary: A device that automatically waters plants.
|
||||||
Image: waterer2.jpg
|
Image: waterer2.jpg
|
||||||
Tags: feed
|
Tags: feed
|
||||||
|
Guid: 42d4bea55f674ed48fa07f369bd24aeb
|
||||||
|
|
||||||
One day I decided watering my one plant was too much work, so I automated it. It's also great for when I'm on vacation. The plant is a year old now and doesn't look as good as it used to (kinda like you). So this machine is like its life support.
|
One day I decided watering my one plant was too much work, so I automated it. It's also great for when I'm on vacation. The plant is a year old now and doesn't look as good as it used to (kinda like you). So this machine is like its life support.
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ Summary: An outline of my projects at Calgary's makerspace Protospace.
|
|||||||
Image: protospace1.jpg
|
Image: protospace1.jpg
|
||||||
Wide: true
|
Wide: true
|
||||||
Tags: feed
|
Tags: feed
|
||||||
|
Guid: 0f2fac18522b4c268f0df7ec10cdc171
|
||||||
|
|
||||||
[Protospace](https://protospace.ca) is Calgary's original makerspace, a place where people go to make things and work on projects. It's a two-bay industrial shop with a full wood working area, metal working area, electronics lab, two laser cutters, five 3D printers, and sewing room. Members pay $55/month for 24/7 access to the facility and everyone is equal: Protospace has no owners and decisions are made by the membership.
|
[Protospace](https://protospace.ca) is Calgary's original makerspace, a place where people go to make things and work on projects. It's a two-bay industrial shop with a full wood working area, metal working area, electronics lab, two laser cutters, five 3D printers, and sewing room. Members pay $55/month for 24/7 access to the facility and everyone is equal: Protospace has no owners and decisions are made by the membership.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
Title: Protovac Retro Terminal
|
||||||
|
Date: 2025-06-14
|
||||||
|
Category: Creations
|
||||||
|
Summary: A retro dumb terminal interface at my local makerspace.
|
||||||
|
Image: protovac1.jpg
|
||||||
|
Tags: feed
|
||||||
|
Guid: 0e0f9b63f1344aefbf6fce80f6e813ee
|
||||||
|
|
||||||
|
Protovac is a retro dumb terminal interface that lives at my local makerspace, [[Protospace]]. Its main use is printing storage labels and name tags for members and guests when they visit.
|
||||||
|
|
||||||
|
![[protovac1.jpg]]
|
||||||
|
|
||||||
|
An 85-year-old member donated the 1983 Morrow MDT-60 video display terminal that he bought new from London Drugs and kept in his closet. Originally this terminal is supposed to connect to a mainframe computer (perhaps in a different room) and display text over a serial connection.
|
||||||
|
|
||||||
|
In this case it connects to a Raspberry Pi computer mounted to the back over 9600 baud serial UART. The Pi has been configured to output a terminal over its UART pins and auto login the protovac user with `agetty`. The protovac user's shell has been replaced with the Python script that runs the curses-based TUI.
|
||||||
|
|
||||||
|
You can find the [source code](https://github.com/Protospace/protovac) on Protospace's GitHub.
|
||||||
|
|
||||||
|
In addition to printing labels for members, Protovac:
|
||||||
|
|
||||||
|
- can control the train in the Protospace welcome room
|
||||||
|
- displays stats about Protospace (next meeting, next class, member counts, etc)
|
||||||
|
- can send a message to our marquee LED sign
|
||||||
|
- has a chat interface to message ChatGPT
|
||||||
|
- has an interface to access Wolfram Alpha
|
||||||
|
- can play the games NetHack, Moria, 2048, Zork, and Hitchhiker's
|
||||||
|
|
||||||
|
Here's what the home screen looks like:
|
||||||
|
```
|
||||||
|
_______ _______ ___ _________ ___ ____ ____ _ ______
|
||||||
|
|_ __ \|_ __ \ .' `. | _ _ | .' `.|_ _| |_ _|/ \ .' ___ |
|
||||||
|
| |__) | | |__) | / .-. \|_/ | | \_|/ .-. \ \ \ / / / _ \ / .' \_|
|
||||||
|
| ___/ | __ / | | | | | | | | | | \ \ / / / ___ \ | |
|
||||||
|
_| |_ _| | \ \_\ `-' / _| |_ \ `-' / \ ' /_/ / \ \_\ `.___.'\
|
||||||
|
|_____| |____| |___|`.___.' |_____| `.___.' \_/|____| |____|`.____ .'
|
||||||
|
|
||||||
|
[I] Info [N] Nametag UNIVERSAL COMPUTER
|
||||||
|
. * - )-
|
||||||
|
[S] Stats [L] Label . * o . *
|
||||||
|
|
|
||||||
|
[G] LED Sign [Z] Games . . -O-
|
||||||
|
| * . -0-
|
||||||
|
[C] Classes [V] Protovac Sign
|
||||||
|
. . | *
|
||||||
|
[P] Protocoin * -O- .
|
||||||
|
. * | ,
|
||||||
|
[M] Message . o
|
||||||
|
.---.
|
||||||
|
[T] Think = _/__[0]\_ . * o '
|
||||||
|
= = (_________) .
|
||||||
|
[A] About . *
|
||||||
|
* - ) - *
|
||||||
|
Copyright (c) 1985 Bikeshed Computer Systems Ltd.
|
||||||
|
```
|
||||||
|
|
||||||
|
If you press the "C" key, for example, a list of Protospace classes appears:
|
||||||
|
|
||||||
|
```
|
||||||
|
PROTOVAC UNIVERSAL COMPUTER
|
||||||
|
|
||||||
|
Protospace Classes
|
||||||
|
================== Instructor Cost Students
|
||||||
|
|
||||||
|
[PAST] Woodworking Tools 1: Intro to Saws
|
||||||
|
Sun Jun 22, 2025 2:00 PM Mike M. $20.00 5 / 6
|
||||||
|
|
||||||
|
Woodworking Tools 2: Jointer, Thickness Planer, Drum Sander
|
||||||
|
Sun Jun 22, 2025 5:00 PM Mike M. $20.00 6 / 6
|
||||||
|
|
||||||
|
Blender Phreaking Phrydays
|
||||||
|
Fri Jun 27, 2025 7:00 PM Jeff D. Free 0
|
||||||
|
|
||||||
|
New Member Orientation and Safety
|
||||||
|
Sun Jun 29, 2025 3:00 PM Cole N. Free 3 / 10
|
||||||
|
|
||||||
|
Laser I: Basic Cutting and Engraving (Thunder Laser)
|
||||||
|
Wed Jul 9, 2025 8:30 PM Craig P. $20.00 6 / 8
|
||||||
|
|
||||||
|
Woodworking Tools 1: Intro to Saws
|
||||||
|
Sun Jul 20, 2025 2:00 PM Vince K. $20.00 6 / 6
|
||||||
|
|
||||||
|
[B] Back [J] Down [K] Up
|
||||||
|
```
|
||||||
|
|
||||||
|
The "S" key shows stats about Protospace:
|
||||||
|
|
||||||
|
```
|
||||||
|
PROTOVAC UNIVERSAL COMPUTER
|
||||||
|
|
||||||
|
Protospace Stats
|
||||||
|
================
|
||||||
|
|
||||||
|
Next meeting: None
|
||||||
|
|
||||||
|
Next clean: None
|
||||||
|
|
||||||
|
Next class: Woodworking Tools 2: Jointer, Thickness Planer, Drum Sander
|
||||||
|
Sun Jun 22, 2025 5:00 PM
|
||||||
|
|
||||||
|
Last class: Woodworking Tools 1: Intro to Saws
|
||||||
|
Sun Jun 22, 2025 2:00 PM
|
||||||
|
|
||||||
|
Member count: 464 Green: 408 Paused / expired: 1590
|
||||||
|
|
||||||
|
Card scans: 18
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[B] Back
|
||||||
|
```
|
||||||
@@ -2,8 +2,9 @@ Title: QotNews
|
|||||||
Date: 2022-05-18
|
Date: 2022-05-18
|
||||||
Category: Projects
|
Category: Projects
|
||||||
Summary: Hacker News, Reddit, Lobsters, and Tildes articles pre-rendered in reader mode. Optimized for speed and distraction-free reading.
|
Summary: Hacker News, Reddit, Lobsters, and Tildes articles pre-rendered in reader mode. Optimized for speed and distraction-free reading.
|
||||||
Image: qotnews1.jpg
|
Image: qotnews1.png
|
||||||
Tags: feed
|
Tags: feed
|
||||||
|
Guid: 445a54d8799746d2b86ee8134cbc441c
|
||||||
|
|
||||||
[QotNews](https://news.t0.vc) is a news meta-aggregator. It gathers top articles from four news aggregators: Hacker News, Reddit, Lobsters, and Tildes along with their comments. The articles are then transformed into readable versions with consistent formatting and distractions removed. All articles in the main feed are preloaded by the client so they load instantly when clicked on.
|
[QotNews](https://news.t0.vc) is a news meta-aggregator. It gathers top articles from four news aggregators: Hacker News, Reddit, Lobsters, and Tildes along with their comments. The articles are then transformed into readable versions with consistent formatting and distractions removed. All articles in the main feed are preloaded by the client so they load instantly when clicked on.
|
||||||
|
|
||||||
|
|||||||
@@ -1,48 +1,77 @@
|
|||||||
Title: Things I Recommend
|
Title: Recommendations
|
||||||
Date: 2022-06-24
|
Date: 2022-06-24
|
||||||
Category: Writing
|
Category: Writing
|
||||||
Summary: Software and products that I recommend you use.
|
Summary: Software and products that I recommend you use.
|
||||||
Wide: true
|
Wide: true
|
||||||
Tags: feed
|
Tags: feed
|
||||||
|
Guid: ec7e1d66bbe343d59235e0b185ee44d6
|
||||||
|
|
||||||
This outlines some software and devices I recommend you use: uBlock Origin, Sponsorblock, Aegis Authenticator, ThruNite T1 flashlights, Logitech G Pro Wireless mice, and ThinkPad Laptops. Nothing here was sponsored.
|
This outlines some software and devices I recommend you use: uBlock Origin, Sponsorblock, Aegis Authenticator, ThinkPad Laptops, a flashlight, a Leatherman, and various phone apps. Nothing here was sponsored.
|
||||||
|
|
||||||
[TOC]
|
[TOC]
|
||||||
|
|
||||||
## Software
|
## Software
|
||||||
|
|
||||||
### uBlock Origin
|
### uBlock Origin
|
||||||
|
|
||||||
uBlock Origin is an open source ad blocker and something I install immediately on all my devices. Running an ad blocker makes browsing the web way better. It removes distracting ads (even from YouTube), invasive tracking, and makes you safer by removing potentially [fake links](https://news.t0.vc/LOBW/c#drekipus1657325184). It's the best piece of software I use even though it mostly remains unseen. And it even works on your phone.
|
uBlock Origin is an open source ad blocker and something I install immediately on all my devices. Running an ad blocker makes browsing the web way better. It removes distracting ads (even from YouTube), invasive tracking, and makes you safer by removing potentially [fake links](https://news.t0.vc/LOBW/c#drekipus1657325184). It's the best piece of software I use even though it mostly remains unseen. And it even works on your phone.
|
||||||
|
|
||||||
You can install it on [Firefox Desktop](https://addons.mozilla.org/en-CA/firefox/addon/ublock-origin/), [Firefox Android](https://addons.mozilla.org/en-CA/android/addon/ublock-origin/), and [Chrome Desktop](https://chrome.google.com/webstore/detail/ublock-origin/cjpalhdlnbpafiamejdnhcphjbkeiagm?hl=en). Make sure you install the correct "Origin" version and avoid "ublock.org".
|
You can install it on [Firefox Desktop](https://addons.mozilla.org/en-CA/firefox/addon/ublock-origin/), [Firefox Android](https://addons.mozilla.org/en-CA/android/addon/ublock-origin/), and [Chrome Desktop](https://chrome.google.com/webstore/detail/ublock-origin/cjpalhdlnbpafiamejdnhcphjbkeiagm?hl=en). Make sure you install the correct "Origin" version and avoid "ublock.org".
|
||||||
|
|
||||||
### SponsorBlock
|
### SponsorBlock
|
||||||
|
|
||||||
SponsorBlock automatically skips over sponsored segments in YouTube videos. Not YouTube ads (that's what uBlock Origin is for), but the actual parts of the video sponsored by companies to advertise their products. It uses a crowd-sourced database of timestamps to seamlessly jump over those parts. It also allows you to skip to the highlight of some videos by pressing "enter" so you aren't wasting time watching exposition.
|
SponsorBlock automatically skips over sponsored segments in YouTube videos. Not YouTube ads (that's what uBlock Origin is for), but the actual parts of the video sponsored by companies to advertise their products. It uses a crowd-sourced database of timestamps to seamlessly jump over those parts. It also allows you to skip to the highlight of some videos by pressing "enter" so you aren't wasting time watching exposition.
|
||||||
|
|
||||||
You can install it on [Firefox Desktop](https://addons.mozilla.org/en-CA/firefox/addon/sponsorblock/) and [Chrome Desktop](https://chrome.google.com/webstore/detail/sponsorblock-for-youtube/mnjggcdmjocbbbhaepdhchncahnbgone?hl=en).
|
You can install it on [Firefox Desktop](https://addons.mozilla.org/en-CA/firefox/addon/sponsorblock/) and [Chrome Desktop](https://chrome.google.com/webstore/detail/sponsorblock-for-youtube/mnjggcdmjocbbbhaepdhchncahnbgone?hl=en).
|
||||||
|
|
||||||
![[recommend1.png | a screenshot of a conversation with a friend who likes sponsorblock]]
|
|
||||||
|
|
||||||
![[recommend2.png | another]]
|
|
||||||
|
|
||||||
### Aegis Authenticator
|
### Aegis Authenticator
|
||||||
Aegis is a two-factor authenticator (2fa) app for Android that's free and open source. The killer feature and why I recommend it is that it supports automatic encrypted backups of the database in JSON format. You can unlock the app with a password or optional fingerprint. It supported true OLED dark mode. An alternative app is andOTP but there were [problems](https://news.t0.vc/EQYR/c#williamwchuang1553266688) with the backups' encryption back when I switched to Aegis.
|
|
||||||
|
|
||||||
Enabling 2fa on your accounts is essential for security, especially preventing credential stuffing attacks. Backing up your 2fa data is important in case you lose your phone. It saves you having to dig out recovery codes or try and convince someone that you own the account.
|
Aegis is a two-factor authenticator (2fa) app for Android that's free and open source. The killer feature and why I recommend it is that it supports automatic encrypted backups of the database in JSON format. You can unlock the app with a password or fingerprint. An alternative app is andOTP but there were [problems](https://news.t0.vc/EQYR/c#williamwchuang1553266688) with the backups' encryption back when I switched to Aegis.
|
||||||
|
|
||||||
You can install it on Android via the [Play Store](https://play.google.com/store/apps/details?id=com.beemdevelopment.aegis) or [F-Droid](https://f-droid.org/en/packages/com.beemdevelopment.aegis).
|
You can install it on Android via the [Play Store](https://play.google.com/store/apps/details?id=com.beemdevelopment.aegis) or [F-Droid](https://f-droid.org/en/packages/com.beemdevelopment.aegis).
|
||||||
|
|
||||||
|
### Misc. Phone Apps
|
||||||
|
|
||||||
|
FOSS App Store: F-Droid
|
||||||
|
|
||||||
|
QR code scanner: Binary Eye
|
||||||
|
|
||||||
|
Plant identification: PlantNet
|
||||||
|
|
||||||
|
Isolate apps: Shelter
|
||||||
|
|
||||||
|
Audiobook player: Voice
|
||||||
|
|
||||||
|
Phone sensors: phyphox
|
||||||
|
|
||||||
|
Maps: Organic Maps
|
||||||
|
|
||||||
|
Photos: Immich
|
||||||
|
|
||||||
|
Calculator: andanCalc PRO
|
||||||
|
|
||||||
|
PDF viewer: Pdf Viewer Plus
|
||||||
|
|
||||||
|
NFC reader: NFC Tools
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Devices
|
## Devices
|
||||||
### ThruNite T1 Flashlight
|
|
||||||
This is a tiny rechargeable flashlight that can output 1500 lumens, enough to hurt your eyes if you look directly into it. It has a magnet on the back to stick to things and an optional pocket-hat clip. I liked it so much I've bought three of them and gave one to a friend who then bought himself a couple more.
|
|
||||||
|
|
||||||
![[recommend3.png | a screenshot of a friend telling me how much he likes the flashlight]]
|
|
||||||
|
|
||||||
### Logitech G Pro Wireless Mice
|
|
||||||
I've used a lot of wireless mice, and this one is by far my favourite. It has a USB dongle which I prefer because Bluetooh pairing annoys me. If you disable the LEDs the battery lasts for a few weeks of solid usage.
|
|
||||||
|
|
||||||
I like it so much that I own five of them: one for my office desk, nook desk, laptop bag, home theatre, and surveillance cameras NVR. I leave the last one plugged in so that when a mouse battery dies, I can swap it with a fully charged mouse that's ready to go. It also ensures they wear at a somewhat even rate.
|
|
||||||
|
|
||||||
### ThinkPad Laptops
|
### ThinkPad Laptops
|
||||||
|
|
||||||
I've had several different laptops over the years and have settled on buying ThinkPads going forward. I hate Lenovo as company because of their [Superfish scandal](https://en.wikipedia.org/wiki/Superfish#Lenovo_security_incident), but I can't deny that ThinkPads are absolutely solid. I currently own a ThinkPad X1 Carbon 6th Gen.
|
I've had several different laptops over the years and have settled on buying ThinkPads going forward. I hate Lenovo as company because of their [Superfish scandal](https://en.wikipedia.org/wiki/Superfish#Lenovo_security_incident), but I can't deny that ThinkPads are absolutely solid. I currently own a ThinkPad X1 Carbon 6th Gen.
|
||||||
|
|
||||||
A lot of Linux developers use ThinkPads which means Linux is well supported on them and the drivers just work. The laptops are easy to pop open and service. Many parts are user-replaceable. My laptop charges off a small USB-C phone charger that I carry around. In a state of sickness-induced exhaustion, I spilled an entire glass of Gatorade on it and then drenched it in water to try and flush it away. I took the back off, drained it, and then pointed a fan at it for 24 hours. The laptop was working fine the next day.
|
A lot of Linux developers use ThinkPads which means Linux is well supported on them and the drivers just work. The laptops are easy to pop open and service. Many parts are user-replaceable. My laptop charges off a small USB-C phone charger that I carry around. In a state of sickness-induced exhaustion, I spilled an entire glass of Gatorade on it and then drenched it in water to try and flush it away. I took the back off, drained it, and then pointed a fan at it for 24 hours. The laptop was working fine the next day.
|
||||||
|
|
||||||
|
### RovyVon E5 Flashlight
|
||||||
|
|
||||||
|
Small USB-C flashlight that's very bright and has a side light that's useful for camping.
|
||||||
|
|
||||||
|
<https://www.amazon.ca/dp/B0B465DBW3>
|
||||||
|
|
||||||
|
### Leatherman Skeletool CX
|
||||||
|
|
||||||
|
My preferred multitool that I everyday carry. It's light, thin, and the cutters cross over instead of pinching.
|
||||||
|
|
||||||
|
<https://www.amazon.ca/LEATHERMAN-Lightweight-Minimalist-Multi-Tool-Nightshade/dp/B0CX2HRTBP>
|
||||||
@@ -3,26 +3,31 @@ Date: 2024-02-21
|
|||||||
Category: Notes
|
Category: Notes
|
||||||
Summary: About the hydroponics garden in my basement.
|
Summary: About the hydroponics garden in my basement.
|
||||||
|
|
||||||
I had a "Secret Garden" in a storage room in the basement of my house. It was a [[Hydroponics | hydroponics]] system growing leafy greens and herbs. You can see an hourly photo of it below:
|
I have a "Secret Garden" in my basement. It's a [[Hydroponics | hydroponics]] system growing leafy greens and herbs. You can see an hourly photo of it below:
|
||||||
|
|
||||||
<a href="/media/garden_hi.jpg"></a>
|
<a href="/media/garden_hi.jpg"></a>
|
||||||
|
|
||||||
Click the above photo for a larger version.
|
Click the above photo for a larger version.
|
||||||
|
|
||||||
There's usually kale, spinach, cilantro, parsley, and green onion growing. Sometimes dill and basil. It's currently not active this fall of 2024 because I'm planning travel.
|
There's usually kale, spinach, cilantro, parsley, and green onion growing. Sometimes dill and basil.
|
||||||
|
|
||||||
## Nutrient Film Technique
|
## Kratky Method
|
||||||
|
|
||||||
The garden uses nutrient film technique (NFT) to continuously deliver a shallow stream of nutrient solution to the plants growing in 2" ABS pipe. A submersible fountain water pump sends nutrients up 1/4" irrigation hose to each of the four pipes. The nutrients flow down the slope passed all the roots and return to the ~40 L reservoir. An air stone oxygenates the water. Two computer case fans ensure adequate [[Airflow|airflow]].
|
This garden uses the [Kratky method](https://en.wikipedia.org/wiki/Kratky_method) of hydroponics. The plants sit in a 30 L reservoir of nutrient solution. As the plants grow, they drink the water level down until a layer of air forms. This air is what oxygenates the plants after the oxygen dissolved in the water is depleted. The resulting system is completely passive, requiring no air pumps, water pumps, recirculation, filters, or heat and no risk of leaks or flooding.
|
||||||
|
|
||||||
|
## Previous Garden
|
||||||
|
|
||||||
|
My last garden used nutrient film technique (NFT) to continuously deliver a shallow stream of nutrient solution to the plants growing in 2" ABS pipe. A submersible fountain water pump sends nutrients up 1/4" irrigation hose to each of the four pipes. The nutrients flow down the slope passed all the roots and return to the ~40 L reservoir. An air stone oxygenates the water. Two computer case fans ensure adequate [[Airflow|airflow]].
|
||||||
|
|
||||||
![[nft1.png | four black pipes supported horizontally by a wooden frame. eight small seedlings are growing out of the pipe under four grow lights total. two fans on the right. a pink towel is down below, covering the reservoir.]]
|
![[nft1.png | four black pipes supported horizontally by a wooden frame. eight small seedlings are growing out of the pipe under four grow lights total. two fans on the right. a pink towel is down below, covering the reservoir.]]
|
||||||
|
|
||||||
The nutrients are kept in a reservoir underneath the towel, which helps block light and limit [[Algae Growth|algae growth]]:
|
The nutrients are kept in a reservoir underneath the towel, which helps block light and limit [[Algae Growth|algae growth]]:
|
||||||
|
|
||||||
![[nft2.jpg | the reservoir with the towel removed. the yellow lid is cut in half and has a tube poking out of it with a distribution cap for the 1/4 inch irrigation lines to connect to. black return pipes are on the left, pointing down into the reservoir through a mesh bag acting as a filter.]]
|
![[nft2.jpg | the reservoir with the towel removed. the yellow lid is cut in half and has a tube poking out of it with a distribution cap for the 1/4 inch irrigation lines to connect to. black return pipes are on the left, pointing down into the reservoir through a mesh bag acting as a filter.]]
|
||||||
## Previous Garden
|
|
||||||
|
|
||||||
My previous system [[Helios Alpha]] could grow up to six plants. It's designed around a single 102 L plastic tote. It holds enough water to harvest lettuce once before refilling. After the initial setup, the system can be ignored for weeks.
|
## First Garden
|
||||||
|
|
||||||
|
My first system [[Helios Alpha]] could grow up to six plants. It's designed around a single 102 L plastic tote. It holds enough water to harvest lettuce a few times before refilling. After the initial setup, the system can be ignored for weeks.
|
||||||
|
|
||||||
![[heliosalpha1.jpg | the hydroponics system from two angles. a black tub with yellow lid, covered in tin foil with six holes for plants. grow lights above suspended by metal shelving.]]
|
![[heliosalpha1.jpg | the hydroponics system from two angles. a black tub with yellow lid, covered in tin foil with six holes for plants. grow lights above suspended by metal shelving.]]
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ These graphs are live and generated every 10 minutes, assuming the script works:
|
|||||||
|
|
||||||
Black: power (W), green: energy (kWh)
|
Black: power (W), green: energy (kWh)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Black: PM10 (ug/m³), red: PM2.5 (ug/m³), blue: CO₂ (ppm), green: VOC
|
Black: PM10 (ug/m³), red: PM2.5 (ug/m³), blue: CO₂ (ppm), green: VOC
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ Black: total (MJ), green: delta (MJ)
|
|||||||
|
|
||||||
Black: total (L), green: delta (L)
|
Black: total (L), green: delta (L)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Black: light (lx)
|
Black: light (lx)
|
||||||
|
|
||||||
@@ -61,6 +61,6 @@ The data gets collected by a central Python script that process and stores it in
|
|||||||
|
|
||||||
## InfluxDB Regrets
|
## InfluxDB Regrets
|
||||||
|
|
||||||
My biggest regret was using InfluxDB. It's a stupid database that I wouldn't recommend it to anyone. I ran into timezone issues with `group by time()`. It assumes the column data type is an integer if your sensor happens to send it a whole number at first and it won't let you change it. Their docs are a confusing mess. They dropped the SQL-like InfluxQL syntax for querying with a pipeline-like syntax called Flux in version 2.0. Debian's repos seem to be staying with version 1.x though. You can only delete data by time ranges, not values. It also logs every single thing to `/var/log/syslog` and there's no easy way to disable it (completely). They shut down InfluxDB cloud in Belgium and [didn't warn customers](https://community.influxdata.com/t/getting-weird-results-from-gcp-europe-west1/30615/7) before deleting all their data.
|
My biggest regret was using InfluxDB. It's a stupid database that I wouldn't recommend to anyone. I ran into timezone issues with `group by time()`. It assumes the column data type is an integer if your sensor happens to send it a whole number at first and it won't let you change it. Their docs are a confusing mess. They dropped the SQL-like InfluxQL syntax for querying with a pipeline-like syntax called Flux in version 2.0. Debian's repos seem to be staying with version 1.x though. You can only delete data by time ranges, not values. It also logs every single thing to `/var/log/syslog` and there's no easy way to disable it (completely). They shut down InfluxDB cloud in Belgium and [didn't warn customers](https://community.influxdata.com/t/getting-weird-results-from-gcp-europe-west1/30615/7) before deleting all their data. They changed schemas again in version 3.x and made useful features closed-source. Read the comments [here](https://news.t0.vc/TUTF/c#doctoboggan1750217574).
|
||||||
|
|
||||||
Just stick to SQLite or Postgres.
|
Just stick to SQLite or Postgres.
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ Category: Creations
|
|||||||
Summary: About my time volunteering with the University of Calgary Solar Car Team, where I designed a maximum power point tracker.
|
Summary: About my time volunteering with the University of Calgary Solar Car Team, where I designed a maximum power point tracker.
|
||||||
Image: solar2.jpg
|
Image: solar2.jpg
|
||||||
Tags: feed
|
Tags: feed
|
||||||
|
Guid: 7259d46cfc0440acba56d43d1749314b
|
||||||
|
|
||||||
I joined the University of Calgary Solar Car Team in my first semester for a chance to learn things, gain practical experience, and meet people that share my interests. The car was the top Canadian team in a 3000 km race from Darwin to Adelaide, Australia in 2011. We met up at a shop on campus every Saturday morning to work on the new Generation IV of the solar car.
|
I joined the University of Calgary Solar Car Team in my first semester for a chance to learn things, gain practical experience, and meet people that share my interests. The car was the top Canadian team in a 3000 km race from Darwin to Adelaide, Australia in 2011. We met up at a shop on campus every Saturday morning to work on the new Generation IV of the solar car.
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ Title: Spaceport
|
|||||||
Date: 2022-05-16
|
Date: 2022-05-16
|
||||||
Category: Projects
|
Category: Projects
|
||||||
Summary: Member portal for Calgary Protospace. It tracks dues, courses, training, access cards, and more.
|
Summary: Member portal for Calgary Protospace. It tracks dues, courses, training, access cards, and more.
|
||||||
Image: spaceport1.jpg
|
Image: spaceport1.png
|
||||||
Tags: feed
|
Tags: feed
|
||||||
|
Guid: 3a7b5606eefc45cbad523d7548a864a5
|
||||||
|
|
||||||
[Spaceport](https://my.protospace.ca) is the member portal that I wrote for [[Protospace]], a makerspace that I frequent in Calgary. It is by far my largest project and the one I've spent the most time on. It has a database of all our members and tracks their transactions like dues and training fees. It allows members to sign up for classes and our instructors to teach courses. It also manages the access cards that members use to get into the building.
|
[Spaceport](https://my.protospace.ca) is the member portal that I wrote for [[Protospace]], a makerspace that I frequent in Calgary. It is by far my largest project and the one I've spent the most time on. It has a database of all our members and tracks their transactions like dues and training fees. It allows members to sign up for classes and our instructors to teach courses. It also manages the access cards that members use to get into the building.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
Title: Three Drawer Cabinet
|
||||||
|
Date: 2025-03-18
|
||||||
|
Category: Creations
|
||||||
|
Summary: A three-drawer cabinet with a laser etched design.
|
||||||
|
Image: cabinet1.jpg
|
||||||
|
xTags: feed
|
||||||
|
|
||||||
|
I built a three drawer cabinet for storing embroidery machine supplies at my local makerspace, [[Protospace]]. It was built to fit under the machine, inside its metal stand.
|
||||||
|
|
||||||
|
![[cabinet1.jpg]]
|
||||||
|
|
||||||
|
The side features a laser etched flower design I found online. The cabinet is assembled using pocket screws because I wanted to learn how to use them.
|
||||||
|
|
||||||
|
Here's what it looks like installed:
|
||||||
|
|
||||||
|
![[cabinet2.jpg]]
|
||||||
@@ -4,6 +4,7 @@ Category: Creations
|
|||||||
Summary: A coffee table made out of wooden wine creates.
|
Summary: A coffee table made out of wooden wine creates.
|
||||||
Image: wine3.jpg
|
Image: wine3.jpg
|
||||||
Tags: feed
|
Tags: feed
|
||||||
|
Guid: f0e36fec844d422eb7e0d626788c7b0a
|
||||||
|
|
||||||
My close friend Odai saw a simple coffee table design online that was built out of four wooden wine crates. They are quite cheap and available at any hardware store. We each wanted to make one so went and bought eight crates and some plywood to use as a base.
|
My close friend Odai saw a simple coffee table design online that was built out of four wooden wine crates. They are quite cheap and available at any hardware store. We each wanted to make one so went and bought eight crates and some plywood to use as a base.
|
||||||
|
|
||||||
|
|||||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" height="300" stroke="#000" stroke-linecap="square" stroke-width="28" width="300" xmlns="http://www.w3.org/2000/svg"><path d="M201.962 210a60 60 0 10-103.924-60l-50 86.603M98.038 210a60 60 0 10103.924-60l-50-86.603M150 120a60 60 0 100 120h100"/></svg>
|
||||||
|
After Width: | Height: | Size: 268 B |
|
After Width: | Height: | Size: 269 KiB |
|
After Width: | Height: | Size: 219 KiB |
|
After Width: | Height: | Size: 198 KiB |
|
After Width: | Height: | Size: 236 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 316 KiB |
|
After Width: | Height: | Size: 560 KiB |
|
After Width: | Height: | Size: 339 KiB |
|
After Width: | Height: | Size: 641 KiB |
|
After Width: | Height: | Size: 244 KiB |
|
After Width: | Height: | Size: 446 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 133 KiB |
|
After Width: | Height: | Size: 162 KiB |
|
After Width: | Height: | Size: 154 KiB |
|
After Width: | Height: | Size: 169 KiB |
|
After Width: | Height: | Size: 182 KiB |
|
After Width: | Height: | Size: 201 KiB |
|
Before Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 140 KiB |
|
Before Width: | Height: | Size: 216 KiB After Width: | Height: | Size: 209 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 22 KiB |
@@ -2,9 +2,10 @@ Title: t-zero Services
|
|||||||
Date: 2022-05-27
|
Date: 2022-05-27
|
||||||
Category: Writing
|
Category: Writing
|
||||||
Summary: A list of minimal microservices on my t0.vc domain.
|
Summary: A list of minimal microservices on my t0.vc domain.
|
||||||
|
Image: t0services1.svg
|
||||||
Wide: true
|
Wide: true
|
||||||
Short: 6
|
|
||||||
Tags: feed
|
Tags: feed
|
||||||
|
Guid: 0440222b638144d5a6c376e1aaf43755
|
||||||
|
|
||||||
The t-zero Services are a collection of minimalist microservices that I host on my t0.vc domain. The letter "t" meaning me, and "0" meaning small. They're all meant to do exactly one thing reliably and stay online for as long as I can host them.
|
The t-zero Services are a collection of minimalist microservices that I host on my t0.vc domain. The letter "t" meaning me, and "0" meaning small. They're all meant to do exactly one thing reliably and stay online for as long as I can host them.
|
||||||
|
|
||||||
@@ -13,6 +14,8 @@ The smallest t-zero is the main domain itself at [t0.vc](https://t0.vc) and it s
|
|||||||
|
|
||||||
The rest of the t-zero services are hosted on its subdomains.
|
The rest of the t-zero services are hosted on its subdomains.
|
||||||
|
|
||||||
|
![[t0services1.svg]]
|
||||||
|
|
||||||
## t0txt
|
## t0txt
|
||||||
The second t-zero I wrote was [[t0txt]], a pastebin that is compatible with the command line and `curl`. This allows me to very easily pipe text data into it and immediately get a URL that I can share. I copied the idea from [sprunge.us](http://sprunge.us/) which kept going down because he'd forget to pay his Google Cloud bill.
|
The second t-zero I wrote was [[t0txt]], a pastebin that is compatible with the command line and `curl`. This allows me to very easily pipe text data into it and immediately get a URL that I can share. I copied the idea from [sprunge.us](http://sprunge.us/) which kept going down because he'd forget to pay his Google Cloud bill.
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,16 @@ Title: t0txt
|
|||||||
Date: 2022-05-15
|
Date: 2022-05-15
|
||||||
Category: Projects
|
Category: Projects
|
||||||
Summary: Minimal command line pastebin. Allows you to upload text notes from a bash pipe or web browser.
|
Summary: Minimal command line pastebin. Allows you to upload text notes from a bash pipe or web browser.
|
||||||
|
Image: t0txt1.png
|
||||||
Tags: feed
|
Tags: feed
|
||||||
|
Guid: a5fd74baa289491e9c4e931cdfcd2170
|
||||||
|
|
||||||
[t0txt](https://txt.t0.vc) is a minimalist pastebin. You can upload text notes from the command line by using a bash alias or by submitting text through the web form.
|
[t0txt](https://txt.t0.vc) is a minimalist pastebin. You can upload text notes from the command line by using a bash alias or by submitting text through the web form.
|
||||||
|
|
||||||
You can find the [source code](https://github.com/tannercollin/t0txt) on Github.
|
You can find the [source code](https://github.com/tannercollin/t0txt) on Github.
|
||||||
|
|
||||||
|
![[t0txt1.png]]
|
||||||
|
|
||||||
The pastes you upload take the form of [txt.t0.vc/IMLV](https://txt.t0.vc/IMLV), where they are identified by four unique capital letters. This makes it easy to memorize the URL while moving it between devices.
|
The pastes you upload take the form of [txt.t0.vc/IMLV](https://txt.t0.vc/IMLV), where they are identified by four unique capital letters. This makes it easy to memorize the URL while moving it between devices.
|
||||||
|
|
||||||
I wrote t0txt in July 2019 and plan to continue hosting it indefinitely. I use it quite often for sysadmin and automation work, so I'm committed to keeping it alive. Here's an example use case:
|
I wrote t0txt in July 2019 and plan to continue hosting it indefinitely. I use it quite often for sysadmin and automation work, so I'm committed to keeping it alive. Here's an example use case:
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import pprint
|
||||||
|
|
||||||
|
from pelican import signals
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
THUMBNAIL_MAX_SIZE = 448
|
||||||
|
|
||||||
|
def generator_finalized(generator):
|
||||||
|
"""
|
||||||
|
Generates thumbnails for images specified in article metadata.
|
||||||
|
"""
|
||||||
|
output_path = generator.settings['OUTPUT_PATH']
|
||||||
|
content_path = generator.settings['PATH']
|
||||||
|
media_path = os.path.join(content_path, 'media')
|
||||||
|
thumb_dir = os.path.join(output_path, 'media', 'thumbnails')
|
||||||
|
|
||||||
|
if not os.path.exists(thumb_dir):
|
||||||
|
try:
|
||||||
|
os.makedirs(thumb_dir)
|
||||||
|
log.info(f"Created thumbnail directory: {thumb_dir}")
|
||||||
|
except OSError as e:
|
||||||
|
log.error(f"Could not create thumbnail directory {thumb_dir}: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
for article in generator.articles:
|
||||||
|
if hasattr(article, 'image'):
|
||||||
|
image_path_rel_to_content = article.image
|
||||||
|
# image_path_rel_to_content is often like 'media/imagename.jpg'
|
||||||
|
# or just 'imagename.jpg' if it's directly in 'content/media/'
|
||||||
|
# and STATIC_PATHS includes 'media'.
|
||||||
|
# We assume article.image is a path relative to the 'content' folder.
|
||||||
|
|
||||||
|
source_image_full_path = os.path.join(media_path, image_path_rel_to_content)
|
||||||
|
|
||||||
|
if not os.path.exists(source_image_full_path):
|
||||||
|
log.warning(f"Source image not found for article '{article.slug}': {source_image_full_path}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
image_filename = os.path.basename(image_path_rel_to_content)
|
||||||
|
thumb_path = os.path.join(thumb_dir, image_filename)
|
||||||
|
|
||||||
|
_, ext = os.path.splitext(image_filename)
|
||||||
|
ext_lower = ext.lower()
|
||||||
|
if ext_lower not in ['.jpg', '.jpeg', '.png']:
|
||||||
|
log.info(f"Skipping non-JPG/PNG image for article '{article.slug}': {image_filename}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
log.debug(f"Processing image: {source_image_full_path}")
|
||||||
|
img = Image.open(source_image_full_path)
|
||||||
|
|
||||||
|
# Preserve original format, handle potential conversion issues for some modes
|
||||||
|
original_format = img.format
|
||||||
|
if img.mode == 'P' and 'transparency' in img.info: # Palette mode with transparency
|
||||||
|
img = img.convert('RGBA')
|
||||||
|
elif img.mode not in ('RGB', 'RGBA', 'L'): # L is grayscale
|
||||||
|
log.info(f"Converting image {image_filename} from mode {img.mode} to RGB for thumbnailing.")
|
||||||
|
img = img.convert('RGB')
|
||||||
|
|
||||||
|
img.thumbnail((THUMBNAIL_MAX_SIZE, THUMBNAIL_MAX_SIZE))
|
||||||
|
|
||||||
|
save_kwargs = {}
|
||||||
|
if original_format:
|
||||||
|
save_kwargs['format'] = original_format
|
||||||
|
if original_format == 'JPEG':
|
||||||
|
save_kwargs['quality'] = 95 # Adjust quality for JPEGs
|
||||||
|
save_kwargs['optimize'] = True
|
||||||
|
elif original_format == 'PNG':
|
||||||
|
save_kwargs['optimize'] = True
|
||||||
|
|
||||||
|
img.save(thumb_path, **save_kwargs)
|
||||||
|
log.info(f"Generated thumbnail for '{article.slug}': {thumb_path}")
|
||||||
|
|
||||||
|
# Optionally, add thumbnail URL to article metadata if needed by templates
|
||||||
|
# This depends on how SITEURL and paths are structured.
|
||||||
|
# For now, just creating the file.
|
||||||
|
# article.thumbnail_url = f"{generator.settings.get('SITEURL', '')}/media/thumbs/{image_filename}"
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
log.error(f"Image file not found: {source_image_full_path}")
|
||||||
|
except IOError as e:
|
||||||
|
log.error(f"Could not open or process image {source_image_full_path}: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"An unexpected error occurred while processing {source_image_full_path}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def register():
|
||||||
|
signals.article_generator_finalized.connect(generator_finalized)
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
inkscape:groupmode="layer"
|
inkscape:groupmode="layer"
|
||||||
id="layer1">
|
id="layer1">
|
||||||
<rect
|
<rect
|
||||||
style="fill:#000000;stroke-width:0.264999"
|
style="fill:#1a1a1a;stroke-width:0.264999"
|
||||||
id="rect909"
|
id="rect909"
|
||||||
width="212.19583"
|
width="212.19583"
|
||||||
height="121.17917"
|
height="121.17917"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
@@ -47,7 +47,7 @@
|
|||||||
inkscape:groupmode="layer"
|
inkscape:groupmode="layer"
|
||||||
id="layer1">
|
id="layer1">
|
||||||
<rect
|
<rect
|
||||||
style="fill:#000000;stroke-width:0.264999"
|
style="fill:#1a1a1a;stroke-width:0.264999"
|
||||||
id="rect909"
|
id="rect909"
|
||||||
width="212.19583"
|
width="212.19583"
|
||||||
height="121.17917"
|
height="121.17917"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
@@ -47,7 +47,7 @@
|
|||||||
inkscape:groupmode="layer"
|
inkscape:groupmode="layer"
|
||||||
id="layer1">
|
id="layer1">
|
||||||
<rect
|
<rect
|
||||||
style="fill:#000000;stroke-width:0.264999"
|
style="fill:#1a1a1a;stroke-width:0.264999"
|
||||||
id="rect909"
|
id="rect909"
|
||||||
width="212.19583"
|
width="212.19583"
|
||||||
height="121.17917"
|
height="121.17917"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,31 @@
|
|||||||
|
<mxfile host="app.diagrams.net" modified="2024-01-27T01:41:03.200Z" agent="Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0" etag="OL6IpkjM6-81aN4SoHv_" version="23.0.2" type="device">
|
||||||
|
<diagram name="Page-1" id="5-Wfztd7QZj7HGqn7hWo">
|
||||||
|
<mxGraphModel dx="928" dy="525" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
|
||||||
|
<root>
|
||||||
|
<mxCell id="0" />
|
||||||
|
<mxCell id="1" parent="0" />
|
||||||
|
<mxCell id="xaIvDc9CACh0lx6NfuLJ-1" value="" style="shape=image;imageAspect=0;aspect=fixed;verticalLabelPosition=bottom;verticalAlign=top;image=https://upload.wikimedia.org/wikipedia/commons/4/4a/Debian-OpenLogo.svg;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="210" y="210" width="109" height="144" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="xaIvDc9CACh0lx6NfuLJ-2" value="" style="shape=image;imageAspect=0;aspect=fixed;verticalLabelPosition=bottom;verticalAlign=top;image=https://upload.wikimedia.org/wikipedia/commons/9/9e/UbuntuCoF.svg;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="580" y="212" width="140" height="140" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="xaIvDc9CACh0lx6NfuLJ-3" value="" style="shape=image;imageAspect=0;aspect=fixed;verticalLabelPosition=bottom;verticalAlign=top;image=https://upload.wikimedia.org/wikipedia/commons/3/35/Tux.svg;" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="380" y="212" width="134" height="134" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="xaIvDc9CACh0lx6NfuLJ-4" value="" style="endArrow=none;html=1;rounded=0;strokeWidth=5;strokeColor=#808080;" edge="1" parent="1">
|
||||||
|
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||||
|
<mxPoint x="320" y="307" as="sourcePoint" />
|
||||||
|
<mxPoint x="370" y="257" as="targetPoint" />
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="xaIvDc9CACh0lx6NfuLJ-5" value="" style="endArrow=none;html=1;rounded=0;strokeWidth=5;strokeColor=#808080;" edge="1" parent="1">
|
||||||
|
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||||
|
<mxPoint x="520" y="304" as="sourcePoint" />
|
||||||
|
<mxPoint x="570" y="254" as="targetPoint" />
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
</root>
|
||||||
|
</mxGraphModel>
|
||||||
|
</diagram>
|
||||||
|
</mxfile>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<mxfile host="app.diagrams.net" modified="2024-01-27T01:25:37.000Z" agent="Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0" etag="WvQ24rCeBP-gvlG-6t5J" version="23.0.2" type="device">
|
||||||
|
<diagram name="Page-1" id="dd472eb7-4b8b-5cd9-a60b-b15522922e76">
|
||||||
|
<mxGraphModel dx="1114" dy="630" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1100" pageHeight="850" background="none" math="0" shadow="0">
|
||||||
|
<root>
|
||||||
|
<mxCell id="0" />
|
||||||
|
<mxCell id="1" parent="0" />
|
||||||
|
<mxCell id="374e34682ed331ee-1" value="<font style="font-size: 36px;">t0.vc</font>" style="ellipse;whiteSpace=wrap;html=1;rounded=0;shadow=0;dashed=0;comic=0;fontFamily=Verdana;fontSize=22;fontColor=default;fillColor=#f5f5f5;strokeColor=#666666;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="290" y="180" width="450" height="180" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="45236fa5f4b8e91a-1" value="t0txt" style="ellipse;whiteSpace=wrap;html=1;rounded=0;shadow=0;dashed=0;comic=0;fontFamily=Verdana;fontSize=22;fontColor=default;fillColor=#f5f5f5;strokeColor=#666666;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="430" y="120" width="160" height="100" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="45236fa5f4b8e91a-2" value="<div>t0url</div>" style="ellipse;whiteSpace=wrap;html=1;rounded=0;shadow=0;dashed=0;comic=0;fontFamily=Verdana;fontSize=22;fontColor=default;fillColor=#f5f5f5;strokeColor=#666666;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="240" y="285" width="160" height="100" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="45236fa5f4b8e91a-3" value="t0reg" style="ellipse;whiteSpace=wrap;html=1;rounded=0;shadow=0;dashed=0;comic=0;fontFamily=Verdana;fontSize=22;fontColor=default;fillColor=#f5f5f5;strokeColor=#666666;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="620" y="280" width="160" height="110" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="45236fa5f4b8e91a-6" value="t0pic" style="ellipse;whiteSpace=wrap;html=1;rounded=0;shadow=0;dashed=0;comic=0;fontFamily=Verdana;fontSize=22;fontColor=default;fillColor=#f5f5f5;strokeColor=#666666;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="620" y="150" width="160" height="100" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="45236fa5f4b8e91a-7" value="t0sig" style="ellipse;whiteSpace=wrap;html=1;rounded=0;shadow=0;dashed=0;comic=0;fontFamily=Verdana;fontSize=22;fontColor=default;fillColor=#f5f5f5;strokeColor=#666666;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="435" y="310" width="160" height="100" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="45236fa5f4b8e91a-8" value="t0dns" style="ellipse;whiteSpace=wrap;html=1;rounded=0;shadow=0;dashed=0;comic=0;fontFamily=Verdana;fontSize=22;fontColor=default;fillColor=#f5f5f5;strokeColor=#666666;" parent="1" vertex="1">
|
||||||
|
<mxGeometry x="240" y="160" width="160" height="100" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
</root>
|
||||||
|
</mxGraphModel>
|
||||||
|
</diagram>
|
||||||
|
</mxfile>
|
||||||
@@ -2,6 +2,11 @@
|
|||||||
# -*- coding: utf-8 -*- #
|
# -*- coding: utf-8 -*- #
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import sys
|
||||||
|
sys.path.append('.')
|
||||||
|
|
||||||
|
import generate_thumbnails
|
||||||
|
|
||||||
PATH = 'content'
|
PATH = 'content'
|
||||||
|
|
||||||
TIMEZONE = 'Canada/Mountain'
|
TIMEZONE = 'Canada/Mountain'
|
||||||
@@ -33,6 +38,7 @@ MARKDOWN = {
|
|||||||
PLUGINS = [
|
PLUGINS = [
|
||||||
'obsidian',
|
'obsidian',
|
||||||
'linkclass',
|
'linkclass',
|
||||||
|
'generate_thumbnails',
|
||||||
]
|
]
|
||||||
|
|
||||||
STATIC_PATHS = ['media', 'extra']
|
STATIC_PATHS = ['media', 'extra']
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
# -*- coding: utf-8 -*- #
|
# -*- coding: utf-8 -*- #
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import sys
|
||||||
|
sys.path.append('.')
|
||||||
|
|
||||||
|
import generate_thumbnails
|
||||||
|
|
||||||
PATH = 'content'
|
PATH = 'content'
|
||||||
|
|
||||||
TIMEZONE = 'Canada/Mountain'
|
TIMEZONE = 'Canada/Mountain'
|
||||||
@@ -33,6 +38,7 @@ MARKDOWN = {
|
|||||||
PLUGINS = [
|
PLUGINS = [
|
||||||
'obsidian',
|
'obsidian',
|
||||||
'linkclass',
|
'linkclass',
|
||||||
|
'generate_thumbnails',
|
||||||
]
|
]
|
||||||
|
|
||||||
STATIC_PATHS = ['media', 'extra']
|
STATIC_PATHS = ['media', 'extra']
|
||||||
|
|||||||
@@ -3,6 +3,11 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
import sys
|
||||||
|
sys.path.append('.')
|
||||||
|
|
||||||
|
import swap_guids
|
||||||
|
|
||||||
AUTHOR = 'Tanner'
|
AUTHOR = 'Tanner'
|
||||||
SITENAME = 'Tanner\'s Site (t0.vc)'
|
SITENAME = 'Tanner\'s Site (t0.vc)'
|
||||||
SITEURL = 'https://t0.vc'
|
SITEURL = 'https://t0.vc'
|
||||||
@@ -13,7 +18,7 @@ TIMEZONE = 'Canada/Mountain'
|
|||||||
|
|
||||||
DEFAULT_LANG = 'en'
|
DEFAULT_LANG = 'en'
|
||||||
|
|
||||||
# Feed generation is usually not desired when developing
|
#FEED_MAX_ITEMS = 15
|
||||||
FEED_ALL_ATOM = None
|
FEED_ALL_ATOM = None
|
||||||
CATEGORY_FEED_ATOM = None
|
CATEGORY_FEED_ATOM = None
|
||||||
TRANSLATION_FEED_ATOM = None
|
TRANSLATION_FEED_ATOM = None
|
||||||
@@ -21,8 +26,8 @@ AUTHOR_FEED_ATOM = None
|
|||||||
AUTHOR_FEED_RSS = None
|
AUTHOR_FEED_RSS = None
|
||||||
#TAG_FEED_ATOM = 'feeds/{slug}/atom.xml'
|
#TAG_FEED_ATOM = 'feeds/{slug}/atom.xml'
|
||||||
#TAG_FEED_RSS = 'feeds/{slug}/rss.xml'
|
#TAG_FEED_RSS = 'feeds/{slug}/rss.xml'
|
||||||
TAG_FEED_ATOM = 'test-atom.xml'
|
TAG_FEED_ATOM = 'atom.xml'
|
||||||
TAG_FEED_RSS = 'test-rss.xml'
|
TAG_FEED_RSS = 'rss.xml'
|
||||||
RSS_FEED_SUMMARY_ONLY = False # include full content
|
RSS_FEED_SUMMARY_ONLY = False # include full content
|
||||||
|
|
||||||
DEFAULT_PAGINATION = False
|
DEFAULT_PAGINATION = False
|
||||||
@@ -43,6 +48,7 @@ MARKDOWN = {
|
|||||||
PLUGINS = [
|
PLUGINS = [
|
||||||
'obsidian',
|
'obsidian',
|
||||||
'linkclass',
|
'linkclass',
|
||||||
|
'swap_guids',
|
||||||
]
|
]
|
||||||
|
|
||||||
STATIC_PATHS = ['media', 'extra', 'text']
|
STATIC_PATHS = ['media', 'extra', 'text']
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import logging
|
||||||
|
import pprint
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from pelican import signals
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def modify_feed(context, feed):
|
||||||
|
articles = {}
|
||||||
|
|
||||||
|
for article in context['articles']:
|
||||||
|
if article.title in articles:
|
||||||
|
raise Exception(f"Duplicate article title found: {article.title}")
|
||||||
|
articles[article.title] = article
|
||||||
|
|
||||||
|
for item in feed.items:
|
||||||
|
item_title = item['title']
|
||||||
|
article = articles.get(item_title)
|
||||||
|
if not article:
|
||||||
|
raise Exception(f"Article not found for title: {item_title}")
|
||||||
|
|
||||||
|
if not hasattr(article, 'guid') or not article.guid:
|
||||||
|
log.info(f"Article '{article.title}' ({article.source_path}) is missing a guid. Generating and embedding one.")
|
||||||
|
new_guid_str = uuid.uuid4().hex
|
||||||
|
guid_text_to_embed = f"Guid: {new_guid_str}"
|
||||||
|
|
||||||
|
source_path = article.source_path
|
||||||
|
|
||||||
|
# Ensure article object has the _content attribute
|
||||||
|
if not hasattr(article, '_content'):
|
||||||
|
log.error(f"Article '{article.title}' does not have '_content' attribute. Cannot embed Guid into source file.")
|
||||||
|
raise Exception(f"Cannot find raw content for article '{article.title}' to embed Guid.")
|
||||||
|
|
||||||
|
# Read the original file content.
|
||||||
|
# Python's open() in text mode uses universal newlines by default, converting \r\n and \r to \n.
|
||||||
|
# Pelican's MarkdownReader also provides article._content with \n newlines.
|
||||||
|
try:
|
||||||
|
with open(source_path, 'r', encoding='utf-8') as f:
|
||||||
|
current_body_content = f.read()
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Failed to read original content from '{source_path}': {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
# Split this body content to find its first paragraph.
|
||||||
|
# Paragraphs in Markdown are separated by one or more blank lines (\n\n).
|
||||||
|
body_parts = current_body_content.split('\n\n', 1)
|
||||||
|
first_paragraph_of_body = body_parts[0]
|
||||||
|
rest_of_body_content = body_parts[1] if len(body_parts) > 1 else ""
|
||||||
|
|
||||||
|
# Append the Guid text to the end of the first paragraph of the body.
|
||||||
|
# .rstrip() removes any trailing whitespace/newlines from the paragraph itself before appending.
|
||||||
|
modified_first_paragraph_of_body = first_paragraph_of_body.rstrip() + '\n' + guid_text_to_embed
|
||||||
|
|
||||||
|
# Construct the full new file content by combining the original metadata part and the new body.
|
||||||
|
# This preserves the original metadata block verbatim (including comments, formatting, and original newline characters if any within it,
|
||||||
|
# as metadata_part_from_file is a direct slice from original_file_content_universal_newlines which has \n newlines).
|
||||||
|
full_new_content = modified_first_paragraph_of_body + '\n\n' + rest_of_body_content
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(source_path, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(full_new_content)
|
||||||
|
log.info(f"Successfully wrote updated content with embedded Guid to '{source_path}'.")
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Failed to write updated content to '{source_path}': {e}")
|
||||||
|
raise # Re-raise the exception to halt processing if file write fails
|
||||||
|
|
||||||
|
# Set article.guid for the current Pelican run, so it's used for the feed item
|
||||||
|
article.guid = new_guid_str
|
||||||
|
log.debug(f"Set in-memory article.guid = '{new_guid_str}' for '{article.title}'.")
|
||||||
|
|
||||||
|
item['unique_id'] = article.guid
|
||||||
|
|
||||||
|
|
||||||
|
def register():
|
||||||
|
signals.feed_generated.connect(modify_feed)
|
||||||
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
{% block meta %}
|
{% block meta %}
|
||||||
<title>{{ article.title|striptags }} | t0.vc</title>
|
<title>{{ article.title|striptags }} | t0.vc</title>
|
||||||
<link rel="canonical" href="https://tannercollin.com/{{ article.slug }}/" />
|
<link rel="canonical" href="https://tanner.vc/{{ article.slug }}/" />
|
||||||
{% if article.date %}
|
{% if article.date %}
|
||||||
<meta name="date" content="{{article.date}}" />
|
<meta name="date" content="{{article.date}}" />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -7,6 +7,12 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
*, *::before, *::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
p, h1, h2, h3, h4, h5, h6 {
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
a.external {
|
a.external {
|
||||||
background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12'%3E%3Cpath fill='%23fff' stroke='%23000' d='M1.5 4.518h5.982V10.5H1.5z'/%3E%3Cpath fill='%23000' d='M5.765 1H11v5.39L9.427 7.937l-1.31-1.31L5.393 9.35l-2.69-2.688 2.81-2.808L4.2 2.544z'/%3E%3Cpath fill='%23fff' d='m9.995 2.004.022 4.885L8.2 5.07 5.32 7.95 4.09 6.723l2.882-2.88-1.85-1.852z'/%3E%3C/svg%3E%0A");
|
background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12'%3E%3Cpath fill='%23fff' stroke='%23000' d='M1.5 4.518h5.982V10.5H1.5z'/%3E%3Cpath fill='%23000' d='M5.765 1H11v5.39L9.427 7.937l-1.31-1.31L5.393 9.35l-2.69-2.688 2.81-2.808L4.2 2.544z'/%3E%3Cpath fill='%23fff' d='m9.995 2.004.022 4.885L8.2 5.07 5.32 7.95 4.09 6.723l2.882-2.88-1.85-1.852z'/%3E%3C/svg%3E%0A");
|
||||||
background-position: center right;
|
background-position: center right;
|
||||||
@@ -44,10 +50,6 @@
|
|||||||
height: auto;
|
height: auto;
|
||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
}
|
}
|
||||||
.floated {
|
|
||||||
float: left;
|
|
||||||
margin-right: 1rem;
|
|
||||||
}
|
|
||||||
@media screen and (min-width:63rem) {
|
@media screen and (min-width:63rem) {
|
||||||
.content .aside {
|
.content .aside {
|
||||||
display: inline;
|
display: inline;
|
||||||
@@ -66,7 +68,8 @@
|
|||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<p><a href="/">Home | t0.vc</a></p>
|
<p>← <a href="/">Home (t0.vc)</a></p>
|
||||||
|
<p><a href="/rss.xml">RSS Feed</a> | <a href="/atom.xml">Atom Feed</a></p>
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,16 +9,16 @@ CV
|
|||||||
|
|
||||||
Firmware Engineer - Cabana Blockchain, '18-
|
Firmware Engineer - Cabana Blockchain, '18-
|
||||||
Lead Hardware Engineer - Critical Control, '16-'18
|
Lead Hardware Engineer - Critical Control, '16-'18
|
||||||
Electrical Engineer - Opener Aero, '16-'16
|
Electrical Engineer - Pivotal Aero, '16-'16
|
||||||
BSc Electrical Engineering - University of Calgary
|
BSc Electrical Engineering - University of Calgary
|
||||||
|
|
||||||
Blog
|
Creations
|
||||||
|
|
||||||
<a href=6>Makerspace Tours
|
<a href=5>Camper Trailer
|
||||||
<a href=p>Bypassing ISP Blocked Ports
|
<a href=3>Bash Register
|
||||||
<a href=j>Japan Photography
|
<a href=0>Protovac Terminal
|
||||||
<a href=h>Hydroponics
|
<a href=7>Fake Dog for Home Security
|
||||||
<a href=x>[more]</a>
|
<a href=z>[more]</a>
|
||||||
|
|
||||||
Projects
|
Projects
|
||||||
|
|
||||||
@@ -28,10 +28,10 @@ Projects
|
|||||||
<a href=n>Notica
|
<a href=n>Notica
|
||||||
<a href=y>[more]</a>
|
<a href=y>[more]</a>
|
||||||
|
|
||||||
Creations
|
Blog
|
||||||
|
|
||||||
<a href=7>Fake Dog for Home Security
|
<a href=h>Hydroponics
|
||||||
<a href=5>Garage Door Opener Hack
|
<a href=j>Japan Photography
|
||||||
<a href=3>Theatre Acoustic Panels
|
<a href=p>Bypassing ISP Blocked Ports
|
||||||
<a href=0>Wine Crate Coffee Table
|
<a href=6>Backup Strategy
|
||||||
<a href=z>[more]
|
<a href=x>[more]
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="802"
|
||||||
|
height="458"
|
||||||
|
viewBox="0 0 212.19583 121.17917"
|
||||||
|
version="1.1"
|
||||||
|
id="svg5"
|
||||||
|
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
|
||||||
|
sodipodi:docname="logo-path.svg"
|
||||||
|
inkscape:export-filename="logo-export.svg"
|
||||||
|
inkscape:export-xdpi="96"
|
||||||
|
inkscape:export-ydpi="96"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview7"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="0.52964612"
|
||||||
|
inkscape:cx="769.38164"
|
||||||
|
inkscape:cy="262.43938"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1011"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="32"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="text911" />
|
||||||
|
<defs
|
||||||
|
id="defs2">
|
||||||
|
<rect
|
||||||
|
x="27.741714"
|
||||||
|
y="556.36462"
|
||||||
|
width="592.50952"
|
||||||
|
height="240.91045"
|
||||||
|
id="rect913" />
|
||||||
|
</defs>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<rect
|
||||||
|
style="fill:#1a1a1a;stroke-width:0.264999"
|
||||||
|
id="rect909"
|
||||||
|
width="212.19583"
|
||||||
|
height="121.17917"
|
||||||
|
x="0"
|
||||||
|
y="0" />
|
||||||
|
<g
|
||||||
|
aria-label="tanner█"
|
||||||
|
transform="matrix(0.54302647,0,0,0.54302647,-10.487989,-292.51606)"
|
||||||
|
id="text911"
|
||||||
|
style="font-size:76.3777px;line-height:1.25;white-space:pre;shape-inside:url(#rect913);display:inline;fill:#ffffff">
|
||||||
|
<path
|
||||||
|
d="m 50.64058,570.31312 v 11.85942 h 15.588808 v 5.33302 H 50.64058 v 22.67463 q 0,4.62443 1.752809,6.45182 1.752808,1.8274 6.116183,1.8274 h 7.719816 v 5.48219 h -8.391105 q -7.719816,0 -10.889789,-3.09539 -3.169973,-3.09538 -3.169973,-10.66602 V 587.50556 H 32.627675 v -5.33302 h 11.150846 v -11.85942 z"
|
||||||
|
style="font-family:monospace;-inkscape-font-specification:monospace"
|
||||||
|
id="path287" />
|
||||||
|
<path
|
||||||
|
d="m 99.905688,602.94519 h -2.274922 q -6.004301,0 -9.062393,2.12575 -3.020798,2.08845 -3.020798,6.26535 0,3.76668 2.274922,5.85513 2.274922,2.08845 6.302652,2.08845 5.668658,0 8.913221,-3.91585 3.24456,-3.95314 3.28185,-10.88978 v -1.52905 z m 13.313882,-2.83433 v 23.83074 h -6.89935 v -6.19077 q -2.20033,3.72938 -5.55677,5.51948 -3.319153,1.75281 -8.092759,1.75281 -6.37724,0 -10.181207,-3.58021 -3.803968,-3.61749 -3.803968,-9.65909 0,-6.97394 4.661725,-10.59144 4.699019,-3.6175 13.761412,-3.6175 h 9.211567 v -1.08152 q -0.0373,-4.99737 -2.53598,-7.23499 -2.49868,-2.27493 -7.98087,-2.27493 -3.505617,0 -7.085822,1.00694 -3.580204,1.00693 -6.97394,2.94621 v -6.86206 q 3.803967,-1.45446 7.272291,-2.16304 3.505617,-0.74588 6.787471,-0.74588 5.18384,0 8.83863,1.52905 3.69209,1.52904 5.96701,4.58713 1.41716,1.86469 2.01386,4.62444 0.5967,2.72244 0.5967,8.20463 z"
|
||||||
|
style="font-family:monospace;-inkscape-font-specification:monospace"
|
||||||
|
id="path289" />
|
||||||
|
<path
|
||||||
|
d="m 158.90448,598.0597 v 25.8819 h -6.89936 v -25.8819 q 0,-5.63136 -1.97657,-8.27922 -1.97657,-2.64786 -6.19077,-2.64786 -4.8109,0 -7.42146,3.43103 -2.57328,3.39373 -2.57328,9.77097 v 23.60698 h -6.86205 v -41.76906 h 6.86205 v 6.26536 q 1.8274,-3.5802 4.96008,-5.4076 3.13268,-1.86469 7.42147,-1.86469 6.37724,0 9.50991,4.2142 3.16998,4.17691 3.16998,12.67989 z"
|
||||||
|
style="font-family:monospace;-inkscape-font-specification:monospace"
|
||||||
|
id="path291" />
|
||||||
|
<path
|
||||||
|
d="m 204.88774,598.0597 v 25.8819 h -6.89935 v -25.8819 q 0,-5.63136 -1.97658,-8.27922 -1.97657,-2.64786 -6.19077,-2.64786 -4.8109,0 -7.42146,3.43103 -2.57327,3.39373 -2.57327,9.77097 v 23.60698 h -6.86206 v -41.76906 h 6.86206 v 6.26536 q 1.82739,-3.5802 4.96007,-5.4076 3.13268,-1.86469 7.42147,-1.86469 6.37724,0 9.50992,4.2142 3.16997,4.17691 3.16997,12.67989 z"
|
||||||
|
style="font-family:monospace;-inkscape-font-specification:monospace"
|
||||||
|
id="path293" />
|
||||||
|
<path
|
||||||
|
d="m 253.14591,601.34156 v 3.35644 h -29.72316 v 0.22376 q 0,6.82477 3.54291,10.55415 3.5802,3.72938 10.06932,3.72938 3.28186,0 6.86206,-1.04423 3.58021,-1.04423 7.64523,-3.16997 v 6.82476 q -3.91585,1.60364 -7.57064,2.38681 -3.6175,0.82046 -7.01123,0.82046 -9.73368,0 -15.21587,-5.81783 -5.48219,-5.85513 -5.48219,-16.11093 0,-9.99473 5.37031,-15.96174 5.3703,-5.96701 14.32081,-5.96701 7.98088,0 12.56802,5.4076 4.62443,5.4076 4.62443,14.76835 z m -6.86206,-2.01387 q -0.14918,-6.04159 -2.87163,-9.17427 -2.68515,-3.16998 -7.75711,-3.16998 -4.96007,0 -8.16734,3.28186 -3.20726,3.28185 -3.80397,9.09968 z"
|
||||||
|
style="font-family:monospace;-inkscape-font-specification:monospace"
|
||||||
|
id="path295" />
|
||||||
|
<path
|
||||||
|
d="m 300.73279,590.78741 q -2.20034,-1.71551 -4.47526,-2.49868 -2.27492,-0.78317 -4.99737,-0.78317 -6.41453,0 -9.80827,4.02773 -3.39373,4.02773 -3.39373,11.63566 v 20.77265 h -6.89936 v -41.76906 h 6.89936 v 8.16735 q 1.71551,-4.43797 5.25842,-6.78748 3.58021,-2.3868 8.46569,-2.3868 2.53598,0 4.73632,0.634 2.20033,0.63399 4.2142,1.97657 z"
|
||||||
|
style="font-family:monospace;-inkscape-font-specification:monospace"
|
||||||
|
id="path297" />
|
||||||
|
<path
|
||||||
|
d="m 302.89585,643.03602 v -90.73581 h 47.475 v 90.73581 z"
|
||||||
|
style="font-family:monospace;-inkscape-font-specification:monospace"
|
||||||
|
id="path299" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 25 KiB |
@@ -21,30 +21,28 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
<a href="/" aria-label="Return home">
|
<a href="/" aria-label="Return home">
|
||||||
<svg version="1.1" viewBox="0 0 212.2 121.18" xmlns="http://www.w3.org/2000/svg">
|
<img width="160" height="91" src="/theme/logo-path-export.svg" />
|
||||||
<rect width="212.2" height="121.18" stroke-width=".265" style="fill:#1a1a1a" />
|
|
||||||
<g transform="matrix(.54303 0 0 .54303 -10.488 -292.52)" fill="#fff" style="shape-inside:url(#rect913);white-space:pre">
|
|
||||||
<path d="m50.641 570.31v11.859h15.589v5.333h-15.589v22.675q0 4.6244 1.7528 6.4518 1.7528 1.8274 6.1162 1.8274h7.7198v5.4822h-8.3911q-7.7198 0-10.89-3.0954-3.17-3.0954-3.17-10.666v-22.675h-11.151v-5.333h11.151v-11.859z"/>
|
|
||||||
<path d="m99.906 602.95h-2.2749q-6.0043 0-9.0624 2.1258-3.0208 2.0884-3.0208 6.2654 0 3.7667 2.2749 5.8551t6.3027 2.0884q5.6687 0 8.9132-3.9158 3.2446-3.9531 3.2818-10.89v-1.529zm13.314-2.8343v23.831h-6.8994v-6.1908q-2.2003 3.7294-5.5568 5.5195-3.3192 1.7528-8.0928 1.7528-6.3772 0-10.181-3.5802-3.804-3.6175-3.804-9.6591 0-6.9739 4.6617-10.591 4.699-3.6175 13.761-3.6175h9.2116v-1.0815q-0.0373-4.9974-2.536-7.235-2.4987-2.2749-7.9809-2.2749-3.5056 0-7.0858 1.0069-3.5802 1.0069-6.9739 2.9462v-6.8621q3.804-1.4545 7.2723-2.163 3.5056-0.74588 6.7875-0.74588 5.1838 0 8.8386 1.529 3.6921 1.529 5.967 4.5871 1.4172 1.8647 2.0139 4.6244 0.5967 2.7224 0.5967 8.2046z"/>
|
|
||||||
<path d="m158.9 598.06v25.882h-6.8994v-25.882q0-5.6314-1.9766-8.2792t-6.1908-2.6479q-4.8109 0-7.4215 3.431-2.5733 3.3937-2.5733 9.771v23.607h-6.862v-41.769h6.862v6.2654q1.8274-3.5802 4.9601-5.4076 3.1327-1.8647 7.4215-1.8647 6.3772 0 9.5099 4.2142 3.17 4.1769 3.17 12.68z"/>
|
|
||||||
<path d="m204.89 598.06v25.882h-6.8994v-25.882q0-5.6314-1.9766-8.2792-1.9766-2.6479-6.1908-2.6479-4.8109 0-7.4215 3.431-2.5733 3.3937-2.5733 9.771v23.607h-6.8621v-41.769h6.8621v6.2654q1.8274-3.5802 4.9601-5.4076 3.1327-1.8647 7.4215-1.8647 6.3772 0 9.5099 4.2142 3.17 4.1769 3.17 12.68z"/>
|
|
||||||
<path d="m253.15 601.34v3.3564h-29.723v0.22376q0 6.8248 3.5429 10.554 3.5802 3.7294 10.069 3.7294 3.2819 0 6.8621-1.0442 3.5802-1.0442 7.6452-3.17v6.8248q-3.9158 1.6036-7.5706 2.3868-3.6175 0.82046-7.0112 0.82046-9.7337 0-15.216-5.8178-5.4822-5.8551-5.4822-16.111 0-9.9947 5.3703-15.962 5.3703-5.967 14.321-5.967 7.9809 0 12.568 5.4076 4.6244 5.4076 4.6244 14.768zm-6.8621-2.0139q-0.14918-6.0416-2.8716-9.1743-2.6852-3.17-7.7571-3.17-4.9601 0-8.1673 3.2819-3.2073 3.2818-3.804 9.0997z"/>
|
|
||||||
<path d="m300.73 590.79q-2.2003-1.7155-4.4753-2.4987t-4.9974-0.78317q-6.4145 0-9.8083 4.0277-3.3937 4.0277-3.3937 11.636v20.773h-6.8994v-41.769h6.8994v8.1674q1.7155-4.438 5.2584-6.7875 3.5802-2.3868 8.4657-2.3868 2.536 0 4.7363 0.634 2.2003 0.63399 4.2142 1.9766z"/>
|
|
||||||
<path d="m302.9 643.04v-90.736h47.475v90.736z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
<p> </p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="footer">
|
<p class="footer">
|
||||||
<span class="wname">Webring:</span> <a href="https://nice42q.de/" class="wprev">< Previous</a> | <a href="https://webring.t0.vc" class="windex">Index</a> | <a href="https://udia.ca" class="wnext">Next ></a>
|
<span class="wname">Webring:</span> <a href="https://www.rottenwheel.com" class="wprev">< Previous</a> | <a href="https://webring.t0.vc" class="windex">Index</a> | <a href="https://udia.ca" class="wnext">Next ></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<p class="xxiivv">
|
||||||
|
<a href="https://webring.xxiivv.com/#tanner" target="_blank" rel="noopener">
|
||||||
|
<img src="/extra/xxiivv-icon.black.svg" alt="XXIIVV webring"/>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
<p class="footer">
|
<p class="footer">
|
||||||
© 2012–2022 Tanner
|
© 2012–2025 Tanner
|
||||||
</p>
|
</p>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -12,6 +12,10 @@
|
|||||||
Hi, I'm <a href="/about">Tanner</a>! I like <a href="/secret-garden">growing plants</a>, home automation, <a href="/sensors">sensors</a>, bots, Python, Debian, climbing, and makerspaces.
|
Hi, I'm <a href="/about">Tanner</a>! I like <a href="/secret-garden">growing plants</a>, home automation, <a href="/sensors">sensors</a>, bots, Python, Debian, climbing, and makerspaces.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Please sign my <a href=g>Guestbook</a>!
|
||||||
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Email: <a href="mailto:site@tanner.vc">site@tanner.vc</a> <br />
|
Email: <a href="mailto:site@tanner.vc">site@tanner.vc</a> <br />
|
||||||
</p>
|
</p>
|
||||||
@@ -49,10 +53,17 @@
|
|||||||
|
|
||||||
<p>Sometimes I create art or interactive tech.</p>
|
<p>Sometimes I create art or interactive tech.</p>
|
||||||
|
|
||||||
|
<div class="creations">
|
||||||
{% for article in articles_page.object_list if article.category.name == 'Creations' %}
|
{% for article in articles_page.object_list if article.category.name == 'Creations' %}
|
||||||
<h3><a href="{{ article.url }}">{{ article.title }}</a></h3>
|
<div class="creation">
|
||||||
|
<a href="{{ article.url }}">
|
||||||
|
<img src="media/thumbnails/{{ article.image }}" alt="{{ article.summary|striptags }}" />
|
||||||
|
{{ article.title }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<h2>Writing</h2>
|
<h2>Writing</h2>
|
||||||
|
|
||||||
@@ -68,7 +79,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li>Firmware Engineer at <a class="external" href="https://cabanablockchain.com" target="_blank" rel="noreferrer noopener">Cabana Blockchain</a>, 2018–</li>
|
<li>Firmware Engineer at <a class="external" href="https://cabanablockchain.com" target="_blank" rel="noreferrer noopener">Cabana Blockchain</a>, 2018–</li>
|
||||||
<li>Lead Hardware Engineer at <a class="external" href="https://criticalcontrol.com/" target="_blank" rel="noreferrer noopener">Critical Control</a>, 2016–2018</li>
|
<li>Lead Hardware Engineer at <a class="external" href="https://criticalcontrol.com/" target="_blank" rel="noreferrer noopener">Critical Control</a>, 2016–2018</li>
|
||||||
<li>Electrical Engineer at <a class="external" href="https://www.opener.aero/" target="_blank" rel="noreferrer noopener">Opener Aero</a>, 2016–2016</li>
|
<li>Electrical Engineer at <a class="external" href="https://pivotal.aero/" target="_blank" rel="noreferrer noopener">Pivotal Aero</a>, 2016–2016</li>
|
||||||
<li>Electrical Engineer Intern at <a class="external" href="https://www.pason.com/" target="_blank" rel="noreferrer noopener">Pason Systems</a>, 2014–2015</li>
|
<li>Electrical Engineer Intern at <a class="external" href="https://www.pason.com/" target="_blank" rel="noreferrer noopener">Pason Systems</a>, 2014–2015</li>
|
||||||
<li>BSc. Electrical Engineering from University of Calgary</li>
|
<li>BSc. Electrical Engineering from University of Calgary</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
|
*, *::before, *::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
p, h1, h2, h3, h4, h5, h6 {
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
font-family: serif;
|
font-family: serif;
|
||||||
}
|
}
|
||||||
@@ -27,6 +35,40 @@
|
|||||||
font: 1.1rem/1.5 serif;
|
font: 1.1rem/1.5 serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.index .creations {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index .creations .creation {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.index .creations .creation a {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.creation > a {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
|
||||||
|
margin: 1rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
font: 1.1rem/1.5 serif;
|
||||||
|
|
||||||
|
width: 14rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.creations .creation img {
|
||||||
|
max-width: 14rem;
|
||||||
|
max-height: 14rem;
|
||||||
|
width: auto;
|
||||||
|
|
||||||
|
margin: 0 auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.source {
|
.source {
|
||||||
font: 0.9rem/1.5 serif;
|
font: 0.9rem/1.5 serif;
|
||||||
}
|
}
|
||||||
@@ -62,6 +104,19 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.xxiivv {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xxiivv img {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xxiivv a {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
.toc {
|
.toc {
|
||||||
float: right;
|
float: right;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
@@ -100,19 +155,15 @@
|
|||||||
font: 1.1rem/1.5 serif;
|
font: 1.1rem/1.5 serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content img:not(.floated) {
|
.content img {
|
||||||
width: 100%;
|
max-width: min(100%, 36rem);
|
||||||
max-width: 36rem;
|
max-height: 36rem;
|
||||||
height: auto;
|
height: auto;
|
||||||
|
width: auto;
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content .floated {
|
|
||||||
float: left;
|
|
||||||
margin-right: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content.index {
|
.content.index {
|
||||||
margin-top: 3rem;
|
margin-top: 3rem;
|
||||||
}
|
}
|
||||||
@@ -220,4 +271,8 @@
|
|||||||
.nofilter img {
|
.nofilter img {
|
||||||
filter: initial !important;
|
filter: initial !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.xxiivv img {
|
||||||
|
filter: invert(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||