Going Electric

I am done with gasoline and I'm never going back. Early last year, I sold my gasoline-powered car and went all in on an electric vehicle. I skipped right over hybrids, which I viewed as being burdened with the additional complexity of having to haul around two sources of locomotion. I figured that if I was going to try something new, I should just make the switch and not try to hedge against the new thing at the cost of added complexity and maintenance. Electric vehicles are well enough established (particularly in California) that there was little reason to avoid making the jump, and I'm so glad I made the switch.

After checking out everything from the large F-150 Lightning down to a petite little Chevy Bolt, I settled on a used 2022 Volkswagen ID.4 which had just come off lease. With only about 20,000 miles on the odometer, it was still very much like new. But the fact that it was previously a lease meant it was likely handled carefully, and the fact that it was now used meant it was extremely affordable for an EV at a time when EVs are often more expensive than gas vehicles. I ended up paying about $25,000 for it, and that felt like a steal for what I got.

This being my first EV, I was initially a little bit wary about going electric, but now that I have a year's worth of daily-driver experience with the technology, it's so crystal clear to me that this is the future of vehicles and that the internal combustion engine's best days are behind it. It feels like a seismic shift both in the way I drive and use energy.

Like most EVs, the ID.4 has some really great features. The very first difference you notice is the instant acceleration. You start moving the moment you press the pedal. The best way to describe it is like the difference between pre-iPhone touchscreens which often struggled to keep up with the movement of your finger, whereas the iPhone made the screen contents feel like they moved directly with your finger. EV acceleration feels tactile and direct, rather than propagated through a buffer delay. Because it's electric, the ID.4 moves so smoothly and quietly that it really feels like a much more expensive luxury car. It also has a tight turning radius and can thus easily achieve a 180 degree turn-around inside the width of a narrow street without having to perform a multi-point turn.

I was coming from a 2009 Toyota Corolla base trim, so many of the features of the ID.4 are just the current state of vehicles made within the last decade and not EV exclusives, but are nonetheless delightful to have, including:

  • Illuminated exterior and (RGB) interior handles
  • Comfortable heated seats
  • Heated steering wheel
  • Remote climate control
  • Back up camera
  • Side mirror lane occupancy indicators
  • Built-in USB-C ports
  • Apple CarPlay

CarPlay, in particular, was a dealbreaker. The Chevy dealer tried to convince me that GM's Bluetooth solution was still very good, but I wasn't having that at all. The ID.4 also has walk-up unlock, walk-away lock, and auto-enters Park, so I never really think about what mode the car is in: I walk up (it unlocks), I get in, buckle, drive somewhere, stop, unbuckle (it enters Park), then I just get out and walk away (it locks). I never have to think about turning it on or off myself, making for a frictionless experience.

The ID.4 has two drive modes, "D" and "B". D mode (the default Drive mode) makes the car drive much more like gas car, with the ability to coast for longer distances and do little regenerative braking. But toggle into B mode and the car behaves more like its true EV self, doing more aggressive regenerative braking when you ease off the accelerator pedal, allowing you to recover some electricity while cornering or going downhill. It took a short few days before B mode was far and away my preferred mode, making the D mode (and thus the behavior of gas cars) feel almost slippery by comparison.

The major consideration when choosing an EV is how and when you'll charge the battery. Chargers are popping up in more and more places, but you very likely spend a lot of time not driving with the car parked somewhere for hours at a time. That's probably the best time and place to charge. A couple years ago, we installed a modest home solar and battery storage system, and adding a home EV charger (Level 2 @ 220V 40A) charger later was a straightforward addition. While I could have achieved sufficient charging without any special charger using a regular household plug (Level 1 @ 110V 15-20A), the Level 2 home charger makes filling up my car only take a few hours and is easily done while I'm sleeping or working from home while the sun is shining on the solar panels. Powering my car is now effectively free using energy from the sun instead of disposable energy from oil. I can plug in at convenient times and almost always have a "full tank" ready to go without having to consider the gas station on the way home -- my home is the station. And so is my work. And the grocery store. Almost every destination has something available or nearby, but having a sufficient charger at home is what really puts "range anxiety" fears to rest.

On the interior, one of the compelling reasons for choosing the ID.4 was its minimalist, unadorned heads-up display. The dashboard user interface is big, bright, and clear, with simple numerals for the speed, flanked by cruise control and next-turn instructions (which can use the built-in navigation or CarPlay). This approach was not true for the Kia Niro, which had a fake analog speedometer needle and gradients and shadows abound. There are a few things that could be better, though:

  • The steering wheel and center console use touch controls, and while they're not easy to press accidentally, they do require you to look away from the road to adjust. In my Corolla, I could adjust the heat mode, fan speed, and temperature by feel alone, and that felt safer. VW is, thankfully, moving away from touch controls in future models.
  • Remote climate control sometimes takes a minute or more to respond. I never had this feature at all before, though, so any availability was still a welcome feature.
  • Wireless CarPlay is convenient, but a little laggy and skips occasionally. This might just be wireless CarPlay's fault and not VW's. Wired CarPlay via USB-C works reliably, is very responsive, and charges my phone faster than the available Qi charger (and with less heat).
  • I wish the VW app had a web app counterpart. How hard could this be, given they already have an app?

Minor gripes aside, none of these are dealbreakers. Overall, the ID.4 is a competitively priced EV that was, for me, a great entry into the world of electric vehicles. My experience so far has been extremely positive, and if it were totaled, I would buy one again in a heartbeat. I'm never buying a gasoline-powered anything again.

Running Nuxt 3 Behind an nginx Reverse Proxy

I was attempting to run Nuxt 3 RC1 in development mode behind a local nginx reverse proxy, but ran into several issues. There are a number of reasons to run a development application server behind a TLS-terminating reverse proxy, including more closely mirroring a production setup, ensuring an application performs correctly when proxied, and gaining HTTPS support to enable use of newer HTTPS-only user-agent APIs.

However, when nginx reverse proxies to the Nuxt server using a configuration like the following, the application will load correctly in the browser but Vite (bundled with Nuxt) will no longer be able connect to its backend websocket server to provide hot module replacement (HMR).

location / {
  proxy_pass http://127.0.0.1:3000;
  proxy_set_header X-Real-IP $remote_addr;
  proxy_set_header Host $host;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

To fix this, Vite needs to be made aware it's being reverse proxied, and nginx needs to pass through Vite's websocket connection.

In the Nuxt config (nuxt.config.ts), add the following Vite config:

vite: {
  server: {
    hmr: {
      protocol: 'wss',
      clientPort: 443,
      path: 'hmr/'
    }
  }
}

Vite will now use the secure websocket protocol over the same HTTPS port as the application, but it will request it at a new, distinct path.

In the nginx config, add a new location directive to match the configured Vite path (/_nuxt is always prepended), have it perform an HTTP Upgrade, and then reverse proxy to Vite's websocket server, which always listens on port 24678:

location /_nuxt/hmr/ {
  proxy_http_version 1.1;
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection "Upgrade";
  proxy_pass http://127.0.0.1:24678;
}

After restarting the development server and nginx, Nuxt 3 and Vite HMR work great behind nginx, which now handles TLS termination and reverse proxying of HTTP and websocket traffic.

Update 2023-09-28

As of Nuxt 3.7.4, the nuxt.config.ts configuration is unnecessary, though having the following nginx config in place avoids a WebSocket error about the WebSocket host being undefined:

location /_nuxt/ {
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_cache_bypass $http_upgrade;
    proxy_pass http://127.0.0.1:3000;
}

Note the differences from the above config -- the location path dropped hmr/, the Vite HMR port (24678) became the default Nuxt port (3000), and the Host header was also added (but the Host header is likely not critical for this scenario).

Update 2026-03-23

Tangentially related: In development, Nuxt Icon collections are loaded from the Nuxt server at /api/_nuxt_icon, but this path might introduce a conflict if you are integrating a completely separate non-Nuxt API at /api. In my case, I often have a Python API expecting requests bearing that path prefix. My local nginx setup forwards requests with /api prefixed paths to Python, resulting in a Nuxt 404 on Nuxt Icon collection manifests such as /api/_nuxt_icon/bi.json?icons=escape or /api/_nuxt_icon/solar.json?icons=check.

Setting a localApiEndpoint overrides the default value of /api/_nuxt_icon both in how the Nuxt server route is offered and how the Nuxt client requests the collection JSON. The issue and fix are both documented on GitHub. In nuxt.config.ts, add something like the following:

icon: {
  localApiEndpoint: '/_nuxt_icon',
},

Note that /api is absent from the value of localApiEndpoint, durecting Nuxt Icon to use a path other than /api/_nuxt_icon. Anything that will not be picked up by an external API is valid here, and will make the Nuxt Icon module work again.

Building cen64 on macOS

For testing Nintendo 64 homebrew ROMs, cen64 is the most accurate emulator (though it doesn't run at full speed yet). Here's how to build it from source on macOS:

  1. Install XQuartz from the official distributed disk image
  2. brew install cmake glew
  3. git clone https://github.com/n64dev/cen64.git
  4. cd cen64
  5. mkdir build
  6. cd build
  7. cmake ..
  8. make

If you'd like to enable cen64's debug logging, create a debug build when running cmake:

cmake -DCMAKE_BUILD_TYPE=Debug ..

When running cen64 outside of an XQuartz X11 terminal, it may report:

Using NTSC-U PIFROM
create_device: Failed to initialize the VI.
Failed to create a device.

To fix this, you can run it within an XQuartz X11 terminal, or set the DISPLAY environment variable to something like :0 either in your .bashrc file or inline during invocation:

DISPLAY=:0 ./cen64 /path/to/pifdata.bin /path/to/rom.z64

DISPLAY needs to be set because cen64 calls XOpenDisplay with a NULL display name (presumably to default to your DISPLAY environment variable), but if it's not set, XOpenDisplay returns NULL and cen64 has no display within which to create a window for rendering Nintendo 64 content.

For extremely verbose register-level output, edit CMakeLists.txt and set DEBUG_MMIO_REGISTER_ACCESS to ON. Make sure to remove any cached data in build/ to ensure your changes are reflected, then recompile and re-run.

Update 2024-03-02

Development on cen64 has not progressed in many months and is now considered unmaintained. ares, a cross-platform, open source, multi-system emulator is now regarded as the best emulator for Nintendo 64 development.

BrewBot - Sending Coffee Notifications to Slack

At work, we have a coffee machine that serves dozens of people in the building, and it's difficult to know when to come get fresh coffee. You might arrive when it's empty and be tasked with making more, but the ideal situation is to arrive just as a fresh pot is being brewed.

We also use Slack for team chat and various notifications, so integrating the coffee machine status was a no-brainer. Using a non-invasive/inductive current sensor and Raspberry Pi, the following setup monitors coffee machine energy consumption and waits for a significant rise in current draw, followed by several minutes of sustained usage. Once a time threshold has passed, it does an HTTP POST to a Slack webhook, sleeps for about 15 minutes, then starts monitoring again. This "brewbot" code is available on GitHub, and a parts list can be found below.

Completed kit

Packaged in box

ADC board

Full parts list:

Related reading: Hyper Text Coffee Pot Control Protocol

GottaGo

Working at Blackboard Mobile making unique mobile apps is fun, but occasionally it's interesting to do something completely different at work, just to see what you can come up with. To that end, we recently hosted our first Hackathon, where small teams of co-workers had 24 straight hours to create a project of any theme using resources available inside our outside of the office, and the results would be judged by our peers. One of the other benefits of working in a growing industry is that we're expanding our staff almost weekly. Unfortunately, though, that means that the building we're in is less and less able to handle the increasing capacity. Specifically, the bathrooms are occupied more frequently, resulting in either a return trip to your desk only to try again later, or an awkward wait of unknown duration.

header

Working with Joe Taylor and Eric Littlejohn, our Hackathon project set out to make the office bathroom availability more visible and accessible through a combination of hardware and software. The piece of the project that lets this all work is a microswitch installed inside each door jamb, such that when the locking bolt is engaged, the switch is tripped. That way, we can do anything with resulting data, knowing that when the door is locked, the room is in use. Running wire down through the hollow metal trim was tedious and time consuming, and involved a lot of false starts and fishing around with a straightened coat hanger, but we finally got a run of wire inside each frame.

door switch

On each office floor there are two bathrooms side by side, and the pair of switches inside the door jambs are wired to a single Arduino fitted with an Ethernet Shield for network connectivity. The Arduino samples the switches many times per second, providing near-instant feedback.

arduinno board

After debouncing the switch input signal over about 50 milliseconds, the Arduino waits for a definitive change in state -- from locked to unlocked, or unlocked to locked -- before illuminating LED lights on the wall.

wall lights

After the light corresponding to the now-occupied bathroom is lit, the Arduino also performs an HTTP POST, sending the event details (floor level, which room, and current occupancy state) to an in-house webserver running Node.js and MongoDB. The webserver records the data and makes it visible on a web page for viewers to check the availability digitally, for those who can't see the wall mounted lights from their seating position.

event sequence animation

If you'd like to employ a project like this, the code we hacked together is available on GitHub, and the wiring is rather straightforward:

  • All components share a common ground
  • LED anodes are wired to room_a_led_pin and room_b_led_pin and brought high when doors are locked, and low when unlocked
  • Switches bring room_a_switch_pin and room_b_switch_pin low when triggered, and the Arduino uses the INPUT_PULLUP pinMode for the unlocked state

Our Hackathon project came in at second place, losing to an as-yet-unannounced software project, but we had a lot of fun staying up and hacking all night!