text:
#!/usr/bin/python3
# -*- coding: utf-8 -*-

# Copyright (c) 2018 Niklas Ekström

import select
import sys
import socket
import time
import os
import struct
import glob
import logging
import json

logging.basicConfig(format = '%(levelname)s, %(asctime)s, %(name)s, line %(lineno)d: %(message)s')
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

CONFIG_FILE_PATH = '/etc/opt/a314/a314fs.conf'
METAFILE_EXTENSION = ':a314'

with open(CONFIG_FILE_PATH, encoding='utf-8') as f:
    cfg = json.load(f)
    devs = cfg['devices']
    dev = devs['PI0']
    SHARED_DIRECTORY = dev['path']

MSG_REGISTER_REQ        = 1
MSG_REGISTER_RES        = 2
MSG_DEREGISTER_REQ      = 3
MSG_DEREGISTER_RES      = 4
MSG_READ_MEM_REQ        = 5
MSG_READ_MEM_RES        = 6
MSG_WRITE_MEM_REQ       = 7
MSG_WRITE_MEM_RES       = 8
MSG_CONNECT             = 9
MSG_CONNECT_RESPONSE    = 10
MSG_DATA                = 11
MSG_EOS                 = 12
MSG_RESET               = 13

def wait_for_msg():
    header = b''
    while len(header) < 9:
        data = drv.recv(9 - len(header))
        if not data:
            logger.error('Connection to a314d was closed, terminating.')
            exit(-1)
        header += data
    (plen, stream_id, ptype) = struct.unpack('=IIB', header)
    payload = b''
    while len(payload) < plen:
        data = drv.recv(plen - len(payload))
        if not data:
            logger.error('Connection to a314d was closed, terminating.')
            exit(-1)
        payload += data
    return (stream_id, ptype, payload)

def send_register_req(name):
    m = struct.pack('=IIB', len(name), 0, MSG_REGISTER_REQ) + name
    drv.sendall(m)

def send_read_mem_req(address, length):
    m = struct.pack('=IIBII', 8, 0, MSG_READ_MEM_REQ, address, length)
    drv.sendall(m)

def read_mem(address, length):
    send_read_mem_req(address, length)
    stream_id, ptype, payload = wait_for_msg()
    if ptype != MSG_READ_MEM_RES:
        logger.error('Expected MSG_READ_MEM_RES but got %s. Shutting down.', ptype)
        exit(-1)
    return payload

def send_write_mem_req(address, data):
    m = struct.pack('=IIBI', 4 + len(data), 0, MSG_WRITE_MEM_REQ, address) + data
    drv.sendall(m)

def write_mem(address, data):
    send_write_mem_req(address, data)
    stream_id, ptype, payload = wait_for_msg()
    if ptype != MSG_WRITE_MEM_RES:
        logger.error('Expected MSG_WRITE_MEM_RES but got %s. Shutting down.', ptype)
        exit(-1)

def send_connect_response(stream_id, result):
    m = struct.pack('=IIBB', 1, stream_id, MSG_CONNECT_RESPONSE, result)
    drv.sendall(m)

def send_data(stream_id, data):
    m = struct.pack('=IIB', len(data), stream_id, MSG_DATA) + data
    drv.sendall(m)

def send_eos(stream_id):
    m = struct.pack('=IIB', 0, stream_id, MSG_EOS)
    drv.sendall(m)

def send_reset(stream_id):
    m = struct.pack('=IIB', 0, stream_id, MSG_RESET)
    drv.sendall(m)

ACTION_NIL              = 0
ACTION_GET_BLOCK        = 2
ACTION_SET_MAP          = 4
ACTION_DIE              = 5
ACTION_EVENT            = 6
ACTION_CURRENT_VOLUME   = 7
ACTION_LOCATE_OBJECT    = 8
ACTION_RENAME_DISK      = 9
ACTION_WRITE            = ord('W')
ACTION_READ             = ord('R')
ACTION_FREE_LOCK        = 15
ACTION_DELETE_OBJECT    = 16
ACTION_RENAME_OBJECT    = 17
ACTION_MORE_CACHE       = 18
ACTION_COPY_DIR         = 19
ACTION_WAIT_CHAR        = 20
ACTION_SET_PROTECT      = 21
ACTION_CREATE_DIR       = 22
ACTION_EXAMINE_OBJECT   = 23
ACTION_EXAMINE_NEXT     = 24
ACTION_DISK_INFO        = 25
ACTION_INFO             = 26
ACTION_FLUSH            = 27
ACTION_SET_COMMENT      = 28
ACTION_PARENT           = 29
ACTION_TIMER            = 30
ACTION_INHIBIT          = 31
ACTION_DISK_TYPE        = 32
ACTION_DISK_CHANGE      = 33
ACTION_SET_DATE         = 34
ACTION_SAME_LOCK        = 40
ACTION_SCREEN_MODE      = 994
ACTION_READ_RETURN      = 1001
ACTION_WRITE_RETURN     = 1002
ACTION_FINDUPDATE       = 1004
ACTION_FINDINPUT        = 1005
ACTION_FINDOUTPUT       = 1006
ACTION_END              = 1007
ACTION_SEEK             = 1008
ACTION_TRUNCATE         = 1022
ACTION_WRITE_PROTECT    = 1023
ACTION_EXAMINE_FH       = 1034
ACTION_UNSUPPORTED      = 65535

ERROR_NO_FREE_STORE             = 103
ERROR_TASK_TABLE_FULL           = 105
ERROR_LINE_TOO_LONG             = 120
ERROR_FILE_NOT_OBJECT           = 121
ERROR_INVALID_RESIDENT_LIBRARY  = 122
ERROR_NO_DEFAULT_DIR            = 201
ERROR_OBJECT_IN_USE             = 202
ERROR_OBJECT_EXISTS             = 203
ERROR_DIR_NOT_FOUND             = 204
ERROR_OBJECT_NOT_FOUND          = 205
ERROR_BAD_STREAM_NAME           = 206
ERROR_OBJECT_TOO_LARGE          = 207
ERROR_ACTION_NOT_KNOWN          = 209
ERROR_INVALID_COMPONENT_NAME    = 210
ERROR_INVALID_LOCK              = 211
ERROR_OBJECT_WRONG_TYPE         = 212
ERROR_DISK_NOT_VALIDATED        = 213
ERROR_DISK_WRITE_PROTECTED      = 214
ERROR_RENAME_ACROSS_DEVICES     = 215
ERROR_DIRECTORY_NOT_EMPTY       = 216
ERROR_TOO_MANY_LEVELS           = 217
ERROR_DEVICE_NOT_MOUNTED        = 218
ERROR_SEEK_ERROR                = 219
ERROR_COMMENT_TOO_BIG           = 220
ERROR_DISK_FULL                 = 221
ERROR_DELETE_PROTECTED          = 222
ERROR_WRITE_PROTECTED           = 223
ERROR_READ_PROTECTED            = 224
ERROR_NOT_A_DOS_DISK            = 225
ERROR_NO_DISK                   = 226
ERROR_NO_MORE_ENTRIES           = 232

SHARED_LOCK         = -2
EXCLUSIVE_LOCK      = -1

LOCK_DIFFERENT      = -1
LOCK_SAME           = 0
LOCK_SAME_VOLUME    = 1

MODE_OLDFILE        = 1005  # Open existing file read/write positioned at beginning of file.
MODE_NEWFILE        = 1006  # Open freshly created file (delete old file) read/write, exclusive lock.
MODE_READWRITE      = 1004  # Open old file w/shared lock, creates file if doesn't exist.

OFFSET_BEGINNING    = -1    # Relative to Begining Of File.
OFFSET_CURRENT      = 0     # Relative to Current file position.
OFFSET_END          = 1     # relative to End Of File.

ST_ROOT             = 1
ST_USERDIR          = 2
ST_SOFTLINK         = 3     # looks like dir, but may point to a file!
ST_LINKDIR          = 4     # hard link to dir
ST_FILE             = -3    # must be negative for FIB!
ST_LINKFILE         = -4    # hard link to file
ST_PIPEFILE         = -5

current_stream_id = 0

class ObjectLock(object):
    def __init__(self, key, mode, path):
        self.key = key
        self.mode = mode
        self.path = path
        self.entry_it = None

locks = {}

next_key = 1

def get_key():
    global next_key
    key = next_key
    next_key = 1 if next_key == 0x7fffffff else (next_key + 1)
    while key in locks:
        key += 1
    return key

def find_path(key, name):
    i = name.find(':')
    if i != -1:
        vol = name[:i].lower()
        if vol == '' or vol == 'pi0' or vol == 'pidisk':
            key = 0
        name = name[i + 1:]

    if key == 0:
        cp = ()
    else:
        cp = locks[key].path

    while name:
        i = name.find('/')
        if i == -1:
            comp = name
            name = ''
        else:
            comp = name[:i]
            name = name[i + 1:]

        if len(comp) == 0:
            if len(cp) == 0:
                return None
            cp = cp[:-1]
        else:
            p = '.' if len(cp) == 0 else '/'.join(cp)
            entries = os.listdir(p)
            found = False
            for e in entries:
                if comp.lower() == e.lower():
                    cp = cp + (e,)
                    found = True
                    break
            if not found:
                if len(name) == 0:
                    cp = cp + (comp,)
                else:
                    return None

    return cp

def read_metadata(path):
    protection = 0
    comment = ''

    if not os.path.isfile(path + METAFILE_EXTENSION):
        return (protection, comment)

    try:
        f = open(path + METAFILE_EXTENSION, 'r')
        for line in f:
            if line[0] == 'p':
                try:
                    protection = int(line[1:].strip())
                except ValueError:
                    pass
            elif line[0] == 'c':
                comment = line[1:].strip()[:79]
        f.close()
    except FileNotFoundError:
        pass
    return (protection, comment)

def write_metadata(path, protection=None, comment=None):
    p, c = read_metadata(path)

    if protection == None:
        protection = p
    if comment == None:
        comment = c

    if (p, c) == (protection, comment):
        return True

    try:
        f = open(path + METAFILE_EXTENSION, 'w')
        f.write('p' + str(protection) + '\n')
        f.write('c' + comment + '\n')
        f.close()
    except FileNotFoundError as e:
        logger.warning('Failed to write metadata for file %s: %s', path, e)
        return False
    return True

def process_locate_object(key, mode, name):
    logger.debug('ACTION_LOCATE_OBJECT, key: %s, mode: %s, name: %s', key, mode, name)

    cp = find_path(key, name)

    if cp is None or not (len(cp) == 0 or os.path.exists('/'.join(cp))):
        return struct.pack('>HH', 0, ERROR_OBJECT_NOT_FOUND)

    # TODO: Must check if there is already a lock for this path,
    # and if so, if the locks are compatible.

    key = get_key()
    locks[key] = ObjectLock(key, mode, cp)
    return struct.pack('>HHI', 1, 0, key)

def process_free_lock(key):
    logger.debug('ACTION_FREE_LOCK, key: %s', key)
    if key in locks:
        del locks[key]
    return struct.pack('>HH', 1, 0)

def process_copy_dir(prev_key):
    logger.debug('ACTION_COPY_DIR, prev_key: %s', prev_key)
    ol = locks[prev_key]
    key = get_key()
    locks[key] = ObjectLock(key, ol.mode, ol.path)
    return struct.pack('>HHI', 1, 0, key)

def process_parent(prev_key):
    logger.debug('ACTION_PARENT, prev_key: %s', prev_key)
    ol = locks[prev_key]
    if len(ol.path) == 0:
        key = 0
    else:
        key = get_key()
        locks[key] = ObjectLock(key, SHARED_LOCK, ol.path[:-1])
    return struct.pack('>HHI', 1, 0, key)

def mtime_to_dmt(mtime):
    mtime = int(mtime)
    days = mtime // 86400
    left = mtime - days * 86400
    mins = left // 60
    secs = left - mins * 60
    ticks = secs * 50
    days -= 2922 # Days between 1970-01-01 and 1978-01-01
    days = max(0, days) # If days are before Amiga epoc
    return (days, mins, ticks)

def process_examine_object(key):
    logger.debug('ACTION_EXAMINE_OBJECT, key: %s', key)
    ol = locks[key]

    if len(ol.path) == 0:
        fn = 'PiDisk'
        path = '.'
    else:
        fn = ol.path[-1]
        path = '/'.join(ol.path)

    days, mins, ticks = mtime_to_dmt(os.path.getmtime(path))
    protection, comment = read_metadata(path)

    if os.path.isfile(path):
        size = os.path.getsize(path)
        type_ = ST_FILE
    else:
        size = 0
        type_ = ST_USERDIR
        ol.entry_it = os.scandir(path)

    size = min(size, 2 ** 31 - 1)
    fn = (chr(len(fn)) + fn).encode('latin-1', 'ignore')
    comment = (chr(len(comment)) + comment).encode('latin-1', 'ignore')
    return struct.pack('>HHHhIIIII', 1, 0, 666, type_, size, protection, days, mins, ticks) + fn + comment

def process_examine_next(key, disk_key):
    logger.debug('ACTION_EXAMINE_NEXT, key: %s, disk_key: %s', key, disk_key)
    ol = locks[key]

    if len(ol.path) == 0:
        path = '.'
    else:
        path = '/'.join(ol.path)

    if not os.path.isdir(path):
        return struct.pack('>HH', 0, ERROR_OBJECT_WRONG_TYPE)

    disk_key += 1

    entry = next(ol.entry_it, None)
    while entry and entry.name.endswith(METAFILE_EXTENSION):
        entry = next(ol.entry_it, None)

    if not entry:
        return struct.pack('>HH', 0, ERROR_NO_MORE_ENTRIES)

    fn = entry.name
    path = ('/'.join(ol.path + (fn,)))

    days, mins, ticks = mtime_to_dmt(entry.stat().st_mtime)
    protection, comment = read_metadata(path)

    if os.path.isfile(path):
        size = os.path.getsize(path)
        type_ = ST_FILE
    else:
        size = 0
        type_ = ST_USERDIR

    size = min(size, 2 ** 31 - 1)
    fn = (chr(len(fn)) + fn).encode('latin-1', 'ignore')
    comment = (chr(len(comment)) + comment).encode('latin-1', 'ignore')
    return struct.pack('>HHHhIIIII', 1, 0, disk_key, type_, size, protection, days, mins, ticks) + fn + comment

def process_examine_fh(arg1):
    logger.debug('ACTION_EXAMINE_FH, arg1: %s', arg1)

    fn = open_file_handles[arg1].f.name
    path = os.path.realpath(fn)
    days, mins, ticks = mtime_to_dmt(os.path.getmtime(path))
    protection, comment = read_metadata(path)

    if os.path.isfile(path):
        size = os.path.getsize(path)
        type_ = ST_FILE
    else:
        size = 0
        type_ = ST_USERDIR

    size = min(size, 2 ** 31 - 1)
    fn = (chr(len(fn)) + fn).encode('latin-1', 'ignore')
    comment = (chr(len(comment)) + comment).encode('latin-1', 'ignore')
    return struct.pack('>HHHhIIIII', 1, 0, 666, type_, size, protection, days, mins, ticks) + fn + comment

next_fp = 1

open_file_handles = {}

def get_file_ptr():
    global next_fp
    fp = next_fp
    next_fp = 1 if next_fp == 0x7fffffff else next_fp + 1
    while fp in open_file_handles:
        fp += 1
    return fp

class OpenFileHandle(object):
    def __init__(self, fp, f, p):
        self.fp = fp
        self.f = f
        self.p = p

def process_findxxx(mode, key, name):
    if mode == ACTION_FINDINPUT:
        logger.debug('ACTION_FINDINPUT, key: %s, name: %s', key, name)
    elif mode == ACTION_FINDOUTPUT:
        logger.debug('ACTION_FINDOUTPUT, key: %s, name: %s', key, name)
    elif mode == ACTION_FINDUPDATE:
        logger.debug('ACTION_FINDUPDATE, key: %s, name: %s', key, name)

    cp = find_path(key, name)
    if cp is None:
        return struct.pack('>HH', 0, ERROR_OBJECT_NOT_FOUND)

    path = '/'.join(cp)
    if len(cp) == 0 or os.path.isdir(path):
        return struct.pack('>HH', 0, ERROR_OBJECT_WRONG_TYPE)

    # TODO: Must check if there already exists a non-compatible lock for this path.

    # TODO: This must be handled better. Especially error reporting.

    protection, _ = read_metadata(path)
    try:
        if mode == MODE_OLDFILE:
            f = open(path, 'r+b')
        elif mode == MODE_READWRITE:
            f = open(path, 'r+b')
            if protection & 16:
                protection = protection & 0b11101111
                write_metadata(path, protection=protection)
        elif mode == MODE_NEWFILE:
            if protection & 0x1:
                return struct.pack('>HH', 0, ERROR_DELETE_PROTECTED)
            elif protection & 0x4:
                return struct.pack('>HH', 0, ERROR_WRITE_PROTECTED)
            f = open(path, 'w+b')
            if protection & 16:
                protection = protection & 0b11101111
                write_metadata(path, protection=protection)
    except IOError:
        if mode == MODE_READWRITE:
            try:
                f = open(path, 'w+b')
                if protection & 16:
                    protection = protection & 0b11101111
                    write_metadata(path, protection=protection)
            except IOError:
                return struct.pack('>HH', 0, ERROR_OBJECT_NOT_FOUND)
        else:
            return struct.pack('>HH', 0, ERROR_OBJECT_NOT_FOUND)

    fp = get_file_ptr()
    ofh = OpenFileHandle(fp, f, protection)
    open_file_handles[fp] = ofh

    return struct.pack('>HHI', 1, 0, fp)

def process_read(arg1, address, length):
    logger.debug('ACTION_READ, arg1: %s, address: %s, length: %s', arg1, address, length)
    protection = open_file_handles[arg1].p
    if protection & 0x8:
        return struct.pack('>HH', 0, ERROR_READ_PROTECTED)
    f = open_file_handles[arg1].f
    data = f.read(length)
    if len(data) != 0:
        write_mem(address, data)
    return struct.pack('>HHI', 1, 0, len(data))

def process_write(arg1, address, length):
    logger.debug('ACTION_WRITE, arg1: %s, address: %s, length: %s', arg1, address, length)
    protection = open_file_handles[arg1].p
    if protection & 0x4:
        return struct.pack('>HH', 0, ERROR_WRITE_PROTECTED)
    data = read_mem(address, length)
    f = open_file_handles[arg1].f
    f.seek(0, 1)
    try:
        f.write(data)
    except IOError:
        return struct.pack('>HH', 0, ERROR_DISK_FULL)
    return struct.pack('>HHI', 1, 0, length)

def process_seek(arg1, new_pos, mode):
    logger.debug('ACTION_SEEK, arg1: %s, new_pos: %s, mode: %s', arg1, new_pos, mode)

    f = open_file_handles[arg1].f
    old_pos = f.tell()

    from_what = 0
    if mode == OFFSET_CURRENT:
        from_what = 1
    elif mode == OFFSET_END:
        from_what = 2

    f.seek(new_pos, from_what)

    return struct.pack('>HHi', 1, 0, old_pos)

def process_end(arg1):
    logger.debug('ACTION_END, arg1: %s', arg1)

    if arg1 in open_file_handles:
        f = open_file_handles.pop(arg1).f
        f.close()

    return struct.pack('>HH', 1, 0)

def process_delete_object(key, name):
    logger.debug('ACTION_DELETE_OBJECT, key: %s, name: %s', key, name)

    cp = find_path(key, name)
    if cp is None or len(cp) == 0:
        return struct.pack('>HH', 0, ERROR_OBJECT_NOT_FOUND)

    path = '/'.join(cp)
    is_dir = os.path.isdir(path)

    protection, _ = read_metadata(path)
    if protection & 0x1:
        return struct.pack('>HH', 0, ERROR_DELETE_PROTECTED)

    try:
        if is_dir:
            os.rmdir(path)
        else:
            os.remove(path)
        if os.path.isfile(path + METAFILE_EXTENSION):
            os.remove(path + METAFILE_EXTENSION)
    except:
        if is_dir:
            return struct.pack('>HH', 0, ERROR_DIRECTORY_NOT_EMPTY)
        else:
            return struct.pack('>HH', 0, ERROR_OBJECT_NOT_FOUND)

    return struct.pack('>HH', 1, 0)

def process_rename_object(key, name, target_dir, new_name):
    logger.debug('ACTION_RENAME_OBJECT, key: %s, name: %s, target_dir: %s, new_name: %s', key, name, target_dir, new_name)

    cp1 = find_path(key, name)
    if cp1 is None or len(cp1) == 0:
        return struct.pack('>HH', 0, ERROR_OBJECT_NOT_FOUND)

    from_path = '/'.join(cp1)
    if not os.path.exists(from_path):
        return struct.pack('>HH', 0, ERROR_OBJECT_NOT_FOUND)

    cp2 = find_path(target_dir, new_name)
    if cp2 is None or len(cp2) == 0:
        return struct.pack('>HH', 0, ERROR_OBJECT_NOT_FOUND)

    to_path = '/'.join(cp2)

    if from_path == to_path:
        return struct.pack('>HH', 1, 0)

    if os.path.exists(to_path):
        return struct.pack('>HH', 0, ERROR_OBJECT_EXISTS)

    try:
        os.rename(from_path, to_path)
        if os.path.isfile(from_path + METAFILE_EXTENSION):
            os.rename(from_path + METAFILE_EXTENSION, to_path + METAFILE_EXTENSION)
    except OSError:
        return struct.pack('>HH', 0, ERROR_OBJECT_IN_USE)
    except Exception as e:
        logger.warning('ACTION_RENAME unexpected exception')
        logger.warning(e)
        return struct.pack('>HH', 0, ERROR_OBJECT_NOT_FOUND)

    return struct.pack('>HH', 1, 0)

def process_create_dir(key, name):
    logger.debug('ACTION_CREATE_DIR, key: %s, name: %s', key, name)

    cp = find_path(key, name)
    if cp is None or len(cp) == 0:
        return struct.pack('>HH', 0, ERROR_OBJECT_NOT_FOUND)

    try:
        path = '/'.join(cp)
        os.makedirs(path)
    except:
        return struct.pack('>HH', 0, ERROR_OBJECT_NOT_FOUND)

    key = get_key()
    locks[key] = ObjectLock(key, SHARED_LOCK, cp)
    return struct.pack('>HHI', 1, 0, key)

def process_set_protect(key, name, mask):
    logger.debug('ACTION_SET_PROTECT, key: %s, name: %s, mask: %s', key, name, mask)

    cp = find_path(key, name)
    if cp is None or len(cp) == 0:
        return struct.pack('>HH', 0, ERROR_OBJECT_NOT_FOUND)

    path = '/'.join(cp)
    if write_metadata(path, protection=mask):
        return struct.pack('>HH', 1, 0)
    else:
        return struct.pack('>HH', 0, ERROR_OBJECT_NOT_FOUND)

def process_set_comment(key, name, comment):
    logger.debug('ACTION_SET_COMMENT, key: %s, name: %s, comment: %s', key, name, comment)

    if len(comment) > 79:
        return struct.pack('>HH', 0, ERROR_COMMENT_TOO_BIG)

    cp = find_path(key, name)
    if cp is None or len(cp) == 0:
        return struct.pack('>HH', 0, ERROR_OBJECT_NOT_FOUND)

    path = '/'.join(cp)
    if write_metadata(path, comment=comment):
        return struct.pack('>HH', 1, 0)
    else:
        return struct.pack('>HH', 0, ERROR_OBJECT_NOT_FOUND)

def process_same_lock(key1, key2):
    logger.debug('ACTION_SAME_LOCK, key1: %s key2: %s', key1, key2)

    if not (key1 in locks and key2 in locks):
        return struct.pack('>HH', 0, LOCK_DIFFERENT)
    elif locks[key1].path == locks[key2].path:
        return struct.pack('>HH', 1, LOCK_SAME)
    else:
        return struct.pack('>HH', 0, LOCK_SAME_VOLUME)

def process_request(req):
    #logger.debug('len(req): %s, req: %s', len(req), list(req))

    (rtype,) = struct.unpack('>H', req[:2])

    if rtype == ACTION_LOCATE_OBJECT:
        key, mode, nlen = struct.unpack('>IHB', req[2:9])
        name = req[9:9+nlen].decode('latin-1')
        return process_locate_object(key, mode, name)
    elif rtype == ACTION_FREE_LOCK:
        (key,) = struct.unpack('>I', req[2:6])
        return process_free_lock(key)
    elif rtype == ACTION_COPY_DIR:
        (key,) = struct.unpack('>I', req[2:6])
        return process_copy_dir(key)
    elif rtype == ACTION_PARENT:
        (key,) = struct.unpack('>I', req[2:6])
        return process_parent(key)
    elif rtype == ACTION_EXAMINE_OBJECT:
        (key,) = struct.unpack('>I', req[2:6])
        return process_examine_object(key)
    elif rtype == ACTION_EXAMINE_NEXT:
        key, disk_key = struct.unpack('>IH', req[2:8])
        return process_examine_next(key, disk_key)
    elif rtype == ACTION_EXAMINE_FH:
        (arg1,) = struct.unpack('>I', req[2:6])
        return process_examine_fh(arg1)
    elif rtype == ACTION_FINDINPUT or rtype == ACTION_FINDOUTPUT or rtype == ACTION_FINDUPDATE:
        key, nlen = struct.unpack('>IB', req[2:7])
        name = req[7:7+nlen].decode('latin-1')
        return process_findxxx(rtype, key, name)
    elif rtype == ACTION_READ:
        arg1, address, length = struct.unpack('>III', req[2:14])
        return process_read(arg1, address, length)
    elif rtype == ACTION_WRITE:
        arg1, address, length = struct.unpack('>III', req[2:14])
        return process_write(arg1, address, length)
    elif rtype == ACTION_SEEK:
        arg1, new_pos, mode = struct.unpack('>Iii', req[2:14])
        return process_seek(arg1, new_pos, mode)
    elif rtype == ACTION_END:
        (arg1,) = struct.unpack('>I', req[2:6])
        return process_end(arg1)
    elif rtype == ACTION_DELETE_OBJECT:
        key, nlen = struct.unpack('>IB', req[2:7])
        name = req[7:7+nlen].decode('latin-1')
        return process_delete_object(key, name)
    elif rtype == ACTION_RENAME_OBJECT:
        key, target_dir, nlen, nnlen = struct.unpack('>IIBB', req[2:12])
        name = req[12:12+nlen].decode('latin-1')
        new_name = req[12+nlen:12+nlen+nnlen].decode('latin-1')
        return process_rename_object(key, name, target_dir, new_name)
    elif rtype == ACTION_CREATE_DIR:
        key, nlen = struct.unpack('>IB', req[2:7])
        name = req[7:7+nlen].decode('latin-1')
        return process_create_dir(key, name)
    elif rtype == ACTION_SET_PROTECT:
        key, mask, nlen = struct.unpack('>IIB', req[2:11])
        name = req[11:11+nlen].decode('latin-1')
        return process_set_protect(key, name, mask)
    elif rtype == ACTION_SET_COMMENT:
        key, nlen, clen = struct.unpack('>IBB', req[2:8])
        name = req[8:8+nlen].decode('latin-1')
        comment = req[8+nlen:8+nlen+clen].decode('latin-1')
        return process_set_comment(key, name, comment)
    elif rtype == ACTION_SAME_LOCK:
        key1, key2 = struct.unpack('>II', req[2:10])
        return process_same_lock(key1, key2)
    elif rtype == ACTION_UNSUPPORTED:
        (dp_Type,) = struct.unpack('>H', req[2:4])
        logger.warning('Unsupported action %d (Amiga/a314fs)', dp_Type)
        return struct.pack('>HH', 0, ERROR_ACTION_NOT_KNOWN)
    else:
        logger.warning('Unsupported action %d (a314d/a314fs)', rtype)
        return struct.pack('>HH', 0, ERROR_ACTION_NOT_KNOWN)

done = False

try:
    idx = sys.argv.index('-ondemand')
except ValueError:
    idx = -1

if idx != -1:
    fd = int(sys.argv[idx + 1])
    drv = socket.socket(fileno=fd)
else:
    drv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    drv.connect(('localhost', 7110))

    send_register_req(b'a314fs')
    stream_id, ptype, payload = wait_for_msg()
    if payload[0] != 1:
        logger.error('Unable to register service a314fs, shutting down')
        drv.close()
        done = True

if not done:
    os.chdir(SHARED_DIRECTORY)
    logger.info('a314fs is running, shared directory: %s', SHARED_DIRECTORY)

while not done:
    stream_id, ptype, payload = wait_for_msg()

    if ptype == MSG_CONNECT:
        if payload == b'a314fs':
            if current_stream_id is not None:
                send_reset(current_stream_id)
            current_stream_id = stream_id
            send_connect_response(stream_id, 0)
        else:
            send_connect_response(stream_id, 3)
    elif ptype == MSG_DATA:
        address, length = struct.unpack('>II', payload)
        req = read_mem(address + 2, length - 2)
        res = process_request(req)
        write_mem(address + 2, res)
        send_data(stream_id, struct.pack('>I', len(res) + 2))
    elif ptype == MSG_EOS:
        if stream_id == current_stream_id:
            logger.debug('Got EOS, stream closed')
            send_eos(stream_id)
            current_stream_id = None
    elif ptype == MSG_RESET:
        if stream_id == current_stream_id:
            logger.debug('Got RESET, stream closed')
            current_stream_id = None


text:
		cnop	0,4
		dc.l	0
		dc.l	1
		dc.l	4
		dc.l	1

repo_language: C

mime_type: text/xml


text:
		dc.l	0

		movem.l	a0-a6,-(a7)
		lsl.l	#2,d1
		move.l	d1,a0
		bsr	_start
		movem.l	(a7)+,a0-a6
		jmp	(a6)


text:
PI0:    FileSystem      = l:a314fs
        Device          = a314.device
        Unit            = 0
        Surfaces        = 1
        BlocksPerTrack  = 10
        Reserved        = 2
        LowCyl          = 0
        HighCyl         = 100
        StackSize       = 1000
        Mount           = 1
        DosType         = 0x33313400
#


text:
#!/usr/bin/python3
# -*- coding: utf-8 -*-

# Copyright (c) 2019 Niklas Ekström

import fcntl
import logging
import os
import select
import socket
import struct
import sys

fcntl.F_SETPIPE_SZ = 1031

logging.basicConfig(format = '%(levelname)s, %(asctime)s, %(name)s, line %(lineno)d: %(message)s')
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

MSG_REGISTER_REQ        = 1
MSG_REGISTER_RES        = 2
MSG_DEREGISTER_REQ      = 3
MSG_DEREGISTER_RES      = 4
MSG_READ_MEM_REQ        = 5
MSG_READ_MEM_RES        = 6
MSG_WRITE_MEM_REQ       = 7
MSG_WRITE_MEM_RES       = 8
MSG_CONNECT             = 9
MSG_CONNECT_RESPONSE    = 10
MSG_DATA                = 11
MSG_EOS                 = 12
MSG_RESET               = 13

def wait_for_msg():
    header = b''
    while len(header) < 9:
        data = drv.recv(9 - len(header))
        if not data:
            logger.error('Connection to a314d was closed, terminating.')
            exit(-1)
        header += data
    (plen, stream_id, ptype) = struct.unpack('=IIB', header)
    payload = b''
    while len(payload) < plen:
        data = drv.recv(plen - len(payload))
        if not data:
            logger.error('Connection to a314d was closed, terminating.')
            exit(-1)
        payload += data
    return (stream_id, ptype, payload)

def send_register_req(name):
    m = struct.pack('=IIB', len(name), 0, MSG_REGISTER_REQ) + name
    drv.sendall(m)

def send_read_mem_req(address, length):
    m = struct.pack('=IIBII', 8, 0, MSG_READ_MEM_REQ, address, length)
    drv.sendall(m)

def read_mem(address, length):
    send_read_mem_req(address, length)
    stream_id, ptype, payload = wait_for_msg()
    if ptype != MSG_READ_MEM_RES:
        logger.error('Expected MSG_READ_MEM_RES but got %s. Shutting down.', ptype)
        exit(-1)
    return payload

def send_write_mem_req(address, data):
    m = struct.pack('=IIBI', 4 + len(data), 0, MSG_WRITE_MEM_REQ, address) + data
    drv.sendall(m)

def write_mem(address, data):
    send_write_mem_req(address, data)
    stream_id, ptype, payload = wait_for_msg()
    if ptype != MSG_WRITE_MEM_RES:
        logger.error('Expected MSG_WRITE_MEM_RES but got %s. Shutting down.', ptype)
        exit(-1)

def send_connect_response(stream_id, result):
    m = struct.pack('=IIBB', 1, stream_id, MSG_CONNECT_RESPONSE, result)
    drv.sendall(m)

def send_data(stream_id, data):
    m = struct.pack('=IIB', len(data), stream_id, MSG_DATA) + data
    drv.sendall(m)

def send_eos(stream_id):
    m = struct.pack('=IIB', 0, stream_id, MSG_EOS)
    drv.sendall(m)

def send_reset(stream_id):
    m = struct.pack('=IIB', 0, stream_id, MSG_RESET)
    drv.sendall(m)

current_stream_id = None
first_msg = True
raw_received = b''
is_empty = [True, True]

def process_msg_data(payload):
    global ptrs, first_msg, raw_received

    if first_msg:
        ptrs = struct.unpack('>II', payload)
        logger.debug('Received pointers %s', ptrs)
        first_msg = False
        return

    buf_index = payload[0]

    if len(raw_received) < 900*2:
        if not is_empty[buf_index]:
            data = b'\x00' * (900*2)
            send_write_mem_req(ptrs[buf_index], data)
            is_empty[buf_index] = True
    else:
        ldata = raw_received[0:900*2:2]
        rdata = raw_received[1:900*2:2]
        data = ldata + rdata
        raw_received = raw_received[900*2:]
        send_write_mem_req(ptrs[buf_index], data)
        is_empty[buf_index] = False

def process_drv_msg(stream_id, ptype, payload):
    global current_stream_id, first_msg

    if ptype == MSG_CONNECT:
        if payload == b'piaudio' and current_stream_id is None:
            logger.info('Amiga connected')
            current_stream_id = stream_id
            first_msg = True
            send_connect_response(stream_id, 0)
        else:
            send_connect_response(stream_id, 3)
    elif current_stream_id == stream_id:
        if ptype == MSG_DATA:
            process_msg_data(payload)
        elif ptype == MSG_EOS:
            pass
        elif ptype == MSG_RESET:
            current_stream_id = None
            logger.info('Amiga disconnected')

done = False

try:
    idx = sys.argv.index('-ondemand')
except ValueError:
    idx = -1

if idx != -1:
    fd = int(sys.argv[idx + 1])
    drv = socket.socket(fileno=fd)
else:
    drv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    drv.connect(('localhost', 7110))
    drv.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)

    send_register_req(b'piaudio')
    _, _, payload = wait_for_msg()
    if payload[0] != 1:
        logger.error('Unable to register piaudio with driver, shutting down')
        drv.close()
        done = True

rbuf = b''

PIPE_NAME = '/tmp/piaudio_pipe'

if not done:
    exists = True
    try:
        if (os.stat(PIPE_NAME).st_mode & 0o170000) != 0o10000:
            logger.error('A file that is not a named pipe exists at ' + PIPE_NAME)
            done = True
    except:
        exists = False

if not done and not exists:
    try:
        os.mkfifo(PIPE_NAME)
    except:
        logger.error('Unable to create named pipe at ' + PIPE_NAME)
        done = True

if not done:
    try:
        pipe_fd = os.open(PIPE_NAME, os.O_RDONLY | os.O_NONBLOCK)
        fcntl.fcntl(pipe_fd, fcntl.F_SETPIPE_SZ, 4096)
    except:
        logger.error('Unable to open named pipe at ' + PIPE_NAME)
        done = True

if not done:
    logger.info('piaudio service is running')

while not done:
    sel_fds = [drv]

    if idx == -1:
        sel_fds.append(sys.stdin)

    if len(raw_received) < 900*2:
        sel_fds.append(pipe_fd)

    rfd, wfd, xfd = select.select(sel_fds, [], [], 5.0)

    for fd in rfd:
        if fd == sys.stdin:
            line = sys.stdin.readline()
            if not line or line.startswith('quit'):
                if current_stream_id is not None:
                    send_reset(current_stream_id)
                drv.close()
                done = True
        elif fd == drv:
            buf = drv.recv(1024)
            if not buf:
                if current_stream_id is not None:
                    send_reset(current_stream_id)
                drv.close()
                done = True
            else:
                rbuf += buf
                while True:
                    if len(rbuf) < 9:
                        break

                    (plen, stream_id, ptype) = struct.unpack('=IIB', rbuf[:9])
                    if len(rbuf) < 9 + plen:
                        break

                    rbuf = rbuf[9:]
                    payload = rbuf[:plen]
                    rbuf = rbuf[plen:]

                    process_drv_msg(stream_id, ptype, payload)
        elif fd == pipe_fd:
            data = os.read(pipe_fd, 900*2)

            if len(data) == 0:
                os.close(pipe_fd)

                l = len(raw_received)
                c = l // (900*2)
                if c * 900*2 < l:
                    raw_received += b'\x00' * ((c + 1) * 900*2 - l)

                pipe_fd = os.open(PIPE_NAME, os.O_RDONLY | os.O_NONBLOCK)
                fcntl.fcntl(pipe_fd, fcntl.F_SETPIPE_SZ, 4096)
            else:
                raw_received += data


text: vc piaudio.c -lamiga -o PiAudio


text:
#include <exec/types.h>
#include <exec/memory.h>

#include <libraries/dos.h>

#include <devices/audio.h>

#include <proto/alib.h>
#include <proto/exec.h>

#include <stdio.h>
#include <string.h>
#include <time.h>

#include "../a314device/a314.h"
#include "../a314device/proto_a314.h"

#define SERVICE_NAME "piaudio"

#define FREQ 18000
#define BUFFER_LEN_MS 50
#define SAMPLES (FREQ * BUFFER_LEN_MS / 1000)

#define R0		1
#define L0		2
#define L1		4
#define R1		8

#define LEFT_CHAN_MASK	(L0 | L1)
#define RIGHT_CHAN_MASK	(R0 | R1)

#define LEFT 0
#define RIGHT 1

struct MsgPort *sync_mp = NULL;
struct MsgPort *async_mp = NULL;

struct A314_IORequest *sync_a314_req = NULL;
struct A314_IORequest *write_a314_req = NULL;

struct Library *A314Base;

char *audio_buffers[4] = { NULL, NULL, NULL, NULL };
ULONG a314_buffers[2] = { INVALID_A314_ADDRESS, INVALID_A314_ADDRESS };

struct IOAudio *sync_audio_req = NULL;
struct IOAudio *async_audio_req[4] = { NULL, NULL, NULL, NULL };

ULONG allocated_channels;

BOOL a314_device_open = FALSE;
BOOL audio_device_open = FALSE;
BOOL stream_open = FALSE;
BOOL pending_a314_write = FALSE;
BOOL has_a314_chip_memory = FALSE;

ULONG socket;
int back_index = 0;
char awbuf[8];

void start_a314_cmd(struct MsgPort *reply_port, struct A314_IORequest *ior, UWORD cmd, char *buffer, int length)
{
	ior->a314_Request.io_Message.mn_ReplyPort = reply_port;
	ior->a314_Request.io_Command = cmd;
	ior->a314_Request.io_Error = 0;
	ior->a314_Socket = socket;
	ior->a314_Buffer = buffer;
	ior->a314_Length = length;
	SendIO((struct IORequest *)ior);
}

BYTE a314_connect(char *name)
{
	socket = time(NULL);
	start_a314_cmd(sync_mp, sync_a314_req, A314_CONNECT, name, strlen(name));
	Wait(1L << sync_mp->mp_SigBit);
	GetMsg(sync_mp);
	return sync_a314_req->a314_Request.io_Error;
}

BYTE a314_write(char *buffer, int length)
{
	start_a314_cmd(sync_mp, sync_a314_req, A314_WRITE, buffer, length);
	Wait(1L << sync_mp->mp_SigBit);
	GetMsg(sync_mp);
	return sync_a314_req->a314_Request.io_Error;
}

BYTE a314_eos()
{
	start_a314_cmd(sync_mp, sync_a314_req, A314_EOS, NULL, 0);
	Wait(1L << sync_mp->mp_SigBit);
	GetMsg(sync_mp);
	return sync_a314_req->a314_Request.io_Error;
}

BYTE a314_reset()
{
	start_a314_cmd(sync_mp, sync_a314_req, A314_RESET, NULL, 0);
	Wait(1L << sync_mp->mp_SigBit);
	GetMsg(sync_mp);
	return sync_a314_req->a314_Request.io_Error;
}

void start_a314_write(char *buffer, int length)
{
	start_a314_cmd(async_mp, write_a314_req, A314_WRITE, buffer, length);
	pending_a314_write = TRUE;
}

void submit_async_audio_req(int index)
{
	ULONG mask = ((index & 1) == LEFT) ? LEFT_CHAN_MASK : RIGHT_CHAN_MASK;
	ULONG unit = allocated_channels & mask;

	async_audio_req[index]->ioa_Request.io_Message.mn_ReplyPort = async_mp;
	async_audio_req[index]->ioa_Request.io_Command = CMD_WRITE;
	async_audio_req[index]->ioa_Request.io_Flags = ADIOF_PERVOL;
	async_audio_req[index]->ioa_Request.io_Unit = (void*)unit;
	async_audio_req[index]->ioa_Data = audio_buffers[index];
	async_audio_req[index]->ioa_Length = SAMPLES;
	async_audio_req[index]->ioa_Period = 197;
	async_audio_req[index]->ioa_Volume = 64;
	async_audio_req[index]->ioa_Cycles = 1;
	BeginIO((struct IORequest *)async_audio_req[index]);
}

int main()
{
	LONG old_priority = SetTaskPri(FindTask(NULL), 50);

	sync_mp = CreatePort(NULL, 0);
	if (!sync_mp)
	{
		printf("Unable to create sync reply message port\n");
		goto cleanup;
	}

	async_mp = CreatePort(NULL, 0);
	if (!async_mp)
	{
		printf("Unable to create async reply message port\n");
		goto cleanup;
	}

	sync_a314_req = (struct A314_IORequest *)CreateExtIO(sync_mp, sizeof(struct A314_IORequest));
	write_a314_req = (struct A314_IORequest *)CreateExtIO(sync_mp, sizeof(struct A314_IORequest));
	if (!sync_a314_req || !write_a314_req)
	{
		printf("Unable to create A314_IORequest\n");
		goto cleanup;
	}

	if (OpenDevice(A314_NAME, 0, (struct IORequest *)sync_a314_req, 0))
	{
		printf("Unable to open a314.device\n");
		goto cleanup;
	}

	a314_device_open = TRUE;

	A314Base = &(sync_a314_req->a314_Request.io_Device->dd_Library);

	memcpy(write_a314_req, sync_a314_req, sizeof(struct A314_IORequest));

	audio_buffers[0] = AllocMem(SAMPLES * 2, MEMF_A314 | MEMF_CHIP | MEMF_CLEAR);
	if (audio_buffers[0])
	{
		audio_buffers[2] = AllocMem(SAMPLES * 2, MEMF_A314 | MEMF_CHIP | MEMF_CLEAR);
		if (!audio_buffers[2])
		{
			printf("Unable to allocate audio buffers in A314 chip memory\n");
			goto cleanup;
		}
		has_a314_chip_memory = TRUE;
	}
	else
	{
		audio_buffers[0] = AllocMem(SAMPLES * 2, MEMF_CHIP | MEMF_CLEAR);
		audio_buffers[2] = AllocMem(SAMPLES * 2, MEMF_CHIP | MEMF_CLEAR);
		if (!audio_buffers[0] || !audio_buffers[2])
		{
			printf("Unable to allocate audio buffers in chip memory\n");
			goto cleanup;
		}

		a314_buffers[0] = AllocMemA314(SAMPLES * 2);
		a314_buffers[1] = AllocMemA314(SAMPLES * 2);
		if (a314_buffers[0] == INVALID_A314_ADDRESS ||
			a314_buffers[1] == INVALID_A314_ADDRESS)
		{
			printf("Unable to allocate buffers in A314 memory\n");
			goto cleanup;
		}

		WriteMemA314(a314_buffers[0], audio_buffers[0], SAMPLES * 2);
		WriteMemA314(a314_buffers[1], audio_buffers[2], SAMPLES * 2);
	}

	audio_buffers[1] = audio_buffers[0] + SAMPLES;
	audio_buffers[3] = audio_buffers[2] + SAMPLES;

	sync_audio_req = (struct IOAudio *)CreateExtIO(sync_mp, sizeof(struct IOAudio));
	if (!sync_audio_req)
	{
		printf("Unable to allocate sync audio request\n");
		goto cleanup;
	}

	int i;
	for (i = 0; i < 4; i++)
	{
		async_audio_req[i] = AllocMem(sizeof(struct IOAudio), MEMF_PUBLIC);
		if (!async_audio_req[i])
		{
			printf("Unable to allocate async audio request\n");
			goto cleanup;
		}
	}

	UBYTE which_channels[] = { L0 | R0, L0 | R1, L1 | R0, L1 | R1 };

	sync_audio_req->ioa_Request.io_Message.mn_ReplyPort = sync_mp;
	sync_audio_req->ioa_Request.io_Message.mn_Node.ln_Pri = 127;
	sync_audio_req->ioa_Request.io_Command = ADCMD_ALLOCATE;
	sync_audio_req->ioa_Request.io_Flags = ADIOF_NOWAIT;
	sync_audio_req->ioa_AllocKey = 0;
	sync_audio_req->ioa_Data = which_channels;
	sync_audio_req->ioa_Length = sizeof(which_channels);

	if (OpenDevice(AUDIONAME, 0, (struct IORequest *)sync_audio_req, 0))
	{
		printf("Unable to open audio.device\n");
		goto cleanup;
	}

	audio_device_open = TRUE;

	allocated_channels = (ULONG)sync_audio_req->ioa_Request.io_Unit;

	for (i = 0; i < 4; i++)
		memcpy(async_audio_req[i], sync_audio_req, sizeof(struct IOAudio));

	if (a314_connect(SERVICE_NAME) != A314_CONNECT_OK)
	{
		printf("Unable to connect to piaudio service\n");
		goto cleanup;
	}

	stream_open = TRUE;

	ULONG *buf_ptrs = (ULONG *)awbuf;
	if (has_a314_chip_memory)
	{
		buf_ptrs[0] = TranslateAddressA314(audio_buffers[0]);
		buf_ptrs[1] = TranslateAddressA314(audio_buffers[2]);
	}
	else
	{
		buf_ptrs[0] = a314_buffers[0];
		buf_ptrs[1] = a314_buffers[1];
	}

	if (a314_write(awbuf, 8) != A314_WRITE_OK)
	{
		printf("Unable to write buffer pointers\n");
		goto cleanup;
	}

	printf("PiAudio started, allocated channels: L%d, R%d\n",
		(allocated_channels & LEFT_CHAN_MASK) == L0 ? 0 : 1,
		(allocated_channels & RIGHT_CHAN_MASK) == R0 ? 0 : 1);

	sync_audio_req->ioa_Request.io_Command = CMD_STOP;
	DoIO((struct IORequest *)sync_audio_req);

	submit_async_audio_req(back_index + LEFT);
	submit_async_audio_req(back_index + RIGHT);

	sync_audio_req->ioa_Request.io_Command = CMD_START;
	DoIO((struct IORequest *)sync_audio_req);

	int pending_audio_reqs = 2;

	ULONG portsig = 1L << async_mp->mp_SigBit;

	printf("Press ctrl-c to exit...\n");

	while (TRUE)
	{
		if (pending_audio_reqs <= 2)
		{
			back_index ^= 2;

			if (!has_a314_chip_memory)
			{
				char *dst = audio_buffers[back_index];
				ULONG address = a314_buffers[back_index >> 1];
				ReadMemA314(dst, address, SAMPLES * 2);
			}

			submit_async_audio_req(back_index + LEFT);
			submit_async_audio_req(back_index + RIGHT);

			pending_audio_reqs += 2;

			if (!pending_a314_write)
			{
				awbuf[0] = back_index == 0 ? 0 : 1;
				start_a314_write(awbuf, 1);
			}
		}

		ULONG signal = Wait(SIGBREAKF_CTRL_C | portsig);

		if (signal & SIGBREAKF_CTRL_C)
			break;
		else if (signal & portsig)
		{
			struct Message *msg;
			while (msg = GetMsg(async_mp))
			{
				if (msg == (struct Message *)write_a314_req)
				{
					if (write_a314_req->a314_Request.io_Error == A314_WRITE_OK)
						pending_a314_write = FALSE;
					else
						goto cleanup;
				}
				else
					pending_audio_reqs--;
			}
		}
	}

cleanup:
	if (stream_open)
		a314_reset();
	if (audio_device_open)
		CloseDevice((struct IORequest *)sync_audio_req);
	for (i = 3; i >= 0; i--)
		if (async_audio_req[i])
			FreeMem(async_audio_req[i], sizeof(struct IOAudio));
	if (sync_audio_req)
		DeleteExtIO((struct IORequest *)sync_audio_req);
	if (a314_buffers[1] != INVALID_A314_ADDRESS)
		FreeMemA314(a314_buffers[1], SAMPLES * 2);
	if (a314_buffers[0] != INVALID_A314_ADDRESS)
		FreeMemA314(a314_buffers[0], SAMPLES * 2);
	if (audio_buffers[2])
		FreeMem(audio_buffers[2], SAMPLES * 2);
	if (audio_buffers[0])
		FreeMem(audio_buffers[0], SAMPLES * 2);
	if (a314_device_open)
		CloseDevice((struct IORequest *)sync_a314_req);
	if (write_a314_req)
		DeleteExtIO((struct IORequest *)write_a314_req);
	if (sync_a314_req)
		DeleteExtIO((struct IORequest *)sync_a314_req);
	if (async_mp)
		DeletePort(async_mp);
	if (sync_mp)
		DeletePort(sync_mp);
	SetTaskPri(FindTask(NULL), old_priority);

	return 0;
}


text:
# PiAudio

PiAudio is a service that integrates with ALSA, the sound sub-system on the Raspberry Pi, and lets sound samples be played via Paula on the Amiga.

The *piaudio* program should run on the Amiga. It allocates two sounds channels, one left and one right. Using the following command the piaudio program can run in the background:
```
run piaudio >NIL:
```

The file *.asoundrc* should be stored in `/home/pi` on the RPi. Most programs that play audio on the RPi can then be used. One such program is mpg123, which is started as:
```
mpg123 -a amiga song.mp3
```


text:
/*
 * Copyright (c) 2020 Niklas Ekström
 *
 * Thanks to Christian Vogelgsang and Mike Sterling for inspiration gained from their SANA-II drivers:
 * - https://github.com/cnvogelg/plipbox
 * - https://github.com/mikestir/k1208-drivers
 */

#include <exec/types.h>
#include <exec/execbase.h>
#include <exec/devices.h>
#include <exec/errors.h>
#include <exec/ports.h>
#include <libraries/dos.h>
#include <devices/sana2.h>

#include <proto/alib.h>
#include <proto/exec.h>
#include <proto/dos.h>

#include <string.h>

#include "../a314device/a314.h"
#include "../a314device/proto_a314.h"

// Defines.

#define DEVICE_NAME		"a314eth.device"
#define SERVICE_NAME		"ethernet"

#define TASK_PRIO		10

#define MACADDR_SIZE		6
#define NIC_BPS			10000000

#define ETH_MTU			1500
#define RAW_MTU			1518

#define READ_FRAME_REQ		1
#define WRITE_FRAME_REQ		2
#define READ_FRAME_RES		3
#define WRITE_FRAME_RES		4

#define ET_RBUF_CNT		2
#define ET_WBUF_CNT		2
#define ET_BUF_CNT		(ET_RBUF_CNT + ET_WBUF_CNT)

// Typedefs.

typedef BOOL (*buf_copy_func_t)(__reg("a0") void *dst, __reg("a1") void *src, __reg("d0") LONG size);

// Structs.

#pragma pack(push, 1)
struct EthHdr
{
	unsigned char eh_Dst[MACADDR_SIZE];
	unsigned char eh_Src[MACADDR_SIZE];
	unsigned short eh_Type;
};
#pragma pack(pop)

#pragma pack(push, 1)
struct ServiceMsg
{
	ULONG sm_Address;
	UWORD sm_Length;
	UWORD sm_Kind;
};
#pragma pack(pop)

struct BufDesc
{
	struct MinNode bd_Node;
	void *bd_Buffer;
	ULONG bd_BufferAddress;
	int bd_Length;
};

// Constants.

const char device_name[] = DEVICE_NAME;
const char id_string[] = DEVICE_NAME " 1.0 (20 July 2020)";

static const char service_name[] = SERVICE_NAME;
static const char a314_device_name[] = A314_NAME;

static const unsigned char macaddr[MACADDR_SIZE] = { 0x40, 0x61, 0x33, 0x31, 0x34, 0x65 };

// Global variables.

BPTR saved_seg_list;
struct ExecBase *SysBase;
struct DosLibrary *DOSBase;
struct Library *A314Base;

buf_copy_func_t copyfrom;
buf_copy_func_t copyto;

volatile struct List ut_rbuf_list;
volatile struct List ut_wbuf_list;

struct BufDesc et_bufs[ET_BUF_CNT];

struct List et_rbuf_free_list;
struct List et_rbuf_pending_list;
struct List et_rbuf_has_data_list;

struct List et_wbuf_free_list;
struct List et_wbuf_pending_list;

LONG a314_socket;
short last_req_kind;

struct MsgPort a314_mp;

struct A314_IORequest read_ior;
struct A314_IORequest write_ior;
struct A314_IORequest reset_ior;

BOOL pending_a314_read;
BOOL pending_a314_write;
BOOL pending_a314_reset;

struct ServiceMsg a314_read_buf;
struct ServiceMsg a314_write_buf;

volatile ULONG sana2_sigmask;
volatile ULONG shutdown_sigmask;

volatile struct Task *init_task;

struct Process *device_process;
volatile int device_start_error;

// External declarations.

extern void device_process_seglist();

// Procedures.

static struct Library *init_device(__reg("a6") struct ExecBase *sys_base, __reg("a0") BPTR seg_list, __reg("d0") struct Library *dev)
{
	saved_seg_list = seg_list;

	dev->lib_Node.ln_Type = NT_DEVICE;
	dev->lib_Node.ln_Name = (char *)device_name;
	dev->lib_Flags = LIBF_SUMUSED | LIBF_CHANGED;
	dev->lib_Version = 1;
	dev->lib_Revision = 0;
	dev->lib_IdString = (APTR)id_string;

	SysBase = *(struct ExecBase **)4;
	DOSBase = (struct DosLibrary *)OpenLibrary(DOSNAME, 0);

	return dev;
}

static BPTR expunge(__reg("a6") struct Library *dev)
{
	if (dev->lib_OpenCnt)
	{
		dev->lib_Flags |= LIBF_DELEXP;
		return 0;
	}

	// Shady way of waiting for device process to terminate before unloading.
	Delay(10);

	CloseLibrary((struct Library *)DOSBase);

	Remove(&dev->lib_Node);
	FreeMem((char *)dev - dev->lib_NegSize, dev->lib_NegSize + dev->lib_PosSize);
	return saved_seg_list;
}

static void send_a314_cmd(struct A314_IORequest *ior, UWORD cmd, char *buffer, int length)
{
	ior->a314_Request.io_Command = cmd;
	ior->a314_Request.io_Error = 0;
	ior->a314_Socket = a314_socket;
	ior->a314_Buffer = buffer;
	ior->a314_Length = length;
	SendIO((struct IORequest *)ior);
}

static void do_a314_cmd(struct A314_IORequest *ior, UWORD cmd, char *buffer, int length)
{
	ior->a314_Request.io_Command = cmd;
	ior->a314_Request.io_Error = 0;
	ior->a314_Socket = a314_socket;
	ior->a314_Buffer = buffer;
	ior->a314_Length = length;
	DoIO((struct IORequest *)ior);
}

static void copy_from_bd_and_reply(struct IOSana2Req *ios2, struct BufDesc *bd)
{
	// TODO: Use S2_DMACopyToBuff32 instead of S2_CopyToBuff (if possible).
	// This will avoid copying from A314 shared memory to shadow buffer
	// before copying to stack's buffer.

	ReadMemA314(bd->bd_Buffer, bd->bd_BufferAddress, bd->bd_Length);

	struct EthHdr *eh = bd->bd_Buffer;

	if (ios2->ios2_Req.io_Flags & SANA2IOF_RAW)
	{
		ios2->ios2_DataLength = bd->bd_Length;
		copyto(ios2->ios2_Data, bd->bd_Buffer, ios2->ios2_DataLength);
		ios2->ios2_Req.io_Flags = SANA2IOF_RAW;
	}
	else
	{
		ios2->ios2_DataLength = bd->bd_Length - sizeof(struct EthHdr);
		copyto(ios2->ios2_Data, &eh[1], ios2->ios2_DataLength);
		ios2->ios2_Req.io_Flags = 0;
	}

	memcpy(ios2->ios2_SrcAddr, eh->eh_Src, MACADDR_SIZE);
	memcpy(ios2->ios2_DstAddr, eh->eh_Dst, MACADDR_SIZE);

	BOOL bcast = TRUE;
	for (int i = 0; i < MACADDR_SIZE; i++)
	{
		if (eh->eh_Dst[i] != 0xff)
		{
			bcast = FALSE;
			break;
		}
	}

	if (bcast)
		ios2->ios2_Req.io_Flags |= SANA2IOF_BCAST;

	ios2->ios2_PacketType = eh->eh_Type;

	ios2->ios2_Req.io_Error = 0;
	ReplyMsg(&ios2->ios2_Req.io_Message);
}

static void copy_to_bd_and_reply(struct BufDesc *bd, struct IOSana2Req *ios2)
{
	// TODO: Use S2_DMACopyFromBuff32 instead of S2_CopyFromBuff (if possible).
	// This will avoid copying to shadow buffer before writing to
	// A314 shared memory.

	struct EthHdr *eh = bd->bd_Buffer;

	if (ios2->ios2_Req.io_Flags & SANA2IOF_RAW)
	{
		copyfrom(bd->bd_Buffer, ios2->ios2_Data, ios2->ios2_DataLength);
		bd->bd_Length = ios2->ios2_DataLength;
	}
	else
	{
		eh->eh_Type = ios2->ios2_PacketType;
		memcpy(eh->eh_Src, macaddr, sizeof(macaddr));
		memcpy(eh->eh_Dst, ios2->ios2_DstAddr, MACADDR_SIZE);
		copyfrom(&eh[1], ios2->ios2_Data, ios2->ios2_DataLength);
		bd->bd_Length = ios2->ios2_DataLength + sizeof(struct EthHdr);
	}

	WriteMemA314(bd->bd_BufferAddress, bd->bd_Buffer, bd->bd_Length);

	ios2->ios2_Req.io_Error = 0;
	ReplyMsg(&ios2->ios2_Req.io_Message);
}

static void handle_a314_reply(struct A314_IORequest *ior)
{
	if (ior == &write_ior)
	{
		pending_a314_write = FALSE;

		if (ior->a314_Request.io_Error == A314_WRITE_OK)
		{
			// Start new write later.
		}
		else // A314_WRITE_RESET
		{
			// TODO: Handle. What if pi-side is shutting down.
		}
	}
	else if (ior == &read_ior)
	{
		pending_a314_read = FALSE;

		if (ior->a314_Request.io_Error == A314_READ_OK)
		{
			if (a314_read_buf.sm_Kind == WRITE_FRAME_RES)
			{
				struct BufDesc *bd = (struct BufDesc *)RemHead(&et_wbuf_pending_list);
				AddTail(&et_wbuf_free_list, (struct Node *)bd);
			}
			else // READ_FRAME_RES
			{
				struct BufDesc *bd = (struct BufDesc *)RemHead(&et_rbuf_pending_list);
				bd->bd_Length = a314_read_buf.sm_Length;
				AddTail(&et_rbuf_has_data_list, (struct Node *)bd);
			}

			send_a314_cmd(&read_ior, A314_READ, (void *)&a314_read_buf, sizeof(a314_read_buf));
			pending_a314_read = TRUE;
		}
		else // A314_READ_RESET
		{
			// TODO: Handle. What if pi-side is shutting down.
		}
	}
	else if (ior == &reset_ior)
	{
		pending_a314_reset = FALSE;
	}
}

static struct IOSana2Req *remove_matching_rbuf(ULONG type)
{
	struct Node *node = ut_rbuf_list.lh_Head;
	while (node->ln_Succ)
	{
		struct IOSana2Req *ios2 = (struct IOSana2Req *)node;
		if (ios2->ios2_PacketType == type)
		{
			Remove(node);
			return ios2;
		}
		node = node->ln_Succ;
	}
	return NULL;
}

static void complete_read_reqs()
{
	struct Node *node = et_rbuf_has_data_list.lh_Head;
	if (!node->ln_Succ)
		return;

	Forbid();
	while (node->ln_Succ)
	{
		struct BufDesc *bd = (struct BufDesc *)node;

		UWORD eh_type;
		// Hardcoded offset (12) and size (2) of eh_Type in EthHdr.
		ReadMemA314(&eh_type, bd->bd_BufferAddress + 12, 2);

		node = node->ln_Succ;

		struct IOSana2Req *ios2 = remove_matching_rbuf(eh_type);
		if (ios2)
		{
			copy_from_bd_and_reply(ios2, bd);
		}

		Remove((struct Node *)bd);
		AddTail(&et_rbuf_free_list, (struct Node *)bd);
	}
	Permit();
}

static void maybe_write_req()
{
	if (pending_a314_write)
		return;

	BOOL free_et_wbuf = et_wbuf_free_list.lh_Head->ln_Succ != NULL;
	BOOL idle_et_rbuf = et_rbuf_free_list.lh_Head->ln_Succ != NULL;

	Forbid();

	BOOL waiting_ut_wbuf = ut_wbuf_list.lh_Head->ln_Succ != NULL;

	BOOL want_wbuf = free_et_wbuf && waiting_ut_wbuf;
	BOOL want_rbuf = idle_et_rbuf;

	if (!want_rbuf && !want_wbuf)
	{
		Permit();
		return;
	}

	short next_req_kind = 0;

	if (last_req_kind == WRITE_FRAME_REQ)
		next_req_kind = want_rbuf ? READ_FRAME_REQ : WRITE_FRAME_REQ;
	else
		next_req_kind = want_wbuf ? WRITE_FRAME_REQ : READ_FRAME_REQ;

	struct IOSana2Req *ios2 = NULL;
	if (next_req_kind == WRITE_FRAME_REQ)
		ios2 = (struct IOSana2Req*)RemHead((struct List *)&ut_wbuf_list);

	Permit();

	struct BufDesc *bd;

	if (next_req_kind == READ_FRAME_REQ)
	{
		bd = (struct BufDesc *)RemHead(&et_rbuf_free_list);
		bd->bd_Length = RAW_MTU;
		AddTail(&et_rbuf_pending_list, (struct Node *)&bd->bd_Node);
	}
	else // WRITE_FRAME_REQ
	{
		bd = (struct BufDesc *)RemHead(&et_wbuf_free_list);
		copy_to_bd_and_reply(bd, ios2);
		AddTail(&et_wbuf_pending_list, (struct Node *)bd);
	}

	a314_write_buf.sm_Address = bd->bd_BufferAddress;
	a314_write_buf.sm_Length = bd->bd_Length;
	a314_write_buf.sm_Kind = next_req_kind;

	send_a314_cmd(&write_ior, A314_WRITE, (void *)&a314_write_buf, sizeof(a314_write_buf));
	pending_a314_write = TRUE;

	last_req_kind = next_req_kind;
}

void device_process_run()
{
	ULONG sana2_signal = AllocSignal(-1);
	sana2_sigmask = 1UL << sana2_signal;

	ULONG shutdown_signal = AllocSignal(-1);
	shutdown_sigmask = 1UL << shutdown_signal;

	a314_mp.mp_SigBit = AllocSignal(-1);
	a314_mp.mp_SigTask = FindTask(NULL);

	do_a314_cmd(&reset_ior, A314_CONNECT, (char *)service_name, strlen(service_name));
	device_start_error = reset_ior.a314_Request.io_Error == A314_CONNECT_OK ? 0 : -1;

	Signal((struct Task *)init_task, SIGF_SINGLE);

	if (device_start_error)
		return;

	ULONG a314_sigmask = 1UL << a314_mp.mp_SigBit;

	send_a314_cmd(&read_ior, A314_READ, (void *)&a314_read_buf, sizeof(a314_read_buf));
	pending_a314_read = TRUE;

	BOOL shutting_down = FALSE;

	while (TRUE)
	{
		complete_read_reqs();
		maybe_write_req();

		if (shutting_down && !pending_a314_read && !pending_a314_write && !pending_a314_reset)
			break;

		ULONG sigs = Wait(a314_sigmask | sana2_sigmask | shutdown_sigmask);

		if ((sigs & shutdown_sigmask) && !shutting_down)
		{
			send_a314_cmd(&reset_ior, A314_RESET, NULL, 0);
			pending_a314_reset = TRUE;
			shutting_down = TRUE;
		}

		if (sigs & a314_sigmask)
		{
			struct A314_IORequest *ior;
			while ((ior = (struct A314_IORequest *)GetMsg(&a314_mp)))
				handle_a314_reply(ior);
		}
	}

	Signal((struct Task *)init_task, SIGF_SINGLE);
}

static struct TagItem *FindTagItem(Tag tagVal, struct TagItem *tagList)
{
	struct TagItem *ti = tagList;
	while (ti && ti->ti_Tag != tagVal)
	{
		switch (ti->ti_Tag)
		{
		case TAG_DONE:
			return NULL;
		case TAG_MORE:
			ti = (struct TagItem *)ti->ti_Data;
			break;
		case TAG_SKIP:
			ti += ti->ti_Data + 1;
			break;
		case TAG_IGNORE:
		default:
			ti++;
			break;
		}
	}
	return ti;
}

static ULONG GetTagData(Tag tagVal, ULONG defaultData, struct TagItem *tagList)
{
	struct TagItem *ti = FindTagItem(tagVal, tagList);
	return ti ? ti->ti_Data : defaultData;
}

static void open(__reg("a6") struct Library *dev, __reg("a1") struct IOSana2Req *ios2, __reg("d0") ULONG unitnum, __reg("d1") ULONG flags)
{
	ios2->ios2_Req.io_Error = IOERR_OPENFAIL;
	ios2->ios2_Req.io_Message.mn_Node.ln_Type = NT_REPLYMSG;

	if (unitnum != 0 || dev->lib_OpenCnt)
		return;

	dev->lib_OpenCnt++;

	copyfrom = (buf_copy_func_t)GetTagData(S2_CopyFromBuff, 0, (struct TagItem *)ios2->ios2_BufferManagement);
	copyto = (buf_copy_func_t)GetTagData(S2_CopyToBuff, 0, (struct TagItem *)ios2->ios2_BufferManagement);
	ios2->ios2_BufferManagement = (void *)0xdeadbeefUL;

	memset(&a314_mp, 0, sizeof(a314_mp));
	a314_mp.mp_Node.ln_Pri = 0;
	a314_mp.mp_Node.ln_Type = NT_MSGPORT;
	a314_mp.mp_Node.ln_Name = (char *)device_name;
	a314_mp.mp_Flags = PA_SIGNAL;
	NewList(&a314_mp.mp_MsgList);

	memset(&write_ior, 0, sizeof(write_ior));
	write_ior.a314_Request.io_Message.mn_ReplyPort = &a314_mp;
	write_ior.a314_Request.io_Message.mn_Length = sizeof(write_ior);
	write_ior.a314_Request.io_Message.mn_Node.ln_Type = NT_REPLYMSG;

	A314Base = NULL;
	if (OpenDevice((char *)a314_device_name, 0, (struct IORequest *)&write_ior, 0))
		goto error;

	A314Base = &(write_ior.a314_Request.io_Device->dd_Library);

	memcpy(&read_ior, &write_ior, sizeof(read_ior));
	memcpy(&reset_ior, &write_ior, sizeof(reset_ior));

	struct DateStamp ds;
	DateStamp(&ds);
	a314_socket = (ds.ds_Minute * 60 * TICKS_PER_SECOND) + ds.ds_Tick;

	last_req_kind = WRITE_FRAME_REQ;

	NewList((struct List *)&ut_rbuf_list);
	NewList((struct List *)&ut_wbuf_list);

	NewList(&et_rbuf_free_list);
	NewList(&et_rbuf_pending_list);
	NewList(&et_rbuf_has_data_list);

	NewList(&et_wbuf_free_list);
	NewList(&et_wbuf_pending_list);

	for (int i = 0; i < ET_BUF_CNT; i++)
		memset(&et_bufs[i], 0, sizeof(struct BufDesc));

	for (int i = 0; i < ET_BUF_CNT; i++)
	{
		struct BufDesc *bd = &et_bufs[i];

		bd->bd_Buffer = AllocMem(RAW_MTU, 0);
		if (!bd->bd_Buffer)
			goto error;

		bd->bd_BufferAddress = AllocMemA314(RAW_MTU);
		if (bd->bd_BufferAddress == INVALID_A314_ADDRESS)
			goto error;

		if (i < ET_RBUF_CNT)
			AddTail(&et_rbuf_free_list, (struct Node*)&bd->bd_Node);
		else
			AddTail(&et_wbuf_free_list, (struct Node*)&bd->bd_Node);
	}

	init_task = FindTask(NULL);

	struct MsgPort *device_mp = CreateProc((char *)device_name, TASK_PRIO, ((ULONG)device_process_seglist) >> 2, 2048);
	if (!device_mp)
		goto error;

	device_process = (struct Process *)((char *)device_mp - sizeof(struct Task));

	Wait(SIGF_SINGLE);

	if (device_start_error)
		goto error;

	ios2->ios2_Req.io_Error = 0;
	return;

error:
	for (int i = ET_BUF_CNT - 1; i >= 0; i--)
	{
		if (et_bufs[i].bd_BufferAddress)
			FreeMemA314(et_bufs[i].bd_BufferAddress, RAW_MTU);

		if (et_bufs[i].bd_Buffer)
			FreeMem(et_bufs[i].bd_Buffer, RAW_MTU);
	}

	if (A314Base)
	{
		CloseDevice((struct IORequest *)&write_ior);
		A314Base = NULL;
	}

	dev->lib_OpenCnt--;
}

static BPTR close(__reg("a6") struct Library *dev, __reg("a1") struct IOSana2Req *ios2)
{
	init_task = FindTask(NULL);
	Signal(&device_process->pr_Task, shutdown_sigmask);
	Wait(SIGF_SINGLE);

	for (int i = ET_BUF_CNT - 1; i >= 0; i--)
	{
		FreeMemA314(et_bufs[i].bd_BufferAddress, RAW_MTU);
		FreeMem(et_bufs[i].bd_Buffer, RAW_MTU);
	}

	CloseDevice((struct IORequest *)&write_ior);
	A314Base = NULL;

	ios2->ios2_Req.io_Device = NULL;
	ios2->ios2_Req.io_Unit = NULL;

	dev->lib_OpenCnt--;

	if (dev->lib_OpenCnt == 0 && (dev->lib_Flags & LIBF_DELEXP))
		return expunge(dev);

	return 0;
}

static void device_query(struct IOSana2Req *req)
{
	struct Sana2DeviceQuery *query;

	query = req->ios2_StatData;
	query->DevQueryFormat = 0;
	query->DeviceLevel = 0;

	if (query->SizeAvailable >= 18)
		query->AddrFieldSize = MACADDR_SIZE * 8;

	if (query->SizeAvailable >= 22)
		query->MTU = ETH_MTU;

	if (query->SizeAvailable >= 26)
		query->BPS = NIC_BPS;

	if (query->SizeAvailable >= 30)
		query->HardwareType = S2WireType_Ethernet;

	query->SizeSupplied = query->SizeAvailable < 30 ? query->SizeAvailable : 30;
}

static void begin_io(__reg("a6") struct Library *dev, __reg("a1") struct IOSana2Req *ios2)
{
	ios2->ios2_Req.io_Error = S2ERR_NO_ERROR;
	ios2->ios2_WireError = S2WERR_GENERIC_ERROR;

	switch (ios2->ios2_Req.io_Command)
	{
	case CMD_READ:
		if (!ios2->ios2_BufferManagement)
		{
			ios2->ios2_Req.io_Error = S2ERR_BAD_ARGUMENT;
			ios2->ios2_WireError = S2WERR_BUFF_ERROR;
			break;
		}

		Forbid();
		AddTail((struct List *)&ut_rbuf_list, &ios2->ios2_Req.io_Message.mn_Node);
		Permit();

		ios2->ios2_Req.io_Flags &= ~SANA2IOF_QUICK;
		ios2 = NULL;

		Signal(&device_process->pr_Task, sana2_sigmask);
		break;

	case S2_BROADCAST:
		memset(ios2->ios2_DstAddr, 0xff, MACADDR_SIZE);
		/* Fall through */

	case CMD_WRITE:
		if (((ios2->ios2_Req.io_Flags & SANA2IOF_RAW) != 0 && ios2->ios2_DataLength > RAW_MTU) ||
			((ios2->ios2_Req.io_Flags & SANA2IOF_RAW) == 0 && ios2->ios2_DataLength > ETH_MTU))
		{
			ios2->ios2_Req.io_Error = S2ERR_MTU_EXCEEDED;
			break;
		}

		if (!ios2->ios2_BufferManagement)
		{
			ios2->ios2_Req.io_Error = S2ERR_BAD_ARGUMENT;
			ios2->ios2_WireError = S2WERR_BUFF_ERROR;
			break;
		}

		Forbid();
		AddTail((struct List *)&ut_wbuf_list, &ios2->ios2_Req.io_Message.mn_Node);
		Permit();

		ios2->ios2_Req.io_Flags &= ~SANA2IOF_QUICK;
		ios2 = NULL;

		Signal(&device_process->pr_Task, sana2_sigmask);
		break;

	case S2_ONLINE:
	case S2_OFFLINE:
	case S2_CONFIGINTERFACE:
		break;
	case S2_GETSTATIONADDRESS:
		memcpy(ios2->ios2_SrcAddr, macaddr, sizeof(macaddr));
		memcpy(ios2->ios2_DstAddr, macaddr, sizeof(macaddr));
		break;
	case S2_DEVICEQUERY:
		device_query(ios2);
		break;

	case S2_ONEVENT:
	case S2_TRACKTYPE:
	case S2_UNTRACKTYPE:
	case S2_GETTYPESTATS:
	case S2_READORPHAN:
	case S2_GETGLOBALSTATS:
	case S2_GETSPECIALSTATS:
		break;

	default:
		ios2->ios2_Req.io_Error = IOERR_NOCMD;
		ios2->ios2_WireError = S2WERR_GENERIC_ERROR;
		break;
	}

	if (ios2)
	{
		if (ios2->ios2_Req.io_Flags & SANA2IOF_QUICK)
			ios2->ios2_Req.io_Message.mn_Node.ln_Type = NT_MESSAGE;
		else
			ReplyMsg(&ios2->ios2_Req.io_Message);
	}
}

static void remove_from_list(struct List *list, struct Node *node)
{
	for (struct Node *n = list->lh_Head; n->ln_Succ; n = n->ln_Succ)
	{
		if (n == node)
		{
			Remove(n);
			return;
		}
	}
}

static ULONG abort_io(__reg("a6") struct Library *dev, __reg("a1") struct IOSana2Req *ios2)
{
	Forbid();
	remove_from_list((struct List *)&ut_rbuf_list, &ios2->ios2_Req.io_Message.mn_Node);
	remove_from_list((struct List *)&ut_wbuf_list, &ios2->ios2_Req.io_Message.mn_Node);
	Permit();

	ios2->ios2_Req.io_Error = IOERR_ABORTED;
	ios2->ios2_WireError = 0;
	ReplyMsg(&ios2->ios2_Req.io_Message);

	return 0;
}

static ULONG device_vectors[] =
{
	(ULONG)open,
	(ULONG)close,
	(ULONG)expunge,
	0,
	(ULONG)begin_io,
	(ULONG)abort_io,
	-1,
};

ULONG auto_init_tables[] =
{
	sizeof(struct Library),
	(ULONG)device_vectors,
	0,
	(ULONG)init_device,
};


text: vc romtag.asm device.c -O3 -nostdlib -o a314eth.device -lamiga


text:
	XDEF	_device_process_seglist
	XREF	_device_process_run

RTC_MATCHWORD:	equ	$4afc
RTF_AUTOINIT:	equ	(1<<7)
NT_DEVICE:	equ	3
VERSION:	equ	1
PRIORITY:	equ	0

		section	code,code

		moveq	#-1,d0
		rts

romtag:
		dc.w	RTC_MATCHWORD
		dc.l	romtag
		dc.l	endcode
		dc.b	RTF_AUTOINIT
		dc.b	VERSION
		dc.b	NT_DEVICE
		dc.b	PRIORITY
		dc.l	_device_name
		dc.l	_id_string
		dc.l	_auto_init_tables
endcode:

		cnop	0,4

		dc.l	16
_device_process_seglist:
		dc.l	0
		jmp	_device_process_run


text:
#!/usr/bin/python3
# -*- coding: utf-8 -*-

# Copyright (c) 2020 Niklas Ekström

import logging
import os
import pytun
import select
import socket
import struct
import sys
import time

logging.basicConfig(format = '%(levelname)s, %(asctime)s, %(name)s, line %(lineno)d: %(message)s')
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

MSG_REGISTER_REQ        = 1
MSG_REGISTER_RES        = 2
MSG_DEREGISTER_REQ      = 3
MSG_DEREGISTER_RES      = 4
MSG_READ_MEM_REQ        = 5
MSG_READ_MEM_RES        = 6
MSG_WRITE_MEM_REQ       = 7
MSG_WRITE_MEM_RES       = 8
MSG_CONNECT             = 9
MSG_CONNECT_RESPONSE    = 10
MSG_DATA                = 11
MSG_EOS                 = 12
MSG_RESET               = 13

def wait_for_msg():
    header = b''
    while len(header) < 9:
        data = drv.recv(9 - len(header))
        if not data:
            logger.error('Connection to a314d was closed, terminating.')
            exit(-1)
        header += data
    (plen, stream_id, ptype) = struct.unpack('=IIB', header)
    payload = b''
    while len(payload) < plen:
        data = drv.recv(plen - len(payload))
        if not data:
            logger.error('Connection to a314d was closed, terminating.')
            exit(-1)
        payload += data
    return (stream_id, ptype, payload)

def send_register_req(name):
    m = struct.pack('=IIB', len(name), 0, MSG_REGISTER_REQ) + name
    drv.sendall(m)

def send_read_mem_req(address, length):
    m = struct.pack('=IIBII', 8, 0, MSG_READ_MEM_REQ, address, length)
    drv.sendall(m)

def read_mem(address, length):
    send_read_mem_req(address, length)
    _, ptype, payload = wait_for_msg()
    if ptype != MSG_READ_MEM_RES:
        logger.error('Expected MSG_READ_MEM_RES but got %s. Shutting down.', ptype)
        exit(-1)
    return payload

def send_write_mem_req(address, data):
    m = struct.pack('=IIBI', 4 + len(data), 0, MSG_WRITE_MEM_REQ, address) + data
    drv.sendall(m)

def write_mem(address, data):
    send_write_mem_req(address, data)
    _, ptype, _ = wait_for_msg()
    if ptype != MSG_WRITE_MEM_RES:
        logger.error('Expected MSG_WRITE_MEM_RES but got %s. Shutting down.', ptype)
        exit(-1)

def send_connect_response(stream_id, result):
    m = struct.pack('=IIBB', 1, stream_id, MSG_CONNECT_RESPONSE, result)
    drv.sendall(m)

def send_data(stream_id, data):
    m = struct.pack('=IIB', len(data), stream_id, MSG_DATA) + data
    drv.sendall(m)

def send_eos(stream_id):
    m = struct.pack('=IIB', 0, stream_id, MSG_EOS)
    drv.sendall(m)

def send_reset(stream_id):
    m = struct.pack('=IIB', 0, stream_id, MSG_RESET)
    drv.sendall(m)

### A314 communication routines above. Actual driver below.

current_stream_id = None
done = False
rbuf = b''

DEV_NAME = 'tap0'
SERVICE_NAME = b'ethernet'

READ_FRAME_REQ = 1
WRITE_FRAME_REQ = 2
READ_FRAME_RES = 3
WRITE_FRAME_RES = 4

mem_read_queue = []
mem_write_queue = []

# Can buffer as many frames as fit in memory.
# Maybe should have a limit on the number of buffers?
waiting_read_reqs = []
buffered_frames = []

DROP_START_SECS = 15.0

def process_tap_frame(frame):
    if current_stream_id is None:
        return

    global drop_start
    if drop_start:
        if time.time() < start_time + DROP_START_SECS:
            return
        drop_start = False

    if waiting_read_reqs:
        stream_id, address, length = waiting_read_reqs.pop(0)

        if length < len(frame):
            logger.error('Fatal error, read frame from TAP larger than buffer')

        mem_write_queue.append((stream_id, address, len(frame)))
        send_write_mem_req(address, frame)
    else:
        buffered_frames.append(frame)

def process_stream_data(stream_id, data):
    address, length, kind = struct.unpack('>IHH', data)
    if kind == WRITE_FRAME_REQ:
        mem_read_queue.append((stream_id, address, length))
        send_read_mem_req(address, length)
    elif kind == READ_FRAME_REQ:
        if buffered_frames:
            frame = buffered_frames.pop(0)

            if length < len(frame):
                logger.error('Fatal error, read frame from TAP larger than buffer')

            mem_write_queue.append((stream_id, address, len(frame)))
            send_write_mem_req(address, frame)
        else:
            waiting_read_reqs.append((stream_id, address, length))

def process_read_mem_res(frame):
    tap.write(frame)

    stream_id, address, length = mem_read_queue.pop(0)
    if stream_id == current_stream_id:
        send_data(stream_id, struct.pack('>IHH', address, length, WRITE_FRAME_RES))

def process_write_mem_res():
    stream_id, address, length = mem_write_queue.pop(0)
    if stream_id == current_stream_id:
        send_data(stream_id, struct.pack('>IHH', address, length, READ_FRAME_RES))

def process_drv_msg(stream_id, ptype, payload):
    global current_stream_id

    if ptype == MSG_CONNECT:
        if payload == SERVICE_NAME and current_stream_id is None:
            logger.info('Amiga connected')
            current_stream_id = stream_id
            send_connect_response(stream_id, 0)
        else:
            send_connect_response(stream_id, 3)
    elif ptype == MSG_READ_MEM_RES:
        process_read_mem_res(payload)
    elif ptype == MSG_WRITE_MEM_RES:
        process_write_mem_res()
    elif current_stream_id == stream_id:
        if ptype == MSG_DATA:
            process_stream_data(stream_id, payload)
        elif ptype == MSG_EOS:
            # EOS is not used.
            pass
        elif ptype == MSG_RESET:
            current_stream_id = None
            waiting_read_reqs.clear()
            buffered_frames.clear()
            logger.info('Amiga disconnected')

try:
    idx = sys.argv.index('-ondemand')
except ValueError:
    idx = -1

if idx != -1:
    fd = int(sys.argv[idx + 1])
    drv = socket.socket(fileno=fd)
else:
    drv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    drv.connect(('localhost', 7110))
    drv.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)

    send_register_req(SERVICE_NAME)
    _, _, payload = wait_for_msg()
    if payload[0] != 1:
        logger.error('Unable to register ethernet with driver, shutting down')
        drv.close()
        done = True

if not done:
    try:
        tap = pytun.TunTapDevice(name=DEV_NAME, flags=pytun.IFF_TAP | pytun.IFF_NO_PI)
    except:
        logger.error('Unable to open tap device at ' + DEV_NAME)
        done = True

    start_time = time.time()
    drop_start = False # This doesn't seem to be needed any longer.

if not done:
    logger.info('Ethernet service is running')

while not done:
    try:
        rl, _, _ = select.select([drv, tap], [], [], 10.0)
    except KeyboardInterrupt:
        rl = []
        if current_stream_id is not None:
            send_reset(current_stream_id)
        drv.close()
        done = True

    if drv in rl:
        buf = drv.recv(1600)
        if not buf:
            if current_stream_id is not None:
                send_reset(current_stream_id)
            drv.close()
            done = True
        else:
            rbuf += buf
            while True:
                if len(rbuf) < 9:
                    break

                (plen, stream_id, ptype) = struct.unpack('=IIB', rbuf[:9])
                if len(rbuf) < 9 + plen:
                    break

                payload = rbuf[9:9+plen]
                rbuf = rbuf[9+plen:]

                process_drv_msg(stream_id, ptype, payload)

    if tap in rl:
        frame = tap.read(1600)
        process_tap_frame(frame)

tap.close()
logger.info('Ethernet service stopped')


text:
# a314eth.device - SANA-II driver for A314

This SANA-II driver works by copying Ethernet frames back and forth between the Amiga and a virtual ethernet interface (tap0) on the Raspberry Pi. The Pi will, when configured properly, do network address translation (NAT) and route packets from the Amiga to the Internet.

## Configuring the Raspberry Pi

If the `install-pi.sh` script was run to setup the Raspberry Pi then the following
steps have already been carried out, and no more configuration should be needed
unless your network require some special configuration.

- Install pytun: `sudo pip3 install python-pytun`.
- Copy `ethernet.py` to `/opt/a314/ethernet.py`.
- Update `/etc/opt/a314/a314d.conf` with a line that starts `ethernet.py` on demand.
  - In order for `a314d` to pick up the changes in `a314d.conf` you'll have to restart `a314d`, either by `sudo systemctl restart a314d` or by rebooting the Pi.
- Copy `pi-config/tap0` to `/etc/network/interfaces.d/tap0`. This file creates a tap device with ip address 192.168.2.1 when the Raspberry Pi is booted up.
- Enable ip forwarding by uncommenting the line in `/etc/sysctl.conf` that says `net.ipv4.ip_forward=1`.
- Add the lines in `pi-config/rc.local` to the bottom of `/etc/rc.local` just before `exit 0`. This create iptables rules that forwards packets from the `tap0` interface to the `wlan0` interface.
  - Please note that if the Pi is connected using wired ethernet then `wlan0` should be changed to `eth0`.

## Configuring the Amiga

This has been tested with the Roadshow and the MiamiDX TCP/IP stacks. These instructions show how to configure Roadshow and Miami for a314ethernet, they do not describe how to install either.

### Common

- Build the `a314eth.device` binary, for example using the `rpi_docker_build.sh` script.
- Copy `bin/a314eth.device` to `DEVS:`.

### Roadshow

- Copy `amiga-config/A314Eth` to `DEVS:NetInterfaces/A314Eth`.
- Copy `amiga-config/routes` to `DEVS:Internet/routes`.
- Copy `amiga-config/name_resolution` to `DEVS:Internet/name_resolution`.
  - You should change the nameserver to a DNS server that works on your network.
  - Note that there may be settings in the above two files (`routes` and `name_resolution`) that you wish to keep, so look through the changes you are about to make first.

Reboot the Amiga and with some luck you should be able to access the Internet from your Amiga.

### Miami

- Create new device entry under **Hardware**
  - Click **New** and select **Ethernet**
  - Name it accordingly, for example `a314eth`
  - **Type** should be **SANA-II driver**
  - For **Driver**, type in, or browse to and select, `DEVS:a314eth.device` and click **OK**
- Create a new network interface in **Interfaces**
  - Click **New** and select **Ethernet** and **Internet**, click **OK**
  - Select the previously created **a314eth** and click **OK**
  - Set **IP** to **static** and type in `192.168.2.2`
  - Set **Netmask** to **static** and type in `255.255.255.0`
  - Set Gateway to **static** and type in `192.168.2.1`
  - Click **OK**
- Add a nameserver in **Database**
  - Select **DNS servers** from the spinner
  - Click **Add** and type in the address of your DNS server and press enter (you can use for example Google's DNS server at `8.8.8.8` if unsure)
- Remember to save your settings!

Click **Online** and hopefully your Amiga now has internet access.

Autostarting Miami and further configuration is left to the user.

## Networking Tips and Tricks

### Accessing services on your Amiga from other computers on the network

The above described configuration uses a method called masquerading to create a separate network for your Amiga behind the Pi. While this has the advantage of shielding it from attacks from other computers, sometimes you may want to be able to access network services on your Amiga from other computers besides the Pi.

To facilitate that access, the firewall on the Pi needs to be configured to forward incoming traffic on specific ports on the external interface to the Amiga. The example below assumes that the Pi and Amiga have both been setup as described earlier in this document.

```bash
iptables -A PREROUTING -t nat -i wlan0 -p tcp --dport 21 -j DNAT --to 192.168.2.2:21
iptables -A FORWARD -p tcp -d 192.168.2.2 --dport 21 -j ACCEPT
```

By adding these lines to `/etc/rc.local` directly following the other /iptables/ lines, the Pi firewall will start accepting connections to port 21 (FTP) and forwarding them to port 21 on the Amiga allowing an FTP client to connect to an FTP server running on the Amiga.


text:
device=a314eth.device
address=192.168.2.2
netmask=255.255.255.0


text: default 192.168.2.1


text: nameserver 8.8.8.8


text:
iptables -t nat -A POSTROUTING -o wlan0 -j MASQUERADE
iptables -A FORWARD -i wlan0 -o tap0 -m state --state RELATED,ESTABLISHED -j ACCEPT
iptables -A FORWARD -i tap0 -o wlan0 -j ACCEPT


text:
auto tap0

iface tap0 inet static
	address 192.168.2.1
	netmask 255.255.255.0
	broadcast 192.168.2.255
	pre-up ip tuntap add tap0 mode tap user ##USER## group ##GROUP##
	up ip link set dev tap0 up
	post-down ip link del dev tap0


text: vc hid.c -lamiga -o PiHid


text:
/*
 * Copyright (c) 2022 Niklas Ekström
 */

#include <exec/types.h>
#include <exec/ports.h>
#include <exec/tasks.h>
#include <exec/nodes.h>
#include <exec/lists.h>
#include <exec/execbase.h>
#include <exec/memory.h>
#include <exec/interrupts.h>

#include <devices/input.h>
#include <devices/inputevent.h>
#include <libraries/dos.h>

#include <proto/alib.h>
#include <proto/exec.h>

#include <stdio.h>
#include <string.h>
#include <time.h>

#include "../a314device/a314.h"

#define SERVICE_NAME "hid"

static struct MsgPort *mp;
static ULONG socket;

static struct A314_IORequest *cmsg;
static struct A314_IORequest *rmsg;
static struct A314_IORequest *wmsg;

static UBYTE arbuf[256];
static UBYTE awbuf[256];

static BOOL pending_a314_read = FALSE;
static BOOL pending_a314_write = FALSE;
static BOOL pending_a314_reset = FALSE;

static BOOL stream_closed = FALSE;

static struct MsgPort *id_mp;
static struct IOStdReq *id_req;
static struct InputEvent generated_event;
static struct Interrupt input_event_handler;
static UWORD last_qualifier;

#pragma pack(push, 1)
struct EventMessage
{
	WORD dx;
	WORD dy;
	UWORD qualifier;
	UBYTE code;
};
#pragma pack(pop)

static void start_a314_cmd(struct A314_IORequest *msg, UWORD command, char *buffer, int length)
{
	msg->a314_Request.io_Command = command;
	msg->a314_Request.io_Error = 0;

	msg->a314_Socket = socket;
	msg->a314_Buffer = buffer;
	msg->a314_Length = length;

	SendIO((struct IORequest *)msg);
}

static LONG a314_connect(char *name)
{
	socket = time(NULL);
	start_a314_cmd(cmsg, A314_CONNECT, name, strlen(name));
	return WaitIO((struct IORequest *)cmsg);
}

static void start_a314_read()
{
	start_a314_cmd(rmsg, A314_READ, arbuf, 255);
	pending_a314_read = TRUE;
}

static void start_a314_write(int length)
{
	start_a314_cmd(wmsg, A314_WRITE, awbuf, length);
	pending_a314_write = TRUE;
}

static LONG sync_a314_write(int length)
{
	start_a314_write(length);
	pending_a314_write = FALSE;
	return WaitIO((struct IORequest *)wmsg);
}

static void start_a314_reset()
{
	start_a314_cmd(cmsg, A314_RESET, NULL, 0);
	pending_a314_reset = TRUE;
}

static LONG sync_a314_reset()
{
	start_a314_reset();
	pending_a314_reset = FALSE;
	return WaitIO((struct IORequest *)cmsg);
}

static struct InputEvent *event_handler_func(__reg("a0") struct InputEvent *ie)
{
	if (ie->ie_Class == IECLASS_RAWKEY || ie->ie_Class == IECLASS_RAWMOUSE)
		last_qualifier = ie->ie_Qualifier;
	return ie;
}

static void add_event_handler()
{
	input_event_handler.is_Code = (void *)event_handler_func;
	input_event_handler.is_Data = NULL;
	input_event_handler.is_Node.ln_Pri = 70;
	input_event_handler.is_Node.ln_Name = SERVICE_NAME;

	id_req->io_Data = (void *)&input_event_handler;
	id_req->io_Command = IND_ADDHANDLER;
	DoIO((struct IORequest *)id_req);
}

static void rem_event_handler()
{
	id_req->io_Data = (void *)&input_event_handler;
	id_req->io_Command = IND_REMHANDLER;
	DoIO((struct IORequest *)id_req);
}

static void write_input_event()
{
	struct EventMessage *em = (struct EventMessage *)arbuf;

	UWORD qualifier = em->qualifier;
	if (qualifier & IEQUALIFIER_INTERRUPT)
	{
		generated_event.ie_Class = IECLASS_RAWKEY;
		qualifier = (qualifier & 0x7000) | (last_qualifier & 0x00ff);
	}
	else if (qualifier & IEQUALIFIER_RELATIVEMOUSE)
	{
		generated_event.ie_Class = IECLASS_RAWMOUSE;
		qualifier |= last_qualifier & 0x00ff; // Last keyboard qualifiers.
	}
	else
	{
		generated_event.ie_Class = IECLASS_RAWKEY;
		qualifier |= last_qualifier & 0x7000; // Last mouse qualifiers.
	}
	generated_event.ie_Code = em->code;
	generated_event.ie_Qualifier = qualifier;
	generated_event.ie_X = em->dx;
	generated_event.ie_Y = em->dy;

	// Other fields in generated_event are zero.
	// TODO: Would it be preferable to set ie_TimeStamp?

	id_req->io_Data = (void *)&generated_event;
	id_req->io_Command = IND_WRITEEVENT;
	id_req->io_Length = sizeof(struct InputEvent);
	DoIO((struct IORequest *)id_req);
}

static void handle_a314_read_completed()
{
	pending_a314_read = FALSE;

	if (stream_closed)
		return;

	int res = rmsg->a314_Request.io_Error;
	if (res == A314_READ_OK)
	{
		write_input_event();
		start_a314_read();
	}
	else if (res == A314_READ_EOS)
	{
		start_a314_reset();
		stream_closed = TRUE;
	}
	else if (res == A314_READ_RESET)
		stream_closed = TRUE;
}

int main()
{
	LONG old_priority = SetTaskPri(FindTask(NULL), 60);

	mp = CreatePort(NULL, 0);
	cmsg = (struct A314_IORequest *)CreateExtIO(mp, sizeof(struct A314_IORequest));

	if (OpenDevice(A314_NAME, 0, (struct IORequest *)cmsg, 0) != 0)
	{
		printf("Unable to open a314.device\n");
		goto fail_out1;
	}

	wmsg = (struct A314_IORequest *)CreateExtIO(mp, sizeof(struct A314_IORequest));
	rmsg = (struct A314_IORequest *)CreateExtIO(mp, sizeof(struct A314_IORequest));
	memcpy(wmsg, cmsg, sizeof(struct A314_IORequest));
	memcpy(rmsg, cmsg, sizeof(struct A314_IORequest));

	if (a314_connect(SERVICE_NAME) != A314_CONNECT_OK)
	{
		printf("Unable to connect to " SERVICE_NAME " service\n");
		goto fail_out2;
	}

	id_mp = CreatePort(NULL, 0);
	id_req = (struct IOStdReq *)CreateExtIO(id_mp, sizeof(struct IOStdReq));
	OpenDevice("input.device", 0, (struct IORequest *)id_req, 0);

	add_event_handler();

	start_a314_read();

	ULONG portsig = 1 << mp->mp_SigBit;

	printf("Press ctrl-c to exit...\n");

	while (TRUE)
	{
		ULONG signal = Wait(portsig | SIGBREAKF_CTRL_C);

		if (signal & portsig)
		{
			struct Message *msg;
			while (msg = GetMsg(mp))
			{
				if (msg == (struct Message *)rmsg)
					handle_a314_read_completed();
				else if (msg == (struct Message *)wmsg)
					pending_a314_write = FALSE;
				else if (msg == (struct Message *)cmsg)
					pending_a314_reset = FALSE;
			}
		}

		if (signal & SIGBREAKF_CTRL_C)
		{
			start_a314_reset();
			stream_closed = TRUE;
		}

		if (stream_closed && !pending_a314_read && !pending_a314_write && !pending_a314_reset)
			break;
	}

	rem_event_handler();

	CloseDevice((struct IORequest *)id_req);
	DeleteExtIO((struct IORequest *)id_req);
	DeletePort(id_mp);
fail_out2:
	CloseDevice((struct IORequest *)cmsg);
	DeleteExtIO((struct IORequest *)rmsg);
	DeleteExtIO((struct IORequest *)wmsg);
fail_out1:
	DeleteExtIO((struct IORequest *)cmsg);
	DeletePort(mp);
	SetTaskPri(FindTask(NULL), old_priority);
	return 0;
}


text:
#!/usr/bin/python3
# -*- coding: utf-8 -*-

# Copyright (c) 2022 Niklas Ekström

import logging
logging.basicConfig(format='%(levelname)s, %(asctime)s, %(name)s, line %(lineno)d: %(message)s')

import errno
import fcntl
import os
import platform
import pyudev
import select
import struct
import time
from typing import Optional, Tuple, Dict

from a314d import A314d

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

SERVICE_NAME = 'hid'
MIN_SEND_INTERVAL = 0.02 # 20 ms.

EVIOCGRAB = 0x40044590

# Input event codes take from:
# https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h

EV_SYN = 0x00
EV_KEY = 0x01
EV_REL = 0x02
EV_ABS = 0x03
EV_MSC = 0x04

REL_X = 0x00
REL_Y = 0x01
REL_WHEEL_HI_RES = 0x0b

MSC_SERIAL = 0x00
MSC_PULSELED = 0x01
MSC_GESTURE = 0x02
MSC_RAW = 0x03
MSC_SCAN = 0x04

BTN_MOUSE = 0x110
BTN_LEFT = 0x110
BTN_RIGHT = 0x111
BTN_MIDDLE = 0x112
BTN_SIDE = 0x113
BTN_EXTRA = 0x114
BTN_FORWARD = 0x115
BTN_BACK = 0x116
BTN_TASK = 0x117

KEY_RESERVED = 0
KEY_ESC = 1
KEY_1 = 2
KEY_2 = 3
KEY_3 = 4
KEY_4 = 5
KEY_5 = 6
KEY_6 = 7
KEY_7 = 8
KEY_8 = 9
KEY_9 = 10
KEY_0 = 11
KEY_MINUS = 12
KEY_EQUAL = 13
KEY_BACKSPACE = 14
KEY_TAB = 15
KEY_Q = 16
KEY_W = 17
KEY_E = 18
KEY_R = 19
KEY_T = 20
KEY_Y = 21
KEY_U = 22
KEY_I = 23
KEY_O = 24
KEY_P = 25
KEY_LEFTBRACE = 26
KEY_RIGHTBRACE = 27
KEY_ENTER = 28
KEY_LEFTCTRL = 29
KEY_A = 30
KEY_S = 31
KEY_D = 32
KEY_F = 33
KEY_G = 34
KEY_H = 35
KEY_J = 36
KEY_K = 37
KEY_L = 38
KEY_SEMICOLON = 39
KEY_APOSTROPHE = 40
KEY_GRAVE = 41
KEY_LEFTSHIFT = 42
KEY_BACKSLASH = 43
KEY_Z = 44
KEY_X = 45
KEY_C = 46
KEY_V = 47
KEY_B = 48
KEY_N = 49
KEY_M = 50
KEY_COMMA = 51
KEY_DOT = 52
KEY_SLASH = 53
KEY_RIGHTSHIFT = 54
KEY_KPASTERISK = 55
KEY_LEFTALT = 56
KEY_SPACE = 57
KEY_CAPSLOCK = 58
KEY_F1 = 59
KEY_F2 = 60
KEY_F3 = 61
KEY_F4 = 62
KEY_F5 = 63
KEY_F6 = 64
KEY_F7 = 65
KEY_F8 = 66
KEY_F9 = 67
KEY_F10 = 68
KEY_NUMLOCK = 69
KEY_SCROLLLOCK = 70
KEY_KP7 = 71
KEY_KP8 = 72
KEY_KP9 = 73
KEY_KPMINUS = 74
KEY_KP4 = 75
KEY_KP5 = 76
KEY_KP6 = 77
KEY_KPPLUS = 78
KEY_KP1 = 79
KEY_KP2 = 80
KEY_KP3 = 81
KEY_KP0 = 82
KEY_KPDOT = 83
KEY_102ND = 86
KEY_F11 = 87
KEY_F12 = 88
KEY_RO = 89
KEY_KATAKANA = 90
KEY_HIRAGANA = 91
KEY_HENKAN = 92
KEY_MUHENKAN = 94
KEY_KPJPCOMMA = 95
KEY_KPENTER = 96
KEY_RIGHTCTRL = 97
KEY_KPSLASH = 98
KEY_SYSRQ = 99
KEY_RIGHTALT = 100
KEY_LINEFEED = 101
KEY_HOME = 102
KEY_UP = 103
KEY_PAGEUP = 104
KEY_LEFT = 105
KEY_RIGHT = 106
KEY_END = 107
KEY_DOWN = 108
KEY_PAGEDOWN = 109
KEY_INSERT = 110
KEY_DELETE = 111
KEY_MACRO = 112
KEY_MUTE = 113
KEY_VOLUMEDOWN = 114
KEY_VOLUMEUP = 115
KEY_POWER = 116
KEY_KPEQUAL = 117
KEY_KPPLUSMINUS = 118
KEY_PAUSE = 119
KEY_SCALE = 120
KEY_KPCOMMA = 121
KEY_HANGEUL = 122
KEY_HANGUEL = KEY_HANGEUL
KEY_HANJA = 123
KEY_YEN = 124
KEY_LEFTMETA = 125
KEY_RIGHTMETA = 126
KEY_COMPOSE = 127

# Amiga input event definitions taken from <devices/inputevent.h>
IEQUALIFIER_LSHIFT = 0x0001
IEQUALIFIER_RSHIFT = 0x0002
IEQUALIFIER_CAPSLOCK = 0x0004
IEQUALIFIER_CONTROL = 0x0008
IEQUALIFIER_LALT = 0x0010
IEQUALIFIER_RALT = 0x0020
IEQUALIFIER_LCOMMAND = 0x0040
IEQUALIFIER_RCOMMAND = 0x0080
IEQUALIFIER_NUMERICPAD = 0x0100
IEQUALIFIER_REPEAT = 0x0200
IEQUALIFIER_INTERRUPT = 0x0400
IEQUALIFIER_MULTIBROADCAST = 0x0800
IEQUALIFIER_MIDBUTTON = 0x1000
IEQUALIFIER_RBUTTON = 0x2000
IEQUALIFIER_LEFTBUTTON = 0x4000
IEQUALIFIER_RELATIVEMOUSE = 0x8000

IECODE_UP_PREFIX = 0x80
IECODE_LBUTTON = 0x68
IECODE_RBUTTON = 0x69
IECODE_MBUTTON = 0x6A
IECODE_NOBUTTON = 0xFF

NM_WHEEL_UP = 0x7a
NM_WHEEL_DOWN = 0x7b

MOUSE_CODE_QUAL = {
    BTN_LEFT: (IECODE_LBUTTON, IEQUALIFIER_LEFTBUTTON),
    BTN_RIGHT: (IECODE_RBUTTON, IEQUALIFIER_RBUTTON),
    BTN_MIDDLE: (IECODE_MBUTTON, IEQUALIFIER_MIDBUTTON)
}

RK_GRAVE = 0x0
RK_1 = 0x1
RK_2 = 0x2
RK_3 = 0x3
RK_4 = 0x4
RK_5 = 0x5
RK_6 = 0x6
RK_7 = 0x7
RK_8 = 0x8
RK_9 = 0x9
RK_0 = 0xa
RK_MINUS = 0xb
RK_EQUAL = 0xc
RK_BACKSLASH = 0xd
RK_BACKSPACE = 0x41
RK_TAB = 0x42
RK_Q = 0x10
RK_W = 0x11
RK_E = 0x12
RK_R = 0x13
RK_T = 0x14
RK_Y = 0x15
RK_U = 0x16
RK_I = 0x17
RK_O = 0x18
RK_P = 0x19
RK_LEFTBRACE = 0x1a
RK_RIGHTBRACE = 0x1b
RK_ENTER = 0x44
RK_CTRL = 0x63
RK_CAPSLOCK = 0x62
RK_A = 0x20
RK_S = 0x21
RK_D = 0x22
RK_F = 0x23
RK_G = 0x24
RK_H = 0x25
RK_J = 0x26
RK_K = 0x27
RK_L = 0x28
RK_SEMICOLON = 0x29
RK_APOSTROPHE = 0x2a
RK_INTERNATIONAL1 = 0x2b
RK_LEFTSHIFT = 0x60
RK_LESSTHAN = 0x30
RK_Z = 0x31
RK_X = 0x32
RK_C = 0x33
RK_V = 0x34
RK_B = 0x35
RK_N = 0x36
RK_M = 0x37
RK_COMMA = 0x38
RK_DOT = 0x39
RK_SLASH = 0x3a
RK_RIGHTSHIFT = 0x61
RK_LEFTALT = 0x64
RK_LEFTMETA = 0x66
RK_SPACE = 0x40
RK_RIGHTMETA = 0x67
RK_RIGHTALT = 0x64
RK_ESC = 0x45
RK_F1 = 0x50
RK_F2 = 0x51
RK_F3 = 0x52
RK_F4 = 0x53
RK_F5 = 0x54
RK_F6 = 0x55
RK_F7 = 0x56
RK_F8 = 0x57
RK_F9 = 0x58
RK_F10 = 0x59
RK_UP = 0x4c
RK_DOWN = 0x4d
RK_RIGHT = 0x4e
RK_LEFT = 0x4f
RK_DELETE = 0x46
RK_HELP = 0x5f
RK_KPLEFTPAREN = 0x5a
RK_KPRIGHTPAREN = 0x5b
RK_KPSLASH = 0x5c
RK_KPASTERISK = 0x5d
RK_KP7 = 0x3d
RK_KP8 = 0x3e
RK_KP9 = 0x3f
RK_KPMINUS = 0x4a
RK_KP4 = 0x2d
RK_KP5 = 0x2e
RK_KP6 = 0x2f
RK_KPPLUS = 0x5e
RK_KP1 = 0x1d
RK_KP2 = 0x1e
RK_KP3 = 0x1f
RK_KP0 = 0xf
RK_KPDOT = 0x3c
RK_KPENTER = 0x43

CODE_MAP = {KEY_GRAVE: RK_GRAVE, KEY_1: RK_1, KEY_2: RK_2, KEY_3: RK_3, KEY_4: RK_4, KEY_5: RK_5, KEY_6: RK_6, KEY_7: RK_7, KEY_8: RK_8, KEY_9: RK_9, KEY_0: RK_0, KEY_MINUS: RK_MINUS, KEY_EQUAL: RK_EQUAL, KEY_INSERT: RK_BACKSLASH, KEY_BACKSPACE: RK_BACKSPACE, KEY_TAB: RK_TAB, KEY_Q: RK_Q, KEY_W: RK_W, KEY_E: RK_E, KEY_R: RK_R, KEY_T: RK_T, KEY_Y: RK_Y, KEY_U: RK_U, KEY_I: RK_I, KEY_O: RK_O, KEY_P: RK_P, KEY_LEFTBRACE: RK_LEFTBRACE, KEY_RIGHTBRACE: RK_RIGHTBRACE, KEY_ENTER: RK_ENTER, KEY_LEFTCTRL: RK_CTRL, KEY_RIGHTCTRL: RK_CTRL, KEY_CAPSLOCK: RK_CAPSLOCK, KEY_A: RK_A, KEY_S: RK_S, KEY_D: RK_D, KEY_F: RK_F, KEY_G: RK_G, KEY_H: RK_H, KEY_J: RK_J, KEY_K: RK_K, KEY_L: RK_L, KEY_SEMICOLON: RK_SEMICOLON, KEY_APOSTROPHE: RK_APOSTROPHE, KEY_BACKSLASH: RK_INTERNATIONAL1, KEY_LEFTSHIFT: RK_LEFTSHIFT, KEY_102ND: RK_LESSTHAN, KEY_Z: RK_Z, KEY_X: RK_X, KEY_C: RK_C, KEY_V: RK_V, KEY_B: RK_B, KEY_N: RK_N, KEY_M: RK_M, KEY_COMMA: RK_COMMA, KEY_DOT: RK_DOT, KEY_SLASH: RK_SLASH, KEY_RIGHTSHIFT: RK_RIGHTSHIFT, KEY_LEFTALT: RK_LEFTALT, KEY_LEFTMETA: RK_LEFTMETA, KEY_SPACE: RK_SPACE, KEY_RIGHTMETA: RK_RIGHTMETA, KEY_RIGHTALT: RK_RIGHTALT, KEY_ESC: RK_ESC, KEY_F1: RK_F1, KEY_F2: RK_F2, KEY_F3: RK_F3, KEY_F4: RK_F4, KEY_F5: RK_F5, KEY_F6: RK_F6, KEY_F7: RK_F7, KEY_F8: RK_F8, KEY_F9: RK_F9, KEY_F10: RK_F10, KEY_UP: RK_UP, KEY_DOWN: RK_DOWN, KEY_RIGHT: RK_RIGHT, KEY_LEFT: RK_LEFT, KEY_DELETE: RK_DELETE, KEY_HOME: RK_HELP, KEY_NUMLOCK: RK_KPLEFTPAREN, KEY_SCROLLLOCK: RK_KPRIGHTPAREN, KEY_KPSLASH: RK_KPSLASH, KEY_KPASTERISK: RK_KPASTERISK, KEY_KP7: RK_KP7, KEY_KP8: RK_KP8, KEY_KP9: RK_KP9, KEY_KPMINUS: RK_KPMINUS, KEY_KP4: RK_KP4, KEY_KP5: RK_KP5, KEY_KP6: RK_KP6, KEY_KPPLUS: RK_KPPLUS, KEY_KP1: RK_KP1, KEY_KP2: RK_KP2, KEY_KP3: RK_KP3, KEY_KP0: RK_KP0, KEY_KPDOT: RK_KPDOT, KEY_KPENTER: RK_KPENTER}

SHIFT_KEY_QUALS = {
    RK_LEFTSHIFT: IEQUALIFIER_LSHIFT,
    RK_RIGHTSHIFT: IEQUALIFIER_RSHIFT,
    RK_CAPSLOCK: IEQUALIFIER_CAPSLOCK,
    RK_CTRL: IEQUALIFIER_CONTROL,
    RK_LEFTALT: IEQUALIFIER_LALT,
    RK_RIGHTALT: IEQUALIFIER_RALT,
    RK_LEFTMETA: IEQUALIFIER_LCOMMAND,
    RK_RIGHTMETA: IEQUALIFIER_RCOMMAND
}

NUMPAD_KEYS = set([
    RK_KPLEFTPAREN, RK_KPRIGHTPAREN, RK_KPSLASH, RK_KPASTERISK,
    RK_KP7, RK_KP8, RK_KP9, RK_KPMINUS,
    RK_KP4, RK_KP5, RK_KP6, RK_KPPLUS,
    RK_KP1, RK_KP2, RK_KP3,
    RK_KP0, RK_KPDOT, RK_KPENTER
])

class Device(object):
    def __init__(self, name: str, type: str, fd: int):
        self.name = name
        self.type = type
        self.fd = fd

        self.qualifier: int = IEQUALIFIER_RELATIVEMOUSE if type == 'mouse' else 0
        self.buffered_movement: Optional[Tuple[int, int]] = None

    def fileno(self):
        return self.fd

class HidService(object):
    def __init__(self):
        self.a314d = A314d(SERVICE_NAME)

        is_64bit = '64' in platform.architecture()[0]
        self.event_length = 24 if is_64bit else 16

        self.devices: Dict[str, Device] = {}

        self.context = pyudev.Context()

        for device in self.context.list_devices(subsystem='input'):
            device_type = None

            for dl in device.device_links:
                if dl.endswith('event-mouse'):
                    device_type = 'mouse'
                    break
                elif dl.endswith('event-kbd'):
                    device_type = 'keyboard'
                    break

            if device_type:
                self.open_device(device, device_type)

        self.monitor = pyudev.Monitor.from_netlink(self.context)
        self.monitor.filter_by('input')
        self.monitor.start()

        self.rbuf = b''
        self.current_stream_id = None
        self.next_send = time.time()

    def open_device(self, device, device_type: str):
        device_name = device.get('DEVNAME')
        fd = os.open(device_name, os.O_RDONLY | os.O_NONBLOCK)
        if fd < 0:
            logger.error('Unable to open %s device %s', device_type, device_name)
        else:
            try:
                fcntl.ioctl(fd, EVIOCGRAB, 1)
                logger.info('Opened %s device %s', device_type, device.get('ID_SERIAL'))
                self.devices[device_name] = Device(device_name, device_type, fd)
            except:
                logger.warning('Unable to grab %s device %s', device_type, device_name)
                os.close(fd)

    def process_a314d_msg(self, stream_id, ptype, payload):
        if ptype == self.a314d.MSG_CONNECT:
            if payload == SERVICE_NAME.encode() and self.current_stream_id is None:
                logger.info('Amiga connected to HID service')
                self.current_stream_id = stream_id
                self.a314d.send_connect_response(stream_id, 0)
            else:
                self.a314d.send_connect_response(stream_id, 3)
        elif self.current_stream_id == stream_id:
            if ptype == self.a314d.MSG_DATA:
                # Shouldn't receive messages from Amiga.
                pass
            elif ptype == self.a314d.MSG_EOS:
                # Shouldn't receive messages from Amiga.
                pass
            elif ptype == self.a314d.MSG_RESET:
                self.current_stream_id = None
                for dev in self.devices.values():
                    dev.qualifier &= IEQUALIFIER_RELATIVEMOUSE
                    dev.buffered_movement = None
                logger.info('Amiga disconnected from HID service')

    def handle_a314d_readable(self):
        buf = self.a314d.drv.recv(1024)
        if not buf:
            logger.error('Connection to a314d was closed, shutting down')
            exit(-1)

        self.rbuf += buf
        while True:
            if len(self.rbuf) < 9:
                break

            (plen, stream_id, ptype) = struct.unpack('=IIB', self.rbuf[:9])
            if len(self.rbuf) < 9 + plen:
                break

            self.rbuf = self.rbuf[9:]
            payload = self.rbuf[:plen]
            self.rbuf = self.rbuf[plen:]

            self.process_a314d_msg(stream_id, ptype, payload)

    def handle_udev_event(self):
        device = self.monitor.poll()
        if device is None:
            return

        device_type = None

        for dl in device.device_links:
            if dl.endswith('event-mouse'):
                device_type = 'mouse'
                break
            elif dl.endswith('event-kbd'):
                device_type = 'keyboard'
                break

        action = device.action
        if not device_type or action not in ('add', 'remove'):
            return

        device_name: str = device.get('DEVNAME')

        if action == 'add':
            if device_name not in self.devices:
                self.open_device(device, device_type)
        elif action == 'remove':
            if device_name in self.devices:
                dev = self.devices[device_name]
                os.close(dev.fd)
                del self.devices[device_name]
                logger.info('Removed %s device %s', dev.type, dev.name)

    def handle_dev_readable(self, dev: Device):
        try:
            data = os.read(dev.fd, self.event_length)
        except OSError as e:
            if e.errno != errno.ENODEV:
                raise

            os.close(dev.fd)
            logger.info('Removed %s device %s', dev.type, dev.name)
            if dev.name in self.devices:
                del self.devices[dev.name]
            return

        if not self.current_stream_id or len(data) < self.event_length:
            return

        typ, code, value = struct.unpack_from('<HHi', data, self.event_length - 8)
        logger.debug(f'Event: type={typ}, code={code:2}, value={value}')

        do_send = False
        iecode = IECODE_NOBUTTON
        extra_qualifier = 0

        if typ == EV_KEY:
            if dev.type == 'mouse':
                if code in MOUSE_CODE_QUAL:
                    iec, qual_mask = MOUSE_CODE_QUAL[code]
                    if value == 0:
                        dev.qualifier &= ~qual_mask
                        iecode = iec | IECODE_UP_PREFIX
                    elif value == 1:
                        dev.qualifier |= qual_mask
                        iecode = iec
                    do_send = True
            elif dev.type == 'keyboard':
                if code in CODE_MAP and value >= 0 and value <= 2:
                    iecode = CODE_MAP[code]

                    if iecode in SHIFT_KEY_QUALS:
                        qual_mask = SHIFT_KEY_QUALS[iecode]
                        if iecode == RK_CAPSLOCK:
                            if value == 1:
                                dev.qualifier ^= qual_mask
                        else:
                            if value == 0:
                                dev.qualifier &= ~qual_mask
                            elif value == 1:
                                dev.qualifier |= qual_mask
                        if value != 2:
                            do_send = True
                    else:
                        do_send = True

                    if iecode in NUMPAD_KEYS:
                        extra_qualifier |= IEQUALIFIER_NUMERICPAD

                    if value == 0:
                        iecode |= IECODE_UP_PREFIX
                    elif value == 2:
                        extra_qualifier |= IEQUALIFIER_REPEAT
        elif typ == EV_REL:
            if code == REL_WHEEL_HI_RES:
                # Bewarned: The IEQUALIFIER_INTERRUPT qualifier bit
                # is repurposed to signal a scroll wheel event.
                # The Amiga side will fix up the qualifier bits.
                iecode = NM_WHEEL_UP if value > 0 else NM_WHEEL_DOWN
                self.a314d.send_data(self.current_stream_id, struct.pack('>hhHB', 0, 0, dev.qualifier | IEQUALIFIER_INTERRUPT, iecode))
                return

            dx: int = 0
            dy: int = 0
            if code == REL_X:
                dx = value
            elif code == REL_Y:
                dy = value

            if dev.buffered_movement is None:
                dev.buffered_movement = (dx, dy)
            else:
                bdx, bdy = dev.buffered_movement
                dev.buffered_movement = (bdx + dx, bdy + dy)

        now = time.time()

        if do_send or dev.buffered_movement and self.next_send <= now:
            if dev.buffered_movement:
                dx, dy = dev.buffered_movement
            else:
                dx, dy = 0, 0
            self.a314d.send_data(self.current_stream_id, struct.pack('>hhHB', dx, dy, dev.qualifier | extra_qualifier, iecode))
            dev.buffered_movement = None
            self.next_send = now + MIN_SEND_INTERVAL

    def run(self):
        logger.info('HID service is running')

        while True:
            sel_fds = [self.a314d, self.monitor] + list(self.devices.values())

            timeout = 5.0

            any_buffered_movement = any(dev.buffered_movement for dev in self.devices.values())
            if any_buffered_movement and self.current_stream_id:
                now = time.time()
                if now < self.next_send:
                    timeout = self.next_send - now
                else:
                    for dev in self.devices.values():
                        if dev.buffered_movement:
                            self.a314d.send_data(self.current_stream_id, struct.pack('>hhHB', *dev.buffered_movement, dev.qualifier, IECODE_NOBUTTON))
                            dev.buffered_movement = None
                    self.next_send = now + MIN_SEND_INTERVAL

            rfd, _, _ = select.select(sel_fds, [], [], timeout)

            for fd in rfd:
                if fd == self.a314d:
                    self.handle_a314d_readable()
                elif fd == self.monitor:
                    self.handle_udev_event()
                else:
                    self.handle_dev_readable(fd)

if __name__ == '__main__':
    service = HidService()
    service.run()


text: vc remotewb.c vblank_server.asm -lamiga -o RemoteWB


text:
	XDEF	_VBlankServer
	CODE

;struct VBlankData
;{
;	struct Task *task;
;	ULONG signal;
;};

	; a1 points to VBlankData structure
_VBlankServer:
	move.l	$4.w,a6
	move.l	4(a1),d0	; signal
	move.l	0(a1),a1	; task
	jsr	-324(a6)
	moveq	#0,d0
	rts