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:
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.