MERIDA eSPRESSO 400 S EQ · SHIMANO STEPS E6100 · ESP32-S3

DIY E-TUBE Tap

A receive-only listener that plugs into your e-bike's E-TUBE wire, decodes what the motor, battery and display say to each other, and shows it on your own touchscreen and over Bluetooth.

Bus2-wire, power + data
Carrier~1 MHz BPSK*
Data rate~500 kbit/s*
BrainXIAO ESP32-S3
Draw~17 mA @ 8 V (no screen)

* Figures reported by hobbyists who probed Di2/E-TUBE wiring. Confirm them on your own bike in Phase 3. Hobby project, not affiliated with or endorsed by Shimano. Adding third-party parts may affect your warranty.

SC-E5000 display + your touchscreen unit Y-cable + JC200s, near the display 504 Wh battery in the down tube DU-E6100 motor: E-TUBE wire starts here
The bike: Merida eSpresso 400 S EQ. The dashed orange line is roughly where the E-TUBE wire runs inside the frame, from the motor up the down tube to the display on the handlebar. Everything in this guide plugs in at the display end, so nothing inside the frame gets touched.
Spotify now playing screen on the touchscreen
ADD-ON
Ride Audio

Control Spotify on your phone from the touchscreen, or let the ESP32-S3 play internet radio, MP3s and Spotify Connect through its own speaker.

Open the add-on guide →

Why this is a good learning project

You'll touch almost every layer of embedded engineering in one build: protecting a circuit from a vehicle's power bus, pulling a small high-frequency signal off a DC line, squaring it up with a comparator, timing pulses in hardware with the ESP32's RMT peripheral, working out an unknown protocol from captured data, and presenting the result on a touch UI and over Bluetooth Low Energy.

The guide is in build order. Each phase ends with a gate: a check that must pass before you continue. Tick the checkboxes as you go; your progress stays in this browser.

Chapter 02What it can and can't do

Shimano's own tools do two jobs: they report data, and they let E-TUBE Project change settings and update firmware. This build does the first job well. The second job needs Shimano's private protocol and signed firmware, so leave it to Shimano's tools.

FeatureStatusWhy
Plug into the bike without cutting factory wiringYesShimano's passive EW-JC130 Y-cable and EW-JC200 couplers do the splitting.
Read speed, assist mode, battery, error codesYes, with workThe bus is undocumented. You find which bytes mean what (Chapter 09).
Show data on your touchscreenYesLVGL graphics library on the ESP32-S3.
Send data to a phone over Bluetooth LEYesStandard BLE services you define yourself.
Switch assist mode and lights from the touchscreenYes, with add-onThe ESP32 presses the bike's own switch buttons electronically (Chapter 12). The bus only ever sees genuine Shimano messages.
Send your own commands on the busResearch onlyNeeds a transmitter and decoded write commands. Unknown messages can put the motor into an error state.
Change assist tuning profilesUse the appDone in E-TUBE Project. With the SC-E5000, that means Shimano's wired SM-PCE02 PC link or a Shimano dealer.
Work with the E-TUBE Project app / update firmwareNoFirmware comes from Shimano's servers and is most likely signed. A failed update can brick a drive unit. Use Shimano's SM-PCE02 PC link or a dealer.
Raise the 25 km/h assist limitNot coveredIt's a legal limit for pedelecs in the EU and Australia. Removing it makes the bike legally a moped and affects insurance and warranty.
Recommended setup

Keep the SC-E5000 as the bike's display unit, and use this tap as your own read-only dashboard. For settings and firmware, use Shimano's wired SM-PCE02 PC link or a Shimano dealer.

EW-EN100 or SC-E5000, not both

The EW-EN100 and the SC-E5000 fill the same role in a STEPS system, and Shimano's manual allows only one of them per bike. You can't add an EW-EN100 alongside your display. You can only replace the display with it (option D in Chapter 12), and then this touchscreen becomes your only screen.

Chapter 03How the E-TUBE bus works

The EW-SD50 cable has only two conductors. One is ground. The other carries a steady DC voltage (reported at roughly 7–9 V) that powers the display and other parts, and the data rides on top of that DC as a small high-frequency wave. This is power-line communication, the same idea as household powerline network adapters.

The wave is a ~1 MHz carrier using BPSK (binary phase-shift keying). The carrier never changes frequency or loudness. To send data, the transmitter flips the wave upside down (a 180° phase shift) at bit boundaries. Hobbyists report 2 carrier cycles per bit, which gives 500 kbit/s.

After a comparator turns the wave into a square wave, every normal half-cycle is a 500 ns pulse. A phase flip at a zero crossing joins two half-cycles of the same polarity, which gives one 1 µs pulse. To decode the bus you only have to find the long pulses. Try it below.

Phase flip → long pulse → bit change. Top: the bits. Middle: the wave on the wire, then the same wave after the 1 nF capacitor removes the DC and the bias network re-centres it on 1.65 V. Bottom: the comparator output the ESP32 sees. Shaded pulses are the 1 µs ones; each one sits exactly on a bit boundary where the bit value changed.

Two ways to read the flips

Absolute BPSK: a flip means "this bit differs from the last one". You recover the bits by toggling at every flip, up to an unknown first bit. Differential BPSK (DBPSK): a flip is a 1 and no flip is a 0. You won't know which scheme Shimano uses until you look at real frames. The decoder in Chapter 09 prints both.

Chapter 04System design

Your board sits on a branch of the motor-to-display cable, never in the middle of it. If your board fails or comes unplugged, the bike keeps working.

DU-E6100 drive unit original EW-SD50 (in frame) JC200 L2 50 Y EW-JC130 Y-cable 3 plugs · passive L3 · 250 mm SC-E5000 display (unchanged) L1 · 350 mm JC200 EW-SD50 pigtail (cut) your board (IP67 box) Protect + power fuse · TVS · choke · buck AC tap 1 nF + bias Comparator TLV3501 XIAO ESP32-S3 GPIO4 · RMT 3.3 V (Stage B) SPI + I²C Touchscreen LVGL dashboard BLE Phone nRF Connect / your app
The motor and display connection stays all Shimano cable. Your board hangs off leg L1. The data path (orange) only listens through a capacitor. The power path (teal) is wired in only after decoding works (Stage B). Thick lines are E-TUBE cables, thin lines are wires on your board.
BEFORE Motor Display one EW-SD50 cable AFTER Motor same cable, unplugged from display JC200 Y L3 → display port Display L1 → your board
The only new parts in the bike's path are one coupler and the Y-cable. The orange parts are what you add. To go back to stock, plug the original cable straight into the display again.

Chapter 05Bill of materials

Prices are typical Australian retail in AUD, including GST. They're estimates, so check before you order. Where to buy in Australia:

  • Shimano parts: your Merida dealer, 99 Bikes, or online bike stores. Bike shops can order E-TUBE parts through Shimano's Australian distributor.
  • Electronics: Core Electronics (XIAO, Pololu); Altronics or Jaycar (passives, enclosures, heat-shrink); DigiKey AU, element14 or Mouser AU (TLV3501, inductors; free shipping above about A$60).

A · Bike interface (Shimano parts)

PartQtyWhat it does≈ A$
EW-JC130 Junction B Y-cable, 350/50/250 mm1Passive 3-plug Y splitter. L2 to the motor cable, L3 to the display, L1 to your board.55–75
EW-JC200 2-port junction2Socket-to-socket coupler that joins two plugs. #1 joins the motor cable to L2, #2 joins L1 to your pigtail.60–80
EW-SD50 E-TUBE cable, 150 mm1Cut it about 80 mm from one plug. The plug end is your board pigtail.15–25
TL-EW02 plug tool1Plugs and unplugs E-TUBE connectors without bending the pins.15–20
Subtotal145–200

Cheaper alternative: skip JC200 #2 and the EW-SD50 by cutting leg L1 of the JC130 and soldering it straight to your board. That saves about A$45–65, but the JC130 can't be reused afterwards.

B · Electronics

RefPartQtyNotes≈ A$
MCUSeeed XIAO ESP32-S31Or ESP32-S3-DevKitC-1 while prototyping. If your touchscreen is an all-in-one ESP32-S3 board, use that instead.15–20
U2TLV3501AIDBVR comparator + SOT-23-6 adapter board14.5 ns, rail-to-rail input, runs on 3.3 V. LMV7219 is an alternative.7–10
U1Pololu D36V6F3 3.3 V buck module14.2–50 V in, 600 mA out.12–15
C11 nF, C0G/NP0, 100 V, 08052Blocks the DC, passes the 1 MHz data. Must be C0G for a stable value.1
L1470 µH power inductor, ≥150 mA, self-resonance > 2 MHz1Stops the buck converter from loading the data signal. Check the datasheet's self-resonant frequency (SRF).2–4
F1Resettable PTC fuse, 100 mA hold (200 mA if the screen runs from the bus)1Protects the bike if your board shorts.1
D1SS14 Schottky diode1Reverse-polarity protection, 0.4 V drop.0.50
TVS1SMAJ15A TVS diode1Absorbs voltage spikes above 15 V.1
D2BAT54S dual Schottky1Clamps the comparator input between GND and 3.3 V.0.50
R110 kΩ1Sets how lightly you load the bus.5
R2, R3100 kΩ2Bias the signal to mid-supply (1.65 V).
R41 MΩ (also get 2.2 MΩ and 470 kΩ)1Hysteresis feedback. The spares let you tune it.
R5, R647 kΩ2Threshold divider around the trimmer.
R7100 Ω1Series resistor on the GPIO line.
RV110 kΩ multi-turn trimmer1Sets the comparator threshold, about 1.49–1.81 V.2–3
C2, C410 µF 25 V ceramic, 100 nF ceramic ×34Buck input capacitor and comparator decoupling.2
C3100 µF low-ESR, 6.3 V1Handles the S3's current spikes when the radio transmits.1
JP12-pin header + jumper1Connects bus power. Left open in Stage A.0.50
PCBPerfboard 5×7 cm, or a small custom PCB1JLCPCB charges about A$8 for 5 boards, plus shipping to Australia.4–10
BoxIP67 enclosure ~60×40×20 mm + PG7 cable gland1Jaycar and Altronics stock these. Or 3D-print one and pot the board.10–15
Misc26 AWG silicone wire, glue-lined heat-shrink, conformal coating—Weatherproofing.20–25
Subtotal (touchscreen not included, you already have it)≈ 85–115

C · Tools

ToolWhy you need it≈ A$
Oscilloscope, ≥ 20 MHz (Hantek 6022BE, FNIRSI DSO or better)To see the 1 MHz carrier and measure its amplitude. You can't tune the comparator without it.110–250
24 MHz USB logic analyzer + PulseView (sigrok)Checks the comparator output independently of your firmware.15–25
MultimeterBus voltage, wire identification, current draw.30–50
ADuM3160 USB isolatorPrevents ground loops between the bike and a mains-powered laptop.25–35
Fine-tip soldering iron, flux, tweezersSOT-23 and 0805 parts.—
Shimano parts
A$145–200

Or about A$100–135 with the cut-leg alternative.

Electronics
A$85–115

Touchscreen not included.

Tools, if you have none
A$180–360

Scope, logic analyzer, multimeter, isolator.

D · Software (free)

ESP-IDF v5.x (RMT RX driver, NimBLE), LVGL 9 + esp_lvgl_port, Python 3 with pyserial, PulseView, and nRF Connect on your phone.

Chapter 06Circuit design

The board is two independent circuits that share only ground: a power path that feeds the ESP32 from the bus, and a signal path that listens to it. Build and test the signal path first. The power path stays disconnected (JP1 open) until Stage B.

Power path

J1 pigtail BUS+ GND F1 PTC 100 mA D1 SS14 TVS1 SMAJ15A JP1 close in Stage B L1 470 µH blocks 1 MHz C2 10 µF U1 buck D36V6F3 → 3.3 V VIN VOUT GND + C3 100 µF → XIAO 3V3 GND → XIAO GND
Each part handles one failure mode. F1: your board shorts. D1: wires swapped. TVS1: voltage spikes. L1: the buck converter's input capacitors would otherwise short out the 1 MHz data for everyone on the bus. JP1 keeps all of this disconnected until Stage B.

Signal path

BUS+ (J1) C1 1 nF C0G R1 10 kΩ 3V3 D2 BAT54S A R2 100k R3 100k R4 1 MΩ hysteresis ≈ 27 mV + − U2 TLV3501 · VCC 3V3, 100 nF at pin 3V3 R5 47k RV1 10k R6 47k R7 100 Ω XIAO D3 GPIO4 · RMT RX GND (J1) — shared with XIAO GND
The bus only ever sees C1 + R1, about 10 kΩ at 1 MHz. C1 removes the DC. R2/R3 re-centre the signal on 1.65 V. D2 keeps node A between 0 and 3.3 V. RV1 places the threshold just beside the idle level. R4 adds a little positive feedback so noise near the threshold can't cause chatter. Nothing ever drives back towards the bus.

The numbers behind the parts

High-pass corner
≈ 2.7 kHz
f = 1 / (2π · (R1 + R2∥R3) · C1) = 1 / (2π · 60 kΩ · 1 nF)

Far below the 1 MHz carrier, so the data passes. DC (0 Hz) is blocked completely.

Load on the bus
≈ 10.2 kΩ
|ZC1| at 1 MHz = 1 / (2π · 1 MHz · 1 nF) = 159 Ω, + R1 10 kΩ

Light enough that the motor and display won't notice the extra listener.

Choke impedance
≈ 2.95 kΩ
XL = 2π · 1 MHz · 470 µH

Isolates the buck's input capacitors from the data. A 100 µH part would give only 630 Ω, too low.

Hysteresis
≈ 27 mV
ΔV ≈ 3.3 V · (R1∥R2∥R3) / R4 = 3.3 · 8.3 kΩ / 1 MΩ

For a weak signal (< 80 mV p-p) use 2.2 MΩ (12 mV). For a noisy one, use 470 kΩ (58 mV).

Threshold range
1.49–1.81 V
3.3 V · (47k … 57k) / 104k

RV1 sweeps about ±160 mV around the 1.65 V bias. Set it just off idle so a silent bus reads a steady level.

Bus current (Stage B)
≈ 17 / 46 mA
(35 mA, or 95 mA with screen) · 3.3 V / 0.85 / 8 V

Without / with the touchscreen. Measure your real draw before mounting.

PCB layout

A 60 × 40 mm two-layer board. The layout follows the schematic: power runs along the top, the listening circuit sits in the bottom-left corner, and the XIAO is in the bottom-right corner. The top-layer drawing is to scale (10 px = 1 mm). Use it as the placement guide when you lay out the board in KiCad, or as a map when you build it on perfboard.

POWER SIGNAL BUS+ GND F1 D1 TVS1 JP1 L1 470µH C2 U1 3.3 V buck VIN GND OUT C3 USB-C XIAO ESP32-S3 (top view) D0 D1 D2 D3 D4 D5 D6 5V GND 3V3 D10 D9 D8 D7 C1 R1 D2 R2 R3 U2 C4 R4 R7 RV1 R5 R6 60 mm 40 mm LEGEND power trace 0.5 mm signal 0.25 mm bottom layer local / GND link pad via to GND/3V3 silkscreen GND pour
Top layer, to scale. The listening path from C1 to the XIAO's D3 pad (orange) is about 35 mm long, with no power trace crossing it. The 3.3 V feed reaches the XIAO on the bottom layer (dashed) so it never cuts across the signal area. Every GND pad and via joins a ground pour that fills both layers. The slot next to J1 is where a cable tie anchors the pigtail.
Rule 1 · Short signal path

Keep C1 → U2 → D3 under 40 mm, and R7 within 5 mm of the XIAO pad. A long trace picks up noise and slows the edges RMT has to measure.

Rule 2 · Decouple U2

Place C4 (100 nF) within 2 mm of U2's supply pin, with its GND via right next to the pad. Fast comparators oscillate without it.

Rule 3 · Quiet ground

Fill both layers with GND and stitch them with a via every 5 mm. Never route the 3.3 V or bus-power traces through the signal zone.

Rule 4 · Noisy parts far away

The buck converter switches fast, so keep it at the opposite end of the board from the comparator. L1 sits between the two zones on purpose.

Rule 5 · Widths and clearances

Power traces 0.5 mm, signal 0.25 mm, 0.2 mm clearance. Everything here is well inside JLCPCB's standard 2-layer limits.

Rule 6 · Built for a bike

Four M2.5 mounting holes, a strain-relief slot, conformal coating (keep it off the trimmer and USB), and no parts taller than 6 mm except the Pololu board.

Footprints and ordering

RefKiCad footprintHand-solder tip
R1–R7, C1Resistor_SMD / Capacitor_SMD: 0805Tin one pad, place the part with tweezers, then solder the other side.
C40603Do it right after U2, before nearby parts get in the way.
C2, F11206Easy.
C31210 (100 µF ceramic) or a 6.3 mm SMD electrolyticCheck polarity if you use an electrolytic.
D1, TVS1Diode_SMD: SMAThe cathode band matches the silkscreen bar.
D2SOT-23Flux, then drag-solder.
U2SOT-23-6Pin 1 dot faces C1. Check for bridges with a magnifier.
L1Inductor_SMD 7.3 × 7.3 mm (match your part)Use plenty of heat, since the pads are large.
RV1Potentiometer_SMD: Bourns 3314JSolder last, and keep flux out of the wiper.
U13 × 2.54 mm pin headerSolder the module flat onto short pins.
XIAOSeeed XIAO castellated (Seeed KiCad library)Or fit pin headers so you can swap the XIAO.
J12 × 2.5 mm pads + 3 × 7 mm slotTin the pigtail cores first.

From KiCad to a board: draw the schematic, assign the footprints above, place parts as in the drawing, route, add the GND pours (B.Cu and F.Cu), run the design rules check (DRC), then File → Plot Gerbers plus a drill file. At JLCPCB, choose 2 layers, 1.6 mm, lead-free HASL or ENIG, 5 pieces. That's about A$8 for the boards plus A$15–25 shipping to Australia.

Chapter 07ESP32-S3 wiring

The pin plan is for the XIAO ESP32-S3 with an SPI display and I²C touch controller, the most common touchscreen setup. Check your screen's pin names against the table and adjust. If your screen is an all-in-one ESP32-S3 board, connect the comparator to any free GPIO that isn't a strapping pin and change TAP_GPIO in the firmware.

USB-C XIAO ESP32-S3 D0GPIO1 → LCD DC D1GPIO2 → LCD CS D2GPIO3 · strapping pin, leave free D3GPIO4 ← comparator (via R7) D4GPIO5 ↔ touch SDA D5GPIO6 → touch SCL D6GPIO43 ← touch INT 5VUSB only, leave unused GNDbus GND · U2 · screen 3V3U1 out (Stage B) · U2 · screen D10GPIO9 → LCD MOSI D9GPIO8 ← LCD MISO (optional) D8GPIO7 → LCD SCK D7GPIO44 → LCD RST / backlight PWM
Only one pin carries bus data: D3 (GPIO4). Everything else is for the screen. GPIO3 and the S3's other strapping pins (GPIO 0, 45, 46) set boot behaviour, so avoid them. D6/D7 are the default UART pins; logs go over USB instead, so they're free.
FromToStageNote
Pigtail GNDBoard GND rail → XIAO GNDA + BAlways connected.
Pigtail BUS+C1 (signal tap)A + BListen only.
Pigtail BUS+F1 → D1 → JP1 → L1 → U1B onlyJP1 stays open in Stage A.
U1 VOUT (3.3 V)XIAO 3V3 pin + C3B onlyUnplug USB before closing JP1. Never feed both at once.
U2 OUTR7 → XIAO D3 / GPIO4A + BKeep this wire short (< 5 cm).
XIAO USB-CLaptop on battery, or via USB isolatorAAvoids a ground loop through the bike.

Chapter 08Build, step by step

0 of 0 done
Golden rules

Battery out whenever you plug or unplug E-TUBE connectors. Your board never drives the bus. Disconnect the tap before any Shimano firmware update. If the display shows an E or W error code, unplug the tap first and ask questions second.

1
bench · 20 min

Look for a spare port

Before buying the Y-cable, check the DU-E6100 motor and the SC-E5000 display for an unused E-TUBE port covered by a small rubber dummy plug. If you find one, your pigtail can plug straight in and you can skip the JC130 and JC200 #1. Look under the motor cover and behind the display mount.

Motor (look under the cover) DU-E6100 in use spare?rubber cap Display (back side) SC-E5000 from motor spare?
What to look for. A spare port is a small round E-TUBE socket closed with a rubber dummy cap. This is an example layout: port count and position differ by model, and your bike may have none.
2
bike · 30 min

Install the Y-cable

  1. Turn the system off and remove the battery.
  2. Use the TL-EW02 to unplug the motor cable from the display. Pull the plug straight; never pull on the wire.
  3. Plug the motor cable into one side of EW-JC200 #1. Plug the JC130's 50 mm leg (L2) into the other side.
  4. Plug the 250 mm leg (L3) into the display.
  5. Leave the 350 mm leg (L1) loose for now, with JC200 #2 on its end (an empty port does no harm).
  6. Battery in, power on. The display should work normally with no error code.
Step 1 · Unplug Step 2 · Couple Step 3 · Connect Display pull straight TL-EW02 battery out first JC200 Y motor cable L2 50 motor cable + L2 into JC200 #1 Y L3 250 Display JC200#2 empty port is fine L1 waits for your board
Three plug moves, no cutting. Every plug clicks in with the TL-EW02, battery out. Afterwards, power on and check the display before going further.
3
bench + bike · 45 min

Make the pigtail and measure the bus

  1. Cut the 150 mm EW-SD50 about 80 mm from one plug. Strip the outer jacket 15 mm and the two cores 3 mm. Keep the bare ends apart, and cover each with a piece of heat-shrink left loose for now.
  2. Plug the pigtail into JC200 #2 on leg L1. Battery in, power on.
  3. Voltage: multimeter on DC volts, probes on the two cores. A positive reading means the red probe is on BUS+. Write down the value.
  4. Signal: scope ground clip on GND, probe tip through a 1 nF capacitor to BUS+, 1× probe setting, AC coupling, 0.5 µs/div. Press the assist buttons and watch for bursts. Note the peak-to-peak amplitude and how often bursts come.
  5. Mark BUS+ with red heat-shrink, then shrink both cores.
plug cut leftover piece (bin it, or keep for practice) ≈ 80 mm Cut end, close up outer jacket red heat-shrink = BUS+ 15 mm 3 mm bare copper: keep the two tips apart until each one is covered
Making the pigtail. Only the plug end goes on the bike. Which core is BUS+ is decided by measuring (next figure), not by colour.
pigtail BUS+ GND 8.2 V⎓ multimeter · DC V red probe black probe 1 nF probe tip ground clip AC coupling · 0.5 µs/div · note p-p phase flip
Two measurements, bike on. A positive DC reading means the red probe is on BUS+. On the scope, measure the peak-to-peak height of the burst. That number sets R4 and RV1 in Phase 4.
MeasureExpectedYour value
DC bus voltage≈ 7–9 V
Carrier frequency≈ 1 MHz
Signal amplitudeunknown, sets R4 and RV1
Burst interval at idleunknown
4
bench · 2 h

Build the signal path and bench-test it

  1. Solder U2 onto its SOT-23 adapter first and check for bridges with a magnifier.
  2. Build the signal path from the schematic: C1, R1, D2, R2/R3, R4, RV1/R5/R6, R7, plus a 100 nF capacitor right at U2's supply pins. Tie U2's shutdown pin to the "enabled" level from its datasheet.
  3. Fit the power-path parts too, but leave JP1 open.
  4. Self-test without the bike: flash the test firmware, which outputs a 1 MHz square wave on GPIO2 through the LEDC peripheral. Feed it into C1 through a 10 kΩ + 1 kΩ divider, giving about 300 mV.
  5. Turn RV1 until the logic analyzer on GPIO4 shows a clean 500 ns / 500 ns square wave. Remove the test signal: GPIO4 should sit still.
XIAO D3 / GPIO4 D1 / GPIO2 GND 10 kΩ ≈ 300 mV 1 kΩ your signal path C1 → … → U2 → R7 logic analyzer CH0 on GPIO4 expect 500 ns / 500 ns
The ESP32 tests itself. GPIO2 makes a 1 MHz square wave and the divider shrinks it to about 300 mV, similar to a bus signal. Turn RV1 until GPIO4 shows a clean square wave, then disconnect the 10 kΩ: GPIO4 should go quiet.
5
bike · Stage A

Connect to the bike, powered from USB

  1. Battery out. Solder the pigtail to J1: BUS+ to the C1/F1 side, GND to the GND rail.
  2. Plug the pigtail into JC200 #2. Plug the XIAO into a laptop running on battery, or through the USB isolator.
  3. Battery in, power on. Check the display for errors.
  4. Start the capture firmware and the Python logger (Chapter 10). You should see lines starting with F and pulse widths near 500 and 1000 ns.
  5. Re-tune RV1 if you see stray pulses during silent gaps. Change R4 if it still chatters.
BikeY-cable leg L1 JC200#2 pigtail your board ● signal path: connected ○ JP1 open: no bus power XIAO powered by USB USB serial · pulses Laptop running on battery etube_decode.py or ADuM3160 isolator if on the charger
Stage A: the bike provides data only. The XIAO gets its power from USB, and JP1 stays open, so nothing draws current from the bus. Keeping the laptop on battery means there's no ground path back through the bike.
6
desk · evenings

Decode the protocol

Follow Chapter 09. This is the long, fun part. Build a table of which bits change when you change one thing on the bike.

ECO10110010011100101101000110011010HIGH10110010011111001101000101101100 assist mode field? changes every time → CRC? same message type, two captures (illustration, not real Shimano data)
Line captures up and look for the columns that change. A small group that follows the button you pressed is a data field. A block at the end that changes whenever anything changes is almost always a checksum.
7
desk · 1–2 days

Dashboard and Bluetooth

Move your decoder tables into firmware, draw the LVGL screen (Chapter 11), and add the BLE service. Test everything with the bike on a stand.

decoder state mode · battery · speed SPI · 10 fps 23.4 NORMAL · 72 % Bluetooth LE notify nRF Connect Battery Service 0x2A19 Level 72 % E-TUBE Tap · Status
One source of truth, two outputs. The decoder holds the latest values. The screen redraws from them up to 10 times a second, and BLE notifies the phone when a value changes.
8
bike · Stage B

Power the board from the bike

  1. Battery out, USB unplugged. Close JP1.
  2. Put the multimeter (mA range) in series with BUS+ for the first power-up.
  3. Battery in, power on. Check: display has no errors, current ≈ 17 mA without the screen (≈ 46 mA with it), the S3 boots.
  4. Check the scope again: the carrier amplitude should be the same as in Phase 3. If it dropped, L1's impedance is too low, so use a bigger choke.
BUS+ 17.3 mA meter in series F1 · D1 JP1 closed L1 470 µH U1 → 3.3 V XIAO 3V3 boots from bike USB unplugged scope via 1 nF: carrier p-p same as Phase 3?
First power-up from the bike, with two checks. The meter confirms the draw: about 17 mA without the screen, about 46 mA with it. The scope confirms the power path isn't loading the data. Unplug the USB first so the XIAO is never fed from two supplies.
9
bike · 1 h

Weatherproof and mount

  1. Conformal-coat the board (not the USB port or trimmer). Fit it into the IP67 box with the pigtail through the PG7 gland.
  2. Mount the box near the junction: on the head tube cables or inside a frame cover, away from the motor (heat) and brake rotor.
  3. Leave a strain-relief loop so a pull unplugs the pigtail from JC200 #2 rather than breaking a solder joint.
  4. Mount the touchscreen on the bars with a waterproof cover. Dim the backlight or have it switch off when idle.
  5. Short test ride, then check for error codes and for any chafing points.
motor battery touchscreen + SC-E5000 on bars IP67 tap box + Y-cable on the head tube cable bundle, with a strain-relief loop keep the box away from motor heat brake rotors: keep cables clear brake rotors: keep cables clear
Short cables, dry and cool. The Y-cable is only 250–350 mm per leg, so the tap box ends up near the display, on the head tube cable bundle or under a frame cover. The strain-relief loop means a snag pulls the plug out of the JC200 instead of tearing a solder joint.

Chapter 09Decoding the protocol

The ESP32 sends raw pulse widths to your laptop. The laptop turns them into bits and compares frames. Doing the analysis in Python keeps the firmware simple, and you can re-decode old captures as your understanding improves.

Method: change one thing, see what changes

  1. Baseline. Bike on, nothing touched, for 30 s. Frames that repeat are polling or heartbeats. Note how often each kind appears.
  2. Group by length and start. Frames that begin with the same bits and have the same length are probably the same message type from the same sender.
  3. Change one variable, then look at which bit positions change in that message type.
  4. Look for checksums. The last 8 or 16 bits changing whenever anything changes is a CRC. Try CRC-8 and CRC-16 variants with a tool like reveng.
ExperimentDo thisLook for
Assist modeStep OFF → ECO → NORMAL → HIGH → backA small field (2–3 bits) stepping through 4 values.
Walk modeHold walk assist for 5 sOne flag bit, or a separate message type.
Wheel speedBike on stand, assist OFF, spin the rear wheel by hand at a steady rateA field that rises with speed, or a pulse-period field that falls.
LightsToggle lights (if connected to the motor)One flag bit.
BatteryCapture before and after a rideA byte that falls slowly. Compare with the % on the display.
ErrorsCompare with a known error code if one ever appearsNote it, but don't create faults on purpose.

Laptop decoder

This turns the logger's pulse widths into bits. Each width is rounded to whole 500 ns half-cycles, so timing errors can't build up across a long frame. In a test with ±150 ns of random jitter on every pulse, it decoded 20,000 out of 20,000 simulated frames correctly.

#!/usr/bin/env python3
"""etube_decode.py: read 'F n w0 w1 ...' lines from the ESP32 and print bits.
Usage: python3 etube_decode.py /dev/cu.usbmodem1101 [label]"""
import sys, collections, serial

HALF_NS = 500          # one carrier half-cycle (1 MHz carrier)

def decode(widths_ns):
    # Returns (bpsk_bits, dbpsk_bits). Time is counted in whole half-cycles.
    units, flips = 0, set()
    for w in widths_ns:
        n = round(w / HALF_NS)
        if n == 2:                         # 1 us pulse = phase flip at its centre
            flips.add((units + 1) // 4)    # 4 half-cycles per bit
        units += n
    nbits = units // 4
    bpsk, cur = [], 1                      # first bit is unknown; assume 1
    for k in range(nbits):
        if k in flips:
            cur ^= 1
        bpsk.append(cur)
    dbpsk = [1 if k in flips else 0 for k in range(nbits)]
    return bpsk, dbpsk

def main():
    port = serial.Serial(sys.argv[1], 115200, timeout=1)
    label = sys.argv[2] if len(sys.argv) > 2 else "capture"
    seen = collections.Counter()
    with open(f"{label}.log", "a") as log:
        for raw in port:
            parts = raw.decode(errors="ignore").split()
            if len(parts) < 3 or parts[0] != "F":
                continue
            widths = [int(x) for x in parts[2:] if x != "0"]
            bpsk, dbpsk = decode(widths)
            a = "".join(map(str, bpsk))
            b = "".join(map(str, dbpsk))
            seen[a] += 1
            line = f"{len(bpsk):4d}  BPSK {a}\n      DBPSK {b}"
            print(line + ("" if seen[a] > 1 else "   <-- new"))
            log.write(f"{label}\t{a}\t{b}\n")

if __name__ == "__main__":
    main()

Run it once per experiment with a label (baseline, eco, high, …), then compare the logs side by side. Messages marked <-- new are the first time a bit pattern was seen, which helps a lot when you press a button.

Chapter 10Firmware

GPIO4comparator edges RMT RX + DMA12.5 ns ticks, hardware frame capture taskidle > 20 µs ends frame widths USB serial loggerphase 5–6: to etube_decode.py phase 7+ decoderyour field tables state LVGL dashboard · core 0 NimBLE GATT server Capture + decode pinned to core 1 so the UI never makes you miss a frame.
The hardware does the timing, so the CPU only handles whole frames. RMT time-stamps every edge into memory via DMA and raises one interrupt per frame. Start with only the top row (capture + USB log) and add the bottom row once you know what the fields mean.

Capture + logger (ESP-IDF v5)

This is the whole Stage A firmware: it receives frames on GPIO4 and prints pulse widths in nanoseconds. While a line is printing, the next frame can be missed. That's fine for exploration; once you need every frame, use a double buffer.

// main/tap_main.c: E-TUBE tap, Stage A capture firmware
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "driver/rmt_rx.h"
#include "driver/ledc.h"
#include "esp_err.h"

#define TAP_GPIO       4            // XIAO D3, comparator output
#define TEST_GPIO      2            // XIAO D1, 1 MHz self-test output (Phase 4 only)
#define RMT_RES_HZ     80000000     // 12.5 ns per tick
#define RX_SYMBOLS     2048

static rmt_channel_handle_t rx_chan;
static rmt_symbol_word_t rx_buf[RX_SYMBOLS];
static QueueHandle_t rx_queue;

static const rmt_receive_config_t rx_cfg = {
    .signal_range_min_ns = 100,     // drop glitches shorter than 100 ns
    .signal_range_max_ns = 20000,   // 20 us without an edge ends the frame
};

static bool IRAM_ATTR on_rx_done(rmt_channel_handle_t ch,
                                 const rmt_rx_done_event_data_t *ev, void *ctx)
{
    BaseType_t woken = pdFALSE;
    xQueueSendFromISR(rx_queue, ev, &woken);
    return woken == pdTRUE;
}

static void tap_init(void)
{
    rmt_rx_channel_config_t cfg = {
        .gpio_num = TAP_GPIO,
        .clk_src = RMT_CLK_SRC_DEFAULT,
        .resolution_hz = RMT_RES_HZ,
        .mem_block_symbols = RX_SYMBOLS,
        .flags.with_dma = true,     // ESP32-S3 supports DMA on RMT RX
    };
    ESP_ERROR_CHECK(rmt_new_rx_channel(&cfg, &rx_chan));
    rx_queue = xQueueCreate(4, sizeof(rmt_rx_done_event_data_t));
    rmt_rx_event_callbacks_t cbs = { .on_recv_done = on_rx_done };
    ESP_ERROR_CHECK(rmt_rx_register_event_callbacks(rx_chan, &cbs, NULL));
    ESP_ERROR_CHECK(rmt_enable(rx_chan));
}

// Phase 4 self-test: 1 MHz, 50 % square wave on TEST_GPIO
static void test_signal_start(void)
{
    ledc_timer_config_t t = {
        .speed_mode = LEDC_LOW_SPEED_MODE, .timer_num = LEDC_TIMER_0,
        .duty_resolution = LEDC_TIMER_4_BIT, .freq_hz = 1000000,
        .clk_cfg = LEDC_AUTO_CLK,
    };
    ESP_ERROR_CHECK(ledc_timer_config(&t));
    ledc_channel_config_t c = {
        .gpio_num = TEST_GPIO, .speed_mode = LEDC_LOW_SPEED_MODE,
        .channel = LEDC_CHANNEL_0, .timer_sel = LEDC_TIMER_0, .duty = 8,
    };
    ESP_ERROR_CHECK(ledc_channel_config(&c));
}

static void tap_task(void *arg)
{
    rmt_rx_done_event_data_t ev;
    for (;;) {
        ESP_ERROR_CHECK(rmt_receive(rx_chan, rx_buf, sizeof(rx_buf), &rx_cfg));
        xQueueReceive(rx_queue, &ev, portMAX_DELAY);
        printf("F %u", (unsigned)ev.num_symbols);
        for (size_t i = 0; i < ev.num_symbols; i++) {
            const rmt_symbol_word_t *s = &ev.received_symbols[i];
            // ticks * 12.5 ns; the last symbol's duration1 may be 0
            printf(" %u %u", s->duration0 * 25 / 2, s->duration1 * 25 / 2);
        }
        printf("\n");
    }
}

void app_main(void)
{
    tap_init();
#ifdef CONFIG_TAP_SELFTEST
    test_signal_start();
#endif
    xTaskCreatePinnedToCore(tap_task, "tap", 4096, NULL, 10, NULL, 1);
}
Project setup

idf.py create-project etube_tap, idf.py set-target esp32s3, then in menuconfig set the console to USB Serial/JTAG. Add a TAP_SELFTEST bool option in Kconfig.projbuild for Phase 4, or simply call test_signal_start() while bench-testing.

Bluetooth LE layout

ServiceUUIDCharacteristicData
Battery Service0x180FBattery Level 0x2A19uint8 %, notify. Phones and bike computers understand it automatically.
Device Information0x180AModel, firmware revStrings, read.
E-TUBE Tap (custom)generate a random 128-bit UUIDStatusPacked struct, notify: speed ×10 (uint16), assist mode (uint8), battery (uint8), error code (uint16).
Raw frameLast decoded frame bytes, notify. Useful for wireless capture once the board is sealed in its box.

Chapter 11Touchscreen dashboard

Example layout, sample values. A 320×240 screen in landscape. On its own, the tap only shows the mode. Add the button add-on from Chapter 12 and tapping ECO, NORMAL or HIGH presses the bike's switch for you.

  • Use esp_lvgl_port with your display driver (ST7789, ILI9341 or GC9A01) and touch driver (CST816 or FT6236).
  • Update the screen at most 10 times a second from the decoder's state, never from inside the capture task.
  • Swipe screens: trip stats, a raw-frame viewer (handy while decoding), and a settings page for brightness and units.
  • Large, high-contrast numbers stay readable in sunlight. Turn the backlight off after 60 s with the bike stopped.
Music on the same screen

The Ride Audio add-on adds a mini player to this screen, plus Now playing, Radio and MP3 screens.

Chapter 12Controlling assist and lights

To make the touchscreen change things, something has to tell the motor. There are three places the ESP32 could do that from, and they carry very different risks. The diagram shows where each one attaches.

A · Press the switch's own buttons (recommended) Touchscreen ESP32-S3 PhotoMOS ×3isolated contacts Assist switchits own chip sends genuine frames Motor B · Transmit your own frames on the bus Touchscreen ESP32-S3 BPSK transmitter (new hardware)+ every write command decoded unknown device → E/W? Motor C · Wire into the display's circuit board Touchscreen ESP32-S3 SC-E5000 openedlisten to its internal chip signals Motor
Option A is the only one where the motor never sees anything new. The ESP32 "presses" buttons on the genuine Shimano switch, and the switch sends its normal messages. In B, your board talks on the bus itself. In C, you open the display and listen to the signals between its chips, which may already be decoded into clean bits (see below).
OptionRead numbersChange assistLightsRisk to the bikeDifficulty
A · Button add-on + bus tapTapYesYes*Low. The switch is opened and must be resealed.Moderate, a weekend
B · Bus transmitterTapIn theoryIn theoryHigh: error codes, unknown motor reactionMonths of research
C · Display chip tapDepends on chipsVia its buttons (= A)Via its buttons (= A)Medium–high: display opened, waterproofing lostHard
D · Shimano minimal setup: EW-EN100 + switch, no SC displayTapSwitch, or ADepends on layoutLow, official partsEasy. Check compatibility first.

* Lights work with option A if the light toggle is a button you can reach, on the switch or on the display. Wire that button the same way.

Can my touchscreen fully replace the display?

As the thing you look at: yes. As the thing that runs the bike: not by itself. On STEPS, the display or an EW-EN100 is the system's user-interface unit. There can only be one of them, and the motor expects a recognised Shimano unit in that role. An unknown device won't be accepted.

A clean way to free up the handlebar is option D. Shimano lists display-less layouts for its STEPS motors, where an EW-EN100 plus an assist switch takes the display's place. If your E6100 supports that layout, remove the SC-E5000 and fit the EW-EN100 in its place. The two can't be fitted together. The EW-EN100 also gives you the E-TUBE Project phone app, and your touchscreen becomes the only screen. Confirm with your Merida dealer or Shimano's compatibility chart for your exact motor and firmware first, and check which button toggles the lights in that layout.

Can I take the signals from the display's chips instead of the motor wire?

Possibly. It depends on which chips are on the display's circuit board. The display only knows what reaches it over the same E-TUBE wire, so the data is the same either way. What can differ is its form: inside the display, the job of turning the 1 MHz wave into bits may already be done by a chip, leaving clean digital signals you can read with a logic analyzer.

inside the SC-E5000 (opened) E-TUBEwire coupling+ filter 1 MHz wave bus modemseparate chip? main chipMCU LCD driverseparate chip? LCD buttons 1 digital bitsUART / SPI? 2 screen contentssegment writes 3 button lines your current tap sees this
Three possible tap points, each existing only if the board is built that way. Dashed chips may not exist as separate parts. If the bus modem is built into the main chip, tap point 1 disappears and the only signal left is the same analog wave as on the wire. If the main chip drives the LCD itself, tap point 2 disappears too.
Tap pointWhat you'd getExists ifDifficulty
1 · modem → main chipAlready-demodulated bits, probably UART or SPI. No comparator and no BPSK decoding: PulseView can decode it directly.The bus interface is a separate chipEasiest to decode, if it exists
2 · main chip → LCD driverExactly what's on screen (speed digits, battery bars) as writes to the driver's segment memory.The display uses a separate LCD driver chip (HT1621-style)Moderate: map memory bits to segments
3 · buttonsButton presses, and a place to "press" them (Chapter 12 method)AlwaysEasy

A warning from others' teardowns: one hobbyist who decapped a Shimano chip reported a custom Renesas-based design with the bus logic built in (unverified, from the EEVblog thread). If the SC-E5000 works the same way, tap point 1 doesn't exist. Only opening yours will tell you.

How to find out

  1. Battery out. Open the display carefully, noting every seal and clip. Photograph both sides of the board in sharp focus.
  2. Read the chip markings and look up their datasheets. Look for the main chip, any small chip near the E-TUBE connector (a modem or line driver), and any chip with many traces running to the LCD (an LCD driver).
  3. Measure the logic voltage on the chips' supply pins. The ESP32-S3 accepts 3.3 V only, so a 5 V board needs a level shifter or resistor divider first.
  4. Bike on, logic analyzer on the lines between chips. A line that bursts at the same moments the scope shows bursts on the E-TUBE wire is the digital link. Try PulseView's UART and SPI decoders on it.
  5. Connect the S3 as a listener only: input pin through a 1 kΩ series resistor, with a shared ground. Never drive these lines.

Trade-offs: you lose the display's warranty and factory waterproof seal, and thin wires have to leave the housing and be resealed. If the display fails, you lose the bike's controls. The gain, if tap point 1 exists, is that you skip the analog front end and BPSK decoding completely.

Send me photos of both sides of the board, or the chip markings, and I'll tell you which tap points your display actually has.

Option A: the button add-on

Inside the assist switch, each button is a small tactile switch on a circuit board. A PhotoMOS relay is a tiny solid-state switch controlled by an internal LED. Solder its output across a tactile switch, and when the ESP32 lights the LED, the relay closes that switch exactly like a finger press. The relay's two sides are electrically isolated, and its output works in either direction, so you don't need to know which pad is which.

XIAO D4 SDA D5 SCL I²C (shared with touch) U3 MCP23008 GP0 GP1 GP2 VDD 3V3 · A0–A2, VSS → GND RESET → 3V3 · address 0x20 inside the assist switch (opened) 470 Ω AQY212 UPbutton pads 470 Ω AQY212 DOWNbutton pads 470 Ω AQY212 LIGHTbutton pads LED cathodes → GND (board)
Three contact closures, no electrical connection to the bike. The MCP23008 adds 8 outputs over the I²C bus the touch controller already uses, since the XIAO has no free pins left. Each output lights a PhotoMOS LED (about 4 mA through 470 Ω). Each relay's output simply bridges one button, just as your thumb would.
  1. Battery out. Open the assist switch (normally the left-hand bar unit). Note every screw and the gasket, and photograph the circuit board.
  2. Find the pads. Each tactile switch has 4 legs in 2 connected pairs. With the multimeter on continuity, find the pair that only beeps while the button is held down.
  3. Prove it. Battery in. Briefly bridge those two pads with insulated tweezers: the assist mode should change. Battery out again.
  4. Solder 30 AWG Kynar wire to each pad pair. Many switch boards share one common pad, which gets you down to 4 wires for 3 buttons. Hot-glue the wires near the joints for strain relief.
  5. Route and seal. Take the wires out through a small hole in the housing and seal it with neutral-cure silicone (acid-cure types corrode copper). Refit the gasket.
  6. Connect to the relay outputs on your board through a small locking connector (JST-PH), so the switch can be disconnected.
  7. Close the loop in firmware: press for 150 ms, then watch the bus tap. If the decoded assist mode didn't change within 300 ms, show a warning instead of pressing again and again.
Keep these on the real buttons

Don't map walk assist to the touchscreen, because it drives the motor with no pedalling. Never let firmware press buttons on its own while riding. The MCP23008 starts up with every output off, so a reboot can't leave a button "held".

// main/buttons.c: press the switch's buttons through MCP23008 + PhotoMOS
#include "driver/i2c_master.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

#define MCP23008_ADDR  0x20
#define REG_IODIR      0x00
#define REG_OLAT       0x0A

enum { BTN_UP = 0, BTN_DOWN = 1, BTN_LIGHT = 2 };

static i2c_master_dev_handle_t mcp;

static void mcp_write(uint8_t reg, uint8_t val)
{
    uint8_t buf[2] = { reg, val };
    ESP_ERROR_CHECK(i2c_master_transmit(mcp, buf, sizeof(buf), 50));
}

// bus: the I2C bus handle your touch driver already created
void buttons_init(i2c_master_bus_handle_t bus)
{
    i2c_device_config_t dev = {
        .dev_addr_length = I2C_ADDR_BIT_LEN_7,
        .device_address = MCP23008_ADDR,
        .scl_speed_hz = 400000,
    };
    ESP_ERROR_CHECK(i2c_master_bus_add_device(bus, &dev, &mcp));
    mcp_write(REG_OLAT, 0x00);      // all relays open before enabling outputs
    mcp_write(REG_IODIR, 0xF8);     // GP0..GP2 outputs, rest inputs
}

// 150 ms = short press. Use about 1500 ms only where Shimano's manual
// says a long press is needed (lights on some layouts). Never walk mode.
void button_press(int btn, int ms)
{
    mcp_write(REG_OLAT, (uint8_t)(1u << btn));
    vTaskDelay(pdMS_TO_TICKS(ms));
    mcp_write(REG_OLAT, 0x00);
}

Add-on parts (AUD)

RefPartQtyNotes≈ A$
U3MCP23008-E/P I²C 8-bit GPIO expander (DIP-18)1Shares the touch controller's I²C pins.3–4
K1–K3AQY212EH PhotoMOS relay (or Toshiba TLP222A)360 V, 500 mA, isolated, polarity-free output.9–15
R8–R10470 Ω resistor3About 4 mA LED current from 3.3 V.1
—30 AWG Kynar wire-wrap wire1 rollFine enough for switch pads.8
J3JST-PH 4-pin connector pair1Lets you disconnect the switch.3
—Neutral-cure silicone sealant1For resealing the switch housing.10–12
Add-on subtotal≈ 34–43

Chapter 133D-printed case

A handlebar head unit that holds the touchscreen and the tap board together. It's a single parametric OpenSCAD file, etube_case.scad, that produces four printable parts: bezel, shell, mount and gasket. All the dimensions below are the file's defaults, sized for a typical 2.8-inch SPI touchscreen module. Measure your own screen first and change the numbers at the top of the file. Everything else resizes automatically.

With this case, the tap board lives on the handlebar instead of in a separate box near the head tube. The pigtail comes in through the cable gland and plugs into JC200 #2 on the Y-cable's 350 mm leg, which sits near the display.

etube_case.scad open in OpenSCAD 2021.01: parameters in the editor on the left, and on the right the exploded assembly with the dark bezel on top, the yellow TPU gasket, the shell with its cable-gland hole, and the handlebar mount with strap slots underneath. The console reports outer size 104.8 x 68.8 mm, total thickness 45 mm.
The case file in OpenSCAD 2021.01, exploded assembly. From the top: the bezel, the TPU gasket (yellow), the shell with the cable-gland hole in its side and the screen press posts inside, and the handlebar mount with its strap slots underneath. The console line confirms the default size: 104.8 × 68.8 mm, 45 mm thick, with 10.5 mm deep screw counterbores.

Measure your screen first

ParameterWhat to measure (calipers)Default
scr_pcbModule circuit board, width × height86 × 50 mm
scr_pcb_tBoard thickness1.6 mm
scr_glass_tGlass and backlight, from the board's front surface to the glass face4.0 mm
viewLit area of the screen, minus 1 mm on every side58 × 44 mm
view_offHow far the lit area's centre is from the board's centre, seen from the front0, 0
scr_rearTallest thing on the back of the screen board (usually the pin header)11 mm
press_pts4 spots on the back of the screen board with no parts, where posts can press±39.5, ±21.5
bar_dHandlebar diameter where the unit will sit31.8 mm

Drawings

FRONT · rider side 104.8 mm 68.8 mm 58 × 44 window 45° chamfer outside dashed: clear sheet 68 × 54 PG7 USB-C BACK · bar side tap board inside (hidden) M3×30 bar axis mount 44 × 26 · strap slots dashed PG7 USB-C SECTION A–A · cut through the centre, seen from the left U1 XIAO 1 2 3 4 5 6 7 8 9 45 mm = shell 35 + gasket 1 + bezel 9 1 · Handlebar Ø31.8 + 1 mm rubber shim, two straps 2 · Mount (PETG) · 2 × M3×10 into heat-set inserts 3 · Shell · 2.4 mm back · 4 × M3×30 from behind 4 · Tap board on 5 mm standoffs, M2.5 self-tappers 5 · Press posts (beside the cut) + 1.5 mm foam 6 · Gasket · TPU 95A · 1.0 mm 7 · Touchscreen module: board + glass 8 · 1 mm clear polycarbonate in an inside pocket 9 · Bezel · 2.4 mm front · M3 heat-set inserts
Default dimensions at 3 px = 1 mm. The four case screws go in from the back into inserts in the bezel, so the front face stays smooth and has no holes for water. The USB-C port faces down, away from rain. The cable gland is on the left side as seen from the front. Press posts sit beside the tap board, which is why they appear dashed in a cut through the centre.
Plate 1 · PETG or ASA · no supports shellfloor down bezelfront face down mount plate down Plate 2 · TPU 95A gasket or cut it from 1 mm silicone sheet
Every part prints flat, without supports. The window chamfer is 45°, and the clear-sheet pocket opens towards the inside, so nothing overhangs while printing. The parts fit a 220 × 220 mm bed (Ender 3, Prusa MK4, Bambu A1/P1).

Garmin mount adapter

Instead of strapping to the bar, the head unit can click into a Garmin quarter-turn mount: the out-front and stem mounts made for Garmin Edge computers, plus the many third-party mounts built to the same shape. Print the garmin part in place of mount. It screws onto the same two M3 inserts on the case back, so you can swap between the two at any time.

Garmin doesn't publish the tab's dimensions. The defaults below come from community measurements, so print the 10-minute fit test first and adjust before printing the full adapter.

TAB · top view (6 px = 1 mm) neck Ø 20 (dashed) 27.5 tip to tip 21.5 across flats SIDE VIEW plate 6 mm · M3 heads recessed 2.6 2.4 tab mount's lips slide into the 2.6 gap CASE BACK 2 × M3 × 10 into the bar-mount inserts tab 4 mm below the case centre centre
Same screws, different mount. The adapter plate (teal) uses the bar mount's two inserts. The tab (orange) sits just below the case centre, so the unit balances on the mount and both screw heads stay reachable with a hex key. The tab's flats and tips are what the quarter turn locks under the mount's lips.
  1. Fit test: set PART = "garmin_test", render, and print it (about 10 minutes). Push it into your Garmin mount turned 90°, then twist a quarter turn.
  2. Won't go in: lower g_tab_w by 0.2 mm. Goes in but won't twist: raise g_neck_h by 0.2 mm, or lower g_tab_t. Rattles when locked: lower g_neck_h by 0.2 mm. Reprint the test after each change.
  3. Check the locked direction: mark "up" on the test disc. If "up" points sideways when locked, set g_angle = 90.
  4. Print the adapter: PART = "garmin", plate down, 6 walls and 50 % infill so the tabs are solid. The tabs overhang the neck by about 4 mm, which most printers handle without supports. If yours sags, enable supports for this part only.
  5. Fit it: remove the bar mount and screw the adapter on with the same two M3 × 10 screws.
Heavier than a Garmin

The head unit weighs roughly 250 g, about twice a Garmin Edge. Use a metal or sturdy out-front mount, tighten its bar clamp well, and add a small lanyard from the case to the bar for rough roads. Prefer a bought tab? Set g_printed = false to get a flat pad, and stick a Garmin-compatible male tab adapter to it with 3M VHB tape.

Print settings

SettingValueWhy
MaterialPETG, or ASA for full sunPLA starts softening around 60 °C. A dark case in an Australian summer gets hotter than that.
Layer height0.2 mmGood balance of strength and print time.
Walls4 perimetersSolid material around the heat-set inserts and screw holes.
Top / bottom5 layersWatertight faces.
Infill25–30 % gyroidStiff in every direction.
SupportsNoneDesigned to print flat.
AdhesionBrim for ASAASA corners tend to lift.
GasketTPU 95A, 100 % infill, slow (20 mm/s)Soft enough to seal when the screws are tightened.

Case hardware (AUD)

PartQtyNotes≈ A$
M3 heat-set inserts (≈ 5 mm long, for a 4.0 mm hole)64 in the bezel, 2 for the mount. Check the hole size against the insert's datasheet and set insert_d to match.8–12 (pack)
M3 × 30 socket-head screws, stainless4Case screws, fitted from the back.4
M3 × 10 socket-head screws, stainless2Mount to case.2
M2.5 × 6 self-tapping screws4Hold the tap board on its standoffs.3
PG7 cable gland, IP681For the pigtail. Jaycar or Altronics.3–5
1 mm clear polycarbonate sheet1Cut to 68 × 54 mm. Acrylic tends to crack when cut, so polycarbonate is better.8–12
Thin clear double-sided tape (3M VHB 0.25 mm)1Seals the window sheet to the bezel.10
1.5 mm EVA foam pads4On top of the press posts.5
Rubber shim, 1 mm (an old inner tube works)1Grip on the bar, protects the paint.0
Rubber mounting straps or 12 mm hook-and-loop2Through the strap slots and around the bar.8
Optional: Garmin-compatible out-front mount, 31.8 mm1Only for the Garmin adapter. A sturdy or metal mount is best, since the unit is heavier than a Garmin.20–60
USB-C silicone dust plug1Seals the USB port while riding.3
Subtotal, plus about 150 g of PETG and 15 g of TPU≈ 54–64

Assembly

  1. Inserts: with a soldering iron at about 230 °C, press the 4 inserts into the bezel bosses (from the back face) and the 2 inserts into the shell's mount bosses (from the inside). Let them cool before handling.
  2. Window: put a frame of VHB tape around the inside pocket, press the polycarbonate sheet in, and hold it for 30 s.
  3. Screen: lay the touchscreen module glass-down onto the sheet, centred on the window. Stick a foam pad on each press post.
  4. Tap board: screw it onto the 4 standoffs. Fit the PG7 gland in the left wall, feed the pigtail through it, and solder the pigtail to J1. Connect the screen wires to the XIAO.
  5. Close: lay the TPU gasket on the shell rim, bring the bezel down, and tighten the 4 M3 × 30 screws a little at a time in a cross pattern until the gasket is evenly squeezed.
  6. Mount: screw the mount onto the back (2 × M3 × 10), wrap the shim around the bar, and strap the unit on. Plug the USB-C port.
Making it thinner

The default is 45 mm thick because the Pololu module stands on pins (tap_parts = 13). Solder the Pololu flat, or use a low-profile buck module, and set tap_parts to 7 to save 6 mm. Trimming the screen's pin header and lowering scr_rear saves more. The file prints the new size in OpenSCAD's console when you render.

The OpenSCAD file

Install OpenSCAD 2021.01 or newer and download hardware/case/etube_case.scad from the project repository. Set PART to each part name in turn (bezel, shell, mount or garmin, gasket), press F6 to render, and export each one as an STL for your slicer. MOUNT = "garmin" shows the Garmin adapter in the assembly view.

Chapter 14Troubleshooting

SymptomLikely causeFix
E or W code on the display after connectingTap loading the bus, or a bad plugUnplug the tap and power-cycle. Check plugs are fully seated. Increase R1 to 22 kΩ.
No pulses at all on GPIO4Threshold outside the signal swingTurn RV1 slowly through its range with the scope on node A and the logic analyzer on GPIO4.
Pulses during silent gapsThreshold too close to idle, or noiseMove RV1 a little further from 1.65 V. Change R4 to 470 kΩ for more hysteresis.
Pulse widths spread widely (300–1300 ns)Slow edges or a weak signalShorten the wire from U2 to GPIO4. Check the 100 nF at U2. Use 2.2 MΩ for R4.
Signal smaller in Stage B than Stage AL1 impedance too low at 1 MHzUse 1 mH, or add a second 470 µH in series. Check its SRF is above 2 MHz.
S3 resets when BLE startsSupply dips during radio transmitFit C3 (100 µF) right at the 3V3 pin. Check the polyfuse isn't tripping.
Decoded frames differ each time for the same stateMissed edges, or a counter or CRC fieldLook at which bits change. A field that counts up steadily is a sequence number, not an error.
Hum or noise only with the laptop on its chargerGround loopRun the laptop on battery, or use the ADuM3160 isolator.

Chapter 15Glossary

E-TUBE
Shimano's wired system for Di2 shifting and STEPS e-bikes. Two wires carry both power and data.
EW-SD50
The E-TUBE cable with the small round plugs used on your bike. The newer EW-SD300 has different plugs and doesn't fit.
Junction A / B
Shimano's names for junction units. A has a button and LED (active). B is a passive split or join.
Carrier
The steady 1 MHz wave the data rides on.
BPSK
Binary phase-shift keying: data is sent by flipping the carrier upside down (180°).
DBPSK
Differential BPSK: a flip means 1 and no flip means 0, so you don't need to know the starting phase.
AC coupling
Passing a signal through a capacitor so changes get through but steady DC doesn't.
Bias
A steady voltage (here 1.65 V) that the AC signal is centred on so it fits inside 0–3.3 V.
Comparator
A chip that outputs high when + is above −, and low otherwise. It turns a wave into clean digital edges.
Hysteresis
A small gap between the switch-on and switch-off thresholds, so noise near the threshold can't make the output flicker.
Choke
An inductor that passes DC but resists fast changes, used here to keep the power supply from absorbing the data.
TVS diode
Transient-voltage suppressor. Conducts hard above its rated voltage to absorb spikes.
PTC fuse
A fuse that heats up and cuts current on overload, then resets when it cools.
RMT
The ESP32's "remote control" peripheral, which measures pulse lengths in hardware with 12.5 ns resolution.
DMA
Direct memory access: hardware copies data into RAM without the CPU's involvement.
Strapping pin
A GPIO whose level at power-up sets how the chip boots. Don't connect anything that drives it.
GATT
How Bluetooth LE organises data into services and characteristics that a phone can read or subscribe to.
LVGL
An open-source graphics library for small screens, with buttons, labels, charts and touch input.
CRC
A checksum at the end of a frame that lets the receiver detect corrupted data.

Chapter 16Sources

  1. EEVblog forum: Shimano Di2 modulation. Two-wire bus, BPSK, ~1 MHz carrier, ~500 kbit/s.
  2. Hackaday: reverse engineering Shimano bike electronics (kwakeham's Di2 wireless work).
  3. PyPI: ngorke. E-Tube related tools.
  4. Shimano: EW-EN100. Supported motors E5000/E6100/E7000/E8000.
  5. bike-components: EW-JC200. 2-port EW-SD50 junction.
  6. Rose: EW-JC304. EW-SD300 plugs, not compatible with this bike's cables.
  7. 99 Bikes: Merida eSpresso 400 S EQ. E6100 drive, SC-E5000 display, 504 Wh battery.