<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-GB">
	<id>https://www.penguindevelopment.org/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Link</id>
	<title>Penguin Development - User contributions [en-gb]</title>
	<link rel="self" type="application/atom+xml" href="https://www.penguindevelopment.org/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Link"/>
	<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/wiki/Special:Contributions/Link"/>
	<updated>2026-04-19T13:30:38Z</updated>
	<subtitle>User contributions</subtitle>
	<generator>MediaWiki 1.41.0</generator>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=Light_glyph_lamp&amp;diff=263</id>
		<title>Light glyph lamp</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=Light_glyph_lamp&amp;diff=263"/>
		<updated>2024-01-04T22:21:06Z</updated>

		<summary type="html">&lt;p&gt;Link: /* Testing the electronics */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The &#039;&#039;&#039;light glyph lamp&#039;&#039;&#039; is an open-source touch-activated LED lamp based on the light glyph seen in &#039;&#039;[[w:The Owl House|The Owl House]]&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
All source files can be found at [https://proj.penguindevelopment.org/light-glyph-lamp/].&lt;br /&gt;
&lt;br /&gt;
[[File:Light-glyph-lamp_demo.gif|frame|Demonstration video]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:Light-glyph-lamp_pcb-assembled.jpg|thumb|128px|Printed circuit board, assembled]]&lt;br /&gt;
&lt;br /&gt;
== 3D printable parts ==&lt;br /&gt;
&lt;br /&gt;
The exterior parts of the lamp can be 3D printed. FDM is highly recommended for the structural parts, although the orb and diffuser may use resin instead. The STL files and OpenSCAD source code can be found at [https://proj.penguindevelopment.org/light-glyph-lamp/light-glyph-lamp_printable-parts-0.2.tar.xz].&lt;br /&gt;
&lt;br /&gt;
=== Requirements ===&lt;br /&gt;
&lt;br /&gt;
* FDM 3D printer&lt;br /&gt;
* ~150 g of filament&lt;br /&gt;
&lt;br /&gt;
Diffuser and orb:&lt;br /&gt;
* ~50 g of translucent white filament&lt;br /&gt;
OR&lt;br /&gt;
* Resin printer&lt;br /&gt;
* ~30 ml of translucent white resin&lt;br /&gt;
&lt;br /&gt;
Glyph:&lt;br /&gt;
* ~10 g of translucent dark filament&lt;br /&gt;
* Multi-extrusion capability (optional but recommended)&lt;br /&gt;
OR&lt;br /&gt;
* Translucent dark resin&lt;br /&gt;
* Syringe with large-bore (~0.5 mm) needle&lt;br /&gt;
* UV torch or laser pointer&lt;br /&gt;
* ~10 g of PVA filament&lt;br /&gt;
&lt;br /&gt;
=== Main parts ===&lt;br /&gt;
&lt;br /&gt;
The box and tube may be printed with any filament. In the demonstration video&#039;s build, [https://colorfabb.com/woodfill colorFabb woodFill] is used for the box, and [https://www.esun3d.com/epa-cf-product/ eSUN ePA-CF] is used for the tube. The tube can be printed lying on its side, but &#039;&#039;do not&#039;&#039; use supports.&lt;br /&gt;
&lt;br /&gt;
The diffuser and orb should be printed in a translucent white filament or resin. In the demonstration video&#039;s build, [https://www.esun3d.com/eresin-pla-product/ clear eSUN eResin-PLA], dyed with SigWong white alcohol ink is used. Depending on the desired luminosity, the orb may get somewhat hot; in this case, PLA filament may be less suitable.&lt;br /&gt;
&lt;br /&gt;
=== Faceplate and glyph ===&lt;br /&gt;
&lt;br /&gt;
Printing the faceplate is somewhat more complex, as it requires two colours. Furthermore, the glyph should have a greater translucency than the other faceplate material, but a darker colour when not backlit. There are several ways to achieve this, two of which are highlighted here.&lt;br /&gt;
&lt;br /&gt;
==== Pure FDM with multi-extrusion or filament swapping ====&lt;br /&gt;
&lt;br /&gt;
In this approach, the STLs for the &#039;&#039;&#039;faceplate&#039;&#039;&#039; and the &#039;&#039;&#039;glyph&#039;&#039;&#039; are used. If using Cura as a slicer, set up a multi-extrusion printer, which [https://scholtzan.net/blog/cura-multicolor-single-extruder/ may be emulated using filament swaps]. Load the STLs, and assign each to its own extruder, then combine the models. In PrusaSlicer, first import the faceplate, then right-click and use &amp;quot;add part&amp;quot; to import the glyph. As in Cura, [https://forum.prusa3d.com/forum/postid/188236/ multi-extrusion may be emulated with filament swaps].&lt;br /&gt;
&lt;br /&gt;
Few filaments exist which are dark but translucent. One possibility is the [https://www.matterhackers.com/store/l/translucent-grey-mh-build-series-petg-filament-175mm-1kg/sk/MX9SD5D4 translucent grey PETG by Matterhackers]. Although it is generally not recommended to mix filament types in multi-extrusion (as materials with different shrinkage rates tend to separate), the glyph STL contains a &amp;quot;hidden&amp;quot; brim, which allows it to stay mechanically locked to the faceplate even if two different materials are used.&lt;br /&gt;
&lt;br /&gt;
==== Resin inlay ====&lt;br /&gt;
&lt;br /&gt;
The resin inlay approach is used in the build shown in the demonstration video. Here, the glyph is not printed, but made manually using resin. This step requires a water-soluble filament such as &#039;&#039;&#039;PVA&#039;&#039;&#039;, a dark-but-translucent 3D resin such as &#039;&#039;&#039;[https://www.esun3d.com/hard-tough-resin-product/ eSUN Hard-Tough black]&#039;&#039;&#039;, a &#039;&#039;&#039;syringe&#039;&#039;&#039; with a large-diameter (~0.5 mm inner diameter) needle, and a &#039;&#039;&#039;UV torch or laser pointer&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
The STLs to use are the &#039;&#039;&#039;faceplate&#039;&#039;&#039; and the &#039;&#039;&#039;raft&#039;&#039;&#039;. Place the raft on the build plate, and the faceplate on top of it, upside-down, &#039;&#039;without&#039;&#039; a gap. In the G-code, insert a filament change (M600) after 0.4 mm. Also insert a pause (M601) or filament change after 1.6 mm.&lt;br /&gt;
&lt;br /&gt;
Start printing the raft using a water-soluble filament such as PVA. At the first M600, switch to the desired material for the faceplate. At the pause, use the syringe to deposit a &#039;&#039;thin&#039;&#039; layer of resin, ideally 0.1 mm or less, in the cutout where the glyph should be. Thoroughly cure the resin with the UV torch or laser pointer. &#039;&#039;&#039;N.B. make absolutely sure you obey basic resin safety guidelines, and in particular &#039;&#039;DO NOT breathe the fumes when curing&#039;&#039;.&#039;&#039;&#039; Repeat this, in thin layers, until the entire cutout is filled, including the brim. Then resume the print, and when it finishes, again build up the remaining 0.6 mm of the glyph cutout with resin.&lt;br /&gt;
&lt;br /&gt;
Finally, &#039;&#039;very carefully&#039;&#039; remove the print—raft and all—from the build plate, and place it in warm water until the raft is fully dissolved. N.B. being too aggressive when removing the print, or peeling off the raft before it has sufficiently dissolved, can cause the resin inlay to crack.&lt;br /&gt;
&lt;br /&gt;
== Electronics ==&lt;br /&gt;
&lt;br /&gt;
The electronics are designed with [http://www.geda-project.org/ gEDA]. The gschem and PCB files can be found at [https://proj.penguindevelopment.org/light-glyph-lamp/light-glyph-lamp_electronic-designs-0.2.tar.xz]. The PCB gerbers can be found at [https://proj.penguindevelopment.org/light-glyph-lamp/light-glyph-lamp_gerbers-0.2.zip]. The PCB is difficult to fabricate at home as it uses plated through-holes and vias with a small drill size; it is recommended to use a service like [https://jlcpcb.com/ JLCPCB] to manufacture the board (&amp;lt;€10 for 5 copies, including shipping). The gerber zip can be uploaded directly to JLCPCB. The components are all inexpensively available from AliExpress.&lt;br /&gt;
&lt;br /&gt;
[[File:Light-glyph-lamp_pcb-front.jpg|thumb|128px|Front side of blank PCB]] [[File:Light-glyph-lamp_pcb-back.jpg|thumb|128px|Back side of blank PCB]]&lt;br /&gt;
&lt;br /&gt;
=== Requirements ===&lt;br /&gt;
&lt;br /&gt;
* Fine-tipped soldering iron (regulated station recommended)&lt;br /&gt;
* Needle-nose tweezers&lt;br /&gt;
* A steady hand—many components use a 0402 package&lt;br /&gt;
* AVR ISP programmer, e.g. [https://www.aliexpress.com/item/32869664871.html AVRISP MKII]&lt;br /&gt;
* Light glyph PCB&lt;br /&gt;
* 2× [https://www.aliexpress.com/item/1005006115949873.html TO220 heatsink] and [https://www.aliexpress.com/item/1005001602601725.html thermal pads] (optional but recommended)—fasten with M3x6 screws&lt;br /&gt;
&lt;br /&gt;
Full BOM:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;margin:auto&amp;quot;&lt;br /&gt;
|+ Bill of materials&lt;br /&gt;
|-&lt;br /&gt;
! Component ID !! Package/footprint !! Value/part name !! AliExpress link&lt;br /&gt;
|-&lt;br /&gt;
| C1 || 0402 || 100 nF || [https://www.aliexpress.com/item/32965092877.html]&lt;br /&gt;
|-&lt;br /&gt;
| C2 || 0402 || 100 nF || [https://www.aliexpress.com/item/32965092877.html]&lt;br /&gt;
|-&lt;br /&gt;
| C3 || Pol SMD, 12.5 mm || 1000 µF || [https://www.aliexpress.com/item/1005002285145345.html]&lt;br /&gt;
|-&lt;br /&gt;
| C4 || 0603 || 1 µF || [https://www.aliexpress.com/item/32966526545.html]&lt;br /&gt;
|-&lt;br /&gt;
| C5 || 0402 || 100 nF || [https://www.aliexpress.com/item/32965092877.html]&lt;br /&gt;
|-&lt;br /&gt;
| C6 || Pol SMD, 6.3 mm || 100 µF || [https://www.aliexpress.com/item/1005002285145345.html]&lt;br /&gt;
|-&lt;br /&gt;
| C7 || 0603 || 1 µF || [https://www.aliexpress.com/item/32966526545.html]&lt;br /&gt;
|-&lt;br /&gt;
| C8 || 0402 || 100 nF || [https://www.aliexpress.com/item/32965092877.html]&lt;br /&gt;
|-&lt;br /&gt;
| C9 || Pol SMD, 6.3 mm || 100 µF || [https://www.aliexpress.com/item/1005002285145345.html]&lt;br /&gt;
|-&lt;br /&gt;
| C10 || 0402 || 10 nF || [https://www.aliexpress.com/item/32965092877.html]&lt;br /&gt;
|-&lt;br /&gt;
| C11 || 0402 || 10 nF || [https://www.aliexpress.com/item/32965092877.html]&lt;br /&gt;
|-&lt;br /&gt;
| CS || 0402 || 1.5 nF&amp;lt;ref group=&amp;quot;lower-alpha&amp;quot;&amp;gt;Value may vary; see &amp;quot;Sense capacitor&amp;quot; below&amp;lt;/ref&amp;gt; || [https://www.aliexpress.com/item/32965092877.html]&lt;br /&gt;
|-&lt;br /&gt;
| CONN1 || Terminal, 5.08 mm pitch || Degson DG129-5.08-02P || [https://www.aliexpress.com/item/1005005975744435.html]&lt;br /&gt;
|-&lt;br /&gt;
| CONN2 || None; plated through hole || - || -&lt;br /&gt;
|-&lt;br /&gt;
| CONN3 || Terminal, 5.08 mm pitch || Degson DG129-5.08-02P || [https://www.aliexpress.com/item/1005005975744435.html]&lt;br /&gt;
|-&lt;br /&gt;
| D1-D48 || Through-hole LED, 3 mm || Warm white || [https://www.aliexpress.com/item/1005005881762668.html]&lt;br /&gt;
|-&lt;br /&gt;
| J1 || Header, 2×3, 2.54 mm pitch || Male ISP header || [https://www.aliexpress.com/item/1005006142829300.html]&lt;br /&gt;
|-&lt;br /&gt;
| J2 || Header, 1×3, 2.54 mm pitch || Male header || [https://www.aliexpress.com/item/1005003817088431.html]&lt;br /&gt;
|-&lt;br /&gt;
| J3 || Header, 1×3, 2.54 mm pitch || Male header || [https://www.aliexpress.com/item/1005003817088431.html]&lt;br /&gt;
|-&lt;br /&gt;
| Q1 || TO220W || IRLZ44N || [https://www.aliexpress.com/item/1005002474344551.html]&lt;br /&gt;
|-&lt;br /&gt;
| Q2 || TO220W || IRLZ44N || [https://www.aliexpress.com/item/1005002474344551.html]&lt;br /&gt;
|-&lt;br /&gt;
| R1 || 0603 || 100 kΩ || [https://www.aliexpress.com/item/1005002384017954.html]&lt;br /&gt;
|-&lt;br /&gt;
| R2 || 0603 || 100 kΩ || [https://www.aliexpress.com/item/1005002384017954.html]&lt;br /&gt;
|-&lt;br /&gt;
| R3 || 0603 || 10 kΩ || [https://www.aliexpress.com/item/1005002384017954.html]&lt;br /&gt;
|-&lt;br /&gt;
| R4 || 3296W trimpot || 10 kΩ || [https://www.aliexpress.com/item/1005006247969166.html]&lt;br /&gt;
|-&lt;br /&gt;
| R5 || 3296W trimpot || 10 kΩ || [https://www.aliexpress.com/item/1005006247969166.html]&lt;br /&gt;
|-&lt;br /&gt;
| R6-R21 || 0603 || 180 Ω&amp;lt;ref group=&amp;quot;lower-alpha&amp;quot;&amp;gt;Value may vary strongly depending on the 3 mm LEDs used. 180 Ω is appropriate for most white or blue LEDs; red/yellow/green LEDs typically need a significantly higher resistance.&amp;lt;/ref&amp;gt; || [https://www.aliexpress.com/item/1005002384017954.html]&lt;br /&gt;
|-&lt;br /&gt;
| RS || 0603 || 10 kΩ || [https://www.aliexpress.com/item/1005002384017954.html]&lt;br /&gt;
|-&lt;br /&gt;
| U1 || SO14 || ATtiny44A || [https://www.aliexpress.com/item/4001135654697.html]&lt;br /&gt;
|-&lt;br /&gt;
| U2 || SOT23-6 || AT42QT1010 || [https://www.aliexpress.com/item/1005006079721484.html]&lt;br /&gt;
|-&lt;br /&gt;
| U3 || SOT223 || AMS1117-5.0 || [https://www.aliexpress.com/item/1005001424098090.html]&lt;br /&gt;
|-&lt;br /&gt;
| U4 || SOT223 || AMS1117-5.0 || [https://www.aliexpress.com/item/1005001424098090.html]&lt;br /&gt;
|}&lt;br /&gt;
&amp;lt;references group=&amp;quot;lower-alpha&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Electronics assembly ===&lt;br /&gt;
&lt;br /&gt;
Soldering the components is reasonably straight-forward. First solder the SMD resistors (R6-R21) on the front side. Then flip to the back side and solder the SMD components by increasing height, starting with the 0402 capacitors and ending with the 1000 µF capacitor. Attach the heatsinks to the TO220W MOSFETs and place them on the backside (the heatsink aligns with the markings on the board). Then solder the trim potentiometers and the connectors. CONN1 and CONN3. Note that these should both face towards the centre line of the board. Leave CONN2 open for the time being; we will make and connect a sense pad on the glyph later.&lt;br /&gt;
&lt;br /&gt;
Finally, flip to the front side and install the 3 mm LEDs, inserting them up to the &amp;quot;wings&amp;quot; on the pins. Note that &#039;&#039;&#039;the square hole is the &#039;&#039;cathode&#039;&#039;&#039;&#039;&#039;, i.e. the &#039;&#039;&#039;short&#039;&#039;&#039; pin of the LEDs. Trim the legs off the LEDs.&lt;br /&gt;
&lt;br /&gt;
=== Flashing the firmware ===&lt;br /&gt;
&lt;br /&gt;
The firmware and source code can be found at [https://proj.penguindevelopment.org/light-glyph-lamp/light-glyph-lamp_firmware-0.2.tar.xz]. To flash it, connect the ISP programmer to the ISP header J1, taking note of the correct orientation. &#039;&#039;&#039;DO NOT&#039;&#039;&#039; connect 12V power; the programmer should power the circuit. N.B. the &amp;quot;Orb BRT&amp;quot; text on the PCB should be ignored at this stage.&lt;br /&gt;
&lt;br /&gt;
Flashing is easily done using AVRDUDE, e.g. &amp;lt;syntaxhighlight&amp;gt;avrdude -p t44 -c avrispmkII -U flash:w:flash.hex&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Sense capacitor ===&lt;br /&gt;
&lt;br /&gt;
One point of trial and error exists in the sense capacitor, CS. Typically, a value between 1 and 10 nF should be used, with higher values increasing touch sensitivity as well as noise sensitivity. It is highly recommended to have several values within this range available and experiment until you find the best option. Choosing a value that is too small will make the lamp unresponsive to touch, or only sensitive to touch with a full hand. On the other hand, choosing a value that is too large may cause the lamp to oscillate between off and on states.&lt;br /&gt;
&lt;br /&gt;
== Assembly ==&lt;br /&gt;
&lt;br /&gt;
=== Requirements ===&lt;br /&gt;
&lt;br /&gt;
* 12V adapter with 5.5 mm × 2.5 mm barrel connector, e.g. [https://www.aliexpress.com/item/1005003282695860.html]&lt;br /&gt;
* 12V LED strip, ~30 cm, e.g. [https://www.aliexpress.com/item/32809840774.html]&lt;br /&gt;
* [https://www.aliexpress.com/item/1005002601060281.html 5.5 mm × 2.5 barrel connector, female]&lt;br /&gt;
* ~50 cm each of red and black hookup wire&lt;br /&gt;
* ~4 mm diameter and ~10 mm diameter heat shrink tubing (optional but recommended)&lt;br /&gt;
* 4× [https://www.aliexpress.com/item/1005006355377113.html M3x12 screws]&lt;br /&gt;
* [https://www.aliexpress.com/item/1005004654217011.html 70 mm copper tape]&lt;br /&gt;
* [https://www.aliexpress.com/item/4001160286339.html 0.3 mm enamelled copper wire]&lt;br /&gt;
* [https://www.aliexpress.com/item/1005005016478093.html Silver conductive glue]&lt;br /&gt;
* Small-tipped hobby knife, e.g. [https://www.xacto.com/knives-blades.html X-Acto]&lt;br /&gt;
* Multimeter&lt;br /&gt;
* Soldering iron&lt;br /&gt;
* 4× [https://www.aliexpress.com/item/1005004535859664.html M3 brass inserts, 5 mm outer diameter]&lt;br /&gt;
* Hot glue gun&lt;br /&gt;
* 2× [https://www.aliexpress.com/item/10000002338839.html 2.54 mm header jumper]&lt;br /&gt;
* Flat-head jeweller&#039;s screwdriver&lt;br /&gt;
&lt;br /&gt;
=== Making the sense pad ===&lt;br /&gt;
&lt;br /&gt;
On the back side of the face plate, cover everything inside the glyph circle with copper tape, then cut out and remove the glyph lines from the tape using the hobby knife. Hold the faceplate in front of a light and look at it from the front to verify no tape obscures any part of the glyph lines.&lt;br /&gt;
&lt;br /&gt;
Strip the enamel from ~8 cm of the copper wire, and cut the stripped wire into ~1 cm pieces. With the faceplate face-down and the &amp;quot;arrow&amp;quot; of the glyph pointing away from you, start at the copper tape &amp;quot;island&amp;quot; at the lower right, and place the lengths of copper wire across the glyph lines until all islands are connected. There should be a single current path from every island to the one on the lower right; make sure there are no loops. Fix the wires in place with the conductive glue.&lt;br /&gt;
&lt;br /&gt;
Now take around 10 cm of copper wire, and strip the first 2 or 3 centimetres. Bend the stripped end into a zig-zag or spiral, and place it on the lower right island, with the unstripped tail pointing toward the lower-right standoff. Again fix the stripped wire in place with the conductive glue. The glue will not be conductive until it has fully set; this takes several hours, and you should leave the faceplate in a safe location for the full duration. While you wait for the glue to set (it is recommended to leave it overnight), you can assemble the orb and power connector.&lt;br /&gt;
&lt;br /&gt;
=== Orb assembly ===&lt;br /&gt;
&lt;br /&gt;
Begin by preparing a ~30 cm length of LED strip. Completely remove the adhesive tape (not just the protective film that covers it), and attach ~35 cm leads using the red and black hookup wires (red on +, black on -). Secure the solder joints with heat shrink tubing (or insulation tape), using just enough to cover the joints. Feed the LED strip into the orb, using a thin, blunt pin or pointy tweezers to manipulate it on the inside, if necessary. Continue until only the leads stick out.&lt;br /&gt;
&lt;br /&gt;
Feed the other end of the leads into the short end of the tube, until they come out of the other end. Gently pull from the long side until you can insert the short side into the orb until you hit the square fins. At this point, you may opt to leave the the tube out of the box to make for easier testing later; in this case, strip around 6 mm of insulation from the ends of the leads. However, if you wish to proceed, insert the remainder of the leads down the channel at the back of the box, and use needle-nose tweezers or pliers to pull them further into the box. Again, gently pull, now inserting the long side of the tube into the channel until you hit the square fins on that side.&lt;br /&gt;
&lt;br /&gt;
If necessary, trim down the leads so about 10 cm is inside the box. Strip around 6 mm of insulation from the ends.&lt;br /&gt;
&lt;br /&gt;
=== Power connector assembly ===&lt;br /&gt;
&lt;br /&gt;
Take the female barrel connector and attach ~10 cm black and red leads. The red one (+) should go on the short pin and the black on the long pin (-), although it is a good idea to first connect the 12V adapter and verify the polarity. Optionally insulate the connection with some heat shrink tubing.&lt;br /&gt;
&lt;br /&gt;
At this point, you may wish to leave the connector out of the box to make for easier testing, and install it later. If so, strip around 6 mm of insulation from the ends of the leads. Otherwise, insert the leads from the outside of the box into the small truncated circular hole in the back. Push the barrel connector into the hole, noting the flat sides. From the inside, feed the leads through the nut included with the barrel connector. Fasten the nut onto the connector with your fingers and tighten it with needle-nose pliers. Finally, strip ~6 mm of insulation from the end of the leads.&lt;br /&gt;
&lt;br /&gt;
=== Faceplate and electronic assembly ===&lt;br /&gt;
&lt;br /&gt;
Once the conductive glue on the faceplate wires has set, verify electrical continuity across the copper islands with a multimeter, or (les ideally) with a low-power LED indicator. To do so, &#039;&#039;carefully&#039;&#039; strip a few millimetres of enamel off the wire &amp;quot;tail&amp;quot; pointing away from the lower-right copper island (lower-left if seen from the front). Probe the resistance/continuity between this stripped end and each of the copper islands. No connection should read more than a few tens of ohms, tops.&lt;br /&gt;
&lt;br /&gt;
Now slide the diffuser over the standoffs. The flat side of the diffuser should face towards the glyph. Insert the brass inserts into the standoff holes and push them in using a soldering iron until they are flush with the surface. Leave them to cool off for a minute or so.&lt;br /&gt;
&lt;br /&gt;
Take the assembled PCB and fix it onto the faceplate with the 4 M3x12 screws. Do not tighten the screws. Ensure that the 3 mm LEDs face towards the diffuser and are aligned with the glyph. The copper wire tail should be close to CONN2 on the PCB. With some needle-nose tweezers, gently feed this copper wire through the CONN2 hole from the LED side. Pull it through the hole until there&#039;s around 5 mm of slack against the side of the diffuser. Do not pull hard; the conductive glue is very weak and you may tear it otherwise. Now solder the copper wire onto the CONN2 hole on the component side. You will need to melt through the insulating enamel before you can make an electrical connection, so be patient. Verify the connection between CONN2 and the copper islands on the faceplate, then trim the excess copper wire. (This is important, as it will interfere with the capacitive touch sensor.)&lt;br /&gt;
&lt;br /&gt;
=== Testing the electronics ===&lt;br /&gt;
&lt;br /&gt;
Insert the leads for the power jack into the terminal labelled &amp;quot;PWR&amp;quot; and tighten the screws. Note that the ground (black wire) should be closest to the board edge. Then attach the orb leads to the terminal labelled &amp;quot;Orb&amp;quot;, noting that the connections are reversed: here, +12 V (red wire) should be closest to the board edge.&lt;br /&gt;
&lt;br /&gt;
Place one of the jumpers across the top pins of J2, labelled &amp;quot;Glyph BRT&amp;quot; (BRT stands for BRightness Test), and the other across the GND and MOSI pins of J1, labelled &amp;quot;Orb BRT&amp;quot;. Now connect the 12V adapter to the barrel jack and insert it in the wall socket. If everything is OK, the glyph should light up. You can now use the potentiometer R4, labelled &amp;quot;glyph&amp;quot;, to adjust the brightness. Once you are satisfied, remove the Glyph BRT jumper, &#039;&#039;carefully&#039;&#039;&#039; so as to not create a short-circuit (you may disconnect power first to eliminate the risk). The glyph should turn off and the orb should turn on. Adjust the brightness until you are satisfied, then remove the Orb BRT jumper.&lt;br /&gt;
&lt;br /&gt;
Now, all LEDs should be off, and the lamp will enter idle mode. At this point, touching the front of the glyph should cause it to light up briefly, after which the orb turns on, as in the demonstration video. If it does not, it usually means the value of CS is too low; see &amp;quot;Sense capacitor&amp;quot; in the previous section. However, if increasing CS does not make the lamp responsive to touch, you can try touching the wire stub at CONN2 directly (make sure your hands are dry). If this does turn the lamp on, it means the copper tape on the glyph is not properly connected to CONN2. If it still does not turn on when touching CONN2 while having a large (≥ 10 nF) CS, you have most likely damaged U2 (the touch sensor, not the band) or another component when soldering.&lt;br /&gt;
&lt;br /&gt;
=== Finishing up ===&lt;br /&gt;
&lt;br /&gt;
Once the electronics work as desired, disconnect power and use some hot glue around the corners of the PCB to secure it gently to the faceplate and diffuser. Use only a small drop; you do not want to cover any electronic components or get glue in the screw threads. It should be easy to peel off at a later stage, if desired.&lt;br /&gt;
&lt;br /&gt;
Now remove the M3x12 screws holding the board in place. At this stage, make sure the BRT jumpers are removed! Then turn the assembly over and place it in the box. Insert the M3x12 screws from the bottom side of the box and gently tighten them. Take care not to over-tighten, particularly if you used woodFill, as this is a weak filament and can easily be damaged by twisting.&lt;br /&gt;
&lt;br /&gt;
Finally, reconnect power. You should see the glyph and orb flash briefly, in sequence, and then turn off. Ensure the lamp properly responds to touch. Congratulations, you&#039;re done, hoot hoot!&lt;br /&gt;
&lt;br /&gt;
== Troubleshooting and notes ==&lt;br /&gt;
&lt;br /&gt;
Occasionally, the lamp may start oscillating between states. In the current version, this can sometimes be stopped by pushing down on the glyph with a full hand or by grabbing the box from the sides, but the only sure-fire way to stop it is to temporarily disconnect power. This will hopefully be eliminated in a future revision by letting the microcontroller reset the touch sensor. In the mean time, use the smallest possible value for CS that you can get away with to minimise the failure rate.&lt;br /&gt;
&lt;br /&gt;
For advanced users, the current revision of the board allows you to use an external trigger signal, e.g. from a [https://www.aliexpress.com/item/1005005762960287.html TTP223 board]. To do so, remove the AT42QT1010 (U2) and attach the external board to the TRIG pin CONN4. In the default configuration, the external trigger should be an active-high push-pull output. To use an active-low signal, define &amp;lt;code&amp;gt;TRIG_ACTIVE_LOW&amp;lt;/code&amp;gt; in the firmware. To use an open-drain trigger signal, remove R3 and define &amp;lt;code&amp;gt;TRIG_OPEN_DRAIN&amp;lt;/code&amp;gt; in the firmware.&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=Main_Page&amp;diff=262</id>
		<title>Main Page</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=Main_Page&amp;diff=262"/>
		<updated>2024-01-04T18:18:34Z</updated>

		<summary type="html">&lt;p&gt;Link: /* Hardware */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Welcome to &#039;&#039;&#039;Penguin Development&#039;&#039;&#039;! We make [[w:free and open-source software|free and open-source software]] and [[w:Open-source hardware|hardware]].&lt;br /&gt;
&lt;br /&gt;
See below for current releases. All software and hardware is licensed under the [https://gnu.org/licenses/gpl.html GPL] (version 3 or higher) unless otherwise stated.&lt;br /&gt;
&lt;br /&gt;
=Software=&lt;br /&gt;
* [[AUTOMOME]] — the OTTish AUTOMEME clone&lt;br /&gt;
* [[Ising]] — Monte Carlo Ising model simulator&lt;br /&gt;
* [[stackermann]] — a stack-based [[w:Ackermann function|Ackermann function]] calculator&lt;br /&gt;
* [[catan-gen]] — convergent map creation app for [https://www.catan.com/ Catan]&lt;br /&gt;
&lt;br /&gt;
=Hardware=&lt;br /&gt;
* [[Light glyph lamp]] — Capacitive touch lamp based on the light glyph from &#039;&#039;The Owl House&#039;&#039;&lt;br /&gt;
* [[timelapse]] — time-lapse trigger for digital cameras&lt;br /&gt;
&lt;br /&gt;
=Code snippets and how-tos=&lt;br /&gt;
* [[Animated 3D plotting with Blender]]&lt;br /&gt;
* [[Matrix Synapse and mautrix-whatsapp in a VPN]]&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=Light_glyph_lamp&amp;diff=261</id>
		<title>Light glyph lamp</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=Light_glyph_lamp&amp;diff=261"/>
		<updated>2024-01-04T18:17:52Z</updated>

		<summary type="html">&lt;p&gt;Link: Created page with &amp;quot;The &amp;#039;&amp;#039;&amp;#039;light glyph lamp&amp;#039;&amp;#039;&amp;#039; is an open-source touch-activated LED lamp based on the light glyph seen in &amp;#039;&amp;#039;The Owl House&amp;#039;&amp;#039;.  All source files can be found at [https://proj.penguindevelopment.org/light-glyph-lamp/].  Demonstration video   Printed circuit board, assembled  == 3D printable parts ==  The exterior parts of the lamp can be 3D printed. FDM is highl...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;The &#039;&#039;&#039;light glyph lamp&#039;&#039;&#039; is an open-source touch-activated LED lamp based on the light glyph seen in &#039;&#039;[[w:The Owl House|The Owl House]]&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
All source files can be found at [https://proj.penguindevelopment.org/light-glyph-lamp/].&lt;br /&gt;
&lt;br /&gt;
[[File:Light-glyph-lamp_demo.gif|frame|Demonstration video]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[File:Light-glyph-lamp_pcb-assembled.jpg|thumb|128px|Printed circuit board, assembled]]&lt;br /&gt;
&lt;br /&gt;
== 3D printable parts ==&lt;br /&gt;
&lt;br /&gt;
The exterior parts of the lamp can be 3D printed. FDM is highly recommended for the structural parts, although the orb and diffuser may use resin instead. The STL files and OpenSCAD source code can be found at [https://proj.penguindevelopment.org/light-glyph-lamp/light-glyph-lamp_printable-parts-0.2.tar.xz].&lt;br /&gt;
&lt;br /&gt;
=== Requirements ===&lt;br /&gt;
&lt;br /&gt;
* FDM 3D printer&lt;br /&gt;
* ~150 g of filament&lt;br /&gt;
&lt;br /&gt;
Diffuser and orb:&lt;br /&gt;
* ~50 g of translucent white filament&lt;br /&gt;
OR&lt;br /&gt;
* Resin printer&lt;br /&gt;
* ~30 ml of translucent white resin&lt;br /&gt;
&lt;br /&gt;
Glyph:&lt;br /&gt;
* ~10 g of translucent dark filament&lt;br /&gt;
* Multi-extrusion capability (optional but recommended)&lt;br /&gt;
OR&lt;br /&gt;
* Translucent dark resin&lt;br /&gt;
* Syringe with large-bore (~0.5 mm) needle&lt;br /&gt;
* UV torch or laser pointer&lt;br /&gt;
* ~10 g of PVA filament&lt;br /&gt;
&lt;br /&gt;
=== Main parts ===&lt;br /&gt;
&lt;br /&gt;
The box and tube may be printed with any filament. In the demonstration video&#039;s build, [https://colorfabb.com/woodfill colorFabb woodFill] is used for the box, and [https://www.esun3d.com/epa-cf-product/ eSUN ePA-CF] is used for the tube. The tube can be printed lying on its side, but &#039;&#039;do not&#039;&#039; use supports.&lt;br /&gt;
&lt;br /&gt;
The diffuser and orb should be printed in a translucent white filament or resin. In the demonstration video&#039;s build, [https://www.esun3d.com/eresin-pla-product/ clear eSUN eResin-PLA], dyed with SigWong white alcohol ink is used. Depending on the desired luminosity, the orb may get somewhat hot; in this case, PLA filament may be less suitable.&lt;br /&gt;
&lt;br /&gt;
=== Faceplate and glyph ===&lt;br /&gt;
&lt;br /&gt;
Printing the faceplate is somewhat more complex, as it requires two colours. Furthermore, the glyph should have a greater translucency than the other faceplate material, but a darker colour when not backlit. There are several ways to achieve this, two of which are highlighted here.&lt;br /&gt;
&lt;br /&gt;
==== Pure FDM with multi-extrusion or filament swapping ====&lt;br /&gt;
&lt;br /&gt;
In this approach, the STLs for the &#039;&#039;&#039;faceplate&#039;&#039;&#039; and the &#039;&#039;&#039;glyph&#039;&#039;&#039; are used. If using Cura as a slicer, set up a multi-extrusion printer, which [https://scholtzan.net/blog/cura-multicolor-single-extruder/ may be emulated using filament swaps]. Load the STLs, and assign each to its own extruder, then combine the models. In PrusaSlicer, first import the faceplate, then right-click and use &amp;quot;add part&amp;quot; to import the glyph. As in Cura, [https://forum.prusa3d.com/forum/postid/188236/ multi-extrusion may be emulated with filament swaps].&lt;br /&gt;
&lt;br /&gt;
Few filaments exist which are dark but translucent. One possibility is the [https://www.matterhackers.com/store/l/translucent-grey-mh-build-series-petg-filament-175mm-1kg/sk/MX9SD5D4 translucent grey PETG by Matterhackers]. Although it is generally not recommended to mix filament types in multi-extrusion (as materials with different shrinkage rates tend to separate), the glyph STL contains a &amp;quot;hidden&amp;quot; brim, which allows it to stay mechanically locked to the faceplate even if two different materials are used.&lt;br /&gt;
&lt;br /&gt;
==== Resin inlay ====&lt;br /&gt;
&lt;br /&gt;
The resin inlay approach is used in the build shown in the demonstration video. Here, the glyph is not printed, but made manually using resin. This step requires a water-soluble filament such as &#039;&#039;&#039;PVA&#039;&#039;&#039;, a dark-but-translucent 3D resin such as &#039;&#039;&#039;[https://www.esun3d.com/hard-tough-resin-product/ eSUN Hard-Tough black]&#039;&#039;&#039;, a &#039;&#039;&#039;syringe&#039;&#039;&#039; with a large-diameter (~0.5 mm inner diameter) needle, and a &#039;&#039;&#039;UV torch or laser pointer&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
The STLs to use are the &#039;&#039;&#039;faceplate&#039;&#039;&#039; and the &#039;&#039;&#039;raft&#039;&#039;&#039;. Place the raft on the build plate, and the faceplate on top of it, upside-down, &#039;&#039;without&#039;&#039; a gap. In the G-code, insert a filament change (M600) after 0.4 mm. Also insert a pause (M601) or filament change after 1.6 mm.&lt;br /&gt;
&lt;br /&gt;
Start printing the raft using a water-soluble filament such as PVA. At the first M600, switch to the desired material for the faceplate. At the pause, use the syringe to deposit a &#039;&#039;thin&#039;&#039; layer of resin, ideally 0.1 mm or less, in the cutout where the glyph should be. Thoroughly cure the resin with the UV torch or laser pointer. &#039;&#039;&#039;N.B. make absolutely sure you obey basic resin safety guidelines, and in particular &#039;&#039;DO NOT breathe the fumes when curing&#039;&#039;.&#039;&#039;&#039; Repeat this, in thin layers, until the entire cutout is filled, including the brim. Then resume the print, and when it finishes, again build up the remaining 0.6 mm of the glyph cutout with resin.&lt;br /&gt;
&lt;br /&gt;
Finally, &#039;&#039;very carefully&#039;&#039; remove the print—raft and all—from the build plate, and place it in warm water until the raft is fully dissolved. N.B. being too aggressive when removing the print, or peeling off the raft before it has sufficiently dissolved, can cause the resin inlay to crack.&lt;br /&gt;
&lt;br /&gt;
== Electronics ==&lt;br /&gt;
&lt;br /&gt;
The electronics are designed with [http://www.geda-project.org/ gEDA]. The gschem and PCB files can be found at [https://proj.penguindevelopment.org/light-glyph-lamp/light-glyph-lamp_electronic-designs-0.2.tar.xz]. The PCB gerbers can be found at [https://proj.penguindevelopment.org/light-glyph-lamp/light-glyph-lamp_gerbers-0.2.zip]. The PCB is difficult to fabricate at home as it uses plated through-holes and vias with a small drill size; it is recommended to use a service like [https://jlcpcb.com/ JLCPCB] to manufacture the board (&amp;lt;€10 for 5 copies, including shipping). The gerber zip can be uploaded directly to JLCPCB. The components are all inexpensively available from AliExpress.&lt;br /&gt;
&lt;br /&gt;
[[File:Light-glyph-lamp_pcb-front.jpg|thumb|128px|Front side of blank PCB]] [[File:Light-glyph-lamp_pcb-back.jpg|thumb|128px|Back side of blank PCB]]&lt;br /&gt;
&lt;br /&gt;
=== Requirements ===&lt;br /&gt;
&lt;br /&gt;
* Fine-tipped soldering iron (regulated station recommended)&lt;br /&gt;
* Needle-nose tweezers&lt;br /&gt;
* A steady hand—many components use a 0402 package&lt;br /&gt;
* AVR ISP programmer, e.g. [https://www.aliexpress.com/item/32869664871.html AVRISP MKII]&lt;br /&gt;
* Light glyph PCB&lt;br /&gt;
* 2× [https://www.aliexpress.com/item/1005006115949873.html TO220 heatsink] and [https://www.aliexpress.com/item/1005001602601725.html thermal pads] (optional but recommended)—fasten with M3x6 screws&lt;br /&gt;
&lt;br /&gt;
Full BOM:&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;margin:auto&amp;quot;&lt;br /&gt;
|+ Bill of materials&lt;br /&gt;
|-&lt;br /&gt;
! Component ID !! Package/footprint !! Value/part name !! AliExpress link&lt;br /&gt;
|-&lt;br /&gt;
| C1 || 0402 || 100 nF || [https://www.aliexpress.com/item/32965092877.html]&lt;br /&gt;
|-&lt;br /&gt;
| C2 || 0402 || 100 nF || [https://www.aliexpress.com/item/32965092877.html]&lt;br /&gt;
|-&lt;br /&gt;
| C3 || Pol SMD, 12.5 mm || 1000 µF || [https://www.aliexpress.com/item/1005002285145345.html]&lt;br /&gt;
|-&lt;br /&gt;
| C4 || 0603 || 1 µF || [https://www.aliexpress.com/item/32966526545.html]&lt;br /&gt;
|-&lt;br /&gt;
| C5 || 0402 || 100 nF || [https://www.aliexpress.com/item/32965092877.html]&lt;br /&gt;
|-&lt;br /&gt;
| C6 || Pol SMD, 6.3 mm || 100 µF || [https://www.aliexpress.com/item/1005002285145345.html]&lt;br /&gt;
|-&lt;br /&gt;
| C7 || 0603 || 1 µF || [https://www.aliexpress.com/item/32966526545.html]&lt;br /&gt;
|-&lt;br /&gt;
| C8 || 0402 || 100 nF || [https://www.aliexpress.com/item/32965092877.html]&lt;br /&gt;
|-&lt;br /&gt;
| C9 || Pol SMD, 6.3 mm || 100 µF || [https://www.aliexpress.com/item/1005002285145345.html]&lt;br /&gt;
|-&lt;br /&gt;
| C10 || 0402 || 10 nF || [https://www.aliexpress.com/item/32965092877.html]&lt;br /&gt;
|-&lt;br /&gt;
| C11 || 0402 || 10 nF || [https://www.aliexpress.com/item/32965092877.html]&lt;br /&gt;
|-&lt;br /&gt;
| CS || 0402 || 1.5 nF&amp;lt;ref group=&amp;quot;lower-alpha&amp;quot;&amp;gt;Value may vary; see &amp;quot;Sense capacitor&amp;quot; below&amp;lt;/ref&amp;gt; || [https://www.aliexpress.com/item/32965092877.html]&lt;br /&gt;
|-&lt;br /&gt;
| CONN1 || Terminal, 5.08 mm pitch || Degson DG129-5.08-02P || [https://www.aliexpress.com/item/1005005975744435.html]&lt;br /&gt;
|-&lt;br /&gt;
| CONN2 || None; plated through hole || - || -&lt;br /&gt;
|-&lt;br /&gt;
| CONN3 || Terminal, 5.08 mm pitch || Degson DG129-5.08-02P || [https://www.aliexpress.com/item/1005005975744435.html]&lt;br /&gt;
|-&lt;br /&gt;
| D1-D48 || Through-hole LED, 3 mm || Warm white || [https://www.aliexpress.com/item/1005005881762668.html]&lt;br /&gt;
|-&lt;br /&gt;
| J1 || Header, 2×3, 2.54 mm pitch || Male ISP header || [https://www.aliexpress.com/item/1005006142829300.html]&lt;br /&gt;
|-&lt;br /&gt;
| J2 || Header, 1×3, 2.54 mm pitch || Male header || [https://www.aliexpress.com/item/1005003817088431.html]&lt;br /&gt;
|-&lt;br /&gt;
| J3 || Header, 1×3, 2.54 mm pitch || Male header || [https://www.aliexpress.com/item/1005003817088431.html]&lt;br /&gt;
|-&lt;br /&gt;
| Q1 || TO220W || IRLZ44N || [https://www.aliexpress.com/item/1005002474344551.html]&lt;br /&gt;
|-&lt;br /&gt;
| Q2 || TO220W || IRLZ44N || [https://www.aliexpress.com/item/1005002474344551.html]&lt;br /&gt;
|-&lt;br /&gt;
| R1 || 0603 || 100 kΩ || [https://www.aliexpress.com/item/1005002384017954.html]&lt;br /&gt;
|-&lt;br /&gt;
| R2 || 0603 || 100 kΩ || [https://www.aliexpress.com/item/1005002384017954.html]&lt;br /&gt;
|-&lt;br /&gt;
| R3 || 0603 || 10 kΩ || [https://www.aliexpress.com/item/1005002384017954.html]&lt;br /&gt;
|-&lt;br /&gt;
| R4 || 3296W trimpot || 10 kΩ || [https://www.aliexpress.com/item/1005006247969166.html]&lt;br /&gt;
|-&lt;br /&gt;
| R5 || 3296W trimpot || 10 kΩ || [https://www.aliexpress.com/item/1005006247969166.html]&lt;br /&gt;
|-&lt;br /&gt;
| R6-R21 || 0603 || 180 Ω&amp;lt;ref group=&amp;quot;lower-alpha&amp;quot;&amp;gt;Value may vary strongly depending on the 3 mm LEDs used. 180 Ω is appropriate for most white or blue LEDs; red/yellow/green LEDs typically need a significantly higher resistance.&amp;lt;/ref&amp;gt; || [https://www.aliexpress.com/item/1005002384017954.html]&lt;br /&gt;
|-&lt;br /&gt;
| RS || 0603 || 10 kΩ || [https://www.aliexpress.com/item/1005002384017954.html]&lt;br /&gt;
|-&lt;br /&gt;
| U1 || SO14 || ATtiny44A || [https://www.aliexpress.com/item/4001135654697.html]&lt;br /&gt;
|-&lt;br /&gt;
| U2 || SOT23-6 || AT42QT1010 || [https://www.aliexpress.com/item/1005006079721484.html]&lt;br /&gt;
|-&lt;br /&gt;
| U3 || SOT223 || AMS1117-5.0 || [https://www.aliexpress.com/item/1005001424098090.html]&lt;br /&gt;
|-&lt;br /&gt;
| U4 || SOT223 || AMS1117-5.0 || [https://www.aliexpress.com/item/1005001424098090.html]&lt;br /&gt;
|}&lt;br /&gt;
&amp;lt;references group=&amp;quot;lower-alpha&amp;quot; /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Electronics assembly ===&lt;br /&gt;
&lt;br /&gt;
Soldering the components is reasonably straight-forward. First solder the SMD resistors (R6-R21) on the front side. Then flip to the back side and solder the SMD components by increasing height, starting with the 0402 capacitors and ending with the 1000 µF capacitor. Attach the heatsinks to the TO220W MOSFETs and place them on the backside (the heatsink aligns with the markings on the board). Then solder the trim potentiometers and the connectors. CONN1 and CONN3. Note that these should both face towards the centre line of the board. Leave CONN2 open for the time being; we will make and connect a sense pad on the glyph later.&lt;br /&gt;
&lt;br /&gt;
Finally, flip to the front side and install the 3 mm LEDs, inserting them up to the &amp;quot;wings&amp;quot; on the pins. Note that &#039;&#039;&#039;the square hole is the &#039;&#039;cathode&#039;&#039;&#039;&#039;&#039;, i.e. the &#039;&#039;&#039;short&#039;&#039;&#039; pin of the LEDs. Trim the legs off the LEDs.&lt;br /&gt;
&lt;br /&gt;
=== Flashing the firmware ===&lt;br /&gt;
&lt;br /&gt;
The firmware and source code can be found at [https://proj.penguindevelopment.org/light-glyph-lamp/light-glyph-lamp_firmware-0.2.tar.xz]. To flash it, connect the ISP programmer to the ISP header J1, taking note of the correct orientation. &#039;&#039;&#039;DO NOT&#039;&#039;&#039; connect 12V power; the programmer should power the circuit. N.B. the &amp;quot;Orb BRT&amp;quot; text on the PCB should be ignored at this stage.&lt;br /&gt;
&lt;br /&gt;
Flashing is easily done using AVRDUDE, e.g. &amp;lt;syntaxhighlight&amp;gt;avrdude -p t44 -c avrispmkII -U flash:w:flash.hex&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Sense capacitor ===&lt;br /&gt;
&lt;br /&gt;
One point of trial and error exists in the sense capacitor, CS. Typically, a value between 1 and 10 nF should be used, with higher values increasing touch sensitivity as well as noise sensitivity. It is highly recommended to have several values within this range available and experiment until you find the best option. Choosing a value that is too small will make the lamp unresponsive to touch, or only sensitive to touch with a full hand. On the other hand, choosing a value that is too large may cause the lamp to oscillate between off and on states.&lt;br /&gt;
&lt;br /&gt;
== Assembly ==&lt;br /&gt;
&lt;br /&gt;
=== Requirements ===&lt;br /&gt;
&lt;br /&gt;
* 12V adapter with 5.5 mm × 2.5 mm barrel connector, e.g. [https://www.aliexpress.com/item/1005003282695860.html]&lt;br /&gt;
* 12V LED strip, ~30 cm, e.g. [https://www.aliexpress.com/item/32809840774.html]&lt;br /&gt;
* [https://www.aliexpress.com/item/1005002601060281.html 5.5 mm × 2.5 barrel connector, female]&lt;br /&gt;
* ~50 cm each of red and black hookup wire&lt;br /&gt;
* ~4 mm diameter and ~10 mm diameter heat shrink tubing (optional but recommended)&lt;br /&gt;
* 4× [https://www.aliexpress.com/item/1005006355377113.html M3x12 screws]&lt;br /&gt;
* [https://www.aliexpress.com/item/1005004654217011.html 70 mm copper tape]&lt;br /&gt;
* [https://www.aliexpress.com/item/4001160286339.html 0.3 mm enamelled copper wire]&lt;br /&gt;
* [https://www.aliexpress.com/item/1005005016478093.html Silver conductive glue]&lt;br /&gt;
* Small-tipped hobby knife, e.g. [https://www.xacto.com/knives-blades.html X-Acto]&lt;br /&gt;
* Multimeter&lt;br /&gt;
* Soldering iron&lt;br /&gt;
* 4× [https://www.aliexpress.com/item/1005004535859664.html M3 brass inserts, 5 mm outer diameter]&lt;br /&gt;
* Hot glue gun&lt;br /&gt;
* 2× [https://www.aliexpress.com/item/10000002338839.html 2.54 mm header jumper]&lt;br /&gt;
* Flat-head jeweller&#039;s screwdriver&lt;br /&gt;
&lt;br /&gt;
=== Making the sense pad ===&lt;br /&gt;
&lt;br /&gt;
On the back side of the face plate, cover everything inside the glyph circle with copper tape, then cut out and remove the glyph lines from the tape using the hobby knife. Hold the faceplate in front of a light and look at it from the front to verify no tape obscures any part of the glyph lines.&lt;br /&gt;
&lt;br /&gt;
Strip the enamel from ~8 cm of the copper wire, and cut the stripped wire into ~1 cm pieces. With the faceplate face-down and the &amp;quot;arrow&amp;quot; of the glyph pointing away from you, start at the copper tape &amp;quot;island&amp;quot; at the lower right, and place the lengths of copper wire across the glyph lines until all islands are connected. There should be a single current path from every island to the one on the lower right; make sure there are no loops. Fix the wires in place with the conductive glue.&lt;br /&gt;
&lt;br /&gt;
Now take around 10 cm of copper wire, and strip the first 2 or 3 centimetres. Bend the stripped end into a zig-zag or spiral, and place it on the lower right island, with the unstripped tail pointing toward the lower-right standoff. Again fix the stripped wire in place with the conductive glue. The glue will not be conductive until it has fully set; this takes several hours, and you should leave the faceplate in a safe location for the full duration. While you wait for the glue to set (it is recommended to leave it overnight), you can assemble the orb and power connector.&lt;br /&gt;
&lt;br /&gt;
=== Orb assembly ===&lt;br /&gt;
&lt;br /&gt;
Begin by preparing a ~30 cm length of LED strip. Completely remove the adhesive tape (not just the protective film that covers it), and attach ~35 cm leads using the red and black hookup wires (red on +, black on -). Secure the solder joints with heat shrink tubing (or insulation tape), using just enough to cover the joints. Feed the LED strip into the orb, using a thin, blunt pin or pointy tweezers to manipulate it on the inside, if necessary. Continue until only the leads stick out.&lt;br /&gt;
&lt;br /&gt;
Feed the other end of the leads into the short end of the tube, until they come out of the other end. Gently pull from the long side until you can insert the short side into the orb until you hit the square fins. At this point, you may opt to leave the the tube out of the box to make for easier testing later; in this case, strip around 6 mm of insulation from the ends of the leads. However, if you wish to proceed, insert the remainder of the leads down the channel at the back of the box, and use needle-nose tweezers or pliers to pull them further into the box. Again, gently pull, now inserting the long side of the tube into the channel until you hit the square fins on that side.&lt;br /&gt;
&lt;br /&gt;
If necessary, trim down the leads so about 10 cm is inside the box. Strip around 6 mm of insulation from the ends.&lt;br /&gt;
&lt;br /&gt;
=== Power connector assembly ===&lt;br /&gt;
&lt;br /&gt;
Take the female barrel connector and attach ~10 cm black and red leads. The red one (+) should go on the short pin and the black on the long pin (-), although it is a good idea to first connect the 12V adapter and verify the polarity. Optionally insulate the connection with some heat shrink tubing.&lt;br /&gt;
&lt;br /&gt;
At this point, you may wish to leave the connector out of the box to make for easier testing, and install it later. If so, strip around 6 mm of insulation from the ends of the leads. Otherwise, insert the leads from the outside of the box into the small truncated circular hole in the back. Push the barrel connector into the hole, noting the flat sides. From the inside, feed the leads through the nut included with the barrel connector. Fasten the nut onto the connector with your fingers and tighten it with needle-nose pliers. Finally, strip ~6 mm of insulation from the end of the leads.&lt;br /&gt;
&lt;br /&gt;
=== Faceplate and electronic assembly ===&lt;br /&gt;
&lt;br /&gt;
Once the conductive glue on the faceplate wires has set, verify electrical continuity across the copper islands with a multimeter, or (les ideally) with a low-power LED indicator. To do so, &#039;&#039;carefully&#039;&#039; strip a few millimetres of enamel off the wire &amp;quot;tail&amp;quot; pointing away from the lower-right copper island (lower-left if seen from the front). Probe the resistance/continuity between this stripped end and each of the copper islands. No connection should read more than a few tens of ohms, tops.&lt;br /&gt;
&lt;br /&gt;
Now slide the diffuser over the standoffs. The flat side of the diffuser should face towards the glyph. Insert the brass inserts into the standoff holes and push them in using a soldering iron until they are flush with the surface. Leave them to cool off for a minute or so.&lt;br /&gt;
&lt;br /&gt;
Take the assembled PCB and fix it onto the faceplate with the 4 M3x12 screws. Do not tighten the screws. Ensure that the 3 mm LEDs face towards the diffuser and are aligned with the glyph. The copper wire tail should be close to CONN2 on the PCB. With some needle-nose tweezers, gently feed this copper wire through the CONN2 hole from the LED side. Pull it through the hole until there&#039;s around 5 mm of slack against the side of the diffuser. Do not pull hard; the conductive glue is very weak and you may tear it otherwise. Now solder the copper wire onto the CONN2 hole on the component side. You will need to melt through the insulating enamel before you can make an electrical connection, so be patient. Verify the connection between CONN2 and the copper islands on the faceplate, then trim the excess copper wire. (This is important, as it will interfere with the capacitive touch sensor.)&lt;br /&gt;
&lt;br /&gt;
=== Testing the electronics ===&lt;br /&gt;
&lt;br /&gt;
Insert the leads for the power jack into the terminal labelled &amp;quot;PWR&amp;quot; and tighten the screws. Note that the ground (black wire) should be closest to the board edge. Then attach the orb leads to the terminal labelled &amp;quot;Orb&amp;quot;, noting that the connections are reversed: here, +12 V (red wire) should be closest to the board edge.&lt;br /&gt;
&lt;br /&gt;
Place one of the jumpers across the top pins of J2, labelled &amp;quot;Glyph BRT&amp;quot; (BRT stands for BRightness Test), and the other across the GND and MOSI pins of J1, labelled &amp;quot;Orb BRT&amp;quot;. Now connect the 12V adapter to the barrel jack and insert it in the wall socket. If everything is OK, the glyph should light up. You can now use the potentiometer R4, labelled &amp;quot;glyph&amp;quot;, to adjust the brightness. Once you are satisfied, remove the Glyph BRT jumper, &#039;&#039;carefully&#039;&#039;&#039; so as to not create a short-circuit (you may disconnect power first to eliminate the risk). The glyph should turn off and the orb should turn on. Adjust the brightness until you are satisfied, then remove the Orb BRT jumper.&lt;br /&gt;
&lt;br /&gt;
Now, all LEDs should be off, and the lamp will enter idle mode. At this point, touching the front of the glyph should cause it to light up briefly, after which the orb turns on, as in the demonstration video. If it does not, it usually means the value of CS is too low; see &amp;quot;Sense capacitor&amp;quot; in the previous section. However, increasing CS does not make the lamp responsive to touch, you can try touching the wire stub at CONN2 directly (make sure your hands are dry). If this does turn the lamp on, it means the copper tape on the glyph is not properly connected to CONN2. If it still does not turn on when touching CONN2 while having a large (≥ 10 nF) CS, you have most likely damaged U2 (the touch sensor, not the band) or another component when soldering.&lt;br /&gt;
&lt;br /&gt;
=== Finishing up ===&lt;br /&gt;
&lt;br /&gt;
Once the electronics work as desired, disconnect power and use some hot glue around the corners of the PCB to secure it gently to the faceplate and diffuser. Use only a small drop; you do not want to cover any electronic components or get glue in the screw threads. It should be easy to peel off at a later stage, if desired.&lt;br /&gt;
&lt;br /&gt;
Now remove the M3x12 screws holding the board in place. At this stage, make sure the BRT jumpers are removed! Then turn the assembly over and place it in the box. Insert the M3x12 screws from the bottom side of the box and gently tighten them. Take care not to over-tighten, particularly if you used woodFill, as this is a weak filament and can easily be damaged by twisting.&lt;br /&gt;
&lt;br /&gt;
Finally, reconnect power. You should see the glyph and orb flash briefly, in sequence, and then turn off. Ensure the lamp properly responds to touch. Congratulations, you&#039;re done, hoot hoot!&lt;br /&gt;
&lt;br /&gt;
== Troubleshooting and notes ==&lt;br /&gt;
&lt;br /&gt;
Occasionally, the lamp may start oscillating between states. In the current version, this can sometimes be stopped by pushing down on the glyph with a full hand or by grabbing the box from the sides, but the only sure-fire way to stop it is to temporarily disconnect power. This will hopefully be eliminated in a future revision by letting the microcontroller reset the touch sensor. In the mean time, use the smallest possible value for CS that you can get away with to minimise the failure rate.&lt;br /&gt;
&lt;br /&gt;
For advanced users, the current revision of the board allows you to use an external trigger signal, e.g. from a [https://www.aliexpress.com/item/1005005762960287.html TTP223 board]. To do so, remove the AT42QT1010 (U2) and attach the external board to the TRIG pin CONN4. In the default configuration, the external trigger should be an active-high push-pull output. To use an active-low signal, define &amp;lt;code&amp;gt;TRIG_ACTIVE_LOW&amp;lt;/code&amp;gt; in the firmware. To use an open-drain trigger signal, remove R3 and define &amp;lt;code&amp;gt;TRIG_OPEN_DRAIN&amp;lt;/code&amp;gt; in the firmware.&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=File:Light-glyph-lamp_demo.gif&amp;diff=260</id>
		<title>File:Light-glyph-lamp demo.gif</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=File:Light-glyph-lamp_demo.gif&amp;diff=260"/>
		<updated>2024-01-04T17:55:29Z</updated>

		<summary type="html">&lt;p&gt;Link: Light glyph lamp demonstration.&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Summary ==&lt;br /&gt;
Light glyph lamp demonstration.&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=File:Light-glyph-lamp_pcb-back.jpg&amp;diff=257</id>
		<title>File:Light-glyph-lamp pcb-back.jpg</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=File:Light-glyph-lamp_pcb-back.jpg&amp;diff=257"/>
		<updated>2024-01-04T16:36:54Z</updated>

		<summary type="html">&lt;p&gt;Link: Light glyph lamp PCB, back side, as fabricated by JLCPCB.&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Summary ==&lt;br /&gt;
Light glyph lamp PCB, back side, as fabricated by JLCPCB.&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=File:Light-glyph-lamp_pcb-front.jpg&amp;diff=255</id>
		<title>File:Light-glyph-lamp pcb-front.jpg</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=File:Light-glyph-lamp_pcb-front.jpg&amp;diff=255"/>
		<updated>2024-01-04T16:33:47Z</updated>

		<summary type="html">&lt;p&gt;Link: Light glyph lamp PCB, front side, as fabricated by JLCPCB.&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Summary ==&lt;br /&gt;
Light glyph lamp PCB, front side, as fabricated by JLCPCB.&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=File:Light-glyph-lamp_pcb-assembled.jpg&amp;diff=254</id>
		<title>File:Light-glyph-lamp pcb-assembled.jpg</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=File:Light-glyph-lamp_pcb-assembled.jpg&amp;diff=254"/>
		<updated>2024-01-04T16:30:37Z</updated>

		<summary type="html">&lt;p&gt;Link: Light glyph lamp PCB with components soldered, and attached to the faceplate.&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Summary ==&lt;br /&gt;
Light glyph lamp PCB with components soldered, and attached to the faceplate.&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=MediaWiki:Cite_link_label_group-lower-alpha&amp;diff=253</id>
		<title>MediaWiki:Cite link label group-lower-alpha</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=MediaWiki:Cite_link_label_group-lower-alpha&amp;diff=253"/>
		<updated>2024-01-04T09:29:21Z</updated>

		<summary type="html">&lt;p&gt;Link: Created page with &amp;quot;a b c d e f g h i j k l m n o p q r s t u v w x y z aa ab ac ad ae af ag ah ai aj ak al am an ao ap aq ar as at au av aw ax ay az ba bb bc bd be bf bg bh bi bj bk bl bm bn bo bp bq br bs bt bu bv bw bx by bz ca cb cc cd ce cf cg ch ci cj ck cl cm cn co cp cq cr cs ct cu cv cw cx cy cz da db dc dd de df dg dh di dj dk dl dm dn do dp dq dr ds dt du dv dw dx dy dz ea eb ec ed ee ef eg eh ei ej ek el em en eo ep eq er es et eu ev ew ex ey ez fa fb fc fd fe ff fg fh fi fj fk...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;a b c d e f g h i j k l m n o p q r s t u v w x y z aa ab ac ad ae af ag ah ai aj ak al am an ao ap aq ar as at au av aw ax ay az ba bb bc bd be bf bg bh bi bj bk bl bm bn bo bp bq br bs bt bu bv bw bx by bz ca cb cc cd ce cf cg ch ci cj ck cl cm cn co cp cq cr cs ct cu cv cw cx cy cz da db dc dd de df dg dh di dj dk dl dm dn do dp dq dr ds dt du dv dw dx dy dz ea eb ec ed ee ef eg eh ei ej ek el em en eo ep eq er es et eu ev ew ex ey ez fa fb fc fd fe ff fg fh fi fj fk fl fm fn fo fp fq fr fs ft fu fv fw fx fy fz ga gb gc gd ge gf gg gh gi gj gk gl gm gn go gp gq gr gs gt gu gv gw gx gy gz ha hb hc hd he hf hg hh hi hj hk hl hm hn ho hp hq hr hs ht hu hv hw hx hy hz ia ib ic id ie if ig ih ii ij ik il im in io ip iq ir is it iu iv iw ix iy iz ja jb jc jd je jf jg jh ji jj jk jl jm jn jo jp jq jr js jt ju jv jw jx jy jz ka kb kc kd ke kf kg kh ki kj kk kl km kn ko kp kq kr ks kt ku kv kw kx ky kz la lb lc ld le lf lg lh li lj lk ll lm ln lo lp lq lr ls lt lu lv lw lx ly lz ma mb mc md me mf mg mh mi mj mk ml mm mn mo mp mq mr ms mt mu mv mw mx my mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt nu nv nw nx ny nz oa ob oc od oe of og oh oi oj ok ol om on oo op oq or os ot ou ov ow ox oy oz pa pb pc pd pe pf pg ph pi pj pk pl pm pn po pp pq pr ps pt pu pv pw px py pz qa qb qc qd qe qf qg qh qi qj qk ql qm qn qo qp qq qr qs qt qu qv qw qx qy qz ra rb rc rd re rf rg rh ri rj rk rl rm rn ro rp rq rr rs rt ru rv rw rx ry rz sa sb sc sd se sf sg sh si sj sk sl sm sn so sp sq sr ss st su sv sw sx sy sz ta tb tc td te tf tg th ti tj tk tl tm tn to tp tq tr ts tt tu tv tw tx ty tz ua ub uc ud ue uf ug uh ui uj uk ul um un uo up uq ur us ut uu uv uw ux uy uz va vb vc vd ve vf vg vh vi vj vk vl vm vn vo vp vq vr vs vt vu vv vw vx vy vz wa wb wc wd we wf wg wh wi wj wk wl wm wn wo wp wq wr ws wt wu wv ww wx wy wz xa xb xc xd xe xf xg xh xi xj xk xl xm xn xo xp xq xr xs xt xu xv xw xx xy xz ya yb yc yd ye yf yg yh yi yj yk yl ym yn yo yp yq yr ys yt yu yv yw yx yy yz za zb zc zd ze zf zg zh zi zj zk zl zm zn zo zp zq zr zs zt zu zv zw zx zy zz aaa aab aac aad aae aaf aag aah aai aaj aak aal aam aan aao aap aaq aar aas aat aau aav aaw aax aay aaz aba abb abc abd abe abf abg abh abi abj abk abl abm abn abo abp abq abr abs abt abu abv abw abx aby abz aca acb acc acd ace acf acg ach aci acj ack acl acm acn aco acp acq acr acs act acu acv acw acx acy acz ada adb adc add ade adf adg adh adi adj adk adl adm adn ado adp adq adr ads adt adu adv adw adx ady adz aea aeb aec aed aee aef aeg aeh aei aej aek ael aem aen aeo aep aeq aer aes aet aeu aev aew aex aey aez afa afb afc afd afe aff afg afh afi afj afk afl afm afn afo afp afq afr afs aft afu afv afw afx afy afz aga agb agc agd age agf agg agh agi agj agk agl agm agn ago agp agq agr ags agt agu agv agw agx agy agz aha ahb ahc ahd ahe ahf ahg ahh ahi ahj ahk ahl ahm ahn aho ahp ahq ahr ahs aht ahu ahv ahw ahx ahy ahz aia aib aic aid aie aif aig aih aii aij aik ail aim ain aio aip aiq air ais ait aiu aiv aiw aix aiy aiz aja ajb ajc ajd aje ajf ajg ajh aji ajj ajk ajl ajm ajn ajo ajp ajq ajr ajs ajt aju ajv ajw ajx ajy ajz aka akb akc akd ake akf akg akh aki akj akk akl akm akn ako akp akq akr aks akt aku akv akw akx aky akz ala alb alc ald ale alf alg alh ali alj alk all alm aln alo alp alq alr als alt alu alv alw alx aly alz ama amb amc amd ame amf amg amh ami amj amk aml amm amn amo amp amq amr ams amt amu amv amw amx amy amz ana anb anc and ane anf ang anh ani anj ank anl anm ann ano anp anq anr ans ant anu anv anw anx any anz aoa aob aoc aod aoe aof aog aoh aoi aoj aok aol aom aon aoo aop aoq aor aos aot aou aov aow aox aoy aoz apa apb apc apd ape apf apg aph api apj apk apl apm apn apo app apq apr aps apt apu apv apw apx apy apz aqa aqb aqc aqd aqe aqf aqg aqh aqi aqj aqk aql aqm aqn aqo aqp aqq aqr aqs aqt aqu aqv aqw aqx aqy aqz ara arb arc ard are arf arg arh ari arj ark arl arm arn aro arp arq arr ars art aru arv arw arx ary arz asa asb asc asd ase asf asg ash asi asj ask asl asm asn aso asp asq asr ass ast asu asv asw asx asy asz ata atb atc atd ate atf atg ath ati atj atk atl atm atn ato atp atq atr ats att atu atv atw atx aty atz aua aub auc aud aue auf aug auh aui auj auk aul aum aun auo aup auq aur aus aut auu auv auw aux auy auz ava avb avc avd ave avf avg avh avi avj avk avl avm avn avo avp avq avr avs avt avu avv avw avx avy avz awa awb awc awd awe awf awg awh awi awj awk awl awm awn awo awp awq awr aws awt awu awv aww awx awy awz axa axb axc axd axe axf axg axh axi axj axk axl axm axn axo axp axq axr axs axt axu axv axw axx axy axz aya ayb ayc ayd aye ayf ayg ayh ayi ayj ayk ayl aym ayn ayo ayp ayq ayr ays ayt ayu ayv ayw ayx ayy ayz aza azb azc azd aze azf azg azh azi azj azk azl azm azn azo azp azq azr azs azt azu azv azw azx azy azz&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=Catan-gen&amp;diff=252</id>
		<title>Catan-gen</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=Catan-gen&amp;diff=252"/>
		<updated>2022-11-20T12:45:58Z</updated>

		<summary type="html">&lt;p&gt;Link: /* Notes */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{lowercase}}&lt;br /&gt;
&#039;&#039;&#039;catan-gen&#039;&#039;&#039; is a fully convergent map creation app for [https://www.catan.com/ Catan]. It is written in &amp;lt;code&amp;gt;C++&amp;lt;/code&amp;gt; and uses &amp;lt;code&amp;gt;libadwaita&amp;lt;/code&amp;gt; to provide a UX that is friendly on both desktop and mobile devices.&lt;br /&gt;
&lt;br /&gt;
=Dependencies=&lt;br /&gt;
* Unix-like system (tested: Gentoo, Mobian on PinePhone, Mac OS Monterrey)&lt;br /&gt;
* [https://www.gtk.org/ GTK4] &amp;gt;= 4.6.0&lt;br /&gt;
* [https://gitlab.gnome.org/GNOME/libadwaita libadwaita] &amp;gt;= 1.0.0&lt;br /&gt;
* [https://eigen.tuxfamily.org/index.php?title=Main_Page Eigen] &amp;gt;= 3.4&lt;br /&gt;
* [https://www.cairographics.org/ Cairo] &amp;gt;= 1.14&lt;br /&gt;
* [https://github.com/open-source-parsers/jsoncpp jsoncpp] &amp;gt;= 1.9&lt;br /&gt;
* [https://www.boost.org/ Boost] &amp;gt; 1.74&lt;br /&gt;
* [https://github.com/fmtlib/fmt fmt] &amp;gt; 8.1.0 (as of catan-gen 0.2)&lt;br /&gt;
&lt;br /&gt;
=Obtaining catan-gen=&lt;br /&gt;
catan-gen is available as a source tarball at http://proj.penguindevelopment.org/catan-gen/. [http://proj.penguindevelopment.org/catan-gen/catan-gen-latest.tar.gz Direct link to the latest version.]&lt;br /&gt;
&lt;br /&gt;
Installation follows the usual configure/make/make install procedure.&lt;br /&gt;
&lt;br /&gt;
=Using catan-gen=&lt;br /&gt;
&#039;&#039;&#039;N.B. these instructions apply to version 0.1. As of version 0.2, there are some (initially disabled) UI elements on the startup screen that allow growing/shrinking and/or wiping an existing map.&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Upon launching catan-gen, you will be presented with a simple UI:&lt;br /&gt;
&lt;br /&gt;
[[File:Catan-gen-ui-firstopen.png]]&lt;br /&gt;
&lt;br /&gt;
No map has been generated yet, since the app obviously can&#039;t divine what size of map you want; you can set that on this page. A radius of 0 corresponds to a single tile, a radius of 1 adds the 6 nearest-neighbour tiles around it, and so on. Press the Generate button and enlarge the window to find a map filled with water:&lt;br /&gt;
&lt;br /&gt;
[[File:Catan-gen-blank-map.png]]&lt;br /&gt;
&lt;br /&gt;
Before going into the details, let&#039;s have a look at the toolbar:&lt;br /&gt;
&lt;br /&gt;
[[File:Catan-gen-toolbar.png]]&lt;br /&gt;
&lt;br /&gt;
From left to right, these buttons are &#039;&#039;zoom out&#039;&#039;, &#039;&#039;zoom in&#039;&#039;, &#039;&#039;terrain paint mode&#039;&#039;, &#039;&#039;paint tile type&#039;&#039;, &#039;&#039;roll tile types&#039;&#039;, &#039;&#039;roll numbers&#039;&#039;, &#039;&#039;clear numbers&#039;&#039; and &#039;&#039;clear tile types&#039;&#039;. The zoom buttons are self-explanatory. &#039;&#039;Terrain paint mode&#039;&#039; is a toggle button, and is enabled by default. In this mode, you can draw land areas directly onto your map, simply by pressinging the tiles. &#039;&#039;Paint tile type&#039;&#039; (the downwards arrow) lets you choose &#039;&#039;what&#039;&#039; to draw, and is set to &amp;quot;Unspecified&amp;quot; by default. Press a few tiles to draw your map:&lt;br /&gt;
&lt;br /&gt;
[[File:Catan-gen-unspecified-paint.png]]&lt;br /&gt;
&lt;br /&gt;
If you want to switch a tile back to water, simply press it again. Once you&#039;re satisfied with the shape of your map, it is time to roll tile types, which changes the unspecified tiles into resources. But before you press &#039;&#039;roll tile  types&#039;&#039;, press the &#039;&#039;Tileset&#039;&#039; button in the titlebar. The page that appears allows you specify how many tiles of each resource you want to generate.&lt;br /&gt;
&lt;br /&gt;
[[File:Catan-gen-tileset.png]]&lt;br /&gt;
&lt;br /&gt;
For each resource, you can specify the minimum and maximum number of tiles you want to have. The current number is also listed. Colour-blind users may also want to adjust the colour of each tile type. (N.B. this list can scroll; on desktops, use the mouse scroll wheel or the scrollbar that appears when hovering; on touchscreen devices, just drag to scroll.) When you are satisfied, go back to the map page and press the &#039;&#039;roll tile type&#039;&#039; button (the die) to populate your map:&lt;br /&gt;
&lt;br /&gt;
[[File:Catan-gen-fill-tiles.png]]&lt;br /&gt;
&lt;br /&gt;
If you don&#039;t have enough unspecified tiles to satisfy the minimum tile amounts listed on the tileset page, you will be met with an error instead. If you are unhappy with the generated map, you can still freely change the tiles by pressing them. Alternatively, you can press &#039;&#039;clear tile types&#039;&#039; to wipe every resource back to unspecified (N.B. this includes tiles you have manually set, so be careful with this button). Next, you can auto-generate the numbers that go on each resource, simply by pressing &#039;&#039;roll numbers&#039;&#039; (the circled 8).&lt;br /&gt;
&lt;br /&gt;
[[File:Catan-gen-fill-numbers.png]]&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;Clear numbers&#039;&#039; removes them again, if you are unhappy. If you want to change a single number, you will need to turn off terrain paint mode. Once turned off, pressing a tile will open an overlay that lets you set the type and number directly:&lt;br /&gt;
&lt;br /&gt;
[[File:Catan-gen-tile-overlay.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
(Note that the clicked tile has been highlighted in green.) Press the checkmark button on the right-hand side to accept the changes, or the cross on the left-hand side to discard them.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;NOTE: unfortunately, selecting tile types and numbers from this interface can be unreliable due to [https://gitlab.gnome.org/GNOME/gtk/-/issues/2877 GTK bug 2877]. If the app refuses to accept your selection, you can use the keyboard to navigate to the drop-down and select an entry. Sadly, this does not appear to work with Squeekboard, so you will need a physical (Bluetooth or USB) keyboard on Phosh-based devices.&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
When you are completely satisfied with your map, you can export it from the hamburger menu (top-left corner). &amp;quot;Export SVG&amp;quot; produces a vector graphic representation identical to the one you see on-screen. &amp;quot;Export JSON&amp;quot; produces a textual representation of the map, which can be reimported at a later time. Of course, you can also generate a new map, which brings you back to the startup view.&lt;br /&gt;
&lt;br /&gt;
=Notes=&lt;br /&gt;
Although catan-gen currently suffices at basic mapping, some new features are currently planned, namely &#039;&#039;&#039;custom tile types&#039;&#039;&#039; (which would include the ability to import and export the tile set via JSON) and &#039;&#039;&#039;automatic generation&#039;&#039;&#039; of a map outline, which would most likely also incorporate the placement of &#039;&#039;&#039;harbours&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
Currently, generated maps are always hexagonal, which can waste large swaths of space if a different shape is required. It is already possible to manually create non-hexagonal maps by importing a JSON file as a template, e.g. by exporting a blank hexagonal map and editing the JSON file to remove unwanted tiles. However, this is subject to an important symmetry constraint: &#039;&#039;the centre of the map must correspond to the centre of tile (0, 0)&#039;&#039;. Failure to meet this requirement currently makes it impossible to correctly interpret mouse clicks/finger taps on the map. Since this is not trivial to resolve and the work-around is inconvenient but not prohibitively complicated, generating non-hexagonal maps most likely will not be supported from within the app.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Bugs and feedback==&lt;br /&gt;
{{bugs and feedback}}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[Category:Software]]&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=Catan-gen&amp;diff=251</id>
		<title>Catan-gen</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=Catan-gen&amp;diff=251"/>
		<updated>2022-11-20T12:41:48Z</updated>

		<summary type="html">&lt;p&gt;Link: /* Using catan-gen */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{lowercase}}&lt;br /&gt;
&#039;&#039;&#039;catan-gen&#039;&#039;&#039; is a fully convergent map creation app for [https://www.catan.com/ Catan]. It is written in &amp;lt;code&amp;gt;C++&amp;lt;/code&amp;gt; and uses &amp;lt;code&amp;gt;libadwaita&amp;lt;/code&amp;gt; to provide a UX that is friendly on both desktop and mobile devices.&lt;br /&gt;
&lt;br /&gt;
=Dependencies=&lt;br /&gt;
* Unix-like system (tested: Gentoo, Mobian on PinePhone, Mac OS Monterrey)&lt;br /&gt;
* [https://www.gtk.org/ GTK4] &amp;gt;= 4.6.0&lt;br /&gt;
* [https://gitlab.gnome.org/GNOME/libadwaita libadwaita] &amp;gt;= 1.0.0&lt;br /&gt;
* [https://eigen.tuxfamily.org/index.php?title=Main_Page Eigen] &amp;gt;= 3.4&lt;br /&gt;
* [https://www.cairographics.org/ Cairo] &amp;gt;= 1.14&lt;br /&gt;
* [https://github.com/open-source-parsers/jsoncpp jsoncpp] &amp;gt;= 1.9&lt;br /&gt;
* [https://www.boost.org/ Boost] &amp;gt; 1.74&lt;br /&gt;
* [https://github.com/fmtlib/fmt fmt] &amp;gt; 8.1.0 (as of catan-gen 0.2)&lt;br /&gt;
&lt;br /&gt;
=Obtaining catan-gen=&lt;br /&gt;
catan-gen is available as a source tarball at http://proj.penguindevelopment.org/catan-gen/. [http://proj.penguindevelopment.org/catan-gen/catan-gen-latest.tar.gz Direct link to the latest version.]&lt;br /&gt;
&lt;br /&gt;
Installation follows the usual configure/make/make install procedure.&lt;br /&gt;
&lt;br /&gt;
=Using catan-gen=&lt;br /&gt;
&#039;&#039;&#039;N.B. these instructions apply to version 0.1. As of version 0.2, there are some (initially disabled) UI elements on the startup screen that allow growing/shrinking and/or wiping an existing map.&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Upon launching catan-gen, you will be presented with a simple UI:&lt;br /&gt;
&lt;br /&gt;
[[File:Catan-gen-ui-firstopen.png]]&lt;br /&gt;
&lt;br /&gt;
No map has been generated yet, since the app obviously can&#039;t divine what size of map you want; you can set that on this page. A radius of 0 corresponds to a single tile, a radius of 1 adds the 6 nearest-neighbour tiles around it, and so on. Press the Generate button and enlarge the window to find a map filled with water:&lt;br /&gt;
&lt;br /&gt;
[[File:Catan-gen-blank-map.png]]&lt;br /&gt;
&lt;br /&gt;
Before going into the details, let&#039;s have a look at the toolbar:&lt;br /&gt;
&lt;br /&gt;
[[File:Catan-gen-toolbar.png]]&lt;br /&gt;
&lt;br /&gt;
From left to right, these buttons are &#039;&#039;zoom out&#039;&#039;, &#039;&#039;zoom in&#039;&#039;, &#039;&#039;terrain paint mode&#039;&#039;, &#039;&#039;paint tile type&#039;&#039;, &#039;&#039;roll tile types&#039;&#039;, &#039;&#039;roll numbers&#039;&#039;, &#039;&#039;clear numbers&#039;&#039; and &#039;&#039;clear tile types&#039;&#039;. The zoom buttons are self-explanatory. &#039;&#039;Terrain paint mode&#039;&#039; is a toggle button, and is enabled by default. In this mode, you can draw land areas directly onto your map, simply by pressinging the tiles. &#039;&#039;Paint tile type&#039;&#039; (the downwards arrow) lets you choose &#039;&#039;what&#039;&#039; to draw, and is set to &amp;quot;Unspecified&amp;quot; by default. Press a few tiles to draw your map:&lt;br /&gt;
&lt;br /&gt;
[[File:Catan-gen-unspecified-paint.png]]&lt;br /&gt;
&lt;br /&gt;
If you want to switch a tile back to water, simply press it again. Once you&#039;re satisfied with the shape of your map, it is time to roll tile types, which changes the unspecified tiles into resources. But before you press &#039;&#039;roll tile  types&#039;&#039;, press the &#039;&#039;Tileset&#039;&#039; button in the titlebar. The page that appears allows you specify how many tiles of each resource you want to generate.&lt;br /&gt;
&lt;br /&gt;
[[File:Catan-gen-tileset.png]]&lt;br /&gt;
&lt;br /&gt;
For each resource, you can specify the minimum and maximum number of tiles you want to have. The current number is also listed. Colour-blind users may also want to adjust the colour of each tile type. (N.B. this list can scroll; on desktops, use the mouse scroll wheel or the scrollbar that appears when hovering; on touchscreen devices, just drag to scroll.) When you are satisfied, go back to the map page and press the &#039;&#039;roll tile type&#039;&#039; button (the die) to populate your map:&lt;br /&gt;
&lt;br /&gt;
[[File:Catan-gen-fill-tiles.png]]&lt;br /&gt;
&lt;br /&gt;
If you don&#039;t have enough unspecified tiles to satisfy the minimum tile amounts listed on the tileset page, you will be met with an error instead. If you are unhappy with the generated map, you can still freely change the tiles by pressing them. Alternatively, you can press &#039;&#039;clear tile types&#039;&#039; to wipe every resource back to unspecified (N.B. this includes tiles you have manually set, so be careful with this button). Next, you can auto-generate the numbers that go on each resource, simply by pressing &#039;&#039;roll numbers&#039;&#039; (the circled 8).&lt;br /&gt;
&lt;br /&gt;
[[File:Catan-gen-fill-numbers.png]]&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;Clear numbers&#039;&#039; removes them again, if you are unhappy. If you want to change a single number, you will need to turn off terrain paint mode. Once turned off, pressing a tile will open an overlay that lets you set the type and number directly:&lt;br /&gt;
&lt;br /&gt;
[[File:Catan-gen-tile-overlay.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
(Note that the clicked tile has been highlighted in green.) Press the checkmark button on the right-hand side to accept the changes, or the cross on the left-hand side to discard them.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;NOTE: unfortunately, selecting tile types and numbers from this interface can be unreliable due to [https://gitlab.gnome.org/GNOME/gtk/-/issues/2877 GTK bug 2877]. If the app refuses to accept your selection, you can use the keyboard to navigate to the drop-down and select an entry. Sadly, this does not appear to work with Squeekboard, so you will need a physical (Bluetooth or USB) keyboard on Phosh-based devices.&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
When you are completely satisfied with your map, you can export it from the hamburger menu (top-left corner). &amp;quot;Export SVG&amp;quot; produces a vector graphic representation identical to the one you see on-screen. &amp;quot;Export JSON&amp;quot; produces a textual representation of the map, which can be reimported at a later time. Of course, you can also generate a new map, which brings you back to the startup view.&lt;br /&gt;
&lt;br /&gt;
=Notes=&lt;br /&gt;
Although catan-gen currently suffices at basic mapping, some new features are currently planned. &#039;&#039;&#039;Internationalisation&#039;&#039;&#039; will be relatively simple to add to the app as-is. A more complex feature --- the most complex one currently planned --- is the support of &#039;&#039;&#039;custom tile types&#039;&#039;&#039;, which would include the ability to import and export the tile set via JSON.&lt;br /&gt;
&lt;br /&gt;
Currently, generated maps are always hexagonal, which can waste large swaths of space if a different shape is required. It is already possible to manually create non-hexagonal maps by importing a JSON file as a template, e.g. by exporting a blank hexagonal map and editing the JSON file to remove unwanted tiles. However, this is subject to an important symmetry constraint: &#039;&#039;the centre of the map must correspond to the centre of tile (0, 0)&#039;&#039;. Failure to meet this requirement currently makes it impossible to correctly interpret mouse clicks/finger taps on the map. Since this is not trivial to resolve and the work-around is inconvenient but not prohibitively complicated, generating non-hexagonal maps most likely will not be supported from within the app.&lt;br /&gt;
&lt;br /&gt;
Allowing the map to be &#039;&#039;&#039;extended&#039;&#039;&#039; or &#039;&#039;&#039;shrunk&#039;&#039;&#039; without discarding it completely is relatively simple and will most likely be included in the next non-bugfix update.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Automatic generation&#039;&#039;&#039; of a map outline is the final complex feature currently planned. This would most likely also incorporate the placement of &#039;&#039;&#039;harbours&#039;&#039;&#039;, which are not currently included.&lt;br /&gt;
&lt;br /&gt;
==Bugs and feedback==&lt;br /&gt;
{{bugs and feedback}}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[Category:Software]]&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=Catan-gen&amp;diff=250</id>
		<title>Catan-gen</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=Catan-gen&amp;diff=250"/>
		<updated>2022-11-20T12:37:46Z</updated>

		<summary type="html">&lt;p&gt;Link: /* Dependencies */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{lowercase}}&lt;br /&gt;
&#039;&#039;&#039;catan-gen&#039;&#039;&#039; is a fully convergent map creation app for [https://www.catan.com/ Catan]. It is written in &amp;lt;code&amp;gt;C++&amp;lt;/code&amp;gt; and uses &amp;lt;code&amp;gt;libadwaita&amp;lt;/code&amp;gt; to provide a UX that is friendly on both desktop and mobile devices.&lt;br /&gt;
&lt;br /&gt;
=Dependencies=&lt;br /&gt;
* Unix-like system (tested: Gentoo, Mobian on PinePhone, Mac OS Monterrey)&lt;br /&gt;
* [https://www.gtk.org/ GTK4] &amp;gt;= 4.6.0&lt;br /&gt;
* [https://gitlab.gnome.org/GNOME/libadwaita libadwaita] &amp;gt;= 1.0.0&lt;br /&gt;
* [https://eigen.tuxfamily.org/index.php?title=Main_Page Eigen] &amp;gt;= 3.4&lt;br /&gt;
* [https://www.cairographics.org/ Cairo] &amp;gt;= 1.14&lt;br /&gt;
* [https://github.com/open-source-parsers/jsoncpp jsoncpp] &amp;gt;= 1.9&lt;br /&gt;
* [https://www.boost.org/ Boost] &amp;gt; 1.74&lt;br /&gt;
* [https://github.com/fmtlib/fmt fmt] &amp;gt; 8.1.0 (as of catan-gen 0.2)&lt;br /&gt;
&lt;br /&gt;
=Obtaining catan-gen=&lt;br /&gt;
catan-gen is available as a source tarball at http://proj.penguindevelopment.org/catan-gen/. [http://proj.penguindevelopment.org/catan-gen/catan-gen-latest.tar.gz Direct link to the latest version.]&lt;br /&gt;
&lt;br /&gt;
Installation follows the usual configure/make/make install procedure.&lt;br /&gt;
&lt;br /&gt;
=Using catan-gen=&lt;br /&gt;
Upon launching catan-gen, you will be presented with a simple UI:&lt;br /&gt;
&lt;br /&gt;
[[File:Catan-gen-ui-firstopen.png]]&lt;br /&gt;
&lt;br /&gt;
No map has been generated yet, since the app obviously can&#039;t divine what size of map you want; you can set that on this page. A radius of 0 corresponds to a single tile, a radius of 1 adds the 6 nearest-neighbour tiles around it, and so on. Press the Generate button and enlarge the window to find a map filled with water:&lt;br /&gt;
&lt;br /&gt;
[[File:Catan-gen-blank-map.png]]&lt;br /&gt;
&lt;br /&gt;
Before going into the details, let&#039;s have a look at the toolbar:&lt;br /&gt;
&lt;br /&gt;
[[File:Catan-gen-toolbar.png]]&lt;br /&gt;
&lt;br /&gt;
From left to right, these buttons are &#039;&#039;zoom out&#039;&#039;, &#039;&#039;zoom in&#039;&#039;, &#039;&#039;terrain paint mode&#039;&#039;, &#039;&#039;paint tile type&#039;&#039;, &#039;&#039;roll tile types&#039;&#039;, &#039;&#039;roll numbers&#039;&#039;, &#039;&#039;clear numbers&#039;&#039; and &#039;&#039;clear tile types&#039;&#039;. The zoom buttons are self-explanatory. &#039;&#039;Terrain paint mode&#039;&#039; is a toggle button, and is enabled by default. In this mode, you can draw land areas directly onto your map, simply by pressinging the tiles. &#039;&#039;Paint tile type&#039;&#039; (the downwards arrow) lets you choose &#039;&#039;what&#039;&#039; to draw, and is set to &amp;quot;Unspecified&amp;quot; by default. Press a few tiles to draw your map:&lt;br /&gt;
&lt;br /&gt;
[[File:Catan-gen-unspecified-paint.png]]&lt;br /&gt;
&lt;br /&gt;
If you want to switch a tile back to water, simply press it again. Once you&#039;re satisfied with the shape of your map, it is time to roll tile types, which changes the unspecified tiles into resources. But before you press &#039;&#039;roll tile  types&#039;&#039;, press the &#039;&#039;Tileset&#039;&#039; button in the titlebar. The page that appears allows you specify how many tiles of each resource you want to generate.&lt;br /&gt;
&lt;br /&gt;
[[File:Catan-gen-tileset.png]]&lt;br /&gt;
&lt;br /&gt;
For each resource, you can specify the minimum and maximum number of tiles you want to have. The current number is also listed. Colour-blind users may also want to adjust the colour of each tile type. (N.B. this list can scroll; on desktops, use the mouse scroll wheel or the scrollbar that appears when hovering; on touchscreen devices, just drag to scroll.) When you are satisfied, go back to the map page and press the &#039;&#039;roll tile type&#039;&#039; button (the die) to populate your map:&lt;br /&gt;
&lt;br /&gt;
[[File:Catan-gen-fill-tiles.png]]&lt;br /&gt;
&lt;br /&gt;
If you don&#039;t have enough unspecified tiles to satisfy the minimum tile amounts listed on the tileset page, you will be met with an error instead. If you are unhappy with the generated map, you can still freely change the tiles by pressing them. Alternatively, you can press &#039;&#039;clear tile types&#039;&#039; to wipe every resource back to unspecified (N.B. this includes tiles you have manually set, so be careful with this button). Next, you can auto-generate the numbers that go on each resource, simply by pressing &#039;&#039;roll numbers&#039;&#039; (the circled 8).&lt;br /&gt;
&lt;br /&gt;
[[File:Catan-gen-fill-numbers.png]]&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;Clear numbers&#039;&#039; removes them again, if you are unhappy. If you want to change a single number, you will need to turn off terrain paint mode. Once turned off, pressing a tile will open an overlay that lets you set the type and number directly:&lt;br /&gt;
&lt;br /&gt;
[[File:Catan-gen-tile-overlay.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
(Note that the clicked tile has been highlighted in green.) Press the checkmark button on the right-hand side to accept the changes, or the cross on the left-hand side to discard them.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;NOTE: unfortunately, selecting tile types and numbers from this interface can be unreliable due to [https://gitlab.gnome.org/GNOME/gtk/-/issues/2877 GTK bug 2877]. If the app refuses to accept your selection, you can use the keyboard to navigate to the drop-down and select an entry. Sadly, this does not appear to work with Squeekboard, so you will need a physical (Bluetooth or USB) keyboard on Phosh-based devices.&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
When you are completely satisfied with your map, you can export it from the hamburger menu (top-left corner). &amp;quot;Export SVG&amp;quot; produces a vector graphic representation identical to the one you see on-screen. &amp;quot;Export JSON&amp;quot; produces a textual representation of the map, which can be reimported at a later time. Of course, you can also generate a new map, which brings you back to the startup view.&lt;br /&gt;
&lt;br /&gt;
=Notes=&lt;br /&gt;
Although catan-gen currently suffices at basic mapping, some new features are currently planned. &#039;&#039;&#039;Internationalisation&#039;&#039;&#039; will be relatively simple to add to the app as-is. A more complex feature --- the most complex one currently planned --- is the support of &#039;&#039;&#039;custom tile types&#039;&#039;&#039;, which would include the ability to import and export the tile set via JSON.&lt;br /&gt;
&lt;br /&gt;
Currently, generated maps are always hexagonal, which can waste large swaths of space if a different shape is required. It is already possible to manually create non-hexagonal maps by importing a JSON file as a template, e.g. by exporting a blank hexagonal map and editing the JSON file to remove unwanted tiles. However, this is subject to an important symmetry constraint: &#039;&#039;the centre of the map must correspond to the centre of tile (0, 0)&#039;&#039;. Failure to meet this requirement currently makes it impossible to correctly interpret mouse clicks/finger taps on the map. Since this is not trivial to resolve and the work-around is inconvenient but not prohibitively complicated, generating non-hexagonal maps most likely will not be supported from within the app.&lt;br /&gt;
&lt;br /&gt;
Allowing the map to be &#039;&#039;&#039;extended&#039;&#039;&#039; or &#039;&#039;&#039;shrunk&#039;&#039;&#039; without discarding it completely is relatively simple and will most likely be included in the next non-bugfix update.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Automatic generation&#039;&#039;&#039; of a map outline is the final complex feature currently planned. This would most likely also incorporate the placement of &#039;&#039;&#039;harbours&#039;&#039;&#039;, which are not currently included.&lt;br /&gt;
&lt;br /&gt;
==Bugs and feedback==&lt;br /&gt;
{{bugs and feedback}}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[Category:Software]]&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=Catan-gen&amp;diff=249</id>
		<title>Catan-gen</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=Catan-gen&amp;diff=249"/>
		<updated>2022-08-09T11:59:33Z</updated>

		<summary type="html">&lt;p&gt;Link: /* Dependencies */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{lowercase}}&lt;br /&gt;
&#039;&#039;&#039;catan-gen&#039;&#039;&#039; is a fully convergent map creation app for [https://www.catan.com/ Catan]. It is written in &amp;lt;code&amp;gt;C++&amp;lt;/code&amp;gt; and uses &amp;lt;code&amp;gt;libadwaita&amp;lt;/code&amp;gt; to provide a UX that is friendly on both desktop and mobile devices.&lt;br /&gt;
&lt;br /&gt;
=Dependencies=&lt;br /&gt;
* Unix-like system (tested: Gentoo, Mobian on PinePhone, Mac OS Monterrey)&lt;br /&gt;
* [https://www.gtk.org/ GTK4] &amp;gt;= 4.6.0&lt;br /&gt;
* [https://gitlab.gnome.org/GNOME/libadwaita libadwaita] &amp;gt;= 1.0.0&lt;br /&gt;
* [https://eigen.tuxfamily.org/index.php?title=Main_Page Eigen] &amp;gt;= 3.4&lt;br /&gt;
* [https://www.cairographics.org/ Cairo] &amp;gt;= 1.14&lt;br /&gt;
* [https://github.com/open-source-parsers/jsoncpp jsoncpp] &amp;gt;= 1.9&lt;br /&gt;
* [https://www.boost.org/ Boost] &amp;gt; 1.74&lt;br /&gt;
&lt;br /&gt;
=Obtaining catan-gen=&lt;br /&gt;
catan-gen is available as a source tarball at http://proj.penguindevelopment.org/catan-gen/. [http://proj.penguindevelopment.org/catan-gen/catan-gen-latest.tar.gz Direct link to the latest version.]&lt;br /&gt;
&lt;br /&gt;
Installation follows the usual configure/make/make install procedure.&lt;br /&gt;
&lt;br /&gt;
=Using catan-gen=&lt;br /&gt;
Upon launching catan-gen, you will be presented with a simple UI:&lt;br /&gt;
&lt;br /&gt;
[[File:Catan-gen-ui-firstopen.png]]&lt;br /&gt;
&lt;br /&gt;
No map has been generated yet, since the app obviously can&#039;t divine what size of map you want; you can set that on this page. A radius of 0 corresponds to a single tile, a radius of 1 adds the 6 nearest-neighbour tiles around it, and so on. Press the Generate button and enlarge the window to find a map filled with water:&lt;br /&gt;
&lt;br /&gt;
[[File:Catan-gen-blank-map.png]]&lt;br /&gt;
&lt;br /&gt;
Before going into the details, let&#039;s have a look at the toolbar:&lt;br /&gt;
&lt;br /&gt;
[[File:Catan-gen-toolbar.png]]&lt;br /&gt;
&lt;br /&gt;
From left to right, these buttons are &#039;&#039;zoom out&#039;&#039;, &#039;&#039;zoom in&#039;&#039;, &#039;&#039;terrain paint mode&#039;&#039;, &#039;&#039;paint tile type&#039;&#039;, &#039;&#039;roll tile types&#039;&#039;, &#039;&#039;roll numbers&#039;&#039;, &#039;&#039;clear numbers&#039;&#039; and &#039;&#039;clear tile types&#039;&#039;. The zoom buttons are self-explanatory. &#039;&#039;Terrain paint mode&#039;&#039; is a toggle button, and is enabled by default. In this mode, you can draw land areas directly onto your map, simply by pressinging the tiles. &#039;&#039;Paint tile type&#039;&#039; (the downwards arrow) lets you choose &#039;&#039;what&#039;&#039; to draw, and is set to &amp;quot;Unspecified&amp;quot; by default. Press a few tiles to draw your map:&lt;br /&gt;
&lt;br /&gt;
[[File:Catan-gen-unspecified-paint.png]]&lt;br /&gt;
&lt;br /&gt;
If you want to switch a tile back to water, simply press it again. Once you&#039;re satisfied with the shape of your map, it is time to roll tile types, which changes the unspecified tiles into resources. But before you press &#039;&#039;roll tile  types&#039;&#039;, press the &#039;&#039;Tileset&#039;&#039; button in the titlebar. The page that appears allows you specify how many tiles of each resource you want to generate.&lt;br /&gt;
&lt;br /&gt;
[[File:Catan-gen-tileset.png]]&lt;br /&gt;
&lt;br /&gt;
For each resource, you can specify the minimum and maximum number of tiles you want to have. The current number is also listed. Colour-blind users may also want to adjust the colour of each tile type. (N.B. this list can scroll; on desktops, use the mouse scroll wheel or the scrollbar that appears when hovering; on touchscreen devices, just drag to scroll.) When you are satisfied, go back to the map page and press the &#039;&#039;roll tile type&#039;&#039; button (the die) to populate your map:&lt;br /&gt;
&lt;br /&gt;
[[File:Catan-gen-fill-tiles.png]]&lt;br /&gt;
&lt;br /&gt;
If you don&#039;t have enough unspecified tiles to satisfy the minimum tile amounts listed on the tileset page, you will be met with an error instead. If you are unhappy with the generated map, you can still freely change the tiles by pressing them. Alternatively, you can press &#039;&#039;clear tile types&#039;&#039; to wipe every resource back to unspecified (N.B. this includes tiles you have manually set, so be careful with this button). Next, you can auto-generate the numbers that go on each resource, simply by pressing &#039;&#039;roll numbers&#039;&#039; (the circled 8).&lt;br /&gt;
&lt;br /&gt;
[[File:Catan-gen-fill-numbers.png]]&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;Clear numbers&#039;&#039; removes them again, if you are unhappy. If you want to change a single number, you will need to turn off terrain paint mode. Once turned off, pressing a tile will open an overlay that lets you set the type and number directly:&lt;br /&gt;
&lt;br /&gt;
[[File:Catan-gen-tile-overlay.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
(Note that the clicked tile has been highlighted in green.) Press the checkmark button on the right-hand side to accept the changes, or the cross on the left-hand side to discard them.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;NOTE: unfortunately, selecting tile types and numbers from this interface can be unreliable due to [https://gitlab.gnome.org/GNOME/gtk/-/issues/2877 GTK bug 2877]. If the app refuses to accept your selection, you can use the keyboard to navigate to the drop-down and select an entry. Sadly, this does not appear to work with Squeekboard, so you will need a physical (Bluetooth or USB) keyboard on Phosh-based devices.&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
When you are completely satisfied with your map, you can export it from the hamburger menu (top-left corner). &amp;quot;Export SVG&amp;quot; produces a vector graphic representation identical to the one you see on-screen. &amp;quot;Export JSON&amp;quot; produces a textual representation of the map, which can be reimported at a later time. Of course, you can also generate a new map, which brings you back to the startup view.&lt;br /&gt;
&lt;br /&gt;
=Notes=&lt;br /&gt;
Although catan-gen currently suffices at basic mapping, some new features are currently planned. &#039;&#039;&#039;Internationalisation&#039;&#039;&#039; will be relatively simple to add to the app as-is. A more complex feature --- the most complex one currently planned --- is the support of &#039;&#039;&#039;custom tile types&#039;&#039;&#039;, which would include the ability to import and export the tile set via JSON.&lt;br /&gt;
&lt;br /&gt;
Currently, generated maps are always hexagonal, which can waste large swaths of space if a different shape is required. It is already possible to manually create non-hexagonal maps by importing a JSON file as a template, e.g. by exporting a blank hexagonal map and editing the JSON file to remove unwanted tiles. However, this is subject to an important symmetry constraint: &#039;&#039;the centre of the map must correspond to the centre of tile (0, 0)&#039;&#039;. Failure to meet this requirement currently makes it impossible to correctly interpret mouse clicks/finger taps on the map. Since this is not trivial to resolve and the work-around is inconvenient but not prohibitively complicated, generating non-hexagonal maps most likely will not be supported from within the app.&lt;br /&gt;
&lt;br /&gt;
Allowing the map to be &#039;&#039;&#039;extended&#039;&#039;&#039; or &#039;&#039;&#039;shrunk&#039;&#039;&#039; without discarding it completely is relatively simple and will most likely be included in the next non-bugfix update.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Automatic generation&#039;&#039;&#039; of a map outline is the final complex feature currently planned. This would most likely also incorporate the placement of &#039;&#039;&#039;harbours&#039;&#039;&#039;, which are not currently included.&lt;br /&gt;
&lt;br /&gt;
==Bugs and feedback==&lt;br /&gt;
{{bugs and feedback}}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[Category:Software]]&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=Main_Page&amp;diff=248</id>
		<title>Main Page</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=Main_Page&amp;diff=248"/>
		<updated>2022-08-06T11:16:55Z</updated>

		<summary type="html">&lt;p&gt;Link: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Welcome to &#039;&#039;&#039;Penguin Development&#039;&#039;&#039;! We make [[w:free and open-source software|free and open-source software]] and [[w:Open-source hardware|hardware]].&lt;br /&gt;
&lt;br /&gt;
See below for current releases. All software and hardware is licensed under the [https://gnu.org/licenses/gpl.html GPL] (version 3 or higher) unless otherwise stated.&lt;br /&gt;
&lt;br /&gt;
=Software=&lt;br /&gt;
* [[AUTOMOME]] — the OTTish AUTOMEME clone&lt;br /&gt;
* [[Ising]] — Monte Carlo Ising model simulator&lt;br /&gt;
* [[stackermann]] — a stack-based [[w:Ackermann function|Ackermann function]] calculator&lt;br /&gt;
* [[catan-gen]] — convergent map creation app for [https://www.catan.com/ Catan]&lt;br /&gt;
&lt;br /&gt;
=Hardware=&lt;br /&gt;
* [[timelapse]] — time-lapse trigger for digital cameras&lt;br /&gt;
&lt;br /&gt;
=Code snippets and how-tos=&lt;br /&gt;
* [[Animated 3D plotting with Blender]]&lt;br /&gt;
* [[Matrix Synapse and mautrix-whatsapp in a VPN]]&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=Main_Page&amp;diff=247</id>
		<title>Main Page</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=Main_Page&amp;diff=247"/>
		<updated>2022-08-06T11:16:35Z</updated>

		<summary type="html">&lt;p&gt;Link: /* Software */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Welcome to &#039;&#039;&#039;Penguin Development&#039;&#039;&#039;! We make [[w:free and open-source software|free and open-source software]] and [[w:Open-source hardware|hardware]].&lt;br /&gt;
&lt;br /&gt;
See below for current releases. All software and hardware is licensed under the [https://gnu.org/licenses/gpl.html GPL] (version 3 or higher) unless otherwise stated.&lt;br /&gt;
&lt;br /&gt;
=Software=&lt;br /&gt;
* [[AUTOMOME]] — the OTTish AUTOMEME clone&lt;br /&gt;
* [[Ising]] — Monte Carlo Ising model simulator&lt;br /&gt;
* [[stackermann]] — a stack-based [[w:Ackermann function|Ackermann function]] calculator&lt;br /&gt;
* [[catan-gen]] — convergent map creation app for [https://www.catan.com/ Catan]&lt;br /&gt;
&lt;br /&gt;
=Hardware=&lt;br /&gt;
* [[timelapse]] — time-lapse trigger for digital cameras&lt;br /&gt;
&lt;br /&gt;
=Code snippets and how-tos=&lt;br /&gt;
* [[Animated 3D plotting with Blender]]&lt;br /&gt;
* [[Matrix Synapse and mautrix-whatsapp in a VPN]]&lt;br /&gt;
&lt;br /&gt;
=Other=&lt;br /&gt;
* [[LineageOS builds|Unofficial LineageOS builds]]&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=Catan-gen&amp;diff=246</id>
		<title>Catan-gen</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=Catan-gen&amp;diff=246"/>
		<updated>2022-08-06T11:15:29Z</updated>

		<summary type="html">&lt;p&gt;Link: Created page with &amp;quot;{{lowercase}} &amp;#039;&amp;#039;&amp;#039;catan-gen&amp;#039;&amp;#039;&amp;#039; is a fully convergent map creation app for [https://www.catan.com/ Catan]. It is written in &amp;lt;code&amp;gt;C++&amp;lt;/code&amp;gt; and uses &amp;lt;code&amp;gt;libadwaita&amp;lt;/code&amp;gt; to...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{lowercase}}&lt;br /&gt;
&#039;&#039;&#039;catan-gen&#039;&#039;&#039; is a fully convergent map creation app for [https://www.catan.com/ Catan]. It is written in &amp;lt;code&amp;gt;C++&amp;lt;/code&amp;gt; and uses &amp;lt;code&amp;gt;libadwaita&amp;lt;/code&amp;gt; to provide a UX that is friendly on both desktop and mobile devices.&lt;br /&gt;
&lt;br /&gt;
=Dependencies=&lt;br /&gt;
* Unix-like system (tested: Gentoo, Mobian on PinePhone, Mac OS X)&lt;br /&gt;
* [https://www.gtk.org/ GTK4] &amp;gt;= 4.6.0&lt;br /&gt;
* [https://gitlab.gnome.org/GNOME/libadwaita libadwaita] &amp;gt;= 1.0.0&lt;br /&gt;
* [https://eigen.tuxfamily.org/index.php?title=Main_Page Eigen] &amp;gt;= 3.4&lt;br /&gt;
* [https://www.cairographics.org/ Cairo] &amp;gt;= 1.14&lt;br /&gt;
* [https://github.com/open-source-parsers/jsoncpp jsoncpp] &amp;gt;= 1.9&lt;br /&gt;
* [https://www.boost.org/ Boost] &amp;gt; 1.74&lt;br /&gt;
&lt;br /&gt;
=Obtaining catan-gen=&lt;br /&gt;
catan-gen is available as a source tarball at http://proj.penguindevelopment.org/catan-gen/. [http://proj.penguindevelopment.org/catan-gen/catan-gen-latest.tar.gz Direct link to the latest version.]&lt;br /&gt;
&lt;br /&gt;
Installation follows the usual configure/make/make install procedure.&lt;br /&gt;
&lt;br /&gt;
=Using catan-gen=&lt;br /&gt;
Upon launching catan-gen, you will be presented with a simple UI:&lt;br /&gt;
&lt;br /&gt;
[[File:Catan-gen-ui-firstopen.png]]&lt;br /&gt;
&lt;br /&gt;
No map has been generated yet, since the app obviously can&#039;t divine what size of map you want; you can set that on this page. A radius of 0 corresponds to a single tile, a radius of 1 adds the 6 nearest-neighbour tiles around it, and so on. Press the Generate button and enlarge the window to find a map filled with water:&lt;br /&gt;
&lt;br /&gt;
[[File:Catan-gen-blank-map.png]]&lt;br /&gt;
&lt;br /&gt;
Before going into the details, let&#039;s have a look at the toolbar:&lt;br /&gt;
&lt;br /&gt;
[[File:Catan-gen-toolbar.png]]&lt;br /&gt;
&lt;br /&gt;
From left to right, these buttons are &#039;&#039;zoom out&#039;&#039;, &#039;&#039;zoom in&#039;&#039;, &#039;&#039;terrain paint mode&#039;&#039;, &#039;&#039;paint tile type&#039;&#039;, &#039;&#039;roll tile types&#039;&#039;, &#039;&#039;roll numbers&#039;&#039;, &#039;&#039;clear numbers&#039;&#039; and &#039;&#039;clear tile types&#039;&#039;. The zoom buttons are self-explanatory. &#039;&#039;Terrain paint mode&#039;&#039; is a toggle button, and is enabled by default. In this mode, you can draw land areas directly onto your map, simply by pressinging the tiles. &#039;&#039;Paint tile type&#039;&#039; (the downwards arrow) lets you choose &#039;&#039;what&#039;&#039; to draw, and is set to &amp;quot;Unspecified&amp;quot; by default. Press a few tiles to draw your map:&lt;br /&gt;
&lt;br /&gt;
[[File:Catan-gen-unspecified-paint.png]]&lt;br /&gt;
&lt;br /&gt;
If you want to switch a tile back to water, simply press it again. Once you&#039;re satisfied with the shape of your map, it is time to roll tile types, which changes the unspecified tiles into resources. But before you press &#039;&#039;roll tile  types&#039;&#039;, press the &#039;&#039;Tileset&#039;&#039; button in the titlebar. The page that appears allows you specify how many tiles of each resource you want to generate.&lt;br /&gt;
&lt;br /&gt;
[[File:Catan-gen-tileset.png]]&lt;br /&gt;
&lt;br /&gt;
For each resource, you can specify the minimum and maximum number of tiles you want to have. The current number is also listed. Colour-blind users may also want to adjust the colour of each tile type. (N.B. this list can scroll; on desktops, use the mouse scroll wheel or the scrollbar that appears when hovering; on touchscreen devices, just drag to scroll.) When you are satisfied, go back to the map page and press the &#039;&#039;roll tile type&#039;&#039; button (the die) to populate your map:&lt;br /&gt;
&lt;br /&gt;
[[File:Catan-gen-fill-tiles.png]]&lt;br /&gt;
&lt;br /&gt;
If you don&#039;t have enough unspecified tiles to satisfy the minimum tile amounts listed on the tileset page, you will be met with an error instead. If you are unhappy with the generated map, you can still freely change the tiles by pressing them. Alternatively, you can press &#039;&#039;clear tile types&#039;&#039; to wipe every resource back to unspecified (N.B. this includes tiles you have manually set, so be careful with this button). Next, you can auto-generate the numbers that go on each resource, simply by pressing &#039;&#039;roll numbers&#039;&#039; (the circled 8).&lt;br /&gt;
&lt;br /&gt;
[[File:Catan-gen-fill-numbers.png]]&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;Clear numbers&#039;&#039; removes them again, if you are unhappy. If you want to change a single number, you will need to turn off terrain paint mode. Once turned off, pressing a tile will open an overlay that lets you set the type and number directly:&lt;br /&gt;
&lt;br /&gt;
[[File:Catan-gen-tile-overlay.png]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
(Note that the clicked tile has been highlighted in green.) Press the checkmark button on the right-hand side to accept the changes, or the cross on the left-hand side to discard them.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;NOTE: unfortunately, selecting tile types and numbers from this interface can be unreliable due to [https://gitlab.gnome.org/GNOME/gtk/-/issues/2877 GTK bug 2877]. If the app refuses to accept your selection, you can use the keyboard to navigate to the drop-down and select an entry. Sadly, this does not appear to work with Squeekboard, so you will need a physical (Bluetooth or USB) keyboard on Phosh-based devices.&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
When you are completely satisfied with your map, you can export it from the hamburger menu (top-left corner). &amp;quot;Export SVG&amp;quot; produces a vector graphic representation identical to the one you see on-screen. &amp;quot;Export JSON&amp;quot; produces a textual representation of the map, which can be reimported at a later time. Of course, you can also generate a new map, which brings you back to the startup view.&lt;br /&gt;
&lt;br /&gt;
=Notes=&lt;br /&gt;
Although catan-gen currently suffices at basic mapping, some new features are currently planned. &#039;&#039;&#039;Internationalisation&#039;&#039;&#039; will be relatively simple to add to the app as-is. A more complex feature --- the most complex one currently planned --- is the support of &#039;&#039;&#039;custom tile types&#039;&#039;&#039;, which would include the ability to import and export the tile set via JSON.&lt;br /&gt;
&lt;br /&gt;
Currently, generated maps are always hexagonal, which can waste large swaths of space if a different shape is required. It is already possible to manually create non-hexagonal maps by importing a JSON file as a template, e.g. by exporting a blank hexagonal map and editing the JSON file to remove unwanted tiles. However, this is subject to an important symmetry constraint: &#039;&#039;the centre of the map must correspond to the centre of tile (0, 0)&#039;&#039;. Failure to meet this requirement currently makes it impossible to correctly interpret mouse clicks/finger taps on the map. Since this is not trivial to resolve and the work-around is inconvenient but not prohibitively complicated, generating non-hexagonal maps most likely will not be supported from within the app.&lt;br /&gt;
&lt;br /&gt;
Allowing the map to be &#039;&#039;&#039;extended&#039;&#039;&#039; or &#039;&#039;&#039;shrunk&#039;&#039;&#039; without discarding it completely is relatively simple and will most likely be included in the next non-bugfix update.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Automatic generation&#039;&#039;&#039; of a map outline is the final complex feature currently planned. This would most likely also incorporate the placement of &#039;&#039;&#039;harbours&#039;&#039;&#039;, which are not currently included.&lt;br /&gt;
&lt;br /&gt;
==Bugs and feedback==&lt;br /&gt;
{{bugs and feedback}}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[Category:Software]]&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=Template:Bugs_and_feedback&amp;diff=245</id>
		<title>Template:Bugs and feedback</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=Template:Bugs_and_feedback&amp;diff=245"/>
		<updated>2022-08-06T11:13:36Z</updated>

		<summary type="html">&lt;p&gt;Link: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Report bugs via email at &amp;lt;tt&amp;gt;bugs[a&amp;lt;!-- --&amp;gt;t]proj[dot]penguindevelopment[dot]org&amp;lt;/tt&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Send your feedback to &amp;lt;tt&amp;gt;feedback[a&amp;lt;!-- --&amp;gt;t]proj[dot]penguindevelopment[dot]org&amp;lt;/tt&amp;gt;.&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=File:Catan-gen-toolbar.png&amp;diff=244</id>
		<title>File:Catan-gen-toolbar.png</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=File:Catan-gen-toolbar.png&amp;diff=244"/>
		<updated>2022-08-06T09:36:15Z</updated>

		<summary type="html">&lt;p&gt;Link: Toolbar in catan-gen.&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Summary ==&lt;br /&gt;
Toolbar in catan-gen.&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=File:Catan-gen-unspecified-paint.png&amp;diff=243</id>
		<title>File:Catan-gen-unspecified-paint.png</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=File:Catan-gen-unspecified-paint.png&amp;diff=243"/>
		<updated>2022-08-06T09:08:57Z</updated>

		<summary type="html">&lt;p&gt;Link: Map with unspecified tiles in catan-gen.&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Summary ==&lt;br /&gt;
Map with unspecified tiles in catan-gen.&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=File:Catan-gen-tileset.png&amp;diff=242</id>
		<title>File:Catan-gen-tileset.png</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=File:Catan-gen-tileset.png&amp;diff=242"/>
		<updated>2022-08-06T09:08:21Z</updated>

		<summary type="html">&lt;p&gt;Link: Tileset page in catan-gen.&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Summary ==&lt;br /&gt;
Tileset page in catan-gen.&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=File:Catan-gen-tile-overlay.png&amp;diff=241</id>
		<title>File:Catan-gen-tile-overlay.png</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=File:Catan-gen-tile-overlay.png&amp;diff=241"/>
		<updated>2022-08-06T09:07:46Z</updated>

		<summary type="html">&lt;p&gt;Link: Active tile overlay in catan-gen.&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Summary ==&lt;br /&gt;
Active tile overlay in catan-gen.&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=File:Catan-gen-fill-tiles.png&amp;diff=240</id>
		<title>File:Catan-gen-fill-tiles.png</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=File:Catan-gen-fill-tiles.png&amp;diff=240"/>
		<updated>2022-08-06T09:06:16Z</updated>

		<summary type="html">&lt;p&gt;Link: Map with unnumbered resource tiles in catan-gen.&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Summary ==&lt;br /&gt;
Map with unnumbered resource tiles in catan-gen.&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=File:Catan-gen-fill-numbers.png&amp;diff=239</id>
		<title>File:Catan-gen-fill-numbers.png</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=File:Catan-gen-fill-numbers.png&amp;diff=239"/>
		<updated>2022-08-06T09:05:41Z</updated>

		<summary type="html">&lt;p&gt;Link: Map filled with numbered resources in catan-gen.&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Summary ==&lt;br /&gt;
Map filled with numbered resources in catan-gen.&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=File:Catan-gen-blank-map.png&amp;diff=238</id>
		<title>File:Catan-gen-blank-map.png</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=File:Catan-gen-blank-map.png&amp;diff=238"/>
		<updated>2022-08-06T09:04:30Z</updated>

		<summary type="html">&lt;p&gt;Link: Blank map in catan-gen.&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Summary ==&lt;br /&gt;
Blank map in catan-gen.&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=File:Catan-gen-ui-firstopen.png&amp;diff=237</id>
		<title>File:Catan-gen-ui-firstopen.png</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=File:Catan-gen-ui-firstopen.png&amp;diff=237"/>
		<updated>2022-08-06T09:03:56Z</updated>

		<summary type="html">&lt;p&gt;Link: UI when opening catan-gen.&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Summary ==&lt;br /&gt;
UI when opening catan-gen.&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=Matrix_Synapse_and_mautrix-whatsapp_in_a_VPN&amp;diff=236</id>
		<title>Matrix Synapse and mautrix-whatsapp in a VPN</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=Matrix_Synapse_and_mautrix-whatsapp_in_a_VPN&amp;diff=236"/>
		<updated>2021-07-26T14:35:41Z</updated>

		<summary type="html">&lt;p&gt;Link: /* Step 4: migrate WhatsApp proper to a VM */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;WhatsApp&#039;&#039;&#039; is a popular communication app for Android and iOS. It is owned by Facebook, which has a hostile attitude towards alternative clients and open source software, along with [https://stallman.org/facebook.html being notorious for its egregious privacy violations (and de-facto support of white supremacy and fascism)]. It stands to reason, then, that WhatsApp is not an app that any privacy-conscious individual should want to have installed on their mobile phone, which typically holds a vast quantity of personal data. However, the unfortunate truth is that it tends to be virtually impossible for many of us to stop using WhatsApp without permanently losing contact with several friends, family members, clients or colleagues. Thus, one may seek to find a middle ground: keeping WhatsApp well away from one&#039;s personal devices, but somehow still connecting to its network using these devices. Given Facebook&#039;s hostile attitude to third-party clients, one might expect such a task to be impossible... or is it?&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Enter the Matrix.&#039;&#039;&#039; [https://matrix.org/ Matrix] is a free (as in speech), feature-rich decentralised communication ecosystem that can serve as a platform for instant messaging, e.g. using [https://www.element.io/ Element] as a client. Although I would highly encourage switching from WhatsApp to Element whenever possible, it turns out Matrix is in fact capable of communicating with the WhatsApp network. This is done using [https://github.com/tulir/mautrix-whatsapp mautrix-whatsapp], which masquerades as a WhatsApp Web client and uses it to communicate with the actual WhatsApp client running on a phone or emulator.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;A note:&#039;&#039;&#039; this guide is essentially an amalgamation of the most relevant bits of various other guides: [https://wiki.debian.org/OpenVPN the Debian OpenVPN guide], [https://openvpn.net/community-resources/how-to/#installing-openvpn the OpenVPN community installation guide], [https://www.natrius.eu/dokuwiki/doku.php?id=digital:server:matrixsynapse the Synapse installation guide by &#039;&#039;&#039;natrius&#039;&#039;&#039;], the [https://gist.github.com/marcopaganini/0823d31d43557f9711e21b43a3223fce Nginx/TLS reverse proxy guide by &#039;&#039;&#039;Marco Paganini&#039;&#039;&#039;], [http://blog.hoxnox.com/inet/ssl_nginx.md.html the Nginx/easy-rsa guide by &#039;&#039;&#039;hoxnox&#039;&#039;&#039;], and the [https://github.com/tulir/mautrix-whatsapp/wiki/Bridge-setup official mautrix-whatsapp bridge setup guide by &#039;&#039;&#039;Tulir Asokan&#039;&#039;&#039;]. These individual guides explain their steps in greater depth than I do here, and I highly, highly recommend reading through all of them before tackling a project like this one. &#039;&#039;&#039;Also be very aware that I am not an expert in any of this. If you need enterprise-grade security, close this browser tab now and consult an actual expert.&#039;&#039;&#039; That said, I have made an honest attempt to hammer down any security holes I could think of, and I am presently running this set-up myself.&lt;br /&gt;
&lt;br /&gt;
=The set-up=&lt;br /&gt;
[[File:Mautrix-whatsapp-setup-pathified.svg]]&lt;br /&gt;
&lt;br /&gt;
This guide will explain one possible configuration to use WhatsApp from a Matrix client. In this configuration, a Matrix homeserver (Synapse) and mautrix-whatsapp will be installed on a (virtual) private server, along with OpenVPN. The setup is completely contained within the virtual private network (VPN) created by OpenVPN, and does not require any entrypoints from the outside world except through the VPN (although you may want to enable SSH access for setup and administration). It therefore has a high degree of inherent security. A reverse HTTPS proxy is set up using Nginx with certificates generated by easy-rsa: this is necessary for some clients to be able to connect.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;iptables&amp;lt;/code&amp;gt; is used for setting up routing and firewalling of the server, and only IPv4 is considered for the time being. I wish to &#039;&#039;eventually&#039;&#039; migrate to &amp;lt;code&amp;gt;nftables&amp;lt;/code&amp;gt; and a dual IPv4/IPv6 stack, however this will take more time and effort than I have available, especially considering the current setup &amp;quot;just works&amp;quot; for me.&lt;br /&gt;
&lt;br /&gt;
This guide is primarily focussed on a &amp;quot;single owner, multiple devices&amp;quot; configuration: extra security precautions should be taken if one wants to allow multiple individuals in the VPN, and using multiple WhatsApp accounts in particular is beyond the scope of this guide.&lt;br /&gt;
&lt;br /&gt;
N.B. it is up to you to decide where WhatsApp (the actual proprietary app) goes in the graphic above: as shown, it is put on the VPS (in an emulator or VM), but it is also possible to leave it off the VPS, e.g. running on a normal phone. Off the VPS, you additionally have the choice of whether you want to route its traffic through the VPN (recommended) or not.&lt;br /&gt;
&lt;br /&gt;
==Requirements==&lt;br /&gt;
* A WhatsApp account, and the WhatsApp app running either on a phone or in an emulator on a device with a webcam&lt;br /&gt;
* A (virtual) private server ((V)PS) with the following specs:&lt;br /&gt;
** A static IPv4 address&lt;br /&gt;
** Linux, ideally Debian 10&lt;br /&gt;
** root/&amp;lt;code&amp;gt;sudo&amp;lt;/code&amp;gt; access&lt;br /&gt;
** At least 1 GB RAM&lt;br /&gt;
** At least 5 GB disk space for the software and text message storage&lt;br /&gt;
** More disk space for the media (videos, etc.) you send and receive&lt;br /&gt;
* Decent knowledge of Linux administration&lt;br /&gt;
* A free weekend or so to set up, test and tweak everything&lt;br /&gt;
&lt;br /&gt;
The environment used in this guide is a VPS running Debian 10 with full root access; all commands are run as root unless otherwise stated. It is assumed that the VPS is in a fresh-off-the-shelf state with little more than &amp;lt;code&amp;gt;sshd&amp;lt;/code&amp;gt; installed and running.&lt;br /&gt;
&lt;br /&gt;
=Step 0: iptables and OpenVPN=&lt;br /&gt;
The first and foremost things to get working are the firewall and VPN server. &#039;&#039;Henceforth, we shall use &#039;&#039;&#039;192.168.16.0/24&#039;&#039;&#039; as the OpenVPN address space, and &#039;&#039;&#039;port 1194&#039;&#039;&#039; (UDP or TCP) as the OpenVPN port. Take extra care if you want to use something else. We shall use &#039;&#039;&#039;123.123.123.123&#039;&#039;&#039; as the externally reachable IP of the server; always replace this by whatever the actual address is.&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Log in to your VPS and install iptables as follows:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
apt install iptables iptables-persistent&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Let us begin by setting up some basic security:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
iptables -A INPUT -i lo -j ACCEPT&lt;br /&gt;
iptables -A INPUT -s 127.0.0.1/32 -j ACCEPT&lt;br /&gt;
iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT&lt;br /&gt;
iptables -A INPUT -p icmp -m icmp --icmp-type 8 -j ACCEPT&lt;br /&gt;
iptables -A INPUT -p icmp -m icmp --icmp-type 11 -j ACCEPT&lt;br /&gt;
iptables -A INPUT -p icmp -m icmp --icmp-type 3 -j ACCEPT&lt;br /&gt;
iptables -A INPUT -p tcp -m tcp --dport 22 -j ACCEPT&lt;br /&gt;
iptables -A INPUT -j DROP&lt;br /&gt;
iptables-save &amp;gt;/etc/iptables/rules.v4&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This will drop all incoming connections apart from SSH and a few ICMP messages. The last line preserves your iptables rules when rebooting.&lt;br /&gt;
&lt;br /&gt;
The next step is to install OpenVPN. The instructions provided here mostly follow [https://wiki.debian.org/OpenVPN Debian&#039;s OpenVPN guide]; see that page for more in-depth information. Start by installing the required packages:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
apt install easy-rsa openvpn&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Next, we create a certificate authority directory in &amp;lt;code&amp;gt;/etc/openvpn&amp;lt;/code&amp;gt; and edit the config:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
make-cadir /etc/openvpn/easy-rsa&lt;br /&gt;
cd /etc/openvpn/easy-rsa&lt;br /&gt;
editor vars&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;vars&amp;lt;/code&amp;gt; file has sensible defaults, but you may want to make a few changes (see the inline documentation if you are unsure):&lt;br /&gt;
# uncomment the line with &amp;lt;code&amp;gt;set_var EASYRSA_DN    &amp;quot;cn_only&amp;quot;&amp;lt;/code&amp;gt;&lt;br /&gt;
# set the key size to 4096: &amp;lt;code&amp;gt;set_var EASYRSA_KEY_SIZE      4096&amp;lt;/code&amp;gt;&lt;br /&gt;
# set the CA validity to something long, like 10 years: &amp;lt;code&amp;gt;set_var EASYRSA_CA_EXPIRE     3650&amp;lt;/code&amp;gt;&lt;br /&gt;
# set the certificate validity to something similar: &amp;lt;code&amp;gt;set_var EASYRSA_CERT_EXPIRE   3650&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
After writing the vars file, create the certificate authority, a server certificate and Diffie-Hellman parameters:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
./easyrsa init-pki&lt;br /&gt;
./easyrsa build-ca&lt;br /&gt;
./easyrsa build-server-full server nopass&lt;br /&gt;
./easyrsa gen-dh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
The &amp;lt;code&amp;gt;build-ca&amp;lt;/code&amp;gt; step will prompt you for a name and password. The name can be whatever you like, e.g. &amp;quot;My OpenVPN CA&amp;quot;. Choose something strong but memorable as the password. Every further interaction with the certificate authority will require this password.&lt;br /&gt;
&lt;br /&gt;
Now create a certificate for every client (desktop, laptop, tablet, mobile phone, refrigerator...) you want to have access to the VPN/your WhatsApp account. It&#039;s a good idea to use a file name that allows you to tell these certificates apart, e.g. &amp;lt;code&amp;gt;hostname.n&amp;lt;/code&amp;gt;, where &amp;lt;code&amp;gt;hostname&amp;lt;/code&amp;gt; is the hostname of the device and &amp;lt;code&amp;gt;n&amp;lt;/code&amp;gt; is a number indicating this is the n&#039;th certificate issued for that hostname:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
./easyrsa build-client-full alpha.0 nopass&lt;br /&gt;
./easyrsa build-client-full bravo.0 nopass&lt;br /&gt;
# And so on...&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Finally, per the [https://openvpn.net/community-resources/how-to/#installing-openvpn OpenVPN community installation guide], generate a shared secret TLS key:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
cd /etc/openvpn/easy-rsa/pki/private&lt;br /&gt;
openvpn --genkey --secret ta.key&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Now edit the OpenVPN configuration file:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
cd /etc/openvpn&lt;br /&gt;
editor server.conf&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A viable example configuration file follows. See &amp;lt;code&amp;gt;openvpn(8)&amp;lt;/code&amp;gt; if it is unclear what an option does. Be sure to save this file as /etc/openvpn/server.conf.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;cfg&amp;quot; line&amp;gt;&lt;br /&gt;
mode server&lt;br /&gt;
# Enable TLS encryption.&lt;br /&gt;
tls-server&lt;br /&gt;
# Listen on port 1194 (the default).&lt;br /&gt;
port 1194&lt;br /&gt;
# Set the protocol here. UDP is the default, but it may give you trouble&lt;br /&gt;
# connecting on low-quality public Wi-Fi. You can use `proto tcp-server&#039;&lt;br /&gt;
# here instead, but note that this causes extra overhead.&lt;br /&gt;
# Choose `proto tcp-server&#039; if you want to redirect port 443 to OpenVPN&lt;br /&gt;
# to allow connecting over some poorly configured public WiFi networks.&lt;br /&gt;
proto udp&lt;br /&gt;
&lt;br /&gt;
# Use TUN device, as opposed to TAP. In 99% of cases, TUN is what you want.&lt;br /&gt;
# TAP configuration is outside the scope of this article.&lt;br /&gt;
dev tun&lt;br /&gt;
# IP pool to be used for OpenVPN. The parameters as given will put the server&lt;br /&gt;
# at 192.168.16.1 and clients at addresses up to 192.168.16.255.&lt;br /&gt;
server 192.168.16.0 255.255.255.0&lt;br /&gt;
&lt;br /&gt;
# ca.crt file of the certificate authority we just set up.&lt;br /&gt;
ca /etc/openvpn/easy-rsa/pki/ca.crt&lt;br /&gt;
# Server certificate.&lt;br /&gt;
cert /etc/openvpn/easy-rsa/pki/issued/server.crt&lt;br /&gt;
# Server private key.&lt;br /&gt;
key /etc/openvpn/easy-rsa/pki/private/server.key&lt;br /&gt;
# Diffie-Hellman parameters.&lt;br /&gt;
dh /etc/openvpn/easy-rsa/pki/dh.pem&lt;br /&gt;
# TLS key.&lt;br /&gt;
tls-auth /etc/openvpn/easy-rsa/pki/private/ta.key 0&lt;br /&gt;
&lt;br /&gt;
# Timeout parameters for sending pings/restarting OpenVPN. See openvpn(8).&lt;br /&gt;
keepalive 20 120&lt;br /&gt;
# Enable the following if you want clients to be able to address&lt;br /&gt;
# one another directly.&lt;br /&gt;
#client-to-client&lt;br /&gt;
&lt;br /&gt;
# Compress the stream with LZO to limit bandwidth.&lt;br /&gt;
comp-lzo&lt;br /&gt;
# Allow at most 10 clients to connect at the same time.&lt;br /&gt;
# Increase if necessary, but make sure your network and server can handle&lt;br /&gt;
# the load.&lt;br /&gt;
max-clients 10&lt;br /&gt;
&lt;br /&gt;
# Drop root privileges.&lt;br /&gt;
user nobody&lt;br /&gt;
group nogroup&lt;br /&gt;
&lt;br /&gt;
# Persist keys and TUN device across restarts, since we are dropping root.&lt;br /&gt;
persist-key&lt;br /&gt;
persist-tun&lt;br /&gt;
&lt;br /&gt;
# Status file.&lt;br /&gt;
status /var/log/openvpn-status.log&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Next, configure the firewall to allow OpenVPN clients. The first step depends on the protocol you chose set in the OpenVPN server configuration. If you chose UDP:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
iptables -I INPUT 1 -p udp -m udp --dport 1194 -j ACCEPT&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
OR, if you chose TCP:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
iptables -I INPUT 1 -p tcp -m tcp --dport 1194 -j ACCEPT&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Next, set up forwarding and masquerading, and start the server! N.B. it is assumed here that the ethernet device of your server is eth0, and that OpenVPN creates the device &amp;lt;code&amp;gt;tun0&amp;lt;/code&amp;gt; (i.e. nothing else sets up a TUN device before OpenVPN). Change tun0 and/or eth0 as necessary if this is not true.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
iptables -A FORWARD -i tun0 -j ACCEPT&lt;br /&gt;
iptables -A FORWARD -i tun0 -o eth0 -m state --state RELATED,ESTABLISHED -j ACCEPT&lt;br /&gt;
iptables -A FORWARD -i eth0 -o tun0 -m state --state RELATED,ESTABLISHED -j ACCEPT&lt;br /&gt;
iptables -A POSTROUTING -s 192.168.16.0/24 -o eth0 -j MASQUERADE&lt;br /&gt;
iptables-save &amp;gt;/etc/iptables/rules.v4&lt;br /&gt;
echo &amp;quot;net.ipv4.ip_forward = 1&amp;quot; &amp;gt;&amp;gt;/etc/sysctl.conf&lt;br /&gt;
sysctl -p&lt;br /&gt;
systemctl enable openvpn.service&lt;br /&gt;
systemctl restart openvpn.service&lt;br /&gt;
ifconfig&lt;br /&gt;
ss -plunt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Make sure OpenVPN starts correctly: &amp;lt;code&amp;gt;ifconfig&amp;lt;/code&amp;gt; should show &amp;lt;code&amp;gt;tun0&amp;lt;/code&amp;gt;, along with something like &amp;lt;code&amp;gt;inet 192.168.16.1  netmask 255.255.255.255  destination 192.168.16.2&amp;lt;/code&amp;gt;. &amp;lt;code&amp;gt;ss -plunt&amp;lt;/code&amp;gt; should show &amp;lt;code&amp;gt;0.0.0.0:1194&amp;lt;/code&amp;gt; somewhere in the column labelled &amp;quot;Local Address:Port&amp;quot;. If this is not the case, check &amp;lt;code&amp;gt;/var/log/daemon.log&amp;lt;/code&amp;gt; for entries containing &amp;lt;code&amp;gt;ovpn-server&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
You must now copy some certificate files to your clients. As an example, if you have a Linux client named &amp;quot;alpha&amp;quot; and can access your server as root via SSH on 123.123.123.123, run the following commands as root on &amp;quot;alpha&amp;quot; (assuming OpenVPN is already installed):&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
cd /etc/openvpn&lt;br /&gt;
scp 123.123.123.123:/etc/openvpn/easy-rsa/pki/&#039;{ca.crt,issued/alpha.0.crt,private/alpha.0.key,private/ta.key}&#039; .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
An example of a client configuration file (save as &amp;lt;code&amp;gt;/etc/openvpn/client.conf&amp;lt;/code&amp;gt;) is listed below.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;cfg&amp;quot; line&amp;gt;&lt;br /&gt;
client&lt;br /&gt;
dev tun&lt;br /&gt;
# Use `proto udp&#039; if that&#039;s what the server uses.&lt;br /&gt;
# Else use `proto tcp-client&#039;.&lt;br /&gt;
proto udp&lt;br /&gt;
remote 123.123.123.123 1194&lt;br /&gt;
resolv-retry infinite&lt;br /&gt;
nobind&lt;br /&gt;
&lt;br /&gt;
user nobody&lt;br /&gt;
group nobody&lt;br /&gt;
persist-key&lt;br /&gt;
persist-tun&lt;br /&gt;
&lt;br /&gt;
ca /etc/openvpn/ca.crt&lt;br /&gt;
cert /etc/openvpn/alpha.0.crt&lt;br /&gt;
key /etc/openvpn/alpha.0.key&lt;br /&gt;
&lt;br /&gt;
ns-cert-type server&lt;br /&gt;
&lt;br /&gt;
tls-auth /etc/openvpn/ta.key 1&lt;br /&gt;
&lt;br /&gt;
comp-lzo&lt;br /&gt;
&lt;br /&gt;
# Uncomment the following if you want to redirect all client traffic through&lt;br /&gt;
# the VPN. (Without it, only 192.168.16.0/24 will be routed through the VPN.)&lt;br /&gt;
#redirect-gateway def1&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
On &amp;quot;alpha&amp;quot;, run &amp;lt;code&amp;gt;openvpn --config /etc/openvpn/client.conf&amp;lt;/code&amp;gt; and check if it will connect. Running &amp;lt;code&amp;gt;ifconfig&amp;lt;/code&amp;gt; on &amp;quot;alpha&amp;quot; should show a &amp;lt;code&amp;gt;tun0&amp;lt;/code&amp;gt; with an IP in 129.168.16.0/24 and a destination of 192.168.16.1. You should be able to SSH over the VPN too, using&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
ssh root@192.168.16.1&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
If this connects, your VPN works and you&#039;re done with this step!&lt;br /&gt;
&lt;br /&gt;
=Step 1: PostgreSQL and Synapse=&lt;br /&gt;
Synapse is the name of the Matrix homeserver reference implementation, which we will be installing in this section. This procedure loosely follows [https://www.natrius.eu/dokuwiki/doku.php?id=digital:server:matrixsynapse the guide by &#039;&#039;&#039;natrius&#039;&#039;&#039;], with some key differences:&lt;br /&gt;
# We will thoroughly disable federation.&lt;br /&gt;
# We will use easy-rsa to add self-signed certificates to the Nginx reverse proxy.&lt;br /&gt;
# We will use &amp;lt;code&amp;gt;iptables&amp;lt;/code&amp;gt; instead of &amp;lt;code&amp;gt;ufw&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Before installing Synapse, first install PostgreSQL and its Python binding:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
apt install postgresql python3-psycopg2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Now &amp;lt;code&amp;gt;su&amp;lt;/code&amp;gt; to the PostgreSQL admin account and start the PostgreSQL shell:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
su - postgres&lt;br /&gt;
psql&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
As in natrius&#039; guide, create the synapse database and a user to own it:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot; line&amp;gt;&lt;br /&gt;
CREATE USER &amp;quot;synapse&amp;quot; WITH PASSWORD &#039;password&#039;;&lt;br /&gt;
CREATE DATABASE synapse ENCODING &#039;UTF8&#039; LC_COLLATE=&#039;C&#039; LC_CTYPE=&#039;C&#039; template=template0 OWNER &amp;quot;synapse&amp;quot;;&lt;br /&gt;
\q&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Replace &#039;password&#039; with a strong, ideally random password. That&#039;s all that&#039;s needed as far as PostgreSQL goes, so log out of the &amp;lt;code&amp;gt;postgres&amp;lt;/code&amp;gt; account.&lt;br /&gt;
&lt;br /&gt;
As in the aforementioned guide, first add the relevant repositories and perform any necessary updates, and then install Synapse:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
apt install lsb-release wget apt-transport-https&lt;br /&gt;
wget -O /usr/share/keyrings/matrix-org-archive-keyring.gpg https://packages.matrix.org/debian/matrix-org-archive-keyring.gpg&lt;br /&gt;
echo &amp;quot;deb [signed-by=/usr/share/keyrings/matrix-org-archive-keyring.gpg] https://packages.matrix.org/debian/ $(lsb_release -cs) main&amp;quot; | tee /etc/apt/sources.list.d/matrix-org.list&lt;br /&gt;
apt update &amp;amp;&amp;amp; apt upgrade&lt;br /&gt;
apt install matrix-synapse-py3&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
When asked for a hostname, enter &amp;lt;code&amp;gt;localhost&amp;lt;/code&amp;gt;. Next, edit the configuration file, &amp;lt;code&amp;gt;/etc/matrix-synapse/homeserver.yaml&amp;lt;/code&amp;gt;. In the &amp;lt;code&amp;gt;listeners&amp;lt;/code&amp;gt; section, change the uncommented lines to the following:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;yaml&amp;quot; line&amp;gt;&lt;br /&gt;
  - port: 8008&lt;br /&gt;
    tls: false&lt;br /&gt;
    type: http&lt;br /&gt;
    x_forwarded: true&lt;br /&gt;
    bind_addresses: [&#039;0.0.0.0&#039;]&lt;br /&gt;
&lt;br /&gt;
    resources:&lt;br /&gt;
      - names: [client]&lt;br /&gt;
        compress: false&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
That is, we bind 0.0.0.0 (to listen to the whole network for client connections) and remove federation.&lt;br /&gt;
&lt;br /&gt;
We will be even more thorough in disabling federation: search for &amp;lt;code&amp;gt;federation_domain_whitelist&amp;lt;/code&amp;gt;, which (for the Debian 10 configuration file of Synapse 1.23) should be a commented line with a few domains under it. Add a new line after that comment section:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;yaml&amp;quot;&amp;gt;&lt;br /&gt;
federation_domain_whitelist: []&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Next scroll down to &amp;lt;code&amp;gt;federation_ip_range_blacklist&amp;lt;/code&amp;gt; and remove the IPs underneath that line. Replace them with the IPv4 and IPv6 catchalls:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;yaml&amp;quot; line&amp;gt;&lt;br /&gt;
  - &#039;0.0.0.0/0&#039;&lt;br /&gt;
  - &#039;::/0&#039;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
With federation now thoroughly disabled, look for &amp;lt;code&amp;gt;enable_registration: false&amp;lt;/code&amp;gt; and uncomment this line. As in natrius&#039; guide, look for &amp;lt;code&amp;gt;registration_shared_secret: &amp;lt;PRIVATE STRING&amp;gt;&amp;lt;/code&amp;gt;, uncomment it, and replace &amp;lt;code&amp;gt;&amp;lt;PRIVATE STRING&amp;gt;&amp;lt;/code&amp;gt; with a long, randomly generated alphanumeric string, e.g. generated by the command&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
cat /dev/urandom | tr -dc &#039;a-zA-Z0-9&#039; | fold -w 32 | head -n 1&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Write out the configuration file and exit your editor. We now need to open port 8008 to VPN clients:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
iptables -I INPUT 1 -s 192.168.16.0/24 -i tun0 -p tcp -m tcp --dport 8008 -j ACCEPT&lt;br /&gt;
iptables-save &amp;gt;/etc/iptables/rules.v4&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Now start Synapse and check if it is running:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
systemctl start matrix-synapse.service&lt;br /&gt;
ss -plunt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The last command should show that Synapse is listening on port 8008.&lt;br /&gt;
&lt;br /&gt;
You can now create an account on your homeserver:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
register_new_matrix_user -c /etc/matrix-synapse/homeserver.yaml http://localhost:8008&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
This will prompt for a username and password, as well as the shared secret you entered into the configuration file earlier.&lt;br /&gt;
&lt;br /&gt;
If you have a PC or laptop that you&#039;ve given access to the VPN, you can install the &#039;&#039;&#039;desktop&#039;&#039;&#039; [https://element.io/get-started Element] client on it. You should then be able to connect to &amp;lt;code&amp;gt;http://192.168.16.1:8008&amp;lt;/code&amp;gt; using the account you just created. At this moment, the Android and Web clients do NOT support HTTP connections, so these clients cannot connect to your Synapse installation yet. Which brings us to...&lt;br /&gt;
&lt;br /&gt;
=Step 2: Nginx and TLS=&lt;br /&gt;
To allow most clients to connect to your homeserver (provided they have access to the VPN), you will need to set up a reverse proxy that adds a TLS layer so they can communicate over HTTPS. While the Synapse guide by natrius recommends Let&#039;s Encrypt certificates, in a fully private context like we are considering, it makes sense to set up your own certificate authority.&lt;br /&gt;
&lt;br /&gt;
I opted to build a new certificate authority in the Synapse configuration directory and reuse the configuration I picked for OpenVPN:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
make-cadir /etc/matrix-synapse/easy-rsa&lt;br /&gt;
cd /etc/matrix-synapse/easy-rsa&lt;br /&gt;
cp /etc/openvpn/easy-rsa/vars .&lt;br /&gt;
./easyrsa init-pki&lt;br /&gt;
./easyrsa --subject-alt-name=&amp;quot;IP:192.168.16.1&amp;quot; build-ca&lt;br /&gt;
# Choose a new CA name when prompted. The password you are prompted for&lt;br /&gt;
# should ideally also be different from the one you used for OpenVPN.&lt;br /&gt;
./easyrsa --subject-alt-name=&amp;quot;IP:192.168.16.1&amp;quot; build-server-full matrix-synapse nopass&lt;br /&gt;
# For all clients:&lt;br /&gt;
./easyrsa --subject-alt-name=&amp;quot;IP:192.168.16.1&amp;quot; build-client-full alpha.0 nopass&lt;br /&gt;
./easyrsa --subject-alt-name=&amp;quot;IP:192.168.16.1&amp;quot; build-client-full bravo.0 nopass&lt;br /&gt;
# ...and so on&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Note that we are now including &amp;lt;code&amp;gt;--subject-alt-name=&amp;quot;IP:192.168.16.1&amp;quot;&amp;lt;/code&amp;gt; in our commands. HTTPS requires certificates to be bound to a domain or IP, and browsers tend to get ornery if they are not. We do not need Diffie-Hellman parameters now, but we do need to generate a certificate revocation list so Nginx will be able to reject revoked certificates, and we must also export our client certificates in a format browsers and mobile OSes understand:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; list&amp;gt;&lt;br /&gt;
# For all clients:&lt;br /&gt;
./easyrsa export-p12 alpha.0&lt;br /&gt;
./easyrsa export-p12 bravo.0&lt;br /&gt;
# ...and so on&lt;br /&gt;
./easyrsa gen-crl&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
The export step asks you to provide a password, which allows you to securely transport the exported certificates to the clients they are to be installed on.&lt;br /&gt;
&lt;br /&gt;
Next up, Nginx. Install the package, remove the default configuration, and create a new one:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
apt install nginx&lt;br /&gt;
systemctl stop nginx.service&lt;br /&gt;
unlink /etc/nginx/sites-enabled/default&lt;br /&gt;
ln -s /etc/nginx/sites-available/synapse /etc/nginx/sites-enabled/synapse&lt;br /&gt;
editor /etc/nginx/sites-available/synapse&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
An example configuration that sets up an HTTPS reverse proxy on port 10443 is&lt;br /&gt;
given below.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;nginx&amp;quot; line&amp;gt;&lt;br /&gt;
server {&lt;br /&gt;
    listen 10443 ssl;&lt;br /&gt;
    server_name localhost;&lt;br /&gt;
&lt;br /&gt;
    ssl on;&lt;br /&gt;
    ssl_certificate /etc/matrix-synapse/easy-rsa/pki/issued/matrix-synapse.crt;&lt;br /&gt;
    ssl_certificate_key /etc/matrix-synapse/easy-rsa/pki/private/matrix-synapse.key;&lt;br /&gt;
    ssl_client_certificate /etc/matrix-synapse/easy-rsa/pki/ca.crt;&lt;br /&gt;
    ssl_crl /etc/matrix-synapse/easy-rsa/pki/crl.pem;&lt;br /&gt;
    # Enable mutual TLS. This option provides maximal security by requiring&lt;br /&gt;
    # per-client certificates. Unfortunately, client-side support for this&lt;br /&gt;
    # tends to be shaky at best, so turn it off and restart Nginx if you are&lt;br /&gt;
    # not able to connect.&lt;br /&gt;
    ssl_verify_client on;&lt;br /&gt;
&lt;br /&gt;
    access_log /var/log/nginx-synapse-access_log.log;&lt;br /&gt;
    error_log /var/log/nginx-synapse-error_log.log;&lt;br /&gt;
&lt;br /&gt;
    # There is no need to serve files, and we will not be using&lt;br /&gt;
    # .well-known, since we don&#039;t use federation. So we just reverse-proxy&lt;br /&gt;
    # Synapse&#039;s HTTP at the root and that&#039;s it.&lt;br /&gt;
    location / {&lt;br /&gt;
        proxy_pass http://127.0.0.1:8008;&lt;br /&gt;
        proxy_set_header X-Forwarded-For $remote_addr;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Now unblock port 10443 for the VPN and start Nginx:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
iptables -I INPUT 1 -s 192.168.16.0/24 -i tun0 -p tcp -m tcp --dport 10443 -j ACCEPT&lt;br /&gt;
iptables-save &amp;gt;/etc/iptables/rules.v4&lt;br /&gt;
systemctl enable nginx.service&lt;br /&gt;
systemctl start nginx.service&lt;br /&gt;
# Check if it&#039;s running&lt;br /&gt;
ss -plunt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The last command should show Nginx listening on port 10443. Now copy &amp;lt;code&amp;gt;/etc/matrix-synapse/easy-rsa/pki/ca.crt&amp;lt;/code&amp;gt; to all clients and import it as a trusted system certificate. Also copy each &amp;lt;code&amp;gt;p12&amp;lt;/code&amp;gt; certificate in &amp;lt;code&amp;gt;/etc/matrix-synapse/easy-rsa/pki/private/&amp;lt;/code&amp;gt; to its respective client and import it.&lt;br /&gt;
&lt;br /&gt;
The desktop and android apps should now be able to at least communicate with the server via HTTPS at &amp;lt;code&amp;gt;https://192.168.16.1:10443&amp;lt;/code&amp;gt;. If, however, you get an error stating the client did not send the correct certificate, you may have to disable &amp;lt;code&amp;gt;ssl_verify_client&amp;lt;/code&amp;gt; in the Nginx configuration file and restart Nginx. Note that the canonical Element-web implementation at app.element.io may not be able to talk to your server regardless; I believe this is because browsers will restrict accessing local networks from sites loaded from remote servers. Installing element-web locally will help; see Optional Extras at the end of this guide.&lt;br /&gt;
&lt;br /&gt;
=Step 3: mautrix-whatsapp=&lt;br /&gt;
Being a rather niche package, mautrix-whatsapp is not in the Debian repositories and must be self-compiled. It also has some dependencies on versions of packages too recent to be found in the repositories of Debian 10 (buster)&lt;br /&gt;
&lt;br /&gt;
First and foremost, version 1.14 of Go (&amp;lt;code&amp;gt;golang-1.14&amp;lt;/code&amp;gt;) is needed at the time of writing, which has been backported to buster. If you are running buster, install Go as follows:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
echo &#039;deb http://deb.debian.org/debian buster-backports main&#039; &amp;gt;&amp;gt;/etc/apt/sources.list&lt;br /&gt;
apt update&lt;br /&gt;
apt install -t buster-backports golang&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Alternatively, if you are using Debian 11 (bullseye) or Unstable (sid), simply install golang directly:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
apt install golang&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Regardless of your Debian version, you will need to install &amp;lt;code&amp;gt;ffmpeg&amp;lt;/code&amp;gt;, a GNU toolchain, and &amp;lt;code&amp;gt;git&amp;lt;/code&amp;gt;, if they are not already installed:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
apt install ffmpeg make autoconf automake libtool gcc cmake git&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Next, we need to install a very recent version of &amp;lt;code&amp;gt;olm&amp;lt;/code&amp;gt;, which must be hand-compiled at the time of writing. It is probably best to perform the following steps as a regular user instead of root, but I couldn&#039;t be bothered creating a new user. Caveat emptor.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
cd&lt;br /&gt;
git clone https://gitlab.matrix.org/matrix-org/olm.git&lt;br /&gt;
cd olm&lt;br /&gt;
cmake . -Bbuild&lt;br /&gt;
cd build&lt;br /&gt;
make&lt;br /&gt;
# if you dropped root, use `sudo make install&#039; here instead&lt;br /&gt;
make install&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
We are now ready to compile mautrix-whatsapp. The following steps may also be executed as a regular user if you want. Clone the repo and compile:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
cd&lt;br /&gt;
git clone https://github.com/tulir/mautrix-whatsapp.git&lt;br /&gt;
cd mautrix-whatsapp&lt;br /&gt;
export LD_LIBRARY_PATH=/usr/local/lib&lt;br /&gt;
./build.sh&lt;br /&gt;
# Only execute the following if you executed the above as a regular user&lt;br /&gt;
cd ..&lt;br /&gt;
sudo cp -r mautrix-whatsapp /root/mautrix-whatsapp&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Again as root, copy &amp;lt;code&amp;gt;/root/mautrix-whatsapp/example-config.yaml&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;/root/mautrix-whatsapp/config.yaml&amp;lt;/code&amp;gt; and edit the latter.&lt;br /&gt;
* Under &amp;lt;code&amp;gt;homeserver&amp;lt;/code&amp;gt;, set &amp;lt;code&amp;gt;address&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;http://localhost:8008&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;domain&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;localhost&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Under &amp;lt;code&amp;gt;appservice&amp;lt;/code&amp;gt;, set &amp;lt;code&amp;gt;hostname&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;127.0.0.1&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Under &amp;lt;code&amp;gt;database&amp;lt;/code&amp;gt; (in the &amp;lt;code&amp;gt;appservice&amp;lt;/code&amp;gt; section), set &amp;lt;code&amp;gt;type&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;postgres&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;uri&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;postgres://synapse:&amp;lt;password&amp;gt;@localhost/synapse?sslmode=disable&amp;lt;/code&amp;gt;, where &amp;lt;code&amp;gt;&amp;lt;password&amp;gt;&amp;lt;/code&amp;gt; should be replaced with the PostgreSQL password you picked earlier.&lt;br /&gt;
* Under &amp;lt;code&amp;gt;bridge&amp;lt;/code&amp;gt;, set &amp;lt;code&amp;gt;private_chat_portal_meta&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;permissions&amp;lt;/code&amp;gt; block under &amp;lt;code&amp;gt;bridge&amp;lt;/code&amp;gt; should be something like this:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;yaml&amp;quot; line&amp;gt;&lt;br /&gt;
    permissions:&lt;br /&gt;
        &amp;quot;*&amp;quot;: relaybot&lt;br /&gt;
        &amp;quot;localhost&amp;quot;: user&lt;br /&gt;
        &amp;quot;@alice:localhost&amp;quot;: admin&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Here &amp;lt;code&amp;gt;alice&amp;lt;/code&amp;gt; should be replaced with the username you chose when issuing the &amp;lt;code&amp;gt;register_new_matrix_user&amp;lt;/code&amp;gt; command in Step 1.&lt;br /&gt;
&lt;br /&gt;
Finally, set &amp;lt;code&amp;gt;directory&amp;lt;/code&amp;gt; under &amp;lt;code&amp;gt;logging&amp;lt;/code&amp;gt; to something more suitable, like &amp;lt;code&amp;gt;/var/log/mautrix-whatsapp&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Now generate the appservice registration file, copy it to the Synapse configuration directory, and change its ownership so Synapse can read it:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
cd /root/mautrix-whatsapp&lt;br /&gt;
export LD_LIBRARY_PATH=/usr/local/lib&lt;br /&gt;
./mautrix-whatsapp -g&lt;br /&gt;
cp registration.yaml /etc/matrix-synapse/wa_registration.yaml&lt;br /&gt;
chown matrix-synapse /etc/matrix-synapse/wa_registration.yaml&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Edit &amp;lt;code&amp;gt;/etc/matrix-synapse/homeserver.yaml&amp;lt;/code&amp;gt;. Find the line with &amp;lt;code&amp;gt;app_service_config_files&amp;lt;/code&amp;gt; (which is commented out) and, below the comment, add&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;yaml&amp;quot; line&amp;gt;&lt;br /&gt;
app_service_config_files:&lt;br /&gt;
   - &amp;quot;/etc/matrix-synapse/wa_registration.yaml&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Now restart Synapse:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
systemctl restart matrix-synapse.service&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
It is now time to test mautrix-whatsapp. For testing purposes, start the bridge in the foreground:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
/root/mautrix-whatsapp/mautrix-whatsapp&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Connect to your homeserver with Element-Desktop or Element-Android and start a direct chat with &amp;lt;code&amp;gt;@whatsappbot:localhost&amp;lt;/code&amp;gt;. You should get a message saying the room has been set up as the bridge management/status room. Type &amp;lt;code&amp;gt;help&amp;lt;/code&amp;gt; and send the message. If the bot replies, you&#039;re good; if not, check the logs, and check/adjust your configuration carefully and restart Synapse and mautrix-whatsapp.&lt;br /&gt;
&lt;br /&gt;
If all is working, go back to the terminal where you started &amp;lt;code&amp;gt;/root/mautrix-whatsapp/mautrix-whatsapp&amp;lt;/code&amp;gt; and terminate the process with Ctrl-C. Now create a new file &amp;lt;code&amp;gt;/etc/systemd/system/mautrix-whatsapp.service&amp;lt;/code&amp;gt; and enter the following:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ini&amp;quot; line&amp;gt;&lt;br /&gt;
[Unit]&lt;br /&gt;
Description=WhatsApp to Matrix bridge&lt;br /&gt;
Wants=matrix-synapse.service&lt;br /&gt;
&lt;br /&gt;
[Service]&lt;br /&gt;
Type=exec&lt;br /&gt;
Environment=&amp;quot;LD_LIBRARY_PATH=/usr/local/lib&amp;quot;&lt;br /&gt;
WorkingDirectory=/root/mautrix-whatsapp&lt;br /&gt;
ExecStart=/root/mautrix-whatsapp/mautrix-whatsapp&lt;br /&gt;
Restart=always&lt;br /&gt;
RestartSec=10&lt;br /&gt;
SyslogIdentifier=mautrix-whatsapp&lt;br /&gt;
&lt;br /&gt;
[Install]&lt;br /&gt;
WantedBy=multi-user.target&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You should now be able to start mautrix-whatsapp using systemd:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
systemctl start mautrix-whatsapp.service&lt;br /&gt;
systemctl enable mautrix-whatsapp.service&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Wait a few seconds for mautrix-whatsapp to finish loading, then message &amp;lt;code&amp;gt;help&amp;lt;/code&amp;gt; to your bot again to make sure it still works. If it does, send it &amp;lt;code&amp;gt;login&amp;lt;/code&amp;gt;, and use the actual (official) WhatsApp client to scan the WhatsApp Web QR code that the bot sends you. It should immediately start pulling a few chats, but most likely not your entire history. Ignore it for now. Open your Element client&#039;s settings, go to Help &amp;amp; Advanced, and find your access token. Copy it. Then send the bridge bot &amp;lt;code&amp;gt;login-matrix &amp;lt;access_token&amp;gt;&amp;lt;/code&amp;gt;, replacing &amp;lt;code&amp;gt;&amp;lt;access_token&amp;gt;&amp;lt;/code&amp;gt; with the access token you just copied. This will enable double puppeting; see [https://github.com/tulir/mautrix-whatsapp/wiki/Authentication the Authentication guide].&lt;br /&gt;
&lt;br /&gt;
If you are not content with the meagre amount of chats/messages pulled by the bot, go into each room it created and send &amp;lt;code&amp;gt;!wa delete-portal&amp;lt;/code&amp;gt;. Then go back to your terminal/SSH session and edit &amp;lt;code&amp;gt;/root/mautrix-whatsapp/config.yaml&amp;lt;/code&amp;gt; again. In the &amp;lt;code&amp;gt;bridge&amp;lt;/code&amp;gt; section, edit these settings:&lt;br /&gt;
* &amp;lt;code&amp;gt;initial_chat_sync_count&amp;lt;/code&amp;gt; -- this is the amount of WhatsApp group portals that should be created. You probably want to set this to at least the number of WhatsApp groups you&#039;re in, including &amp;quot;one-on-one&amp;quot; groups for people you&#039;ve direct-messaged. If unsure, just pick a number that&#039;s definitely larger than that.&lt;br /&gt;
* &amp;lt;code&amp;gt;initial_history_fill_count&amp;lt;/code&amp;gt; -- this is the amount of old messages that will be fetched. If you have some very active or old rooms, you will need a very large number here. One caveat: mautrix-whatsapp can get a bit wonky if it&#039;s syncing a large amount of messages, and may need to be killed and restarted a few times for the process to complete. That means you should also be monitoring the sync process.&lt;br /&gt;
* &amp;lt;code&amp;gt;sync_max_chat_age&amp;lt;/code&amp;gt; -- this is the age cutoff in seconds for synced messages. Set it to something like 2000000000 (two billion seconds, or roughly 63 years) to effectively disable the age cutoff.&lt;br /&gt;
* &amp;lt;code&amp;gt;initial_history_disable_notifications&amp;lt;/code&amp;gt; -- this determines whether you get a &amp;quot;new message&amp;quot; notification for every synced old message. If you set &amp;lt;code&amp;gt;initial_history_fill_count&amp;lt;/code&amp;gt; to a large number, do yourself a &#039;&#039;huge&#039;&#039; favour and set this to &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Now restart mautrix-whatsapp:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
systemctl restart mautrix-whatsapp.service&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Finally, send the bridge bot &amp;lt;code&amp;gt;sync --create-all&amp;lt;/code&amp;gt;. It will now start to pull the chats you chose. Bear in mind that this can take a very long time. If you notice it stops making progress, restart mautrix-whatsapp and send &amp;lt;code&amp;gt;sync --create-all&amp;lt;/code&amp;gt; again: it will continue at the point it was stopped.&lt;br /&gt;
&lt;br /&gt;
You are now basically done, although you probably still have WhatsApp running on your phone. The next step will explain how to migrate it to a VM.&lt;br /&gt;
&lt;br /&gt;
=Step 4: migrate WhatsApp proper to a VM=&lt;br /&gt;
To be added. [https://github.com/tulir/mautrix-whatsapp/wiki/Android-VM-Setup The official guide by &#039;&#039;&#039;Alistair Francis&#039;&#039;&#039;] should get you quite far.&lt;br /&gt;
&lt;br /&gt;
Caveat: if you do not have access to KVM on your server, you will need to use an ARM AVD in softemu mode. In my experience, Android versions higher than 4.1.x are too slow to be usable. However, 4.1.2 on an emulated Nexus One API 16 (WVGA) in softemu mode can run WhatsApp on a very modest VPS (albeit slowly).&lt;br /&gt;
&lt;br /&gt;
=Step 5: optional extras=&lt;br /&gt;
Finally, there are a few things you can do to extend this setup.&lt;br /&gt;
&lt;br /&gt;
==Redirect port 443 to OpenVPN==&lt;br /&gt;
Some public Wi-Fi networks only allow access to a very restricted set of ports so clients are limited to web-browsing and checking email. This annoying bit of shit-tier network administration is easily circumvented by piping your traffic through a VPN, which, of course, you just set up -- but you have to be able to connect to the VPN in the first place, and port 1194 (OpenVPN&#039;s default port) is practically guaranteed to be blocked by such networks. Luckily, the default HTTPS port, TCP 443, is practically guaranteed to NOT be blocked, so if your OpenVPN server is using TCP, you can just redirect traffic from 443 to 1194 and log in to your VPN on basically any network. &#039;&#039;N.B. switch your OpenVPN over to TCP if you want to do this but chose UDP earlier.&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
A few commands are all that&#039;s needed:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
iptables -A PREROUTING -i eth0 -p tcp -m tcp --dport 443 -j REDIRECT --to-ports 1194&lt;br /&gt;
iptables -I INPUT 1 -p tcp -m tcp --dport 443 -j ACCEPT&lt;br /&gt;
iptables-save &amp;gt;/etc/iptables/rules.v4&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You can now use &amp;lt;code&amp;gt;remote 123.123.123.123 443&amp;lt;/code&amp;gt; in your client configuration files. Also be sure to add &amp;lt;code&amp;gt;redirect-gateway def1&amp;lt;/code&amp;gt; on clients that connect to public Wi-Fi!&lt;br /&gt;
&lt;br /&gt;
==Install element-web==&lt;br /&gt;
The official element-web implementation at app.element.io does not work with a private homeserver, but element-web &#039;&#039;will&#039;&#039; work if you install it on your homeserver. &#039;&#039;&#039;Note that the authors of element-web specifically recommend against running element-web on the same domain as the homeserver.&#039;&#039;&#039; However, as access is limited to the VPN, the risk should be limited as long as only trusted and experienced users are allowed access to the VPN in the first place.&lt;br /&gt;
&lt;br /&gt;
Download the latest release (1.75-rc1 at the time of writing; replace the version as necessary in what follows, obviously) from [https://github.com/vector-im/element-web/releases], extract it, move it to the proper place, and create the config file:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
wget https://github.com/vector-im/element-web/releases/download/v1.7.15-rc.1/element-v1.7.15-rc.1.tar.gz&lt;br /&gt;
tar -xvzf element-v1.7.15-rc.1.tar.gz&lt;br /&gt;
mv element-v1.7.15-rc.1 /var/www/element-web&lt;br /&gt;
cd /var/www/element-web&lt;br /&gt;
cp config.sample.json config.json&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Edit &amp;lt;code&amp;gt;config.json&amp;lt;/code&amp;gt;. Set &amp;lt;code&amp;gt;&amp;quot;base_url&amp;quot;&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;&amp;quot;https://192.168.16.1:10443&amp;quot;&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;&amp;quot;server_name&amp;quot;&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;&amp;quot;localhost&amp;quot;&amp;lt;/code&amp;gt; in the &amp;lt;code&amp;gt;&amp;quot;m.homeserver&amp;quot;&amp;lt;/code&amp;gt; block near the top. In the root block, set &amp;lt;code&amp;gt;&amp;quot;disable_custom_urls&amp;quot;&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;&amp;quot;default_federate&amp;quot;&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Since we&#039;ve already set up Nginx, we can just tell it to serve element-web as well, and we can even reuse the certificate authority. Edit the configuration file you created earlier, &amp;lt;code&amp;gt;/etc/nginx/sites-available/synapse&amp;lt;/code&amp;gt; and add the following block at the end:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;nginx&amp;quot; line&amp;gt;&lt;br /&gt;
server {&lt;br /&gt;
    listen 10444 ssl;&lt;br /&gt;
    server_name localhost;&lt;br /&gt;
&lt;br /&gt;
    root /var/www/element-web;&lt;br /&gt;
    index index.html;&lt;br /&gt;
&lt;br /&gt;
    ssl on;&lt;br /&gt;
    ssl_certificate /etc/matrix-synapse/easy-rsa/pki/issued/element-web.crt;&lt;br /&gt;
    ssl_certificate_key /etc/matrix-synapse/easy-rsa/pki/private/element-web.key;&lt;br /&gt;
    ssl_client_certificate /etc/matrix-synapse/easy-rsa/pki/ca.crt;&lt;br /&gt;
    ssl_crl /etc/matrix-synapse/easy-rsa/pki/crl.pem;&lt;br /&gt;
    ssl_verify_client on;&lt;br /&gt;
&lt;br /&gt;
    access_log /var/log/nginx-element-web-access_log.log;&lt;br /&gt;
    error_log /var/log/nginx-element-web-error_log.log;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
If you did not manage to get &amp;lt;code&amp;gt;ssl_verify_client&amp;lt;/code&amp;gt; to work earlier, remove that line here too. Also, if you did not use port 443 for OpenVPN, you may use &amp;lt;code&amp;gt;listen 443 ssl;&amp;lt;/code&amp;gt; instead of &amp;lt;code&amp;gt;listen 10444 ssl;&amp;lt;/code&amp;gt; if you want (so you can just point your browser at https://192.168.16.1). Finally, add an iptables rule to allow connecting from within the VPN, and restart Nginx:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
# Replace 10444 with 443 if necessary&lt;br /&gt;
iptables -I INPUT 1 -s 192.168.16.0/24 -i tun0 -p tcp -m tcp --dport 10444 -j ACCEPT&lt;br /&gt;
iptables-save &amp;gt;/etc/iptables/rules.v4&lt;br /&gt;
systemctl restart nginx.service&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Block Facebook on clients==&lt;br /&gt;
On all clients (except the phone running WhatsApp, if you have not moved it to a VM on the server yet), you can now completely block all Facebook-owned IPs and still retain access to WhatsApp using your Matrix homeserver. While tedious to do by hand (considering Facebook&#039;s IP pool grows like an aggressive tumour), it is easily automated on Linux with a cron job and iptables.&lt;br /&gt;
&lt;br /&gt;
First of all, create a new iptables chain that will be used for automated IP blocking, and add it to the front of your INPUT and OUTPUT chains:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
iptables -N BLOCK&lt;br /&gt;
iptables -I INPUT 1 -j BLOCK&lt;br /&gt;
iptables -I OUTPUT 1 -j BLOCK&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Next, we will create a script that finds the list of IPs owned by Facebook and adds them to the BLOCK chain. N.B. this requires &amp;lt;code&amp;gt;whois&amp;lt;/code&amp;gt;, which should be available in any package manager under that name, so install it first if necessary. Then create the file &amp;lt;code&amp;gt;/etc/cron.hourly/ipblock.sh&amp;lt;/code&amp;gt; with the following contents:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
iptables=/sbin/iptables&lt;br /&gt;
&lt;br /&gt;
$iptables -F BLOCK&lt;br /&gt;
&lt;br /&gt;
asns=&amp;quot;AS32934&amp;quot;&lt;br /&gt;
for asn in asns&lt;br /&gt;
do&lt;br /&gt;
    ips=&amp;quot;$(whois -h whois.radb.net -- -i origin -T route $asn | grep route: | awk &#039;{print $2}&#039;)&amp;quot;&lt;br /&gt;
    for i in $ips&lt;br /&gt;
    do&lt;br /&gt;
        $iptables -A BLOCK -d $i -j REJECT --reject-with icmp-host-unreachable&lt;br /&gt;
        $iptables -A BLOCK -s $i -j REJECT --reject-with icmp-host-unreachable&lt;br /&gt;
    done&lt;br /&gt;
done&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This script queries whois.radb.net to find all IPs registered under the [https://en.wikipedia.org/wiki/Autonomous_system_(Internet) autonomous system number] AS32934, which is Facebook. You may add other ASNs too if you want. Note that although the list &amp;quot;only&amp;quot; contains 200 entries or so (at the time of writing), they include mask bits in CIDR notation: this list in fact represents tens of thousands of IP addresses, all owned by Facebook.&lt;br /&gt;
&lt;br /&gt;
Make sure to make the script executable, and run it once if you don&#039;t want to wait until the next hour for cron to do it.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
chmod +x /etc/cron.hourly/ipblock.sh&lt;br /&gt;
/etc/cron.hourly/ipblock.sh&lt;br /&gt;
iptables-save &amp;gt;/etc/iptables/rules.v4&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Honourable mention: Pi-hole==&lt;br /&gt;
This is an honourable mention because I will not describe the installation process here and it is not related to mautrix-whatsapp. Nevertheless, as following this guide leaves you with a perfectly usable VPN server, and people reading this are likely to be privacy-conscious, I feel it is worthwhile to link [https://pi-hole.net/ Pi-hole] here. Pi-hole is a very powerful ad-blocking framework acting at DNS level. Like Synapse, you can configure Pi-hole to only be accessible to clients inside your VPN (by allowing only connections from 192.168.16.0/24 on device tun0). On your VPN clients, you can set 192.168.16.1 as your primary DNS server, so all clients are protected from malicious ads when connected to the VPN.&lt;br /&gt;
&lt;br /&gt;
==Honourable mention: Tor proxy==&lt;br /&gt;
Your VPS can also serve as a VPN-wide proxy that sends traffic through [https://www.torproject.org/ Tor]. [https://www.marcus-povey.co.uk/2016/03/24/using-tor-as-a-http-proxy/ &#039;&#039;&#039;Marcus Povey&#039;&#039;&#039; has created a guide on how to set up a Tor proxy with polipo.]&lt;br /&gt;
&lt;br /&gt;
Considering Tor&#039;s abysmal transfer speed, you probably don&#039;t &#039;&#039;always&#039;&#039; want to redirect traffic over Tor. Most browsers support [https://en.wikipedia.org/wiki/Proxy_auto-config proxy auto-config] (PAC), which allows you to fine-tune which sites get piped through the proxy. For example, the following PAC uses the Tor proxy to handle [https://en.wikipedia.org/wiki/.onion .onion] domains:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;javascript&amp;quot; line&amp;gt;&lt;br /&gt;
function FindProxyForURL (url, host)&lt;br /&gt;
{&lt;br /&gt;
    if (shExpMatch (host, &amp;quot;*.onion&amp;quot;))&lt;br /&gt;
    {&lt;br /&gt;
        return &amp;quot;PROXY 192.168.16.1:8123&amp;quot;&lt;br /&gt;
    }&lt;br /&gt;
    return &amp;quot;DIRECT;&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Much more advanced configurations are possible with proxy auto-config, e.g. redirecting all HTTP traffic but not HTTPS, whitelisting/blacklisting domains.&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=Matrix_Synapse_and_mautrix-whatsapp_in_a_VPN&amp;diff=235</id>
		<title>Matrix Synapse and mautrix-whatsapp in a VPN</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=Matrix_Synapse_and_mautrix-whatsapp_in_a_VPN&amp;diff=235"/>
		<updated>2020-12-12T13:35:19Z</updated>

		<summary type="html">&lt;p&gt;Link: Remove global LD_LIBRARY_PATH.&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;WhatsApp&#039;&#039;&#039; is a popular communication app for Android and iOS. It is owned by Facebook, which has a hostile attitude towards alternative clients and open source software, along with [https://stallman.org/facebook.html being notorious for its egregious privacy violations (and de-facto support of white supremacy and fascism)]. It stands to reason, then, that WhatsApp is not an app that any privacy-conscious individual should want to have installed on their mobile phone, which typically holds a vast quantity of personal data. However, the unfortunate truth is that it tends to be virtually impossible for many of us to stop using WhatsApp without permanently losing contact with several friends, family members, clients or colleagues. Thus, one may seek to find a middle ground: keeping WhatsApp well away from one&#039;s personal devices, but somehow still connecting to its network using these devices. Given Facebook&#039;s hostile attitude to third-party clients, one might expect such a task to be impossible... or is it?&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Enter the Matrix.&#039;&#039;&#039; [https://matrix.org/ Matrix] is a free (as in speech), feature-rich decentralised communication ecosystem that can serve as a platform for instant messaging, e.g. using [https://www.element.io/ Element] as a client. Although I would highly encourage switching from WhatsApp to Element whenever possible, it turns out Matrix is in fact capable of communicating with the WhatsApp network. This is done using [https://github.com/tulir/mautrix-whatsapp mautrix-whatsapp], which masquerades as a WhatsApp Web client and uses it to communicate with the actual WhatsApp client running on a phone or emulator.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;A note:&#039;&#039;&#039; this guide is essentially an amalgamation of the most relevant bits of various other guides: [https://wiki.debian.org/OpenVPN the Debian OpenVPN guide], [https://openvpn.net/community-resources/how-to/#installing-openvpn the OpenVPN community installation guide], [https://www.natrius.eu/dokuwiki/doku.php?id=digital:server:matrixsynapse the Synapse installation guide by &#039;&#039;&#039;natrius&#039;&#039;&#039;], the [https://gist.github.com/marcopaganini/0823d31d43557f9711e21b43a3223fce Nginx/TLS reverse proxy guide by &#039;&#039;&#039;Marco Paganini&#039;&#039;&#039;], [http://blog.hoxnox.com/inet/ssl_nginx.md.html the Nginx/easy-rsa guide by &#039;&#039;&#039;hoxnox&#039;&#039;&#039;], and the [https://github.com/tulir/mautrix-whatsapp/wiki/Bridge-setup official mautrix-whatsapp bridge setup guide by &#039;&#039;&#039;Tulir Asokan&#039;&#039;&#039;]. These individual guides explain their steps in greater depth than I do here, and I highly, highly recommend reading through all of them before tackling a project like this one. &#039;&#039;&#039;Also be very aware that I am not an expert in any of this. If you need enterprise-grade security, close this browser tab now and consult an actual expert.&#039;&#039;&#039; That said, I have made an honest attempt to hammer down any security holes I could think of, and I am presently running this set-up myself.&lt;br /&gt;
&lt;br /&gt;
=The set-up=&lt;br /&gt;
[[File:Mautrix-whatsapp-setup-pathified.svg]]&lt;br /&gt;
&lt;br /&gt;
This guide will explain one possible configuration to use WhatsApp from a Matrix client. In this configuration, a Matrix homeserver (Synapse) and mautrix-whatsapp will be installed on a (virtual) private server, along with OpenVPN. The setup is completely contained within the virtual private network (VPN) created by OpenVPN, and does not require any entrypoints from the outside world except through the VPN (although you may want to enable SSH access for setup and administration). It therefore has a high degree of inherent security. A reverse HTTPS proxy is set up using Nginx with certificates generated by easy-rsa: this is necessary for some clients to be able to connect.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;iptables&amp;lt;/code&amp;gt; is used for setting up routing and firewalling of the server, and only IPv4 is considered for the time being. I wish to &#039;&#039;eventually&#039;&#039; migrate to &amp;lt;code&amp;gt;nftables&amp;lt;/code&amp;gt; and a dual IPv4/IPv6 stack, however this will take more time and effort than I have available, especially considering the current setup &amp;quot;just works&amp;quot; for me.&lt;br /&gt;
&lt;br /&gt;
This guide is primarily focussed on a &amp;quot;single owner, multiple devices&amp;quot; configuration: extra security precautions should be taken if one wants to allow multiple individuals in the VPN, and using multiple WhatsApp accounts in particular is beyond the scope of this guide.&lt;br /&gt;
&lt;br /&gt;
N.B. it is up to you to decide where WhatsApp (the actual proprietary app) goes in the graphic above: as shown, it is put on the VPS (in an emulator or VM), but it is also possible to leave it off the VPS, e.g. running on a normal phone. Off the VPS, you additionally have the choice of whether you want to route its traffic through the VPN (recommended) or not.&lt;br /&gt;
&lt;br /&gt;
==Requirements==&lt;br /&gt;
* A WhatsApp account, and the WhatsApp app running either on a phone or in an emulator on a device with a webcam&lt;br /&gt;
* A (virtual) private server ((V)PS) with the following specs:&lt;br /&gt;
** A static IPv4 address&lt;br /&gt;
** Linux, ideally Debian 10&lt;br /&gt;
** root/&amp;lt;code&amp;gt;sudo&amp;lt;/code&amp;gt; access&lt;br /&gt;
** At least 1 GB RAM&lt;br /&gt;
** At least 5 GB disk space for the software and text message storage&lt;br /&gt;
** More disk space for the media (videos, etc.) you send and receive&lt;br /&gt;
* Decent knowledge of Linux administration&lt;br /&gt;
* A free weekend or so to set up, test and tweak everything&lt;br /&gt;
&lt;br /&gt;
The environment used in this guide is a VPS running Debian 10 with full root access; all commands are run as root unless otherwise stated. It is assumed that the VPS is in a fresh-off-the-shelf state with little more than &amp;lt;code&amp;gt;sshd&amp;lt;/code&amp;gt; installed and running.&lt;br /&gt;
&lt;br /&gt;
=Step 0: iptables and OpenVPN=&lt;br /&gt;
The first and foremost things to get working are the firewall and VPN server. &#039;&#039;Henceforth, we shall use &#039;&#039;&#039;192.168.16.0/24&#039;&#039;&#039; as the OpenVPN address space, and &#039;&#039;&#039;port 1194&#039;&#039;&#039; (UDP or TCP) as the OpenVPN port. Take extra care if you want to use something else. We shall use &#039;&#039;&#039;123.123.123.123&#039;&#039;&#039; as the externally reachable IP of the server; always replace this by whatever the actual address is.&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Log in to your VPS and install iptables as follows:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
apt install iptables iptables-persistent&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Let us begin by setting up some basic security:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
iptables -A INPUT -i lo -j ACCEPT&lt;br /&gt;
iptables -A INPUT -s 127.0.0.1/32 -j ACCEPT&lt;br /&gt;
iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT&lt;br /&gt;
iptables -A INPUT -p icmp -m icmp --icmp-type 8 -j ACCEPT&lt;br /&gt;
iptables -A INPUT -p icmp -m icmp --icmp-type 11 -j ACCEPT&lt;br /&gt;
iptables -A INPUT -p icmp -m icmp --icmp-type 3 -j ACCEPT&lt;br /&gt;
iptables -A INPUT -p tcp -m tcp --dport 22 -j ACCEPT&lt;br /&gt;
iptables -A INPUT -j DROP&lt;br /&gt;
iptables-save &amp;gt;/etc/iptables/rules.v4&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This will drop all incoming connections apart from SSH and a few ICMP messages. The last line preserves your iptables rules when rebooting.&lt;br /&gt;
&lt;br /&gt;
The next step is to install OpenVPN. The instructions provided here mostly follow [https://wiki.debian.org/OpenVPN Debian&#039;s OpenVPN guide]; see that page for more in-depth information. Start by installing the required packages:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
apt install easy-rsa openvpn&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Next, we create a certificate authority directory in &amp;lt;code&amp;gt;/etc/openvpn&amp;lt;/code&amp;gt; and edit the config:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
make-cadir /etc/openvpn/easy-rsa&lt;br /&gt;
cd /etc/openvpn/easy-rsa&lt;br /&gt;
editor vars&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;vars&amp;lt;/code&amp;gt; file has sensible defaults, but you may want to make a few changes (see the inline documentation if you are unsure):&lt;br /&gt;
# uncomment the line with &amp;lt;code&amp;gt;set_var EASYRSA_DN    &amp;quot;cn_only&amp;quot;&amp;lt;/code&amp;gt;&lt;br /&gt;
# set the key size to 4096: &amp;lt;code&amp;gt;set_var EASYRSA_KEY_SIZE      4096&amp;lt;/code&amp;gt;&lt;br /&gt;
# set the CA validity to something long, like 10 years: &amp;lt;code&amp;gt;set_var EASYRSA_CA_EXPIRE     3650&amp;lt;/code&amp;gt;&lt;br /&gt;
# set the certificate validity to something similar: &amp;lt;code&amp;gt;set_var EASYRSA_CERT_EXPIRE   3650&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
After writing the vars file, create the certificate authority, a server certificate and Diffie-Hellman parameters:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
./easyrsa init-pki&lt;br /&gt;
./easyrsa build-ca&lt;br /&gt;
./easyrsa build-server-full server nopass&lt;br /&gt;
./easyrsa gen-dh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
The &amp;lt;code&amp;gt;build-ca&amp;lt;/code&amp;gt; step will prompt you for a name and password. The name can be whatever you like, e.g. &amp;quot;My OpenVPN CA&amp;quot;. Choose something strong but memorable as the password. Every further interaction with the certificate authority will require this password.&lt;br /&gt;
&lt;br /&gt;
Now create a certificate for every client (desktop, laptop, tablet, mobile phone, refrigerator...) you want to have access to the VPN/your WhatsApp account. It&#039;s a good idea to use a file name that allows you to tell these certificates apart, e.g. &amp;lt;code&amp;gt;hostname.n&amp;lt;/code&amp;gt;, where &amp;lt;code&amp;gt;hostname&amp;lt;/code&amp;gt; is the hostname of the device and &amp;lt;code&amp;gt;n&amp;lt;/code&amp;gt; is a number indicating this is the n&#039;th certificate issued for that hostname:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
./easyrsa build-client-full alpha.0 nopass&lt;br /&gt;
./easyrsa build-client-full bravo.0 nopass&lt;br /&gt;
# And so on...&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Finally, per the [https://openvpn.net/community-resources/how-to/#installing-openvpn OpenVPN community installation guide], generate a shared secret TLS key:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
cd /etc/openvpn/easy-rsa/pki/private&lt;br /&gt;
openvpn --genkey --secret ta.key&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Now edit the OpenVPN configuration file:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
cd /etc/openvpn&lt;br /&gt;
editor server.conf&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A viable example configuration file follows. See &amp;lt;code&amp;gt;openvpn(8)&amp;lt;/code&amp;gt; if it is unclear what an option does. Be sure to save this file as /etc/openvpn/server.conf.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;cfg&amp;quot; line&amp;gt;&lt;br /&gt;
mode server&lt;br /&gt;
# Enable TLS encryption.&lt;br /&gt;
tls-server&lt;br /&gt;
# Listen on port 1194 (the default).&lt;br /&gt;
port 1194&lt;br /&gt;
# Set the protocol here. UDP is the default, but it may give you trouble&lt;br /&gt;
# connecting on low-quality public Wi-Fi. You can use `proto tcp-server&#039;&lt;br /&gt;
# here instead, but note that this causes extra overhead.&lt;br /&gt;
# Choose `proto tcp-server&#039; if you want to redirect port 443 to OpenVPN&lt;br /&gt;
# to allow connecting over some poorly configured public WiFi networks.&lt;br /&gt;
proto udp&lt;br /&gt;
&lt;br /&gt;
# Use TUN device, as opposed to TAP. In 99% of cases, TUN is what you want.&lt;br /&gt;
# TAP configuration is outside the scope of this article.&lt;br /&gt;
dev tun&lt;br /&gt;
# IP pool to be used for OpenVPN. The parameters as given will put the server&lt;br /&gt;
# at 192.168.16.1 and clients at addresses up to 192.168.16.255.&lt;br /&gt;
server 192.168.16.0 255.255.255.0&lt;br /&gt;
&lt;br /&gt;
# ca.crt file of the certificate authority we just set up.&lt;br /&gt;
ca /etc/openvpn/easy-rsa/pki/ca.crt&lt;br /&gt;
# Server certificate.&lt;br /&gt;
cert /etc/openvpn/easy-rsa/pki/issued/server.crt&lt;br /&gt;
# Server private key.&lt;br /&gt;
key /etc/openvpn/easy-rsa/pki/private/server.key&lt;br /&gt;
# Diffie-Hellman parameters.&lt;br /&gt;
dh /etc/openvpn/easy-rsa/pki/dh.pem&lt;br /&gt;
# TLS key.&lt;br /&gt;
tls-auth /etc/openvpn/easy-rsa/pki/private/ta.key 0&lt;br /&gt;
&lt;br /&gt;
# Timeout parameters for sending pings/restarting OpenVPN. See openvpn(8).&lt;br /&gt;
keepalive 20 120&lt;br /&gt;
# Enable the following if you want clients to be able to address&lt;br /&gt;
# one another directly.&lt;br /&gt;
#client-to-client&lt;br /&gt;
&lt;br /&gt;
# Compress the stream with LZO to limit bandwidth.&lt;br /&gt;
comp-lzo&lt;br /&gt;
# Allow at most 10 clients to connect at the same time.&lt;br /&gt;
# Increase if necessary, but make sure your network and server can handle&lt;br /&gt;
# the load.&lt;br /&gt;
max-clients 10&lt;br /&gt;
&lt;br /&gt;
# Drop root privileges.&lt;br /&gt;
user nobody&lt;br /&gt;
group nogroup&lt;br /&gt;
&lt;br /&gt;
# Persist keys and TUN device across restarts, since we are dropping root.&lt;br /&gt;
persist-key&lt;br /&gt;
persist-tun&lt;br /&gt;
&lt;br /&gt;
# Status file.&lt;br /&gt;
status /var/log/openvpn-status.log&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Next, configure the firewall to allow OpenVPN clients. The first step depends on the protocol you chose set in the OpenVPN server configuration. If you chose UDP:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
iptables -I INPUT 1 -p udp -m udp --dport 1194 -j ACCEPT&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
OR, if you chose TCP:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
iptables -I INPUT 1 -p tcp -m tcp --dport 1194 -j ACCEPT&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Next, set up forwarding and masquerading, and start the server! N.B. it is assumed here that the ethernet device of your server is eth0, and that OpenVPN creates the device &amp;lt;code&amp;gt;tun0&amp;lt;/code&amp;gt; (i.e. nothing else sets up a TUN device before OpenVPN). Change tun0 and/or eth0 as necessary if this is not true.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
iptables -A FORWARD -i tun0 -j ACCEPT&lt;br /&gt;
iptables -A FORWARD -i tun0 -o eth0 -m state --state RELATED,ESTABLISHED -j ACCEPT&lt;br /&gt;
iptables -A FORWARD -i eth0 -o tun0 -m state --state RELATED,ESTABLISHED -j ACCEPT&lt;br /&gt;
iptables -A POSTROUTING -s 192.168.16.0/24 -o eth0 -j MASQUERADE&lt;br /&gt;
iptables-save &amp;gt;/etc/iptables/rules.v4&lt;br /&gt;
echo &amp;quot;net.ipv4.ip_forward = 1&amp;quot; &amp;gt;&amp;gt;/etc/sysctl.conf&lt;br /&gt;
sysctl -p&lt;br /&gt;
systemctl enable openvpn.service&lt;br /&gt;
systemctl restart openvpn.service&lt;br /&gt;
ifconfig&lt;br /&gt;
ss -plunt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Make sure OpenVPN starts correctly: &amp;lt;code&amp;gt;ifconfig&amp;lt;/code&amp;gt; should show &amp;lt;code&amp;gt;tun0&amp;lt;/code&amp;gt;, along with something like &amp;lt;code&amp;gt;inet 192.168.16.1  netmask 255.255.255.255  destination 192.168.16.2&amp;lt;/code&amp;gt;. &amp;lt;code&amp;gt;ss -plunt&amp;lt;/code&amp;gt; should show &amp;lt;code&amp;gt;0.0.0.0:1194&amp;lt;/code&amp;gt; somewhere in the column labelled &amp;quot;Local Address:Port&amp;quot;. If this is not the case, check &amp;lt;code&amp;gt;/var/log/daemon.log&amp;lt;/code&amp;gt; for entries containing &amp;lt;code&amp;gt;ovpn-server&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
You must now copy some certificate files to your clients. As an example, if you have a Linux client named &amp;quot;alpha&amp;quot; and can access your server as root via SSH on 123.123.123.123, run the following commands as root on &amp;quot;alpha&amp;quot; (assuming OpenVPN is already installed):&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
cd /etc/openvpn&lt;br /&gt;
scp 123.123.123.123:/etc/openvpn/easy-rsa/pki/&#039;{ca.crt,issued/alpha.0.crt,private/alpha.0.key,private/ta.key}&#039; .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
An example of a client configuration file (save as &amp;lt;code&amp;gt;/etc/openvpn/client.conf&amp;lt;/code&amp;gt;) is listed below.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;cfg&amp;quot; line&amp;gt;&lt;br /&gt;
client&lt;br /&gt;
dev tun&lt;br /&gt;
# Use `proto udp&#039; if that&#039;s what the server uses.&lt;br /&gt;
# Else use `proto tcp-client&#039;.&lt;br /&gt;
proto udp&lt;br /&gt;
remote 123.123.123.123 1194&lt;br /&gt;
resolv-retry infinite&lt;br /&gt;
nobind&lt;br /&gt;
&lt;br /&gt;
user nobody&lt;br /&gt;
group nobody&lt;br /&gt;
persist-key&lt;br /&gt;
persist-tun&lt;br /&gt;
&lt;br /&gt;
ca /etc/openvpn/ca.crt&lt;br /&gt;
cert /etc/openvpn/alpha.0.crt&lt;br /&gt;
key /etc/openvpn/alpha.0.key&lt;br /&gt;
&lt;br /&gt;
ns-cert-type server&lt;br /&gt;
&lt;br /&gt;
tls-auth /etc/openvpn/ta.key 1&lt;br /&gt;
&lt;br /&gt;
comp-lzo&lt;br /&gt;
&lt;br /&gt;
# Uncomment the following if you want to redirect all client traffic through&lt;br /&gt;
# the VPN. (Without it, only 192.168.16.0/24 will be routed through the VPN.)&lt;br /&gt;
#redirect-gateway def1&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
On &amp;quot;alpha&amp;quot;, run &amp;lt;code&amp;gt;openvpn --config /etc/openvpn/client.conf&amp;lt;/code&amp;gt; and check if it will connect. Running &amp;lt;code&amp;gt;ifconfig&amp;lt;/code&amp;gt; on &amp;quot;alpha&amp;quot; should show a &amp;lt;code&amp;gt;tun0&amp;lt;/code&amp;gt; with an IP in 129.168.16.0/24 and a destination of 192.168.16.1. You should be able to SSH over the VPN too, using&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
ssh root@192.168.16.1&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
If this connects, your VPN works and you&#039;re done with this step!&lt;br /&gt;
&lt;br /&gt;
=Step 1: PostgreSQL and Synapse=&lt;br /&gt;
Synapse is the name of the Matrix homeserver reference implementation, which we will be installing in this section. This procedure loosely follows [https://www.natrius.eu/dokuwiki/doku.php?id=digital:server:matrixsynapse the guide by &#039;&#039;&#039;natrius&#039;&#039;&#039;], with some key differences:&lt;br /&gt;
# We will thoroughly disable federation.&lt;br /&gt;
# We will use easy-rsa to add self-signed certificates to the Nginx reverse proxy.&lt;br /&gt;
# We will use &amp;lt;code&amp;gt;iptables&amp;lt;/code&amp;gt; instead of &amp;lt;code&amp;gt;ufw&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Before installing Synapse, first install PostgreSQL and its Python binding:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
apt install postgresql python3-psycopg2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Now &amp;lt;code&amp;gt;su&amp;lt;/code&amp;gt; to the PostgreSQL admin account and start the PostgreSQL shell:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
su - postgres&lt;br /&gt;
psql&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
As in natrius&#039; guide, create the synapse database and a user to own it:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot; line&amp;gt;&lt;br /&gt;
CREATE USER &amp;quot;synapse&amp;quot; WITH PASSWORD &#039;password&#039;;&lt;br /&gt;
CREATE DATABASE synapse ENCODING &#039;UTF8&#039; LC_COLLATE=&#039;C&#039; LC_CTYPE=&#039;C&#039; template=template0 OWNER &amp;quot;synapse&amp;quot;;&lt;br /&gt;
\q&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Replace &#039;password&#039; with a strong, ideally random password. That&#039;s all that&#039;s needed as far as PostgreSQL goes, so log out of the &amp;lt;code&amp;gt;postgres&amp;lt;/code&amp;gt; account.&lt;br /&gt;
&lt;br /&gt;
As in the aforementioned guide, first add the relevant repositories and perform any necessary updates, and then install Synapse:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
apt install lsb-release wget apt-transport-https&lt;br /&gt;
wget -O /usr/share/keyrings/matrix-org-archive-keyring.gpg https://packages.matrix.org/debian/matrix-org-archive-keyring.gpg&lt;br /&gt;
echo &amp;quot;deb [signed-by=/usr/share/keyrings/matrix-org-archive-keyring.gpg] https://packages.matrix.org/debian/ $(lsb_release -cs) main&amp;quot; | tee /etc/apt/sources.list.d/matrix-org.list&lt;br /&gt;
apt update &amp;amp;&amp;amp; apt upgrade&lt;br /&gt;
apt install matrix-synapse-py3&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
When asked for a hostname, enter &amp;lt;code&amp;gt;localhost&amp;lt;/code&amp;gt;. Next, edit the configuration file, &amp;lt;code&amp;gt;/etc/matrix-synapse/homeserver.yaml&amp;lt;/code&amp;gt;. In the &amp;lt;code&amp;gt;listeners&amp;lt;/code&amp;gt; section, change the uncommented lines to the following:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;yaml&amp;quot; line&amp;gt;&lt;br /&gt;
  - port: 8008&lt;br /&gt;
    tls: false&lt;br /&gt;
    type: http&lt;br /&gt;
    x_forwarded: true&lt;br /&gt;
    bind_addresses: [&#039;0.0.0.0&#039;]&lt;br /&gt;
&lt;br /&gt;
    resources:&lt;br /&gt;
      - names: [client]&lt;br /&gt;
        compress: false&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
That is, we bind 0.0.0.0 (to listen to the whole network for client connections) and remove federation.&lt;br /&gt;
&lt;br /&gt;
We will be even more thorough in disabling federation: search for &amp;lt;code&amp;gt;federation_domain_whitelist&amp;lt;/code&amp;gt;, which (for the Debian 10 configuration file of Synapse 1.23) should be a commented line with a few domains under it. Add a new line after that comment section:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;yaml&amp;quot;&amp;gt;&lt;br /&gt;
federation_domain_whitelist: []&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Next scroll down to &amp;lt;code&amp;gt;federation_ip_range_blacklist&amp;lt;/code&amp;gt; and remove the IPs underneath that line. Replace them with the IPv4 and IPv6 catchalls:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;yaml&amp;quot; line&amp;gt;&lt;br /&gt;
  - &#039;0.0.0.0/0&#039;&lt;br /&gt;
  - &#039;::/0&#039;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
With federation now thoroughly disabled, look for &amp;lt;code&amp;gt;enable_registration: false&amp;lt;/code&amp;gt; and uncomment this line. As in natrius&#039; guide, look for &amp;lt;code&amp;gt;registration_shared_secret: &amp;lt;PRIVATE STRING&amp;gt;&amp;lt;/code&amp;gt;, uncomment it, and replace &amp;lt;code&amp;gt;&amp;lt;PRIVATE STRING&amp;gt;&amp;lt;/code&amp;gt; with a long, randomly generated alphanumeric string, e.g. generated by the command&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
cat /dev/urandom | tr -dc &#039;a-zA-Z0-9&#039; | fold -w 32 | head -n 1&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Write out the configuration file and exit your editor. We now need to open port 8008 to VPN clients:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
iptables -I INPUT 1 -s 192.168.16.0/24 -i tun0 -p tcp -m tcp --dport 8008 -j ACCEPT&lt;br /&gt;
iptables-save &amp;gt;/etc/iptables/rules.v4&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Now start Synapse and check if it is running:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
systemctl start matrix-synapse.service&lt;br /&gt;
ss -plunt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The last command should show that Synapse is listening on port 8008.&lt;br /&gt;
&lt;br /&gt;
You can now create an account on your homeserver:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
register_new_matrix_user -c /etc/matrix-synapse/homeserver.yaml http://localhost:8008&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
This will prompt for a username and password, as well as the shared secret you entered into the configuration file earlier.&lt;br /&gt;
&lt;br /&gt;
If you have a PC or laptop that you&#039;ve given access to the VPN, you can install the &#039;&#039;&#039;desktop&#039;&#039;&#039; [https://element.io/get-started Element] client on it. You should then be able to connect to &amp;lt;code&amp;gt;http://192.168.16.1:8008&amp;lt;/code&amp;gt; using the account you just created. At this moment, the Android and Web clients do NOT support HTTP connections, so these clients cannot connect to your Synapse installation yet. Which brings us to...&lt;br /&gt;
&lt;br /&gt;
=Step 2: Nginx and TLS=&lt;br /&gt;
To allow most clients to connect to your homeserver (provided they have access to the VPN), you will need to set up a reverse proxy that adds a TLS layer so they can communicate over HTTPS. While the Synapse guide by natrius recommends Let&#039;s Encrypt certificates, in a fully private context like we are considering, it makes sense to set up your own certificate authority.&lt;br /&gt;
&lt;br /&gt;
I opted to build a new certificate authority in the Synapse configuration directory and reuse the configuration I picked for OpenVPN:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
make-cadir /etc/matrix-synapse/easy-rsa&lt;br /&gt;
cd /etc/matrix-synapse/easy-rsa&lt;br /&gt;
cp /etc/openvpn/easy-rsa/vars .&lt;br /&gt;
./easyrsa init-pki&lt;br /&gt;
./easyrsa --subject-alt-name=&amp;quot;IP:192.168.16.1&amp;quot; build-ca&lt;br /&gt;
# Choose a new CA name when prompted. The password you are prompted for&lt;br /&gt;
# should ideally also be different from the one you used for OpenVPN.&lt;br /&gt;
./easyrsa --subject-alt-name=&amp;quot;IP:192.168.16.1&amp;quot; build-server-full matrix-synapse nopass&lt;br /&gt;
# For all clients:&lt;br /&gt;
./easyrsa --subject-alt-name=&amp;quot;IP:192.168.16.1&amp;quot; build-client-full alpha.0 nopass&lt;br /&gt;
./easyrsa --subject-alt-name=&amp;quot;IP:192.168.16.1&amp;quot; build-client-full bravo.0 nopass&lt;br /&gt;
# ...and so on&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Note that we are now including &amp;lt;code&amp;gt;--subject-alt-name=&amp;quot;IP:192.168.16.1&amp;quot;&amp;lt;/code&amp;gt; in our commands. HTTPS requires certificates to be bound to a domain or IP, and browsers tend to get ornery if they are not. We do not need Diffie-Hellman parameters now, but we do need to generate a certificate revocation list so Nginx will be able to reject revoked certificates, and we must also export our client certificates in a format browsers and mobile OSes understand:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; list&amp;gt;&lt;br /&gt;
# For all clients:&lt;br /&gt;
./easyrsa export-p12 alpha.0&lt;br /&gt;
./easyrsa export-p12 bravo.0&lt;br /&gt;
# ...and so on&lt;br /&gt;
./easyrsa gen-crl&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
The export step asks you to provide a password, which allows you to securely transport the exported certificates to the clients they are to be installed on.&lt;br /&gt;
&lt;br /&gt;
Next up, Nginx. Install the package, remove the default configuration, and create a new one:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
apt install nginx&lt;br /&gt;
systemctl stop nginx.service&lt;br /&gt;
unlink /etc/nginx/sites-enabled/default&lt;br /&gt;
ln -s /etc/nginx/sites-available/synapse /etc/nginx/sites-enabled/synapse&lt;br /&gt;
editor /etc/nginx/sites-available/synapse&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
An example configuration that sets up an HTTPS reverse proxy on port 10443 is&lt;br /&gt;
given below.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;nginx&amp;quot; line&amp;gt;&lt;br /&gt;
server {&lt;br /&gt;
    listen 10443 ssl;&lt;br /&gt;
    server_name localhost;&lt;br /&gt;
&lt;br /&gt;
    ssl on;&lt;br /&gt;
    ssl_certificate /etc/matrix-synapse/easy-rsa/pki/issued/matrix-synapse.crt;&lt;br /&gt;
    ssl_certificate_key /etc/matrix-synapse/easy-rsa/pki/private/matrix-synapse.key;&lt;br /&gt;
    ssl_client_certificate /etc/matrix-synapse/easy-rsa/pki/ca.crt;&lt;br /&gt;
    ssl_crl /etc/matrix-synapse/easy-rsa/pki/crl.pem;&lt;br /&gt;
    # Enable mutual TLS. This option provides maximal security by requiring&lt;br /&gt;
    # per-client certificates. Unfortunately, client-side support for this&lt;br /&gt;
    # tends to be shaky at best, so turn it off and restart Nginx if you are&lt;br /&gt;
    # not able to connect.&lt;br /&gt;
    ssl_verify_client on;&lt;br /&gt;
&lt;br /&gt;
    access_log /var/log/nginx-synapse-access_log.log;&lt;br /&gt;
    error_log /var/log/nginx-synapse-error_log.log;&lt;br /&gt;
&lt;br /&gt;
    # There is no need to serve files, and we will not be using&lt;br /&gt;
    # .well-known, since we don&#039;t use federation. So we just reverse-proxy&lt;br /&gt;
    # Synapse&#039;s HTTP at the root and that&#039;s it.&lt;br /&gt;
    location / {&lt;br /&gt;
        proxy_pass http://127.0.0.1:8008;&lt;br /&gt;
        proxy_set_header X-Forwarded-For $remote_addr;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Now unblock port 10443 for the VPN and start Nginx:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
iptables -I INPUT 1 -s 192.168.16.0/24 -i tun0 -p tcp -m tcp --dport 10443 -j ACCEPT&lt;br /&gt;
iptables-save &amp;gt;/etc/iptables/rules.v4&lt;br /&gt;
systemctl enable nginx.service&lt;br /&gt;
systemctl start nginx.service&lt;br /&gt;
# Check if it&#039;s running&lt;br /&gt;
ss -plunt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The last command should show Nginx listening on port 10443. Now copy &amp;lt;code&amp;gt;/etc/matrix-synapse/easy-rsa/pki/ca.crt&amp;lt;/code&amp;gt; to all clients and import it as a trusted system certificate. Also copy each &amp;lt;code&amp;gt;p12&amp;lt;/code&amp;gt; certificate in &amp;lt;code&amp;gt;/etc/matrix-synapse/easy-rsa/pki/private/&amp;lt;/code&amp;gt; to its respective client and import it.&lt;br /&gt;
&lt;br /&gt;
The desktop and android apps should now be able to at least communicate with the server via HTTPS at &amp;lt;code&amp;gt;https://192.168.16.1:10443&amp;lt;/code&amp;gt;. If, however, you get an error stating the client did not send the correct certificate, you may have to disable &amp;lt;code&amp;gt;ssl_verify_client&amp;lt;/code&amp;gt; in the Nginx configuration file and restart Nginx. Note that the canonical Element-web implementation at app.element.io may not be able to talk to your server regardless; I believe this is because browsers will restrict accessing local networks from sites loaded from remote servers. Installing element-web locally will help; see Optional Extras at the end of this guide.&lt;br /&gt;
&lt;br /&gt;
=Step 3: mautrix-whatsapp=&lt;br /&gt;
Being a rather niche package, mautrix-whatsapp is not in the Debian repositories and must be self-compiled. It also has some dependencies on versions of packages too recent to be found in the repositories of Debian 10 (buster)&lt;br /&gt;
&lt;br /&gt;
First and foremost, version 1.14 of Go (&amp;lt;code&amp;gt;golang-1.14&amp;lt;/code&amp;gt;) is needed at the time of writing, which has been backported to buster. If you are running buster, install Go as follows:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
echo &#039;deb http://deb.debian.org/debian buster-backports main&#039; &amp;gt;&amp;gt;/etc/apt/sources.list&lt;br /&gt;
apt update&lt;br /&gt;
apt install -t buster-backports golang&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Alternatively, if you are using Debian 11 (bullseye) or Unstable (sid), simply install golang directly:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
apt install golang&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Regardless of your Debian version, you will need to install &amp;lt;code&amp;gt;ffmpeg&amp;lt;/code&amp;gt;, a GNU toolchain, and &amp;lt;code&amp;gt;git&amp;lt;/code&amp;gt;, if they are not already installed:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
apt install ffmpeg make autoconf automake libtool gcc cmake git&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Next, we need to install a very recent version of &amp;lt;code&amp;gt;olm&amp;lt;/code&amp;gt;, which must be hand-compiled at the time of writing. It is probably best to perform the following steps as a regular user instead of root, but I couldn&#039;t be bothered creating a new user. Caveat emptor.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
cd&lt;br /&gt;
git clone https://gitlab.matrix.org/matrix-org/olm.git&lt;br /&gt;
cd olm&lt;br /&gt;
cmake . -Bbuild&lt;br /&gt;
cd build&lt;br /&gt;
make&lt;br /&gt;
# if you dropped root, use `sudo make install&#039; here instead&lt;br /&gt;
make install&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
We are now ready to compile mautrix-whatsapp. The following steps may also be executed as a regular user if you want. Clone the repo and compile:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
cd&lt;br /&gt;
git clone https://github.com/tulir/mautrix-whatsapp.git&lt;br /&gt;
cd mautrix-whatsapp&lt;br /&gt;
export LD_LIBRARY_PATH=/usr/local/lib&lt;br /&gt;
./build.sh&lt;br /&gt;
# Only execute the following if you executed the above as a regular user&lt;br /&gt;
cd ..&lt;br /&gt;
sudo cp -r mautrix-whatsapp /root/mautrix-whatsapp&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Again as root, copy &amp;lt;code&amp;gt;/root/mautrix-whatsapp/example-config.yaml&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;/root/mautrix-whatsapp/config.yaml&amp;lt;/code&amp;gt; and edit the latter.&lt;br /&gt;
* Under &amp;lt;code&amp;gt;homeserver&amp;lt;/code&amp;gt;, set &amp;lt;code&amp;gt;address&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;http://localhost:8008&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;domain&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;localhost&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Under &amp;lt;code&amp;gt;appservice&amp;lt;/code&amp;gt;, set &amp;lt;code&amp;gt;hostname&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;127.0.0.1&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Under &amp;lt;code&amp;gt;database&amp;lt;/code&amp;gt; (in the &amp;lt;code&amp;gt;appservice&amp;lt;/code&amp;gt; section), set &amp;lt;code&amp;gt;type&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;postgres&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;uri&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;postgres://synapse:&amp;lt;password&amp;gt;@localhost/synapse?sslmode=disable&amp;lt;/code&amp;gt;, where &amp;lt;code&amp;gt;&amp;lt;password&amp;gt;&amp;lt;/code&amp;gt; should be replaced with the PostgreSQL password you picked earlier.&lt;br /&gt;
* Under &amp;lt;code&amp;gt;bridge&amp;lt;/code&amp;gt;, set &amp;lt;code&amp;gt;private_chat_portal_meta&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;permissions&amp;lt;/code&amp;gt; block under &amp;lt;code&amp;gt;bridge&amp;lt;/code&amp;gt; should be something like this:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;yaml&amp;quot; line&amp;gt;&lt;br /&gt;
    permissions:&lt;br /&gt;
        &amp;quot;*&amp;quot;: relaybot&lt;br /&gt;
        &amp;quot;localhost&amp;quot;: user&lt;br /&gt;
        &amp;quot;@alice:localhost&amp;quot;: admin&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Here &amp;lt;code&amp;gt;alice&amp;lt;/code&amp;gt; should be replaced with the username you chose when issuing the &amp;lt;code&amp;gt;register_new_matrix_user&amp;lt;/code&amp;gt; command in Step 1.&lt;br /&gt;
&lt;br /&gt;
Finally, set &amp;lt;code&amp;gt;directory&amp;lt;/code&amp;gt; under &amp;lt;code&amp;gt;logging&amp;lt;/code&amp;gt; to something more suitable, like &amp;lt;code&amp;gt;/var/log/mautrix-whatsapp&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Now generate the appservice registration file, copy it to the Synapse configuration directory, and change its ownership so Synapse can read it:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
cd /root/mautrix-whatsapp&lt;br /&gt;
export LD_LIBRARY_PATH=/usr/local/lib&lt;br /&gt;
./mautrix-whatsapp -g&lt;br /&gt;
cp registration.yaml /etc/matrix-synapse/wa_registration.yaml&lt;br /&gt;
chown matrix-synapse /etc/matrix-synapse/wa_registration.yaml&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Edit &amp;lt;code&amp;gt;/etc/matrix-synapse/homeserver.yaml&amp;lt;/code&amp;gt;. Find the line with &amp;lt;code&amp;gt;app_service_config_files&amp;lt;/code&amp;gt; (which is commented out) and, below the comment, add&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;yaml&amp;quot; line&amp;gt;&lt;br /&gt;
app_service_config_files:&lt;br /&gt;
   - &amp;quot;/etc/matrix-synapse/wa_registration.yaml&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Now restart Synapse:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
systemctl restart matrix-synapse.service&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
It is now time to test mautrix-whatsapp. For testing purposes, start the bridge in the foreground:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
/root/mautrix-whatsapp/mautrix-whatsapp&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Connect to your homeserver with Element-Desktop or Element-Android and start a direct chat with &amp;lt;code&amp;gt;@whatsappbot:localhost&amp;lt;/code&amp;gt;. You should get a message saying the room has been set up as the bridge management/status room. Type &amp;lt;code&amp;gt;help&amp;lt;/code&amp;gt; and send the message. If the bot replies, you&#039;re good; if not, check the logs, and check/adjust your configuration carefully and restart Synapse and mautrix-whatsapp.&lt;br /&gt;
&lt;br /&gt;
If all is working, go back to the terminal where you started &amp;lt;code&amp;gt;/root/mautrix-whatsapp/mautrix-whatsapp&amp;lt;/code&amp;gt; and terminate the process with Ctrl-C. Now create a new file &amp;lt;code&amp;gt;/etc/systemd/system/mautrix-whatsapp.service&amp;lt;/code&amp;gt; and enter the following:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ini&amp;quot; line&amp;gt;&lt;br /&gt;
[Unit]&lt;br /&gt;
Description=WhatsApp to Matrix bridge&lt;br /&gt;
Wants=matrix-synapse.service&lt;br /&gt;
&lt;br /&gt;
[Service]&lt;br /&gt;
Type=exec&lt;br /&gt;
Environment=&amp;quot;LD_LIBRARY_PATH=/usr/local/lib&amp;quot;&lt;br /&gt;
WorkingDirectory=/root/mautrix-whatsapp&lt;br /&gt;
ExecStart=/root/mautrix-whatsapp/mautrix-whatsapp&lt;br /&gt;
Restart=always&lt;br /&gt;
RestartSec=10&lt;br /&gt;
SyslogIdentifier=mautrix-whatsapp&lt;br /&gt;
&lt;br /&gt;
[Install]&lt;br /&gt;
WantedBy=multi-user.target&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You should now be able to start mautrix-whatsapp using systemd:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
systemctl start mautrix-whatsapp.service&lt;br /&gt;
systemctl enable mautrix-whatsapp.service&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Wait a few seconds for mautrix-whatsapp to finish loading, then message &amp;lt;code&amp;gt;help&amp;lt;/code&amp;gt; to your bot again to make sure it still works. If it does, send it &amp;lt;code&amp;gt;login&amp;lt;/code&amp;gt;, and use the actual (official) WhatsApp client to scan the WhatsApp Web QR code that the bot sends you. It should immediately start pulling a few chats, but most likely not your entire history. Ignore it for now. Open your Element client&#039;s settings, go to Help &amp;amp; Advanced, and find your access token. Copy it. Then send the bridge bot &amp;lt;code&amp;gt;login-matrix &amp;lt;access_token&amp;gt;&amp;lt;/code&amp;gt;, replacing &amp;lt;code&amp;gt;&amp;lt;access_token&amp;gt;&amp;lt;/code&amp;gt; with the access token you just copied. This will enable double puppeting; see [https://github.com/tulir/mautrix-whatsapp/wiki/Authentication the Authentication guide].&lt;br /&gt;
&lt;br /&gt;
If you are not content with the meagre amount of chats/messages pulled by the bot, go into each room it created and send &amp;lt;code&amp;gt;!wa delete-portal&amp;lt;/code&amp;gt;. Then go back to your terminal/SSH session and edit &amp;lt;code&amp;gt;/root/mautrix-whatsapp/config.yaml&amp;lt;/code&amp;gt; again. In the &amp;lt;code&amp;gt;bridge&amp;lt;/code&amp;gt; section, edit these settings:&lt;br /&gt;
* &amp;lt;code&amp;gt;initial_chat_sync_count&amp;lt;/code&amp;gt; -- this is the amount of WhatsApp group portals that should be created. You probably want to set this to at least the number of WhatsApp groups you&#039;re in, including &amp;quot;one-on-one&amp;quot; groups for people you&#039;ve direct-messaged. If unsure, just pick a number that&#039;s definitely larger than that.&lt;br /&gt;
* &amp;lt;code&amp;gt;initial_history_fill_count&amp;lt;/code&amp;gt; -- this is the amount of old messages that will be fetched. If you have some very active or old rooms, you will need a very large number here. One caveat: mautrix-whatsapp can get a bit wonky if it&#039;s syncing a large amount of messages, and may need to be killed and restarted a few times for the process to complete. That means you should also be monitoring the sync process.&lt;br /&gt;
* &amp;lt;code&amp;gt;sync_max_chat_age&amp;lt;/code&amp;gt; -- this is the age cutoff in seconds for synced messages. Set it to something like 2000000000 (two billion seconds, or roughly 63 years) to effectively disable the age cutoff.&lt;br /&gt;
* &amp;lt;code&amp;gt;initial_history_disable_notifications&amp;lt;/code&amp;gt; -- this determines whether you get a &amp;quot;new message&amp;quot; notification for every synced old message. If you set &amp;lt;code&amp;gt;initial_history_fill_count&amp;lt;/code&amp;gt; to a large number, do yourself a &#039;&#039;huge&#039;&#039; favour and set this to &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Now restart mautrix-whatsapp:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
systemctl restart mautrix-whatsapp.service&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Finally, send the bridge bot &amp;lt;code&amp;gt;sync --create-all&amp;lt;/code&amp;gt;. It will now start to pull the chats you chose. Bear in mind that this can take a very long time. If you notice it stops making progress, restart mautrix-whatsapp and send &amp;lt;code&amp;gt;sync --create-all&amp;lt;/code&amp;gt; again: it will continue at the point it was stopped.&lt;br /&gt;
&lt;br /&gt;
You are now basically done, although you probably still have WhatsApp running on your phone. The next step will explain how to migrate it to a VM.&lt;br /&gt;
&lt;br /&gt;
=Step 4: migrate WhatsApp proper to a VM=&lt;br /&gt;
I have not performed this step yet myself. However, [https://github.com/tulir/mautrix-whatsapp/wiki/Android-VM-Setup the official guide by &#039;&#039;&#039;Alistair Francis&#039;&#039;&#039;] should be easy enough to follow. I will update this guide when I&#039;ve done this myself.&lt;br /&gt;
&lt;br /&gt;
N.B. in my current setup, WhatsApp runs on a [https://lineageos.org/ LineageOS] phone, and its traffic is piped through the VPN.&lt;br /&gt;
&lt;br /&gt;
=Step 5: optional extras=&lt;br /&gt;
Finally, there are a few things you can do to extend this setup.&lt;br /&gt;
&lt;br /&gt;
==Redirect port 443 to OpenVPN==&lt;br /&gt;
Some public Wi-Fi networks only allow access to a very restricted set of ports so clients are limited to web-browsing and checking email. This annoying bit of shit-tier network administration is easily circumvented by piping your traffic through a VPN, which, of course, you just set up -- but you have to be able to connect to the VPN in the first place, and port 1194 (OpenVPN&#039;s default port) is practically guaranteed to be blocked by such networks. Luckily, the default HTTPS port, TCP 443, is practically guaranteed to NOT be blocked, so if your OpenVPN server is using TCP, you can just redirect traffic from 443 to 1194 and log in to your VPN on basically any network. &#039;&#039;N.B. switch your OpenVPN over to TCP if you want to do this but chose UDP earlier.&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
A few commands are all that&#039;s needed:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
iptables -A PREROUTING -i eth0 -p tcp -m tcp --dport 443 -j REDIRECT --to-ports 1194&lt;br /&gt;
iptables -I INPUT 1 -p tcp -m tcp --dport 443 -j ACCEPT&lt;br /&gt;
iptables-save &amp;gt;/etc/iptables/rules.v4&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You can now use &amp;lt;code&amp;gt;remote 123.123.123.123 443&amp;lt;/code&amp;gt; in your client configuration files. Also be sure to add &amp;lt;code&amp;gt;redirect-gateway def1&amp;lt;/code&amp;gt; on clients that connect to public Wi-Fi!&lt;br /&gt;
&lt;br /&gt;
==Install element-web==&lt;br /&gt;
The official element-web implementation at app.element.io does not work with a private homeserver, but element-web &#039;&#039;will&#039;&#039; work if you install it on your homeserver. &#039;&#039;&#039;Note that the authors of element-web specifically recommend against running element-web on the same domain as the homeserver.&#039;&#039;&#039; However, as access is limited to the VPN, the risk should be limited as long as only trusted and experienced users are allowed access to the VPN in the first place.&lt;br /&gt;
&lt;br /&gt;
Download the latest release (1.75-rc1 at the time of writing; replace the version as necessary in what follows, obviously) from [https://github.com/vector-im/element-web/releases], extract it, move it to the proper place, and create the config file:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
wget https://github.com/vector-im/element-web/releases/download/v1.7.15-rc.1/element-v1.7.15-rc.1.tar.gz&lt;br /&gt;
tar -xvzf element-v1.7.15-rc.1.tar.gz&lt;br /&gt;
mv element-v1.7.15-rc.1 /var/www/element-web&lt;br /&gt;
cd /var/www/element-web&lt;br /&gt;
cp config.sample.json config.json&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Edit &amp;lt;code&amp;gt;config.json&amp;lt;/code&amp;gt;. Set &amp;lt;code&amp;gt;&amp;quot;base_url&amp;quot;&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;&amp;quot;https://192.168.16.1:10443&amp;quot;&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;&amp;quot;server_name&amp;quot;&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;&amp;quot;localhost&amp;quot;&amp;lt;/code&amp;gt; in the &amp;lt;code&amp;gt;&amp;quot;m.homeserver&amp;quot;&amp;lt;/code&amp;gt; block near the top. In the root block, set &amp;lt;code&amp;gt;&amp;quot;disable_custom_urls&amp;quot;&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;&amp;quot;default_federate&amp;quot;&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Since we&#039;ve already set up Nginx, we can just tell it to serve element-web as well, and we can even reuse the certificate authority. Edit the configuration file you created earlier, &amp;lt;code&amp;gt;/etc/nginx/sites-available/synapse&amp;lt;/code&amp;gt; and add the following block at the end:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;nginx&amp;quot; line&amp;gt;&lt;br /&gt;
server {&lt;br /&gt;
    listen 10444 ssl;&lt;br /&gt;
    server_name localhost;&lt;br /&gt;
&lt;br /&gt;
    root /var/www/element-web;&lt;br /&gt;
    index index.html;&lt;br /&gt;
&lt;br /&gt;
    ssl on;&lt;br /&gt;
    ssl_certificate /etc/matrix-synapse/easy-rsa/pki/issued/element-web.crt;&lt;br /&gt;
    ssl_certificate_key /etc/matrix-synapse/easy-rsa/pki/private/element-web.key;&lt;br /&gt;
    ssl_client_certificate /etc/matrix-synapse/easy-rsa/pki/ca.crt;&lt;br /&gt;
    ssl_crl /etc/matrix-synapse/easy-rsa/pki/crl.pem;&lt;br /&gt;
    ssl_verify_client on;&lt;br /&gt;
&lt;br /&gt;
    access_log /var/log/nginx-element-web-access_log.log;&lt;br /&gt;
    error_log /var/log/nginx-element-web-error_log.log;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
If you did not manage to get &amp;lt;code&amp;gt;ssl_verify_client&amp;lt;/code&amp;gt; to work earlier, remove that line here too. Also, if you did not use port 443 for OpenVPN, you may use &amp;lt;code&amp;gt;listen 443 ssl;&amp;lt;/code&amp;gt; instead of &amp;lt;code&amp;gt;listen 10444 ssl;&amp;lt;/code&amp;gt; if you want (so you can just point your browser at https://192.168.16.1). Finally, add an iptables rule to allow connecting from within the VPN, and restart Nginx:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
# Replace 10444 with 443 if necessary&lt;br /&gt;
iptables -I INPUT 1 -s 192.168.16.0/24 -i tun0 -p tcp -m tcp --dport 10444 -j ACCEPT&lt;br /&gt;
iptables-save &amp;gt;/etc/iptables/rules.v4&lt;br /&gt;
systemctl restart nginx.service&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Block Facebook on clients==&lt;br /&gt;
On all clients (except the phone running WhatsApp, if you have not moved it to a VM on the server yet), you can now completely block all Facebook-owned IPs and still retain access to WhatsApp using your Matrix homeserver. While tedious to do by hand (considering Facebook&#039;s IP pool grows like an aggressive tumour), it is easily automated on Linux with a cron job and iptables.&lt;br /&gt;
&lt;br /&gt;
First of all, create a new iptables chain that will be used for automated IP blocking, and add it to the front of your INPUT and OUTPUT chains:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
iptables -N BLOCK&lt;br /&gt;
iptables -I INPUT 1 -j BLOCK&lt;br /&gt;
iptables -I OUTPUT 1 -j BLOCK&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Next, we will create a script that finds the list of IPs owned by Facebook and adds them to the BLOCK chain. N.B. this requires &amp;lt;code&amp;gt;whois&amp;lt;/code&amp;gt;, which should be available in any package manager under that name, so install it first if necessary. Then create the file &amp;lt;code&amp;gt;/etc/cron.hourly/ipblock.sh&amp;lt;/code&amp;gt; with the following contents:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
iptables=/sbin/iptables&lt;br /&gt;
&lt;br /&gt;
$iptables -F BLOCK&lt;br /&gt;
&lt;br /&gt;
asns=&amp;quot;AS32934&amp;quot;&lt;br /&gt;
for asn in asns&lt;br /&gt;
do&lt;br /&gt;
    ips=&amp;quot;$(whois -h whois.radb.net -- -i origin -T route $asn | grep route: | awk &#039;{print $2}&#039;)&amp;quot;&lt;br /&gt;
    for i in $ips&lt;br /&gt;
    do&lt;br /&gt;
        $iptables -A BLOCK -d $i -j REJECT --reject-with icmp-host-unreachable&lt;br /&gt;
        $iptables -A BLOCK -s $i -j REJECT --reject-with icmp-host-unreachable&lt;br /&gt;
    done&lt;br /&gt;
done&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This script queries whois.radb.net to find all IPs registered under the [https://en.wikipedia.org/wiki/Autonomous_system_(Internet) autonomous system number] AS32934, which is Facebook. You may add other ASNs too if you want. Note that although the list &amp;quot;only&amp;quot; contains 200 entries or so (at the time of writing), they include mask bits in CIDR notation: this list in fact represents tens of thousands of IP addresses, all owned by Facebook.&lt;br /&gt;
&lt;br /&gt;
Make sure to make the script executable, and run it once if you don&#039;t want to wait until the next hour for cron to do it.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
chmod +x /etc/cron.hourly/ipblock.sh&lt;br /&gt;
/etc/cron.hourly/ipblock.sh&lt;br /&gt;
iptables-save &amp;gt;/etc/iptables/rules.v4&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Honourable mention: Pi-hole==&lt;br /&gt;
This is an honourable mention because I will not describe the installation process here and it is not related to mautrix-whatsapp. Nevertheless, as following this guide leaves you with a perfectly usable VPN server, and people reading this are likely to be privacy-conscious, I feel it is worthwhile to link [https://pi-hole.net/ Pi-hole] here. Pi-hole is a very powerful ad-blocking framework acting at DNS level. Like Synapse, you can configure Pi-hole to only be accessible to clients inside your VPN (by allowing only connections from 192.168.16.0/24 on device tun0). On your VPN clients, you can set 192.168.16.1 as your primary DNS server, so all clients are protected from malicious ads when connected to the VPN.&lt;br /&gt;
&lt;br /&gt;
==Honourable mention: Tor proxy==&lt;br /&gt;
Your VPS can also serve as a VPN-wide proxy that sends traffic through [https://www.torproject.org/ Tor]. [https://www.marcus-povey.co.uk/2016/03/24/using-tor-as-a-http-proxy/ &#039;&#039;&#039;Marcus Povey&#039;&#039;&#039; has created a guide on how to set up a Tor proxy with polipo.]&lt;br /&gt;
&lt;br /&gt;
Considering Tor&#039;s abysmal transfer speed, you probably don&#039;t &#039;&#039;always&#039;&#039; want to redirect traffic over Tor. Most browsers support [https://en.wikipedia.org/wiki/Proxy_auto-config proxy auto-config] (PAC), which allows you to fine-tune which sites get piped through the proxy. For example, the following PAC uses the Tor proxy to handle [https://en.wikipedia.org/wiki/.onion .onion] domains:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;javascript&amp;quot; line&amp;gt;&lt;br /&gt;
function FindProxyForURL (url, host)&lt;br /&gt;
{&lt;br /&gt;
    if (shExpMatch (host, &amp;quot;*.onion&amp;quot;))&lt;br /&gt;
    {&lt;br /&gt;
        return &amp;quot;PROXY 192.168.16.1:8123&amp;quot;&lt;br /&gt;
    }&lt;br /&gt;
    return &amp;quot;DIRECT;&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Much more advanced configurations are possible with proxy auto-config, e.g. redirecting all HTTP traffic but not HTTPS, whitelisting/blacklisting domains.&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=Main_Page&amp;diff=234</id>
		<title>Main Page</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=Main_Page&amp;diff=234"/>
		<updated>2020-12-12T10:22:46Z</updated>

		<summary type="html">&lt;p&gt;Link: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Welcome to &#039;&#039;&#039;Penguin Development&#039;&#039;&#039;! We make [[w:free and open-source software|free and open-source software]] and [[w:Open-source hardware|hardware]].&lt;br /&gt;
&lt;br /&gt;
See below for current releases. All software and hardware is licensed under the [https://gnu.org/licenses/gpl.html GPL] (version 3 or higher) unless otherwise stated.&lt;br /&gt;
&lt;br /&gt;
=Software=&lt;br /&gt;
* [[AUTOMOME]] — the OTTish AUTOMEME clone&lt;br /&gt;
* [[Ising]] — Monte Carlo Ising model simulator&lt;br /&gt;
* [[stackermann]] — a stack-based [[w:Ackermann function|Ackermann function]] calculator&lt;br /&gt;
&lt;br /&gt;
=Hardware=&lt;br /&gt;
* [[timelapse]] — time-lapse trigger for digital cameras&lt;br /&gt;
&lt;br /&gt;
=Code snippets and how-tos=&lt;br /&gt;
* [[Animated 3D plotting with Blender]]&lt;br /&gt;
* [[Matrix Synapse and mautrix-whatsapp in a VPN]]&lt;br /&gt;
&lt;br /&gt;
=Other=&lt;br /&gt;
* [[LineageOS builds|Unofficial LineageOS builds]]&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=Matrix_Synapse_and_mautrix-whatsapp_in_a_VPN&amp;diff=233</id>
		<title>Matrix Synapse and mautrix-whatsapp in a VPN</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=Matrix_Synapse_and_mautrix-whatsapp_in_a_VPN&amp;diff=233"/>
		<updated>2020-12-12T10:21:02Z</updated>

		<summary type="html">&lt;p&gt;Link: Created page with &amp;quot;&amp;#039;&amp;#039;&amp;#039;WhatsApp&amp;#039;&amp;#039;&amp;#039; is a popular communication app for Android and iOS. It is owned by Facebook, which has a hostile attitude towards alternative clients and open source software,...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;WhatsApp&#039;&#039;&#039; is a popular communication app for Android and iOS. It is owned by Facebook, which has a hostile attitude towards alternative clients and open source software, along with [https://stallman.org/facebook.html being notorious for its egregious privacy violations (and de-facto support of white supremacy and fascism)]. It stands to reason, then, that WhatsApp is not an app that any privacy-conscious individual should want to have installed on their mobile phone, which typically holds a vast quantity of personal data. However, the unfortunate truth is that it tends to be virtually impossible for many of us to stop using WhatsApp without permanently losing contact with several friends, family members, clients or colleagues. Thus, one may seek to find a middle ground: keeping WhatsApp well away from one&#039;s personal devices, but somehow still connecting to its network using these devices. Given Facebook&#039;s hostile attitude to third-party clients, one might expect such a task to be impossible... or is it?&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Enter the Matrix.&#039;&#039;&#039; [https://matrix.org/ Matrix] is a free (as in speech), feature-rich decentralised communication ecosystem that can serve as a platform for instant messaging, e.g. using [https://www.element.io/ Element] as a client. Although I would highly encourage switching from WhatsApp to Element whenever possible, it turns out Matrix is in fact capable of communicating with the WhatsApp network. This is done using [https://github.com/tulir/mautrix-whatsapp mautrix-whatsapp], which masquerades as a WhatsApp Web client and uses it to communicate with the actual WhatsApp client running on a phone or emulator.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;A note:&#039;&#039;&#039; this guide is essentially an amalgamation of the most relevant bits of various other guides: [https://wiki.debian.org/OpenVPN the Debian OpenVPN guide], [https://openvpn.net/community-resources/how-to/#installing-openvpn the OpenVPN community installation guide], [https://www.natrius.eu/dokuwiki/doku.php?id=digital:server:matrixsynapse the Synapse installation guide by &#039;&#039;&#039;natrius&#039;&#039;&#039;], the [https://gist.github.com/marcopaganini/0823d31d43557f9711e21b43a3223fce Nginx/TLS reverse proxy guide by &#039;&#039;&#039;Marco Paganini&#039;&#039;&#039;], [http://blog.hoxnox.com/inet/ssl_nginx.md.html the Nginx/easy-rsa guide by &#039;&#039;&#039;hoxnox&#039;&#039;&#039;], and the [https://github.com/tulir/mautrix-whatsapp/wiki/Bridge-setup official mautrix-whatsapp bridge setup guide by &#039;&#039;&#039;Tulir Asokan&#039;&#039;&#039;]. These individual guides explain their steps in greater depth than I do here, and I highly, highly recommend reading through all of them before tackling a project like this one. &#039;&#039;&#039;Also be very aware that I am not an expert in any of this. If you need enterprise-grade security, close this browser tab now and consult an actual expert.&#039;&#039;&#039; That said, I have made an honest attempt to hammer down any security holes I could think of, and I am presently running this set-up myself.&lt;br /&gt;
&lt;br /&gt;
=The set-up=&lt;br /&gt;
[[File:Mautrix-whatsapp-setup-pathified.svg]]&lt;br /&gt;
&lt;br /&gt;
This guide will explain one possible configuration to use WhatsApp from a Matrix client. In this configuration, a Matrix homeserver (Synapse) and mautrix-whatsapp will be installed on a (virtual) private server, along with OpenVPN. The setup is completely contained within the virtual private network (VPN) created by OpenVPN, and does not require any entrypoints from the outside world except through the VPN (although you may want to enable SSH access for setup and administration). It therefore has a high degree of inherent security. A reverse HTTPS proxy is set up using Nginx with certificates generated by easy-rsa: this is necessary for some clients to be able to connect.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;iptables&amp;lt;/code&amp;gt; is used for setting up routing and firewalling of the server, and only IPv4 is considered for the time being. I wish to &#039;&#039;eventually&#039;&#039; migrate to &amp;lt;code&amp;gt;nftables&amp;lt;/code&amp;gt; and a dual IPv4/IPv6 stack, however this will take more time and effort than I have available, especially considering the current setup &amp;quot;just works&amp;quot; for me.&lt;br /&gt;
&lt;br /&gt;
This guide is primarily focussed on a &amp;quot;single owner, multiple devices&amp;quot; configuration: extra security precautions should be taken if one wants to allow multiple individuals in the VPN, and using multiple WhatsApp accounts in particular is beyond the scope of this guide.&lt;br /&gt;
&lt;br /&gt;
N.B. it is up to you to decide where WhatsApp (the actual proprietary app) goes in the graphic above: as shown, it is put on the VPS (in an emulator or VM), but it is also possible to leave it off the VPS, e.g. running on a normal phone. Off the VPS, you additionally have the choice of whether you want to route its traffic through the VPN (recommended) or not.&lt;br /&gt;
&lt;br /&gt;
==Requirements==&lt;br /&gt;
* A WhatsApp account, and the WhatsApp app running either on a phone or in an emulator on a device with a webcam&lt;br /&gt;
* A (virtual) private server ((V)PS) with the following specs:&lt;br /&gt;
** A static IPv4 address&lt;br /&gt;
** Linux, ideally Debian 10&lt;br /&gt;
** root/&amp;lt;code&amp;gt;sudo&amp;lt;/code&amp;gt; access&lt;br /&gt;
** At least 1 GB RAM&lt;br /&gt;
** At least 5 GB disk space for the software and text message storage&lt;br /&gt;
** More disk space for the media (videos, etc.) you send and receive&lt;br /&gt;
* Decent knowledge of Linux administration&lt;br /&gt;
* A free weekend or so to set up, test and tweak everything&lt;br /&gt;
&lt;br /&gt;
The environment used in this guide is a VPS running Debian 10 with full root access; all commands are run as root unless otherwise stated. It is assumed that the VPS is in a fresh-off-the-shelf state with little more than &amp;lt;code&amp;gt;sshd&amp;lt;/code&amp;gt; installed and running.&lt;br /&gt;
&lt;br /&gt;
=Step 0: iptables and OpenVPN=&lt;br /&gt;
The first and foremost things to get working are the firewall and VPN server. &#039;&#039;Henceforth, we shall use &#039;&#039;&#039;192.168.16.0/24&#039;&#039;&#039; as the OpenVPN address space, and &#039;&#039;&#039;port 1194&#039;&#039;&#039; (UDP or TCP) as the OpenVPN port. Take extra care if you want to use something else. We shall use &#039;&#039;&#039;123.123.123.123&#039;&#039;&#039; as the externally reachable IP of the server; always replace this by whatever the actual address is.&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Log in to your VPS and install iptables as follows:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
apt install iptables iptables-persistent&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Let us begin by setting up some basic security:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
iptables -A INPUT -i lo -j ACCEPT&lt;br /&gt;
iptables -A INPUT -s 127.0.0.1/32 -j ACCEPT&lt;br /&gt;
iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT&lt;br /&gt;
iptables -A INPUT -p icmp -m icmp --icmp-type 8 -j ACCEPT&lt;br /&gt;
iptables -A INPUT -p icmp -m icmp --icmp-type 11 -j ACCEPT&lt;br /&gt;
iptables -A INPUT -p icmp -m icmp --icmp-type 3 -j ACCEPT&lt;br /&gt;
iptables -A INPUT -p tcp -m tcp --dport 22 -j ACCEPT&lt;br /&gt;
iptables -A INPUT -j DROP&lt;br /&gt;
iptables-save &amp;gt;/etc/iptables/rules.v4&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This will drop all incoming connections apart from SSH and a few ICMP messages. The last line preserves your iptables rules when rebooting.&lt;br /&gt;
&lt;br /&gt;
The next step is to install OpenVPN. The instructions provided here mostly follow [https://wiki.debian.org/OpenVPN Debian&#039;s OpenVPN guide]; see that page for more in-depth information. Start by installing the required packages:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
apt install easy-rsa openvpn&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Next, we create a certificate authority directory in &amp;lt;code&amp;gt;/etc/openvpn&amp;lt;/code&amp;gt; and edit the config:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
make-cadir /etc/openvpn/easy-rsa&lt;br /&gt;
cd /etc/openvpn/easy-rsa&lt;br /&gt;
editor vars&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;vars&amp;lt;/code&amp;gt; file has sensible defaults, but you may want to make a few changes (see the inline documentation if you are unsure):&lt;br /&gt;
# uncomment the line with &amp;lt;code&amp;gt;set_var EASYRSA_DN    &amp;quot;cn_only&amp;quot;&amp;lt;/code&amp;gt;&lt;br /&gt;
# set the key size to 4096: &amp;lt;code&amp;gt;set_var EASYRSA_KEY_SIZE      4096&amp;lt;/code&amp;gt;&lt;br /&gt;
# set the CA validity to something long, like 10 years: &amp;lt;code&amp;gt;set_var EASYRSA_CA_EXPIRE     3650&amp;lt;/code&amp;gt;&lt;br /&gt;
# set the certificate validity to something similar: &amp;lt;code&amp;gt;set_var EASYRSA_CERT_EXPIRE   3650&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
After writing the vars file, create the certificate authority, a server certificate and Diffie-Hellman parameters:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
./easyrsa init-pki&lt;br /&gt;
./easyrsa build-ca&lt;br /&gt;
./easyrsa build-server-full server nopass&lt;br /&gt;
./easyrsa gen-dh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
The &amp;lt;code&amp;gt;build-ca&amp;lt;/code&amp;gt; step will prompt you for a name and password. The name can be whatever you like, e.g. &amp;quot;My OpenVPN CA&amp;quot;. Choose something strong but memorable as the password. Every further interaction with the certificate authority will require this password.&lt;br /&gt;
&lt;br /&gt;
Now create a certificate for every client (desktop, laptop, tablet, mobile phone, refrigerator...) you want to have access to the VPN/your WhatsApp account. It&#039;s a good idea to use a file name that allows you to tell these certificates apart, e.g. &amp;lt;code&amp;gt;hostname.n&amp;lt;/code&amp;gt;, where &amp;lt;code&amp;gt;hostname&amp;lt;/code&amp;gt; is the hostname of the device and &amp;lt;code&amp;gt;n&amp;lt;/code&amp;gt; is a number indicating this is the n&#039;th certificate issued for that hostname:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
./easyrsa build-client-full alpha.0 nopass&lt;br /&gt;
./easyrsa build-client-full bravo.0 nopass&lt;br /&gt;
# And so on...&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Finally, per the [https://openvpn.net/community-resources/how-to/#installing-openvpn OpenVPN community installation guide], generate a shared secret TLS key:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
cd /etc/openvpn/easy-rsa/pki/private&lt;br /&gt;
openvpn --genkey --secret ta.key&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Now edit the OpenVPN configuration file:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
cd /etc/openvpn&lt;br /&gt;
editor server.conf&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
A viable example configuration file follows. See &amp;lt;code&amp;gt;openvpn(8)&amp;lt;/code&amp;gt; if it is unclear what an option does. Be sure to save this file as /etc/openvpn/server.conf.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;cfg&amp;quot; line&amp;gt;&lt;br /&gt;
mode server&lt;br /&gt;
# Enable TLS encryption.&lt;br /&gt;
tls-server&lt;br /&gt;
# Listen on port 1194 (the default).&lt;br /&gt;
port 1194&lt;br /&gt;
# Set the protocol here. UDP is the default, but it may give you trouble&lt;br /&gt;
# connecting on low-quality public Wi-Fi. You can use `proto tcp-server&#039;&lt;br /&gt;
# here instead, but note that this causes extra overhead.&lt;br /&gt;
# Choose `proto tcp-server&#039; if you want to redirect port 443 to OpenVPN&lt;br /&gt;
# to allow connecting over some poorly configured public WiFi networks.&lt;br /&gt;
proto udp&lt;br /&gt;
&lt;br /&gt;
# Use TUN device, as opposed to TAP. In 99% of cases, TUN is what you want.&lt;br /&gt;
# TAP configuration is outside the scope of this article.&lt;br /&gt;
dev tun&lt;br /&gt;
# IP pool to be used for OpenVPN. The parameters as given will put the server&lt;br /&gt;
# at 192.168.16.1 and clients at addresses up to 192.168.16.255.&lt;br /&gt;
server 192.168.16.0 255.255.255.0&lt;br /&gt;
&lt;br /&gt;
# ca.crt file of the certificate authority we just set up.&lt;br /&gt;
ca /etc/openvpn/easy-rsa/pki/ca.crt&lt;br /&gt;
# Server certificate.&lt;br /&gt;
cert /etc/openvpn/easy-rsa/pki/issued/server.crt&lt;br /&gt;
# Server private key.&lt;br /&gt;
key /etc/openvpn/easy-rsa/pki/private/server.key&lt;br /&gt;
# Diffie-Hellman parameters.&lt;br /&gt;
dh /etc/openvpn/easy-rsa/pki/dh.pem&lt;br /&gt;
# TLS key.&lt;br /&gt;
tls-auth /etc/openvpn/easy-rsa/pki/private/ta.key 0&lt;br /&gt;
&lt;br /&gt;
# Timeout parameters for sending pings/restarting OpenVPN. See openvpn(8).&lt;br /&gt;
keepalive 20 120&lt;br /&gt;
# Enable the following if you want clients to be able to address&lt;br /&gt;
# one another directly.&lt;br /&gt;
#client-to-client&lt;br /&gt;
&lt;br /&gt;
# Compress the stream with LZO to limit bandwidth.&lt;br /&gt;
comp-lzo&lt;br /&gt;
# Allow at most 10 clients to connect at the same time.&lt;br /&gt;
# Increase if necessary, but make sure your network and server can handle&lt;br /&gt;
# the load.&lt;br /&gt;
max-clients 10&lt;br /&gt;
&lt;br /&gt;
# Drop root privileges.&lt;br /&gt;
user nobody&lt;br /&gt;
group nogroup&lt;br /&gt;
&lt;br /&gt;
# Persist keys and TUN device across restarts, since we are dropping root.&lt;br /&gt;
persist-key&lt;br /&gt;
persist-tun&lt;br /&gt;
&lt;br /&gt;
# Status file.&lt;br /&gt;
status /var/log/openvpn-status.log&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Next, configure the firewall to allow OpenVPN clients. The first step depends on the protocol you chose set in the OpenVPN server configuration. If you chose UDP:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
iptables -I INPUT 1 -p udp -m udp --dport 1194 -j ACCEPT&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
OR, if you chose TCP:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
iptables -I INPUT 1 -p tcp -m tcp --dport 1194 -j ACCEPT&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Next, set up forwarding and masquerading, and start the server! N.B. it is assumed here that the ethernet device of your server is eth0, and that OpenVPN creates the device &amp;lt;code&amp;gt;tun0&amp;lt;/code&amp;gt; (i.e. nothing else sets up a TUN device before OpenVPN). Change tun0 and/or eth0 as necessary if this is not true.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
iptables -A FORWARD -i tun0 -j ACCEPT&lt;br /&gt;
iptables -A FORWARD -i tun0 -o eth0 -m state --state RELATED,ESTABLISHED -j ACCEPT&lt;br /&gt;
iptables -A FORWARD -i eth0 -o tun0 -m state --state RELATED,ESTABLISHED -j ACCEPT&lt;br /&gt;
iptables -A POSTROUTING -s 192.168.16.0/24 -o eth0 -j MASQUERADE&lt;br /&gt;
iptables-save &amp;gt;/etc/iptables/rules.v4&lt;br /&gt;
echo &amp;quot;net.ipv4.ip_forward = 1&amp;quot; &amp;gt;&amp;gt;/etc/sysctl.conf&lt;br /&gt;
sysctl -p&lt;br /&gt;
systemctl enable openvpn.service&lt;br /&gt;
systemctl restart openvpn.service&lt;br /&gt;
ifconfig&lt;br /&gt;
ss -plunt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Make sure OpenVPN starts correctly: &amp;lt;code&amp;gt;ifconfig&amp;lt;/code&amp;gt; should show &amp;lt;code&amp;gt;tun0&amp;lt;/code&amp;gt;, along with something like &amp;lt;code&amp;gt;inet 192.168.16.1  netmask 255.255.255.255  destination 192.168.16.2&amp;lt;/code&amp;gt;. &amp;lt;code&amp;gt;ss -plunt&amp;lt;/code&amp;gt; should show &amp;lt;code&amp;gt;0.0.0.0:1194&amp;lt;/code&amp;gt; somewhere in the column labelled &amp;quot;Local Address:Port&amp;quot;. If this is not the case, check &amp;lt;code&amp;gt;/var/log/daemon.log&amp;lt;/code&amp;gt; for entries containing &amp;lt;code&amp;gt;ovpn-server&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
You must now copy some certificate files to your clients. As an example, if you have a Linux client named &amp;quot;alpha&amp;quot; and can access your server as root via SSH on 123.123.123.123, run the following commands as root on &amp;quot;alpha&amp;quot; (assuming OpenVPN is already installed):&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
cd /etc/openvpn&lt;br /&gt;
scp 123.123.123.123:/etc/openvpn/easy-rsa/pki/&#039;{ca.crt,issued/alpha.0.crt,private/alpha.0.key,private/ta.key}&#039; .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
An example of a client configuration file (save as &amp;lt;code&amp;gt;/etc/openvpn/client.conf&amp;lt;/code&amp;gt;) is listed below.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;cfg&amp;quot; line&amp;gt;&lt;br /&gt;
client&lt;br /&gt;
dev tun&lt;br /&gt;
# Use `proto udp&#039; if that&#039;s what the server uses.&lt;br /&gt;
# Else use `proto tcp-client&#039;.&lt;br /&gt;
proto udp&lt;br /&gt;
remote 123.123.123.123 1194&lt;br /&gt;
resolv-retry infinite&lt;br /&gt;
nobind&lt;br /&gt;
&lt;br /&gt;
user nobody&lt;br /&gt;
group nobody&lt;br /&gt;
persist-key&lt;br /&gt;
persist-tun&lt;br /&gt;
&lt;br /&gt;
ca /etc/openvpn/ca.crt&lt;br /&gt;
cert /etc/openvpn/alpha.0.crt&lt;br /&gt;
key /etc/openvpn/alpha.0.key&lt;br /&gt;
&lt;br /&gt;
ns-cert-type server&lt;br /&gt;
&lt;br /&gt;
tls-auth /etc/openvpn/ta.key 1&lt;br /&gt;
&lt;br /&gt;
comp-lzo&lt;br /&gt;
&lt;br /&gt;
# Uncomment the following if you want to redirect all client traffic through&lt;br /&gt;
# the VPN. (Without it, only 192.168.16.0/24 will be routed through the VPN.)&lt;br /&gt;
#redirect-gateway def1&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
On &amp;quot;alpha&amp;quot;, run &amp;lt;code&amp;gt;openvpn --config /etc/openvpn/client.conf&amp;lt;/code&amp;gt; and check if it will connect. Running &amp;lt;code&amp;gt;ifconfig&amp;lt;/code&amp;gt; on &amp;quot;alpha&amp;quot; should show a &amp;lt;code&amp;gt;tun0&amp;lt;/code&amp;gt; with an IP in 129.168.16.0/24 and a destination of 192.168.16.1. You should be able to SSH over the VPN too, using&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
ssh root@192.168.16.1&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
If this connects, your VPN works and you&#039;re done with this step!&lt;br /&gt;
&lt;br /&gt;
=Step 1: PostgreSQL and Synapse=&lt;br /&gt;
Synapse is the name of the Matrix homeserver reference implementation, which we will be installing in this section. This procedure loosely follows [https://www.natrius.eu/dokuwiki/doku.php?id=digital:server:matrixsynapse the guide by &#039;&#039;&#039;natrius&#039;&#039;&#039;], with some key differences:&lt;br /&gt;
# We will thoroughly disable federation.&lt;br /&gt;
# We will use easy-rsa to add self-signed certificates to the Nginx reverse proxy.&lt;br /&gt;
# We will use &amp;lt;code&amp;gt;iptables&amp;lt;/code&amp;gt; instead of &amp;lt;code&amp;gt;ufw&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Before installing Synapse, first install PostgreSQL and its Python binding:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
apt install postgresql python3-psycopg2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Now &amp;lt;code&amp;gt;su&amp;lt;/code&amp;gt; to the PostgreSQL admin account and start the PostgreSQL shell:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
su - postgres&lt;br /&gt;
psql&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
As in natrius&#039; guide, create the synapse database and a user to own it:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;sql&amp;quot; line&amp;gt;&lt;br /&gt;
CREATE USER &amp;quot;synapse&amp;quot; WITH PASSWORD &#039;password&#039;;&lt;br /&gt;
CREATE DATABASE synapse ENCODING &#039;UTF8&#039; LC_COLLATE=&#039;C&#039; LC_CTYPE=&#039;C&#039; template=template0 OWNER &amp;quot;synapse&amp;quot;;&lt;br /&gt;
\q&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Replace &#039;password&#039; with a strong, ideally random password. That&#039;s all that&#039;s needed as far as PostgreSQL goes, so log out of the &amp;lt;code&amp;gt;postgres&amp;lt;/code&amp;gt; account.&lt;br /&gt;
&lt;br /&gt;
As in the aforementioned guide, first add the relevant repositories and perform any necessary updates, and then install Synapse:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
apt install lsb-release wget apt-transport-https&lt;br /&gt;
wget -O /usr/share/keyrings/matrix-org-archive-keyring.gpg https://packages.matrix.org/debian/matrix-org-archive-keyring.gpg&lt;br /&gt;
echo &amp;quot;deb [signed-by=/usr/share/keyrings/matrix-org-archive-keyring.gpg] https://packages.matrix.org/debian/ $(lsb_release -cs) main&amp;quot; | tee /etc/apt/sources.list.d/matrix-org.list&lt;br /&gt;
apt update &amp;amp;&amp;amp; apt upgrade&lt;br /&gt;
apt install matrix-synapse-py3&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
When asked for a hostname, enter &amp;lt;code&amp;gt;localhost&amp;lt;/code&amp;gt;. Next, edit the configuration file, &amp;lt;code&amp;gt;/etc/matrix-synapse/homeserver.yaml&amp;lt;/code&amp;gt;. In the &amp;lt;code&amp;gt;listeners&amp;lt;/code&amp;gt; section, change the uncommented lines to the following:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;yaml&amp;quot; line&amp;gt;&lt;br /&gt;
  - port: 8008&lt;br /&gt;
    tls: false&lt;br /&gt;
    type: http&lt;br /&gt;
    x_forwarded: true&lt;br /&gt;
    bind_addresses: [&#039;0.0.0.0&#039;]&lt;br /&gt;
&lt;br /&gt;
    resources:&lt;br /&gt;
      - names: [client]&lt;br /&gt;
        compress: false&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
That is, we bind 0.0.0.0 (to listen to the whole network for client connections) and remove federation.&lt;br /&gt;
&lt;br /&gt;
We will be even more thorough in disabling federation: search for &amp;lt;code&amp;gt;federation_domain_whitelist&amp;lt;/code&amp;gt;, which (for the Debian 10 configuration file of Synapse 1.23) should be a commented line with a few domains under it. Add a new line after that comment section:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;yaml&amp;quot;&amp;gt;&lt;br /&gt;
federation_domain_whitelist: []&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Next scroll down to &amp;lt;code&amp;gt;federation_ip_range_blacklist&amp;lt;/code&amp;gt; and remove the IPs underneath that line. Replace them with the IPv4 and IPv6 catchalls:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;yaml&amp;quot; line&amp;gt;&lt;br /&gt;
  - &#039;0.0.0.0/0&#039;&lt;br /&gt;
  - &#039;::/0&#039;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
With federation now thoroughly disabled, look for &amp;lt;code&amp;gt;enable_registration: false&amp;lt;/code&amp;gt; and uncomment this line. As in natrius&#039; guide, look for &amp;lt;code&amp;gt;registration_shared_secret: &amp;lt;PRIVATE STRING&amp;gt;&amp;lt;/code&amp;gt;, uncomment it, and replace &amp;lt;code&amp;gt;&amp;lt;PRIVATE STRING&amp;gt;&amp;lt;/code&amp;gt; with a long, randomly generated alphanumeric string, e.g. generated by the command&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
cat /dev/urandom | tr -dc &#039;a-zA-Z0-9&#039; | fold -w 32 | head -n 1&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Write out the configuration file and exit your editor. We now need to open port 8008 to VPN clients:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
iptables -I INPUT 1 -s 192.168.16.0/24 -i tun0 -p tcp -m tcp --dport 8008 -j ACCEPT&lt;br /&gt;
iptables-save &amp;gt;/etc/iptables/rules.v4&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Now start Synapse and check if it is running:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
systemctl start matrix-synapse.service&lt;br /&gt;
ss -plunt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The last command should show that Synapse is listening on port 8008.&lt;br /&gt;
&lt;br /&gt;
You can now create an account on your homeserver:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
register_new_matrix_user -c /etc/matrix-synapse/homeserver.yaml http://localhost:8008&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
This will prompt for a username and password, as well as the shared secret you entered into the configuration file earlier.&lt;br /&gt;
&lt;br /&gt;
If you have a PC or laptop that you&#039;ve given access to the VPN, you can install the &#039;&#039;&#039;desktop&#039;&#039;&#039; [https://element.io/get-started Element] client on it. You should then be able to connect to &amp;lt;code&amp;gt;http://192.168.16.1:8008&amp;lt;/code&amp;gt; using the account you just created. At this moment, the Android and Web clients do NOT support HTTP connections, so these clients cannot connect to your Synapse installation yet. Which brings us to...&lt;br /&gt;
&lt;br /&gt;
=Step 2: Nginx and TLS=&lt;br /&gt;
To allow most clients to connect to your homeserver (provided they have access to the VPN), you will need to set up a reverse proxy that adds a TLS layer so they can communicate over HTTPS. While the Synapse guide by natrius recommends Let&#039;s Encrypt certificates, in a fully private context like we are considering, it makes sense to set up your own certificate authority.&lt;br /&gt;
&lt;br /&gt;
I opted to build a new certificate authority in the Synapse configuration directory and reuse the configuration I picked for OpenVPN:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
make-cadir /etc/matrix-synapse/easy-rsa&lt;br /&gt;
cd /etc/matrix-synapse/easy-rsa&lt;br /&gt;
cp /etc/openvpn/easy-rsa/vars .&lt;br /&gt;
./easyrsa init-pki&lt;br /&gt;
./easyrsa --subject-alt-name=&amp;quot;IP:192.168.16.1&amp;quot; build-ca&lt;br /&gt;
# Choose a new CA name when prompted. The password you are prompted for&lt;br /&gt;
# should ideally also be different from the one you used for OpenVPN.&lt;br /&gt;
./easyrsa --subject-alt-name=&amp;quot;IP:192.168.16.1&amp;quot; build-server-full matrix-synapse nopass&lt;br /&gt;
# For all clients:&lt;br /&gt;
./easyrsa --subject-alt-name=&amp;quot;IP:192.168.16.1&amp;quot; build-client-full alpha.0 nopass&lt;br /&gt;
./easyrsa --subject-alt-name=&amp;quot;IP:192.168.16.1&amp;quot; build-client-full bravo.0 nopass&lt;br /&gt;
# ...and so on&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Note that we are now including &amp;lt;code&amp;gt;--subject-alt-name=&amp;quot;IP:192.168.16.1&amp;quot;&amp;lt;/code&amp;gt; in our commands. HTTPS requires certificates to be bound to a domain or IP, and browsers tend to get ornery if they are not. We do not need Diffie-Hellman parameters now, but we do need to generate a certificate revocation list so Nginx will be able to reject revoked certificates, and we must also export our client certificates in a format browsers and mobile OSes understand:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; list&amp;gt;&lt;br /&gt;
# For all clients:&lt;br /&gt;
./easyrsa export-p12 alpha.0&lt;br /&gt;
./easyrsa export-p12 bravo.0&lt;br /&gt;
# ...and so on&lt;br /&gt;
./easyrsa gen-crl&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
The export step asks you to provide a password, which allows you to securely transport the exported certificates to the clients they are to be installed on.&lt;br /&gt;
&lt;br /&gt;
Next up, Nginx. Install the package, remove the default configuration, and create a new one:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
apt install nginx&lt;br /&gt;
systemctl stop nginx.service&lt;br /&gt;
unlink /etc/nginx/sites-enabled/default&lt;br /&gt;
ln -s /etc/nginx/sites-available/synapse /etc/nginx/sites-enabled/synapse&lt;br /&gt;
editor /etc/nginx/sites-available/synapse&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
An example configuration that sets up an HTTPS reverse proxy on port 10443 is&lt;br /&gt;
given below.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;nginx&amp;quot; line&amp;gt;&lt;br /&gt;
server {&lt;br /&gt;
    listen 10443 ssl;&lt;br /&gt;
    server_name localhost;&lt;br /&gt;
&lt;br /&gt;
    ssl on;&lt;br /&gt;
    ssl_certificate /etc/matrix-synapse/easy-rsa/pki/issued/matrix-synapse.crt;&lt;br /&gt;
    ssl_certificate_key /etc/matrix-synapse/easy-rsa/pki/private/matrix-synapse.key;&lt;br /&gt;
    ssl_client_certificate /etc/matrix-synapse/easy-rsa/pki/ca.crt;&lt;br /&gt;
    ssl_crl /etc/matrix-synapse/easy-rsa/pki/crl.pem;&lt;br /&gt;
    # Enable mutual TLS. This option provides maximal security by requiring&lt;br /&gt;
    # per-client certificates. Unfortunately, client-side support for this&lt;br /&gt;
    # tends to be shaky at best, so turn it off and restart Nginx if you are&lt;br /&gt;
    # not able to connect.&lt;br /&gt;
    ssl_verify_client on;&lt;br /&gt;
&lt;br /&gt;
    access_log /var/log/nginx-synapse-access_log.log;&lt;br /&gt;
    error_log /var/log/nginx-synapse-error_log.log;&lt;br /&gt;
&lt;br /&gt;
    # There is no need to serve files, and we will not be using&lt;br /&gt;
    # .well-known, since we don&#039;t use federation. So we just reverse-proxy&lt;br /&gt;
    # Synapse&#039;s HTTP at the root and that&#039;s it.&lt;br /&gt;
    location / {&lt;br /&gt;
        proxy_pass http://127.0.0.1:8008;&lt;br /&gt;
        proxy_set_header X-Forwarded-For $remote_addr;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Now unblock port 10443 for the VPN and start Nginx:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
iptables -I INPUT 1 -s 192.168.16.0/24 -i tun0 -p tcp -m tcp --dport 10443 -j ACCEPT&lt;br /&gt;
iptables-save &amp;gt;/etc/iptables/rules.v4&lt;br /&gt;
systemctl enable nginx.service&lt;br /&gt;
systemctl start nginx.service&lt;br /&gt;
# Check if it&#039;s running&lt;br /&gt;
ss -plunt&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The last command should show Nginx listening on port 10443. Now copy &amp;lt;code&amp;gt;/etc/matrix-synapse/easy-rsa/pki/ca.crt&amp;lt;/code&amp;gt; to all clients and import it as a trusted system certificate. Also copy each &amp;lt;code&amp;gt;p12&amp;lt;/code&amp;gt; certificate in &amp;lt;code&amp;gt;/etc/matrix-synapse/easy-rsa/pki/private/&amp;lt;/code&amp;gt; to its respective client and import it.&lt;br /&gt;
&lt;br /&gt;
The desktop and android apps should now be able to at least communicate with the server via HTTPS at &amp;lt;code&amp;gt;https://192.168.16.1:10443&amp;lt;/code&amp;gt;. If, however, you get an error stating the client did not send the correct certificate, you may have to disable &amp;lt;code&amp;gt;ssl_verify_client&amp;lt;/code&amp;gt; in the Nginx configuration file and restart Nginx. Note that the canonical Element-web implementation at app.element.io may not be able to talk to your server regardless; I believe this is because browsers will restrict accessing local networks from sites loaded from remote servers. Installing element-web locally will help; see Optional Extras at the end of this guide.&lt;br /&gt;
&lt;br /&gt;
=Step 3: mautrix-whatsapp=&lt;br /&gt;
Being a rather niche package, mautrix-whatsapp is not in the Debian repositories and must be self-compiled. It also has some dependencies on versions of packages too recent to be found in the repositories of Debian 10 (buster)&lt;br /&gt;
&lt;br /&gt;
First and foremost, version 1.14 of Go (&amp;lt;code&amp;gt;golang-1.14&amp;lt;/code&amp;gt;) is needed at the time of writing, which has been backported to buster. If you are running buster, install Go as follows:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
echo &#039;deb http://deb.debian.org/debian buster-backports main&#039; &amp;gt;&amp;gt;/etc/apt/sources.list&lt;br /&gt;
apt update&lt;br /&gt;
apt install -t buster-backports golang&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Alternatively, if you are using Debian 11 (bullseye) or Unstable (sid), simply install golang directly:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
apt install golang&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Regardless of your Debian version, you will need to install &amp;lt;code&amp;gt;ffmpeg&amp;lt;/code&amp;gt;, a GNU toolchain, and &amp;lt;code&amp;gt;git&amp;lt;/code&amp;gt;, if they are not already installed:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
apt install ffmpeg make autoconf automake libtool gcc cmake git&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Next, we need to install a very recent version of &amp;lt;code&amp;gt;olm&amp;lt;/code&amp;gt;, which must be hand-compiled at the time of writing. It is probably best to perform the following steps as a regular user instead of root, but I couldn&#039;t be bothered creating a new user. Caveat emptor.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
cd&lt;br /&gt;
git clone https://gitlab.matrix.org/matrix-org/olm.git&lt;br /&gt;
cd olm&lt;br /&gt;
cmake . -Bbuild&lt;br /&gt;
cd build&lt;br /&gt;
make&lt;br /&gt;
# if you dropped root, use `sudo make install&#039; here instead&lt;br /&gt;
make install&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Since &amp;lt;code&amp;gt;olm&amp;lt;/code&amp;gt; is installed in /usr/local, we&#039;ll need to update the library path. These steps should be executed as root again:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
cat &amp;gt;&amp;gt;/etc/profile.d/librarypath.sh &amp;lt;&amp;lt;EOF&lt;br /&gt;
LD_LIBRARY_PATH=&amp;quot;/usr/local/lib&amp;quot;&lt;br /&gt;
export LD_LIBRARY_PATH&lt;br /&gt;
EOF&lt;br /&gt;
. /etc/profile&lt;br /&gt;
. ~/.bashrc&lt;br /&gt;
echo $LD_LIBRARY_PATH&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The last command should output &amp;lt;code&amp;gt;/usr/local/lib&amp;lt;/code&amp;gt;. If so, we are now ready to compile mautrix-whatsapp. The following steps may be executed as a regular user if you want. Clone the repo and compile:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
cd&lt;br /&gt;
git clone https://github.com/tulir/mautrix-whatsapp.git&lt;br /&gt;
cd mautrix-whatsapp&lt;br /&gt;
./build.sh&lt;br /&gt;
# Only execute the following if you executed the above as a regular user&lt;br /&gt;
cd ..&lt;br /&gt;
sudo cp -r mautrix-whatsapp /root/mautrix-whatsapp&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Again as root, copy &amp;lt;code&amp;gt;/root/mautrix-whatsapp/example-config.yaml&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;/root/mautrix-whatsapp/config.yaml&amp;lt;/code&amp;gt; and edit the latter.&lt;br /&gt;
* Under &amp;lt;code&amp;gt;homeserver&amp;lt;/code&amp;gt;, set &amp;lt;code&amp;gt;address&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;http://localhost:8008&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;domain&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;localhost&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Under &amp;lt;code&amp;gt;appservice&amp;lt;/code&amp;gt;, set &amp;lt;code&amp;gt;hostname&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;127.0.0.1&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Under &amp;lt;code&amp;gt;database&amp;lt;/code&amp;gt; (in the &amp;lt;code&amp;gt;appservice&amp;lt;/code&amp;gt; section), set &amp;lt;code&amp;gt;type&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;postgres&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;uri&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;postgres://synapse:&amp;lt;password&amp;gt;@localhost/synapse?sslmode=disable&amp;lt;/code&amp;gt;, where &amp;lt;code&amp;gt;&amp;lt;password&amp;gt;&amp;lt;/code&amp;gt; should be replaced with the PostgreSQL password you picked earlier.&lt;br /&gt;
* Under &amp;lt;code&amp;gt;bridge&amp;lt;/code&amp;gt;, set &amp;lt;code&amp;gt;private_chat_portal_meta&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;permissions&amp;lt;/code&amp;gt; block under &amp;lt;code&amp;gt;bridge&amp;lt;/code&amp;gt; should be something like this:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;yaml&amp;quot; line&amp;gt;&lt;br /&gt;
    permissions:&lt;br /&gt;
        &amp;quot;*&amp;quot;: relaybot&lt;br /&gt;
        &amp;quot;localhost&amp;quot;: user&lt;br /&gt;
        &amp;quot;@alice:localhost&amp;quot;: admin&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Here &amp;lt;code&amp;gt;alice&amp;lt;/code&amp;gt; should be replaced with the username you chose when issuing the &amp;lt;code&amp;gt;register_new_matrix_user&amp;lt;/code&amp;gt; command in Step 1.&lt;br /&gt;
&lt;br /&gt;
Finally, set &amp;lt;code&amp;gt;directory&amp;lt;/code&amp;gt; under &amp;lt;code&amp;gt;logging&amp;lt;/code&amp;gt; to something more suitable, like &amp;lt;code&amp;gt;/var/log/mautrix-whatsapp&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Now generate the appservice registration file, copy it to the Synapse configuration directory, and change its ownership so Synapse can read it:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
cd /root/mautrix-whatsapp&lt;br /&gt;
./mautrix-whatsapp -g&lt;br /&gt;
cp registration.yaml /etc/matrix-synapse/wa_registration.yaml&lt;br /&gt;
chown matrix-synapse /etc/matrix-synapse/wa_registration.yaml&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Edit &amp;lt;code&amp;gt;/etc/matrix-synapse/homeserver.yaml&amp;lt;/code&amp;gt;. Find the line with &amp;lt;code&amp;gt;app_service_config_files&amp;lt;/code&amp;gt; (which is commented out) and, below the comment, add&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;yaml&amp;quot; line&amp;gt;&lt;br /&gt;
app_service_config_files:&lt;br /&gt;
   - &amp;quot;/etc/matrix-synapse/wa_registration.yaml&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Now restart Synapse:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
systemctl restart matrix-synapse.service&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
It is now time to test mautrix-whatsapp. For testing purposes, start the bridge in the foreground:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
/root/mautrix-whatsapp/mautrix-whatsapp&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Connect to your homeserver with Element-Desktop or Element-Android and start a direct chat with &amp;lt;code&amp;gt;@whatsappbot:localhost&amp;lt;/code&amp;gt;. You should get a message saying the room has been set up as the bridge management/status room. Type &amp;lt;code&amp;gt;help&amp;lt;/code&amp;gt; and send the message. If the bot replies, you&#039;re good; if not, check the logs, and check/adjust your configuration carefully and restart Synapse and mautrix-whatsapp.&lt;br /&gt;
&lt;br /&gt;
If all is working, go back to the terminal where you started &amp;lt;code&amp;gt;/root/mautrix-whatsapp/mautrix-whatsapp&amp;lt;/code&amp;gt; and terminate the process with Ctrl-C. Now create a new file &amp;lt;code&amp;gt;/etc/systemd/system/mautrix-whatsapp.service&amp;lt;/code&amp;gt; and enter the following:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ini&amp;quot; line&amp;gt;&lt;br /&gt;
[Unit]&lt;br /&gt;
Description=WhatsApp to Matrix bridge&lt;br /&gt;
Wants=matrix-synapse.service&lt;br /&gt;
&lt;br /&gt;
[Service]&lt;br /&gt;
Type=exec&lt;br /&gt;
Environment=&amp;quot;LD_LIBRARY_PATH=/usr/local/lib&amp;quot;&lt;br /&gt;
WorkingDirectory=/root/mautrix-whatsapp&lt;br /&gt;
ExecStart=/root/mautrix-whatsapp/mautrix-whatsapp&lt;br /&gt;
Restart=always&lt;br /&gt;
RestartSec=10&lt;br /&gt;
SyslogIdentifier=mautrix-whatsapp&lt;br /&gt;
&lt;br /&gt;
[Install]&lt;br /&gt;
WantedBy=multi-user.target&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You should now be able to start mautrix-whatsapp using systemd:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
systemctl start mautrix-whatsapp.service&lt;br /&gt;
systemctl enable mautrix-whatsapp.service&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Wait a few seconds for mautrix-whatsapp to finish loading, then message &amp;lt;code&amp;gt;help&amp;lt;/code&amp;gt; to your bot again to make sure it still works. If it does, send it &amp;lt;code&amp;gt;login&amp;lt;/code&amp;gt;, and use the actual (official) WhatsApp client to scan the WhatsApp Web QR code that the bot sends you. It should immediately start pulling a few chats, but most likely not your entire history. Ignore it for now. Open your Element client&#039;s settings, go to Help &amp;amp; Advanced, and find your access token. Copy it. Then send the bridge bot &amp;lt;code&amp;gt;login-matrix &amp;lt;access_token&amp;gt;&amp;lt;/code&amp;gt;, replacing &amp;lt;code&amp;gt;&amp;lt;access_token&amp;gt;&amp;lt;/code&amp;gt; with the access token you just copied. This will enable double puppeting; see [https://github.com/tulir/mautrix-whatsapp/wiki/Authentication the Authentication guide].&lt;br /&gt;
&lt;br /&gt;
If you are not content with the meagre amount of chats/messages pulled by the bot, go into each room it created and send &amp;lt;code&amp;gt;!wa delete-portal&amp;lt;/code&amp;gt;. Then go back to your terminal/SSH session and edit &amp;lt;code&amp;gt;/root/mautrix-whatsapp/config.yaml&amp;lt;/code&amp;gt; again. In the &amp;lt;code&amp;gt;bridge&amp;lt;/code&amp;gt; section, edit these settings:&lt;br /&gt;
* &amp;lt;code&amp;gt;initial_chat_sync_count&amp;lt;/code&amp;gt; -- this is the amount of WhatsApp group portals that should be created. You probably want to set this to at least the number of WhatsApp groups you&#039;re in, including &amp;quot;one-on-one&amp;quot; groups for people you&#039;ve direct-messaged. If unsure, just pick a number that&#039;s definitely larger than that.&lt;br /&gt;
* &amp;lt;code&amp;gt;initial_history_fill_count&amp;lt;/code&amp;gt; -- this is the amount of old messages that will be fetched. If you have some very active or old rooms, you will need a very large number here. One caveat: mautrix-whatsapp can get a bit wonky if it&#039;s syncing a large amount of messages, and may need to be killed and restarted a few times for the process to complete. That means you should also be monitoring the sync process.&lt;br /&gt;
* &amp;lt;code&amp;gt;sync_max_chat_age&amp;lt;/code&amp;gt; -- this is the age cutoff in seconds for synced messages. Set it to something like 2000000000 (two billion seconds, or roughly 63 years) to effectively disable the age cutoff.&lt;br /&gt;
* &amp;lt;code&amp;gt;initial_history_disable_notifications&amp;lt;/code&amp;gt; -- this determines whether you get a &amp;quot;new message&amp;quot; notification for every synced old message. If you set &amp;lt;code&amp;gt;initial_history_fill_count&amp;lt;/code&amp;gt; to a large number, do yourself a &#039;&#039;huge&#039;&#039; favour and set this to &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Now restart mautrix-whatsapp:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
systemctl restart mautrix-whatsapp.service&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Finally, send the bridge bot &amp;lt;code&amp;gt;sync --create-all&amp;lt;/code&amp;gt;. It will now start to pull the chats you chose. Bear in mind that this can take a very long time. If you notice it stops making progress, restart mautrix-whatsapp and send &amp;lt;code&amp;gt;sync --create-all&amp;lt;/code&amp;gt; again: it will continue at the point it was stopped.&lt;br /&gt;
&lt;br /&gt;
You are now basically done, although you probably still have WhatsApp running on your phone. The next step will explain how to migrate it to a VM.&lt;br /&gt;
&lt;br /&gt;
=Step 4: migrate WhatsApp proper to a VM=&lt;br /&gt;
I have not performed this step yet myself. However, [https://github.com/tulir/mautrix-whatsapp/wiki/Android-VM-Setup the official guide by &#039;&#039;&#039;Alistair Francis&#039;&#039;&#039;] should be easy enough to follow. I will update this guide when I&#039;ve done this myself.&lt;br /&gt;
&lt;br /&gt;
N.B. in my current setup, WhatsApp runs on a [https://lineageos.org/ LineageOS] phone, and its traffic is piped through the VPN.&lt;br /&gt;
&lt;br /&gt;
=Step 5: optional extras=&lt;br /&gt;
Finally, there are a few things you can do to extend this setup.&lt;br /&gt;
&lt;br /&gt;
==Redirect port 443 to OpenVPN==&lt;br /&gt;
Some public Wi-Fi networks only allow access to a very restricted set of ports so clients are limited to web-browsing and checking email. This annoying bit of shit-tier network administration is easily circumvented by piping your traffic through a VPN, which, of course, you just set up -- but you have to be able to connect to the VPN in the first place, and port 1194 (OpenVPN&#039;s default port) is practically guaranteed to be blocked by such networks. Luckily, the default HTTPS port, TCP 443, is practically guaranteed to NOT be blocked, so if your OpenVPN server is using TCP, you can just redirect traffic from 443 to 1194 and log in to your VPN on basically any network. &#039;&#039;N.B. switch your OpenVPN over to TCP if you want to do this but chose UDP earlier.&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
A few commands are all that&#039;s needed:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
iptables -A PREROUTING -i eth0 -p tcp -m tcp --dport 443 -j REDIRECT --to-ports 1194&lt;br /&gt;
iptables -I INPUT 1 -p tcp -m tcp --dport 443 -j ACCEPT&lt;br /&gt;
iptables-save &amp;gt;/etc/iptables/rules.v4&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You can now use &amp;lt;code&amp;gt;remote 123.123.123.123 443&amp;lt;/code&amp;gt; in your client configuration files. Also be sure to add &amp;lt;code&amp;gt;redirect-gateway def1&amp;lt;/code&amp;gt; on clients that connect to public Wi-Fi!&lt;br /&gt;
&lt;br /&gt;
==Install element-web==&lt;br /&gt;
The official element-web implementation at app.element.io does not work with a private homeserver, but element-web &#039;&#039;will&#039;&#039; work if you install it on your homeserver. &#039;&#039;&#039;Note that the authors of element-web specifically recommend against running element-web on the same domain as the homeserver.&#039;&#039;&#039; However, as access is limited to the VPN, the risk should be limited as long as only trusted and experienced users are allowed access to the VPN in the first place.&lt;br /&gt;
&lt;br /&gt;
Download the latest release (1.75-rc1 at the time of writing; replace the version as necessary in what follows, obviously) from [https://github.com/vector-im/element-web/releases], extract it, move it to the proper place, and create the config file:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
wget https://github.com/vector-im/element-web/releases/download/v1.7.15-rc.1/element-v1.7.15-rc.1.tar.gz&lt;br /&gt;
tar -xvzf element-v1.7.15-rc.1.tar.gz&lt;br /&gt;
mv element-v1.7.15-rc.1 /var/www/element-web&lt;br /&gt;
cd /var/www/element-web&lt;br /&gt;
cp config.sample.json config.json&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Edit &amp;lt;code&amp;gt;config.json&amp;lt;/code&amp;gt;. Set &amp;lt;code&amp;gt;&amp;quot;base_url&amp;quot;&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;&amp;quot;https://192.168.16.1:10443&amp;quot;&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;&amp;quot;server_name&amp;quot;&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;&amp;quot;localhost&amp;quot;&amp;lt;/code&amp;gt; in the &amp;lt;code&amp;gt;&amp;quot;m.homeserver&amp;quot;&amp;lt;/code&amp;gt; block near the top. In the root block, set &amp;lt;code&amp;gt;&amp;quot;disable_custom_urls&amp;quot;&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;&amp;quot;default_federate&amp;quot;&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;false&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Since we&#039;ve already set up Nginx, we can just tell it to serve element-web as well, and we can even reuse the certificate authority. Edit the configuration file you created earlier, &amp;lt;code&amp;gt;/etc/nginx/sites-available/synapse&amp;lt;/code&amp;gt; and add the following block at the end:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;nginx&amp;quot; line&amp;gt;&lt;br /&gt;
server {&lt;br /&gt;
    listen 10444 ssl;&lt;br /&gt;
    server_name localhost;&lt;br /&gt;
&lt;br /&gt;
    root /var/www/element-web;&lt;br /&gt;
    index index.html;&lt;br /&gt;
&lt;br /&gt;
    ssl on;&lt;br /&gt;
    ssl_certificate /etc/matrix-synapse/easy-rsa/pki/issued/element-web.crt;&lt;br /&gt;
    ssl_certificate_key /etc/matrix-synapse/easy-rsa/pki/private/element-web.key;&lt;br /&gt;
    ssl_client_certificate /etc/matrix-synapse/easy-rsa/pki/ca.crt;&lt;br /&gt;
    ssl_crl /etc/matrix-synapse/easy-rsa/pki/crl.pem;&lt;br /&gt;
    ssl_verify_client on;&lt;br /&gt;
&lt;br /&gt;
    access_log /var/log/nginx-element-web-access_log.log;&lt;br /&gt;
    error_log /var/log/nginx-element-web-error_log.log;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
If you did not manage to get &amp;lt;code&amp;gt;ssl_verify_client&amp;lt;/code&amp;gt; to work earlier, remove that line here too. Also, if you did not use port 443 for OpenVPN, you may use &amp;lt;code&amp;gt;listen 443 ssl;&amp;lt;/code&amp;gt; instead of &amp;lt;code&amp;gt;listen 10444 ssl;&amp;lt;/code&amp;gt; if you want (so you can just point your browser at https://192.168.16.1). Finally, add an iptables rule to allow connecting from within the VPN, and restart Nginx:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
# Replace 10444 with 443 if necessary&lt;br /&gt;
iptables -I INPUT 1 -s 192.168.16.0/24 -i tun0 -p tcp -m tcp --dport 10444 -j ACCEPT&lt;br /&gt;
iptables-save &amp;gt;/etc/iptables/rules.v4&lt;br /&gt;
systemctl restart nginx.service&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Block Facebook on clients==&lt;br /&gt;
On all clients (except the phone running WhatsApp, if you have not moved it to a VM on the server yet), you can now completely block all Facebook-owned IPs and still retain access to WhatsApp using your Matrix homeserver. While tedious to do by hand (considering Facebook&#039;s IP pool grows like an aggressive tumour), it is easily automated on Linux with a cron job and iptables.&lt;br /&gt;
&lt;br /&gt;
First of all, create a new iptables chain that will be used for automated IP blocking, and add it to the front of your INPUT and OUTPUT chains:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
iptables -N BLOCK&lt;br /&gt;
iptables -I INPUT 1 -j BLOCK&lt;br /&gt;
iptables -I OUTPUT 1 -j BLOCK&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Next, we will create a script that finds the list of IPs owned by Facebook and adds them to the BLOCK chain. N.B. this requires &amp;lt;code&amp;gt;whois&amp;lt;/code&amp;gt;, which should be available in any package manager under that name, so install it first if necessary. Then create the file &amp;lt;code&amp;gt;/etc/cron.hourly/ipblock.sh&amp;lt;/code&amp;gt; with the following contents:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
iptables=/sbin/iptables&lt;br /&gt;
&lt;br /&gt;
$iptables -F BLOCK&lt;br /&gt;
&lt;br /&gt;
asns=&amp;quot;AS32934&amp;quot;&lt;br /&gt;
for asn in asns&lt;br /&gt;
do&lt;br /&gt;
    ips=&amp;quot;$(whois -h whois.radb.net -- -i origin -T route $asn | grep route: | awk &#039;{print $2}&#039;)&amp;quot;&lt;br /&gt;
    for i in $ips&lt;br /&gt;
    do&lt;br /&gt;
        $iptables -A BLOCK -d $i -j REJECT --reject-with icmp-host-unreachable&lt;br /&gt;
        $iptables -A BLOCK -s $i -j REJECT --reject-with icmp-host-unreachable&lt;br /&gt;
    done&lt;br /&gt;
done&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This script queries whois.radb.net to find all IPs registered under the [https://en.wikipedia.org/wiki/Autonomous_system_(Internet) autonomous system number] AS32934, which is Facebook. You may add other ASNs too if you want. Note that although the list &amp;quot;only&amp;quot; contains 200 entries or so (at the time of writing), they include mask bits in CIDR notation: this list in fact represents tens of thousands of IP addresses, all owned by Facebook.&lt;br /&gt;
&lt;br /&gt;
Make sure to make the script executable, and run it once if you don&#039;t want to wait until the next hour for cron to do it.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot; line&amp;gt;&lt;br /&gt;
chmod +x /etc/cron.hourly/ipblock.sh&lt;br /&gt;
/etc/cron.hourly/ipblock.sh&lt;br /&gt;
iptables-save &amp;gt;/etc/iptables/rules.v4&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Honourable mention: Pi-hole==&lt;br /&gt;
This is an honourable mention because I will not describe the installation process here and it is not related to mautrix-whatsapp. Nevertheless, as following this guide leaves you with a perfectly usable VPN server, and people reading this are likely to be privacy-conscious, I feel it is worthwhile to link [https://pi-hole.net/ Pi-hole] here. Pi-hole is a very powerful ad-blocking framework acting at DNS level. Like Synapse, you can configure Pi-hole to only be accessible to clients inside your VPN (by allowing only connections from 192.168.16.0/24 on device tun0). On your VPN clients, you can set 192.168.16.1 as your primary DNS server, so all clients are protected from malicious ads when connected to the VPN.&lt;br /&gt;
&lt;br /&gt;
==Honourable mention: Tor proxy==&lt;br /&gt;
Your VPS can also serve as a VPN-wide proxy that sends traffic through [https://www.torproject.org/ Tor]. [https://www.marcus-povey.co.uk/2016/03/24/using-tor-as-a-http-proxy/ &#039;&#039;&#039;Marcus Povey&#039;&#039;&#039; has created a guide on how to set up a Tor proxy with polipo.]&lt;br /&gt;
&lt;br /&gt;
Considering Tor&#039;s abysmal transfer speed, you probably don&#039;t &#039;&#039;always&#039;&#039; want to redirect traffic over Tor. Most browsers support [https://en.wikipedia.org/wiki/Proxy_auto-config proxy auto-config] (PAC), which allows you to fine-tune which sites get piped through the proxy. For example, the following PAC uses the Tor proxy to handle [https://en.wikipedia.org/wiki/.onion .onion] domains:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;javascript&amp;quot; line&amp;gt;&lt;br /&gt;
function FindProxyForURL (url, host)&lt;br /&gt;
{&lt;br /&gt;
    if (shExpMatch (host, &amp;quot;*.onion&amp;quot;))&lt;br /&gt;
    {&lt;br /&gt;
        return &amp;quot;PROXY 192.168.16.1:8123&amp;quot;&lt;br /&gt;
    }&lt;br /&gt;
    return &amp;quot;DIRECT;&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Much more advanced configurations are possible with proxy auto-config, e.g. redirecting all HTTP traffic but not HTTPS, whitelisting/blacklisting domains.&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=File:Mautrix-whatsapp-setup-pathified.svg&amp;diff=232</id>
		<title>File:Mautrix-whatsapp-setup-pathified.svg</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=File:Mautrix-whatsapp-setup-pathified.svg&amp;diff=232"/>
		<updated>2020-12-12T09:50:54Z</updated>

		<summary type="html">&lt;p&gt;Link: Link uploaded a new version of File:Mautrix-whatsapp-setup-pathified.svg&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Summary ==&lt;br /&gt;
Graphical representation of a possible mautrix-whatsapp setup in a VPN.&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=File:Mautrix-whatsapp-setup-pathified.svg&amp;diff=231</id>
		<title>File:Mautrix-whatsapp-setup-pathified.svg</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=File:Mautrix-whatsapp-setup-pathified.svg&amp;diff=231"/>
		<updated>2020-12-11T17:50:15Z</updated>

		<summary type="html">&lt;p&gt;Link: Graphical representation of a possible mautrix-whatsapp setup in a VPN.&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Summary ==&lt;br /&gt;
Graphical representation of a possible mautrix-whatsapp setup in a VPN.&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=Main_Page&amp;diff=222</id>
		<title>Main Page</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=Main_Page&amp;diff=222"/>
		<updated>2017-01-04T08:57:29Z</updated>

		<summary type="html">&lt;p&gt;Link: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Welcome to &#039;&#039;&#039;Penguin Development&#039;&#039;&#039;! We make [[w:free and open-source software|free and open-source software]] and [[w:Open-source hardware|hardware]].&lt;br /&gt;
&lt;br /&gt;
See below for current releases. All software and hardware is licensed under the [https://gnu.org/licenses/gpl.html GPL] (version 3 or higher) unless otherwise stated.&lt;br /&gt;
&lt;br /&gt;
=Software=&lt;br /&gt;
* [[AUTOMOME]] — the OTTish AUTOMEME clone&lt;br /&gt;
* [[Ising]] — Monte Carlo Ising model simulator&lt;br /&gt;
* [[stackermann]] — a stack-based [[w:Ackermann function|Ackermann function]] calculator&lt;br /&gt;
&lt;br /&gt;
=Hardware=&lt;br /&gt;
* [[timelapse]] — time-lapse trigger for digital cameras&lt;br /&gt;
&lt;br /&gt;
=Code snippets and how-tos=&lt;br /&gt;
* [[Animated 3D plotting with Blender]]&lt;br /&gt;
&lt;br /&gt;
=Other=&lt;br /&gt;
* [[LineageOS builds|Unofficial LineageOS builds]]&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=LineageOS_builds&amp;diff=221</id>
		<title>LineageOS builds</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=LineageOS_builds&amp;diff=221"/>
		<updated>2017-01-04T08:56:37Z</updated>

		<summary type="html">&lt;p&gt;Link: Created page with &amp;quot;[http://lineageos.org/ LineageOS] is a gratis and mostly free-as-in-freedom mobile operating system based on Android. It is a fork of the now-discontinued CyanogenMod. Until t...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[http://lineageos.org/ LineageOS] is a gratis and mostly free-as-in-freedom mobile operating system based on Android. It is a fork of the now-discontinued CyanogenMod. Until the automatic build infrastructure is back online, unofficial builds for devices [[User:Link|I]] own will be available here.&lt;br /&gt;
&lt;br /&gt;
=Disclaimer=&lt;br /&gt;
All builds that are found here are UNOFFICIAL and UNSUPPORTED. If you wish to use them, you must do so AT YOUR OWN RISK. No instructions on flashing ROMs will be provided here; these can be found on the [https://web.archive.org/web/20161224215208/https://wiki.cyanogenmod.org/w/Main_Page archived CyanogenMod Wiki]. Penguin Development cannot be held accountable if you brick your system! You and you alone are responsible for the consequences of flashing a custom ROM on your device.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;LineageOS is &#039;&#039;not&#039;&#039; a Penguin Development project.&#039;&#039;&#039; The builds available here are simply compiled versions of the source code from the [https://github.com/lineageos LineageOS github repository], without modifications (unless specified). The purpose of this page is simply to save you the trouble of setting up a 100GB dev environment and using hours of CPU time to compile the source. For help in using the ROMs, see e.g. the archived CyanogenMod Wiki or [https://www.xda-developers.com/ XDA-developers]. Report bugs to the LineageOS maintainers directly.&lt;br /&gt;
&lt;br /&gt;
=Devices=&lt;br /&gt;
[http://proj.penguindevelopment.org/lineage/htc/m7 HTC One m7]&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=Main_Page&amp;diff=220</id>
		<title>Main Page</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=Main_Page&amp;diff=220"/>
		<updated>2016-11-10T10:06:28Z</updated>

		<summary type="html">&lt;p&gt;Link: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Welcome to &#039;&#039;&#039;Penguin Development&#039;&#039;&#039;! We make [[w:free and open-source software|free and open-source software]] and [[w:Open-source hardware|hardware]].&lt;br /&gt;
&lt;br /&gt;
See below for current releases. All software and hardware is licensed under the [https://gnu.org/licenses/gpl.html GPL] (version 3 or higher) unless otherwise stated.&lt;br /&gt;
&lt;br /&gt;
=Software=&lt;br /&gt;
* [[AUTOMOME]] — the OTTish AUTOMEME clone&lt;br /&gt;
* [[Ising]] — Monte Carlo Ising model simulator&lt;br /&gt;
* [[stackermann]] — a stack-based [[w:Ackermann function|Ackermann function]] calculator&lt;br /&gt;
&lt;br /&gt;
=Hardware=&lt;br /&gt;
* [[timelapse]] — time-lapse trigger for digital cameras&lt;br /&gt;
&lt;br /&gt;
=Code snippets and how-tos=&lt;br /&gt;
* [[Animated 3D plotting with Blender]]&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=Animated_3D_plotting_with_Blender&amp;diff=219</id>
		<title>Animated 3D plotting with Blender</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=Animated_3D_plotting_with_Blender&amp;diff=219"/>
		<updated>2016-11-09T15:15:40Z</updated>

		<summary type="html">&lt;p&gt;Link: /* Output video */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[https://www.blender.org/ Blender] is an extremely versatile 3D creation and animation suite. Since it is fully scriptable in [https://www.python.org/ Python], Blender may be used to generate &#039;&#039;&#039;animated 3-dimensional plots&#039;&#039;&#039; of data or mathematical functions. Below is an example of one way to generate such a plot.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=Important information=&lt;br /&gt;
The script below has been tested with Blender 2.78a for Linux. It should generally be compatible with other versions and operating systems. The script works with the pristine Blender startup file; heavily modified startup files may break certain assumptions.&lt;br /&gt;
&lt;br /&gt;
To run the script, simply call &amp;lt;code&amp;gt;blender --python blenderplot-ani.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To create the plot in a different base file, instead use &amp;lt;code&amp;gt;blender basefile.blend --python blenderplot-ani.py&amp;lt;/code&amp;gt;.&lt;br /&gt;
The order of arguments matters here.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Note that the animation data will not be saved with your .blend file!&#039;&#039;&#039; This means you must run the Python script on a pristine &amp;quot;background&amp;quot; .blend file each time you wish to examine the geometry or create a render. If the .blend file is saved, only the first frame will be captured.&lt;br /&gt;
&lt;br /&gt;
=The script=&lt;br /&gt;
Code for &amp;lt;code&amp;gt;blenderplot-ani.py&amp;lt;/code&amp;gt; follows.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot; line&amp;gt;&lt;br /&gt;
#!/bin/true&lt;br /&gt;
# vim: se fo=tcroq tw=78 :&lt;br /&gt;
# Simple animated 3D plot example using Blender ( https://www.blender.org/ ).&lt;br /&gt;
# Given the function f(k, x, t)=exp(ikx-iωt), plots Re(f) against x and k,&lt;br /&gt;
# with colour given by Im(f) and t being the time.&lt;br /&gt;
#&lt;br /&gt;
# For pedagogical purposes, this just computes f at each frame (twice: once&lt;br /&gt;
# for the vertex positions and once for their colours). This is horribly&lt;br /&gt;
# inefficient; it would be much better to generate a 3D array for f(x, k, t)&lt;br /&gt;
# once and slice this array for each frame -- however, this is left as an&lt;br /&gt;
# exercise to the reader.&lt;br /&gt;
#&lt;br /&gt;
# To run, call&lt;br /&gt;
#   blender --python blenderplot-ani.py&lt;br /&gt;
&lt;br /&gt;
import os.path&lt;br /&gt;
&lt;br /&gt;
import numpy as np&lt;br /&gt;
&lt;br /&gt;
import bpy&lt;br /&gt;
&lt;br /&gt;
### Begin user settings&lt;br /&gt;
omega = 1&lt;br /&gt;
font = &#039;/usr/share/fonts/cm-unicode/cmunti.ttf&#039; # Must be a unicode font!&lt;br /&gt;
### End user settings&lt;br /&gt;
&lt;br /&gt;
### Begin generic Blender rendering code&lt;br /&gt;
# Global object counter.&lt;br /&gt;
obj_ind = 10000&lt;br /&gt;
&lt;br /&gt;
plot_id = None&lt;br /&gt;
&lt;br /&gt;
line_material = bpy.data.materials.new(&#039;line&#039;)&lt;br /&gt;
line_material.diffuse_color = (0, 0, 0)&lt;br /&gt;
line_material.diffuse_shader = &#039;LAMBERT&#039;&lt;br /&gt;
line_material.specular_color = (0, 0, 0)&lt;br /&gt;
line_material.specular_shader = &#039;COOKTORR&#039;&lt;br /&gt;
line_material.use_shadows = False&lt;br /&gt;
line_material.use_cast_shadows = False&lt;br /&gt;
line_material.use_raytrace = True&lt;br /&gt;
line_material.ambient = 0&lt;br /&gt;
&lt;br /&gt;
text_material = bpy.data.materials.new(&#039;text&#039;)&lt;br /&gt;
text_material.diffuse_color = (.15, .05, .035)&lt;br /&gt;
text_material.diffuse_shader = &#039;OREN_NAYAR&#039;&lt;br /&gt;
text_material.diffuse_intensity = .9&lt;br /&gt;
text_material.roughness = 2&lt;br /&gt;
text_material.specular_color = (.6, .2, .1)&lt;br /&gt;
text_material.specular_shader = &#039;PHONG&#039;&lt;br /&gt;
text_material.specular_hardness = 80&lt;br /&gt;
text_material.specular_intensity = .85&lt;br /&gt;
text_material.use_shadows = True&lt;br /&gt;
text_material.use_cast_shadows = False&lt;br /&gt;
text_material.use_raytrace = True&lt;br /&gt;
text_material.raytrace_mirror.use = True&lt;br /&gt;
text_material.mirror_color = (.7, .3, .15)&lt;br /&gt;
text_material.raytrace_mirror.reflect_factor = .3&lt;br /&gt;
text_material.emit = 0&lt;br /&gt;
text_material.ambient = 0&lt;br /&gt;
&lt;br /&gt;
plot_material = bpy.data.materials.new(&#039;plot&#039;)&lt;br /&gt;
plot_material.specular_color = (.5, .5, .5)&lt;br /&gt;
plot_material.specular_shader = &#039;COOKTORR&#039;&lt;br /&gt;
plot_material.specular_intensity = .2&lt;br /&gt;
plot_material.use_shadows = True&lt;br /&gt;
plot_material.use_transparent_shadows = True&lt;br /&gt;
plot_material.use_raytrace = True&lt;br /&gt;
plot_material.use_transparency = True&lt;br /&gt;
plot_material.transparency_method = &#039;RAYTRACE&#039;&lt;br /&gt;
plot_material.alpha = .95&lt;br /&gt;
plot_material.specular_alpha = 1&lt;br /&gt;
plot_material.raytrace_transparency.depth = 5&lt;br /&gt;
plot_material.use_vertex_color_paint = True&lt;br /&gt;
&lt;br /&gt;
text_font = bpy.data.fonts.load(os.path.abspath(os.path.expanduser(font)))&lt;br /&gt;
&lt;br /&gt;
def heatmap(heat):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Heat map: given a &amp;quot;heat&amp;quot; between 0 and 1, return a tuple of RGB&lt;br /&gt;
    values.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    r = np.max((2*heat-1., 0))&lt;br /&gt;
    b = np.max((1.-2*heat, 0))&lt;br /&gt;
    return (r, 1.-r-b, b)&lt;br /&gt;
&lt;br /&gt;
def zheat(z, zmin, zmax, **kwargs):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Colour a vertex based on its z-height compared to the minimum and&lt;br /&gt;
    maximum z-values that occur.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    return heatmap((z-zmin)/(zmax-zmin if zmin != zmax else 1))&lt;br /&gt;
&lt;br /&gt;
def plot_function(x, y, func, auto_axes = True, xmarks=None,&lt;br /&gt;
        ymarks = None, zmarks = None, labels = None, thickness = 0.025,&lt;br /&gt;
        text_rot = None, colourfunc=zheat, zmin = None, zmax = None):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Plot the function (lambda) func of x and y. The resulting surface is&lt;br /&gt;
    smooth-shaded. Vertices may be coloured according to colourfunc: this is a&lt;br /&gt;
    function that accepts the following parameters and returns an RGB-tuple:&lt;br /&gt;
        x, y, z, xmin, xmax, ymin, ymax, zmin, zmax&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    global obj_ind, plot_id&lt;br /&gt;
&lt;br /&gt;
    ids = {&lt;br /&gt;
        &#039;axes&#039;: [],&lt;br /&gt;
        &#039;axis_labels&#039;: [],&lt;br /&gt;
        &#039;xmarks&#039;: [],&lt;br /&gt;
        &#039;ymarks&#039;: [],&lt;br /&gt;
        &#039;zmarks&#039;: [],&lt;br /&gt;
        &#039;xlabels&#039;: [],&lt;br /&gt;
        &#039;ylabels&#039;: [],&lt;br /&gt;
        &#039;zlabels&#039;: []&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    if text_rot is None:&lt;br /&gt;
        text_rot = np.array((0, 0, 0))&lt;br /&gt;
&lt;br /&gt;
    if plot_id is None:&lt;br /&gt;
        obj_id = &#039;plot_{}&#039;.format(obj_ind)&lt;br /&gt;
        obj_ind += 1&lt;br /&gt;
        # Generate all vertices in the plot at z = 0&lt;br /&gt;
        verts = [(i, j, 0) for i in x for j in y]&lt;br /&gt;
        faces = []&lt;br /&gt;
        count = 0&lt;br /&gt;
        # Build faces from the vertices&lt;br /&gt;
        for i in range(len(y)*(len(x)-1)):&lt;br /&gt;
            if count &amp;lt; len(y)-1:&lt;br /&gt;
                faces.append((i, i+1, i+len(y)+1, i+len(y)))&lt;br /&gt;
                count += 1&lt;br /&gt;
            else:&lt;br /&gt;
                count = 0&lt;br /&gt;
&lt;br /&gt;
        # Create a mesh and an object at the origin&lt;br /&gt;
        mesh = bpy.data.meshes.new(obj_id)&lt;br /&gt;
        obj = bpy.data.objects.new(obj_id, mesh)&lt;br /&gt;
        obj.location = (0, 0, 0)&lt;br /&gt;
        bpy.context.scene.objects.link(obj)&lt;br /&gt;
        mesh.from_pydata(verts, [], faces)&lt;br /&gt;
        mesh.update(calc_edges=True)&lt;br /&gt;
&lt;br /&gt;
        # Create a new vertex colour map&lt;br /&gt;
        colours = obj.data.vertex_colors.new()&lt;br /&gt;
&lt;br /&gt;
        # Set material&lt;br /&gt;
        obj.data.materials.append(plot_material)&lt;br /&gt;
&lt;br /&gt;
        # Smooth-shade polygons&lt;br /&gt;
        for pol in obj.data.polygons:&lt;br /&gt;
            pol.use_smooth = True&lt;br /&gt;
    else:&lt;br /&gt;
        obj = bpy.data.objects[plot_id]&lt;br /&gt;
        colours = obj.data.vertex_colors.active&lt;br /&gt;
&lt;br /&gt;
    verts = obj.data.vertices&lt;br /&gt;
&lt;br /&gt;
    # Move vertices to their correct position&lt;br /&gt;
    for v in verts:&lt;br /&gt;
        v.co.z = func(v.co.x, v.co.y)&lt;br /&gt;
    obj.data.update(calc_edges=True)&lt;br /&gt;
&lt;br /&gt;
    sv = sorted([(v.co.x, v.co.y, v.co.z) for v in verts], key=lambda q: q[2])&lt;br /&gt;
&lt;br /&gt;
    # Colour vertices&lt;br /&gt;
    for pol in obj.data.polygons:&lt;br /&gt;
        for idx in pol.loop_indices:&lt;br /&gt;
            co = obj.data.vertices[obj.data.loops[idx].vertex_index].co&lt;br /&gt;
            colours.data[idx].color = colourfunc(x=co.x, y=co.y, z=co.z,&lt;br /&gt;
                xmin=np.min(x), xmax=np.max(x),&lt;br /&gt;
                ymin=np.min(y), ymax=np.max(y),&lt;br /&gt;
                zmin=sv[0][2], zmax=sv[-1][2])&lt;br /&gt;
&lt;br /&gt;
    if auto_axes and plot_id is None:&lt;br /&gt;
        # Axes&lt;br /&gt;
        ids[&#039;axes&#039;].append(add_line(np.array((min(x), min(y), 0)),&lt;br /&gt;
            np.array((max(x), min(y), 0)), thickness, False))&lt;br /&gt;
        ids[&#039;axes&#039;].append(add_line(np.array((max(x), min(y), 0)),&lt;br /&gt;
            np.array((max(x), max(y), 0)), thickness, False))&lt;br /&gt;
        ids[&#039;axes&#039;].append(add_line(&lt;br /&gt;
            np.array((min(x), min(y), sv[0][2] if zmin is None else zmin)),&lt;br /&gt;
            np.array((min(x), min(y), sv[-1][2] if zmax is None else zmax)),&lt;br /&gt;
            thickness, False))&lt;br /&gt;
        # Axis marks&lt;br /&gt;
        if xmarks is not None:&lt;br /&gt;
            for pos, label in xmarks:&lt;br /&gt;
                p = np.array((pos, min(y), 0))&lt;br /&gt;
                ids[&#039;xmarks&#039;].append(add_line(p,&lt;br /&gt;
                    p-np.array((0, 1.5*thickness, 0)), thickness, False))&lt;br /&gt;
                if label is not None and len(label) &amp;gt; 0:&lt;br /&gt;
                    ids[&#039;xlabels&#039;].append(add_text(&lt;br /&gt;
                        p-np.array((0, 7*thickness, 0)), label,&lt;br /&gt;
                        thickness, text_rot))&lt;br /&gt;
        if ymarks is not None:&lt;br /&gt;
            for pos, label in ymarks:&lt;br /&gt;
                p = np.array((max(x), pos, 0))&lt;br /&gt;
                ids[&#039;ymarks&#039;].append(add_line(p,&lt;br /&gt;
                    p+np.array((1.5*thickness, 0, 0)), thickness, False))&lt;br /&gt;
                if label is not None and len(label) &amp;gt; 0:&lt;br /&gt;
                    ids[ &#039;ylabels&#039;].append(add_text(&lt;br /&gt;
                        p+np.array((7*thickness, 0, 0)), label,&lt;br /&gt;
                        thickness, text_rot))&lt;br /&gt;
        if zmarks is not None:&lt;br /&gt;
            for pos, label in zmarks:&lt;br /&gt;
                p = np.array((min(x), min(y), pos))&lt;br /&gt;
                ids[&#039;zmarks&#039;].append(add_line(p,&lt;br /&gt;
                    p-np.array((1.5*thickness/np.sqrt(2),&lt;br /&gt;
                        1.5*thickness/np.sqrt(2), 0)), thickness, False))&lt;br /&gt;
                if label is not None and len(label) &amp;gt; 0:&lt;br /&gt;
                    ids[&#039;zlabels&#039;].append(add_text(&lt;br /&gt;
                        p-np.array((7*thickness, 7*thickness, 0))/np.sqrt(2),&lt;br /&gt;
                        label, thickness, text_rot))&lt;br /&gt;
&lt;br /&gt;
        # Axis labels&lt;br /&gt;
        if labels is not None:&lt;br /&gt;
            ids[&#039;axis_labels&#039;].append(add_text(&lt;br /&gt;
                np.array((max(x)+8*thickness, min(y), 0)),&lt;br /&gt;
                labels[0], 2*thickness, text_rot))&lt;br /&gt;
            ids[&#039;axis_labels&#039;].append(add_text(&lt;br /&gt;
                np.array((max(x), max(y)+8*thickness, 0)),&lt;br /&gt;
                labels[1], 2*thickness, text_rot))&lt;br /&gt;
            ids[&#039;axis_labels&#039;].append(add_text(&lt;br /&gt;
                np.array((min(x), min(y),&lt;br /&gt;
                    (sv[-1][2] if zmax is None else zmax) + 8*thickness)),&lt;br /&gt;
                labels[2], 2*thickness, text_rot))&lt;br /&gt;
&lt;br /&gt;
    if plot_id is None:&lt;br /&gt;
        plot_id = obj_id&lt;br /&gt;
&lt;br /&gt;
    ids[&#039;plot&#039;] = plot_id&lt;br /&gt;
&lt;br /&gt;
    return ids&lt;br /&gt;
&lt;br /&gt;
def add_text(r, text, size=0.025, rotation=None):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Add text at the position r. Size is a relative parameter; use&lt;br /&gt;
    trial-and-error here.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    global obj_ind&lt;br /&gt;
    obj_id = &#039;text_{}&#039;.format(obj_ind)&lt;br /&gt;
    obj_ind += 1&lt;br /&gt;
    rot = [np.pi/2, 0, 0] if rotation is None else rotation.tolist()&lt;br /&gt;
    bpy.ops.object.text_add(location=r.tolist(), rotation=rot)&lt;br /&gt;
    obj = bpy.context.active_object&lt;br /&gt;
    obj.name = obj_id&lt;br /&gt;
    obj.data.name = obj_id&lt;br /&gt;
    obj.data.body = text&lt;br /&gt;
&lt;br /&gt;
    # Set the font&lt;br /&gt;
    obj.data.font = text_font&lt;br /&gt;
&lt;br /&gt;
    obj.data.offset_x = -2*size&lt;br /&gt;
    obj.data.offset_y = -2*size&lt;br /&gt;
    obj.data.shear = 0.0&lt;br /&gt;
    obj.data.size = 8*size&lt;br /&gt;
    obj.data.space_character = 1&lt;br /&gt;
    obj.data.space_word = 4*size&lt;br /&gt;
    obj.data.extrude = size/3&lt;br /&gt;
&lt;br /&gt;
    obj.data.materials.append(text_material)&lt;br /&gt;
&lt;br /&gt;
    return obj_id&lt;br /&gt;
&lt;br /&gt;
def add_line(r1, r2, w=0.01, rel_w=True):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Add a &amp;quot;line&amp;quot; (cylinder) between the points r1 and r2. The width is&lt;br /&gt;
    either w (if rel_w is False) or w*|r2-r1| (if rel_w is True).&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    global obj_ind&lt;br /&gt;
    obj_id = &#039;line_{}&#039;.format(obj_ind)&lt;br /&gt;
    obj_ind += 1&lt;br /&gt;
    rc = (r2+r1)/2 # Centroid&lt;br /&gt;
    rr = r2-rc # Position of r2 relative to centroid&lt;br /&gt;
    r = np.sqrt(np.sum(rr**2))&lt;br /&gt;
    theta = np.arccos(rr[2]/r)&lt;br /&gt;
    phi = np.arctan2(rr[1], rr[0])&lt;br /&gt;
    bpy.ops.mesh.primitive_cylinder_add(vertices=16,&lt;br /&gt;
            radius=.5*w*(r if rel_w else 1), depth=2*r,&lt;br /&gt;
            location=rc.tolist(), rotation=(0, theta, phi))&lt;br /&gt;
    obj = bpy.context.active_object&lt;br /&gt;
    obj.name = obj_id&lt;br /&gt;
    for pol in obj.data.polygons:&lt;br /&gt;
        pol.use_smooth = True&lt;br /&gt;
    obj.data.materials.append(line_material)&lt;br /&gt;
&lt;br /&gt;
    return obj_id&lt;br /&gt;
### End generic Blender rendering code&lt;br /&gt;
&lt;br /&gt;
def frame_change(scene):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Update the plot for a given frame.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    frame = min(max(scene.frame_current, 0), n_frames - 1)&lt;br /&gt;
    plot_function(x, k, funcgen(t[frame]), colourfunc=colgen(t[frame]))&lt;br /&gt;
    # Update the t-indicator&lt;br /&gt;
    bpy.data.objects[ttext_id].data.body = &#039;t = {: &amp;gt;4.3f}&#039;.format(t[frame])&lt;br /&gt;
&lt;br /&gt;
if __name__ == &#039;__main__&#039;:&lt;br /&gt;
    # Set up x, y and t data&lt;br /&gt;
    n_frames = 51&lt;br /&gt;
    nx = 101&lt;br /&gt;
    nk = 101&lt;br /&gt;
    xscale = 1/2&lt;br /&gt;
    kscale = 1/3&lt;br /&gt;
    zscale = 2&lt;br /&gt;
    x = np.linspace(0, 10, nx)*xscale&lt;br /&gt;
    k = np.linspace(0, 4*np.pi, nk)*kscale&lt;br /&gt;
    t = np.linspace(0, 10, n_frames)&lt;br /&gt;
&lt;br /&gt;
    # Function to plot&lt;br /&gt;
    func = lambda x, k, t: np.exp(1j*(k*x-omega*t))*zscale&lt;br /&gt;
    # Generator for plottable function f(x, k) at time t&lt;br /&gt;
    funcgen = lambda t: lambda x, k: np.real(func(x, k, t))&lt;br /&gt;
    # Colour generator&lt;br /&gt;
    colgen = lambda t: lambda x, y, **kwargs: \&lt;br /&gt;
            heatmap((np.imag(func(x, y, t))+1)/2)&lt;br /&gt;
&lt;br /&gt;
    # Absolute z range for axes&lt;br /&gt;
    azmin = -1*zscale&lt;br /&gt;
    azmax = 1*zscale&lt;br /&gt;
&lt;br /&gt;
    # Axis marks&lt;br /&gt;
    xmarks = [(i*xscale, str(i)) for i in range(1, 11)]&lt;br /&gt;
    kmarks = [(j*np.pi/2*kscale, &#039;{}π/2&#039;.format(j if j != 1 else &#039;&#039;) \&lt;br /&gt;
        if j % 2 == 1 else &#039;{}π&#039;.format(j // 2 if j != 2 else &#039;&#039;)) \&lt;br /&gt;
        for j in range(1, 9)]&lt;br /&gt;
    zmarks = [(z/10*zscale, str(z/10)) for z in range(-10, 12, 2)]&lt;br /&gt;
&lt;br /&gt;
    # Hide the 吸牛 splash screen&lt;br /&gt;
    bpy.context.user_preferences.view.show_splash = False&lt;br /&gt;
&lt;br /&gt;
    # Remove existing meshes&lt;br /&gt;
    for item in bpy.context.scene.objects:&lt;br /&gt;
        if item.type == &#039;MESH&#039;:&lt;br /&gt;
            bpy.context.scene.objects.unlink(item)&lt;br /&gt;
    for item in bpy.data.objects:&lt;br /&gt;
        if item.type == &#039;MESH&#039;:&lt;br /&gt;
            bpy.data.objects.remove(item)&lt;br /&gt;
    for item in bpy.data.meshes:&lt;br /&gt;
        bpy.data.meshes.remove(item)&lt;br /&gt;
&lt;br /&gt;
    # Set the camera position&lt;br /&gt;
    bpy.data.objects[&#039;Camera&#039;].location = (11, -6, 5.5)&lt;br /&gt;
    bpy.data.objects[&#039;Camera&#039;].rotation_euler = (1.1, 0, 0.8)&lt;br /&gt;
&lt;br /&gt;
    # Let all texts face the camera&lt;br /&gt;
    rot = np.array(bpy.data.objects[&#039;Camera&#039;].rotation_euler)&lt;br /&gt;
&lt;br /&gt;
    # Initial t=0 plot; this also sets up the axes.&lt;br /&gt;
    print(plot_function(x, k, funcgen(0),&lt;br /&gt;
        True, labels=(&#039;x&#039;, &#039;k&#039;, &#039;z&#039;), text_rot=rot, xmarks=xmarks,&lt;br /&gt;
        ymarks=kmarks, zmarks=zmarks, zmin=azmin, zmax=azmax,&lt;br /&gt;
        colourfunc=colgen(0)))&lt;br /&gt;
&lt;br /&gt;
    # Text label indicating current time&lt;br /&gt;
    ttext_id = add_text(np.array((5.6, -1.2, 0)), &#039;t = {: &amp;gt;4.2f}&#039;.format(0),&lt;br /&gt;
        size=0.05, rotation=rot)&lt;br /&gt;
&lt;br /&gt;
    # Set min/max/current frame&lt;br /&gt;
    bpy.data.scenes[&#039;Scene&#039;].frame_start = 0&lt;br /&gt;
    bpy.data.scenes[&#039;Scene&#039;].frame_end = n_frames - 1&lt;br /&gt;
    bpy.data.scenes[&#039;Scene&#039;].frame_current = 0&lt;br /&gt;
&lt;br /&gt;
    # Add frame change handler. This is what makes the animation happen!&lt;br /&gt;
    bpy.app.handlers.frame_change_pre.append(frame_change)&lt;br /&gt;
&lt;br /&gt;
    # Add some environment lighting&lt;br /&gt;
    wld = bpy.data.worlds[&#039;World&#039;]&lt;br /&gt;
    wld.light_settings.use_environment_light = True&lt;br /&gt;
    wld.light_settings.environment_energy = .5&lt;br /&gt;
&lt;br /&gt;
    # Add a white backdrop plane&lt;br /&gt;
    plane_material = bpy.data.materials.new(&#039;backdrop&#039;)&lt;br /&gt;
    plane_material.diffuse_color = (1, 1, 1)&lt;br /&gt;
    plane_material.use_shadeless = True&lt;br /&gt;
    bpy.ops.mesh.primitive_plane_add(location=(0, 0, -5))&lt;br /&gt;
    bpy.context.active_object.scale = (50, 50, 0)&lt;br /&gt;
    bpy.context.active_object.data.materials.append(plane_material)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=Output video=&lt;br /&gt;
&amp;lt;html5media height=&amp;quot;270&amp;quot; width=&amp;quot;480&amp;quot;&amp;gt;File:Animated 3D plot.ogv&amp;lt;/html5media&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[File:Animated 3D plot.ogv]] / [http://www.penguindevelopment.org/images/8/88/Animated_3D_plot.ogv Animated_3D_plot.ogv]&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=Main_Page&amp;diff=218</id>
		<title>Main Page</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=Main_Page&amp;diff=218"/>
		<updated>2016-11-09T14:53:12Z</updated>

		<summary type="html">&lt;p&gt;Link: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Welcome to &#039;&#039;&#039;Penguin Development&#039;&#039;&#039;, a (currently one-man) organisation developing [[w:free and open-source software|free and open-source software]] and [[w:Open-source hardware|hardware]].&lt;br /&gt;
&lt;br /&gt;
See below for current releases. All software and hardware is licensed under the [https://gnu.org/licenses/gpl.html GPL] (version 3 or higher) unless otherwise stated.&lt;br /&gt;
&lt;br /&gt;
=Software=&lt;br /&gt;
* [[AUTOMOME]] — the OTTish AUTOMEME clone&lt;br /&gt;
* [[Ising]] — Monte Carlo Ising model simulator&lt;br /&gt;
* [[stackermann]] — a stack-based [[w:Ackermann function|Ackermann function]] calculator&lt;br /&gt;
&lt;br /&gt;
=Hardware=&lt;br /&gt;
* [[timelapse]] — time-lapse trigger for digital cameras&lt;br /&gt;
&lt;br /&gt;
=Code snippets and how-tos=&lt;br /&gt;
* [[Animated 3D plotting with Blender]]&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=Animated_3D_plotting_with_Blender&amp;diff=217</id>
		<title>Animated 3D plotting with Blender</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=Animated_3D_plotting_with_Blender&amp;diff=217"/>
		<updated>2016-11-09T14:52:48Z</updated>

		<summary type="html">&lt;p&gt;Link: Change to first-level headers&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[https://www.blender.org/ Blender] is an extremely versatile 3D creation and animation suite. Since it is fully scriptable in [https://www.python.org/ Python], Blender may be used to generate &#039;&#039;&#039;animated 3-dimensional plots&#039;&#039;&#039; of data or mathematical functions. Below is an example of one way to generate such a plot.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=Important information=&lt;br /&gt;
The script below has been tested with Blender 2.78a for Linux. It should generally be compatible with other versions and operating systems. The script works with the pristine Blender startup file; heavily modified startup files may break certain assumptions.&lt;br /&gt;
&lt;br /&gt;
To run the script, simply call &amp;lt;code&amp;gt;blender --python blenderplot-ani.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To create the plot in a different base file, instead use &amp;lt;code&amp;gt;blender basefile.blend --python blenderplot-ani.py&amp;lt;/code&amp;gt;.&lt;br /&gt;
The order of arguments matters here.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Note that the animation data will not be saved with your .blend file!&#039;&#039;&#039; This means you must run the Python script on a pristine &amp;quot;background&amp;quot; .blend file each time you wish to examine the geometry or create a render. If the .blend file is saved, only the first frame will be captured.&lt;br /&gt;
&lt;br /&gt;
=The script=&lt;br /&gt;
Code for &amp;lt;code&amp;gt;blenderplot-ani.py&amp;lt;/code&amp;gt; follows.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot; line&amp;gt;&lt;br /&gt;
#!/bin/true&lt;br /&gt;
# vim: se fo=tcroq tw=78 :&lt;br /&gt;
# Simple animated 3D plot example using Blender ( https://www.blender.org/ ).&lt;br /&gt;
# Given the function f(k, x, t)=exp(ikx-iωt), plots Re(f) against x and k,&lt;br /&gt;
# with colour given by Im(f) and t being the time.&lt;br /&gt;
#&lt;br /&gt;
# For pedagogical purposes, this just computes f at each frame (twice: once&lt;br /&gt;
# for the vertex positions and once for their colours). This is horribly&lt;br /&gt;
# inefficient; it would be much better to generate a 3D array for f(x, k, t)&lt;br /&gt;
# once and slice this array for each frame -- however, this is left as an&lt;br /&gt;
# exercise to the reader.&lt;br /&gt;
#&lt;br /&gt;
# To run, call&lt;br /&gt;
#   blender --python blenderplot-ani.py&lt;br /&gt;
&lt;br /&gt;
import os.path&lt;br /&gt;
&lt;br /&gt;
import numpy as np&lt;br /&gt;
&lt;br /&gt;
import bpy&lt;br /&gt;
&lt;br /&gt;
### Begin user settings&lt;br /&gt;
omega = 1&lt;br /&gt;
font = &#039;/usr/share/fonts/cm-unicode/cmunti.ttf&#039; # Must be a unicode font!&lt;br /&gt;
### End user settings&lt;br /&gt;
&lt;br /&gt;
### Begin generic Blender rendering code&lt;br /&gt;
# Global object counter.&lt;br /&gt;
obj_ind = 10000&lt;br /&gt;
&lt;br /&gt;
plot_id = None&lt;br /&gt;
&lt;br /&gt;
line_material = bpy.data.materials.new(&#039;line&#039;)&lt;br /&gt;
line_material.diffuse_color = (0, 0, 0)&lt;br /&gt;
line_material.diffuse_shader = &#039;LAMBERT&#039;&lt;br /&gt;
line_material.specular_color = (0, 0, 0)&lt;br /&gt;
line_material.specular_shader = &#039;COOKTORR&#039;&lt;br /&gt;
line_material.use_shadows = False&lt;br /&gt;
line_material.use_cast_shadows = False&lt;br /&gt;
line_material.use_raytrace = True&lt;br /&gt;
line_material.ambient = 0&lt;br /&gt;
&lt;br /&gt;
text_material = bpy.data.materials.new(&#039;text&#039;)&lt;br /&gt;
text_material.diffuse_color = (.15, .05, .035)&lt;br /&gt;
text_material.diffuse_shader = &#039;OREN_NAYAR&#039;&lt;br /&gt;
text_material.diffuse_intensity = .9&lt;br /&gt;
text_material.roughness = 2&lt;br /&gt;
text_material.specular_color = (.6, .2, .1)&lt;br /&gt;
text_material.specular_shader = &#039;PHONG&#039;&lt;br /&gt;
text_material.specular_hardness = 80&lt;br /&gt;
text_material.specular_intensity = .85&lt;br /&gt;
text_material.use_shadows = True&lt;br /&gt;
text_material.use_cast_shadows = False&lt;br /&gt;
text_material.use_raytrace = True&lt;br /&gt;
text_material.raytrace_mirror.use = True&lt;br /&gt;
text_material.mirror_color = (.7, .3, .15)&lt;br /&gt;
text_material.raytrace_mirror.reflect_factor = .3&lt;br /&gt;
text_material.emit = 0&lt;br /&gt;
text_material.ambient = 0&lt;br /&gt;
&lt;br /&gt;
plot_material = bpy.data.materials.new(&#039;plot&#039;)&lt;br /&gt;
plot_material.specular_color = (.5, .5, .5)&lt;br /&gt;
plot_material.specular_shader = &#039;COOKTORR&#039;&lt;br /&gt;
plot_material.specular_intensity = .2&lt;br /&gt;
plot_material.use_shadows = True&lt;br /&gt;
plot_material.use_transparent_shadows = True&lt;br /&gt;
plot_material.use_raytrace = True&lt;br /&gt;
plot_material.use_transparency = True&lt;br /&gt;
plot_material.transparency_method = &#039;RAYTRACE&#039;&lt;br /&gt;
plot_material.alpha = .95&lt;br /&gt;
plot_material.specular_alpha = 1&lt;br /&gt;
plot_material.raytrace_transparency.depth = 5&lt;br /&gt;
plot_material.use_vertex_color_paint = True&lt;br /&gt;
&lt;br /&gt;
text_font = bpy.data.fonts.load(os.path.abspath(os.path.expanduser(font)))&lt;br /&gt;
&lt;br /&gt;
def heatmap(heat):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Heat map: given a &amp;quot;heat&amp;quot; between 0 and 1, return a tuple of RGB&lt;br /&gt;
    values.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    r = np.max((2*heat-1., 0))&lt;br /&gt;
    b = np.max((1.-2*heat, 0))&lt;br /&gt;
    return (r, 1.-r-b, b)&lt;br /&gt;
&lt;br /&gt;
def zheat(z, zmin, zmax, **kwargs):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Colour a vertex based on its z-height compared to the minimum and&lt;br /&gt;
    maximum z-values that occur.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    return heatmap((z-zmin)/(zmax-zmin if zmin != zmax else 1))&lt;br /&gt;
&lt;br /&gt;
def plot_function(x, y, func, auto_axes = True, xmarks=None,&lt;br /&gt;
        ymarks = None, zmarks = None, labels = None, thickness = 0.025,&lt;br /&gt;
        text_rot = None, colourfunc=zheat, zmin = None, zmax = None):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Plot the function (lambda) func of x and y. The resulting surface is&lt;br /&gt;
    smooth-shaded. Vertices may be coloured according to colourfunc: this is a&lt;br /&gt;
    function that accepts the following parameters and returns an RGB-tuple:&lt;br /&gt;
        x, y, z, xmin, xmax, ymin, ymax, zmin, zmax&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    global obj_ind, plot_id&lt;br /&gt;
&lt;br /&gt;
    ids = {&lt;br /&gt;
        &#039;axes&#039;: [],&lt;br /&gt;
        &#039;axis_labels&#039;: [],&lt;br /&gt;
        &#039;xmarks&#039;: [],&lt;br /&gt;
        &#039;ymarks&#039;: [],&lt;br /&gt;
        &#039;zmarks&#039;: [],&lt;br /&gt;
        &#039;xlabels&#039;: [],&lt;br /&gt;
        &#039;ylabels&#039;: [],&lt;br /&gt;
        &#039;zlabels&#039;: []&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    if text_rot is None:&lt;br /&gt;
        text_rot = np.array((0, 0, 0))&lt;br /&gt;
&lt;br /&gt;
    if plot_id is None:&lt;br /&gt;
        obj_id = &#039;plot_{}&#039;.format(obj_ind)&lt;br /&gt;
        obj_ind += 1&lt;br /&gt;
        # Generate all vertices in the plot at z = 0&lt;br /&gt;
        verts = [(i, j, 0) for i in x for j in y]&lt;br /&gt;
        faces = []&lt;br /&gt;
        count = 0&lt;br /&gt;
        # Build faces from the vertices&lt;br /&gt;
        for i in range(len(y)*(len(x)-1)):&lt;br /&gt;
            if count &amp;lt; len(y)-1:&lt;br /&gt;
                faces.append((i, i+1, i+len(y)+1, i+len(y)))&lt;br /&gt;
                count += 1&lt;br /&gt;
            else:&lt;br /&gt;
                count = 0&lt;br /&gt;
&lt;br /&gt;
        # Create a mesh and an object at the origin&lt;br /&gt;
        mesh = bpy.data.meshes.new(obj_id)&lt;br /&gt;
        obj = bpy.data.objects.new(obj_id, mesh)&lt;br /&gt;
        obj.location = (0, 0, 0)&lt;br /&gt;
        bpy.context.scene.objects.link(obj)&lt;br /&gt;
        mesh.from_pydata(verts, [], faces)&lt;br /&gt;
        mesh.update(calc_edges=True)&lt;br /&gt;
&lt;br /&gt;
        # Create a new vertex colour map&lt;br /&gt;
        colours = obj.data.vertex_colors.new()&lt;br /&gt;
&lt;br /&gt;
        # Set material&lt;br /&gt;
        obj.data.materials.append(plot_material)&lt;br /&gt;
&lt;br /&gt;
        # Smooth-shade polygons&lt;br /&gt;
        for pol in obj.data.polygons:&lt;br /&gt;
            pol.use_smooth = True&lt;br /&gt;
    else:&lt;br /&gt;
        obj = bpy.data.objects[plot_id]&lt;br /&gt;
        colours = obj.data.vertex_colors.active&lt;br /&gt;
&lt;br /&gt;
    verts = obj.data.vertices&lt;br /&gt;
&lt;br /&gt;
    # Move vertices to their correct position&lt;br /&gt;
    for v in verts:&lt;br /&gt;
        v.co.z = func(v.co.x, v.co.y)&lt;br /&gt;
    obj.data.update(calc_edges=True)&lt;br /&gt;
&lt;br /&gt;
    sv = sorted([(v.co.x, v.co.y, v.co.z) for v in verts], key=lambda q: q[2])&lt;br /&gt;
&lt;br /&gt;
    # Colour vertices&lt;br /&gt;
    for pol in obj.data.polygons:&lt;br /&gt;
        for idx in pol.loop_indices:&lt;br /&gt;
            co = obj.data.vertices[obj.data.loops[idx].vertex_index].co&lt;br /&gt;
            colours.data[idx].color = colourfunc(x=co.x, y=co.y, z=co.z,&lt;br /&gt;
                xmin=np.min(x), xmax=np.max(x),&lt;br /&gt;
                ymin=np.min(y), ymax=np.max(y),&lt;br /&gt;
                zmin=sv[0][2], zmax=sv[-1][2])&lt;br /&gt;
&lt;br /&gt;
    if auto_axes and plot_id is None:&lt;br /&gt;
        # Axes&lt;br /&gt;
        ids[&#039;axes&#039;].append(add_line(np.array((min(x), min(y), 0)),&lt;br /&gt;
            np.array((max(x), min(y), 0)), thickness, False))&lt;br /&gt;
        ids[&#039;axes&#039;].append(add_line(np.array((max(x), min(y), 0)),&lt;br /&gt;
            np.array((max(x), max(y), 0)), thickness, False))&lt;br /&gt;
        ids[&#039;axes&#039;].append(add_line(&lt;br /&gt;
            np.array((min(x), min(y), sv[0][2] if zmin is None else zmin)),&lt;br /&gt;
            np.array((min(x), min(y), sv[-1][2] if zmax is None else zmax)),&lt;br /&gt;
            thickness, False))&lt;br /&gt;
        # Axis marks&lt;br /&gt;
        if xmarks is not None:&lt;br /&gt;
            for pos, label in xmarks:&lt;br /&gt;
                p = np.array((pos, min(y), 0))&lt;br /&gt;
                ids[&#039;xmarks&#039;].append(add_line(p,&lt;br /&gt;
                    p-np.array((0, 1.5*thickness, 0)), thickness, False))&lt;br /&gt;
                if label is not None and len(label) &amp;gt; 0:&lt;br /&gt;
                    ids[&#039;xlabels&#039;].append(add_text(&lt;br /&gt;
                        p-np.array((0, 7*thickness, 0)), label,&lt;br /&gt;
                        thickness, text_rot))&lt;br /&gt;
        if ymarks is not None:&lt;br /&gt;
            for pos, label in ymarks:&lt;br /&gt;
                p = np.array((max(x), pos, 0))&lt;br /&gt;
                ids[&#039;ymarks&#039;].append(add_line(p,&lt;br /&gt;
                    p+np.array((1.5*thickness, 0, 0)), thickness, False))&lt;br /&gt;
                if label is not None and len(label) &amp;gt; 0:&lt;br /&gt;
                    ids[ &#039;ylabels&#039;].append(add_text(&lt;br /&gt;
                        p+np.array((7*thickness, 0, 0)), label,&lt;br /&gt;
                        thickness, text_rot))&lt;br /&gt;
        if zmarks is not None:&lt;br /&gt;
            for pos, label in zmarks:&lt;br /&gt;
                p = np.array((min(x), min(y), pos))&lt;br /&gt;
                ids[&#039;zmarks&#039;].append(add_line(p,&lt;br /&gt;
                    p-np.array((1.5*thickness/np.sqrt(2),&lt;br /&gt;
                        1.5*thickness/np.sqrt(2), 0)), thickness, False))&lt;br /&gt;
                if label is not None and len(label) &amp;gt; 0:&lt;br /&gt;
                    ids[&#039;zlabels&#039;].append(add_text(&lt;br /&gt;
                        p-np.array((7*thickness, 7*thickness, 0))/np.sqrt(2),&lt;br /&gt;
                        label, thickness, text_rot))&lt;br /&gt;
&lt;br /&gt;
        # Axis labels&lt;br /&gt;
        if labels is not None:&lt;br /&gt;
            ids[&#039;axis_labels&#039;].append(add_text(&lt;br /&gt;
                np.array((max(x)+8*thickness, min(y), 0)),&lt;br /&gt;
                labels[0], 2*thickness, text_rot))&lt;br /&gt;
            ids[&#039;axis_labels&#039;].append(add_text(&lt;br /&gt;
                np.array((max(x), max(y)+8*thickness, 0)),&lt;br /&gt;
                labels[1], 2*thickness, text_rot))&lt;br /&gt;
            ids[&#039;axis_labels&#039;].append(add_text(&lt;br /&gt;
                np.array((min(x), min(y),&lt;br /&gt;
                    (sv[-1][2] if zmax is None else zmax) + 8*thickness)),&lt;br /&gt;
                labels[2], 2*thickness, text_rot))&lt;br /&gt;
&lt;br /&gt;
    if plot_id is None:&lt;br /&gt;
        plot_id = obj_id&lt;br /&gt;
&lt;br /&gt;
    ids[&#039;plot&#039;] = plot_id&lt;br /&gt;
&lt;br /&gt;
    return ids&lt;br /&gt;
&lt;br /&gt;
def add_text(r, text, size=0.025, rotation=None):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Add text at the position r. Size is a relative parameter; use&lt;br /&gt;
    trial-and-error here.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    global obj_ind&lt;br /&gt;
    obj_id = &#039;text_{}&#039;.format(obj_ind)&lt;br /&gt;
    obj_ind += 1&lt;br /&gt;
    rot = [np.pi/2, 0, 0] if rotation is None else rotation.tolist()&lt;br /&gt;
    bpy.ops.object.text_add(location=r.tolist(), rotation=rot)&lt;br /&gt;
    obj = bpy.context.active_object&lt;br /&gt;
    obj.name = obj_id&lt;br /&gt;
    obj.data.name = obj_id&lt;br /&gt;
    obj.data.body = text&lt;br /&gt;
&lt;br /&gt;
    # Set the font&lt;br /&gt;
    obj.data.font = text_font&lt;br /&gt;
&lt;br /&gt;
    obj.data.offset_x = -2*size&lt;br /&gt;
    obj.data.offset_y = -2*size&lt;br /&gt;
    obj.data.shear = 0.0&lt;br /&gt;
    obj.data.size = 8*size&lt;br /&gt;
    obj.data.space_character = 1&lt;br /&gt;
    obj.data.space_word = 4*size&lt;br /&gt;
    obj.data.extrude = size/3&lt;br /&gt;
&lt;br /&gt;
    obj.data.materials.append(text_material)&lt;br /&gt;
&lt;br /&gt;
    return obj_id&lt;br /&gt;
&lt;br /&gt;
def add_line(r1, r2, w=0.01, rel_w=True):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Add a &amp;quot;line&amp;quot; (cylinder) between the points r1 and r2. The width is&lt;br /&gt;
    either w (if rel_w is False) or w*|r2-r1| (if rel_w is True).&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    global obj_ind&lt;br /&gt;
    obj_id = &#039;line_{}&#039;.format(obj_ind)&lt;br /&gt;
    obj_ind += 1&lt;br /&gt;
    rc = (r2+r1)/2 # Centroid&lt;br /&gt;
    rr = r2-rc # Position of r2 relative to centroid&lt;br /&gt;
    r = np.sqrt(np.sum(rr**2))&lt;br /&gt;
    theta = np.arccos(rr[2]/r)&lt;br /&gt;
    phi = np.arctan2(rr[1], rr[0])&lt;br /&gt;
    bpy.ops.mesh.primitive_cylinder_add(vertices=16,&lt;br /&gt;
            radius=.5*w*(r if rel_w else 1), depth=2*r,&lt;br /&gt;
            location=rc.tolist(), rotation=(0, theta, phi))&lt;br /&gt;
    obj = bpy.context.active_object&lt;br /&gt;
    obj.name = obj_id&lt;br /&gt;
    for pol in obj.data.polygons:&lt;br /&gt;
        pol.use_smooth = True&lt;br /&gt;
    obj.data.materials.append(line_material)&lt;br /&gt;
&lt;br /&gt;
    return obj_id&lt;br /&gt;
### End generic Blender rendering code&lt;br /&gt;
&lt;br /&gt;
def frame_change(scene):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Update the plot for a given frame.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    frame = min(max(scene.frame_current, 0), n_frames - 1)&lt;br /&gt;
    plot_function(x, k, funcgen(t[frame]), colourfunc=colgen(t[frame]))&lt;br /&gt;
    # Update the t-indicator&lt;br /&gt;
    bpy.data.objects[ttext_id].data.body = &#039;t = {: &amp;gt;4.3f}&#039;.format(t[frame])&lt;br /&gt;
&lt;br /&gt;
if __name__ == &#039;__main__&#039;:&lt;br /&gt;
    # Set up x, y and t data&lt;br /&gt;
    n_frames = 51&lt;br /&gt;
    nx = 101&lt;br /&gt;
    nk = 101&lt;br /&gt;
    xscale = 1/2&lt;br /&gt;
    kscale = 1/3&lt;br /&gt;
    zscale = 2&lt;br /&gt;
    x = np.linspace(0, 10, nx)*xscale&lt;br /&gt;
    k = np.linspace(0, 4*np.pi, nk)*kscale&lt;br /&gt;
    t = np.linspace(0, 10, n_frames)&lt;br /&gt;
&lt;br /&gt;
    # Function to plot&lt;br /&gt;
    func = lambda x, k, t: np.exp(1j*(k*x-omega*t))*zscale&lt;br /&gt;
    # Generator for plottable function f(x, k) at time t&lt;br /&gt;
    funcgen = lambda t: lambda x, k: np.real(func(x, k, t))&lt;br /&gt;
    # Colour generator&lt;br /&gt;
    colgen = lambda t: lambda x, y, **kwargs: \&lt;br /&gt;
            heatmap((np.imag(func(x, y, t))+1)/2)&lt;br /&gt;
&lt;br /&gt;
    # Absolute z range for axes&lt;br /&gt;
    azmin = -1*zscale&lt;br /&gt;
    azmax = 1*zscale&lt;br /&gt;
&lt;br /&gt;
    # Axis marks&lt;br /&gt;
    xmarks = [(i*xscale, str(i)) for i in range(1, 11)]&lt;br /&gt;
    kmarks = [(j*np.pi/2*kscale, &#039;{}π/2&#039;.format(j if j != 1 else &#039;&#039;) \&lt;br /&gt;
        if j % 2 == 1 else &#039;{}π&#039;.format(j // 2 if j != 2 else &#039;&#039;)) \&lt;br /&gt;
        for j in range(1, 9)]&lt;br /&gt;
    zmarks = [(z/10*zscale, str(z/10)) for z in range(-10, 12, 2)]&lt;br /&gt;
&lt;br /&gt;
    # Hide the 吸牛 splash screen&lt;br /&gt;
    bpy.context.user_preferences.view.show_splash = False&lt;br /&gt;
&lt;br /&gt;
    # Remove existing meshes&lt;br /&gt;
    for item in bpy.context.scene.objects:&lt;br /&gt;
        if item.type == &#039;MESH&#039;:&lt;br /&gt;
            bpy.context.scene.objects.unlink(item)&lt;br /&gt;
    for item in bpy.data.objects:&lt;br /&gt;
        if item.type == &#039;MESH&#039;:&lt;br /&gt;
            bpy.data.objects.remove(item)&lt;br /&gt;
    for item in bpy.data.meshes:&lt;br /&gt;
        bpy.data.meshes.remove(item)&lt;br /&gt;
&lt;br /&gt;
    # Set the camera position&lt;br /&gt;
    bpy.data.objects[&#039;Camera&#039;].location = (11, -6, 5.5)&lt;br /&gt;
    bpy.data.objects[&#039;Camera&#039;].rotation_euler = (1.1, 0, 0.8)&lt;br /&gt;
&lt;br /&gt;
    # Let all texts face the camera&lt;br /&gt;
    rot = np.array(bpy.data.objects[&#039;Camera&#039;].rotation_euler)&lt;br /&gt;
&lt;br /&gt;
    # Initial t=0 plot; this also sets up the axes.&lt;br /&gt;
    print(plot_function(x, k, funcgen(0),&lt;br /&gt;
        True, labels=(&#039;x&#039;, &#039;k&#039;, &#039;z&#039;), text_rot=rot, xmarks=xmarks,&lt;br /&gt;
        ymarks=kmarks, zmarks=zmarks, zmin=azmin, zmax=azmax,&lt;br /&gt;
        colourfunc=colgen(0)))&lt;br /&gt;
&lt;br /&gt;
    # Text label indicating current time&lt;br /&gt;
    ttext_id = add_text(np.array((5.6, -1.2, 0)), &#039;t = {: &amp;gt;4.2f}&#039;.format(0),&lt;br /&gt;
        size=0.05, rotation=rot)&lt;br /&gt;
&lt;br /&gt;
    # Set min/max/current frame&lt;br /&gt;
    bpy.data.scenes[&#039;Scene&#039;].frame_start = 0&lt;br /&gt;
    bpy.data.scenes[&#039;Scene&#039;].frame_end = n_frames - 1&lt;br /&gt;
    bpy.data.scenes[&#039;Scene&#039;].frame_current = 0&lt;br /&gt;
&lt;br /&gt;
    # Add frame change handler. This is what makes the animation happen!&lt;br /&gt;
    bpy.app.handlers.frame_change_pre.append(frame_change)&lt;br /&gt;
&lt;br /&gt;
    # Add some environment lighting&lt;br /&gt;
    wld = bpy.data.worlds[&#039;World&#039;]&lt;br /&gt;
    wld.light_settings.use_environment_light = True&lt;br /&gt;
    wld.light_settings.environment_energy = .5&lt;br /&gt;
&lt;br /&gt;
    # Add a white backdrop plane&lt;br /&gt;
    plane_material = bpy.data.materials.new(&#039;backdrop&#039;)&lt;br /&gt;
    plane_material.diffuse_color = (1, 1, 1)&lt;br /&gt;
    plane_material.use_shadeless = True&lt;br /&gt;
    bpy.ops.mesh.primitive_plane_add(location=(0, 0, -5))&lt;br /&gt;
    bpy.context.active_object.scale = (50, 50, 0)&lt;br /&gt;
    bpy.context.active_object.data.materials.append(plane_material)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=Output video=&lt;br /&gt;
&amp;lt;html5media height=&amp;quot;270&amp;quot; width=&amp;quot;480&amp;quot;&amp;gt;File:Animated 3D plot.ogv&amp;lt;/html5media&amp;gt;&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=Main_Page&amp;diff=216</id>
		<title>Main Page</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=Main_Page&amp;diff=216"/>
		<updated>2016-11-09T14:51:25Z</updated>

		<summary type="html">&lt;p&gt;Link: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Welcome to &#039;&#039;&#039;Penguin Development&#039;&#039;&#039;, a (currently one-man) organisation developing [[w:free and open-source software|free and open-source software]] and [[w:Open-source hardware|hardware]].&lt;br /&gt;
&lt;br /&gt;
See below for current releases. All software and hardware is licensed under the [https://gnu.org/licenses/gpl.html GPL] (version 3 or higher) unless otherwise stated.&lt;br /&gt;
&lt;br /&gt;
=Software=&lt;br /&gt;
* [[AUTOMOME]] — the OTTish AUTOMEME clone&lt;br /&gt;
* [[Ising]] — Monte Carlo Ising model simulator&lt;br /&gt;
* [[stackermann]] — a stack-based [[w:Ackermann function|Ackermann function]] calculator&lt;br /&gt;
&lt;br /&gt;
=Hardware=&lt;br /&gt;
* [[timelapse]] — time-lapse trigger for digital cameras&lt;br /&gt;
&lt;br /&gt;
=Code snippets and how-tos=&lt;br /&gt;
[[Animated 3D plotting with Blender]]&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=Animated_3D_plotting_with_Blender&amp;diff=215</id>
		<title>Animated 3D plotting with Blender</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=Animated_3D_plotting_with_Blender&amp;diff=215"/>
		<updated>2016-11-09T14:26:28Z</updated>

		<summary type="html">&lt;p&gt;Link: Embed video with Html5mediator.&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[https://www.blender.org/ Blender] is an extremely versatile 3D creation and animation suite. Since it is fully scriptable in [https://www.python.org/ Python], Blender may be used to generate &#039;&#039;&#039;animated 3-dimensional plots&#039;&#039;&#039; of data or mathematical functions. Below is an example of one way to generate such a plot.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Important information ==&lt;br /&gt;
The script below has been tested with Blender 2.78a for Linux. It should generally be compatible with other versions and operating systems. The script works with the pristine Blender startup file; heavily modified startup files may break certain assumptions.&lt;br /&gt;
&lt;br /&gt;
To run the script, simply call &amp;lt;code&amp;gt;blender --python blenderplot-ani.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To create the plot in a different base file, instead use &amp;lt;code&amp;gt;blender basefile.blend --python blenderplot-ani.py&amp;lt;/code&amp;gt;.&lt;br /&gt;
The order of arguments matters here.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Note that the animation data will not be saved with your .blend file!&#039;&#039;&#039; This means you must run the Python script on a pristine &amp;quot;background&amp;quot; .blend file each time you wish to examine the geometry or create a render. If the .blend file is saved, only the first frame will be captured.&lt;br /&gt;
&lt;br /&gt;
== The script ==&lt;br /&gt;
Code for &amp;lt;code&amp;gt;blenderplot-ani.py&amp;lt;/code&amp;gt; follows.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot; line&amp;gt;&lt;br /&gt;
#!/bin/true&lt;br /&gt;
# vim: se fo=tcroq tw=78 :&lt;br /&gt;
# Simple animated 3D plot example using Blender ( https://www.blender.org/ ).&lt;br /&gt;
# Given the function f(k, x, t)=exp(ikx-iωt), plots Re(f) against x and k,&lt;br /&gt;
# with colour given by Im(f) and t being the time.&lt;br /&gt;
#&lt;br /&gt;
# For pedagogical purposes, this just computes f at each frame (twice: once&lt;br /&gt;
# for the vertex positions and once for their colours). This is horribly&lt;br /&gt;
# inefficient; it would be much better to generate a 3D array for f(x, k, t)&lt;br /&gt;
# once and slice this array for each frame -- however, this is left as an&lt;br /&gt;
# exercise to the reader.&lt;br /&gt;
#&lt;br /&gt;
# To run, call&lt;br /&gt;
#   blender --python blenderplot-ani.py&lt;br /&gt;
&lt;br /&gt;
import os.path&lt;br /&gt;
&lt;br /&gt;
import numpy as np&lt;br /&gt;
&lt;br /&gt;
import bpy&lt;br /&gt;
&lt;br /&gt;
### Begin user settings&lt;br /&gt;
omega = 1&lt;br /&gt;
font = &#039;/usr/share/fonts/cm-unicode/cmunti.ttf&#039; # Must be a unicode font!&lt;br /&gt;
### End user settings&lt;br /&gt;
&lt;br /&gt;
### Begin generic Blender rendering code&lt;br /&gt;
# Global object counter.&lt;br /&gt;
obj_ind = 10000&lt;br /&gt;
&lt;br /&gt;
plot_id = None&lt;br /&gt;
&lt;br /&gt;
line_material = bpy.data.materials.new(&#039;line&#039;)&lt;br /&gt;
line_material.diffuse_color = (0, 0, 0)&lt;br /&gt;
line_material.diffuse_shader = &#039;LAMBERT&#039;&lt;br /&gt;
line_material.specular_color = (0, 0, 0)&lt;br /&gt;
line_material.specular_shader = &#039;COOKTORR&#039;&lt;br /&gt;
line_material.use_shadows = False&lt;br /&gt;
line_material.use_cast_shadows = False&lt;br /&gt;
line_material.use_raytrace = True&lt;br /&gt;
line_material.ambient = 0&lt;br /&gt;
&lt;br /&gt;
text_material = bpy.data.materials.new(&#039;text&#039;)&lt;br /&gt;
text_material.diffuse_color = (.15, .05, .035)&lt;br /&gt;
text_material.diffuse_shader = &#039;OREN_NAYAR&#039;&lt;br /&gt;
text_material.diffuse_intensity = .9&lt;br /&gt;
text_material.roughness = 2&lt;br /&gt;
text_material.specular_color = (.6, .2, .1)&lt;br /&gt;
text_material.specular_shader = &#039;PHONG&#039;&lt;br /&gt;
text_material.specular_hardness = 80&lt;br /&gt;
text_material.specular_intensity = .85&lt;br /&gt;
text_material.use_shadows = True&lt;br /&gt;
text_material.use_cast_shadows = False&lt;br /&gt;
text_material.use_raytrace = True&lt;br /&gt;
text_material.raytrace_mirror.use = True&lt;br /&gt;
text_material.mirror_color = (.7, .3, .15)&lt;br /&gt;
text_material.raytrace_mirror.reflect_factor = .3&lt;br /&gt;
text_material.emit = 0&lt;br /&gt;
text_material.ambient = 0&lt;br /&gt;
&lt;br /&gt;
plot_material = bpy.data.materials.new(&#039;plot&#039;)&lt;br /&gt;
plot_material.specular_color = (.5, .5, .5)&lt;br /&gt;
plot_material.specular_shader = &#039;COOKTORR&#039;&lt;br /&gt;
plot_material.specular_intensity = .2&lt;br /&gt;
plot_material.use_shadows = True&lt;br /&gt;
plot_material.use_transparent_shadows = True&lt;br /&gt;
plot_material.use_raytrace = True&lt;br /&gt;
plot_material.use_transparency = True&lt;br /&gt;
plot_material.transparency_method = &#039;RAYTRACE&#039;&lt;br /&gt;
plot_material.alpha = .95&lt;br /&gt;
plot_material.specular_alpha = 1&lt;br /&gt;
plot_material.raytrace_transparency.depth = 5&lt;br /&gt;
plot_material.use_vertex_color_paint = True&lt;br /&gt;
&lt;br /&gt;
text_font = bpy.data.fonts.load(os.path.abspath(os.path.expanduser(font)))&lt;br /&gt;
&lt;br /&gt;
def heatmap(heat):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Heat map: given a &amp;quot;heat&amp;quot; between 0 and 1, return a tuple of RGB&lt;br /&gt;
    values.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    r = np.max((2*heat-1., 0))&lt;br /&gt;
    b = np.max((1.-2*heat, 0))&lt;br /&gt;
    return (r, 1.-r-b, b)&lt;br /&gt;
&lt;br /&gt;
def zheat(z, zmin, zmax, **kwargs):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Colour a vertex based on its z-height compared to the minimum and&lt;br /&gt;
    maximum z-values that occur.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    return heatmap((z-zmin)/(zmax-zmin if zmin != zmax else 1))&lt;br /&gt;
&lt;br /&gt;
def plot_function(x, y, func, auto_axes = True, xmarks=None,&lt;br /&gt;
        ymarks = None, zmarks = None, labels = None, thickness = 0.025,&lt;br /&gt;
        text_rot = None, colourfunc=zheat, zmin = None, zmax = None):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Plot the function (lambda) func of x and y. The resulting surface is&lt;br /&gt;
    smooth-shaded. Vertices may be coloured according to colourfunc: this is a&lt;br /&gt;
    function that accepts the following parameters and returns an RGB-tuple:&lt;br /&gt;
        x, y, z, xmin, xmax, ymin, ymax, zmin, zmax&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    global obj_ind, plot_id&lt;br /&gt;
&lt;br /&gt;
    ids = {&lt;br /&gt;
        &#039;axes&#039;: [],&lt;br /&gt;
        &#039;axis_labels&#039;: [],&lt;br /&gt;
        &#039;xmarks&#039;: [],&lt;br /&gt;
        &#039;ymarks&#039;: [],&lt;br /&gt;
        &#039;zmarks&#039;: [],&lt;br /&gt;
        &#039;xlabels&#039;: [],&lt;br /&gt;
        &#039;ylabels&#039;: [],&lt;br /&gt;
        &#039;zlabels&#039;: []&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    if text_rot is None:&lt;br /&gt;
        text_rot = np.array((0, 0, 0))&lt;br /&gt;
&lt;br /&gt;
    if plot_id is None:&lt;br /&gt;
        obj_id = &#039;plot_{}&#039;.format(obj_ind)&lt;br /&gt;
        obj_ind += 1&lt;br /&gt;
        # Generate all vertices in the plot at z = 0&lt;br /&gt;
        verts = [(i, j, 0) for i in x for j in y]&lt;br /&gt;
        faces = []&lt;br /&gt;
        count = 0&lt;br /&gt;
        # Build faces from the vertices&lt;br /&gt;
        for i in range(len(y)*(len(x)-1)):&lt;br /&gt;
            if count &amp;lt; len(y)-1:&lt;br /&gt;
                faces.append((i, i+1, i+len(y)+1, i+len(y)))&lt;br /&gt;
                count += 1&lt;br /&gt;
            else:&lt;br /&gt;
                count = 0&lt;br /&gt;
&lt;br /&gt;
        # Create a mesh and an object at the origin&lt;br /&gt;
        mesh = bpy.data.meshes.new(obj_id)&lt;br /&gt;
        obj = bpy.data.objects.new(obj_id, mesh)&lt;br /&gt;
        obj.location = (0, 0, 0)&lt;br /&gt;
        bpy.context.scene.objects.link(obj)&lt;br /&gt;
        mesh.from_pydata(verts, [], faces)&lt;br /&gt;
        mesh.update(calc_edges=True)&lt;br /&gt;
&lt;br /&gt;
        # Create a new vertex colour map&lt;br /&gt;
        colours = obj.data.vertex_colors.new()&lt;br /&gt;
&lt;br /&gt;
        # Set material&lt;br /&gt;
        obj.data.materials.append(plot_material)&lt;br /&gt;
&lt;br /&gt;
        # Smooth-shade polygons&lt;br /&gt;
        for pol in obj.data.polygons:&lt;br /&gt;
            pol.use_smooth = True&lt;br /&gt;
    else:&lt;br /&gt;
        obj = bpy.data.objects[plot_id]&lt;br /&gt;
        colours = obj.data.vertex_colors.active&lt;br /&gt;
&lt;br /&gt;
    verts = obj.data.vertices&lt;br /&gt;
&lt;br /&gt;
    # Move vertices to their correct position&lt;br /&gt;
    for v in verts:&lt;br /&gt;
        v.co.z = func(v.co.x, v.co.y)&lt;br /&gt;
    obj.data.update(calc_edges=True)&lt;br /&gt;
&lt;br /&gt;
    sv = sorted([(v.co.x, v.co.y, v.co.z) for v in verts], key=lambda q: q[2])&lt;br /&gt;
&lt;br /&gt;
    # Colour vertices&lt;br /&gt;
    for pol in obj.data.polygons:&lt;br /&gt;
        for idx in pol.loop_indices:&lt;br /&gt;
            co = obj.data.vertices[obj.data.loops[idx].vertex_index].co&lt;br /&gt;
            colours.data[idx].color = colourfunc(x=co.x, y=co.y, z=co.z,&lt;br /&gt;
                xmin=np.min(x), xmax=np.max(x),&lt;br /&gt;
                ymin=np.min(y), ymax=np.max(y),&lt;br /&gt;
                zmin=sv[0][2], zmax=sv[-1][2])&lt;br /&gt;
&lt;br /&gt;
    if auto_axes and plot_id is None:&lt;br /&gt;
        # Axes&lt;br /&gt;
        ids[&#039;axes&#039;].append(add_line(np.array((min(x), min(y), 0)),&lt;br /&gt;
            np.array((max(x), min(y), 0)), thickness, False))&lt;br /&gt;
        ids[&#039;axes&#039;].append(add_line(np.array((max(x), min(y), 0)),&lt;br /&gt;
            np.array((max(x), max(y), 0)), thickness, False))&lt;br /&gt;
        ids[&#039;axes&#039;].append(add_line(&lt;br /&gt;
            np.array((min(x), min(y), sv[0][2] if zmin is None else zmin)),&lt;br /&gt;
            np.array((min(x), min(y), sv[-1][2] if zmax is None else zmax)),&lt;br /&gt;
            thickness, False))&lt;br /&gt;
        # Axis marks&lt;br /&gt;
        if xmarks is not None:&lt;br /&gt;
            for pos, label in xmarks:&lt;br /&gt;
                p = np.array((pos, min(y), 0))&lt;br /&gt;
                ids[&#039;xmarks&#039;].append(add_line(p,&lt;br /&gt;
                    p-np.array((0, 1.5*thickness, 0)), thickness, False))&lt;br /&gt;
                if label is not None and len(label) &amp;gt; 0:&lt;br /&gt;
                    ids[&#039;xlabels&#039;].append(add_text(&lt;br /&gt;
                        p-np.array((0, 7*thickness, 0)), label,&lt;br /&gt;
                        thickness, text_rot))&lt;br /&gt;
        if ymarks is not None:&lt;br /&gt;
            for pos, label in ymarks:&lt;br /&gt;
                p = np.array((max(x), pos, 0))&lt;br /&gt;
                ids[&#039;ymarks&#039;].append(add_line(p,&lt;br /&gt;
                    p+np.array((1.5*thickness, 0, 0)), thickness, False))&lt;br /&gt;
                if label is not None and len(label) &amp;gt; 0:&lt;br /&gt;
                    ids[ &#039;ylabels&#039;].append(add_text(&lt;br /&gt;
                        p+np.array((7*thickness, 0, 0)), label,&lt;br /&gt;
                        thickness, text_rot))&lt;br /&gt;
        if zmarks is not None:&lt;br /&gt;
            for pos, label in zmarks:&lt;br /&gt;
                p = np.array((min(x), min(y), pos))&lt;br /&gt;
                ids[&#039;zmarks&#039;].append(add_line(p,&lt;br /&gt;
                    p-np.array((1.5*thickness/np.sqrt(2),&lt;br /&gt;
                        1.5*thickness/np.sqrt(2), 0)), thickness, False))&lt;br /&gt;
                if label is not None and len(label) &amp;gt; 0:&lt;br /&gt;
                    ids[&#039;zlabels&#039;].append(add_text(&lt;br /&gt;
                        p-np.array((7*thickness, 7*thickness, 0))/np.sqrt(2),&lt;br /&gt;
                        label, thickness, text_rot))&lt;br /&gt;
&lt;br /&gt;
        # Axis labels&lt;br /&gt;
        if labels is not None:&lt;br /&gt;
            ids[&#039;axis_labels&#039;].append(add_text(&lt;br /&gt;
                np.array((max(x)+8*thickness, min(y), 0)),&lt;br /&gt;
                labels[0], 2*thickness, text_rot))&lt;br /&gt;
            ids[&#039;axis_labels&#039;].append(add_text(&lt;br /&gt;
                np.array((max(x), max(y)+8*thickness, 0)),&lt;br /&gt;
                labels[1], 2*thickness, text_rot))&lt;br /&gt;
            ids[&#039;axis_labels&#039;].append(add_text(&lt;br /&gt;
                np.array((min(x), min(y),&lt;br /&gt;
                    (sv[-1][2] if zmax is None else zmax) + 8*thickness)),&lt;br /&gt;
                labels[2], 2*thickness, text_rot))&lt;br /&gt;
&lt;br /&gt;
    if plot_id is None:&lt;br /&gt;
        plot_id = obj_id&lt;br /&gt;
&lt;br /&gt;
    ids[&#039;plot&#039;] = plot_id&lt;br /&gt;
&lt;br /&gt;
    return ids&lt;br /&gt;
&lt;br /&gt;
def add_text(r, text, size=0.025, rotation=None):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Add text at the position r. Size is a relative parameter; use&lt;br /&gt;
    trial-and-error here.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    global obj_ind&lt;br /&gt;
    obj_id = &#039;text_{}&#039;.format(obj_ind)&lt;br /&gt;
    obj_ind += 1&lt;br /&gt;
    rot = [np.pi/2, 0, 0] if rotation is None else rotation.tolist()&lt;br /&gt;
    bpy.ops.object.text_add(location=r.tolist(), rotation=rot)&lt;br /&gt;
    obj = bpy.context.active_object&lt;br /&gt;
    obj.name = obj_id&lt;br /&gt;
    obj.data.name = obj_id&lt;br /&gt;
    obj.data.body = text&lt;br /&gt;
&lt;br /&gt;
    # Set the font&lt;br /&gt;
    obj.data.font = text_font&lt;br /&gt;
&lt;br /&gt;
    obj.data.offset_x = -2*size&lt;br /&gt;
    obj.data.offset_y = -2*size&lt;br /&gt;
    obj.data.shear = 0.0&lt;br /&gt;
    obj.data.size = 8*size&lt;br /&gt;
    obj.data.space_character = 1&lt;br /&gt;
    obj.data.space_word = 4*size&lt;br /&gt;
    obj.data.extrude = size/3&lt;br /&gt;
&lt;br /&gt;
    obj.data.materials.append(text_material)&lt;br /&gt;
&lt;br /&gt;
    return obj_id&lt;br /&gt;
&lt;br /&gt;
def add_line(r1, r2, w=0.01, rel_w=True):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Add a &amp;quot;line&amp;quot; (cylinder) between the points r1 and r2. The width is&lt;br /&gt;
    either w (if rel_w is False) or w*|r2-r1| (if rel_w is True).&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    global obj_ind&lt;br /&gt;
    obj_id = &#039;line_{}&#039;.format(obj_ind)&lt;br /&gt;
    obj_ind += 1&lt;br /&gt;
    rc = (r2+r1)/2 # Centroid&lt;br /&gt;
    rr = r2-rc # Position of r2 relative to centroid&lt;br /&gt;
    r = np.sqrt(np.sum(rr**2))&lt;br /&gt;
    theta = np.arccos(rr[2]/r)&lt;br /&gt;
    phi = np.arctan2(rr[1], rr[0])&lt;br /&gt;
    bpy.ops.mesh.primitive_cylinder_add(vertices=16,&lt;br /&gt;
            radius=.5*w*(r if rel_w else 1), depth=2*r,&lt;br /&gt;
            location=rc.tolist(), rotation=(0, theta, phi))&lt;br /&gt;
    obj = bpy.context.active_object&lt;br /&gt;
    obj.name = obj_id&lt;br /&gt;
    for pol in obj.data.polygons:&lt;br /&gt;
        pol.use_smooth = True&lt;br /&gt;
    obj.data.materials.append(line_material)&lt;br /&gt;
&lt;br /&gt;
    return obj_id&lt;br /&gt;
### End generic Blender rendering code&lt;br /&gt;
&lt;br /&gt;
def frame_change(scene):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Update the plot for a given frame.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    frame = min(max(scene.frame_current, 0), n_frames - 1)&lt;br /&gt;
    plot_function(x, k, funcgen(t[frame]), colourfunc=colgen(t[frame]))&lt;br /&gt;
    # Update the t-indicator&lt;br /&gt;
    bpy.data.objects[ttext_id].data.body = &#039;t = {: &amp;gt;4.3f}&#039;.format(t[frame])&lt;br /&gt;
&lt;br /&gt;
if __name__ == &#039;__main__&#039;:&lt;br /&gt;
    # Set up x, y and t data&lt;br /&gt;
    n_frames = 51&lt;br /&gt;
    nx = 101&lt;br /&gt;
    nk = 101&lt;br /&gt;
    xscale = 1/2&lt;br /&gt;
    kscale = 1/3&lt;br /&gt;
    zscale = 2&lt;br /&gt;
    x = np.linspace(0, 10, nx)*xscale&lt;br /&gt;
    k = np.linspace(0, 4*np.pi, nk)*kscale&lt;br /&gt;
    t = np.linspace(0, 10, n_frames)&lt;br /&gt;
&lt;br /&gt;
    # Function to plot&lt;br /&gt;
    func = lambda x, k, t: np.exp(1j*(k*x-omega*t))*zscale&lt;br /&gt;
    # Generator for plottable function f(x, k) at time t&lt;br /&gt;
    funcgen = lambda t: lambda x, k: np.real(func(x, k, t))&lt;br /&gt;
    # Colour generator&lt;br /&gt;
    colgen = lambda t: lambda x, y, **kwargs: \&lt;br /&gt;
            heatmap((np.imag(func(x, y, t))+1)/2)&lt;br /&gt;
&lt;br /&gt;
    # Absolute z range for axes&lt;br /&gt;
    azmin = -1*zscale&lt;br /&gt;
    azmax = 1*zscale&lt;br /&gt;
&lt;br /&gt;
    # Axis marks&lt;br /&gt;
    xmarks = [(i*xscale, str(i)) for i in range(1, 11)]&lt;br /&gt;
    kmarks = [(j*np.pi/2*kscale, &#039;{}π/2&#039;.format(j if j != 1 else &#039;&#039;) \&lt;br /&gt;
        if j % 2 == 1 else &#039;{}π&#039;.format(j // 2 if j != 2 else &#039;&#039;)) \&lt;br /&gt;
        for j in range(1, 9)]&lt;br /&gt;
    zmarks = [(z/10*zscale, str(z/10)) for z in range(-10, 12, 2)]&lt;br /&gt;
&lt;br /&gt;
    # Hide the 吸牛 splash screen&lt;br /&gt;
    bpy.context.user_preferences.view.show_splash = False&lt;br /&gt;
&lt;br /&gt;
    # Remove existing meshes&lt;br /&gt;
    for item in bpy.context.scene.objects:&lt;br /&gt;
        if item.type == &#039;MESH&#039;:&lt;br /&gt;
            bpy.context.scene.objects.unlink(item)&lt;br /&gt;
    for item in bpy.data.objects:&lt;br /&gt;
        if item.type == &#039;MESH&#039;:&lt;br /&gt;
            bpy.data.objects.remove(item)&lt;br /&gt;
    for item in bpy.data.meshes:&lt;br /&gt;
        bpy.data.meshes.remove(item)&lt;br /&gt;
&lt;br /&gt;
    # Set the camera position&lt;br /&gt;
    bpy.data.objects[&#039;Camera&#039;].location = (11, -6, 5.5)&lt;br /&gt;
    bpy.data.objects[&#039;Camera&#039;].rotation_euler = (1.1, 0, 0.8)&lt;br /&gt;
&lt;br /&gt;
    # Let all texts face the camera&lt;br /&gt;
    rot = np.array(bpy.data.objects[&#039;Camera&#039;].rotation_euler)&lt;br /&gt;
&lt;br /&gt;
    # Initial t=0 plot; this also sets up the axes.&lt;br /&gt;
    print(plot_function(x, k, funcgen(0),&lt;br /&gt;
        True, labels=(&#039;x&#039;, &#039;k&#039;, &#039;z&#039;), text_rot=rot, xmarks=xmarks,&lt;br /&gt;
        ymarks=kmarks, zmarks=zmarks, zmin=azmin, zmax=azmax,&lt;br /&gt;
        colourfunc=colgen(0)))&lt;br /&gt;
&lt;br /&gt;
    # Text label indicating current time&lt;br /&gt;
    ttext_id = add_text(np.array((5.6, -1.2, 0)), &#039;t = {: &amp;gt;4.2f}&#039;.format(0),&lt;br /&gt;
        size=0.05, rotation=rot)&lt;br /&gt;
&lt;br /&gt;
    # Set min/max/current frame&lt;br /&gt;
    bpy.data.scenes[&#039;Scene&#039;].frame_start = 0&lt;br /&gt;
    bpy.data.scenes[&#039;Scene&#039;].frame_end = n_frames - 1&lt;br /&gt;
    bpy.data.scenes[&#039;Scene&#039;].frame_current = 0&lt;br /&gt;
&lt;br /&gt;
    # Add frame change handler. This is what makes the animation happen!&lt;br /&gt;
    bpy.app.handlers.frame_change_pre.append(frame_change)&lt;br /&gt;
&lt;br /&gt;
    # Add some environment lighting&lt;br /&gt;
    wld = bpy.data.worlds[&#039;World&#039;]&lt;br /&gt;
    wld.light_settings.use_environment_light = True&lt;br /&gt;
    wld.light_settings.environment_energy = .5&lt;br /&gt;
&lt;br /&gt;
    # Add a white backdrop plane&lt;br /&gt;
    plane_material = bpy.data.materials.new(&#039;backdrop&#039;)&lt;br /&gt;
    plane_material.diffuse_color = (1, 1, 1)&lt;br /&gt;
    plane_material.use_shadeless = True&lt;br /&gt;
    bpy.ops.mesh.primitive_plane_add(location=(0, 0, -5))&lt;br /&gt;
    bpy.context.active_object.scale = (50, 50, 0)&lt;br /&gt;
    bpy.context.active_object.data.materials.append(plane_material)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Output video ==&lt;br /&gt;
&amp;lt;html5media height=&amp;quot;270&amp;quot; width=&amp;quot;480&amp;quot;&amp;gt;File:Animated 3D plot.ogv&amp;lt;/html5media&amp;gt;&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=Animated_3D_plotting_with_Blender&amp;diff=214</id>
		<title>Animated 3D plotting with Blender</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=Animated_3D_plotting_with_Blender&amp;diff=214"/>
		<updated>2016-11-09T14:08:55Z</updated>

		<summary type="html">&lt;p&gt;Link: Created page with &amp;quot;[https://www.blender.org/ Blender] is an extremely versatile 3D creation and animation suite. Since it is fully scriptable in [https://www.python.org/ Python], Blender may be...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;[https://www.blender.org/ Blender] is an extremely versatile 3D creation and animation suite. Since it is fully scriptable in [https://www.python.org/ Python], Blender may be used to generate &#039;&#039;&#039;animated 3-dimensional plots&#039;&#039;&#039; of data or mathematical functions. Below is an example of one way to generate such a plot.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Important information ==&lt;br /&gt;
The script below has been tested with Blender 2.78a for Linux. It should generally be compatible with other versions and operating systems. The script works with the pristine Blender startup file; heavily modified startup files may break certain assumptions.&lt;br /&gt;
&lt;br /&gt;
To run the script, simply call &amp;lt;code&amp;gt;blender --python blenderplot-ani.py&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To create the plot in a different base file, instead use &amp;lt;code&amp;gt;blender basefile.blend --python blenderplot-ani.py&amp;lt;/code&amp;gt;.&lt;br /&gt;
The order of arguments matters here.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Note that the animation data will not be saved with your .blend file!&#039;&#039;&#039; This means you must run the Python script on a pristine &amp;quot;background&amp;quot; .blend file each time you wish to examine the geometry or create a render. If the .blend file is saved, only the first frame will be captured.&lt;br /&gt;
&lt;br /&gt;
== The script ==&lt;br /&gt;
Code for &amp;lt;code&amp;gt;blenderplot-ani.py&amp;lt;/code&amp;gt; follows.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot; line&amp;gt;&lt;br /&gt;
#!/bin/true&lt;br /&gt;
# vim: se fo=tcroq tw=78 :&lt;br /&gt;
# Simple animated 3D plot example using Blender ( https://www.blender.org/ ).&lt;br /&gt;
# Given the function f(k, x, t)=exp(ikx-iωt), plots Re(f) against x and k,&lt;br /&gt;
# with colour given by Im(f) and t being the time.&lt;br /&gt;
#&lt;br /&gt;
# For pedagogical purposes, this just computes f at each frame (twice: once&lt;br /&gt;
# for the vertex positions and once for their colours). This is horribly&lt;br /&gt;
# inefficient; it would be much better to generate a 3D array for f(x, k, t)&lt;br /&gt;
# once and slice this array for each frame -- however, this is left as an&lt;br /&gt;
# exercise to the reader.&lt;br /&gt;
#&lt;br /&gt;
# To run, call&lt;br /&gt;
#   blender --python blenderplot-ani.py&lt;br /&gt;
&lt;br /&gt;
import os.path&lt;br /&gt;
&lt;br /&gt;
import numpy as np&lt;br /&gt;
&lt;br /&gt;
import bpy&lt;br /&gt;
&lt;br /&gt;
### Begin user settings&lt;br /&gt;
omega = 1&lt;br /&gt;
font = &#039;/usr/share/fonts/cm-unicode/cmunti.ttf&#039; # Must be a unicode font!&lt;br /&gt;
### End user settings&lt;br /&gt;
&lt;br /&gt;
### Begin generic Blender rendering code&lt;br /&gt;
# Global object counter.&lt;br /&gt;
obj_ind = 10000&lt;br /&gt;
&lt;br /&gt;
plot_id = None&lt;br /&gt;
&lt;br /&gt;
line_material = bpy.data.materials.new(&#039;line&#039;)&lt;br /&gt;
line_material.diffuse_color = (0, 0, 0)&lt;br /&gt;
line_material.diffuse_shader = &#039;LAMBERT&#039;&lt;br /&gt;
line_material.specular_color = (0, 0, 0)&lt;br /&gt;
line_material.specular_shader = &#039;COOKTORR&#039;&lt;br /&gt;
line_material.use_shadows = False&lt;br /&gt;
line_material.use_cast_shadows = False&lt;br /&gt;
line_material.use_raytrace = True&lt;br /&gt;
line_material.ambient = 0&lt;br /&gt;
&lt;br /&gt;
text_material = bpy.data.materials.new(&#039;text&#039;)&lt;br /&gt;
text_material.diffuse_color = (.15, .05, .035)&lt;br /&gt;
text_material.diffuse_shader = &#039;OREN_NAYAR&#039;&lt;br /&gt;
text_material.diffuse_intensity = .9&lt;br /&gt;
text_material.roughness = 2&lt;br /&gt;
text_material.specular_color = (.6, .2, .1)&lt;br /&gt;
text_material.specular_shader = &#039;PHONG&#039;&lt;br /&gt;
text_material.specular_hardness = 80&lt;br /&gt;
text_material.specular_intensity = .85&lt;br /&gt;
text_material.use_shadows = True&lt;br /&gt;
text_material.use_cast_shadows = False&lt;br /&gt;
text_material.use_raytrace = True&lt;br /&gt;
text_material.raytrace_mirror.use = True&lt;br /&gt;
text_material.mirror_color = (.7, .3, .15)&lt;br /&gt;
text_material.raytrace_mirror.reflect_factor = .3&lt;br /&gt;
text_material.emit = 0&lt;br /&gt;
text_material.ambient = 0&lt;br /&gt;
&lt;br /&gt;
plot_material = bpy.data.materials.new(&#039;plot&#039;)&lt;br /&gt;
plot_material.specular_color = (.5, .5, .5)&lt;br /&gt;
plot_material.specular_shader = &#039;COOKTORR&#039;&lt;br /&gt;
plot_material.specular_intensity = .2&lt;br /&gt;
plot_material.use_shadows = True&lt;br /&gt;
plot_material.use_transparent_shadows = True&lt;br /&gt;
plot_material.use_raytrace = True&lt;br /&gt;
plot_material.use_transparency = True&lt;br /&gt;
plot_material.transparency_method = &#039;RAYTRACE&#039;&lt;br /&gt;
plot_material.alpha = .95&lt;br /&gt;
plot_material.specular_alpha = 1&lt;br /&gt;
plot_material.raytrace_transparency.depth = 5&lt;br /&gt;
plot_material.use_vertex_color_paint = True&lt;br /&gt;
&lt;br /&gt;
text_font = bpy.data.fonts.load(os.path.abspath(os.path.expanduser(font)))&lt;br /&gt;
&lt;br /&gt;
def heatmap(heat):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Heat map: given a &amp;quot;heat&amp;quot; between 0 and 1, return a tuple of RGB&lt;br /&gt;
    values.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    r = np.max((2*heat-1., 0))&lt;br /&gt;
    b = np.max((1.-2*heat, 0))&lt;br /&gt;
    return (r, 1.-r-b, b)&lt;br /&gt;
&lt;br /&gt;
def zheat(z, zmin, zmax, **kwargs):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Colour a vertex based on its z-height compared to the minimum and&lt;br /&gt;
    maximum z-values that occur.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    return heatmap((z-zmin)/(zmax-zmin if zmin != zmax else 1))&lt;br /&gt;
&lt;br /&gt;
def plot_function(x, y, func, auto_axes = True, xmarks=None,&lt;br /&gt;
        ymarks = None, zmarks = None, labels = None, thickness = 0.025,&lt;br /&gt;
        text_rot = None, colourfunc=zheat, zmin = None, zmax = None):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Plot the function (lambda) func of x and y. The resulting surface is&lt;br /&gt;
    smooth-shaded. Vertices may be coloured according to colourfunc: this is a&lt;br /&gt;
    function that accepts the following parameters and returns an RGB-tuple:&lt;br /&gt;
        x, y, z, xmin, xmax, ymin, ymax, zmin, zmax&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    global obj_ind, plot_id&lt;br /&gt;
&lt;br /&gt;
    ids = {&lt;br /&gt;
        &#039;axes&#039;: [],&lt;br /&gt;
        &#039;axis_labels&#039;: [],&lt;br /&gt;
        &#039;xmarks&#039;: [],&lt;br /&gt;
        &#039;ymarks&#039;: [],&lt;br /&gt;
        &#039;zmarks&#039;: [],&lt;br /&gt;
        &#039;xlabels&#039;: [],&lt;br /&gt;
        &#039;ylabels&#039;: [],&lt;br /&gt;
        &#039;zlabels&#039;: []&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    if text_rot is None:&lt;br /&gt;
        text_rot = np.array((0, 0, 0))&lt;br /&gt;
&lt;br /&gt;
    if plot_id is None:&lt;br /&gt;
        obj_id = &#039;plot_{}&#039;.format(obj_ind)&lt;br /&gt;
        obj_ind += 1&lt;br /&gt;
        # Generate all vertices in the plot at z = 0&lt;br /&gt;
        verts = [(i, j, 0) for i in x for j in y]&lt;br /&gt;
        faces = []&lt;br /&gt;
        count = 0&lt;br /&gt;
        # Build faces from the vertices&lt;br /&gt;
        for i in range(len(y)*(len(x)-1)):&lt;br /&gt;
            if count &amp;lt; len(y)-1:&lt;br /&gt;
                faces.append((i, i+1, i+len(y)+1, i+len(y)))&lt;br /&gt;
                count += 1&lt;br /&gt;
            else:&lt;br /&gt;
                count = 0&lt;br /&gt;
&lt;br /&gt;
        # Create a mesh and an object at the origin&lt;br /&gt;
        mesh = bpy.data.meshes.new(obj_id)&lt;br /&gt;
        obj = bpy.data.objects.new(obj_id, mesh)&lt;br /&gt;
        obj.location = (0, 0, 0)&lt;br /&gt;
        bpy.context.scene.objects.link(obj)&lt;br /&gt;
        mesh.from_pydata(verts, [], faces)&lt;br /&gt;
        mesh.update(calc_edges=True)&lt;br /&gt;
&lt;br /&gt;
        # Create a new vertex colour map&lt;br /&gt;
        colours = obj.data.vertex_colors.new()&lt;br /&gt;
&lt;br /&gt;
        # Set material&lt;br /&gt;
        obj.data.materials.append(plot_material)&lt;br /&gt;
&lt;br /&gt;
        # Smooth-shade polygons&lt;br /&gt;
        for pol in obj.data.polygons:&lt;br /&gt;
            pol.use_smooth = True&lt;br /&gt;
    else:&lt;br /&gt;
        obj = bpy.data.objects[plot_id]&lt;br /&gt;
        colours = obj.data.vertex_colors.active&lt;br /&gt;
&lt;br /&gt;
    verts = obj.data.vertices&lt;br /&gt;
&lt;br /&gt;
    # Move vertices to their correct position&lt;br /&gt;
    for v in verts:&lt;br /&gt;
        v.co.z = func(v.co.x, v.co.y)&lt;br /&gt;
    obj.data.update(calc_edges=True)&lt;br /&gt;
&lt;br /&gt;
    sv = sorted([(v.co.x, v.co.y, v.co.z) for v in verts], key=lambda q: q[2])&lt;br /&gt;
&lt;br /&gt;
    # Colour vertices&lt;br /&gt;
    for pol in obj.data.polygons:&lt;br /&gt;
        for idx in pol.loop_indices:&lt;br /&gt;
            co = obj.data.vertices[obj.data.loops[idx].vertex_index].co&lt;br /&gt;
            colours.data[idx].color = colourfunc(x=co.x, y=co.y, z=co.z,&lt;br /&gt;
                xmin=np.min(x), xmax=np.max(x),&lt;br /&gt;
                ymin=np.min(y), ymax=np.max(y),&lt;br /&gt;
                zmin=sv[0][2], zmax=sv[-1][2])&lt;br /&gt;
&lt;br /&gt;
    if auto_axes and plot_id is None:&lt;br /&gt;
        # Axes&lt;br /&gt;
        ids[&#039;axes&#039;].append(add_line(np.array((min(x), min(y), 0)),&lt;br /&gt;
            np.array((max(x), min(y), 0)), thickness, False))&lt;br /&gt;
        ids[&#039;axes&#039;].append(add_line(np.array((max(x), min(y), 0)),&lt;br /&gt;
            np.array((max(x), max(y), 0)), thickness, False))&lt;br /&gt;
        ids[&#039;axes&#039;].append(add_line(&lt;br /&gt;
            np.array((min(x), min(y), sv[0][2] if zmin is None else zmin)),&lt;br /&gt;
            np.array((min(x), min(y), sv[-1][2] if zmax is None else zmax)),&lt;br /&gt;
            thickness, False))&lt;br /&gt;
        # Axis marks&lt;br /&gt;
        if xmarks is not None:&lt;br /&gt;
            for pos, label in xmarks:&lt;br /&gt;
                p = np.array((pos, min(y), 0))&lt;br /&gt;
                ids[&#039;xmarks&#039;].append(add_line(p,&lt;br /&gt;
                    p-np.array((0, 1.5*thickness, 0)), thickness, False))&lt;br /&gt;
                if label is not None and len(label) &amp;gt; 0:&lt;br /&gt;
                    ids[&#039;xlabels&#039;].append(add_text(&lt;br /&gt;
                        p-np.array((0, 7*thickness, 0)), label,&lt;br /&gt;
                        thickness, text_rot))&lt;br /&gt;
        if ymarks is not None:&lt;br /&gt;
            for pos, label in ymarks:&lt;br /&gt;
                p = np.array((max(x), pos, 0))&lt;br /&gt;
                ids[&#039;ymarks&#039;].append(add_line(p,&lt;br /&gt;
                    p+np.array((1.5*thickness, 0, 0)), thickness, False))&lt;br /&gt;
                if label is not None and len(label) &amp;gt; 0:&lt;br /&gt;
                    ids[ &#039;ylabels&#039;].append(add_text(&lt;br /&gt;
                        p+np.array((7*thickness, 0, 0)), label,&lt;br /&gt;
                        thickness, text_rot))&lt;br /&gt;
        if zmarks is not None:&lt;br /&gt;
            for pos, label in zmarks:&lt;br /&gt;
                p = np.array((min(x), min(y), pos))&lt;br /&gt;
                ids[&#039;zmarks&#039;].append(add_line(p,&lt;br /&gt;
                    p-np.array((1.5*thickness/np.sqrt(2),&lt;br /&gt;
                        1.5*thickness/np.sqrt(2), 0)), thickness, False))&lt;br /&gt;
                if label is not None and len(label) &amp;gt; 0:&lt;br /&gt;
                    ids[&#039;zlabels&#039;].append(add_text(&lt;br /&gt;
                        p-np.array((7*thickness, 7*thickness, 0))/np.sqrt(2),&lt;br /&gt;
                        label, thickness, text_rot))&lt;br /&gt;
&lt;br /&gt;
        # Axis labels&lt;br /&gt;
        if labels is not None:&lt;br /&gt;
            ids[&#039;axis_labels&#039;].append(add_text(&lt;br /&gt;
                np.array((max(x)+8*thickness, min(y), 0)),&lt;br /&gt;
                labels[0], 2*thickness, text_rot))&lt;br /&gt;
            ids[&#039;axis_labels&#039;].append(add_text(&lt;br /&gt;
                np.array((max(x), max(y)+8*thickness, 0)),&lt;br /&gt;
                labels[1], 2*thickness, text_rot))&lt;br /&gt;
            ids[&#039;axis_labels&#039;].append(add_text(&lt;br /&gt;
                np.array((min(x), min(y),&lt;br /&gt;
                    (sv[-1][2] if zmax is None else zmax) + 8*thickness)),&lt;br /&gt;
                labels[2], 2*thickness, text_rot))&lt;br /&gt;
&lt;br /&gt;
    if plot_id is None:&lt;br /&gt;
        plot_id = obj_id&lt;br /&gt;
&lt;br /&gt;
    ids[&#039;plot&#039;] = plot_id&lt;br /&gt;
&lt;br /&gt;
    return ids&lt;br /&gt;
&lt;br /&gt;
def add_text(r, text, size=0.025, rotation=None):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Add text at the position r. Size is a relative parameter; use&lt;br /&gt;
    trial-and-error here.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    global obj_ind&lt;br /&gt;
    obj_id = &#039;text_{}&#039;.format(obj_ind)&lt;br /&gt;
    obj_ind += 1&lt;br /&gt;
    rot = [np.pi/2, 0, 0] if rotation is None else rotation.tolist()&lt;br /&gt;
    bpy.ops.object.text_add(location=r.tolist(), rotation=rot)&lt;br /&gt;
    obj = bpy.context.active_object&lt;br /&gt;
    obj.name = obj_id&lt;br /&gt;
    obj.data.name = obj_id&lt;br /&gt;
    obj.data.body = text&lt;br /&gt;
&lt;br /&gt;
    # Set the font&lt;br /&gt;
    obj.data.font = text_font&lt;br /&gt;
&lt;br /&gt;
    obj.data.offset_x = -2*size&lt;br /&gt;
    obj.data.offset_y = -2*size&lt;br /&gt;
    obj.data.shear = 0.0&lt;br /&gt;
    obj.data.size = 8*size&lt;br /&gt;
    obj.data.space_character = 1&lt;br /&gt;
    obj.data.space_word = 4*size&lt;br /&gt;
    obj.data.extrude = size/3&lt;br /&gt;
&lt;br /&gt;
    obj.data.materials.append(text_material)&lt;br /&gt;
&lt;br /&gt;
    return obj_id&lt;br /&gt;
&lt;br /&gt;
def add_line(r1, r2, w=0.01, rel_w=True):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Add a &amp;quot;line&amp;quot; (cylinder) between the points r1 and r2. The width is&lt;br /&gt;
    either w (if rel_w is False) or w*|r2-r1| (if rel_w is True).&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    global obj_ind&lt;br /&gt;
    obj_id = &#039;line_{}&#039;.format(obj_ind)&lt;br /&gt;
    obj_ind += 1&lt;br /&gt;
    rc = (r2+r1)/2 # Centroid&lt;br /&gt;
    rr = r2-rc # Position of r2 relative to centroid&lt;br /&gt;
    r = np.sqrt(np.sum(rr**2))&lt;br /&gt;
    theta = np.arccos(rr[2]/r)&lt;br /&gt;
    phi = np.arctan2(rr[1], rr[0])&lt;br /&gt;
    bpy.ops.mesh.primitive_cylinder_add(vertices=16,&lt;br /&gt;
            radius=.5*w*(r if rel_w else 1), depth=2*r,&lt;br /&gt;
            location=rc.tolist(), rotation=(0, theta, phi))&lt;br /&gt;
    obj = bpy.context.active_object&lt;br /&gt;
    obj.name = obj_id&lt;br /&gt;
    for pol in obj.data.polygons:&lt;br /&gt;
        pol.use_smooth = True&lt;br /&gt;
    obj.data.materials.append(line_material)&lt;br /&gt;
&lt;br /&gt;
    return obj_id&lt;br /&gt;
### End generic Blender rendering code&lt;br /&gt;
&lt;br /&gt;
def frame_change(scene):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Update the plot for a given frame.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    frame = min(max(scene.frame_current, 0), n_frames - 1)&lt;br /&gt;
    plot_function(x, k, funcgen(t[frame]), colourfunc=colgen(t[frame]))&lt;br /&gt;
    # Update the t-indicator&lt;br /&gt;
    bpy.data.objects[ttext_id].data.body = &#039;t = {: &amp;gt;4.3f}&#039;.format(t[frame])&lt;br /&gt;
&lt;br /&gt;
if __name__ == &#039;__main__&#039;:&lt;br /&gt;
    # Set up x, y and t data&lt;br /&gt;
    n_frames = 51&lt;br /&gt;
    nx = 101&lt;br /&gt;
    nk = 101&lt;br /&gt;
    xscale = 1/2&lt;br /&gt;
    kscale = 1/3&lt;br /&gt;
    zscale = 2&lt;br /&gt;
    x = np.linspace(0, 10, nx)*xscale&lt;br /&gt;
    k = np.linspace(0, 4*np.pi, nk)*kscale&lt;br /&gt;
    t = np.linspace(0, 10, n_frames)&lt;br /&gt;
&lt;br /&gt;
    # Function to plot&lt;br /&gt;
    func = lambda x, k, t: np.exp(1j*(k*x-omega*t))*zscale&lt;br /&gt;
    # Generator for plottable function f(x, k) at time t&lt;br /&gt;
    funcgen = lambda t: lambda x, k: np.real(func(x, k, t))&lt;br /&gt;
    # Colour generator&lt;br /&gt;
    colgen = lambda t: lambda x, y, **kwargs: \&lt;br /&gt;
            heatmap((np.imag(func(x, y, t))+1)/2)&lt;br /&gt;
&lt;br /&gt;
    # Absolute z range for axes&lt;br /&gt;
    azmin = -1*zscale&lt;br /&gt;
    azmax = 1*zscale&lt;br /&gt;
&lt;br /&gt;
    # Axis marks&lt;br /&gt;
    xmarks = [(i*xscale, str(i)) for i in range(1, 11)]&lt;br /&gt;
    kmarks = [(j*np.pi/2*kscale, &#039;{}π/2&#039;.format(j if j != 1 else &#039;&#039;) \&lt;br /&gt;
        if j % 2 == 1 else &#039;{}π&#039;.format(j // 2 if j != 2 else &#039;&#039;)) \&lt;br /&gt;
        for j in range(1, 9)]&lt;br /&gt;
    zmarks = [(z/10*zscale, str(z/10)) for z in range(-10, 12, 2)]&lt;br /&gt;
&lt;br /&gt;
    # Hide the 吸牛 splash screen&lt;br /&gt;
    bpy.context.user_preferences.view.show_splash = False&lt;br /&gt;
&lt;br /&gt;
    # Remove existing meshes&lt;br /&gt;
    for item in bpy.context.scene.objects:&lt;br /&gt;
        if item.type == &#039;MESH&#039;:&lt;br /&gt;
            bpy.context.scene.objects.unlink(item)&lt;br /&gt;
    for item in bpy.data.objects:&lt;br /&gt;
        if item.type == &#039;MESH&#039;:&lt;br /&gt;
            bpy.data.objects.remove(item)&lt;br /&gt;
    for item in bpy.data.meshes:&lt;br /&gt;
        bpy.data.meshes.remove(item)&lt;br /&gt;
&lt;br /&gt;
    # Set the camera position&lt;br /&gt;
    bpy.data.objects[&#039;Camera&#039;].location = (11, -6, 5.5)&lt;br /&gt;
    bpy.data.objects[&#039;Camera&#039;].rotation_euler = (1.1, 0, 0.8)&lt;br /&gt;
&lt;br /&gt;
    # Let all texts face the camera&lt;br /&gt;
    rot = np.array(bpy.data.objects[&#039;Camera&#039;].rotation_euler)&lt;br /&gt;
&lt;br /&gt;
    # Initial t=0 plot; this also sets up the axes.&lt;br /&gt;
    print(plot_function(x, k, funcgen(0),&lt;br /&gt;
        True, labels=(&#039;x&#039;, &#039;k&#039;, &#039;z&#039;), text_rot=rot, xmarks=xmarks,&lt;br /&gt;
        ymarks=kmarks, zmarks=zmarks, zmin=azmin, zmax=azmax,&lt;br /&gt;
        colourfunc=colgen(0)))&lt;br /&gt;
&lt;br /&gt;
    # Text label indicating current time&lt;br /&gt;
    ttext_id = add_text(np.array((5.6, -1.2, 0)), &#039;t = {: &amp;gt;4.2f}&#039;.format(0),&lt;br /&gt;
        size=0.05, rotation=rot)&lt;br /&gt;
&lt;br /&gt;
    # Set min/max/current frame&lt;br /&gt;
    bpy.data.scenes[&#039;Scene&#039;].frame_start = 0&lt;br /&gt;
    bpy.data.scenes[&#039;Scene&#039;].frame_end = n_frames - 1&lt;br /&gt;
    bpy.data.scenes[&#039;Scene&#039;].frame_current = 0&lt;br /&gt;
&lt;br /&gt;
    # Add frame change handler. This is what makes the animation happen!&lt;br /&gt;
    bpy.app.handlers.frame_change_pre.append(frame_change)&lt;br /&gt;
&lt;br /&gt;
    # Add some environment lighting&lt;br /&gt;
    wld = bpy.data.worlds[&#039;World&#039;]&lt;br /&gt;
    wld.light_settings.use_environment_light = True&lt;br /&gt;
    wld.light_settings.environment_energy = .5&lt;br /&gt;
&lt;br /&gt;
    # Add a white backdrop plane&lt;br /&gt;
    plane_material = bpy.data.materials.new(&#039;backdrop&#039;)&lt;br /&gt;
    plane_material.diffuse_color = (1, 1, 1)&lt;br /&gt;
    plane_material.use_shadeless = True&lt;br /&gt;
    bpy.ops.mesh.primitive_plane_add(location=(0, 0, -5))&lt;br /&gt;
    bpy.context.active_object.scale = (50, 50, 0)&lt;br /&gt;
    bpy.context.active_object.data.materials.append(plane_material)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Output video ==&lt;br /&gt;
[[File:Animated 3D plot.ogv]]&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=File:Animated_3D_plot.ogv&amp;diff=213</id>
		<title>File:Animated 3D plot.ogv</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=File:Animated_3D_plot.ogv&amp;diff=213"/>
		<updated>2016-11-09T13:50:37Z</updated>

		<summary type="html">&lt;p&gt;Link: Example of an animated 3D plot made with Blender. See Animated 3D plotting with Blender.&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Example of an animated 3D plot made with Blender. See [[Animated 3D plotting with Blender]].&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=Main_Page&amp;diff=212</id>
		<title>Main Page</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=Main_Page&amp;diff=212"/>
		<updated>2016-04-15T10:42:13Z</updated>

		<summary type="html">&lt;p&gt;Link: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Welcome to &#039;&#039;&#039;Penguin Development&#039;&#039;&#039;, a (currently one-man) organisation developing [[w:free and open-source software|free and open-source software]] and [[w:Open-source hardware|hardware]].&lt;br /&gt;
&lt;br /&gt;
See below for current releases. All software and hardware is licensed under the [https://gnu.org/licenses/gpl.html GPL] (version 3 or higher) unless otherwise stated.&lt;br /&gt;
&lt;br /&gt;
=Software=&lt;br /&gt;
* [[AUTOMOME]] — the OTTish AUTOMEME clone&lt;br /&gt;
* [[Ising]] — Monte Carlo Ising model simulator&lt;br /&gt;
* [[stackermann]] — a stack-based [[w:Ackermann function|Ackermann function]] calculator&lt;br /&gt;
&lt;br /&gt;
=Hardware=&lt;br /&gt;
* [[timelapse]] — time-lapse trigger for digital cameras&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=Main_Page&amp;diff=211</id>
		<title>Main Page</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=Main_Page&amp;diff=211"/>
		<updated>2016-04-15T10:41:53Z</updated>

		<summary type="html">&lt;p&gt;Link: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Welcome to &#039;&#039;&#039;Penguin Development&#039;&#039;&#039;, a (currently one-man) organisation developing [[w:free and open-source software|free and open-source software]] and [[w:Open-source hardware|hardware]].&lt;br /&gt;
&lt;br /&gt;
See below for current releases. All software and hardware is licensed under the [https://gnu.org/licenses/gpl.html GPL] (version 3 or higher) unless otherwise stated.&lt;br /&gt;
&lt;br /&gt;
=Software=&lt;br /&gt;
* [[AUTOMOME]] — the OTTish AUTOMEME clone&lt;br /&gt;
* [[Ising]] — Monte-Carlo Ising model simulator&lt;br /&gt;
* [[stackermann]] — a stack-based [[w:Ackermann function|Ackermann function]] calculator&lt;br /&gt;
&lt;br /&gt;
=Hardware=&lt;br /&gt;
* [[timelapse]] — time-lapse trigger for digital cameras&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=Ising&amp;diff=210</id>
		<title>Ising</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=Ising&amp;diff=210"/>
		<updated>2016-04-15T10:41:03Z</updated>

		<summary type="html">&lt;p&gt;Link: Created page with &amp;quot;&amp;#039;&amp;#039;&amp;#039;Ising&amp;#039;&amp;#039;&amp;#039; is a Monte Carlo Ising model simulator on a &amp;#039;&amp;#039;D&amp;#039;&amp;#039;-cubic lattice of sides &amp;#039;&amp;#039;L&amp;#039;&amp;#039;, using helical boundary conditions. &amp;#039;&amp;#039;D&amp;#039;&amp;#039;...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;Ising&#039;&#039;&#039; is a [[w:Monte Carlo method|Monte Carlo]] [[w:Ising model|Ising model]] simulator on a &#039;&#039;D&#039;&#039;-cubic lattice of sides &#039;&#039;L&#039;&#039;, using helical boundary conditions. &#039;&#039;D&#039;&#039; and &#039;&#039;L&#039;&#039; are specified at compile-time. It is written in C++11.&lt;br /&gt;
&lt;br /&gt;
==Dependencies==&lt;br /&gt;
* GNU getopt&lt;br /&gt;
* A recent version of GCC (tested 4.8.5)&lt;br /&gt;
* boost::filesystem (optional but highly recommended)&lt;br /&gt;
&lt;br /&gt;
==Obtaining and installing Ising==&lt;br /&gt;
Ising can be obtained from http://proj.penguindevelopment.org/ising/. [http://proj.penguindevelopment.org/ising/ising-latest.tar.xz Direct link to the latest version.]&lt;br /&gt;
&lt;br /&gt;
Ising is installed using the standard autotools procedure, whereby &#039;&#039;L&#039;&#039; may be set through the parameter &#039;&#039;GRIDSIZE&#039;&#039;:&amp;lt;pre&amp;gt;./configure GRIDSIZE=100&lt;br /&gt;
make&lt;br /&gt;
make install&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
This will build Ising in 1D, 2D, 3D and 4D.&lt;br /&gt;
&lt;br /&gt;
Ising can be built on Windows using Cygwin, though the execution speed appears to be suboptimal.&lt;br /&gt;
&lt;br /&gt;
==Using Ising==&lt;br /&gt;
See the included man-page for information on how to use Ising.&lt;br /&gt;
&lt;br /&gt;
==Using Ising as an API==&lt;br /&gt;
Since Ising makes heavy use of C++ templates, the implementation is entirely contained within header files. This means that no further libraries are necessary. To use Ising as an API, simply&amp;lt;pre&amp;gt;&lt;br /&gt;
#include &amp;lt;ising/ising.h&amp;gt;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Bugs and feedback==&lt;br /&gt;
{{bugs and feedback}}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[Category:Software]]&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=AUTOMOME&amp;diff=209</id>
		<title>AUTOMOME</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=AUTOMOME&amp;diff=209"/>
		<updated>2015-02-25T22:12:53Z</updated>

		<summary type="html">&lt;p&gt;Link: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;AUTOMOME&#039;&#039;&#039; is a random [[w:Internet meme|internet meme]] generator based on [[w:Snowclone|snowclone]] templates. It is a black-box reimplementation and extension of [http://automeme.net/ AUTOMEME] (now defunct), based on the English dialect known as &#039;&#039;OTTish&#039;&#039;, spoken in the [http://forums.xkcd.com/viewtopic.php?f=7&amp;amp;t=101043 xkcd forums thread of the Time comic], better known as the &#039;&#039;One True Thread&#039;&#039; or &#039;&#039;OTC&#039;&#039;. The canonical web version is available at http://automome.penguindevelopment.org/.&lt;br /&gt;
&lt;br /&gt;
=Dependencies=&lt;br /&gt;
* Python 3, tested versions 3.2.3 and 3.4.1&lt;br /&gt;
* Unicode-compatible system&lt;br /&gt;
* Apache web sever or compatible (hosted web version only)&lt;br /&gt;
* Modern graphical web browser (web client only)&lt;br /&gt;
* Qt 4 and PySide 1.2.x (qautomome only)&lt;br /&gt;
&lt;br /&gt;
=Obtaining and installing AUTOMOME=&lt;br /&gt;
AUTOMOME can be obtained from http://proj.penguindevelopment.org/automome/. [http://proj.penguindevelopment.org/automome/automome-latest.tar.xz Direct link to the latest version.]&lt;br /&gt;
&lt;br /&gt;
AUTOMOME is designed to run from its own directory; &amp;lt;code&amp;gt;cd&amp;lt;/code&amp;gt;ing to the directory and calling &amp;lt;code&amp;gt;automome.py&amp;lt;/code&amp;gt; directly should work. Likewise, the web version should work out of the box if the AUTOMOME directory is in a location served by Apache.&lt;br /&gt;
&lt;br /&gt;
If a setup is desired where the dictionary files (&amp;lt;code&amp;gt;*.dict.txt&amp;lt;/code&amp;gt;) are placed in a different directory from the script files, the &amp;lt;code&amp;gt;AUTOMOME_PATH&amp;lt;/code&amp;gt; environment variable may be set to the path to the dictionary files. &amp;lt;code&amp;gt;automome-web.py&amp;lt;/code&amp;gt; may be placed in a different directory from &amp;lt;code&amp;gt;automome.py&amp;lt;/code&amp;gt;, so long as &amp;lt;code&amp;gt;automome.py&amp;lt;/code&amp;gt; is in the module search path; see [https://docs.python.org/3/tutorial/modules.html#the-module-search-path].&lt;br /&gt;
&lt;br /&gt;
The client-side web interface &amp;lt;code&amp;gt;automome.html&amp;lt;/code&amp;gt; by default attempts to get its data from &amp;lt;code&amp;gt;automome-web.py&amp;lt;/code&amp;gt; at the same base URL as &amp;lt;code&amp;gt;automome.html&amp;lt;/code&amp;gt;: e.g. if you host a web version of AUTOMOME at http://example.com/molpy/grapevine/automome.html, it will try to fetch data from http://example.com/molpy/grapevine/automome-web.py. Likewise, it will try to locate &amp;lt;code&amp;gt;cuegan.svg&amp;lt;/code&amp;gt; in the same directory.&lt;br /&gt;
&lt;br /&gt;
=Using AUTOMOME=&lt;br /&gt;
There are several ways to use AUTOMOME: as of version 4.0, it can be used as a text-only command-line version (&amp;lt;code&amp;gt;automome.py&amp;lt;/code&amp;gt;), on the web using a fancy AJAX interface that gives nice formatting (&amp;lt;code&amp;gt;automome.html&amp;lt;/code&amp;gt;, and the canonical version at http://automome.penguindevelopment.org/), as a text-only web version (&amp;lt;code&amp;gt;automome-web.py&amp;lt;/code&amp;gt;), or using the graphical &#039;&#039;&#039;qautomome&#039;&#039;&#039; (&amp;lt;code&amp;gt;qautomome.py&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==Using the command-line version==&lt;br /&gt;
The plaintext command-line version can be used on any system with Python 3.x, and is perhaps the simplest way to use AUTOMOME. At present, it supports only two significant options: picking the categories and the number of memes displayed. &amp;lt;code&amp;gt;automome.py --help&amp;lt;/code&amp;gt; should provide an adequate explanation:&amp;lt;pre&amp;gt;$ automome.py --help&lt;br /&gt;
Usage: automome.py [option] [num]&lt;br /&gt;
[option] may be one of the following:&lt;br /&gt;
    -h, --help                      show this message and exit&lt;br /&gt;
    -V, --version                   show version and exit&lt;br /&gt;
    -c [cats], --categories=[cats]  generate memes from the categories [cats]&lt;br /&gt;
                                    (see below); the default is aom&lt;br /&gt;
&lt;br /&gt;
[cats] may be any combination of the following letters:&lt;br /&gt;
    a       molpish memes from the original AUTOMEME (possibly OTTified)&lt;br /&gt;
    o       molpish other memes, i.e. non-AUTOMEME memes&lt;br /&gt;
    m       molpish meta-memes, i.e. memes originating in the OTT&lt;br /&gt;
    A       unmolpish AUTOMEME memes&lt;br /&gt;
    O       unmolpish other memes&lt;br /&gt;
    t       OTToMeme memes (xkcd memes by azule)&lt;br /&gt;
&lt;br /&gt;
[num] is the number of memes to generate; if it is missing or invalid, it is&lt;br /&gt;
assumed to be 1.&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
As an example, the following code produces 10 memes from the &amp;quot;Molpish other&amp;quot; and &amp;quot;Meta-memes&amp;quot; categories:&amp;lt;pre&amp;gt;&lt;br /&gt;
automome.py -c om 10&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Using the web GUI==&lt;br /&gt;
The [http://automome.penguindevelopment.org/ web GUI] offers more advanced features, including formatted text and the ability to filter memes. A quick run-through of features:&lt;br /&gt;
===Main layout===&lt;br /&gt;
From the main layout, clicking the image of Cuegan produces a single meme. There is not much more to be said.&lt;br /&gt;
&lt;br /&gt;
===Meme categories===&lt;br /&gt;
Clicking the &#039;&#039;Meme categories&#039;&#039; link allows you to change the categories of memes that will be displayed; the categories are the same as those used in the text-only version.&lt;br /&gt;
&lt;br /&gt;
===Filter settings===&lt;br /&gt;
The &#039;&#039;Filter settings&#039;&#039; tab is slightly more complicated. First and foremost is the &#039;&#039;Filter expression&#039;&#039; box. This allows you to enter any [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions JavaScript regular expression], and only memes matching that expression will be displayed. Keep in mind that the regular expression is case sensitive; almost all words are uppercase. The regular expression is applied to the text-only version of the meme, so italicised words will be surrounded by underscores, and so on. If you wish to match all memes, leave the box blank or enter &amp;lt;code&amp;gt;.*&amp;lt;/code&amp;gt;. &lt;br /&gt;
&lt;br /&gt;
The &#039;&#039;Lock meme&#039;&#039; checkbox allows you to lock the template of the current meme: if it is active, clicking the Cuegan image will only produce memes using the same template as the currently shown meme.&lt;br /&gt;
&lt;br /&gt;
===Copyable formats===&lt;br /&gt;
The &#039;&#039;Copyable formats&#039;&#039; tab provides three easy-to-copy output styles: plaintext (the same as &amp;lt;code&amp;gt;automome.py&amp;lt;/code&amp;gt; itself would produce), BBCode (used on most forums) and (X)HTML, which can be used on your own websites.&lt;br /&gt;
&lt;br /&gt;
==Using the text-only web version==&lt;br /&gt;
The text-only web version is a lightweight wrapper around the command-line version. In fact, if it is at all possible, you should use the command-line version directly. Nevertheless, if you still wish to use the text-only web version, simply point a browser at automome-web.py, either at your site or [http://automome.penguindevelopment.org/automome-web.py the canonical version]. Categories can be passed using the &amp;lt;code&amp;gt;c=&amp;lt;/code&amp;gt; GET-parameter, and the number of memes can be passed using the &amp;lt;code&amp;gt;n=&amp;lt;/code&amp;gt; GET-parameter. The categories are the same as the ones used in the text-only version, and the number of memes is limited to at most 100 at a time.&lt;br /&gt;
&lt;br /&gt;
As an example, http://automome.penguindevelopment.org/automome-web.py?n=10&amp;amp;c=om produces 10 memes from the &amp;quot;Molpish other&amp;quot; and &amp;quot;Meta-memes&amp;quot; categories.&lt;br /&gt;
&lt;br /&gt;
=Using qautomome=&lt;br /&gt;
qautomome is essentially a stand-alone version of the web client; most of the web client&#039;s feature descriptions transfer directly to qautomome and therefore aren&#039;t listed separately in this section. One notable capability of qautomome is the export function: qautomome can export the full history of memes to a file (or it can generate a set amount of new memes to export); the currently supported formats are [[w:Comma-separated values|CSV]] (which has plaintext, BBCode and (X)HTML fields) and [[w:fortune (Unix)|fortune]]. You can find this feature in the Copy/export tab.&lt;br /&gt;
&lt;br /&gt;
In qautomome, there is no separate back-button; you can instead right-click Cuegan to view the previous meme. Keyboard-based operation is also possible; use the space bar, enter key or right arrow key for the next meme and backspace or the left arrow key for the previous one. Holding shift inverts the function.&lt;br /&gt;
&lt;br /&gt;
==Bugs and feedback==&lt;br /&gt;
{{bugs and feedback}}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[Category:Software]]&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=Main_Page&amp;diff=208</id>
		<title>Main Page</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=Main_Page&amp;diff=208"/>
		<updated>2015-02-16T21:02:52Z</updated>

		<summary type="html">&lt;p&gt;Link: /* Software */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;Welcome to &#039;&#039;&#039;Penguin Development&#039;&#039;&#039;, a (currently one-man) organisation developing [[w:free and open-source software|free and open-source software]] and [[w:Open-source hardware|hardware]].&lt;br /&gt;
&lt;br /&gt;
See below for current releases. All software and hardware is licensed under the [https://gnu.org/licenses/gpl.html GPL] (version 3 or higher) unless otherwise stated.&lt;br /&gt;
&lt;br /&gt;
=Software=&lt;br /&gt;
* [[AUTOMOME]] — the OTTish AUTOMEME clone&lt;br /&gt;
* [[stackermann]] — a stack-based [[w:Ackermann function|Ackermann function]] calculator&lt;br /&gt;
&lt;br /&gt;
=Hardware=&lt;br /&gt;
* [[timelapse]] — time-lapse trigger for digital cameras&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=AUTOMOME&amp;diff=207</id>
		<title>AUTOMOME</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=AUTOMOME&amp;diff=207"/>
		<updated>2015-02-16T21:01:47Z</updated>

		<summary type="html">&lt;p&gt;Link: Created page with &amp;quot;&amp;#039;&amp;#039;&amp;#039;AUTOMOME&amp;#039;&amp;#039;&amp;#039; is a random internet meme generator based on snowclone templates. It is a black-box reimplementation and extension of [http:...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;AUTOMOME&#039;&#039;&#039; is a random [[w:Internet meme|internet meme]] generator based on [[w:Snowclone|snowclone]] templates. It is a black-box reimplementation and extension of [http://automeme.net/ AUTOMEME] (now defunct), based on the English dialect known as &#039;&#039;OTTish&#039;&#039;, spoken in the [http://forums.xkcd.com/viewtopic.php?f=7&amp;amp;t=101043 xkcd forums thread of the Time comic], better known as the &#039;&#039;One True Thread&#039;&#039; or &#039;&#039;OTC&#039;&#039;. The canonical web version is available at http://automome.penguindevelopment.org/.&lt;br /&gt;
&lt;br /&gt;
=Dependencies=&lt;br /&gt;
* Python 3, tested versions 3.2.3 and 3.4.1&lt;br /&gt;
* Unicode-compatible system&lt;br /&gt;
* Apache web sever or compatible (hosted web version only)&lt;br /&gt;
* Modern graphical web browser (web client only)&lt;br /&gt;
&lt;br /&gt;
=Obtaining and installing AUTOMOME=&lt;br /&gt;
AUTOMOME can be obtained from http://proj.penguindevelopment.org/automome/. [http://proj.penguindevelopment.org/automome/automome-latest.tar.xz Direct link to the latest version.]&lt;br /&gt;
&lt;br /&gt;
AUTOMOME is designed to run from its own directory; &amp;lt;code&amp;gt;cd&amp;lt;/code&amp;gt;ing to the directory and calling &amp;lt;code&amp;gt;automome.py&amp;lt;/code&amp;gt; directly should work. Likewise, the web version should work out of the box if the AUTOMOME directory is in a location served by Apache.&lt;br /&gt;
&lt;br /&gt;
If a setup is desired where the dictionary files (&amp;lt;code&amp;gt;*.dict.txt&amp;lt;/code&amp;gt;) are placed in a different directory from the script files, the &amp;lt;code&amp;gt;AUTOMOME_PATH&amp;lt;/code&amp;gt; environment variable may be set to the path to the dictionary files. &amp;lt;code&amp;gt;automome-web.py&amp;lt;/code&amp;gt; may be placed in a different directory from &amp;lt;code&amp;gt;automome.py&amp;lt;/code&amp;gt;, so long as &amp;lt;code&amp;gt;automome.py&amp;lt;/code&amp;gt; is in the module search path; see [https://docs.python.org/3/tutorial/modules.html#the-module-search-path].&lt;br /&gt;
&lt;br /&gt;
The client-side web interface &amp;lt;code&amp;gt;automome.html&amp;lt;/code&amp;gt; by default attempts to get its data from &amp;lt;code&amp;gt;automome-web.py&amp;lt;/code&amp;gt; at the same base URL as &amp;lt;code&amp;gt;automome.html&amp;lt;/code&amp;gt;: e.g. if you host a web version of AUTOMOME at http://example.com/molpy/grapevine/automome.html, it will try to fetch data from http://example.com/molpy/grapevine/automome-web.py. Likewise, it will try to locate &amp;lt;code&amp;gt;cuegan.svg&amp;lt;/code&amp;gt; in the same directory.&lt;br /&gt;
&lt;br /&gt;
=Using AUTOMOME=&lt;br /&gt;
There are several ways to use AUTOMOME: as of version 3.x, it can be used as a text-only command-line version (&amp;lt;code&amp;gt;automome.py&amp;lt;/code&amp;gt;), on the web using a fancy AJAX interface that gives nice formatting (&amp;lt;code&amp;gt;automome.html&amp;lt;/code&amp;gt;, and the canonical version at http://automome.penguindevelopment.org/), or as a text-only web version (&amp;lt;code&amp;gt;automome-web.py&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
==Using the command-line version==&lt;br /&gt;
The plaintext command-line version can be used on any system with Python 3.x, and is perhaps the simplest way to use AUTOMOME. At present, it supports only two significant options: picking the categories and the number of memes displayed. &amp;lt;code&amp;gt;automome.py --help&amp;lt;/code&amp;gt; should provide an adequate explanation:&amp;lt;pre&amp;gt;$ automome.py --help&lt;br /&gt;
Usage: automome.py [option] [num]&lt;br /&gt;
[option] may be one of the following:&lt;br /&gt;
    -h, --help                      show this message and exit&lt;br /&gt;
    -V, --version                   show version and exit&lt;br /&gt;
    -c [cats], --categories=[cats]  generate memes from the categories [cats]&lt;br /&gt;
                                    (see below); the default is aom&lt;br /&gt;
&lt;br /&gt;
[cats] may be any combination of the following letters:&lt;br /&gt;
    a       molpish memes from the original AUTOMEME (possibly OTTified)&lt;br /&gt;
    o       molpish other memes, i.e. non-AUTOMEME memes&lt;br /&gt;
    m       molpish meta-memes, i.e. memes originating in the OTT&lt;br /&gt;
    A       unmolpish AUTOMEME memes&lt;br /&gt;
    O       unmolpish other memes&lt;br /&gt;
    t       OTToMeme memes (xkcd memes by azule)&lt;br /&gt;
&lt;br /&gt;
[num] is the number of memes to generate; if it is missing or invalid, it is&lt;br /&gt;
assumed to be 1.&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
As an example, the following code produces 10 memes from the &amp;quot;Molpish other&amp;quot; and &amp;quot;Meta-memes&amp;quot; categories:&amp;lt;pre&amp;gt;&lt;br /&gt;
automome.py -c om 10&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Using the web GUI==&lt;br /&gt;
The [http://automome.penguindevelopment.org/ web GUI] offers more advanced features, including formatted text and the ability to filter memes. A quick run-through of features:&lt;br /&gt;
===Main layout===&lt;br /&gt;
From the main layout, clicking the image of Cuegan produces a single meme. There is not much more to be said.&lt;br /&gt;
&lt;br /&gt;
===Meme categories===&lt;br /&gt;
Clicking the &#039;&#039;Meme categories&#039;&#039; link allows you to change the categories of memes that will be displayed; the categories are the same as those used in the text-only version.&lt;br /&gt;
&lt;br /&gt;
===Filter settings===&lt;br /&gt;
The &#039;&#039;Filter settings&#039;&#039; tab is slightly more complicated. First and foremost is the &#039;&#039;Filter expression&#039;&#039; box. This allows you to enter any [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions JavaScript regular expression], and only memes matching that expression will be displayed. Keep in mind that the regular expression is case sensitive; almost all words are uppercase. The regular expression is applied to the text-only version of the meme, so italicised words will be surrounded by underscores, and so on. If you wish to match all memes, leave the box blank or enter &amp;lt;code&amp;gt;.*&amp;lt;/code&amp;gt;. &lt;br /&gt;
&lt;br /&gt;
The &#039;&#039;Lock meme&#039;&#039; checkbox allows you to lock the template of the current meme: if it is active, clicking the Cuegan image will only produce memes using the same template as the currently shown meme.&lt;br /&gt;
&lt;br /&gt;
===Copyable formats===&lt;br /&gt;
The &#039;&#039;Copyable formats&#039;&#039; tab provides three easy-to-copy output styles: plaintext (the same as &amp;lt;code&amp;gt;automome.py&amp;lt;/code&amp;gt; itself would produce), BBCode (used on most forums) and (X)HTML, which can be used on your own websites.&lt;br /&gt;
&lt;br /&gt;
==Using the text-only web version==&lt;br /&gt;
The text-only web version is a lightweight wrapper around the command-line version. In fact, if it is at all possible, you should use the command-line version directly. Nevertheless, if you still wish to use the text-only web version, simply point a browser at automome-web.py, either at your site or [http://automome.penguindevelopment.org/automome-web.py the canonical version]. Categories can be passed using the &amp;lt;code&amp;gt;c=&amp;lt;/code&amp;gt; GET-parameter, and the number of memes can be passed using the &amp;lt;code&amp;gt;n=&amp;lt;/code&amp;gt; GET-parameter. The categories are the same as the ones used in the text-only version, and the number of memes is limited to at most 100 at a time.&lt;br /&gt;
&lt;br /&gt;
As an example, http://automome.penguindevelopment.org/automome-web.py?n=10&amp;amp;c=om produces 10 memes from the &amp;quot;Molpish other&amp;quot; and &amp;quot;Meta-memes&amp;quot; categories.&lt;br /&gt;
&lt;br /&gt;
==Bugs and feedback==&lt;br /&gt;
{{bugs and feedback}}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[Category:Software]]&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=Stackermann&amp;diff=206</id>
		<title>Stackermann</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=Stackermann&amp;diff=206"/>
		<updated>2013-09-17T19:30:59Z</updated>

		<summary type="html">&lt;p&gt;Link: /* Normal usage */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{lowercase}}&lt;br /&gt;
&#039;&#039;&#039;stackermann&#039;&#039;&#039; is a program that calculates the [[w:Ackermann function|Ackermann function]]. It utilises [http://gmplib.org/ GMP] in conjunction with a simplistic stack data-type to calculate the enormous values efficiently.&lt;br /&gt;
&lt;br /&gt;
=Dependencies=&lt;br /&gt;
* UNIX-like system (probably includes Cygwin; not tested, though.)&lt;br /&gt;
* ≥ gmp-4.0&lt;br /&gt;
&lt;br /&gt;
Additionally, accurate computation timing requires librt; this is used by default and is present on just about any sane system.&lt;br /&gt;
&lt;br /&gt;
=Obtaining stackermann=&lt;br /&gt;
stackermann is available as a source tarball at http://proj.penguindevelopment.org/stackermann/. [http://proj.penguindevelopment.org/stackermann/stackermann-latest.tar.gz Direct link to the latest version.]&lt;br /&gt;
&lt;br /&gt;
Installation follows the usual configure/make/make install procedure.&lt;br /&gt;
&lt;br /&gt;
=Quick overview=&lt;br /&gt;
==Normal usage==&lt;br /&gt;
Given &#039;&#039;m&#039;&#039; and &#039;&#039;n&#039;&#039;, stackermann simply calculates the value and prints it to stdout:&lt;br /&gt;
&amp;lt;pre&amp;gt;$ stackermann 4 1&lt;br /&gt;
A(4, 1)=65533&amp;lt;/pre&amp;gt;&lt;br /&gt;
Use the &amp;lt;tt&amp;gt;-t&amp;lt;/tt&amp;gt; option to suppress the leading &#039;&#039;A(m, n)=&#039;&#039;:&amp;lt;pre&amp;gt;$ stackermann -t 4 1&lt;br /&gt;
65533&amp;lt;/pre&amp;gt;&lt;br /&gt;
If &#039;&#039;n&#039;&#039; is not given, &#039;&#039;n=m&#039;&#039; is assumed:&amp;lt;pre&amp;gt;$ stackermann 3&lt;br /&gt;
A(3, 3)=61&amp;lt;/pre&amp;gt;&lt;br /&gt;
The &amp;lt;tt&amp;gt;-s&amp;lt;/tt&amp;gt; option may be used to additionally print computation statistics to stderr:&amp;lt;pre&amp;gt;$ stackermann -s 3&lt;br /&gt;
A(3, 3)=61&lt;br /&gt;
Processing time: 0.001 s.&lt;br /&gt;
8 stack ops; max stack size 4 (~72 bytes).&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Benchmark mode==&lt;br /&gt;
In benchmark mode (the &amp;lt;tt&amp;gt;-b&amp;lt;/tt&amp;gt; option), stackermann does not print the calculated value at all; instead, it prints statistics on what it took to calculate it. The output produced is printed as a space-separated list of—respectively—total computation time in seconds, total number of stack pushes, maximum number of stack entries, and approximate maximum stack size in bytes.&lt;br /&gt;
&amp;lt;pre&amp;gt;$ stackermann -b 4 2&lt;br /&gt;
0.050 131103 65534 1048552&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Additional options==&lt;br /&gt;
For an up-to-date overview of all available options and features, see the manpage included in the source tarball.&lt;br /&gt;
&lt;br /&gt;
=Limitations, bugs and feedback=&lt;br /&gt;
==Limitations==&lt;br /&gt;
To improve efficiency, stackermann does not allow the &#039;&#039;m&#039;&#039;-parameter to be greater than 255. In reality, this is unlikely to pose a problem, since no current computer will have the memory or speed required to compute the values with high &#039;&#039;m&#039;&#039;-parameters anyway. &#039;&#039;This is not a bug; do NOT report it as one!&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
==Bugs and feedback==&lt;br /&gt;
{{bugs and feedback}}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[Category:Software]]&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=Stackermann&amp;diff=205</id>
		<title>Stackermann</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=Stackermann&amp;diff=205"/>
		<updated>2013-09-17T12:02:44Z</updated>

		<summary type="html">&lt;p&gt;Link: Bah humbug.&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{lowercase}}&lt;br /&gt;
&#039;&#039;&#039;stackermann&#039;&#039;&#039; is a program that calculates the [[w:Ackermann function|Ackermann function]]. It utilises [http://gmplib.org/ GMP] in conjunction with a simplistic stack data-type to calculate the enormous values efficiently.&lt;br /&gt;
&lt;br /&gt;
=Dependencies=&lt;br /&gt;
* UNIX-like system (probably includes Cygwin; not tested, though.)&lt;br /&gt;
* ≥ gmp-4.0&lt;br /&gt;
&lt;br /&gt;
Additionally, accurate computation timing requires librt; this is used by default and is present on just about any sane system.&lt;br /&gt;
&lt;br /&gt;
=Obtaining stackermann=&lt;br /&gt;
stackermann is available as a source tarball at http://proj.penguindevelopment.org/stackermann/. [http://proj.penguindevelopment.org/stackermann/stackermann-latest.tar.gz Direct link to the latest version.]&lt;br /&gt;
&lt;br /&gt;
Installation follows the usual configure/make/make install procedure.&lt;br /&gt;
&lt;br /&gt;
=Quick overview=&lt;br /&gt;
==Normal usage==&lt;br /&gt;
Given &#039;&#039;m&#039;&#039; and &#039;&#039;n&#039;&#039;, stackermann simply calculates the value and prints it to stdout:&lt;br /&gt;
&amp;lt;pre&amp;gt;$ stackermann 4 1&lt;br /&gt;
A(4, 1)=65533&amp;lt;/pre&amp;gt;&lt;br /&gt;
Use the &amp;lt;tt&amp;gt;-t&amp;lt;/tt&amp;gt; option to suppress the leading &#039;&#039;A(m, n)=&#039;&#039;:&amp;lt;pre&amp;gt;$ stackermann -t 4 1&lt;br /&gt;
65533&amp;lt;/pre&amp;gt;&lt;br /&gt;
If &#039;&#039;n&#039;&#039; is not given, &#039;&#039;n=m&#039;&#039; is assumed:&amp;lt;pre&amp;gt;$stackermann 3&lt;br /&gt;
A(3, 3)=61&amp;lt;/pre&amp;gt;&lt;br /&gt;
The &amp;lt;tt&amp;gt;-s&amp;lt;/tt&amp;gt; option may be used to additionally print computation statistics to stderr:&amp;lt;pre&amp;gt;$ stackermann -s 3&lt;br /&gt;
A(3, 3)=61&lt;br /&gt;
Processing time: 0.001 s.&lt;br /&gt;
8 stack ops; max stack size 4 (~72 bytes).&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Benchmark mode==&lt;br /&gt;
In benchmark mode (the &amp;lt;tt&amp;gt;-b&amp;lt;/tt&amp;gt; option), stackermann does not print the calculated value at all; instead, it prints statistics on what it took to calculate it. The output produced is printed as a space-separated list of—respectively—total computation time in seconds, total number of stack pushes, maximum number of stack entries, and approximate maximum stack size in bytes.&lt;br /&gt;
&amp;lt;pre&amp;gt;$ stackermann -b 4 2&lt;br /&gt;
0.050 131103 65534 1048552&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Additional options==&lt;br /&gt;
For an up-to-date overview of all available options and features, see the manpage included in the source tarball.&lt;br /&gt;
&lt;br /&gt;
=Limitations, bugs and feedback=&lt;br /&gt;
==Limitations==&lt;br /&gt;
To improve efficiency, stackermann does not allow the &#039;&#039;m&#039;&#039;-parameter to be greater than 255. In reality, this is unlikely to pose a problem, since no current computer will have the memory or speed required to compute the values with high &#039;&#039;m&#039;&#039;-parameters anyway. &#039;&#039;This is not a bug; do NOT report it as one!&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
==Bugs and feedback==&lt;br /&gt;
{{bugs and feedback}}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[Category:Software]]&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=Stackermann&amp;diff=204</id>
		<title>Stackermann</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=Stackermann&amp;diff=204"/>
		<updated>2013-09-17T12:02:05Z</updated>

		<summary type="html">&lt;p&gt;Link: /* Normal usage */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{lowercase}}&lt;br /&gt;
&#039;&#039;&#039;stackermann&#039;&#039;&#039; is a program that calculates the [[w:Ackermann function|Ackermann function]]. It utilises [http://gmplib.org/ GMP] in conjunction with a simplistic stack data-type to calculate the enormous values efficiently.&lt;br /&gt;
&lt;br /&gt;
=Dependencies=&lt;br /&gt;
* UNIX-like system (probably includes Cygwin; not tested, though.)&lt;br /&gt;
* ≥ gmp-4.0&lt;br /&gt;
&lt;br /&gt;
Additionally, accurate computation timing requires librt; this is used by default and is present on just about any sane system.&lt;br /&gt;
&lt;br /&gt;
=Obtaining stackermann=&lt;br /&gt;
stackermann is available as a source tarball at http://proj.penguindevelopment.org/stackermann/. [http://proj.penguindevelopment.org/stackermann/stackermann-latest.tar.gz Direct link to the latest version.]&lt;br /&gt;
&lt;br /&gt;
Installation follows the usual configure/make/make install procedure.&lt;br /&gt;
&lt;br /&gt;
=Quick overview=&lt;br /&gt;
==Normal usage==&lt;br /&gt;
Given &#039;&#039;m&#039;&#039; and &#039;&#039;n&#039;&#039;, stackermann simply calculates the value and prints it to stdout:&lt;br /&gt;
&amp;lt;pre&amp;gt;$ stackermann -t 4 1&lt;br /&gt;
A(4, 1)=65533&amp;lt;/pre&amp;gt;&lt;br /&gt;
Use the &amp;lt;tt&amp;gt;-t&amp;lt;/tt&amp;gt; option to suppress the leading &#039;&#039;A(m, n)=&#039;&#039;:&amp;lt;pre&amp;gt;$ stackermann 4 1&lt;br /&gt;
65533&amp;lt;/pre&amp;gt;&lt;br /&gt;
If &#039;&#039;n&#039;&#039; is not given, &#039;&#039;n=m&#039;&#039; is assumed:&amp;lt;pre&amp;gt;$stackermann 3&lt;br /&gt;
A(3, 3)=61&amp;lt;/pre&amp;gt;&lt;br /&gt;
The &amp;lt;tt&amp;gt;-s&amp;lt;/tt&amp;gt; option may be used to additionally print computation statistics to stderr:&amp;lt;pre&amp;gt;$ stackermann -s 3&lt;br /&gt;
A(3, 3)=61&lt;br /&gt;
Processing time: 0.001 s.&lt;br /&gt;
8 stack ops; max stack size 4 (~72 bytes).&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Benchmark mode==&lt;br /&gt;
In benchmark mode (the &amp;lt;tt&amp;gt;-b&amp;lt;/tt&amp;gt; option), stackermann does not print the calculated value at all; instead, it prints statistics on what it took to calculate it. The output produced is printed as a space-separated list of—respectively—total computation time in seconds, total number of stack pushes, maximum number of stack entries, and approximate maximum stack size in bytes.&lt;br /&gt;
&amp;lt;pre&amp;gt;$ stackermann -b 4 2&lt;br /&gt;
0.050 131103 65534 1048552&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Additional options==&lt;br /&gt;
For an up-to-date overview of all available options and features, see the manpage included in the source tarball.&lt;br /&gt;
&lt;br /&gt;
=Limitations, bugs and feedback=&lt;br /&gt;
==Limitations==&lt;br /&gt;
To improve efficiency, stackermann does not allow the &#039;&#039;m&#039;&#039;-parameter to be greater than 255. In reality, this is unlikely to pose a problem, since no current computer will have the memory or speed required to compute the values with high &#039;&#039;m&#039;&#039;-parameters anyway. &#039;&#039;This is not a bug; do NOT report it as one!&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
==Bugs and feedback==&lt;br /&gt;
{{bugs and feedback}}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[Category:Software]]&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
	<entry>
		<id>https://www.penguindevelopment.org/index.php?title=Timelapse&amp;diff=203</id>
		<title>Timelapse</title>
		<link rel="alternate" type="text/html" href="https://www.penguindevelopment.org/index.php?title=Timelapse&amp;diff=203"/>
		<updated>2013-08-18T16:24:16Z</updated>

		<summary type="html">&lt;p&gt;Link: /* Pictures */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{lowercase}}&lt;br /&gt;
&#039;&#039;&#039;timelapse&#039;&#039;&#039; is a user-configurable automatic [[w:Time-lapse photography|time-lapse]] trigger for digital cameras; it allows taking photos at regular intervals without requiring user intervention. timelapse is based on the [http://www.atmel.com/devices/ATTINY2313.aspx ATtiny2313] microcontroller and provides a user-configurable timer with a resolution of 1 millisecond. It was designed with Canon EOS DSLR cameras in mind, but may well work with other brands.&lt;br /&gt;
&lt;br /&gt;
timelapse was designed so that it may be built at home by electronics hobbyists.&lt;br /&gt;
&lt;br /&gt;
=Features=&lt;br /&gt;
* Configurable delay of up to 999999.999 seconds (~11.57 days) with 1 ms resolution&lt;br /&gt;
* Configurable pulse width (with the same parameters as the delay) allowing timed exposure&lt;br /&gt;
* Configurable shot count; 1-10000 or infinite&lt;br /&gt;
* Fully digital configuration with LCD&lt;br /&gt;
* User settings stored in ROM&lt;br /&gt;
* Powered from 9 V battery&lt;br /&gt;
&lt;br /&gt;
=Obtaining timelapse=&lt;br /&gt;
At present, the only way to obtain timelapse is to build and flash it yourself. The current (v0.1) board is a rough prototype that nonetheless does the job. A redesign of the PCB is planned; this should make it suitable to integrate into a case.&lt;br /&gt;
&lt;br /&gt;
A tarball containing the design schematics, PCB layout, firmware sources and firmware HEX is available at http://proj.penguindevelopment.org/timelapse/. [http://proj.penguindevelopment.org/timelapse/timelapse-latest.tar.gz (Direct link to latest version.)] The schematic is in [http://wiki.geda-project.org/geda:gaf gEDA/gaf] .sch format, and the PCB layout is in [http://pcb.geda-project.org/ gEDA PCB] .pcb format.&lt;br /&gt;
&lt;br /&gt;
For instructions on building the timelapse PCB, see the [[/README/]]. For instructions on flashing the firmware (and optionally compiling it yourself), see [[/INSTALL/]] (both files are also included in the tarball). [[/Parts list|A list of components is also available.]] For instruction on DIY PCB fabbing, use your favourite search engine. ;) (If you have a laser printer, you can try the toner transfer method, which was used to build the first prototype; [http://www.dr-lex.be/hardware/tonertransfer.html][http://www.riccibitti.com/pcb/pcb.htm].)&lt;br /&gt;
&lt;br /&gt;
Keep in mind that you need an AVR programmer to flash the firmware. The [http://learn.adafruit.com/usbtinyisp USBTinyISP by Adafruit industries] is cheap and does the job.&lt;br /&gt;
&lt;br /&gt;
=User guide=&lt;br /&gt;
Operating timelapse is fairly straight-forward. A quick guide can be found in the [[/README/]].&lt;br /&gt;
&lt;br /&gt;
=Bugs and feedback=&lt;br /&gt;
{{bugs and feedback}}&lt;br /&gt;
&lt;br /&gt;
=Pictures=&lt;br /&gt;
[[File:Timelapse startup.jpg|left|baseline|thumb|300px|Prototype showing the startup screen.]]&lt;br /&gt;
&amp;lt;br clear=&amp;quot;all&amp;quot; /&amp;gt;&lt;br /&gt;
For more pictures, see [http://users.penguindevelopment.org/Link/gallery2/v/electronics/ my personal gallery].&lt;br /&gt;
&lt;br /&gt;
[[Category:Hardware]]&lt;/div&gt;</summary>
		<author><name>Link</name></author>
	</entry>
</feed>