Radu Matei

Brașov, Romania

Telecom engineer, into cybersecurity.

I built a tracked robot for my thesis and got a 9.00. Last summer I tested power equipment at Siemens Energy. Now I'm doing a master's in Brașov.

radu@site

Move over the signal. Left and right change the frequency, up and down the amplitude.Drag sideways across the signal to change the frequency.

2.0 Hz1.0 V

--:----

in Brașov, where I live

6.0 kV
Siemens EnergySummer 2025. ClickTap the breaker.
7f3a 9c01 e2b4 CybersecurityGoIT course done, CCNA 1 in progress

My thesis robot

Camera
Electronics
Chassis
Tracks
CameraTelemetry

© 2026 Radu Matei. All rights reserved.

No cookies, no analytics, no tracking. While this tab is open, it remembers one thing: whether you have found the menu, so the hint stops. Fonts and code load from this site, so your browser contacts no one else, unless you ask the Password Helper page to check a password for leaks: then the first 5 characters of its hash go to Have I Been Pwned. The host, GitHub Pages, keeps standard server logs, including IP addresses, for security.

Type: Instrument Sans and IBM Plex Mono (SIL OFL). 3D: three.js (MIT).

Projects

Things I've built, biggest first.

02 · Desktop app

A password manager for your computer. It tells you how strong a password is, checks if it showed up in a known leak, keeps everything in an encrypted vault and shows your 2FA codes.

ElectronNode.jsAES-256Have I Been Pwned
Code on GitHub
03 · Web app

My friends and I used it to pick where to go on a trip. Four voting rounds, live flight prices, runs on my own server.

Node.jsSQLiteDocker
Full page coming soon
04 · Arduino build

A turret that loads and fires ping pong balls. You aim it with a knob, press the button, and it fires three in a row.

ArduinoC++ServosStepper motor
Code on GitHubFull page coming soon
05 · Web app · University

Bike rental for a university campus. Three small services that work together: accounts, bikes and stations, and bookings.

JavaSpring BootMicroservicesH2
Private repo · full page coming soon
06 · Lab

Virtual machines where I practice attacking and defending: Parrot OS, Metasploitable 2 and GNS3.

VMwareParrot OSGNS3
Full page coming soon
07 · Game · in progress

The card game my friends and I play, with our own house rules, so we can play it online.

PythonJavaScriptElectron
Full page coming soon

Bachelor's thesis · 2026 · graded 9.00

DataLink Rover

Pick a part to explore.

← → to choose · Enter to open

Scroll

Taken apart

Eight parts, each built to its real size from my CAD, my photos and the thesis. Keep scrolling to go through them.

01 / 08

Tracks and motors

A T101 aluminium tank kit with ABS plastic tracks. Each track has its own 33GB-520 gear motor, 12 V and 320 rpm, fed straight from the 7.4 V pack.

No steering: one track runs faster than the other, so the rover turns on the spot.

left = y + xright = y − x

02 / 08

Frame and plate

The T101 is one bent aluminium sheet: a slotted deck between the tracks, its sides bent down to carry the wheels. The motors hang under the deck.

My 3D printed chassis sits right on top of it: the battery box stands on the deck and the electronics plate rests on the box. Pins on the plate's front and corner tabs hold the five distance sensors.

03 / 08

Raspberry Pi 5, 8 GB

The rover's computer. It runs two services that start on boot: rover.py takes the commands, reads the sensors and drives, and camera_stream.py streams both cameras.

The extra memory is for the self-docking model I want to run on it next.

04 / 08

Arduino Nano and DRV8833

Linux can't promise timing, so a Nano does the real-time part. The Pi sends it short text lines over USB at 115200 baud.

M 182 -64S 90

The DRV8833 H-bridge is glued to the front of the battery box, with a 1000 µF capacitor on its supply that catches the dips when the motors start. It switches battery power to the motors, and if the lines stop for half a second, the Nano stops them on its own.

05 / 08

Distance sensors

Five VL53L0X laser time-of-flight sensors, three at the front and two at the back. They all have the same I²C address, so a TCA9548A switches them on one at a time.

Under 400 mm the rover slows down, and at 120 mm it stops. Turning stays free, so it can always get out.

06 / 08

Two cameras

Two Waveshare IMX219 cameras, 8 MP with a 120° lens, each streaming 1920 × 1080 at 30 fps.

The front one is the driving view. The other sits on a mast and turns on an MG90S servo that the right stick controls. The tower and the camera mount are parts I designed.

07 / 08

Power

Four Samsung 21700 cells in 2S2P: 7.2 V, 10 Ah, about 72 Wh, roughly four hours of driving. Two double holders slide into the battery box from the Pi's side. There's no power switch: to turn it off, I pull the holders out.

A 2S BMS protects the cells and a USB-C module under the Nano shield charges them. Under the plate, an XL4015 makes 5.1 V for the Pi; a second one on top makes 3.3 V for the fibre converter. The motors run straight off the pack, and an INA219 measures the battery.

08 / 08

Fibre link

The backup path. A media converter turns the Pi's Ethernet into light on a single strand of single-mode fibre: 1310 nm one way, 1550 nm the other, up to 3 km.

The converter has its own 3.3 V supply, so the backup path doesn't depend on the Pi's. And light in glass can't be jammed by radio.

Ready to go

On power-up the camera runs the servo check from rover.py: full left, full right, centre. Then it drives off.

The code

Twenty times a second, rover.py takes the newest command, limits it near obstacles and turns it into two track speeds.

rover.py · control loop
def axis_limit(dist_mm, stop=120, slow=400):
    """1.0 = free, 0.0 = blocked, linear in between."""
    if dist_mm is None:  return 1.0     # a dead sensor never blocks driving
    if dist_mm <= stop:  return 0.0
    if dist_mm >= slow:  return 1.0
    return (dist_mm - stop) / float(slow - stop)

# collision limit on the drive axis only; turning stays free
if y > 0:   y *= axis_limit(min_valid([tof[i] for i in FRONT_IDX]))
elif y < 0: y *= axis_limit(min_valid([tof[i] for i in REAR_IDX]))
l = max(-1.0, min(1.0, y + x))   # tank mixing
r = max(-1.0, min(1.0, y - x))
nano.send(f'M {int(l * 255 * TRIM_L)} {int(r * 255 * TRIM_R)}')

The Nano only has to understand two kinds of line.

rover.ino · Arduino Nano
void handleLine(char *line) {
  if (line[0] == 'M') {                // M <left> <right>, -255..255
    int l = 0, r = 0;
    if (sscanf(line + 1, "%d %d", &l, &r) == 2) {
      setMotor(PIN_L_IN1, PIN_L_IN2, l);
      setMotor(PIN_R_IN1, PIN_R_IN2, r);
    }
  } else if (line[0] == 'S') {         // S <angle>
    int a = 0;
    if (sscanf(line + 1, "%d", &a) == 1) steer.write(a);
  }
}

Photos

The rover's printed plate on the T101 chassis with the Pi 5, the Nano, the buck converter, the servo mast and the fibre converter
The electronics plate on the T101, from the front.
The rover's plate seen from above
From above: Pi 5, Nano, XL4015, TCA9548A and the fibre converter.
Drive test videocoming soon

Straight from the CAD file

This is the real model, every part where it sits. Keep scrolling to go through them.

01 / 08

Housing

The frame and the two grips, 3D printed in black PLA. I worked out where everything goes; the CAD modelling was done together with a friend.

02 / 08

Screen

A 7-inch 800 × 480 capacitive touch screen: HDMI for the picture, USB for touch.

It runs my control interface, built in Electron: both cameras, both batteries and a map of nearby obstacles.

03 / 08

Joysticks

The left stick drives, the right one turns the rover's camera. The Pi has no analog inputs, so an ADS1015 reads all four axes over I²C.

A six-step calibration measures each stick's real forward and turn directions, so a crooked stick still drives straight.

04 / 08

Power switch

A small green MTS-102 toggle on the top edge turns the controller on and off.

Three solder lugs inside, an M6 bushing through the wall and a nut on the outside.

05 / 08

Raspberry Pi 5, 4 GB

The controller is the Wi-Fi access point the rover joins. Its Pi reads the sticks, sends every command 20 times a second over both links, and shows the two video streams.

It sits upside down under the screen. A 30 mm fan under it blows through a window in the back cover.

06 / 08

Power and sensing

A printed frame hangs off the Pi's mounting holes. It carries the 30 mm fan and the XL4015 that makes 5.1 V for the Pi, so one fan cools both.

A second XL4015 makes 3.3 V for the fibre converter. A 2S BMS protects the cells, and an INA219 measures the battery for the on-screen gauge.

07 / 08

Batteries

Four Samsung INR21700-50S cells, two in each grip, in 2S2P: 7.2 V and 10 Ah, about 72 Wh. That's roughly four hours.

08 / 08

Covers

The middle cover holds the Pi in its middle and the small boards in its two side pockets. The bottom cover closes everything up, with a window for the fan.

Switched on

The screen wakes up with my interface: the mast camera full screen, the front camera in the corner, both batteries and the obstacle map.

The code

The calibration in monitor.py: each reading is projected onto the stick's own measured directions.

monitor.py · joystick calibration
def calibrate_xy(ax, ay, which):
    c = joycal[which]
    dx, dy = ax - c['cx'], ay - c['cy']
    fp = dx * c['fwd_unit'][0]  + dy * c['fwd_unit'][1]    # onto "forward"
    tp = dx * c['turn_unit'][0] + dy * c['turn_unit'][1]   # onto "turn"
    y = max(-1.0, min(1.0, fp / c['fwd_scale']))
    x = max(-1.0, min(1.0, tp / c['turn_scale']))
    return _deadzone(x, c['deadzone']), _deadzone(y, c['deadzone'])

Photos

The finished controller from the front: screen, two joysticks and the green cells in the grips
Finished, from the front.
The controller from the back: the screen's board, the cover with the fan and the HDMI cable
The back, with the cooling fan.
The controller held in one hand
In the hand.
The controller's Pi 5 and wiring inside the printed case
Inside, during assembly.
The control interface: live video, picture in picture, batteries and proximity map
The interface, before a camera is connected.
The system monitor window showing the controller, the rover and the dock
The system monitor for all three Pis.
01 / 07

Your thumbs

The left stick drives and the right one turns the camera. An ADS1015 converts their four analog axes into numbers the Pi can read.

02 / 07

Calibrated, then packed

The Pi corrects each reading with the stick's calibration and packs both sticks into a tiny message with a timestamp.

{"m":"dlr1","t":1749…,"j1":{"x":0.42,"y":0.80},"j2":{"x":-0.1,"y":0}}

03 / 07

Two roads at once

Every 50 ms the same packet goes out on both links: Wi-Fi on 2.4 GHz, where the controller is the access point, and a single fibre through the two media converters.

It's UDP: a late command isn't worth resending, the next one is already on its way.

04 / 07

First copy wins

Both copies reach the rover. It keeps whichever arrives first and drops the twin with the same timestamp.

05 / 07

Safety, then the motors

The distance sensors cap the speed near obstacles, tank mixing makes two track speeds, and the Nano drives the motors and the camera servo.

No command for 0.4 s and the motors stop.

06 / 07

And back

Ten times a second the rover sends its distances, battery, servo angle and failsafe state, also on both links. Both cameras stream video to the screen.

The controller always shows the freshest data.

07 / 07

Jam the radio

For the thesis demo I jammed the Wi-Fi band. The radio packets stopped, and after 1.5 s of silence the controller switched to the fibre on its own.

The data kept coming the whole time.

Controller to rover

That's the whole trip, 20 times a second while you drive.

20 Hz commands10 Hz telemetry1.5 s failover

The code

Three small pieces make the link redundant. The controller sends every command on both links.

monitor.py · controller
pkt = json.dumps({'m': 'dlr1', 't': int(time.time() * 1000),
                  'j1': {'x': j1['x'], 'y': j1['y'], 'ok': j1['available']},
                  'j2': {'x': j2['x'], 'y': j2['y'], 'ok': j2['available']}}).encode()
for ip in ROVER_IPS:                       # Wi-Fi first, then fibre
    _cmd_sock.sendto(pkt, (ip, CMD_PORT))

The rover keeps the first copy and drops the late twin.

rover.py · rover
t = int(d.get('t', 0))
if t <= last and (last - t) < 60000:   # same or older: it came by the other link
    continue
last = t

The controller listens to both links and uses Wi-Fi while it's fresh.

monitor.py · picking the link
chosen = None
for ip in ROVER_IPS:                       # Wi-Fi wins while it is fresh
    entry = _tel_last.get(ip)
    if entry and (now - entry[0]) <= TEL_TIMEOUT:   # 1.5 s
        chosen = ip
        break                                  # otherwise the fibre takes over

In numbers

20 Hzcommands, on both links
10 Hztelemetry back
1.5 sof Wi-Fi silence before the fibre takes over
0.4 swithout a command and the motors stop
2 × 1080p30MJPEG video streams
3 kmreach of the fibre converters

02 · Desktop app · Electron

Password Helper

A password manager for your computer, and the strength checker it grew out of. The checker works right here: it runs in this page, and nothing you type is stored or sent.

Nothing to check yetType a password, or try one of these.

The rules

  • At least 15 characters
  • Hard to guess: no common words, names or patterns
  • Not in known breaches (not checked yet)

From NIST SP 800-63B-4: length and not being guessable are what count. Rules like “add a symbol” are out.

Time to crack

Offline, slow hash
—
Offline, fast hash
—
Online, no limits
—
Online, rate-limited
—

What an attacker tries first

Patterns show up here as you type.

Has it leaked?

Have I Been Pwned knows close to a billion leaked passwords. The app asks it without ever telling it the password: only the first 5 characters of the password’s SHA-1 hash leave. Back comes every leaked hash that starts the same way, and the match is made here.

SHA-1 of your password

········································

sentnever leaves this page

This pagehashes, compares
Have I Been Pwnedapi.pwnedpasswords.com

Type a password at the top first.

Those 5 characters go to Have I Been Pwned only when you press the button, and the page asks for a padded answer so its size gives nothing away. Nothing is stored.

Make a better one

Random characters, or a passphrase of random words from the EFF list. Every pick comes from the browser’s secure random generator, drawn so that no character is likelier than another.

— bits of entropy ·

The vault

One master password protects everything, and it is never stored. It goes through PBKDF2 with SHA-256, 600,000 times over, to make a 256-bit key; the key seals the vault with AES-GCM. Try the same steps on a note of your own.

Salt
IV
Locked note

Verification codes

The app keeps 2FA codes next to the password they protect. Each code comes from a secret key and the time (TOTP, RFC 6238) and changes every 30 seconds. This one is live: add the key below to any authenticator app and it will show the same code.

······

Demo key JBSW Y3DP EHPK 3PXP · SHA-1 · 6 digits · 30 s

In the app the secret never reaches the window you see; only the current code does.

Locked down

An app that holds passwords has to assume the worst about everything around it. Each layer below is there for one way things could go wrong.

Sandboxed windowThe interface runs with no Node.js and context isolation, and can only call a small, checked set of functions.
No network from the pageIts Content-Security-Policy allows no outside connections at all; fonts and icons ship with the app.
Secrets stay in the main processThe key, the decrypted vault and the 2FA secrets live there; the window gets one password at a time, hidden again after 30 s.
Clipboard wipedCopying goes straight from the main process to the clipboard, which is cleared after 30 seconds.
Auto-lockAfter inactivity, on sleep, optionally on minimise, and growing delays after wrong master passwords.
Navigation lockedNo leaving the app, no new windows, no camera or notification prompts. Links open in your browser.

In numbers

600,000PBKDF2-SHA256 rounds per unlock
5 of 40hash characters ever sent for a leak check
15+characters, the NIST minimum it checks for
EN + ROdictionaries in the strength check
7,776words to build passphrases from
30 sbefore a copied password is wiped

How it got here

  1. 1.0A PyQt6 strength checker for the desktop.
  2. ElectronRebuilt to hold passwords: an encrypted vault, a hardened window, tests.
  3. 2.0A Mac-style window, 2FA codes, imports from browsers, Recently Deleted.
  4. NextA self-hosted sync server that only ever sees encrypted data.
Code on GitHub

Record

Where I've been and what I picked up along the way.

Siemens Energy

Internship, PRAM department, Petrobrazi refinery.

UniTBV · ETTI

B.Eng. Telecommunications, Brașov.

Certifications

[Add yours, each with a verification link.]

About

Off the clock: competitive tennis, F1 and games.

Tennis

Competitive player. The serve animation lands here.

F1

Race-engineer brain, sofa on Sundays.

Games

[Your current rotation]

Contact

Open a channel. I read everything.

Email

[your email]

LinkedIn

[linkedin.com/in/…]

GitHub

[github.com/…]