Skip to content

Commit

Permalink
Using config fully now instead of constants
Browse files Browse the repository at this point in the history
except for the initial stepper delay.. need to calculate that then calibrate from it.
  • Loading branch information
jewzaam committed Feb 7, 2021
1 parent 1d4a3fe commit a94fb64
Show file tree
Hide file tree
Showing 4 changed files with 201 additions and 103 deletions.
55 changes: 55 additions & 0 deletions src/microPython/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import os.path
import time, datetime
import yaml

CONFIG_FILENAME="stepper.yaml"

def save(config):
# update history
if 'calculated_ms' in config['delays']:
latest=None
if len(config['history']) > 0:
latest=config['history'][0]

# assume structure of history is valid

# create new history if any delays have changed OR we don't have any history
create_history=False
if latest is not None:
for key in config['delays'].keys():
h_value=latest[key]
d_value=config['delays'][key]
if h_value != d_value:
create_history=True
break
else:
create_history=True

if create_history:
new_history={}
new_history['timestamp']=datetime.datetime.fromtimestamp(time.time()).strftime('%Y-%m-%d %H:%M:%S')
for key in config['delays'].keys():
new_history[key]=config['delays'][key]
config['history'].insert(0, new_history)

with open(CONFIG_FILENAME, 'w') as outfile:
yaml.dump(config, outfile, default_flow_style=False)

def load():
config={}
if os.path.exists(CONFIG_FILENAME):
with open(CONFIG_FILENAME, 'r') as infile:
config=yaml.load(infile, Loader=yaml.FullLoader)

# initialize requrired child objects and arrays
objects = ['pins','delays']
arrays = ['history']

for obj in objects:
if obj not in config:
config[obj]={}
for arr in arrays:
if arr not in config:
config[arr]=[]

return config
185 changes: 100 additions & 85 deletions src/microPython/main.py
Original file line number Diff line number Diff line change
@@ -1,49 +1,34 @@
from machine import Pin
import utime as time
import math

# WARNING printing on the pico makes a big difference in sleep accuracy. use debug with caution!
debug = False

# global total steps taken
step=0

# Order in datasheet: orange, yellow, pink, blue, red (always 0)
# https://components101.com/motors/28byj-48-stepper-motor

gpio_power = 18
pin_power = Pin(gpio_power, Pin.OUT)
pin_led = Pin(25, Pin.OUT)

# pins in order per data sheet: orange, yellow, pink, blue
gpio_coils=[28,27,26,22]
pin_coils=[
Pin(gpio_coils[0], Pin.OUT),
Pin(gpio_coils[1], Pin.OUT),
Pin(gpio_coils[2], Pin.OUT),
Pin(gpio_coils[3], Pin.OUT)
]

# adjust if different
Sequence = [
[1,0,0,0],
[1,1,0,0],
[0,1,0,0],
[0,1,1,0],
[0,0,1,0],
[0,0,1,1],
[0,0,0,1],
[1,0,0,1],
]
SequenceCount = len(Sequence)
import ujson as json

CONFIG_FILENAME="stepper.json"
pins={}
config={}
totals={
'enabled': False,
'ms': 0,
'steps': 0
}

def load_config():
config={}
with open(CONFIG_FILENAME, 'r') as f:
config=json.loads(f.read())
return config

def tick():
# get some tick metric, diff_ms must handle the unit
return time.ticks_us()

def diff_ms(now, then):
global totals
# see tick() for units
return time.ticks_diff(now, then) / 1000.0
diff_ms=time.ticks_diff(now, then) / 1000.0
if totals['enabled']:
totals['ms']+=diff_ms
return diff_ms

def usleep(us,accuracy=0.99):
start = time.ticks_us()
Expand All @@ -58,40 +43,60 @@ def usleep(us,accuracy=0.99):
return now

def startup():
global pins,config

print("STARTUP")
pin_led.on()
pin_power.on()

def shutdown(step_delay_ms,start_tick):
for pin in pin_coils:
pin.off()
pin_power.off()
pin_led.off()

has_startup=False

for key in config['gp_out'].keys():
value=config['gp_out'][key]
if key == "startup_gp_on":
# special case, we handle it later
has_startup=True
elif isinstance(value,list):
pins[key]=[]
for g in value:
pins[key].append(Pin(g, Pin.OUT))
else:
pins[key]=Pin(value, Pin.OUT)

if has_startup:
for key in config['gp_out']['startup_gp_on']:
pins[key].on()

def shutdown(step_delay_ms):
global config

# Shut it down!
for key in pins.keys():
value=pins[key]
if isinstance(value,list):
for pin in value:
pin.off()
else:
pins[key].off()

# print how we did, in case someone is watching
now_tick=tick()
total_time_ms=diff_ms(now_tick,start_tick)
total_delta_ms=total_time_ms - (step * step_delay_ms)

if total_time_ms > 0.0:
total_error_p=math.floor(total_delta_ms/total_time_ms*10000.0)/100.0
print("total_steps: {}".format(totals['steps']))
if totals['ms'] > 0:
total_delta_ms=totals['ms'] - (totals['steps'] * step_delay_ms)
total_error_p=math.floor(total_delta_ms/totals['ms']*10000.0)/100.0
print("total_ms: {}".format(totals['ms']))
print("total_delta_ms: {}".format(total_delta_ms))
print("total_error: {} %".format(total_error_p))
else:
total_error_p="UNKNOWN"

print("total_steps: {}".format(step))
print("total_time_ms: {}".format(total_time_ms))
print("total_delta_ms: {}".format(total_delta_ms))
print("total_error: {} %".format(total_error_p))
print("WARNING: not enough time to collect overall timing metrics")
print("")
print("SHUTDOWN")

def setOutput(values):
i = 0
for pin in pin_coils:
pin.value(values[i])
i = i + 1
for i in range(0, len(values)):
pins['stepper'][i].value(values[i])

def calibrate_delay(expected_duration_ms,step_delay_ms,step_count,start_tick,end_tick):
global config

# default to the current delay, in case we decide not to calculate a new one
calibrated_delay_ms = step_delay_ms

Expand All @@ -101,7 +106,7 @@ def calibrate_delay(expected_duration_ms,step_delay_ms,step_count,start_tick,end
# how far off is the actual from expected:
calibration_factor = actual_duration_ms / expected_duration_ms

if debug:
if config['debug']:
print("expected_duration_ms: {}".format(expected_duration_ms))
print("actual_duration_ms: {}".format(actual_duration_ms))
print("step_delay_ms: {}".format(step_delay_ms))
Expand All @@ -120,18 +125,22 @@ def calibrate_delay(expected_duration_ms,step_delay_ms,step_count,start_tick,end
# average the current delay and calculated to try to reduce oscillation
calibrated_delay_ms = (calculated_delay_ms + step_delay_ms) / 2

if debug:
if config['debug']:
print("ahead_ms: {}".format(actual_duration_ms - expected_duration_ms))
print("momentary_error: {} %".format(math.floor((1-actual_duration_ms/expected_duration_ms)*10000.0)/100.0))
print("calibrated_delay_ms: {}".format(calibrated_delay_ms))

if debug:
if config['debug']:
print("")

return calibrated_delay_ms

def forward(calibration_target,step_delay_ms,start_tick):
global step
def forward(step_delay_ms):
global config,totals

calibration_target=config['calibration']['accuracy']
sequence=config['tracker']['stepper']['sequence']
sequence_len=len(sequence)

# the value we get for step delay is our ideal / target value
# it will be recalcuated to adjust for real time as we can't get 100% accurate by just saying "sleep X ms"
Expand All @@ -144,21 +153,23 @@ def forward(calibration_target,step_delay_ms,start_tick):
# * calibration cycle aligns with sequence (don't cut a sequence in half!)

# how many steps to use for each calibration
calibration_frequency = 50 * SequenceCount
calibration_frequency = 50 * sequence_len

calibration_start_tick = start_tick
step = 1
calibration_start_tick = tick()
calibration_step = 1

if debug:
if config['debug']:
print("calibration_start_tick: {}".format(calibration_start_tick))
print("calibration_frequency: {}".format(calibration_frequency))
print("")

while True:
setOutput(Sequence[step%SequenceCount])
# get the values for each stepper coil and set them
sequence_values=sequence[calibration_step%sequence_len]
for i in range(0, len(sequence_values)):
pins['stepper'][i].value(sequence_values[i])

if step%calibration_frequency == 0:
if calibration_step%calibration_frequency == 0:
# calibrate_delay(expected_duration_ms,step_delay_ms,step_count,start_time_ms,end_time_ms):
expected_duration_ms = calibration_step * target_ms_per_step
now_tick = tick()
Expand All @@ -170,50 +181,54 @@ def forward(calibration_target,step_delay_ms,start_tick):
now_tick
)
# reset calibration window (be sure not to jump in the sequence, use modulus)
calibration_step = step % SequenceCount
calibration_step = calibration_step % sequence_len
calibration_start_tick = now_tick

# turn off Pico LED once we're within calibration target
calibration_p=abs(1.0 - (step_delay_ms / calibrated_delay_ms))
if calibration_p < calibration_target:
pin_led.off()
# start the "totals" (we only care about post-calibration)
totals['enabled']=True
# and turn off any pins we should turn off
for pin_key in config['calibration']['calibrated_gp_off']:
pins[pin_key].off()

step_delay_ms = calibrated_delay_ms
if debug:
if config['debug']:
# useful overall stats for verifying calibration is doing what it should: keep the TOTAL drift (delta) down.
# note this is always shown on shutdown even if debug is disabled
total_time_ms=diff_ms(now_tick,start_tick)
total_delta_ms=total_time_ms - (step * target_ms_per_step)
total_delta_ms=totals['ms'] - (calibration_step * target_ms_per_step)

if total_time_ms > 0.0:
total_error_p=math.floor(total_delta_ms/total_time_ms*10000.0)/100.0
if totals['ms'] > 0.0:
total_error_p=math.floor(total_delta_ms/totals['ms']*10000.0)/100.0
else:
total_error_p="UNKNOWN"

print("total_steps: {}".format(step))
print("total_time_ms: {}".format(total_time_ms))
print("total_steps: {}".format(totals['steps']))
print("total_ms: {}".format(totals['ms']))
print("total_delta_ms: {}".format(total_delta_ms))
print("total_error: {} %".format(total_error_p))
print("")

# use custom sleep, it's a busy wait. specify accuracy < 100% else all sleeps are LONGER than target. 99% is pretty good
usleep(step_delay_ms*1000.0,0.99)
# increment global step, used only for "total" debug and shutdown info
step = step + 1
if totals['enabled']:
totals['steps']+=1
# increment the calibration step
calibration_step = calibration_step + 1

if __name__ == '__main__':
# TODO load the initial step delay from configuration
calibration_target=0.001
step_delay_ms=4.633737
start_tick=tick()
config=load_config()

try:
# fire up power and pico's LED (just so we know it should be doing something)
startup()
forward(calibration_target,step_delay_ms,start_tick)
forward(step_delay_ms)
except KeyboardInterrupt:
pass
finally:
# abort abort! turn off all output and print metrics
shutdown(step_delay_ms,start_tick)
shutdown(step_delay_ms)
46 changes: 46 additions & 0 deletions src/microPython/stepper.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"debug": false,
"gp_out": {
"v5": 18,
"led": 25,
"stepper":[28,27,26,22],
"startup_gp_on":["v5","led"]
},
"gp_in": {
"forward": -1,
"reverse": -1,
"stop": -1,
"slower": -1,
"faster": -1,
"lock": -1,
"unkock": -1
},
"controls": {
"hold_unlock_s": 5,
"hold_lock_s": 5,
"faster_increment": 0.1,
"slower_increment": 0.1,
"reverse_speed_multiplier": 4
},
"tracker": {
"radius": 200,
"pitch": 0.7874,
"gear_ratio": "10:43",
"stepper": {
"sequence": [
[1,0,0,0],
[1,1,0,0],
[0,1,0,0],
[0,1,1,0],
[0,0,1,0],
[0,0,1,1],
[0,0,0,1],
[1,0,0,1]
]
}
},
"calibration": {
"accuracy": 0.001,
"calibrated_gp_off": ["led"]
}
}
Loading

0 comments on commit a94fb64

Please sign in to comment.