3D Printer Conversion to Automatic Watch Cleaning Machine

by daveburkeaus in Circuits > Reuse

2020 Views, 11 Favorites, 0 Comments

3D Printer Conversion to Automatic Watch Cleaning Machine

PXL_20231117_052413071.jpg
AWC Full Test Short

Watch cleaning machines make the cleaning of mechanical watches both convenient and very effective but they can be expensive with most manual machines starting at over $1000 AUD and automatic machines starting at around $8,000 AUD. This is a big expense for a hobbiest like me.

It dawned on me that a 3D printer could be converted into a watch cleaning machine by replacing the hotend on the X axis with a spinning motor. How hard could it be, right!

Specifications

  • Fully automatic
  • 1 x Cleaning Chamber
  • 2 x Rinse Chamber
  • 1 x Drying Chamber

NB: I've decided to make it easier for myself so I purchases the Elma SE Basket Complete and the Elma Basket Holder for RM/SE. You could of course rig up your own basket system on the cheap

Features

The cleaning stage and the two rinse stages consist of two phases, the wash/rinse phase and the spin phase to expel liquid. The drying stage only has a spin phase. All features are configured via a configuration file. These include:

  • Setting the duration of each phase of the clean
  • Setting the motor speed for each phase of the clean
  • Setting the motor action for each phase of the clean. These are forward, backward and agitate (a user configured combination of forward and backward movements)

Supplies

  • Creality CR10S Pro V2 (or any printer with 300mm x 300mm x 350mm build area) - AUD $150 (2nd hand)
  • Raspberry Pi 3 Model B+ or equivalent. - AUD $62
  • Creality Endstop Switch - AUD $10 (I've got a BL-Touch on my Ender 3v2 so I'm using that printer's Z endstop switch)
  • Klipper from Klipper3D. Klipper is a great printer firmware that allows you to run GCode from a Rasberry Pi - FREE
  • Moonraker from Arksine. Moonraker is an API web server for Klipper does a great job providing APIs that are used by the UI Mainsail to interface with Klipper - FREE
  • Mainsail from mainsail-crew. Mainsail is an awesome browser based interface for Klipper and can be used from anywhere on any device - FREE
  • KIAUTH from th33xitus (Optional) KIAUTH is a fantastic app that installs Klipper, Moonraker and Mainsail with a click of a mouse. You can do all of this yourself by why would you? - FREE
  • Elma Basket Holder for RM/SE - AUD $77
  • Elma Basket Complete - Ø64mm - AUD $284
  • Flinders 1400ml Round Glass Canister ($6 each) x 3 - AUD $18
  • 24v 7A Dual Channel DC Motor Driver - AUD $30.50
  • 24v Geared DC Motor - High Torque - 360rpm - AUD $50
  • 8mm * 8mm Rigid Coupling - AUD $8
  • 8mm Smooth Rod - 300mm - AUD $3.50
  • 24V 200W Energy Saving PTC Car Fan Air Heater - AUD $13
  • 2N2222A NPN Transistor - AUD $0.65
  • PC Boards Vero Type Strip - 95mm x 75mm - AUD $9.95
  • TOTAL AUD $716 (AUD $355 without the Elma basket complete and holder)

Replace Z Homing Sensor With Mechanical Endstop Switch (Optional)

Creality V2.4 (CR-10S Pro) Board.png
Klipper Config (Z Stepprt).png
PXL_20231118_031721324.jpg

The Creality CR10s Pro V2 comes with a BL-Touch style sensor for Z axis homing. For the watch cleaning machine, the Z axis home needs to be at the top of the gantry and not the build plate. The mechanical endstop switch is plugged into the Z- socket on the board. Below is the update to the klipper config file that reverses the Z axis homing position and uses the mechanical endstop switch.

[stepper_z]
step_pin: PL3
dir_pin: !PL1
enable_pin: !PK0
rotation_distance: 8
microsteps: 16
full_steps_per_rotation: 200
# endstop_pin: probe:z_virtual_endstop
# position_endstop: 0 
endstop_pin: !PD3 # was probe:z_virtual_endstop
position_endstop: 350 # Was 0 but needs to be the top of the gantry
homing_positive_dir: true # Home away from zero (ie towards the top of the gantry)
position_min: -1
position_max: 351

Remove the Buildplate

PXL_20231118_032343558.MP.jpg

I wanted to make the build area as big as possible so I removed the buildplate so that all was left was the metal base. In hindsight, I shouldn't have bothered. It didn't give me that much extra space and it was a pain to get the heater element off the bed.

Remove the Extruder and the Hotend Assembly

I kept the carriage for the hotend assembly to mount the motor on. The structs for the extruder were used to mount the DC motor

Edit Klipper to Avoid "ADC Out of Range" Error

klipper1.png
klipper2.png

Now that the hotend assembly is removed, Klipper reports an "ADC out of range error" as it expects to get readings back from the now removed hotend thermistor. This is a simple code change to the adccmds.c file. Find the line that reads:

try_shutdown("ADC out of range");

Add // in front of the word "try" and save. The line should now be:

//try_shutdown("ADC out of range");

You will now have to recompile Klipper and flash it to your printer. This step can differ depending on the printer used but you can use the recommended app, KIAUTH, to make this step simpler or follow the Klipper instructions

Mount the Motor Driver

PXL_20231118_035701421.MP.jpg
PXL_20231118_035726215.jpg

I used the structs that mounted the extruder to mount the 24v motor driver. The power for the driver came from the 24v power for the hotend. The driver is used to control the DC motor and the fan for the PTC heater. This driver was attached to a 3D printed mount and then screwed on to the structs

Mount the 24v DC Motor

PXL_20231118_042126390.jpg
PXL_20231118_042133874.jpg

I reused the carriage that moved the hotend assembly to mount the DC motor. I 3D printed the mount but I've found that it gets a bit of a wobble up so it may have been better getting this fabricated from metal for more rigidity

Attach the Basket to the Motor

PXL_20231118_042418703.MP.jpg

Use an 8mm * 8mm Rigid Coupling to attach an 8mm Smooth Rod to the Elma basket.


Connect the PTC Heater

PXL_20231118_043933340.jpg
PXL_20231118_043937439.jpg
0yc2qpr1kcu91.jpg
PXL_20231118_043648480.jpg
ksnip_20231118-162006.png

This is the step that took me longest to figure out. I wanted to use existing the 24v power supply for the heater bed so I snipped the wires and connected the PTC heater. I thought I could just issue the M140 - Set Bed Temperature gcode command and the power would then drive the PTC heater. However, for safety reasons, Klipper expects the bed to heat at a certain rate and because I don't have a heater bed and thermistor, Klipper would shut the PTC heater down after about 10 seconds. I tried editing the Klipper code but the result was the same.

I then found that if I could send 5v to the MOSFET that controlled the heater bed I could bypass the motherboard and the errors. A Raspberry Pi only outputs 3.3v on it's GPIO so I couldn't use those but I could use the 5v supply from RPi. I then rigged up a 2N2222A NPN Transistor that would allow the 5v to flow from the RPi to the heater bed MOSFET when 3.3v is applied to the transistor gate.

I did contemplate using a hairdryer or some other externally powered device but I really wanted to keep everything contained to the printer and RPi

Install the Jar Holders

PXL_20231118_052732985.MP.jpg

I 3D printed inter-locking jar holders and attached with bull dog clips

Code to Control the Cleaning Process

ksnip_20231118-163304.png

To control the printer, motor and PTC heater I created a Pyton Flask application. The code controlls the motor and PTC heater via the RPi GPIOs and the printer itself on controlled via calls to the Moonraker API.

Although I'm a software developer, this is my first app written in Python and I know it could be improved. The template is meant to updated with a status indicating the current clean/rinse/dry cycle but I haven't completed that bit and honestly it's a nice to have that I probably won't get to. The code can be found here

Configuring the Printer

The app.config file is where settings such as jar locations, motor direction, motor speed and duration of each cycle is configured. It's documented so it should be self explanatory. I used mainsail to move the motor around and find the coordinates of each jar and the PTC heater

[moonraker]
ip_address = http://xxx.xxx.xxx.xxx:7125

[general]
#pause_after_move: delay between the printer reaching a location and the next command in seconds 
pause_after_move = 2
#heater_cool_down: duration in seconds to keep the fan on after the heater is turned off
heater_cool_down_duration = 120

[motor_gpio_pins]
motor_control_input_1 = 29
motor_control_input_2 = 31
#motor_enable: PWM pin
motor_enable = 32

[heater_gpio_pins]
heater_fan_control_input_1 = 18
heater_fan_control_input_2 = 22
#heater_fan_enable: PWM pin
heater_fan_enable = 16
heater_element = 36

[clean_cycle]
receptacle_location_x = 87
receptacle_location_y = 87
#receptacle_location_wash_z: fluid location in receptacle
receptacle_location_wash_z = 155
#receptacle_location_expel_z: space in receptacle to expel fluid
receptacle_location_expel_z = 235
#receptacle_height: expressed in mm
receptacle_height = 175
#receptacle_clearance: how far to move the basket clear of the receptacle. Distance from receptacle_location_expel_z in mm
receptacle_clearance = 85

[clean_cycle_wash]
#duration: expressed in seconds
duration = 300
#mode: agitate or spin
mode = agitate
# if wash mode is spin
# direction: forward or backward
# direction
#speed: expressed as a percentage of max motor RPMs
# speed = 0
# if wash mode is agitate
#speed_forward: expressed as a percentage of max motor RPMs
speed_forward = 70
#time_forward:expressed in seconds
time_forward = 1
#time_brake_forward: expressed in seconds
time_brake_forward = 0.5
#speed_backward: expressed as a percentage of max motor RPMs
speed_backward = 70
#time_backward: expressed in seconds
time_backward = 1
#time_brake_backward: expressed in seconds
time_brake_backward = 0.5

[clean_cycle_expel]
#duration: expressed in seconds
duration = 120
#mode: agitate or spin
mode = spin
# if wash mode is spin
# direction: forward or backward
direction = forward
#speed: expressed as a percentage of max motor RPMs
speed = 100
# if wash mode is agitate
#speed_forward: expressed as a percentage of max motor RPMs
# speed_forward = 30
#time_forward:expressed in seconds
# time_forward = 1
#time_brake_forward: expressed in seconds
# time_brake_forward = 0.5
#speed_backward: expressed as a percentage of max motor RPMs
# speed_backward = 30
#time_backward: expressed in seconds
# time_backward = 1
#time_brake_backward: expressed in seconds
# time_brake_backward = 0.5

[rinse_cycle_1]
receptacle_location_x = 247
receptacle_location_y = 87
#receptacle_location_wash_z: fluid location in receptacle
receptacle_location_wash_z = 155
#receptacle_location_expel_z: space in receptacle to expel fluid
receptacle_location_expel_z = 235
#receptacle_height: expressed in mm
receptacle_height = 175
#receptacle_clearance: how far to move the basket clear of the receptacle. Distance from receptacle_location_expel_z in mm
receptacle_clearance = 85

[rinse_cycle_1_wash]
#duration: expressed in seconds
duration = 180
#mode: agitate or spin
mode = agitate
# if wash mode is spin
# direction: forward or backward
# direction
#speed: expressed as a percentage of max motor RPMs
# speed = 0
# if wash mode is agitate
#speed_forward: expressed as a percentage of max motor RPMs
speed_forward = 70
#time_forward:expressed in seconds
time_forward = 1
#time_brake_forward: expressed in seconds
time_brake_forward = 0.5
#speed_backward: expressed as a percentage of max motor RPMs
speed_backward = 70
#time_backward: expressed in seconds
time_backward = 1
#time_brake_backward: expressed in seconds
time_brake_backward = 0.5

[rinse_cycle_1_expel]
#duration: expressed in seconds
duration = 120
#mode: agitate or spin
mode = spin
# if wash mode is spin
# direction: forward or backward
direction = forward
#speed: expressed as a percentage of max motor RPMs
speed = 100
# if wash mode is agitate
#speed_forward: expressed as a percentage of max motor RPMs
# speed_forward = 30
#time_forward:expressed in seconds
# time_forward = 1
#time_brake_forward: expressed in seconds
# time_brake_forward = 0.5
#speed_backward: expressed as a percentage of max motor RPMs
# speed_backward = 30
#time_backward: expressed in seconds
# time_backward = 1
#time_brake_backward: expressed in seconds
# time_brake_backward = 0.5

[rinse_cycle_2]
receptacle_location_x = 247
receptacle_location_y = 248
#receptacle_location_wash_z: fluid location in receptacle
receptacle_location_wash_z = 155
#receptacle_location_expel_z: space in receptacle to expel fluid
receptacle_location_expel_z = 235
#receptacle_height: expressed in mm
receptacle_height = 175
#receptacle_clearance: how far to move the basket clear of the receptacle. Distance from receptacle_location_expel_z in mm
receptacle_clearance = 85

[rinse_cycle_2_wash]
#duration: expressed in seconds
duration = 180
#mode: agitate or spin
mode = agitate
# if wash mode is spin
# direction: forward or backward
# direction
#speed: expressed as a percentage of max motor RPMs
# speed = 0
# if wash mode is agitate
#speed_forward: expressed as a percentage of max motor RPMs
speed_forward = 70
#time_forward:expressed in seconds
time_forward = 1
#time_brake_forward: expressed in seconds
time_brake_forward = 0.5
#speed_backward: expressed as a percentage of max motor RPMs
speed_backward = 70
#time_backward: expressed in seconds
time_backward = 1
#time_brake_backward: expressed in seconds
time_brake_backward = 0.5

[rinse_cycle_2_expel]
#duration: expressed in seconds
duration = 120
#mode: agitate or spin
mode = spin
# if wash mode is spin
# direction: forward or backward
direction = forward
#speed: expressed as a percentage of max motor RPMs
speed = 100
# if wash mode is agitate
#speed_forward: expressed as a percentage of max motor RPMs
# speed_forward = 30
#time_forward:expressed in seconds
# time_forward = 1
#time_brake_forward: expressed in seconds
# time_brake_forward = 0.5
#speed_backward: expressed as a percentage of max motor RPMs
# speed_backward = 30
#time_backward: expressed in seconds
# time_backward = 1
#time_brake_backward: expressed in seconds
# time_brake_backward = 0.5

[dry_cycle]
receptacle_location_x = 87
receptacle_location_y = 248
#receptacle_location_wash_z: fluid location in receptacle
receptacle_location_wash_z = 300
#receptacle_location_expel_z: space in receptacle to expel fluid
receptacle_location_expel_z = 235
#receptacle_height: expressed in mm
receptacle_height = 175
#receptacle_clearance: how far to move the basket clear of the receptacle. Distance from receptacle_location_expel_z in mm
receptacle_clearance = 85

[dry_cycle_spin]
#duration: expressed in seconds
duration = 360
#mode: agitate or spin
mode = spin
# if wash mode is spin
# direction: forward or backward
direction = forward
#speed: expressed as a percentage of max motor RPMs
speed = 25
# if wash mode is agitate
#speed_forward: expressed as a percentage of max motor RPMs
# speed_forward = 30
#time_forward:expressed in seconds
# time_forward = 1
#time_brake_forward: expressed in seconds
# time_brake_forward = 0.5
#speed_backward: expressed as a percentage of max motor RPMs
# speed_backward = 30
#time_backward: expressed in seconds
# time_backward = 1
#time_brake_backward: expressed in seconds
# time_brake_backward = 0.5