<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Richard Lee]]></title><description><![CDATA[Thoughts, stories and ideas.]]></description><link>https://rjdlee.com/</link><image><url>https://rjdlee.com/favicon.png</url><title>Richard Lee</title><link>https://rjdlee.com/</link></image><generator>Ghost 5.33</generator><lastBuildDate>Tue, 07 Apr 2026 20:29:32 GMT</lastBuildDate><atom:link href="https://rjdlee.com/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[One Page Application Pageviews in Universal Analytics 2]]></title><description><![CDATA[<h1 id="introduction"><strong>Introduction</strong></h1><p>I had lots of issues getting setup with Universal Analytics (UA) with a one page app I&apos;ve had to work on very recently. This tutorial will not explain very much because I believe the issue with UA is not that it&apos;s hard, but rather that</p>]]></description><link>https://rjdlee.com/one-page-application-pageviews-in-universal-analytics-2/</link><guid isPermaLink="false">63d745b911a23e6694bf7951</guid><category><![CDATA[Overflow]]></category><dc:creator><![CDATA[Richard Lee]]></dc:creator><pubDate>Mon, 30 Jan 2023 04:21:30 GMT</pubDate><content:encoded><![CDATA[<h1 id="introduction"><strong>Introduction</strong></h1><p>I had lots of issues getting setup with Universal Analytics (UA) with a one page app I&apos;ve had to work on very recently. This tutorial will not explain very much because I believe the issue with UA is not that it&apos;s hard, but rather that there&apos;s a lack of information out there. This will help you get started on tracking pageviews for your one page application.</p><h2 id="prerequisites"><strong>Prerequisites:</strong></h2><ul><li>A Google Tag Manager Universal Analytics (UA) account</li><li>A Google Analytics account or other analytics platform (but this tutorial won&apos;t cover those)</li></ul><hr><h2 id="1-create-a-data-layer"><strong>1. Create a Data Layer</strong></h2><p>A data layer stores information from our code and passes that on to UA automatically.</p><p>All you need to do is declare a dataLayer array. Optionally it can have default values for your variables.</p><pre><code>&lt;!-- Google Tag Manager Data --&gt;
  &lt;script type=&quot;text/javascript&quot;&gt;
    var dataLayer = [{&apos;variableName&apos; : &apos;variableOptionalValue&apos;}];
  &lt;/script&gt;
  &lt;!-- Google Tag Manager Code --&gt;
  ...
  &lt;!-- End Google Tag Manager Code --&gt;</code></pre><p>Next, add an event listener (such as onClick) to your HTML so you know when the user interacts with something.</p><pre><code>&lt;a onClick=&quot;uaClick&quot;&gt;</code></pre><p>And finally, add the data to the dataLayer so UA can keep track of it.</p><pre><code>$scope.uaClick = function(pageName) {
  	dataLayer.push({&apos;pageName&apos;: &apos;index&apos;});
  };</code></pre><h3 id="notes"><strong>Notes:</strong></h3><ul><li>Make sure the dataLayer instantiation comes before the Google Tag Manager Code</li><li>Use dataLayer.push({&apos;variableName&apos; : &apos;variableData&apos;});</li></ul><hr><h2 id="2-creating-a-variable"><strong>2. Creating a Variable</strong></h2><ol><li>Go to the Variables tab on the left menu.</li><li>Create a new User-Defined Variable.</li><li>Set Variable Name to anything, set Data Layer Variable Name to your variableName.</li><li>Data Layer Version to Version 2 and optional default value.</li></ol><hr><h2 id="3-creating-a-trigger"><strong>3. Creating a Trigger</strong></h2><ol><li>Go to the Triggers tab.</li><li>Create a new Trigger.</li><li>The event we are probably listening for is a Click event, but this is where things may become ambiguous.</li><li>I wanted all the click events from the variableName to be registered so I did:</li></ol><!--kg-card-begin: html--><table style="border-collapse: collapse; border-spacing: 0px; font-size: 16px; line-height: 1.5; margin: 0px 0px 30px; text-align: left; width: 740px; color: rgb(102, 102, 102); font-family: &quot;droid serif&quot;, serif; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><tbody><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">variableName</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">matches RegEx</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">\w*</td></tr></tbody></table><!--kg-card-end: html--><p>This matches any word or letter and number combination.</p><p>5. The rest of the options should be self explanatory.</p><hr><h2 id="4-creating-a-tag"><strong>4. Creating a Tag</strong></h2><p>Now we are bringing together the components we have created from the other parts. Note that for testing on localhost, you need to set</p><ol><li>Go to the Tags tab.</li><li>Create a new Tag.</li><li>Set tracking ID to your Google Analytics tracking ID.</li><li>Set Track Type to Page View.</li><li>Under Basic Configuration, set Document Path to {{Page URL}}/{{Variable Name}} to create the page URL.</li><li>Set Document Title to {{Variable Name}} to set the page</li><li>If you are testing on localhost, go to Advanced Configuration and set Use Debug Version to True.</li></ol><hr><p>If you have any questions, send me a message [here](http://rileedesign.com).</p><h2 id="resources"><strong>Resources:</strong></h2><ol><li>http://cutroni.com/blog/2012/10/01/all-about-google-tag-manager/</li><li>https://developers.google.com/analytics/devguides/collection/upgrade/reference/gtm</li><li>https://support.google.com/tagmanager/answer/6106716</li><li>http://moz.com/ugc/tracking-google-analytics-events-with-google-tag-manager</li><li>https://developers.google.com/tag-manager/devguide</li><li>http://www.razorsocial.com/google-tag-manager/</li></ol><h2 id="support-forums"><strong>Support Forums:</strong></h2><ol><li>https://productforums.google.com/forum/?utm_medium=email&amp;utm_source=footer#!forum/tag-manager</li><li>http://stackoverflow.com/questions/tagged/universal-analytics</li></ol>]]></content:encoded></item><item><title><![CDATA[An AgarIO Clone]]></title><description><![CDATA[<p>AgarIO has been all the rage for the past month now. It&apos;s been featured on countless major Youtube channels, has at least 100,000 users playing at any given time and seems to have been acquired or partnered with Miniclip.</p><p>I had the excellent idea to develop an</p>]]></description><link>https://rjdlee.com/an-agario-clone/</link><guid isPermaLink="false">63d745a011a23e6694bf7948</guid><category><![CDATA[Overflow]]></category><dc:creator><![CDATA[Richard Lee]]></dc:creator><pubDate>Mon, 30 Jan 2023 04:20:57 GMT</pubDate><content:encoded><![CDATA[<p>AgarIO has been all the rage for the past month now. It&apos;s been featured on countless major Youtube channels, has at least 100,000 users playing at any given time and seems to have been acquired or partnered with Miniclip.</p><p>I had the excellent idea to develop an AgarIO clone, which would allows friends to join each other easier and to have a more efficient rendering engine.</p><h1 id="things-go-wrong"><strong>Things Go Wrong</strong></h1><p>Soon after starting, I realized I&apos;d made a number of serious mistakes, but I&apos;ve learned a lot along the way.</p><p>The first mistake was cloning a game without providing any true benefit because AgarIO actually has a gamemode where you can join your friends and seems to be improving its rendering engine over time.</p><p>The second mistake I made was assuming such a game would be easy to develop and a game engine would not be necessary. I could have never been further off from reality; collision handling and rendering efficiently [ to name a few] are hard.</p><p>Nonetheless, I keep pushing onwards because I get to learn a ton of stuff.</p><h1 id="collision-detection-and-handling"><strong>Collision Detection and Handling</strong></h1><p>Detecting a collision between to circles is not hard. Find the distance between the position of two circles, if that distance is greater than the radii added, there is a collision.</p><p>Handling a collision similar to AgarIO does it is much harder and I&apos;ll save that for a post in the future because I haven&apos;t completely ironed out my algorithm.</p><h1 id="client-and-server-communication"><strong>Client and Server Communication</strong></h1><p>I used SocketsIO, NodeJS, and ExpressJS for the backend of this project. I encode and decode requests using binary JSON.</p><p>When a client connects to the server, they receive information about the entire map state (players, viruses, and food). After that, they send input events (mousemove, w keypress, and space keypress). The server uses this input to simulate the map at some number of ticks per second.</p><p>Say a client sends a mousemove event, this means they&apos;ve changed the direction their cells are moving towards. The server will receive this and updates its cell objects for this client accordingly and will send the new velocity to the other players.</p><p>The clients do not compute any important logic (collisions with viruses, other players&apos; cells, splitting, shooting, etc.), they just interpolate from the velocity and position received from the server. This prevents cheating, but retains responsiveness on the frontend.</p><p>I&apos;m still working out how to determine exactly what data the server should be sending the clients and when/ how often.</p><h1 id="rendering"><strong>Rendering</strong></h1><p>The way I render currently has become a bit of a monstrosity.</p><p>Players&apos; cells are rendered in the DOM and animated with the top and left styles so they do not have to be redrawn to a canvas every time they move.</p><p>Viruses and food are rendered on separate canvases and are only re-rendered when a food or viruses moves, is added, or is removed. There is potential for improvement by using a <a href="https://web.archive.org/web/20210919135218/http://gamedevelopment.tutsplus.com/tutorials/quick-tip-use-quadtrees-to-detect-likely-collisions-in-2d-space--gamedev-374">quadtree.</a> I haven&apos;t quite gotten around to that.</p><h1 id="issues"><strong>Issues</strong></h1><ul><li>My render and logic loops are separate. I try to keep track of state changes made by the logic loop so rendering can be performed as efficiently as possible, but this introduces a lot of complexity that I haven&apos;t figured out how to eliminate</li><li>Similarly, determining how to track and send state changes to the client from the server while using the least bandwidth possible is difficult</li><li>Collision handling will require me to do a bit more vector math on paper</li></ul><h1 id="moving-forward"><strong>Moving Forward</strong></h1><p>I&apos;ll continue to update as I solve each of these issues. If you have any questions or would like me to expand more on any of these topics, I&apos;d be glad to do it.</p><p>tl;dr Use a game engine.</p>]]></content:encoded></item><item><title><![CDATA[Matrices for Game Development]]></title><description><![CDATA[<p>Tanks is an online multiplayer tanks game based off Wii Tanks and inspired by Agario. This project was started in <a href="https://web.archive.org/web/20210919130025/https://rjdlee.com/projects/tanks/">2013</a>, it was revamped in early 2015 and has been progressing ever since, but I am taking an indefinite pause from it to work on other more useful ventures. It&</p>]]></description><link>https://rjdlee.com/matrices-for-game-development/</link><guid isPermaLink="false">63d7457a11a23e6694bf793e</guid><category><![CDATA[Overflow]]></category><dc:creator><![CDATA[Richard Lee]]></dc:creator><pubDate>Mon, 30 Jan 2023 04:20:24 GMT</pubDate><content:encoded><![CDATA[<p>Tanks is an online multiplayer tanks game based off Wii Tanks and inspired by Agario. This project was started in <a href="https://web.archive.org/web/20210919130025/https://rjdlee.com/projects/tanks/">2013</a>, it was revamped in early 2015 and has been progressing ever since, but I am taking an indefinite pause from it to work on other more useful ventures. It&apos;s been an amazing learning experience and I&apos;ll try to document the important parts I&apos;ve learned about game development. A live (old) version of the game can be found at <a href="https://web.archive.org/web/20210919130025/http://tank.rjdlee.com/">http://tank.rjdlee.com</a>; tell your friends to connect to that link to see how the multiplayer works.</p><p>Now, if you&apos;re actually looking to build a production quality game, do not try building your game engine. People have spent thousands (or more) of hours making these engines stable.</p><h2 id="matrices"><strong>Matrices</strong></h2><p>I started this project with no knowledge about vectors and matrices. I won&apos;t go into detail about them, but I&apos;ll guide you in the right direction.</p><p>Entities (such as tanks) have a bounding box, which describes their shape and location on the map.</p><p>When the entity rotates or moves, its bounding box needs to be updated. For rotation, use a <a href="https://web.archive.org/web/20210919130025/https://en.wikipedia.org/wiki/Rotation_matrix">rotation matrix</a>. For movement, just move/ translate each bounding box vertex by the amount the entity translated.</p><p>For rotation matrices, I created a general purpose function to multiply two matrices. Pass it a rotation matrix:</p><pre><code>[
	[ Math.cos( angle ), -Math.sin( angle ) ],
	[ Math.sin( angle ), -Math.cos( angle ) ]
]</code></pre><p>And pass it a bounding box vertex coordinate:</p><pre><code>[
	[ 43110 ],
	[ 41 ]
]</code></pre><p>And it will give you a rotated coordinate. Rotating a tank barrel is a bit different though because it&apos;s rotating around one of its ends. I used a transform origin for this. Essentially, add or subtract a certain amount to each vertex to change how the shape rotates.</p><p>Say we have a rectangle with coordinates: (-2, 1), (2, 1), (-2, -1), and (2, -1). We want to rotate it around the point: (-2, 0).</p><p>1. Add 2 to each point&apos;s x-coordinate: (0, 1), (4, 1), (0, -1), and (4, -1)</p><p>2. Perform the rotation</p><p>3. Subtract 2 from the resulting points&apos; x-coordinates</p><p>Matrix multiplication code:</p><pre><code>multiply_matrices( matrix_a = [], matrix_b = [] )
	multiply_matrices( matrix_a = [], matrix_b = [] )
{
	if ( matrix_a.length === 0 || matrix_b.length === 0 )
		return [];
	// Number of rows in matrix_a
	let height = matrix_a.length;
	// Number of columns in matrix_b
	let width = matrix_b[ 0 ].length;
	// Create an empty matrix to store the result
	let matrix = Array( height );
	// Iterate through each row of matrix_a
	for ( let a_y = 0; a_y &lt; height; a_y++ )
	{
		let a_row = matrix_a[ a_y ];
		matrix[ a_y ] = Array( width );
		// Iterate through each column of matrix_b
		for ( let b_x = 0; b_x &lt; width; b_x++ )
		{
			let cell = 0;
			// Iterate through the bigger of the width of matrix_a or height of matrix_b
			for ( let i = 0; i &lt; Math.max( a_row.length, matrix_b.length ); i++ )
			{
				cell += a_row[ i ] * matrix_b[ i ][ b_x ];
			}
			matrix[ a_y ].push( cell );
		}
	}
	return matrix;
}</code></pre><h2 id="other-parts"><strong>Other Parts</strong></h2><p>2. <a href="https://web.archive.org/web/20210919130025/https://rjdlee.com/game-collision-detection-and-handling/">Collisions</a></p><p>3. <a href="https://web.archive.org/web/20210919130025/https://rjdlee.com/game-lag-compensation-and-interpolation/">Interpolation</a></p>]]></content:encoded></item><item><title><![CDATA[Game Collision Detection and Handling]]></title><description><![CDATA[<p>In this post, I will talk about collision detection and handling.</p><p>There are plenty of useful resources on collision detection:</p><ul><li><a href="https://web.archive.org/web/20210919134321/http://www.dyn4j.org/2010/01/sat/">http://www.dyn4j.org/2010/01/sat/</a></li><li><a href="https://web.archive.org/web/20210919134321/http://gamedevelopment.tutsplus.com/tutorials/collision-detection-using-the-separating-axis-theorem--gamedev-169">http://gamedevelopment.tutsplus.com/tutorials/collision-detection-using-the-separating-axis-theorem--gamedev-169</a></li><li><a href="https://web.archive.org/web/20210919134321/http://www.metanetsoftware.com/technique/tutorialA.html">http://www.metanetsoftware.com/technique/tutorialA.html</a></li><li><a href="https://web.archive.org/web/20210919134321/https://github.com/rjdlee/Tanks/blob/master/src/common/collision/collision.js">https://github.com/rjdlee/Tanks/blob/master/src/common/collision/</a></li></ul>]]></description><link>https://rjdlee.com/game-collision-detection-and-handling/</link><guid isPermaLink="false">63d7455b11a23e6694bf7932</guid><category><![CDATA[Overflow]]></category><dc:creator><![CDATA[Richard Lee]]></dc:creator><pubDate>Mon, 30 Jan 2023 04:19:59 GMT</pubDate><content:encoded><![CDATA[<p>In this post, I will talk about collision detection and handling.</p><p>There are plenty of useful resources on collision detection:</p><ul><li><a href="https://web.archive.org/web/20210919134321/http://www.dyn4j.org/2010/01/sat/">http://www.dyn4j.org/2010/01/sat/</a></li><li><a href="https://web.archive.org/web/20210919134321/http://gamedevelopment.tutsplus.com/tutorials/collision-detection-using-the-separating-axis-theorem--gamedev-169">http://gamedevelopment.tutsplus.com/tutorials/collision-detection-using-the-separating-axis-theorem--gamedev-169</a></li><li><a href="https://web.archive.org/web/20210919134321/http://www.metanetsoftware.com/technique/tutorialA.html">http://www.metanetsoftware.com/technique/tutorialA.html</a></li><li><a href="https://web.archive.org/web/20210919134321/https://github.com/rjdlee/Tanks/blob/master/src/common/collision/collision.js">https://github.com/rjdlee/Tanks/blob/master/src/common/collision/collision.js</a> (my own code)</li></ul><p>I did not find so much on collision handling. Once you&apos;ve detected a collision between two convex shapes, you need to figure out what to do with the objects. Should they push each other around? Should their velocities cancel out?</p><p>In Tanks, I used two methods. If a tank collides when moving forwards or backwards, it should slide along the other surface. If a tank collides when turning, it should seemingly push off the other surface.</p><p>When detecting collisions, it is important to find the exact surface your object is colliding on. To find the surface, find the surface with the least overlap.</p><p>After that, simply project the velocity of the moving object onto that surface&apos;s vector. If the moving object is colliding with more than one surface, you can project its velocity multiple times.</p><p>One thing to note is when trying to move away from a surface, if the moving object is detected as colliding, its velocity may continually be projected on the surface and the object will not be able to move away. This can be solved by ensuring the object is no longer colliding with the surface after detection or determining its direction relative to the surface.</p><p>When a tank rotates, find how much it overlaps the surface and the surface it is colliding with. And move away using the normal of the surface multiplied by the length of the overlap.</p><pre><code>var displacementVector = {
	x: overlap * surface.y,
	y: overlap * surface.x
};
if ( surface.x &lt; 0 )
	displacementVector.y = -displacementVector.y;
if ( surface.y &lt; 0 )
	displacementVector.x = -displacementVector.x;
this.move( displacementVector.x, displacementVector.y );</code></pre><p>I apologize if this was not clear, I did plan for these posts to be much better thought out, but I&apos;m trying to free up my time for other things.</p><h2 id="other-parts"><strong>Other Parts</strong></h2><p>1. <a href="https://web.archive.org/web/20210919134321/https://rjdlee.com/matrices-for-game-development-2/">Matrices</a></p><p>3. <a href="https://web.archive.org/web/20210919134321/https://rjdlee.com/game-lag-compensation-and-interpolation/">Interpolation</a></p>]]></content:encoded></item><item><title><![CDATA[Game Lag Compensation and Interpolation]]></title><description><![CDATA[<p>&quot;The version I have uploaded to <a href="https://web.archive.org/web/20210919150645/http://tank.rjdlee.com/">http://tank.rjdlee.com</a> does not currently have server interpolation, but I have implemented it in the current code.</p><p>When I was programming the initial version of multiplayer Tanks, I forgot about the effects of latency on gameplay and only saw the effects</p>]]></description><link>https://rjdlee.com/game-lag-compensation-and-interpolation/</link><guid isPermaLink="false">63d7454511a23e6694bf7929</guid><category><![CDATA[Overflow]]></category><dc:creator><![CDATA[Richard Lee]]></dc:creator><pubDate>Mon, 30 Jan 2023 04:19:28 GMT</pubDate><content:encoded><![CDATA[<p>&quot;The version I have uploaded to <a href="https://web.archive.org/web/20210919150645/http://tank.rjdlee.com/">http://tank.rjdlee.com</a> does not currently have server interpolation, but I have implemented it in the current code.</p><p>When I was programming the initial version of multiplayer Tanks, I forgot about the effects of latency on gameplay and only saw the effects manifest on the live version.</p><p>There are a number of resources on this subject, but those don&apos;t make it too much easier to implement.</p><ul><li><a href="https://web.archive.org/web/20210919150645/https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networkinghttps://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking">https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking</a></li><li><a href="https://web.archive.org/web/20210919150645/http://gafferongames.com/networked-physics/snapshots-and-interpolation/http://gafferongames.com/networked-physics/snapshots-and-interpolation/">http://gafferongames.com/networked-physics/snapshots-and-interpolation/</a></li><li><a href="https://web.archive.org/web/20210919150645/http://www.gabrielgambetta.com/fpm3.html">http://www.gabrielgambetta.com/fpm3.html</a></li></ul><p>I followed the methods above and implemented a snapshot system on the <a href="https://web.archive.org/web/20210919150645/https://github.com/rjdlee/Tanks/blob/master/src/server/game_map.js">server</a>. It produces <a href="https://web.archive.org/web/20210919150645/https://github.com/rjdlee/Tanks/blob/master/src/common/game/game_map_state.js">compressed versions</a> of my game&apos;s map and all its entities at set intervals. Then, I created <a href="https://web.archive.org/web/20210919150645/https://github.com/rjdlee/Tanks/blob/master/src/server/game_map.js">code</a> to load past snapshots of my game and replay changes on top of those.</p><p>An issue I noticed with my current code is when I rewind my game to a past snapshot, I can&apos;t take snapshots of it (otherwise I&apos;d be snapshotting old states). This can lock up the game for a long time. The solution to this would be to have a map specifically for replaying old versions running in parallel to the actual map. It may be a bit difficult to converge the replaying map into the actual map, though.</p><p>The snapshot system also allowed me to produce a list of changes between two snapshots, which I could send to clients periodically to update them with information about the game&apos;s current state.</p><p>I hope you enjoyed my short series on game development, once again, I apologize for the brevity of it.</p><h2 id="other-parts"><strong>Other Parts</strong></h2><ol><li><a href="https://web.archive.org/web/20210919150645/https://rjdlee.com/matrices-for-game-development-2/">Matrices</a></li><li><a href="https://web.archive.org/web/20210919150645/https://rjdlee.com/game-collision-detection-and-handling/">Collisions</a></li></ol>]]></content:encoded></item><item><title><![CDATA[Arduino WeMos with L298N Controller and 28BYJ-48 Stepper Motor]]></title><description><![CDATA[<p>In the <a href="https://web.archive.org/web/20210919132433/https://rjdlee.com/getting-started-with-wemos-d1-on-mac-osx">previous tutorial</a>, we set up our WeMos D1 board.</p><p>Now, we are going to use it to control a stepper motor. Specifically, the L298N Dual H Bridge DC Stepper Motor Controller and the 28BYJ-48 5V DC Stepper Motor.</p><h3 id="l298n"><strong>L298N:</strong></h3><h3 id="28byj-48"><strong>28BYJ-48:</strong></h3><hr><p>Here is the reference table from the previous</p>]]></description><link>https://rjdlee.com/arduino-wemos-with-l298n-controller-and-28byj-48-stepper-motor/</link><guid isPermaLink="false">63d7451a11a23e6694bf791e</guid><category><![CDATA[Overflow]]></category><dc:creator><![CDATA[Richard Lee]]></dc:creator><pubDate>Mon, 30 Jan 2023 04:19:00 GMT</pubDate><content:encoded><![CDATA[<p>In the <a href="https://web.archive.org/web/20210919132433/https://rjdlee.com/getting-started-with-wemos-d1-on-mac-osx">previous tutorial</a>, we set up our WeMos D1 board.</p><p>Now, we are going to use it to control a stepper motor. Specifically, the L298N Dual H Bridge DC Stepper Motor Controller and the 28BYJ-48 5V DC Stepper Motor.</p><h3 id="l298n"><strong>L298N:</strong></h3><h3 id="28byj-48"><strong>28BYJ-48:</strong></h3><hr><p>Here is the reference table from the previous tutorial:</p><!--kg-card-begin: html--><table style="border-collapse: collapse; border-spacing: 0px; font-size: 16px; line-height: 1.5; margin: 0px 0px 30px; text-align: left; width: 740px; color: rgb(102, 102, 102); font-family: &quot;droid serif&quot;, serif; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><thead></thead><tbody><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">Official WeMos D1 Board Label</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">Knockoff WeMos D1 Board Label</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">Digital Port</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">Arduino Software</td></tr></tbody><tbody><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">RX</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">RX&lt;-d0&lt; td=&quot;&quot;&gt;</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D0</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">3</td></tr><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">TX</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">TX-&gt;D0</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D1</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">1</td></tr><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D0</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D2</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D2</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">16</td></tr><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">SCL/D1</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D15/SCL/D3</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D3</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">5</td></tr><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">SDA/D2</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D14/SDA/D4</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D4</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">4</td></tr><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D3</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D13/SCK/D5</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D5</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">0</td></tr><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D4</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D12/MISO/D6</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D6</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">2</td></tr><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D5</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D11/MOSI/D7</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D7</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">14</td></tr><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D6</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D8</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D8</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">12</td></tr><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D7</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">TX1/D9</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D9</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">13</td></tr><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">SS/D8</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D10/SS</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D10</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">14</td></tr><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">MOSI/D7</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D11/MOSI</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D11</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">13</td></tr><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">MISO/D6</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D12/MISO</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D12</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">12</td></tr><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">SCK/D5</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D13/SCK</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D13</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">14</td></tr><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">SDA/D2</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D14/SDA</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D14</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">4</td></tr><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">SCL/D1</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D15/SCL</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D15</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">5</td></tr></tbody></table><!--kg-card-end: html--><h1 id="wiring"><strong>Wiring</strong></h1><h2 id="motor-controller-to-board"><strong>Motor Controller to Board</strong></h2><p>Let&apos;s start by connecting the motor controller (L298N) to the board (WeMos D1).</p><p>Using pliers, pull the caps off ENA and ENB (I have no idea what they&apos;re for):</p><p>Using male to female DuPont jumper wires, make the following connections:</p><!--kg-card-begin: html--><table style="border-collapse: collapse; border-spacing: 0px; font-size: 16px; line-height: 1.5; margin: 0px 0px 30px; text-align: left; width: 740px; color: rgb(102, 102, 102); font-family: &quot;droid serif&quot;, serif; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><thead></thead><tbody><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">WeMos D1 Board</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">L298N Motor Controller</td></tr></tbody><tbody><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D2</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">ENA</td></tr><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D15/SCL/D3</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">IN1</td></tr><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D14/SDA/D4</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">IN2</td></tr><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D13/SCK/D5</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">IN3</td></tr><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D12/MISO/D6</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">IN4</td></tr><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D11/MOSI/D7</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">ENB</td></tr></tbody></table><!--kg-card-end: html--><h2 id="motor-controller-to-motor"><strong>Motor Controller to Motor</strong></h2><p>Here&apos;s a diagram of the 28BYJ-48 stepper motor:</p><p>If you compare this diagram to the actual motor, the colours of the wires connected to the motor correspond. Remember this when connecting the motor.</p><p>Using male to male DuPont jumper wires, make the following connections:</p><!--kg-card-begin: html--><table style="border-collapse: collapse; border-spacing: 0px; font-size: 16px; line-height: 1.5; margin: 0px 0px 30px; text-align: left; width: 740px; color: rgb(102, 102, 102); font-family: &quot;droid serif&quot;, serif; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><thead><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">L298N Motor Controller</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">28BYJ-48 Stepper Motor</td></tr></thead><tbody><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">OUT1</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">1. Blue</td></tr><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">OUT2</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">3. Yellow</td></tr><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">OUT3</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">4. Pink</td></tr><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">OUT4</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">5. Orange</td></tr></tbody></table><!--kg-card-end: html--><h2 id="motor-controller-to-power"><strong>Motor Controller to Power</strong></h2><p>We need to supply the motor controller with power. You can use any DC (7 - 35V).</p><ol><li>If your power supply doesn&apos;t have two wires coming from it (hot and ground), you&apos;ll have to cut it and strip the two ends.</li><li>Plug the power supply&apos;s hot end into +12V on the motor controller.</li><li>I recommend a breadboard for this part. Connect the GND on the motor controller (with a male to male jumper), the motor&apos;s red wire, one of the GND&apos;s on the board, and the power supply&apos;s ground together. This is called tying the grounds together.</li></ol><h1 id="code"><strong>Code</strong></h1><p>If you read about bipolar and unipolar stepper motors, you will understand the following code. Basically, the magnets in the motor must turn on in a sequence to rotate the motor&apos;s head.</p><p>Remember the pins may look random, but correspond to the digital I/O ports as seen in the table at the top of this tutorial.</p><pre><code>/*
 * Driving a 5V stepper motor using Keyes L298N Dual Motor Driver;
 * Chienline @2015;
 */
const int ENA = 7;
const int IN1 = 6;
const int IN2 = 5;
const int ENB = 8;
const int IN4 = 9;
const int IN3 = 10;
const int ledPin = 13;
void setup()
{
  pinMode(ENA,OUTPUT);
  pinMode(IN1,OUTPUT);
  pinMode(IN2,OUTPUT);
  pinMode(ENB,OUTPUT);
  pinMode(IN3,OUTPUT);
  pinMode(IN4,OUTPUT);
  pinMode(ledPin,OUTPUT);
  digitalWrite(ledPin, LOW);
  //delay is used to control the speed, the lower the faster.
  //reverse(step,delay);
  reverse(80,20);
  //forward(step,delay);
  forward(80,20);
}
void loop()
{
}
void reverse(int i, int j) {
  // set both motors ON
  digitalWrite(ENA, HIGH);
  digitalWrite(ENB, HIGH);
  while (1)   {
    digitalWrite(IN1, 0);
    digitalWrite(IN2, 1);
    digitalWrite(IN3, 0);
    digitalWrite(IN4, 1);
    delay(j);
    i--;
    if (i &lt; 1) break; 
    digitalWrite(IN1, 0);
    digitalWrite(IN2, 1);
    digitalWrite(IN3, 1);
    digitalWrite(IN4, 0);
    delay(j);  
    i--;
    if (i &lt; 1) break;
    digitalWrite(IN1, 1);
    digitalWrite(IN2, 0);
    digitalWrite(IN3, 1);
    digitalWrite(IN4, 0);
    delay(j);
    i--;
    if (i &lt; 1) break;
    digitalWrite(IN1, 1);
    digitalWrite(IN2, 0);
    digitalWrite(IN3, 0);
    digitalWrite(IN4, 1);
    delay(j);  
    i--;
    if (i &lt; 1) break;
  }
  // set both motors OFF
  digitalWrite(ENA, LOW);
  digitalWrite(ENB, LOW);
}  // end reverse()
void forward(int i, int j) {
  // Set both motors ON
  digitalWrite(ENA, HIGH);
  digitalWrite(ENB, HIGH);
  while (1)   {
    digitalWrite(IN1, 0);
    digitalWrite(IN2, 1);
    digitalWrite(IN3, 0);
    digitalWrite(IN4, 1);
    delay(j);  
    i--;
    if (i &lt; 1) break;
    digitalWrite(IN1, 1);
    digitalWrite(IN2, 0);
    digitalWrite(IN3, 0);
    digitalWrite(IN4, 1);
    delay(j);
    i--;
    if (i &lt; 1) break;
    digitalWrite(IN1, 1);
    digitalWrite(IN2, 0);
    digitalWrite(IN3, 1);
    digitalWrite(IN4, 0);
    delay(j);  
    i--;
    if (i &lt; 1) break;
    digitalWrite(IN1, 0);
    digitalWrite(IN2, 1);
    digitalWrite(IN3, 1);
    digitalWrite(IN4, 0);
    delay(j);
    i--;
    if (i &lt; 1) break;
  }
  // set both motors OFF
  digitalWrite(ENA, LOW);
  digitalWrite(ENB, LOW);
}  // end forward()</code></pre><p>Code courtesy of: <a href="https://web.archive.org/web/20210919132433/http://www.instructables.com/id/Driving-Bi-Polar-Stepper-Motor-with-Keyes-L298N/?ALLSTEPS">http://www.instructables.com/id/Driving-Bi-Polar-Stepper-Motor-with-Keyes-L298N/?ALLSTEPS</a></p><h1 id="final-words"><strong>Final Words</strong></h1><p>Once again, I will not be talking about the WeMos D1/ ESP8266&apos;s WIFI since there are many other tutorials out there for that.</p><p>I now have to decide what I&apos;m going to do with this WIFI-capable stepper motor. I was originally going to make some blinds, but I don&apos;t have enough blinds to make it worth it.</p><p>Hopefully, you&apos;ve enjoyed this tutorial, if you have any questions or comments, please get in touch with me.</p>]]></content:encoded></item><item><title><![CDATA[Getting Started with WeMos D1 on Mac OSX]]></title><description><![CDATA[<p>It took me way too long to get started with the WeMos D1 R2 board, partly because mine is an eBay knockoff.</p><h1 id="getting-started"><strong><strong><strong>Getting Started</strong></strong></strong></h1><ol><li>Download and install the Arduino IDE (just called Arduino): <a href="https://web.archive.org/web/20210919143612/https://web.archive.org/web/20180826043911/https://www.arduino.cc/en/Main/Software">https://www.arduino.cc/en/Main/Software</a> Note: I use version 1.6.8, newer versions may</li></ol>]]></description><link>https://rjdlee.com/getting-started-with-wemos-d1-on-mac-osx-2/</link><guid isPermaLink="false">63d744ef11a23e6694bf7912</guid><category><![CDATA[Overflow]]></category><dc:creator><![CDATA[Richard Lee]]></dc:creator><pubDate>Mon, 30 Jan 2023 04:18:18 GMT</pubDate><content:encoded><![CDATA[<p>It took me way too long to get started with the WeMos D1 R2 board, partly because mine is an eBay knockoff.</p><h1 id="getting-started"><strong><strong><strong>Getting Started</strong></strong></strong></h1><ol><li>Download and install the Arduino IDE (just called Arduino): <a href="https://web.archive.org/web/20210919143612/https://web.archive.org/web/20180826043911/https://www.arduino.cc/en/Main/Software">https://www.arduino.cc/en/Main/Software</a> Note: I use version 1.6.8, newer versions may not work.</li><li>Open Arduino</li><li>Open File &#x2192; Preferences</li><li>Copy and paste <a href="https://web.archive.org/web/20210919143612/https://web.archive.org/web/20180826043911/http://arduino.esp8266.com/stable/package_esp8266com_index.json">http://arduino.esp8266.com/stable/package<em>esp8266com</em>index.json</a> into Additional Boards Manager URLs field. You can add multiple URLs, separating them with commas</li><li>Open Tools &#x2192; Board:xxx &#x2192; Boards Manager and install esp8266 by ESP8266 Community</li><li>Open Tools &#x2192; Board:xxx and select WeMos D1 R2 &amp; mini.</li><li>Open Tools &#x2192; Upload Speed and select 115200. If you&apos;re brave, you can try 230400 to upload code to the board faster, but watch the console for errors.</li><li>Download a driver so your Mac recognizes the WeMos D1 board (the site looks sketchy, but I can confirm the driver is legit): Note: This driver is needed for both real and knockoff D1 boards. <a href="https://web.archive.org/web/20210919143612/https://web.archive.org/web/20180826043911/http://www.wch.cn/download/CH341SER_MAC_ZIP.html">http://www.wch.cn/download/CH341SER<em>MAC</em>ZIP.html</a></li><li>Plug your WeMos D1 board into your Mac using a micro USB.</li><li>Open Tools &#x2192; Port:xxx and in the dropdown, select the option with &quot;usb&quot; in its name.</li><li>Your Tools dropdown should look like this:</li></ol><hr><h1 id="testing"><strong><strong><strong>Testing</strong></strong></strong></h1><ol><li>To make sure everything is working properly, let&apos;s download WeMos&apos; example files: <a href="https://web.archive.org/web/20210919143612/https://web.archive.org/web/20180826043911/https://github.com/wemos/D1_mini_Examples/archive/master.zip">https://github.com/wemos/D1<em>mini</em>Examples/archive/master.zip</a></li><li>Rename the uncompressed directory to D1<em>mini</em>Examples</li><li>In Arduino, open File &#x2192; Preferences</li><li>In Finder, navigate to the Sketchbook location show in in Arduino&apos;s Preferences</li><li>Move the D1<em>mini</em>Examples directory to Sketchbook location</li><li>The path will look like Sketchbook<em>directory/D1</em>mini_Examples</li><li>Restart the Arduino IDE</li><li>All examples are under File&#x2192;Sketchbook&#x2192;D1<em>mini</em>Examples</li></ol><hr><h1 id="hello-world"><strong><strong><strong>Hello World</strong></strong></strong></h1><ol><li>Open File&#x2192;Sketchbook&#x2192;D1<em>mini</em>Examples&#x2192;01.Basics&#x2192;HelloWorld</li><li>Click Upload</li><li>After upload, open Tools&#x2192;Serial Monitor, set baudrate to 9600 baud</li></ol><hr><h1 id="digital-io"><strong><strong><strong>Digital I/O</strong></strong></strong></h1><p>If you&apos;ve gotten to this point, good work! Now we&apos;re going to learn a bit more about the WeMos D1&apos;s I/O ports. They&apos;re f***ed.</p><p>The fact that I have a knockoff board doesn&apos;t help either. Anyways, enough complaining. Basically, the labels on the WeMos D1 are not the same as those used in Arduino programs.</p><p>As an example, if I want to use digital port 3 (D3) as an output, this is the code I would use:</p><pre><code class="language-c">void setup() {  
  pinMode(5, OUTPUT);
}

void loop() {  
  // Turn on for 1 second
  digitalWrite(5, HIGH);
  delay(1000);

  // Then off for 1 second
  digitalWrite(5, LOW);
  delay(1000);
}
</code></pre><p>I&apos;ve created a couple of reference tables for your convenience.</p><!--kg-card-begin: html--><table style="border-collapse: collapse; border-spacing: 0px; font-size: 16px; line-height: 1.5; margin: 0px 0px 30px; text-align: left; width: 740px; color: rgb(102, 102, 102); font-family: &quot;droid serif&quot;, serif; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><thead><tr><th style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px; color: rgb(17, 17, 17); font-weight: 700;">Official WeMos D1 Board Label</th><th style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px; color: rgb(17, 17, 17); font-weight: 700;">Knockoff WeMos D1 Board Label</th><th style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px; color: rgb(17, 17, 17); font-weight: 700;">Digital Port</th><th style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px; color: rgb(17, 17, 17); font-weight: 700;">Arduino Software</th></tr></thead><tbody><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">RX</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">RX&lt;-D0</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D0</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">3</td></tr><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">TX</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">TX-&gt;D0</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D1</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">1</td></tr><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D0</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D2</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D2</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">16</td></tr><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">SCL/D1</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D15/SCL/D3</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D3</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">5</td></tr><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">SDA/D2</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D14/SDA/D4</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D4</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">4</td></tr><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D3</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D13/SCK/D5</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D5</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">0</td></tr><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D4</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D12/MISO/D6</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D6</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">2</td></tr><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D5</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D11/MOSI/D7</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D7</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">14</td></tr><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D6</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D8</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D8</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">12</td></tr><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D7</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">TX1/D9</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D9</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">13</td></tr><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">SS/D8</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D10/SS</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D10</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">14</td></tr><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">MOSI/D7</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D11/MOSI</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D11</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">13</td></tr><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">MISO/D6</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D12/MISO</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D12</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">12</td></tr><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">SCK/D5</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D13/SCK</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D13</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">14</td></tr><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">SDA/D2</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D14/SDA</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D14</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">4</td></tr><tr><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">SCL/D1</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D15/SCL</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">D15</td><td style="border-bottom: 1px solid rgb(238, 238, 238); padding: 10px 5px;">5</td></tr></tbody></table><!--kg-card-end: html--><p>Note: I can&apos;t guarantee the official WeMos D1 board mappings are correct, but they&apos;re based off this:<br><a href="https://web.archive.org/web/20210919143612/https://web.archive.org/web/20180826043911/http://www.instructables.com/id/Programming-the-WeMos-Using-Arduino-SoftwareIDE/?ALLSTEPS">http://www.instructables.com/id/Programming-the-WeMos-Using-Arduino-SoftwareIDE/?ALLSTEPS</a></p><hr><h1 id="troubleshooting"><strong><strong><strong>Troubleshooting</strong></strong></strong></h1><ol><li>Restart the Arduino IDE</li><li>Try a different micro USB cable</li><li>In Finder, navigate to ~/Library (to get to Library, click on Go, then press control, and click on Library). Rename Arduino15 to Arduino15-backup.Download and install the latest version of the Arduino IDE (replace your current version)Open Arduino, re-install esp8266 in the Boards Manager.</li></ol><p>If you have any other troubleshooting tips, send them my way.</p><hr><h1 id="notes"><strong><strong><strong>Notes</strong></strong></strong></h1><p>A good amount of the content here is based on the official WeMos documentation ( <a href="https://web.archive.org/web/20210919143612/https://web.archive.org/web/20180826043911/http://www.wemos.cc/tutorial/get_started_in_arduino.html">http://www.wemos.cc/tutorial/get<em>started</em>in_arduino.html</a> ). I&apos;ve simply filled in many of the gaps preventing my board from functioning properly.</p><p>This is what an actual WeMos D1 board looks like:</p><figure class="kg-card kg-image-card"><img src="https://web.archive.org/web/20210919143612im_/https://web.archive.org/web/20180826043911im_/https://svbtleusercontent.com/tszry2pdrn7zlw_small.jpg" class="kg-image" alt="r2_1.jpg" loading="lazy"></figure><p>This is what my knockoff WeMos D1 board looks like:</p><figure class="kg-card kg-image-card"><img src="https://web.archive.org/web/20210919143612im_/https://web.archive.org/web/20180826043911im_/https://svbtleusercontent.com/qaafchea71yyjq_small.jpg" class="kg-image" alt="IMG_0539.jpg" loading="lazy"></figure><hr><h1 id="resources"><strong><strong><strong>Resources</strong></strong></strong></h1><ul><li><a href="https://web.archive.org/web/20210919143612/https://web.archive.org/web/20180826043911/http://www.wemos.cc/tutorial/get_started_in_arduino.html">http://www.wemos.cc/tutorial/get<em>started</em>in_arduino.html</a></li><li><a href="https://web.archive.org/web/20210919143612/https://web.archive.org/web/20180826043911/http://forum.wemos.cc/topic/102/wemos-d1-r2-not-see-usb-ports-on-mac/2">http://forum.wemos.cc/topic/102/wemos-d1-r2-not-see-usb-ports-on-mac/2</a></li><li><a href="https://web.archive.org/web/20210919143612/https://web.archive.org/web/20180826043911/http://www.instructables.com/id/Programming-the-WeMos-Using-Arduino-SoftwareIDE/?ALLSTEPS">http://www.instructables.com/id/Programming-the-WeMos-Using-Arduino-SoftwareIDE/?ALLSTEPS</a></li><li><a href="https://web.archive.org/web/20210919143612/https://web.archive.org/web/20180826043911/http://forum.wemos.cc/topic/102/wemos-d1-r2-not-see-usb-ports-on-mac/2">http://forum.wemos.cc/topic/102/wemos-d1-r2-not-see-usb-ports-on-mac/2</a></li></ul><hr><h1 id="final-words"><strong><strong><strong>Final Words</strong></strong></strong></h1><p>Now you know how to set up your WeMos D1 board and use its ports. I&apos;m going to leave out discussion on its WIFI capabilities because there&apos;s plenty of documentation on that already.</p><p>In the next article, I will discuss how to connect a stepper motor to the WeMos D1.</p>]]></content:encoded></item><item><title><![CDATA[The Case for Sleeping]]></title><description><![CDATA[<p>Back in high school, my life revolved around Minecraft. From the time I got back from school to late into the night, my Skype online friends and I were Minecrafting. On weekends, I would record videos for my <a href="https://web.archive.org/web/20210919134700/https://www.youtube.com/watch?v=U3w18pzX2Kg">Youtube channel</a>. As fun as this time was, sleeping 4-5 hours each</p>]]></description><link>https://rjdlee.com/the-case-for-sleeping/</link><guid isPermaLink="false">63d744c111a23e6694bf7902</guid><category><![CDATA[Shower Thought]]></category><dc:creator><![CDATA[Richard Lee]]></dc:creator><pubDate>Mon, 30 Jan 2023 04:17:35 GMT</pubDate><content:encoded><![CDATA[<p>Back in high school, my life revolved around Minecraft. From the time I got back from school to late into the night, my Skype online friends and I were Minecrafting. On weekends, I would record videos for my <a href="https://web.archive.org/web/20210919134700/https://www.youtube.com/watch?v=U3w18pzX2Kg">Youtube channel</a>. As fun as this time was, sleeping 4-5 hours each night was taking a toll on my body. I&apos;d fall asleep in half my classes, developed IBS, and my sinuses would never stop running.</p><p>After spending a few thousand hours of my life in Minecraft, I knew it was time to move on in my final year of high school. I started sleeping enough (9 hours) that I could naturally wake up in time for school. Magically, my IBS went away, my sinuses cleared up, and of course, I stopped sleeping in class.</p><p>I&apos;m not sure if it&apos;s increased mental awareness or increased physical sensitivity, but I&apos;ve become a lot more sensitive to sleep deficiency. If I lose even a few hours of sleep and don&apos;t make it up, my IBS and sinus issues return. However, the biggest impact is in my mental state. When I&apos;m tired, I become angry far easier and a lot less patient. I&apos;ve noticed this behaviour in others as well.</p><p>One objection to sleeping 9 hours a day is that I&apos;m missing out on life, but are you really living life if you&apos;re a zombie? When someone is tired, they&apos;re generally observing life rather than participating it. It&apos;s almost like they&apos;re living in third person. Experiences don&apos;t trigger the same emotions or form the same memories that you&apos;ll remember later in life. On top of this, you lose the self control to put down your electronics and you&apos;ll often end up sleeping even later in a positive (it&apos;s actually a pretty negative thing) feedback cycle.</p><p>All this sounds great, but that&apos;s not the full story. Once in a blue moon, there will be a night I stay up and experience intense emotions and inspiration. These times have lurched me into action more than any other time.</p><p>At some point in high school, I was obsessed with LED lights. I knew they were the future and wanted to take part in selling them. My friend and I schemed about the idea, but we never really kicked it into action. Then one night, I felt an intense urge to put make my idea a reality. I spent hours building a business plan and began making sales calls the next day.</p><p><em>I wasn&apos;t the best salesman so the business didn&apos;t really pan out, but I&apos;m amazed at how my emotions that night suddenly kicked me into action.</em></p><p>It&apos;s hard to say whether staying up later resulted in these heightened emotions or whether my racing brain kept me up later, but it shows that it doesn&apos;t always make sense to maintain a strict sleep schedule. This can be likened to the tales of programmers working late into the night producing their best work. As a software engineer myself, I can attest to having a few of these sessions in the past. Whether I produced quality work is unclear.</p><p>As long as it doesn&apos;t become a pattern, it may not be a bad idea to stay up late every so often. Who knows what inspiration you&apos;ll get. Just be sure to make up the sleep.</p><p>Before wrapping up, let&apos;s quickly talk about how much sleep you should get. I found that simply letting your body wake up naturally is the best way to figure out how much sleep you need. I found myself waking up after very close to 9 hours of sleep every night. There are two separate sleep concepts you should be aware of: sleep debt and sleep cycles.</p><p>Sleep debt accumulates when you get less sleep than your body requires (9 hours for me). You should attempt to make up this lost sleep by sleeping longer one day within two weeks. Your body should naturally want to sleep the extra time. From what I&apos;ve read, after two weeks, you&apos;re no longer able to make up the lost sleep and your IQ will be permanently decreased.</p><p>Have you ever been woken up in the middle of an incredible dream? I have been many times. It feels bad when you&apos;re experiencing bliss one moment then wake up and realize you&apos;re back in real life. Sleep cycles correspond to the different phases your brain undergoes during sleep. Interrupting these sleep cycles can lead to grogginess when you wake up. They generally occur in about 90 minute intervals meaning you should sleep in multiples of this (7.5, 9 hours, etc.).</p><p>There&apos;s always those times when you just can&apos;t seem to fall asleep. Resist the urge to grab your phone and start Redditing or messaging people (I fail at this sometimes). Instead, use that time to question all the decisions you&apos;ve made resulting in your sleepless night. In my experience, there&apos;s a few things you can do to increase your chances of falling asleep when you want to.</p><ul><li>Install <a href="https://web.archive.org/web/20210919134700/https://justgetflux.com/">f.lux</a> or enable night mode (if you&apos;re on an Apple device). I&apos;ve had people complain that it makes them sleepy.<em> That&apos;s exactly what it&apos;s supposed to do.</em></li><li>If you feel sleepy, you&apos;ve missed your chance to fall asleep for about another half hour.</li><li>Try not to do any intense physical activity within two hours of going to bed</li><li>Try not to talk (vocally) within an hour of going to bed</li><li>Sleep in a dark and cool room</li></ul><p>I haven&apos;t cited any studies because I wanted to add my personal anecdotes to the conversation about sleep. This is my story of how I recovered from persistent sleep deprivation. Hopefully it inspires you to fix your own sleep so that you can live a more fulfilling life.</p><p>I just spent way too long writing this at work and should probably go home and sleep now. I think it&apos;s worth sacrificing a bit of sleep when I have the inspiration to write though.</p><p><em>Hmm, or maybe I should pick Minecraft back up.</em></p>]]></content:encoded></item><item><title><![CDATA[Managing State in MobX]]></title><description><![CDATA[<p>If you&apos;ve ever used <a href="https://web.archive.org/web/20210919125821/https://redux.js.org/">Redux</a> for managing state in your web application, you&apos;ll know how much boilerplate you have to write before you can mutate its state. Sure, there&apos;s plenty of Redux middleware for reducing boilerplate and making asynchronous requests (such as XHR). However,</p>]]></description><link>https://rjdlee.com/managing-state-in-mobx/</link><guid isPermaLink="false">63d7449c11a23e6694bf78f4</guid><category><![CDATA[Overflow]]></category><category><![CDATA[Frontend]]></category><dc:creator><![CDATA[Richard Lee]]></dc:creator><pubDate>Mon, 30 Jan 2023 04:16:53 GMT</pubDate><content:encoded><![CDATA[<p>If you&apos;ve ever used <a href="https://web.archive.org/web/20210919125821/https://redux.js.org/">Redux</a> for managing state in your web application, you&apos;ll know how much boilerplate you have to write before you can mutate its state. Sure, there&apos;s plenty of Redux middleware for reducing boilerplate and making asynchronous requests (such as XHR). However, the more middleware you add, the more mental overhead you&apos;ll have to deal with.</p><p>I like doing things the JavaScript way; it just makes life so much easier for my team and I. If you&apos;re having to hack around some weird issue in JavaScript, someone has probably already solved the issue, but their solution has gotten lost in the vast JavaScript ecosystem. That is unless you&apos;re doing some insane next level stuff. Why write your own left padding function when you can just use <a href="https://web.archive.org/web/20210919125821/https://www.npmjs.com/package/left-pad">left-pad</a>? Why bother writing a backend for some generic CRUD dashboard if you can just use <a href="https://web.archive.org/web/20210919125821/https://github.com/parse-community/parse-server">Parse</a>?</p><p>Anyways, I digress. Up until a couple months ago, I had the illusion that Redux was the only mainstream state management plugin for React. Then my team told me about <a href="https://web.archive.org/web/20210919125821/https://mobx.js.org/">MobX</a>. Instead of the fancy action/reducer pattern Redux uses, MobX simply makes your data observable. Your app will react anytime your data is mutated.</p><h3 id="motivation"><strong>Motivation</strong></h3><p>In order to motivate the rest of this article, let me provide examples for both MobX and Redux.</p><pre><code>import { createStore, dispatch } from &apos;redux&apos;;

// Command
const ADD_TODO = &apos;ADD_TODO&apos;;

// Action Creator
function addTodo(text) {
  return {
    type: ADD_TODO,
    text,
  };
}

// Reducer
function rootReducer(state = { text: &apos;&apos; }, action) {
    return {
        ...state,
        text: action.text,
    };
}

// Store
const store = createStore(rootReducer);

// Every time the state changes, log it
const unsubscribe = store.subscribe(() =&gt; console.log(store.getState()));

// Output: { text: &apos;dubemon&apos; }
dispatch(addTodo(&apos;dubemon&apos;));

unsubscribe();
</code></pre><pre><code>import { observable, autorun } from &apos;mobx&apos;;

class Store {
    @observable text = &apos;&apos;;
}

const store = new Store();

// Every time the state changes, log it
const disposer = autorun(() =&gt; console.log(store));

// Output: { text: &apos;dubemon&apos; };
store.text = &apos;dubemon&apos;;

disposer();
</code></pre><p><em>Wow, MobX has way less boilerplate. Case closed. Thanks for reading!</em></p><p>That was my first thought when I saw MobX, but I wouldn&apos;t be writing this article if that were the case. Unlike Redux, MobX is completely unopinionated and simply makes your data observable. It&apos;s up to you to structure your data, which can lead to maintainability issues if you&apos;re inexperienced.</p><p>The biggest concept to keep in mind is MobX pairs well with object oriented programming (OOP).</p><p>The one thing MobX and Redux encourage is keeping most of your application&apos;s state in a single store. Having a single store allows you to save your application&apos;s state in snapshots. Snapshots make time travel debugging possible where you can see how the application&apos;s state changed over time. You can also persist snapshots so that a user&apos;s session can be reloaded even if they leave the page.</p><h3 id="data-modelling"><strong>Data Modelling</strong></h3><p>Let&apos;s look at an example of a Store containing a collections of Todo objects. First we define our OOP data models: Todo. Then, we create our Store state, which is a collection of Todo objects.</p><pre><code>class Todo {
	@observable text = &apos;&apos;;
}

class Store {
	@observable todos = [];
}</code></pre><p>Rather than allowing users to directly modify Todo&apos;s internal text property, we should instead provide a setText method in case we ever want to add validation or transformation logic. setText is decorated with action to make it clear that it mutates the state, but this is only cosmetic.</p><pre><code>class Todo {
	@observable text = &apos;&apos;;

	@action
	setText() {
		this.text = text;
	}
}</code></pre><h3 id="data-collections"><strong>Data Collections</strong></h3><p>Managing collections in MobX becomes tricky. If you replace the Todos array with a new array, observers watching for changes to todos will be stuck watching the old array and won&apos;t pick up on changes to the new array. &#xA0;The following snippet is a contrived example of how to modify the Store&apos;s collection without breaking its observers.</p><pre><code>class Store {
	@observable todos = [];

	// BAD: todos is re-assigned
	addTodo(todo) {
		this.todos = [...this.todos, todo];
	}

	// GOOD: todos is mutated in place
	addTodo(todo) {
		this.todos.push([]);
	}

	// BAD: todos is re-assigned
	removeTodo(index) {
		this.todos = [...this.todos.slice(0, index), ...this.todos.slice(index + 1)];
	}

	// GOOD: todos is mutated in place
	removeTodo(index) {
		// Modify the array with
		this.todos.splice(index, 1);
	}
}</code></pre><p>Let&apos;s say we want to keep track of the Todo we&apos;re currently editing. We can add a property for the current Todo and its index in the Todos array.</p><pre><code>@observable currentTodoIndex = 0;
@observable currentTodo = null;

setCurrentTodo(index) {
	this.currentTodo = this.todos[index];

	// It&apos;s likely we&apos;ll also need to access the index at some point
	this.currentTodoIndex = index;
}

getCurrentTodo() {
	return this.currentTodo;
}

getCurrentTodoIndex() {
	return this.currentTodoIndex;
}</code></pre><p>The downside of this approach is now we&apos;re storing two additional pieces of state that more or less describe the same object. An alternative to this approach is to take advantage of MobX&apos;s computed properties and create a getter.</p><pre><code>@observable currentTodoIndex = 0;

setCurrentTodo(index) {
	this.currentTodoIndex = index;
}

// Access this property using store.currentTodo 
@computed get currentTodo() {
	return this.todos[this.getCurrentTodoIndex()];
}

// Alternatively, we can just use a plain old function, 
// but this won&apos;t update observers
getCurrentTodo() {
	return this.todos[this.getCurrentTodoIndex()];
}

getCurrentTodoIndex() {
	return this.currentTodoIndex;
}</code></pre><h3 id="asynchronous-actions"><strong>Asynchronous Actions</strong></h3><p>The examples we&apos;ve gone through so far have been fairly straightfoward. However, at some point, you&apos;ll probably have to make HTTP or some other type of asynchronous requests. We&apos;re going to add <a href="https://web.archive.org/web/20210919125821/https://www.github.com/mobxjs/mobx-utils">mobx-utils</a> to our project to help us in dealing with these async functions. Specifically, mobx-utils has a fromPromise function that wraps a promise that makes it easy to get its current state or observe changes to its state.</p><pre><code>import React, { Component } from &quot;react&quot;;
import { render } from &quot;react-dom&quot;;
import { observer, Provider, inject } from &quot;mobx-react&quot;;
import { observable, autorun } from &quot;mobx&quot;;
import { fromPromise } from &quot;mobx-utils&quot;;

class Store {
	@observable counter = null;

	getCounter() {
		// Get the current value of the counter promise
		// null if the promise is pending
		return this.counter.value;
	}

	fetchData() {
		// Pretend Promise.resolve is making an HTTP request
		this.counter = fromPromise(Promise.resolve(2));
	}

	fetchError() {
		// Pretend Promise.reject is a failed HTTP request
		this.counter = fromPromise(Promise.reject(3));
	}
}

// Allow our React component to observe our Store&apos;s state
@inject(({ store }) =&gt; ({ store }))
@observer
class Child extends Component {
	render() {
		const { store } = this.props;
		
		// We can display the promise&apos;s current state
		switch (store.counter.state) {
			case &quot;pending&quot;:
			return &lt;div&gt;Loading...&lt;/div&gt;;
			case &quot;rejected&quot;:
			return &lt;div&gt;Ooops... {store.counter.value}&lt;/div&gt;;
			case &quot;fulfilled&quot;:
			return &lt;div&gt;Gotcha: {store.counter.value}&lt;/div&gt;;
			default:
			return &lt;div&gt;Loading...&lt;/div&gt;;
		}
	}
}

class App extends Component {
	store = new Store();

	componentDidMount() {
		// Child will show the pending then the fulfilled state
		this.store.fetchData();

		// Child will show the pending then the rejected state
		setTimeout(() =&gt; this.store.fetchError(), 1000);
	}

	render() {
		return (
			&lt;Provider store={this.store}&gt;
				&lt;Child /&gt;
			&lt;/Provider&gt;
		);
	}
}

render(&lt;App /&gt;, document.getElementById(&quot;root&quot;));</code></pre><p>If you ever find yourself mutating the same property in an asynchronous function multiple times, use <a href="https://web.archive.org/web/20210919125821/https://mobx.js.org/best/actions.html#flows">asynchronous flows</a> to ensure your observers correctly receive the mutations.</p><h3 id="splitting-your-store"><strong>Splitting Your Store</strong></h3><p>Now you know patterns for creating a store, setting and getting your observable properties, and dealing with asynchronous functions. You have bigger dreams than that though.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://web.archive.org/web/20210919125821im_/https://rjdlee.com/content/images/2018/11/All-Your-State-Are-Belong-to-Us.jpg" class="kg-image" alt loading="lazy"><figcaption>DON&apos;T GET TOO CARRIED AWAY</figcaption></figure><p>It&apos;s not unreasonable to think you&apos;ll have 50 observable collections in your store. However, keeping all of these in your root store is unreasonable. Instead, your root store can contain sub-stores. Generally your sub-stores should not rely on each other, but maybe you didn&apos;t fully plan things through like I did. If a store has to observe another store, don&apos;t do that in the constructor because the other store may not be initialized yet. Instead, create a setup function that will run after the constructor.</p><pre><code>import { observable, autorun } from &quot;mobx&quot;;

class RootStore {
	store1 = new Store1(this)
	store2 = new Store2(this)

	constructor() {
		this.store1.setup();
	}
}

class Store1 {
	@observable counter = 1;

	constructor(rootStore) {
		this.rootStore = rootStore;
		autorun(() =&gt; console.log(this.rootStore.store2.counter));
		// Uncaught exception since store2 is undefined
	}

	setup() {
		autorun(() =&gt; console.log(this.rootStore.store2.counter));
		// Output: 2 \n 3
	}
}

class Store2 {
	@observable counter = 2;

	constructor(rootStore) {
		this.rootStore = rootStore;
	}
}

const store = new RootStore();
store.store2.counter = 3;</code></pre><h1 id="pitfalls"><strong>Pitfalls</strong></h1><p>I wish I was a masterful enough writer to weave in the following pitfalls into the rest of the article. One day I&apos;ll get there. In the meantime, enjoy the fruits of my painful debugging labour.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://web.archive.org/web/20210919125821im_/https://rjdlee.com/content/images/2018/11/Charmander.jpg" class="kg-image" alt loading="lazy"><figcaption>MY FACE WHEN DEBUGGING THESE PITFALLS</figcaption></figure><p>If you define a function, which mutates state as an @action, any mutations inside that function will not trigger observers until the entire action is completed.</p><pre><code>import { observable, action, autorun } from &quot;mobx&quot;;

class Store {
  @observable counter = 1;

  @action actionCount() {
    this.counter = 0;
    this.counter = 1;
  }

  count() {
    this.counter = 0;
    this.counter = 1;
  }
}

const store = new Store();
autorun(() =&gt; console.log(store.counter));
// Output: 1
store.actionCount();
// Output: 1
store.count();
// Output: 0 1</code></pre><p>It is possible to nest non-primitive data types and observe the inner types, but there are some pitfalls. You can nest non-observable types, because they will automatically be observed once added to their parent, but keep in mind that they will not be observable before then.</p><pre><code>import { observable, autorun } from &quot;mobx&quot;;

class Store {
    @observable map = new Map();

    // Not an @action since we want to watch changes as they happen
    actionCount() {
        const arr = [];
        // Triggers autorun on line 17 and adds an observer
        this.map.set(&apos;key&apos;, arr);
        arr.push(1);
        
		// No output
        this.map.get(&apos;key&apos;).push(2);
        
		// Output: { ..., added: [2] }
	}
}

const store = new Store();
autorun(() =&gt; store.map.get(&apos;key&apos;) &amp;&amp;
	store.map.get(&apos;key&apos;).observe(console.log)
);
store.actionCount();</code></pre><p>I hope you learned something about MobX today. I&apos;ve only been using it personally for two months and am sure there&apos;s better patterns than what I&apos;ve described here. I learned a lot from experimenting in <a href="https://web.archive.org/web/20210919125821/http://codesandbox.io/">CodeSandbox</a> and by reading all the other great (but often outdated or not idiomatic) resources on <a href="https://web.archive.org/web/20210919125821/https://github.com/mobxjs/awesome-mobx#real-life-examples">awesome-mobx</a>.</p><p><em>Click <a href="https://web.archive.org/web/20210919125821/https://rjdlee.com/projects/pokedex/">here</a> if you want to see a simple React MobX project I built that implements some of these principles.</em></p>]]></content:encoded></item><item><title><![CDATA[Thoughts on Identity Verification]]></title><description><![CDATA[<p>If you&apos;ve ever been asked to upload a photo of your government ID to prove your identity, you&apos;ve gone through an identity verification process. If you haven&apos;t, this is a screenshot of what Coinbase&apos;s process looks like.</p><p>About a year ago, I</p>]]></description><link>https://rjdlee.com/brainstorm/</link><guid isPermaLink="false">63d7446611a23e6694bf78e4</guid><category><![CDATA[Brainstorm]]></category><dc:creator><![CDATA[Richard Lee]]></dc:creator><pubDate>Mon, 30 Jan 2023 04:16:05 GMT</pubDate><content:encoded><![CDATA[<p>If you&apos;ve ever been asked to upload a photo of your government ID to prove your identity, you&apos;ve gone through an identity verification process. If you haven&apos;t, this is a screenshot of what Coinbase&apos;s process looks like.</p><p>About a year ago, I was shocked to learn some companies still use a manual verification process despite the competitive market of automated providers: Onfido, Acuant, Mitek, Jumio, etc. These findings led me to naively assume I could create my own automated provider using off the shelf deep learning libraries. Of course, things are never that easy. The rest of this post documents what I&apos;ve learned about this industry and where I see it going in the future.</p><p>Any organization with an online presence that deals with real people&apos;s identities and/or money should have an identity verification to reduce fraud, and comply with anti-money laundering (AML) and know-your-customer (KYC) regulations.</p><p>Under US regulation; <em>31 C.F.R. &#xA7; 103.121 implementing PATRIOT Act Section 326</em></p><ul><li><em>A bank must obtain, at a minimum, the following information from the customer prior to opening an account:</em><br>1. <em>Name</em><br>2. <em>Date of birth, for an individual</em><br>3. <em>Address</em><br>4. <em>Identification number (for a U.S. person, an SSN)</em></li></ul><p>Market research was mostly directed towards the Canadian market where I talked to a number of financial organizations. There are <a href="https://web.archive.org/web/20211019122501/http://fintrac-canafe.gc.ca/guidance-directives/client-clientele/Guide11/11-eng.asp">separate processes</a> for verifying the identities of individuals, corporations, and other entities to comply with the Proceeds of Crime (Money Laundering) and Terrorist Financing Act (PCMLTFA).</p><p>For individuals, there are three methods of verification. The first two are single process where the individual must provide a government issued photo identification document <strong><strong>in person </strong></strong>or where you retrieve the credit file (must have 3 years of history) for the individual. The third method is a dual process where individuals must submit two different documents such as an utility bill, credit card statement, or Canadian birth certificate.</p><p>Requiring an individual to be physically present defeats the entire purpose of automated online verification. Credit file lookup solely relies upon an individual providing their name, date of birth, and address. The dual process method requires the most work on the individual&apos;s part since they must retrieve multiple documents, but also provides the greatest assurance the individual&apos;s identity is not stolen.</p><p>Corporations and other entities must provide documents confirming their existence such as the corporation&#x2019;s certificate of corporate status or a partnership agreement.</p><p>In essence, at least for the Canadian market, asking users to submit photos of their photo identification documents doesn&apos;t provide compliance with Canadian regulations. However, there is still value in this type of verification. Organizations that provide goods or services with monetary value for their need to combat fraud on their platforms. For example, AirBnB needs to ensure its users aren&apos;t going to trash their rentals. Roboadvisors simply manage their users&apos; money and don&apos;t need to have the same level of fraud protection.</p><p>Honestly, the market research part of starting a business is so painful, but it&apos;s well worth it. I learned there are two reasons for performing identity verification: KYC and AML compliance, and fraud protection. Within these, credit checks and other types of documents are commonly used for providing an additional layer of verification. It&apos;s also possible corporate and entity verification are an underserved market (pending more research).</p><h1 id="technical-stuff"><strong>Technical Stuff</strong></h1><p>Let&apos;s put on our engineer hat now and &#xA0;imagine what we would need to provide an identity verification solution that provides regulation compliance and fraud protection for both individuals and corporations.</p><p>We need to build an application that requires some or all of the following user input:</p><ul><li>Provide their name, date of birth, and address</li><li>Upload a photo of their photo identification document</li><li>Upload a photo of their face</li><li>Upload a photo of a textual document such as a utility bill or credit card statement</li></ul><p>After performing some kind of trendy, state of the art deep learning, the application will perform some or all of the following actions:</p><ul><li>Determine if the photo identification document is legitimate by comparing it to the photo of the user&apos;s face</li><li>Retrieve the user&apos;s credit file</li><li>Ensure the user&apos;s credit file, photo identification document, textual documents, and provided name, data of birth, and address all match</li><li>Search the user&apos;s information in databases</li></ul><p>The exact set of inputs and outputs will change depending on the customer&apos;s requirements and whether the user is an individual, or representing a corporation or entity.</p><p>The core of the engineering problem is in validating and extracting information from the photo identification or text documents. Validation involves determining whether the document and the uploaded photo of the document are legitimate or not. Extracting information generally involves extracting text using optical character recognition (OCR).</p><p>Let&apos;s focus on photo identification documents for now. Fake driver&apos;s licenses are commonly used for sneaking into bars and have reached a point where they&#x2019;re quite convincing upon visual inspection. The barcodes in driver&apos;s licenses encode information that can be compared to the text on the front of the license. Depending on the quality of a fake document, its barcode and text may have mismatched content. OCR can be used to extract text while a barcode decoder could be used to decode PDF 417 barcodes (common in photo identification documents). If the barcode and text match; the license&apos;s information (especially the license number) should be looked up in a database.</p><figure class="kg-card kg-image-card"><img src="https://web.archive.org/web/20211019122501im_/https://rjdlee.com/content/images/2018/11/Text-equals-barcode-1.jpg" class="kg-image" alt loading="lazy"></figure><p>We also need to ensure photos of photo identification documents have not been manipulated. Metadata (such as EXIF metadata found in JPEG photos), can include information about the means by which a photo was taken. For example, I once came across a document photo where the metadata contained &#x201C;Photoshop&#x201D;. That&apos;s an amateur mistake, though. Metadata can be edited to hide signs of manipulation and more advanced techniques need to be used: error-level analysis, luminance gradient, average distance, HSV and Lab colorspace histograms, JPEG resave quality estimate, extract JPEG quantization tables, and copy-move detection. As it stands now, these techniques require manual analysis, but I believe it&apos;s possible to automate these with [the almighty] deep learning.</p><p>The techniques described so far only account for text and barcodes. They could easily fall prey to a piece of paper with text on it. Of course, the solution is always deep learning. We could use image classification to label the document (e.g. Ontario&apos;s driver&apos;s license). Users can still spoof this if they have a fake driver&apos;s license, but that&apos;s where database lookup comes in.</p><p>A photo of a document is insufficient to determine if that document has been stolen. This is where ID selfies come in; in addition to a photo of their document, users must also take a photo of themselves holding their document. Facial recognition could then be used to compare the user&#x2019;s face with the face on their document. Unfortunately, from a usability perspective, ID selfies create more friction since users have to provide three photos instead of two (front, back, selfie). The selfie&apos;s resolution would be too low for extracting the front of the document&apos;s data.</p><p>What happens if a user holds up a piece of paper with a face on it or is wearing a mask? We can user a technique called <a href="https://web.archive.org/web/20211019122501/http://web.yonsei.ac.kr/jksuhr/papers/Face%20Liveness%20Detection%20Based%20on%20Texture%20and%20Frequency%20Analyses.pdf">liveness analysis</a>, which basically requires users to take a video ID selfie. In this video, they are asked to make facial gestures or say something. Once again, deep learning can be used to ensure the face belongs to a living human and that they are not wearing a mask.</p><p>If you think we&#x2019;re done, think again. Check out this very convincing deepfake of Obama.</p><figure class="kg-card kg-embed-card kg-card-hascaption"><iframe src="https://web.archive.org/web/20211019122501if_/https://www.youtube.com/embed/cQ54GDm1eL0?feature=oembed" frameborder="0" allow="autoplay &apos;self&apos;; fullscreen &apos;self&apos;" allowfullscreen id="fitvid965341" style="max-width: 100%; position: absolute; top: 0px; left: 0px; width: 740px; height: 416.25px;"></iframe><figcaption>OBAMA DEEPFAKE</figcaption></figure><p>A user could use a deepfake video in lieu of their actual face. For those that don&#x2019;t know, deepfakes are a relatively recent invention where deep learning generative adverisal networks (GAN) are used to apply facial gestures from one person&apos;s face to another person&#x2019;s face. Detecting deepfakes is an active area of research and the only techniques I&#x2019;ve seen are monitoring <a href="https://web.archive.org/web/20211019122501/https://arxiv.org/abs/1806.02877">eyebrow movements</a> and <a href="https://web.archive.org/web/20211019122501/https://arxiv.org/pdf/1811.00656.pdf">warping of the face</a>.</p><p><strong><strong>The scariest part about deepfakes is the fact that any methods for detecting their inauthenticity can be used to improve the underlying deep learning models.</strong></strong></p><p>In summary, a number of techniques can be employed to detect lost, stolen, or fake documents, but these are not foolproof, especially with the advent of deepfakes. The techniques discussed were analyzing metadata, error-level analysis, OCR, barcode detection, image classification, and liveness analysis. These techniques require users be able to submit photos of the front and back of their documents, ID selfies, and video ID selfies.</p><p><em>I did not dive into deep learning specifics because I am not an expert at deep learning. I understand the basics, but I have no clue why how to design my own networks. How do you choose between 34 or 100 layers other than guess and test?</em></p><h2 id="closing-thoughts"><strong>Closing Thoughts</strong></h2><p>I&#x2019;ve had a lot of conversations with product managers, compliance officers, and executives at financial tech companies in the past few months. Existing automated photo document verification appear to be serving the market sufficiently. There is greater demand for complete anti-fraud solutions because a lot of these companies are building their own fraud decision systems that combine signals from a number of user verification techniques including document verification, credit checks, and database lookups. There are a number of companies entering the complete solution space, but their decision-making processes are not always transparent nor accurate.</p><p>One of the largest cryptocurrency exchanges, Coinbase, received bad press for facilitating actors performing money laundering. In the fourth quarter of 2017, cryptocurrency exchanges saw 10 times increase in verifications for clients. This surge has overwhelmed exchanges and forced some to stop accepting new customers due to slow, manual verification processes. If Coinbase had been using a [hypothetical] complete anti-fraud solution, they wouldn&apos;t have run into these bottlenecks.</p><p>I hope you enjoyed this brief overview on the identity verification space. If anything here piqued your interested or if I made any mistakes, please reach out to me on LinkedIn!</p><hr><p><em>Disclaimer: This blog does not provide legal advice and does not create an attorney-client relationship. If you need legal advice, please contact an attorney directly.</em></p>]]></content:encoded></item><item><title><![CDATA[How LED Bulbs Illuminated My Life]]></title><description><![CDATA[<p><em>I love LED lights. Anytime I see an LED light, I give a nod of approval. This is the story of how I became obsessed with LED lights.</em></p><hr><p>It all started at the University of Toronto science camp. Along with my brother and cousin, we scavenged any and all the</p>]]></description><link>https://rjdlee.com/how-led-bulbs-illuminated-my-life/</link><guid isPermaLink="false">63d7443e11a23e6694bf78d6</guid><category><![CDATA[Brainstorm]]></category><dc:creator><![CDATA[Richard Lee]]></dc:creator><pubDate>Mon, 30 Jan 2023 04:15:22 GMT</pubDate><content:encoded><![CDATA[<p><em>I love LED lights. Anytime I see an LED light, I give a nod of approval. This is the story of how I became obsessed with LED lights.</em></p><hr><p>It all started at the University of Toronto science camp. Along with my brother and cousin, we scavenged any and all the gizmos we could find from syringes to batteries to motors to light emitting diodes (LEDs). We didn&apos;t really accomplish much with this loot other than illuminating the LEDs and making the motors spin really fast by connecting the batteries in series. Over the years, I had similar encounters with simple electronics that kept growing my interest.</p><p>In grade 10, I&apos;m not sure what, but something gave me an urge to start an LED business. LED bulbs were just beginning to gain traction. Their upfront cost was significantly higher than that of incandescent or fluorescent bulbs, but their reduced operating costs could pay off in as little as a year. I thought this would be my chance to make it big.</p><p>The first step I took was purchasing $300 worth of LED bulb samples after a few awkward Skype calls with Chinese manufacturers from Alibaba. I did my best to learn how to not get scammed when dealing with distant manufacturers. I received all of my samples in about a month. I knew nothing about shipping goods and was hit with an unwelcome surprise when I was charged for duties and taxes. Since then I&apos;ve learned you&apos;re supposed to tell the manufacturer to write down the value of the goods much lower than they actually are. When I was setting up a merchant account with DHL for shipping my bulbs to customers, they asked what kind of volume I thought I&apos;d be doing. My answer was $10,000/month. You have to <em>be ambitious if you want to make it big.</em></p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://web.archive.org/web/20211019114844im_/https://rjdlee.com/content/images/2018/12/Screen-Shot-2018-12-01-at-11.59.57-PM.png" class="kg-image" alt loading="lazy"><figcaption>MY FIRST ORDER</figcaption></figure><p>I wanted to be selling quality products so I had to do quality testing on my LED bulbs. I was a high school student. I didn&apos;t have money for fancy equipment like <a href="https://web.archive.org/web/20211019114844/https://www.alibaba.com/trade/search?fsb=y&amp;IndexArea=product_en&amp;CatId=&amp;SearchText=Thermal+Shock+Test+Chamber">thermal shock test chambers</a> for testing the lights. Instead, I just gave my parents&apos; house a free makeover with one condition - the lights always had to be on.</p><p>For a long while, I distracted myself with purchasing and testing the quality of my Alibaba samples. Not much energy was put into figuring out how to actually sell the bulbs.</p><p>I took a hiatus to play Minecraft until grade 12 when I finally broke my addiction. This time around, I had confidence in my purchasing skills and knew I had to focus on actually selling the bulbs. I set up a Wordpress shop website and spent a bunch of time taking photos of samples and writing copy. Once again, I distracted myself with things that didn&apos;t matter. I purchased even more bulb samples, but never actually came around to marketing. However, I did manage to sell a few bulbs to one of my teachers.</p><p>I was doubting whether I&apos;d be able to sell any bulbs before I&apos;d even tried. Ikea had just announced it would only sell LED fixtures and bulbs by 2016. I pondered on how I could provide more value to the LED industry instead of just being a middleman. Eventually, I came up with a pretty bright idea (sorry).</p><p>LED bulbs are composed of three primary parts:</p><ul><li>Housing (acts as a heatsink)</li><li>LED array (contains one or more LEDs)</li><li>Driver (a circuit that regulates power to the LED array)</li></ul><p>I found that the driver is generally the limiting factor in the lifespan of bulbs and contributes a significant portion to their cost. Due to their cost, manufacturers use cheaper components in the drivers (especially cheap capacitors), resulting in even lower lifespans.</p><p>My idea was to separate the driver from the rest of the bulb. This way, multiple bulbs could share a single, high-quality driver that could easily be replaced if it ever broke. My electrical knowledge was too limited at this point to know if this driverless LED bulb idea was actually viable.</p><p>Fast forwarding to my first year in university, I had a teaching assistant, John, who was the only reason I passed my physics course. John was an electrical engineer pursuing a quantum computing Ph.D. I pitched my driverless LED bulb idea to him. He said the idea was viable and became my &#x201C;co-founder&#x201D;.</p><p>To fund development, we decided to resell normal LED bulbs as middlemen in the short term. This time I create a landing page, product catalog, and brochure.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://web.archive.org/web/20211019114844im_/https://rjdlee.com/content/images/2018/12/Betta-Lux-Logo-2.jpg" class="kg-image" alt loading="lazy"><figcaption>MY FANCY NEW BRANDING</figcaption></figure><p>We began to cold call local small business owners. Out of 67 calls, I landed 0 meetings and John landed 1 meeting. He gave the business some samples and never heard back afterward. One of my calls was with the international construction project manager at Costco. I couldn&#x2019;t explain why our bulbs were better than any of the other LED bulbs on the market. Our sales pitch and material lacked a solid value proposition. My pitch delivery was also terrible and I probably came off as a typical telemarketer. We also sent a &quot;proposal&quot; to the Toronto Transit Commission:</p><hr><p><em>Hello TTC,</em></p><p><em>We are Beta Lighting, an LED lighting solutions company. We have operated in Ontario for several years focusing on lighting solutions for local organizations. We sell a wide variety of LED lights including bulbs and tubes and design solutions to retrofit existing CFL technology with LED technology.</em></p><p><em>As passengers of the TTC subways and buses, we noticed the infrastructure uses CFL lighting. We believe we can work with the TTC to retrofit its infrastructure with energy-efficient and environmentally-friendly LED lighting. The costs can be recouped in 2 years and after that, it&#x2019;s all savings.</em></p><p><em>What do we, Beta Lighting, need to do to help the TTC make the move to LED lighting?</em></p><hr><p>Eventually, we lost momentum as we got caught up in other things in life. Since then, LED bulbs have gone on to absolutely dominate the lighting market.</p><p><em>That&apos;s the story. Now let&apos;s talk about what I learned and where the LED market is at now.</em></p><h1 id="lessons-learned"><strong>Lessons Learned</strong></h1><p>Honestly, the biggest mistake I made was not committing to the idea. When I first had the idea in grade 10 (2010), I was ahead of the curve. Each time I took a hiatus, my chances worsened. At some point, I toyed with the idea of using Fulfillment by Amazon, but I&apos;m not sure why I didn&apos;t actually commit. Instead, I focused on cold call tactics, which aren&apos;t great for a low margin, low price product like LED bulbs. Even Facebook ads were cheap at the time. One of the biggest struggles I have when starting a business is spending money where it counts. I&apos;m a frugal person in general and find it hard to spend any money even when there&apos;s the possibility of a payoff.</p><h1 id="the-state-of-the-led-market"><strong>The State of the LED Market</strong></h1><p>I haven&apos;t extensively researched current LED bulbs, but I believe their cost has decreased because they have cheaper drivers and superior heat management (allowing their housing to be smaller). The first LED bulb samples I purchased had a heavy metal heatsink as its housing. Now, the housings are just cheap plastic.</p><figure class="kg-card kg-image-card"><img src="https://web.archive.org/web/20211019114844im_/https://rjdlee.com/content/images/2018/12/LED-Comparison.jpg" class="kg-image" alt loading="lazy"></figure><p>Interestingly, the Amazon Basics LED bulbs are only rated for a 15,000hour lifespan when the LED bulbs I bought were rated for 50,000hours. Light emitting diodes don&apos;t burn out like incandescent bulbs. Instead, they slowly dim as they get older. I&apos;ve seen claims that some diodes will retain 80% of their original brightness after 100,000hours or 11.5 years of continuous use. Clearly, the driver is the limiting factor here.</p><p>The brightness or amount of light emitted by a source is measured in lumens, a unit of luminous flux. The 40watt incandescent equivalent Amazon Basics LED bulbs require 6.5watts to output 450lm. The bulbs I purchased required 5watts to output 400-450lm.</p><p>Of course I never actually tested my LED bulbs&apos; full lifespan, luminous flux, or power requirements since I didn&apos;t have the right equipment. However, from the numbers, it looks like they were superior in every way except price, coming in at $3.02/bulb from the manufacturer compared to $2.33/bulb from Amazon (who is a middleman and adds to the cost).</p><p>I didn&apos;t mean for this to become a post about Amazon Basics bulbs, but there is something weird happening there. If I go back to Alibaba, the LED bulbs have specifications more similar to my original bulbs, but cost a third of the price. It&apos;s not unthinkable that Alibaba manufacturers would overstate their bulbs&apos; actual specifications.</p><p>LED bulbs have given rise to new types of lighting fixtures where the LEDs are integrated directly into them. When the LED dies, the fixture dies. For the average consumer, this is fine as the LEDs could last upwards of a decade with light use.</p><p>Let&apos;s revisit the driverless LED bulb idea. It already existed at the time I thought of it, but was still an <a href="https://web.archive.org/web/20211019114844/https://finance.yahoo.com/news/led-lighting-market-grow-over-123000345.html">emerging technology</a>. <a href="https://web.archive.org/web/20211019114844/https://www.ledsupply.com/blog/understanding-led-drivers/">AC drivers</a> function exactly like I envisioned. Multiple LED bulbs could be connected to a single AC driver as long as they match its voltage and current specifications (depends on if the bulb wiring is series and/or parallel).</p><p>Every time I think about my timing in the LED bulb market, I give myself an undeserved pat on the back. I was ahead of the curve, but failed to execute my vision.</p><hr><p><em>If you&apos;re interested, you can check out the <a href="https://web.archive.org/web/20211019114844/https://rjdlee.com/projects/lights/index.html">landing page</a> and <a href="https://web.archive.org/web/20211019114844/https://rjdlee.com/projects/lights/catalogue/index.html">brochure</a> I made for Betta Lux. Hit me up if you want a lights.</em></p>]]></content:encoded></item><item><title><![CDATA[How a Software Engineer Cooks]]></title><description><![CDATA[<p>Despite being allergic to [almost] everything, I love food and I love cooking. Being the competitive person I am, I want to hone my culinary skills to that of a Michelin-starred chef. This is an extremely ambitious goal of course, especially because I have to juggle it with software engineering.</p>]]></description><link>https://rjdlee.com/how-a-software-engineer-cooks/</link><guid isPermaLink="false">63d7440811a23e6694bf78c5</guid><category><![CDATA[Shower Thought]]></category><category><![CDATA[Food]]></category><dc:creator><![CDATA[Richard Lee]]></dc:creator><pubDate>Mon, 30 Jan 2023 04:14:40 GMT</pubDate><content:encoded><![CDATA[<p>Despite being allergic to [almost] everything, I love food and I love cooking. Being the competitive person I am, I want to hone my culinary skills to that of a Michelin-starred chef. This is an extremely ambitious goal of course, especially because I have to juggle it with software engineering. However, I believe certain engineering principles can be applied to culinary education.</p><h1 id="understanding-your-customers"><strong>Understanding Your Customers</strong></h1><p>When you got a fine dining restaurant, you&apos;re not going for just the food. You&apos;re going for the service, ambiance, and novel ideas. The plates you&apos;re eating from were probably made from a local potter. The servers will notice every single action you make and appear at just the right times. Your food will come out at just the right time. These restaurants perform an immense number of activities to create exceptional customer experiences.</p><p>This is similar to how engineers must focus on creating great user experiences. It&apos;s not so much about what you build as it is what you contribute. If you&apos;re building a library to share with other engineers, you need documentation and tooling. If you&apos;re building a new messaging app, you need to create a user interface anyone can understand and use. In the first example, the target customer is other engineers, and in the second, the target customer is end users.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://web.archive.org/web/20210919144235im_/https://rjdlee.com/content/images/2019/01/Grandma-Using-Computer.jpg" class="kg-image" alt loading="lazy"><figcaption>BEST OF LUCK IF YOUR TARGET MARKET IS THE ELDERLY</figcaption></figure><p>Although it&apos;s entirely possible to build something and hope customers will come, your chances will be a lot better if you think about the problem you&apos;re trying to solve before creating anything. You might see Michelin starred chefs simply cooking from their heart, but bear in mind all the chefs who did the same thing and didn&apos;t make it (I&apos;ve met a lot of them). Maybe you&apos;re opening a new bubble tea shop because there&apos;s a certain part of the city with a high number of young Chinese people who are underserved in terms of bubble tea. Unlike software problems, food problems are generally not major pain points but are luxuries.</p><p>Let&apos;s look at another example. Near Niagara Falls, a popular tourist destination, a restaurant on a busy road went out of business. An enterprising couple had been keeping their eyes on the Niagara Falls restaurant market for two years. When they saw this particular restaurant close, they sensed their opportunity and purchased the place. They turned the restaurant into a family-oriented place. Their plan was a complete success and within another two years, they sold the restaurant for a healthy profit. This couple understood the market and knew there were lots of young families passing by to visit Niagara Falls. These families were looking for a quick kid-friendly bite after a long drive. I didn&apos;t go into detail here because the important part is that this couple spent two years understanding their customers and jumped on the opportunity they found.</p><p>Understanding your basic customer demographics (age, sex, income, race, location, etc.) goes a long way when starting any business. However, you&apos;re just hypothesizing your solution. Software engineers can use the agile methodology to test their hypotheses to minimize false positives. Applying this methodology to food can mean testing products at a farmer&apos;s market or searching for areas elsewhere in the world with similar demographics and looking at what restaurants exist there. If demand is limited, then perhaps you&apos;re not addressing a real problem or you need to work on your solution. You need your target market to love your product because acquiring new customers is a lot harder than keeping existing customers. This is especially important in the food world, which is a physical process, unlike most software. If people like your food, they&apos;re going to bring their friends and family.</p><h1 id="solving-your-customers-needs"><strong>Solving Your Customer&apos;s Needs</strong></h1><p>Once you&apos;ve found your target market, you need to come up with a solution to address their [food-related] problems. This goes beyond just coming up with a dish. You need to create an entire experience (not necessarily a fine dining experience).</p><p>When someone first begins software development, they&apos;ll often solve a problem by patching together pieces of code other people wrote into something that works, but doesn&apos;t account for customer experience. It will probably be difficult for future developers to maintain and will be bug-ridden. As a software engineer refines their craft, they&apos;ll work with their customers to create an optimal experience that precisely solves the problem without wasted work and uses fundamental techniques.</p><p>When someone first begins cooking, they&apos;ll often follow a recipe they&apos;ve chosen based off what they&apos;re craving at the moment. The recipe probably tastes great, but there&apos;s no way it can account for every type of person that will every cook it. In short, it&apos;s a suboptimal solution. As the cook refines their craft, they&apos;ll understand their customers and use fundamental techniques to create an optimal experience that precisely solves the problem.</p><p>Identifying a problem is a difficult task. Creating an [almost] optimal solution is even harder. How do you come up with any solution? The key is knowing fundamentals and being able to extract relevant patterns from them. This is what we call abstraction in the software world (maybe in other worlds too). In terms of cooking, the fundamentals are often basic techniques like knife skills, braising, boiling, steaming, grilling, etc. However, the definition of fundamentals changes based on your skill as a chef. The local food movement extends fundamentals to understanding where ingredients come from. The modernist movement extends fundamentals to understanding the chemistry behind core techniques.</p><p>Unless you&apos;re a creative genius, you&apos;ll need a starting point, perhaps an existing dish, that solves the problem to some extent. Then you break that starting point down and question whether each component meets your customers&apos; needs. I&apos;ll make this more concrete with an example in a second.</p><h2 id="solving-fictional-peoples-needs"><strong>Solving Fictional People&apos;s Needs</strong></h2><p>A little while back, a friend showed me an anime about food called Shokugeki. I&apos;m not one for anime, but this particular one blew me away with its culinary insights amidst extravagant &quot;foodgasms&quot;. Despite being fictional, I believe the scenarios it presents are accurate.</p><p>In one of these scenarios (season 1, episode 16), the main character and his father were having a themed cook-off. The theme was to present a dish for people who had just woken up and needed something to prepare them for their day. In this situation, the chefs were given a problem and had to find a solution. The main character used risotto (typically quite rich) as a starting point. He recognized the need for a dish that&apos;s light and nutritious. &#xA0;He substituted stock for apple juice and topped the risotto with apples for lightness and bacon for nutrition.</p><p>His father used ramen (also typically quite rich) as a starting point but made a few modifications. He infused the noodles with yuzu citrus to make them more refreshing. The stock was rich, but light since it was made from kombu, shiitake mushrooms, and grated taro instead of the classic pork, kombu, and bonito stock. He recognized that soups do a great job of warming the body (great if you&apos;re just waking up) and further emphasized this effect by adding chili, ginger, and garlic. To finish the dish off, he prepared toppings like onion, carrot, and tempeh (similar to tofu) with techniques suited to each topping.</p><p>The father won by a landslide because he better addressed the problem. The combination of all the different components of his dish created something that doesn&apos;t overwhelm someone who&apos;s just woken up but instead provides them with the energy needed to start their day. Although the main character&apos;s dish addressed the problem, it didn&apos;t provide the same satisfying energy. The father was able to draw on a far richer set of fundamentals and create a much different dish than ramen while the main character only tweaked a couple aspects of risotto.</p><p>When you&apos;re coming up with a dish, you&apos;re drawing on all of the techniques and knowledge you&apos;ve acquired in the past, and composing it together.</p><h2 id="solving-my-parents-food-needs"><strong>Solving My Parent&apos;s [Food] Needs</strong></h2><p>Let&apos;s go through another example to make things more concrete. This one comes from my own cooking. I&apos;ve had the opportunity of traveling to a lot of different places and experiencing different cuisines in my life. If you were to ask me what my favorite cuisine was, I wouldn&apos;t be able to tell you. Anyways, I was home for the holidays and had the chance to share my cooking with my family. This is going to be a little long so brace yourself.</p><p>One particular day, I was cooking dinner for just my parents. I accounted for a number of factors when formulating the dish:</p><ul><li>My parents are open to any cuisine just like I am</li><li>It&apos;s winter and my parents were spending the day out in the cold so the dish should warm the body</li><li>It had been a couple weeks since my dad last had rice and he was beginning to crave it</li><li>In previous meals, I had been using &quot;restaurant levels&quot; of salt, but this was too much for my family, which preferred milder seasoning</li><li>My mom prefers dishes low in fat</li></ul><p>These factors are great to keep in mind when working on the dish, but I needed a starting point. I scoured the freezer and found beef cheeks, a tough cut with little fat and a lot of tendons that become &quot;melt in your mouth&quot; tender when slow cooked. I decided to use a classic French recipe for the beef cheeks where you slow cook it with stock, red wine, mirepoix (carrots, onion, and celery), and bouquet garni (thyme, parsley, peppercorn, bay leaf).</p><p>Braised beef cheeks are definitely hearty enough to warm the body, but aren&apos;t enough to be satisfying. After a bit more scouring, I found sweet potatoes and persimmons. I didn&apos;t really know what direction to take this dish in so I did a quick search online. I found a recipe for an Indian sweet potato paratha, a type of flakey flatbread.</p><p>A lightbulb went off in my head. This dish was beginning to resemble Mexican barbacoa (slow-cooked meat) tacos. However, I still needed to incorporate rice. American burritos seemed better suited to the task than tacos. Refried beans could be substituted for a sweet potato dhal (lentil stew). Rice could be spiced with garam masala (spice blend) and turmeric. I laced each element of these elements with a bit of ginger to intensify the warmth without overpowering the other flavors.</p><p>The dish was becoming quite heavy with the beef cheeks, sweet potato, and rice components. A sweet salsa would provide a refreshing lightness to the dish. I&apos;d seen mango salsas in the past and thought persimmon could be substituted instead of mango. Sour cream could be used to mellow the pungent spices and provide a thermal contrast (since it&apos;s cold).</p><p>By this point, I&apos;d met all my requirements and come up with a unique dish that my parents loved. This kind of recipe analysis is just the tip of the iceberg. A professional chef would spend far longer analyzing their customers&apos; needs and determining the composition of their dish.</p><h2 id="dont-forget-about-the-gram"><strong>Don&apos;t Forget About the &quot;Gram&quot;</strong></h2><p><em>Gram means Instagram for the uninformed.</em></p><p>Like we&apos;ve discussed, there&apos;s a lot more to cooking in the real world than being given a problem and coming up with a dish to address it. Don&apos;t forget about the rest of the customer experience. For example, if you&apos;re opening that bubble tea shop for young Chinese people, make sure the drinks are photogenic and the lighting is good.</p><p>In a restaurant, your target customers are not only the diners but also your waiters and line cooks. If you create a dish that&apos;s too complicated, the line cooks will be in for a bad day. In software, I see engineers forgetting their customers are also the other engineers working with them. Don&apos;t forget this.</p><h2 id="putting-it-together"><strong>Putting it Together</strong></h2><p>Let&apos;s recap what we learned today. I know you want to just dive into cooking, but you need to spend time understanding your customers&apos; needs. From there, you can begin formulating an experience to meet those needs. That involves composing dishes, ensuring your support staff can operate properly, and creating a suitable atmosphere. There&apos;s a lot of moving parts and I&apos;m still figuring out how to account for all of them. It might just come with experience.</p><p>There&apos;s so much more I want to talk about, but that will have to wait for another time. You may not agree with everything I&apos;ve said here, but this is where I&apos;m at with cooking and software engineering at this point in time. I&apos;m certain I&apos;ll refine my approaches in the future, but hopefully, it gives you a good starting point to hone your own culinary skills.</p>]]></content:encoded></item><item><title><![CDATA[How We Created the Most Clapped Story On Medium Ever]]></title><description><![CDATA[<p><a href="https://web.archive.org/web/20211019130037/https://medium.com/@adrianmachado_30070/hello-world-79436a73e443">SEE IT HERE</a></p><p>I was clapping my friend, Adrian&#x2019;s <a href="https://web.archive.org/web/20211019130037/https://medium.com/@adrianmachado_30070/the-real-fees-of-gig-economies-a0041ff91d0c">blog post</a> earlier today when I found out Medium limits you to 50 claps. My friend deserved far more praise than that, at least 51 claps.</p><p>Behind me were Adrian and Blake. Adrian asked, what if we modify the</p>]]></description><link>https://rjdlee.com/how-we-created-the-most-clapped-story-on-medium-ever/</link><guid isPermaLink="false">63d743e411a23e6694bf78b5</guid><category><![CDATA[Overflow]]></category><category><![CDATA[Frontend]]></category><dc:creator><![CDATA[Richard Lee]]></dc:creator><pubDate>Mon, 30 Jan 2023 04:13:47 GMT</pubDate><content:encoded><![CDATA[<p><a href="https://web.archive.org/web/20211019130037/https://medium.com/@adrianmachado_30070/hello-world-79436a73e443">SEE IT HERE</a></p><p>I was clapping my friend, Adrian&#x2019;s <a href="https://web.archive.org/web/20211019130037/https://medium.com/@adrianmachado_30070/the-real-fees-of-gig-economies-a0041ff91d0c">blog post</a> earlier today when I found out Medium limits you to 50 claps. My friend deserved far more praise than that, at least 51 claps.</p><p>Behind me were Adrian and Blake. Adrian asked, what if we modify the HTTP request. Brilliant! Right there and then, we knew we were onto something.</p><p>We analyzed the HTTP requests being sent when we clapped. It looked like Medium was sending changes to the number of claps rather than absolute values.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://web.archive.org/web/20211019130037im_/https://cdn-images-1.medium.com/max/1600/1*SdVtkT4yziUhqBNWu3-jNQ.png" class="kg-image" alt loading="lazy"><figcaption>PLEASE DON&#x2019;T STEAL MY&#xA0;USERID</figcaption></figure><p>The first step was simply trying to send our own request. With the three of us giggling away, I fired up Postman and copied the request body in. &#x201C;xsrf token missing&#x201D;, we completely forgot about all the request headers. It&#x2019;d be too much effort to company them from Safari to Postman manually.</p><p>Real engineers use Chrome, which has this handy little feature that generates a JavaScript fetch function (sends an HTTP request) based off a past HTTP request. After clapping once, we generated our fetch function and executed that. It worked!</p><p>At this point, Blake and Adrian started working through edge cases. What would happen if we sent more than 50 claps or negative numbers? Could we cause a value to overflow if we sent a really big number? How about alphabetical characters? Medium&#x2019;s defenses seemed impenetrable.</p><p>Then we tried non-integers. At first, it seemed like nothing was happening and we nearly gave up. But as we kept throwing every number we could possibly think of at their API, something strange happened. After sending 0.2 and refreshing the page, my clap counter actually showed 10.2. It worked!</p><p>We hadn&#x2019;t noticed it, but somewhere along the way, Adrian&#x2019;s blog post went from 136 claps to 102. I had somehow given him negative claps. I began frantically trying to re-create the steps I had done. After a solid 20 minutes of trying every possible combination of numbers, we cracked the secret (which is a secret).</p><p>With the secret in our hands, I whipped out a simple script to repeat these steps over and over. We didn&#x2019;t want to raise any alarms over at Medium so we added delays between subsequent loops.</p><p>Here&#x2019;s to the next 12 hours of infinite claps before Medium shuts this party down!</p>]]></content:encoded></item><item><title><![CDATA[Where Do Ideas Come From?]]></title><description><![CDATA[<p>I was recently traveling around Asia. Without cell service or a computer, I spent a whole lot of time doing &#x201C;nothing&#x201D;. My brain had the space to absorb the world around me and to listen to my subconscious aura.</p><blockquote><strong>How are there so many poorly designed sinks that</strong></blockquote>]]></description><link>https://rjdlee.com/where-do-ideas-come-from/</link><guid isPermaLink="false">63d743ba11a23e6694bf78aa</guid><category><![CDATA[Shower Thought]]></category><category><![CDATA[Writing]]></category><dc:creator><![CDATA[Richard Lee]]></dc:creator><pubDate>Mon, 30 Jan 2023 04:13:05 GMT</pubDate><content:encoded><![CDATA[<p>I was recently traveling around Asia. Without cell service or a computer, I spent a whole lot of time doing &#x201C;nothing&#x201D;. My brain had the space to absorb the world around me and to listen to my subconscious aura.</p><blockquote><strong>How are there so many poorly designed sinks that spray water around or are too short to put my hands under?</strong></blockquote><blockquote><strong>Why are houses so monotonous in Toronto?</strong></blockquote><blockquote><strong>Is the key to success truly just persistence and doing what other people won&#x2019;t?</strong></blockquote><p>These thoughts appeared so often that I began to recognize patterns in my creative process.</p><p>It all starts with an infantile thought like, &#x201C;Why am I still using Microsoft Word in 2019?&#x201D;</p><p>Then the thought may go dormant for a while... Or it may not come back.</p><p>I try to scribble them down before they disappear into the void. This is useful for breathing life back into deceased thoughts.</p><p>Each time a thought resurfaces, it matures a little more, &#x201C;What happened to the all-so-popular Evernote that was supposed to change the way we write forever?&#x201D;</p><p>At a certain point, I&#x2019;ll have developed enough interesting questions or observations to write something more concrete like a blog post or business plan. Just like I&#x2019;m doing now.</p><p>If I can&#x2019;t answer a question myself or the thought extends beyond my personal life, I take a look at what other people are saying about the topic. When I was researching writing apps, I came across these two interesting pages:</p><ul><li><a href="https://web.archive.org/web/20211019125527/https://news.ycombinator.com/item?id=8270759">Hacker News: Designing a Personal Knowledgebase</a></li><li><a href="https://web.archive.org/web/20211019125527/https://luhmann.surge.sh/communicating-with-slip-boxes">Communicating with Slip Boxes</a></li></ul><p>I&#x2019;ll make my own notes about what people say. Eventually, I&#x2019;ll have outlined all the information I need. The last step is to weave it together into a compelling piece, and of course, draw some pictures.</p><hr><p>Back in high school, I was hungry for business ideas. I didn&apos;t understand how people could just come up with them out of thin air.</p><p>Now I understand. When your mind isn&apos;t distracted by your phone or the bustle of everyday life, it has extra capacity.</p><p>Your mind has space to observe. It can connect the dots between past memories. It&apos;s as easy as that.</p><p>So do yourself a favour. <strong><strong>Do nothing.</strong></strong></p>]]></content:encoded></item><item><title><![CDATA[How Opportunity Is Created Explained With Food]]></title><description><![CDATA[<p><em>Bread goes with butter.</em><br><em>Ketchup goes with hot dogs.</em><br><em><a href="https://web.archive.org/web/20211019122542/http://www.oprah.com/food/chocolate-avocado-pudding-recipe">Avocado goes with chocolate.</a></em></p><p>There are infinite food pairings around the world. It&#x2019;s all a twisted mess of relationships like a ball of cotton candy.</p><p>When you think about it&#x2026; You can draw relationships between just about anything.</p>]]></description><link>https://rjdlee.com/how-opportunity-is-created-explained-with-food/</link><guid isPermaLink="false">63d7438311a23e6694bf789d</guid><category><![CDATA[Shower Thought]]></category><category><![CDATA[Writing]]></category><dc:creator><![CDATA[Richard Lee]]></dc:creator><pubDate>Mon, 30 Jan 2023 04:12:25 GMT</pubDate><content:encoded><![CDATA[<p><em>Bread goes with butter.</em><br><em>Ketchup goes with hot dogs.</em><br><em><a href="https://web.archive.org/web/20211019122542/http://www.oprah.com/food/chocolate-avocado-pudding-recipe">Avocado goes with chocolate.</a></em></p><p>There are infinite food pairings around the world. It&#x2019;s all a twisted mess of relationships like a ball of cotton candy.</p><p>When you think about it&#x2026; You can draw relationships between just about anything.</p><p><em>Addition is the opposite of subtraction.</em><br><em>Programming is like writing, but for nerds.</em><br><em>Having your heart broken is like&#x2026; Well, there&#x2019;s nothing quite like this one.</em></p><p>Everything on this planet is related in some way. Everything you know, think, and feel is encompassed in your personal knowledge graph, a small piece of the world&#x2019;s graph.</p><p>Understanding the information graph is extremely helpful for learning new things and discovering opportunities. Before diving in, we need to do our &quot;mise en place&#x201D; (prepare our ingredients before cooking). Let&#x2019;s review what a graph is.</p><p>Not the pie or bar kind, but the software kind. Let&#x2019;s go back to our food pairings. Bread, butter, hot dogs, and ketchup are called <strong><strong>nodes</strong></strong>. The pairings between them are called <strong><strong>edges</strong></strong>. Nodes are bits and pieces of information while edges are the relationships that connect the information together.</p><p><em>When Christopher Columbus discovered North America, he connected &#xA0;Europe to the Americas. He brought treasures like chocolate and corn back to Europe.</em></p><p><em>Elon Musk used his position on the forefront of battery technology to bring the electric car to the masses.</em></p><p><em>Did you know everyone on Earth is connected to everyone else by at most six other people?</em></p><p>Breakthrough scientific discoveries uncover new nodes and expand the world&#x2019;s graph. More nodes create more opportunities for edges.</p><p>When someone&#x2019;s personal knowledge graph has an edge or node no one else has, they might have an advantage. This is known as information asymmetry.</p><p>If every node with edges to every other node, the world would be a whole lot more efficient and really, really boring. There would be no point in visiting new countries or meeting new people because you already know everything about them.</p><p>Lucky for us, not all knowledge is readily available. Despite everything being connected in a single graph, it&#x2019;s locked away by space and time. Even though you&#x2019;ve likely had chocolate and avocados before, you&#x2019;ve probably never had them on the same plate or in succession so that they mix in your mouth.</p><h3 id="information-is-organized-by-space-and-time"><strong>Information is organized by space and time.</strong></h3><p>The closer pieces of information (nodes) are in physical or mental space, the easier they are to recall. When you remember one node, the other nodes nearby are also easy to remember.</p><p>When you think of baguettes, you may also think about croissants or the Eiffel Tower because they&#x2019;re all closely related to Paris or France.</p><p>Pieces of information (nodes) that occur successively or repeatedly will also be easy to remember.</p><p>When I memorize new recipes or techniques, &#xA0;I often practice them once, then the next day, then the next month, then a few months later. This is known as repetition learning. The less a piece of information is used, the further into obscurity it will go. Edges disappear.</p><hr><p>It&#x2019;s become easier than ever to find related information in space and time. Google is a way to find information across the world. It connects your personal knowledge graph to the world&#x2019;s knowledge graph. In the future, Elon Musk&#x2019;s Neuralink will connect your brain directly to the world&#x2019;s knowledge graph. For now, personal connections are the best way to uncover valuable information few people have.</p><p>Google and Neuralink help you find existing nodes and edges, which provide you with more opportunities to create edges. The important part is you need to create your own edges. <strong><strong>To get ahead, you need to connect nodes of information together yourself.</strong></strong></p><p><a href="https://web.archive.org/web/20211019122542/https://rjdlee.com/where-do-ideas-come-from/">Being bored and doing nothing is the best way to do so...</a></p>]]></content:encoded></item></channel></rss>