I'm a Hacker in its true meaning — simply put, a computer technology enthusiast. My primary focus is on backend engineering in all its forms. Since 2017, Golang has been my go-to language, and I continue to use it extensively today.
Feel free to check out my CV: Download PDF (from january 2022)
I began my professional journey in 2012, and since then, I have solved countless business challenges using a variety of technologies, including:
JavaScript and Node.js: Developing high-performance backends and dynamic web applications.
C++: Creating high-load backends and efficient applications for embedded systems.
GNU/Linux: Setting up and managing servers, along with OS-level software configuration using Ansible.
Databases: Storing vast amounts of data in MongoDB and various SQL databases.
ClickHouse: Collecting millions of metrics and clicks, processed through Kafka or RabbitMQ.
Kubernetes: Running and managing a variety of setups, from bare-metal to EKS.
Helm: Deploying numerous Helm charts, both with and without Tiller.
Cloud Platforms: Saving costs with well-planned configurations in GCP and AWS.
CI/CD: Automating repetitive tasks using Jenkins and GitHub Actions.
i have a small toy quadcopter. it was not bad, but the original plastic frame had too many unnecessary parts and the flight time was not very interesting.
so i decided to use it as a small hardware experiment.
i removed the original frame and other plastic parts, designed my own frame, and printed it. after putting the electronics, motors, propellers, and the original battery back onto the new frame, the total weight became:
47g
the original toy quadcopter was:
59g
so the custom frame version saved:
59g - 47g = 12g
for such a small drone, 12g is a lot.
after that the next obvious question was: if the drone is now lighter, can i use the saved weight for a bigger battery?
original battery
the original battery is marked as:
HPY 752035
3.7V
380mAh
1.41Wh
it is a 1s lipo battery. 3.7v is the nominal voltage, not the full charge voltage.
for a 1s lipo:
nominal voltage: 3.7V
full charge voltage: 4.20V
the test document for this battery lists it as a polymer lithium-ion battery with:
this is useful because it shows that the original battery is not just a random small lithium cell. it is a high-rate lipo made for a drone.
charging it from a laboratory power supply
before changing the battery, i wanted to understand how to charge the original one safely.
with a laboratory power supply, the correct mode is cc/cv:
cc = constant current
cv = constant voltage
the settings are:
voltage: 4.20V
current limit: chosen charging current
for a slow and safe charge, the standard current from the test document is:
76mA
but i first used:
200mA
for a 380mah battery this is:
200mA / 380mAh = 0.53C
so 200ma is not extreme. it is more than the standard current, but far below the listed maximum.
then i also thought about 500ma:
500mA / 380mAh = 1.3C
this is still below the documented max charge current, but i would not use it as my default with a lab power supply. the battery may support it, but fast charging is not the same as gentle charging.
my practical charging settings are:
4.20V
200mA current limit
stop when current drops to around 8-10mA
the important part is not to go above 4.20v.
with a lab power supply there is no smart lipo charger logic. it will not really "finish" the charge for me. i need to stop it when the voltage is at 4.20v and the current has fallen close to the cutoff current.
why the battery current rating is so high
the max charging current in the document looks very high:
1900mA
for a 380mah battery this is:
1900mA / 380mAh = 5C
and the max discharge current is even higher:
7600mA / 380mAh = 20C
this makes sense for a drone battery.
small quadcopters need short bursts of high current. motors do not behave like a small led or a microcontroller board. when the drone climbs, corrects position, or recovers from movement, the motors can pull several amps.
that is also why replacing the battery with an ordinary li-ion cell is a bad idea.
a normal li-ion cell may have the same voltage and even more capacity, but if it cannot provide enough current, the voltage will sag. the drone may become weak, reset the controller, trigger low battery mode early, or overheat the cell.
so for this experiment i only considered 1s lipo drone batteries.
weight after the custom frame
the rebuilt drone weighs:
47g with original battery
the original battery weighs:
9.60g
so the drone without battery is:
47g - 9.60g = 37.4g
this number is the base for all battery calculations.
custom frame without battery: 37.4g
now i can compare battery options.
option 1: 1600mah battery
the first tempting option was a much larger battery:
1600mAh
43g
the total weight would become:
37.4g + 43g = 80.4g
so the drone would be:
80.4g total
compared to the original toy quadcopter:
80.4g - 59g = 21.4g heavier
that is about:
21.4g / 59g = 36% heavier than stock
compared to my custom frame with the original battery:
80.4g - 47g = 33.4g heavier
that is about:
33.4g / 47g = 71% heavier
the battery capacity increase looks great:
1600mAh / 380mAh = 4.2x
but this does not mean the drone will fly 4.2 times longer.
the battery also becomes more than half of the whole aircraft weight:
43g / 80.4g = 53%
at this point the drone becomes almost a flying battery with motors attached.
it may still fly, but the motors will need much more throttle just to hover. the drone will probably become less responsive, and the motors may become hot much faster.
i would test this battery only carefully:
mount it exactly in the center
hover low over something soft
fly for 20-30 seconds
land and check motor temperature
stop if the motors are hot or if hover needs too much throttle
if it hovers at 70-80% throttle, this setup is not good. it means there is not enough thrust reserve.
so the 1600mah battery is interesting, but too extreme for this drone.
option 2: 650mah battery
the second option is much more reasonable:
650mAh
18g
the total weight would become:
37.4g + 18g = 55.4g
this is a very different result.
compared to the original toy quadcopter:
59g - 55.4g = 3.6g lighter
so even with a bigger battery, the drone is still lighter than it was originally.
compared to my custom frame with the original battery:
55.4g - 47g = 8.4g heavier
this is a much smaller penalty.
the capacity increase is:
650mAh / 380mAh = 1.7x
this is not as impressive on paper as 1600mah, but it is much more balanced. the drone should still have a normal thrust-to-weight ratio, and the motors should not suffer as much.
this is exactly the kind of tradeoff that makes sense after rebuilding the frame: use the saved weight for a battery that is larger, but not absurdly larger.
expected flight time
the original battery has:
380mAh
the better replacement candidate has:
650mAh
that is:
1.7x more capacity
because the total drone weight with the 650mah battery is still lower than the original stock weight, the flight time increase may be close to the capacity increase.
if the original flight time was around 5 minutes, i would expect something like:
7-8 minutes
maybe a little more, depending on the motors, propellers, battery quality, and flight style.
with the 1600mah battery the stored energy is much higher, but the extra weight may destroy much of the benefit. the flight time may increase, but the drone will probably become heavy and inefficient.
final comparison
the numbers are:
original toy quadcopter:
59g total
custom printed frame with original battery:
47g total
custom frame without battery:
37.4g
original battery:
380mAh
9.60g
big battery option:
1600mAh
43g
80.4g total
balanced battery option:
650mAh
18g
55.4g total
the 1600mah option gives:
4.2x capacity
but 80.4g total weight
the 650mah option gives:
1.7x capacity
and 55.4g total weight
for this drone, the 650mah battery is the better engineering choice.
conclusion
after rebuilding the quadcopter with my own 3d-printed frame, i saved enough weight to use a better battery.
but the best battery is not the biggest one.
the 1600mah pack is too heavy for this size of drone. it may fly, but it will likely make the quadcopter slow, inefficient, and hard on the motors.
the 650mah pack looks like the sweet spot. it gives noticeably more energy while keeping the total weight below the original stock weight.
so the plan is:
use a 1s 3.7v lipo
keep full charge voltage at 4.20v
check connector polarity
avoid random low-current li-ion cells
use a drone/high-rate lipo
test motor temperature after short flights
choose 650mah instead of 1600mah
this is the nice part of small hardware projects. the final answer is not hidden in a datasheet only. it appears when the numbers, the weight scale, and the real object on the table all agree with each other.
i used to connect my vinyl record player to a speaker manually when i want to listen records.
it was annoying and not practical to touch wires every time, so i decided to use my headless linux server that surprisignly stay nearby as a network audio bridge and listen records through my studio PC.
this article describes how i built an automated analog-to-network pipeline using pipewire, vban, and wireplumber.
hardware setup
the physical setup was straightforward. i connected the audio output of the record player to the line-in jack on the server’s motherboard.
on the linux side i added my user to the audio group to allow access to the sound device and used alsamixer to unmute the capture channel and enable the line-in input.
at this point the server could capture audio from the record player.
phase 1: proof of concept
before automating the entire pipeline, i first confirmed that audio could be transmitted over the network with low latency.
for this experiment i used pipewire together with the vban network protocol.
i created the following configuration file:
~/.config/pipewire/pipewire.conf.d/vban-send.conf
this file loads the libpipewire-module-vban-send module and configures it to send audio packets to my windows pc on port 6980.
this configuration creates a pipewire sink named vban-linein-send. any audio routed into this sink is transmitted as a vban stream.
to test the connection i played an audio file directly into the new sink:
pw-play --target=vban-linein-send test.wav
on my windows pc i opened voicemeeter banana, enabled the vban receiver, and configured it to listen for the stream named LineInStream.
the test audio file played through the speakers immediately, confirming that the network connection worked. voicemeeter project is awesome, support them.
understanding the audio graph
pipewire represents audio routing as a graph of connected nodes. once the capture device and the vban sender are active, the signal path looks like this:
vinyl player
│
▼
motherboard line-in
(alsa capture node)
│
▼
pipewire audio graph
│
▼
vban send node
(libpipewire-module-vban-send)
│
▼
udp network stream (through a home network)
│
▼
windows pc (voicemeeter vban receiver)
│
▼
hi-fi speakers
the remaining task was to connect the capture node to the vban node automatically during startup.
phase 2: creating a persistent setup
the goal was to make the system operate automatically. if server reboots, it should capture audio from the line-in port and stream it to the windows machine without manual intervention.
first, i enabled systemd lingering so that pipewire and wireplumber could run without an active login session:
loginctl enable-linger your-username
next, i wrote a wireplumber lua script using objectmanager. the script detects the alsa capture ports and links them to the vban sender ports whenever both are present in the pipewire graph.
while implementing this persistent configuration i encountered two issues.
hurdle 1: line-in gain reset
during the first full test the audio sounded distorted.
the cause was the default line-in gain configured by the motherboard codec. the capture boost was too high for the signal coming from the record player.
lowering the gain in alsamixer removed the distortion. however, after each reboot the gain returned to its original value.
this happens because modern pipewire systems are managed by wireplumber, which applies its own mixer state during startup and overrides alsa settings.
the correct approach was to configure the volume through wireplumber.
first i located the capture device:
wpctl status
then i set the capture level:
wpctl set-volume 48 0.10
wireplumber stores this setting in its state database and reapplies it automatically after each restart.
hurdle 2: the script was not loaded
after solving the volume issue, i placed my lua script in:
this fragment instructs wireplumber to load the script during startup.
after rebooting the server the audio was still silent. investigation showed that the script was not being loaded.
the reason is related to wireplumber’s configuration hierarchy. if the file wireplumber.conf does not exist in the user configuration directory, wireplumber uses the system configuration in /usr/share/wireplumber and ignores user configuration fragments.
the solution was to copy the base configuration file:
once this file existed in the user configuration directory, wireplumber detected the custom fragment and loaded the lua script correctly.
final result
after resolving these issues the system operates automatically.
when the server starts:
pipewire launches in the background
wireplumber restores the capture volume
the vban module initializes
the lua script links the line-in capture ports to the vban sender
from that moment the server continuously transmits the audio signal from the record player to the windows pc in my studio. in voicemeeter i can then route the incoming vinyl stream to any output device, such as the sound bar, the studio hi-fi speakers, or headphones.
i bought a netac portable ssd (1tb) to use for backups. i use this drive with my linux computer and my samsung android phone, so i formatted it as exfat using the phone.
at first, the drive was very fast. i could copy large files quickly on both devices.
but recently, the speed on linux became very slow. a 300gb backup file that i created months ago now took 3 days to read. the speed was not just slow; it was unstable. the read speed would start normally, then drop to 0 mb/s for several seconds, then start again, and then stop again. it kept freezing and restarting.
i thought the drive was broken, but the problem was actually how the software handles "garbage collection" on the drive. here is how i found the problem and fixed it.
the symptoms
the drive (vendor id 0dd8, product id 2320) connected correctly, but reading files was very difficult.
before: fast reads and writes.
now: reading data (especially small blocks) was very slow.
the main problem: the speed would stop completely (0 mb/s) many times. the drive remained connected, but it paused working.
context: i used the drive with my android phone for months. android writes files correctly, but it does not clean up deleted files on external usb drives.
finding the problem
1. testing the drive
i used a tool called f3probe to test the drive. this tool usually checks for fake drives, but it also measures speed. the results showed the problem:
average write time: 201µs (normal is ~1-10µs)
probe time: 14 minutes (should be less than 10 seconds)
the write time was 200 times slower than normal.
here is why: the ssd was full of "garbage" data. because i used it with my phone for months, i wrote and deleted many files. but android does not send a command called trim to external drives. trim tells the drive which data is deleted and can be erased. without trim, the drive thinks it is 100% full of valid data.
when i tried to read files on linux, the drive's internal controller had to search through all this old data. it became overloaded, paused all work to organize its memory (causing the drop to 0 mb/s), and then started again.
2. the solution: trim
to fix this, the computer must send the trim (or scsi unmap) command. this command cleans the drive.
i tried to run the trim command on linux:
sudo fstrim -v /mnt/usb
# fstrim: the discard operation is not supported
the error message said "not supported." the usb chip inside the netac enclosure was reporting incorrect information to linux. it said it could not do trim, even though the ssd inside could.
the fix: force the unmap command
i had to force linux to ignore the usb chip's report and send the trim command anyway.
step 1: force the setting
i found the device setting in the system files and changed it to unmap.
(note: replace /dev/sda with your correct device name)
# find the specific id for the disk
ls /sys/block/sda/device/scsi_disk/
# output example: 6:0:0:0
# force 'unmap' mode
echo "unmap" | sudo tee /sys/class/scsi_disk/6:0:0:0/provisioning_mode
there is no official linux/arm64 quay image at the moment
Yes, quay is being built for linux/ppc64le in addition to a «default» linux/amd64 one.
ok, solution
I've started building a multiplatform quay docker image that supports both linux/amd64 and linux/arm64. Nothing custom but just an image. This https://do.cr.tokarch.uk/ runs natively on arm64 now.
RedHat provides several ISO images that let you install a system. They are DD compatible and one can flash an ISO into USB drive by using either DD or Fedora Media Writer.
If you are lucky enough you may boot this USB in a BIOS-legacy mode, but not in the EFI mode. The installation also continues in BIOS-legacy and doesn't create any EFI-compatible partitions. No secure boot at all.
For those who care about UEFI in their system there are no structured information on RedHat docs.
see into an image
An official ISO has a both isolinux bootloader (bios-mode) and grub (efi-mode). The latter one disappear when you DD the image to USB drive. It may be just because of some misconfiguration during the ISO creating process. I tried to repack an ISO but had no luck about EFI mode.
efi-mode easy way
EFI boot was desinged to be very simple. There are no hidden magic stuff (almost) behind the bootloader. EFI-enabled partitions is just a VFAT-formatted partition with a custom PART-GUID. There are some limitations about the size and some others between different platforms, but let's keep it simple for now. EFI partitions has type code EF00 and PART-GUID C12A7328-F81F-11D2-BA4B-00A0C93EC93B.
GPT fdisk (gdisk) version 1.0.9
Partition table scan:
MBR: protective
BSD: not present
APM: not present
GPT: present
Found valid GPT with protective MBR; using GPT.
Command (? for help): p
Disk disk.raw: 3104768 sectors, 1.5 GiB
Sector size (logical): 512 bytes
Disk identifier (GUID): 2A2619EB-3FA6-4C34-A716-1DCA94AD43B7
Partition table holds up to 128 entries
Main partition table begins at sector 2 and ends at sector 33
First usable sector is 34, last usable sector is 3104734
Partitions will be aligned on 2048-sector boundaries
Total free space is 4029 sectors (2.0 MiB)
Number Start (sector) End (sector) Size Code Name
1 2048 3102719 1.5 GiB EF00 EFI system partition
Command (? for help): x
Expert command (? for help): i
Using 1
Partition GUID code: C12A7328-F81F-11D2-BA4B-00A0C93EC93B (EFI system partition)
Partition unique GUID: 2A1143B9-AFE9-48CE-8B47-21535F031770
First sector: 2048 (at 1024.0 KiB)
Last sector: 3102719 (at 1.5 GiB)
Partition size: 3100672 sectors (1.5 GiB)
Attribute flags: 0000000000000000
Partition name: 'EFI system partition'
gdisk util makes everything simple. To prepare an EFI-compatible partition you need to set the type to EF00 during the creating process, then PART-GUID will be filled automatically:
Command (? for help): n
Partition number (1-128, default 1):
First sector (34-3104734, default = 2048) or {+-}size{KMGTP}:
Last sector (2048-3104734, default = 3102719) or {+-}size{KMGTP}:
Current type is 8300 (Linux filesystem)
Hex code or GUID (L to show codes, Enter = 8300): ef00
Changed type of partition to 'EFI system partition'
almost it
Let's mount everything we need to make it work. First, locate an RedHat installation ISO, e.g. rhel-baseos-9.0-x86_64-boot.iso. Then locate a newly created EFI partition, e.g. /dev/sdc1.
$ realpath rhel-baseos-9.0-x86_64-boot.iso
/home/mainnika/rhel-baseos-9.0-x86_64-boot.iso
$ stat /dev/sdc1
File: /dev/sdc1
EFI partition needs to be formatted first, please notice a label argument -n RHEL9. This is necessary for the bootloader to find a boot root partition by label.
For some weird reason there is an invalid bootloader in EFI folder, BOOTX64.EFI. Let's replace it with grub which is right there as well and remove some of leftovers.
The most important step is to change a grub.cfg and let him use right paths and kernel. You might see here the label we've used during formatting.
# sed -i 's/RHEL-9-0-0-BaseOS-x86_64/RHEL9/g' /tmp/tmp.A7IJSAwhHy-efi-mount/EFI/BOOT/grub.cfg
# sed -i 's/images\/pxeboot/isolinux/g' /tmp/tmp.A7IJSAwhHy-efi-mount/EFI/BOOT/grub.cfg
The last step is to copy installation files from ISO media to EFI partition.