<?xml version='1.0' encoding='UTF-8'?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><id>https://systemoflevers.com</id><title>System of Levers</title><updated>2026-01-09T21:39:00.277570+00:00</updated><author><name>Jesse Doherty</name></author><link href="https://systemoflevers.com/atom.xml" rel="self"/><link href="https://systemoflevers.com"/><generator uri="https://lkiesow.github.io/python-feedgen" version="1.0.0">python-feedgen</generator><subtitle>Solving problems no one else seems to be havign</subtitle><entry><id>https://systemoflevers.com/blog/2021/12/16/gameboy-wifi-cart/index.html</id><title>gameboy wifi cart</title><updated>2021-12-16T00:00:00-04:56</updated><content type="html">&lt;h1&gt;I'm making a WiFi GameBoy Cartridge!&lt;/h1&gt;
&lt;p&gt;It works, which is cool!&lt;/p&gt;
&lt;p&gt;It's not finished, which is ok.&lt;/p&gt;
&lt;p&gt;And Sebastian Staacks just posted a &lt;a href="https://there.oughta.be/a/wifi-game-boy-cartridge"&gt;nice write up&lt;/a&gt; about finishing his own, which makes me a bit sad, but they have enough differences to be interesting.&lt;/p&gt;
&lt;p&gt;So here's mine!&lt;/p&gt;
&lt;p&gt;&lt;a href="wifi_cart.jpg"&gt;&lt;img alt="A GameBoy connected to a breadboard implementing a WiFi enabled cartridge" src="wifi_cart_small.jpg" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I was hoping to have a completed PCB and a demo before writing about it, but since I got scooped maybe I can ride some coattails! Unfortunately I'm trying to get this out fast so that I hopefully don't lose all my motivation and I'm not practiced at writing, soooooo this is going to be rough... Also my site isn't ready. Oh well!&lt;/p&gt;
&lt;h1&gt;What is it?&lt;/h1&gt;
&lt;p&gt;It's a GameBoy "cartridge" that can connect to WiFi. When I started this project I also started watching &lt;a href="https://eater.net/"&gt;Ben Eater&lt;/a&gt;'s videos building a breadboard CPU so that's why it looks the way it does.&lt;/p&gt;
&lt;p&gt;My goal was to build a cartridge that looked exactly like a normal GameBoy game but could connect to the internet. Exactly like Sebastian did!&lt;/p&gt;
&lt;h1&gt;How does it work?&lt;/h1&gt;
&lt;p&gt;In some important ways it works the same as Sebastian's does. I have a ROM chip that has GameBoy code and images, I have an ESP8266 to do the WiFi, I have some hardware logic to map the ESP to a magic GameBoy memory address, and I have a big honkin capacitor to keep the ESP from overloading the GameBoy's power system.&lt;/p&gt;
&lt;h2&gt;What's different?&lt;/h2&gt;
&lt;p&gt;There are a few small differences and a few bigger ones.&lt;/p&gt;
&lt;h3&gt;Small differences&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;I'm using flash memory for my ROM. I think it was cheaper?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;I went with a 2200uF capacitor for power. I'll have to try smaller values now because the 2200uF tantalum capacitor is over $10 (CAD). I got the 2200uF from a YouTube video about evening out the peaks from an ESP8266. The link to the video that I wrote down (on paper) doesn't work so either it's gone or I wrote it wrong.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;I'm using a wemos D1 mini for my ESP. My plan is to use a more compact ESP module and a 3.3V regulator once I get to the PCB stage. The wemos is just very convenient for now.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;I've mapped the ESP to the GameBoy's cartridge RAM address space instead of a ROM address. I wasn't including RAM in my cartridge and this meant I could use the same address for sending to and reading from the ESP.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Bigger differences&lt;/h3&gt;
&lt;h4&gt;The data channel&lt;/h4&gt;
&lt;p&gt;The biggest difference is that I'm not connecting 8 of the ESP's GPIO pins directly to the data bus (with a bus transceiver). Instead I'm using 2 shift registers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A CD74HCT597, parallel-in-serial-out (PISO), to capture data from the GameBoy to feed it to the ESP.&lt;/li&gt;
&lt;li&gt;A CD74HC4094, serial-in-parallel-out (SIPO), to get data from the ESP and store it until the the GB is ready to read it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The reason I went with shift registers is because I wanted to avoid the issue of matching the GameBoy's timing as much as possible and to use fewer of the ESP's pins. From Sebastian's description of the timing issues, and seeing as I don't have an oscilloscope, I think I made the right choice for me. &lt;/p&gt;
&lt;p&gt;The reason I chose those particular shift registers is because I didn't know what I was doing and I mostly picked at random!&lt;/p&gt;
&lt;p&gt;I have them hooked up to the ESP's SPI bus with 2 additional ESP pins setup as interrupts, one to tell the ESP that the GameBoy wrote some data to the PISO and the other to tell it the GameBoy read from SIPO. This setup gave me reasonable speed. In one test I was able to have the GameBoy copy a value from its memory over to the ESP 10 times in a loop. Something like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ld b, 10
    ld hl, $C005
.write10
    ld a, [hli]
    ld [rESP], a
    dec b
    jr nz .write10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(basically a &lt;code&gt;for&lt;/code&gt; loop, if you don't read assembly)&lt;/p&gt;
&lt;p&gt;Where I have &lt;code&gt;rESP&lt;/code&gt; defined as &lt;code&gt;rESP EQU $A000&lt;/code&gt;. &lt;code&gt;$A000&lt;/code&gt; is the memory address I use to communicate with the ESP.&lt;/p&gt;
&lt;p&gt;Getting fast two-way data was a bit of a problem though since the two shift registers need different SPI modes and switching the modes caused too much of a delay to be work well. To overcome this problem I used one of the ESP's GPIOs as a "ready" flag that the GameBoy could read from the least significant bit at address &lt;code&gt;$B000&lt;/code&gt;. A 0 (the ready pin is low) meant the ESP wasn't ready for data. A 1 (the ready pin is high) meant the ESP was ready. In my tests I would also have the ESP load up the SIPO before setting ready to 1.&lt;/p&gt;
&lt;p&gt;Surprisingly this didn't work!&lt;/p&gt;
&lt;p&gt;At least when I had code that looked like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    ld [rESP], a
.wait
    ld a, [rESP_READY]
    and 1
    jr nz, .wait
    ld a, [rESP]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It turns out that the ESP couldn't get the ready pin to 0 before the GameBoy read it! I was setting it to 0 as the first step in my interrupt handler. I really didn't expect it to miss the timing.&lt;/p&gt;
&lt;p&gt;So I switched to inverting the ready pin to indicate the ESP is ready. This means that the ESP doesn't need to do anything to indicate it isn't ready and it can definitely do nothing faster than the GameBoy! Of course it means the GameBoy needs to keep track of the last value of the ready flag, which could get annoying later.&lt;/p&gt;
&lt;h4&gt;Logic hardware&lt;/h4&gt;
&lt;p&gt;In order to control the shift registers and ESP interrupts I need to detect specific values on some of the cartridge's address pins and control pins. Specifically I need to be watching address pins A12-15 (0 based numbering) and the WR, RD, and CS pins. When those pins have the right values I'll either:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;latch the data pins into the PISO register and trigger the interrupt pin to tell the ESP it has data to look at&lt;/li&gt;
&lt;li&gt;set the output enabled on the SIPO so the GB gets the data from it and trigger the interrupt pin to tell the ESP it can send another byte&lt;/li&gt;
&lt;li&gt;get the value of the ESP's ready pin onto the data pin D0&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Sebastian solved his similar control problem with logic gate. I almost did that, in fact I bought a bunch of logic gates. I think I got lucky though, because I also bought an ATF16V8B programmable logic device (PLD)! This is a little chip with a bunch of logic gates inside and you can program it to work like an arbitrary logical expression. I bought it because I didn't know any better, it sounded like it would be really handy, and because Ben Eater's videos got me over-confident enough to think I could program anything using an arduino. I couldn't!&lt;/p&gt;
&lt;p&gt;But luck struck some more because it turns out that programming a flash memory chip with an arduino is a pain in the butt! So instead of turning that into a project I went and bought a TL866II Plus chip programmer to program my flash memory. And it just so happens that it can also program the ATF16V8B! It took a while to get it all working, but now I have the one chip to do all my hardware logic!&lt;/p&gt;
&lt;p&gt;Another detail is that when the GameBoy is reading the ready pin, I'm actually only driving D0 and leaving all the other data pins floating. This isn't a good thing to do, but nothing has broken yet. My feeling/guess is that the GameBoy hardware would have protection against a truly floating data pin but I haven't checked...&lt;/p&gt;
&lt;h4&gt;Too much power&lt;/h4&gt;
&lt;p&gt;This is one area where I'm maybe pushing my luck. You see, it turns out that the ESP's GPIO pins are &lt;a href="https://hackaday.com/2016/07/28/ask-hackaday-is-the-esp8266-5v-tolerant/"&gt;apparently 5V tolerant&lt;/a&gt;. So I don't step any of the logic down to 3.3V feeding into the ESP. Also all the chips the ESP talks to will happily register 3.3V as high (I don't remember if I checked this when buying components). It's possible that the GameBoy might not be happy with 3.3V but the ESP never talks directly to the GameBoy. Even the ESP's ready pin goes through the PLD. This saved me a bunch of circuitry and components and so far nothing's exploded!&lt;/p&gt;
&lt;h1&gt;So what's the state of the project?&lt;/h1&gt;
&lt;p&gt;Well, it's not done...&lt;/p&gt;
&lt;p&gt;It's actually working though, and I've run some simple tests of communicating between the GameBoy and the ESP. I've also done some &lt;strong&gt;VERY&lt;/strong&gt; simple demos using the WiFi (I made a cloud based increment operator).&lt;/p&gt;
&lt;p&gt;I was just getting started designing the PCB, but I've never done any PCB design before so who knows how many mistakes I'll make with that.&lt;/p&gt;
&lt;p&gt;So best case scenario, if I get enough time to work on the PCB, don't make any mistakes with it, and don't destroy any of my components when soldering them I could have the hardware done in a month. That is very unrealistic!&lt;/p&gt;
&lt;p&gt;I'll keep chugging along though. Hopefully this will be a good kick in the pants to get me actually making content about it regularly!&lt;/p&gt;</content><link href="https://systemoflevers.com/blog/2021/12/16/gameboy-wifi-cart/index.html"/></entry><entry><id>https://systemoflevers.com/blog/2021/12/24/web-service-api-authentication-basics/index.html</id><title>web service api authentication basics</title><updated>2021-12-24T00:00:00-04:56</updated><content type="html">&lt;h1&gt;Web Service API Authentication Basics&lt;/h1&gt;
&lt;p&gt;This is a very simplified overview of some Web Service API auth stuff. My main reason for writing this is to have something to point to from a future post. I'm not even remotely an expert in this area but here are some basics.&lt;/p&gt;
&lt;p&gt;I'm not going to cover &lt;a href="https://en.wikipedia.org/wiki/Cross-origin_resource_sharing"&gt;CORS&lt;/a&gt;. If you want an introduction to it here's a &lt;a href="https://wizardzines.com/comics/cors/"&gt;comic&lt;/a&gt; about it from &lt;a href="https://wizardzines.com/"&gt;Wizard Zines&lt;/a&gt;.&lt;/p&gt;
&lt;h1&gt;What Are Web Service APIs&lt;/h1&gt;
&lt;p&gt;I'm talking about APIs that you interact with over the internet using HTTP, not local ones (like a library's API). Basically a website but meant for machines instead of humans. You'll have a URL that you can make a request to and get a response. Often the response will be in &lt;a href="https://en.wikipedia.org/wiki/JSON"&gt;JSON&lt;/a&gt;. For example if you make a request to &lt;a href="https://cat-fact.herokuapp.com/facts/random"&gt;https://cat-fact.herokuapp.com/facts/random&lt;/a&gt; you'll get a JSON object with a random cat fact! You can try it by navigating to it with your browser or if you're on a machine with &lt;a href="https://en.wikipedia.org/wiki/CURL"&gt;&lt;code&gt;curl&lt;/code&gt;&lt;/a&gt; you can run the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-shell"&gt;curl https://cat-fact.herokuapp.com/facts/random
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Categories of APIs&lt;/h2&gt;
&lt;p&gt;For the topic of authentication I'm going to say there are three categories of these APIs.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Open/Public&lt;/li&gt;
&lt;li&gt;API-Key Based&lt;/li&gt;
&lt;li&gt;Token Based&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Open/Public&lt;/h3&gt;
&lt;p&gt;Open/Public APIs are ones that are URLs that you can make requests to without asking anyone for permission. These APIs are unauthenticated and have no sense of who's accessing them, like a user or account. This means these APIs are usually for fetching data and don't give you a way of changing/uploading/posting anything. It's not usually a good idea to let the anonymous public modify your service.&lt;/p&gt;
&lt;p&gt;The random cat-fact API is an example of an open API. It's just there, you can get cat facts whenever you want. If you want you could build an app that just serves cat facts fetched from that API. You probably shouldn't make it an important part of your infrastructure though because the cat-facts service probably doesn't have the resources for such a thing.&lt;/p&gt;
&lt;h3&gt;API-Key Based&lt;/h3&gt;
&lt;p&gt;API-key based APIs are ones that require you to have a secret value called an API-key that you either include in the request's URL, a request header, or as part of the payload. An API-key is tied to an account you have with the service and you'll usually get it from the service's website. For example IFTTT Webhooks are like this. &lt;/p&gt;
&lt;p&gt;Once you have an API-key these APIs are basically work like an open/public API except you'll have to include the key in your request. In fact you can make any API-Key based API into a public API just by publicizing you API-key! That's probably not a good idea though since the service may revoke your key or even charge you for usage. That's one of the downsides of API-keys, they need to stay secret and you need to trust anyone you're sharing it with. Once you've shared it with someone the only way to stop them from using it is to revoke/regenerate your API-key. This will break everyone else who's using the key and you'll need to update it. For example if you've setup a bunch of ESP8266's to send requests to an IFTTT webhook. But you also shared your webhook url (with the key) with your good buddy Jerk-O. In a shocking turn of events Jerk-O turned out to be a jerk and started using it to troll. The only way to stop them is to regenerate your key and go and reprogram/update all your ESPs.&lt;/p&gt;
&lt;p&gt;OAuth has a concept of "bearer tokens" that seems similar to an API-key.&lt;/p&gt;
&lt;h3&gt;Token-Based APIs&lt;/h3&gt;
&lt;p&gt;I'm going to be very vague here. I don't have deep understanding of this stuff. Auth is an area where mistakes can be bad and thinking you know more than you do can lead to mistakes. So if this stuff is important to you maybe find a better source than some guy's blog with two posts (including this one)!&lt;/p&gt;
&lt;p&gt;Access tokens are a piece of data that describes access permissions to a resource. Kiiinda like API-keys! The difference tends to be that an access token has metadata that describes what it can be used to access and for how long. There also tends to be mechanism to for a user, who we'll call Jesse, who has an account on one service, say twitter, to give a token to another service, like a GameBoy twitter client, that will let the GameBoy interact with twitter on Jesse's behalf. For example to like a tweet! If you've ever been using an app or website and suddenly get a little pop-up that says "hey do you want example.com to have access to your google calendar?" with your google account photo and such then this is what's happening. It's using OAuth2 to get an access token so that example.com can access your calendar.&lt;/p&gt;
&lt;p&gt;There also tends to be cryptography involved! The tokens will have cryptographic properties, like a &lt;a href="https://en.wikipedia.org/wiki/Digital_signature"&gt;signature&lt;/a&gt;, so that they can be verified without just needing a list of all the tokens that have been issued.&lt;/p&gt;</content><link href="https://systemoflevers.com/blog/2021/12/24/web-service-api-authentication-basics/index.html"/></entry><entry><id>https://systemoflevers.com/blog/2022/01/20/gameboy-twitter-client-proof-of-concept/index.html</id><title>gameboy twitter client proof of concept</title><updated>2022-01-20T00:00:00-04:56</updated><content type="html">&lt;h1&gt;GameBoy Twitter Client Proof-of-Concept&lt;/h1&gt;
&lt;p&gt;I didn't have an interesting demo of my &lt;a href="/blog/2021/12/16/gameboy-wifi-cart/"&gt;WiFi GameBoy cartridge&lt;/a&gt;. I decided that it would be fun to use it to &lt;em&gt;like&lt;/em&gt; &lt;a href="https://twitter.com/diconx"&gt;@diconx&lt;/a&gt;'s &lt;a href="https://twitter.com/diconx/status/1471428560846000128"&gt;tweet&lt;/a&gt; about his &lt;a href="https://there.oughta.be/a/wifi-game-boy-cartridge"&gt;WiFi cartridge&lt;/a&gt; and &lt;a href="https://twitter.com/systemoflevers/status/14726604214632202at"&gt;tweet a video&lt;/a&gt; of it at him!&lt;/p&gt;
&lt;p&gt;Of course up to this point I've just been sending handfuls of bytes between the GameBoy and ESP so that I could figure out the hardware and basic communication protocol. Everything seemed to be working reliably though and I figured there was probably a Twitter API library for the ESP8266. How hard could it be? Actually not that hard but I made some silly mistakes and banged my head against the Twitter API a bit.&lt;/p&gt;
&lt;h1&gt;What Is It&lt;/h1&gt;
&lt;p&gt;A proof-of-concept Twitter Client for the GameBoy. It's able to load a tweet and render it on the GameBoy. A user can then &lt;em&gt;like&lt;/em&gt; that tweet by pressing right and the A-button.&lt;/p&gt;
&lt;p&gt;There's no way to change what tweet is viewed. The tweet ID is hardcoded but the tweet content is loaded using the Twitter API. There's no way to unlike the tweet, or to show if the tweet is currently liked. You also can't write a tweet. Also the auth tokens are hardcoded so it can only make me like the tweet!&lt;/p&gt;
&lt;h1&gt;How Does It Work&lt;/h1&gt;
&lt;p&gt;There are three parts: &lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A web service that talks to the Twitter api and formats the tweet text for easier rendering on the GameBoy.&lt;/li&gt;
&lt;li&gt;An ESP8266 that talks to the web service.&lt;/li&gt;
&lt;li&gt;A GameBoy ROM that renders the tweet and handles user input.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You can find the code for it &lt;a href="https://github.com/systemoflevers/gb-twitter"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Web Service&lt;/h2&gt;
&lt;p&gt;In principle this isn't needed. The ESP could be made to talk directly to the Twitter API. I was trying to get this done quickly though, and I was having trouble working out the right authentication steps so I figured doing it in python with a mature library would be easier. It means that the ESP code can be a lot simpler and I was able to do some pre-processing of the tweet text to make it much simpler for the GameBoy to render.&lt;/p&gt;
&lt;h3&gt;Talking to Twitter&lt;/h3&gt;
&lt;p&gt;Twitter has an API! To use it you'll need to setup a new app at &lt;a href="https://apps.twitter.com/app/new"&gt;https://apps.twitter.com/app/new&lt;/a&gt;. Then you'll be able to get credentials that you can use to talk to twitter. If you're not familiar with web API authentication I wrote a &lt;a href="/blog/2021/12/24/web-service-api-authentication-basics"&gt;brief overview&lt;/a&gt; covering some of the basics. You can also check out the &lt;a href="https://developer.twitter.com/en/docs/authentication/oauth-2-0/user-access-token"&gt;twitter docs&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;To talk to the Twitter API from my web service I used the &lt;a href="https://www.tweepy.org/"&gt;Tweepy&lt;/a&gt; library. I'm using the Twitter V2 API so I used the &lt;a href="https://docs.tweepy.org/en/stable/client.html"&gt;&lt;code&gt;tweepy.Client&lt;/code&gt;&lt;/a&gt; class. You can follow their &lt;a href="https://docs.tweepy.org/en/stable/auth_tutorial.html"&gt;authentication tutorial&lt;/a&gt; to get the right tokens to have the service act on your behalf. You'll have to do the OAuth dance to get them. Using the &lt;code&gt;consumer_key&lt;/code&gt; and &lt;code&gt;consumer_secret&lt;/code&gt; that you get from the Twitter dev portal for your app.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;auth = tweepy.OAuthHandler(consumer_key, consumer_secret)
print(auth.get_authorization_url())
# Visit that URL and take tone of the oauth token and verifier code. Fill them
# in in the following lines.

auth.request_token = { 'oauth_token' : &amp;quot;[TOKEN]&amp;quot;,
                       'oauth_token_secret' : &amp;quot;[VERIFIER]&amp;quot; }
auth.get_access_token(&amp;quot;[VERIFIER]&amp;quot;)
print(auth.access_token)
print(auth.access_token_secret)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Save the &lt;code&gt;access_token&lt;/code&gt; and &lt;code&gt;access_token_secret&lt;/code&gt; and fill those in in the web service code. Now the web service will be able to access twitter on your behalf.&lt;/p&gt;
&lt;h3&gt;Formatting Tweets&lt;/h3&gt;
&lt;p&gt;Since I was already using python to fetch the tweet I decided to also do some pre-processing of the tweet to make it easier for the GameBoy to display it. The web service does three things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Make all lines exactly 20 characters, padding with spaces if needed.&lt;/li&gt;
&lt;li&gt;Make all lines start and end with a space to give a blank border on either side when displayed on the GameBoy.&lt;/li&gt;
&lt;li&gt;Avoid having a line break in the middle of a word when possible.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This makes the GameBoy code to display the tweet much simpler.&lt;/p&gt;
&lt;h2&gt;ESP Code&lt;/h2&gt;
&lt;p&gt;The ESP is a Wemos D1 mini programmed with Arduino. It communicates with the GameBoy using a pair of shift registers, which are normally connected to the ESP's SPI bus but I had them wired up differently while experimenting and using Arduino's &lt;code&gt;shiftIn()&lt;/code&gt; and &lt;code&gt;shiftOut()&lt;/code&gt; functions. I hadn't realized that at first and spent a bunch of time debugging why nothing was working! A few more details about this hardware setup can be found in my &lt;a href="blog/2021/12/16/gameboy-wifi-cart"&gt;first post&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The ESP takes care of fetching the tweet for the hardcoded tweet-ID, sending it to the GameBoy, and waiting for input to &lt;em&gt;like&lt;/em&gt; the tweet. This is managed with a sequence of states:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;kConnecting&lt;/code&gt;&lt;ol&gt;
&lt;li&gt;Send the byte value &lt;code&gt;255&lt;/code&gt; to the GameBoy (the actual value is ignored).&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kConnected&lt;/code&gt;&lt;ol&gt;
&lt;li&gt;Wait for the GameBoy to read the sent byte (this is just to synchronize with the GameBoy).&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kGettingTweet&lt;/code&gt;&lt;ol&gt;
&lt;li&gt;Request the tweet from the web service.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kSendingToGB&lt;/code&gt;&lt;ol&gt;
&lt;li&gt;Send the tweet to the GameBoy 1 character byte at a time (assumes ASCII).&lt;/li&gt;
&lt;li&gt;When done, send the value &lt;code&gt;0&lt;/code&gt; to indicate the end.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kWaitingForLike&lt;/code&gt;&lt;ol&gt;
&lt;li&gt;Wait until the GameBoy sends a byte.&lt;/li&gt;
&lt;li&gt;When it does, send &lt;em&gt;like&lt;/em&gt; request to the web service.&lt;/li&gt;
&lt;li&gt;When that completes send a byte to the GameBoy to indicate it's done. &lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kDone&lt;/code&gt;&lt;ol&gt;
&lt;li&gt;do nothing.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Apparently I never used &lt;code&gt;kConnecting&lt;/code&gt;, oops! Basically this state is entirely handled by the setup function, and once it's out of there it's assumed to be connected.&lt;/p&gt;
&lt;h2&gt;GameBoy Code&lt;/h2&gt;
&lt;p&gt;The GameBoy code is written in assembly. Due to the limited nature of the proof of concept it fairly straight forward. It basically runs in lockstep with the esp code. It also displays some text so I know what step of the initialization it's on. There are two things that help keep the GameBoy and ESP in sync. The GameBoy can read a "status-bit" from the least-significant bit at address &lt;code&gt;$B000&lt;/code&gt;. This is how the ESP tells the GameBoy it's ready. The other way is for the GameBoy to read from or write to &lt;code&gt;$A000&lt;/code&gt;, which I call the ESP register, or &lt;code&gt;rESP&lt;/code&gt;. The ESP receives an interrupt for a read from or write to that address (separate ones for read/write).&lt;/p&gt;
&lt;p&gt;The sequence is as follows:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Do some display related initialization.&lt;/li&gt;
&lt;li&gt;Copy over the string "Connecting..".&lt;/li&gt;
&lt;li&gt;Wait for the ESP to indicate it's ready, while blinking the last "." in "Connecting..".&lt;/li&gt;
&lt;li&gt;Display "Connected!!" for a few frames.&lt;/li&gt;
&lt;li&gt;Ready a byte from the ESP to tell it to continue.&lt;/li&gt;
&lt;li&gt;Display "Loading.." with animated ".".&lt;/li&gt;
&lt;li&gt;Wait for the ESP to indicate it's ready.&lt;/li&gt;
&lt;li&gt;Copy the tweet from the ESP and display it.&lt;/li&gt;
&lt;li&gt;Display a little bar at the bottom with an empty heart in heart.&lt;/li&gt;
&lt;li&gt;Wait for the the right-button to be pressed.&lt;/li&gt;
&lt;li&gt;Display an arrow icon next to the heart.&lt;/li&gt;
&lt;li&gt;Wait for the A-button to be pressed.&lt;/li&gt;
&lt;li&gt;Send the byte for the ASCII character "L" to the GameBoy.&lt;/li&gt;
&lt;li&gt;Wait for the ESP to indicate it's ready, which the GameBoy assumes means it finished liking the tweet.&lt;/li&gt;
&lt;li&gt;Read a byte from the ESP to acknowledge.&lt;/li&gt;
&lt;li&gt;Fill in the heart icon to indicate the tweet was liked.&lt;/li&gt;
&lt;li&gt;Loop forever.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The text (and any graphics) can only be updated when the GameBoy's display hardware isn't accessing video RAM (VRAM). You can find more details about this in the Pan Docs' &lt;a href="https://gbdev.io/pandocs/Rendering.html"&gt;rendering section&lt;/a&gt;. The section on the &lt;a href="https://gbdev.io/pandocs/STAT.html"&gt;LCD status register&lt;/a&gt; has a good overview of what parts of VRAM are accessible when and the section on the &lt;a href="https://gbdev.io/pandocs/pixel_fifo.html"&gt;pixel FIFO&lt;/a&gt; gives details about timing.&lt;/p&gt;
&lt;p&gt;Since I didn't need precise screen updates I did all my updates during VBlank, which is a chunk of time after the GameBoy has finished drawing an entire frame. The GameBoy has an interrupt to tell you when VBlank is. It also has a &lt;code&gt;halt&lt;/code&gt; command that will stop code execution until an interrupt occurs. So VBlank interrupt handler just returns immediately and I have &lt;code&gt;halt&lt;/code&gt;s whenever I need to wait for a VBlank. There are other &lt;a href="https://gbdev.io/pandocs/Interrupt_Sources.html"&gt;types of interrupts&lt;/a&gt; but I only have the VBlank one turned on.&lt;/p&gt;
&lt;p&gt;For displaying the Tweet, rather than worry about fitting it all in VBlank I just turn off the the display. &lt;strong&gt;Only do this during VBlank see &lt;a href="https://gbdev.io/pandocs/LCDC.html#lcdc7---lcd-enable"&gt;here&lt;/a&gt; for details.&lt;/strong&gt; Once the display is off I can copy everything over and turn it on.&lt;/p&gt;</content><link href="https://systemoflevers.com/blog/2022/01/20/gameboy-twitter-client-proof-of-concept/index.html"/></entry><entry><id>https://systemoflevers.com/blog/2022/03/01/named-pipes-in-python/index.html</id><title>Named Pipes to Turn CLI Programs Into Python Functions</title><updated>2022-03-01T00:00:00-04:56</updated><content type="html">&lt;h2&gt;Tl;dr&lt;/h2&gt;
&lt;p&gt;A named pipe is like a file that doesn't store anything. It has a path (the name I guess). It can be opened, read from, and written to, but the content is temporary and stored in memory.&lt;/p&gt;
&lt;p&gt;Since a named pipe is also a pipe it acts a bit different than a normal file. When you open it you can only open it as read-only or write-only not read-write. The idea is you'd have one process with it open for write (the producer) and one process with it open for read (the consumer). This matches a pipe since they have a read-end and a write-end. At least in Unix.&lt;/p&gt;
&lt;p&gt;A call to &lt;code&gt;open&lt;/code&gt; for read or write will block until something else opens it for the other. Calls to read or write will also block if the named pipe is empty or full respectively. &lt;/p&gt;
&lt;p&gt;Named pipes can be useful if you need to create CLI pipelines with programs that consume or produce multiple inputs and outputs. You have to be careful not to create deadlocks due to the blocking behaviour though. You also can't use this with a program that needs to seek in a file or that reads and writes the same file.&lt;/p&gt;
&lt;p&gt;I'm using them to run file based CLI programs from Python without writing to the disk.&lt;/p&gt;
&lt;h2&gt;What I Was Trying To Do&lt;/h2&gt;
&lt;p&gt;I'm using two command line programs &lt;code&gt;rgbasm&lt;/code&gt; and &lt;code&gt;rgblink&lt;/code&gt;. They're part of the &lt;a href="https://rgbds.gbdev.io/"&gt;RGBDS&lt;/a&gt; GameBoy assembler toolchain. If I were running them from the command line I would use the following two commands:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;rgbasm A.asm -o A.o
rgblink A.o B.o -o A.gb -m A.map -n A.sym
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I want my Python script to be able to provide &lt;code&gt;A.asm&lt;/code&gt; and get back the contents of &lt;code&gt;A.gb&lt;/code&gt;, &lt;code&gt;A.map&lt;/code&gt;, and &lt;code&gt;A.sym&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;So I can just have my script write &lt;code&gt;A.asm&lt;/code&gt;, use &lt;code&gt;subprocess.run()&lt;/code&gt; to run the two commands then read in the three files!&lt;/p&gt;
&lt;h3&gt;Unnecessary Constraints!&lt;/h3&gt;
&lt;p&gt;I didn't want to write anything to disk.&lt;/p&gt;
&lt;h4&gt;Why?&lt;/h4&gt;
&lt;p&gt;My justification was that I want to run this in a server and having it writing to and reading from the disk felt wrong. That's a bad reason though. This is for a personal toy project. Making scalable, production quality software is not the goal! I'm not saying I achieved scalable, production quality software though, just that it's not the goal.&lt;/p&gt;
&lt;p&gt;The actual reason was that I thought it would be possible to do and I wanted to figure out how. That's a good reason! At least for a personal toy project. Even if it is a bit of a detour.&lt;/p&gt;
&lt;h2&gt;What Did I Know Going In&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Linux stuff&lt;ul&gt;
&lt;li&gt;pipes exist&lt;/li&gt;
&lt;li&gt;you can use them to route stdin/out/err from one program to another programs&lt;/li&gt;
&lt;li&gt;files exist&lt;/li&gt;
&lt;li&gt;everything is a file: directories, devices, programs, you, the internet, the computer, your home, the love you feel for others, this list entry&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Python&lt;ul&gt;
&lt;li&gt;normal file stuff&lt;/li&gt;
&lt;li&gt;how to use the subprocess module to run programs&lt;/li&gt;
&lt;li&gt;I can yell &lt;code&gt;PIPE&lt;/code&gt; to communicate with subprocesses over stdin/out/err&lt;/li&gt;
&lt;li&gt;some thread stuff&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Named Pipes / FIFOs&lt;/h2&gt;
&lt;p&gt;I quickly came across named pipes and how to create them in Python with &lt;code&gt;os.mkfifo&lt;/code&gt; or from the command line with &lt;code&gt;mkfifo&lt;/code&gt; (I don't know about windows though).  After being briefly distracted by going overboard with context managers to create and delete a temp directory and a bunch of FIFOs I ran into my first gotcha. My program froze when I tried to open a FIFO.&lt;/p&gt;
&lt;p&gt;The problem was that I was only opening one end of the FIFO/pipe and that will block until the other end is opened. &lt;/p&gt;
&lt;p&gt;This was lucky! Confusing, but lucky! You see I didn't know what I was doing and I very well could have accidentally written my code in a way that missed this problem. Then I never would have learned that opening a FIFO/named pipe will block unless the other end of also open. I also wouldn't have realized that I knew less what I was doing than usual and I'd need to pay more attention.&lt;/p&gt;
&lt;p&gt;You can use &lt;code&gt;os.open()&lt;/code&gt; to open without blocking and that's what I did at first. Something like this (rough memory probably not exactly what I did):&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;def opener(path, flags):
    return os.open(path, flags | os.O_NONBLOCK)

with open('fifo_in', 'w', opener=opener) as wf, \
         open('fifo_out', 'rb', opener=opener) as rf:
    wf.write(input_data)
    p = subprocess.Popen(['rgbasm', 'fifo_in', '-o', 'fifo_middle'])
    subprocess.run(['rgblink', 'fifo_middle', '-o', 'fifo_out'])
    p.wait()
    output = rf.read()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I was surprised when it worked. But I'm pretty sure this can deadlock in a few ways though. Here's what's happening:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;There are three named pipes:&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fifo_in&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fifo_middle&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fifo_out&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fifo_in&lt;/code&gt; and &lt;code&gt;fifo_out&lt;/code&gt; are used to get data into &lt;code&gt;rgbasm&lt;/code&gt; and &lt;code&gt;rgblink&lt;/code&gt; respectively&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fifo_middle&lt;/code&gt; is used to get data from &lt;code&gt;rgbasm&lt;/code&gt; into &lt;code&gt;rgblink&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fifo_in&lt;/code&gt; and &lt;code&gt;fifo_out&lt;/code&gt; are opened by the Python code using a specially defined opener function. &lt;code&gt;fifo_out&lt;/code&gt; is opened as binary because it contains binary data.&lt;/li&gt;
&lt;li&gt;Some data is written to &lt;code&gt;fifo_in&lt;/code&gt;. This is the input to &lt;code&gt;rgbasm&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rgbasm&lt;/code&gt; is run using &lt;code&gt;subprocess.Popen()&lt;/code&gt; so Python doesn't block waiting for it to complete.&lt;ul&gt;
&lt;li&gt;&lt;code&gt;rgbasm&lt;/code&gt; itself will be blocked when it tries to open &lt;code&gt;fifo_middle&lt;/code&gt; because (presumably) it's trying to open one end of a named pipe with a normal open without the &lt;code&gt;O_NONBLOCK&lt;/code&gt; flag. So &lt;code&gt;rgbasm&lt;/code&gt; won't finish until something opens &lt;code&gt;fifo_middle&lt;/code&gt; for reading. &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rgblink&lt;/code&gt; is run using &lt;code&gt;subprocess.run()&lt;/code&gt;. Python will block (wait) until it completes before moving on.&lt;ul&gt;
&lt;li&gt;&lt;code&gt;rgblink&lt;/code&gt; will open &lt;code&gt;fifo_middle&lt;/code&gt; for reading and &lt;code&gt;fifo_out&lt;/code&gt; for writing. Neither of these blocks because &lt;code&gt;fifo_middle&lt;/code&gt; is already opened for writing by &lt;code&gt;rgbasm&lt;/code&gt; and &lt;code&gt;fifo_out&lt;/code&gt; is already open for reading by the python code.&lt;/li&gt;
&lt;li&gt;This unblocks &lt;code&gt;rgbasm&lt;/code&gt; and it can start producing output and everything move along.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;I added a &lt;code&gt;p.wait()&lt;/code&gt; just to make sure &lt;code&gt;rgbasm&lt;/code&gt; was done.&lt;/li&gt;
&lt;li&gt;Finally I read the output!&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;So What's the Problem?&lt;/h3&gt;
&lt;p&gt;For one thing, it only works because the input isn't very big! Pipes/named-pipes/FIFOs have a maximum capacity. If you try to write to one when it's full then it will block or fail if &lt;code&gt;O_NONBLOCK&lt;/code&gt; is set. Reading from one that's empty will have similar results. There's more info in the &lt;a href="https://man7.org/linux/man-pages/man7/pipe.7.html"&gt;man(7) page&lt;/a&gt; in the &lt;strong&gt;I/O on pipes and FIFOs&lt;/strong&gt;, &lt;strong&gt;Pipe capacity&lt;/strong&gt;, and &lt;strong&gt;PIPE_BUF&lt;/strong&gt; sections. So if the input data were bigger then the python script would crash on &lt;code&gt;wf.write(input_data)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Similarly, if the output were too big then &lt;code&gt;rgblink&lt;/code&gt; would block when it tries to write to a full pipe. This means the python script would be blocked from reading anything from the pipe so everything will just be stuck!&lt;/p&gt;
&lt;h2&gt;Maybe Don't Use &lt;code&gt;O_NONBLOCK&lt;/code&gt;?&lt;/h2&gt;
&lt;p&gt;Ok, I could change it to something like:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;p_rgbasm = subprocess.Popen(['rgbasm', 'fifo_in', '-o', 'fifo_middle'])
p_rgblink = subprocess.Popen(['rgblink', 'fifo_middle', '-o', 'fifo_out'])
with open('fifo_in', 'w') as wf, \
         open('fifo_out', 'rb') as rf:
   wf.write(input_data)
   p_rgblink.wait()
   output = rf.read()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we're not using &lt;code&gt;O_NONBLOCK&lt;/code&gt; so at least reads and writes won't fail in the Python code. I even think this might work for this case! Here's what I think is happening:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;rgbasm&lt;/code&gt; and &lt;code&gt;rgblink&lt;/code&gt; are started and running in the background. They're both definitely blocked though since they need to open &lt;code&gt;fifo_in&lt;/code&gt; and &lt;code&gt;fifo_out&lt;/code&gt; respectively, and their other ends aren't open yet.&lt;/li&gt;
&lt;li&gt;I open the other ends of&lt;code&gt;fifo_in&lt;/code&gt; and &lt;code&gt;fifo_out&lt;/code&gt; so &lt;code&gt;rgbasm&lt;/code&gt; &lt;code&gt;rgblink&lt;/code&gt; &lt;em&gt;should&lt;/em&gt; be unblocked.&lt;ul&gt;
&lt;li&gt;They'll both still get blocked when they try to read from empty pipes (&lt;code&gt;rgbasm&lt;/code&gt; from &lt;code&gt;fifo_in&lt;/code&gt; and &lt;code&gt;rgblink&lt;/code&gt; from &lt;code&gt;fifo_middle&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;I write to &lt;code&gt;fifo_in&lt;/code&gt;. Now &lt;code&gt;rgbasm&lt;/code&gt; is unblocked and can read from &lt;code&gt;fifo_in&lt;/code&gt;. It should also be writing to &lt;code&gt;fifo_middle&lt;/code&gt; so &lt;code&gt;rgblink&lt;/code&gt; isn't blocked and it can read and start producing output and writing to &lt;code&gt;fifo_out&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;I wait for &lt;code&gt;rgblink&lt;/code&gt; to be done.&lt;/li&gt;
&lt;li&gt;I read the output from &lt;code&gt;fifo_out&lt;/code&gt;. This will unblock &lt;code&gt;rgblink&lt;/code&gt; if &lt;code&gt;fifo_out&lt;/code&gt; got full...&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Oh shoot&lt;/strong&gt;, I never get to &lt;code&gt;rf.read()&lt;/code&gt; when &lt;code&gt;fifo_out&lt;/code&gt; is full because I'm still waiting on &lt;code&gt;p_rgblink.wait()&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I could probably fix this by getting rid of &lt;code&gt;p_rgblink.wait()&lt;/code&gt; but I don't actually know if &lt;code&gt;rf.read()&lt;/code&gt; will always return the entire output. I think it will as long as &lt;code&gt;rgblink&lt;/code&gt; doesn't do something strange like close the file and reopen it to write some more. The reason I think this is because &lt;code&gt;read()&lt;/code&gt; will read until it hits an end-of-file. I think that only happens when all the write-ends of the pipe are closed.&lt;/p&gt;
&lt;p&gt;Anyway we have other problems.&lt;/p&gt;
&lt;h2&gt;Other Problems&lt;/h2&gt;
&lt;p&gt;The Python script could have blocked on &lt;code&gt;open('fifo_in', 'w')&lt;/code&gt; because we have no guarantee about how &lt;code&gt;rgbasm&lt;/code&gt; and &lt;code&gt;rgblink&lt;/code&gt; open files. For example if:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;rgbasm&lt;/code&gt; opens them in the order&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;fifo_middle&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fifo_in&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;and &lt;code&gt;rgblink&lt;/code&gt; opens its files in the order&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;fifo_out&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fifo_middle&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So now we're in a deadlock!&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The Python script is blocked waiting for &lt;code&gt;rgbasm&lt;/code&gt; to open &lt;code&gt;fifo_in&lt;/code&gt;. &lt;/li&gt;
&lt;li&gt;&lt;code&gt;rgbasm&lt;/code&gt; won't open &lt;code&gt;fifo_in&lt;/code&gt; since it's blocked waiting for &lt;code&gt;rgblink&lt;/code&gt; to open &lt;code&gt;fifo_middle&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rgblink&lt;/code&gt; won't open &lt;code&gt;fifo_middle&lt;/code&gt; since it's blocked waiting for the Python script to open &lt;code&gt;fifo_out&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The Python script won't open &lt;code&gt;fifo_out&lt;/code&gt; because of point 1!&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Grumble&lt;/h2&gt;
&lt;p&gt;Maybe there's something in &lt;a href="https://docs.python.org/3/library/asyncio.html"&gt;asyncio&lt;/a&gt; that could help me?&lt;/p&gt;
&lt;p&gt;Ok, no, that's more complicated than I thought it would be and I don't see a magic wand for this anyway.&lt;/p&gt;
&lt;h2&gt;Fine I'll Use Threads&lt;/h2&gt;
&lt;p&gt;I already have deadlocks. I can't make it much worse right?&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;import threading
import subprocess
import io

def doRGBStuff(input_data):
    def do_write(file_name, data):
        with open(file_name', 'w') as wf:
            wf.write(data)

    def do_read(file_name, out_stream):
        with open(file_name, 'rb') as rf:
            out_stream.write(rf.read())

    write_thread = threading.Thread(target=do_write, args=('fifo_in', input_data))
    write_thread.start()

    p_rgbasm = subprocess.Popen(['rgbasm', 'fifo_in', '-o', 'fifo_middle'])
    p_rgblink = subprocess.Popen(['rgblink', 'fifo_middle', '-o', 'fifo_out'])

    out_stream = io.BytesIO()
    read_thread = threading.Thread(target=do_read, args=('fifo_out', out_stream))
    read_thread.start()
    read_thread.join()
    return out_stream.getvalue()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we have &lt;code&gt;write_thread&lt;/code&gt; to write data into &lt;code&gt;fifo_in&lt;/code&gt; and &lt;code&gt;read_thread&lt;/code&gt; to read from &lt;code&gt;fifo_out&lt;/code&gt; and put it into an &lt;code&gt;io.BytesIO&lt;/code&gt; stream. Everything that interacts with the fifos is either a thread or subprocess so when they block it won't block the main Python script. The script blocks at the end with &lt;code&gt;read_thread.join()&lt;/code&gt;. This should wait until &lt;code&gt;rgblink&lt;/code&gt; finishes writing its output. It still won't work properly if &lt;code&gt;rgblink&lt;/code&gt; closes and reopens the output file in the middle.&lt;/p&gt;
&lt;p&gt;I can't think of anything that would let this deadlock, but maybe I'm missing something.&lt;/p&gt;
&lt;p&gt;To complete the original task of getting three outputs I'll just need two more fifos and reading threads.&lt;/p&gt;
&lt;p&gt;Also I haven't tested any of the specific code in this post! It's based on some other code that &lt;em&gt;DOES&lt;/em&gt; work though!&lt;/p&gt;</content><link href="https://systemoflevers.com/blog/2022/03/01/named-pipes-in-python/index.html"/></entry><entry><id>https://systemoflevers.com/blog/2023/a-big-gb-game/index.html</id><title>A Very Big GameBoy "Game"</title><updated>2023-05-12T00:00:00-04:56</updated><content type="html">&lt;p&gt;I made a big GameBoy "game"! Here's the &lt;a href="https://github.com/systemoflevers/scrolling_all_directions"&gt;code&lt;/a&gt; including the &lt;a href="https://github.com/systemoflevers/scrolling_all_directions/raw/main/scrolling.gb"&gt;ROM&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;All you can do is scroll around to see the level. It only has one level and it's 32,766x16,384 pixels big!&lt;/p&gt;
&lt;p&gt;It looks like this:
&lt;span class="smallimg pixelimg"&gt;
&lt;img alt="a screenshot from my big &amp;quot;game&amp;quot;" src="https://systemoflevers.com/blog/2023/a-big-gb-game/bgb00010.png" /&gt;
&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;Ok not very exciting.&lt;/p&gt;
&lt;p&gt;Let's see how it works!&lt;/p&gt;
&lt;h1&gt;How it Works&lt;/h1&gt;
&lt;p&gt;I'm not using any compression or in-game procedural generation for the level map so the entire 32,768x16,384 pixel level is stored entirely in the game ROM or on the cartridge if you were to put it on a cartridge for some reason. The GameBoy uses tile-based graphics with 8x8 tiles. So the level is 4,096x2,048 tiles big.&lt;/p&gt;
&lt;p&gt;The game is designed to use an &lt;a href="https://gbdev.io/pandocs/MBC5.html"&gt;MBC5&lt;/a&gt; type cartridge. I chose that type of cartridge because it's allows the biggest ROM size for standard cartridge size, Giving me 8MB to work with! It supports 512 ROM banks, each 16KB.&lt;/p&gt;
&lt;p&gt;To make things simple bank 0 is only used for code and the tile graphics. The map data is in the other 511 banks.&lt;/p&gt;
&lt;p&gt;The map data is made up of 1-byte tile indexes that say what tile goes where. As you move around the level the game copies those indexes from the game ROM to the tile map in the GameBoy's VRAM.&lt;/p&gt;
&lt;p&gt;So the real problem is to know where to copy from and where to copy to. I ended up solving that problem by keeping track of what part of the level map was on screen with two 16-bit variables: &lt;code&gt;vWorldColumn&lt;/code&gt; and &lt;code&gt;vWorldRow&lt;/code&gt;. I used those two variables track what tile in the world/level map is being displayed in the top left corner of the screen. The trick was that I could use a world column and row coordinate to get the ROM address of the needed tile map entry, and the address of where it goes in the VRAM tile map.&lt;/p&gt;
&lt;h2&gt;Why it's Complicated&lt;/h2&gt;
&lt;p&gt;If you've ever done any programming where you've had to switch between 2D array indexing and 1D array indexing this might not sound very complicated. If you know the 2D array's dimensions &lt;code&gt;W&lt;/code&gt;x&lt;code&gt;H&lt;/code&gt; and you want the value at some 2D index &lt;code&gt;x,y&lt;/code&gt; then you can do some math:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;index = x + y * W
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and that gives you the 1D index. So that would let you get the ROM address of the map data you're looking for. To get the VRAM address you could do some more math:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;romIndex = (x % 32) + (y % 32) * 32
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first problem is that the GameBoy is an 8-bit computer and those variables are 16-bits. That math isn't coming cheap. Getting the &lt;code&gt;romIndex&lt;/code&gt; would be easy enough but the &lt;code&gt;x + y * W&lt;/code&gt; would be a pain.&lt;/p&gt;
&lt;p&gt;The other problem is ROM banks. If you're not careful then you could end up doing a bunch of bank changes while copying one column of tile map entries. I'd rather avoid that.&lt;/p&gt;
&lt;h2&gt;How I Map&lt;/h2&gt;
&lt;p&gt;I got around these problems by assigning meaning to the different bits of &lt;code&gt;vWorldRow&lt;/code&gt; and &lt;code&gt;vWorldColumn&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;vWorldRow&lt;/code&gt; has the structure:&lt;/p&gt;
&lt;style&gt;
      .src-bank { color: #D81B60; }
      .src-addr { color: #1E88E5; }
&lt;/style&gt;

&lt;pre class="codehilite"&gt;
&lt;code&gt;
&lt;span class="addr-un"&gt;0000 0&lt;/span&gt;&lt;span class="src-bank"&gt;PQQ QQ&lt;/span&gt;&lt;span class="src-addr"&gt;Rr rrrr&lt;/span&gt;
&lt;/code&gt;
&lt;/pre&gt;

&lt;p&gt;and &lt;code&gt;vWorldColumn&lt;/code&gt; has the structure:&lt;/p&gt;
&lt;pre class="codehilite"&gt;
&lt;code&gt;
&lt;span class="addr-un"&gt;0000&lt;/span&gt; &lt;span class="src-bank"&gt;SSSS&lt;/span&gt; &lt;span class="src-addr"&gt;TTTt tttt&lt;/span&gt;
&lt;/code&gt;
&lt;/pre&gt;

&lt;p&gt;The first thing to notice is the leading zeros in both. &lt;code&gt;vWorldRow&lt;/code&gt; only uses the 11 least significant bits and &lt;code&gt;vWorldColumn&lt;/code&gt; uses 12. That lets me have 2,048 rows and 4,096 columns.&lt;/p&gt;
&lt;p&gt;The different letters categorize the rest of the bits into their different meanings.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;P&lt;/code&gt; and &lt;code&gt;Q&lt;/code&gt; from &lt;code&gt;vWorldRow&lt;/code&gt; and &lt;code&gt;S&lt;/code&gt; from &lt;code&gt;vWorldColumn&lt;/code&gt; are the 9 bits of the bank number:&lt;/p&gt;
&lt;pre class="codehilite"&gt;
&lt;code&gt;
0000 000&lt;span class="src-bank"&gt;P QQQQ SSSS&lt;/span&gt;
&lt;/code&gt;
&lt;/pre&gt;

&lt;p&gt;The rest are for the ROM address to where the tile map entry is stored:&lt;/p&gt;
&lt;pre class="codehilite"&gt;
&lt;code&gt;
01&lt;span class="src-addr"&gt;Rr rrrr TTTt tttt&lt;/span&gt;
&lt;/code&gt;
&lt;/pre&gt;

&lt;p&gt;The address starts with &lt;code&gt;01&lt;/code&gt; because that's the banked half of the ROM addresses (controlled by the current bank number). Address that start with &lt;code&gt;00&lt;/code&gt; are always bank 0. Address that start with &lt;code&gt;1&lt;/code&gt; aren't for ROM. &lt;/p&gt;
&lt;p&gt;The lower case letters give you the tile map entry's address in VRAM:&lt;/p&gt;
&lt;pre class="codehilite"&gt;
&lt;code&gt;
1001 10&lt;span class="src-addr"&gt;rr rrrt tttt&lt;/span&gt;
&lt;/code&gt;
&lt;/pre&gt;

&lt;p&gt;The address starts with &lt;code&gt;1001 10&lt;/code&gt; because I'm using tile map 0, which starts at &lt;code&gt;$9800&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;How Does That Compute&lt;/h3&gt;
&lt;p&gt;The easy part is getting the ROM address since the needed bits align nicely to 8-bit values. I can just take the rightmost 8 bits from &lt;code&gt;vWorldColumn&lt;/code&gt; as is to get the rightmost 8 bits of the ROM address. I can copy the rightmost 8 bits of &lt;code&gt;vWorldRow&lt;/code&gt; and set the left 2 bits to &lt;code&gt;01&lt;/code&gt; to get the leftmost 8 bits of the ROM address.&lt;/p&gt;
&lt;p&gt;Since the bank number portion of &lt;code&gt;vWorldRow&lt;/code&gt; is spread across 2 bytes it's a bit more trouble. Here's some code:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-tasm"&gt;worldToSource:
  ;; Converts vWorldColumn and vWorldRow into a copy source address and bank.
  ;; Stores the address in vCopySoure and the bank in vSourceBank.
  ;; Modifies:
  ;; A, B, C
  ;; vWorldRow has the form:
  ;;   xxxx xPQQ QQRR RRRR
  ;; vWorldColumn has the form:
  ;;   xxxx SSSS TTTT TTTT
  ;;
  ;; This function produces
  ;; vCopySource with the form:
  ;;   01RR RRRR TTTT TTTT
  ;; vSourceBank with the form:
  ;;   0000 000P QQQQ SSSS

  ;; First get the source address because it's simple.
  ldh a, [vWorldRow + 1]
  ld c, a ; need this later for the bank too
  and %00111111
  set 6, a
  ldh [vCopySource], a
  ldh a, [vWorldColumn + 1]
  ldh [vCopySource + 1], a

  ;; Next get the source bank.
  ldh a, [vWorldRow]
  and %00000111

  sla c ; Get the highest bit of the lower byte of vWorldRow in the carry flag.
  rla ; Rotate the carry flag into the higher byte of vWorldRow and get the
      ; high bit into the carry flag.
  sla c
  rla
  ;; A is now %000PQQQQ
  swap a
  ;; A is now %QQQQ000P
  ld b, a
  res 0, b ; B is now %QQQQ0000

  and %00000001 ; Clear all but P from A.
  ldh [vSourceBank], a

  ldh a, [vWorldColumn]
  and %00001111
  ;; A now has 0000 SSSS
  ;; and B has QQQQ 0000
  or b
  ;; Now A has QQQQ SSSS
  ldh [vSourceBank + 1], a
  ret
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Getting the VRAM address involves a bit of bit twiddling. Getting the right bits out of &lt;code&gt;vWorldColumn&lt;/code&gt; and &lt;code&gt;vWorldRow&lt;/code&gt; is simple enough, but the bits need to be shifted around to get the right VRAM address:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-tasm"&gt;worldToDest:
  ;; Converts vWorldColumn and vWorldRow into a copy destination based on
  ;; _SCRN0. Stores the address in vCopyDest.
  ;; Modifies:
  ;;   A, B
  ;; vWorldRow has the form:
  ;;   xxxx xPQQ QQRr rrrr
  ;; vWorldColumn has the form:
  ;;   xxxx SSSS TTTt tttt
  ;;
  ;; This function produces
  ;; vCopyDest with the form:
  ;;   1001 10rr rrrt tttt
  ldh a, [vWorldRow + 1]
  and %00011111
  sla a
  ;; A now has 00rr rrr0.
  swap a
  ld b, a ; Will use B to calculate the lower byte later.
  ;; Use A to calculate the high byte.
  and %00000011
  or HIGH(_SCRN0)
  ;; A now contains 1001 10rr.
  ld [vCopyDest], a
  ld a, b
  and %11100000
  ld b, a
  ldh a, [vWorldColumn + 1]
  and %00011111
  or b
  ld [vCopyDest + 1], a
  ret
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Problem&lt;/h3&gt;
&lt;p&gt;One problem that this scheme has is that it assumes bank 0 is usable for tile map entries but I said that I avoided putting any of that data in bank 0. To fix that I just created a big dead-zone for the part of the level that would have been stored in bank 0. I render it as just # characters. That area is 256x64 tiles big so the level is actually 32,766x16,384 - 2048x512 pixels. You can still go to that area it's just even more boring than the rest of the level.&lt;/p&gt;
&lt;h2&gt;Generating the Level&lt;/h2&gt;
&lt;p&gt;The level is generating with a &lt;a href="https://github.com/systemoflevers/scrolling_all_directions/blob/main/gen_tile_map.py"&gt;python script&lt;/a&gt;. I made it to help debug and validate the GameBoy code. It splits the level into 8 tile wide chunks and each chunk displays its row and column. That way it helps me figure out if it's loading the right tile map entries as I scroll.&lt;/p&gt;
&lt;p&gt;The code is pretty short:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-py"&gt;#!/usr/bin/python3

import random

def pad(l, length):
  needed_padding = length - len(l)
  return ([0] * needed_padding) + l


def toHexCharacters(n):
  return [int(d, 16) for d in list(hex(n))[2:]]


def main():
  tile_values_sections = []

  col_start = 0x100
  for i in range(0x4000, 0x800000, 8):
    col_addr = i &amp;amp; 0xff
    row_addr = (i &amp;gt;&amp;gt; 8) &amp;amp; 0x3f
    col_bank = (i &amp;gt;&amp;gt; 14) &amp;amp; 0xf
    row_bank = (i &amp;gt;&amp;gt; 18) &amp;amp; 0x1f
    row = (row_bank &amp;lt;&amp;lt; 6) | row_addr
    col = (col_bank &amp;lt;&amp;lt; 8) | (col_addr &amp;gt;&amp;gt; 3)
    row_digits = pad(toHexCharacters(row), 3)
    col_digits = pad(toHexCharacters(col), 3)
    tile_values_sections.append(row_digits + [16] + col_digits + [17])
  map_values = [i for c in tile_values_sections for i in c]
  map_bytes = bytes(map_values)
  with open('long_map.tilemap', 'wb') as f:
    f.write(map_bytes)


if __name__ == '__main__':
  main()
&lt;/code&gt;&lt;/pre&gt;</content><link href="https://systemoflevers.com/blog/2023/a-big-gb-game/index.html"/></entry><entry><id>https://systemoflevers.com/blog/2026/01/09/youtube-analytics/index.html</id><title>A Peak at My YouTube Analytics</title><updated>2026-01-09T00:00:00-04:56</updated><content type="html">&lt;p&gt;If you don't know I have a YouTube channel called &lt;a href="https://youtube.com/@systemoflevers"&gt;SystemOfLevers&lt;/a&gt;. This is going to be a dive into some of that channel's analytics.&lt;/p&gt;
&lt;p&gt;One thing about starting a YouTube channel that I've found challenging is that I had no context for the analytics. For the most part YouTube studio (the admin site for channels) gives you comparisons based on your own past performance. Things like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;how does the view count for the last 28-days compare to the previous 28-days&lt;/li&gt;
&lt;li&gt;how does your latest video rank against to the previous 9 videos at the same point in their lifetime&lt;/li&gt;
&lt;li&gt;is the video's retention better or worse that your typical retention&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Don't get me wrong, I definitely think comparing against yourself is the right way to go. Comparing against well established channels that have already figured things out is probably a great way to kill your motivation. As someone new to YouTube it's going to be VERY hard get close to their performance. You're just starting out, you're going to be bad at it, that's how you learn.&lt;/p&gt;
&lt;p&gt;That being said I don't think the "algorithm" cares if your latest video is the best one you've made so far. It cares how the video compares to all the other videos on YouTube. At least I think that's the case.&lt;/p&gt;
&lt;p&gt;It would be nice to have something to compare against. So this is a peak into my channel's analytics so that anyone else starting out can have at least one point of comparison.&lt;/p&gt;
&lt;h2&gt;About My Channel&lt;/h2&gt;
&lt;h3&gt;History&lt;/h3&gt;
&lt;p&gt;I published my first video in May 2022. It's still a relatively small channel. At the time of writing it has 5981 subscribers and has 13k views in the last 28-days. I've published 31 long-form videos and 11 shorts. I was fully monetized in February 2025 (that means I earn money from ads). I also got a sponsorship last year that I'm not going to discuss further.&lt;/p&gt;
&lt;p&gt;My channel has primarily focused on talking about Game Boy technical details.&lt;/p&gt;
&lt;p&gt;I got some good luck with my &lt;a href="https://youtu.be/txkHN6izK2Y"&gt;first video&lt;/a&gt;, it was picked up by &lt;a href="https://news.ycombinator.com"&gt;hacker news&lt;/a&gt; for a bit. I think my second one was too. But my 2nd-4th videos never got the same traction that the 1st one did. They were also pretty labour intensive to create so I wasn't releasing them very often. It was like 2 a year.&lt;/p&gt;
&lt;p&gt;I found another hit of success in February 2023 with a &lt;a href="https://youtu.be/Xx4hmsFzlKY"&gt;video about the FPGBC&lt;/a&gt;, an FPGA based reproduction of the Game Boy Color. I think the success was because it was the right video at the right time. For a while it was my most popular video. It was also a lot less effort than my previous videos!&lt;/p&gt;
&lt;h3&gt;The Big One&lt;/h3&gt;
&lt;p&gt;My BIG hit of success came with a video I published at the end of January 2025 about &lt;a href="https://youtu.be/hy2yY5a1Z-0"&gt;telling you to make a Game Boy emulator&lt;/a&gt;. It took off in a huge way. It's currently at 140k views. It accounts for more than a third of my channel's views and a bit less that half of its subscribers. It's also the reason I was monetized!&lt;/p&gt;
&lt;p&gt;I have no idea why that particular video did so well. In fact I almost didn't publish it. I thought it was boring and not particularly valuable to anyone. Also the video quality is kinda off in my opinion. The audio isn't my favorite either, but it's fine.&lt;/p&gt;
&lt;p&gt;I've tried to reproduce that success but I haven't had much luck. None of my videos since have even cracked 10k views. That's not to say they aren't successful. If you ignore that 140k video then they're definitely strong performers in the context of my channel.&lt;/p&gt;
&lt;p&gt;I've tried two ways to reproduce the success:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;continuing the topic with followup videos&lt;/li&gt;
&lt;li&gt;making a similar video for a different topic&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The followup videos had diminishing returns. The 5th one in the series hasn't broken 1k views yet.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://youtu.be/_SHaveLOt0M"&gt;similar video&lt;/a&gt; did quite well. As long as you don't compare it to the 140k video.&lt;/p&gt;
&lt;h3&gt;How I Feel About the Channel&lt;/h3&gt;
&lt;p&gt;I'm not going to lie, having that one video take off the way it did definitely helped improve my feeling about the channel. I wasn't exactly down about the channel, but I wasn't exactly feeling confident it would go anywhere. Before that big success I was getting nervous I'd miss my chance to get monetized. One of the requirements for monetization is 4,000 watch hours in the last 365 days. Before the big emulator video the bulk of my channel's watch hours came from my video about the FPGBC, published on February 23rd, 2024, and most of the watch time was within a couple month of that publishing. I published the emulator video on January 24th, 2025. By that point I only had 2,690 watch hours and I was getting maybe 5 new hours a day. So I was running out of time. In a few months a huge chunk of my watch time would pass beyond that 365 day mark and it'd be even harder to get monetized!&lt;/p&gt;
&lt;p&gt;There are of course other measures of success. I got a bunch of positive comments from people about my videos. I think I was getting better at making them too. My audio certainly improved. And I was still enjoying making them. Being monetized would be nice though...&lt;/p&gt;
&lt;h2&gt;The Analytics&lt;/h2&gt;
&lt;p&gt;As of 2026-01-09&lt;/p&gt;
&lt;h3&gt;Channel&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Lifetime views: 339k&lt;/li&gt;
&lt;li&gt;Lifetime watch time (hours): 13k&lt;/li&gt;
&lt;li&gt;Lifetime click-through-rate (CTR): 4.7%&lt;/li&gt;
&lt;li&gt;Last 365 days views: 235k&lt;/li&gt;
&lt;li&gt;Last 365 days watch time (hours): 9.7k&lt;/li&gt;
&lt;li&gt;Last 365 days click-through-rate (CTR): 4.8%&lt;/li&gt;
&lt;li&gt;Last 28 days views: 13k&lt;/li&gt;
&lt;li&gt;Last 28 days watch time (hours): 0.8k&lt;/li&gt;
&lt;li&gt;Last 28 days click-through-rate (CTR): 5.1%&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Channel views over time since it was created&lt;/h4&gt;
&lt;p&gt;&lt;img alt="A graph of my channel's lifetime views over time. There are two spikes near the start corresponding to my first two videos. Around the middle there's a bump followed by a permanent increase in views. That's from my video about the FPGBC. Later there's a big bump that gradually tapers off with another permanent increase in view rate. That's the big emulator video. After that there are a few spiky bumps for other published videos." src="channel_lifetime_views.png" /&gt;&lt;/p&gt;
&lt;p&gt;The two spikes on the left are when I released the first two videos. The little bump in the middle is the FPGBC video. You can see that it did result in an increase in the baseline views per day. The big bump is the big emulator video, and that also resulted in an increase in the baseline. Subsequent bumps are other videos being released.&lt;/p&gt;
&lt;h4&gt;Subscribers Over Time&lt;/h4&gt;
&lt;p&gt;&lt;img alt="A graph of my channel's lifetime subscribers over time. It follows a similar pattern to the view count graph above." src="channel_lifetime_subs.png" /&gt;&lt;/p&gt;
&lt;h4&gt;Monthly CTR Over Time&lt;/h4&gt;
&lt;p&gt;&lt;img alt="The overall CTR for my channel aggregated by month graphed over time. I can't really glean anything from it." src="monthly_ctr.png" /&gt;&lt;/p&gt;
&lt;h4&gt;Monthly Average View Duration Over Time&lt;/h4&gt;
&lt;p&gt;&lt;img alt="A graph of the channel's overall average view duration aggregated by month over time. It increases over time for a bit then levels off around 2:30-3:00." src="monthly_view_duration.png" /&gt;&lt;/p&gt;
&lt;h4&gt;Monthly Percent Viewed Over Time&lt;/h4&gt;
&lt;p&gt;&lt;img alt="A graph of the channel's overall average percent viewed aggregated by month over time. I don't know if there's any meaning here." src="monthly_view_percent.png" /&gt;
Maybe if I made more videos this graph could be helpful? I don't know.&lt;/p&gt;
&lt;h3&gt;First Video&lt;/h3&gt;
&lt;h4&gt;First 24 Hours Views&lt;/h4&gt;
&lt;p&gt;&lt;img alt="A graph of cumulative views of my first video for the first 24 hours from the video being published. It shows a small bump at the start followed by slow growth. Then a bigger bump around the 16h mark with gradually slowing growth" src="first_video_first_24h_views.png" /&gt;&lt;/p&gt;
&lt;h4&gt;Lifetime Views&lt;/h4&gt;
&lt;p&gt;&lt;img alt="A graph of cumulative views for the lifetime of my first video. It shows a big jump up to around 5k right at the beginning followed by gradual growth for the rest." src="first_video_lifetime_views.png" /&gt;&lt;/p&gt;
&lt;h4&gt;Watch Retention&lt;/h4&gt;
&lt;p&gt;&lt;img alt="Data about viewer retention for my first view. The average view is 2:22 with an average percent view of 33.3%. There's a graph showing viewer retention over the duration of the video. It shows a steep drop off right at the start, followed by gradual decline and a faster decline for the last bit of the video. There is text below the graph that says &amp;quot;52% of viewers are still watching at around the 0:30 mark, which is typical. Learn more by comparing to your other videos.&amp;quot;." src="first_retention.png" /&gt;&lt;/p&gt;
&lt;p&gt;This graph shows the percent of viewers who are watching at a particular point in the video. It primarily captures when people stop watching the video, but it's also affected by people skipping around in the video, which can cause the retention to increase at a particular point and in more extreme cases could cause retention to go above 100%.&lt;/p&gt;
&lt;h3&gt;Best Video (the one about emulators)&lt;/h3&gt;
&lt;h4&gt;First 24 Hours Views&lt;/h4&gt;
&lt;p&gt;&lt;img alt="Cumulative views of my best video over its first 24 hours. It's showing a gradually increasing rate of views over time." src="emulator_first_24h_views.png" /&gt;&lt;/p&gt;
&lt;h4&gt;Lifetime Views&lt;/h4&gt;
&lt;p&gt;&lt;img alt="Cumulative views of my best video over its lifetime. It shows a fastish increase for first month or so, then a gradual slowing over time." src="emulator_lifetime_views.png" /&gt;&lt;/p&gt;
&lt;h4&gt;Watch Retention&lt;/h4&gt;
&lt;p&gt;&lt;img alt="Viewer retention for my best video. Average view duration is 2:11. Average view percent is 34.3%. The graph shows a drop off at the start but it's not very steep. Then a gradual decrease over time. The initial dro poff is significantly less steep than my first video. Text at the bottom reads &amp;quot;60% of viewers are still watching at around the 0:30 mark, which is typical. Learn more by comparing to your other videos" src="emulator_retention.png" /&gt;&lt;/p&gt;
&lt;h4&gt;Retention Comparison&lt;/h4&gt;
&lt;p&gt;&lt;img alt="A graph showing my best video's retention compared to my other videos. The y-axis is labeled &amp;quot;low&amp;quot;, &amp;quot;average&amp;quot;, and &amp;quot;high&amp;quot;. The line stays roughly around the average line with some wiggling above and below. The only thing I can really get from it is that there's slightly better retention at the start." src="emulator_compare_retention.png" /&gt;&lt;/p&gt;
&lt;p&gt;This graph shows the video's retention compared to my other videos. I'm not sure if it's videos publish before this one or all videos I've published. When the line goes into the high region it's saying the retention is better than others, and low means worse.&lt;/p&gt;
&lt;h4&gt;First 24 Hours CTR&lt;/h4&gt;
&lt;p&gt;&lt;img alt="A graph of click-through-rate in the first 24hours of my best video. It shows some high initial CTR, going as high as nearly 22%. Then it levels off around 7%" src="emulator_first_24h_ctr.png" /&gt;&lt;/p&gt;
&lt;h4&gt;Lifetime CTR&lt;/h4&gt;
&lt;p&gt;&lt;img alt="A graph of the ctr for the lifetime of my best video. The overall rate is 4.7% and the graph stays around there with a bunch of variance." src="emulator_lifetime_ctr.png" /&gt;&lt;/p&gt;
&lt;h3&gt;Worst Video&lt;/h3&gt;
&lt;p&gt;This is a video about &lt;a href="https://youtu.be/99W2Ys0fQfQ"&gt;using javascript to generate visuals for a video&lt;/a&gt;. It did not do well. It's kinda outside of my core topic though so whatever. My thinking was that I was doing the work anyways, might as well have gotten some content out of it. The funny thing is that the stuff I was doing in this video was for my &lt;a href="https://youtu.be/jQTByAUF7X4"&gt;3rd worst video&lt;/a&gt; (I'm not counting my 10 hour fireplace videos btw).&lt;/p&gt;
&lt;h4&gt;First 24 Hours Views&lt;/h4&gt;
&lt;p&gt;&lt;img alt="Views for my worst video over its first 24 hours. It's slow and ends at 50 views. The graph also shows a &amp;quot;usual&amp;quot; range. This video is below that range around the 7hour mark." src="worst_first_24h_views.png" /&gt;&lt;/p&gt;
&lt;h4&gt;Lifetime Views&lt;/h4&gt;
&lt;p&gt;&lt;img alt="Views for my worst video over its lifetime. Not much going on here, it's almost a horizontal line. Below the &amp;quot;usual&amp;quot; range since day 1. Ends at 521 views." src="worst_lifetime_views.png" /&gt;&lt;/p&gt;
&lt;h4&gt;Watch Retention&lt;/h4&gt;
&lt;p&gt;&lt;img alt="Retention data for my worst video. Average view duration is 2:03. Average percentage viewed is 12.4%. The retention graph has a sharp drop off at the start stays kinda steady at around 15% for most of the video. Text at the bottom reads &amp;quot;41% of views are still watching at around the 0:30 mark, which is below typical.&amp;quot;" src="worst_retention.png" /&gt;&lt;/p&gt;
&lt;h4&gt;First 24 Hours CTR&lt;/h4&gt;
&lt;p&gt;&lt;img alt="The CTR for my worst video for its first 24 hours. The overall rate is1.7%. It's 0 for 5 of the 24 hours. it's max is 6%. Not good." src="worst_first_24h_ctr.png" /&gt;&lt;/p&gt;
&lt;h3&gt;A Clickbait Video&lt;/h3&gt;
&lt;p&gt;I made a video about &lt;a href="https://youtu.be/14WtP6hGm-s"&gt;why I'm personally disappointed when I buy flash carts for consoles&lt;/a&gt;. It was a very low effort video and I made the thumbnail and title a little bit click-baity.&lt;/p&gt;
&lt;h4&gt;Retention&lt;/h4&gt;
&lt;p&gt;&lt;img alt="Retention data for my clickbait video. Average view duration is 1:33. Average percent viewed is 38.6%. The graph has a pretty small drop off at the start and just fairly gradual decrease in retention over time. It ends around 15%. Text at the bottom reads &amp;quot;66% of viewers are still watching at around the 0:30 mark, which is typical.&amp;quot;" src="clickbait_retention.png" /&gt;&lt;/p&gt;
&lt;h4&gt;Lifetime CTR&lt;/h4&gt;
&lt;p&gt;&lt;img alt="CTR of my clickbait video over its lifetime. lots of variance for the first 3rd of it's life, then much less after that. Stays around 15%" src="clickbait_lifetime_ctr.png" /&gt;&lt;/p&gt;
&lt;h4&gt;Lifetime View Percent&lt;/h4&gt;
&lt;p&gt;&lt;img alt="Lifetime percent viewed of my clickbait video over its lifetime. Lots of variance at the start, which gradually diminishes until the first quarter of the lifetime. Stays a bit below 40%." src="clickbait_lifetime_view_percent.png" /&gt;&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Well that's about it. If I had some kind of summary to say here I've completely forgotten it. So, I hope this data helps someone!&lt;/p&gt;
&lt;p&gt;Remember to Like and Subscribe!&lt;/p&gt;</content><link href="https://systemoflevers.com/blog/2026/01/09/youtube-analytics/index.html"/></entry></feed>