├── README.md ├── opzgo.py └── setup.sh /README.md: -------------------------------------------------------------------------------- 1 | # OPZgo 2 | 3 | >Ultra-portable backups for Teenage Engineering's OP-Z 4 | 5 | ![OPZgo running on Raspberry Pi Zero](https://i.imgur.com/aqGDum8.jpg) 6 | 7 | Inspired by [tacoe's OP1GO](https://github.com/tacoe/OP1GO), this small script allows for full OP-Z backups while on the go using a Raspberry Pi Zero W. Simply plug your OP-Z into the Pi Zero and it will automatically create a timestamped full backup of your OP-Z including any projects, sample packs, bounces and configurations. 8 | 9 | ### What's needed 10 | 11 | * OP-Z 12 | * Raspberry Pi Zero W 13 | * Micro SD Card (at least 4GB) 14 | * USB-C to USB micro cable 15 | * USB-A to USB micro cable (for power) 16 | * Power source (i.e. power adapter, power bank) 17 | 18 | 19 | ## Usage 20 | 21 | 1. Plug in your Pi to a power source and wait for it to boot up (the green LED should stop blinking once fully booted). 22 | 2. Plug in your OP-Z first (powered off) and wait for a long blink indicating the OP-Z is recognized. 23 | 3. Now that everything is ready, put your OP-Z into [Content Mode](https://teenage.engineering/guides/op-z/disk-modes) by holding the Track button while turning on the unit. 24 | 4. The Pi's LED will then blink 5 times indicating the backup process has begun. During the backup process, the Pi's LED will blink rapidly. 25 | 5. Once finished, the LED will long blink for 5 seconds to signal the backup has completed. The OP-Z will be automatically unmounted and ejected. You should see the OP-Z reboot into normal mode. 26 | 6. Wait 5 seconds after that long LED blink and the Pi should do a couple more blinks indicating it is shutting down gracefully. 27 | 7. Once you see no more LED blinks the Pi has safely shut down. It's now safe to disconnect the OP-Z and unplug the Pi. 28 | 29 | 30 | ## Setup 31 | 32 | **Quick Start:** 33 | 34 | 1. Download the [latest OPZgo image here](https://mega.nz/#!KpVTlQKA!0iSO4_0hDjeTeQvDeuK2WALMTdKEfOoMUL8eYqAzXQE). 35 | 2. Flash to a SD card using [Etcher](https://www.balena.io/etcher/). 36 | 3. Plug it into your Raspberry Pi and you're ready to start making backups! 37 | 38 | **Manual Setup:** 39 | 40 | If you wish to manually install it yourself, check out the [instructions here](https://github.com/chrisdiana/OPZgo/wiki/Manual-Setup). 41 | 42 | 43 | ## Accessing Backups 44 | 45 | You can access backups by plugging the SD card into a computer. You should see a disk called `BOOT`. Within `BOOT` all backups are saved to `opzgo/backups/` as timestamped directories each time you trigger a backup. 46 | 47 | 48 | ### Troubleshooting & a few things to note 49 | 50 | * Sometimes the OP-Z will fail to connect or mount. If after a long time (>5 minutes) you still don't see the series of LED patterns described above, do NOT assume the backup was successful. Try unplugging the power from the Pi, reboot and try again. 51 | 52 | * The script will only backup once per boot so if you want to backup again you will have to restart the process. 53 | 54 | * This software is provided "as is", without warranty. 55 | -------------------------------------------------------------------------------- /opzgo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | """ 4 | 5 | OPZgo 6 | ----- 7 | Ultra-portable backups for Teenage Engineering's OP-Z 8 | 9 | :usage: sudo python3 opzgo.py 10 | :version: 0.1.1 11 | :copyright: 2020 Chris Diana 12 | :license: MIT 13 | 14 | """ 15 | 16 | import os 17 | import sys 18 | import re 19 | import time 20 | import usb.core 21 | import usb.util 22 | import shutil 23 | import subprocess 24 | from datetime import datetime 25 | 26 | VENDOR = 0x2367 27 | PRODUCT = 0x000c 28 | USBID_OPZ = "*OP-Z_Disk*" 29 | MOUNT_DIR = "/media/opz" 30 | BACKUP_DIR_FORMAT = "%Y-%m-%d_%H-%M-%S" 31 | 32 | DEFAULT_BACKUP_ROOT = "/opzgo" 33 | BACKUP_ROOT = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_BACKUP_ROOT 34 | BACKUPS_DIR = os.path.join(BACKUP_ROOT, "backups") 35 | 36 | # OP-Z connection 37 | def ensure_connection(): 38 | if not is_connected(): 39 | print("Connect your OP-Z and put it into Content Mode (Hold Track + Power)...") 40 | wait_for_connection() 41 | 42 | def is_connected(): 43 | return usb.core.find(idVendor=VENDOR, idProduct=PRODUCT) is not None 44 | 45 | def wait_for_connection(): 46 | try: 47 | while True: 48 | time.sleep(1) 49 | if is_connected(): 50 | break 51 | except KeyboardInterrupt: 52 | sys.exit(0) 53 | 54 | # mounting 55 | def wait_for_mount(source, target, fs, options): 56 | print("Put your OP-Z into Content Mode (Hold Track + Power)...") 57 | try: 58 | while True: 59 | time.sleep(5) 60 | if mount_device(source, target, fs, options): 61 | break 62 | except KeyboardInterrupt: 63 | sys.exit(0) 64 | 65 | def mount_device(source, target, fs, options=''): 66 | status = False 67 | ret = os.system('mount {} {}'.format(source, target)) 68 | if ret == 0: 69 | status = True 70 | return status 71 | 72 | def unmount_device(target): 73 | ret = os.system('umount {}'.format(target)) 74 | if ret != 0: 75 | raise RuntimeError("Error unmounting {}: {}".format(target, ret)) 76 | 77 | def eject_device(target): 78 | subprocess.call(["eject", target]) 79 | 80 | def get_mount_path(): 81 | o = os.popen('readlink -f /dev/disk/by-id/' + USBID_OPZ).read() 82 | if USBID_OPZ in o: 83 | raise RuntimeError("Error getting OP-Z mount path: {}".format(o)) 84 | else: 85 | return o.rstrip() 86 | 87 | # copying 88 | def forcedir(path): 89 | if not os.path.isdir(path): 90 | os.makedirs(path) 91 | 92 | def backup_files(source, destination): 93 | dstroot = os.path.join(destination, datetime.now().strftime(BACKUP_DIR_FORMAT)) 94 | subprocess.call(["rsync", "-rP", source + '/', dstroot]) 95 | blink(1) 96 | 97 | # utils 98 | def blink(count): 99 | os.system("echo none | sudo tee /sys/class/leds/led0/trigger >/dev/null 2>&1") 100 | for i in range(0,count): 101 | os.system("echo 0 | sudo tee /sys/class/leds/led0/brightness >/dev/null 2>&1") 102 | time.sleep(0.15) 103 | os.system("echo 1 | sudo tee /sys/class/leds/led0/brightness >/dev/null 2>&1") 104 | time.sleep(0.05) 105 | 106 | def blink_long(count): 107 | os.system("echo none | sudo tee /sys/class/leds/led0/trigger >/dev/null 2>&1") 108 | os.system("echo 0 | sudo tee /sys/class/leds/led0/brightness >/dev/null 2>&1") 109 | time.sleep(count) 110 | os.system("echo 1 | sudo tee /sys/class/leds/led0/brightness >/dev/null 2>&1") 111 | 112 | def blink_yay(): 113 | os.system("echo none | sudo tee /sys/class/leds/led0/trigger >/dev/null 2>&1") 114 | for i in range(0,30): 115 | os.system("echo 0 | sudo tee /sys/class/leds/led0/brightness >/dev/null 2>&1") 116 | time.sleep(0.01) 117 | os.system("echo 1 | sudo tee /sys/class/leds/led0/brightness >/dev/null 2>&1") 118 | time.sleep(0.01) 119 | 120 | def shutdown(): 121 | os.system("sudo shutdown -h now") 122 | 123 | 124 | ## Main ## 125 | 126 | # create mount point and local backup folders 127 | blink(2) 128 | forcedir(BACKUPS_DIR) 129 | forcedir(MOUNT_DIR) 130 | 131 | # wait until OP-Z is connected 132 | print(" > Starting - waiting for OP-Z to connect") 133 | ensure_connection() 134 | time.sleep(5) 135 | blink_long(3) 136 | 137 | # mount OP-Z 138 | mount_path = get_mount_path() 139 | print(" > OP-Z device path: %s" % mount_path) 140 | wait_for_mount(mount_path, MOUNT_DIR, 'ext4', 'rw') 141 | print(" > Device mounted at %s" % MOUNT_DIR) 142 | 143 | # copy files to local storage 144 | blink(5) 145 | print(" > Copying files...") 146 | backup_files(MOUNT_DIR, BACKUPS_DIR) 147 | 148 | # unmount OP-Z 149 | print(" > Unmounting OP-Z") 150 | unmount_device(MOUNT_DIR) 151 | eject_device(mount_path) 152 | print(" > Done.") 153 | blink_yay() 154 | blink_long(5) 155 | shutdown() -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | sudo apt-get install python3-pip git netatalk exfat-utils exfat-fuse rsync eject 2 | sudo pip3 install pyusb --------------------------------------------------------------------------------