Christmas 2023: DIY electronics kit for children

Wow, two weeks have passed since Christmas. Wow, we’re one week into 2024. Happy New Year!

I have an 8-year-old niece and I thought I’d give her an electronics kit for Christmas. Well, it seems that in modern electronics kits, electronic components are covered in a bunch of plastic, making everything look kind of childish. They are a little safer I suppose because they are too big to be swallowed. But at least I (as a child) didn’t really like stuff with a “Fisher-Price” look, I tended to want the real deal. So I decided to make my own electronics kit with my own manual! I just put in a few components I mostly had on hand (I also washed them before, just in case I’d used them somewhere dirty before):

  • 5 red LEDs with a low forward voltage (around 1.7V), this way you can show that they light up (barely) with very fresh alkaline batteries. (Note: most red LEDs have a higher forward voltage of 1.85V or so, and wouldn’t light up at all)
  • 5 blue LEDs
  • 5 white LEDs
  • 5 100 uF (or so) electrolytic capacitors
  • 5 470 uF (or so) electrolytic capacitors
  • 5 470 ohm (or so) resistors
  • 5 2n2222 transistors
  • 2 CR2032 batteries
  • 2 CR2032 battery holders (the ones I bought can just barely fit into a breadboard)
  • Optional: 4x AA/AAA battery holder and AA/AAA batteries (referenced in text, so make sure to remove reference if you don’t want to include them)
  • Some wires
  • 1 breadboard

I didn’t include a multimeter, but if the child’s/person’s household doesn’t have one, it might make sense to include one. (The manual doesn’t really fully explain how to use multimeters, however.)

In the manual, the first experiment makes use of the fact that blue and white LEDs have a forward voltage that is quite compatible with CR2032 coin cells. The experiment just sandwiches a coin cell between an LED’s legs.

Then the manual explains that this doesn’t work with red LEDs and a resistor is used to limit the current. (Actually it will probably be okay for a while because coin cells don’t give a lot of current.) Some effort is made to explain voltage, current, and resistance, but (hopefully) on a level that is (possibly, barely) understandable by an 8-year-old.

A little later, capacitors are explained a bit. And transistors. The final experiment is a circuit that has some twinkling LEDs. The following is a similar circuit, just to give you an idea:

Old video shot on a potato.

And here’s the completed set, just before wrapping it:

DIY electronics kit, complete with “Instuction Manual” (d’oh!)

The manual is in .odt format (can be opened in LibreOffice and similar) and you can download it below. It includes some copyrighted pictures from other sites (the battery pic, the diode symbol(s), the transistor pic, the capacitor pic, and the empty breadboard pics). The front page image is AI-generated but heavily edited. Note: LibreOffice may have a bug that prevents emoji from being included in exported PDF files. My printed booklet ended up not containing emoji and I noticed only much later. :(

I made a booklet using the following LibreOffice print settings and a stapler:

Make sure to select “A4” and “Landscape” before selecting “Brochure”, otherwise LibreOffice might get the layout wrong.

If you want to customize the manual for somebody else, you should read through the entire thing. Edit it to your heart’s content. There is at least one reference to “8-year-olds”, maybe do a search. There are references to “dad or uncle” and “parents”. The “license” is “public domain”. Feel free to give credit, but you don’t have to.

Converting paths to circles in Inkscape

Or alternatively: how to get svg2shenzhen to recognize your drill paths as drill holes

(My) rationale: there is an Inkscape extension called svg2shenzhen. This extension creates Gerber and KiCad files that can be used to create printed circuit boards, from standard SVG files. Also, this extension has a funny name. Older, DIY printed circuit boards are just a high-DPI bitmap. Using Inkscape and this extension, you can trace the bitmap and then convert it to Gerber. However, most PCBs (especially old PCBs) need to have holes drilled. The drill locates are just circles in the bitmap, and after tracing, they’re just paths. The svg2shenzhen extension (at the time of this writing) detects circles in a certain layer as locations that need to be drilled, but not paths.

I’m not an Inkscape expert, but AFAIK Inkscape (at the time of this writing) doesn’t have a built-in tool to convert (mostly) circular paths to circles. So I wrote a simple extension that does this! It works fine on Linux. Not so sure about Windows.

Extensions are made of only two files, a file that describes the extension, and the extension code (which is in Python in many cases). These two files just have to be placed into the location shown in Edit -> Preferences -> System -> User extensions, which in my case is ~/.config/inkscape/extensions/.

Here are the two files, you can copy them into a text editor like Kate or gedit or Notepad, what have you, and save them into the above directory. I recommend keeping my file names, path2circle.inx and Note, some skeleton code in was generated by ChatGPT, though it was quite wrong. That’s where some of the verbose comments and extraneous code came from.


<?xml version="1.0" encoding="UTF-8"?>
            <submenu name="Custom"/>
        <command location="inx" interpreter="python"></command>

import inkex
from inkex import Circle

class Path2Circle(inkex.EffectExtension):
    def effect(self):
        # Iterate through all the selected objects in the SVG
        for node in self.svg.selection:
            # Check if the object is a path (or any other object type)
            if node.tag.endswith("path"):
                # Get the bounding box of the object
                x, y, width, height = self.get_object_dimensions(node)
                x = x.minimum
                y = y.minimum
                # with open('/path/to/debug/directory/debug_output.txt', 'a') as f:
                    # print(f"Object Dimensions: x={x}, y={y}, width={width}, height={height}", file=f)
                layer = self.svg.get_current_layer()
                diameter = min(width, height)
                layer.add(self.add_circle(x, y, diameter/2))

    def add_circle(self, x, y, radius):
        """Add a circle at the given location"""
        elem = Circle() = (x+radius, y+radius)
        elem.radius = radius
        return elem

    def get_object_dimensions(self, object_node):
        # Get the bounding box of the object
        bbox = object_node.bounding_box()

        # Extract the bounding box coordinates
        x = bbox.x
        y = bbox.y
        width = bbox.width
        height = bbox.height

        return x, y, width, height

if __name__ == '__main__':

To use the extension, you probably first need to restart Inkscape. (You do not need to restart Inkscape after changing the extension, however.) Select all the paths you’d like to convert, and then hit Extensions -> Custom -> Path2Circle. Note: the extension doesn’t actually care if the paths even remotely look like circles, so make sure to select the correct paths. You can easily modify the extension to calculate the radius differently, or e.g. replace paths with other objects, such as squares, rectangle, or ellipses. Let me know if you need help doing that.

Playing on the Nintendo Switch with a generic USB-HID controller (specifically, a fake PS3 controller)

In a previous post, we did some warmups — playing MSX games using a fake PS3 controller. In this post, we’ll be using GP2040-CE to control a Nintendo Switch. (No analog stick support implemented at the moment, but it wouldn’t be hard.) At the time of writing, most of the code to do this is already there! And I’m sure there will be support for USB-HID controllers in no time, so this post will probably be outdated soon.
Update: Analog is implemented too, and the diff below has been updated.

Anyway, you just need to put GP2040-CE on your Pico and get into the web configuration. In add-ons, you enable keyboard support, and then set up the “Keyboard Host Configuration”, which looks like this:

GP2040-CE keyboard mapping and other configuration

Then you can connect a generic USB keyboard to the Raspberry Pi Pico, and connect the Pico to the Nintendo Switch. (For electrical reasons, I do not recommend setting a pin for 5V power here, and just putting the host USB +5 on the VBUS pin of the Pico.)

Things that could come in handy: Breadboard, Raspberry Pi Pico, USB port that can be connected to one of the Pico’s GPIO pins

If everything works and you can control Sonic using your keyboard, great, you can move on to the next step! If it didn’t work, it probably won’t magically get better from here on out, so make sure to check those connections. The green/white wires can be D+/D- or D-/D+!

Now we’ll perform a small modification to the existing code. I’m basing my work on commit 961c49d5b969ee749ae17bd4cbb2f0bad2380e71. Beware, this may or may not work with your controller. I’d recommend taking a look at the above-mentioned previous post where we modify a Pico-PIO-USB example and to check if your controller behaves the same way. I have only two controllers to test with, and I only tested with one! Anyway, here’s the diff:

diff --git a/headers/addons/keyboard_host.h b/headers/addons/keyboard_host.h
index af9c61b..74fb628 100644
--- a/headers/addons/keyboard_host.h
+++ b/headers/addons/keyboard_host.h
@@ -53,6 +53,7 @@ private:
 	bool _keyboard_host_enabled;
 	uint8_t getKeycodeFromModifier(uint8_t modifier);
 	void process_kbd_report(uint8_t dev_addr, hid_keyboard_report_t const *report);
+	void process_usb_gamepad_report(uint8_t dev_addr, const uint8_t *report);
 	GamepadState _keyboard_host_state;
 	KeyboardButtonMapping _keyboard_host_mapDpadUp;
 	KeyboardButtonMapping _keyboard_host_mapDpadDown;
@@ -74,4 +75,4 @@ private:
 	KeyboardButtonMapping _keyboard_host_mapButtonA2;
-#endif  // _KeyboardHost_H_
\ No newline at end of file
+#endif  // _KeyboardHost_H_
diff --git a/src/addons/keyboard_host.cpp b/src/addons/keyboard_host.cpp
index a5294e9..8f59f4a 100644
--- a/src/addons/keyboard_host.cpp
+++ b/src/addons/keyboard_host.cpp
@@ -63,12 +63,15 @@ void KeyboardHostAddon::setup() {
 void KeyboardHostAddon::preprocess() {
   Gamepad *gamepad = Storage::getInstance().GetGamepad();
+  gamepad->setDpadMode(DpadMode::DPAD_MODE_DIGITAL);
+  gamepad->hasLeftAnalogStick = true;
+  gamepad->hasRightAnalogStick = true;
   gamepad->state.dpad     |= _keyboard_host_state.dpad;
   gamepad->state.buttons  |= _keyboard_host_state.buttons;
-  gamepad->state.lx       |= _keyboard_host_state.lx;
-  gamepad->       |=;
-  gamepad->state.rx       |= _keyboard_host_state.rx;
-  gamepad->state.ry       |= _keyboard_host_state.ry;
+  gamepad->state.lx       = _keyboard_host_state.lx;
+  gamepad->       =;
+  gamepad->state.rx       = _keyboard_host_state.rx;
+  gamepad->state.ry       = _keyboard_host_state.ry;
   gamepad->       |=;
   gamepad->state.rt       |= _keyboard_host_state.rt;
@@ -89,10 +92,11 @@ void KeyboardHostAddon::report_received(uint8_t dev_addr, uint8_t instance, uint
   uint8_t const itf_protocol = tuh_hid_interface_protocol(dev_addr, instance);
   // tuh_hid_report_received_cb() will be invoked when report is available
-  if (itf_protocol != HID_ITF_PROTOCOL_KEYBOARD)
-    return;
-  process_kbd_report(dev_addr, (hid_keyboard_report_t const*) report );
+  if (itf_protocol == HID_ITF_PROTOCOL_KEYBOARD) {
+    process_kbd_report(dev_addr, (hid_keyboard_report_t const*) report );
+  } else {
+    process_usb_gamepad_report(dev_addr, report);
+  }
 uint8_t KeyboardHostAddon::getKeycodeFromModifier(uint8_t modifier) {
@@ -161,4 +165,96 @@ void KeyboardHostAddon::process_kbd_report(uint8_t dev_addr, hid_keyboard_report
         _keyboard_host_state.rt = 0;
\ No newline at end of file
+// 0x1: L2
+// 0x2: R2
+// 0x4: L1
+// 0x8: R1
+// 0x10: Triangle
+// 0x20: Circle
+// 0x40: X
+// 0x80: Square?
+// 0x100: ?
+// 0x200: ?
+// 0x400: R3
+// 0x800: Start
+// 0x1000: Up
+// 0x2000: Right
+// 0x4000: Down?
+// 0x8000: Left?
+#define BUTTON_L2 0x1
+#define BUTTON_R2 0x2
+#define BUTTON_L1 0x4
+#define BUTTON_R1 0x8
+#define BUTTON_SQUARE 0x10
+#define BUTTON_CROSS 0x20
+#define BUTTON_CIRCLE 0x40
+#define BUTTON_TRIANGLE 0x80
+#define BUTTON_SELECT 0x100 // ?
+#define BUTTON_L3 0x200 // ?
+#define BUTTON_R3 0x400 // ?
+#define BUTTON_START 0x800
+#define BUTTON_UP 0x1000
+#define BUTTON_RIGHT 0x2000
+#define BUTTON_DOWN 0x4000
+#define BUTTON_LEFT 0x8000
+void KeyboardHostAddon::process_usb_gamepad_report(uint8_t dev_addr, const uint8_t *report)
+  _keyboard_host_state.dpad = 0;
+  _keyboard_host_state.buttons = 0;
+  _keyboard_host_state.lx = GAMEPAD_JOYSTICK_MID;
+  _keyboard_host_state.rx = GAMEPAD_JOYSTICK_MID;
+  _keyboard_host_state.ry = GAMEPAD_JOYSTICK_MID;
+ = 0;
+  _keyboard_host_state.rt = 0;
+  uint16_t button_state = report[2] << 8 | report[3];
+  uint8_t left_analog_x = report[6];
+  uint8_t left_analog_y = report[7];
+  uint8_t right_analog_x = report[8];
+  uint8_t right_analog_y = report[9];
+  const GamepadOptions& gamepadOptions = Storage::getInstance().getGamepadOptions();
+  _keyboard_host_state.dpad |=
+            ((button_state & BUTTON_UP)    ? (gamepadOptions.invertYAxis ? _keyboard_host_mapDpadDown.buttonMask : _keyboard_host_mapDpadUp.buttonMask) : _keyboard_host_state.dpad)
+          | ((button_state & BUTTON_DOWN)  ? (gamepadOptions.invertYAxis ? _keyboard_host_mapDpadUp.buttonMask : _keyboard_host_mapDpadDown.buttonMask) : _keyboard_host_state.dpad)
+          | ((button_state & BUTTON_LEFT)  ? _keyboard_host_mapDpadLeft.buttonMask  : _keyboard_host_state.dpad)
+          | ((button_state & BUTTON_RIGHT) ? _keyboard_host_mapDpadRight.buttonMask : _keyboard_host_state.dpad)
+        ;
+  _keyboard_host_state.buttons |=
+      ((button_state & BUTTON_CROSS)  ? _keyboard_host_mapButtonB1.buttonMask  : _keyboard_host_state.buttons)
+    | ((button_state & BUTTON_CIRCLE)  ? _keyboard_host_mapButtonB2.buttonMask  : _keyboard_host_state.buttons)
+    | ((button_state & BUTTON_SQUARE)  ? _keyboard_host_mapButtonB3.buttonMask  : _keyboard_host_state.buttons)
+    | ((button_state & BUTTON_TRIANGLE)  ? _keyboard_host_mapButtonB4.buttonMask  : _keyboard_host_state.buttons)
+    | ((button_state & BUTTON_L1)  ? _keyboard_host_mapButtonL1.buttonMask  : _keyboard_host_state.buttons)
+    | ((button_state & BUTTON_R1)  ? _keyboard_host_mapButtonR1.buttonMask  : _keyboard_host_state.buttons)
+    | ((button_state & BUTTON_L2)  ? _keyboard_host_mapButtonL2.buttonMask  : _keyboard_host_state.buttons)
+    | ((button_state & BUTTON_R2)  ? _keyboard_host_mapButtonR2.buttonMask  : _keyboard_host_state.buttons)
+    | ((button_state & BUTTON_SELECT)  ? _keyboard_host_mapButtonS1.buttonMask  : _keyboard_host_state.buttons)
+    | ((button_state & BUTTON_START)  ? _keyboard_host_mapButtonS2.buttonMask  : _keyboard_host_state.buttons)
+    | ((button_state & BUTTON_L3)  ? _keyboard_host_mapButtonL3.buttonMask  : _keyboard_host_state.buttons)
+    | ((button_state & BUTTON_R3)  ? _keyboard_host_mapButtonR3.buttonMask  : _keyboard_host_state.buttons)
+  ;
+  /*
+   * #define GAMEPAD_JOYSTICK_MIN 0
+   * lx, ly, rx, ry are 16-bit values, but our joystick produces 8-bit values
+   * our joystick's middle is at 0x80, so it would probably be slightly better to adjust that.
+   */
+  _keyboard_host_state.lx = (left_analog_x == 0x80) ? GAMEPAD_JOYSTICK_MID : (left_analog_x << 8 | left_analog_x);
+ = (left_analog_y == 0x80) ? GAMEPAD_JOYSTICK_MID : (left_analog_y << 8 | left_analog_y);
+  _keyboard_host_state.rx = (right_analog_x == 0x80) ? GAMEPAD_JOYSTICK_MID : (right_analog_x << 8 | right_analog_x);
+  _keyboard_host_state.ry = (right_analog_y == 0x80) ? GAMEPAD_JOYSTICK_MID : (right_analog_y << 8 | right_analog_y);
+ = 0;
+  _keyboard_host_state.rt = 0;

Good luck. Miraculously, everything worked perfectly for me. The keyboard worked immediately, the above code modification worked immediately without having to do any debugging, and I’ve gotta say, my fake PS3 controller feels quite okay! (Note that you will have to press the PlayStation button after connecting your PS3 controller.)

The red part just hides some clutter.

PS3 controller repair log

Symptoms on real PS3: probably “crazy behavior”, I didn’t actually test on a real PS3.
Symptoms when connected to a computer with a program open that displays the gamepad status: random button presses, button “flickering”, buttons going on and off randomly, possibly depending on how the controller is held.

Cause in my case: rubber cushion is worn out, and/or physical damage to the controller’s case and/or loss of one of the screws. The rubber cushion sits on a piece of plastic, and a flex cable is sandwiched between the rubber cushion and the PCB. If the rubber cushion loses some of its original height, for example due to wear, or if one of the controller’s screws are lost and the PCB isn’t pressed as hard against the rubber cushion as it used to, buttons will randomly appear pressed or unpressed. (When there is absolutely no connection between the flex cable and the PCB, all buttons will appear pressed. When the connection is flaky, buttons may appear pressed all the time, or go on and off.)

Fix: adding a little height to the cushion fixed the problem for me.

In this pic, I’m holding the flex cable with one of my fingers. The tube (it’s a piece of heat shrink, actually) is what I added to improve contact between the flex cable and PCB. The original rubber is still there.

「DETEKER」の「電子工作でよく使う電子部品セット」の トランジスターについて


トランジスターには色々な特性があります。例えば、電圧の限界値、許容電流、周波数の限界、ノイズ、ゲイン。これらの特性によって、そのトランジスターの用途が決まってきます。これらの特性は、トランジスターの「データシート」で簡単に調べられます。ネットで、例えば「2n2222 datasheet」のような検索をすればすぐに出てきます。

DETEKER のキットに含まれているトランジスターは18種もありますが、残念ながらすべての用途がカバーできるわけではないです。キットに含まれているすべてのトランジスターを調べた結果、これらのカテゴリーのトランジスターがありました。

  • オーディオ
  • 汎用
  • 電波
  • 高電圧

また、NPN トランジスターと同じ特性を持った PNP 版のトランジスターも多く含まれています。

個人的には残念だと思うのは、モーターの駆動に使えるトランジスターがないこと。(小型モーターなら、2n2222 あたりでいけなくはないかもしれませんが。)

さて、表を作ったのでご確認ください。載せていない属性もたくさんあります。データシートも、用途によっては、ノイズ値などはあったりなかったりします。(PC でご覧の場合は Shift+マウスホイールなどで横にスクロールができます。)

The following table shows several important properties of the transistors that come in DETEKER’s “Basic Electronic Components Kit” that is available on Amazon. (Use Shift+mouse wheel to scroll horizontally)

トランジスター名C-E 耐電圧C 電流ゲイン (最低は)
低電流時 (1 mA)
ゲイン (最大)
低電流時 (1 mA)
ゲイン (標準値)
低電流時 (1 mA)
ゲイン (最低は)
高電流時 (50 mA)
周波数特性種類データシートに書いてある用途カテゴリーデータシートの URL
Transistor nameMax C-E VoltageC currentGain (min)
at low current
(e.g. 1 mA)
Gain (max)
at low current
(e.g. 1 mA)
Gain (typical)
at low current
(e.g. 1 mA)
Gain (min)
(at high current)
FrequencyTypeDatasheets explicitly mentionAuthor’s interpretationDatasheet URL
S805020700 mA10040011040NPNClass B push-pull audio amplifier
General purpose
Complementary to S8550
Audio (オーディオ)
General purpose (汎用)
S855020700 mA10040011040PNPSee S8050Audio
General purpose
S901240500 mA6440040150 MHzPNPAudio
S9013NPNDesigned for use in 1W output amplifier of portable radios in class B push-pull operation
Complementary to S9012
S901445100 mA601000280NPNPre-amplifier, low level & low noise
Complementary to S9015
S9015PNPSee S9014Audio
S90183050 mA28198100GBWP
700 MHz min, 1100 typ
NPNAM/FM amplifier, local oscillator of FM/VHF tunerRF (電波)
A1015-50-150 mA7040025
(at -150 mA)
PNPLow-frequency amplifier
Complementary to C1815
C1815NPNSee A1015Audio
A42305200 mA2540
(at 30 mA)
50 MHz
20V, 10 mA
NPNHigh voltageHigh voltage (高電圧)
A92-305-200 mA2540
(at 30 mA)
50 MHz
20V, 10 mA
PNPHigh voltageHigh voltage
A733-50-100 mA90600200Current Gain Bandwidth
100 MHz min, 180 typ
PNPAF output amplifierAudio
C94550100 mA90600200Current Gain Bandwidth
100 MHz min, 180 typ
NPNAF output amplifierAudio
2N222230800 mA50300250 MHzNPNGeneral purpose
2N3906-40200 mA80300250 MHzPNPSwitching and amplifier
Complementary to 2N3904
General purpose
2N3904NPNHigh-speed switching
Complementary to 2N3906
General purpose
2N5401150200 mA50240Current gain bandwidth product
100 MHz min, 300 max
PNPGeneral purposeGeneral purpose
2N5551160600 mA8030
(at 50 mA)
NPNHigh voltage