Minimal data logging and sensor setup for Lincoln Utility Board

It is possible to run the Lincoln Utility Board (LUB) with a very small number of components:

  • The sockets for the RPi Pico chip.
  • The header pins for the SD card cable.
  • The I2C connection pins for the PiicoDev sensors and clock. The clock does not have a battery; rather a super capacitor. If an advanced student ever needs milliseconds via interrupt, then a clock with a glued in battery can be provided. (Coin batteries are poisonous if swallowed, so they are not available; they may accessible in rare cases, if glued.)
  • The socket for the PiicoDev display.

This is an easy way to get started and the programs work well. The sensors plug in with standard cables and for that part no soldering is required. The four steps above each require some basic soldering skills and this pre-built layout can be provided at an economical price. The LUB has space for a lot more components which allows the scope of the student projects to grow and include control of devices.

A micropython program is provided below. The driver python files used, along with the PiicoDev sensor and clock are available from Core-Electronics AU.

The PiicoDev I2C socket is on the board and the display is PiicoDev for compatibility.

This project is suitable for multiple levels at High School, depending on the approach. Plugging everything in is quite easy, along with running the provided program. The students would then examine the data that they collected.

More advanced students would edit the program and change the layout of the data file. They could even add another sensor, without too much difficulty. Advanced options are shown under Lincoln Railway.

More advanced again would be to graph the pressure, for instance, over a 2 day, then 7 day, run. Success would be confirmed by seeing the graph appear gradually.

At University biology students could use this setup with biological sensors, as the basic data capture method is straight forward.

Computer students at Uni could write interactive programs that use rotary encoder to manipulate the graphical display, of data that has been collected. It is also possible to use the RPi Pico W with WiFi. Ethernet options for a simpler board are also being developed.

  • MCU units with sensors and datalogging.
  • Break out boards with user interface. Suitable for datalogging and interactive function including stepper motors.
  • Break out board customizations. Sensors, stepper motors, WiFi, SD cards and Fram.
  • Design of circuit boards, including design of circuit boards that are a customization of a break out board pilot project.
  • WiFi and server collection of data.

Basic unit with SD card and holder

This is the basic Micropython unit in the image above, including the SD card and holder extras. You can buy PiicoDev (PD) along with some Adafruit and DFrobot I2C components. Often the same plugs and sockets are used but you need to check on a case by case basis for software compatibility. The safest thing to get started may be to use just PD sensors and devices. (If the non PD demo creates a machine.SoftI2C object and uses that to instantiate the device, this often crashes the PD devices; machine.I2C objects usually work fine.)

You can print the plastic holders yourself if you wish. Print cylindrical housing or print rectangular housing. I use JLCPCB’s service and opt for black nylon, as it has a high melting point. I have not printed these files before, only the previous ones, that have now been edited, where there were no holes to bolt in the reader. I recommend print only one unit to start with, in case there might some printing issues. I know that the unedited versions did print properly before: cylindrical housing and rectangular housing.

Fully populated board

A similar fully populated is shown above. It is possible to simply install the components that will be used. The millisecond timer sub-breakout board is not needed if seconds are sufficiently precise. If millisecond time is needed for students that the coin cell can be glued.

Auxiliary files for python script below

Acknowledgements

IDE (Integrated Development Environment) for Micropython

Thonny is uncomplicated and gives students a good start to using Python scripts. It is easy to upload libraries to the MCU, such as the RPi Pico and run the main program from the host computer. For more advanced students and more fully featured debugging, one can use Visual Studio Code, with the Python Add-on. Visual Studio Community itself, often fails to find libraries that are available and other similar problems occur with it, as well.

  • MCU units with sensors and datalogging.
  • Break out boards with user interface. Suitable for datalogging and interactive function including stepper motors.
  • Break out board customizations. Sensors, stepper motors, WiFi, SD cards and Fram.
  • Design of circuit boards, including design of circuit boards that are a customization of a break out board pilot project.
  • WiFi and server collection of data.

===================
main.py
===================
from machine import Pin, ADC
import sys
import gfx
import time
import sdcard
import uos
import PiicoDev_Unified
from PiicoDev_Unified import sleep_ms
from PiicoDev_RV3028 import PiicoDev_RV3028
from PiicoDev_BME280 import PiicoDev_BME280
from PiicoDev_SSD1306 import *
import math


adc = machine.ADC(4)
vpin = machine.ADC(29)

# Assign chip select (CS) pin (and start it high)
cs = machine.Pin(17, machine.Pin.OUT)

# Intialize SPI peripheral (start with 1 MHz)
spi = machine.SPI(0,
                  baudrate=1000000,
                  polarity=0,
                  phase=0,
                  bits=8,
                  firstbit=machine.SPI.MSB,
                  sck=machine.Pin(18),
                  mosi=machine.Pin(19),
                  miso=machine.Pin(16))

# Initialize SD card
sd = sdcard.SDCard(spi, cs)

# Mount filesystem
vfs = uos.VfsFat(sd)
uos.mount(vfs, "/sd")



rtc = PiicoDev_RV3028(0, None, 20, 21)
sensor = PiicoDev_BME280(0, None, 20, 21)
display = create_PiicoDev_SSD1306(0x3C, 0, None, 20, 21, None)
displaya = create_PiicoDev_SSD1306(0x3D, 0, None, 20, 21, 1)
oledg = gfx.GFX(128, 64, display.pixel)


display.fill(0) # empty the frame buffer
display.pixel(10,10,1)
display.show()
sleep_ms(1000)


display.fill(0) # empty the frame buffer
oledg.circle(40, 40, 10, 1)
display.show()
sleep_ms(1000)

display.fill(0) # empty the frame buffer
for x in range(6):
    oledg.ellipse(20+17*x, 15, 15, 7, 1)
oledg.printstring16(13,30,"Microtron", 1)
display.show()
sleep_ms(1000)



rtc.day = 4
rtc.month = 12
rtc.year = 2024
rtc.hour = 12
rtc.minute = 45
rtc.second = 00
rtc.ampm = '24' # 'AM','PM' or '24'. Defaults to 24-hr time
rtc.weekday = 4 # Rolls over at midnight, works independently of the calendar date
rtc.setDateTime() # Sets the time with the above values

# Get the current time
rtc.getDateTime()

print("sys.implementation:{}".format(sys.implementation))
print("sys.version:{}".format(sys.version))


while True:
    print(rtc.timestamp())

    ADC_voltage = adc.read_u16() * (3.3 / (65536))
    temperature_celcius = 27 - (ADC_voltage - 0.706)/0.001721
    temp_fahrenheit=32+(1.8*temperature_celcius)
    
    tempC, presPa, humRH = sensor.values() # read all data from the sensor
    pres_hPa = presPa / 100 # convert air pressurr Pascals -> hPa (or mbar, if you prefer)
    print(str(tempC)+" °C  " + str(pres_hPa)+" hPa  " + str(humRH)+" %RH")

    
    print("Temperature: {}°C {}°F".format(temperature_celcius,temp_fahrenheit))
    
    adc_reading  = vpin.read_u16()
    adc_voltage  = (adc_reading * 3.3) / 65535
    vsys_voltage = adc_voltage * 3
    
    print("""VSYS voltage:{}""".format(vsys_voltage))

    displaya.fill(0)
    displaya.text(rtc.timestamp(),10,5, 1)
    displaya.text("Pi:{0:.1f}C,".format(temperature_celcius) + "V:{0:.1f}".format(vsys_voltage),10,20, 1)
    displaya.text("T:{0:.1f}C,".format(tempC) + "P:{0:.1f}mbar".format(pres_hPa),10,35, 1)
    displaya.text("Hum: {0:.1f}%RH".format(humRH),10,50, 1)
    displaya.show()

    with open("/sd/test04.txt",'a') as f:
        f.write(rtc.timestamp() + "\n")
        f.write("Temperature: {}°C {}°F\n".format(temperature_celcius,temp_fahrenheit))
        f.write(str(tempC)+" °C  " + str(pres_hPa)+" hPa  " + str(humRH)+" %RH\n")
        f.write("VSYS voltage:{}\n\n".format(vsys_voltage))
    time.sleep_ms(500)
    
    


Here is some data collected on the SD card using this setup.


2024-11-22 13:55:02
Temperature: 16.28353°C 61.31035°F
20.12 °C  1010.873 hPa  49.40625 %RH
VSYS voltage:4.885873

2024-11-22 13:55:02
Temperature: 16.28353°C 61.31035°F
20.12 °C  1010.882 hPa  49.41895 %RH
VSYS voltage:4.900375

2024-11-22 13:55:03
Temperature: 16.28353°C 61.31035°F
20.12 °C  1010.875 hPa  49.42969 %RH
VSYS voltage:4.88829

2024-11-22 13:55:03
Temperature: 16.28353°C 61.31035°F
20.12 °C  1010.891 hPa  49.44043 %RH
VSYS voltage:4.890707

2024-11-22 13:55:04
Temperature: 15.81538°C 60.46769°F
20.12 °C  1010.898 hPa  49.45117 %RH
VSYS voltage:4.885873

2024-11-22 13:55:05
Temperature: 15.81538°C 60.46769°F
20.13 °C  1010.891 hPa  49.42969 %RH
VSYS voltage:4.900375

2024-11-22 13:55:05
Temperature: 15.81538°C 60.46769°F
20.13 °C  1010.866 hPa  49.44043 %RH
VSYS voltage:4.88829

2024-11-22 13:55:06
Temperature: 16.28353°C 61.31035°F
20.13 °C  1010.858 hPa  49.45215 %RH
VSYS voltage:4.883456

2024-11-22 13:55:07
Temperature: 16.28353°C 61.31035°F
20.13 °C  1010.885 hPa  49.44141 %RH
VSYS voltage:4.893124

2024-11-22 13:55:07
Temperature: 16.28353°C 61.31035°F
20.13 °C  1010.871 hPa  49.43066 %RH
VSYS voltage:4.881039

2024-11-22 13:55:08
Temperature: 16.28353°C 61.31035°F
20.13 °C  1010.864 hPa  49.41992 %RH
VSYS voltage:4.893124

2024-11-22 13:55:08
Temperature: 15.81538°C 60.46769°F
nan °C  nan hPa  nan %RH
VSYS voltage:4.893124

Servo Motors

Angular Servo

This type of servo motor can be set positionally. PiicoDev provide an I2C server driver. The code below is the Core-Electronics example code with the I2C and channel settings customised. Video.

# Drive an angular servo with generally safe-to-use default properties

from PiicoDev_Unified import sleep_ms
from PiicoDev_Servo import PiicoDev_Servo, PiicoDev_Servo_Driver

# Initialise the Servo Driver Module
controller = PiicoDev_Servo_Driver(bus=0,sda=20,scl=21) 

# Simple setup: Attach a servo to channel 2 of the controller with default properties
servo = PiicoDev_Servo(controller, 2)

# Customised setup - Attach a servo to channel 1 of the controller with the following properties:
#    - min_us: the minimum expected pulse length (microsecconds)
#    - max_us: the maximum expected pulse length (microsecconds)
#    - degrees: the angular range of the servo in degrees
# Uncomment the line below to use customised properties
# servo = PiicoDev_Servo(controller, 1, min_us=600, max_us=2400, degrees=180)

# Step the servo
servo.angle = 0
sleep_ms(1000)
servo.angle = 90
sleep_ms(1000)
servo.angle = 180
sleep_ms(1000)
servo.angle = 0
sleep_ms(2000)

# Sweep the servo slowly 0->180°
for x in range(0,180,5):
    servo.angle = x
    sleep_ms(40)

Continuous Servo

This motor is set with a direction and speed. Video.

# Drive a Continuous Rotation (a.k.a. 360 degree) servo.

from PiicoDev_Unified import sleep_ms
from PiicoDev_Servo import PiicoDev_Servo, PiicoDev_Servo_Driver

controller = PiicoDev_Servo_Driver(bus=0,sda=20,scl=21)

continuous_servo = PiicoDev_Servo(controller, 1, midpoint_us=1500, range_us=1800) # Connect a 360° servo to channel 1



continuous_servo.speed = 1    # fast
sleep_ms(1000)
continuous_servo.speed = 0.2  # slow
sleep_ms(1000)

continuous_servo.speed = -0.2 # slow reverse
sleep_ms(1000)
continuous_servo.speed = -1   # fast reverse
sleep_ms(1000)

continuous_servo.speed = 0    # stop

Relay Board and LED Array

Here is a video of the relay board working with the LED array and the Lincoln Utility board. If users want an updated version of this board, with greater spacing between the relays and slots in the board, then that can be arranged.

This LincolnRelay Board is available for $10NZD plus postage, as shown directly below. There is an option to provide the board with the SMD transistors and optocoupler chips mounted; please enquire. Relay mounting is also available.

import latch74hc259
import time

alatch0=latch74hc259.SN74HC259(8) 
alatch1=latch74hc259.SN74HC259(1) 
alatch2=latch74hc259.SN74HC259(26)

alatch1.d0.value=0
alatch1.d1.value=0
alatch1.d2.value=0
alatch1.d3.value=0
alatch1.d4.value=0
alatch1.d5.value=0
alatch1.d6.value=0
alatch1.d7.value=0


count=0
while 1:
   
    
    
    alatch1.d0.value=(1 << count) & (1 << 0) > 0
    alatch1.d1.value=(1 << count) & (1 << 1) > 0
    alatch1.d2.value=(1 << count) & (1 << 2) > 0
    alatch1.d3.value=(1 << count) & (1 << 3) > 0
    alatch1.d4.value=(1 << count) & (1 << 4) > 0
    alatch1.d5.value=(1 << count) & (1 << 5) > 0
    alatch1.d6.value=(1 << count) & (1 << 6) > 0
    alatch1.d7.value=(1 << count) & (1 << 7) > 0
    
    alatch2.d0.value=(1 << count) & (1 << 0) > 0
    alatch2.d1.value=(1 << count) & (1 << 1) > 0
    alatch2.d2.value=(1 << count) & (1 << 2) > 0
    alatch2.d3.value=(1 << count) & (1 << 3) > 0
    alatch2.d4.value=(1 << count) & (1 << 4) > 0
    alatch2.d5.value=(1 << count) & (1 << 5) > 0
    alatch2.d6.value=(1 << count) & (1 << 6) > 0
    alatch2.d7.value=(1 << count) & (1 << 7) > 0
    
    count += 1
    if count==8:
        count=0        
    time.sleep_ms(500)

Stepper Motor. (LincolnStepper)

Here is a video of a stepper motor running. The 5 volt supply can be provided from the Lincoln Utility Board, by setting the jumpers for VBUS or VSYS when using USB supply. Here is a video of a test of the user friendly driver for this single motor scenario.

import latch74hc259
import time

alatch0=latch74hc259.SN74HC259(8) 
alatch1=latch74hc259.SN74HC259(1) 
alatch2=latch74hc259.SN74HC259(26)

alatch1.d0.value=0
alatch1.d1.value=0
alatch1.d2.value=0
alatch1.d3.value=0
alatch1.d4.value=0
alatch1.d5.value=0
alatch1.d6.value=0
alatch1.d7.value=0

direction=False
step=False

count=0
while 1:
    alatch1.d0.value=direction
    alatch1.d1.value=True
    time.sleep_ms(10)
    alatch1.d1.value=False
    
    
    if count < 8:
        alatch2.d0.value=(1 << count) & (1 << 0) > 0
        alatch2.d1.value=(1 << count) & (1 << 1) > 0
        alatch2.d2.value=(1 << count) & (1 << 2) > 0
        alatch2.d3.value=(1 << count) & (1 << 3) > 0
        alatch2.d4.value=(1 << count) & (1 << 4) > 0
        alatch2.d5.value=(1 << count) & (1 << 5) > 0
        alatch2.d6.value=(1 << count) & (1 << 6) > 0
        alatch2.d7.value=(1 << count) & (1 << 7) > 0
    else:
        alatch2.d0.value=(1 << count) & (1 << 8) > 0
        alatch2.d1.value=(1 << count) & (1 << 9) > 0
        alatch2.d2.value=(1 << count) & (1 << 10) > 0
        alatch2.d3.value=(1 << count) & (1 << 11) > 0
        alatch2.d4.value=(1 << count) & (1 << 12) > 0
        alatch2.d5.value=(1 << count) & (1 << 13) > 0
        alatch2.d6.value=(1 << count) & (1 << 14) > 0
        alatch2.d7.value=(1 << count) & (1 << 15) > 0
    count += 1
    if count==16:
        count=0
        direction= not direction
    time.sleep_ms(50)

========================================
lincolnstepper1driver.py
# user friendly driver for single motor scenario
========================================

# Copyright (c) 2024 Stephen Eichler for Microtron Ltd NZ
# Written 13/3/2025
# import lincolnstepper1driver
# stpdrv0=lincolnstepper1driver.STEPDRV(1) 
# stpdrv0.DV1.ms1 = 1
# stpdrv0.DV2.dir = 1
# stpdrv0.DV3.step()


import latch74hc259
import time

class STEPDRV:

    def __init__(self, latchid):
        if latchid == 1:            
            self.alatchn=latch74hc259.SN74HC259(1, reset=True)
        elif latchid == 2:
            self.alatchn=latch74hc259.SN74HC259(26, reset=True)
        self.DV1 = Driver(self, 1)      
       
    @property
    def enable(self):
        return self.alatchn.d7.value

    @enable.setter
    def enable(self, value):
        self.alatchn.d7.value=value

    @property
    def reset(self):
        return self.alatchn.d3.value

    @reset.setter
    def reset(self, value):
        self.alatchn.d3.value=value
        
    @property
    def sleep(self):
        return self.alatchn.D2.value

    @sleep.setter
    def sleep(self, value):
        self.alatchn.d2.value=value        
    
    def _step(self, driver):

        Dbit = self.alatchn.d1
            
        if Dbit != "":
            Dbit.value = 0            
            time.sleep_us(2)
            Dbit.value = 1
            time.sleep_us(2)
            Dbit.value = 0
        

    #def _set_value(self, channel, val):
    def _set_ms1(self, driver, val):
        self._setalatbit(6, val)

    def _set_ms2(self, driver, val):
        self._setalatbit(5, val)

    def _set_ms3(self, driver, val):
        self._setalatbit( 4, val)

    def _set_dir(self, driver, val):
        self._setalatbit(0, val)

    def _setalatbit(self, dbitindex, val):
            
        if dbitindex == 0:
            self.alatchn.d0.value = val
        elif dbitindex == 1:
            self.alatchn.d1.value = val
        elif dbitindex == 2:
            self.alatchn.d2.value = val
        elif dbitindex == 3:
            self.alatchn.d3.value = val
        elif dbitindex == 4:
            self.alatchn.d4.value = val
        elif dbitindex == 5:
            self.alatchn.d5.value = val
        elif dbitindex == 6:
            self.alatchn.d6.value = val
        elif dbitindex == 7:
            self.alatchn.d7.value = val

class Driver:
    def __init__(self, stpdv_instance, index):
        self._stpdv = stpdv_instance
        self.driver_index = index
        self._ms1=0
        self._ms2=0    
        self._ms3=0    
        self._dir=0
        
    @property
    def ms1(self):
        return self._ms1
    @property
    def ms2(self):
        return self._ms2
    @property
    def ms3(self):
        return self._ms3
    @property
    def direction(self):
        return self._direction

    @ms1.setter
    def ms1(self, value):
        self._ms1=value
        self._stpdv._set_ms1(self, value)  
        
    @ms2.setter
    def ms2(self, value):
        self._ms2=value
        self._stpdv._set_ms2(self, value)  

    @ms3.setter
    def ms3(self, value):
        self._ms3=value
        self._stpdv._set_ms3(self, value)  

    @direction.setter
    def direction(self, value):
        self._dir=value
        self._stpdv._set_dir(self, value)  

    def step(self):
        self._stpdv._step(self)
        
        
 
=======================================     
lincstep1_main.py
# test of the user friendly driver for this single motor scenario
=======================================  

import lincolnstepper1driver
import time
import sys

stpdrv0=lincolnstepper1driver.STEPDRV(1) 

print ("Press enter to continue.")

while 1:
    stpdrv0.DV1.ms1 = 1
    time.sleep_ms(500)
    print ("stpdrv0.DV1.ms1")
    data = sys.stdin.buffer.read(2)
    stpdrv0.DV1.ms1 = 0

    stpdrv0.DV1.ms2 = 1
    time.sleep_ms(500)
    print ("stpdrv0.DV1.ms2")
    data = sys.stdin.buffer.read(2)
    stpdrv0.DV1.ms2 = 0

    stpdrv0.DV1.ms3 = 1
    time.sleep_ms(500)
    print ("stpdrv0.DV1.ms3")
    data = sys.stdin.buffer.read(2)
    stpdrv0.DV1.ms3 = 0

    stpdrv0.DV1.direction = 1
    time.sleep_ms(500)
    print ("stpdrv0.DV1.direction")
    data = sys.stdin.buffer.read(2)
    stpdrv0.DV1.direction = 0

    stpdrv0.enable = 1
    time.sleep_ms(500)
    print ("stpdrv0.enable")
    data = sys.stdin.buffer.read(2)
    stpdrv0.enable = 0

    stpdrv0.reset = 1
    time.sleep_ms(500)
    print ("stpdrv0.reset")
    data = sys.stdin.buffer.read(2)
    stpdrv0.reset = 0

    stpdrv0.sleep = 1
    time.sleep_ms(500)
    print ("stpdrv0.sleep")
    data = sys.stdin.buffer.read(2)
    stpdrv0.sleep = 0

    stpdrv0.DV1.step()


    
    

Stepper motor with 2 relays peripheral board. (LincolnStepper2Relay)

This Lincoln peripheral board has one stepper driver and two relays. Here is the test video.

The LincolnStepRelay is suited to the Lincoln utility board or the Welton/WeltonCpy boards, as a peripheral. It typically runs on 3.3V logic but the A4988 drivers can run on 5V as well. 12V power is needed for the relay coils, however if this is the same voltage as for the stepper motor driver then only the one external supply is needed. The jumpers can be set to implement this option. Right and left jumpers should occupy the same/parallel position.

This I2C LincolnStepRelay Board is available for $10NZD plus postage, as shown directly below. There is an option to provide the board with the SMD transistors and optocoupler chip mounted; please enquire. Relay mounting is also available.

=========================================================
lincolnstepper1driver2relay.py
# Software driver for Lincoln peripheral 1 stepper driver & 2 relays board
=========================================================
# Copyright (c) 2024 Stephen Eichler for Microtron Ltd NZ
# Written 13/3/2025
# import lincolnstepper1driver
# stpdrv0=lincolnstepper1driver.STEPDRV(1) 
# stpdrv0.DV1.ms1 = 1
# stpdrv0.DV2.dir = 1
# stpdrv0.DV3.step()


import latch74hc259
import time
from machine import Pin

class STEPDRV:

    def __init__(self, latchid):
        if latchid == 1:            
            self.alatchn=latch74hc259.SN74HC259(1, reset=True)
            self.pin8 = Pin(8,Pin.OUT)
            self.pin8.value(1)
            self.pin26 = Pin(26,Pin.OUT)
            self.pin26.value(1)
        elif latchid == 2:
            self.alatchn=latch74hc259.SN74HC259(26, reset=True)
            self.pin8 = Pin(8,Pin.OUT)
            self.pin8.value(1)
            self.pin1 = Pin(1,Pin.OUT)
            self.pin1.value(1)
        self.DV1 = Driver(self, 1)      
       
    @property
    def enable(self):
        return self.alatchn.d7.value

    @enable.setter
    def enable(self, value):
        self.alatchn.d7.value=value

    @property
    def relay2(self):
        return self.alatchn.d3.value

    @relay2.setter
    def relay2(self, value):
        self.alatchn.d3.value=value
        
    @property
    def relay1(self):
        return self.alatchn.d2.value

    @relay1.setter
    def relay1(self, value):
        self.alatchn.d2.value=value        
    
    def _step(self, driver):

        Dbit = self.alatchn.d1
            
        if Dbit != "":
            Dbit.value = 0            
            time.sleep_us(2)
            Dbit.value = 1
            time.sleep_us(2)
            Dbit.value = 0
        

    #def _set_value(self, channel, val):
    def _set_ms1(self, driver, val):
        self._setalatbit(6, val)

    def _set_ms2(self, driver, val):
        self._setalatbit(5, val)

    def _set_ms3(self, driver, val):
        self._setalatbit( 4, val)

    def _set_dir(self, driver, val):
        self._setalatbit(0, val)

    def _setalatbit(self, dbitindex, val):
            
        if dbitindex == 0:
            self.alatchn.d0.value = val
        elif dbitindex == 1:
            self.alatchn.d1.value = val
        elif dbitindex == 2:
            self.alatchn.d2.value = val
        elif dbitindex == 3:
            self.alatchn.d3.value = val
        elif dbitindex == 4:
            self.alatchn.d4.value = val
        elif dbitindex == 5:
            self.alatchn.d5.value = val
        elif dbitindex == 6:
            self.alatchn.d6.value = val
        elif dbitindex == 7:
            self.alatchn.d7.value = val

class Driver:
    def __init__(self, stpdv_instance, index):
        self._stpdv = stpdv_instance
        self.driver_index = index
        self._ms1=0
        self._ms2=0    
        self._ms3=0    
        self._dir=0
        
    @property
    def ms1(self):
        return self._ms1
    @property
    def ms2(self):
        return self._ms2
    @property
    def ms3(self):
        return self._ms3
    @property
    def direction(self):
        return self._direction

    @ms1.setter
    def ms1(self, value):
        self._ms1=value
        self._stpdv._set_ms1(self, value)  
        
    @ms2.setter
    def ms2(self, value):
        self._ms2=value
        self._stpdv._set_ms2(self, value)  

    @ms3.setter
    def ms3(self, value):
        self._ms3=value
        self._stpdv._set_ms3(self, value)  

    @direction.setter
    def direction(self, value):
        self._dir=value
        self._stpdv._set_dir(self, value)  

    def step(self):
        self._stpdv._step(self)
        
============================================
lincstep1relay2more_main.py
============================================
import lincolnstepper1driver2relay
import time

stpdrv0=lincolnstepper1driver2relay.STEPDRV(1) 

stpdrv0.DV1.ms1 = 0
stpdrv0.DV1.ms2 = 0
stpdrv0.DV1.ms3 = 0

direction=False
count=0
loops=0

while 1:
    
    stpdrv0.DV1.direction = direction
    stpdrv0.DV1.step()

    count += 1
    if count==16:
        count=0
        loops += 1
        direction= not direction
        if loops % 2 == 0:
            stpdrv0.relay1 = not stpdrv0.relay1
        else:
            stpdrv0.relay2 = not stpdrv0.relay2
        
    time.sleep_ms(50)

        

Stepper Motors: 3 drivers on peripheral board. (Lincoln3Stepper)

Here is a video of a test of the user friendly 3 hardware driver, software driver. Here is a video of the hardware test.

===========================================
lincolnstepper3driver.py
# user friendly 3 hardware driver, software driver
===========================================

# Copyright (c) 2024 Stephen Eichler for Microtron Ltd NZ
# Written 13/3/2025
# import lincolnstepper3driver
# stpdrv0=lincolnstepper3driver.STEPDRV() 
# stpdrv0.DV1.ms1 = 1
# stpdrv0.DV2.dir = 1
# stpdrv0.DV3.step()


import latch74hc259
import time

class STEPDRV:

    def __init__(self):
        self.alatch1=latch74hc259.SN74HC259(1, reset=True)
        self.alatch2=latch74hc259.SN74HC259(26, reset=True)
        self.DV1 = Driver(self, 1)
        self.DV2 = Driver(self, 2)
        self.DV3 = Driver(self, 3)
        
    @property
    def spare(self):
        return self.alatch2.d0.value

    @spare.setter
    def spare(self, value):
        self.alatch2.d0.value=value
      
    def _step(self, driver):
        dbitindex = {3:2,2:7,1:4}[driver.driver_index]
        dbyteindex = {3:2,2:2,1:1}[driver.driver_index]

        alatch = ""
        if dbyteindex == 1:
            alatch = self.alatch1
        else:
            alatch = self.alatch2
            
        Dbit = ""
        if dbitindex == 2:
            Dbit = alatch.d2
        elif dbitindex == 4:
            Dbit = alatch.d4
        elif dbitindex == 7:
            Dbit = alatch.d7
            
        if Dbit != "":
            Dbit.value = 0            
            time.sleep_us(2)
            Dbit.value = 1
            time.sleep_us(2)
            Dbit.value = 0
        
    def _setalatbit(self, dbyteindex, dbitindex, val):
        alatch = ""
        if dbyteindex == 1:
            alatch = self.alatch1
        else:
            alatch = self.alatch2
            
        if dbitindex == 0:
            alatch.d0.value = val
        elif dbitindex == 1:
            alatch.d1.value = val
        elif dbitindex == 2:
            alatch.d2.value = val
        elif dbitindex == 3:
            alatch.d3.value = val
        elif dbitindex == 4:
            alatch.d4.value = val
        elif dbitindex == 5:
            alatch.d5.value = val
        elif dbitindex == 6:
            alatch.d6.value = val
        elif dbitindex == 7:
            alatch.d7.value = val
    
    def _set_ms1(self, driver, val):
        dbitindex = {3:5,2:2,1:7}[driver.driver_index]
        dbyteindex = {3:2,2:1,1:1}[driver.driver_index]
        self._setalatbit(dbyteindex, dbitindex, val)

    def _set_ms2(self, driver, val):
        dbitindex = {3:4,2:1,1:6}[driver.driver_index]
        dbyteindex = {3:2,2:1,1:1}[driver.driver_index]
        self._setalatbit(dbyteindex, dbitindex, val)

    def _set_ms3(self, driver, val):
        dbitindex = {3:3,2:0,1:5}[driver.driver_index]
        dbyteindex = {3:2,2:1,1:1}[driver.driver_index]
        self._setalatbit(dbyteindex, dbitindex, val)

    def _set_dir(self, driver, val):
        dbitindex = {3:1,2:6,1:3}[driver.driver_index]
        dbyteindex = {3:2,2:2,1:1}[driver.driver_index]
        self._setalatbit(dbyteindex, dbitindex, val)


class Driver:
    def __init__(self, stpdv_instance, index):
        self._stpdv = stpdv_instance
        self.driver_index = index
        self._ms1=0
        self._ms2=0    
        self._ms3=0    
        self._dir=0
        
    @property
    def ms1(self):
        return self._ms1
    @property
    def ms2(self):
        return self._ms2
    @property
    def ms3(self):
        return self._ms3
    @property
    def direction(self):
        return self._direction

    @ms1.setter
    def ms1(self, value):
        self._ms1=value
        self._stpdv._set_ms1(self, value)  
        
    @ms2.setter
    def ms2(self, value):
        self._ms2=value
        self._stpdv._set_ms2(self, value)  

    @ms3.setter
    def ms3(self, value):
        self._ms3=value
        self._stpdv._set_ms3(self, value)  

    @direction.setter
    def direction(self, value):
        self._dir=value
        self._stpdv._set_dir(self, value)  

    def step(self):
        self._stpdv._step(self)
        
        
===========================================   
lincstep3_main.py 
# test of the user friendly 3 hardware driver, software driver
===========================================

import lincolnstepper3driver
import time
import sys

stpdrv0=lincolnstepper3driver.STEPDRV() 
print ("Press enter to continue.")

while 1:
    stpdrv0.DV1.ms1 = 1
    time.sleep_ms(500)
    print ("stpdrv0.DV1.ms1")
    data = sys.stdin.buffer.read(2)
    stpdrv0.DV1.ms1 = 0

    stpdrv0.DV1.ms2 = 1
    time.sleep_ms(500)
    print ("stpdrv0.DV1.ms2")
    data = sys.stdin.buffer.read(2)
    stpdrv0.DV1.ms2 = 0

    stpdrv0.DV1.ms3 = 1
    time.sleep_ms(500)
    print ("stpdrv0.DV1.ms3")
    data = sys.stdin.buffer.read(2)
    stpdrv0.DV1.ms3 = 0

    stpdrv0.DV1.direction = 1
    time.sleep_ms(500)
    print ("stpdrv0.DV1.direction")
    data = sys.stdin.buffer.read(2)
    stpdrv0.DV1.direction = 0

    stpdrv0.DV2.ms1 = 1
    time.sleep_ms(500)
    print ("stpdrv0.DV2.ms1")
    data = sys.stdin.buffer.read(2)
    stpdrv0.DV2.ms1 = 0

    stpdrv0.DV2.ms2 = 1
    time.sleep_ms(500)
    print ("stpdrv0.DV2.ms2")
    data = sys.stdin.buffer.read(2)
    stpdrv0.DV2.ms2 = 0

    stpdrv0.DV2.ms3 = 1
    time.sleep_ms(500)
    print ("stpdrv0.DV2.ms3")
    data = sys.stdin.buffer.read(2)
    stpdrv0.DV2.ms3 = 0

    stpdrv0.DV2.direction = 1
    time.sleep_ms(500)
    print ("stpdrv0.DV2.direction")
    data = sys.stdin.buffer.read(2)
    stpdrv0.DV2.direction = 0

    stpdrv0.DV3.ms1 = 1
    time.sleep_ms(500)
    print ("stpdrv0.DV3.ms1")
    data = sys.stdin.buffer.read(2)
    stpdrv0.DV3.ms1 = 0

    stpdrv0.DV3.ms2 = 1
    time.sleep_ms(500)
    print ("stpdrv0.DV3.ms2")
    data = sys.stdin.buffer.read(2)
    stpdrv0.DV3.ms2 = 0

    stpdrv0.DV3.ms3 = 1
    time.sleep_ms(500)
    print ("stpdrv0.DV3.ms3")
    data = sys.stdin.buffer.read(2)
    stpdrv0.DV3.ms3 = 0

    stpdrv0.DV3.direction = 1
    time.sleep_ms(500)
    print ("stpdrv0.DV3.direction")
    data = sys.stdin.buffer.read(2)
    stpdrv0.DV3.direction = 0

    stpdrv0.DV1.step()
    stpdrv0.DV2.step()
    stpdrv0.DV3.step()

    stpdrv0.spare = 1
    time.sleep_ms(500)
    print ("stpdrv0.spare")
    data = sys.stdin.buffer.read(2)
    stpdrv0.spare = 0

================================================
lincstep3more_main.py
# Hardware test with the user friendly 3 hardware driver, software driver 
================================================
import lincolnstepper3driver
import time
import sys

stpdrv0=lincolnstepper3driver.STEPDRV() 

stpdrv0.DV1.ms1 = 0
stpdrv0.DV1.ms2 = 0
stpdrv0.DV1.ms3 = 0
stpdrv0.DV2.ms1 = 0
stpdrv0.DV2.ms2 = 0
stpdrv0.DV2.ms3 = 0
stpdrv0.DV3.ms1 = 0
stpdrv0.DV3.ms2 = 0
stpdrv0.DV3.ms3 = 0

direction=False
count=0

while 1:

    stpdrv0.DV1.direction = direction
    stpdrv0.DV2.direction = direction
    stpdrv0.DV3.direction = direction

    stpdrv0.DV1.step()
    stpdrv0.DV2.step()
    stpdrv0.DV3.step()
    
    count += 1
    if count==16:
        count=0
        direction= not direction
    
    time.sleep_ms(50)