Convincing a Sony a6000 to transfer photos over Kermit-over-Telnet
I recently got myself a Sony a6000
(well, the α6000, GREEK SMALL LETTER ALPHA, but we all get ASCIIified some day).
It is a very nice camera and
I really can’t complain about the quality of the photos it takes…
but I sure can complain about how you get the photos out.
You get 3 standard options, in descending order of speed:
-
pulling the SD card manually
-
microUSB cable
-
WiFi, via apps that you can’t get anymore from Sony. (and it doesn’t pull raws, a really annoying limitation for how I work)
I don’t especially like handling SD cards, I bought a microUSB cable for the first time in years for this camera alone (at least it supports charging the battery using USB Battery Charging 1.2, at a whopping… 5V/1.5A, or 7.5W), and the WiFi thing is unworkable.
Let’s marginally improve on the WiFi thing. This guide may or not work for other Sony cameras; I only have the a6000 to test with.
A brief review of current warranty-voiding transfer methods
I don’t think there’s a single a6000 under warranty anymore, so let’s just install some third-party apps, insofar as the community actually exists.
pmcaFilesystemServer: somewhat annoying to script against, subject to HTTP’s lack of robustness
Wireless ADB (via OpenMemories: Tweak): needs you to install ADB on host side, slowest method
FTP (OM: Tweak, log in via Telnet daemon):
one-at-a-time via the included BusyBox’s ftpget and ftpput,
needs knowledge of the local side’s IP.
None of these methods are fast; at best, I measure speeds around 500 kbyte/s, half the speed that the WiFi chip in here is capable of. Wireless ADB is especially bad here, that one’s a mere 250 kbyte/s!
Let’s take a look inside
I’m going to assume you have OpenMemories: Tweak installed so you can start the Telnet daemon in here. If you haven’t, go do that first.
Connect your camera to any convenient 2.4 GHz WiFi network you might have lying around; you can also use your phone’s hotspot if you can drop it to 2.4 GHz. Let’s Telnet in, and… the BusyBox in here is spartan.
BusyBox v1.13.4 (2012-07-17 15:11:04 JST) multi-call binary
Copyright (C) 1998-2008 Erik Andersen, Rob Landley, Denys Vlasenko
and others. Licensed under GPLv2.
See source distribution for full notice.
Usage: busybox [function] [arguments]...
or: function [arguments]...
BusyBox is a multi-call binary that combines many common Unix
utilities into a single executable. Most people will create a
link to busybox for each function they wish to use and BusyBox
will act like whatever it was invoked as!
Currently defined functions:
[, [[, ash, basename, bunzip2, bzcat, cat, chgrp, chmod, chown,
chroot, cmp, cp, cpio, cut, date, dd, dirname, dmesg, echo, egrep,
env, expr, false, fgrep, freeramdisk, ftpget, ftpput, fuser,
getty, grep, gunzip, gzip, halt, ifconfig, insmod, kill, killall,
klogd, ln, logger, login, ls, lsmod, md5sum, mkdir, mkfifo, mknod,
mktemp, modprobe, mount, mv, pidof, ping, ping6, pivot_root,
poweroff, pwd, readlink, reboot, rm, rmdir, rmmod, route, sed,
sfdisk, sh, sha1sum, sleep, sort, stty, swapoff, swapon, switch_root,
sync, syslogd, tail, tar, taskset, telnet, telnetd, test, touch,
true, umount, uname, uniq, usleep, vconfig, wc, xargs, zcat
If it weren’t for the ftpget and ftpput in there,
I don’t think I’d have the patience to even bother.
There’s not even base64 or uudecode in this set –
good luck with whatever contraption you might rig up with cat << \EOF.
All this thing has is Telnet (server, client) and FTP (client-only).
There’s 4 Cortex-A7s, 200 MB of RAM, kernel 3.0.x (Alpine seems fine with this, funny enough), and less than 100 MB of writeable internal flash available. Whatever you’re installing in here, it better be small.
The system’s also trying to fight you.
You can remount / read-write but
it exhibits all sorts of weird behavior;
files copied into there and most other filesystems
exist in some weird halfway house of existence
where it feels like the inode exists,
but the actual contents are unreadable.
Most places that could be remounted rw
do this, too.
How do you install anything to this?
Thankfully, there’s a mildly-persistent way to get new programs into PATH.
4 things:
-
/android/data/local/tmpis writeable, is relatively spacious (70 MB free if you haven’t tampered with it), persistent across reboots, and isn’t subject to Weirdness, -
a read-write
/can reliably hold new directories. -
bind mounts exist (
mount -o bindworks just fine), and -
$PATH includes the funny little path
/devel/usr/bin.
We can follow the following procedure to “install” new software,
and you only need to do this every time you pull the battery out.
Assuming you put your (static) binaries in /android/data/local/tmp/bin,
the setup looks like:
# mount -o remount,rw /
# mkdir -p /devel/usr/bin
# mount -o bind /android/data/local/tmp/bin /devel/usr/bin
# mount -o remount,ro /
So, if we wanted to install an alternative file transfer method, we need something intended for absolutely barebones hardware.
Let’s talk about Kermit
Kermit is many things, but for our purposes it is “just” a nicer way to run file transfers.1 It comes from the 80s and runs on damn near every platform you’ve heard of, and tens more you haven’t. It also feels like a program from that era, but what it lacks in modern conveniences, it more than makes up for in robustness and features.
Kermit runs over anything that looks like a serial line
and anything that looks like a remote shell,
acting as something of an ‘overlay’ on top of the session
in the style of Mosh.
This does mean that it needs full control
of the session; you cannot expect Kermit to work over Mosh.
Additionally, both ends must have Kermit installed to benefit
from this arrangement. Technically the Kermit command
TRANSMIT works without a Kermit on the other end, but
that’s just a slightly more convenient Ctrl+C/Ctrl+V.
For your trouble, it enables bidirectional file transfers
over the same single connection.
kermit -g to get files out, kermit -s to send files in.
First, get C-Kermit onto your computer. The package you want is usually
some variation on kermit; I’ve seen kermit and ckermit floating around.
Second, get C-Kermit onto the a6000. Just.
Just.
Hm?
What do you mean you don’t have an Android 2.3.7 NDK sysroot lying around? There are reasons for and against dynamic linking but I’m sure not experienced enough to cobble together a suitable sysroot.
Let’s build a static C-Kermit. I’m going to assume you have a Linux box; a VM will be fine here.
Building a static C-Kermit for the a6000
Versions
You have two options: Kermit 9.x (released 2011),
or the “beta” Kermit 10.x.
While I would go for the latest release, it’s… old.
There’s stuff in here for pre-ANSI C compilers.
Given the age and extreme portability of this codebase,
today’s compilers (GCC 15+, Clang 21+)
don’t much like what’s going on.
It’s only worse in the 9.x release.
Sure, Kermit 10.x needs to be compiled with -fpermissive,
but 9.x needs an exorcist.
qemu-user instructions
Fun fact: some aarch64 machines don’t do 32-bit
Turns out Qualcomm Oryon and Apple’s M-series don’t know how to execute 32-bit ARM.
At all. Not even userspace. chroot does not work. You lose! /bin/sh: exec format error!
If this is you, you’re SOL; you have to use qemu-user like the rest of us.
C-Kermit is kinda a pain to cross-compile, so assuming you don’t have an armv7l machine, we’ll need to run qemu-user to run an armv7l Alpine minirootfs, available at alpinelinux.org.
If you already have an armv7l machine, skip ahead.
Install a static qemu-arm binary for your machine
(Debian has some, if I remember correctly) and do
$ mkdir alpine-arm-chroot
$ sudo tar -C alpine-arm-chroot --numeric-owner --xattrs-include='*.*' -xpvf alpine-minirootfs-*.tar.gz
$ cp $(type -p qemu-arm) alpine-arm-chroot/usr/bin/
$ sudo chroot . qemu-arm /bin/ash
$ apk add build-base
If you’re doing this on an Android device in Termux,
(painful, but I respect the hustle)
you can run proot-distro install -a arm -n alpine-arm alpine.
PRoot is not fast, but C-Kermit isn’t a large program, either.
Compiling
The following will produce a reasonably small2 (650k) static Kermit binary. It also has basically nothing, and I wish it had even less.
LNKFLAGS='-static' KFLAGS='-fpermissive -DNONOSETBUF -DNOLOCAL -DNODIAL -DNOISL -DNOFRILLS -DNONET -DNOHELP -DNOFTP -DNOHTTP -DNOUNICODE -Oz' make linux
Transferring the binary
The only file transfer protocol the a6000 understands is, er, FTP.
I don’t know about you,
but I don’t encounter many people who operate FTP servers.
You’re free to run a server any way you like,
but I quite like using pyftpdlib for this purpose,
along with uv to avoid having to install
anything to the local system.
On the local end, in the directory
that you put your kermit binary in:
$ uv run --with pyftpdlib --module pyftpdlib
You will have to know your local side’s IP at this point.
ip a should tell you.
Then:
# mkdir /android/local/tmp/bin
# ftpget -P 2121 $LOCAL_IP /android/data/local/tmp/bin/kermit path/to/built-wermit
I showed you how to bind /android/local/tmp/bin to /devel/usr/bin earlier,
but here’s a copy-paste friendly version of that.
Remember to replace the placeholder of $CAMERA_IP
with your actual camera’s IP.
CAMERA_IP=192.168.100.250
{ cat << EOF
mount -o remount,rw /
mkdir -p /devel/usr/bin
mount -o bind /android/local/tmp/bin /devel/usr/bin
mount -o remount,ro /
exit
EOF
; sleep 1 } | telnet $CAMERA_IP
Telnet is not SSH
The Telnet clients I’m aware of cannot execute commands like SSH can.
Cheeky tricks like ssh user@host 'tar c /path' | tar x?
They don’t work here.
You’re really supposed to use Expect (or other chat(8) script-esque interpreter) to script Telnet, but it’s also possible to just bang everything out into Telnet’s stdin and sleep for a few moments.
Don’t do this if you need to wait for a remote string before firing off a response, use Expect or chat(8) or Kermit or something here.
Your mileage may vary.
Making it all come together, part 1
You can write these lines out manually each time, but I suspect you’d much prefer to write this into a script. Thankfully, Kermit is also a scripting language.
This is a pretty basic sync script; this is fine if you don’t rename your files.
#!/usr/bin/kermit +
# It does have to be like that, yes.
# I can't make this /usr/bin/env kermit.
#
# Run as either ./a6k-getphotos [IP address] at your shell (chmod +x it first)
# or run Kermit and type __take a6k-getphotos [IP address]__
# at the prompt that appears.
IF DEFINED \%1 { }
ELSE {
EXIT 1 "Usage: a6k-getphotos [IP address of camera]"
}
# The telnetd in here is weird and old,
# we need to skip negotiating some stuff or
# Kermit will just keep waiting until the end of time(out).
SET TELOPT TERMINAL-TYPE REFUSED
SET TELOPT NEW-ENVRIRONMENT REFUSED
SET TELOPT COM-PORT-CONTROL REFUSED
# This line in particular is load-bearing and will bewilder you.
#
# If your local end is an Android phone running Termux
# and the camera connected to the phone's hotspot,
# your phone's Kermit, for a reason I hope to comprehend some day,
# will fail to negotiate WILL KERMIT and DO KERMIT.
# We need to skip negotiating both of these straight away.
SET TELOPT KERMIT REFUSED REFUSED
# I'm willing to take the marginal hit in performance for reliability.
# It's not *often* that files transferred over the WiFi end up corrupted,
# but it has happened to me once or twice.
SET RELIABLE OFF
# It may not look like it, but this logs you in.
#
# Kermit scripts by default just plow straight through, error or not.
# Let's be slightly more reasonable about this.
SET HOST \%1
IF FAILED {
exit 1 "Failed to connect to camera"
}
# Adjust this to taste.
# The SD card root is at /android/mnt/sdcard/
# photos are under ./DCIM/???MSDCF
# You can figure out your own local-side directory structure.
CD /path/to/photos/
REMOTE CD /android/mnt/sdcard/DCIM/100MSDCF
# Get all the photos out.
GET *.JPG
GET *.ARW
BYE
EXIT
If you plan on logging into the camera using Kermit, the following script should make your life a bit easier:
#!/usr/bin/kermit +
# Run as either ./a6k-klogin [IP address] at your shell (chmod +x first)
# or run Kermit and type __take a6k-klogin [IP address]__
# at the prompt that appears.
IF DEFINED \%1 { }
ELSE {
EXIT 1 "Usage: a6k-klogin [IP address of camera]"
}
SET TELOPT TERMINAL-TYPE REFUSED
SET TELOPT NEW-ENVRIRONMENT REFUSED
SET TELOPT COM-PORT-CONTROL REFUSED
SET TELOPT KERMIT REFUSED REFUSED
SET RELIABLE OFF
TELNET \%1
EXIT
Have this setup script, too, if you want to remain at C-Kermit’s prompt:
#!/usr/bin/kermit +
# Run as either ./a6k-ksetup [IP address] at your shell (chmod +x first)
# or run Kermit and type __take a6k-ksetup [IP address]__
# at the prompt that appears.
IF DEFINED \%1 { }
ELSE {
EXIT 1 "Usage: a6k-ksetup [IP address of camera]"
}
SET TELOPT TERMINAL-TYPE REFUSED
SET TELOPT NEW-ENVRIRONMENT REFUSED
SET TELOPT COM-PORT-CONTROL REFUSED
SET TELOPT KERMIT REFUSED REFUSED
SET RELIABLE OFF
SET HOST \%1
IF FAILURE {
EXIT "Failed to connect to camera"
}
Making it all come together, part 2
But if you’re like me, you rename your photos to be more descriptive, which means the only reliable way to determine “is this a duplicate?” is by the timestamp embedded in the file metadata.
This is… substantially more complex, and a lot more janky. I should note this method only has second-level precision, which might cause problems for you if you take a lot of burst shots.
First off, we need to get a more functional BusyBox in. We can steal one from Alpine, that’s to say, from within that chroot earlier:
# apk add busybox-static
and copy out the chroot’s ./usr/bin/busybox.static, however you do it.
You can do the ftpget thing earlier, but you also have Kermit now.
The underlined bits are what you actually type.
$ kermit C-Kermit 10.0.416 Beta.12, 2025/03/22, for Linux (64-bit) Copyright (C) 1985, 2025, Trustees of Columbia University in the City of New York. Open Source 3-clause BSD license since 2011. Type ? or HELP for help. (~) C-Kermit> take a6k-ksetup [IP address of camera] (~) C-Kermit> send busybox.static /android/data/local/tmp/bin/busybox.static (~) C-Kermit> lineout chmod +x /android/data/local/tmp/bin/busybox.static (~) C-Kermit> exit
So we need to compare file dates between the local and camera sides. First, the camera itself needs to be able to parse the file dates and list them alongside the files.
#!/bin/sh
# See that awk there?
# That needs to be adjusted based on the camera's timezone.
# I'm in +08, so I must subtract 8 hours for the timestamps to line up.
# I hope you don't need to change your camera's timezone, ever,
# because I don't know what you'd do then.
busybox.static head -c1k "$1" | \
busybox.static strings | \
busybox.static grep -E '^[0-9]{4}:' | \
busybox.static sed -e 's/:/-/2' -e 's/:/-/1' | \
busybox.static xargs -I{} busybox.static date +%s {} | \
busybox.static awk '{print $1 - (8 * 60 * 60)}' | \
busybox.static xargs -I{} busybox.static printf '%s:%s\n' {} "$1"
Send this over by doing
$ kermit C-Kermit 10.0.416 Beta.12, 2025/03/22, for Linux (64-bit) Copyright (C) 1985, 2025, Trustees of Columbia University in the City of New York. Open Source 3-clause BSD license since 2011. Type ? or HELP for help. (~) C-Kermit> take a6k-ksetup [IP address of camera] (~) C-Kermit> send parsedates /android/data/local/tmp/bin/parsedates (~) C-Kermit> lineout chmod +x /android/data/local/tmp/bin/parsedates (~) C-Kermit> exit
That’s all we need to send to the camera.
Now we’ll need to write 2 scripts to our local end.
Here’s the first, a Bash script that reads off the metadata dates using exiftool.
Install this to somewhere in your PATH.
#!/usr/bin/env bash
declare -a searchpath
# Populate this searchpath with every directory
# that you know of that contains your .ARWs.
searchpath=( "$HOME"/Syncthing/raws/ "$HOME"/Downloads/raws "$HOME"/storage/downloads/Syncthing/raws "$STROOT"/raws . )
# This is just a filename.
localstamps=LSTAMPS.TXT
rm "$localstamps"
for path in "${searchpath[@]}"
do
find "$path" -type f -name '*.ARW' -exec exiftool -sonydatetime -dateFormat '%s' -qq {} + \
| tr -d ' ' | cut -f 2 -d : | sed '/========/d' | sed '/filesread/d' \
>> "$localstamps"
# Couldn't make it work with videos, unfortunately.
# find "$path" -type f \( -name '*.MP4' -o -name '*.MTS' \) -exec exiftool -mediacreatedate -dateFormat '%s' -qq {} + \
# | tr -d ' ' | cut -f 2 -d : | sed '/========/d' | sed '/filesread/d' \
# >> "$localstamps"
done
# RSTAMPS.TXT will be introduced later,
# but it's basically a listing of file timestamps glued to the file paths.
# As you might expect, GETLIST.TXT is a list of files to get.
grep --file=LSTAMPS.TXT -v RSTAMPS.TXT | cut -f 2 -d : > GETLIST.TXT
And this one, which you can choose to install or just TAKE from a Kermit prompt.
#!/usr/bin/kermit +
IF DEFINED \%1 { }
ELSE {
EXIT 1 "Usage: a6k-getnewphotos [IP address of camera]"
}
# The camera's Telnet is old and weird, as previously discussed.
set telopt terminal-type refused
set telopt new-environment refused
set telopt com-port-control refused
set telopt kermit refused refused
set reliable off
set file collision overwrite
set host \%1
if fail {
exit 1 "Connection failed"
}
echo "Generating RSTAMPS.TXT"
lineout "2>/dev/null rm /android/mnt/sdcard/RSTAMPS.TXT"
# This is a *very* slow process.
# Takes about a minute to complete, so...
lineout "busybox.static find /android/mnt/sdcard -type f -name '*.ARW' -exec parsedate {} ';' | busybox.static tr -s '\\n' > /android/mnt/sdcard/RSTAMPS.TXT"
# ...I can't get INPUT to work right for me using the shell prompt,
# so instead I'm doing a little trickery:
# buffering in that shell pipeline so it produces the known output tmp_updater
# once that previous shell pipeline completes.
lineout "ls | busybox.static head -n20 | busybox.static tail -n1"
pause 5
# This line then picks up on it.
input 120 "tmp_updater"
if fail {
bye
exit 2 "Failed to get RSTAMPS.TXT"
}
# GET will time out if I don't do that, so that trickery is necessary.
get /android/mnt/sdcard/RSTAMPS.TXT
echo "Generating LSTAMPS.TXT"
run a6k-localstamps
# current file
declare \%f
# file channel number
declare \%c
# GETLIST.TXT is a list of files to get, generated by a6k-localstamps.
file open /read \%c GETLIST.TXT
while true {
file read /line \%c \%f
if fail { break }
get \%f
}
bye
This will GET all the files that don’t have a timestamp
that the local side has.
Footnotes
-
complete.org’s Kermit page is probably the most accessible explanation for why you’d want to use Kermit, even outside of the context of embedded/constrained systems. ↩
-
UPX’d binaries crash the Linux half of this camera. Straight away. No negotiation. Dead. ↩