#!/usr/bin/env python3
import os
from fcntl import ioctl
from time import sleep
import struct
from collections import namedtuple
import cffi
import datetime
import sys

ffi = cffi.FFI()

# Per cbd, "SS310 modem", "shannon310". This is a Samsung Galaxy S7 SM-G930F.
# ["/sbin/cbd", "-d", "-tss310", "-bm", "-mm", "-P", "platform/155a0000.ufs/by-name/RADIO"]

# Aha! If we stay as root, then the security_request(2, 0, 0) fails
# with error code 11. cbd does setuid(1001) (while retaining
# capabilities!) before doing security_request(2, 0, 0), and in that
# case it succeeds with error code 0. HOWEVER it seems like you can
# ask cbd to stay as root with `-or`, in which case it gets the error
# code 11 too - which it ignores! The modem seems (?) well-configured
# afterward, so perhaps it's not required?

def mkstruct(name, fields, spec):
    cls = namedtuple(name, fields)
    def iter_load(bs):
        return map(cls._make, struct.iter_unpack(spec, bs))
    cls.iter_load = iter_load
    def load(bs):
        return cls._make(struct.unpack(spec, bs))
    cls.load = load
    def iter_save(items):
        return b''.join(struct.pack(spec, *item) for item in items)
    cls.iter_save = iter_save
    def save(item):
        return struct.pack(spec, *item)
    cls.save = save
    return cls

TOC = mkstruct('TOC', 'name offset loadaddr size crc entryid', '<12sIIIII')

# struct modem_firmware
FirmwareChunk = mkstruct('FirmwareChunk', 'binary size m_offset b_offset mode len', '<QIIIII')

# struct modem_sec_req
SecReq = mkstruct('SecReq', 'mode size_boot size_main', '<III4x')

def get_srinfo(fd):
    buf = bytearray(struct.pack('<I1020x', 1020)) # SHMEM_SRINFO_SIZE in link_device_shmem.c
    print('get_srinfo', ioctl(fd, 0x6f45, buf))
    return buf

# enum modem_state from modem_prj.h
enum_modem_state = [
    'STATE_OFFLINE',
    'STATE_CRASH_RESET',
    'STATE_CRASH_EXIT',
    'STATE_BOOTING',
    'STATE_ONLINE',
    'STATE_NV_REBUILDING',
    'STATE_LOADER_DONE',
    'STATE_SIM_ATTACH',
    'STATE_SIM_DETACH',
    'STATE_CRASH_WATCHDOG',
]

def modem_status(fd):
    return enum_modem_state[ioctl(fd, 0x6f27)]

def modem_on(fd):
    print('modem_on', ioctl(fd, 0x6f19))

def modem_boot_on(fd):
    print('modem_boot_on', ioctl(fd, 0x6f22))

def modem_boot_off(fd):
    print('modem_boot_off', ioctl(fd, 0x6f23))

def modem_dl_start(fd):
    print('modem_dl_start', ioctl(fd, 0x6f28))

def modem_reset(fd):
    print('modem_reset', ioctl(fd, 0x6f21))

def security_request(fd, req):
    print(req, ioctl(fd, 0x6f53, bytearray(req.save())))

def send_chunk(fd, total_size, load_offset, chunk, file_offset):
    p = ffi.from_buffer(chunk)
    mode = 0 ## cbd uses mode 0 for all three uploads
    mf = FirmwareChunk(int(ffi.cast('long long', p)), total_size, load_offset, file_offset, mode, len(chunk))
    print(mf, ioctl(fd, 0x6f40, bytearray(mf.save())))

def send_region(fd, fh, entry, base_loadaddr):
    load_offset = entry.loadaddr - base_loadaddr
    file_offset = entry.offset
    fh.seek(file_offset)
    total_size = entry.size
    remaining = total_size
    while remaining > 0:
        chunksize = min(remaining, 62 * 1024)
        chunk = fh.read(chunksize)
        send_chunk(fd, total_size, load_offset, chunk, file_offset)
        load_offset = load_offset + chunksize
        file_offset = file_offset + chunksize
        remaining = remaining - chunksize

print('poke_modem')

boot0_path = '/dev/umts_boot0'
firmware_partition = '/dev/disk/by-partlabel/RADIO'
nv_data_path = '/efs/nv_data.bin'

with open('/sys/power/wake_lock', 'wb') as f:
    f.write(b'ss310')

boot0_fd = os.open(boot0_path, os.O_RDWR)

with open(firmware_partition, 'rb') as f:
    toc = list(TOC.iter_load(f.read(512)))

(boot_toc_entry, main_toc_entry, nv_toc_entry) = toc[1:4]
print(toc[0])
print(boot_toc_entry)
print(main_toc_entry)
print(nv_toc_entry)

# get_srinfo(boot0_fd)
# modem_status(boot0_fd)
modem_reset(boot0_fd)
security_request(boot0_fd, SecReq(2, 0, 0))

with open(firmware_partition, 'rb') as f:
    send_region(boot0_fd, f, boot_toc_entry, boot_toc_entry.loadaddr)
    send_region(boot0_fd, f, main_toc_entry, boot_toc_entry.loadaddr)
with open(nv_data_path, 'rb') as f:
    send_region(boot0_fd, f, nv_toc_entry, boot_toc_entry.loadaddr)

security_request(boot0_fd, SecReq(0, boot_toc_entry.size, main_toc_entry.size))
modem_on(boot0_fd)
modem_boot_on(boot0_fd)
modem_dl_start(boot0_fd)

os.write(boot0_fd, bytearray(struct.pack('I', 0x0000900d)))
print(hex(struct.unpack('I', os.read(boot0_fd, 4))[0]))
os.write(boot0_fd, bytearray(struct.pack('I', 0x00009f00)))
print(hex(struct.unpack('I', os.read(boot0_fd, 4))[0]))

modem_boot_off(boot0_fd)

os.close(boot0_fd)

with open('/sys/power/wake_unlock', 'wb') as f:
    f.write(b'ss310')

sys.stdout.flush()
with open(boot0_path, 'rb') as f:
    while True:
        i = f.read(512)
        if len(i) > 0:
            print(str(datetime.datetime.now()), '({:03x})'.format(len(i)), modem_status(f.raw.fileno()), i.hex())
            sys.stdout.flush()
        else:
            sleep(1)
