diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 1fec7d9e90d4419c478d7534087c797988ff8dbe..4d6842e2290005f41a2fc1e3c9ca1ab2e158945a 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -32,10 +32,16 @@ test:download:deb:
     - export DEBIAN_FRONTEND=noninteractive
     - apt-get -y update
     - apt-get -y install $FLASHDEPS
+    - rm -rf output
   script:
-    - scripts/librem5-devkit-flash-image --board librem5 --variant plain  --dist amber-phone --skip-flash --skip-cleanup --dir=download/
-    - ls -l download/librem5.img download/flash_librem5.lst  download/u-boot-librem5.imx
+    - scripts/librem5-flash-image --board librem5r3 --variant plain  --dist amber-phone --skip-flash --skip-cleanup --dir=download/
+    - ls -l download/librem5*.img download/flash_librem5*.lst  download/u-boot-librem5*.imx
+    - mkdir -p output/
+    - cp download/flash_*.lst output/
     - rm -rf download
+  artifacts:
+    paths:
+      - output/
 
 test:download:pip:
   <<: *tags
@@ -48,7 +54,7 @@ test:download:pip:
     - virtualenv --python=python3 test-download
     - source test-download/bin/activate
     - pip install -r requirements.txt
-    - python3 scripts/librem5-devkit-flash-image --board librem5 --variant plain  --dist amber-phone --skip-flash --skip-cleanup --dir=download/
-    - ls -l download/librem5.img download/flash_librem5.lst  download/u-boot-librem5.imx
+    - python3 scripts/librem5-flash-image --board librem5r3 --variant plain  --dist amber-phone --skip-flash --skip-cleanup --dir=download/
+    - ls -l download/librem5*.img download/flash_librem5*.lst  download/u-boot-librem5*.imx
     - rm -rf download
 
diff --git a/debian/changelog b/debian/changelog
index 93dfbf28b7892507ad20a21bc0b97e61e657a592..0bd076228e0a3f274bb773541fd23ad1d5ba25cb 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,80 @@
+librem5-devkit-tools (0.0.14) amber-phone; urgency=medium
+
+  [ Martin Kepplinger ]
+  * pytests: test_cameras: update to runtime pm sysfs interface
+
+ -- Martin Kepplinger <martin.kepplinger@puri.sm>  Mon, 30 Nov 2020 19:50:59 +0100
+
+librem5-devkit-tools (0.0.13) amber-phone; urgency=high
+
+  [ Guido Günther ]
+  * librem5-flash-image: Fix help
+  * librem5-flash-image: Use amber-phone as default
+  * librem5-flash-image: Use revision for uuu_mods too
+    No need to pass both
+  * librem5-flash-image: Use dogwood by default
+  * flash-image: Drop unused global
+  * flash-image: Print defaults
+    This makes it simpler for people who found --help
+  * debian: Add librem5-flash-image manpage based on help output
+  * debian: Add flash-image deps as build-deps
+    This ensure help2man can work
+  * librem5-flash-image: Support r4
+
+  [ Martin Kepplinger ]
+  * pytests: add basic test for the torch
+  * pytests: add basic test for gnss
+  * pytests: add basic test for the status led
+  * pytests: add basic test for usb0 network device
+  * pytests: test_audio: check existence of both modem and codec cards
+  * pytests: add basic test for the m41t80 rtc
+  * pytests: add touchscreen firmware version check
+
+  [ Angus Ainslie ]
+  * uuu_scripts/burn_fuses_dogwood.lst: move the version number
+  * uuu_scripts/burn_fuses_evergreen.lst: add a script to burn the evergreen fuses
+  * debian/librem5-check.install: copy the switch test
+  * test_scripts/keys_test.py: add a cli based key test
+  * debian/librem5-check.install: install the key test
+
+  [ Dorota Czaplejewicz ]
+  * pytests: Test i2c communication with cameras
+
+ -- Martin Kepplinger <martin.kepplinger@puri.sm>  Fri, 13 Nov 2020 11:55:44 +0100
+
+librem5-devkit-tools (0.0.12) amber-phone; urgency=medium
+
+  [ Sebastian Krzyszkowiak ]
+  * Remove /etc/network/interfaces.d/usb0 file
+
+  [ Angus Ainslie (Purism) ]
+  * compare_flash.sh: add a script to check the flash contents
+
+  [ Martin Kepplinger ]
+  * pytests: support new Board names for hardware revisions
+  * pytests/test_modem.py: add support for the bm818 alsa device name
+
+  [ Guido Günther ]
+  * librem5-devkit-flash-image: Report proper error when job wasn't found
+  * librem5-devkit-flash: Strip board revision from uboot board name
+  * librem5-devkit-flash-image: Use images with revision
+  * gitlab-ci: Use librem5r3 for the download test (Closes: #24)
+  * gitlab-ci: Keep uuu script around
+  * gitlab-ci: Be less picky about image names
+  * Drop devkit from flash script name
+  * Drop devkit from host package
+  * Drop devkit from udev rule name
+  * Drop devkit from check package
+  * Drop udev rule that gives the dialog group access to the gnss subsytem
+    (Closes: #26)
+
+  [ Angus Ainslie ]
+  * 99-gnss.rules: Add a udev rule
+  * scripts: flash-image: add a script to flash local images
+  * uuu_scripts: drop deprecated scripts
+
+ -- Martin Kepplinger <martin.kepplinger@puri.sm>  Mon, 21 Sep 2020 11:03:08 +0200
+
 librem5-devkit-tools (0.0.11.0pureos0.1) byzantium; urgency=medium
 
   * Upload to byzantium
diff --git a/debian/clean b/debian/clean
new file mode 100644
index 0000000000000000000000000000000000000000..f03cb7433c9da52f26a67496b47b5c91f3ff3037
--- /dev/null
+++ b/debian/clean
@@ -0,0 +1 @@
+debian/librem5-flash-image.1
diff --git a/debian/control b/debian/control
index 7802d2158edb02ae2ec41ee40bf3c3829e4791de..9b35e0b560a99de4403a296180c2ee7c6beec113 100644
--- a/debian/control
+++ b/debian/control
@@ -5,6 +5,12 @@ Maintainer: Guido Günther <agx@sigxcpu.org>
 Build-Depends:
  debhelper (>= 11),
  flake8,
+ help2man,
+ python3-coloredlogs,
+ python3-jenkins,
+ python3-requests,
+ python3-tqdm,
+ python3-yaml,
  shellcheck,
 Standards-Version: 4.1.3
 Homepage: https://source.puri.sm/Librem5/librem5-devkit-tools/
@@ -29,8 +35,10 @@ Description: Basic configuration for the librem5-devkit
  This package contains initial configuration for the
  librem5 devkits.
 
-Package: librem5-devkit-check
+Package: librem5-check
 Architecture: all
+Conflicts: librem5-devkit-check (<< 0.0.12)
+Replaces: librem5-devkit-check (<< 0.0.12)
 Depends:
  ${misc:Depends},
  ${shlibs:Depends},
@@ -47,13 +55,15 @@ Depends:
  python3-yaml,
  usb-modeswitch,
  usbutils,
-Description: Check script for the librem5 devkit
- librem5-devkit-check performs various check on
- the a librem5 devkit to make sure the hardware
+Description: Check script for the Librem5 and the Devkit
+ librem5-check performs various checks on
+ a Librem5 or a Librem 5 devkit to make sure the hardware
  got detected correctly.
 
-Package: librem5-devkit-host
+Package: librem5-host
 Architecture: all
+Conflicts: librem5-devkit-host (<< 0.0.12)
+Replaces: librem5-devkit-host (<< 0.0.12)
 Depends:
  ${misc:Depends},
  ${shlibs:Depends},
@@ -65,9 +75,10 @@ Depends:
  python3-yaml,
  usbutils,
  uuu,
-Description: Tools for the librem5 devkit (host side)
- Scripts useful to for working with the librem-t5 devkit.
+Description: Tools for the librem5 and the Devkit (host side)
+ Scripts useful to for working with the Librem 5 and the
+ Librem 5 Devkit.
  .
  These tools are useful on the host side (e.g. the machine
  you use to flash the image from). They are not useful on
- the devkit itself.
+ the Librem 5 or the Devkit itself.
diff --git a/debian/librem5-devkit-check.install b/debian/librem5-check.install
similarity index 76%
rename from debian/librem5-devkit-check.install
rename to debian/librem5-check.install
index c029d75d8a9d3b785fd3b1ce223b168f316a4546..9d5f8d75e31dbfbc2065ddca0f15c1df9495f406 100644
--- a/debian/librem5-devkit-check.install
+++ b/debian/librem5-check.install
@@ -7,3 +7,5 @@ test_scripts/fb-color.py          usr/share/librem5-devkit/tools/
 test_scripts/vibra_test.py        usr/share/librem5-devkit/tools/
 test_scripts/power_key.py         usr/share/librem5-devkit/tools/
 test_scripts/gps_config.py        usr/share/librem5-devkit/tools/
+test_scripts/switch_test.py       usr/share/librem5-devkit/tools/
+test_scripts/keys_test.py         usr/share/librem5-devkit/tools/
diff --git a/debian/librem5-devkit-base.maintscript b/debian/librem5-devkit-base.maintscript
index db67d9b54c74bc3f939cb9365735d79b212e3034..797e463f8b477d726a8536d6c86ff9cd75af49ff 100644
--- a/debian/librem5-devkit-base.maintscript
+++ b/debian/librem5-devkit-base.maintscript
@@ -1,2 +1,3 @@
-rm_conffile /etc/network/interfaces.d/usb0 0.0.8 librem5-devkit-base
+rm_conffile /etc/network/interfaces.d/usb0 0.0.12 librem5-devkit-base
 rm_conffile /etc/profile.d/etnaviv.sh 0.0.11 librem5-devkit-base
+rm_conffile /etc/udev/rules.d/99-gnss.rules 0.0.12 librem5-devkit-base
diff --git a/debian/librem5-devkit-host.install b/debian/librem5-devkit-host.install
deleted file mode 100644
index c11afe17519ac8ac6167203e22e4c1b04399378f..0000000000000000000000000000000000000000
--- a/debian/librem5-devkit-host.install
+++ /dev/null
@@ -1,2 +0,0 @@
-scripts/librem5-devkit-flash-image /usr/bin/
-scripts/librem5-usbnet /usr/bin/
diff --git a/debian/librem5-devkit-host.examples b/debian/librem5-host.examples
similarity index 100%
rename from debian/librem5-devkit-host.examples
rename to debian/librem5-host.examples
diff --git a/debian/librem5-host.install b/debian/librem5-host.install
new file mode 100644
index 0000000000000000000000000000000000000000..177c019aa81df9816fff55557493c41f0b3ee403
--- /dev/null
+++ b/debian/librem5-host.install
@@ -0,0 +1,2 @@
+scripts/librem5-*flash-image /usr/bin/
+scripts/librem5-usbnet /usr/bin/
diff --git a/debian/librem5-devkit-host.librem5_devkit.udev b/debian/librem5-host.librem5.udev
similarity index 100%
rename from debian/librem5-devkit-host.librem5_devkit.udev
rename to debian/librem5-host.librem5.udev
diff --git a/debian/librem5-host.manpages b/debian/librem5-host.manpages
new file mode 100644
index 0000000000000000000000000000000000000000..7af7843e51d5cc5fb44824ec3a819c2b7709e512
--- /dev/null
+++ b/debian/librem5-host.manpages
@@ -0,0 +1 @@
+librem5-flash-image.1
diff --git a/debian/rules b/debian/rules
index 28a77d902930312b38e6cfee06c7f9a1270c0b29..4dcd28d3e5c80dbfc64a671d0cee0f16453c90c0 100755
--- a/debian/rules
+++ b/debian/rules
@@ -1,10 +1,12 @@
 #!/usr/bin/make -f
 
+include /usr/share/dpkg/default.mk
+
 %:
 	dh $@
 
 override_dh_installudev:
-	dh_installudev --name=librem5_devkit
+	dh_installudev --name=librem5
 
 override_dh_installsystemd:
 	dh_installsystemd --no-start --name=resize_rootfs
@@ -16,3 +18,9 @@ ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))
 else
 	@echo "Checks disabled via DEB_BUILD_OPTIONS"
 endif
+
+override_dh_installman:
+	help2man -N --version-string=$(DEB_VERSION_UPSTREAM) \
+		--version-string=0.0.12 scripts/librem5-flash-image \
+		 > debian/tmp/librem5-flash-image.1
+	dh_installman
diff --git a/pytests/boardtype.py b/pytests/boardtype.py
index f56fa8248ac8df89c38aada7d867c53196b0f39f..0f4325b88466dc88d973fe872e5e1b0c56ad0de5 100644
--- a/pytests/boardtype.py
+++ b/pytests/boardtype.py
@@ -7,8 +7,12 @@ def _get_board_type():
         return f.readline().strip().strip('\00')
 
 
+def is_birch():
+    return _get_board_type() == 'Purism Librem 5r2'
+
+
 def is_librem5():
-    return _get_board_type() == r'Purism Librem 5'
+    return r'Purism Librem 5r' in _get_board_type()
 
 
 def is_devkit():
diff --git a/pytests/test_audio.py b/pytests/test_audio.py
index 57a5aaf980a5974f61b8afc23affe02e31f4625c..69c5c09c3e549f01c613732a92a1320957f5bfcf 100644
--- a/pytests/test_audio.py
+++ b/pytests/test_audio.py
@@ -26,6 +26,11 @@ def test_wm8962():
     assert b"[wm8962]" in out
 
 
+def test_codec_and_modem_cards_exist():
+    assert(os.path.exists('/sys/class/sound/card0'))
+    assert(os.path.exists('/sys/class/sound/card1'))
+
+
 def test_headphone_detection():
     "Check if we registered the headphone detection IRQ"
     f = open("/proc/interrupts")
diff --git a/pytests/test_cameras.py b/pytests/test_cameras.py
new file mode 100644
index 0000000000000000000000000000000000000000..6bad657331ad6eddd6bf5ca8fa0a1d2a291ea6f5
--- /dev/null
+++ b/pytests/test_cameras.py
@@ -0,0 +1,77 @@
+from contextlib import contextmanager
+import subprocess
+import pytest
+
+from . import boardtype
+
+
+def set_power(on):
+    value = 'on' if on else 'auto'
+    address = '10' if boardtype.is_birch() else '2d'
+    with open('/sys/bus/i2c/devices/2-0020/power/control', 'w') as f:
+        f.write(value)
+    with open('/sys/bus/i2c/devices/3-00' + address + '/power/control', 'w') as f:
+        f.write(value)
+
+
+@contextmanager
+def power_up():
+    set_power(True)
+    try:
+        yield
+    finally:
+        set_power(False)
+
+
+@pytest.mark.skipif(
+    boardtype.is_birch(),
+    reason="Birch is missing the connection to the shutdown line.")
+@pytest.mark.skipif(not boardtype.is_librem5(), reason="Not a phone")
+def test_selfie_camera_i2c():
+    with power_up():
+        # TODO: bringup depends on the kernel
+        readout = subprocess.check_output([
+            'i2ctransfer',
+            '-f',  # don't let the loaded driver prevent the test
+            '-y', '2',
+            'w2@0x20', '0x0f', '0x16', 'r2'])
+        assert(readout == b'0x46 0x08\n')
+
+
+@pytest.mark.skipif(not boardtype.is_librem5(), reason="Not a phone")
+def test_big_camera_i2c():
+    with power_up():
+        address = '0x10' if boardtype.is_birch() else '0x2d'
+        readout = subprocess.check_output([
+            'i2ctransfer',
+            '-f',  # don't let the loaded driver prevent the test
+            '-y', '3',
+            'w2@' + address, '0x00', '0x00', 'r2'])
+        assert(readout == b'0x30 0xc6\n')
+
+
+@pytest.mark.skipif(not boardtype.is_librem5(), reason="Not a phone")
+def test_big_camera_focus_driver():
+    """Tests the focus driver based on observed behaviour."""
+
+    def regsetread(byte1, byte2):
+        # The driver should be at 0x18 according to the datasheet,
+        # so leaving this for easy change.
+        address = '0x0c'
+        return subprocess.check_output([
+            'i2ctransfer',
+            '-f',  # don't let any loaded driver prevent the test
+            '-y', '3',
+            'w2@' + address, byte1, byte2, 'r2'])
+
+    with power_up():
+        # Set config bits.
+        readout = regsetread('0x00', '0x0f')
+        assert(readout == b'0x00 0x0f\n')
+        # Switch to "Ringing setting" mode,
+        # which probably means "Change settings".
+        readout = regsetread('0xec', '0xa3')
+        assert(readout == b'0x00 0x00\n')
+        # Switch back to drive mode.
+        readout = regsetread('0xdc', '0x51')
+        assert(readout == b'0x00 0x0f\n')
diff --git a/pytests/test_gnss.py b/pytests/test_gnss.py
new file mode 100644
index 0000000000000000000000000000000000000000..8155f79e1a859405ac63d9dc401445a1653eaf97
--- /dev/null
+++ b/pytests/test_gnss.py
@@ -0,0 +1,9 @@
+from . import boardtype
+
+import os
+import pytest
+
+
+@pytest.mark.skipif(not boardtype.is_librem5(), reason="Not a phone")
+def test_gnss_exists():
+    assert(os.path.exists('/dev/gnss0'))
diff --git a/pytests/test_modem.py b/pytests/test_modem.py
index cb251e9eddc9732f1b4c764a072d9ac593e6967f..128f1063460f9c9e87ad53d6bea8b3c8bf4cba78 100644
--- a/pytests/test_modem.py
+++ b/pytests/test_modem.py
@@ -25,7 +25,7 @@ def test_modem_alsa():
     out = subprocess.run(["aplay", "-l"], capture_output=True)
     assert(out.returncode == 0)
     if boardtype.is_librem5():
-        assert(b"gtm601" in out.stdout)
+        assert((b"gtm601" in out.stdout) or (b"bm818" in out.stdout))
     else:
         assert(b": SIM7100 [SIMCom SIM7100]" in out.stdout)
 
diff --git a/pytests/test_rtc.py b/pytests/test_rtc.py
new file mode 100644
index 0000000000000000000000000000000000000000..57d929487caf7853e0a0a1be0e291edf50a7d65c
--- /dev/null
+++ b/pytests/test_rtc.py
@@ -0,0 +1,3 @@
+def test_rtc0_exists():
+    with open('/sys/class/rtc/rtc0/name') as f:
+        assert('rtc-m41t80' in f.readline().strip())
diff --git a/pytests/test_status_led.py b/pytests/test_status_led.py
new file mode 100644
index 0000000000000000000000000000000000000000..705e414e0cde83ae385eba90dfd4e55fe42c42c8
--- /dev/null
+++ b/pytests/test_status_led.py
@@ -0,0 +1,11 @@
+from . import boardtype
+
+import os
+import pytest
+
+
+@pytest.mark.skipif(not boardtype.is_librem5(), reason="Not a phone")
+def test_status_led_exists():
+    assert(os.path.exists('/sys/class/leds/red:status/brightness'))
+    assert(os.path.exists('/sys/class/leds/green:status/brightness'))
+    assert(os.path.exists('/sys/class/leds/blue:status/brightness'))
diff --git a/pytests/test_torch.py b/pytests/test_torch.py
new file mode 100644
index 0000000000000000000000000000000000000000..8f012ebf0ce617c66bdddab1c0c8a085b7e09dcf
--- /dev/null
+++ b/pytests/test_torch.py
@@ -0,0 +1,9 @@
+from . import boardtype
+
+import os
+import pytest
+
+
+@pytest.mark.skipif(not boardtype.is_librem5(), reason="Not a phone")
+def test_torch_exists():
+    assert(os.path.exists('/sys/class/leds/white:torch/brightness'))
diff --git a/pytests/test_touch.py b/pytests/test_touch.py
index 84e0daa44d21c9069a8e42733bd456a9626ba758..f5a1885e8f7556485c738636304cb61e50993a6b 100644
--- a/pytests/test_touch.py
+++ b/pytests/test_touch.py
@@ -1,5 +1,19 @@
 from . import boardtype
 
+import pytest
+
+
+def read_firmware_version():
+    with open('/sys/kernel/debug/edt_ft5x06/fw_version') as f:
+        return int(f.read())
+
+
+@pytest.mark.skipif(not boardtype.is_librem5(), reason="Not a phone")
+@pytest.mark.skipif(abs(read_firmware_version()) > 100,
+                    reason="Firmware version only readable while screen on.")
+def test_firmware_version():
+    assert(read_firmware_version() == 3)
+
 
 def test_touch_controller():
     if boardtype.is_librem5():
diff --git a/pytests/test_usb_net.py b/pytests/test_usb_net.py
new file mode 100644
index 0000000000000000000000000000000000000000..d99083c1a2ac99ea3650d6b92d272155473a90c8
--- /dev/null
+++ b/pytests/test_usb_net.py
@@ -0,0 +1,5 @@
+import os
+
+
+def test_usb0_exists():
+    assert(os.path.exists('/sys/class/net/usb0'))
diff --git a/scripts/compare_flash.sh b/scripts/compare_flash.sh
new file mode 100755
index 0000000000000000000000000000000000000000..40153f06fc2e90cc04a8469ea255db6c334a76f1
--- /dev/null
+++ b/scripts/compare_flash.sh
@@ -0,0 +1,52 @@
+#!/bin/sh
+
+set -e
+
+if [ $# -lt 1 ]; then
+    echo "Usage: $0 <file>"
+    echo "\tCompare a file to the NOR flash contents"
+    exit 1
+fi
+
+cleanup()
+{
+    [ -z "${UUU_SCRIPT}" ] || rm -f "${UUU_SCRIPT}" 
+}
+trap cleanup EXIT
+
+FILE=$1
+
+SIZE=$(stat --printf="%s" ${FILE})
+SIZE=$(echo "(${SIZE}/4096+1)*4096" | bc)
+HEX_SIZE=$(printf "0x%x" ${SIZE})
+# the last block will contain some junk so only campre the beginning
+SIZE=$(echo "(${SIZE}/4-1024)" | bc)
+HEX_WORDS=$(printf "0x%x" ${SIZE})
+UUU_SCRIPT=$(mktemp -p ./)
+
+cat << EOF > ${UUU_SCRIPT}
+uuu_version 1.0.1
+CFG: FB:  -vid 0x316d -pid 0x4c05
+CFG: SDP: -chip MX8MQ -compatible MX8MQ -vid 0x316d -pid 0x4c05
+
+SDP: boot -f files/u-boot-librem5.imx
+# This command will be run when use SPL
+SDPU: delay 1000
+SDPU: write -f files/u-boot-librem5.imx -offset 0x57c00
+SDPU: jump
+SDPV: delay 1000
+SDPV: write -f files/u-boot-librem5.imx -skipspl
+SDPV: jump
+FB: ucmd setenv fastboot_buffer 0x43000000
+FB: download -f ${FILE}
+FB: ucmd sf probe
+FB: ucmd sf read 0x44000000 0 ${HEX_SIZE}
+FB: delay 2000
+FB: ucmd cmp 0x43000000 0x44000000 ${HEX_WORDS}
+FB: Done
+EOF
+
+echo "About to run ${UUU_SCRIPT}"
+cat ${UUU_SCRIPT}
+
+uuu -v ${UUU_SCRIPT}
diff --git a/scripts/flash-image b/scripts/flash-image
new file mode 100755
index 0000000000000000000000000000000000000000..1a7d5fad2d76348fe082ceb660e1df54f05eae08
--- /dev/null
+++ b/scripts/flash-image
@@ -0,0 +1,123 @@
+#!/usr/bin/env python3
+
+import argparse
+import logging
+import os
+import re
+import shutil
+import subprocess
+import sys
+import tempfile
+
+try:
+    import coloredlogs
+    have_colored_logs = True
+except ImportError:
+    have_colored_logs = False
+
+
+BOARD_TYPE = 'librem5r3'
+IMAGE = '{}.img'
+UBOOT = 'u-boot-{}.imx'
+UUU_SCRIPT = 'flash_{}.lst'
+UUU_SCRIPT_TMPL = '''uuu_version 1.0.1
+
+{mods}
+SDP: boot -f {uboot}
+# This command will be run when use SPL
+SDPU: delay 1000
+SDPU: write -f {uboot} -offset 0x57c00
+SDPU: jump
+SDPV: delay 1000
+SDPV: write -f {uboot} -skipspl
+SDPV: jump
+# This command will be run when ROM support stream mode
+SDPS: boot -f {uboot}
+SDPU: delay 1000
+FB: ucmd setenv fastboot_dev mmc
+FB: ucmd setenv mmcdev 0
+FB: flash -raw2sparse all {image}
+FB: Done
+'''
+
+
+def write_uuu_script(target, image, uboot, mods):
+    with open(target, 'w+') as f:
+        f.write(UUU_SCRIPT_TMPL.format(image=os.path.basename(image),
+                                       uboot=os.path.basename(uboot),
+                                       mods=mods))
+
+
+def flash_image(uuu_target, debug):
+    if debug:
+        subprocess.check_call(['uuu', '-v', uuu_target])
+    else:
+        subprocess.check_call(['uuu', uuu_target])
+
+
+def main():
+    uuu_mods = ''
+    parser = argparse.ArgumentParser(description='Flash a librem5 phone')
+    parser.add_argument('--dir', type=str, default=None,
+                        help='Look in dir for image and u-boot')
+    parser.add_argument('--board', type=str, default=BOARD_TYPE,
+                        help='Name of the image to flash ( devkit, librem5r2, librem5r3 )')
+
+    group = parser.add_argument_group(title='Testing and debugging options')
+    group.add_argument('--debug', action="store_true", default=False,
+                       help='Enable debug output')
+    group.add_argument('--uboot', type=str,
+                       help='u-boot version to flash')
+    args = parser.parse_args()
+
+    level = logging.DEBUG if args.debug else logging.INFO
+    if have_colored_logs:
+        coloredlogs.install(level=level, fmt='%(asctime)s %(levelname)s %(message)s')
+    else:
+        logging.basicConfig(level=level, format='%(asctime)s %(levelname)s %(message)s')
+
+    # uboot builds don't carry board revisions (yet?)
+    if args.uboot:
+        uboot_board = args.uboot
+    else:
+        if re.match('librem5r[0-9]$', args.board):
+            uboot_board = args.board[:-2]
+
+    imagedir = args.dir if args.dir is not None else '.'
+    outdir = tempfile.mkdtemp(prefix='flash_', dir='.')
+
+    try:
+        logging.info("loading from {}".format(imagedir))
+        if args.dir == outdir:
+            os.makedirs(args.dir, exist_ok=True)
+
+        image_target = os.path.join(imagedir, IMAGE.format(args.board))
+        uboot_target = os.path.join(imagedir, UBOOT.format(uboot_board))
+        uuu_target = os.path.join(outdir, UUU_SCRIPT.format(args.board))
+
+        if not os.path.isfile(image_target) and not os.path.isfile(uboot_target):
+            raise FileNotFoundError
+
+        if not os.path.isabs(image_target):
+            image_target = os.path.join('..', image_target)
+        if not os.path.isabs(uboot_target):
+            uboot_target = os.path.join('..', uboot_target)
+
+        os.symlink(image_target, os.path.join(outdir, IMAGE.format(args.board)))
+        os.symlink(uboot_target, os.path.join(outdir, UBOOT.format(uboot_board)))
+
+        write_uuu_script(uuu_target, image_target, uboot_target, uuu_mods)
+        flash_image(uuu_target, args.debug)
+    except KeyboardInterrupt:
+        logging.error("CTRL-C pressed.")
+        return 1
+    finally:
+        if args.dir != outdir:
+            logging.info("Cleaning up.")
+            shutil.rmtree(outdir)
+
+    return 0
+
+
+if __name__ == '__main__':
+    sys.exit(main())
diff --git a/scripts/librem5-devkit-flash-image b/scripts/librem5-devkit-flash-image
deleted file mode 100755
index 635133e878137f0de1e697e1900d31ddf9874967..0000000000000000000000000000000000000000
--- a/scripts/librem5-devkit-flash-image
+++ /dev/null
@@ -1,344 +0,0 @@
-#!/usr/bin/env python3
-
-import argparse
-import datetime
-import hashlib
-import itertools
-import jenkins
-import logging
-import lzma
-import os
-import requests
-import shutil
-import subprocess
-import sys
-import tempfile
-import time
-import tqdm
-import yaml
-
-try:
-    import coloredlogs
-    have_colored_logs = True
-except ImportError:
-    have_colored_logs = False
-
-
-from urllib.parse import urljoin
-
-JENKINS = 'https://arm01.puri.sm'
-BOARD_TYPE = 'devkit'
-BOARD_VARIANT = 'current'
-DIST = 'buster+ci'
-IMAGE = '{}.img'
-META_YAML = 'files/meta.yml'
-IMAGE_JOB_NAME = 'Images/Image Build'
-UBOOT = 'u-boot-{}.imx'
-UBOOT_JOB_NAME = 'u-boot_builds/uboot_{}_build'
-UUU_SCRIPT = 'flash_{}.lst'
-UUU_SCRIPT_TMPL = '''uuu_version 1.0.1
-
-{mods}
-SDP: boot -f {uboot}
-# This command will be run when use SPL
-SDPU: delay 1000
-SDPU: write -f {uboot} -offset 0x57c00
-SDPU: jump
-SDPV: delay 1000
-SDPV: write -f {uboot} -skipspl
-SDPV: jump
-# This command will be run when ROM support stream mode
-SDPS: boot -f {uboot}
-SDPU: delay 1000
-#FB: ucmd setenv fastboot_buffer 0x43000000
-FB: ucmd setenv fastboot_dev mmc
-FB: ucmd setenv mmcdev 0
-FB: flash -raw2sparse all {image}
-FB: Done
-'''
-BLOCK_SIZE = 8192
-UNCOMPRESSED_SIZE = 3600000000
-
-
-class VerifyImageException(Exception):
-    pass
-
-
-class PrematureEndException(Exception):
-    pass
-
-
-def verify_image(image, meta):
-    m = hashlib.sha256()
-    size = int(meta['image']['size'])
-    hexdigest = meta['image']['sha256sum']
-
-    filesize = os.path.getsize(image)
-    if filesize != size:
-        raise VerifyImageException(
-            "Image file \"{}\" size {} does not match {}".format(
-                os.path.basename(image), filesize, size))
-
-    logging.info("Calculating sha256sum of {}".format(image))
-    bar = tqdm.tqdm(total=size,
-                    desc='Checking',
-                    leave=False)
-    with open(image, 'rb') as f:
-        while True:
-            data = f.read(BLOCK_SIZE)
-            if data:
-                m.update(data)
-                bar.update(n=len(data))
-            else:
-                break
-    bar.close()
-    if m.hexdigest() != hexdigest:
-        raise VerifyImageException("Checksum of image {} "
-                                   "does not match {}".format(m.hexdigest(), hexdigest))
-
-
-def resuming_stream(url, expected_size, max_attempts):
-    position = 0
-
-    if max_attempts < 1:
-        retries = itertools.count()
-    else:
-        retries = range(max_attempts)
-
-    for i in retries:
-        try:
-            resp = requests.get(url,
-                                stream=True,
-                                headers={'Range': 'bytes={}-'.format(position)}
-                                )
-            resp.raise_for_status()
-
-            if resp.status_code != requests.codes.partial_content:
-                position = 0
-            logging.debug('Proceeding from {} bytes'.format(position))
-
-            for data in resp.iter_content(BLOCK_SIZE):
-                position += len(data)
-                yield data
-
-            if position < expected_size:
-                raise PrematureEndException()
-            return
-        except (requests.exceptions.ConnectionError, PrematureEndException):
-            if i == max_attempts - 1:
-                logging.error("Max connection errors reached, aborting")
-                raise
-            logging.info("Connection error, retrying")
-            time.sleep(5)
-
-
-def stream_file(url, attempts):
-    resp = requests.head(url, stream=True)
-    resp.raise_for_status()
-    ts = int(resp.headers.get('content-length', 0))
-    return resuming_stream(url, ts, attempts), ts
-
-
-def needs_download(target, meta):
-    if not os.path.exists(target):
-        return True
-
-    try:
-        verify_image(target, meta)
-    except VerifyImageException:
-        return True
-
-    return False
-
-
-def download_image(url, target, attempts):
-    decomp = lzma.LZMADecompressor()
-
-    # We expect metadata to be right next to the image
-    meta_yml_url = "{}/{}".format(url.rsplit('/', 1)[0], META_YAML)
-    resp = requests.get(meta_yml_url)
-    resp.raise_for_status()
-    meta = yaml.safe_load(resp.text)
-    uncompressed_size = int(meta['image']['size'])
-    logging.debug("Image size is %d", uncompressed_size)
-
-    if not needs_download(target, meta):
-        logging.info("Image already up to date - no download needed.")
-        return
-
-    logging.info("Downloading image from {}".format(url))
-    stream, ts = stream_file(url, attempts)
-    download_bar = tqdm.tqdm(total=ts,
-                             desc='Download',
-                             leave=False)
-    decompress_bar = tqdm.tqdm(total=uncompressed_size,
-                               desc='Decompr.',
-                               leave=False)
-    with open(target, 'wb+') as f:
-        for data in stream:
-            if data:
-                out = decomp.decompress(data)
-                decompress_bar.update(len(out))
-                f.write(out)
-            download_bar.update(n=len(data))
-    download_bar.close()
-    decompress_bar.close()
-    verify_image(target, meta)
-
-
-def find_image(jobname, type, variant, dist):
-    server = jenkins.Jenkins(JENKINS)
-    logging.info("Looking for {} {} {} image".format(type, variant, dist))
-    info = server.get_job_info(jobname)
-    for build in info['builds']:
-        resp = requests.get(build['url'] + '/api/json')
-        resp.raise_for_status()
-        json = resp.json()
-        if (json['description'].startswith(variant + ' ' + type) and
-                dist in json['description'] and
-                json['result'] == 'SUCCESS'):
-            found = json
-            break
-    else:
-        found = None
-    return found
-
-
-def find_uboot(jobname):
-    server = jenkins.Jenkins(JENKINS)
-
-    info = server.get_job_info(jobname)
-    for build in info['builds']:
-        resp = requests.get(build['url'] + '/api/json')
-        resp.raise_for_status()
-        json = resp.json()
-        if (json['result'] == 'SUCCESS'):
-            found = json
-            break
-    else:
-        found = None
-    return found
-
-
-def download_uboot(url, target):
-    logging.info("Downloading uboot from {}".format(url))
-    resp = requests.get(url, stream=True)
-    resp.raise_for_status()
-    ts = int(resp.headers.get('content-length', 0))
-    bar = tqdm.tqdm(total=ts, leave=False)
-    with open(target, 'wb+') as f:
-        for data in resp.iter_content(BLOCK_SIZE):
-            if data:
-                f.write(data)
-            bar.update(n=len(data))
-
-
-def write_uuu_script(target, image, uboot, mods):
-    with open(target, 'w+') as f:
-        f.write(UUU_SCRIPT_TMPL.format(image=os.path.basename(image),
-                                       uboot=os.path.basename(uboot),
-                                       mods=mods))
-
-
-def flash_image(uuu_target, debug):
-    if debug:
-        subprocess.check_call(['uuu', '-v', uuu_target])
-    else:
-        subprocess.check_call(['uuu', uuu_target])
-
-
-def main():
-    uuu_mods = ''
-    parser = argparse.ArgumentParser(description='Process some integers.')
-    parser.add_argument('--dir', type=str, default=None,
-                        help='Download files to dir (instead of a temporary directory)')
-    parser.add_argument('--dist', type=str, default=DIST,
-                        help="Download an image for this distribution, default is '{}'".format(DIST))
-    parser.add_argument('--skip-cleanup', action='store_true', default=False,
-                        help='Skip temporary directory cleanup')
-    parser.add_argument('--skip-flash', action='store_true', default=False,
-                        help='Do all the preparations but don\'t flash')
-    parser.add_argument('--download-attempts', type=int, default=10,
-                        help="Maximum number of attempts to resume "
-                        "devkit image download. 0-unlimited")
-    parser.add_argument('--variant', choices=['legacy', 'current', 'next', 'plain'], default=BOARD_VARIANT,
-                        help='Variant of the board to download ( legacy, current, next )')
-    parser.add_argument('--board', choices=['devkit', 'librem5'], default=BOARD_TYPE,
-                        help='Type of the board to download ( devkit, librem5 )')
-    parser.add_argument('--board_rev', choices=['chestnut', 'dogwood'],
-                        help='Revison of the phone ( chestnut, dogwood )')
-
-    group = parser.add_argument_group(title='Testing and debugging options')
-    group.add_argument('--debug', action="store_true", default=False,
-                       help='Enable debug output')
-    group.add_argument('--image-job', type=str, default=IMAGE_JOB_NAME,
-                       help='Jenkins job to download the image from')
-    group.add_argument('--uboot-job', type=str,
-                       help='Jenkins job to download the uboot from')
-    args = parser.parse_args()
-
-    level = logging.DEBUG if args.debug else logging.INFO
-    if have_colored_logs:
-        coloredlogs.install(level=level, fmt='%(asctime)s %(levelname)s %(message)s')
-    else:
-        logging.basicConfig(level=level, format='%(asctime)s %(levelname)s %(message)s')
-
-    # Check available downloads upfront so it's less likely we fail
-    # later:
-    image_ref = find_image(args.image_job, args.board, args.variant, args.dist)
-    if image_ref:
-        image_ref['ts'] = datetime.datetime.fromtimestamp(image_ref['timestamp'] / 1000).strftime('%c')
-        logging.info("Found disk image Build {id} '{description}' from {ts}".format(**image_ref))
-    else:
-        logging.error("No matching image found")
-        return 1
-
-    if args.uboot_job:
-        uboot_ref = find_uboot(args.uboot_job)
-    else:
-        uboot_ref = find_uboot(UBOOT_JOB_NAME.format(args.board))
-
-    if uboot_ref:
-        uboot_ref['ts'] = datetime.datetime.fromtimestamp(uboot_ref['timestamp'] / 1000).strftime('%c')
-        logging.info("Found uboot Build {id} from {ts}".format(**uboot_ref))
-    else:
-        logging.error("No matching uboot found")
-        return 1
-
-    if args.board_rev and args.board_rev in ["dogwood"]:
-        uuu_mods = "CFG: SDP: -chip MX8MQ -compatible MX8MQ -vid 0x316d -pid 0x4c05"
-
-    outdir = args.dir if args.dir is not None else tempfile.mkdtemp(prefix='devkit_image_', dir='.')
-    try:
-        logging.info("Downloading to {}".format(outdir))
-        if args.dir == outdir:
-            os.makedirs(args.dir, exist_ok=True)
-
-        image_target = os.path.join(outdir, IMAGE.format(args.board))
-        uboot_target = os.path.join(outdir, UBOOT.format(args.board))
-        uuu_target = os.path.join(outdir, UUU_SCRIPT.format(args.board))
-
-        download_image(urljoin(image_ref['url'], 'artifact/{}.xz').format(IMAGE.format(args.board)),
-                       image_target, args.download_attempts)
-        download_uboot(urljoin(uboot_ref['url'],
-                       'artifact/output/uboot-{}/{}'.format(args.board, UBOOT.format(args.board))),
-                       uboot_target)
-        write_uuu_script(uuu_target, image_target, uboot_target, uuu_mods)
-        if not args.skip_flash:
-            flash_image(uuu_target, args.debug)
-    except VerifyImageException as e:
-        logging.error(e)
-        return 1
-    except KeyboardInterrupt:
-        logging.error("CTRL-C pressed.")
-        return 1
-    finally:
-        if args.dir != outdir and not args.skip_cleanup:
-            logging.info("Cleaning up.")
-            shutil.rmtree(outdir)
-
-    return 0
-
-
-if __name__ == '__main__':
-    sys.exit(main())
diff --git a/scripts/librem5-devkit-flash-image b/scripts/librem5-devkit-flash-image
new file mode 120000
index 0000000000000000000000000000000000000000..05296ada6ca435e4b8887b13263e84dbd9e105f4
--- /dev/null
+++ b/scripts/librem5-devkit-flash-image
@@ -0,0 +1 @@
+librem5-flash-image
\ No newline at end of file
diff --git a/scripts/librem5-flash-image b/scripts/librem5-flash-image
new file mode 100755
index 0000000000000000000000000000000000000000..7cd6828addd3170940d74b4eb48464e9db591399
--- /dev/null
+++ b/scripts/librem5-flash-image
@@ -0,0 +1,356 @@
+#!/usr/bin/env python3
+
+import argparse
+import datetime
+import hashlib
+import itertools
+import jenkins
+import logging
+import lzma
+import os
+import re
+import requests
+import shutil
+import subprocess
+import sys
+import tempfile
+import time
+import tqdm
+import yaml
+
+try:
+    import coloredlogs
+    have_colored_logs = True
+except ImportError:
+    have_colored_logs = False
+
+
+from urllib.parse import urljoin
+
+JENKINS = 'https://arm01.puri.sm'
+BOARD_TYPE = 'librem5r3'
+BOARD_VARIANT = 'plain'
+DIST = 'amber-phone'
+IMAGE = '{}.img'
+META_YAML = 'files/meta.yml'
+IMAGE_JOB_NAME = 'Images/Image Build'
+UBOOT = 'u-boot-{}.imx'
+UBOOT_JOB_NAME = 'u-boot_builds/uboot_{}_build'
+UUU_SCRIPT = 'flash_{}.lst'
+UUU_SCRIPT_TMPL = '''uuu_version 1.0.1
+
+{mods}
+SDP: boot -f {uboot}
+# This command will be run when use SPL
+SDPU: delay 1000
+SDPU: write -f {uboot} -offset 0x57c00
+SDPU: jump
+SDPV: delay 1000
+SDPV: write -f {uboot} -skipspl
+SDPV: jump
+# This command will be run when ROM support stream mode
+SDPS: boot -f {uboot}
+SDPU: delay 1000
+#FB: ucmd setenv fastboot_buffer 0x43000000
+FB: ucmd setenv fastboot_dev mmc
+FB: ucmd setenv mmcdev 0
+FB: flash -raw2sparse all {image}
+FB: Done
+'''
+BLOCK_SIZE = 8192
+RETRIES = 10
+
+
+class VerifyImageException(Exception):
+    pass
+
+
+class PrematureEndException(Exception):
+    pass
+
+
+def verify_image(image, meta):
+    m = hashlib.sha256()
+    size = int(meta['image']['size'])
+    hexdigest = meta['image']['sha256sum']
+
+    filesize = os.path.getsize(image)
+    if filesize != size:
+        raise VerifyImageException(
+            "Image file \"{}\" size {} does not match {}".format(
+                os.path.basename(image), filesize, size))
+
+    logging.info("Calculating sha256sum of {}".format(image))
+    bar = tqdm.tqdm(total=size,
+                    desc='Checking',
+                    leave=False)
+    with open(image, 'rb') as f:
+        while True:
+            data = f.read(BLOCK_SIZE)
+            if data:
+                m.update(data)
+                bar.update(n=len(data))
+            else:
+                break
+    bar.close()
+    if m.hexdigest() != hexdigest:
+        raise VerifyImageException("Checksum of image {} "
+                                   "does not match {}".format(m.hexdigest(), hexdigest))
+
+
+def resuming_stream(url, expected_size, max_attempts):
+    position = 0
+
+    if max_attempts < 1:
+        retries = itertools.count()
+    else:
+        retries = range(max_attempts)
+
+    for i in retries:
+        try:
+            resp = requests.get(url,
+                                stream=True,
+                                headers={'Range': 'bytes={}-'.format(position)}
+                                )
+            resp.raise_for_status()
+
+            if resp.status_code != requests.codes.partial_content:
+                position = 0
+            logging.debug('Proceeding from {} bytes'.format(position))
+
+            for data in resp.iter_content(BLOCK_SIZE):
+                position += len(data)
+                yield data
+
+            if position < expected_size:
+                raise PrematureEndException()
+            return
+        except (requests.exceptions.ConnectionError, PrematureEndException):
+            if i == max_attempts - 1:
+                logging.error("Max connection errors reached, aborting")
+                raise
+            logging.info("Connection error, retrying")
+            time.sleep(5)
+
+
+def stream_file(url, attempts):
+    resp = requests.head(url, stream=True)
+    resp.raise_for_status()
+    ts = int(resp.headers.get('content-length', 0))
+    return resuming_stream(url, ts, attempts), ts
+
+
+def needs_download(target, meta):
+    if not os.path.exists(target):
+        return True
+
+    try:
+        verify_image(target, meta)
+    except VerifyImageException:
+        return True
+
+    return False
+
+
+def download_image(url, target, attempts):
+    decomp = lzma.LZMADecompressor()
+
+    # We expect metadata to be right next to the image
+    meta_yml_url = "{}/{}".format(url.rsplit('/', 1)[0], META_YAML)
+    resp = requests.get(meta_yml_url)
+    resp.raise_for_status()
+    meta = yaml.safe_load(resp.text)
+    uncompressed_size = int(meta['image']['size'])
+    logging.debug("Image size is %d", uncompressed_size)
+
+    if not needs_download(target, meta):
+        logging.info("Image already up to date - no download needed.")
+        return
+
+    logging.info("Downloading image from {}".format(url))
+    stream, ts = stream_file(url, attempts)
+    download_bar = tqdm.tqdm(total=ts,
+                             desc='Download',
+                             leave=False)
+    decompress_bar = tqdm.tqdm(total=uncompressed_size,
+                               desc='Decompr.',
+                               leave=False)
+    with open(target, 'wb+') as f:
+        for data in stream:
+            if data:
+                out = decomp.decompress(data)
+                decompress_bar.update(len(out))
+                f.write(out)
+            download_bar.update(n=len(data))
+    download_bar.close()
+    decompress_bar.close()
+    verify_image(target, meta)
+
+
+def find_image(jobname, type, variant, dist):
+    server = jenkins.Jenkins(JENKINS)
+    logging.info("Looking for {} {} {} image".format(type, variant, dist))
+    try:
+        info = server.get_job_info(jobname)
+    except jenkins.NotFoundException:
+        logging.error("Job %s not found", jobname)
+        return None
+    for build in info['builds']:
+        resp = requests.get(build['url'] + '/api/json')
+        resp.raise_for_status()
+        json = resp.json()
+        if (json['description'].startswith(variant + ' ' + type) and
+                dist in json['description'] and
+                json['result'] == 'SUCCESS'):
+            found = json
+            break
+    else:
+        found = None
+    return found
+
+
+def find_uboot(jobname):
+    server = jenkins.Jenkins(JENKINS)
+
+    try:
+        info = server.get_job_info(jobname)
+    except jenkins.NotFoundException:
+        logging.error("Job %s not found", jobname)
+        return None
+    for build in info['builds']:
+        resp = requests.get(build['url'] + '/api/json')
+        resp.raise_for_status()
+        json = resp.json()
+        if (json['result'] == 'SUCCESS'):
+            found = json
+            break
+    else:
+        found = None
+    return found
+
+
+def download_uboot(url, target):
+    logging.info("Downloading uboot from {}".format(url))
+    resp = requests.get(url, stream=True)
+    resp.raise_for_status()
+    ts = int(resp.headers.get('content-length', 0))
+    bar = tqdm.tqdm(total=ts, leave=False)
+    with open(target, 'wb+') as f:
+        for data in resp.iter_content(BLOCK_SIZE):
+            if data:
+                f.write(data)
+            bar.update(n=len(data))
+
+
+def write_uuu_script(target, image, uboot, mods):
+    with open(target, 'w+') as f:
+        f.write(UUU_SCRIPT_TMPL.format(image=os.path.basename(image),
+                                       uboot=os.path.basename(uboot),
+                                       mods=mods))
+
+
+def flash_image(uuu_target, debug):
+    if debug:
+        subprocess.check_call(['uuu', '-v', uuu_target])
+    else:
+        subprocess.check_call(['uuu', uuu_target])
+
+
+def main():
+    uuu_mods = ''
+    parser = argparse.ArgumentParser(description='Flash a Librem 5 or Librem 5 Devkit.')
+    parser.add_argument('--dir', type=str, default=None,
+                        help='Download files to dir (instead of a temporary directory)')
+    parser.add_argument('--dist', type=str, default=DIST,
+                        help="Download an image for this distribution, default is '{}'".format(DIST))
+    parser.add_argument('--skip-cleanup', action='store_true', default=False,
+                        help='Skip temporary directory cleanup')
+    parser.add_argument('--skip-flash', action='store_true', default=False,
+                        help='Do all the preparations but don\'t flash')
+    parser.add_argument('--download-attempts', type=int, default=RETRIES,
+                        help="Maximum number of attempts to resume "
+                        "image download. 0: unlimited, default is {}".format(RETRIES))
+    parser.add_argument('--variant', choices=['legacy', 'current', 'next', 'plain'], default=BOARD_VARIANT,
+                        help="Variant of the board to download ( legacy, current, next ), "
+                        "default is '{}'".format(BOARD_VARIANT))
+    parser.add_argument('--board', choices=['devkit', 'librem5r2', 'librem5r3', 'librem5r4'], default=BOARD_TYPE,
+                        help="Type of the board to download ( devkit, librem5r2, librem5r3, librem5r4 ) "
+                        "default is '{}'".format(BOARD_TYPE))
+
+    group = parser.add_argument_group(title='Testing and debugging options')
+    group.add_argument('--debug', action="store_true", default=False,
+                       help='Enable debug output')
+    group.add_argument('--image-job', type=str, default=IMAGE_JOB_NAME,
+                       help='Jenkins job to download the image from')
+    group.add_argument('--uboot-job', type=str,
+                       help='Jenkins job to download the uboot from')
+    args = parser.parse_args()
+
+    level = logging.DEBUG if args.debug else logging.INFO
+    if have_colored_logs:
+        coloredlogs.install(level=level, fmt='%(asctime)s %(levelname)s %(message)s')
+    else:
+        logging.basicConfig(level=level, format='%(asctime)s %(levelname)s %(message)s')
+
+    # Check available downloads upfront so it's less likely we fail
+    # later:
+    image_ref = find_image(args.image_job, args.board, args.variant, args.dist)
+    if image_ref:
+        image_ref['ts'] = datetime.datetime.fromtimestamp(image_ref['timestamp'] / 1000).strftime('%c')
+        logging.info("Found disk image Build {id} '{description}' from {ts}".format(**image_ref))
+    else:
+        logging.error("No matching image found")
+        return 1
+
+    if args.uboot_job:
+        uboot_ref = find_uboot(args.uboot_job)
+        uboot_board = args.board
+    else:
+        # uboot builds don't carry board revisions (yet?)
+        uboot_board = args.board[:-2] if re.match('librem5r[0-9]$', args.board) else args.board
+        uboot_ref = find_uboot(UBOOT_JOB_NAME.format(uboot_board))
+
+    if uboot_ref:
+        uboot_ref['ts'] = datetime.datetime.fromtimestamp(uboot_ref['timestamp'] / 1000).strftime('%c')
+        logging.info("Found uboot Build {id} from {ts}".format(**uboot_ref))
+    else:
+        logging.error("No matching uboot found")
+        return 1
+
+    if args.board in ["librem5r3", 'librem5r4']:
+        uuu_mods = "CFG: SDP: -chip MX8MQ -compatible MX8MQ -vid 0x316d -pid 0x4c05"
+
+    outdir = args.dir if args.dir is not None else tempfile.mkdtemp(prefix='devkit_image_', dir='.')
+    try:
+        logging.info("Downloading to {}".format(outdir))
+        if args.dir == outdir:
+            os.makedirs(args.dir, exist_ok=True)
+
+        image_target = os.path.join(outdir, IMAGE.format(args.board))
+        uboot_target = os.path.join(outdir, UBOOT.format(uboot_board))
+        uuu_target = os.path.join(outdir, UUU_SCRIPT.format(args.board))
+
+        download_image(urljoin(image_ref['url'], 'artifact/{}.xz').format(IMAGE.format(args.board)),
+                       image_target, args.download_attempts)
+        download_uboot(urljoin(uboot_ref['url'],
+                       'artifact/output/uboot-{}/{}'.format(uboot_board, UBOOT.format(uboot_board))),
+                       uboot_target)
+        write_uuu_script(uuu_target, image_target, uboot_target, uuu_mods)
+        if not args.skip_flash:
+            flash_image(uuu_target, args.debug)
+    except VerifyImageException as e:
+        logging.error(e)
+        return 1
+    except KeyboardInterrupt:
+        logging.error("CTRL-C pressed.")
+        return 1
+    finally:
+        if args.dir != outdir and not args.skip_cleanup:
+            logging.info("Cleaning up.")
+            shutil.rmtree(outdir)
+
+    return 0
+
+
+if __name__ == '__main__':
+    sys.exit(main())
diff --git a/test_scripts/keys_test.py b/test_scripts/keys_test.py
new file mode 100755
index 0000000000000000000000000000000000000000..158803620e80a5747c1949297a25ea623d2e8fe4
--- /dev/null
+++ b/test_scripts/keys_test.py
@@ -0,0 +1,86 @@
+#!/usr/bin/python3
+
+from evdev import ecodes, InputDevice
+from time import time
+from select import select
+import sys
+import evdev
+
+gpiokeys = {
+    ecodes.KEY_WIMAX: 0,
+    ecodes.KEY_WLAN: 0,
+    ecodes.KEY_CAMERA: 0,
+    ecodes.KEY_VOLUMEUP: 0,
+    ecodes.KEY_VOLUMEDOWN: 0,
+}
+
+powerkey = {
+    ecodes.KEY_POWER: 0
+}
+
+key_on = 1
+key_off = 2
+
+
+def keytest(name, keys):
+    path = [path for path in evdev.list_devices() if evdev.InputDevice(path).name == name]
+    if not path:
+        print("key path not found ", path)
+        return 1
+
+    foundpath = ''.join(path)
+    dev = InputDevice(foundpath)
+
+    starttime = time()
+    while time() - starttime < 60:
+        # Block for a 500ms or until there are events to be read.
+        r, _, _ = select([dev], [], [], 0.5)
+
+        if r:
+            for event in dev.read():
+                if event.type == ecodes.EV_KEY:
+                    print("got key :", event)
+                    if event.code in keys:
+                        if event.value == 1:
+                            keys[event.code] = keys[event.code] | key_on
+                        elif event.value == 0:
+                            keys[event.code] = keys[event.code] | key_off
+
+        count = 0
+        for key in keys:
+            if keys[key] == 3:
+                count = count + 1
+
+        if count == len(keys):
+            print("All keys toggled")
+            return 0
+
+    print("key test timed out ", keys)
+    return 1
+
+
+def testgpiokeys():
+    print("toggle all of the switches and push the volume buttons")
+    return keytest('gpio-keys', gpiokeys)
+
+
+def testpowerkey():
+    print("press and release the power button")
+    return keytest('30370000.snvs:snvs-powerkey', powerkey)
+
+
+def main(args):
+    if testgpiokeys():
+        print("gpio key test : FAILED")
+        return 1
+
+    if testpowerkey():
+        print("power key test : FAILED")
+        return 1
+
+    print("key tests : PASSED")
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main(sys.argv))
diff --git a/uuu_scripts/burn_fuses_dogwood.lst b/uuu_scripts/burn_fuses_dogwood.lst
index 6767f9a090ea48508d62f842c711c117a85c72ef..438f64c994c21e91d71181f73e1d8cd384c4dc1a 100644
--- a/uuu_scripts/burn_fuses_dogwood.lst
+++ b/uuu_scripts/burn_fuses_dogwood.lst
@@ -18,8 +18,6 @@ SDPS: boot -f ../files/u-boot-librem5.imx
 
 # this is the eMMC configuration
 FB: ucmd fuse prog -y 1 3 0x10002022
-# USB PID:VID - fusemap 0x620
-FB: ucmd fuse prog -y 8 2 0x4c05316d
-# Dogwood is version 3 - fusemap 0x780
-FB: ucmd fuse prog -y 14 0 3
+# Dogwood is version 3 
+FB: ucmd fuse prog -y 9 0 3
 FB: Done
diff --git a/uuu_scripts/flash_librem5.lst b/uuu_scripts/burn_fuses_evergreen.lst
similarity index 54%
rename from uuu_scripts/flash_librem5.lst
rename to uuu_scripts/burn_fuses_evergreen.lst
index bbd2f054eda3c86bc6687ad4dc58a0548125b628..a42e046dc9dbfef01e6d5ada3b9ec2ddac0a29e6 100644
--- a/uuu_scripts/flash_librem5.lst
+++ b/uuu_scripts/burn_fuses_evergreen.lst
@@ -1,8 +1,6 @@
 uuu_version 1.0.1
-# Using the uboot in ../files/u-boot-librem5.imx
-# flash a new image ../files/librem5.img
-CFG: FB:  -vid 0x316d -pid 0x4c05
-CFG: SDP: -chip MX8MQ -compatible MX8MQ -vid 0x316d -pid 0x4c05
+# Set the boot mode fuses to boot from eMMC
+## If the board has already been programmed this will time out
 
 SDP: boot -f ../files/u-boot-librem5.imx
 
@@ -18,8 +16,8 @@ SDPV: jump
 # This command will be run when ROM support stream mode
 SDPS: boot -f ../files/u-boot-librem5.imx
 
-FB: ucmd mmc dev 0
-FB: ucmd setenv fastboot_dev mmc
-FB: ucmd setenv mmcdev 0
-FB: flash -raw2sparse all ../files/librem5.img
+# this is the eMMC configuration
+FB: ucmd fuse prog -y 1 3 0x10002022
+# Evergreen is version 4
+FB: ucmd fuse prog -y 9 0 4
 FB: Done
diff --git a/uuu_scripts/flash_librem5-devkit.lst b/uuu_scripts/flash_librem5-devkit.lst
deleted file mode 100644
index abbcd046dbf11b11653f4b455d59fc1dd4fa1dd3..0000000000000000000000000000000000000000
--- a/uuu_scripts/flash_librem5-devkit.lst
+++ /dev/null
@@ -1,19 +0,0 @@
-uuu_version 1.0.1
-# Using the uboot in ../files/u-boot-devkit.imx
-# flash a new image ../files/devkit.img
-
-SDP: boot -f ../files/u-boot-devkit.imx
-# This command will be run when use SPL
-SDPU: delay 1000
-SDPU: write -f ../files/u-boot-devkit.imx -offset 0x57c00
-SDPU: jump
-
-SDPV: delay 1000
-SDPV: write -f ../files/u-boot-devkit.imx -skipspl
-SDPV: jump
-
-FB: ucmd mmc dev 0
-FB: ucmd setenv fastboot_dev mmc
-FB: ucmd setenv mmcdev 0
-FB: flash -raw2sparse all ../files/devkit.img
-FB: Done