Automate Deployments
You can download the following files from github.com/chriscummings/remote-arduino-deployment
On GitHub: example.env
On GitHub: constants.py
constants.py
import os
# Shared constants
# ==============================================================================
# These constants are used by both deploy.py and flash.py
# User directory on RPi
RPI_USER_DIR: str = '/home/YOUR_USER'
# Project directory on RPi
PROJECT_DIR: str = '/home/YOUR_USER/demeter'
# Path to arduino-cli on RPi
RPI_ARDUINO_CLI: str = os.path.join(RPI_USER_DIR, 'bin/arduino-cli')
# Deploy script constants
# ==============================================================================
# These constants are only used by deploy.py
# RPi directory for source files
SKETCH_DESTINATION_DIRECTORY: str = PROJECT_DIR
# Arduino libaries to download to RPi
SKETCH_LIBS: list[str] = [
'WatchDog',
# etc..
]
# Sketch source files.
# Feel free to change this to a glob command or something similar so long as it
# return a list or strings. However, be sure to include constants.py or migrate
# the constants over some how.
SKETCH_FILES: list[str] = [
'contants.py' # Copy this constants file to RPi for ease of updates
'my_sketch.ino',
'my_sketch.h',
# etc...
]
# Path flash.py on RPi
RPI_FLASH_SCRIPT: str = os.path.join(RPI_USER_DIR, 'flash.py')
# Local (deploy) SSH log file
LOCAL_SSH_LOG: str = 'ssh.log'
# RPi-side-only flash script constants
# ==============================================================================
# These constants are only used byt flash.py
# Arduino-specific params for arduino-cli.
# See: https://avrdude.readthedocs.io/en/latest/2-Command_Line_Options.html
ARDUINO_CORE = 'arduino:avr:uno'
AVRDUDE_PROGRAMMER_ID = 'arduino'
AVRDUDE_PART_NO = 'm328p'
# Default output for arduino-cli compiled .hex files on RPi
COMPILED_SKETCH_PATH = '/tmp/arduino/sketches/'
# Path to avrdude on RPi
RPI_AVRDUDE = os.path.join(
RPI_USER_DIR,
'.arduino15/packages/arduino/tools/avrdude/6.3.0-arduino17/bin/avrdude')
# Path to avrdude.conf on RPi
AVRDUDE_CONF = os.path.join(RPI_USER_DIR, 'avrdude.conf')
# RPi GPIO pin used to pull Arduino's reset pin
GPIO_RESET_PIN = 4
# Duration to 'hold' Arduino's reset
ARDUINO_RESET_INTERVAL = 1
Getting Code on to the Pi
On GitHub: deploy.py
deploy.py
import os
from dotenv import load_dotenv
import paramiko
import constants
conn: paramiko.client.SSHClient
def setup_connection() -> paramiko.client.SSHClient:
'''Configures SSH connection'''
paramiko.util.log_to_file(constants.LOCAL_SSH_LOG)
ssh: paramiko.client.SSHClient = paramiko.client.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(
os.getenv('RPI_IP'),
port=int(os.getenv('RPI_PORT', '')),
username=os.getenv('RPI_USER'),
password=os.getenv('RPI_PASS'))
return ssh
def run_rpi_cmd(cmd: str) -> str:
'''Executes cmd on RPi'''
stdin, stdout, stderr = conn.exec_command(cmd)
stdout.channel.set_combine_stderr(True)
output = '\n'.join(stdout.readlines()) # Waits for command to finish
return output
def teardown_connection() -> None:
'''Cleans up open SSH connections'''
if conn:
conn.close()
def transfer_src() -> None:
'''Copies source code to RPi'''
# Setup SFTP conn
transport: paramiko.transport.Transport = paramiko.Transport(
os.getenv('RPI_IP'),
int(os.getenv('RPI_PORT', '')))
transport.connect(None,
os.getenv('RPI_USER'),
os.getenv('RPI_PASS'))
sftp: paramiko.sftp_client.SFTPClient = paramiko.SFTPClient.from_transport(transport)
try:
# Ensure directory
run_rpi_cmd(f'mkdir {constants.SKETCH_DESTINATION_DIRECTORY}')
# Copy files
for sketch_file in constants.SKETCH_FILES:
sftp.put(
os.path.abspath(sketch_file),
os.path.join(constants.SKETCH_DESTINATION_DIRECTORY, sketch_file))
except Exception as err:
print(err) # TODO: log
finally:
if sftp: sftp.close()
if transport: transport.close()
def fetch_sketch_libs() -> None:
'''Downloads sketch libraries to RPi'''
output = run_rpi_cmd(f'{constants.RPI_ARDUINO_CLI} lib list')
for lib in constants.SKETCH_LIBS:
print(f'Checking for required sketch library {lib}')
if lib not in output:
print(f'Installing {lib}')
output = run_rpi_cmd(f'{constants.RPI_ARDUINO_CLI} lib install "{lib}"')
print(output)
def compile_and_flash() -> None:
'''Compiles sketch on RPi and flashes Arduino'''
output = run_rpi_cmd(f'python3 {constants.RPI_FLASH_SCRIPT} {constants.SKETCH_DESTINATION_DIRECTORY}')
print(output)
def deploy() -> None:
conn = setup_connection()
try:
transfer_src()
fetch_sketch_libs()
compile_and_flash()
except Exception as err:
print(err)
finally:
teardown_connection()
if __name__ == '__main__':
load_dotenv()
deploy()
Compiling and Flashing Firmware
On GitHub: flash.py
flash.py
import os
import sys
import time
import subprocess
from glob import glob
import RPi.GPIO as GPIO
import contants
def configure_gpio_reset_pin() -> None:
'''Sets up RPi GPIO pin for Arduino reset'''
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
GPIO.setup(contants.GPIO_RESET_PIN, GPIO.OUT)
def delete_sketches() -> None:
'''Delete old compiled sketches'''
subprocess.run(['rm', '-rf', os.path.join(contants.COMPILED_SKETCH_PATH, '*')])
def get_ino_path() -> str:
'''Returns first .ino found. Assumes only one .ino!'''
# First param is src folder
target_dir: str = sys.argv[1]
ino_filepath = glob(os.path.join(target_dir, '*.ino'))[0]
if not ino_filepath: raise Exception(f'No .ino file found in {target_dir}')
return ino_filepath
def compile(ino_filepath: str) -> None:
'''Runs arduino-cli compilation'''
subprocess.run([
contants.RPI_ARDUINO_CLI,
'compile',
'-b', contants.ARDUINO_CORE,
ino_filepath])
def get_compiled_sketch_path() -> str:
'''Returns first compiled .hex found.'''
# Assumes previous copilations have been deleted!
path: str = ''
for filepath in glob(os.path.join(contants.COMPILED_SKETCH_PATH, '*', '*')):
root, ext = os.path.splitext(filepath)
if ext == '.hex' and 'bootloader' not in root:
path = filepath
break
if path == '': raise Exception(f'No .hex found in {contants.COMPILED_SKETCH_PATH}')
return path
def reset_arduino() -> None:
'''Hard resets Arduino'''
GPIO.output(contants.GPIO_RESET_PIN, GPIO.LOW)
time.sleep(contants.ARDUINO_RESET_INTERVAL)
GPIO.output(contants.GPIO_RESET_PIN, GPIO.HIGH)
def flash(hex_file: str) -> None:
'''Runs AVRDUDE cmd to flash Arduino'''
subprocess.run([
contants.RPI_AVRDUDE,
'-c', contants.AVRDUDE_PROGRAMMER_ID,
'-p', contants.AVRDUDE_PART_NO,
'-C', contants.AVRDUDE_CONF,
'-U', f'flash:w:{hex_file}'])
def run():
configure_gpio_reset_pin()
delete_sketches()
compile(get_ino_path())
flash(get_compiled_sketch_path())
reset_arduino()
if __name__ == "__main__":
run()