`` elements. The resulting
+HTML for a code block then looks like this:
+
+.. code:: html
+
+
+
+
+ The code!
+
+
+
+
+You can find the (rather short) source of the ``rst2html`` wrapper `below <#rst2html-wrapper>`_.
+
+The CSS
+-------
+
+This modified HTML structure of the code listing gets accompanied by some CSS to make it flow nicely. Here is a listing
+of the complete CSS controlling the listing. The only bit that isn't included here is the actual syntax styling rules
+for the pygments tokens.
+
+.. code:: css
+
+ /*****************************************************/
+ /* Code block formatting / syntax highlighting rules */
+ /*****************************************************/
+
+ .code {
+ font-family: "Fira Code";
+ font-size: 13px;
+ text-align: left; /* Override default content "justify" alignment */
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ overflow-x: auto;
+ display: grid;
+ align-items: start;
+ grid-template-columns: min-content 1fr;
+ }
+
+ .code > .line {
+ padding-left: calc(2em + 5px);
+ text-indent: -2em;
+ padding-top: 2px;
+ min-width: 15em;
+ }
+
+ /* Make individual syntax tokens wrap anywhere */
+ .code > .line > span {
+ overflow-wrap: anywhere;
+ white-space: pre-wrap;
+ }
+
+ /* We render line numbers in CSS! */
+ .code > .lineno {
+ counter-increment: lineno;
+ word-break: keep-all;
+ margin: 0;
+ padding-left: 15px;
+ padding-right: 5px;
+ overflow: clip;
+ position: relative;
+ text-align: right;
+ color: var(--c-text-muted);
+ border-right: 1px solid var(--c-fg-highlight);
+ align-self: stretch;
+ }
+
+ /* We also handle line continuation markers in CSS. */
+ .code > .lineno::after {
+ position: absolute;
+ right: 5px;
+ content: "\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳";
+ white-space: pre;
+ color: var(--c-text-muted);
+ }
+
+ /* Insert the actual line number */
+ .code > .lineno::before {
+ content: counter(lineno);
+ }
+
+ .code::before {
+ counter-reset: lineno;
+ }
+
+ .code .hll {}
+ /* Following are about 50 lines that define the styling of each kind of pygments syntax highlight token. These lines
+ all look like the following: */
+ .code .c { color: var(--c-text); font-weight: 400 } /* Comment */
+
+This CSS does a few things:
+
+ 1. It renders the ```` code listing element using a two-column CSS ``display: grid`` layout. The left column is
+ used for the line numbers, and the right column is used for the code lines.
+ 2. It numbers the lines using a `CSS Counter`_. CSS counters are meant for things like numbering headings and such, but
+ they are a perfect fit for our purpose.
+ 3. It inserts the counter value as the line number into the ```` element's ``::before``
+ pseudo-element. A side effect of using the ``::before`` pseudo-element is that without doing anything extra, the
+ line numbers will remain outside of the normal text selection so they will neither be highlighted when selecting
+ listing content, nor will they be copied when copy/pasting the listing content.
+ 4. It inserts a string of ``"\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳"`` into the line number span's
+ ``::after`` pseudo-element. This string evaluates to a sequence of unicode arrows separated by line breaks, and
+ starting with an empty line. The ``::after`` pseudo-element is positioned using ``position: absolute``, and the
+ parent ```` has ``position: relative`` set. This way, the arrow pseudo-element gets placed on
+ top of the lineno span without affecting the layout at all. By setting ``overflow: clip`` on the parent ````, the arrow pseudo-element gets cut off vertically wherever the parent lineno element naturally
+ ends.
+
+The line number span is inserted into the parent ```` element's CSS grid using ``align-self: stretch``, which
+causes it to vertically stretch to fill the available space. Since the line number span only contains the line number,
+its minimum height is a single line. As a result, it will stretch higher only when the corresponding code line in the
+right grid column stretches vertically because of line wrapping. When that happens, part of the arrow pseudo-element
+starts showing through from behind the ``overflow: clip`` of the line number span, and one arrow gets rendered for each
+wrapped listing line.
+
+When the page is too narrow, we don't want the code listing's lines to wrapp into a column of single characters. To
+prevent that, we simply set a ``min-width`` on the ```` in the right column, and set ``overflow-x:
+auto`` on the listing ````. This results in a horizontal scroll bar appearing whenever the listing gets too narrow.
+
+You can try out the line wrapping by resizing this page!
+
+rst2html wrapper
+----------------
+
+Here is the python ``rst2html`` wrapper that monkey-patches code rendering. I made hugo invoke this while building the
+page by simply overriding the ``PATH`` environment variable.
+
+.. code:: python
+
+ #!/usr/bin/env python3
+ # Based on https://gist.github.com/mastbaum/2655700 for the basic plugin scaffolding
+
+ import sys
+ import re
+
+ import docutils.core
+ from docutils.transforms import Transform
+ from docutils.nodes import TextElement, Inline, Text
+ from docutils.parsers.rst import Directive, directives
+ from docutils.writers.html4css1 import Writer, HTMLTranslator
+
+
+ class UnfuckedHTMLTranslator(HTMLTranslator):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.in_literal_block = False
+
+ def visit_literal_block(self, node):
+ # Insert an empty "lineno" span before each line. We insert the line numbers using pure CSS in a ::before
+ # pseudo-element. This has the added advantage that the line numbers don't get included in text selection.
+ # These line number spans are also used to show line continuation markers when a line is wrapped.
+ self.in_literal_block = True
+ self.body.append(self.starttag(node, 'pre', CLASS='literal-block'))
+ self.body.append('')
+
+ def depart_literal_block(self, node):
+ self.in_literal_block = False
+ self.body.append('\n\n')
+
+ def visit_Text(self, node):
+ if self.in_literal_block:
+ for match in re.finditer('([^\n]*)(\n|$)', node.astext()):
+ text, end = match.groups()
+
+ if text:
+ super().visit_Text(Text(text))
+
+ if end == '\n':
+ if isinstance(node.parent, Inline):
+ self.depart_inline(node.parent)
+ self.body.append(f'\n')
+ if isinstance(node.parent, Inline):
+ self.visit_inline(node.parent)
+
+ else:
+ super().visit_Text(node)
+
+
+ html_writer = Writer()
+ html_writer.translator_class = UnfuckedHTMLTranslator
+ docutils.core.publish_cmdline(writer=html_writer)
+
+.. _Hugo: https://gohugo.io/
+.. _RestructuredText: https://www.sphinx-doc.org/en/master/usage/restructuredtext/index.html
+.. _Pygments: https://pygments.org/
+.. _`monkey-patched`: https://en.wikipedia.org/wiki/Monkey_patch
+.. _`CSS Counter`: https://developer.mozilla.org/en-US/docs/Web/CSS/counter
diff --git a/content/blog/epa-sgd-crypto/index.rst b/content/blog/epa-sgd-crypto/index.rst
new file mode 100644
index 0000000..ccbf648
--- /dev/null
+++ b/content/blog/epa-sgd-crypto/index.rst
@@ -0,0 +1,100 @@
+---
+title: "75 Million Lives, Two Keys"
+date: 2025-01-05T23:42:00+01:00
+draft: true
+---
+
+2025 has begun. In this new year, with its new national healthcare record system, the country of Germany will start one
+of the largest rollouts of a cryptographic system in history. While the system has received scrutiny as well as
+resulting harsh criticism from a number of parties ranging from NGOs to everyday civilians, the system has received
+surprisingly little attention from the academic applied cryptography crowd. Additionally, previous criticism of
+the system has largely revolved around organizational issues. While valid, we belive that some cryptographic issues at
+the core of the system have escaped attention unitl now. In particular, at the core of the system is a key escrow system
+that contains several questionable design choices and that in its overall design seems out of place in 2025.
+
+The aim of the system is to serve as a shared storage for all healthcare records of a person. In the system, a person's
+entire patient file with all documentation on the treatment process including test results, images and other raw data
+will be stored in something vaguely resembling cloud storage such that all healthcare providers that the person visits
+can access the entire file. This centralized, synchronized storage eliminates the need for transferring this data
+between hospitals and doctors offices by fax, mail or physical media as it was common practice until now. After a
+development and testing phase lasting approximately five years, the German government decided to roll out the system to
+everybody insured under Germany's mandatory national health insurance scheme, totalling approximately 75 million people,
+on January 15th 2025.
+
+In this article, we will give an overview of the system's cryptographic design before highlighting a few odd
+design choices that could amount to a viable attack vector to the powerful adversaies
+
+## Context and involved parties
+
+Germany has a national, mandatory health insurance system. The system is open to any permanent resident of the country
+irrespective of citizenship. The system is mandatory in that while residents can choose between a number of both
+publically owned as well as private healthcare providers, it is not possible to opt out of the system. The public health
+insurance providers cover approximately 90% of German residents. These providers are organized in an umbrella
+organization named "GKV Spitzenverband". The resposibility of this umbrella organization largely revolves around
+negotiating prices with pharmaceutical companies and with healthcare providers as a publically sanctioned cartel, but
+also includes the specification and operation of shared IT infrastructure for billing and data exchange between
+healthcare providers.
+
+While GKV Spitzenverband is the party that ultimately holds responsibility for the regulatory administration of national
+healthcare IT infrastructure, it has delegated large parts of both the technical specification of this infrastructure as
+well as its day-to-day operation to Gematik GmbH, a state-owned limited liability corporation created specifically for
+the purpose of developing and implementing national healthcare IT standards. The electronic healthcare record system we
+describe in this article was standardized and implemented by Gematik GmbH under the direction of GKV Spitzenverband.
+
+Healthcare providers in Germany need to be registered with GKV Spitzenverband to serve members of public health
+insurance providers. Since these public providers constitute approximately 90% market share, the vast majority of
+healthcare providers are registered this way.
+
+Before the new national health record system, a number of healthcare IT processes have already been standardized and
+implemented by the parties above. In particular, every insured person already owns a cryptographic smartcard that acts
+as their proof of identity when accessing healthcare services. On the other side of such transactions, healthcare
+providers are likewise identified by cryptographic smartcards. Until now, these cards were used to facilitate billing of
+services from healthcare providers to insurers and to transfer prescriptions from prescribing doctors to pharmacies.
+
+A central role in this existing infrastructure is assumed by VPN gateways that link healthcare providers to
+the centrally-run backend infrastructure. Gematik GmbH calls these devices "Konnektor". They are specially-built
+hardware devices that contain multiple smart cards to authenticate the VPN connection towards the backend, and besides
+acting as a standard VPN gateway for client applications in the healthcare provider's network to tunnnel their backend
+requests through, the Konnektors also perform cryptographic operations in some of Gematik GmbH's protocols, such as
+authenticating certain requests using signatures.
+
+## Design principles
+
+The new health record system was built on top of the existing infrastructure described above. In particular, access to
+health records is managed through keys stored in the patient's and the healthcare provider's existing smartcards, and
+all backend communication is tunneled through the existing VPN. Access to the files is mediated through the healthcare
+provider's existing patient management software. While in early drafts of the system, access to healthcare records
+through the patient's smartcard was gated behind a PIN, the impracticality of making the entire patient populace
+remember PINs led the implementors to scrap this provision, meaning that the patient's smartcard is all a healthcare
+provider needs to access the patient's record.
+
+A critical cornerstone in the system's design is that the system's designers decided that a lost smartcard should not
+lead to any data loss. As a consequence of this decision, while some of the record's access keys are kept on the
+patient smartcard, in contravention to conventional smartcard designs the same keys are kept accessible in a centralized
+key escrow system named "Schlüsselgenerierungsdienst" and abbreviated as SGD. Furthermore, these keys are not generated
+on the smartcard either -- instead, the key escrow system generates these access keys, one copy of which is then
+transmitted and stored inside the smartcard.
+
+The system supports re-issuing a smartcard to gain access to a healthcare record. Since the record's privacy pivots on
+this process, the system incorporates some organziational countermeasures that aim to make it hard to gain access to a
+re-issued copy of a patient smartcard without the patient's help or otherwise multiple colluding parties.
+
+## Cryptographic design
+
+
+
+## The implied adversary model
+
+While Gematik GmbH publishes detailed specifications of the systems they standardize, these specifications and some
+associated implementation guidelines are about the extent of public information. Software implementations are being kept
+secret, and while standardization results are available, a large fraction of design rationale is discussed behind closed
+doors. From an academic perspective, the most glaring omission in Gematik GmbH's public documents is any definition of a
+threat model or an adversary model. As a result of this, we will deduce an adversary model below by contextualizing the
+published standards in the national healthcare setting. We will base our further analysis of the system on this
+adversary model.
+
+
+
+## Previous reviews and audits of the system
+
+[0] https://www.destatis.de/DE/Themen/Arbeit/Arbeitsmarkt/Qualitaet-Arbeit/Dimension-2/krankenversicherungsschutz.html
diff --git a/content/blog/hsm-basics/index.rst b/content/blog/hsm-basics/index.rst
new file mode 100644
index 0000000..74b7275
--- /dev/null
+++ b/content/blog/hsm-basics/index.rst
@@ -0,0 +1,214 @@
+---
+title: "Hardware Security Module Basics"
+date: 2019-05-17T15:29:20+08:00
+summary: >
+ I gave a short introduction into Hardware Security Modules at our university workgroup, including an overview on
+ interesting research directions.
+---
+
+On May 17 2019 I gave a short presentation on the fundamentals of hardware security modules at the weekly seminar of
+Prof. Mori's security research working group at Waseda University. The motivation for this was that outside of low-level
+hardware security people and people working in the financial industry HSMs are not thought about that often. In
+particular most network or systems security people would not consider them an option. Also it could turn out to be
+really interesting to think about what could be done with an HSM in conjunction with modern cryptography (instead of
+just plain old RSA-OAEP and AES-CBC).
+
+`Click here to download a PDF with the slides for this talk. `__
+
+Ideas for research in HSMs
+==========================
+
+Preparing for this talk brought me back to some research ideas I've been working on for a while now. Since I'm not sure
+I'll find the time to properly research this topic, I thought it would be great to write down some rought outlines first
+for future reference.
+
+The Problem with current HSM tech
+---------------------------------
+
+Currently, HSMs are only used in certain specific niche applications such as certificate authority key management and
+financial transaction data handling. One key reason for this is that HSMs currently don't provide the affordances that
+would be needed for them to be adopted more widely by the cryptographic and security engineering community. As far as I
+can tell, the two core missing affordances are:
+
+1. To be more widely adopted, HSMs must become less expensive. Currently, they go for several tens of thousands of Euro,
+ which puts them outside most budgets.
+2. To be more widely adopted, HSMs must provide the standardized programming interfaces familiar to cryptographic
+ developers. Currently, every HSM vendor has their own custom cryptographic API and a developer will have to train on
+ one specific vendor's tooling. Furthermore, any documentation of these internals is kept secret behind NDAs. This
+ constitutes a high barrier to entry, decreasing adoption in particular with young developers accustomed to
+ open-source ecosystems.
+
+Attacking cost of implementation
+--------------------------------
+
+The first issue can be addressed by simply creating a viable low-cost alternative. There is no fundamental technical
+reason for the high cost of HSMs. This cost is instead due to manufacturers trying to recoup their expenses for R&D as
+well as certification from the small volumes HSMs are sold in.
+
+Compared to system integration and certification the pure R&D cost of HSM defense mechanisms themselves is not too high
+in an academic context it should be feasible to develop a sort of HSM blueprint that can then be cheaply produced by
+anyone in need. Since the application areas outlined here are far from the core business areas of the clients of
+established HSM vendors this would most likely not be a realistic threat to any established vendor's business and a
+co-existence of both should not pose any problems in the short term.
+
+Benefits of an academic HSM standard
+------------------------------------
+
+Tackling the high cost of current HSM hardware with an open-source HSM blueprint would yield
+several academic advantages beyond cost reduction.
+
+1. An open-source blueprint could serve as an academic reference design to evaluate and compare other HSM designs
+ against. For instance this would not only allow quantifying the effectiveness of academic security measures but also
+ allow an evaluation of commercial HSMs.
+2. An open-source blueprint could stimulate academic research in this academically very quiet albeit commercially
+ important area. This research would ultimately benefit everyone employing HSMs by raising security standards in the
+ field. Since HSMs are never solely relied upon for overal system security both defensive and offensive security
+ research would yield these benefits.
+3. An open-source blueprint would encourage new people to get into the field and both apply HSMs to practical problems
+ as well as improve HSMs themselves. Currently, this is highly discouraged due to the strictly proprietary nature of
+ all available systems.
+4. Finally, developing an open-source HSM blueprint might yield new findings in adjacent academic areas due to the
+ hightly multi-disciplinary nature of security research in general and HSM design in particular.
+
+Scope of an academic HSM standard
+---------------------------------
+
+An academic HSM blueprint would need to be flexible so that researchers can adapt it to their particular problem. A
+modular architecture would lend itself to this flexibility. Fundamentally, there would be three components to this
+architecture. First, a **base** containing infrastructure such as the surveillance microcontroller, power supplies,
+power supply filtering and hardware DPA countermeasures, and possibly a standardized mechanical and electrical
+interface.
+
+Next to the base, a system integrator would put their *payload*. The nature of this payload is intentionally kept
+unspecified, and it might be anything from a cryptographic microcontroller to a small embedded system such as a
+raspberry pi single board computer. Keeping the *payload* open like this achieves two benefits: It gives the HSM
+blueprint's user *their* familiar tooling and the hardware *they* need, allowing fast adoption. Someone well-versed in
+e.g. Javascript could literally implement their cryptography in Javascript, run it on an off-the-shelf raspberry pi and
+just apply the HSM blueprint around it. In addition, keeping the *payload* open reduces the scope of what needs to be
+implemented. Building a general SDK on top of something like a bare ARM SoC such as a TI OMAP or a Freescale/NXP IMX
+would be a considerable additional burden, on top of the actual HSM design. Keeping the *payload* open allows research
+to concentrate on the actual point, the HSM design.
+
+The final and most important component would be a set of *security measures* that can be combined with the base to
+form the final HSM. Each of these *security measures* would entail a detailed specification of its design, manufacture
+and security properties. These *security measures* could be simple things like tamper switches or potting, but could
+also be complex things like security meshes.
+
+Given these three components -- *base*, *payload* and *security measures* as detailed specifications any engineer should
+be able to design and manufacture a HSM customized to their needs. Unifying these three components within the HSM
+blueprint would be a set of reference designs. Each reference design would implement a particular parametrization of the
+three architectural components with a physical hole cut out where the payload would go.. These reference designs would
+for one serve to guide any implementer on the customization and integration of their own derivation from the blueprint.
+In addition it would serve as an extremely simple, low-cost point of entry into the ecosystem. A curious researcher
+could simply replicate the reference design and put their existing payload inside. Practically this might mean e.g. a
+researcher having PCBs produced according to the design files for a reference design for a mesh-based HSM, producing
+their own mesh, physically glueing a raspberry pi SBC into the middle of it, and potting the resulting system. Given the
+ease of prototype PCB fabrication today this would realistically allow evaluation of HSM technologies on a budget that
+is orders of magnitude less than the cost of current HSMs.
+
+Research ideas for tamper detection mechanisms
+==============================================
+
+The core component of an HSM blueprint would be a suite of tamper detection mechanisms. Following are a few ideas on how
+to improve on the current state of the art of membrane tamper switches plus temperature sensors plus PCB and printed
+security meshes plus potting.
+
+DIY or small lab mesh production
+--------------------------------
+**Analog sensing** meshes are a proven technology where instead of just monitoring for continuity and shorts, analog
+parameters of the mesh traces such as inductance and mutual capacitance are monitored. In 2019, `Immler et al. published
+a paper `__ where took this principle and turned it all the
+way up. They directly derived a cryptographic secret from the analog properties of their HSM's security mesh in an
+attempt to built a `Physically Unclonable Function, or PUF
+`__. The idea with PUFs is that they reproduce some entropy
+that comes from random tolerances of their production process. The same PUF will always yield (approximately) the same
+key, but since you cannot control these random production variations, in practice the resulting PUF cannot be cloned.
+Note however, that its secrets can of course be copied if you find a way to read them out.
+
+As Immler et al. demonstrated in their paper, you don't need any secret sauce to create an analog mesh sensing circuit.
+All you need are a bunch of (admittedly, expensive) off-the-shelf analog ICs. The interesting bit here is that by
+applying more advanced analog sensing, weaknesses of an otherwise coarse mesh desing could maybe be alleviated. That is,
+instead of monitoring a very fine mesh for continuity, you could instead closely monitor inductance and capacitance of a
+more coarse mesh. This trade-off between sensing circuit complexity (resp. cost) and mesh production capabilities may
+allow someone with a poorly equipped lab to still make a decent HSM. The question is, how do you produce a "decent" mesh
+given only basic tools? Here are some ideas.
+
+**3D metal patterning techniques** refers to any technique for producing thin, patterned metal structures on a
+three-dimensional plastic substrate. The basic process would consist of 3D-printing the polymer substrate, depositing a
+thin metal layer on top and then patterning this metal layer. A good starting point here would be the recent work of
+`Ben Kraznow`_ on this exact thing.
+
+**Copper filament methods** would be any method embedding copper wire from a spool in some resin or other matrix. This
+could mean either of a systematic approach of carefully winding or folding the copper wire into patterns or a
+non-systematic approach of simply stuffing a large tangle of copper wire into a small space. The main challenge with the
+former would be to find a non-tedious way of production. The main challenge with the latter would be to find process
+parameters that guarantee complete coverage of the HSM without holes or other areas of lower sensitivity to intrusions.
+Both approaches would require careful consideration of the overall design including the polymer resin supporting
+structure to ensure sensitivity against attacks since copper wire is mechanically much stronger than the micrometre-thin
+metal coatings used in patterning techniques.
+
+Envelope measurement
+--------------------
+
+Finally, I think there is another set of currently under-utilized tamper-detection methods that would be very
+interesting to explore. I am not aware of an academic term for these, so I am just going to dub them *envelope
+measurement* here.
+
+The fundamental apporach of a mesh is to build a physical security envelope (the mesh) that physically detects when it
+is disturbed (open or short circuits). This approach works well but has the disadvantage that these meshes are rather
+complex to manufacture since effectively every part of them is acting as a sensing element. A conceptually more complex
+but in practice potentially simpler approach might be to split the functions of security envelope and sensing element.
+This would mean that in place of the mesh, some form of passive element such as metal foil forms the security envelope
+which is then checked for tampering using a very sensitive sensor inside. This remote-sensing approach might simplify
+the manufacture of the envelope itself and thus yield a design that is more easily customized. Following are a few ideas
+on how to approach this envelope measurement problem.
+
+**Ultrasonic** If the HSM is potted, a few ultrasonic transducers could be added inside the potting. With several
+transducers, any one could be used to transmit ultrasound while the others measure complex phase and energy of the
+signal they receive. The circuitry for this could be made fairly simple if using a static transmit frequency or a low
+chirp rate by using a homodyne receiver built around a comparator fed into some timers. This approach would likely
+detect any mechanical attack and would also rule out chemical attacks involving liquids (though starting from which
+amount of liquid depends on receiver sensitivity). The main disadvantages might be high power consumption and cost and
+size of the ultrasonic transducers. Traditional cheap transducers made for air as a transmission medium are fairly large
+and might not adequately couple into potting compound. If somehow one could convince a standard small piezo element to
+do the same job that would be great as far as cost and size are concerned. A concern in some fringe use cases might be
+suceptibility to ambient noise, though this could easily be reduced at the expense of space and heat dissipation
+capacity by adding sound dampening on the outside. A likely attack vector against this approach might be using a laser
+cutter to drill a hole through the potting compound, then inserting probes carefully chosen to not couple too much
+to the potting compound ultrasonically.
+
+**Light** In either an unpotted HSM or one potted with a transparent (at some wavelengths) potting compound one could
+embed LEDs and photodiodes in a similar setup to the ultrasonic setup described above. In contrast to the ultrasound,
+the LEDs would literally have to light up the HSM's interior and shadows might be an issue since the HSM is likely some
+flat rectangular shape. A possible solution to this would be to coat both the embedded payload and the lid with some
+highly reflective paint such as some glossy silver paint or simple white paint. The basic approach might be as simple as
+simply turning on several LEDs distributed throughout the HSM in turn and measuring amplitude at several photodetectors,
+or as complex as doing a LIDAR-like phase measurement sweeping through a range of frequencies to determine not only
+absorption but also phase/distance characteristics between emitter LED and detector photodiode. Using some high-gain TIA
+along with a homodyne detector (lock-in amplifier) and changing emitter intensity, very precise measurements of both
+absorption and phase might be possible, as might be measurements through almost opaque, diffuse potting compounds such
+as a grey epoxide resin. The main disadvantages of this method would likely be the need to thoroughly light-proof the
+entire HSM (likely by wrapping it in metal foil) and the potentially high cost of transmitter and receiver circuitry
+(nice TIAs aren't cheap). To be effective against attacks using e.g. very fine drills and probes the system would likely
+have to be very sensitive.
+
+**Radar** Finally, one could turn to standard radar techniques to fingerprint the inside of the HSM. The goal here would
+be fingerprinting instead of mapping since only changes need to be detected. In this approach one could use homodyne
+detection to improve sensitivity and reduce receiver complexity, and sweep frequencies similar to an FMCW radar (but
+probably without exploiting the self-demodulation effect). Besides high cost, this approach has two disadvantages.
+First, such a system would likely not go beyond 24GHz or maybe 40GHz due to component availability issues. Even at 40GHz
+the wavelength in the potting compound would be in the order of magnitude of several millimeters. Fine intrusions using
+some tool chosen to not interact too much with the EM field inside the HSM such as a heated ceramic needle or simply a
+laser cutter might not be detectable using this approach. In any case, this system would certainly not be able to detect
+small holes piercing the HSM enclosure. The HSM enclosure would have to be made into an RF shield, likely by using some
+metal foil in it.
+
+Overall in the author's opinion these three techniques are most promising in order *Light*, *Ultrasonic*, *Radar*. Light
+would prbably provide the best sensitivity at expense of some cost. Ultrasonic might be used in conjunction with light
+to cover some additional angles since it is potentially very low-cost. Radar seems hard to engineer into a solution that
+works reliably and also would likely be at least an order of magnitude more expensive than the other two technologies
+while not providing better sensitivity.
+
+.. _`Ben Kraznow`: https://www.youtube.com/watch?v=Z228xymQYho
+.. _affordances: https://en.wikipedia.org/wiki/Affordance
+
diff --git a/blog/hsm-basics/mori_semi_hsm_talk_web.pdf b/content/blog/hsm-basics/mori_semi_hsm_talk_web.pdf
similarity index 100%
rename from blog/hsm-basics/mori_semi_hsm_talk_web.pdf
rename to content/blog/hsm-basics/mori_semi_hsm_talk_web.pdf
diff --git a/content/blog/ihsm-worlds-first-diy-hsm/index.rst b/content/blog/ihsm-worlds-first-diy-hsm/index.rst
new file mode 100644
index 0000000..22cf8e4
--- /dev/null
+++ b/content/blog/ihsm-worlds-first-diy-hsm/index.rst
@@ -0,0 +1,44 @@
+---
+title: "New Paper on Inertial Hardware Security Modules"
+date: 2021-11-23T23:42:20+01:00
+summary: >
+ Paper announcement: We have published a paper on how you can DIY a tamper-sensing hardware security module from any
+ single-board computer using a moving tamper-sensing mesh made from cheap PCBs.
+---
+
+World's First DIY HSM
+=====================
+
+Last week, Prof. Dr. Björn Scheuermann and I have `published our first joint paper on Hardware Security Modules
+`__. In our paper, we introduce Inertial Hardware Security
+Modules (IHSMs), a new way of building high-security HSMs from basic components. I think the technology we demonstrate
+in our paper might allow some neat applications where some civil organization deploys a service that no one, not even
+they themselves, can snoop on. Anyone can built an IHSM without needing any fancy equipment, which makes me optimistic
+that maybe the ideas of the `Cypherpunk movement `__ aren't obsolete
+after all, despite even the word "crypto" having been co-opted by radical capitalist environmental destructionists.
+
+An IHSM is basically an ultra-secure enclosure for something like a server or a raspberry pi that even someone with
+unlimited resources would have a really hard time cracking without destroying all data stored in it. The principle of an
+IHSM is the same as that of a `normal HSM`_. You have a payload that contains really secret data. There's really no way
+to prevent an attacker with physical access to the thing from opening it given enough time and abrasive discs for their
+angle grinder. So what you do instead is that you make it self-destruct its secrets within microseconds of anyone
+tampering with it. Usually, such HSMs are used for storing credit card pins and other financial data. They're expensive
+as fuck, all the while being about the same processing speed as a smartphone. Traditional HSMs use printed or
+lithographically patterned conductive foils for their security mesh. These foils are not an off-the-shelf component and
+are made in a completely custom manufacturing process. To create your own, you would have to re-engineer that entire
+process and probably spend some serious money on production machines.
+
+Inertial HSMs take the concept of traditional HSMs, but replace the usual tamper detection mesh with a few security mesh
+PCBs. These PCBs are coarser than traditional meshes by orders of magnitude, and would alone not even be close to enough
+to keep out even a moderately motivated attacker. IHSMs fix this issue by spinning the entire tamper detection mesh at
+very high speed. To tamper with the mesh, an attacker would have to stop it. This, in turn, can be easily detected by
+the mesh's alarm circuitry using a simple accelerometer as a rotation sensor.
+
+In our paper, we have shown a working prototype of the core concepts one needs to build such an IHSM. To build an IHSM
+you only need a basic electronics lab. I built the prototype in our paper at home during one of Germany's COVID
+lockdowns. You can have a look at our code and CAD on `my git `__. What is missing right
+now is an integration of all of these fragments into something cohesive that an interested person with the right tools
+could go out and build. We are planning to release this sort of documentation at some point, but right now we are
+focusing our effort on the next iteration of the design instead. Stay tuned for updates ;)
+
+.. _`normal HSM`: {{[}}
diff --git a/content/blog/jupyterlab-notebook-file-oneliner/index.rst b/content/blog/jupyterlab-notebook-file-oneliner/index.rst
new file mode 100644
index 0000000..5a543de
--- /dev/null
+++ b/content/blog/jupyterlab-notebook-file-oneliner/index.rst
@@ -0,0 +1,21 @@
+---
+title: "Getting the .ipynb Notebook File Location From a Running Jupyter Lab Notebook"
+date: 2025-06-29T23:42:00+01:00
+summary: >
+ If you need to get the path of the ipynb file in a running #Jupyter notebook, this one-liner will do the trick. It
+ seems chatgpt is confused, and a bunch of other approaches on the web look fragile and/or unnecessarily complex to
+ me.
+---
+
+If you need to get the path of the ipynb file in a running #Jupyter notebook, this one-liner will do the trick. It seems
+chatgpt is confused, and a bunch of other approaches on the web look fragile and/or unnecessarily complex to me.
+
+.. code:: python
+
+ import sys
+ Path(json.loads(Path(sys.argv[-1]).read_bytes())['jupyter_session'])
+
+The way this works is that for each notebook, jupyter starts a python "kernel" process that actually runs the notebook's
+code. That kernel gets a json file with info on the notebook's location on the disk passed through its command line.
+Since we're running code in that exact python process, we can just grab that json file from sys.argv, and read it
+ourselves.
diff --git a/blog/kicad-mesh-plugin/images/anim.webp b/content/blog/kicad-mesh-plugin/images/anim.webp
old mode 100644
new mode 100755
similarity index 100%
rename from blog/kicad-mesh-plugin/images/anim.webp
rename to content/blog/kicad-mesh-plugin/images/anim.webp
diff --git a/blog/kicad-mesh-plugin/images/cells-0.svg b/content/blog/kicad-mesh-plugin/images/cells-0.svg
old mode 100644
new mode 100755
similarity index 100%
rename from blog/kicad-mesh-plugin/images/cells-0.svg
rename to content/blog/kicad-mesh-plugin/images/cells-0.svg
diff --git a/blog/kicad-mesh-plugin/images/cells-100.svg b/content/blog/kicad-mesh-plugin/images/cells-100.svg
old mode 100644
new mode 100755
similarity index 100%
rename from blog/kicad-mesh-plugin/images/cells-100.svg
rename to content/blog/kicad-mesh-plugin/images/cells-100.svg
diff --git a/blog/kicad-mesh-plugin/images/cells-25.svg b/content/blog/kicad-mesh-plugin/images/cells-25.svg
old mode 100644
new mode 100755
similarity index 100%
rename from blog/kicad-mesh-plugin/images/cells-25.svg
rename to content/blog/kicad-mesh-plugin/images/cells-25.svg
diff --git a/blog/kicad-mesh-plugin/images/cells-50.svg b/content/blog/kicad-mesh-plugin/images/cells-50.svg
old mode 100644
new mode 100755
similarity index 100%
rename from blog/kicad-mesh-plugin/images/cells-50.svg
rename to content/blog/kicad-mesh-plugin/images/cells-50.svg
diff --git a/blog/kicad-mesh-plugin/images/cells-75.svg b/content/blog/kicad-mesh-plugin/images/cells-75.svg
old mode 100644
new mode 100755
similarity index 100%
rename from blog/kicad-mesh-plugin/images/cells-75.svg
rename to content/blog/kicad-mesh-plugin/images/cells-75.svg
diff --git a/blog/kicad-mesh-plugin/images/grid-vis-plain.svg b/content/blog/kicad-mesh-plugin/images/grid-vis-plain.svg
old mode 100644
new mode 100755
similarity index 100%
rename from blog/kicad-mesh-plugin/images/grid-vis-plain.svg
rename to content/blog/kicad-mesh-plugin/images/grid-vis-plain.svg
diff --git a/blog/kicad-mesh-plugin/images/grid-vis.svg b/content/blog/kicad-mesh-plugin/images/grid-vis.svg
old mode 100644
new mode 100755
similarity index 100%
rename from blog/kicad-mesh-plugin/images/grid-vis.svg
rename to content/blog/kicad-mesh-plugin/images/grid-vis.svg
diff --git a/blog/kicad-mesh-plugin/images/kicad-mesh-outline.png b/content/blog/kicad-mesh-plugin/images/kicad-mesh-outline.png
old mode 100644
new mode 100755
similarity index 100%
rename from blog/kicad-mesh-plugin/images/kicad-mesh-outline.png
rename to content/blog/kicad-mesh-plugin/images/kicad-mesh-outline.png
diff --git a/blog/kicad-mesh-plugin/images/kicad-mesh-result-large.png b/content/blog/kicad-mesh-plugin/images/kicad-mesh-result-large.png
old mode 100644
new mode 100755
similarity index 100%
rename from blog/kicad-mesh-plugin/images/kicad-mesh-result-large.png
rename to content/blog/kicad-mesh-plugin/images/kicad-mesh-result-large.png
diff --git a/blog/kicad-mesh-plugin/images/kicad-mesh-settings.png b/content/blog/kicad-mesh-plugin/images/kicad-mesh-settings.png
old mode 100644
new mode 100755
similarity index 100%
rename from blog/kicad-mesh-plugin/images/kicad-mesh-settings.png
rename to content/blog/kicad-mesh-plugin/images/kicad-mesh-settings.png
diff --git a/blog/kicad-mesh-plugin/images/kicad-mesh-settings2.png b/content/blog/kicad-mesh-plugin/images/kicad-mesh-settings2.png
old mode 100644
new mode 100755
similarity index 100%
rename from blog/kicad-mesh-plugin/images/kicad-mesh-settings2.png
rename to content/blog/kicad-mesh-plugin/images/kicad-mesh-settings2.png
diff --git a/blog/kicad-mesh-plugin/images/maze_tiles.svg b/content/blog/kicad-mesh-plugin/images/maze_tiles.svg
old mode 100644
new mode 100755
similarity index 100%
rename from blog/kicad-mesh-plugin/images/maze_tiles.svg
rename to content/blog/kicad-mesh-plugin/images/maze_tiles.svg
diff --git a/blog/kicad-mesh-plugin/images/maze_tiles_plain.svg b/content/blog/kicad-mesh-plugin/images/maze_tiles_plain.svg
old mode 100644
new mode 100755
similarity index 100%
rename from blog/kicad-mesh-plugin/images/maze_tiles_plain.svg
rename to content/blog/kicad-mesh-plugin/images/maze_tiles_plain.svg
diff --git a/blog/kicad-mesh-plugin/images/modern_art.svg b/content/blog/kicad-mesh-plugin/images/modern_art.svg
old mode 100644
new mode 100755
similarity index 100%
rename from blog/kicad-mesh-plugin/images/modern_art.svg
rename to content/blog/kicad-mesh-plugin/images/modern_art.svg
diff --git a/blog/kicad-mesh-plugin/images/tiles-25-small.svg b/content/blog/kicad-mesh-plugin/images/tiles-25-small.svg
old mode 100644
new mode 100755
similarity index 100%
rename from blog/kicad-mesh-plugin/images/tiles-25-small.svg
rename to content/blog/kicad-mesh-plugin/images/tiles-25-small.svg
diff --git a/blog/kicad-mesh-plugin/images/traces-25-small.svg b/content/blog/kicad-mesh-plugin/images/traces-25-small.svg
old mode 100644
new mode 100755
similarity index 100%
rename from blog/kicad-mesh-plugin/images/traces-25-small.svg
rename to content/blog/kicad-mesh-plugin/images/traces-25-small.svg
diff --git a/content/blog/kicad-mesh-plugin/index.rst b/content/blog/kicad-mesh-plugin/index.rst
new file mode 100644
index 0000000..0969bf3
--- /dev/null
+++ b/content/blog/kicad-mesh-plugin/index.rst
@@ -0,0 +1,229 @@
+---
+title: "Kicad Mesh Plugin"
+date: 2020-08-18T13:15:39+02:00
+summary: >
+ I wrote a little KiCad plugin that you can use to create security meshes, heaters and other things where you need
+ one or more traces cover the entire surface of a PCB. The plugin supports arbitrary PCB shapes, cutouts, and can
+ route around existing footprints and traces on the PCB.
+---
+
+.. raw:: html
+
+
+ ]
+
+
+Tamper Detection Meshes
+=======================
+
+Cryptography is at the foundation of our modern, networked world. From email to card payment infrastructure in brick and
+mortar stores, cryptographic keys secure almost every part of our digital lives againts cybercriminals or curious
+surveillance capitalists. Without cryptography, many of the things we routinely do in our lives such as paying for
+groceries with a credit card, messaging a friend on `Signal `_ or unlocking a car with its keyfob
+would not be possible. The security of all of these systems in its core lies on the secrecy of cryptographic keys.
+Systems differ in what kind of keys they use, how often these keys are replaced and the intricacies of the cryptographic
+operations these keys fit into but all have in common that their security relies on keeping the keys secret.
+
+In practice, this secrecy has been implemented in many different ways. Mass-market software such as browsers or
+messenger apps usually relies on some operating system facility to tell the computer "*please keep this piece of memory
+away from all other applications*". While on desktop operating systems usually this does not provide much of a barrier
+to other programs on the same computer, on modern mobile operating systems this approach is actually quite secure.
+However, given sufficient resources no security is perfect. All of these systems can be compromised if the host
+operating system is compromised sufficiently, and for organizations with considerable resources a market has sprung up
+that offers turn-key solutions for all wiretapping needs.
+
+In some applications, this level of security has not been considered sufficient. Particularly financial infrastructure
+is such a high-profile target that a lot of effort has been put into the security of cryptographic implementations. The
+best cryptographic algorithm is useless if it is run on a compromised system (from that system's point of view anyway).
+One of the core cryptographic components in financial applications are smartcards like they are used as payment cards in
+most countries nowadays. These smartcards contain a small, specialized cryptographic microcontroller that is designed to
+be hard to tamper with. Though one of the design goals of the system is to reduce the amount of sensitive information
+stored on the card, things such as copying of a card can only be hindered by making the chip hard to read out.
+
+.. raw:: html
+
+
+
+
+
+With smartcards being the means of choice on one side of the counter in electronic payments, on the other side of the
+counter a different technology prevails. Attacks on payment terminals are bound to have much more dire consequences than
+attacks on individual cards since one terminal might see hundreds of cards being read every day. For this reason, the
+level of attack countermeasures employed in these terminals is a considerable step up from bare smartcards. While a
+smartcard is made physically hard to tamper, it does not have a battery and it can only detect tampering once it is
+powered by a reader. This allows for well-equipped attackers to use tools such as Focused Ion Beam (FIB) workstations to
+circumvent the smartcard's defences while it is powered down, and then power up the card to carry out the actual attack.
+
+The answer to this problem in electronic payment infrastructure is called *Hardware Security Module*, or HSM. An HSM is
+similar to a smartcard in its function (cryptographic processing using keys that are meant to never leave the protection
+of the HSM). The one major between the two is that an HSM has its own battery and is continuously powered from its
+manufacture to the day it is scrapped. If the HSM looses power at any point in time, it uses a small amount of energy
+stored internally to securely wipe all cryptographic secrets from its memory within a few milliseconds.
+
+Being powered at all times allows the HSM to actively detect and respond to attacks. The most common way this is done is
+by wrapping the juicy secret parts in a foil or a printed circuit board that is patterned with a long and convoluted
+maze of wires, called a *mesh*. The HSM is continuously monitoring these wires for changes (such as shorts, breaks or
+changes in resistance) and will sound the alarm when any are detected. Practically, this presents a considerable hurdle
+to any attacker: They have to find a way to disable or circumvent the mesh while it is being monitored by the HSM. In
+practice, often this is no insurmountable challenge but it again increases attack costs.
+
+DIY Meshes
+==========
+
+Throughout my studies in security research I have always had an interest in HSMs. I have taken apart my fair share of
+HSMs and at this point, to understand the technology more, I want to experiment with building my own HSM. In last year's
+`HSM basics <{{[}}>`_ post I have lined out some ideas for a next generation design that
+deviates from the bread-and-butter apporoach of using a mesh as the primary security feature. Before embarking on
+practical experiments with these ideas, I want to first take a stab at replicating the current state of the art as best
+I can. State of the art meshes often use exotic substrates such as 3D plastic parts with traces chemically deposited on
+their surface or special flexible substrates with conductive ink traces. These technologies will likely be too
+cumbersome for me to implement myself only for a few prototypes, and industrial manufacturers will most likely be too
+expensive. Thus, I will concentrate on regular PCB technology for now.
+
+The idea of a mesh on a PCB is pretty simple: You have one or several traces that you try to cover every corner of the
+mesh PCB's area with. To be most effective, the traces should be as thin and as close together as possible. To increase
+the chances of a manipulation being detected, multiple traces can also be used that can then be monitored for shorts
+between them.
+
+While one can feasibly lay out these traces by hand, this really is an ideal application of a simple auto-router. While
+general PCB autorouting is *hard*, auto-routing just a few traces to approximate a space-filling curve is not. Since I
+am just starting out, I went with the simplest algorithmic solution I could think of. I first approximate the area
+designated to the mesh with a square grid whose cells are a multiple of my trace/space size. The mesh will only be drawn
+into grid cells that are fully inside the set boundaries. All cells outside or going across the border are discarded in
+this step.
+
+I decided to implement this auto-router in a KiCAD plugin. Though KiCADs plugin API is not the best, it was just about
+usable for this task.
+
+.. raw:: html
+
+
+ ]
+ The process starts out with the mesh shape being defined inside KiCAD. The mesh's outline is drawn
+ onto one of the graphical "Eco" layers. A footprint is placed to serve as a placeholder for the mesh's
+ connections to the outside world. This footprint is later used as the starting point for the mesh generation
+ algorithm.
+
+
+.. raw:: html
+
+
+
+ A visualization of the grid fitting process. First, a grid large enough to contain the mesh border
+ is generated. Then, every cell is checked for overlap with the mesh border area. If the cell is fully inside, it
+ (yellow), it is considered in the mesh generation later. Cells outside (gray) or on the border (red) are
+ discarded.
+
+
+After generating the grid, starting from the place I want to connect to the mesh, I walk the grid's cells one by one to
+generate a tree that covers the entire grid's area. To set the mesh's starting place I place a footprint on the board
+(dark gray in the picture above) whose designator I then tell my script. The tree generation algorithm looks like a
+depth-first search, except all checks are random. Depending on the level of randomness used at each step of the
+algorithm it yields more or less organized-looking results. Below are five example runs of the algorithm at differing
+levels of randomness with the cells colored according to their distance from the tree root. 0% randomness means that the
+algorithm is going to try cells in forward direction first on every step, and only then try out left and right. 100%
+means that on every step, the algorithm is choosing a new direction at random.
+
+.. raw:: html
+
+
+
+After I have built this tree like you would do in a depth-first search, I draw my one or several mesh mesh traces into
+it. The core observation here is that there is only 16 possible ways a cell can be connected: It has four neighbors,
+each of which it can either be connected to or not, which results in 2^4 options. If you consider rotations and
+mirroring, this works out to rotations or mirrored versions of only six base tiles: The empty tile, a tile with all four
+sides connected, a straight through, a 90 degree bend, and a "T"-junction—see the illustration below.
+
+.. raw:: html
+
+
+
+
+ There are six possible tile types in our connectivity graph inside its square tiling. This graphic illustrates
+ all sixteen rotations of these with how they would look in a two-conductor mesh.
+
+
+
+After tiling the grid according to the key above, we get the result below.
+
+.. raw:: html
+
+
+
+
+ An auto-routed mesh with traces colored according to tile types.
+
+
+
+.. raw:: html
+
+
+
+
+ The same mesh, but with traces all black.
+
+
+
+Putting it all together got me the KiCAD plugin you can see in the screenshot below.
+
+.. raw:: html
+
+
+
+
+ The plugin settings window open.
+
+
+
+.. raw:: html
+
+
+
+
+ After runing the plugin, the generated mesh looks like this in pcbnew.
+
+
+
+I am fairly happy with the result, but getting there was a medium pain. Especially KiCAD's plugin API is still very
+unfinieshed. It is hard to use, most parts are completely undocumented and if you use anything but its most basic parts
+things tend to break. One particular pain point for me was that after generating the mesh, the traces have been added to
+the board, but are still invisible for some reason. You have to save the board first, then re-load the file for them to
+become visible. Also KiCAD crashes whenever the plugin tries to remove a trace, so currently my workflow involves always
+making a copy of the board file first and treating mesh generation as a non-reversible finishing step.
+
+`Check out the code on my cgit `_.
+
+.. ::
+
+ .. raw:: html
+
+
+
+
+
+
diff --git a/projects/kicoil/blahaj-demo.png b/content/blog/kicoil-theory/header.png
similarity index 100%
rename from projects/kicoil/blahaj-demo.png
rename to content/blog/kicoil-theory/header.png
diff --git a/content/blog/kicoil-theory/index.rst b/content/blog/kicoil-theory/index.rst
new file mode 100644
index 0000000..c746cea
--- /dev/null
+++ b/content/blog/kicoil-theory/index.rst
@@ -0,0 +1,40 @@
+---
+title: "The KiCoil Planar Coil Generator"
+date: 2025-12-31T13:15:39+02:00
+summary: >
+ I wrote a layout tool generating planar coils that can handle spiral coils, toroidal coils, and hybrids in between
+ the two.
+---
+
+.. raw:: html
+
+
+
+
+
+A planar coil is a coil that is made from flat traces in some printing process like PCB or IC manufacturing, instead of
+being wound from wire. A few weeks ago, I needed one such planar coil that
+
+
+Project State
+-------------
+
+Currently, circular coils are special cased. Their layouts are directly generated, without the use of polygon
+offsetting. Windings are efficiently approximated using circular arcs. The circular coil layout code is solid, and
+contains decent (albeit not infallible) parameter sanity checks. Its main limitation is that sometimes, clearances can
+be violated a bit.
+
+The arbitrary shape code path is less stable, and produces faulty output in some cases. The most common error is
+crossing traces near the first vertex of the polygon when the polygon has highly convex or concave parts. I'm still
+improving this code path, but as long as you check the output, any errors it produces should be easy to fix by hand.
+
+If you would like to contribute, I'd welcome any ideas on the arbitrary shape code path. I think there is no single
+optimal solution here, and a generic algorithm that can be adjusted to favor for instance shape accuracy versus winding
+smoothness would be nice.
+
+All project links are listed on `https://jaseg.de/projects/kicoil/ `__. You can check
+out the code on my git at `https://git.jaseg.de/kicoil.git `__. Issues are tracked on
+codeberg at `https://codeberg.org/jaseg/kicoil `__. The kicad addon can be installed
+from the KiCad plugin manager, and you can install the standalone kicoil python package `from PyPI
+`__.
+
diff --git a/blog/led-characterization/images/daylight_spectrum_dvd.jpg b/content/blog/led-characterization/images/daylight_spectrum_dvd.jpg
similarity index 100%
rename from blog/led-characterization/images/daylight_spectrum_dvd.jpg
rename to content/blog/led-characterization/images/daylight_spectrum_dvd.jpg
diff --git a/blog/led-characterization/images/driver_ringing_strong.jpg b/content/blog/led-characterization/images/driver_ringing_strong.jpg
similarity index 100%
rename from blog/led-characterization/images/driver_ringing_strong.jpg
rename to content/blog/led-characterization/images/driver_ringing_strong.jpg
diff --git a/blog/led-characterization/images/driver_ringing_weak.jpg b/content/blog/led-characterization/images/driver_ringing_weak.jpg
similarity index 100%
rename from blog/led-characterization/images/driver_ringing_weak.jpg
rename to content/blog/led-characterization/images/driver_ringing_weak.jpg
diff --git a/blog/led-characterization/images/electronics_whole.jpg b/content/blog/led-characterization/images/electronics_whole.jpg
similarity index 100%
rename from blog/led-characterization/images/electronics_whole.jpg
rename to content/blog/led-characterization/images/electronics_whole.jpg
diff --git a/blog/led-characterization/images/hsv_cylinder.png b/content/blog/led-characterization/images/hsv_cylinder.png
similarity index 100%
rename from blog/led-characterization/images/hsv_cylinder.png
rename to content/blog/led-characterization/images/hsv_cylinder.png
diff --git a/blog/led-characterization/images/photodiode_sensitivity.svg b/content/blog/led-characterization/images/photodiode_sensitivity.svg
similarity index 100%
rename from blog/led-characterization/images/photodiode_sensitivity.svg
rename to content/blog/led-characterization/images/photodiode_sensitivity.svg
diff --git a/blog/led-characterization/images/preamp_back.jpg b/content/blog/led-characterization/images/preamp_back.jpg
similarity index 100%
rename from blog/led-characterization/images/preamp_back.jpg
rename to content/blog/led-characterization/images/preamp_back.jpg
diff --git a/blog/led-characterization/images/preamp_front.jpg b/content/blog/led-characterization/images/preamp_front.jpg
similarity index 100%
rename from blog/led-characterization/images/preamp_front.jpg
rename to content/blog/led-characterization/images/preamp_front.jpg
diff --git a/blog/led-characterization/images/preamp_schematic.jpg b/content/blog/led-characterization/images/preamp_schematic.jpg
similarity index 100%
rename from blog/led-characterization/images/preamp_schematic.jpg
rename to content/blog/led-characterization/images/preamp_schematic.jpg
diff --git a/blog/led-characterization/images/processed_plot_cheap_rgb.svg b/content/blog/led-characterization/images/processed_plot_cheap_rgb.svg
similarity index 100%
rename from blog/led-characterization/images/processed_plot_cheap_rgb.svg
rename to content/blog/led-characterization/images/processed_plot_cheap_rgb.svg
diff --git a/blog/led-characterization/images/raw_plot_cheap_rgb.svg b/content/blog/led-characterization/images/raw_plot_cheap_rgb.svg
similarity index 100%
rename from blog/led-characterization/images/raw_plot_cheap_rgb.svg
rename to content/blog/led-characterization/images/raw_plot_cheap_rgb.svg
diff --git a/blog/led-characterization/images/rgb_cube.svg b/content/blog/led-characterization/images/rgb_cube.svg
similarity index 100%
rename from blog/led-characterization/images/rgb_cube.svg
rename to content/blog/led-characterization/images/rgb_cube.svg
diff --git a/blog/led-characterization/images/spectrograph_step1_parts.jpg b/content/blog/led-characterization/images/spectrograph_step1_parts.jpg
similarity index 100%
rename from blog/led-characterization/images/spectrograph_step1_parts.jpg
rename to content/blog/led-characterization/images/spectrograph_step1_parts.jpg
diff --git a/blog/led-characterization/images/spectrograph_step2.jpg b/content/blog/led-characterization/images/spectrograph_step2.jpg
similarity index 100%
rename from blog/led-characterization/images/spectrograph_step2.jpg
rename to content/blog/led-characterization/images/spectrograph_step2.jpg
diff --git a/blog/led-characterization/images/spectrograph_step3.jpg b/content/blog/led-characterization/images/spectrograph_step3.jpg
similarity index 100%
rename from blog/led-characterization/images/spectrograph_step3.jpg
rename to content/blog/led-characterization/images/spectrograph_step3.jpg
diff --git a/blog/led-characterization/images/spectrograph_step4_complete.jpg b/content/blog/led-characterization/images/spectrograph_step4_complete.jpg
similarity index 100%
rename from blog/led-characterization/images/spectrograph_step4_complete.jpg
rename to content/blog/led-characterization/images/spectrograph_step4_complete.jpg
diff --git a/blog/led-characterization/images/zeus_hammer_breadboard.jpg b/content/blog/led-characterization/images/zeus_hammer_breadboard.jpg
similarity index 100%
rename from blog/led-characterization/images/zeus_hammer_breadboard.jpg
rename to content/blog/led-characterization/images/zeus_hammer_breadboard.jpg
diff --git a/blog/led-characterization/images/zeus_hammer_breadboard_original.jpg b/content/blog/led-characterization/images/zeus_hammer_breadboard_original.jpg
similarity index 100%
rename from blog/led-characterization/images/zeus_hammer_breadboard_original.jpg
rename to content/blog/led-characterization/images/zeus_hammer_breadboard_original.jpg
diff --git a/blog/led-characterization/images/zeus_hammer_schematic.jpg b/content/blog/led-characterization/images/zeus_hammer_schematic.jpg
similarity index 100%
rename from blog/led-characterization/images/zeus_hammer_schematic.jpg
rename to content/blog/led-characterization/images/zeus_hammer_schematic.jpg
diff --git a/blog/led-characterization/images/zeus_hammer_schematic_original.jpg b/content/blog/led-characterization/images/zeus_hammer_schematic_original.jpg
similarity index 100%
rename from blog/led-characterization/images/zeus_hammer_schematic_original.jpg
rename to content/blog/led-characterization/images/zeus_hammer_schematic_original.jpg
diff --git a/content/blog/led-characterization/index.rst b/content/blog/led-characterization/index.rst
new file mode 100644
index 0000000..31e81cc
--- /dev/null
+++ b/content/blog/led-characterization/index.rst
@@ -0,0 +1,510 @@
+---
+title: "LED Characterization"
+date: 2018-05-02T11:18:38+02:00
+summary: >
+ Recently, I have been working on a small driver for ambient lighting using 12V LED strips like you can get
+ inexpensively from China. I wanted to be able to just throw one of these somewhere, stick down some LED tape, hook
+ it up to a small transformer and be able to control it through Wifi. When I was writing the firmware, I noticed that
+ when fading between different colors, the colors look *all wrong*! This observation led me down a rabbit hole of
+ color perception and LED peculiarities.
+---
+
+Preface
+-------
+
+Recently, I have been working on a `small driver`_ for ambient lighting using 12V LED strips like you can get
+inexpensively from China. I wanted to be able to just throw one of these somewhere, stick down some LED tape, hook it up
+to a small transformer and be able to control it through Wifi. When I was writing the firmware, I noticed that when
+fading between different colors, the colors look *all wrong*! This observation led me down a rabbit hole of color
+perception and LED peculiarities.
+
+The idea of the LED driver was that it can be used either with up to eight single-color LED tapes or, much more
+interesting, with up to two RGB or RGBW (red-green-blue-white) LED tapes. For ambient lighting high color resolution was
+really important so you could dim it down a lot without flickering. I ended up using the same driver stage I used in the
+`multichannel LED driver`_ project for its great color resolution and low hardware requirements.
+
+.. raw:: html
+
+
+
+ An illustration of the RGB color cube.
+ Picture by
+ Maklaan from Wikimedia Commons,
+ CC-BY-SA 3.0
+
+
+
+To make setting colors over Wifi more intuitive I implemented support for HSV colors. RGB is fine for communication
+between computers, but I think HSV is easier to work with when manually inputting colors from the command line. RGB is
+close to how most monitors, cameras and the human visual apparatus work on a very low level but doesn't match
+higher-level human color perception very well. When we describe a color we tend to think in terms of "hue" or
+"brightness", and computing a measure of those from RGB values is not easy.
+
+Colors and Color Spaces
+-----------------------
+
+`Color spaces`_ are a mathematical abstraction of the concept of color. When we say "RGB", most of the time we actually
+mean `sRGB`_, a standardized notion of how to map three numbers labelled "red", "green" and "blue" onto a perceived
+color. `HSV`_ is an early attempt to more closely align these numbers with our perception. After HSV, a number of other
+*perceptual* color spaces such as `XYZ (CIE 1931)`_ and `CIE Lab/LCh`_ were born, further improving this alignment. In
+this mathematical model, mapping a color from one color space into another color space is just a coordinate
+transformation.
+
+.. raw:: html
+
+
+
+ An illustration of the HSV color space as a cylinder.
+ Picture by
+ SharkD from Wikimedia Commons,
+ CC-BY-SA 3.0
+
+
+
+CIE 1931 XYZ is much larger than any other color space, which is why it is a good basis to express other color spaces
+in. In XYZ there are many coordinates that are outside of what the human eye can perceive. Below is an illustration of
+the sRGB space within XYZ. The wireframe cube is (0,0,0) to (1,1,1) in XYZ. The colorful object in the middle is what
+of sRGB fits inside XYZ, and the lines extending out from it indicate the space that can be expressed in sRGB but not in
+XYZ. The fat white curve is a projection of the *monochromatic spectral locus*, that is the curve of points you get in
+XYZ for pure visible wavelengths.
+
+As you can see, sRGB is *much* smaller than XYZ or even the part within the monochromatic locus that we can perceive. In
+particular in the blues and greens we loose *a lot* of colors to sRGB.
+
+.. raw:: html
+
+
+
+ Illustration of the measured sRGB color space within XYZ. The thick, white line is the spectral
+ locus.
+
+ mkv/h264 download /
+ webm download
+
+
+
+The wrong colors I got when fading between colors were caused by this coordinate transformation being askew. Thinking
+over the problem, there are several sources for imperfections:
+
+* The LED driver may not be entirely linear. For most modulations such as PWM the brightness will be linear starting
+ from a certain value, but there is probably an offset caused by imperfect edges of the LED current. This offset can be
+ compensated with software calibration. I built a calibration setup for driver linearity in the `multichannel LED
+ driver`_ project. Below are pictures of ringing on the edges of an LED driver's waveform.
+
+* The red, green and blue channels of the LEDs used on the LED tape are not matched. This skews the RGB color space.
+ In practice, the blue channel of my RGB tape to me *looks* much brighter than the red channel.
+
+* The precise colors of the red, green and blue channels of the LEDs are unknown. Though the red channel *looks* red, it
+ may be of a slightly different hue compared to the reference red used in `sRGB`_ which would also skew the RGB color
+ space.
+
+.. raw:: html
+
+
+
+These last two errors are tricky to compensate. What I needed for that was basically a model of the *perceived* colors
+of the LED tape's color channels. A way of doing his is to record the spectra of all color channels and then evaluate
+their respective XYZ coordinates. If all three channels are measured in one go with the same setup the relative
+magnitudes of the channels in XYZ will be accurate.
+
+To map any color to the LEDs, the color's XYZ coordinates simply have to be mapped onto the linear coordinate system
+produced by these three points within XYZ. LEDs are mostly linear in their luminous flux vs. current characteristic so
+this model will be adequate. The spectral integrals mapping the channels' measured responses to XYZ need only be
+calculated once and their results can be used as scaling factors thereafter.
+
+Measuring the spectrum
+----------------------
+
+In order to compensate for the cheap LED tape's non-ideal performance I had to measure the LED's red, green and blue
+channels' spectra. The obvious thing would be to go out and buy a `spectrograph`_, or ask someone to borrow theirs. The
+former is kind of expensive, and I did not want to wait two weeks for the thing to arrive. The latter I could probably
+not do every time I got new LED tape. Thus the only choice was to build my own.
+
+Luckily, building your own spectrometer is really easy. The first thing you need is something that splits incident light
+into its constituent wavelengths. In professional devices this is called the *`monochromator`_*, since it allows extraction
+of small color bands from the spectrum. The second thing is some sort of optics that project the incident light onto a
+screen behind the monochromator. In professional devices lenses or curved mirrors are used. In a simple homebrew job a
+pinhole as you would use in a `camera obscura`_ does a remarkably nice job.
+
+For the monochromator component several things could be used. A prism would work, but I did not have any. The
+alternative is a `diffraction grating`_. Professional gratings are quite specialized pieces of equipment and thus
+rather expensive. Luckily, there is a common household item that works almost as well: A regular CD or DVD. The
+microscopic grooves that are used to record data in a CD or DVD work the same as the grooves in a professional
+diffraction grating.
+
+Household spectra
+-----------------
+
+From this starting point, a few seconds on my favorite search engine yielded an `article by two researchers from the
+National Science Museum in Tokyo`_ providing a nice blueprint for a simple cardboard-and-DVD construction for use in
+classrooms. I replicated their device using a DVD and it worked beautifully. Daylight and several types of small LEDs I
+had around did show the expected spectra. Small red, yellow, green, and blue LEDs showed narrow spectra, daylight one
+continuous broad one, and white LEDs a continuous broad one with a distinct bright spot in the blue part. The
+single-color LED spectra are quite narrow since they are determined by the LED's semiconductor's band gap, which is
+specific to the semiconductor used and is quite precise. White LEDs are in fact a blue LED chip covered with a so-called
+*phosphor*. This phosphor is not elementary phosphorus but an anorganic compound that absorbs the LED chip's blue light
+and re-emits a broader spectrum of more yellow-ish wavelengths instead. The final LED spectrum is a superposition of
+both spectra, with some of the original blue light leaking through the phosphor mixing with the broadband yellow
+spectrum of the phosphor.
+
+.. raw:: html
+
+
+
+
+Now that I had a spectrograph, I needed a somewhat predictable way of measuring the spectrum it gave me.
+
+Measuring a spectrum
+--------------------
+
+Pointing a camera at the spectrograph would be the obvious thing to do. This produces pretty images but has one critical
+flaw: I wanted to acquire quantitative measurements of brightness across the spectrum. Since I don't have a precise
+technical datasheet specifying the spectral response of any of my cameras I can't compare the absolute brightness of
+different colors on their pictures. Some other sensor was needed.
+
+.. raw:: html
+
+
+
+ The daylight spectrum as seen using a DVD as a grating.
+ Picture by
+ Xofc from Wikimedia Commons,
+ CC-BY-SA 4.0
+
+
+
+
+Measuring light intensity
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Looking around my lab, I found a bag of `SFH2701`_ visible-light photodiodes. Their
+datasheet includes their spectral response so I can compensate for that, allowing precise-ish absolute intensity
+measurements. Just like LEDs, photodiodes are extremely linear across several orders of magnitude. The datasheet of the
+classic `BPW34`_ photodiode shows that this photodiode's light current is exactly proportional to illuminance over at
+least three orders of magnitude. The `SFH2701`_ datasheet does not include a similar graph but its performance will be
+similar. The `SFH2701`_ photodiodes I had at hand were perfect for the job compared to the vintage `BPW34`_ since their
+active sensing area is really small (0.6mm by 0.6mm) compared to the BPW34 (a whopping 3mm by 3mm). If I were to use a
+`BPW34`_ I would have to insert some small apterture in front of it so it does not catch too broad a part of the
+spectrum at once. The `SFH2701`_ is small enough that if I just point it at the projected spectrum directly I will
+already get only a small part of the spectrum inside its 0.6mm active area.
+
+To convert the photodiode's tiny photocurrent into a measurable voltage I built another copy of the `transimpedance
+amplifier`_ circuit I already used in the `multichannel LED driver`_. A `transimpedance amplifier`_ is an
+amplifiert that produces a large voltage from a small current. The weird name comes from the fact that it works kind of
+like an amplified resistor (which can be generalized as an *impedance* electrically). Apply a current to a resistor and
+you get a voltage. A transimpedance amplifiert does the same with the difference that its input always stays at 0V,
+making it look like an ideal current sink to the connected current source.
+
+Transimpedance amplifiers are common in optoelectronics to convert small photocurrents to voltages. In this instance I
+built a very simple circuit with a dampened transimpedance amplifier stage followed by a simple RC filter for noise
+rejection and a regular non-inverting amplifier using another op-amp from the same chip to further boost the filtered
+transimpedance amplifier output. I put all the passives setting amplifier response (the gain-setting resistors and the
+filter resistor and capacitors) on a small removable adapter so I could easily change them if necessary. I put a small
+trimpot on the virtual ground both amplifers use as a reference so I could trim that if necessary.
+
+.. raw:: html
+
+
+
+ The photodiode preamplifier schematic. Schematic drawn with an unlicensed copy of
+ DaveCAD.
+
+
+Following are pictures of the preamplifier board. The connectors on the top-left side are two copies of the analog
+signal for the ADC and a small panel meter. The SMA connector is used as the photodiode input since coax cables are
+generally low-leakage and have built-in shielding. The circuit is powered via the micro-USB connector and the analog
+ground bias voltage can be adjusted using the trimpot.
+
+For easy replacement, all passives setting gain and frequency response are on a small, pluggable carrier PCB made from a
+SMD-to-DIP adapter.
+
+Flying-wire construction is just fine for this low-frequency circuit. In a high-speed photodiode preamp, the
+transimpedance amplifier circuit would be highly sensitive to stray capacitance, but we're not aiming at high speed
+here.
+
+.. raw:: html
+
+
+
+Given a way to measure intensity what remains missing is a way to scan a single photodiode across the spectrum.
+
+Scanning the projection
+~~~~~~~~~~~~~~~~~~~~~~~
+
+A cheap linear stage can be found in any old CD or DVD drive. These drives use a small linear stage based on a
+stepper-driven screw to move the laser unit radially. Removing the laser unit and connecting a leftover stepper driver
+module I was left with a small linear stage with about 45 steps per cm without microstepping enabled. The driver I used
+was an `A4988`_ module that required at least 8V motor drive voltage. I used a small micro USB-input boost converter
+module to generate a stable 10V supply for the motor driver, with the USB's 5V rail used as a logic supply for the motor
+driver.
+
+The `SFH2701`_ can easily be mounted to the linear stage using a small SMD breakout board glued in place with thin wires
+connecting it to the transimpedance amplifier. The DVD drive linear stage is not very strong so it is important that
+this wire does not put too much strain on it.
+
+Above the photodiode, I mounted a small piece of paper on the linear stage to be used as a projection screen to align
+the linear stage in front of the spectrometer viewing window. A line on the screen paper points to the photodiode die in
+parallel to the linear stage allowing precise alignment.
+
+The whole unit with photodiode preamplifier, linear stage, photodiode and stepper motor driver finally looks like this:
+
+.. raw:: html
+
+
+
+ The complete electronics setup. The buspirate on the right interfaces to a computer and controls the
+ stepper driver and ADC'es the preamp output. The two panel meters show the preamp output and stepper voltage for
+ setup.
+
+
+The projection of the spectrum can be adjusted by moving the light source relative to the entry slot and by moving
+around the grating DVD.
+
+The capture process
+~~~~~~~~~~~~~~~~~~~
+
+To capture a spectrum, first the light source has to be mounted near the spectrograph's entry slot. The LED tape I
+tested I just taped face-down directly into it. Next, the grating DVD has to be adjusted to make sure the spectrum
+covers a sensible part of the photodiode's path. Mostly, this boils down to adjusting the photodiode distance and height
+to match the vertical extent and wiggling the grating DVD to adjust the projection's horizontal position.
+
+After the optics are set-up, the photodiode preamplifier has to be adjusted. In my experiments, most LED tape at 5GΩ
+required a high-ish amplification. The goal in this step is to maximize the peak response of the preamp to be just
+shy of its VCC rail to make best use of its dynamic range. To adjust the pre-amp, I took several very coarsely-spaced
+measurements to give me an estimate of the peak while I did not yet know its precise location.
+
+Since stray daylight totally swamped out the weak projection of the LED's spectrum I shielded the entire setup with a
+small box made of black cardboard and two black t-shirts on top. This shielding proved adequate for all my measurements
+but I had to be careful not to accidentially move the DVD that was stuck into the spectrograph with the shielding
+t-shirts.
+
+For capturing a single spectrum I wrote a small python script that will automatically move the stepper in adjustable
+intervals and take two measurements at each point, one with the LED tape off that can be used for offset calibration and
+one with the LED tape on. All measurements are stored in a sqlite database that can then be accesssed from other
+scripts.
+
+I built a small script that shows the progress of the current run and an jupyter notebook for data analysis. The jupyter
+notebook is capable of live-updating a graph with the in-progress spectrum's data. This was quite useful as a sanity
+check for when I made some mistake easy to spot in the resulting data.
+
+After one color channel is captured, the LED tape has to be manually set to the next color and the next measurement can
+begin.
+
+.. raw:: html
+
+
+
+ A plot of the raw preamp output voltage versus stepper position. From left to right, the three peaks
+ are blue, green and red. Step 0 corresponds to the bottommost stepper position and the shortest wavelength.
+
+
+
+
+Data analysis
+~~~~~~~~~~~~~
+
+Data analysis consists of three major steps: Offset- and stray light removal, wavelength and amplitude calibration and
+color space mapping.
+
+Offset removal
+**************
+The first task is to remove the offset caused by dark current as well as stray light of the LED's bright primary
+reflection on the DVD. The LED is very bright and only a small part of its light gets reflected by the grating towards
+the photodiode screen. The remaining part of the light is reflected onto the table in front of the DVD spectrograph.
+Though I covered all of this with black cardboard, some of that light ultimately gets reflected onto the photodiode.
+This causes a large offset, in particular in the blue part of the spectrum since in this part the photodiode is closest
+to the spectrograph's opening.
+
+The composite offset can be approximated with a second-order polynomial that is fitted to all the data outside of the
+main peak's area. Since at this point the wavelength of each data point is still unknown this is done with a rough first
+estimate of the three colors' peaks' locations and widths.
+
+Wavelength- and amplitude calibration
+*************************************
+The photodiode's response is strongly wavelength-dependent. In particular in the blue band, the photodiode's sensitivity
+gets very poor down to about 20% at the edge to ultraviolet. This effect is strong enough to move the apparent location
+of the blue peak towards red.
+
+.. raw:: html
+
+
+
+ A plot of the photodiode's relative sensitivity in the visible spectrum. The sensitivity is
+ normalized against its peak at 820nm.
+
+
+
+The problem is that in order to remove this non-linearity, we would already have to know the wavelength of the measured
+light. Since I don't, I settled for a two-step process. First, a coarse wavelength calibration is done relative to the
+red peak and the short-wavelength edge of the blue peak. The photodiode measurements are then sensitivity-corrected
+using this coarse measurement. Then all three channel peaks are measured in the resulting data and a fine wavelength
+estimate is produced by a least-squares fit of a linear function. This fine estimate is then used for a second
+sensitivity correction of all original measurements and the scale is changed from stepper motor step count to
+wavelength in nanometers.
+
+.. raw:: html
+
+
+
+ A plot of the processed measurements. From left to right, the three peaks are blue, green and red.
+
+
+
+.. FIXME re-do these measurements, avoiding clipping
+.. FIXME re-do calibration using CCFL
+.. FIXME calibration for brightness imbalance due to wedge-shaped projection of spectrum
+
+Color space mapping
+*******************
+Finally, to achieve the objective of measuring the LED tape's channels' precise color coordinates the measured spetra
+have to be matched against the color spaces' *color matching functions*. The color matching functions describe how
+strong the color space's idealized *standard observer* would react to light at a particular wavelength. Going from a
+measured spectrum to color coordinates XYZ works by integrating over the product of the measurement and each color
+coordinate's color matching function.
+
+The result are three color coordinates X, Y and Z for each channel R, G and B yielding nine coordinates in total. When
+written as a matrix conversion between XYZ color space and LED-RGB color space is as simple as multiplying that matrix
+(or its inverse) and a vector from one of the color spaces.
+
+In XYZ space, the set of colors that can be produced with this LED tape is described by the `parallelepiped`_ spanned by
+the three channel's XYZ vectors. In the following figures, you can see a three-dimensional model of the RGB LED's color
+space (colorful) as well as sRGB (white) for comparison plotted within CIE 1931 XYZ. There is no natural map to scale
+both so for this illustration the LED color space has been scaled to fit. These figures were made with blender and a few
+lines of python. The blender project file including all settings and the python script to generate the color space
+models can be found in the `project repo`_.
+
+.. raw:: html
+
+
+
+ Illustration of the measured LED color space scaled to fit within XYZ with sRGB (light gray) for
+ comparison. The thick, white line is the spectral locus.
+
+ mkv/h264 download /
+ webm download
+
+
+
+As you can see, the result is pretty disappointing. The LED's color space parallepiped is very narrow, which is because
+the blue channel is much brighter than the other two channels. An easy fix for this is to scale-up the RGB space and
+drop any values outside XYZ. The scaling factor is a trade-off between color space coverage and brightness. You can
+produce the most colors when you clip all channels to brightness of the weakest channel (green in this case), but that
+will make the result very dim. Scaling brightness like that stretches the RGB parallelepiped along its major axis. Up to
+a point the number of possible colors (the gamut) increases at expense of maximum brightness. When the parallelepiped is
+stretched far enought for all three channel vectors to be outside the 1,1,1 XYZ-cube, maximum brightness continues to
+decrease but the gamut stays constant. I don't know a simple scientific way to solve this problem, so I just played
+around with a couple of factors and settled on 2.5 as a reasonable compromise. Below is an illustration.
+
+.. raw:: html
+
+
+
+ Illustration of the measured LED color space at scale factor 2.5 within XYZ with sRGB (light gray)
+ for comparison. The thick, white line is the spectral locus.
+
+ mkv/h264 download /
+ webm download
+
+
+
+Firmware implementation
+-----------------------
+In the end, the above measurements yield two matrices: One for mapping XYZ to RGB, and one for mapping RGB to XYZ. Of
+the several versions of CIE XYZ I chose the CIE 1931 XYZ color space as a basis for the firmware because it is most
+popular. Mapping a color coordinate in one color space to the other is as simple as performing nine floating-point
+multiplications and six additions. Mapping Lab or Lch to RGB is done by first mapping Lab/Lch to XYZ, then XYZ to RGB.
+Lab to XYZ is somewhat complex since it requires a floating-point power for gamma correction, but any self-respecting
+libc will have one of those so this is still no problem. Lch also requires floating-point sine and cosine functions, but
+these should still be no problem on most hardware.
+
+My implementation of these conversions in the ESP8266 firmware of my `Wifi LED driver`_ can be found `on Github`_. You
+can view the Jupyter notebook most of the analysis above `here `__.
+
+.. _`on Github`: https://github.com/jaseg/esp_led_drv/blob/master/user/led_controller.c
+.. _`project repo`: https://github.com/jaseg/led_drv
+.. _`Wifi LED driver`: {{[}}
+.. _`small driver`: {{][}}
+.. _`multichannel LED driver`: {{][}}
+.. _`sRGB`: https://en.wikipedia.org/wiki/SRGB
+.. _`CC BY-SA 3.0`: https://creativecommons.org/licenses/by-sa/3.0
+.. _`Color spaces`: https://en.wikipedia.org/wiki/Color_space
+.. _`HSV`: https://en.wikipedia.org/wiki/HSL_and_HSV
+.. _`CIE Lab/LCh`: https://en.wikipedia.org/wiki/Lab_color_space
+.. _`XYZ (CIE 1931)`: https://en.wikipedia.org/wiki/CIE_1931_color_space
+.. _`camera obscura`: https://en.wikipedia.org/wiki/Pinhole_camera
+.. _`article by two researchers from the National Science Museum in Tokyo`: http://www.candac.ca/candacweb/sites/default/files/BuildaSpectroscope.pdf
+.. _`spectrograph`: https://en.wikipedia.org/wiki/Ultraviolet%E2%80%93visible_spectroscopy
+.. _`monochromator`: https://en.wikipedia.org/wiki/Monochromator
+.. _`diffraction grating`: https://en.wikipedia.org/wiki/Diffraction_grating
+.. _`SFH2701`: https://dammedia.osram.info/media/resource/hires/osram-dam-2495903/SFH%202701.pdf
+.. _`BPW34`: http://www.vishay.com/docs/81521/bpw34.pdf
+.. _`transimpedance amplifier`: https://en.wikipedia.org/wiki/Transimpedance_amplifier
+.. _`A4988`: https://www.pololu.com/file/0J450/A4988.pdf
+.. _`parallelepiped`: https://en.wikipedia.org/wiki/Parallelepiped
diff --git a/blog/led-characterization/video/led_within_srgb_fancy_camera_path_scale=2.5.mkv b/content/blog/led-characterization/video/led_within_srgb_fancy_camera_path_scale=2.5.mkv
similarity index 100%
rename from blog/led-characterization/video/led_within_srgb_fancy_camera_path_scale=2.5.mkv
rename to content/blog/led-characterization/video/led_within_srgb_fancy_camera_path_scale=2.5.mkv
diff --git a/blog/led-characterization/video/led_within_srgb_fancy_camera_path_scale=2.5.webm b/content/blog/led-characterization/video/led_within_srgb_fancy_camera_path_scale=2.5.webm
similarity index 100%
rename from blog/led-characterization/video/led_within_srgb_fancy_camera_path_scale=2.5.webm
rename to content/blog/led-characterization/video/led_within_srgb_fancy_camera_path_scale=2.5.webm
diff --git a/blog/led-characterization/video/led_within_srgb_scale=1.0.mkv b/content/blog/led-characterization/video/led_within_srgb_scale=1.0.mkv
similarity index 100%
rename from blog/led-characterization/video/led_within_srgb_scale=1.0.mkv
rename to content/blog/led-characterization/video/led_within_srgb_scale=1.0.mkv
diff --git a/blog/led-characterization/video/led_within_srgb_scale=1.0.webm b/content/blog/led-characterization/video/led_within_srgb_scale=1.0.webm
similarity index 100%
rename from blog/led-characterization/video/led_within_srgb_scale=1.0.webm
rename to content/blog/led-characterization/video/led_within_srgb_scale=1.0.webm
diff --git a/blog/led-characterization/video/led_within_srgb_scale=2.5.mkv b/content/blog/led-characterization/video/led_within_srgb_scale=2.5.mkv
similarity index 100%
rename from blog/led-characterization/video/led_within_srgb_scale=2.5.mkv
rename to content/blog/led-characterization/video/led_within_srgb_scale=2.5.mkv
diff --git a/blog/led-characterization/video/led_within_srgb_scale=2.5.webm b/content/blog/led-characterization/video/led_within_srgb_scale=2.5.webm
similarity index 100%
rename from blog/led-characterization/video/led_within_srgb_scale=2.5.webm
rename to content/blog/led-characterization/video/led_within_srgb_scale=2.5.webm
diff --git a/blog/led-characterization/video/led_within_srgb_scale=3.mkv b/content/blog/led-characterization/video/led_within_srgb_scale=3.mkv
similarity index 100%
rename from blog/led-characterization/video/led_within_srgb_scale=3.mkv
rename to content/blog/led-characterization/video/led_within_srgb_scale=3.mkv
diff --git a/blog/led-characterization/video/led_within_srgb_scale=3.webm b/content/blog/led-characterization/video/led_within_srgb_scale=3.webm
similarity index 100%
rename from blog/led-characterization/video/led_within_srgb_scale=3.webm
rename to content/blog/led-characterization/video/led_within_srgb_scale=3.webm
diff --git a/blog/led-characterization/video/sRGB.mkv b/content/blog/led-characterization/video/sRGB.mkv
similarity index 100%
rename from blog/led-characterization/video/sRGB.mkv
rename to content/blog/led-characterization/video/sRGB.mkv
diff --git a/blog/led-characterization/video/sRGB.webm b/content/blog/led-characterization/video/sRGB.webm
similarity index 100%
rename from blog/led-characterization/video/sRGB.webm
rename to content/blog/led-characterization/video/sRGB.webm
diff --git a/blog/led-characterization/video/scale=1.mkv b/content/blog/led-characterization/video/scale=1.mkv
similarity index 100%
rename from blog/led-characterization/video/scale=1.mkv
rename to content/blog/led-characterization/video/scale=1.mkv
diff --git a/blog/led-characterization/video/scale=1.webm b/content/blog/led-characterization/video/scale=1.webm
similarity index 100%
rename from blog/led-characterization/video/scale=1.webm
rename to content/blog/led-characterization/video/scale=1.webm
diff --git a/blog/led-characterization/video/scale=2.5.mkv b/content/blog/led-characterization/video/scale=2.5.mkv
similarity index 100%
rename from blog/led-characterization/video/scale=2.5.mkv
rename to content/blog/led-characterization/video/scale=2.5.mkv
diff --git a/blog/led-characterization/video/scale=2.5.webm b/content/blog/led-characterization/video/scale=2.5.webm
similarity index 100%
rename from blog/led-characterization/video/scale=2.5.webm
rename to content/blog/led-characterization/video/scale=2.5.webm
diff --git a/blog/led-characterization/video/scale=5.mkv b/content/blog/led-characterization/video/scale=5.mkv
similarity index 100%
rename from blog/led-characterization/video/scale=5.mkv
rename to content/blog/led-characterization/video/scale=5.mkv
diff --git a/blog/led-characterization/video/scale=5.webm b/content/blog/led-characterization/video/scale=5.webm
similarity index 100%
rename from blog/led-characterization/video/scale=5.webm
rename to content/blog/led-characterization/video/scale=5.webm
diff --git a/content/blog/make-cgit-serve-pdfs-directly/index.rst b/content/blog/make-cgit-serve-pdfs-directly/index.rst
new file mode 100644
index 0000000..5da8dc3
--- /dev/null
+++ b/content/blog/make-cgit-serve-pdfs-directly/index.rst
@@ -0,0 +1,26 @@
+---
+title: "How to make cgit serve PDF files as direct downloads"
+date: 2025-11-17T23:42:00+01:00
+summary: >
+ cgit is great, but by default when you click on a PDF file in a repository content listing it will show you a
+ hexdump of the file. You can access the actual file by clicking the "plain" link on top of the listing, but that's
+ not only annoying, for large PDF files rendering the hexdump can also hang browser tabs.
+---
+
+cgit is great, but by default when you click on a PDF file in a repository content listing it will show you a
+hexdump of the file. You can access the actual file by clicking the "plain" link on top of the listing, but that's
+not only annoying, for large PDF files rendering the hexdump can also hang browser tabs.
+
+I found a quick and easy solution to this problem, which I'm documenting here because it seems nobody on the
+internet has really done this before, and the usual AI assistants (ChatGPT and Claude) are both deeply confused.
+
+You just add a simple rewrite rule to your nginx config that 302-redirects requests to ``/tree/.../foobar.pdf`` to
+``/plain/.../foobar.pdf``. Here's the rule, make sure you put them in your nginx config *before* the location directive
+proxying requests to cgit.
+
+.. code:: nginx
+
+ location ~ ^/([^/]+)/tree/(.*\.pdf)$ {
+ return 302 /$1/plain/$2;
+ }
+
diff --git a/blog/multichannel-led-driver/images/asymmetric_iled.svg b/content/blog/multichannel-led-driver/images/asymmetric_iled.svg
similarity index 100%
rename from blog/multichannel-led-driver/images/asymmetric_iled.svg
rename to content/blog/multichannel-led-driver/images/asymmetric_iled.svg
diff --git a/blog/multichannel-led-driver/images/asymmetric_vgate.svg b/content/blog/multichannel-led-driver/images/asymmetric_vgate.svg
similarity index 100%
rename from blog/multichannel-led-driver/images/asymmetric_vgate.svg
rename to content/blog/multichannel-led-driver/images/asymmetric_vgate.svg
diff --git a/blog/multichannel-led-driver/images/bcm_schema.jpg b/content/blog/multichannel-led-driver/images/bcm_schema.jpg
similarity index 100%
rename from blog/multichannel-led-driver/images/bcm_schema.jpg
rename to content/blog/multichannel-led-driver/images/bcm_schema.jpg
diff --git a/blog/multichannel-led-driver/images/corrected_brightness_sim.svg b/content/blog/multichannel-led-driver/images/corrected_brightness_sim.svg
similarity index 100%
rename from blog/multichannel-led-driver/images/corrected_brightness_sim.svg
rename to content/blog/multichannel-led-driver/images/corrected_brightness_sim.svg
diff --git a/blog/multichannel-led-driver/images/driver_linearity_raw.svg b/content/blog/multichannel-led-driver/images/driver_linearity_raw.svg
similarity index 100%
rename from blog/multichannel-led-driver/images/driver_linearity_raw.svg
rename to content/blog/multichannel-led-driver/images/driver_linearity_raw.svg
diff --git a/blog/multichannel-led-driver/images/driver_output_ltspice_schematic.jpg b/content/blog/multichannel-led-driver/images/driver_output_ltspice_schematic.jpg
similarity index 100%
rename from blog/multichannel-led-driver/images/driver_output_ltspice_schematic.jpg
rename to content/blog/multichannel-led-driver/images/driver_output_ltspice_schematic.jpg
diff --git a/blog/multichannel-led-driver/images/driver_pcb_built.jpg b/content/blog/multichannel-led-driver/images/driver_pcb_built.jpg
similarity index 100%
rename from blog/multichannel-led-driver/images/driver_pcb_built.jpg
rename to content/blog/multichannel-led-driver/images/driver_pcb_built.jpg
diff --git a/blog/multichannel-led-driver/images/driver_ringing_strong.jpg b/content/blog/multichannel-led-driver/images/driver_ringing_strong.jpg
similarity index 100%
rename from blog/multichannel-led-driver/images/driver_ringing_strong.jpg
rename to content/blog/multichannel-led-driver/images/driver_ringing_strong.jpg
diff --git a/blog/multichannel-led-driver/images/driver_ringing_weak.jpg b/content/blog/multichannel-led-driver/images/driver_ringing_weak.jpg
similarity index 100%
rename from blog/multichannel-led-driver/images/driver_ringing_weak.jpg
rename to content/blog/multichannel-led-driver/images/driver_ringing_weak.jpg
diff --git a/blog/multichannel-led-driver/images/led_strip_alight.jpg b/content/blog/multichannel-led-driver/images/led_strip_alight.jpg
similarity index 100%
rename from blog/multichannel-led-driver/images/led_strip_alight.jpg
rename to content/blog/multichannel-led-driver/images/led_strip_alight.jpg
diff --git a/blog/multichannel-led-driver/images/linearization_setup.jpg b/content/blog/multichannel-led-driver/images/linearization_setup.jpg
similarity index 100%
rename from blog/multichannel-led-driver/images/linearization_setup.jpg
rename to content/blog/multichannel-led-driver/images/linearization_setup.jpg
diff --git a/blog/multichannel-led-driver/images/olsndot_output_schematic.jpg b/content/blog/multichannel-led-driver/images/olsndot_output_schematic.jpg
similarity index 100%
rename from blog/multichannel-led-driver/images/olsndot_output_schematic.jpg
rename to content/blog/multichannel-led-driver/images/olsndot_output_schematic.jpg
diff --git a/blog/multichannel-led-driver/images/olsndot_pcb.png b/content/blog/multichannel-led-driver/images/olsndot_pcb.png
similarity index 100%
rename from blog/multichannel-led-driver/images/olsndot_pcb.png
rename to content/blog/multichannel-led-driver/images/olsndot_pcb.png
diff --git a/blog/multichannel-led-driver/images/olsndot_schematic.png b/content/blog/multichannel-led-driver/images/olsndot_schematic.png
similarity index 100%
rename from blog/multichannel-led-driver/images/olsndot_schematic.png
rename to content/blog/multichannel-led-driver/images/olsndot_schematic.png
diff --git a/blog/multichannel-led-driver/images/overshoot_sim_r0.svg b/content/blog/multichannel-led-driver/images/overshoot_sim_r0.svg
similarity index 100%
rename from blog/multichannel-led-driver/images/overshoot_sim_r0.svg
rename to content/blog/multichannel-led-driver/images/overshoot_sim_r0.svg
diff --git a/blog/multichannel-led-driver/images/overshoot_sim_r100.svg b/content/blog/multichannel-led-driver/images/overshoot_sim_r100.svg
similarity index 100%
rename from blog/multichannel-led-driver/images/overshoot_sim_r100.svg
rename to content/blog/multichannel-led-driver/images/overshoot_sim_r100.svg
diff --git a/blog/multichannel-led-driver/images/pwm_schema.jpg b/content/blog/multichannel-led-driver/images/pwm_schema.jpg
similarity index 100%
rename from blog/multichannel-led-driver/images/pwm_schema.jpg
rename to content/blog/multichannel-led-driver/images/pwm_schema.jpg
diff --git a/blog/multichannel-led-driver/images/uncorrected_brightness_sim.svg b/content/blog/multichannel-led-driver/images/uncorrected_brightness_sim.svg
similarity index 100%
rename from blog/multichannel-led-driver/images/uncorrected_brightness_sim.svg
rename to content/blog/multichannel-led-driver/images/uncorrected_brightness_sim.svg
diff --git a/content/blog/multichannel-led-driver/index.rst b/content/blog/multichannel-led-driver/index.rst
new file mode 100644
index 0000000..f157458
--- /dev/null
+++ b/content/blog/multichannel-led-driver/index.rst
@@ -0,0 +1,462 @@
+---
+title: "32-Channel LED tape driver"
+date: 2018-05-02T11:31:14+02:00
+summary: >
+ Together, a friend and I outfitted the small staircase at Berlin's Chaos Computer Club with nice, shiny RGB-WW LED
+ tape for ambient lighting. For this installation, I made a 32-channel LED driver that achieves high dynamic range on
+ all 32 channels using a cheap microcontroller by using Binary Code Modulation.
+---
+
+Theoretical basics
+==================
+
+Together, a friend and I outfitted the small staircase at Berlin's Chaos Computer Club with nice, shiny RGB-WW LED tape
+for ambient lighting. This tape is like regular RGB tape but with an additional warm white channel, which makes for much
+more natural pastels and whites. There are several variants of RGBW tape. Cheap ones have separate RGB and white LEDs,
+which is fine for indirect lighting but does not work for direct lighting. Since we wanted to mount our tape in channels
+at the front of the steps, we had to use the slightly more expensive variant with integrated RGBW LEDs. These are LEDs
+in the 5050 (5.0mm by 5.0mm) form factor common with RGB LEDs that have a small section divided off for the white
+channel. The red, green and blue LED chips sit together in the larger section covered with clear epoxy and the white
+channel is made up from the usual blue LED inside a yellow phosphor in the smaller section.
+
+Since we wanted to light up all of 15 steps, and for greatest visual effect we would have liked to be able to control
+each step individually we had to find a way to control 60 channels of LED tape with a reasonable amount of hardware.
+
+LED tape has integrated series resistors and runs off a fixed 12V or 24V constant-voltage supply. This means you don't
+need a complex constant-current driver as you'd need with high-power LEDs. You can just hook up a section of LED tape
+to a beefy MOSFET to control it. Traditionally, you would do *Pulse Width Modulation* (PWM) on the MOSFET's input to
+control the LED tape's brightness.
+
+Pulse Width Modulation
+----------------------
+
+`Pulse Width Modulation`_ is a technique of controlling the brightness of a load such as an LED with a digital signal.
+The basic idea is that if you turn the LED on and off much too fast for anyone to notice, you can control its power by
+changing how long you turn it on versus how long you leave it off.
+
+PWM divides each second into a large number of periods. At the beginning of each period, you turn the LED on. After
+that, you wait a certain time until you turn it off. Then, you wait for the next period to begin. The periods are always
+the same length but you can set when you turn off the LED. If you turn it off right away, it's off almost all the time
+and it looks like it's off to your eye. If you turn it off right at the end, it's on almost all the time and it looks
+super bright to your eye. Now, if you turn it off halfway into the cycle, it's on half the time and it will look to your
+eye as half as bright as before. This means that you can control the LED's brightness with only a digital signal and
+good timing.
+
+.. raw:: html
+
+
+ ]
+ Waveforms of two PWM cycles at different duty cycles.
+
+
+PWM works great if you have a dedicated PWM output on your microcontroller. It's extremely simple in both hardware and
+software. Unfortunately for us, controlling 32 channels with PWM is not that easy. Cheap microcontrollers only have `a
+handful of hardware PWM outputs`_, so we'd either have to do everything in software, bit-banging our LED modulation, or
+we'd have to use a dedicated chip.
+
+Doing PWM in software is both error-prone and slow. Since the maximum dynamic range of a PWM signal is limited by the
+shortest duty cycle it can do, software PWM being slow means it has poor PWM resolution at maybe 8 bits at most. Poor
+color resolution is not a problem if all you're doing is to fade around the `HSV rainbow`_, but for ambient lighting
+where you *really* want to control the brightness down to a faint shimmer you need all the color resolution you can get.
+
+If you rule out software PWM, what remains are dedicated `hardware PWM controllers`_. Most of these have either of three
+issues:
+
+* They're expensive
+* They don't have generous PWM resolution either (12 bits if you're lucky)
+* They're meant to drive small LEDs such as a 7-segment display directly and you can't just hook up a MOSFET to their
+ output
+
+This means we're stuck in a dilemma between two poor solutions if we'd want to do PWM. Luckily for us, PWM is not the
+only modulation in town.
+
+.. _`Pulse Width Modulation`: https://en.wikipedia.org/wiki/Pulse-width_modulation
+.. _`a handful of hardware PWM outputs`: https://www.nxp.com/parametricSearch#/&c=c731_c380_c173_c161_c163&page=1
+.. _`HSV rainbow`: https://en.wikipedia.org/wiki/HSL_and_HSV
+.. _`hardware PWM controllers`: http://www.ti.com/lit/ds/symlink/tlc5940.pdf
+
+Binary Code Modulation
+----------------------
+
+PWM is the bread-and-butter of the maker crowd. Everyone and their cat is doing it and it works really well most of the
+time. Unbeknownst to most of the maker crowd, there is however another popular modulation method that's mostly used in
+professional LED systems: Enter `*Binary Code Modulation* (BCM) `_.
+
+BCM is to PWM sort of what barcodes are to handwriting. While PWM is easy to understand and simple to implement if all
+you have is a counter and an IO pin, BCM is more complicated. On the other hand, computers can do complicated and BCM
+really shines in multi-channel applications.
+
+Similar to PWM, BCM works by turning on and off the LED in short periods fast enough to make your eye perceive it as
+partially on all the time. In PWM the channel's brightness is linearly dependent on its duty cycle, i.e. the percentage
+it is turned on. In PWM the duty cycle D is the total period T divided by the on period T_on. The issue with doing PWM
+on many channels at once is that you have to turn off each channel at the exact time to match its duty cycle.
+Controlling many IO pins at once with precise timing is really hard to do in software.
+
+BCM avoids this by further dividing each period into smaller periods which we'll call *bit periods* and splitting each
+channel's duty cycle into chunks the size of these bit periods. The amazingly elegant thing in BCM now is that as you
+can guess from the name these bit periods are weighted in powers of two. Say the shortest bit period lasts 1
+microsecond. Then the second-shortest bit period is 2 microseconds and the third is 4, the fifth 8, the sixth 16 and so
+on.
+
+.. raw:: html
+
+
+
+ Waveforms of a single 4-bit BCM cycle at different duty cycles. This BCM can produce 16 different
+ levels.
+
+
+Staggered like this, you turn on the LED for integer value of microseconds by turning it on in the bit periods
+corresponding to the binary bits of that value. If I want my LED to light for 19 microseconds every period, I turn it on
+in the 16 microsecond bit period, the 2 microsecond bit period and the 1 microsecond bit period and leave it off for the
+4 and 8 mircosecond bit periods.
+
+Now, how this is better instead of just more complicated than plain old PWM might not be clear yet. But consider this:
+Turning on and off a large number of channels, each at its own arbitrary time is hard because doing the timing in
+software is hard. We can't use hardware timers since we only have two or three of those, and we have 32 channels.
+However, we can use one hardware timer to trigger a really cheap external latch to turn on or off the 32 channels all at
+once. With this setup, we can only controll all channels at once, but we can do so with very precise timing.
+
+All we need to do is to set our timer to the durations of the BCM bit periods, and we can get the same result as we'd
+get with PWM with only one hardware timer and a bit of code that is not timing-critical anymore.
+
+Applications of Binary Code Modulation
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+BCM is a truly wondrous technique, and outside of hobbyist circles it is in fact very widely known. Though we're using
+it to control just 32 channels here, you can do much more channels without any problems. The most common application
+where BCM is invariably used is *any* kind of LED screen. Controlling the thousands and thousands of LEDs in an LED
+screen with PWM with a dedicated timer for each LED would not be feasible. With BCM, all you need to dedicate to a
+single LED is a flipflop (or part of one if you're multiplexing). In fact, there is a whole range of `ICs with no other
+purpose than to enable BCM on large LED matrices `_. Basically, these are a
+high-speed shift register with latched outputs much like the venerable 74HC595_, only their outputs are constant-current
+sinks made so that you can directly connect an LED to them.
+
+.. _74HC595: http://www.ti.com/lit/ds/symlink/sn74hc595.pdf
+
+Running BCM on LED tape
+~~~~~~~~~~~~~~~~~~~~~~~
+
+In our case, we don't need any special driver chips to control our LED tape. We just connect the outputs of a 74HC595_
+shift register to one MOSFET_ each, and then we directly connect the LED tape to these MOSFETs. The MOSFETs allow us to
+drive a couple of amps into the LED tape from the weak outputs of the shift register.
+
+The BCM timing is done by hooking up two timer channels of our microcontroller to the shift registers *strobe* and
+*reset* inputs. We set the timer to PWM mode so we can generate pulses with precise timing. At the beginning of each
+bit period, a pulse will strobe the data for this bit period that we shifted in previously. At the end of the bit
+period, one pulse will reset the shift register and one will strobe the freshly-reset zeros into the outputs.
+
+.. raw:: html
+
+
+
+
+ The schematic of a single output of this LED driver. Multiple shift register stages can be cascaded.
+
+
+
+
+Our implementation of this system runs on an STM32F030F4P6_, the smallest, cheapest ARM microcontroller you can get from
+ST. This microcontroller has only 16kB of flash and 1kB of RAM, but that's plenty for our use. We use its SPI controller
+to feed the modulation data to the shift registers really fast, and we use two timer channels to control the shift
+registers' reset and strobe.
+
+We can easily cascade shift registers without any ill side-effects, and even hundreds of channels should be no problem
+for this setup. The only reason we chose to stick to a 32-channel board is the mechanics of it. We thought it would be
+easier to have several small boards instead of having one huge board with loads of connectors and cables coming off it.
+
+The BOM cost per channel for our system is 3ct for a reasonable MOSFET, about 1ct for one eighth of a shift register
+plus less than a cent for one resistor between shift register and MOSFET. In the end, the connectors are more expensive
+than the driving circuitry.
+
+.. _MOSFET: https://en.wikipedia.org/wiki/MOSFET
+.. _STM32F030F4P6: http://www.st.com/resource/en/datasheet/stm32f030f4.pdf
+
+Hardware design
+===============
+
+From this starting point, we made a very prototype-y hardware design for a 32-channel 12V LED tape driver. The design is
+based on the STM32F030F4P6_ driving the shift registers as explained above. The system is controlled through an RS485_
+bus that is connected up to the microcontroller's UART using an MAX485_-compatible RS485 transceiver. The LED tape is
+connected using 9-pin SUB-D_ connectors since they are cheap and good enough for the small current of our short segments
+of LED tape. The MOSFETs we use are small SOT-23_ logic-level MOSFETs. In various prototypes we used both International
+Rectifier's IRLML6244_ as well as Alpha & Omega Semiconductor's AO3400_. Both are good up to about 30V/5A. Since we're
+only driving about 2m of LED tape per channel we're not going above about 0.5A and the MOSFETs don't even get warm.
+
+.. _RS485: https://en.wikipedia.org/wiki/RS-485
+.. _MAX485: https://datasheets.maximintegrated.com/en/ds/MAX1487-MAX491.pdf
+.. _IRLML6244: https://www.infineon.com/dgdl/?fileId=5546d462533600a4015356686fed261f
+.. _AO3400: http://aosmd.com/pdfs/datasheet/AO3400.pdf
+.. _SUB-D: https://en.wikipedia.org/wiki/D-subminiature
+.. _SOT-23: http://www.nxp.com/documents/outline_drawing/SOT23.pdf
+
+Switching nonlinearities
+------------------------
+During testing of our initial prototype, we noticed that the brightness seemed to jump around when fading to very low
+values. It turned out that our extremely simple LED driving circuit consisting of only the shift register directly
+driving a MOSFET, which in turn directly drives the LED tape was maybe a little bit too simple. After some measurements
+it turned out that we were looking at about 6Vpp of ringing on the driver's output voltage. The picture below is the
+voltrage we saw on our oscilloscope on the LED tape.
+
+.. raw:: html
+
+
+
+ Bad ringing on the LED output voltage caused by wiring inductance. Note that the effect on the
+ actual LED current is less bad than this looks since the LED's V/I curve is nonlinear.
+
+
+
+Dynamic switching behavior: Cause and Effect
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+A bit of LTSpice_ action later we found that the inductance of the few metres of cable leading to the LED tape is the
+likely culprit. The figure below is the schematic used for the simulations.
+
+.. raw:: html
+
+
+
+ The schematic of the simulation in LTSpice
+
+
+As tested, the driver does not include any per-output smoothing so the ~.5A transient on each BCM cycle hits the cable
+in full. Combined with the cable inductance, this works out to a considerable lag of the rising edge of the LED
+current, and bad ringing on its falling edge. Below is the voltage on the LED output from an LTSpice simulation of our
+driver.
+
+.. raw:: html
+
+
+
+ The result of our LTSpice simulation. This simulation assumes 1µH of wiring inductance and 50Ω of
+ output impedance on the part of the shift register. The ringing at the gate visible in the gate voltage graph is
+ due to feed-through of the ringing at the output through the MOSFET's parasitic Cgd.
+
+
+We were able to reduce the rining and limit the effect somewhat by putting a 220Ω series resistor in between the shift
+register output and the MOSFET gate. This resistor forms an RC circuit with the MOSFET's nanofarad or two of gate
+capacitance. The result of this is that the LED current passing the wire's ESL rises slightly more slowly and thus the
+series inductance gets excited slightly less, and the overshoot decreases. Below is a picture of the waveform with the
+damping resistor in place and a picture of our measurement for comparison. The resistor values don't agree perfectly
+since the estimated ESL and stray capacitance of the wiring is probably way off.
+
+.. raw:: html
+
+
+
+ Adding a resistor in front of the MOSFET gate to slow the transition damped the ringing somewhat,
+ but ultimately it cannot be eliminated entirely. Note how you can actually see the miller plateau on the
+ trailing edge of this signal.
+
+
+
+.. raw:: html
+
+
+
+ The LTSpice simulation result with the same parameters as above but with an extra 100Ω between the
+ shfit register's output and the MOSFET's gate.
+
+
+A side effect of this fix is that now the effective on-time of the LED tape is much longer than the duty cycle at the
+shift register's output at very small duty cycles (1µs or less). This is caused by the MOSFET's `miller
+plateau`_. For illustration, below is a graph of both the excitation waveform (the boxy line) and the resulting LED
+current (the other ones) both without damping (top) and with 220Ω damping (bottom). As you can see the effective duty
+cycle of the LED current is not at all equal to the 50% duty cycle of the excitation square wave.
+
+.. raw:: html
+
+
+
+ Simulated LED duty cycle with and without damping. The damping resistance used in this simulation
+ was 220Ω.
+
+
+.. raw:: html
+
+
+
+ The MOSFET gate voltage from the simulation in the figure above. You can clearly see how the miller
+ plateau (the horizontal part of the trace at about 1V) is getting much wider with added damped, and how the
+ resulting gate charge/discharge curve is not at all that of a capacitor anymore.
+
+
+
+
+In conclusion, we have three major causes for our calculated LED brightness not matching reality:
+
+* Ringing of the equivalent series inductance of the wiring leading up to the LED tape
+* Miller plateau lag
+* The damping resistor and the MOSFET gate forming an RC filter that helps with wire ESL ringing but worsens the miller
+ plateau issue and deforms the LED current edges.
+
+Added up, these three effects yield a picture that agrees well with our simulations and measurements. The overall effect
+is neglegible at long period durations (>10µs), but gets really bad at short period durations (<1µs). The effect is
+non-linear, so correcting for it is not as simple as adding an offset.
+
+.. _LTSpice: http://www.analog.com/en/design-center/design-tools-and-calculators/ltspice-simulator.html
+.. _`miller plateau`: https://www.vishay.com/docs/68214/turnonprocess.pdf
+
+Measuring LED tape brightness
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+In order to correct for the nonlinearities mentioned above, we decided to implement a lookup table mapping BCM period to
+actual timer setting. That is, each row of the table contains the actual period length we need to set the
+microcontroller's timer to in order to get our intended brightness steps.
+
+To calibrate our driver, we needed a setup for reproducible measurement of the relative brightness of our LED tape at
+different settings. Absolute brightness is not of interest to us as the eye can't perceive it. To perform the
+calibration, the LED driver is set to enable each single BCM period in turn, i.e. brightness values 1, 2, 4, 8, 16 etc.
+
+The setup we used to measure the LED tape's brightness consists of a bunch of LED tape stuck into a tin can for
+shielding against both stray light and electromagnetic interference and a photodiode looking at the LED tape. We used
+the venerable BPW34_ photodiode in our setup as I had a bunch leftover from another project and because they are quite
+sensitive owing to their physically large die area.
+
+.. raw:: html
+
+
+
+ The LED brighness measurement setup. The big tin can contains a bunch of LED tape and the
+ photodiode. The breadboard on the right is used for the photodiode preamplifier and for jumpering around the LED
+ tape's channels. The red board next to it is the buspirate used as ADC. The board on the bottom left is a
+ TTL-to-RS485 converter and the board in the middle is the unit under test.
+
+
+The photodiode's photocurrent is converted into a voltage using a very simple transimpedance amplifier based around a
+MCP6002_ opamp that was damped into oblivion with a couple nanofarads of capacitance in its feedback loop. The MCP6002_
+is a fine choice here since I had a bunch and because it is a CMOS opamp, meaning it has low bias current that would
+mess up our measurements. For many applications, opamp bias current is not a big issue but when using the opamp to
+directly measure very small currents at its input it quickly swamps out the signal for most BJT-input types.
+
+The transimpedance amplifier's output is read from the computer using the ADC input of a buspirate USB thinggamajob. In
+general I would not recommend the buspirate as a tool for this job since it's ADC is not particularly good and it's
+programming interface is positively atrocious, but it was what I had and it beat first wiring up one of the dedicated
+ADC chips I had in my parts bin.
+
+The computer runs a small python script cycling the LED tape through all its BCM period settings and taking a brightness
+measurement at each step. Later on, these measurements can be plotted to visualize the resulting slope's linearity, and
+we can even do a simulation of the resulting brightness for all possible control values by just adding the measured
+photocurrents for a certain BCM setpoint just as our retinas would do.
+
+.. raw:: html
+
+
+
+
+ A plot of the measured brightness of our LED tape for each BCM period. The brightness values are normalized
+ to the value measured at the LSB setpoint (brightness=1/65535). Ideally, this plot would show a straight
+ line with slope 1. Obviously, it doesn't. The bend in the curve is caused by the above-mentioned duty cycle
+ offset adding an offset to all brightness values. Shown is both the raw data (light), which has essentially zero
+ measurement error and a linear fit (dark).
+
+ The plot is in log-log to approximate how the human eye would perceive brightness, i.e. highly sensitive at
+ low values but not very sensitive at all at large values.
+
+
+
+While it would be possible to fully automate the optimization of BCM driver lookup tables, we needed only one and in the
+end I just sat down and manually tweaked the ideal values we initially calculated until I liked the result. You can see
+the resulting brightness curve below.
+
+.. raw:: html
+
+
+
+.. _BPW34: http://www.vishay.com/docs/81521/bpw34.pdf
+.. _MCP6002: http://ww1.microchip.com/downloads/en/DeviceDoc/21733j.pdf
+
+Controlling the driver
+----------------------
+
+Now that our driver was behaving linear enough that you couldn't see it actually wasn't we needed a nice way to control
+it from a computer of our choice. In the ultimate application (our staircase) we'll use a raspberry pi for this. Since
+we already settled on an RS485_ bus for its robustness and simplicity, we had to device a protocol to control the driver
+over this bus. Here, we settled on a simple, COBS_-based protocol for the reasons I wrote about in `How to talk to your
+microcontroller over serial `_.
+
+To address our driver nodes, we modified the Makefile to build a random 32-bit MAC into each firmware image. The
+protocol has only five message types:
+
+1. A 0-byte *ping* packet, to which each node would reply with its own address in the
+ first 100ms after boot. This can be used to initially discover the addresses of all nodes connected to the bus. You'd
+ spam the bus with *ping* packets, and then hit reset on each node in turn. The control computer would then receive
+ each device's MAC address as you hit reset.
+2. A 4-byte *address* packet that says which device that the following packet is for. This way of us using the packet
+ length instead of a packet type field is not particularly elegant, but our system is simple enough and it was easy to
+ implement.
+3. A 64-byte *frame buffer* packet that contains 16 bits of left-aligned brightness data for every channel
+4. A one-byte *get status* packet that tells the device to respond with...
+5. ...a 27-byte status packet containing a brief description of the firmware (version number, channel count, bit depth
+ etc.) as well as the device's current life stats (VCC, temperature, uptime, UART frame errors etc.).
+
+Wrapped up in a nice python interface we can now easily enumerate any drivers we connect to a bus, query their status
+and control their outputs.
+
+.. _COBS: https://en.wikipedia.org/wiki/Consistent_Overhead_Byte_Stuffing
+
+Conclusion
+----------
+
+.. raw:: html
+
+
+
+Putting some thought into the control circuitry and software, you can easily control large numbers of channels of LEDs
+using extremely inexpensive driving hardware without any compromises on dynamic range. The design we settled on can
+drive 32 channels of LED tape with a dynamic range of 14bit at a BOM cost of below 10€. All it really takes is a couple
+of shift registers and a mildly bored STM32 microcontroller.
+
+Get a PDF file of the schematic and PCB layout `here `__ or download the CAD files
+and the firmware sources `from github `_. You can view the Jupyter notebook used to
+analyze the brightness measurement data `here `__.
+
diff --git a/blog/multichannel-led-driver/olsndot_v02_schematics_and_pcb.pdf b/content/blog/multichannel-led-driver/olsndot_v02_schematics_and_pcb.pdf
similarity index 100%
rename from blog/multichannel-led-driver/olsndot_v02_schematics_and_pcb.pdf
rename to content/blog/multichannel-led-driver/olsndot_v02_schematics_and_pcb.pdf
diff --git a/content/blog/note-git-server-move.rst b/content/blog/note-git-server-move.rst
new file mode 100644
index 0000000..9fde442
--- /dev/null
+++ b/content/blog/note-git-server-move.rst
@@ -0,0 +1,18 @@
+---
+title: "Housekeeping note: git.jaseg.de has moved"
+date: 2026-05-30T10:00:00+02:00
+summary: >
+ A small note: As part of moving the servers and website to a new, more suitable host for my Ashen and Yanartas
+ projects as well as the creation of yasec, my freelance consulting business, I've moved git.jaseg.de from a custom
+ cgit/gitolite setup to a forgejo instance. This may have broken some links, especially deep links into the old cgit.
+ If you notice any broken links, please reach out through email.
+---
+
+A small note: As part of moving the servers and website to a new, more suitable host for my Ashen_ and Yanartas_
+projects as well as the creation of yasec_, my freelance consulting business, I've moved `git.jaseg.de
+`__ from a custom cgit/gitolite setup to a forgejo instance. This may have broken some links,
+especially deep links into the old cgit. If you notice any broken links, please reach out through email.
+
+.. _Ashen: https://yasec.de/projects/ashen/
+.. _Yanartas: https://yasec.de/projects/yanartas/
+.. _yasec: https://yasec.de/
diff --git a/blog/paper-sampling-mesh-monitor/fig_edge_risetime.pdf b/content/blog/paper-sampling-mesh-monitor/fig_edge_risetime.pdf
similarity index 100%
rename from blog/paper-sampling-mesh-monitor/fig_edge_risetime.pdf
rename to content/blog/paper-sampling-mesh-monitor/fig_edge_risetime.pdf
diff --git a/blog/paper-sampling-mesh-monitor/fig_edge_risetime.png b/content/blog/paper-sampling-mesh-monitor/fig_edge_risetime.png
similarity index 100%
rename from blog/paper-sampling-mesh-monitor/fig_edge_risetime.png
rename to content/blog/paper-sampling-mesh-monitor/fig_edge_risetime.png
diff --git a/content/blog/paper-sampling-mesh-monitor/index.rst b/content/blog/paper-sampling-mesh-monitor/index.rst
new file mode 100644
index 0000000..9fd1c7f
--- /dev/null
+++ b/content/blog/paper-sampling-mesh-monitor/index.rst
@@ -0,0 +1,104 @@
+---
+title: "New paper: Monitoring Tamper-Sensing Meshes Using Low-Cost, Embedded Time-Domain Reflectometry"
+date: 2025-10-20T23:42:00+01:00
+summary: >
+ I've got a new paper accepted at CHES, to be published in TCHES 2026/1 around beginning of December and out `on
+ eprint now `__. The topic of the paper is a way of monitoring a tamper-sensing
+ mesh through time-domain reflectometry using very cheap components. The end result is a circuit that costs about
+ 10 € in parts that is able to measure TDR responses with a few hundred picoseconds of resolution.
+---
+
+.. raw:: html
+
+
+
+
+ The final setup. On the right is the measurement board, and on the left is the mesh test specimen plugged
+ in. In a real application, you would integrate both into your target circuit.
+
+
+
+I've got a new paper accepted at CHES, to be published in TCHES 2026/1 around beginning of December and out
+`on eprint now `__. The topic of the paper is a way of monitoring a tamper-sensing
+mesh through time-domain reflectometry using very cheap components. The end result is a circuit that costs about 10 € in
+parts that is able to measure TDR responses with a few hundred picoseconds of resolution.
+
+Tamper-Sensing meshes are squiggly circuit traces that are used to tamper-proof high-security devices like hardware
+security modules, ATM pin pads and countertop card payment terminals. Any area where you would like to prevent an
+attacker from drilling or sawing through in a physical attack, you completely cover with one or more such circuit traces
+in a meandering pattern. I've written up some work on a KiCad plugin for creating these meshes `in another post
+<{{< ref "blog/kicad-mesh-plugin" >}}>`__.
+
+Up to now, the state of the art in monitoring these security meshes has mostly been finding ways to precisely monitor
+their ohmic resistance in the analog domain. This has the disadvantage of both being fairly complex in circuitry and of
+presenting a steep trade-off between sensitivity and false-positive rate since all you get out of the whole mesh is a
+single analog measurement containing maybe 12 to 16 bits of entropy. There have been a few papers on using more advanced
+RF techniques, but they all either required really expensive circuitry and/or highly customized meshes that for instance
+couldn't easily be fitted into arbitrary shapes.
+
+.. raw:: html
+
+
+
+
+ The sampling edges as measured by the board itself. As you can see, using a cheap microcontroller and some
+ cheap display signal redriver ICs along with commodity RF schottkies you can get pretty spicy edges on a
+ budget. Link to full resolution.
+
+
+
+In this paper, I wrote up a method using the high-resolution timer of an inexpensive `STM32G4-series microcontroller
+`__ together with a DisplayPort/HDMI "redriver" chips meant for
+amplifying high-speed display signals to create fast pulse edges. I characterized several chips, with the best
+performers being TI's `TDP0604 `__ and Diodes' `PI3HDX12211
+`__, coming in at 2 to 5 € depending on where and how much you buy. The
+fast edges generated by these drivers are then fed to a set of four-diode sampling gates using cheap RF schottky diodes
+to create a really cheap but fast time-domain reflectometer. Using this TDRD circuit, a security mesh can be monitored
+much more precisely than before, since the circuit creates a sort of fingerprint of the mesh's trace along its length.
+
+.. raw:: html
+
+
+
+One of the fun highlights of this project to me was micro-soldering test boards using different redriver ICs. Above, you
+can see the result of that soldering work. I was really happy with my cheap aliexpress microscope and with my fancy
+titanium tweezers!
+
+Have a look into the paper, where I wrote up details on the circuitry as well as a whole bunch of (>1000!) measurements
+characterizing the system. As it turns out, it's really sensitive to attacks while being reasonably robust to
+environmental disturbances. In fact, it's so sensitive that the circuit can distinguish multiple identical (!) copies of
+the same mesh produces by JLCPCB from their manufacturing tolerances such as FR-4 fiber weave alignment.
+
+You can find a preprint of the paper `on eprint `__, and I'll update this post with a
+link to the published version of the paper when it becomes available. The eprint is identical to the published version
+as of now.
+
+The source code of the project is available at `https://git.jaseg.de/sampling-mesh-monitor.git `__.
diff --git a/blog/paper-sampling-mesh-monitor/pic_74lvc_small.jpg b/content/blog/paper-sampling-mesh-monitor/pic_74lvc_small.jpg
similarity index 100%
rename from blog/paper-sampling-mesh-monitor/pic_74lvc_small.jpg
rename to content/blog/paper-sampling-mesh-monitor/pic_74lvc_small.jpg
diff --git a/blog/paper-sampling-mesh-monitor/pic_board_setup_2_small.jpg b/content/blog/paper-sampling-mesh-monitor/pic_board_setup_2_small.jpg
similarity index 100%
rename from blog/paper-sampling-mesh-monitor/pic_board_setup_2_small.jpg
rename to content/blog/paper-sampling-mesh-monitor/pic_board_setup_2_small.jpg
diff --git a/blog/paper-sampling-mesh-monitor/pic_max3748_small.jpg b/content/blog/paper-sampling-mesh-monitor/pic_max3748_small.jpg
similarity index 100%
rename from blog/paper-sampling-mesh-monitor/pic_max3748_small.jpg
rename to content/blog/paper-sampling-mesh-monitor/pic_max3748_small.jpg
diff --git a/blog/paper-sampling-mesh-monitor/pic_pi3hdx_small.jpg b/content/blog/paper-sampling-mesh-monitor/pic_pi3hdx_small.jpg
similarity index 100%
rename from blog/paper-sampling-mesh-monitor/pic_pi3hdx_small.jpg
rename to content/blog/paper-sampling-mesh-monitor/pic_pi3hdx_small.jpg
diff --git a/blog/paper-sampling-mesh-monitor/pic_tdp0604_small.jpg b/content/blog/paper-sampling-mesh-monitor/pic_tdp0604_small.jpg
similarity index 100%
rename from blog/paper-sampling-mesh-monitor/pic_tdp0604_small.jpg
rename to content/blog/paper-sampling-mesh-monitor/pic_tdp0604_small.jpg
diff --git a/content/blog/pixacao/example-2026-05-30.svg b/content/blog/pixacao/example-2026-05-30.svg
new file mode 100644
index 0000000..4e5a9eb
--- /dev/null
+++ b/content/blog/pixacao/example-2026-05-30.svg
@@ -0,0 +1,644 @@
+
diff --git a/content/blog/pixacao/index.rst b/content/blog/pixacao/index.rst
new file mode 100644
index 0000000..77175da
--- /dev/null
+++ b/content/blog/pixacao/index.rst
@@ -0,0 +1,39 @@
+---
+title: "New Artwork: Pixação Experiments"
+date: 2026-05-30T09:44:44+02:00
+summary: >
+ I published a piece of algorithmic art fusing a Pixação graffiti-inspired lettering style with a layout similar to a
+ Thai Haw-taew (five row) Yantra blessing tattoo.
+---
+
+.. raw:: html
+
+
+
+
+
+I made a small piece of algorithmic art fusing a Pixação graffiti-inspired lettering style with a layout similar to a
+Thai Haw-taew (five row) Yantra blessing tattoo because it looks cool. Please have a look at `the live version here.
+`__
+
+The artwork encodes the latest `NIST Randomness Beacon`_ at the time of viewing, so it's the same for everyone viewing
+it simultaneously but it changes unpredictably roughly every 1-3 minutes. The beacon's hexadecimal content is inserted
+into the artwork with a Pixação-inspired font that I created. The artwork is loosely based on a Thai buddhist Sak Yant
+tattoo, but deviates from it in some details because it's not intended to be a buddhist spiritual artifact.
+
+Keeping with the spirit of the five row Sak Yant tattoo it is inspired by, this artwork also conveys a blessing.
+However, where a five row Sak Yant tattoo blesses its wearer, this artwork is meant to bless your computer when you
+print it out and place it near it. Its blessing provides protection from unforseen circumstances by encoding the most
+unforseen of things: 120 bit of the 512 bit of entropy in a NIST v2.0 interoperable randomness beacon.
+
+If you enjoy it, please feel free to share it with your friends. If you print it and you want to share, I'd love to see
+a photo of it. You can reach me through my email or on mastodon.
+
+`Here's the live version of the artwork. `__
+
+.. _`NIST Randomness Beacon`: https://csrc.nist.gov/Projects/interoperable-randomness-beacons/beacon-20
+
diff --git a/content/blog/private-contact-discovery/index.rst b/content/blog/private-contact-discovery/index.rst
new file mode 100644
index 0000000..386bd6e
--- /dev/null
+++ b/content/blog/private-contact-discovery/index.rst
@@ -0,0 +1,37 @@
+---
+title: "Private Contact Discovery"
+date: 2019-06-22T10:30:00+08:00
+summary: >
+ I gave a short introduction into Private Contact Discovery protocols at our university workgroup.
+---
+
+Private Contact Discovery (PCD) is the formal name for the problem modern smartphone messenger applications have on
+installation: Given a user's address book, find out which of their contacts also use the same messenger without the
+messenger's servers learning anything about the user's address book. The widespread non-private way to do this is to
+simply upload the user's address book to the app's operator's servers and do an SQL JOIN keyed on the phone number field
+against the database of registered users. People have tried sprinkling some hashes over these phone numbers in an
+attempt to improve privacy, but obviously running a brute-force preimage attack given a domain of maybe a few billion
+valid inputs is not cryptographically hard.
+
+Private Contact Discovery can be phrased in terms of Private Set Intersection (PSI), the cryptographic problem of having
+two parties holding one set each find the intersection of their sets without disclosing any other information. PSI has
+been an active field of research for a while and already yielded useful results for some use cases. Alas, none of those
+results were truly practical yet for usage in PCD in a typical messenger application. They would require too much CPU
+time or too much data to be transferred.
+
+At USENIX Security 2019, Researchers from technical universities Graz and Darmstadt published a paper titled *Private
+Contact Discovery at Scale*
+(`eprint `__ | `PDF `__).
+In this paper, they basically optimize the hell out of existing cryptographic solutions to private contact discovery,
+jumping from a still-impractical state of the art right to practicality. Their scheme allows a client with 1k contacts
+to run PCD against a server with 1B contacts in about 3s on a phone. The main disadvantage of their scheme is that it
+requires the client to in advance download a compressed database of all users, that clocks in at about 1GB for 1B users.
+
+I found this paper very interesting for its immediate practical applicability. As an excuse to dig into the topic some
+more, I gave a short presentation at my university lab's research seminar on this paper
+(slides: `PDF `__ | `ODP `__).
+
+Even if you're not working on secure communication systems on a day-to-day basis this paper might interest you. If
+you're working with social account information of any kind I can highly recommend giving it a look. Not only might your
+users benefit from improved privacy, but your company might be able to avoid a bunch of data protection and
+accountability issues by simply not producing as much sensitive data in the first place.
diff --git a/blog/private-contact-discovery/mori_semi_psi_talk.odp b/content/blog/private-contact-discovery/mori_semi_psi_talk.odp
similarity index 100%
rename from blog/private-contact-discovery/mori_semi_psi_talk.odp
rename to content/blog/private-contact-discovery/mori_semi_psi_talk.odp
diff --git a/blog/private-contact-discovery/mori_semi_psi_talk.pdf b/content/blog/private-contact-discovery/mori_semi_psi_talk.pdf
similarity index 100%
rename from blog/private-contact-discovery/mori_semi_psi_talk.pdf
rename to content/blog/private-contact-discovery/mori_semi_psi_talk.pdf
diff --git a/content/blog/serial-protocols/index.rst b/content/blog/serial-protocols/index.rst
new file mode 100644
index 0000000..2f9bb2d
--- /dev/null
+++ b/content/blog/serial-protocols/index.rst
@@ -0,0 +1,249 @@
+---
+title: "How to talk to your microcontroller over serial"
+date: 2018-05-19T08:09:46+02:00
+---
+
+Scroll to the end for the `TL;DR `_.
+
+In this article I will give an overview on the protocols spoken on serial ports, highlighting common pitfalls. I will
+summarize some points on how to design a serial protocol that is simple to implement and works reliably even under error
+conditions.
+
+If you have done low-level microcontroller firmware you will regularly have had to stuff some data up a serial port to
+another microcontroller or to a computer. In the age of USB, an old-school serial port is still the simplest and
+quickest way to get communication to a control computer up and running. Integrating a ten thousand-line USB stack into
+your firmware and writing the necessary low-level drivers on the host side might take days. Poking a few registers to
+set up your UART to talk to an external hardware USB to serial converter is a matter of minutes.
+
+This simplicity is treacherous, though. Oftentimes, you start writing your serial protocol as needs arise. Things might
+start harmless with something like ``SET_LED ON\n``, but as the code grows it is easy to end up in a hot mess of command
+modes, protocol states that breaks under stress. The ways in which serial protocols break are manifold. The simplest one
+is that at some point a character is mangled, leading to both ends of the conversation ending up in misaligned protocol
+states. With a fragile protocol, you might end up in a state that is hard to recover from. In extreme cases, this leads
+to code such as `this gem`_ performing some sort of arcane ritual to get back to some known state, and all just because
+someone did not do their homework. Below we'll embark on a journey through the lands of protocol design, exploring the
+facets of this deceptively simple problem.
+
+.. _`this gem`: https://github.com/juhasch/pyBusPirateLite/blob/dece35f6e421d4f6a007d1db98d148e2f2126ebb/pyBusPirateLite/base.py#L113
+
+Text-based serial protocols
+===========================
+
+The first serial protocol you've likely written is a human-readable, text-based one. Text-based protocols have the big
+advantage that you can just print them on a terminal and you can immediately see what's happening. In most cases you can
+even type out the protocol with your bare hands, meaning that you don't really need a debugging tool beyond a serial
+console.
+
+However, text-based protocols also have a number of disadvantages. Depending on your application, these might not matter
+and in many cases a text-based protocol is the most sensible solution. But then, in some cases they might and it's good
+to know when you hit one of them.
+
+Problems
+--------
+
+Low information density
+~~~~~~~~~~~~~~~~~~~~~~~
+
+Generally, you won't be able to stuff much more than four or five bit of information down a serial port using a
+single byte of a human-readable protocol. In many cases you will get much less. If you have 10 commands that are only
+issued a couple times a second nobody cares that you spend maybe ten bytes per command on nice, verbose strings such as
+``SET LED``. But if you're trying to squeeze a half-kilobyte framebuffer down the line you might start to notice the
+difference between hex and base-64 encoding, and a binary protocol might really be more up to the job.
+
+Complex parsing code
+~~~~~~~~~~~~~~~~~~~~
+
+On the computer side of thing, with the whole phalanx of an operating system, the standard library of your programming
+language of choice and for all intents and purposes unlimted CPU and memory resources to spare you can easily parse
+anything spoken on a serial port in real time, even at a blazing fast full Megabaud. The microcontroller side however is
+an entirely different beast. On a small microcontroller, printf_ alone will eat about half your flash. On most small
+microcontrollers, you just won't get a regex library even though it would make parsing textual commands *so much
+simpler*. Lacking these resources, you might end up hand-knitting a lot of low-level C code to do something seemingly
+simple such as parsing ``set_channel (13, 1.1333)\n``. These issues have to be taken into account in the protocol design
+from the beginning. For example, you don't really need matching parentheses, don't use them.
+
+Fragile protocol state
+~~~~~~~~~~~~~~~~~~~~~~
+
+Say you have a ``SET_DISPLAY`` command. Now say your display can display four lines of text. The obvious approach to this
+is probably the SMTP_ or HTTP_ way of sending ``SET_DISPLAY\nThis is line 1\nThis is line 2\n\n``. This would certainly
+work, but it is very fragile. With this protocol, you're in trouble if at any point the terminating second newline
+character gets mangled (say, someone unplugs the cable, or the control computer reboots, or a cosmic ray hits something
+and ``0x10 '\n'`` turns into ``0x50 'P'``).
+
+.. _SMTP: https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol
+.. _HTTP: https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol
+
+Timeouts don't work
+~~~~~~~~~~~~~~~~~~~
+
+You might try to solve the problem of your protocol state machine tangling up with a timeout. "If I don't get a valid
+command for more than 200ms I go back to default state." But consider the above example. Say, your control computer
+sends a ``SET_DISPLAY`` command every 100ms. If in one of them the state machine tangles up, the parser hangs since the
+timeout is never hit, because a new line of text is arriving every 100ms.
+
+Framing is hard
+~~~~~~~~~~~~~~~
+
+You might also try to drop the second newline and using a convention such as ``SET_DISPLAY`` is followed by two lines of
+text, then commands resume.". This works as long as your display contents never look like commands. If you are only ever
+displaying the same three messages on a character LCD that might work, but if you're displaying binary framebuffer
+data you've lost.
+
+Solutions
+---------
+
+Keep the state machine simple
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+In a text-based protocol, always use a single line of text to represent a single command. Don't do protocol states or
+modes where you can toggle between different interpretations for a line. If you have to send human-readable text as part
+of a command (such as ``SET_DISPLAY``), escape it so it doesn't contain any newlines.
+
+This way, you keep your protocol state machine simple. If at any time your serial trips and flips a bit or looses a byte
+your protocol will recover on the next newline character, returning to its base state.
+
+Encode numbers in hex when possible
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Printing a number in hexadecimal is a very tidy operation, even on the smalest 8-bit microcontrollers. In contrast,
+printing decimal requires both division and remainder in a loop which might get annoyingly code- and time-intensive on
+large numbers (say a 32-bit int) and small microcontrollers.
+
+If you have to send fractional values, consider their precision. Instead of sending a 12 bit ADC result as a 32-bit
+float formatted like ``0.176513671875`` sending ``0x2d3`` and dividing by 4096 on the host might be more sensible. If you
+really have to communicate big floats and you can't take the overhead of including both printf_ and scanf_ you can
+use hexadecimal floating point, which is basically ``hex((int)foo) + "." + hex((int)(65536*(foo - (int)foo)))`` for four
+digits. You can also just hex-encode the binary IEEE-754_ representation of the float, sending ``hex(*(int *)&float)``.
+Most programming languages will have a `simple, built-in means to parse this sort of thing
+`__.
+
+.. _printf: http://git.musl-libc.org/cgit/musl/tree/src/stdio/vfprintf.c
+.. _scanf: http://git.musl-libc.org/cgit/musl/tree/src/stdio/vfscanf.c
+.. _IEEE-754: https://en.wikipedia.org/wiki/IEEE_754
+
+Escape multiline strings
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+If you have to send arbitrary strings, escape special characters. This not only has the advantage of yielding a robust
+protocol: It also ensures you can actually see everything that's going on when debugging. The string ``"\r\n"`` is easy to
+distinguish from ``"\n"`` while your terminal emulator might not care.
+
+The simplest encoding to use is the C-style backslash encoding. Host-side, most languages will have a `built-in means of
+escaping a string like that `__.
+
+Encoding binary data
+--------------------
+
+For binary data, hex and base-64 are the most common encodings. Since hex is simpler to implement I'd go with it unless
+I really need the 30% bandwidth improvement base-64 brings.
+
+Binary serial protocols
+=======================
+
+In contrast to anything human-readable, binary protocols are generally more bandwidth-efficient and are easier to format
+and parse. However, binary protocols come with their own version of the caveats we discussed for text-based protocols.
+
+The framing problem in binary protocols
+---------------------------------------
+
+The most basic problems with binary protocols as with text-based ones is framing, i.e. splitting up the continuous
+serial data stream into discrete packets. The issue is that it is that you have to somehow mark boundaries between
+frames. The simplest way would be to use some special character to delimit frames, but then any 8-bit character you
+could choose could also occur within a frame.
+
+SLIP/PPP-like special character framing
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Some protocols solve this problem much like we have solved it above for strings in line-based protocols, by escaping any
+occurence of the special delimiter character within frames. That is, if you want to use ``0x00`` as a delimiter, you would
+encode a packet containing ``0xde 0xad 0x00 0xbe 0xef`` as something like ``0xde 0xad 0x01 0x02 0xbe 0xef``, replacing the
+null byte with a magic sequence. This framing works, but is has one critical disadvantage: The length of the resulting
+escaped data is dependent on the raw data, and in the worst case twice as long. In a raw packet consisting entirely of
+null bytes, every byte must be escaped with two escape bytes. This means that in this case the packet length doubles,
+and in this particular case we're even less efficient than base-64.
+
+Highly variable packet length is also bad since it makes it very hard to make any timing guarantees for our protocol.
+
+9-bit framing
+~~~~~~~~~~~~~
+
+A framing mode sometimes used is to configure the UARTs to transmit 9-bit characters and to use the 9th bit to designate
+control characters. This works really well, and gives plenty of control characters to work with. The main problem with
+this is that a 9-bit serial interface is highly nonstandard and you need UARTs on both ends that actually support this
+mode. Another issue is that though more efficient than both delmitier-based and purely text-based protocols, it still
+incurs an extra about 10% of bandwidth overhead. This is not a lot if all you're sending is a little command every now
+and then, but if you're trying to push large amounts of data through your serial it's still bad.
+
+COBS
+~~~~
+
+Given the limitations of the two above-mentioned framing formats, we really want something better. The `Serial Line
+Internet Protocol (SLIP)`_ as well as the `Point to Point Protocol (PPP)`_, standardized in 1988 and 1994 respectively,
+both use escape sequences. This might come as a surprise, but humanity has actually still made significant technological
+progress on protocols for 8-bit serial interfaces until the turn of the millennium. In 1999, `Consistent Overhead Byte
+Stuffing (COBS)`_ (`wiki `__) was published by a few
+researchers from Apple Computer and Stanford University. As a reaction on the bandwidth doubling problem present in
+PPP_, COBS *always* has an overhead of a single byte, no matter what or how long a packet's content is.
+
+COBS uses the null byte as a delimiter interleaves all the raw packet data and a `run-length encoding`_ of the non-zero
+portions of the raw packet. That is, it prepends the number of bytes until the first zero byte to the packet, plus one.
+Then it takes all the leading non-zero bytes of the packet, unmodified. Then, it again encodes the distance from the
+first zero to the second zero, plus one. And then it takes the second non-zero run of bytes unmodified. And so on. At
+the end, the packet is terminated with a zero byte.
+
+The result of this scheme is that the encoded packet does not contain any zero bytes, as every zero byte has been
+replaced with the number of bytes until the next zero byte, plus one, and that can't be zero. Both formatter and parser
+each have to keep a counter running to keep track of the distances between zero bytes. The first byte of the packet
+initializes that counter and is dropped by the parser. After that, every encoded byte received results in one raw byte
+parsed.
+
+While this might sound more complicated than the escaping explained above, the gains in predictability and efficiency
+are worth it. An implementation of encoder and decoder should each be about ten lines of C or two lines of Python. A
+minor asymmetry of the protocol is that while decoding can be done in-place, encoding either needs two passes or you
+need to scan forward for the next null byte.
+
+.. _`Point to Point Protocol (PPP)`: https://en.wikipedia.org/wiki/Point-to-Point_Protocol
+.. _PPP: https://en.wikipedia.org/wiki/Point-to-Point_Protocol
+.. _`Serial Line Internet Protocol (SLIP)`: https://en.wikipedia.org/wiki/Serial_Line_Internet_Protocol
+.. _`Consistent Overhead Byte Stuffing (COBS)`: http://www.stuartcheshire.org/papers/COBSforToN.pdf
+.. _`Point-to-Point Protocol (PPP)`: https://en.wikipedia.org/wiki/Point-to-Point_Protocol
+.. _`run-length encoding`: https://en.wikipedia.org/wiki/Run-length_encoding
+
+State machines and error recovery
+---------------------------------
+
+In binary protocols even more than in textual ones it is tempting to build complex state machines triggering actions on
+a sequence of protocol packets. Please resist that temptation. As with textual protocols keeping the protocol state to
+the minimum possible allows for a self-synchronizing protocol. A serial protocol should be designed such that if due to
+a dropped packet or two both ends will naturally re-synchronize within another packet or two. A simple way of doing that
+is to always transmit one semantic command per packet and to design these commands in the most idempotent_ way possible.
+For example, when filling a framebuffer piece by piece, include the offset in each piece instead of keeping track of it
+on the receiving side.
+
+.. _idempotent: https://en.wikipedia.org/wiki/Idempotence#Computer_science_meaning
+
+Conclusion
+==========
+
+Here's your five-step guide to serial bliss:
+
+1. Unless you have super-special requirements, always use the slowest you can get away with from 9600Bd, 115200Bd or
+ 1MBd. 8N1 framing if you're talking to anything but another microcontroller on the same board. Using common values
+ like these makes it easier when you'll inevitably have to guess these at some point in the future ;)
+2. If you're doing something simple and speed is not a particular concern, use a human-readable text-based protocol. Use
+ one command/reply per line, begin each line with some sort of command word and format numbers in hexadecimal. Bonus
+ points for the device replying to unknown commands with a human-readable status message and printing a brief protocol
+ overview on boot.
+3. If you're doing something even slightly nontrivial or need moderate throughput (>1k commands per second or >20 byte of
+ data per command) use a COBS-based protocol. A good starting point is a ``[target MAC][command ID][command
+ arguments]`` packet format for multidrop busses. For single-drop you may decide to drop the MAC address.
+4. Always include some sort of "status" command that prints life stats such as VCC, temperature, serial framing errors
+ and uptime. You'll need some sort of ping command anyway and that command might as well do something useful.
+5. If at all possible, keep your protocol context-free across packets/lines. That is, a certain command should always be
+ self-contained, and no command should change the meaning of the next packet/line/command that is sent. This is really
+ important to allow for self-synchronization. If you really need to break up something into multiple commands, say you
+ want to set a large framebuffer in pieces, do it in a idempotent_ way: Instead of sending something like ``FRAMEBUFFER
+ INCOMING:\n[byte 0-16]\n[byte 17-32]\n[...]\nEND OF FRAME`` rather send ``FRAMEBUFFER DATA FOR OFFSET 0: [byte
+ 0-16]\nFRAMEBUFFER DATA FOR OFFSET 17: [byte 17-32]\n[...]\nSWAP BUFFERS\n``.
+
diff --git a/content/blog/sybil-resistance-identity/images/succulents.jpg b/content/blog/sybil-resistance-identity/images/succulents.jpg
new file mode 100755
index 0000000..938bffd
Binary files /dev/null and b/content/blog/sybil-resistance-identity/images/succulents.jpg differ
diff --git a/content/blog/sybil-resistance-identity/index-old.rst b/content/blog/sybil-resistance-identity/index-old.rst
new file mode 100644
index 0000000..734cc5a
--- /dev/null
+++ b/content/blog/sybil-resistance-identity/index-old.rst
@@ -0,0 +1,246 @@
+---
+title: "Theia Attack Resistance and Digital Identity"
+date: 2020-09-09T15:00:00+02:00
+draft: true
+---
+
+.. raw:: html
+
+
+
+
+Theia in Cyberspace
+===================
+
+In informatics, the term *distributed system* is used to describe the aggregate behavior of a complex network made up of
+individual computers. For decades, computer scientists to some success have been trying to figure out how exactly the
+individual computers that make up such a distributed system need to be programmed for the resulting amalgamation to
+behave in a predictable, maybe even a desirable way. Though seemingly simple on its surface, this problem has a
+surprising depth to it that has yielded research questions for a whole field for several decades now. One particular
+as-of-yet unsolved problem is resistance against *theia attacks* (or "sybil" attacks in older terminology).
+
+ Named after the 1973 book by Flora Rheta Schreiber on dissociative identity disorder, a sybil attack is an
+ attack where one computer in a distributed system pretends to be multiple computers to gain an advantage. From your
+ author's standpoint, naming a type of computer security attack after a medical condition was an unfortunate choice.
+ For this reason this post uses the term *Theia attack* to refer to the same concept. Theia is a greek godess of light
+ and glitter and the name alludes to the attacker performing something alike an optical illusion, causing the attacked
+ to perceive multiple distinct images that in the end are all only reflections of the same attacker.
+
+The core insight of computer science research on theia attacks is that there cannot be any technological way of
+preventing such an attack, and any practical countermeasure must be grounded in some authority or ground truth that is
+external to the systems—bridging from technology to its social or political context.
+
+Looking around, we can see a parallel between this question ("which computer is a real computer?") and a social issue
+that recently has been growing in importance: Just like computers can pretend to be other computers, they can also
+pretend to be humans. As can humans. Be it within the context of election manipulation or down-to-earth astroturfing_
+the recurring issue is that in today's online communities, it is hard for an individual to tell who of their online
+acquaintances are who they seem to be. Different platforms attempt different solutions to this problem, and all fail in
+some way or another. Facebook employs good old snitching, turning people against each other and asking them "Do you know
+this person?". Twitter is more laid-back and avoids this Stasi_ methodology in favor of requiring a working mobile phone
+number from its subjects, essentially short-circuiting identity verification to the phone company's check of their
+subscriber's national passport.
+
+.. the preceding is a simplified representation of these platform's practices. In particular facebook uses several
+ methods depending on the case. I think this abbreviated discussion should be ok for the sake of the argument. I am
+ not 100% certain on the accuracy on the accuracy of the statement though. Does fb still do the snitching thing? Is
+ twitter usually content with a phone number?
+
+Trusting Crypto-Anarchist Authorities
+=====================================
+
+Beyond these centralistic solutions to the problem, crypto-anarchists and anarcho-capitalists have been brewing on some
+interesting novel approaches to online identity based on *blockchain* distributed ledger technology. Distributed
+ledgers are a distributed systems design pattern that yields a system that works like an append-only logbook.
+Participants can create new entries in this logbook, but no one—neither the original author, nor other participants—can
+retroactively change a logbook entry once it has been written. In the blockchain model, past entries are essentially
+written into stone. This near-perfect immutability is what opens them for a number of use cases from cryptographic
+pseudo-currencies [#cryptocurrency]_.
+
+An overview over a variety of these unconventional blockchain identity verification approaches can be found in `this
+unpublished 2020 survey by Siddarth, Ivliev, Siri and Berman `_.
+They walk their readers through a number of different projects that try to solve the question "Is this human who they
+pretend to be?" using joint socio-technological approaches. In the following few sections, you may find a short outline
+of a small selection of them. The conlusion of this post will be a commentary on these approaches, and on the underlying
+problem of identity in a digital world.
+
+.. BrightID
+
+In one scheme, identity is determined by "notary" computers that aggregate large amounts of information on a user's
+social contacts. These computers then run an algorithm derived from the SybilGuard_, SybilLimit_ and SybilInfer_ lineage
+of random-walk based algorithms. These algorithms assume that authentic social graphs are small world graphs: Everyone
+knows everyone else through a friend's friend's friend. They also assume that there is an upper bound on how many
+connections with authentic users an attacker can forge: Anyone who is not embedded into the graph well enough is cut
+out. Like this, they put an upper limit on the number of theia identites an attacker can assume given a certian number
+of connections to real people.
+
+Disregarding the catastrophic privacy issues of storing large amounts of data on social relationships on someone else's
+computer, this second assumption is where this model unfortunately breaks down. Applying common sense, it is completely
+realistic for an attacker to forge a large number of social connections: This is precisely what most of social media
+marketing is about! A more malicious angle on this would be to consider how in meatspace [#meatspacefn]_ multi-level
+marketing schemes are successful in coaxing people to abuse their social graphs to disastrous consequences to the
+well-being of themselves and others. Similar schemes would certainly be possible in cyberspace as well. An additional
+point to consider is that the upper limit SybilGuard_ and others place on the number of fake identities one can have is
+simply not that strict at all. An attacker could still get away with a reasonable number of false identities before
+getting caught by any such algorithm.
+
+.. Duniter
+
+In another scheme, identity is awarded to anyone who can convince several people already in the network to vouch for
+them, and who is at most a few degrees removed from one of several pre-determined celebrities. Apart from again being
+vulnerable to conmen and other scammers, this system has the glaring flaw of roundly refusing to recognize any person
+who is not willing or able to engage with multiple of its members. Along with the system's informal requirement for
+members to only vouch for people they have physically met this leads to a nonstarter in a cyberspace that grown
+specifically *because* it transcends national borders and physical distance—two most serious obstacles to in-person
+communication.
+
+.. Idena Network
+
+The last scheme I will outline in this post is based around a set of `Turing tests`_; that is, quizzes that are designed
+to tell apart man and machine. In this system, all participants have to simultaneously undergo a Turing test once in a
+fortnight. The idea is that this limits the number of theia identities an attacker can assume since they can only solve
+that many Turing tests at the same time. The system uses a particular type of picture classification-based Turing test
+and does not seem to be designed with the blind or mentally disabled in mind with accessibility concerns nowhere to be
+found in the so-called "manifesto" published by its creators. But even ignoring that, the system obviously fails at an
+even more basic level: The idea that everyone takes a Turing test at the same time only works in a world without time
+zones. Or jobs for that matter. Also, it assumes that an attacker cannot simply hire a small army of people someplace
+else to fool the system.
+
+.. _SybilLimit: https://www.comp.nus.edu.sg/~yuhf/yuh-sybillimit.pdf
+.. _SybilGuard: http://www.math.cmu.edu/~adf/research/SybilGuard.pdf
+.. _SybilInfer: https://www.princeton.edu/~pmittal/publications/sybilinfer-ndss09.pdf
+.. _`Turing Tests`: https://en.wikipedia.org/wiki/Turing_test
+
+Identity between Cyberspace and Meatspace
+=========================================
+
+A common thread in these solutions, from the Facebook'esque Stasi_ methods to the crypto-anarchist challenge-response
+utopias, is that they all approach digital identity as a question of Objective Truth™ that can unanimously be decided at
+a system level—or that can be externalized to the next larger system such as the state. Alas, the important question
+remains unasked:
+
+ What *is* identity?
+
+The answer to this question certainly depends on the system being examined. For example, an important reason the
+capitalist corporations mentioned above require knowledge about their users' identity is to generate plausible
+statistics for the advertisers that form their customer base, similar to how a farmer will keep statics on yield and
+quality for the buyers of his crop. With this background, a full decoupling of platform accounts from a notion of legal
+identity seems at odds with the platform's business model—and we will have to adjust our expectations for reform
+accordingly.
+
+A common thread among all systems mentioned above is that they all have a social component to them. For this common use
+case of social systems, I want to make a suggestion on how we can approach digital identity in a more practical, less
+discriminatory [#discriminatory]_ manner than any of the methods we discussed above. I think both using people's social
+connections and proxying the decisions of external authorities such as the state are bad systems to decide who is a
+person and who is not. I will now illustrate this point a bit. Let us think about how many digital identities a human
+beign might have. First, consider the case of n=0, someone who simply wants no business with the system at all. For
+simplicity, let us assume that we have solved this issue of consent, i.e. every person who is identified by the system
+consents to this practice. For n=1, the approaches outlined above all provide some approximate solution. States may not
+grant every human sufficient ID (e.g. children, the mentally disabled or prisoners might be left out), and the social
+systems might fail to catch people who simply do not have any friends, but otherwise their approximations hold. Maybe.
+But what about n=2, n=3, ...? None of these systems adequately consider cases where a human being might legitimately
+wish to hold multiple digital identities, non-maliciously.
+
+Consider a hypothetical lesbian, conservative politician. An active social media presence is a core component of a
+modern politician's carreer. At the same time, "conservative homophobe" is still well within the realm of tautology and
+it would be legitimate for this politician to wish to not disclose a large fraction of their private life to the world
+at large. They might have a separate online identity for matters related to it. For this politician, the social
+relationship-based systems referenced above would either incorporate outing as a design feature, or they would force
+the politician to choose either of their two identities: To choose between private life and carreer. When deferring to
+the state as the decider over personhood, at least the platform's operator would know about the outrageously sensitive
+link between the politician's online identities. Clearly, no such solution can be considered socially just.
+
+Let us try not to be caught up on saving the world at this point. The issue of conservative homophobia is out of the
+scope of our consideration, and it is not one that anyone can solve in the near future. Magical realism aside, least of
+all can some technological thing beckon this change. There is a case for legitimate uses of multiple, separate digital
+identities, and we do not have a technical or political answer to it. All hope is not lost yet, though. We can easily
+undo this gordian knot by acknowledging an unspoken assumption that underlies any social relationships between real
+people, past the procrustean bed of computer systems or organizational structures these relationships are cast into.
+
+ As a function of social interaction, digital identities conform to roles_ in sociological terminology, and are not
+ at all the same as personhood_. Roles are subjective and arise from a relationship between people, and a single
+ person might legitimately perform different roles depending on context.
+
+When computer scientists or programmers are creating new systems, there always is an (often implicit) modelling stage.
+Formally, during this stage a domain expert and a modeller with a computer science background come together, each
+contributing their knowledge to form a model that is both appropriate for real-world use and practical from an
+engineering point of view. In practice, these two roles are often necessarily fulfilled by the same person, who is often
+also the programmer of the thing. This leads to many computer systems using poor models. A typical example of this issue
+are systems requiring a person's name that use three input fields labelled "First Name", "Middle Initial" and "Last
+Name". These systems are often created by US-American programmers, who are used to this naming schema from their lived
+experience. Unfortunately, this schema breaks down for those few billion people who use their last name first, who have
+more than one middle name, or who have multiple given names and do not normally use the first one of those.
+
+Once a system creator's implicit assumptions have been encoded into the system like this, it is often very hard to get
+out of that situation. A pattern to use during careful modelling is to keep the model flexible to account for unforeseen
+corner cases. For example, when modelling a system requiring a person's name, one would have to ask what the name is
+used for. It may be the most sensible decision to simply ask the user for their name twice: Once in first name/last name
+format for e.g. tax purposes, and once with a free-form text field for e.g. displaying on their account page.
+
+While for names, many systems already use some form of flexible model by e.g. having a *handle* or *nickname* separate
+from the *display name*, "social" systems still often are stuck with an identity model based around a concept of a
+single, rigid identity. In practice, people perform different roles_ in different circumstances. When asking for a
+person's identity, one would get wildly different answers from different people. A person's identity as perceived by
+others is coupled to their relationship more than to some underlying, biological or administrative truth. Thinking back
+to the straw man politician above, this is evident in subtle ways in almost all our everyday relationships: Some people
+may know me by my legal name, some by my online nickname. To some I may be a computer scientist, to some a flatmate.
+None of my friends and acquaintances have ever wanted to see my passport, or asked to take my DNA to ascertain that I am
+a distinct human being from the other humans they know. Likewise, identifying me by my social connections is impractical
+as it would require an exceedingly weird amount of what can only be described as snooping. Yet, this concept of a
+single, consistent, global, true identity is exactly what up to now all technological solutions to the identity problem
+are trying to achieve.
+
+Building Bridges
+================
+
+I think I can offer you one main take-aways from the discussion above.
+
+ During modelling social systems, focus on relationships—not identity.
+
+Rephrased into more actionable points, as someone designing a social digital system, do the following:
+
+0. Early in the design stages, take the time to consider fundamental modelling issues like this one. If you don't, you
+ will likely get stuck with a sub-optimal model that will be hard to get rid of.
+1. Where possible, be flexible. Allow people to chose their own identifier. Don't require them to use their real names,
+ they may not wish to disclose those or they may not be in a format that is useful to you (they may be too long, too
+ short, too ubiquituous, in foreign characters etc.). A free-form text field with a reasonable length limit is a good
+ approach here.
+2. Do not use credit cards or phone numbers to identify people. There are many people who do not have either, and
+ scammers can simply buy this data in bulk on the darknet.
+3. Allow people to create multiple identites [#accountswitchopsec]_, and acknowledge the role of social relationships in
+ your interaction features. People have very legitimate reasons to separate areas of their lifes, and it is not for
+ you or your computer to decide who is who to whom. If your thing requires a global search function, re-consider the
+ data protection aspects of your system. If you want to encourage social functions in the face of bots and trolls,
+ make it easy for people to share their identities out-of-band, such as through a QR code or a copy-and-pasteable
+ short link. If you require someone's legal name or address for billing purposes, unify these identities behind the
+ scenes if at all and allow them to act as if fully independent in public.
+
+While change of perspective comes with its share of user experience challenges, but also with a promise for a more
+human, more dignified online experience. Perhaps we can find a way to adapt cyberspace to humans, instead of continuing
+trying it the other way around.
+
+.. _astroturfing: https://en.wikipedia.org/wiki/Astroturfing
+.. _Stasi: https://en.wikipedia.org/wiki/Stasi
+
+.. [#cryptocurrency] Pseudo-currencies in that, while they provide some aspects of a regular currency such as ownership
+ and transactions, they lack most others. Traditional currencies are backed by states, regulated by central banks
+ tasked with maintaining their stability and ultimately provide accountability through law enforcement, courts
+ and political elections.
+
+.. [#discriminatory] Discriminatory as in discriminating against minorities, but also as in deciding what is and what is
+ not.
+
+.. [#accountswitchopsec] This does mean that you should not actively prevent people from creating multiple accounts. It
+ does not necessarily entail building a proper user interface around this practice. If you do the latter, e.g. by
+ offering a "switch identity" button or an identiy drop-down menu on a post submission form, you can easily
+ encourage slip-ups that might disclose the connection between two identities, and you make it possible for
+ someone hacking a single login to learn about this connection as well.
+
+.. [#meatspacefn] Meatspace_ is where people physically are, as opposed to cyberspace
+
+.. _Meatspace: https://dictionary.cambridge.org/dictionary/english/meatspace
+.. _roles: https://en.wikipedia.org/wiki/Role
+.. _personhood: https://en.wikipedia.org/wiki/Personhood
diff --git a/content/blog/sybil-resistance-identity/index.rst b/content/blog/sybil-resistance-identity/index.rst
new file mode 100644
index 0000000..3c9e008
--- /dev/null
+++ b/content/blog/sybil-resistance-identity/index.rst
@@ -0,0 +1,90 @@
+---
+title: "Identity between Cyberspace and Meatspace"
+date: 2020-09-09T15:00:00+02:00
+draft: true
+---
+
+.. raw:: html
+
+
+
+Identity in Cyberspace
+======================
+
+.. Identity is a frequent problem
+.. Easy solutions abound
+.. Precise modelling is uncommon
+.. True identity is sensitive, hard to handle
+..
+.. Often, conversational features emphasized -> true identity is unnecessary
+.. Social role theory
+.. Call to action
+
+Most computer systems that interface with humans have a concept of user identity. The data structures used for its
+storage vary, but usually one *account* corresponds to one human *user*. In many applications, the system operator tries
+to ensure that one user cannot create multiple accounts. In online social networks, astrotufing_ and trolling are easier
+to fight when limits are imposed on account creation. In online stores, fraud prevention means the store operator needs
+their customers legal identity and the operator must be able to ban offending customers. In mobile messaging systems,
+users have to be able to find each other by some identifier such as name or phone number, and this identifier has to be
+unique and hard to forge.
+
+Today, in systems that allow anyone to create an account have largely converged to require either an email address or a
+mobile phone number. Email addresses are used by systems that are less vulnerable to abuse and that are used on laptop
+or desktop computers. Mobile phone numbers are abundantly used in smartphone apps, as well as in systems more prone to
+abuse such as online social networks or ecommerce. Both are easily verified using a confirmation email or SMS.
+
+When designing or programming an online system, it is uncommon that the precise real-world semantics of accounts are
+modelled. Most computer systems use ad-hoc data models. During their creation, their programmers implicit assumptions
+about the world are encoded into these data models. Most of the time this works fine, but it does lead to significant
+blind spots that can make systems break down for a fraction of their users.
+
+Lives in Meatspace
+==================
+
+A consequence of the proliferation of phone numbers being used to identify people is that most people will not be able
+to create multiple accounts. *"That's the point!"* you might say, but while we want to prevent scammers, spammers and
+boored schoolchildren from messing with our systems, everybody else may have legitimate reasons to have more than one
+account.
+
+We can apply sociology's model of roles_ to understand this issue. In sociology, a role is the comprehensive pattern of
+rules and expectations that govern an individual's behavior corresponding to their social position. A key fact is that
+most people occupy mutliple roles. A parent may also be a company employee or a wife and perform accordingly given the
+circumstances. Systems that tie digital identity to legal personhood through the contracts behind phone numbers impede
+their users' attempts at role separation. Effects of this are e.g. that nowadays employers routinely screen applicants'
+social media accounts for unacceptable content.
+
+While this role conflict merely amounts to a minor inconvenience to most there are many to who it poses an existential
+problem. Consider an LGBT+ person living in a repressive country or a politically conservative person living in a
+very liberal city. Both have legitimate reasons to strictly separate parts of their private lives from others. For both,
+much is at stake. Yet, both will have to practically circumvent most online systems registration barriers to implement
+this separation.
+
+Trusting the User
+=================
+
+While there is no single solution to these issues, there are several possible mitigations. The first and most important
+one is to systematically think about the system's data model when creating it. Which assumptions about the real world
+are inherent in it? Are these assumptions likely to cause issues? Ad-hoc models are easily created, but hard to get rid
+of when they start causing problems.
+
+A general guideline on identity should be that hindering trolls by requiring things like phone numbers or credit card
+numbers is very likely to also be an obstacle to many entirely legitimate uses. Captchas_ or invitation links can help
+to keep out the trolls. Another approach is to limit the damage a troll can cause with things like effective moderation
+systems, reputation systems or by limiting the reach of newly created accounts.
+
+Outside of e-commerce, actually tying a digital account to a real-world identity is very rarely necessary. The value of
+a messenger app is not in the names in its contacts list, but the conversations behind these names. When two people meet
+each other on the street, their interaction is shaped by a myriad of social factors—but *not* by them showing each other
+their photo ID.
+
+Humans with their messy identities do not fit today's cyberspace well. Let's adapt cyberspace to humans, instead of
+trying it the other way around.
+
+.. _astroturfing: https://en.wikipedia.org/wiki/Astroturfing
+.. _roles: https://en.wikipedia.org/wiki/Role
+.. _Captchas: https://link.springer.com/content/pdf/10.1007/3-540-39200-9_18.pdf
+
diff --git a/blog/telekom-gpon-sfp/images/edgerouter_interface_config.png b/content/blog/telekom-gpon-sfp/images/edgerouter_interface_config.png
similarity index 100%
rename from blog/telekom-gpon-sfp/images/edgerouter_interface_config.png
rename to content/blog/telekom-gpon-sfp/images/edgerouter_interface_config.png
diff --git a/blog/telekom-gpon-sfp/images/edgerouter_route_config.png b/content/blog/telekom-gpon-sfp/images/edgerouter_route_config.png
similarity index 100%
rename from blog/telekom-gpon-sfp/images/edgerouter_route_config.png
rename to content/blog/telekom-gpon-sfp/images/edgerouter_route_config.png
diff --git a/blog/telekom-gpon-sfp/images/edgerouter_sfp_config.png b/content/blog/telekom-gpon-sfp/images/edgerouter_sfp_config.png
similarity index 100%
rename from blog/telekom-gpon-sfp/images/edgerouter_sfp_config.png
rename to content/blog/telekom-gpon-sfp/images/edgerouter_sfp_config.png
diff --git a/blog/telekom-gpon-sfp/images/edgerouter_snat_config.png b/content/blog/telekom-gpon-sfp/images/edgerouter_snat_config.png
similarity index 100%
rename from blog/telekom-gpon-sfp/images/edgerouter_snat_config.png
rename to content/blog/telekom-gpon-sfp/images/edgerouter_snat_config.png
diff --git a/blog/telekom-gpon-sfp/images/edgerouter_snat_config2.png b/content/blog/telekom-gpon-sfp/images/edgerouter_snat_config2.png
similarity index 100%
rename from blog/telekom-gpon-sfp/images/edgerouter_snat_config2.png
rename to content/blog/telekom-gpon-sfp/images/edgerouter_snat_config2.png
diff --git a/blog/telekom-gpon-sfp/images/sfp_onu_ploam_pw_config.png b/content/blog/telekom-gpon-sfp/images/sfp_onu_ploam_pw_config.png
similarity index 100%
rename from blog/telekom-gpon-sfp/images/sfp_onu_ploam_pw_config.png
rename to content/blog/telekom-gpon-sfp/images/sfp_onu_ploam_pw_config.png
diff --git a/blog/telekom-gpon-sfp/images/sfp_onu_reset.png b/content/blog/telekom-gpon-sfp/images/sfp_onu_reset.png
similarity index 100%
rename from blog/telekom-gpon-sfp/images/sfp_onu_reset.png
rename to content/blog/telekom-gpon-sfp/images/sfp_onu_reset.png
diff --git a/blog/telekom-gpon-sfp/images/sfp_onu_web_if.png b/content/blog/telekom-gpon-sfp/images/sfp_onu_web_if.png
similarity index 100%
rename from blog/telekom-gpon-sfp/images/sfp_onu_web_if.png
rename to content/blog/telekom-gpon-sfp/images/sfp_onu_web_if.png
diff --git a/content/blog/telekom-gpon-sfp/index.rst b/content/blog/telekom-gpon-sfp/index.rst
new file mode 100644
index 0000000..d22894a
--- /dev/null
+++ b/content/blog/telekom-gpon-sfp/index.rst
@@ -0,0 +1,219 @@
+---
+title: "Ubiquiti EdgeRouter on Deutsche Telekom GPON Fiber"
+date: 2022-02-21T20:00:00+01:00
+summary: >
+ Short tutorial on getting a Deutsche Telekom GPON internet connection running using a SFP ONU unit in an Ubiquiti
+ EdgeRouter.
+---
+
+Disclaimer
+==========
+
+I provide this guide as a reference for other knowledgeable users without any warranty. Please feel free to use this as
+a resource but do not hold me responsible if this does not work for you. There is a significant chance that due to an
+error on my side or due to Telekom changing their setup this guide will not work for you, and you may end up having to
+pay for an unsuccessful Telekom technician visit. That is your own risk, and I do not assume any liability.
+
+Tl;dr
+=====
+
+The "Telekom Digitalisierungsbox Glasfasermodem" is a GPON ONT in SFP form factor that works with an Ubiquiti EdgeRouter
+6P's SFP port. You can order it from Telekom or other vendors using the Telekom P/N 40823569 or its EAN 4718937619382.
+It costs about the same as the separate plastic box modem, but saves a lot of space and does not require a separate
+power supply.
+
+To configure, first access the SFP ONT's web interface at ``10.10.1.1`` by configuring your SPF port's IP to static
+``10.10.1.2``. User credentials are either admin/admin or admin/1234. In the web interface, set put PLOAM password into the
+"SLID" setting in ASCII mode, then save & reboot the device. Now, configure PPPoE on the router's SFP port using the
+PPPoE UID ``[anschlusskennung] [zugangsnummer] "#" [mitbenutzernummer] "@t-online.de"`` and your "Persönliches Kennwort" as
+PPPoE password. Set the VLAN to ``7``, and you are good to go.
+
+Background
+==========
+
+I moved into a new apartment that has a fiber internet connection operated by Deutsche Telekom. Having made some poor
+experiences with AVM's FritzBox brand of routers that is commonly used by German carriers, I decided to use my own
+Router instead of the one provided by Deutsche Telekom. Like other German providers, Telekom charges exorbitant amounts
+in monthly fees for their routers, so even though my choice ended up being a high-end piece of commercial equipment I
+will still be cheaper than going with Telekom's much shittier device when added up over a two-year contract period.
+
+The hardware I chose is the Ubiquiti EdgeRouter 6P. This device is from Ubiquiti's commercial lineup and is intended to
+power something like a small branch office of a company. It comes in a small form factor (as opposed to larger rackmount
+units), it does not consume a lot of power, it has five PoE-capable Ethernet ports which I can directly connect up to
+the Ubiquiti Unifi UAP access point that I already have, and it has a powerful configuration interface. It can even
+act as a VPN endpoint!
+
+Telekom's fiber internet offering for residential customers is GPON-based. GPON stands for "Gigabit Passive Optical
+Network" and means that instead of patching through one fiber or pair of fibers to each customer, several customers in
+one building are connected to a single fiber through optical splitters. These optical splitters are passive, i.e. they
+are just fancy pieces of glass and fibers and do not require electrical power. The advantage of GPON is lower initial
+cost for the operator, the disadvantage is that competing providers can only ever hope to get traffic handed through by
+Telekom and will never be able to use their own equipment on the "network" end of the fiber.
+
+Telekom wants you to connect to its fiber network through a small plastic box that they call "modem", and that the rest
+of the world calls "ONT", or Optical Network Terminator. Telekom's ONT has an upstream optical port with an LC
+connector, and a regular RJ45 ethernet port downstream. The "modem" in fact contains an entire linux system that
+terminates the ITU-standard suite of protocols that is used to manage what happens on the fiber, e.g. scheduling of
+transmission slots and adjustment of transmitter laser power.
+
+Looking at Telekom's plastic box ONT and my nice and shiny EdgeRouter, I was not a fan of this solution. Doing some
+research I found out that you can in fact get GPON ONTs in an SFP module form factor. My EdgeRouter has an SFP slot, so
+if I could get one of these that is compatible with Telekom's GPON flavor I could theoretically just plug it into my
+EdgeRouter's SFP slot with no separate power supply needed, saving a lot of space in the process.
+
+Finding a GPON SFP ONT that is compatible with Telekom's network turned out to be the hard part. While there are lots of
+commercial devices that look like they *should be* compatible, I could not be sure and I did not feel like sinking lots
+of money and weeks of trial and error into figuring out which are and which are not. After about half a dozen calls with
+various Telekom customer service departments I found the solution that ultimately ended up working: For their business
+customer fiber internet offering, Telekom uses the same GPON standard, but different ONT equipment. Their router for
+business customers is called "Digitalisierungsbox" and it in fact comes with an SFP GPON ONT. And, as it turns out, you
+can order that SFP GPON ONT separately for about 50 € (the same as the plastic box one) from either Telekom or a number
+of independent online stores. The Telekom part number of the thing is 40823569, the EAN is 4718937619382.
+
+Below is a list of steps that I had to undertake in order to get my EdgeRouter/SFP ONT setup to work.
+
+Hardware Setup
+==============
+
+The hardware setup is really simple. The SFP ONU is plugged into the EdgeRouter's SFP port. The ONU is connected to
+the Telekom Fiber through the LC/APC to SC/APC adapter cable that is included in its package. Telekom's technician will
+install an LC/APC coupler to join both cables. To configure the EdgeRouter, connect yourself through an ethernet cable
+*on port 2*. Ubiquiti's setup wizards assume the WAN interface is either port 1 or the SFP port (port 5), and default to
+use port 2 as their LAN interface even when port 5 is configured as the only WAN port. The default IP for the EdgeRouter
+is ``192.168.1.1``, and the default UID/PW is ubnt/ubnt.
+
+Configuration
+=============
+
+Getting access to the SFP ONU's config interface
+------------------------------------------------
+
+In this section I am assuming you want to configure the SFP ONU while it is plugged into the EdgeRouter from a laptop
+connected to the EdgeRouter's ethernet port 2. To do this, we have to first configure the right IP/subnet on the
+EdgeRouter's SFP interface, then patch connections between the SFP ONU and the laptop through the EdgeRouter.
+
+1. First, inside the EdgeRouter's config interface we need to configure a static IP with accompanying SNAT rule on the
+ SFP port to allow us to access the SFP module's web interface through the laptop connected to the EdgeRouter. For
+ this, configure the eth5 interface (which is the SFP port) to use the static IP ``10.10.1.2/24``.
+
+.. raw:: html
+
+
+
+
+
+ SFP interface configuration to access the SFP ONU from a laptop connected to the EdgeRouter's LAN
+ port
+
+
+2. With the SFP port assigned an IP address, we need to add a NAT rule to forward connections from the configuration
+ laptop on eth2 to the SFP port. We do this by adding a source NAT rule with masquerading enabled, for the TCP
+ protocol, with destination address ``10.10.1.0/24`` (the SFP config interface's private network).
+
+.. raw:: html
+
+
+
+
+
+ Source NAT configuration to access the SFP ONU from LAN. eth5, masquerading on, TCP, destination
+ 10.10.1.1 (the SFP ONU's IP).
+
+
+3. Finally, make sure that your laptop will actually use the EdgeRouter as its gateway for IPs within ``10.10.1.0/24``.
+ On the laptop, disable any VPNs, disconnect your Wifi and make sure that IP r shows a default route pointing at the
+ EdgeRouter's ``192.168.1.1``. If that isn't the case, on Linux you can manually add the necessary route by using
+ ``sudo ip r a 10.10.1.0/24 via 192.168.1.1 dev enp5s0``
+
+After setting up this temporary route, you should be able to access the SFP ONU's configuration web interface by
+pointing a browser at ``http://10.10.1.1/`` Just make sure you use plain-text HTTP here, not secure HTTP**S**. The
+default login credentials for the device are admin/1234.
+
+.. raw:: html
+
+
+
+
+
+ The SFP ONU's web interface.
+
+
+Configuring the PLOAM password / SLID / ONT-Installationskennung
+----------------------------------------------------------------
+
+On the SFP ONU's web interface, we only have to change one single setting: Under "Setup", we have to set what the SFP
+ONU calls "SLID" to the PLOAM password for the interface. Telekom calls this the "ONT-Installationskennung". You get
+this from your Telekom technician. In the config interface, select ASCII mode and enter the number using the format
+``ABCD000000`` with four capital letters followed by six zeros. If necessary, you can read the SFP ONU's serial number
+on this page.
+
+.. raw:: html
+
+
+
+
+
+ The SFP ONU's config interface to set SLID/PLOAM PW/ONT-Installationskennung.
+
+
+Press "Save Config" on the top right of the web page, then select "Reset ONU" and click "Apply" under the "Reset ONU"
+link on the left. Make sure to not select the factory reset option instead.
+
+.. raw:: html
+
+
+
+
+
+ Rebooting the SFP ONU.
+
+
+With the ONU configured, after the reset the "GPON Information" page from the left menu under "Status" from the top menu
+should show ``GPON Line Status: O5``. You can now remove the SNAT rule and IP address from the SFP interface in the
+EdgeRouter's config. I recommend this since there is no way to change the ONU's default credentials, and leaving the
+SNAT rule in place makes it vulnerable to attacks from your LAN. If you use the EdgeRouter's setup wizard in the next
+step, that wizard will reset all of these settings.
+
+Configuring PPPoE and NAT
+-------------------------
+
+Our ONU now has a low-level connection to Telekom's fiber network. The next step is to configure the EdgeRouter to
+authenticate with the ONU through PPPoE. The easiest way to do this is to use the EdgeRouter's "Basic Setup" wizard as
+described in the `EdgeOS User Guide`. In the wizard, select the SFP port (``eth5``) as the internet/WAN port. Select
+``Internet Connection Type`` as ``PPPoE``, then enter the PPPoE credentials you got from your Telekom technician. The
+password is your "Persönliches Kennwort" that you also use to log in to your customer account on Telekom's website. The
+account name is ``[anschlusskennung] [zugangsnummer] "#" [mitbenutzernummer] "@t-online.de"``, so something like
+``002712345678012345678901#0001@t-online.de``. Enable "Internet connection is on VLAN" and enter VLAN ID ``7``. This is
+necessary because of the way Telekom set up their triple play (TV/phone/internet) service. After following through with
+the wizard, your internet should be already working on port 2 of the router. Note that despite selecting the SFP port as
+the router's WAN port, the wizard will still reserve port 1 (``eth0``) for another WAN interface, so you will only be
+able to access the configuration interface through port 2 (``eth1``) after the wizard is done. You can of course change
+this later.
+
+That's it, you're done and your internet should be working!
+
+Having Fun with the SPF GPON ONU
+================================
+
+If you want to dig deeper into the internals of Telekom's GPON implementation, the SFP ONU's firmware is a great
+starting point. Default credentials are all admin/admin or admin/1234 and you can even get a regular busybox shell on
+the device through SSH. The device's firmware is based on OpenWRT, and the source for large parts of the core control
+components can be found under open source licenses as well. While I would strictly advice you to not mess around with
+the actual modem settings because due to GPON you share a medium with your neighbors and might very well disrupt their
+internet if you mess up, inspecting the ONU's firmware is a great way to learn about the inner workings of a modern GPON
+network.
+
+If you are interested in messing around with the SFP ONU, there is a github repository where interesting thins are
+collected `here `__.
+
+.. _`EdgeOS User Guide`: https://dl.ubnt.com/guides/edgemax/EdgeOS_UG.pdf
+
diff --git a/blog/thors-hammer/images/thors_hammer_breadboard.jpg b/content/blog/thors-hammer/images/thors_hammer_breadboard.jpg
similarity index 100%
rename from blog/thors-hammer/images/thors_hammer_breadboard.jpg
rename to content/blog/thors-hammer/images/thors_hammer_breadboard.jpg
diff --git a/blog/thors-hammer/images/thors_hammer_schematic.jpg b/content/blog/thors-hammer/images/thors_hammer_schematic.jpg
similarity index 100%
rename from blog/thors-hammer/images/thors_hammer_schematic.jpg
rename to content/blog/thors-hammer/images/thors_hammer_schematic.jpg
diff --git a/content/blog/thors-hammer/index.rst b/content/blog/thors-hammer/index.rst
new file mode 100644
index 0000000..650716e
--- /dev/null
+++ b/content/blog/thors-hammer/index.rst
@@ -0,0 +1,64 @@
+---
+title: "Thor's Hammer"
+date: 2018-05-03T11:59:37+02:00
+summary: >
+ In case you were having an inferiority complex because your friends' IBM Model M keyboards are so much louder than
+ the shitty rubber dome freebie you got with your pc... Here's the solution: Thor's Hammer, a simple typing cadence
+ enhancer for PS/2 keyboards.
+---
+
+In case you were having an inferiority complex because your friends' IBM Model M keyboards are so much louder than the
+shitty rubber dome freebie you got with your pc... Here's the solution: Thor's Hammer, a simple typing cadence enhancer
+for `PS/2`_ keyboards.
+
+.. raw:: html
+
+
+
+ A demonstration of the completed project.
+
+ h264 download /
+ webm download
+
+
+
+The connects to the keyboard's PS/2 clock line and briefly actuates a large solenoid on each key press. An interesting
+fact about PS/2 is that the clock line is only active as long as either the host computer or the input device actually
+want to send data. In case of a keyboard that's the case when a key is pressed or when the host changes the keyboard's
+LED state, otherwise the clock line is silent. We ignore the LED activity for now as it's generally coupled to key
+presses. By just triggering an NE555 configured as astable flipflop we can stretch each train of clock pulses to a
+pulse a few tens of milliseconds long that is enough to actuate the solenoid.
+
+.. raw:: html
+
+
+
+ The schematic of the driver stretching the PS/2 clock pulses to drive the solenoid.
+
+
+
+Since PS/2 sends each key press and key release separately this circuit will pulse twice per keystroke. It would be
+possible to ignore one of them but I figure the added noise just adds to the experience.
+
+Built on a breadboard, the circuit looks like this.
+
+.. raw:: html
+
+
+
+ The completed circuit built up on a breadboard and attached to a keyboard.
+
+
+
+Since my solenoid did not have a tensioning spring I used a rubber band and some vinyl tape to make an adjustable
+tensioner. The small orange USB hub serves as an end-stop because I had nothing else of the right shape. The sound and
+resonance of the thing can be adjusted to taste by moving the end stop, adjusting the tensioning rubber and tuning the
+excitation duration using the potentiometer. My particular solenoid was a bit slow so I added some pieces of circuit
+board as shims between the plunger and the case to limit the plunger's travel inside the solenoid core.
+
+.. _`PS/2`: https://en.wikipedia.org/wiki/PS/2_port
+
diff --git a/blog/thors-hammer/video/thors_hammer.mkv b/content/blog/thors-hammer/video/thors_hammer.mkv
similarity index 100%
rename from blog/thors-hammer/video/thors_hammer.mkv
rename to content/blog/thors-hammer/video/thors_hammer.mkv
diff --git a/blog/thors-hammer/video/thors_hammer.mov b/content/blog/thors-hammer/video/thors_hammer.mov
similarity index 100%
rename from blog/thors-hammer/video/thors_hammer.mov
rename to content/blog/thors-hammer/video/thors_hammer.mov
diff --git a/blog/thors-hammer/video/thors_hammer.webm b/content/blog/thors-hammer/video/thors_hammer.webm
similarity index 100%
rename from blog/thors-hammer/video/thors_hammer.webm
rename to content/blog/thors-hammer/video/thors_hammer.webm
diff --git a/blog/wifi-led-driver/images/board_in_case.jpg b/content/blog/wifi-led-driver/images/board_in_case.jpg
similarity index 100%
rename from blog/wifi-led-driver/images/board_in_case.jpg
rename to content/blog/wifi-led-driver/images/board_in_case.jpg
diff --git a/blog/wifi-led-driver/images/board_in_case.small.jpg b/content/blog/wifi-led-driver/images/board_in_case.small.jpg
similarity index 100%
rename from blog/wifi-led-driver/images/board_in_case.small.jpg
rename to content/blog/wifi-led-driver/images/board_in_case.small.jpg
diff --git a/blog/wifi-led-driver/images/boards.jpg b/content/blog/wifi-led-driver/images/boards.jpg
similarity index 100%
rename from blog/wifi-led-driver/images/boards.jpg
rename to content/blog/wifi-led-driver/images/boards.jpg
diff --git a/blog/wifi-led-driver/images/boards.small.jpg b/content/blog/wifi-led-driver/images/boards.small.jpg
similarity index 100%
rename from blog/wifi-led-driver/images/boards.small.jpg
rename to content/blog/wifi-led-driver/images/boards.small.jpg
diff --git a/blog/wifi-led-driver/images/layout.png b/content/blog/wifi-led-driver/images/layout.png
similarity index 100%
rename from blog/wifi-led-driver/images/layout.png
rename to content/blog/wifi-led-driver/images/layout.png
diff --git a/blog/wifi-led-driver/images/schematic.png b/content/blog/wifi-led-driver/images/schematic.png
similarity index 100%
rename from blog/wifi-led-driver/images/schematic.png
rename to content/blog/wifi-led-driver/images/schematic.png
diff --git a/content/blog/wifi-led-driver/index.rst b/content/blog/wifi-led-driver/index.rst
new file mode 100644
index 0000000..2f89856
--- /dev/null
+++ b/content/blog/wifi-led-driver/index.rst
@@ -0,0 +1,153 @@
+---
+title: "Wifi Led Driver"
+date: 2018-05-02T11:31:03+02:00
+summary: >
+ After the multichannel LED driver was completed, I was just getting used to controlling LEDs at 14-bit resolution.
+ I liked the board we designed in this project, but at 32 channels it was a bit large for most use cases. Sometimes I
+ just want to pop a piece of LED tape or two somewhere, but I don't need a full 32 channels of control. I ended up
+ thinking that a smaller version of the 32-channel driver that didn't require a separate control computer would be
+ handy. So I sat down and designed a variant of the design with only 8 channels instead of 32 and an on-board ESP8266
+ module instead of the RS485 transceiver for WiFi connectivity.
+---
+
+Project motivation
+==================
+
+.. FIXME finished project picture with LED tape
+.. raw:: html
+
+
+
+ The completed driver board installed in the 3D-printed case. This device can now be connected to
+ 12V and two segments of LED tape that can then be controlled trough Wifi. The ESP8266 module goes on the pin
+ header on the left and was removed for this picture.
+
+
+
+After the `multichannel LED driver`_ was completed, I was just getting used to controlling LEDs at 14-bit resolution.
+I liked the board we designed in this project, but at 32 channels it was a bit large for most use cases. Sometimes I
+just want to pop a piece of LED tape or two somewhere, but I don't need a full 32 channels of control. I ended up
+thinking that a smaller version of the 32-channel driver that didn't require a separate control computer would be
+handy. So I sat down and designed a variant of the design with only 8 channels instead of 32 and an on-board ESP8266_
+module instead of the RS485_ transceiver for WiFi connectivity.
+
+The Electronics
+===============
+
+The schematic was mostly copy-pasted from the 32-channel design. The PCB was designed from scratch. This time, I went
+for a 5x7cm form factor to allow for enough room for all connectors and to give the ESP8266_'s WiFi antenna enough
+space. The board has two 5-pin Phoenix-style_ for two RGB-White (RGBW) tapes and one 2-pin Phoenix-style_ connector for
+12V power input. The control circuitry and the serial protocol are unchanged, but the STM32_ now talks to an ESP-01_
+module running custom firmware.
+
+The LEDs are driven using a 74HC595_ shift register controlling a bunch of AO3400_ MOSFETs_, with resistors in front of
+the MOSFETs_' gates to slow down the transitions a bit to reduce brighntess nonlinearities and EMI_ resulting from
+ringing of the LED tape's wiring inductance.
+
+The board has two spots for either `self-resettable fuses (polyfuses) `__ or regular melting-wire fuses_ in
+a small SMD_ package, one for each RGBW output. For low currents the self-resettable fuses should be okay but at higher
+currents their `trip times get long enough that they become unlikely to trip in time to save anything
+`__, so plain old non-resettable fuses would be the way to go there.
+
+.. FIXME finished board photos
+.. FIXME board with test tape picture
+
+.. raw:: html
+
+
+
+.. raw:: html
+
+
+
+ The completed PCBs of this project (front) and the `multichannel LED driver`_ project the driver
+ circuitry was derived from (back).
+
+
+
+
+The Firmware
+============
+
+The STM32_ firmware only had to be slightly modified to accomodate the reduced channel count since the protocol remains
+unchanged. The ESP firmware is based on esphttpd_ by Spritetm_. The modifications to the webserver firmware are pretty
+basic. First, the UART console has been disabled since I use the UART to talk to the STM32. The few bootloader messages
+popping out the UART on boot are not an issue, since they're unlikely to contain the fixed 32-bit address prefix the
+serial protocol requires for the STM32_ to do anything.
+
+Second, I added LED control by adding drivers for the serial protocol and a bunch of colorspace conversion functions.
+When I first tested the prototype software, I noticed that color reproduction was extremely poor. When I just sent a
+HSV_ rainbow fade from a python command line, the result looked totally wrong. The fade did not seem to go at a constant
+speed and some colors, in particular yellow, orange and greens, were not visible at all. The problem turned out to be a
+stark mismatch of the red, green and blue channels of the LED tape and less-than-optimal color reproduction of the pure
+colors. I decided to properly measure the LED tape's color reproduction so I could compensate for it in software. This
+turned out to be an extremely interesting project, the details of which you can read in my `LED characterization`_
+article.
+
+Third, I updated the built-in websites with some ad-hoc documentation on how to use the thing and a basic interface for
+LED control.
+
+.. FIXME screenshot of firmware website
+
+Making an enclosure
+===================
+
+To be actually useful, the driver needed a robust enclosure. Bare PCBs are nice for prototyping, but for actually
+putting the thing anywhere it needs a case to protect it against random destruction.
+
+The board has four mounting holes with comfortable spacing in its corners to allow easy mounting inside a 3D-printed
+case. The case itself is described in an OpenSCAD_ script. To make it look a little nicer, a little 3D relief is laid
+into the lid. The 3D relief is generated with a bit of blender magic. The source STL_ model is loaded into blender, then
+blender's amazingly flexible rendering system is used to export a depth map of a projection of the model as a PNG_ file.
+This depth map is then imported as a triangle mesh into OpenSCAD_.
+
+For the relief to look good, I chose a rather high resolution for the depth map. This unfortunately leads to extreme
+memory use and processing time on the part of OpenSCAD_, but since I have access to a sufficiently fast machine that is
+not a problem. Just be careful if you try opening the OpenSCAD_ file on your machine, OpenSCAD_ will probably crash
+unless you're on a beefy machine or interrupt it when it starts auto-rendering the file.
+
+The board is mounted into the enclosure using knurled insert nuts that are pressed into a 3D-printed hole using a bit of
+violence.
+
+.. _`multichannel LED driver`: {{[}}
+.. _`LED characterization`: {{][}}
+.. _ESP8266: https://en.wikipedia.org/wiki/ESP8266
+.. _RS485: https://en.wikipedia.org/wiki/RS-485
+.. _Phoenix-style: https://www.phoenixcontact.com/online/portal/de?uri=pxc-oc-itemdetail:pid=1757019&library=dede&tab=1
+.. _STM32: http://www.st.com/resource/en/datasheet/stm32f030f4.pdf
+.. _ESP-01: http://www.watterott.com/de/ESP8266-WiFi-Serial-Transceiver-Modul
+.. _74HC595: http://www.ti.com/lit/ds/symlink/sn74hc595.pdf
+.. _AO3400: http://aosmd.com/pdfs/datasheet/AO3400.pdf
+.. _MOSFETs: https://en.wikipedia.org/wiki/MOSFET
+.. _EMI: https://en.wikipedia.org/wiki/Electromagnetic_interference
+.. _polyfuse: https://en.wikipedia.org/wiki/Resettable_fuse
+.. _SMD: https://en.wikipedia.org/wiki/Surface-mount_technology
+.. _fuses: https://en.wikipedia.org/wiki/Fuse_(electrical)
+.. _littlefuse-16r-datasheet: http://m.littelfuse.com/~/media/electronics/datasheets/resettable_ptcs/littelfuse_ptc_16r_datasheet.pdf.pdf
+.. _OpenSCAD: http://www.openscad.org/
+.. _STL: https://en.wikipedia.org/wiki/STL_(file_format)
+.. _PNG: https://en.wikipedia.org/wiki/Portable_Network_Graphics
+.. _esphttpd: https://github.com/Spritetm/esphttpd
+.. _Spritetm: http://spritesmods.com/
+.. _`HSV`: https://en.wikipedia.org/wiki/HSL_and_HSV
+
diff --git a/blog/wifi-led-driver/resource/lyza_schematic_and_pcb.pdf b/content/blog/wifi-led-driver/resource/lyza_schematic_and_pcb.pdf
similarity index 100%
rename from blog/wifi-led-driver/resource/lyza_schematic_and_pcb.pdf
rename to content/blog/wifi-led-driver/resource/lyza_schematic_and_pcb.pdf
diff --git a/content/blog/wsdiff-static-html-diffs/index.rst b/content/blog/wsdiff-static-html-diffs/index.rst
new file mode 100644
index 0000000..51f9175
--- /dev/null
+++ b/content/blog/wsdiff-static-html-diffs/index.rst
@@ -0,0 +1,127 @@
+---
+title: "wsdiff: Responsive diffs in plain HTML"
+date: 2025-07-25T23:42:00+01:00
+summary: >
+ There's many tools that render diffs on the web, but almost none that work well on small screens such as phones. I
+ fixed this by publishing wsdiff, a diffing tool written in Python that produces diffs as beautiful, responsive,
+ static, self-contained HTML pages. wsdiffs wrap text to fit the window, and dynamically switch between unified and
+ split diffs based on screen size using only CSS.
+---
+
+Demo
+----
+
+First off, have a demo. Because of the width of this page, the output will show an unified diff. To try out the split
+diff layout, make sure your browser window is wide enough and open the demo in a separate tab using `this link
+`__.
+
+wsdiff supports dark mode, try it out by toggling dark mode in your operating system!
+
+.. raw:: html
+
+
+
+Core Features
+-------------
+
+There's many tools that render diffs on the web, but almost none that work well on small screens such as phones. I fixed
+this by publishing `wsdiff `__, a diffing tool written in Python that produces diffs
+as beautiful, responsive, static, self-contained HTML pages. wsdiffs wrap text to fit the window, and dynamically switch
+between unified and split diffs based on screen size using only CSS.
+
+Responsive Line Wrapping
+........................
+
+The first challenge I solved was wrapping source code lines to match the available screen space. Other tools often just
+show horizontal scroll bars, which is an okay workaround when you're mostly working with hard-wrapped source code on a
+laptop or desktop screen, but which results in catastrophic UX on any phone.
+
+I solved line breaking with a combination of CSS-controlled, web-standard word breaking rules: ``overflow-wrap:
+anywhere`` for source code (`MDN link `__) and
+``white-space: pre-wrap`` to preserve whitespace accurately (`MDN link
+`__). To make both sides of the split diff align, and to
+align line numbers with wrapped source code lines, the diff is laid out using a `CSS grid layout`_. In side-by-side
+view, the layout has four columns: two for line numbers and two for the content. In unified view, the left ("old")
+content column is dropped, and the deleted or modified lines that are highlighted in it in side-by-side view are slotted
+into the remaining right column.
+
+When soft-wrapping source code, text editors will often display a little curved arrow marker to indicate that a line was
+soft-wrapped, and that there is not actually a newline character in the file at that location. wsdiff solves this
+using the same technique I used for the soft-wrapping code blocks in this blog, described `here <{{][}}>`__. It inserts a string of ``"\a↳\a↳\a↳\a↳\a↳..."`` into the line number
+element's ``::after`` pseudo-element. This string evaluates to a sequence of unicode arrows separated by line breaks,
+and starting with an empty line. The ``::after`` pseudo-element is positioned using ``position: absolute``, and the
+parent ```` has ``position: relative`` set. This way, the arrow pseudo-element gets placed on top
+of the lineno span without affecting the layout at all. By setting ``overflow: clip`` on the parent ````, the arrow pseudo-element gets cut off vertically wherever the parent line number element naturally
+ends. Since both the line and the line number element share a grid row, the line number element always matches the
+height of the soft-wrapped line.
+
+Responsive Split/Unified Layout Selection
+.........................................
+
+To dynamically change between unified and side-by-side views, wsdiff uses a web-standard `Media Query`_. By default, the
+page is laid out for side-by-side view. In the HTML source, the diff is listed as it is displayed in side-by-side view,
+with the old and new lines along with their line numbers interleaved.
+
+The magic happens when the media query gets triggered by a narrow screen width. The media query re-adjusts the layout in
+four core steps:
+
+ 1. All unchanged lines in the left (old) column are hidden.
+ 2. The left content column of the grid layout is hidden, so that now there are three columns: old line number, new line
+ number, and unified content.
+ 3. All deleted or changed lines from the left (old) column are re-located to the right column. They naturally slot in
+ in the right spot because they already appear in the right order in the HTML source.
+ 4. By slotting in the old lines in the right column, we have created gaps in the line number columns. Every deleted
+ line has an empty cell in the new line number column, and every inserted line has one in the old line number column.
+ The CSS adjusts the layout of these empty cells such that the border lines align nicely, and it overrides the
+ newline markers so that they only show in the right (new) line number column, not both.
+
+Since this is all CSS, it happens automatically and near-instantly. Since it is using only web standard features, it
+works across browsers and platforms.
+
+Unchanged Line Folding in CSS
+.............................
+
+When showing the diff of a large file, it is usually best to hide large runs of unchanged lines. wsdiff does this
+similar to code folding in text editors. When a long run of unchanged lines is detected, a marker is placed spanning the
+diff. This marker contains a checkbox that can be toggled to hide the unchanged lines. This feature is done completely
+in CSS using a ``:has(input[type="checkbox"]:checked)`` selector.
+
+The actual mechanics are quite simple. To cleanly hide the lines, they must be placed in a container ``]``. That div
+has a CSS subgrid layout using ``display: grid; grid-template-columns: subgrid;``, meaning that its contents align to
+the surrounding diff grid.
+
+Dark Mode
+.........
+
+Integrating a website with the OS-level dark mode is surprisingly easy. All you need is a `Media Query`_ that selects
+for ``@media (prefers-color-scheme: dark)`` and you're good. wsdiff uses named colors using `CSS Custom Properties`_, so
+the actual dark mode media query only needs to override these color properties, and the rest of the CSS will be
+re-computed automatically.
+
+Limitations: Text selection
+...........................
+
+A limitation in having a combined, single HTML source for both side-by-side and unified diffs is that text selection
+only works naturally in either mode. You can't make text selection work in both simultaneously without re-sorting the
+lines in the HTML source, since there is no way to override the text selection order from pure CSS. In wsdiff, I worked
+around this issue by just disabling text selection on the unchanged lines in the left (old) column, so selecting text in
+the right column copies the unified diff as one would expect.
+
+Try it yourself!
+----------------
+
+You can find the demo from above at `this link `__.
+
+You can install wsdiff yourself `from PyPI
`__:
+
+.. code:: sh
+
+ $ pip install -U wsdiff
+ Successfully installed wsdiff-0.3.1
+ $ wsdiff old.py new.py -o diff.html
+
+.. _`CSS grid layout`: https://css-tricks.com/snippets/css/complete-guide-grid/
+.. _`Media Query`: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Using_media_queries
+.. _`CSS Custom Properties`: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_cascading_variables/Using_CSS_custom_properties
diff --git a/content/imprint/index.rst b/content/imprint/index.rst
new file mode 100644
index 0000000..bbff4e2
--- /dev/null
+++ b/content/imprint/index.rst
@@ -0,0 +1,103 @@
+---
+title: "Impressum"
+noindex: true
+---
+
+Impressum
+---------
+
+.. raw:: html
+
+
+ Sebastian Götte
+ c/o Praxis Dr. Götte
+ Krankenhausstr. 1a
+ 54634 Bitburg
+ imprint@jaseg.net
+
+
+ Inhaltlich Verantwortlicher gem. § 55 II RStV: Sebastian Götte (Anschrift s.o.)
+
+Lizenz dieser Seite
+-------------------
+
+.. raw:: html
+
+ Dieses Werk ist lizenziert unter einer
+
+ Creative Commons Namensnennung - Nicht-kommerziell - Weitergabe unter gleichen Bedingungen 4.0 International
+ Lizenz (CC-BY-NC-SA)
+ .
+
+
+
+
+
+Haftungsbeschränkung für Inhalte der Website
+--------------------------------------------
+
+Gemäß § 7 Abs. 1 TMG ist der Verantwortliche der Website i. S. v. § 5 TMG für eigene Informationen, die er zur Nutzung
+bereithält, nach den allgemeinen Gesetzen verantwortlich.
+
+Der Verantwortliche der vorbezeichneten Website übernimmt keine Haftung auf Aktualität, Richtigkeit und Vollständigkeit
+der auf dieser Website zur Verfügung gestellten Inhalte. Dies gilt nicht, wenn dem Verantwortlichen vorsätzliches oder
+grob fahrlässiges Verhalten vorzuwerfen ist. Die Inhalte wurden mit der größtmöglichen Sorgfalt erstellt. Dennoch kann
+die inhaltliche Richtigkeit insbesondere bei komplexen Themen nicht gewährleistet werden, so dass der Verantwortliche
+den Nutzern empfiehlt, bei wichtigen Informationen bei den zuständigen Stellen anzufragen oder rechtliche Beratung in
+Anspruch zu nehmen. Sofern kostenpflichtige Inhalte oder Dienste auf der Website zur Verfügung gestellt werden, handelt
+es sich dabei um unverbindliche Invitatio ad offerendum, welche lediglich zur Abgabe eines Angebots durch den Nutzer
+aufrufen und selbst kein verbindliches Angebot darstellen.
+
+Gemäß §§ 8 ff. TMG ist der Website-Betreiber für fremde Inhalte, die er für einen Nutzer veröffentlicht, nicht
+verantwortlich, sofern
+
+* er keine Kenntnis von der rechtswidrigen Handlung oder der Information hat und ihm im Falle von
+ Schadensersatzansprüchen auch keine Tatsachen oder Umstände bekannt sind, aus denen die rechtswidrige Handlung oder
+ die Information offensichtlich wird, oder
+
+* er unverzüglich tätig geworden ist, um die Information zu entfernen oder den Zugang zu ihr zu sperren, sobald er
+ diese Kenntnis erlangt hat.
+
+Erlangt der Website-Betreiber Kenntnis von solchen rechtswidrigen Inhalten, werden diese unverzüglich entfernt.
+
+Haftung für ausgehende Links
+----------------------------
+
+Der Website-Betreiber verlinkt von seiner Website auf fremde Websites. Durch diese sog. „Hyperlinks“ wird der Nutzer
+direkt auf die fremde Website geleitet. Der Website-Betreiber hat dabei keinerlei Einfluss auf die Informationen der
+fremden Website. Daher kann keine Haftung für die Aktualität, Richtigkeit und Vollständigkeit der Inhalte der fremden
+Website übernommen werden. Der Website-Betreiber versichert jedoch, dass ihm zum Zeitpunkt des Setzens der Verlinkung
+keinerlei rechtliche Verstöße bekannt waren und er die fremde Website im Rahmen des Zumutbaren geprüft hat.
+
+Erhält der Website-Betreiber Kenntnis von der Rechtswidrigkeit der verlinkten Inhalte, wird der entsprechende Link
+unverzüglich entfernt.
+
+Urheberrechte
+-------------
+
+Die auf dieser Webseite veröffentlichten Inhalte unterliegen dem deutschen Urheberrecht.
+
+Als Urheber i. S. v. § 7 UrhG stehen dem Website-Betreiber die alleinigen und ausschließlichen Verwertungsrechte gemäß
+§§ 15 ff. UrhG zu. Eine Vervielfältigung oder Verwendung sämtlicher Inhalte der Website in fremden elektronischen oder
+gedruckten Medien ist ohne vorherige Zustimmung des Website-Betreibers untersagt und wird ggf. verfolgt.
+
+Datenschutz
+-----------
+
+Die Website kann grundsätzlich ohne Eingabe von personenbezogenen Daten wie z. B. Name oder E-Mail-Adresse genutzt
+werden. Sollte die Möglichkeit der Eingabe solcher Daten bestehen, so erfolgt die Mitteilung dieser Daten auf
+freiwilliger Basis durch den Nutzer. Eine Weitergabe dieser Daten an Dritte ist ohne die ausdrückliche Zustimmung durch
+den Nutzer ausgeschlossen.
+
+Ein lückenloser Schutz der übermittelten Daten vor dem Zugriff durch unbefugte Dritte ist jedoch nicht möglich, da es im
+Bereich der Datenübermittlung im Internet zu Sicherheitslücken kommen kann. Der Website-Betreiber versucht jedoch, die
+Gefahr des unbefugten Zugriffs durch geeignete Maßnahmen zu unterbinden und im Falle der Kenntnis von einer
+Sicherheitslücke diese durch geeignete Maßnahmen zu schließen.
+
+Der Nutzung der im Impressum veröffentlichten Kontaktdaten durch Dritte zur Übersendung von nicht ausdrücklich
+gewünschter Werbung, insbesondere Spam-Mails, wird widersprochen und im Falle der Nichtbeachtung ggf. mit rechtlichen
+Schritten verfolgt.
+
+(Quelle: `BUSE HERZ GRUNST Rechtsanwälte
`__)
+
diff --git a/content/projects/8seg/index.rst b/content/projects/8seg/index.rst
new file mode 100644
index 0000000..43e5c85
--- /dev/null
+++ b/content/projects/8seg/index.rst
@@ -0,0 +1,25 @@
+---
+title: "8seg"
+external_links:
+ - name: Sources and hardware design files
+ url: "https://git.jaseg.de/8seg.git"
+ - name: Enclosure 3D models
+ url: "https://www.printables.com/model/695260-parametric-flexible-aluminium-profile-case"
+ - name: Technical overview blog post
+ url: "/blog/8seg"
+summary: >
+ 8seg is an experimental textual display. It is made from a 45m by 1.5m large lacework banner that can be put up in a
+ variety of spaces, conforming to the space's size and shape through bending and folding.
+---
+
+8seg
+====
+
+8seg is an experimental textual display. It is made from a 45m by 1.5m large lacework banner that can be put up in a
+variety of spaces, conforming to the space's size and shape through bending and folding.
+
+8seg was last set up at 37C3 in the entrance hall, on the second-floor railing opposite Hall X.
+
+By popular request, you can find 3D models for the printable parts of the power supply enclosures `here
+`__
+
diff --git a/content/projects/_index.rst b/content/projects/_index.rst
new file mode 100644
index 0000000..9f9d49b
--- /dev/null
+++ b/content/projects/_index.rst
@@ -0,0 +1,7 @@
+---
+title: Projects
+hide_date: true
+---
+I maintain a number of open-source projects. Most of these I started out of some personal need or interest.
+I strive to keep all of them up to date and maintained, so if you notice an issue with one of them, please
+open an issue on the project's issue tracker.
diff --git a/content/projects/gerbolyze/README.rst b/content/projects/gerbolyze/README.rst
new file mode 100644
index 0000000..0e8262b
--- /dev/null
+++ b/content/projects/gerbolyze/README.rst
@@ -0,0 +1,700 @@
+Gerbolyze renders SVG vector and PNG/JPG raster images into existing gerber PCB manufacturing files.
+Vector data from SVG files is rendered losslessly *without* an intermediate rasterization/revectorization step.
+Still, gerbolyze supports (almost) the full SVG 1.1 spec including complex, self-intersecting paths with holes,
+patterns, dashes and transformations.
+
+Raster images can either be vectorized through contour tracing (like gerbolyze v1.0 did) or they can be embedded using
+high-resolution grayscale emulation while (mostly) guaranteeing trace/space design rules.
+
+Try gerbolyze online at https://dyna.kokoroyukuma.de/gerboweb
+
+.. figure:: pics/pcbway_sample_02_small.jpg
+ :width: 800px
+
+ Drawing by `トーコ Toko `__ converted using Gerbolyze and printed at PCBWay.
+
+
+Tooling for PCB art is quite limited in both open source and closed source ecosystems. Something as simple as putting a
+pretty picture on a PCB can be an extremely tedious task. Depending on the PCB tool used, various arcane incantations
+may be necessary and even modestly complex images will slow down most PCB tools to a crawl.
+
+Gerbolyze solves this problem in a toolchain-agnostic way by directly vectorizing SVG vector and PNG or JPG bitmap files
+onto existing gerber layers. Gerbolyze processes any spec-compliant SVG and "gerbolyzes" SVG vector data into a Gerber
+spec-compliant form. Gerbolyze has been tested against both the leading open-source KiCAD toolchain and the
+industry-standard Altium Designer. Gerbolyze is written with performance in mind and will happily vectorize tens of
+thousands of primitives, generating tens of megabytes of gerber code without crapping itself. With gerbolyze you can
+finally be confident that your PCB fab's toolchain will fall over before yours does if you overdo it with the high-poly
+anime silkscreen.
+
+Gerbolyze is based on gerbonara_.
+
+.. image:: pics/process-overview.png
+ :width: 800px
+
+.. contents::
+
+Tl;dr: Produce high-quality artistic PCBs in three easy steps!
+--------------------------------------------------------------
+
+Gerbolyze works in three steps.
+
+1. Generate a scale-accurate template of the finished PCB from your CAD tool's gerber output:
+
+ .. code::
+
+ $ gerbolyze template --top template_top.svg [--bottom template_bottom.svg] my_gerber_dir
+
+2. Load the resulting template image Inkscape_ or another SVG editing program. Put your artwork on the appropriate SVG
+ layer. Dark colors become filled gerber primitives, bright colors become unfilled primitives. You can directly put
+ raster images (PNG/JPG) into this SVG as well, just position and scale them like everything else. SVG clips work for
+ images, too. Masks are not supported.
+
+3. Vectorize the edited SVG template image drectly into the PCB's gerber files:
+
+ .. code::
+
+ $ gerbolyze paste --top template_top_edited.svg [--bottom ...] my_gerber_dir output_gerber_dir
+
+Quick Start Installation (Any Platform)
+---------------------------------------
+
+.. code-block:: shell
+
+ python -m pip install --user gerbolyze
+
+To uninstall, run
+
+.. code-block:: shell
+
+ python -m pip uninstall gerbolyze gerbonara resvg-wasi svg-flatten-wasi
+
+To update, run
+
+.. code-block:: shell
+
+ python -m pip install --user --upgrade --upgrade-strategy eager gerbolyze
+
+Speeding up gerbolyze using natively-built binaries
+---------------------------------------------------
+
+This will install gerbolyze's binary dependency resvg and gerbolyze's svg-flatten utility as pre-built cross-platform
+WASM binaries. When you first run gerbolyze, it will take some time (~30s) to link these binaries for your system. The
+output is cached, so any future run is going to be fast.
+
+WASM is slower than natively-built binaries. To speed up gerbolyze, you can natively build its two binary dependencies:
+
+1. Install resvg natively using rust's cargo package manager: ``cargo install resvg``
+2. Install gerbolyze's svg-flatten utility natively. You can get pre-built binaries from gerbolyze's gitlab CI jobs `at
+ this link `__ by clicking the three dots on the
+ right next to the version you want. These pre-built binaries should work on any x86_64 linux since they are
+ statically linked. You can also build svg-flatten yourself by running ``make`` inside the ``svg-flatten`` folder from
+ a gerbolyze checkout.
+
+Gerbolyze will pick up these binaries when installed in your ``$PATH``. resvg is also picked up when it is installed by
+cargo in your home's ``~/.cargo``, even if it's not in your ``$PATH``. You can override the resvg, usvg or svg-flatten
+binary that gerbolyze uses by giving it the absoulute path to a binary in the ``$RESVG``, ``$USVG`` and ``$SVG_FLATTEN``
+environment variables.
+
+
+Build from source (any distro)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. code:: sh
+
+ git clone --recurse-submodules https://git.jaseg.de/gerbolyze.git
+ cd gerbolyze
+
+ python3 -m venv
+ source venv/bin/activate
+ python3 setup.py install
+
+Features
+--------
+
+Input on the left, output on the right.
+
+.. image:: pics/test_svg_readme_composited.png
+ :width: 800px
+
+* Almost full SVG 1.1 static spec coverage (!)
+
+ * Paths with beziers, self-intersections and holes
+ * Strokes, even with dashes and markers
+ * Pattern fills and strokes
+ * Transformations and nested groups
+ * Proper text rendering with support for complex text layout (e.g. Arabic)
+ * elements via either built-in vectorizer or built-in halftone processor
+ * (some) CSS
+
+* Writes Gerber, SVG or KiCAD S-Expression (``.kicad_mod``) formats
+* Can export from top/bottom SVGs to a whole gerber layer stack at once with filename autodetection
+* Can export SVGs to ``.kicad_mod`` files like svg2mod (but with full SVG support)
+* Beziers flattening with configurable tolerance using actual math!
+* Polygon intersection removal
+* Polygon hole removal (!)
+* Optionally vector-compositing of output: convert black/white/transparent image to black/transparent image
+* Renders SVG templates from input gerbers for accurate and easy scaling and positioning of artwork
+* layer masking with offset (e.g. all silk within 1mm of soldermask)
+* Can read gerbers from zip files
+* Limited SVG support for board outline layers (no fill/region support)
+* Dashed lines supported on board outline layers
+
+Gerbolyze is the end-to-end "paste this svg into these gerbers" command that handles all layers on both board sides at
+once. The heavy-duty computer geometry logic of gerbolyze is handled by the svg-flatten utility (``svg-flatten``
+directory). svg-flatten reads an SVG file and renders it into a variety of output formats. svg-flatten can be used like
+a variant of the popular svg2mod that supports all of SVG and handles arbitrary input ```` elements.
+
+Algorithm Overview
+------------------
+
+This is the algorithm gerbolyze uses to process a stack of gerbers.
+
+* Map input files to semantic layers by their filenames
+* For each layer:
+
+ * load input gerber
+ * Pass mask layers through ``gerbv`` for conversion to SVG
+ * Pass mask layers SVG through ``svg-flatten --dilate``
+ * Pass input SVG through ``svg-flatten --only-groups [layer]``
+ * Overlay input gerber, mask and input svg
+ * Write result to output gerber
+
+This is the algorithm svg-flatten uses to process an SVG.
+
+* pass input SVG through usvg_
+* iterate depth-first through resulting SVG.
+
+ * for groups: apply transforms and clip and recurse
+ * for images: Vectorize using selected vectorizer
+ * for paths:
+
+ * flatten path using Cairo
+ * remove self-intersections using Clipper
+ * if stroke is set: process dash, then offset using Clipper
+ * apply pattern fills
+ * clip to clip-path
+ * remove holes using Clipper
+
+* for KiCAD S-Expression export: vector-composite results using CavalierContours: subtract each clear output primitive
+ from all previous dark output primitives
+
+Web interface
+-------------
+
+You can try gerbolyze online at https://dyna.kokoroyukuma.de/gerboweb
+
+The web interface does not expose all of gerbolyze's bells and whistles, but it allows you to simply paste a single SVG
+file on a board to try out gerbolyze. Upload your design on the web interface, then download the template for either the
+top or bottom side, and put your artwork on the appropriate layer of that template using Inkscape_. Finally, upload the
+modified template and let gerbolyze process your design.
+
+Command-line usage
+------------------
+.. _command_line_usage:
+
+Generate SVG template from Gerber files:
+
+.. code-block:: shell
+
+ gerbolyze template [options] [--top|--bottom] input_dir_or.zip output.svg
+
+Render design from an SVG made with the template above into a set of gerber files:
+
+.. code-block:: shell
+
+ gerbolyze paste [options] artwork.svg input_dir_or.zip output_dir_or.zip
+
+Use svg-flatten to convert an SVG file into Gerber or flattened SVG:
+
+.. code-block:: shell
+
+ svg-flatten [options] --format [gerber|svg] [input_file.svg] [output_file]
+
+Use svg-flatten to convert an SVG file into the given layer of a KiCAD S-Expression (``.kicad_mod``) file:
+
+.. code-block:: shell
+
+ svg-flatten [options] --format kicad --sexp-layer F.SilkS --sexp-mod-name My_Module [input_file.svg] [output_file]
+
+Use svg-flatten to convert an SVG file into a ``.kicad_mod`` with SVG layers fed into separate KiCAD layers based on
+their IDs like the popular ``svg2mod`` is doing:
+
+Note:
+ Right now, the input SVG's layers must have *ids* that match up KiCAD's s-exp layer names. Note that when you name
+ a layer in Inkscape that only sets a ``name`` attribute, but does not change the ID. In order to change the ID in
+ Inkscape, you have to use Inkscape's "object properties" context menu function.
+
+ Also note that svg-flatten expects the layer names KiCAD uses in their S-Expression format. These are *different* to
+ the layer names KiCAD exposes in the UI (even though most of them match up!).
+
+ For your convenience, there is an SVG template with all the right layer names and IDs located next to this README.
+
+.. code-block:: shell
+
+ svg-flatten [options] --format kicad --sexp-mod-name My_Module [input_file.svg] [output_file]
+
+``gerbolyze template``
+~~~~~~~~~~~~~~~~~~~~~~
+
+Usage: ``gerbolyze template [OPTIONS] INPUT``
+
+Generate SVG template for gerbolyze paste from gerber files.
+
+INPUT may be a gerber file, directory of gerber files or zip file with gerber files. The output file contains a preview
+image of the input gerbers to allow you to position your artwork, as well as prepared Inkscape layers corresponding to
+each gerber layer. Simply place your artwork in this SVG template using Inkscape. Starting in v3.0, gerbolyze
+automatically keeps track of which board side (top or bottom) is contained in an SVG template.
+
+Options:
+********
+``--top | --bottom``
+ Output top or bottom side template. This affects both the preview image and the prepared Inkscape layers.
+
+``--vector | --raster``
+ Embed preview renders into output file as SVG vector graphics instead of rendering them to PNG bitmaps. The
+ resulting preview may slow down your SVG editor.
+
+``--raster-dpi FLOAT``
+ DPI for rastering preview
+
+``--bbox TEXT``
+ Output file bounding box. Format: "w,h" to force [w] mm by [h] mm output canvas OR "x,y,w,h" to force [w] mm by [h]
+ mm output canvas with its bottom left corner at the given input gerber coördinates.
+
+
+``gerbolyze paste``
+~~~~~~~~~~~~~~~~~~~
+(see `below `__)
+
+Usage: ``gerbolyze paste [OPTIONS] INPUT_GERBERS OVERLAY_SVG OUTPUT_GERBERS``
+
+Render vector data and raster images from SVG file into gerbers. The SVG input file can be generated using ``gerbolyze
+template`` and contains the name and board side of each layer. Note that for board outline layers, handling slightly
+differs from other layers as PCB fabs do not support filled Gerber regions on these layers.
+
+Options:
+********
+
+``--bbox TEXT``
+ Output file bounding box. Format: "w,h" to force [w] mm by [h] mm output canvas OR "x,y,w,h" to force [w] mm by [h]
+ mm output canvas with its bottom left corner at the given input gerber coördinates. This **must match the ``--bbox`` value given to
+ template**!
+
+``--subtract TEXT``
+ Use user subtraction script from argument (see `below `_)
+
+``--no-subtract``
+ Disable subtraction (see `below `_)
+
+``--dilate FLOAT``
+ Default dilation for subtraction operations in mm (see `below `_)
+
+``--trace-space FLOAT``
+ Passed through to svg-flatten, see `below `__.
+
+``--vectorizer TEXT``
+ Passed through to svg-flatten, see `its description below `__. Also have a look at `the examples below `_.
+
+``--vectorizer-map TEXT``
+ Passed through to svg-flatten, see `below `__.
+
+``--exclude-groups TEXT``
+ Passed through to svg-flatten, see `below `__.
+
+
+.. _outline_layers:
+
+Outline layers
+**************
+
+Outline layers require special handling since PCB fabs do not support filled G36/G37 polygons on these layers. The main
+difference between normal layers and outline layers is how strokes are handled. On outline layers, strokes are
+translated to normal Gerber draw commands (D01, D02 etc.) with an aperture set to the stroke's width instead of tracing
+them to G36/G37 filled regions. This means that on outline layers, SVG end caps and line join types do not work: All
+lines are redered with round joins and end caps.
+
+One exception from this are patterns, which work as expected for both fills and strokes with full support for joins and
+end caps.
+
+Dashed strokes are supported on outline layers and can be used to make easy mouse bites.
+
+.. _subtraction_script:
+
+Subtraction scripts
+*******************
+
+.. image:: pics/subtract_example.png
+ :width: 800px
+
+Subtraction scripts tell ``gerbolyze paste`` to remove an area around certain input layers to from an overlay layer.
+When a input layer is given in the subtraction script, gerbolyze will dilate (extend outwards) everything on this input
+layer and remove it from the target overlay layer. By default, Gerbolyze subtracts the mask layer from the silk layer to
+make sure there are no silk primitives that overlap bare copper, and subtracts each input layer from its corresponding
+overlay to make sure the two do not overlap. In the picture above you can see both at work: The overlay contains
+halftone primitives all over the place. The subtraction script has cut out an area around all pads (mask layer) and all
+existing silkscreen. You can turn off this behavior by passing ``--no-subtract`` or pass your own "script".
+
+The syntax of these scripts is:
+
+.. code-block::
+
+ {target layer} -= {source layer} {dilation} [; ...]
+
+The target layer must be ``out.{layer name}`` and the source layer ``in.{layer name}``. The layer names are gerbolyze's
+internal layer names, i.e.: ``paste, silk, mask, copper, outline, drill``
+
+The dilation value is optional, but can be a float with a leading ``+`` or ``-``. If given, before subtraction the
+source layer's features will be extended by that many mm. If not given, the dilation defaults to the value given by
+``--dilate`` if given or 0.1 mm otherwise. To disable dilation, simply pass ``+0`` here.
+
+Multiple commands can be separated by semicolons ``;`` or line breaks.
+
+The default subtraction script is:
+
+.. code-block::
+
+ out.silk -= in.mask
+ out.silk -= in.silk+0.5
+ out.mask -= in.mask+0.5
+ out.copper -= in.copper+0.5
+
+.. _svg_flatten:
+
+``svg-flatten``
+~~~~~~~~~~~~~~~
+
+Usage: ``svg-flatten [OPTIONS]... [INPUT_FILE] [OUTPUT_FILE]``
+
+Specify ``-`` for stdin/stdout.
+
+Options:
+********
+
+``-h, --help``
+ Print help and exit
+
+``-v, --version``
+ Print version and exit
+
+``-o, --format``
+ Output format. Supported: gerber, gerber-outline (for board outline layers), svg, s-exp (KiCAD S-Expression)
+
+``-p, --precision``
+ Number of decimal places use for exported coordinates (gerber: 1-9, SVG: >=0). Note that not all gerber viewers are
+ happy with too many digits. 5 or 6 is a reasonable choice.
+
+``--clear-color``
+ SVG color to use in SVG output for "clear" areas (default: white)
+
+``--dark-color``
+ SVG color to use in SVG output for "dark" areas (default: black)
+
+``-f, --flip-gerber-polarity``
+ Flip polarity of all output gerber primitives for --format gerber.
+
+``-d, --trace-space``
+ Minimum feature size of elements in vectorized graphics (trace/space) in mm. Default: 0.1mm.
+
+``--no-header``
+ Do not export output format header/footer, only export the primitives themselves
+
+``--flatten``
+ Flatten output so it only consists of non-overlapping white polygons. This perform composition at the vector level.
+ Potentially slow. This defaults to on when using KiCAD S-Exp export because KiCAD does not know polarity or colors.
+
+``--no-flatten``
+ Disable automatic flattening for KiCAD S-Exp export
+
+``--dilate``
+ Dilate output gerber primitives by this amount in mm. Used for masking out other layers.
+
+``-g, --only-groups``
+ Comma-separated list of group IDs to export.
+
+``-b, --vectorizer``
+ Vectorizer to use for bitmap images. One of poisson-disc (default), hex-grid, square-grid, binary-contours,
+ dev-null. Have a look at `the examples below `_.
+
+``--vectorizer-map``
+ Map from image element id to vectorizer. Overrides --vectorizer. Format: id1=vectorizer,id2=vectorizer,...
+
+ You can use this to set a certain vectorizer for specific images, e.g. if you want to use both halftone
+ vectorization and contour tracing in the same SVG. Note that you can set an ```` element's SVG ID from within
+ Inkscape though the context menu's Object Properties tool.
+
+``--force-svg``
+ Force SVG input irrespective of file name
+
+``--force-png``
+ Force bitmap graphics input irrespective of file name
+
+``-s, --size``
+ Bitmap mode only: Physical size of output image in mm. Format: 12.34x56.78
+
+``--sexp-mod-name``
+ Module name for KiCAD S-Exp output. This is a mandatory argument if using S-Exp output.
+
+``--sexp-layer``
+ Layer for KiCAD S-Exp output. Defaults to auto-detect layers from SVG layer/top-level group IDs. If given, SVG
+ groups and layers are completely ignored and everything is simply vectorized into this layer, though you cna still
+ use ``-g`` for group selection.
+
+``-a, --preserve-aspect-ratio``
+ Bitmap mode only: Preserve aspect ratio of image. Allowed values are meet, slice. Can also parse full SVG
+ preserveAspectRatio syntax.
+
+``--no-usvg``
+ Do not preprocess input using usvg (do not use unless you know *exactly* what you're doing)
+
+``--usvg-dpi``
+ Passed through to usvg's --dpi, in case the input file has different ideas of DPI than usvg has.
+
+``--scale``
+ Scale input svg lengths by this factor (-o gerber only).
+
+``-e, --exclude-groups``
+ Comma-separated list of group IDs to exclude from export. Takes precedence over --only-groups.
+
+.. _vectorization:
+
+Gerbolyze image vectorization
+-----------------------------
+
+Gerbolyze has two built-in strategies to translate pixel images into vector images. One is its built-in halftone
+processor that tries to approximate grayscale. The other is its built-in binary vectorizer that traces contours in
+black-and-white images. Below are examples for the four options.
+
+The vectorizers can be used in isolation through ``svg-flatten`` with either an SVG input that contains an image or a
+PNG/JPG input.
+
+The vectorizer can be controlled globally using the ``--vectorizer`` flag in both ``gerbolyze`` and ``svg-flatten``. It
+can also be set on a per-image basis in both using ``--vectorizer-map [image svg id]=[option]["," ...]``.
+
+.. for f in vec_*.png; convert -background white -gravity center $f -resize 500x500 -extent 500x500 (basename -s .png $f)-square.png; end
+.. for vec in hexgrid square poisson contours; convert vec_"$vec"_whole-square.png vec_"$vec"_detail-square.png -background transparent -splice 25x0+0+0 +append -chop 25x0+0+0 vec_"$vec"_composited.png; end
+
+``--vectorizer poisson-disc`` (the default)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. image:: pics/vec_poisson_composited.png
+ :width: 800px
+
+``--vectorizer hex-grid``
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. image:: pics/vec_hexgrid_composited.png
+ :width: 800px
+
+``--vectorizer square-grid``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. image:: pics/vec_square_composited.png
+ :width: 800px
+
+``--vectorizer binary-contours``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. image:: pics/vec_contours_composited.png
+ :width: 800px
+
+The binary contours vectorizer requires a black-and-white binary input image. As you can see, like every bitmap tracer
+it will produce some artifacts. For artistic input this is usually not too bad as long as the input data is
+high-resolution. Antialiased edges in the input image are not only OK, they may even help with an accurate
+vectorization.
+
+GIMP halftone preprocessing guide
+---------------------------------
+
+Gerbolyze has its own built-in halftone processor, but you can also use the high-quality "newsprint" filter built into
+GIMP_ instead if you like. This section will guide you through this. The PNG you get out of this can then be fed into
+gerbolyze using ``--vectorizer binary-contours``.
+
+1 Import your desired artwork
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Though anime or manga pictures are highly recommended, you can use any image including photographs. Be careful to select
+a picture with comparatively low detail that remains recognizable at very low resolution. While working on a screen this
+is hard to vizualize, but the grain resulting from the low resolution of a PCB's silkscreen is quite coarse.
+
+.. image:: screenshots/02import02.png
+ :width: 800px
+
+2 Convert the image to grayscale
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. image:: screenshots/06grayscale.png
+ :width: 800px
+
+3 Fine-tune the image's contrast
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+To look well on the PCB, contrast is critical. If your source image is in color, you may have lost some contrast during
+grayscale conversion. Now is the time to retouch that using the GIMP's color curve tool.
+
+When using the GIMP's newsprint filter, bright grays close to white and dark grays close to black will cause very small
+dots that might be beyond your PCB manufacturer's maximum resolution. To control this case, add small steps at the ends
+of the grayscale value curve as shown (exaggerated) in the picture below. These steps saturate very bright grays to
+white and very dark grays to black while preserving the values in the middle.
+
+.. image:: screenshots/08curve_cut.png
+ :width: 800px
+
+4 Retouch details
+~~~~~~~~~~~~~~~~~
+
+Therer might be small details that don't look right yet, such as the image's background color or small highlights that
+merge into the background now. You can manually change the color of any detail now using the GIMP's flood-fill tool.
+
+If you don't want the image's background to show up on the final PCB at all, just make it black.
+
+Particularly on low-resolution source images it may make sense to apply a blur with a radius similar to the following
+newsprint filter's cell size (10px) to smooth out the dot pattern generated by the newsprint filter.
+
+.. image:: screenshots/09retouch.png
+ :width: 800px
+
+In the following example, I retouched the highlights in the hair of the character in the picture to make them completely
+white instead of light-gray, so they still stand out nicely in the finished picture.
+
+.. image:: screenshots/10retouched.png
+ :width: 800px
+
+5 Run the newsprint filter
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Now, run the GIMP's newsprint filter, under filters, distorts, newsprint.
+
+The first important settings is the spot size, which should be larger than your PCB's minimum detail size (about 10px
+with ``gerbolyze render`` default settings for good-quality silkscreen). In general the cheap and fast standard option of chinese PCB houses will require a larger detail size, but when you order specialty options like large size, 4-layer or non-green color along with a longer turnaround time you'll get much better-quality silk screen.
+
+The second important setting is oversampling, which should be set to four or slightly higher. This improves the result
+of the edge reconstruction of ``gerbolyze vectorize``.
+
+.. image:: screenshots/11newsprint.png
+ :width: 800px
+
+The following are examples on the detail resulting from the newsprint filter.
+
+.. image:: screenshots/12newsprint.png
+ :width: 800px
+
+6 Export the image for use with ``gerbolyze vectorize``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Simply export the image as a PNG file. Below are some pictures of the output ``gerbolyze vectorize`` produced for this
+example.
+
+.. image:: screenshots/14result_cut.png
+ :width: 800px
+
+.. image:: screenshots/15result_cut.png
+ :width: 800px
+
+Manufacturing Considerations
+----------------------------
+
+The main consideration when designing artwork for PCB processes is the processes' trace/space design rule. The two
+things you can do here is one, to be creative with graphical parts of the design and avoid extremely narrow lines,
+wedges or other thin features that will not come out well. Number two is to keep detail in raster images several times
+larger than the manufacturing processes native capability. For example, to target a trace/space design rule of 100 µm,
+the smallest detail in embedded raster graphics should not be much below 1mm.
+
+Gerbolyze's halftone vectorizers have built-in support for trace/space design rules. While they can still produce small
+artifacts that violate these rules, their output should be close enough to satifsy board houses and close enough for the
+result to look good. The way gerbolyze does this is to clip the halftone cell's values to zero whenevery they get too
+small, and to forcefully split or merge two neighboring cells when they get too close. While this process introduces
+slight steps at the top and bottom of grayscale response, for most inputs these are not noticeable.
+
+On the other hand, for SVG vector elements as well as for traced raster images, Gerbolyze cannot help with these design
+rules. There is no heuristic that would allow Gerbolyze to non-destructively "fix" a design here, so all that's on the
+roadmap here is to eventually include a gerber-level design rule checker.
+
+As far as board houses go, I have made good experiences with the popular Chinese board houses. In my experience, JLC
+will just produce whatever you send them with little fucks being given about design rule adherence or validity of the
+input gerbers. This is great if you just want artistic circuit boards without much of a hassle, and you don't care if
+they come out exactly as you imagined. The worst I've had happen was when an older version of gerbolyze generated
+polygons with holes assuming standard fill-rule processing. The in the board house's online gerber viewer things looked
+fine, and neither did they complain during file review. However, the resulting boards looked completely wrong because
+all the dark halftones were missing.
+
+PCBWay on the other hand has a much more rigurous file review process. They will complain when you throw
+illegal garbage gerbers at them, and they will helpfully guide you through your design rule violations. In this way you
+get much more of a professional service from them and for designs that have to be functional their higher level of
+scrutiny definitely is a good thing. For the design you saw in the first picture in this article, I ended up begging
+them to just plot my files if it doesn't physically break their machines and to their credit, while they seemed unhappy
+about it they did it and the result looks absolutely stunning.
+
+PCBWay is a bit more expensive on their lowest-end offering than JLC, but I found that for anything else (large boards,
+multi-layer, gold plating etc.) their prices match. PCBWay offers a much broader range of manufacturing options such as
+flexible circuit boards, multi-layer boards, thick or thin substrates and high-temperature substrates.
+
+When in doubt about how your design is going to come out on the board, do not hesitate to contact your board house. Most
+of the end customer-facing online PCB services have a number of different factories that do a number of different
+fabrication processes for them depending on order parameters. Places like PCBWay have exceptional quality control and
+good customer service, but that is mostly focused on the technical aspects of the PCB. If you rely on visual aspects
+like silkscreen uniformity or solder mask color that is a strong no concern to everyone else in the electronics
+industry, you may find significant variations between manufacturers or even between orders with the same manufacturer
+and you may encounter challenges communicating your requirements.
+
+Limitations
+-----------
+
+SVG raster features
+~~~~~~~~~~~~~~~~~~~
+
+Currently, SVG masks and filters are not supported. Though SVG is marketed as a "vector graphics format", these two
+features are really raster primitives that all SVG viewers perform at the pixel level after rasterization. Since
+supporting these would likely not end up looking like what you want, it is not a planned feature. If you need masks or
+filters, simply export the relevant parts of the SVG as a PNG then include that in your template.
+
+Gerber pass-through
+~~~~~~~~~~~~~~~~~~~
+
+Since gerbolyze has to composite your input gerbers with its own output, it has to fully parse and re-serialize them.
+gerbolyze gerbonara_ for all its gerber parsing needs. Thus, gerbonara will interpret your gerbers and output will be in
+gerbonara's gerber "dialect". If you find a corner case where this does not work and the output looks wrong, please file
+a bug report with an example file on the gerbonara_ bug tracker. *Always* check the output files for errors before
+submitting them to production.
+
+Gerbolyze is provided without any warranty, but still please open an issue or `send me an email
+`__ if you find any errors or inconsistencies.
+
+Trace/Space design rule adherence
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+While the grayscale halftone vectorizers do a reasonable job adhering to a given trace/space design rule, they can still
+produce small parts of output that violate it. For the contour vectorizer as well as for all SVG primitives, you are
+responsible for adhering to design rules yourself as there is no algorithm that gerboyze could use to "fix" its input.
+
+A design rule checker is planned as a future addition to gerbolyze, but is not yet part of it. If in doubt, talk to your
+fab and consider doing a test run of your design before ordering assembled boards ;)
+
+Gallery
+-------
+
+.. image:: pics/sample3.jpg
+ :width: 400px
+
+For a demonstration of ``gerbolyze convert``, check out the `Gerbolyze Protoboard Index`_, where you can download gerber
+files for over 7.000 SMD and THT protoboard layouts.
+
+Licensing
+---------
+
+This tool is licensed under the rather radical AGPLv3 license. Briefly, this means that you have to provide users of a
+webapp using this tool in the backend with this tool's source.
+
+I get that some people have issues with the AGPL. In case this license prevents you from using this software, please
+send me `an email `__ and I can grant you an exception. I want this software to be useful to as
+many people as possible and I wouldn't want the license to be a hurdle to anyone. OTOH I see a danger of some cheap
+board house just integrating a fork into their webpage without providing their changes back upstream, and I want to
+avoid that so the default license is still AGPL.
+
+.. _usvg: https://github.com/RazrFalcon/resvg
+.. _Inkscape: https://inkscape.org/
+.. _pcb-tools: https://github.com/curtacircuitos/pcb-tools
+.. _pcb-tools-extension: https://github.com/opiopan/pcb-tools-extension
+.. _GIMP: https://gimp.org/
+.. _gerbonara: https://gitlab.com/gerbolyze/gerbonara
+.. _`Gerbolyze Protoboard Index`: https://dyna.kokoroyukuma.de/protos/
+
diff --git a/content/projects/gerbolyze/index.rst b/content/projects/gerbolyze/index.rst
new file mode 100644
index 0000000..0f0ed47
--- /dev/null
+++ b/content/projects/gerbolyze/index.rst
@@ -0,0 +1,17 @@
+---
+title: "Gerbolyze"
+external_links:
+ - name: Sources
+ url: "https://git.jaseg.de/gerbolyze.git"
+ - name: Issues
+ url: "https://github.com/jaseg/gerbolyze/issues"
+ - name: Docs
+ url: "https://gerbolyze.gitlab.io/gerbolyze"
+ - name: PyPI
+ url: "https://pypi.org/project/gerbolyze"
+summary: >
+ Gerbolyze is a tool that allows the modification of Gerber PCB artwork with a vector graphics editor like Inkscape.
+ Gerbolyze directly converts between SVG and Gerber, and accurately reproduces details that other tools can not.
+---
+
+.. include:: content/projects/gerbolyze/README.rst
diff --git a/projects/gerbolyze/pics/ex-flattening.png b/content/projects/gerbolyze/pics/ex-flattening.png
similarity index 100%
rename from projects/gerbolyze/pics/ex-flattening.png
rename to content/projects/gerbolyze/pics/ex-flattening.png
diff --git a/projects/gerbolyze/pics/ex-intersections.png b/content/projects/gerbolyze/pics/ex-intersections.png
similarity index 100%
rename from projects/gerbolyze/pics/ex-intersections.png
rename to content/projects/gerbolyze/pics/ex-intersections.png
diff --git a/projects/gerbolyze/pics/ex-strokes.png b/content/projects/gerbolyze/pics/ex-strokes.png
similarity index 100%
rename from projects/gerbolyze/pics/ex-strokes.png
rename to content/projects/gerbolyze/pics/ex-strokes.png
diff --git a/projects/gerbolyze/pics/ex-svg-joins.png b/content/projects/gerbolyze/pics/ex-svg-joins.png
similarity index 100%
rename from projects/gerbolyze/pics/ex-svg-joins.png
rename to content/projects/gerbolyze/pics/ex-svg-joins.png
diff --git a/projects/gerbolyze/pics/ex-svg-strokes.png b/content/projects/gerbolyze/pics/ex-svg-strokes.png
similarity index 100%
rename from projects/gerbolyze/pics/ex-svg-strokes.png
rename to content/projects/gerbolyze/pics/ex-svg-strokes.png
diff --git a/projects/gerbolyze/pics/ex-svg-winding.png b/content/projects/gerbolyze/pics/ex-svg-winding.png
similarity index 100%
rename from projects/gerbolyze/pics/ex-svg-winding.png
rename to content/projects/gerbolyze/pics/ex-svg-winding.png
diff --git a/projects/gerbolyze/pics/fr4_comparison2.jpg b/content/projects/gerbolyze/pics/fr4_comparison2.jpg
similarity index 100%
rename from projects/gerbolyze/pics/fr4_comparison2.jpg
rename to content/projects/gerbolyze/pics/fr4_comparison2.jpg
diff --git a/projects/gerbolyze/pics/pcbway_sample_01_small.jpg b/content/projects/gerbolyze/pics/pcbway_sample_01_small.jpg
similarity index 100%
rename from projects/gerbolyze/pics/pcbway_sample_01_small.jpg
rename to content/projects/gerbolyze/pics/pcbway_sample_01_small.jpg
diff --git a/projects/gerbolyze/pics/pcbway_sample_02_small.jpg b/content/projects/gerbolyze/pics/pcbway_sample_02_small.jpg
similarity index 100%
rename from projects/gerbolyze/pics/pcbway_sample_02_small.jpg
rename to content/projects/gerbolyze/pics/pcbway_sample_02_small.jpg
diff --git a/projects/gerbolyze/pics/pcbway_sample_03_small.jpg b/content/projects/gerbolyze/pics/pcbway_sample_03_small.jpg
similarity index 100%
rename from projects/gerbolyze/pics/pcbway_sample_03_small.jpg
rename to content/projects/gerbolyze/pics/pcbway_sample_03_small.jpg
diff --git a/projects/gerbolyze/pics/process-overview.png b/content/projects/gerbolyze/pics/process-overview.png
similarity index 100%
rename from projects/gerbolyze/pics/process-overview.png
rename to content/projects/gerbolyze/pics/process-overview.png
diff --git a/projects/gerbolyze/pics/process-overview.svg b/content/projects/gerbolyze/pics/process-overview.svg
similarity index 100%
rename from projects/gerbolyze/pics/process-overview.svg
rename to content/projects/gerbolyze/pics/process-overview.svg
diff --git a/projects/gerbolyze/pics/sample1.jpg b/content/projects/gerbolyze/pics/sample1.jpg
similarity index 100%
rename from projects/gerbolyze/pics/sample1.jpg
rename to content/projects/gerbolyze/pics/sample1.jpg
diff --git a/projects/gerbolyze/pics/sample2.jpg b/content/projects/gerbolyze/pics/sample2.jpg
similarity index 100%
rename from projects/gerbolyze/pics/sample2.jpg
rename to content/projects/gerbolyze/pics/sample2.jpg
diff --git a/projects/gerbolyze/pics/sample3.jpg b/content/projects/gerbolyze/pics/sample3.jpg
similarity index 100%
rename from projects/gerbolyze/pics/sample3.jpg
rename to content/projects/gerbolyze/pics/sample3.jpg
diff --git a/projects/gerbolyze/pics/subtract_example.png b/content/projects/gerbolyze/pics/subtract_example.png
similarity index 100%
rename from projects/gerbolyze/pics/subtract_example.png
rename to content/projects/gerbolyze/pics/subtract_example.png
diff --git a/projects/gerbolyze/pics/test_svg_readme.svg b/content/projects/gerbolyze/pics/test_svg_readme.svg
similarity index 100%
rename from projects/gerbolyze/pics/test_svg_readme.svg
rename to content/projects/gerbolyze/pics/test_svg_readme.svg
diff --git a/projects/gerbolyze/pics/test_svg_readme_composited.png b/content/projects/gerbolyze/pics/test_svg_readme_composited.png
similarity index 100%
rename from projects/gerbolyze/pics/test_svg_readme_composited.png
rename to content/projects/gerbolyze/pics/test_svg_readme_composited.png
diff --git a/projects/gerbolyze/pics/vec_contours_composited.png b/content/projects/gerbolyze/pics/vec_contours_composited.png
similarity index 100%
rename from projects/gerbolyze/pics/vec_contours_composited.png
rename to content/projects/gerbolyze/pics/vec_contours_composited.png
diff --git a/projects/gerbolyze/pics/vec_hexgrid_composited.png b/content/projects/gerbolyze/pics/vec_hexgrid_composited.png
similarity index 100%
rename from projects/gerbolyze/pics/vec_hexgrid_composited.png
rename to content/projects/gerbolyze/pics/vec_hexgrid_composited.png
diff --git a/projects/gerbolyze/pics/vec_poisson_composited.png b/content/projects/gerbolyze/pics/vec_poisson_composited.png
similarity index 100%
rename from projects/gerbolyze/pics/vec_poisson_composited.png
rename to content/projects/gerbolyze/pics/vec_poisson_composited.png
diff --git a/projects/gerbolyze/pics/vec_square_composited.png b/content/projects/gerbolyze/pics/vec_square_composited.png
similarity index 100%
rename from projects/gerbolyze/pics/vec_square_composited.png
rename to content/projects/gerbolyze/pics/vec_square_composited.png
diff --git a/projects/gerbolyze/screenshots/01import01.png b/content/projects/gerbolyze/screenshots/01import01.png
similarity index 100%
rename from projects/gerbolyze/screenshots/01import01.png
rename to content/projects/gerbolyze/screenshots/01import01.png
diff --git a/projects/gerbolyze/screenshots/02import02.png b/content/projects/gerbolyze/screenshots/02import02.png
similarity index 100%
rename from projects/gerbolyze/screenshots/02import02.png
rename to content/projects/gerbolyze/screenshots/02import02.png
diff --git a/projects/gerbolyze/screenshots/03paste.png b/content/projects/gerbolyze/screenshots/03paste.png
similarity index 100%
rename from projects/gerbolyze/screenshots/03paste.png
rename to content/projects/gerbolyze/screenshots/03paste.png
diff --git a/projects/gerbolyze/screenshots/04scale_cut.png b/content/projects/gerbolyze/screenshots/04scale_cut.png
similarity index 100%
rename from projects/gerbolyze/screenshots/04scale_cut.png
rename to content/projects/gerbolyze/screenshots/04scale_cut.png
diff --git a/projects/gerbolyze/screenshots/05position.png b/content/projects/gerbolyze/screenshots/05position.png
similarity index 100%
rename from projects/gerbolyze/screenshots/05position.png
rename to content/projects/gerbolyze/screenshots/05position.png
diff --git a/projects/gerbolyze/screenshots/06grayscale.png b/content/projects/gerbolyze/screenshots/06grayscale.png
similarity index 100%
rename from projects/gerbolyze/screenshots/06grayscale.png
rename to content/projects/gerbolyze/screenshots/06grayscale.png
diff --git a/projects/gerbolyze/screenshots/07curve_settings.png b/content/projects/gerbolyze/screenshots/07curve_settings.png
similarity index 100%
rename from projects/gerbolyze/screenshots/07curve_settings.png
rename to content/projects/gerbolyze/screenshots/07curve_settings.png
diff --git a/projects/gerbolyze/screenshots/08curve_cut.png b/content/projects/gerbolyze/screenshots/08curve_cut.png
similarity index 100%
rename from projects/gerbolyze/screenshots/08curve_cut.png
rename to content/projects/gerbolyze/screenshots/08curve_cut.png
diff --git a/projects/gerbolyze/screenshots/09retouch.png b/content/projects/gerbolyze/screenshots/09retouch.png
similarity index 100%
rename from projects/gerbolyze/screenshots/09retouch.png
rename to content/projects/gerbolyze/screenshots/09retouch.png
diff --git a/projects/gerbolyze/screenshots/10retouched.png b/content/projects/gerbolyze/screenshots/10retouched.png
similarity index 100%
rename from projects/gerbolyze/screenshots/10retouched.png
rename to content/projects/gerbolyze/screenshots/10retouched.png
diff --git a/projects/gerbolyze/screenshots/11newsprint.png b/content/projects/gerbolyze/screenshots/11newsprint.png
similarity index 100%
rename from projects/gerbolyze/screenshots/11newsprint.png
rename to content/projects/gerbolyze/screenshots/11newsprint.png
diff --git a/projects/gerbolyze/screenshots/11newsprint_settings.png b/content/projects/gerbolyze/screenshots/11newsprint_settings.png
similarity index 100%
rename from projects/gerbolyze/screenshots/11newsprint_settings.png
rename to content/projects/gerbolyze/screenshots/11newsprint_settings.png
diff --git a/projects/gerbolyze/screenshots/12newsprint.png b/content/projects/gerbolyze/screenshots/12newsprint.png
similarity index 100%
rename from projects/gerbolyze/screenshots/12newsprint.png
rename to content/projects/gerbolyze/screenshots/12newsprint.png
diff --git a/projects/gerbolyze/screenshots/13newsprint.png b/content/projects/gerbolyze/screenshots/13newsprint.png
similarity index 100%
rename from projects/gerbolyze/screenshots/13newsprint.png
rename to content/projects/gerbolyze/screenshots/13newsprint.png
diff --git a/projects/gerbolyze/screenshots/14newsprint.png b/content/projects/gerbolyze/screenshots/14newsprint.png
similarity index 100%
rename from projects/gerbolyze/screenshots/14newsprint.png
rename to content/projects/gerbolyze/screenshots/14newsprint.png
diff --git a/projects/gerbolyze/screenshots/14result_cut.png b/content/projects/gerbolyze/screenshots/14result_cut.png
similarity index 100%
rename from projects/gerbolyze/screenshots/14result_cut.png
rename to content/projects/gerbolyze/screenshots/14result_cut.png
diff --git a/projects/gerbolyze/screenshots/15result_cut.png b/content/projects/gerbolyze/screenshots/15result_cut.png
similarity index 100%
rename from projects/gerbolyze/screenshots/15result_cut.png
rename to content/projects/gerbolyze/screenshots/15result_cut.png
diff --git a/projects/gerbolyze/screenshots/16result_cut.png b/content/projects/gerbolyze/screenshots/16result_cut.png
similarity index 100%
rename from projects/gerbolyze/screenshots/16result_cut.png
rename to content/projects/gerbolyze/screenshots/16result_cut.png
diff --git a/projects/gerbolyze/screenshots/17caveat_cut.png b/content/projects/gerbolyze/screenshots/17caveat_cut.png
similarity index 100%
rename from projects/gerbolyze/screenshots/17caveat_cut.png
rename to content/projects/gerbolyze/screenshots/17caveat_cut.png
diff --git a/projects/gerbolyze/screenshots/18caveat_cut.png b/content/projects/gerbolyze/screenshots/18caveat_cut.png
similarity index 100%
rename from projects/gerbolyze/screenshots/18caveat_cut.png
rename to content/projects/gerbolyze/screenshots/18caveat_cut.png
diff --git a/content/projects/gerbonara/index.rst b/content/projects/gerbonara/index.rst
new file mode 100644
index 0000000..6069351
--- /dev/null
+++ b/content/projects/gerbonara/index.rst
@@ -0,0 +1,141 @@
+---
+title: "Gerbonara"
+external_links:
+ - name: Sources
+ url: "https://git.jaseg.de/gerbonara.git"
+ - name: Issues
+ url: "https://gitlab.com/gerbolyze/gerbonara/issues"
+ - name: Docs
+ url: "https://gerbolyze.gitlab.io/gerbonara"
+ - name: PyPI
+ url: "https://pypi.org/project/gerbonara"
+summary: >
+ Gerbonara is a user-friendly, powerful tool for reading, writing, modification and rendering of Gerber PCB artwork
+ from the command line or from Python code. Gerbonara supports the Gerber dialects of all industry-standard EDA
+ tools.
+---
+
+Gerbonara is a library to read, modify and write PCB manufacturing files such as Gerber, Excellon and IPC-356 through a
+pythonic API. Gerbonara can open a folder of manufacturing files, and parse file names and metadata to figure out which
+file contains what. Gerbonara is tested using an extensive library of real-world example files from CAD tools including
+KiCAD, Altium, Eagle, Allegro, gEDA, Fritzing, Siemens/Mentor Graphics PADS, and Target3001!.
+
+Gerbonara's API is built on two principles:
+
+**Meaningful, object-oriented API**
+ Gerbonara abstracts away the details of the underlying file format such as tool indices, coordinate notation and
+ graphical state, and presents meaningful "graphical objects" such as a `primitives.Line`,
+ `primitives.Arc`, or `Region` through its API. These objects can be easily created,
+ manipulated or deleted from code without breaking anything else. You can even copy graphical objects between files,
+ and Gerbonara will automatically convert coordinate format, units etc. for you. `GerberFile` and
+ `ExcellonFile` use the same types of `graphic objects `, so objects can be directly
+ copied between file types without conversion.
+
+**Unit-safety**
+ Gerbonara embeds physical `LengthUnit` information in all objects. The high-level API such as
+ `LayerStack.merge` or `GerberFile.offset` accepts arguments with an explicitly given unit and
+ automatically converts them as needed. Objects can be copied between `GerberFile` instances and unit
+ conversion will be handled transparently in the background.
+
+Gerbonara was started as an extensive refactoring of the pcb-tools_ and pcb-tools-extension_ packages. Both of these
+have statement-based APIs, that is, they parse input files into one python object for every line in the file. This means
+that when saving files they can recreate the input file almost byte by byte, but manipulating a file by changing
+statements without breaking things is *hard*.
+
+Gerbonara powers gerbolyze_, a tool for converting SVG_ vector graphics files into Gerber, and embedding SVG_ into
+existing Gerber files exported from a normal PCB tool for artistic purposes.
+
+Features
+========
+
+ * File I/O
+ * Gerber, Excellon (drill file), IPC-356 (netlist) read and write
+ * supports file-level operations: offset, rotate, merge for all file types
+ * Modification API (`GraphicObject`)
+ * Rendering API (`GraphicPrimitive`)
+ * SVG export
+ * Full aperture macro support, including transformations (offset, rotation)
+
+Quick Start
+===========
+
+First, install gerbonara from PyPI using pip:
+
+.. code-block:: shell
+
+ pip install --user gerbonara
+
+Then, you are ready to read and write gerber files:
+
+.. code-block:: python
+
+ from gerbonara import LayerStack
+
+ stack = LayerStack.from_directory('output/gerber')
+ w, h = stack.outline.size('mm')
+ print(f'Board size is {w:.1f} mm x {h:.1f} mm')
+
+Command-Line Interface
+======================
+
+Gerbonara comes with a `built-in command-line interface` that has functions for analyzing, rendering,
+modifying, and merging Gerber files. To access it, use either the ``gerbonara`` command that is part of the python
+package, or run ``python -m gerbonara`` For a list of functions or help on their usage, you can use:
+
+.. code:: console
+
+ $ python -m gerbonara --help
+ [...]
+ $ python -m gerbonara render --help
+
+Development
+===========
+
+Gerbonara is developed on Gitlab under the gerbolyze org:
+
+https://gitlab.com/gerbolyze/gerbonara/
+
+A mirror of the repository can be found at:
+
+https://git.jaseg.de/gerbonara
+
+Our issue tracker is also on Gitlab:
+
+https://gitlab.com/gerbolyze/gerbonara/-/issues
+
+The documentation can be found at gitlab:
+
+https://gerbolyze.gitlab.io/gerbonara/
+
+With Gerbonara, we aim to support as many different format variants as possible. If you have a file that Gerbonara can't
+open, please file an issue on our issue tracker. Even if Gerbonara can open all your files, for regression testing we
+are very interested in example files generated by any CAD or CAM tool that is not already on the list of supported
+tools.
+
+Supported CAD Tools
+===================
+
+Compatibility with the output of these CAD tools is tested as part of our test suite using example files generated by
+these tools. Note that not all of these tools come with default Gerber file naming rules, so YMMV if your Gerbers use
+some non-standard naming convention.
+
+ * Allegro
+ * Altium
+ * Diptrace
+ * Eagle
+ * EasyEDA
+ * Fritzing
+ * gEDA
+ * KiCAD
+ * pcb-rnd
+ * Siemens / Mentor Graphics Xpedition
+ * Siemens PADS
+ * Target 3001!
+ * Upverter
+ * Zuken CR-8000
+
+.. _pcb-tools: https://github.com/opiopan/pcb-tools-extension
+.. _pcb-tools-extension: https://github.com/curtacircuitos/pcb-tools/issues
+.. _gerbolyze: https://github.com/jaseg/gerbolyze
+.. _SVG: https://en.wikipedia.org/wiki/Scalable_Vector_Graphics
+
diff --git a/content/projects/kicoil/blahaj-demo.png b/content/projects/kicoil/blahaj-demo.png
new file mode 100644
index 0000000..2b95b66
Binary files /dev/null and b/content/projects/kicoil/blahaj-demo.png differ
diff --git a/content/projects/kicoil/index.rst b/content/projects/kicoil/index.rst
new file mode 100644
index 0000000..1590449
--- /dev/null
+++ b/content/projects/kicoil/index.rst
@@ -0,0 +1,23 @@
+---
+title: "kicoil"
+date: 2025-12-28T23:42:00+02:00
+external_links:
+ - name: Sources
+ url: "https://git.jaseg.de/kicoil.git"
+ - name: Issue tracker
+ url: "https://codeberg.org/jaseg/kicoil/issues/"
+summary: >
+ Planar inductor supporting arbitrary shapes of spiral coils, toroidal coils, and hybrids
+---
+
+kicoil
+======
+
+.. image:: blahaj-demo.png
+ :width: 400px
+
+KiCoil generates planar inductors as footprints. It supports arbitrary shapes with presets such as circles, rectangles,
+circular sectors, and SVG import for arbitrary shapes. It can generate spiral and toroid inductors as well as arbitrary
+intermediates between spiral and toroid inductors. By playing around with this, you can create inductors that have lower
+parasitics and higher self-resonant frequency than standard multilayer spiral inductors.
+
diff --git a/content/projects/kimesh/README.rst b/content/projects/kimesh/README.rst
new file mode 100644
index 0000000..2c6ac45
--- /dev/null
+++ b/content/projects/kimesh/README.rst
@@ -0,0 +1,64 @@
+KiCAD Security Mesh Generator
+=============================
+
+.. image:: kicad-mesh-result-large.png
+ :width: 800
+ :alt: A screenshot of KiCAD showing a PCB security mesh generated by KiMesh.
+
+This repository contains KiMesh, a KiCAD pcbnew plugin that generates security mesh traces on a KiCAD PCB.
+
+Installation
+------------
+
+KiMesh has two parts: The pcbnew plugin that generates the traces, and the magic footprints that you use to tell the
+plugin how many traces of which dimensions to generate where.
+
+To install the plugin, copy the "kimesh" directory into your KiCAD installation's scripting plugin folder. Usually, this
+is `~/.config/kicad/scripting/plugins/` for KiCAD stable installations or
+`~/.config/kicad/[major version].99/scripting/plugins/` for nightly builds. On Windows, these paths can be found in your
+user account's `AppData/Roaming` directory.
+
+To install the footprint libraries, the easiest way is to download the library zip from the project's repo
+`[link] `__, unpack it to your project folder, and
+add the unpacked libraries as project-specific libraries through KiCad's library management thingy.
+
+Usage
+-----
+
+To work, KiMesh requires four things:
+
+1. An area free of other features such as footprints or traces where to generate the mesh.
+2. One or more "graphic polygons" on a drawing layer that specify the area of the mesh.
+3. A closed board outline on the `Edge.Cuts` layer.
+4. One of the magic footprints from the KiMesh anchor library that defines the mesh's number of wires and their
+ dimensions, and tells KiMesh where to start the mesh and in which direction to start it.
+
+You can choose any layer for the outline polygons, such as the pre-defined `User.Eco1` or `User.X` layers, or you can
+define your own. You just have to select that layer later in KiMesh's generator dialog. Note that KiMesh only processes
+graphic polygons on that layer, and ignores other shapes such as lines, rectangles or circles. You can still use other
+shapes, but you have to manually convert them to polygons before running KiMesh. To convert other shapes to
+a polygon, select them, open the context menu with a right click, then choose the "Create from Selection 🞂 Create
+Polygon from Selection" entry. For rectangles or circles, use the "Use Centerlines" option. For lines or arcs, use the
+"Create bounding hull" option.
+
+Place the magic anchor footprint on the outline of the mesh's shape polygons so that you have space to route out the
+traces. The anchor footprint has an arrow on the `F.Fab` layer that indicates in which direction the mesh will be
+generated.
+
+I recommend adding the mesh to the schematic with one of KiCad's built-in `Connector_02xN_Top_Bottom` footprints. For a
+mesh with k wires, choose a symbol with two rows of 2k pins each. For instance, for two mesh wires, choose
+`Connector_02x04_Top_bottom`. Then assign one of the magic footprints to that symbol. To avoid DRC warnings, join the
+two halves of the mesh on the output side of the footprint. That's the right side in default orientation, where the
+higher-numbered pins are.
+
+.. image:: screenshot-mesh-schematic.png
+ :width: 800
+ :alt: A screenshot of the connector footprint mentioned in the previous paragraph, shown conencted up as described in
+ KiCad's schematic editor.
+
+Theory of operation
+-------------------
+
+I have published a post_ on my blog on the theory of operation behind KiMesh.
+
+.. _post: https://jaseg.de/blog/kicad-mesh-plugin/
diff --git a/content/projects/kimesh/index.rst b/content/projects/kimesh/index.rst
new file mode 100644
index 0000000..8613e3c
--- /dev/null
+++ b/content/projects/kimesh/index.rst
@@ -0,0 +1,16 @@
+---
+title: "KiMesh"
+date: 2023-10-04T23:42:00+02:00
+external_links:
+ - name: Sources
+ url: "https://git.jaseg.de/kimesh.git"
+ - name: Issues
+ url: "https://github.com/jaseg/kimesh/issues"
+ - name: Docs
+ url: "https://jaseg.de/projects/kimesh"
+summary: >
+ KiMesh is a KiCad plugin that automatically creates security meshes with two or traces covering an
+ arbitrarily-shaped outline on the board.
+---
+
+.. include:: content/projects/kimesh/README.rst
diff --git a/projects/kimesh/kicad-mesh-result-large.png b/content/projects/kimesh/kicad-mesh-result-large.png
similarity index 100%
rename from projects/kimesh/kicad-mesh-result-large.png
rename to content/projects/kimesh/kicad-mesh-result-large.png
diff --git a/projects/kimesh/screenshot-mesh-schematic.png b/content/projects/kimesh/screenshot-mesh-schematic.png
similarity index 100%
rename from projects/kimesh/screenshot-mesh-schematic.png
rename to content/projects/kimesh/screenshot-mesh-schematic.png
diff --git a/projects/lolcat-c/LOLCat-Rainbow.jpg b/content/projects/lolcat-c/LOLCat-Rainbow.jpg
similarity index 100%
rename from projects/lolcat-c/LOLCat-Rainbow.jpg
rename to content/projects/lolcat-c/LOLCat-Rainbow.jpg
diff --git a/content/projects/lolcat-c/index.rst b/content/projects/lolcat-c/index.rst
new file mode 100644
index 0000000..858f6de
--- /dev/null
+++ b/content/projects/lolcat-c/index.rst
@@ -0,0 +1,107 @@
+---
+title: "lolcat-c"
+external_links:
+ - name: Sources
+ url: "https://git.jaseg.de/lolcat.git"
+ - name: Github
+ url: "https://github.com/jaseg/lolcat"
+ - name: Issues
+ url: "https://github.com/jaseg/lolcat/issues"
+summary: >
+ lolcat-c is a small, high-performance re-implementation of the
+ `lolcat `__
+ rainbow cat utility. lolcat-c is meant as a lolcat that you can actually use in production. It is fast, not slowing
+ down whatever you pipe through it, and it robustly handles real-world terminal output including escape sequences.
+---
+
+What?
+=====
+
+.. image:: LOLCat-Rainbow.jpg
+
+Screenshot
+==========
+
+.. image:: screenshot.png
+
+.. image:: sl.gif
+
+Installation
+============
+
+Archlinux
+---------
+
+There's an `AUR package `__:
+
+.. code:: sh
+
+ $ git clone https://aur.archlinux.org/packages/c-lolcat
+ $ cd c-lolcat
+ $ makepkg -csi
+
+Fedora
+------
+
+.. code:: sh
+
+ $ dnf install lolcat
+
+Ubuntu (Snap)
+-------------
+
+See `this awesome blog post by a kind person from the internet `__:
+
+.. code:: sh
+
+ $ snap install lolcat-c
+
+Mac
+---
+
+Build loclcat with:
+
+.. code:: sh
+
+ $ make lolcat
+
+...and put the resulting binary at a place of your choice.
+
+Others
+------
+
+.. code:: sh
+
+ $ make && sudo make install
+
+Why?
+====
+
+This `lolcat` clone is an attempt to reduce the world's carbon dioxide emissions by optimizing inefficient code. It's
+>10x as fast and <0.1% as large as the original one.
+
+.. code:: sh
+
+ newton~/d/lolcat <3 dmesg>foo
+ newton~/d/lolcat <3 time upstream/bin/lolcat foo
+ 13.51user 1.34system 0:15.99elapsed 92%CPU (0avgtext+0avgdata 10864maxresident)k
+ 0inputs+0outputs (0major+1716minor)pagefaults 0swaps
+ newton~/d/lolcat <3 time ./lolcat foo
+ 0.02user 0.00system 0:00.09elapsed 34%CPU (0avgtext+0avgdata 1936maxresident)k
+ 0inputs+0outputs (0major+117minor)pagefaults 0swaps
+
+Bonus comparison with `python-lolcat `__:
+
+.. code:: sh
+
+ newton~/d/lolcat <3 dmesg>foo
+ $ time python-lolcat foo
+ 12.27user 0.00system 0:12.29elapsed 99%CPU (0avgtext+0avgdata 11484maxresident)k
+ 0inputs+0outputs (0major+1627minor)pagefaults 0swaps
+ $ time c-lolcat foo
+ 0.29user 0.00system 0:00.30elapsed 98%CPU (0avgtext+0avgdata 468maxresident)k
+ 0inputs+0outputs (0major+21minor)pagefaults 0swaps
+
+(Read: `c-lolcat << python-lolcat << ruby-lolcat`)
+
+
diff --git a/projects/lolcat-c/screenshot.png b/content/projects/lolcat-c/screenshot.png
similarity index 100%
rename from projects/lolcat-c/screenshot.png
rename to content/projects/lolcat-c/screenshot.png
diff --git a/projects/lolcat-c/sl.gif b/content/projects/lolcat-c/sl.gif
similarity index 100%
rename from projects/lolcat-c/sl.gif
rename to content/projects/lolcat-c/sl.gif
diff --git a/content/projects/python-mpv/README.rst b/content/projects/python-mpv/README.rst
new file mode 100644
index 0000000..26815d1
--- /dev/null
+++ b/content/projects/python-mpv/README.rst
@@ -0,0 +1,401 @@
+.. vim: tw=120 sw=4 et
+
+python-mpv is a ctypes-based python interface to the mpv media player. It gives you more or less full control of all
+features of the player, just as the lua interface does.
+
+Installation
+------------
+
+.. code:: bash
+
+ pip install mpv
+
+
+...though you can also realistically just copy `mpv.py`_ into your project as it's all nicely contained in one file.
+
+Requirements
+~~~~~~~~~~~~
+
+libmpv
+......
+``libmpv.so`` either locally (in your current working directory) or somewhere in your system library search path. This
+module is somewhat lenient as far as ``libmpv`` versions are concerned but since ``libmpv`` is changing quite frequently
+you'll only get all the newest features when using an up-to-date version of this module. The unit tests for this module
+do some basic automatic version compatibility checks. If you discover anything missing here, please open an `issue`_ or
+submit a `pull request`_ on github.
+
+On Windows you can place libmpv anywhere in your ``%PATH%`` (e.g. next to ``python.exe``) or next to this module's
+``mpv.py``. Before falling back to looking in the mpv module's directory, python-mpv uses the DLL search order built
+into ctypes, which is different to the one Windows uses internally. Consult `this stackoverflow post
+`__ for details.
+
+Python >= 3.7 (officially)
+..........................
+The ``main`` branch officially only supports recent python releases (3.5 onwards), but there is the somewhat outdated
+but functional `py2compat branch`_ providing Python 2 compatibility.
+
+.. _`py2compat branch`: https://github.com/jaseg/python-mpv/tree/py2compat
+.. _`issue`: https://github.com/jaseg/python-mpv/issues
+.. _`pull request`: https://github.com/jaseg/python-mpv/pulls
+
+Supported Platforms
+...................
+
+**Linux**, **Windows** and **OSX** all seem to work mostly fine. For some notes on the installation on Windows see
+`this comment`__. Shared library handling is quite bad on windows, so expect some pain there. On OSX there seems to be
+some bug int the event logic. See `issue 36`_ and `issue 61`_ for details. Creating a pyQT window and having mpv draw
+into it seems to be a workaround (about 10loc), but in case you want this fixed please weigh in on the issue tracker
+since right now there is not many OSX users.
+
+.. __: https://github.com/jaseg/python-mpv/issues/60#issuecomment-352719773
+.. _`issue 61`: https://github.com/jaseg/python-mpv/issues/61
+.. _`issue 36`: https://github.com/jaseg/python-mpv/issues/36
+
+Usage
+-----
+
+.. code:: python
+
+ import mpv
+ player = mpv.MPV(ytdl=True)
+ player.play('https://youtu.be/DOmdB7D-pUU')
+ player.wait_for_playback()
+
+python-mpv mostly exposes mpv's built-in API to python, adding only some porcelain on top. Most "`input commands `_" are mapped to methods of the MPV class. Check out these methods and their docstrings in `the source `__ for things you can do. Additional controls and status information are exposed through `MPV properties `_. These can be accessed like ``player.metadata``, ``player.fullscreen`` and ``player.loop_playlist``.
+
+Threading
+~~~~~~~~~
+
+The ``mpv`` module starts one thread for event handling, since MPV sends events that must be processed quickly. The
+event queue has a fixed maxmimum size and some operations can cause a large number of events to be sent.
+
+If you want to handle threading yourself, you can pass ``start_event_thread=False`` to the ``MPV`` constructor and
+manually call the ``MPV`` object's ``_loop`` function. If you have some strong need to not use threads and use some
+external event loop (such as asyncio) instead you can do that, too with some work. The API of the backend C ``libmpv``
+has a function for producing a sort of event file descriptor for a handle. You can use that to produce a file descriptor
+that can be passed to an event loop to tell it to wake up the python-mpv event handler on every incoming event.
+
+All API functions are thread-safe. If one is not, please file an issue on github.
+
+Advanced Usage
+~~~~~~~~~~~~~~
+
+Logging, Properties, Python Key Bindings, Screenshots and youtube-dl
+....................................................................
+
+.. code:: python
+
+ #!/usr/bin/env python3
+ import mpv
+
+ def my_log(loglevel, component, message):
+ print('[{}] {}: {}'.format(loglevel, component, message))
+
+ player = mpv.MPV(log_handler=my_log, ytdl=True, input_default_bindings=True, input_vo_keyboard=True)
+
+ # Property access, these can be changed at runtime
+ @player.property_observer('time-pos')
+ def time_observer(_name, value):
+ # Here, _value is either None if nothing is playing or a float containing
+ # fractional seconds since the beginning of the file.
+ print('Now playing at {:.2f}s'.format(value))
+
+ player.fullscreen = True
+ player.loop_playlist = 'inf'
+ # Option access, in general these require the core to reinitialize
+ player['vo'] = 'gpu'
+
+ @player.on_key_press('q')
+ def my_q_binding():
+ print('THERE IS NO ESCAPE')
+
+ @player.on_key_press('s')
+ def my_s_binding():
+ pillow_img = player.screenshot_raw()
+ pillow_img.save('screenshot.png')
+
+ player.play('https://youtu.be/DLzxrzFCyOs')
+ player.wait_for_playback()
+
+ del player
+
+Skipping silence using libav filters
+....................................
+
+The following code uses the libav silencedetect filter to skip silence at the beginning of a file. It works by loading
+the filter, then parsing its output from mpv's log. Thanks to Sean DeNigris on github (#202) for the original code!
+
+.. code:: python
+
+ #!/usr/bin/env python3
+ import sys
+ import mpv
+
+ p = mpv.MPV()
+ p.play(sys.argv[1])
+
+ def skip_silence():
+ p.set_loglevel('debug')
+ p.af = 'lavfi=[silencedetect=n=-20dB:d=1]'
+ p.speed = 100
+ def check(evt):
+ toks = evt['event']['text'].split()
+ if 'silence_end:' in toks:
+ return float(toks[2])
+ p.time_pos = p.wait_for_event('log_message', cond=check)
+ p.speed = 1
+ p.af = ''
+
+ skip_silence()
+ p.wait_for_playback()
+
+Video overlays
+..............
+
+.. code:: python
+
+ #!/usr/bin/env python3
+ import time
+ from PIL import Image, ImageDraw, ImageFont
+ import mpv
+
+ player = mpv.MPV()
+
+ player.loop = True
+ player.play('test.webm')
+ player.wait_until_playing()
+
+ font = ImageFont.truetype('DejaVuSans.ttf', 40)
+
+ while not player.core_idle:
+
+ time.sleep(0.5)
+ overlay = player.create_image_overlay()
+
+ for pos in range(0, 500, 5):
+ ts = player.time_pos
+ if ts is None:
+ break
+
+ img = Image.new('RGBA', (400, 150), (255, 255, 255, 0))
+ d = ImageDraw.Draw(img)
+ d.text((10, 10), 'Hello World', font=font, fill=(0, 255, 255, 128))
+ d.text((10, 60), f't={ts:.3f}', font=font, fill=(255, 0, 255, 255))
+
+ overlay.update(img, pos=(2*pos, pos))
+ time.sleep(0.05)
+
+ overlay.remove()
+
+
+Playlist handling
+.................
+
+.. code:: python
+
+ #!/usr/bin/env python3
+ import mpv
+
+ player = mpv.MPV(ytdl=True, input_default_bindings=True, input_vo_keyboard=True)
+
+ player.playlist_append('https://youtu.be/PHIGke6Yzh8')
+ player.playlist_append('https://youtu.be/Ji9qSuQapFY')
+ player.playlist_append('https://youtu.be/6f78_Tf4Tdk')
+
+ player.playlist_pos = 0
+
+ while True:
+ # To modify the playlist, use player.playlist_{append,clear,move,remove}. player.playlist is read-only
+ print(player.playlist)
+ player.wait_for_playback()
+
+Directly feeding mpv data from python
+.....................................
+
+.. code:: python
+
+ #!/usr/bin/env python3
+ import mpv
+
+ player = mpv.MPV()
+ @player.python_stream('foo')
+ def reader():
+ with open('test.webm', 'rb') as f:
+ while True:
+ yield f.read(1024*1024)
+
+ player.play('python://foo')
+ player.wait_for_playback()
+
+Using external subtitles
+........................
+
+The easiest way to load custom subtitles from a file is to pass the ``--sub-file`` option to the ``loadfile`` call:
+
+.. code:: python
+
+ #!/usr/bin/env python3
+ import mpv
+
+ player = mpv.MPV()
+ player.loadfile('test.webm', sub_file='test.srt')
+ player.wait_for_playback()
+
+Note that you can also pass many other options to ``loadfile``. See the mpv docs for details.
+
+If you want to add subtitle files or streams at runtime, you can use the ``sub-add`` command. ``sub-add`` can only be
+called once the player is done loading the file and starts playing. An easy way to wait for this is to wait for the
+``core-idle`` property.
+
+.. code:: python
+
+ #!/usr/bin/env python3
+ import mpv
+
+ player = mpv.MPV()
+ player.play('test.webm')
+ player.wait_until_playing()
+ player.sub_add('test.srt')
+ player.wait_for_playback()
+
+Using MPV's built-in GUI
+........................
+
+python-mpv is using mpv via libmpv. libmpv is meant for embedding into other applications and by default disables most
+GUI features such as the OSD or keyboard input. To enable the built-in GUI, use the following options when initializing
+the MPV instance. See `Issue 102`_ for more details
+
+.. _`issue 102`: https://github.com/jaseg/python-mpv/issues/61
+
+.. code:: python
+
+ # Enable the on-screen controller and keyboard shortcuts
+ player = mpv.MPV(input_default_bindings=True, input_vo_keyboard=True, osc=True)
+
+ # Alternative version using the old "floating box" style on-screen controller
+ player = mpv.MPV(player_operation_mode='pseudo-gui',
+ script_opts='osc-layout=box,osc-seekbarstyle=bar,osc-deadzonesize=0,osc-minmousemove=3',
+ input_default_bindings=True,
+ input_vo_keyboard=True,
+ osc=True)
+
+PyQT embedding
+..............
+
+.. code:: python
+
+ #!/usr/bin/env python3
+ import mpv
+ import sys
+
+ from PyQt5.QtWidgets import *
+ from PyQt5.QtCore import *
+
+ class Test(QMainWindow):
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.container = QWidget(self)
+ self.setCentralWidget(self.container)
+ self.container.setAttribute(Qt.WA_DontCreateNativeAncestors)
+ self.container.setAttribute(Qt.WA_NativeWindow)
+ player = mpv.MPV(wid=str(int(self.container.winId())),
+ vo='x11', # You may not need this
+ log_handler=print,
+ loglevel='debug')
+ player.play('test.webm')
+
+ app = QApplication(sys.argv)
+
+ # This is necessary since PyQT stomps over the locale settings needed by libmpv.
+ # This needs to happen after importing PyQT before creating the first mpv.MPV instance.
+ import locale
+ locale.setlocale(locale.LC_NUMERIC, 'C')
+ win = Test()
+ win.show()
+ sys.exit(app.exec_())
+
+PyGObject embedding
+...................
+
+.. code:: python
+
+ #!/usr/bin/env python3
+ import gi
+
+ import mpv
+
+ gi.require_version('Gtk', '3.0')
+ from gi.repository import Gtk
+
+
+ class MainClass(Gtk.Window):
+
+ def __init__(self):
+ super(MainClass, self).__init__()
+ self.set_default_size(600, 400)
+ self.connect("destroy", self.on_destroy)
+
+ widget = Gtk.Frame()
+ self.add(widget)
+ self.show_all()
+
+ # Must be created >after< the widget is shown, else property 'window' will be None
+ self.mpv = mpv.MPV(wid=str(widget.get_property("window").get_xid()))
+ self.mpv.play("test.webm")
+
+ def on_destroy(self, widget, data=None):
+ self.mpv.terminate()
+ Gtk.main_quit()
+
+
+ if __name__ == '__main__':
+ # This is necessary since like Qt, Gtk stomps over the locale settings needed by libmpv.
+ # Like with Qt, this needs to happen after importing Gtk but before creating the first mpv.MPV instance.
+ import locale
+ locale.setlocale(locale.LC_NUMERIC, 'C')
+
+ application = MainClass()
+ Gtk.main()
+
+Using OpenGL from PyGObject
+...........................
+
+Just like it is possible to render into a GTK widget through X11 windows, it `also is possible to render into a GTK
+widget using OpenGL `__ through this python API.
+
+Using OpenGL from PyQt5/QML
+...........................
+
+Robozman_ has mangaed to `make mpv render into a PyQt5/QML widget using OpenGL
+`__ through this python API.
+
+Using mpv inside imgui inside OpenGL via GLFW
+.............................................
+
+dfaker_ has written a demo (`link `__) that uses mpv to render video into an `imgui `__ UI running on an OpenGL context inside `GLFW `__. Check out their demo to see how to integrate with imgui/OpenGL and how to access properties and manage the lifecycle of an MPV instance.
+
+Running tests
+-------------
+
+Use pytest to run tests.
+
+Coding Conventions
+------------------
+
+The general aim is `PEP 8`_, with liberal application of the "consistency" section. 120 cells line width. Four spaces.
+No tabs. Probably don't bother making pure-formatting PRs except if you think it *really* helps readability or it
+*really* irks you if you don't.
+
+License
+-------
+
+python-mpv inherits the underlying libmpv's license, which can be either GPLv2 or later (default) or LGPLv2.1 or later.
+For details, see `the mpv copyright page`_.
+
+.. _`PEP 8`: https://www.python.org/dev/peps/pep-0008/
+.. _`mpv.py`: https://raw.githubusercontent.com/jaseg/python-mpv/main/mpv.py
+.. _cosven: https://github.com/cosven
+.. _Robozman: https://gitlab.com/robozman
+.. _dfaker: https://github.com/dfaker
+.. _`the mpv copyright page`: https://github.com/mpv-player/mpv/blob/master/Copyright
+
diff --git a/content/projects/python-mpv/index.rst b/content/projects/python-mpv/index.rst
new file mode 100644
index 0000000..12b1c18
--- /dev/null
+++ b/content/projects/python-mpv/index.rst
@@ -0,0 +1,18 @@
+---
+title: "python-mpv"
+external_links:
+ - name: Sources
+ url: "https://git.jaseg.de/python-mpv.git"
+ - name: Issues
+ url: "https://github.com/jaseg/python-mpv/issues"
+ - name: Docs
+ url: "https://neinseg.gitlab.io/python-mpv"
+ - name: PyPI
+ url: "https://pypi.org/project/mpv"
+summary: >
+ python-mpv is a small, ctypes-based Python library wrapping the libmpv media player library. Despite its small size
+ and simple API, python-mpv allows advanced control over libmpv and beyond simple remote control of mpv can be used
+ to embed mpv in OpenGL, Qt, and GTK-based Python applications.
+---
+
+.. include:: content/projects/python-mpv/README.rst
diff --git a/content/projects/svg-flatten/index.rst b/content/projects/svg-flatten/index.rst
new file mode 100644
index 0000000..ae9a8f3
--- /dev/null
+++ b/content/projects/svg-flatten/index.rst
@@ -0,0 +1,22 @@
+---
+title: "svg-flatten"
+external_links:
+ - name: Sources
+ url: "https://git.jaseg.de/gerbolyze.git/tree/svg-flatten?h=main"
+ - name: Issues
+ url: "https://github.com/jaseg/gerbolyze/issues"
+ - name: Docs
+ url: "https://gerbolyze.gitlab.io/svg-flatten"
+summary: >
+ svg-flatten is a command-line utility that performs vector occlusion and clipping on SVG files, producing a
+ flattened SVG file without overlapping elements, without changing what the file looks like. svg-flatten is used as a
+ part of gerbolyze.
+---
+
+svg-flatten is a command-line utility that performs vector occlusion and clipping on SVG files, producing a flattened
+SVG file without overlapping elements, without changing what the file looks like. svg-flatten is used as a part of
+gerbolyze.
+
+I developed svg-flatten as part of gerbolyze_, but it can be used independently.
+
+.. _gerbolyze: {{< ref "projects/gerbolyze" >}}
diff --git a/content/projects/wsdiff/index.rst b/content/projects/wsdiff/index.rst
new file mode 100644
index 0000000..44cd4c3
--- /dev/null
+++ b/content/projects/wsdiff/index.rst
@@ -0,0 +1,63 @@
+---
+title: "wsdiff"
+external_links:
+ - name: Sources
+ url: "https://git.jaseg.de/wsdiff.git"
+ - name: Issues
+ url: "https://github.com/jaseg/wsdiff/issues"
+ - name: PyPI
+ url: "https://pypi.org/project/wsdiff"
+summary: >
+ wsdiff is a command-line utility that produces self-contained, syntax-highlighted, HTML-formatted diffs that support
+ both unified and side-by-side diffs from a single source file using nothing but CSS magic.
+---
+
+wsdiff is a python script that produces a diff of two files or directories as a single, self-contained HTML file. The
+resulting diff works without Javascript and will automatically switch between inline and side-by-side formats depending
+on available screen space.
+
+Installation
+============
+
+.. code:: sh
+
+ $ pip install wsdiff
+
+Usage
+=====
+
+::
+
+ usage: wsdiff [-h] [-b] [-s SYNTAX_CSS] [-l LEXER] [-L] [-t PAGETITLE]
+ [-o OUTPUT] [--header] [--content]
+ [old] [new]
+
+ Given two source files or directories this application creates an html page
+ that highlights the differences between the two.
+
+ positional arguments:
+ old source file or directory to compare ("before" file)
+ new source file or directory to compare ("after" file)
+
+ options:
+ -h, --help show this help message and exit
+ -b, --open Open output file in a browser
+ -s SYNTAX_CSS, --syntax-css SYNTAX_CSS
+ Path to custom Pygments CSS file for code syntax
+ highlighting
+ -l LEXER, --lexer LEXER
+ Manually select pygments lexer (default: guess from
+ filename, use -L to list available lexers.)
+ -L, --list-lexers List available lexers for -l/--lexer
+ -t PAGETITLE, --pagetitle PAGETITLE
+ Override page title of output HTML file
+ -o OUTPUT, --output OUTPUT
+ Name of output file (default: stdout)
+ --header Only output HTML header with stylesheets and stuff,
+ and no diff
+ --content Only output HTML content, without header
+
+Example Output
+==============
+
+.. image:: latest.png
diff --git a/projects/wsdiff/latest.png b/content/projects/wsdiff/latest.png
similarity index 100%
rename from projects/wsdiff/latest.png
rename to content/projects/wsdiff/latest.png
diff --git a/deploy.sh b/deploy.sh
new file mode 100755
index 0000000..4679b53
--- /dev/null
+++ b/deploy.sh
@@ -0,0 +1,4 @@
+set -e
+hugo
+~/.cargo/bin/pagefind --site public
+rsync -avP public/ wendelstein:/var/www/jaseg.de/
diff --git a/docutils.conf b/docutils.conf
new file mode 100644
index 0000000..308c0f7
--- /dev/null
+++ b/docutils.conf
@@ -0,0 +1,3 @@
+[restructuredtext parser]
+syntax_highlight: short
+
diff --git a/hack/rst2html b/hack/rst2html
new file mode 100755
index 0000000..f9c9146
--- /dev/null
+++ b/hack/rst2html
@@ -0,0 +1,53 @@
+#!/usr/bin/env python3
+# Based on https://gist.github.com/mastbaum/2655700 for the basic plugin scaffolding
+
+import sys
+import re
+
+import docutils.core
+from docutils.transforms import Transform
+from docutils.nodes import TextElement, Inline, Text
+from docutils.parsers.rst import Directive, directives
+from docutils.writers.html4css1 import Writer, HTMLTranslator
+
+
+class UnfuckedHTMLTranslator(HTMLTranslator):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.in_literal_block = False
+
+ def visit_literal_block(self, node):
+ # Insert an empty "lineno" span before each line. We insert the line numbers using pure CSS in a ::before
+ # pseudo-element. This has the added advantage that the line numbers don't get included in text selection.
+ # These line number spans are also used to show line continuation markers when a line is wrapped.
+ self.in_literal_block = True
+ self.body.append(self.starttag(node, 'pre', CLASS='literal-block'))
+ self.body.append('')
+
+ def depart_literal_block(self, node):
+ self.in_literal_block = False
+ self.body.append('\n\n')
+
+ def visit_Text(self, node):
+ if self.in_literal_block:
+ for match in re.finditer('([^\n]*)(\n|$)', node.astext()):
+ text, end = match.groups()
+
+ if text:
+ super().visit_Text(Text(text))
+
+ if end == '\n':
+ if isinstance(node.parent, Inline):
+ self.depart_inline(node.parent)
+ self.body.append(f'\n')
+ if isinstance(node.parent, Inline):
+ self.visit_inline(node.parent)
+
+ else:
+ super().visit_Text(node)
+
+
+html_writer = Writer()
+html_writer.translator_class = UnfuckedHTMLTranslator
+docutils.core.publish_cmdline(writer=html_writer)
+
diff --git a/imprint/index.html b/imprint/index.html
deleted file mode 100644
index 096a92f..0000000
--- a/imprint/index.html
+++ /dev/null
@@ -1,152 +0,0 @@
-
-
-
- Impressum | Home
-
-
-
-
-
-
-
-
-
-
-
-
-