<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>weird autumn&apos;s blog</title><description>who up xml-ing they rss</description><link>https://mck.is/</link><language>en-gb</language><item><title>Decoding a 20-Year-Old Puzzle</title><link>https://mck.is/blog/2025/an-old-puzzle/</link><guid isPermaLink="true">https://mck.is/blog/2025/an-old-puzzle/</guid><description>Deobfuscating and deciphering</description><pubDate>Thu, 31 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Yesterday I was at a workshop on &lt;a href=&quot;https://indico.cern.ch/event/1560038/attachments/3088048/5517717/2025%20web%20pentesting%20-%20summer%20student%20workshop.pdf&quot;&gt;web penetration testing&lt;/a&gt; for CERN summer students; although I&apos;d covered most of the content myself previously, the exercises were a lot of fun to work through! The extra puzzle at the end especially hooked me:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;This is not a real exercise for finding security bugs, but rather a puzzle for those who finished other exercises and want to have a more difficult challenge.&lt;/p&gt;
&lt;p&gt;Open &lt;a href=&quot;./secret.html&quot;&gt;secret.html&lt;/a&gt;. As you see, it is a single HTML file with some JavaScript embedded. There is a secret message that will only be revealed when you provide a correct password. Find the password! It should be simple - how could a static HTML page possibly hide something, right?&lt;/p&gt;
&lt;p&gt;(I found this puzzle public on the Web - I don&apos;t know who&apos;s the author, so I can&apos;t credit them. The steps to solve the puzzle proposed in part Hint and Solution are mine - there could be other ways to solve it.)&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/page.BMMXcO1f_1mCOC4.webp&quot; alt=&quot;Screenshot of a page containing a text box asking for a password to be entered, with a button labeled &amp;quot;Go!&amp;quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Stop!&lt;/h2&gt;
&lt;p&gt;I heavily recommend you attempt to get the password yourself before reading any further. You&apos;ll have more fun and learn more that way! You have been warned.&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;Hint 1&amp;lt;/summary&amp;gt;
What javascript has been run? What does it produce?
&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;Hint 2&amp;lt;/summary&amp;gt;
If you assume the password only contains ASCII characters, what length must it be?
&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;h2&gt;Solving the puzzle&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!--hppage status=&quot;protected&quot;--&amp;gt;
&amp;lt;html&amp;gt;
	&amp;lt;script language=&quot;JavaScript&quot;&amp;gt;
		document.write(unescape(&quot;%3C%53%43...&quot;));
		hp_d01(unescape(&quot;&amp;gt;#//JGCF/...&quot;));
		hp_d01(unescape(&quot;&amp;gt;#//@MF[/...&quot;));
	&amp;lt;/script&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;center&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Note: The code is modified to be slightly more readable&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;Viewing the source, there&apos;s already a couple of noticeable things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;There&apos;s almost no actual HTML to the page, essentially only a html tag and a script tag - no input field asking for the password&lt;/li&gt;
&lt;li&gt;A function not seemingly defined anywhere (&lt;code&gt;hp_d01&lt;/code&gt;) is being called&lt;/li&gt;
&lt;li&gt;The script tags have a &lt;code&gt;LANGUAGE=&quot;JavaScript&quot;&lt;/code&gt; attribute, depricated in HTML 4.01 in 1999&lt;/li&gt;
&lt;li&gt;All the javascript is obfuscated&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Interestingly, none of the &quot;javascript deobfuscators&quot; that I found online seem to have any idea what to do with it. Good news for us - that means we have the fun of figuring it out for ourselves!&lt;/p&gt;
&lt;p&gt;While we could use &quot;Inspect&quot; to view the state of the DOM after the javascript has run, but it&apos;s possible it could rewrite the DOM multiple times to hide something from it, so to make sure we should decode it ourselves. Running &lt;code&gt;console.log(unescape(&quot;%3C%53%43...&quot;))&lt;/code&gt; we get:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script language=&quot;JavaScript&quot;&amp;gt;
	hp_ok = true;
	function hp_d01(s) {
		if (!hp_ok) return;
		var o = &quot;&quot;,
			ar = new Array(),
			os = &quot;&quot;,
			ic = 0;
		for (i = 0; i &amp;lt; s.length; i++) {
			c = s.charCodeAt(i);
			if (c &amp;lt; 128) c = c ^ 2;
			os += String.fromCharCode(c);
			if (os.length &amp;gt; 80) {
				ar[ic++] = os;
				os = &quot;&quot;;
			}
		}
		o = ar.join(&quot;&quot;) + os;
		document.write(o);
	}
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Our missing undefined function! It seems to be decoding ascii characters in the input string &lt;code&gt;s&lt;/code&gt; by xor-ing them with 2. The chunking it does appears to do nothing, so tidying it up, renaming some variables, and modifying it to print the output instead of writing it to the DOM:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let hp_ok = true;

function hp_d01(string) {
	if (!hp_ok) return;

	let output = &quot;&quot;;
	for (let index = 0; index &amp;lt; string.length; index++) {
		let charCode = string.charCodeAt(index);
		if (charCode &amp;lt; 128) charCode = charCode ^ 2;
		output += String.fromCharCode(charCode);
	}

	console.log(output);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Running the rest of the obfuscated javascript through &lt;code&gt;unescape&lt;/code&gt; and &lt;code&gt;hp_d01&lt;/code&gt;, we finally get the HTML being rendered by the browser, along with two functions for us to look at:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script language=&quot;JavaScript&quot;&amp;gt;
	function Kod(s, pass) {
		// ...
	}

	function f(form) {
		// ...
	}
&amp;lt;/script&amp;gt;

&amp;lt;center&amp;gt;
	&amp;lt;form name=&quot;form&quot; method=&quot;post&quot; action=&quot;&quot;&amp;gt;
		&amp;lt;b&amp;gt;Enter password:&amp;lt;/b&amp;gt;
		&amp;lt;input type=&quot;password&quot; name=&quot;pass&quot; size=&quot;30&quot; maxlength=&quot;30&quot; value=&quot;&quot; /&amp;gt;
		&amp;lt;input type=&quot;button&quot; value=&quot; Go! &quot; onClick=&quot;f(this.form)&quot; /&amp;gt;
	&amp;lt;/form&amp;gt;
&amp;lt;/center&amp;gt;

&amp;lt;table width=&quot;100%&quot; border=&quot;0&quot;&amp;gt;
	&amp;lt;tr bgcolor=&quot;#445577&quot; align=&quot;center&quot;&amp;gt;
		&amp;lt;td&amp;gt;
			&amp;lt;font face=&quot;Arial, Helvetica, sans-serif&quot; color=&quot;#FFFFFF&quot; size=&quot;-1&quot;&amp;gt;
				This webpage was protected by HTMLProtector
			&amp;lt;/font&amp;gt;
		&amp;lt;/td&amp;gt;
	&amp;lt;/tr&amp;gt;
&amp;lt;/table&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;Kod&lt;/code&gt; function seems to be what decodes the secret message; an XOR cipher. For now, it doesn&apos;t give us much to work with.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function Kod(s, pass) {
	var i = 0;
	var BlaBla = &quot;&quot;;
	for (j = 0; j &amp;lt; s.length; j++) {
		BlaBla += String.fromCharCode(pass.charCodeAt(i++) ^ s.charCodeAt(j));
		if (i &amp;gt;= pass.length) i = 0;
	}
	return BlaBla;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;f&lt;/code&gt; function looks much more interesting though, and is the one actually called by the form being submitted:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function f(form) {
	var pass = document.form.pass.value;
	var hash = 0;
	for (j = 0; j &amp;lt; pass.length; j++) {
		var n = pass.charCodeAt(j);
		hash += (n - j + 33) ^ 31025;
	}

	if (hash == 124313) {
		var Secret = &quot;&quot; + &quot;\x68\x56\x42\...&quot; + &quot;&quot;;
		var s = Kod(Secret, pass);
		document.write(s);
	} else {
		alert(&quot;Wrong password!&quot;);
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This contains both the secret message, and some sort of &quot;hash&quot;. Although we won&apos;t be able to figure out the password from this alone, it still gives us some useful information. Assuming the password only contains ASCII characters, each character will be a max value of 127, so when xor-ing with 31025:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    0111 1001 0011 0001
xor 0000 0000 0??? ????
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The most significant digits will remain unchanged. Given that for each character the result is just added to the sum so far, &lt;code&gt;124313 / 31025 = 4.006...&lt;/code&gt; - the password must only be 4 characters long. Unfortunately, assuming the password could be made out of any combination of printable ascii characters, we&apos;re still left with &lt;code&gt;95 ** 4 = 81,450,625&lt;/code&gt; possible passwords!&lt;/p&gt;
&lt;p&gt;There&apos;s two options I can think of from here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use frequency analysis to find the most likely password(s)&lt;/li&gt;
&lt;li&gt;Brute force checking passwords to see which ones create a printable ascii output from the secret message&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In this case I decided to go with brute force - It seemed like the simplest approach, and 81 million passwords really isn&apos;t that many to check with how fast computers are.&lt;/p&gt;
&lt;p&gt;First, we can get a list of all passwords that pass the &quot;hash&quot; check:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const charSet: string[] = [];
for (let index = 32; index &amp;lt; 127; index++) {
	charSet.push(String.fromCharCode(index));
}
console.log(&quot;Working with char set: &quot;, charSet.join(&quot;&quot;));

const passwords: string[] = [];
function generatePasswords(generateChar: number = 0, soFar: string = &quot;&quot;) {
	for (let index = 0; index &amp;lt; charSet.length; index++) {
		const current = soFar + charSet[index];

		if (generateChar &amp;lt; 3) {
			generatePasswords(generateChar + 1, current);
		} else if (checkHash(current)) {
			passwords.push(current);
		}
	}
}

generatePasswords();
console.log(
	&quot;Generated&quot;,
	passwords.length,
	&quot;passwords passing the &apos;hash&apos; check&quot;
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Only around 0.3% of passwords pass this check, but that still leaves us with 263,137 passwords to check! A few too many to do manually, but making some assumptions about what characters are unlikely to be in the decoded output, we can narrow it down to a single option:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function isPrintableASCII(str: string) {
	return /^[\x20-\x7F]*$/.test(str);
}

for (let password of passwords) {
	const decoded = attemptDecode(password);
	if (!isPrintableASCII(decoded)) continue;
	if (/[+@#`$]/.test(decoded)) continue;

	console.log(&quot;Found possibly valid password&quot;, password);
	console.log(&quot;Decoded:&quot;, decoded);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Checking all these passwords took less than 5 seconds, and is enough to get the secret message! With the assumptions made to get here, it&apos;s possible this approach wouldn&apos;t have worked, but I got lucky. If you want the message and password, this has given you everything to decode it!&lt;/p&gt;
</content:encoded></item><item><title>GNSS &quot;War Room&quot;</title><link>https://mck.is/gnss/</link><guid isPermaLink="true">https://mck.is/gnss/</guid><description>A space-based PNT dashboard inspired by the movie WarGames</description><pubDate>Thu, 01 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This is the dissertation for my BSc Computer Science final year project, November 2024 - April 2025.&lt;/p&gt;
&lt;h3&gt;Abstract&lt;/h3&gt;
&lt;p&gt;Global Navigation Satellite Systems (GNSS) are used to provide positioning data to billions of devices and people across the world.&lt;/p&gt;
&lt;p&gt;With current tools being unable to adequately present GNSS data engagingly on a large scale, this project both provides a tool for visualising GNSS data in real-time, and acts as a display piece optimised to take advantage of the matrix of displays in QUB&apos;s Cyber Physical Systems Lab, aiming to get people interested in the topic of GNSS. Given the constraints of this environment, the project is architected to allow different parts of the system to run across multiple computers.&lt;/p&gt;
&lt;p&gt;Heavily inspired by the displays in the movie &quot;WarGames&quot; (1983), it features satellite tracking, measures of accuracy, stability, and interference, along with positioning data.&lt;/p&gt;
&lt;h3&gt;Code&lt;/h3&gt;
&lt;p&gt;The code for this project is available online on GitHub [^2].&lt;/p&gt;
&lt;p&gt;[^2]: A. McKee, GNSS War Room (GitHub). Python. [Online]. Available: https://github.com/autumn-mck/gnss-war-room&lt;/p&gt;
&lt;h3&gt;Acknowledgements&lt;/h3&gt;
&lt;p&gt;I am incredibly grateful to my supervisor, Dr. David Laverty, both for providing guidance throughout the project, and for fuelling my passion to do the best I could do.&lt;br /&gt;
Thanks to my family, for their continued support throughout.&lt;br /&gt;
Thank you to all my friends, I wouldn&apos;t be who I am today without you.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Table of Contents&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#10-introduction-and-problem-area&quot;&gt;1.0 Introduction and Problem Area&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#20-system-requirements-and-specification&quot;&gt;2.0 System Requirements and Specification&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#21-use-cases&quot;&gt;2.1 Use cases&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#211-lab-visitors-demonstration&quot;&gt;2.1.1 Lab visitors demonstration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#212-students-learning-about-gnss&quot;&gt;2.1.2 Students learning about GNSS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#213-researcher-monitoring-signal-qualityinterference&quot;&gt;2.1.3 Researcher monitoring signal quality/interference&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#22-functional-requirements&quot;&gt;2.2 Functional requirements&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#23-non-functional-requirements&quot;&gt;2.3 Non-functional requirements&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#30-design&quot;&gt;3.0 Design&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#31-data-flow&quot;&gt;3.1 Data flow&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#32-colour-palette&quot;&gt;3.2 Colour palette&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#33-font&quot;&gt;3.3 Font&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#34-map&quot;&gt;3.4 Map&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#35-security&quot;&gt;3.5 Security&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#36-performance&quot;&gt;3.6 Performance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#37-persistence&quot;&gt;3.7 Persistence&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#38-data-input&quot;&gt;3.8 Data input&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#39-user-interface&quot;&gt;3.9 User Interface&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#40-implementation&quot;&gt;4.0 Implementation&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#41-hardware&quot;&gt;4.1 Hardware&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#411-gnss-receiver&quot;&gt;4.1.1 GNSS Receiver&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#412-computers&quot;&gt;4.1.2 Computers&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#42-languagesdevelopment-environments&quot;&gt;4.2 Languages/development environments&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#43-librariespackages&quot;&gt;4.3 Libraries/packages&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#431-desktop-client&quot;&gt;4.3.1 Desktop client&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#432-mqtt-broker&quot;&gt;4.3.2 MQTT Broker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#433-web-api&quot;&gt;4.3.3 Web API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#434-web-ui&quot;&gt;4.3.4 Web UI&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#44-important-functionsalgorithms&quot;&gt;4.4 Important functions/algorithms&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#441-elevationazimuth-to-latitudelongitude&quot;&gt;4.4.1 Elevation/azimuth to latitude/longitude&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#442-elevationazimuth-to-position-on-a-radial-graph&quot;&gt;4.4.2 Elevation/azimuth to position on a radial graph&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#443-latitudelongitude-to-gall-stereographic-coordinates&quot;&gt;4.4.3 Latitude/longitude to Gall stereographic coordinates&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#45-issues-encountered&quot;&gt;4.5 Issues encountered&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#451-custom-ui-generator&quot;&gt;4.5.1 Custom UI generator&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#452-qt-bug&quot;&gt;4.5.2 Qt bug&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#453-responsive-chart-generator&quot;&gt;4.5.3 Responsive chart generator&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#454-custom-3d-globesatellite-viewer&quot;&gt;4.5.4 Custom 3D globe/satellite viewer&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#46-code-organisation&quot;&gt;4.6 Code organisation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#50-testing&quot;&gt;5.0 Testing&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#51-unit-tests&quot;&gt;5.1 Unit tests&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#52-network-performance&quot;&gt;5.2 Network Performance&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#521-same-computer&quot;&gt;5.2.1 Same computer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#522-same-network&quot;&gt;5.2.2 Same network&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#523-different-network&quot;&gt;5.2.3 Different network&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#524-analysis&quot;&gt;5.2.4 Analysis&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#53-uptime&quot;&gt;5.3 Uptime&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#54-stress-testing&quot;&gt;5.4 Stress-testing&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#60-system-evaluation-and-experimental-results&quot;&gt;6.0 System Evaluation and Experimental Results&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#61-comparison-with-existing-software&quot;&gt;6.1 Comparison with existing software&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#62-conclusions&quot;&gt;6.2 Conclusions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#63-future-work&quot;&gt;6.3 Future work&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#70-appendices&quot;&gt;7.0 Appendices&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#71-photos-and-screenshots&quot;&gt;7.1 Photos and screenshots&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#80-references&quot;&gt;8.0 References&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;1.0 Introduction and Problem Area&lt;/h3&gt;
&lt;p&gt;Global Navigation Satellite System (GNSS) is a term used to describe a constellation of satellites providing positioning, navigation, and timing services to billions of devices across the globe. The most well-known of these systems is the Global Positioning System (GPS), however several others are also in operation, including Galileo, GLONASS, and BeiDou.&lt;/p&gt;
&lt;p&gt;GNSS satellites contain precise and accurate atomic clocks. By making use of signals broadcast from at least four satellites, GNSS receivers can determine their position on Earth to within a few metres - a technique known as trilateration or multilateration [^3]. A GNSS receiver outputs significantly more data than just the position and speed of its receiver – it also includes the derived time and date, information on all satellites currently in view, which satellites are being used for tracking, along with measures of error (dilution of precision) [^4]. This positioning data can be further enhanced by a range of further techniques, including real-time kinematic positioning (RTK), giving a precise location relative to a base station, and Differential GPS/GNSS, using a network of reference stations with precisely known positions.&lt;/p&gt;
&lt;p&gt;[^3]: P. R. Escobal, H. F. Fliegel, R. M. Jaffe, P. M. Muller, K. M. Ong, and O. H. Vonroos, &apos;A 3-D Multilateration: A Precision Geodetic Measurement System&apos;, JPL Q. Tech. Rev., vol. 2, no. 3, 1972, [Online]. Available: https://ntrs.nasa.gov/citations/19730002255&lt;/p&gt;
&lt;p&gt;[^4]: PA3FUA, &apos;GPS - NMEA sentence information&apos;. [Online]. Available: https://aprs.gids.nl/nmea/&lt;/p&gt;
&lt;p&gt;It would be useful to have a display of data from a GNSS receiver on a matrix of displays in QUB&apos;s Cyber Lab, to aid with teaching, research, or testing, as it would provide a visual representation of GNSS data and quality information. Although this matrix of displays has already been built (see &amp;lt;a href=&quot;#displays&quot;&amp;gt;The matrix of displays in the Cyber Lab&amp;lt;/a&amp;gt;), it currently received very limited use.&lt;/p&gt;
&lt;p&gt;With the goals of interesting people in the topic of GNSS, and of fitting the room available for the display (i.e. being viewed from, several metres away), the UI should attempt to emulate the look of something from a movie – in this case, WarGames (1983) has been chosen due to its eye-catching use of strong colours, picturing a &quot;war room&quot; [^5] replicable by the Cyber Lab&apos;s set of displays, and the continued use of WarGames&apos; &quot;war room&quot; in the background of other films [^6].&lt;/p&gt;
&lt;p&gt;[^5]: J. Badman, WarGames, (1983).&lt;/p&gt;
&lt;p&gt;[^6]: A. Kückes, &apos;Screen Art: WarGames&apos;. [Online]. Available: https://hp9845.net/9845/software/screenart/wargames/&lt;/p&gt;
&lt;p&gt;Although software for viewing some data from GNSS receivers already exists, all have issues that would render them unsuitable for this use-case, either requiring the receiver to be attached to the same device displaying the data, not being optimised for the matrix of displays in the Cyber Lab, or displaying a limited subset of data (See &amp;lt;a href=&quot;#61-comparison-with-existing-software&quot;&amp;gt;6.1 Comparison with existing software&amp;lt;/a&amp;gt;). Additionally, they do not combine this with the data from a precise time source, to display the precision of the GNSS time. To cover cases where the data should be displayed in other locations, a web front-end should also be provided, alongside an API, to allow other user interfaces to be created without having to re-implement all the logic required for parsing and extracting data from NMEA messages.&lt;/p&gt;
&lt;p&gt;As the Ashby building also has receivers for ADS-B (Automatic Dependent Surveillance–Broadcast, i.e. aircraft positioning) and AIS (automatic identification system, i.e. ship positioning), it would also be useful to have a display of this data alongside the GNSS satellite data to provide a more complete picture of positioning and navigation data.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;2.0 System Requirements and Specification&lt;/h3&gt;
&lt;p&gt;Throughout the project the earth has been assumed to be a perfect sphere, rather than more accurately an ellipsoid or geoid [^7], as for this project the difference is negligible – it is more limited by the resolution of a satellite&apos;s position being limited by being given in integer format [^4].&lt;/p&gt;
&lt;p&gt;[^7]: H. Fan, &apos;On an Earth ellipsoid best-fitted to the Earth surface&apos;, J. Geod., vol. 72, pp. 511–515, 1998, doi: https://doi.org/10.1007/s001900050190.&lt;/p&gt;
&lt;h4&gt;2.1 Use cases&lt;/h4&gt;
&lt;h5&gt;2.1.1 Lab visitors demonstration&lt;/h5&gt;
&lt;p&gt;Scenario: A group of people are touring the Ashby building during an open night or other event, and the &quot;War Room&quot; is already displayed, or ran by the demonstrator&lt;br /&gt;
Goal: Impress visitors at a glance, showcase the capabilities of the CPSL, and generate interest in GNSS&lt;br /&gt;
Usage:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Attract attention with the large scale and eye-catching aesthetic, creating a visually-striking installation&lt;/li&gt;
&lt;li&gt;Show the world map with real-time satellite positions, using the key to point out different satellite networks, and explaining how GNSS works&lt;/li&gt;
&lt;li&gt;Using the polar grid, show where in the sky various satellites are&lt;/li&gt;
&lt;li&gt;Demonstrate that the latitude and longitude mentioned in the statistics window match up with those measured from a mobile phone or similar&lt;/li&gt;
&lt;li&gt;Explain how the project utilises the facilities of the Ashby building, with the antenna on the roof plumbed down to a GNSS receiver, displayed on the large matrix of screens&lt;/li&gt;
&lt;li&gt;Act as a &quot;screensaver&quot;-style application to display in the background while other tasks are taking place in the lab&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;2.1.2 Students learning about GNSS&lt;/h5&gt;
&lt;p&gt;Scenario: In a lecture or similar lab session&lt;br /&gt;
Goal: Provide a visual tool to help students understand how GNSS can be utilised&lt;br /&gt;
Usage:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sorting the signal to noise ratio (SNR) chart for the satellites, sort by elevation angle to see the correlation with SNR, demonstrating how having to travel through less of the atmosphere reduces interference.&lt;/li&gt;
&lt;li&gt;Use the ability to replay prerecorded data to demonstrate specific scenarios, e.g. ongoing interference, jamming, spoofing, or the effect of a GNSS receiver cold start&lt;/li&gt;
&lt;li&gt;Referencing the trail of previous satellite positions in the 2D and 3D world maps, demonstrate how the satellite orbits provide constant global coverage. Depending on where on earth the receiver is located, this could also be used to show the different orbits used by some satellites (e.g. most QZSS satellites in geosynchronous orbit, some GPS satellites in geostationary orbit)&lt;/li&gt;
&lt;li&gt;Students can access the web UI and API to experiment with on their own devices, without having to have their own GNSS receiver, or installing and setting up the whole project&lt;/li&gt;
&lt;/ul&gt;
&lt;h5&gt;2.1.3 Researcher monitoring signal quality/interference&lt;/h5&gt;
&lt;p&gt;Scenario: A researcher testing a new GNSS antenna/receiver or investigating local interference&lt;br /&gt;
Goal: Provide a detailed view of the output of the GNSS receiver for analysis and testing&lt;br /&gt;
Usage:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Track DOP and fix quality in the statistics window to optimise placement of the antenna&lt;/li&gt;
&lt;li&gt;Use SNR chart to see which networks the antenna/receiver support, and how strong a signal it gets for each of them&lt;/li&gt;
&lt;li&gt;Monitor the raw GNSS log for debugging purposes, e.g. identifying an unsupported GNSS by its talker ID&lt;/li&gt;
&lt;li&gt;Utilise the system&apos;s stability and proven uptime for long-term tracking of stability&lt;/li&gt;
&lt;li&gt;Check if specific azimuths are being blocked/degraded by nearby buildings&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2.2 Functional requirements&lt;/h4&gt;
&lt;p&gt;A set of functional requirements (as in what the system is supposed to do) is provided below, as a set of aims for the project:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A window should display the location of satellites overlaid on a map of the earth&lt;/li&gt;
&lt;li&gt;On this map, the user should be able to zoom in and out, to focus on specific parts of the map in more detail&lt;/li&gt;
&lt;li&gt;On this map, the user should be able to move which area of the earth is at the centre/being zoomed in on&lt;/li&gt;
&lt;li&gt;On this map, the user should be able to toggle if satellite ground tracks are displayed or not, to reduce the visual noise of the display&lt;/li&gt;
&lt;li&gt;On this map, the user should be able to toggle if data from the GNSS receiver is displayed or not&lt;/li&gt;
&lt;li&gt;On this map, the user should be able to toggle if data from the ADS-B receiver is displayed or not&lt;/li&gt;
&lt;li&gt;On this map, the user should be able to toggle if data from the AIS receiver is displayed or not&lt;/li&gt;
&lt;li&gt;On this map, the user should be able to toggle between several different scaling methods - the map being scaled with both the window&apos;s width and height, or just scaling with the width, or just scaling with the height, or not scaling with either and remaining at a constant physical size regardless of the window&apos;s size.&lt;/li&gt;
&lt;li&gt;On this map, the user should be able to toggle on and off useful features for identifying locations of satellites, e.g. country borders, and large cities.&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;li&gt;A window should display the relative positions of satellites on a polar grid&lt;/li&gt;
&lt;li&gt;On this polar grid, the user should be able to toggle if previous positions of satellites are displayed or not&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;li&gt;On both the map and polar grid, a key should be displayed to allow the user to differentiate which network each satellite belongs to&lt;/li&gt;
&lt;li&gt;On both the map and polar grid, the user should be able to move the displayed location of the key, to avoid it covering useful data.&lt;/li&gt;
&lt;li&gt;On both the map and polar grid, the user should be able to toggle if the key is displayed at all or not.&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;li&gt;A window should display the raw message log of the GNSS receiver&lt;/li&gt;
&lt;li&gt;A window should display a range of statistics derived from the receiver; i.e. the measured location, time, quality of the connection to the satellites, Allan variance compared to precise time, etc.&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;li&gt;A window should display a chart of each satellite&apos;s signal to noise ratio&lt;/li&gt;
&lt;li&gt;On this chart, the user should be able to toggle if untracked satellites are displayed or not&lt;/li&gt;
&lt;li&gt;On this chart, the user should be able to toggle between if the chart is sorted by which network they are a part of, or their signal to noise ratio, or their elevation&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;li&gt;For all windows, the user should be able to toggle if the window is full-screened or not&lt;/li&gt;
&lt;li&gt;For all windows, the user should be able to resize the window, with the contents adapting to this new size&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;li&gt;The web UI should provide all the above windows, allowing for possibly reduced functionality due to being a different method of interaction&lt;/li&gt;
&lt;li&gt;The web UI should additionally a 3D version of the map&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;li&gt;The user should be able to configure the application by editing a human-readable configuration file&lt;/li&gt;
&lt;li&gt;In this configuration file, the user should be able to configure which windows appear on startup&lt;/li&gt;
&lt;li&gt;In this configuration file, the user should be able to set the initial state of each window&lt;/li&gt;
&lt;li&gt;In this configuration file, the user should be able to set which MQTT broker is used&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;li&gt;The network each satellite is part of should be indicated by a unique colour, with a key showing which colour is which network&lt;/li&gt;
&lt;li&gt;The system should allow for multiple windows of the same type, e.g. two maps focused on different points&lt;/li&gt;
&lt;li&gt;The system should allow live GNSS data to be displayed&lt;/li&gt;
&lt;li&gt;The system should allow prerecorded GNSS data to be replayed and displayed&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2.3 Non-functional requirements&lt;/h4&gt;
&lt;p&gt;Similarly, a set of non-functional requirements (as in how the system should do what it does) are provided as a set of aims:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;As the idea for the project was originally inspired by the movie WarGames, the system should have a similar aesthetic to the &quot;war room&quot; in the movie [5] (colour scheme, font, etc.)&lt;/li&gt;
&lt;li&gt;The system should be able to run for a period of one month without crashing or encountering other issues. (Due to the time constraints of this project, this is the longest possible significant amount of time)&lt;/li&gt;
&lt;li&gt;The system should process data in real-time, i.e. within one cycle of mains power - 50hz, so 20ms. (not end-to-end latency as dependent on too many uncontrollable external factors, e.g. screen refresh rate)&lt;/li&gt;
&lt;li&gt;The application should allows multiple colour schemes to be selected from, e.g. a light theme to adapt to brighter environments&lt;/li&gt;
&lt;li&gt;Application should be configurable through a configuration file in a human readable format, i.e. no need to change the code to change which windows appear on startup&lt;/li&gt;
&lt;li&gt;Chosen map projection should attempt to minimise distortion or aim for equal-area projection (rather than preserving Rhumb lines as straight lines (i.e. Mercator projection))&lt;/li&gt;
&lt;li&gt;The system should have a secure configuration by default to prevent unauthorised access, preventing anybody from publishing malicious data.&lt;/li&gt;
&lt;li&gt;System should be able to run on both Linux and Windows (MacOS unavailable for testing)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;3.0 Design&lt;/h3&gt;
&lt;h4&gt;3.1 Data flow&lt;/h4&gt;
&lt;p&gt;Within the system, there are a predefined set of data producers, limited in number due to being tied to physical hardware:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GNSS receiver&lt;/li&gt;
&lt;li&gt;AIS receiver&lt;/li&gt;
&lt;li&gt;ADS-B receiver&lt;/li&gt;
&lt;li&gt;Precise time signal&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There may also be any number of data consumers, as ideally the system should allow any number of instances of the desktop and web UI to be running at a given time (i.e. not requiring one person to stop the desktop app if another person wants to also run the desktop app on their device, without requiring duplication of hardware).&lt;/p&gt;
&lt;p&gt;This problem is perfectly suited for the application to follow the publisher/subscriber pattern, to allow the multiple sources of data to publish to a single central broker, to be subscribed to by any clients. Although this model has its disadvantages, they can either be worked around (e.g. past messages not being stored is mitigated by storing the parsed data on the subscriber), or are not an issue for this use case (e.g. bi-directional communication not being required) [^8].&lt;/p&gt;
&lt;p&gt;This architecture allows for more data sources, including possibly GNSS receivers in other locations around the world, providing more complete coverage by tracking all satellites throughout their orbit, to be added with minimal modifications. It allows the system to be more easily expanded in future to support other forms of data, i.e. AIS and ADS-B data, and possibly Broadcast Positioning System data (BPS). It also allows multiple clients to be running simultaneously, overcoming the limitation of only a single application being able to read from a serial port at a time [^9].&lt;/p&gt;
&lt;p&gt;[^8]: Microsoft, &apos;Publisher-Subscriber pattern&apos;. [Online]. Available: https://learn.microsoft.com/en-us/azure/architecture/patterns/publisher-subscriber&lt;/p&gt;
&lt;p&gt;[^9]: A. Torhamo, serialmux. Python. [Online]. Available: https://github.com/alexer/serialmux&lt;/p&gt;
&lt;p&gt;MQTT was chosen as the protocol used for communication due to being built for a publish/subscribe architecture, and its lightweight nature. This also provides the benefit of being able to use an existing reliable MQTT broker, rather than attempting to implement a custom protocol and server using TCP or WebSockets, and missing edge cases or security considerations. Whilst this approach results in any new client subscribing not having access to previously published data, in the case of GNSS this is not required, as the whole current state of the system is given by the receiver each second.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/dataflow.Bb7cIFUG_Z1SCwJE.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Note: Precision time signal and AIS have not yet been implemented by the deadline of this project (External delays in obtaining access to data), however it demonstrates how the architecture allows it to be easily extended in future.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Both the desktop and web UI utilise the same Python logic for parsing and rendering this data, with the desktop UI passing the produced SVGs to each window. This minimises the difference between the two interfaces, reducing the risk of interface-specific bugs.&lt;/p&gt;
&lt;p&gt;For the web UI, the generated data and SVGs are saved to disk, so that they can be served by the API running in a separate thread. The user&apos;s browser can then request the updated data once every second. This allows the web UI to more easily scale to a large number of users, as the more complex task parsing and generating all the data is a constant load no matter the number of users. Although this approach prevents a scaling to zero approach, where the system can stop running any instances while there are no users, this is already prevented by the requirement of needing to parse and keep track of the continually incoming data.&lt;/p&gt;
&lt;p&gt;Below is an example configuration of the system in the Cyber lab. Note that although in this case the GNSS publisher and MQTT broker are both running on the same Raspberry Pi, they may also be run on separate computers connected via the internet. The GNSS publisher and MQTT broker correspond one-to-one with the above diagram as they are atomic (i.e. cannot be divided into further concepts), unlike the data processing engine/SVG rendering/Qt windows, which while separate logically, are all part of the same application on the desktop.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/hardware.BymKR924_ZqAAg3.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;3.2 Colour palette&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/warGames.CCibbG3I_GAa7j.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The default colour palette used for the application makes use of contrasting saturated colours, intended to mimic the look of a display from the 1980s – specifically those from WarGames&apos; &quot;war room&quot;. Against the black background, all other colours pass the Web Content Accessibility Guidelines (WCAG) 2.2 [^10] on both contrast for graphical objects and user interface components (level AA), and normal text (level AA) [^11]. Due to its colours being manually picked to be close to those of WarGames [^5], it is the preset choice in the example configuration.&lt;/p&gt;
&lt;p&gt;[^10]: Web Content Accessibility Guidelines, Dec. 12, 2024. [Online]. Available: https://www.w3.org/TR/WCAG2/&lt;/p&gt;
&lt;p&gt;[^11]: Contrast Checker. WebAIM. [Online]. Available: https://webaim.org/resources/contrastchecker/&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/macchiato.DvSsZyGX_GAa7j.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;To provide an alternate look less visually demanding than the default palette, colours from the &quot;Catppuccin Macchiato&quot; theme [^12] were chosen. Despite the reduced contrast, against the background all colours still pass WCAG 2.2 on both contrast for graphical objects and user interface components (level AA), and normal text (level AA).&lt;/p&gt;
&lt;p&gt;[^12]: &apos;Palette • Catppuccin&apos;. [Online]. Available: https://catppuccin.com/palette/&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/cga.BfAQ3CRQ_GAa7j.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;A palette using a selection of colours resembling those from IBM&apos;s Colour Graphics Adapters, released in the early 1980s (Although not accurate to what would have been displayed on displays of the time, along with CGA not producing vector graphics) [^13]. Against the background, all other colours pass WCAG 2.2 on both contrast for graphical objects and user interface components (level AA), and normal text (level AA), except for the darker grey (#555). As this darker shade was only use for the background of the polar grid, and as this was not the default colour palette, this was deemed acceptable.&lt;/p&gt;
&lt;p&gt;[^13]: VileR, &apos;The IBM 5153&apos;s True CGA Palette and Color Output&apos;. [Online]. Available: https://int10h.org/blog/2022/06/ibm-5153-color-true-cga-palette/&lt;/p&gt;
&lt;h4&gt;3.3 Font&lt;/h4&gt;
&lt;p&gt;To be able to display text (statistics, map/graph keys, etc.) a font also needed to be selected.&lt;/p&gt;
&lt;p&gt;The HP 1345A, a digital vector-based graphics display, was released in 1982 [^14], with a &quot;resolution&quot; of 2048 by 2048 addressable points [^15]. This was the make of display utilised to drive the display used to capture the graphics in WarGames [^16], although the 1345A&apos;s built-in character set was not the one used for the graphics in the movie. As WarGame&apos;s character set appears to be either custom for the movie, or from some other undocumented source, and recreating the character set for this project was regrettably deemed out of scope for now, the 1345A&apos;s built-in character set was deemed to suitable. Thankfully, the character generator used by the 1345A has already been reverse-engineered by Poul-Henning Kamp [^17].&lt;/p&gt;
&lt;p&gt;[^14]: M. Mislanghe, &apos;Signal Analyzers&apos;. [Online]. Available: https://hpmemoryproject.org/collection/siganal.htm&lt;/p&gt;
&lt;p&gt;[^15]: K. Hasebe, W. R. Mason, and T. J. Zamborel, &apos;A Fast, Compact, High-Quality Digital Display for Instrumentation Applications&apos;, Hewlett-Packard Journal, vol. 33, pp. 20–28, Jan. 1982.&lt;/p&gt;
&lt;p&gt;[^16]: &apos;Upfront: HP graphics play a heavy role in box-office smash.&apos;, Measure: For the people of Hewlett-Packard, p. 2, Jan. 1984.&lt;/p&gt;
&lt;p&gt;[^17]: P.-H. Kamp, &apos;HP1345A (and WarGames)&apos;. [Online]. Available: https://phk.freebsd.dk/hacks/Wargames/&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/hp1345.BEvIPLq-_28cFbf.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Although some characters appear different – most noticeably the A, which has a flat top on the displays in WarGames [^5] – overall the characters match the very angular style very closely.&lt;/p&gt;
&lt;h4&gt;3.4 Map&lt;/h4&gt;
&lt;p&gt;A map upon which the locations of satellites can be displayed is required. To further the stylised look this project aims for, the &quot;1981&quot; map from &quot;Project Linework&quot; by Daniel Huffman [^18] was chosen for its angular, polygonal look, matching that of the world maps in WarGames [^5]. Its availability in multiple formats (most importantly, GeoJSON and SVG) made it an excellent foundation for the project&apos;s maps to be built on.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/1981.BJNzCtSC_Z6kgNt.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Although this does not match the Mercator projection used for the maps in the movie [^5], the chosen Gall stereographic projection [^19] instead minimises distortion (i.e. Greenland is not significantly oversized). An argument could instead be made for using Equal Earth projection, which preserves area between all map areas instead of minimising distortion [^20], and although it would be possible to re-project to this using the provided GeoJSON data, this was deemed out of scope due to time constraints and providing insufficient benefits.&lt;/p&gt;
&lt;p&gt;[^18]: D. Huffman, &apos;Project Linework&apos;. [Online]. Available: https://www.projectlinework.org/&lt;/p&gt;
&lt;p&gt;[^19]: J. Gall, &apos;USE OF CYLINDRICAL PROJECTIONS FOR GEOGRAPHICAL, ASTRONOMICAL, AND SCIENTIFIC PURPOSES.&apos;, Scott. Geogr. Mag., vol. 1, pp. 119–123, 1885.&lt;/p&gt;
&lt;p&gt;[^20]: B. Šavrič, T. Patterson, and J. Bernhard, &apos;The Equal Earth map projection&apos;, Int. J. Geogr. Inf. Sci., vol. 33, no. 3, pp. 454–465, 2019, doi: doi:10.1080/13658816.2018.1504949.&lt;/p&gt;
&lt;h4&gt;3.5 Security&lt;/h4&gt;
&lt;p&gt;Given the nature of the application primarily as a locally running desktop app, along with the web UI not accepting any user input, the scope of security issues is limited by the small surface-area available to attack. However time has been taken to ensure the application remains secure where applicable.&lt;/p&gt;
&lt;p&gt;Adhering to the principle of least privilege [^21], an unauthorised user is unable to publish to the MQTT broker with the given configuration, preventing any unauthorised user from publishing potentially malicious data. This is done by making use of an Access Control List, managing which authenticated users can publish to which channels. This may be further restricted to also only allow authenticated users to subscribe to channels, however this was deemed unnecessary for the current implementation of the project given both that the application will primarily run locally within a single network (see 5.2.3 Analysis), and the location of the Ashby building is not sensitive information.&lt;/p&gt;
&lt;p&gt;TLS may also be enabled to further secure the system, and although it has not been set in the default provided configuration (again, intended primarily for in-network), it has been enabled for the version hosted over the internet [^22].&lt;/p&gt;
&lt;p&gt;[^21]: &apos;What is the principle of least privilege?&apos; [Online]. Available: https://www.cloudflare.com/learning/access-management/principle-of-least-privilege/&lt;/p&gt;
&lt;p&gt;[^22]: &apos;GNSS War Room (hosted)&apos;. [Online]. Available: https://gnss.mck.is/&lt;/p&gt;
&lt;h4&gt;3.6 Performance&lt;/h4&gt;
&lt;p&gt;Web version robustness serves static file that is regenerated once per second, rather than per user, to ensure the application cannot be so easily overwhelmed by requests. This allows many users to view the web interface simultaneously, limited only by the server&apos;s ability to serve static files.&lt;/p&gt;
&lt;p&gt;The performance of the system has been stress-tested by replaying the prerecorded NMEA sentences at faster than real-time [^23], and only begins buffering its output at a speed-up of 100x, and continues to function without other issues up to the limit at which messages can be sent to the MQTT broker. (See 5.4 Stress-testing)&lt;/p&gt;
&lt;p&gt;[^23]: A. McKee, GNSS &apos;war room&apos; stress test. [Online Video]. Available: https://www.youtube.com/watch?v=7mFt8V8Rtiw&lt;/p&gt;
&lt;h4&gt;3.7 Persistence&lt;/h4&gt;
&lt;p&gt;Data was decided to be not persistent, meaning all displayed data is lost any time the application is closed or restarted. This is as in the case where a non-negligible amount of time has passed since the application last ran, the previous data is likely to be so discontinuous as to be effectively useless; i.e. if the old positions of a satellite were connected to the current one, the direct path could end up travelling through the centre of the earth.&lt;/p&gt;
&lt;h4&gt;3.8 Data input&lt;/h4&gt;
&lt;p&gt;The system assumes the GNSS receiver outputs NMEA 0183 sentences. When pre-recorded data is used, it is stored in a tab separated format, with the first value being the length of time since the previous sentence (used so data can be replayed with the correct timings), and the second value being the NMEA message, e.g.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0.648986 $GNRMC,121949.00,A,5434.78768,N,00556.20344,W,0.010,,240225,,,D,V\*00
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For both live and prerecorded data, only the NMEA sentence is published to the MQTT broker, meaning whether data is live or not is invisible to the rest of the system.
To further reduce the risk of errors, the system is built to deal with invalid data by ignoring it, e.g. if the GNSS receiver or serial link encounters an issue resulting in invalid data being read, the system will simply discard the invalid data and continue running.&lt;/p&gt;
&lt;h4&gt;3.9 User Interface&lt;/h4&gt;
&lt;p&gt;The user interface was designed for each window to be readable at a glance, assuming the primary use-case of displaying data in the CPSL.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/ui-2.D_FDfI8J_Z1x9v2U.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The world map displays where above the earth satellites are located. Satellites are coloured by which network they belong to, allowing easy differentiation between satellites&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/ui-1.CsIc1exB_1HBjcF.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The polar grid displays where satellites are in the sky, from the point of view of the antenna.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/ui-3.VzoD51R5_Z2fVYnk.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The statistics menu displays various statistics in text form that are not better visualised in some better form, or should additionally be provided in text form. Data is grouped together to allow the desired data to be found more easily at a glance.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/ui-4.Dnucxy4x_Z1HaRl9.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The signal quality graph displays signal to noise ratio for each satellite currently in view. Each satellite is labelled with its unique Pseudo-Random Noise (PRN) code.&lt;/p&gt;
&lt;p&gt;These UI designs were used as guidelines rather than strict targets, allowing change as the implementation details grew clearer. As an example, the bars on the satellite signal quality chart were changed to also be coloured by which network the satellite is part of, using the same key as the maps, as it became clear the chart was not easy to read as it was. (See &amp;lt;a href=&quot;#running&quot;&amp;gt;8.3 Photos and Screenshots for final implementation&amp;lt;/a&amp;gt;)&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;4.0 Implementation&lt;/h3&gt;
&lt;h4&gt;4.1 Hardware&lt;/h4&gt;
&lt;h5&gt;4.1.1 GNSS Receiver&lt;/h5&gt;
&lt;p&gt;As part of the project, researching and selecting which GNSS receiver to initially use was required. Real Time Kinematic (RTK) receivers were mostly ignored, as they generally cost significantly more, and the additional precision provided was of little use for the goal of this project. Ultimately, the ArduSimple simpleGNSS Pro was selected, as it provided an adequate number of features while remaining significantly cheaper than many of the other alternatives, and allowed the output to be read easily via serial through USB.&lt;/p&gt;
&lt;p&gt;It did come with several downsides:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Not supporting GLONASS (This was unclear until the receiver arrived, as some datasheets claimed it was supported, while others stated it was not)&lt;/li&gt;
&lt;li&gt;A less accurate pulse than some other modules (Within 30 ns, rather than within 10 ns or so)&lt;/li&gt;
&lt;li&gt;No support for outputting the raw data from the receiver&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;However, these were deemed to be acceptable for getting started.&lt;/p&gt;
&lt;p&gt;Towards the end of the project, a simpleRTK2B Budget was also obtained, allowing GLONASS satellites to be observed, and even occasionally receiving a signal from QZSS satellites. Other than experimental attempts to enable OSNMA, which is still in its public testing phase [^24], the receivers were left on their default settings.&lt;/p&gt;
&lt;p&gt;[^24]: European Union Agency for the Space Programme, &apos;GALILEO OPEN SERVICE NAVIGATION MESSAGE AUTHENTICATION (OSNMA)&apos;, 2021. [Online]. Available: https://www.gsc-europa.eu/sites/default/files/sites/all/files/Galileo_OSNMA_Info_Note.pdf&lt;/p&gt;
&lt;h5&gt;4.1.2 Computers&lt;/h5&gt;
&lt;p&gt;To connect to the GNSS receiver and publish to the broker, a Raspberry Pi 2 running Raspberry Pi OS (a Linux distribution optimised for Raspberry Pi&apos;s) was chosen from what was already available on-hand, due to having the required USB port and Ethernet connection required to connect to both the GNSS receiver and Queen&apos;s network. It also provides the minimal computation power required for the task while drawing a relatively minimal amount of energy.&lt;/p&gt;
&lt;p&gt;For displaying the application on the matrix of screens in the Cyber Lab, the desktop computer running Windows 10 already in place for this task was chosen. Due to requiring the ability to output to 6 displays simultaneously, with its two GPUs it was the only computer available that could manage the task.&lt;/p&gt;
&lt;h4&gt;4.2 Languages/development environments&lt;/h4&gt;
&lt;p&gt;Python was chosen as the main programming language for the project, as it works well for rapid prototyping and development, due to the flexibility provided by dynamic typing, simpler syntax making restructuring easier when the project is not yet fully understood, and large package ecosystem. TypeScript, which could later be transpiled to JavaScript, was chosen for the client-side aspects of the web view, as it provided a significantly simpler solution than attempting to compile Python to WebAssembly or similar. C++ was also used in the forked code-base for the W.O.P.R. (Above: W.O.P.R. modified to display GNSS data (currently mean SNR) ), so its development was continued using the Arduino IDE to flash to the ESP32 used.&lt;/p&gt;
&lt;p&gt;The application was created to run across multiple platforms, and has been tested to work on both Linux and Windows, including less powerful machines such as a Raspberry Pi 2.
Development was conducted primarily on Linux, specifically within an Arch Linux container created by Distrobox, containing the minimal requirements for running Python and VSCode, running on a NixOS host machine. This approach was used to work around issues encountered with developing Python applications on NixOS, where VSCode was unable to find the Python libraries used (see [^25]).&lt;/p&gt;
&lt;p&gt;[^25]: A. McKee, &apos;Bodging Python venv on NixOS&apos;. [Online]. Available: https://mck.is/blog/2024/python-nixos-bodge/&lt;/p&gt;
&lt;h4&gt;4.3 Libraries/packages&lt;/h4&gt;
&lt;p&gt;The pySerial library [^26] is used to read the raw NMEA sentences produced by the GNSS receiver, which can then be parsed by the pynmeagps library [^27]. Several Python packages for parsing NMEA messages appear to be available (pynmea2, pynmeagps, etc.) however after testing all were found to be equally suitable for this project. For the purpose of making a decision, pynmeagps was chosen.&lt;/p&gt;
&lt;p&gt;To assist in reading the configuration files, the pyjson5 [^28] is used to read JSON5 files (an extension of JSON that supports comments, trailing commas, etc.) [^29], as this allows the configuration to be documented within the file itself. The dataclass-wizard library [^30] is then used to map the dictionaries parsed from the configuration files to Python&apos;s dataclasses to provide more reliable type-hinting, aiming to improve the robustness of the application. Finally, the python-dotenv library [^31] is used to provide an alternate method for loading environment variables that should not be stored with the rest of the configuration, e.g. the password for publishing to the MQTT broker.&lt;/p&gt;
&lt;p&gt;In an attempt to reduce the number of bugs encountered at runtime and flag other potential code issues, pylint [^32] is used as a static analysis tool, along with mypy [^33] to provide further static analysis. To provide consistent code formatting throughout the Python code, in the aim of helping ensure the project&apos;s code is reliably readable to others, the formatting functionality of the ruff library [^34] was used. To run the set of tests for the projects, pytest [^35] was chosen.&lt;/p&gt;
&lt;p&gt;These libraries, as well as those detailed below, are all listed in the &quot;requirements.txt&quot; file in the repository, however the more minimal set of libraries required to run only the web interface is provided in the &quot;shell.nix&quot; file to be used with the Nix package manager, aiming to make the web interface as easy as possible to deploy reliably and reproducibly [^36].&lt;/p&gt;
&lt;p&gt;[^26]: pySerial. Python. [Online]. Available: https://github.com/pyserial/pyserial&lt;/p&gt;
&lt;p&gt;[^27]: pynmeagps. Python. SEMU Consulting. [Online]. Available: https://github.com/semuconsulting/pynmeagps&lt;/p&gt;
&lt;p&gt;[^28]: R. Kijewski, PyJSON5. Python. [Online]. Available: https://github.com/Kijewski/pyjson5&lt;/p&gt;
&lt;p&gt;[^29]: &apos;JSON5&apos;. [Online]. Available: https://json5.org/&lt;/p&gt;
&lt;p&gt;[^30]: R. Nag, Dataclass Wizard. Python. [Online]. Available: https://github.com/rnag/dataclass-wizard&lt;/p&gt;
&lt;p&gt;[^31]: python-dotenv. Python. [Online]. Available: https://github.com/theskumar/python-dotenv&lt;/p&gt;
&lt;p&gt;[^32]: pylint. Python. [Online]. Available: https://github.com/pylint-dev/pylint&lt;/p&gt;
&lt;p&gt;[^33]: mypy. Python. Python. [Online]. Available: https://github.com/python/mypy&lt;/p&gt;
&lt;p&gt;[^34]: ruff. Rust. Astral. [Online]. Available: https://github.com/astral-sh/ruff&lt;/p&gt;
&lt;p&gt;[^35]: pytest. Python. [Online]. Available: https://github.com/pytest-dev/pytest&lt;/p&gt;
&lt;p&gt;[^36]: Nix. [Online]. Available: https://nixos.org/&lt;/p&gt;
&lt;h5&gt;4.3.1 Desktop client&lt;/h5&gt;
&lt;p&gt;For the main frontend, the Qt framework was chosen, as it provides a native cross-platform framework for user interfaces, along with being featureful and flexible enough to do everything required for the project. Bindings for Python are provided through the PyQt6 library [^37], with the PyQt6-WebEngine library providing additional bindings for Qt&apos;s Chromium-based web view, required for the 3D globe view. PySide6 provides alternative bindings, however as this project is intended to be open-source, the licensing makes no difference in this case [^38].&lt;/p&gt;
&lt;p&gt;[^37]: PyQt. Riverbank Computing. [Online]. Available: https://www.riverbankcomputing.com/software/pyqt/&lt;/p&gt;
&lt;p&gt;[^38]: M. Fitzpatrick, &apos;PyQt6 vs PySide6&apos;, https://www.pythonguis.com/faq/pyqt6-vs-pyside6/.&lt;/p&gt;
&lt;h5&gt;4.3.2 MQTT Broker&lt;/h5&gt;
&lt;p&gt;To act as a test environment during development and provide a suitable default option for the MQTT broker, a docker-compose file, along with configuration files, for Mosquitto [^39] are provided in the repository. This was selected to allow the broker to be ran as easily as possible through a single command, without having to worry about the environment it is running within. The docker compose file and configuration have been tested to work without issues on both Docker and Podman.&lt;/p&gt;
&lt;p&gt;For publishing and subscribing to the MQTT broker, the paho-mqtt library was chosen, as it appeared to be the only actively supported library for Python supporting both MQTT 5.0 and 3.1.&lt;/p&gt;
&lt;p&gt;[^39]: Mosquitto. [Online]. Available: https://mosquitto.org/&lt;/p&gt;
&lt;h5&gt;4.3.3 Web API&lt;/h5&gt;
&lt;p&gt;Flask was picked to serve the web interface, as in this case only a minimal framework is needed to serve a set of mostly static files. Gunicorn was chosen as the default Web Server Gateway Interface (WSGI), as Flask recommends a dedicated WSGI be used in production [^40]. Other WSGI servers are available.&lt;/p&gt;
&lt;p&gt;Given that the chosen source of potential navigation systems interference uses the somewhat hexagonal h3 positioning system, to make use of its data, the h3 Python library was also a requirement [^41].&lt;/p&gt;
&lt;p&gt;[^40]: Flask, &apos;Deploying to Production&apos;. [Online]. Available: https://flask.palletsprojects.com/en/stable/deploying/&lt;/p&gt;
&lt;p&gt;[^41]: Uber, &apos;H3: Uber&apos;s Hexagonal Hierarchical Spatial Index&apos;. [Online]. Available: https://www.uber.com/en-GB/blog/h3/&lt;/p&gt;
&lt;h5&gt;4.3.4 Web UI&lt;/h5&gt;
&lt;p&gt;For the web interface, Three.js [^42] is used to render the 3D globe visualisation. Although it is possible to manage WebGL or WebGPU manually, especially since none of Three.js&apos; more advanced features are used for this project, as this is not the sole focus of the project it was deemed more suitable to use Three.js as an abstraction over the top of these. WebGL was chosen over WebGPU, as at time of writing, WebGPU has limited support [^43].&lt;/p&gt;
&lt;p&gt;For the country polygons given in the geoJSON used for the project to be merged into whole continents, the mapshaper [^44] package is used.&lt;/p&gt;
&lt;p&gt;To transcompile the written typescript into JavaScript, and bundle it into a single file along with the code from the libraries it relies on, rolldown was chosen. Although it is not currently recommended for production use [^45], it has been tested to work for this project, and will likely see significant future adoption once the industry-standard tool vite [^46] begins using it.&lt;/p&gt;
&lt;p&gt;Although Bun [^47] is used as the default package manager and runner for scripts within the &lt;code&gt;package.json&lt;/code&gt;, none of Bun&apos;s additional functionality was used, allowing npm, pnpm, yarn, or any similar tool to easily be used instead.&lt;/p&gt;
&lt;p&gt;[^42]: three.js. Javascript. [Online]. Available: https://github.com/mrdoob/three.js/&lt;/p&gt;
&lt;p&gt;[^43]: A. Deveria, &apos;Can I use WebGPU&apos;. [Online]. Available: https://caniuse.com/webgpu&lt;/p&gt;
&lt;p&gt;[^44]: M. Bloch, mapshaper. [Online]. Available: https://github.com/mbloch/mapshaper&lt;/p&gt;
&lt;p&gt;[^45]: rolldown. Rust. VoidZero. [Online]. Available: https://github.com/rolldown/rolldown&lt;/p&gt;
&lt;p&gt;[^46]: &apos;State of JS 2024: Libraries&apos;. [Online]. Available: https://2024.stateofjs.com/en-US/libraries/&lt;/p&gt;
&lt;p&gt;[^47]: Bun. Zig. Oven. [Online]. Available: https://bun.sh/&lt;/p&gt;
&lt;h4&gt;4.4 Important functions/algorithms&lt;/h4&gt;
&lt;h5&gt;4.4.1 Elevation/azimuth to latitude/longitude&lt;/h5&gt;
&lt;p&gt;Problem: The position of each GNSS satellite is given by an angle of elevation between 0 and 90 degrees; with 0 being the horizon, and 90 being directly overhead, along with an azimuth between 0 and 359 degrees; with 0 being true north. That is, each satellite is given as a point on the surface of an imaginary hemisphere normal to the earth at the point from which the measurement was taken.&lt;/p&gt;
&lt;p&gt;However to display the location of each satellite on a map, its position is instead needed in the form of a latitude and longitude; the conversion between the two is nontrivial.&lt;/p&gt;
&lt;p&gt;As initial searches were unable to find any existing documentation on how to perform this conversion, a solution was derived from scratch as described below; however I later discovered a document from the EASA on the same problem that takes a different approach [^48], while being mathematically equivalent, verified in a set of unit tests.&lt;/p&gt;
&lt;p&gt;[^48]: &apos;Deviation Request ETSO-C145c#5 for an ETSO approval for CS-ETSO applicable to Airborne Navigation Sensors Using the Global Positioning System Augmented by the Satellite Based Augmentation System (ETSO- C145c)&apos;. European Aviation Safety Agency. [Online]. Available: https://www.easa.europa.eu/sites/default/files/dfu/ETSO.Dev_.C145_5_v11.pdf&lt;/p&gt;
&lt;p&gt;The approach used was to first arrive at the normalised XYZ coordinates for the satellite, which is then significantly easier to convert to latitude and longitude.&lt;/p&gt;
&lt;p&gt;The problem can be simplified by first looking at a two-dimensional version, where only the elevation is provided. This can later be extended into the third dimension by rotating the 2D version by the azimuth about the measurement position.&lt;/p&gt;
&lt;p&gt;The satellite lies on the line that passes through the measurement position at the given angle of elevation. See [^49] for an interactive 2D visualisation.&lt;/p&gt;
&lt;p&gt;[^49]: A. McKee, &apos;2D visualisation&apos;. [Online]. Available: https://www.desmos.com/calculator/oskkcd5rdb&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/2d.BvfH7tUn_1kTLa.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;(Green: the earth, black: the orbit of the satellite, blue: the line the satellite is on from the view of the antenna - the satellite is located where the two overlap)&lt;/p&gt;
&lt;p&gt;In this 2D version, the line that passes through the measurement point at the given elevation angle can be given as:&lt;/p&gt;
&lt;p&gt;&amp;lt;section&amp;gt;
&amp;lt;math display=&quot;block&quot; class=&quot;tml-display&quot; style=&quot;display:block math;&quot;&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;msub&amp;gt;&amp;lt;mi&amp;gt;y&amp;lt;/mi&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;mi&amp;gt;d&amp;lt;/mi&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;/msub&amp;gt;&amp;lt;mo&amp;gt;=&amp;lt;/mo&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mi&amp;gt;tan&amp;lt;/mi&amp;gt;&amp;lt;mo&amp;gt;⁡&amp;lt;/mo&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;mo form=&quot;prefix&quot; stretchy=&quot;false&quot;&amp;gt;(&amp;lt;/mo&amp;gt;&amp;lt;mi&amp;gt;e&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;l&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;e&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;v&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;a&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;t&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;i&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;o&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;n&amp;lt;/mi&amp;gt;&amp;lt;mo form=&quot;postfix&quot; stretchy=&quot;false&quot;&amp;gt;)&amp;lt;/mo&amp;gt;&amp;lt;msub&amp;gt;&amp;lt;mi&amp;gt;x&amp;lt;/mi&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;mi&amp;gt;d&amp;lt;/mi&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;/msub&amp;gt;&amp;lt;mo&amp;gt;+&amp;lt;/mo&amp;gt;&amp;lt;mi&amp;gt;g&amp;lt;/mi&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;/math&amp;gt;
&amp;lt;/section&amp;gt;&lt;/p&gt;
&lt;p&gt;where &amp;lt;math class=&quot;tml-display&quot; style=&quot;display:block math;&quot;&amp;gt;&amp;lt;mi&amp;gt;g&amp;lt;/mi&amp;gt;&amp;lt;/math&amp;gt; is the distance from the centre of the earth the measurement was taken from (i.e. the radius of the earth plus the altitude of the receiver)&lt;/p&gt;
&lt;p&gt;The equation of the circle [^50] that represents the orbit of the satellite can be given as:&lt;/p&gt;
&lt;p&gt;[^50]: CGP Books, &apos;CCEA GCSE Maths Revision Guide: Higher&apos;, pp. 44, 60.&lt;/p&gt;
&lt;p&gt;&amp;lt;section&amp;gt;
&amp;lt;math display=&quot;block&quot; class=&quot;tml-display&quot; style=&quot;display:block math;&quot;&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;msubsup&amp;gt;&amp;lt;mi&amp;gt;x&amp;lt;/mi&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;mi&amp;gt;d&amp;lt;/mi&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;/msubsup&amp;gt;&amp;lt;mo&amp;gt;+&amp;lt;/mo&amp;gt;&amp;lt;msubsup&amp;gt;&amp;lt;mi&amp;gt;y&amp;lt;/mi&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;mi&amp;gt;d&amp;lt;/mi&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;/msubsup&amp;gt;&amp;lt;mo&amp;gt;=&amp;lt;/mo&amp;gt;&amp;lt;msup&amp;gt;&amp;lt;mi&amp;gt;o&amp;lt;/mi&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;/msup&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;/math&amp;gt;
&amp;lt;/section&amp;gt;&lt;/p&gt;
&lt;p&gt;where &amp;lt;math class=&quot;tml-display&quot; style=&quot;display:block math;&quot;&amp;gt;&amp;lt;mi&amp;gt;o&amp;lt;/mi&amp;gt;&amp;lt;/math&amp;gt; is the radius of the satellite&apos;s orbit.&lt;/p&gt;
&lt;p&gt;Substituting in the equation for the line the satellite is measured to be on, we get the quadratic:&lt;/p&gt;
&lt;p&gt;&amp;lt;section&amp;gt;
&amp;lt;math display=&quot;block&quot; class=&quot;tml-display&quot; style=&quot;display:block math;&quot;&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mo form=&quot;prefix&quot; stretchy=&quot;false&quot;&amp;gt;(&amp;lt;/mo&amp;gt;&amp;lt;mn&amp;gt;1&amp;lt;/mn&amp;gt;&amp;lt;mo&amp;gt;+&amp;lt;/mo&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mi&amp;gt;tan&amp;lt;/mi&amp;gt;&amp;lt;mo&amp;gt;⁡&amp;lt;/mo&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;mo form=&quot;prefix&quot; stretchy=&quot;false&quot;&amp;gt;(&amp;lt;/mo&amp;gt;&amp;lt;mi&amp;gt;e&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;l&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;e&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;v&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;a&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;t&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;i&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;o&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;n&amp;lt;/mi&amp;gt;&amp;lt;msup&amp;gt;&amp;lt;mo form=&quot;postfix&quot; stretchy=&quot;false&quot;&amp;gt;)&amp;lt;/mo&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;/msup&amp;gt;&amp;lt;mo form=&quot;postfix&quot; stretchy=&quot;false&quot;&amp;gt;)&amp;lt;/mo&amp;gt;&amp;lt;msubsup&amp;gt;&amp;lt;mi&amp;gt;x&amp;lt;/mi&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;mi&amp;gt;d&amp;lt;/mi&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;/msubsup&amp;gt;&amp;lt;mo&amp;gt;+&amp;lt;/mo&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;mi&amp;gt;g&amp;lt;/mi&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mspace width=&quot;0.1667em&quot;&amp;gt;&amp;lt;/mspace&amp;gt;&amp;lt;mi&amp;gt;tan&amp;lt;/mi&amp;gt;&amp;lt;mo&amp;gt;⁡&amp;lt;/mo&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;mo form=&quot;prefix&quot; stretchy=&quot;false&quot;&amp;gt;(&amp;lt;/mo&amp;gt;&amp;lt;mi&amp;gt;e&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;l&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;e&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;v&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;a&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;t&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;i&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;o&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;n&amp;lt;/mi&amp;gt;&amp;lt;mo form=&quot;postfix&quot; stretchy=&quot;false&quot;&amp;gt;)&amp;lt;/mo&amp;gt;&amp;lt;msub&amp;gt;&amp;lt;mi&amp;gt;x&amp;lt;/mi&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;mi&amp;gt;d&amp;lt;/mi&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;/msub&amp;gt;&amp;lt;mo&amp;gt;+&amp;lt;/mo&amp;gt;&amp;lt;msup&amp;gt;&amp;lt;mi&amp;gt;g&amp;lt;/mi&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;/msup&amp;gt;&amp;lt;mo&amp;gt;−&amp;lt;/mo&amp;gt;&amp;lt;msup&amp;gt;&amp;lt;mi&amp;gt;o&amp;lt;/mi&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;/msup&amp;gt;&amp;lt;mo&amp;gt;=&amp;lt;/mo&amp;gt;&amp;lt;mn&amp;gt;0&amp;lt;/mn&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;/math&amp;gt;
&amp;lt;/section&amp;gt;&lt;/p&gt;
&lt;p&gt;For which the quadratic formula [^50] may be used to give &amp;lt;math class=&quot;tml-display&quot; style=&quot;display:block math;&quot;&amp;gt;&amp;lt;msub&amp;gt;&amp;lt;mi&amp;gt;x&amp;lt;/mi&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;mi&amp;gt;d&amp;lt;/mi&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;/msub&amp;gt;&amp;lt;/math&amp;gt; directly in terms of &amp;lt;math class=&quot;tml-display&quot; style=&quot;display:block math;&quot;&amp;gt;&amp;lt;mi&amp;gt;g&amp;lt;/mi&amp;gt;&amp;lt;/math&amp;gt;, &amp;lt;math class=&quot;tml-display&quot; style=&quot;display:block math;&quot;&amp;gt;&amp;lt;mi&amp;gt;o&amp;lt;/mi&amp;gt;&amp;lt;/math&amp;gt;, and the elevation.&lt;/p&gt;
&lt;p&gt;&amp;lt;section&amp;gt;
&amp;lt;math display=&quot;block&quot; class=&quot;tml-display&quot; style=&quot;display:block math;&quot;&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;msub&amp;gt;&amp;lt;mi&amp;gt;x&amp;lt;/mi&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;mi&amp;gt;d&amp;lt;/mi&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;/msub&amp;gt;&amp;lt;mo&amp;gt;=&amp;lt;/mo&amp;gt;&amp;lt;mfrac&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mo lspace=&quot;0em&quot; rspace=&quot;0em&quot;&amp;gt;−&amp;lt;/mo&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;mi&amp;gt;g&amp;lt;/mi&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mspace width=&quot;0.1667em&quot;&amp;gt;&amp;lt;/mspace&amp;gt;&amp;lt;mi&amp;gt;tan&amp;lt;/mi&amp;gt;&amp;lt;mo&amp;gt;⁡&amp;lt;/mo&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;mo form=&quot;prefix&quot; stretchy=&quot;false&quot;&amp;gt;(&amp;lt;/mo&amp;gt;&amp;lt;mi&amp;gt;e&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;l&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;v&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;a&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;t&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;i&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;o&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;n&amp;lt;/mi&amp;gt;&amp;lt;mo form=&quot;postfix&quot; stretchy=&quot;false&quot;&amp;gt;)&amp;lt;/mo&amp;gt;&amp;lt;mo&amp;gt;±&amp;lt;/mo&amp;gt;&amp;lt;msqrt&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mo form=&quot;prefix&quot; stretchy=&quot;false&quot; lspace=&quot;0em&quot; rspace=&quot;0em&quot;&amp;gt;(&amp;lt;/mo&amp;gt;&amp;lt;mo form=&quot;prefix&quot; stretchy=&quot;false&quot;&amp;gt;−&amp;lt;/mo&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;mi&amp;gt;g&amp;lt;/mi&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mspace width=&quot;0.1667em&quot;&amp;gt;&amp;lt;/mspace&amp;gt;&amp;lt;mi&amp;gt;tan&amp;lt;/mi&amp;gt;&amp;lt;mo&amp;gt;⁡&amp;lt;/mo&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;mo form=&quot;prefix&quot; stretchy=&quot;false&quot;&amp;gt;(&amp;lt;/mo&amp;gt;&amp;lt;mi&amp;gt;e&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;l&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;v&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;a&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;t&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;i&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;o&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;n&amp;lt;/mi&amp;gt;&amp;lt;mo form=&quot;postfix&quot; stretchy=&quot;false&quot;&amp;gt;)&amp;lt;/mo&amp;gt;&amp;lt;msup&amp;gt;&amp;lt;mo form=&quot;postfix&quot; stretchy=&quot;false&quot;&amp;gt;)&amp;lt;/mo&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;/msup&amp;gt;&amp;lt;mo&amp;gt;−&amp;lt;/mo&amp;gt;&amp;lt;mn&amp;gt;4&amp;lt;/mn&amp;gt;&amp;lt;mo form=&quot;prefix&quot; stretchy=&quot;false&quot;&amp;gt;(&amp;lt;/mo&amp;gt;&amp;lt;mn&amp;gt;1&amp;lt;/mn&amp;gt;&amp;lt;mo&amp;gt;+&amp;lt;/mo&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mi&amp;gt;tan&amp;lt;/mi&amp;gt;&amp;lt;mo&amp;gt;⁡&amp;lt;/mo&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;mo form=&quot;prefix&quot; stretchy=&quot;false&quot;&amp;gt;(&amp;lt;/mo&amp;gt;&amp;lt;mi&amp;gt;e&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;l&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;e&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;v&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;a&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;t&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;i&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;o&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;n&amp;lt;/mi&amp;gt;&amp;lt;msup&amp;gt;&amp;lt;mo form=&quot;postfix&quot; stretchy=&quot;false&quot;&amp;gt;)&amp;lt;/mo&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;/msup&amp;gt;&amp;lt;mo form=&quot;postfix&quot; stretchy=&quot;false&quot;&amp;gt;)&amp;lt;/mo&amp;gt;&amp;lt;mo form=&quot;prefix&quot; stretchy=&quot;false&quot;&amp;gt;(&amp;lt;/mo&amp;gt;&amp;lt;msup&amp;gt;&amp;lt;mi&amp;gt;g&amp;lt;/mi&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;/msup&amp;gt;&amp;lt;mo&amp;gt;−&amp;lt;/mo&amp;gt;&amp;lt;msup&amp;gt;&amp;lt;mi&amp;gt;o&amp;lt;/mi&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;/msup&amp;gt;&amp;lt;mo form=&quot;postfix&quot; stretchy=&quot;false&quot; lspace=&quot;0em&quot; rspace=&quot;0em&quot;&amp;gt;)&amp;lt;/mo&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;/msqrt&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;mo form=&quot;prefix&quot; stretchy=&quot;false&quot;&amp;gt;(&amp;lt;/mo&amp;gt;&amp;lt;mn&amp;gt;1&amp;lt;/mn&amp;gt;&amp;lt;mo&amp;gt;+&amp;lt;/mo&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mi&amp;gt;tan&amp;lt;/mi&amp;gt;&amp;lt;mo&amp;gt;⁡&amp;lt;/mo&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;mo form=&quot;prefix&quot; stretchy=&quot;false&quot;&amp;gt;(&amp;lt;/mo&amp;gt;&amp;lt;mi&amp;gt;e&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;l&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;e&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;v&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;a&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;t&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;i&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;o&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;n&amp;lt;/mi&amp;gt;&amp;lt;msup&amp;gt;&amp;lt;mo form=&quot;postfix&quot; stretchy=&quot;false&quot;&amp;gt;)&amp;lt;/mo&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;/msup&amp;gt;&amp;lt;mo form=&quot;postfix&quot; stretchy=&quot;false&quot; lspace=&quot;0em&quot; rspace=&quot;0em&quot;&amp;gt;)&amp;lt;/mo&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;/mfrac&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;/math&amp;gt;
&amp;lt;/section&amp;gt;&lt;/p&gt;
&lt;p&gt;Similarly, to get &amp;lt;math class=&quot;tml-display&quot; style=&quot;display:block math;&quot;&amp;gt;&amp;lt;msub&amp;gt;&amp;lt;mi&amp;gt;y&amp;lt;/mi&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;mi&amp;gt;d&amp;lt;/mi&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;/msub&amp;gt;&amp;lt;/math&amp;gt;, the equation for the line passing through the measurement point at the given elevation angle may be rewritten as:&lt;/p&gt;
&lt;p&gt;&amp;lt;section&amp;gt;
&amp;lt;math display=&quot;block&quot; class=&quot;tml-display&quot; style=&quot;display:block math;&quot;&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mi&amp;gt;x&amp;lt;/mi&amp;gt;&amp;lt;mo&amp;gt;∗&amp;lt;/mo&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;mi&amp;gt;d&amp;lt;/mi&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;mo&amp;gt;=&amp;lt;/mo&amp;gt;&amp;lt;mo form=&quot;prefix&quot; stretchy=&quot;false&quot;&amp;gt;(&amp;lt;/mo&amp;gt;&amp;lt;mi&amp;gt;y&amp;lt;/mi&amp;gt;&amp;lt;mo&amp;gt;∗&amp;lt;/mo&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;mi&amp;gt;d&amp;lt;/mi&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;mo&amp;gt;−&amp;lt;/mo&amp;gt;&amp;lt;mi&amp;gt;g&amp;lt;/mi&amp;gt;&amp;lt;mo form=&quot;postfix&quot; stretchy=&quot;false&quot;&amp;gt;)&amp;lt;/mo&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mspace width=&quot;0.1667em&quot;&amp;gt;&amp;lt;/mspace&amp;gt;&amp;lt;mi&amp;gt;tan&amp;lt;/mi&amp;gt;&amp;lt;mo&amp;gt;⁡&amp;lt;/mo&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;mo form=&quot;prefix&quot; stretchy=&quot;false&quot;&amp;gt;(&amp;lt;/mo&amp;gt;&amp;lt;mi&amp;gt;e&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;l&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;e&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;v&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;a&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;t&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;i&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;o&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;n&amp;lt;/mi&amp;gt;&amp;lt;msup&amp;gt;&amp;lt;mo form=&quot;postfix&quot; stretchy=&quot;false&quot;&amp;gt;)&amp;lt;/mo&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mo lspace=&quot;0em&quot; rspace=&quot;0em&quot;&amp;gt;−&amp;lt;/mo&amp;gt;&amp;lt;mn&amp;gt;1&amp;lt;/mn&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;/msup&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;/math&amp;gt;
&amp;lt;/section&amp;gt;&lt;/p&gt;
&lt;p&gt;Which can again be substituted into the equation for the circle representing the boring of the satellite, giving:&lt;/p&gt;
&lt;p&gt;&amp;lt;section&amp;gt;
&amp;lt;math display=&quot;block&quot; class=&quot;tml-display&quot; style=&quot;display:block math;&quot;&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mi&amp;gt;y&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;_&amp;lt;/mi&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;mi&amp;gt;d&amp;lt;/mi&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;mo&amp;gt;=&amp;lt;/mo&amp;gt;&amp;lt;mfrac&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mo lspace=&quot;0em&quot; rspace=&quot;0em&quot;&amp;gt;−&amp;lt;/mo&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;mi&amp;gt;g&amp;lt;/mi&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mspace width=&quot;0.1667em&quot;&amp;gt;&amp;lt;/mspace&amp;gt;&amp;lt;mi&amp;gt;tan&amp;lt;/mi&amp;gt;&amp;lt;mo&amp;gt;⁡&amp;lt;/mo&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;mo form=&quot;prefix&quot; stretchy=&quot;false&quot;&amp;gt;(&amp;lt;/mo&amp;gt;&amp;lt;mi&amp;gt;e&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;l&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;e&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;v&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;a&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;t&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;i&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;o&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;n&amp;lt;/mi&amp;gt;&amp;lt;msup&amp;gt;&amp;lt;mo form=&quot;postfix&quot; stretchy=&quot;false&quot;&amp;gt;)&amp;lt;/mo&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mo lspace=&quot;0em&quot; rspace=&quot;0em&quot;&amp;gt;−&amp;lt;/mo&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;/msup&amp;gt;&amp;lt;mo&amp;gt;±&amp;lt;/mo&amp;gt;&amp;lt;msqrt&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mo form=&quot;prefix&quot; stretchy=&quot;false&quot; lspace=&quot;0em&quot; rspace=&quot;0em&quot;&amp;gt;(&amp;lt;/mo&amp;gt;&amp;lt;mo form=&quot;prefix&quot; stretchy=&quot;false&quot;&amp;gt;−&amp;lt;/mo&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;mi&amp;gt;g&amp;lt;/mi&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mspace width=&quot;0.1667em&quot;&amp;gt;&amp;lt;/mspace&amp;gt;&amp;lt;mi&amp;gt;tan&amp;lt;/mi&amp;gt;&amp;lt;mo&amp;gt;⁡&amp;lt;/mo&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;mo form=&quot;prefix&quot; stretchy=&quot;false&quot;&amp;gt;(&amp;lt;/mo&amp;gt;&amp;lt;mi&amp;gt;e&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;l&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;e&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;v&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;a&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;t&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;i&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;o&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;n&amp;lt;/mi&amp;gt;&amp;lt;msup&amp;gt;&amp;lt;mo form=&quot;postfix&quot; stretchy=&quot;false&quot;&amp;gt;)&amp;lt;/mo&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mo lspace=&quot;0em&quot; rspace=&quot;0em&quot;&amp;gt;−&amp;lt;/mo&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;/msup&amp;gt;&amp;lt;msup&amp;gt;&amp;lt;mo form=&quot;postfix&quot; stretchy=&quot;false&quot;&amp;gt;)&amp;lt;/mo&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;/msup&amp;gt;&amp;lt;mo&amp;gt;−&amp;lt;/mo&amp;gt;&amp;lt;mn&amp;gt;4&amp;lt;/mn&amp;gt;&amp;lt;mo form=&quot;prefix&quot; stretchy=&quot;false&quot;&amp;gt;(&amp;lt;/mo&amp;gt;&amp;lt;mo form=&quot;prefix&quot; stretchy=&quot;false&quot;&amp;gt;−&amp;lt;/mo&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mi&amp;gt;tan&amp;lt;/mi&amp;gt;&amp;lt;mo&amp;gt;⁡&amp;lt;/mo&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;mo form=&quot;prefix&quot; stretchy=&quot;false&quot;&amp;gt;(&amp;lt;/mo&amp;gt;&amp;lt;mi&amp;gt;e&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;l&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;e&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;v&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;a&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;t&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;i&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;o&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;n&amp;lt;/mi&amp;gt;&amp;lt;msup&amp;gt;&amp;lt;mo form=&quot;postfix&quot; stretchy=&quot;false&quot;&amp;gt;)&amp;lt;/mo&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mo lspace=&quot;0em&quot; rspace=&quot;0em&quot;&amp;gt;−&amp;lt;/mo&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;/msup&amp;gt;&amp;lt;mo&amp;gt;−&amp;lt;/mo&amp;gt;&amp;lt;mn&amp;gt;1&amp;lt;/mn&amp;gt;&amp;lt;mo form=&quot;postfix&quot; stretchy=&quot;false&quot;&amp;gt;)&amp;lt;/mo&amp;gt;&amp;lt;mo form=&quot;prefix&quot; stretchy=&quot;false&quot;&amp;gt;(&amp;lt;/mo&amp;gt;&amp;lt;msup&amp;gt;&amp;lt;mi&amp;gt;o&amp;lt;/mi&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;/msup&amp;gt;&amp;lt;mo&amp;gt;−&amp;lt;/mo&amp;gt;&amp;lt;msup&amp;gt;&amp;lt;mi&amp;gt;g&amp;lt;/mi&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;/msup&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mi&amp;gt;tan&amp;lt;/mi&amp;gt;&amp;lt;mo&amp;gt;⁡&amp;lt;/mo&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;mo form=&quot;prefix&quot; stretchy=&quot;false&quot;&amp;gt;(&amp;lt;/mo&amp;gt;&amp;lt;mi&amp;gt;e&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;l&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;e&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;v&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;a&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;t&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;i&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;o&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;n&amp;lt;/mi&amp;gt;&amp;lt;msup&amp;gt;&amp;lt;mo form=&quot;postfix&quot; stretchy=&quot;false&quot;&amp;gt;)&amp;lt;/mo&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mo lspace=&quot;0em&quot; rspace=&quot;0em&quot;&amp;gt;−&amp;lt;/mo&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;/msup&amp;gt;&amp;lt;mo form=&quot;postfix&quot; stretchy=&quot;false&quot; lspace=&quot;0em&quot; rspace=&quot;0em&quot;&amp;gt;)&amp;lt;/mo&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;/msqrt&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;mo form=&quot;prefix&quot; stretchy=&quot;false&quot;&amp;gt;(&amp;lt;/mo&amp;gt;&amp;lt;mo form=&quot;prefix&quot; stretchy=&quot;false&quot;&amp;gt;−&amp;lt;/mo&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mi&amp;gt;tan&amp;lt;/mi&amp;gt;&amp;lt;mo&amp;gt;⁡&amp;lt;/mo&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;mo form=&quot;prefix&quot; stretchy=&quot;false&quot;&amp;gt;(&amp;lt;/mo&amp;gt;&amp;lt;mi&amp;gt;e&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;l&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;e&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;v&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;a&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;t&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;i&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;o&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;n&amp;lt;/mi&amp;gt;&amp;lt;msup&amp;gt;&amp;lt;mo form=&quot;postfix&quot; stretchy=&quot;false&quot;&amp;gt;)&amp;lt;/mo&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mo lspace=&quot;0em&quot; rspace=&quot;0em&quot;&amp;gt;−&amp;lt;/mo&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;/msup&amp;gt;&amp;lt;mo&amp;gt;−&amp;lt;/mo&amp;gt;&amp;lt;mn&amp;gt;1&amp;lt;/mn&amp;gt;&amp;lt;mo form=&quot;postfix&quot; stretchy=&quot;false&quot; lspace=&quot;0em&quot; rspace=&quot;0em&quot;&amp;gt;)&amp;lt;/mo&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;/mfrac&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;/math&amp;gt;
&amp;lt;/section&amp;gt;&lt;/p&gt;
&lt;p&gt;To extend into the third dimension, the plane representing the two-dimensional solution can by rotated about the axis by the azimuth; i.e.&lt;/p&gt;
&lt;p&gt;&amp;lt;section&amp;gt;
&amp;lt;math display=&quot;block&quot; class=&quot;tml-display&quot; style=&quot;display:block math;&quot;&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mi&amp;gt;x&amp;lt;/mi&amp;gt;&amp;lt;mo&amp;gt;=&amp;lt;/mo&amp;gt;&amp;lt;msub&amp;gt;&amp;lt;mi&amp;gt;x&amp;lt;/mi&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;mi&amp;gt;d&amp;lt;/mi&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;/msub&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mi&amp;gt;cos&amp;lt;/mi&amp;gt;&amp;lt;mo&amp;gt;⁡&amp;lt;/mo&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;mo form=&quot;prefix&quot; stretchy=&quot;false&quot;&amp;gt;(&amp;lt;/mo&amp;gt;&amp;lt;mi&amp;gt;a&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;z&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;i&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;m&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;u&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;t&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;h&amp;lt;/mi&amp;gt;&amp;lt;mo form=&quot;postfix&quot; stretchy=&quot;false&quot;&amp;gt;)&amp;lt;/mo&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;/math&amp;gt;
&amp;lt;/section&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;section&amp;gt;
&amp;lt;math display=&quot;block&quot; class=&quot;tml-display&quot; style=&quot;display:block math;&quot;&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mi&amp;gt;y&amp;lt;/mi&amp;gt;&amp;lt;mo&amp;gt;=&amp;lt;/mo&amp;gt;&amp;lt;msub&amp;gt;&amp;lt;mi&amp;gt;y&amp;lt;/mi&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;mi&amp;gt;d&amp;lt;/mi&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;/msub&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;/math&amp;gt;
&amp;lt;/section&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;section&amp;gt;
&amp;lt;math display=&quot;block&quot; class=&quot;tml-display&quot; style=&quot;display:block math;&quot;&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mi&amp;gt;z&amp;lt;/mi&amp;gt;&amp;lt;mo&amp;gt;=&amp;lt;/mo&amp;gt;&amp;lt;msub&amp;gt;&amp;lt;mi&amp;gt;x&amp;lt;/mi&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;mi&amp;gt;d&amp;lt;/mi&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;/msub&amp;gt;&amp;lt;mi&amp;gt;s&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;i&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;n&amp;lt;/mi&amp;gt;&amp;lt;mo form=&quot;prefix&quot; stretchy=&quot;false&quot;&amp;gt;(&amp;lt;/mo&amp;gt;&amp;lt;mi&amp;gt;a&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;z&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;i&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;m&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;u&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;t&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;h&amp;lt;/mi&amp;gt;&amp;lt;mo form=&quot;postfix&quot; stretchy=&quot;false&quot;&amp;gt;)&amp;lt;/mo&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;/math&amp;gt;
&amp;lt;/section&amp;gt;&lt;/p&gt;
&lt;p&gt;Consequently, &amp;lt;math class=&quot;tml-display&quot; style=&quot;display:block math;&quot;&amp;gt;&amp;lt;mi&amp;gt;x&amp;lt;/mi&amp;gt;&amp;lt;/math&amp;gt;, &amp;lt;math class=&quot;tml-display&quot; style=&quot;display:block math;&quot;&amp;gt;&amp;lt;mi&amp;gt;x&amp;lt;/mi&amp;gt;&amp;lt;/math&amp;gt;, and &amp;lt;math class=&quot;tml-display&quot; style=&quot;display:block math;&quot;&amp;gt;&amp;lt;mi&amp;gt;z&amp;lt;/mi&amp;gt;&amp;lt;/math&amp;gt; can be calculated directly in terms of &amp;lt;math class=&quot;tml-display&quot; style=&quot;display:block math;&quot;&amp;gt;&amp;lt;mi&amp;gt;g&amp;lt;/mi&amp;gt;&amp;lt;/math&amp;gt;, &amp;lt;math class=&quot;tml-display&quot; style=&quot;display:block math;&quot;&amp;gt;&amp;lt;mi&amp;gt;o&amp;lt;/mi&amp;gt;&amp;lt;/math&amp;gt;, the elevation, and the azimuth.&lt;/p&gt;
&lt;p&gt;For an interactive visualisation of this 3D solution, see [^51].&lt;/p&gt;
&lt;p&gt;[^51]: A. McKee, &apos;3D visualisation&apos;. [Online]. Available: https://www.desmos.com/3d/jxqcoesfg3&lt;/p&gt;
&lt;p&gt;These coordinates can then easily be converted to a latitude and longitude, after being normalised to unit length:&lt;/p&gt;
&lt;p&gt;&amp;lt;section&amp;gt;
&amp;lt;math display=&quot;block&quot; class=&quot;tml-display&quot; style=&quot;display:block math;&quot;&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mi&amp;gt;l&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;a&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;t&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;i&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;t&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;u&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;d&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;e&amp;lt;/mi&amp;gt;&amp;lt;mo&amp;gt;=&amp;lt;/mo&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mi&amp;gt;arcsin&amp;lt;/mi&amp;gt;&amp;lt;mo&amp;gt;⁡&amp;lt;/mo&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;mo form=&quot;prefix&quot; stretchy=&quot;false&quot;&amp;gt;(&amp;lt;/mo&amp;gt;&amp;lt;mfrac&amp;gt;&amp;lt;mi&amp;gt;x&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;o&amp;lt;/mi&amp;gt;&amp;lt;/mfrac&amp;gt;&amp;lt;mo form=&quot;postfix&quot; stretchy=&quot;false&quot;&amp;gt;)&amp;lt;/mo&amp;gt;&amp;lt;mo separator=&quot;true&quot;&amp;gt;,&amp;lt;/mo&amp;gt;&amp;lt;mtext&amp;gt; &amp;lt;/mtext&amp;gt;&amp;lt;mi&amp;gt;l&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;o&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;n&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;g&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;i&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;t&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;u&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;d&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;e&amp;lt;/mi&amp;gt;&amp;lt;mo&amp;gt;=&amp;lt;/mo&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mi&amp;gt;arctan&amp;lt;/mi&amp;gt;&amp;lt;mo&amp;gt;⁡&amp;lt;/mo&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;mo form=&quot;prefix&quot; stretchy=&quot;false&quot;&amp;gt;(&amp;lt;/mo&amp;gt;&amp;lt;mfrac&amp;gt;&amp;lt;mi&amp;gt;z&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;o&amp;lt;/mi&amp;gt;&amp;lt;/mfrac&amp;gt;&amp;lt;mo separator=&quot;true&quot;&amp;gt;,&amp;lt;/mo&amp;gt;&amp;lt;mfrac&amp;gt;&amp;lt;mi&amp;gt;y&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;o&amp;lt;/mi&amp;gt;&amp;lt;/mfrac&amp;gt;&amp;lt;mo form=&quot;postfix&quot; stretchy=&quot;false&quot;&amp;gt;)&amp;lt;/mo&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;/math&amp;gt;
&amp;lt;/section&amp;gt;&lt;/p&gt;
&lt;p&gt;This solution has been verified to produce the same results as the method from the EASA document [^48].&lt;/p&gt;
&lt;h5&gt;4.4.2 Elevation/azimuth to position on a radial graph&lt;/h5&gt;
&lt;p&gt;To display the positions off the satellites on a radial/radar graph, the angle of elevation and azimuth must be converted to Cartesian coordinates acting as a position on a radial graph. For this project, an orthographic projection was chosen due to its simplicity, however other mappings may be used to reduce distortion for low angles of elevation.&lt;/p&gt;
&lt;p&gt;The distance of the point representing the satellite from the centre of the radial graph is calculated as:&lt;/p&gt;
&lt;p&gt;&amp;lt;section&amp;gt;
&amp;lt;math display=&quot;block&quot; class=&quot;tml-display&quot; style=&quot;display:block math;&quot;&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mi&amp;gt;d&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;i&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;s&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;t&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;a&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;n&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;c&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;e&amp;lt;/mi&amp;gt;&amp;lt;mo&amp;gt;=&amp;lt;/mo&amp;gt;&amp;lt;mi&amp;gt;r&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;a&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;d&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;i&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;u&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;s&amp;lt;/mi&amp;gt;&amp;lt;mtext&amp;gt; &amp;lt;/mtext&amp;gt;&amp;lt;mo&amp;gt;×&amp;lt;/mo&amp;gt;&amp;lt;mtext&amp;gt; &amp;lt;/mtext&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mi&amp;gt;cos&amp;lt;/mi&amp;gt;&amp;lt;mo&amp;gt;⁡&amp;lt;/mo&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;mo form=&quot;prefix&quot; stretchy=&quot;false&quot;&amp;gt;(&amp;lt;/mo&amp;gt;&amp;lt;mi&amp;gt;e&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;l&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;e&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;v&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;a&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;t&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;i&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;o&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;n&amp;lt;/mi&amp;gt;&amp;lt;mo form=&quot;postfix&quot; stretchy=&quot;false&quot;&amp;gt;)&amp;lt;/mo&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;/math&amp;gt;
&amp;lt;/section&amp;gt;&lt;/p&gt;
&lt;p&gt;where &amp;lt;math class=&quot;tml-display&quot; style=&quot;display:block math;&quot;&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mi&amp;gt;r&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;a&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;d&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;i&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;u&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;s&amp;lt;/mi&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;/math&amp;gt; is the the radius of the radial graph. Assuming the point &amp;lt;math class=&quot;tml-display&quot; style=&quot;display:block math;&quot;&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mn&amp;gt;0&amp;lt;/mn&amp;gt;&amp;lt;mo separator=&quot;true&quot;&amp;gt;,&amp;lt;/mo&amp;gt;&amp;lt;mn&amp;gt;0&amp;lt;/mn&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;/math&amp;gt; is at the centre of the graph, this allows the coordinates to be calculated as:&lt;/p&gt;
&lt;p&gt;&amp;lt;section&amp;gt;
&amp;lt;math display=&quot;block&quot; class=&quot;tml-display&quot; style=&quot;display:block math;&quot;&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mi&amp;gt;x&amp;lt;/mi&amp;gt;&amp;lt;mo&amp;gt;=&amp;lt;/mo&amp;gt;&amp;lt;mi&amp;gt;d&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;i&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;s&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;t&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;a&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;n&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;c&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;e&amp;lt;/mi&amp;gt;&amp;lt;mtext&amp;gt; &amp;lt;/mtext&amp;gt;&amp;lt;mo&amp;gt;×&amp;lt;/mo&amp;gt;&amp;lt;mtext&amp;gt; &amp;lt;/mtext&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mi&amp;gt;sin&amp;lt;/mi&amp;gt;&amp;lt;mo&amp;gt;⁡&amp;lt;/mo&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;mo form=&quot;prefix&quot; stretchy=&quot;false&quot;&amp;gt;(&amp;lt;/mo&amp;gt;&amp;lt;mi&amp;gt;a&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;z&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;i&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;m&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;u&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;t&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;h&amp;lt;/mi&amp;gt;&amp;lt;mo form=&quot;postfix&quot; stretchy=&quot;false&quot;&amp;gt;)&amp;lt;/mo&amp;gt;&amp;lt;mo separator=&quot;true&quot;&amp;gt;,&amp;lt;/mo&amp;gt;&amp;lt;mtext&amp;gt; &amp;lt;/mtext&amp;gt;&amp;lt;mi&amp;gt;y&amp;lt;/mi&amp;gt;&amp;lt;mo&amp;gt;=&amp;lt;/mo&amp;gt;&amp;lt;mi&amp;gt;d&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;i&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;s&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;t&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;a&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;n&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;c&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;e&amp;lt;/mi&amp;gt;&amp;lt;mtext&amp;gt; &amp;lt;/mtext&amp;gt;&amp;lt;mo&amp;gt;×&amp;lt;/mo&amp;gt;&amp;lt;mtext&amp;gt; &amp;lt;/mtext&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mi&amp;gt;cos&amp;lt;/mi&amp;gt;&amp;lt;mo&amp;gt;⁡&amp;lt;/mo&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;mo form=&quot;prefix&quot; stretchy=&quot;false&quot;&amp;gt;(&amp;lt;/mo&amp;gt;&amp;lt;mi&amp;gt;a&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;z&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;i&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;m&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;u&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;t&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;h&amp;lt;/mi&amp;gt;&amp;lt;mo form=&quot;postfix&quot; stretchy=&quot;false&quot;&amp;gt;)&amp;lt;/mo&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;/math&amp;gt;
&amp;lt;/section&amp;gt;&lt;/p&gt;
&lt;h5&gt;4.4.3 Latitude/longitude to Gall stereographic coordinates&lt;/h5&gt;
&lt;p&gt;As a Gall stereographic projection is used for the chosen map, to overlay features on it (satellite locations, cities, etc.), the latitude and longitude of the feature must be projected into the coordinate space of the map [^52].&lt;/p&gt;
&lt;p&gt;[^52]: M. LAPAINE, &apos;Gall Stereographic Projection and its Generalization&apos;, Geod. List, vol. 77, no. 1, pp. 3–5, 2023.&lt;/p&gt;
&lt;p&gt;&amp;lt;section&amp;gt;
&amp;lt;math display=&quot;block&quot; class=&quot;tml-display&quot; style=&quot;display:block math;&quot;&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mi&amp;gt;x&amp;lt;/mi&amp;gt;&amp;lt;mo&amp;gt;=&amp;lt;/mo&amp;gt;&amp;lt;mfrac&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mi&amp;gt;r&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;a&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;d&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;i&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;u&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;s&amp;lt;/mi&amp;gt;&amp;lt;mo&amp;gt;×&amp;lt;/mo&amp;gt;&amp;lt;mi&amp;gt;l&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;o&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;n&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;g&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;i&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;t&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;u&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;d&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;e&amp;lt;/mi&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;msqrt&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;/msqrt&amp;gt;&amp;lt;/mfrac&amp;gt;&amp;lt;mo separator=&quot;true&quot;&amp;gt;,&amp;lt;/mo&amp;gt;&amp;lt;mi&amp;gt;y&amp;lt;/mi&amp;gt;&amp;lt;mo&amp;gt;=&amp;lt;/mo&amp;gt;&amp;lt;mi&amp;gt;r&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;a&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;d&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;i&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;u&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;s&amp;lt;/mi&amp;gt;&amp;lt;mo form=&quot;prefix&quot; stretchy=&quot;false&quot;&amp;gt;(&amp;lt;/mo&amp;gt;&amp;lt;mn&amp;gt;1&amp;lt;/mn&amp;gt;&amp;lt;mo&amp;gt;+&amp;lt;/mo&amp;gt;&amp;lt;mfrac&amp;gt;&amp;lt;msqrt&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;/msqrt&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;/mfrac&amp;gt;&amp;lt;mo form=&quot;postfix&quot; stretchy=&quot;false&quot;&amp;gt;)&amp;lt;/mo&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mspace width=&quot;0.1667em&quot;&amp;gt;&amp;lt;/mspace&amp;gt;&amp;lt;mi&amp;gt;tan&amp;lt;/mi&amp;gt;&amp;lt;mo&amp;gt;⁡&amp;lt;/mo&amp;gt;&amp;lt;mspace width=&quot;0.1667em&quot;&amp;gt;&amp;lt;/mspace&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;mfrac&amp;gt;&amp;lt;mrow&amp;gt;&amp;lt;mi&amp;gt;l&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;a&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;t&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;i&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;t&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;u&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;d&amp;lt;/mi&amp;gt;&amp;lt;mi&amp;gt;e&amp;lt;/mi&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;mn&amp;gt;2&amp;lt;/mn&amp;gt;&amp;lt;/mfrac&amp;gt;&amp;lt;/mrow&amp;gt;&amp;lt;/math&amp;gt;
&amp;lt;/section&amp;gt;&lt;/p&gt;
&lt;h4&gt;4.5 Issues encountered&lt;/h4&gt;
&lt;h5&gt;4.5.1 Custom UI generator&lt;/h5&gt;
&lt;p&gt;To allow for the customisation required for the WarGames styling, none of Qt&apos;s built-in elements could be used, therefore requiring the implementation of a completely custom UI generator for every window, based on Qt&apos;s SVG engine. This is what allowed the web UI to be added later, reusing the same SVG generation engine.&lt;/p&gt;
&lt;h5&gt;4.5.2 Qt bug&lt;/h5&gt;
&lt;p&gt;During the process of development a bug was discovered in Qt&apos;s SVG support, which I reported as QTBUG-132468 [^53]. This issue occurred whenever a SVG polyline was created with all its points being the same, and a stroke with a round linecap were applied. The expected behaviour in this case was that the line would display as a dot, the behaviour exhibited in Firefox, Chromium, and Inkscape. Instead, nothing displayed at all. This issue was triggered by any dots in the HP1345A font, e.g. with full stops, exclamation marks, colons, the dots above lowercase &quot;i&quot;s, etc.&lt;/p&gt;
&lt;p&gt;The workaround for this was completed in commit 6627a73, by modifying the character construction function to keep track of if all points were the same, and if so, add in an SVG circle element to act as the required dot. This bug has since been fixed in Qt 6.8.3 and Qt 6.9 [^53], however as as of 26/04/2025 these versions are not yet widely packaged (only on a few rolling releases such as Arch, not yet even Debian Sid) [^54], so a workaround remains in place.&lt;/p&gt;
&lt;p&gt;[^53]: A. McKee, &apos;Polyline with stroke displays nothing when all points are the same&apos;. [Online]. Available: https://bugreports.qt.io/browse/QTBUG-132468&lt;/p&gt;
&lt;p&gt;[^54]: &apos;Qt packaging&apos;. Accessed: Apr. 12, 2025. [Online]. Available: https://repology.org/project/qt/badges&lt;/p&gt;
&lt;h5&gt;4.5.3 Responsive chart generator&lt;/h5&gt;
&lt;p&gt;To display the signal to noise (SNR) chart, a custom SVG chart generator had to be written, as the existing available solutions either did not function in real time (i.e. intended for creating a non-updating chart), or required browser JavaScript (not suitable for the desktop UI). Additionally, the chart has to be responsive, scaling the bars to fit within the window, whilst not squishing or stretching the labels, along with hiding some labels if space is insufficient.&lt;/p&gt;
&lt;h5&gt;4.5.4 Custom 3D globe/satellite viewer&lt;/h5&gt;
&lt;p&gt;The initial plan had been to use the pre-built three-globe or globe.gl libraries, as they make it extremely easy to display data around a 3D globe. However when it came to displaying the previous positions of satellites, neither had any built-in way of doing so. The solution to this was to implement a custom solution relying on only three.js, allowing the logic required for rendering previous satellite positions to be implemented. This also allowed the globe to utilise the same Project Linework &quot;1981&quot; [^18] map used for the 2D map, rather than a rendered image as the other libraries required, increasing the visual cohesion of the project.&lt;/p&gt;
&lt;h4&gt;4.6 Code organisation&lt;/h4&gt;
&lt;p&gt;Each of the main &quot;views&quot; (map, polar grid, raw messages, signal graph, and statistics) has its own sub-folder within the &quot;views&quot; folder.
In each sub-folder:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&quot;window.py&quot; contains the Qt-exclusive code, limited to resizing the window and its contents, and handling user input&lt;/li&gt;
&lt;li&gt;&quot;generate.py&quot; contains the functions for generating the initial version of the view&lt;/li&gt;
&lt;li&gt;&quot;update.py&quot;, if it exists, contains the functions for updating the initial version of the view with the latest data. In cases where separate updating code does not exist, the &quot;initial&quot; version of the view is generated from scratch for every refresh (signal graph, statistics).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Although this separation of generation and updating introduces some complexity, it is required in cases where the generation incurs a significant performance impact (primarily for the map, where e.g. 28 thousand city locations cannot be parsed and inserted onto a map every second in Python)&lt;br /&gt;
Separating the Qt-exclusive code allows the same &quot;generate&quot; and &quot;update&quot; functions to be used by both the desktop and web UI.&lt;/p&gt;
&lt;p&gt;The functions for generating the HP1345A font are stored within the &quot;font&quot; folder, along with the downloaded ROM files.&lt;/p&gt;
&lt;p&gt;The functions responsible for managing the state of currently-known GNSS data, along with parsing this data from NMEA messages, and converting it into other useful forms (i.e. elevation/azimuth to latitude/longitude) are stored within the &quot;gnss&quot; folder.&lt;/p&gt;
&lt;p&gt;The &quot;palettes&quot; folder contains the function for loading the selected palette, along with the palettes themselves.&lt;/p&gt;
&lt;p&gt;The functions for interacting with the receiver (publishing live data, recording a log, replaying a recorded log) are stored within the &quot;receiver&quot; folder.&lt;/p&gt;
&lt;p&gt;The &quot;web&quot; folder contains all the code required for the web view, although to be usable it must first be transpiled using the script specified in the package.json file.&lt;/p&gt;
&lt;p&gt;Finally, the &quot;misc&quot; folder contains functions that do not fit neatly into some other group, which includes: loading the configuration file, fetching data from GPSJam, and publishing/subscribing to the MQTT broker.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;5.0 Testing&lt;/h3&gt;
&lt;h4&gt;5.1 Unit tests&lt;/h4&gt;
&lt;p&gt;A set of unit tests were written with pytest, to test the most critical functions of the application, or where there is only a single possible correct solution (i.e. 4.4 Important functions/algorithms).
Unit tests have been avoided in cases where they are actively not useful, e.g. the exact character output of the SVG generator.
Thanks to a pull request by Christian Clauss [^55], these tests are run every time a change is committed to main or a pull request is made, ensuring on an ongoing basis that the main functionality of the project continues to work. Ruff and mypy are also run automatically to reduce the risk of any errors in code that is not ran as part of the unit tests.&lt;/p&gt;
&lt;p&gt;[^55]: C. Clauss, &apos;GitHub Actions: Test with pytest&apos;. [Online]. Available: https://github.com/autumn-mck/gnss-war-room/pull/3&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/pytest.C_m3LlGs_1aMXwu.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;5.2 Network Performance&lt;/h4&gt;
&lt;p&gt;To validate the requirement of the data being processed in &quot;real time&quot;, a benchmark standard of one cycle of mains power (50hz, i.e. 20ms) will be used.
The round-trip time (RTT) of sampled NMEA messages were measured across a range of network conditions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MQTT broker hosted on the same computer, i.e. localhost.&lt;/li&gt;
&lt;li&gt;MQTT broker hosted on a different computer on the same local network, both via WiFi.&lt;/li&gt;
&lt;li&gt;MQTT broker hosted on a different computer on a different network over the internet, 1400km away (VPS hosted in Falkenstein, Germany)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For each, 10,000 sampled messages were sent sequentially. The code used for this testing may be found in the &quot;scripts/mqtTest.py&quot; file of the repository.&lt;/p&gt;
&lt;h5&gt;5.2.1 Same computer&lt;/h5&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/sameComputer.CWz-KexV_22YW05.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Median: 0.57ms&lt;br /&gt;
90th percentile: 0.73ms&lt;br /&gt;
99th percentile: 0.93ms&lt;br /&gt;
99.9th percentile: 1.36ms&lt;/p&gt;
&lt;h5&gt;5.2.2 Same network&lt;/h5&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/sameNetwork.DoVsgQb4_Z708ya.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Median: 25.14ms&lt;br /&gt;
90th percentile: 48.21ms&lt;br /&gt;
99th percentile: 104.26ms&lt;br /&gt;
99.9th percentile: 182.21ms&lt;/p&gt;
&lt;h5&gt;5.2.3 Different network&lt;/h5&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/differentNetwork.ZpZi5ESR_2jlalR.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Median: 137.71ms&lt;br /&gt;
90th percentile: 156.00ms&lt;br /&gt;
99th percentile: 201.68ms&lt;br /&gt;
99.9th percentile: 235.06ms&lt;/p&gt;
&lt;h5&gt;5.2.4 Analysis&lt;/h5&gt;
&lt;p&gt;All scenarios appear to follow a log-normal distribution, as expected from other research [^56]. With the standard of being within 20ms, and judging against even the 99.9th percentile, both running on the same device, and within the same network, are deemed to be &quot;real time&quot;.&lt;/p&gt;
&lt;p&gt;Although the messages sent over the internet incur significant latency, similarly to that in a series of tubes, the application continues to function without issues. This setup may still be considered acceptable for visual display, however no longer meets the requirements for &quot;real time&quot;.&lt;/p&gt;
&lt;p&gt;[^56]: I. Antoniou, V. V. Ivanov, and P. V. Zrelov, &apos;On the log-normal distribution of network traffic&apos;, Phys. Nonlinear Phenom., vol. 167, no. 1–2, pp. 72–85, 2002, doi: https://doi.org/10.1016/S0167-2789(02)00431-1.&lt;/p&gt;
&lt;h4&gt;5.3 Uptime&lt;/h4&gt;
&lt;p&gt;To ensure the application continues to function under normal conditions for significant periods of time, the system has been left running whilst continually being fed live data for as long as possible. This was done using the already established least-favourable network connection of over the internet. The system was hosted online [^22] and monitored regularly to ensure continued functionality.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/uptime.D8iGmICi_Z2oI5AU.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;5.4 Stress-testing&lt;/h4&gt;
&lt;p&gt;To ensure the application continues to function under considerably harsher than normal conditions, the system was set up to replay prerecorded NMEA messages at 100 times faster than real-time for an hour. As seen in the recording [^23], it does so with no issues.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;6.0 System Evaluation and Experimental Results&lt;/h3&gt;
&lt;h4&gt;6.1 Comparison with existing software&lt;/h4&gt;
&lt;p&gt;&amp;lt;section&amp;gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;u-center/u-center 2 [^57]&lt;/th&gt;
&lt;th&gt;GNSS Radar [^58]&lt;/th&gt;
&lt;th&gt;GPSTest [^59]&lt;/th&gt;
&lt;th&gt;RTKLIB [^60]&lt;/th&gt;
&lt;th&gt;GNSS War Room [^2]&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Open-source&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lat/long/alt&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dilution of Precision&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Error vs known fixed position&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Future satellite positions&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Estimated interference&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Raw NMEA Messages&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Satellite positions (Polar grid)&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Satellite positions (World map)&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Satellite positions (3D globe)&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AIS/ADS-B integration&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;~ (prerecorded ADS-B)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SNR Chart&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Works without dedicated GNSS receiver&lt;/td&gt;
&lt;td&gt;~ (prerecorded data)&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;~ (prerecorded data)&lt;/td&gt;
&lt;td&gt;~ (prerecorded data or web view)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Allows replaying prerecorded data&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Allows viewing live data&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Desktop support&lt;/td&gt;
&lt;td&gt;~ (Windows only)&lt;/td&gt;
&lt;td&gt;Y (web only)&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;~ (Windows only)&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mobile support&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;~ (Android only)&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;Y (web only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eye-catching theme&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;Y&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&amp;lt;/section&amp;gt;&lt;/p&gt;
&lt;p&gt;[^57]: u-center. ublox. [Online]. Available: https://www.u-blox.com/en/product/u-center&lt;/p&gt;
&lt;p&gt;[^58]: T. Suzuki, GNSS Radar. Javascript. [Online]. Available: https://github.com/taroz/GNSS-Radar&lt;/p&gt;
&lt;p&gt;[^59]: S. Barbeau, GPSTest. Kotlin. [Online]. Available: https://github.com/barbeau/gpstest&lt;/p&gt;
&lt;p&gt;[^60]: T. Takasu, RTKLIB. C. [Online]. Available: https://www.rtklib.com/&lt;/p&gt;
&lt;h4&gt;6.2 Conclusions&lt;/h4&gt;
&lt;p&gt;Unfortunately not all of the originally planned features of the project were implemented, due to initial over-ambition for the precision time integration, along with unexpected external delays in obtaining access to AIS and ADS-B data, which were both only received in the last week of the project. Despite this, the GNSS &quot;War Room&quot; compares very favourably to similar software, matching 18 out of 20 listed features, with the next highest (RTKLIB) only having 12 out of 20. It proved resilient running under real-world conditions, running for a month with no issues, and performed well in testing even under abnormal conditions.&lt;/p&gt;
&lt;p&gt;Maintaining the flexibility of the project throughout its journey helped significantly, as it allowed the project to continue moving in the correct direction as the problem being tackled was further understood. The publish-subscribe architecture has proven successful and reliable as the core of the system, and provides a solid foundation for future work, proven by the initial integration of tracking planes using ADS-B data in the last week of the project. The API has also provided utility in integration with other projects, including displaying GNSS data on a mini-W.O.P.R. (Above: W.O.P.R. modified to display GNSS data (currently mean SNR) ), as shown in the project demo.&lt;/p&gt;
&lt;p&gt;Whilst GNSS does have military uses, and the project is titled &quot;War Room&quot;, I see no useful military or commercial applications for this project, as it is primarily aimed at demonstration purposes, and to get people interested in the topic. Similarly the commercial/societal impact is expected to be negligible, due to the project&apos;s narrow focus on education.&lt;/p&gt;
&lt;p&gt;The project has received extremely positive feedback any time it has been seen by others, including when people touring the Ashby building have seen the project displayed, with the &quot;WarGames&quot; styling regularly catching people&apos;s attention. Although this had the risk of potentially reducing usability, and requiring a completely custom UI generator to be implemented, the application remains functional and has successfully gotten people interested in the topic of GNSS – especially me.&lt;/p&gt;
&lt;p&gt;Overall this project has been successful with its goals, and I am very happy with the end result.&lt;/p&gt;
&lt;h4&gt;6.3 Future work&lt;/h4&gt;
&lt;p&gt;Despite its success, room still remains for further features to be added to the project.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Extracting and visualising more ADS-B data&lt;/li&gt;
&lt;li&gt;Implement originally planned precision time and AIS integration&lt;/li&gt;
&lt;li&gt;Integration with spoofing detection, i.e. as in [^61]&lt;/li&gt;
&lt;li&gt;Integration with security/authentication extensions, e.g. OSNMA&lt;/li&gt;
&lt;li&gt;Set up other GNSS publishers around the world to further monitor satellite locations&lt;/li&gt;
&lt;li&gt;Further visualisations of past data (i.e. DOP over time, previous receiver locations)&lt;/li&gt;
&lt;li&gt;Further testing: although the existing system has been heavily tested, it is likely that additional integration and end-to-end tests would benefit future stability.&lt;/li&gt;
&lt;li&gt;User interface for configuration, rather than requiring editing a text file&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;[^61]: D. Laverty, C. Kelsey, and J. O&apos;Raw, &apos;GNSS Time Signal Spoofing Detector for Electrical Substations&apos;, presented at the 2022 IEEE Power &amp;amp; Energy Society General Meeting, IEEE. doi: 10.1109/PESGM48719.2022.9916781.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;7.0 Appendices&lt;/h3&gt;
&lt;h4&gt;7.1 Photos and screenshots&lt;/h4&gt;
&lt;p&gt;&amp;lt;center&amp;gt;&amp;lt;i id=&quot;displays&quot;&amp;gt;Below: The matrix of displays in the Cyber Lab&amp;lt;/i&amp;gt;&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/displays.Wf6wEG2f_W0Dzi.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;&amp;lt;i id=&quot;running&quot;&amp;gt;Below: The system running in the Cyber Lab&amp;lt;/i&amp;gt;&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/pic.CED1MWnt_Z1sUfNL.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;&amp;lt;i id=&quot;globe&quot;&amp;gt;Below: Screenshot of the web view, displaying the 3D globe&amp;lt;/i&amp;gt;&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/globe.RD0bqvZq_20dxGo.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;&amp;lt;i id=&quot;badge&quot;&amp;gt;Below: replica ID badge made for project demo costume&amp;lt;/i&amp;gt;&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/badge.BXkiVjmp_Z2tHWX6.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;&amp;lt;i id=&quot;badge&quot;&amp;gt;Below: W.O.P.R. modified to display GNSS data (currently mean SNR)&amp;lt;/i&amp;gt;&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/wopr.ByzGUsuL_1ql4Q.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;8.0 References&lt;/h3&gt;
</content:encoded></item><item><title>The mirage of a system: An 83 year long waiting list</title><link>https://mck.is/blog/2025/posiwid/</link><guid isPermaLink="true">https://mck.is/blog/2025/posiwid/</guid><description>At what point does a system functionally not exist?</description><pubDate>Fri, 07 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Hiya! I&apos;m Autumn, a trans woman currently living in Northern Ireland. This post is about the waiting list masquerading as a healthcare system for trans people in the UK.&lt;/p&gt;
&lt;p&gt;It&apos;s primarily focused on Northern Ireland, not just because I live here, but also because when searching online for information about the system for trans people in the UK, NI is usually either barely mentioned, or just completely forgotten about.&lt;/p&gt;
&lt;p&gt;If you haven&apos;t seen the video titled &lt;a href=&quot;https://www.youtube.com/watch?v=v1eWIshUzr8&quot;&gt;&quot;I Emailed My Doctor 133 Times: The Crisis In the British Healthcare System&quot;&lt;/a&gt;, by Abigail Thorn, I strongly recommend it - it dives deeper into the whole system in England, and is very well produced!&lt;/p&gt;
&lt;h2&gt;The way it works from a distance&lt;/h2&gt;
&lt;p&gt;The way the system is currently set up in the UK, to get your gender legally recognised (assuming your gender identity happens to fall neatly into the binary deemed acceptable by the UK government), you must send an application to the government&apos;s Gender Recognition Panel, who decide if you&apos;re &quot;trans enough&quot; (not an actual quote) to be legally recognised as your own gender.&lt;/p&gt;
&lt;p&gt;As part of this application, you are required to have two reports from medical doctors and/or a clinical psychologist, which according to the UK government&apos;s website, &quot;you’ll probably need to pay for them even if you use NHS medical practitioners&quot;[^1], plus paying £5 (previously £140) for the application itself. They apparently &quot;look at your application within 22 weeks of applying&quot;[^2]. How it takes them almost 6 months to read a few letters, I have no idea.&lt;/p&gt;
&lt;p&gt;[^1]: &lt;a href=&quot;https://www.gov.uk/apply-gender-recognition-certificate/what-documents-you-need&quot;&gt;Apply for a Gender Recognition Certificate: What documents you need&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;[^2]: &lt;a href=&quot;https://www.gov.uk/apply-gender-recognition-certificate&quot;&gt;Apply for a Gender Recognition Certificate: Overview&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;One of these reports must confirm your diagnosis of &quot;gender dysphoria&quot;, a definitely real diagnosis that is always 100% of the time absolutely applicable. To get this diagnosis, you must either:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;a.&lt;/strong&gt; attempt to navigate the NHS[^3] system by getting referred to a Gender Identity Clinic/Gender Dysphoria Clinic by your GP, and get a gender dysphoria diagnosis there, or&lt;br /&gt;
&lt;strong&gt;b.&lt;/strong&gt; sign up to one of several private clinics, if you can afford to pay - all online, since none exist in Northern Ireland&lt;/p&gt;
&lt;p&gt;[^3]: When I say NHS in relation to Northern Ireland, technically I mean Health and Social Care&lt;/p&gt;
&lt;p&gt;Getting access to healthcare, be it hormones, surgery, etc, works similarly, requiring you to go through either a gender clinic or privately.&lt;/p&gt;
&lt;p&gt;In a better world private healthcare wouldn&apos;t exist, but as an example for how expensive it is, one clinic charges: £195 for a &quot;set-up fee&quot;, £30 every month for a &quot;monthly membership&quot;, £65 for a consultation to create your &quot;transition pack&quot;, then £185 for the 45 minute &quot;formal diagnosis session&quot;, for a minimum of £475. That doesn&apos;t even include hormones, blood tests, etc, which I&apos;ve heard from a friend costs an extra ~£40 per month on average.&lt;/p&gt;
&lt;p&gt;As for the NHS, there is a single clinic to cover all of Northern Ireland - the Brackenburn Gender Identity Clinic in Belfast.&lt;/p&gt;
&lt;p&gt;Even in theory, this system sucks. It&apos;s overly complex, with a whole bunch of unnecessary steps.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Why are you &lt;em&gt;required&lt;/em&gt; to be referred by your GP, when all this does is allow for delays?&lt;/li&gt;
&lt;li&gt;Dysphoria sure is a real, I&apos;ve felt it enough, but as a requirement for healthcare it means people to either lie as needed to get the treatment that will help them, or be honest and risk being denied.&lt;/li&gt;
&lt;li&gt;A governmental Gender Recognition Panel reads more like the setup for a joke than part of any system designed to help people.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The mirage&lt;/h2&gt;
&lt;p&gt;A couple of years after I realised I was trans, I began attempting to take the NHS pathway. When going to my GP to ask to be referred, I was able to cite the Royal College of GPs&apos; guidance on &lt;a href=&quot;https://www.rcgp.org.uk/representing-you/policy-areas/transgender-care&quot;&gt;&quot;The role of the GP in transgender care&quot;&lt;/a&gt;, which they say is &quot;to promptly refer, where appropriate, to a Gender Identity Clinic&quot;. (As in, not refuse, or introduce arbitrary delays, which I have heard from others attempting to do the same).&lt;/p&gt;
&lt;p&gt;Thankfully in my case, my GP listened to me, and within a few weeks I received a letter from the Brackenburn clinic (despite the NHS having my email address), asking me to email or phone them to confirm that I did actually wish to opt into the service - an unnecessary step which only serves to add further delays, with the risk of people missing the letter and not added to the waiting list, therefore being forgotten about. A week later, I received another two separate letters from the Brackenburn clinic to say I had been added to the waiting list, and also that they are &quot;unable to provide ... a timeframe regarding an initial appointment&quot;.&lt;/p&gt;
&lt;p&gt;In an &lt;a href=&quot;https://web.archive.org/web/20191217023650/https://belfasttrust.hscni.net/pdf/BrackenburnClinic-FAQ.pdf&quot;&gt;FAQ that has since been removed&lt;/a&gt; from their website, Brackenburn state they &quot;endeavour to see people for their first appointment within the set target of 13 weeks&quot;. At time of writing (March 2025), I have been waiting for 52 weeks.&lt;/p&gt;
&lt;h2&gt;The desert&lt;/h2&gt;
&lt;p&gt;Despite being told &quot;we are no longer giving out waiting list position numbers&quot;[^4], I can still make a guess. Last month, I made an &amp;lt;abbr title=&quot;Freedom of Information&quot;&amp;gt;FoI&amp;lt;/abbr&amp;gt; request to the Belfast Health and Social Care Trust. After taking longer than the twenty working days they &quot;must&quot;[^5] respond within, I received the &lt;a href=&quot;https://belfasttrust.hscni.net/download/720/march/21673/33142-brackenburn-gender-identity-clinic-waiting-times.pdf&quot;&gt;latest numbers as of the end of 2024&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;[^4]: &lt;a href=&quot;https://belfasttrust.hscni.net/service/brackenburn-clinic/&quot;&gt;Belfast Trust: Brackenburn Clinic&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;[^5]: &lt;a href=&quot;https://www.legislation.gov.uk/ukpga/2000/36/section/10&quot;&gt;Freedom of Information Act 2000, Section 10&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;As of the start of March 2025, there are currently 1,083 people currently on the waiting list. In 2024, only 13 people on this list were seen for initial appointments. This isn&apos;t lower than normal, in 2023 there were also only 13 people seen for initial appointments. At this current rate, anybody joining the waiting list can currently expect to wait &lt;strong&gt;&lt;em&gt;over 83 years for an initial appointment&lt;/em&gt;&lt;/strong&gt;[^6]. You won&apos;t actually have to wait this long though - many people in front of you in the waiting list will die of old age, or otherwise, before they ever receive their initial appointments.&lt;/p&gt;
&lt;p&gt;[^6]: I have no idea how the average wait for people getting an initial appointment last year was 4 years, when reportedly, there were already people &lt;a href=&quot;https://www.belfastlive.co.uk/news/northern-ireland/northern-irelands-only-nhs-gender-29969172&quot;&gt;waiting for 7 years last year&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Also shockingly, &lt;strong&gt;&lt;em&gt;not a single person under the age of 18 was seen for an initial appointment in 2024&lt;/em&gt;&lt;/strong&gt;, through the &quot;Knowing Our Identity&quot; service.&lt;/p&gt;
&lt;p&gt;Apparently, I have the right to change my legal gender, and the right to access medical treatment. All I have to do is wait until around my 80th birthday.&lt;/p&gt;
&lt;p&gt;The system, as it is, does not want trans people to exist in Northern Ireland, unless you can afford to pay for private healthcare. If a waiting list is so long that you cannot expect to ever receive an appointment within your lifetime, the only thing it provides is a misdirected sense of hope.&lt;/p&gt;
&lt;p&gt;It doesn&apos;t need to be this way. I am far from alone in saying the UK needs to get rid of the system as it is, remove the waiting lists and requirements to be trans enough by somebody else&apos;s standards, and move to an informed consent model similar to other countries.&lt;/p&gt;
&lt;h2&gt;No winds of change&lt;/h2&gt;
&lt;p&gt;I&apos;ve attempted to contact my local MLAs with the following email, not that I expected much to come out of that alone. (Did you know you can contact your local MLAs about transferred matters, including health? You can see who represents you using &lt;a href=&quot;https://www.theyworkforyou.com/&quot;&gt;TheyWorkForYou&lt;/a&gt;, and see their phone numbers or email addresses on the &lt;a href=&quot;https://aims.niassembly.gov.uk/mlas/emails.aspx&quot;&gt;Northern Ireland Assembly&lt;/a&gt; website!)&lt;/p&gt;
&lt;p&gt;I received responses from 3 of my 5 MLAs.&lt;/p&gt;
&lt;p&gt;&amp;lt;section&amp;gt;&lt;/p&gt;
&lt;p&gt;Dear (name),&lt;/p&gt;
&lt;p&gt;I am writing to you as one of your constituents to bring to your attention the appalling waiting times for the NHS&apos; Brackenburn Gender Identity clinic, the only one in Northern Ireland providing assessment for and support of transgender people.&lt;/p&gt;
&lt;p&gt;Per a recent FoI request [1], there are 1,083 people, including me, currently on the waiting list for an initial appointment, with only 13 initial appointments being given per year. At this current rate, it will take 83 years for anybody currently being referred to receive an initial appointment.&lt;/p&gt;
&lt;p&gt;Given that everybody on this waiting list must be 17.5 or older, anybody currently joining the waiting list can expect to be dead before ever receiving an initial appointment.&lt;/p&gt;
&lt;p&gt;How is this in any way acceptable, and what is your plan to address this specific issue?&lt;/p&gt;
&lt;p&gt;Yours sincerely,
Autumn McKee&lt;/p&gt;
&lt;p&gt;[1]
https://belfasttrust.hscni.net/download/720/march/21673/33142-brackenburn-gender-identity-clinic-waiting-times.pdf&lt;/p&gt;
&lt;p&gt;&amp;lt;/section&amp;gt;&lt;/p&gt;
&lt;p&gt;Doug Beattie (UUP) responded to say &quot;I see no meaningful movement on this until there is recurrent funding&quot;, mentioning no plan to do anything to actually get funding. When asked again, he responded &quot;The Finance department is held by Sinn Fein, they have not allocated recurrent funding for the health minister to deal with waiting lists. Until they do, sadly, I can only see the GIC waiting list grown until the funding is found.&quot; Not his fault, that&apos;s fair. He didn&apos;t mention a plan to secure funding - maybe he doesn&apos;t have time to come up with one.&lt;/p&gt;
&lt;p&gt;&quot;Nobody is saying that all transgender women are predatory, but, again, the very presence of a male in a space designed for women creates problems&quot;[^7] - Doug Beattie, after proprosing legislation to force trans women into men&apos;s prisons last month. Interesting what he does have time for.&lt;/p&gt;
&lt;p&gt;[^7]: &lt;a href=&quot;https://www.newsletter.co.uk/news/politics/mlas-reject-uup-motion-on-banning-vulnerable-men-from-womens-prisons-as-alliance-brand-move-insensitive-4953326&quot;&gt;&quot;MLAs reject UUP motion on banning &apos;vulnerable&apos; men from women&apos;s prisons - as Alliance brand move &apos;insensitive&apos;&quot;&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Eóin Tennyson (Alliance) responded to say &quot;... the Minister of Health has advised that his Department is currently developing a new model of care for gender identity services in Northern Ireland.&quot;, and that &quot;The Minister has further advised ... to develop a business case for the additional resources necessary to effectively restart gender identity services in Northern Ireland. ... The business case was due to be submitted prior to Christmas, and I will again contact the Department to seek an update on its status.&quot; He also offered to have my individual case raised with the Department directly, below.&lt;/p&gt;
&lt;p&gt;I asked Eóin if the new model being developed plans to move towards an informed consent model, as otherwise it will not resolve the fundamental issue of locking treatment and support behind higher standards for trans people that create the waiting list issue in the first place. &quot;Unfortunately, I do not yet have that level of insight in respect of the Gender Review Group&apos;s work which is confidential at this stage.&quot;&lt;/p&gt;
&lt;p&gt;Two weeks after my case was raised with the department directly, I was sent the response from Mike Nesbitt (UUP), the current Minister of Health.&lt;/p&gt;
&lt;p&gt;&quot;Following significant engagement, my Department has developed a proposal for a new Lifespan Gender Service to create a more efficient patient pathway, a faster patient flow rate through endocrine, to ensure waiting lists decrease and patients receive the care they need in a timely way.&lt;/p&gt;
&lt;p&gt;My officials are currently considering the business case for the new Lifespan Gender Service, including its funding requirements. They will shortly provide me with advice for my consideration.&quot;&lt;/p&gt;
&lt;p&gt;We will if the &quot;Lifespan Gender Service&quot; fundamentally changes anything.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;John O&apos;Dowd (Sinn Féin) responded to say &quot;Sinn Féin will continue to lobby the Health Minister requesting detail on how his department intends to address this issue.&quot; So it seems that Sinn Féin are blaming the UUP for not doing anything, while the UUP blame Sinn Féin for not providing funding to do anything. (at times it very much seems to me as if the whole political system of Northern Ireland is designed to not function at all)&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Over 1 year later, Jonathan Buckley (DUP) and Diane Dodds (DUP) have not yet responded.&lt;/p&gt;
&lt;h2&gt;There are oases&lt;/h2&gt;
&lt;p&gt;We continue to fight and survive, as we always have.&lt;/p&gt;
&lt;p&gt;You don&apos;t need somebody else&apos;s approval to exist.&lt;/p&gt;
&lt;p&gt;Community is more important than ever. Be kind, and support each other.&lt;/p&gt;
&lt;p&gt;&amp;lt;3&lt;/p&gt;
</content:encoded></item><item><title>7 great games I played for the first time in 2024</title><link>https://mck.is/blog/2025/7-great-games/</link><guid isPermaLink="true">https://mck.is/blog/2025/7-great-games/</guid><description>Not deliberately a listicle</description><pubDate>Wed, 01 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Citizen Sleeper&lt;/h2&gt;
&lt;p&gt;Released: 2022, playtime: 8.1 hours&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/sleeper.D3MtntdK_Z1QExIm.webp&quot; alt=&quot;Performing an action during a cycle in Citizen Sleeper&quot; /&gt;&lt;/p&gt;
&lt;p&gt;You play as a robot who has just escaped from the corporation that legally owned you, and they&apos;ll soon be hunting you down. Your body is falling apart, and requires constant upkeep to be able to get anything done, the materials for which you&apos;ll need to obtain in ways that feel dubiously legal. &amp;lt;!-- you can tell that the person who made it is also trans --&amp;gt;&lt;/p&gt;
&lt;p&gt;That&apos;s not what citizen sleeper is about though - it&apos;s a game about hope, about existing in a world that feels at times actively hostile to your existence yet existing anyway, about people helping each other, and maybe working towards something better.&lt;/p&gt;
&lt;p&gt;There&apos;s also a fantastic &lt;a href=&quot;https://www.youtube.com/watch?v=ofBbL2vT20g&quot;&gt;interview with the developer&lt;/a&gt; which has criminally few views.&lt;/p&gt;
&lt;p&gt;Cyberpunk in the actual meaning of the word, not just as an aesthetic. Intensely relatable. Stay in the orbit of others, and take care of each other.&lt;/p&gt;
&lt;h2&gt;Vampire the Masquerade: Bloodlines&lt;/h2&gt;
&lt;p&gt;Released: 2007, playtime: 29.3 hours&lt;/p&gt;
&lt;p&gt;Such an amazing game, despite completely falling apart towards the end. (I love the ending[s] itself though!) One of the best RPGs ever made, and one of the worst sewer levels ever in a game.&lt;/p&gt;
&lt;p&gt;Like a lot of games its age you&apos;ll need the &lt;a href=&quot;https://www.moddb.com/mods/vtmb-unofficial-patch&quot;&gt;VTMB Unofficial Patch&lt;/a&gt;, which fixes a lot of bugs and optionally adds back some cut content.&lt;/p&gt;
&lt;h2&gt;Animal Well&lt;/h2&gt;
&lt;p&gt;Released: 2024, playtime: 14 hours&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/animal.DuCzU8K4_23zgBc.webp&quot; alt=&quot;A set of animal statues, colourfully lit&quot; /&gt;&lt;/p&gt;
&lt;p&gt;A platformer and puzzle metroidvania. You find secrets. You keep finding secrets. Then you get some item that allows you to find even more secrets. Incredibly pretty, incredibly well made, incredibly fun to play.&lt;/p&gt;
&lt;h2&gt;Webfishing&lt;/h2&gt;
&lt;p&gt;Released: 2024, playtime: 19.1 hours&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/fishing.D6VYkwrZ_Z12GS2J.webp&quot; alt=&quot;A bunch of polygonal cats fishing in a lake&quot; /&gt;&lt;/p&gt;
&lt;p&gt;A chatroom with a multiplayer fishing minigame attached - If you&apos;ve ever used chatrooms before, you know the experience! The vibe of the game really preselected people with similar interests to me - I&apos;ve never had people identify what my username is a reference to before, and everybody I met was fun to talk to. These cats gay! Good for them.&lt;/p&gt;
&lt;p&gt;Also, the displayed servers are all people local enough to you - it was great to hang out with a bunch of people in ireland and scotland.&lt;/p&gt;
&lt;h2&gt;Lookouts&lt;/h2&gt;
&lt;p&gt;Released: 2022, playtime: a couple hours&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/my_man.Bfs45wxy_Z1ebHNA.webp&quot; alt=&quot;The two coolest guys ever&quot; /&gt;&lt;/p&gt;
&lt;p&gt;oh my god it&apos;s the cutest thing ever i love both of them so much they&apos;re the best&lt;/p&gt;
&lt;h2&gt;Milk inside a bag of milk inside a bag of milk&lt;/h2&gt;
&lt;p&gt;Released: 2020, playtime: 18 minutes&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/milk.CfGaJhMj_Z1eai1M.webp&quot; alt=&quot;An almost photo of a supermarket aisle, with the text &amp;quot;Or rather, a bag of milk inside a bag of milk inside...&amp;quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;A game about buying milk, and mental illness. It&apos;s short. It&apos;s good. Play milk outside a bag of milk outside a bag of milk too.&lt;/p&gt;
&lt;h2&gt;Tactical Breach Wizards&lt;/h2&gt;
&lt;p&gt;Released: 2024, playtime: 6.7 hours[^1]&lt;/p&gt;
&lt;p&gt;[^1]: Note: the game is longer than this, I&apos;m only a couple hours in and just love it already&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/wizzards.Cm-xehzm_1LtSsJ.webp&quot; alt=&quot;Midway through a scenario against the less lethal pyromancer and traffic warlock&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Have you ever played Into the Breach and loved it, but felt like it could be even better? Tactical Beach Wizards is that.&lt;br /&gt;
Have you ever needed to blast somebody out of a window? Tactical Beach Lizards does that.&lt;br /&gt;
Have you ever ached to read one of the most tightly written and funny scripts ever? Practical Beach Lizards has that.&lt;br /&gt;
Have you ever wanted to feel like a genius for solving a really good puzzle? Practical Peach Blizzards allows that, if you&apos;re smarter than I am.&lt;br /&gt;
You should play Actual Peach Blizzards.&lt;/p&gt;
&lt;h2&gt;Honorable mentions:&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Umurangi Generation - I&apos;m angry we&apos;re living in the pre-apocalypse&lt;/li&gt;
&lt;li&gt;The Witness - Despises your time and probably you as well, otherwise it&apos;s great&lt;/li&gt;
&lt;li&gt;(the) Gnorp Apologue - This is the usecase for writing your own game engine in rust&lt;/li&gt;
&lt;li&gt;Balatro&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;I&apos;m looking forward to in 2025:&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Silksong - 2025 for sure!&lt;/li&gt;
&lt;li&gt;Citizen Sleeper 2: Starward Vector - played the demo&lt;/li&gt;
&lt;li&gt;Slay the Spire 2 - honesly I don&apos;t know how it could improve on the first, but I&apos;m excited to see regardless&lt;/li&gt;
&lt;li&gt;A game that didn&apos;t yet have a name when I played the demo at NIDC 2024, a mixture of N++ and Lunar Lander&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Pseudoscripting with &lt;noscript&gt;</title><link>https://mck.is/blog/2024/scripting-with-noscript/</link><guid isPermaLink="true">https://mck.is/blog/2024/scripting-with-noscript/</guid><description>Browsers hate this one weird trick!</description><pubDate>Fri, 13 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Update: a couple months later, this has been mostly outdated by the new &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/if&quot;&gt;CSS&apos; if()&lt;/a&gt; function - I recommend you go read about it, CSS gets cooler and cooler every year :D&lt;/p&gt;
&lt;p&gt;The new Container Style Query allows you to combine CSS that should only be applied when javascript is disabled with the rest of your CSS, in the same file/stylesheet.&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- can be in the head, or wherever you need it --&amp;gt;
&amp;lt;noscript&amp;gt;
&amp;lt;style&amp;gt;
:root {
--noscript: true;
}
&amp;lt;/style&amp;gt;
&amp;lt;/noscript&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- the stylesheet with the rest of your CSS --&amp;gt;
&amp;lt;style&amp;gt;
.nojs {
display: none;
}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@container style(--noscript: true) {
	.nojs {
		display: block;
	}

	.js {
		display: none;
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/style&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;p class=&quot;js&quot;&amp;gt;You can see this paragraph because javascript is enabled!&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;p class=&quot;nojs&quot;&amp;gt;
You can see this other paragraph because javascript is disabled!
&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- can be in the head, or wherever you need it --&amp;gt;
&amp;lt;noscript&amp;gt;
	&amp;lt;style&amp;gt;
		:root {
			--noscript: true;
		}
	&amp;lt;/style&amp;gt;
&amp;lt;/noscript&amp;gt;

&amp;lt;!-- the stylesheet with the rest of your CSS --&amp;gt;
&amp;lt;style&amp;gt;
	.nojs {
		display: none;
	}

	@container style(--noscript: true) {
		.nojs {
			display: block;
		}

		.js {
			display: none;
		}
	}
&amp;lt;/style&amp;gt;

&amp;lt;p class=&quot;js&quot;&amp;gt;You can see this paragraph because javascript is enabled!&amp;lt;/p&amp;gt;

&amp;lt;p class=&quot;nojs&quot;&amp;gt;
	You can see this other paragraph because javascript is disabled!
&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I doubt there&apos;s any scenario where this is particularly useful, as &lt;a href=&quot;https://caniuse.com/css-container-queries-style&quot;&gt;support for Container Style Queries&lt;/a&gt; was only added to Chrome in 2023, Safari in 2024, and Firefox not at all at the time of writing. There&apos;s definitely a few people in the world who have the combination of an up to date browser and javascript disabled, but not many, and I don&apos;t know if any websites in the last decade actually make use of &lt;code&gt;&amp;lt;noscript&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Still, I think it&apos;s neat trick, I like staying up to date with what&apos;s supported in CSS, and I&apos;m now using it on the main page of this site :D&lt;/p&gt;
</content:encoded></item><item><title>Bodging Python venv on NixOS</title><link>https://mck.is/blog/2024/python-nixos-bodge/</link><guid isPermaLink="true">https://mck.is/blog/2024/python-nixos-bodge/</guid><description>Using computers in a non-intended way is always more fun!</description><pubDate>Thu, 21 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Short backstory: I&apos;ve been trying out NixOS on my laptop for a few months now, and now I&apos;m needing to set up python on it to work on my final year project.&lt;/p&gt;
&lt;p&gt;On most linux distros, &lt;a href=&quot;https://wiki.archlinux.org/title/Python/Virtual_environment&quot;&gt;venv&lt;/a&gt; is the way to go for managing python packages, however due to NixOS not being &lt;a href=&quot;https://en.wikipedia.org/wiki/Filesystem_Hierarchy_Standard&quot;&gt;FHS-compliant&lt;/a&gt;, it&apos;s not as simple.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://wiki.nixos.org/wiki/Python&quot;&gt;NixOS wiki&lt;/a&gt; has a few suggestions:&lt;/p&gt;
&lt;h2&gt;The recommended way (shell.nix)&lt;/h2&gt;
&lt;p&gt;As an example, here&apos;s what the shell.nix for my final year project was looking like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# shell.nix
let
  # We pin to a specific nixpkgs commit for reproducibility.
  # Last updated: 2024-10-19. Check for new commits at https://status.nixos.org.
  pkgs = import (fetchTarball &quot;https://github.com/NixOS/nixpkgs/archive/8c4dc69b9732f6bbe826b5fbb32184987520ff26.tar.gz&quot;) { };
in
pkgs.mkShell {
  packages = [
    (pkgs.python3.withPackages (python-pkgs: with python-pkgs; [
      # select Python packages here
      pyqt6
      dataclass-wizard
      pylint
      pynmeagps
      pyserial
    ]))
  ];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It&apos;s pretty easy to manage, and &lt;em&gt;almost&lt;/em&gt; entirely works! Except. Because it&apos;s a nix shell and doesn&apos;t create a venv, VSCode doesn&apos;t know where to look for the packages, and constantly complains about missing imports. While absolutely not essential, I like type hinting and auto-completion within my IDE. Starting VSCode from within the shell didn&apos;t seem to work either, alas.&lt;/p&gt;
&lt;h2&gt;What else can I try?&lt;/h2&gt;
&lt;p&gt;Does creating a venv with the system python work? Sort of, until you need to use a compiled package, where you&apos;ll get an error like &lt;code&gt;ImportError: libstdc++.so.6: cannot open shared object file: No such file or directory&lt;/code&gt;. As much as I love NixOS, the combination of it&apos;s unique way of handling things, and my lack of knowledge of the &quot;proper&quot; way of doing things, are a bit of a pain sometimes.&lt;/p&gt;
&lt;p&gt;The NixOS wiki suggests a few solution, the first of which is a script called &quot;&lt;a href=&quot;https://github.com/GuillaumeDesforges/fix-python/&quot;&gt;fix-python&lt;/a&gt;&quot;, which attempts to patch the binary files in the venv to use the correct paths. An extremely cool idea! Unfortunately after running it, I was still getting the same error. The &lt;code&gt;libstdc++.so.6&lt;/code&gt; file it was failing to fetch is part of one of the packages &lt;code&gt;fix-python&lt;/code&gt; patches so it should have worked - it&apos;s possible it&apos;s related to &lt;a href=&quot;https://github.com/GuillaumeDesforges/fix-python/issues/6&quot;&gt;this issue&lt;/a&gt;, however I don&apos;t have time to debug (yet), I have a project I need to work on.&lt;/p&gt;
&lt;p&gt;I know the other solutions on the wiki are likely to work, but would take more time for me to learn than I have right now - I need to get working on my project so I have something to demo in a few days! So what else do I already know that I can bodge into a working solution?&lt;/p&gt;
&lt;h2&gt;The bodged way&lt;/h2&gt;
&lt;p&gt;If you&apos;ve not heard of it before, &lt;a href=&quot;https://github.com/89luca89/distrobox&quot;&gt;distrobox&lt;/a&gt; is a really useful tool that makes setting up a podman/docker container running a different distro, and integrating it with the rest of your system, really easy. Is this overkill? Absolutely. Does it work? Yes!&lt;/p&gt;
&lt;p&gt;Creating the container was easy:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;distrobox create --name arch-gnss --init --image archlinux:latest --pull --additional-packages &quot;fish python nano&quot;
# wait a bit for it to download the latest image and set up the container
distrobox enter arch-gnss -- fish # running fish since that&apos;s the shell i like
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now I&apos;m in the container, I can set up a venv as I would on any other distro:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python -m venv .venv
source .venv/bin/activate.fish
pip install -r requirements.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And it works! But I&apos;ve still not solved the earlier issue of VSCode not knowing where to look for the packages. So rather than using my host system&apos;s VSCode, I can install it in the container with &lt;code&gt;sudo pacman -S code&lt;/code&gt;. I&apos;m using the &quot;Code - OSS&quot; package rather than the &lt;code&gt;visual-studio-code-bin&lt;/code&gt; package as they have store their config in different places, to avoid any conflicts, but I can still copy over my pre-existing config with &lt;code&gt;cp &quot;~/.config/Code/User/settings.json&quot; &quot;~/.config/Code - OSS/User/settings.json&quot;&lt;/code&gt;. However this comes with a new issue: Microsoft, in their infinite wisdom, have decided that you can only run pylance, their extension for type hinting, on the official build of VSCode. Closed source software is great, isn&apos;t it? As &lt;a href=&quot;https://stackoverflow.com/questions/75345501/make-python-code-highlighting-for-vscodium-equal-to-vscode&quot;&gt;documented in this StackOverflow question&lt;/a&gt;, it&apos;s possible to manually install an old version of the extension from 2023 - I&apos;m not sure how long this&apos;ll continue to work for, but I only need it for a couple months. Incredibly janky, but it works!&lt;/p&gt;
&lt;p&gt;As a final step, I can export the container&apos;s vscode to my host system to make it easier to run:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;distrobox-export --app /usr/share/applications/code-oss.desktop # from within the container
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/distrobox.BNhgiC9__Z1Nm2Rz.webp&quot; alt=&quot;Screenshot showing VSCode with a working venv, thinking it&apos;s in an Arch Linux system, alongside a display showing the system is running NixOS&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Was there some other way to solve this? Almost certainly. But I didn&apos;t have time to find it, and this works for now. And that&apos;s it! A bodged and janky, but working, solution.&lt;/p&gt;
</content:encoded></item><item><title>Quick! It&apos;s an emergency and I need to read NMEA sentences from the ArduSimple simpleGNSS!</title><link>https://mck.is/blog/2024/nmea/</link><guid isPermaLink="true">https://mck.is/blog/2024/nmea/</guid><description>A guide for that highly specific scenario.</description><pubDate>Wed, 20 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;#what-does-any-of-this-stuff-mean&quot;&gt;Skip to the glossary&lt;/a&gt; at the bottom if you don&apos;t know what any of that means, we don&apos;t have time for that now!&lt;/p&gt;
&lt;p&gt;First, connect your antenna to your receiver via the SMA connector labelled &quot;RF IN&quot;. Connect the the receiver to your computer via USB, making sure to use the port lablled &quot;POWER + GPS&quot; on the receiver. The receiver should power on, and two LEDs should begin pulsing every second.&lt;/p&gt;
&lt;h2&gt;In the command line&lt;/h2&gt;
&lt;p&gt;If you&apos;re on Linux: Install &lt;a href=&quot;https://github.com/tio/tio&quot;&gt;tio&lt;/a&gt; with your package manager, then run:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tio --baudrate 38400 /dev/ttyUSB0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you&apos;re on Windows: Install &lt;a href=&quot;https://www.putty.org/&quot;&gt;PuTTY&lt;/a&gt;, then run:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;putty -serial COM4 -sercfg 38400
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(Might be COM3, COM5, etc. Check Device Manager)&lt;/p&gt;
&lt;h2&gt;In python&lt;/h2&gt;
&lt;p&gt;Install the &lt;code&gt;pyserial&lt;/code&gt; package with pip:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install pyserial
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;import os
from serial import Serial

SERIAL_PORT = &quot;/dev/ttyUSB0&quot; if os.name == &quot;posix&quot; else &quot;COM4&quot;
BAUDRATE = 38400
with Serial(SERIAL_PORT, BAUDRATE) as stream:
    while True:
        print(stream.readline().decode(&quot;utf-8&quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Never mind, I want a GUI&lt;/h2&gt;
&lt;p&gt;On windows, I&apos;d recommend &lt;a href=&quot;https://www.u-blox.com/en/product/u-center&quot;&gt;u-center 2&lt;/a&gt;. On Linux, there seemingly aren&apos;t many good options (yet :3), but &lt;a href=&quot;https://github.com/semuconsulting/PyGPSClient&quot;&gt;PyGPSClient&lt;/a&gt; seems to work well.&lt;/p&gt;
&lt;h2&gt;What does any of this stuff mean?&lt;/h2&gt;
&lt;p&gt;GNSS: Global Navigation Satellite System - the generic term for any satellite navigation system, the most well-known of which is GPS (US). Galileo (EU), GLONASS (Russia), and BeiDou (China) also exist. Although they&apos;re ran by different countries*, they all cover the entire globe.&lt;/p&gt;
&lt;p&gt;NMEA (0183): The standard communication specification for GNSS receivers, among other things.&lt;/p&gt;
&lt;p&gt;SMA: A type of coaxial connector, used to connect the antenna to the receiver.&lt;/p&gt;
&lt;p&gt;ArduSimple simpleGNSS: The receiver I&apos;m using for this guide and my final year project, as it&apos;s cheap and easy to use. Other products are available.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/receiver.D7mPfeFb_ZLGCYi.webp&quot; alt=&quot;Photo of the receiver&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Baud rate: The rate at which bits are expected to be transmitted over a serial connection.&lt;/p&gt;
&lt;p&gt;A short blog post while I&apos;m busy with my final year project, as I wanted to make sure I don&apos;t forget about having a &quot;blog&quot; for another two years.&lt;/p&gt;
</content:encoded></item><item><title>Running a fediverse instance: The technical side is the easy bit</title><link>https://mck.is/blog/2024/running-an-instance/</link><guid isPermaLink="true">https://mck.is/blog/2024/running-an-instance/</guid><description>People are a lot more difficult</description><pubDate>Thu, 18 Jul 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This blog post is a couple of thoughts on running a fediverse instance after growing one from a single user instance to a community of about 20 friends over a year and a half - a few things I think went well, what I could&apos;ve done better, and a caution that it&apos;s not as easy as I originally thought it would be.&lt;/p&gt;
&lt;p&gt;This isn&apos;t a blog post to explain the fediverse. In short, it&apos;s a social network that on the backend functions similarly to email, in that people can be on different providers (gmail, icloud, etc) and still talk to each other. (Although the fediverse is a lot less centralised than email, with thousands of instances of all sizes)&lt;/p&gt;
&lt;h2&gt;The easy bit&lt;/h2&gt;
&lt;p&gt;There&apos;s not too much involved in the technical side, at least on the small scale. (when things scale up it definitely gets a lot more complex - admins like Jo on tech.lgbt deserve a lot of thanks for all the work they do to keep an instance of that size running smoothly)&lt;/p&gt;
&lt;p&gt;Initially setting up fedi software confused me a bit, having never really hosted anything myself before, but following the documentation got me going fairly quickly, and as far as I know it&apos;s only gotten easier since then (some instance software seems to have added install scripts to make the setup as easy as possible, which is great for helping people get started!)&lt;/p&gt;
&lt;p&gt;Ongoing maintenance didn&apos;t involve much either. Updates are easy and only take a couple of minutes. Even when I chose to migrate the whole instance to a dedicated machine (hosting others who are popular and post &lt;em&gt;hard&lt;/em&gt; means a noticeable spike in CPU usage every time they post), everything went smoothly and everything was back up and running within an hour.&lt;/p&gt;
&lt;p&gt;There were some mysterious ongoing issues even the developers seemed to have no clue what to do about (nobody seems to be sure if pleroma/akkoma database &quot;rot&quot; is real or not, and it is only spoken about with superstition and in hushed tones, in case it might overhear), but everything still kept working.&lt;/p&gt;
&lt;p&gt;I don&apos;t bring any of this to say I&apos;m good at the technical side of things, but as a caution: &lt;strong&gt;&lt;em&gt;Being able to handle the technical side of things is not enough for running an instance with other people on it!!&lt;/em&gt;&lt;/strong&gt; You can find the technical part easy, and still find running an instance to be a lot of work.&lt;/p&gt;
&lt;h2&gt;The difficult bit&lt;/h2&gt;
&lt;p&gt;No, the difficult bit of running an instance is the social side - everything that comes with providing a platform for others, and being part of a much wider network of instances containing over a million accounts.&lt;/p&gt;
&lt;p&gt;As I see it, handling a fediverse instance has 2 parts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Managing everybody on your instance&lt;/li&gt;
&lt;li&gt;Managing everybody on other instances&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;On your instance&lt;/h3&gt;
&lt;p&gt;Managing an instance well can be a lot of work, so I tried to keep things as easy as I could for the first part by limiting the number of people on the instance, and only accepting others I already knew and trusted on to the instance (mostly it was folk asking if they could join the instance after we got to know each other, and a few being ones I invited myself)&lt;/p&gt;
&lt;p&gt;This meant a few things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I could trust them to not cause too many issues for me - not having to worry much about getting and handling reports is great&lt;/li&gt;
&lt;li&gt;Everybody there shared me as a friend - people I get on well with seem to get on well with each other, even when they&apos;re very different, so it minimised drama within the instance. As a whole, it was a comfy and cosy space&lt;/li&gt;
&lt;li&gt;If an issue with another instance did occur, it usually meant either:
&lt;ul&gt;
&lt;li&gt;I was happy backing the person on my instance, and try to talk with the other instance&apos;s admin to resolve the issue&lt;/li&gt;
&lt;li&gt;or it was some minor issue that I could send a quick message to the the admin of the other issue to resolve&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;However it also comes with the fairly significant downside that when there is a legitimate issue or drama, it&apos;s been caused by somebody you&apos;re close friends with. Depending on the scale of the issue, it can be emotionally exhausting to navigate and deal with.&lt;/p&gt;
&lt;p&gt;Building a community is also part of it - a good instance feels like it&apos;s more than just a collection of accounts that happen to share the same server. I think it was the most important thing that made my instance so special, to me and others. Leading by example as what you want to see in other people seemed to work for me.&lt;/p&gt;
&lt;p&gt;I didn&apos;t originally intend for anybody else to be on my instance (it was something I set up for just myself, but then a friend asked if she could move to my instance, and it grew from there), so I didn&apos;t have any rules to start with. When a couple people joined I threw a basic set of rules together (no transphobia, racism, harassment, etc), but with so closely limiting who could join in the first place, they served more as a set of guidelines for what other instances would be defederated for, rather than being needed for anybody on my instance. If you don&apos;t feel like coming up with your own set of rules from scratch, you can do what I did and mostly copy the rules from an instance I trusted. Speaking of:&lt;/p&gt;
&lt;h3&gt;On other instances&lt;/h3&gt;
&lt;p&gt;Unless you&apos;ve had to do content moderation on the wider internet, I don&apos;t think you&apos;re aware of how awful it can be. You will see things you will wish you had never seen. That&apos;s as much detail as I&apos;ll give here.&lt;/p&gt;
&lt;p&gt;For the most part I tried to be as proactive an approach to moderation as possible possible, which I think was a fair part in maintaining the comfy atmosphere of the instance. If you can, try to keep a close eye on everything going on in the rest of the fediverse, although remember you can (and &lt;strong&gt;need to&lt;/strong&gt;) also take breaks.&lt;/p&gt;
&lt;p&gt;I also put the work into maintaining and verifying my own blocklist for the instance - this takes a lot of work, and is one of the things that led to me completely burning out. My suggestion is you either:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;don&apos;t try to do everything yourself! You can get other people to help you. If I ever wanted to try running an instance again, this is the approach I&apos;d want to take.&lt;/li&gt;
&lt;li&gt;rely on the blocklist of another instance/group of people you trust (not my preferred option, as I had bad experiences with my instance ending up on some trusted blocklists, and even the managers of the blocklist being unsure as to the reason, although if it works for you then great)&lt;/li&gt;
&lt;li&gt;acknowledge that your moderation will be reactive rather than proactive (if the people on your instance like this approach then great, I&apos;ve seen it work really well on other instances)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I&apos;d also recommend being as transparent as you can about moderation decisions to everybody on your instance - I understand wanting to keep your instance&apos;s blocklist private to avoid scrutiny, but being transparent to the people on your instance about the moderation decisions you&apos;re making, why you&apos;re making them, and being open to disagreement is not only the right thing to do in my opinion, but it also shows them the work you&apos;re putting in to keep everything running, and helps them trust you running the instance.&lt;/p&gt;
&lt;h2&gt;Did my approach work?&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/positive-1.D1fuXQU1_29pfFT.webp&quot; alt=&quot;Well, this has been the nicest place I&apos;ve ever had the pleasure of inhabiting on the internet but it&apos;s time to migrate away. Thanks for running this place Autumn, it really did feel like home.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/positive-2.h7m5eubz_U0sTH.webp&quot; alt=&quot;0w0.is is shutting down in 3 months. I&apos;ve hopped instances quite a bit since joining fedi last November, but 0w0.is has been a very special place to me. For the first time I found somewhere out of anywhere that had a homely feel to it, where I felt comfortable to make any random noise that came to mind. The other beings here are amazing and I&apos;m fortunate to have met all of them through here. Autumn has been the best admin I could ask for, always being vigilant, sensible, and understanding in all the drama that affected this place. She&apos;s cute and wholesome and adds all the emojis I ask for and she&apos;s super cute&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/positive-3.ChQ11d9B_Z1KPVJp.webp&quot; alt=&quot;0w0 is a community to me, and one of the most banging places on this Fediverse. Everyone who is and has been on here is righteously funny and amazing. This has been the coolest and cosiest place I have ever had the privelage of existing in, second only to being with my IRL friends. This is no small part due to the tireless work of Autumn. She is a wonderful person, a close friend, and one of my top sisters. But not only that, she is the pinnacle of being an admin. Autumn was responsible for creating this community and I applaud her totally. May she enjoy her break.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/positive-4.3twwRZnQ_krnTS.webp&quot; alt=&quot;It has been a wonderful few months since I migrated. I am extremely thankful to the work Autumn has done on 0w0.is - while I&apos;ve beebn one of the most recent creatures to join the instance, and even though I may not have interacted with everyone in here, I appreciate how cozy and frankly welcoming my time on here has been. I am understandably a bit sad seeing all of this go - but I am glad I can look back on every interaction I&apos;ve had in a positive light.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/positive-5.DTtFaeIg_Z2uCo8s.webp&quot; alt=&quot;0w0.is has been the comfiest online space I&apos;ve ever been in. I dont just mean fedi instance - out of any internet community I&apos;ve been a part of, 0w0.is has felt like the safest, nicest, coziest space I could&apos;ve ever asked for. Autumn did an amazing job with handling this instance and I think every instance admin should aspire to get to the same level 0w0 was at. This instance felt so friendly and charming, Autumn harbored a really great community here. It&apos;s gonna be sad to see it go.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Yes.&lt;/p&gt;
&lt;p&gt;Not really much more to say here. Managing to create a space that got these posts is something I&apos;m indescribably proud of.&lt;/p&gt;
&lt;h2&gt;Ending&lt;/h2&gt;
&lt;p&gt;Looking back, the main thing I now think I could&apos;ve done better is to get help from others. I tried to run everything by myself, and although I did it well, it completely exhausted me and burnt me out, taking many months to recover. Having other people to help would&apos;ve allowed me to feel like I could take breaks, and made running the instance sustainable. I hope I&apos;ve learnt from this.&lt;/p&gt;
&lt;p&gt;I&apos;ve tried to write something about fedi a couple of times now, but not managed to get something that feels right and fully contains my thoughts on something that took quite a bit of my time for a year or so to run. I&apos;m happy enough with this post, covering a small aspect of the whole thing at least. Rest in peace, &lt;a href=&quot;https://0w0.is/&quot;&gt;0w0.is&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Lenovo ideapad 5 14are05 review</title><link>https://mck.is/blog/2024/14are05-review/</link><guid isPermaLink="true">https://mck.is/blog/2024/14are05-review/</guid><description>Because why not review a 4 year old laptop that nobody can buy anymore?</description><pubDate>Fri, 12 Jul 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;What?&lt;/h1&gt;
&lt;p&gt;I&apos;m looking into replacing my current laptop (a lenovo ideapad 5 14are05) as the 8gb of RAM I bought it with is beginning to become a limitation, and I&apos;ve had to be actively keeping an eye on memory usage when trying to do anything more intensive. Although it&apos;s been a fantastic laptop and otherwise I&apos;ve liked it a lot, I don&apos;t like having to keep an eye on memory usage, so the memory not being upgradable is now become an issue for me.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/picture.Bvqg4fJ1_2mpfQQ.webp&quot; alt=&quot;Photo of the laptop, open and running NixOS, from the front&quot; /&gt;&lt;/p&gt;
&lt;p&gt;So now that I&apos;ve pretty much finished using it as a laptop (I&apos;ll very likely attempt to repurpose it into a server at some point), I felt like writing an overview of its specs and how I found using it, to me understand what I&apos;ve liked about it and what I want from a laptop.&lt;/p&gt;
&lt;p&gt;As a warning, this blog post is going to be fairly dry and boring - even more so than most of my other blog posts, it&apos;s meant to be written, rather than actually read! For something more interesting, maybe see &lt;a href=&quot;../small-projects-for-fun/&quot;&gt;my blog post on writing a music display for my website&lt;/a&gt;.&lt;/p&gt;
&lt;h1&gt;Hardware&lt;/h1&gt;
&lt;h2&gt;CPU&lt;/h2&gt;
&lt;p&gt;Ryzen 3 4300U&lt;/p&gt;
&lt;p&gt;The main reason I picked this laptop specifically! It was one of the first and cheapest laptops to release with the Zen 2 architecture, which I&apos;d been impressed by when I built my desktop PC with a 3600x the previous year. Although the 4300u is the lowest end 4000 series mobile CPU, with 4 cores/4 threads it&apos;s still amazing for the price, and has performed well for all my needs. The iGPU is enough to run some basic indie games - more performance would&apos;ve been nice, but it covered the basics well enough.&lt;/p&gt;
&lt;h2&gt;Display&lt;/h2&gt;
&lt;p&gt;14&quot; 1080p IPS screen @ 60Hz&lt;/p&gt;
&lt;p&gt;It&apos;s able to get bright enough for most of the time, but it&apos;s definitely too dim for direct sunlight (thankfully with the weather here, that&apos;s pretty rare)
I don&apos;t have any fancy tools for measuring colour accuracy, but to me it&apos;s a good looking and well-balanced screen, what you&apos;d expect from a decent IPS panel.
Mine has no noticeable uniformity issues, and the matte screen and good viewing angles have made it a great screen for my general use.&lt;/p&gt;
&lt;p&gt;At 60Hz it&apos;s not any smoother than any of the rest of my monitors, but given the performance of the integrated graphics, it&apos;d only really be the mouse cursor and moving windows around that would benefit. (Again at the price, I&apos;m not really complaining or expecting better)&lt;/p&gt;
&lt;p&gt;The hinges are well enough built, with the screws securing them in place fairly well spread out. (They&apos;re badly labelled as M2.5xL5.5, but at least some thought went into that!). The display cable doesn&apos;t seem to have worn out and the hinges are still good after 4 years of use, so I&apos;m happy to call this aspect well designed! (Although unfortunately does take two hands to effectively open)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/left-hinge.Si873KU4_1lp8Hq.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/right-hinge.D1y_tfBh_ZpDqU.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Storage&lt;/h2&gt;
&lt;p&gt;Has space for a M.2 2280 NVMe drive, and a M.2 2242 NVMe drive, with both slots being easily accessible. The configuration I bought came with a tiny 128GB 2242 drive, which is mostly my fault as that&apos;s deliberately what I chose to save money. I&apos;ve upgraded and added more storage since, but 128GB was just enough for a basic windows install and the handful of programs I needed for A-level software systems development, even if it felt like a bit of a squeeze at times. Even with that lowest possible spec drive, it still had a DRAM cache and always felt fast (having any sort of SSD is almost always the best first upgrade for an older system)&lt;/p&gt;
&lt;h2&gt;Battery&lt;/h2&gt;
&lt;p&gt;57Wh&lt;/p&gt;
&lt;p&gt;The battery is secured by 2 helpfully labelled M2x3 screws, and is easily replaceable (not that I&apos;ve needed to, thankfully!)&lt;br /&gt;
4 years later, lenovo are still selling replacement batteries, for £72 (Part number 5B10W86939) - feels a little more expensive than it should be, but still good to see some effort towards repairing or extending the lifespan of devices like this.&lt;/p&gt;
&lt;h2&gt;Webcam&lt;/h2&gt;
&lt;p&gt;It has one, it&apos;s worked well enough when I needed it for drum lessons and uni lectures. Like most laptop webcams it&apos;s pretty bad, but worked well enough for my needs.&lt;/p&gt;
&lt;h2&gt;Speakers&lt;/h2&gt;
&lt;p&gt;It has them, I guess? I don&apos;t think I&apos;ve ever used them, nor do I ever really use other laptop speakers to be able to compare - I stick to headphones and earbuds to avoid &lt;s&gt;annoying others&lt;/s&gt; making others jealous of my music taste.&lt;/p&gt;
&lt;h2&gt;Ports&lt;/h2&gt;
&lt;p&gt;Left side:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Barrel plug (5.5x2.5, centre positive) - the power brick it came with was a 65w charger, which has made the charging experience about as nice as it could be without also being USB C&lt;/li&gt;
&lt;li&gt;USB C 3.0 port - supports charging and DisplayPort over USB C thankfully! So I can still charge it with the other USB-C chargers I have&lt;/li&gt;
&lt;li&gt;HDMI port - Not had any need to use it&lt;/li&gt;
&lt;li&gt;3.5mm headphone/mic combo jack - has occasionally had issues with crackling while trying to drive 250ohm DT990s, but otherwise worked well enough&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Right side:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;2x USB A 3.0 ports, with a reasonable amount of space between them (never had issues trying to plug in two things at once, compared to some other laptops which stick the ports closer together)&lt;/li&gt;
&lt;li&gt;SD card reader - I&apos;ve only used it for reading some files from an old Raspberry Pi&apos;s SD card, so I can say it works, but I&apos;ve not had any reason to test its speed&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Other&lt;/h2&gt;
&lt;p&gt;Has a Realtek 8822CS wifi/bluetooth card, which I&apos;ve not had any issues with. No fingerprint reader or anything fancy - It&apos;s a very standard laptop&lt;/p&gt;
&lt;p&gt;Trackpad 10.5cm x 6.5cm - has felt a little on the small side&lt;/p&gt;
&lt;p&gt;Keyboard - has been pretty good! Not fantastic, but I&apos;ve done a lot of typing on it by now, and when I&apos;m typing I don&apos;t notice the keyboard, which I think is ideal&lt;/p&gt;
&lt;p&gt;Overall construction - feels well built and solid. The metal top feels and looks nice, I like how small and off to the side the logo is (although I&apos;d prefer none at all, as somebody who doesn&apos;t enjoy paying a company money to advertise their products). The bottom half is some kind of plastic, but feels surprisingly decent, and has held up so far over time.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/picture2.BRH7Vda9_2mWw99.webp&quot; alt=&quot;Photo of the laptop from a side angle&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;Software&lt;/h1&gt;
&lt;h2&gt;Linux support&lt;/h2&gt;
&lt;p&gt;By the time I got around to installing linux on it (maybe a year or so after I got it?) pretty much everything was fully supported, so for the most part everything just worked! Apparently the 14are05 was popular enough it got its on page on &lt;a href=&quot;https://wiki.archlinux.org/title/Lenovo_IdeaPad_5_14are05&quot;&gt;the Arch wiki&lt;/a&gt;, which covers some of the original issues.&lt;/p&gt;
&lt;p&gt;I did have an issue with the trackpad occasionally thinking it was instead a mouse, which broke gestures (including just scrolling) and significantly increased its sensitivity, but this was always fixed by quickly putting the laptop to sleep for a second so I wasn&apos;t too bothered. (Did I ever make a bug report about this? No. Should I have? Yes! Will I? Nah)&lt;/p&gt;
&lt;h1&gt;Overall&lt;/h1&gt;
&lt;p&gt;I&apos;m really glad I bought it when I did - I loved it when it was new, and for the most part it&apos;s lasted very well over the last 4 years.&lt;/p&gt;
&lt;p&gt;Since I got a student discount when buying it, it was also an amazing price at £414. For that, there was nothing else available at the time that really came close.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/price.BwjOKFOq_Z13LA0w.webp&quot; alt=&quot;£345 plus £69 VAT for £414 total&quot; /&gt;&lt;/p&gt;
&lt;p&gt;I am slightly sad to be replacing it, but I have plans to use it for other things so I&apos;ll still have it around. It&apos;s been a great laptop!&lt;/p&gt;
</content:encoded></item><item><title>We reached peak smartphone years ago</title><link>https://mck.is/blog/2024/stagnation-of-phones/</link><guid isPermaLink="true">https://mck.is/blog/2024/stagnation-of-phones/</guid><description>Maybe that&apos;s ok? They&apos;re pretty good as they are</description><pubDate>Sun, 21 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Recently my Flip 4, which I&apos;d been using for about a year, stopped charging, claiming the &quot;temperature was too low&quot;. So before sending it in to be repaired (before it being returned with them saying the battery/charging port issue couldn&apos;t be fixed under warrenty, as the screen had some tiny cracks in it??), I switched everything back to my OnePlus 7t, which I&apos;d kept around just in case something like this happened.&lt;/p&gt;
&lt;p&gt;The 7t is definitely still a fantastic phone, but this also prompted me to check in on the current state of phones again. So what&apos;s changed since the 7t was released back in September 2019?&lt;/p&gt;
&lt;h2&gt;Hardware&lt;/h2&gt;
&lt;h3&gt;Screens&lt;/h3&gt;
&lt;p&gt;The 90Hz screen of the 7t is still amazingly smooth and responsive. On newer phones 120Hz is becoming more common, and it is marginally smoother - but I can only spot the difference if I have the two side by side, and it&apos;s such a minor improvement I don&apos;t think it&apos;s worth the battery life.&lt;/p&gt;
&lt;p&gt;(The easiest way to make any android phone feel snappier is to set the animation scales to 0.5x in developer settings - I honestly feel like this should be the default)&lt;/p&gt;
&lt;p&gt;Like the 7t, most new phone screens are also still beautiful OLEDs of some type or another - high dynamic range, good viewing angles, vibrant colours. If you have a phone, it&apos;s very likely it has the best screen you have access to.&lt;/p&gt;
&lt;p&gt;Screen resolution increases are far beyond the point of diminishing returns and just at the point of not being noticeable at all. Even with the current phone sizes, 1080p is more than enough for individual pixels to not be noticeable. It&apos;s nice that it seems most new phones are choosing not to waste money and battery life to go above this.&lt;/p&gt;
&lt;p&gt;Phone bezels haven&apos;t been pushed out much further - screens have been about as close to the edge as they can be for a while now, with great screen-to-body ratios.&lt;/p&gt;
&lt;h3&gt;Cameras&lt;/h3&gt;
&lt;p&gt;These days, software is a much more important part of how photos look. The cameras themselves still have about the same resolution, although improvements like a larger sensor and optical zoom can still be made. They definitely have continued to improve over time, but my 7t is still more than capable enough for whatever random photos I feel like taking.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/7t-photo-1.BPsio1We_1EJNkS.webp&quot; alt=&quot;Photo of a dirt lane, with trees overhead, and lilac flowers either side of the lane&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/7t-photo-2.BD6evz8Z_Z2toEeq.webp&quot; alt=&quot;Close-up photo of ice crystals&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Compared to photos from the flip 4, I really can&apos;t tell if it&apos;s better or not? If I&apos;m having to pixel peep to find any differences then they must be pretty negligible. Both can take fantastic photos, which is more than good enough for me!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/flip4-photo-1.CNjqtEd__ZB64AU.webp&quot; alt=&quot;Photo of the flowerbeds in front of the palm house in Botanic Gardens, Belfast&quot; /&gt;&lt;/p&gt;
&lt;p&gt;(It bothers me that the flowerbed in this photo is off-centre, I&apos;ll try to retake it this summer)&lt;/p&gt;
&lt;h3&gt;Batteries&lt;/h3&gt;
&lt;p&gt;Battery capacities have increased over time, but the changes have been fairly small and incremental. Using OnePlus as an example:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/battery.DFdb7tkl_Z2k1mHa.webp&quot; alt=&quot;Graph of OnePlus x / xT phones&apos; battery capacity. It&apos;s a fairly linear graph with only small increases in capacity over time&quot; /&gt;&lt;/p&gt;
&lt;p&gt;There&apos;s not really anything exciting here. Definitely nice to have, but not particularly exciting, and hardly worth releasing nearly 2 phones every year for.&lt;/p&gt;
&lt;h3&gt;Aesthetics / form factor&lt;/h3&gt;
&lt;p&gt;On the outside, phones haven&apos;t changed much. They&apos;re still flat rectangles, a mixture of glass and either metal or plastic, with camera bumps still changing shape and jumping around. Some phones are experimenting with folding in various directions, but having tried the flip 4 myself, it feels more like a slightly inconvenient gimmick rather than a useful feature.&lt;/p&gt;
&lt;p&gt;Phones keep alternating between rounded and flat edges, so there can at least be some visual difference between phone generations, but whichever you prefer is just personal preference.&lt;/p&gt;
&lt;p&gt;Front cameras are still in either a teardrop or pinhole at best - at some point, under-screen cameras will likely become more common, but it&apos;s not enough of a change for me to look forward to.&lt;/p&gt;
&lt;h3&gt;Performance&lt;/h3&gt;
&lt;p&gt;CPU and GPU performance and memory have kept increasing, but none of it enables any new use for phones. It feels like chasing after higher numbers for the sake of having something to advertise, regardless of how much of a difference it actually makes for the end user.&lt;/p&gt;
&lt;p&gt;My 5 year old phone is still perfectly capable of displaying everything smoothly at 90Hz, with no hitches or slowdowns. Maybe these specs still matter if you&apos;re playing some intensive 3d game on your phone, but the only mobile games I&apos;ve wanted to play have been Slay the Spire and Dead Cells (Both aamzing games! and ported well to mobile)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/dead-cells.Dhults6P_Z1XhQbb.webp&quot; alt=&quot;Dead cells screenshot, the start of the ramparts on mobile&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Other&lt;/h3&gt;
&lt;p&gt;Most phones today are equally lacking in a headphone jack compared to the 7t :(&lt;/p&gt;
&lt;p&gt;Even the need for storage has significantly slowed down - the base 128gb my 7t came with is plenty for my 34gb of music, and 21gb of all my photos and videos from the last decade.&lt;/p&gt;
&lt;h2&gt;Software&lt;/h2&gt;
&lt;p&gt;Thanks to all the volunteers who contribute to &lt;a href=&quot;https://lineageos.org/&quot;&gt;LineageOS&lt;/a&gt;, my phone from 5 years ago runs Android 14, and the most recent security patch.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/android-14.Ck0m_DWU_1XR5O6.webp&quot; alt=&quot;Running Android 14 with the April 2024 security update&quot; /&gt;&lt;/p&gt;
&lt;p&gt;As a side-note, the look of stock android has really grown on me. Its much more frequent use of material UI and overall design just feel comfy to me.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/android-notification-shade.DXzZlI3b_Z1CBBre.webp&quot; alt=&quot;Screenshot of LineageOS 20&apos;s notification shade, accented green&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Compared to the updates from OnePlus themselves, which:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Usually took months to release&lt;/li&gt;
&lt;li&gt;Frequently introduces more bugs which were never later fixed (e.g. split-screening apps broke with the update to either Android 11 or 12)&lt;/li&gt;
&lt;li&gt;Only received 2 major Android updates (10 -&amp;gt; 12)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It&apos;s a shame that such a great phone was just abandoned by the manufacturer, when it&apos;s capable of lasting much longer.&lt;/p&gt;
&lt;h2&gt;So?&lt;/h2&gt;
&lt;p&gt;The 7t was £550 when it launched (nearly 5 years ago at time of writing!), so it&apos;s not as if it was extremely expensive relative to other phones at the time - the S10 launched at £800 the same year.&lt;/p&gt;
&lt;p&gt;In my opinion, this lack of change has actually a good thing in this case. It means manufacturers are having to focus on providing longer-term support for their devices (e.g. at the start of this year, samsung announced they&apos;d extend their android and security updates to 7 years! Although I&apos;m waiting to see how well they follow through on that). Hopefully this will also lead to less e-waste, with people replacing their devices less frequently.&lt;/p&gt;
&lt;p&gt;However, what also needs to happen is for phones to become more repairable. Replacing a battery or screen should be as easy as possible. Fairphone have proven this is doable. When a phone is still just as capable as it was when it was released, and its only issue is the battery having been worn down over time, I think it&apos;s reasonable that a user should be able to replace the battery themselves.&lt;/p&gt;
&lt;p&gt;At some point in the future, phones will likely change or be replaced in some way that actually allows them to do something that they can&apos;t currently, but I don&apos;t see that happening any time soon. It&apos;s not folding phones, and it&apos;s definitely not an &quot;AI&quot; wearable computer pin.&lt;/p&gt;
&lt;p&gt;Until then, at 5 years old my phone isn&apos;t feeling old at all.&lt;/p&gt;
</content:encoded></item><item><title>Prototyping and small projects are fun!</title><link>https://mck.is/blog/2024/small-projects-for-fun/</link><guid isPermaLink="true">https://mck.is/blog/2024/small-projects-for-fun/</guid><description>They&apos;re also a good excuse to try out new things</description><pubDate>Fri, 01 Mar 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I recently had an idea for a project: I wonder if it&apos;s possible for me to display what music I&apos;m currently listening to on my website?&lt;br /&gt;
(Spoiler: it is! Although it might just say &quot;Currently offline&quot; depending on when you&apos;re reading this)&lt;/p&gt;
&lt;p&gt;&amp;lt;p class=&quot;music-display-container&quot;&amp;gt;
&amp;lt;music-display nowPlayingApi=&quot;https://music-display.mck.is/now-playing&quot; websocketUrl=&quot;wss://music-display.mck.is/now-playing-ws&quot;&amp;gt;
&amp;lt;/music-display&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;script src=&quot;https://music-display.mck.is/musicDisplayComponent.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;Why? It sounded like a nice, short project, I think it&apos;s fun showing off what music I like, and I&apos;ve had it displayed as my discord status for a while now, which gets friends talking about music!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/hey-i-know-that-song.BTxIR-fM_Z1wg66v.webp&quot; alt=&quot;hey i know that song&quot; /&gt;&lt;/p&gt;
&lt;p&gt;So I started thinking over the minimum parts I&apos;d need to get working:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Something on my website to display the currently playing song&lt;/li&gt;
&lt;li&gt;A server of some sort for me to send what music I&apos;m listening to to&lt;/li&gt;
&lt;li&gt;Some way to get or send what music I&apos;m playing&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I already knew MusicBee (the music player I normally use on desktop) had some sort of plugin system, since it&apos;s already how I was sharing what I was listening to on Discord - presumably I should be able to do something similar, just sending the data to my website instead?&lt;/p&gt;
&lt;p&gt;Thankfully the &lt;a href=&quot;https://www.getmusicbee.com/help/api/&quot;&gt;MusicBee website&lt;/a&gt; has at least some documentation on the plugin API, the requirements for it (targeting .NET Framework 4.0), and an interface for all the methods it supports.&lt;br /&gt;
Mildly annoying for me that it&apos;s .NET Framework (the version that only runs on windows) as I usually use linux, but I can easily switch to windows for compiling and testing the plugin, it should still run fine through wine later on. (Also getting to use more C# was nice, I&apos;ve been enjoying it a lot recently!)&lt;/p&gt;
&lt;p&gt;Some quick browsing through the provided &lt;code&gt;MusicBeeApiInterface&lt;/code&gt; and DiscordBee&apos;s source code, I found the methods I&apos;d need for getting info about the currently playing song, and how to received notifications when the song changes - it was all pretty nice to use! It also provided the album art as a base64 encoded string, which made it even easier for me to test sending stuff to the server.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var playingData = new
{
	Artist = _mbApiInterface.NowPlaying_GetFileTag(MetaDataType.Artist),
	Title = _mbApiInterface.NowPlaying_GetFileTag(MetaDataType.TrackTitle),
	Album = _mbApiInterface.NowPlaying_GetFileTag(MetaDataType.Album),
	DurationMs = _mbApiInterface.NowPlaying_GetDuration(),
	PositionMs = _mbApiInterface.Player_GetPosition(),
	PlayState = _mbApiInterface.Player_GetPlayState(),
	AlbumArt = _mbApiInterface.NowPlaying_GetArtwork()
};

using var client = new System.Net.WebClient();
client.UploadString(&quot;http://localhost:3000&quot;, &quot;POST&quot;, JsonConvert.SerializeObject(playingData));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For the server, I decided to just quickly throw it together using &lt;a href=&quot;https://bun.sh&quot;&gt;Bun&lt;/a&gt;, since I&apos;ve found javascript(/typescript) to be perfect for this scale of project. (I chose Bun over NodeJS only because I wanted to try out its &lt;code&gt;Bun.serve&lt;/code&gt; API, I really hope this server will never be under enough load for any performance differences to matter)&lt;br /&gt;
To test that the idea worked at all, I told it to just log anything it received.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Bun.serve({
	port: 3000,
	async fetch(req) {
		const body = (await req.json()) as PlayingData;

		console.table({
			...body,
			playState: PlayState[body.playState], // number -&amp;gt; string
			albumArt: &quot;base64 encoded&quot;, // way too long to log
		});

		const buffer = Buffer.from(body.albumArt, &quot;base64&quot;);
		Bun.write(&quot;albumArt.jpg&quot;, buffer);

		return new Response();
	},
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;p&amp;gt;
&amp;lt;video controls&amp;gt;
&amp;lt;source src=&quot;/blog/initial-music-prototype.mp4&quot;&amp;gt;&amp;lt;/source&amp;gt;
&amp;lt;/video&amp;gt;
&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;We&apos;ve proven the concept!&lt;/p&gt;
&lt;p&gt;But I&apos;m still missing some way to display this on my website. There&apos;s so many different toolkits/frameworks/etc. I could pick do this with (svelte, react, htmx, vue, angular, solid, etc, etc.), but to get started I&apos;ll just use vanilla JS and CSS, to fetch the data from the server, and update the DOM with it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;setInterval(() =&amp;gt; {
	const response = await fetch(&quot;http://localhost:3000/now-playing&quot;);
	const playingData = await response.json();

	document.getElementById(&quot;albumArt&quot;).src = `data:image/png;base64, ${playingData.albumArt}`;

	document.getElementById(&quot;songTitle&quot;).innerText = playingData.title;
	document.getElementById(&quot;album&quot;).innerText = ` - ${playingData.album}`;

	document.getElementById(&quot;artist&quot;).innerText = playingData.artist;

	let position = document.getElementById(&quot;position&quot;);
	position.innerText = new Date(playingData.positionMs).toISOString().substr(14, 5);

	let duration = document.getElementById(&quot;duration&quot;);
	duration.innerText = new Date(playingData.durationMs).toISOString().substr(14, 5);

	let progressBar = document.getElementById(&quot;progressBar&quot;);
	progressBar.value = playingData.positionMs;
	progressBar.max = playingData.durationMs;
}, 10);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;p&amp;gt;
&amp;lt;video controls&amp;gt;
&amp;lt;source src=&quot;/blog/initial-music-web-ui.mp4&quot;&amp;gt;&amp;lt;/source&amp;gt;
&amp;lt;/video&amp;gt;
&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;Looking good!&lt;/p&gt;
&lt;p&gt;However this initial solution isn&apos;t great - polling the server every 10 milliseconds is more than a bit excessive, but I still want it to update quickly.&lt;br /&gt;
I&apos;d heard of websockets before but didn&apos;t know much about them - after a bit of reading (MDN is fantastic), they seemed pretty much perfect! I can just have the server send the data to the client whenever it changes, and the client can update the DOM then.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const webSocket = new WebSocket(&quot;ws://localhost:3000/now-playing-ws&quot;);

webSocket.onmessage = (event) =&amp;gt; {
	fullUpdate(JSON.parse(event.data));
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But this meant the progress indicator only updated when the song changed or I manually moved to a different part of the song. &lt;code&gt;setInterval&lt;/code&gt; again to position it would definitely have worked, but I felt like it&apos;d be more interesting to try to understand CSS animations a bit better and animate progress with it instead. After quite a bit of fiddling around, I figured out that setting a negative &lt;code&gt;animation-delay&lt;/code&gt; would allow me to position the marker at the currently correct position, and animate over time to the end of the song.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;seekBarPositionMarker.style.animation = `moveRight ${playingData.durationMs}ms linear -${currentPosition}ms forwards`;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Perfect! With a bit of extra polish, the whole thing looks pretty good.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/autumns-back-again.rCSBJkn4_18Enp5.webp&quot; alt=&quot;Autumn&apos;s Back Again by northh&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/movies-for-guys.RE0rtSud_2hJc2.webp&quot; alt=&quot;movies for guys by Jane Remover&quot; /&gt;&lt;/p&gt;
&lt;p&gt;I decided to try out CSS nesting for this, as of a couple months ago it&apos;s &lt;a href=&quot;https://caniuse.com/css-nesting&quot;&gt;supported in all major browsers&lt;/a&gt;. Which helps keep the CSS a bit more organised and readable:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#artContainer {
	position: relative;
	height: 100%;
	width: var(--albumArtSize);
	display: flex;
	justify-content: center;
	align-items: center;

	#pauseSymbol {
		width: var(--albumArtSize);
		height: var(--albumArtSize);
		filter: drop-shadow(5px 5px 2px var(--base));
		display: none;

		&amp;amp;.paused {
			display: block;
		}
	}

	#albumArt {
		width: var(--albumArtSize);
		max-height: var(--albumArtSize);
		border-radius: var(--border-radius);

		&amp;amp;.paused {
			filter: grayscale(70%) brightness(70%);
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CSS variables are great! I don&apos;t see them used often enough.&lt;/p&gt;
&lt;p&gt;And then a bit more work after I realised it should probably look good when squished down on mobile too:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/locals.EHCzOZQD_PubiJ.webp&quot; alt=&quot;locals (girls like us) by Underscores&quot; /&gt;&lt;/p&gt;
&lt;p&gt;CSS Grid turned out to be a great help for this - I&apos;ve never actually used it before, but now I have a decent understanding of how they work, and can&apos;t wait to use them more.&lt;/p&gt;
&lt;p&gt;&amp;lt;section&amp;gt;&lt;/p&gt;
&lt;p&gt;As a side-note, I found a tiny odd difference between Firefox and Chromium. I was using an SVG for the pause button, and decided to use some css variables to define how rounded the corners should be (&lt;code&gt;rx=&quot;calc(var(--border-radius) / 2)&quot;&lt;/code&gt;).&lt;br /&gt;
Firefox logs a warning saying &lt;code&gt;Unexpected value&lt;/code&gt; but still renders it the way I intended:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/pause-firefox.Bm6AFDW9_Sf2r3.webp&quot; alt=&quot;Firefox&apos;s pause button with rounded corners&quot; /&gt;&lt;/p&gt;
&lt;p&gt;While Chromium logs an error and renders it with sharp corners:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/pause-chromium.CRgXX5s-_Z2fHt1b.webp&quot; alt=&quot;Chromium&apos;s pause button with sharp corners&quot; /&gt;&lt;/p&gt;
&lt;p&gt;I have no idea which one is &quot;correct&quot; (or if there even is an answer to that), but I found it odd since they both support CSS variables for the &lt;code&gt;fill&lt;/code&gt; without any issues.&lt;/p&gt;
&lt;p&gt;&amp;lt;/section&amp;gt;&lt;/p&gt;
&lt;p&gt;I still needed to host this server I&apos;d made somewhere - I decided rather than tying myself to a specific cloud provider and potentially dealing with issues from that in the future, I&apos;d just host it on the cheapest Hetzner cloud instance. (I&apos;ve been using their servers for a couple of years now an not had any issues yet, and their stuff is actually reasonably priced)&lt;/p&gt;
&lt;p&gt;I also decided to use this as a chance to try out NixOS on a server, and use Caddy instead of Nginx for the reverse proxy. I honestly can&apos;t believe how simple this was to set up compared to anything I&apos;ve done with Nginx in the past, it was just a few lines in my &lt;code&gt;configuration.nix&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services.caddy = {
	enable = true;
	email = &quot;...&quot;;
	virtualHosts.&quot;music-display.mck.is&quot; = {
		extraConfig = &apos;&apos;
			reverse_proxy localhost:3000
		&apos;&apos;;
	};
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(And then wondering why it wasn&apos;t working for a few minutes before realising I&apos;d forgotten about DNS)&lt;/p&gt;
&lt;p&gt;The final thing I wanted to do was to make it so I could easily add this to any page on my website, ideally without having to copy and paste a bunch of JS and CSS around, or having to worry about how to keep the display on my website when I decide to change my website&apos;s tech stack in a few years. I landed on using a web component for this - I&apos;d used them a little before and even though I feel like their implementation could be quite a bit better, I still really love the idea of them.
This means all I need to do to add it to any page is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;music-display
	nowPlayingApi=&quot;https://music-display.mck.is/now-playing&quot;
	websocketUrl=&quot;wss://music-display.mck.is/now-playing-ws&quot;&amp;gt;
&amp;lt;/music-display&amp;gt;

&amp;lt;script src=&quot;https://music-display.mck.is/musicDisplayComponent.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;style is:global&amp;gt;
music-display {
display: flex;
--base: #181926;
}&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.music-display-container {
	display: flex;
	justify-content: center;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/style&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;p class=&quot;music-display-container&quot;&amp;gt;
&amp;lt;music-display nowPlayingApi=&quot;https://music-display.mck.is/now-playing&quot; websocketUrl=&quot;wss://music-display.mck.is/now-playing-ws&quot;&amp;gt;
&amp;lt;/music-display&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;script src=&quot;https://music-display.mck.is/musicDisplayComponent.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;And we&apos;re done!&lt;/p&gt;
&lt;p&gt;Both the &lt;a href=&quot;https://github.com/autumn-mck/MusicDisplayMusicBeePlugin&quot;&gt;MusicBee plugin&lt;/a&gt; and &lt;a href=&quot;https://github.com/autumn-mck/MusicDisplayServer&quot;&gt;server + web component&lt;/a&gt; are open source, so feel free to take a look! You can see some of the stuff I skipped over here, like resizing the album art so I&apos;m not trying to POST megabytes of JSON, or adding authentication to the server. You could even host it yourself, if you wanted to for some reason? (I&apos;m not really sure why I did this, so I&apos;m not sure why you would either)&lt;/p&gt;
&lt;p&gt;I&apos;m really happy with how this whole project turned out! It went extremely (and unusually) smoothly, I managed to learn quite a bit anyway, and had fun putting the whole thing together. I also have some ideas for doing more with this in the future, but I&apos;m also happy calling it &quot;done&quot; in its current state.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update 2024-03-07:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I&apos;ve also made a &lt;a href=&quot;https://powerampapp.com/&quot;&gt;Poweramp&lt;/a&gt; plugin to send the same information to the server, so it also displays what I&apos;m listening to on my phone! (&lt;a href=&quot;https://github.com/autumn-mck/MusicDisplayPowerampPlugin&quot;&gt;Poweramp plugin code&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;&amp;lt;p&amp;gt;
&amp;lt;video controls&amp;gt;
&amp;lt;source src=&quot;/blog/mobile.mp4&quot;&amp;gt;&amp;lt;/source&amp;gt;
&amp;lt;/video&amp;gt;
&amp;lt;/p&amp;gt;&lt;/p&gt;
</content:encoded></item><item><title>Minecraft is and isn&apos;t</title><link>https://mck.is/blog/2024/minecraft/</link><guid isPermaLink="true">https://mck.is/blog/2024/minecraft/</guid><description>A game I&apos;ve been playing for about a decade now</description><pubDate>Fri, 02 Feb 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;You probably know what Minecraft is, right? That game with the blocks, a 3D sandbox game of a world filled with voxels.&lt;/p&gt;
&lt;p&gt;But that&apos;s not really what it&apos;s about, to me at least.&lt;/p&gt;
&lt;p&gt;It&apos;s not about punching a tree to get wood. It&apos;s about going over to a friend&apos;s house, where you get to play Minecraft splitscreen on their Xbox 360, while they teach you how to play, since you don&apos;t even know what most of the buttons do yet.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/mcpe.Dh97wiYp_gGtBF.webp&quot; alt=&quot;A pocket edition world, with tall, steep cliffs on both sides of a valley filled with water. The distance is extremely foggy.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;It&apos;s about getting pocket edition, exploring its weird, wonderful world generation, building a house, then going to school to talk about it the next day.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/Chickens.s1zklSnZ_2ezU1h.webp&quot; alt=&quot;The ground is almost completely covered with hundreds of chickens. Some houses are visible in the background.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;It&apos;s not about building. It&apos;s about attempting to build a chicken farm, making a mistake, exploding hundreds of chickens everywhere, laughing about it with friends for several minutes before they help you clean it up, and chickens becoming an in-joke between you.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/Rain.Dv3t8AGV_1tQqy8.webp&quot; alt=&quot;Somebody building a house on the edge of a cliff. It&apos;s raining, dark, and foggy.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;It&apos;s not about surviving. It&apos;s about helping a friend build a house on the edge of a cliff despite the darkness, and rain pouring down around you, working together to try to survive.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/Ocean.BB5oLWYU_Z2niKPi.webp&quot; alt=&quot;The sun rising on an empty ocean. Some land is visible on the left.&quot; /&gt;&lt;/p&gt;
&lt;p&gt;It&apos;s not about exploration. It&apos;s about leaving that friend behind to explore a vast and isolating ocean, a feeling only made real by other people being in the same world, but not being with you.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/DragonSlayer.PkYGR9Mx_1mMIMC.webp&quot; alt=&quot;An item titled &amp;quot;Dragon Slayer&amp;quot; - Obtained by participating in the Dragon Fight Event, 18th January 2024&quot; /&gt;&lt;/p&gt;
&lt;p&gt;It&apos;s not about beating the ender dragon. It&apos;s about Queen&apos;s Computing Society having their own Minecraft server, and putting on an event for beating the ender dragon and an extra challenge for everybody taking part.&lt;/p&gt;
&lt;p&gt;I&apos;ve played Minecraft for thousands of hours now, but only a tiny percent of that has ever been in single-player worlds.&lt;/p&gt;
&lt;p&gt;It&apos;s not about Minecraft. Minecraft could have been anything. It&apos;s about friends, and the time we spent together.&lt;/p&gt;
</content:encoded></item><item><title>MusicBee on Linux</title><link>https://mck.is/blog/2024/musicbee-on-linux/</link><guid isPermaLink="true">https://mck.is/blog/2024/musicbee-on-linux/</guid><description>Wine is magic!</description><pubDate>Fri, 26 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;If you&apos;re just wanting a script to install musicbee, see this &lt;a href=&quot;https://gist.github.com/autumn-mck/6d7fcbbc08f5d18be09f2cc219084675&quot;&gt;installation script&lt;/a&gt;!&lt;/strong&gt;. It will likely be more up-to-date than this blog post is. Be careful when running scripts you find on the internet.&lt;/p&gt;
&lt;p&gt;When I was using Windows, &lt;a href=&quot;https://getmusicbee.com/&quot;&gt;MusicBee&lt;/a&gt; was easily my favourite music player - for its customizability, how feature-rich it felt, and for me to as easily as possible play music the way I want to play it (either by artist or by album).&lt;/p&gt;
&lt;p&gt;But when I switched to linux and tried out a range of music players that support it (&lt;a href=&quot;https://wiki.archlinux.org/title/List_of_applications#Graphical_13&quot;&gt;and I tried a lot of them&lt;/a&gt;), I ended up feeling disappointed in various ways by each of them. So what could I do to get MusicBee working on linux? And how much can I automate its installation?&lt;/p&gt;
&lt;p&gt;Some quick searching brought up this &lt;a href=&quot;https://getmusicbee.com/forum/index.php?topic=30205.0&quot;&gt;thread on the MusicBee forum&lt;/a&gt;. Perfect! So it looks like all we need to install to set up the &lt;a href=&quot;https://wiki.archlinux.org/title/Wine#WINEPREFIX&quot;&gt;wine prefix&lt;/a&gt; is &lt;code&gt;dotnet48&lt;/code&gt;, &lt;code&gt;xmllite&lt;/code&gt;, and &lt;code&gt;gdiplus&lt;/code&gt;. This could be done through &lt;a href=&quot;https://wiki.winehq.org/Winetricks&quot;&gt;winetricks&lt;/a&gt;&apos; GUI, but I&apos;m looking to automate this! (After all, it&apos;s definitely worth spending an hour now to save one minute of clicking buttons in the future).&lt;/p&gt;
&lt;p&gt;So to automate it, we&apos;ll export a couple of environment variables: Normally wineboot prompts us to install wine-mono, but we don&apos;t seem to need it for MusicBee, so we&apos;ll disable that too.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export WINEPREFIX=&quot;$HOME/.local/share/wineprefixes/MusicBee/&quot;
export WINEDLLOVERRIDES=&quot;mscoree,mshtml=&quot; # We don&apos;t need wine-mono installed, no need to give a warning over it. https://bugs.winehq.org/show_bug.cgi?id=47316#c4
export WINEDEBUG=-all # Don&apos;t print any debugging messages for wine

winetricks --unattended dotnet48 xmllite gdiplus
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note: If for some reason you&apos;re like me and want to compile a &lt;a href=&quot;https://github.com/catppuccin/musicbee&quot;&gt;MusicBee skin&lt;/a&gt;, this wine prefix is able to run the required &lt;code&gt;SkinCreator.exe&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Now we can download and run the MusicBee installer ourselves, but I was having fun automating stuff! MusicBee has two official download hosts: mega.nz and MajorGeeks. It looks like it could be downloaded from mega.nz by using &lt;a href=&quot;https://megatools.megous.com/&quot;&gt;Megatools&lt;/a&gt;, but I don&apos;t feel like having to install that just for installing MusicBee. So can I do anything with MajorGeeks?&lt;/p&gt;
&lt;p&gt;By looking at Firefox&apos;s network request inspector, it looks like the site first sends a request to &lt;code&gt;https://www.majorgeeks.com/index.php?ct=files&amp;amp;action=download&amp;amp;=&lt;/code&gt;, which responds with a 302 (Moved temporarily) redirecting us to the actual download file. Cool! After a bit of experimentation, I was able to figure out that it figures out where to redirect us to based on the &lt;code&gt;PHPSESSID&lt;/code&gt; cookie.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/redirect.C-sLyByu_2prCQV.webp&quot; alt=&quot;A list of GET requests, showing the one that fetches the file at the bottom&quot; /&gt;&lt;/p&gt;
&lt;p&gt;But how do we get that from the command line? Thankfully since this cookie is created and used by PHP on the server-side, we don&apos;t have to worry about running javascript or something. After some online searching and looking through cURL&apos;s manual page, it looks like the options we need are &lt;code&gt;-c&lt;/code&gt; to save the cookies, and we can then use &lt;code&gt;-b&lt;/code&gt; to load the file for our next request.&lt;/p&gt;
&lt;p&gt;This gets us a zip file though, so we can pipe it to &lt;code&gt;funzip&lt;/code&gt; to quickly extract the first file (In this case the only file, the installer), and write it to a temporary location.&lt;/p&gt;
&lt;p&gt;Finally, we&apos;ve got our installer! But if we just try to run it now, it (fairly unsurprisingly) just brings up the graphical installer. I want to automate this!&lt;/p&gt;
&lt;p&gt;I&apos;m not sure what MusicBee&apos;s installer is created with (possibly NSIS?), but after some searching to see what the common options were, I found that the &quot;/S&quot; flag makes it install silently (i.e. without bringing up a GUI to ask us to confirm stuff, and installs just with the default options).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mb_temp_file=$(mktemp --tmpdir MusicBee-XXXXXX.exe) # Create a temporary file to download musicbee to
curl -c ./MG_MB_cookie.txt https://www.majorgeeks.com/mg/getmirror/musicbee,1.html
curl -L -b ./MG_MB_cookie.txt &apos;https://www.majorgeeks.com/index.php?ct=files&amp;amp;action=download&amp;amp;=&apos; | funzip &amp;gt; &quot;$mb_temp_file&quot; # Download the zip file containing the installer, pipe it through funzip to unzip it, and write it to the temp file
rm ./MG_MB_cookie.txt
wine &quot;$mb_temp_file&quot; &quot;/S&quot; # Assumes variables are set as above
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So are we done? MusicBee seems to launch, but if I try to play &lt;code&gt;.m4a&lt;/code&gt; files (which music from the iTunes store seems to use), MusicBee crashes? Looks like I got too far ahead of myself - if I&apos;d read further down the forum thread, I&apos;d see the creator of MusicBee saying it probably needed &lt;code&gt;bass_aac.dll&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;From &lt;a href=&quot;https://getmusicbee.com/forum/index.php?topic=23454.0&quot;&gt;a different forum post&lt;/a&gt;, it looks like we can download &lt;code&gt;bass_aac.dll&lt;/code&gt; from un4seen.com, then move it into the MusicBee installation folder.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bass_aac_download_url=&quot;https://www.un4seen.com/files/z/2/bass_aac24.zip&quot;
installation_location=&quot;$WINEPREFIX/drive_c/Program Files/MusicBee/&quot;

curl -L -o bass_aac.zip $bass_aac_download_url
unzip -o bass_aac.zip bass_aac.dll
rm &quot;./bass_aac.zip&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And that&apos;s it! MusicBee now works.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/broken_cjk.CRWcMgHX_17ERVw.webp&quot; alt=&quot;Screenshot showing empty boxes being displayed in place of japanese characters&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Except of course not. One of the artists of an album I own uses japanese characters, which seems to be displayed as just boxes. Installing &lt;code&gt;cjkfonts&lt;/code&gt; (fonts with chinese, japanese, and korean characters) with winetricks seems to &lt;em&gt;partially&lt;/em&gt; fix the issue - except places where the text is bolded, where it still just displays boxes. I&apos;ve not been able to find a better solution yet unfortunately, although I only discovered this issue a few weeks ago, so I&apos;m still searching.&lt;/p&gt;
&lt;p&gt;Also as a note, if you want to scrobble to an external service, but are getting an error saying &quot;Cannot connect to LastFM&quot; or similar, try installing &lt;code&gt;lib32-gnutls&lt;/code&gt; or whatever it&apos;s named for your distro (I encountered this with Arch, but it doesn&apos;t seem to be an issue on NixOS)&lt;/p&gt;
&lt;p&gt;Ok, now it actually works!&lt;/p&gt;
&lt;h2&gt;But what if we want to go further?&lt;/h2&gt;
&lt;p&gt;There&apos;s no reason to, but what if we do anyway?&lt;/p&gt;
&lt;p&gt;First, let&apos;s try to integrate it better with our desktop environment. Wine automatically creates an &lt;a href=&quot;https://wiki.archlinux.org/title/Desktop_entries&quot;&gt;XDG Desktop file&lt;/a&gt; for us, but we can do marginally better.&lt;/p&gt;
&lt;p&gt;Wine&apos;s attempt looks something like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Desktop Entry]
Name=MusicBee
Exec=env WINEPREFIX=&quot;[prefix here]&quot; wine C:\\\\path\\\\to\\\\application.lnk
Type=Application
StartupNotify=true
Path=/path/to/exe
Icon=737E_MusicBee.0
StartupWMClass=musicbee.exe
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Reading through the &lt;a href=&quot;https://specifications.freedesktop.org/desktop-entry-spec/latest/&quot;&gt;XDG Desktop entry specification&lt;/a&gt;, it looks like we could improve a few things. By default, MusicBee is just listed under the category of &quot;Wine&quot; - from the available categories, I feel like AudioVideo, Audio, Player, and Music fit better. By default it also doesn&apos;t know that MusicBee can be used to open music files, eg from a file manager - I randomly picked a couple of common audio formats to associate with it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cat &amp;gt; &quot;./MusicBee.desktop&quot; &amp;lt;&amp;lt; EOL
[Desktop Entry]
Name=MusicBee
Type=Application
StartupNotify=true
Version=1.5
Comment=The Ultimate Music Manager and Player
Icon=737E_MusicBee.0
Categories=AudioVideo;Audio;Player;Music;
MimeType=audio/mp4;audio/mpeg;audio/aac;audio/flac;audio/ogg;
Exec=env WINEPREFIX=&quot;$WINEPREFIX&quot; installation_location=&quot;$installation_location&quot; &quot;$installation_location/launch.sh&quot; &quot;%f&quot;
StartupWMClass=musicbee.exe
SingleMainWindow=true
EOL

desktop-file-install --dir=&quot;$HOME/.local/share/applications/&quot; --rebuild-mime-info-cache &quot;./MusicBee.desktop&quot;

# Remove the XDG Desktop entry generated by wine (ours is better)
rm -r &quot;$HOME/.local/share/applications/wine/Programs/MusicBee/&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Trying to play a file from the file manager doesn&apos;t seem to work by default though :(&lt;br /&gt;
MusicBee does &lt;a href=&quot;https://breezewiki.com/musicbee/wiki/Command_Line_Parameters&quot;&gt;support specifying the file as a command line argument&lt;/a&gt;, so let&apos;s see what we can do with that! Just giving it the default linux file path doesn&apos;t seem to work - not too surprising, it&apos;s probably only expecting something in the Windows format. So we&apos;ll add a little script to rewrite that for us! All that&apos;s needed is to replace the &apos;/&apos;s with &apos;\&apos;s, and say it&apos;s on the Z: drive (What wine uses to represent the linux system&apos;s folders).&lt;/p&gt;
&lt;p&gt;This is the &lt;code&gt;launch.sh&lt;/code&gt; file used for the desktop file above:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cat &amp;gt; &quot;./launch.sh&quot; &amp;lt;&amp;lt; EOL
#!/usr/bin/env sh
file=\$(echo \$1 | tr &apos;/&apos; &apos;\\\\&apos;) # Replace / with \\
wine &quot;\$installation_location/MusicBee.exe&quot; &quot;/Play&quot; &quot;Z:\$file&quot;
EOL
chmod +x ./launch.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And everything works! Perfect.&lt;/p&gt;
&lt;h2&gt;But what if we want to go even further?&lt;/h2&gt;
&lt;p&gt;Why? Because it&apos;s fun.&lt;/p&gt;
&lt;p&gt;MusicBee supports addons, for example &lt;a href=&quot;https://github.com/sll552/DiscordBee&quot;&gt;DiscordBee&lt;/a&gt;. What do we need to do to get it working? First let&apos;s just copy it to MusicBee&apos;s plugin folder, which would be enough on windows.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd ./Plugins
curl -o DiscordBee.zip -L https://github.com/sll552/DiscordBee/releases/download/v3.1.0/DiscordBee-Release-v3.1.0.zip
unzip -o ./DiscordBee.zip
rm ./DiscordBee.zip
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This isn&apos;t enough unfortunately - Discord&apos;s own desktop linux client, nor any of the better alternative desktop clients, seem to pick up on stuff through wine (I don&apos;t know if it&apos;s doable or not, I&apos;m tempted to look into it and see).&lt;/p&gt;
&lt;p&gt;But luckily I found &lt;a href=&quot;https://github.com/0e4ef622/wine-discord-ipc-bridge&quot;&gt;wine-discord-ipc-bridge&lt;/a&gt;, which does what it says in the name! All we need to do is run it in the same wine prefix as musicbee, which is pretty easy to modify the last line of our above &lt;code&gt;launch.sh&lt;/code&gt; to do.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cat &amp;gt; &quot;./launch.sh&quot; &amp;lt;&amp;lt; EOL
#!/usr/bin/env sh
file=\$(echo \$1 | tr &apos;/&apos; &apos;\\\\&apos;) # Replace / with \\
wine &quot;\$installation_location/winediscordipcbridge.exe&quot; &amp;amp; wine &quot;\$installation_location/MusicBee.exe&quot; &quot;/Play&quot; &quot;Z:\$file&quot;
EOL
chmod +x ./launch.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/discordbee.oGezmpmf_LTaGw.webp&quot; alt=&quot;Screenshot with a discord status showing the same track currently playing within MusicBee, with its album art and current progress&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Ok, we&apos;re finally done (for now)&lt;/h2&gt;
&lt;p&gt;You can see the &lt;a href=&quot;https://gist.github.com/autumn-mck/6d7fcbbc08f5d18be09f2cc219084675&quot;&gt;script for installing MusicBee, the DLLs to play &lt;code&gt;.m4a&lt;/code&gt; files, and an improved desktop file&lt;/a&gt;, or a &lt;a href=&quot;https://gist.github.com/autumn-mck/ef1fba379cb2429083cf76369d0b032a&quot;&gt;script that does that, plus install DiscordBee, a couple of skins, and downloads the settings I use&lt;/a&gt; - the latter isn&apos;t really useful to anybody other than me, but it&apos;s available if you want to customise it yourself!&lt;/p&gt;
&lt;p&gt;I&apos;m hardly the first person to do most of this stuff, but it was still a lot of fun! I enjoy doing things with computers and software that weren&apos;t exactly originally intended, and figuring out how I can solve problems.&lt;/p&gt;
&lt;p&gt;I&apos;ve heard that even ripping CDs works, but given that &lt;a href=&quot;https://www.bbc.co.uk/news/newsbeat-33566933&quot;&gt;it&apos;s apparently not legal in the UK&lt;/a&gt;, I of course would never do so myself.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/cd.J3MSfO1D_bCie8.webp&quot; alt=&quot;Screenshot of the MusicBee CD ripping user interface&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Wine may not be an emulator, but as far as I&apos;m concerned, it is magic.&lt;/p&gt;
</content:encoded></item><item><title>New year, new site!</title><link>https://mck.is/blog/2024/new-site/</link><guid isPermaLink="true">https://mck.is/blog/2024/new-site/</guid><description>Almost two years, actually!</description><pubDate>Sat, 06 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Whoops, hi again! It&apos;s been a bit longer than I meant it to be since my last update to this site or blog post, and I&apos;m here to fix that.&lt;/p&gt;
&lt;h2&gt;New site&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/new-site.CGOe73bG_Z290J4G.webp&quot; alt=&quot;Screenshot of the main page of the current site design&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The new site is built with &lt;a href=&quot;https://astro.build/&quot;&gt;Astro&lt;/a&gt;, and... that&apos;s pretty much it actually! I enjoy keeping my site pretty minimal, and the extra flexibility writing my own CSS gives me.&lt;br /&gt;
Why astro?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It works well as a static site generator (I don&apos;t need to worry much about hosting for now when it&apos;s just a bunch of static files when built)&lt;/li&gt;
&lt;li&gt;It allows me to finally try something with more JSX-like syntax (not sure how I managed to avoid it for this long)&lt;/li&gt;
&lt;li&gt;It&apos;s flexible enough that I feel like I can do exactly I want with it, without me having to write everything from scratch&lt;/li&gt;
&lt;li&gt;It&apos;s powerful enough that I know I&apos;ll be able to extend it in future if I feel like making something more dynamic&lt;/li&gt;
&lt;li&gt;Takes care of making sure most images are optimised well enough&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It also comes with some other nice features like syntax highlighting:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var text = File.ReadAllLines(&quot;input.txt&quot;);

// Advent of Code 2023 Day 6
// https://adventofcode.com/2023/day/6
// Part 1
var totalTimes = ReadAsSeparateNumbersFromLine(text[0]);
var distancesToBeat = ReadAsSeparateNumbersFromLine(text[1]);

var marginOfError = 1;
for (var race = 0; race &amp;lt; totalTimes.Length; race++)
{
    marginOfError *= MarginOfError((int)totalTimes[race], distancesToBeat[race]);
}
Console.WriteLine($&quot;Part 1: {marginOfError}&quot;);

return;

long[] ReadAsSeparateNumbersFromLine(string line)
{
    return line
        .Split(&apos; &apos;, StringSplitOptions.RemoveEmptyEntries)
        [1..]
        .Select(long.Parse)
        .ToArray();
}

int MarginOfError(int timeAvailable, long distanceToBeat)
{
    return Enumerable
        .Range(0, timeAvailable)
        .Select(guess =&amp;gt; CalcDistanceTravelled(guess, timeAvailable))
        .Count(distance =&amp;gt; distance &amp;gt;= distanceToBeat);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A lot of this is also standard in other static site generators, but compared to my own previous custom solution, it&apos;s a whole lot of features!&lt;/p&gt;
&lt;p&gt;I also took this as an opportunity to redo quite a bit of the CSS (e.g. &lt;a href=&quot;/blog&quot;&gt;/blog&lt;/a&gt; now looks significantly nicer), but I kept the bits of design I still like. (I actually think the old design still looks good! But for some parts I felt like doing something new)&lt;/p&gt;
&lt;p&gt;I&apos;ve also got a couple of blog posts planned out, so hopefully I&apos;ll get around to writing those! They&apos;ll be a mixture of stuff, nothing as technical or in-depth as my CSC1028 posts though (yet) - more like my FTL one, either in theme or rambliness.&lt;/p&gt;
&lt;p&gt;Finally, I&apos;ll be keeping a log of changes to the site at &lt;a href=&quot;/changelog&quot;&gt;/changelog&lt;/a&gt; - I like looking back on how stuff like a personal site can change over time!&lt;/p&gt;
</content:encoded></item><item><title>Blog post for CSC1028</title><link>https://mck.is/CSC1028/</link><guid isPermaLink="true">https://mck.is/CSC1028/</guid><description>A summary of the project</description><pubDate>Fri, 04 Mar 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;URL Understanding Tool&lt;/h1&gt;
&lt;p&gt;This project was created over the course of 10 weeks for my CSC1028 module, and although it is not a fully complete project, it provides a framework for future work.&lt;/p&gt;
&lt;h3&gt;Aims of the project&lt;/h3&gt;
&lt;p&gt;This project aims to be a tool for cybersecurity or power users to provide as much relevant metadata on a given URL as possible. Although there are only currently a few sources of data, the application is set up to be as easy as possible to add sources to.&lt;/p&gt;
&lt;p&gt;The project has 3 main parts:&lt;/p&gt;
&lt;h2&gt;HTTP APIs&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/autumn-mck/CSC1028APIs&quot;&gt;Code&lt;/a&gt;&lt;br /&gt;
The main component of this project is a set of HTTP APIs that can be queried for information on a URL/IP address to provide information from various sources, from local databases to external APIs.
The current data sources are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A local MongoDB database containing data from Project Sonar&lt;/li&gt;
&lt;li&gt;A local MongoDB database containing data on phishing/malware URLs from &lt;a href=&quot;https://phishtank.org/&quot;&gt;Phishtank&lt;/a&gt;, &lt;a href=&quot;https://openphish.com/&quot;&gt;OpenPhish&lt;/a&gt;, &lt;a href=&quot;https://urlhaus.abuse.ch/&quot;&gt;URLHaus&lt;/a&gt; and &lt;a href=&quot;https://malwarediscoverer.com/&quot;&gt;MalwareDiscoverer&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Earliest page/hostname archive date, from &lt;a href=&quot;https://archive.org&quot;&gt;https://archive.org&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.similarweb.com/&quot;&gt;Similarweb&lt;/a&gt; global website rank&lt;/li&gt;
&lt;li&gt;IP Geolocation data (Currently from &lt;a href=&quot;https://ip-api.com/&quot;&gt;https://ip-api.com/&lt;/a&gt;, could probably be improved - this section did not have much thought put into it, and was mostly done as a proof of concept)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://stackshare.io/&quot;&gt;Stackshare&lt;/a&gt; data&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Several of these can also be queried via the command line, i.e. &lt;code&gt;node queryArchiveDate.js example.com&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;For more information on dealing with Project Sonar&apos;s data, see &lt;a href=&quot;/project-sonar/&quot;&gt;my how-to guide&lt;/a&gt;, but in summary, the data is stored in a local MongoDB database which, when full, can fill up to 60gb. We then use &lt;a href=&quot;https://docs.mongodb.com/manual/core/index-text/&quot;&gt;text indexes&lt;/a&gt; to allow &lt;em&gt;extremely&lt;/em&gt; performant queries to be made.&lt;/p&gt;
&lt;p&gt;Note on Project Sonar&apos;s data:
6 days after I wrote my how-to guide, &lt;a href=&quot;https://www.rapid7.com/blog/post/2022/02/10/evolving-how-we-share-rapid7-research-data-2/&quot;&gt;Rapid7 switched to requiring you to apply&lt;/a&gt; to access Project Sonar&apos;s data. Except now, a few weeks later, it no longer requires an account again, and this time I cannot find any blog post etc. mentioning this change back, so I do not know if this is a permanent or temporary change.
Update on 28/03/22: This appears to be a permanent change. See &lt;a href=&quot;https://opendata.rapid7.com/about/&quot;&gt;https://opendata.rapid7.com/about/&lt;/a&gt; to apply for access.&lt;/p&gt;
&lt;h3&gt;Retrieving data&lt;/h3&gt;
&lt;p&gt;To retrieve the data used for the above HTTP APIs, some of the modules send a request to an external API, while some query a local MongoDB database. To fetch the data used to fill up the MongoDB database, there exists two programs: One for parsing and inserting Project Sonar&apos;s data, and one for fetching, parsing and inserting malware/phishing data.&lt;/p&gt;
&lt;h3&gt;Creating the HTTP APIs&lt;/h3&gt;
&lt;p&gt;To create and manage the HTTP APIs, there is a single program (&lt;code&gt;createAllAPI.js&lt;/code&gt;) that opens up all the APIs when run (Ports 10130 to 10135 by default). This program does almost nothing itself, and imports functionality from other modules to create the APIs (Notably &lt;code&gt;createHTTPServer.js&lt;/code&gt;, which will take any function and open up an API for it on the given port.). This approach allows new APIs to be added with ease, and allows you to manage which modules are started.&lt;/p&gt;
&lt;h3&gt;Running the application&lt;/h3&gt;
&lt;p&gt;For developing any of this project, you&apos;ll need a few things set up and installed. I&apos;d recommend following the setup process I used in &lt;a href=&quot;https://mck.is/project-sonar/#setup&quot;&gt;my how-to guide&lt;/a&gt;. You&apos;ll also want to install the dependencies listed in &lt;code&gt;package.json&lt;/code&gt; with &lt;code&gt;npm install &amp;lt;package_name&amp;gt;&lt;/code&gt;.
To actually get the data, you&apos;ll first want to run &lt;code&gt;./fetch/fetchMalwarePhishingData.js&lt;/code&gt; and &lt;code&gt;./fetch/fetchMalwarePhishingData.js&lt;/code&gt; (Assuming you&apos;ve downloaded Project Sonar&apos;s data in a similar way as I did in my &lt;a href=&quot;https://mck.is/project-sonar/#parsing-a-local-copy-of-project-sonar&quot;&gt;how-to guide&lt;/a&gt;).&lt;br /&gt;
You can then run &lt;code&gt;npm start&lt;/code&gt; to start the APIs (This command then calls &lt;code&gt;node ./create/createAllAPI.js&lt;/code&gt;, as specified in &lt;code&gt;package.json&lt;/code&gt;).&lt;/p&gt;
&lt;h3&gt;Testing plan&lt;/h3&gt;
&lt;p&gt;The easiest way to ensure the node.js APIs are working is to start the application by running &lt;code&gt;npm start&lt;/code&gt; and querying them in your browser. For example, to query the archive date API, which is hosted on port 10133, you&apos;d visit &lt;code&gt;http://localhost:10133/example.com&lt;/code&gt; .&lt;br /&gt;
&lt;img src=&quot;./DebuggingAPI.png&quot; alt=&quot;Example output from the API&quot; /&gt;&lt;/p&gt;
&lt;p&gt;I&apos;ve found that the best way to debug it is to make thorough use of &lt;code&gt;console.log(...);&lt;/code&gt; to make sure I know the state of variables over time, which is extremely useful in helping to detect any issues.&lt;/p&gt;
&lt;h3&gt;Further development&lt;/h3&gt;
&lt;p&gt;I&apos;ve tried to make adding additional functionality to the API as easy as possible. All APIs are set up in the &lt;code&gt;createAllAPI.js&lt;/code&gt; file, which itself contains very little code, and as a whole, the application is developed very modularly. As an example, we&apos;ll cover how querying similarweb works, as it has more requirements to get working than other functions. (You can view all the code for this &lt;a href=&quot;https://github.com/autumn-mck/CSC1028APIs/blob/master/query/querySimilarweb.js&quot;&gt;here&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;All query functions are stored in the &lt;code&gt;create&lt;/code&gt; folder, and our function for querying similarweb is stored in &lt;code&gt;querySimilarweb.js&lt;/code&gt;. At the top of the file, we begin by importing any other modules or functions we&apos;ll need.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import &quot;dotenv/config&quot;;
import getRemoteJSON from &quot;./queryRemoteJSON.js&quot;;
import parseHostname from &quot;../parse/parseHostname.js&quot;;
import createCli from &quot;../create/createCli.js&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;First of all, we&apos;re importing the &lt;code&gt;dotenv&lt;/code&gt; module, as querying similarweb requires an API key, which we&apos;ll store in a &lt;code&gt;.env&lt;/code&gt; file (More on that later). We&apos;re also using a couple of other functions that are defined in other files - &lt;code&gt;queryRemoteJSON.js&lt;/code&gt; fetches JSON from a URL and &lt;code&gt;parseHostname.js&lt;/code&gt; parses the string containing the URL into a URL object. &lt;code&gt;createCli.js&lt;/code&gt; is used for allowing the function to be used via the command line (i.e. &lt;code&gt;node ./query/querySimilarweb.js example.com&lt;/code&gt;), but this part of the application is just an optional extra that some of the functions provide, and isn&apos;t worth worrying much about.&lt;/p&gt;
&lt;p&gt;Next up, we want to create the function that will actually be used to make the query. Since we&apos;re planning to use this function later on in a different file, we&apos;ll also need to export the function using &lt;code&gt;export default&lt;/code&gt;. The function will also need to take in the URL that is being queried. We can then parse this into a URL object, which will allow us to select just the hostname, by using the &lt;code&gt;parseHostname&lt;/code&gt; function we imported earlier. Then we build the string for the API that we&apos;re querying, and we can use the &lt;code&gt;getRemoteJSON&lt;/code&gt; function we imported earlier to query the API, and we can return the result.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let parsed = parseHostname(url);
// Construct the URL to query
const fetchUrl = `https://api.similarweb.com/v1/similar-rank/${parsed.hostname}/rank?api_key=${process.env.SIMILARWEB_KEY}`;

// Get the result of the query
let res = await Promise.resolve(getRemoteJSON(fetchUrl));

// If an error occurred, return -1, otherwise return the rank
if (res.meta.status === &quot;Error&quot;) return -1;
else return res.similar_rank.rank;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, you might have noticed that nowhere in there are we creating our own queryable HTTP API or anything. So how does that happen?&lt;br /&gt;
This is where the advantage of the application&apos;s modularity comes in. To see this in action, we can go back to the &lt;code&gt;createAllAPI.js&lt;/code&gt; file.&lt;/p&gt;
&lt;p&gt;Again, at the top of the file, we&apos;re importing all the functions we need from other files (notably &lt;code&gt;import createHttpServer from &quot;./createHttpServer.js&quot;;&lt;/code&gt; and &lt;code&gt;import fetchSimilarwebRank from &quot;../query/querySimilarweb.js&quot;;&lt;/code&gt;). Then, in our main function, we can call the &lt;code&gt;createHttpServer&lt;/code&gt; function we&apos;ve just imported, and we pass it the port we want it to use, and the function we want to use. In this case we&apos;re using port 10131 (Picked because it is not used by any major applications, see &lt;a href=&quot;https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers&quot;&gt;https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers&lt;/a&gt;) and the &lt;code&gt;fetchSimilarwebRank&lt;/code&gt; function. This &lt;code&gt;createHttpServer.js&lt;/code&gt; allows us to add additional functionality on different ports with minimal effort, as it handles all of the networking side of the API, without this having to be re-architected for each additional function provided. You shouldn&apos;t even need to understand exactly what the &lt;code&gt;createHttpServer&lt;/code&gt; function does to be able to use it, but if you do, the code is well commented.&lt;/p&gt;
&lt;h3&gt;.env Files and API keys&lt;/h3&gt;
&lt;p&gt;Since I can&apos;t just share my API keys for anybody to use, the application makes use of a &lt;code&gt;.env&lt;/code&gt; file to store these. This allows these secret values to be stored in a file that is not publicly exposed.&lt;/p&gt;
&lt;p&gt;However, this means you will have to get your own API keys for the services that require them. Currently this is just similarweb and stackshare.&lt;br /&gt;
For getting a free similarweb API key (5000 requests per month), &lt;a href=&quot;https://support.similarweb.com/hc/en-us/articles/4414317910929-Website-DigitalRank-API#UUID-b25b8106-20c9-2d5a-e7b2-cdee63a4eaa6_section-idm4621956633339232800133052352&quot;&gt;see here&lt;/a&gt;&lt;br /&gt;
For getting a free stackshare API key (100 requests per month), &lt;a href=&quot;https://www.stackshare.io/api&quot;&gt;see here&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Once you&apos;ve got the API keys you want, you can then create a &lt;code&gt;.env&lt;/code&gt; file, using the provided &lt;code&gt;.env.template&lt;/code&gt; file as a template. The result should look something like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SIMILARWEB_KEY=abc1234
STACKSHARE_KEY=abcd
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Electron App&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./ElectronUI2.png&quot; alt=&quot;Electron app UI&quot; /&gt;&lt;br /&gt;
&lt;a href=&quot;https://github.com/autumn-mck/CSC1028ElectronApp&quot;&gt;Code&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The electron app provides a user-friendly interface allowing the user to make queries regarding any URL, and displays the data to the user in a better format than the entirely raw JSON, however further steps should be taken as the current presentation is still not easily readable.&lt;/p&gt;
&lt;p&gt;Since it is built with electron, the page is little more than a HTML page with some javascript behind it! As a result, all this app has to do is query the back-end HTTP APIs and display the result to the user!&lt;/p&gt;
&lt;h3&gt;Running the application&lt;/h3&gt;
&lt;p&gt;Assuming you&apos;ve followed the steps above for running/developing the central node.js app (Which you should have done, as this electron app isn&apos;t too useful without it), not much more is required to run the electron app. After opening the folder, you&apos;ll need to run &lt;code&gt;npm install --save-dev electron&lt;/code&gt; to install everything required for electron. You can then run &lt;code&gt;npm start&lt;/code&gt; to start the app.&lt;br /&gt;
You might also want to look at &lt;a href=&quot;https://www.electronjs.org/docs/latest/tutorial/quick-start/&quot;&gt;https://www.electronjs.org/docs/latest/tutorial/quick-start/&lt;/a&gt; for an introduction to Electron.&lt;/p&gt;
&lt;h3&gt;Further development&lt;/h3&gt;
&lt;p&gt;The electron app itself is thankfully not too complex.&lt;br /&gt;
First, there&apos;s the &lt;code&gt;main.js&lt;/code&gt; file, which is a node.js application that is used to launch the electron browser window itself, which is &lt;code&gt;index.html&lt;/code&gt;. This just works like a standard web page - the HTML is stored in &lt;code&gt;index.html&lt;/code&gt;, the CSS in &lt;code&gt;index.css&lt;/code&gt; (The CSS probably doesn&apos;t need to much editing - It&apos;s designed to work well with just plain HTML), and the javascript is in &lt;code&gt;renderer.js&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The javascript doesn&apos;t have to do too much in this case - it only needs to query the Node.js APIs created earlier, and display the results to the user. If you&apos;re looking for something to improve in the electron application, I&apos;d suggest this - currently, only the raw data returned is displayed to the user.&lt;/p&gt;
&lt;h2&gt;Browser Addon&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./BasicAddon.png&quot; alt=&quot;Basic Addon UI&quot; /&gt;&lt;br /&gt;
&lt;a href=&quot;https://github.com/autumn-mck/CSC1028FFAddon&quot;&gt;Code&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The browser addon is extremely similar to the electron app, providing a user-friendly front end to the data, built with HTML and javascript. As it is integrated into the browser, it can automatically fetch and cache data as the user navigates the web.&lt;br /&gt;
Note: The addon currently only supports Firefox, however it could be ported to support Chromium-based browsers extremely easily, as both share an extremely similar base API, with only a few functions being located in different namespaces, but providing the same results. (See &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Chrome_incompatibilities&quot;&gt;Chrome incompatibilities&lt;/a&gt; on MDN for details)&lt;/p&gt;
&lt;p&gt;The addon&apos;s UI is also currently lacking as I chose to shift focus away from it, as I decided the Electron UI was more important initially. However, since both are based on HTML and javascript, and the Electron app was built upon the framework of the browser addon, the updates for the Electron app should be able to be ported without too much effort.&lt;/p&gt;
&lt;h3&gt;Installing the addon&lt;/h3&gt;
&lt;p&gt;(Currently Firefox-only)&lt;br /&gt;
Installing the addon is thankfully easy. Navigate to &lt;code&gt;about:debugging&lt;/code&gt; and click on the &quot;This Firefox&quot; tab. Click on &quot;Load Temporary Add-on...&quot; and navigate to the folder containing the addon files. Click on any of the files (e.g. &lt;code&gt;manifest.json&lt;/code&gt;) and load it. The addon is now loaded! Whenever you update your code and save it, you just need to click the &quot;Reload&quot; button that appears.&lt;/p&gt;
&lt;p&gt;I&apos;d also recommend looking at &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions&quot;&gt;MDN&lt;/a&gt; for excellent documentation of the WebExtension APIs.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./LoadingAddon.png&quot; alt=&quot;Loading the addon&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Further development&lt;/h3&gt;
&lt;p&gt;The browser addon is similar in concept to the electron application, except with the front-end for displaying data being decoupled from the backend for requesting data.&lt;/p&gt;
&lt;p&gt;The backend is stored in &lt;code&gt;background.js&lt;/code&gt;, which as the name suggests, runs in the background. It uses event listeners to tell when the user changes to a different tab/web page, and if the data for that page has not been requested, request it and cache it by storing it in the addon storage.&lt;/p&gt;
&lt;p&gt;The front-end is in &lt;code&gt;popup/urlInfo.html&lt;/code&gt; (This, and the background script file, are determined in &lt;code&gt;manifest.json&lt;/code&gt;.), which provides a UI similar to the electron app whenever the user clicks on the toolbar button, which queries the cache and displays the data for the user&apos;s current tab.&lt;/p&gt;
&lt;h2&gt;Improvements and vision&lt;/h2&gt;
&lt;p&gt;The project in its current state is nowhere near complete, but serves as a foundation to build further upon.&lt;/p&gt;
&lt;p&gt;There are many possible new data sources that could be integrated into the project, for example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Crowdsourced datasets, eg Trustpilot and other user-driven sources of metadata&lt;/li&gt;
&lt;li&gt;What tech stack companies are using, and alert the user to suspicious activity if a different result is actually found, using information from &lt;a href=&quot;https://stackshare.io&quot;&gt;https://stackshare.io&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Further integration with archives, e.g. thumbnails of pages from the &lt;a href=&quot;https://web.archive.org/&quot;&gt;Wayback Machine&lt;/a&gt; - If a webpage has only existed for a few days its chance of being malware or a phishing attack are higher&lt;/li&gt;
&lt;li&gt;Data from &lt;a href=&quot;https://commoncrawl.org/&quot;&gt;Common Crawl&lt;/a&gt; to find sites that point to a given page (They were having &lt;a href=&quot;https://groups.google.com/g/common-crawl/c/kEHzXZNu5To&quot;&gt;issues with 503 errors&lt;/a&gt; when I last looked into integrating this, although it appears to have been fixed since.)&lt;/li&gt;
&lt;li&gt;Possibly other sources of data like Mozilla Observatory or Google Lighthouse.&lt;/li&gt;
&lt;li&gt;Add an extension page to the browser addon providing functionality similar to the electron app, allowing the user to query any URL&lt;/li&gt;
&lt;li&gt;General improvements to the user experience.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And many other possible sources of interesting metadata!&lt;/p&gt;
</content:encoded></item><item><title>How to deal with Project Sonar&apos;s data</title><link>https://mck.is/project-sonar/</link><guid isPermaLink="true">https://mck.is/project-sonar/</guid><description>From the beginning</description><pubDate>Tue, 01 Feb 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Note: I no longer have a copy of project sonar&apos;s data lying around, please don&apos;t email me to ask (even if you claim to be part of a &quot;cyber security team in my government&quot;). Also, there might be a better way of doing what I&apos;ve done in this post - this was admittedly just documenting the first way I discovered.&lt;/p&gt;
&lt;h2&gt;What is Project Sonar?&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://opendata.rapid7.com/&quot;&gt;Project Sonar&lt;/a&gt; is a data collection project containing information from scans across the internet: DNS records, SSL Certificates, and also scans of many commonly used ports with TCP/UDP.&lt;/p&gt;
&lt;p&gt;Update: 6 days after this guide was originally posted, &lt;a href=&quot;https://www.rapid7.com/blog/post/2022/02/10/evolving-how-we-share-rapid7-research-data-2/&quot;&gt;Rapid7 switched to requiring you to apply&lt;/a&gt; to access Project Sonar&apos;s data. Except now, a few weeks later (01/03/2022), it no longer requires an account again, and this time I cannot find any blog post etc. mentioning this change back, so I do not know if this is a permanent or temporary change.&lt;br /&gt;
Update on 28/03/22: This appears to be a permanent change. See &lt;a href=&quot;https://opendata.rapid7.com/about/&quot;&gt;https://opendata.rapid7.com/about/&lt;/a&gt; to apply for access.&lt;/p&gt;
&lt;h3&gt;Why should you use it?&lt;/h3&gt;
&lt;p&gt;Project Sonar&apos;s forward DNS data can be used as a reverse DNS lookup (Finding a list of domains that point to a given IP address) more reliably than the standard method (&lt;a href=&quot;https://www.cloudflare.com/learning/dns/dns-records/dns-ptr-record/&quot;&gt;PTR Records&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;It can also be used for subdomain enumeration (Finding subdomains under the given domain), which can reveal web applications and other services are publicly exposed to the Internet.&lt;/p&gt;
&lt;p&gt;The port scans can also be used to guess at what software a server is running and publicly exposed.&lt;/p&gt;
&lt;h2&gt;Project Sonar&apos;s data&lt;/h2&gt;
&lt;p&gt;I&apos;ll also explain a bit about what data Project Sonar contains:&lt;/p&gt;
&lt;h4&gt;Forward DNS (FDNS)&lt;/h4&gt;
&lt;p&gt;Contains data from the &lt;a href=&quot;https://en.wikipedia.org/wiki/Domain_Name_System&quot;&gt;Domain Name System&lt;/a&gt; (DNS), used to get from a &lt;a href=&quot;https://en.wikipedia.org/wiki/Hostname&quot;&gt;hostname&lt;/a&gt; like &quot;example.com&quot; to an IP address like &lt;code&gt;93.184.216.34&lt;/code&gt; (A records for an IPv4 address, AAAA record for an IPv6 address, or a CNAME rerecord, which points to another hostname). It also stores information like where an email should be sent to and what to do with an email if it is suspected of being spam (See &lt;a href=&quot;https://www.gov.uk/government/publications/email-security-standards/domainkeys-identified-mail-dkim&quot;&gt;DKIM&lt;/a&gt; for more on that). For more on DNS records, &lt;a href=&quot;https://www.cloudflare.com/en-gb/learning/dns/dns-records/&quot;&gt;Cloudflare&lt;/a&gt; has some good documentation.&lt;/p&gt;
&lt;h4&gt;Reverse DNS (RDNS)&lt;/h4&gt;
&lt;p&gt;This dataset contains the results of PTR Lookups, which is essentially the reverse of A records mention above. However PTR lookups are not perfectly reliable, and it is generally recommended to use A records to resolve IP addresses to a hostname instead of PTR Lookups.&lt;/p&gt;
&lt;h4&gt;HTTP GET Responses&lt;/h4&gt;
&lt;p&gt;This dataset contains the results of &lt;a href=&quot;https://en.wikipedia.org/wiki/GET_request&quot;&gt;HTTP GET requests&lt;/a&gt; against ports commonly used for HTTP (Generally port 80 is used for the majority of HTTP requests).&lt;/p&gt;
&lt;h4&gt;HTTPS GET Responses&lt;/h4&gt;
&lt;p&gt;Same as above, except against ports commonly used for &lt;a href=&quot;https://en.wikipedia.org/wiki/HTTPS&quot;&gt;HTTPS&lt;/a&gt;. (Port 443 is the most commonly used for HTTPS)&lt;/p&gt;
&lt;h4&gt;TCP Scans&lt;/h4&gt;
&lt;p&gt;This dataset contains responses from many commonly used &lt;a href=&quot;https://en.wikipedia.org/wiki/Transmission_Control_Protocol&quot;&gt;TCP&lt;/a&gt; ports, which can be used to check what services a server may be running, or could be vulnerable. To see what a given port is used for, check &lt;a href=&quot;https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml&quot;&gt;https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml&lt;/a&gt; (&lt;a href=&quot;https://en.wikipedia.org/wiki/Internet_Assigned_Numbers_Authority&quot;&gt;IANA&lt;/a&gt; are in charge of managing what ports are &quot;officially&quot; used for to avoid multiple services using the same port)&lt;/p&gt;
&lt;h4&gt;UDP Scans&lt;/h4&gt;
&lt;p&gt;Same as the above TCP scans, but with &lt;a href=&quot;https://en.wikipedia.org/wiki/User_Datagram_Protocol&quot;&gt;UDP&lt;/a&gt;.&lt;/p&gt;
&lt;h4&gt;SSL Certificates &amp;amp; More SSL Certificates (non-443)&lt;/h4&gt;
&lt;p&gt;Contains data on certificates used for securing HTTPS connections.&lt;/p&gt;
&lt;h2&gt;Setup&lt;/h2&gt;
&lt;p&gt;To start, you&apos;re going to want to be using an IDE - I&apos;d recommend &lt;a href=&quot;https://code.visualstudio.com/&quot;&gt;Visual Studio Code&lt;/a&gt;. This guide is written assuming you&apos;re using VS Code, but everything will still work if you choose a different IDE. It&apos;s also assuming you&apos;ve not used Node.js before - if you have, you might want to skip to &lt;a href=&quot;#start-programming&quot;&gt;Start Programming&lt;/a&gt;. Finally, all the code for this guide is also &lt;a href=&quot;https://github.com/autumn-mck/ProjectSonarTutorial&quot;&gt;available here&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;Start by making a new folder to hold your project - I called mine ProjectSonarTutorial - and open it in VS Code. We&apos;re going to need to install Node.js too - a convenient way to do so is using a version manager like &lt;a href=&quot;https://github.com/nvm-sh/nvm&quot;&gt;nvm&lt;/a&gt; for Linux and MacOS, or &lt;a href=&quot;https://github.com/coreybutler/nvm-windows&quot;&gt;nvm-windows&lt;/a&gt; for Windows.&lt;/p&gt;
&lt;p&gt;Once you have one of these installed (Note: On windows, you may have to restart your computer to use nvm), we can install install Node.js. Open up a terminal as administrator (Or run the commands with &lt;code&gt;sudo&lt;/code&gt; on linux/mac) and run &lt;code&gt;nvm install 16&lt;/code&gt;. This will install the latest version of Node 16, currently 16.13.2 (We&apos;re using Node 16 instead of the newer 17 as some packages are currently incompatible with it), then run &lt;code&gt;nvm use 16.13.2&lt;/code&gt;. Now we have node.js installed and set up!&lt;/p&gt;
&lt;p&gt;We&apos;re also going to be using MongoDB - I used a local installation for this tutorial. To install it, follow the instructions over at &lt;a href=&quot;https://docs.mongodb.com/manual/installation/&quot;&gt;https://docs.mongodb.com/manual/installation/&lt;/a&gt;. MongoDB compass might be installed along side it, but if not, I&apos;d recommend installing it too - it&apos;s a useful tool for inspecting your databases.&lt;/p&gt;
&lt;p&gt;Returning to VS Code, we can open up its built in terminal with &lt;code&gt;ctrl + &apos;&lt;/code&gt;. We&apos;re going to need a few external packages later, so we might as well install them now. First up, we&apos;ll generate the package.json file (Where information like what packages your program depends on is stored), by running &lt;code&gt;npm init&lt;/code&gt;. &lt;code&gt;npm&lt;/code&gt; stands for Node Package Manager, and is how you can install external packages (Like the MongoDB Node.js Driver) to use in your program. &lt;code&gt;npm init&lt;/code&gt;&apos;s defaults are probably good enough, however you can change them if you wish. Next up, open the &lt;code&gt;package.json&lt;/code&gt; file that was created, and add the line &lt;code&gt;&quot;type&quot;: &quot;module&quot;,&lt;/code&gt; below the description line - This marks our program as using the newer &lt;code&gt;import ... from ...&lt;/code&gt; syntax instead of the older &lt;code&gt;var ... = require(...)&lt;/code&gt; syntax. Be aware that some tutorials still make use of the old syntax, however. Finally, run &lt;code&gt;npm install mongodb&lt;/code&gt; and &lt;code&gt;npm install tldts-experimental&lt;/code&gt; to install the packages that we need.&lt;/p&gt;
&lt;h2&gt;Start programming&lt;/h2&gt;
&lt;p&gt;Now we can begin to get to the interesting stuff: create a file called &lt;code&gt;fetchData.js&lt;/code&gt;. At the top of it, we can add:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { MongoClient } from &quot;mongodb&quot;;
import { parse as tldParse } from &quot;tldts-experimental&quot;;
import zlib from &quot;zlib&quot;;
import fs from &quot;fs&quot;;
import { get as getHttps } from &quot;https&quot;;
import readline from &quot;readline&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This imports what we need from the two packages we just installed, along with what we&apos;ll need from node&apos;s core modules.&lt;/p&gt;
&lt;p&gt;We&apos;ll then add our main function:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Main function
 */
async function main() {
	// Content of main function goes here
}

// Run the main function
main().catch(console.error);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To make sure everything is up and running, add the typical &quot;Hello World&quot; to the main function with &lt;code&gt;console.log(&quot;Hello World!&quot;);&lt;/code&gt;. To run the program, go to the terminal and run &lt;code&gt;node fetchData.js&lt;/code&gt; - Hopefully you should be greeted with &quot;Hello World!&quot; being logged.&lt;/p&gt;
&lt;p&gt;Next up we&apos;ll connect to MongoDB. Since we&apos;re using a local database, the connection URI should be as simple as &lt;code&gt;&quot;mongodb://localhost:27017&quot;&lt;/code&gt;. Then we can create a new MongoClient, and pass our connection string to its constructor. Then we can open the connection with &lt;code&gt;await client.connect();&lt;/code&gt; To make sure everything is working, we can print a list of all databases. Let&apos;s make a function for it!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async function listDatabases(client) {
	let dbList = await client.db().admin().listDatabases();

	console.log(&quot;Databases:&quot;);
	dbList.databases.forEach((db) =&amp;gt; console.log(` - ${db.name}`));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Call the new listDatabases function from within our main function, and pass it the MongoClient we created, after opening the client&apos;s connection. Running our code so far (with &lt;code&gt;node fetchData.js&lt;/code&gt;) we should get something like this:
&lt;img src=&quot;./ListDatabases.png&quot; alt=&quot;List of databases&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Your code so far should be similar to&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { MongoClient } from &quot;mongodb&quot;;
import { parse as tldParse } from &quot;tldts-experimental&quot;;
import zlib from &quot;zlib&quot;;
import fs from &quot;fs&quot;;
import { get as getHttps } from &quot;https&quot;;
import readline from &quot;readline&quot;;

/**
 * Main function
 */
async function main() {
	// Database is currently hosted on same machine
	const uri = &quot;mongodb://localhost:27017&quot;;
	const client = new MongoClient(uri);

	try {
		// Connect to MongoDB
		await client.connect();

		// List databases
		await listDatabases(client);
	} catch (e) {
		console.error(e);
	}
}

async function listDatabases(client) {
	let dbList = await client.db().admin().listDatabases();

	console.log(&quot;Databases:&quot;);
	dbList.databases.forEach((db) =&amp;gt; console.log(` - ${db.name}`));
}

// Run the main function
main().catch(console.error);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You might have have noticed that the program still appears to be running, and you can no longer type in the terminal. You can press &lt;code&gt;Ctrl + c&lt;/code&gt; when focused on the terminal to stop the currently running program at any time.&lt;/p&gt;
&lt;p&gt;Next, we need to fetch the data. There are 2 options for this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use a local copy of the file that we can parse&lt;/li&gt;
&lt;li&gt;Stream the data from the web and parse it as we receive it
Both options are shown in this tutorial (See &lt;a href=&quot;#parsing-a-local-copy-of-project-sonar&quot;&gt;Parsing a local copy of Project Sonar&lt;/a&gt; and &lt;a href=&quot;#fetching-and-parsing-an-online-version-of-project-sonar&quot;&gt;Fetching and parsing an online version of Project Sonar&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I&apos;d probably recommend using the local copy, as it does not depend on your internet connection&apos;s reliability, but it does require you to have the space to store the compressed file, in addition to the storage space required by the MongoDB database itself.&lt;/p&gt;
&lt;p&gt;Project Sonar&apos;s data can be found at &lt;a href=&quot;https://opendata.rapid7.com/sonar.fdns_v2/&quot;&gt;https://opendata.rapid7.com/sonar.fdns_v2/&lt;/a&gt;. In this guide, I&apos;m going to be parsing the DNS A Records, so, we need the file ending in &lt;code&gt;-fdns_a.json.gz&lt;/code&gt;. Do note that the file is large (17gb) and be careful not to unzip it - uncompressed, it is over 200gb!&lt;/p&gt;
&lt;h2&gt;Parsing a local copy of Project Sonar&lt;/h2&gt;
&lt;p&gt;Let&apos;s add a new function, &lt;code&gt;readFromFile&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async function readFromFile(client) {
	const sonarDataLocation = &quot;fdns_a.json.gz&quot;;
	let stream = fs.createReadStream(sonarDataLocation);
	parseSonar(client, stream);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;sonarDataLocation&lt;/code&gt; should be wherever you saved the data to - either a relative path, in the current case (&lt;code&gt;fdns_a.json&lt;/code&gt; is in the same folder as &lt;code&gt;fetchData.js&lt;/code&gt;), or an absolute path, like &lt;code&gt;C:\\Users\\autumn\\Downloads\\fdns_a.json.gz&lt;/code&gt;. We then create a &lt;a href=&quot;https://nodejs.org/api/stream.html#stream&quot;&gt;read stream&lt;/a&gt; - not the actual data itself - that we can later read through and parse. &lt;code&gt;fs&lt;/code&gt; is Node.js&apos;s filesystem module, allowing us to interact with local files. We then pass this stream, and the MongoClient passed into the function, to a function that does not yet exist - it&apos;s next for us to make.&lt;/p&gt;
&lt;p&gt;Finally, let&apos;s call this method from the main function with &lt;code&gt;readFromFile(client);&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Alternatively, if you don&apos;t want to have the file saved locally:&lt;/p&gt;
&lt;h2&gt;Fetching and parsing an online version of Project Sonar&lt;/h2&gt;
&lt;p&gt;This method is a bit more complicated, but means that we do not have to keep a copy saved on our machine, taking up space. It will require you to have a reliable internet connection, however.&lt;/p&gt;
&lt;p&gt;Let&apos;s add a new function, &lt;code&gt;readFromWeb&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async function readFromWeb(client, url) {
	getHttps(url, function (res) {
		// Code here
	}).on(&quot;error&quot;, function (e) {
		console.error(e);
	});
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This function calls the get method from node&apos;s https package that we imported earlier as &lt;code&gt;getHttps&lt;/code&gt;. It gets the result of this call as &lt;code&gt;res&lt;/code&gt;, currently does northing with it, and will log any errors. So what do we do with this result? First of all, we need to deal with redirects. &lt;a href=&quot;https://opendata.rapid7.com/sonar.fdns_v2/2022-01-28-1643328400-fdns_a.json.gz&quot;&gt;https://opendata.rapid7.com/sonar.fdns_v2/2022-01-28-1643328400-fdns_a.json.gz&lt;/a&gt;, The link on Project Sonar&apos;s site, actually redirects to backblaze, where the data is actually hosted, before allowing you to download it.&lt;/p&gt;
&lt;p&gt;Fortunately, we can check if we need to redirect based on the result&apos;s &lt;a href=&quot;https://httpstatuses.com/&quot;&gt;HTTP status code&lt;/a&gt;. If the status is 200, we&apos;re in the right place, and can return the result to be used elsewhere. If the status is 301 or 303, we should follow the redirect by calling the readFromWeb method again, with the new URL being passed in as an argument. I&apos;ve added the following code inside the above &lt;code&gt;getHttps&lt;/code&gt; call:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (res.statusCode === 200) {
	parseSonar(client, res);
} else if (res.statusCode === 301 || res.statusCode === 302) {
	// Recursively follow redirects, only a 200 will resolve.
	console.log(`Redirecting to: ${res.headers.location}`);
	readFromWeb(client, res.headers.location);
} else {
	console.log(
		`Download request failed, response status: ${res.statusCode} ${res.statusMessage}`
	);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This function gets a &lt;a href=&quot;https://nodejs.org/api/stream.html#stream&quot;&gt;read stream&lt;/a&gt; - not the actual data itself - that we can later read through and parse. We can then pass it to a function that does not yet exist (We&apos;ll add it shortly) along with the MongoClient this function was passed.&lt;/p&gt;
&lt;p&gt;Now we can return to our main method and add in something to call our new function&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const dataUrl =
	&quot;https://opendata.rapid7.com/sonar.fdns_v2/2022-01-28-1643328400-fdns_a.json.gz&quot;;
readFromWeb(client, dataUrl);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that if this doesn&apos;t work, make sure you have the latest link from &lt;a href=&quot;https://opendata.rapid7.com/sonar.fdns_v2/&quot;&gt;https://opendata.rapid7.com/sonar.fdns_v2/&lt;/a&gt;, as downloading older/newer versions requires an account.&lt;/p&gt;
&lt;h2&gt;Parsing our input&lt;/h2&gt;
&lt;p&gt;So now, using either of the above methods, we have a stream that will allow us to read in the project sonar data. Unfortunately, we still have two things to deal with before getting to anything useful: We have to get data out of the stream, and then we have to decompress the data we&apos;ve been given - it&apos;s currently still &lt;a href=&quot;https://en.wikipedia.org/wiki/Gzip&quot;&gt;gzipped&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Luckily, we can deal with both of those problems pretty quickly! Let&apos;s create a new function:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async function parseSonar(client, readstream) {
	// Pipe the response into gunzip to decompress
	let gunzip = zlib.createGunzip();

	let lineReader = readline.createInterface({
		input: readstream.pipe(gunzip),
	});
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What we&apos;re doing here is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Creating a writable stream called &lt;code&gt;gunzip&lt;/code&gt; with &lt;code&gt;zlib&lt;/code&gt;, node.js&apos;s module for compression/decompression&lt;/li&gt;
&lt;li&gt;Piping our readstream of compressed Project Sonar data to this &lt;code&gt;gunzip&lt;/code&gt; object&lt;/li&gt;
&lt;li&gt;Taking the output of that, and using it as the input for a readline object, which allows us to parse the data one line at a time. (It also means we don&apos;t have to worry about buffers stopping mid-line and giving us all sorts of errors.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Quite a lot for a few lines of code!&lt;br /&gt;
Now, we still need to get our data out of this linereader. To do this, we can use the &lt;code&gt;&quot;line&quot;&lt;/code&gt; event that the linereader &apos;emits&apos; to let us know when we have a new line to parse, with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lineReader.on(&quot;line&quot;, (line) =&amp;gt; {
	// We&apos;ll parse the line in here
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So we&apos;ve got a line of data - now what?&lt;br /&gt;
The data is in JSON form, and luckily for us, we can simply use javascript&apos;s &lt;code&gt;JSON.parse()&lt;/code&gt; to parse it. Next up, we need to break the hostname (eg &lt;code&gt;subdomain.example.com/path&lt;/code&gt;) into it parts - we need just the &lt;code&gt;example&lt;/code&gt; bit (This is required for performance - I&apos;ll explain more once we get to that point). We can do this pretty easily by using the &lt;code&gt;tldts-experimental&lt;/code&gt; package&apos;s &lt;code&gt;parse&lt;/code&gt; function we imported earlier as &lt;code&gt;tldParse&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;First, we need to deal with the many records beginning with &lt;code&gt;*.&lt;/code&gt;. If we don&apos;t remove this from the start of the hostname, we cannot properly parse it. Next, let&apos;s parse it with &lt;code&gt;tldParse&lt;/code&gt; and log it, to make sure everything is working so far.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let lineJson = JSON.parse(line);
let hostname = lineJson.name;

if (hostname.substring(0, 2) === &quot;*.&quot;) hostname = hostname.substring(2);

let tldParsed = tldParse(hostname);

console.log(tldParsed);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should now hopefully see lines of JSON being printed! We should probably remove that &lt;code&gt;console.log&lt;/code&gt; for now though - printing out every single line hurts our performance.&lt;br /&gt;
Note that there are still a few invalid hostnames - Some beginning with &lt;code&gt;/&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt; or &lt;code&gt;*&lt;/code&gt;. I don&apos;t know why these are here, but given that only around 0.2% of the results are invalid, it&apos;s probably safe enough to ignore them for now.&lt;/p&gt;
&lt;h2&gt;MongoDB&lt;/h2&gt;
&lt;p&gt;Now we need to start thinking about MongoDB. Whilst MongoDB is fast, it is unfortunately not fast enough to get us a quick result from 1.7 billion items. To speed it up, we&apos;ll make use of &lt;a href=&quot;https://docs.mongodb.com/manual/core/index-text/&quot;&gt;text indexes&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Back in our main function, let&apos;s add a line to create this text index.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;await client
	.db(&quot;test_db&quot;)
	.collection(&quot;sonardata&quot;)
	.createIndex({ domainWithoutSuffix: &quot;text&quot; });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can call your database and collection whatever you want - this is just what I&apos;m using. We&apos;re using the domain without the suffix as our index, as that&apos;s what I&apos;m wanting to query later on. If, however, you wanted to query IP address, to find out which domains point to a given IP address, you&apos;d use it as your text index instead.&lt;/p&gt;
&lt;p&gt;We also don&apos;t want redundant data building up each time we run our program - let&apos;s add something to drop the collection each time the program is run. (We don&apos;t need to add anything to create the collection again - MongoDB does this automatically for us whenever we try to add data to it.)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Drop the collection containing Project Sonar data
try {
	await client.db(&quot;test_db&quot;).collection(&quot;sonardata&quot;).drop();
} catch {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nice! Now we can begin actually adding the data to MongoDB.&lt;br /&gt;
Returning back to our &lt;code&gt;parseSonar&lt;/code&gt; function - items can be inserted in bulk to MongoDB to increase performance, up to 100k items - so let&apos;s do that. After we&apos;ve created the linereader, let&apos;s create an array and a counter to keep track of how many items we have.&lt;/p&gt;
&lt;p&gt;Now, after the JSON has been parsed, we can increment our counter and add whatever data we want to our buffer array. Then, when our counter is evenly divisible by 100,000, we can log how many lines have been parsed, send our data to be added to MongoDB, and clear our buffer array. Our &lt;code&gt;parseSonar&lt;/code&gt; function should now look something like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async function parseSonar(client, readstream) {
	// Pipe the response into gunzip to decompress
	let gunzip = zlib.createGunzip();

	let lineReader = readline.createInterface({
		input: readstream.pipe(gunzip),
	});

	let arr = [];
	let count = 0;
	lineReader.on(&quot;line&quot;, (line) =&amp;gt; {
		let lineJson = JSON.parse(line);
		let hostname = lineJson.name;
		if (hostname.substring(0, 2) === &quot;*.&quot;) hostname = hostname.substring(2);

		let tldParsed = tldParse(hostname);

		if (tldParsed.domainWithoutSuffix) {
			count++;
			// What data you&apos;re putting in the array depends on what you&apos;re planning to do with it
			arr.push({
				domainWithoutSuffix: tldParsed.domainWithoutSuffix,
				publicSuffix: tldParsed.publicSuffix,
				subdomain: tldParsed.subdomain,
				name: lineJson.name,
				type: lineJson.type,
				value: lineJson.value,
			});

			if (count % 100000 === 0) {
				console.log(`${count} lines parsed`);
				createManyListings(client, arr, &quot;sonardata&quot;);
				arr = [];
			}
		}
	});
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nearly done now! We just need to add the &lt;code&gt;createManyListings&lt;/code&gt; function. Thankfully, it&apos;s pretty simple:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async function createManyListings(
	client,
	newListing,
	collection,
	dbName = &quot;test_db&quot;
) {
	client
		.db(dbName)
		.collection(collection)
		.insertMany(newListing, { ordered: false });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The only thing to note here is that we&apos;re telling MongoDB that our data is not/does not need to be ordered, helping increase our performance slightly. Running the program now will begin filling up our database with data. Unfortunately, this is still a slow process - We have about 1.7 billion lines to parse! Finally, you may also run into memory issues with NodeJS, as by default it can only use &lt;a href=&quot;https://www.the-data-wrangler.com/nodejs-memory-limits/&quot;&gt;up to 1.7gb of memory&lt;/a&gt;, and MongoDB cannot always keep up with the rate we are sending it data at (It&apos;s inconsistent). Since we only need our application to run for long enough to allow us to fetch all the data, we can take the quick and easy approach of just giving NodeJS more memory. We can do this by running &lt;code&gt;node --max-old-space-size=8000 fetchData.js&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Querying MongoDB&lt;/h2&gt;
&lt;p&gt;So, we have our data sitting in a collection in MongoDB. Now what?&lt;/p&gt;
&lt;p&gt;Create a new file called &lt;code&gt;queryData.js&lt;/code&gt;. We can follow the basic template of the previous file to get started:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { MongoClient } from &quot;mongodb&quot;;

/**
 * Main function
 */
async function main() {
	// Database is currently hosted on same machine
	const uri = &quot;mongodb://localhost:27017&quot;;
	const client = new MongoClient(uri);

	try {
		// Connect to the MongoDB cluster
		await client.connect();

		// Run query here
	} catch (e) {
		// Log any errors
		console.error(e);
	} finally {
		await client.close();
	}
}

// Run the main function
main().catch(console.error);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we need to come up with a query! As an example, I&apos;ll search for subdomains of rapid7.&lt;br /&gt;
To actually query this, I&apos;ll use:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let query = { $text: { $search: &quot;rapid7&quot; }, domainWithoutSuffix: &quot;rapid7&quot; };
await findMany(client, query, &quot;sonardata&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To explain what the query actually means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;$text: { $search: &quot;rapid7&quot; }&lt;/code&gt; is how we&apos;re able to make queries with a reasonable level of performance - It makes use of the text index we set up earlier, and matches with all &lt;code&gt;domainWithoutSuffix&lt;/code&gt;s that &lt;strong&gt;contain&lt;/strong&gt; (&lt;em&gt;not&lt;/em&gt; match exactly) the given query.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;domainWithoutSuffix: &quot;rapid7&quot;&lt;/code&gt; narrows that down further to only the exact matches.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We could continue to further narrow this down if we wanted (For more info, see &lt;a href=&quot;https://docs.mongodb.com/manual/tutorial/query-documents/&quot;&gt;https://docs.mongodb.com/manual/tutorial/query-documents/&lt;/a&gt;). First though, we need to add in the &lt;code&gt;findMany&lt;/code&gt; function that we&apos;re calling.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async function findMany(
	client,
	query,
	collection,
	db_name = &quot;test_db&quot;,
	maxResults = 500
) {
	const cursor = client
		.db(db_name)
		.collection(collection)
		.find(query)
		.limit(maxResults);

	const results = await cursor.toArray();

	if (results.length &amp;gt; 0) {
		console.log(&quot;Found items:&quot;);
		results.forEach((result, i) =&amp;gt; {
			console.log(result);
		});
	} else {
		console.log(&quot;No results found with the given query!&quot;);
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This function is fairly simple - all it does is fetch the results of the query to the given collection as an array, then if there are results, print them.&lt;/p&gt;
&lt;p&gt;And that&apos;s it! Now you&apos;re able to query Project Sonar&apos;s data! Although this guide only covered the DNS A records, the same principles apply to the port scans, SSL certificates, etc.&lt;/p&gt;
&lt;h2&gt;Final note on compiling to executable&lt;/h2&gt;
&lt;p&gt;This won&apos;t be relevant to everybody, but as I encountered issues with it I&apos;m putting it here in the hope it will help somebody.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;pkg&lt;/code&gt;, the most popular option for compiling NodeJS executables, still does not support the several-year old javascript ES6 modules (See &lt;a href=&quot;https://github.com/vercel/pkg/issues/1291&quot;&gt;this&lt;/a&gt; github issue and &lt;a href=&quot;https://github.com/vercel/pkg/pull/1323&quot;&gt;this&lt;/a&gt; pull request for updates), so we&apos;ll be using &lt;a href=&quot;https://github.com/nexe/nexe&quot;&gt;nexe&lt;/a&gt; instead. Unfortunately, the latest version of NodeJS that &lt;code&gt;nexe&lt;/code&gt; has pre-compiled is 14, and we need to be using version 16, so we&apos;ll need to compile it ourselves later. Don&apos;t worry, this isn&apos;t too difficult!&lt;/p&gt;
&lt;p&gt;Let&apos;s begin by installing &lt;code&gt;nexe&lt;/code&gt; with &lt;code&gt;npm i nexe -g&lt;/code&gt;. We&apos;ll then need to follow the instructions for building &lt;a href=&quot;https://github.com/nodejs/node/blob/v16.x/BUILDING.md&quot;&gt;here&lt;/a&gt;. (Use the section for Linux, Windows or MacOS depending on what you are using)&lt;/p&gt;
&lt;p&gt;We can select which file we want &lt;code&gt;nexe&lt;/code&gt; to build by running then run &lt;code&gt;nexe --build&lt;/code&gt; to start building. (If you get an error, you probably don&apos;t have everything required installed. Look at the output of &lt;code&gt;nexe  --build --verbose&lt;/code&gt; to see what you&apos;re missing). This will probably take a while - on my laptop, it took half an hour, and fifteen minutes on my desktop. (To get an estimate of how long it&apos;ll take for you, see &lt;a href=&quot;https://openbenchmarking.org/test/pts/build-nodejs&quot;&gt;https://openbenchmarking.org/test/pts/build-nodejs&lt;/a&gt;.)&lt;/p&gt;
&lt;p&gt;Once that step has completed, we can finally build our application by running &lt;code&gt;nexe &amp;lt;filename&amp;gt; -b&lt;/code&gt;, and additionally can target other platforms by using &lt;code&gt;--target win&lt;/code&gt;, or &lt;code&gt;--target linux&lt;/code&gt;, etc. as additional arguments.&lt;/p&gt;
</content:encoded></item><item><title>I love FTL: Faster Than Light</title><link>https://mck.is/blog/2022/i-love-ftl/</link><guid isPermaLink="true">https://mck.is/blog/2022/i-love-ftl/</guid><description>I ramble about a game I like for too long</description><pubDate>Sat, 29 Jan 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I originally wrote this blog post as the content of a page for one of my university modules, but I&apos;ve decided to stick it here too!
I had planned for this to be a list of some of my favourite indie games, but it quickly spiralled into just being about FTL.&lt;/p&gt;
&lt;p&gt;(I&apos;m planning to get around to that list soon though!)&lt;/p&gt;
&lt;h2&gt;FTL: Faster Than Light&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/FTL.R4u5iF3G_28alyx.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;&amp;lt;i&amp;gt;I should probably have put out those fires...&amp;lt;/i&amp;gt;&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;FTL was a completely new experience to me in several ways. It was the first PC game I bought (now the only platform I play on), the first indie game I played (now the vast majority of the games I play), and the first game I ever chose to buy myself, instead of having it bought for me.&lt;/p&gt;
&lt;p&gt;As a result, it&apos;s probably not possible for me to talk about it without looking through rose-tinted glasses to some extent, so take my opinion with a grain of salt.&lt;/p&gt;
&lt;p&gt;For an overview: You play as the captain of a ship, belonging to the federation. You must manage your crew and energy usage and make decisions on how to survive throughout your journey and help defend the federation against the rebels.&lt;/p&gt;
&lt;p&gt;FTL is a &quot;rougelike&quot; game, meaning on each playthrough you start from scratch. Weak and with little to defend yourself, you must upgrade your ship and take on new crewmembers to hope to survive the many dangers you find as you explore the universe. With a wide range of weapons, from laser machine guns that take time to start up but are deadly once unleashed, to fire beams to kill your enemy ship&apos;s crew while leaving their ship (mostly) intact, and systems like hacking to take over a ship&apos;s navigation or teleporters to form boarding crews, there are many ways to play the game. Unfortunately for you, enemy ships also have access to the same arsenal of systems, and they aren&apos;t willing to go down without a fight.&lt;/p&gt;
&lt;p&gt;The game also has an amazing soundtrack (+advanced edition soundtrack) by Ben Prunty, which perfectly fits the atmosphere of the game. It really wouldn&apos;t be the same without this great music.&lt;/p&gt;
&lt;p&gt;To make each run more different, the game also has unlockable ships (8 ships with 3 variants, and 2 more hidden ships with 2 variants each, for a total of a lot of ships), offering several different playstyles and extra challenges.&lt;/p&gt;
&lt;h2&gt;Choices&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/FTLChoice.DnfT9iYW_Z1fg8kK.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;&amp;lt;i&amp;gt;Choices in FTL&amp;lt;/i&amp;gt;&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;Extra options you have access to are highlighted in blue&lt;/p&gt;
&lt;p&gt;Another one of my favourite things about FTL is the choices it gives you. As an example:&lt;/p&gt;
&lt;p&gt;&quot;You arrive at the distress beacon near a small asteroid belt and find a ship with pirate markings partially crushed between two large rocks. It must have been illegally mining the belt without proper equipment.&quot;&lt;br /&gt;
Your choices to respond to this are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&quot;Try to dislodge the pirates by shooting at the rocks.&quot; - Will you damage their ship by doing this? What if they think you&apos;re trying to attack them, or since they&apos;re pirates, decide to attack you anyway? However, they may also reward you for your assistance.&lt;/li&gt;
&lt;li&gt;&quot;Destroy and loot the ship. They&apos;re just pirates.&quot; - You might get some scrap (The game&apos;s currency) from destroying them, but what if they have friends around that see you?&lt;/li&gt;
&lt;li&gt;However, if you have a beam weapon or beam drone, you can &quot;(Beam Weapon) Carefully cut the ship out.&quot; - Guaranteed to work, and you&apos;ll get your scrap reward, however it requires having specific equipment on your ship.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is just one single simple event that you can come across in your exploration, and even it has complexity in its choices. There never is a perfect choice, and you can only guess at what the outcome of your actions will be.&lt;/p&gt;
&lt;p&gt;The game costs £7, and regularly goes on sale for the incredibly low price of £1.74 - well worth the hundreds of hours of fun I&apos;ve gotten from this game (Note: At the time of writing, it is 75% off on steam!). As a warning though - FTL is hard. You&apos;ll die. A lot. Easy mode is a must when starting out, and I&apos;m still not convinced hard mode is actually beatable.&lt;/p&gt;
&lt;p&gt;FTL is one of the few games I&apos;ve properly obsessed over - I can&apos;t recommend it enough.&lt;/p&gt;
&lt;h2&gt;FTL: Multiverse&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://mck.is/_astro/Multiverse.DdJg3cCl_cr9x6.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Finally, the game has some amazing mods, particularly FTL: Multiverse. (The base game is good enough that I put over 100 hours into the vanilla game before looking at mods, but I recommend you try some out eventually). It essentially acts as a sequel, building significantly on the vanilla game - everything from significantly expanding the base game&apos;s atmospheric world building, to completely overhauling many mechanics of the base game and adding a lot of new content, and 20 new great music tracks to the vanilla&apos;s already stellar soundtrack. It essentially overhauls, expands, or refurbishes nearly every aspect of the game, whether it&apos;s ships, crew, events, weapons, etc. while still feeling balanced, and retaining what made the original game so great.&lt;/p&gt;
&lt;p&gt;The mod released in 2019, 7 years after the game released, and is still under active development, being on version 5.0 at the time of writing, with 5.1 set to release a few weeks from now, bringing a new mechanic, questline and sector. Multiverse is seriously one of my favourite mods I have ever played, and I love it.
Conclusion: What was this blog post?&lt;/p&gt;
&lt;p&gt;Very rambly. Next blog post will probably be on something completely different, possibly how I&apos;m making this blog? Probably equally rambly though.&lt;/p&gt;
</content:encoded></item></channel></rss>