Project Week!

It’s officially Project Week! I took next week off from work to catch up on the piles of projects collecting on my workbench.

This week is off to a great start. The printer has been going non-stop, and one project is already completed. Portfolio post incoming.

Pixel Lamp

A few more prints need to finish before the portfolio post goes up. This one is cool, but there’s so much more to come…

Discussion here: https://x.com/cmcwain/status/1829964889261023337

Societal Consistency

Societal Consistency

Lately, I’ve been thinking a lot about how our society has been splitting in half. Let’s take this image as an example of the extreme thinking that a significant portion of the American people have.

The argument demonstrated in the few sentences above is based on the idea that fascism is coming and will end our lives as we know it. They sincerely believe that the world is out to get them, most of the country is evil, and the only solutions are to eliminate their aggressors or end themselves.

Second Amendment aside, no one is encouraging others who are in an unstable mental state to carry or use a weapon of any type. Given how many shootings we’ve seen lately where the motivation was racial, religious, political, transgender, or ideologically based, it’s an incredibly poor decision to allow their enraged mentality opportunities to hurt others.

This all begs the question, though: How does a large group of anything demonstrate the exact same behavior? Control. They’re being manipulated by the same sources, while their peer group (similarly influenced) provides a feedback loop that amplifies the original signal.

I see this behavior fairly consistently throughout. Unsuspecting people going about their lives all hear the same message on the news. They allow it to occupy space in their brain. It festers until they can release the stress that comes with it (ideally in company). They either unload that burden onto someone who hasn’t yet heard this snippet of information or the receiving end agrees and confirms their belief that this revelation is potentially apocalyptic.

The problem is that when someone agrees, they don’t simply nod and go along. Conversations typically involve adding their two cents or fixing gaps in their understanding. This agreement serves as nothing more than amplification, and we all know that misery loves company. Now both parties share in that same burden, a bond with trust is formed, and they’re ready to actively oppose anyone who feels otherwise or questions their beliefs.

Cults are formed when this methodology and easily manipulated people mix. We see the result, but the cause is the real issue.

Now, the ones who want control find the source, pump money and/or blackmail into the mix, and suddenly the powerful become significantly more powerful. Meanwhile, citizens are trying to make sense of the hysteria, fighting amongst themselves over these new “facts” they’re dealing with. Never once do they consider if any of it is true. That doesn’t matter, though. They have peers who agree and support their beliefs. The larger that group becomes, the closer to reality all of it seems, as they’re all repeating the same lines.

Weird

/1620

Discussion here: https://x.com/cmcwain/status/1829964889261023337

Upcoming Projects

There are so many things I want to do, but there’s never enough time to get them all done! I’m seriously considering taking a week off to tackle as many projects as possible — they’re really starting to pile up.

Here’s my list for the not-too-distant future:

  • Build an open-source light gun for arcade fun at home
  • Make a soldering fume extractor from spare parts
  • Design an 8x8 LED matrix to display pictures with MicroPython
  • Use WLED (or a 2040) to build a digital lava lamp
  • Design a laptop holder that attaches to my computer desk
  • Make a shelf out of a used skateboard
  • Create an N64 “neon” sign
  • T-shirt printing with a 3D printer (not the way you’re thinking)
  • Automated stock trading using algorithms
  • How-to’s:
    • Scrape websites and use the data for productivity
    • Host a site and fix your email with a personal domain
    • Bridge Minecraft chat with Discord
  • Collections:
    • Interesting Python libraries and how to build them
  • Personal goals:
    • Solve a Rubik’s Cube
    • Beat Mike Tyson in Punch-Out!!
  • Reviews:
    • Fallout: London

At this rate, I’m completing one project every two weeks on average. But since my brain adds a few potential projects every day, there’s a lot of filtering happening. I’d also like to start doing short, supplementary videos — but only for things that would make the most sense to show off that way.

Turtle Case

Designed and printed a new case. Yellow makes the phone so much easier to find, and the Turtles make anything totally bodacious!

The turtle sprites are a little washed out because i didn’t merge them as thick layers. The yellow backing of the phone is showing through, causing the turtles to have a lighter feel to them. I’m not going to redo it. It looks good, so we a’ight.

I’m never using a black phone case again though. Bright colored electronics are far superior to hidden ninja devices that need expensive bluetooth tags and apps to find.

Turtle Case

Salad Prep

It’s amazing how very little effort in meal prepping can positively impact your entire week. I love getting simple things done ahead of time, and to-go salads are at the top of my list. It honestly feels weird not to have a salad ready to go when things get busy.

These containers from Amazon are great for everything except freezing. If you want to freeze, these containers are a little more sturdy, but they’re more expensive and don’t store nearly as compact.

The typical salad ingredients are mixed greens, spinach, any veggies or meat you have on-hand, and a little bit of salad dressing. You toss the dressing in the container and shake until mixed. This batch has thinly sliced red onions, red peppers, sliced carrots, dried cranberries, leftover grilled chicken thighs, fried jalapenos (Trader Joe’s), and a southwestern salad dressing. Gang gang ice cream so good! Make sure you put a paper towel in with your prepped veggies to keep them fresh, longer. These will last for at least a week.

My day starts with a cup of cold brew coffee, a smoothie, daily vitamins, a prep’d salad, then whatever for dinner. If there was a way to prep smoothies, I’d be set until a few days after the impending nuclear apocalypse. Maybe then we’ll feast on some 18-legged chickens together! BBQ at my place when you see the mushroom clouds!

Salad Prep

Bing

Today, Satya Nadella was sitting behind his giant mahogany desk when he noticed an anomoly. “Wait a minute. This guy is cheating! Let’s get him! Our stockholders depend on it!” Shortly after, my accounts got Bing Bonged straight onto the time out bench. Let’s see if we could do something about this…

Bing

Well, crap. I typically use a VPN for reasons, and assumed that was why I was flagged. Turns out they figured out I was bot-ersizing searches. Naughty me. I’ve learned my lesson and will promise to do better.

With that obvious fib aside, let’s add in some randomized user agents for desktop and mobile. Users typically have a bit more randomization going on in their lives. Sometimes they’ll do some searches, go to the bathroom, use a different computer, eat lunch, use a different browser, search for porn, and quickly hide the browser from family members that don’t understand what privacy means.

def get_random_user_agent(isMobile=False):
    desktop_user_agents = [
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0",
        "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15",
    ]

    mobile_user_agents = [
        "Mozilla/5.0 (Linux; Android 10; SM-G975F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Mobile Safari/537.36",
        "Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Mobile/15E148 Safari/605.1.15",
        "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; Nexus 5 Build/KOT49H) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
    ]

    if isMobile:
        return random.choice(mobile_user_agents)
    else:
        return random.choice(desktop_user_agents)

While we’re at it, why not set some helpful browser arguments and some random resolutions. Using a browser without a resolution would be a giant red flag for the server on the other end. Let’s not make their machines worry that we’re trying anything nefarious.

context_args = {
    'viewport': {'width': random.choice([1366, 1440, 1536, 1920]), 'height': random.choice([768, 900, 1024, 1080])},
    'user_agent': get_random_user_agent(isMobile),
    'java_script_enabled': True,
    'bypass_csp': True
}
try:
    browser = playwright.firefox.launch(headless=HEADLESS)
    context = browser.new_context(**context_args)

    context.add_init_script('''() => {
        Object.defineProperty(navigator, 'webdriver', { get: () => false });
        window.navigator.chrome = { runtime: {} };
        Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] });
        Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5] });
    }''')

    page = context.new_page()
    page.wait_for_timeout(random.uniform(3000, 7000))

Let’s add in a realistic delay function. Humans don’t usually act so robotic. Typing is hard. Navigating with the mouse takes time and dexterity. Let’s add some blood and guts into our searching curiosity. Combined with our random Wikipedia article search, we’re now more human than human.

def human_typing(page, selector, text, typo_probability=0.008):
    def make_typo(char):
        typos = {
            'insert': lambda c: c + random.choice('abcdefghijklmnopqrstuvwxyz'),
            'delete': lambda c: '',
            'replace': lambda c: random.choice('abcdefghijklmnopqrstuvwxyz')
        }
        typo_type = random.choice(['insert', 'delete', 'replace'])
        return typos[typo_type](char)

    page.click(selector)  # Ensure the search box is focused

    for char in text:
        if random.random() < typo_probability:
            char = make_typo(char)

        page.type(selector, char)
        sleep(random.uniform(0.1, 0.5))

There we go! We should be all set now. Let’s see if there are any errors…

Bing

Awesome! Thank you Mr. Bot. You’re the best! Now to set it and forget it. I’ll standby for my “job’s done” Slack notification to make sure everything goes well the next few days.

Naturally, I don’t give two craps about the rewards. This is purely entertainment value with trying to avoid getting flagged. Maybe we should think about not completing 100% of the rewards all the time. That may put me on an internal list, huh?

On an unrelated note, I’ve added my Twitter account on the left if you wanna chat. I’d rather not add a comment section with embedded, privacy ignoring Javascript. I care about y’alls well-being. ❤️

Kidney

Had a blood test this week, and the numbers are looking really good.

Normal kidney function is measured using two main values: eGFR (Estimated Glomerular Filtration Rate) and creatinine level. A good creatinine level is anywhere between 0.6 and 1.35 mg/dL (for females, up to 1.1 mg/dL), while eGFR should be above 60 mL/min/1.73 m².

For the past year, my creatinine level was hovering around 1.4 mg/dL, which put me in CKD (Chronic Kidney Disease) Stage 3A. This week’s tests, however, came back with a measurement of 1.2 mg/dL. After doing the math (more on that below), I’m now in CKD Stage 2. This puts me closer to having normally functioning kidneys.

Time to celebrate with a triple cheeseburger and a pint of whiskey!

Stages of CKD

  • Stage 1: eGFR ≥ 90
  • Stage 2: eGFR 60-89
  • Stage 3A: eGFR 45-59
  • Stage 3B: eGFR 30-44
  • Stage 4: eGFR 15-29
  • Stage 5: eGFR < 15 (or on dialysis)

My doctor won’t give me a specific eGFR number if it’s over 60, but here is the formula (and in Python) so we can calculate it:

Males:   eGFR = 141 × min(κScr, 1)α × max(κScr, 1)^-1.209 × 0.993^Age × 1.159 (if Black)
Females: eGFR = 144 × min(κScr, 1)α × max(κScr, 1)^-1.209 × 0.993^Age × 1.018 (if Black)

egfr.py

serum_creatinine = 1.2  # mg/dL
age = 44                # years
gender = "male"         # male

kappa = 0.9             # 0.7 for female
alpha = -0.411          # -0.329 for females

eGFR = 141 * min(serum_creatinine / kappa, 1)**alpha * max(serum_creatinine / kappa, 1)**(-1.209) * 0.993**age

print(eGFR)

What is eGFR / Creatinine?

eGFR is a measure of how well your kidneys are filtering waste from your blood. It’s calculated based on factors like your serum creatinine levels, age, sex, and race. The eGFR helps in assessing the stage of kidney disease and is expressed in milliliters per minute per 1.73 square meters of body surface area (mL/min/1.73 m²).

Creatinine is a waste product produced by muscle metabolism and is usually filtered out of the blood by the kidneys. The serum creatinine test measures the amount of creatinine in your blood and is used to evaluate kidney function. Higher levels of creatinine in the blood can indicate impaired kidney function.

Finger

It’s difficult to type after just splitting my thumb on a kitchen knife. The blade went right alongside the edge of the nail. Damn thing was so sharp, it didn’t even hurt at first.

Every 3-6 months, I’ll give my knives a good sharpening. It just so happens, that if you look away for a minute, there’s a pretty good chance that you’ll touch the painful part of the knife. I wasn’t even halfway through (~300grit) when the nakiri nabbed me.

Bummer man.

Finger

Daylight

The Tron lights in my office wrap around the baseboards and give a cool hue throughout the day. Through the built-in WLED software, they were set to change to a random color every two hours. This was the peak functionality allowed through their built-in settings page. However, the brilliant creators of the script also included an API that allows users to control the settings remotely. Knowing this, I spent some time writing Python code to change the lights dynamically. You can see it below.

The idea was to have them mimic the colors of a day using a daylight color scale. I’ve translated those colors into RGB values and set the brightness to allow for a calmer transition into the evening. When guests are over and sleeping, it’s not necessary to keep the room bright with glowing LEDs. This way, they’ll be able to see where they’re walking in the dark and won’t need to wear eye shades in bed.

After more testing and tweaking, I’ll add holiday transitions. For example, red, green, and white for Christmas, and maybe purple and green for Halloween. WLED also provides animation templates, so perhaps animated candy cane lights would look cool.

This was a little project enhancement, but I figured it was cool enough to share.

lights.py

    import requests
    import time
    from datetime import datetime
    import logging

    logging.basicConfig(filename='/home/pi/myproject/my_script.log', level=logging.DEBUG, 
                        format='%(asctime)s - %(levelname)s - %(message)s')

    addresses = [
        'http://{wled_ip_address}/json/state'   # add additional WLED installs here
    ]

    # Define hourly colors
    colors = {
        1: {'on': True, 'bri': 10, 'seg': [{'col': [[1, 30, 70]]}]},
        2: {'on': True, 'bri': 15, 'seg': [{'col': [[0, 48, 96]]}]},
        3: {'on': True, 'bri': 20, 'seg': [{'col': [[0, 46, 93]]}]},
        4: {'on': True, 'bri': 25, 'seg': [{'col': [[0, 52, 89]]}]},
        5: {'on': True, 'bri': 25, 'seg': [{'col': [[0, 52, 89]]}]},
        6: {'on': True, 'bri': 35, 'seg': [{'col': [[0, 81, 118]]}]},
        7: {'on': True, 'bri': 45, 'seg': [{'col': [[7, 92, 133]]}]},
        8: {'on': True, 'bri': 80, 'seg': [{'col': [[13, 138, 168]]}]},
        9: {'on': True, 'bri': 110, 'seg': [{'col': [[89, 191, 194]]}]},
        10: {'on': True, 'bri': 128, 'seg': [{'col': [[197, 228, 193]]}]},
        11: {'on': True, 'bri': 128, 'seg': [{'col': [[227, 224, 122]]}]},
        12: {'on': True, 'bri': 128, 'seg': [{'col': [[246, 207, 100]]}]},
        13: {'on': True, 'bri': 128, 'seg': [{'col': [[255, 188, 107]]}]},
        14: {'on': True, 'bri': 128, 'seg': [{'col': [[252, 181, 93]]}]},
        15: {'on': True, 'bri': 128, 'seg': [{'col': [[253, 175, 92]]}]},
        16: {'on': True, 'bri': 100, 'seg': [{'col': [[244, 150, 78]]}]},
        17: {'on': True, 'bri': 90, 'seg': [{'col': [[241, 122, 113]]}]},
        18: {'on': True, 'bri': 80, 'seg': [{'col': [[213, 102, 135]]}]},
        19: {'on': True, 'bri': 75, 'seg': [{'col': [[124, 59, 133]]}]},
        20: {'on': True, 'bri': 60, 'seg': [{'col': [[71, 35, 124]]}]},
        21: {'on': True, 'bri': 45, 'seg': [{'col': [[46, 24, 111]]}]},
        22: {'on': True, 'bri': 45, 'seg': [{'col': [[33, 37, 101]]}]},
        23: {'on': True, 'bri': 40, 'seg': [{'col': [[11, 21, 74]]}]},
        0: {'on': True, 'bri': 10, 'seg': [{'col': [[2, 18, 69]]}]}
    }

    # Set color
    def set_color(address, color):
        try:
            response = requests.post(address, json=color)
            if response.status_code == 200:
                logging.info(f'Successfully set {address} to {color}')
            else:
                logging.error(f'Failed to set {address} to {color}, status code: {response.status_code}')
        except requests.exceptions.RequestException as e:
            logging.error(f'Error setting {address} to {color}: {e}')

    # Main loop
    while True:
        current_hour = datetime.now().hour
        color = colors.get(current_hour)

        if color:
            for address in addresses:
                set_color(address, color)
            logging.info(f'All addresses set to color for hour {current_hour}. Waiting for ten minutes...')
        else:
            logging.error(f'No color setting found for hour {current_hour}')

        time.sleep(600)  # Wait for ten minutes

Quick Morning Project

The overhead bedroom fan stopped working a while back, and my wife was looking to replace the entire system. It turns out that the controller’s batteries were so old that they leaked and corroded some of the internals. These things are easily 20+ years old, so it’s not surprising that they’ve shit the bed. Rather than spend the money, time, and effort replacing the entire fan, it was obviously easier to take the controller apart and see if something could be done.

Fan Controller 1

After soaking the area in alcohol and cleaning the area with a brush, I pulled everything out that was salvagable. The area I’m calling the “negative pole spring assembly” was the most challenging part. There aren’t any replacements in my bin to speak of, so I grabbed some 24 AWG hook-up wire from my bench and wrapped it around a 1/4" guitar jack. It was the only thing nearby that fit the shape needed. Soldering it into place was real quick and easy given lots of tinning and solder.

Fan Controller 2

Now it works like a champ! No problemo.