1000 Year Clock Build-Diary

Lockdown Coding

Well the inevitable happened, and NZ went back into lockdown again.

We’ve managed to be Covid free apart from a one month lockdown in April 2020. We got lucky basically – we managed to lockdown fast enough that everyone was on board before the Tory dickheads started lying about it, so we managed to get rid of it completely.

Well now it’s back – we went into lockdown at midnight, with 4 hours notice.

So… mainly planning, designing and coding.

I had an initial run at the structure of the code with Javascript (rather than Python) because I’ve never used Python before, and I figured it was better to get the semantics sorted out, so when I did move to Python, it would purely be a matter of syntax-error fixing.

I started programming computers in 1979. Leave me alone. I know what I’m doing.

This is the javascript:

jQuery( function(){ 
		
		
		var wheels={
			'minutes' : {multiplier:1, units:60,location:0, fn:'d.getMinutes()'},	// multiplier increases the number of cog holes, allowing the cog to actually function
			'hours' : {multiplier:2, units:24,location:0, fn:'d.getHours()'},
			'days' : {multiplier:2, units:31,location:0, fn:'d.getDate()'},
			
			'week_days' : {multiplier:9, units:7,location:0, fn:'d.getDay()'},		// 7 for example, is not enough. 
			'months' : {multiplier:2, units:12,location:0, fn:'d.getMonth()'},
			'years' : {multiplier:2, units:10,location:0, fn:'parse_year(3)'},
			
			'decades' : {multiplier:2, units:10,location:0, fn:'parse_year(2)'},
			'centuries' : {multiplier:2, units:10,location:0, fn:'parse_year(1)'},
			'millenia' : {multiplier:2, units:10,location:0, fn:'parse_year(0)'},
		};
		
		
		var week_names = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
		var month_names = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
		
		
		var month_lengths=[31,28,31,30,31,30,31,31,30,31,30,31];  // give or take a leap year
		
		var steps_per_mm=12;			// lead-screw stepper characteristics
		
		var steps_per_rotation=200;		// cog stepper characteristics
		
		var wheel_and_gap_width=30;		// in mm
		
		var setTimeout_loop;			// give the loop a name so it can be stopped
		
		var hole_offset=10;				// distance from edge of wheel to holes
		
		var current_wheel_position;		// save us from going to the limit switch each time Most of the time it will be over the minutes wheel
		
		
		

		// for js demo
		var rate=$('#rate').val();
		$('#rate').change(function(){rate=$('#rate').val();})								// the clock speed
		for (k in wheels) wheels[k].location=Math.floor((Math.random() * wheels[k].units)); // randomise the positions
		
		
		// get it to do it's boot-sequence on command
		
		$('#boot').click(function(){
		
			clearTimeout(setTimeout_loop);	
			
			initialise_wheels();
			increment_time();
		})

		// go !

		initialise_wheels(); 
		increment_time();		// will subsequently call itself forever
		
	/*
	|
	| parse_year
	|----------------------------------------------------------------------------------------------------------------------------------
	|
	|	return nth year digit
	*/	
	function parse_year(n){
		
		let d = new Date();
	
		return d.getFullYear().toString().charAt(n);
	}

	
	/*
	|
	| travel_x
	|----------------------------------------------------------------------------------------------------------------------------------
	|
	|	move gantry to required wheel 
	*/
	function travel_x(wheel_number){	
		
		current_wheel_position=wheel_number;

		var num_steps=(wheel_number * wheel_and_gap_width-hole_offset) * steps_per_mm;
		
		if (wheel_number==0){ 				// initialisation ; travel back until it hits the limit switch
			
			num_steps=(wheel_and_gap_width-hole_offset) * steps_per_mm;  	// then over to minutes
			current_wheel_position=1;
		}
			
		
		// some code to make it actually move.
		// p(num_steps);
	
	}
	
	
	/*
	|
	| rotate
	|----------------------------------------------------------------------------------------------------------------------------------
	|
	|	move a wheel
	*/
	function rotate(k,direction){		

				
		// some code to make it actually move - move "multiplier" number of turns
		//	p(wheels[k].multiplier + ' => ' + wheels[k].location);
		// the cog always returns to the flyover state. It might not be a bad idea to check this with the hall-effect sensor

	}	
	
	
	/*
	|
	| initialise_wheels
	|----------------------------------------------------------------------------------------------------------------------------------
	|
	| 
	*/
	function initialise_wheels(){
		
		
		initialise_cog();		// important to do this first or it will crash. 
								// it means the hall sensor needs to be on the shaft up near the motors so can be initialised from anywhere
								// it's ok if it inadvertently turns a cog here. We're initialising

		travel_x(0);			// go to limit switch, then minutes 
						
		let d = new Date();		// this is needed for the eval function
		
		var current;			// variable for current time/date unit, which it gets from the eval function
		
		var wheel_number=1;							// refers to physical wheels, so starts at 1
		
				
		for (k in wheels){							//  spins the wheels backwards to zero, then forwards to the correct time
			
			travel_x(wheel_number);					// move gantry to correct point
			
			for(; wheels[k].location > 0; wheels[k].location-- ){	// zero the wheel
				
				rotate(k,-1);						// the wheels go in both directions -1 == backwards
				output(); 							// why the actual fuck does this not refresh the dom in real time? 
			}
			
			
			current=eval('' + wheels[k].fn);		// get the current date/time for each unit. Kindof hacky. I don't like eval
			
			for(; wheels[k].location < current; wheels[k].location++ ){	// spin the wheel to the current time
			
				rotate(k,1);
				output(); 
			}
			
			wheel_number++;
		}	
	}
	
	
	

	/*
	|
	| initialise_cog
	|----------------------------------------------------------------------------------------------------------------------------------
	|
	| 
	*/
	function initialise_cog(){

		// turn cog until hall-effect sensor triggered. This is the flyover state
	}	
	
	
	/*
	|
	| get_time_from_web
	|----------------------------------------------------------------------------------------------------------------------------------
	|	
	| 	this sets system time of Pi. 
	*/
	function get_time_from_web(){
		
		// if it can't get to web, it just uses the Pi's time.
		// the clock itself is basically just a complicated way of displaying Pi-time. 
		
	}	

	
	/*
	|
	| increment_time
	|----------------------------------------------------------------------------------------------------------------------------------
	|	
	|	called once a minute. Needs to do it whether or not connected to the web.
	|	
	|	Current time is stored in wheels.location
	|
	*/
	function increment_time(){
	
		var stop_counting=0;
		
		for (k in wheels){
			
			switch(k){
				
				case 'week_days': break;	// handled by days below
			
				case 'days' :	
							
					wheels[k].location++;
					rotate(k,1); 
					
					if (wheels['months'].location==1){		// February? 
						
						let d = new Date();
						var year=d.getFullYear();
						var is_leapyear=((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0);  // python might have a better way of doing this	
						
						if(!is_leapyear && wheels[k].location <=28) stop_counting=1; 
						if(is_leapyear && wheels[k].location <=29) stop_counting=1;  
					
					}else{
					
						if (wheels[k].location <= month_lengths[wheels['months'].location]) stop_counting=1;
					}
					
					// also do the weekday names
					wheels['week_days'].location=(wheels['week_days'].location==wheels['week_days'].units)?0:wheels['week_days'].location+1;
					rotate(k,1); 
						
				break;
						
				default:
						
					wheels[k].location++;
					rotate(k,1); 
					
					if (wheels[k].location <= wheels[k].units) stop_counting=1;
						
				break;
				
			} // switch statement ends
				
			if (stop_counting==1) break;	// bail on loop
			
			// else
			
			wheels[k].location=0; // otherwise set location to zero and move on to the next wheel	
			
		}	// for(k in wheels) loop ends
		
	

		output();		// for demo
		
		setTimeout_loop=setTimeout(function(){ increment_time();}, rate); // python might have a better way of doing this. One can but dream.

	}	





	/*
	|
	| output JS demo only. 
	|----------------------------------------------------------------------------------------------------------------------------------
	|	
	|
	*/
	function output(){

	//  raw output	
	//	var str='';
	//	for (key in wheels) str+= (str==''?'':', ') + key + ":" + wheels[key].location;		
	
		var str = wheels['hours'].location + ':' + wheels['minutes'].location + ' ' + week_names[wheels['week_days'].location];
		str+= ' ' + wheels['days'].location + ' ' + month_names[wheels['months'].location];
		str+= ' ' + wheels['millenia'].location+wheels['centuries'].location+wheels['decades'].location+wheels['years'].location;

		
		console.log(str); 				// this outputs	to log in real time
		
		$('#date_time').html(str);		// this only outputs when the whole function calling it has run. Pain in the arse? 
			
	}	

	
	// misc debugging functions
	
	function sleep(milliseconds) {
	  const date = Date.now();
	  let currentDate = null;
	  do {
		currentDate = Date.now();
	  } while (currentDate - date < milliseconds);
	}	
	
	function p(str){console.log(str);}
	
})
	


Which does something like this : https://neoiko.com/movement.html

Which is not especially exciting I suppose. It’s supposed to start off with a random date, then do a quick “boot-sequence” where it zeroes all of the wheels, but Javascript (20 years later) is still one of my least favourite languages, and dammed if I’m going to spend a fuck-ton of time trying to figure out how to output things to the screen in real-time because it’s got some weirdo way of prioritising things.

Look how beautiful the code is tho. The best coding advice I ever received was “code like a girl”… which I took to mean “make everything beautiful, with special attention to indentation, coding-conventions etc etc”. I don’t mean that in any mean or derogatory sense. Lest we forget:

https://www.wired.com/2015/10/margaret-hamilton-nasa-apollo/

And the poor little component below comes from Adafruit.com and she seriously knows what she’s doing. Me regrettably, not so much.

Anyway – coding like a girl makes aesthetics and readability a high priority as a during-developent goal, and trust me, it seriously speeds things up, and makes the whole experience more enjoyable than if the whole thing is an epic shambles like (for example) my workshop, which I shall tidy as soon as we get out of lockdown.

So… over to Python. Took about 4 hours to translate, which is not bad going considering I’ve never used Python before. Neat little language. I really like it. It has the feel of “a freshly re-written code-base”.

I haven’t gone too deeply into pin-assignment because of the theramin situation (which I shall explain in a later post)… I’m going to use Python Stepper hats as drivers – which have their own libraries etc, so this particular bit of code will change anyway.

Unfortunately the one I attempted to use (before I even got to try the code), accidentally (and literally) caught fire so now I have to wait another couple of weeks for another one to turn up. There doesn’t appear to be anyone in New Zealand who sells them… apart from the Protoneer guy, and while I guess that would work, it kindof seems like overkill. I use one of those to drive my CNC machine.

Anyhoo… the first casualty:

Poor little thing. It never stood a chance.

Anyway – the Python code (for those lost souls who are interested in such things) is:

# hello_psg.py

import PySimpleGUI as sg
import datetime
import random

import RPi.GPIO as GPIO
from time import sleep



#
#
# Clock Class
#----------------------------------------------------------------------------------------------------------------------------------
#
# 
class Clock:
	def __init__(self):
		
		# k - is used as the generic key for this object throughout this class. 
		# self.wheels[k]['location'] is the current time for each wheel stored as an integer
		
		self.wheels={
		'minutes' : {'multiplier':1, 'units':60,'location':0, 'format_code':'%M'},				# multiplier increases the number of cog holes, allowing the cog to actually function
		'hours' : {'multiplier':2, 'units':24,'location':0, 'format_code':'%H'},
		'days' : {'multiplier':2, 'units':31,'location':0, 'format_code':'%d'},

		'week_days' : {'multiplier':9, 'units':7,'location':0, 'format_code':'%w'},				# 7 for example, is not enough cog-holes to turn a wheel 
		'months' : {'multiplier':2, 'units':12,'location':0, 'format_code':'%m'},
		'years' : {'multiplier':2, 'units':10,'location':0, 'format_code':'3'},

		'decades' : {'multiplier':2, 'units':10,'location':0, 'format_code':'2'},
		'centuries' : {'multiplier':2, 'units':10,'location':0, 'format_code':'1'},
		'millenia' : {'multiplier':2, 'units':10,'location':0, 'format_code':'0'}
		}
		
		
		self.week_names = ['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday']
		self.month_names = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
		                                                                                                    
		
		self.month_lengths=[31,28,31,30,31,30,31,31,30,31,30,31]  # give or take a leap year
		
		self.steps_per_mm=12			# lead-screw stepper characteristics
		
		self.steps_per_rotation=200		# cog stepper characteristics
		
		self.wheel_and_gap_width=30		# in mm
		
		self.setTimeout_loop=''			# give the loop a name so it can be stopped
		
		self.hole_offset=10				# distance from edge of wheel to holes
		
		self.current_wheel_position=0		# save us from going to the limit switch each time Most of the time it will be over the minutes wheel

		self.millisecs_second=10			# for demo purposes so can change speed of the clock so we can actually see it working



		# pins
		
		self.step_pin_gantry=20						
		self.direction_pin_gantry=21
		self.delay_gantry=.2
		
		self.step_pin_cog=20						
		self.direction_pin_cog=21
		self.delay_cog=.2


		self.window = sg.Window('1000 Year Clock', [
			[sg.Text('Time: '), sg.Text('', size=(30,1), key='_time_')],
			[sg.Exit()]
		],size=(300, 200))

	#
	#
	# rotate_wheel
	#----------------------------------------------------------------------------------------------------------------------------------
	#
	# rotate a wheel by one unit
	#
	
	def rotate_wheel(self,k,direction):
		

		self.travel_to(k)			# move cog so it's over relevant wheel
		
		num_steps=self.steps_per_rotation * self.wheels[k]['multiplier']
		
		
		GPIO.setwarnings(False)
		GPIO.cleanup()
		GPIO.setmode(GPIO.BCM)

		GPIO.setup(self.step_pin_cog, GPIO.OUT)
		GPIO.output(self.step_pin_cog,GPIO.LOW)
		
		GPIO.setup(self.direction_pin_cog, GPIO.OUT)
		GPIO.output(self.direction_pin_cog,direction)
		
		for x in range(num_steps):
			
			GPIO.output(self.step_pin_cog,GPIO.HIGH)
			sleep(self.delay_cog)
			GPIO.output(self.step_pin_cog,GPIO.LOW)
			sleep(self.delay_cog)
			print (x)
			
		GPIO.cleanup(
		
		self.output()				# output to screen

		

	#
	#
	# travel_to
	#----------------------------------------------------------------------------------------------------------------------------------
	#
	# move gantry to required wheel 
	#
	#    
	#
	
	def travel_to(self,k):
		
		
		if k=="initialise":
			pass						# pull gantry back until it triggers the limit-switch (hall effect)
										# then move to minutes
		else:
			
			destination=list(self.wheels).index(k) 	# get the numeric position of the destination wheel
					
			if destination==self.current_wheel_position: # 99% of the time this will happen
				return		
				
			
			self.disengage_cog()			# don't move the gantry without disengaging the cog			
			
			num_steps=0	# default in case we're already at the right location
			
			if self.current_wheel_position < destination:
				
				num_steps=(destination-self.current_wheel_position) * self.wheel_and_gap_width * self.steps_per_mm
				

			if self.current_wheel_position > destination:
				
				num_steps=(self.current_wheel_position-destination) * self.wheel_and_gap_width * self.steps_per_mm
				
			
		
			self.current_wheel_position=destination

	#
	#
	# disengage_cog
	#----------------------------------------------------------------------------------------------------------------------------------
	#
	# rotate until the magnet is over the hall-effect sensor 
	
	
	def disengage_cog(self):
		
		return ''

	#
	#
	# initialise_wheels
	#----------------------------------------------------------------------------------------------------------------------------------
	#
	# 
	
	def initialise_wheels(self):
		
		# randomize them so we've got something interesting for the boot sequence to do :: IRL this will be governed by hall-effect sensors
		for k,v in self.wheels.items():
			self.wheels[k]['location']=random.randint(0, self.wheels[k]['units']-1)		
		

		self.travel_to('initialise')						
		
		
		current=self.wheels[k]['location']		# store the current value so we can change wheels[k]

				
		for k,v in self.wheels.items():			# loop through wheels
			
			for self.wheels[k]['location'] in range(current,-1,-1):			# count backwards to zero from wherever the wheel currently is
				self.rotate_wheel(k,-1)

			
			
			if self.wheels[k]['format_code'].find('%') ==0:
				
				now=int(datetime.datetime.now().strftime(self.wheels[k]['format_code']))
			else:
				now=int(str(datetime.datetime.now().strftime('%Y'))[int(self.wheels[k]['format_code'])])  
				
				
			a=0 if (k=='week_days' or k=='months') else 1	
				
			for self.wheels[k]['location'] in range(a,now+a):				# count backwards to zero from wherever the wheel currently is
				self.rotate_wheel(k,1)
			

    
			
		# The main loop
		# Once a day it should drop out of this and re-initialise
			
		while True:
			
			event,values = self.window.Read(timeout=1000)
			
			if event in (None, 'Exit'):
				break
				
			self.increment_time()
				
		self.window.Close()	
			
			
			
			
	#
	#
	# increment_time
	#----------------------------------------------------------------------------------------------------------------------------------
	#
	# called once a minute. Needs to do it whether or not connected to the web.
	#	
	# Current time is stored in wheels.location 
	
	def increment_time(self):		
			
		stop_counting=0

		year=int(datetime.datetime.now().strftime('%Y'))
	
		is_leapyear=0
		
		if (( year%400 == 0)or (( year%4 == 0 ) and ( year%100 != 0))):
			is_leapyear = 1				

		

		for k,v in self.wheels.items():			# loop through wheels

			if k=='week_days':
			
				pass
			
			elif k=='days':
				
				self.wheels[k]['location']+=1
				
				if self.wheels['months']['location']==1: 		#february? 
							
					if is_leapyear==0 and self.wheels[k]['location'] <=28:
						stop_counting=1; 
						
					if is_leapyear==1 and wheels[k].location <=29:
						stop_counting=1;  
					
				if self.wheels[k]['location'] <= self.wheels[k]['units']:
					stop_counting=1;	
					
				self.rotate_wheel(k,1);
				 
				
				if self.wheels['week_days']['location']>=self.wheels['week_days']['units']:
					self.wheels['week_days']['location']=0
				else:
					self.wheels['week_days']['location']+=1
				
					
				self.rotate_wheel('week_days',1); 

			else:                               
				
				self.wheels[k]['location']+=1
				self.rotate_wheel(k,1); 
				
				if self.wheels[k]['location'] <= self.wheels[k]['units']:
					stop_counting=1;

			self.output()

			if stop_counting==1:		# bail on loop
				break 
			
			else:
				self.wheels[k]['location']=0	#otherwise set location to zero and move on to the next wheel

				

	#
	#
	# output
	#----------------------------------------------------------------------------------------------------------------------------------
	#
	# output to screen for the purpose of demo
	# 
	
	def output(self):

		
		ss = str(self.wheels['hours']['location']) + ':' + str(self.wheels['minutes']['location'])  + ' ' + self.week_names[self.wheels['week_days']['location']]
		ss+= ' ' + str(self.wheels['days']['location']) + ' ' + self.month_names[self.wheels['months']['location']]
		ss+= ' ' + str(self.wheels['millenia']['location']) + str(self.wheels['centuries']['location']) + str(self.wheels['decades']['location']) + str(self.wheels['years']['location'])

		event, values = self.window.Read(timeout=100)

		if event in (None, 'Exit'):
			self.window.Close()	
				
		self.window.find_element('_time_').Update(ss)
		




clock = Clock()

clock.initialise_wheels()

My fave line in the whole thing is

from time import sleep

I might try and work that into a song or something.

Later.

Now. Lockdown. Lockdown lockdown lockdown. Lockdown lockdown lockdown lockdown lockdown lockdown lockdown.

429th day of lockdown, relaxing at home with family and friends

Leave a Reply

Your email address will not be published. Required fields are marked *