How to pass host device to podman container in user mode using podman-compose

One of the main ideas of containerization is a way to achieve repeatable, hassle-free results. So, when I see headlines that podman gives the same experience and supports docker-compose like deployments (or even provides socket for native docker-compose), I expect that I can grab someone else’s docker-compose file, run $ podman-compose up and enjoy working installation of something. But it’s a trap.

In theory, if you leave service state management out of the equation, it works, but only if the payload doesn’t need access to devices, privileged ports, and you don’t need to organize interpods file sharing or sharing files between the host and pods.

Nevertheless, from time to time when I install a new host with the intention to use containers, I give podman a try as the “new shiny better” alternative to docker. I allocate time (like one evening), and if I’m unable to make it work as I need, I purge it and fall back to docker.

The last time I wanted to run prind in podman, my idea was:

  1. Create separate user
  2. Add it to dialout and video groups on the host system
  3. Run services in user-mode with podman-compose

Looks simple, right? But not in the case of podman.
First of all, device pass-thru doesn’t work in user mode, so if you want to use podman’s --device option, or device: section of compose file, you have to run it under root, which immediately wipes out a major part of podman’s advantages.
Second, podman uses user namespaces, uids:gids which  you see in a container not the same as in a host system. Because of that you may have user root in container which started by regular user, but in fact uids:gids will be offset to a host system by values configured in /etc/{subuid,subgid}, which means that even you have video group in the container, it will be different from the video group that the host system user may be a member of.
Because of that, my first two steps of the plan don’t work out of the box.

To counter that, quite recently, podman has an elegant and beautiful solution: you should add a group named keep-groups to the container with the --group-add option. So, here it is? You just add the group keep-groups to the groups section of the compose file? End of the story? Everything works as expected and everyone is happy? Nope!

For some reason it doesn’t work (at least in debian’s podman-compose 1.0.6-1~bpo12+1). But before this masterpiece was born, the same effect may be achieved with --annotation run.oci.keep_original_groups=1 and it works with compose. You need to mount volume /dev and add the next section:

- /dev:/dev
run.oci.keep_original_groups: 1

It makes groups looks weird and counter-intuitive in containers and forbids you adding new groups with `groups:` section, but it’s better than nothing:

print3d@aurora:~$ id
uid=1001(print3d) gid=1001(print3d) groups=1001(print3d),20(dialout),44(video),100(users)
print3d@aurora:~$ podman run --group-add keep-groups -v /dev:/dev -it m5p3nc3r/v4l-utils
Trying to pull
Getting image source signatures
Copying blob 366c4c59e228 done
Copying blob 5843afab3874 done
Copying config 13e86697f5 done
Writing manifest to image destination
Storing signatures
/ # id
uid=0(root) gid=0(root) groups=65534(nobody),65534(nobody),65534(nobody),0(root)
/ # ls -l /dev/{ttyUSB1,video0}
ls: /dev/{ttyUSB1,video0}: No such file or directory
/ # ls -ln /dev/ttyUSB1
crw-rw---- 1 65534 65534 188, 1 Dec 26 15:26 /dev/ttyUSB1
/ # ls -ln /dev/video0
crw-rw---- 1 65534 65534 81, 0 Dec 17 15:01 /dev/video0
/ # v4l2-ctl --list-devices
UVC Camera (046d:0825) (usb-0000:04:00.3-5):

The story of how I spent the evening enableing TMC2208 spreadCycle on Creality 1.1.5 board

I have ender 5 which come with creality 1.1.5 board with one little surprise, Marlin’s linear advance doesn’t work on it (klipper seems not to be happy too).
The reason is TMC2208 drivers which are in default stealthChop mode which doesn’t work well with rapid speed and direction changes.

TMC2208 is highly configurable in comparison to old drivers like A4988, but it utilizes half-duplex serial interface. Also it has default configuration stored in OTP (one time programming memory) which again may be changed via serial interface. So, here is two options, connect TMC2208 to onboard microcontroller  and let Marlin/Klipper to configure TMC2208 or change OTP.
It’s not so easy to find spare pin on this board (at least I thought so), so I decided to change OTP register.

Serial interface is exposed on PIN14 (PDN_UART) of TMC2208 chip:
TMC2208 package

On popular stepstick type drivers which looks like this:

This pin is exposed and easily available, but it’s not the case. On Creality 1.1.5 board these drivers integrated.
I didn’t found the schematic for revision 1.1.5, but I’ve found PCB view of older revision. I’ve visually compared traces, vias, elements and designates around driver and found them very similar if not the same.

There is PCB view of extruder’s driver:

Extruder's driver

And there is a photo of board I have:

Creality 1.1.5 board

The needed PIN 14 is connected to PIN12 and 10K pull-up resistor.

To change OTP register I needed half-duplex serial and I had three most obvious options out of my head:

  • Use usb to serial adapter and join TX and RX lines
  • Use separate controller and do bing bang thing
  • Use onboard controller and just upload an arduino sketch to do the same (or even use TMC2208Stepper lib to just write OTP register)

I had no spare arduino around and wasn’t sure that will be able to get access to Marlin’s calibration stored in EEPROM and decided to use the first option (it didn’t work well and here is few different reason why which I will write at the end).

First you need ScriptCommunicator to send commands to TMC2208 from there:
Next, you need to get TMC2208.scez bundle from there:
Download them somewhere, they will be used later.

The solution for making half-duplex from usb to serial adapter which is in top of google result looks like that:

And here is my initial implementation:

Half-duplex implementation
Resistor is just pushed into headers which are connected to RX and TX, only wire connected to RX is used to communicate with TMC2208.
My first idea was to solder wire to R24 (I need to enable spreadCycle only for extruder’s driver) and use usb to serial adapter like this:

1st attempt to solder wire directly to R24

The whole construction (5V and GND were connected to ISP header’s pins 2 and 6 respectively):
FTDI to Creality board connection

When everything ready, there is time to open TMC2208.scez, I used the version for linux, so for me it was command like:

/PATH/TO/ /PATH/TO/TMC2208.scez

But unfortunately it didn’t work. Each time I hit connect button I got a message “Sending failed. Check hardware connection and serial port.” First I tried to lower connection speed (TMC2208 automatically detects baudrate, 115200 was configured in TMC2208.scez), but without positive result. Next I was checking all the connections between FTDI, resistor and TMC chip – no success. Un-pluging VCC from FTDI and powering board with external PSU – no connection.

I started to think what can went wrong, the fact that old  board revision for A4988 drivers looks pretty similar made me think that creality just put new chip in place of old one and here is obvious candidate INDEX PIN(12) which is connected to PDN. According to datasheet  INDEX is digital output, so if it is push-pull, it will definitely mess with serial communication. Only option to fix it is to cut trace between them and solder wire directly to PDN. Luckily it’s just two layer board, so needed trace can be easily located on the back side:

Cut like that:

Back cut PDN to INDEX trace

And solder wire. Wire should be thin and soft otherwise there is a risk to peal off trace completely. Also it’s worth to check that here is no connectivity between wire and R24 after soldering:

Back of the board, wire soldered to PDN

I thought that I would finally be able to configure TMC, but to my surprise only change I observed was an checksum error message which I got time to time instead of “Sending failed”.
It was around 1:30 after midnight and I almost gave up, when recalled in the very last moment that I have CH341 based programmer. I give it a try and finally it worked:

Configurator finally connected
Only additional change I made, I powered board from external supply, because it was easier than searching for 5V on programmer:
CH341 connected to board

Next to change of OTP (step by step video may be foun there).

OTP bits can be changed once, that action is irreversible additional attention is needed there.

On “OTP Programmer” tab the byte #2 bit #7 should be written to enable spreadCycle mode. After that driver goes to disabled state, until “duration of slow decay phase” is configured to some value other than 0. For me it’s still opaque which value should be written, the SilentStepStick configurator suggests value 3, the same value used as default for stealthChop mode. Without having better ideas I wrote the same, first 4 bits of byte #1 controls  duration, to write value 3,  bit #0 and bit #1 should be written.
Complete sequence is below:

Byte 2 bit 7 Byte 1 bit 1 Byte 1 bit 0

To make sure that OTP configured correctly, it’s needed to click “Read all Registers” button on “Register Settings” tab (not sure why on my screenshot I have OTP_PWM_GRAD equals 2 probably I made screenshot after writing only byte #1 bit #1):

Read OTP bits

Or disconnect and connect to driver again, “Tuning” tab should have enabled spreadCycle and TOFF set to 3:

Mission complete


Looking back, I see that here is not so much sense in changing OTP in that way or doing it at all.
First  making half-duplex serial just by connecting TX and RX with 1k resistor seems wrong. Atmel’s app not AVR947 suggest that it should looks like that:

Correct half-duplex joining

Which makes more sense and explains strange voltage around 2.8V I saw on PDN pin when I was troubleshooting FTDI. Possible explanations why FTDI didn’t work for me is that CH341 has different  threshold/voltage levels or has pull-up or my FTDI was partially damaged after series of unfortunate incidents.

Next if for some reason OTP should be changed, it’s easier to use MISO, MOSI or SCK pin from ISP pin header and make arduino sketch.

And finally, there I found that board has partially populated 3 PIN footprint, unused pin connected to pin #35 (PA2) of atmega installed on the board. Without  bltouch it’s the easiest option to have constant connection between controller and driver, which allows to use dynamic configuration. Even more with klipper it’s possible (but don’t know why) to have constant connection to each driver and even have bltouch by using SCK, MOSI, MISO (bye sdcard), BEEPER and PA2:

Unused GPIO

So far I have no bltouch, so even with configure OTP I’m going to solder a wire from PA2 to PDN just to have an option adjust driver configuration on the fly.

Thank for reading.

How to fix “Encryption credentials have expired” on xerox b215

Looks like I have new hobby  donated by xerox (if you can avoid greedy lying xerox, do it) – fixing my printer.
This time it just suddenly stopped to work with message “Encryption credentials have expired”. Previously I saw an option ‘Create new certificate’ on printer’s web page and my assumption was that probably certificate installed on printer was expired. At least I faced with that issues on embedded hardware like BMC’s many times, I tried to click on ‘Create new certificate’ button but it didn’t helped.
Let’s say thank you to xerox engineers and launch wireshark to figure out what happened. When I tried to resume print queue I saw communication on port 631 (IPP), which I able to decode as TLS in wireshark. openssl s_client shown expired certificate. Here is no option to uppload own key and certificate, but here is an option to downloads certificate signing request under Properties->Security->Machine Digital Certificate. So, I just created CA certificate:

$ openssl req -x509 -sha256 -days 3650 -newkey rsa:2048 -keyout rootCA.key -out rootCA.crt

Signed it using the next config:

$ cat > ./printer.conf << EOF
subjectAltName = @alt_names
DNS.1 = printer
DNS.2 = printer.local
IP.1 =
$ openssl x509 -req -CA rootCA.crt -CAkey rootCA.key -in PRINTER_request_sslCertificate.pem -out printer.crt -days 3649 -CAcreateserial -extfile printer.conf

And uploaded to printer.
Bonus point for SAN.

Make xerox b215 work with samba 4 again

Recently I bought xerox b215 (if you can, buy something other than xerox or hp) and wanted to make it scan to smb share. I already had configured samba in container using servercontainers/samba image.
So, it’s just to add another new share and configure user for scanner, right? Wrong!
It’ just didn’t worked. Thanks xerox’s engineers who decided not to burden end-user with diagnostic messages. It started scanning and after a second  returned back to the scan screen. Samba with log level 10 didn’t help me too, I just saw that client tried to connect and that all.
The tool which helped me is wireshark, I’ve found that after NTLMSSP_AUTH request from scanner samba sends STATUS_LOGON_FAILURE.

A little bit of “letsgoogleit” and voila ntlm auth = ntlmv1-permitted allowed me not to configure FTP for that lovely xerox.

Fix EFS dynamic provision on EKS

Probably it’s an obvious thing for people with more experience, but I spent an evening trying to figure out what’s wrong.

I have an EKS configured with terraform module terraform-aws-eks and IRSA configured like this:

module "efs_csi_irsa_role" {
  source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks"
  role_name             = "efs-csi"
  attach_efs_csi_policy = true
  oidc_providers = {
    ex = {
      provider_arn               = module.eks.oidc_provider_arn
      namespace_service_accounts = ["kube-system:efs-csi-controller-sa"]

At some point it started to work with static provisioning, but when I tried to use dynamic it stopped with the next errors in efs-csi-controller pod:

I1204 23:55:08.556870       1 controller.go:61] CreateVolume: called with args {Name:pvc-f725e33d-b1e5-44ff-a400-1f9ff8388296 CapacityRange:required_bytes:5368709120  VolumeCapabilities:[mount:&lt;&gt; access_mode: ] Parameters:map[basePath:/dynamic_provisioning directoryPerms:700 fileSystemId:fs-031e4372b15a36d5a gidRangeEnd:2000 gidRangeStart:1000 provisioningMode:efs-ap] Secrets:map[] VolumeContentSource: AccessibilityRequirements: XXX_NoUnkeyedLiteral:{} XXX_unrecognized:[] XXX_sizecache:0}
I1204 23:55:08.556934       1 cloud.go:238] Calling DescribeFileSystems with input: {
  FileSystemId: "fs-031e4372b15a36d5a"
E1204 23:55:08.597320       1 driver.go:103] GRPC error: rpc error: code = Unauthenticated desc = Access Denied. Please ensure you have the right AWS permissions: Access denied

And here is what I missed, official documentation uses eksctl for IRSA:

eksctl create iamserviceaccount \
    --cluster my-cluster \
    --namespace kube-system \
    --name efs-csi-controller-sa \
    --attach-policy-arn arn:aws:iam::111122223333:policy/AmazonEKS_EFS_CSI_Driver_Policy \
    --approve \
    --region region-code

SA creation is disabled with helm:

helm upgrade -i aws-efs-csi-driver aws-efs-csi-driver/aws-efs-csi-driver \
    --namespace kube-system \
    --set \
    --set controller.serviceAccount.create=false \

So I missed service annotation. The thing which have helped me to figure out what’s wrong (no it wasn’t careful reading of the documentation) was CloudTrail:

    "userIdentity": {
        "type": "AssumedRole",
        "principalId": "EKYQJEOBHPAS7L:i-deadbeede490d57b1",
        "arn": "arn:aws:sts::111122223333:assumed-role/default_node_group-eks-node-group-20220727213424437600000003/i-deadbeede490d57b1",
        "accountId": "111122223333",
        "sessionContext": {
            "sessionIssuer": {
                "type": "Role",
                "principalId": "EKYQJEOBHPAS7L",
                "arn": "arn:aws:iam::111122223333:role/default_node_group-eks-node-group-20220727213424437600000003",
                "accountId": "111122223333",
                "userName": "default_node_group-eks-node-group-20220727213424437600000003"
            "webIdFederationData": {},
            "attributes": {
                "creationDate": "2022-12-04T23:20:40Z",
                "mfaAuthenticated": "false"
            "ec2RoleDelivery": "2.0"
    "errorMessage": "User: arn:aws:sts::111122223333:assumed-role/default_node_group-eks-node-group-20220727213424437600000003/i-deadbeede490d57b1 is not authorized to perform: elasticfilesystem:DescribeFileSystems on the specified resource",

Assuming role as a node differently not what I expected.

If I have been more thoughtful I may ask myself what comment “## Enable if EKS IAM for SA is used” was doing in aws-efs-csi-driver’s values.yaml but I hadn’t.
Evening spent, lesson learned.


And  that update of service account doesn’t lead to magical appear of  AWS_WEB_IDENTITY_TOKEN_FILE env in container is a thing that worth to remember.


Looks like static provisioning will work even with broken IRSA for EFS, since NFS which is under the hood of EFS not be bothered by IAM existence in any sense.

Mikrotik: update WiFi PSK with randomly generated password and send it in telegram

Recently I upgraded my home WiFi with mikrotik cAP ac, the main reason to do it was an intention to configure guest WiFi with transparent traffic routing via TOR. Since I have intention to share guest password freely, I needed a way to change it from time to time. Firstly I wanted to make a telegram bot which would change wifi password via api call, but later I learned that it may be done with ugly scripting language which device supports.
More than that it has HTTP client, so I can change password by schedule and send it in telegram.

Here is the pre-requisites:
First, you should register the new bot with help of BotFather and get bot token, it looks like 110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw
Second, you need to get your chat id with bot. I got it by sending a message to newly registered bot and fetching an update with curl:

$ curl ''|jq '.result[]'

(chat id looks like 76615696).

The next thing to do is to create some sort of function to send message to telegram, it may be created with CLI or in WebFig -> System -> Scripts. Name tg_send, policy: read, write, policy, test

:local BotToken "110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw";
:local ChatID "76615696";
:local ParseMode "html";
:local DisableWebPagePreview True;
:local SendText $MessageText;
:local tgUrl "$BotToken/sendMessage?chat_id=$ChatID&text=$SendText&parse_mode=$ParseMode&disable_web_page_preview=$DisableWebPagePreview";
/tool fetch http-method=get url=$tgUrl keep-result=no;

It’s important to replace BotToken and ChatID with your own token and id.

Previous script allows to make notification when password changed, the next one do the trick. Name: change_guest_pw, policy: read, write, policy, test, password.

:local ProfileName guest
:local DeviceName [/system identity get name];
:local PW ([:pick ([/certificate scep-server otp generate minutes-valid=0 as-value]->"password") 0 10])
:interface wireless security-profiles set $ProfileName wpa2-pre-shared-key="$PW";
:local MessageText "\F0\9F\94\91 <b>$DeviceName:</b> new guest pw: <code>$PW</code>";
:local SendTelegramMessage [:parse [/system script get tg_send source]]; 
$SendTelegramMessage MessageText=$MessageText;

Here is important to replace ProfileName with your own security profile which is ‘guest‘ in my case.
After that change_guest_pw can be scheduled to be executed regularly.


One of the first thing which I noticed when I got the new access point was useless hardware button which just switched off all LEDs. It’s possible to use it as a trigger for script execution.
Cli commands:

/system routerboard mode-button
set enabled=yes on-event=change_guest_pw

make it change WiFI password as well or it may be configured in WebFig -> System -> RouterBoard -> Mode Button

Test of 5 AC-DC PSUs cheaper than $1.5

I need  220AC to 5V PSU for exhaust fan controller, the trickiest part – it should be as small as possible. The main load is LEDs, same time supplied voltage should be clean enough for analog to digital conversions doing by attiny85. So I bought and test 6 different PSU. Because of seller mistake one of them turned up to be 220 to 12V PSU, so only 5 of them were tested.

All of them produces awful output exception of #1 and #3 (hilink)
The winner is #1, it’s one of the smallest (12X25X18 mm), one of the cheapest ($0.77), rated at 5V 700mA power supply.
No one of them (with exception of hi-link) doesn’t look safe enought to be treated as galvanic isolated PSU.
Here is a table for quick comparison, but out of the box (without changing capacitors or adding filters), only #1 and #3 worth to buy:

Continue reading

Repairing of kitchenaid phase control board

It’s a story how I spent thee days troubleshooting 9 elements circuit when 7 of them are passive. I didn’t found what’s wrong, but fixed it.

I have  control board marked as W10354309, it’s European 220V model of phase regulator.
I wonder which logic behind kitchenaid’s parts marking, I found several part numbers for 220V version: 3184417, 4163707, 4163712, 9701269, 9706596, W10217542, W10538289, W10911442, W11174552, WPW10538289
(110V version have same idea and same schematic, just different values and ratings for some elements)
I don’t know why they do that. Probably because they use the same part on different models and/or under different brands.
So, I have 5ksm125 mixer and W10354309 phase control.

Service manual says, that at the first speed planetary shaft should have near 60RPM, but in my case it had near 120RPM and I was unable to decrease it by tuning control plate.

Here I should make a digression, these mixers have ability to maintain constant RPM under different load.  I was surprised when I learn how do they do that. One of the main component comes right from the steam engine era, it’s centrifugal governor which is placed on the shaft of the motor, here it is:

Yellow thing is the governor itself, black cylinders highlighted with green – weights, central pin stroked with blue is a pin which provides feedback to control plate. It works simple, the more RPM motor have the more pin extends.

Next component is so called control plate, in fact it has simple main switch and a T shaped contact. The main switch  just break circuit when you move switch lever to off. T-contact plate just shorts contacts on a plate in 3 different configuration. The white tab on a picture above is a dielectric tab on T-contact, governor’s central pin pushes this tab and changes which contacts are closed on control plate. Here is control plate from the other side:

And the last component is a phase control board, it’s basically dimmer if you google for ‘dimmer circuit’ you will find the same scheme as used in phase control board with one exception, usually dimmers have variable resistor for smooth regulation, phase control board has resistors network in which resistors shorts by control board in 3 different configuration. You can see it  behind top edge of contral board on the picture above and on closeup photo on picture below:

So, how its work together? Here is schematic from repair manual with comments and nominals added by me:

As I sad before control plate can be in 3 different states:
The first: the motor has too low RPM or doesn’t turn at all. T-contact fully closed, it shorts resistor network (R1, R2, R3, R5) completely and feeds motor with almost full sine wave (DIAC Q2 opens at around of 30V, so the start of the wave is chopped a little bit)
The third: the motor has too much RPM. T-contact fully opened, resistors network has maximum resistance,  phase control board feeds motor with minimum amount of energy (manual says that it should provide 40V RMS, I don’t understand why it’s true for both 110V and 220V version, but looks like it is).
The second: this state is somewhere in between too low RPM and too much RPM, control board shorts R1, equivalent resistance is ((R5+R3)* R2)/(R5+R3+R2), manual says that it should provide 80V RMS.

The more RPM motor have, the more central pin of centrifugal governor extents, the more it shift T-contact. When T-contact shifting, it opens circuit with bottom contact first and with upper contact next (check schematic above). When you select mixer’s speed you change distance between control plate and governor, the more distance it has the faster motor should spins to get equilibrium between the first and the third states.

Finally I can tell about my issue.
Usually when phase control is broken mixer doesn’t cho-cho at all or doing it on max speed, my story was slightly different, it had near 120 constant RPM on the first 3 speeds, next speed or two  increased RPM to the maximum, and other speeds did nothing.

When I saw schematic, I was pretty sure that I just need to replace DIAC. In circuits like this, if something works wrong in 99 cases of 100 it caused by broken semiconductor. When TRIAC failed it usually stays open or shorted (motor shouldn’t run at all or run at full speed).

I changed DIAC but  nothing changed, motor had RPM above nominal, but not the maximum. RPM was enough to extent governor’s central pin to the maximum and open both contacts on control plate.

The next suspect was TRIAC, here is only two semiconductors, if one of them is OK, the other one is broken, right? Wrong. I tried two different TRIACs without success. BTA12-600SW (it has the same characteristics like original one. Logic level gate, gate’s current 10mA , snuberless, but rated for 12A instead of 6A) and BTA06-600CW ( it isn’t logic level and had gate current around 35mA, it produced visible sparks during re-commutations on control plate, so don’t use it).

What should be suspected next? Capacitors? Both had less than 5% difference of capacitance from their nominals. I tried other capacitors, RPM of motor changed, but not significantly (in theory failed capacitors may have noticeable different capacity under high voltage, but I tested them with low voltage LCR meter).

After that I started to go crazy, I even de-solder every resistor, but they had correct values.
I spent near 3 days trying to find what’s wrong.
I had a lot of theories: failed resistor which heats when voltage applied and changes its resistance, semi-broken wires, semi-broken motor etc.
I even found a topic in which people had the same issue, but no one find the solution:

Soon after I started my experiments, I found that everything works as expected when I put R4 with increased value, but I wanted to find why circuit which had right elements didn’t work as it should.
At the end of the third day I gave up. I tried to replace every resistor, every capacitor in circuit and it didn’t helped, I tried to solder wires in parallel with existent,
In the end I decided to put 3.6KOhm R4 instead of original 560Ohm.

Here is my observations:

  • Manuals says that you can check phase control by putting sheet of non conductive material (like papper) between T-contact and contact which it touches, if it’s OK it should provide around 40V, but I got 50V. When I lovered voltage to 40V I got response from control plate regulation.
  • Motor starts spinning at around 9V DC.
  • Coils of stator has resistance of 7.8 Ohm each, rotor has resistance near 4 Ohm between nearest contacts, resistance of motor (between red and white wire) near 40 Ohm.
  • Circuit is sensible to element’s values, even when I tried to put capacitors with the same value I got slightly different RPM. My circuit has 1% R5, old scheme from manual has 3 resistors in series, usually this approach used when resistors have breakdown voltage less than voltage drop on them or when you want to use few cheap 5% 10% resistors instead of precise one.
  • Probably phase control boards with  different part numbers more stable. I found photos of others boards and saw that resistors have values different from values that observed. Here is an example from amazon:

Simple temperature controlled fan regulator

Some time ago I pulled out temperature fan controller from one of old ATX PSU.
With 12.2V input it provides 4.5V at 25°C and raises output voltage till 11.8V at ~70°C. After changing Q2 to higher voltage transistor and tuning of R4-R5 this divider this controller should be suitable for 12V fans with 24/36/48V input.
Here is result of my reverse engineered schematic: