How I repaired my dishwasher using fundamental principles for troubleshooting

I have a Samsung DW-FN320T dishwasher, which is about 5+ years old. It was not used very often because of maid at home. But now due to COVID lockdown the dishwasher started getting used heavily – almost twice a day because authorities banned maids to contain the spread of the disease.

One day though, it stopped working – the symptom, it showed heater error which as per the manual means either of temperature sensor, heating coil or some low sensing device is dead. So I called up my usual home repair technician, he came and found some plastic part of a mixer-grinder blade in the drain basin of dishwasher. Meanwhile we also discovered that the drain pipe is broken so he replaced that, but that still didn’t solve heater error.

He was not very positive about this because it seems Samsung has phased out this model in India (and they don’t sell dishwashers anymore) and availability of spare parts is a problem. He left saying he’ll try to procure the heater sensor.

But I wasn’t convinced that there is a heater related problem, because the dishwasher was completing cycles partially – for a cycle of say 2h 30m it would complete about 20-30 minutes then stop with heater error and I also observed it wasn’t pumping water to the spray arms in some cycles occasionally. The problem of pump not working was completely random and there was no pattern irrespective of wash program selected.

In addition to this, before the machine stopped working completely there were days when dishes wouldn’t come clean or the soap won’t dissolve but dishwasher used to show END (wash cycle complete).

So I opened up the dishwasher (a hell of a job that is) and then checked all the wiring for any loose connection etc. The problem here was – motor not running sometimes, so I was suspecting some kind of loose connection or faulty component such as a relay (which drives the motor) on the controller board.

To check what is happening I used a simple multimeter – put it in AC mode and inserted the probes in the connector of the motor. Then I started the dishwasher in the smallest cycle (pre-wash) – when I discovered the motor was getting power but it wasn’t spinning. This was weird, because if it isn’t spinning that means motor is dead, but a dead motor does not spin occasionally!

The shaft the motor was accessible from outside, so to check if the shaft is jammed I tried to turn it manually using a screwdriver (with the dishwasher being off) – it was moving freely without any resistance to motion. Then turned on the dishwasher again and started pre-wash – the motor started.

This made me suspect the capacitor of motor, because in single phase motors a capacitor is used to phase-shift the current in second winding which gives the starting torque. Also whenever the motor was turned on by the controller and it didn’t spin there was a sound coming from it. In one such instance, when power was being applied to motor (confirmed using multimeter), turned the shaft manually and it did start spinning.

The dead capacitor

This confirmed the doubt on capacitor. When capacitor loses it’s capacitance due to age/other factors, it is not able to provide the current required to produce the starting torque to spin the motor. Once a motor starts though it continues to spin due to inertia.

New capacitor

Now the next challenge was to finding a compatible capacitor (3 uF) to replace this – since it has a bolt thread, it was fixed using a nut on the motor itself. Considering the urgency I had planned to wire up two ceiling fan capacitors in parallel (which are typically 2.25 uF) in case I don’t get a new one. Luckily a friend of mine helped in procuring a 3 uF capacitor but it didn’t have the bolt thread.

Mounting

So in order to fit the new capacitor I did a simple jugaad / hack – put a simple nut-bolt on the mounting hole and tied it on the bolt using cable ties.

After fitting the capacitor and making the machine upright, I ran some test cycles in short long ones to check if the problem is fixed – it was fixed. The motor started spinning immediately without getting stuck / jammed at any time.

The lesson from this exercise is that one should always think from basics when approaching a problem to be solved, this applies to any field – whether it is electronics, electrical or computer science.

Also often people say – what is the use of subjects taught in school in real life. AC motor working was a part of 11th grade when I was in school. Yes, the subjects taught in school do have some use in real life. That’s how I was able to fix my dishwasher and saved a lot of money in that process in spite of not being an electrical or electronics engineer.

Fixed my Soundbar’s faulty remote

I have a Phillips sound bar, must be about 6-7 years old. Today while watching TV, I couldn’t increase the sound volume using the remote, and unfortunately using the remote control is the only way to change the Soundbar’s settings.

Update 16 Feb 2021: It stopped working again but this time it was a dry soldered IR LED. Reheated (or in simple speak, resoldered) the contacts and got it working again.

Then I took few drops of isopropyl alcohol on my finger and smeared it on the PCB as well as on the rear side (contact side) of the rubber sheet with buttons. Let it dry for sometime then tried to test if it works, and it did!

E-waste and money both saved, or I’d probably have to buy a new remote which might have been difficult to procure given the age of the Soundbar.

Speeding up PHP CLI applications using OpCache

I run a few PHP based applications which require background jobs, and I use the usual crond via crontab to run the jobs. PHP has an excellent feature – OpCache which can cache the compiled code in memory to speed up the web applications, where typically the PHP-FPM process is a long running process so the compiled code can be fetched on next request. But with CLI applications there is no shared state or memory to store the compiled code.

The Solution

Use the file cache feature of PHP, configurable in php.ini using opcache.file_cache configuration option. Note that OpCache on CLI must be enabled using opcache.enable_cli=1, by default it stays off.

With multiple users running background jobs

The opcache.file_cache path must point to a directory where the user running the PHP script must have write access so that PHP can create the .bin files. Now here’s the catch, what happens if we have multiple users running different PHP scripts? They cannot share a common file cache directory for security reasons or it can cause cache poisoning. PHP seems to generate some hash string based directory structure but since all my PHP applications live in a common directory but are run by different users, it was not working when using a single directory. Consider /home as the common path and /home/user1, /home/user2 as different users.

Solution for multiple users

Specify opcache.file_cache path in the crontab! Here’s how:

*/5 * * * * /usr/bin/php7.3 -d opcache.file_cache=/var/cache/php/user1 -f /home/user1/app/cron.php

Create the /var/cache/php directory with permissions similar to /tmp (sticky bit) and the individual directories as well (they don’t seem to get created automatically):

mkdir /var/cache/php
chmod 1777 /var/cache/php

mkdir /var/cache/php/user1
chown user1:group1 /var/cache/php/user1
chmod 700 /var/cache/php/user1

It is also a good idea to enable the file consistency check option opcache.file_cache_consistency_checks=1, it will prevent errors in case cache gets corrupted for any reason.

Enjoy fast background jobs!

KeepassXC SSH Agent in WSL and OpenSSH for Windows

I use KeepassXC for managing passwords – it is a fantastic FOSS tool for that. If you don’t use a password manager or use a paid one, do give it a try, I used to be a paid LastPass user few years ago till I discovered KeepassXC.

It also has support for storing SSH keys inside the database file and exporting it via SSH-Agent when the database file is unlocked and removing it when it is locked. An excellent feature which eliminates the need to store the ssh keys un-encrypted.

I never got around setting up the SSH agent thing using KeepassXC, because I just used to copy my ~/.ssh folder around – primarily two devices, Windows desktop and Linux laptop. In my .zshrc I use keychain so that the keys are added to my agent whenever I open the terminal – in WSL as well as in Linux.

Now the catch here is that KeepassXC cannot export SSH keys to the SSH Agent running inside WSL, this means either I have to ditch the idea of storing ssh keys inside Keepass or use native Windows OpenSSH. While searching I came across this article A Better Windows 10+WSL SSH Experience in which the author has done agent sharing setup between native Windows OpenSSH and WSL using a named pipe <=> socket proxy (since inside WSL the applications speak Unix stuff and Windows OpenSSH agent listens on named pipe instead of Unix socket, obviously).

In the process I also updated the Windows OpenSSH version because the one installed by default was giving me a warning when SSHing to one of the servers and ssh was switching back to password based authentication:

warning: agent returned different signature type ssh-rsa (expected rsa-sha2-512)

The solution for this problem is basically to update OpenSSH to a newer version. The Windows OpenSSH version in my system was 7.7, updated to 8.1.

Now with the current setup my keys are safely stored inside the KeepassXC database file, and agent forwarding works in both native Windows OpenSSH and WSL! 😀

Solving Windows Store Error 0x80240007

So yet again, I faced an annoying update related issue with Windows but this time it was Windows Store. It was throwing error code 0x80240007 for any app update. Previous encounter with Windows Updates and it’s solution was here.

It’s absolutely disgusting how Microsoft makes it extremely difficult to find out errors in their operating system. If someone from Microsoft is reading this, please give more helpful error messages! Such error codes and their numerous solutions online make it seem like for any error that occurs in Windows the solution is to reset and reinstall, WTF!

I wasn’t able to install any app from the store nor was it updating, and it was throwing the errors. I searched a bit about this, but didn’t get any useful solution. In event logs I had three error codes 0x80240007 0x80070002 0x80073CF0. The screenshots of Windows Event Log:

I had completely given up on this after numerous searches and trying out the solutions mentioned on those pages, to the extent I uninstalled all the store apps – the most significant ones for me being Skype, Whatsapp, Telegram; and actually I discovered this issue when I knew Telegram was updated but I was wondering why it wasn’t updating on my desktop.

Then today when updating my password manager KeepassXC the MSI installer gave me an error code 2203. Initially I thought the two were unrelated issues (store and MSI error) but you see in the event log it has complained about some msixvc. Then I discovered MSIs are installed using a command msiexec, so I checked out it’s available options in which I found a /log option which logs the installation messages / errors etc.

msiexec /log keepassxc.log /i KeepassXC.msi

In the log generated I found an interesting error:

Database: C:\Windows\Installer\inprogressinstallinfo.ipi. Cannot open database file. System error

A little bit of searching and some site mentioned it could be because the temporary files directory is not writable by the installer. But I had solved the temporary folder issue without which I wasn’t able to update at all, this got me thinking. In the environment variables I had set the system temp folder as %windir%\Temp. Just as a fluke I changed it to C:\Windows\Temp and after a reboot tried installing KeepassXC again. It worked! And all the store apps updated as well. Apparently %windir% doesn’t expand to C:\Windows when MSIs are installed?

So the lesson is, don’t tamper with the temp folder on Windows. This is not Linux where you can just do mount -t tmpfs /tmp for performance and to save the SSD unwanted write cycles.

Windows Defender Update Error 0x80070643

So my Windows 10 installation was throwing this update error 0x80070643 since a last few days:

Went through many solutions such as resetting the windows update service, removing downloaded files, etc. but nothing helped to solve the problem. The thing is, Windows 10 seems to hide the actual error codes behind some generic error codes so you actually do not know what is causing the error 😐
Finally I found this helpful blog post A broken Windows Defender update which had some steps how to get detailed log of why it’s failing.

I ran Get-WindowsUpdateLog in PowerShell and went through the update log file where I found the error code why it was actually failing – 0x80092003. Let’s see what this error code means – all the Windows error codes seems have been documented on COM Error Codes (Security and Setup).
So the error code 0x80092003 means CRYPT_E_FILE_ERROR which is basically “An error occurred while reading or writing to a file.”. It doesn’t specify what is the error though!

I made a wild guess, that probably it is not able to write to a temporary file or something. On my system temporary folders were set to be on RAMDISK using IMDisk Toolkit. So I went to the environment variables page and changed the system TMP and TEMP variables (only) to point to the original %windir%\Temp folder and rebooted. Then I tried to run the update again, and it worked! My personal temporary folder continues to be on RAMDISK.

A Golang program to dump serial data into CSV file

A unique situation in which I wanted to dump the memory of a program running on a microcontroller – the program can send data through serial port, but for it to make sense for the programmer it has to be dumped in a readable format. And another challenge was that the controller was programmable only from MS Windows. So this dumping program must be able to run on Win64.

I chose Golang for this purpose as I don’t know the serial port reading API of Win32/64 but I can build for Win64 using Golang on Linux and a nice Serial library was available for Golang as well.

package main

import (
	"fmt"
	"github.com/tarm/serial"
	"bufio"
	"os"
	"strconv"
	"time"
	"encoding/hex"
	"bytes"
	"encoding/binary"
	"sync"
)

type mt_var struct {
	u32 uint32
	i32 int32
	f32 float32
	choice int64
}

func (v *mt_var) Parse(varvalue_b []byte) {
	reader := bytes.NewReader(varvalue_b)

	switch v.choice {
	case 1:
		if err := binary.Read(reader, binary.BigEndian, &v.u32); err != nil {
			fmt.Println("Error occurred while parsing bytes to uint32: ", err)
		}
	case 2:
		if err := binary.Read(reader, binary.BigEndian, &v.i32); err != nil {
			fmt.Println("Error occurred while parsing bytes to int32: ", err)
		}
	case 3:
		if err := binary.Read(reader, binary.BigEndian, &v.f32); err != nil {
			fmt.Println("Error occurred while parsing bytes to float: ", err)
		}
	}
}

func (v mt_var) String() (ret string) {
	switch v.choice {
	case 1:
		ret = fmt.Sprintf("%v", v.u32)
	case 2:
		ret = fmt.Sprintf("%v", v.i32)
	case 3:
		ret = fmt.Sprintf("%v", v.f32)
	}
	return
}

func main() {
	scanner := bufio.NewScanner(os.Stdin)

	defer func() {
		fmt.Println("Press any key to exit")
		scanner.Scan()
	}()

	fmt.Print("Enter serial port name: ")
	scanner.Scan()
	portname := scanner.Text()

	fmt.Print("Enter baud rate: ")
	scanner.Scan()
	baud_s := scanner.Text()
	baud, err := strconv.ParseInt(baud_s, 10, 64)

	if err != nil {
		fmt.Println("Unable to parse baud rate: ", err)
		return
	}

	fmt.Print("Enter serial port timeout in seconds: ")
	scanner.Scan()
	timeout_s := scanner.Text()
	timeout, err := strconv.ParseInt(timeout_s, 10, 64)

	if err != nil {
		fmt.Println("Unable to parse timeout value: ", err)
		return
	}

	fmt.Print("Select data type of variables (1 - uint32, 2 - int32, 3 - float32): ")
	scanner.Scan()
	dt_var_s := scanner.Text()
	dt_var, err := strconv.ParseInt(dt_var_s, 10, 64)

	if err != nil {
		fmt.Println("Unable to parse choice value for data type: ", err)
		return
	} else if dt_var < 1 || dt_var > 3 {
		fmt.Println("Invalid choice ", dt_var, " for data type selection")
		return
	}

	fmt.Print("Enter number of variables to be read per timestamp: ")
	scanner.Scan()
	num_vars_s := scanner.Text()
	num_vars, err := strconv.ParseInt(num_vars_s, 10, 64)

	if err != nil {
		fmt.Println("Unable to parse num vars value: ", err)
		return
	}

	fmt.Print("Enter number of bytes per variable: ")
	scanner.Scan()
	num_bytes_per_var_s := scanner.Text()
	num_bytes_per_var, err := strconv.ParseInt(num_bytes_per_var_s, 10, 64)

	if err != nil {
		fmt.Println("Unable to parse num bytes per var value: ", err)
		return
	}

	fmt.Print("Enter total number of timestamps: ")
	scanner.Scan()
	num_timestamps_s := scanner.Text()
	num_timestamps, err := strconv.ParseInt(num_timestamps_s, 10, 64)

	if err != nil{
		fmt.Println("Unable to parse num timestamps value: ", err)
		return
	}

	fmt.Print("Enter DAQ sample time: ")
	scanner.Scan()
	sampletime_s := scanner.Text()
	sampletime, err := strconv.ParseInt(sampletime_s, 10, 64)

	if err != nil {
		fmt.Println("Unable to parse sample time value: ", err)
		return
	}

	c := &serial.Config{Name: portname, Baud: int(baud), ReadTimeout: time.Second * time.Duration(timeout)}
	port, err := serial.OpenPort(c)

	if err != nil {
		fmt.Println("Unable to open serial port: ", err)
		return
	}

	defer func() {
		fmt.Println("Closing serial port")
		port.Close()
	}()

	var wg sync.WaitGroup
	var stop_serial_writer = make(chan bool)

	fmt.Println("Press any key to start reading serial port")
	scanner.Scan()

	wg.Add(1)
	go serial_writer(port, stop_serial_writer, &wg)

	fmt.Println("Starting to read port")

	writer_channel := make(chan []byte, 1000)

	wg.Add(1)
	go writer(writer_channel, num_vars, &wg, sampletime, num_bytes_per_var, dt_var)

	bytes_per_timestamp := num_vars * num_bytes_per_var
	var bytecount int

outer:
	for i := int64(1); i <= num_timestamps; i++ {
		buf := make([]byte, bytes_per_timestamp)

		for j := int64(0); j < bytes_per_timestamp; j++ {
			if n, err := port.Read(buf[j:j+1]); err != nil {
				fmt.Println("Error occurred ", err)
				break outer
			} else {
				bytecount += n
				fmt.Printf("Read %d bytes\r", bytecount)
			}
		}

		writer_channel <- buf
	}

	fmt.Println()

	close(writer_channel)
	stop_serial_writer <- true

	fmt.Println("waiting for all goroutines to exit")
	wg.Wait()
}

func writer(channel chan []byte, num_vars int64, wg *sync.WaitGroup, sampletime int64, bytes_per_var int64, dt_var int64) {
	defer func() {
		wg.Done()
	}()

	home_directory, err := os.UserHomeDir()
	if err != nil {
		fmt.Println("Unable to fetch user home directory: ", err)
		return
	}

	time_str := time.Now().Format("2006-01-02-15-04-05")
	filename_raw := fmt.Sprintf("%s%cserial2csv_raw_%s.txt", home_directory, os.PathSeparator, time_str)
	filename_csv := fmt.Sprintf("%s%cserial2csv_%s.csv", home_directory, os.PathSeparator, time_str)

	f_raw, err := os.Create(filename_raw)
	if err != nil {
		fmt.Println("Unable to create file ", filename_raw, ": ", err)
		return
	}
	defer f_raw.Close()

	f_csv, err := os.Create(filename_csv)
	if err != nil {
		fmt.Println("Unable to create file ", filename_csv, ": ", err)
		return
	}
	defer f_csv.Close()

	bytes_written := 0
	var o_sampletime int64


	for data := range channel {
		hexdata := make([]byte, hex.EncodedLen(len(data)))
		hex.Encode(hexdata, data)
		if n, err := f_raw.Write(hexdata); err != nil {
			bytes_written += n
			fmt.Println("Error occurred while writing to file ", filename_raw, ": ", err)
		}

		fmt.Fprintf(f_csv, "%v,", o_sampletime)
		o_sampletime += sampletime

		for varnumber := int64(1); varnumber <= num_vars; varnumber++ {
			var err error
			var n int

			varvalue_b := data[varnumber * bytes_per_var - bytes_per_var:varnumber * bytes_per_var]
			var varvalue = mt_var{choice: dt_var}
			varvalue.Parse(varvalue_b)

			if varnumber == num_vars {
				n, err = fmt.Fprintln(f_csv, varvalue)
			} else {
				n, err = fmt.Fprintf(f_csv, "%v,", varvalue)
			}

			if err != nil {
				fmt.Println("Error while writing to file ", filename_csv, ": ", err)
			} else {
				bytes_written += n
			}
		}
	}

	fmt.Println("Total bytes written to files ", bytes_written)
	fmt.Println("RAW Data: ", filename_raw)
	fmt.Println("CSV Data: ", filename_csv)
}

func serial_writer(port *serial.Port, stop chan bool, wg *sync.WaitGroup) {
	fmt.Println("Starting serial port writer - sending 1 continuously")

	defer wg.Done()

	data := []byte{1}
	zero := []byte{0}

outer_one:
	for {
		select {
		case <-stop:
			fmt.Println("stopping to send 1 on serial")
			break outer_one
		default:
			port.Write(data)
		}
	}

	zero_stop_timer := time.NewTimer(2 * time.Second)

outer_zero:
	for {
		select {
		case <-zero_stop_timer.C:
			fmt.Println("stopping to send 0 on serial")
			break outer_zero
		default:
			port.Write(zero)
		}
	}

	zero_stop_timer.Stop()
}

The same is available on my Github.

Building this program on Linux, for Win64 is pretty easy (my Linux box is amd64):

GOOS=windows go build serial2csv_sync_mdt.go

You may require some changes in the build command if building for 32 bit or from a 32 bit system.

Golang on OpenWRT MIPS

I have been tracking Golang for quite a while since I came to know about it I guess about 3 years ago primarily because it is very easy to use and build static binaries that just work about anywhere. And no dealing with memory allocation stuff which often lead to frustrations and segmentation fault bugs soaking up hours of your time to solve those.

As a OpenWRT user running a Go program on OpenWRT had been one of my most desired things. So here it is, finally, a hello world program running my TP Link WR740N (which is a MIPS 32 bit CPU, ar71xx in OpenWRT tree):

system type             : Atheros AR9330 rev 1
machine                 : TP-LINK TL-WR741ND v4
processor               : 0
cpu model               : MIPS 24Kc V7.4
BogoMIPS                : 265.42
wait instruction        : yes
microsecond timers      : yes
tlb_entries             : 16
extra interrupt vector  : yes
hardware watchpoint     : yes, count: 4, address/irw mask: [0x0ffc, 0x0ffc, 0x0ffb, 0x0ffb]
isa                     : mips1 mips2 mips32r1 mips32r2
ASEs implemented        : mips16
shadow register sets    : 1
kscratch registers      : 0
package                 : 0
core                    : 0
VCED exceptions         : not available
VCEI exceptions         : not available
package main

import "fmt"

func main() {
        fmt.Println("hello world")
}

First I built it with GOOS=linux GOARCH=mips go build hello but it did not run and gave error “Illegal Instruction”. Then I tried it with GOOS=linux GOARCH=mipsle go build hello which again, did not work because the CPU of this TP Link is big endian, not little endian. After a bit of searching I came across this GoMips guide on Golang’s Github which builds it using GOMIPS=softfloat. I tried the same and my program works! It will now be easy to build complex stuff that runs on embedded devices without resorting to C/C++.

$ GOOS=linux GOARCH=mips GOMIPS=softfloat go build hello
$ scp hello root@IP:/tmp/

root@740n-2:/tmp# ./hello
hello world
root@740n-2:/tmp#                                                                                        

Change username and hostname for Ubuntu instances on AWS

If you have used Ubuntu images on AWS, you might have noticed that the default username of the user on the instance is ‘ubuntu’. And the hostname is dynamically generated according to the public IP. Both of these can be changed using cloud-config supported on Ubuntu images – the config has to be provided in the User Data section in Advanced on the Configure Instance tab.

YAML configuration to change the parameters:

#cloud-config
fqdn: myhostname
system_info:
  default_user:
    name: myusername

A lot more things are possible using the cloud-config method and it is supported on other operating system images as well such as CentOS. Take a look at Cloud config examples.

Using privileged mode (become) in Ansible without a password

So I was working on automating some stuff using Ansible when the necessity to have password less superuser access came up. A simple way would be adding the ansible management key to the root account itself and allow SSH to root, but allowing ssh to root is usually a bad idea.

I tried many things – NOPASSWD in sudo entry, requiretty, etc. And after nearly two hours of digging a spark ignited and I found a way – Linux has PAM module called pam_wheel.so which can implicitly allow root access via su when a user is present in the wheel group (the group can be configured in module options). This module is disabled by default on most Linux distributions, in fact Ubuntu doesn’t even have a wheel group. But in this particular case I was managing CentOS which has the wheel group.

Add the Ansible management user to the wheel group and enable the pam_wheel.so module:

# /etc/pam.d/su

# Uncomment the following line to implicitly trust users in the "wheel" group.
auth            sufficient      pam_wheel.so trust use_uid

Now when you SSH to the machine using the ansible user and run su – it will give you root access without asking for password. Consequently, now when you set become_method = su in your Ansible configuration by way of editing config files, setting variables in playbook or inventory, etc. Ansible will become privileged without a password.